smtplib.py 18 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 39 40 41
# 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.
#    
# This was modified from the Python 1.5 library HTTP lib.

42
import socket
43 44
import string
import re
45
import rfc822
46
import types
47 48 49 50

SMTP_PORT = 25
CRLF="\r\n"

51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
# Exception classes used by this module. 
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
80
    exceptions, this sets `sender' to the string that the SMTP refused.
81 82 83 84 85 86 87 88
    """

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

89
class SMTPRecipientsRefused(SMTPException):
90
    """All recipient addresses refused.
91
    The errors for each recipient are accessible through the attribute
92 93 94 95 96 97 98 99 100 101 102 103 104
    'recipients', which is a dictionary of exactly the same sort as 
    SMTP.sendmail() returns.  
    """

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


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

class SMTPConnectError(SMTPResponseException):
105
    """Error during connection establishment."""
106 107

class SMTPHeloError(SMTPResponseException):
108
    """The server refused our HELO reply."""
109

110

111 112 113
def quoteaddr(addr):
    """Quote a subset of the email addresses defined by RFC 821.

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

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

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

136

137
class SMTP:
138 139 140 141
    """This class manages a connection to an SMTP or ESMTP server.
    SMTP Objects:
        SMTP objects have the following attributes:    
            helo_resp 
142
                This is the message given by the server in response to the 
143 144 145
                most recent HELO command.
                
            ehlo_resp
146
                This is the message given by the server in response to the 
147 148 149 150 151 152 153 154
                most recent EHLO command. This is usually multiline.

            does_esmtp 
                This is a True value _after you do an EHLO command_, if the
                server supports ESMTP.

            esmtp_features 
                This is a dictionary, which, if the server supports ESMTP,
155 156
                will _after you do an EHLO command_, contain the names of the
                SMTP service extensions this server supports, and their
157
                parameters (if any).
158 159

                Note, all extension names are mapped to lower case in the 
160 161
                dictionary. 

162 163 164 165
        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.
        """
166 167 168 169
    debuglevel = 0
    file = None
    helo_resp = None
    ehlo_resp = None
170
    does_esmtp = 0
171

172 173 174
    def __init__(self, host = '', port = 0):
        """Initialize a new instance.

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

        """
181
        self.esmtp_features = {}
182 183 184 185
        if host:
            (code, msg) = self.connect(host, port)
            if code != 220:
                raise SMTPConnectError(code, msg)
186 187 188 189
    
    def set_debuglevel(self, debuglevel):
        """Set the debug output level.

190 191
        A non-false value results in debug messages for connection and for all
        messages sent to and received from the server.
192 193 194 195 196 197

        """
        self.debuglevel = debuglevel

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

199 200 201
        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.
202

203 204
        Note: This method is automatically invoked by __init__, if a host is
        specified during instantiation.
205 206 207 208 209 210 211 212 213 214 215 216

        """
        if not port:
            i = string.find(host, ':')
            if i >= 0:
                host, port = host[:i], host[i+1:]
                try: port = string.atoi(port)
                except string.atoi_error:
                    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)
Guido van Rossum's avatar
Guido van Rossum committed
217
        self.sock.connect((host, port))
218 219
        (code,msg)=self.getreply()
        if self.debuglevel >0 : print "connect:", msg
220
        return (code,msg)
221 222 223 224
    
    def send(self, str):
        """Send `str' to the server."""
        if self.debuglevel > 0: print 'send:', `str`
225
        if self.sock:
226
            try:
227 228 229
                sendptr = 0
                while sendptr < len(str):
                    sendptr = sendptr + self.sock.send(str[sendptr:])
230
            except socket.error:
231
                raise SMTPServerDisconnected('Server not connected')
Guido van Rossum's avatar
Guido van Rossum committed
232
        else:
233
            raise SMTPServerDisconnected('please run connect() first')
Guido van Rossum's avatar
Guido van Rossum committed
234
 
235
    def putcmd(self, cmd, args=""):
236
        """Send a command to the server."""
237 238 239 240
        if args == "":
            str = '%s%s' % (cmd, CRLF)
        else:
            str = '%s %s%s' % (cmd, args, CRLF)
241 242
        self.send(str)
    
243
    def getreply(self):
244 245 246
        """Get a reply from the server.
        
        Returns a tuple consisting of:
247 248 249 250 251 252

          - 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).
253 254

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

278 279
        errmsg = string.join(resp,"\n")
        if self.debuglevel > 0: 
Guido van Rossum's avatar
Guido van Rossum committed
280
            print 'reply: retcode (%s); Msg: %s' % (errcode,errmsg)
281 282 283
        return errcode, errmsg
    
    def docmd(self, cmd, args=""):
284
        """Send a command, and return its response code."""
285
        self.putcmd(cmd,args)
286
        return self.getreply()
287

288
    # std smtp commands
289
    def helo(self, name=''):
290 291 292 293
        """SMTP 'helo' command.
        Hostname to send for this command defaults to the FQDN of the local
        host.
        """
294 295 296
        if name:
            self.putcmd("helo", name)
        else:
297
            self.putcmd("helo", socket.getfqdn())
298 299
        (code,msg)=self.getreply()
        self.helo_resp=msg
300
        return (code,msg)
301

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

332 333 334
    def has_extn(self, opt):
        """Does the server support a given SMTP service extension?"""
        return self.esmtp_features.has_key(string.lower(opt))
335

336
    def help(self, args=''):
337 338
        """SMTP 'help' command.
        Returns help text from server."""
339
        self.putcmd("help", args)
340
        return self.getreply()
341 342

    def rset(self):
343
        """SMTP 'rset' command -- resets session."""
344
        return self.docmd("rset")
345 346

    def noop(self):
347
        """SMTP 'noop' command -- doesn't do anything :>"""
348
        return self.docmd("noop")
349

350
    def mail(self,sender,options=[]):
351
        """SMTP 'mail' command -- begins mail xfer session."""
352 353
        optionlist = ''
        if options and self.does_esmtp:
354 355
            optionlist = ' ' + string.join(options, ' ')
        self.putcmd("mail", "FROM:%s%s" % (quoteaddr(sender) ,optionlist))
356
        return self.getreply()
357

358
    def rcpt(self,recip,options=[]):
359
        """SMTP 'rcpt' command -- indicates 1 recipient for this mail."""
360 361
        optionlist = ''
        if options and self.does_esmtp:
362 363
            optionlist = ' ' + string.join(options, ' ')
        self.putcmd("rcpt","TO:%s%s" % (quoteaddr(recip),optionlist))
364
        return self.getreply()
365 366

    def data(self,msg):
367
        """SMTP 'DATA' command -- sends message data to server. 
368

369
        Automatically quotes lines beginning with a period per rfc821.
370 371 372
        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.
373
        """
374 375 376 377
        self.putcmd("data")
        (code,repl)=self.getreply()
        if self.debuglevel >0 : print "data:", (code,repl)
        if code <> 354:
378
            raise SMTPDataError(code,repl)
379
        else:
380 381 382 383 384
            q = quotedata(msg)
            if q[-2:] != CRLF:
                q = q + CRLF
            q = q + "." + CRLF
            self.send(q)
385 386
            (code,msg)=self.getreply()
            if self.debuglevel >0 : print "data:", (code,msg)
387
            return (code,msg)
388

389
    def verify(self, address):
390
        """SMTP 'verify' command -- checks for address validity."""
391 392
        self.putcmd("vrfy", quoteaddr(address))
        return self.getreply()
393 394
    # a.k.a.
    vrfy=verify
395 396

    def expn(self, address):
397
        """SMTP 'verify' command -- checks for address validity."""
398 399 400
        self.putcmd("expn", quoteaddr(address))
        return self.getreply()

401
    # some useful methods
402 403
    def sendmail(self, from_addr, to_addrs, msg, mail_options=[],
                 rcpt_options=[]): 
404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
        """This command performs an entire mail transaction. 

        The arguments are: 
            - 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.
            - msg          : The message to send. 
            - 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
422 423 424
        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.
425 426 427 428 429

        This method may raise the following exceptions:

         SMTPHeloError          The server didn't reply properly to
                                the helo greeting. 
430
         SMTPRecipientsRefused  The server rejected ALL recipients
431 432 433 434 435 436 437
                                (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.
438

439
        Example:
440 441 442
      
         >>> import smtplib
         >>> s=smtplib.SMTP("localhost")
Guido van Rossum's avatar
Guido van Rossum committed
443
         >>> tolist=["one@one.org","two@two.org","three@three.org","four@four.org"]
444 445 446 447 448 449 450 451 452
         >>> 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()
        
453 454
        In the above example, the message was accepted for delivery to three
        of the four addresses, and one was rejected, with the error code
455
        550.  If all addresses are accepted, then the method will return an
456 457 458
        empty dictionary.

        """
459 460 461 462 463
        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)
464
        esmtp_opts = []
465 466 467 468 469 470
        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:
471
                esmtp_opts.append(option)
472

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


    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):
507
        """Terminate the SMTP session."""
508 509
        self.docmd("quit")
        self.close()
510

511

512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535
# 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 + ": ")
        return string.strip(sys.stdin.readline())

    fromaddr = prompt("From")
    toaddrs  = string.splitfields(prompt("To"), ',')
    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()