Clarify portability and main program.
[python/dscho.git] / Lib / smtplib.py
blob07f53fa195a6ec54bc4e9c64e58955d4748df65b
1 #!/usr/bin/python
2 """SMTP/ESMTP client class.
4 Author: The Dragon De Monsyne <dragondm@integral.org>
5 ESMTP support, test code and doc fixes added by
6 Eric S. Raymond <esr@thyrsus.com>
7 Better RFC 821 compliance (MAIL and RCPT, and CRLF in data)
8 by Carey Evans <c.evans@clear.net.nz>, for picky mail servers.
10 (This was modified from the Python 1.5 library HTTP lib.)
12 This should follow RFC 821 (SMTP) and RFC 1869 (ESMTP).
14 Notes:
16 Please remember, when doing ESMTP, that the names of the SMTP service
17 extensions are NOT the same thing as the option keyords for the RCPT
18 and MAIL commands!
20 Example:
22 >>> import smtplib
23 >>> s=smtplib.SMTP("localhost")
24 >>> print s.help()
25 This is Sendmail version 8.8.4
26 Topics:
27 HELO EHLO MAIL RCPT DATA
28 RSET NOOP QUIT HELP VRFY
29 EXPN VERB ETRN DSN
30 For more info use "HELP <topic>".
31 To report bugs in the implementation send email to
32 sendmail-bugs@sendmail.org.
33 For local information send email to Postmaster at your site.
34 End of HELP info
35 >>> s.putcmd("vrfy","someone@here")
36 >>> s.getreply()
37 (250, "Somebody OverHere <somebody@here.my.org>")
38 >>> s.quit()
40 """
42 import socket
43 import string, re
44 import rfc822
45 import types
47 SMTP_PORT = 25
48 CRLF="\r\n"
50 # used for exceptions
51 SMTPServerDisconnected="Server not connected"
52 SMTPSenderRefused="Sender address refused"
53 SMTPRecipientsRefused="All Recipients refused"
54 SMTPDataError="Error transmitting message data"
56 def quoteaddr(addr):
57 """Quote a subset of the email addresses defined by RFC 821.
59 Should be able to handle anything rfc822.parseaddr can handle."""
61 m=None
62 try:
63 m=rfc822.parseaddr(addr)[1]
64 except AttributeError:
65 pass
66 if not m:
67 #something weird here.. punt -ddm
68 return addr
69 else:
70 return "<%s>" % m
72 def quotedata(data):
73 """Quote data for email.
75 Double leading '.', and change Unix newline '\n', or Mac '\r' into
76 Internet CRLF end-of-line."""
77 return re.sub(r'(?m)^\.', '..',
78 re.sub(r'(?:\r\n|\n|\r(?!\n))', CRLF, data))
80 class SMTP:
81 """This class manages a connection to an SMTP or ESMTP server.
82 SMTP Objects:
83 SMTP objects have the following attributes:
84 helo_resp
85 This is the message given by the server in responce to the
86 most recent HELO command.
88 ehlo_resp
89 This is the message given by the server in responce to the
90 most recent EHLO command. This is usually multiline.
92 does_esmtp
93 This is a True value _after you do an EHLO command_, if the
94 server supports ESMTP.
96 esmtp_features
97 This is a dictionary, which, if the server supports ESMTP,
98 will _after you do an EHLO command_, contain the names of the
99 SMTP service extentions this server supports, and their
100 parameters (if any).
101 Note, all extention names are mapped to lower case in the
102 dictionary.
104 For method docs, see each method's docstrings. In general, there is
105 a method of the same name to preform each SMTP comand, and there
106 is a method called 'sendmail' that will do an entiere mail
107 transaction."""
109 debuglevel = 0
110 file = None
111 helo_resp = None
112 ehlo_resp = None
113 does_esmtp = 0
115 def __init__(self, host = '', port = 0):
116 """Initialize a new instance.
118 If specified, `host' is the name of the remote host to which
119 to connect. If specified, `port' specifies the port to which
120 to connect. By default, smtplib.SMTP_PORT is used.
123 self.esmtp_features = {}
124 if host: self.connect(host, port)
126 def set_debuglevel(self, debuglevel):
127 """Set the debug output level.
129 A non-false value results in debug messages for connection and
130 for all messages sent to and received from the server.
133 self.debuglevel = debuglevel
135 def connect(self, host='localhost', port = 0):
136 """Connect to a host on a given port.
138 If the hostname ends with a colon (`:') followed by a number,
139 and there is no port specified, that suffix will be stripped
140 off and the number interpreted as the port number to use.
142 Note: This method is automatically invoked by __init__,
143 if a host is specified during instantiation.
146 if not port:
147 i = string.find(host, ':')
148 if i >= 0:
149 host, port = host[:i], host[i+1:]
150 try: port = string.atoi(port)
151 except string.atoi_error:
152 raise socket.error, "nonnumeric port"
153 if not port: port = SMTP_PORT
154 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
155 if self.debuglevel > 0: print 'connect:', (host, port)
156 self.sock.connect(host, port)
157 (code,msg)=self.getreply()
158 if self.debuglevel >0 : print "connect:", msg
159 return msg
161 def send(self, str):
162 """Send `str' to the server."""
163 if self.debuglevel > 0: print 'send:', `str`
164 if self.sock:
165 try:
166 self.sock.send(str)
167 except socket.error:
168 raise SMTPServerDisconnected
169 else:
170 raise SMTPServerDisconnected
172 def putcmd(self, cmd, args=""):
173 """Send a command to the server.
175 str = '%s %s%s' % (cmd, args, CRLF)
176 self.send(str)
178 def getreply(self):
179 """Get a reply from the server.
181 Returns a tuple consisting of:
182 - server response code (e.g. '250', or such, if all goes well)
183 Note: returns -1 if it can't read response code.
184 - server response string corresponding to response code
185 (note : multiline responses converted to a single,
186 multiline string)
188 resp=[]
189 self.file = self.sock.makefile('rb')
190 while 1:
191 line = self.file.readline()
192 if self.debuglevel > 0: print 'reply:', `line`
193 resp.append(string.strip(line[4:]))
194 code=line[:3]
195 #check if multiline resp
196 if line[3:4]!="-":
197 break
198 try:
199 errcode = string.atoi(code)
200 except(ValueError):
201 errcode = -1
203 errmsg = string.join(resp,"\n")
204 if self.debuglevel > 0:
205 print 'reply: retcode (%s); Msg: %s' % (errcode,errmsg)
206 return errcode, errmsg
208 def docmd(self, cmd, args=""):
209 """ Send a command, and return its response code """
211 self.putcmd(cmd,args)
212 (code,msg)=self.getreply()
213 return code
214 # std smtp commands
216 def helo(self, name=''):
217 """ SMTP 'helo' command. Hostname to send for this command
218 defaults to the FQDN of the local host """
219 name=string.strip(name)
220 if len(name)==0:
221 name=socket.gethostbyaddr(socket.gethostname())[0]
222 self.putcmd("helo",name)
223 (code,msg)=self.getreply()
224 self.helo_resp=msg
225 return code
227 def ehlo(self, name=''):
228 """ SMTP 'ehlo' command. Hostname to send for this command
229 defaults to the FQDN of the local host. """
230 name=string.strip(name)
231 if len(name)==0:
232 name=socket.gethostbyaddr(socket.gethostname())[0]
233 self.putcmd("ehlo",name)
234 (code,msg)=self.getreply()
235 # According to RFC1869 some (badly written)
236 # MTA's will disconnect on an ehlo. Toss an exception if
237 # that happens -ddm
238 if code == -1 and len(msg) == 0:
239 raise SMTPServerDisconnected
240 self.ehlo_resp=msg
241 if code<>250:
242 return code
243 self.does_esmtp=1
244 #parse the ehlo responce -ddm
245 resp=string.split(self.ehlo_resp,'\n')
246 del resp[0]
247 for each in resp:
248 m=re.match(r'(?P<feature>[A-Za-z0-9][A-Za-z0-9\-]*)',each)
249 if m:
250 feature=string.lower(m.group("feature"))
251 params=string.strip(m.string[m.end("feature"):])
252 self.esmtp_features[feature]=params
253 return code
255 def has_extn(self, opt):
256 """Does the server support a given SMTP service extension?"""
257 return self.esmtp_features.has_key(string.lower(opt))
259 def help(self, args=''):
260 """ SMTP 'help' command. Returns help text from server """
261 self.putcmd("help", args)
262 (code,msg)=self.getreply()
263 return msg
265 def rset(self):
266 """ SMTP 'rset' command. Resets session. """
267 code=self.docmd("rset")
268 return code
270 def noop(self):
271 """ SMTP 'noop' command. Doesn't do anything :> """
272 code=self.docmd("noop")
273 return code
275 def mail(self,sender,options=[]):
276 """ SMTP 'mail' command. Begins mail xfer session. """
277 optionlist = ''
278 if options and self.does_esmtp:
279 optionlist = string.join(options, ' ')
280 self.putcmd("mail", "FROM:%s %s" % (quoteaddr(sender) ,optionlist))
281 return self.getreply()
283 def rcpt(self,recip,options=[]):
284 """ SMTP 'rcpt' command. Indicates 1 recipient for this mail. """
285 optionlist = ''
286 if options and self.does_esmtp:
287 optionlist = string.join(options, ' ')
288 self.putcmd("rcpt","TO:%s %s" % (quoteaddr(recip),optionlist))
289 return self.getreply()
291 def data(self,msg):
292 """ SMTP 'DATA' command. Sends message data to server.
293 Automatically quotes lines beginning with a period per rfc821. """
294 self.putcmd("data")
295 (code,repl)=self.getreply()
296 if self.debuglevel >0 : print "data:", (code,repl)
297 if code <> 354:
298 return -1
299 else:
300 self.send(quotedata(msg))
301 self.send("%s.%s" % (CRLF, CRLF))
302 (code,msg)=self.getreply()
303 if self.debuglevel >0 : print "data:", (code,msg)
304 return code
306 def vrfy(self, address):
307 return self.verify(address)
309 def verify(self, address):
310 """ SMTP 'verify' command. Checks for address validity. """
311 self.putcmd("vrfy", quoteaddr(address))
312 return self.getreply()
314 def expn(self, address):
315 """ SMTP 'verify' command. Checks for address validity. """
316 self.putcmd("expn", quoteaddr(address))
317 return self.getreply()
320 #some useful methods
321 def sendmail(self, from_addr, to_addrs, msg, mail_options=[],
322 rcpt_options=[]):
323 """ This command performs an entire mail transaction.
324 The arguments are:
325 - from_addr : The address sending this mail.
326 - to_addrs : a list of addresses to send this mail to
327 (a string will be treated as a list with 1 address)
328 - msg : the message to send.
329 - mail_options : list of ESMTP options (such as 8bitmime)
330 for the mail command
331 - rcpt_options : List of ESMTP options (such as DSN commands)
332 for all the rcpt commands
333 If there has been no previous EHLO or HELO command this session,
334 this method tries ESMTP EHLO first. If the server does ESMTP, message
335 size and each of the specified options will be passed to it.
336 If EHLO fails, HELO will be tried and ESMTP options suppressed.
338 This method will return normally if the mail is accepted for at least
339 one recipient. Otherwise it will throw an exception (either
340 SMTPSenderRefused, SMTPRecipientsRefused, or SMTPDataError)
341 That is, if this method does not throw an exception, then someone
342 should get your mail. If this method does not throw an exception,
343 it returns a dictionary, with one entry for each recipient that was
344 refused.
346 Example:
348 >>> import smtplib
349 >>> s=smtplib.SMTP("localhost")
350 >>> tolist=["one@one.org","two@two.org","three@three.org","four@four.org"]
351 >>> msg = '''
352 ... From: Me@my.org
353 ... Subject: testin'...
355 ... This is a test '''
356 >>> s.sendmail("me@my.org",tolist,msg)
357 { "three@three.org" : ( 550 ,"User unknown" ) }
358 >>> s.quit()
360 In the above example, the message was accepted for delivery to
361 three of the four addresses, and one was rejected, with the error
362 code 550. If all addresses are accepted, then the method
363 will return an empty dictionary.
365 if not self.helo_resp and not self.ehlo_resp:
366 if self.ehlo() >= 400:
367 self.helo()
368 esmtp_opts = []
369 if self.does_esmtp:
370 # Hmmm? what's this? -ddm
371 # self.esmtp_features['7bit']=""
372 if self.has_extn('size'):
373 esmtp_opts.append("size=" + `len(msg)`)
374 for option in mail_options:
375 esmtp_opts.append(option)
377 (code,resp) = self.mail(from_addr, esmtp_opts)
378 if code <> 250:
379 self.rset()
380 raise SMTPSenderRefused
381 senderrs={}
382 if type(to_addrs) == types.StringType:
383 to_addrs = [to_addrs]
384 for each in to_addrs:
385 (code,resp)=self.rcpt(each, rcpt_options)
386 if (code <> 250) and (code <> 251):
387 senderrs[each]=(code,resp)
388 if len(senderrs)==len(to_addrs):
389 # the server refused all our recipients
390 self.rset()
391 raise SMTPRecipientsRefused
392 code=self.data(msg)
393 if code <>250 :
394 self.rset()
395 raise SMTPDataError
396 #if we got here then somebody got our mail
397 return senderrs
400 def close(self):
401 """Close the connection to the SMTP server."""
402 if self.file:
403 self.file.close()
404 self.file = None
405 if self.sock:
406 self.sock.close()
407 self.sock = None
410 def quit(self):
411 """Terminate the SMTP session."""
412 self.docmd("quit")
413 self.close()
415 # Test the sendmail method, which tests most of the others.
416 # Note: This always sends to localhost.
417 if __name__ == '__main__':
418 import sys, rfc822
420 def prompt(prompt):
421 sys.stdout.write(prompt + ": ")
422 return string.strip(sys.stdin.readline())
424 fromaddr = prompt("From")
425 toaddrs = string.splitfields(prompt("To"), ',')
426 print "Enter message, end with ^D:"
427 msg = ''
428 while 1:
429 line = sys.stdin.readline()
430 if not line:
431 break
432 msg = msg + line
433 print "Message length is " + `len(msg)`
435 server = SMTP('localhost')
436 server.set_debuglevel(1)
437 server.sendmail(fromaddr, toaddrs, msg)
438 server.quit()