⌨️ Thế Giới Keycap Docs Mail

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:

Email 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ếp thegioikeycap@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á":

  1. 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.
  2. Không tra TXT theo type+name rồ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:

  1. Đặt màu qua option chuẩn — base #ed5d47 (trước là tím #8526ff mặc định của WooCommerce, lệch hẳn thương hiệu), nền #f5f2f1, chữ #3d3535, footer có hotline.
  2. Bổ sung CSS qua filter woocommerce_email_styles — WC tự inline bằng Emogrifier nên vẫn ăn ở Gmail/Outlook.
  3. 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.
  4. tgk_send_html_email() cho email tự soạn của theme (kích hoạt tài khoản) dùng lại emails/email-header.php + email-footer.php nên trông giống hệt email đơn hàng.

Ba cái bẫy đã gặp

  1. style_inline() nằm ở WC_Email, KHÔNG phải WC_Emails. WC()->mailer() trả về WC_Emails (lớp quản lý) — gọi style_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.
  2. WooCommerce đặt border-radius: 3px !important cho #template_container — rule của mình phải có !important mới thắng, dù đứng sau.
  3. ĐỪ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ỗi code 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.