blob: dd8398836ff96c0af9172c5438ed939ac4654778 [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#
62# Please note that this script requires Python 2.0
63#
Barry Warsawb1027642004-07-12 23:10:08 +000064# Author: Barry Warsaw <barry@python.org>
Barry Warsaw7e0d9562001-01-31 22:51:35 +000065#
66# TODO:
67#
68# - support mailbox delivery
69# - alias files
70# - ESMTP
71# - handle error codes from the backend smtpd
72
73import sys
74import os
75import errno
76import getopt
77import time
78import socket
79import asyncore
80import asynchat
Richard Jones803ef8a2010-07-24 09:51:40 +000081from warnings import warn
Barry Warsaw7e0d9562001-01-31 22:51:35 +000082
Skip Montanaro0de65802001-02-15 22:15:14 +000083__all__ = ["SMTPServer","DebuggingServer","PureProxy","MailmanProxy"]
Barry Warsaw7e0d9562001-01-31 22:51:35 +000084
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 = ''
Barry Warsaw0e8427e2001-10-04 16:27:04 +000097COMMASPACE = ', '
Barry Warsaw7e0d9562001-01-31 22:51:35 +000098
99
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000100
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000101def usage(code, msg=''):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000102 print(__doc__ % globals(), file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000103 if msg:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000104 print(msg, file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000105 sys.exit(code)
106
107
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000108
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000109class SMTPChannel(asynchat.async_chat):
110 COMMAND = 0
111 DATA = 1
112
113 def __init__(self, server, conn, addr):
114 asynchat.async_chat.__init__(self, conn)
Richard Jones803ef8a2010-07-24 09:51:40 +0000115 self.smtp_server = server
116 self.conn = conn
117 self.addr = addr
118 self.received_lines = []
119 self.smtp_state = self.COMMAND
120 self.seen_greeting = ''
121 self.mailfrom = None
122 self.rcpttos = []
123 self.received_data = ''
124 self.fqdn = socket.getfqdn()
125 self.peer = conn.getpeername()
126 print('Peer:', repr(self.peer), file=DEBUGSTREAM)
127 self.push('220 %s %s' % (self.fqdn, __version__))
Josiah Carlsond74900e2008-07-07 04:15:08 +0000128 self.set_terminator(b'\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000129
Richard Jones803ef8a2010-07-24 09:51:40 +0000130 # properties for backwards-compatibility
131 @property
132 def __server(self):
133 warn("Access to __server attribute on SMTPChannel is deprecated, "
134 "use 'smtp_server' instead", PendingDeprecationWarning, 2)
135 return self.smtp_server
136 @__server.setter
137 def __server(self, value):
138 warn("Setting __server attribute on SMTPChannel is deprecated, "
139 "set 'smtp_server' instead", PendingDeprecationWarning, 2)
140 self.smtp_server = value
141
142 @property
143 def __line(self):
144 warn("Access to __line attribute on SMTPChannel is deprecated, "
145 "use 'received_lines' instead", PendingDeprecationWarning, 2)
146 return self.received_lines
147 @__line.setter
148 def __line(self, value):
149 warn("Setting __line attribute on SMTPChannel is deprecated, "
150 "set 'received_lines' instead", PendingDeprecationWarning, 2)
151 self.received_lines = value
152
153 @property
154 def __state(self):
155 warn("Access to __state attribute on SMTPChannel is deprecated, "
156 "use 'smtp_state' instead", PendingDeprecationWarning, 2)
157 return self.smtp_state
158 @__state.setter
159 def __state(self, value):
160 warn("Setting __state attribute on SMTPChannel is deprecated, "
161 "set 'smtp_state' instead", PendingDeprecationWarning, 2)
162 self.smtp_state = value
163
164 @property
165 def __greeting(self):
166 warn("Access to __greeting attribute on SMTPChannel is deprecated, "
167 "use 'seen_greeting' instead", PendingDeprecationWarning, 2)
168 return self.seen_greeting
169 @__greeting.setter
170 def __greeting(self, value):
171 warn("Setting __greeting attribute on SMTPChannel is deprecated, "
172 "set 'seen_greeting' instead", PendingDeprecationWarning, 2)
173 self.seen_greeting = value
174
175 @property
176 def __mailfrom(self):
177 warn("Access to __mailfrom attribute on SMTPChannel is deprecated, "
178 "use 'mailfrom' instead", PendingDeprecationWarning, 2)
179 return self.mailfrom
180 @__mailfrom.setter
181 def __mailfrom(self, value):
182 warn("Setting __mailfrom attribute on SMTPChannel is deprecated, "
183 "set 'mailfrom' instead", PendingDeprecationWarning, 2)
184 self.mailfrom = value
185
186 @property
187 def __rcpttos(self):
188 warn("Access to __rcpttos attribute on SMTPChannel is deprecated, "
189 "use 'rcpttos' instead", PendingDeprecationWarning, 2)
190 return self.rcpttos
191 @__rcpttos.setter
192 def __rcpttos(self, value):
193 warn("Setting __rcpttos attribute on SMTPChannel is deprecated, "
194 "set 'rcpttos' instead", PendingDeprecationWarning, 2)
195 self.rcpttos = value
196
197 @property
198 def __data(self):
199 warn("Access to __data attribute on SMTPChannel is deprecated, "
200 "use 'received_data' instead", PendingDeprecationWarning, 2)
201 return self.received_data
202 @__data.setter
203 def __data(self, value):
204 warn("Setting __data attribute on SMTPChannel is deprecated, "
205 "set 'received_data' instead", PendingDeprecationWarning, 2)
206 self.received_data = value
207
208 @property
209 def __fqdn(self):
210 warn("Access to __fqdn attribute on SMTPChannel is deprecated, "
211 "use 'fqdn' instead", PendingDeprecationWarning, 2)
212 return self.fqdn
213 @__fqdn.setter
214 def __fqdn(self, value):
215 warn("Setting __fqdn attribute on SMTPChannel is deprecated, "
216 "set 'fqdn' instead", PendingDeprecationWarning, 2)
217 self.fqdn = value
218
219 @property
220 def __peer(self):
221 warn("Access to __peer attribute on SMTPChannel is deprecated, "
222 "use 'peer' instead", PendingDeprecationWarning, 2)
223 return self.peer
224 @__peer.setter
225 def __peer(self, value):
226 warn("Setting __peer attribute on SMTPChannel is deprecated, "
227 "set 'peer' instead", PendingDeprecationWarning, 2)
228 self.peer = value
229
230 @property
231 def __conn(self):
232 warn("Access to __conn attribute on SMTPChannel is deprecated, "
233 "use 'conn' instead", PendingDeprecationWarning, 2)
234 return self.conn
235 @__conn.setter
236 def __conn(self, value):
237 warn("Setting __conn attribute on SMTPChannel is deprecated, "
238 "set 'conn' instead", PendingDeprecationWarning, 2)
239 self.conn = value
240
241 @property
242 def __addr(self):
243 warn("Access to __addr attribute on SMTPChannel is deprecated, "
244 "use 'addr' instead", PendingDeprecationWarning, 2)
245 return self.addr
246 @__addr.setter
247 def __addr(self, value):
248 warn("Setting __addr attribute on SMTPChannel is deprecated, "
249 "set 'addr' instead", PendingDeprecationWarning, 2)
250 self.addr = value
251
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000252 # Overrides base class for convenience
253 def push(self, msg):
Josiah Carlsond74900e2008-07-07 04:15:08 +0000254 asynchat.async_chat.push(self, bytes(msg + '\r\n', 'ascii'))
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000255
256 # Implementation of base class abstract method
257 def collect_incoming_data(self, data):
Richard Jones803ef8a2010-07-24 09:51:40 +0000258 self.received_lines.append(str(data, "utf8"))
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000259
260 # Implementation of base class abstract method
261 def found_terminator(self):
Richard Jones803ef8a2010-07-24 09:51:40 +0000262 line = EMPTYSTRING.join(self.received_lines)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000263 print('Data:', repr(line), file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000264 self.received_lines = []
265 if self.smtp_state == self.COMMAND:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000266 if not line:
267 self.push('500 Error: bad syntax')
268 return
269 method = None
270 i = line.find(' ')
271 if i < 0:
272 command = line.upper()
273 arg = None
274 else:
275 command = line[:i].upper()
276 arg = line[i+1:].strip()
277 method = getattr(self, 'smtp_' + command, None)
278 if not method:
279 self.push('502 Error: command "%s" not implemented' % command)
280 return
281 method(arg)
282 return
283 else:
Richard Jones803ef8a2010-07-24 09:51:40 +0000284 if self.smtp_state != self.DATA:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000285 self.push('451 Internal confusion')
286 return
287 # Remove extraneous carriage returns and de-transparency according
288 # to RFC 821, Section 4.5.2.
289 data = []
290 for text in line.split('\r\n'):
291 if text and text[0] == '.':
292 data.append(text[1:])
293 else:
294 data.append(text)
Richard Jones803ef8a2010-07-24 09:51:40 +0000295 self.received_data = NEWLINE.join(data)
296 status = self.__server.process_message(self.peer,
297 self.mailfrom,
298 self.rcpttos,
299 self.received_data)
300 self.rcpttos = []
301 self.mailfrom = None
302 self.smtp_state = self.COMMAND
Josiah Carlsond74900e2008-07-07 04:15:08 +0000303 self.set_terminator(b'\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000304 if not status:
305 self.push('250 Ok')
306 else:
307 self.push(status)
308
309 # SMTP and ESMTP commands
310 def smtp_HELO(self, arg):
311 if not arg:
312 self.push('501 Syntax: HELO hostname')
313 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000314 if self.seen_greeting:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000315 self.push('503 Duplicate HELO/EHLO')
316 else:
Richard Jones803ef8a2010-07-24 09:51:40 +0000317 self.seen_greeting = arg
318 self.push('250 %s' % self.fqdn)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000319
320 def smtp_NOOP(self, arg):
321 if arg:
322 self.push('501 Syntax: NOOP')
323 else:
324 self.push('250 Ok')
325
326 def smtp_QUIT(self, arg):
327 # args is ignored
328 self.push('221 Bye')
329 self.close_when_done()
330
331 # factored
332 def __getaddr(self, keyword, arg):
333 address = None
334 keylen = len(keyword)
335 if arg[:keylen].upper() == keyword:
336 address = arg[keylen:].strip()
Barry Warsawebf54272001-11-04 03:04:25 +0000337 if not address:
338 pass
339 elif address[0] == '<' and address[-1] == '>' and address != '<>':
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000340 # Addresses can be in the form <person@dom.com> but watch out
341 # for null address, e.g. <>
342 address = address[1:-1]
343 return address
344
345 def smtp_MAIL(self, arg):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000346 print('===> MAIL', arg, file=DEBUGSTREAM)
Guido van Rossum8ce8a782007-11-01 19:42:39 +0000347 address = self.__getaddr('FROM:', arg) if arg else None
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000348 if not address:
349 self.push('501 Syntax: MAIL FROM:<address>')
350 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000351 if self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000352 self.push('503 Error: nested MAIL command')
353 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000354 self.mailfrom = address
355 print('sender:', self.mailfrom, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000356 self.push('250 Ok')
357
358 def smtp_RCPT(self, arg):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000359 print('===> RCPT', arg, file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000360 if not self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000361 self.push('503 Error: need MAIL command')
362 return
Guido van Rossum8ce8a782007-11-01 19:42:39 +0000363 address = self.__getaddr('TO:', arg) if arg else None
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000364 if not address:
365 self.push('501 Syntax: RCPT TO: <address>')
366 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000367 self.rcpttos.append(address)
368 print('recips:', self.rcpttos, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000369 self.push('250 Ok')
370
371 def smtp_RSET(self, arg):
372 if arg:
373 self.push('501 Syntax: RSET')
374 return
375 # Resets the sender, recipients, and data, but not the greeting
Richard Jones803ef8a2010-07-24 09:51:40 +0000376 self.mailfrom = None
377 self.rcpttos = []
378 self.received_data = ''
379 self.smtp_state = self.COMMAND
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000380 self.push('250 Ok')
381
382 def smtp_DATA(self, arg):
Richard Jones803ef8a2010-07-24 09:51:40 +0000383 if not self.rcpttos:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000384 self.push('503 Error: need RCPT command')
385 return
386 if arg:
387 self.push('501 Syntax: DATA')
388 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000389 self.smtp_state = self.DATA
Josiah Carlsond74900e2008-07-07 04:15:08 +0000390 self.set_terminator(b'\r\n.\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000391 self.push('354 End data with <CR><LF>.<CR><LF>')
392
393
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000394
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000395class SMTPServer(asyncore.dispatcher):
Richard Jones803ef8a2010-07-24 09:51:40 +0000396 # SMTPChannel class to use for managing client connections
397 channel_class = SMTPChannel
398
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000399 def __init__(self, localaddr, remoteaddr):
400 self._localaddr = localaddr
401 self._remoteaddr = remoteaddr
402 asyncore.dispatcher.__init__(self)
Giampaolo Rodolà610aa4f2010-06-30 17:47:39 +0000403 try:
404 self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
405 # try to re-use a server port if possible
406 self.set_reuse_addr()
407 self.bind(localaddr)
408 self.listen(5)
409 except:
410 self.close()
411 raise
412 else:
413 print('%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
414 self.__class__.__name__, time.ctime(time.time()),
415 localaddr, remoteaddr), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000416
417 def handle_accept(self):
418 conn, addr = self.accept()
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000419 print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000420 channel = self.channel_class(self, conn, addr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000421
422 # API for "doing something useful with the message"
423 def process_message(self, peer, mailfrom, rcpttos, data):
424 """Override this abstract method to handle messages from the client.
425
426 peer is a tuple containing (ipaddr, port) of the client that made the
427 socket connection to our smtp port.
428
429 mailfrom is the raw address the client claims the message is coming
430 from.
431
432 rcpttos is a list of raw addresses the client wishes to deliver the
433 message to.
434
435 data is a string containing the entire full text of the message,
436 headers (if supplied) and all. It has been `de-transparencied'
437 according to RFC 821, Section 4.5.2. In other words, a line
438 containing a `.' followed by other text has had the leading dot
439 removed.
440
441 This function should return None, for a normal `250 Ok' response;
442 otherwise it returns the desired response string in RFC 821 format.
443
444 """
Guido van Rossumb8b45ea2001-04-15 13:06:04 +0000445 raise NotImplementedError
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000446
Tim Peters658cba62001-02-09 20:06:00 +0000447
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000448
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000449class DebuggingServer(SMTPServer):
450 # Do something with the gathered message
451 def process_message(self, peer, mailfrom, rcpttos, data):
452 inheaders = 1
453 lines = data.split('\n')
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000454 print('---------- MESSAGE FOLLOWS ----------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000455 for line in lines:
456 # headers first
457 if inheaders and not line:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000458 print('X-Peer:', peer[0])
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000459 inheaders = 0
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000460 print(line)
461 print('------------ END MESSAGE ------------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000462
463
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000464
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000465class PureProxy(SMTPServer):
466 def process_message(self, peer, mailfrom, rcpttos, data):
467 lines = data.split('\n')
468 # Look for the last header
469 i = 0
470 for line in lines:
471 if not line:
472 break
473 i += 1
474 lines.insert(i, 'X-Peer: %s' % peer[0])
475 data = NEWLINE.join(lines)
476 refused = self._deliver(mailfrom, rcpttos, data)
477 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000478 print('we got some refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000479
480 def _deliver(self, mailfrom, rcpttos, data):
481 import smtplib
482 refused = {}
483 try:
484 s = smtplib.SMTP()
485 s.connect(self._remoteaddr[0], self._remoteaddr[1])
486 try:
487 refused = s.sendmail(mailfrom, rcpttos, data)
488 finally:
489 s.quit()
Guido van Rossumb940e112007-01-10 16:19:56 +0000490 except smtplib.SMTPRecipientsRefused as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000491 print('got SMTPRecipientsRefused', file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000492 refused = e.recipients
Guido van Rossumb940e112007-01-10 16:19:56 +0000493 except (socket.error, smtplib.SMTPException) as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000494 print('got', e.__class__, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000495 # All recipients were refused. If the exception had an associated
496 # error code, use it. Otherwise,fake it with a non-triggering
497 # exception code.
498 errcode = getattr(e, 'smtp_code', -1)
499 errmsg = getattr(e, 'smtp_error', 'ignore')
500 for r in rcpttos:
501 refused[r] = (errcode, errmsg)
502 return refused
503
504
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000505
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000506class MailmanProxy(PureProxy):
507 def process_message(self, peer, mailfrom, rcpttos, data):
Guido van Rossum68937b42007-05-18 00:51:22 +0000508 from io import StringIO
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000509 from Mailman import Utils
510 from Mailman import Message
511 from Mailman import MailList
512 # If the message is to a Mailman mailing list, then we'll invoke the
513 # Mailman script directly, without going through the real smtpd.
514 # Otherwise we'll forward it to the local proxy for disposition.
515 listnames = []
516 for rcpt in rcpttos:
517 local = rcpt.lower().split('@')[0]
518 # We allow the following variations on the theme
519 # listname
520 # listname-admin
521 # listname-owner
522 # listname-request
523 # listname-join
524 # listname-leave
525 parts = local.split('-')
526 if len(parts) > 2:
527 continue
528 listname = parts[0]
529 if len(parts) == 2:
530 command = parts[1]
531 else:
532 command = ''
533 if not Utils.list_exists(listname) or command not in (
534 '', 'admin', 'owner', 'request', 'join', 'leave'):
535 continue
536 listnames.append((rcpt, listname, command))
537 # Remove all list recipients from rcpttos and forward what we're not
538 # going to take care of ourselves. Linear removal should be fine
539 # since we don't expect a large number of recipients.
540 for rcpt, listname, command in listnames:
541 rcpttos.remove(rcpt)
Tim Peters658cba62001-02-09 20:06:00 +0000542 # If there's any non-list destined recipients left,
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000543 print('forwarding recips:', ' '.join(rcpttos), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000544 if rcpttos:
545 refused = self._deliver(mailfrom, rcpttos, data)
546 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000547 print('we got refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000548 # Now deliver directly to the list commands
549 mlists = {}
550 s = StringIO(data)
551 msg = Message.Message(s)
552 # These headers are required for the proper execution of Mailman. All
Mark Dickinson934896d2009-02-21 20:59:32 +0000553 # MTAs in existence seem to add these if the original message doesn't
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000554 # have them.
Barry Warsaw820c1202008-06-12 04:06:45 +0000555 if not msg.get('from'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000556 msg['From'] = mailfrom
Barry Warsaw820c1202008-06-12 04:06:45 +0000557 if not msg.get('date'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000558 msg['Date'] = time.ctime(time.time())
559 for rcpt, listname, command in listnames:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000560 print('sending message to', rcpt, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000561 mlist = mlists.get(listname)
562 if not mlist:
563 mlist = MailList.MailList(listname, lock=0)
564 mlists[listname] = mlist
565 # dispatch on the type of command
566 if command == '':
567 # post
568 msg.Enqueue(mlist, tolist=1)
569 elif command == 'admin':
570 msg.Enqueue(mlist, toadmin=1)
571 elif command == 'owner':
572 msg.Enqueue(mlist, toowner=1)
573 elif command == 'request':
574 msg.Enqueue(mlist, torequest=1)
575 elif command in ('join', 'leave'):
576 # TBD: this is a hack!
577 if command == 'join':
578 msg['Subject'] = 'subscribe'
579 else:
580 msg['Subject'] = 'unsubscribe'
581 msg.Enqueue(mlist, torequest=1)
582
583
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000584
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000585class Options:
586 setuid = 1
587 classname = 'PureProxy'
588
589
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000590
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000591def parseargs():
592 global DEBUGSTREAM
593 try:
594 opts, args = getopt.getopt(
595 sys.argv[1:], 'nVhc:d',
596 ['class=', 'nosetuid', 'version', 'help', 'debug'])
Guido van Rossumb940e112007-01-10 16:19:56 +0000597 except getopt.error as e:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000598 usage(1, e)
599
600 options = Options()
601 for opt, arg in opts:
602 if opt in ('-h', '--help'):
603 usage(0)
604 elif opt in ('-V', '--version'):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000605 print(__version__, file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000606 sys.exit(0)
607 elif opt in ('-n', '--nosetuid'):
608 options.setuid = 0
609 elif opt in ('-c', '--class'):
610 options.classname = arg
611 elif opt in ('-d', '--debug'):
612 DEBUGSTREAM = sys.stderr
613
614 # parse the rest of the arguments
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000615 if len(args) < 1:
616 localspec = 'localhost:8025'
617 remotespec = 'localhost:25'
618 elif len(args) < 2:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000619 localspec = args[0]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000620 remotespec = 'localhost:25'
Barry Warsawebf54272001-11-04 03:04:25 +0000621 elif len(args) < 3:
622 localspec = args[0]
623 remotespec = args[1]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000624 else:
625 usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
626
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000627 # split into host/port pairs
628 i = localspec.find(':')
629 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000630 usage(1, 'Bad local spec: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000631 options.localhost = localspec[:i]
632 try:
633 options.localport = int(localspec[i+1:])
634 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000635 usage(1, 'Bad local port: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000636 i = remotespec.find(':')
637 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000638 usage(1, 'Bad remote spec: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000639 options.remotehost = remotespec[:i]
640 try:
641 options.remoteport = int(remotespec[i+1:])
642 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000643 usage(1, 'Bad remote port: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000644 return options
645
646
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000647
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000648if __name__ == '__main__':
649 options = parseargs()
650 # Become nobody
651 if options.setuid:
652 try:
653 import pwd
654 except ImportError:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000655 print('Cannot import module "pwd"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000656 sys.exit(1)
657 nobody = pwd.getpwnam('nobody')[2]
658 try:
659 os.setuid(nobody)
Guido van Rossumb940e112007-01-10 16:19:56 +0000660 except OSError as e:
Guido van Rossum4ba3d652001-03-02 06:42:34 +0000661 if e.errno != errno.EPERM: raise
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000662 print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000663 sys.exit(1)
Skip Montanaro90e01532004-06-26 19:18:49 +0000664 classname = options.classname
665 if "." in classname:
666 lastdot = classname.rfind(".")
667 mod = __import__(classname[:lastdot], globals(), locals(), [""])
668 classname = classname[lastdot+1:]
669 else:
670 import __main__ as mod
Skip Montanaro90e01532004-06-26 19:18:49 +0000671 class_ = getattr(mod, classname)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000672 proxy = class_((options.localhost, options.localport),
673 (options.remotehost, options.remoteport))
674 try:
675 asyncore.loop()
676 except KeyboardInterrupt:
677 pass