使用.Net发电子邮件小结

2019-08-06 14:38:11 浏览数 (1)

电子邮件是怎么发出去的?

电子邮件是通过互联网发出去的,互联网中传输层协议有TCP/IP协议,邮件服务在基于TCP/IP底层协议之上的应用层实现SMTP、POP3、IMAP4等协议,通过这些协议实现了邮件的收发服务。

TCP/IP协议?

互联网中的两个终端在建立和断开连接会通过:

TCP的3次握手和4次挥手完成。

建立连接,3次握手:

1. 客户端A发送SYN包(SYN=1)到服务器B,并进入SYN_SEND状态,等待服务器B确认。

2. 服务器B收到SYN包,必须确认客户A的SYN(ACK=1),同时自己也发送一个SYN包(SYN=1),即SYN ACK包,此时服务器B进入SYN_RECV状态。

3. 客户端A收到服务器B的SYN+ACK包,向服务器B发送确认包ACK(ACK=1),此包发送完毕,客户端A和服务器B进入ESTABLISHED状态,完成三次握手。

四次挥手,关闭连接:

由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。

1. 客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送。

2. 服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1。(关闭了一个单向通道)

3. 服务器B关闭与客户端A的连接,发送一个FIN给客户端A。

4. 客户端A发回ACK报文确认,并将确认序号设置为收到序号加1。(关闭了整个通道)

为什么建立连接协议是三次握手,而关闭连接却是四次挥手呢?

建立连接时,服务端LISTEN状态下的SOCKET当收到SYN报文的连接请求后,它可以把ACK和SYN放在一个报文里来发送。

关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;

但未必你所有的数据都全部发送给对方了,所以你可能未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以关闭连接的ACK报文和FIN报文多数情况下都是分开发送的。

SMTP、POP3、IMAP4等协议?

1. SMTP

Simple Mail Transfer Protocol(即简单邮件传输协议),它是一组用于从源地址到目的地址传送邮件的规则,简单的说就是:From-->To的传送规则。由SMTP来控制信件中转的方式。SMTP属于TCP/IP家族中的一员,它帮助每一台计算机在发送或中转信件时找到下一个目的地。通过SMTP协议所指定的服务器,就可以把E-Mail寄到收信人的服务器上。SMTP服务器则是遵循SMTP协议的邮件发送服务器,用来中转你发出的电子邮件。

SMTP目前已是事实上的E-Mail传输的标准。

2. POP3

Post Office Protocol 3(即邮局协议的第3个版本),负责从邮件服务器中检索电子邮件。它要求邮件服务器完成下面几种任务之一:

从邮件服务器中检索邮件并从服务器中删除这个邮件;

从邮件服务器中检索邮件但不删除它;

不检索邮件,只是询问是否有新邮件到达。

POP3是因特网电子邮件的第一个离线协议标准。

3. IMAP4

Internet Message Access Protocol 4(即交互式数据消息访问协议第四个版本),提供脱机和联机访问功能。是一种优于POP的新协议,是美国斯坦福大学在1986年开始研发的多重邮箱电子邮件系统。和POP一样,IMAP也能下载邮件、从服务器中删除邮件或询问是否有新邮件,但IMAP克服了POP的一些缺点。例如,请求邮件服务器只下载所选中的邮件而不是全部邮件。客户机可先阅读邮件信息的标题和发送者的名字再决定是否下载这个邮件。通过用户的客户机电子邮件程序,IMAP可让用户在服务器上创建并管理邮件文件夹或邮箱、删除邮件、查询某封信的一部分或全部内容,完成所有这些工作时都不需要把邮件从服务器下载到用户的个人计算机上。

默认情况下,当 IMAP4 电子邮件应用程序将电子邮件下载到客户端计算机,下载邮件的副本会保留在电子邮件服务器上。正是由于用户的电子邮件副本保留在电子邮件服务器上,用户可以从多台计算机上访问相同的电子邮件。也可以实现电子邮件服务器上的多个文件夹与客户端计算机上的多个文件夹同步。

SMTP/POP3工作方式如图:

常见的邮箱类型有哪些?

免费邮箱、vip邮箱、域名邮箱、企业邮箱等。

免费邮箱就像gmail,hotmail,qq邮箱等等。该网站上你请求电子邮件服务和一些个人信息的地方会显示广告。

部分免费邮件SMTP服务器参考设置:

SMTP服务器一般是smtp.domain.com,是以smtp开头的二级域名。port端口一般是25,但是也不一定,想谷歌邮箱就是587端口,这个可以在邮件服务器上指定,我们可以通过登入到邮箱对其具体的stmp,pop3和IMAP4等信息进行查看。

vip邮箱就是邮件商家提供的收费版邮件服务,一般没有广告,和其他一些服务,例如smtp.vip.qq.com,邮箱地址:36****23@vip.qq.com, 不过现在都是免费邮箱是主流。

域名邮箱是个性化邮件服务,能让您用自己的域名做为后缀即“@自己的域名”,前提是你需要一个域名(通常域名要收费)。

“企业邮箱”是域名邮箱,但通常是指通过付费方式获得更好服务的邮箱。例如,您公司域名为www.abc.com,则SMTP服务器为:mail.abc.com或者smtp.abc.com,或者一个ip地址,邮箱地址:office@abc.com;

邮件发送相关.NET类库

使用System.Net.Mail命名空间。

类型有:

System.Net.Mail.MailMessage

MailMessage实例对象代表着一个实实在在的邮件,里面有邮件的各种信息,包括发送人,收件人,抄送人,主题,内容,附件,优先级,文本的编码方式等,下面列出它的重要的属性:

代码语言:javascript复制
public MailAddress From
public MailAddress Sender
public MailAddressCollection To
public MailAddressCollection CC
public MailAddressCollection Bcc
public NameValueCollection Headers
public Encoding HeadersEncoding
public string Subject
public Encoding SubjectEncoding
public string Body
public bool IsBodyHtml
public Encoding BodyEncoding
public TransferEncoding BodyTransferEncoding
public AttachmentCollection Attachments
public MailPriority Priority

因为代表了一个实实在在的邮件实体,所以没有什么方法,不过可以看一下它的构造器:

代码语言:javascript复制
public MailMessage()
public MailMessage(string from, string to)
public MailMessage(string from, string to, string subject, string body)
public MailMessage(MailAddress from, MailAddress to)

System.Net.Mail.SmtpClient

SmtpClient实例对象代表着一个邮件服务的客户端,通过它可以指定邮件服务器地址和端口,资格证书(用户名和密码)这样可以顺利连接到SMTP服务器上,以及提供了发邮件的方法。

属性有:

代码语言:javascript复制
public string Host
public int Port
public ICredentialsByHost Credentials
public SmtpDeliveryMethod DeliveryMethod
public bool EnableSsl
public int Timeout
public bool UseDefaultCredentials

构造器:

代码语言:javascript复制
public SmtpClient()
public SmtpClient(string host)
public SmtpClient(string host, int port)

发邮件的方法:

代码语言:javascript复制
public void Send(MailMessage message)
public void Send(string from, string recipients, string subject, string body)
public void SendAsync(MailMessage message, object userToken)
public void SendAsync(string from, string recipients, string subject, string body, object userToken)
public Task SendMailAsync(MailMessage message)
public Task SendMailAsync(string from, string recipients, string subject, string body)

邮件发送完成的事件:

代码语言:javascript复制
public event SendCompletedEventHandler SendCompleted;

System.Net.NetworkCredential

NetworkCredential对象就是代表着资格证书(用户名和密码),能不能连接上Smtp服务器就靠它了,初始化好之后应该赋值给SmtpClient实例对象的Credentials属性。

代码语言:javascript复制
SmtpClient.Credentials = new NetworkCredential(userName, password);

了解了这几个基础类之后,我们动手写一个小Demo。

建立一个NetworkCredentialProvider类,通过它初始化NetworkCredential对象:

代码语言:javascript复制
public class NetworkCredentialProvider
{
  public static NetworkCredential GetNetworkCredential(string userName, string password)
  {
    return new NetworkCredential(userName, password);
  }
}

建一个SmtpSettingAttribute特性,保存Smtp服务器的配置信息:

代码语言:javascript复制
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
public class SmtpSettingAttribute : Attribute
{
  public string Host { get; set; }
  public int Port { get; set; }
  public bool EnableSsl { get; set; }
}

建立一个EmailType枚举类,每一个枚举值表示一种邮件,并通过特性配置对应的Smtp服务信息:

代码语言:javascript复制
[Flags]
public enum EmailType
{
  None = 0,

  [SmtpSetting(EnableSsl = false, Host = "10.17.0.119", Port = 25)]
  SelfServer = 1,

  [SmtpSetting(EnableSsl = true, Host = "smtp.gmail.com", Port = 587)]
  Gmail = 2,

  [SmtpSetting(EnableSsl = true, Host = "smtp.live.com", Port = 25)]
  HotMail = 4,

  [SmtpSetting(EnableSsl = false, Host = "smtp.qq.com", Port = 25)]
  QQ_FoxMail = 8,

  [SmtpSetting(EnableSsl = false, Host = "smtp.126.com", Port = 25)]
  Mail_126 = 16,

  [SmtpSetting(EnableSsl = false, Host = "smtp.163.com", Port = 25)]
  Mail_163 = 32,

  [SmtpSetting(EnableSsl = false, Host = "smtp.sina.com", Port = 25)]
  Sina = 64,

  [SmtpSetting(EnableSsl = false, Host = "smtp.tom.com", Port = 25)]
  Tom = 128,

  [SmtpSetting(EnableSsl = false, Host = "smtp.sohu.com", Port = 25)]
  SoHu = 256,

  [SmtpSetting(EnableSsl = true, Host = "smtp.office365.com", Port = 587)]
  Outlook = 512,
}

创建一个SmtpClientFactory类,用来创建客户端代理对象:

代码语言:javascript复制
public class SmtpClientFactory
{
  public static SmtpClient Create(EmailType emailType, string userName, string password)
  {
    Type type = emailType.GetType();
    FieldInfo field = type.GetField(emailType.ToString());
    var smtpSetting = field.GetCustomAttribute<SmtpSettingAttribute>();

    return Create(smtpSetting.Host, smtpSetting.Port, smtpSetting.EnableSsl, userName, password);
  }

  public static SmtpClient Create(string host, int port, bool enableSsl, string userName, string password)
  {
    NetworkCredential networkCredential = NetworkCredentialProvider.GetNetworkCredential(userName, password);

    SmtpClient smtpClient = new SmtpClient(host, port);
    smtpClient.DeliveryMethod = SmtpDeliveryMethod.Network;
    // useDefaultCredentials
    // false:则连接到服务器时会将 Credentials 属性中设置的值用作凭据。
    //        如果 UseDefaultCredentials 属性设置为 false 并且尚未设置 Credentials 属性,则将邮件以匿名方式发送到服务器。
    //        若 SMTP 服务器要求在验证客户端的身份则会抛出异常。。
    // true:System.Net.CredentialCache.DefaultCredentials (应用程序系统凭证)会随请求一起发送。
    smtpClient.UseDefaultCredentials = false;
    smtpClient.Timeout = 100000;

    smtpClient.Credentials = networkCredential;
    smtpClient.EnableSsl = enableSsl;

    return smtpClient;
  }
}

最后在Main方法中,进行测试:

代码语言:javascript复制
static void Main(string[] args)
{
  var smtpClient = SmtpClientFactory.Create(EmailType.Outlook, "xxx@outlook.com", "pwdxxx");

  MailMessage mailMessage = new MailMessage();
  mailMessage.From = new MailAddress("xxx@outlook.com", "xxx", Encoding.UTF8);
  mailMessage.To.Add("xxx@xx.com");
  mailMessage.Priority = MailPriority.High;
  mailMessage.Subject = "Nestor develop test";
  mailMessage.SubjectEncoding = Encoding.UTF8;
  mailMessage.IsBodyHtml = true;
  mailMessage.BodyEncoding = Encoding.UTF8;
  mailMessage.Body = "This is body. -nestor";

  smtpClient.Send(mailMessage);
  smtpClient.Dispose();

  Console.ReadKey();
}

到这里代码部分基本结束了,顺利测试通过。

我们对应该注意到的问题,进行简单的分析:

1、一个SmtpClient一次只能发送一个MailMessage,不管是同步还是异步发送,所以批量发送也会因为这个条件而被阻塞。

2、若要异步发送大批量邮件,方案:应当多个线程、每个线程去使用一个单独的SmtpClient去发送。(但要注意不合理分配资源会更加降低性能)

3、何时使用 SmtpClient.SendAsync() 异步发送呢?是在发件内容、附件、加密等因素造成一条短信发送比较耗时的情况下使用。

4、SmtpClient非线程安全类

5、构造的 SmtpClient 实例由外部进行Dispose()。SmtpHelper类只简单提供构造,不做释放操作。

6、SmtpClient 没有提供 Finalize() 终结器,所以GC不会进行回收,只能由外部使用完后进行显示释放,否则会发生内存泄露问题

7、useDefaultCredentials是false:则连接到服务器时会将 Credentials 属性中设置的值用作凭据。如果UseDefaultCredentials属性设置为 false 并且尚未设置 Credentials 属性,则将邮件以匿名方式发送到服务器。若SMTP 服务器要求在验证客户端的身份则会抛出异常。当为true时DefaultCredentials (应用程序系统凭证)会随请求一起发送。

0 人点赞