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

Skip to content

Commit ae01046

Browse files
committed
Add login() method and SMTPAuthenticationError exception. SF patch
#460112 by Gerhard Haering. (With slight layout changes to conform to docstrings guidelines and to prevent a line longer than 78 characters. Also fixed some docstrings that Gerhard didn't touch.)
1 parent f166994 commit ae01046

1 file changed

Lines changed: 93 additions & 3 deletions

File tree

Lib/smtplib.py

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
'''SMTP/ESMTP client class.
44
5-
This should follow RFC 821 (SMTP) and RFC 1869 (ESMTP).
5+
This should follow RFC 821 (SMTP), RFC 1869 (ESMTP) and RFC 2554 (SMTP
6+
Authentication).
67
78
Notes:
89
@@ -36,18 +37,21 @@
3637
# Eric S. Raymond <[email protected]>
3738
# Better RFC 821 compliance (MAIL and RCPT, and CRLF in data)
3839
# by Carey Evans <[email protected]>, for picky mail servers.
40+
# RFC 2554 (authentication) support by Gerhard Haering <[email protected]>.
3941
#
4042
# This was modified from the Python 1.5 library HTTP lib.
4143

4244
import socket
4345
import re
4446
import rfc822
4547
import types
48+
import base64
49+
import hmac
4650

4751
__all__ = ["SMTPException","SMTPServerDisconnected","SMTPResponseException",
4852
"SMTPSenderRefused","SMTPRecipientsRefused","SMTPDataError",
49-
"SMTPConnectError","SMTPHeloError","quoteaddr","quotedata",
50-
"SMTP"]
53+
"SMTPConnectError","SMTPHeloError","SMTPAuthenticationError",
54+
"quoteaddr","quotedata","SMTP"]
5155

5256
SMTP_PORT = 25
5357
CRLF="\r\n"
@@ -80,6 +84,7 @@ def __init__(self, code, msg):
8084

8185
class SMTPSenderRefused(SMTPResponseException):
8286
"""Sender address refused.
87+
8388
In addition to the attributes set by on all SMTPResponseException
8489
exceptions, this sets `sender' to the string that the SMTP refused.
8590
"""
@@ -92,6 +97,7 @@ def __init__(self, code, msg, sender):
9297

9398
class SMTPRecipientsRefused(SMTPException):
9499
"""All recipient addresses refused.
100+
95101
The errors for each recipient are accessible through the attribute
96102
'recipients', which is a dictionary of exactly the same sort as
97103
SMTP.sendmail() returns.
@@ -111,6 +117,12 @@ class SMTPConnectError(SMTPResponseException):
111117
class SMTPHeloError(SMTPResponseException):
112118
"""The server refused our HELO reply."""
113119

120+
class SMTPAuthenticationError(SMTPResponseException):
121+
"""Authentication error.
122+
123+
Most probably the server didn't accept the username/password
124+
combination provided.
125+
"""
114126

115127
def quoteaddr(addr):
116128
"""Quote a subset of the email addresses defined by RFC 821.
@@ -416,6 +428,84 @@ def expn(self, address):
416428
return self.getreply()
417429

418430
# some useful methods
431+
432+
def login(self, user, password):
433+
"""Log in on an SMTP server that requires authentication.
434+
435+
The arguments are:
436+
- user: The user name to authenticate with.
437+
- password: The password for the authentication.
438+
439+
If there has been no previous EHLO or HELO command this session, this
440+
method tries ESMTP EHLO first.
441+
442+
This method will return normally if the authentication was successful.
443+
444+
This method may raise the following exceptions:
445+
446+
SMTPHeloError The server didn't reply properly to
447+
the helo greeting.
448+
SMTPAuthenticationError The server didn't accept the username/
449+
password combination.
450+
SMTPError No suitable authentication method was
451+
found.
452+
"""
453+
454+
def encode_cram_md5(challenge, user, password):
455+
challenge = base64.decodestring(challenge)
456+
response = user + " " + hmac.HMAC(password, challenge).hexdigest()
457+
return base64.encodestring(response)[:-1]
458+
459+
def encode_plain(user, password):
460+
return base64.encodestring("%s\0%s\0%s" %
461+
(user, user, password))[:-1]
462+
463+
AUTH_PLAIN = "PLAIN"
464+
AUTH_CRAM_MD5 = "CRAM-MD5"
465+
466+
if self.helo_resp is None and self.ehlo_resp is None:
467+
if not (200 <= self.ehlo()[0] <= 299):
468+
(code, resp) = self.helo()
469+
if not (200 <= code <= 299):
470+
raise SMTPHeloError(code, resp)
471+
472+
if not self.has_extn("auth"):
473+
raise SMTPException("SMTP AUTH extension not supported by server.")
474+
475+
# Authentication methods the server supports:
476+
authlist = self.esmtp_features["auth"].split()
477+
478+
# List of authentication methods we support: from preferred to
479+
# less preferred methods. Except for the purpose of testing the weaker
480+
# ones, we prefer stronger methods like CRAM-MD5:
481+
preferred_auths = [AUTH_CRAM_MD5, AUTH_PLAIN]
482+
#preferred_auths = [AUTH_PLAIN, AUTH_CRAM_MD5]
483+
484+
# Determine the authentication method we'll use
485+
authmethod = None
486+
for method in preferred_auths:
487+
if method in authlist:
488+
authmethod = method
489+
break
490+
if self.debuglevel > 0: print "AuthMethod:", authmethod
491+
492+
if authmethod == AUTH_CRAM_MD5:
493+
(code, resp) = self.docmd("AUTH", AUTH_CRAM_MD5)
494+
if code == 503:
495+
# 503 == 'Error: already authenticated'
496+
return (code, resp)
497+
(code, resp) = self.docmd(encode_cram_md5(resp, user, password))
498+
elif authmethod == AUTH_PLAIN:
499+
(code, resp) = self.docmd("AUTH",
500+
AUTH_PLAIN + " " + encode_plain(user, password))
501+
elif authmethod == None:
502+
raise SMTPError("No suitable authentication method found.")
503+
if code not in [235, 503]:
504+
# 235 == 'Authentication successful'
505+
# 503 == 'Error: already authenticated'
506+
raise SMTPAuthenticationError(code, resp)
507+
return (code, resp)
508+
419509
def sendmail(self, from_addr, to_addrs, msg, mail_options=[],
420510
rcpt_options=[]):
421511
"""This command performs an entire mail transaction.

0 commit comments

Comments
 (0)