Jump to content
Berocoder

Simulate blocking mode to send Email

Recommended Posts

Normally a developer want to use async mode when send emails to make main thread more responsive.

But I am in the situation to maintain an old ERP application. We use Delphi 11.3 for that.
Currently it use 2 kind of email solutions.

- SMTP. This contact an inhouse emailserver

- SendGrid https://sendgrid.com

 

There is now a request for a third email solution OutLook Azure from one big customer.

So TMS FNC CloudPack component was bought.
It was fairly easy to send emails once you get client id, secret etc correct.

But the component only use asynchronous design which is correct from architecture point of view.
The current code that use email method expect synchronous design.

 

As long as emailing is working my current solution is ok.
But there is no way to check and notify user in case of problem.
It can be network or other reasons for error.
 

To rewrite it require a major refactoring, that is also risky.
So the most pragmatic solution would be to simulate blocking mode with FNC CloudPack if possible.

So made 2 public methods in the mailing class.
Send and SendWait. Send use asynchronous on places where it is possible and SendWait use synchronous.

Current implementation

function TATMail.SendCloudMail(const aReceiver, aReplyTo, aCc, aSubject, aBody,
  aAttachments: string; ACallBack: TEMailCallBack; out AErrorMessage: string): Boolean;
var
  oOutLook: TOutlookEmail;
begin
  oOutLook := TOutlookEmail.Create(aReceiver, aReplyTo, aCc, aSubject, aBody, aAttachments, ACallBack);
  if not Assigned(ACallBack) then
  begin
    while true do
    begin
      if oOutLook.Done then
        break;

      Application.ProcessMessages;
    end;
    TraceLog.Trace('After blocking');
    AErrorMessage := oOutLook.fMessage;
    Result := oOutLook.fSuccess;
  end
  else
    Result := True;  // Doesn't matter. None blocking code should never check result.
end;

I am aware that ProcessMessages is famous for cause random bugs and would prefer to no using it.
I see that email component use CheckSynchronize to trigger the callback event. I tested 

while event.Waitfor(100) <> wrSignaled do
begin
  CheckSynchronize(50);
end;

I asked similar thing on Stack overflow https://stackoverflow.com/questions/79094352/how-can-i-convert-code-to-synkron-in-delphi
And @Dalija Prasnikar suggested that. But CheckSynchronize is supposed to run in the background thread not in main thread ?
Could be some misunderstanding. Anyway it didn't work.
So I search for a solution in main thread that cooperate well when backgroud thread is calling CheckSynchronize.
There is also some code on pastebin https://pastebin.com/EXsf4ywP
Only as illustration. It won't compile

 

Edited by Berocoder

Share this post


Link to post

CheckSynchronize must be called from main thread, just like Application.ProcessMessages. 

 

If the issue is that background thread is calling TThread.Synchronize or Tthread.Queue, then CheckSynchronize must work. If it doesn't work then the problem is in something else. I don't use FNC components so I cannot say what the actual issue is.

  • Like 1

Share this post


Link to post

Not a direct answer, but we (in our custom ERP solution), put the emails in a table (kind of a mailqueue) , and another process (service) polls and/or gets signaled when to scan the table for messages to send. 

With this we have better control, and are sure that the email is send in the background.


We also can control a minimum, maximum datetime to send the message etc etc.

(sometimes an email message is irrelevant if it didn't get send after a specific date)
 
So not high prio mails are send like every 5 minutes, high prio directly.


Only problem is that interactive emails (send with a client like outlook) are not under the same control. 

 

The service itself is highly configurable, with different outgoing servers, accounts etc etc., 

 

Edited by mvanrijnen

Share this post


Link to post

I find a solution now. I have not done much of thread-handling before
Here are the modified code:

function TATMail.SendCloudMail(const aReceiver, aReplyTo, aCc, aSubject, aBody,
  aAttachments: string; ACallBack: TEMailCallBack; out AErrorMessage: string): Boolean;
var
  oOutLook: TOutlookEmail;
begin
  oOutLook := TOutlookEmail.Create(aReceiver, aReplyTo, aCc, aSubject, aBody, aAttachments, ACallBack);
  if not Assigned(ACallBack) then
  begin
    // Simulate blocking
    while oOutLook.EventSignal.WaitFor(100) = wrTimeout do
    begin
      CheckSynchronize(50);
    end;

    AErrorMessage := oOutLook.fMessage;
    Result := oOutLook.fSuccess;
  end
  else
    Result := True;  // Doesn't matter. None blocking code should never check result.
end;

I made 2 important changes
1. Call SetEvent in the event that is called by FNC mailcomponent after mail was sent. I somehow thought that this happen automatically.
2. In while loop compare with wrTimeOut instead of "<> wrSignaled". But when I think about it now maybe not so important...

Edited by Berocoder

Share this post


Link to post
1 hour ago, Berocoder said:

    // Simulate blocking
    while oOutLook.EventSignal.WaitFor(100) = wrTimeout do
    begin
      CheckSynchronize(50);
    end;

 

A more efficient approach would be to wait on both your EventSignal and the RTL's Classes.SyncEvent at the same time, calling CheckSynchronize() only when SyncEvent is signaled (ie, when there are sync requests pending).  This way, when neither condition is true, your calling thread can actually go to sleep instead of running a busy loop.  For example:

 

uses
  ..., Classes, Windows;

var
  Handles: array[0..1] of THandle;
begin
  ...
  // Simulate blocking
  Handles[0] := oOutLook.EventSignal.Handle;
  Handles[1] := Classes.SyncEvent;
  repeat
    case Windows.WaitForMultipleObjects(2, @Handles[0], False, Infinite) of
      WAIT_OBJECT_0: Break;
      WAIT_OBJECT_0 + 1: CheckSynchronize;
    else
      RaiseLastOSError;
  until False;
  ...
end;

 

Edited by Remy Lebeau
  • Like 1

Share this post


Link to post
1 hour ago, Remy Lebeau said:

repeat case Windows.WaitForMultipleObjects(2, @Handles[0], False, Infinite) of WAIT_OBJECT_0: Break; WAIT_OBJECT_0 + 1: CheckSynchronize; else RaiseLastOSError; until False;

 

And WaitForMultipleObjectsEx allows you to add alertable flags to handle Paint message or all depending on requirement.

Share this post


Link to post
26 minutes ago, FredS said:

And WaitForMultipleObjectsEx allows you to add alertable flags to handle Paint message or all depending on requirement.

WaitForMultipleObjectsEx() allows you to stop waiting if an I/O completion routine or APC is executed on the calling thread.  That has nothing to do with message processing.  You are thinking of MsgWaitForMultipleObjects() instead.

Edited by Remy Lebeau
  • Like 1

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×