smtplib.py 12.3 KB
Newer Older
1 2
#!/usr/bin/python
"""SMTP/ESMTP client class.
3 4

Author: The Dragon De Monsyne <dragondm@integral.org>
5 6
ESMTP support, test code and doc fixes added by
Eric S. Raymond <esr@thyrsus.com>
7 8
Better RFC 821 compliance (MAIL and RCPT, and CRLF in data)
by Carey Evans <c.evans@clear.net.nz>, for picky mail servers.
9 10 11

(This was modified from the Python 1.5 library HTTP lib.)

12
This should follow RFC 821 (SMTP) and RFC 1869 (ESMTP).
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42

Example:

>>> 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()

"""

import socket
import string,re

SMTP_PORT = 25
CRLF="\r\n"

# used for exceptions 
Guido van Rossum's avatar
Guido van Rossum committed
43
SMTPServerDisconnected="Server not connected"
44 45
SMTPSenderRefused="Sender address refused"
SMTPRecipientsRefused="All Recipients refused"
46
SMTPDataError="Error transmitting message data"
47

48 49 50 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
def quoteaddr(addr):
    """Quote a subset of the email addresses defined by RFC 821.

    Technically, only a <mailbox> is allowed.  In addition,
    email addresses without a domain are permitted.

    Addresses will not be modified if they are already quoted
    (actually if they begin with '<' and end with '>'."""
    if re.match('(?s)\A<.*>\Z', addr):
        return addr

    localpart = None
    domain = ''
    try:
        at = string.rindex(addr, '@')
        localpart = addr[:at]
        domain = addr[at:]
    except ValueError:
        localpart = addr

    pat = re.compile(r'([<>()\[\]\\,;:@\"\001-\037\177])')
    return '<%s%s>' % (pat.sub(r'\\\1', localpart), domain)

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

    Double leading '.', and change Unix newline '\n' into
    Internet CRLF end-of-line."""
    return re.sub(r'(?m)^\.', '..',
                  re.sub(r'\r?\n', CRLF, data))

79
class SMTP:
80 81 82 83 84 85 86
    """This class manages a connection to an SMTP or ESMTP server."""
    debuglevel = 0
    file = None
    helo_resp = None
    ehlo_resp = None
    esmtp_features = []

87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
    def __init__(self, host = '', port = 0):
        """Initialize a new instance.

        If specified, `host' is the name of the remote host to which
        to connect.  If specified, `port' specifies the port to which
        to connect.  By default, smtplib.SMTP_PORT is used.

        """
        if host: self.connect(host, port)
    
    def set_debuglevel(self, debuglevel):
        """Set the debug output level.

        A non-false value results in debug messages for connection and
        for all messages sent to and received from the server.

        """
        self.debuglevel = debuglevel

106 107 108 109 110
    def verify(self, address):
        """ SMTP 'verify' command. Checks for address validity. """
        self.putcmd("vrfy", address)
        return self.getreply()

111 112
    def connect(self, host='localhost', port = 0):
        """Connect to a host on a given port.
113 114 115 116 117

        If the hostname ends with a colon (`:') followed by a number,
        that suffix will be stripped off and the number interpreted as
        the port number to use.

118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
        Note:  This method is automatically invoked by __init__,
        if a host is specified during instantiation.

        """
        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)
        self.sock.connect(host, port)
133 134 135
        (code,msg)=self.getreply()
        if self.debuglevel >0 : print "connect:", msg
        return msg
136 137 138 139
    
    def send(self, str):
        """Send `str' to the server."""
        if self.debuglevel > 0: print 'send:', `str`
140
        if self.sock:
Guido van Rossum's avatar
Guido van Rossum committed
141 142
            self.sock.send(str)
        else:
143
            raise SMTPServerDisconnected
Guido van Rossum's avatar
Guido van Rossum committed
144
 
145 146 147 148 149 150
    def putcmd(self, cmd, args=""):
        """Send a command to the server.
        """
        str = '%s %s%s' % (cmd, args, CRLF)
        self.send(str)
    
151
    def getreply(self, linehook=None):
152 153 154 155
        """Get a reply from the server.
        
        Returns a tuple consisting of:
        - server response code (e.g. '250', or such, if all goes well)
156
          Note: returns -1 if it can't read response code.
157
        - server response string corresponding to response code
158
                (note : multiline responses converted to a single, 
Guido van Rossum's avatar
Guido van Rossum committed
159
                 multiline string)
160 161
        """
        resp=[]
162 163
        self.file = self.sock.makefile('rb')
        while 1:
164 165
            line = self.file.readline()
            if self.debuglevel > 0: print 'reply:', `line`
166 167 168 169
            resp.append(string.strip(line[4:]))
            code=line[:3]
            #check if multiline resp
            if line[3:4]!="-":
170
                break
171 172
            elif linehook:
                linehook(line)
173 174 175
        try:
            errcode = string.atoi(code)
        except(ValueError):
176
            errcode = -1
177

178 179
        errmsg = string.join(resp,"\n")
        if self.debuglevel > 0: 
Guido van Rossum's avatar
Guido van Rossum committed
180
            print 'reply: retcode (%s); Msg: %s' % (errcode,errmsg)
181 182 183
        return errcode, errmsg
    
    def docmd(self, cmd, args=""):
184
        """ Send a command, and return its response code """
185 186 187 188
        
        self.putcmd(cmd,args)
        (code,msg)=self.getreply()
        return code
189 190 191 192 193
# std smtp commands

    def helo(self, name=''):
        """ SMTP 'helo' command. Hostname to send for this command  
        defaults to the FQDN of the local host """
194 195 196 197 198 199 200
        name=string.strip(name)
        if len(name)==0:
                name=socket.gethostbyaddr(socket.gethostname())[0]
        self.putcmd("helo",name)
        (code,msg)=self.getreply()
        self.helo_resp=msg
        return code
201

202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
    def ehlo(self, name=''):
        """ SMTP 'ehlo' command. Hostname to send for this command  
        defaults to the FQDN of the local host """
        name=string.strip(name)
        if len(name)==0:
                name=socket.gethostbyaddr(socket.gethostname())[0]
        self.putcmd("ehlo",name)
        (code,msg)=self.getreply(self.ehlo_hook)
        self.ehlo_resp=msg
        return code

    def ehlo_hook(self, line):
        # Interpret EHLO response lines
        if line[4] in string.uppercase+string.digits:
            self.esmtp_features.append(string.lower(string.strip(line)[4:]))

    def has_option(self, opt):
        """Does the server support a given SMTP option?"""
        return opt in self.esmtp_features

222
    def help(self, args=''):
223
        """ SMTP 'help' command. Returns help text from server """
224
        self.putcmd("help", args)
225 226
        (code,msg)=self.getreply()
        return msg
227 228 229

    def rset(self):
        """ SMTP 'rset' command. Resets session. """
230 231
        code=self.docmd("rset")
        return code
232 233

    def noop(self):
234
        """ SMTP 'noop' command. Doesn't do anything :> """
235 236
        code=self.docmd("noop")
        return code
237

238
    def mail(self,sender,options=[]):
239
        """ SMTP 'mail' command. Begins mail xfer session. """
240 241 242 243
        if options:
            options = " " + string.joinfields(options, ' ')
        else:
            options = ''
244
        self.putcmd("mail", "from:" + quoteaddr(sender) + options)
245
        return self.getreply()
246 247 248

    def rcpt(self,recip):
        """ SMTP 'rcpt' command. Indicates 1 recipient for this mail. """
249
        self.putcmd("rcpt","to:%s" % quoteaddr(recip))
250
        return self.getreply()
251 252 253

    def data(self,msg):
        """ SMTP 'DATA' command. Sends message data to server. 
254
            Automatically quotes lines beginning with a period per rfc821. """
255 256 257 258 259 260
        self.putcmd("data")
        (code,repl)=self.getreply()
        if self.debuglevel >0 : print "data:", (code,repl)
        if code <> 354:
            return -1
        else:
261 262
            self.send(quotedata(msg))
            self.send("%s.%s" % (CRLF, CRLF))
263 264
            (code,msg)=self.getreply()
            if self.debuglevel >0 : print "data:", (code,msg)
265 266
            return code

267 268
#some useful methods
    def sendmail(self,from_addr,to_addrs,msg,options=[]):
269
        """ This command performs an entire mail transaction. 
270
            The arguments are: 
271 272 273
               - from_addr : The address sending this mail.
               - to_addrs :  a list of addresses to send this mail to
               - msg : the message to send. 
274
               - encoding : list of ESMTP options (such as 8bitmime)
275

276 277 278 279 280
	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 the
        option is in the feature set the server advertises).  If EHLO fails,
        HELO will be tried and ESMTP options suppressed.
281

282 283 284
        This method will return normally if the mail is accepted for at least 
        one recipient. Otherwise it will throw an exception (either
        SMTPSenderRefused, SMTPRecipientsRefused, or SMTPDataError)
285
        That is, if this method does not throw an exception, then someone 
286 287
        should get your mail.  If this method does not throw an exception,
        it returns a dictionary, with one entry for each recipient that was 
Guido van Rossum's avatar
Guido van Rossum committed
288
        refused. 
289

290
        Example:
291 292 293
      
         >>> import smtplib
         >>> s=smtplib.SMTP("localhost")
Guido van Rossum's avatar
Guido van Rossum committed
294
         >>> tolist=["one@one.org","two@two.org","three@three.org","four@four.org"]
295 296 297 298 299 300 301 302 303
         >>> 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()
        
Guido van Rossum's avatar
Guido van Rossum committed
304 305 306 307
         In the above example, the message was accepted for delivery to 
         three of the four addresses, and one was rejected, with the error
         code 550. If all addresses are accepted, then the method
         will return an empty dictionary.  
308
         """
309 310 311 312 313 314 315 316 317 318 319 320 321
        if not self.helo_resp and not self.ehlo_resp:
            if self.ehlo() >= 400:
                self.helo()
        if self.esmtp_features:
            self.esmtp_features.append('7bit')
        esmtp_opts = []
        if 'size' in self.esmtp_features:
            esmtp_opts.append("size=" + `len(msg)`)
        for option in options:
            if option in self.esmtp_features:
                esmtp_opts.append(option)
        (code,resp) = self.mail(from_addr, esmtp_opts)
        if code <> 250:
322 323 324
            self.rset()
            raise SMTPSenderRefused
        senderrs={}
325
        for each in to_addrs:
326 327
            (code,resp)=self.rcpt(each)
            if (code <> 250) and (code <> 251):
Guido van Rossum's avatar
Guido van Rossum committed
328
                senderrs[each]=(code,resp)
329
        if len(senderrs)==len(to_addrs):
330
            # the server refused all our recipients
331 332 333
            self.rset()
            raise SMTPRecipientsRefused
        code=self.data(msg)
334
        if code <>250 :
335
            self.rset()
336
            raise SMTPDataError
337
        #if we got here then somebody got our mail
338
        return senderrs         
339 340 341 342 343 344 345 346 347 348 349 350 351


    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):
352
        """Terminate the SMTP session."""
353 354
        self.docmd("quit")
        self.close()
355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381

# 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()