thegioikeycap.vn — Email
1. Tình trạng — ĐÃ SỬA XONG 2026-09-05
Email hoạt động qua Brevo (gói free 300 thư/ngày). Đã kiểm chứng 3 thư đều delivered,
gồm cả email đặt lại mật khẩu thật gửi qua form trên web.
Bối cảnh lỗi cũ
Site chưa từng gửi được email nào cho tới 2026-09-05. Phát hiện 2026-09-04 khi kiểm tra luồng quên mật khẩu.
Nguyên nhân: container keycap_php không có MTA nào (không sendmail/ssmtp/msmtp/postfix/exim), sendmail_path trỏ /usr/sbin/sendmail — file không tồn tại. wp_mail() luôn trả false kèm lỗi "Không thể khởi tạo hàm thư".
Đây là vấn đề có sẵn của image container, không phải do cutover sang WP thuần.
Quan trọng: luồng KHÔNG hỏng, chỉ khâu gửi hỏng
Đã kiểm chứng: yêu cầu reset mật khẩu vẫn sinh đúng user_activation_key trong DB. Đừng đi sửa luồng — chỉ cần cắm được SMTP là mọi thứ chạy.
Những email đang câm lặng
Tất cả đều đang BẬT trong WooCommerce nhưng không đi được:
| Gửi cho | |
|---|---|
customer_reset_password |
khách (quên mật khẩu) |
customer_new_account |
khách (đăng ký) |
customer_processing_order |
khách (xác nhận đơn) |
customer_completed_order |
khách (hoàn thành đơn) |
new_order |
admin@thegioikeycap.vn (thông báo đơn mới) |
2. Phần NHẬN thư — đã có sẵn (Cloudflare Email Routing)
Domain đang dùng Cloudflare Email Routing, hoạt động tốt:
- MX →
route1/2/3.mx.cloudflare.net - SPF:
v=spf1 include:_spf.mx.cloudflare.net ~all - DKIM:
cf2024-1._domainkey - DMARC:
p=none(chỉ báo cáo, không chặn) - Quy tắc:
admin@thegioikeycap.vn→ chuyển tiếpthegioikeycap@gmail.com
Cloudflare KHÔNG gửi được email. Email Routing chỉ nhận + chuyển tiếp, không có SMTP outbound. Đừng mất thời gian tìm cách gửi qua Cloudflare.
⚠️ Còn một điểm phải tự mắt xác nhận
Đã gửi thử tới admin@thegioikeycap.vn và Brevo báo delivered — nghĩa là MX của
Cloudflare đã nhận thư. Nhưng delivered chỉ chứng minh tới được Cloudflare, không
chứng minh Cloudflare đã chuyển tiếp tiếp về thegioikeycap@gmail.com: nếu địa chỉ đích
chưa verify thì Cloudflare nhận xong rồi huỷ, vẫn hiện delivered.
Token thiếu quyền đọc email/routing/addresses nên không kiểm tra được bằng API.
Cách xác nhận: mở hộp thư thegioikeycap@gmail.com, tìm thư tiêu đề
"[Test 1/2] Kiem tra chuyen tiep admin@thegioikeycap.vn". Có thư = luồng thông báo đơn
mới chạy được. Không có = vào Cloudflare → Email Routing → Destination addresses để verify.
3. Phần GỬI — Brevo (đã chọn)
Đã cài plugin FluentSMTP (free, không quảng cáo nâng cấp, có log email + nút gửi thử).
Đã cấu hình
| Mục | Giá trị |
|---|---|
| Tài khoản Brevo | thegioikeycap@gmail.com, gói free 300 thư/ngày |
| Plugin | FluentSMTP, provider Sendinblue (dùng API key v3, không qua cổng SMTP) |
| Người gửi | Thế Giới Keycap <admin@thegioikeycap.vn> |
| Domain Brevo | verified: true, authenticated: true |
| Log email | bật, giữ 14 ngày — xem ở wp-admin → FluentSMTP |
DNS đã áp trên Cloudflare:
| Bản ghi | Giá trị |
|---|---|
CNAME brevo1._domainkey |
b1.thegioikeycap-vn.dkim.brevo.com (proxied=false) |
CNAME brevo2._domainkey |
b2.thegioikeycap-vn.dkim.brevo.com (proxied=false) |
TXT @ |
brevo-code:9def7603ae497c3db8a67005e6f136dd |
TXT @ (SPF) |
v=spf1 include:_spf.mx.cloudflare.net include:spf.brevo.com ~all |
TXT _dmarc |
giữ nguyên của Cloudflare — Brevo báo status: true, không cần đổi |
Kết quả kiểm chứng
| Thư | Người nhận | Kết quả |
|---|---|---|
| Test thủ công | thegioikeycap@gmail.com | ✅ delivered + opened |
| Test chuyển tiếp | admin@thegioikeycap.vn | ✅ delivered (Cloudflare MX nhận) |
| Đặt lại mật khẩu (form thật) | thegioikeycap+resettest@gmail.com | ✅ delivered |
Nếu cần cấu hình lại từ đầu
set -a; source /srv/thegioikeycap.vn/.secrets.env; set +a
/srv/thegioikeycap.vn/docker/setup-brevo-dns.sh
Script tự hỏi Brevo lấy bản ghi cần thêm (không phải chép tay), rồi áp lên Cloudflare.
Hai chỗ script cố tình xử lý cẩn thận — đừng "đơn giản hoá":
- SPF phải GỘP, không tạo mới. Một domain chỉ được có đúng một bản ghi TXT bắt
đầu bằng
v=spf1; hai bản ghi là SPF permerror và thư vào spam. Cloudflare Email Routing đã chiếm sẵn một cái. - Không tra TXT theo
type+namerồi ghi đè. Gốc domain có nhiều TXT (SPF, brevo-code, google-site-verification). Ghi đè theo name là xoá mất SPF. Phải khớp theo nội dung (v=spf1.../brevo-code:...).
Ngoài ra: DKIM của Brevo là CNAME (brevo1/brevo2._domainkey), không phải TXT, và
phải đặt proxied=false — bật proxy thì Cloudflare trả IP của nó thay vì CNAME thật và
Brevo không xác minh được chữ ký.
Cấu hình gửi
Đang dùng provider Sendinblue của FluentSMTP (gọi thẳng API v3 bằng API key), không qua cổng SMTP — ít điểm hỏng hơn, không lo firewall chặn port 587.
Nếu muốn đổi sang SMTP thuần: host smtp-relay.brevo.com, port 587, TLS, login là email
tài khoản Brevo, password là SMTP key (không phải API key).
From phải là admin@thegioikeycap.vn — khớp domain đã xác thực thì DKIM/SPF mới align.
3b. Thiết kế email (2026-09-05)
Toàn bộ email — đơn hàng, tài khoản, kích hoạt — dùng chung một bộ nhận diện.
KHÔNG chép đè template email của WooCommerce. Template email là bảng HTML lồng nhau
viết riêng cho trình đọc mail cổ (Outlook dùng engine Word). Chép đè là ôm việc bảo trì
mỗi lần WC cập nhật và rất dễ vỡ ở client không test được. Thay vào đó inc/emails.php:
- Đặt màu qua option chuẩn — base
#ed5d47(trước là tím#8526ffmặc định của WooCommerce, lệch hẳn thương hiệu), nền#f5f2f1, chữ#3d3535, footer có hotline. - Bổ sung CSS qua filter
woocommerce_email_styles— WC tự inline bằng Emogrifier nên vẫn ăn ở Gmail/Outlook. - Bật ảnh sản phẩm 64px trong bảng đơn qua
woocommerce_email_order_items_args— khách mua keycap nhận diện bằng hình nhanh hơn bằng tên. tgk_send_html_email()cho email tự soạn của theme (kích hoạt tài khoản) dùng lạiemails/email-header.php+email-footer.phpnên trông giống hệt email đơn hàng.
Ba cái bẫy đã gặp
style_inline()nằm ởWC_Email, KHÔNG phảiWC_Emails.WC()->mailer()trả vềWC_Emails(lớp quản lý) — gọistyle_inline()trên đó là fatal "undefined method". Phải(new WC_Email())->style_inline($html). Bỏ bước inline thì email hiện trần không style.- WooCommerce đặt
border-radius: 3px !importantcho#template_container— rule của mình phải có!importantmới thắng, dù đứng sau. - ĐỪNG dùng
@example.comđể test. Địa chỉ đó không tồn tại → hard bounce, làm xấu uy tín gửi của domain. Đã lỡ tạo 4 bounce ngày 05/09. Dùng plus-addressing của hộp thư thật:thegioikeycap+test@gmail.com— vẫn về đúng inbox mà không bounce.
Cách xem trước email mà không gửi
$e = WC()->mailer()->get_emails()['WC_Email_Customer_Processing_Order'];
$e->object = wc_get_order( 123 ); $e->recipient = 'x@y.z';
file_put_contents( $path, $e->style_inline( $e->get_content_html() ) );
Rồi mở file bằng chromium để chụp. Xoá file ngay sau khi xem nếu đặt trong
wp-content/uploads/ — thư mục đó công khai, và email đơn hàng chứa địa chỉ khách thật.
4. Lưu ý về token Cloudflare
- Token là account-owned (tiền tố
cfat_) → verify qua/accounts/{id}/tokens/verify, không dùng/user/tokens/verify(trả "Invalid API Token" gây hiểu nhầm là token sai). - Token bị chặn theo IP: IPv6 của server (
2a0d:f302:153:85e5::1) không nằm trong danh sách cho phép → lỗicode 9109. Mọi lệnh curl phải ép-4. - Đã kiểm chứng token ghi được DNS (tạo + xoá thử một TXT tạm).
- Token đã đi qua kênh chat → nên thu hồi và tạo lại sau khi cấu hình xong.
5. Kiểm thử sau khi cắm SMTP
# 1. wp_mail có gửi được không
docker exec keycap_php wp --allow-root --path=/srv/thegioikeycap.vn/www eval '
add_action("wp_mail_failed", fn($e) => print("LỖI: ".$e->get_error_message()."\n"));
var_dump(wp_mail("thegioikeycap@gmail.com", "Test", "Thư thử từ website"));'
# 2. Thư tới admin@ có được Cloudflare chuyển tiếp không
# → gửi thử tới admin@thegioikeycap.vn, xem có về thegioikeycap@gmail.com
Sau đó đặt 1 đơn test để xác nhận email xác nhận đơn về được hộp thư khách.
Nhớ tắt tạm plugin send-app trước khi đặt đơn test — nó hook woocommerce_new_order và sẽ bắn thông báo thật.
6. Email "Huỷ đơn hàng → khách" — CỐ Ý TẮT
Trạng thái: TẮT. WooCommerce mặc định tắt; đã bật thử rồi tắt lại theo yêu cầu chủ shop (06/09/2026).
Lý do quan trọng, đừng bật lại nếu chưa cân nhắc: shop đang đặt "Tạm giữ tồn kho 60 phút", nên đơn chuyển khoản chưa thanh toán sẽ tự huỷ sau 60 phút. Bật email này lên là mỗi khách chưa kịp chuyển khoản đều nhận một thư báo huỷ — dễ hiểu nhầm thành shop từ chối bán.
Đã kiểm chứng cơ chế tự huỷ chạy thật: ghi chú đơn #1224 — "Tạm giữ tồn kho trong 60 phút được áp dụng" → huỷ sau đó.
Muốn bật lại thì nên viết nội dung riêng phân biệt hai trường hợp (tự huỷ vì quá hạn thanh toán / shop chủ động huỷ). Nội dung mặc định của WooCommerce chỉ nói cụt là đơn đã huỷ.
7. Tiêu đề email theo từng mốc đơn hàng
Từng gặp lỗi ở đơn #1730 (06/09/2026): hai email khác mốc mà trùng tiêu đề và trùng nội dung, khách không biết đâu là xác nhận đã thanh toán.
Nay mỗi mốc có tiêu đề riêng, và mốc "đang xử lý" tách nhánh theo hình thức thanh toán:
| Mốc | Tiêu đề |
|---|---|
| Chờ thanh toán | Đang chờ thanh toán |
| Đang xử lý — COD | Đã xác nhận đơn hàng {số} — đang chuẩn bị giao |
| Đang xử lý — chuyển khoản | Đã nhận thanh toán — đơn hàng {số} đang được chuẩn bị |
| Hoàn thành | Đơn hàng {số} đã giao |
Code: inc/emails.php, filter woocommerce_email_subject_*.
⚠️ Mỗi filter đổi nội dung email HTML đều phải đi kèm một filter
woocommerce_email_headers đặt Content-Type: text/html, nếu không khách nhận
được mã HTML thô.
Email hệ thống WordPress (tài khoản mới, đổi mật khẩu, đổi email) và 3 email tồn
kho nằm ở inc/emails-system.php. Hiện 23/24 email đã thương hiệu hoá — cái
còn lại là email huỷ đơn đang tắt ở §6.