Loupe

Envoyer un email sans en oublier la pièce jointe: la technique du développeur

Toute personne qui envoie des emails, à titre personnel ou professionnel, a déjà été confronté à ce problème: on rédige son email (en indiquant à son interlocuteur de regarder le document attaché) et, par mégarde, on oublie d'insérer la fameuse pièce jointe. Et le premier réflexe, dans ce cas, est bien sûr de se dire: "Non mais quel boulet je fais" :)

 Il y a sans doutes une option, dans Outlook, pour éviter cela mais en tant que développeur, je me suis dit que ce serait plus simple de le faire via un petit addin Outlook / VSTO.

Pour cela, rien de plus simple car Outlook expose l'évènement ItemSend, qui survient lorsqu'un élément (email, invitation, etc.) est envoyé (ou, plus précisément, lorsque l'utilisateur a cliqué sur le bouton d'envoi):

Il suffit alors de s'y abonner:

private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
    this.Application.ItemSend += OnItemSend;
}

Ensuite, il suffit d'analyser le contenu du mail pour en extraire des mots-clés et essayé de déterminer si on indique vouloir partager une pièce jointe alors que celle-ci est manquante:

private static readonly string[] ATTACHMENTS_KEYWORDS = {"pj", "pièce jointe", "piece jointe", "attachment", "ci-joint", "attaché à", "attached to" };

private void OnItemSend(object item, ref bool cancel)
{
    var itemSubject = string.Empty;
    var itemContent = string.Empty;
    var attachmentCount = 0;

    // Get subject and content from mail or appointment
    var mailItem = item as Outlook.MailItem;
    if (mailItem != null)
    {
        itemSubject = mailItem.Subject.ToLowerInvariant();
        itemContent = mailItem.BodyFormat == Outlook.OlBodyFormat.olFormatHTML ? mailItem.HTMLBody.ToLowerInvariant() : mailItem.Body.ToLowerInvariant();
        attachmentCount = mailItem.Attachments.Count;
    }

    var appointmentItem = item as Outlook.AppointmentItem;
    if (appointmentItem != null)
    {
        itemSubject = appointmentItem.Subject.ToLowerInvariant();
        itemContent = appointmentItem.Body.ToLowerInvariant();
        attachmentCount = appointmentItem.Attachments.Count;
    }

    // Check contents to detect "attachment" keywords
    var regex = new Regex(@"\b(" + string.Join("|", ATTACHMENTS_KEYWORDS.Select(Regex.Escape).ToArray()) + @"\b)");

    if ((regex.Match(itemSubject).Success || regex.Match(itemContent).Success) && attachmentCount == 0)
    {
        // A keyword has been found so ask the user
        if (MessageBox.Show($"Looks like you are sending an email with an attachment but you forget it!{Environment.NewLine}Send it anyway?", "Boulet Protector", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.No)
        {
            cancel = true;
        }
    }
}

Bien que fonctionnelle, cette méthode présente tout de même un inconvénient: les signatures ou autres élément insérés "inline" sont considérés comme des pièces jointes! Donc, même si vous n'avez pas de PJ mais une signature, le code indiquera que tout est OK donc il y a une faille.

Pour éviter cela, il faut regarder si au moins une des pièces jointes n'est pas "inline", en utilisant les PropertyAccessor:

private void OnItemSend(object item, ref bool cancel)
{
    var itemSubject = string.Empty;
    var itemContent = string.Empty;
    var hasValidAttachment = false;

    // Get subject and content from mail or appointment
    var mailItem = item as Outlook.MailItem;
    if (mailItem != null)
    {
        itemSubject = mailItem.Subject.ToLowerInvariant();
        itemContent = mailItem.BodyFormat == Outlook.OlBodyFormat.olFormatHTML ? mailItem.HTMLBody.ToLowerInvariant() : mailItem.Body.ToLowerInvariant();
        hasValidAttachment = HasValidAttachments(mailItem);
    }

    var appointmentItem = item as Outlook.AppointmentItem;
    if (appointmentItem != null)
    {
        itemSubject = appointmentItem.Subject.ToLowerInvariant();
        itemContent = appointmentItem.Body.ToLowerInvariant();
        hasValidAttachment = HasValidAttachments(appointmentItem);
    }

    Marshal.ReleaseComObject(item);

    // Check contents to detect "attachment" keywords
    var regex = new Regex(@"\b(" + string.Join("|", ATTACHMENTS_KEYWORDS.Select(Regex.Escape).ToArray()) + @"\b)");

    if ((regex.Match(itemSubject).Success || regex.Match(itemContent).Success) && !hasValidAttachment)
    {
        // A keyword has been found so ask the user
        if (MessageBox.Show($"Looks like you are sending an email with an attachment but you forget it!{Environment.NewLine}Send it anyway?", "Boulet Protector", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.No)
        {
            cancel = true;
        }
    }
}

public static bool HasValidAttachments(Outlook.MailItem mailItem)
{
    const string PR_ATTACH_METHOD = "http://schemas.microsoft.com/mapi/proptag/0x37050003";
    const string PR_ATTACH_FLAGS = "http://schemas.microsoft.com/mapi/proptag/0x37140003";

    var attachments = new List<Outlook.Attachment>();

    // if this is a plain text email, every attachment is a non-inline attachment
    if (mailItem.BodyFormat == Outlook.OlBodyFormat.olFormatPlain && mailItem.Attachments.Count > 0)
    {
        attachments.AddRange(mailItem.Attachments.Cast<object>().Select(attachment => attachment as Outlook.Attachment));
    }

    // if the body format is RTF ...
    if (mailItem.BodyFormat == Outlook.OlBodyFormat.olFormatRichText)
    {
        // add every attachment where the PR_ATTACH_METHOD property is NOT 6 (ATTACH_OLE)
        attachments.AddRange(mailItem.Attachments.Cast<object>().Select(attachment => attachment as Outlook.Attachment).Where(thisAttachment => (int)thisAttachment.PropertyAccessor.GetProperty(PR_ATTACH_METHOD) != 6));
    }

    // if the body format is HTML ...
    if (mailItem.BodyFormat == Outlook.OlBodyFormat.olFormatHTML)
    {
        // add every attachment where the ATT_MHTML_REF property is NOT 4 (ATT_MHTML_REF)
        attachments.AddRange(mailItem.Attachments.Cast<object>().Select(attachment => attachment as Outlook.Attachment).Where(thisAttachment => (int)thisAttachment.PropertyAccessor.GetProperty(PR_ATTACH_FLAGS) != 4));
    }

    return attachments.Count > 0;
}

public static bool HasValidAttachments(Outlook.AppointmentItem appointmentItem)
{
    const string PR_ATTACH_FLAGS = "http://schemas.microsoft.com/mapi/proptag/0x37140003";

    var attachments = new List<Outlook.Attachment>();

    attachments.AddRange(appointmentItem.Attachments.Cast<object>().Select(attachment => attachment as Outlook.Attachment).Where(thisAttachment => (int)thisAttachment.PropertyAccessor.GetProperty(PR_ATTACH_FLAGS) != 4));

    return attachments.Count > 0;
}

Et voilà ! A présent, il me suffit d'envoyer un email (ou un RM) avec du texte indiquant que je veux envoyer une pièce jointe, et l'omettre, pour avoir un beau message d'avertissement:

 Error.png

Bien sûr, il s'agit plus là d'un code de type POC que pour de la production, il serait nécessaire d'améliorer la détection des pièces jointes (inlines) mais, c'est vendredi, tout est permis ;)

 

Happy coding!

 

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus