在 .NET 应用中使用通用 SMTP 代码发送电子邮件





5.00/5 (12投票s)
自上次写了一篇文章以来,我曾多次被要求在 Yahoo! 或 Bing 等平台上分享代码。我想写一篇包含代码的文章,涵盖所有这些供应商。
引言
正如标题所明确提到的,写这篇文章的唯一原因是为了涵盖 .NET 应用程序中每个 SMTP 供应商代码的基本要求。如果你的供应商是谷歌的邮件服务——Gmail——那么你很可能可以跳过这篇文章,回到我很久以前写的那篇关于如何通过 C# 程序发送电子邮件的文章,通过 .NET 框架和通用问题发送电子邮件 – 使用 C# 代码。那篇文章将涵盖理解库的最重要部分,以及如何与所有设置进行通信,以便能够从你自己的账户发送电子邮件。
因此,从这篇文章中可以得出我的结论是,我可以轻松地与任何可能想问“
引用如何使用 {你的 SMTP 提供商} 发送电子邮件?
或者在 OP 要求我们检查他们代码中的错误时。他们需要明白,发送电子邮件的问题不仅仅是代码不好,还涉及其他几个问题。在这篇文章中,所有这些要点都将被涵盖,以便有一个标准的“工作”SMTP 代码,你可以在自己的应用程序中尝试,以确定问题是出在代码中,还是出在使用来发送电子邮件的账户中。
本文、提供的内容和代码示例仅供参考,可能包含错误。你可以选择大胆地复制/粘贴并在你的代码中尝试,或者仅从中获取有用的部分并考虑将其用于你的应用程序。
提供的代码设计
在我深入研究我实现的内容以及我在哪里找到它之前,我想告诉你这段代码的设计,我是如何做的,以及它可能对你有什么用。我将尝试解决一些问题,这就是为什么代码的设计要求我在后台编写一个工厂类来提供不同 SMTP 供应商的对象,在此代码示例中,我为以下服务实现了不同的对象:
- 谷歌邮箱
- 雅虎邮箱
- Hotmail — 即我们所知的 Outlook
- Office 365
SendGrid
— 我发现有很多关于SendGrid
API 的问题,所以我想分享一段代码,它确实可以从他们的服务器发送电子邮件。- 自定义对象,用于以不同方式实现任何内容。
这些是主要的 SMTP 服务提供商,在市面上都很常见,而且大多数问题都来自他们的用户。GitHub 上提供的代码示例将帮助你理解如何编写发送电子邮件的代码,或者帮助你理解如何为每个提供商编写 SmtpClient
对象。有时,它们需要不同的配置,有时它们只是相同的机器人,只是名字不同。
也就是说,有一些事情与当前的 SMTP 提供商情况以及账户的管理方式相矛盾,这将需要我在本文底部添加一个单独的部分来支持和讨论用户账户的各个方面,以便通过 .NET 应用程序进行 SMTP 编程。
ISmtpClient 接口
我开始创建一个接口,其中包含创建 SMTP 客户端具体实现所需的信息。此接口的目的是确保我可以将此接口实现到多个类中,并为它们提供实现。然而,存在一些问题——我将在本文末尾讨论。对该接口做一个简单的介绍,应该会有所帮助,对吧,所以...
- 该接口是一个 SMTP 客户端的定义,它将包含。
- SMTP 服务主机地址
- 连接端口信息
- 是否启用 SSL
- 要发送的消息
- 等等。
- SMTP 主机可以稍后更改。
- 端口地址,可以是默认的(25)或配置的(587,或服务器期望的任何其他端口)。
- 启用 SSL 支持,尽管这始终是必需的。
- 与服务器通信时使用的凭据。
因此,我最终得到了以下 interface
我将在此处省略一些内容,以便在下面的后续部分中进行解释(例如,ISmtpClient
中的 IDisposable
实现),所以请耐心等待。但是,到目前为止,我们已经有了可以在后续过程中用于创建 SMTP 客户端的基本实现——只是问一下,我没有在重复这句话吗? :D
好了,下一节将讨论我如何实现这个 ISmtpClient
接口。请继续阅读。
创建 SmtpClient 实例的工厂类 — SmtpClientFactory
一旦该接口开发完成,我就开始创建一个工厂类,它将帮助我为不同的服务创建不同的 SMTP 客户端。我不喜欢设计模式,因为我从来不理解它们……但不知何故,我总是在我的程序中实现它们,我的合著者告诉我,我在程序中设计的东西实际上是工厂模式,哦,好吧,乞丐不能挑剔,我想。为了设计客户端的核心创建者,我创建了一个 enum
来指定我将要创建并在后续流程中使用的客户端类型,enum
就像这样简单:
public enum ClientType { Gmail, Outlook, Yahoo, Office365, SendGrid, Custom }
正如你所见,这个枚举为我感兴趣的每种可能的客户端都有一个值。另外,还有一种特殊的客户端,它对读者来说没有什么可见之处,但只是一个想法,即这是一个自定义客户端,必须在运行时提供所有信息,因为工厂模式将期望这个家伙的大量信息。
ISmtpClient
的实现要求我将一些最重要的元素传递给工厂类的主方法,我做到了(尽管现在跳过),相应类型的类实例被生成。遵循工厂模式,通过创建实现类的实例而不是接口本身来返回对象的创建,其返回值是接口的实例。
这个类的 enum
部分已经讨论过了,只剩下一个函数要讲。GetClient
函数是核心函数,它负责抽象,并扮演创建实例并提供给我们的角色。现在,该函数本身实现如下:
// Method returns the ISmtpClient as an instance of the required object
public static ISmtpClient GetClient(ClientType type, NetworkCredential credentials,
bool withSsl, string host = "", int port = 25)
{
ISmtpClient client;
if (type == ClientType.Gmail)
{
client = new GmailClient(credentials, withSsl);
}
else if (type == ClientType.Office365)
{
client = new Office365Client(credentials, withSsl);
}
else if (type == ClientType.Outlook)
{
client = new OutlookClient(credentials, withSsl);
}
else if (type == ClientType.Yahoo)
{
client = new YahooClient(credentials, withSsl);
}
else if (type == ClientType.SendGrid)
{
client = new SendGridClient(credentials, withSsl);
}
else
{
// Custom
if (host == "")
{
throw new Exception("Host name is required for a custom SMTP client.");
}
client = new CustomClient(host, port, withSsl, credentials);
}
return client;
}
目前,请忽略我将在下一节中讨论的类。在此之前,只需理解这个函数返回我们所需的客户端。它期望用户输入:
ClientType
来决定需要哪种类型的客户端。请记住,其余的东西取决于客户端的类型,凭据、SSL 和无 SSL、端口和其他设置都取决于第一个参数。NetworkCredential
对象,用于存储用户的凭据,例如用户名和密码组合。- SSL 允许,尽管根据我的经验,我发现你在主要的 SMTP 供应商(如 Google Mail、Outlook 等)中始终需要使用 SSL。
- 主机和端口设置是创建
CustomClient
对象所必需的,它需要对通信方式有更多的控制,例如关闭 SSL,或使用不同的端口。但是,默认端口是 25。
这段代码没有什么特别之处,除了它检查主机名是否为空,并且客户端是 CustomClient
类型,然后它会提示用户输入一些内容并抛出异常。
仅提供部分实现
我不想分享太多相同的冗余实现代码,而是想在这里分享其中一些,以便你能理解我想传达的意思。请放心,你随时可以在 GitHub 上阅读代码示例,它已经提供给你了——请参阅顶部的下载部分。
// Implementing the interface, to capture the fields and functions.
public class GmailClient : ISmtpClient
{
// Host acts as a getter-only property here, implemented via lambda.
public string Host { get => "smtp.gmail.com"; }
public int Port { get; set; }
public bool EnforceSsl { get; set; }
public NetworkCredential Credentials { get; set; }
public SmtpClient Client { get; set; }
public GmailClient(NetworkCredential credentials, bool ssl)
{
Credentials = credentials;
Port = 25;
EnforceSsl = ssl;
if (ssl)
{
Port = 587;
}
Client = new SmtpClient(Host, Port);
}
// Async function to send the email, whereas the event handler can still be applied.
async Task ISmtpClient.Send(MailMessage message)
{
Client.EnableSsl = EnforceSsl;
Client.Credentials = Credentials;
// Send
await Client.SendMailAsync(message);
}
// The Dispose() function, to properly send the QUIT command to SMTP server
public void Dispose()
{
Client.Dispose();
}
}
其余命名类型的客户端都以类似的方式实现服务,而自定义客户端类型则略有不同。所以,这就是我将向你展示如何实现另一种类型的客户端,即自定义客户端类型。
// Implementing the interface, to capture the fields and functions.
public class CustomClient : ISmtpClient
{
// Needs a setter, so that object can set Host information later on
public string Host { get; set; }
public int Port { get; set; }
public bool EnforceSsl { get; set; }
public NetworkCredential Credentials { get; set; }
public SmtpClient Client { get; set; }
// Constructor, requires somewhat more information.
public CustomClient(string host, int port, bool ssl, NetworkCredential credentials)
{
// In the custom mode, we can set the host ourself.
Host = host;
Port = port;
Credentials = credentials;
EnforceSsl = ssl;
Client = new SmtpClient(Host, Port);
}
// Async function to send the email, whereas the event handler can still be applied.
async Task ISmtpClient.Send(MailMessage message)
{
Client.EnableSsl = EnforceSsl;
Client.Credentials = Credentials;
// Send
await Client.SendMailAsync(message);
}
// The Dispose() function, to properly send the QUIT command to SMTP server
public void Dispose()
{
Client.Dispose();
}
}
两种实现之间唯一的主要区别在于它们的构造函数,因为其中一个比另一个需要更多的信息,而另一个已经包含了关于主机和其他一些东西的信息。
其次,就像我们在之前的 SmtpClientFactory
类中看到的,我们使用了额外的参数来填充自定义客户端的详细信息,
GetClient(ClientType type, NetworkCredential credentials, bool withSsl, string host = "", int port = 25)
对应于以下构造函数
public CustomClient(string host, int port, bool ssl, NetworkCredential credentials)
而其他的则不获取值,我们的程序也不需要传递这些值。在这种情况下,最好始终遵循类型,并忽略传递硬编码的值。但是,在某些情况下,我们可能需要直接传递一些信息,例如,当我们不知道主机名、连接端口以及是否需要 SSL 时。当然,市面上有一些服务,一些不安全的服务,它们提供免费访问其服务器发送电子邮件,然后它们会跟踪你的电子邮件、内容和个人信息进行出售并从中获利。
实现 IDisposable 接口
如上所述,是时候讨论在我自己的 ISmtpClient
接口中实现 IDisposable
接口了,其中它已经被 .NET 框架的 SmtpClient
实现了。事情是这样的,当我编写应用程序代码时,我没有在我的 interface
中实现 interface
,而是使用了类似这样的代码:
// I know the signature says, "async Task ISmtpClient.Send(params) {}" but, read the paragraphs below
void ISmtpClient.Send(MailMessage message, NetworkCredential credentials, bool withSsl) {
// Create the object here
using (var client = new SmtpClient(Host, Port)) {
/* Other stuff,
* Send the email here, do a proper clean out, etc.
*/
}
}
然后,后来我才发现 SmtpClient
中还有一个 API,它允许你异步发送电子邮件,而且通常建议考虑异步方法,而不是同步方法,除非你真的知道你在做什么并且需要同步代码。现在,有了这段代码,我将需要实际在函数内部等待,或者至少让对象等待直到电子邮件发送出去,然后再清理对象。
将 IDisposable
引入此处的目的是,以便将正确处理对象处置的控制权交给我自己的程序范围,并控制对象何时被处置。因此,为了进行必要的更改,我做了以下工作:
- 将函数签名更改为标记为异步上下文
- 将返回类型从
void
更改为Task
— 这在异步上下文中又是void
。 - 应用
await
关键字并使用了异步函数。
“using
” 上下文在需要清理程序一段时间以来一直在使用的资源时很有帮助。你可以在我之前的一篇文章中阅读更多相关信息,也可以在 MSDN 或 Microsoft Docs 网站上查找。它基本上是一个语言结构,允许你确保一个对象在离开作用域之前释放它所持有的所有资源。在我写下这段代码之前,我不想错过这个重要的因素,这就是为什么必须确保代码中使用了 using
块。正如你将看到的,SmtpClient
的 Dispose
函数被调用,以向服务器发送 QUIT
命令并正确终止连接,然后离开上下文并被 GC 删除。
异步上下文的解释将在下一节中,请继续阅读。
客户端中的异步功能
现在我们已经将各个部分合并并总结在一起,让我们为客户端应用一些最终的润色,并使其支持图形应用程序的异步功能。上面的函数现在已更改为以下代码示例:
// Async keyword, and the Task return type
async Task ISmtpClient.Send(MailMessage message)
{
Client.EnableSsl = EnforceSsl;
Client.Credentials = Credentials;
// Sending of the mail message asynchronously, and waiting till this finishes.
await Client.SendMailAsync(message);
}
这两个更改的好处,第一个是 IDisposable
,第二个是添加了 async/await 关键字到内容,这允许我编写异步代码,同时正确地处理 SmtpClient
对象的处置。
请注意,还有一个函数 SendAsync()
。该函数不与 async
/await
设计一起使用,因为它返回 void
,因此无法 await
。因此,如果你打算使用 async
/await
,则必须避免使用该函数,而是考虑带有“Mail
”的函数。现在我们的大部分代码都已经处理完毕,让我们向下移动一步,尝试从我们的代码中发送电子邮件。
注意:我不会为每个客户端发送电子邮件,因为我已经完成了,而是只通过默认的 Google Mail 客户端发送电子邮件并展示结果,并在文章结尾给出一些建议。
开始使用 System.Net.Mail
.NET 框架为邮件发送提供了哪些支持,这是你首先想了解的。在 SmtpClient
对象中,你可以处理几件事情,这正是我们在本节中要看到的。SmtpClient
对象在 .NET Framework 中支持基本的 SMTP 通信级别,正如 **RFC 2821** 中所定义的那样。你需要查阅该文档以了解更多关于 SMTP 的信息。我在这里要谈论的是,在本文中,我将介绍一些最佳实践,以确保你的应用程序能够持续运行,并且显然不会崩溃。
乐观尝试 — 成功命中
现在,首先我会尝试运行的代码并展示我的成果,没有必要再次分享代码,或者说我使用了哪个客户端。需要注意的是,如果你提供准确的详细信息,客户端就能正常工作,我将在稍后讨论这些准确的详细信息。这是我的测试结果:
这证明了代码可以正常工作。其次,我根据每个服务提供商的最佳实践测试了代码,以确保通用代码是这里所需的准确骨架。现在,让我们尝试让代码出错一次。邮件消息是:
继续前进,现在。
以多种方式破坏代码
.NET 框架的 SmtpClient
在这部分会让你恼火。API 不会告诉你连接是否建立、是否正常工作、是否准备好接收电子邮件消息,或者是否没有。它唯一能告诉你的时候就是当你调用 Send
函数时!这完全没有用,但如果我们要坚持一切.NET,我们就无能为力。在这个客户端中,有几件事情可能会出错。我列举几个给你:
- 你的凭据有误
- 你的端口不正确,也许你想使用默认端口(25)而不是安全端口。
- 你没有启用基于 SSL 的通信,而是想通过 SSL 通道进行通信。
在所有这些情况下,你只需要确保在发送电子邮件之前启用 SSL 通道。
// somewhere inside the client
client.EnableSsl = true;
// rest of the stuff
在几乎所有可能的情况下,客户端都会崩溃并给你这个错误——或类似的错误。
异常是在 Visual Studio 窗口中捕获的,但你已经明白了。在大多数情况下都会抛出错误,这很可能是 .NET 的 SMTP 编程中最常遇到的错误,因为 API 对让你进一步控制服务器-客户端通信状态不感兴趣。然而,这个错误的主要原因是我们将 EnableSsl
字段设置为 false
。
奖励:理解不同提供商的行为
最后,我想谈谈不同提供商的实现方式以及他们如何期望客户访问 API。我们知道的一点是,所有这些供应商都是服务提供商,面向消费者,主要用作消费者基础,而不是企业导向(SendGrid
、Office 365 和 Google G Suite 等是例外,其他 SMTP 提供商也一样,通常来说)。它们都有相似的行为,并提供类似的服务。在我为这篇文章的探索过程中,否则,可能仍然会有一个问题,比如:
引用我已经启用了 SSL,并且我的用户名/密码是正确的,但我仍然无法发送电子邮件。可能是什么问题?
这类问题需要一些提供商特定的细节,并需要深入研究其中的每一个。我在它们之中注意到了一些事情,在最后的这一部分,我想与你分享……
安全实现
由于所有这些供应商都是面向消费者的,他们会禁用其服务的可编程性。例如,Google Mail 和 Yahoo 要求我进入设置,不仅启用 SMTP 服务,还启用低安全性应用程序。
例如,看看我收件箱的以下截图,Google Mail 告诉我我需要检查我的账户详细信息(例如密码)是否被泄露,或者如果我知道发生了什么,就允许这些设备访问。
还有其他几项检查,旨在确保你的账户安全。而在另一方面,在应用程序服务提供商(如 Office 365 或 SendGrid
)中,你的账户将受到强密码的保护,因为它们是面向企业的服务提供商,它们不喜欢锁定用户,一旦他们已经使用 API 密钥或用户名/密码组合登录。
通信安全
现在让我们来谈谈通信的安全性,邮件在网络上传播时会发生什么?这是整个 API 中最令人困惑的事情之一。API 说,通信是通过 SSL 进行的,这是胡说八道……通信不是通过 SSL 进行的,而是通过 TLS 进行的(它不是 SSL)。
因此,当你读到以下代码行时
client.EnableSsl = true;
请理解,这是 TLS 在进行,而不是 SSL。我建议你更多地了解 TLS(传输层安全性)和 SSL(安全套接层),以更好地理解它们。
请查阅以下 RFC 文档的链接,以更好地理解事情是如何发生的:
通过阅读这些链接,大多数困惑都会消除,因为困惑仅仅是由于命名约定和人们说出这些技术的名称。仅此而已。
其次,不确定,但所有客户端都要求你启用 SSL 通信,否则它们总是会导致错误。启用 SSL 然后发送电子邮件也是一种更好的做法,你应该避免使用不认真对待安全的服务器和服务提供商。即使没有发生什么严重的事情,你的电子邮件账户也会向公众开放,并且垃圾邮件发送者总是在寻找电子邮件地址。
杂项
或者我称之为“最后的想法”。所有 SMTP 提供商都遵循我上面分享的 RFC 文档,并且总是推荐找到一个符合这些标准的库,我发现 .NET 框架的 SmtpClient
实现很差,原因很明显并且在文章中有所分享(例如,客户端无法告知连接是否已建立,或连接状态如何,除非你实际上尝试做些什么)。我也正在尝试找到一个有用的库,一旦可用就会更新,在此之前,没有什么可说的了。
在我寻找的过程中,我发现 Office 365 在发送电子邮件和生成响应方面有点迟钝,而 SendGrid
甚至更糟。SendGrid
所做的是他们用 OKAY(或任何 SMTP 对这个英文命令的替代命令)回复,但他们直到过了一段时间才发送电子邮件……这让我很困扰,并且不算作快速发送。Google Mail、Yahoo 和 Outlook(不是 Office 365)对我来说都足够快。
尽管如此,暂时来说,我的默认电子邮件提供商仍然是 Google Mail,很快我可能会迁移到 Office 365 或 Outlook,因此我将调查为什么他们的服务器发送电子邮件会有点晚。
我希望你觉得这篇文章很有用,并将在这里提供的内容用作参考,或用于任何你可能想要的用途。这段代码不是一个简单的示例函数,而是类似于一个完整的库,所以你可以获取你需要的部分并在你自己的代码中实现它。