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()
209 if address
[0] == '<' and address
[-1] == '>' and address
!= '<>':
210 # Addresses can be in the form <person@dom.com> but watch out
211 # for null address, e.g. <>
212 address
= address
[1:-1]
215 def smtp_MAIL(self
, arg
):
216 print >> DEBUGSTREAM
, '===> MAIL', arg
217 address
= self
.__getaddr
('FROM:', arg
)
219 self
.push('501 Syntax: MAIL FROM:<address>')
222 self
.push('503 Error: nested MAIL command')
224 self
.__mailfrom
= address
225 print >> DEBUGSTREAM
, 'sender:', self
.__mailfrom
228 def smtp_RCPT(self
, arg
):
229 print >> DEBUGSTREAM
, '===> RCPT', arg
230 if not self
.__mailfrom
:
231 self
.push('503 Error: need MAIL command')
233 address
= self
.__getaddr
('TO:', arg
)
235 self
.push('501 Syntax: RCPT TO: <address>')
237 if address
.lower().startswith('stimpy'):
238 self
.push('503 You suck %s' % address
)
240 self
.__rcpttos
.append(address
)
241 print >> DEBUGSTREAM
, 'recips:', self
.__rcpttos
244 def smtp_RSET(self
, arg
):
246 self
.push('501 Syntax: RSET')
248 # Resets the sender, recipients, and data, but not the greeting
249 self
.__mailfrom
= None
252 self
.__state
= self
.COMMAND
255 def smtp_DATA(self
, arg
):
256 if not self
.__rcpttos
:
257 self
.push('503 Error: need RCPT command')
260 self
.push('501 Syntax: DATA')
262 self
.__state
= self
.DATA
263 self
.set_terminator('\r\n.\r\n')
264 self
.push('354 End data with <CR><LF>.<CR><LF>')
268 class SMTPServer(asyncore
.dispatcher
):
269 def __init__(self
, localaddr
, remoteaddr
):
270 self
._localaddr
= localaddr
271 self
._remoteaddr
= remoteaddr
272 asyncore
.dispatcher
.__init
__(self
)
273 self
.create_socket(socket
.AF_INET
, socket
.SOCK_STREAM
)
274 # try to re-use a server port if possible
275 self
.socket
.setsockopt(
276 socket
.SOL_SOCKET
, socket
.SO_REUSEADDR
,
277 self
.socket
.getsockopt(socket
.SOL_SOCKET
, socket
.SO_REUSEADDR
) |
1)
280 print '%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
281 self
.__class
__.__name
__, time
.ctime(time
.time()),
282 localaddr
, remoteaddr
)
284 def handle_accept(self
):
285 conn
, addr
= self
.accept()
286 print >> DEBUGSTREAM
, 'Incoming connection from %s' % repr(addr
)
287 channel
= SMTPChannel(self
, conn
, addr
)
289 # API for "doing something useful with the message"
290 def process_message(self
, peer
, mailfrom
, rcpttos
, data
):
291 """Override this abstract method to handle messages from the client.
293 peer is a tuple containing (ipaddr, port) of the client that made the
294 socket connection to our smtp port.
296 mailfrom is the raw address the client claims the message is coming
299 rcpttos is a list of raw addresses the client wishes to deliver the
302 data is a string containing the entire full text of the message,
303 headers (if supplied) and all. It has been `de-transparencied'
304 according to RFC 821, Section 4.5.2. In other words, a line
305 containing a `.' followed by other text has had the leading dot
308 This function should return None, for a normal `250 Ok' response;
309 otherwise it returns the desired response string in RFC 821 format.
312 raise NotImplementedError
315 class DebuggingServer(SMTPServer
):
316 # Do something with the gathered message
317 def process_message(self
, peer
, mailfrom
, rcpttos
, data
):
319 lines
= data
.split('\n')
320 print '---------- MESSAGE FOLLOWS ----------'
323 if inheaders
and not line
:
324 print 'X-Peer:', peer
[0]
327 print '------------ END MESSAGE ------------'
331 class PureProxy(SMTPServer
):
332 def process_message(self
, peer
, mailfrom
, rcpttos
, data
):
333 lines
= data
.split('\n')
334 # Look for the last header
340 lines
.insert(i
, 'X-Peer: %s' % peer
[0])
341 data
= NEWLINE
.join(lines
)
342 refused
= self
._deliver
(mailfrom
, rcpttos
, data
)
343 # TBD: what to do with refused addresses?
344 print >> DEBUGSTREAM
, 'we got some refusals'
346 def _deliver(self
, mailfrom
, rcpttos
, data
):
351 s
.connect(self
._remoteaddr
[0], self
._remoteaddr
[1])
353 refused
= s
.sendmail(mailfrom
, rcpttos
, data
)
356 except smtplib
.SMTPRecipientsRefused
, e
:
357 print >> DEBUGSTREAM
, 'got SMTPRecipientsRefused'
358 refused
= e
.recipients
359 except (socket
.error
, smtplib
.SMTPException
), e
:
360 print >> DEBUGSTREAM
, 'got', e
.__class
__
361 # All recipients were refused. If the exception had an associated
362 # error code, use it. Otherwise,fake it with a non-triggering
364 errcode
= getattr(e
, 'smtp_code', -1)
365 errmsg
= getattr(e
, 'smtp_error', 'ignore')
367 refused
[r
] = (errcode
, errmsg
)
372 class MailmanProxy(PureProxy
):
373 def process_message(self
, peer
, mailfrom
, rcpttos
, data
):
374 from cStringIO
import StringIO
376 from Mailman
import Utils
377 from Mailman
import Message
378 from Mailman
import MailList
379 # If the message is to a Mailman mailing list, then we'll invoke the
380 # Mailman script directly, without going through the real smtpd.
381 # Otherwise we'll forward it to the local proxy for disposition.
384 local
= rcpt
.lower().split('@')[0]
385 # We allow the following variations on the theme
392 parts
= local
.split('-')
400 if not Utils
.list_exists(listname
) or command
not in (
401 '', 'admin', 'owner', 'request', 'join', 'leave'):
403 listnames
.append((rcpt
, listname
, command
))
404 # Remove all list recipients from rcpttos and forward what we're not
405 # going to take care of ourselves. Linear removal should be fine
406 # since we don't expect a large number of recipients.
407 for rcpt
, listname
, command
in listnames
:
409 # If there's any non-list destined recipients left,
410 print >> DEBUGSTREAM
, 'forwarding recips:', ' '.join(rcpttos
)
412 refused
= self
._deliver
(mailfrom
, rcpttos
, data
)
413 # TBD: what to do with refused addresses?
414 print >> DEBUGSTREAM
, 'we got refusals'
415 # Now deliver directly to the list commands
418 msg
= Message
.Message(s
)
419 # These headers are required for the proper execution of Mailman. All
420 # MTAs in existance seem to add these if the original message doesn't
422 if not msg
.getheader('from'):
423 msg
['From'] = mailfrom
424 if not msg
.getheader('date'):
425 msg
['Date'] = time
.ctime(time
.time())
426 for rcpt
, listname
, command
in listnames
:
427 print >> DEBUGSTREAM
, 'sending message to', rcpt
428 mlist
= mlists
.get(listname
)
430 mlist
= MailList
.MailList(listname
, lock
=0)
431 mlists
[listname
] = mlist
432 # dispatch on the type of command
435 msg
.Enqueue(mlist
, tolist
=1)
436 elif command
== 'admin':
437 msg
.Enqueue(mlist
, toadmin
=1)
438 elif command
== 'owner':
439 msg
.Enqueue(mlist
, toowner
=1)
440 elif command
== 'request':
441 msg
.Enqueue(mlist
, torequest
=1)
442 elif command
in ('join', 'leave'):
443 # TBD: this is a hack!
444 if command
== 'join':
445 msg
['Subject'] = 'subscribe'
447 msg
['Subject'] = 'unsubscribe'
448 msg
.Enqueue(mlist
, torequest
=1)
454 classname
= 'PureProxy'
460 opts
, args
= getopt
.getopt(
461 sys
.argv
[1:], 'nVhc:d',
462 ['class=', 'nosetuid', 'version', 'help', 'debug'])
463 except getopt
.error
, e
:
467 for opt
, arg
in opts
:
468 if opt
in ('-h', '--help'):
470 elif opt
in ('-V', '--version'):
471 print >> sys
.stderr
, __version__
473 elif opt
in ('-n', '--nosetuid'):
475 elif opt
in ('-c', '--class'):
476 options
.classname
= arg
477 elif opt
in ('-d', '--debug'):
478 DEBUGSTREAM
= sys
.stderr
480 # parse the rest of the arguments
485 usage(1, 'Not enough arguments')
486 # split into host/port pairs
487 i
= localspec
.find(':')
489 usage(1, 'Bad local spec: "%s"' % localspec
)
490 options
.localhost
= localspec
[:i
]
492 options
.localport
= int(localspec
[i
+1:])
494 usage(1, 'Bad local port: "%s"' % localspec
)
495 i
= remotespec
.find(':')
497 usage(1, 'Bad remote spec: "%s"' % remotespec
)
498 options
.remotehost
= remotespec
[:i
]
500 options
.remoteport
= int(remotespec
[i
+1:])
502 usage(1, 'Bad remote port: "%s"' % remotespec
)
507 if __name__
== '__main__':
508 options
= parseargs()
514 print >> sys
.stderr
, \
515 'Cannot import module "pwd"; try running with -n option.'
517 nobody
= pwd
.getpwnam('nobody')[2]
521 if e
.errno
!= errno
.EPERM
: raise
522 print >> sys
.stderr
, \
523 'Cannot setuid "nobody"; try running with -n option.'
526 class_
= getattr(__main__
, options
.classname
)
527 proxy
= class_((options
.localhost
, options
.localport
),
528 (options
.remotehost
, options
.remoteport
))
531 except KeyboardInterrupt: