Mail multipart/alternative vs multipart/mixed
I hit this challenge today and I found these answers useful but not quite explicit enough for me.
Edit: Just found the Apache Commons Email that wraps this up nicely, meaning you don't need to know below.
If your requirement is an email with:
- text and html versions
- html version has embedded (inline) images
- attachments
The only structure I found that works with Gmail/Outlook/iPad is:
- mixed
- alternative
- text
- related
- html
- inline image
- inline image
- attachment
- attachment
- alternative
And the code is:
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;
}
}
And an example of using it with from 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();
}
}
I hit this issue. This architecture (from Lain's answer) worked for me. Here is the solution in Python.
- mixed
- alternative
- text
- related
- html
- inline image
- inline image
- attachment
- attachment
- alternative
Here is the main email creation function:
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())}
Here is the full code for sending an email containing html/text/attachment:
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 = "[email protected]"
sender = "[email protected]"
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()
Use multipart/mixed
with the first part as multipart/alternative
and subsequent parts for the attachments. In turn, use text/plain
and text/html
parts within the multipart/alternative
part.
A capable email client should then recognise the multipart/alternative
part and display the text part or html part as necessary. It should also show all of the subsequent parts as attachment parts.
The important thing to note here is that, in multipart MIME messages, it is perfectly valid to have parts within parts. In theory, that nesting can extend to any depth. Any reasonably capable email client should then be able to recursively process all of the message parts.
Messages have content. Content can be text, html, a DataHandler or a Multipart, and there can only be one content. Multiparts only have BodyParts but can have more than one. BodyParts, like Messages, can have content which has already been described.
A message with HTML, text and an a attachment can be viewed hierarchically like this:
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 )
And the code to build such a message:
// 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);