0

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 := 'from@mail.com';
  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:= 'mail@mail.com';
  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?

Ken White
  • 123,280
  • 14
  • 225
  • 444
snowdev
  • 1
  • 1
  • 2
    Why are you setting the `TIdMessage.ContentType` to `'multipart/mixed'` when the only thing in the email body is just the HTML? Your email does not contain MIME-encoded parts in it, so using `'multipart/...'` is wrong. `'text/html'` is the correct media type to use in this example. Don't use `'multipart/...'` unless you are making use of the `TIdMessage.MessageParts` to put multiple parts into the email body. – Remy Lebeau Jun 01 '23 at 19:48
  • 2
    Also, why are you breaking up your base64 into multiple lines? – Remy Lebeau Jun 01 '23 at 19:50
  • Try setting `TIdMessage.ContentTransferEncoding` to `'quoted-printable'`, see if that helps. – Remy Lebeau Jun 01 '23 at 19:59
  • 1
    It depends on the mail client. Outlook won't display inline Base64 encoded images. – Freddie Bell Jun 02 '23 at 04:55
  • Not a fix but note that the [](https://html.spec.whatwg.org/dev/embedded-content.html#the-img-element) tag does not use and does not need a closing slash and never has in any HTML specification. – Rob Jun 02 '23 at 07:24
  • I was testing other stuffs, this is why i used `'multipart/...'` but event using `'text/html'` doesn't work, the image is broke on the mail client (i was testing receiving in gmail). I tested the same html sending to an office365 account (outlook client) and when I trusted the mailer the image loaded. @FreddieBell answer is partially correct, gmail dont support base64 embedded images. https://stackoverflow.com/questions/3279523/base64-images-to-gmail/12786336 – snowdev Jun 03 '23 at 13:03
  • I using a CEF based HTML editor to edit the email body as HTML, this was the best Delphi way I found. CEF browser converts the embedded images to base64 by default. I'll try to convert the base64 to image file as temp file and include in the body with cid attribute, some stuff like this: https://niclogic.wordpress.com/delphi-pages/send-html-emails-using-delphi-and-indy-with-embedded-images/ I think that this is the best way to try to load all embedded images for most mail clients as possible. – snowdev Jun 03 '23 at 13:10
  • More ideas are appreciated. Thanks guys. – snowdev Jun 03 '23 at 13:13
  • I specifically mentioned "inline" images. An email which came from Outlook, where the user had pasted images into the email body, contains CID: references, which won't display in a web browser. If the CID: references are replaced with inline base64 encoded images, an Outlook client won't display them. The solution seems to be to upload the images to website and replace the CID: references with URL's. But you are adding base64-encoded attachments... – Freddie Bell Jun 03 '23 at 14:38

2 Answers2

0

Generic solution for using images in html part of email it’s build in images as attachment and use attachment ID as SRC of image. something like:

<img src="cid:2.jpg" alt="Red dot" />

In this case all client mail viewers must show email as it's should be.

Here is part of my old project that add all html images into attachments from 'file names' into corrent identifiers. It's not optimal sourses but you can get main idia from it.

uses
  IdBaseComponent,
  IdComponent, IdTCPConnection, IdTCPClient, IdExplicitTLSClientServerBase,
  IdMessageClient, IdSMTPBase, IdSMTP, IdIOHandler, IdIOHandlerSocket,
  IdIOHandlerStack, IdSSL, IdSSLOpenSSL,
  idMessage, IdAttachment,
  idText, IdAttachmentFile, math;

function HtmlDecode (const AStr: String): String;
begin
    Result := StringReplace(AStr,   '&apos;', '''', [rfReplaceAll]);    {Do not Localize}
    Result := StringReplace(Result, '&quot;', '"', [rfReplaceAll]);    {Do not Localize}
    Result := StringReplace(Result, '&gt;', '>', [rfReplaceAll]);    {Do not Localize}
    Result := StringReplace(Result, '&lt;', '<', [rfReplaceAll]);    {Do not Localize}
    Result := StringReplace(Result, '&amp;', '&', [rfReplaceAll]);    {Do not Localize}
end;

function IsXDigit(Ch : char) : Boolean;
begin
    Result := (ch in ['0'..'9']) or (ch in ['a'..'f']) or (ch in ['A'..'F']);
end;

function XDigit(Ch : char) : Integer;
begin
    if ch in ['0'..'9'] then
        Result := ord(Ch) - ord('0')
    else
        Result := (ord(Ch) and 15) + 9;
end;

function htoin(value : PChar; len : Integer) : Integer;
var
    i : Integer;
begin
  Result := 0;
  i      := 0;
  while (i < len) and (Value[i] = ' ') do
      i := i + 1;
  while (i < len) and (isxDigit(Value[i])) do begin
      Result := Result * 16 + xdigit(Value[i]);
      i := i + 1;
  end;
end;

function htoi2(value : PChar) : Integer;
begin
    Result := htoin(value, 2);
end;

function UrlDecode(S : String) : String;
var
    I  : Integer;
    Ch : Char;
begin
    Result := '';
    I := 1;
    while (I <= Length(S)) do begin
        Ch := S[I];
        if Ch = '%' then begin
            Ch := chr(htoi2(@S[I + 1]));
            Inc(I, 2);
        end
        else if Ch = '+' then
            Ch := ' ';
        Result := Result + Ch;
        Inc(I);
    end;
end;

procedure TForm3.HandleIMGID(ASourceDir: String;
  var AHTMString: String; AMessage: TIDMessage; AParentPart : integer = -1);
var
  IMGPos : Integer ;
  CurrentPoint : Pchar ;
  AFile : String ;
  AAttachment : TIdAttachment ;
begin
  CurrentPoint := pchar(AHTMString);
  IMGPos := 0 ;
  while pos('src="', CurrentPoint) <> 0 do begin
    IMGPos := IMGPos + pos('src="',CurrentPoint) + 4 ;
    CurrentPoint := pchar(AHTMString) + IMGPos ;
    if pos('"', CurrentPoint) <> 0 then begin
      AFile := copy(AHTMString, IMGPos + 1, pos('"', CurrentPoint) - 1);
      AFile := UrlDecode(afile) ;
      AFile := StringReplace(AFile, '/', '\', [rfReplaceAll]);
      if FileExists(IncludeTrailingBackslash(ASourceDir) + AFile) then
        AFile := IncludeTrailingBackslash(ASourceDir) + AFile;

      if FileExists(AFile) then begin
        AAttachment := TIdAttachmentFile.Create(AMessage.MessageParts, AFile);
        AAttachment.FileName := HtmlDecode(ExtractFileName(AFile));
        AAttachment.ContentType := 'image/jpeg';
        AAttachment.Headers.Add('Content-ID: <' + AAttachment.FileName + '>');
        AAttachment.ParentPart := AParentPart;
        delete(AHTMString, IMGPos + 1, pos('"', CurrentPoint) - 1);
        insert('cid:' + AAttachment.FileName, AHTMString, IMGPos + 1);
      end{if};
      CurrentPoint := CurrentPoint + min(pos('"', CurrentPoint), 0);
      AFile := '';
    end{if};
  end{while};
end;

procedure TForm3.Button1Click(Sender: TObject);
var
  s : string;
  xMessage : TIDMessage;
  APlainHTML, APlainText, ATextPart, AEmail : TIdText ;
begin
  IdSMTP1.Username := 'from@mail.com';
  IdSMTP1.Password := 'password';
  IdSMTP1.Port := 587;
  IdSMTP1.Host := 'hosturl.com';

  IdSMTP1.IOHandler := IdSSLIOHandlerSocketOpenSSL1;
  IdSMTP1.UseTLS := utUseExplicitTLS;
  IdSMTP1.Connect;

  xMessage := TIDMessage.Create(self) ;
  try
    //fill message attributes
    xMessage.From.Name := 'from@mail.com';
    xMessage.From.Address := 'from@mail.com';
    xMessage.Subject := 'Some test subject';

    //fill Recipients
    xMessage.Recipients.Clear ;

    xMessage.Recipients.EMailAddresses := 'recipient@gmail.com';

    xMessage.BccList.Clear;
    xMessage.ccList.Clear;

    //fill message content
    xMessage.ContentType := 'multipart/mixed';
    AEmail := TIdText.create(xMessage.MessageParts);
    AEmail.ContentType := 'multipart/related; type="multipart/alternative"';

    ATextPart := TIdText.create(xMessage.MessageParts);
    ATextPart.ContentType := 'multipart/alternative';
    ATextPart.ParentPart := 0;

    APlainText :=  TIdText.create(xMessage.MessageParts);
    APlainText.ContentType := 'text/plain';
    APlainText.Body.Text := 'Some plain text of mail';
    APlainText.ParentPart := ATextPart.Index;

    //load html
    APlainHTML := TIdText.create(xMessage.MessageParts);
    APlainHTML.ContentType := 'text/html';
    APlainHTML.ParentPart := ATextPart.Index;
    s :=  '<html>' + #13#10 +
          '  <head>' + #13#10 +
          '  </head>' + #13#10 +
          '  <body>' + #13#10 +
          '    <div>' + #13#10 +
          '      <p>Some text</p>' + #13#10 +
//            '      <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4' + #13#10 +
//            '        //8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" alt="Red dot" />' + #13#10 +
          '      <img src="2.jpg" alt="Red dot" />' + #13#10 +
          '    </div>' + #13#10 +
          '  </body>' + #13#10 +
          '</html>';
    HandleIMGID('C:\temp', s, xMessage, 0);
//    HandleIMGID(FSettings.TeplateFolder, s, xMessage, 0);
    APlainHTML.Body.Text := s;

    if IdSMTP1.Authenticate then begin
      IdSMTP1.Send(xMessage);
    end;
  finally
    xMessage.Free;
  end;
end;

P.S. All images from attachments will not be shown as attached files if it's uses in html part.

0

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@mail.com';
    LMessage.From.Name:= 'from name';
    LMessage.Recipients.Add.Address := 'email@mail.com';
    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@email.com';
    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.

snowdev
  • 1
  • 1