🐳 راه‌اندازی با داکر

شروع سریع

docker pull ghcr.io/themadorg/madmail:latest

تگ‌های تصویر

Tag توضیحات
latest آخرین نسخه پایدار از شاخه main
X.Y.Z نسخه مشخص (مثلاً 0.18.0)

روش ۱: ساده با آی‌پی (SQLite)

docker run -d --name madmail \ -p 25:25 \ -p 143:143 \ -p 465:465 \ -p 587:587 \ -p 993:993 \ -p 80:80 \ -p 443:443 \ -p 1080:1080 \ -v ./maddy.conf:/data/maddy.conf \ -v ./data:/data \ -v ./tls:/data/tls \ -e MADDY_HOSTNAME=SERVER_IP \ -e MADDY_DOMAIN="[SERVER_IP]" \ -e MADDY_PUBLIC_IP=SERVER_IP \ ghcr.io/themadorg/madmail:latest

یک فایل docker-compose.yml و یک فایل maddy.conf نیاز دارید. در این حالت گواهینامه TLS خودامضا (self-signed) به صورت خودکار تولید می‌شود.

docker-compose.yml
services:
  madmail:
    image: ghcr.io/themadorg/madmail:latest
    restart: always
    ports:
      # SMTP: Incoming/Outgoing email (Standard)
      - "25:25"
      # IMAP: Reading email (STARTTLS)
      - "143:143"
      # SUBMISSION: Outgoing email (TLS/SSL)
      - "465:465"
      # SUBMISSION: Outgoing email (STARTTLS)
      - "587:587"
      # IMAPS: Reading email (TLS/SSL)
      - "993:993"
      # API: Admin and Registration (HTTP)
      - "80:80"
      # API: Admin and Registration (HTTPS)
      - "443:443"
      # PROXY: Shadowsocks service
      - "1080:1080"
    volumes:
      - ./maddy.conf:/data/maddy.conf
      - ./data:/data
      - ./tls:/data/tls
    environment:
      - MADDY_HOSTNAME=SERVER_IP
      - MADDY_DOMAIN=[SERVER_IP]
      - MADDY_PUBLIC_IP=SERVER_IP
      - MADDY_SQLITE_UNSAFE_SYNC_OFF=1
maddy.conf
$(hostname) = {env:MADDY_HOSTNAME}
$(primary_domain) = {env:MADDY_DOMAIN}
$(local_domains) = $(primary_domain) $(hostname)
$(public_ip) = {env:MADDY_PUBLIC_IP}

tls file /data/tls/fullchain.pem /data/tls/privkey.pem

state_dir /data

auth.pass_table local_authdb {
    auto_create yes
    table sql_table {
        driver sqlite3
        dsn /data/credentials.db
        table_name passwords
    }
}

storage.imapsql local_mailboxes {
    auto_create yes
    driver sqlite3
    dsn /data/imapsql.db
}

hostname $(hostname)

table.chain local_rewrites {
    optional_step regexp "(.+)\+(.+)@(.+)" "$1@$3"
    optional_step static {
        entry postmaster postmaster@$(primary_domain)
    }
}

msgpipeline local_routing {
    destination postmaster $(local_domains) {
        modify {
            replace_rcpt &local_rewrites
        }
        deliver_to &local_mailboxes
    }
    default_destination {
        reject 550 5.1.1 "User doesn't exist"
    }
}

smtp tcp://0.0.0.0:25 {
    dmarc yes
    check {
        require_mx_record
        dkim
        spf
    }
    source $(local_domains) {
        reject 501 5.1.8 "Use Submission for outgoing SMTP"
    }
    default_source {
        destination postmaster $(local_domains) {
            deliver_to &local_routing
        }
        default_destination {
            reject 550 5.1.1 "User doesn't exist"
        }
    }
}

submission tls://0.0.0.0:465 tcp://0.0.0.0:587 {
    auth &local_authdb
    tls file /data/tls/fullchain.pem /data/tls/privkey.pem
    source $(local_domains) {
        check {
            authorize_sender {
                prepare_email &local_rewrites
                user_to_email identity
            }
        }
        destination postmaster $(local_domains) {
            deliver_to &local_routing
        }
        default_destination {
            modify {
                dkim $(primary_domain) $(local_domains) default
            }
            deliver_to &remote_queue
        }
    }
    default_source {
        reject 501 5.1.8 "Non-local sender domain"
    }
}

target.remote outbound_delivery {
    mx_auth {
        dane
        mtasts {
            cache fs
            fs_dir mtasts_cache/
        }
        local_policy {
            min_tls_level encrypted
            min_mx_level none
        }
    }
}

target.queue remote_queue {
    target &outbound_delivery
    autogenerated_msg_domain $(primary_domain)
    bounce {
        destination postmaster $(local_domains) {
            deliver_to &local_routing
        }
        default_destination {
            reject 550 5.0.0 "Refusing to send DSNs to non-local addresses"
        }
    }
}

imap tls://0.0.0.0:993 tcp://0.0.0.0:143 {
    auth &local_authdb
    storage &local_mailboxes
    tls file /data/tls/fullchain.pem /data/tls/privkey.pem
}

chatmail tls://0.0.0.0:443 tcp://0.0.0.0:80 {
    mail_domain $(primary_domain)
    mx_domain $(hostname)
    web_domain $(hostname)
    public_ip $(public_ip)
    auth_db local_authdb
    storage local_mailboxes
    tls file /data/tls/fullchain.pem /data/tls/privkey.pem
    ss_addr 0.0.0.0:1080
}
نکته: وقتی از آی‌پی بجای دامنه استفاده می‌کنید، باید آن را در MADDY_DOMAIN داخل براکت قرار دهید: [203.0.113.50]

روش ۲: با PostgreSQL

مشکل database is locked در SQLite با استفاده از PostgreSQL برطرف می‌شود. همچنین pub/sub برای آپدیت‌های IMAP بلادرنگ فعال می‌شود.

docker network create mad-net && \ docker run -d --name db --network mad-net \ -e POSTGRES_PASSWORD=pass \ -e POSTGRES_DB=maddy \ postgres:16-alpine && \ docker run -d --name madmail --network mad-net \ -p 25:25 \ -p 143:143 \ -p 465:465 \ -p 587:587 \ -p 993:993 \ -p 80:80 \ -p 443:443 \ -p 1080:1080 \ -v ./maddy.conf:/data/maddy.conf \ -v ./data:/data \ -v ./tls:/data/tls \ -e MADDY_HOSTNAME=SERVER_IP \ -e MADDY_DOMAIN="[SERVER_IP]" \ -e MADDY_PUBLIC_IP=SERVER_IP \ ghcr.io/themadorg/madmail:latest
docker-compose.yml
services:
  madmail:
    image: ghcr.io/themadorg/madmail:latest
    restart: always
    ports:
      # SMTP: Incoming/Outgoing email (Standard)
      - "25:25"
      # IMAP: Reading email (STARTTLS)
      - "143:143"
      # SUBMISSION: Outgoing email (TLS/SSL)
      - "465:465"
      # SUBMISSION: Outgoing email (STARTTLS)
      - "587:587"
      # IMAPS: Reading email (TLS/SSL)
      - "993:993"
      # API: Admin and Registration (HTTP)
      - "80:80"
      # API: Admin and Registration (HTTPS)
      - "443:443"
      # PROXY: Shadowsocks service
      - "1080:1080"
    volumes:
      - ./maddy.conf:/data/maddy.conf
      - ./data:/data
      - ./tls:/data/tls
    environment:
      - MADDY_HOSTNAME=SERVER_IP
      - MADDY_DOMAIN=[SERVER_IP]
      - MADDY_PUBLIC_IP=SERVER_IP
      - POSTGRES_USER=madmail
      - POSTGRES_PASSWORD=madmail_pass
      - POSTGRES_DB=madmail
    depends_on:
      - db

  db:
    image: postgres:alpine
    restart: always
    environment:
      - POSTGRES_USER=madmail
      - POSTGRES_PASSWORD=madmail_pass
      - POSTGRES_DB=madmail
    volumes:
      - ./pgdata:/var/lib/postgresql/data
maddy.conf
$(hostname) = {env:MADDY_HOSTNAME}
$(primary_domain) = {env:MADDY_DOMAIN}
$(local_domains) = $(primary_domain) $(hostname)
$(public_ip) = {env:MADDY_PUBLIC_IP}

tls file /data/tls/fullchain.pem /data/tls/privkey.pem

state_dir /data

auth.pass_table local_authdb {
    auto_create yes
    table sql_table {
        driver postgres
        dsn "host=db dbname={env:POSTGRES_DB} user={env:POSTGRES_USER} password={env:POSTGRES_PASSWORD} sslmode=disable"
        table_name passwords
    }
}

storage.imapsql local_mailboxes {
    auto_create yes
    driver postgres
    dsn "host=db dbname={env:POSTGRES_DB} user={env:POSTGRES_USER} password={env:POSTGRES_PASSWORD} sslmode=disable"
}

hostname $(hostname)

table.chain local_rewrites {
    optional_step regexp "(.+)\+(.+)@(.+)" "$1@$3"
    optional_step static {
        entry postmaster postmaster@$(primary_domain)
    }
}

msgpipeline local_routing {
    destination postmaster $(local_domains) {
        modify {
            replace_rcpt &local_rewrites
        }
        deliver_to &local_mailboxes
    }
    default_destination {
        reject 550 5.1.1 "User doesn't exist"
    }
}

smtp tcp://0.0.0.0:25 {
    dmarc yes
    check {
        require_mx_record
        dkim
        spf
    }
    source $(local_domains) {
        reject 501 5.1.8 "Use Submission for outgoing SMTP"
    }
    default_source {
        destination postmaster $(local_domains) {
            deliver_to &local_routing
        }
        default_destination {
            reject 550 5.1.1 "User doesn't exist"
        }
    }
}

submission tls://0.0.0.0:465 tcp://0.0.0.0:587 {
    auth &local_authdb
    tls file /data/tls/fullchain.pem /data/tls/privkey.pem
    source $(local_domains) {
        check {
            authorize_sender {
                prepare_email &local_rewrites
                user_to_email identity
            }
        }
        destination postmaster $(local_domains) {
            deliver_to &local_routing
        }
        default_destination {
            modify {
                dkim $(primary_domain) $(local_domains) default
            }
            deliver_to &remote_queue
        }
    }
    default_source {
        reject 501 5.1.8 "Non-local sender domain"
    }
}

target.remote outbound_delivery {
    mx_auth {
        dane
        mtasts {
            cache fs
            fs_dir mtasts_cache/
        }
        local_policy {
            min_tls_level encrypted
            min_mx_level none
        }
    }
}

target.queue remote_queue {
    target &outbound_delivery
    autogenerated_msg_domain $(primary_domain)
    bounce {
        destination postmaster $(local_domains) {
            deliver_to &local_routing
        }
        default_destination {
            reject 550 5.0.0 "Refusing to send DSNs to non-local addresses"
        }
    }
}

imap tls://0.0.0.0:993 tcp://0.0.0.0:143 {
    auth &local_authdb
    storage &local_mailboxes
    tls file /data/tls/fullchain.pem /data/tls/privkey.pem
}

chatmail tls://0.0.0.0:443 tcp://0.0.0.0:80 {
    mail_domain $(primary_domain)
    mx_domain $(hostname)
    web_domain $(hostname)
    public_ip $(public_ip)
    auth_db local_authdb
    storage local_mailboxes
    tls file /data/tls/fullchain.pem /data/tls/privkey.pem
    ss_addr 0.0.0.0:1080
}

روش ۳: دامنه با TLS خودکار (Let's Encrypt)

docker run -d --name madmail \ -p 25:25 \ -p 143:143 \ -p 465:465 \ -p 587:587 \ -p 993:993 \ -p 80:80 \ -p 443:443 \ -p 1080:1080 \ -v ./maddy.conf:/data/maddy.conf \ -v ./data:/data \ -e MADDY_HOSTNAME=chat.example.org \ -e MADDY_DOMAIN=chat.example.org \ -e MADDY_PUBLIC_IP=SERVER_IP \ -e MADDY_ACME_EMAIL=admin@example.org \ ghcr.io/themadorg/madmail:latest
⚠️ پیش‌نیازها:
docker-compose.yml
services:
  madmail:
    image: ghcr.io/themadorg/madmail:latest
    restart: always
    ports:
      # SMTP: Incoming/Outgoing email (Standard)
      - "25:25"
      # IMAP: Reading email (STARTTLS)
      - "143:143"
      # SUBMISSION: Outgoing email (TLS/SSL)
      - "465:465"
      # SUBMISSION: Outgoing email (STARTTLS)
      - "587:587"
      # IMAPS: Reading email (TLS/SSL)
      - "993:993"
      # HTTP: ACME HTTP-01 challenge handler + redirect to HTTPS
      - "80:80"
      # HTTPS: Admin API, Registration, ALPN-multiplexed SMTP+IMAP
      - "443:443"
      # PROXY: Shadowsocks service
      - "1080:1080"
    volumes:
      - ./maddy.conf:/data/maddy.conf
      - ./data:/data
    environment:
      - MADDY_HOSTNAME=chat.example.org
      - MADDY_DOMAIN=chat.example.org
      - MADDY_PUBLIC_IP=203.0.113.50
      - MADDY_ACME_EMAIL=admin@example.org
maddy.conf
$(hostname) = {env:MADDY_HOSTNAME}
$(primary_domain) = {env:MADDY_DOMAIN}
$(local_domains) = $(primary_domain)
$(public_ip) = {env:MADDY_PUBLIC_IP}

tls {
    loader autocert {
        hostname {env:MADDY_HOSTNAME}
        email {env:MADDY_ACME_EMAIL}
        cache_dir /data/autocert
        agreed
    }
}

state_dir /data

auth.pass_table local_authdb {
    auto_create yes
    table sql_table {
        driver sqlite3
        dsn /data/credentials.db
        table_name passwords
    }
}

storage.imapsql local_mailboxes {
    auto_create yes
    driver sqlite3
    dsn /data/imapsql.db
}

hostname $(hostname)

table.chain local_rewrites {
    optional_step regexp "(.+)\+(.+)@(.+)" "$1@$3"
    optional_step static {
        entry postmaster postmaster@$(primary_domain)
    }
}

msgpipeline local_routing {
    destination postmaster $(local_domains) {
        modify {
            replace_rcpt &local_rewrites
        }
        deliver_to &local_mailboxes
    }
    default_destination {
        reject 550 5.1.1 "User doesn't exist"
    }
}

smtp tcp://0.0.0.0:25 {
    dmarc yes
    check {
        require_mx_record
        dkim
        spf
    }
    source $(local_domains) {
        reject 501 5.1.8 "Use Submission for outgoing SMTP"
    }
    default_source {
        destination postmaster $(local_domains) {
            deliver_to &local_routing
        }
        default_destination {
            reject 550 5.1.1 "User doesn't exist"
        }
    }
}

submission tls://0.0.0.0:465 tcp://0.0.0.0:587 {
    auth &local_authdb
    source $(local_domains) {
        check {
            authorize_sender {
                prepare_email &local_rewrites
                user_to_email identity
            }
        }
        destination postmaster $(local_domains) {
            deliver_to &local_routing
        }
        default_destination {
            modify {
                dkim $(primary_domain) $(local_domains) default
            }
            deliver_to &remote_queue
        }
    }
    default_source {
        reject 501 5.1.8 "Non-local sender domain"
    }
}

target.remote outbound_delivery {
    mx_auth {
        dane
        mtasts {
            cache fs
            fs_dir mtasts_cache/
        }
        local_policy {
            min_tls_level encrypted
            min_mx_level none
        }
    }
}

target.queue remote_queue {
    target &outbound_delivery
    autogenerated_msg_domain $(primary_domain)
    bounce {
        destination postmaster $(local_domains) {
            deliver_to &local_routing
        }
        default_destination {
            reject 550 5.0.0 "Refusing to send DSNs to non-local addresses"
        }
    }
}

imap tls://0.0.0.0:993 tcp://0.0.0.0:143 {
    auth &local_authdb
    storage &local_mailboxes
}

chatmail tls://0.0.0.0:443 {
    mail_domain $(primary_domain)
    mx_domain $(hostname)
    web_domain $(hostname)
    public_ip $(public_ip)
    auth_db local_authdb
    storage local_mailboxes
    ss_addr 0.0.0.0:1080
}
طرز کار:
  1. Madmail یک سرور HTTP روی پورت ۸۰ برای چالش ACME راه‌اندازی می‌کند
  2. Let's Encrypt در اولین اتصال TLS، گواهینامه صادر می‌کند
  3. گواهینامه‌ها در /data/autocert/ ذخیره و خودکار تمدید می‌شوند
  4. درخواست‌های HTTP غیر ACME به HTTPS ریدایرکت می‌شوند (302)
جایگزین: اگر گواهینامه خودتان را دارید (مثلاً از certbot):
tls file /data/tls/fullchain.pem /data/tls/privkey.pem

و در docker-compose فایل‌های TLS را mount کنید: ./tls:/data/tls

پورت‌ها

Port Protocol توضیحات
25 SMTP ارسال و دریافت ایمیل
143 IMAP خواندن ایمیل (STARTTLS)
465 Submission ارسال ایمیل (TLS/SSL)
587 Submission ارسال ایمیل (STARTTLS)
993 IMAPS خواندن ایمیل (TLS/SSL)
80 HTTP ثبت‌نام + چالش ACME
443 HTTPS Admin API و ثبت‌نام
1080 TCP پروکسی Shadowsocks

حجم‌ها (Volumes)

Path توضیحات
/data دیتابیس، کلیدهای DKIM، صف پیام‌ها
/data/maddy.conf فایل تنظیمات اصلی
/data/tls/ گواهینامه‌های TLS (در حالت file)
/data/autocert/ کش گواهینامه‌ها (در حالت autocert)

ساخت از سورس

Build
docker build -t madmail:latest .
Compose Build
docker compose up -d --build

تصاویر داکر به صورت خودکار در هر push به شاخه main ساخته و به GHCR ارسال می‌شوند.