Skip to main content

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).

Dokumen pendamping

Spec endpoint + payload schema: architecture.md. Detail infrastruktur: architecture/infrastruktur_kesles_merchant.md §5.3.


1. Identitas Service

ItemValue
Nama serviceemail_service
Port127.0.0.1:8094 (loopback — hanya caller di VM yang sama)
Main DBdb_kesles_merchant_notification
Schema / tabelemail.email_messages, email.email_send_attempts
Sourceservices/email_service/
Provider SMTPGmail SMTP (smtp.gmail.com:587, provider tercatat smtp_gmail)

2. Tanggung Jawab

  • Menerima request kirim email dari caller internal (merchant_core_api) via POST /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 (status queuedsent/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.

EndpointMethodAuthCatatan
/healthGETLiveness — selalu 200; JSON {service, status, smtp_configured, postgres_configured, app_env}
/readyGETReadiness — cek SMTP configured + Postgres ping; 503 bila salah satu tidak siap
/internal/email/sendPOSTEksekusi SMTP + persist row. Di-wrap idempotency middleware (Redis). Body {template, to_email, payload}
/internal/email/logPOSTAudit-only INSERT row (tanpa kirim). Dipertahankan untuk tooling; tidak dipakai caller runtime
/internal/email/messages/{id}GETLookup status pesan by UUID; 404 bila tidak ditemukan
/internal/email/templatesGET/POSTTemplate catalog list/create (proxy dashboard)
/internal/email/templates/{id}GET/PATCH/...Template catalog item by id
/internal/email/message-logsGETMessage logs read-only untuk panel dashboard
/internal/email/overviewGETKPI overview read-only (panel Email Overview & Health)
/internal/email/send-volumeGETTimeseries volume kirim untuk chart
/internal/email/provider-healthGETStatus 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 clientTemplateCaller (file)
SendOTPotpinternal/auth/otp_service.go (OTP mobile via email)
SendOTPDashboardotpinternal/auth/dashboard_auth_service.go (2FA dashboard)
SendOTPProfileotpinternal/auth/profile_auth_service.go (verifikasi email profil)
SendPasswordResetpassword_resetinternal/auth/dashboard_auth_service.go
SendDeviceLoginAlertdevice_login_alertinternal/auth/otp_service.go
SendStaffInvitationstaff_invitationinternal/staffinvitations/service.go
SendMobileUserWelcomemobile_user_welcomeinternal/httpapi/internal_manual_registration_handlers.go
SendDashboardUserWelcomedashboard_user_welcomeinternal/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

ItemValue
Folder/home/enalfarid/kesles_merchant/merchant_email/
Binaryemail-service (Linux ELF amd64, stripped)
BuildCGO_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 unitemail-service.service (User=enalfarid, Restart=on-failure, RestartSec=10)
Port127.0.0.1:8094 (loopback)
Logssudo journalctl -u email-service.service [-f] [--since="1 hour ago"]

7. Konfigurasi (Env)

EnvWajibKeterangan
APP_ENVya (prod: production)menentukan file .env.<APP_ENV> yang di-load
APP_PORTdefault 8094port HTTP
INTERNAL_NOTIFICATION_API_KEYya (prod)shared-secret auth /internal/*; harus = EMAIL_SERVICE_API_KEY di core_api
POSTGRES_DSNya (prod)DSN ke db_kesles_merchant_notification
SMTP_HOSTdefault smtp.gmail.comhost SMTP
SMTP_PORTdefault 587port SMTP
SMTP_USERNAME / SMTP_PASSWORDyakredensial SMTP
SMTP_FROM_NAMEdefault Kesles Merchantnama pengirim
SMTP_FROM_EMAILyaalamat pengirim
EMAIL_RETRY_MAX / EMAIL_RETRY_INITIAL_BACKOFF_MS / EMAIL_RETRY_MAX_BACKOFF_MSdefault 3 / 500 / 10000retry policy
REDIS_ADDR (+ REDIS_USERNAME/REDIS_PASSWORD/REDIS_DB)opsionalidempotency 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: requireInternalKey membandingkan X-Internal-API-Key dengan subtle.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) + Shutdown dengan window agar in-flight request selesai sebelum stop.
  • Body limit: http.MaxBytesReader 256 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): sanitizeHeaderValue strip \r/\n pada From-Name, To, Subject; to_email divalidasi via net/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 dengan DialContext + per-step deadline + watchdog conn.Close() saat ctx.Done() agar goroutine tidak ter-orphan saat SMTP lambat (lifetime terbatas oleh context.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-view notification.* sudah di-drop via mig 012)
  • Tabel: email.email_messages (writer + reader), email.email_send_attempts
  • Store: services/email_service/internal/store/postgres.goInsert / MarkSent / MarkFailed dengan 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:

KomponenPath
Backendservices/dashboard_api/internal/app/dashboard_dev_email_bulk.go (POST /api/dashboard/dev/email-bulk/execute, net/smtp stdlib)
UIapps/merchant_dashboard/.../panels/email_bulk_messaging_panel.dart (Sidebar → Email API → Bulk Messaging)
Operator guidemerchant_docs/docs/operations/bulk-messaging-operator-guide.md

11. Cross-Reference

PathRole
architecture.mdSpec endpoint + payload schema + transport env + deployment
architecture/infrastruktur_kesles_merchant.md§5.3 detail service
architecture/notification-event-catalog.mdKatalog event master