mirror of
https://github.com/python/cpython.git
synced 2025-12-04 00:30:19 +00:00
Latest version by The Dragon, who writes:
I did some bugfixes, and fixed a major problem with the esmtp suport (I think the person who did that part misunderstood RFC1869) Some of the interface fer esmtp-related things has changed as a result. I also added some documentation to the SMTP class' docstring.
This commit is contained in:
parent
5053efc2c4
commit
fcfb6323fb
1 changed files with 120 additions and 66 deletions
184
Lib/smtplib.py
184
Lib/smtplib.py
|
|
@ -3,14 +3,20 @@
|
||||||
|
|
||||||
Author: The Dragon De Monsyne <dragondm@integral.org>
|
Author: The Dragon De Monsyne <dragondm@integral.org>
|
||||||
ESMTP support, test code and doc fixes added by
|
ESMTP support, test code and doc fixes added by
|
||||||
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.
|
||||||
|
|
||||||
(This was modified from the Python 1.5 library HTTP lib.)
|
(This was modified from the Python 1.5 library HTTP lib.)
|
||||||
|
|
||||||
This should follow RFC 821 (SMTP) and RFC 1869 (ESMTP).
|
This should follow RFC 821 (SMTP) and RFC 1869 (ESMTP).
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
Please remember, when doing ESMTP, that the names of the SMTP service
|
||||||
|
extensions are NOT the same thing as the option keyords for the RCPT
|
||||||
|
and MAIL commands!
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
>>> import smtplib
|
>>> import smtplib
|
||||||
|
|
@ -34,7 +40,8 @@ End of HELP info
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import socket
|
import socket
|
||||||
import string,re
|
import string, re
|
||||||
|
import rfc822
|
||||||
|
|
||||||
SMTP_PORT = 25
|
SMTP_PORT = 25
|
||||||
CRLF="\r\n"
|
CRLF="\r\n"
|
||||||
|
|
@ -48,41 +55,61 @@ SMTPDataError="Error transmitting message data"
|
||||||
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.
|
||||||
|
|
||||||
Technically, only a <mailbox> is allowed. In addition,
|
Should be able to handle anything rfc822.parseaddr can handle."""
|
||||||
email addresses without a domain are permitted.
|
|
||||||
|
|
||||||
Addresses will not be modified if they are already quoted
|
m=None
|
||||||
(actually if they begin with '<' and end with '>'."""
|
|
||||||
if re.match('(?s)\A<.*>\Z', addr):
|
|
||||||
return addr
|
|
||||||
|
|
||||||
localpart = None
|
|
||||||
domain = ''
|
|
||||||
try:
|
try:
|
||||||
at = string.rindex(addr, '@')
|
m=rfc822.parseaddr(addr)[1]
|
||||||
localpart = addr[:at]
|
except AttributeError:
|
||||||
domain = addr[at:]
|
pass
|
||||||
except ValueError:
|
if not m:
|
||||||
localpart = addr
|
#something weird here.. punt -ddm
|
||||||
|
return addr
|
||||||
pat = re.compile(r'([<>()\[\]\\,;:@\"\001-\037\177])')
|
else:
|
||||||
return '<%s%s>' % (pat.sub(r'\\\1', localpart), domain)
|
return "<%s>" % m
|
||||||
|
|
||||||
def quotedata(data):
|
def quotedata(data):
|
||||||
"""Quote data for email.
|
"""Quote data for email.
|
||||||
|
|
||||||
Double leading '.', and change Unix newline '\n' into
|
Double leading '.', and change Unix newline '\n', or Mac '\r' into
|
||||||
Internet CRLF end-of-line."""
|
Internet CRLF end-of-line."""
|
||||||
return re.sub(r'(?m)^\.', '..',
|
return re.sub(r'(?m)^\.', '..',
|
||||||
re.sub(r'\r?\n', CRLF, data))
|
re.sub(r'(?:\r\n|\n|\r(?!\n))', CRLF, data))
|
||||||
|
|
||||||
class SMTP:
|
class SMTP:
|
||||||
"""This class manages a connection to an SMTP or ESMTP server."""
|
"""This class manages a connection to an SMTP or ESMTP server.
|
||||||
|
SMTP Objects:
|
||||||
|
SMTP objects have the following attributes:
|
||||||
|
helo_resp
|
||||||
|
This is the message given by the server in responce to the
|
||||||
|
most recent HELO command.
|
||||||
|
|
||||||
|
ehlo_resp
|
||||||
|
This is the message given by the server in responce to the
|
||||||
|
most recent EHLO command. This is usually multiline.
|
||||||
|
|
||||||
|
does_esmtp
|
||||||
|
This is a True value _after you do an EHLO command_, if the
|
||||||
|
server supports ESMTP.
|
||||||
|
|
||||||
|
esmtp_features
|
||||||
|
This is a dictionary, which, if the server supports ESMTP,
|
||||||
|
will _after you do an EHLO command_, contain the names of the
|
||||||
|
SMTP service extentions this server supports, and their
|
||||||
|
parameters (if any).
|
||||||
|
Note, all extention names are mapped to lower case in the
|
||||||
|
dictionary.
|
||||||
|
|
||||||
|
For method docs, see each method's docstrings. In general, there is
|
||||||
|
a method of the same name to preform each SMTP comand, and there
|
||||||
|
is a method called 'sendmail' that will do an entiere mail
|
||||||
|
transaction."""
|
||||||
|
|
||||||
debuglevel = 0
|
debuglevel = 0
|
||||||
file = None
|
file = None
|
||||||
helo_resp = None
|
helo_resp = None
|
||||||
ehlo_resp = None
|
ehlo_resp = None
|
||||||
esmtp_features = []
|
does_esmtp = 0
|
||||||
|
|
||||||
def __init__(self, host = '', port = 0):
|
def __init__(self, host = '', port = 0):
|
||||||
"""Initialize a new instance.
|
"""Initialize a new instance.
|
||||||
|
|
@ -92,6 +119,7 @@ class SMTP:
|
||||||
to connect. By default, smtplib.SMTP_PORT is used.
|
to connect. By default, smtplib.SMTP_PORT is used.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
self.esmtp_features = {}
|
||||||
if host: self.connect(host, port)
|
if host: self.connect(host, port)
|
||||||
|
|
||||||
def set_debuglevel(self, debuglevel):
|
def set_debuglevel(self, debuglevel):
|
||||||
|
|
@ -103,17 +131,12 @@ class SMTP:
|
||||||
"""
|
"""
|
||||||
self.debuglevel = debuglevel
|
self.debuglevel = debuglevel
|
||||||
|
|
||||||
def verify(self, address):
|
|
||||||
""" SMTP 'verify' command. Checks for address validity. """
|
|
||||||
self.putcmd("vrfy", address)
|
|
||||||
return self.getreply()
|
|
||||||
|
|
||||||
def connect(self, host='localhost', port = 0):
|
def connect(self, host='localhost', port = 0):
|
||||||
"""Connect to a host on a given port.
|
"""Connect to a host on a given port.
|
||||||
|
|
||||||
If the hostname ends with a colon (`:') followed by a number,
|
If the hostname ends with a colon (`:') followed by a number,
|
||||||
that suffix will be stripped off and the number interpreted as
|
and there is no port specified, that suffix will be stripped
|
||||||
the port number to use.
|
off and the number interpreted as the port number to use.
|
||||||
|
|
||||||
Note: This method is automatically invoked by __init__,
|
Note: This method is automatically invoked by __init__,
|
||||||
if a host is specified during instantiation.
|
if a host is specified during instantiation.
|
||||||
|
|
@ -138,7 +161,10 @@ class SMTP:
|
||||||
"""Send `str' to the server."""
|
"""Send `str' to the server."""
|
||||||
if self.debuglevel > 0: print 'send:', `str`
|
if self.debuglevel > 0: print 'send:', `str`
|
||||||
if self.sock:
|
if self.sock:
|
||||||
self.sock.send(str)
|
try:
|
||||||
|
self.sock.send(str)
|
||||||
|
except socket.error:
|
||||||
|
raise SMTPServerDisconnected
|
||||||
else:
|
else:
|
||||||
raise SMTPServerDisconnected
|
raise SMTPServerDisconnected
|
||||||
|
|
||||||
|
|
@ -148,7 +174,7 @@ class SMTP:
|
||||||
str = '%s %s%s' % (cmd, args, CRLF)
|
str = '%s %s%s' % (cmd, args, CRLF)
|
||||||
self.send(str)
|
self.send(str)
|
||||||
|
|
||||||
def getreply(self, linehook=None):
|
def getreply(self):
|
||||||
"""Get a reply from the server.
|
"""Get a reply from the server.
|
||||||
|
|
||||||
Returns a tuple consisting of:
|
Returns a tuple consisting of:
|
||||||
|
|
@ -168,8 +194,6 @@ class SMTP:
|
||||||
#check if multiline resp
|
#check if multiline resp
|
||||||
if line[3:4]!="-":
|
if line[3:4]!="-":
|
||||||
break
|
break
|
||||||
elif linehook:
|
|
||||||
linehook(line)
|
|
||||||
try:
|
try:
|
||||||
errcode = string.atoi(code)
|
errcode = string.atoi(code)
|
||||||
except(ValueError):
|
except(ValueError):
|
||||||
|
|
@ -201,23 +225,35 @@ class SMTP:
|
||||||
|
|
||||||
def ehlo(self, name=''):
|
def ehlo(self, name=''):
|
||||||
""" SMTP 'ehlo' command. Hostname to send for this command
|
""" SMTP 'ehlo' command. Hostname to send for this command
|
||||||
defaults to the FQDN of the local host """
|
defaults to the FQDN of the local host. """
|
||||||
name=string.strip(name)
|
name=string.strip(name)
|
||||||
if len(name)==0:
|
if len(name)==0:
|
||||||
name=socket.gethostbyaddr(socket.gethostname())[0]
|
name=socket.gethostbyaddr(socket.gethostname())[0]
|
||||||
self.putcmd("ehlo",name)
|
self.putcmd("ehlo",name)
|
||||||
(code,msg)=self.getreply(self.ehlo_hook)
|
(code,msg)=self.getreply()
|
||||||
|
# According to RFC1869 some (badly written)
|
||||||
|
# MTA's will disconnect on an ehlo. Toss an exception if
|
||||||
|
# that happens -ddm
|
||||||
|
if code == -1 and len(msg) == 0:
|
||||||
|
raise SMTPServerDisconnected
|
||||||
self.ehlo_resp=msg
|
self.ehlo_resp=msg
|
||||||
|
if code<>250:
|
||||||
|
return code
|
||||||
|
self.does_esmtp=1
|
||||||
|
#parse the ehlo responce -ddm
|
||||||
|
resp=string.split(self.ehlo_resp,'\n')
|
||||||
|
del resp[0]
|
||||||
|
for each in resp:
|
||||||
|
m=re.match(r'(?P<feature>[A-Za-z0-9][A-Za-z0-9\-]*)',each)
|
||||||
|
if m:
|
||||||
|
feature=string.lower(m.group("feature"))
|
||||||
|
params=string.strip(m.string[m.end("feature"):])
|
||||||
|
self.esmtp_features[feature]=params
|
||||||
return code
|
return code
|
||||||
|
|
||||||
def ehlo_hook(self, line):
|
def has_extn(self, opt):
|
||||||
# Interpret EHLO response lines
|
"""Does the server support a given SMTP service extension?"""
|
||||||
if line[4] in string.uppercase+string.digits:
|
return self.esmtp_features.has_key(string.lower(opt))
|
||||||
self.esmtp_features.append(string.lower(string.strip(line)[4:]))
|
|
||||||
|
|
||||||
def has_option(self, opt):
|
|
||||||
"""Does the server support a given SMTP option?"""
|
|
||||||
return opt in self.esmtp_features
|
|
||||||
|
|
||||||
def help(self, args=''):
|
def help(self, args=''):
|
||||||
""" SMTP 'help' command. Returns help text from server """
|
""" SMTP 'help' command. Returns help text from server """
|
||||||
|
|
@ -237,16 +273,18 @@ class SMTP:
|
||||||
|
|
||||||
def mail(self,sender,options=[]):
|
def mail(self,sender,options=[]):
|
||||||
""" SMTP 'mail' command. Begins mail xfer session. """
|
""" SMTP 'mail' command. Begins mail xfer session. """
|
||||||
if options:
|
optionlist = ''
|
||||||
options = " " + string.joinfields(options, ' ')
|
if options and self.does_esmtp:
|
||||||
else:
|
optionlist = string.join(options, ' ')
|
||||||
options = ''
|
self.putcmd("mail", "FROM:%s %s" % (quoteaddr(sender) ,optionlist))
|
||||||
self.putcmd("mail", "from:" + quoteaddr(sender) + options)
|
|
||||||
return self.getreply()
|
return self.getreply()
|
||||||
|
|
||||||
def rcpt(self,recip):
|
def rcpt(self,recip,options=[]):
|
||||||
""" SMTP 'rcpt' command. Indicates 1 recipient for this mail. """
|
""" SMTP 'rcpt' command. Indicates 1 recipient for this mail. """
|
||||||
self.putcmd("rcpt","to:%s" % quoteaddr(recip))
|
optionlist = ''
|
||||||
|
if options and self.does_esmtp:
|
||||||
|
optionlist = string.join(options, ' ')
|
||||||
|
self.putcmd("rcpt","TO:%s %s" % (quoteaddr(recip),optionlist))
|
||||||
return self.getreply()
|
return self.getreply()
|
||||||
|
|
||||||
def data(self,msg):
|
def data(self,msg):
|
||||||
|
|
@ -264,20 +302,35 @@ class SMTP:
|
||||||
if self.debuglevel >0 : print "data:", (code,msg)
|
if self.debuglevel >0 : print "data:", (code,msg)
|
||||||
return code
|
return code
|
||||||
|
|
||||||
|
def vrfy(self, address):
|
||||||
|
return self.verify(address)
|
||||||
|
|
||||||
|
def verify(self, address):
|
||||||
|
""" SMTP 'verify' command. Checks for address validity. """
|
||||||
|
self.putcmd("vrfy", quoteaddr(address))
|
||||||
|
return self.getreply()
|
||||||
|
|
||||||
|
def expn(self, address):
|
||||||
|
""" SMTP 'verify' command. Checks for address validity. """
|
||||||
|
self.putcmd("expn", quoteaddr(address))
|
||||||
|
return self.getreply()
|
||||||
|
|
||||||
|
|
||||||
#some useful methods
|
#some useful methods
|
||||||
def sendmail(self,from_addr,to_addrs,msg,options=[]):
|
def sendmail(self,from_addr,to_addrs,msg,mail_options=[],rcpt_options=[]):
|
||||||
""" This command performs an entire mail transaction.
|
""" This command performs an entire mail transaction.
|
||||||
The arguments are:
|
The arguments are:
|
||||||
- from_addr : The address sending this mail.
|
- from_addr : The address sending this mail.
|
||||||
- to_addrs : a list of addresses to send this mail to
|
- to_addrs : a list of addresses to send this mail to
|
||||||
- msg : the message to send.
|
- msg : the message to send.
|
||||||
- encoding : list of ESMTP options (such as 8bitmime)
|
- mail_options : list of ESMTP options (such as 8bitmime)
|
||||||
|
for the mail command
|
||||||
|
- rcpt_options : List of ESMTP options (such as DSN commands)
|
||||||
|
for all the rcpt commands
|
||||||
If there has been no previous EHLO or HELO command this session,
|
If there has been no previous EHLO or HELO command this session,
|
||||||
this method tries ESMTP EHLO first. If the server does ESMTP, message
|
this method tries ESMTP EHLO first. If the server does ESMTP, message
|
||||||
size and each of the specified options will be passed to it (if the
|
size and each of the specified options will be passed to it.
|
||||||
option is in the feature set the server advertises). If EHLO fails,
|
If EHLO fails, HELO will be tried and ESMTP options suppressed.
|
||||||
HELO will be tried and ESMTP options suppressed.
|
|
||||||
|
|
||||||
This method will return normally if the mail is accepted for at least
|
This method will return normally if the mail is accepted for at least
|
||||||
one recipient. Otherwise it will throw an exception (either
|
one recipient. Otherwise it will throw an exception (either
|
||||||
|
|
@ -309,21 +362,22 @@ class SMTP:
|
||||||
if not self.helo_resp and not self.ehlo_resp:
|
if not self.helo_resp and not self.ehlo_resp:
|
||||||
if self.ehlo() >= 400:
|
if self.ehlo() >= 400:
|
||||||
self.helo()
|
self.helo()
|
||||||
if self.esmtp_features:
|
|
||||||
self.esmtp_features.append('7bit')
|
|
||||||
esmtp_opts = []
|
esmtp_opts = []
|
||||||
if 'size' in self.esmtp_features:
|
if self.does_esmtp:
|
||||||
esmtp_opts.append("size=" + `len(msg)`)
|
# Hmmm? what's this? -ddm
|
||||||
for option in options:
|
# self.esmtp_features['7bit']=""
|
||||||
if option in self.esmtp_features:
|
if self.has_extn('size'):
|
||||||
|
esmtp_opts.append("size=" + `len(msg)`)
|
||||||
|
for option in mail_options:
|
||||||
esmtp_opts.append(option)
|
esmtp_opts.append(option)
|
||||||
|
|
||||||
(code,resp) = self.mail(from_addr, esmtp_opts)
|
(code,resp) = self.mail(from_addr, esmtp_opts)
|
||||||
if code <> 250:
|
if code <> 250:
|
||||||
self.rset()
|
self.rset()
|
||||||
raise SMTPSenderRefused
|
raise SMTPSenderRefused
|
||||||
senderrs={}
|
senderrs={}
|
||||||
for each in to_addrs:
|
for each in to_addrs:
|
||||||
(code,resp)=self.rcpt(each)
|
(code,resp)=self.rcpt(each, rcpt_options)
|
||||||
if (code <> 250) and (code <> 251):
|
if (code <> 250) and (code <> 251):
|
||||||
senderrs[each]=(code,resp)
|
senderrs[each]=(code,resp)
|
||||||
if len(senderrs)==len(to_addrs):
|
if len(senderrs)==len(to_addrs):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue