Ticket #4572: smtpclient-old.rst

File smtpclient-old.rst, 29.3 KB (added by jdb, 4 years ago)

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