Outils pour utilisateurs

Outils du site


expedition_mail

Expédition de mail texte ou html (avec et sans pièces jointes)

[modificaton le 23/2/2015: ajout du code en Python 3]

Objectif

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…

Présentation du code

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:

  • la classe “ServeurSMTP()” qui n'est en fait qu'un objet qui conserve les données du compte SMTP: adresse, port, login, mdpasse, passées comme paramètres à l'instanciation de la classe.
  • la classe “MessageSMTP()” dont la fonction est d'empaqueter correctement les différents éléments du mail (en-tête, texte et éventuelles pièces jointes) passés en paramètres à l'instanciation de la classe. Après l'instanciation, sont disponibles: l'expéditeur, la liste des destinataires (to + cc + bcc) et le mail empaqueté sous forme de chaine.
  • la fonction envoieSMTP() qui envoie le mail au bon endroit, quand on lui donne les 2 variables d'instance précédentes: le serveur à utiliser et le mail à envoyer!

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.

Le code complet

Ce code traite les 4 cas suivants:

  • envoi d'un mail “texte” (plusieurs codages possibles)
  • envoi d'un mail “texte” avec une ou plusieurs pièces jointes
  • envoi d'un mail “html” (plusieurs codages possibles)
  • envoi d'un mail “html” avec une ou plusieurs pièces jointes

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

Envoi d'un mail "texte" sans pièce jointe

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


Envoi d'un mail "texte" avec une ou plusieurs pièces jointes

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"]
 
...


Envoi d'un mail "html" sans pièce jointe

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>&nbsp;</p>
<p><i><font size="4" color="red">J'espère que tu vas bien!</font></i></p>
<p>&nbsp;</p>
<p>A bientôt!</p>
<p>&nbsp;</p>
<p>Tyrtamos</p>
</body>
</html>
"""
 
...
 
# type de texte: 'plain', 'html', ... Ici, c'est 'html'
typetexte = 'html'
 
...


Envoi d'un mail "html" avec une ou plusieurs pièces jointes

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"]
 
...

expedition_mail.txt · Dernière modification: 2015/02/23 21:12 par tyrtamos

Outils de la page