Collectives™ on Stack Overflow
Find centralized, trusted content and collaborate around the technologies you use most.
Learn more about Collectives
Teams
Q&A for work
Connect and share knowledge within a single location that is structured and easy to search.
Learn more about Teams
When creating
email messages
you are supposed to set the
Content-Type
to
multipart/alternative
when sending HTML and TEXT or
multipart/mixed
when sending TEXT and attachments.
So what do you do if you want to send HTML, Text, and attachments? Use both?
–
–
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
related
inline image
inline image
attachment
attachment
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);
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();
–
–
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);
–
–
I've made a Hierarchy chart to better help visualize the ideal structure. Each message flows separately from Leaf to Root.
Microsoft Reference: MIME Hierarchies of Body Parts
Microsoft Reference: MIME Message Body Parts
Bellow is an example showcasing the hierarchy displayed in the chart above that can server as a template for you to use. It contains all the headers you should use and non of the headers that will get you marked as spam.
You can copy and paste this into a text file and save it as message.eml
. You can then use an email client such as Outlook to send it. To open it in Outolook, just double-click it.
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">
<body style="font-family: consolas, monospace;">
Dear recipient,
Please find attached three files with important information.
You may also view the three files bellow:
<li><img src="cid:50eef9fb" alt="Red"></li>
<li><img src="cid:e338d1eb" alt="Green"></li>
<li><img src="cid:e98e57c2" alt="Blue"></li>
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
</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--
I hit this issue. This architecture (from Lain's answer) worked for me.
Here is the solution in Python.
mixed
alternative
related
inline image
inline image
attachment
attachment
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):
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()
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
<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=
</body>=0D
</html>=0D
--AlternativeBoundaryString--
--RelatedBoundaryString
Content-Type: image/jpgeg;name="logo.png"
Content-Transfer-Encoding: base64
Content-Disposition: inline;filename="logo.png"
Content-ID: <logo.png@qcode.co.uk>
amtsb2hiaXVvbHJueXZzNXQ2XHVmdGd5d2VoYmFmaGpremxidTh2b2hydHVqd255aHVpbnRyZnhu
dWkgb2l1b3NydGhpdXRvZ2hqdWlyb2h5dWd0aXJlaHN1aWhndXNpaHhidnVqZmtkeG5qaG5iZ3Vy
a25qbW9nNXRwbF0nemVycHpvemlnc3k5aDZqcm9wdHo7amlodDhpOTA4N3U5Nnkwb2tqMm9sd3An
LGZ2cDBbZWRzcm85eWo1Zmtsc2xrZ3g=
--RelatedBoundaryString
Content-Type: image/jpgeg;name="masthead.png"
Content-Transfer-Encoding: base64
Content-Disposition: inline;filename="masthead.png"
Content-ID: <masthead.png@qcode.co.uk>
aXR4ZGh5Yjd1OHk3MzQ4eXFndzhpYW9wO2tibHB6c2tqOTgwNXE0aW9qYWJ6aXBqOTBpcjl2MC1t
dGlmOTA0cW05dGkwbWk0OXQwYVttaXZvcnBhXGtsbGo7emt2c2pkZnI7Z2lwb2F1amdpNTh1NDlh
eXN6dWdoeXhiNzhuZzdnaHQ3eW9zemlqb2FqZWt0cmZ1eXZnamhka3JmdDg3aXV2dWd5aGVidXdz
dhyuhehe76YTGSFGA=
--RelatedBoundaryString--
--MixedBoundaryString
Content-Type: application/pdf;name="Invoice_1.pdf"
Content-Transfer-Encoding: base64
Content-Disposition: attachment;filename="Invoice_1.pdf"
aGZqZGtsZ3poZHVpeWZoemd2dXNoamRibngganZodWpyYWRuIHVqO0hmSjtyRVVPIEZSO05SVURF
SEx1aWhudWpoZ3h1XGh1c2loZWRma25kamlsXHpodXZpZmhkcnVsaGpnZmtsaGVqZ2xod2plZmdq
a2psajY1ZWxqanNveHV5ZXJ3NTQzYXRnZnJhZXdhcmV0eXRia2xhanNueXVpNjRvNWllc3l1c2lw
dWg4NTA0
--MixedBoundaryString
Content-Type: application/pdf;name="SpecialOffer.pdf"
Content-Transfer-Encoding: base64
Content-Disposition: attachment;filename="SpecialOffer.pdf"
aXBvY21odWl0dnI1dWk4OXdzNHU5NTgwcDN3YTt1OTQwc3U4NTk1dTg0dTV5OGlncHE1dW4zOTgw
cS0zNHU4NTk0eWI4OTcwdjg5MHE4cHV0O3BvYTt6dWI7dWlvenZ1em9pdW51dDlvdTg5YnE4N3Z3
OTViOHk5cDV3dTh5bnB3dWZ2OHQ5dTh2cHVpO2p2Ymd1eTg5MGg3ajY4bjZ2ODl1ZGlvcjQ1amts
dfnhgjdfihn=
--MixedBoundaryString--
Schema multipart/related/alternative
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--
–
Building on Iain's example, I had a similar need to compose these emails with separate plaintext, HTML and multiple attachments, but using PHP. Since we are using Amazon SES to send emails with attachments, the API currently requires you to build the email from scratch using the sendRawEmail(...) function.
After much investigation (and greater than normal frustration), the problem was solved and the PHP source code posted so that it may help others experiencing a similar problem. Hope this help someone out - the troop of monkeys I forced to work on this problem are now exhausted.
PHP Source Code for sending emails with attachments using Amazon SES.
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 = "";
–
–
Great Answer Lain!
There were a couple things I did to make this work in a broader set of devices. At the end I will list the clients I tested on.
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;
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");
textPart.setContent(messageText, "text/plain; charset=UTF-8");
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.
The "mixed" subtype of "multipart" is intended for use when the body
parts are independent and need to be bundled in a particular order.
Any "multipart" subtypes that an implementation does not recognize
must be treated as being of subtype "mixed".
Alternative Subtype
The "multipart/alternative" type is syntactically identical to
"multipart/mixed", but the semantics are different. In particular,
each of the body parts is an "alternative" version of the same
information
Source
text - can contain [cid:imageid.png] or [image: imagename.jpg]
html - can contain <img src="cid:imageid.png">
inline image 1 (CID)
inline image 2 (CID)
inline image 3 (CID)
attachment 1
attachment 2
attachment 3
Another format being used:
mixed
alternative
text - can contain [cid:imageid.png] or [image: imagename.jpg]
related
html - can contain <img src="cid:imageid.png">
inline image 1 (CID)
inline image 2 (CID)
inline image 3 (CID)
I cannot comment which one is more common, but I'd go with Microsoft/Gmail format. Reader programs should support both, and writer/generator programs should probably stick with the first version.
If the message doesn't contain attachments then the root would be related and if it also doesn't contain inline images then the root would be alternative (based on first version).
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.