lindev.fr

Aller au contenu | Aller au menu | Aller à la recherche

25 mai 2015

Gérer les Bounces avec Amazon

Ce billet va présenter deux outils Amazon autour d'un exemple concret, la gestion des Bounces en utilisant Amazon en relais pour vos mails.

Bounces

Lorsque vous envoyez une "newsletter" à de nombreuses adresses mails, il y a ce que l'on appel les "BOUNCES" , qui traduit donne "rebonds". Il s'agit en fait du retour des mails qui partent vers une adresse qui ne fonctionne pas. Il y a plusieurs raisons qui peuvent engendrer un tel rebond, certaines raisons sont temporaires, d'autres définitives. Par exemple :

  • Le destinataire n’existe pas ( définitif )
  • Boite pleine ( temporaire )
  • Erreur Serveur ( temporaire )
  • Plainte ( définitif )
  • Pas de réponse ( temporaire )
  • Pas de résolution DNS ( définitif )

Pourquoi traiter ces rebonds ?

Tout est une histoire de réputation, en effet ne pas traiter ces rebonds et les laisser s'augmenter à chaque newsletter, peut tout doucement vous faire passer pour un "spammeur", d'autant que nous utilisons le relais mails Amazon ( service SES ) qui va réduire votre capacité à envoyer des mails si le nombre de bounces augmente sans action de votre part.

Deuxième raison, financière, pourquoi continuer à envoyer et payer des mails à des adresses qui n’existent pas ! ( ok le cout du mail est ridicule avec Amazon, mais c'est une question de principe )

Comment les gérer ?

Souvent, vous définissez une adresse de retour via ue entête spécifique, dédiée au retour "système" ( Entête : return-path ), ainsi les bounces atterrissement dans cette boite mail dédiée et c'est à vous de les traiter. ( les outils des mailing intègrent souvent une fonction de récupération des bounces par une lecture de boite mail en pop ).

Cependant, Amazon permet d'aller un peu plus loin grâce à son service de notification SNS.
Le principe est simple, lorsque Amazon reçoit un "Bounce" il va transmettre un message de notification dans le topic associé . A ce "topic" nous allons lui associer une "inscription" ( qui est en fait une action à lancer lorsque qu'un "message" arrive sur ce topic ). Dans notre cas, ce sera une simple requête HTTP avec dans son corps les informations du Bounce en format JSON.

Création du Topic

Depuis l'interface Amazon SNS, vous créez un nouveau "topic"

Capture_d_e_cran_2015-05-24_a__11.04.03.png

On va le nommer Bounces

Capture_d_e_cran_2015-05-24_a__11.04.33.png

Puis nous allons créer une "subscription"

Capture_d_e_cran_2015-05-24_a__11.04.57.png

On va donc choisir le protocole HTTP et dans le champ "endpoint", nous mettons l'url qui pointe sur notre script décrit plus bas ( exemple : http://lindev.fr/sns.php )

Capture_d_e_cran_2015-05-24_a__11.05.20.png

Pour le moment, voici notre script sns.php :

<?php
ob_start();
var_dump( $_POST );
$content = ob_get_clean();
$fp = fopen('sns_validation.txt', a+);
fwrite($fp,$content);
fclose($fp);

Le but étant d'enregistrer dans un fichier texte, ce que nous envoi Amazon . NÉCESSAIRE AU DÉBUT POUR LA VALIDATION DE LE SOUSCRIPTION

Vous pouvez maintenant valider votre formulaire d'ajout de "subscription", vous allez alors voir apparaitre dans le fichier sns_validation.txt une url pour valider définitivement l'enregistrement.
Il vous suffit alors d'ouvrir cette url pour terminer l'enregistrement.

SES et SNS, le lien

Maintenant nous allons faire en sorte que tout les bounces liés à un domaine géré par le service SES d'amazon, utilisent le système de notification SNS et surtout le "topic" fraichement créé.

Pour cela, nous allons dans la section SES d'amazon, ( le domaine doit auparavant être validé pour pouvoir relayer des mails, pour cela il suffit d'ajouter des champs TXT dans la zone DNS. La documentation d'amazon est très bien faite à ce sujet ).

Nous allons donc dans la section Notifications, et l'on édite la configuration

Capture_d_e_cran_2015-05-24_a__11.06.36.png

Et là nous allons choisir les options adéquats

Capture_d_e_cran_2015-05-24_a__11.06.52.png

On sélectionne donc notre souscription dans les menus "Bounces" et "Complaints" uniquement, car ce sont ces deux cas que nous souhaitons traiter automatiquement.

Et voilà, après validation, dés qu'un "bounce" montre le bout de son nez, un message est transmit au service SNS et une requête HTTP avec toutes les informations nécessaires dans un format JSON est lancée en POST.

Tests

Pour tester le service, Amazon nous met à disposition des adresses spécifiques, qui simulent chaque cas.

Ne vous reste donc plus qu'à envoyer un mail à ces deux adresses, et regarder le contenu des données POST qui vont s'ajouter dans votre fichier sns_validation.txt

Cas simple

Imaginons, que nous souhaitons, simplement lister les mails des Bounces et Complaints dans un fichier TXT avec trois champs

  1. Date de réception du Bounce
  2. Adresse mail
  3. Type de Bounce ( temporaire ou définitif ou complaint ).

Nous allons donc extraire ces données du JSON, notre script sns.php devient :

<?php

// Fetch the raw POST body containing the message
$postBody = file_get_contents('php://input');

// JSON decode the body to an array of message data
$msg = json_decode($postBody, true);

if ($msg) {

    //Ouverture du fichier de log
    $fp = fopen('sns_bounces.txt', 'a+');

    $data = json_decode($msg['Message'],true);
    $typeMsg = $data['notificationType'];//A utiliser pour séparer plaintes et Bounces

    switch( $typeMsg ){
        case 'Bounce':
            $bounceType = $data['bounce']['bounceType'];
            foreach( $data['mail']['destination'] AS &$mail ){
                //Ecriture dans la liste txt
                fwrite($fp, date('Y-m-d H:i:s').";".$mail.";". $bounceType . "\n");
            }
        break;
        case 'Complaint':
            foreach( $data['mail']['destination'] AS &$mail ){
                //Ecriture dans la liste txt
                fwrite($fp, date('Y-m-d H:i:s').";".$mail.";complaint" . "\n");
            }
        break;
    }
    fclose($fp);

}

Vous allez alors vous retrouver avec une liste exploitable comme un CSV .

L'idée, c'est d'améliorer ce script de démo pour une désinscription automatique dans votre outil de mailing ( très simple avec phplist )

Restez informé

inscrivez vous à la newsletter pour recevoir les nouveaux billets par mail. ( formulaire en haut à droite )

17 janv. 2014

Django, gérer les FormSets dynamiquement

Les formsets sous Django sont très pratiques, notamment pour gérer les formulaires à "champs multiples".

qu'est-ce que j'entends par "champs multiples" ... ceci :

champs_multiples.png

Dans ce formulaire, j'ai donc deux champs uniques

  1. Largeur
  2. Hauteur

Puis vient enfin mon formset qui est composé de trois champs

  1. Désignation
  2. Quantité
  3. Liste : R | RV

J'ai donc au final un formulaire dont je ne connais pas à l'avance la quantité de données à traiter puisque le formset permet d'ajouter dynamiquement ses champs. Et c'est précisément ce que nous allons voir dans ce billet .

  • Comment créer un formset
  • Comment le rendre dynamique pour l'utilisateur ( ajout / suppression )
  • Comment valider les données en une seule ligne ( Merci Django :) )

Créer un Formset

Nous allons reproduire le formulaire présenté ci-dessus , nous aurons donc un formulaire classique , puis un formset de trois champs .

Formulaire classique

class ConfForm(forms.Form):
        """
        Formulaire de configuration du module
        """
        format_x = forms.IntegerField( label="Largeur", required=True, min_value=0, widget=forms.TextInput(attrs={'placeholder':'mm','class':'form-control input-sm'}) )
        format_y = forms.IntegerField( label="Hauteur", required=True, min_value=0, widget=forms.TextInput(attrs={'placeholder':'mm','class':'form-control input-sm'}) )

On ne peut pas plus .. classique pour les détails je vous laisse voir la doc de Django

Le formset

class SorteForm(forms.Form):

    #Champs multiples
    designation = forms.CharField( label="Désignation", required=True, widget=forms.TextInput(attrs={'placeholder':'','class':'form-control input-sm'}) )
    quantite = forms.IntegerField( label="Quantité", required=True, min_value=0, widget=forms.TextInput(attrs={'placeholder':'','class':'form-control input-sm'}) )
    rectoverso = forms.ChoiceField(label='',choices = [(False,'Recto Seul'),(True,'Recto Verso')],required=True,widget=forms.Select( attrs={'class':'form-control input-sm'}))

Vous allez me dire ... "c'est une déclaration de formulaire classique" .. et bien oui rien ne différencie au niveau déclaration un formulaire qui sera utilisé de façon classique, d'un formulaire qui sera utilisé au sein d'un formset .

Tout ceci pour en venir au fait qu'un formset, n'est ni plus, ni moins qu'un mécanisme proposé par Django pour gérer l'affichage, la validation des données et quelques autres petites choses comme les limitations min/max en un minimum d'efforts pour le développeur ...

Ça tombe bien je suis fainéant pour ce qui est de la gestion de formulaires ( pas vous ? ).

Maintenant que nous avons nos deux déclarations de formulaires, voyons comment les implémenter dans une vue.

Dans la vue

Note : Je ne vais pas expliquer ici la mise en forme CSS du formulaire, ce n'est pas l'objectif du billet, sachez juste que j'utilise BootstrapV3 dans la présentation du début.

Commençons avec une vue standard ... vide

def show_form(self, request):
    """ Affichage du formulaire + formset """

    # Formulaire standard à deux champs
    form = ConfForm(auto_id=True)

    #Création du formset avec une seule itération : extra=1
    sorteForm = formset_factory(SorteForm,extra=1)
        
    # Récupération du formulaire géré par le mécanisme formset
    formset = sorteForm()

    # Affichage du template 
    return render_to_response('my_form.html', {'form':form, 'sorteForm':formset}, context_instance=RequestContext(request) )

Le template

D'habitude, pour afficher un formulaire sans mise en forme particulière , la balise form se suffit à elle même.
Pour le formset , c'est la même chose, si ce n'est que nous allons devoir mettre cette balise dans une itération, car rappelez-vous, un formset peut contenir plusieurs formulaires.

Ce qui va donc nous donner : ( sans mise en forme )

<!-- formulaire standard -->
{{ form }}

<!-- Champs utilisés par le mécanisme formset de Django -->
{{ sorteForm.management_form }}

<!-- itération des formulaires du formset -->
{% for form in sorteForm %}
    
    {{ form }}

{% endfor %}

La balise management_form est utilisée par le mécanisme formset pour gérer les éventuels ajouts dynamiques, modifications, etc ... bref .. fonctionner correctement .
Voici son contenu

<!-- nombre de formulaire (s) -->
<input id="id_form-TOTAL_FORMS" type="hidden" value="1" name="form-TOTAL_FORMS">
<!-- nombre de formulaires à l'origine --> 
<input id="id_form-INITIAL_FORMS" type="hidden" value="1" name="form-INITIAL_FORMS">
<!-- nombre max de formulaires gérés si ajout dynamique -->
<input id="id_form-MAX_NUM_FORMS" type="hidden" value="1000" name="form-MAX_NUM_FORMS">

La validation et l'enregistrement

Oui oui .. je sais .. pour le moment il n'y a rien de dynamique, pas d'ajout / suppression ... patience, commençons par boucler la boucle avec la gestion des données POST, nous verrons ensuite pour dynamiser le formulaire .

En effet, l'étape de validation et traitement, ne varie pas selon le nombre de formulaire(s) dans le formset, c'est là toute la force des formset avec Django .

Voici la vue de validation , qui reçoit les données POST

    def save_form(self):
        """  Check & Enregistre les données   """

        # Check si la méthode est bien en POST
        if request.method == 'POST':

            # Instancie et "bound" le formulaire
            form = ConfForm( request.POST, auto_id=True )

            # Création du formset
            sorteForm = formset_factory( SorteForm )
            # "Bound" le formset
            formset = sorteForm( request.POST )

            #Verifie si les données du formulaire simple sont valides
            if form.is_valid() :
                # Vérifie le(s) formulaire(s) du formset 
                if formset.is_valid():

                    #Affiche les données du formulaire simple
                    print form.cleaned_data

                    # Itération du formset pour afficher les données 
                    for fs in formset:
                        print fs.cleaned_data
                        
                else:
                    print = str( formset.errors )
            else:
                print = str( form.errors )

Forcément ici le code ne sert à rien, si ce n'est à expliquer comment valider formulaire + formset pour un traitement des données .. ( enregistrement en base, actions, mails etc ... ou comme ici un simple affichage ;) )

Dynamisme

Nous voilà avec un super formulaire + formset, qui s'affichent correctement, avec les données validées ( peu importe le nombre de formulaire(s) dans le formset ), mais maintenant nous allons voir comment ajouter / supprimer dynamiquement un formulaire dans le formset .

A partir de maintenant tout se passe dans le template , le principe est simple, nous ajoutons un boutton "+" pour ajouter un formulaire au formset et, pour chaque formulaire du formset un boutton "-" qui permettra de retirer le retirer .

Pour ajouter un formulaire au formset, nous allons faire appel à du javascript, qui va cloner un élément du DOM, ré-indexer les formulaires ( du formset ) puis mettre à jour la valeur du champ de management form-TOTAL_FORMS.

Pourquoi ré-indexer

Si l'on regarde le code généré par le formset, les champs sont indexés pour ne pas mélanger les données entres formulaires, vérifier la cohérence des données etc ..

<!-- Form 1 -->
<input id="id_form-0-designation" class="form-control input-sm" type="text" value="V2" placeholder="" name="form-0-designation">
<input id="id_form-0-quantite" class="form-control input-sm" type="text" value="1000" placeholder="" name="form-0-quantite">
....
<!-- Form n -->
<input id="id_form-n-designation" class="form-control input-sm" type="text" value="V2" placeholder="" name="form-n-designation">
<input id="id_form-n-quantite" class="form-control input-sm" type="text" value="1000" placeholder="" name="form-n-quantite">
...

Formulaire vide

Quel formulaire copier dans le DOM .. et puis .. je ne souhaite pas copier le contenu des champs ...ça tombe bien , encore une fois Django nous livre tout sur un plateau d'argent, grâce à l'attribut empty_form.

Nous allons donc dans un div hors de la balise form inclure le code suivant {{ sorteForm.empty_form }} ce qui va donc nous donner

<div id="my_form">
    <form action="#" name="form_test">
    <!-- Le formulaire standard -->
    {{ form }}

    <button class="btn btn-success btn-sm" id="bt_add_sorte" type="button">+</button>

    {{ sorteForm.management_form }}
        <!-- Les formulaires du formset -->
        <div id="formsetZone">
            {% for form in sorteForm %}
                <div class="nsorte">
                {{ form }}
                </div>
            {% endfor %}
        </div>
    </form>
</div>

<!-- Element à copier pour un ajout au formset -->
<div style="display:none;">
    <div id="eform" class="nsorte" >
        {{ sorteForm.empty_form }}
        <div class='btn btn-warning btn-sm bt_rm_sorte'>
                <i class='glyphicon glyphicon-trash'></i>
        </div>
    </div>
</div>

Voilà à quoi ressemble ce formulaire vide

<label for="id_form-__prefix__-designation">Désignation&nbsp;:</label>
<input class="form-control input-sm" id="id_form-__prefix__-designation" name="form-__prefix__-designation" placeholder="" type="text">
<label for="id_form-__prefix__-quantite">Quantité&nbsp;:</label>
<input class="form-control input-sm" id="id_form-__prefix__-quantite" name="form-__prefix__-quantite" placeholder="" type="text">
<select class="form-control input-sm" id="id_form-__prefix__-rectoverso" name="form-__prefix__-rectoverso">
<option value="False">Recto Seul</option>
<option value="True">Recto Verso</option>
</select>

L'objectif pour l'ajout est donc :

  1. Binder l’événement onclick du bouton avec l'id bt_add_sorte
  2. Cloner le contenu du div avec l'id eform
  3. Ajouter le données clonées à la fin du div id formsetZone
  4. Ré-indexer les formulaires du formset pour remplacer prefix par le bon numéro de formulaire
  5. Incrémenter la valeur du champ de management avec l'id id_form-TOTAL_FORMS

Pour la suppression :

  1. Binder les boutons dont la classe est bt_rm_sorte
  2. Enlever du DOM le formulaire à retirer ( conteneur parent avec la classe nsorte )
  3. Re-indexer les formulaires du Formset
  4. Décrémenter a valeur du champ de management avec l'id id_form-TOTAL_FORMS

vive le javascript

Tout ça en javascript, grâce à JQuery

$(document).ready(function(){

    /**************************************************************************
    *
    *                                      Gesion ADD REMOVE Formset 
    *
    ***************************************************************************/

    index_form = function( fset, index ){

        $(fset).find(':input').each(function() {
            var name = $(this).attr('name').replace( new RegExp('(\_\_prefix\_\_|\\d)') , index );
            var id = 'id_' + name;
            $(this).attr({'name': name, 'id': id});
        });

        $(fset).find('label').each(function() {
            var newFor = $(this).attr('for').replace( new RegExp('(\_\_prefix\_\_|\\d)') , index );
            var id = 'label_' + newFor;
            $(this).attr({'id':id, 'for':newFor});
        });

    }

    reindex_formset = function( formset_zone ){

        var formset = $(formset_zone).find( '.nsorte' );
        for( var cpt=0;cpt<formset.length;cpt++ ){
            index_form( formset[cpt], cpt );
        };

        $("#id_form-TOTAL_FORMS").val( parseInt( cpt ) );

    };



    /**************************************************************************
    *
    *                               Gesion Des evenements formulaire
    *
    ***************************************************************************/


    set_event = function(){
            //Bind le(s) bt delete sorte
            $(".bt_rm_sorte").on('click',function(){
                $(this).parents(".nsorte").remove();
                reindex_formset( "#formsetZone" );
            });
    };

    $("#bt_add_sorte").on('click',function(){

        //Copy eform
        $( "#eform" ).clone(true).appendTo( $("#formsetZone") );

        reindex_formset( "#formsetZone" );

    });

    set_event();


});

Conclusion

Nous avons fait le tour pour gérer les formset dynamiquement, au diable les galères pour gérer ce genre de cas , merci Django .

Ch.