source: python/vendor/Python-2.6.5/Lib/smtpd.py

Last change on this file was 2, checked in by Yuri Dario, 15 years ago

Initial import for vendor code.

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