blob: 748fcaefeb4285b6daaf22d121b78fdee8cf8841 [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, "
Florent Xicluna67317752011-12-10 11:07:42 +0100145 "use 'smtp_server' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000146 return self.smtp_server
147 @__server.setter
148 def __server(self, value):
149 warn("Setting __server attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100150 "set 'smtp_server' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000151 self.smtp_server = value
152
153 @property
154 def __line(self):
155 warn("Access to __line attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100156 "use 'received_lines' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000157 return self.received_lines
158 @__line.setter
159 def __line(self, value):
160 warn("Setting __line attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100161 "set 'received_lines' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000162 self.received_lines = value
163
164 @property
165 def __state(self):
166 warn("Access to __state attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100167 "use 'smtp_state' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000168 return self.smtp_state
169 @__state.setter
170 def __state(self, value):
171 warn("Setting __state attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100172 "set 'smtp_state' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000173 self.smtp_state = value
174
175 @property
176 def __greeting(self):
177 warn("Access to __greeting attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100178 "use 'seen_greeting' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000179 return self.seen_greeting
180 @__greeting.setter
181 def __greeting(self, value):
182 warn("Setting __greeting attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100183 "set 'seen_greeting' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000184 self.seen_greeting = value
185
186 @property
187 def __mailfrom(self):
188 warn("Access to __mailfrom attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100189 "use 'mailfrom' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000190 return self.mailfrom
191 @__mailfrom.setter
192 def __mailfrom(self, value):
193 warn("Setting __mailfrom attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100194 "set 'mailfrom' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000195 self.mailfrom = value
196
197 @property
198 def __rcpttos(self):
199 warn("Access to __rcpttos attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100200 "use 'rcpttos' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000201 return self.rcpttos
202 @__rcpttos.setter
203 def __rcpttos(self, value):
204 warn("Setting __rcpttos attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100205 "set 'rcpttos' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000206 self.rcpttos = value
207
208 @property
209 def __data(self):
210 warn("Access to __data attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100211 "use 'received_data' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000212 return self.received_data
213 @__data.setter
214 def __data(self, value):
215 warn("Setting __data attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100216 "set 'received_data' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000217 self.received_data = value
218
219 @property
220 def __fqdn(self):
221 warn("Access to __fqdn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100222 "use 'fqdn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000223 return self.fqdn
224 @__fqdn.setter
225 def __fqdn(self, value):
226 warn("Setting __fqdn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100227 "set 'fqdn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000228 self.fqdn = value
229
230 @property
231 def __peer(self):
232 warn("Access to __peer attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100233 "use 'peer' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000234 return self.peer
235 @__peer.setter
236 def __peer(self, value):
237 warn("Setting __peer attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100238 "set 'peer' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000239 self.peer = value
240
241 @property
242 def __conn(self):
243 warn("Access to __conn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100244 "use 'conn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000245 return self.conn
246 @__conn.setter
247 def __conn(self, value):
248 warn("Setting __conn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100249 "set 'conn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000250 self.conn = value
251
252 @property
253 def __addr(self):
254 warn("Access to __addr attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100255 "use 'addr' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000256 return self.addr
257 @__addr.setter
258 def __addr(self, value):
259 warn("Setting __addr attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100260 "set 'addr' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000261 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):
R David Murray669b7552012-03-20 16:16:29 -0400377 if not self.seen_greeting:
378 self.push('503 Error: send HELO first');
379 return
380
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000381 print('===> MAIL', arg, file=DEBUGSTREAM)
Guido van Rossum8ce8a782007-11-01 19:42:39 +0000382 address = self.__getaddr('FROM:', arg) if arg else None
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000383 if not address:
384 self.push('501 Syntax: MAIL FROM:<address>')
385 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000386 if self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000387 self.push('503 Error: nested MAIL command')
388 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000389 self.mailfrom = address
390 print('sender:', self.mailfrom, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000391 self.push('250 Ok')
392
393 def smtp_RCPT(self, arg):
R David Murray669b7552012-03-20 16:16:29 -0400394 if not self.seen_greeting:
395 self.push('503 Error: send HELO first');
396 return
397
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000398 print('===> RCPT', arg, file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000399 if not self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000400 self.push('503 Error: need MAIL command')
401 return
Guido van Rossum8ce8a782007-11-01 19:42:39 +0000402 address = self.__getaddr('TO:', arg) if arg else None
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000403 if not address:
404 self.push('501 Syntax: RCPT TO: <address>')
405 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000406 self.rcpttos.append(address)
407 print('recips:', self.rcpttos, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000408 self.push('250 Ok')
409
410 def smtp_RSET(self, arg):
411 if arg:
412 self.push('501 Syntax: RSET')
413 return
414 # Resets the sender, recipients, and data, but not the greeting
Richard Jones803ef8a2010-07-24 09:51:40 +0000415 self.mailfrom = None
416 self.rcpttos = []
417 self.received_data = ''
418 self.smtp_state = self.COMMAND
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000419 self.push('250 Ok')
420
421 def smtp_DATA(self, arg):
R David Murray669b7552012-03-20 16:16:29 -0400422 if not self.seen_greeting:
423 self.push('503 Error: send HELO first');
424 return
425
Richard Jones803ef8a2010-07-24 09:51:40 +0000426 if not self.rcpttos:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000427 self.push('503 Error: need RCPT command')
428 return
429 if arg:
430 self.push('501 Syntax: DATA')
431 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000432 self.smtp_state = self.DATA
Josiah Carlsond74900e2008-07-07 04:15:08 +0000433 self.set_terminator(b'\r\n.\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000434 self.push('354 End data with <CR><LF>.<CR><LF>')
435
436
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000437
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000438class SMTPServer(asyncore.dispatcher):
Richard Jones803ef8a2010-07-24 09:51:40 +0000439 # SMTPChannel class to use for managing client connections
440 channel_class = SMTPChannel
441
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000442 def __init__(self, localaddr, remoteaddr):
443 self._localaddr = localaddr
444 self._remoteaddr = remoteaddr
445 asyncore.dispatcher.__init__(self)
Giampaolo RodolĂ 610aa4f2010-06-30 17:47:39 +0000446 try:
447 self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
448 # try to re-use a server port if possible
449 self.set_reuse_addr()
450 self.bind(localaddr)
451 self.listen(5)
452 except:
453 self.close()
454 raise
455 else:
456 print('%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
457 self.__class__.__name__, time.ctime(time.time()),
458 localaddr, remoteaddr), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000459
Giampaolo RodolĂ 977c7072010-10-04 21:08:36 +0000460 def handle_accepted(self, conn, addr):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000461 print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000462 channel = self.channel_class(self, conn, addr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000463
464 # API for "doing something useful with the message"
465 def process_message(self, peer, mailfrom, rcpttos, data):
466 """Override this abstract method to handle messages from the client.
467
468 peer is a tuple containing (ipaddr, port) of the client that made the
469 socket connection to our smtp port.
470
471 mailfrom is the raw address the client claims the message is coming
472 from.
473
474 rcpttos is a list of raw addresses the client wishes to deliver the
475 message to.
476
477 data is a string containing the entire full text of the message,
478 headers (if supplied) and all. It has been `de-transparencied'
479 according to RFC 821, Section 4.5.2. In other words, a line
480 containing a `.' followed by other text has had the leading dot
481 removed.
482
483 This function should return None, for a normal `250 Ok' response;
484 otherwise it returns the desired response string in RFC 821 format.
485
486 """
Guido van Rossumb8b45ea2001-04-15 13:06:04 +0000487 raise NotImplementedError
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000488
Tim Peters658cba62001-02-09 20:06:00 +0000489
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000490
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000491class DebuggingServer(SMTPServer):
492 # Do something with the gathered message
493 def process_message(self, peer, mailfrom, rcpttos, data):
494 inheaders = 1
495 lines = data.split('\n')
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000496 print('---------- MESSAGE FOLLOWS ----------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000497 for line in lines:
498 # headers first
499 if inheaders and not line:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000500 print('X-Peer:', peer[0])
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000501 inheaders = 0
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000502 print(line)
503 print('------------ END MESSAGE ------------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000504
505
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000506
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000507class PureProxy(SMTPServer):
508 def process_message(self, peer, mailfrom, rcpttos, data):
509 lines = data.split('\n')
510 # Look for the last header
511 i = 0
512 for line in lines:
513 if not line:
514 break
515 i += 1
516 lines.insert(i, 'X-Peer: %s' % peer[0])
517 data = NEWLINE.join(lines)
518 refused = self._deliver(mailfrom, rcpttos, data)
519 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000520 print('we got some refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000521
522 def _deliver(self, mailfrom, rcpttos, data):
523 import smtplib
524 refused = {}
525 try:
526 s = smtplib.SMTP()
527 s.connect(self._remoteaddr[0], self._remoteaddr[1])
528 try:
529 refused = s.sendmail(mailfrom, rcpttos, data)
530 finally:
531 s.quit()
Guido van Rossumb940e112007-01-10 16:19:56 +0000532 except smtplib.SMTPRecipientsRefused as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000533 print('got SMTPRecipientsRefused', file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000534 refused = e.recipients
Guido van Rossumb940e112007-01-10 16:19:56 +0000535 except (socket.error, smtplib.SMTPException) as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000536 print('got', e.__class__, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000537 # All recipients were refused. If the exception had an associated
538 # error code, use it. Otherwise,fake it with a non-triggering
539 # exception code.
540 errcode = getattr(e, 'smtp_code', -1)
541 errmsg = getattr(e, 'smtp_error', 'ignore')
542 for r in rcpttos:
543 refused[r] = (errcode, errmsg)
544 return refused
545
546
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000547
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000548class MailmanProxy(PureProxy):
549 def process_message(self, peer, mailfrom, rcpttos, data):
Guido van Rossum68937b42007-05-18 00:51:22 +0000550 from io import StringIO
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000551 from Mailman import Utils
552 from Mailman import Message
553 from Mailman import MailList
554 # If the message is to a Mailman mailing list, then we'll invoke the
555 # Mailman script directly, without going through the real smtpd.
556 # Otherwise we'll forward it to the local proxy for disposition.
557 listnames = []
558 for rcpt in rcpttos:
559 local = rcpt.lower().split('@')[0]
560 # We allow the following variations on the theme
561 # listname
562 # listname-admin
563 # listname-owner
564 # listname-request
565 # listname-join
566 # listname-leave
567 parts = local.split('-')
568 if len(parts) > 2:
569 continue
570 listname = parts[0]
571 if len(parts) == 2:
572 command = parts[1]
573 else:
574 command = ''
575 if not Utils.list_exists(listname) or command not in (
576 '', 'admin', 'owner', 'request', 'join', 'leave'):
577 continue
578 listnames.append((rcpt, listname, command))
579 # Remove all list recipients from rcpttos and forward what we're not
580 # going to take care of ourselves. Linear removal should be fine
581 # since we don't expect a large number of recipients.
582 for rcpt, listname, command in listnames:
583 rcpttos.remove(rcpt)
Tim Peters658cba62001-02-09 20:06:00 +0000584 # If there's any non-list destined recipients left,
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000585 print('forwarding recips:', ' '.join(rcpttos), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000586 if rcpttos:
587 refused = self._deliver(mailfrom, rcpttos, data)
588 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000589 print('we got refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000590 # Now deliver directly to the list commands
591 mlists = {}
592 s = StringIO(data)
593 msg = Message.Message(s)
594 # These headers are required for the proper execution of Mailman. All
Mark Dickinson934896d2009-02-21 20:59:32 +0000595 # MTAs in existence seem to add these if the original message doesn't
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000596 # have them.
Barry Warsaw820c1202008-06-12 04:06:45 +0000597 if not msg.get('from'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000598 msg['From'] = mailfrom
Barry Warsaw820c1202008-06-12 04:06:45 +0000599 if not msg.get('date'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000600 msg['Date'] = time.ctime(time.time())
601 for rcpt, listname, command in listnames:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000602 print('sending message to', rcpt, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000603 mlist = mlists.get(listname)
604 if not mlist:
605 mlist = MailList.MailList(listname, lock=0)
606 mlists[listname] = mlist
607 # dispatch on the type of command
608 if command == '':
609 # post
610 msg.Enqueue(mlist, tolist=1)
611 elif command == 'admin':
612 msg.Enqueue(mlist, toadmin=1)
613 elif command == 'owner':
614 msg.Enqueue(mlist, toowner=1)
615 elif command == 'request':
616 msg.Enqueue(mlist, torequest=1)
617 elif command in ('join', 'leave'):
618 # TBD: this is a hack!
619 if command == 'join':
620 msg['Subject'] = 'subscribe'
621 else:
622 msg['Subject'] = 'unsubscribe'
623 msg.Enqueue(mlist, torequest=1)
624
625
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000626
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000627class Options:
628 setuid = 1
629 classname = 'PureProxy'
630
631
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000632
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000633def parseargs():
634 global DEBUGSTREAM
635 try:
636 opts, args = getopt.getopt(
637 sys.argv[1:], 'nVhc:d',
638 ['class=', 'nosetuid', 'version', 'help', 'debug'])
Guido van Rossumb940e112007-01-10 16:19:56 +0000639 except getopt.error as e:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000640 usage(1, e)
641
642 options = Options()
643 for opt, arg in opts:
644 if opt in ('-h', '--help'):
645 usage(0)
646 elif opt in ('-V', '--version'):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000647 print(__version__, file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000648 sys.exit(0)
649 elif opt in ('-n', '--nosetuid'):
650 options.setuid = 0
651 elif opt in ('-c', '--class'):
652 options.classname = arg
653 elif opt in ('-d', '--debug'):
654 DEBUGSTREAM = sys.stderr
655
656 # parse the rest of the arguments
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000657 if len(args) < 1:
658 localspec = 'localhost:8025'
659 remotespec = 'localhost:25'
660 elif len(args) < 2:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000661 localspec = args[0]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000662 remotespec = 'localhost:25'
Barry Warsawebf54272001-11-04 03:04:25 +0000663 elif len(args) < 3:
664 localspec = args[0]
665 remotespec = args[1]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000666 else:
667 usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
668
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000669 # split into host/port pairs
670 i = localspec.find(':')
671 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000672 usage(1, 'Bad local spec: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000673 options.localhost = localspec[:i]
674 try:
675 options.localport = int(localspec[i+1:])
676 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000677 usage(1, 'Bad local port: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000678 i = remotespec.find(':')
679 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000680 usage(1, 'Bad remote spec: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000681 options.remotehost = remotespec[:i]
682 try:
683 options.remoteport = int(remotespec[i+1:])
684 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000685 usage(1, 'Bad remote port: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000686 return options
687
688
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000689
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000690if __name__ == '__main__':
691 options = parseargs()
692 # Become nobody
Florent Xicluna711f87c2011-10-20 23:03:43 +0200693 classname = options.classname
694 if "." in classname:
695 lastdot = classname.rfind(".")
696 mod = __import__(classname[:lastdot], globals(), locals(), [""])
697 classname = classname[lastdot+1:]
698 else:
699 import __main__ as mod
700 class_ = getattr(mod, classname)
701 proxy = class_((options.localhost, options.localport),
702 (options.remotehost, options.remoteport))
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000703 if options.setuid:
704 try:
705 import pwd
706 except ImportError:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000707 print('Cannot import module "pwd"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000708 sys.exit(1)
709 nobody = pwd.getpwnam('nobody')[2]
710 try:
711 os.setuid(nobody)
Guido van Rossumb940e112007-01-10 16:19:56 +0000712 except OSError as e:
Guido van Rossum4ba3d652001-03-02 06:42:34 +0000713 if e.errno != errno.EPERM: raise
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000714 print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000715 sys.exit(1)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000716 try:
717 asyncore.loop()
718 except KeyboardInterrupt:
719 pass