2
votes

I'm using Delphi 10.2 Tokyo and Indy 10.6.2.5366.

I'm trying to receive some SMTP messages from a foreign system, and the messages received are using multi-part MIME. I need to extract some fields and values from the message.

When I read the body as in the Indy SMTPServer Demo, the body is empty, but the body is written to the saved file.

How do I read that body into a TStringList? Is this a manual thing I must develop, or is there some method (couldn't find one) to do this?

This is what I do (Inspired from the demo):

procedure TForm1.IdSMTPServer1MsgReceive(ASender: TIdSMTPServerContext;
  AMsg: TStream; var LAction: TIdDataReply);
var
 LMsg : TIdMessage;
 LMsg2 : TIdMessage;
 LStream : TFileStream;
 i : integer;
 AMsgpart : TidMessagePart;

begin
// When a message is received by the server, this event fires.
// The message data is made available in the AMsg : TStream.
// In this example, we will save it to a temporary file, and the load it using
// IdMessage and parse some header elements.

  AddToLog('Msg recv '+formatdatetime('hh:nn:ss', Now));

  LMsg2 := TIdMessage.Create;
  try
    LMsg2.LoadFromStream(AMsg);
    ToLabel.Caption := LMsg2.Recipients.EMailAddresses;
    FromLabel.Caption := LMsg2.From.Text;
    SubjectLabel.Caption := LMsg2.Subject;

// not working
//    Memo1.Lines := LMsg2.Body;

// so what to do with this multi-part message in MIME format.
    for i := 0 to LMsg2.MessageParts.Count-1 do
    begin
      if (LMsg2.MessageParts.Items[i].PartType = mptText) then
       begin
         //AddToLog(LMsg2.MessageParts.Items[i].);
         AMsgpart := LMsg2.MessageParts.Items[i];




       end
    end;
  finally
    FreeAndNil(LMsg2);
  end;

  // Just write to a file as in demo
  LStream := TFileStream.Create(ExtractFilePath(Application.exename) + format('test(%d).eml',[mailno]), fmCreate);
  Try
    LStream.CopyFrom(AMsg, 0);
  Finally
    FreeAndNil(LStream);
  End;

end;

The message stream when written to a file looks like this

Received: from WIN-2SP97MPF39L[192.168.10.141] (helo=DesigoCC1) by HPNB2.hnit.local[192.168.10.131] with esmtp (My SMTP Server)
From: Alarms@DCC.dk
Date: Fri, 02 Feb 2018 09:46:39 +0100
To: mail@mail.com
Subject: =?UTF-8?B?QWxhcm0=?=
MIME-Version: 1.0
Content-Type: multipart/mixed;
       boundary="----=_NextPart_000_0006_01CAB9FA.E6640E80"

This is a multi-part message in MIME format.

------=_NextPart_000_0006_01CAB9FA.E6640E80
Content-Type: text/plain;
    format=flowed;
    charset="utf-8";
    reply-type=original
Content-Transfer-Encoding: 7bit

Date:2/2/2018 9:45:03 AM:02/02 09:45
Category:Information:
Desc:Servers.Main Server:
DescCompl:Project.Management System.Servers.Main Server
Tag:Mail.SMTP Status
TagCompl:Mail.SMTP Status [Mail]
Status:Quiet
EventComplProject.Management System.Servers.Main Server
Message:N/A
Cause:Mail Error (Wrong SMTP Server):Mail Error 
-----
Message ID: -(6138661-
------=_NextPart_000_0006_01CAB9FA.E6640E80--
1

1 Answers

1
votes

The TStream provided by the OnMsgReceive event is the raw email data as sent by the remote client. However, it is NOT in a format that TIdMessage.LoadFromStream() expects by default. Specifically, the TIdMessage.LoadFrom...() methods expect the data to be in an escaped format that SMTP uses during transmission. That escaping is undone before the OnMsgReceive event is fired. So, loading the TStream as-is may cause parsing issues.

Fortunately, Indy has a IdMessageHelper unit to address this very issue (see New TIdMessage helper, which describes the issue in more detail). It introduces new LoadFrom...() methods that add an AUsesDotTransparency parameter that you can set to False so TIdMessage won't expect the escaping anymore while parsing.

As for the content of the email, multi-part MIME bodies are not stored in the TIdMessage.Body property, they are stored in TIdMessagePart-derived objects, like TIdText, in the TIdMessage.MessageParts property instead. So, you need to scan through that property looking for the text you are interested in.

You also have to keep in mind that most of Indy's TCP-based servers are multi-threaded. The OnMsgReceive event is fired in a worker thread, so if you want to access the UI, you must synchronize with the main UI thread.

With that said, try something more like this:

uses
  ..., IdGlobalProtocols, IdMessageParts, IdText, IdMessage, IdMessageHelper;

procedure TForm1.IdSMTPServer1MsgReceive(ASender: TIdSMTPServerContext;
  AMsg: TStream; var LAction: TIdDataReply);
var
 LMsg : TIdMessage;
 LStream : TFileStream;
 i : integer;
 LMsgPart : TIdMessagePart;
 LText : TIdText;
begin
  // When a message is received by the server, this event fires.
  // The message data is made available in the AMsg : TStream.

  AddToLog('Msg recv ' + FormatDateTime('hh:nn:ss', Now));

  LMsg := TIdMessage.Create;
  try
    //LMsg.LoadFromStream(AMsg);
    LMsg.LoadFromStream(AMsg, False);

    TThread.Synchronize(nil,
      procedure
      begin
        ToLabel.Caption := LMsg.Recipients.EMailAddresses;
        // FYI, the *true* recipients of the email are stored in the
        // ASender.RCPTList property, which may include additional
        // recipients not specified by TIdMessage.Recipients due to BCCs...

        FromLabel.Caption := LMsg.From.Text;
        // FYI, ASender also has a From property, which is the *true* sender
        // of the email, which may be different than what TIdMessage.From
        // says...

        SubjectLabel.Caption := LMsg.Subject;
      end
    );

    if IsHeaderMediaType(LMsg.ContentType, 'multipart') then
    begin
      LText := nil;
      for i := 0 to LMsg.MessageParts.Count-1 do
      begin
        LMsgPart := LMsg.MessageParts[i];
        if (LMsgPart is TIdText) and
           IsHeaderMediaType(LMsgPart.ContentType, 'text/plain') then
        begin
          //AddToLog(LMsgPart);
          LText := TIdText(LMsgPart);
          Break;
        end
      end;
      TThread.Synchronize(nil,
        procedure
        begin
          if LText <> nil then
            Memo1.Lines := LText.Body
          else
            Memo1.Clear;
        end
      );
    end
    else if IsHeaderMediaType(LMsg.ContentType, 'text') then
    begin
      TThread.Synchronize(nil,
        procedure
        begin
          Memo1.Lines := LMsg.Body;
        end
      );
    end else
    begin
      TThread.Synchronize(nil,
        procedure
        begin
          Memo1.Clear;
        end
      );
    end;
  finally
    LMsg.Free;
  end;

  // Just write to a file as in demo
  LStream := TFileStream.Create(ExtractFilePath(Application.ExeName) + Format('test(%d).eml', [mailno]), fmCreate);
  try
    LStream.CopyFrom(AMsg, 0);
  finally
    LStream.Free;
  end;

  // Alternatively, AMsg is a TMemoryStream by default, unless you
  // provide your own TStream in the OnBeforeMsg event...
  //
  // TMemoryStream(AMsg).SaveToFile(ExtractFilePath(Application.ExeName) + Format('test(%d).eml', [mailno]));
end;