diff -r ca696ca204e0 Doc/library/smtpd.rst --- a/Doc/library/smtpd.rst Sat Aug 09 16:40:49 2014 -0400 +++ b/Doc/library/smtpd.rst Tue Aug 12 20:10:38 2014 +0200 @@ -42,9 +42,11 @@ *enable_SMTPUTF8* determins whether the ``SMTPUTF8`` extension (as defined in :RFC:`6531`) should be enabled. The default is ``False``. If - *enable_SMTPUTF* is set to ``True``, the :meth:`process_smtputf8_message` - method must be defined. A :exc:`ValueError` is raised if both - *enable_SMTPUTF8* and *decode_data* are set to ``True`` at the same time. + set to ``True``, ``SMTPUTF8`` and ``BODY=8BITMIME`` are accepted as + parameters to the ``MAIL`` command and are passed to + :meth:`process_message` as elements of ``kwargs['mail_options']`` if given. + A :exc:`ValueError` is raised if both *enable_SMTPUTF8* and *decode_data* + are set to ``True`` at the same time. A dictionary can be specified in *map* to avoid using a global socket map. @@ -52,8 +54,11 @@ should be decoded using UTF-8. The default is ``True`` for backward compatibility reasons, but will change to ``False`` in Python 3.6. Specify the keyword value explicitly to avoid the :exc:`DeprecationWarning`. + When *decode_data* is set to ``True`` the server accepts ``BODY=8BITMIME`` + as parameter to the ``MAIL`` command and passes it to + :meth:`process_message` as element of ``kwargs['mail_options']`` if given. - .. method:: process_message(peer, mailfrom, rcpttos, data) + .. method:: process_message(peer, mailfrom, rcpttos, data, \*\*kwargs) Raise a :exc:`NotImplementedError` exception. Override this in subclasses to do something useful with this message. Whatever was passed in the @@ -67,20 +72,17 @@ argument will be a unicode string. If it is set to ``False``, it will be a bytes object. + *kwargs* is a dictionary containing additional information. It is empty + unless ``decode_data=False`` or ``enable_SMTPUTF8=True`` was given as + init parameter. Otherwise it contains the following keys: + *mail_options*: a list of all received parameters to the ``MAIL`` + command (the elements are uppercase strings; example: + ``['BODY=8BITMIME', 'SMTPUTF8']``). + *rcpt_options*: same as *mail_options* but for the ``RCPT`` command. + Return ``None`` to request a normal ``250 Ok`` response; otherwise return the desired response string in :RFC:`5321` format. - .. method:: process_smtputf8_message(peer, mailfrom, rcpttos, data) - - Raise a :exc:`NotImplementedError` exception. Override this in - subclasses to do something useful with messages when *enable_SMTPUTF8* - has been set to ``True`` and the SMTP client requested ``SMTPUTF8``, - since this method is called rather than :meth:`process_message` when the - client actively requests ``SMTPUTF8``. The *data* argument will always - be a bytes object, and any non-``None`` return value should conform to - :rfc:`6531`; otherwise, the API is the same as for - :meth:`process_message`. - .. attribute:: channel_class Override this in subclasses to use a custom :class:`SMTPChannel` for diff -r ca696ca204e0 Lib/smtpd.py --- a/Lib/smtpd.py Sat Aug 09 16:40:49 2014 -0400 +++ b/Lib/smtpd.py Tue Aug 12 20:10:38 2014 +0200 @@ -147,11 +147,13 @@ decode_data = True self._decode_data = decode_data if decode_data: + self.enable_8BITMIME = False self._emptystring = '' self._linesep = '\r\n' self._dotsep = '.' self._newline = NEWLINE else: + self.enable_8BITMIME = True self._emptystring = b'' self._linesep = b'\r\n' self._dotsep = b'.' @@ -381,10 +383,11 @@ data.append(text) self.received_data = self._newline.join(data) args = (self.peer, self.mailfrom, self.rcpttos, self.received_data) - if self.require_SMTPUTF8: - status = self.smtp_server.process_smtputf8_message(*args) - else: - status = self.smtp_server.process_message(*args) + kwargs = { + 'mail_options': self.mail_options, + 'rcpt_options': self.rcpt_options, + } if not self._decode_data else {} + status = self.smtp_server.process_message(*args, **kwargs) self._set_post_data_state() if not status: self.push('250 OK') @@ -419,8 +422,9 @@ if self.data_size_limit: self.push('250-SIZE %s' % self.data_size_limit) self.command_size_limits['MAIL'] += 26 + if self.enable_8BITMIME: + self.push('250-8BITMIME') if self.enable_SMTPUTF8: - self.push('250-8BITMIME') self.push('250-SMTPUTF8') self.command_size_limits['MAIL'] += 10 self.push('250 HELP') @@ -453,11 +457,11 @@ return address, rest return address.addr_spec, rest - def _getparams(self, params): + def _getparams(self, param_list): # Return any parameters that appear to be syntactically valid according # to RFC 1869, ignore all others. (Postel rule: accept what we can.) params = [param.split('=', 1) if '=' in param else (param, True) - for param in params.split()] + for param in param_list] return {k: v for k, v in params if k.isalnum()} def smtp_HELP(self, arg): @@ -508,7 +512,7 @@ def smtp_MAIL(self, arg): if not self.seen_greeting: - self.push('503 Error: send HELO first'); + self.push('503 Error: send HELO first') return print('===> MAIL', arg, file=DEBUGSTREAM) syntaxerr = '501 Syntax: MAIL FROM:
' @@ -531,19 +535,20 @@ if self.mailfrom: self.push('503 Error: nested MAIL command') return - params = self._getparams(params.upper()) - if params is None: + self.mail_options = params.upper().split() + params_dict = self._getparams(self.mail_options) + if params_dict is None: self.push(syntaxerr) return - body = params.pop('BODY', '7BIT') - if self.enable_SMTPUTF8 and params.pop('SMTPUTF8', False): - if body != '8BITMIME': - self.push('501 Syntax: MAIL FROM:
' - ' [BODY=8BITMIME SMTPUTF8]') - return + body = params_dict.pop('BODY', '7BIT') + if body != '7BIT' and (self._decode_data or body != '8BITMIME'): + params_dict.update({'BODY': body}) + if params_dict.pop('SMTPUTF8', False): + if self.enable_SMTPUTF8: + self.require_SMTPUTF8 = True else: - self.require_SMTPUTF8 = True - size = params.pop('SIZE', None) + params_dict.update({'SMTPUTF8': True}) + size = params_dict.pop('SIZE', None) if size: if not size.isdigit(): self.push(syntaxerr) @@ -551,7 +556,7 @@ elif self.data_size_limit and int(size) > self.data_size_limit: self.push('552 Error: message size exceeds fixed maximum message size') return - if len(params.keys()) > 0: + if len(params_dict.keys()) > 0: self.push('555 MAIL FROM parameters not recognized or not implemented') return self.mailfrom = address @@ -579,13 +584,16 @@ return if params: if self.extended_smtp: - params = self._getparams(params.upper()) + self.rcpt_options = params.upper().split() + params = self._getparams(self.rcpt_options) if params is None: self.push(syntaxerr) return else: self.push(syntaxerr) return + else: + self.rcpt_options = [] if not address: self.push(syntaxerr) return @@ -676,7 +684,7 @@ self._decode_data) # API for "doing something useful with the message" - def process_message(self, peer, mailfrom, rcpttos, data): + def process_message(self, peer, mailfrom, rcpttos, data, **kwargs): """Override this abstract method to handle messages from the client. peer is a tuple containing (ipaddr, port) of the client that made the @@ -694,6 +702,14 @@ containing a `.' followed by other text has had the leading dot removed. + kwargs is a dictionary containing additional information. It is empty + unless decode_data=False or enable_SMTPUTF8=True was given as init + parameter. + It may contain the following keys: + - 'mail_options': list of parameters to the mail command. All + elements are strings. Example: ['BODY=8BITMIME', 'SMTPUTF8']. + - 'rcpt_options': same for the rcpt command. + This function should return None for a normal `250 Ok' response; otherwise, it should return the desired response string in RFC 821 format. @@ -701,19 +717,6 @@ """ raise NotImplementedError - # API for processing messeges needing Unicode support (RFC 6531, RFC 6532). - def process_smtputf8_message(self, peer, mailfrom, rcpttos, data): - """Same as ``process_message`` but for messages for which the client - has sent the SMTPUTF8 parameter with the MAIL command (see the - enable_SMTPUTF8 parameter of the constructor). - - This function should return None for a normal `250 Ok' response; - otherwise, it should return the desired response string in RFC 6531 - format. - - """ - raise NotImplementedError - class DebuggingServer(SMTPServer): @@ -734,13 +737,13 @@ line = repr(line) print(line) - def process_message(self, peer, mailfrom, rcpttos, data): + def process_message(self, peer, mailfrom, rcpttos, data, **kwargs): print('---------- MESSAGE FOLLOWS ----------') - self._print_message_content(peer, data) - print('------------ END MESSAGE ------------') - - def process_smtputf8_message(self, peer, mailfrom, rcpttos, data): - print('----- SMTPUTF8 MESSAGE FOLLOWS ------') + if kwargs: + if kwargs.get('mail_options'): + print('mail options: %s' % kwargs['mail_options']) + if kwargs.get('rcpt_options'): + print('rcpt options: %s\n' % kwargs['rcpt_options']) self._print_message_content(peer, data) print('------------ END MESSAGE ------------') diff -r ca696ca204e0 Lib/test/test_smtpd.py --- a/Lib/test/test_smtpd.py Sat Aug 09 16:40:49 2014 -0400 +++ b/Lib/test/test_smtpd.py Tue Aug 12 20:10:38 2014 +0200 @@ -16,13 +16,12 @@ else: self.return_status = b'return status' - def process_message(self, peer, mailfrom, rcpttos, data): + def process_message(self, peer, mailfrom, rcpttos, data, **kw): self.messages.append((peer, mailfrom, rcpttos, data)) if data == self.return_status: return '250 Okish' - - def process_smtputf8_message(self, *args, **kwargs): - return '250 SMTPUTF8 message okish' + if 'mail_options' in kw and 'SMTPUTF8' in kw['mail_options']: + return '250 SMTPUTF8 message okish' class DummyDispatcherBroken(Exception): @@ -54,22 +53,6 @@ write_line(b'DATA') self.assertRaises(NotImplementedError, write_line, b'spam\r\n.\r\n') - def test_process_smtputf8_message_unimplemented(self): - server = smtpd.SMTPServer((support.HOST, 0), ('b', 0), - enable_SMTPUTF8=True) - conn, addr = server.accept() - channel = smtpd.SMTPChannel(server, conn, addr, enable_SMTPUTF8=True) - - def write_line(line): - channel.socket.queue_recv(line) - channel.handle_read() - - write_line(b'EHLO example') - write_line(b'MAIL From: BODY=8BITMIME SMTPUTF8') - write_line(b'RCPT To: ') - write_line(b'DATA') - self.assertRaises(NotImplementedError, write_line, b'spam\r\n.\r\n') - def test_decode_data_default_warns(self): with self.assertWarns(DeprecationWarning): smtpd.SMTPServer((support.HOST, 0), ('b', 0)) @@ -168,7 +151,8 @@ enable_SMTPUTF8=True) stdout = s.getvalue() self.assertEqual(stdout, textwrap.dedent("""\ - ----- SMTPUTF8 MESSAGE FOLLOWS ------ + ---------- MESSAGE FOLLOWS ---------- + mail options: ['BODY=8BITMIME', 'SMTPUTF8'] b'From: test' b'X-Peer: peer-address' b'' @@ -201,6 +185,72 @@ self.assertEqual(server.socket.family, socket.AF_INET) +class TestMailOptionParsing(unittest.TestCase): + error_response = (b'555 MAIL FROM parameters not recognized or not ' + b'implemented\r\n') + + def setUp(self): + smtpd.socket = asyncore.socket = mock_socket + self.old_debugstream = smtpd.DEBUGSTREAM + self.debug = smtpd.DEBUGSTREAM = io.StringIO() + + def write_line(self, channel, line): + channel.socket.queue_recv(line) + channel.handle_read() + + def test_with_decode_data_true(self): + server = DummyServer((support.HOST, 0), ('b', 0), decode_data=True) + conn, addr = server.accept() + channel = smtpd.SMTPChannel(server, conn, addr, decode_data=True) + self.write_line(channel, b'EHLO example') + for line in [ + b'MAIL from: size=20 SMTPUTF8', + b'MAIL from: size=20 SMTPUTF8 BODY=8BITMIME', + b'MAIL from: size=20 BODY=UNKNOWN', + b'MAIL from: size=20 body=8bitmime', + ]: + self.write_line(channel, line) + self.assertEqual(channel.socket.last, self.error_response) + self.write_line( + channel, b'MAIL from: size=20') + self.assertEqual(channel.socket.last, b'250 OK\r\n') + + def test_with_decode_data_false(self): + server = DummyServer((support.HOST, 0), ('b', 0), decode_data=False) + conn, addr = server.accept() + channel = smtpd.SMTPChannel(server, conn, addr, decode_data=False) + self.write_line(channel, b'EHLO example') + for line in [ + b'MAIL from: size=20 SMTPUTF8', + b'MAIL from: size=20 SMTPUTF8 BODY=8BITMIME', + b'MAIL from: size=20 BODY=UNKNOWN', + ]: + self.write_line(channel, line) + self.assertEqual(channel.socket.last, self.error_response) + self.write_line( + channel, b'MAIL from: size=20 body=8bitmime') + self.assertEqual(channel.socket.last, b'250 OK\r\n') + + def test_with_enable_smtputf8_true(self): + server = DummyServer((support.HOST, 0), ('b', 0), enable_SMTPUTF8=True) + conn, addr = server.accept() + channel = smtpd.SMTPChannel(server, conn, addr, enable_SMTPUTF8=True) + self.write_line(channel, b'EHLO example') + self.write_line( + channel, + b'MAIL from: size=20 SMTPUTF8 BODY=UNKNOWN') + self.assertEqual(channel.socket.last, self.error_response) + self.write_line( + channel, + b'MAIL from: size=20 body=8bitmime smtputf8') + self.assertEqual(channel.socket.last, b'250 OK\r\n') + + def tearDown(self): + asyncore.close_all() + asyncore.socket = smtpd.socket = socket + smtpd.DEBUGSTREAM = self.old_debugstream + + class SMTPDChannelTest(unittest.TestCase): def setUp(self): smtpd.socket = asyncore.socket = mock_socket