Chapter 8: Emails et SMS

Emails et SMS

Mail

Paramétrer les emails

Web2py fournit la classe gluon.tools.Mail pour rendre facile l'envoi de mails en utilisant web2py. On peut définir un mailer avec

from gluon.tools import Mail
mail = Mail()
mail.settings.server = 'smtp.example.com:25'
mail.settings.sender = 'you@example.com'
mail.settings.login = 'username:password'

Notez, si votre application utilise Auth (présenté dans le chapitre suivant), l'objet auth inclura son propre mailer dans auth.settings.mailer, pour que vous puissiez l'utiliser à la place comme :

mail = auth.settings.mailer
mail.settings.server = 'smtp.example.com:25'
mail.settings.sender = 'you@example.com'
mail.settings.login = 'username:password'

Vous avez besoin de remplacer le mail.settings avec les bons paramètres pour votre serveur SMTP. Définissez mail.settings.login = None si le serveur SMTP ne nécessite pas d'authentification. Si vous ne voulez pas utiliser TLS, définissez mail.settings.tls = False

email logging

Pour des raisons de débogage, vous pouvez définir

mail.settings.server = 'logging'

et les emails ne seront pas envoyés mais loggés dans la console à la place.

Configurer l'email pour Google App Engine

email from GAE

Pour envoyer les emails depuis le compte Google App Engine :

mail.settings.server = 'gae'

Au moment de l'écriture du livre web2py, il n'y a pas de support pour les pièces jointes et les mails chiffrés sur Google App Engine. Notez que cron et scheduler ne fonctionnent pas sur GAE.

x509 et chiffrement PGP

PGP
x509

Il est possible d'envoyer des emails chiffrés x509 (SMIME) en utilisant les paramètres suivants :

mail.settings.cipher_type = 'x509'
mail.settings.sign = True
mail.settings.sign_passphrase = 'your passphrase'
mail.settings.encrypt = True
mail.settings.x509_sign_keyfile = 'filename.key'
mail.settings.x509_sign_certfile = 'filename.cert'
mail.settings.x509_crypt_certfiles = 'filename.cert'

Il est possible d'envoyer des mails chiffrés PGP. Tout d'abord, vous avez besoin d'installer le package python-pyme. Ensuite vous pouvez utiliser GnuPG (PGP) pour créer les fichiers de clé pour l'émetteur (prenez l'adresse email depuis mail.settings.sender) et mettez les fichiers pubring.gpg et secring.gpg dans un répertoire (e.g. "/home/www-data/.gnupg").

Utilisez les paramètres suivants :

mail.settings.gpg_home = '/home/www-data/.gnupg/'
mail.settings.cipher_type = 'gpg'
mail.settings.sign = True
mail.settings.sign_passphrase = 'your passphrase'
mail.settings.encrypt = True

Envoi d'emails

mail.send
email html
email attachments

Une fois que mail est défini, il peut être utilisé pour envoyer un email via :

mail.send(to=['somebody@example.com'],
          subject='hello',
          # If reply_to is omitted, then mail.settings.sender is used
          reply_to='us@example.com',
          message='hi there')

Mail retourne True s'il réussit à envoyer l'email et False autrement. Une liste complète des arguments pour mail.send() est la suivante :

send(self, to, subject='None', message='None', attachments=[],
     cc=[], bcc=[], reply_to=[], sender=None, encoding='utf-8',
     raw=True, headers={})

Notez, to, cc, et bcc prennent chacun une liste d'adresses email.

sender par défaut à None et dans ce cas, l'émetteur sera défini à mail.settings.sender.

headers est un dictionnaire d'en-têtes pour redéfinir les en-têtes juste avant d'envoyer l'email. Par exemple :

headers = {'Return-Path' : 'bounces@example.org'}

La suite montre des exemples complémentaires pour l'usage de mail.send().

Email en texte simple

mail.send('you@example.com',
  'Message subject',
  'Plain text body of the message')

Emails HTML

mail.send('you@example.com',
  'Message subject',
  '<html>html body</html>')

Si le corps d'email démarre avec <html> et finit avec </html>, il sera envoyé comme un email HTML.

Combiner des emails texte et HTML

Le message email peut être un tuple (text, html) :

mail.send('you@example.com',
  'Message subject',
  ('Plain text body', '<html>html body</html>'))

Emails cc et bcc

mail.send('you@example.com',
  'Message subject',
  'Plain text body',
  cc=['other1@example.com', 'other2@example.com'],
  bcc=['other3@example.com', 'other4@example.com'])

Pièces jointes

mail.send('you@example.com',
  'Message subject',
  '<html><img src="cid:photo" /></html>',
  attachments = mail.Attachment('/path/to/photo.jpg', content_id='photo'))

Multiples pièces jointes

mail.send('you@example.com',
  'Message subject',
  'Message body',
  attachments = [mail.Attachment('/path/to/fist.file'),
                 mail.Attachment('/path/to/second.file')])

Envoi de messages SMS

SMS

L'envoi de messages SMS depuis une application web2py nécessite un service tiers qui peut relayer les message au récepteur. Habituellement ce n'est pas un service gratuit, mais diffère selon le pays. Nous avons essayé quelques uns de ces services avec un peu de succès. Les sociétés de téléphones bloquent les emails en provenance de ces services puisqu'ils peuvent être utilisés comme source de spam.

Un meilleur moyen est d'utiliser les sociétés de téléphones directement pour relayer les SMS. Chaque société de téléphonie a une adresse email uniquement associée avec chaque numéro de téléphone, donc les SMS peuvent être envoyés comme emails au numéro de téléphone.

web2py est fourni avec un module pour accompagner dans ce process :

from gluon.contrib.sms_utils import SMSCODES, sms_email
email = sms_email('1 (111) 111-1111','T-Mobile USA (tmail)')
mail.send(to=email, subject='test', message='test')

SMSCODES est un dictionnaire qui mappe les noms des principales sociétés de téléphonie aux adresses email de distribution. La fonction sms_email prend un numéro de téléphone (comme chaîne) et le nom d'une société de téléphone et retourne l'adresse email du téléphone.

Utiliser le template système pour générer des messages

emails

Il est possible d'utiliser le template système pour générer des emails. Par exemple, considérons la table de base de données

db.define_table('person', Field('name'))

où vous voulez envoyer à chaque personne de la base le message suivant, stocké dans un fichier vue "message.html" :

Dear {{=person.name}},
You have won the second prize, a set of steak knives.

Vous pouvez faire ceci de la façon suivante

for person in db(db.person).select():
    context = dict(person=person)
    message = response.render('message.html', context)
    mail.send(to=['who@example.com'],
              subject='None',
              message=message)

La plupart du travail est fait dans la déclaration

response.render('message.html', context)

Il rend la vue "message.html" avec les variables définies dans le dictionnaire "context", et il retourne une chaîne avec le mail texte rendu. Le contexte est un dictionnaire qui contient les variables qui seront visibles par le fichier template.

Si le message commence avec <html> et finit avec </html>, l'email sera un email HTML.

Note, si vous voulez inclure un lien vers votre site web dans un email HTML, vous pouvez utiliser la fonction URL. Cependant, par défaut, la fonction URL génère une URL relative, qui ne fonctionnera pas depuis un email. Pour générer une URL absolue, vous avez besoin de spécifier les arguments scheme et host à la fonction URL. Par exemple :

<a href="{{=URL(..., scheme=True, host=True)}}">Click here</a>

ou

<a href="{{=URL(..., scheme='http', host='www.site.com')}}">Click here</a>

Le même mécanisme qui est utilisé pour générer un email texte peut aussi être utilisé pour générer des messages SMS ou n'importe quel autre type de message basé sur un template.

Envoyer des messages en utilisant une tâche en arrière-plan

L'opération d'envoyer un message email peut prendre plusieurs secondes de par le besoin d'établir la connexion et la communication avec un potentiel serveur SMTP distant. Pour éviter l'utilisateur d'avoir à attendre que l'opération d'envoi soit complète, il est parfois désirable de mettre en file un email à envoyer plus tard via une tâche d'arrière-plan. Comme décrit dans le Chapitre 4, ce peut être fait en paramétrant une tâche maison de file ou en utilisant le scheduler web2py. Nous fournissons ici un exemple utilisant un tâche de file maison.

D'abord, dans un fichier de modèle à l'intérieur de notre application nous définission un modèle de base de données pour stocker notre file d'email :

db.define_table('queue',
    Field('status'),
    Field('email'),
    Field('subject'),
    Field('message'))

Depuis un contrôleur, nous pouvons alors mettre en attente les messages à envoyer avec :

db.queue.insert(status='pending',
                email='you@example.com',
                subject='test',
                message='test')

Ensuite, nous avons besoin d'un script s'exécutant en arrière plan qui lit la file d'attente et envoie les emails :

## in file /app/private/mail_queue.py
import time
while True:
    rows = db(db.queue.status=='pending').select()
    for row in rows:
        if mail.send(to=row.email,
            subject=row.subject,
            message=row.message):
            row.update_record(status='sent')
        else:
            row.update_record(status='failed')
        db.commit()
    time.sleep(60) # check every minute

Finalement, comme décrit dans le Chapitre 4, nous avons besoin de démarrer le script mail_queue.py comme si c'était à l'intérieur d'un contrôleur dans notre application :

python web2py.py -S app -M -N -R applications/app/private/mail_queue.py

-S app indique à web2py de démarrer "mail_queue.py" comme "app", -M indique à web2py d'exécuter les modèles, et -N indique à web2py de ne pas démarrer le cron.

Nous supposons ici que l'objet mail référencé dans "mail_queue.py" est défini dans un fichier modèle dans notre application et est donc disponible dans le script "mail_queue.py" de par l'option -M. Notez également qu'il est important de faire un commit sur tout changement dès que possible afin de ne pas verrouiller la base de données avec les autres processus concurrents.

Comme noté dans le Chapitre 4, ce type de processus d'arrière-plan ne devrait pas être exécuté via cron (sauf peut être pour le cron @reboot) puisque vous avez besoin d'être sûr qu'il n'y a pas plus d'une instance en cours d'exécution au même moment.

Notez, un inconvénient à envoyer des mails via un processus d'arrière plan est qu'il est difficile de fournir un feedback à l'utilisateur dans le cas où un email échoue. Si l'email est envoyé directement depuis l'action contrôleur vous pouvez récupérer toute erreur et immédiatement retourner un message d'erreur à l'utilisateur. Avec un processus d'arrière plan, cependant, l'email est envoyé de manière asynchrone, après que l'action ait déjà retourné sa réponse, et il devient donc plus complexe de notifier l'utilisateur de l'échec.

Lire et gérer les boites d'email (Expérimental)

L'adaptateur IMAP est prévu comme une interface avec les serveurs email IMAP pour effectuer de simples requêtes dans la syntaxe web2py de la DAL, donc la lecture d'email, la recherche et tout autre service relatif au mail IMAP (comme ceux implémentés par les marques comme Google(r), et Yahoo(r) peuvent être gérés depuis les applications web2py.

Il créé sa table et ses noms de champ de manière "statique", signifiant que le developpeur devrait laisser les définitions de table et de champ à l'instance de DAL en appelant la méthode d'adaptateur .define_tables(). Les tables sont définies avec la liste d'information du serveur IMAP de la boite mail.

Connexion

Pour un simple compte mail, voici le code recommandé pour démarrer le support IMAP au modèles de l'application

# Replace user, password, server and port in the connection string
# Set port as 993 for SSL support
imapdb = DAL("imap://user:password@server:port", pool_size=1)
imapdb.define_tables()

Notez que <imapdb>.define_tables() retourne un dictionnaire de chaînes mappant les noms de table de la DAL aux noms des boites mail du serveur avec la structure {<tablename>: <server mailbox name>, ...}, pour que vous puissiez récupérer le nom de la boite mail actuelle dans le serveur IMAP.

Si vous voulez définir votre propre configuration tablename/mailbox et passer la configuration de nom automatique, vous pouvez passer un dictionnaire personnalisé à l'adaptateur par ce moyen :

imapdb.define_tables({"inbox": "MAILBOX", "trash", "SPAM"})

Pour gérer les noms de différentes boites mail natives pour l'interface utilisateur, les attributs suivants donnent accès aux noms mappés automatiquement de la boite mail à l'adaptateur (ce que la boite mail native a est ce qu'a le nom de la table et vice versa) :

AttributeTypeFormat
imapdb.mailboxesdict{<tablename>: <server native name>, ...}
imapdb.<table>.mailboxstring"server native name"

Le premier peut être utile pour retrouver une requête IMAP définie par le service mail natif.

# mailbox is a string containing the actual mailbox name
tablenames = dict([(v,k) for k,v in imapdb.mailboxes.items()])
myset = imapdb(imapdb[tablenames[mailbox]])

Rassembler les mails et mettre à jour les flags

Voici une liste de commandes IMAP que vous pourriez utiliser dans le contrôleur. Pour les exemples, il est supposé que votre service IMAP a une boite mail nommée INBOX, qui est le cas pour les comptes GMail(r).

Pour compter le nombre de messages non lus aujourd'hui plus petits que 6000 octets depuis la boite de réception, faites

q = imapdb.INBOX.seen == False
q &= imapdb.INBOX.created == request.now.date()
q &= imapdb.INBOX.size < 6000
unread = imapdb(q).count()

Vous pouvez rassembler les messages de requêtes précédentes avec

rows = imapdb(q).select()

Les opérateurs de requêtes habituels sont implémentés, incluant "belongs"

messages = imapdb(imapdb.INBOX.uid.belongs(<uid sequence>)).select()

Note: Il est fortement conseillé que vous conserviez les résultats de requête en dessous d'une limite donnée en taille pour éviter de surcharger le serveur avec des commandes select trop larges.

Pour effectuer des requêtes email plus rapide, il est recommandé de passer un ensemble de champs filtré :

fields = ["INBOX.uid", "INBOX.sender", "INBOX.subject", "INBOX.created"]
rows = imapdb(q).select(*fields)

L'adaptateur sait quand retrouver des charges de message partiels (champs comme content, size et attachments nécessitent de récupérer les données complètes du message)

Il est possible de filtrer les résultats de la requête select avec limitby et les séquences de champs de boite mail

# Replace the arguments with actual values
myset.select(<fields sequence>, limitby=(<int>, <int>))

Disons que vous voulez avoir une action d'application qui montre un message de boite mail. Nous retrouvons d'abord le message (si votre service IMAP le supporte, rassemblez les messages par champ uuid pour éviter d'utiliser les références d'une ancienne séquence).

mymessage = imapdb(imapdb.INBOX.uid == <uid>).select().first()

Autrement, vous pouvez utiliser l'id des messages.

mymessage = imapdb.INBOX[<id>]

Notez qu'utiliser l'id du message comme référence n'est pas recommandé, puisque la séquence de nombres peut chant avec les opérations de maintenant de boite mail comme les suppressions de message. Si vous voulez toujours enregistrer les références en messages (i.e. dans le champ d'un autre enregistrement de la base de données), la solution est d'utiliser le champ uid comme référence dès que supportée, et retrouve chaque message avec la valeur enregistrée.

Finalement, ajoutez quelque chose comme ce qui suit pour montrer le contenu du message dans la vue

{{=P(T("Message from"), " ", mymessage.sender)}}
{{=P(T("Received on"), " ", mymessage.created)}}
{{=H5(mymessage.subject)}}
{{for text in mymessage.content:}}
  {{=DIV(text)}}
  {{=TR()}}
{{pass}}

Comme attendu, nous pouvons profiter du helper SQLTABLE pour construire les listes de message dans les vues

{{=SQLTABLE(myset.select(), linkto=URL(...))}}

Et bien sûr, il est possible de fournir un helper de formulaire avec la valeur de l'id de séquence appropriée

{{=SQLFORM(imapdb.INBOX, <message id>, fields=[...])}}

Les champs supportés disponibles avec l'adaptateur courant sont les suivants :

FieldTypeDescription
uidstring
answeredbooleanFlag
createddate
contentlist:stringUne liste de texte ou de parties html
tostring
ccstring
bccstring
sizeintegerle nombre d'octets du message*
deletedbooleanFlag
draftbooleanFlag
flaggedbooleanFlag
senderstring
recentbooleanFlag
seenbooleanFlag
subjectstring
mimestringLa déclaration de l'en-tête MIME
emailstringLe message complet RFC822
attachmentslistToute partie non textuelle décodée en dictionnaire
encodingstringLe charset détecté du message

*Côté application, est mesuré comme la longueur de la chaîne message RFC822

WARNING: Comme les ids de ligne sont mappés à des numéros de séquence email, assurez-vous que votre client IMAP web2py ne supprime pas les messages durant les actions de select ou d'update, pour éviter de mettre à jour ou de supprimer des messages différents.

Les opérations standards CRUD à la base de données ne sont pas supportées. Il n'y a aucun moyen de définir des champs personnalisés ou des tables et faire les insertions avec des types de données différents puisque la mise à jour des boites mail avec les services IMAP est généralement réduite à poster des mises à jour de flag au serveur. Encore une fois, il est possible d'accéder à ces commandes de flags via l'interface DAL IMAP.

Pour marquer les messages de la dernière requête comme vus

seen = imapdb(q).update(seen=True)

Nous supprimons ici les messages dans la base de données IMAP qui a les mails de Mr. Gumby

deleted = 0
for tablename in imapdb.tables():
    deleted += imapdb(imapdb[tablename].sender.contains("gumby")).delete()

Il est également possible de marquer les messages pour suppression au lieu de les effacer directement avec

myset.update(deleted=True)
IMAP
 top