Email Service
email_service adalah service notifikasi email Kesles Merchant — standalone Go service yang mengeksekusi pengiriman email transaksional (OTP, password reset, device login alert, staff invitation, welcome) via SMTP dan mempersist setiap pengiriman ke database notification. Service ini live di produksi dan menjadi satu-satunya jalur kirim email; merchant_core_api memanggilnya lewat HTTP internal (tidak ada lagi SMTP embedded di core_api).
Spec endpoint + payload schema: architecture.md. Detail infrastruktur: architecture/infrastruktur_kesles_merchant.md §5.3.
1. Identitas Service
| Item | Value |
|---|---|
| Nama service | email_service |
| Port | 127.0.0.1:8094 (loopback — hanya caller di VM yang sama) |
| Main DB | db_kesles_merchant_notification |
| Schema / tabel | email.email_messages, email.email_send_attempts |
| Source | services/email_service/ |
| Provider SMTP | Gmail SMTP (smtp.gmail.com:587, provider tercatat smtp_gmail) |
2. Tanggung Jawab
- Menerima request kirim email dari caller internal (
merchant_core_api) viaPOST /internal/email/send. - Render template HTML (6 template) dari nama template + payload map.
- Eksekusi pengiriman SMTP ke Gmail.
- Persist setiap pesan ke
email.email_messages(statusqueued→sent/failed) untuk audit + lookup status. - Menyediakan endpoint read-only (overview, message logs, send volume, provider health) dan CRUD template catalog untuk panel dashboard.
3. Endpoint Inventory
Semua endpoint /internal/* membutuhkan header X-Internal-API-Key yang valid dan dibatasi body 256 KiB.
| Endpoint | Method | Auth | Catatan |
|---|---|---|---|
/health | GET | — | Liveness — selalu 200; JSON {service, status, smtp_configured, postgres_configured, app_env} |
/ready | GET | — | Readiness — cek SMTP configured + Postgres ping; 503 bila salah satu tidak siap |
/internal/email/send | POST | ✅ | Eksekusi SMTP + persist row. Di-wrap idempotency middleware (Redis). Body {template, to_email, payload} |
/internal/email/log | POST | ✅ | Audit-only INSERT row (tanpa kirim). Dipertahankan untuk tooling; tidak dipakai caller runtime |
/internal/email/messages/{id} | GET | ✅ | Lookup status pesan by UUID; 404 bila tidak ditemukan |
/internal/email/templates | GET/POST | ✅ | Template catalog list/create (proxy dashboard) |
/internal/email/templates/{id} | GET/PATCH/... | ✅ | Template catalog item by id |
/internal/email/message-logs | GET | ✅ | Message logs read-only untuk panel dashboard |
/internal/email/overview | GET | ✅ | KPI overview read-only (panel Email Overview & Health) |
/internal/email/send-volume | GET | ✅ | Timeseries volume kirim untuk chart |
/internal/email/provider-health | GET | ✅ | Status provider untuk card Provider health |
4. Wiring Caller (core_api → email_service)
merchant_core_api memanggil service via HTTP client internal/emailservice/client.go (emailServiceClient). Tidak ada dispatcher/feature-flag/fallback SMTP lagi — setiap caller memanggil method wrapper yang langsung melakukan POST /internal/email/send.
| Method client | Template | Caller (file) |
|---|---|---|
SendOTP | otp | internal/auth/otp_service.go (OTP mobile via email) |
SendOTPDashboard | otp | internal/auth/dashboard_auth_service.go (2FA dashboard) |
SendOTPProfile | otp | internal/auth/profile_auth_service.go (verifikasi email profil) |
SendPasswordReset | password_reset | internal/auth/dashboard_auth_service.go |
SendDeviceLoginAlert | device_login_alert | internal/auth/otp_service.go |
SendStaffInvitation | staff_invitation | internal/staffinvitations/service.go |
SendMobileUserWelcome | mobile_user_welcome | internal/httpapi/internal_manual_registration_handlers.go |
SendDashboardUserWelcome | dashboard_user_welcome | internal/httpapi/internal_notification_handlers.go |
Wiring di core_api: EMAIL_SERVICE_BASE_URL=http://127.0.0.1:8094 + EMAIL_SERVICE_API_KEY (harus identik dengan INTERNAL_NOTIFICATION_API_KEY di email_service).
Alur kirim
core_api caller → emailServiceClient.SendXxx()
└→ HTTP POST http://127.0.0.1:8094/internal/email/send
├→ X-Internal-API-Key: <key>
└→ body: {template, to_email, payload: {...}}
└→ email_service:
├→ INSERT email.email_messages (provider=smtp_gmail, status=queued)
├→ render template + deliverViaSMTP() → Gmail
├→ MarkSent / MarkFailed
└→ return {status, template, message_id}
Bila email_service down, email tidak terkirim (tidak ada fallback path di core_api).
5. Template
6 file HTML di services/email_service/internal/email/templates/:
otp.html · password_reset.html · device_login_alert.html · staff_invitation.html · mobile_user_welcome.html · dashboard_user_welcome.html
6. VM Deployment
| Item | Value |
|---|---|
| Folder | /home/enalfarid/kesles_merchant/merchant_email/ |
| Binary | email-service (Linux ELF amd64, stripped) |
| Build | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o dist/email-service ./cmd/server |
| Env file | .env.production (gitignored — production secret) |
| Systemd unit | email-service.service (User=enalfarid, Restart=on-failure, RestartSec=10) |
| Port | 127.0.0.1:8094 (loopback) |
| Logs | sudo journalctl -u email-service.service [-f] [--since="1 hour ago"] |
7. Konfigurasi (Env)
| Env | Wajib | Keterangan |
|---|---|---|
APP_ENV | ya (prod: production) | menentukan file .env.<APP_ENV> yang di-load |
APP_PORT | default 8094 | port HTTP |
INTERNAL_NOTIFICATION_API_KEY | ya (prod) | shared-secret auth /internal/*; harus = EMAIL_SERVICE_API_KEY di core_api |
POSTGRES_DSN | ya (prod) | DSN ke db_kesles_merchant_notification |
SMTP_HOST | default smtp.gmail.com | host SMTP |
SMTP_PORT | default 587 | port SMTP |
SMTP_USERNAME / SMTP_PASSWORD | ya | kredensial SMTP |
SMTP_FROM_NAME | default Kesles Merchant | nama pengirim |
SMTP_FROM_EMAIL | ya | alamat pengirim |
EMAIL_RETRY_MAX / EMAIL_RETRY_INITIAL_BACKOFF_MS / EMAIL_RETRY_MAX_BACKOFF_MS | default 3 / 500 / 10000 | retry policy |
REDIS_ADDR (+ REDIS_USERNAME/REDIS_PASSWORD/REDIS_DB) | opsional | idempotency dedupe; kosong = middleware no-op |
Env file di-load via godotenv.Load (bukan Overload) sehingga env yang di-inject systemd tidak ditimpa file.
8. Security & Reliability (Konfigurasi Saat Ini)
- Auth constant-time:
requireInternalKeymembandingkanX-Internal-API-Keydengansubtle.ConstantTimeCompare(timing-safe); request tetap ditolak bila key tidak di-set. - HTTP timeout guards:
ReadHeaderTimeout 5s,ReadTimeout 30s,WriteTimeout 30s,IdleTimeout 120s(mitigasi Slowloris/slow client). - Graceful shutdown:
signal.NotifyContext(SIGTERM/SIGINT) +Shutdowndengan window agar in-flight request selesai sebelum stop. - Body limit:
http.MaxBytesReader256 KiB membungkus seluruh endpoint/internal/*. - Postgres pool:
MaxOpenConns 25,MaxIdleConns 5,ConnMaxLifetime 5m(ConnMaxIdleTime 5m). - Driver:
pgx/v5/stdlib(sql.Open("pgx", ...)). - Email header injection (CRLF):
sanitizeHeaderValuestrip\r/\npada From-Name, To, Subject;to_emaildivalidasi vianet/mail.ParseAddress→ 400 bila invalid. - Error sanitization: error SMTP dan error lookup DB tidak dibocorkan ke caller (response generik, detail hanya di slog + persisted ke
MarkFailed). - SMTP delivery watchdog:
deliverViaSMTP()low-level denganDialContext+ per-step deadline + watchdogconn.Close()saatctx.Done()agar goroutine tidak ter-orphan saat SMTP lambat (lifetime terbatas olehcontext.WithTimeout). - Idempotency: middleware Redis-backed pada
/internal/email/send(fail-open per-request bila Redis tidak reachable).
9. Database
- DB:
db_kesles_merchant_notification - Schema:
email(hasil split per-service; compat-viewnotification.*sudah di-drop via mig012) - Tabel:
email.email_messages(writer + reader),email.email_send_attempts - Store:
services/email_service/internal/store/postgres.go—Insert/MarkSent/MarkFaileddengan slog error eksplisit (email_mark_*_failed/email_insert_failed) - Migrations:
merchant_database/db_kesles_merchant_notification/migrations/v1/(003_email_messages.sql,010_split_service_schemas.sql,011_template_catalog.sql,012_drop_notification_compat_views.sql)
Provider yang tercatat pada row produksi = smtp_gmail (bukti pengiriman dieksekusi oleh service).
10. Bulk Messaging (Jalur Terpisah)
Untuk broadcast/reminder operasional masih ada jalur paralel di dashboard_api (super-admin only), terpisah dari email_service:
| Komponen | Path |
|---|---|
| Backend | services/dashboard_api/internal/app/dashboard_dev_email_bulk.go (POST /api/dashboard/dev/email-bulk/execute, net/smtp stdlib) |
| UI | apps/merchant_dashboard/.../panels/email_bulk_messaging_panel.dart (Sidebar → Email API → Bulk Messaging) |
| Operator guide | merchant_docs/docs/operations/bulk-messaging-operator-guide.md |
11. Cross-Reference
| Path | Role |
|---|---|
architecture.md | Spec endpoint + payload schema + transport env + deployment |
architecture/infrastruktur_kesles_merchant.md | §5.3 detail service |
architecture/notification-event-catalog.md | Katalog event master |