Search code examples
delphiutf-8indyindy10delphi-10.1-berlin

Cannot send UTF-8 message with TIdSMTP


I have button to send a mail:

procedure TRealization.Button17Click(Sender: TObject);
var
  EmailCore: TStringStream;
  EmailBody, sInvoices: string;
  FilePath: string;
  MailTitle: string;
  i: integer;
begin
  if (Languages.ItemIndex > -1) then begin
    EmailCore := TStringStream.Create('', TEncoding.UTF8);
    if Languages.ItemIndex = 0 then begin
        FilePath := ExtractFilePath(Application.ExeName)+'/languages/pl.html';
        MailTitle := 'Płatność dla '+Monitoring.l_client_name.Caption;
    end else if Languages.ItemIndex = 1 then begin
        FilePath := ExtractFilePath(Application.ExeName)+'/languages/en.html';
        MailTitle := 'Invoice payment for '+Monitoring.l_client_name.Caption;
    end;
    EmailCore.LoadFromFile(FilePath);
    for i := 0 to (preview_invoices.RowCount-1) do begin
      sInvoices := sInvoices+'<tr><td>'+preview_invoices.Cells[2,i]+'</td><td>'+preview_invoices.Cells[4,i]+'</td><td>'+preview_invoices.Cells[4,i]+'</td><td>'+preview_invoices.Cells[5,i]+'</td><td>'+preview_invoices.Cells[6,i]+'</td></tr>';
    end;
    EmailBody := StringReplace(EmailCore.DataString, ':LIST:', sInvoices, [rfReplaceAll]);
    EmailBody := StringReplace(EmailBody, ':COMPANY:', Monitoring.l_client_name.Caption, [rfReplaceAll]);
    EmailBody := StringReplace(EmailBody, ':VAT:', Monitoring.l_client_vat.Caption, [rfReplaceAll]);
    EmailBody := StringReplace(EmailBody, ':NAME:', Main.l_uname.Caption, [rfReplaceAll]);
    Main.SendEmailIndy('my.mail.com',
        Main.l_uname.Caption,
        _GlobalData.EMail,
        Emails.Text,
        _GlobalData.EMail,
        '',
        MailTitle,
        EMailBody,
        true,
        nil);
    EmailCore.Free;
  end;
end;

And this is a function for sending e-mails:

procedure TMain.SendEmailIndy(
        const SMTPServer: string;
        const FromName, FromAddress: string;
        const ToAddresses: string; //comma "," separated list of e-mail addresses
        const CCAddresses: string; //comma "," separated list of e-mail addresses
        const BCCAddresses: string; //comma "," separated list of e-mail addresses
        const Subject: string;
        const EmailBody: string;
        const IsBodyHtml: Boolean; //verses Plain Text
        const Attachments: TStrings);
var
    smtp: TIdSMTP; // IdSmtp.pas
    msg: TidMessage; // IdMessage.pas
    builder: TIdCustomMessageBuilder; //IdMessageBuilder.pas
    s: string;
    emailAddress: string;
begin
    msg := TidMessage.Create(nil);
    msg.Encoding := meMIME;
    msg.ContentType := 'text/html';
    msg.CharSet := 'UTF-8';
    msg.ContentTransferEncoding:= 'quoted-printable';
    try
        if IsBodyHtml then begin
            builder := TIdMessageBuilderHtml.Create;
            TIdMessageBuilderHtml(builder).Html.Text := EmailBody
        end else begin
            builder := TIdMessageBuilderPlain.Create;
        end;

        try
            if Attachments <> nil then
            begin
                for s in Attachments do
                    builder.Attachments.Add(s);
            end;

            builder.FillMessage(msg);
        finally
            builder.Free;
        end;

        msg.From.Name := FromName;
        msg.From.Address := FromAddress;
        msg.Subject := Subject;

        //If the message is plaintext then we must fill the body outside of the PlainText email builder.
        //(the PlainTextBuilder is unable to build plaintext e-mail)
        if not IsBodyHtml then begin
            msg.Body.Text := EmailBody;
        end;

        for s in ToAddresses.Split([',']) do
        begin
            emailAddress := Trim(s);
            if emailAddress <> '' then
            begin
                with msg.recipients.Add do
                begin
                    //Name := '<Name of recipient>';
                    Address := emailAddress;
                end;
            end;
        end;

        for s in CCAddresses.Split([',']) do
        begin
            emailAddress := Trim(s);
            if emailAddress <> '' then
                msg.CCList.Add.Address := emailAddress;
        end;

        for s in BCCAddresses.Split([',']) do
        begin
            emailAddress := Trim(s);
            if emailAddress <> '' then
                msg.BccList.Add.Address := emailAddress;
        end;

        smtp := TIdSMTP.Create(nil);
        try
            smtp.Host := SMTPServer; // IP Address of SMTP server
            Smtp.UseTLS := utNoTLSSupport;
            smtp.Port := 587; //The default already is port 25 (the SMTP port)
            smtp.Username := _GlobalData.EMail;
            smtp.Password := _GlobalData.Password;
            //Indy (and C# SmtpClient class) already defaults to the computer name
            //smtp.HeloName :=
            smtp.Connect;
        try
            smtp.Send(msg);
            ShowMessage('Wiadomość wysłana.');
            log('EMAIL', 'Wysłano zapytanie do '+ToAddresses, StrToInt(Monitoring.t_mid.Caption));
        finally
          //smtp.Disconnect();
        end;
    finally
        //smtp.Free;
    end;
finally
    //msg.Free;
end;
end;

The problem is that when I receive an email, the body (e-mail title is actually fine and displays all characters) contains question marks where Polish or German letters are (ą, ż, ó, ę, ö, ä etc.). The message is badly encoded. I tried using UTF8Encode and other solutions found here but nothing actually worked. E-mails keep being sent with corrupted content.

I would appreciate any help.


Solution

  • Your assignment of TIdMessage.CharSet is ignored because TIdMessageBuilder resets the CharSet (and the ContentType, ContentTransferEncoding, and Encoding properties) before populating the TIdMessage.

    As such, you need to set the TIdMessageBuilderPlain.PlainTextCharSet and TIdMessageBuilderHtml.HtmlCharSet properties instead.

    Also, you don't need to use TIdMessageBuilderPlain and TIdMessageBuilderHtml separately. You can use TIdMessageBuilderHtml by itself, as it can create both plain-text and HTML emails, depending on which properties are filled. Your comment that "If the message is plaintext then we must fill the body outside of the PlainText email builder. the PlainTextBuilder is unable to build plaintext e-mail" is completely untrue, as explained in my blog article on how to use TIdMessageBuilderHtml.

    Try this function code:

    procedure TMain.SendEmailIndy(
      const SMTPServer: string;
      const FromName, FromAddress: string;
      const ToAddresses: string; //comma "," separated list of e-mail addresses
      const CCAddresses: string; //comma "," separated list of e-mail addresses
      const BCCAddresses: string; //comma "," separated list of e-mail addresses
      const Subject: string;
      const EmailBody: string;
      const IsBodyHtml: Boolean; //verses Plain Text
      const Attachments: TStrings);
    var
      smtp: TIdSMTP; // IdSmtp.pas
      msg: TidMessage; // IdMessage.pas
      builder: TIdMessageBuilderHtml; //IdMessageBuilder.pas
      s: string;
      emailAddress: string;
    begin
      msg := TIdMessage.Create(nil);
      try
        builder := TIdMessageBuilderHtml.Create;
        try
          if IsBodyHtml then
          begin
            builder.Html.Text := EmailBody;
            builder.HtmlCharSet := 'utf-8';
          end else
          begin
            builder.PlainText.Text := EmailBody;
            builder.PlainTextCharSet := 'utf-8';
          end;
    
          if Attachments <> nil then
          begin
            for s in Attachments do
              builder.Attachments.Add(s);
          end;
    
          builder.FillMessage(msg);
        finally
          builder.Free;
        end;
    
        msg.From.Name := FromName;
        msg.From.Address := FromAddress;
        msg.Subject := Subject;
    
        msg.Recipients.EmailAddresses := ToAddresses;
        msg.CCList.EmailAddresses := CCAddresses;
        msg.BccList.EmailAddresses := BCCAddresses;
    
        smtp := TIdSMTP.Create(nil);
        try
          smtp.Host := SMTPServer; // IP Address of SMTP server
          smtp.UseTLS := utNoTLSSupport;
          smtp.Port := 587; //The default already is port 25 (the SMTP port)
          smtp.Username := _GlobalData.EMail;
          smtp.Password := _GlobalData.Password;
          //Indy (and C# SmtpClient class) already defaults to the computer name
          //smtp.HeloName :=
          smtp.Connect;
          try
            smtp.Send(msg);
            log('EMAIL', 'Wysłano zapytanie do '+ToAddresses, StrToInt(Monitoring.t_mid.Caption));
          finally
            smtp.Disconnect;
          end;
          ShowMessage('Wiadomość wysłana.');
        finally
          smtp.Free;
        end;
      finally
        msg.Free;
      end;
    end;
    

    Also, just FYI, if you use UseTLS=utNoTLSSupport then you should be using Port=25. If you want to use Port=587 then you should be using UseTLS=utUseExplicitTLS instead (which requires an SSLIOHandler be assigned to the TIdSMTP.IOHandler property, like TIdSSLIOHandlerSocketOpenSSL).