2 """An RFC 821 smtp proxy.
4 Usage: %(program)s [options] localhost:port remotehost:port
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).
16 Print the version number and exit.
20 Use `classname' as the concrete SMTP proxy class. Uses `SMTPProxy' by
25 Turn on debugging prints.
29 Print this message and exit.
31 Version: %(__version__)s
37 # This file implements the minimal SMTP protocol as defined in RFC 821. It
38 # has a hierarchy of classes which implement the backend functionality for the
39 # smtpd. A number of classes are provided:
41 # SMTPServer - the base class for the backend. Raises NotImplementedError
42 # if you try to use it.
44 # DebuggingServer - simply prints each message it receives on stdout.
46 # PureProxy - Proxies all messages to a real smtpd which does final
47 # delivery. One known problem with this class is that it doesn't handle
48 # SMTP errors from the backend server at all. This should be fixed
49 # (contributions are welcome!).
51 # MailmanProxy - An experimental hack to work with GNU Mailman
52 # <www.list.org>. Using this server as your real incoming smtpd, your
53 # mailhost will automatically recognize and accept mail destined to Mailman
54 # lists when those lists are created. Every message not destined for a list
55 # gets forwarded to a real backend smtpd, as with PureProxy. Again, errors
56 # are not handled correctly yet.
58 # Please note that this script requires Python 2.0
60 # Author: Barry Warsaw <barry@digicool.com>
64 # - support mailbox delivery
67 # - handle error codes from the backend smtpd
78 __all__
= ["SMTPServer","DebuggingServer","PureProxy","MailmanProxy"]
81 __version__
= 'Python SMTP proxy version 0.2'
85 def write(self
, msg
): pass
89 DEBUGSTREAM
= Devnull()
95 def usage(code
, msg
=''):
96 print >> sys
.stderr
, __doc__
% globals()
98 print >> sys
.stderr
, msg
103 class SMTPChannel(asynchat
.async_chat
):
107 def __init__(self
, server
, conn
, addr
):
108 asynchat
.async_chat
.__init
__(self
, conn
)
109 self
.__server
= server
113 self
.__state
= self
.COMMAND
115 self
.__mailfrom
= None
118 self
.__fqdn
= socket
.gethostbyaddr(
119 socket
.gethostbyname(socket
.gethostname()))[0]
120 self
.__peer
= conn
.getpeername()
121 print >> DEBUGSTREAM
, 'Peer:', repr(self
.__peer
)
122 self
.push('220 %s %s' % (self
.__fqdn
, __version__
))
123 self
.set_terminator('\r\n')
125 # Overrides base class for convenience
127 asynchat
.async_chat
.push(self
, msg
+ '\r\n')
129 # Implementation of base class abstract method
130 def collect_incoming_data(self
, data
):
131 self
.__line
.append(data
)
133 # Implementation of base class abstract method
134 def found_terminator(self
):
135 line
= EMPTYSTRING
.join(self
.__line
)
137 if self
.__state
== self
.COMMAND
:
139 self
.push('500 Error: bad syntax')
144 command
= line
.upper()
147 command
= line
[:i
].upper()
148 arg
= line
[i
+1:].strip()
149 method
= getattr(self
, 'smtp_' + command
, None)
151 self
.push('502 Error: command "%s" not implemented' % command
)
156 if self
.__state
!= self
.DATA
:
157 self
.push('451 Internal confusion')
159 # Remove extraneous carriage returns and de-transparency according
160 # to RFC 821, Section 4.5.2.
162 for text
in line
.split('\r\n'):
163 if text
and text
[0] == '.':
164 data
.append(text
[1:])
167 self
.__data
= NEWLINE
.join(data
)
168 status
= self
.__server
.process_message(self
.__peer
,
173 self
.__mailfrom
= None
174 self
.__state
= self
.COMMAND
175 self
.set_terminator('\r\n')
181 # SMTP and ESMTP commands
182 def smtp_HELO(self
, arg
):
184 self
.push('501 Syntax: HELO hostname')
187 self
.push('503 Duplicate HELO/EHLO')
189 self
.__greeting
= arg
190 self
.push('250 %s' % self
.__fqdn
)
192 def smtp_NOOP(self
, arg
):
194 self
.push('501 Syntax: NOOP')
198 def smtp_QUIT(self
, arg
):
201 self
.close_when_done()
204 def __getaddr(self
, keyword
, arg
):
206 keylen
= len(keyword
)
207 if arg
[:keylen
].upper() == keyword
:
208 address
= arg
[keylen
:].strip()
211 elif address
[0] == '<' and address
[-1] == '>' and address
!= '<>':
212 # Addresses can be in the form <person@dom.com> but watch out
213 # for null address, e.g. <>
214 address
= address
[1:-1]
217 def smtp_MAIL(self
, arg
):
218 print >> DEBUGSTREAM
, '===> MAIL', arg
219 address
= self
.__getaddr
('FROM:', arg
)
221 self
.push('501 Syntax: MAIL FROM:<address>')
224 self
.push('503 Error: nested MAIL command')
226 self
.__mailfrom
= address
227 print >> DEBUGSTREAM
, 'sender:', self
.__mailfrom
230 def smtp_RCPT(self
, arg
):
231 print >> DEBUGSTREAM
, '===> RCPT', arg
232 if not self
.__mailfrom
:
233 self
.push('503 Error: need MAIL command')
235 address
= self
.__getaddr
('TO:', arg
)
237 self
.push('501 Syntax: RCPT TO: <address>')
239 if address
.lower().startswith('stimpy'):
240 self
.push('503 You suck %s' % address
)
242 self
.__rcpttos
.append(address
)
243 print >> DEBUGSTREAM
, 'recips:', self
.__rcpttos
246 def smtp_RSET(self
, arg
):
248 self
.push('501 Syntax: RSET')
250 # Resets the sender, recipients, and data, but not the greeting
251 self
.__mailfrom
= None
254 self
.__state
= self
.COMMAND
257 def smtp_DATA(self
, arg
):
258 if not self
.__rcpttos
:
259 self
.push('503 Error: need RCPT command')
262 self
.push('501 Syntax: DATA')
264 self
.__state
= self
.DATA
265 self
.set_terminator('\r\n.\r\n')
266 self
.push('354 End data with <CR><LF>.<CR><LF>')
270 class SMTPServer(asyncore
.dispatcher
):
271 def __init__(self
, localaddr
, remoteaddr
):
272 self
._localaddr
= localaddr
273 self
._remoteaddr
= remoteaddr
274 asyncore
.dispatcher
.__init
__(self
)
275 self
.create_socket(socket
.AF_INET
, socket
.SOCK_STREAM
)
276 # try to re-use a server port if possible
277 self
.socket
.setsockopt(
278 socket
.SOL_SOCKET
, socket
.SO_REUSEADDR
,
279 self
.socket
.getsockopt(socket
.SOL_SOCKET
, socket
.SO_REUSEADDR
) |
1)
282 print '%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
283 self
.__class
__.__name
__, time
.ctime(time
.time()),
284 localaddr
, remoteaddr
)
286 def handle_accept(self
):
287 conn
, addr
= self
.accept()
288 print >> DEBUGSTREAM
, 'Incoming connection from %s' % repr(addr
)
289 channel
= SMTPChannel(self
, conn
, addr
)
291 # API for "doing something useful with the message"
292 def process_message(self
, peer
, mailfrom
, rcpttos
, data
):
293 """Override this abstract method to handle messages from the client.
295 peer is a tuple containing (ipaddr, port) of the client that made the
296 socket connection to our smtp port.
298 mailfrom is the raw address the client claims the message is coming
301 rcpttos is a list of raw addresses the client wishes to deliver the
304 data is a string containing the entire full text of the message,
305 headers (if supplied) and all. It has been `de-transparencied'
306 according to RFC 821, Section 4.5.2. In other words, a line
307 containing a `.' followed by other text has had the leading dot
310 This function should return None, for a normal `250 Ok' response;
311 otherwise it returns the desired response string in RFC 821 format.
314 raise NotImplementedError
317 class DebuggingServer(SMTPServer
):
318 # Do something with the gathered message
319 def process_message(self
, peer
, mailfrom
, rcpttos
, data
):
321 lines
= data
.split('\n')
322 print '---------- MESSAGE FOLLOWS ----------'
325 if inheaders
and not line
:
326 print 'X-Peer:', peer
[0]
329 print '------------ END MESSAGE ------------'
333 class PureProxy(SMTPServer
):
334 def process_message(self
, peer
, mailfrom
, rcpttos
, data
):
335 lines
= data
.split('\n')
336 # Look for the last header
342 lines
.insert(i
, 'X-Peer: %s' % peer
[0])
343 data
= NEWLINE
.join(lines
)
344 refused
= self
._deliver
(mailfrom
, rcpttos
, data
)
345 # TBD: what to do with refused addresses?
346 print >> DEBUGSTREAM
, 'we got some refusals'
348 def _deliver(self
, mailfrom
, rcpttos
, data
):
353 s
.connect(self
._remoteaddr
[0], self
._remoteaddr
[1])
355 refused
= s
.sendmail(mailfrom
, rcpttos
, data
)
358 except smtplib
.SMTPRecipientsRefused
, e
:
359 print >> DEBUGSTREAM
, 'got SMTPRecipientsRefused'
360 refused
= e
.recipients
361 except (socket
.error
, smtplib
.SMTPException
), e
:
362 print >> DEBUGSTREAM
, 'got', e
.__class
__
363 # All recipients were refused. If the exception had an associated
364 # error code, use it. Otherwise,fake it with a non-triggering
366 errcode
= getattr(e
, 'smtp_code', -1)
367 errmsg
= getattr(e
, 'smtp_error', 'ignore')
369 refused
[r
] = (errcode
, errmsg
)
374 class MailmanProxy(PureProxy
):
375 def process_message(self
, peer
, mailfrom
, rcpttos
, data
):
376 from cStringIO
import StringIO
378 from Mailman
import Utils
379 from Mailman
import Message
380 from Mailman
import MailList
381 # If the message is to a Mailman mailing list, then we'll invoke the
382 # Mailman script directly, without going through the real smtpd.
383 # Otherwise we'll forward it to the local proxy for disposition.
386 local
= rcpt
.lower().split('@')[0]
387 # We allow the following variations on the theme
394 parts
= local
.split('-')
402 if not Utils
.list_exists(listname
) or command
not in (
403 '', 'admin', 'owner', 'request', 'join', 'leave'):
405 listnames
.append((rcpt
, listname
, command
))
406 # Remove all list recipients from rcpttos and forward what we're not
407 # going to take care of ourselves. Linear removal should be fine
408 # since we don't expect a large number of recipients.
409 for rcpt
, listname
, command
in listnames
:
411 # If there's any non-list destined recipients left,
412 print >> DEBUGSTREAM
, 'forwarding recips:', ' '.join(rcpttos
)
414 refused
= self
._deliver
(mailfrom
, rcpttos
, data
)
415 # TBD: what to do with refused addresses?
416 print >> DEBUGSTREAM
, 'we got refusals'
417 # Now deliver directly to the list commands
420 msg
= Message
.Message(s
)
421 # These headers are required for the proper execution of Mailman. All
422 # MTAs in existance seem to add these if the original message doesn't
424 if not msg
.getheader('from'):
425 msg
['From'] = mailfrom
426 if not msg
.getheader('date'):
427 msg
['Date'] = time
.ctime(time
.time())
428 for rcpt
, listname
, command
in listnames
:
429 print >> DEBUGSTREAM
, 'sending message to', rcpt
430 mlist
= mlists
.get(listname
)
432 mlist
= MailList
.MailList(listname
, lock
=0)
433 mlists
[listname
] = mlist
434 # dispatch on the type of command
437 msg
.Enqueue(mlist
, tolist
=1)
438 elif command
== 'admin':
439 msg
.Enqueue(mlist
, toadmin
=1)
440 elif command
== 'owner':
441 msg
.Enqueue(mlist
, toowner
=1)
442 elif command
== 'request':
443 msg
.Enqueue(mlist
, torequest
=1)
444 elif command
in ('join', 'leave'):
445 # TBD: this is a hack!
446 if command
== 'join':
447 msg
['Subject'] = 'subscribe'
449 msg
['Subject'] = 'unsubscribe'
450 msg
.Enqueue(mlist
, torequest
=1)
456 classname
= 'PureProxy'
462 opts
, args
= getopt
.getopt(
463 sys
.argv
[1:], 'nVhc:d',
464 ['class=', 'nosetuid', 'version', 'help', 'debug'])
465 except getopt
.error
, e
:
469 for opt
, arg
in opts
:
470 if opt
in ('-h', '--help'):
472 elif opt
in ('-V', '--version'):
473 print >> sys
.stderr
, __version__
475 elif opt
in ('-n', '--nosetuid'):
477 elif opt
in ('-c', '--class'):
478 options
.classname
= arg
479 elif opt
in ('-d', '--debug'):
480 DEBUGSTREAM
= sys
.stderr
482 # parse the rest of the arguments
487 usage(1, 'Not enough arguments')
488 # split into host/port pairs
489 i
= localspec
.find(':')
491 usage(1, 'Bad local spec: "%s"' % localspec
)
492 options
.localhost
= localspec
[:i
]
494 options
.localport
= int(localspec
[i
+1:])
496 usage(1, 'Bad local port: "%s"' % localspec
)
497 i
= remotespec
.find(':')
499 usage(1, 'Bad remote spec: "%s"' % remotespec
)
500 options
.remotehost
= remotespec
[:i
]
502 options
.remoteport
= int(remotespec
[i
+1:])
504 usage(1, 'Bad remote port: "%s"' % remotespec
)
509 if __name__
== '__main__':
510 options
= parseargs()
516 print >> sys
.stderr
, \
517 'Cannot import module "pwd"; try running with -n option.'
519 nobody
= pwd
.getpwnam('nobody')[2]
523 if e
.errno
!= errno
.EPERM
: raise
524 print >> sys
.stderr
, \
525 'Cannot setuid "nobody"; try running with -n option.'
528 class_
= getattr(__main__
, options
.classname
)
529 proxy
= class_((options
.localhost
, options
.localport
),
530 (options
.remotehost
, options
.remoteport
))
533 except KeyboardInterrupt: