添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
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'm not sure what the 'correct' way to do this is. I've certainly seen mp/alt messages that had a mp/text part and a mp/mixed part containing HTML and attachment ... but that meant that the attachment was only visible when viewing HTML not when viewing TEXT so it 'smells' wrong. You could try mp/mixed with an mp/alt part containing both message formats and a second part to contain the attachment, but I don't know what clients would make of it. dajames Oct 14, 2010 at 11:45 @Iain Your answer is very special for being the only one to contain the (very weird) structure that gmail expects. I'll award bounty to it. PascalVKooten Jun 19, 2015 at 18:54

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(); Thank you for the superb answer @Iain! I'm having a hard time trying to get the right MIME structure for my case, in which I attempt to add an HTML part 'prefix' to the email's body; but some clients get empty body with no attachments, some get the body only in the attachments (empty body) (Outlook on Windows), and some work well (GMail web, Android App, etc.). Please take a look if possible: stackoverflow.com/questions/47312409/… – shachar0n Nov 15, 2017 at 16:27 Amazing answer. Helped fix my issue with not knowing how to send an HTML and text version of an email in one. – dinukadev Dec 17, 2019 at 4:01

    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.

    Don't forget to order the subparts of your multipart/alternative correctly. The last entry is the best/highest priority part, so you probably want to put the text/html part as the last subpart. Per RFC1341. – Luna Jun 4, 2015 at 11:36 @Wilt: multipart/alternative denotes that only one of the included parts should be displayed - e.g. one part is text/plain and one part is text/html. So the email client should not display both parts but only one. i.e They are not related. multipart/related indicates that the various subparts, are all part of the main root part, e.g. the main part is text/html and the subparts are embedded images. See here for more info. – RaelB Jun 14, 2017 at 19:09 Just a quick tip - If you have access to a *nix box, you can use the mutt CLI client to verify that you've set up your multipart MIME messages correctly. If you press v while viewing a message, it will display and allow traversal of the nested tree of MIME parts. – rinogo Jun 11, 2018 at 20:33

    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);
                    @splahout Thank you so much and congratulations for your clear and wide answer, for me it has been so useful.
    – Marti Pàmies Solà
                    Oct 23, 2019 at 11:08
                    I really appreciate you clearing out the details of doing what every answer is trying to do with code. Thanks!
    – cafebabe1991
                    Sep 15, 2022 at 13:30
    

    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--
                    Links to external resources are encouraged, but please add context around the link so your fellow users will have some idea what it is and why it’s there. Always quote the most relevant part of an important link, in case the target site is unreachable or goes permanently offline. See how to answer
    – SilverNak
                    Sep 23, 2017 at 10:08
    

    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 = ""; This is genial solution man. Generaly $boundary contain whole body with attachments but only $boundary2 contain HTML or plain text. Genial solution. Tell me please, this your solution for sending plain text, is this alternative message if mail client not support HTML? Thanks! – Ivijan Stefan Stipić Sep 1, 2017 at 8:47 Thanks. Yes, I send both plain text and HTML with the above solution. The code simply strips out the HTML using the strip_tags($body) for providing the plain text in cases for browsers that don't want to use the HTML. If desired you could put your own custom string there instead (i.e. $body_plain_text). – RightHandedMonkey Sep 1, 2017 at 13:01

    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.

  •