Search code examples
htmlemaildelphibase64indy

How to send embedded base64 image (HTML) by email correctly?


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?


Solution

  • 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.