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:
parent
f166994b83
commit
ae01046f7b
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
'''SMTP/ESMTP client class.
|
'''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:
|
Notes:
|
||||||
|
|
||||||
|
@ -36,6 +37,7 @@ Example:
|
||||||
# Eric S. Raymond <esr@thyrsus.com>
|
# Eric S. Raymond <esr@thyrsus.com>
|
||||||
# Better RFC 821 compliance (MAIL and RCPT, and CRLF in data)
|
# Better RFC 821 compliance (MAIL and RCPT, and CRLF in data)
|
||||||
# by Carey Evans <c.evans@clear.net.nz>, for picky mail servers.
|
# 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.
|
# This was modified from the Python 1.5 library HTTP lib.
|
||||||
|
|
||||||
|
@ -43,11 +45,13 @@ import socket
|
||||||
import re
|
import re
|
||||||
import rfc822
|
import rfc822
|
||||||
import types
|
import types
|
||||||
|
import base64
|
||||||
|
import hmac
|
||||||
|
|
||||||
__all__ = ["SMTPException","SMTPServerDisconnected","SMTPResponseException",
|
__all__ = ["SMTPException","SMTPServerDisconnected","SMTPResponseException",
|
||||||
"SMTPSenderRefused","SMTPRecipientsRefused","SMTPDataError",
|
"SMTPSenderRefused","SMTPRecipientsRefused","SMTPDataError",
|
||||||
"SMTPConnectError","SMTPHeloError","quoteaddr","quotedata",
|
"SMTPConnectError","SMTPHeloError","SMTPAuthenticationError",
|
||||||
"SMTP"]
|
"quoteaddr","quotedata","SMTP"]
|
||||||
|
|
||||||
SMTP_PORT = 25
|
SMTP_PORT = 25
|
||||||
CRLF="\r\n"
|
CRLF="\r\n"
|
||||||
|
@ -80,6 +84,7 @@ class SMTPResponseException(SMTPException):
|
||||||
|
|
||||||
class SMTPSenderRefused(SMTPResponseException):
|
class SMTPSenderRefused(SMTPResponseException):
|
||||||
"""Sender address refused.
|
"""Sender address refused.
|
||||||
|
|
||||||
In addition to the attributes set by on all SMTPResponseException
|
In addition to the attributes set by on all SMTPResponseException
|
||||||
exceptions, this sets `sender' to the string that the SMTP refused.
|
exceptions, this sets `sender' to the string that the SMTP refused.
|
||||||
"""
|
"""
|
||||||
|
@ -92,6 +97,7 @@ class SMTPSenderRefused(SMTPResponseException):
|
||||||
|
|
||||||
class SMTPRecipientsRefused(SMTPException):
|
class SMTPRecipientsRefused(SMTPException):
|
||||||
"""All recipient addresses refused.
|
"""All recipient addresses refused.
|
||||||
|
|
||||||
The errors for each recipient are accessible through the attribute
|
The errors for each recipient are accessible through the attribute
|
||||||
'recipients', which is a dictionary of exactly the same sort as
|
'recipients', which is a dictionary of exactly the same sort as
|
||||||
SMTP.sendmail() returns.
|
SMTP.sendmail() returns.
|
||||||
|
@ -111,6 +117,12 @@ class SMTPConnectError(SMTPResponseException):
|
||||||
class SMTPHeloError(SMTPResponseException):
|
class SMTPHeloError(SMTPResponseException):
|
||||||
"""The server refused our HELO reply."""
|
"""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):
|
def quoteaddr(addr):
|
||||||
"""Quote a subset of the email addresses defined by RFC 821.
|
"""Quote a subset of the email addresses defined by RFC 821.
|
||||||
|
@ -416,6 +428,84 @@ class SMTP:
|
||||||
return self.getreply()
|
return self.getreply()
|
||||||
|
|
||||||
# some useful methods
|
# 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=[],
|
def sendmail(self, from_addr, to_addrs, msg, mail_options=[],
|
||||||
rcpt_options=[]):
|
rcpt_options=[]):
|
||||||
"""This command performs an entire mail transaction.
|
"""This command performs an entire mail transaction.
|
||||||
|
|
Loading…
Reference in New Issue