cd /news/ai-agents/automate-email-replies-on-linux-with… · home › topics › ai-agents › article
[ARTICLE · art-145358] src=mailkite.dev ↗ pub= topic=ai-agents verified=true sentiment=· neutral

Automate email replies on Linux with AI agents

A MailKite demo shows backend developers how to automate email replies on Linux by keeping mail acceptance separate from AI inference, using a local worker that persists requests, suppresses reply loops, and holds approval drafts. The companion automation directory requires Node 22.13+, and on a fresh database the run produces one completed support job, one suppressed automated message, one approval draft awaiting review, and one threaded reply in SQLite, with no external email sent. The comparison rates setup cost across Postfix + Dovecot, Haraka, WildDuck, Stalwart, Postal, and MailKite Server, concluding IMAP is the least disruptive path for an existing Postfix/Dovecot host.

by read10 min views1 publishedOct 5, 2026
Automate email replies on Linux with AI agents
Image: Mailkite (auto-discovered)

For backend developers automating email replies on Linux, keep mail acceptance separate from AI decisions. Compare IMAP, Sieve, SMTP hooks, REST, JMAP and webhooks, then run a local worker that persists requests, suppresses reply loops and holds approval drafts.

npm ci --ignore-scripts
npm run demo
npm test

Run in the companion automation directory; commands come from quickstart.sh. Node 22.13+ is required. On a fresh database, the run produces one completed support job, one suppressed automated message, one approval draft awaiting review, and one threaded reply in SQLite. Repeating it preserves those totals. No external email is sent.

Which Linux mail-server interface should an agent consume? #

Consume stored mail or durable application events, rather than making inference part of SMTP acceptance. An unavailable model should delay a support draft, not cause the mail server to lose a customer’s request. Our open-source Linux email-server pillar separates the infrastructure roles; this comparison focuses on reply automation.

These setup ratings are qualitative, not installation benchmarks. Moderate means configuring an integrated server and an adapter. High means coordinating mail infrastructure with application storage or several services. Customization describes where code lives, not whether the software can send email. An existing installation can make the nominally harder option the smallest change.

Stack Concrete intake → reply path Setup / customization cost
Postfix + Dovecot IMAP poll/IDLE → worker → Postfix submission; Sieve for fixed rules High greenfield; low disruption on an existing mailbox
Haraka rcpt validation + durablequeue hook → worker → outbound transport High; direct JavaScript control, queue policy is yours
WildDuck REST message reads → worker → submit API / ZoneMTA High; mailbox API plus MongoDB, Redis and SMTP components
Stalwart JMAP discovery/query/changes → worker → EmailSubmission Moderate; integrated stack, stateful protocol work
Postal Inbound HTTP route → worker → send API High; application-friendly, distinct retry contract
MailKite Server Signed metadata webhook → raw retrieval → worker → SDK basic send High; backend and edges, narrower hosted compatibility

For support acknowledgements, a queue-backed event is enough. For triage that changes folders or flags, mailbox APIs earn their complexity. Approval workflows need a durable held state regardless of which server receives the message.

Keep Postfix and Dovecot when the mailbox already exists #

IMAP is the least disruptive automation path for an established Postfix/Dovecot host. Authenticate a worker to a dedicated mailbox, fetch MIME without marking it read, persist a job, then advance a durable cursor. Scope its identity to account, mailbox, UIDVALIDITY and UID. IMAP IDLE, RFC 2177, supplies mailbox notifications when advertised. It doesn’t deliver a durable application job. Exit IDLE with DONE before fetching; reconnect and reconcile after interruptions. Polling is simpler when occasional latency is acceptable. Either approach still needs parsing, deduplication and an outbound submission credential.

Sequence numbers change. An unread flag is user interface state, not a work queue.

Dovecot Pigeonhole runs Sieve during LDA/LMTP delivery. Use fileinto for predictable routing and vacation for a fixed auto-response. Standard Sieve cannot execute arbitrary external programs. Dovecot’s nonstandard extprograms extensions require explicit configuration; they aren’t a portable promise that a Sieve script can call an LLM.

A separate Postfix pipe transport can import accepted mail into your queue. Its success exit means delivery completed; defer on temporary persistence failures. Configure a fixed executable and restricted user. Keep expensive inference out of that importer too. The Python integration guide expands the mailbox-worker path.

Haraka gives control over acceptance; REST and JMAP give mailbox state #

Choose Haraka for SMTP decisions, WildDuck for a mailbox API, and Stalwart for protocol-based synchronization. They aren’t interchangeable webhook servers. Haraka’s plugin contract requires recipient and queue handlers. Validate the receiving address at rcpt, persist MIME plus a job at queue, and return OK only after success. Storage failure should return DENYSOFT; don’t wait for inference.

Haraka has outbound machinery, but a custom application queue still owns model retries and reply policy. The Node.js Linux server guide covers that JavaScript boundary. Hook order matters: OK stops later handlers on that hook, so a misplaced acceptance handler can bypass intended processing.

WildDuck exposes message listing, retrieval and source reads. A triage worker can enumerate a dedicated mailbox, persist each source using its scoped API identity, and update flags after recording the decision. Its submission API accepts a registered sender and a reference object containing mailbox, message ID and reply action. ZoneMTA handles delivery. The suite architecture includes Haraka, Rspamd, MongoDB and Redis; that’s considerable infrastructure for one support address.

With Stalwart, discover the JMAP session, obtain account IDs and capabilities, then use Email/query and Email/get. Persist Email/changes state only after intake commits. Push prompts resynchronization rather than replacing it. Create the reply Email and submit it through EmailSubmission with an authorized identity. RFC 8621 defines these methods and their per-object errors. JMAP’s object ID isn’t the MIME Message-ID used in reply headers. This is worthwhile for synchronized mailbox tooling; it’s extra work for a single acknowledgement.

Postal’s inbound route needs its own failure mapping #

Postal can deliver raw or processed incoming mail over HTTP, but its retry rules need deliberate handling. Configure an inbound route to an endpoint, normalize its envelope and message fields, and persist before returning 200. The HTTP payload documentation requires a response within five seconds and says 5xx responses fail immediately.

That differs from the common “return 503 and the webhook provider retries” assumption. Document an adapter’s response policy against your deployed Postal version, including how outages enter its documented retry path. The demo’s loopback /events contract returns 503 on failure; it is not a drop-in Postal receiver. Postal suits self-hosted transactional applications, but its infrastructure and failure mapping remain operator work.

Persist eligibility and drafts before producing a reply #

Loop prevention is application logic, not a model instruction. RFC 3834 recommends withholding replies to non-no Auto-Submitted messages and marking generated responses auto-replied. The demo also suppresses null reverse-paths, self mail and list traffic. These checks run before the fixture model, so an automated notification never consumes inference.

The queue key includes adapter source, stable delivery ID and receiving address. Sender-controlled Message-ID alone would collapse unrelated deliveries or let a forged ID suppress another request. Decisions are saved before the reply sink runs. A lease fences competing workers; failed work backs off and becomes a visible dead job after five attempts.

composeReply in mail.mjs fixes From to the receiving address and To to the adapter-supplied envelope sender. It extends References with the parent’s Message-ID, uses inherited In-Reply-To when References is absent, and omits threading fields when no valid parent exists. A deterministic reply Message-ID stays stable across retries. The model supplies only an action and body.

An envelope sender isn’t proof of the sender’s identity. The demo trusts its local intake adapter and records replies without sending them. Before enabling remote replies, add sender-authorization policy, rate limits and a rule for unauthenticated mail. Loop guards alone don’t prevent replies to forged return addresses.

The offline suite passed 13 tests covering restart/replay, failed persistence, loop suppression, saved-draft retries, exhaustion, lease fencing, approvals, missing parents, model wire format and SDK contracts. We fixed a List-ID check exposed by those tests: Mailparser’s parsed header map isn’t the right place to assume that original header key survives. The guard now checks original header lines.

One local reply is not exactly-once SMTP.

If a remote send succeeds and the worker dies before recording completion, a retry can duplicate it. A deterministic Message-ID doesn’t prove provider deduplication. Reconcile uncertain delivery outcomes or use a verified transport idempotency contract before replacing the local sink.

Predict the retry: the model’s draft was saved, then the local reply sink failed. After reopening SQLite, should the worker ask the model for another draft?

Reveal the saved-draft exercise and run the fault test #

The worker reuses the saved draft. The test injects a sink failure at time 100 ms and asserts a due time of 1100 ms. Work at 1099 ms does nothing. After reopening the database, work at 1100 ms writes the original Stable draft; the model-call counter remains one. Those times are a controlled test clock, not measured latency.

After installing dependencies in automation/, run node --test --test-name-pattern='retry persists decision' test/automation.test.mjs. Inspect the saved-decision and restart assertions, especially the replacement model that throws if called.

For a second exercise, run node --test --test-name-pattern='review requires explicit approval' test/automation.test.mjs. It checks zero replies while held, then one local reply after explicit approval without regenerating the draft. Neither exercise invokes a live model or sends email.

MailKite Server webhooks and hosted agent routes are different contracts #

A durable webhook lets you own model selection and approval state without implementing a mailbox client. MailKite Server, which we build, provides that path and also has BYO-provider agent routes. Its webhook implementation posts metadata plus an admin-authenticated raw-message URL. Authenticate delivery, retrieve MIME from a configured trusted origin, and persist your job before acknowledgement.

Its signatures use seconds. The Node SDK’s hosted webhook verifier uses milliseconds, so it isn’t a compatible Server verifier. Don’t disable freshness to disguise the mismatch. The self-hosted route implementation pins reply destinations, but automated-mail avoidance is prompt advice there, not the demo’s header guard. Local agent routes are message-only; failures are logged, and documented outbound caps are missing. Choose the retrying webhook plus your worker when durable inference matters.

Hosted MailKite instead exposes SDK management routes. This complete function from mailkite.mjs is verified against a loopback contract fixture:

import { MailKite } from 'mailkite';

export async function createSupportAgent(sessionToken, address, baseUrl = 'https://api.mailkite.dev') {
  const mk = new MailKite({ accessToken: sessionToken, baseUrl });
  return mk.createRoute({
    match: address,
    action: 'agent',
    agentPrompt: 'Triage support mail. Do not approve refunds or change accounts. Escalate uncertain requests to the account owner.',
    agentContext: 'message',
  });
}

Creation requires a management/session credential.

Hosted mk.agent explicitly invokes an existing agent; it doesn’t create inbound routing. Self-hosted routes use their own admin interface, not that hosted SDK management endpoint. The Server developer API supports basic SDK sending with a local base URL/key, but refuses hosted templates, attachments and scheduling. External delivery needs its configured smarthost. The provider-building guide covers that operational split.

This is a narrower fit than WildDuck or Stalwart for mailbox synchronization. Haraka plus your queue offers more bespoke SMTP control; Postal offers a self-hosted transactional platform. Hosted routes reduce application plumbing, but don’t replace your business authorization or human approval state.

Customize support, triage and approval separately #

Give the model a bounded drafting job, then enforce actions in code. For support, retrieve approved documentation and draft a short answer. For triage, save labels and urgency without sending anything. For approvals, tie a held draft to an authenticated application record; an email requesting “approved” isn’t authorization to execute the underlying operation.

The default fixture recognizes an approval request and holds it. The optional OpenAI-compatible adapter accepts a configured model/base URL, uses a deadline and validates JSON. CLI drafts from that real adapter always enter review. Manual approval releases only the saved draft to the local sink. Neither the CLI nor its tests send live mail; the model’s HTTP test uses a fixture, not inference validation.

Run the automation demo, then customize its eligibility rules and held-state workflow before building a deployed adapter. The programmatic server comparison explains the broader integration boundaries. Automating email replies on Linux becomes manageable when the mail server owns acceptance, the worker owns durable decisions, and the application owns permission to act.

Official sources and interfaces checked 2026-10-05. Recheck by 2026-11-04 and on upgrades. The companion research and verification notes distinguish tested local behavior from deployment work.

── more in #ai-agents 4 stories · sorted by recency
── more on @mailkite 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/automate-email-repli…] indexed:0 read:10min 2026-10-05 · —