blob: 8651ba4167d1c371819e5da5a7f4517d52359b53 [file] [log] [blame]
Tor Norbye3a2425a2013-11-04 10:16:08 -08001#! /usr/bin/env python
2
3'''SMTP/ESMTP client class.
4
5This should follow RFC 821 (SMTP), RFC 1869 (ESMTP), RFC 2554 (SMTP
6Authentication) and RFC 2487 (Secure SMTP over TLS).
7
8Notes:
9
10Please remember, when doing ESMTP, that the names of the SMTP service
11extensions are NOT the same thing as the option keywords for the RCPT
12and MAIL commands!
13
14Example:
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
44import socket
45import re
46import email.Utils
47import base64
48import hmac
49from email.base64MIME import encode as encode_base64
50from sys import stderr
51
52__all__ = ["SMTPException","SMTPServerDisconnected","SMTPResponseException",
53 "SMTPSenderRefused","SMTPRecipientsRefused","SMTPDataError",
54 "SMTPConnectError","SMTPHeloError","SMTPAuthenticationError",
55 "quoteaddr","quotedata","SMTP"]
56
57SMTP_PORT = 25
58CRLF="\r\n"
59
60OLDSTYLE_AUTH = re.compile(r"auth=(.*)", re.I)
61
62# Exception classes used by this module.
63class SMTPException(Exception):
64 """Base class for all exceptions raised by this module."""
65
66class 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
74class 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
88class 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
101class 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
114class SMTPDataError(SMTPResponseException):
115 """The SMTP server didn't accept the data."""
116
117class SMTPConnectError(SMTPResponseException):
118 """Error during connection establishment."""
119
120class SMTPHeloError(SMTPResponseException):
121 """The server refused our HELO reply."""
122
123class SMTPAuthenticationError(SMTPResponseException):
124 """Authentication error.
125
126 Most probably the server didn't accept the username/password
127 combination provided.
128 """
129
130class 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
148class 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
167def 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
186def 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
196class 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 hasattr(self, 'sock') and 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 'expn' command -- expands a mailing list."""
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.
730if __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()