Skip to content

Email Service

API

  • Go
  • Fiber (API, middleware)
  • Gorm (ORM)
  • MariaDB (Database)
  • Redis (Cache)
  • Docker (Containerization)
  • Swagger (API Documentation)

App

  • TypeScript
  • Vue.js
  • Vite (Bundler)
  • Tailwind (Styling)
  • Docker (Containerization)

Mailserver

Browser Extension

Installation

Prerequisites

Important

Docker Mailserver officially supports Linux. If you want to run it on macOS, please read this.

Config

cp api/.env.sample api/.env
cp app/.env.sample app/.env
cp mailserver/.env.sample mailserver/.env
cd mailserver
mkdir -p docker-data/dms/config/rspamd/override.d
cp config/postfix-main.cf.sample docker-data/dms/config/postfix-main.cf
cp config/postfix-relay-domains.cf.sample docker-data/dms/config/postfix-relay-domains.cf
cp config/postfix-aliases.cf.sample docker-data/dms/config/postfix-aliases.cf
cp config/user-patches.sh.sample docker-data/dms/config/user-patches.sh
cp config/rspamd/override.d/milter_headers.conf.sample docker-data/dms/config/rspamd/override.d/milter_headers.conf

Important

Make sure to set up the required config:

  • api/.env
  • app/src/env.json
  • mailserver/.env
  • create and configure config.json (registry credentials for daemon): cp config.json.sample config.json

Tip

For local testing, you can use MailHog or MailTrap as outbound SMTP client (SMTP_CLIENT_*).

API + App

Move to api directory

cd api

Run

docker compose up -d

App:
http://localhost:3001

API:
http://localhost:3000

Mailserver

Move to mailserver directory

cd mailserver

Run

# localhost
docker compose up -d

# Staging|Production
docker compose -f compose.deploy.yml up -d

Setup Postfix

docker exec -it mailserver sh

# Build the db file
postmap /etc/postfix/virtual

# Update the alias table
newaliases

# Enable DKIM signing, this will output the contents of DKIM TXT DNS record (mail._domainkey.domain.com)
setup config dkim selector mail

# Restart Postfix
supervisorctl restart postfix

# Show logs
setup debug show-mail-logs

Setup DKIM Signing

Update dkim_signing.conf:

nano docker-data/dms/config/rspamd/override.d/dkim_signing.conf

dkim_signing.conf:

# documentation: https://rspamd.com/doc/modules/dkim_signing.html

enabled = true;

sign_authenticated = true;
sign_local = true;
try_fallback = true;

use_domain = "header";
use_domain_sign_local = "header";
use_redis = false; # don't change unless Redis also provides the DKIM keys
use_esld = true;
allow_username_mismatch = true;

check_pubkey = false; # you want to use this in the beginning

# Fallback: sign any unrecognized domain with domain.com key (d=domain.com)
path = "/tmp/docker-mailserver/rspamd/dkim/rsa-2048-mail-domain.com.private.txt";
selector = "mail";

domain {
    domain.com {
        path = "/tmp/docker-mailserver/rspamd/dkim/rsa-2048-mail-domain.com.private.txt";
        selector = "mail";
    }
}

Restart Mailserver:

docker compose down
docker compose up -d

Test DKIM signing with https://dkimvalidator.com:

echo "Test email" | mail -s "Test email" wyygMXeSfnzl5l@dkimvalidator.com

Update Mailserver

docker compose pull
docker compose down
docker compose up -d

Browser Extension

Move to wxt directory

cd wxt

Run (development)

yarn dev
yarn dev:firefox

Build (beta, testing)

yarn build
yarn build:staging
yarn build:production
yarn build:firefox

Build (distribution)

yarn zip
yarn zip:firefox

Add New Domain

Mailserver

Update postfix-virtual.cf:

nano docker-data/dms/config/postfix-virtual.cf
@domain.com curl_email

Setup Postfix:

docker exec -it mailserver sh
postmap /etc/postfix/virtual
newaliases
setup config dkim selector mail domain domain.com
supervisorctl restart postfix

Update dkim_signing.conf:

nano docker-data/dms/config/rspamd/override.d/dkim_signing.conf
domain.com {
    path = "/tmp/docker-mailserver/rspamd/dkim/rsa-2048-mail-domain.com.private.txt";
    selector = "mail";
}

Restart mailserver:

docker compose down
docker compose -f compose.deploy.yml up -d

DNS Records

MX Records:

domain.com. . 14400 IN MX 10 mail.domain.com.
mail.domain.com. . 14400 IN MX 10 MAIL_SERVER_IPV4

Check:

dig MX domain.com +short
dig MX mail.domain.com +short

SPF Records:

domain.com. 3600 IN TXT "v=spf1 ip4:MAIL_SERVER_IPV4 -all"
mail.domain.com. 3600 IN TXT "v=spf1 ip4:MAIL_SERVER_IPV4 -all"

Check:

dig TXT domain.com +short
dig TXT mail.domain.com +short

DMARC (TXT record):

_dmarc.domain.com. 3600 IN TXT "v=DMARC1; p=quarantine"
_dmarc.mail.domain.com. 3600 IN TXT "v=DMARC1; p=quarantine"

Check:

dig TXT _dmarc.domain.com +short
dig TXT _dmarc.mail.domain.com +short

DKIM (TXT record):

mail._domainkey.domain.com. 3600 IN TXT v=DKIM1;k=rsa;p=DKIM_PUBLIC_KEY

Check:

dig TXT mail._domainkey.domain.com +short

API

Update .env:

nano .env
DOMAINS=domain1.com,domain2.com

Restart api:

docker compose down
docker compose up -d

App

Update GitHub Actions env variables:

STAGING_VITE_DOMAINS=domain1.com,domain2.com
PROD_VITE_DOMAINS=domain1.com,domain2.com

Restore DB from backup

DB backup is stored locally on the host machine in the ${HOME}/backups directory.

Unpack backup

cd ${HOME}/backups
gpg -o backup.tar.gz -d backup-latest.tar.gz.gpg
tar -xvf backup.tar.gz

Restore DB

# Stop the containers
docker compose down

# Clone the volume
docker volume create email_db_clone
docker run --rm -v email_db:/from -v email_db_clone:/to alpine sh -c "cp -a /from/. /to/"

# Remove the original volume
docker volume rm email_db

# Recreate the original volume from backup
docker run -d --name restore -v email_db:/email_db alpine
docker cp /unpacked_volume_dir/. restore:/email_db
docker stop restore && docker rm restore

# Start the containers
docker compose up -d

Staging Deployment

Pushing to the staging branch automatically builds and deploys images tagged staging via the CI Staging GitHub Action (see ci_staging.yml). It can also be triggered manually to deploy any branch onto the staging environment, still publishing under the staging tag.

Trigger via GitHub UI

  1. Go to ActionsCI StagingRun workflow.
  2. Optionally enter a branch to deploy (defaults to staging).
  3. Run the workflow and watch the logs.

Trigger via GitHub CLI

gh workflow run ci_staging.yml -f branch=my-branch
gh run watch

Revert Production Deployment

Production images are pushed with both a latest tag (watched by watchtower for auto-deploy) and the git tag of the release, e.g. v1.4.2 (see ci_production.yml). Reverting re-publishes an older version's images as latest via the Revert Production GitHub Action, so watchtower redeploys them on its next poll.

Trigger via GitHub UI

  1. Go to ActionsRevert ProductionRun workflow.
  2. Enter version with the git tag to roll back to (e.g. v1.4.2).
  3. Run the workflow and watch the logs; an unknown/never-released version fails fast.

Trigger via GitHub CLI

gh workflow run revert_production.yml -f version=v1.4.2
gh run watch

Tip

List available version tags for an image directly from the registry:

curl -u "$REGISTRY_USERNAME:$REGISTRY_PASSWORD" https://<registry>/v2/email/api/tags/list

Warning

Only releases built after version tagging was added have a version tag to revert to. The API also runs GORM auto-migration on startup, so reverting across a release with schema changes may leave the DB incompatible with the older API version.

Test

Run API tests:

go test ./... -v
go vet ./...
gosec ./...

Send test email:

docker exec -it mailserver sh
echo "Test email body" | mail -s "Test subject" example.alias@example.net

API Documentation

API docs:
http://localhost:3000/docs

Generate API docs:

cd api
swag init -g cmd/main.go

Tip

With Task, run task docs to generate API documentation.

About

Audited, Open-Source Email Aliasing Service

Resources

Code of conduct

Contributing

Security policy

Stars

65 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages