I want to use Indy to send emails with embedded images, and for those cases the HTML template must have the base64 converted image.
Sample HTML template:
<html>
<head>
</head>
<body>
<div>
<p>Some text</p>
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4
//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" alt="Red dot" />
</div>
</body>
</html>
This HTML is just for testing, but even with this simple base64 image and plain text, when I send it via email with Indy, I don't receive the image correctly. I receive the HTML code, or the text with a broken image, or the image don't even load (comes with a blank space).
BUT, when I open the HTML file in a common browser (ie, Chrome or Firefox), the image loads without problem.
I've tried the following routine:
uses
idMessage, idText, IdSMTP, IdSSLOpenSSL, IdExplicitTLSClientServerBase;
procedure SendMail;
var
html: TStringList;
email: TIdMessage;
idSMTP: TIdSMTP;
idSSL: TIdSSLIOHandlerSocketOpenSSL;
begin
html:= TStringlist.Create;
html.LoadFromFile('<my_html_file>');
email := TIdMessage.Create(nil);
email.From.Text := '[email protected]';
email.From.Name:= 'from name'; ///
email.Recipients.EMailAddresses := 'recipient';
email.Subject := 'From DELPHI';
email.ContentType := 'multipart/mixed'; //email comes with HTML text
//email.ContentType := 'text/html'; //email comes with plain text, but not images
email.Body.Assign(html);
// SSL stuff //
idSSL:= TIdSSLIOHandlerSocketOpenSSL.Create(nil);
idSSL.SSLOptions.Mode:= sslmClient;
idSSL.SSLOptions.Method:= sslvSSLv23;
// SMTP stuff //
idSMTP:= TIdSMTP.Create(nil);
idSMTP.IOHandler:= idSSL;
idSMTP.Host:= 'smtp.office365.com';
idSMTP.Port:= 587;
idSMTP.AuthType := satDefault;
//idSMTP.UseTLS:= utUseImplicitTLS;
idSMTP.UseTLS:= utUseExplicitTLS;
idSMTP.Username:= '[email protected]';
idSMTP.Password:= 'pass';
try
idSMTP.Connect();
idSMTP.Send(email);
ShowMessage('Sent');
except
on E: Exception do
ShowMessage('Failed: ' + E.Message);
end;
end;
I also tried to use TIdMessageBuilderHtml
, but without success on this case.
What am I doing wrong?
As I mentioned earlier my idea was (and is) similar of @Oleksandr Morozevych answer, I basically convert all images from body from base64 into a temporary binary (image) file which is attached on the mail message, and the body <img src="data:image/png;base64,iVBORw0KGgoAAAAN...
is replaced with <img src="cid:cid_image_id.jpg" />
, becoming and inline image in the email body.
Here an example:
procedure SendMail();
var
LHtmlPart: TIdText;
LMessage: TIdMessage;
LImagePart: TIdAttachmentFile;
LHtmlText: String;
LAttachment: TIdAttachmentFile;
SMTP: TIdSMTP;
SSL: TIdSSLIOHandlerSocketOpenSSL;
begin
LMessage:= TIdMessage.Create(nil);
try
// Message stuff //
LMessage.From.Text := '[email protected]';
LMessage.From.Name:= 'from name';
LMessage.Recipients.Add.Address := '[email protected]';
LMessage.Subject := 'subject';
LMessage.ContentType := 'multipart/mixed';
// Build HTML message //
LHtmlPart:= TIdText.Create(LMessage.MessageParts);
LHtmlPart.ContentType:= 'text/html';
LHtmlText:= TFile.ReadAllText('filename.html');
// base64 to temporary file and attach images to message //
DecodeHtmlImages(LHtmlText, LMessage, LImagePart);
LHtmlPart.Body.Text:= LHtmlText;
// Attachs (not inline images) //
LAttachment:= TIdAttachmentFile.Create(LMessage.MessageParts, 'filename1');
LAttachment.FileName:= ExtractFileName('filename1');
LAttachment:= TIdAttachmentFile.Create(LMessage.MessageParts, 'filename2');
LAttachment.FileName:= ExtractFileName('filename2');
SSL:= TIdSSLIOHandlerSocketOpenSSL.Create(nil);
SSL.SSLOptions.Mode:= sslmClient;
SSL.SSLOptions.Method:= sslvSSLv23;
SMTP:= TIdSMTP.Create(nil);
SMTP.IOHandler:= SSL;
SMTP.Host:= 'smtp.office365.com';
SMTP.Port:= 587;
SMTP.AuthType := satDefault;
// ms mail service //
SMTP.UseTLS:= utUseExplicitTLS;
SMTP.Username:= '[email protected]';
SMTP.Password:= 'password';
try
SMTP.Connect();
SMTP.Send(LMessage);
except
ShowMessage('Failed: ' + E.Message);
end;
finally
LHtmlPart.Free;
LImagePart.Free;
LMessage.Free;
SMTP.Free;
SSL.Free;
end;
Additional functions I made used above:
procedure DecodeHtmlImages(var ABody: String; var AMessage: TIdMessage; var AImagePart: TIdAttachmentFile);
var
LStream: TMemoryStream;
LMatch: TMatch;
LMatches: TMatchCollection;
LBase64: String;
LImageData: TBytes;
LFilename: String;
const
EXP_ENCODED_SOURCE = 'src\s*=\s*"([cid^].+?)"';
begin
LMatches:= TRegEx.Matches(ABody, EXP_ENCODED_SOURCE, [roIgnoreCase]);
for LMatch in LMatches do
begin
// step 1 - convert and save temp file //
LBase64:= ExtractBase64FromHTML(LMatch.Value);
Lstream := TBytesStream.Create(TNetEncoding.Base64.DecodeStringToBytes(LBase64));
try
LFilename:= IncludeTrailingPathDelimiter(System.IOUtils.TPath.GetTempPath) + 'tmp_' + FormatDateTime('yyyymmddhhnnsszzz', Now) + '.' + ExtractImageExtensionFromHTML(ABody);
LStream.SaveToFile(LFilename);
finally
LStream.Free;
end;
// step 2 - replace base64 code for "cid" and attach all images //
if FileExists(LFilename) then
begin
AImagePart:= TIdAttachmentFile.Create(AMessage.MessageParts, LFilename);
try
AImagePart.ContentType:= Format('image/%s', [StringReplace(TPath.GetExtension(LFilename), '.', '', [rfIgnoreCase])]);
AImagePart.ContentDisposition:= 'inline';
AImagePart.FileIsTempFile:= True;
AImagePart.ExtraHeaders.Values['content-id']:= TPath.GetFileName(LFilename);
AImagePart.DisplayName:= TPath.GetFileName(LFilename);
ABody:= StringReplace(ABody, LMatch.Value, Format('src="cid:%s"', [TPath.GetFileName(LFilename)]), [rfIgnoreCase]);
finally
//freeAndNil(LImagePart); // cant be freed yet //
end;
end;
end;
end;
function ExtractBase64FromHTML(HTML: string): string;
var
RegEx: TRegEx;
Match: TMatch;
begin
RegEx := TRegEx.Create('data:image\/[a-zA-Z]*;base64,([^"]+)', [roIgnoreCase]);
Match := RegEx.Match(HTML);
if Match.Success then
Result := Match.Groups[1].Value
else
Result := '';
end;
function ExtractImageExtensionFromHTML(htmlContent: string): string;
var
regex: TRegEx;
match: TMatch;
begin
regex := TRegEx.Create('data:image\/(.*?);base64');
match := regex.Match(htmlContent);
if match.Success then
Result := match.Groups.Item[1].Value
else
Result := '';
end;
I made this for test and works well, it is able to send multiple images that is in the html message (need be in base64 format), in a near future I'll refactor entire code to interfaced interact to segregate and decouple the code.