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