Vega strike Python Modules doc  0.5.1
Documentation of the " Modules " folder of Vega strike
 All Data Structures Namespaces Files Functions Variables
smtplib.py
Go to the documentation of this file.
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 rfc822
47 import types
48 import base64
49 import hmac
50 
51 __all__ = ["SMTPException","SMTPServerDisconnected","SMTPResponseException",
52  "SMTPSenderRefused","SMTPRecipientsRefused","SMTPDataError",
53  "SMTPConnectError","SMTPHeloError","SMTPAuthenticationError",
54  "quoteaddr","quotedata","SMTP"]
55 
56 SMTP_PORT = 25
57 CRLF="\r\n"
58 
59 # Exception classes used by this module.
60 class SMTPException(Exception):
61  """Base class for all exceptions raised by this module."""
62 
64  """Not connected to any SMTP server.
65 
66  This exception is raised when the server unexpectedly disconnects,
67  or when an attempt is made to use the SMTP instance before
68  connecting it to a server.
69  """
70 
72  """Base class for all exceptions that include an SMTP error code.
73 
74  These exceptions are generated in some instances when the SMTP
75  server returns an error code. The error code is stored in the
76  `smtp_code' attribute of the error, and the `smtp_error' attribute
77  is set to the error message.
78  """
79 
80  def __init__(self, code, msg):
81  self.smtp_code = code
82  self.smtp_error = msg
83  self.args = (code, msg)
84 
86  """Sender address refused.
87 
88  In addition to the attributes set by on all SMTPResponseException
89  exceptions, this sets `sender' to the string that the SMTP refused.
90  """
91 
92  def __init__(self, code, msg, sender):
93  self.smtp_code = code
94  self.smtp_error = msg
95  self.sender = sender
96  self.args = (code, msg, sender)
97 
99  """All recipient addresses refused.
100 
101  The errors for each recipient are accessible through the attribute
102  'recipients', which is a dictionary of exactly the same sort as
103  SMTP.sendmail() returns.
104  """
105 
106  def __init__(self, recipients):
107  self.recipients = recipients
108  self.args = ( recipients,)
109 
110 
112  """The SMTP server didn't accept the data."""
113 
114 class SMTPConnectError(SMTPResponseException):
115  """Error during connection establishment."""
116 
118  """The server refused our HELO reply."""
119 
121  """Authentication error.
122 
123  Most probably the server didn't accept the username/password
124  combination provided.
125  """
126 
128  """A fake socket object that really wraps a SSLObject.
129 
130  It only supports what is needed in smtplib.
131  """
132  def __init__(self, realsock, sslobj):
133  self.realsock = realsock
134  self.sslobj = sslobj
135 
136  def send(self, str):
137  self.sslobj.write(str)
138  return len(str)
139 
140  def close(self):
141  self.realsock.close()
142 
144  """A fake file like object that really wraps a SSLObject.
145 
146  It only supports what is needed in smtplib.
147  """
148  def __init__( self, sslobj):
149  self.sslobj = sslobj
150 
151  def readline(self):
152  str = ""
153  chr = None
154  while chr != "\n":
155  chr = self.sslobj.read(1)
156  str += chr
157  return str
158 
159  def close(self):
160  pass
161 
162 def quoteaddr(addr):
163  """Quote a subset of the email addresses defined by RFC 821.
164 
165  Should be able to handle anything rfc822.parseaddr can handle.
166  """
167  m=None
168  try:
169  m=rfc822.parseaddr(addr)[1]
170  except AttributeError:
171  pass
172  if not m:
173  #something weird here.. punt -ddm
174  return addr
175  else:
176  return "<%s>" % m
177 
178 def quotedata(data):
179  """Quote data for email.
180 
181  Double leading '.', and change Unix newline '\\n', or Mac '\\r' into
182  Internet CRLF end-of-line.
183  """
184  return re.sub(r'(?m)^\.', '..',
185  re.sub(r'(?:\r\n|\n|\r(?!\n))', CRLF, data))
186 
187 
188 class SMTP:
189  """This class manages a connection to an SMTP or ESMTP server.
190  SMTP Objects:
191  SMTP objects have the following attributes:
192  helo_resp
193  This is the message given by the server in response to the
194  most recent HELO command.
195 
196  ehlo_resp
197  This is the message given by the server in response to the
198  most recent EHLO command. This is usually multiline.
199 
200  does_esmtp
201  This is a True value _after you do an EHLO command_, if the
202  server supports ESMTP.
203 
204  esmtp_features
205  This is a dictionary, which, if the server supports ESMTP,
206  will _after you do an EHLO command_, contain the names of the
207  SMTP service extensions this server supports, and their
208  parameters (if any).
209 
210  Note, all extension names are mapped to lower case in the
211  dictionary.
212 
213  See each method's docstrings for details. In general, there is a
214  method of the same name to perform each SMTP command. There is also a
215  method called 'sendmail' that will do an entire mail transaction.
216  """
217  debuglevel = 0
218  file = None
219  helo_resp = None
220  ehlo_resp = None
221  does_esmtp = 0
222 
223  def __init__(self, host = '', port = 0):
224  """Initialize a new instance.
225 
226  If specified, `host' is the name of the remote host to which to
227  connect. If specified, `port' specifies the port to which to connect.
228  By default, smtplib.SMTP_PORT is used. An SMTPConnectError is raised
229  if the specified `host' doesn't respond correctly.
230 
231  """
232  self.esmtp_features = {}
233  if host:
234  (code, msg) = self.connect(host, port)
235  if code != 220:
236  raise SMTPConnectError(code, msg)
237 
238  def set_debuglevel(self, debuglevel):
239  """Set the debug output level.
240 
241  A non-false value results in debug messages for connection and for all
242  messages sent to and received from the server.
243 
244  """
245  self.debuglevel = debuglevel
246 
247  def connect(self, host='localhost', port = 0):
248  """Connect to a host on a given port.
249 
250  If the hostname ends with a colon (`:') followed by a number, and
251  there is no port specified, that suffix will be stripped off and the
252  number interpreted as the port number to use.
253 
254  Note: This method is automatically invoked by __init__, if a host is
255  specified during instantiation.
256 
257  """
258  if not port and (host.find(':') == host.rfind(':')):
259  i = host.rfind(':')
260  if i >= 0:
261  host, port = host[:i], host[i+1:]
262  try: port = int(port)
263  except ValueError:
264  raise socket.error, "nonnumeric port"
265  if not port: port = SMTP_PORT
266  if self.debuglevel > 0: print 'connect:', (host, port)
267  msg = "getaddrinfo returns an empty list"
268  self.sock = None
269  for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
270  af, socktype, proto, canonname, sa = res
271  try:
272  self.sock = socket.socket(af, socktype, proto)
273  if self.debuglevel > 0: print 'connect:', (host, port)
274  self.sock.connect(sa)
275  except socket.error, msg:
276  if self.debuglevel > 0: print 'connect fail:', (host, port)
277  if self.sock:
278  self.sock.close()
279  self.sock = None
280  continue
281  break
282  if not self.sock:
283  raise socket.error, msg
284  (code, msg) = self.getreply()
285  if self.debuglevel > 0: print "connect:", msg
286  return (code, msg)
287 
288  def send(self, str):
289  """Send `str' to the server."""
290  if self.debuglevel > 0: print 'send:', `str`
291  if self.sock:
292  try:
293  self.sock.sendall(str)
294  except socket.error:
295  self.close()
296  raise SMTPServerDisconnected('Server not connected')
297  else:
298  raise SMTPServerDisconnected('please run connect() first')
299 
300  def putcmd(self, cmd, args=""):
301  """Send a command to the server."""
302  if args == "":
303  str = '%s%s' % (cmd, CRLF)
304  else:
305  str = '%s %s%s' % (cmd, args, CRLF)
306  self.send(str)
307 
308  def getreply(self):
309  """Get a reply from the server.
310 
311  Returns a tuple consisting of:
312 
313  - server response code (e.g. '250', or such, if all goes well)
314  Note: returns -1 if it can't read response code.
315 
316  - server response string corresponding to response code (multiline
317  responses are converted to a single, multiline string).
318 
319  Raises SMTPServerDisconnected if end-of-file is reached.
320  """
321  resp=[]
322  if self.file is None:
323  self.file = self.sock.makefile('rb')
324  while 1:
325  line = self.file.readline()
326  if line == '':
327  self.close()
328  raise SMTPServerDisconnected("Connection unexpectedly closed")
329  if self.debuglevel > 0: print 'reply:', `line`
330  resp.append(line[4:].strip())
331  code=line[:3]
332  # Check that the error code is syntactically correct.
333  # Don't attempt to read a continuation line if it is broken.
334  try:
335  errcode = int(code)
336  except ValueError:
337  errcode = -1
338  break
339  # Check if multiline response.
340  if line[3:4]!="-":
341  break
342 
343  errmsg = "\n".join(resp)
344  if self.debuglevel > 0:
345  print 'reply: retcode (%s); Msg: %s' % (errcode,errmsg)
346  return errcode, errmsg
347 
348  def docmd(self, cmd, args=""):
349  """Send a command, and return its response code."""
350  self.putcmd(cmd,args)
351  return self.getreply()
352 
353  # std smtp commands
354  def helo(self, name=''):
355  """SMTP 'helo' command.
356  Hostname to send for this command defaults to the FQDN of the local
357  host.
358  """
359  if name:
360  self.putcmd("helo", name)
361  else:
362  self.putcmd("helo", socket.getfqdn())
363  (code,msg)=self.getreply()
364  self.helo_resp=msg
365  return (code,msg)
366 
367  def ehlo(self, name=''):
368  """ SMTP 'ehlo' command.
369  Hostname to send for this command defaults to the FQDN of the local
370  host.
371  """
372  self.esmtp_features = {}
373  if name:
374  self.putcmd("ehlo", name)
375  else:
376  self.putcmd("ehlo", socket.getfqdn())
377  (code,msg)=self.getreply()
378  # According to RFC1869 some (badly written)
379  # MTA's will disconnect on an ehlo. Toss an exception if
380  # that happens -ddm
381  if code == -1 and len(msg) == 0:
382  self.close()
383  raise SMTPServerDisconnected("Server not connected")
384  self.ehlo_resp=msg
385  if code != 250:
386  return (code,msg)
387  self.does_esmtp=1
388  #parse the ehlo response -ddm
389  resp=self.ehlo_resp.split('\n')
390  del resp[0]
391  for each in resp:
392  m=re.match(r'(?P<feature>[A-Za-z0-9][A-Za-z0-9\-]*)',each)
393  if m:
394  feature=m.group("feature").lower()
395  params=m.string[m.end("feature"):].strip()
396  self.esmtp_features[feature]=params
397  return (code,msg)
398 
399  def has_extn(self, opt):
400  """Does the server support a given SMTP service extension?"""
401  return self.esmtp_features.has_key(opt.lower())
402 
403  def help(self, args=''):
404  """SMTP 'help' command.
405  Returns help text from server."""
406  self.putcmd("help", args)
407  return self.getreply()
408 
409  def rset(self):
410  """SMTP 'rset' command -- resets session."""
411  return self.docmd("rset")
412 
413  def noop(self):
414  """SMTP 'noop' command -- doesn't do anything :>"""
415  return self.docmd("noop")
416 
417  def mail(self,sender,options=[]):
418  """SMTP 'mail' command -- begins mail xfer session."""
419  optionlist = ''
420  if options and self.does_esmtp:
421  optionlist = ' ' + ' '.join(options)
422  self.putcmd("mail", "FROM:%s%s" % (quoteaddr(sender) ,optionlist))
423  return self.getreply()
424 
425  def rcpt(self,recip,options=[]):
426  """SMTP 'rcpt' command -- indicates 1 recipient for this mail."""
427  optionlist = ''
428  if options and self.does_esmtp:
429  optionlist = ' ' + ' '.join(options)
430  self.putcmd("rcpt","TO:%s%s" % (quoteaddr(recip),optionlist))
431  return self.getreply()
432 
433  def data(self,msg):
434  """SMTP 'DATA' command -- sends message data to server.
435 
436  Automatically quotes lines beginning with a period per rfc821.
437  Raises SMTPDataError if there is an unexpected reply to the
438  DATA command; the return value from this method is the final
439  response code received when the all data is sent.
440  """
441  self.putcmd("data")
442  (code,repl)=self.getreply()
443  if self.debuglevel >0 : print "data:", (code,repl)
444  if code != 354:
445  raise SMTPDataError(code,repl)
446  else:
447  q = quotedata(msg)
448  if q[-2:] != CRLF:
449  q = q + CRLF
450  q = q + "." + CRLF
451  self.send(q)
452  (code,msg)=self.getreply()
453  if self.debuglevel >0 : print "data:", (code,msg)
454  return (code,msg)
455 
456  def verify(self, address):
457  """SMTP 'verify' command -- checks for address validity."""
458  self.putcmd("vrfy", quoteaddr(address))
459  return self.getreply()
460  # a.k.a.
461  vrfy=verify
462 
463  def expn(self, address):
464  """SMTP 'verify' command -- checks for address validity."""
465  self.putcmd("expn", quoteaddr(address))
466  return self.getreply()
467 
468  # some useful methods
469 
470  def login(self, user, password):
471  """Log in on an SMTP server that requires authentication.
472 
473  The arguments are:
474  - user: The user name to authenticate with.
475  - password: The password for the authentication.
476 
477  If there has been no previous EHLO or HELO command this session, this
478  method tries ESMTP EHLO first.
479 
480  This method will return normally if the authentication was successful.
481 
482  This method may raise the following exceptions:
483 
484  SMTPHeloError The server didn't reply properly to
485  the helo greeting.
486  SMTPAuthenticationError The server didn't accept the username/
487  password combination.
488  SMTPException No suitable authentication method was
489  found.
490  """
491 
492  def encode_cram_md5(challenge, user, password):
493  challenge = base64.decodestring(challenge)
494  response = user + " " + hmac.HMAC(password, challenge).hexdigest()
495  return base64.encodestring(response)[:-1]
496 
497  def encode_plain(user, password):
498  return base64.encodestring("%s\0%s\0%s" %
499  (user, user, password))[:-1]
500 
501  AUTH_PLAIN = "PLAIN"
502  AUTH_CRAM_MD5 = "CRAM-MD5"
503 
504  if self.helo_resp is None and self.ehlo_resp is None:
505  if not (200 <= self.ehlo()[0] <= 299):
506  (code, resp) = self.helo()
507  if not (200 <= code <= 299):
508  raise SMTPHeloError(code, resp)
509 
510  if not self.has_extn("auth"):
511  raise SMTPException("SMTP AUTH extension not supported by server.")
512 
513  # Authentication methods the server supports:
514  authlist = self.esmtp_features["auth"].split()
515 
516  # List of authentication methods we support: from preferred to
517  # less preferred methods. Except for the purpose of testing the weaker
518  # ones, we prefer stronger methods like CRAM-MD5:
519  preferred_auths = [AUTH_CRAM_MD5, AUTH_PLAIN]
520  #preferred_auths = [AUTH_PLAIN, AUTH_CRAM_MD5]
521 
522  # Determine the authentication method we'll use
523  authmethod = None
524  for method in preferred_auths:
525  if method in authlist:
526  authmethod = method
527  break
528  if self.debuglevel > 0: print "AuthMethod:", authmethod
529 
530  if authmethod == AUTH_CRAM_MD5:
531  (code, resp) = self.docmd("AUTH", AUTH_CRAM_MD5)
532  if code == 503:
533  # 503 == 'Error: already authenticated'
534  return (code, resp)
535  (code, resp) = self.docmd(encode_cram_md5(resp, user, password))
536  elif authmethod == AUTH_PLAIN:
537  (code, resp) = self.docmd("AUTH",
538  AUTH_PLAIN + " " + encode_plain(user, password))
539  elif authmethod == None:
540  raise SMTPException("No suitable authentication method found.")
541  if code not in [235, 503]:
542  # 235 == 'Authentication successful'
543  # 503 == 'Error: already authenticated'
544  raise SMTPAuthenticationError(code, resp)
545  return (code, resp)
546 
547  def starttls(self, keyfile = None, certfile = None):
548  """Puts the connection to the SMTP server into TLS mode.
549 
550  If the server supports TLS, this will encrypt the rest of the SMTP
551  session. If you provide the keyfile and certfile parameters,
552  the identity of the SMTP server and client can be checked. This,
553  however, depends on whether the socket module really checks the
554  certificates.
555  """
556  (resp, reply) = self.docmd("STARTTLS")
557  if resp == 220:
558  sslobj = socket.ssl(self.sock, keyfile, certfile)
559  self.sock = SSLFakeSocket(self.sock, sslobj)
560  self.file = SSLFakeFile(sslobj)
561  return (resp, reply)
562 
563  def sendmail(self, from_addr, to_addrs, msg, mail_options=[],
564  rcpt_options=[]):
565  """This command performs an entire mail transaction.
566 
567  The arguments are:
568  - from_addr : The address sending this mail.
569  - to_addrs : A list of addresses to send this mail to. A bare
570  string will be treated as a list with 1 address.
571  - msg : The message to send.
572  - mail_options : List of ESMTP options (such as 8bitmime) for the
573  mail command.
574  - rcpt_options : List of ESMTP options (such as DSN commands) for
575  all the rcpt commands.
576 
577  If there has been no previous EHLO or HELO command this session, this
578  method tries ESMTP EHLO first. If the server does ESMTP, message size
579  and each of the specified options will be passed to it. If EHLO
580  fails, HELO will be tried and ESMTP options suppressed.
581 
582  This method will return normally if the mail is accepted for at least
583  one recipient. It returns a dictionary, with one entry for each
584  recipient that was refused. Each entry contains a tuple of the SMTP
585  error code and the accompanying error message sent by the server.
586 
587  This method may raise the following exceptions:
588 
589  SMTPHeloError The server didn't reply properly to
590  the helo greeting.
591  SMTPRecipientsRefused The server rejected ALL recipients
592  (no mail was sent).
593  SMTPSenderRefused The server didn't accept the from_addr.
594  SMTPDataError The server replied with an unexpected
595  error code (other than a refusal of
596  a recipient).
597 
598  Note: the connection will be open even after an exception is raised.
599 
600  Example:
601 
602  >>> import smtplib
603  >>> s=smtplib.SMTP("localhost")
604  >>> tolist=["one@one.org","two@two.org","three@three.org","four@four.org"]
605  >>> msg = '''
606  ... From: Me@my.org
607  ... Subject: testin'...
608  ...
609  ... This is a test '''
610  >>> s.sendmail("me@my.org",tolist,msg)
611  { "three@three.org" : ( 550 ,"User unknown" ) }
612  >>> s.quit()
613 
614  In the above example, the message was accepted for delivery to three
615  of the four addresses, and one was rejected, with the error code
616  550. If all addresses are accepted, then the method will return an
617  empty dictionary.
618 
619  """
620  if self.helo_resp is None and self.ehlo_resp is None:
621  if not (200 <= self.ehlo()[0] <= 299):
622  (code,resp) = self.helo()
623  if not (200 <= code <= 299):
624  raise SMTPHeloError(code, resp)
625  esmtp_opts = []
626  if self.does_esmtp:
627  # Hmmm? what's this? -ddm
628  # self.esmtp_features['7bit']=""
629  if self.has_extn('size'):
630  esmtp_opts.append("size=" + `len(msg)`)
631  for option in mail_options:
632  esmtp_opts.append(option)
633 
634  (code,resp) = self.mail(from_addr, esmtp_opts)
635  if code != 250:
636  self.rset()
637  raise SMTPSenderRefused(code, resp, from_addr)
638  senderrs={}
639  if isinstance(to_addrs, types.StringTypes):
640  to_addrs = [to_addrs]
641  for each in to_addrs:
642  (code,resp)=self.rcpt(each, rcpt_options)
643  if (code != 250) and (code != 251):
644  senderrs[each]=(code,resp)
645  if len(senderrs)==len(to_addrs):
646  # the server refused all our recipients
647  self.rset()
648  raise SMTPRecipientsRefused(senderrs)
649  (code,resp) = self.data(msg)
650  if code != 250:
651  self.rset()
652  raise SMTPDataError(code, resp)
653  #if we got here then somebody got our mail
654  return senderrs
655 
656 
657  def close(self):
658  """Close the connection to the SMTP server."""
659  if self.file:
660  self.file.close()
661  self.file = None
662  if self.sock:
663  self.sock.close()
664  self.sock = None
665 
666 
667  def quit(self):
668  """Terminate the SMTP session."""
669  self.docmd("quit")
670  self.close()
671 
672 
673 # Test the sendmail method, which tests most of the others.
674 # Note: This always sends to localhost.
675 if __name__ == '__main__':
676  import sys
677 
678  def prompt(prompt):
679  sys.stdout.write(prompt + ": ")
680  return sys.stdin.readline().strip()
681 
682  fromaddr = prompt("From")
683  toaddrs = prompt("To").split(',')
684  print "Enter message, end with ^D:"
685  msg = ''
686  while 1:
687  line = sys.stdin.readline()
688  if not line:
689  break
690  msg = msg + line
691  print "Message length is " + `len(msg)`
692 
693  server = SMTP('localhost')
694  server.set_debuglevel(1)
695  server.sendmail(fromaddr, toaddrs, msg)
696  server.quit()