Twisted 邮件教程:从头开始构建 SMTP 客户端¶
简介¶
本教程将引导您完成一个极其简单的 SMTP 客户端应用程序的创建。在本教程结束时,您将了解如何创建和启动一个使用 SMTP 协议的 TCP 客户端,使其连接到相应的邮件交换服务器,并传输要传递的消息。
在本教程的大部分内容中,将使用 twistd
启动应用程序。在最后,我们将探讨启动 Twisted 应用程序的其他可能性。在此之前,请确保您已安装 twistd
并且可以方便地访问它,以便在运行每个示例 .tac
文件时使用。
SMTP 客户端 1¶
第一步是创建 smtpclient-1.tac
,以便 twistd
使用。
from twisted.application import service
.tac
文件的第一行导入 twisted.application.service
,这是一个包含 Twisted 中许多基本服务类和辅助函数的模块。特别是,我们将使用 Application
函数来创建一个新的应用程序服务。应用程序服务只是充当一个中心对象,用于存储某些类型的部署配置。
application = service.Application("SMTP Client Tutorial")
.tac
文件的第二行创建一个新的应用程序服务,并将其绑定到本地名称 application
。 twistd
在它运行的每个 .tac
文件中都需要此本地名称。它使用对象上的各种配置信息来确定其行为。例如,"SMTP Client Tutorial"
将用作要将应用程序状态序列化到的 .tap
文件的名称,如果需要的话。
这就是第一个示例的全部内容。现在我们已经有了足够多的 .tac
文件来传递给 twistd
。如果我们使用 twistd
命令行运行 smtpclient-1.tac
twistd -ny smtpclient-1.tac
我们将得到以下输出
exarkun@boson:~/mail/tutorial/smtpclient$ twistd -ny smtpclient-1.tac
18:31 EST [-] Log opened.
18:31 EST [-] twistd 2.0.0 (/usr/bin/python2.4 2.4.1) starting up
18:31 EST [-] reactor class: twisted.internet.selectreactor.SelectReactor
18:31 EST [-] Loading smtpclient-1.tac...
18:31 EST [-] Loaded.
正如我们所预期的那样,并没有发生太多事情。我们可以通过发出 ^C
来关闭此服务器
18:34 EST [-] Received SIGINT, shutting down.
18:34 EST [-] Main loop terminated.
18:34 EST [-] Server Shut Down.
exarkun@boson:~/mail/tutorial/smtpclient$
SMTP 客户端 2¶
我们的 SMTP 客户端的第一个版本并不十分有趣。它甚至没有建立任何 TCP 连接!smtpclient-2.tac
将更接近于这种复杂程度。首先,我们需要导入更多内容
from twisted.application import internet
from twisted.internet import protocol
twisted.application.internet
是另一个应用程序服务模块。它提供建立出站连接(以及创建网络服务器,尽管我们目前对这些部分不感兴趣)的服务。 twisted.internet.protocol
提供了许多 Twisted 核心概念的基本实现,例如工厂和协议。
smtpclient-2.tac
的下一行实例化了一个新的客户端工厂。
smtpClientFactory = protocol.ClientFactory()
客户端工厂负责在建立连接时构造协议实例。它们可能只需要创建单个实例,或者如果建立了许多不同的连接,则可能需要创建许多实例,或者它们可能根本不需要创建任何实例,如果没有任何连接成功建立的话。
现在我们有了客户端工厂,我们需要以某种方式将其连接到网络。 smtpclient-2.tac
的下一行就是这么做的
smtpClientService = internet.TCPClient(None, None, smtpClientFactory)
我们暂时忽略 internet.TCPClient
的前两个参数,而是关注第三个参数。 TCPClient
是这些应用程序服务类之一。它创建到指定地址的 TCP 连接,然后使用其第三个参数(客户端工厂)来获取协议实例。然后,它将 TCP 连接与协议实例关联起来,并退出。
我们可以尝试以与运行 smtpclient-1.tac
相同的方式运行 smtpclient-2.tac
,但结果可能有点令人失望
exarkun@boson:~/mail/tutorial/smtpclient$ twistd -ny smtpclient-2.tac
18:55 EST [-] Log opened.
18:55 EST [-] twistd SVN-Trunk (/usr/bin/python2.4 2.4.1) starting up
18:55 EST [-] reactor class: twisted.internet.selectreactor.SelectReactor
18:55 EST [-] Loading smtpclient-2.tac...
18:55 EST [-] Loaded.
18:55 EST [-] Starting factory <twisted.internet.protocol.ClientFactory
instance at 0xb791e46c>
18:55 EST [-] Traceback (most recent call last):
File "twisted/scripts/twistd.py", line 187, in runApp
app.runReactorWithLogging(config, oldstdout, oldstderr)
File "twisted/application/app.py", line 128, in runReactorWithLogging
reactor.run()
File "twisted/internet/posixbase.py", line 200, in run
self.mainLoop()
File "twisted/internet/posixbase.py", line 208, in mainLoop
self.runUntilCurrent()
--- <exception caught here> ---
File "twisted/internet/base.py", line 533, in runUntilCurrent
call.func(*call.args, **call.kw)
File "twisted/internet/tcp.py", line 489, in resolveAddress
if abstract.isIPAddress(self.addr[0]):
File "twisted/internet/abstract.py", line 315, in isIPAddress
parts = string.split(addr, '.')
File "/usr/lib/python2.4/string.py", line 292, in split
return s.split(sep, maxsplit)
exceptions.AttributeError: 'NoneType' object has no attribute 'split'
18:55 EST [-] Received SIGINT, shutting down.
18:55 EST [-] Main loop terminated.
18:55 EST [-] Server Shut Down.
exarkun@boson:~/mail/tutorial/smtpclient$
发生了什么?事实证明,TCPClient
的前两个参数很重要。我们将在下一个示例中讨论它们。
SMTP 客户端 3¶
我们的 SMTP 客户端的第三个版本只改变了一件事。来自第二个版本的这一行
smtpClientService = internet.TCPClient(None, None, smtpClientFactory)
将其前两个参数从 None
更改为更有意义的内容
smtpClientService = internet.TCPClient('localhost', 25, smtpClientFactory)
这将客户端引导到连接到localhost 的端口25。这不是我们最终想要的地址,但它是一个很好的占位符。我们可以运行 smtpclient-3.tac
并看看这种改变给我们带来了什么
exarkun@boson:~/mail/tutorial/smtpclient$ twistd -ny smtpclient-3.tac
19:10 EST [-] Log opened.
19:10 EST [-] twistd SVN-Trunk (/usr/bin/python2.4 2.4.1) starting up
19:10 EST [-] reactor class: twisted.internet.selectreactor.SelectReactor
19:10 EST [-] Loading smtpclient-3.tac...
19:10 EST [-] Loaded.
19:10 EST [-] Starting factory <twisted.internet.protocol.ClientFactory
instance at 0xb791e48c>
19:10 EST [-] Enabling Multithreading.
19:10 EST [Uninitialized] Traceback (most recent call last):
File "twisted/python/log.py", line 56, in callWithLogger
return callWithContext({"system": lp}, func, *args, **kw)
File "twisted/python/log.py", line 41, in callWithContext
return context.call({ILogContext: newCtx}, func, *args, **kw)
File "twisted/python/context.py", line 52, in callWithContext
return self.currentContext().callWithContext(ctx, func, *args, **kw)
File "twisted/python/context.py", line 31, in callWithContext
return func(*args,**kw)
--- <exception caught here> ---
File "twisted/internet/selectreactor.py", line 139, in _doReadOrWrite
why = getattr(selectable, method)()
File "twisted/internet/tcp.py", line 543, in doConnect
self._connectDone()
File "twisted/internet/tcp.py", line 546, in _connectDone
self.protocol = self.connector.buildProtocol(self.getPeer())
File "twisted/internet/base.py", line 641, in buildProtocol
return self.factory.buildProtocol(addr)
File "twisted/internet/protocol.py", line 99, in buildProtocol
p = self.protocol()
exceptions.TypeError: 'NoneType' object is not callable
19:10 EST [Uninitialized] Stopping factory
<twisted.internet.protocol.ClientFactory instance at
0xb791e48c>
19:10 EST [-] Received SIGINT, shutting down.
19:10 EST [-] Main loop terminated.
19:10 EST [-] Server Shut Down.
exarkun@boson:~/mail/tutorial/smtpclient$
进展微乎其微,但服务仍然引发了异常。这次是因为我们没有为工厂指定协议类。我们将在下一个示例中做到这一点。
SMTP 客户端 4¶
在前面的示例中,我们遇到了问题,因为我们没有正确设置客户端工厂的协议属性(或者根本没有设置)。 ClientFactory.buildProtocol
是负责创建协议实例的方法。默认实现调用工厂的 protocol
属性,将自身作为名为 factory
的属性添加到生成的实例中,并将其返回。在 smtpclient-4.tac
中,我们将纠正导致 smtpclient-3.tac 中出现回溯的疏忽
smtpClientFactory.protocol = protocol.Protocol
运行此版本的客户端,我们可以看到输出不再出现回溯
exarkun@boson:~/doc/mail/tutorial/smtpclient$ twistd -ny smtpclient-4.tac
19:29 EST [-] Log opened.
19:29 EST [-] twistd SVN-Trunk (/usr/bin/python2.4 2.4.1) starting up
19:29 EST [-] reactor class: twisted.internet.selectreactor.SelectReactor
19:29 EST [-] Loading smtpclient-4.tac...
19:29 EST [-] Loaded.
19:29 EST [-] Starting factory <twisted.internet.protocol.ClientFactory
instance at 0xb791e4ac>
19:29 EST [-] Enabling Multithreading.
19:29 EST [-] Received SIGINT, shutting down.
19:29 EST [Protocol,client] Stopping factory
<twisted.internet.protocol.ClientFactory instance at
0xb791e4ac>
19:29 EST [-] Main loop terminated.
19:29 EST [-] Server Shut Down.
exarkun@boson:~/doc/mail/tutorial/smtpclient$
但这意味着什么呢? twisted.internet.protocol.Protocol
是基本协议实现。对于熟悉经典 UNIX 网络服务的人来说,它等同于丢弃服务。它永远不会产生任何输出,并且会丢弃所有输入。这没什么用,当然不像 SMTP 客户端。让我们看看如何在下一个示例中改进这一点。
SMTP 客户端 5¶
在 smtpclient-5.tac
中,我们将首次使用 Twisted 的 SMTP 协议实现。我们将进行明显的更改,简单地将 twisted.internet.protocol.Protocol
替换为 twisted.mail.smtp.ESMTPClient
。不用担心 E 在 ESMTP 中。它表明我们实际上正在使用 SMTP 协议的较新版本。Twisted 中有一个 SMTPClient
,但实际上没有理由使用它。
smtpclient-5.tac 添加了一个新的导入
from twisted.mail import smtp
Twisted 中所有与邮件相关的代码都存在于 twisted.mail
包之下。更具体地说,与 SMTP 协议实现有关的所有内容都在 twisted.mail.smtp
模块中定义。
接下来,我们删除了在 smtpclient-4.tac 中添加的一行
smtpClientFactory.protocol = protocol.Protocol
并在其位置添加类似的一行
smtpClientFactory.protocol = smtp.ESMTPClient
我们的客户端工厂现在使用一个作为 SMTP 客户端行为的协议实现。当我们尝试运行此版本时会发生什么?
exarkun@boson:~/doc/mail/tutorial/smtpclient$ twistd -ny smtpclient-5.tac
19:42 EST [-] Log opened.
19:42 EST [-] twistd SVN-Trunk (/usr/bin/python2.4 2.4.1) starting up
19:42 EST [-] reactor class: twisted.internet.selectreactor.SelectReactor
19:42 EST [-] Loading smtpclient-5.tac...
19:42 EST [-] Loaded.
19:42 EST [-] Starting factory <twisted.internet.protocol.ClientFactory
instance at 0xb791e54c>
19:42 EST [-] Enabling Multithreading.
19:42 EST [Uninitialized] Traceback (most recent call last):
File "twisted/python/log.py", line 56, in callWithLogger
return callWithContext({"system": lp}, func, *args, **kw)
File "twisted/python/log.py", line 41, in callWithContext
return context.call({ILogContext: newCtx}, func, *args, **kw)
File "twisted/python/context.py", line 52, in callWithContext
return self.currentContext().callWithContext(ctx, func, *args, **kw)
File "twisted/python/context.py", line 31, in callWithContext
return func(*args,**kw)
--- <exception caught here> ---
File "twisted/internet/selectreactor.py", line 139, in _doReadOrWrite
why = getattr(selectable, method)()
File "twisted/internet/tcp.py", line 543, in doConnect
self._connectDone()
File "twisted/internet/tcp.py", line 546, in _connectDone
self.protocol = self.connector.buildProtocol(self.getPeer())
File "twisted/internet/base.py", line 641, in buildProtocol
return self.factory.buildProtocol(addr)
File "twisted/internet/protocol.py", line 99, in buildProtocol
p = self.protocol()
exceptions.TypeError: __init__() takes at least 2 arguments (1 given)
19:42 EST [Uninitialized] Stopping factory
<twisted.internet.protocol.ClientFactory instance at
0xb791e54c>
19:43 EST [-] Received SIGINT, shutting down.
19:43 EST [-] Main loop terminated.
19:43 EST [-] Server Shut Down.
exarkun@boson:~/doc/mail/tutorial/smtpclient$
糟糕,又回到了获取回溯。这次,buildProtocol
的默认实现似乎不再足够。它在没有参数的情况下实例化协议,但 ESMTPClient
至少需要一个参数。在客户端的下一个版本中,我们将覆盖 buildProtocol
来解决这个问题。
SMTP 客户端 6¶
smtpclient-6.tac
引入了一个 twisted.internet.protocol.ClientFactory
子类,它具有覆盖的 buildProtocol
方法来克服前面示例中遇到的问题。
class SMTPClientFactory(protocol.ClientFactory):
protocol = smtp.ESMTPClient
def buildProtocol(self, addr):
return self.protocol(secret=None, identity='example.com')
覆盖的方法与基本实现几乎相同:唯一的变化是它将两个参数的值传递给 twisted.mail.smtp.ESMTPClient
的初始化程序。 secret
参数用于 SMTP 身份验证(我们还没有尝试)。 identity
参数用作标识我们自己的身份。另一个需要注意的细微变化是, protocol
属性现在在类定义中定义,而不是在创建实例后附加到实例上。这意味着它现在是一个类属性,而不是一个实例属性,就这个例子而言,这没有区别。在某些情况下,这种差异很重要:在创建自己的工厂时,请确保您了解每种方法的含义。
还需要进行一项更改:我们现在将实例化 SMTPClientFactory
,而不是实例化 twisted.internet.protocol.ClientFactory
。
smtpClientFactory = SMTPClientFactory()
运行此版本的代码,我们观察到代码仍然不是完全没有回溯。
exarkun@boson:~/doc/mail/tutorial/smtpclient$ twistd -ny smtpclient-6.tac
21:17 EST [-] Log opened.
21:17 EST [-] twistd SVN-Trunk (/usr/bin/python2.4 2.4.1) starting up
21:17 EST [-] reactor class: twisted.internet.selectreactor.SelectReactor
21:17 EST [-] Loading smtpclient-6.tac...
21:17 EST [-] Loaded.
21:17 EST [-] Starting factory <__builtin__.SMTPClientFactory instance
at 0xb77fd68c>
21:17 EST [-] Enabling Multithreading.
21:17 EST [ESMTPClient,client] Traceback (most recent call last):
File "twisted/python/log.py", line 56, in callWithLogger
return callWithContext({"system": lp}, func, *args, **kw)
File "twisted/python/log.py", line 41, in callWithContext
return context.call({ILogContext: newCtx}, func, *args, **kw)
File "twisted/python/context.py", line 52, in callWithContext
return self.currentContext().callWithContext(ctx, func, *args, **kw)
File "twisted/python/context.py", line 31, in callWithContext
return func(*args,**kw)
--- <exception caught here> ---
File "twisted/internet/selectreactor.py", line 139, in _doReadOrWrite
why = getattr(selectable, method)()
File "twisted/internet/tcp.py", line 351, in doRead
return self.protocol.dataReceived(data)
File "twisted/protocols/basic.py", line 221, in dataReceived
why = self.lineReceived(line)
File "twisted/mail/smtp.py", line 1039, in lineReceived
why = self._okresponse(self.code,'\n'.join(self.resp))
File "twisted/mail/smtp.py", line 1281, in esmtpState_serverConfig
self.tryTLS(code, resp, items)
File "twisted/mail/smtp.py", line 1294, in tryTLS
self.authenticate(code, resp, items)
File "twisted/mail/smtp.py", line 1343, in authenticate
self.smtpState_from(code, resp)
File "twisted/mail/smtp.py", line 1062, in smtpState_from
self._from = self.getMailFrom()
File "twisted/mail/smtp.py", line 1137, in getMailFrom
raise NotImplementedError
exceptions.NotImplementedError:
21:17 EST [ESMTPClient,client] Stopping factory
<__builtin__.SMTPClientFactory instance at 0xb77fd68c>
21:17 EST [-] Received SIGINT, shutting down.
21:17 EST [-] Main loop terminated.
21:17 EST [-] Server Shut Down.
exarkun@boson:~/doc/mail/tutorial/smtpclient$
我们通过这个版本的示例所完成的是,在 SMTP 事务中导航到足够远的地方,Twisted 现在有兴趣回调到应用程序级代码以确定其下一步应该是什么。在下一个示例中,我们将看到如何向它提供这些信息。
SMTP 客户端 7¶
SMTP 客户端 7 是我们的 SMTP 客户端的第一个版本,它实际上包含要传输的消息数据。为了简单起见,消息被定义为新类的一部分。在一个发送电子邮件的有用程序中,消息数据可能从文件系统、数据库中提取,或者根据用户输入生成。 smtpclient-7.tac
然而,定义了一个新类, SMTPTutorialClient
,它具有三个类属性(mailFrom
、mailTo
和 mailData
)
class SMTPTutorialClient(smtp.ESMTPClient):
mailFrom = "[email protected]"
mailTo = "[email protected]"
mailData = '''\
Date: Fri, 6 Feb 2004 10:14:39 -0800
From: Tutorial Guy <[email protected]>
To: Tutorial Gal <[email protected]>
Subject: Tutorate!
Hello, how are you, goodbye.
'''
此静态定义的数据稍后在类定义中被 SMTPClient 回调 API 的三个方法访问。Twisted 期望以下三个方法中的每一个都被定义并返回具有特定含义的对象。首先, getMailFrom
def getMailFrom(self):
result = self.mailFrom
self.mailFrom = None
return result
此方法用于确定消息的 反向路径,也称为 信封发件人。此值将在发送 MAIL FROM
SMTP 命令时使用。该方法必须返回一个字符串,该字符串符合 RFC 2821 对 反向路径 的定义。简单来说,它应该是一个像 "[email protected]"
这样的字符串。SMTP 协议只允许一个 信封发件人,因此它不能是字符串列表或地址的逗号分隔列表。我们对 getMailFrom
的实现比仅仅返回一个字符串做得更多;我们稍后会回到这一点。
下一个方法是 getMailTo
def getMailTo(self):
return [self.mailTo]
getMailTo
与 getMailFrom
类似。它返回一个或多个 RFC 2821 地址(这次是 正向路径 或 信封收件人)。由于 SMTP 允许多个收件人,因此 getMailTo
返回这些地址的列表。该列表必须包含至少一个地址,即使只有一个收件人,它也必须在列表中。
我们将定义的最后一个回调方法是 getMailData
def getMailData(self):
return StringIO.StringIO(self.mailData)
这个也很简单:它返回一个文件或类文件对象,其中包含消息内容。在我们的例子中,我们返回一个 StringIO
,因为我们已经有一个包含我们消息的字符串。如果 getMailData
返回的文件的内容跨越多行(就像电子邮件消息通常那样),那么这些行应该用 \n
分隔(就像在 "rt"
模式下打开文本文件时一样):必要的换行符转换将由 SMTPClient
自动执行。
在 smtpclient-7.tac 中定义了一个新的回调方法。这个方法不是为了向 Twisted 提供有关消息的信息,而是为了让 Twisted 向应用程序提供有关消息传输成功或失败的信息
def sentMail(self, code, resp, numOk, addresses, log):
print('Sent', numOk, 'messages')
sentMail
的每个参数都提供了一些关于消息传输事务成功或失败的信息。 code
是最终命令的响应代码。对于成功的事务,它将是 250。对于瞬态故障(应该重试的故障),它将在 400 到 499 之间(包括 400 和 499)。对于永久性故障(无论重试多少次都无法正常工作),它将在 500 到 599 之间。
SMTP 客户端 8¶
到目前为止,我们已经成功地创建了一个 Twisted 客户端应用程序,它启动、连接到(可能)远程主机、传输一些数据并断开连接。但是,值得注意的是,应用程序关闭缺失。在开发过程中,按下 ^C 很好,但这并不是一个长期的解决方案。幸运的是,程序化关闭非常简单。 smtpclient-8.tac
使用这两行扩展了 sentMail
from twisted.internet import reactor
reactor.stop()
反应器的 stop
方法会导致主事件循环退出,从而允许 Twisted 服务器关闭。使用此版本的示例,我们看到程序在发送消息后实际上会终止,无需用户干预。
exarkun@boson:~/doc/mail/tutorial/smtpclient$ twistd -ny smtpclient-8.tac
19:52 EST [-] Log opened.
19:52 EST [-] twistd SVN-Trunk (/usr/bin/python2.4 2.4.1) starting up
19:52 EST [-] reactor class: twisted.internet.selectreactor.SelectReactor
19:52 EST [-] Loading smtpclient-8.tac...
19:52 EST [-] Loaded.
19:52 EST [-] Starting factory <__builtin__.SMTPClientFactory instance
at 0xb791beec>
19:52 EST [-] Enabling Multithreading.
19:52 EST [SMTPTutorialClient,client] Sent 1 messages
19:52 EST [SMTPTutorialClient,client] Stopping factory
<__builtin__.SMTPClientFactory instance at 0xb791beec>
19:52 EST [-] Main loop terminated.
19:52 EST [-] Server Shut Down.
exarkun@boson:~/doc/mail/tutorial/smtpclient$
SMTP 客户端 9¶
在本教程 SMTP 客户端中,还有一项任务需要完成:我们不会始终通过知名主机发送邮件,而是会查找收件人地址的邮件交换服务器,并尝试将消息传递到该主机。
在 smtpclient-9.tac
中,我们将通过定义一个函数来实现此功能的第一步,该函数返回特定域的邮件交换主机
def getMailExchange(host):
return 'localhost'
显然,这还没有返回正确的邮件交换主机(实际上,它返回了我们一直使用的相同主机),但是将确定要连接到哪个主机的逻辑提取到像这样的函数中,是朝着最终目标迈出的第一步。现在我们有了 getMailExchange
,我们将在构建 TCPClient
服务时调用它
smtpClientService = internet.TCPClient(
getMailExchange('example.net'), 25, smtpClientFactory)
我们将在下一个示例中扩展 getMailExchange
的定义。
SMTP 客户端 10¶
在前面的示例中,我们定义了 getMailExchange
来返回一个字符串,表示特定域的邮件交换主机。虽然这朝着正确的方向迈出了一步,但事实证明,这并不是很大的一步。确定特定域的邮件交换主机将涉及网络流量(特别是,一些 DNS 请求)。这些可能需要任意长的时间,因此我们需要引入一个 Deferred
来表示 getMailExchange
的结果。 smtpclient-10.tac
重新定义了它
def getMailExchange(host):
return defer.succeed('localhost')
defer.succeed
是一个函数,它创建一个新的 Deferred
,该 Deferred
已经有一个结果,在本例中是 'localhost'
。现在我们需要调整我们的 TCPClient
构建代码,以预期并正确处理此 Deferred
def cbMailExchange(exchange):
smtpClientFactory = SMTPClientFactory()
smtpClientService = internet.TCPClient(exchange, 25, smtpClientFactory)
smtpClientService.setServiceParent(application)
getMailExchange('example.net').addCallback(cbMailExchange)
对 Deferred
的深入探讨超出了本文档的范围。有关此类内容,请参阅 Deferred 参考 TCPClient
直到 Deferred
由 getMailExchange
返回并触发。一旦触发,我们将正常地继续创建我们的 SMTPClientFactory
和 TCPClient
,以及设置 TCPClient
的服务父级,就像我们在前面的示例中所做的那样。
SMTP 客户端 11¶
最后,我们准备执行邮件交换查找。我们通过调用专门为此任务提供的对象来实现这一点,即 twisted.mail.relaymanager.MXCalculator
。
def getMailExchange(host):
def cbMX(mxRecord):
return str(mxRecord.name)
return relaymanager.MXCalculator().getMX(host).addCallback(cbMX)
由于 getMX
返回的是 Record_MX
对象而不是字符串,因此我们进行了一些后处理以获得我们想要的结果。我们已经将本教程应用程序的其余部分转换为期望 Deferred
来自 getMailExchange
,因此不需要进一步更改。 smtpclient-11.tac
通过能够查找收件人域的邮件交换主机、连接到它、完成 SMTP 事务、报告其结果并最终关闭反应器来完成本教程。