blob: e8459f09209e5e9b3deb36fb3fb281573a2b0c30 [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
112 def __init__(self, server, conn, addr):
113 asynchat.async_chat.__init__(self, conn)
Richard Jones803ef8a2010-07-24 09:51:40 +0000114 self.smtp_server = server
115 self.conn = conn
116 self.addr = addr
117 self.received_lines = []
118 self.smtp_state = self.COMMAND
119 self.seen_greeting = ''
120 self.mailfrom = None
121 self.rcpttos = []
122 self.received_data = ''
123 self.fqdn = socket.getfqdn()
124 self.peer = conn.getpeername()
125 print('Peer:', repr(self.peer), file=DEBUGSTREAM)
126 self.push('220 %s %s' % (self.fqdn, __version__))
Josiah Carlsond74900e2008-07-07 04:15:08 +0000127 self.set_terminator(b'\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000128
Richard Jones803ef8a2010-07-24 09:51:40 +0000129 # properties for backwards-compatibility
130 @property
131 def __server(self):
132 warn("Access to __server attribute on SMTPChannel is deprecated, "
133 "use 'smtp_server' instead", PendingDeprecationWarning, 2)
134 return self.smtp_server
135 @__server.setter
136 def __server(self, value):
137 warn("Setting __server attribute on SMTPChannel is deprecated, "
138 "set 'smtp_server' instead", PendingDeprecationWarning, 2)
139 self.smtp_server = value
140
141 @property
142 def __line(self):
143 warn("Access to __line attribute on SMTPChannel is deprecated, "
144 "use 'received_lines' instead", PendingDeprecationWarning, 2)
145 return self.received_lines
146 @__line.setter
147 def __line(self, value):
148 warn("Setting __line attribute on SMTPChannel is deprecated, "
149 "set 'received_lines' instead", PendingDeprecationWarning, 2)
150 self.received_lines = value
151
152 @property
153 def __state(self):
154 warn("Access to __state attribute on SMTPChannel is deprecated, "
155 "use 'smtp_state' instead", PendingDeprecationWarning, 2)
156 return self.smtp_state
157 @__state.setter
158 def __state(self, value):
159 warn("Setting __state attribute on SMTPChannel is deprecated, "
160 "set 'smtp_state' instead", PendingDeprecationWarning, 2)
161 self.smtp_state = value
162
163 @property
164 def __greeting(self):
165 warn("Access to __greeting attribute on SMTPChannel is deprecated, "
166 "use 'seen_greeting' instead", PendingDeprecationWarning, 2)
167 return self.seen_greeting
168 @__greeting.setter
169 def __greeting(self, value):
170 warn("Setting __greeting attribute on SMTPChannel is deprecated, "
171 "set 'seen_greeting' instead", PendingDeprecationWarning, 2)
172 self.seen_greeting = value
173
174 @property
175 def __mailfrom(self):
176 warn("Access to __mailfrom attribute on SMTPChannel is deprecated, "
177 "use 'mailfrom' instead", PendingDeprecationWarning, 2)
178 return self.mailfrom
179 @__mailfrom.setter
180 def __mailfrom(self, value):
181 warn("Setting __mailfrom attribute on SMTPChannel is deprecated, "
182 "set 'mailfrom' instead", PendingDeprecationWarning, 2)
183 self.mailfrom = value
184
185 @property
186 def __rcpttos(self):
187 warn("Access to __rcpttos attribute on SMTPChannel is deprecated, "
188 "use 'rcpttos' instead", PendingDeprecationWarning, 2)
189 return self.rcpttos
190 @__rcpttos.setter
191 def __rcpttos(self, value):
192 warn("Setting __rcpttos attribute on SMTPChannel is deprecated, "
193 "set 'rcpttos' instead", PendingDeprecationWarning, 2)
194 self.rcpttos = value
195
196 @property
197 def __data(self):
198 warn("Access to __data attribute on SMTPChannel is deprecated, "
199 "use 'received_data' instead", PendingDeprecationWarning, 2)
200 return self.received_data
201 @__data.setter
202 def __data(self, value):
203 warn("Setting __data attribute on SMTPChannel is deprecated, "
204 "set 'received_data' instead", PendingDeprecationWarning, 2)
205 self.received_data = value
206
207 @property
208 def __fqdn(self):
209 warn("Access to __fqdn attribute on SMTPChannel is deprecated, "
210 "use 'fqdn' instead", PendingDeprecationWarning, 2)
211 return self.fqdn
212 @__fqdn.setter
213 def __fqdn(self, value):
214 warn("Setting __fqdn attribute on SMTPChannel is deprecated, "
215 "set 'fqdn' instead", PendingDeprecationWarning, 2)
216 self.fqdn = value
217
218 @property
219 def __peer(self):
220 warn("Access to __peer attribute on SMTPChannel is deprecated, "
221 "use 'peer' instead", PendingDeprecationWarning, 2)
222 return self.peer
223 @__peer.setter
224 def __peer(self, value):
225 warn("Setting __peer attribute on SMTPChannel is deprecated, "
226 "set 'peer' instead", PendingDeprecationWarning, 2)
227 self.peer = value
228
229 @property
230 def __conn(self):
231 warn("Access to __conn attribute on SMTPChannel is deprecated, "
232 "use 'conn' instead", PendingDeprecationWarning, 2)
233 return self.conn
234 @__conn.setter
235 def __conn(self, value):
236 warn("Setting __conn attribute on SMTPChannel is deprecated, "
237 "set 'conn' instead", PendingDeprecationWarning, 2)
238 self.conn = value
239
240 @property
241 def __addr(self):
242 warn("Access to __addr attribute on SMTPChannel is deprecated, "
243 "use 'addr' instead", PendingDeprecationWarning, 2)
244 return self.addr
245 @__addr.setter
246 def __addr(self, value):
247 warn("Setting __addr attribute on SMTPChannel is deprecated, "
248 "set 'addr' instead", PendingDeprecationWarning, 2)
249 self.addr = value
250
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000251 # Overrides base class for convenience
252 def push(self, msg):
Josiah Carlsond74900e2008-07-07 04:15:08 +0000253 asynchat.async_chat.push(self, bytes(msg + '\r\n', 'ascii'))
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000254
255 # Implementation of base class abstract method
256 def collect_incoming_data(self, data):
Richard Jones803ef8a2010-07-24 09:51:40 +0000257 self.received_lines.append(str(data, "utf8"))
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000258
259 # Implementation of base class abstract method
260 def found_terminator(self):
Richard Jones803ef8a2010-07-24 09:51:40 +0000261 line = EMPTYSTRING.join(self.received_lines)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000262 print('Data:', repr(line), file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000263 self.received_lines = []
264 if self.smtp_state == self.COMMAND:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000265 if not line:
266 self.push('500 Error: bad syntax')
267 return
268 method = None
269 i = line.find(' ')
270 if i < 0:
271 command = line.upper()
272 arg = None
273 else:
274 command = line[:i].upper()
275 arg = line[i+1:].strip()
276 method = getattr(self, 'smtp_' + command, None)
277 if not method:
278 self.push('502 Error: command "%s" not implemented' % command)
279 return
280 method(arg)
281 return
282 else:
Richard Jones803ef8a2010-07-24 09:51:40 +0000283 if self.smtp_state != self.DATA:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000284 self.push('451 Internal confusion')
285 return
286 # Remove extraneous carriage returns and de-transparency according
287 # to RFC 821, Section 4.5.2.
288 data = []
289 for text in line.split('\r\n'):
290 if text and text[0] == '.':
291 data.append(text[1:])
292 else:
293 data.append(text)
Richard Jones803ef8a2010-07-24 09:51:40 +0000294 self.received_data = NEWLINE.join(data)
Georg Brandl17e3d692010-07-31 10:08:09 +0000295 status = self.smtp_server.process_message(self.peer,
296 self.mailfrom,
297 self.rcpttos,
298 self.received_data)
Richard Jones803ef8a2010-07-24 09:51:40 +0000299 self.rcpttos = []
300 self.mailfrom = None
301 self.smtp_state = self.COMMAND
Josiah Carlsond74900e2008-07-07 04:15:08 +0000302 self.set_terminator(b'\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000303 if not status:
304 self.push('250 Ok')
305 else:
306 self.push(status)
307
308 # SMTP and ESMTP commands
309 def smtp_HELO(self, arg):
310 if not arg:
311 self.push('501 Syntax: HELO hostname')
312 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000313 if self.seen_greeting:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000314 self.push('503 Duplicate HELO/EHLO')
315 else:
Richard Jones803ef8a2010-07-24 09:51:40 +0000316 self.seen_greeting = arg
317 self.push('250 %s' % self.fqdn)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000318
319 def smtp_NOOP(self, arg):
320 if arg:
321 self.push('501 Syntax: NOOP')
322 else:
323 self.push('250 Ok')
324
325 def smtp_QUIT(self, arg):
326 # args is ignored
327 self.push('221 Bye')
328 self.close_when_done()
329
330 # factored
331 def __getaddr(self, keyword, arg):
332 address = None
333 keylen = len(keyword)
334 if arg[:keylen].upper() == keyword:
335 address = arg[keylen:].strip()
Barry Warsawebf54272001-11-04 03:04:25 +0000336 if not address:
337 pass
338 elif address[0] == '<' and address[-1] == '>' and address != '<>':
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000339 # Addresses can be in the form <person@dom.com> but watch out
340 # for null address, e.g. <>
341 address = address[1:-1]
342 return address
343
344 def smtp_MAIL(self, arg):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000345 print('===> MAIL', arg, file=DEBUGSTREAM)
Guido van Rossum8ce8a782007-11-01 19:42:39 +0000346 address = self.__getaddr('FROM:', arg) if arg else None
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000347 if not address:
348 self.push('501 Syntax: MAIL FROM:<address>')
349 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000350 if self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000351 self.push('503 Error: nested MAIL command')
352 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000353 self.mailfrom = address
354 print('sender:', self.mailfrom, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000355 self.push('250 Ok')
356
357 def smtp_RCPT(self, arg):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000358 print('===> RCPT', arg, file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000359 if not self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000360 self.push('503 Error: need MAIL command')
361 return
Guido van Rossum8ce8a782007-11-01 19:42:39 +0000362 address = self.__getaddr('TO:', arg) if arg else None
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000363 if not address:
364 self.push('501 Syntax: RCPT TO: <address>')
365 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000366 self.rcpttos.append(address)
367 print('recips:', self.rcpttos, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000368 self.push('250 Ok')
369
370 def smtp_RSET(self, arg):
371 if arg:
372 self.push('501 Syntax: RSET')
373 return
374 # Resets the sender, recipients, and data, but not the greeting
Richard Jones803ef8a2010-07-24 09:51:40 +0000375 self.mailfrom = None
376 self.rcpttos = []
377 self.received_data = ''
378 self.smtp_state = self.COMMAND
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000379 self.push('250 Ok')
380
381 def smtp_DATA(self, arg):
Richard Jones803ef8a2010-07-24 09:51:40 +0000382 if not self.rcpttos:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000383 self.push('503 Error: need RCPT command')
384 return
385 if arg:
386 self.push('501 Syntax: DATA')
387 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000388 self.smtp_state = self.DATA
Josiah Carlsond74900e2008-07-07 04:15:08 +0000389 self.set_terminator(b'\r\n.\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000390 self.push('354 End data with <CR><LF>.<CR><LF>')
391
392
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000393
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000394class SMTPServer(asyncore.dispatcher):
Richard Jones803ef8a2010-07-24 09:51:40 +0000395 # SMTPChannel class to use for managing client connections
396 channel_class = SMTPChannel
397
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000398 def __init__(self, localaddr, remoteaddr):
399 self._localaddr = localaddr
400 self._remoteaddr = remoteaddr
401 asyncore.dispatcher.__init__(self)
Giampaolo Rodolà610aa4f2010-06-30 17:47:39 +0000402 try:
403 self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
404 # try to re-use a server port if possible
405 self.set_reuse_addr()
406 self.bind(localaddr)
407 self.listen(5)
408 except:
409 self.close()
410 raise
411 else:
412 print('%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
413 self.__class__.__name__, time.ctime(time.time()),
414 localaddr, remoteaddr), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000415
Giampaolo Rodolà5c8c9a22010-08-21 18:35:05 +0000416 def handle_accept(self)
417 try:
418 conn, addr = self.accept()
419 except TypeError:
420 # sometimes accept() might return None
421 return
422 except socket.error, err:
423 # ECONNABORTED might be thrown
424 if err[0] != errno.ECONNABORTED:
425 raise
426 return
427 else:
428 # sometimes addr == None instead of (ip, port)
429 if addr == None:
430 return
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000431 print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000432 channel = self.channel_class(self, conn, addr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000433
434 # API for "doing something useful with the message"
435 def process_message(self, peer, mailfrom, rcpttos, data):
436 """Override this abstract method to handle messages from the client.
437
438 peer is a tuple containing (ipaddr, port) of the client that made the
439 socket connection to our smtp port.
440
441 mailfrom is the raw address the client claims the message is coming
442 from.
443
444 rcpttos is a list of raw addresses the client wishes to deliver the
445 message to.
446
447 data is a string containing the entire full text of the message,
448 headers (if supplied) and all. It has been `de-transparencied'
449 according to RFC 821, Section 4.5.2. In other words, a line
450 containing a `.' followed by other text has had the leading dot
451 removed.
452
453 This function should return None, for a normal `250 Ok' response;
454 otherwise it returns the desired response string in RFC 821 format.
455
456 """
Guido van Rossumb8b45ea2001-04-15 13:06:04 +0000457 raise NotImplementedError
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000458
Tim Peters658cba62001-02-09 20:06:00 +0000459
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000460
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000461class DebuggingServer(SMTPServer):
462 # Do something with the gathered message
463 def process_message(self, peer, mailfrom, rcpttos, data):
464 inheaders = 1
465 lines = data.split('\n')
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000466 print('---------- MESSAGE FOLLOWS ----------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000467 for line in lines:
468 # headers first
469 if inheaders and not line:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000470 print('X-Peer:', peer[0])
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000471 inheaders = 0
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000472 print(line)
473 print('------------ END MESSAGE ------------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000474
475
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000476
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000477class PureProxy(SMTPServer):
478 def process_message(self, peer, mailfrom, rcpttos, data):
479 lines = data.split('\n')
480 # Look for the last header
481 i = 0
482 for line in lines:
483 if not line:
484 break
485 i += 1
486 lines.insert(i, 'X-Peer: %s' % peer[0])
487 data = NEWLINE.join(lines)
488 refused = self._deliver(mailfrom, rcpttos, data)
489 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000490 print('we got some refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000491
492 def _deliver(self, mailfrom, rcpttos, data):
493 import smtplib
494 refused = {}
495 try:
496 s = smtplib.SMTP()
497 s.connect(self._remoteaddr[0], self._remoteaddr[1])
498 try:
499 refused = s.sendmail(mailfrom, rcpttos, data)
500 finally:
501 s.quit()
Guido van Rossumb940e112007-01-10 16:19:56 +0000502 except smtplib.SMTPRecipientsRefused as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000503 print('got SMTPRecipientsRefused', file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000504 refused = e.recipients
Guido van Rossumb940e112007-01-10 16:19:56 +0000505 except (socket.error, smtplib.SMTPException) as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000506 print('got', e.__class__, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000507 # All recipients were refused. If the exception had an associated
508 # error code, use it. Otherwise,fake it with a non-triggering
509 # exception code.
510 errcode = getattr(e, 'smtp_code', -1)
511 errmsg = getattr(e, 'smtp_error', 'ignore')
512 for r in rcpttos:
513 refused[r] = (errcode, errmsg)
514 return refused
515
516
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000517
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000518class MailmanProxy(PureProxy):
519 def process_message(self, peer, mailfrom, rcpttos, data):
Guido van Rossum68937b42007-05-18 00:51:22 +0000520 from io import StringIO
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000521 from Mailman import Utils
522 from Mailman import Message
523 from Mailman import MailList
524 # If the message is to a Mailman mailing list, then we'll invoke the
525 # Mailman script directly, without going through the real smtpd.
526 # Otherwise we'll forward it to the local proxy for disposition.
527 listnames = []
528 for rcpt in rcpttos:
529 local = rcpt.lower().split('@')[0]
530 # We allow the following variations on the theme
531 # listname
532 # listname-admin
533 # listname-owner
534 # listname-request
535 # listname-join
536 # listname-leave
537 parts = local.split('-')
538 if len(parts) > 2:
539 continue
540 listname = parts[0]
541 if len(parts) == 2:
542 command = parts[1]
543 else:
544 command = ''
545 if not Utils.list_exists(listname) or command not in (
546 '', 'admin', 'owner', 'request', 'join', 'leave'):
547 continue
548 listnames.append((rcpt, listname, command))
549 # Remove all list recipients from rcpttos and forward what we're not
550 # going to take care of ourselves. Linear removal should be fine
551 # since we don't expect a large number of recipients.
552 for rcpt, listname, command in listnames:
553 rcpttos.remove(rcpt)
Tim Peters658cba62001-02-09 20:06:00 +0000554 # If there's any non-list destined recipients left,
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000555 print('forwarding recips:', ' '.join(rcpttos), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000556 if rcpttos:
557 refused = self._deliver(mailfrom, rcpttos, data)
558 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000559 print('we got refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000560 # Now deliver directly to the list commands
561 mlists = {}
562 s = StringIO(data)
563 msg = Message.Message(s)
564 # These headers are required for the proper execution of Mailman. All
Mark Dickinson934896d2009-02-21 20:59:32 +0000565 # MTAs in existence seem to add these if the original message doesn't
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000566 # have them.
Barry Warsaw820c1202008-06-12 04:06:45 +0000567 if not msg.get('from'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000568 msg['From'] = mailfrom
Barry Warsaw820c1202008-06-12 04:06:45 +0000569 if not msg.get('date'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000570 msg['Date'] = time.ctime(time.time())
571 for rcpt, listname, command in listnames:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000572 print('sending message to', rcpt, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000573 mlist = mlists.get(listname)
574 if not mlist:
575 mlist = MailList.MailList(listname, lock=0)
576 mlists[listname] = mlist
577 # dispatch on the type of command
578 if command == '':
579 # post
580 msg.Enqueue(mlist, tolist=1)
581 elif command == 'admin':
582 msg.Enqueue(mlist, toadmin=1)
583 elif command == 'owner':
584 msg.Enqueue(mlist, toowner=1)
585 elif command == 'request':
586 msg.Enqueue(mlist, torequest=1)
587 elif command in ('join', 'leave'):
588 # TBD: this is a hack!
589 if command == 'join':
590 msg['Subject'] = 'subscribe'
591 else:
592 msg['Subject'] = 'unsubscribe'
593 msg.Enqueue(mlist, torequest=1)
594
595
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000596
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000597class Options:
598 setuid = 1
599 classname = 'PureProxy'
600
601
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000602
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000603def parseargs():
604 global DEBUGSTREAM
605 try:
606 opts, args = getopt.getopt(
607 sys.argv[1:], 'nVhc:d',
608 ['class=', 'nosetuid', 'version', 'help', 'debug'])
Guido van Rossumb940e112007-01-10 16:19:56 +0000609 except getopt.error as e:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000610 usage(1, e)
611
612 options = Options()
613 for opt, arg in opts:
614 if opt in ('-h', '--help'):
615 usage(0)
616 elif opt in ('-V', '--version'):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000617 print(__version__, file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000618 sys.exit(0)
619 elif opt in ('-n', '--nosetuid'):
620 options.setuid = 0
621 elif opt in ('-c', '--class'):
622 options.classname = arg
623 elif opt in ('-d', '--debug'):
624 DEBUGSTREAM = sys.stderr
625
626 # parse the rest of the arguments
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000627 if len(args) < 1:
628 localspec = 'localhost:8025'
629 remotespec = 'localhost:25'
630 elif len(args) < 2:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000631 localspec = args[0]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000632 remotespec = 'localhost:25'
Barry Warsawebf54272001-11-04 03:04:25 +0000633 elif len(args) < 3:
634 localspec = args[0]
635 remotespec = args[1]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000636 else:
637 usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
638
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000639 # split into host/port pairs
640 i = localspec.find(':')
641 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000642 usage(1, 'Bad local spec: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000643 options.localhost = localspec[:i]
644 try:
645 options.localport = int(localspec[i+1:])
646 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000647 usage(1, 'Bad local port: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000648 i = remotespec.find(':')
649 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000650 usage(1, 'Bad remote spec: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000651 options.remotehost = remotespec[:i]
652 try:
653 options.remoteport = int(remotespec[i+1:])
654 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000655 usage(1, 'Bad remote port: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000656 return options
657
658
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000659
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000660if __name__ == '__main__':
661 options = parseargs()
662 # Become nobody
663 if options.setuid:
664 try:
665 import pwd
666 except ImportError:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000667 print('Cannot import module "pwd"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000668 sys.exit(1)
669 nobody = pwd.getpwnam('nobody')[2]
670 try:
671 os.setuid(nobody)
Guido van Rossumb940e112007-01-10 16:19:56 +0000672 except OSError as e:
Guido van Rossum4ba3d652001-03-02 06:42:34 +0000673 if e.errno != errno.EPERM: raise
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000674 print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000675 sys.exit(1)
Skip Montanaro90e01532004-06-26 19:18:49 +0000676 classname = options.classname
677 if "." in classname:
678 lastdot = classname.rfind(".")
679 mod = __import__(classname[:lastdot], globals(), locals(), [""])
680 classname = classname[lastdot+1:]
681 else:
682 import __main__ as mod
Skip Montanaro90e01532004-06-26 19:18:49 +0000683 class_ = getattr(mod, classname)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000684 proxy = class_((options.localhost, options.localport),
685 (options.remotehost, options.remoteport))
686 try:
687 asyncore.loop()
688 except KeyboardInterrupt:
689 pass