I have known Postfix for a long time.

Like many people who have run Linux mail servers, I have configured domains, users, aliases, lookup tables, spam filters and mail delivery. Years ago, I integrated Postfix with LDAP. In another engagement, we replaced a struggling mail setup with Postfix, SpamAssassin, Amavis and ClamAV, and within a week the client’s systems were flying.

So when I recently started looking at email architecture again, I had a practical question in mind: could some of the email services that organisations like ours pay vendors for be replaced with solutions we build and run ourselves, on open source?

I thought I was returning to familiar territory.

I was wrong.

Knowing how to configure Postfix and understanding how to architect an email platform turned out to be two different things. And four familiar terms started demanding much more attention:

Milter. LMTP. LDA. Virtual.

At first glance, they look like competing options in Postfix email architecture. They are not. Understanding why changed the way I think about mail systems, and this article explains what each one is, where it sits and why.


The Question That Changes Everything

Imagine an email arrives at your server. A lot needs to happen:

  • It may need to be checked for spam and malware.
  • Policies may need to be applied.
  • DKIM signatures may need to be verified or added.
  • The server must decide whether to accept the message at all.
  • If accepted, it eventually has to reach a mailbox, or another server.

It is tempting to summarise all of this as “Postfix receives the email and delivers it.”

But that hides the one question that explains everything else:

At what moment does our server accept responsibility for the email?

Once I looked at Milter, LMTP, LDA and Virtual from that angle, the alphabet soup started making sense.


The Most Important Line in Email Delivery: 250 OK

Here is a simplified conversation between a sending server and Postfix:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
Internet Sender
      |
      |  MAIL FROM / RCPT TO / DATA
      v
   Postfix SMTP
      |
      |  Filtering and policy decisions
      |
      |  250 OK  (or 4xx / 5xx)
      v
===============================
   RESPONSIBILITY ACCEPTED
===============================
      |
      v
 Postfix Queue
      |
      v
 Delivery (local mailbox or remote server)

That 250 OK matters far more than it appears.

Before it, the receiving server can still say “No, I am not accepting this message.” The sender keeps responsibility. If the message was spam, it simply never becomes your problem.

After it, the message is yours. If something goes wrong later, you cannot go back in time and change the answer you already gave. You can only queue, retry, bounce or discard.

Every component in this article lives on one side of that line or the other. That is the whole key.


Milter, LMTP, LDA and Virtual at a Glance

ComponentWhat it isSide of the 250 OK lineWhy you would choose it
MilterA protocol that lets Postfix consult an external filter (such as Rspamd) during the SMTP conversationBefore acceptanceReject, modify or tag mail while the sender is still connected
LMTPA delivery protocol for handing accepted mail to a mailbox service (such as Dovecot)After acceptanceScalable mailbox delivery with per-recipient results
LDAAn external program Postfix runs to deliver each message (such as dovecot-lda)After acceptanceSimple delivery with mailbox features, for smaller setups
VirtualPostfix’s own built-in delivery agent for virtual mailboxesAfter acceptanceThe simplest path: Postfix writes straight to Maildir or mbox

The conclusion is immediate:

Milter does not compete with LMTP, LDA or Virtual. Milter decides whether we accept a message. The other three decide how we deliver a message we have already accepted.


Milter: Decide While the Sender Is Still There

A remote server connects and says: “I have an email for one of your users.”

Postfix could accept it first and inspect it later. But if the message is obviously spam or breaks policy, why accept responsibility for it at all?

That is the purpose of Milter (short for mail filter). It began as Sendmail’s filtering interface, and Postfix has supported it since version 2.3. It lets Postfix talk to a filtering or policy engine during the SMTP transaction.

Think of it as a security officer at the gate. The visitor has arrived but is not yet inside. The officer inspects and advises: allow, reject, modify or tag.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Internet Sender
      |
      v
 Postfix SMTP  ------>  Milter (e.g. Rspamd)
      |                     |
      |                 Analyse
      |                     |
      |  <---- Accept / Reject / Modify
      v
  250 OK  or  5xx Reject  (sent to the original sender)

What matters is not simply that Milter filters. It is when it filters. Because the original SMTP session is still open, a rejection goes straight back to the sender. Responsibility never transfers to you.

In Postfix, this is the smtpd_milters setting (and non_smtpd_milters for locally submitted mail).

Milter is not the only way to decide before acceptance

Postfix has other mechanisms on the same side of the line:

  • Policy delegation (check_policy_service): an external service answers accept or reject based on the envelope, such as sender, recipient, client IP and rate limits.
  • Before-queue content filtering (smtpd_proxy_filter): the whole message passes through a filter before Postfix answers the sender.

And there is one on the other side:

  • After-queue content filtering (content_filter): Postfix accepts and queues the message first, hands it to a filter such as Amavis, and receives it back for delivery. This is the classic SpamAssassin and Amavis design many of us deployed years ago. It works well, but any decision it makes comes after 250 OK.

So the real rule is not “you must use Milter”. It is:

If you want the original sender to receive the rejection, the decision must happen before you accept responsibility for the message.

Milter is simply the most widely used, well-established way to plug content and policy engines into that point.


Then I Asked: Why Not LMTP?

This is where things became more interesting for me.

If LMTP hands a message to another service and gets back a success or failure response, why not use LMTP to send the message to a scanner and let that decide whether to accept it?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Internet Sender
      |
      v
SMTP Frontend
      |
      |---- LMTP ----> Scanner / Policy Engine
      |                       |
      |<---- Result ----------|
      |
      +---- 250 / 5xx ----> Sender

There is nothing magical about the letters LMTP that makes this impossible. A frontend could hold the original session open, pass the message downstream, wait for a verdict and only then reply.

But that is not how Postfix uses LMTP. And the reason reveals what LMTP was designed for.


LMTP: Delivery After the Decision Has Been Made

LMTP (Local Mail Transfer Protocol, RFC 2033) was designed for the last hop: handing mail to a system that stores mailboxes but does not want to run its own mail queue.

That design choice is the whole point. The queue, and with it the responsibility for retries, stays with Postfix. Dovecot only has to answer one question: “Can you put this into these mailboxes right now?”

So in Postfix, LMTP is a delivery transport. By the time it runs, the message has already been accepted and queued, and the original sender has usually disconnected. If Dovecot now says “I cannot deliver this”, Postfix cannot replace its earlier 250 OK with a rejection. That conversation is over. Postfix must handle it as a delivery failure: retry later or bounce.

The precise statement, then, is not “LMTP cannot work before acceptance.” It is:

Postfix’s LMTP transport is designed for delivery after Postfix has accepted and queued the message.

Why LMTP suits mailbox platforms

LMTP’s signature feature is the per-recipient response. If one message goes to three users, Dovecot can answer each separately:

1
2
3
User A: Delivered
User B: Delivered
User C: Mailbox over quota

Plain SMTP gives one answer for the whole message after DATA. LMTP gives one per recipient, which is exactly what a mailbox store needs.

LMTP also talks to a persistent service, so there is no new process to start for every message. That matters as volume grows.

In Postfix, this typically looks like virtual_transport = lmtp:unix:private/dovecot-lmtp.

Milter and LMTP: similar on the surface, opposite in purpose

Both involve Postfix talking to another component and getting a response. The difference is the question being asked:

  • Milter asks: Should I accept, reject or modify this message?
  • LMTP asks: I have accepted this message. Can you deliver it to these mailboxes?

LDA: The Traditional Delivery Approach

“LDA” causes confusion because the term means two things in Postfix discussions:

  • Postfix’s own local delivery agent, which handles system users, /etc/aliases and .forward files.
  • An external Local Delivery Agent that Postfix runs per message, most commonly Dovecot’s dovecot-lda (historically called deliver). Procmail and maildrop played this role in older setups.

In mailbox architecture discussions, “LDA” usually means the second one. Postfix hands each accepted message to a program, which writes it into the mailbox, applying Sieve rules and quotas along the way.

1
Postfix Queue  -->  start LDA process  -->  Mailbox  -->  process exits

It works, and for smaller environments it works perfectly well. But every delivery means starting a program, passing it the message, waiting and cleaning up. Postfix’s concurrency limits prevent an uncontrolled explosion of processes, but at higher volumes a persistent service (LMTP) is usually the cleaner design.

LDA is not wrong. It trades efficiency at scale for simplicity.


Virtual: When Postfix Delivers Directly

“Virtual” also has two meanings in Postfix:

  • Virtual alias domains, which only rewrite addresses to other addresses.
  • The virtual delivery agent, which actually stores mail for users who are not system accounts.

Here we mean the delivery agent. Postfix takes an accepted message and writes it directly into the right Maildir or mbox location, based on its own lookup tables.

1
Postfix Queue  -->  virtual delivery agent  -->  Maildir

No external process. No extra protocol. For a simple mail host, that simplicity is genuinely powerful.

The trade-off is features. The virtual agent has no Sieve filtering and no native quota enforcement. Once you need those, or deeper integration with an IMAP platform, handing delivery to Dovecot over LMTP becomes the better fit.

Across LMTP, LDA and Virtual, the question is never “which is universally better?” It is “what are we trying to build?”


A Modern Mailbox Architecture, Responsibility by Responsibility

Put each piece on its proper side of the line, and a clean mailbox architecture emerges:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
                    INTERNET
                       |
                       v
               +---------------+
               |  Postfix SMTP |
               +---------------+
                       |
                       v
               +---------------+
               | Milter/Rspamd |
               | Spam / Policy |
               +---------------+
                       |
                Accept / Reject
                       |
                    250 OK
          =========================
           RESPONSIBILITY ACCEPTED
          =========================
                       |
                       v
               +---------------+
               | Postfix Queue |
               +---------------+
                       |
                     LMTP
                       |
                       v
               +---------------+
               |    Dovecot    |
               | Mailbox Layer |
               +---------------+
                       |
                       v
                  User Mailbox

Each component now has one clear job:

  • Postfix handles SMTP and mail transport, and owns the queue.
  • Rspamd via Milter makes filtering and policy decisions before acceptance.
  • LMTP provides a clean hand-off for delivery.
  • Dovecot owns mailboxes, quotas, Sieve and IMAP.

The system becomes easier to reason about because no single component is asked to solve every problem.


What Goes Wrong When Decisions Sit on the Wrong Side of the Line

Many email architecture failures are not configuration typos. They are decisions placed after 250 OK that belonged before it.

FailureWhat happensArchitectural fix
BackscatterSpam is accepted, rejected later, and a bounce is sent to the (usually forged) sender address. Your server becomes a spam source and your IP reputation suffersReject spam during SMTP (Milter or before-queue filtering), not after queueing
Unknown recipients acceptedPostfix does not know which users exist, accepts mail for anyone, then bounces it at deliveryGive Postfix the list of valid recipients, or have it verify them with the mailbox layer before saying 250 OK
Over-quota found too lateMail is accepted, LMTP reports the mailbox full, Postfix must bounceCheck quota at SMTP time, for example with Dovecot’s quota-status policy service
Filter goes downWhen the Milter service stops, Postfix falls back to milter_default_action. With the default (tempfail), all inbound mail is deferred. With accept, mail flows unscannedChoose the fallback deliberately, and monitor the filter like any critical dependency
Slow scanning during SMTPHeavy content scans hold the sender’s connection open until it times out, and mail retries pile upKeep SMTP-time checks fast; move expensive analysis to after-queue processing where a late decision is acceptable

The pattern is the same every time: decide early what you can, and only accept what you are prepared to be responsible for.


But What About OTP and Bulk Email?

Everything above assumes we are building mailboxes.

But what if the goal is a service that sends OTPs, transactional messages or bulk communication? There may be no local mailbox at all, and the architecture changes completely.

Instead of:

1
Postfix -> LMTP -> Dovecot -> Mailbox

it looks more like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Application
     |
     v
 SMTP / API
     |
     v
Policy & Routing
     |
     v
    Queue
     |
     v
Outbound SMTP Gateways
     |
     v
Gmail / Microsoft / Yahoo / Others

LMTP may not even be part of the main path. The hard problems move elsewhere: rate limiting, IP reputation, bounce processing and deliverability.

And that reveals a bigger lesson.


Postfix Is Not “The Email Platform”

For years, many of us casually called a machine “the Postfix server.”

Technically, that is understandable. Architecturally, it limits how we think. Postfix is an extraordinarily capable Mail Transfer Agent, but a modern email platform involves far more:

  • SMTP transport, queues and routing
  • Spam and malware filtering
  • Authentication, DKIM, SPF and DMARC
  • Customer, domain and policy management
  • Rate limiting and IP reputation
  • Mailbox storage, IMAP and webmail
  • Bounce processing, reporting and observability

Once we stop asking “How do I configure Postfix to do everything?” and start asking “Which component is responsible for each function, and at what point in the message’s life?”, the architecture becomes clearer, and far more scalable.


The Bigger Lesson for System Architecture

This journey started with four technical terms. It ended with a principle that applies well beyond email:

Do not choose technologies before understanding responsibilities and transaction boundaries.

Milter and LMTP both connect Postfix to another service, but they sit on opposite sides of the most important boundary in email:

  • Before acceptance: we can still reject the message to the original sender.
  • After acceptance: the message is our responsibility.

LDA, LMTP and Virtual are simply different ways of handling delivery after that responsibility has been accepted. The right choice depends on whether you are building a small mail host, an enterprise mailbox platform, a transactional email service or something at Internet scale.

For a modern mailbox environment, Postfix + Milter/Rspamd + LMTP + Dovecot gives a clean separation of responsibilities. But it is not a universal formula. It is an architectural choice driven by the problem you are solving.


Frequently Asked Questions

What is the difference between LMTP and LDA in Postfix?

Both deliver mail that Postfix has already accepted. An LDA is a program Postfix starts for each message, while LMTP is a protocol for talking to a mailbox service that is always running, such as Dovecot. LMTP avoids starting a process per message and reports results per recipient, which is why it is usually preferred for larger mailbox platforms.

Does Milter replace LMTP?

No. Milter works during the SMTP conversation and helps decide whether to accept a message. LMTP works after acceptance and handles delivery to mailboxes. A typical modern setup uses both.

What is the difference between Milter and content_filter in Postfix?

Milter inspects mail before Postfix accepts it, so rejections go back to the original sender. content_filter is an after-queue design: Postfix accepts the message first, then passes it to a filter such as Amavis. Any rejection at that stage can only become a bounce or a discard.

When should I use the Postfix virtual delivery agent?

Use it when you want the simplest possible mail host: Postfix writes directly to Maildir or mbox with no external components. If you need quotas, Sieve filtering or tight IMAP integration, delivering to Dovecot over LMTP is the better fit.

Why should a mail server reject instead of bounce?

A rejection during SMTP tells the real sending server immediately, and the message never becomes your responsibility. A bounce after acceptance is a new email sent to the sender address, which spammers usually forge. Sending those bounces (backscatter) can get your server’s IP listed as a spam source.


Back to the Question I Started With

This exploration did not begin with a comparison of four Postfix components. It began with a practical question: could organisations like ours replace some of the email services we buy with solutions we build and run ourselves?

The answer is more encouraging than I expected. Break an email platform into its responsibilities, and you discover that most building blocks already exist: Postfix, Rspamd, Dovecot, databases, message queues, identity platforms and observability tools. As with the power of choice that open source gives enterprises, the challenge is not building every component from scratch.

The real challenge is architecting them correctly, deciding where each responsibility belongs, adding intelligence between them and operating the whole system reliably at scale. For a country thinking seriously about digital freedom and control over its own infrastructure, email is one of the most important places to start.

So that is where this series goes next:

Can we build our own modern email service provider using open source?

I think that journey is worth exploring.