mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Port various other bugfixes in smtplib.py from upstream
This commit is contained in:
parent
0b86d5175a
commit
415ebb7a6c
@ -1,3 +1,4 @@
|
|||||||
|
#!/usr/bin/env python2
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
'''SMTP/ESMTP client class.
|
'''SMTP/ESMTP client class.
|
||||||
@ -47,21 +48,23 @@ import re
|
|||||||
import email.utils
|
import email.utils
|
||||||
import base64
|
import base64
|
||||||
import hmac
|
import hmac
|
||||||
import sys
|
|
||||||
from email.base64mime import encode as encode_base64
|
from email.base64mime import encode as encode_base64
|
||||||
|
from sys import stderr
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
__all__ = ["SMTPException","SMTPServerDisconnected","SMTPResponseException",
|
__all__ = ["SMTPException", "SMTPServerDisconnected", "SMTPResponseException",
|
||||||
"SMTPSenderRefused","SMTPRecipientsRefused","SMTPDataError",
|
"SMTPSenderRefused", "SMTPRecipientsRefused", "SMTPDataError",
|
||||||
"SMTPConnectError","SMTPHeloError","SMTPAuthenticationError",
|
"SMTPConnectError", "SMTPHeloError", "SMTPAuthenticationError",
|
||||||
"quoteaddr","quotedata","SMTP"]
|
"quoteaddr", "quotedata", "SMTP"]
|
||||||
|
|
||||||
SMTP_PORT = 25
|
SMTP_PORT = 25
|
||||||
SMTP_SSL_PORT = 465
|
SMTP_SSL_PORT = 465
|
||||||
CRLF="\r\n"
|
CRLF = "\r\n"
|
||||||
|
_MAXLINE = 8192 # more than 8 times larger than RFC 821, 4.5.3
|
||||||
|
|
||||||
OLDSTYLE_AUTH = re.compile(r"auth=(.*)", re.I)
|
OLDSTYLE_AUTH = re.compile(r"auth=(.*)", re.I)
|
||||||
|
|
||||||
|
|
||||||
# Exception classes used by this module.
|
# Exception classes used by this module.
|
||||||
class SMTPException(Exception):
|
class SMTPException(Exception):
|
||||||
"""Base class for all exceptions raised by this module."""
|
"""Base class for all exceptions raised by this module."""
|
||||||
@ -130,6 +133,7 @@ class SMTPAuthenticationError(SMTPResponseException):
|
|||||||
combination provided.
|
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.
|
||||||
|
|
||||||
@ -149,6 +153,13 @@ def quoteaddr(addr):
|
|||||||
else:
|
else:
|
||||||
return "<%s>" % m
|
return "<%s>" % m
|
||||||
|
|
||||||
|
def _addr_only(addrstring):
|
||||||
|
displayname, addr = email.utils.parseaddr(addrstring)
|
||||||
|
if (displayname, addr) == ('', ''):
|
||||||
|
# parseaddr couldn't parse it, so use it as is.
|
||||||
|
return addrstring
|
||||||
|
return addr
|
||||||
|
|
||||||
def quotedata(data):
|
def quotedata(data):
|
||||||
"""Quote data for email.
|
"""Quote data for email.
|
||||||
|
|
||||||
@ -172,10 +183,14 @@ else:
|
|||||||
def __init__(self, sslobj):
|
def __init__(self, sslobj):
|
||||||
self.sslobj = sslobj
|
self.sslobj = sslobj
|
||||||
|
|
||||||
def readline(self):
|
def readline(self, size=-1):
|
||||||
|
if size < 0:
|
||||||
|
size = None
|
||||||
str = ""
|
str = ""
|
||||||
chr = None
|
chr = None
|
||||||
while chr != "\n":
|
while chr != "\n":
|
||||||
|
if size is not None and len(str) >= size:
|
||||||
|
break
|
||||||
chr = self.sslobj.read(1)
|
chr = self.sslobj.read(1)
|
||||||
if not chr:
|
if not chr:
|
||||||
break
|
break
|
||||||
@ -222,10 +237,11 @@ class SMTP:
|
|||||||
ehlo_msg = "ehlo"
|
ehlo_msg = "ehlo"
|
||||||
ehlo_resp = None
|
ehlo_resp = None
|
||||||
does_esmtp = 0
|
does_esmtp = 0
|
||||||
|
default_port = SMTP_PORT
|
||||||
|
|
||||||
def __init__(self, host='', port=0, local_hostname=None,
|
def __init__(self, host='', port=0, local_hostname=None,
|
||||||
timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
|
timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
|
||||||
debug_to=partial(print, file=sys.stderr)):
|
debug_to=partial(print, file=stderr)):
|
||||||
"""Initialize a new instance.
|
"""Initialize a new instance.
|
||||||
|
|
||||||
If specified, `host' is the name of the remote host to which to
|
If specified, `host' is the name of the remote host to which to
|
||||||
@ -241,7 +257,6 @@ class SMTP:
|
|||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.debug = debug_to
|
self.debug = debug_to
|
||||||
self.esmtp_features = {}
|
self.esmtp_features = {}
|
||||||
self.default_port = SMTP_PORT
|
|
||||||
if host:
|
if host:
|
||||||
(code, msg) = self.connect(host, port)
|
(code, msg) = self.connect(host, port)
|
||||||
if code != 220:
|
if code != 220:
|
||||||
@ -296,7 +311,7 @@ class SMTP:
|
|||||||
if not port and (host.find(':') == host.rfind(':')):
|
if not port and (host.find(':') == host.rfind(':')):
|
||||||
i = host.rfind(':')
|
i = host.rfind(':')
|
||||||
if i >= 0:
|
if i >= 0:
|
||||||
host, port = host[:i], host[i+1:]
|
host, port = host[:i], host[i + 1:]
|
||||||
try:
|
try:
|
||||||
port = int(port)
|
port = int(port)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -351,21 +366,25 @@ class SMTP:
|
|||||||
|
|
||||||
Raises SMTPServerDisconnected if end-of-file is reached.
|
Raises SMTPServerDisconnected if end-of-file is reached.
|
||||||
"""
|
"""
|
||||||
resp=[]
|
resp = []
|
||||||
if self.file is None:
|
if self.file is None:
|
||||||
self.file = self.sock.makefile('rb')
|
self.file = self.sock.makefile('rb')
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
line = self.file.readline()
|
line = self.file.readline(_MAXLINE + 1)
|
||||||
except socket.error:
|
except socket.error as e:
|
||||||
line = ''
|
self.close()
|
||||||
|
raise SMTPServerDisconnected("Connection unexpectedly closed: " +
|
||||||
|
str(e))
|
||||||
if line == '':
|
if line == '':
|
||||||
self.close()
|
self.close()
|
||||||
raise SMTPServerDisconnected("Connection unexpectedly closed")
|
raise SMTPServerDisconnected("Connection unexpectedly closed")
|
||||||
if self.debuglevel > 0:
|
if self.debuglevel > 0:
|
||||||
self.debug('reply:', repr(line))
|
self.debug('reply:', repr(line))
|
||||||
|
if len(line) > _MAXLINE:
|
||||||
|
raise SMTPResponseException(500, "Line too long.")
|
||||||
resp.append(line[4:].strip())
|
resp.append(line[4:].strip())
|
||||||
code=line[:3]
|
code = line[:3]
|
||||||
# Check that the error code is syntactically correct.
|
# Check that the error code is syntactically correct.
|
||||||
# Don't attempt to read a continuation line if it is broken.
|
# Don't attempt to read a continuation line if it is broken.
|
||||||
try:
|
try:
|
||||||
@ -374,7 +393,7 @@ class SMTP:
|
|||||||
errcode = -1
|
errcode = -1
|
||||||
break
|
break
|
||||||
# Check if multiline response.
|
# Check if multiline response.
|
||||||
if line[3:4]!="-":
|
if line[3:4] != "-":
|
||||||
break
|
break
|
||||||
|
|
||||||
errmsg = "\n".join(resp)
|
errmsg = "\n".join(resp)
|
||||||
@ -384,7 +403,7 @@ class SMTP:
|
|||||||
|
|
||||||
def docmd(self, cmd, args=""):
|
def docmd(self, cmd, args=""):
|
||||||
"""Send a command, and return its response code."""
|
"""Send a command, and return its response code."""
|
||||||
self.putcmd(cmd,args)
|
self.putcmd(cmd, args)
|
||||||
return self.getreply()
|
return self.getreply()
|
||||||
|
|
||||||
# std smtp commands
|
# std smtp commands
|
||||||
@ -394,9 +413,9 @@ class SMTP:
|
|||||||
host.
|
host.
|
||||||
"""
|
"""
|
||||||
self.putcmd("helo", name or self.local_hostname)
|
self.putcmd("helo", name or self.local_hostname)
|
||||||
(code,msg)=self.getreply()
|
(code, msg) = self.getreply()
|
||||||
self.helo_resp=msg
|
self.helo_resp = msg
|
||||||
return (code,msg)
|
return (code, msg)
|
||||||
|
|
||||||
def ehlo(self, name=''):
|
def ehlo(self, name=''):
|
||||||
""" SMTP 'ehlo' command.
|
""" SMTP 'ehlo' command.
|
||||||
@ -405,19 +424,19 @@ class SMTP:
|
|||||||
"""
|
"""
|
||||||
self.esmtp_features = {}
|
self.esmtp_features = {}
|
||||||
self.putcmd(self.ehlo_msg, name or self.local_hostname)
|
self.putcmd(self.ehlo_msg, name or self.local_hostname)
|
||||||
(code,msg)=self.getreply()
|
(code, msg) = self.getreply()
|
||||||
# According to RFC1869 some (badly written)
|
# According to RFC1869 some (badly written)
|
||||||
# MTA's will disconnect on an ehlo. Toss an exception if
|
# MTA's will disconnect on an ehlo. Toss an exception if
|
||||||
# that happens -ddm
|
# that happens -ddm
|
||||||
if code == -1 and len(msg) == 0:
|
if code == -1 and len(msg) == 0:
|
||||||
self.close()
|
self.close()
|
||||||
raise SMTPServerDisconnected("Server not connected")
|
raise SMTPServerDisconnected("Server not connected")
|
||||||
self.ehlo_resp=msg
|
self.ehlo_resp = msg
|
||||||
if code != 250:
|
if code != 250:
|
||||||
return (code,msg)
|
return (code, msg)
|
||||||
self.does_esmtp=1
|
self.does_esmtp = 1
|
||||||
# parse the ehlo response -ddm
|
# parse the ehlo response -ddm
|
||||||
resp=self.ehlo_resp.split('\n')
|
resp = self.ehlo_resp.split('\n')
|
||||||
del resp[0]
|
del resp[0]
|
||||||
for each in resp:
|
for each in resp:
|
||||||
# To be able to communicate with as many SMTP servers as possible,
|
# To be able to communicate with as many SMTP servers as possible,
|
||||||
@ -437,16 +456,16 @@ class SMTP:
|
|||||||
# It's actually stricter, in that only spaces are allowed between
|
# It's actually stricter, in that only spaces are allowed between
|
||||||
# parameters, but were not going to check for that here. Note
|
# parameters, but were not going to check for that here. Note
|
||||||
# that the space isn't present if there are no parameters.
|
# that the space isn't present if there are no parameters.
|
||||||
m=re.match(r'(?P<feature>[A-Za-z0-9][A-Za-z0-9\-]*) ?',each)
|
m = re.match(r'(?P<feature>[A-Za-z0-9][A-Za-z0-9\-]*) ?', each)
|
||||||
if m:
|
if m:
|
||||||
feature=m.group("feature").lower()
|
feature = m.group("feature").lower()
|
||||||
params=m.string[m.end("feature"):].strip()
|
params = m.string[m.end("feature"):].strip()
|
||||||
if feature == "auth":
|
if feature == "auth":
|
||||||
self.esmtp_features[feature] = self.esmtp_features.get(feature, "") \
|
self.esmtp_features[feature] = self.esmtp_features.get(feature, "") \
|
||||||
+ " " + params
|
+ " " + params
|
||||||
else:
|
else:
|
||||||
self.esmtp_features[feature]=params
|
self.esmtp_features[feature] = params
|
||||||
return (code,msg)
|
return (code, msg)
|
||||||
|
|
||||||
def has_extn(self, opt):
|
def has_extn(self, opt):
|
||||||
"""Does the server support a given SMTP service extension?"""
|
"""Does the server support a given SMTP service extension?"""
|
||||||
@ -466,23 +485,23 @@ class SMTP:
|
|||||||
"""SMTP 'noop' command -- doesn't do anything :>"""
|
"""SMTP 'noop' command -- doesn't do anything :>"""
|
||||||
return self.docmd("noop")
|
return self.docmd("noop")
|
||||||
|
|
||||||
def mail(self,sender,options=[]):
|
def mail(self, sender, options=[]):
|
||||||
"""SMTP 'mail' command -- begins mail xfer session."""
|
"""SMTP 'mail' command -- begins mail xfer session."""
|
||||||
optionlist = ''
|
optionlist = ''
|
||||||
if options and self.does_esmtp:
|
if options and self.does_esmtp:
|
||||||
optionlist = ' ' + ' '.join(options)
|
optionlist = ' ' + ' '.join(options)
|
||||||
self.putcmd("mail", "FROM:%s%s" % (quoteaddr(sender) ,optionlist))
|
self.putcmd("mail", "FROM:%s%s" % (quoteaddr(sender), optionlist))
|
||||||
return self.getreply()
|
return self.getreply()
|
||||||
|
|
||||||
def rcpt(self,recip,options=[]):
|
def rcpt(self, recip, options=[]):
|
||||||
"""SMTP 'rcpt' command -- indicates 1 recipient for this mail."""
|
"""SMTP 'rcpt' command -- indicates 1 recipient for this mail."""
|
||||||
optionlist = ''
|
optionlist = ''
|
||||||
if options and self.does_esmtp:
|
if options and self.does_esmtp:
|
||||||
optionlist = ' ' + ' '.join(options)
|
optionlist = ' ' + ' '.join(options)
|
||||||
self.putcmd("rcpt","TO:%s%s" % (quoteaddr(recip),optionlist))
|
self.putcmd("rcpt", "TO:%s%s" % (quoteaddr(recip), optionlist))
|
||||||
return self.getreply()
|
return self.getreply()
|
||||||
|
|
||||||
def data(self,msg):
|
def data(self, msg):
|
||||||
"""SMTP 'DATA' command -- sends message data to server.
|
"""SMTP 'DATA' command -- sends message data to server.
|
||||||
|
|
||||||
Automatically quotes lines beginning with a period per rfc821.
|
Automatically quotes lines beginning with a period per rfc821.
|
||||||
@ -491,32 +510,32 @@ class SMTP:
|
|||||||
response code received when the all data is sent.
|
response code received when the all data is sent.
|
||||||
"""
|
"""
|
||||||
self.putcmd("data")
|
self.putcmd("data")
|
||||||
(code,repl)=self.getreply()
|
(code, repl) = self.getreply()
|
||||||
if self.debuglevel >0 :
|
if self.debuglevel > 0:
|
||||||
self.debug("data:", (code,repl))
|
self.debug("data:", (code, repl))
|
||||||
if code != 354:
|
if code != 354:
|
||||||
raise SMTPDataError(code,repl)
|
raise SMTPDataError(code, repl)
|
||||||
else:
|
else:
|
||||||
q = quotedata(msg)
|
q = quotedata(msg)
|
||||||
if q[-2:] != CRLF:
|
if q[-2:] != CRLF:
|
||||||
q = q + CRLF
|
q = q + CRLF
|
||||||
q = q + "." + CRLF
|
q = q + "." + CRLF
|
||||||
self.send(q)
|
self.send(q)
|
||||||
(code,msg)=self.getreply()
|
(code, msg) = self.getreply()
|
||||||
if self.debuglevel > 0 :
|
if self.debuglevel > 0 :
|
||||||
self.debug("data:", (code,msg))
|
self.debug("data:", (code, msg))
|
||||||
return (code,msg)
|
return (code, msg)
|
||||||
|
|
||||||
def verify(self, address):
|
def verify(self, address):
|
||||||
"""SMTP 'verify' command -- checks for address validity."""
|
"""SMTP 'verify' command -- checks for address validity."""
|
||||||
self.putcmd("vrfy", quoteaddr(address))
|
self.putcmd("vrfy", _addr_only(address))
|
||||||
return self.getreply()
|
return self.getreply()
|
||||||
# a.k.a.
|
# a.k.a.
|
||||||
vrfy=verify
|
vrfy = verify
|
||||||
|
|
||||||
def expn(self, address):
|
def expn(self, address):
|
||||||
"""SMTP 'expn' command -- expands a mailing list."""
|
"""SMTP 'expn' command -- expands a mailing list."""
|
||||||
self.putcmd("expn", quoteaddr(address))
|
self.putcmd("expn", _addr_only(address))
|
||||||
return self.getreply()
|
return self.getreply()
|
||||||
|
|
||||||
# some useful methods
|
# some useful methods
|
||||||
@ -725,22 +744,22 @@ class SMTP:
|
|||||||
for option in mail_options:
|
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(code, resp, from_addr)
|
raise SMTPSenderRefused(code, resp, from_addr)
|
||||||
senderrs={}
|
senderrs = {}
|
||||||
if isinstance(to_addrs, basestring):
|
if isinstance(to_addrs, basestring):
|
||||||
to_addrs = [to_addrs]
|
to_addrs = [to_addrs]
|
||||||
for each in to_addrs:
|
for each in to_addrs:
|
||||||
(code,resp)=self.rcpt(each, rcpt_options)
|
(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):
|
||||||
# the server refused all our recipients
|
# the server refused all our recipients
|
||||||
self.rset()
|
self.rset()
|
||||||
raise SMTPRecipientsRefused(senderrs)
|
raise SMTPRecipientsRefused(senderrs)
|
||||||
(code,resp) = self.data(msg)
|
(code, resp) = self.data(msg)
|
||||||
if code != 250:
|
if code != 250:
|
||||||
self.rset()
|
self.rset()
|
||||||
raise SMTPDataError(code, resp)
|
raise SMTPDataError(code, resp)
|
||||||
@ -749,16 +768,24 @@ class SMTP:
|
|||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Close the connection to the SMTP server."""
|
"""Close the connection to the SMTP server."""
|
||||||
if self.file:
|
try:
|
||||||
self.file.close()
|
file = self.file
|
||||||
self.file = None
|
self.file = None
|
||||||
if self.sock:
|
if file:
|
||||||
self.sock.close()
|
file.close()
|
||||||
|
finally:
|
||||||
|
sock = self.sock
|
||||||
self.sock = None
|
self.sock = None
|
||||||
|
if sock:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
def quit(self):
|
def quit(self):
|
||||||
"""Terminate the SMTP session."""
|
"""Terminate the SMTP session."""
|
||||||
res = self.docmd("quit")
|
res = self.docmd("quit")
|
||||||
|
# A new EHLO is required after reconnecting with connect()
|
||||||
|
self.ehlo_resp = self.helo_resp = None
|
||||||
|
self.esmtp_features = {}
|
||||||
|
self.does_esmtp = False
|
||||||
self.close()
|
self.close()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@ -772,15 +799,17 @@ if _have_ssl:
|
|||||||
are also optional - they can contain a PEM formatted private key and
|
are also optional - they can contain a PEM formatted private key and
|
||||||
certificate chain file for the SSL connection.
|
certificate chain file for the SSL connection.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
default_port = SMTP_SSL_PORT
|
||||||
|
|
||||||
def __init__(self, host='', port=0, local_hostname=None,
|
def __init__(self, host='', port=0, local_hostname=None,
|
||||||
keyfile=None, certfile=None,
|
keyfile=None, certfile=None,
|
||||||
timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
|
timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
|
||||||
debug_to=partial(print, file=sys.stderr)):
|
debug_to=partial(print, file=stderr)):
|
||||||
self.keyfile = keyfile
|
self.keyfile = keyfile
|
||||||
self.certfile = certfile
|
self.certfile = certfile
|
||||||
SMTP.__init__(self, host, port, local_hostname, timeout,
|
SMTP.__init__(self, host, port, local_hostname, timeout,
|
||||||
debug_to=debug_to)
|
debug_to=debug_to)
|
||||||
self.default_port = SMTP_SSL_PORT
|
|
||||||
|
|
||||||
def _get_socket(self, host, port, timeout):
|
def _get_socket(self, host, port, timeout):
|
||||||
if self.debuglevel > 0:
|
if self.debuglevel > 0:
|
||||||
@ -825,18 +854,40 @@ class LMTP(SMTP):
|
|||||||
try:
|
try:
|
||||||
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
self.sock.connect(host)
|
self.sock.connect(host)
|
||||||
except socket.error as msg:
|
except socket.error:
|
||||||
if self.debuglevel > 0:
|
if self.debuglevel > 0:
|
||||||
self.debug('connect fail:', host)
|
self.debug('connect fail:', host)
|
||||||
if self.sock:
|
if self.sock:
|
||||||
self.sock.close()
|
self.sock.close()
|
||||||
self.sock = None
|
self.sock = None
|
||||||
raise socket.error(msg)
|
raise
|
||||||
(code, msg) = self.getreply()
|
(code, msg) = self.getreply()
|
||||||
if self.debuglevel > 0:
|
if self.debuglevel > 0:
|
||||||
self.debug("connect:", msg)
|
self.debug("connect:", msg)
|
||||||
return (code, msg)
|
return (code, msg)
|
||||||
|
|
||||||
|
|
||||||
|
# Test the sendmail method, which tests most of the others.
|
||||||
|
# Note: This always sends to localhost.
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def prompt(prompt):
|
||||||
|
sys.stdout.write(prompt + ": ")
|
||||||
|
return sys.stdin.readline().strip()
|
||||||
|
|
||||||
|
fromaddr = prompt("From")
|
||||||
|
toaddrs = prompt("To").split(',')
|
||||||
|
print ("Enter message, end with ^D:")
|
||||||
|
msg = ''
|
||||||
|
while 1:
|
||||||
|
line = sys.stdin.readline()
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
msg = msg + line
|
||||||
|
print ("Message length is %d" % len(msg))
|
||||||
|
|
||||||
|
server = SMTP('localhost')
|
||||||
|
server.set_debuglevel(1)
|
||||||
|
server.sendmail(fromaddr, toaddrs, msg)
|
||||||
|
server.quit()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user