使用 Postfix, Dovecot, SQLite 搭建邮件服务器。
名次解释:
- FQDN:完全限定域名。完整域名由主机名称与母域名两部分所组成。
- rDNS:反向 DNS 解析。查询 DNS 来确定与 IP 地址关联的域名。
约定:
- 本文中所用域名为:example.com,FQDN 为 mail.example.com。
基本的准备
依次完成如下工作:
- 打开服务器相关端口:
- 25 (SMTP)
- 465 (SMTPS)
- 143 (IMAP)
- 993 (IMAPS)
- 设置一条 A 记录,使你的 FQDN 指向服务器 ip。
- 给你的域名设置一条 MX 记录。
- 给你的服务器设置反向 DNS 解析,使你的服务器 ip 指向你的 FQDN。
- 设置服务器主机名 (hostname) 为你的 FQDN。
获取域名 SSL 证书
使用 acme.sh 签发即可。下面这篇文章可能会有一点帮助。
利用 GitHub Action 实现 SSL 证书的自动续签
将证书文件放置在如下位置:
/opt/ssl_certificate/fullchain.pem
和 /opt/ssl_certificate/key.pem
安装软件包
包括 Postfix、Dovecot、SQLite3 以及一些辅助性的软件。安装完成后,备份配置文件。
1
2
3
4
5
| # apt install postfix postfix-sqlite -y
# apt install dovecot-imapd dovecot-lmtpd dovecot-sqlite -y
# apt install sqlite3 -y
# apt install postgrey postfix-policyd-spf-python opendkim opendkim-tools fail2ban -y
# tar -czvf backup.tar.gz /etc/postfix /etc/dovecot /etc/opendkim.conf
|
顺便新建用户与用户组 vmail
。
1
2
3
| # groupadd -g 5000 vmail
# useradd -g vmail -u 5000 vmail -d /var/mail -s /sbin/nologin
# chown -R vmail:vmail /var/mail
|
设置 DKIM记录并配置 opendkim 服务
编辑配置文件。
1
| # vim /etc/opendkim.conf
|
在配置文件中找到如下几行,取消这几行的注释,并将 simple
改为 relaxed/simple
。
1
2
3
| Canonicalization simple
Mode sv
SubDomains no
|
接着找到这一行 (#ADSPAction continue
),并将下面的内容添加到这行后面。如果没有找到这一行,只需要将下面的内容添加到这一行 (SubDomains no
) 后面即可。
1
2
3
4
5
| AutoRestart yes
AutoRestartRate 10/1M
Background yes
DNSTimeout 5
SignatureAlgorithm rsa-sha256
|
将如下内容添加到文件末尾。在 Ubuntu 18.04 上,UserID 已经设置为 opendkim
,无需重复添加。
1
2
3
4
5
6
7
8
9
10
11
12
13
| #OpenDKIM user
# Remember to add user postfix to group opendkim
UserID opendkim
# Map domains in From addresses to keys used to sign messages
KeyTable refile:/etc/opendkim/key.table
SigningTable refile:/etc/opendkim/signing.table
# Hosts to ignore when verifying signatures
ExternalIgnoreList /etc/opendkim/trusted.hosts
# A set of internal hosts whose mail should be signed
InternalHosts /etc/opendkim/trusted.hosts
|
然后找到这一行:
1
| Socket local:/var/run/opendkim/opendkim.sock
|
把它替换为:
1
| Socket local:/var/spool/postfix/opendkim/opendkim.sock
|
保存文件并退出 vim。
执行如下命令,创建所需文件夹并编辑配置文件。
1
2
| # mkdir -p /etc/opendkim/keys/example.com
# vim /etc/opendkim/signing.table
|
写入如下内容:
1
| *@example.com default._domainkey.example.com
|
保存。注意上述内容中的域名要替换为你要部署的真实域名,后续不再提示。
编辑文件。
1
| # vim /etc/opendkim/key.table
|
写入如下内容:
1
| default._domainkey.example.com example.com:default:/etc/opendkim/keys/example.com/default.private
|
保存。注意,上面的内容为一整行,而不是两行分开。
编辑文件。
1
| # vim /etc/opendkim/trusted.hosts
|
写入如下内容:
1
2
3
4
| 127.0.0.1
localhost
*.example.com
|
保存。
生成 opendkim key 并输出。
1
2
3
| # opendkim-genkey -b 2048 -d example.com -D /etc/opendkim/keys/example.com -s default -v
# chown -R opendkim:opendkim /etc/opendkim
# cat /etc/opendkim/keys/example.com/default.txt
|
将输出的 opendkim key 按如下格式设置域名的 TXT 记录。注意删除记录值中多余的双引号。
TXT default._domainkey v=DKIM1; K=rsa; p=xxxxxx;
使用如下命令测试是否配置成功。
1
| # opendkim-testkey -d example.com -s default -vvv
|
执行如下命令,使 Postfix 可以使用 opendkim 服务。
1
2
3
4
| # gpasswd -a postfix opendkim
# mkdir /var/spool/postfix/opendkim
# chown opendkim:postfix /var/spool/postfix/opendkim
# systemctl restart opendkim
|
设置 SPF 记录
按如下格式设置域名的 TXT 记录。
TXT @ v=spf1 mx ~all
设置 DMARC 记录
按如下格式设置域名的 TXT 记录。
TXT _dmarc v=DMARC1; p=none; pct=100; sp=none; adkim=r; aspf=r; fo=1; rua=mailto:your_[email protected]; ruf=mailto:your_[email protected];
配置拦截政策
白名单
1
| # vim /etc/postfix/rbl_override
|
1
2
| dripemail2.com OK //drip.com
mlsend.com OK //mailerlite email marketing service
|
1
| # postmap /etc/postfix/rbl_override
|
HELO/EHLO 主机名
1
| # vim /etc/postfix/helo_access
|
1
2
| optimus-webapi-prod-2.localdomain OK
va-massmail-02.rakutenmarketing.com OK
|
1
| # postmap /etc/postfix/helo_access
|
灰名单
1
| # vim /etc/default/postgrey
|
找到下面这一行:
1
| POSTGREY_OPTS="--inet=10023"
|
把它改为:
1
| POSTGREY_OPTS="--inet=127.0.0.1:10023 --delay=60"
|
然后重启 postgrey 服务。
1
| # systemctl restart postgrey
|
配置 Fail2ban 工具
fail2ban 能有效防止常见的洪水攻击。
编辑 postfix 规则。
1
| # vim /etc/fail2ban/jail.d/postfix.conf
|
内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
| [postfix]
enabled = true
logpath = %(syslog_mail)s
maxretry = 3
bantime = 1h
filter = postfix
[postfix-flood-attack]
enabled = true
logpath = %(syslog_mail)s
bantime = 1h
filter = postfix-flood-attack
port = smtp,smtps,imap,imaps,sieve
|
保存。然后编辑 dovecot 规则。
1
| # vim /etc/fail2ban/jail.d/dovecot.conf
|
内容如下:
1
2
3
4
5
6
| [dovecot]
enabled = true
logpath = %(syslog_mail)s
maxretry = 3
bantime = 1h
filter = dovecot
|
保存。然后编辑 postfix 过滤器。
1
| # vim /etc/fail2ban/filter.d/postfix-flood-attack.conf
|
内容如下:
1
2
3
4
5
6
7
8
9
10
| [INCLUDES]
before = common.conf
[Definition]
_daemon = postfix(-\w+)?/(?:submission/|smtps/)?smtp[ds]
failregex = ^%(__prefix_line)slost connection after AUTH from \S+\[<HOST>\]$
ignoreregex =
[Init]
journalmatch = _SYSTEMD_UNIT=postfix.service
|
保存。重启 fail2ban 服务。
1
| # systemctl restart fail2ban
|
创建数据库表
创建并调用 sqlite 数据库。
1
| # sqlite3 /etc/postfix/postfix.sqlite
|
调整格式化输出。
1
2
3
| sqlite> .header on
sqlite> .mode column
sqlite> .timer on
|
依次执行如下 SQL 语句,创建数据库表。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| CREATE TABLE domains (
id INTEGER PRIMARY KEY AUTOINCREMENT,
domain varchar(255) NOT NULL UNIQUE,
description varchar(255) NOT NULL default '',
active tinyint(1) NOT NULL default '1'
);
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username varchar(255) NOT NULL,
password varchar(255) NOT NULL,
domain_id INTEGER NOT NULL,
quota bigint(20) NOT NULL default '0',
created datetime NOT NULL default (datetime('now', 'localtime')),
active tinyint(1) NOT NULL default '1',
FOREIGN KEY(domain_id) REFERENCES domains(id)
);
CREATE TABLE aliases (
id INTEGER PRIMARY KEY AUTOINCREMENT,
domain_id int(11) NOT NULL,
source varchar(255) NOT NULL,
destination varchar(255) NOT NULL,
active tinyint(1) NOT NULL default '1',
FOREIGN KEY(domain_id) REFERENCES domains(id)
);
INSERT INTO domains (domain) VALUES ('example.com');
INSERT INTO users (username, password, domain_id) VALUES ('i', '{CRYPT}$2y$05$....', 1);
INSERT INTO aliases (domain_id, source, destination) VALUES (1, '[email protected]', '[email protected]');
|
注:邮箱用户密码可暂时使用如下命令生成:
1
| # doveadm pw -p "passwd"
|
配置 Postfix
编辑 /etc/postfix/main.cf
。
1
| # vim /etc/postfix/main.cf
|
替换为如下内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
| #----- General -----
myhostname = mail.example.com
myorigin = /etc/mailname
mydestination = localhost
relayhost =
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = all
inet_protocols = all
mynetworks_style = host
append_dot_mydomain = no
readme_directory = no
compatibility_level = 2
#----- SMTPD parameters -----
biff = no
#delay_warning_time = 4h
disable_vrfy_command = yes
strict_rfc821_envelopes = yes
#smtputf8_enable = no
smtpd_banner = $myhostname ESMTP $mail_name (Ubuntu)
smtpd_delay_reject = yes
smtpd_helo_required = yes
smtpd_timeout = 30s
smtpd_recipient_limit = 40
#smtpd_etrn_restrictions = reject
#smtpd_reject_unlisted_sender = yes
#smtpd_reject_unlisted_recipient = yes
#smtpd_hard_error_limit = 1
smtp_always_send_ehlo = yes
smtp_helo_timeout = 15s
smtp_rcpt_timeout = 15s
#maximal_queue_lifetime = 1d
#bounce_queue_lifetime = 1d
minimal_backoff_time = 180s
maximal_backoff_time = 3h
#----- TLS parameters -----
smtpd_use_tls = yes
smtpd_tls_auth_only = yes
smtpd_tls_cert_file = /opt/ssl_certificate/fullchain.pem
smtpd_tls_key_file = /opt/ssl_certificate/key.pem
smtpd_tls_ciphers = high
smtpd_tls_security_level = may
smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtpd_tls_loglevel = 0
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
smtpd_tls_received_header = yes
smtp_tls_security_level = may
smtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtp_tls_loglevel = 0
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
#----- SASL parameters -----
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
smtpd_sasl_security_options = noanonymous
#smtpd_sasl_local_domain =
#smtpd_sasl_authenticated_header = no
#broken_sasl_auth_clients = no
#----- Virtual domains -----
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
virtual_transport = lmtp:unix:private/dovecot-lmtp
virtual_mailbox_domains = sqlite:/etc/postfix/sqlite_mailbox_domains.cf
virtual_mailbox_maps = sqlite:/etc/postfix/sqlite_mailbox_maps.cf
virtual_alias_maps = sqlite:/etc/postfix/sqlite_alias_maps.cf
#virtual_uid_maps = static:5000
#virtual_gid_maps = static:5000
#----- Restrictions -----
smtpd_recipient_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
reject_non_fqdn_recipient,
reject_unknown_recipient_domain,
reject_unlisted_recipient,
reject_unauth_destination,
reject_unauth_pipelining,
check_policy_service unix:private/policyd-spf,
check_policy_service inet:127.0.0.1:10023,
check_client_access hash:/etc/postfix/rbl_override,
reject_rhsbl_helo dbl.spamhaus.org,
reject_rhsbl_reverse_client dbl.spamhaus.org,
reject_rhsbl_sender dbl.spamhaus.org
smtpd_sender_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
reject_sender_login_mismatch,
reject_non_fqdn_sender,
reject_unknown_sender_domain,
reject_unknown_reverse_client_hostname,
reject_unknown_client_hostname,
reject_unauth_pipelining
smtpd_helo_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
check_helo_access hash:/etc/postfix/helo_access,
reject_invalid_helo_hostname,
reject_non_fqdn_helo_hostname,
reject_unknown_helo_hostname
smtpd_relay_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
defer_unauth_destination,
check_helo_access hash:/etc/postfix/helo_access,
reject_invalid_helo_hostname,
reject_non_fqdn_helo_hostname,
reject_unknown_helo_hostname
smtpd_client_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
reject_unknown_client_hostname,
reject_rbl_client bl.spamcop.net,
reject_rbl_client zen.spamhaus.org,
reject_rbl_client blackholes.easynet.nl
smtpd_data_restrictions =
reject_unauth_pipelining
#----- policyd-spf -----
policyd-spf_time_limit = 3600
#----- Opendkim -----
milter_default_action = accept
milter_protocol = 6
smtpd_milters = local:/opendkim/opendkim.sock
non_smtpd_milters = $smtpd_milters
|
保存。然后编辑 /etc/postfix/master.cf
。
1
| # vim /etc/postfix/master.cf
|
取消 smtps
小节的注释,并调整为如下几行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| smtps inet n - y - - smtpd
-o syslog_name=postfix/smtps
-o smtpd_tls_security_level=encrypt
-o smtpd_tls_wrappermode=yes
-o smtpd_sasl_auth_enable=yes
-o smtpd_sasl_type=dovecot
-o smtpd_sasl_path=private/auth
-o smtpd_sasl_security_options=noanonymous
-o smtpd_sasl_local_domain=$myhostname
-o smtpd_reject_unlisted_recipient=no
-o smtpd_sender_login_maps=sqlite:/etc/postfix/sqlite_mailbox_maps.cf
-o smtpd_sender_restrictions=reject_sender_login_mismatch
-o smtpd_client_restrictions=permit_sasl_authenticated,reject
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
-o smtpd_recipient_restrictions=reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_mynetworks,permit_sasl_authenticated,reject
-o milter_macro_daemon_name=ORIGINATING
|
在文件末尾添加下面几行:
1
2
| policyd-spf unix - n n - 0 spawn
user=policyd-spf argv=/usr/bin/policyd-spf
|
保存。然后编辑 sqlite_mailbox_domains.cf
1
| # vim /etc/postfix/sqlite_mailbox_domains.cf
|
内容为:
1
2
3
| dbpath = /etc/postfix/postfix.sqlite
query = SELECT domain FROM domains WHERE domain='%s'
result_format = %s
|
保存。然后编辑 sqlite_mailbox_maps.cf
1
| # vim /etc/postfix/sqlite_mailbox_maps.cf
|
内容为:
1
2
3
| dbpath = /etc/postfix/postfix.sqlite
query = SELECT username||'@'|| FROM users INNER JOIN domains on users.domain_id=domains.id WHERE username='%u'
result_format = %s
|
保存。然后编辑 sqlite_alias_maps.cf
1
| # vim /etc/postfix/sqlite_alias_maps.cf
|
内容为:
1
2
3
| dbpath = /etc/postfix/postfix.sqlite
query = SELECT destination FROM aliases WHERE source = '%s'
result_format = %s
|
配置 Dovecot
编辑 /etc/dovecot/dovecot.conf
将 protocols = imap lmtp
添加到 # Enable installed protocols
的后面。
保存。然后编辑 /etc/dovecot/conf.d/10-auth.conf
修改文件内容为如下几行:
1
2
3
| disable_plaintext_auth = yes
auth_mechanisms = plain login
!include auth-sql.conf.ext
|
保存。然后编辑 /etc/dovecot/conf.d/10-mail.conf
修改文件内容为如下几行:
1
2
| mail_location = maildir:/var/mail/%d/%n/
mail_privileged_group = mail
|
保存。然后编辑 /etc/dovecot/conf.d/10-master.conf
编辑 service auth
块为:
1
2
3
4
5
6
7
8
9
10
11
12
| service auth {
unix_listener auth-userdb {
mode = 0600
user = vmail
group = vmail
}
unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
}
|
编辑 service lmtp
块为:
1
2
3
4
5
6
7
| service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
mode = 0600
user = postfix
group = postfix
}
}
|
编辑 service auth-worker
块为:
1
2
3
| service auth-worker {
#user = vmail
}
|
保存。然后编辑 /etc/dovecot/conf.d/10-ssl.conf
修改文件内容为如下几行:
1
2
3
4
| ssl = required
ssl_cert = </opt/ssl_certificate/fullchain.pem
ssl_key = </opt/ssl_certificate/key.pem
ssl_min_protocol = TLSv1.2
|
保存。然后编辑 /etc/dovecot/conf.d/auth-sql.conf.ext
修改文件内容为如下几行:
1
2
3
4
5
6
7
8
| passdb {
driver = sql
args = /etc/dovecot/dovecot-sql.conf.ext
}
userdb {
driver = static
args = uid=vmail gid=vmail home=/var/mail/%d/%n
}
|
保存。然后编辑 /etc/dovecot/dovecot-sql.conf.ext
修改文件内容为如下几行:
1
2
3
4
5
6
7
| driver = sqlite
connect = /etc/postfix/postfix.sqlite
default_pass_scheme = SHA512-CRYPT
password_query = \
SELECT username, domain, password \
FROM users INNER JOIN domains ON users.domain_id=domains.id \
WHERE username = '%n' AND domain = '%d'
|
测试
Newsletters spam test
参考
以上。