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
@@ -129,6 +129,18 @@ def __init__(self, recipients):
129129class SMTPDataError (SMTPResponseException ):
130130 """The SMTP server didn't accept the data."""
131131
132+ class LMTPDataError (SMTPResponseException ):
133+ """The LMTP server didn't accept the data.
134+
135+ The errors for each recipient are accessible through the attribute
136+ 'recipients', which is a dictionary of exactly the same sort as
137+ SMTP.sendmail() returns.
138+ """
139+
140+ def __init__ (self , recipients ):
141+ self .recipients = recipients
142+ self .args = (recipients ,)
143+
132144class SMTPConnectError (SMTPResponseException ):
133145 """Error during connection establishment."""
134146
@@ -827,6 +839,9 @@ def sendmail(self, from_addr, to_addrs, msg, mail_options=(),
827839 SMTPDataError The server replied with an unexpected
828840 error code (other than a refusal of
829841 a recipient).
842+ LMTPDataError The server replied with an unexpected
843+ error code (other than a refusal of
844+ a recipient) for ALL recipients.
830845 SMTPNotSupportedError The mail_options parameter includes 'SMTPUTF8'
831846 but the SMTPUTF8 extension is not supported by
832847 the server.
@@ -869,12 +884,15 @@ def sendmail(self, from_addr, to_addrs, msg, mail_options=(),
869884 else :
870885 self ._rset ()
871886 raise SMTPSenderRefused (code , resp , from_addr )
887+ rcpts = []
872888 senderrs = {}
873889 if isinstance (to_addrs , str ):
874890 to_addrs = [to_addrs ]
875891 for each in to_addrs :
876892 (code , resp ) = self .rcpt (each , rcpt_options )
877- if (code != 250 ) and (code != 251 ):
893+ if (code == 250 ) or (code == 251 ):
894+ rcpts .append (each )
895+ else :
878896 senderrs [each ] = (code , resp )
879897 if code == 421 :
880898 self .close ()
@@ -883,13 +901,26 @@ def sendmail(self, from_addr, to_addrs, msg, mail_options=(),
883901 # the server refused all our recipients
884902 self ._rset ()
885903 raise SMTPRecipientsRefused (senderrs )
886- (code , resp ) = self .data (msg )
887- if code != 250 :
888- if code == 421 :
889- self .close ()
890- else :
904+ if hasattr (self , 'multi_data' ):
905+ rcpt_errs_size = len (senderrs )
906+ for rcpt , code , resp in self .multi_data (msg , rcpts ):
907+ if code != 250 :
908+ senderrs [rcpt ] = (code , resp )
909+ if code == 421 :
910+ self .close ()
911+ raise LMTPDataError (senderrs )
912+ if rcpt_errs_size + len (rcpts ) == len (senderrs ):
913+ # the server refused all our recipients
891914 self ._rset ()
892- raise SMTPDataError (code , resp )
915+ raise LMTPDataError (senderrs )
916+ else :
917+ code , resp = self .data (msg )
918+ if code != 250 :
919+ if code == 421 :
920+ self .close ()
921+ else :
922+ self ._rset ()
923+ raise SMTPDataError (code , resp )
893924 #if we got here then somebody got our mail
894925 return senderrs
895926
@@ -1097,6 +1128,27 @@ def connect(self, host='localhost', port=0, source_address=None):
10971128 self ._print_debug ('connect:' , msg )
10981129 return (code , msg )
10991130
1131+ def multi_data (self , msg , rcpts ):
1132+ """SMTP 'DATA' command -- sends message data to server
1133+
1134+ Differs from data in that it yields multiple results for each
1135+ recipient. This is necessary for LMTP processing and different
1136+ from SMTP processing.
1137+
1138+ Automatically quotes lines beginning with a period per rfc821.
1139+ Raises SMTPDataError if there is an unexpected reply to the
1140+ DATA command; the return value from this method is the final
1141+ response code received when the all data is sent. If msg
1142+ is a string, lone '\\ r' and '\\ n' characters are converted to
1143+ '\\ r\\ n' characters. If msg is bytes, it is transmitted as is.
1144+ """
1145+ yield (rcpts [0 ],) + super ().data (msg )
1146+ for rcpt in rcpts [1 :]:
1147+ (code , msg ) = self .getreply ()
1148+ if self .debuglevel > 0 :
1149+ self ._print_debug ('connect:' , msg )
1150+ yield (rcpt , code , msg )
1151+
11001152
11011153# Test the sendmail method, which tests most of the others.
11021154# Note: This always sends to localhost.
0 commit comments