Sooner or later every Delphi server has to send email - password resets, confirmations,
reports with a PDF attached - and often read it too. The usual answer is to drop a
TIdSMTP into the server. It works, until it doesn't: OpenSSL DLLs to keep with
every exe, a slow mail host holding up a request, a failed send lost because nobody retried
it, and every application that sends mail carrying its own copy of the same code.
PBS Email Service takes a different approach. One Windows service is the only mail client in the system. Everything else writes a small text file into a queue folder, and reads received mail as plain files from another. This article explains the design and what the service does, and the full source is free to download.
To send an email, write one UTF-8 text file into the To_Send folder:
TO=recipient@example.com, second@example.com
FROMNAME=Your Company
SUBJECT=Booking 1234 confirmed
HTML=1
NOREPLY=1
PRIORITY=1
ATTACH=C:\Data\pdf\booking_1234.pdf
ATTACH=C:\Data\pdf\terms.pdf
BODY=
<p>Everything after BODY= is the body.</p>
Only TO and SUBJECT are required. TO takes several
addresses separated by commas or semicolons. NOREPLY sets Reply-To to
noreply@ your domain, PRIORITY sends it at highest priority, and
ATTACH may repeat for as many files as you like - they are read when the message
is sent and never deleted.
The one rule that matters: write the file as .tmp and rename it to
.eml when it is complete. A rename is atomic, so the service never picks up a
half-written file. Name it yyyymmdd_hhnnss_<GUID>.eml; the timestamp is how
the service judges its age.
A file that is not valid UTF-8 is read in the ANSI code page instead, so a writer that got the encoding wrong still sends.
The download includes Email.Send.pas, which writes the file for you. It has no
Indy and no other dependencies, so it can go into any application. Set it up once:
uses
Email.Send;
TEmail_Send.Company_Name := 'Your Company';
TEmail_Send.Company_Colour := '#0066cc';
TEmail_Send.Path_To_Queue := 'C:\Mail\Emails\To_Send\';
then queue from any thread:
if not TEmail_Send.Queue_Email ('user@example.com',
'Your booking',
LHtml_Body,
True, { HTML }
True, { no-reply }
False, { high priority }
[LPdf_File]) { attachments }
then Log (TEmail_Send.Last_Error);
Company_Colour is there for your own HTML templates: inherit from
TEmail_Send and add methods such as Queue_Password_Reset that build
a branded body and call Queue_Email.
Most send-and-forget code treats every failure the same way. SMTP does not. A reply from 500 to 599 is permanent - the address does not exist, the message was rejected - and retrying will never help. A 4xx reply is temporary - greylisting, a full mailbox, a busy server - and the same message will usually go through a minute later. A timeout, socket or TLS error has no reply code at all and is also worth retrying.
Indy raises EIdSMTPReplyError with the reply code in ErrorCode,
so the distinction is a few lines:
except
on E : EIdSMTPReplyError
do begin
Result := E.Message;
AIs_Permanent := (E.ErrorCode >= 500) and (E.ErrorCode <= 599);
end;
on E : Exception
do Result := E.Message; { timeouts, sockets, TLS: try again }
end;
What the service does with each outcome:
| Outcome | What happens to the file |
|---|---|
| Sent | Moved to Sent |
| Transient failure | Stays in To_Send and is retried after a backoff |
| Permanent failure, or a file it cannot parse | Moved to Send_Errors
with a _error.txt beside it holding the reason |
| Older than the give-up time (24 hours by default) | Moved to
Send_Errors without another attempt |
Two details stop a bad day from becoming a worse one. Each pass takes new files first, oldest first, and only then files whose backoff has expired, so a pile of failing mail never holds up today's password reset. And after any pass that set files aside, one summary email listing them goes to an administrator address - through the same queue, so it follows the same rules.
Most mail hosts now want port 587 with STARTTLS (explicit TLS), and many cloud hosts block port 25 outbound altogether. The Indy setup is short once you know it:
LSSL.SSLOptions.Method := sslvTLSv1_2;
LSSL.SSLOptions.Mode := sslmClient;
LSSL.SSLOptions.SSLVersions := [sslvTLSv1_2];
LSMTP.Host := 'mail.yourdomain.com';
LSMTP.Port := 587;
LSMTP.IOHandler := LSSL;
LSMTP.UseTLS := utUseExplicitTLS;
LMsg.CharSet := 'UTF-8';
With attachments the message goes as multipart/mixed: the body as the first
part, plain text or HTML, then each file as its own part under its own name. POP3 on port 995
uses utUseImplicitTLS with the same TLS 1.2 options.
The error everyone meets once. Delphi's standard TIdSSLIOHandlerSocketOpenSSL
loads the OpenSSL 1.0.2 DLLs, libeay32.dll and ssleay32.dll. They are
not part of Windows and not part of Delphi, and they must match the exe: a 64-bit exe needs the
64-bit DLLs, a 32-bit exe the 32-bit ones. The mismatch is the usual cause, since both sets have
the same names. Put the right pair beside the exe.
The service checks for the DLLs when it starts and stops with a message naming what is missing, instead of failing on the first email an hour later. With a mail service that is one folder to get right, not one per application.
A test system running on a copy of live data has real customers' addresses in it. Set
Redirect_Domain and every recipient ending in it goes to one address of yours
instead, with the original recipients in square brackets at the front of the subject so you
can see who would have received it. Leave Redirect_To blank and those recipients
are simply dropped.
Each send outcome - Sent, Retry or Failed - is one JSON line in a daily journal file, with
the time in UTC, the recipients, the subject and the reason. Put a tag such as
[TEST] or [TRAINING] in the subject and that mail goes into its own
journal folder, so live, test and training systems can share one service and still be reported
apart. It is easy to show on a web page as a list of emails sent. The journal, Sent
and Send_Errors folders tidy themselves after a set number of days.
Every minute, or whatever interval you set, the service collects the POP3 mailbox and writes each message to disk as a set of files sharing one name:
| File | Content |
|---|---|
<name>.eml | The raw message, written last so a reader that waits for it always sees a complete set |
<name>.txt | From, To, Subject, Date and Cc, then the plain-text body |
<name>.html | The HTML body, when there is one |
<name>_1.pdf, _2.jpg ... | Each attachment, numbered in order |
Your application needs no mail library to use them: wait for the .eml, read the
.txt, open the attachments, and move the set away once processed. That is how a
server turns an emailed booking form or invoice into a database row without ever touching
POP3.
Two things make the metadata more useful than the raw headers:
SENT_TO, and the forwarding headers are stripped from the body.TO.For testing, every received file can also be copied to a second folder so a test system
processes exactly the same mail as live. Messages can be left in the mailbox rather than
deleted, and a replay mode feeds a saved .eml through the parser instead of the
mailbox, so a message that was handled wrongly can be run again until it is right. An optional
.dec file and the included Test_Email_Parser show exactly how a message
was put together.
A mail service that has quietly stopped is worse than none. So:
[SEND], [RECV] or [SVC]. Every send failure is one line
naming the file, its subject and the actual error, ending will retry or
Moved to Send_Errors.| Program | Purpose |
|---|---|
PBS_Email_Service | The Windows service |
PBS_Email_Server | The same sender and receiver in a console window, to watch them live |
Email_Test | Tests the SMTP connection and login, then queues a test message |
Test_Email_Parser | Dumps the headers and parts of an
.eml file |
Full source for all four, Email.Send.pas for your applications, the shared
library units they use, a sample email.ini, and PBS_Email_Service.md
documenting every setting, the queue and received file formats and troubleshooting. The sender
and receiver can each be turned off in the ini, for example to run a send-only service.
libeay32.dll and ssleay32.dll, for the
exe's platformPBS_Email_Service.exe, email.ini and the two OpenSSL DLLs to a
folder.email.ini, and restrict access to
it - it holds the passwords.Email_Test.exe there to prove the SMTP login.PBS_Email_Service.exe /install, then start
"PBS Email Service".
Please chip in if helpful