Mail mehrteilig / alternativ vs mehrteilig / gemischt


148

Bei der Erstellung von E - Mail - Nachrichten Sie sollen den festlegen Content-Type auf multipart/alternativebeim Senden von HTML und TEXT odermultipart/mixed wenn Text und Anhänge senden.

Was tun Sie also, wenn Sie HTML, Text und Anhänge senden möchten? Verwende beide?


1
Ich bin mir nicht sicher, wie ich das richtig machen soll. Ich habe sicherlich mp / alt-Nachrichten gesehen, die einen mp / text-Teil und einen mp / gemischten Teil mit HTML und Anhang enthielten ... aber das bedeutete, dass der Anhang nur beim Anzeigen von HTML sichtbar war, nicht beim Anzeigen von TEXT, sodass er "riecht". falsch. Sie könnten versuchen, mp / mit einem mp / alt-Teil zu mischen, der beide Nachrichtenformate und einen zweiten Teil enthält, um den Anhang zu enthalten, aber ich weiß nicht, was Clients daraus machen würden.
Dajames

@Iain Ihre Antwort ist etwas ganz Besonderes, da Sie als einzige die (sehr seltsame) Struktur enthält, die Google Mail erwartet. Ich werde Kopfgeld dafür vergeben.
PascalVKooten

Hier ist eine schöne ASCII-Kunst: stackoverflow.com/a/40420648/633961
guettli

Antworten:


147

Ich habe diese Herausforderung heute gemeistert und fand diese Antworten nützlich, aber für mich nicht explizit genug.

Bearbeiten : Ich habe gerade die Apache Commons-E-Mail gefunden , die dies gut zusammenfasst, was bedeutet, dass Sie es unten nicht wissen müssen.

Wenn Ihre Anforderung eine E-Mail mit:

  1. Text- und HTML-Versionen
  2. Die HTML-Version enthält eingebettete (Inline-) Bilder
  3. Anhänge

Die einzige Struktur, die ich gefunden habe und die mit Google Mail / Outlook / iPad funktioniert, ist:

  • gemischt
    • Alternative
      • Text
      • verbunden
        • html
        • Inline-Bild
        • Inline-Bild
    • Anhang
    • Anhang

Und der Code lautet:

import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.activation.URLDataSource;
import javax.mail.BodyPart;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMultipart;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Created by StrongMan on 25/05/14.
 */
public class MailContentBuilder {

    private static final Pattern COMPILED_PATTERN_SRC_URL_SINGLE = Pattern.compile("src='([^']*)'",  Pattern.CASE_INSENSITIVE);
    private static final Pattern COMPILED_PATTERN_SRC_URL_DOUBLE = Pattern.compile("src=\"([^\"]*)\"",  Pattern.CASE_INSENSITIVE);

    /**
     * Build an email message.
     *
     * The HTML may reference the embedded image (messageHtmlInline) using the filename. Any path portion is ignored to make my life easier
     * e.g. If you pass in the image C:\Temp\dog.jpg you can use <img src="dog.jpg"/> or <img src="C:\Temp\dog.jpg"/> and both will work
     *
     * @param messageText
     * @param messageHtml
     * @param messageHtmlInline
     * @param attachments
     * @return
     * @throws MessagingException
     */
    public Multipart build(String messageText, String messageHtml, List<URL> messageHtmlInline, List<URL> attachments) throws MessagingException {
        final Multipart mpMixed = new MimeMultipart("mixed");
        {
            // alternative
            final Multipart mpMixedAlternative = newChild(mpMixed, "alternative");
            {
                // Note: MUST RENDER HTML LAST otherwise iPad mail client only renders the last image and no email
                addTextVersion(mpMixedAlternative,messageText);
                addHtmlVersion(mpMixedAlternative,messageHtml, messageHtmlInline);
            }
            // attachments
            addAttachments(mpMixed,attachments);
        }

        //msg.setText(message, "utf-8");
        //msg.setContent(message,"text/html; charset=utf-8");
        return mpMixed;
    }

    private Multipart newChild(Multipart parent, String alternative) throws MessagingException {
        MimeMultipart child =  new MimeMultipart(alternative);
        final MimeBodyPart mbp = new MimeBodyPart();
        parent.addBodyPart(mbp);
        mbp.setContent(child);
        return child;
    }

    private void addTextVersion(Multipart mpRelatedAlternative, String messageText) throws MessagingException {
        final MimeBodyPart textPart = new MimeBodyPart();
        textPart.setContent(messageText, "text/plain");
        mpRelatedAlternative.addBodyPart(textPart);
    }

    private void addHtmlVersion(Multipart parent, String messageHtml, List<URL> embeded) throws MessagingException {
        // HTML version
        final Multipart mpRelated = newChild(parent,"related");

        // Html
        final MimeBodyPart htmlPart = new MimeBodyPart();
        HashMap<String,String> cids = new HashMap<String, String>();
        htmlPart.setContent(replaceUrlWithCids(messageHtml,cids), "text/html");
        mpRelated.addBodyPart(htmlPart);

        // Inline images
        addImagesInline(mpRelated, embeded, cids);
    }

    private void addImagesInline(Multipart parent, List<URL> embeded, HashMap<String,String> cids) throws MessagingException {
        if (embeded != null)
        {
            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;
                // Gmail requires the cid have <> around it
                htmlPartImg.setHeader("Content-ID", "<"+newFileName+">");
                htmlPartImg.setDisposition(BodyPart.INLINE);
                parent.addBodyPart(htmlPartImg);
            }
        }
    }

    private void addAttachments(Multipart parent, List<URL> attachments) throws MessagingException {
        if (attachments != null)
        {
            for (URL attachment : attachments)
            {
                final MimeBodyPart mbpAttachment = new MimeBodyPart();
                DataSource htmlPartImgDs = new URLDataSource(attachment);
                mbpAttachment.setDataHandler(new DataHandler(htmlPartImgDs));
                String fileName = attachment.getFile();
                fileName = getFileName(fileName);
                mbpAttachment.setDisposition(BodyPart.ATTACHMENT);
                mbpAttachment.setFileName(fileName);
                parent.addBodyPart(mbpAttachment);
            }
        }
    }

    public String replaceUrlWithCids(String html, HashMap<String,String> cids)
    {
        html = replaceUrlWithCids(html, COMPILED_PATTERN_SRC_URL_SINGLE, "src='cid:@cid'", cids);
        html = replaceUrlWithCids(html, COMPILED_PATTERN_SRC_URL_DOUBLE, "src=\"cid:@cid\"", cids);
        return html;
    }

    private String replaceUrlWithCids(String html, Pattern pattern, String replacement, HashMap<String,String> cids) {
        Matcher matcherCssUrl = pattern.matcher(html);
        StringBuffer sb = new StringBuffer();
        while (matcherCssUrl.find())
        {
            String fileName = matcherCssUrl.group(1);
            // Disregarding file path, so don't clash your filenames!
            fileName = getFileName(fileName);
            // A cid must start with @ and be globally unique
            String cid = "@" + UUID.randomUUID().toString() + "_" + fileName;
            if (cids.containsKey(fileName))
                cid = cids.get(fileName);
            else
                cids.put(fileName,cid);
            matcherCssUrl.appendReplacement(sb,replacement.replace("@cid",cid));
        }
        matcherCssUrl.appendTail(sb);
        html = sb.toString();
        return html;
    }

    private String getFileName(String fileName) {
        if (fileName.contains("/"))
            fileName = fileName.substring(fileName.lastIndexOf("/")+1);
        return fileName;
    }
}

Und ein Beispiel für die Verwendung mit Google Mail

/**
 * Created by StrongMan on 25/05/14.
 */
import com.sun.mail.smtp.SMTPTransport;

import java.net.URL;
import java.security.Security;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.activation.URLDataSource;
import javax.mail.*;
import javax.mail.internet.*;

/**
 *
 * http://stackoverflow.com/questions/14744197/best-practices-sending-javamail-mime-multipart-emails-and-gmail
 * http://stackoverflow.com/questions/3902455/smtp-multipart-alternative-vs-multipart-mixed
 *
 *
 *
 * @author doraemon
 */
public class GoogleMail {


    private GoogleMail() {
    }

    /**
     * Send email using GMail SMTP server.
     *
     * @param username GMail username
     * @param password GMail password
     * @param recipientEmail TO recipient
     * @param title title of the message
     * @param messageText message to be sent
     * @throws AddressException if the email address parse failed
     * @throws MessagingException if the connection is dead or not in the connected state or if the message is not a MimeMessage
     */
    public static void Send(final String username, final String password, String recipientEmail, String title, String messageText, String messageHtml, List<URL> messageHtmlInline, List<URL> attachments) throws AddressException, MessagingException {
        GoogleMail.Send(username, password, recipientEmail, "", title, messageText, messageHtml, messageHtmlInline,attachments);
    }

    /**
     * Send email using GMail SMTP server.
     *
     * @param username GMail username
     * @param password GMail password
     * @param recipientEmail TO recipient
     * @param ccEmail CC recipient. Can be empty if there is no CC recipient
     * @param title title of the message
     * @param messageText message to be sent
     * @throws AddressException if the email address parse failed
     * @throws MessagingException if the connection is dead or not in the connected state or if the message is not a MimeMessage
     */
    public static void Send(final String username, final String password, String recipientEmail, String ccEmail, String title, String messageText, String messageHtml, List<URL> messageHtmlInline, List<URL> attachments) throws AddressException, MessagingException {
        Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider());
        final String SSL_FACTORY = "javax.net.ssl.SSLSocketFactory";

        // Get a Properties object
        Properties props = System.getProperties();
        props.setProperty("mail.smtps.host", "smtp.gmail.com");
        props.setProperty("mail.smtp.socketFactory.class", SSL_FACTORY);
        props.setProperty("mail.smtp.socketFactory.fallback", "false");
        props.setProperty("mail.smtp.port", "465");
        props.setProperty("mail.smtp.socketFactory.port", "465");
        props.setProperty("mail.smtps.auth", "true");

        /*
        If set to false, the QUIT command is sent and the connection is immediately closed. If set
        to true (the default), causes the transport to wait for the response to the QUIT command.

        ref :   http://java.sun.com/products/javamail/javadocs/com/sun/mail/smtp/package-summary.html
                http://forum.java.sun.com/thread.jspa?threadID=5205249
                smtpsend.java - demo program from javamail
        */
        props.put("mail.smtps.quitwait", "false");

        Session session = Session.getInstance(props, null);

        // -- Create a new message --
        final MimeMessage msg = new MimeMessage(session);

        // -- Set the FROM and TO fields --
        msg.setFrom(new InternetAddress(username + "@gmail.com"));
        msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse(recipientEmail, false));

        if (ccEmail.length() > 0) {
            msg.setRecipients(Message.RecipientType.CC, InternetAddress.parse(ccEmail, false));
        }

        msg.setSubject(title);

        // mixed
        MailContentBuilder mailContentBuilder = new MailContentBuilder();
        final Multipart mpMixed = mailContentBuilder.build(messageText, messageHtml, messageHtmlInline, attachments);
        msg.setContent(mpMixed);
        msg.setSentDate(new Date());

        SMTPTransport t = (SMTPTransport)session.getTransport("smtps");

        t.connect("smtp.gmail.com", username, password);
        t.sendMessage(msg, msg.getAllRecipients());
        t.close();
    }

}

1
Könnte jemand kommentieren, wie dies in PHP gemacht wird?
RightHandedMonkey

1
@RightHandedMonkey Vielleicht möchten Sie dies als neue Frage stellen, ich kann nicht für PHP sprechen.
Iain

1
Vielen Dank für die hervorragende Antwort @Iain! Es fällt mir schwer, die richtige MIME-Struktur für meinen Fall zu finden, in der ich versuche, dem Textkörper der E-Mail ein HTML-Teil-Präfix hinzuzufügen. Einige Clients erhalten jedoch einen leeren Textkörper ohne Anhänge, andere den Textkörper nur in den Anhängen (leerer Textkörper) (Outlook unter Windows) und einige funktionieren gut (GMail-Web, Android-App usw.). Bitte werfen Sie einen Blick, wenn möglich: stackoverflow.com/questions/47312409/…
shachar0n

1
Erstaunliche Antwort. Hat geholfen, mein Problem zu beheben, weil ich nicht wusste, wie man eine HTML- und Textversion einer E-Mail in einer sendet.
Dinukadev

1
Es ist nicht in Form von "<" id-left "@" id-right ">".
Michael-O

113

Verwenden Sie multipart/mixedmit dem ersten Teil als multipart/alternativeund den nachfolgenden Teilen für die Anhänge. Verwenden text/plainund text/htmlTeile innerhalb des multipart/alternativeTeils.

Ein fähiger E-Mail-Client sollte dann den multipart/alternativeTeil erkennen und den Textteil oder HTML-Teil nach Bedarf anzeigen. Es sollten auch alle nachfolgenden Teile als Anbauteile angezeigt werden.

Hierbei ist zu beachten, dass es in mehrteiligen MIME-Nachrichten durchaus gültig ist, Teile in Teilen zu haben. Theoretisch kann sich diese Verschachtelung auf jede Tiefe erstrecken. Jeder einigermaßen fähige E-Mail-Client sollte dann in der Lage sein, alle Nachrichtenteile rekursiv zu verarbeiten.


17
Vergessen Sie nicht, die Teile von Ihnen multipart/alternativerichtig zu bestellen . Der letzte Eintrag ist der Teil mit der besten / höchsten Priorität, daher möchten Sie den text/htmlTeil wahrscheinlich als letzten Unterabschnitt einfügen. Per RFC1341 .
Luna

4
Was multipart/relatedist mit und wann?
Wilt

4
@Wilt: Gibt an, multipart/alternativedass nur eines der enthaltenen Teile angezeigt werden soll - z. B. ein Teil text/plainund ein Teil text/html. Der E-Mail-Client sollte also nicht beide Teile anzeigen, sondern nur einen. dh sie sind nicht verwandt. multipart/relatedgibt an, dass die verschiedenen Unterteile alle Teil des Hauptstammteils sind, z. B. der Hauptteil text/htmlund die Unterteile eingebettete Bilder sind. Sehen Sie hier für weitere Informationen.
RaelB

Nur ein kurzer Tipp - Wenn Sie Zugriff auf eine * nix-Box haben, können Sie mit dem muttCLI-Client überprüfen, ob Sie Ihre mehrteiligen MIME-Nachrichten korrekt eingerichtet haben. Wenn Sie vwährend der Anzeige einer Nachricht drücken , wird der verschachtelte Baum der MIME-Teile angezeigt und durchlaufen.
Rinogo

21

Nachrichten haben Inhalt. Inhalt kann Text, HTML, ein DataHandler oder ein Multipart sein, und es kann nur einen Inhalt geben. Multiparts haben nur BodyParts, können aber mehrere haben. BodyParts können wie Nachrichten bereits beschriebene Inhalte enthalten.

Eine Nachricht mit HTML, Text und einem Anhang kann hierarchisch wie folgt angezeigt werden:

message
  mainMultipart (content for message, subType="mixed")
    ->htmlAndTextBodyPart (bodyPart1 for mainMultipart)
      ->htmlAndTextMultipart (content for htmlAndTextBodyPart, subType="alternative")
        ->textBodyPart (bodyPart2 for the htmlAndTextMultipart)
          ->text (content for textBodyPart)
        ->htmlBodyPart (bodyPart1 for htmlAndTextMultipart)
          ->html (content for htmlBodyPart)
    ->fileBodyPart1 (bodyPart2 for the mainMultipart)
      ->FileDataHandler (content for fileBodyPart1 )

Und der Code zum Erstellen einer solchen Nachricht:

    // the parent or main part if you will
    Multipart mainMultipart = new MimeMultipart("mixed");

    // this will hold text and html and tells the client there are 2 versions of the message (html and text). presumably text
    // being the alternative to html
    Multipart htmlAndTextMultipart = new MimeMultipart("alternative");

    // set text
    MimeBodyPart textBodyPart = new MimeBodyPart();
    textBodyPart.setText(text);
    htmlAndTextMultipart.addBodyPart(textBodyPart);

    // set html (set this last per rfc1341 which states last = best)
    MimeBodyPart htmlBodyPart = new MimeBodyPart();
    htmlBodyPart.setContent(html, "text/html; charset=utf-8");
    htmlAndTextMultipart.addBodyPart(htmlBodyPart);

    // stuff the multipart into a bodypart and add the bodyPart to the mainMultipart
    MimeBodyPart htmlAndTextBodyPart = new MimeBodyPart();
    htmlAndTextBodyPart.setContent(htmlAndTextMultipart);
    mainMultipart.addBodyPart(htmlAndTextBodyPart);

    // attach file body parts directly to the mainMultipart
    MimeBodyPart filePart = new MimeBodyPart();
    FileDataSource fds = new FileDataSource("/path/to/some/file.txt");
    filePart.setDataHandler(new DataHandler(fds));
    filePart.setFileName(fds.getName());
    mainMultipart.addBodyPart(filePart);

    // set message content
    message.setContent(mainMultipart);

@splahout Vielen Dank und herzlichen Glückwunsch zu Ihrer klaren und umfassenden Antwort. Für mich war es so nützlich.
Marti Pàmies Solà

9

Ich bin auf dieses Problem gestoßen. Diese Architektur (nach Lains Antwort) hat bei mir funktioniert. Hier ist die Lösung in Python.

  • gemischt
    • Alternative
      • Text
      • verbunden
        • html
        • Inline-Bild
        • Inline-Bild
    • Anhang
    • Anhang

Hier ist die Hauptfunktion zum Erstellen von E-Mails:

def create_message_with_attachment(
    sender, to, subject, msgHtml, msgPlain, attachmentFile):
    """Create a message for an email.

    Args:
      sender: Email address of the sender.
      to: Email address of the receiver.
      subject: The subject of the email message.
      message_text: The text of the email message.
      file: The path to the file to be attached.

    Returns:
      An object containing a base64url encoded email object.
    """
    message = MIMEMultipart('mixed')
    message['to'] = to
    message['from'] = sender
    message['subject'] = subject

    message_alternative = MIMEMultipart('alternative')
    message_related = MIMEMultipart('related')

    message_related.attach(MIMEText(msgHtml, 'html'))
    message_alternative.attach(MIMEText(msgPlain, 'plain'))
    message_alternative.attach(message_related)

    message.attach(message_alternative)

    print "create_message_with_attachment: file:", attachmentFile
    content_type, encoding = mimetypes.guess_type(attachmentFile)

    if content_type is None or encoding is not None:
        content_type = 'application/octet-stream'
    main_type, sub_type = content_type.split('/', 1)
    if main_type == 'text':
        fp = open(attachmentFile, 'rb')
        msg = MIMEText(fp.read(), _subtype=sub_type)
        fp.close()
    elif main_type == 'image':
        fp = open(attachmentFile, 'rb')
        msg = MIMEImage(fp.read(), _subtype=sub_type)
        fp.close()
    elif main_type == 'audio':
        fp = open(attachmentFile, 'rb')
        msg = MIMEAudio(fp.read(), _subtype=sub_type)
        fp.close()
    else:
        fp = open(attachmentFile, 'rb')
        msg = MIMEBase(main_type, sub_type)
        msg.set_payload(fp.read())
        fp.close()
    filename = os.path.basename(attachmentFile)
    msg.add_header('Content-Disposition', 'attachment', filename=filename)
    message.attach(msg)

    return {'raw': base64.urlsafe_b64encode(message.as_string())}

Hier ist der vollständige Code zum Senden einer E-Mail mit HTML / Text / Anhang:

import httplib2
import os
import oauth2client
from oauth2client import client, tools
import base64
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from apiclient import errors, discovery
import mimetypes
from email.mime.image import MIMEImage
from email.mime.audio import MIMEAudio
from email.mime.base import MIMEBase

SCOPES = 'https://www.googleapis.com/auth/gmail.send'
CLIENT_SECRET_FILE1 = 'client_secret.json'
location = os.path.realpath(
    os.path.join(os.getcwd(), os.path.dirname(__file__)))
CLIENT_SECRET_FILE = os.path.join(location, CLIENT_SECRET_FILE1)
APPLICATION_NAME = 'Gmail API Python Send Email'

def get_credentials():
    home_dir = os.path.expanduser('~')
    credential_dir = os.path.join(home_dir, '.credentials')
    if not os.path.exists(credential_dir):
        os.makedirs(credential_dir)
    credential_path = os.path.join(credential_dir,
                                   'gmail-python-email-send.json')
    store = oauth2client.file.Storage(credential_path)
    credentials = store.get()
    if not credentials or credentials.invalid:
        flow = client.flow_from_clientsecrets(CLIENT_SECRET_FILE, SCOPES)
        flow.user_agent = APPLICATION_NAME
        credentials = tools.run_flow(flow, store)
        print 'Storing credentials to ' + credential_path
    return credentials

def SendMessageWithAttachment(sender, to, subject, msgHtml, msgPlain, attachmentFile):
    credentials = get_credentials()
    http = credentials.authorize(httplib2.Http())
    service = discovery.build('gmail', 'v1', http=http)
    message1 = create_message_with_attachment(sender, to, subject, msgHtml, msgPlain, attachmentFile)
    SendMessageInternal(service, "me", message1)

def SendMessageInternal(service, user_id, message):
    try:
        message = (service.users().messages().send(userId=user_id, body=message).execute())
        print 'Message Id: %s' % message['id']
        return message
    except errors.HttpError, error:
        print 'An error occurred: %s' % error
        return "error"

def create_message_with_attachment(
    sender, to, subject, msgHtml, msgPlain, attachmentFile):
    """Create a message for an email.

    Args:
      sender: Email address of the sender.
      to: Email address of the receiver.
      subject: The subject of the email message.
      message_text: The text of the email message.
      file: The path to the file to be attached.

    Returns:
      An object containing a base64url encoded email object.
    """
    message = MIMEMultipart('mixed')
    message['to'] = to
    message['from'] = sender
    message['subject'] = subject

    message_alternative = MIMEMultipart('alternative')
    message_related = MIMEMultipart('related')

    message_related.attach(MIMEText(msgHtml, 'html'))
    message_alternative.attach(MIMEText(msgPlain, 'plain'))
    message_alternative.attach(message_related)

    message.attach(message_alternative)

    print "create_message_with_attachment: file:", attachmentFile
    content_type, encoding = mimetypes.guess_type(attachmentFile)

    if content_type is None or encoding is not None:
        content_type = 'application/octet-stream'
    main_type, sub_type = content_type.split('/', 1)
    if main_type == 'text':
        fp = open(attachmentFile, 'rb')
        msg = MIMEText(fp.read(), _subtype=sub_type)
        fp.close()
    elif main_type == 'image':
        fp = open(attachmentFile, 'rb')
        msg = MIMEImage(fp.read(), _subtype=sub_type)
        fp.close()
    elif main_type == 'audio':
        fp = open(attachmentFile, 'rb')
        msg = MIMEAudio(fp.read(), _subtype=sub_type)
        fp.close()
    else:
        fp = open(attachmentFile, 'rb')
        msg = MIMEBase(main_type, sub_type)
        msg.set_payload(fp.read())
        fp.close()
    filename = os.path.basename(attachmentFile)
    msg.add_header('Content-Disposition', 'attachment', filename=filename)
    message.attach(msg)

    return {'raw': base64.urlsafe_b64encode(message.as_string())}


def main():
    to = "to@address.com"
    sender = "from@address.com"
    subject = "subject"
    msgHtml = "Hi<br/>Html Email"
    msgPlain = "Hi\nPlain Email"
    attachment = "/path/to/file.pdf"
    SendMessageWithAttachment(sender, to, subject, msgHtml, msgPlain, attachment)

if __name__ == '__main__':
    main()

5

Aufbauend auf Iains Beispiel hatte ich ein ähnliches Bedürfnis, diese E-Mails mit separatem Klartext, HTML und mehreren Anhängen zu verfassen, aber mit PHP. Da wir Amazon SES zum Senden von E-Mails mit Anhängen verwenden, müssen Sie die E-Mail derzeit mithilfe der Funktion sendRawEmail (...) von Grund auf neu erstellen.

Nach vielen Nachforschungen (und mehr als normaler Frustration) wurde das Problem gelöst und der PHP-Quellcode veröffentlicht, damit er anderen helfen kann, bei denen ein ähnliches Problem auftritt. Hoffe, das hilft jemandem - die Affentruppe, die ich gezwungen habe, an diesem Problem zu arbeiten, ist jetzt erschöpft.

PHP-Quellcode zum Senden von E-Mails mit Anhängen über 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 = "";

}

?>

Das ist geniale Lösung Mann. Im Allgemeinen $boundaryenthalten sie den ganzen Körper mit Anhängen, aber nur $boundary2HTML oder einfachen Text. Geniale Lösung. Sagen Sie mir bitte, dies ist Ihre Lösung für das Senden von Klartext. Ist dies eine alternative Nachricht, wenn der Mail-Client HTML nicht unterstützt? Vielen Dank!
Ivijan Stefan Stipić

Vielen Dank. Ja, ich sende sowohl einfachen Text als auch HTML mit der obigen Lösung. Der Code entfernt einfach den HTML-Code mithilfe der strip_tags ($ body), um den Klartext in Fällen für Browser bereitzustellen, die den HTML-Code nicht verwenden möchten. Falls gewünscht, können Sie stattdessen Ihre eigene benutzerdefinierte Zeichenfolge dort einfügen (z. B. $ body_plain_text).
RightHandedMonkey

5

Tolle Antwort Lain!

Es gab ein paar Dinge, die ich getan habe, um diese Arbeit in einem breiteren Satz von Geräten zu machen. Am Ende werde ich die Clients auflisten, auf denen ich getestet habe.

  1. Ich habe einen neuen Build-Konstruktor hinzugefügt, der die Parameteranhänge nicht enthielt und kein MimeMultipart ("gemischt") verwendete. Sie müssen nicht gemischt werden, wenn Sie nur Inline-Bilder senden.

    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. In der addTextVersion-Methode habe ich beim Hinzufügen von Inhalten einen Zeichensatz hinzugefügt, der wahrscheinlich übergeben werden könnte / sollte, aber ich habe ihn nur statisch hinzugefügt.

    textPart.setContent(messageText, "text/plain");
    to
    textPart.setContent(messageText, "text/plain; charset=UTF-8");
    
  3. Das letzte Element wurde zur addImagesInline-Methode hinzugefügt. Ich habe die Einstellung des Bilddateinamens zum Header durch den folgenden Code hinzugefügt. Wenn Sie dies nicht tun, werden zumindest auf dem Standard-Mail-Client von Android Inline-Bilder mit dem Namen Unbekannt angezeigt, die nicht automatisch heruntergeladen und in E-Mails angezeigt werden.

    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);
    }
    

Dies ist also die Liste der Clients, auf denen ich getestet habe. Outlook 2010, Outlook Web App, Internet Explorer 11, Firefox, Chrome, Outlook mit der nativen App von Apple, E-Mail über Google Mail - Browser-Mail-Client, Internet Explorer 11, Firefox, Chrome, Standard-Mail-Client für Android, Standard-Mail-Client für osx IPhone, Google Mail E-Mail-Client unter Android, Google Mail-E-Mail-Client unter IPhone, E-Mail über Yahoo - Browser-E-Mail-Client, Internet Explorer 11, Firefox, Chrome, Standard-E-Mail-Client für Android, Standard-E-Mail-Client für osx IPhone.

Hoffe das hilft jemand anderem.


Danke für das Feedback, alle guten Punkte. Ich werde sehen, wie ich sie in meine Antwort einbeziehe.
Iain

5

Hier ist das Beste: Mehrteilige / gemischte MIME-Nachricht mit Anhängen und Inline-Bildern

Und Bild: https://www.qcode.co.uk/images/mime-nesting-structure.png

From: from@qcode.co.uk
To: to@@qcode.co.uk
Subject: Example Email
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="MixedBoundaryString"

--MixedBoundaryString
Content-Type: multipart/related; boundary="RelatedBoundaryString"

--RelatedBoundaryString
Content-Type: multipart/alternative; boundary="AlternativeBoundaryString"

--AlternativeBoundaryString
Content-Type: text/plain;charset="utf-8"
Content-Transfer-Encoding: quoted-printable

This is the plain text part of the email.

--AlternativeBoundaryString
Content-Type: text/html;charset="utf-8"
Content-Transfer-Encoding: quoted-printable

<html>
  <body>=0D
    <img src=3D=22cid:masthead.png=40qcode.co.uk=22 width 800 height=3D80=
 =5C>=0D
    <p>This is the html part of the email.</p>=0D
    <img src=3D=22cid:logo.png=40qcode.co.uk=22 width 200 height=3D60 =5C=
>=0D
  </body>=0D
</html>=0D

--AlternativeBoundaryString--

--RelatedBoundaryString
Content-Type: image/jpgeg;name="logo.png"
Content-Transfer-Encoding: base64
Content-Disposition: inline;filename="logo.png"
Content-ID: <logo.png@qcode.co.uk>

amtsb2hiaXVvbHJueXZzNXQ2XHVmdGd5d2VoYmFmaGpremxidTh2b2hydHVqd255aHVpbnRyZnhu
dWkgb2l1b3NydGhpdXRvZ2hqdWlyb2h5dWd0aXJlaHN1aWhndXNpaHhidnVqZmtkeG5qaG5iZ3Vy
...
...
a25qbW9nNXRwbF0nemVycHpvemlnc3k5aDZqcm9wdHo7amlodDhpOTA4N3U5Nnkwb2tqMm9sd3An
LGZ2cDBbZWRzcm85eWo1Zmtsc2xrZ3g=

--RelatedBoundaryString
Content-Type: image/jpgeg;name="masthead.png"
Content-Transfer-Encoding: base64
Content-Disposition: inline;filename="masthead.png"
Content-ID: <masthead.png@qcode.co.uk>

aXR4ZGh5Yjd1OHk3MzQ4eXFndzhpYW9wO2tibHB6c2tqOTgwNXE0aW9qYWJ6aXBqOTBpcjl2MC1t
dGlmOTA0cW05dGkwbWk0OXQwYVttaXZvcnBhXGtsbGo7emt2c2pkZnI7Z2lwb2F1amdpNTh1NDlh
...
...
eXN6dWdoeXhiNzhuZzdnaHQ3eW9zemlqb2FqZWt0cmZ1eXZnamhka3JmdDg3aXV2dWd5aGVidXdz
dhyuhehe76YTGSFGA=

--RelatedBoundaryString--

--MixedBoundaryString
Content-Type: application/pdf;name="Invoice_1.pdf"
Content-Transfer-Encoding: base64
Content-Disposition: attachment;filename="Invoice_1.pdf"

aGZqZGtsZ3poZHVpeWZoemd2dXNoamRibngganZodWpyYWRuIHVqO0hmSjtyRVVPIEZSO05SVURF
SEx1aWhudWpoZ3h1XGh1c2loZWRma25kamlsXHpodXZpZmhkcnVsaGpnZmtsaGVqZ2xod2plZmdq
...
...
a2psajY1ZWxqanNveHV5ZXJ3NTQzYXRnZnJhZXdhcmV0eXRia2xhanNueXVpNjRvNWllc3l1c2lw
dWg4NTA0

--MixedBoundaryString
Content-Type: application/pdf;name="SpecialOffer.pdf"
Content-Transfer-Encoding: base64
Content-Disposition: attachment;filename="SpecialOffer.pdf"

aXBvY21odWl0dnI1dWk4OXdzNHU5NTgwcDN3YTt1OTQwc3U4NTk1dTg0dTV5OGlncHE1dW4zOTgw
cS0zNHU4NTk0eWI4OTcwdjg5MHE4cHV0O3BvYTt6dWI7dWlvenZ1em9pdW51dDlvdTg5YnE4N3Z3
...
...
OTViOHk5cDV3dTh5bnB3dWZ2OHQ5dTh2cHVpO2p2Ymd1eTg5MGg3ajY4bjZ2ODl1ZGlvcjQ1amts
dfnhgjdfihn=

--MixedBoundaryString--

.

Schema mehrteilig / verwandt / alternativ

Header
|From: email
|To: email
|MIME-Version: 1.0
|Content-Type: multipart/mixed; boundary="boundary1";
Message body
|multipart/mixed --boundary1
|--boundary1
|   multipart/related --boundary2
|   |--boundary2
|   |   multipart/alternative --boundary3
|   |   |--boundary3
|   |   |text/plain
|   |   |--boundary3
|   |   |text/html
|   |   |--boundary3--
|   |--boundary2    
|   |Inline image
|   |--boundary2    
|   |Inline image
|   |--boundary2--
|--boundary1    
|Attachment1
|--boundary1
|Attachment2
|--boundary1
|Attachment3
|--boundary1--
|
.

1
Links zu externen Ressourcen werden empfohlen. Fügen Sie jedoch einen Kontext um den Link hinzu, damit Ihre Mitbenutzer eine Vorstellung davon haben, was es ist und warum es dort ist. Zitieren Sie immer den relevantesten Teil eines wichtigen Links, falls die Zielwebsite nicht erreichbar ist oder dauerhaft offline geht. Sehen Sie, wie man antwortet
SilverNak

Diese Antwort funktioniert bei mir nicht. Nur der erste Anhang wird tatsächlich gesendet ...
Jim

1

Gemischter Untertyp

Der "gemischte" Subtyp von "mehrteilig" ist zur Verwendung vorgesehen, wenn die Körperteile unabhängig sind und in einer bestimmten Reihenfolge gebündelt werden müssen. Alle "mehrteiligen" Subtypen, die eine Implementierung nicht erkennt, müssen als vom Subtyp "gemischt" behandelt werden.

Alternativer Subtyp

Der Typ "mehrteilig / alternativ" ist syntaktisch identisch mit "mehrteilig / gemischt", aber die Semantik ist unterschiedlich. Insbesondere ist jedes der Körperteile eine "alternative" Version derselben Information

Quelle

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.