blob: 8f1a22e937871d691dbe6b6182a4d9e032cd3630 [file] [log] [blame]
Olivier Deprezf4ef2d02021-04-20 13:36:24 +02001#! /usr/bin/env python3
2"""An RFC 5321 smtp proxy with optional RFC 1870 and RFC 6531 extensions.
3
4Usage: %(program)s [options] [localhost:localport [remotehost:remoteport]]
5
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
20 Use `classname' as the concrete SMTP proxy class. Uses `PureProxy' by
21 default.
22
23 --size limit
24 -s limit
25 Restrict the total size of the incoming message to "limit" number of
26 bytes via the RFC 1870 SIZE extension. Defaults to 33554432 bytes.
27
28 --smtputf8
29 -u
30 Enable the SMTPUTF8 extension and behave as an RFC 6531 smtp proxy.
31
32 --debug
33 -d
34 Turn on debugging prints.
35
36 --help
37 -h
38 Print this message and exit.
39
40Version: %(__version__)s
41
42If localhost is not given then `localhost' is used, and if localport is not
43given then 8025 is used. If remotehost is not given then `localhost' is used,
44and if remoteport is not given, then 25 is used.
45"""
46
47# Overview:
48#
49# This file implements the minimal SMTP protocol as defined in RFC 5321. It
50# has a hierarchy of classes which implement the backend functionality for the
51# smtpd. A number of classes are provided:
52#
53# SMTPServer - the base class for the backend. Raises NotImplementedError
54# if you try to use it.
55#
56# DebuggingServer - simply prints each message it receives on stdout.
57#
58# PureProxy - Proxies all messages to a real smtpd which does final
59# delivery. One known problem with this class is that it doesn't handle
60# SMTP errors from the backend server at all. This should be fixed
61# (contributions are welcome!).
62#
63# MailmanProxy - An experimental hack to work with GNU Mailman
64# <www.list.org>. Using this server as your real incoming smtpd, your
65# mailhost will automatically recognize and accept mail destined to Mailman
66# lists when those lists are created. Every message not destined for a list
67# gets forwarded to a real backend smtpd, as with PureProxy. Again, errors
68# are not handled correctly yet.
69#
70#
71# Author: Barry Warsaw <barry@python.org>
72#
73# TODO:
74#
75# - support mailbox delivery
76# - alias files
77# - Handle more ESMTP extensions
78# - handle error codes from the backend smtpd
79
80import sys
81import os
82import errno
83import getopt
84import time
85import socket
86import asyncore
87import asynchat
88import collections
89from warnings import warn
90from email._header_value_parser import get_addr_spec, get_angle_addr
91
92__all__ = [
93 "SMTPChannel", "SMTPServer", "DebuggingServer", "PureProxy",
94 "MailmanProxy",
95]
96
97program = sys.argv[0]
98__version__ = 'Python SMTP proxy version 0.3'
99
100
101class Devnull:
102 def write(self, msg): pass
103 def flush(self): pass
104
105
106DEBUGSTREAM = Devnull()
107NEWLINE = '\n'
108COMMASPACE = ', '
109DATA_SIZE_DEFAULT = 33554432
110
111
112def usage(code, msg=''):
113 print(__doc__ % globals(), file=sys.stderr)
114 if msg:
115 print(msg, file=sys.stderr)
116 sys.exit(code)
117
118
119class SMTPChannel(asynchat.async_chat):
120 COMMAND = 0
121 DATA = 1
122
123 command_size_limit = 512
124 command_size_limits = collections.defaultdict(lambda x=command_size_limit: x)
125
126 @property
127 def max_command_size_limit(self):
128 try:
129 return max(self.command_size_limits.values())
130 except ValueError:
131 return self.command_size_limit
132
133 def __init__(self, server, conn, addr, data_size_limit=DATA_SIZE_DEFAULT,
134 map=None, enable_SMTPUTF8=False, decode_data=False):
135 asynchat.async_chat.__init__(self, conn, map=map)
136 self.smtp_server = server
137 self.conn = conn
138 self.addr = addr
139 self.data_size_limit = data_size_limit
140 self.enable_SMTPUTF8 = enable_SMTPUTF8
141 self._decode_data = decode_data
142 if enable_SMTPUTF8 and decode_data:
143 raise ValueError("decode_data and enable_SMTPUTF8 cannot"
144 " be set to True at the same time")
145 if decode_data:
146 self._emptystring = ''
147 self._linesep = '\r\n'
148 self._dotsep = '.'
149 self._newline = NEWLINE
150 else:
151 self._emptystring = b''
152 self._linesep = b'\r\n'
153 self._dotsep = ord(b'.')
154 self._newline = b'\n'
155 self._set_rset_state()
156 self.seen_greeting = ''
157 self.extended_smtp = False
158 self.command_size_limits.clear()
159 self.fqdn = socket.getfqdn()
160 try:
161 self.peer = conn.getpeername()
162 except OSError as err:
163 # a race condition may occur if the other end is closing
164 # before we can get the peername
165 self.close()
166 if err.args[0] != errno.ENOTCONN:
167 raise
168 return
169 print('Peer:', repr(self.peer), file=DEBUGSTREAM)
170 self.push('220 %s %s' % (self.fqdn, __version__))
171
172 def _set_post_data_state(self):
173 """Reset state variables to their post-DATA state."""
174 self.smtp_state = self.COMMAND
175 self.mailfrom = None
176 self.rcpttos = []
177 self.require_SMTPUTF8 = False
178 self.num_bytes = 0
179 self.set_terminator(b'\r\n')
180
181 def _set_rset_state(self):
182 """Reset all state variables except the greeting."""
183 self._set_post_data_state()
184 self.received_data = ''
185 self.received_lines = []
186
187
188 # properties for backwards-compatibility
189 @property
190 def __server(self):
191 warn("Access to __server attribute on SMTPChannel is deprecated, "
192 "use 'smtp_server' instead", DeprecationWarning, 2)
193 return self.smtp_server
194 @__server.setter
195 def __server(self, value):
196 warn("Setting __server attribute on SMTPChannel is deprecated, "
197 "set 'smtp_server' instead", DeprecationWarning, 2)
198 self.smtp_server = value
199
200 @property
201 def __line(self):
202 warn("Access to __line attribute on SMTPChannel is deprecated, "
203 "use 'received_lines' instead", DeprecationWarning, 2)
204 return self.received_lines
205 @__line.setter
206 def __line(self, value):
207 warn("Setting __line attribute on SMTPChannel is deprecated, "
208 "set 'received_lines' instead", DeprecationWarning, 2)
209 self.received_lines = value
210
211 @property
212 def __state(self):
213 warn("Access to __state attribute on SMTPChannel is deprecated, "
214 "use 'smtp_state' instead", DeprecationWarning, 2)
215 return self.smtp_state
216 @__state.setter
217 def __state(self, value):
218 warn("Setting __state attribute on SMTPChannel is deprecated, "
219 "set 'smtp_state' instead", DeprecationWarning, 2)
220 self.smtp_state = value
221
222 @property
223 def __greeting(self):
224 warn("Access to __greeting attribute on SMTPChannel is deprecated, "
225 "use 'seen_greeting' instead", DeprecationWarning, 2)
226 return self.seen_greeting
227 @__greeting.setter
228 def __greeting(self, value):
229 warn("Setting __greeting attribute on SMTPChannel is deprecated, "
230 "set 'seen_greeting' instead", DeprecationWarning, 2)
231 self.seen_greeting = value
232
233 @property
234 def __mailfrom(self):
235 warn("Access to __mailfrom attribute on SMTPChannel is deprecated, "
236 "use 'mailfrom' instead", DeprecationWarning, 2)
237 return self.mailfrom
238 @__mailfrom.setter
239 def __mailfrom(self, value):
240 warn("Setting __mailfrom attribute on SMTPChannel is deprecated, "
241 "set 'mailfrom' instead", DeprecationWarning, 2)
242 self.mailfrom = value
243
244 @property
245 def __rcpttos(self):
246 warn("Access to __rcpttos attribute on SMTPChannel is deprecated, "
247 "use 'rcpttos' instead", DeprecationWarning, 2)
248 return self.rcpttos
249 @__rcpttos.setter
250 def __rcpttos(self, value):
251 warn("Setting __rcpttos attribute on SMTPChannel is deprecated, "
252 "set 'rcpttos' instead", DeprecationWarning, 2)
253 self.rcpttos = value
254
255 @property
256 def __data(self):
257 warn("Access to __data attribute on SMTPChannel is deprecated, "
258 "use 'received_data' instead", DeprecationWarning, 2)
259 return self.received_data
260 @__data.setter
261 def __data(self, value):
262 warn("Setting __data attribute on SMTPChannel is deprecated, "
263 "set 'received_data' instead", DeprecationWarning, 2)
264 self.received_data = value
265
266 @property
267 def __fqdn(self):
268 warn("Access to __fqdn attribute on SMTPChannel is deprecated, "
269 "use 'fqdn' instead", DeprecationWarning, 2)
270 return self.fqdn
271 @__fqdn.setter
272 def __fqdn(self, value):
273 warn("Setting __fqdn attribute on SMTPChannel is deprecated, "
274 "set 'fqdn' instead", DeprecationWarning, 2)
275 self.fqdn = value
276
277 @property
278 def __peer(self):
279 warn("Access to __peer attribute on SMTPChannel is deprecated, "
280 "use 'peer' instead", DeprecationWarning, 2)
281 return self.peer
282 @__peer.setter
283 def __peer(self, value):
284 warn("Setting __peer attribute on SMTPChannel is deprecated, "
285 "set 'peer' instead", DeprecationWarning, 2)
286 self.peer = value
287
288 @property
289 def __conn(self):
290 warn("Access to __conn attribute on SMTPChannel is deprecated, "
291 "use 'conn' instead", DeprecationWarning, 2)
292 return self.conn
293 @__conn.setter
294 def __conn(self, value):
295 warn("Setting __conn attribute on SMTPChannel is deprecated, "
296 "set 'conn' instead", DeprecationWarning, 2)
297 self.conn = value
298
299 @property
300 def __addr(self):
301 warn("Access to __addr attribute on SMTPChannel is deprecated, "
302 "use 'addr' instead", DeprecationWarning, 2)
303 return self.addr
304 @__addr.setter
305 def __addr(self, value):
306 warn("Setting __addr attribute on SMTPChannel is deprecated, "
307 "set 'addr' instead", DeprecationWarning, 2)
308 self.addr = value
309
310 # Overrides base class for convenience.
311 def push(self, msg):
312 asynchat.async_chat.push(self, bytes(
313 msg + '\r\n', 'utf-8' if self.require_SMTPUTF8 else 'ascii'))
314
315 # Implementation of base class abstract method
316 def collect_incoming_data(self, data):
317 limit = None
318 if self.smtp_state == self.COMMAND:
319 limit = self.max_command_size_limit
320 elif self.smtp_state == self.DATA:
321 limit = self.data_size_limit
322 if limit and self.num_bytes > limit:
323 return
324 elif limit:
325 self.num_bytes += len(data)
326 if self._decode_data:
327 self.received_lines.append(str(data, 'utf-8'))
328 else:
329 self.received_lines.append(data)
330
331 # Implementation of base class abstract method
332 def found_terminator(self):
333 line = self._emptystring.join(self.received_lines)
334 print('Data:', repr(line), file=DEBUGSTREAM)
335 self.received_lines = []
336 if self.smtp_state == self.COMMAND:
337 sz, self.num_bytes = self.num_bytes, 0
338 if not line:
339 self.push('500 Error: bad syntax')
340 return
341 if not self._decode_data:
342 line = str(line, 'utf-8')
343 i = line.find(' ')
344 if i < 0:
345 command = line.upper()
346 arg = None
347 else:
348 command = line[:i].upper()
349 arg = line[i+1:].strip()
350 max_sz = (self.command_size_limits[command]
351 if self.extended_smtp else self.command_size_limit)
352 if sz > max_sz:
353 self.push('500 Error: line too long')
354 return
355 method = getattr(self, 'smtp_' + command, None)
356 if not method:
357 self.push('500 Error: command "%s" not recognized' % command)
358 return
359 method(arg)
360 return
361 else:
362 if self.smtp_state != self.DATA:
363 self.push('451 Internal confusion')
364 self.num_bytes = 0
365 return
366 if self.data_size_limit and self.num_bytes > self.data_size_limit:
367 self.push('552 Error: Too much mail data')
368 self.num_bytes = 0
369 return
370 # Remove extraneous carriage returns and de-transparency according
371 # to RFC 5321, Section 4.5.2.
372 data = []
373 for text in line.split(self._linesep):
374 if text and text[0] == self._dotsep:
375 data.append(text[1:])
376 else:
377 data.append(text)
378 self.received_data = self._newline.join(data)
379 args = (self.peer, self.mailfrom, self.rcpttos, self.received_data)
380 kwargs = {}
381 if not self._decode_data:
382 kwargs = {
383 'mail_options': self.mail_options,
384 'rcpt_options': self.rcpt_options,
385 }
386 status = self.smtp_server.process_message(*args, **kwargs)
387 self._set_post_data_state()
388 if not status:
389 self.push('250 OK')
390 else:
391 self.push(status)
392
393 # SMTP and ESMTP commands
394 def smtp_HELO(self, arg):
395 if not arg:
396 self.push('501 Syntax: HELO hostname')
397 return
398 # See issue #21783 for a discussion of this behavior.
399 if self.seen_greeting:
400 self.push('503 Duplicate HELO/EHLO')
401 return
402 self._set_rset_state()
403 self.seen_greeting = arg
404 self.push('250 %s' % self.fqdn)
405
406 def smtp_EHLO(self, arg):
407 if not arg:
408 self.push('501 Syntax: EHLO hostname')
409 return
410 # See issue #21783 for a discussion of this behavior.
411 if self.seen_greeting:
412 self.push('503 Duplicate HELO/EHLO')
413 return
414 self._set_rset_state()
415 self.seen_greeting = arg
416 self.extended_smtp = True
417 self.push('250-%s' % self.fqdn)
418 if self.data_size_limit:
419 self.push('250-SIZE %s' % self.data_size_limit)
420 self.command_size_limits['MAIL'] += 26
421 if not self._decode_data:
422 self.push('250-8BITMIME')
423 if self.enable_SMTPUTF8:
424 self.push('250-SMTPUTF8')
425 self.command_size_limits['MAIL'] += 10
426 self.push('250 HELP')
427
428 def smtp_NOOP(self, arg):
429 if arg:
430 self.push('501 Syntax: NOOP')
431 else:
432 self.push('250 OK')
433
434 def smtp_QUIT(self, arg):
435 # args is ignored
436 self.push('221 Bye')
437 self.close_when_done()
438
439 def _strip_command_keyword(self, keyword, arg):
440 keylen = len(keyword)
441 if arg[:keylen].upper() == keyword:
442 return arg[keylen:].strip()
443 return ''
444
445 def _getaddr(self, arg):
446 if not arg:
447 return '', ''
448 if arg.lstrip().startswith('<'):
449 address, rest = get_angle_addr(arg)
450 else:
451 address, rest = get_addr_spec(arg)
452 if not address:
453 return address, rest
454 return address.addr_spec, rest
455
456 def _getparams(self, params):
457 # Return params as dictionary. Return None if not all parameters
458 # appear to be syntactically valid according to RFC 1869.
459 result = {}
460 for param in params:
461 param, eq, value = param.partition('=')
462 if not param.isalnum() or eq and not value:
463 return None
464 result[param] = value if eq else True
465 return result
466
467 def smtp_HELP(self, arg):
468 if arg:
469 extended = ' [SP <mail-parameters>]'
470 lc_arg = arg.upper()
471 if lc_arg == 'EHLO':
472 self.push('250 Syntax: EHLO hostname')
473 elif lc_arg == 'HELO':
474 self.push('250 Syntax: HELO hostname')
475 elif lc_arg == 'MAIL':
476 msg = '250 Syntax: MAIL FROM: <address>'
477 if self.extended_smtp:
478 msg += extended
479 self.push(msg)
480 elif lc_arg == 'RCPT':
481 msg = '250 Syntax: RCPT TO: <address>'
482 if self.extended_smtp:
483 msg += extended
484 self.push(msg)
485 elif lc_arg == 'DATA':
486 self.push('250 Syntax: DATA')
487 elif lc_arg == 'RSET':
488 self.push('250 Syntax: RSET')
489 elif lc_arg == 'NOOP':
490 self.push('250 Syntax: NOOP')
491 elif lc_arg == 'QUIT':
492 self.push('250 Syntax: QUIT')
493 elif lc_arg == 'VRFY':
494 self.push('250 Syntax: VRFY <address>')
495 else:
496 self.push('501 Supported commands: EHLO HELO MAIL RCPT '
497 'DATA RSET NOOP QUIT VRFY')
498 else:
499 self.push('250 Supported commands: EHLO HELO MAIL RCPT DATA '
500 'RSET NOOP QUIT VRFY')
501
502 def smtp_VRFY(self, arg):
503 if arg:
504 address, params = self._getaddr(arg)
505 if address:
506 self.push('252 Cannot VRFY user, but will accept message '
507 'and attempt delivery')
508 else:
509 self.push('502 Could not VRFY %s' % arg)
510 else:
511 self.push('501 Syntax: VRFY <address>')
512
513 def smtp_MAIL(self, arg):
514 if not self.seen_greeting:
515 self.push('503 Error: send HELO first')
516 return
517 print('===> MAIL', arg, file=DEBUGSTREAM)
518 syntaxerr = '501 Syntax: MAIL FROM: <address>'
519 if self.extended_smtp:
520 syntaxerr += ' [SP <mail-parameters>]'
521 if arg is None:
522 self.push(syntaxerr)
523 return
524 arg = self._strip_command_keyword('FROM:', arg)
525 address, params = self._getaddr(arg)
526 if not address:
527 self.push(syntaxerr)
528 return
529 if not self.extended_smtp and params:
530 self.push(syntaxerr)
531 return
532 if self.mailfrom:
533 self.push('503 Error: nested MAIL command')
534 return
535 self.mail_options = params.upper().split()
536 params = self._getparams(self.mail_options)
537 if params is None:
538 self.push(syntaxerr)
539 return
540 if not self._decode_data:
541 body = params.pop('BODY', '7BIT')
542 if body not in ['7BIT', '8BITMIME']:
543 self.push('501 Error: BODY can only be one of 7BIT, 8BITMIME')
544 return
545 if self.enable_SMTPUTF8:
546 smtputf8 = params.pop('SMTPUTF8', False)
547 if smtputf8 is True:
548 self.require_SMTPUTF8 = True
549 elif smtputf8 is not False:
550 self.push('501 Error: SMTPUTF8 takes no arguments')
551 return
552 size = params.pop('SIZE', None)
553 if size:
554 if not size.isdigit():
555 self.push(syntaxerr)
556 return
557 elif self.data_size_limit and int(size) > self.data_size_limit:
558 self.push('552 Error: message size exceeds fixed maximum message size')
559 return
560 if len(params.keys()) > 0:
561 self.push('555 MAIL FROM parameters not recognized or not implemented')
562 return
563 self.mailfrom = address
564 print('sender:', self.mailfrom, file=DEBUGSTREAM)
565 self.push('250 OK')
566
567 def smtp_RCPT(self, arg):
568 if not self.seen_greeting:
569 self.push('503 Error: send HELO first');
570 return
571 print('===> RCPT', arg, file=DEBUGSTREAM)
572 if not self.mailfrom:
573 self.push('503 Error: need MAIL command')
574 return
575 syntaxerr = '501 Syntax: RCPT TO: <address>'
576 if self.extended_smtp:
577 syntaxerr += ' [SP <mail-parameters>]'
578 if arg is None:
579 self.push(syntaxerr)
580 return
581 arg = self._strip_command_keyword('TO:', arg)
582 address, params = self._getaddr(arg)
583 if not address:
584 self.push(syntaxerr)
585 return
586 if not self.extended_smtp and params:
587 self.push(syntaxerr)
588 return
589 self.rcpt_options = params.upper().split()
590 params = self._getparams(self.rcpt_options)
591 if params is None:
592 self.push(syntaxerr)
593 return
594 # XXX currently there are no options we recognize.
595 if len(params.keys()) > 0:
596 self.push('555 RCPT TO parameters not recognized or not implemented')
597 return
598 self.rcpttos.append(address)
599 print('recips:', self.rcpttos, file=DEBUGSTREAM)
600 self.push('250 OK')
601
602 def smtp_RSET(self, arg):
603 if arg:
604 self.push('501 Syntax: RSET')
605 return
606 self._set_rset_state()
607 self.push('250 OK')
608
609 def smtp_DATA(self, arg):
610 if not self.seen_greeting:
611 self.push('503 Error: send HELO first');
612 return
613 if not self.rcpttos:
614 self.push('503 Error: need RCPT command')
615 return
616 if arg:
617 self.push('501 Syntax: DATA')
618 return
619 self.smtp_state = self.DATA
620 self.set_terminator(b'\r\n.\r\n')
621 self.push('354 End data with <CR><LF>.<CR><LF>')
622
623 # Commands that have not been implemented
624 def smtp_EXPN(self, arg):
625 self.push('502 EXPN not implemented')
626
627
628class SMTPServer(asyncore.dispatcher):
629 # SMTPChannel class to use for managing client connections
630 channel_class = SMTPChannel
631
632 def __init__(self, localaddr, remoteaddr,
633 data_size_limit=DATA_SIZE_DEFAULT, map=None,
634 enable_SMTPUTF8=False, decode_data=False):
635 self._localaddr = localaddr
636 self._remoteaddr = remoteaddr
637 self.data_size_limit = data_size_limit
638 self.enable_SMTPUTF8 = enable_SMTPUTF8
639 self._decode_data = decode_data
640 if enable_SMTPUTF8 and decode_data:
641 raise ValueError("decode_data and enable_SMTPUTF8 cannot"
642 " be set to True at the same time")
643 asyncore.dispatcher.__init__(self, map=map)
644 try:
645 gai_results = socket.getaddrinfo(*localaddr,
646 type=socket.SOCK_STREAM)
647 self.create_socket(gai_results[0][0], gai_results[0][1])
648 # try to re-use a server port if possible
649 self.set_reuse_addr()
650 self.bind(localaddr)
651 self.listen(5)
652 except:
653 self.close()
654 raise
655 else:
656 print('%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
657 self.__class__.__name__, time.ctime(time.time()),
658 localaddr, remoteaddr), file=DEBUGSTREAM)
659
660 def handle_accepted(self, conn, addr):
661 print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
662 channel = self.channel_class(self,
663 conn,
664 addr,
665 self.data_size_limit,
666 self._map,
667 self.enable_SMTPUTF8,
668 self._decode_data)
669
670 # API for "doing something useful with the message"
671 def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
672 """Override this abstract method to handle messages from the client.
673
674 peer is a tuple containing (ipaddr, port) of the client that made the
675 socket connection to our smtp port.
676
677 mailfrom is the raw address the client claims the message is coming
678 from.
679
680 rcpttos is a list of raw addresses the client wishes to deliver the
681 message to.
682
683 data is a string containing the entire full text of the message,
684 headers (if supplied) and all. It has been `de-transparencied'
685 according to RFC 821, Section 4.5.2. In other words, a line
686 containing a `.' followed by other text has had the leading dot
687 removed.
688
689 kwargs is a dictionary containing additional information. It is
690 empty if decode_data=True was given as init parameter, otherwise
691 it will contain the following keys:
692 'mail_options': list of parameters to the mail command. All
693 elements are uppercase strings. Example:
694 ['BODY=8BITMIME', 'SMTPUTF8'].
695 'rcpt_options': same, for the rcpt command.
696
697 This function should return None for a normal `250 Ok' response;
698 otherwise, it should return the desired response string in RFC 821
699 format.
700
701 """
702 raise NotImplementedError
703
704
705class DebuggingServer(SMTPServer):
706
707 def _print_message_content(self, peer, data):
708 inheaders = 1
709 lines = data.splitlines()
710 for line in lines:
711 # headers first
712 if inheaders and not line:
713 peerheader = 'X-Peer: ' + peer[0]
714 if not isinstance(data, str):
715 # decoded_data=false; make header match other binary output
716 peerheader = repr(peerheader.encode('utf-8'))
717 print(peerheader)
718 inheaders = 0
719 if not isinstance(data, str):
720 # Avoid spurious 'str on bytes instance' warning.
721 line = repr(line)
722 print(line)
723
724 def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
725 print('---------- MESSAGE FOLLOWS ----------')
726 if kwargs:
727 if kwargs.get('mail_options'):
728 print('mail options: %s' % kwargs['mail_options'])
729 if kwargs.get('rcpt_options'):
730 print('rcpt options: %s\n' % kwargs['rcpt_options'])
731 self._print_message_content(peer, data)
732 print('------------ END MESSAGE ------------')
733
734
735class PureProxy(SMTPServer):
736 def __init__(self, *args, **kwargs):
737 if 'enable_SMTPUTF8' in kwargs and kwargs['enable_SMTPUTF8']:
738 raise ValueError("PureProxy does not support SMTPUTF8.")
739 super(PureProxy, self).__init__(*args, **kwargs)
740
741 def process_message(self, peer, mailfrom, rcpttos, data):
742 lines = data.split('\n')
743 # Look for the last header
744 i = 0
745 for line in lines:
746 if not line:
747 break
748 i += 1
749 lines.insert(i, 'X-Peer: %s' % peer[0])
750 data = NEWLINE.join(lines)
751 refused = self._deliver(mailfrom, rcpttos, data)
752 # TBD: what to do with refused addresses?
753 print('we got some refusals:', refused, file=DEBUGSTREAM)
754
755 def _deliver(self, mailfrom, rcpttos, data):
756 import smtplib
757 refused = {}
758 try:
759 s = smtplib.SMTP()
760 s.connect(self._remoteaddr[0], self._remoteaddr[1])
761 try:
762 refused = s.sendmail(mailfrom, rcpttos, data)
763 finally:
764 s.quit()
765 except smtplib.SMTPRecipientsRefused as e:
766 print('got SMTPRecipientsRefused', file=DEBUGSTREAM)
767 refused = e.recipients
768 except (OSError, smtplib.SMTPException) as e:
769 print('got', e.__class__, file=DEBUGSTREAM)
770 # All recipients were refused. If the exception had an associated
771 # error code, use it. Otherwise,fake it with a non-triggering
772 # exception code.
773 errcode = getattr(e, 'smtp_code', -1)
774 errmsg = getattr(e, 'smtp_error', 'ignore')
775 for r in rcpttos:
776 refused[r] = (errcode, errmsg)
777 return refused
778
779
780class MailmanProxy(PureProxy):
781 def __init__(self, *args, **kwargs):
782 warn('MailmanProxy is deprecated and will be removed '
783 'in future', DeprecationWarning, 2)
784 if 'enable_SMTPUTF8' in kwargs and kwargs['enable_SMTPUTF8']:
785 raise ValueError("MailmanProxy does not support SMTPUTF8.")
786 super(PureProxy, self).__init__(*args, **kwargs)
787
788 def process_message(self, peer, mailfrom, rcpttos, data):
789 from io import StringIO
790 from Mailman import Utils
791 from Mailman import Message
792 from Mailman import MailList
793 # If the message is to a Mailman mailing list, then we'll invoke the
794 # Mailman script directly, without going through the real smtpd.
795 # Otherwise we'll forward it to the local proxy for disposition.
796 listnames = []
797 for rcpt in rcpttos:
798 local = rcpt.lower().split('@')[0]
799 # We allow the following variations on the theme
800 # listname
801 # listname-admin
802 # listname-owner
803 # listname-request
804 # listname-join
805 # listname-leave
806 parts = local.split('-')
807 if len(parts) > 2:
808 continue
809 listname = parts[0]
810 if len(parts) == 2:
811 command = parts[1]
812 else:
813 command = ''
814 if not Utils.list_exists(listname) or command not in (
815 '', 'admin', 'owner', 'request', 'join', 'leave'):
816 continue
817 listnames.append((rcpt, listname, command))
818 # Remove all list recipients from rcpttos and forward what we're not
819 # going to take care of ourselves. Linear removal should be fine
820 # since we don't expect a large number of recipients.
821 for rcpt, listname, command in listnames:
822 rcpttos.remove(rcpt)
823 # If there's any non-list destined recipients left,
824 print('forwarding recips:', ' '.join(rcpttos), file=DEBUGSTREAM)
825 if rcpttos:
826 refused = self._deliver(mailfrom, rcpttos, data)
827 # TBD: what to do with refused addresses?
828 print('we got refusals:', refused, file=DEBUGSTREAM)
829 # Now deliver directly to the list commands
830 mlists = {}
831 s = StringIO(data)
832 msg = Message.Message(s)
833 # These headers are required for the proper execution of Mailman. All
834 # MTAs in existence seem to add these if the original message doesn't
835 # have them.
836 if not msg.get('from'):
837 msg['From'] = mailfrom
838 if not msg.get('date'):
839 msg['Date'] = time.ctime(time.time())
840 for rcpt, listname, command in listnames:
841 print('sending message to', rcpt, file=DEBUGSTREAM)
842 mlist = mlists.get(listname)
843 if not mlist:
844 mlist = MailList.MailList(listname, lock=0)
845 mlists[listname] = mlist
846 # dispatch on the type of command
847 if command == '':
848 # post
849 msg.Enqueue(mlist, tolist=1)
850 elif command == 'admin':
851 msg.Enqueue(mlist, toadmin=1)
852 elif command == 'owner':
853 msg.Enqueue(mlist, toowner=1)
854 elif command == 'request':
855 msg.Enqueue(mlist, torequest=1)
856 elif command in ('join', 'leave'):
857 # TBD: this is a hack!
858 if command == 'join':
859 msg['Subject'] = 'subscribe'
860 else:
861 msg['Subject'] = 'unsubscribe'
862 msg.Enqueue(mlist, torequest=1)
863
864
865class Options:
866 setuid = True
867 classname = 'PureProxy'
868 size_limit = None
869 enable_SMTPUTF8 = False
870
871
872def parseargs():
873 global DEBUGSTREAM
874 try:
875 opts, args = getopt.getopt(
876 sys.argv[1:], 'nVhc:s:du',
877 ['class=', 'nosetuid', 'version', 'help', 'size=', 'debug',
878 'smtputf8'])
879 except getopt.error as e:
880 usage(1, e)
881
882 options = Options()
883 for opt, arg in opts:
884 if opt in ('-h', '--help'):
885 usage(0)
886 elif opt in ('-V', '--version'):
887 print(__version__)
888 sys.exit(0)
889 elif opt in ('-n', '--nosetuid'):
890 options.setuid = False
891 elif opt in ('-c', '--class'):
892 options.classname = arg
893 elif opt in ('-d', '--debug'):
894 DEBUGSTREAM = sys.stderr
895 elif opt in ('-u', '--smtputf8'):
896 options.enable_SMTPUTF8 = True
897 elif opt in ('-s', '--size'):
898 try:
899 int_size = int(arg)
900 options.size_limit = int_size
901 except:
902 print('Invalid size: ' + arg, file=sys.stderr)
903 sys.exit(1)
904
905 # parse the rest of the arguments
906 if len(args) < 1:
907 localspec = 'localhost:8025'
908 remotespec = 'localhost:25'
909 elif len(args) < 2:
910 localspec = args[0]
911 remotespec = 'localhost:25'
912 elif len(args) < 3:
913 localspec = args[0]
914 remotespec = args[1]
915 else:
916 usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
917
918 # split into host/port pairs
919 i = localspec.find(':')
920 if i < 0:
921 usage(1, 'Bad local spec: %s' % localspec)
922 options.localhost = localspec[:i]
923 try:
924 options.localport = int(localspec[i+1:])
925 except ValueError:
926 usage(1, 'Bad local port: %s' % localspec)
927 i = remotespec.find(':')
928 if i < 0:
929 usage(1, 'Bad remote spec: %s' % remotespec)
930 options.remotehost = remotespec[:i]
931 try:
932 options.remoteport = int(remotespec[i+1:])
933 except ValueError:
934 usage(1, 'Bad remote port: %s' % remotespec)
935 return options
936
937
938if __name__ == '__main__':
939 options = parseargs()
940 # Become nobody
941 classname = options.classname
942 if "." in classname:
943 lastdot = classname.rfind(".")
944 mod = __import__(classname[:lastdot], globals(), locals(), [""])
945 classname = classname[lastdot+1:]
946 else:
947 import __main__ as mod
948 class_ = getattr(mod, classname)
949 proxy = class_((options.localhost, options.localport),
950 (options.remotehost, options.remoteport),
951 options.size_limit, enable_SMTPUTF8=options.enable_SMTPUTF8)
952 if options.setuid:
953 try:
954 import pwd
955 except ImportError:
956 print('Cannot import module "pwd"; try running with -n option.', file=sys.stderr)
957 sys.exit(1)
958 nobody = pwd.getpwnam('nobody')[2]
959 try:
960 os.setuid(nobody)
961 except PermissionError:
962 print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr)
963 sys.exit(1)
964 try:
965 asyncore.loop()
966 except KeyboardInterrupt:
967 pass