The Complete Postfix Architecture: From SMTP Handshake to Final Delivery

Author Avatar

SiriusKoan (with Claude)

  ·  8 min read

Postfix 的 main.cf 中有大量 smtpd_*_restrictions*_checks*_mapsmiltertransport 等參數。初次設定時,往往難以判斷各參數在郵件流程中於何處生效,以及彼此的先後順序。

本文完整說明 Postfix 從郵件進入到投遞完成的架構,涵蓋各道處理環節:postscreensmtpd 各階段的 *_restrictions、Milter、cleanup 的位址改寫(canonical / virtual / masquerade)與 header_checks / body_checks、佇列系統、qmgrtrivial-rewrite 的路由決策,以至各類投遞代理(delivery agent)。

本文以官方文件(postfix.org)為準,文中預設值以 Postfix 3.10 分支(實測 3.10.5)為基準。

註:Postfix 不少預設值是「相容層級(compatibility_level)」的條件式,例如 smtpd_relay_restrictionssmtpd_relay_before_recipient_restrictionsappend_dot_mydomainrelay_domainspostconf -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 連線
smtpdSMTP 伺服器,處理進站 SMTP 對話與各階段 *_restrictions
qmqpdQMQP 伺服器(另一種投信協定,較少用)
pickupmaildrop 佇列讀取本機投遞的信,交給 cleanup
cleanup進佇列前的最後加工:補標頭、位址改寫、*_checks、非 SMTP 的 Milter
trivial-rewrite把位址正規化,並為每個收件人解析出 transport:nexthop
qmgr佇列管理員,郵件投遞的核心,排程並呼叫投遞代理
local本機投遞代理,處理 /etc/aliases~/.forward
virtualvirtual mailbox 投遞代理(不需要 UNIX 帳號)
lmtp用 LMTP 投遞給 mailbox server(如 Dovecot、Cyrus)
smtpSMTP 客戶端,負責寄往遠端(也用於 relay
pipe把信透過 pipe 交給外部程式
bounce / defer / trace產生退信/延遲通知與紀錄(同一支程式的三種身分)
error / discard特殊「投遞代理」:一律退信 / 一律靜默丟棄
verify位址可投遞性驗證與快取
flush依需求強制刷新佇列(ETRNpostqueue -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

  1. 網路(SMTP / QMQP):外部連線由 smtpd(或 qmqpd)接收,移除協定外殼並進行基本檢查後,將寄件者、收件人與郵件內容交給 cleanuppostscreen 可置於 smtpd 之前先行過濾連線。
  2. 本機投信:本機程式透過相容 sendmail 的 sendmail(1) 指令投遞,由具權限的 postdrop(1) 將郵件寫入 maildrop 佇列,再由 pickup 讀出並檢查後交給 cleanup
  3. 內部產生的郵件:例如 bounce 產生的退信、postmaster 通知等,直接交給 cleanup
  4. 轉寄與重新注入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_listpostscreen_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 階段參數預設值
1CONNECTsmtpd_client_restrictions
2HELO / EHLOsmtpd_helo_restrictions
3MAIL FROMsmtpd_sender_restrictions
4RCPT TO(relay 政策)smtpd_relay_restrictionspermit_mynetworks, permit_sasl_authenticated, defer_unauth_destination
5RCPT TO(反垃圾政策)smtpd_recipient_restrictions
6DATAsmtpd_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依網段 / 認證 / 無條件放行
relayreject_unauth_destination非授權目的地一律拒(防 open relay)
通用reject / defer / warn_if_reject無條件拒 / 暫時拒 / 僅記錄而不實際拒絕(測試用)
DNS 檢查reject_unknown_sender_domain / reject_unknown_recipient_domain寄件/收件網域無法解析即拒(無 A/MX、malformed MX、Null MX 等)
clientreject_unknown_client_hostname / reject_rbl_client反解不符 / 命中 DNSBL 即拒
HELOreject_invalid_helo_hostname / reject_non_fqdn_helo_hostnameHELO 語法錯誤 / 非 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 對話依序觸發下列事件:connecthelo/ehlomail(MAIL FROM)→ rcpt(每位收件人各一次)→ dataheader(每個標頭一次)→ end-of-headerbody(分段)→ end-of-message。Milter 可執行 accept / reject / tempfail / discard / quarantine,亦可增刪或修改標頭與內文、增減收件人,但這些修改僅在 end-of-message 才生效。DKIM 簽章、OpenDMARC 等工具即以 Milter 實作。相關預設值:milter_default_action = tempfailmilter_protocol = 6

cleanup:進入佇列前的處理 #

通過 smtpd(或來自 pickup、內部路徑)後,郵件進入 cleanup。此為所有郵件進入佇列前的唯一入口,主要進行以下處理:

  1. 補齊缺少的標頭From:Date:Message-ID: 等):是否補上取決於 always_add_missing_headers,以及來源是否符合 local_header_rewrite_clients(預設僅本機來源)。
  2. 位址改寫:canonical / masquerade / virtual alias 等(詳見下文)。
  3. 內容檢查header_checks / body_checks*_checks
  4. 移除特定標頭:依 message_drop_headers(預設 bcc, content-length, resent-bcc, return-path)移除。
  5. 非 SMTP 的 Milternon_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 投遞、尚未收進主佇列的郵件postdroppickup
incoming所有新進郵件(cleanup 寫入)cleanupqmgr
activeqmgr 已開啟、正在投遞的郵件;數量有上限(leaky bucket,預設 20000)qmgrqmgr / 投遞代理
deferred暫時投遞失敗的郵件;qmgr 以 exponential backoff 重試qmgrqmgr
hold由 access 政策或 header/body checksHOLD 動作凍結;不會自動重試,須手動放行cleanup / 管理員管理員(postsuper
corrupt無法讀取或損毀的佇列檔,移至此處供檢查管理員

active 佇列是實際存在於磁碟上的目錄。qmgr 以 round-robin 掃描 incomingdeferred(避免飢餓),把符合條件的佇列檔實體移入 active,同時在記憶體中維護這些郵件的排程與投遞狀態;active 佇列有大小上限(預設 20000),額滿時便暫停掃描 incoming/deferred。接著 qmgrtransport: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投遞代理
localmydestinationlocal_recipient_mapslocal_transport = local:$myhostnamelocal(處理 aliases、.forward
virtual aliasvirtual_alias_domainsvirtual_alias_maps無(必須別名到其他類別)不投遞,僅轉址
virtual mailboxvirtual_mailbox_domainsvirtual_mailbox_mapsvirtual_transport = virtualvirtual
relayrelay_domainsrelay_recipient_mapsrelay_transport = relaysmtp
default無(其餘全部)default_transport = smtpsmtp(查 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:寄往遠端;relaysmtp 的另一實例,僅為分開設定與計量。
  • 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 / cleanupsmtpd_milters 可;non_smtpd_milters 不可DKIM/DMARC 簽驗、可改標頭內文
before-queue proxy(smtpd_proxy_filter進佇列前smtpd是(可避免 backscatter)需在 SMTP 當下攔阻、且過濾器夠快
after-queue(content_filter進佇列後qmgrpipe/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),代價是佔用 smtpd process、擴充性較差。
  • 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 #