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.)
This commit is contained in:
Guido van Rossum 2001-09-11 15:57:46 +00:00
parent f166994b83
commit ae01046f7b
1 changed files with 93 additions and 3 deletions

View File

@ -2,7 +2,8 @@
'''SMTP/ESMTP client class.
This should follow RFC 821 (SMTP) and RFC 1869 (ESMTP).
This should follow RFC 821 (SMTP), RFC 1869 (ESMTP) and RFC 2554 (SMTP
Authentication).
Notes:
@ -36,6 +37,7 @@ Example:
# Eric S. Raymond <esr@thyrsus.com>
# Better RFC 821 compliance (MAIL and RCPT, and CRLF in data)
# by Carey Evans <c.evans@clear.net.nz>, for picky mail servers.
# RFC 2554 (authentication) support by Gerhard Haering <gerhard@bigfoot.de>.
#
# This was modified from the Python 1.5 library HTTP lib.
@ -43,11 +45,13 @@ import socket
import re
import rfc822
import types
import base64
import hmac
__all__ = ["SMTPException","SMTPServerDisconnected","SMTPResponseException",
"SMTPSenderRefused","SMTPRecipientsRefused","SMTPDataError",
"SMTPConnectError","SMTPHeloError","quoteaddr","quotedata",
"SMTP"]
"SMTPConnectError","SMTPHeloError","SMTPAuthenticationError",
"quoteaddr","quotedata","SMTP"]
SMTP_PORT = 25
CRLF="\r\n"
@ -80,6 +84,7 @@ class SMTPResponseException(SMTPException):
class SMTPSenderRefused(SMTPResponseException):
"""Sender address refused.
In addition to the attributes set by on all SMTPResponseException
exceptions, this sets `sender' to the string that the SMTP refused.
"""
@ -92,6 +97,7 @@ class SMTPSenderRefused(SMTPResponseException):
class SMTPRecipientsRefused(SMTPException):
"""All recipient addresses refused.
The errors for each recipient are accessible through the attribute
'recipients', which is a dictionary of exactly the same sort as
SMTP.sendmail() returns.
@ -111,6 +117,12 @@ class SMTPConnectError(SMTPResponseException):
class SMTPHeloError(SMTPResponseException):
"""The server refused our HELO reply."""
class SMTPAuthenticationError(SMTPResponseException):
"""Authentication error.
Most probably the server didn't accept the username/password
combination provided.
"""
def quoteaddr(addr):
"""Quote a subset of the email addresses defined by RFC 821.
@ -416,6 +428,84 @@ class SMTP:
return self.getreply()
# some useful methods
def login(self, user, password):
"""Log in on an SMTP server that requires authentication.
The arguments are:
- user: The user name to authenticate with.
- password: The password for the authentication.
If there has been no previous EHLO or HELO command this session, this
method tries ESMTP EHLO first.
This method will return normally if the authentication was successful.
This method may raise the following exceptions:
SMTPHeloError The server didn't reply properly to
the helo greeting.
SMTPAuthenticationError The server didn't accept the username/
password combination.
SMTPError No suitable authentication method was
found.
"""
def encode_cram_md5(challenge, user, password):
challenge = base64.decodestring(challenge)
response = user + " " + hmac.HMAC(password, challenge).hexdigest()
return base64.encodestring(response)[:-1]
def encode_plain(user, password):
return base64.encodestring("%s\0%s\0%s" %
(user, user, password))[:-1]
AUTH_PLAIN = "PLAIN"
AUTH_CRAM_MD5 = "CRAM-MD5"
if self.helo_resp is None and self.ehlo_resp is None:
if not (200 <= self.ehlo()[0] <= 299):
(code, resp) = self.helo()
if not (200 <= code <= 299):
raise SMTPHeloError(code, resp)
if not self.has_extn("auth"):
raise SMTPException("SMTP AUTH extension not supported by server.")
# Authentication methods the server supports:
authlist = self.esmtp_features["auth"].split()
# List of authentication methods we support: from preferred to
# less preferred methods. Except for the purpose of testing the weaker
# ones, we prefer stronger methods like CRAM-MD5:
preferred_auths = [AUTH_CRAM_MD5, AUTH_PLAIN]
#preferred_auths = [AUTH_PLAIN, AUTH_CRAM_MD5]
# Determine the authentication method we'll use
authmethod = None
for method in preferred_auths:
if method in authlist:
authmethod = method
break
if self.debuglevel > 0: print "AuthMethod:", authmethod
if authmethod == AUTH_CRAM_MD5:
(code, resp) = self.docmd("AUTH", AUTH_CRAM_MD5)
if code == 503:
# 503 == 'Error: already authenticated'
return (code, resp)
(code, resp) = self.docmd(encode_cram_md5(resp, user, password))
elif authmethod == AUTH_PLAIN:
(code, resp) = self.docmd("AUTH",
AUTH_PLAIN + " " + encode_plain(user, password))
elif authmethod == None:
raise SMTPError("No suitable authentication method found.")
if code not in [235, 503]:
# 235 == 'Authentication successful'
# 503 == 'Error: already authenticated'
raise SMTPAuthenticationError(code, resp)
return (code, resp)
def sendmail(self, from_addr, to_addrs, msg, mail_options=[],
rcpt_options=[]):
"""This command performs an entire mail transaction.