Ticket #4572: smtpclient.rst

File smtpclient.rst, 28.9 KB (added by jdb, 4 years ago)

Corrected verison of the SMTP tutorial

Line 
1
2:LastChangedDate: $LastChangedDate$
3:LastChangedRevision: $LastChangedRevision$
4:LastChangedBy: $LastChangedBy$
5
6Twisted Mail Tutorial: Building an SMTP Client from Scratch
7===========================================================
8
9Introduction
10------------
11
12This tutorial will walk you through the creation of an extremely
13simple SMTP client application.  By the time the tutorial is complete,
14you will understand how to create and start a TCP client speaking the
15SMTP protocol, have it connect to an appropriate mail exchange server,
16and transmit a message for delivery.
17
18For the majority of this tutorial, ``twistd`` will be used
19to launch the application.  Near the end we will explore other
20possibilities for starting a Twisted application.  Until then, make
21sure that you have ``twistd`` installed and conveniently
22accessible for use in running each of the example ``.tac`` 
23files.
24
25SMTP Client 1
26~~~~~~~~~~~~~
27
28The first step is to create :download:`smtpclient-1.tac` possible for
29use by ``twistd``.
30
31.. code-block:: python
32
33    from twisted.application import service
34
35The first line of the ``.tac`` file imports
36``twisted.application.service``, a module which contains many of the
37basic *service* classes and helper functions available in Twisted.  In
38particular, we will be using the ``Application`` function to create a
39new *application service*.  An *application service* simply acts as a
40central object on which to store certain kinds of deployment
41configuration.
42
43.. code-block:: python
44   
45    application = service.Application("SMTP Client Tutorial")
46
47The second line of the ``.tac`` file creates a new *application
48service* and binds it to the local name ``application``. ``twistd``
49requires this local name in each ``.tac`` file it runs. It uses
50various pieces of configuration on the object to determine its
51behavior. For example, ``"SMTP Client Tutorial"`` will be used as the
52name of the ``.tap`` file into which to serialize application state,
53should it be necessary to do so.
54
55That does it for the first example.  We now have enough of a ``.tac``
56file to pass to ``twistd``. If we run :download:`smtpclient-1.tac`
57using the``twistd`` command line:
58
59.. code-block:: python
60   
61    twistd -ny smtpclient-1.tac
62
63we are rewarded with the following output:
64
65.. code-block:: console
66
67    exarkun@boson:~/mail/tutorial/smtpclient$ twistd -ny smtpclient-1.tac
68    18:31 EST [-] Log opened.
69    18:31 EST [-] twistd 2.0.0 (/usr/bin/python2.4 2.4.1) starting up
70    18:31 EST [-] reactor class: twisted.internet.selectreactor.SelectReactor
71    18:31 EST [-] Loading smtpclient-1.tac...
72    18:31 EST [-] Loaded.
73
74As we expected, not much is going on.  We can shutdown this server
75by issuing ``^C`` :
76
77.. code-block:: console
78   
79    18:34 EST [-] Received SIGINT, shutting down.
80    18:34 EST [-] Main loop terminated.
81    18:34 EST [-] Server Shut Down.
82    exarkun@boson:~/mail/tutorial/smtpclient$
83
84SMTP Client 2
85~~~~~~~~~~~~~
86
87The first version of our SMTP client wasn't very interesting.  It
88didn't even establish any TCP connections!  The
89:download:`smtpclient-2.tac` will come a little bit closer to that
90level of complexity.  First, we need to import a few more things:
91
92.. code-block:: python
93   
94    from twisted.application import internet
95    from twisted.internet import protocol
96
97``twisted.application.internet`` is another *application service*
98module.  It provides services for establishing outgoing connections
99(as well as creating network servers, though we are not interested in
100those parts for the moment).``twisted.internet.protocol`` provides
101base implementations of many of the core Twisted concepts, such as
102*factories* and*protocols* .
103
104The next line of :download:`smtpclient-2.tac` instantiates a new
105*client factory* .
106
107.. code-block:: python
108
109    smtpClientFactory = protocol.ClientFactory()
110
111*Client factories* are responsible for constructing*protocol
112instances* whenever connections are established.  They may be required
113to create just one instance, or many instances if many different
114connections are established, or they may never be required to create
115one at all, if no connection ever manages to be established.
116
117Now that we have a client factory, we'll need to hook it up to the
118network somehow.  The next line of ``smtpclient-2.tac`` does just
119that:
120
121.. code-block:: python
122   
123    smtpClientService = internet.TCPClient(None, None, smtpClientFactory)
124
125We'll ignore the first two arguments to ``internet.TCPClient`` for the
126moment and instead focus on the third. ``TCPClient`` is one of those
127*application service* classes.  It creates TCP connections to a
128specified address and then uses its third argument, a *client
129factory*, to get a *protocol instance*.  It then associates the TCP
130connection with the protocol instance and gets out of the way.
131
132We can try to run ``smtpclient-2.tac`` the same way we
133ran ``smtpclient-1.tac``, but the results might be a little
134disappointing:
135
136.. code-block:: console
137   
138    exarkun@boson:~/mail/tutorial/smtpclient$ twistd -ny smtpclient-2.tac
139    18:55 EST [-] Log opened.
140    18:55 EST [-] twistd SVN-Trunk (/usr/bin/python2.4 2.4.1) starting up
141    18:55 EST [-] reactor class: twisted.internet.selectreactor.SelectReactor
142    18:55 EST [-] Loading smtpclient-2.tac...
143    18:55 EST [-] Loaded.
144    18:55 EST [-] Starting factory <twisted.internet.protocol.ClientFactory
145                  instance at 0xb791e46c>
146    18:55 EST [-] Traceback (most recent call last):
147              File "twisted/scripts/twistd.py", line 187, in runApp
148                app.runReactorWithLogging(config, oldstdout, oldstderr)
149              File "twisted/application/app.py", line 128, in runReactorWithLogging
150                reactor.run()
151              File "twisted/internet/posixbase.py", line 200, in run
152                self.mainLoop()
153              File "twisted/internet/posixbase.py", line 208, in mainLoop
154                self.runUntilCurrent()
155            --- <exception caught here> ---
156              File "twisted/internet/base.py", line 533, in runUntilCurrent
157                call.func(*call.args, **call.kw)
158              File "twisted/internet/tcp.py", line 489, in resolveAddress
159                if abstract.isIPAddress(self.addr[0]):
160              File "twisted/internet/abstract.py", line 315, in isIPAddress
161                parts = string.split(addr, '.')
162              File "/usr/lib/python2.4/string.py", line 292, in split
163                return s.split(sep, maxsplit)
164            exceptions.AttributeError: 'NoneType' object has no attribute 'split'
165   
166    18:55 EST [-] Received SIGINT, shutting down.
167    18:55 EST [-] Main loop terminated.
168    18:55 EST [-] Server Shut Down.
169    exarkun@boson:~/mail/tutorial/smtpclient$
170
171What happened?  Those first two arguments to ``TCPClient`` turned out
172to be important after all.  We'll get to them in the next example.
173
174SMTP Client 3
175~~~~~~~~~~~~~
176
177Version three of our SMTP client only changes one thing.  The line
178from version two:
179
180.. code-block:: python
181   
182    smtpClientService = internet.TCPClient(None, None, smtpClientFactory)
183
184has its first two arguments changed from ``None`` to something with a
185bit more meaning:
186
187.. code-block:: python
188   
189    smtpClientService = internet.TCPClient('localhost', 25, smtpClientFactory)
190
191This directs the client to connect to *localhost* on port*25* .  This
192isn't the address we want ultimately, but it's a good place-holder for
193the time being.  We can run :download:`smtpclient-3.tac` and see what
194this change gets us:
195
196.. code-block:: console
197   
198    exarkun@boson:~/mail/tutorial/smtpclient$ twistd -ny smtpclient-3.tac
199    19:10 EST [-] Log opened.
200    19:10 EST [-] twistd SVN-Trunk (/usr/bin/python2.4 2.4.1) starting up
201    19:10 EST [-] reactor class: twisted.internet.selectreactor.SelectReactor
202    19:10 EST [-] Loading smtpclient-3.tac...
203    19:10 EST [-] Loaded.
204    19:10 EST [-] Starting factory <twisted.internet.protocol.ClientFactory
205                  instance at 0xb791e48c>
206    19:10 EST [-] Enabling Multithreading.
207    19:10 EST [Uninitialized] Traceback (most recent call last):
208              File "twisted/python/log.py", line 56, in callWithLogger
209                return callWithContext({"system": lp}, func, *args, **kw)
210              File "twisted/python/log.py", line 41, in callWithContext
211                return context.call({ILogContext: newCtx}, func, *args, **kw)
212              File "twisted/python/context.py", line 52, in callWithContext
213                return self.currentContext().callWithContext(ctx, func, *args, **kw)
214              File "twisted/python/context.py", line 31, in callWithContext
215                return func(*args,**kw)
216            --- <exception caught here> ---
217              File "twisted/internet/selectreactor.py", line 139, in _doReadOrWrite
218                why = getattr(selectable, method)()
219              File "twisted/internet/tcp.py", line 543, in doConnect
220                self._connectDone()
221              File "twisted/internet/tcp.py", line 546, in _connectDone
222                self.protocol = self.connector.buildProtocol(self.getPeer())
223              File "twisted/internet/base.py", line 641, in buildProtocol
224                return self.factory.buildProtocol(addr)
225              File "twisted/internet/protocol.py", line 99, in buildProtocol
226                p = self.protocol()
227            exceptions.TypeError: 'NoneType' object is not callable
228   
229    19:10 EST [Uninitialized] Stopping factory
230              <twisted.internet.protocol.ClientFactory instance at
231              0xb791e48c>
232    19:10 EST [-] Received SIGINT, shutting down.
233    19:10 EST [-] Main loop terminated.
234    19:10 EST [-] Server Shut Down.
235    exarkun@boson:~/mail/tutorial/smtpclient$
236
237A meagre amount of progress, but the service still raises an
238exception. This time, it's because we haven't specified a *protocol
239class* for the factory to use.  We'll do that in the next example.
240
241SMTP Client 4
242~~~~~~~~~~~~~
243
244In the previous example, we ran into a problem because we hadn't set
245up our *client factory's* *protocol* attribute correctly (or at all).
246``ClientFactory.buildProtocol`` is the method responsible for creating
247a *protocol instance*.  The default implementation calls the factory's
248``protocol`` attribute, adds itself as an attribute named ``factory``
249to the resulting instance, and returns it.  In
250:download:`smtpclient-4.tac`, we'll correct the oversight that caused
251the traceback in smtpclient-3.tac:
252
253.. code-block:: python
254   
255    smtpClientFactory.protocol = protocol.Protocol
256
257Running this version of the client, we can see the output is once
258again traceback free:
259
260.. code-block:: console
261   
262    exarkun@boson:~/doc/mail/tutorial/smtpclient$ twistd -ny smtpclient-4.tac
263    19:29 EST [-] Log opened.
264    19:29 EST [-] twistd SVN-Trunk (/usr/bin/python2.4 2.4.1) starting up
265    19:29 EST [-] reactor class: twisted.internet.selectreactor.SelectReactor
266    19:29 EST [-] Loading smtpclient-4.tac...
267    19:29 EST [-] Loaded.
268    19:29 EST [-] Starting factory <twisted.internet.protocol.ClientFactory
269                  instance at 0xb791e4ac>
270    19:29 EST [-] Enabling Multithreading.
271    19:29 EST [-] Received SIGINT, shutting down.
272    19:29 EST [Protocol,client] Stopping factory
273              <twisted.internet.protocol.ClientFactory instance at
274              0xb791e4ac>
275    19:29 EST [-] Main loop terminated.
276    19:29 EST [-] Server Shut Down.
277    exarkun@boson:~/doc/mail/tutorial/smtpclient$
278
279But what does this mean?``twisted.internet.protocol.Protocol`` is the
280base*protocol* implementation.  For those familiar with the classic
281UNIX network services, it is equivalent to the *discard* service.  It
282never produces any output and it discards all its input.  Not terribly
283useful, and certainly nothing like an SMTP client.  Let's see how we
284can improve this in the next example.
285
286SMTP Client 5
287~~~~~~~~~~~~~
288
289In :download:`smtpclient-5.tac`, we will begin to use Twisted's SMTP
290protocol implementation for the first time.  We'll make the obvious
291change, simply swapping out ``twisted.internet.protocol.Protocol`` in
292favor of ``twisted.mail.smtp.ESMTPClient``.  Don't worry about the *E*
293in *ESMTP*. It indicates we're actually using a newer version of the
294SMTP protocol. There is an ``SMTPClient`` in Twisted, but there's
295essentially no reason to ever use it.
296
297smtpclient-5.tac adds a new import:
298
299.. code-block:: python
300   
301    from twisted.mail import smtp
302
303All of the mail related code in Twisted exists beneath the
304``twisted.mail`` package. More specifically, everything having to do
305with the SMTP protocol implementation is defined in the
306``twisted.mail.smtp`` module.
307
308Next we remove a line we added in smtpclient-4.tac:
309
310.. code-block:: python
311   
312    smtpClientFactory.protocol = protocol.Protocol
313
314And add a similar one in its place:
315
316.. code-block:: python
317   
318    smtpClientFactory.protocol = smtp.ESMTPClient
319
320Our client factory is now using a protocol implementation which
321behaves as an SMTP client.  What happens when we try to run this
322version?
323
324.. code-block:: console
325   
326    exarkun@boson:~/doc/mail/tutorial/smtpclient$ twistd -ny smtpclient-5.tac
327    19:42 EST [-] Log opened.
328    19:42 EST [-] twistd SVN-Trunk (/usr/bin/python2.4 2.4.1) starting up
329    19:42 EST [-] reactor class: twisted.internet.selectreactor.SelectReactor
330    19:42 EST [-] Loading smtpclient-5.tac...
331    19:42 EST [-] Loaded.
332    19:42 EST [-] Starting factory <twisted.internet.protocol.ClientFactory
333                  instance at 0xb791e54c>
334    19:42 EST [-] Enabling Multithreading.
335    19:42 EST [Uninitialized] Traceback (most recent call last):
336              File "twisted/python/log.py", line 56, in callWithLogger
337                return callWithContext({"system": lp}, func, *args, **kw)
338              File "twisted/python/log.py", line 41, in callWithContext
339                return context.call({ILogContext: newCtx}, func, *args, **kw)
340              File "twisted/python/context.py", line 52, in callWithContext
341                return self.currentContext().callWithContext(ctx, func, *args, **kw)
342              File "twisted/python/context.py", line 31, in callWithContext
343                return func(*args,**kw)
344            --- <exception caught here> ---
345              File "twisted/internet/selectreactor.py", line 139, in _doReadOrWrite
346                why = getattr(selectable, method)()
347              File "twisted/internet/tcp.py", line 543, in doConnect
348                self._connectDone()
349              File "twisted/internet/tcp.py", line 546, in _connectDone
350                self.protocol = self.connector.buildProtocol(self.getPeer())
351              File "twisted/internet/base.py", line 641, in buildProtocol
352                return self.factory.buildProtocol(addr)
353              File "twisted/internet/protocol.py", line 99, in buildProtocol
354                p = self.protocol()
355            exceptions.TypeError: __init__() takes at least 2 arguments (1 given)
356   
357    19:42 EST [Uninitialized] Stopping factory
358              <twisted.internet.protocol.ClientFactory instance at
359              0xb791e54c>
360    19:43 EST [-] Received SIGINT, shutting down.
361    19:43 EST [-] Main loop terminated.
362    19:43 EST [-] Server Shut Down.
363    exarkun@boson:~/doc/mail/tutorial/smtpclient$
364
365Oops, back to getting a traceback.  This time, the default
366implementation of ``buildProtocol`` seems no longer to be sufficient.
367It instantiates the protocol with no arguments, but ``ESMTPClient``
368wants at least one argument.  In the next version of the client, we'll
369override ``buildProtocol`` to fix this problem.
370
371SMTP Client 6
372~~~~~~~~~~~~~
373
374:download:`smtpclient-6.tac` introduces
375a ``twisted.internet.protocol.ClientFactory`` subclass with an
376overridden ``buildProtocol`` method to overcome the problem
377encountered in the previous example.
378
379.. code-block:: python
380
381    class SMTPClientFactory(protocol.ClientFactory):
382        protocol = smtp.ESMTPClient
383   
384        def buildProtocol(self, addr):
385            return self.protocol(secret=None, identity='example.com')
386
387The overridden method does almost the same thing as the base
388implementation: the only change is that it passes values for two
389arguments to ``twisted.mail.smtp.ESMTPClient`` 's initializer.
390The ``secret`` argument is used for SMTP authentication
391(which we will not attempt yet).  The ``identity`` argument
392is used as a to identify ourselves Another minor change to note is
393that the ``protocol`` attribute is now defined in the class
394definition, rather than tacked onto an instance after one is created.
395This means it is a class attribute, rather than an instance attribute,
396now, which makes no difference as far as this example is concerned.
397There are circumstances in which the difference is important: be sure
398you understand the implications of each approach when creating your
399own factories.
400
401One other change is required: instead of
402instantiating ``twisted.internet.protocol.ClientFactory`` , we
403will now instantiate ``SMTPClientFactory`` :
404
405.. code-block:: python
406   
407    smtpClientFactory = SMTPClientFactory()
408
409Running this version of the code, we observe that the code**still**
410isn't quite traceback-free.
411
412.. code-block:: console
413   
414    exarkun@boson:~/doc/mail/tutorial/smtpclient$ twistd -ny smtpclient-6.tac
415    21:17 EST [-] Log opened.
416    21:17 EST [-] twistd SVN-Trunk (/usr/bin/python2.4 2.4.1) starting up
417    21:17 EST [-] reactor class: twisted.internet.selectreactor.SelectReactor
418    21:17 EST [-] Loading smtpclient-6.tac...
419    21:17 EST [-] Loaded.
420    21:17 EST [-] Starting factory <__builtin__.SMTPClientFactory instance
421                  at 0xb77fd68c>
422    21:17 EST [-] Enabling Multithreading.
423    21:17 EST [ESMTPClient,client] Traceback (most recent call last):
424              File "twisted/python/log.py", line 56, in callWithLogger
425                return callWithContext({"system": lp}, func, *args, **kw)
426              File "twisted/python/log.py", line 41, in callWithContext
427                return context.call({ILogContext: newCtx}, func, *args, **kw)
428              File "twisted/python/context.py", line 52, in callWithContext
429                return self.currentContext().callWithContext(ctx, func, *args, **kw)
430              File "twisted/python/context.py", line 31, in callWithContext
431                return func(*args,**kw)
432            --- <exception caught here> ---
433              File "twisted/internet/selectreactor.py", line 139, in _doReadOrWrite
434                why = getattr(selectable, method)()
435              File "twisted/internet/tcp.py", line 351, in doRead
436                return self.protocol.dataReceived(data)
437              File "twisted/protocols/basic.py", line 221, in dataReceived
438                why = self.lineReceived(line)
439              File "twisted/mail/smtp.py", line 1039, in lineReceived
440                why = self._okresponse(self.code,'\n'.join(self.resp))
441              File "twisted/mail/smtp.py", line 1281, in esmtpState_serverConfig
442                self.tryTLS(code, resp, items)
443              File "twisted/mail/smtp.py", line 1294, in tryTLS
444                self.authenticate(code, resp, items)
445              File "twisted/mail/smtp.py", line 1343, in authenticate
446                self.smtpState_from(code, resp)
447              File "twisted/mail/smtp.py", line 1062, in smtpState_from
448                self._from = self.getMailFrom()
449              File "twisted/mail/smtp.py", line 1137, in getMailFrom
450                raise NotImplementedError
451            exceptions.NotImplementedError:
452   
453    21:17 EST [ESMTPClient,client] Stopping factory
454              <__builtin__.SMTPClientFactory instance at 0xb77fd68c>
455    21:17 EST [-] Received SIGINT, shutting down.
456    21:17 EST [-] Main loop terminated.
457    21:17 EST [-] Server Shut Down.
458    exarkun@boson:~/doc/mail/tutorial/smtpclient$
459
460What we have accomplished with this iteration of the example is to
461navigate far enough into an SMTP transaction that Twisted is now
462interested in calling back to application-level code to determine what
463its next step should be.  In the next example, we'll see how to
464provide that information to it.
465
466SMTP Client 7
467~~~~~~~~~~~~~
468
469SMTP Client 7 is the first version of our SMTP client which actually
470includes message data to transmit.  For simplicity's sake, the message
471is defined as part of a new class.  In a useful program which sent
472email, message data might be pulled in from the filesystem, a
473database, or be generated based on user-input.
474:download:`smtpclient-7.tac`, however, defines a new class,
475``SMTPTutorialClient`` , with three class attributes (``mailFrom``,
476``mailTo``, and ``mailData``):
477
478.. code-block:: python
479   
480    class SMTPTutorialClient(smtp.ESMTPClient):
481        mailFrom = "tutorial_sender@example.com"
482        mailTo = "tutorial_recipient@example.net"
483        mailData = '''\
484    Date: Fri, 6 Feb 2004 10:14:39 -0800
485    From: Tutorial Guy <tutorial_sender@example.com>
486    To: Tutorial Gal <tutorial_recipient@example.net>
487    Subject: Tutorate!
488   
489    Hello, how are you, goodbye.
490    '''
491
492This statically defined data is accessed later in the class definition
493by three of the methods which are part of the *SMTPClient callback
494API*.  Twisted expects each of the three methods below to be defined
495and to return an object with a particular meaning.  First,
496``getMailFrom``:
497
498.. code-block:: python
499
500    def getMailFrom(self):
501        result = self.mailFrom
502        self.mailFrom = None
503        return result
504
505This method is called to determine the *reverse-path*, otherwise
506known as the *envelope from*, of the message.  This value will be
507used when sending the ``MAIL FROM`` SMTP command.  The method must
508return a string which conforms to the `RFC 2821
509<http://www.faqs.org/rfcs/rfc2821.html>`_ definition of a
510*reverse-path*.  In simpler terms, it should be a string like
511``"alice@example.com"``.  Only one *envelope from* is allowed by the
512SMTP protocol, so it cannot be a list of strings or a comma separated
513list of addresses.  Our implementation of ``getMailFrom`` does a
514little bit more than just return a string; we'll get back to this in a
515little bit.
516
517The next method is ``getMailTo`` :
518
519.. code-block:: python
520   
521    def getMailTo(self):
522        return [self.mailTo]
523
524
525``getMailTo`` is similar to ``getMailFrom``.  It returns one or more
526RFC 2821 addresses (this time a*forward-path*, or *envelope to*).
527Since SMTP allows multiple recipients, ``getMailTo`` returns a list of
528these addresses.  The list must contain at least one address, and even
529if there is exactly one recipient, it must still be in a list.
530
531The final callback we will define to provide information to Twisted is
532``getMailData``:
533
534.. code-block:: python
535   
536    def getMailData(self):
537        return StringIO.StringIO(self.mailData)
538
539This one is quite simple as well: it returns a file or a file-like
540object which contains the message contents.  In our case, we return
541a ``StringIO`` since we already have a string containing our message.
542If the contents of the file returned by ``getMailData`` span multiple
543lines (as email messages often do), the lines should be ``\n``
544delimited (as they would be when opening a text file in the ``"rt"``
545mode): necessary newline translation will be performed by
546``SMTPClient`` automatically.
547
548There is one more new callback method defined in smtpclient-7.tac.
549This one isn't for providing information about the messages to
550Twisted, but for Twisted to provide information about the success or
551failure of the message transmission to the application:
552
553.. code-block:: python
554   
555    def sentMail(self, code, resp, numOk, addresses, log):
556        print 'Sent', numOk, 'messages'
557
558Each of the arguments to ``sentMail`` provides some information about
559the success or failure of the message transmission transaction.
560``code`` is the response code from the ultimate command.  For
561successful transactions, it will be 250.  For transient failures
562(those which should be retried), it will be between 400 and 499,
563inclusive.  For permanent failures (this which will never work, no
564matter how many times you retry them), it will be between 500 and 599.
565
566SMTP Client 8
567~~~~~~~~~~~~~
568
569Thus far we have succeeded in creating a Twisted client application
570which starts up, connects to a (possibly) remote host, transmits some
571data, and disconnects.  Notably missing, however, is application
572shutdown.  Hitting ^C is fine during development, but it's not exactly
573a long-term solution.  Fortunately, programmatic shutdown is extremely
574simple.  :download:`smtpclient-8.tac` extends``sentMail`` with these
575two lines:
576
577.. code-block:: python
578   
579    from twisted.internet import reactor
580    reactor.stop()
581
582The ``stop`` method of the reactor causes the main event loop to exit,
583allowing a Twisted server to shut down.  With this version of the
584example, we see that the program actually terminates after sending the
585message, without user-intervention:
586
587.. code-block:: console
588   
589    exarkun@boson:~/doc/mail/tutorial/smtpclient$ twistd -ny smtpclient-8.tac
590    19:52 EST [-] Log opened.
591    19:52 EST [-] twistd SVN-Trunk (/usr/bin/python2.4 2.4.1) starting up
592    19:52 EST [-] reactor class: twisted.internet.selectreactor.SelectReactor
593    19:52 EST [-] Loading smtpclient-8.tac...
594    19:52 EST [-] Loaded.
595    19:52 EST [-] Starting factory <__builtin__.SMTPClientFactory instance
596                  at 0xb791beec>
597    19:52 EST [-] Enabling Multithreading.
598    19:52 EST [SMTPTutorialClient,client] Sent 1 messages
599    19:52 EST [SMTPTutorialClient,client] Stopping factory
600              <__builtin__.SMTPClientFactory instance at 0xb791beec>
601    19:52 EST [-] Main loop terminated.
602    19:52 EST [-] Server Shut Down.
603    exarkun@boson:~/doc/mail/tutorial/smtpclient$
604
605
606SMTP Client 9
607~~~~~~~~~~~~~
608
609One task remains to be completed in this tutorial SMTP client:
610instead of always sending mail through a well-known host, we will look
611up the mail exchange server for the recipient address and try to
612deliver the message to that host.
613
614In :download:`smtpclient-9.tac`, we'll take the
615first step towards this feature by defining a function which returns
616the mail exchange host for a particular domain:
617
618.. code-block:: python
619   
620    def getMailExchange(host):
621        return 'localhost'
622
623Obviously this doesn't return the correct mail exchange host yet
624(in fact, it returns the exact same host we have been using all
625along), but pulling out the logic for determining which host to
626connect to into a function like this is the first step towards our
627ultimate goal.  Now that we have ``getMailExchange``, we'll
628call it when constructing our ``TCPClient`` service:
629
630.. code-block:: python
631   
632    smtpClientService = internet.TCPClient(
633        getMailExchange('example.net'), 25, smtpClientFactory)
634
635We'll expand on the definition of ``getMailExchange`` in
636the next example.
637
638SMTP Client 10
639~~~~~~~~~~~~~~
640
641In the previous example we defined ``getMailExchange`` to return a
642string representing the mail exchange host for a particular domain.
643While this was a step in the right direction, it turns out not to be a
644very big one.  Determining the mail exchange host for a particular
645domain is going to involve network traffic (specifically, some DNS
646requests).  These might take an arbitrarily large amount of time, so
647we need to introduce a ``Deferred`` to represent the result of
648``getMailExchange``:download:`smtpclient-10.tac` redefines it
649thusly:
650
651.. code-block:: python
652   
653    def getMailExchange(host):
654        return defer.succeed('localhost')
655
656``defer.succeed`` is a function which creates a new ``Deferred`` which
657already has a result, in this case ``'localhost'``.  Now we need to
658adjust our ``TCPClient`` -constructing code to expect and properly
659handle this ``Deferred`` :
660
661.. code-block:: python
662   
663    def cbMailExchange(exchange):
664        smtpClientFactory = SMTPClientFactory()
665   
666        smtpClientService = internet.TCPClient(exchange, 25, smtpClientFactory)
667        smtpClientService.setServiceParent(application)
668   
669    getMailExchange('example.net').addCallback(cbMailExchange)
670
671An in-depth exploration of ``Deferred``\ s is beyond the scope of this
672document.  For such a look, see the `Deferred Reference
673<../../../core/howto/defer.html>`_ ``TCPClient`` until the ``Deferred``
674returned by ``getMailExchange`` fires. Once it does, we proceed
675normally through the creation of our ``SMTPClientFactory`` and
676``TCPClient``, as well as set the ``TCPClient``\ 's service parent,
677just as we did in the previous examples.
678
679SMTP Client 11
680~~~~~~~~~~~~~~
681
682At last we're ready to perform the mail exchange lookup.  We do this
683by calling on an object provided specifically for this
684task, ``twisted.mail.relaymanager.MXCalculator``:
685
686.. code-block:: python
687   
688    def getMailExchange(host):
689        def cbMX(mxRecord):
690            return str(mxRecord.exchange)
691        return relaymanager.MXCalculator().getMX(host).addCallback(cbMX)
692
693Because ``getMX`` returns a ``Record_MX`` object rather than a string,
694we do a little bit of post-processing to get the results we want.  We
695have already converted the rest of the tutorial application to expect
696a ``Deferred`` from ``getMailExchange`` , so no further changes are
697required.  :download:`smtpclient-11.tac` completes this tutorial by
698being able to both look up the mail exchange host for the recipient
699domain, connect to it, complete an SMTP transaction, report its
700results, and finally shut down the reactor.
701
702
703
704
705
706..
707    TODO: write a conclusion
708   
709   <h3>Conclusion</h3>
710   
711   <p>XXX wrap it up</p>
712   
713