3 '''SMTP/ESMTP client class.
5 This should follow RFC 821 (SMTP) and RFC 1869 (ESMTP).
9 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
16 >>> s=smtplib.SMTP("localhost")
18 This is Sendmail version 8.8.4
20 HELO EHLO MAIL RCPT DATA
21 RSET NOOP QUIT HELP VRFY
23 For more info use "HELP <topic>".
24 To report bugs in the implementation send email to
25 sendmail-bugs@sendmail.org.
26 For local information send email to Postmaster at your site.
28 >>> s.putcmd("vrfy","someone@here")
30 (250, "Somebody OverHere <somebody@here.my.org>")
34 # Author: The Dragon De Monsyne <dragondm@integral.org>
35 # ESMTP support, test code and doc fixes added by
36 # Eric S. Raymond <esr@thyrsus.com>
37 # Better RFC 821 compliance (MAIL and RCPT, and CRLF in data)
38 # by Carey Evans <c.evans@clear.net.nz>, for picky mail servers.
40 # This was modified from the Python 1.5 library HTTP lib.
47 __all__
= ["SMTPException","SMTPServerDisconnected","SMTPResponseException",
48 "SMTPSenderRefused","SMTPRecipientsRefused","SMTPDataError",
49 "SMTPConnectError","SMTPHeloError","quoteaddr","quotedata",
55 # Exception classes used by this module.
56 class SMTPException(Exception):
57 """Base class for all exceptions raised by this module."""
59 class SMTPServerDisconnected(SMTPException
):
60 """Not connected to any SMTP server.
62 This exception is raised when the server unexpectedly disconnects,
63 or when an attempt is made to use the SMTP instance before
64 connecting it to a server.
67 class SMTPResponseException(SMTPException
):
68 """Base class for all exceptions that include an SMTP error code.
70 These exceptions are generated in some instances when the SMTP
71 server returns an error code. The error code is stored in the
72 `smtp_code' attribute of the error, and the `smtp_error' attribute
73 is set to the error message.
76 def __init__(self
, code
, msg
):
79 self
.args
= (code
, msg
)
81 class SMTPSenderRefused(SMTPResponseException
):
82 """Sender address refused.
83 In addition to the attributes set by on all SMTPResponseException
84 exceptions, this sets `sender' to the string that the SMTP refused.
87 def __init__(self
, code
, msg
, sender
):
91 self
.args
= (code
, msg
, sender
)
93 class SMTPRecipientsRefused(SMTPException
):
94 """All recipient addresses refused.
95 The errors for each recipient are accessible through the attribute
96 'recipients', which is a dictionary of exactly the same sort as
97 SMTP.sendmail() returns.
100 def __init__(self
, recipients
):
101 self
.recipients
= recipients
102 self
.args
= ( recipients
,)
105 class SMTPDataError(SMTPResponseException
):
106 """The SMTP server didn't accept the data."""
108 class SMTPConnectError(SMTPResponseException
):
109 """Error during connection establishment."""
111 class SMTPHeloError(SMTPResponseException
):
112 """The server refused our HELO reply."""
116 """Quote a subset of the email addresses defined by RFC 821.
118 Should be able to handle anything rfc822.parseaddr can handle.
122 m
=rfc822
.parseaddr(addr
)[1]
123 except AttributeError:
126 #something weird here.. punt -ddm
132 """Quote data for email.
134 Double leading '.', and change Unix newline '\\n', or Mac '\\r' into
135 Internet CRLF end-of-line.
137 return re
.sub(r
'(?m)^\.', '..',
138 re
.sub(r
'(?:\r\n|\n|\r(?!\n))', CRLF
, data
))
142 """This class manages a connection to an SMTP or ESMTP server.
144 SMTP objects have the following attributes:
146 This is the message given by the server in response to the
147 most recent HELO command.
150 This is the message given by the server in response to the
151 most recent EHLO command. This is usually multiline.
154 This is a True value _after you do an EHLO command_, if the
155 server supports ESMTP.
158 This is a dictionary, which, if the server supports ESMTP,
159 will _after you do an EHLO command_, contain the names of the
160 SMTP service extensions this server supports, and their
163 Note, all extension names are mapped to lower case in the
166 See each method's docstrings for details. In general, there is a
167 method of the same name to perform each SMTP command. There is also a
168 method called 'sendmail' that will do an entire mail transaction.
176 def __init__(self
, host
= '', port
= 0):
177 """Initialize a new instance.
179 If specified, `host' is the name of the remote host to which to
180 connect. If specified, `port' specifies the port to which to connect.
181 By default, smtplib.SMTP_PORT is used. An SMTPConnectError is raised
182 if the specified `host' doesn't respond correctly.
185 self
.esmtp_features
= {}
187 (code
, msg
) = self
.connect(host
, port
)
189 raise SMTPConnectError(code
, msg
)
191 def set_debuglevel(self
, debuglevel
):
192 """Set the debug output level.
194 A non-false value results in debug messages for connection and for all
195 messages sent to and received from the server.
198 self
.debuglevel
= debuglevel
200 def connect(self
, host
='localhost', port
= 0):
201 """Connect to a host on a given port.
203 If the hostname ends with a colon (`:') followed by a number, and
204 there is no port specified, that suffix will be stripped off and the
205 number interpreted as the port number to use.
207 Note: This method is automatically invoked by __init__, if a host is
208 specified during instantiation.
214 host
, port
= host
[:i
], host
[i
+1:]
215 try: port
= int(port
)
217 raise socket
.error
, "nonnumeric port"
218 if not port
: port
= SMTP_PORT
219 self
.sock
= socket
.socket(socket
.AF_INET
, socket
.SOCK_STREAM
)
220 if self
.debuglevel
> 0: print 'connect:', (host
, port
)
222 self
.sock
.connect((host
, port
))
226 (code
,msg
)=self
.getreply()
227 if self
.debuglevel
>0 : print "connect:", msg
231 """Send `str' to the server."""
232 if self
.debuglevel
> 0: print 'send:', `
str`
235 self
.sock
.sendall(str)
237 raise SMTPServerDisconnected('Server not connected')
239 raise SMTPServerDisconnected('please run connect() first')
241 def putcmd(self
, cmd
, args
=""):
242 """Send a command to the server."""
244 str = '%s%s' % (cmd
, CRLF
)
246 str = '%s %s%s' % (cmd
, args
, CRLF
)
250 """Get a reply from the server.
252 Returns a tuple consisting of:
254 - server response code (e.g. '250', or such, if all goes well)
255 Note: returns -1 if it can't read response code.
257 - server response string corresponding to response code (multiline
258 responses are converted to a single, multiline string).
260 Raises SMTPServerDisconnected if end-of-file is reached.
263 if self
.file is None:
264 self
.file = self
.sock
.makefile('rb')
266 line
= self
.file.readline()
269 raise SMTPServerDisconnected("Connection unexpectedly closed")
270 if self
.debuglevel
> 0: print 'reply:', `line`
271 resp
.append(line
[4:].strip())
273 # Check that the error code is syntactically correct.
274 # Don't attempt to read a continuation line if it is broken.
280 # Check if multiline response.
284 errmsg
= "\n".join(resp
)
285 if self
.debuglevel
> 0:
286 print 'reply: retcode (%s); Msg: %s' % (errcode
,errmsg
)
287 return errcode
, errmsg
289 def docmd(self
, cmd
, args
=""):
290 """Send a command, and return its response code."""
291 self
.putcmd(cmd
,args
)
292 return self
.getreply()
295 def helo(self
, name
=''):
296 """SMTP 'helo' command.
297 Hostname to send for this command defaults to the FQDN of the local
301 self
.putcmd("helo", name
)
303 self
.putcmd("helo", socket
.getfqdn())
304 (code
,msg
)=self
.getreply()
308 def ehlo(self
, name
=''):
309 """ SMTP 'ehlo' command.
310 Hostname to send for this command defaults to the FQDN of the local
314 self
.putcmd("ehlo", name
)
316 self
.putcmd("ehlo", socket
.getfqdn())
317 (code
,msg
)=self
.getreply()
318 # According to RFC1869 some (badly written)
319 # MTA's will disconnect on an ehlo. Toss an exception if
321 if code
== -1 and len(msg
) == 0:
322 raise SMTPServerDisconnected("Server not connected")
327 #parse the ehlo response -ddm
328 resp
=self
.ehlo_resp
.split('\n')
331 m
=re
.match(r
'(?P<feature>[A-Za-z0-9][A-Za-z0-9\-]*)',each
)
333 feature
=m
.group("feature").lower()
334 params
=m
.string
[m
.end("feature"):].strip()
335 self
.esmtp_features
[feature
]=params
338 def has_extn(self
, opt
):
339 """Does the server support a given SMTP service extension?"""
340 return self
.esmtp_features
.has_key(opt
.lower())
342 def help(self
, args
=''):
343 """SMTP 'help' command.
344 Returns help text from server."""
345 self
.putcmd("help", args
)
346 return self
.getreply()
349 """SMTP 'rset' command -- resets session."""
350 return self
.docmd("rset")
353 """SMTP 'noop' command -- doesn't do anything :>"""
354 return self
.docmd("noop")
356 def mail(self
,sender
,options
=[]):
357 """SMTP 'mail' command -- begins mail xfer session."""
359 if options
and self
.does_esmtp
:
360 optionlist
= ' ' + ' '.join(options
)
361 self
.putcmd("mail", "FROM:%s%s" % (quoteaddr(sender
) ,optionlist
))
362 return self
.getreply()
364 def rcpt(self
,recip
,options
=[]):
365 """SMTP 'rcpt' command -- indicates 1 recipient for this mail."""
367 if options
and self
.does_esmtp
:
368 optionlist
= ' ' + ' '.join(options
)
369 self
.putcmd("rcpt","TO:%s%s" % (quoteaddr(recip
),optionlist
))
370 return self
.getreply()
373 """SMTP 'DATA' command -- sends message data to server.
375 Automatically quotes lines beginning with a period per rfc821.
376 Raises SMTPDataError if there is an unexpected reply to the
377 DATA command; the return value from this method is the final
378 response code received when the all data is sent.
381 (code
,repl
)=self
.getreply()
382 if self
.debuglevel
>0 : print "data:", (code
,repl
)
384 raise SMTPDataError(code
,repl
)
391 (code
,msg
)=self
.getreply()
392 if self
.debuglevel
>0 : print "data:", (code
,msg
)
395 def verify(self
, address
):
396 """SMTP 'verify' command -- checks for address validity."""
397 self
.putcmd("vrfy", quoteaddr(address
))
398 return self
.getreply()
402 def expn(self
, address
):
403 """SMTP 'verify' command -- checks for address validity."""
404 self
.putcmd("expn", quoteaddr(address
))
405 return self
.getreply()
407 # some useful methods
408 def sendmail(self
, from_addr
, to_addrs
, msg
, mail_options
=[],
410 """This command performs an entire mail transaction.
413 - from_addr : The address sending this mail.
414 - to_addrs : A list of addresses to send this mail to. A bare
415 string will be treated as a list with 1 address.
416 - msg : The message to send.
417 - mail_options : List of ESMTP options (such as 8bitmime) for the
419 - rcpt_options : List of ESMTP options (such as DSN commands) for
420 all the rcpt commands.
422 If there has been no previous EHLO or HELO command this session, this
423 method tries ESMTP EHLO first. If the server does ESMTP, message size
424 and each of the specified options will be passed to it. If EHLO
425 fails, HELO will be tried and ESMTP options suppressed.
427 This method will return normally if the mail is accepted for at least
428 one recipient. It returns a dictionary, with one entry for each
429 recipient that was refused. Each entry contains a tuple of the SMTP
430 error code and the accompanying error message sent by the server.
432 This method may raise the following exceptions:
434 SMTPHeloError The server didn't reply properly to
436 SMTPRecipientsRefused The server rejected ALL recipients
438 SMTPSenderRefused The server didn't accept the from_addr.
439 SMTPDataError The server replied with an unexpected
440 error code (other than a refusal of
443 Note: the connection will be open even after an exception is raised.
448 >>> s=smtplib.SMTP("localhost")
449 >>> tolist=["one@one.org","two@two.org","three@three.org","four@four.org"]
452 ... Subject: testin'...
454 ... This is a test '''
455 >>> s.sendmail("me@my.org",tolist,msg)
456 { "three@three.org" : ( 550 ,"User unknown" ) }
459 In the above example, the message was accepted for delivery to three
460 of the four addresses, and one was rejected, with the error code
461 550. If all addresses are accepted, then the method will return an
465 if self
.helo_resp
is None and self
.ehlo_resp
is None:
466 if not (200 <= self
.ehlo()[0] <= 299):
467 (code
,resp
) = self
.helo()
468 if not (200 <= code
<= 299):
469 raise SMTPHeloError(code
, resp
)
472 # Hmmm? what's this? -ddm
473 # self.esmtp_features['7bit']=""
474 if self
.has_extn('size'):
475 esmtp_opts
.append("size=" + `
len(msg
)`
)
476 for option
in mail_options
:
477 esmtp_opts
.append(option
)
479 (code
,resp
) = self
.mail(from_addr
, esmtp_opts
)
482 raise SMTPSenderRefused(code
, resp
, from_addr
)
484 if type(to_addrs
) == types
.StringType
:
485 to_addrs
= [to_addrs
]
486 for each
in to_addrs
:
487 (code
,resp
)=self
.rcpt(each
, rcpt_options
)
488 if (code
!= 250) and (code
!= 251):
489 senderrs
[each
]=(code
,resp
)
490 if len(senderrs
)==len(to_addrs
):
491 # the server refused all our recipients
493 raise SMTPRecipientsRefused(senderrs
)
494 (code
,resp
) = self
.data(msg
)
497 raise SMTPDataError(code
, resp
)
498 #if we got here then somebody got our mail
503 """Close the connection to the SMTP server."""
513 """Terminate the SMTP session."""
518 # Test the sendmail method, which tests most of the others.
519 # Note: This always sends to localhost.
520 if __name__
== '__main__':
524 sys
.stdout
.write(prompt
+ ": ")
525 return sys
.stdin
.readline().strip()
527 fromaddr
= prompt("From")
528 toaddrs
= prompt("To").split(',')
529 print "Enter message, end with ^D:"
532 line
= sys
.stdin
.readline()
536 print "Message length is " + `
len(msg
)`
538 server
= SMTP('localhost')
539 server
.set_debuglevel(1)
540 server
.sendmail(fromaddr
, toaddrs
, msg
)