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 文件的第二行创建一个新的应用程序服务,并将其绑定到本地名称 applicationtwistd 在它运行的每个 .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。不用担心 EESMTP 中。它表明我们实际上正在使用 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,它具有三个类属性(mailFrommailTomailData

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]

getMailTogetMailFrom 类似。它返回一个或多个 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 直到 DeferredgetMailExchange 返回并触发。一旦触发,我们将正常地继续创建我们的 SMTPClientFactoryTCPClient,以及设置 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 事务、报告其结果并最终关闭反应器来完成本教程。