Package pyzmail :: Module generate
[hide private]
[frames] | no frames]

Source Code for Module pyzmail.generate

  1  # 
  2  # pyzmail/generate.py 
  3  # (c) Alain Spineux <alain.spineux@gmail.com> 
  4  # http://www.magiksys.net/pyzmail 
  5  # Released under LGPL 
  6   
  7  """ 
  8  Useful functions to compose and send emails. 
  9   
 10  For short: 
 11   
 12  >>> payload, mail_from, rcpt_to, msg_id=compose_mail((u'Me', 'me@foo.com'), 
 13  ... [(u'Him', 'him@bar.com')], u'the subject', 'iso-8859-1', ('Hello world', 'us-ascii'), 
 14  ... attachments=[('attached', 'text', 'plain', 'text.txt', 'us-ascii')]) 
 15  ... #doctest: +SKIP 
 16  >>> error=send_mail(payload, mail_from, rcpt_to, 'localhost', smtp_port=25) 
 17  ... #doctest: +SKIP 
 18  """ 
 19   
 20  import os, sys 
 21  import time 
 22  import base64 
 23  import smtplib, socket 
 24  import email 
 25  import email.encoders 
 26  import email.header 
 27  import email.utils 
 28  import email.mime 
 29  import email.mime.base 
 30  import email.mime.text 
 31  import email.mime.image 
 32  import email.mime.multipart 
 33   
 34  import utils 
 35   
36 -def format_addresses(addresses, header_name=None, charset=None):
37 """ 38 Convert a list of addresses into a MIME-compliant header for a From, To, Cc, 39 or any other I{address} related field. 40 This mixes the use of email.utils.formataddr() and email.header.Header(). 41 42 @type addresses: list 43 @param addresses: list of addresses, can be a mix of string a tuple of the form 44 C{[ 'address@domain', (u'Name', 'name@domain'), ...]}. 45 If C{u'Name'} contains non us-ascii characters, it must be a 46 unicode string or encoded using the I{charset} argument. 47 @type header_name: string or None 48 @keyword header_name: the name of the header. Its length is used to limit 49 the length of the first line of the header according the RFC's 50 requirements. (not very important, but it's better to match the 51 requirements when possible) 52 @type charset: str 53 @keyword charset: the encoding charset for non unicode I{name} and a B{hint} 54 for encoding of unicode string. In other words, 55 if the I{name} of an address in a byte string containing non 56 I{us-ascii} characters, then C{name.decode(charset)} 57 must generate the expected result. If a unicode string 58 is used instead, charset will be tried to encode the 59 string, if it fail, I{utf-8} will be used. 60 With B{Python 3.x} I{charset} is no more a hint and an exception will 61 be raised instead of using I{utf-8} has a fall back. 62 @rtype: str 63 @return: the encoded list of formated addresses separated by commas, 64 ready to use as I{Header} value. 65 66 >>> print format_addresses([('John', 'john@foo.com') ], 'From', 'us-ascii').encode() 67 John <john@foo.com> 68 >>> print format_addresses([(u'l\\xe9o', 'leo@foo.com') ], 'To', 'iso-8859-1').encode() 69 =?iso-8859-1?q?l=E9o?= <leo@foo.com> 70 >>> print format_addresses([(u'l\\xe9o', 'leo@foo.com') ], 'To', 'us-ascii').encode() 71 ... # don't work in 3.X because charset is more than a hint 72 ... #doctest: +SKIP 73 =?utf-8?q?l=C3=A9o?= <leo@foo.com> 74 >>> # because u'l\xe9o' cannot be encoded into us-ascii, utf8 is used instead 75 >>> print format_addresses([('No\\xe9', 'noe@f.com'), (u'M\u0101ori', 'maori@b.com') ], 'Cc', 'iso-8859-1').encode() 76 ... # don't work in 3.X because charset is more than a hint 77 ... #doctest: +SKIP 78 =?iso-8859-1?q?No=E9?= <noe@f.com> , =?utf-8?b?TcSBb3Jp?= <maori@b.com> 79 >>> # 'No\xe9' is already encoded into iso-8859-1, but u'M\u0101ori' cannot be encoded into iso-8859-1 80 >>> # then utf8 is used here 81 >>> print format_addresses(['a@bar.com', ('John', 'john@foo.com') ], 'From', 'us-ascii').encode() 82 a@bar.com , John <john@foo.com> 83 """ 84 header=email.header.Header(charset=charset, header_name=header_name) 85 for i, address in enumerate(addresses): 86 if i!=0: 87 # add separator between addresses 88 header.append(',', charset='us-ascii') 89 90 try: 91 name, addr=address 92 except ValueError: 93 # address is not a tuple, their is no name, only email address 94 header.append(address, charset='us-ascii') 95 else: 96 # check if address name is a unicode or byte string in "pure" us-ascii 97 if utils.is_usascii(name): 98 # name is a us-ascii byte string, i can use formataddr 99 formated_addr=email.utils.formataddr((name, addr)) 100 # us-ascii must be used and not default 'charset' 101 header.append(formated_addr, charset='us-ascii') 102 else: 103 # this is not as "pure" us-ascii string 104 # Header will use "RFC2047" to encode the address name 105 # if name is byte string, charset will be used to decode it first 106 header.append(name) 107 # here us-ascii must be used and not default 'charset' 108 header.append('<%s>' % (addr,), charset='us-ascii') 109 110 return header
111 112
113 -def build_mail(text, html=None, attachments=[], embeddeds=[]):
114 """ 115 Generate the core of the email message regarding the parameters. 116 The structure of the MIME email may vary, but the general one is as follow:: 117 118 multipart/mixed (only if attachments are included) 119 | 120 +-- multipart/related (only if embedded contents are included) 121 | | 122 | +-- multipart/alternative (only if text AND html are available) 123 | | | 124 | | +-- text/plain (text version of the message) 125 | | +-- text/html (html version of the message) 126 | | 127 | +-- image/gif (where to include embedded contents) 128 | 129 +-- application/msword (where to add attachments) 130 131 @param text: the text version of the message, under the form of a tuple: 132 C{(encoded_content, encoding)} where I{encoded_content} is a byte string 133 encoded using I{encoding}. 134 I{text} can be None if the message has no text version. 135 @type text: tuple or None 136 @keyword html: the HTML version of the message, under the form of a tuple: 137 C{(encoded_content, encoding)} where I{encoded_content} is a byte string 138 encoded using I{encoding} 139 I{html} can be None if the message has no HTML version. 140 @type html: tuple or None 141 @keyword attachments: the list of attachments to include into the mail, in the 142 form [(data, maintype, subtype, filename, charset), ..] where : 143 - I{data} : is the raw data, or a I{charset} encoded string for 'text' 144 content. 145 - I{maintype} : is a MIME main type like : 'text', 'image', 'application' .... 146 - I{subtype} : is a MIME sub type of the above I{maintype} for example : 147 'plain', 'png', 'msword' for respectively 'text/plain', 'image/png', 148 'application/msword'. 149 - I{filename} this is the filename of the attachment, it must be a 150 'us-ascii' string or a tuple of the form 151 C{(encoding, language, encoded_filename)} 152 following the RFC2231 requirement, for example 153 C{('iso-8859-1', 'fr', u'r\\xe9pertoir.png'.encode('iso-8859-1'))} 154 - I{charset} : if I{maintype} is 'text', then I{data} must be encoded 155 using this I{charset}. It can be None for non 'text' content. 156 @type attachments: list 157 @keyword embeddeds: is a list of documents embedded inside the HTML or text 158 version of the message. It is similar to the I{attachments} list, 159 but I{filename} is replaced by I{content_id} that is related to 160 the B{cid} reference into the HTML or text version of the message. 161 @type embeddeds: list 162 @rtype: inherit from email.Message 163 @return: the message in a MIME object 164 165 >>> mail=build_mail(('Hello world', 'us-ascii'), attachments=[('attached', 'text', 'plain', 'text.txt', 'us-ascii')]) 166 >>> mail.set_boundary('===limit1==') 167 >>> print mail.as_string(unixfrom=False) 168 Content-Type: multipart/mixed; boundary="===limit1==" 169 MIME-Version: 1.0 170 <BLANKLINE> 171 --===limit1== 172 Content-Type: text/plain; charset="us-ascii" 173 MIME-Version: 1.0 174 Content-Transfer-Encoding: 7bit 175 <BLANKLINE> 176 Hello world 177 --===limit1== 178 Content-Type: text/plain; charset="us-ascii" 179 MIME-Version: 1.0 180 Content-Transfer-Encoding: 7bit 181 Content-Disposition: attachment; filename="text.txt" 182 <BLANKLINE> 183 attached 184 --===limit1==-- 185 """ 186 187 main=text_part=html_part=None 188 if text: 189 content, charset=text 190 main=text_part=email.mime.text.MIMEText(content, 'plain', charset) 191 192 if html: 193 content, charset=html 194 main=html_part=email.mime.text.MIMEText(content, 'html', charset) 195 196 if not text_part and not html_part: 197 main=text_part=email.mime.text.MIMEText('', 'plain', 'us-ascii') 198 elif text_part and html_part: 199 # need to create a multipart/alternative to include text and html version 200 main=email.mime.multipart.MIMEMultipart('alternative', None, [text_part, html_part]) 201 202 if embeddeds: 203 related=email.mime.multipart.MIMEMultipart('related') 204 related.attach(main) 205 for part in embeddeds: 206 if not isinstance(part, email.mime.base.MIMEBase): 207 data, maintype, subtype, content_id, charset=part 208 if (maintype=='text'): 209 part=email.mime.text.MIMEText(data, subtype, charset) 210 else: 211 part=email.mime.base.MIMEBase(maintype, subtype) 212 part.set_payload(data) 213 email.encoders.encode_base64(part) 214 part.add_header('Content-ID', '<'+content_id+'>') 215 part.add_header('Content-Disposition', 'inline') 216 related.attach(part) 217 main=related 218 219 if attachments: 220 mixed=email.mime.multipart.MIMEMultipart('mixed') 221 mixed.attach(main) 222 for part in attachments: 223 if not isinstance(part, email.mime.base.MIMEBase): 224 data, maintype, subtype, filename, charset=part 225 if (maintype=='text'): 226 part=email.mime.text.MIMEText(data, subtype, charset) 227 else: 228 part=email.mime.base.MIMEBase(maintype, subtype) 229 part.set_payload(data) 230 email.encoders.encode_base64(part) 231 part.add_header('Content-Disposition', 'attachment', filename=filename) 232 mixed.attach(part) 233 main=mixed 234 235 return main
236
237 -def complete_mail(message, sender, recipients, subject, default_charset, cc=[], bcc=[], message_id_string=None, date=None, headers=[]):
238 """ 239 Fill in the From, To, Cc, Subject, Date and Message-Id I{headers} of 240 one existing message regarding the parameters. 241 242 @type message:email.Message 243 @param message: the message to fill in 244 @type sender: tuple 245 @param sender: a tuple of the form (u'Sender Name', 'sender.address@domain.com') 246 @type recipients: list 247 @param recipients: a list of addresses. Address can be tuple or string like 248 expected by L{format_addresses()}, for example: C{[ 'address@dmain.com', 249 (u'Recipient Name', 'recipient.address@domain.com'), ... ]} 250 @type subject: str 251 @param subject: The subject of the message, can be a unicode string or a 252 string encoded using I{default_charset} encoding. Prefert unicode to 253 byte string here. 254 @type default_charset: str 255 @param default_charset: The default charset for this email. Arguments 256 that are non unicode string are supposed to be encoded using this charset. 257 This I{charset} will be used has an hint when encoding mail content. 258 @type cc: list 259 @keyword cc: The I{carbone copy} addresses. Same format as the I{recipients} 260 argument. 261 @type bcc: list 262 @keyword bcc: The I{blind carbone copy} addresses. Same format as the I{recipients} 263 argument. 264 @type message_id_string: str or None 265 @keyword message_id_string: if None, don't append any I{Message-ID} to the 266 mail, let the SMTP do the job, else use the string to generate a unique 267 I{ID} using C{email.utils.make_msgid()}. The generated value is 268 returned as last argument. For example use the name of your application. 269 @type date: int or None 270 @keyword date: utc time in second from the epoch or None. If None then 271 use curent time C{time.time()} instead. 272 @type headers: list of tuple 273 @keyword headers: a list of C{(field, value)} tuples to fill in the mail 274 header fields. Values are encoded using I{default_charset}. 275 @rtype: tuple 276 @return: B{(payload, mail_from, rcpt_to, msg_id)} 277 - I{payload} (str) is the content of the email, generated from the message 278 - I{mail_from} (str) is the address of the sender to pass to the SMTP host 279 - I{rcpt_to} (list) is a list of the recipients addresses to pass to the SMTP host 280 of the form C{[ 'a@b.com', c@d.com', ]}. This combine all recipients, 281 I{carbone copy} addresses and I{blind carbone copy} addresses. 282 - I{msg_id} (None or str) None if message_id_string==None else the generated value for 283 the message-id. If not None, this I{Message-ID} is already written 284 into the payload. 285 286 >>> import email.mime.text 287 >>> msg=email.mime.text.MIMEText('The text.', 'plain', 'us-ascii') 288 >>> # I could use build_mail() instead 289 >>> payload, mail_from, rcpt_to, msg_id=complete_mail(msg, ('Me', 'me@foo.com'), 290 ... [ ('Him', 'him@bar.com'), ], 'Non unicode subject', 'iso-8859-1', 291 ... cc=['her@bar.com',], date=1313558269, headers=[('User-Agent', u'pyzmail'), ]) 292 >>> print payload 293 ... # 3.X encode User-Agent: using 'iso-8859-1' even if it contains only us-asccii 294 ... # doctest: +ELLIPSIS 295 Content-Type: text/plain; charset="us-ascii" 296 MIME-Version: 1.0 297 Content-Transfer-Encoding: 7bit 298 From: Me <me@foo.com> 299 To: Him <him@bar.com> 300 Cc: her@bar.com 301 Subject: =?iso-8859-1?q?Non_unicode_subject?= 302 Date: ... 303 User-Agent: ...pyzmail... 304 <BLANKLINE> 305 The text. 306 >>> print 'mail_from=%r rcpt_to=%r' % (mail_from, rcpt_to) 307 mail_from='me@foo.com' rcpt_to=['him@bar.com', 'her@bar.com'] 308 """ 309 def getaddr(address): 310 if isinstance(address, tuple): 311 return address[1] 312 else: 313 return address
314 315 mail_from=getaddr(sender[1]) 316 rcpt_to=map(getaddr, recipients) 317 rcpt_to.extend(map(getaddr, cc)) 318 rcpt_to.extend(map(getaddr, bcc)) 319 320 message['From'] = format_addresses([ sender, ], header_name='from', charset=default_charset) 321 if recipients: 322 message['To'] = format_addresses(recipients, header_name='to', charset=default_charset) 323 if cc: 324 message['Cc'] = format_addresses(cc, header_name='cc', charset=default_charset) 325 message['Subject'] = email.header.Header(subject, default_charset) 326 if date: 327 utc_from_epoch=date 328 else: 329 utc_from_epoch=time.time() 330 message['Date'] = email.utils.formatdate(utc_from_epoch, localtime=True) 331 332 if message_id_string: 333 msg_id=message['Message-Id']=email.utils.make_msgid(message_id_string) 334 else: 335 msg_id=None 336 337 for field, value in headers: 338 message[field]=email.header.Header(value, default_charset) 339 340 payload=message.as_string() 341 342 return payload, mail_from, rcpt_to, msg_id 343
344 -def compose_mail(sender, recipients, subject, default_charset, text, html=None, attachments=[], embeddeds=[], cc=[], bcc=[], message_id_string=None, date=None, headers=[]):
345 """ 346 Compose an email regarding the arguments. Call L{build_mail()} and 347 L{complete_mail()} at once. 348 349 Read the B{parameters} descriptions of both functions L{build_mail()} and L{complete_mail()}. 350 351 Returned value is the same as for L{build_mail()} and L{complete_mail()}. 352 You can pass the returned values to L{send_mail()} or L{send_mail2()}. 353 354 @rtype: tuple 355 @return: B{(payload, mail_from, rcpt_to, msg_id)} 356 357 >>> payload, mail_from, rcpt_to, msg_id=compose_mail((u'Me', 'me@foo.com'), [(u'Him', 'him@bar.com')], u'the subject', 'iso-8859-1', ('Hello world', 'us-ascii'), attachments=[('attached', 'text', 'plain', 'text.txt', 'us-ascii')]) 358 """ 359 message=build_mail(text, html, attachments, embeddeds) 360 return complete_mail(message, sender, recipients, subject, default_charset, cc, bcc, message_id_string, date, headers)
361 362
363 -def send_mail2(payload, mail_from, rcpt_to, smtp_host, smtp_port=25, smtp_mode='normal', smtp_login=None, smtp_password=None):
364 """ 365 Send the message to a SMTP host. Look at the L{send_mail()} documentation. 366 L{send_mail()} call this function and catch all exceptions to convert them 367 into a user friendly error message. The returned value 368 is always a dictionary. It can be empty if all recipients have been 369 accepted. 370 371 @rtype: dict 372 @return: This function return the value returnd by C{smtplib.SMTP.sendmail()} 373 or raise the same exceptions. 374 375 This method will return normally if the mail is accepted for at least one 376 recipient. Otherwise it will raise an exception. That is, if this 377 method does not raise an exception, then someone should get your mail. 378 If this method does not raise an exception, it returns a dictionary, 379 with one entry for each recipient that was refused. Each entry contains a 380 tuple of the SMTP error code and the accompanying error message sent by the server. 381 382 @raise smtplib.SMTPException: Look at the standard C{smtplib.SMTP.sendmail()} documentation. 383 384 """ 385 if smtp_mode=='ssl': 386 smtp=smtplib.SMTP_SSL(smtp_host, smtp_port) 387 else: 388 smtp=smtplib.SMTP(smtp_host, smtp_port) 389 if smtp_mode=='tls': 390 smtp.starttls() 391 392 if smtp_login and smtp_password: 393 if sys.version_info<(3, 0): 394 # python 2.x 395 # login and password must be encoded 396 # because HMAC used in CRAM_MD5 require non unicode string 397 smtp.login(smtp_login.encode('utf-8'), smtp_password.encode('utf-8')) 398 else: 399 #python 3.x 400 smtp.login(smtp_login, smtp_password) 401 try: 402 ret=smtp.sendmail(mail_from, rcpt_to, payload) 403 finally: 404 try: 405 smtp.quit() 406 except Exception, e: 407 pass 408 409 return ret
410
411 -def send_mail(payload, mail_from, rcpt_to, smtp_host, smtp_port=25, smtp_mode='normal', smtp_login=None, smtp_password=None):
412 """ 413 Send the message to a SMTP host. Handle SSL, TLS and authentication. 414 I{payload}, I{mail_from} and I{rcpt_to} can come from values returned by 415 L{complete_mail()}. This function call L{send_mail2()} but catch all 416 exceptions and return friendly error message instead. 417 418 @type payload: str 419 @param payload: the mail content. 420 @type mail_from: str 421 @param mail_from: the sender address, for example: C{'me@domain.com'}. 422 @type rcpt_to: list 423 @param rcpt_to: The list of the recipient addresses in the form 424 C{[ 'a@b.com', c@d.com', ]}. No names here, only email addresses. 425 @type smtp_host: str 426 @param smtp_host: the IP address or the name of the SMTP host. 427 @type smtp_port: int 428 @keyword smtp_port: the port to connect to on the SMTP host. Default is C{25}. 429 @type smtp_mode: str 430 @keyword smtp_mode: the way to connect to the SMTP host, can be: 431 C{'normal'}, C{'ssl'} or C{'tls'}. default is C{'normal'} 432 @type smtp_login: str or None 433 @keyword smtp_login: If authentication is required, this is the login. 434 Be carefull to I{UTF8} encode your login if it contains 435 non I{us-ascii} characters. 436 @type smtp_password: str or None 437 @keyword smtp_password: If authentication is required, this is the password. 438 Be carefull to I{UTF8} encode your password if it 439 contains non I{us-ascii} characters. 440 441 @rtype: dict or str 442 @return: This function return a dictionary of failed recipients 443 or a string with an error message. 444 445 If all recipients have been accepted the dictionary is empty. If the 446 returned value is a string, none of the recipients will get the message. 447 448 The dictionary is exactly of the same sort as 449 smtplib.SMTP.sendmail() returns with one entry for each recipient that 450 was refused. Each entry contains a tuple of the SMTP error code and 451 the accompanying error message sent by the server. 452 453 Example: 454 455 >>> send_mail('Subject: hello\\n\\nmessage', 'a@foo.com', [ 'b@bar.com', ], 'localhost') #doctest: +SKIP 456 {} 457 458 Here is how to use the returned value:: 459 if isinstance(ret, dict): 460 if ret: 461 print 'failed' recipients: 462 for recipient, (code, msg) in ret.iteritems(): 463 print 'code=%d recipient=%s\terror=%s' % (code, recipient, msg) 464 else: 465 print 'success' 466 else: 467 print 'Error:', ret 468 469 To use your GMail account to send your mail:: 470 smtp_host='smtp.gmail.com' 471 smtp_port=587 472 smtp_mode='tls' 473 smtp_login='your.gmail.addresse@gmail.com' 474 smtp_password='your.gmail.password' 475 476 Use your GMail address for the sender ! 477 478 """ 479 480 error=dict() 481 try: 482 ret=send_mail2(payload, mail_from, rcpt_to, smtp_host, smtp_port, smtp_mode, smtp_login, smtp_password) 483 except (socket.error, ), e: 484 error='server %s:%s not responding: %s' % (smtp_host, smtp_port, e) 485 except smtplib.SMTPAuthenticationError, e: 486 error='authentication error: %s' % (e, ) 487 except smtplib.SMTPRecipientsRefused, e: 488 # code, error=e.recipients[recipient_addr] 489 error='all recipients refused: '+', '.join(e.recipients.keys()) 490 except smtplib.SMTPSenderRefused, e: 491 # e.sender, e.smtp_code, e.smtp_error 492 error='sender refused: %s' % (e.sender, ) 493 except smtplib.SMTPDataError, e: 494 error='SMTP protocol mismatch: %s' % (e, ) 495 except smtplib.SMTPHeloError, e: 496 error="server didn't reply properly to the HELO greeting: %s" % (e, ) 497 except smtplib.SMTPException, e: 498 error='SMTP error: %s' % (e, ) 499 # except Exception, e: 500 # raise # unknown error 501 else: 502 # failed addresses and error messages 503 error=ret 504 505 return error
506