]> git.lizzy.rs Git - plan9front.git/blob - sys/lib/python/smtplib.py
turn ptrdiff_t into a 64 bit type
[plan9front.git] / sys / lib / python / smtplib.py
1 #! /usr/bin/env python
2
3 '''SMTP/ESMTP client class.
4
5 This should follow RFC 821 (SMTP), RFC 1869 (ESMTP), RFC 2554 (SMTP
6 Authentication) and RFC 2487 (Secure SMTP over TLS).
7
8 Notes:
9
10 Please remember, when doing ESMTP, that the names of the SMTP service
11 extensions are NOT the same thing as the option keywords for the RCPT
12 and MAIL commands!
13
14 Example:
15
16   >>> import smtplib
17   >>> s=smtplib.SMTP("localhost")
18   >>> print s.help()
19   This is Sendmail version 8.8.4
20   Topics:
21       HELO    EHLO    MAIL    RCPT    DATA
22       RSET    NOOP    QUIT    HELP    VRFY
23       EXPN    VERB    ETRN    DSN
24   For more info use "HELP <topic>".
25   To report bugs in the implementation send email to
26       sendmail-bugs@sendmail.org.
27   For local information send email to Postmaster at your site.
28   End of HELP info
29   >>> s.putcmd("vrfy","someone@here")
30   >>> s.getreply()
31   (250, "Somebody OverHere <somebody@here.my.org>")
32   >>> s.quit()
33 '''
34
35 # Author: The Dragon De Monsyne <dragondm@integral.org>
36 # ESMTP support, test code and doc fixes added by
37 #     Eric S. Raymond <esr@thyrsus.com>
38 # Better RFC 821 compliance (MAIL and RCPT, and CRLF in data)
39 #     by Carey Evans <c.evans@clear.net.nz>, for picky mail servers.
40 # RFC 2554 (authentication) support by Gerhard Haering <gerhard@bigfoot.de>.
41 #
42 # This was modified from the Python 1.5 library HTTP lib.
43
44 import socket
45 import re
46 import email.Utils
47 import base64
48 import hmac
49 from email.base64MIME import encode as encode_base64
50 from sys import stderr
51
52 __all__ = ["SMTPException","SMTPServerDisconnected","SMTPResponseException",
53            "SMTPSenderRefused","SMTPRecipientsRefused","SMTPDataError",
54            "SMTPConnectError","SMTPHeloError","SMTPAuthenticationError",
55            "quoteaddr","quotedata","SMTP"]
56
57 SMTP_PORT = 25
58 CRLF="\r\n"
59
60 OLDSTYLE_AUTH = re.compile(r"auth=(.*)", re.I)
61
62 # Exception classes used by this module.
63 class SMTPException(Exception):
64     """Base class for all exceptions raised by this module."""
65
66 class SMTPServerDisconnected(SMTPException):
67     """Not connected to any SMTP server.
68
69     This exception is raised when the server unexpectedly disconnects,
70     or when an attempt is made to use the SMTP instance before
71     connecting it to a server.
72     """
73
74 class SMTPResponseException(SMTPException):
75     """Base class for all exceptions that include an SMTP error code.
76
77     These exceptions are generated in some instances when the SMTP
78     server returns an error code.  The error code is stored in the
79     `smtp_code' attribute of the error, and the `smtp_error' attribute
80     is set to the error message.
81     """
82
83     def __init__(self, code, msg):
84         self.smtp_code = code
85         self.smtp_error = msg
86         self.args = (code, msg)
87
88 class SMTPSenderRefused(SMTPResponseException):
89     """Sender address refused.
90
91     In addition to the attributes set by on all SMTPResponseException
92     exceptions, this sets `sender' to the string that the SMTP refused.
93     """
94
95     def __init__(self, code, msg, sender):
96         self.smtp_code = code
97         self.smtp_error = msg
98         self.sender = sender
99         self.args = (code, msg, sender)
100
101 class SMTPRecipientsRefused(SMTPException):
102     """All recipient addresses refused.
103
104     The errors for each recipient are accessible through the attribute
105     'recipients', which is a dictionary of exactly the same sort as
106     SMTP.sendmail() returns.
107     """
108
109     def __init__(self, recipients):
110         self.recipients = recipients
111         self.args = ( recipients,)
112
113
114 class SMTPDataError(SMTPResponseException):
115     """The SMTP server didn't accept the data."""
116
117 class SMTPConnectError(SMTPResponseException):
118     """Error during connection establishment."""
119
120 class SMTPHeloError(SMTPResponseException):
121     """The server refused our HELO reply."""
122
123 class SMTPAuthenticationError(SMTPResponseException):
124     """Authentication error.
125
126     Most probably the server didn't accept the username/password
127     combination provided.
128     """
129
130 class SSLFakeSocket:
131     """A fake socket object that really wraps a SSLObject.
132
133     It only supports what is needed in smtplib.
134     """
135     def __init__(self, realsock, sslobj):
136         self.realsock = realsock
137         self.sslobj = sslobj
138
139     def send(self, str):
140         self.sslobj.write(str)
141         return len(str)
142
143     sendall = send
144
145     def close(self):
146         self.realsock.close()
147
148 class SSLFakeFile:
149     """A fake file like object that really wraps a SSLObject.
150
151     It only supports what is needed in smtplib.
152     """
153     def __init__(self, sslobj):
154         self.sslobj = sslobj
155
156     def readline(self):
157         str = ""
158         chr = None
159         while chr != "\n":
160             chr = self.sslobj.read(1)
161             str += chr
162         return str
163
164     def close(self):
165         pass
166
167 def quoteaddr(addr):
168     """Quote a subset of the email addresses defined by RFC 821.
169
170     Should be able to handle anything rfc822.parseaddr can handle.
171     """
172     m = (None, None)
173     try:
174         m = email.Utils.parseaddr(addr)[1]
175     except AttributeError:
176         pass
177     if m == (None, None): # Indicates parse failure or AttributeError
178         # something weird here.. punt -ddm
179         return "<%s>" % addr
180     elif m is None:
181         # the sender wants an empty return address
182         return "<>"
183     else:
184         return "<%s>" % m
185
186 def quotedata(data):
187     """Quote data for email.
188
189     Double leading '.', and change Unix newline '\\n', or Mac '\\r' into
190     Internet CRLF end-of-line.
191     """
192     return re.sub(r'(?m)^\.', '..',
193         re.sub(r'(?:\r\n|\n|\r(?!\n))', CRLF, data))
194
195
196 class SMTP:
197     """This class manages a connection to an SMTP or ESMTP server.
198     SMTP Objects:
199         SMTP objects have the following attributes:
200             helo_resp
201                 This is the message given by the server in response to the
202                 most recent HELO command.
203
204             ehlo_resp
205                 This is the message given by the server in response to the
206                 most recent EHLO command. This is usually multiline.
207
208             does_esmtp
209                 This is a True value _after you do an EHLO command_, if the
210                 server supports ESMTP.
211
212             esmtp_features
213                 This is a dictionary, which, if the server supports ESMTP,
214                 will _after you do an EHLO command_, contain the names of the
215                 SMTP service extensions this server supports, and their
216                 parameters (if any).
217
218                 Note, all extension names are mapped to lower case in the
219                 dictionary.
220
221         See each method's docstrings for details.  In general, there is a
222         method of the same name to perform each SMTP command.  There is also a
223         method called 'sendmail' that will do an entire mail transaction.
224         """
225     debuglevel = 0
226     file = None
227     helo_resp = None
228     ehlo_resp = None
229     does_esmtp = 0
230
231     def __init__(self, host = '', port = 0, local_hostname = None):
232         """Initialize a new instance.
233
234         If specified, `host' is the name of the remote host to which to
235         connect.  If specified, `port' specifies the port to which to connect.
236         By default, smtplib.SMTP_PORT is used.  An SMTPConnectError is raised
237         if the specified `host' doesn't respond correctly.  If specified,
238         `local_hostname` is used as the FQDN of the local host.  By default,
239         the local hostname is found using socket.getfqdn().
240
241         """
242         self.esmtp_features = {}
243         if host:
244             (code, msg) = self.connect(host, port)
245             if code != 220:
246                 raise SMTPConnectError(code, msg)
247         if local_hostname is not None:
248             self.local_hostname = local_hostname
249         else:
250             # RFC 2821 says we should use the fqdn in the EHLO/HELO verb, and
251             # if that can't be calculated, that we should use a domain literal
252             # instead (essentially an encoded IP address like [A.B.C.D]).
253             fqdn = socket.getfqdn()
254             if '.' in fqdn:
255                 self.local_hostname = fqdn
256             else:
257                 # We can't find an fqdn hostname, so use a domain literal
258                 addr = '127.0.0.1'
259                 try:
260                     addr = socket.gethostbyname(socket.gethostname())
261                 except socket.gaierror:
262                     pass
263                 self.local_hostname = '[%s]' % addr
264
265     def set_debuglevel(self, debuglevel):
266         """Set the debug output level.
267
268         A non-false value results in debug messages for connection and for all
269         messages sent to and received from the server.
270
271         """
272         self.debuglevel = debuglevel
273
274     def connect(self, host='localhost', port = 0):
275         """Connect to a host on a given port.
276
277         If the hostname ends with a colon (`:') followed by a number, and
278         there is no port specified, that suffix will be stripped off and the
279         number interpreted as the port number to use.
280
281         Note: This method is automatically invoked by __init__, if a host is
282         specified during instantiation.
283
284         """
285         if not port and (host.find(':') == host.rfind(':')):
286             i = host.rfind(':')
287             if i >= 0:
288                 host, port = host[:i], host[i+1:]
289                 try: port = int(port)
290                 except ValueError:
291                     raise socket.error, "nonnumeric port"
292         if not port: port = SMTP_PORT
293         if self.debuglevel > 0: print>>stderr, 'connect:', (host, port)
294         msg = "getaddrinfo returns an empty list"
295         self.sock = None
296         for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
297             af, socktype, proto, canonname, sa = res
298             try:
299                 self.sock = socket.socket(af, socktype, proto)
300                 if self.debuglevel > 0: print>>stderr, 'connect:', sa
301                 self.sock.connect(sa)
302             except socket.error, msg:
303                 if self.debuglevel > 0: print>>stderr, 'connect fail:', msg
304                 if self.sock:
305                     self.sock.close()
306                 self.sock = None
307                 continue
308             break
309         if not self.sock:
310             raise socket.error, msg
311         (code, msg) = self.getreply()
312         if self.debuglevel > 0: print>>stderr, "connect:", msg
313         return (code, msg)
314
315     def send(self, str):
316         """Send `str' to the server."""
317         if self.debuglevel > 0: print>>stderr, 'send:', repr(str)
318         if self.sock:
319             try:
320                 self.sock.sendall(str)
321             except socket.error:
322                 self.close()
323                 raise SMTPServerDisconnected('Server not connected')
324         else:
325             raise SMTPServerDisconnected('please run connect() first')
326
327     def putcmd(self, cmd, args=""):
328         """Send a command to the server."""
329         if args == "":
330             str = '%s%s' % (cmd, CRLF)
331         else:
332             str = '%s %s%s' % (cmd, args, CRLF)
333         self.send(str)
334
335     def getreply(self):
336         """Get a reply from the server.
337
338         Returns a tuple consisting of:
339
340           - server response code (e.g. '250', or such, if all goes well)
341             Note: returns -1 if it can't read response code.
342
343           - server response string corresponding to response code (multiline
344             responses are converted to a single, multiline string).
345
346         Raises SMTPServerDisconnected if end-of-file is reached.
347         """
348         resp=[]
349         if self.file is None:
350             self.file = self.sock.makefile('rb')
351         while 1:
352             line = self.file.readline()
353             if line == '':
354                 self.close()
355                 raise SMTPServerDisconnected("Connection unexpectedly closed")
356             if self.debuglevel > 0: print>>stderr, 'reply:', repr(line)
357             resp.append(line[4:].strip())
358             code=line[:3]
359             # Check that the error code is syntactically correct.
360             # Don't attempt to read a continuation line if it is broken.
361             try:
362                 errcode = int(code)
363             except ValueError:
364                 errcode = -1
365                 break
366             # Check if multiline response.
367             if line[3:4]!="-":
368                 break
369
370         errmsg = "\n".join(resp)
371         if self.debuglevel > 0:
372             print>>stderr, 'reply: retcode (%s); Msg: %s' % (errcode,errmsg)
373         return errcode, errmsg
374
375     def docmd(self, cmd, args=""):
376         """Send a command, and return its response code."""
377         self.putcmd(cmd,args)
378         return self.getreply()
379
380     # std smtp commands
381     def helo(self, name=''):
382         """SMTP 'helo' command.
383         Hostname to send for this command defaults to the FQDN of the local
384         host.
385         """
386         self.putcmd("helo", name or self.local_hostname)
387         (code,msg)=self.getreply()
388         self.helo_resp=msg
389         return (code,msg)
390
391     def ehlo(self, name=''):
392         """ SMTP 'ehlo' command.
393         Hostname to send for this command defaults to the FQDN of the local
394         host.
395         """
396         self.esmtp_features = {}
397         self.putcmd("ehlo", name or self.local_hostname)
398         (code,msg)=self.getreply()
399         # According to RFC1869 some (badly written)
400         # MTA's will disconnect on an ehlo. Toss an exception if
401         # that happens -ddm
402         if code == -1 and len(msg) == 0:
403             self.close()
404             raise SMTPServerDisconnected("Server not connected")
405         self.ehlo_resp=msg
406         if code != 250:
407             return (code,msg)
408         self.does_esmtp=1
409         #parse the ehlo response -ddm
410         resp=self.ehlo_resp.split('\n')
411         del resp[0]
412         for each in resp:
413             # To be able to communicate with as many SMTP servers as possible,
414             # we have to take the old-style auth advertisement into account,
415             # because:
416             # 1) Else our SMTP feature parser gets confused.
417             # 2) There are some servers that only advertise the auth methods we
418             #    support using the old style.
419             auth_match = OLDSTYLE_AUTH.match(each)
420             if auth_match:
421                 # This doesn't remove duplicates, but that's no problem
422                 self.esmtp_features["auth"] = self.esmtp_features.get("auth", "") \
423                         + " " + auth_match.groups(0)[0]
424                 continue
425
426             # RFC 1869 requires a space between ehlo keyword and parameters.
427             # It's actually stricter, in that only spaces are allowed between
428             # parameters, but were not going to check for that here.  Note
429             # that the space isn't present if there are no parameters.
430             m=re.match(r'(?P<feature>[A-Za-z0-9][A-Za-z0-9\-]*) ?',each)
431             if m:
432                 feature=m.group("feature").lower()
433                 params=m.string[m.end("feature"):].strip()
434                 if feature == "auth":
435                     self.esmtp_features[feature] = self.esmtp_features.get(feature, "") \
436                             + " " + params
437                 else:
438                     self.esmtp_features[feature]=params
439         return (code,msg)
440
441     def has_extn(self, opt):
442         """Does the server support a given SMTP service extension?"""
443         return opt.lower() in self.esmtp_features
444
445     def help(self, args=''):
446         """SMTP 'help' command.
447         Returns help text from server."""
448         self.putcmd("help", args)
449         return self.getreply()[1]
450
451     def rset(self):
452         """SMTP 'rset' command -- resets session."""
453         return self.docmd("rset")
454
455     def noop(self):
456         """SMTP 'noop' command -- doesn't do anything :>"""
457         return self.docmd("noop")
458
459     def mail(self,sender,options=[]):
460         """SMTP 'mail' command -- begins mail xfer session."""
461         optionlist = ''
462         if options and self.does_esmtp:
463             optionlist = ' ' + ' '.join(options)
464         self.putcmd("mail", "FROM:%s%s" % (quoteaddr(sender) ,optionlist))
465         return self.getreply()
466
467     def rcpt(self,recip,options=[]):
468         """SMTP 'rcpt' command -- indicates 1 recipient for this mail."""
469         optionlist = ''
470         if options and self.does_esmtp:
471             optionlist = ' ' + ' '.join(options)
472         self.putcmd("rcpt","TO:%s%s" % (quoteaddr(recip),optionlist))
473         return self.getreply()
474
475     def data(self,msg):
476         """SMTP 'DATA' command -- sends message data to server.
477
478         Automatically quotes lines beginning with a period per rfc821.
479         Raises SMTPDataError if there is an unexpected reply to the
480         DATA command; the return value from this method is the final
481         response code received when the all data is sent.
482         """
483         self.putcmd("data")
484         (code,repl)=self.getreply()
485         if self.debuglevel >0 : print>>stderr, "data:", (code,repl)
486         if code != 354:
487             raise SMTPDataError(code,repl)
488         else:
489             q = quotedata(msg)
490             if q[-2:] != CRLF:
491                 q = q + CRLF
492             q = q + "." + CRLF
493             self.send(q)
494             (code,msg)=self.getreply()
495             if self.debuglevel >0 : print>>stderr, "data:", (code,msg)
496             return (code,msg)
497
498     def verify(self, address):
499         """SMTP 'verify' command -- checks for address validity."""
500         self.putcmd("vrfy", quoteaddr(address))
501         return self.getreply()
502     # a.k.a.
503     vrfy=verify
504
505     def expn(self, address):
506         """SMTP 'verify' command -- checks for address validity."""
507         self.putcmd("expn", quoteaddr(address))
508         return self.getreply()
509
510     # some useful methods
511
512     def login(self, user, password):
513         """Log in on an SMTP server that requires authentication.
514
515         The arguments are:
516             - user:     The user name to authenticate with.
517             - password: The password for the authentication.
518
519         If there has been no previous EHLO or HELO command this session, this
520         method tries ESMTP EHLO first.
521
522         This method will return normally if the authentication was successful.
523
524         This method may raise the following exceptions:
525
526          SMTPHeloError            The server didn't reply properly to
527                                   the helo greeting.
528          SMTPAuthenticationError  The server didn't accept the username/
529                                   password combination.
530          SMTPException            No suitable authentication method was
531                                   found.
532         """
533
534         def encode_cram_md5(challenge, user, password):
535             challenge = base64.decodestring(challenge)
536             response = user + " " + hmac.HMAC(password, challenge).hexdigest()
537             return encode_base64(response, eol="")
538
539         def encode_plain(user, password):
540             return encode_base64("\0%s\0%s" % (user, password), eol="")
541
542
543         AUTH_PLAIN = "PLAIN"
544         AUTH_CRAM_MD5 = "CRAM-MD5"
545         AUTH_LOGIN = "LOGIN"
546
547         if self.helo_resp is None and self.ehlo_resp is None:
548             if not (200 <= self.ehlo()[0] <= 299):
549                 (code, resp) = self.helo()
550                 if not (200 <= code <= 299):
551                     raise SMTPHeloError(code, resp)
552
553         if not self.has_extn("auth"):
554             raise SMTPException("SMTP AUTH extension not supported by server.")
555
556         # Authentication methods the server supports:
557         authlist = self.esmtp_features["auth"].split()
558
559         # List of authentication methods we support: from preferred to
560         # less preferred methods. Except for the purpose of testing the weaker
561         # ones, we prefer stronger methods like CRAM-MD5:
562         preferred_auths = [AUTH_CRAM_MD5, AUTH_PLAIN, AUTH_LOGIN]
563
564         # Determine the authentication method we'll use
565         authmethod = None
566         for method in preferred_auths:
567             if method in authlist:
568                 authmethod = method
569                 break
570
571         if authmethod == AUTH_CRAM_MD5:
572             (code, resp) = self.docmd("AUTH", AUTH_CRAM_MD5)
573             if code == 503:
574                 # 503 == 'Error: already authenticated'
575                 return (code, resp)
576             (code, resp) = self.docmd(encode_cram_md5(resp, user, password))
577         elif authmethod == AUTH_PLAIN:
578             (code, resp) = self.docmd("AUTH",
579                 AUTH_PLAIN + " " + encode_plain(user, password))
580         elif authmethod == AUTH_LOGIN:
581             (code, resp) = self.docmd("AUTH",
582                 "%s %s" % (AUTH_LOGIN, encode_base64(user, eol="")))
583             if code != 334:
584                 raise SMTPAuthenticationError(code, resp)
585             (code, resp) = self.docmd(encode_base64(password, eol=""))
586         elif authmethod is None:
587             raise SMTPException("No suitable authentication method found.")
588         if code not in (235, 503):
589             # 235 == 'Authentication successful'
590             # 503 == 'Error: already authenticated'
591             raise SMTPAuthenticationError(code, resp)
592         return (code, resp)
593
594     def starttls(self, keyfile = None, certfile = None):
595         """Puts the connection to the SMTP server into TLS mode.
596
597         If the server supports TLS, this will encrypt the rest of the SMTP
598         session. If you provide the keyfile and certfile parameters,
599         the identity of the SMTP server and client can be checked. This,
600         however, depends on whether the socket module really checks the
601         certificates.
602         """
603         (resp, reply) = self.docmd("STARTTLS")
604         if resp == 220:
605             sslobj = socket.ssl(self.sock, keyfile, certfile)
606             self.sock = SSLFakeSocket(self.sock, sslobj)
607             self.file = SSLFakeFile(sslobj)
608         return (resp, reply)
609
610     def sendmail(self, from_addr, to_addrs, msg, mail_options=[],
611                  rcpt_options=[]):
612         """This command performs an entire mail transaction.
613
614         The arguments are:
615             - from_addr    : The address sending this mail.
616             - to_addrs     : A list of addresses to send this mail to.  A bare
617                              string will be treated as a list with 1 address.
618             - msg          : The message to send.
619             - mail_options : List of ESMTP options (such as 8bitmime) for the
620                              mail command.
621             - rcpt_options : List of ESMTP options (such as DSN commands) for
622                              all the rcpt commands.
623
624         If there has been no previous EHLO or HELO command this session, this
625         method tries ESMTP EHLO first.  If the server does ESMTP, message size
626         and each of the specified options will be passed to it.  If EHLO
627         fails, HELO will be tried and ESMTP options suppressed.
628
629         This method will return normally if the mail is accepted for at least
630         one recipient.  It returns a dictionary, with one entry for each
631         recipient that was refused.  Each entry contains a tuple of the SMTP
632         error code and the accompanying error message sent by the server.
633
634         This method may raise the following exceptions:
635
636          SMTPHeloError          The server didn't reply properly to
637                                 the helo greeting.
638          SMTPRecipientsRefused  The server rejected ALL recipients
639                                 (no mail was sent).
640          SMTPSenderRefused      The server didn't accept the from_addr.
641          SMTPDataError          The server replied with an unexpected
642                                 error code (other than a refusal of
643                                 a recipient).
644
645         Note: the connection will be open even after an exception is raised.
646
647         Example:
648
649          >>> import smtplib
650          >>> s=smtplib.SMTP("localhost")
651          >>> tolist=["one@one.org","two@two.org","three@three.org","four@four.org"]
652          >>> msg = '''\\
653          ... From: Me@my.org
654          ... Subject: testin'...
655          ...
656          ... This is a test '''
657          >>> s.sendmail("me@my.org",tolist,msg)
658          { "three@three.org" : ( 550 ,"User unknown" ) }
659          >>> s.quit()
660
661         In the above example, the message was accepted for delivery to three
662         of the four addresses, and one was rejected, with the error code
663         550.  If all addresses are accepted, then the method will return an
664         empty dictionary.
665
666         """
667         if self.helo_resp is None and self.ehlo_resp is None:
668             if not (200 <= self.ehlo()[0] <= 299):
669                 (code,resp) = self.helo()
670                 if not (200 <= code <= 299):
671                     raise SMTPHeloError(code, resp)
672         esmtp_opts = []
673         if self.does_esmtp:
674             # Hmmm? what's this? -ddm
675             # self.esmtp_features['7bit']=""
676             if self.has_extn('size'):
677                 esmtp_opts.append("size=%d" % len(msg))
678             for option in mail_options:
679                 esmtp_opts.append(option)
680
681         (code,resp) = self.mail(from_addr, esmtp_opts)
682         if code != 250:
683             self.rset()
684             raise SMTPSenderRefused(code, resp, from_addr)
685         senderrs={}
686         if isinstance(to_addrs, basestring):
687             to_addrs = [to_addrs]
688         for each in to_addrs:
689             (code,resp)=self.rcpt(each, rcpt_options)
690             if (code != 250) and (code != 251):
691                 senderrs[each]=(code,resp)
692         if len(senderrs)==len(to_addrs):
693             # the server refused all our recipients
694             self.rset()
695             raise SMTPRecipientsRefused(senderrs)
696         (code,resp) = self.data(msg)
697         if code != 250:
698             self.rset()
699             raise SMTPDataError(code, resp)
700         #if we got here then somebody got our mail
701         return senderrs
702
703
704     def close(self):
705         """Close the connection to the SMTP server."""
706         if self.file:
707             self.file.close()
708         self.file = None
709         if self.sock:
710             self.sock.close()
711         self.sock = None
712
713
714     def quit(self):
715         """Terminate the SMTP session."""
716         self.docmd("quit")
717         self.close()
718
719
720 # Test the sendmail method, which tests most of the others.
721 # Note: This always sends to localhost.
722 if __name__ == '__main__':
723     import sys
724
725     def prompt(prompt):
726         sys.stdout.write(prompt + ": ")
727         return sys.stdin.readline().strip()
728
729     fromaddr = prompt("From")
730     toaddrs  = prompt("To").split(',')
731     print "Enter message, end with ^D:"
732     msg = ''
733     while 1:
734         line = sys.stdin.readline()
735         if not line:
736             break
737         msg = msg + line
738     print "Message length is %d" % len(msg)
739
740     server = SMTP('localhost')
741     server.set_debuglevel(1)
742     server.sendmail(fromaddr, toaddrs, msg)
743     server.quit()