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