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

Skip to content

Commit 296e143

Browse files
committed
Changes by Per Cederquist and The Dragon.
Per writes: """ The application where Signum Support uses smtplib needs to be able to report good error messages to the user when sending email fails. To help in diagnosing problems it is useful to be able to report the entire message sent by the server, not only the SMTP error code of the offending command. A lot of the functions in sendmail.py unfortunately discards the message, leaving only the code. The enclosed patch fixes that problem. The enclosed patch also introduces a base class for exceptions that include an SMTP error code and error message, and make the code and message available on separate attributes, so that surrounding code can deal with them in whatever way it sees fit. I've also added some documentation to the exception classes. The constructor will now raise an exception if it cannot connect to the SMTP server. The data() method will raise an SMTPDataError if it doesn't receive the expected 354 code in the middle of the exchange. According to section 5.2.10 of RFC 1123 a smtp client must accept "any text, including no text at all" after the error code. If the response of a HELO command contains no text self.helo_resp will be set to the empty string (""). The patch fixes the test in the sendmail() method so that helo_resp is tested against None; if it has the empty string as value the sendmail() method would invoke the helo() method again. The code no longer accepts a -1 reply from the ehlo() method in sendmail(). [Text about removing SMTPRecipientsRefused deleted --GvR] """ and also: """ smtplib.py appends an extra blank line to the outgoing mail if the `msg' argument to the sendmail method already contains a trailing newline. This patch should fix the problem. """ The Dragon writes: """ Mostly I just re-added the SMTPRecipientsRefused exception (the exeption object now has the appropriate info in it ) [Per had removed this in his patch --GvR] and tweaked the behavior of the sendmail method whence it throws the newly added SMTPHeloException (it was closing the connection, which it shouldn't. whatever catches the exception should do that. ) I pondered the change of the return values to tuples all around, and after some thinking I decided that regularizing the return values was too much of the Right Thing (tm) to not do. My one concern is that code expecting an integer & getting a tuple may fail silently. (i.e. if it's doing : x.somemethod() >= 400: expecting an integer, the expression will always be true if it gets a tuple instead. ) However, most smtplib code I've seen only really uses the sendmail() method, so this wouldn't bother it. Usually code I've seen that calls the other methods usually only calls helo() and ehlo() for doing ESMTP, a feature which was not in the smtplib included with 1.5.1, and thus I would think not much code uses it yet. """
1 parent 630a9a6 commit 296e143

1 file changed

Lines changed: 118 additions & 49 deletions

File tree

Lib/smtplib.py

Lines changed: 118 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,65 @@
4848
SMTP_PORT = 25
4949
CRLF="\r\n"
5050

51-
# used for exceptions
52-
class SMTPException(Exception): pass
53-
class SMTPServerDisconnected(SMTPException): pass
54-
class SMTPSenderRefused(SMTPException): pass
55-
class SMTPRecipientsRefused(SMTPException): pass
56-
class SMTPDataError(SMTPException): pass
51+
# Exception classes used by this module.
52+
class SMTPException(Exception):
53+
"""Base class for all exceptions raised by this module."""
54+
55+
class SMTPServerDisconnected(SMTPException):
56+
"""Not connected to any SMTP server.
57+
58+
This exception is raised when the server unexpectedly disconnects,
59+
or when an attempt is made to use the SMTP instance before
60+
connecting it to a server.
61+
"""
62+
63+
class SMTPResponseException(SMTPException):
64+
"""Base class for all exceptions that include an SMTP error code.
65+
66+
These exceptions are generated in some instances when the SMTP
67+
server returns an error code. The error code is stored in the
68+
`smtp_code' attribute of the error, and the `smtp_error' attribute
69+
is set to the error message.
70+
"""
71+
72+
def __init__(self, code, msg):
73+
self.smtp_code = code
74+
self.smtp_error = msg
75+
self.args = (code, msg)
76+
77+
class SMTPSenderRefused(SMTPResponseException):
78+
"""Sender address refused.
79+
In addition to the attributes set by on all SMTPResponseException
80+
exceptions, this sets `sender' to the string that the SMTP refused
81+
"""
82+
83+
def __init__(self, code, msg, sender):
84+
self.smtp_code = code
85+
self.smtp_error = msg
86+
self.sender = sender
87+
self.args = (code, msg, sender)
88+
89+
class SMTPRecipientsRefused(SMTPResponseException):
90+
"""All recipient addresses refused.
91+
The errors for each recipient are accessable thru the attribute
92+
'recipients', which is a dictionary of exactly the same sort as
93+
SMTP.sendmail() returns.
94+
"""
95+
96+
def __init__(self, recipients):
97+
self.recipients = recipients
98+
self.args = ( recipients,)
99+
100+
101+
102+
class SMTPDataError(SMTPResponseException):
103+
"""The SMTP server didn't accept the data."""
104+
105+
class SMTPConnectError(SMTPResponseException):
106+
"""Error during connection establishment"""
107+
108+
class SMTPHeloError(SMTPResponseException):
109+
"""The server refused our HELO reply"""
57110

58111
def quoteaddr(addr):
59112
"""Quote a subset of the email addresses defined by RFC 821.
@@ -120,11 +173,15 @@ def __init__(self, host = '', port = 0):
120173
121174
If specified, `host' is the name of the remote host to which to
122175
connect. If specified, `port' specifies the port to which to connect.
123-
By default, smtplib.SMTP_PORT is used.
176+
By default, smtplib.SMTP_PORT is used. An SMTPConnectError is
177+
raised if the specified `host' doesn't respond correctly.
124178
125179
"""
126180
self.esmtp_features = {}
127-
if host: self.connect(host, port)
181+
if host:
182+
(code, msg) = self.connect(host, port)
183+
if code != 220:
184+
raise SMTPConnectError(code, msg)
128185

129186
def set_debuglevel(self, debuglevel):
130187
"""Set the debug output level.
@@ -159,7 +216,7 @@ def connect(self, host='localhost', port = 0):
159216
self.sock.connect(host, port)
160217
(code,msg)=self.getreply()
161218
if self.debuglevel >0 : print "connect:", msg
162-
return msg
219+
return (code,msg)
163220

164221
def send(self, str):
165222
"""Send `str' to the server."""
@@ -191,23 +248,23 @@ def getreply(self):
191248
Raises SMTPServerDisconnected if end-of-file is reached.
192249
"""
193250
resp=[]
194-
if self.file is None:
195-
self.file = self.sock.makefile('rb')
251+
if self.file is None:
252+
self.file = self.sock.makefile('rb')
196253
while 1:
197254
line = self.file.readline()
198-
if line == '':
199-
self.close()
200-
raise SMTPServerDisconnected("Connection unexpectedly closed")
255+
if line == '':
256+
self.close()
257+
raise SMTPServerDisconnected("Connection unexpectedly closed")
201258
if self.debuglevel > 0: print 'reply:', `line`
202259
resp.append(string.strip(line[4:]))
203260
code=line[:3]
204-
# Check that the error code is syntactically correct.
205-
# Don't attempt to read a continuation line if it is broken.
206-
try:
207-
errcode = string.atoi(code)
208-
except ValueError:
209-
errcode = -1
210-
break
261+
# Check that the error code is syntactically correct.
262+
# Don't attempt to read a continuation line if it is broken.
263+
try:
264+
errcode = string.atoi(code)
265+
except ValueError:
266+
errcode = -1
267+
break
211268
# Check if multiline response.
212269
if line[3:4]!="-":
213270
break
@@ -220,8 +277,7 @@ def getreply(self):
220277
def docmd(self, cmd, args=""):
221278
"""Send a command, and return its response code."""
222279
self.putcmd(cmd,args)
223-
(code,msg)=self.getreply()
224-
return code
280+
return self.getreply()
225281

226282
# std smtp commands
227283
def helo(self, name=''):
@@ -235,7 +291,7 @@ def helo(self, name=''):
235291
self.putcmd("helo",name)
236292
(code,msg)=self.getreply()
237293
self.helo_resp=msg
238-
return code
294+
return (code,msg)
239295

240296
def ehlo(self, name=''):
241297
""" SMTP 'ehlo' command.
@@ -254,7 +310,7 @@ def ehlo(self, name=''):
254310
raise SMTPServerDisconnected("Server not connected")
255311
self.ehlo_resp=msg
256312
if code<>250:
257-
return code
313+
return (code,msg)
258314
self.does_esmtp=1
259315
#parse the ehlo responce -ddm
260316
resp=string.split(self.ehlo_resp,'\n')
@@ -265,7 +321,7 @@ def ehlo(self, name=''):
265321
feature=string.lower(m.group("feature"))
266322
params=string.strip(m.string[m.end("feature"):])
267323
self.esmtp_features[feature]=params
268-
return code
324+
return (code,msg)
269325

270326
def has_extn(self, opt):
271327
"""Does the server support a given SMTP service extension?"""
@@ -275,18 +331,15 @@ def help(self, args=''):
275331
"""SMTP 'help' command.
276332
Returns help text from server."""
277333
self.putcmd("help", args)
278-
(code,msg)=self.getreply()
279-
return msg
334+
return self.getreply()
280335

281336
def rset(self):
282337
"""SMTP 'rset' command -- resets session."""
283-
code=self.docmd("rset")
284-
return code
338+
return self.docmd("rset")
285339

286340
def noop(self):
287341
"""SMTP 'noop' command -- doesn't do anything :>"""
288-
code=self.docmd("noop")
289-
return code
342+
return self.docmd("noop")
290343

291344
def mail(self,sender,options=[]):
292345
"""SMTP 'mail' command -- begins mail xfer session."""
@@ -306,19 +359,23 @@ def rcpt(self,recip,options=[]):
306359

307360
def data(self,msg):
308361
"""SMTP 'DATA' command -- sends message data to server.
362+
309363
Automatically quotes lines beginning with a period per rfc821.
364+
Raises SMTPDataError if there is an unexpected reply to the
365+
DATA command; the return value from this method is the final
366+
response code received when the all data is sent.
310367
"""
311368
self.putcmd("data")
312369
(code,repl)=self.getreply()
313370
if self.debuglevel >0 : print "data:", (code,repl)
314371
if code <> 354:
315-
return -1
372+
raise SMTPDataError(code,repl)
316373
else:
317374
self.send(quotedata(msg))
318375
self.send("%s.%s" % (CRLF, CRLF))
319376
(code,msg)=self.getreply()
320377
if self.debuglevel >0 : print "data:", (code,msg)
321-
return code
378+
return (code,msg)
322379

323380
def verify(self, address):
324381
"""SMTP 'verify' command -- checks for address validity."""
@@ -353,11 +410,23 @@ def sendmail(self, from_addr, to_addrs, msg, mail_options=[],
353410
fails, HELO will be tried and ESMTP options suppressed.
354411
355412
This method will return normally if the mail is accepted for at least
356-
one recipient. Otherwise it will throw an exception (either
357-
SMTPSenderRefused, SMTPRecipientsRefused, or SMTPDataError) That is,
358-
if this method does not throw an exception, then someone should get
359-
your mail. If this method does not throw an exception, it returns a
360-
dictionary, with one entry for each recipient that was refused.
413+
one recipient. It returns a dictionary, with one entry for each
414+
recipient that was refused. Each entry contains a tuple of
415+
the SMTP error code and the accompanying error message sent by
416+
the server.
417+
418+
This method may raise the following exceptions:
419+
420+
SMTPHeloError The server didn't reply properly to
421+
the helo greeting.
422+
SMTPRecipientsRefused The server rejected for ALL recipients
423+
(no mail was sent).
424+
SMTPSenderRefused The server didn't accept the from_addr.
425+
SMTPDataError The server replied with an unexpected
426+
error code (other than a refusal of
427+
a recipient).
428+
429+
Note: the connection will be open even after an exception is raised.
361430
362431
Example:
363432
@@ -379,9 +448,11 @@ def sendmail(self, from_addr, to_addrs, msg, mail_options=[],
379448
empty dictionary.
380449
381450
"""
382-
if not self.helo_resp and not self.ehlo_resp:
383-
if self.ehlo() >= 400:
384-
self.helo()
451+
if self.helo_resp is None and self.ehlo_resp is None:
452+
if not (200 <= self.ehlo()[0] <= 299):
453+
(code,resp) = self.helo()
454+
if not (200 <= code <= 299):
455+
raise SMTPHeloError(code, resp)
385456
esmtp_opts = []
386457
if self.does_esmtp:
387458
# Hmmm? what's this? -ddm
@@ -394,7 +465,7 @@ def sendmail(self, from_addr, to_addrs, msg, mail_options=[],
394465
(code,resp) = self.mail(from_addr, esmtp_opts)
395466
if code <> 250:
396467
self.rset()
397-
raise SMTPSenderRefused('%s: %s' % (from_addr, resp))
468+
raise SMTPSenderRefused(code, resp, from_addr)
398469
senderrs={}
399470
if type(to_addrs) == types.StringType:
400471
to_addrs = [to_addrs]
@@ -405,13 +476,11 @@ def sendmail(self, from_addr, to_addrs, msg, mail_options=[],
405476
if len(senderrs)==len(to_addrs):
406477
# the server refused all our recipients
407478
self.rset()
408-
raise SMTPRecipientsRefused(string.join(
409-
map(lambda x:"%s: %s" % (x[0], x[1][1]), senderrs.items()),
410-
'; '))
411-
code=self.data(msg)
412-
if code <>250 :
479+
raise SMTPRecipientsRefused(senderrs)
480+
(code,resp)=self.data(msg)
481+
if code <> 250:
413482
self.rset()
414-
raise SMTPDataError('data transmission error: %s' % code)
483+
raise SMTPDataError(code, resp)
415484
#if we got here then somebody got our mail
416485
return senderrs
417486

0 commit comments

Comments
 (0)