Maddy 是一款用 Go 语言开发的邮件服务器,部署方便,资源占用少,是一款适合个人使用的邮箱服务器。

在我的 CloudCone 1C1G 机 docker 搭建后查看容器,CPU 使用 0.03%,内存使用 20M,所以 1C1G 跑起来毫无压力。

准备

  • 一台 VPS,支持开启 25 端口和 rDNS
  • 一个域名,支持管理 A/AAAA 记录、MX 记录、TXT 记录等
  • 邮箱域名的证书和密钥(使用 Certbot 申请)

安装 docker

docker-compose.yml 的配置如下

version: "3.8"
services:
  maddy:
    image: foxcpp/maddy:latest
    container_name: maddy
    ports:
      - 25:25
      - 143:143
      - 465:465
      - 993:993
    volumes:
      - /opt/docker/data/maddy:/data
      - /etc/letsencrypt/live/mail.example.com/fullchain.pem:/data/local_certs/cert.pem:ro
      - /etc/letsencrypt/live/mail.example.com/privkey.pem:/data/local_certs/key.pem:ro
    environment:
      - TZ=Asia/Shanghai
      - MADDY_HOSTNAME=mail.example.com
      - MADDY_DOMAIN=example.com

启动后开放的端口及作用

  • 25 SMTP Transfer->Exchange ClearText
  • 465 SMTPs User->Submission TLS
  • 143 IMAP Delivery->User ClearText
  • 993 IMAPs Delivery->User TLS
  • 587 ESMTP User->Submission ClearText/STARTTLS

获取 tls 证书

安装 Certbot

# Ubuntu/Debian
sudo apt-get install certbot
# Centos
sudo yum install certbot

获取 tls 证书:

sudo certbot certonly --standalone -d mail.example.com

按照提示操作,将获得证书和私钥,存储在 /etc/letsencrypt/live/mail.example.com/ 目录下。

配置 maddy

# 预设了三个变量,方便后续使用
$(hostname) = mail.example.com  # 外界通过这个域名找到你的邮件服务器
$(primary_domain) = example.com # 你的邮箱 @ 后面的域名
$(local_domains) = $(primary_domain)

# 如果要使用 nginx 反代,这里可以选择 tls off,但如此一来没法生成 dkim 密钥对
# 在之后检查时日志内会有安全警告,故推荐直接用 maddy 管理
# tls off
# tls file /etc/letsencrypt/live/$(local_domains)/fullchain.pem /etc/letsencrypt/live/$(local_domains)/privkey.pem
tls file /data/local_certs/cert.pem /data/local_certs/key.pem 

# 数据用 SQLite3 存储较为简单、轻量
auth.pass_table local_authdb {
    table sql_table {
        driver sqlite3
        dsn credentials.db
        table_name passwords
    }
}

storage.imapsql local_mailboxes {
    driver sqlite3
    dsn imapsql.db
}

# ----------------------------------------------------------------------------
# SMTP endpoints + message routing
hostname $(hostname)
table.chain local_rewrites {
    optional_step regexp "(.+)\+(.+)@(.+)" "$1@$3"
    optional_step static {
        entry postmaster postmaster@$(primary_domain)
    }
    optional_step file /data/aliases
}

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 使用 25 号端口发送邮件
smtp tcp://[::]:25 {
    # tls self_signed
    limits {
        # Up to 20 msgs/sec across max. 10 SMTP connections
        all rate 20 1s
        all concurrency 10
    }
    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"
        }
    }
}

# 如果使用 nginx 反代则这里监听到本地端口即可 tcp://127.0.0.1:587
# 不使用则邮件客户端以 SSL/TLS 方式直接访问该地址
submission tls://[::]:465 {
    limits {
        # Up to 50 msgs/sec across any amount of SMTP connections
        all rate 50 1s
    }
    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 {
    limits {
        # Up to 20 msgs/sec across max. 10 SMTP connections
        # for each recipient domain.
        destination rate 20 1s
        destination concurrency 10
    }
    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 endpoints
# 同上,使用 nginx 反代则改为监听本地端口 tcp://127.0.0.1:143
imap tls://[::]:993 {
    auth &local_authdb
    storage &local_mailboxes
}

启动 maddy

docker compose up -d

创建 maddy 用户

# 进入容器
docker exec -it maddy /bin/sh
# 创建邮箱
maddy creds create [email protected]
maddy imap-acct create [email protected]

获取 DKIM 的公钥

/ # cat data/dkim_keys/*.dns
v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG...zAuhVJKPYUPTnJ3yTIzi4R2XbzfwIDAQAB

复制保存,将在设置 DNS 记录使用

设置 DNS 记录

  • A 记录,将邮箱域名(mail.example.com)指向你的服务器 IP 地址
  • MX(Mail Exchange)记录指向你的邮件服务器地址,确保可以接收邮件。将邮件域([email protected],其中 example.com 就代表邮件域)指向第一步的邮箱域名,优先级默认为 10(数字越低,优先级越高)
  • PTR 记录(或者叫 rDNS),可以通过 ip 地址反向解析出邮箱域名。不设置也没关系,但是会被某些服务器标记为垃圾邮件
  • SPF(Sender Policy Framework)是用来指定哪些服务器有权限代表你的域名发送邮件
  • DMARC(Domain-based Message Authentication, Reporting, and Conformance) 用于指定邮件的处理策略,以及接收未通过验证的邮件报告
  • DKIM(DomainKeys Identified Mail) 用于对邮件进行数字签名,验证邮件是否被篡改

DNS 验证

# install dig
apt-get install dnsutils
# 验证 MX 记录
$ dig +short MX example.com
mail.example.com
# 验证 A 记录
$ dig +short A mail.example.com
71.2.3.1
# 验证 rDNS(PTR) 记录
$ dig +short -x 71.2.3.1
mail.example.com

mail.example.com 为邮件服务器的域名为例

序号 名称 类型 内容 说明
1 mail A 71.2.3.1 A
2 mail AAAA 2607:b102::e
3 smtp CNAME mail.example.com CNAME
4 imap CNAME mail.example.com
5 @ MX mail.example.com MX
6 @ TXT v=spf1 mx ~all SPF
7 mail TXT v=spf1 mx ~all
8 _dmarc TXT v=DMARC1; p=quarantine; ruf=mailto:[email protected] DMARC
9 default._domainkey TXT v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG...zAuhVJKPYUPTnJ3yTIzi4R2XbzfwIDAQAB DKIM

最终,我在 cloudflare 新添加的 DNS 记录如下

测试检查

测试自建邮件服务器 mail.example.com,邮箱 [email protected] 收发邮件情况。

我们使用 Mailspring —— 一个支持 Mac、Linux 和 Windows 的、开源的,邮件客户端来操作。

使用 [email protected] 连接后

  1. 往自己的 Gmail 邮箱发邮件,检查能否收到邮件。
  2. Gmail 收到邮件后,在 Gmail 邮箱后回复,检查在邮件客户端 Mailspring 能否收到邮件。

如上图,可以正常收发邮件。

最后,mail-tester 测试下得分 9.5,这是未针对内容调整,调整后估计分值可以加高。

在线工具

  • 垃圾邮件匹配度检测 https://www.mail-tester.com/?lang=zh
  • MxtoolBox,查询 MX 记录,DKIM 等用于检查是否配置正确 https://mxtoolbox.com/SuperTool.aspx