ยปCore Development>Code coverage>Lib/smtpd.py

Python code coverage for Lib/smtpd.py

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