165 votes

Mail multipart/alternative vs multipart/mixed

Lors de la création messages électroniques vous êtes censé définir le Content-Type a multipart/alternative lors de l'envoi de HTML et de TEXTE ou multipart/mixed lors de l'envoi de TEXTE et de pièces jointes.

Alors que faire si vous voulez envoyer du HTML, du texte et des pièces jointes ? Utiliser les deux ?

2 votes

Je ne suis pas sûr de la manière "correcte" de procéder. J'ai certainement vu des messages mp/alt qui comportaient une partie mp/text et une partie mp/mixed contenant le HTML et la pièce jointe... mais cela signifiait que la pièce jointe n'était visible qu'à l'affichage du HTML et non du TEXTE, ce qui n'est pas "normal". Vous pourriez essayer le format mp/mixed avec une partie mp/alt contenant les deux formats de message et une seconde partie pour contenir la pièce jointe, mais je ne sais pas ce que les clients en feraient.

0 votes

@Iain Votre réponse est très spéciale car elle est la seule à contenir la structure (très bizarre) attendue par gmail. Je vais lui attribuer une prime.

0 votes

Voici un joli dessin ascii : stackoverflow.com/a/40420648/633961

6voto

RightHandedMonkey Points 614

En m'inspirant de l'exemple d'Iain, j'ai eu un besoin similaire de composer ces courriels avec du texte en clair, du HTML et des pièces jointes multiples, mais en utilisant PHP. Étant donné que nous utilisons Amazon SES pour envoyer des e-mails avec des pièces jointes, l'API vous oblige actuellement à créer l'e-mail à partir de zéro en utilisant la fonction sendRawEmail(...).

Après de nombreuses recherches (et une frustration plus grande que la normale), le problème a été résolu et le code source PHP a été publié afin d'aider d'autres personnes rencontrant un problème similaire. J'espère que cela aidera quelqu'un - la troupe de singes que j'ai forcée à travailler sur ce problème est maintenant épuisée.

Code source PHP pour l'envoi d'e-mails avec pièces jointes en utilisant Amazon SES.

<?php

require_once('AWSSDKforPHP/aws.phar');

use Aws\Ses\SesClient;

/**
 * SESUtils is a tool to make it easier to work with Amazon Simple Email Service
 * Features:
 * A client to prepare emails for use with sending attachments or not
 * 
 * There is no warranty - use this code at your own risk.  
 * @author sbossen with assistance from Michael Deal
 * http://righthandedmonkey.com
 *
 * Update: Error checking and new params input array provided by Michael Deal
 * Update2: Corrected for allowing to send multiple attachments and plain text/html body
 *   Ref: Http://stackoverflow.com/questions/3902455/smtp-multipart-alternative-vs-multipart-mixed/
 */
class SESUtils {

    const version = "1.0";
    const AWS_KEY = "YOUR-KEY";
    const AWS_SEC = "YOUR-SECRET";
    const AWS_REGION = "us-east-1";
    const MAX_ATTACHMENT_NAME_LEN = 60;

    /**
     * Usage:
        $params = array(
          "to" => "email1@gmail.com",
          "subject" => "Some subject",
          "message" => "<strong>Some email body</strong>",
          "from" => "sender@verifiedbyaws",
          //OPTIONAL
          "replyTo" => "reply_to@gmail.com",
          //OPTIONAL
          "files" => array(
            1 => array(
               "name" => "filename1", 
              "filepath" => "/path/to/file1.txt", 
              "mime" => "application/octet-stream"
            ),
            2 => array(
               "name" => "filename2", 
              "filepath" => "/path/to/file2.txt", 
              "mime" => "application/octet-stream"
            ),
          )
        );

      $res = SESUtils::sendMail($params);

     * NOTE: When sending a single file, omit the key (ie. the '1 =>') 
     * or use 0 => array(...) - otherwise the file will come out garbled
     * ie. use:
     *    "files" => array(
     *        0 => array( "name" => "filename", "filepath" => "path/to/file.txt",
     *        "mime" => "application/octet-stream")
     * 
     * For the 'to' parameter, you can send multiple recipiants with an array
     *    "to" => array("email1@gmail.com", "other@msn.com")
     * use $res->success to check if it was successful
     * use $res->message_id to check later with Amazon for further processing
     * use $res->result_text to look for error text if the task was not successful
     * 
     * @param array $params - array of parameters for the email
     * @return \ResultHelper
     */
    public static function sendMail($params) {

        $to = self::getParam($params, 'to', true);
        $subject = self::getParam($params, 'subject', true);
        $body = self::getParam($params, 'message', true);
        $from = self::getParam($params, 'from', true);
        $replyTo = self::getParam($params, 'replyTo');
        $files = self::getParam($params, 'files');

        $res = new ResultHelper();

        // get the client ready
        $client = SesClient::factory(array(
                    'key' => self::AWS_KEY,
                    'secret' => self::AWS_SEC,
                    'region' => self::AWS_REGION
        ));

        // build the message
        if (is_array($to)) {
            $to_str = rtrim(implode(',', $to), ',');
        } else {
            $to_str = $to;
        }

        $msg = "To: $to_str\n";
        $msg .= "From: $from\n";

        if ($replyTo) {
            $msg .= "Reply-To: $replyTo\n";
        }

        // in case you have funny characters in the subject
        $subject = mb_encode_mimeheader($subject, 'UTF-8');
        $msg .= "Subject: $subject\n";
        $msg .= "MIME-Version: 1.0\n";
        $msg .= "Content-Type: multipart/mixed;\n";
        $boundary = uniqid("_Part_".time(), true); //random unique string
        $boundary2 = uniqid("_Part2_".time(), true); //random unique string
        $msg .= " boundary=\"$boundary\"\n";
        $msg .= "\n";

        // now the actual body
        $msg .= "--$boundary\n";

        //since we are sending text and html emails with multiple attachments
        //we must use a combination of mixed and alternative boundaries
        //hence the use of boundary and boundary2
        $msg .= "Content-Type: multipart/alternative;\n";
        $msg .= " boundary=\"$boundary2\"\n";
        $msg .= "\n";
        $msg .= "--$boundary2\n";

        // first, the plain text
        $msg .= "Content-Type: text/plain; charset=utf-8\n";
        $msg .= "Content-Transfer-Encoding: 7bit\n";
        $msg .= "\n";
        $msg .= strip_tags($body); //remove any HTML tags
        $msg .= "\n";

        // now, the html text
        $msg .= "--$boundary2\n";
        $msg .= "Content-Type: text/html; charset=utf-8\n";
        $msg .= "Content-Transfer-Encoding: 7bit\n";
        $msg .= "\n";
        $msg .= $body; 
        $msg .= "\n";
        $msg .= "--$boundary2--\n";

        // add attachments
        if (is_array($files)) {
            $count = count($files);
            foreach ($files as $file) {
                $msg .= "\n";
                $msg .= "--$boundary\n";
                $msg .= "Content-Transfer-Encoding: base64\n";
                $clean_filename = self::clean_filename($file["name"], self::MAX_ATTACHMENT_NAME_LEN);
                $msg .= "Content-Type: {$file['mime']}; name=$clean_filename;\n";
                $msg .= "Content-Disposition: attachment; filename=$clean_filename;\n";
                $msg .= "\n";
                $msg .= base64_encode(file_get_contents($file['filepath']));
                $msg .= "\n--$boundary";
            }
            // close email
            $msg .= "--\n";
        }

        // now send the email out
        try {
            $ses_result = $client->sendRawEmail(
                    array(
                'RawMessage' => array(
                    'Data' => base64_encode($msg)
                )
                    ), array(
                'Source' => $from,
                'Destinations' => $to_str
                    )
            );
            if ($ses_result) {
                $res->message_id = $ses_result->get('MessageId');
            } else {
                $res->success = false;
                $res->result_text = "Amazon SES did not return a MessageId";
            }
        } catch (Exception $e) {
            $res->success = false;
            $res->result_text = $e->getMessage().
                    " - To: $to_str, Sender: $from, Subject: $subject";
        }
        return $res;
    }

    private static function getParam($params, $param, $required = false) {
        $value = isset($params[$param]) ? $params[$param] : null;
        if ($required && empty($value)) {
            throw new Exception('"'.$param.'" parameter is required.');
        } else {
            return $value;
        }
    }

    /**
    Clean filename function - to get a file friendly 
    **/
    public static function clean_filename($str, $limit = 0, $replace=array(), $delimiter='-') {
        if( !empty($replace) ) {
            $str = str_replace((array)$replace, ' ', $str);
        }

        $clean = iconv('UTF-8', 'ASCII//TRANSLIT', $str);
        $clean = preg_replace("/[^a-zA-Z0-9\.\/_| -]/", '', $clean);
        $clean = preg_replace("/[\/| -]+/", '-', $clean);

        if ($limit > 0) {
            //don't truncate file extension
            $arr = explode(".", $clean);
            $size = count($arr);
            $base = "";
            $ext = "";
            if ($size > 0) {
                for ($i = 0; $i < $size; $i++) {
                    if ($i < $size - 1) { //if it's not the last item, add to $bn
                        $base .= $arr[$i];
                        //if next one isn't last, add a dot
                        if ($i < $size - 2)
                            $base .= ".";
                    } else {
                        if ($i > 0)
                            $ext = ".";
                        $ext .= $arr[$i];
                    }
                }
            }
            $bn_size = mb_strlen($base);
            $ex_size = mb_strlen($ext);
            $bn_new = mb_substr($base, 0, $limit - $ex_size);
            // doing again in case extension is long
            $clean = mb_substr($bn_new.$ext, 0, $limit); 
        }
        return $clean;
    }

}

class ResultHelper {

    public $success = true;
    public $result_text = "";
    public $message_id = "";

}

?>

0 votes

C'est une solution géniale. En général, $boundary contiennent le corps entier avec les pièces jointes mais seulement $boundary2 contiennent du HTML ou du texte brut. Solution géniale. Dites-moi, s'il vous plaît, quelle est votre solution pour envoyer du texte brut, est-ce un message alternatif si le client de messagerie ne supporte pas le HTML ? Merci !

0 votes

Merci. Oui, j'envoie à la fois du texte brut et du HTML avec la solution ci-dessus. Le code supprime simplement le HTML en utilisant la fonction strip_tags($body) pour fournir le texte brut dans les cas où les navigateurs ne veulent pas utiliser le HTML. Si vous le souhaitez, vous pouvez mettre votre propre chaîne personnalisée à la place (par exemple, $body_plain_text).

5voto

XDevOne Points 53

Excellente réponse Lain !

J'ai fait deux ou trois choses pour que cela fonctionne sur un plus grand nombre d'appareils. À la fin, je vais énumérer les clients sur lesquels j'ai effectué les tests.

  1. J'ai ajouté un nouveau constructeur qui ne contient pas le paramètre attachments et n'utilise pas MimeMultipart("mixed"). Il n'y a pas besoin de mixed si vous envoyez uniquement des images en ligne.

    public Multipart build(String messageText, String messageHtml, List<URL> messageHtmlInline) throws MessagingException {
    
        final Multipart mpAlternative = new MimeMultipart("alternative");
        {
            //  Note: MUST RENDER HTML LAST otherwise iPad mail client only renders 
            //  the last image and no email
                addTextVersion(mpAlternative,messageText);
                addHtmlVersion(mpAlternative,messageHtml, messageHtmlInline);
        }
    
        return mpAlternative;
    }
  2. Dans la méthode addTextVersion, j'ai ajouté le jeu de caractères lors de l'ajout du contenu. Cela pourrait/devrait probablement être transmis, mais je l'ai simplement ajouté de manière statique.

    textPart.setContent(messageText, "text/plain");
    to
    textPart.setContent(messageText, "text/plain; charset=UTF-8");
  3. Le dernier point était l'ajout à la méthode addImagesInline. J'ai ajouté le nom de fichier de l'image à l'en-tête par le code suivant. Si vous ne faites pas cela, au moins sur le client de messagerie par défaut d'Android, il y aura des images en ligne dont le nom est inconnu et elles ne seront pas automatiquement téléchargées et présentées dans l'e-mail.

    for (URL img : embeded) {
        final MimeBodyPart htmlPartImg = new MimeBodyPart();
        DataSource htmlPartImgDs = new URLDataSource(img);
        htmlPartImg.setDataHandler(new DataHandler(htmlPartImgDs));
        String fileName = img.getFile();
        fileName = getFileName(fileName);
        String newFileName = cids.get(fileName);
        boolean imageNotReferencedInHtml = newFileName == null;
        if (imageNotReferencedInHtml) continue;
        htmlPartImg.setHeader("Content-ID", "<"+newFileName+">");
        htmlPartImg.setDisposition(BodyPart.INLINE);
        **htmlPartImg.setFileName(newFileName);**
        parent.addBodyPart(htmlPartImg);
    }

Enfin, voici la liste des clients sur lesquels j'ai effectué les tests. Outlook 2010, Outlook Web App, Internet Explorer 11, Firefox, Chrome, Outlook utilisant l'application native d'Apple, Courriel passant par Gmail - Navigateur client de messagerie, Internet Explorer 11, Firefox, Chrome, Client de messagerie par défaut Android, client de messagerie par défaut d'osx IPhone, Client de messagerie Gmail sur Android, client de messagerie Gmail sur IPhone, Email passant par Yahoo - Client de messagerie par navigateur, Internet Explorer 11, Firefox, Chrome, Client de messagerie par défaut Android, client de messagerie par défaut d'osx IPhone.

J'espère que cela aidera les autres.

0 votes

Merci pour les commentaires, tous les points sont bons. Je vais voir si je peux les inclure dans ma réponse.

4voto

Shivendra Gupta Points 378

Sous-type mixte

Le sous-type "mixte" de "multipart" est destiné à être utilisé lorsque le corps sont indépendants et doivent être regroupés dans un ordre particulier. Tout sous-type "multipart" qu'une implémentation ne reconnaît pas doit être traité comme étant de sous-type "mixte".

Sous-type alternatif

Le type "multipart/alternative" est syntaxiquement identique au type "multipart/mixed", mais la sémantique est différente. En particulier, chacune des parties du corps est une version "alternative" de la même information

Source :

3voto

ASPiRE Points 1180

J'ai créé un diagramme de hiérarchie pour mieux aider à visualiser la structure idéale. Chaque message circule séparément de la feuille à la racine.

Référence Microsoft : Hiérarchies MIME des parties du corps

Référence Microsoft : Parties du corps du message MIME

img1

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X