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
184 """Quote data for email.
186 Double leading '.', and change Unix newline '\\n', or Mac '\\r' into
187 Internet CRLF end-of-line.
189 return re
.sub(r
'(?m)^\.', '..',
190 re
.sub(r
'(?:\r\n|\n|\r(?!\n))', CRLF
, data
))
194 """This class manages a connection to an SMTP or ESMTP server.
196 SMTP objects have the following attributes:
198 This is the message given by the server in response to the
199 most recent HELO command.
202 This is the message given by the server in response to the
203 most recent EHLO command. This is usually multiline.
206 This is a True value _after you do an EHLO command_, if the
207 server supports ESMTP.
210 This is a dictionary, which, if the server supports ESMTP,
211 will _after you do an EHLO command_, contain the names of the
212 SMTP service extensions this server supports, and their
215 Note, all extension names are mapped to lower case in the
218 See each method's docstrings for details. In general, there is a
219 method of the same name to perform each SMTP command. There is also a
220 method called 'sendmail' that will do an entire mail transaction.
228 def __init__(self
, host
= '', port
= 0, local_hostname
= None):
229 """Initialize a new instance.
231 If specified, `host' is the name of the remote host to which to
232 connect. If specified, `port' specifies the port to which to connect.
233 By default, smtplib.SMTP_PORT is used. An SMTPConnectError is raised
234 if the specified `host' doesn't respond correctly. If specified,
235 `local_hostname` is used as the FQDN of the local host. By default,
236 the local hostname is found using socket.getfqdn().
239 self
.esmtp_features
= {}
241 (code
, msg
) = self
.connect(host
, port
)
243 raise SMTPConnectError(code
, msg
)
244 if local_hostname
is not None:
245 self
.local_hostname
= local_hostname
247 # RFC 2821 says we should use the fqdn in the EHLO/HELO verb, and
248 # if that can't be calculated, that we should use a domain literal
249 # instead (essentially an encoded IP address like [A.B.C.D]).
250 fqdn
= socket
.getfqdn()
252 self
.local_hostname
= fqdn
254 # We can't find an fqdn hostname, so use a domain literal
255 addr
= socket
.gethostbyname(socket
.gethostname())
256 self
.local_hostname
= '[%s]' % addr
258 def set_debuglevel(self
, debuglevel
):
259 """Set the debug output level.
261 A non-false value results in debug messages for connection and for all
262 messages sent to and received from the server.
265 self
.debuglevel
= debuglevel
267 def connect(self
, host
='localhost', port
= 0):
268 """Connect to a host on a given port.
270 If the hostname ends with a colon (`:') followed by a number, and
271 there is no port specified, that suffix will be stripped off and the
272 number interpreted as the port number to use.
274 Note: This method is automatically invoked by __init__, if a host is
275 specified during instantiation.
278 if not port
and (host
.find(':') == host
.rfind(':')):
281 host
, port
= host
[:i
], host
[i
+1:]
282 try: port
= int(port
)
284 raise socket
.error
, "nonnumeric port"
285 if not port
: port
= SMTP_PORT
286 if self
.debuglevel
> 0: print>>stderr
, 'connect:', (host
, port
)
287 msg
= "getaddrinfo returns an empty list"
289 for res
in socket
.getaddrinfo(host
, port
, 0, socket
.SOCK_STREAM
):
290 af
, socktype
, proto
, canonname
, sa
= res
292 self
.sock
= socket
.socket(af
, socktype
, proto
)
293 if self
.debuglevel
> 0: print>>stderr
, 'connect:', sa
294 self
.sock
.connect(sa
)
295 except socket
.error
, msg
:
296 if self
.debuglevel
> 0: print>>stderr
, 'connect fail:', msg
303 raise socket
.error
, msg
304 (code
, msg
) = self
.getreply()
305 if self
.debuglevel
> 0: print>>stderr
, "connect:", msg
309 """Send `str' to the server."""
310 if self
.debuglevel
> 0: print>>stderr
, 'send:', repr(str)
313 self
.sock
.sendall(str)
316 raise SMTPServerDisconnected('Server not connected')
318 raise SMTPServerDisconnected('please run connect() first')
320 def putcmd(self
, cmd
, args
=""):
321 """Send a command to the server."""
323 str = '%s%s' % (cmd
, CRLF
)
325 str = '%s %s%s' % (cmd
, args
, CRLF
)
329 """Get a reply from the server.
331 Returns a tuple consisting of:
333 - server response code (e.g. '250', or such, if all goes well)
334 Note: returns -1 if it can't read response code.
336 - server response string corresponding to response code (multiline
337 responses are converted to a single, multiline string).
339 Raises SMTPServerDisconnected if end-of-file is reached.
342 if self
.file is None:
343 self
.file = self
.sock
.makefile('rb')
345 line
= self
.file.readline()
348 raise SMTPServerDisconnected("Connection unexpectedly closed")
349 if self
.debuglevel
> 0: print>>stderr
, 'reply:', repr(line
)
350 resp
.append(line
[4:].strip())
352 # Check that the error code is syntactically correct.
353 # Don't attempt to read a continuation line if it is broken.
359 # Check if multiline response.
363 errmsg
= "\n".join(resp
)
364 if self
.debuglevel
> 0:
365 print>>stderr
, 'reply: retcode (%s); Msg: %s' % (errcode
,errmsg
)
366 return errcode
, errmsg
368 def docmd(self
, cmd
, args
=""):
369 """Send a command, and return its response code."""
370 self
.putcmd(cmd
,args
)
371 return self
.getreply()
374 def helo(self
, name
=''):
375 """SMTP 'helo' command.
376 Hostname to send for this command defaults to the FQDN of the local
379 self
.putcmd("helo", name
or self
.local_hostname
)
380 (code
,msg
)=self
.getreply()
384 def ehlo(self
, name
=''):
385 """ SMTP 'ehlo' command.
386 Hostname to send for this command defaults to the FQDN of the local
389 self
.esmtp_features
= {}
390 self
.putcmd("ehlo", name
or self
.local_hostname
)
391 (code
,msg
)=self
.getreply()
392 # According to RFC1869 some (badly written)
393 # MTA's will disconnect on an ehlo. Toss an exception if
395 if code
== -1 and len(msg
) == 0:
397 raise SMTPServerDisconnected("Server not connected")
402 #parse the ehlo response -ddm
403 resp
=self
.ehlo_resp
.split('\n')
406 # To be able to communicate with as many SMTP servers as possible,
407 # we have to take the old-style auth advertisement into account,
409 # 1) Else our SMTP feature parser gets confused.
410 # 2) There are some servers that only advertise the auth methods we
411 # support using the old style.
412 auth_match
= OLDSTYLE_AUTH
.match(each
)
414 # This doesn't remove duplicates, but that's no problem
415 self
.esmtp_features
["auth"] = self
.esmtp_features
.get("auth", "") \
416 + " " + auth_match
.groups(0)[0]
419 # RFC 1869 requires a space between ehlo keyword and parameters.
420 # It's actually stricter, in that only spaces are allowed between
421 # parameters, but were not going to check for that here. Note
422 # that the space isn't present if there are no parameters.
423 m
=re
.match(r
'(?P<feature>[A-Za-z0-9][A-Za-z0-9\-]*) ?',each
)
425 feature
=m
.group("feature").lower()
426 params
=m
.string
[m
.end("feature"):].strip()
427 if feature
== "auth":
428 self
.esmtp_features
[feature
] = self
.esmtp_features
.get(feature
, "") \
431 self
.esmtp_features
[feature
]=params
434 def has_extn(self
, opt
):
435 """Does the server support a given SMTP service extension?"""
436 return opt
.lower() in self
.esmtp_features
438 def help(self
, args
=''):
439 """SMTP 'help' command.
440 Returns help text from server."""
441 self
.putcmd("help", args
)
442 return self
.getreply()[1]
445 """SMTP 'rset' command -- resets session."""
446 return self
.docmd("rset")
449 """SMTP 'noop' command -- doesn't do anything :>"""
450 return self
.docmd("noop")
452 def mail(self
,sender
,options
=[]):
453 """SMTP 'mail' command -- begins mail xfer session."""
455 if options
and self
.does_esmtp
:
456 optionlist
= ' ' + ' '.join(options
)
457 self
.putcmd("mail", "FROM:%s%s" % (quoteaddr(sender
) ,optionlist
))
458 return self
.getreply()
460 def rcpt(self
,recip
,options
=[]):
461 """SMTP 'rcpt' command -- indicates 1 recipient for this mail."""
463 if options
and self
.does_esmtp
:
464 optionlist
= ' ' + ' '.join(options
)
465 self
.putcmd("rcpt","TO:%s%s" % (quoteaddr(recip
),optionlist
))
466 return self
.getreply()
469 """SMTP 'DATA' command -- sends message data to server.
471 Automatically quotes lines beginning with a period per rfc821.
472 Raises SMTPDataError if there is an unexpected reply to the
473 DATA command; the return value from this method is the final
474 response code received when the all data is sent.
477 (code
,repl
)=self
.getreply()
478 if self
.debuglevel
>0 : print>>stderr
, "data:", (code
,repl
)
480 raise SMTPDataError(code
,repl
)
487 (code
,msg
)=self
.getreply()
488 if self
.debuglevel
>0 : print>>stderr
, "data:", (code
,msg
)
491 def verify(self
, address
):
492 """SMTP 'verify' command -- checks for address validity."""
493 self
.putcmd("vrfy", quoteaddr(address
))
494 return self
.getreply()
498 def expn(self
, address
):
499 """SMTP 'verify' command -- checks for address validity."""
500 self
.putcmd("expn", quoteaddr(address
))
501 return self
.getreply()
503 # some useful methods
505 def login(self
, user
, password
):
506 """Log in on an SMTP server that requires authentication.
509 - user: The user name to authenticate with.
510 - password: The password for the authentication.
512 If there has been no previous EHLO or HELO command this session, this
513 method tries ESMTP EHLO first.
515 This method will return normally if the authentication was successful.
517 This method may raise the following exceptions:
519 SMTPHeloError The server didn't reply properly to
521 SMTPAuthenticationError The server didn't accept the username/
522 password combination.
523 SMTPException No suitable authentication method was
527 def encode_cram_md5(challenge
, user
, password
):
528 challenge
= base64
.decodestring(challenge
)
529 response
= user
+ " " + hmac
.HMAC(password
, challenge
).hexdigest()
530 return encode_base64(response
, eol
="")
532 def encode_plain(user
, password
):
533 return encode_base64("\0%s\0%s" % (user
, password
), eol
="")
537 AUTH_CRAM_MD5
= "CRAM-MD5"
540 if self
.helo_resp
is None and self
.ehlo_resp
is None:
541 if not (200 <= self
.ehlo()[0] <= 299):
542 (code
, resp
) = self
.helo()
543 if not (200 <= code
<= 299):
544 raise SMTPHeloError(code
, resp
)
546 if not self
.has_extn("auth"):
547 raise SMTPException("SMTP AUTH extension not supported by server.")
549 # Authentication methods the server supports:
550 authlist
= self
.esmtp_features
["auth"].split()
552 # List of authentication methods we support: from preferred to
553 # less preferred methods. Except for the purpose of testing the weaker
554 # ones, we prefer stronger methods like CRAM-MD5:
555 preferred_auths
= [AUTH_CRAM_MD5
, AUTH_PLAIN
, AUTH_LOGIN
]
557 # Determine the authentication method we'll use
559 for method
in preferred_auths
:
560 if method
in authlist
:
564 if authmethod
== AUTH_CRAM_MD5
:
565 (code
, resp
) = self
.docmd("AUTH", AUTH_CRAM_MD5
)
567 # 503 == 'Error: already authenticated'
569 (code
, resp
) = self
.docmd(encode_cram_md5(resp
, user
, password
))
570 elif authmethod
== AUTH_PLAIN
:
571 (code
, resp
) = self
.docmd("AUTH",
572 AUTH_PLAIN
+ " " + encode_plain(user
, password
))
573 elif authmethod
== AUTH_LOGIN
:
574 (code
, resp
) = self
.docmd("AUTH",
575 "%s %s" % (AUTH_LOGIN
, encode_base64(user
, eol
="")))
577 raise SMTPAuthenticationError(code
, resp
)
578 (code
, resp
) = self
.docmd(encode_base64(password
, eol
=""))
579 elif authmethod
is None:
580 raise SMTPException("No suitable authentication method found.")
581 if code
not in (235, 503):
582 # 235 == 'Authentication successful'
583 # 503 == 'Error: already authenticated'
584 raise SMTPAuthenticationError(code
, resp
)
587 def starttls(self
, keyfile
= None, certfile
= None):
588 """Puts the connection to the SMTP server into TLS mode.
590 If the server supports TLS, this will encrypt the rest of the SMTP
591 session. If you provide the keyfile and certfile parameters,
592 the identity of the SMTP server and client can be checked. This,
593 however, depends on whether the socket module really checks the
596 (resp
, reply
) = self
.docmd("STARTTLS")
598 sslobj
= socket
.ssl(self
.sock
, keyfile
, certfile
)
599 self
.sock
= SSLFakeSocket(self
.sock
, sslobj
)
600 self
.file = SSLFakeFile(sslobj
)
603 def sendmail(self
, from_addr
, to_addrs
, msg
, mail_options
=[],
605 """This command performs an entire mail transaction.
608 - from_addr : The address sending this mail.
609 - to_addrs : A list of addresses to send this mail to. A bare
610 string will be treated as a list with 1 address.
611 - msg : The message to send.
612 - mail_options : List of ESMTP options (such as 8bitmime) for the
614 - rcpt_options : List of ESMTP options (such as DSN commands) for
615 all the rcpt commands.
617 If there has been no previous EHLO or HELO command this session, this
618 method tries ESMTP EHLO first. If the server does ESMTP, message size
619 and each of the specified options will be passed to it. If EHLO
620 fails, HELO will be tried and ESMTP options suppressed.
622 This method will return normally if the mail is accepted for at least
623 one recipient. It returns a dictionary, with one entry for each
624 recipient that was refused. Each entry contains a tuple of the SMTP
625 error code and the accompanying error message sent by the server.
627 This method may raise the following exceptions:
629 SMTPHeloError The server didn't reply properly to
631 SMTPRecipientsRefused The server rejected ALL recipients
633 SMTPSenderRefused The server didn't accept the from_addr.
634 SMTPDataError The server replied with an unexpected
635 error code (other than a refusal of
638 Note: the connection will be open even after an exception is raised.
643 >>> s=smtplib.SMTP("localhost")
644 >>> tolist=["one@one.org","two@two.org","three@three.org","four@four.org"]
647 ... Subject: testin'...
649 ... This is a test '''
650 >>> s.sendmail("me@my.org",tolist,msg)
651 { "three@three.org" : ( 550 ,"User unknown" ) }
654 In the above example, the message was accepted for delivery to three
655 of the four addresses, and one was rejected, with the error code
656 550. If all addresses are accepted, then the method will return an
660 if self
.helo_resp
is None and self
.ehlo_resp
is None:
661 if not (200 <= self
.ehlo()[0] <= 299):
662 (code
,resp
) = self
.helo()
663 if not (200 <= code
<= 299):
664 raise SMTPHeloError(code
, resp
)
667 # Hmmm? what's this? -ddm
668 # self.esmtp_features['7bit']=""
669 if self
.has_extn('size'):
670 esmtp_opts
.append("size=%d" % len(msg
))
671 for option
in mail_options
:
672 esmtp_opts
.append(option
)
674 (code
,resp
) = self
.mail(from_addr
, esmtp_opts
)
677 raise SMTPSenderRefused(code
, resp
, from_addr
)
679 if isinstance(to_addrs
, basestring
):
680 to_addrs
= [to_addrs
]
681 for each
in to_addrs
:
682 (code
,resp
)=self
.rcpt(each
, rcpt_options
)
683 if (code
!= 250) and (code
!= 251):
684 senderrs
[each
]=(code
,resp
)
685 if len(senderrs
)==len(to_addrs
):
686 # the server refused all our recipients
688 raise SMTPRecipientsRefused(senderrs
)
689 (code
,resp
) = self
.data(msg
)
692 raise SMTPDataError(code
, resp
)
693 #if we got here then somebody got our mail
698 """Close the connection to the SMTP server."""
708 """Terminate the SMTP session."""
713 # Test the sendmail method, which tests most of the others.
714 # Note: This always sends to localhost.
715 if __name__
== '__main__':
719 sys
.stdout
.write(prompt
+ ": ")
720 return sys
.stdin
.readline().strip()
722 fromaddr
= prompt("From")
723 toaddrs
= prompt("To").split(',')
724 print "Enter message, end with ^D:"
727 line
= sys
.stdin
.readline()
731 print "Message length is %d" % len(msg
)
733 server
= SMTP('localhost')
734 server
.set_debuglevel(1)
735 server
.sendmail(fromaddr
, toaddrs
, msg
)