5555from email .base64mime import body_encode as encode_base64
5656
5757__all__ = ["SMTPException" , "SMTPNotSupportedError" , "SMTPServerDisconnected" , "SMTPResponseException" ,
58- "SMTPSenderRefused" , "SMTPRecipientsRefused" , "SMTPDataError" ,
58+ "SMTPSenderRefused" , "SMTPRecipientsRefused" , "SMTPDataError" , "LMTPDataError" ,
5959 "SMTPConnectError" , "SMTPHeloError" , "SMTPAuthenticationError" ,
6060 "quoteaddr" , "quotedata" , "SMTP" ]
6161
@@ -130,6 +130,18 @@ def __init__(self, recipients):
130130class SMTPDataError (SMTPResponseException ):
131131 """The SMTP server didn't accept the data."""
132132
133+ class LMTPDataError (SMTPResponseException ):
134+ """The LMTP server didn't accept the data.
135+
136+ The errors for each recipient are accessible through the attribute
137+ 'recipients', which is a dictionary of exactly the same sort as
138+ SMTP.sendmail() returns.
139+ """
140+
141+ def __init__ (self , recipients ):
142+ self .recipients = recipients
143+ self .args = (recipients ,)
144+
133145class SMTPConnectError (SMTPResponseException ):
134146 """Error during connection establishment."""
135147
@@ -832,6 +844,9 @@ def sendmail(self, from_addr, to_addrs, msg, mail_options=(),
832844 SMTPDataError The server replied with an unexpected
833845 error code (other than a refusal of
834846 a recipient).
847+ LMTPDataError The server replied with an unexpected
848+ error code (other than a refusal of
849+ a recipient) for ALL recipients.
835850 SMTPNotSupportedError The mail_options parameter includes 'SMTPUTF8'
836851 but the SMTPUTF8 extension is not supported by
837852 the server.
@@ -874,12 +889,15 @@ def sendmail(self, from_addr, to_addrs, msg, mail_options=(),
874889 else :
875890 self ._rset ()
876891 raise SMTPSenderRefused (code , resp , from_addr )
892+ rcpts = []
877893 senderrs = {}
878894 if isinstance (to_addrs , str ):
879895 to_addrs = [to_addrs ]
880896 for each in to_addrs :
881897 (code , resp ) = self .rcpt (each , rcpt_options )
882- if (code != 250 ) and (code != 251 ):
898+ if (code == 250 ) or (code == 251 ):
899+ rcpts .append (each )
900+ else :
883901 senderrs [each ] = (code , resp )
884902 if code == 421 :
885903 self .close ()
@@ -888,13 +906,26 @@ def sendmail(self, from_addr, to_addrs, msg, mail_options=(),
888906 # the server refused all our recipients
889907 self ._rset ()
890908 raise SMTPRecipientsRefused (senderrs )
891- (code , resp ) = self .data (msg )
892- if code != 250 :
893- if code == 421 :
894- self .close ()
895- else :
909+ if hasattr (self , 'multi_data' ):
910+ rcpt_errs_size = len (senderrs )
911+ for rcpt , code , resp in self .multi_data (msg , rcpts ):
912+ if code != 250 :
913+ senderrs [rcpt ] = (code , resp )
914+ if code == 421 :
915+ self .close ()
916+ raise LMTPDataError (senderrs )
917+ if rcpt_errs_size + len (rcpts ) == len (senderrs ):
918+ # the server refused all our recipients
896919 self ._rset ()
897- raise SMTPDataError (code , resp )
920+ raise LMTPDataError (senderrs )
921+ else :
922+ code , resp = self .data (msg )
923+ if code != 250 :
924+ if code == 421 :
925+ self .close ()
926+ else :
927+ self ._rset ()
928+ raise SMTPDataError (code , resp )
898929 #if we got here then somebody got our mail
899930 return senderrs
900931
@@ -1086,6 +1117,27 @@ def connect(self, host='localhost', port=0, source_address=None):
10861117 self ._print_debug ('connect:' , msg )
10871118 return (code , msg )
10881119
1120+ def multi_data (self , msg , rcpts ):
1121+ """SMTP 'DATA' command -- sends message data to server
1122+
1123+ Differs from data in that it yields multiple results for each
1124+ recipient. This is necessary for LMTP processing and different
1125+ from SMTP processing.
1126+
1127+ Automatically quotes lines beginning with a period per rfc821.
1128+ Raises SMTPDataError if there is an unexpected reply to the
1129+ DATA command; the return value from this method is the final
1130+ response code received when the all data is sent. If msg
1131+ is a string, lone '\\ r' and '\\ n' characters are converted to
1132+ '\\ r\\ n' characters. If msg is bytes, it is transmitted as is.
1133+ """
1134+ yield (rcpts [0 ],) + super ().data (msg )
1135+ for rcpt in rcpts [1 :]:
1136+ (code , msg ) = self .getreply ()
1137+ if self .debuglevel > 0 :
1138+ self ._print_debug ('connect:' , msg )
1139+ yield (rcpt , code , msg )
1140+
10891141
10901142# Test the sendmail method, which tests most of the others.
10911143# Note: This always sends to localhost.
0 commit comments