The Complete Postfix Architecture: From SMTP Handshake to Final Delivery
· 8 min read
Postfix 的 main.cf 中有大量 smtpd_*_restrictions、*_checks、*_maps、milter、transport 等參數。初次設定時,往往難以判斷各參數在郵件流程中於何處生效,以及彼此的先後順序。
本文完整說明 Postfix 從郵件進入到投遞完成的架構,涵蓋各道處理環節:postscreen、smtpd 各階段的 *_restrictions、Milter、cleanup 的位址改寫(canonical / virtual / masquerade)與 header_checks / body_checks、佇列系統、qmgr 與 trivial-rewrite 的路由決策,以至各類投遞代理(delivery agent)。
本文以官方文件(postfix.org)為準,文中預設值以 Postfix 3.10 分支(實測 3.10.5)為基準。
註:Postfix 不少預設值是「相容層級(
compatibility_level)」的條件式,例如smtpd_relay_restrictions、smtpd_relay_before_recipient_restrictions、append_dot_mydomain、relay_domains。postconf -d顯示的是內建預設(compatibility_level = 0)下的運算式;而全新安裝的main.cf會把compatibility_level設為當前版本(例如 3.10),實際生效值因此可能與內建預設不同。本文表格列的是「現代相容層級」下的生效值——升級後沿用舊層級的環境,請自行以postconf確認。
整體架構:由 master 監管的多個 daemon #
Postfix 並非單一程式,而是由十餘個各司其職、彼此獨立的 daemon 組成;這些 daemon 透過佇列檔案與 socket 溝通,並由常駐的 master daemon 統一監管。
master(8)is the supervisor that keeps an eye on the well-being of the Postfix mail system.
master 讀取 master.cf,依設定按需求啟動各 daemon(多數 daemon 為短生命週期,僅在有工作時啟動、閒置後結束),並在 daemon 異常結束時將其重新啟動,同時限制每個服務的最大 process 數。
下表列出本文涉及的主要 daemon 及其職責(取自 Postfix 預設 master.cf):
| Daemon | 角色 |
|---|---|
master | 總管,讀 master.cf、啟動與監控所有其他 daemon |
postscreen | 站在 smtpd 前面的輕量前哨,過濾殭屍/spambot 連線 |
smtpd | SMTP 伺服器,處理進站 SMTP 對話與各階段 *_restrictions |
qmqpd | QMQP 伺服器(另一種投信協定,較少用) |
pickup | 從 maildrop 佇列讀取本機投遞的信,交給 cleanup |
cleanup | 進佇列前的最後加工:補標頭、位址改寫、*_checks、非 SMTP 的 Milter |
trivial-rewrite | 把位址正規化,並為每個收件人解析出 transport:nexthop |
qmgr | 佇列管理員,郵件投遞的核心,排程並呼叫投遞代理 |
local | 本機投遞代理,處理 /etc/aliases 與 ~/.forward |
virtual | virtual mailbox 投遞代理(不需要 UNIX 帳號) |
lmtp | 用 LMTP 投遞給 mailbox server(如 Dovecot、Cyrus) |
smtp | SMTP 客戶端,負責寄往遠端(也用於 relay) |
pipe | 把信透過 pipe 交給外部程式 |
bounce / defer / trace | 產生退信/延遲通知與紀錄(同一支程式的三種身分) |
error / discard | 特殊「投遞代理」:一律退信 / 一律靜默丟棄 |
verify | 位址可投遞性驗證與快取 |
flush | 依需求強制刷新佇列(ETRN、postqueue -f) |
proxymap | 資料表查詢代理(讓 chroot 的 daemon 共用查表) |
anvil / scache | 連線速率限制 / SMTP 連線快取 |
tlsmgr | 管理 PRNG 與 TLS session cache |
showq | 提供 mailq / postqueue 的佇列狀態 |
以下為整體架構圖,後續各節將逐一說明:
%%{init: {"flowchart": {"wrappingWidth": 600}}}%%
flowchart TB
subgraph IN["① 郵件入口"]
direction LR
NET["網路 SMTP"] ~~~ LOC["本機投信<br/>sendmail → postdrop → pickup"] ~~~ INT["內部產生<br/>退信 / 通知 / 轉寄"]
end
subgraph SD["② smtpd:postscreen + 七道 *_restrictions"]
direction TB
subgraph SDr1[" "]
direction LR
PS["postscreen"] --> B1["smtpd_client_restrictions<br/>CONNECT"] --> B2["smtpd_helo_restrictions<br/>HELO / EHLO"] --> B3["smtpd_sender_restrictions<br/>MAIL FROM"] --> B4["smtpd_relay_restrictions<br/>RCPT TO・防 open relay"]
end
subgraph SDr2[" "]
direction LR
B5["smtpd_recipient_restrictions<br/>RCPT TO・反垃圾"] --> B6["smtpd_data_restrictions<br/>DATA"] --> B7["smtpd_end_of_data_restrictions<br/>結尾 ."] ~~~ MIL["+ smtpd_milters<br/>貫穿對話"]
end
B4 --> B5
end
style SDr1 fill:none,stroke:none
style SDr2 fill:none,stroke:none
subgraph CL["③ cleanup:進佇列前處理"]
direction LR
K1["補標頭<br/>always_add_missing_headers /<br/>local_header_rewrite_clients"] --> K2["canonical<br/>sender_canonical_maps →<br/>recipient_canonical_maps →<br/>canonical_maps"] --> K3["masquerade_domains"] --> K4["自動 BCC<br/>always_bcc /<br/>sender_bcc_maps /<br/>recipient_bcc_maps"] --> K5["virtual_alias_maps"] --> K6["header_checks /<br/>body_checks"] --> K6b["移除標頭<br/>message_drop_headers"] --> K7["non_smtpd_milters"]
end
subgraph RT["④ qmgr + trivial-rewrite:路由與排程"]
direction LR
Q1[("incoming 佇列")] --> Q2["trivial-rewrite<br/>transport_maps →<br/>address class → relayhost"] --> Q3[("active 佇列")]
end
subgraph DA["⑤ 投遞代理"]
direction TB
A1["local"] ~~~ A2["virtual"] ~~~ A3["lmtp"] ~~~ A4["smtp / relay"] ~~~ A5["pipe"] ~~~ A6["error / discard"]
end
IN --> SD --> CL --> RT --> DA
CL -.->|"HOLD"| HLD[("hold 佇列")]
RT -.->|"暫時失敗"| DEF[("deferred 重試")]
RT -.->|"永久失敗"| BNC["bounce 退信"]
DA -->|"成功"| SENT["status=sent"]
DA -->|"smtp"| MXR["遠端 MX / relayhost"]
DA -.->|"local:aliases/.forward 回流"| CL
整體架構有一個關鍵匯流點:無論由哪條路徑進入,所有郵件都必須先經過 cleanup 才能進入佇列。
郵件如何進入 Postfix #
依官方文件,郵件僅有四種進入 Postfix 的途徑,最終均匯流至 cleanup:
- 網路(SMTP / QMQP):外部連線由
smtpd(或qmqpd)接收,移除協定外殼並進行基本檢查後,將寄件者、收件人與郵件內容交給cleanup;postscreen可置於smtpd之前先行過濾連線。 - 本機投信:本機程式透過相容 sendmail 的
sendmail(1)指令投遞,由具權限的postdrop(1)將郵件寫入maildrop佇列,再由pickup讀出並檢查後交給cleanup。 - 內部產生的郵件:例如
bounce產生的退信、postmaster 通知等,直接交給cleanup。 - 轉寄與重新注入:
local依.forward或 aliases 轉寄的郵件,以及內容過濾器處理後重新注入的郵件,同樣直接交給cleanup。
Network mail enters Postfix via the smtpd(8) or qmqpd(8) servers. … give the sender, recipients and message content to the cleanup(8) server.
smtpd 的存取控制:postscreen 與 *_restrictions #
此為外部郵件遭遇的第一組、也是最關鍵的檢查。
須注意:本機投遞的郵件不經過 smtpd,因此 smtpd_*_restrictions 對其完全不生效;此類郵件的 Milter 亦非 smtpd_milters 而是 non_smtpd_milters(詳見〈內容過濾的四種位置〉)。若僅設定 SMTP 層的限制,將無法阻擋遭入侵的本機程式濫發郵件。
postscreen:smtpd 之前的前哨 #
postscreen 並非預設啟用;一旦啟用,它會置於 smtpd 之前,以單一 process 同時處理大量進站連線,判斷哪些客戶端可交由 smtpd 處理。其目的是攔截殭屍/spambot(此類客戶端常在未輪到時搶先送出指令,或忽略伺服器回應),將有限的 smtpd process 保留給正常客戶端。
須注意,postscreen 有獨立的一組 postscreen_* 參數,不共用 smtpd_*_restrictions,常見者包括 postscreen_access_list、postscreen_dnsbl_sites / postscreen_dnsbl_threshold / postscreen_dnsbl_action,以及 pregreet 測試相關的 postscreen_greet_action 等。通過篩選的客戶端會暫時加入允許快取(postscreen_cache_map),其後直接放行至 smtpd。
smtpd 的七道 restriction 檢查 #
連線進入 smtpd 後,SMTP 對話的每個階段各對應一組限制清單。下表依 SMTP 協定順序列出七道檢查及其在 Postfix 3.10.5 的預設值:
| # | SMTP 階段 | 參數 | 預設值 |
|---|---|---|---|
| 1 | CONNECT | smtpd_client_restrictions | 空 |
| 2 | HELO / EHLO | smtpd_helo_restrictions | 空 |
| 3 | MAIL FROM | smtpd_sender_restrictions | 空 |
| 4 | RCPT TO(relay 政策) | smtpd_relay_restrictions | permit_mynetworks, permit_sasl_authenticated, defer_unauth_destination |
| 5 | RCPT TO(反垃圾政策) | smtpd_recipient_restrictions | 空 |
| 6 | DATA | smtpd_data_restrictions | 空 |
| 7 | 結尾 . 之後 | smtpd_end_of_data_restrictions | 空 |
各清單的評估規則一致:
Each restriction list is evaluated from left to right until some restriction produces a result of PERMIT, REJECT or DEFER. The end of each list is equivalent to a PERMIT result.
亦即由左而右逐條評估,第一個產生 PERMIT / REJECT / DEFER 的規則即為結果並停止;產生 DUNNO(無結論)則續評下一條;整條清單評估完畢等同 PERMIT。因此順序至關重要——若將較寬鬆的 permit_* 置於 reject_unauth_destination 之前,可能使伺服器成為 open relay。
Note:smtpd_delay_reject 為何讓評估「順序」名不副實(預設 yes)
上表看似暗示「HELO 有問題即會在 HELO 階段被拒」,但預設 smtpd_delay_reject = yes,實際行為為:
Current Postfix versions postpone the evaluation of client, helo and sender restriction lists until the RCPT TO or ETRN command.
亦即在預設情況下,Postfix 會先收集 client / HELO / sender 的資訊,但將接受/拒絕的決定延後至 RCPT TO 一併處理。此設計有三項好處:拒絕訊息可同時包含 client、sender 與 recipient,便於除錯;避免與部分不合規客戶端形成「連線—被拒—重連」的迴圈;並可在同一份清單中混用 client/HELO/sender 規則。
以 smtpd_helo_restrictions = reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname 為例,使用非法的 HELO 名稱 -bad-helo- 送信,兩種設定下的行為明顯不同:
Scenario A:smtpd_delay_reject = yes(預設)
C: HELO -bad-helo-
S: 250 mail.example.com ← HELO 被接受
C: MAIL FROM:<[email protected]>
S: 250 2.1.0 Ok ← MAIL FROM 被接受
C: RCPT TO:<[email protected]>
S: 501 5.5.2 <-bad-helo->: Helo command rejected: Invalid name ← 至此才拒絕
Scenario B:smtpd_delay_reject = no
C: HELO -bad-helo-
S: 501 5.5.2 <-bad-helo->: Helo command rejected: Invalid name ← HELO 階段即拒絕
C: MAIL FROM:<[email protected]>
S: 503 5.5.1 Error: send HELO/EHLO first
同一組 HELO 限制,拒絕發生的階段完全不同。這也是規則「看似未生效」的常見原因——規則確已生效,只是延後至 RCPT TO 才回報。
Note:relay 與 recipient restrictions 的分工
第 4、5 道均於 RCPT TO 觸發,刻意區分兩種概念:smtpd_relay_restrictions(relay 政策)決定「允許透過本伺服器將郵件送往何處」,核心目的為防止 open relay;smtpd_recipient_restrictions(反垃圾政策)決定「是否接受這封郵件」。
smtpd_relay_restrictions 於 Postfix 2.10 加入作為安全防線;在此之前兩種政策皆置於 smtpd_recipient_restrictions,可能導致「寬鬆的反垃圾政策意外造成寬鬆的 relay 政策」。有了獨立的 relay 清單,即使反垃圾清單設定有誤,其預設值仍會擋下未授權的轉發。至於兩者的評估先後,由 smtpd_relay_before_recipient_restrictions 決定:compatibility_level ≥ 3.6 時預設 yes(relay 在前),升級後沿用較舊層級的環境則可能相反。
常用 restriction 關鍵字 #
| 類別 | 關鍵字 | 作用 |
|---|---|---|
| 放行 | permit_mynetworks / permit_sasl_authenticated / permit | 依網段 / 認證 / 無條件放行 |
| relay | reject_unauth_destination | 非授權目的地一律拒(防 open relay) |
| 通用 | reject / defer / warn_if_reject | 無條件拒 / 暫時拒 / 僅記錄而不實際拒絕(測試用) |
| DNS 檢查 | reject_unknown_sender_domain / reject_unknown_recipient_domain | 寄件/收件網域無法解析即拒(無 A/MX、malformed MX、Null MX 等) |
| client | reject_unknown_client_hostname / reject_rbl_client | 反解不符 / 命中 DNSBL 即拒 |
| HELO | reject_invalid_helo_hostname / reject_non_fqdn_helo_hostname | HELO 語法錯誤 / 非 FQDN 即拒 |
| 協定 | reject_unauth_pipelining | 客戶端搶先送出指令(spambot 特徵)即拒 |
| 查表 | check_client_access / check_sender_access / check_recipient_access | 查存取表,套用表中回傳的動作 |
| 表回傳 | OK / REJECT / DEFER / DISCARD / DUNNO | 存取表值欄可回傳的動作(DUNNO=無結論,續評下一條) |
SMTP 階段的 Milter #
在 smtpd 對話進行中,Postfix 亦會呼叫 smtpd_milters 指定的 Milter。其原則為:
Postfix inspects information first, then the first configured Milter, the second configured Milter, and so on.
Milter 為 Sendmail 定義的過濾協定,隨 SMTP 對話依序觸發下列事件:connect → helo/ehlo → mail(MAIL FROM)→ rcpt(每位收件人各一次)→ data → header(每個標頭一次)→ end-of-header → body(分段)→ end-of-message。Milter 可執行 accept / reject / tempfail / discard / quarantine,亦可增刪或修改標頭與內文、增減收件人,但這些修改僅在 end-of-message 才生效。DKIM 簽章、OpenDMARC 等工具即以 Milter 實作。相關預設值:milter_default_action = tempfail、milter_protocol = 6。
cleanup:進入佇列前的處理 #
通過 smtpd(或來自 pickup、內部路徑)後,郵件進入 cleanup。此為所有郵件進入佇列前的唯一入口,主要進行以下處理:
- 補齊缺少的標頭(
From:、Date:、Message-ID:等):是否補上取決於always_add_missing_headers,以及來源是否符合local_header_rewrite_clients(預設僅本機來源)。 - 位址改寫:canonical / masquerade / virtual alias 等(詳見下文)。
- 內容檢查:
header_checks/body_checks等*_checks。 - 移除特定標頭:依
message_drop_headers(預設bcc, content-length, resent-bcc, return-path)移除。 - 非 SMTP 的 Milter:
non_smtpd_milters。
實際上 cleanup 是以串流方式處理郵件,envelope、message header、body 走的是不同路徑;上列與後續圖示為便於理解的概念順序,並非嚴格的單一執行序列。完成後,cleanup 將結果寫入 incoming 佇列並通知 qmgr。
以一封缺少 Message-ID 的郵件為例,log 會顯示 cleanup 為其補上:
postfix/cleanup[3631]: 5BFE34C0A62: warning: header Subject: hello pipeline ...: matched-by-header_checks
postfix/cleanup[3631]: 5BFE34C0A62: message-id=<[email protected]>
位址改寫的順序 #
cleanup(搭配 trivial-rewrite)改寫位址的順序如下:
%%{init: {"flowchart": {"wrappingWidth": 600}}}%%
flowchart TB
subgraph R1[" "]
direction LR
S0["1. 正規化成標準形式<br/>append_at_myorigin(user → user@$myorigin)<br/>append_dot_mydomain(user@host → user@host.$mydomain)"] --> S1["2. canonical 對映<br/>sender_canonical_maps → recipient_canonical_maps → canonical_maps"] --> S2["3. masquerade_domains 網域偽裝<br/>host.example.com → example.com"] --> C1[" "]
end
subgraph R2[" "]
direction LR
S3["4. 自動 BCC<br/>always_bcc / sender_bcc_maps / recipient_bcc_maps"] --> S4["5. virtual_alias_maps 虛擬別名<br/>只作用於 envelope 收件人"] --> Q[("寫入 incoming 佇列")]
end
C1 ~~~ S3
style C1 fill:none,stroke:none
style R1 fill:none,stroke:none
style R2 fill:none,stroke:none
以下兩組概念容易混淆:
envelope 與 header 改寫。 部分改寫作用於信封(實際決定投遞的位址),部分作用於標頭(使用者所見的 From:/To:)。自 Postfix 2.2 起,僅符合 local_header_rewrite_clients(預設 permit_inet_interfaces,比對的是連到本機自身介面位址 $inet_interfaces 的來源,而非 permit_mynetworks 的 $mynetworks 網段)的客戶端,其郵件的標頭位址才會被改寫;外部郵件預設僅改信封、不改標頭(除非設定 remote_header_rewrite_domain)。
改寫範圍由 classes 控制。 預設值:
canonical_classes = envelope_sender, envelope_recipient, header_sender, header_recipient(四種皆改)masquerade_classes = envelope_sender, header_sender, header_recipient(刻意不含 envelope_recipient,以免破壞投遞路由)
此外,relocated_maps 用於處理已遷移的使用者:寄往此類位址時,Postfix 會退信並告知新位址。
註:
virtual_alias_maps(虛擬別名,於cleanup展開,作用於所有 class 的收件人)與alias_maps(/etc/aliases,僅於local投遞時展開,且僅作用於本機收件人)屬於兩個不同層級,詳見後文路由一節。
header_checks 與 body_checks #
cleanup 可對郵件進行逐行的正規表示式檢查(pcre: 或 regexp: 表)。四個參數分別檢查不同部位:
| 參數 | 檢查對象 |
|---|---|
header_checks | 主要標頭(MIME 標頭除外) |
mime_header_checks | 只檢查 MIME 相關標頭(預設 $header_checks) |
nested_header_checks | 夾帶的 message/rfc822 附件內的標頭(預設 $header_checks) |
body_checks | 其餘所有內容,含 multipart 邊界(逐行) |
處理上,標頭以「單一邏輯標頭」為單位(跨行時會先合併),內文以「單行」為單位。命中後可採取的動作如下:
| 動作 | 效果 |
|---|---|
REJECT [text] | 拒收整封信,停止檢查 |
DISCARD [text] | 回報投遞成功,實際靜默丟棄 |
HOLD [text] | 把信放進 hold 佇列,等待手動放行(postsuper -H) |
FILTER transport:dest | 覆蓋 content_filter,指定外部過濾器 |
REDIRECT user@domain | 改投到指定位址 |
BCC user@domain | 加一個密件副本收件人 |
PREPEND header: value | 在前面插入一行標頭 |
REPLACE text / IGNORE / STRIP | 取代 / 刪除該行 |
WARN [text] / INFO [text] | 僅記錄 log(測試/稽核用),繼續處理 |
DUNNO(或 OK) | 視為未命中,續看下一行 |
例如 /^Subject:/ WARN matched-by-header_checks 規則,命中時僅於 log 留下 warning: header Subject: ...,不影響投遞。
注意:官方文件多次強調此機制並非用於垃圾郵件或病毒偵測——它不會解碼 BASE64、無法跨行比對,也無法依收件人不同而變化。真正的內容過濾應使用 Milter 或 content_filter(詳見後文〈內容過濾的四種位置〉)。
qmgr 與 trivial-rewrite:佇列、路由與排程 #
郵件進入 incoming 佇列後,接著由 qmgr(郵件投遞的核心)接手。
Postfix 的佇列系統 #
Postfix 於 /var/spool/postfix 下設有數個佇列,各具明確用途:
| 佇列 | 用途 | 寫入者 | 讀取者 |
|---|---|---|---|
maildrop | 本機以 sendmail 投遞、尚未收進主佇列的郵件 | postdrop | pickup |
incoming | 所有新進郵件(cleanup 寫入) | cleanup | qmgr |
active | qmgr 已開啟、正在投遞的郵件;數量有上限(leaky bucket,預設 20000) | qmgr | qmgr / 投遞代理 |
deferred | 暫時投遞失敗的郵件;qmgr 以 exponential backoff 重試 | qmgr | qmgr |
hold | 由 access 政策或 header/body checks 的 HOLD 動作凍結;不會自動重試,須手動放行 | cleanup / 管理員 | 管理員(postsuper) |
corrupt | 無法讀取或損毀的佇列檔,移至此處供檢查 | — | 管理員 |
active 佇列是實際存在於磁碟上的目錄。qmgr 以 round-robin 掃描 incoming 與 deferred(避免飢餓),把符合條件的佇列檔實體移入 active,同時在記憶體中維護這些郵件的排程與投遞狀態;active 佇列有大小上限(預設 20000),額滿時便暫停掃描 incoming/deferred。接著 qmgr 依 transport:nexthop 分組,交予對應的投遞代理。
trivial-rewrite 如何決定投遞代理 #
qmgr 本身不決定路由,而是委由 trivial-rewrite 為每位收件人解析出一組 (transport, nexthop)。解析順序大致如下:
%%{init: {"flowchart": {"wrappingWidth": 600}}}%%
flowchart TB
subgraph RR1[" "]
direction LR
R["收件人位址<br/>(virtual alias 已於 cleanup 展開)"] --> CLASS{"判定 address class"}
CLASS -->|"mydestination"| LOC["local"]
CLASS -->|"virtual_mailbox_domains"| VM["virtual"]
CLASS -->|"relay_domains"| RLY["relay"]
CLASS -->|"其他"| DEF["default"]
LOC --> TM
VM --> TM
RLY --> TM
DEF --> TM
TM{"transport_maps 有對映?<br/>(可覆蓋上述類別的 transport)"}
TM -->|"有"| USE["用表中的 transport:nexthop"]
TM -->|"無"| C1[" "]
end
subgraph RR2[" "]
direction LR
BYCLASS["用該類別的預設 transport<br/>(default 類另可由<br/>sender_dependent_default_transport_maps 覆蓋)"] --> NH{"nexthop:sender_dependent_relayhost_maps /<br/>relayhost?(default 類)"}
NH -->|"有"| VIARELAY["寄給 relayhost"]
NH -->|"無"| MX["查 MX 直接寄"]
end
C1 ~~~ BYCLASS
style C1 fill:none,stroke:none
style RR1 fill:none,stroke:none
style RR2 fill:none,stroke:none
五種位址類別(address class) 為 Postfix 路由的核心。每個類別對應「網域歸屬表、有效收件人表、對應 transport」,預設值如下:
| 類別 | 網域表 | 有效收件人表 | 預設 transport | 投遞代理 |
|---|---|---|---|---|
| local | mydestination | local_recipient_maps | local_transport = local:$myhostname | local(處理 aliases、.forward) |
| virtual alias | virtual_alias_domains | virtual_alias_maps | 無(必須別名到其他類別) | 不投遞,僅轉址 |
| virtual mailbox | virtual_mailbox_domains | virtual_mailbox_maps | virtual_transport = virtual | virtual |
| relay | relay_domains | relay_recipient_maps | relay_transport = relay | smtp |
| default | 無(其餘全部) | 無 | default_transport = smtp | smtp(查 MX 或 relayhost) |
投遞代理 #
qmgr 將 (transport, nexthop) 對應至 master.cf 中的服務名稱,並啟動對應的投遞代理:
local:投遞至 UNIX 信箱或 maildir,處理/etc/aliases與~/.forward。若 alias 或.forward指向其他位址,郵件會重新注入cleanup(回到cleanup階段,即上圖的回流箭頭)。virtual:投遞至 virtual mailbox(不需系統帳號)。lmtp:透過 LMTP 交予 Dovecot、Cyrus 等 mailbox server。smtp/relay:寄往遠端;relay為smtp的另一實例,僅為分開設定與計量。pipe:交予外部程式(例如部分 content filter 或 mailing list 軟體)。error/discard:分別為一律退信與一律靜默丟棄,常用於封鎖特定 transport。
投遞結果分為三種:成功(log 顯示 status=sent);暫時失敗,移入 deferred 稍後重試;永久失敗,交由 bounce 產生退信。
內容過濾的四種位置 #
如前所述,header_checks 僅能進行輕量比對。若需真正的反垃圾或防毒,Postfix 提供數種外掛過濾器的方式,差異在於時機(進佇列前/後)與負擔:
| 方式 | 時機 | 觸發 daemon | 能否在 SMTP 當下退信 | 適合 |
|---|---|---|---|---|
header_checks / body_checks | 進佇列前 | cleanup | 是 | 輕量;封鎖特定 pattern(如特定蠕蟲) |
Milter(smtpd_milters / non_smtpd_milters) | 進佇列前 | smtpd / cleanup | smtpd_milters 可;non_smtpd_milters 不可 | DKIM/DMARC 簽驗、可改標頭內文 |
before-queue proxy(smtpd_proxy_filter) | 進佇列前 | smtpd | 是(可避免 backscatter) | 需在 SMTP 當下攔阻、且過濾器夠快 |
after-queue(content_filter) | 進佇列後 | qmgr → pipe/SMTP 重注入 | 否(只能退信/丟棄/隔離) | 重量級掃描(SpamAssassin、防毒) |
重點如下:
- Milter 的兩個入口:
smtpd_milters作用於 SMTP 進站郵件,於smtpd對話中即時觸發;non_smtpd_milters作用於本機/QMQP 郵件,於cleanup中觸發(cleanup會模擬 connect/helo/mail/rcpt 等事件)。須注意non_smtpd_milters不得 REJECT 或 TEMPFAIL 模擬的 RCPT,否則 Postfix 會回報設定錯誤,郵件將滯留佇列。 - before-queue proxy 可於 SMTP 對話當下即拒絕(無須事後產生退信,避免對偽造寄件者造成 backscatter),代價是佔用
smtpdprocess、擴充性較差。 - after-queue
content_filter可執行任意複雜或耗時的過濾器,不受 SMTP timeout 與記憶體限制,代價是無法於 SMTP 當下拒絕,且會降低效能(SMTP 重注入約 2 倍、pipe 約 4 倍)。重新注入時須以receive_override_options(如no_address_mappings,no_header_body_checks,no_milters)避免重複處理與迴圈。
Milter 相關的預設 macro(可以 postconf -d 查詢):
milter_default_action = tempfail
milter_protocol = 6
milter_connect_macros = j {daemon_name} {daemon_addr} v _
milter_helo_macros = {tls_version} {cipher} {cipher_bits} {cert_subject} {cert_issuer}
milter_mail_macros = i {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}
milter_rcpt_macros = i {rcpt_addr} {rcpt_host} {rcpt_mailer}
milter_data_macros = i
milter_end_of_header_macros = i
milter_end_of_data_macros = i
milter_unknown_command_macros =
References #
- Postfix Architecture Overview
- Postfix SMTP relay and access control (SMTPD_ACCESS_README)
- Postfix Postscreen Howto
- Postfix Address Rewriting
- Postfix Address Classes
- Postfix Virtual Domain Hosting
- Postfix Built-in Content Inspection (header/body checks)
- header_checks(5) manual page
- Postfix Content Inspection overview
- Postfix before-queue Milter support (MILTER_README)
- Postfix after-queue content filter (FILTER_README)
- Postfix before-queue content filter (SMTPD_PROXY_README)
- postconf(5) parameter reference