blob: 32f45ae768434d4d810728880532c5eb6261699f [file] [log] [blame]
Benjamin Peterson90f5ba52010-03-11 22:53:45 +00001#! /usr/bin/env python3
Barry Warsaw406d46e2001-08-13 21:18:01 +00002"""An RFC 2821 smtp proxy.
Barry Warsaw7e0d9562001-01-31 22:51:35 +00003
Barry Warsaw0e8427e2001-10-04 16:27:04 +00004Usage: %(program)s [options] [localhost:localport [remotehost:remoteport]]
Barry Warsaw7e0d9562001-01-31 22:51:35 +00005
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
Barry Warsawf267b622004-10-09 21:44:13 +000020 Use `classname' as the concrete SMTP proxy class. Uses `PureProxy' by
Barry Warsaw7e0d9562001-01-31 22:51:35 +000021 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
Barry Warsaw0e8427e2001-10-04 16:27:04 +000033If 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.
Barry Warsaw7e0d9562001-01-31 22:51:35 +000036"""
37
Barry Warsaw0e8427e2001-10-04 16:27:04 +000038
Barry Warsaw7e0d9562001-01-31 22:51:35 +000039# 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#
Guido van Rossumb8b45ea2001-04-15 13:06:04 +000045# SMTPServer - the base class for the backend. Raises NotImplementedError
Barry Warsaw7e0d9562001-01-31 22:51:35 +000046# 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#
Barry Warsaw7e0d9562001-01-31 22:51:35 +000062#
Barry Warsawb1027642004-07-12 23:10:08 +000063# Author: Barry Warsaw <barry@python.org>
Barry Warsaw7e0d9562001-01-31 22:51:35 +000064#
65# TODO:
66#
67# - support mailbox delivery
68# - alias files
69# - ESMTP
70# - handle error codes from the backend smtpd
71
72import sys
73import os
74import errno
75import getopt
76import time
77import socket
78import asyncore
79import asynchat
Richard Jones803ef8a2010-07-24 09:51:40 +000080from warnings import warn
Barry Warsaw7e0d9562001-01-31 22:51:35 +000081
Skip Montanaro0de65802001-02-15 22:15:14 +000082__all__ = ["SMTPServer","DebuggingServer","PureProxy","MailmanProxy"]
Barry Warsaw7e0d9562001-01-31 22:51:35 +000083
84program = sys.argv[0]
85__version__ = 'Python SMTP proxy version 0.2'
86
87
88class Devnull:
89 def write(self, msg): pass
90 def flush(self): pass
91
92
93DEBUGSTREAM = Devnull()
94NEWLINE = '\n'
95EMPTYSTRING = ''
Barry Warsaw0e8427e2001-10-04 16:27:04 +000096COMMASPACE = ', '
Barry Warsaw7e0d9562001-01-31 22:51:35 +000097
98
Barry Warsaw0e8427e2001-10-04 16:27:04 +000099
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000100def usage(code, msg=''):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000101 print(__doc__ % globals(), file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000102 if msg:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000103 print(msg, file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000104 sys.exit(code)
105
106
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000107
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000108class SMTPChannel(asynchat.async_chat):
109 COMMAND = 0
110 DATA = 1
111
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000112 data_size_limit = 33554432
113 command_size_limit = 512
114
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000115 def __init__(self, server, conn, addr):
116 asynchat.async_chat.__init__(self, conn)
Richard Jones803ef8a2010-07-24 09:51:40 +0000117 self.smtp_server = server
118 self.conn = conn
119 self.addr = addr
120 self.received_lines = []
121 self.smtp_state = self.COMMAND
122 self.seen_greeting = ''
123 self.mailfrom = None
124 self.rcpttos = []
125 self.received_data = ''
126 self.fqdn = socket.getfqdn()
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000127 self.num_bytes = 0
Giampaolo RodolĂ 9cf5ef42010-08-23 22:28:13 +0000128 try:
129 self.peer = conn.getpeername()
130 except socket.error as err:
131 # a race condition may occur if the other end is closing
132 # before we can get the peername
133 self.close()
134 if err.args[0] != errno.ENOTCONN:
135 raise
136 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000137 print('Peer:', repr(self.peer), file=DEBUGSTREAM)
138 self.push('220 %s %s' % (self.fqdn, __version__))
Josiah Carlsond74900e2008-07-07 04:15:08 +0000139 self.set_terminator(b'\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000140
Richard Jones803ef8a2010-07-24 09:51:40 +0000141 # properties for backwards-compatibility
142 @property
143 def __server(self):
144 warn("Access to __server attribute on SMTPChannel is deprecated, "
145 "use 'smtp_server' instead", PendingDeprecationWarning, 2)
146 return self.smtp_server
147 @__server.setter
148 def __server(self, value):
149 warn("Setting __server attribute on SMTPChannel is deprecated, "
150 "set 'smtp_server' instead", PendingDeprecationWarning, 2)
151 self.smtp_server = value
152
153 @property
154 def __line(self):
155 warn("Access to __line attribute on SMTPChannel is deprecated, "
156 "use 'received_lines' instead", PendingDeprecationWarning, 2)
157 return self.received_lines
158 @__line.setter
159 def __line(self, value):
160 warn("Setting __line attribute on SMTPChannel is deprecated, "
161 "set 'received_lines' instead", PendingDeprecationWarning, 2)
162 self.received_lines = value
163
164 @property
165 def __state(self):
166 warn("Access to __state attribute on SMTPChannel is deprecated, "
167 "use 'smtp_state' instead", PendingDeprecationWarning, 2)
168 return self.smtp_state
169 @__state.setter
170 def __state(self, value):
171 warn("Setting __state attribute on SMTPChannel is deprecated, "
172 "set 'smtp_state' instead", PendingDeprecationWarning, 2)
173 self.smtp_state = value
174
175 @property
176 def __greeting(self):
177 warn("Access to __greeting attribute on SMTPChannel is deprecated, "
178 "use 'seen_greeting' instead", PendingDeprecationWarning, 2)
179 return self.seen_greeting
180 @__greeting.setter
181 def __greeting(self, value):
182 warn("Setting __greeting attribute on SMTPChannel is deprecated, "
183 "set 'seen_greeting' instead", PendingDeprecationWarning, 2)
184 self.seen_greeting = value
185
186 @property
187 def __mailfrom(self):
188 warn("Access to __mailfrom attribute on SMTPChannel is deprecated, "
189 "use 'mailfrom' instead", PendingDeprecationWarning, 2)
190 return self.mailfrom
191 @__mailfrom.setter
192 def __mailfrom(self, value):
193 warn("Setting __mailfrom attribute on SMTPChannel is deprecated, "
194 "set 'mailfrom' instead", PendingDeprecationWarning, 2)
195 self.mailfrom = value
196
197 @property
198 def __rcpttos(self):
199 warn("Access to __rcpttos attribute on SMTPChannel is deprecated, "
200 "use 'rcpttos' instead", PendingDeprecationWarning, 2)
201 return self.rcpttos
202 @__rcpttos.setter
203 def __rcpttos(self, value):
204 warn("Setting __rcpttos attribute on SMTPChannel is deprecated, "
205 "set 'rcpttos' instead", PendingDeprecationWarning, 2)
206 self.rcpttos = value
207
208 @property
209 def __data(self):
210 warn("Access to __data attribute on SMTPChannel is deprecated, "
211 "use 'received_data' instead", PendingDeprecationWarning, 2)
212 return self.received_data
213 @__data.setter
214 def __data(self, value):
215 warn("Setting __data attribute on SMTPChannel is deprecated, "
216 "set 'received_data' instead", PendingDeprecationWarning, 2)
217 self.received_data = value
218
219 @property
220 def __fqdn(self):
221 warn("Access to __fqdn attribute on SMTPChannel is deprecated, "
222 "use 'fqdn' instead", PendingDeprecationWarning, 2)
223 return self.fqdn
224 @__fqdn.setter
225 def __fqdn(self, value):
226 warn("Setting __fqdn attribute on SMTPChannel is deprecated, "
227 "set 'fqdn' instead", PendingDeprecationWarning, 2)
228 self.fqdn = value
229
230 @property
231 def __peer(self):
232 warn("Access to __peer attribute on SMTPChannel is deprecated, "
233 "use 'peer' instead", PendingDeprecationWarning, 2)
234 return self.peer
235 @__peer.setter
236 def __peer(self, value):
237 warn("Setting __peer attribute on SMTPChannel is deprecated, "
238 "set 'peer' instead", PendingDeprecationWarning, 2)
239 self.peer = value
240
241 @property
242 def __conn(self):
243 warn("Access to __conn attribute on SMTPChannel is deprecated, "
244 "use 'conn' instead", PendingDeprecationWarning, 2)
245 return self.conn
246 @__conn.setter
247 def __conn(self, value):
248 warn("Setting __conn attribute on SMTPChannel is deprecated, "
249 "set 'conn' instead", PendingDeprecationWarning, 2)
250 self.conn = value
251
252 @property
253 def __addr(self):
254 warn("Access to __addr attribute on SMTPChannel is deprecated, "
255 "use 'addr' instead", PendingDeprecationWarning, 2)
256 return self.addr
257 @__addr.setter
258 def __addr(self, value):
259 warn("Setting __addr attribute on SMTPChannel is deprecated, "
260 "set 'addr' instead", PendingDeprecationWarning, 2)
261 self.addr = value
262
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000263 # Overrides base class for convenience
264 def push(self, msg):
Josiah Carlsond74900e2008-07-07 04:15:08 +0000265 asynchat.async_chat.push(self, bytes(msg + '\r\n', 'ascii'))
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000266
267 # Implementation of base class abstract method
268 def collect_incoming_data(self, data):
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000269 limit = None
270 if self.smtp_state == self.COMMAND:
271 limit = self.command_size_limit
272 elif self.smtp_state == self.DATA:
273 limit = self.data_size_limit
274 if limit and self.num_bytes > limit:
275 return
276 elif limit:
277 self.num_bytes += len(data)
Marc-André Lemburg8f36af72011-02-25 15:42:01 +0000278 self.received_lines.append(str(data, "utf-8"))
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000279
280 # Implementation of base class abstract method
281 def found_terminator(self):
Richard Jones803ef8a2010-07-24 09:51:40 +0000282 line = EMPTYSTRING.join(self.received_lines)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000283 print('Data:', repr(line), file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000284 self.received_lines = []
285 if self.smtp_state == self.COMMAND:
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000286 if self.num_bytes > self.command_size_limit:
287 self.push('500 Error: line too long')
288 self.num_bytes = 0
289 return
290 self.num_bytes = 0
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000291 if not line:
292 self.push('500 Error: bad syntax')
293 return
294 method = None
295 i = line.find(' ')
296 if i < 0:
297 command = line.upper()
298 arg = None
299 else:
300 command = line[:i].upper()
301 arg = line[i+1:].strip()
302 method = getattr(self, 'smtp_' + command, None)
303 if not method:
304 self.push('502 Error: command "%s" not implemented' % command)
305 return
306 method(arg)
307 return
308 else:
Richard Jones803ef8a2010-07-24 09:51:40 +0000309 if self.smtp_state != self.DATA:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000310 self.push('451 Internal confusion')
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000311 self.num_bytes = 0
312 return
313 if self.num_bytes > self.data_size_limit:
314 self.push('552 Error: Too much mail data')
315 self.num_bytes = 0
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000316 return
317 # Remove extraneous carriage returns and de-transparency according
318 # to RFC 821, Section 4.5.2.
319 data = []
320 for text in line.split('\r\n'):
321 if text and text[0] == '.':
322 data.append(text[1:])
323 else:
324 data.append(text)
Richard Jones803ef8a2010-07-24 09:51:40 +0000325 self.received_data = NEWLINE.join(data)
Georg Brandl17e3d692010-07-31 10:08:09 +0000326 status = self.smtp_server.process_message(self.peer,
327 self.mailfrom,
328 self.rcpttos,
329 self.received_data)
Richard Jones803ef8a2010-07-24 09:51:40 +0000330 self.rcpttos = []
331 self.mailfrom = None
332 self.smtp_state = self.COMMAND
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000333 self.num_bytes = 0
Josiah Carlsond74900e2008-07-07 04:15:08 +0000334 self.set_terminator(b'\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000335 if not status:
336 self.push('250 Ok')
337 else:
338 self.push(status)
339
340 # SMTP and ESMTP commands
341 def smtp_HELO(self, arg):
342 if not arg:
343 self.push('501 Syntax: HELO hostname')
344 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000345 if self.seen_greeting:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000346 self.push('503 Duplicate HELO/EHLO')
347 else:
Richard Jones803ef8a2010-07-24 09:51:40 +0000348 self.seen_greeting = arg
349 self.push('250 %s' % self.fqdn)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000350
351 def smtp_NOOP(self, arg):
352 if arg:
353 self.push('501 Syntax: NOOP')
354 else:
355 self.push('250 Ok')
356
357 def smtp_QUIT(self, arg):
358 # args is ignored
359 self.push('221 Bye')
360 self.close_when_done()
361
362 # factored
363 def __getaddr(self, keyword, arg):
364 address = None
365 keylen = len(keyword)
366 if arg[:keylen].upper() == keyword:
367 address = arg[keylen:].strip()
Barry Warsawebf54272001-11-04 03:04:25 +0000368 if not address:
369 pass
370 elif address[0] == '<' and address[-1] == '>' and address != '<>':
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000371 # Addresses can be in the form <person@dom.com> but watch out
372 # for null address, e.g. <>
373 address = address[1:-1]
374 return address
375
376 def smtp_MAIL(self, arg):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000377 print('===> MAIL', arg, file=DEBUGSTREAM)
Guido van Rossum8ce8a782007-11-01 19:42:39 +0000378 address = self.__getaddr('FROM:', arg) if arg else None
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000379 if not address:
380 self.push('501 Syntax: MAIL FROM:<address>')
381 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000382 if self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000383 self.push('503 Error: nested MAIL command')
384 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000385 self.mailfrom = address
386 print('sender:', self.mailfrom, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000387 self.push('250 Ok')
388
389 def smtp_RCPT(self, arg):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000390 print('===> RCPT', arg, file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000391 if not self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000392 self.push('503 Error: need MAIL command')
393 return
Guido van Rossum8ce8a782007-11-01 19:42:39 +0000394 address = self.__getaddr('TO:', arg) if arg else None
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000395 if not address:
396 self.push('501 Syntax: RCPT TO: <address>')
397 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000398 self.rcpttos.append(address)
399 print('recips:', self.rcpttos, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000400 self.push('250 Ok')
401
402 def smtp_RSET(self, arg):
403 if arg:
404 self.push('501 Syntax: RSET')
405 return
406 # Resets the sender, recipients, and data, but not the greeting
Richard Jones803ef8a2010-07-24 09:51:40 +0000407 self.mailfrom = None
408 self.rcpttos = []
409 self.received_data = ''
410 self.smtp_state = self.COMMAND
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000411 self.push('250 Ok')
412
413 def smtp_DATA(self, arg):
Richard Jones803ef8a2010-07-24 09:51:40 +0000414 if not self.rcpttos:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000415 self.push('503 Error: need RCPT command')
416 return
417 if arg:
418 self.push('501 Syntax: DATA')
419 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000420 self.smtp_state = self.DATA
Josiah Carlsond74900e2008-07-07 04:15:08 +0000421 self.set_terminator(b'\r\n.\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000422 self.push('354 End data with <CR><LF>.<CR><LF>')
423
424
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000425
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000426class SMTPServer(asyncore.dispatcher):
Richard Jones803ef8a2010-07-24 09:51:40 +0000427 # SMTPChannel class to use for managing client connections
428 channel_class = SMTPChannel
429
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000430 def __init__(self, localaddr, remoteaddr):
431 self._localaddr = localaddr
432 self._remoteaddr = remoteaddr
433 asyncore.dispatcher.__init__(self)
Giampaolo RodolĂ 610aa4f2010-06-30 17:47:39 +0000434 try:
435 self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
436 # try to re-use a server port if possible
437 self.set_reuse_addr()
438 self.bind(localaddr)
439 self.listen(5)
440 except:
441 self.close()
442 raise
443 else:
444 print('%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
445 self.__class__.__name__, time.ctime(time.time()),
446 localaddr, remoteaddr), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000447
Giampaolo RodolĂ 977c7072010-10-04 21:08:36 +0000448 def handle_accepted(self, conn, addr):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000449 print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000450 channel = self.channel_class(self, conn, addr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000451
452 # API for "doing something useful with the message"
453 def process_message(self, peer, mailfrom, rcpttos, data):
454 """Override this abstract method to handle messages from the client.
455
456 peer is a tuple containing (ipaddr, port) of the client that made the
457 socket connection to our smtp port.
458
459 mailfrom is the raw address the client claims the message is coming
460 from.
461
462 rcpttos is a list of raw addresses the client wishes to deliver the
463 message to.
464
465 data is a string containing the entire full text of the message,
466 headers (if supplied) and all. It has been `de-transparencied'
467 according to RFC 821, Section 4.5.2. In other words, a line
468 containing a `.' followed by other text has had the leading dot
469 removed.
470
471 This function should return None, for a normal `250 Ok' response;
472 otherwise it returns the desired response string in RFC 821 format.
473
474 """
Guido van Rossumb8b45ea2001-04-15 13:06:04 +0000475 raise NotImplementedError
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000476
Tim Peters658cba62001-02-09 20:06:00 +0000477
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000478
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000479class DebuggingServer(SMTPServer):
480 # Do something with the gathered message
481 def process_message(self, peer, mailfrom, rcpttos, data):
482 inheaders = 1
483 lines = data.split('\n')
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000484 print('---------- MESSAGE FOLLOWS ----------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000485 for line in lines:
486 # headers first
487 if inheaders and not line:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000488 print('X-Peer:', peer[0])
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000489 inheaders = 0
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000490 print(line)
491 print('------------ END MESSAGE ------------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000492
493
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000494
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000495class PureProxy(SMTPServer):
496 def process_message(self, peer, mailfrom, rcpttos, data):
497 lines = data.split('\n')
498 # Look for the last header
499 i = 0
500 for line in lines:
501 if not line:
502 break
503 i += 1
504 lines.insert(i, 'X-Peer: %s' % peer[0])
505 data = NEWLINE.join(lines)
506 refused = self._deliver(mailfrom, rcpttos, data)
507 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000508 print('we got some refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000509
510 def _deliver(self, mailfrom, rcpttos, data):
511 import smtplib
512 refused = {}
513 try:
514 s = smtplib.SMTP()
515 s.connect(self._remoteaddr[0], self._remoteaddr[1])
516 try:
517 refused = s.sendmail(mailfrom, rcpttos, data)
518 finally:
519 s.quit()
Guido van Rossumb940e112007-01-10 16:19:56 +0000520 except smtplib.SMTPRecipientsRefused as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000521 print('got SMTPRecipientsRefused', file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000522 refused = e.recipients
Guido van Rossumb940e112007-01-10 16:19:56 +0000523 except (socket.error, smtplib.SMTPException) as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000524 print('got', e.__class__, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000525 # All recipients were refused. If the exception had an associated
526 # error code, use it. Otherwise,fake it with a non-triggering
527 # exception code.
528 errcode = getattr(e, 'smtp_code', -1)
529 errmsg = getattr(e, 'smtp_error', 'ignore')
530 for r in rcpttos:
531 refused[r] = (errcode, errmsg)
532 return refused
533
534
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000535
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000536class MailmanProxy(PureProxy):
537 def process_message(self, peer, mailfrom, rcpttos, data):
Guido van Rossum68937b42007-05-18 00:51:22 +0000538 from io import StringIO
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000539 from Mailman import Utils
540 from Mailman import Message
541 from Mailman import MailList
542 # If the message is to a Mailman mailing list, then we'll invoke the
543 # Mailman script directly, without going through the real smtpd.
544 # Otherwise we'll forward it to the local proxy for disposition.
545 listnames = []
546 for rcpt in rcpttos:
547 local = rcpt.lower().split('@')[0]
548 # We allow the following variations on the theme
549 # listname
550 # listname-admin
551 # listname-owner
552 # listname-request
553 # listname-join
554 # listname-leave
555 parts = local.split('-')
556 if len(parts) > 2:
557 continue
558 listname = parts[0]
559 if len(parts) == 2:
560 command = parts[1]
561 else:
562 command = ''
563 if not Utils.list_exists(listname) or command not in (
564 '', 'admin', 'owner', 'request', 'join', 'leave'):
565 continue
566 listnames.append((rcpt, listname, command))
567 # Remove all list recipients from rcpttos and forward what we're not
568 # going to take care of ourselves. Linear removal should be fine
569 # since we don't expect a large number of recipients.
570 for rcpt, listname, command in listnames:
571 rcpttos.remove(rcpt)
Tim Peters658cba62001-02-09 20:06:00 +0000572 # If there's any non-list destined recipients left,
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000573 print('forwarding recips:', ' '.join(rcpttos), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000574 if rcpttos:
575 refused = self._deliver(mailfrom, rcpttos, data)
576 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000577 print('we got refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000578 # Now deliver directly to the list commands
579 mlists = {}
580 s = StringIO(data)
581 msg = Message.Message(s)
582 # These headers are required for the proper execution of Mailman. All
Mark Dickinson934896d2009-02-21 20:59:32 +0000583 # MTAs in existence seem to add these if the original message doesn't
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000584 # have them.
Barry Warsaw820c1202008-06-12 04:06:45 +0000585 if not msg.get('from'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000586 msg['From'] = mailfrom
Barry Warsaw820c1202008-06-12 04:06:45 +0000587 if not msg.get('date'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000588 msg['Date'] = time.ctime(time.time())
589 for rcpt, listname, command in listnames:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000590 print('sending message to', rcpt, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000591 mlist = mlists.get(listname)
592 if not mlist:
593 mlist = MailList.MailList(listname, lock=0)
594 mlists[listname] = mlist
595 # dispatch on the type of command
596 if command == '':
597 # post
598 msg.Enqueue(mlist, tolist=1)
599 elif command == 'admin':
600 msg.Enqueue(mlist, toadmin=1)
601 elif command == 'owner':
602 msg.Enqueue(mlist, toowner=1)
603 elif command == 'request':
604 msg.Enqueue(mlist, torequest=1)
605 elif command in ('join', 'leave'):
606 # TBD: this is a hack!
607 if command == 'join':
608 msg['Subject'] = 'subscribe'
609 else:
610 msg['Subject'] = 'unsubscribe'
611 msg.Enqueue(mlist, torequest=1)
612
613
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000614
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000615class Options:
616 setuid = 1
617 classname = 'PureProxy'
618
619
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000620
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000621def parseargs():
622 global DEBUGSTREAM
623 try:
624 opts, args = getopt.getopt(
625 sys.argv[1:], 'nVhc:d',
626 ['class=', 'nosetuid', 'version', 'help', 'debug'])
Guido van Rossumb940e112007-01-10 16:19:56 +0000627 except getopt.error as e:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000628 usage(1, e)
629
630 options = Options()
631 for opt, arg in opts:
632 if opt in ('-h', '--help'):
633 usage(0)
634 elif opt in ('-V', '--version'):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000635 print(__version__, file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000636 sys.exit(0)
637 elif opt in ('-n', '--nosetuid'):
638 options.setuid = 0
639 elif opt in ('-c', '--class'):
640 options.classname = arg
641 elif opt in ('-d', '--debug'):
642 DEBUGSTREAM = sys.stderr
643
644 # parse the rest of the arguments
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000645 if len(args) < 1:
646 localspec = 'localhost:8025'
647 remotespec = 'localhost:25'
648 elif len(args) < 2:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000649 localspec = args[0]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000650 remotespec = 'localhost:25'
Barry Warsawebf54272001-11-04 03:04:25 +0000651 elif len(args) < 3:
652 localspec = args[0]
653 remotespec = args[1]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000654 else:
655 usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
656
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000657 # split into host/port pairs
658 i = localspec.find(':')
659 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000660 usage(1, 'Bad local spec: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000661 options.localhost = localspec[:i]
662 try:
663 options.localport = int(localspec[i+1:])
664 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000665 usage(1, 'Bad local port: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000666 i = remotespec.find(':')
667 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000668 usage(1, 'Bad remote spec: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000669 options.remotehost = remotespec[:i]
670 try:
671 options.remoteport = int(remotespec[i+1:])
672 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000673 usage(1, 'Bad remote port: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000674 return options
675
676
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000677
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000678if __name__ == '__main__':
679 options = parseargs()
680 # Become nobody
681 if options.setuid:
682 try:
683 import pwd
684 except ImportError:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000685 print('Cannot import module "pwd"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000686 sys.exit(1)
687 nobody = pwd.getpwnam('nobody')[2]
688 try:
689 os.setuid(nobody)
Guido van Rossumb940e112007-01-10 16:19:56 +0000690 except OSError as e:
Guido van Rossum4ba3d652001-03-02 06:42:34 +0000691 if e.errno != errno.EPERM: raise
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000692 print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000693 sys.exit(1)
Skip Montanaro90e01532004-06-26 19:18:49 +0000694 classname = options.classname
695 if "." in classname:
696 lastdot = classname.rfind(".")
697 mod = __import__(classname[:lastdot], globals(), locals(), [""])
698 classname = classname[lastdot+1:]
699 else:
700 import __main__ as mod
Skip Montanaro90e01532004-06-26 19:18:49 +0000701 class_ = getattr(mod, classname)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000702 proxy = class_((options.localhost, options.localport),
703 (options.remotehost, options.remoteport))
704 try:
705 asyncore.loop()
706 except KeyboardInterrupt:
707 pass