[modificaton le 23/2/2015: ajout du code en Python 3]
J'avais un problème très concret à résoudre. J'ai en charge l'informatique d'un concours photo international, et je suis amené à envoyer des emails à une liste d'adresses extraite d'une base de données. La quantité d'emails à envoyer est d'au moins 400, et ceci dans plus de 35 pays situés dans le monde entier. Certains de ces emails sont avec pièces jointes et d'autres non. Certaines de ces pièces jointes sont spécifiques au destinataire (par exemple le résultat au concours), et d'autre sont des documents généraux.
Bref, je ne me voyais pas envoyer autant d'email avec mon Outlook, et j'ai donc cherché une solution de programmation avec Python.
Je dois dire que j'ai eu du mal. Même avec google, j'ai trouvé peu d'exemples, et souvent des exemples partiels et/ou dépassés. Et la doc officielle Python est assez “rustique” sur le sujet. Quand à la norme officielle sur le protocole SMTP, elle est comme toutes les normes …
Bref, voilà mon code qui me permet de faire à peu près ce que je veux! J'ai même fait ensuite un programme complet pour tous les e-mailings de mon association, mais je ne le diffuse pas, parce qu'il est trop facile d'envoyer des spams avec…
Pour simplifier le code, on va supposer que les adresses sont données sous forme de liste de chaines, et qu'elles ont été vérifiées avant (voir sur ce site les fonctions de vérification de syntaxe des adresses mail)
Il y a 3 parties importantes:
L'avantage d'avoir les éléments du compte dans la classe “ServeurSMTP()” est qu'on peut avoir plusieurs comptes/serveurs, référencés dans une liste, et choisir le compte/serveur à utiliser au dernier moment en donnant son indice dans la liste. On pourra aussi ajouter par la suite des drapeaux supplémentaires pour certains serveurs (cryptage SSL par exemple) sans trop modifier le code global.
Ce code traite les 4 cas suivants:
Après le code, on trouvera ces 4 utilisations détaillées à titre d'exemple
D'abord le code sous Python 2.7:
#!/usr/bin/python # -*- coding: utf-8 -*- # Python 2.7 import sys import os from smtplib import SMTP from email import Encoders from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email.mime.base import MIMEBase from email.utils import formatdate ############################################################################## class ServeurSMTP(object): def __init__(self, adresse="", port=25, login="", mdpasse=""): """conserve les paramètres d'un compte mail sur un serveur SMTP""" self.adresse = adresse self.port = port self.login = login self.mdpasse = mdpasse ############################################################################## class MessageSMTP(object): def __init__(self, exped="", to=[], cc=[], bcc=[], sujet="", corps="", pjointes=[], codage='UTF-8', typetexte='plain'): """fabrique un mail empaqueté correctement à partir des données détaillées""" # préparation des données self.expediteur = exped if type(pjointes)==str or type(pjointes)==unicode: pjointes = pjointes.split(';') if codage==None or codage=="": codage = 'UTF-8' if to==[] or to==['']: self.destinataires = [] self.mail = "" raise ValueError ("échec: pas de destinataire!") # construction du mail à envoyer (en-tête + corps) if pjointes==[]: # message sans pièce jointe msg = MIMEText(corps.encode(codage), typetexte, _charset=codage) else: # message "multipart" avec une ou plusieurs pièce(s) jointe(s) msg = MIMEMultipart('alternatives') msg['From'] = exped msg['To'] = ', '.join(to) msg['Cc'] = ', '.join(cc) msg['Bcc'] = ', '.join(bcc) msg['Date'] = formatdate(localtime=True) msg['Subject'] = sujet.encode(codage) msg['Charset'] = codage msg['Content-Type'] = 'text/' + typetexte + '; charset=' + codage if pjointes!=[]: msg.attach(MIMEText(corps.encode(codage), typetexte, _charset=codage)) # ajout des pièces jointes for fichier in pjointes: part = MIMEBase('application', "octet-stream") try: f = open(fichier,"rb") part.set_payload(f.read()) f.close() except: coderr = "%s" % sys.exc_info()[1] raise ValueError ("échec à la lecture d'un fichier joint (" + coderr + ")") Encoders.encode_base64(part) part.add_header('Content-Disposition', 'attachment', filename="%s" % os.path.basename(fichier)) msg.attach(part) # compactage final du message dans les 2 cas (avec ou sans pièce(s) jointe(s)) self.mail = msg.as_string() # construction de la liste complète de tous les destinataires (to + cc + bcc) self.destinataires = to self.destinataires.extend(cc) self.destinataires.extend(bcc) ############################################################################## def envoieSMTP(message, serveur): """envoie le message correctement compacté au serveur SMTP donné""" try: smtp = SMTP(serveur.adresse, serveur.port) except: coderr = "%s" % sys.exc_info()[1] return u"échec à la connexion (" + coderr + ")" # smtp.set_debuglevel(1) # à décommenter pour avoir tous les échanges du protocole dans la console if serveur.login != "": try: smtp.login(serveur.login, serveur.mdpasse) except: coderr = "%s" % sys.exc_info()[1] smtp.quit() return u"échec: mauvais login/mdpasse (" + coderr + ")" try: rep = smtp.sendmail(message.expediteur, message.destinataires, message.mail) except: coderr = "%s" % sys.exc_info()[1] smtp.quit() return u"échec à l'envoi de mail (" + coderr + ")" smtp.quit() return rep
Et voici le même code en Python 3, et un peu amélioré:
#!/usr/bin/python3 # -*- coding: utf-8 -*- # Python 3 import sys import os from smtplib import SMTP from email import encoders from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email.mime.base import MIMEBase from email.utils import formatdate ############################################################################## class ServeurSMTP(object): def __init__(self, adresse="", port=25, login="", mdpasse=""): """conserve les paramètres d'un compte mail sur un serveur SMTP""" self.adresse = adresse self.port = port self.login = login self.mdpasse = mdpasse ############################################################################## class MessageSMTP(object): def __init__(self, exped="", to=(), cc=(), bcc=(), sujet="", corps="", pjointes=(), codage='UTF-8', typetexte='plain'): """fabrique un mail empaqueté correctement à partir des données détaillées""" #--------------------------------------------------------------------- # correction des arguments selon leur type self.expediteur = exped if isinstance(to, str): to = to.split(';') if to == [] or to == ['']: raise ValueError("échec: pas de destinataire!") if isinstance(cc, str): cc = cc.split(';') if isinstance(bcc, str): bcc = bcc.split(';') if isinstance(pjointes, str): pjointes = pjointes.split(';') if codage == None or codage == "": codage = 'UTF-8' #--------------------------------------------------------------------- # construction du mail à envoyer (en-tête + corps) if pjointes == []: # message sans pièce jointe msg = MIMEText(corps, typetexte, _charset=codage) else: # message "multipart" avec une ou plusieurs pièce(s) jointe(s) msg = MIMEMultipart('alternatives') msg['From'] = exped msg['To'] = ', '.join(to) msg['Cc'] = ', '.join(cc) msg['Bcc'] = ', '.join(bcc) msg['Date'] = formatdate(localtime=True) msg['Subject'] = sujet msg['Charset'] = codage msg['Content-Type'] = 'text/' + typetexte + '; charset=' + codage if pjointes != []: msg.attach(MIMEText(corps, typetexte, _charset=codage)) # ajout des pièces jointes for fichier in pjointes: part = MIMEBase('application', "octet-stream") try: with open(fichier, "rb") as f: part.set_payload(f.read()) except Exception as msgerr: raise ValueError ("échec à la lecture d'un fichier joint (" + msgerr + ")") encoders.encode_base64(part) part.add_header('Content-Disposition', 'attachment', filename="%s" % (os.path.basename(fichier),)) msg.attach(part) # compactage final du message dans les 2 cas (avec ou sans pièce(s) jointe(s)) self.mail = msg.as_string() # construction de la liste complète de tous les destinataires (to + cc + bcc) self.destinataires = to self.destinataires.extend(cc) self.destinataires.extend(bcc) ############################################################################## def envoieSMTP(message, serveur): """envoie le message correctement compacté au serveur SMTP donné""" #========================================================================= # connexion au serveur SMTP smtp = None try: smtp = SMTP(serveur.adresse, serveur.port) except Exception as msgerr: if smtp != None: smtp.quit() return "échec: serveur non reconnu: (" + msgerr + ")" #========================================================================= # à décommenter pour avoir tous les échanges du protocole dans la console # smtp.set_debuglevel(1) #========================================================================= # ouverture de session if serveur.login != "": try: rep = smtp.login(serveur.login, serveur.mdpasse) except Exception as msgerr: if smtp != None: smtp.quit() return "échec: login ou mdp non reconnu (" + msgerr + ")" #========================================================================= # envoi du mail try: rep = smtp.sendmail(message.expediteur, message.destinataires, message.mail) except Exception as msgerr: if smtp != None: smtp.quit() return "échec à l'envoi de mail (" + msgerr + ")" #========================================================================= # ici, l'envoi a été réussi smtp.quit() return "" # retourner une chaine vide est la signature d'un envoi réussi
Exemple d'utilisation:
# Identification du serveur SMTP à utiliser (vous mettez le vôtre!) # si pas d'authentification: ne pas indiquer les 2 derniers paramètres # 25 est le port par défaut serveur = ServeurSMTP("smtp.xxxxx.fr", 25, "tyrtamos", "motdepasse") # adresse de l'expéditeur (vous!): c'est une chaine de caractères exped = "Tyrtamos <tyrtamos@xxxxx.fr>" # adresse du ou des destinataire(s) direct(s): c'est une liste de chaine comportant au moins une adresse (sinon=erreur) to = ["albert@yyyyy.com"] # adresse du ou des destinataire(s) en copie: c'est une liste de chaines, éventuellement vide cc = ["julie@yyyyy.com", "totos <toto@zzzzz.fr>"] # adresse du ou des destinataire(s) en copie cachée: c'est une liste de chaines, éventuellement vide bcc = ["postmaster@xxxxx.com"] # sujet du mail (monolignes). Avec un encodage le permettant (ISO-8859-1, UTF-8), le sujet peut avoir des caractères accentués sujet = "Salut Albert!" # corps du mail (en général multilignes). Avec un encodage le permettant (ISO-8859-1, UTF-8), le sujet peut avoir des caractères accentués corps = """ Salut Albert! J'espère que tu vas bien! A bientôt! Tyrtamos """ # pièces jointes éventuelles (ici, []) pjointes=[] # choix du codage: 'US-ASCII', 'ISO-8859-1', 'ISO-8859-15', 'UTF-8', None (None=application du codage par défaut) # rappel: ISO-8859-15 permet, en plus de l'ISO-8859-1, l'utilisation du sigle de l'Euro (€) codage = 'ISO-8859-15' # type de texte: 'plain', 'html', ... Ici, c'est 'plain' typetexte = 'plain' # création du mail correctement formaté (en-tête + corps) et encodé try: message = MessageSMTP(exped, to, cc, bcc, sujet, corps, pjointes, codage, typetexte) except: print u"%s" % sys.exc_info()[1] sys.exit() # envoi du mail et affichage du résultat rep = envoieSMTP(message, serveur) print rep
C'est identique au cas précédent, à part que la liste des pièces jointes n'est pas vide!
... pjointes = ["fichier1.doc", "fichier2.pdf", "fichier3.jpg", "fichier4.mp3"] ...
C'est identique au cas d'envoi d'un mail “texte” sans pièce jointe, à part les points suivants:
... # corps du mail (en général multilignes). corps = """ <html> <head> <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> <title>Salut Albert!</title> </head> <body bgcolor="white" text="black" link="blue" vlink="purple" alink="red"> <p>Salut Albert!</p> <p> </p> <p><i><font size="4" color="red">J'espère que tu vas bien!</font></i></p> <p> </p> <p>A bientôt!</p> <p> </p> <p>Tyrtamos</p> </body> </html> """ ... # type de texte: 'plain', 'html', ... Ici, c'est 'html' typetexte = 'html' ...
C'est identique au cas précédent, à part que la liste des pièces jointes n'est pas vide!
... pjointes = ["fichier1.doc", "fichier2.pdf", "fichier3.jpg", "fichier4.mp3"] ...