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
50 from sys
import stderr
52 __all__
= ["SMTPException","SMTPServerDisconnected","SMTPResponseException",
53 "SMTPSenderRefused","SMTPRecipientsRefused","SMTPDataError",
54 "SMTPConnectError","SMTPHeloError","SMTPAuthenticationError",
55 "quoteaddr","quotedata","SMTP"]
60 OLDSTYLE_AUTH
= re
.compile(r
"auth=(.*)", re
.I
)
62 # Exception classes used by this module.
63 class SMTPException(Exception):
64 """Base class for all exceptions raised by this module."""
66 class SMTPServerDisconnected(SMTPException
):
67 """Not connected to any SMTP server.
69 This exception is raised when the server unexpectedly disconnects,
70 or when an attempt is made to use the SMTP instance before
71 connecting it to a server.
74 class SMTPResponseException(SMTPException
):
75 """Base class for all exceptions that include an SMTP error code.
77 These exceptions are generated in some instances when the SMTP
78 server returns an error code. The error code is stored in the
79 `smtp_code' attribute of the error, and the `smtp_error' attribute
80 is set to the error message.
83 def __init__(self
, code
, msg
):
86 self
.args
= (code
, msg
)
88 class SMTPSenderRefused(SMTPResponseException
):
89 """Sender address refused.
91 In addition to the attributes set by on all SMTPResponseException
92 exceptions, this sets `sender' to the string that the SMTP refused.
95 def __init__(self
, code
, msg
, sender
):
99 self
.args
= (code
, msg
, sender
)
101 class SMTPRecipientsRefused(SMTPException
):
102 """All recipient addresses refused.
104 The errors for each recipient are accessible through the attribute
105 'recipients', which is a dictionary of exactly the same sort as
106 SMTP.sendmail() returns.
109 def __init__(self
, recipients
):
110 self
.recipients
= recipients
111 self
.args
= ( recipients
,)
114 class SMTPDataError(SMTPResponseException
):
115 """The SMTP server didn't accept the data."""
117 class SMTPConnectError(SMTPResponseException
):
118 """Error during connection establishment."""
120 class SMTPHeloError(SMTPResponseException
):
121 """The server refused our HELO reply."""
123 class SMTPAuthenticationError(SMTPResponseException
):
124 """Authentication error.
126 Most probably the server didn't accept the username/password
127 combination provided.
131 """A fake socket object that really wraps a SSLObject.
133 It only supports what is needed in smtplib.
135 def __init__(self
, realsock
, sslobj
):
136 self
.realsock
= realsock
140 self
.sslobj
.write(str)
146 self
.realsock
.close()
149 """A fake file like object that really wraps a SSLObject.
151 It only supports what is needed in smtplib.
153 def __init__(self
, sslobj
):
160 chr = self
.sslobj
.read(1)
168 """Quote a subset of the email addresses defined by RFC 821.
170 Should be able to handle anything rfc822.parseaddr can handle.
174 m
= email
.Utils
.parseaddr(addr
)[1]
175 except AttributeError:
177 if m
== (None, None): # Indicates parse failure or AttributeError
178 # something weird here.. punt -ddm
181 # the sender wants an empty return address
187 """Quote data for email.
189 Double leading '.', and change Unix newline '\\n', or Mac '\\r' into
190 Internet CRLF end-of-line.
192 return re
.sub(r
'(?m)^\.', '..',
193 re
.sub(r
'(?:\r\n|\n|\r(?!\n))', CRLF
, data
))
197 """This class manages a connection to an SMTP or ESMTP server.
199 SMTP objects have the following attributes:
201 This is the message given by the server in response to the
202 most recent HELO command.
205 This is the message given by the server in response to the
206 most recent EHLO command. This is usually multiline.
209 This is a True value _after you do an EHLO command_, if the
210 server supports ESMTP.
213 This is a dictionary, which, if the server supports ESMTP,
214 will _after you do an EHLO command_, contain the names of the
215 SMTP service extensions this server supports, and their
218 Note, all extension names are mapped to lower case in the
221 See each method's docstrings for details. In general, there is a
222 method of the same name to perform each SMTP command. There is also a
223 method called 'sendmail' that will do an entire mail transaction.
231 def __init__(self
, host
= '', port
= 0, local_hostname
= None):
232 """Initialize a new instance.
234 If specified, `host' is the name of the remote host to which to
235 connect. If specified, `port' specifies the port to which to connect.
236 By default, smtplib.SMTP_PORT is used. An SMTPConnectError is raised
237 if the specified `host' doesn't respond correctly. If specified,
238 `local_hostname` is used as the FQDN of the local host. By default,
239 the local hostname is found using socket.getfqdn().
242 self
.esmtp_features
= {}
244 (code
, msg
) = self
.connect(host
, port
)
246 raise SMTPConnectError(code
, msg
)
247 if local_hostname
is not None:
248 self
.local_hostname
= local_hostname
250 # RFC 2821 says we should use the fqdn in the EHLO/HELO verb, and
251 # if that can't be calculated, that we should use a domain literal
252 # instead (essentially an encoded IP address like [A.B.C.D]).
253 fqdn
= socket
.getfqdn()
255 self
.local_hostname
= fqdn
257 # We can't find an fqdn hostname, so use a domain literal
260 addr
= socket
.gethostbyname(socket
.gethostname())
261 except socket
.gaierror
:
263 self
.local_hostname
= '[%s]' % addr
265 def set_debuglevel(self
, debuglevel
):
266 """Set the debug output level.
268 A non-false value results in debug messages for connection and for all
269 messages sent to and received from the server.
272 self
.debuglevel
= debuglevel
274 def connect(self
, host
='localhost', port
= 0):
275 """Connect to a host on a given port.
277 If the hostname ends with a colon (`:') followed by a number, and
278 there is no port specified, that suffix will be stripped off and the
279 number interpreted as the port number to use.
281 Note: This method is automatically invoked by __init__, if a host is
282 specified during instantiation.
285 if not port
and (host
.find(':') == host
.rfind(':')):
288 host
, port
= host
[:i
], host
[i
+1:]
289 try: port
= int(port
)
291 raise socket
.error
, "nonnumeric port"
292 if not port
: port
= SMTP_PORT
293 if self
.debuglevel
> 0: print>>stderr
, 'connect:', (host
, port
)
294 msg
= "getaddrinfo returns an empty list"
296 for res
in socket
.getaddrinfo(host
, port
, 0, socket
.SOCK_STREAM
):
297 af
, socktype
, proto
, canonname
, sa
= res
299 self
.sock
= socket
.socket(af
, socktype
, proto
)
300 if self
.debuglevel
> 0: print>>stderr
, 'connect:', sa
301 self
.sock
.connect(sa
)
302 except socket
.error
, msg
:
303 if self
.debuglevel
> 0: print>>stderr
, 'connect fail:', msg
310 raise socket
.error
, msg
311 (code
, msg
) = self
.getreply()
312 if self
.debuglevel
> 0: print>>stderr
, "connect:", msg
316 """Send `str' to the server."""
317 if self
.debuglevel
> 0: print>>stderr
, 'send:', repr(str)
320 self
.sock
.sendall(str)
323 raise SMTPServerDisconnected('Server not connected')
325 raise SMTPServerDisconnected('please run connect() first')
327 def putcmd(self
, cmd
, args
=""):
328 """Send a command to the server."""
330 str = '%s%s' % (cmd
, CRLF
)
332 str = '%s %s%s' % (cmd
, args
, CRLF
)
336 """Get a reply from the server.
338 Returns a tuple consisting of:
340 - server response code (e.g. '250', or such, if all goes well)
341 Note: returns -1 if it can't read response code.
343 - server response string corresponding to response code (multiline
344 responses are converted to a single, multiline string).
346 Raises SMTPServerDisconnected if end-of-file is reached.
349 if self
.file is None:
350 self
.file = self
.sock
.makefile('rb')
352 line
= self
.file.readline()
355 raise SMTPServerDisconnected("Connection unexpectedly closed")
356 if self
.debuglevel
> 0: print>>stderr
, 'reply:', repr(line
)
357 resp
.append(line
[4:].strip())
359 # Check that the error code is syntactically correct.
360 # Don't attempt to read a continuation line if it is broken.
366 # Check if multiline response.
370 errmsg
= "\n".join(resp
)
371 if self
.debuglevel
> 0:
372 print>>stderr
, 'reply: retcode (%s); Msg: %s' % (errcode
,errmsg
)
373 return errcode
, errmsg
375 def docmd(self
, cmd
, args
=""):
376 """Send a command, and return its response code."""
377 self
.putcmd(cmd
,args
)
378 return self
.getreply()
381 def helo(self
, name
=''):
382 """SMTP 'helo' command.
383 Hostname to send for this command defaults to the FQDN of the local
386 self
.putcmd("helo", name
or self
.local_hostname
)
387 (code
,msg
)=self
.getreply()
391 def ehlo(self
, name
=''):
392 """ SMTP 'ehlo' command.
393 Hostname to send for this command defaults to the FQDN of the local
396 self
.esmtp_features
= {}
397 self
.putcmd("ehlo", name
or self
.local_hostname
)
398 (code
,msg
)=self
.getreply()
399 # According to RFC1869 some (badly written)
400 # MTA's will disconnect on an ehlo. Toss an exception if
402 if code
== -1 and len(msg
) == 0:
404 raise SMTPServerDisconnected("Server not connected")
409 #parse the ehlo response -ddm
410 resp
=self
.ehlo_resp
.split('\n')
413 # To be able to communicate with as many SMTP servers as possible,
414 # we have to take the old-style auth advertisement into account,
416 # 1) Else our SMTP feature parser gets confused.
417 # 2) There are some servers that only advertise the auth methods we
418 # support using the old style.
419 auth_match
= OLDSTYLE_AUTH
.match(each
)
421 # This doesn't remove duplicates, but that's no problem
422 self
.esmtp_features
["auth"] = self
.esmtp_features
.get("auth", "") \
423 + " " + auth_match
.groups(0)[0]
426 # RFC 1869 requires a space between ehlo keyword and parameters.
427 # It's actually stricter, in that only spaces are allowed between
428 # parameters, but were not going to check for that here. Note
429 # that the space isn't present if there are no parameters.
430 m
=re
.match(r
'(?P<feature>[A-Za-z0-9][A-Za-z0-9\-]*) ?',each
)
432 feature
=m
.group("feature").lower()
433 params
=m
.string
[m
.end("feature"):].strip()
434 if feature
== "auth":
435 self
.esmtp_features
[feature
] = self
.esmtp_features
.get(feature
, "") \
438 self
.esmtp_features
[feature
]=params
441 def has_extn(self
, opt
):
442 """Does the server support a given SMTP service extension?"""
443 return opt
.lower() in self
.esmtp_features
445 def help(self
, args
=''):
446 """SMTP 'help' command.
447 Returns help text from server."""
448 self
.putcmd("help", args
)
449 return self
.getreply()[1]
452 """SMTP 'rset' command -- resets session."""
453 return self
.docmd("rset")
456 """SMTP 'noop' command -- doesn't do anything :>"""
457 return self
.docmd("noop")
459 def mail(self
,sender
,options
=[]):
460 """SMTP 'mail' command -- begins mail xfer session."""
462 if options
and self
.does_esmtp
:
463 optionlist
= ' ' + ' '.join(options
)
464 self
.putcmd("mail", "FROM:%s%s" % (quoteaddr(sender
) ,optionlist
))
465 return self
.getreply()
467 def rcpt(self
,recip
,options
=[]):
468 """SMTP 'rcpt' command -- indicates 1 recipient for this mail."""
470 if options
and self
.does_esmtp
:
471 optionlist
= ' ' + ' '.join(options
)
472 self
.putcmd("rcpt","TO:%s%s" % (quoteaddr(recip
),optionlist
))
473 return self
.getreply()
476 """SMTP 'DATA' command -- sends message data to server.
478 Automatically quotes lines beginning with a period per rfc821.
479 Raises SMTPDataError if there is an unexpected reply to the
480 DATA command; the return value from this method is the final
481 response code received when the all data is sent.
484 (code
,repl
)=self
.getreply()
485 if self
.debuglevel
>0 : print>>stderr
, "data:", (code
,repl
)
487 raise SMTPDataError(code
,repl
)
494 (code
,msg
)=self
.getreply()
495 if self
.debuglevel
>0 : print>>stderr
, "data:", (code
,msg
)
498 def verify(self
, address
):
499 """SMTP 'verify' command -- checks for address validity."""
500 self
.putcmd("vrfy", quoteaddr(address
))
501 return self
.getreply()
505 def expn(self
, address
):
506 """SMTP 'verify' command -- checks for address validity."""
507 self
.putcmd("expn", quoteaddr(address
))
508 return self
.getreply()
510 # some useful methods
512 def login(self
, user
, password
):
513 """Log in on an SMTP server that requires authentication.
516 - user: The user name to authenticate with.
517 - password: The password for the authentication.
519 If there has been no previous EHLO or HELO command this session, this
520 method tries ESMTP EHLO first.
522 This method will return normally if the authentication was successful.
524 This method may raise the following exceptions:
526 SMTPHeloError The server didn't reply properly to
528 SMTPAuthenticationError The server didn't accept the username/
529 password combination.
530 SMTPException No suitable authentication method was
534 def encode_cram_md5(challenge
, user
, password
):
535 challenge
= base64
.decodestring(challenge
)
536 response
= user
+ " " + hmac
.HMAC(password
, challenge
).hexdigest()
537 return encode_base64(response
, eol
="")
539 def encode_plain(user
, password
):
540 return encode_base64("\0%s\0%s" % (user
, password
), eol
="")
544 AUTH_CRAM_MD5
= "CRAM-MD5"
547 if self
.helo_resp
is None and self
.ehlo_resp
is None:
548 if not (200 <= self
.ehlo()[0] <= 299):
549 (code
, resp
) = self
.helo()
550 if not (200 <= code
<= 299):
551 raise SMTPHeloError(code
, resp
)
553 if not self
.has_extn("auth"):
554 raise SMTPException("SMTP AUTH extension not supported by server.")
556 # Authentication methods the server supports:
557 authlist
= self
.esmtp_features
["auth"].split()
559 # List of authentication methods we support: from preferred to
560 # less preferred methods. Except for the purpose of testing the weaker
561 # ones, we prefer stronger methods like CRAM-MD5:
562 preferred_auths
= [AUTH_CRAM_MD5
, AUTH_PLAIN
, AUTH_LOGIN
]
564 # Determine the authentication method we'll use
566 for method
in preferred_auths
:
567 if method
in authlist
:
571 if authmethod
== AUTH_CRAM_MD5
:
572 (code
, resp
) = self
.docmd("AUTH", AUTH_CRAM_MD5
)
574 # 503 == 'Error: already authenticated'
576 (code
, resp
) = self
.docmd(encode_cram_md5(resp
, user
, password
))
577 elif authmethod
== AUTH_PLAIN
:
578 (code
, resp
) = self
.docmd("AUTH",
579 AUTH_PLAIN
+ " " + encode_plain(user
, password
))
580 elif authmethod
== AUTH_LOGIN
:
581 (code
, resp
) = self
.docmd("AUTH",
582 "%s %s" % (AUTH_LOGIN
, encode_base64(user
, eol
="")))
584 raise SMTPAuthenticationError(code
, resp
)
585 (code
, resp
) = self
.docmd(encode_base64(password
, eol
=""))
586 elif authmethod
is None:
587 raise SMTPException("No suitable authentication method found.")
588 if code
not in (235, 503):
589 # 235 == 'Authentication successful'
590 # 503 == 'Error: already authenticated'
591 raise SMTPAuthenticationError(code
, resp
)
594 def starttls(self
, keyfile
= None, certfile
= None):
595 """Puts the connection to the SMTP server into TLS mode.
597 If the server supports TLS, this will encrypt the rest of the SMTP
598 session. If you provide the keyfile and certfile parameters,
599 the identity of the SMTP server and client can be checked. This,
600 however, depends on whether the socket module really checks the
603 (resp
, reply
) = self
.docmd("STARTTLS")
605 sslobj
= socket
.ssl(self
.sock
, keyfile
, certfile
)
606 self
.sock
= SSLFakeSocket(self
.sock
, sslobj
)
607 self
.file = SSLFakeFile(sslobj
)
610 def sendmail(self
, from_addr
, to_addrs
, msg
, mail_options
=[],
612 """This command performs an entire mail transaction.
615 - from_addr : The address sending this mail.
616 - to_addrs : A list of addresses to send this mail to. A bare
617 string will be treated as a list with 1 address.
618 - msg : The message to send.
619 - mail_options : List of ESMTP options (such as 8bitmime) for the
621 - rcpt_options : List of ESMTP options (such as DSN commands) for
622 all the rcpt commands.
624 If there has been no previous EHLO or HELO command this session, this
625 method tries ESMTP EHLO first. If the server does ESMTP, message size
626 and each of the specified options will be passed to it. If EHLO
627 fails, HELO will be tried and ESMTP options suppressed.
629 This method will return normally if the mail is accepted for at least
630 one recipient. It returns a dictionary, with one entry for each
631 recipient that was refused. Each entry contains a tuple of the SMTP
632 error code and the accompanying error message sent by the server.
634 This method may raise the following exceptions:
636 SMTPHeloError The server didn't reply properly to
638 SMTPRecipientsRefused The server rejected ALL recipients
640 SMTPSenderRefused The server didn't accept the from_addr.
641 SMTPDataError The server replied with an unexpected
642 error code (other than a refusal of
645 Note: the connection will be open even after an exception is raised.
650 >>> s=smtplib.SMTP("localhost")
651 >>> tolist=["one@one.org","two@two.org","three@three.org","four@four.org"]
654 ... Subject: testin'...
656 ... This is a test '''
657 >>> s.sendmail("me@my.org",tolist,msg)
658 { "three@three.org" : ( 550 ,"User unknown" ) }
661 In the above example, the message was accepted for delivery to three
662 of the four addresses, and one was rejected, with the error code
663 550. If all addresses are accepted, then the method will return an
667 if self
.helo_resp
is None and self
.ehlo_resp
is None:
668 if not (200 <= self
.ehlo()[0] <= 299):
669 (code
,resp
) = self
.helo()
670 if not (200 <= code
<= 299):
671 raise SMTPHeloError(code
, resp
)
674 # Hmmm? what's this? -ddm
675 # self.esmtp_features['7bit']=""
676 if self
.has_extn('size'):
677 esmtp_opts
.append("size=%d" % len(msg
))
678 for option
in mail_options
:
679 esmtp_opts
.append(option
)
681 (code
,resp
) = self
.mail(from_addr
, esmtp_opts
)
684 raise SMTPSenderRefused(code
, resp
, from_addr
)
686 if isinstance(to_addrs
, basestring
):
687 to_addrs
= [to_addrs
]
688 for each
in to_addrs
:
689 (code
,resp
)=self
.rcpt(each
, rcpt_options
)
690 if (code
!= 250) and (code
!= 251):
691 senderrs
[each
]=(code
,resp
)
692 if len(senderrs
)==len(to_addrs
):
693 # the server refused all our recipients
695 raise SMTPRecipientsRefused(senderrs
)
696 (code
,resp
) = self
.data(msg
)
699 raise SMTPDataError(code
, resp
)
700 #if we got here then somebody got our mail
705 """Close the connection to the SMTP server."""
715 """Terminate the SMTP session."""
720 # Test the sendmail method, which tests most of the others.
721 # Note: This always sends to localhost.
722 if __name__
== '__main__':
726 sys
.stdout
.write(prompt
+ ": ")
727 return sys
.stdin
.readline().strip()
729 fromaddr
= prompt("From")
730 toaddrs
= prompt("To").split(',')
731 print "Enter message, end with ^D:"
734 line
= sys
.stdin
.readline()
738 print "Message length is %d" % len(msg
)
740 server
= SMTP('localhost')
741 server
.set_debuglevel(1)
742 server
.sendmail(fromaddr
, toaddrs
, msg
)