|
1 #! /usr/bin/env python |
|
2 |
|
3 '''SMTP/ESMTP client class. |
|
4 |
|
5 This should follow RFC 821 (SMTP), RFC 1869 (ESMTP), RFC 2554 (SMTP |
|
6 Authentication) and RFC 2487 (Secure SMTP over TLS). |
|
7 |
|
8 Notes: |
|
9 |
|
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 |
|
12 and MAIL commands! |
|
13 |
|
14 Example: |
|
15 |
|
16 >>> import smtplib |
|
17 >>> s=smtplib.SMTP("localhost") |
|
18 >>> print s.help() |
|
19 This is Sendmail version 8.8.4 |
|
20 Topics: |
|
21 HELO EHLO MAIL RCPT DATA |
|
22 RSET NOOP QUIT HELP VRFY |
|
23 EXPN VERB ETRN DSN |
|
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. |
|
28 End of HELP info |
|
29 >>> s.putcmd("vrfy","someone@here") |
|
30 >>> s.getreply() |
|
31 (250, "Somebody OverHere <somebody@here.my.org>") |
|
32 >>> s.quit() |
|
33 ''' |
|
34 |
|
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>. |
|
41 # |
|
42 # This was modified from the Python 1.5 library HTTP lib. |
|
43 |
|
44 import socket |
|
45 import re |
|
46 import email.Utils |
|
47 import base64 |
|
48 import hmac |
|
49 from email.base64MIME import encode as encode_base64 |
|
50 from sys import stderr |
|
51 |
|
52 __all__ = ["SMTPException","SMTPServerDisconnected","SMTPResponseException", |
|
53 "SMTPSenderRefused","SMTPRecipientsRefused","SMTPDataError", |
|
54 "SMTPConnectError","SMTPHeloError","SMTPAuthenticationError", |
|
55 "quoteaddr","quotedata","SMTP"] |
|
56 |
|
57 SMTP_PORT = 25 |
|
58 CRLF="\r\n" |
|
59 |
|
60 OLDSTYLE_AUTH = re.compile(r"auth=(.*)", re.I) |
|
61 |
|
62 # Exception classes used by this module. |
|
63 class SMTPException(Exception): |
|
64 """Base class for all exceptions raised by this module.""" |
|
65 |
|
66 class SMTPServerDisconnected(SMTPException): |
|
67 """Not connected to any SMTP server. |
|
68 |
|
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. |
|
72 """ |
|
73 |
|
74 class SMTPResponseException(SMTPException): |
|
75 """Base class for all exceptions that include an SMTP error code. |
|
76 |
|
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. |
|
81 """ |
|
82 |
|
83 def __init__(self, code, msg): |
|
84 self.smtp_code = code |
|
85 self.smtp_error = msg |
|
86 self.args = (code, msg) |
|
87 |
|
88 class SMTPSenderRefused(SMTPResponseException): |
|
89 """Sender address refused. |
|
90 |
|
91 In addition to the attributes set by on all SMTPResponseException |
|
92 exceptions, this sets `sender' to the string that the SMTP refused. |
|
93 """ |
|
94 |
|
95 def __init__(self, code, msg, sender): |
|
96 self.smtp_code = code |
|
97 self.smtp_error = msg |
|
98 self.sender = sender |
|
99 self.args = (code, msg, sender) |
|
100 |
|
101 class SMTPRecipientsRefused(SMTPException): |
|
102 """All recipient addresses refused. |
|
103 |
|
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. |
|
107 """ |
|
108 |
|
109 def __init__(self, recipients): |
|
110 self.recipients = recipients |
|
111 self.args = ( recipients,) |
|
112 |
|
113 |
|
114 class SMTPDataError(SMTPResponseException): |
|
115 """The SMTP server didn't accept the data.""" |
|
116 |
|
117 class SMTPConnectError(SMTPResponseException): |
|
118 """Error during connection establishment.""" |
|
119 |
|
120 class SMTPHeloError(SMTPResponseException): |
|
121 """The server refused our HELO reply.""" |
|
122 |
|
123 class SMTPAuthenticationError(SMTPResponseException): |
|
124 """Authentication error. |
|
125 |
|
126 Most probably the server didn't accept the username/password |
|
127 combination provided. |
|
128 """ |
|
129 |
|
130 class SSLFakeSocket: |
|
131 """A fake socket object that really wraps a SSLObject. |
|
132 |
|
133 It only supports what is needed in smtplib. |
|
134 """ |
|
135 def __init__(self, realsock, sslobj): |
|
136 self.realsock = realsock |
|
137 self.sslobj = sslobj |
|
138 |
|
139 def send(self, str): |
|
140 self.sslobj.write(str) |
|
141 return len(str) |
|
142 |
|
143 sendall = send |
|
144 |
|
145 def close(self): |
|
146 self.realsock.close() |
|
147 |
|
148 class SSLFakeFile: |
|
149 """A fake file like object that really wraps a SSLObject. |
|
150 |
|
151 It only supports what is needed in smtplib. |
|
152 """ |
|
153 def __init__(self, sslobj): |
|
154 self.sslobj = sslobj |
|
155 |
|
156 def readline(self): |
|
157 str = "" |
|
158 chr = None |
|
159 while chr != "\n": |
|
160 chr = self.sslobj.read(1) |
|
161 str += chr |
|
162 return str |
|
163 |
|
164 def close(self): |
|
165 pass |
|
166 |
|
167 def quoteaddr(addr): |
|
168 """Quote a subset of the email addresses defined by RFC 821. |
|
169 |
|
170 Should be able to handle anything rfc822.parseaddr can handle. |
|
171 """ |
|
172 m = (None, None) |
|
173 try: |
|
174 m = email.Utils.parseaddr(addr)[1] |
|
175 except AttributeError: |
|
176 pass |
|
177 if m == (None, None): # Indicates parse failure or AttributeError |
|
178 # something weird here.. punt -ddm |
|
179 return "<%s>" % addr |
|
180 elif m is None: |
|
181 # the sender wants an empty return address |
|
182 return "<>" |
|
183 else: |
|
184 return "<%s>" % m |
|
185 |
|
186 def quotedata(data): |
|
187 """Quote data for email. |
|
188 |
|
189 Double leading '.', and change Unix newline '\\n', or Mac '\\r' into |
|
190 Internet CRLF end-of-line. |
|
191 """ |
|
192 return re.sub(r'(?m)^\.', '..', |
|
193 re.sub(r'(?:\r\n|\n|\r(?!\n))', CRLF, data)) |
|
194 |
|
195 |
|
196 class SMTP: |
|
197 """This class manages a connection to an SMTP or ESMTP server. |
|
198 SMTP Objects: |
|
199 SMTP objects have the following attributes: |
|
200 helo_resp |
|
201 This is the message given by the server in response to the |
|
202 most recent HELO command. |
|
203 |
|
204 ehlo_resp |
|
205 This is the message given by the server in response to the |
|
206 most recent EHLO command. This is usually multiline. |
|
207 |
|
208 does_esmtp |
|
209 This is a True value _after you do an EHLO command_, if the |
|
210 server supports ESMTP. |
|
211 |
|
212 esmtp_features |
|
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 |
|
216 parameters (if any). |
|
217 |
|
218 Note, all extension names are mapped to lower case in the |
|
219 dictionary. |
|
220 |
|
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. |
|
224 """ |
|
225 debuglevel = 0 |
|
226 file = None |
|
227 helo_resp = None |
|
228 ehlo_resp = None |
|
229 does_esmtp = 0 |
|
230 |
|
231 def __init__(self, host = '', port = 0, local_hostname = None): |
|
232 """Initialize a new instance. |
|
233 |
|
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(). |
|
240 |
|
241 """ |
|
242 self.esmtp_features = {} |
|
243 if host: |
|
244 (code, msg) = self.connect(host, port) |
|
245 if code != 220: |
|
246 raise SMTPConnectError(code, msg) |
|
247 if local_hostname is not None: |
|
248 self.local_hostname = local_hostname |
|
249 else: |
|
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() |
|
254 if '.' in fqdn: |
|
255 self.local_hostname = fqdn |
|
256 else: |
|
257 # We can't find an fqdn hostname, so use a domain literal |
|
258 addr = '127.0.0.1' |
|
259 try: |
|
260 addr = socket.gethostbyname(socket.gethostname()) |
|
261 except socket.gaierror: |
|
262 pass |
|
263 self.local_hostname = '[%s]' % addr |
|
264 |
|
265 def set_debuglevel(self, debuglevel): |
|
266 """Set the debug output level. |
|
267 |
|
268 A non-false value results in debug messages for connection and for all |
|
269 messages sent to and received from the server. |
|
270 |
|
271 """ |
|
272 self.debuglevel = debuglevel |
|
273 |
|
274 def connect(self, host='localhost', port = 0): |
|
275 """Connect to a host on a given port. |
|
276 |
|
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. |
|
280 |
|
281 Note: This method is automatically invoked by __init__, if a host is |
|
282 specified during instantiation. |
|
283 |
|
284 """ |
|
285 if not port and (host.find(':') == host.rfind(':')): |
|
286 i = host.rfind(':') |
|
287 if i >= 0: |
|
288 host, port = host[:i], host[i+1:] |
|
289 try: port = int(port) |
|
290 except ValueError: |
|
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" |
|
295 self.sock = None |
|
296 for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM): |
|
297 af, socktype, proto, canonname, sa = res |
|
298 try: |
|
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 |
|
304 if self.sock: |
|
305 self.sock.close() |
|
306 self.sock = None |
|
307 continue |
|
308 break |
|
309 if not self.sock: |
|
310 raise socket.error, msg |
|
311 (code, msg) = self.getreply() |
|
312 if self.debuglevel > 0: print>>stderr, "connect:", msg |
|
313 return (code, msg) |
|
314 |
|
315 def send(self, str): |
|
316 """Send `str' to the server.""" |
|
317 if self.debuglevel > 0: print>>stderr, 'send:', repr(str) |
|
318 if self.sock: |
|
319 try: |
|
320 self.sock.sendall(str) |
|
321 except socket.error: |
|
322 self.close() |
|
323 raise SMTPServerDisconnected('Server not connected') |
|
324 else: |
|
325 raise SMTPServerDisconnected('please run connect() first') |
|
326 |
|
327 def putcmd(self, cmd, args=""): |
|
328 """Send a command to the server.""" |
|
329 if args == "": |
|
330 str = '%s%s' % (cmd, CRLF) |
|
331 else: |
|
332 str = '%s %s%s' % (cmd, args, CRLF) |
|
333 self.send(str) |
|
334 |
|
335 def getreply(self): |
|
336 """Get a reply from the server. |
|
337 |
|
338 Returns a tuple consisting of: |
|
339 |
|
340 - server response code (e.g. '250', or such, if all goes well) |
|
341 Note: returns -1 if it can't read response code. |
|
342 |
|
343 - server response string corresponding to response code (multiline |
|
344 responses are converted to a single, multiline string). |
|
345 |
|
346 Raises SMTPServerDisconnected if end-of-file is reached. |
|
347 """ |
|
348 resp=[] |
|
349 if self.file is None: |
|
350 self.file = self.sock.makefile('rb') |
|
351 while 1: |
|
352 line = self.file.readline() |
|
353 if line == '': |
|
354 self.close() |
|
355 raise SMTPServerDisconnected("Connection unexpectedly closed") |
|
356 if self.debuglevel > 0: print>>stderr, 'reply:', repr(line) |
|
357 resp.append(line[4:].strip()) |
|
358 code=line[:3] |
|
359 # Check that the error code is syntactically correct. |
|
360 # Don't attempt to read a continuation line if it is broken. |
|
361 try: |
|
362 errcode = int(code) |
|
363 except ValueError: |
|
364 errcode = -1 |
|
365 break |
|
366 # Check if multiline response. |
|
367 if line[3:4]!="-": |
|
368 break |
|
369 |
|
370 errmsg = "\n".join(resp) |
|
371 if self.debuglevel > 0: |
|
372 print>>stderr, 'reply: retcode (%s); Msg: %s' % (errcode,errmsg) |
|
373 return errcode, errmsg |
|
374 |
|
375 def docmd(self, cmd, args=""): |
|
376 """Send a command, and return its response code.""" |
|
377 self.putcmd(cmd,args) |
|
378 return self.getreply() |
|
379 |
|
380 # std smtp commands |
|
381 def helo(self, name=''): |
|
382 """SMTP 'helo' command. |
|
383 Hostname to send for this command defaults to the FQDN of the local |
|
384 host. |
|
385 """ |
|
386 self.putcmd("helo", name or self.local_hostname) |
|
387 (code,msg)=self.getreply() |
|
388 self.helo_resp=msg |
|
389 return (code,msg) |
|
390 |
|
391 def ehlo(self, name=''): |
|
392 """ SMTP 'ehlo' command. |
|
393 Hostname to send for this command defaults to the FQDN of the local |
|
394 host. |
|
395 """ |
|
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 |
|
401 # that happens -ddm |
|
402 if code == -1 and len(msg) == 0: |
|
403 self.close() |
|
404 raise SMTPServerDisconnected("Server not connected") |
|
405 self.ehlo_resp=msg |
|
406 if code != 250: |
|
407 return (code,msg) |
|
408 self.does_esmtp=1 |
|
409 #parse the ehlo response -ddm |
|
410 resp=self.ehlo_resp.split('\n') |
|
411 del resp[0] |
|
412 for each in resp: |
|
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, |
|
415 # because: |
|
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) |
|
420 if auth_match: |
|
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] |
|
424 continue |
|
425 |
|
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) |
|
431 if m: |
|
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, "") \ |
|
436 + " " + params |
|
437 else: |
|
438 self.esmtp_features[feature]=params |
|
439 return (code,msg) |
|
440 |
|
441 def has_extn(self, opt): |
|
442 """Does the server support a given SMTP service extension?""" |
|
443 return opt.lower() in self.esmtp_features |
|
444 |
|
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] |
|
450 |
|
451 def rset(self): |
|
452 """SMTP 'rset' command -- resets session.""" |
|
453 return self.docmd("rset") |
|
454 |
|
455 def noop(self): |
|
456 """SMTP 'noop' command -- doesn't do anything :>""" |
|
457 return self.docmd("noop") |
|
458 |
|
459 def mail(self,sender,options=[]): |
|
460 """SMTP 'mail' command -- begins mail xfer session.""" |
|
461 optionlist = '' |
|
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() |
|
466 |
|
467 def rcpt(self,recip,options=[]): |
|
468 """SMTP 'rcpt' command -- indicates 1 recipient for this mail.""" |
|
469 optionlist = '' |
|
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() |
|
474 |
|
475 def data(self,msg): |
|
476 """SMTP 'DATA' command -- sends message data to server. |
|
477 |
|
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. |
|
482 """ |
|
483 self.putcmd("data") |
|
484 (code,repl)=self.getreply() |
|
485 if self.debuglevel >0 : print>>stderr, "data:", (code,repl) |
|
486 if code != 354: |
|
487 raise SMTPDataError(code,repl) |
|
488 else: |
|
489 q = quotedata(msg) |
|
490 if q[-2:] != CRLF: |
|
491 q = q + CRLF |
|
492 q = q + "." + CRLF |
|
493 self.send(q) |
|
494 (code,msg)=self.getreply() |
|
495 if self.debuglevel >0 : print>>stderr, "data:", (code,msg) |
|
496 return (code,msg) |
|
497 |
|
498 def verify(self, address): |
|
499 """SMTP 'verify' command -- checks for address validity.""" |
|
500 self.putcmd("vrfy", quoteaddr(address)) |
|
501 return self.getreply() |
|
502 # a.k.a. |
|
503 vrfy=verify |
|
504 |
|
505 def expn(self, address): |
|
506 """SMTP 'verify' command -- checks for address validity.""" |
|
507 self.putcmd("expn", quoteaddr(address)) |
|
508 return self.getreply() |
|
509 |
|
510 # some useful methods |
|
511 |
|
512 def login(self, user, password): |
|
513 """Log in on an SMTP server that requires authentication. |
|
514 |
|
515 The arguments are: |
|
516 - user: The user name to authenticate with. |
|
517 - password: The password for the authentication. |
|
518 |
|
519 If there has been no previous EHLO or HELO command this session, this |
|
520 method tries ESMTP EHLO first. |
|
521 |
|
522 This method will return normally if the authentication was successful. |
|
523 |
|
524 This method may raise the following exceptions: |
|
525 |
|
526 SMTPHeloError The server didn't reply properly to |
|
527 the helo greeting. |
|
528 SMTPAuthenticationError The server didn't accept the username/ |
|
529 password combination. |
|
530 SMTPException No suitable authentication method was |
|
531 found. |
|
532 """ |
|
533 |
|
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="") |
|
538 |
|
539 def encode_plain(user, password): |
|
540 return encode_base64("\0%s\0%s" % (user, password), eol="") |
|
541 |
|
542 |
|
543 AUTH_PLAIN = "PLAIN" |
|
544 AUTH_CRAM_MD5 = "CRAM-MD5" |
|
545 AUTH_LOGIN = "LOGIN" |
|
546 |
|
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) |
|
552 |
|
553 if not self.has_extn("auth"): |
|
554 raise SMTPException("SMTP AUTH extension not supported by server.") |
|
555 |
|
556 # Authentication methods the server supports: |
|
557 authlist = self.esmtp_features["auth"].split() |
|
558 |
|
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] |
|
563 |
|
564 # Determine the authentication method we'll use |
|
565 authmethod = None |
|
566 for method in preferred_auths: |
|
567 if method in authlist: |
|
568 authmethod = method |
|
569 break |
|
570 |
|
571 if authmethod == AUTH_CRAM_MD5: |
|
572 (code, resp) = self.docmd("AUTH", AUTH_CRAM_MD5) |
|
573 if code == 503: |
|
574 # 503 == 'Error: already authenticated' |
|
575 return (code, resp) |
|
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=""))) |
|
583 if code != 334: |
|
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) |
|
592 return (code, resp) |
|
593 |
|
594 def starttls(self, keyfile = None, certfile = None): |
|
595 """Puts the connection to the SMTP server into TLS mode. |
|
596 |
|
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 |
|
601 certificates. |
|
602 """ |
|
603 (resp, reply) = self.docmd("STARTTLS") |
|
604 if resp == 220: |
|
605 sslobj = socket.ssl(self.sock, keyfile, certfile) |
|
606 self.sock = SSLFakeSocket(self.sock, sslobj) |
|
607 self.file = SSLFakeFile(sslobj) |
|
608 # RFC 3207: |
|
609 # The client MUST discard any knowledge obtained from |
|
610 # the server, such as the list of SMTP service extensions, |
|
611 # which was not obtained from the TLS negotiation itself. |
|
612 self.helo_resp = None |
|
613 self.ehlo_resp = None |
|
614 self.esmtp_features = {} |
|
615 self.does_esmtp = 0 |
|
616 return (resp, reply) |
|
617 |
|
618 def sendmail(self, from_addr, to_addrs, msg, mail_options=[], |
|
619 rcpt_options=[]): |
|
620 """This command performs an entire mail transaction. |
|
621 |
|
622 The arguments are: |
|
623 - from_addr : The address sending this mail. |
|
624 - to_addrs : A list of addresses to send this mail to. A bare |
|
625 string will be treated as a list with 1 address. |
|
626 - msg : The message to send. |
|
627 - mail_options : List of ESMTP options (such as 8bitmime) for the |
|
628 mail command. |
|
629 - rcpt_options : List of ESMTP options (such as DSN commands) for |
|
630 all the rcpt commands. |
|
631 |
|
632 If there has been no previous EHLO or HELO command this session, this |
|
633 method tries ESMTP EHLO first. If the server does ESMTP, message size |
|
634 and each of the specified options will be passed to it. If EHLO |
|
635 fails, HELO will be tried and ESMTP options suppressed. |
|
636 |
|
637 This method will return normally if the mail is accepted for at least |
|
638 one recipient. It returns a dictionary, with one entry for each |
|
639 recipient that was refused. Each entry contains a tuple of the SMTP |
|
640 error code and the accompanying error message sent by the server. |
|
641 |
|
642 This method may raise the following exceptions: |
|
643 |
|
644 SMTPHeloError The server didn't reply properly to |
|
645 the helo greeting. |
|
646 SMTPRecipientsRefused The server rejected ALL recipients |
|
647 (no mail was sent). |
|
648 SMTPSenderRefused The server didn't accept the from_addr. |
|
649 SMTPDataError The server replied with an unexpected |
|
650 error code (other than a refusal of |
|
651 a recipient). |
|
652 |
|
653 Note: the connection will be open even after an exception is raised. |
|
654 |
|
655 Example: |
|
656 |
|
657 >>> import smtplib |
|
658 >>> s=smtplib.SMTP("localhost") |
|
659 >>> tolist=["one@one.org","two@two.org","three@three.org","four@four.org"] |
|
660 >>> msg = '''\\ |
|
661 ... From: Me@my.org |
|
662 ... Subject: testin'... |
|
663 ... |
|
664 ... This is a test ''' |
|
665 >>> s.sendmail("me@my.org",tolist,msg) |
|
666 { "three@three.org" : ( 550 ,"User unknown" ) } |
|
667 >>> s.quit() |
|
668 |
|
669 In the above example, the message was accepted for delivery to three |
|
670 of the four addresses, and one was rejected, with the error code |
|
671 550. If all addresses are accepted, then the method will return an |
|
672 empty dictionary. |
|
673 |
|
674 """ |
|
675 if self.helo_resp is None and self.ehlo_resp is None: |
|
676 if not (200 <= self.ehlo()[0] <= 299): |
|
677 (code,resp) = self.helo() |
|
678 if not (200 <= code <= 299): |
|
679 raise SMTPHeloError(code, resp) |
|
680 esmtp_opts = [] |
|
681 if self.does_esmtp: |
|
682 # Hmmm? what's this? -ddm |
|
683 # self.esmtp_features['7bit']="" |
|
684 if self.has_extn('size'): |
|
685 esmtp_opts.append("size=%d" % len(msg)) |
|
686 for option in mail_options: |
|
687 esmtp_opts.append(option) |
|
688 |
|
689 (code,resp) = self.mail(from_addr, esmtp_opts) |
|
690 if code != 250: |
|
691 self.rset() |
|
692 raise SMTPSenderRefused(code, resp, from_addr) |
|
693 senderrs={} |
|
694 if isinstance(to_addrs, basestring): |
|
695 to_addrs = [to_addrs] |
|
696 for each in to_addrs: |
|
697 (code,resp)=self.rcpt(each, rcpt_options) |
|
698 if (code != 250) and (code != 251): |
|
699 senderrs[each]=(code,resp) |
|
700 if len(senderrs)==len(to_addrs): |
|
701 # the server refused all our recipients |
|
702 self.rset() |
|
703 raise SMTPRecipientsRefused(senderrs) |
|
704 (code,resp) = self.data(msg) |
|
705 if code != 250: |
|
706 self.rset() |
|
707 raise SMTPDataError(code, resp) |
|
708 #if we got here then somebody got our mail |
|
709 return senderrs |
|
710 |
|
711 |
|
712 def close(self): |
|
713 """Close the connection to the SMTP server.""" |
|
714 if self.file: |
|
715 self.file.close() |
|
716 self.file = None |
|
717 if self.sock: |
|
718 self.sock.close() |
|
719 self.sock = None |
|
720 |
|
721 |
|
722 def quit(self): |
|
723 """Terminate the SMTP session.""" |
|
724 self.docmd("quit") |
|
725 self.close() |
|
726 |
|
727 |
|
728 # Test the sendmail method, which tests most of the others. |
|
729 # Note: This always sends to localhost. |
|
730 if __name__ == '__main__': |
|
731 import sys |
|
732 |
|
733 def prompt(prompt): |
|
734 sys.stdout.write(prompt + ": ") |
|
735 return sys.stdin.readline().strip() |
|
736 |
|
737 fromaddr = prompt("From") |
|
738 toaddrs = prompt("To").split(',') |
|
739 print "Enter message, end with ^D:" |
|
740 msg = '' |
|
741 while 1: |
|
742 line = sys.stdin.readline() |
|
743 if not line: |
|
744 break |
|
745 msg = msg + line |
|
746 print "Message length is %d" % len(msg) |
|
747 |
|
748 server = SMTP('localhost') |
|
749 server.set_debuglevel(1) |
|
750 server.sendmail(fromaddr, toaddrs, msg) |
|
751 server.quit() |