邮件多部分/替代与多部分/混合

188

创建电子邮件消息时,如果要发送HTML和TEXT,则应将Content-Type设置为multipart/alternative,如果要发送TEXT和附件,则应将其设置为multipart/mixed

那么,如果要同时发送HTML、文本和附件,该怎么办?两者都使用吗?


2
我不确定做这件事的“正确”方式是什么。我确实看到过包含 mp/text 部分和包含 HTML 和附件的 mp/mixed 消息,但这意味着只有在查看 HTML 而不是查看 TEXT 时才能看到附件,所以它“感觉”不对。你可以尝试使用包含两种消息格式和第二部分包含附件的 mp/alt 部分的 mp/mixed,但我不知道客户端会怎么处理它。 - dajames
1
@Iain,你的答案非常特别,因为它是唯一一个包含 Gmail 所期望的(非常奇怪的)结构的答案。我会授予它赏金。 - PascalVKooten
这是一份漂亮的ASCII艺术:https://dev59.com/-Zzha4cB1Zd3GeqPAymI#40420648 - guettli
10个回答

198

今天我遇到了这个挑战,我发现这些答案对我来说很有用,但不是很明确。

编辑:刚刚发现 Apache Commons Email 很好地解决了这个问题,这意味着您不需要了解以下内容。

如果您的要求是发送一封包含以下内容的电子邮件:

  1. 文本和 HTML 版本
  2. HTML 版本具有嵌入式(内联)图像
  3. 附件

我发现唯一可行的结构是:

  • mixed
    • alternative
      • text
      • related
        • html
        • inline image
        • inline image
    • attachment
    • attachment

代码如下:

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.*;

/**
 *
 * https://dev59.com/qWUq5IYBdhLWcg3wJNLg
 * https://dev59.com/hG865IYBdhLWcg3wQMWW
 *
 *
 *
 * @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部分“前缀”添加到电子邮件正文中;但是有些客户端会得到没有附件的空正文,有些客户端只会在附件中获得正文(空正文)(Windows上的Outlook),而有些客户端则可以正常工作(GMail Web,Android应用程序等)。如果可能,请查看:https://stackoverflow.com/questions/47312409/adding-an-html-prefix-to-email-mime-causes-some-clients-to-show-empty-body - shachar0n
1
非常棒的答案。帮助我解决了不知道如何在一封电子邮件中发送HTML和文本版本的问题。 - dinukadev
1
它不是"<" id-left "@" id-right ">"的形式。 - Michael-O
显示剩余6条评论

122
使用 multipart/mixed,将第一部分设置为multipart/alternative,并将后续部分用于附件。反过来,在multipart/alternative部分中使用text/plaintext/html部分。
一个能力强大的电子邮件客户端应该会认识到multipart/alternative部分,并根据需要显示文本部分或html部分。它还应该显示所有后续部分作为附件部分。
这里需要注意的重要事项是,在多部分MIME消息中,允许在部分中再包含其他部分。理论上,这种嵌套可以扩展到任意深度。任何合理有能力的电子邮件客户端都应该能够递归处理所有的消息部件。

22
不要忘记正确排序你的 multipart/alternative 子部分。最后一项是最好/优先级最高的部分,因此你可能希望将 text/html 部分放在最后一个子部分。根据 RFC1341 规定。 - Luna
4
multipart/related 是一种 MIME 类型,它允许在一封电子邮件中包含多个部分,并且这些部分可以相互关联。通常情况下,它被用于将 HTML 内容与相关的图像、样式表等资源打包在一起发送。 - Wilt
6
@Wilt: multipart/alternative 表示只有其中一个包含的部分会被显示,例如一个部分是 text/plain ,另一个部分是 text/html 。因此,电子邮件客户端不应该同时显示两个部分,而只显示其中一个。也就是说,它们之间没有关联。multipart/related 则表示各个子部分都是主根部分的一部分,例如主要部分是 text/html ,而子部分是内嵌的图像。更多信息请参见这里 - RaelB
只是一个小技巧 - 如果您可以访问*nix系统,您可以使用mutt CLI客户端来验证您是否正确设置了多部分MIME消息。如果您在查看邮件时按下v键,它将显示并允许遍历MIME部件的嵌套树。 - rinogo

32

消息具有内容。 内容可以是文本,html,DataHandler或Multipart,并且只能有一个内容。 Multipart仅具有BodyPart但可以有多个。 BodyPart像Messages一样可以具有已经描述过的内容。

具有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à
我非常感谢您清楚地解释了如何使用代码实现每个答案所尝试做的细节。谢谢! - cafebabe1991

19
我已经制作了一个层次结构图,以更好地帮助可视化理想的结构。每个消息从叶子到根部分别流动。
微软参考:MIME主体部分的层次结构 微软参考:MIME消息主体部分

img1

以下是一个示例,展示了上面图表中显示的层次结构,可以作为您使用的模板。它包含了您应该使用的所有标题,而没有会被标记为垃圾邮件的标题。
您可以将其复制粘贴到一个文本文件中,并将其保存为message.eml。然后,您可以使用诸如Outlook之类的电子邮件客户端发送它。要在Outlook中打开它,只需双击即可。
From: "no-reply @ Example" no-reply@example.com
To: no-reply@example.com
Subject: Important information
Message-Id: 0eb69c12-ec18-4f6c-ac14-168b2092b477@example.com
Content-Language: en-US
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="mixed_boundary"

--mixed_boundary
Content-Type: multipart/related; boundary="related_boundary"

--related_boundary
Content-Type: multipart/alternative; boundary="alternative_boundary"

--alternative_boundary
Content-Type: text/plain; charset="us-ascii"
Content-Transfer-Encoding: 8bit

Dear recipient,

Please find attached three files with important information.

You may also view the three files bellow:

[cid:50eef9fb]
[cid:e338d1eb]
[cid:e98e57c2]

MAX-76-CHAR-MAX-76-CHAR-MAX-76-CHAR-MAX-76-CHAR-MAX-76-CHAR-MAX-76-CHAR-MAX-

(c) 2023 - Example, Inc.

Message Id: 0eb69c12-ec18-4f6c-ac14-168b2092b477

--alternative_boundary
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: 8bit

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<body style="font-family: consolas, monospace;">
<p>
Dear recipient,
</p>
<p>
Please find attached three files with important information.
</p>
<p>
You may also view the three files bellow:
</p>
<ol>
<li><img src="cid:50eef9fb" alt="Red"></li>
<li><img src="cid:e338d1eb" alt="Green"></li>
<li><img src="cid:e98e57c2" alt="Blue"></li>
</ol>
<p>
MAX-76-CHAR-MAX-76-CHAR-MAX-76-CHAR-MAX-76-CHAR-MAX-76-CHAR-MAX-76-CHAR-MAX-
</p>
<p>
(c) 2023 - Example, Inc.
</p>
<p>
Message Id: 0eb69c12-ec18-4f6c-ac14-168b2092b477
</p>
</body>
</html>

--alternative_boundary--

--related_boundary
Content-Type: image/gif; name="red"
Content-Description: red.gif
Content-Disposition: inline; filename="image001.gif";
Content-ID: <50eef9fb>
Content-Transfer-Encoding: base64

R0lGODdhIAAgAPAAAPRDNslFJiH5BAEAAAEALAAAAAAgACAAAAIehI+py+0Po5y02ouz3rz7D4bi
SJbmiabqyrbuC5sFADs=

--related_boundary
Content-Type: image/gif; name="green"
Content-Description: green.gif
Content-Disposition: inline; filename="image002.gif";
Content-ID: <e338d1eb>
Content-Transfer-Encoding: base64

R0lGODdhIAAgAPAAAC59MslFJiH5BAEAAAEALAAAAAAgACAAAAIehI+py+0Po5y02ouz3rz7D4bi
SJbmiabqyrbuC5sFADs=

--related_boundary
Content-Type: image/gif; name="blue"
Content-Description: blue.gif
Content-Disposition: inline; filename="image003.gif";
Content-ID: <e98e57c2>
Content-Transfer-Encoding: base64

R0lGODdhIAAgAPAAABVlwMlFJiH5BAEAAAEALAAAAAAgACAAAAIehI+py+0Po5y02ouz3rz7D4bi
SJbmiabqyrbuC5sFADs=

--related_boundary--

--mixed_boundary
Content-Type: image/gif; name="red"
Content-Description: red.gif
Content-Disposition: attachment; filename="image004.gif";
Content-Transfer-Encoding: base64

R0lGODdhIAAgAPAAAPRDNslFJiH5BAEAAAEALAAAAAAgACAAAAIehI+py+0Po5y02ouz3rz7D4bi
SJbmiabqyrbuC5sFADs=

--mixed_boundary
Content-Type: image/gif; name="green"
Content-Description: green.gif
Content-Disposition: attachment; filename="image005.gif";
Content-Transfer-Encoding: base64

R0lGODdhIAAgAPAAAC59MslFJiH5BAEAAAEALAAAAAAgACAAAAIehI+py+0Po5y02ouz3rz7D4bi
SJbmiabqyrbuC5sFADs=

--mixed_boundary
Content-Type: image/gif; name="blue"
Content-Description: blue.gif
Content-Disposition: attachment; filename="image006.gif";
Content-Transfer-Encoding: base64

R0lGODdhIAAgAPAAABVlwMlFJiH5BAEAAAEALAAAAAAgACAAAAIehI+py+0Po5y02ouz3rz7D4bi
SJbmiabqyrbuC5sFADs=

--mixed_boundary--

这就是将结构转化为 JSON 后的奇怪样子。

const email = {
  headers: {
    from: "no-reply@example.com",
    to: "no-reply@example.com",
    subject: "Important information",
    messageId: "0eb69c12-ec18-4f6c-ac14-168b2092b477@example.com",
    lang: "en-US",
    mime: "1.0",
    contentType: 'multipart/mixed; boundary="mixed_boundary"',
  },
  parts: [
    {
      contentType: 'multipart/related; boundary="related_boundary"',
      parts: [
        {
          contentType: 'multipart/alternative; boundary="alternative_boundary"',
          parts: [
            {
              contentType: "text/plain; charset=us-ascii",
              encoding: "8bit",
              body: "Hello us-ascii World!"
            },
            {
              contentType: "text/html; charset=utf-8",
              encoding: "8bit",
              body: "<html>Hello utf-8 World!</html>"
            }
          ]
        },
        {
          inline: [
            {
              name: "red",
              description: "red.gif",
              disposition: "inline",
              id: "<50eef9fb>",
              encoding: "base64",
              content: "R0lGODdhIAAgAPAAAPRDNslFJiH5BAEAAAEALAAAAAAgACAAAAIehI+py+0Po5y02ouz3rz7D4biSJbmiabqyrbuC5sFADs="
            },
            {
              name: "green",
              description: "green.gif",
              disposition: "inline",
              id: "<e338d1eb>",
              encoding: "base64",
              content: "R0lGODdhIAAgAPAAAPRDNslFJiH5BAEAAAEALAAAAAAgACAAAAIehI+py+0Po5y02ouz3rz7D4biSJbmiabqyrbuC5sFADs="
            },
            {
              name: "blue",
              description: "blue.gif",
              disposition: "inline",
              id: "<e98e57c2>",
              encoding: "base64",
              content: "R0lGODdhIAAgAPAAAPRDNslFJiH5BAEAAAEALAAAAAAgACAAAAIehI+py+0Po5y02ouz3rz7D4biSJbmiabqyrbuC5sFADs="
            }
          ]
        }
      ]
    },
    {
      attachment: [
        {
          name: "red",
          description: "red.gif",
          disposition: "attachment",
          id: "<50eef9fb>",
          encoding: "base64",
          content: "R0lGODdhIAAgAPAAAPRDNslFJiH5BAEAAAEALAAAAAAgACAAAAIehI+py+0Po5y02ouz3rz7D4biSJbmiabqyrbuC5sFADs="
        },
        {
          name: "green",
          description: "green.gif",
          disposition: "attachment",
          id: "<e338d1eb>",
          encoding: "base64",
          content: "R0lGODdhIAAgAPAAAPRDNslFJiH5BAEAAAEALAAAAAAgACAAAAIehI+py+0Po5y02ouz3rz7D4biSJbmiabqyrbuC5sFADs="
        },
        {
          name: "blue",
          description: "blue.gif",
          disposition: "attachment",
          id: "<e98e57c2>",
          encoding: "base64",
          content: "R0lGODdhIAAgAPAAAPRDNslFJiH5BAEAAAEALAAAAAAgACAAAAIehI+py+0Po5y02ouz3rz7D4biSJbmiabqyrbuC5sFADs="
        }
      ]
    }
  ]
};

2
真的是被低估的答案,这张图非常有帮助!! - JoshuaHew

14

我遇到了这个问题。这个架构(来自Lain的答案)对我有用。

  • 混合
    • 备用
      • 文本
      • 相关
        • HTML
        • 内联图片
        • 内联图片
    • 附件
    • 附件

这里是Python中的解决方案。

以下是创建主要电子邮件的函数:

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/text/附件的电子邮件的完整代码:

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

10
这里是最好的: 带有附件和内联图片的多部分/混合mime消息 还有一张图片: 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
这个答案对我不起作用。只有第一个附件被发送了...... - Jim
链接已经失效。 - hestellezg
你是如何生成 MIME 邮件的(第一个代码片段)? - a_sid

6

在Iain的例子基础上,我有类似的需求,需要使用PHP来组合这些包含独立纯文本、HTML和多个附件的电子邮件。由于我们正在使用Amazon SES发送带附件的电子邮件,目前的API要求您使用sendRawEmail(...)函数从头构建电子邮件。

经过长时间的调查(以及比平常更大的挫折),问题得到了解决,并发布了PHP源代码,以便帮助其他遇到类似问题的人。希望这对某个人有所帮助——我让一群猴子为解决这个问题而努力工作,现在他们已经筋疲力尽了。

使用Amazon SES发送带附件的电子邮件的PHP源代码。

<?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,这是替代消息吗?谢谢! - Ivijan Stefan Stipić
谢谢。是的,我使用上述解决方案发送纯文本和HTML。该代码只是使用strip_tags($body)剥离HTML,以便在浏览器不想使用HTML的情况下提供纯文本。如果需要,您可以自己放置自定义字符串(例如$body_plain_text)。 - RightHandedMonkey

5

太棒了,Lain的回答非常好!

我做了一些事情,以便在更广泛的设备上实现这个功能。最后,我将列出我测试过的客户端。

  1. I added a new build constructor that did not contain the parameter attachments and did not use MimeMultipart("mixed"). There is no need for mixed if you are sending only inline images.

    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 addTextVersion method I added charset when adding content this probably could/should be passed in, but I just added it statically.

    textPart.setContent(messageText, "text/plain");
    to
    textPart.setContent(messageText, "text/plain; charset=UTF-8");
    
  3. The last item was adding to the addImagesInline method. I added setting the image filename to the header by the following code. If you don't do this then at least on Android default mail client it will have inline images that have a name of Unknown and will not automatically download them and present in email.

    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 Web App
  • Internet Explorer 11
  • Firefox
  • Chrome
  • 使用苹果本机应用程序的Outlook
  • 通过Gmail发送电子邮件 - 浏览器邮件客户端:
    • Internet Explorer 11
    • Firefox
    • Chrome
  • Android默认邮件客户端
  • OSX iPhone默认邮件客户端
  • Android上的Gmail邮件客户端
  • iPhone上的Gmail邮件客户端
  • 通过Yahoo发送电子邮件 - 浏览器邮件客户端:
    • Internet Explorer 11
    • Firefox
    • Chrome
  • Android默认邮件客户端
  • OSX iPhone默认邮件客户端

希望对大家有所帮助。


谢谢反馈,都是很好的观点。我会考虑把它们包含在我的答案中。 - Iain

4

混合子类型

"multipart"的“mixed”子类型是用于在需要将独立的正文部分按特定顺序绑定在一起时使用的。任何实现不识别的“multipart”子类型都必须被视为“mixed”子类型。

备选子类型

“multipart/alternative”类型在语法上与“multipart/mixed”完全相同,但语义不同。特别地,每个正文部分都是相同信息的“备选”版本。

来源


1
根据我的研究:
Microsoft 和 Gmail 使用这个格式:
mixed related alternative text - 可包含 [cid:imageid.png] 或 [image: imagename.jpg] html - 可包含 <img src="cid:imageid.png"> inline image 1 (CID) inline image 2 (CID) inline image 3 (CID) attachment 1 attachment 2 attachment 3
另一种使用的格式是:
mixed alternative text - 可包含 [cid:imageid.png] 或 [image: imagename.jpg] related html - 可包含 <img src="cid:imageid.png"> inline image 1 (CID) inline image 2 (CID) inline image 3 (CID) attachment 1 attachment 2 attachment 3
我不能评论哪一个更常见,但我会选择 Microsoft/Gmail 格式。阅读器程序应该支持两种格式,而写入/生成程序应该坚持第一种版本。
如果邮件不包含附件,则根应为 related;如果还不包含内联图片,则根应为 alternative(基于第一个版本)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接