Пошта багатостороння / альтернатива проти багаточастинної / змішана


148

При створенні повідомлення електронної пошти , ви повинні встановити Content-Type для multipart/alternativeпри відправці HTML і TEXT або multipart/mixedпід час надсилання текстових та вкладень.

Отже, що робити, якщо ви хочете надсилати HTML, текст та вкладення? Використовувати обидва?


1
Я не впевнений, який "правильний" спосіб це зробити. Я, звичайно, бачив mp / alt повідомлення, які мали mp / text частину та mp / змішану частину, що містять HTML та вкладення ... але це означало, що вкладення було видно лише при перегляді HTML, а не під час перегляду TEXT, щоб воно "пахло" неправильно. Ви можете спробувати mp / змішати з частиною mp / alt, що містить обидва формати повідомлень, і другу частину, яка містить вкладення, але я не знаю, що з цього зробили б клієнти.
dajames

@Iain Ваша відповідь дуже особлива тим, що єдина, що містить (дуже дивну) структуру, яку очікує Gmail. Я нагороджую за це щедротою.
PascalVKooten

Ось хороший ASCII Art: stackoverflow.com/a/40420648/633961
guettli

Відповіді:


147

Сьогодні я потрапив у цей виклик, і я вважав ці відповіді корисними, але не досить явними для мене.

Редагувати : Щойно знайшли електронну пошту Apache Commons, яка добре завершує це, тобто вам не потрібно знати це нижче.

Якщо вашою вимогою є електронний лист із:

  1. текстові та html версії
  2. html версія має вбудовані (вбудовані) зображення
  3. вкладення

Єдина структура, яку я знайшов, що працює з Gmail / Outlook / iPad, це:

  • змішаний
    • альтернатива
      • текст
      • пов'язані
        • html
        • вбудоване зображення
        • вбудоване зображення
    • вкладення
    • вкладення

А код такий:

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

І приклад використання його з Gmail

/**
 * 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
Може хтось прокоментує, як це зробити в PHP?
RightHandedMonkey

1
@RightHandedMonkey Ви можете задати це як нове запитання, я не можу говорити за php.
Iain

1
Дякую за чудову відповідь @Iain! Мені важко намагатися отримати правильну структуру MIME для мого випадку, в якій я намагаюся додати HTML-частину "префікса" до тіла електронної пошти; але деякі клієнти отримують порожнє тіло без вкладених файлів, деякі отримують тіло лише у вкладених файлах (порожнє тіло) (Outlook у Windows), а деякі працюють добре (веб-сайт GMail, Android App тощо). Будь ласка , зверніть увагу , якщо це можливо: stackoverflow.com/questions/47312409 / ...
shachar0n

1
Дивовижна відповідь. Допомогли виправити свою проблему, не знаючи, як надіслати HTML та текстову версію електронного листа в одній.
dinukadev

1
Це не у формі "<" id-left "@" id-right ">".
Майкл-О

113

Використовуйте multipart/mixedз першою частиною як multipart/alternativeі наступними частинами для насадок. У свою чергу, використовуйте text/plainі text/htmlдеталі всередині multipart/alternativeдеталі.

Потім здатний клієнт електронної пошти повинен розпізнати multipart/alternativeчастину та відобразити текстову частину або частину html за потребою. Він також повинен показувати всі наступні частини як деталі кріплення.

Тут важливо зауважити, що у багатоповерхових MIME-повідомленнях цілком справедливо мати частини всередині частин. Теоретично, це гніздування може поширюватися на будь-яку глибину. Будь-який достатньо здатний клієнт електронної пошти повинен мати можливість рекурсивно обробляти всі частини повідомлення.


17
Не забувайте правильно упорядкувати підрозділи multipart/alternative. Останній запис є найкращою / найвищою пріоритетною частиною, тому ви, ймовірно, хочете поставити text/htmlчастину як останню підрозділ. За RFC1341 .
Луна

4
Що про те multipart/related, і коли це використовувати?
Загинув

4
@ Wilt: multipart/alternativeпозначає, що повинна відображатися лише одна із включених частин - наприклад, одна частина є, text/plainа одна - частина text/html. Тому клієнт електронної пошти повинен відображати не дві частини, а лише одну. тобто вони не пов'язані між собою. multipart/relatedвказує на те, що різні підрозділи є частиною основної кореневої частини, наприклад, основна частина є, text/htmlа підрозділи - вбудованими зображеннями. Дивіться тут для отримання додаткової інформації.
RaelB

Лише швидка порада - Якщо у вас є доступ до * nix box, ви можете скористатися muttклієнтом CLI, щоб перевірити, чи правильно ви налаштували багатоповерхові повідомлення MIME. Якщо натиснути vпід час перегляду повідомлення, воно відобразить та дозволить пройти вкладене дерево MIME-частин.
риного

21

Повідомлення мають зміст. Вміст може бути текстовим, html, DataHandler або Multipart, а вміст може бути лише одним. У кількох частинах є лише BodyParts, але їх може бути більше. BodyParts, як і Повідомлення, може мати вміст, який вже був описаний.

Повідомлення з HTML, текстом та вкладенням можна переглядати ієрархічно так:

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 )

І код для створення такого повідомлення:

    // 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 Дякую вам велике і вітаю за вашу чітку і широку відповідь, для мене це було так корисно.
Marti Pàmies Solà

9

Я торкнувся цього питання. Ця архітектура (з відповіді Лайна) працювала для мене. Ось рішення в Python.

  • змішаний
    • альтернатива
      • текст
      • пов'язані
        • html
        • вбудоване зображення
        • вбудоване зображення
    • вкладення
    • вкладення

Ось основна функція створення електронної пошти:

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

Ось повний код для надсилання електронного листа, що містить html / текст / вкладення:

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

Спираючись на приклад Іена, у мене була подібна потреба складати ці електронні листи з окремим простим текстом, HTML та кількома вкладеннями, але за допомогою PHP. Оскільки ми використовуємо Amazon SES для надсилання електронних листів із вкладеннями, API наразі вимагає від вас створити електронну пошту з нуля за допомогою функції sendRawEmail (...).

Після довгих розслідувань (і більших, ніж звичайних розладів), проблема була вирішена, і вихідний код PHP був розміщений так, що може допомогти іншим, хто відчуває подібну проблему. Сподіваюся, це допоможе комусь вийти з ладу мавп мавп, яких я змусив працювати над цією проблемою, зараз вичерпані.

PHP вихідний код для надсилання електронних листів із вкладеннями за допомогою 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 = "";

}

?>

Це геніальне рішення людини. Як правило, $boundaryмістять у всьому тілі з вкладеннями, але $boundary2містять лише HTML або звичайний текст. Геніальне рішення. Скажіть, будь ласка, це ваше рішення для надсилання простого тексту, чи це альтернативне повідомлення, якщо поштовий клієнт не підтримує HTML? Дякую!
Івіджан Стефан Стипіч

Дякую. Так, я надсилаю як звичайний текст, так і HTML із вказаним вище рішенням. Код просто викреслює HTML, використовуючи strip_tags ($ body) для надання простого тексту у випадках для браузерів, які не хочуть використовувати HTML. При бажанні ви можете замість цього поставити власну власну рядок (тобто $ body_plain_text).
RightHandedMonkey

5

Чудова відповідь Лайн!

Я зробив пару речей, щоб зробити цю роботу в більш широкому наборі пристроїв. Наприкінці я перелічу клієнтів, на яких я тестувався.

  1. Я додав новий конструктор збірки, який не містив додатків параметрів і не використовував MimeMultipart ("змішаний"). Немає потреби в змішаному, якщо ви надсилаєте лише вбудовані зображення.

    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. У методі addTextVersion я додав схему, додаючи вміст, який, ймовірно, міг би / повинен бути переданий, але я просто додав його статично.

    textPart.setContent(messageText, "text/plain");
    to
    textPart.setContent(messageText, "text/plain; charset=UTF-8");
    
  3. Останній елемент додавали до методу addImagesInline. Я додав налаштування імені файлу зображення в заголовок наступним кодом. Якщо ви цього не зробите, то принаймні на поштовому клієнті Android за замовчуванням він матиме вбудовані зображення з іменем Невідомо та не завантажуватиме їх автоматично та відображатиметься в електронній пошті.

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

Отже, нарешті, це список клієнтів, на яких я тестувався. Outlook 2010, веб-додаток Outlook, Internet Explorer 11, Firefox, Chrome, Outlook за допомогою власного додатка Apple, електронна пошта, що проходить через Gmail - поштовий клієнт браузера, Internet Explorer 11, Firefox, Chrome, поштовий клієнт за замовчуванням Android, osx IPhone замовчувальний поштовий клієнт, Gmail поштовий клієнт на Android, поштовий клієнт Gmail на IPhone, електронна пошта, що проходить через Yahoo - поштовий клієнт браузера, Internet Explorer 11, Firefox, Chrome, поштовий клієнт за замовчуванням Android, osx IPhone замовчувальний поштовий клієнт.

Сподіваюсь, що хтось допомагає.


Дякуємо за відгуки, всі хороші моменти. Я побачу про включення їх у свою відповідь.
Iain

5

Ось найкраще: багатомісне / змішане повідомлення міми з вкладеннями та вбудованими зображеннями

І зображення: 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--

.

Схема багаточастинна / пов'язана / альтернативна

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
Посилання на зовнішні ресурси рекомендуються, але, будь ласка, додайте контекст навколо посилання, щоб ваші колеги користувачі мали уявлення про те, що це таке і чому воно існує. Завжди цитуйте найбільш релевантну частину важливого посилання на випадок, якщо цільовий сайт недоступний або перебуває постійно в режимі офлайн. Дивіться, як відповісти
SilverNak

Ця відповідь не працює для мене. Фактично надіслано лише перше вкладення ...
Джим

1

Змішаний підтип

"Змішаний" підтип "багаточастини" призначений для використання, коли деталі кузова є незалежними та потребують комплектації в певному порядку. Будь-які «багаточастинні» підтипи, які реалізація не розпізнає, повинні розглядатися як підтипи «змішані».

Альтернативний підтип

Тип "багаточастинний / альтернативний" синтаксично ідентичний "багаточастинному / змішаному", але семантика відрізняється. Зокрема, кожна з деталей кузова - це "альтернативна" версія тієї ж інформації

Джерело

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.