邮件多部分/选择与多部分/混合

在创建 电邮讯息时,在发送 HTML 和 TEXT 时应将 内容类别设置为 multipart/alternative,在发送 TEXT 和附件时应将 multipart/mixed设置为 multipart/alternative

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

181124 次浏览

使用 multipart/mixed与第一部分作为 multipart/alternative和后续部分的附件。反过来,在 multipart/alternative部分内使用 text/plaintext/html部分。

一个有能力的电子邮件客户端然后应该识别的 multipart/alternative部分,并显示文本部分或 html 部分作为必要的。它还应该显示所有的后续部分作为附件部分。

这里需要注意的重要事项是,在多部分 MIME 消息中,部分内含部分是完全有效的。理论上,这种嵌套可以延伸到任何深度。任何有合理能力的电子邮件客户端应该能够 递归处理所有的消息部分。

消息有内容。内容可以是文本、 html、 DataHandler 或 Multipart,并且只能有一个内容。多部分只有身体部分,但可以有一个以上。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);

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

编辑 : 刚刚发现 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();
}


}

在 Iain 的例子的基础上,我有一个类似的需求,使用单独的明文、 HTML 和多个附件编写这些电子邮件,但使用 PHP。因为我们使用 Amazon SES 发送带附件的电子邮件,所以 API 当前要求您使用 sendRawEmail (...)函数从头构建电子邮件。

经过大量的调查(比正常的挫折感更大) ,问题得到了解决,PHP 源代码得到了发布,这样可以帮助其他遇到类似问题的人。希望这能帮到某人——我被迫解决这个问题的猴群现在已经筋疲力尽了。

用于使用 AmazonSES 发送带附件的电子邮件的 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 = "";


}


?>

伟大的答案 Lain!

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

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

So finally, this is the list of clients I tested on. Outlook 2010, Outlook Web App, Internet Explorer 11, Firefox, Chrome, Outlook using Apple’s native app, Email going through Gmail - Browser mail client, Internet Explorer 11, Firefox, Chrome, Android default mail client, osx IPhone default mail client, Gmail mail client on Android, Gmail mail client on IPhone, Email going through Yahoo - Browser mail client, Internet Explorer 11, Firefox, Chrome, Android default mail client, osx IPhone default mail client.

Hope that helps anyone else.

我遇到了这个问题,这个架构(来自于 Lain 的回答)对我很有用。 下面是 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/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()

这是最好的: 带有附件和内联图像的多部分/混合 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--
|
.

混合子类型

“ multipart”的“混合”子类型用于当主体 部件是独立的,需要按照特定的顺序捆绑。 任何实现不能识别的“多部分”子类型 必须被视为亚型“混合”。

替代子类型

“ multipart/Alternative”类型在语法上与 “多部分/混合”,但语义是不同的。特别是, 身体的每个部分都是相同的“替代”版本 资料

来源

我已经制作了一个层次结构图来更好地帮助可视化理想的结构。

Microsoft 参考文献: 车身部件的 MIME 层次结构

Microsoft 参考文献: MIME 消息体部件

img1

根据我的研究:

微软和 Gmail 使用这种格式:

  • 好坏参半
    • 相关的
      • 另一种选择
        • Text-可以包含[ cid: imageid.png ]或[ image: imagename.jpg ]
        • Html-可以包含 < img src = “ cid: imageid.png”>
      • 内联图像1(CID)
      • 内联图像2(CID)
      • 内联图像3(CID)
    • 附件1
    • 附件2
    • 附件3

正在使用的另一种格式:

  • 好坏参半
    • 另一种选择
      • Text-可以包含[ cid: imageid.png ]或[ image: imagename.jpg ]
      • 相关的
        • Html-可以包含 < img src = “ cid: imageid.png”>
        • 内联图像1(CID)
        • 内联图像2(CID)
        • 内联图像3(CID)
    • 附件1
    • 附件2
    • 附件3

我不能评论哪一种更常见,但我会选择 Microsoft/Gmail 格式。读者程序应该同时支持这两个版本,而编写器/生成器程序应该坚持使用第一个版本。

如果消息不包含附件,那么根应该是 相关的,如果它也不包含内联图像,那么根应该是 另一种选择(基于第一个版本)。