3 '''SMTP/ESMTP client class.
5 This should follow RFC 821 (SMTP), RFC 1869 (ESMTP), RFC 2554 (SMTP
6 Authentication) and RFC 2487 (Secure SMTP over TLS).
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
17 >>> s=smtplib.SMTP("localhost")
19 This is Sendmail version 8.8.4
21 HELO EHLO MAIL RCPT DATA
22 RSET NOOP QUIT HELP VRFY
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.
29 >>> s.putcmd("vrfy","someone@here")
31 (250, "Somebody OverHere <somebody@here.my.org>")
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>.
42 # This was modified from the Python 1.5 library HTTP lib.
49 from email
.base64MIME
import encode
as encode_base64
51 __all__
= ["SMTPException","SMTPServerDisconnected","SMTPResponseException",
52 "SMTPSenderRefused","SMTPRecipientsRefused","SMTPDataError",
53 "SMTPConnectError","SMTPHeloError","SMTPAuthenticationError",
54 "quoteaddr","quotedata","SMTP"]
59 OLDSTYLE_AUTH
= re
.compile(r
"auth=(.*)", re
.I
)
61 # Exception classes used by this module.
62 class SMTPException(Exception):
63 """Base class for all exceptions raised by this module."""
65 class SMTPServerDisconnected(SMTPException
):
66 """Not connected to any SMTP server.
68 This exception is raised when the server unexpectedly disconnects,
69 or when an attempt is made to use the SMTP instance before
70 connecting it to a server.
73 class SMTPResponseException(SMTPException
):
74 """Base class for all exceptions that include an SMTP error code.
76 These exceptions are generated in some instances when the SMTP
77 server returns an error code. The error code is stored in the
78 `smtp_code' attribute of the error, and the `smtp_error' attribute
79 is set to the error message.
82 def __init__(self
, code
, msg
):
85 self
.args
= (code
, msg
)
87 class SMTPSenderRefused(SMTPResponseException
):
88 """Sender address refused.
90 In addition to the attributes set by on all SMTPResponseException
91 exceptions, this sets `sender' to the string that the SMTP refused.
94 def __init__(self
, code
, msg
, sender
):
98 self
.args
= (code
, msg
, sender
)
100 class SMTPRecipientsRefused(SMTPException
):
101 """All recipient addresses refused.
103 The errors for each recipient are accessible through the attribute
104 'recipients', which is a dictionary of exactly the same sort as
105 SMTP.sendmail() returns.
108 def __init__(self
, recipients
):
109 self
.recipients
= recipients
110 self
.args
= ( recipients
,)
113 class SMTPDataError(SMTPResponseException
):
114 """The SMTP server didn't accept the data."""
116 class SMTPConnectError(SMTPResponseException
):
117 """Error during connection establishment."""
119 class SMTPHeloError(SMTPResponseException
):
120 """The server refused our HELO reply."""
122 class SMTPAuthenticationError(SMTPResponseException
):
123 """Authentication error.
125 Most probably the server didn't accept the username/password
126 combination provided.
130 """A fake socket object that really wraps a SSLObject.
132 It only supports what is needed in smtplib.
134 def __init__(self
, realsock
, sslobj
):
135 self
.realsock
= realsock
139 self
.sslobj
.write(str)
145 self
.realsock
.close()
148 """A fake file like object that really wraps a SSLObject.
150 It only supports what is needed in smtplib.
152 def __init__( self
, sslobj
):
159 chr = self
.sslobj
.read(1)
167 """Quote a subset of the email addresses defined by RFC 821.
169 Should be able to handle anything rfc822.parseaddr can handle.
173 m
=rfc822
.parseaddr(addr
)[1]
174 except AttributeError:
176 if m
== (None, None): # Indicates parse failure or AttributeError
177 #something weird here.. punt -ddm
183 """Quote data for email.
185 Double leading '.', and change Unix newline '\\n', or Mac '\\r' into
186 Internet CRLF end-of-line.
188 return re
.sub(r
'(?m)^\.', '..',
189 re
.sub(r
'(?:\r\n|\n|\r(?!\n))', CRLF
, data
))
193 """This class manages a connection to an SMTP or ESMTP server.
195 SMTP objects have the following attributes:
197 This is the message given by the server in response to the
198 most recent HELO command.
201 This is the message given by the server in response to the
202 most recent EHLO command. This is usually multiline.
205 This is a True value _after you do an EHLO command_, if the
206 server supports ESMTP.
209 This is a dictionary, which, if the server supports ESMTP,
210 will _after you do an EHLO command_, contain the names of the
211 SMTP service extensions this server supports, and their
214 Note, all extension names are mapped to lower case in the
217 See each method's docstrings for details. In general, there is a
218 method of the same name to perform each SMTP command. There is also a
219 method called 'sendmail' that will do an entire mail transaction.
227 def __init__(self
, host
= '', port
= 0, local_hostname
= None):
228 """Initialize a new instance.
230 If specified, `host' is the name of the remote host to which to
231 connect. If specified, `port' specifies the port to which to connect.
232 By default, smtplib.SMTP_PORT is used. An SMTPConnectError is raised
233 if the specified `host' doesn't respond correctly. If specified,
234 `local_hostname` is used as the FQDN of the local host. By default,
235 the local hostname is found using socket.getfqdn().
238 self
.esmtp_features
= {}
240 (code
, msg
) = self
.connect(host
, port
)
242 raise SMTPConnectError(code
, msg
)
243 if local_hostname
is not None:
244 self
.local_hostname
= local_hostname
246 # RFC 2821 says we should use the fqdn in the EHLO/HELO verb, and
247 # if that can't be calculated, that we should use a domain literal
248 # instead (essentially an encoded IP address like [A.B.C.D]).
249 fqdn
= socket
.getfqdn()
251 self
.local_hostname
= fqdn
253 # We can't find an fqdn hostname, so use a domain literal
254 addr
= socket
.gethostbyname(socket
.gethostname())
255 self
.local_hostname
= '[%s]' % addr
257 def set_debuglevel(self
, debuglevel
):
258 """Set the debug output level.
260 A non-false value results in debug messages for connection and for all
261 messages sent to and received from the server.
264 self
.debuglevel
= debuglevel
266 def connect(self
, host
='localhost', port
= 0):
267 """Connect to a host on a given port.
269 If the hostname ends with a colon (`:') followed by a number, and
270 there is no port specified, that suffix will be stripped off and the
271 number interpreted as the port number to use.
273 Note: This method is automatically invoked by __init__, if a host is
274 specified during instantiation.
277 if not port
and (host
.find(':') == host
.rfind(':')):
280 host
, port
= host
[:i
], host
[i
+1:]
281 try: port
= int(port
)
283 raise socket
.error
, "nonnumeric port"
284 if not port
: port
= SMTP_PORT
285 if self
.debuglevel
> 0: print 'connect:', (host
, port
)
286 msg
= "getaddrinfo returns an empty list"
288 for res
in socket
.getaddrinfo(host
, port
, 0, socket
.SOCK_STREAM
):
289 af
, socktype
, proto
, canonname
, sa
= res
291 self
.sock
= socket
.socket(af
, socktype
, proto
)
292 if self
.debuglevel
> 0: print 'connect:', (host
, port
)
293 self
.sock
.connect(sa
)
294 except socket
.error
, msg
:
295 if self
.debuglevel
> 0: print 'connect fail:', (host
, port
)
302 raise socket
.error
, msg
303 (code
, msg
) = self
.getreply()
304 if self
.debuglevel
> 0: print "connect:", msg
308 """Send `str' to the server."""
309 if self
.debuglevel
> 0: print 'send:', `
str`
312 self
.sock
.sendall(str)
315 raise SMTPServerDisconnected('Server not connected')
317 raise SMTPServerDisconnected('please run connect() first')
319 def putcmd(self
, cmd
, args
=""):
320 """Send a command to the server."""
322 str = '%s%s' % (cmd
, CRLF
)
324 str = '%s %s%s' % (cmd
, args
, CRLF
)
328 """Get a reply from the server.
330 Returns a tuple consisting of:
332 - server response code (e.g. '250', or such, if all goes well)
333 Note: returns -1 if it can't read response code.
335 - server response string corresponding to response code (multiline
336 responses are converted to a single, multiline string).
338 Raises SMTPServerDisconnected if end-of-file is reached.
341 if self
.file is None:
342 self
.file = self
.sock
.makefile('rb')
344 line
= self
.file.readline()
347 raise SMTPServerDisconnected("Connection unexpectedly closed")
348 if self
.debuglevel
> 0: print 'reply:', `line`
349 resp
.append(line
[4:].strip())
351 # Check that the error code is syntactically correct.
352 # Don't attempt to read a continuation line if it is broken.
358 # Check if multiline response.
362 errmsg
= "\n".join(resp
)
363 if self
.debuglevel
> 0:
364 print 'reply: retcode (%s); Msg: %s' % (errcode
,errmsg
)
365 return errcode
, errmsg
367 def docmd(self
, cmd
, args
=""):
368 """Send a command, and return its response code."""
369 self
.putcmd(cmd
,args
)
370 return self
.getreply()
373 def helo(self
, name
=''):
374 """SMTP 'helo' command.
375 Hostname to send for this command defaults to the FQDN of the local
378 self
.putcmd("helo", name
or self
.local_hostname
)
379 (code
,msg
)=self
.getreply()
383 def ehlo(self
, name
=''):
384 """ SMTP 'ehlo' command.
385 Hostname to send for this command defaults to the FQDN of the local
388 self
.esmtp_features
= {}
389 self
.putcmd("ehlo", name
or self
.local_hostname
)
390 (code
,msg
)=self
.getreply()
391 # According to RFC1869 some (badly written)
392 # MTA's will disconnect on an ehlo. Toss an exception if
394 if code
== -1 and len(msg
) == 0:
396 raise SMTPServerDisconnected("Server not connected")
401 #parse the ehlo response -ddm
402 resp
=self
.ehlo_resp
.split('\n')
405 # To be able to communicate with as many SMTP servers as possible,
406 # we have to take the old-style auth advertisement into account,
408 # 1) Else our SMTP feature parser gets confused.
409 # 2) There are some servers that only advertise the auth methods we
410 # support using the old style.
411 auth_match
= OLDSTYLE_AUTH
.match(each
)
413 # This doesn't remove duplicates, but that's no problem
414 self
.esmtp_features
["auth"] = self
.esmtp_features
.get("auth", "") \
415 + " " + auth_match
.groups(0)[0]
418 # RFC 1869 requires a space between ehlo keyword and parameters.
419 # It's actually stricter, in that only spaces are allowed between
420 # parameters, but were not going to check for that here. Note
421 # that the space isn't present if there are no parameters.
422 m
=re
.match(r
'(?P<feature>[A-Za-z0-9][A-Za-z0-9\-]*) ?',each
)
424 feature
=m
.group("feature").lower()
425 params
=m
.string
[m
.end("feature"):].strip()
426 if feature
== "auth":
427 self
.esmtp_features
[feature
] = self
.esmtp_features
.get(feature
, "") \
430 self
.esmtp_features
[feature
]=params
433 def has_extn(self
, opt
):
434 """Does the server support a given SMTP service extension?"""
435 return opt
.lower() in self
.esmtp_features
437 def help(self
, args
=''):
438 """SMTP 'help' command.
439 Returns help text from server."""
440 self
.putcmd("help", args
)
441 return self
.getreply()
444 """SMTP 'rset' command -- resets session."""
445 return self
.docmd("rset")
448 """SMTP 'noop' command -- doesn't do anything :>"""
449 return self
.docmd("noop")
451 def mail(self
,sender
,options
=[]):
452 """SMTP 'mail' command -- begins mail xfer session."""
454 if options
and self
.does_esmtp
:
455 optionlist
= ' ' + ' '.join(options
)
456 self
.putcmd("mail", "FROM:%s%s" % (quoteaddr(sender
) ,optionlist
))
457 return self
.getreply()
459 def rcpt(self
,recip
,options
=[]):
460 """SMTP 'rcpt' command -- indicates 1 recipient for this mail."""
462 if options
and self
.does_esmtp
:
463 optionlist
= ' ' + ' '.join(options
)
464 self
.putcmd("rcpt","TO:%s%s" % (quoteaddr(recip
),optionlist
))
465 return self
.getreply()
468 """SMTP 'DATA' command -- sends message data to server.
470 Automatically quotes lines beginning with a period per rfc821.
471 Raises SMTPDataError if there is an unexpected reply to the
472 DATA command; the return value from this method is the final
473 response code received when the all data is sent.
476 (code
,repl
)=self
.getreply()
477 if self
.debuglevel
>0 : print "data:", (code
,repl
)
479 raise SMTPDataError(code
,repl
)
486 (code
,msg
)=self
.getreply()
487 if self
.debuglevel
>0 : print "data:", (code
,msg
)
490 def verify(self
, address
):
491 """SMTP 'verify' command -- checks for address validity."""
492 self
.putcmd("vrfy", quoteaddr(address
))
493 return self
.getreply()
497 def expn(self
, address
):
498 """SMTP 'verify' command -- checks for address validity."""
499 self
.putcmd("expn", quoteaddr(address
))
500 return self
.getreply()
502 # some useful methods
504 def login(self
, user
, password
):
505 """Log in on an SMTP server that requires authentication.
508 - user: The user name to authenticate with.
509 - password: The password for the authentication.
511 If there has been no previous EHLO or HELO command this session, this
512 method tries ESMTP EHLO first.
514 This method will return normally if the authentication was successful.
516 This method may raise the following exceptions:
518 SMTPHeloError The server didn't reply properly to
520 SMTPAuthenticationError The server didn't accept the username/
521 password combination.
522 SMTPException No suitable authentication method was
526 def encode_cram_md5(challenge
, user
, password
):
527 challenge
= base64
.decodestring(challenge
)
528 response
= user
+ " " + hmac
.HMAC(password
, challenge
).hexdigest()
529 return encode_base64(response
, eol
="")
531 def encode_plain(user
, password
):
532 return encode_base64("%s\0%s\0%s" % (user
, user
, password
), eol
="")
536 AUTH_CRAM_MD5
= "CRAM-MD5"
539 if self
.helo_resp
is None and self
.ehlo_resp
is None:
540 if not (200 <= self
.ehlo()[0] <= 299):
541 (code
, resp
) = self
.helo()
542 if not (200 <= code
<= 299):
543 raise SMTPHeloError(code
, resp
)
545 if not self
.has_extn("auth"):
546 raise SMTPException("SMTP AUTH extension not supported by server.")
548 # Authentication methods the server supports:
549 authlist
= self
.esmtp_features
["auth"].split()
551 # List of authentication methods we support: from preferred to
552 # less preferred methods. Except for the purpose of testing the weaker
553 # ones, we prefer stronger methods like CRAM-MD5:
554 preferred_auths
= [AUTH_CRAM_MD5
, AUTH_PLAIN
, AUTH_LOGIN
]
556 # Determine the authentication method we'll use
558 for method
in preferred_auths
:
559 if method
in authlist
:
563 if authmethod
== AUTH_CRAM_MD5
:
564 (code
, resp
) = self
.docmd("AUTH", AUTH_CRAM_MD5
)
566 # 503 == 'Error: already authenticated'
568 (code
, resp
) = self
.docmd(encode_cram_md5(resp
, user
, password
))
569 elif authmethod
== AUTH_PLAIN
:
570 (code
, resp
) = self
.docmd("AUTH",
571 AUTH_PLAIN
+ " " + encode_plain(user
, password
))
572 elif authmethod
== AUTH_LOGIN
:
573 (code
, resp
) = self
.docmd("AUTH",
574 "%s %s" % (AUTH_LOGIN
, encode_base64(user
, eol
="")))
576 raise SMTPAuthenticationError(code
, resp
)
577 (code
, resp
) = self
.docmd(encode_base64(password
, eol
=""))
578 elif authmethod
is None:
579 raise SMTPException("No suitable authentication method found.")
580 if code
not in [235, 503]:
581 # 235 == 'Authentication successful'
582 # 503 == 'Error: already authenticated'
583 raise SMTPAuthenticationError(code
, resp
)
586 def starttls(self
, keyfile
= None, certfile
= None):
587 """Puts the connection to the SMTP server into TLS mode.
589 If the server supports TLS, this will encrypt the rest of the SMTP
590 session. If you provide the keyfile and certfile parameters,
591 the identity of the SMTP server and client can be checked. This,
592 however, depends on whether the socket module really checks the
595 (resp
, reply
) = self
.docmd("STARTTLS")
597 sslobj
= socket
.ssl(self
.sock
, keyfile
, certfile
)
598 self
.sock
= SSLFakeSocket(self
.sock
, sslobj
)
599 self
.file = SSLFakeFile(sslobj
)
602 def sendmail(self
, from_addr
, to_addrs
, msg
, mail_options
=[],
604 """This command performs an entire mail transaction.
607 - from_addr : The address sending this mail.
608 - to_addrs : A list of addresses to send this mail to. A bare
609 string will be treated as a list with 1 address.
610 - msg : The message to send.
611 - mail_options : List of ESMTP options (such as 8bitmime) for the
613 - rcpt_options : List of ESMTP options (such as DSN commands) for
614 all the rcpt commands.
616 If there has been no previous EHLO or HELO command this session, this
617 method tries ESMTP EHLO first. If the server does ESMTP, message size
618 and each of the specified options will be passed to it. If EHLO
619 fails, HELO will be tried and ESMTP options suppressed.
621 This method will return normally if the mail is accepted for at least
622 one recipient. It returns a dictionary, with one entry for each
623 recipient that was refused. Each entry contains a tuple of the SMTP
624 error code and the accompanying error message sent by the server.
626 This method may raise the following exceptions:
628 SMTPHeloError The server didn't reply properly to
630 SMTPRecipientsRefused The server rejected ALL recipients
632 SMTPSenderRefused The server didn't accept the from_addr.
633 SMTPDataError The server replied with an unexpected
634 error code (other than a refusal of
637 Note: the connection will be open even after an exception is raised.
642 >>> s=smtplib.SMTP("localhost")
643 >>> tolist=["one@one.org","two@two.org","three@three.org","four@four.org"]
646 ... Subject: testin'...
648 ... This is a test '''
649 >>> s.sendmail("me@my.org",tolist,msg)
650 { "three@three.org" : ( 550 ,"User unknown" ) }
653 In the above example, the message was accepted for delivery to three
654 of the four addresses, and one was rejected, with the error code
655 550. If all addresses are accepted, then the method will return an
659 if self
.helo_resp
is None and self
.ehlo_resp
is None:
660 if not (200 <= self
.ehlo()[0] <= 299):
661 (code
,resp
) = self
.helo()
662 if not (200 <= code
<= 299):
663 raise SMTPHeloError(code
, resp
)
666 # Hmmm? what's this? -ddm
667 # self.esmtp_features['7bit']=""
668 if self
.has_extn('size'):
669 esmtp_opts
.append("size=" + `
len(msg
)`
)
670 for option
in mail_options
:
671 esmtp_opts
.append(option
)
673 (code
,resp
) = self
.mail(from_addr
, esmtp_opts
)
676 raise SMTPSenderRefused(code
, resp
, from_addr
)
678 if isinstance(to_addrs
, basestring
):
679 to_addrs
= [to_addrs
]
680 for each
in to_addrs
:
681 (code
,resp
)=self
.rcpt(each
, rcpt_options
)
682 if (code
!= 250) and (code
!= 251):
683 senderrs
[each
]=(code
,resp
)
684 if len(senderrs
)==len(to_addrs
):
685 # the server refused all our recipients
687 raise SMTPRecipientsRefused(senderrs
)
688 (code
,resp
) = self
.data(msg
)
691 raise SMTPDataError(code
, resp
)
692 #if we got here then somebody got our mail
697 """Close the connection to the SMTP server."""
707 """Terminate the SMTP session."""
712 # Test the sendmail method, which tests most of the others.
713 # Note: This always sends to localhost.
714 if __name__
== '__main__':
718 sys
.stdout
.write(prompt
+ ": ")
719 return sys
.stdin
.readline().strip()
721 fromaddr
= prompt("From")
722 toaddrs
= prompt("To").split(',')
723 print "Enter message, end with ^D:"
726 line
= sys
.stdin
.readline()
730 print "Message length is " + `
len(msg
)`
732 server
= SMTP('localhost')
733 server
.set_debuglevel(1)
734 server
.sendmail(fromaddr
, toaddrs
, msg
)