Vega strike Python Modules doc  0.5.1
Documentation of the " Modules " folder of Vega strike
 All Data Structures Namespaces Files Functions Variables
smtpd.py
Go to the documentation of this file.
1 #! /usr/bin/env python
2 """An RFC 2821 smtp proxy.
3 
4 Usage: %(program)s [options] [localhost:localport [remotehost:remoteport]]
5 
6 Options:
7 
8  --nosetuid
9  -n
10  This program generally tries to setuid `nobody', unless this flag is
11  set. The setuid call will fail if this program is not run as root (in
12  which case, use this flag).
13 
14  --version
15  -V
16  Print the version number and exit.
17 
18  --class classname
19  -c classname
20  Use `classname' as the concrete SMTP proxy class. Uses `SMTPProxy' by
21  default.
22 
23  --debug
24  -d
25  Turn on debugging prints.
26 
27  --help
28  -h
29  Print this message and exit.
30 
31 Version: %(__version__)s
32 
33 If localhost is not given then `localhost' is used, and if localport is not
34 given then 8025 is used. If remotehost is not given then `localhost' is used,
35 and if remoteport is not given, then 25 is used.
36 """
37 
38 
39 # Overview:
40 #
41 # This file implements the minimal SMTP protocol as defined in RFC 821. It
42 # has a hierarchy of classes which implement the backend functionality for the
43 # smtpd. A number of classes are provided:
44 #
45 # SMTPServer - the base class for the backend. Raises NotImplementedError
46 # if you try to use it.
47 #
48 # DebuggingServer - simply prints each message it receives on stdout.
49 #
50 # PureProxy - Proxies all messages to a real smtpd which does final
51 # delivery. One known problem with this class is that it doesn't handle
52 # SMTP errors from the backend server at all. This should be fixed
53 # (contributions are welcome!).
54 #
55 # MailmanProxy - An experimental hack to work with GNU Mailman
56 # <www.list.org>. Using this server as your real incoming smtpd, your
57 # mailhost will automatically recognize and accept mail destined to Mailman
58 # lists when those lists are created. Every message not destined for a list
59 # gets forwarded to a real backend smtpd, as with PureProxy. Again, errors
60 # are not handled correctly yet.
61 #
62 # Please note that this script requires Python 2.0
63 #
64 # Author: Barry Warsaw <barry@digicool.com>
65 #
66 # TODO:
67 #
68 # - support mailbox delivery
69 # - alias files
70 # - ESMTP
71 # - handle error codes from the backend smtpd
72 
73 import sys
74 import os
75 import errno
76 import getopt
77 import time
78 import socket
79 import asyncore
80 import asynchat
81 
82 __all__ = ["SMTPServer","DebuggingServer","PureProxy","MailmanProxy"]
83 
84 program = sys.argv[0]
85 __version__ = 'Python SMTP proxy version 0.2'
86 
87 
88 class Devnull:
89  def write(self, msg): pass
90  def flush(self): pass
91 
92 
93 DEBUGSTREAM = Devnull()
94 NEWLINE = '\n'
95 EMPTYSTRING = ''
96 COMMASPACE = ', '
97 
98 
99 
100 def usage(code, msg=''):
101  print >> sys.stderr, __doc__ % globals()
102  if msg:
103  print >> sys.stderr, msg
104  sys.exit(code)
105 
106 
107 
109  COMMAND = 0
110  DATA = 1
111 
112  def __init__(self, server, conn, addr):
113  asynchat.async_chat.__init__(self, conn)
114  self.__server = server
115  self.__conn = conn
116  self.__addr = addr
117  self.__line = []
118  self.__state = self.COMMAND
119  self.__greeting = 0
120  self.__mailfrom = None
121  self.__rcpttos = []
122  self.__data = ''
123  self.__fqdn = socket.getfqdn()
124  self.__peer = conn.getpeername()
125  print >> DEBUGSTREAM, 'Peer:', repr(self.__peer)
126  self.push('220 %s %s' % (self.__fqdn, __version__))
127  self.set_terminator('\r\n')
128 
129  # Overrides base class for convenience
130  def push(self, msg):
131  asynchat.async_chat.push(self, msg + '\r\n')
132 
133  # Implementation of base class abstract method
134  def collect_incoming_data(self, data):
135  self.__line.append(data)
136 
137  # Implementation of base class abstract method
138  def found_terminator(self):
139  line = EMPTYSTRING.join(self.__line)
140  print >> DEBUGSTREAM, 'Data:', repr(line)
141  self.__line = []
142  if self.__state == self.COMMAND:
143  if not line:
144  self.push('500 Error: bad syntax')
145  return
146  method = None
147  i = line.find(' ')
148  if i < 0:
149  command = line.upper()
150  arg = None
151  else:
152  command = line[:i].upper()
153  arg = line[i+1:].strip()
154  method = getattr(self, 'smtp_' + command, None)
155  if not method:
156  self.push('502 Error: command "%s" not implemented' % command)
157  return
158  method(arg)
159  return
160  else:
161  if self.__state != self.DATA:
162  self.push('451 Internal confusion')
163  return
164  # Remove extraneous carriage returns and de-transparency according
165  # to RFC 821, Section 4.5.2.
166  data = []
167  for text in line.split('\r\n'):
168  if text and text[0] == '.':
169  data.append(text[1:])
170  else:
171  data.append(text)
172  self.__data = NEWLINE.join(data)
173  status = self.__server.process_message(self.__peer,
174  self.__mailfrom,
175  self.__rcpttos,
176  self.__data)
177  self.__rcpttos = []
178  self.__mailfrom = None
179  self.__state = self.COMMAND
180  self.set_terminator('\r\n')
181  if not status:
182  self.push('250 Ok')
183  else:
184  self.push(status)
185 
186  # SMTP and ESMTP commands
187  def smtp_HELO(self, arg):
188  if not arg:
189  self.push('501 Syntax: HELO hostname')
190  return
191  if self.__greeting:
192  self.push('503 Duplicate HELO/EHLO')
193  else:
194  self.__greeting = arg
195  self.push('250 %s' % self.__fqdn)
196 
197  def smtp_NOOP(self, arg):
198  if arg:
199  self.push('501 Syntax: NOOP')
200  else:
201  self.push('250 Ok')
202 
203  def smtp_QUIT(self, arg):
204  # args is ignored
205  self.push('221 Bye')
206  self.close_when_done()
207 
208  # factored
209  def __getaddr(self, keyword, arg):
210  address = None
211  keylen = len(keyword)
212  if arg[:keylen].upper() == keyword:
213  address = arg[keylen:].strip()
214  if not address:
215  pass
216  elif address[0] == '<' and address[-1] == '>' and address != '<>':
217  # Addresses can be in the form <person@dom.com> but watch out
218  # for null address, e.g. <>
219  address = address[1:-1]
220  return address
221 
222  def smtp_MAIL(self, arg):
223  print >> DEBUGSTREAM, '===> MAIL', arg
224  address = self.__getaddr('FROM:', arg)
225  if not address:
226  self.push('501 Syntax: MAIL FROM:<address>')
227  return
228  if self.__mailfrom:
229  self.push('503 Error: nested MAIL command')
230  return
231  self.__mailfrom = address
232  print >> DEBUGSTREAM, 'sender:', self.__mailfrom
233  self.push('250 Ok')
234 
235  def smtp_RCPT(self, arg):
236  print >> DEBUGSTREAM, '===> RCPT', arg
237  if not self.__mailfrom:
238  self.push('503 Error: need MAIL command')
239  return
240  address = self.__getaddr('TO:', arg)
241  if not address:
242  self.push('501 Syntax: RCPT TO: <address>')
243  return
244  if address.lower().startswith('stimpy'):
245  self.push('503 You suck %s' % address)
246  return
247  self.__rcpttos.append(address)
248  print >> DEBUGSTREAM, 'recips:', self.__rcpttos
249  self.push('250 Ok')
250 
251  def smtp_RSET(self, arg):
252  if arg:
253  self.push('501 Syntax: RSET')
254  return
255  # Resets the sender, recipients, and data, but not the greeting
256  self.__mailfrom = None
257  self.__rcpttos = []
258  self.__data = ''
259  self.__state = self.COMMAND
260  self.push('250 Ok')
261 
262  def smtp_DATA(self, arg):
263  if not self.__rcpttos:
264  self.push('503 Error: need RCPT command')
265  return
266  if arg:
267  self.push('501 Syntax: DATA')
268  return
269  self.__state = self.DATA
270  self.set_terminator('\r\n.\r\n')
271  self.push('354 End data with <CR><LF>.<CR><LF>')
272 
273 
274 
276  def __init__(self, localaddr, remoteaddr):
277  self._localaddr = localaddr
278  self._remoteaddr = remoteaddr
280  self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
281  # try to re-use a server port if possible
282  self.set_reuse_addr()
283  self.bind(localaddr)
284  self.listen(5)
285  print >> DEBUGSTREAM, \
286  '%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
287  self.__class__.__name__, time.ctime(time.time()),
288  localaddr, remoteaddr)
289 
290  def handle_accept(self):
291  conn, addr = self.accept()
292  print >> DEBUGSTREAM, 'Incoming connection from %s' % repr(addr)
293  channel = SMTPChannel(self, conn, addr)
294 
295  # API for "doing something useful with the message"
296  def process_message(self, peer, mailfrom, rcpttos, data):
297  """Override this abstract method to handle messages from the client.
298 
299  peer is a tuple containing (ipaddr, port) of the client that made the
300  socket connection to our smtp port.
301 
302  mailfrom is the raw address the client claims the message is coming
303  from.
304 
305  rcpttos is a list of raw addresses the client wishes to deliver the
306  message to.
307 
308  data is a string containing the entire full text of the message,
309  headers (if supplied) and all. It has been `de-transparencied'
310  according to RFC 821, Section 4.5.2. In other words, a line
311  containing a `.' followed by other text has had the leading dot
312  removed.
313 
314  This function should return None, for a normal `250 Ok' response;
315  otherwise it returns the desired response string in RFC 821 format.
316 
317  """
318  raise NotImplementedError
319 
320 
321 
323  # Do something with the gathered message
324  def process_message(self, peer, mailfrom, rcpttos, data):
325  inheaders = 1
326  lines = data.split('\n')
327  print '---------- MESSAGE FOLLOWS ----------'
328  for line in lines:
329  # headers first
330  if inheaders and not line:
331  print 'X-Peer:', peer[0]
332  inheaders = 0
333  print line
334  print '------------ END MESSAGE ------------'
335 
336 
337 
339  def process_message(self, peer, mailfrom, rcpttos, data):
340  lines = data.split('\n')
341  # Look for the last header
342  i = 0
343  for line in lines:
344  if not line:
345  break
346  i += 1
347  lines.insert(i, 'X-Peer: %s' % peer[0])
348  data = NEWLINE.join(lines)
349  refused = self._deliver(mailfrom, rcpttos, data)
350  # TBD: what to do with refused addresses?
351  print >> DEBUGSTREAM, 'we got some refusals'
352 
353  def _deliver(self, mailfrom, rcpttos, data):
354  import smtplib
355  refused = {}
356  try:
357  s = smtplib.SMTP()
358  s.connect(self._remoteaddr[0], self._remoteaddr[1])
359  try:
360  refused = s.sendmail(mailfrom, rcpttos, data)
361  finally:
362  s.quit()
364  print >> DEBUGSTREAM, 'got SMTPRecipientsRefused'
365  refused = e.recipients
366  except (socket.error, smtplib.SMTPException), e:
367  print >> DEBUGSTREAM, 'got', e.__class__
368  # All recipients were refused. If the exception had an associated
369  # error code, use it. Otherwise,fake it with a non-triggering
370  # exception code.
371  errcode = getattr(e, 'smtp_code', -1)
372  errmsg = getattr(e, 'smtp_error', 'ignore')
373  for r in rcpttos:
374  refused[r] = (errcode, errmsg)
375  return refused
376 
377 
378 
380  def process_message(self, peer, mailfrom, rcpttos, data):
381  from cStringIO import StringIO
382  from Mailman import Utils
383  from Mailman import Message
384  from Mailman import MailList
385  # If the message is to a Mailman mailing list, then we'll invoke the
386  # Mailman script directly, without going through the real smtpd.
387  # Otherwise we'll forward it to the local proxy for disposition.
388  listnames = []
389  for rcpt in rcpttos:
390  local = rcpt.lower().split('@')[0]
391  # We allow the following variations on the theme
392  # listname
393  # listname-admin
394  # listname-owner
395  # listname-request
396  # listname-join
397  # listname-leave
398  parts = local.split('-')
399  if len(parts) > 2:
400  continue
401  listname = parts[0]
402  if len(parts) == 2:
403  command = parts[1]
404  else:
405  command = ''
406  if not Utils.list_exists(listname) or command not in (
407  '', 'admin', 'owner', 'request', 'join', 'leave'):
408  continue
409  listnames.append((rcpt, listname, command))
410  # Remove all list recipients from rcpttos and forward what we're not
411  # going to take care of ourselves. Linear removal should be fine
412  # since we don't expect a large number of recipients.
413  for rcpt, listname, command in listnames:
414  rcpttos.remove(rcpt)
415  # If there's any non-list destined recipients left,
416  print >> DEBUGSTREAM, 'forwarding recips:', ' '.join(rcpttos)
417  if rcpttos:
418  refused = self._deliver(mailfrom, rcpttos, data)
419  # TBD: what to do with refused addresses?
420  print >> DEBUGSTREAM, 'we got refusals'
421  # Now deliver directly to the list commands
422  mlists = {}
423  s = StringIO(data)
424  msg = Message.Message(s)
425  # These headers are required for the proper execution of Mailman. All
426  # MTAs in existance seem to add these if the original message doesn't
427  # have them.
428  if not msg.getheader('from'):
429  msg['From'] = mailfrom
430  if not msg.getheader('date'):
431  msg['Date'] = time.ctime(time.time())
432  for rcpt, listname, command in listnames:
433  print >> DEBUGSTREAM, 'sending message to', rcpt
434  mlist = mlists.get(listname)
435  if not mlist:
436  mlist = MailList.MailList(listname, lock=0)
437  mlists[listname] = mlist
438  # dispatch on the type of command
439  if command == '':
440  # post
441  msg.Enqueue(mlist, tolist=1)
442  elif command == 'admin':
443  msg.Enqueue(mlist, toadmin=1)
444  elif command == 'owner':
445  msg.Enqueue(mlist, toowner=1)
446  elif command == 'request':
447  msg.Enqueue(mlist, torequest=1)
448  elif command in ('join', 'leave'):
449  # TBD: this is a hack!
450  if command == 'join':
451  msg['Subject'] = 'subscribe'
452  else:
453  msg['Subject'] = 'unsubscribe'
454  msg.Enqueue(mlist, torequest=1)
455 
456 
457 
458 class Options:
459  setuid = 1
460  classname = 'PureProxy'
461 
462 
463 
464 def parseargs():
465  global DEBUGSTREAM
466  try:
467  opts, args = getopt.getopt(
468  sys.argv[1:], 'nVhc:d',
469  ['class=', 'nosetuid', 'version', 'help', 'debug'])
470  except getopt.error, e:
471  usage(1, e)
472 
473  options = Options()
474  for opt, arg in opts:
475  if opt in ('-h', '--help'):
476  usage(0)
477  elif opt in ('-V', '--version'):
478  print >> sys.stderr, __version__
479  sys.exit(0)
480  elif opt in ('-n', '--nosetuid'):
481  options.setuid = 0
482  elif opt in ('-c', '--class'):
483  options.classname = arg
484  elif opt in ('-d', '--debug'):
485  DEBUGSTREAM = sys.stderr
486 
487  # parse the rest of the arguments
488  if len(args) < 1:
489  localspec = 'localhost:8025'
490  remotespec = 'localhost:25'
491  elif len(args) < 2:
492  localspec = args[0]
493  remotespec = 'localhost:25'
494  elif len(args) < 3:
495  localspec = args[0]
496  remotespec = args[1]
497  else:
498  usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
499 
500  # split into host/port pairs
501  i = localspec.find(':')
502  if i < 0:
503  usage(1, 'Bad local spec: %s' % localspec)
504  options.localhost = localspec[:i]
505  try:
506  options.localport = int(localspec[i+1:])
507  except ValueError:
508  usage(1, 'Bad local port: %s' % localspec)
509  i = remotespec.find(':')
510  if i < 0:
511  usage(1, 'Bad remote spec: %s' % remotespec)
512  options.remotehost = remotespec[:i]
513  try:
514  options.remoteport = int(remotespec[i+1:])
515  except ValueError:
516  usage(1, 'Bad remote port: %s' % remotespec)
517  return options
518 
519 
520 
521 if __name__ == '__main__':
522  options = parseargs()
523  # Become nobody
524  if options.setuid:
525  try:
526  import pwd
527  except ImportError:
528  print >> sys.stderr, \
529  'Cannot import module "pwd"; try running with -n option.'
530  sys.exit(1)
531  nobody = pwd.getpwnam('nobody')[2]
532  try:
533  os.setuid(nobody)
534  except OSError, e:
535  if e.errno != errno.EPERM: raise
536  print >> sys.stderr, \
537  'Cannot setuid "nobody"; try running with -n option.'
538  sys.exit(1)
539  import __main__
540  class_ = getattr(__main__, options.classname)
541  proxy = class_((options.localhost, options.localport),
542  (options.remotehost, options.remoteport))
543  try:
544  asyncore.loop()
545  except KeyboardInterrupt:
546  pass