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.
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:
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:
TIdMessage
is to be MIME-encoded,TIdMessage.IsMsgSinglePartMime
is false,TIdMessage.IsBodyEmpty()
returns false (when TIdMessage.Body
contains non-whitespace text),TIdMessage.ConvertPreamble
is true (which it is by default),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:
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.';
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) ...