#!/bin/env/python # # sendmail.py # (c) Alain Spineux # http://blog.magiksys.net/sending-email-using-python-tutorial # Released under GPL # import os, sys import time import base64 import smtplib import email import email.header import email.utils import email.mime import email.mime.base import email.mime.text import email.mime.image import email.mime.multipart def format_addresses(addresses, header_name=None, charset=None): """This is an extension of email.utils.formataddr. Function expect a list of addresses [ ('name', 'name@domain'), ...]. The len(header_name) is used to limit first line length. The function mix the use Header(), formataddr() and check for 'us-ascii' string to have valid and friendly 'address' header. If one 'name' is not unicode string, then it must encoded using 'charset', Header will use 'charset' to decode it. Unicode string will be encoded following the "Header" rules : ( try first using ascii, then 'charset', then 'uft8') 'name@address' is supposed to be pure us-ascii, it can be unicode string or not (but cannot contains non us-ascii) In short Header() ignore syntax rules about 'address' field, and formataddr() ignore encoding of non us-ascci chars. """ header=email.header.Header(charset=charset, header_name=header_name) for i, (name, addr) in enumerate(addresses): if i!=0: # add separator between addresses header.append(',', charset='us-ascii') # check if address name is a unicode or byte string in "pure" us-ascii try: if isinstance(name, unicode): # convert name in byte string name=name.encode('us-ascii') else: # check id byte string contains only us-ascii chars name.decode('us-ascii') except UnicodeError: # Header will use "RFC2047" to encode the address name # if name is byte string, charset will be used to decode it first header.append(name) # here us-ascii must be used and not default 'charset' header.append('<%s>' % (addr,), charset='us-ascii') else: # name is a us-ascii byte string, i can use formataddr formated_addr=email.utils.formataddr((name, addr)) # us-ascii must be used and not default 'charset' header.append(formated_addr, charset='us-ascii') return header def gen_mail(text, html=None, attachments=[], relateds=[]): """generate the core of the email message. text=(encoded_content, encoding) html=(encoded_content, encoding) attachments=[(data, maintype, subtype, filename, charset), ..] if maintype is 'text', data lmust be encoded using charset filename can be us-ascii or (charset, lang, encoded_filename) where encoded_filename is encoded into charset, lang can be empty but usually the language related to the charset. relateds=[(data, maintype, subtype, content_id, charset), ..] idem attachment above, but content_id is related to the "CID" reference in the html version of the message. """ main=text_part=html_part=None if text: content, charset=text main=text_part=email.mime.text.MIMEText(content, 'plain', charset) if html: content, charset=html main=html_part=email.mime.text.MIMEText(content, 'html', charset) if not text_part and not html_part: main=text_part=email.mime.text.MIMEText('', 'plain', 'us-ascii') elif text_part and html_part: # need to create a multipart/alternative to include text and html version main=email.mime.multipart.MIMEMultipart('alternative', None, [text_part, html_part]) if relateds: related=email.mime.multipart.MIMEMultipart('related') related.attach(main) for part in relateds: if not isinstance(part, email.mime.base.MIMEBase): data, maintype, subtype, content_id, charset=part if (maintype=='text'): part=email.mime.text.MIMEText(data, subtype, charset) else: part=email.mime.base.MIMEBase(maintype, subtype) part.set_payload(data) email.Encoders.encode_base64(part) part.add_header('Content-ID', '<'+content_id+'>') part.add_header('Content-Disposition', 'inline') related.attach(part) main=related if attachments: mixed=email.mime.multipart.MIMEMultipart('mixed') mixed.attach(main) for part in attachments: if not isinstance(part, email.mime.base.MIMEBase): data, maintype, subtype, filename, charset=part if (maintype=='text'): part=email.mime.text.MIMEText(data, subtype, charset) else: part=email.mime.base.MIMEBase(maintype, subtype) part.set_payload(data) email.Encoders.encode_base64(part) part.add_header('Content-Disposition', 'attachment', filename=filename) mixed.attach(part) main=mixed return main def send_mail(smtp_host, sender, recipients, subject, msg, default_charset, cc=[], bcc=[], smtp_port=25, smtp_mode='normal', smtp_login=None, smtp_password=None, message_id_string=None): """ """ mail_from=sender[1] rcpt_to=map(lambda x:x[1], recipients) rcpt_to.extend(map(lambda x:x[1], cc)) rcpt_to.extend(map(lambda x:x[1], bcc)) msg['From'] = format_addresses([ sender, ], header_name='from', charset=default_charset) msg['To'] = format_addresses(recipients, header_name='to', charset=default_charset) msg['Cc'] = format_addresses(cc, header_name='cc', charset=default_charset) msg['Subject'] = email.header.Header(subject, default_charset) utc_from_epoch=time.time() msg['Date'] = email.utils.formatdate(utc_from_epoch, localtime=True) msg['Messsage-Id'] =email.utils.make_msgid(message_id_string) # Send the message errmsg='' failed_addresses=[] try: if smtp_mode=='ssl': smtp=smtplib.SMTP_SSL(smtp_host, smtp_port) else: smtp=smtplib.SMTP(smtp_host, smtp_port) if smtp_mode=='tls': smtp.starttls() if smtp_login and smtp_password: # login and password must be encoded # because HMAC used in CRAM_MD5 require non unicode string smtp.login(smtp_login.encode('utf-8'), smtp_password.encode('utf-8')) ret=smtp.sendmail(mail_from, rcpt_to, msg.as_string()) smtp.quit() except (socket.error, ), e: errmsg='server %s:%s not responding: %s' % (smtp_host, smtp_port, e) except smtplib.SMTPAuthenticationError, e: errmsg='authentication error: %s' % (e, ) except smtplib.SMTPRecipientsRefused, e: # code, errmsg=e.recipients[recipient_addr] errmsg='recipients refused: '+', '.join(e.recipients.keys()) except smtplib.SMTPSenderRefused, e: # e.sender, e.smtp_code, e.smtp_error errmsg='sender refused: %s' % (e.sender, ) except smtplib.SMTPDataError, e: errmsg='SMTP protocol mismatch: %s' % (e, ) except smtplib.SMTPHeloError, e: errmsg="server didn't reply properly to the HELO greeting: %s" % (e, ) except smtplib.SMTPException, e: errmsg='SMTP error: %s' % (e, ) except Exception, e: errmsg=str(e) else: if ret: failed_addresses=ret.keys() errmsg='recipients refused: '+', '.join(failed_addresses) return msg, errmsg, failed_addresses if __name__ == "__main__": smtp_host='max' smtp_port=25 if False: smtp_host='smtp.gmail.com' smtp_port='587' smtp_login='your.addresse@gmail.com' smtp_passwd='your.password' sender=(u'Ala\xefn Spineux', 'alain.spineux@gmail.com') sender=(u'Alain Spineux', u'alain.spineux@gmail.com') root_addr='root@max.asxnet.loc' recipients=[ ('Alain Spineux', root_addr), # (u'Alain Spineux', root_addr), # ('Dr. Alain Spneux', root_addr), # (u'Dr. Alain Spneux', root_addr), # (u'Dr. Ala\xefn Spineux', root_addr), # (u'Dr. Ala\xefn Spineux', root_addr), # ('us_ascii_name_with_a_space_some_where_in_the_middle to_allow_python_Header._split()_to_split_according_RFC2822', root_addr), # (u'This-is-a-very-long-unicode-name-with-one-non-ascii-char-\xf4-to-force-Header()-to-use-RFC2047-encoding-and-split-in-multi-line', root_addr), # ('this_line_is_too_long_and_dont_have_any_white_space_to_allow_Header._split()_to_split_according_RFC2822', root_addr), # ('Alain Spineux', root_addr), ] smile_png=base64.b64decode( """iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOBAMAAADtZjDiAAAAMFBMVEUQEAhaUjlaWlp7e3uMezGU hDGcnJy1lCnGvVretTnn5+/3pSn33mP355T39+//75SdwkyMAAAACXBIWXMAAA7EAAAOxAGVKw4b AAAAB3RJTUUH2wcJDxEjgefAiQAAAAd0RVh0QXV0aG9yAKmuzEgAAAAMdEVYdERlc2NyaXB0aW9u ABMJISMAAAAKdEVYdENvcHlyaWdodACsD8w6AAAADnRFWHRDcmVhdGlvbiB0aW1lADX3DwkAAAAJ dEVYdFNvZnR3YXJlAF1w/zoAAAALdEVYdERpc2NsYWltZXIAt8C0jwAAAAh0RVh0V2FybmluZwDA G+aHAAAAB3RFWHRTb3VyY2UA9f+D6wAAAAh0RVh0Q29tbWVudAD2zJa/AAAABnRFWHRUaXRsZQCo 7tInAAAAaElEQVR4nGNYsXv3zt27TzHcPup6XDBmDsOeBvYzLTynGfacuHfm/x8gfS7tbtobEM3w n2E9kP5n9N/oPZA+//7PP5D8GSCYA6RPzjlzEkSfmTlz+xkgffbkzDlAuvsMWAHDmt0g0AUAmyNE wLAIvcgAAAAASUVORK5CYII= """) angry_gif=base64.b64decode( """R0lGODlhDgAOALMAAAwMCYAAAACAAKaCIwAAgIAAgACAgPbTfoR/YP8AAAD/AAAA//rMUf8A/wD/ //Tw5CH5BAAAAAAALAAAAAAOAA4AgwwMCYAAAACAAKaCIwAAgIAAgACAgPbTfoR/YP8AAAD/AAAA //rMUf8A/wD///Tw5AQ28B1Gqz3S6jop2sxnAYNGaghAHirQUZh6sEDGPQgy5/b9UI+eZkAkghhG ZPLIbMKcDMwLhIkAADs= """) pingu_png=base64.b64decode( """iVBORw0KGgoAAAANSUhEUgAAABoAAAATBAMAAAB8awA1AAAAMFBMVEUQGCE5OUJKa3tSUlJSrdZj xu9rWjl7e4SljDGlnHutnFK9vbXGxsbWrTHW1tbv7+88a/HUAAAACXBIWXMAAA7EAAAOxAGVKw4b AAAAB3RJTUUH2wgJDw8mp5ycCAAAAAd0RVh0QXV0aG9yAKmuzEgAAAAMdEVYdERlc2NyaXB0aW9u ABMJISMAAAAKdEVYdENvcHlyaWdodACsD8w6AAAADnRFWHRDcmVhdGlvbiB0aW1lADX3DwkAAAAJ dEVYdFNvZnR3YXJlAF1w/zoAAAALdEVYdERpc2NsYWltZXIAt8C0jwAAAAh0RVh0V2FybmluZwDA G+aHAAAAB3RFWHRTb3VyY2UA9f+D6wAAAAh0RVh0Q29tbWVudAD2zJa/AAAABnRFWHRUaXRsZQCo 7tInAAAA0klEQVR4nE2OsYrCUBBFJ42woOhuq43f4IfYmD5fsO2yWNhbCFZ21ovgFyQYG1FISCxt Xi8k3KnFx8xOTON0Z86d4ZKKiNowM5QEUD6FU9uCSWwpYThrSQuj2fjLso2DqB9OBqqvJFiVllHa usJedty1NFe2brRbs7ny5aIP8dSXukmyUABQ0CR9AU9d1IOO1EZgjg7n+XEBpOQP0E/rUz2Rlw09 Amte7bdVSs/s7py5i1vFRsnFuW+gdysSu4vzv9vm57faJuY0ywFhAFmupO/zDzDcxlhVE/gbAAAA AElFTkSuQmCC """) text_utf8="""This is the the text part. With a related picture: cid:smile.png and related document: cid:related.txt Bonne journ\xc3\xa9ee. """ utext=u"""This is the the text part. With a related picture: cid:smile.png and related document: cid:related.txt Bonne journ\xe9e. """ data_text=u'Text en Fran\xe7ais' related_text=u'Document relatif en Fran\xe7ais' html=""" This is the html part with a related picture: and related document: here
Bonne journée. """ relateds=[ (smile_png, 'image', 'png', 'smile.png', None), (related_text.encode('iso-8859-1'), 'text', 'plain', 'related.txt', 'iso-8859-1'), ] pingu_att=email.mime.image.MIMEImage(pingu_png, 'png') pingu_att.add_header('Content-Disposition', 'attachment', filename=('iso-8859-1', 'fr', u'ping\xfc.png'.encode('iso-8859-1'))) pingu_att2=email.mime.image.MIMEImage(pingu_png, 'png') pingu_att2.add_header('Content-Disposition', 'attachment', filename='pingu.png') attachments=[ (angry_gif, 'image', 'gif', ('iso-8859-1', 'fr', u'\xe4ngry.gif'.encode('iso-8859-1')), None), (angry_gif, 'image', 'gif', 'angry.gif', None), (data_text.encode('iso-8859-1'), 'text', 'plain', 'document.txt', 'iso-8859-1'), pingu_att, pingu_att2] mail=gen_mail((utext.encode('iso-8859-1'), 'iso-8859-1'), (html, 'us-ascii'), attachments, relateds) msg, errmsg, failed_addresses=send_mail(smtp_host, \ sender, \ recipients, \ u'My Subject', \ mail, \ default_charset='iso-8859-1', cc=[('Gama', root_addr), ], bcc=[('Colombus', root_addr), ], smtp_port=smtp_port, ) print msg print errmsg print failed_addresses