0

I am trying to send an email with a PDF attachment, stored in a BLOB field, using TIdSMTP. For this I am using a TIdAttachmentMemory, but the code as shown results in 'refused by spam filter';

  1. Omitting IdMessage.ContentType := 'multipart/mixed' works but the attachment is not sent (or received?) - as expected.
  2. Leaving this statement and creating the attachment from a file (as in the commented code) it all works fine (i.e. mail correctly received with attachment).

Clearly I am missing something. I am suspecting something in the direction of the attachment not being "closed off" correctly (i.e. left in an incomplete state) or perhaps the incorrect ContentType?

All suggestions welcome. Thanks!

procedure TfrmSendMail.btnSendClick(Sender: TObject);
var
  ms: TMemoryStream;
  Attachment: TIdAttachmentMemory;
  // Attachment: TIdAttachmentFile;
begin
  memStatus.Clear;

  IdSSLIOHandlerSocketOpenSSL.Destination := teHost.Text + ':587';
  IdSSLIOHandlerSocketOpenSSL.Host := teHost.Text;
  // IdSSLIOHandlerSocketOpenSSL.MaxLineAction := maException;
  IdSSLIOHandlerSocketOpenSSL.Port := 587;
  IdSSLIOHandlerSocketOpenSSL.SSLOptions.Method := sslvTLSv1_2;
  IdSSLIOHandlerSocketOpenSSL.SSLOptions.Mode := sslmUnassigned;
  IdSSLIOHandlerSocketOpenSSL.SSLOptions.VerifyMode := [];
  IdSSLIOHandlerSocketOpenSSL.SSLOptions.VerifyDepth := 0;

  IdSMTP.Host := teHost.Text;
  IdSMTP.Port := 587;

  IdMessage.From.Address := teFrom.Text;
  IdMessage.Recipients.EMailAddresses := teTo.Text;
  IdMessage.Subject := teSubject.Text;
  IdMessage.Body.Text := memBody.Text;
  IdMessage.Body.Add('Timestamp: ' + FormatDateTime('yyyy-mm-dd hh:nn:ss', Now()));

  IdMessage.ContentType := 'multipart/mixed';

  if not sqlPDFPDF_Incasso.IsNull then
  begin
    ms := TMemoryStream.Create;
    try
      try
        TBlobField(sqlPDF.FieldByName('PDF_Incasso')).SaveToStream(ms);
        ms.Position := 0;
        Attachment := TIdAttachmentMemory.Create(IdMessage.MessageParts, ms);
        Attachment.ContentType := 'application/pdf';
        Attachment.FileName := 'Invoice.pdf';
      except
        on E: Exception do
          messageDlg('Error creating attachment' + #13#10 + E.Message, mtError, [mbOK], 0);
      end;
    finally
      ms.Free;
    end;
  end;

  // if FileExists(beAttachment.Text) then
  // Attachment := TIdAttachmentFile.Create(IdMessage.MessageParts, beAttachment.Text);

  Screen.Cursor := crHourGlass;
  try
    try
      IdSMTP.Connect;
      IdSMTP.Send(IdMessage);
      memStatus.Lines.Insert(0, 'Email sent - OK.');
    except
      on E: Exception do
        memStatus.Lines.Insert(0, 'ERROR: ' + E.Message);
    end;
  finally
    if assigned(Attachment) then
      Attachment.Free;
    if IdSMTP.Connected then
      IdSMTP.Disconnect(true);
    Screen.Cursor := crDefault;
  end;
end;
Mark
  • 130
  • 1
  • 10
  • Forgot to mention: Delphi 10.3.1 and Indy 10. – Mark Apr 22 '19 at 19:50
  • Have you checked what happens when you send it to a server that doesn't have a spam filter? – Nat Apr 23 '19 at 03:58
  • @Nat: No I haven't. Where would I find such a server? I am just using the SMTP server of my provider (the same as used for regular mail). – Mark Apr 23 '19 at 08:12

1 Answers1

1

You are not populating the TIdMessage correctly (see this blog article for details - your use-case would fall under the "HTML and non-related attachments and no plain-text" section, but replacing HTML with Plain-Text).

In a nutshell, if you include the attachment, setting the TIdMessage.ContentType to 'multipart/mixed' is fine, but you need to put the body text into a TIdText object in the TIdMessage.MessageParts instead of in the TIdMessage.Body. And if you don't include the attachment, using the TIdMessage.Body is fine, but you need to set the TIdMessage.ContentType to 'text/plain' instead.

Try this:

procedure TfrmSendMail.btnSendClick(Sender: TObject);
var
  Text: TIdText;
  Attachment: TIdAttachmentMemory;
  Strm: TStream;
begin
  memStatus.Clear;

  IdSSLIOHandlerSocketOpenSSL.SSLOptions.Method := sslvTLSv1_2;
  IdSSLIOHandlerSocketOpenSSL.SSLOptions.Mode := sslmClient;
  IdSSLIOHandlerSocketOpenSSL.SSLOptions.VerifyMode := [];
  IdSSLIOHandlerSocketOpenSSL.SSLOptions.VerifyDepth := 0;

  IdSMTP.Host := teHost.Text;
  IdSMTP.Port := 587;

  try
    IdMessage.Clear;

    IdMessage.From.Address := teFrom.Text;
    IdMessage.Recipients.EMailAddresses := teTo.Text;
    IdMessage.Subject := teSubject.Text;

    //if FileExists(beAttachment.Text) then
    if not sqlPDFPDF_Incasso.IsNull then
    begin
      IdMessage.ContentType := 'multipart/mixed';

      Text := TIdText.Create(IdMessage.MessageParts, nil);
      Text.Body.Text := memBody.Text;
      Text.Body.Add('Timestamp: ' + FormatDateTime('yyyy-mm-dd hh:nn:ss', Now()));
      Text.ContextType := 'text/plain';

      //Attachment := TIdAttachmentFile.Create(IdMessage.MessageParts, beAttachment.Text);
      Attachment := TIdAttachmentMemory.Create(IdMessage.MessageParts);
      Attachment.ContentType := 'application/pdf';
      Attachment.FileName := 'Invoice.pdf';
      Strm := Attachment.PrepareTempStream;
      try
        TBlobField(sqlPDFPDF_Incasso).SaveToStream(Strm);
      finally
        Attachment.FinishTempStream;
      end;
    end else
    begin
      IdMessage.ContentType := 'text/plain';

      IdMessage.Body.Text := memBody.Text;
      IdMessage.Body.Add('Timestamp: ' + FormatDateTime('yyyy-mm-dd hh:nn:ss', Now()));
    end;

    Screen.Cursor := crHourGlass;
    try
      IdSMTP.Connect;
      try
        IdSMTP.Send(IdMessage);
      finally
        IdSMTP.Disconnect;
      end;
      memStatus.Lines.Insert(0, 'Email sent - OK.');
    finally
      Screen.Cursor := crDefault;
    end;
  except
    on E: Exception do
      memStatus.Lines.Insert(0, 'ERROR: ' + E.Message);
  end;
end;

Alternatively, Indy has a TIdMessageBuilderPlain class that can setup the TIdMessage properly for you (see this blog article for details - your use-case would fall under the "Plain-text and HTML and attachments: Non-related attachments only" section):

uses
  ..., IdMessageBuilder;

procedure TfrmSendMail.btnSendClick(Sender: TObject);
var
  Strm: TStream;
  Bldr: TIdMessageBuilderPlain;
begin
  memStatus.Clear;

  IdSSLIOHandlerSocketOpenSSL.SSLOptions.Method := sslvTLSv1_2;
  IdSSLIOHandlerSocketOpenSSL.SSLOptions.Mode := sslmClient;
  IdSSLIOHandlerSocketOpenSSL.SSLOptions.VerifyMode := [];
  IdSSLIOHandlerSocketOpenSSL.SSLOptions.VerifyDepth := 0;

  IdSMTP.Host := teHost.Text;
  IdSMTP.Port := 587;

  try
    IdMessage.Clear;

    IdMessage.From.Address := teFrom.Text;
    IdMessage.Recipients.EMailAddresses := teTo.Text;
    IdMessage.Subject := teSubject.Text;

    Strm := nil;
    try
      Bldr := TIdMessageBuilderPlain.Create;
      try
        Bldr.PlainText.Text := memBody.Text;
        Bldr.PlainText.Add('Timestamp: ' + FormatDateTime('yyyy-mm-dd hh:nn:ss', Now()));

        //if FileExists(beAttachment.Text) then
        if not sqlPDFPDF_Incasso.IsNull then
        begin
          //Bldr.Attachments.Add(beAttachment.Text);
          Strm := sqlPDFPDF_Incasso.DataSet.CreateBlobStream(sqlPDFPDF_Incasso, bmRead);
          Bldr.Attachments.Add(Strm, 'application/pdf').WantedFileName := 'Invoice.pdf';
        end;

        Bldr.FillMessage(IdMessage);
      finally
        Bldr.Free;
      end;
    finally
      Strm.Free;
    end;

    Screen.Cursor := crHourGlass;
    try
      IdSMTP.Connect;
      try
        IdSMTP.Send(IdMessage);
      finally
        IdSMTP.Disconnect;
      end;
      memStatus.Lines.Insert(0, 'Email sent - OK.');
    finally
      Screen.Cursor := crDefault;
    end;
  except
    on E: Exception do
      memStatus.Lines.Insert(0, 'ERROR: ' + E.Message);
  end;
end;
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • The first example works great. Using PrepareTempStream / FinishTempStream seems to confirm I was on the right track with the assumption that the attachement wasn't prepared ("closed off") correctly. I did some minor tweaking in that I removed the `try` below `Screen.Cursor := crHourGlass;`. There was already a `try` below `IdSMTP.Port := 587;`. It also appears that you removed setting Destination, Host and Port for the IdSSLIOHandlerSocketOpenSSL. Is this intentional (e.g.are they taken from the associated IdSMTP)? Thanks for your help and the example! – Mark Apr 24 '19 at 18:15
  • @Mark "*Using PrepareTempStream / FinishTempStream seems to confirm I was on the right track with the assumption that the attachement wasn't prepared ("closed off") correctly*" - actually, I did that because it is more efficient. `TIdMemoryAttachment` has an internal `TMemoryStream` that you can read the blob into, making your local `TMemoryStream` redundant. You can use your own `TMemoryStream` if you want, the `TIdMemoryAttachment` constructor will make a copy of the data. That part was fine. The point of my answer was that you were not setting up *the rest* of the `TIdMessage` correctly. – Remy Lebeau Apr 24 '19 at 18:26
  • @Mark "*It also appears that you removed setting Destination, Host and Port for the IdSSLIOHandlerSocketOpenSSL. Is this intentional*" - yes, as they are filled in automatically by `TIdTCPClient.Connect()`. – Remy Lebeau Apr 24 '19 at 18:27
  • Thanks for clarifying, I almost missed that point. So, if my understanding is right, I can set some `IdMessage` properties (`To`, `From`, `Subject`) and then define `IdMessage.ContentType := 'multipart/mixed';` so that `MessageParts` can added as required with `ContentType` set for each `MessageParts`? – Mark Apr 24 '19 at 18:34
  • @Mark Something like that. I suggest you read up on how [MIME](https://en.wikipedia.org/wiki/MIME) works. The `TIdMessage.MessageParts` *loosely* follows the MIME model (the default `TIdMessage.Encoding` is `meMIME` when `TIdMessage.MessageParts` is used). – Remy Lebeau Apr 24 '19 at 18:38