diff -r c43362d35d8d Doc/library/smtplib.rst --- a/Doc/library/smtplib.rst Fri Jul 04 17:00:25 2014 -0700 +++ b/Doc/library/smtplib.rst Mon Jul 21 23:24:10 2014 +0200 @@ -161,11 +161,18 @@ The server refused our ``HELO`` message. +.. exception:: SMTPNotSupportedError + + The command or option attempted is not supported by the server. + + .. exception:: SMTPAuthenticationError SMTP authentication went wrong. Most probably the server didn't accept the username/password combination provided. +.. versionadded:: 3.5 + The :exc:`SMTPNotSupportedError` exception. .. seealso:: @@ -287,13 +294,16 @@ :exc:`SMTPAuthenticationError` The server didn't accept the username/password combination. - :exc:`SMTPException` + :exc:`SMTPNotSupportedError` No suitable authentication method was found. Each of the authentication methods supported by :mod:`smtplib` are tried in turn if they are advertised as supported by the server (see :meth:`auth` for a list of supported authentication methods). + .. versionchanged:: 3.5 + :exc:`SMTPNotSupportedError` is used instead of :exc:`SMTPException`. + .. method:: SMTP.auth(mechanism, authobject) @@ -345,7 +355,7 @@ :exc:`SMTPHeloError` The server didn't reply properly to the ``HELO`` greeting. - :exc:`SMTPException` + :exc:`SMTPNotSupportedError` The server does not support the STARTTLS extension. :exc:`RuntimeError` @@ -359,6 +369,9 @@ :attr:`SSLContext.check_hostname` and *Server Name Indicator* (see :data:`~ssl.HAS_SNI`). + .. versionchanged:: 3.5 + :exc:`SMTPNotSupportedError` is used instead of :exc:`SMTPException`. + .. method:: SMTP.sendmail(from_addr, to_addrs, msg, mail_options=[], rcpt_options=[]) @@ -413,12 +426,19 @@ The server replied with an unexpected error code (other than a refusal of a recipient). + :exc:`SMTPNotSupportedError` + ``SMTPUTF8`` was given in the *mail_options* but is not supported by the + server. + Unless otherwise noted, the connection will be open even after an exception is raised. .. versionchanged:: 3.2 *msg* may be a byte string. + .. versionchanged:: 3.5 + A :exc:`SMTPNotSupportedError` may be raised. + .. method:: SMTP.send_message(msg, from_addr=None, to_addrs=None, \ mail_options=[], rcpt_options=[]) @@ -461,6 +481,9 @@ Normally these do not need to be called directly, so they are not documented here. For details, consult the module code. +.. versionadded:: 3.5 + The SMTPUTF8 extension (:RFC:`6531`) is now supported. + .. _smtp-example: diff -r c43362d35d8d Lib/smtplib.py --- a/Lib/smtplib.py Fri Jul 04 17:00:25 2014 -0700 +++ b/Lib/smtplib.py Mon Jul 21 23:24:10 2014 +0200 @@ -70,6 +70,13 @@ class SMTPException(OSError): """Base class for all exceptions raised by this module.""" +class SMTPNotSupportedError(SMTPException): + """The command or option is not supported by the SMTP server. + + This exception is raised when an attempt is made to run a command or a + command with an option which is not supported by the server. + """ + class SMTPServerDisconnected(SMTPException): """Not connected to any SMTP server. @@ -236,6 +243,7 @@ self._host = host self.timeout = timeout self.esmtp_features = {} + self.encoding = 'ascii' self.source_address = source_address if host: @@ -331,7 +339,7 @@ print('send:', repr(s), file=stderr) if hasattr(self, 'sock') and self.sock: if isinstance(s, str): - s = s.encode("ascii") + s = s.encode(self.encoding) try: self.sock.sendall(s) except OSError: @@ -434,7 +442,7 @@ self.does_esmtp = 1 #parse the ehlo response -ddm assert isinstance(self.ehlo_resp, bytes), repr(self.ehlo_resp) - resp = self.ehlo_resp.decode("latin-1").split('\n') + resp = self.ehlo_resp.decode("utf-8").split('\n') del resp[0] for each in resp: # To be able to communicate with as many SMTP servers as possible, @@ -499,7 +507,14 @@ """SMTP 'mail' command -- begins mail xfer session.""" optionlist = '' if options and self.does_esmtp: - optionlist = ' ' + ' '.join(options) + optionlist = ' ' + ' '.join(options).upper() + if 'SMTPUTF8' in options and self.does_esmtp: + if self.has_extn('smtputf8'): + self.encoding = 'utf-8' + else: + raise SMTPNotSupportedError('SMTPUTF8 not supported by server') + else: + self.encoding = 'us-ascii' self.putcmd("mail", "FROM:%s%s" % (quoteaddr(sender), optionlist)) return self.getreply() @@ -529,7 +544,7 @@ raise SMTPDataError(code, repl) else: if isinstance(msg, str): - msg = _fix_eols(msg).encode('ascii') + msg = _fix_eols(msg).encode(self.encoding) q = _quote_periods(msg) if q[-2:] != bCRLF: q = q + bCRLF @@ -643,7 +658,8 @@ self.ehlo_or_helo_if_needed() if not self.has_extn("auth"): - raise SMTPException("SMTP AUTH extension not supported by server.") + raise SMTPNotSupportedError( + "SMTP AUTH extension not supported by server.") # Authentication methods the server claims to support advertised_authlist = self.esmtp_features["auth"].split() @@ -695,7 +711,8 @@ """ self.ehlo_or_helo_if_needed() if not self.has_extn("starttls"): - raise SMTPException("STARTTLS extension not supported by server.") + raise SMTPNotSupportedError( + "STARTTLS extension not supported by server.") (resp, reply) = self.docmd("STARTTLS") if resp == 220: if not _have_ssl: @@ -786,15 +803,19 @@ """ self.ehlo_or_helo_if_needed() esmtp_opts = [] + if self.does_esmtp: + for mail_option in mail_options: + esmtp_opts.append(mail_option.lower()) + if 'smtputf8' in esmtp_opts: + if self.has_extn('smtputf8'): + self.encoding = 'utf-8' + else: + raise SMTPNotSupportedError( + 'SMTPUTF8 not supported by server') if isinstance(msg, str): - msg = _fix_eols(msg).encode('ascii') - if self.does_esmtp: - # Hmmm? what's this? -ddm - # self.esmtp_features['7bit']="" - if self.has_extn('size'): - esmtp_opts.append("size=%d" % len(msg)) - for option in mail_options: - esmtp_opts.append(option) + msg = _fix_eols(msg).encode(self.encoding) + if self.does_esmtp and self.has_extn('size'): + esmtp_opts.insert(0, "size=%d" % len(msg)) (code, resp) = self.mail(from_addr, esmtp_opts) if code != 250: if code == 421: diff -r c43362d35d8d Lib/test/test_smtplib.py --- a/Lib/test/test_smtplib.py Fri Jul 04 17:00:25 2014 -0700 +++ b/Lib/test/test_smtplib.py Mon Jul 21 23:24:10 2014 +0200 @@ -310,6 +310,13 @@ mexpect = '%s%s\n%s' % (MSG_BEGIN, m.decode('ascii'), MSG_END) self.assertEqual(self.output.getvalue(), mexpect) + self.client_evt.set() + self.serv_evt.wait() + self.output.flush() + mexpect = '%s%s\n%s' % (MSG_BEGIN, m.decode('ascii'), MSG_END) + self.assertEqual(self.output.getvalue(), mexpect) + + def testSendNeedingDotQuote(self): # Issue 12283 m = '.A test\n.mes.sage.' @@ -628,13 +635,17 @@ self._extrafeatures = ''.join( [ "250-{0}\r\n".format(x) for x in extra_features ]) super(SimSMTPChannel, self).__init__(*args, **kw) + self.enable_SMTPUTF8 = True def smtp_EHLO(self, arg): - resp = ('250-testhost\r\n' - '250-EXPN\r\n' - '250-SIZE 20000000\r\n' - '250-STARTTLS\r\n' - '250-DELIVERBY\r\n') + resp = ( + '250-testhost\r\n' + '250-EXPN\r\n' + '250-SIZE 20000000\r\n' + '250-STARTTLS\r\n' + '250-DELIVERBY\r\n' + '250-SMTPUTF8\r\n' + '250-8BITMIME\r\n') resp = resp + self._extrafeatures + '250 HELP' self.push(resp) self.seen_greeting = arg @@ -726,6 +737,9 @@ def process_message(self, peer, mailfrom, rcpttos, data): pass + def process_smtputf8_message(self, peer, mailfrom, rcpttos, data): + pass + def add_feature(self, feature): self._extra_features.append(feature) @@ -775,12 +789,15 @@ self.assertEqual(smtp.esmtp_features, {}) # features expected from the test server - expected_features = {'expn':'', - 'size': '20000000', - 'starttls': '', - 'deliverby': '', - 'help': '', - } + expected_features = { + 'expn':'', + 'size': '20000000', + 'starttls': '', + 'deliverby': '', + 'help': '', + 'smtputf8': '', + '8bitmime': '', + } smtp.ehlo() self.assertEqual(smtp.esmtp_features, expected_features) @@ -789,6 +806,47 @@ self.assertFalse(smtp.has_extn('unsupported-feature')) smtp.quit() + def test_send_unicode_without_SMTPUTF8(self): + m = '¡a test message containing unicode!' + smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3) + self.assertRaises(UnicodeEncodeError, smtp.sendmail, 'Alice', 'Bob', m) + + def test_send_unicode_with_SMTPUTF8(self): + m = '¡a test message containing unicode!' + smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3) + smtp.ehlo() + self.assertTrue(smtp.does_esmtp) + self.assertTrue(smtp.has_extn('smtputf8')) + smtp.sendmail('John', 'Sally', m, + mail_options=['BODY=8BITMIME', 'SMTPUTF8']) + + def test_smtpexception_raised_on_unssuported_smtputf8(self): + m = '¡a test message containing unicode!' + class ChannelClass(SimSMTPChannel): + def smtp_EHLO(self, arg): + resp = ( + '250-testhost\r\n' + '250-EXPN\r\n' + '250-SIZE 20000000\r\n' + '250-STARTTLS\r\n' + '250-DELIVERBY\r\n') + resp = resp + self._extrafeatures + '250 HELP' + self.push(resp) + self.seen_greeting = arg + self.extended_smtp = True + self.serv.channel_class = ChannelClass + smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3) + smtp.ehlo() + self.assertTrue(smtp.does_esmtp) + self.assertFalse(smtp.has_extn('smtputf8')) + self.assertRaises( + smtplib.SMTPException, + smtp.sendmail, + 'John', 'Sally', m, mail_options=['BODY=8BITMIME', 'SMTPUTF8']) + self.assertRaises( + smtplib.SMTPException, + smtp.mail, 'John', options=['BODY=8BITMIME', 'SMTPUTF8']) + def testVRFY(self): smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)