SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 已更新 2026-10-18

在 VPS 自架 Mailpit 一次性電子郵件收件匣

使用 Mailpit 在 VPS 接收測試郵件,透過 Web 介面查看內容,讓 staging 應用程式的郵件不會誤寄給真實客戶。

一次性電子郵件收件匣是什麼

一次性電子郵件收件匣是一個小型 SMTP(簡易郵件傳輸協定)伺服器,可接受寄往任何地址的郵件,但不會轉送任何郵件。您的 staging 應用程式會將郵件寄到這裡,而不是真實的郵件服務供應商,所有郵件都會停留在此處。您可以在 Web 介面中查看收到的內容。因此,即使收件人清單錯誤或範本損毀,也不會造成損失,因為郵件不會離開此收件匣。

本指南使用 Docker Compose,在單一 VPS 上建置此服務。Mailpit 是接收所有郵件的 sink。其 SMTP listener 綁定在只有您的應用程式能連線的位置;Web 介面則透過 nginx、傳輸層安全性(TLS)與密碼保護。保留上限可避免收件匣填滿磁碟。如果您不熟悉 Compose,VPS 的 Compose 基礎涵蓋本指南所使用的檔案配置。

這是測試工具,不是郵件伺服器。它沒有帳戶、郵件遞送或垃圾郵件過濾功能。供真實使用者使用的真實信箱,則需要完整的郵件伺服器,例如 Mailcow,工作規模也大得多。

Mailpit、Inbucket 與 MailHog:應選擇哪個郵件接收器

有 3 個工具可執行這項工作。它們的差異在於維護狀態、監聽的連接埠,以及接受訊息後可執行的處理方式。以下版本資訊於 2026 年 10 月確認。

MailHog (mailhog/mailhog) 在 1025 監聽 SMTP,並在 8025 提供介面。它目前仍可運作。但其預設分支自 2022 年 8 月起沒有新的提交,追蹤系統中有超過 200 個未解決問題,因此測試流程會使用未修補的相依元件。請勿以它開始新的工作。

Inbucket (inbucket/inbucket) 在 2500 監聽 SMTP、在 9000 提供 Web 介面,並在 1100 監聽 POP3(郵局通訊協定第 3 版)。3.2.0 版於 2026 年 10 月 10 日發布。它會將訊息儲存為 /storage 下的檔案,並自動清理這些檔案:映像檔會設定 INBUCKET_STORAGE_RETENTIONPERIOD=72h 與 INBUCKET_STORAGE_MAILBOXMSGCAP=300。如果測試需要使用 POP3 用戶端函式庫收取郵件,而不是呼叫 HTTP,請選擇它。

Mailpit (axllent/mailpit) 使用與 MailHog 相同的連接埠 1025 和 8025,因此無須修改應用程式設定即可取代 MailHog。1.31.6 版於 2026 年 10 月 15 日發布;1.30 系列於 8 月以 1.30.7 結束。它將本指南所需的功能內建於二進位檔中:Web 介面與 API(應用程式介面)的密碼檔案、訊息數量上限、訊息存留時間上限,以及收件者篩選器。本指南其餘內容將使用 Mailpit。

Catch-all 的運作方式,以及為何不涉及 DNS

應用程式不會在這裡查詢郵件的遞送位置。應用程式會取得主機和連接埠,建立 TCP 連線,並宣告 RCPT TO:<anyone@example.test>。Mailpit 會接受該收件者資訊,不論其中寫入什麼內容,接著儲存訊息,不會轉送任何內容。系統從未解析該網域,因此即使 .test 是不存在於網域名稱系統(DNS)中的保留名稱,example.test 仍可正常運作。

這就是完整的運作機制,也是收件匣預設安全的原因。不涉及任何 MX(郵件交換程式)記錄,不會嘗試遞送,也不會有任何訊息送達真實使用者。

將 staging 應用程式指向 sink

當應用程式以容器形式執行於同一個 Compose 專案時,將 SMTP 主機設為 mailpit;當應用程式在主機上執行時,則設為 127.0.0.1。將連接埠設為 1025,關閉 TLS,並將使用者名稱和密碼留白。Mailpit 接受匿名郵件。

有些框架沒有認證資料就拒絕寄信。MP_SMTP_AUTH_ACCEPT_ANY=1 會讓 Mailpit 接受任何使用者名稱和密碼,MP_SMTP_AUTH_ALLOW_INSECURE=1 則允許在未加密連線上使用 PLAIN 和 LOGIN 機制。這兩項設定只有在此處安全,因為該監聽器無法從網際網路連入;下方的部署設定會強制套用此限制。

建議從第一天就設定 MP_SMTP_ALLOWED_RECIPIENTS。它接受正規表示式,並拒絕所有不符合該表示式的收件者。將它指向測試網域。如此一來,即使 staging 資料庫仍含有真實客戶的地址,也會在應用程式日誌中產生明確的失敗訊息,而不是讓郵件悄悄進入 sink。

Docker Compose 檔案

先建立目錄與 Web 介面的密碼檔案。htpasswd -B 會寫入 bcrypt 雜湊值,Mailpit 也同樣支援 bcrypt 與純文字。

mkdir -p ~/mailpit/data
cd ~/mailpit
sudo apt update && sudo apt install -y apache2-utils
htpasswd -B -c data/ui-auth qa

寫入 compose.yaml:

services:
  mailpit:
    image: axllent/mailpit:v1.31
    container_name: mailpit
    restart: unless-stopped
    ports:
      - "127.0.0.1:8025:8025"
      - "127.0.0.1:1025:1025"
    volumes:
      - ./data:/data
    environment:
      MP_DATABASE: /data/mailpit.db
      MP_MAX_MESSAGES: 2000
      MP_MAX_AGE: 14d
      MP_UI_AUTH_FILE: /data/ui-auth
      MP_SMTP_AUTH_ACCEPT_ANY: 1
      MP_SMTP_AUTH_ALLOW_INSECURE: 1
      MP_SMTP_ALLOWED_RECIPIENTS: '@example\.test$$'

兩個連續的 dollar sign 並不是打字錯誤。Compose 會將單一 $ 視為變數展開的開頭,因此 $$ 才能將一個字面上的 dollar sign 傳入容器。正規表示式傳到 Mailpit 時會是 @example\.test$。

啟動服務並檢查健康狀態:

docker compose up -d
docker compose ps

STATUS 欄應顯示 Up ... (healthy)。該映像檔內建的 healthcheck 會每 15 秒執行 /mailpit readyz,因此容器若持續處於 starting,或轉為 unhealthy,通常表示它未在容器內的 8025 埠提供服務。這項檢查會讀取 MP_ 環境變數中的設定,但不會讀取命令列旗標。這也是本指南將所有設定放在 environment: 下的原因之一。若改以旗標傳入選項,檢查可能會回報 unhealthy,即使 Mailpit 正常提供服務。先閱讀 docker compose logs mailpit,再進行其他變更。

兩個發布的埠都包含繫結位址,而該位址就是安全性控制項。在容器內,Mailpit 監聽 0.0.0.0,這沒有問題,因為容器擁有自己的網路命名空間。對映的左側決定外部來源是否能連線。寫成 8025:8025 時,Docker 會繫結主機上的所有位址,包括公開位址。

如果 staging 應用程式是此檔案中的服務,請完全刪除 1025 對映,並讓應用程式透過主機名稱 mailpit 的 1025 埠連線。共用 Compose network 上的容器可以直接互相連線,因此 SMTP 埠完全不會接觸主機。Compose network 如何解析服務名稱說明這項查詢機制。

傳送一則訊息並確認已送達

python3 - <<'EOF'
import smtplib
from email.message import EmailMessage

m = EmailMessage()
m["From"] = "staging@example.test"
m["To"] = "anyone@example.test"
m["Subject"] = "Mailpit smoke test"
m.set_content("If this appears in the web interface, the sink works.")
with smtplib.SMTP("127.0.0.1", 1025) as s:
    s.send_message(m)
EOF

成功時,指令不會輸出任何內容。請透過 API 確認訊息已儲存:

curl -s -u qa:yourpassword http://127.0.0.1:8025/api/v1/messages

這會回傳列出已儲存訊息的 JSON。移除 -u 旗標後,相同的請求會遭拒,因為 MP_UI_AUTH_FILE 會同時保護 API 與 Web 介面。任何讀取收件匣的測試也必須一併傳送這些憑證。

Python 指令碼傳回 ConnectionRefusedError,表示沒有任何程式在 127.0.0.1:1025 上監聽。若已移除 SMTP 對映,這就是預期結果;此時必須從同一個 Compose network 中的容器執行檢查。

使用 nginx 搭配密碼發布 Web 介面

目前此介面只在 loopback 位址上回應。nginx 負責 TLS termination,並在任何請求送達介面前要求密碼。

sudo htpasswd -B -c /etc/nginx/mailpit.htpasswd qa
server {
    listen 443 ssl;
    server_name mail-test.example.com;

    ssl_certificate     /etc/letsencrypt/live/mail-test.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mail-test.example.com/privkey.pem;

    auth_basic           "mailpit";
    auth_basic_user_file /etc/nginx/mailpit.htpasswd;

    location / {
        proxy_pass http://127.0.0.1:8025;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

先使用 sudo nginx -t && sudo systemctl reload nginx 檢查語法,再重新載入。若這是你第一次設定反向代理,建議先閱讀一次 反向代理區塊中的各項指令用途。

在 nginx 設定檔與 data/ui-auth 中使用相同的使用者名稱和密碼。nginx 會將瀏覽器的 Authorization 標頭轉送至上游,因此相同的認證資料可透過單次提示同時通過兩項檢查。若使用不同的認證資料,瀏覽器會保留其中一組,但第二項檢查會拒絕該組資料。

Upgrade 和 Connection 標頭不是裝飾用途。Mailpit 會透過 WebSocket 將新郵件推送到已開啟的頁面;如果代理使用 HTTP/1.1,卻沒有這些標頭,就無法升級連線。頁面雖然能正確載入,但內容不會更新:郵件已經抵達,API 也能顯示郵件,清單卻會維持不變,直到你重新載入頁面。

請保留這兩層保護。nginx 密碼保護公開位址,而 MP_UI_AUTH_FILE 保護 8025 埠本身。這點很重要,因為只要保留期限尚未到期,staging 應用程式產生的每一個密碼重設連結,都能在該介面中讀取。

不要讓 sink 成為開放式轉送站

開放式轉送站是任何人都能透過 SMTP 伺服器提交郵件,並將郵件轉送至任意目的地。垃圾郵件寄件者會持續掃描這類伺服器。若在你的位址上發現開放式轉送站,通常會收到濫發通報,甚至導致帳號遭到停權。

Mailpit 預設不是開放式轉送站,因為預設不會將郵件轉送出去。它有 2 個對外傳送功能,而且在完成設定前都處於停用狀態。MP_SMTP_RELAY_CONFIG 指定轉送設定檔,並在介面中啟用 release 動作,讓你透過該伺服器傳送選取的郵件;自動轉送則還需要啟用 MP_SMTP_RELAY_ALL(預設為 false)或 MP_SMTP_RELAY_MATCHING。使用 MP_SMTP_FORWARD_CONFIG 或 MP_SMTP_FORWARD_* 變數設定的 forwarding,會將每封收到的郵件複製到你指定的固定位址。全部維持未設定是刻意的安全選擇。

有 2 種方式會失去這項特性。啟用 MP_SMTP_RELAY_ALL 後,所有收到的郵件都會轉送至原始收件者,並忽略 MP_SMTP_ALLOWED_RECIPIENTS。如果再將 SMTP 埠暴露至網際網路,就會建立可運作的開放式轉送站。MP_SMTP_RELAY_MATCHING 對符合其模式的任何收件者也會執行相同動作。即使不啟用轉送而暴露該埠,陌生人仍無法透過你的伺服器寄信,但可以填滿儲存空間,並將內容放入團隊信任的介面。若啟用 forwarding,這些郵件也會進入接收副本的任何收件匣。

Docker 主機上的陷阱在於防火牆。發布埠時,Docker 會將自己的規則寫入 nat table。目的地為容器的流量會先在此處比對,之後 ufw(uncomplicated firewall)規則才有機會處理。sudo ufw deny 1025/tcp 會回報成功,但實際上不會變更任何設定。為什麼 Docker 發布的埠會直接繞過 ufw 說明規則鏈的處理順序。

修正方式是修改 mapping 中的位址,而不是新增防火牆規則。先確認實際繫結的內容:

sudo ss -ltnp | grep -E ':(1025|8025)'

正常輸出會顯示 127.0.0.1:1025 和 127.0.0.1:8025。若某行顯示 0.0.0.0:1025,表示 mapping 已失去位址,sink 正在監聽網際網路。從另一台機器執行測試時,nc -vz mail-test.example.com 1025 應逾時或遭拒絕。

當應用程式位於另一台伺服器時,不要開放 1025 來連接兩台機器。讓兩台機器加入私有網路或 VPN tunnel,並將 mapping 繫結至該介面的位址。

僅在需要實際接收外部郵件時發布 MX 記錄

MX(mail exchanger)記錄會告訴其他郵件伺服器,哪部主機負責接收某個網域的郵件。省略 MX 記錄本身並不會阻擋郵件:根據 RFC 5321 section 5.1,當網域沒有 MX 記錄時,寄件伺服器會改用該網域的位址記錄(A 或 AAAA);而你為網頁介面指向這部主機的主機名稱已有位址記錄。真正能阻擋網際網路郵件的原因,是這部主機沒有任何服務監聽 port 25。收件匣只會保存你自己的應用程式提交的郵件,這正是測試信箱的用途。

接收實際郵件表示 DNS 記錄會將寄件伺服器導向這部主機(MX 記錄,或寄件伺服器回退使用的位址記錄)、Mailpit 會監聽 port 25(MP_SMTP_BIND_ADDR=0.0.0.0:25),而且該埠已開放。此時,你執行的是一個接收該網域所有地址郵件的公開 catch-all。請清楚了解後續影響。

  • DNS 記錄出現後,幾天內就會開始收到垃圾郵件,因為收集器會讀取 DNS。接著,字典攻擊會逐一嘗試常見名稱,並為每次嘗試儲存一封郵件。
  • 陌生寄件者的附件會寫入磁碟並持續保留。沒有任何機制會過濾這些附件,因此未知寄件者的壓縮檔可能會與你自己的測試郵件並存。
  • 任何得知該網域的人,都能使用該網域下的地址註冊第三方服務,確認郵件會直接送到你的伺服器。若密碼防護曾經失效,這些帳戶就會落入讀取收件匣的人手中。
  • 保留期限不再只是日常維護事項,而會成為承載負荷的重要因素,因為郵件量已不再由你控制。

如果需要透過實際收信檢查郵件投遞能力,請使用專用子網域,將 MP_MAX_AGE 設為較短的期限,並將其中所有內容視為公開資料。如果需要供人員長期使用的信箱,請改用具備過濾與備份功能的正式郵件伺服器。

Retention:無上限的 catch-all 如何填滿磁碟

Mailpit 預設保留 500 封郵件,並定期刪除超出上限的最舊郵件。MP_MAX_MESSAGES: 0 會關閉此上限;若未設定 MP_MAX_AGE,系統就不會自動刪除任何郵件,因此 catch-all 可能在無人察覺的情況下填滿磁碟。MP_MAX_AGE 會加入時間限制,可搭配郵件數量上限使用,也可單獨使用。時間以小時或天數表示,例如 36h 或 14d。

MP_DATABASE 決定這些內容是否能持續保存。未設定此選項時,Mailpit 會將郵件寫入暫存檔,程序結束時該檔案就會被刪除,因此每次重新啟動都會清空收件匣。設定後,郵件會在重新啟動後保留,檔案也會持續增長。

真正消耗空間的是附件。每晚將 2 MB 的 PDF 報告寄送至 300 個測試地址,代表每晚增加 600 MB;單靠郵件數量上限可能無法及時控制增長。請將這項增長納入共用磁碟區的容量規劃,因為像 PhotoPrism 或 Immich 這類大量使用媒體檔案的鄰近服務,可能早已占用小型 VPS 磁碟的大部分空間。

du -h ~/mailpit/data/mailpit.db
df -h /

在 CI 執行之間清空儲存區,不要等到上限觸發:

curl -s -u qa:yourpassword -X DELETE http://127.0.0.1:8025/api/v1/messages

Inbucket 以 INBUCKET_STORAGE_RETENTIONPERIOD 處理相同問題(映像檔中為 72h),並以 INBUCKET_STORAGE_MAILBOXMSGCAP 設定數量上限(300)。無論使用哪一個,請在第一個測試套件指向該服務前先設定限制。

閱讀測試套件中的收件匣

GET /api/v1/messages 會列出目前儲存的內容,GET /api/v1/message/{ID} 會連同各部分與標頭傳回一封郵件,GET /api/v1/search 可進行篩選,DELETE /api/v1/messages 會清空儲存區。您所執行版本的互動式文件位於 http://127.0.0.1:8025/api/v1/。

實用的測試會先傳送郵件,持續輪詢直到郵件出現,再檢查主旨與其中的連結,最後刪除所有內容。請使用短時間的重試迴圈進行輪詢,不要只發出單一請求。應用程式若在背景工作程式中排入郵件佇列,會在 Mailpit 收到郵件前就從傳送呼叫返回。同樣的模式也適用於 自架 API 測試與模擬工具。這通常是完全不接觸正式環境的 staging 環境的另一個組成部分。

FAQ

自架一次性電子郵件信箱是開放式轉送站嗎?

只要關閉轉送,就不是。Mailpit 預設不會將郵件轉送到其他地方:MP_SMTP_RELAY_CONFIG 只會啟用手動釋出所選郵件,而自動轉送還需要另外設定 MP_SMTP_RELAY_ALL 或 MP_SMTP_RELAY_MATCHING。維持未設定狀態時,即使陌生人連到 1025 埠,也無法透過伺服器寄信。他們仍可能填滿儲存空間,因此應將 SMTP 埠綁定到只有應用程式能存取的位址。在 Compose 中發布為 1025:1025 會綁定所有主機位址,而 sudo ufw deny 1025/tcp 無法關閉這個公開存取,因為會先套用 Docker 自己的 nat 規則。

測試網域需要 MX 記錄嗎?

只有在你希望網際網路上的郵件送達時才需要,而且省略 MX 記錄本身並不能阻擋郵件。沒有 MX 記錄時,寄件伺服器會退回使用該網域的 A 或 AAAA 記錄,因此真正能阻擋網際網路郵件的條件,是沒有任何程式監聽 25 埠。這樣一來,信箱只會保存你自己的應用程式透過 SMTP 提交的郵件。若將 DNS 指向這台主機並開放 25 埠,就會變成公開的 catch-all:幾天內收到垃圾郵件、字典攻擊每次嘗試都儲存一封郵件,以及陌生人寄來的附件在沒有篩選的情況下佔滿磁碟。

為什麼只有重新載入頁面時,郵件清單才會更新?

Mailpit 會透過 WebSocket 將新郵件推送到已開啟的頁面。若 nginx 的 location 區塊缺少 proxy_http_version 1.1 以及 Upgrade 和 Connection 標頭,就無法升級該連線,因此頁面能正常載入,之後卻會停止更新。郵件仍會送達,API 也仍會回傳郵件,所以信箱看起來像是內容過期,而不是服務故障。加入這些設定行,重新載入 nginx,然後重新載入頁面。

如何避免信箱填滿磁碟?

讓 MP_MAX_MESSAGES 保持為有效數值,並加入 MP_MAX_AGE。預設上限是 500 封郵件;將其設為 0 會停用依數量刪除,因此除非設定 MP_MAX_AGE,否則不會清理任何郵件。這就是 catch-all 搭配附件時會悄悄增長的原因。MP_MAX_AGE 接受小時或天數,例如 36h 或 14d。在 CI teardown 階段使用 curl -u qa:yourpassword -X DELETE http://127.0.0.1:8025/api/v1/messages 清空儲存區,因為刪除端點使用與 Web 介面相同的認證。只有 Send API(POST /api/v1/send)可以設定獨立認證,而且這是選用設定。Inbucket 以 INBUCKET_STORAGE_RETENTIONPERIOD(72h)和 INBUCKET_STORAGE_MAILBOXMSGCAP(300)提供相同功能。

應該使用 Mailpit、Inbucket 還是 MailHog?

截至 October 2026,新專案建議使用 Mailpit。MailHog 仍可執行,但其預設分支自 August 2022 起就沒有提交,因此包含未修補的相依套件。Inbucket 仍積極維護(3.2.0,October 2026);如果測試需要 POP3,Inbucket 是較好的選擇,因為 Mailpit 的 POP3 伺服器只有在提供認證後才會啟動,認證可以放在密碼檔案中,或設定在 MP_POP3_AUTH 變數中。Mailpit 使用與 MailHog 相同的埠 1025 和 8025,因此替換 MailHog 時,只需在 Compose 檔案中更換一個映像檔名稱。