A simple, self-hosted Mail Transfer Agent (MTA) for libver.gr that handles the full email delivery pipeline, from message submission to direct delivery to recipient mail servers (Gmail, Outlook, etc.) using SMTP.
Running your own MTA requires proper DNS, reverse DNS, and IP reputation; misconfiguration can lead to emails being rejected or marked as spam.
This project was made as part of an internship abroad at the Central Public Library of Veria and is a part of https://events.libver.gr (open-source github repo) that was made for the library.
Transactional emails like password resets, confirmation receipts, and notifications are necessary for the event registration system, this standalone service ensures:
- Privacy: No third-party provider has access to your email data or logs.
- Reliability: A Redis-backed queue ensures that if a destination server is temporarily down, the email isn't lost; it just waits and retries.
- Authority: Every email is signed with DKIM (RSA-2048).
- Node.js (>=20)
- Fastify - API server
- BullMQ - Queue management
- Redis - Durable message queue
- smtp-server - SMTP listener
- Nodemailer - Outbound SMTP and DKIM signing
- mailparser - Email parsing
- Pino - Logging
- Docker & docker-compose - Containerization
This service acts as a lightweight MTA:
- Inbound (SMTP)
Listens on port
2525for the Laravel application to submit emails. - Queueing Messages are pushed into Redis using BullMQ for durability and retry handling.
- MX Resolution The worker performs real-time DNS MX lookups to determine the recipient’s mail server.
- DKIM Signing Each message is signed using a private RSA-2048 key.
- Outbound Delivery A secure SMTP connection (STARTTLS) is established and the message is delivered directly to the destination server.
Security-wise, this service includes:
- Stream parsing to prevent "buffer bomb" attacks.
- Brute-force protection for SMTP and API credentials.
- Network isolation as Redis is not exposed publicly; internal services are bound to
127.0.0.1. - Runs as a non-root user (
appuser) inside the container.
To land in the Inbox, you must configure SPF, DKIM, and DMARC in your DNS (e.g., Cloudflare).
Type: TXT | Name: @ | Value: v=spf1 ip4:YOUR_SERVER_IP -all
If you already have an SPF record (for example from Google Workspace, Outlook, etc.), you need to merge them into a single record, having multiple SPF records will cause SPF validation to fail.
v=spf1 include:_spf.google.com -all
v=spf1 ip4:YOUR_SERVER_IP include:_spf.mx.cloudflare.net -all
v=spf1 ip4:YOUR_SERVER_IP include:_spf.mx.cloudflare.net include:_spf.google.com -all
Type: TXT | Name: mail._domainkey | Value: v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMI... (See dkim-public.pem)
You can have multiple DKIM records (unlike SPF), each one is identified by a selector (in this case: mail in mail._domainkey). This is good as it allows key rotation, you can add a new selector (e.g. mail2._domainkey) without breaking existing emails.
mail, the DNS record must be mail._domainkey.
Long keys are often split into multiple quoted strings. DNS providers usually handle this automatically, but formatting errors will break verification.
Type: TXT | Name: _dmarc | Value: v=DMARC1; p=reject; rua=mailto:admin@libver.gr;
From: domain, otherwise DMARC will fail.
p=reject is strict which is great for security, but misconfiguration can cause legitimate emails to be rejected so it's safer to start with p=none; then monitor reports, and move to quarantine -> reject
You will receive DMARC reports in XML files sent by providers (Google, Microsoft, etc.) showing who is sending emails as your domain and the pass/fail rates
Make sure the rua email exists and can receive mail, otherwise you lose visibility into issues.
Getting the engine running is straightforward.
- Configure Environment: Copy
.env.exampleto.envand set your internal passwords. - Spin up the Stack:
docker-compose up -d --build
- Verify Health: Visit
http://localhost:3000/healthto see the heart beating.
Make sure your environment is properly configured, as without them, emails will likely be rejected or flagged as spam:
- Static IP address
- Reverse DNS (PTR) → resolves to
mail.libver.gr - Port 25 outbound enabled
Once deployed, you can verify everything is working by checking the logs:
docker-compose logs -f appNote: During local or CI testing, emails are not sent to real recipients. Instead, the service uses Ethereal Email via Nodemailer, allowing you to preview sent emails in a web UI