Search code examples
htmldelphismtpemail-attachmentsindy

TIdMessage - Attachments Show Up in Body as Base64


I'm working on sending emails via SMTP using the Indy components (TIdMessage). Such an email needs to be HTML, and needs to carry attachments.

Now if I send an email as plain text (ContentType := 'text/plain'), and attach a file, the email sends just fine, attachment found and everything.

However, once I change the ContentType to text/html, I have a very bizarre result. The entire body of the email gets replaced with apparently the underlying email data (in my words), and shows the attachment in the body as Base64 data.

For example, here's just the first few lines of such a resulting email:

This is a multi-part message in MIME format

--YGuFowdSjNaa=_khosBzZl5L8uGVtfasBX
Content-Type: text/plain; charset="us-ascii"
Content-Transfer-Encoding: quoted-printable
Content-Disposition: inline

This is the body of a test email.
--YGuFowdSjNaa=_khosBzZl5L8uGVtfasBX
Content-Type: application/octet-stream; name="TagLogo.jpg"
Content-Transfer-Encoding: base64
Content-Disposition: inline; filename="TagLogo.jpg"

/9j/4AAQSkZJRgABAQEASwBLAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcU
FhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgo
KCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCADhAG8DASIA
AhEBAxEB/8QAHAAAAgMBAQEBAAAAAAAAAAAAAAcFBggEAgED/8QAPxAAAQIFAwEEBwYFAwQDAAAA
AQIDAAQFBhEHEiExCBNBdSI3OFFhsrMUMkJxgZEVIzNSgmJyoRaSosEk0eH/xAAWAQEBAQAA

Although the original body was only

This is the body of a test email.

The code is very simple, matching all the examples I can find online...

IdMessage.Charset := 'UTF-8';
IdMessage.ContentType := 'text/html'; // <-- text/plain works fine.....
IdMessage.Body := 'This is the body of a test email.';

... (Assigning Other Unrelated Properties) ...

A := TIdAttachmentFile.Create(IdMessage.MessageParts, 'C:\SomeFile.jpg'); // <-- Originally the only LOC here
A.ContentTransfer := 'base64'; // <-- Tried with and without
A.ContentType := 'application/octet-stream'; // <-- Tried with and without
//A.ContentType := 'image/jpeg'; // <-- Tried with and without
A.ContentDisposition := 'inline'; // <-- Tried with and without

Why is this resulting in a "garbage" email, and how do I solve it while supporting HTML email body with attachments?

PS - If it makes any difference, the attachments will be images used in-line in the email body in the end.


Solution

  • Such an email needs to be HTML, and needs to carry attachments

    I have blog articles on Indy's website on this very topic, I suggest you read them:

    HTML Messages

    New HTML Message Builder class

    Why is this resulting in a "garbage" email

    In a nutshell, because you are not populating the TIdMessage correctly.

    In your particular example, you are putting the HTML into the TIdMessage.Body property, but you are also adding an item to the TIdMessage.MessageParts collection. That combination has some special handling inside of TIdMessageClient (which TIdSMTP derives from). In particular, when:

    • the TIdMessage is to be MIME-encoded,
    • and TIdMessage.IsMsgSinglePartMime is false,
    • and TIdMessage.IsBodyEmpty() returns false (when TIdMessage.Body contains non-whitespace text),
    • and TIdMessage.ConvertPreamble is true (which it is by default),
    • and the TIdMessage.MessageParts collection is not empty, and has no TIdText objects in it.

    Then TIdMessageClient.SendBody() will move the HTML to a new TIdText object in the MessageParts collection, and generate a MIME-encoded email body with the TIdMessage.Body text as the first MIME part. The assumption is that in this combination, the user has put the message plain-text in the TIdMessage.Body by accident, so Indy moves it where it needs to be for further processing, without changing anything else in the email. However, that means the TIdMessage.ContentType property is not adjusted (probably a bug that should be looked into). In your case, you are setting the ContentType to text/html when it really needs to be a MIME multipart/... type instead (depending on the relationship of the attachments to the HTML).

    So, you are effectively sending an email like this:

    Content-Type: text/html; charset=us-ascii; boundary="AduWRpEOzrMvJDhg6Jp8EsEFw5Qr1p=_1v"
    MIME-Version: 1.0
    Date: Tue, 8 Aug 2017 12:58:00 -0700
    
    This is a multi-part message in MIME format
    
    --AduWRpEOzrMvJDhg6Jp8EsEFw5Qr1p=_1v
    Content-Type: text/plain; charset="us-ascii"
    Content-Transfer-Encoding: quoted-printable
    Content-Disposition: inline
    
    This is the body of a test email.
    
    --AduWRpEOzrMvJDhg6Jp8EsEFw5Qr1p=_1v
    Content-Type: application/octet-stream;
        name="SomeFile.jpg"
    Content-Transfer-Encoding: base64
    Content-Disposition: inline;
        filename="SomeFile.jpg"
    
    <base64 data here>
    
    --AduWRpEOzrMvJDhg6Jp8EsEFw5Qr1p=_1v--
    

    Which tells the receiver of the email that the entire email is HTML when it really is not. It is actually a mix of multiple MIME types working together. Since you want the image to be inside the HTML, the top-level Content-Type header needed to be multipart/related instead (I go into more detail in my blog articles explaining why that is).

    how do I solve it while supporting HTML email body with attachments?

    If it makes any difference, the attachments will be images used in-line in the email body in the end.

    When dealing with TIdMessage, it is best to either:

    • populate only the TIdMessage.Body by itself, and ignore the TIdMessage.MessageParts collection.

    • put everything in the TIdMessage.MessageParts collection, text and all, and ignore the TIdMessage.Body property.

    In this case, since you need attachments, you need to use the MessageParts collection, so you can either:

    1. put the HTML in a TIdText object within the TIdMessage.MessageParts collection yourself (don't let TIdMessageClient do it for you), and then set the TIdMessage.ContentType to multipart/related:

      IdMessage.ContentType := 'multipart/related; type="text/html"';
      ... (Assigning Other Unrelated Properties) ...
      
      T := TIdText.Create(IdMessage.MessageParts, nil);
      T.ContentType := 'text/html';
      T.Charset := 'utf-8';
      T.Body.Text := '<html><body>This is the body of a test email.<p><img src="cid:myimage"></body></html>';
      
      A := TIdAttachmentFile.Create(IdMessage.MessageParts, 'C:\SomeFile.jpg');
      A.ContentTransfer := 'base64';
      A.ContentType := 'image/jpeg';
      A.ContentDisposition := 'inline';
      A.ContentID := 'myimage';
      

      Or, if you want to provide a plain-text message for non-HTML readers, you need to wrap the HTML and plain-text (in that order) inside of multipart/alternative instead, while keeping the HTML and image attachment inside of multipart/related:

      IdMessage.ContentType := 'multipart/alternative';
      ... (Assigning Other Unrelated Properties) ...
      
      T := TIdText.Create(IdMessage.MessageParts, nil);
      T.ContentType := 'multipart/related; type="text/html"';
      
      T := TIdText.Create(IdMessage.MessageParts, nil);
      T.ContentType := 'text/html';
      T.Charset := 'utf-8';
      T.Body.Text := '<html><body>This is the body of a test email.<p><img src="cid:myimage"></body></html>';
      T.ParentPart := 0;
      
      A := TIdAttachmentFile.Create(IdMessage.MessageParts, 'C:\SomeFile.jpg');
      A.ContentTransfer := 'base64';
      A.ContentType := 'image/jpeg';
      A.ContentDisposition := 'inline';
      A.ContentID := 'myimage';
      A.ParentPart := 0;
      
      T := TIdText.Create(IdMessage.MessageParts, nil);
      T.ContentType := 'text/plain';
      T.Charset := 'utf-8';
      T.Body.Text := 'This is the body of a test email.';
      
    2. use the TIdMessageBuilderHtml class, and let it configure the TIdMessage content for you:

      uses
        ..., IdMessageBuilder;
      
      MB := TIdMessageBuilderHtml.Create;
      try
        // optional...
        MB.PlainText.Text := 'This is the body of a test email.';
        MB.PlainTextCharSet := 'utf-8';
      
        MB.Html.Text := '<html><body>This is the body of a test email.<p><img src="cid:myimage"></body></html>';
        MB.HtmlCharSet := 'utf-8';
        MB.HtmlFiles.Add('C:\SomeFile.jpg', 'myimage');
      
        MB.FillMessage(IdMessage);
      finally
        MB.Free;
      end;
      
      ... (Assigning Other Unrelated Properties) ...