# 自建轻量邮件服务器？先了解传输原理！

## 邮件传输原理

{{< mermaid >}}sequenceDiagram
    participant User1;
    participant MUA1 as Mail User Agent 1;
    participant MTA1 as Mail Transfer Agent 1;
    participant DNS;
    participant MTA2 as Mail Transfer Agent 2;
    participant MDA as Mail Delivery Agent;
    participant MRA as Mail Retrieval Agent;
    participant MUA2 as Mail User Agent 2;
    participant User2;

    User1->>MUA1: 写一封邮件;
    MUA1->>+MTA1: 通过SMTP(s)发送邮件;
    MTA1->>+MTA1: 检查发信地址是否合法;
    MTA1->>+DNS: 查询收信人域名MX记录;
    DNS-->>-MTA1: 响应;
    MTA1->>+MTA2: 邮件转递;
    MTA2->>+DNS: 查询发信人域名MX/TXT记录;
    DNS-->>-MTA2: 响应;
    Note over MTA1,MTA2: 验证发信人MX/DKIM/SPF/DMARC/PTR;
    rect RGB(200,200,200);
      MTA2->>+MDA: 在收信人邮箱中存储邮件;
      MDA-->-MTA2: 确认接收邮件;
    end;
    MTA2-->>-MTA1: 确认邮件已接收;
    MTA1-->>MUA1: 邮件已发送;
    MUA1->>+MUA1: 存到已发信箱;
    MUA1-->>User1: 发送成功;
    rect RGB(200,200,200);
      MUA2->>+MRA: 访问邮箱下载邮件;
      MRA->>+MDA: 访问邮箱以检索邮件内容;
      MDA-->>-MRA: 响应邮件内容;
      MRA-->>-MUA2: 返回邮件;
      User2-)MUA2: 查看邮件;
    end;
{{< /mermaid >}}


注：
- 灰色部分表示对方服务器
- 仅供参考，有任何错误欢迎评论区讨论！

### 基本定义

- MUA（Mail User Agent）是邮件用户代理，也就是用户用来发送和接收邮件的软件。MUA通过IMAP或POP3协议与MRA通信
- MTA（Mail Transfer Agent）是邮件传输代理，也就是邮件服务器上的软件，负责通过SMTP协议发送和转发邮件。MTA会根据收信人地址中的域名查询对应的邮件服务器，并将邮件投递给它
- MDA（Mail Delivery Agent）是邮件投递代理，也就是负责将MTA接收到的邮件保存到本地磁盘或指定地方的软件。MDA通常会进行垃圾邮件和病毒扫描，并提供邮件过滤和自动回复等功能
- MRA（Mail Receive Agent）是邮件接收代理，负责实现IMAP和POP3协议，与MUA进行交互。MRA可以帮助用户从邮件服务器上下载或查看邮件
- SMTP（Simple Mail Transfer Protocol）是一种用于电子邮件传输的标准协议。它定义了如何在互联网上传输和传递电子邮件。SMTP是一个客户端/服务器协议，它使用TCP作为传输层协议，并使用25端口作为默认端口，ESMTP通过465/587端口
- IMAP（Internet Message Access Protocol）是一种用于电子邮件存储和访问的标准协议。IMAP协议通常使用143/993端口
- POP3（Post Office Protocol version 3）是一种用于电子邮件下载的标准协议，它允许用户通过邮件客户端从邮件服务器上下载邮件到本地计算机。它使用110端口进行传输，与SMTP协议配合使用。POP3协议的重点在于下载，不支持在服务器上管理邮件

常用的MUA有：Outlook、Apple Mail、Mutt、Fairemail

常用的MTA有：Sendmail、Postfix

常用的MDA有：procmail、dropmail、Cyrus

常用的MRA有：dovecot、Fetchmail、Getmail

### 传输过程

邮件传输的过程可以分为以下几个步骤：

本过程将模拟 "小明" `<ming@qq.com>`发送至 "小红" `<hong@gmail.com>`

#### 1. 发送

小明通过MUA编写邮件，并指定`hong@gmail.com`，点击发送，MUA使用SMTP(s)将邮件发送到`smtp.qq.com`的`25/465/587`端口

#### 2. 传递

`smtp.qq.com`检查发信地址是否属于该账号，如果属于，则继续投递。`smtp.qq.com`使用DNS查询`hong@gmail.com`对应域的MX记录[^1]。查询成功后，将邮件转发给`gmail-smtp-in.l.google.com`。

#### 3. 接收

该过程最复杂，因为邮件发送并无验证身份的机制，于是后续为了防止垃圾/钓鱼/假冒诈骗邮件的传递，接收方MTA会进行各种检查来确保邮件可信，MDA也会在MTA确认收信后进行扫描病毒等处理过程。如果检查未通过，重则直接退信，轻则归入垃圾邮箱。

在接收邮件时，Gmail会检查DKIM/SPF/DMARC等记录以及PTR记录，以确保邮件的发送者是真实的、可信的，并且该邮件未被篡改。

{{< admonition tip "DNSSEC" true >}}
后续检查基本基于DNS系统，因此域名最好支持DNSSEC以防记录被篡改
{{< /admonition >}}

具体来说，收信方MTA在接收邮件时，会按照以下流程进行验证：

1.  DKIM验证：检查邮件中是否包含了签名过的DKIM头部字段。如果有，Gmail会使用公钥对消息进行验证，以确保该邮件是由相应域名的私钥签名的，防止邮件被篡改
2.  SPF验证：检查邮件的发送服务器是否在发送域名的SPF记录中列出。如果不在，就会认为这封邮件可能是伪造的垃圾/钓鱼邮件
3.  DMARC验证：检查邮件是否符合发送域名的DMARC策略。DMARC可以指定如何处理没有通过DKIM或SPF验证的邮件。例如，可以要求将这些邮件标记为垃圾邮件或拒绝投递
4.  PTR验证：检查邮件所在的IP地址是否与发送邮件的域名的PTR记录相匹配。PTR记录通常用于反向DNS查询，可用于验证邮件发送方的身份

通过上述验证，收信方MTA可以判断邮件是否来自可信的发信人，并且可以有效地防止垃圾邮件、诈骗

#### 4. 存储

`gmail-smtp-in.l.google.com`确认接收邮件后，MDA进行处理，例如扫描/自动回复等。然后将其存储在`hong@gmail.com`的邮箱中

#### 5. 查看

如果小红用的是支持Push的MUA(例如Gmail客户端)，那么现在应该已经收到了邮件提醒，打开后即可看到小明发过来的邮件

~~请原谅我用小明小红，咱就是个起名废~~

## 自建邮件服务器

一般来讲，现在自建邮件服如果不需要图形化界面和其他附加功能，只需要最基本的发送、接受、阻止垃圾邮件，没必要去使用那些过于冗杂的[邮件服务器完全解决方案](https://github.com/awesome-selfhosted/awesome-selfhosted#communication---email---complete-solutions)，例如iRedmail

本部分使用[docker-mailserver](https://docker-mailserver.github.io/docker-mailserver/latest)作为例子来搭建邮件服，它包含了`postfix`, `dovecot`, `SpamAssian`, `OpenDKIM`, `OpenDMARC`, `Fail2Ban`等服务，并简单配置为了开箱即用，基本上半小时就可以打造一个功能完备的邮件服务器

### docker-mailserver

#### 准备服务器

需要满足几个条件

- ip和服务器域名未被列入黑名单(可通过[MX Super Tool](https://mxtoolbox.com/SuperTool.aspx)检查黑名单)
- 开放25端口
- 可配置rDNS/PTR记录(可选，尽量)
- 纯净系统，安装时将hostname主机名填为邮件服务器域名，如`mail.example.org`

如果只是用docker-mailserver配置简单的邮件服，使用人数少，那配置无需太高，1c 512m即可

当然，要安装docker及docker-compose

#### 配置

如果你的邮件服务器准备于二级域名下，如`mail.example.org`指向它，那将`hostname`配置为子域名，`domainname`配置为apex域[^2]即可。

{{< admonition tip "此处并非填写发信域" true >}}
此处仅配置邮件服务器的所在域，例如配置为`mail.example.org`，那后面小节中的发信域mx记录就指向它
{{< /admonition >}}

如果直接用apex域，那就将`hostname`填为`example.org`，删掉`domainname`

```docker-compose
version: '3.3'
services:
  mailserver:
    image: docker.io/mailserver/docker-mailserver:latest
    container_name: mailserver
    hostname: mail
    domainname: example.org
    env_file: mailserver.env
    environment:
      - SSL_TYPE=letsencrypt
    ports:
      - "25:25"    # SMTP  (explicit TLS => STARTTLS)
      - "143:143"  # IMAP4 (explicit TLS => STARTTLS)
      - "465:465"  # ESMTP (implicit TLS)
      - "587:587"  # ESMTP (explicit TLS => STARTTLS)
      - "993:993"  # IMAP4 (implicit TLS)
    volumes:
      - ./docker-data/dms/mail-data/:/var/mail/
      - ./docker-data/dms/mail-state/:/var/mail-state/
      - ./docker-data/dms/mail-logs/:/var/log/mail/
      - ./docker-data/dms/config/:/tmp/docker-mailserver/
      - /etc/localtime:/etc/localtime:ro
      - /etc/letsencrypt:/etc/letsencrypt
    restart: always
    stop_grace_period: 1m
    cap_add:
      - NET_ADMIN
    healthcheck:
      test: "ss --listening --tcp | grep -P 'LISTEN.+:smtp' || exit 1"
      timeout: 3s
      retries: 0
```

#### [环境变量](https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/)

以下是我自用的环境变量，使用前请修改所有`example.org`占位符为你自己准备配置的主发信域名

还有其它一些选项可配置，请看[文档](https://docker-mailserver.github.io/docker-mailserver/latest/config/environment/)了解相关环境变量含义

强烈建议使用certbot申请证书！上一节的`docker-compose.yml`已经配置为兼容certbot与letsencrypt，只需在容器外用certbot申请一个邮件服务器域名的SSL证书即可使用

更多SSL配置方式请看[文档](https://docker-mailserver.github.io/docker-mailserver/latest/config/security/ssl)

```
OVERRIDE_HOSTNAME=
DMS_DEBUG=0
LOG_LEVEL=info
SUPERVISOR_LOGLEVEL=info
ONE_DIR=1
ACCOUNT_PROVISIONER=
POSTMASTER_ADDRESS=postmaster@example.org
ENABLE_UPDATE_CHECK=1
UPDATE_CHECK_INTERVAL=1d
PERMIT_DOCKER=none
TZ=Asia/Shanghai
NETWORK_INTERFACE=
TLS_LEVEL=
SPOOF_PROTECTION=1
ENABLE_SRS=0
ENABLE_OPENDKIM=1
ENABLE_OPENDMARC=1
ENABLE_POP3=
ENABLE_CLAMAV=0
ENABLE_RSPAMD=1
ENABLE_RSPAMD_REDIS=
ENABLE_AMAVIS=1
AMAVIS_LOGLEVEL=1
ENABLE_DNSBL=1
ENABLE_FAIL2BAN=1
FAIL2BAN_BLOCKTYPE=drop
ENABLE_MANAGESIEVE=
POSTSCREEN_ACTION=enforce
SMTP_ONLY=
SSL_TYPE=
SSL_CERT_PATH=
SSL_KEY_PATH=
SSL_ALT_CERT_PATH=
SSL_ALT_KEY_PATH=
VIRUSMAILS_DELETE_DELAY=
POSTFIX_DAGENT=
POSTFIX_MAILBOX_SIZE_LIMIT=
ENABLE_QUOTAS=1
POSTFIX_MESSAGE_SIZE_LIMIT=
CLAMAV_MESSAGE_SIZE_LIMIT=
PFLOGSUMM_TRIGGER=
PFLOGSUMM_RECIPIENT=
PFLOGSUMM_SENDER=
LOGWATCH_INTERVAL=weekly
LOGWATCH_RECIPIENT=
LOGWATCH_SENDER=
REPORT_RECIPIENT=postmaster@example.org
REPORT_SENDER=
LOGROTATE_INTERVAL=weekly
POSTFIX_INET_PROTOCOLS=all
DOVECOT_INET_PROTOCOLS=all
ENABLE_SPAMASSASSIN=1
SPAMASSASSIN_SPAM_TO_INBOX=1
ENABLE_SPAMASSASSIN_KAM=1
MOVE_SPAM_TO_JUNK=1
SA_TAG=-100000.0
SA_TAG2=5.0
SA_KILL=15.0
SA_SPAM_SUBJECT=***SPAM*****
ENABLE_FETCHMAIL=0
FETCHMAIL_POLL=300
ENABLE_LDAP=
LDAP_START_TLS=
LDAP_SERVER_HOST=
LDAP_SEARCH_BASE=
LDAP_BIND_DN=
LDAP_BIND_PW=
LDAP_QUERY_FILTER_USER=
LDAP_QUERY_FILTER_GROUP=
LDAP_QUERY_FILTER_ALIAS=
LDAP_QUERY_FILTER_DOMAIN=
DOVECOT_TLS=
DOVECOT_USER_FILTER=
DOVECOT_PASS_FILTER=
DOVECOT_MAILBOX_FORMAT=maildir
DOVECOT_AUTH_BIND=
ENABLE_POSTGREY=0
POSTGREY_DELAY=300
POSTGREY_MAX_AGE=35
POSTGREY_TEXT="Delayed by Postgrey"
POSTGREY_AUTO_WHITELIST_CLIENTS=5
ENABLE_SASLAUTHD=0
SASLAUTHD_MECHANISMS=
SASLAUTHD_MECH_OPTIONS=
SASLAUTHD_LDAP_SERVER=
SASLAUTHD_LDAP_BIND_DN=
SASLAUTHD_LDAP_PASSWORD=
SASLAUTHD_LDAP_SEARCH_BASE=
SASLAUTHD_LDAP_FILTER=
SASLAUTHD_LDAP_START_TLS=
SASLAUTHD_LDAP_TLS_CHECK_PEER=
SASLAUTHD_LDAP_TLS_CACERT_FILE=
SASLAUTHD_LDAP_TLS_CACERT_DIR=
SASLAUTHD_LDAP_PASSWORD_ATTR=
SASLAUTHD_LDAP_AUTH_METHOD=
SASLAUTHD_LDAP_MECH=
SRS_SENDER_CLASSES=envelope_sender
SRS_EXCLUDE_DOMAINS=
SRS_SECRET=
DEFAULT_RELAY_HOST=
RELAY_HOST=
RELAY_PORT=25
RELAY_USER=
RELAY_PASSWORD=
```

#### 启动&查看日志

启动

`docker-compose up -d`

查看实时日志

`docker-compose logs -f`

#### 管理

您最好的朋友！Setup.sh！

```sh
wget https://raw.githubusercontent.com/docker-mailserver/docker-mailserver/master/setup.sh
chmod a+x ./setup.sh
```

下载`setup.sh`后，可以直接运行以获得帮助信息

由于`docker-mailserver`使用虚拟域，所以任何域名的配置都会在添加相关账号后才会生成

例如使用`./setup.sh email add admin@example.org "<password>"`后，`example.org`的配置和存储位置便会生成

#### 登录

选择任何一个MUA：
```
IMAP:
    host: mail.example.org
    port: 993
    encryption: tls
    username: admin@example.org
    password: <password>
SMTP:
    host: mail.example.org
    port: 465
    encryption: tls
    username: admin@example.org
    password: <password>
```

按照这样填写参数登录即可

### 配置dns记录

接下来，请转到你的域名服务提供商。这里假设已经配置好邮件服务器域名为`mail.example.org`，且已将A记录指向服务器。发信域名为`example.org`

请确保域名已配置好DNSSEC

#### MX

该记录用于声明为域处理邮件的服务器

| 类型 | 名称 | 内容 | 优先级 | TTL |
| ------- | ------- | ------- | ------- | ------- |
| MX | @ | `mail.example.org` | 10 | 1小时 |

#### SPF

SPF（Sender Policy Framework）用于防止邮件伪造，也就是防止垃圾邮件。可以指定哪些IP地址有权代表该域名发送电子邮件。

SPF记录语法如下：

```
v=spf1 a mx ip4:192.0.2.0/24 ip6:2001:0db8::/32 include:example.org ~all
```

- a：该域名的A记录所指向的IP地址允许代表该域名发信
- mx：该域名的MX记录所指向的IP地址允许代表该域名发信
- ip4/ip6：允许该网段ip发信
- include:example.com：example.com域名的SPF记录中包含的IP地址也可以代表该域名发送电子邮件
- ~all：表示除了上述机制之外的所有IP地址都可以尝试代表该域名发送电子邮件，但是会被标记为垃圾邮件。如果将此处改为`-all`，那除了前面被允许的地址外，任何地址都不可以代表该域名发信。建议使用`-all`

建议配置为：

| 类型 | 名称 | 内容 | TTL |
| ------- | ------- | ------- | ------- |
| TXT | @ | `v=spf1 mx -all` | 1小时 |

#### DMARC

DMARC（Domain-based Message Authentication, Reporting & Conformance）基于SPF和DKIM，可以让邮件接收方验证发件人的身份，并向域名所有者提供有关发件人身份验证结果的报告

当邮件接收方收到一封电子邮件时，它会通过SPF和DKIM技术验证发件人的身份。如果发件人的电子邮件地址与SPF记录和DKIM签名匹配，则认为该邮件是合法的；否则就认为该邮件可能是垃圾邮件或欺诈邮件。如果域名所有者启用了DMARC，邮件接收方还会将验证结果报告给域名所有者，以便其进一步分析和处理

DMARC 记录语法如下：

```
v=DMARC1; p=quarantine; sp=none; rua=mailto:postmaster@example.org; ruf=mailto:postmaster@example.org;
```

- p: 用于指定如何处理邮件，有三种取值：none 表示只报告，不采取任何动作，quarantine 表示将邮件放入收件人的垃圾邮件文件夹中，reject 表示直接拒绝邮件。
- sp: 用于指定子域名的处理方式，取值同 p
- rua: 指定接收邮件验证报告的地址，可以是多个邮箱地址，用逗号分隔。
- ruf: 指定接收邮件处理报告地址，其它同上

建议配置为：

| 类型 | 名称 | 内容 | TTL |
| ------- | ------- | ------- | ------- |
| TXT | _dmarc | `v=DMARC1; p=quarantine; sp=quarantine; rua=mailto:postmaster@example.org; ruf=mailto:postmaster@example.org;` | 1小时 |

#### DKIM

首先，通过`./setup.sh config dkim domain "<domain>"`生成发信域名的DKIM公/私钥

接着，查看`docker-data/dms/config/opendkim/keys/<domain>/<selector>.txt`即可找到需要发布到DNS的DKIM公钥，形如`<selector>._domainkey IN      TXT     ( "v=DKIM1; h=sha256; k=rsa; "    "p=xxxxxxx"  "xxxxxxx"   "xxxxxxx" )  ; ----- DKIM key mail for example.org`

使用时需去掉引号与空格，将引号内内容发布至dns，示例如下：

| 类型 | 名称 | 内容 | TTL |
| ------- | ------- | ------- | ------- |
| TXT | `<selector>._domainkey` | `v=DKIM1; h=sha256; k=rsa; p=xxxxxxxxxxxxxxxxxxxxx` | 1小时 |

#### 高级: 多域名使用

由于使用虚拟域，`docker-mailserver`不存在主域名的说法，任何添加了账号的域名都可被使用来发信，因此请谨慎添加账号，以防发送假冒邮件被退信导致域名/ip进黑名单

按照上文，已经配置好`example.org`，该如何添加第二个`test.com`？

只需要按照从"使用setup.sh添加账号"这个步骤起，将发信域名全换为准备添加的第二个域名，重新走一边流程，就可以正常发信/收信了

多个域名同理

## 优化

### PTR/rDNS

PTR 记录提供 IP 地址和域名之间的映射关系

在邮件传输过程中，邮件服务器会根据 IP 地址来确定邮件发送方的域名。如果邮件服务器收到了一封来自未知 IP 地址的邮件，它会尝试使用 PTR 记录查找该 IP 地址对应的域名。如果查找成功，并且该域名与邮件中的发件人域名匹配，那么该邮件就被认为是合法的

例如，iCloud Mail某一台mx服务器的ip地址为`17.42.251.62`，那么`dig -x 17.42.251.62`即可反查到该ip指向的域名

### 附加：解除黑名单/白名单

[DNSWL](https://www.dnswl.org/)是一个不错的ip地址库，可以将域名与其匹配的ip认证为白名单

[SpamHaus](https://check.spamhaus.org/)也可用于检查是否为黑名单，并提供自助申诉

## 检查

[Mail Tester](https://www.mail-tester.com/)可以非常方便地看出得分，10分基本上就不可能被放进垃圾邮箱里了

![得瑟一下](https://thumbnail.moraxyc.com/blog/mailserver/10.jpg)


[^1]: 该记录对于大型邮件服务提供商而言，一般会有多个MTA，这只是优先级最高的那个。例如通过`dig MX gmail.com`可以看到gmail具有五个MX记录`alt4.gmail-smtp-in.l.google.com`,`gmail-smtp-in.l.google.com`,`alt3.gmail-smtp-in.l.google.com`,`alt2.gmail-smtp-in.l.google.com`,`alt1.gmail-smtp-in.l.google.com`

[^2]: Apex域是指域名中的最高层级，不包括任何子域名。例如，在`www.example.org`域中，`example.org`是apex域

