c#socket发送邮件详解

摘要:
//Blog.csdn.net/gaooo/article/details/2027145电子邮件发送在web应用程序中很常见。在ASP时代,大多数人使用第三方提供的组件,如JMAIL和ASPMAIL来发送电子邮件。我们将使用TCPCLIENT类直接与SMTP服务器通信以发送邮件。发件人的名称>通过用户名验证后,服务器将要求输入密码。

本文来自http://blog.csdn.net/gaooo/article/details/2027145

邮件发送在web应用中是屡见不鲜的,在asp时代大家多是利用一些第三方提供的组件如JMAIL、ASPMAIL等进行邮件发送。自从微软推出 Asp.net后,很多程序员开始转向采用C#作为主要的开发语言。asp.net提供了更加强大的功能,同时也提供给了大家一个SMTP类作为邮件发送 之用。但是,随着垃圾邮件的广泛传播,很多邮件服务提供商纷纷增加了SMTP   的认证手续,也就是ESMTP,而微软提供的SMTP类居然不支持认证发送。当然现在网上也出现了一些解决方案,利用其他的一些手段来发出认证信息。但我 想,是不是还有更好的呢?为了解决这个问题,笔者两日茶饭不思,日以继夜,终于找到了一个方法:)。下面,我们将利用TCPCLIENT这个类直接与 SMTP服务器通讯进行邮件的发送。       
实际上原理也就是利用套接字(Socket)和服务器进行对话通讯,按照SMTP协议的规范,和服务器建立联系。我们以往用的一些邮件组件都是这么做的。      
在开始之前,我们要对SMTP协议及其扩展ESMTP有个初步的了解。   

SMTP和ESMTP的一些主要命令格式有以下一些:   
HELO   <信息发送端的名称>   例如:HELO   Localhost   
这相当于和服务器打个招呼,你好,我是某某   
EHLO   <信息发送端的名称>   例如:EHLO   Localhost     
  这是针对ESMTP服务器的接触方式,必须输入这个命令,系统才会开始认证程序       
  AUTH   LOGIN         
  输入这个命令,系统的认证程序将会启动,同时系统会返回一个经过Base64处理过的字符串,意思是“请输入用户名”。接着必须发送用户名给服务器,用户 名也必须经过Base64编码转换,服务器在通过用户名的认证之后会要求输入密码,此时输入经过Base64编码转换后的密码。成功后,即可运行下面的命 令了。         
  MAIL   FROM:<发件人地址>   例如:MAIL   FROM:   webmaster@sina.com   
  这是告诉服务器发件人的邮件地址       
  RCPT   TO:<收件人地址>   例如:RCPT   TO:   webmaster@sina.com   
    
  这是告诉服务器收件人的邮件地址   
    
  DATA     
    
  输入这个命令后,服务器正式开始接受数据   
    
  .   
    
  数据输入完成后,必须输入命令“.”,服务器就会停止数据的接受.   
    
  QUIT   退出系统   
    
  上面是一些基本命令的描述,如果大家还有什么不懂的地方,可以参考TCP/IP有关的书籍,也可以到这个网站看看RFC文档:http://210.25.132.18/rfc/index.html   
    
  现在我们正式开始,看看在C# 中如何来进行工作。   
    
  第一步:创建一个类,命名为MailSend,这个类继承System.Net.Sockets.TcpClient   
    
  using   System;   
  using   System.Net.Sockets;//用于处理网络连接    
  using   System.IO;   //用于处理附件的包   
  using   System.Text;//用于处理文本编码   
  using   System.Data;      
  using   System.Net;       
  public   class   MailSend:TcpClient       
  {         
  public   MailSend()       
  {   

    
  }   
    
  }   
    
  在这里我要讲讲TcpClient这个类,它的主要作用就是为TCP网络服务提供客户端的连接,大家可以看到,他来源于Sockets这个包,实际上是基于   Socket   类构建。不过他以更高的抽象程度提供   TCP   服务,操作起来也更简单。   
    
  第二步:建立一些基本的变量及连接方法   
    
  1、基本变量   
    
  private   String   server;//SMTP服务器域名    
  private   int   port;//端口     
  private   String   username;//用户名       
  private   String   password;//密码       
  private   String   subject;//主题       
  private   String   body;//文本内容     
  private   String   htmlbody;//超文本内容      
  private   String   from;//发件人地址       
  private   String   to;//收件人地址      
  private   String   fromname;//发件人姓名       
  private   String   toname;//收件人姓名      
  private   String   content_type;//邮件类型      
  private   String   encode;//邮件编码       
  private   String   charset;//语言编码       
  private   DataTable   filelist;//附件列表        
  private   int   priority;//邮件优先级       
  以上定义的都是邮件发送所需的一些基本信息,可以将上述变量做为属性来传递。       
  如:     
  public   String   SMTPServer      
  {     
    set{this.server=value;}      
  }   
    
  其余的也可如此.   
    
  2、向服务器写入命令的方法   
    
  变量strCmd为需要输入的命令或数据的字符串      
  变量charset为数据的字符语言编码,一般可以设置为GB2312      
  private   void   WriteStream(String   strCmd,String   charset)      
  {      
    Stream   TcpStream;//定义操作对象      
    strCmd   =   strCmd   +   "/r/n";   //加入换行符      
    TcpStream   =this.GetStream();//获取数据流       
    //将命令行转化为byte[]      
    byte[]   bWrite   =   Encoding.GetEncoding(charset).GetBytes(strCmd.ToCharArray());    
    //由于每次写入的数据大小是有限制的,那么我们将每次写入的数据长度定在75个字节,一旦命令长度超过了75,就分步写入。     
    int   start=0;      
    int   length=bWrite.Length;      
    int   page=0;      
    int   size=75;      
    int   count=size;      
    if   (length>75)     
    {    
      //数据分页      
      if   ((length/size)*size<length)   
      page=length/size+1;      
      else       
      page=length/size;   
      for   (int   i=0;i<page;i++)//根据页数循环写入   
      {   
        start=i*size;   
        if   (i==page-1)   
        count=length-(i*size);   
           TcpStream.Write(bWrite,start,count);//将数据写入到服务器上   
      }   
    }   
    else   
    TcpStream.Write(bWrite,0,bWrite.Length);   
    }     
  catch(Exception)   
  {}   
  }   
    
  本方法中,我们最后用到的也就最重要的就是TcpStream.Write()这句话,前面所做的只是将数据分页,可以分步写入。另外在写入数据时,必须 把字符串转化为byte[]类型。在这里我用的是Stream这个对象,同时你也可以使用NetworkStream这个对象来进行操作,实际效果是一致 的。在下面的返回信息获取中,我就用到了NetworkStream,实际上这也是帮助大家熟悉流操作对象的一个过程。   
    
  3、获取服务器的返回信息   
  private   string   ReceiveStream()    
  {   
  String   sp=null;   
  byte[]   by=new   byte[1024];   
  NetworkStream   ns   =   this.GetStream();//此处即可获取服务器的返回数据流   
  int   size=ns.Read(by,0,by.Length);//读取数据流   
  if   (size>0)   
  {   
    sp=Encoding.Default.GetString(by);//转化为String   
  }   
  return   sp;   
  }   
    
  除了输入DATA命令之后,其余的时间向服务器发送命令,服务器都会返回一些信息,并同时有一个状态码返回,告诉你操作是否成功完成了。一旦输入DATA 命令,也就是数据开始传递的这段时间中,服务器不会返回任何信息,直到输入“.”结束传递,服务器才会返回信息。   
    
  4、发出命令并判断返回信息是否正确,也就是看发出的命令服务器是否接受并通过了。   
  本方法实际上将上面的两个方法结合来用,一个写,一个收,然后进行判断,看是否正确。这样我们就能够监控每步操作是否正常进行了。   
  参数strCmd也就是需要输入的命令或者数据   
  参数state为返回的表明操作成功的状态码   
  private   bool   OperaStream(string   strCmd,string   state)   
  {  

    string   sp=null;   
    bool   success=false;   
    try   
    {   
      WriteStream(strCmd);//写入命令   
      sp   =   ReceiveStream();//接受返回信息   
      if   (sp.IndexOf(state)!=-1)//判断状态码是否正确   
      success=true;   
    }   
  catch(Exception   ex)   
  {  

    Console.Write(ex.ToString());}   
    return   success;   
  }   
    
  我们进行每一步操作时,都是通过状态码来确定是否成功的,那么如果操作成功,就会返回正确的状态码,根据这个原理,我们在这个方法中,同时输入命令和表明操作成功的状态码,通过获取的数据判断返回的是不是正确的状态码,以此来决定是否继续进行下一步操作。   
    
  在这里我要告诉大家一些基本的状态码表示的含义。   
  211   帮助返回系统状态   
  214   帮助信息   
  220   服务准备就绪   
  221   关闭连接   
  250   请求操作就绪   
  251   用户不在本地,转寄到<   P   a   t   h   >   
  354   开始邮件输入   
  421   服务不可用   
  450   操作未执行,邮箱忙   
  451   操作中止,本地错误   
  452   操作未执行,存储空间不足   
  500   命令不可识别或语法错   
  501   参数语法错   
  502   命令不支持   
  503   命令顺序错   
  504   命令参数不支持   
  550   操作未执行,邮箱不可用   
  551   非本地用户   
  552   中止,存储空间不足   
  553   操作未执行,邮箱名不正确   
  554   传输失败   
    
  写完以上的基本方法,我们可以开始和服务器进行连接了。由于现在的服务器有SMTP和ESMTP两种,不同的服务器连接的命令格式不一样,那么我们需要完成一个方法来取得服务器的连接。   
    
  public   bool   getMailServer()   
  {     
    try   
    {     
      //域名解析   
      System.Net.IPAddress   ipaddress=(IPAddress)System.Net.Dns.Resolve(this.server).AddressList.GetValue(0);   
      System.Net.IPEndPoint   endpoint=new   IPEndPoint(ipaddress,25);   
      Connect(endpoint);//连接Smtp服务器   
      ReceiveStream();//获取连接信息   
      if   (this.username!=null)   
      {     
        //开始进行服务器认证   
        //如果状态码是250则表示操作成功   
        if   (!OperaStream("EHLO   Localhost","250"))   
        {   
          this.Close();   
          return   false;   
        }   
      if   (!OperaStream("AUTH   LOGIN","334"))   
      {     
        this.Close();   
        return   false;   
      }   
      username=AuthStream(username);//此处将username转换为Base64码   
      if   (!OperaStream(this.username,"334"))   
      {   
        this.Close();   
        return   false;   
      }   
      password=AuthStream(password);//此处将password转换为Base64码   
      if   (!OperaStream(this.password,"235"))   
      {   
        this.Close();   
        return   false;   
      }   
      return   true;   
    }   
    else   
    {   //如果服务器不需要认证   
      if   (OperaStream("HELO   Localhost","250"))   
      {   
        return   true;   
      }   
      else   
      {     
        return   false;   
      }   
    }   
  }   
  catch(Exception   ex)   
  {   return   false;}   
  }   
    
  上面这个方法主要是用于和服务器取得联系,其中包含了针对两种不同服务器的连接方法,如果用户名不为空,那么我们首先进行ESMTP的连接,否则我

private   string   AuthStream(String   strCmd)   
  {     
    try   
    {   
      byte[]   by=Encoding.Default.GetBytes(strCmd.ToCharArray());   
      strCmd=Convert.ToBase64String(by);   
    }   
    catch(Exception   ex)   
    {

    return   ex.ToString();}   
      return   strCmd;   
    }   
  上面的方法将数据转化为Base64编码字符串,大家如果觉得太抽象了,可以这样试一试,在CMD模式输入telnet   smtp.sohu.com   25   然后回车,就可以连接sohu的SMTP服务器,sohu的SMTP服务器采用ESMTP协议,必须认证,大家可以试着操作一下。   
    
  第三步:关于邮件的附件传递   
  大家有发送邮件时,有时候会包含一些附件,那么本组件也考虑到了这一点。下面我们将会详细讲述如何对附件进行处理   
  filelist=new   DataTable();//已定义变量,初始化操作   
  filelist.Columns.Add(new   DataColumn("filename",typeof(string)));//文件名   
  filelist.Columns.Add(new   DataColumn("filecontent",typeof(string)));//文件内容   
  public   void   LoadAttFile(String   path)   
  {   
  //根据路径读出文件流   
  FileStream   fstr=new   FileStream(path,FileMode.Open);//建立文件流对象   
  byte[]   by=new   byte[Convert.ToInt32(fstr.Length)];   
  fstr.Read(by,0,by.Length);//读取文件内容   
  fstr.Close();//关闭   
  //格式转换   
  String   fileinfo=Convert.ToBase64String(by);//转化为base64编码   
  //增加到文件表中   
  DataRow   dr=filelist.NewRow();   
  dr[0]=Path.GetFileName(path);//获取文件名   
  dr[1]=fileinfo;//文件内容   
  filelist.Rows.Add(dr);//增加   
  }   
  通过这个方法将直接读取出文件的内容信息,然后存储在DataTable对象中,理论上可以读取无数个文件,当然,文件越大,发送时间也就越长。这个方法 只是针对本地的附件加入,如果大家有兴趣,可以自己利用HttpRequest做一个网上文件抓取的程序,直接抓取网上的文件,不过一般来说,这种方法很 少用得到。好了,闲话不谈,我们已经将文件读入,那么之后如何处理呢?请看下面的一个方法。   
    
  private   void   Attachment()   
  {   //对文件列表做循环   
     for   (int   i=0;i<filelist.Rows.Count;i++)   
     {   
       DataRow   dr=filelist.Rows;   
       WriteStream("--unique-boundary-1");//邮件内容分隔符   
       WriteStream("Content-Type:   application/octet-stream;name=/""+dr[0].ToString()+"/"");//文件格式   
       WriteStream("Content-Transfer-Encoding:   base64");//内容的编码   
      WriteStream("Content-Disposition:attachment;filename=/""+dr[0].ToString()+"/"");//文件名   
       WriteStream("");   
       String   fileinfo=dr[1].ToString();   
       WriteStream(fileinfo);//写入文件的内容   
       WriteStream("");   
    }   

  }   
    
  这个方法中我们就用到了WriteStream()方法,大家可能看的有些迷糊,好象无头无尾的,实际上这一段代码,将会在写完邮件的头部信息和文本内容 之后再写入到服务器上,在下面的程序中大家可以看见前面的部分。那么在代码的第七行,表示了文件的类型,我这里用了一个偷懒的方式,采用 application/octet-stream来代替所有的文件类型,实际上针对大部分的常用文件都有自己的一个格式,大家可以根据其文件名的扩展名 进行判断,这里我给出其他的一些格式。   

  扩展名   格式   
  ".gif"   --->"image/gif"     
  ".gz"   --->"application/x-gzip"     
  ".htm"   --->"text/html"     
  ".html"   --->"text/html"     
  ".jpg"   --->"image/jpeg"     
  ".tar"   --->"application/x-tar"     
  ".txt"   --->"text/plain"     
  ".zip"   --->"application/zip"     
  我比较偷懒,如果有需要的朋友,可以补上一些判断,获取文件的原本格式。   


  第四步:关于邮件的头信息   
  前面讲了这么多,就像是吃大餐之前的甜点,现在我们要进入最重要的部份--邮件的头信息,实际上,这个东西我们见得非常的多,大家在收发邮件的时候,查看邮件的属性就会看见一大串代码,里面有一些邮件地址,IP地址什么的,这就是邮件的头信息。   
    
  那么头信息的基本内容现在开讲:   
    
  FROM:<姓名><邮件地址>   格式:FROM:管理员<webmaster@sina.com>   
  TO:<姓名><邮件地址>   格式:TO:水生月<1234@sina.com>   
  SUBJECT:<标题>   格式:SUBJECT:今天的天气很不错!   
  DATE:<时间>   格式:DATE:   Thu,   29   Aug   2002   09:52:47   +0800   (CST)   
  REPLY-TO:<邮件地址>   格式:REPLY-TO:webmaster@sina.com   
  Content-Type:<邮件类型>   格式:Content-Type:   multipart/mixed;   boundary=unique-boundary-1   
  X-Priority:<邮件优先级>   格式:X-Priority:3   
  MIME-Version:<版本>   格式:MIME-Version:1.0   
  Content-Transfer-Encoding:<内容传输编码>   格式:Content-Transfer-Encoding:Base64   
  X-Mailer:<邮件发送者>   格式:X-Mailer:FoxMail   4.0   beta   1   [cn]   
  如果大家安装了OutLook(一般都装了:)),自己给自己发一封信,收下来后,查看邮件的属性,然后会看到包含上面一些信息的数据,大家可以根据 Outlook的头信息为参照。在这里,我重点要讲的是Content-Type这个头信息,实际上我们在邮件发送时常常包含了文本内容,Html超文本 内容以及附件内容,那么此时邮件的格式也就是multipart/mixed,但是这么多内容你要是全放在一块,服务器是不会认识的,那么需要在不同的内 容之间加入分隔符,   
    
  一部分内容完了之后再加入一个结束分隔符,有点像Html。在Content-Type的例子中有一句话boundary=unique- boundary-1,这里就告诉系统我的分隔符叫什么名字。那么在一个邮件中,可以有多个分隔符,其余的分隔符实际上是在你给出的第一个分隔符下扩展 的。说了这么多,看看程序:   
    
  WriteStream("Date:   "+DateTime.Now);//时间   
  WriteStream("From:   "+this.fromname+"<"+this.from+">");//发件人   
  WriteStream("Subject:   "+this.subject);//主题   
  WriteStream("To:"+this.to);//收件人   
  //邮件格式   
  WriteStream("Content-Type:   multipart/mixed;   boundary=/"unique-boundary-1/"");     
  WriteStream("Reply-To:"+this.from);//回复地址   
  WriteStream("X-Priority:"+priority);//优先级   
  WriteStream("MIME-Version:1.0");//MIME版本   
  //数据ID,随意   
  WriteStream("Message-Id:   "+DateTime.Now.ToFileTime()+"@security.com");     
  WriteStream("Content-Transfer-Encoding:"+this.encode);//内容编码   
  WriteStream("X-Mailer:DS   Mail   Sender   V1.0");//邮件发送者       
  WriteStream("");   
    看看这段头信息,里面的变量是事先定义好的,在头信息结束的时候,在写入一段空信息,这样Smtp服务器才会认为你已经写完了。   
    
  WriteStream(AuthStream("This   is   a   multi-part   message   in   MIME   format."));   
  WriteStream("");   
  这里只是一端描述性内容。   
  //从此处开始进行分隔输入   
  WriteStream("--unique-boundary-1");   
  //在此处定义第二个分隔符   
  WriteStream("Content-Type:   multipart/alternative;Boundary=/"unique-boundary-2/"");   
  WriteStream("");   
  //文本信息   
  WriteStream("--unique-boundary-2");   
  WriteStream("Content-Type:   text/plain;charset="+this.charset);   
  WriteStream("Content-Transfer-Encoding:"+this.encode);   
  WriteStream("");   
  WriteStream(body);   
  WriteStream("");//一个部分写完之后就写如空信息,分段   
  //html信息   
  WriteStream("--unique-boundary-2");   
  WriteStream("Content-Type:   text/html;charset="+this.charset);   
  WriteStream("Content-Transfer-Encoding:"+this.encode);   
  WriteStream("");   
  WriteStream(htmlbody);   
  WriteStream("");   
  WriteStream("--unique-boundary-2--");//分隔符的结束符号,尾巴后面多了--   
  WriteStream("");   
  //增加附件   
  Attachment();//这个方法是我们在上面讲过的,实际上他放在这   
  WriteStream("");   
  WriteStream("--unique-boundary-1--")    
  if   (!OperaStream(".","250"))//最后写完了,输入“.”   
  {   
  this.Close();   //关闭连接   
  }   
    
  这就是一封邮件的核心部分,上面的变量都是已定义好的全局变量,由用户传递给对象。整个邮件组件的主要内容到此告一段落。手指都敲酸了,由于本人水平有 限,可能有些地方不太让人满意,在此表示歉意。在研究邮件发送之前,在网上四处搜索资料,却没有收获,似乎大家都愿意把经验烂在肚子里,由于我肠胃不够强 壮,所以希望能够和大家共同分享这顿美餐。最后我们看看如何应用。   
    
  在aspx文件或者其他cs文件中引用:   
    
  MailSend   Ms=new   MailSend();//构造对象   
  Ms.SMTPServer=”smtp.sohu.com”;//传递参数   
  ……   
  Ms.send();//发送邮件   
  在此篇文章中我并没有给出完整的代码,而只是给出了代码片段,但是这已经足够整理出整个程序了。    

免责声明:文章转载自《c#socket发送邮件详解》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇ACE之旅——第一个ACE通讯程序daytimec# 通过程序修改hosts文件下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

服务器端Session和客户端Session(和Cookie区别)

Session其实分为客户端Session和服务器端Session。 当用户首次与Web服务器建立连接的时候,服务器会给用户分发一个 SessionID作为标识。SessionID是一个由24个字符组成的随机字符串。用户每次提交页面,浏览器都会把这个SessionID包含在 HTTP头中提交给Web服务器,这样Web服务器就能区分当前请求页面的是哪一个客户...

HttpClient发送get/post请求

参考博客:https://www.cnblogs.com/LuckyBao/p/6096145.html 1.需要的maven依赖: <!--httpClient需要的依赖--> <dependency> <groupId>org.apache.httpcomponents<...

Linux 内存监控

centos6 init进程是系统中的第一个进程,PID永远为1 查看系统在中静态进程的统计信息 命令: ps 格式: ps [可选项] ax: 显示所有的进程信息 -u: 使用以用户为主的格式输出进程信息 -e: select all processes 显示系统内的进程信息 -l: 使用长格式显示进程信息 -f: full 使用完整的格式显示进程信息...

使用docker安装宝塔面板

后端使用的命令太多了,容易忘记,所以记录在本文中,以便将来查询: 列出所有的容器 ID docker ps -aq 停止所有的容器 docker stop $(docker ps -aq) 删除所有的容器 docker rm $(docker ps -aq) 删除所有的镜像 docker rmi $(docker images -q) docker i...

关于stm32 HardFault_Handler 异常的处理 死机

在系统开发的时候,出现了HardFault_Handler硬件异常,也就是死机,尤其是对于调用了os的一系统,程序量大,检测堆栈溢出,以及数组溢出等,找了半天发现什么都没有的情况下,估计想死的心都有了。如果有些程序开始的时候一切没有问题,但是运行几个小时候,会发现死机了,搞个几天下来估计蛋都碎了一地吧。。。一般来说运行操作系统  是以下几个问题 1.开始的...

kafka的配置,kafka和flume的配置

参考文档:  https://files.cnblogs.com/files/han-guang-xue/kafka.zip 其中实现如图的效果详细步骤如下: #han01.confa1.sources=r1 a1.channels=c1 a1.sinks=k1 a1.sources.r1.type = spooldir a1.sources.r1.s...