Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
C
cpython
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Kirill Smelkov
cpython
Commits
76e13c1c
Commit
76e13c1c
authored
Jul 03, 2014
by
R David Murray
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
#15014: Add 'auth' command to implement auth mechanisms and use it in login.
Patch by Milan Oberkirch.
parent
d8b129f2
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
154 additions
and
68 deletions
+154
-68
Doc/library/smtplib.rst
Doc/library/smtplib.rst
+37
-2
Doc/whatsnew/3.5.rst
Doc/whatsnew/3.5.rst
+7
-0
Lib/smtplib.py
Lib/smtplib.py
+69
-44
Lib/test/test_smtplib.py
Lib/test/test_smtplib.py
+38
-22
Misc/NEWS
Misc/NEWS
+3
-0
No files found.
Doc/library/smtplib.rst
View file @
76e13c1c
...
@@ -240,8 +240,7 @@ An :class:`SMTP` instance has the following methods:
...
@@ -240,8 +240,7 @@ An :class:`SMTP` instance has the following methods:
the server is stored as the :attr:`ehlo_resp` attribute, :attr:`does_esmtp`
the server is stored as the :attr:`ehlo_resp` attribute, :attr:`does_esmtp`
is set to true or false depending on whether the server supports ESMTP, and
is set to true or false depending on whether the server supports ESMTP, and
:attr:`esmtp_features` will be a dictionary containing the names of the
:attr:`esmtp_features` will be a dictionary containing the names of the
SMTP service extensions this server supports, and their
SMTP service extensions this server supports, and their parameters (if any).
parameters (if any).
Unless you wish to use :meth:`has_extn` before sending mail, it should not be
Unless you wish to use :meth:`has_extn` before sending mail, it should not be
necessary to call this method explicitly. It will be implicitly called by
necessary to call this method explicitly. It will be implicitly called by
...
@@ -291,6 +290,42 @@ An :class:`SMTP` instance has the following methods:
...
@@ -291,6 +290,42 @@ An :class:`SMTP` instance has the following methods:
:exc:`SMTPException`
:exc:`SMTPException`
No suitable authentication method was found.
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).
.. method:: SMTP.auth(mechanism, authobject)
Issue an ``SMTP`` ``AUTH`` command for the specified authentication
*mechanism*, and handle the challenge response via *authobject*.
*mechanism* specifies which authentication mechanism is to
be used as argument to the ``AUTH`` command; the valid values are
those listed in the ``auth`` element of :attr:`esmtp_features`.
*authobject* must be a callable object taking a single argument:
data = authobject(challenge)
It will be called to process the server's challenge response; the
*challenge* argument it is passed will be a ``bytes``. It should return
``bytes`` *data* that will be base64 encoded and sent to the server.
The ``SMTP`` class provides ``authobjects`` for the ``CRAM-MD5``, ``PLAIN``,
and ``LOGIN`` mechanisms; they are named ``SMTP.auth_cram_md5``,
``SMTP.auth_plain``, and ``SMTP.auth_login`` respectively. They all require
that the ``user`` and ``password`` properties of the ``SMTP`` instance are
set to appropriate values.
User code does not normally need to call ``auth`` directly, but can instead
call the :meth:`login` method, which will try each of the above mechanisms in
turn, in the order listed. ``auth`` is exposed to facilitate the
implementation of authentication methods not (or not yet) supported directly
by :mod:`smtplib`.
.. versionadded:: 3.5
.. method:: SMTP.starttls(keyfile=None, certfile=None, context=None)
.. method:: SMTP.starttls(keyfile=None, certfile=None, context=None)
...
...
Doc/whatsnew/3.5.rst
View file @
76e13c1c
...
@@ -221,6 +221,13 @@ smtpd
...
@@ -221,6 +221,13 @@ smtpd
addresses in the :class:`~smtpd.SMTPServer` constructor, and have it
addresses in the :class:`~smtpd.SMTPServer` constructor, and have it
successfully connect. (Contributed by Milan Oberkirch in :issue:`14758`.)
successfully connect. (Contributed by Milan Oberkirch in :issue:`14758`.)
smtplib
-------
* A new :meth:`~smtplib.SMTP.auth` method provides a convenient way to
implement custom authentication mechanisms (contributed by Milan Oberkirch in
:issue:`15014`).
socket
socket
------
------
...
...
Lib/smtplib.py
View file @
76e13c1c
...
@@ -571,12 +571,60 @@ class SMTP:
...
@@ -571,12 +571,60 @@ class SMTP:
if not (200 <= code <= 299):
if not (200 <= code <= 299):
raise SMTPHeloError(code, resp)
raise SMTPHeloError(code, resp)
def auth(self, mechanism, authobject):
"""Authentication command - requires response processing.
'
mechanism
' specifies which authentication mechanism is to
be used - the valid values are those listed in the '
auth
'
element of '
esmtp_features
'.
'
authobject
' must be a callable object taking a single argument:
data = authobject(challenge)
It will be called to process the server'
s
challenge
response
;
the
challenge
argument
it
is
passed
will
be
a
bytes
.
It
should
return
bytes
data
that
will
be
base64
encoded
and
sent
to
the
server
.
"""
mechanism = mechanism.upper()
(code, resp) = self.docmd("AUTH", mechanism)
# Server replies with 334 (challenge) or 535 (not supported)
if code == 334:
challenge = base64.decodebytes(resp)
response = encode_base64(
authobject(challenge).encode('ascii'), eol='')
(code, resp) = self.docmd(response)
if code in (235, 503):
return (code, resp)
raise SMTPAuthenticationError(code, resp)
def auth_cram_md5(self, challenge):
"""
Authobject
to
use
with
CRAM
-
MD5
authentication
.
Requires
self
.
user
and
self
.
password
to
be
set
.
"""
return self.user + " " + hmac.HMAC(
self.password.encode('ascii'), challenge, 'md5').hexdigest()
def auth_plain(self, challenge):
"""
Authobject
to
use
with
PLAIN
authentication
.
Requires
self
.
user
and
self
.
password
to
be
set
.
"""
return "
\
0
%s
\
0
%s" % (self.user, self.password)
def auth_login(self, challenge):
"""
Authobject
to
use
with
LOGIN
authentication
.
Requires
self
.
user
and
self
.
password
to
be
set
.
"""
(code, resp) = self.docmd(
encode_base64(self.user.encode('ascii'), eol=''))
if code == 334:
return self.password
raise SMTPAuthenticationError(code, resp)
def login(self, user, password):
def login(self, user, password):
"""
Log
in
on
an
SMTP
server
that
requires
authentication
.
"""
Log
in
on
an
SMTP
server
that
requires
authentication
.
The
arguments
are
:
The
arguments
are
:
- user: The user name to authenticate with.
-
user
:
The
user
name
to
authenticate
with
.
- password: The password for the authentication.
-
password
:
The
password
for
the
authentication
.
If
there
has
been
no
previous
EHLO
or
HELO
command
this
session
,
this
If
there
has
been
no
previous
EHLO
or
HELO
command
this
session
,
this
method
tries
ESMTP
EHLO
first
.
method
tries
ESMTP
EHLO
first
.
...
@@ -593,63 +641,40 @@ class SMTP:
...
@@ -593,63 +641,40 @@ class SMTP:
found
.
found
.
"""
"""
def encode_cram_md5(challenge, user, password):
challenge = base64.decodebytes(challenge)
response = user + " " + hmac.HMAC(password.encode('
ascii
'),
challenge, '
md5
').hexdigest()
return encode_base64(response.encode('
ascii
'), eol='')
def encode_plain(user, password):
s = "
\
0
%s
\
0
%s" % (user, password)
return encode_base64(s.encode('
ascii
'), eol='')
AUTH_PLAIN = "PLAIN"
AUTH_CRAM_MD5 = "CRAM-MD5"
AUTH_LOGIN = "LOGIN"
self.ehlo_or_helo_if_needed()
self.ehlo_or_helo_if_needed()
if not self.has_extn("auth"):
if not self.has_extn("auth"):
raise SMTPException("SMTP AUTH extension not supported by server.")
raise SMTPException("SMTP AUTH extension not supported by server.")
# Authentication methods the server claims to support
# Authentication methods the server claims to support
advertised_authlist = self.esmtp_features["auth"].split()
advertised_authlist = self.esmtp_features["auth"].split()
# List of authentication methods we support: from preferred to
# Authentication methods we can handle in our preferred order:
# less preferred methods. Except for the purpose of testing the weaker
preferred_auths = ['CRAM-MD5', 'PLAIN', 'LOGIN']
# ones, we prefer stronger methods like CRAM-MD5:
preferred_auths = [AUTH_CRAM_MD5, AUTH_PLAIN, AUTH_LOGIN]
# We try the authentication methods the server advertises, but only the
# We try the supported authentications in our preferred order, if
# ones *we* support. And in our preferred order.
# the server supports them.
authlist = [auth for auth in preferred_auths if auth in advertised_authlist]
authlist = [auth for auth in preferred_auths
if auth in advertised_authlist]
if not authlist:
if not authlist:
raise SMTPException("No suitable authentication method found.")
raise SMTPException("No suitable authentication method found.")
# Some servers advertise authentication methods they don't really
# Some servers advertise authentication methods they don't really
# support, so if authentication fails, we continue until we've tried
# support, so if authentication fails, we continue until we've tried
# all methods.
# all methods.
self.user, self.password = user, password
for authmethod in authlist:
for authmethod in authlist:
if
authmethod
==
AUTH_CRAM_MD5
:
method_name = 'auth_' + authmethod.lower().replace('-', '_')
(
code
,
resp
)
=
self
.
docmd
(
"AUTH"
,
AUTH_CRAM_MD5
)
try:
if
code
==
334
:
(code, resp) = self.auth(authmethod, getattr(self, method_name))
(
code
,
resp
)
=
self
.
docmd
(
encode_cram_md5
(
resp
,
user
,
password
))
# 235 == 'Authentication successful'
elif
authmethod
==
AUTH_PLAIN
:
# 503 == 'Error: already authenticated'
(
code
,
resp
)
=
self
.
docmd
(
"AUTH"
,
if code in (235, 503):
AUTH_PLAIN
+
" "
+
encode_plain
(
user
,
password
))
return (code, resp)
elif
authmethod
==
AUTH_LOGIN
:
except SMTPAuthenticationError as e:
(
code
,
resp
)
=
self
.
docmd
(
"AUTH"
,
last_exception = e
"%s %s"
%
(
AUTH_LOGIN
,
encode_base64
(
user
.
encode
(
'ascii'
),
eol
=
''
)))
if
code
==
334
:
# We could not login successfully. Return result of last attempt.
(
code
,
resp
)
=
self
.
docmd
(
encode_base64
(
password
.
encode
(
'ascii'
),
eol
=
''
))
raise last_exception
# 235 == 'Authentication successful'
# 503 == 'Error: already authenticated'
if
code
in
(
235
,
503
):
return
(
code
,
resp
)
# We could not login sucessfully. Return result of last attempt.
raise
SMTPAuthenticationError
(
code
,
resp
)
def starttls(self, keyfile=None, certfile=None, context=None):
def starttls(self, keyfile=None, certfile=None, context=None):
"""
Puts
the
connection
to
the
SMTP
server
into
TLS
mode
.
"""
Puts
the
connection
to
the
SMTP
server
into
TLS
mode
.
...
...
Lib/test/test_smtplib.py
View file @
76e13c1c
...
@@ -10,6 +10,7 @@ import sys
...
@@ -10,6 +10,7 @@ import sys
import
time
import
time
import
select
import
select
import
errno
import
errno
import
base64
import
unittest
import
unittest
from
test
import
support
,
mock_socket
from
test
import
support
,
mock_socket
...
@@ -605,7 +606,8 @@ sim_auth_credentials = {
...
@@ -605,7 +606,8 @@ sim_auth_credentials = {
'cram-md5'
:
(
'TXIUQUBZB21LD2HLCMUUY29TIDG4OWQ0MJ'
'cram-md5'
:
(
'TXIUQUBZB21LD2HLCMUUY29TIDG4OWQ0MJ'
'KWZGQ4ODNMNDA4NTGXMDRLZWMYZJDMODG1'
),
'KWZGQ4ODNMNDA4NTGXMDRLZWMYZJDMODG1'
),
}
}
sim_auth_login_password
=
'C29TZXBHC3N3B3JK'
sim_auth_login_user
=
'TXIUQUBZB21LD2HLCMUUY29T'
sim_auth_plain
=
'AE1YLKFAC29TZXDOZXJLLMNVBQBZB21LCGFZC3DVCMQ='
sim_lists
=
{
'list-1'
:[
'Mr.A@somewhere.com'
,
'Mrs.C@somewhereesle.com'
],
sim_lists
=
{
'list-1'
:[
'Mr.A@somewhere.com'
,
'Mrs.C@somewhereesle.com'
],
'list-2'
:[
'Ms.B@xn--fo-fka.com'
,],
'list-2'
:[
'Ms.B@xn--fo-fka.com'
,],
...
@@ -659,18 +661,16 @@ class SimSMTPChannel(smtpd.SMTPChannel):
...
@@ -659,18 +661,16 @@ class SimSMTPChannel(smtpd.SMTPChannel):
self
.
push
(
'550 No access for you!'
)
self
.
push
(
'550 No access for you!'
)
def
smtp_AUTH
(
self
,
arg
):
def
smtp_AUTH
(
self
,
arg
):
if
arg
.
strip
().
lower
()
==
'cram-md5'
:
mech
=
arg
.
strip
().
lower
()
if
mech
==
'cram-md5'
:
self
.
push
(
'334 {}'
.
format
(
sim_cram_md5_challenge
))
self
.
push
(
'334 {}'
.
format
(
sim_cram_md5_challenge
))
return
elif
mech
not
in
sim_auth_credentials
:
mech
,
auth
=
arg
.
split
()
mech
=
mech
.
lower
()
if
mech
not
in
sim_auth_credentials
:
self
.
push
(
'504 auth type unimplemented'
)
self
.
push
(
'504 auth type unimplemented'
)
return
return
if
mech
==
'plain'
and
auth
==
sim_auth_credentials
[
'plain'
]
:
elif
mech
==
'plain'
:
self
.
push
(
'
235 plain auth ok
'
)
self
.
push
(
'
334
'
)
elif
mech
==
'login'
and
auth
==
sim_auth_credentials
[
'login'
]
:
elif
mech
==
'login'
:
self
.
push
(
'334
Password:
'
)
self
.
push
(
'334 '
)
else
:
else
:
self
.
push
(
'550 No access for you!'
)
self
.
push
(
'550 No access for you!'
)
...
@@ -818,28 +818,28 @@ class SMTPSimTests(unittest.TestCase):
...
@@ -818,28 +818,28 @@ class SMTPSimTests(unittest.TestCase):
self
.
assertEqual
(
smtp
.
expn
(
u
),
expected_unknown
)
self
.
assertEqual
(
smtp
.
expn
(
u
),
expected_unknown
)
smtp
.
quit
()
smtp
.
quit
()
def
testAUTH_PLAIN
(
self
):
# SimSMTPChannel doesn't fully support AUTH because it requires a
self
.
serv
.
add_feature
(
"AUTH PLAIN"
)
# synchronous read to obtain the credentials...so instead smtpd
smtp
=
smtplib
.
SMTP
(
HOST
,
self
.
port
,
local_hostname
=
'localhost'
,
timeout
=
15
)
expected_auth_ok
=
(
235
,
b'plain auth ok'
)
self
.
assertEqual
(
smtp
.
login
(
sim_auth
[
0
],
sim_auth
[
1
]),
expected_auth_ok
)
smtp
.
close
()
# SimSMTPChannel doesn't fully support LOGIN or CRAM-MD5 auth because they
# require a synchronous read to obtain the credentials...so instead smtpd
# sees the credential sent by smtplib's login method as an unknown command,
# sees the credential sent by smtplib's login method as an unknown command,
# which results in smtplib raising an auth error. Fortunately the error
# which results in smtplib raising an auth error. Fortunately the error
# message contains the encoded credential, so we can partially check that it
# message contains the encoded credential, so we can partially check that it
# was generated correctly (partially, because the 'word' is uppercased in
# was generated correctly (partially, because the 'word' is uppercased in
# the error message).
# the error message).
def
testAUTH_PLAIN
(
self
):
self
.
serv
.
add_feature
(
"AUTH PLAIN"
)
smtp
=
smtplib
.
SMTP
(
HOST
,
self
.
port
,
local_hostname
=
'localhost'
,
timeout
=
15
)
try
:
smtp
.
login
(
sim_auth
[
0
],
sim_auth
[
1
])
except
smtplib
.
SMTPAuthenticationError
as
err
:
self
.
assertIn
(
sim_auth_plain
,
str
(
err
))
smtp
.
close
()
def
testAUTH_LOGIN
(
self
):
def
testAUTH_LOGIN
(
self
):
self
.
serv
.
add_feature
(
"AUTH LOGIN"
)
self
.
serv
.
add_feature
(
"AUTH LOGIN"
)
smtp
=
smtplib
.
SMTP
(
HOST
,
self
.
port
,
local_hostname
=
'localhost'
,
timeout
=
15
)
smtp
=
smtplib
.
SMTP
(
HOST
,
self
.
port
,
local_hostname
=
'localhost'
,
timeout
=
15
)
try
:
smtp
.
login
(
sim_auth
[
0
],
sim_auth
[
1
])
try
:
smtp
.
login
(
sim_auth
[
0
],
sim_auth
[
1
])
except
smtplib
.
SMTPAuthenticationError
as
err
:
except
smtplib
.
SMTPAuthenticationError
as
err
:
self
.
assertIn
(
sim_auth_login_
password
,
str
(
err
))
self
.
assertIn
(
sim_auth_login_
user
,
str
(
err
))
smtp
.
close
()
smtp
.
close
()
def
testAUTH_CRAM_MD5
(
self
):
def
testAUTH_CRAM_MD5
(
self
):
...
@@ -857,7 +857,23 @@ class SMTPSimTests(unittest.TestCase):
...
@@ -857,7 +857,23 @@ class SMTPSimTests(unittest.TestCase):
smtp
=
smtplib
.
SMTP
(
HOST
,
self
.
port
,
local_hostname
=
'localhost'
,
timeout
=
15
)
smtp
=
smtplib
.
SMTP
(
HOST
,
self
.
port
,
local_hostname
=
'localhost'
,
timeout
=
15
)
try
:
smtp
.
login
(
sim_auth
[
0
],
sim_auth
[
1
])
try
:
smtp
.
login
(
sim_auth
[
0
],
sim_auth
[
1
])
except
smtplib
.
SMTPAuthenticationError
as
err
:
except
smtplib
.
SMTPAuthenticationError
as
err
:
self
.
assertIn
(
sim_auth_login_password
,
str
(
err
))
self
.
assertIn
(
sim_auth_login_user
,
str
(
err
))
smtp
.
close
()
def
test_auth_function
(
self
):
smtp
=
smtplib
.
SMTP
(
HOST
,
self
.
port
,
local_hostname
=
'localhost'
,
timeout
=
15
)
self
.
serv
.
add_feature
(
"AUTH CRAM-MD5"
)
smtp
.
user
,
smtp
.
password
=
sim_auth
[
0
],
sim_auth
[
1
]
supported
=
{
'CRAM-MD5'
:
smtp
.
auth_cram_md5
,
'PLAIN'
:
smtp
.
auth_plain
,
'LOGIN'
:
smtp
.
auth_login
,
}
for
mechanism
,
method
in
supported
.
items
():
try
:
smtp
.
auth
(
mechanism
,
method
)
except
smtplib
.
SMTPAuthenticationError
as
err
:
self
.
assertIn
(
sim_auth_credentials
[
mechanism
.
lower
()].
upper
(),
str
(
err
))
smtp
.
close
()
smtp
.
close
()
def
test_with_statement
(
self
):
def
test_with_statement
(
self
):
...
...
Misc/NEWS
View file @
76e13c1c
...
@@ -103,6 +103,9 @@ Core and Builtins
...
@@ -103,6 +103,9 @@ Core and Builtins
Library
Library
-------
-------
- Issue #15014: Added '
auth
' method to smtplib to make implementing auth
mechanisms simpler, and used it internally in the login method.
- Issue #21151: Fixed a segfault in the winreg module when ``None`` is passed
- Issue #21151: Fixed a segfault in the winreg module when ``None`` is passed
as a ``REG_BINARY`` value to SetValueEx. Patch by John Ehresman.
as a ``REG_BINARY`` value to SetValueEx. Patch by John Ehresman.
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment