Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 76e13c1

Browse files
committed
#15014: Add 'auth' command to implement auth mechanisms and use it in login.
Patch by Milan Oberkirch.
1 parent d8b129f commit 76e13c1

5 files changed

Lines changed: 154 additions & 68 deletions

File tree

Doc/library/smtplib.rst

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,8 +240,7 @@ An :class:`SMTP` instance has the following methods:
240240
the server is stored as the :attr:`ehlo_resp` attribute, :attr:`does_esmtp`
241241
is set to true or false depending on whether the server supports ESMTP, and
242242
:attr:`esmtp_features` will be a dictionary containing the names of the
243-
SMTP service extensions this server supports, and their
244-
parameters (if any).
243+
SMTP service extensions this server supports, and their parameters (if any).
245244

246245
Unless you wish to use :meth:`has_extn` before sending mail, it should not be
247246
necessary to call this method explicitly. It will be implicitly called by
@@ -291,6 +290,42 @@ An :class:`SMTP` instance has the following methods:
291290
:exc:`SMTPException`
292291
No suitable authentication method was found.
293292

293+
Each of the authentication methods supported by :mod:`smtplib` are tried in
294+
turn if they are advertised as supported by the server (see :meth:`auth`
295+
for a list of supported authentication methods).
296+
297+
298+
.. method:: SMTP.auth(mechanism, authobject)
299+
300+
Issue an ``SMTP`` ``AUTH`` command for the specified authentication
301+
*mechanism*, and handle the challenge response via *authobject*.
302+
303+
*mechanism* specifies which authentication mechanism is to
304+
be used as argument to the ``AUTH`` command; the valid values are
305+
those listed in the ``auth`` element of :attr:`esmtp_features`.
306+
307+
*authobject* must be a callable object taking a single argument:
308+
309+
data = authobject(challenge)
310+
311+
It will be called to process the server's challenge response; the
312+
*challenge* argument it is passed will be a ``bytes``. It should return
313+
``bytes`` *data* that will be base64 encoded and sent to the server.
314+
315+
The ``SMTP`` class provides ``authobjects`` for the ``CRAM-MD5``, ``PLAIN``,
316+
and ``LOGIN`` mechanisms; they are named ``SMTP.auth_cram_md5``,
317+
``SMTP.auth_plain``, and ``SMTP.auth_login`` respectively. They all require
318+
that the ``user`` and ``password`` properties of the ``SMTP`` instance are
319+
set to appropriate values.
320+
321+
User code does not normally need to call ``auth`` directly, but can instead
322+
call the :meth:`login` method, which will try each of the above mechanisms in
323+
turn, in the order listed. ``auth`` is exposed to facilitate the
324+
implementation of authentication methods not (or not yet) supported directly
325+
by :mod:`smtplib`.
326+
327+
.. versionadded:: 3.5
328+
294329

295330
.. method:: SMTP.starttls(keyfile=None, certfile=None, context=None)
296331

Doc/whatsnew/3.5.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,13 @@ smtpd
221221
addresses in the :class:`~smtpd.SMTPServer` constructor, and have it
222222
successfully connect. (Contributed by Milan Oberkirch in :issue:`14758`.)
223223

224+
smtplib
225+
-------
226+
227+
* A new :meth:`~smtplib.SMTP.auth` method provides a convenient way to
228+
implement custom authentication mechanisms (contributed by Milan Oberkirch in
229+
:issue:`15014`).
230+
224231
socket
225232
------
226233

Lib/smtplib.py

Lines changed: 69 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -571,12 +571,60 @@ def ehlo_or_helo_if_needed(self):
571571
if not (200 <= code <= 299):
572572
raise SMTPHeloError(code, resp)
573573

574+
def auth(self, mechanism, authobject):
575+
"""Authentication command - requires response processing.
576+
577+
'mechanism' specifies which authentication mechanism is to
578+
be used - the valid values are those listed in the 'auth'
579+
element of 'esmtp_features'.
580+
581+
'authobject' must be a callable object taking a single argument:
582+
583+
data = authobject(challenge)
584+
585+
It will be called to process the server's challenge response; the
586+
challenge argument it is passed will be a bytes. It should return
587+
bytes data that will be base64 encoded and sent to the server.
588+
"""
589+
590+
mechanism = mechanism.upper()
591+
(code, resp) = self.docmd("AUTH", mechanism)
592+
# Server replies with 334 (challenge) or 535 (not supported)
593+
if code == 334:
594+
challenge = base64.decodebytes(resp)
595+
response = encode_base64(
596+
authobject(challenge).encode('ascii'), eol='')
597+
(code, resp) = self.docmd(response)
598+
if code in (235, 503):
599+
return (code, resp)
600+
raise SMTPAuthenticationError(code, resp)
601+
602+
def auth_cram_md5(self, challenge):
603+
""" Authobject to use with CRAM-MD5 authentication. Requires self.user
604+
and self.password to be set."""
605+
return self.user + " " + hmac.HMAC(
606+
self.password.encode('ascii'), challenge, 'md5').hexdigest()
607+
608+
def auth_plain(self, challenge):
609+
""" Authobject to use with PLAIN authentication. Requires self.user and
610+
self.password to be set."""
611+
return "\0%s\0%s" % (self.user, self.password)
612+
613+
def auth_login(self, challenge):
614+
""" Authobject to use with LOGIN authentication. Requires self.user and
615+
self.password to be set."""
616+
(code, resp) = self.docmd(
617+
encode_base64(self.user.encode('ascii'), eol=''))
618+
if code == 334:
619+
return self.password
620+
raise SMTPAuthenticationError(code, resp)
621+
574622
def login(self, user, password):
575623
"""Log in on an SMTP server that requires authentication.
576624
577625
The arguments are:
578-
- user: The user name to authenticate with.
579-
- password: The password for the authentication.
626+
- user: The user name to authenticate with.
627+
- password: The password for the authentication.
580628
581629
If there has been no previous EHLO or HELO command this session, this
582630
method tries ESMTP EHLO first.
@@ -593,63 +641,40 @@ def login(self, user, password):
593641
found.
594642
"""
595643

596-
def encode_cram_md5(challenge, user, password):
597-
challenge = base64.decodebytes(challenge)
598-
response = user + " " + hmac.HMAC(password.encode('ascii'),
599-
challenge, 'md5').hexdigest()
600-
return encode_base64(response.encode('ascii'), eol='')
601-
602-
def encode_plain(user, password):
603-
s = "\0%s\0%s" % (user, password)
604-
return encode_base64(s.encode('ascii'), eol='')
605-
606-
AUTH_PLAIN = "PLAIN"
607-
AUTH_CRAM_MD5 = "CRAM-MD5"
608-
AUTH_LOGIN = "LOGIN"
609-
610644
self.ehlo_or_helo_if_needed()
611-
612645
if not self.has_extn("auth"):
613646
raise SMTPException("SMTP AUTH extension not supported by server.")
614647

615648
# Authentication methods the server claims to support
616649
advertised_authlist = self.esmtp_features["auth"].split()
617650

618-
# List of authentication methods we support: from preferred to
619-
# less preferred methods. Except for the purpose of testing the weaker
620-
# ones, we prefer stronger methods like CRAM-MD5:
621-
preferred_auths = [AUTH_CRAM_MD5, AUTH_PLAIN, AUTH_LOGIN]
651+
# Authentication methods we can handle in our preferred order:
652+
preferred_auths = ['CRAM-MD5', 'PLAIN', 'LOGIN']
622653

623-
# We try the authentication methods the server advertises, but only the
624-
# ones *we* support. And in our preferred order.
625-
authlist = [auth for auth in preferred_auths if auth in advertised_authlist]
654+
# We try the supported authentications in our preferred order, if
655+
# the server supports them.
656+
authlist = [auth for auth in preferred_auths
657+
if auth in advertised_authlist]
626658
if not authlist:
627659
raise SMTPException("No suitable authentication method found.")
628660

629661
# Some servers advertise authentication methods they don't really
630662
# support, so if authentication fails, we continue until we've tried
631663
# all methods.
664+
self.user, self.password = user, password
632665
for authmethod in authlist:
633-
if authmethod == AUTH_CRAM_MD5:
634-
(code, resp) = self.docmd("AUTH", AUTH_CRAM_MD5)
635-
if code == 334:
636-
(code, resp) = self.docmd(encode_cram_md5(resp, user, password))
637-
elif authmethod == AUTH_PLAIN:
638-
(code, resp) = self.docmd("AUTH",
639-
AUTH_PLAIN + " " + encode_plain(user, password))
640-
elif authmethod == AUTH_LOGIN:
641-
(code, resp) = self.docmd("AUTH",
642-
"%s %s" % (AUTH_LOGIN, encode_base64(user.encode('ascii'), eol='')))
643-
if code == 334:
644-
(code, resp) = self.docmd(encode_base64(password.encode('ascii'), eol=''))
645-
646-
# 235 == 'Authentication successful'
647-
# 503 == 'Error: already authenticated'
648-
if code in (235, 503):
649-
return (code, resp)
650-
651-
# We could not login sucessfully. Return result of last attempt.
652-
raise SMTPAuthenticationError(code, resp)
666+
method_name = 'auth_' + authmethod.lower().replace('-', '_')
667+
try:
668+
(code, resp) = self.auth(authmethod, getattr(self, method_name))
669+
# 235 == 'Authentication successful'
670+
# 503 == 'Error: already authenticated'
671+
if code in (235, 503):
672+
return (code, resp)
673+
except SMTPAuthenticationError as e:
674+
last_exception = e
675+
676+
# We could not login successfully. Return result of last attempt.
677+
raise last_exception
653678

654679
def starttls(self, keyfile=None, certfile=None, context=None):
655680
"""Puts the connection to the SMTP server into TLS mode.

Lib/test/test_smtplib.py

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import time
1111
import select
1212
import errno
13+
import base64
1314

1415
import unittest
1516
from test import support, mock_socket
@@ -605,7 +606,8 @@ def testLineTooLong(self):
605606
'cram-md5': ('TXIUQUBZB21LD2HLCMUUY29TIDG4OWQ0MJ'
606607
'KWZGQ4ODNMNDA4NTGXMDRLZWMYZJDMODG1'),
607608
}
608-
sim_auth_login_password = 'C29TZXBHC3N3B3JK'
609+
sim_auth_login_user = 'TXIUQUBZB21LD2HLCMUUY29T'
610+
sim_auth_plain = 'AE1YLKFAC29TZXDOZXJLLMNVBQBZB21LCGFZC3DVCMQ='
609611

610612
sim_lists = {'list-1':['[email protected]','[email protected]'],
611613
'list-2':['[email protected]',],
@@ -659,18 +661,16 @@ def smtp_EXPN(self, arg):
659661
self.push('550 No access for you!')
660662

661663
def smtp_AUTH(self, arg):
662-
if arg.strip().lower()=='cram-md5':
664+
mech = arg.strip().lower()
665+
if mech=='cram-md5':
663666
self.push('334 {}'.format(sim_cram_md5_challenge))
664-
return
665-
mech, auth = arg.split()
666-
mech = mech.lower()
667-
if mech not in sim_auth_credentials:
667+
elif mech not in sim_auth_credentials:
668668
self.push('504 auth type unimplemented')
669669
return
670-
if mech == 'plain' and auth==sim_auth_credentials['plain']:
671-
self.push('235 plain auth ok')
672-
elif mech=='login' and auth==sim_auth_credentials['login']:
673-
self.push('334 Password:')
670+
elif mech=='plain':
671+
self.push('334 ')
672+
elif mech=='login':
673+
self.push('334 ')
674674
else:
675675
self.push('550 No access for you!')
676676

@@ -818,28 +818,28 @@ def testEXPN(self):
818818
self.assertEqual(smtp.expn(u), expected_unknown)
819819
smtp.quit()
820820

821-
def testAUTH_PLAIN(self):
822-
self.serv.add_feature("AUTH PLAIN")
823-
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
824-
825-
expected_auth_ok = (235, b'plain auth ok')
826-
self.assertEqual(smtp.login(sim_auth[0], sim_auth[1]), expected_auth_ok)
827-
smtp.close()
828-
829-
# SimSMTPChannel doesn't fully support LOGIN or CRAM-MD5 auth because they
830-
# require a synchronous read to obtain the credentials...so instead smtpd
821+
# SimSMTPChannel doesn't fully support AUTH because it requires a
822+
# synchronous read to obtain the credentials...so instead smtpd
831823
# sees the credential sent by smtplib's login method as an unknown command,
832824
# which results in smtplib raising an auth error. Fortunately the error
833825
# message contains the encoded credential, so we can partially check that it
834826
# was generated correctly (partially, because the 'word' is uppercased in
835827
# the error message).
836828

829+
def testAUTH_PLAIN(self):
830+
self.serv.add_feature("AUTH PLAIN")
831+
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
832+
try: smtp.login(sim_auth[0], sim_auth[1])
833+
except smtplib.SMTPAuthenticationError as err:
834+
self.assertIn(sim_auth_plain, str(err))
835+
smtp.close()
836+
837837
def testAUTH_LOGIN(self):
838838
self.serv.add_feature("AUTH LOGIN")
839839
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
840840
try: smtp.login(sim_auth[0], sim_auth[1])
841841
except smtplib.SMTPAuthenticationError as err:
842-
self.assertIn(sim_auth_login_password, str(err))
842+
self.assertIn(sim_auth_login_user, str(err))
843843
smtp.close()
844844

845845
def testAUTH_CRAM_MD5(self):
@@ -857,7 +857,23 @@ def testAUTH_multiple(self):
857857
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
858858
try: smtp.login(sim_auth[0], sim_auth[1])
859859
except smtplib.SMTPAuthenticationError as err:
860-
self.assertIn(sim_auth_login_password, str(err))
860+
self.assertIn(sim_auth_login_user, str(err))
861+
smtp.close()
862+
863+
def test_auth_function(self):
864+
smtp = smtplib.SMTP(HOST, self.port,
865+
local_hostname='localhost', timeout=15)
866+
self.serv.add_feature("AUTH CRAM-MD5")
867+
smtp.user, smtp.password = sim_auth[0], sim_auth[1]
868+
supported = {'CRAM-MD5': smtp.auth_cram_md5,
869+
'PLAIN': smtp.auth_plain,
870+
'LOGIN': smtp.auth_login,
871+
}
872+
for mechanism, method in supported.items():
873+
try: smtp.auth(mechanism, method)
874+
except smtplib.SMTPAuthenticationError as err:
875+
self.assertIn(sim_auth_credentials[mechanism.lower()].upper(),
876+
str(err))
861877
smtp.close()
862878

863879
def test_with_statement(self):

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ Core and Builtins
103103
Library
104104
-------
105105

106+
- Issue #15014: Added 'auth' method to smtplib to make implementing auth
107+
mechanisms simpler, and used it internally in the login method.
108+
106109
- Issue #21151: Fixed a segfault in the winreg module when ``None`` is passed
107110
as a ``REG_BINARY`` value to SetValueEx. Patch by John Ehresman.
108111

0 commit comments

Comments
 (0)