我們合作過的台灣中小企業,幾乎到最後都會問同一個問題:「訪客在我們網站上填個表單,能不能馬上收到回信,同時也寄一份到我們信箱——而且不用另外花錢買 email 平台?」答案是可以,而且最乾淨的組合就是 Firebase 加上 Google Workspace。麻煩的地方在於:一連串很小的設定錯誤,會默默把整條流程弄壞。
這篇文章記錄的是我們實際用過的完整設定:在一個 Firebase 託管的靜態網站上放一個公開的診斷表單,把填答存到 Firestore,再透過 Google Workspace 寄出兩封信——一封給訪客、一封給老闆,而且看得到的寄件地址(FROM)是一個 Google 群組。底下寫的全部都是真的跑得起來的版本,連我們一路上踩到的錯誤也都列出來。
Firebase 寄信流程最難的地方不是程式碼,而是那四層看不見的設定(SMTP Relay、應用程式密碼、DNS 驗證、群組張貼權限),它們必須「同時」都正確,第一封信才寄得出去。
這條寄信流程到底在做什麼?
訪客在 Firebase 託管的頁面上填表單。瀏覽器會往 Cloud Firestore 寫兩種文件:一份 submissions 文件,存表單答案和算出來的分數;以及兩份 mail 文件——一封給訪客、一封給老闆。Trigger Email from Firestore 這個擴充功能會盯著 mail 這個集合,把每份文件交給 Google 的 SMTP Relay,再由 Relay 用你網域上的 Google 群組地址當寄件地址(FROM)把信寄出去。
一句話講完這整套組合:
- Firebase Hosting 負責提供靜態網站
- Cloud Firestore(具名資料庫)儲存填答和待寄信件文件
- Firebase Extensions——Trigger Email from Firestore——負責真正的寄送
- Google Workspace SMTP Relay 負責驗證外寄信件
- 一個 Google 群組 當作看得到的寄件地址(FROM),讓回信都集中在一處
- DNS 為寄件網域發布 SPF、DKIM 和 DMARC 記錄
為什麼要用 Google Workspace 群組寄信?
大多數公司都希望對外的聯絡地址——例如 hello@yourdomain.com——是一個群組,而不是某個人的個人信箱。寄到群組的信會分發給需要看到它的團隊成員,而回信永遠來自同一個地址,團隊可以輪流處理。問題是:Google 群組本身沒有信箱,也沒辦法持有 SMTP 帳密。
要用群組身分寄信,實務上有兩種做法:
- SMTP Relay——用一個真實的 Workspace 使用者身分驗證,但允許「以同網域內任何地址的身分」寄信,包括群組。
- 在某個真實使用者上設定 「Send mail as」別名——可以用在別名和替代地址,但不能用在群組地址。
如果寄件人是群組,選項 1 才是真正可行的做法。這篇文章後面全都建立在它之上。
怎麼設定 Firebase Hosting 和 Firestore?
Trigger Email 擴充功能用的是 Cloud Functions v2,而它需要 Firebase 的隨用隨付 Blaze 方案。在開始之前一定要先升級,不然這些都不會動。升級到 Blaze 之後,你的 firebase.json 應該長這樣——注意 firestore.database 這個欄位,因為我們用的是具名的 Firestore 資料庫,而不是 (default),所以這個欄位是必填的:
{
"hosting": {
"public": "dist",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"rewrites": [{ "source": "**", "destination": "/index.html" }]
},
"firestore": {
"database": "your-named-db",
"rules": "firestore.rules"
}
}
忘了宣告具名資料庫,是「表單送出去了,但信都沒寄出來」最常見的原因——擴充功能盯著的是具名資料庫,但瀏覽器卻默默地把資料寫進 (default)。
前端表單程式碼怎麼運作?
瀏覽器用 Firebase 的 JavaScript SDK 直接寫進 Firestore——完全不用後端。每次送出表單會寫進兩個集合:
submissions/{doc}——原始答案和算出來的分數mail/{doc}——每封外寄信件一份文件,格式照 Trigger Email 擴充功能要求的來寫
表單處理函式大致長這樣:
import { initializeApp } from "https://www.gstatic.com/firebasejs/10.13.2/firebase-app.js";
import { getFirestore, collection, addDoc, serverTimestamp }
from "https://www.gstatic.com/firebasejs/10.13.2/firebase-firestore.js";
const app = initializeApp(firebaseConfig);
const db = getFirestore(app, "your-named-db"); // 具名資料庫
const MAIL_FROM = "7Pyramid AI <hello@yourdomain.com>";
const MAIL_REPLY_TO = "hello@yourdomain.com";
const OWNER_EMAIL = "owner@yourdomain.com";
document.getElementById("diagnose-form").addEventListener("submit", async (e) => {
e.preventDefault();
await addDoc(collection(db, "submissions"), {
name, email, biz, answers, score, band,
createdAt: serverTimestamp(),
page: location.href
});
await addDoc(collection(db, "mail"), {
from: MAIL_FROM,
replyTo: MAIL_REPLY_TO,
to: [email],
message: { subject: "Your reading", text: "...", html: "..." }
});
await addDoc(collection(db, "mail"), {
from: MAIL_FROM,
replyTo: MAIL_REPLY_TO,
to: [OWNER_EMAIL],
message: { subject: "New submission", text: "...", html: "..." }
});
});
有兩個細節要釘死。具名資料庫是 getFirestore(app, "your-named-db") 的第二個參數;漏掉它就會默默寫進 (default)。另外,在每份文件裡設定 from 和 replyTo,會覆蓋擴充功能的「Default FROM」設定,那個只是備援用的。
公開表單的 Firestore 安全規則怎麼寫?
公開表單等於讓網路上任何人都能寫進你的資料庫。正確的形狀是:兩個集合都只允許 create,加上輕量驗證,其他一律拒絕:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /submissions/{doc} {
allow create: if request.resource.data.email is string
&& request.resource.data.email.matches('.+@.+\\..+')
&& request.resource.data.score is number
&& request.resource.data.score >= 0
&& request.resource.data.score <= 10;
allow read, update, delete: if false;
}
match /mail/{doc} {
allow create: if request.resource.data.to is list
&& request.resource.data.to.size() >= 1
&& request.resource.data.message is map
&& request.resource.data.message.subject is string;
allow read, update, delete: if false;
}
match /{document=**} {
allow read, write: if false;
}
}
}
沒有這些規則,你只會落入兩種失敗的其中一種:要嘛是被鎖死的資料庫,連表單都拒收;要嘛是門戶大開的資料庫,任何訪客都能讀到每一筆填答。對一個公開診斷表單來說,「只允許 create 加上欄位驗證」是剛剛好的中間地帶。
怎麼設定 Workspace SMTP Relay?
這就是那條驗證路徑,讓你能用真實 Workspace 使用者的帳密,「以你網域內任何地址(包括群組地址)」的身分寄信。在管理控制台裡,依序進入 Apps → Google Workspace → Gmail → Routing:
往下捲到 SMTP relay service,按 Add another rule,接著在彈出視窗裡照這些設定設定規則:
- Description(描述):Firebase Trigger Email
- Allowed senders(允許的寄件人):Only addresses in my domains(只限我網域內的地址)
- Authentication(驗證):取消勾選「Only accept mail from specified IPs」(Firebase 沒有固定 IP),並勾選「Require SMTP Authentication」
- Encryption(加密):勾選「Require TLS encryption」
存檔。它應該會在 SMTP relay 清單裡顯示為 Enabled(已啟用)。
怎麼用應用程式密碼做驗證?
Relay 是以真實使用者的身分驗證,所以你需要從一個真實的 Workspace 信箱產生一組應用程式密碼。在乾淨的瀏覽器工作階段裡登入那個信箱——不要用你的個人 @gmail.com——然後:
- 在那個使用者上啟用 兩步驟驗證(這是建立應用程式密碼的必要條件)。
- 前往 myaccount.google.com/apppasswords。
- App 名稱填
firebase-smtp-relay→ Create。 - 複製那組 16 字元的密碼——把 Google 顯示在每四個字一組之間的空格去掉。
Google 顯示出來的是 abcd efgh ijkl mnop,真正的密碼是 abcdefghijklmnop——十六個字元,沒有空格。如果你連空格一起貼上去,看起來會沒問題,但會默默地以 535 BadCredentials 錯誤失敗。
怎麼安裝 Trigger Email 擴充功能?
在 Firebase 控制台裡,打開 Build → Extensions → Browse,找到 Trigger Email from Firestore,按 Install。照下面填寫設定:
- Cloud Firestore instance ID:your-named-db
- Email documents collection:mail
- Default FROM address:7Pyramid AI <hello@yourdomain.com>
- Default REPLY-TO address:hello@yourdomain.com
- SMTP connection URI:
smtps://user%40yourdomain.com:APP_PASSWORD@smtp-relay.gmail.com:465?name=yourdomain.com - Users collection / Templates collection:不需要就留空
關於這個 SMTP URI,有四件事完全沒有商量餘地:
smtps://結尾那個 s 代表 465 埠上的隱式 TLS。只有在你的網路擋掉 465 埠時,才改用 587 埠上的smtp://。%40是使用者名稱裡 @ 的 URL 編碼。- 密碼裡沒有空格。
- 在後面加上
?name=yourdomain.com,把你的網域宣告為 EHLO 主機名稱。少了它,Cloud Functions 在 EHLO 時會送出一個通用的容器 ID 或 localhost,Google 就會在驗證都還沒開始前,用 421-4.7.0 錯誤關掉連線。
為什麼 SPF、DKIM 和 DMARC 很重要?
從 2024 年 2 月起,Google 要求外寄寄件人三項記錄都要有。只要少了其中一項,SMTP Relay 就會在 EHLO 交握時對你限速。你可以在任何終端機上驗證這些記錄:
dig +short TXT yourdomain.com | grep spf1
dig +short TXT google._domainkey.yourdomain.com
dig +short TXT _dmarc.yourdomain.com
一組最低可用的設定大概長這樣:
- SPF——鏈裡某處一定要
include _spf.google.com。範例:v=spf1 include:_spf.google.com ~all。如果你用 SPF flattening 服務,要確認 Google 有在攤平後的清單裡。 - DKIM——分兩步:在管理控制台產生 2048 位元金鑰(Apps → Gmail → Authenticate email),把產生出來的 TXT 記錄發布在
google._domainkey.yourdomain.com,然後按 Start authentication。狀態必須顯示「Authenticating email」——Google 說這在他們系統內最多可能要 48 小時才會生效,但通常快得多。 - DMARC——在
_dmarc.yourdomain.com放一筆 TXT 記錄,最低限度是v=DMARC1; p=none; rua=mailto:reports@yourdomain.com。p=none 只做監控,一開始用很安全,等幾週都沒問題後,再收緊成 quarantine 和 reject。
發布 DKIM 的 TXT 記錄只做了一半。如果你一直沒在管理控制台按「Start authentication」,就算 DNS 看起來都對,Google 那邊的 DKIM 簽章還是關著的。少按這一下,要為一大堆 421-4.7.0 錯誤負責。
群組張貼權限要注意什麼?
如果你的寄件地址(FROM)是一個 Google 群組,群組的 Who can post(誰可以張貼) 設定就必須允許內部寄件人。否則群組會把 Relay 想用它自己地址寄信的嘗試擋下來——而且是默默地。SMTP Relay 收下了這封信,接著群組悄悄把它丟掉,你只會在擴充功能的記錄裡看到一個遞送錯誤。
從 groups.google.com → 管理該群組 → Settings → Permissions → Posting permissions → 確認「Members of the organization」(如果你的表單是公開的,就選「Anyone on the web」)被允許張貼。
你會踩到哪些錯誤,又該怎麼解?
第一次設定幾乎不可能不踩到下面至少一個錯誤。這是我們踩過的四個,照它們通常出現的順序排:
1. RESOURCE_ERROR — Eventarc Service Agent Permission Denied
擴充功能會安裝 Cloud Functions v2,而它會用到 Eventarc。在一個全新的專案上,Google 需要佈建一個 Eventarc service agent,並授予它 roles/eventarc.serviceAgent 角色。有時候這會慢半拍。
解法:啟用 Eventarc、Cloud Functions、Cloud Run Admin、Cloud Build 和 Artifact Registry 這幾個 API,等 5~10 分鐘,再到 IAM 並勾選「Include Google-provided role grants」檢查。如果那個 agent 不見了,就用 gcloud 手動授予,然後重試安裝。
2. 535-5.7.8 Username and Password Not Accepted
經典的 Gmail SMTP 驗證失敗。有五個可能原因,照常見程度排:
- 應用程式密碼是在錯誤的 Google 帳號下產生的(用了個人 Gmail,而不是 URI 裡指名的那個 Workspace 使用者)。
- 應用程式密碼貼上時帶著 Google 顯示的四字元空格。
- 那個 Workspace 使用者沒有啟用兩步驟驗證。
- Workspace 管理員政策停用了應用程式密碼。
- URI 使用者名稱裡的 @ 沒有 URL 編碼成
%40。
最乾淨的隔離方法,是寫一支 10 行的 Python 腳本,用 smtplib.SMTP_SSL 和同一組帳密,直接連 smtp-relay.gmail.com:465。如果腳本寄得出去,代表帳密沒問題,問題出在擴充功能的 URI 設定上。
3. 421-4.7.0 Try Again Later, Closing Connection (EHLO)
Google 在 EHLO 交握時就把連線關掉了,根本還沒開始驗證。這是連線層級的拒絕,有三個常見原因:
- 驗證失敗的冷卻期。每一次 535 都會讓一個安全計數器加一。一旦超過門檻,連登入成功也會被擋。解法是停止重試、乖乖等。違反直覺,但有白紙黑字記載。
- 少了 DKIM 或 DMARC。如果你的記錄都發布了,但 DKIM 簽章還沒在管理控制台啟用,Relay 一樣會對你限速。去按 Start authentication。
- 通用的 EHLO 主機名稱。SMTP URI 上少了
?name=yourdomain.com,Cloud Functions 就會用一個通用的容器主機名稱自我介紹。把這個參數加上去再重新部署。
4. 改 dist/index.html 改完一直消失
如果你的專案有建置步驟會把檔案輸出到 dist/,那直接改建置出來的檔案是白費功夫——下一次建置就會把它蓋掉。要改的是原始 HTML,然後重新建置。事後看很明顯,但當你正埋頭處理表單行為時很容易忽略。
怎麼確認整套都接對了?
在你宣告大功告成之前,照順序把下面這些全部跑一遍:
dig +short TXT yourdomain.com | grep spf1
dig +short TXT google._domainkey.yourdomain.com | grep -q "v=DKIM1"
dig +short TXT _dmarc.yourdomain.com | grep -q "v=DMARC1"
然後在管理控制台裡:
- DKIM 狀態顯示 Authenticating email,而不是「Not authenticating mail」
- SMTP Relay 規則顯示 Enabled
- 群組的「Who can post」允許內部寄件人
接著在 Firebase 控制台裡:
- Trigger Email 擴充功能顯示 Enabled,而不是「Updating…」
- 擴充功能設定指向具名的 Firestore 資料庫,而不是
(default) - 一份送出的 mail 文件,大約 30 秒內會拿到
delivery.state: "SUCCESS"
成功時,delivery 這個 map 會長這樣:
delivery.state = "SUCCESS"
delivery.info.accepted = ["visitor@example.com"]
delivery.info.response = "250 2.0.0 OK ..."
7Pyramid 觀點
有一份清單,看起來像在前進、其實只會把事情弄更糟:在冷卻期內換掉應用程式密碼(只是多一次失敗、把冷卻拉長);重裝擴充功能想「重置」(Google 那邊不會被重置,白花五分鐘);在 465 和 587 埠之間反覆切換(帳密錯兩個都失敗、帳密對哪個都行);以及一直送測試填答想「看看現在好了沒」(每送一次都會把冷卻期拉長)。遇到 421-4.7.0,最有效的動作往往是「先什麼都不做,等一小時」。
這對你的生意為什麼重要?
一條跑得起來的 Firebase + Workspace 寄信流程,是台灣中小企業從公開網站抓到名單、並自動回覆,最便宜也最可靠的方式。不用訂閱任何 SaaS、不用 Mailchimp、不用 Zapier——就只是 Firestore 文件加上一個 Workspace 使用者。建置成本是一個工程師一天的細心設定;只要你沒超過 Firebase 免費額度的寫入配額,跑起來的成本基本上是零。
如果你想找人幫你把這套接到自己的網站上——或是卡在一個超過 24 小時都還沒解開的 421-4.7.0——歡迎預約一場免費的 30 分鐘諮詢,7Pyramid AI 團隊會陪你走過你自己的設定。這條一模一樣的流程,我們已經為許多醫療、診所和專業服務的客戶上線過。
常見問題
不行。Google 群組是一個分發地址,不是信箱,所以它沒辦法持有 SMTP 帳密。要讓信看起來是「從群組地址寄出」,你要用同網域內一個真實使用者的身分,去 Workspace SMTP Relay 上驗證,Relay 就會讓那個使用者以任何地址(包括群組)的身分寄信。
幾乎都是三件事的其中之一:SPF、DKIM 或 DMARC 不見了,或還沒在 Workspace 裡生效;你因為之前帳密錯誤而正處在驗證失敗的冷卻期;或是你的寄件人在 EHLO 時沒有宣告網域。解法是把 DNS 修好、至少停止重試一小時,並在 SMTP 連線 URI 後面加上 ?name=yourdomain.com,強制指定 EHLO 主機名稱。
可以,但你在安裝時必須讓擴充功能指向確切的資料庫 ID,而且你的前端必須呼叫 getFirestore(app, "your-db-id")——不能用 default。如果擴充功能是設定給具名資料庫,那寫進預設資料庫的資料是不會觸發它的。這是「表單送出去了,但信都沒來」最常見的原因。
要。從 2024 年 2 月起,Google 對外寄寄件人三項都要求。少了它們,Workspace 會在驗證都還沒開始前,就在 EHLO 交握時對你的 Relay 限速或封鎖。DMARC 可以停在 p=none 做監控,但這筆記錄必須存在而且有效。
每一次 SMTP 驗證失敗,都會讓寄件帳號上的一個安全計數器加一。失敗幾次之後,Google 會觸發一段冷卻期,連有效的嘗試也擋掉。標準等待時間大約是一小時,但在冷卻期內每重試一次都會把它拉長。解法很違反直覺:停止測試、把根本原因修好、然後等。