LumiBaseDocs

Email Service

Tình trạng khả dụng: Module email HTTP đầy đủ mô tả ở đây — templates, layouts, preview, và POST /api/v1/email/send — có trong các image CMS gần đây. Image cũ hơn (được báo cáo khoảng 0.5.0) chỉ có kênh security-notification nội bộ và không expose các route /api/v1/email/*; hãy kiểm tra version image đang deploy nếu /email/send trả 404.

Mục tiêu: cung cấp một dịch vụ gửi email dùng chung ở tầng core, có template/layout store, để Studio và các extension đều dùng được — hoặc cấu hình hoàn toàn bằng env nếu không cần UI.

1. Kiến trúc tổng quan

LumiBase tách email thành hai tầng rõ ràng:

TầngTrách nhiệmVị trí
Transport (gửi byte)SMTP (Nodemailer) cho Docker/Node, MailChannels HTTP cho Cloudflare Workersapps/cms/src/services/email/transport.ts
EmailServiceChọn transport theo runtime, áp default from/replyTo, expose send()apps/cms/src/services/email/email-service.ts
Render engineThay biến {{var}} an toàn (escape HTML mặc định), ghép body vào layoutapps/cms/src/services/email/render.ts
Template/layout storeBảng email_templates, email_layouts (site-scoped, RLS)packages/database/src/schema/platform.ts
HTTP moduleCRUD + preview + send dưới /api/v1/email/*apps/cms/src/modules/email/
UITrang Studio quản lý template/layout + gửi testapps/studio/src/modules/settings/email-page.tsx

Luồng gửi: caller (UI hoặc extension) → POST /api/v1/email/send → module render template (nếu có templateKey) → EmailService.send() → transport phù hợp với runtime → audit log (email_sent / email_send_failed).

Kênh security-notification cũ (modules/notifications/email-channel.ts) nay dùng chung transport này; nó chỉ còn giữ template subject/body cố định theo spec (Req 13.2).

2. Cấu hình bằng env (không cần UI)

EmailService cấu hình hoàn toàn qua biến môi trường. Thêm vào apps/cms/.dev.vars (local) hoặc wrangler secret put (deploy).

BiếnBắt buộcMô tả
LUMIBASE_SMTP_URLDocker: cóChuỗi kết nối SMTP dạng nodemailer, vd smtps://user:[email protected]:465. Không set ⇒ email ở chế độ degraded (không gửi).
LUMIBASE_MAIL_FROMnên cóĐịa chỉ gửi mặc định. Mặc định fallback [email protected].
LUMIBASE_MAIL_REPLY_TOkhôngReply-To mặc định.
LUMIBASE_MAIL_ENABLEDkhôngĐặt "false" để tắt toàn bộ gửi email (kill switch).
LUMIBASE_RUNTIMEkhông"cloudflare" ⇒ dùng MailChannels; còn lại ⇒ SMTP. Mặc định "docker".

Quyết định transport

code
LUMIBASE_RUNTIME === 'cloudflare' → MailChannels (HTTP)
ngược lại                         → SMTP qua LUMIBASE_SMTP_URL (null nếu chưa set → degraded)

Deliverability: trên Cloudflare/MailChannels cần cấu hình SPF/DKIM/DMARC ở tầng DNS; adapter không tự kiểm tra. Trên SMTP, deliverability phụ thuộc nhà cung cấp.

Chế độ degraded

Khi không có transport (Workers thiếu MailChannels, hoặc Docker thiếu LUMIBASE_SMTP_URL, hoặc LUMIBASE_MAIL_ENABLED=false), EmailService.fromEnv() trả về null. Khi đó:

  • GET /api/v1/email/capabilities trả configured: false để UI hiển thị cảnh báo.
  • POST /api/v1/email/send trả 503 EMAIL_NOT_CONFIGURED.
  • Luồng mời teammate degrade im lặng (vẫn tạo user invited, chỉ không gửi mail).

3. Template & layout

  • Layout = vỏ HTML tái dùng, bắt buộc chứa slot {{content}}. Branding/header/footer/style đặt một chỗ.
  • Template = thông điệp có địa chỉ (key, vd teammate_invite) gồm subject + bodyHtml, tùy chọn bodyText, tùy chọn gắn một layout.

Cú pháp biến (render engine)

Cú phápHành vi
{{ name }}Thay + escape HTML (mặc định, an toàn cho biến không tin cậy).
{{{ name }}}Thay không escape (chỉ dùng cho HTML đã tin cậy).
{{content}}Slot trong layout để chèn body template đã render.

Biến được tham chiếu nhưng không có trong variables ⇒ render thành chuỗi rỗng và được gom vào missing (không bao giờ để lại literal {{x}} trong mail). Nếu không có bodyText, engine tự suy ra text/plain từ HTML.

4. HTTP API

Mount dưới /api/v1/email/*, trong stack đã xác thực, gated bởi requireSiteAdmin(). Mọi query đều scoped theo site_id. Envelope chuẩn { data } / { errors: [...] }.

MethodPathMô tả
GET/email/capabilitiesBáo transport có sẵn + from.
GET/POST/email/layoutsList / tạo layout.
PATCH/DELETE/email/layouts/:idSửa / xóa layout.
GET/POST/email/templatesList / tạo template.
PATCH/DELETE/email/templates/:idSửa / xóa template.
POST/email/templates/:key/previewRender thử (không gửi), trả { subject, html, text, missing }.
POST/email/sendRender (nếu templateKey) + gửi. Điểm tích hợp cho extension.
POST/email/testGửi một mail test tới một địa chỉ.

POST /email/send

jsonc
{
  "to": ["[email protected]"],          // bắt buộc, 1..50
  "cc": ["[email protected]"],              // tùy chọn
  "replyTo": "[email protected]",        // tùy chọn
  // chọn ĐÚNG MỘT trong hai:
  "templateKey": "teammate_invite",        // render template đã lưu
  "inline": { "subject": "Hi", "html": "<p>…</p>", "text": "…" },
  "variables": { "name": "Sam" }
}

Phản hồi: { data: { sent: true, subject } }, hoặc 502 DELIVERY_FAILED (kèm retryable), 404 NOT_FOUND (template), 503 EMAIL_NOT_CONFIGURED.

5. Quản lý qua UI

Studio → Settings → Email:

  • Status: hiển thị transport đã cấu hình hay chưa.
  • Templates: tạo/sửa key, subject, layout, body HTML; pane Preview gọi /preview với JSON biến mẫu, render trong iframe; cảnh báo biến thiếu.
  • Layouts: vỏ HTML có slot {{content}}.
  • Send test: gửi mail test nhanh.

6. Mời teammate (invite email)

Route POST /api/v1/users/invite sau khi tạo bản ghi invited sẽ gửi email best-effort (qua ctx.waitUntil trên Workers, fire-and-forget trên Node — không bao giờ làm hỏng invite):

  • Nếu site có template teammate_invite (enabled) ⇒ dùng nó.
  • Nếu không ⇒ dùng message dựng sẵn trong apps/cms/src/modules/email/invite.ts.

Lưu ý: invite trong wizard setup vẫn chỉ tạo bản ghi, không gửi email — vì ở thời điểm setup transport thường chưa cấu hình. Muốn tùy biến nội dung, tạo template teammate_invite trong Studio rồi mời lại qua trang Users.

7. Tích hợp từ extension

Extension không tự gửi SMTP — nó gọi POST /api/v1/email/send của core với một templateKey. Xem ví dụ đầy đủ ở examples/extension-email-setup và hướng dẫn ở contributing/extension-dev.md.

8. Bảo mật & ghi chú

  • Template không bao giờ nhúng secret: payload không mang password hash/token (đảm bảo bởi kiểu dữ liệu).
  • Biến được escape HTML mặc định ⇒ tránh injection từ dữ liệu người dùng.
  • Mọi lần gửi đều ghi audit (email_sent / email_send_failed) kèm templateKey, số người nhận, subject (không log nội dung body).
Last modified: 26/09/2026