smtplib.py 18.1 KB
Newer Older
1 2
#! /usr/bin/env python

3
'''SMTP/ESMTP client class.
4

5
This should follow RFC 821 (SMTP) and RFC 1869 (ESMTP).
6

7 8 9
Notes:

Please remember, when doing ESMTP, that the names of the SMTP service
10
extensions are NOT the same thing as the option keywords for the RCPT
11 12
and MAIL commands!

13 14
Example:

15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
  >>> import smtplib
  >>> s=smtplib.SMTP("localhost")
  >>> print s.help()
  This is Sendmail version 8.8.4
  Topics:
      HELO    EHLO    MAIL    RCPT    DATA
      RSET    NOOP    QUIT    HELP    VRFY
      EXPN    VERB    ETRN    DSN
  For more info use "HELP <topic>".
  To report bugs in the implementation send email to
      sendmail-bugs@sendmail.org.
  For local information send email to Postmaster at your site.
  End of HELP info
  >>> s.putcmd("vrfy","someone@here")
  >>> s.getreply()
  (250, "Somebody OverHere <somebody@here.my.org>")
  >>> s.quit()
32
'''
33

34 35 36 37 38
# Author: The Dragon De Monsyne <dragondm@integral.org>
# ESMTP support, test code and doc fixes added by
#     Eric S. Raymond <esr@thyrsus.com>
# Better RFC 821 compliance (MAIL and RCPT, and CRLF in data)
#     by Carey Evans <c.evans@clear.net.nz>, for picky mail servers.
Tim Peters's avatar
Tim Peters committed
39
#
40 41
# This was modified from the Python 1.5 library HTTP lib.

42
import socket
43
import re
44
import rfc822
45
import types
46

47 48 49 50 51
__all__ = ["SMTPException","SMTPServerDisconnected","SMTPResponseException",
           "SMTPSenderRefused","SMTPRecipientsRefused","SMTPDataError",
           "SMTPConnectError","SMTPHeloError","quoteaddr","quotedata",
           "SMTP"]

52 53 54
SMTP_PORT = 25
CRLF="\r\n"

Tim Peters's avatar
Tim Peters committed
55
# Exception classes used by this module.
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
class SMTPException(Exception):
    """Base class for all exceptions raised by this module."""

class SMTPServerDisconnected(SMTPException):
    """Not connected to any SMTP server.

    This exception is raised when the server unexpectedly disconnects,
    or when an attempt is made to use the SMTP instance before
    connecting it to a server.
    """

class SMTPResponseException(SMTPException):
    """Base class for all exceptions that include an SMTP error code.

    These exceptions are generated in some instances when the SMTP
    server returns an error code.  The error code is stored in the
    `smtp_code' attribute of the error, and the `smtp_error' attribute
    is set to the error message.
    """

    def __init__(self, code, msg):
        self.smtp_code = code
        self.smtp_error = msg
        self.args = (code, msg)

class SMTPSenderRefused(SMTPResponseException):
    """Sender address refused.
    In addition to the attributes set by on all SMTPResponseException
84
    exceptions, this sets `sender' to the string that the SMTP refused.
85 86 87 88 89 90 91 92
    """

    def __init__(self, code, msg, sender):
        self.smtp_code = code
        self.smtp_error = msg
        self.sender = sender
        self.args = (code, msg, sender)

93
class SMTPRecipientsRefused(SMTPException):
94
    """All recipient addresses refused.
95
    The errors for each recipient are accessible through the attribute
Tim Peters's avatar
Tim Peters committed
96 97
    'recipients', which is a dictionary of exactly the same sort as
    SMTP.sendmail() returns.
98 99 100 101 102 103 104 105 106 107 108
    """

    def __init__(self, recipients):
        self.recipients = recipients
        self.args = ( recipients,)


class SMTPDataError(SMTPResponseException):
    """The SMTP server didn't accept the data."""

class SMTPConnectError(SMTPResponseException):
109
    """Error during connection establishment."""
110 111

class SMTPHeloError(SMTPResponseException):
112
    """The server refused our HELO reply."""
113

114

115 116 117
def quoteaddr(addr):
    """Quote a subset of the email addresses defined by RFC 821.

118 119
    Should be able to handle anything rfc822.parseaddr can handle.
    """
120
    m=None
121
    try:
122 123 124 125 126 127 128 129
        m=rfc822.parseaddr(addr)[1]
    except AttributeError:
        pass
    if not m:
        #something weird here.. punt -ddm
        return addr
    else:
        return "<%s>" % m
130 131 132 133

def quotedata(data):
    """Quote data for email.

134
    Double leading '.', and change Unix newline '\\n', or Mac '\\r' into
135 136
    Internet CRLF end-of-line.
    """
137
    return re.sub(r'(?m)^\.', '..',
138
        re.sub(r'(?:\r\n|\n|\r(?!\n))', CRLF, data))
139

140

141
class SMTP:
142 143
    """This class manages a connection to an SMTP or ESMTP server.
    SMTP Objects:
Tim Peters's avatar
Tim Peters committed
144 145 146
        SMTP objects have the following attributes:
            helo_resp
                This is the message given by the server in response to the
147
                most recent HELO command.
Tim Peters's avatar
Tim Peters committed
148

149
            ehlo_resp
Tim Peters's avatar
Tim Peters committed
150
                This is the message given by the server in response to the
151 152
                most recent EHLO command. This is usually multiline.

Tim Peters's avatar
Tim Peters committed
153
            does_esmtp
154 155 156
                This is a True value _after you do an EHLO command_, if the
                server supports ESMTP.

Tim Peters's avatar
Tim Peters committed
157
            esmtp_features
158
                This is a dictionary, which, if the server supports ESMTP,
159 160
                will _after you do an EHLO command_, contain the names of the
                SMTP service extensions this server supports, and their
161
                parameters (if any).
162

Tim Peters's avatar
Tim Peters committed
163 164
                Note, all extension names are mapped to lower case in the
                dictionary.
165

166 167 168 169
        See each method's docstrings for details.  In general, there is a
        method of the same name to perform each SMTP command.  There is also a
        method called 'sendmail' that will do an entire mail transaction.
        """
170 171 172 173
    debuglevel = 0
    file = None
    helo_resp = None
    ehlo_resp = None
174
    does_esmtp = 0
175

176 177 178
    def __init__(self, host = '', port = 0):
        """Initialize a new instance.

179 180
        If specified, `host' is the name of the remote host to which to
        connect.  If specified, `port' specifies the port to which to connect.
181 182
        By default, smtplib.SMTP_PORT is used.  An SMTPConnectError is raised
        if the specified `host' doesn't respond correctly.
183 184

        """
185
        self.esmtp_features = {}
186 187 188 189
        if host:
            (code, msg) = self.connect(host, port)
            if code != 220:
                raise SMTPConnectError(code, msg)
Tim Peters's avatar
Tim Peters committed
190

191 192 193
    def set_debuglevel(self, debuglevel):
        """Set the debug output level.

194 195
        A non-false value results in debug messages for connection and for all
        messages sent to and received from the server.
196 197 198 199 200 201

        """
        self.debuglevel = debuglevel

    def connect(self, host='localhost', port = 0):
        """Connect to a host on a given port.
202

203 204 205
        If the hostname ends with a colon (`:') followed by a number, and
        there is no port specified, that suffix will be stripped off and the
        number interpreted as the port number to use.
206

207 208
        Note: This method is automatically invoked by __init__, if a host is
        specified during instantiation.
209 210 211

        """
        if not port:
212
            i = host.find(':')
213 214
            if i >= 0:
                host, port = host[:i], host[i+1:]
215
                try: port = int(port)
216
                except ValueError:
217 218 219 220
                    raise socket.error, "nonnumeric port"
        if not port: port = SMTP_PORT
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        if self.debuglevel > 0: print 'connect:', (host, port)
221 222 223 224 225
        try:
            self.sock.connect((host, port))
        except socket.error:
            self.close()
            raise
226 227
        (code,msg)=self.getreply()
        if self.debuglevel >0 : print "connect:", msg
228
        return (code,msg)
Tim Peters's avatar
Tim Peters committed
229

230 231 232
    def send(self, str):
        """Send `str' to the server."""
        if self.debuglevel > 0: print 'send:', `str`
233
        if self.sock:
234
            try:
235 236 237
                sendptr = 0
                while sendptr < len(str):
                    sendptr = sendptr + self.sock.send(str[sendptr:])
238
            except socket.error:
239
                raise SMTPServerDisconnected('Server not connected')
Guido van Rossum's avatar
Guido van Rossum committed
240
        else:
241
            raise SMTPServerDisconnected('please run connect() first')
Tim Peters's avatar
Tim Peters committed
242

243
    def putcmd(self, cmd, args=""):
244
        """Send a command to the server."""
245 246 247 248
        if args == "":
            str = '%s%s' % (cmd, CRLF)
        else:
            str = '%s %s%s' % (cmd, args, CRLF)
249
        self.send(str)
Tim Peters's avatar
Tim Peters committed
250

251
    def getreply(self):
252
        """Get a reply from the server.
Tim Peters's avatar
Tim Peters committed
253

254
        Returns a tuple consisting of:
255 256 257 258 259 260

          - server response code (e.g. '250', or such, if all goes well)
            Note: returns -1 if it can't read response code.

          - server response string corresponding to response code (multiline
            responses are converted to a single, multiline string).
261 262

        Raises SMTPServerDisconnected if end-of-file is reached.
263 264
        """
        resp=[]
265 266
        if self.file is None:
            self.file = self.sock.makefile('rb')
267
        while 1:
268
            line = self.file.readline()
269 270 271
            if line == '':
                self.close()
                raise SMTPServerDisconnected("Connection unexpectedly closed")
272
            if self.debuglevel > 0: print 'reply:', `line`
273
            resp.append(line[4:].strip())
274
            code=line[:3]
275 276 277
            # Check that the error code is syntactically correct.
            # Don't attempt to read a continuation line if it is broken.
            try:
278
                errcode = int(code)
279 280 281
            except ValueError:
                errcode = -1
                break
282
            # Check if multiline response.
283
            if line[3:4]!="-":
284 285
                break

286
        errmsg = "\n".join(resp)
Tim Peters's avatar
Tim Peters committed
287
        if self.debuglevel > 0:
Guido van Rossum's avatar
Guido van Rossum committed
288
            print 'reply: retcode (%s); Msg: %s' % (errcode,errmsg)
289
        return errcode, errmsg
Tim Peters's avatar
Tim Peters committed
290

291
    def docmd(self, cmd, args=""):
292
        """Send a command, and return its response code."""
293
        self.putcmd(cmd,args)
294
        return self.getreply()
295

296
    # std smtp commands
297
    def helo(self, name=''):
298 299 300 301
        """SMTP 'helo' command.
        Hostname to send for this command defaults to the FQDN of the local
        host.
        """
302 303 304
        if name:
            self.putcmd("helo", name)
        else:
305
            self.putcmd("helo", socket.getfqdn())
306 307
        (code,msg)=self.getreply()
        self.helo_resp=msg
308
        return (code,msg)
309

310
    def ehlo(self, name=''):
311 312 313 314
        """ SMTP 'ehlo' command.
        Hostname to send for this command defaults to the FQDN of the local
        host.
        """
315 316 317
        if name:
            self.putcmd("ehlo", name)
        else:
318
            self.putcmd("ehlo", socket.getfqdn())
319
        (code,msg)=self.getreply()
Tim Peters's avatar
Tim Peters committed
320 321
        # According to RFC1869 some (badly written)
        # MTA's will disconnect on an ehlo. Toss an exception if
322 323
        # that happens -ddm
        if code == -1 and len(msg) == 0:
324
            raise SMTPServerDisconnected("Server not connected")
325
        self.ehlo_resp=msg
326
        if code != 250:
327
            return (code,msg)
328
        self.does_esmtp=1
329
        #parse the ehlo response -ddm
330
        resp=self.ehlo_resp.split('\n')
331
        del resp[0]
332
        for each in resp:
333 334
            m=re.match(r'(?P<feature>[A-Za-z0-9][A-Za-z0-9\-]*)',each)
            if m:
335 336
                feature=m.group("feature").lower()
                params=m.string[m.end("feature"):].strip()
337
                self.esmtp_features[feature]=params
338
        return (code,msg)
339

340 341
    def has_extn(self, opt):
        """Does the server support a given SMTP service extension?"""
342
        return self.esmtp_features.has_key(opt.lower())
343

344
    def help(self, args=''):
345 346
        """SMTP 'help' command.
        Returns help text from server."""
347
        self.putcmd("help", args)
348
        return self.getreply()
349 350

    def rset(self):
351
        """SMTP 'rset' command -- resets session."""
352
        return self.docmd("rset")
353 354

    def noop(self):
355
        """SMTP 'noop' command -- doesn't do anything :>"""
356
        return self.docmd("noop")
357

358
    def mail(self,sender,options=[]):
359
        """SMTP 'mail' command -- begins mail xfer session."""
360 361
        optionlist = ''
        if options and self.does_esmtp:
362
            optionlist = ' ' + ' '.join(options)
363
        self.putcmd("mail", "FROM:%s%s" % (quoteaddr(sender) ,optionlist))
364
        return self.getreply()
365

366
    def rcpt(self,recip,options=[]):
367
        """SMTP 'rcpt' command -- indicates 1 recipient for this mail."""
368 369
        optionlist = ''
        if options and self.does_esmtp:
370
            optionlist = ' ' + ' '.join(options)
371
        self.putcmd("rcpt","TO:%s%s" % (quoteaddr(recip),optionlist))
372
        return self.getreply()
373 374

    def data(self,msg):
Tim Peters's avatar
Tim Peters committed
375
        """SMTP 'DATA' command -- sends message data to server.
376

377
        Automatically quotes lines beginning with a period per rfc821.
378 379 380
        Raises SMTPDataError if there is an unexpected reply to the
        DATA command; the return value from this method is the final
        response code received when the all data is sent.
381
        """
382 383 384
        self.putcmd("data")
        (code,repl)=self.getreply()
        if self.debuglevel >0 : print "data:", (code,repl)
385
        if code != 354:
386
            raise SMTPDataError(code,repl)
387
        else:
388 389 390 391 392
            q = quotedata(msg)
            if q[-2:] != CRLF:
                q = q + CRLF
            q = q + "." + CRLF
            self.send(q)
393 394
            (code,msg)=self.getreply()
            if self.debuglevel >0 : print "data:", (code,msg)
395
            return (code,msg)
396

397
    def verify(self, address):
398
        """SMTP 'verify' command -- checks for address validity."""
399 400
        self.putcmd("vrfy", quoteaddr(address))
        return self.getreply()
401 402
    # a.k.a.
    vrfy=verify
403 404

    def expn(self, address):
405
        """SMTP 'verify' command -- checks for address validity."""
406 407 408
        self.putcmd("expn", quoteaddr(address))
        return self.getreply()

409
    # some useful methods
410
    def sendmail(self, from_addr, to_addrs, msg, mail_options=[],
Tim Peters's avatar
Tim Peters committed
411 412
                 rcpt_options=[]):
        """This command performs an entire mail transaction.
413

Tim Peters's avatar
Tim Peters committed
414
        The arguments are:
415 416 417
            - from_addr    : The address sending this mail.
            - to_addrs     : A list of addresses to send this mail to.  A bare
                             string will be treated as a list with 1 address.
Tim Peters's avatar
Tim Peters committed
418
            - msg          : The message to send.
419 420 421 422 423 424 425 426 427 428 429
            - mail_options : List of ESMTP options (such as 8bitmime) for the
                             mail command.
            - rcpt_options : List of ESMTP options (such as DSN commands) for
                             all the rcpt commands.

        If there has been no previous EHLO or HELO command this session, this
        method tries ESMTP EHLO first.  If the server does ESMTP, message size
        and each of the specified options will be passed to it.  If EHLO
        fails, HELO will be tried and ESMTP options suppressed.

        This method will return normally if the mail is accepted for at least
430 431 432
        one recipient.  It returns a dictionary, with one entry for each
        recipient that was refused.  Each entry contains a tuple of the SMTP
        error code and the accompanying error message sent by the server.
433 434 435 436

        This method may raise the following exceptions:

         SMTPHeloError          The server didn't reply properly to
Tim Peters's avatar
Tim Peters committed
437
                                the helo greeting.
438
         SMTPRecipientsRefused  The server rejected ALL recipients
439 440 441 442 443 444 445
                                (no mail was sent).
         SMTPSenderRefused      The server didn't accept the from_addr.
         SMTPDataError          The server replied with an unexpected
                                error code (other than a refusal of
                                a recipient).

        Note: the connection will be open even after an exception is raised.
446

447
        Example:
Tim Peters's avatar
Tim Peters committed
448

449 450
         >>> import smtplib
         >>> s=smtplib.SMTP("localhost")
Guido van Rossum's avatar
Guido van Rossum committed
451
         >>> tolist=["one@one.org","two@two.org","three@three.org","four@four.org"]
452 453 454 455 456 457 458 459
         >>> msg = '''
         ... From: Me@my.org
         ... Subject: testin'...
         ...
         ... This is a test '''
         >>> s.sendmail("me@my.org",tolist,msg)
         { "three@three.org" : ( 550 ,"User unknown" ) }
         >>> s.quit()
Tim Peters's avatar
Tim Peters committed
460

461 462
        In the above example, the message was accepted for delivery to three
        of the four addresses, and one was rejected, with the error code
463
        550.  If all addresses are accepted, then the method will return an
464 465 466
        empty dictionary.

        """
467 468 469 470 471
        if self.helo_resp is None and self.ehlo_resp is None:
            if not (200 <= self.ehlo()[0] <= 299):
                (code,resp) = self.helo()
                if not (200 <= code <= 299):
                    raise SMTPHeloError(code, resp)
472
        esmtp_opts = []
473 474 475 476 477 478
        if self.does_esmtp:
            # Hmmm? what's this? -ddm
            # self.esmtp_features['7bit']=""
            if self.has_extn('size'):
                esmtp_opts.append("size=" + `len(msg)`)
            for option in mail_options:
479
                esmtp_opts.append(option)
480

481
        (code,resp) = self.mail(from_addr, esmtp_opts)
482
        if code != 250:
483
            self.rset()
484
            raise SMTPSenderRefused(code, resp, from_addr)
485
        senderrs={}
486 487
        if type(to_addrs) == types.StringType:
            to_addrs = [to_addrs]
488
        for each in to_addrs:
489
            (code,resp)=self.rcpt(each, rcpt_options)
490
            if (code != 250) and (code != 251):
Guido van Rossum's avatar
Guido van Rossum committed
491
                senderrs[each]=(code,resp)
492
        if len(senderrs)==len(to_addrs):
493
            # the server refused all our recipients
494
            self.rset()
495
            raise SMTPRecipientsRefused(senderrs)
496 497
        (code,resp) = self.data(msg)
        if code != 250:
498
            self.rset()
499
            raise SMTPDataError(code, resp)
500
        #if we got here then somebody got our mail
Tim Peters's avatar
Tim Peters committed
501
        return senderrs
502 503 504 505 506 507 508 509 510 511 512 513 514


    def close(self):
        """Close the connection to the SMTP server."""
        if self.file:
            self.file.close()
        self.file = None
        if self.sock:
            self.sock.close()
        self.sock = None


    def quit(self):
515
        """Terminate the SMTP session."""
516 517
        self.docmd("quit")
        self.close()
518

519

520 521 522 523 524 525 526
# Test the sendmail method, which tests most of the others.
# Note: This always sends to localhost.
if __name__ == '__main__':
    import sys, rfc822

    def prompt(prompt):
        sys.stdout.write(prompt + ": ")
527
        return sys.stdin.readline().strip()
528 529

    fromaddr = prompt("From")
530
    toaddrs  = prompt("To").split(',')
531 532 533 534 535 536 537 538 539 540 541 542 543
    print "Enter message, end with ^D:"
    msg = ''
    while 1:
        line = sys.stdin.readline()
        if not line:
            break
        msg = msg + line
    print "Message length is " + `len(msg)`

    server = SMTP('localhost')
    server.set_debuglevel(1)
    server.sendmail(fromaddr, toaddrs, msg)
    server.quit()