Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Codeverta Relay

Platform email self-hosted (Go + React + MariaDB) yang menggabungkan mail server multi-domain ala cPanel dan Sending API ala Amazon SES/Postmark dalam satu binary cvrelay. Lisensi MIT.

Status: v0.8: Fondasi + Sending API + SMTP relay + bounce async, complaint, jeda otomatis + DNS otomatis via Cloudflare + throttle, warmup, relay fallback, pemantauan blocklist + mailbox hosting (terima surat di port 25, baca lewat IMAP, kirim dari Outlook/HP dengan password mailbox, filter spam). Webmail ala Gmail sedang dikerjakan (rencana di Roadmap); POP3, Sieve, dan import IMAP belum ada. Lihat Roadmap.

Yang sudah jalan

Area Isi
Multi-tenant Tenant, kuota harian, pause, isolasi data per tenant_id di setiap query (dites)
API key cv_live_ / cv_test_, scope, batasan stream dan IP, disimpan sebagai hash
Domain DNS wizard (DKIM RSA-2048 + Ed25519, SPF, return-path, DMARC, MX), verifikasi DNS, rotasi DKIM dengan overlap 48 jam
DNS otomatis Tombol "Pasang via Cloudflare": record dibuat lewat API Cloudflare (token platform atau token per klien, terenkripsi). SPF yang ada digabung, DMARC dan MX milik klien tidak ditimpa. Cek DNS terjadwal tiap 10 menit sampai terverifikasi (maks 7 hari), lalu harian; DNS yang rusak setelah terverifikasi ditandai "DNS bermasalah" dan pengiriman dari domain itu dihentikan sampai diperbaiki (error DNS sementara diabaikan)
Sending API POST /v1/messages, batch 500, template, lampiran, scheduled_at + batal, Idempotency-Key 24 jam, mode test
SMTP relay Port 587 (STARTTLS) dan 465 (TLS implisit), AUTH PLAIN/LOGIN dengan username apikey, header X-CV-Stream/Tags/Metadata, blokir IP setelah 5 login gagal. Pengguna mailbox juga bisa mengirim dari Outlook/HP dengan alamat + password mailbox: From dibatasi ke alamat dan aliasnya, batas 500 penerima/hari (bisa diubah), jeda otomatis bila jauh melewati batas atau banyak hard bounce/complaint, unsubscribe newsletter tidak memblokir email pribadi, sesi yang terbuka putus saat password diganti, login gagal lewat SMTP dan IMAP dihitung bersama
Antrean & MTA Antrean per Recipient di MariaDB (FOR UPDATE SKIP LOCKED + lease), prioritas transactional, MX lookup + fallback A, null MX, TLS oportunistik, DKIM dual signing, VERP return-path
Retry & bounce 1m, 5m, 15m, 30m, 1j, 2j, 4j, lalu tiap 4 jam sampai 5 hari; klasifikasi hard/soft/block/mailbox_full/policy; hard bounce masuk suppression
Suppression Per tenant + global, manual, hard bounce, complaint, one-click unsubscribe (RFC 8058) untuk stream broadcast
Bounce async & complaint Port 25 menerima DSN ke alamat VERP b+<token>@bounce.<domain> (token HMAC, alamat palsu ditolak) dan laporan ARF ke fbl@<platform>; dicocokkan lewat VERP atau header X-CV-Ref
Throttle per provider Batas kirim per menit untuk Gmail, Yahoo, Microsoft, dan lainnya (provider dikenali dari host MX, jadi Google Workspace ikut Gmail); provider dijeda 15 menit bila membalas rate-limit (misal 421 4.7.28). Pesan yang ditahan tidak dihitung percobaan
Warmup IP Kuota harian per provider mengikuti kurva (50, 100, 300, 500, 1rb, ... per hari); broadcast hanya boleh 70% kuota sehingga transactional selalu kebagian; kelebihan ditahan sampai besok
Relay fallback Smart host eksternal (Mailgun/Brevo/SES SMTP) untuk semua atau provider tertentu, AUTH + STARTTLS, password terenkripsi; bisa aktif otomatis saat IP masuk blocklist
Pemantauan blocklist IP dicek tiap jam di Spamhaus ZEN, Barracuda, SpamCop; listing dicatat di audit log. Jawaban 127.255.255.x (query ditolak) dianggap "unknown", bukan listing
Jeda otomatis Tenant dijeda bila hard bounce > 5% (minimal 5) atau complaint > 0,1% (minimal 3) dalam 24 jam, setelah 50 penerima; ambang bisa diatur per tenant, angka sebelum admin mengaktifkan ulang tidak dihitung, dicatat di audit log, alasan tampil di panel
Event & webhook queued, sent, deferred, bounced, rejected, unsubscribed; webhook HMAC-SHA256, retry sampai 24 jam, dead-letter, replay, proteksi SSRF
Mailbox hosting Mailbox per alamat (password argon2id, kuota default 2 GB), alias ke beberapa mailbox, catch-all *@domain, sub-address nama+tag@. Port 25 menerima surat untuk domain hosted yang terverifikasi (bukan open relay), menambah Return-Path, Received, Authentication-Results (DKIM) dan membuang header palsu atas nama server ini; header > 256 KB ditolak. Kuota dicek saat RCPT (552)
Filter spam Surat masuk dinilai: SPF (evaluator sendiri, RFC 7208), DKIM, DMARC (relaxed/strict alignment, sp=, pct=), DNSBL (Spamhaus ZEN, Barracuda, SpamCop), pola header (From ganda/rusak, nama tampilan menyamar). Skor ≥ 6 masuk Junk, ≥ 15 (misal pemalsuan domain berkebijakan p=reject) ditolak 550; DNS gagal sementara pada domain p=reject dijawab 451. Header Authentication-Results (spf/dkim/dmarc) dan X-Spam-Status per mailbox. rspamd opsional (CVRELAY_RSPAMD_URL). Daftar izin/blokir pengirim per tenant atau per mailbox (izin hanya berlaku bila domain pengirim terbukti; blokir juga mencocokkan envelope). Junk dan Trash dikosongkan otomatis 30 hari setelah pesan masuk ke sana
IMAP Port 993 (TLS) dan 143 (STARTTLS), login tanpa TLS ditolak. IMAP4rev1 + IDLE, UIDPLUS, MOVE, SPECIAL-USE, NAMESPACE, APPENDLIMIT. Folder bawaan INBOX/Sent/Drafts/Trash/Junk/Archive. Beberapa perangkat sekaligus tetap sinkron (EXISTS/EXPUNGE/FLAGS). Login gagal 10x per 15 menit per IP (IPv6 per /64) diblokir; ganti password atau nonaktifkan mailbox memutus sesi yang terbuka
Panel React console (embed di binary) untuk tim Codeverta: tenant, domain & DNS, mailbox & alias, aktivitas + timeline, template + preview, API key, webhook, suppression

Mulai cepat (Docker)

cp .env.example .env
docker compose run --rm cvrelay ctl genkey     # isi ke CVRELAY_MASTER_KEY
# isi juga CVRELAY_ADMIN_TOKEN, password MariaDB, domain platform
docker compose up -d
open http://localhost:8080                      # login dengan CVRELAY_ADMIN_TOKEN

Membuat tenant dan API key lewat CLI:

docker compose exec cvrelay cvrelay ctl tenant create "Toko ABC"
docker compose exec cvrelay cvrelay ctl apikey create tnt_01J... --scopes=send,read_logs

Mailbox: terima email dan buka di Outlook / HP

  1. Tambah domain dengan tipe hosted atau both, pasang record DNS (termasuk MX 10 <CVRELAY_HOSTNAME>), lalu verifikasi.
  2. Buat mailbox di panel (menu Mailbox) atau lewat API:
curl -X POST https://relay.codeverta.com/v1/mailboxes -H "Authorization: Bearer $KEY" \
  -d '{"address":"contact@client1.com","name":"Kontak","password":"rahasia-panjang","quota_mb":2048}'
  1. Di Outlook / iPhone / Android, tambah akun IMAP:
Server masuk CVRELAY_HOSTNAME, port 993 (SSL/TLS) atau 143 (STARTTLS)
Username alamat lengkap, misal contact@client1.com
Password password mailbox

| Server keluar | CVRELAY_HOSTNAME, port 587 (STARTTLS) atau 465 (SSL/TLS), dengan autentikasi username + password yang sama |

Sertifikat TLS (CVRELAY_TLS_CERT / CVRELAY_TLS_KEY) wajib untuk IMAP dan SMTP. Salinan di Sent disimpan oleh Outlook/HP sendiri lewat IMAP.

Menerima cukup dengan MX + DKIM. Untuk mengirim dari domain bertipe hosted, pasang juga SPF dan return-path (bounce.<domain>) lalu jalankan Cek DNS; sampai itu ada, Outlook mendapat penolakan yang menjelaskan record yang kurang. Batas kirim harian dihitung per hari UTC (reset pukul 07.00 WIB), sama seperti kuota tenant. Bounce dari email pribadi dikirim sebagai pemberitahuan "Tidak terkirim" ke INBOX pengirim, dan alamat yang pernah hard bounce ditolak langsung saat mengirim.

Kirim email

curl https://relay.codeverta.com/v1/messages \
  -H "Authorization: Bearer cv_live_..." \
  -H "Idempotency-Key: order-12345-receipt" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Toko ABC <no-reply@tokoabc.id>",
    "to": ["budi@example.com"],
    "subject": "Invoice #12345",
    "html": "<p>Halo Budi, total Rp 250.000</p>",
    "tags": ["invoice"],
    "metadata": {"order_id": "12345"}
  }'
# 202 {"id":"msg_01J...","status":"queued"}

Spesifikasi lengkap: api/openapi.yaml.

Deliverability (halaman admin)

Daftarkan IP publik server di panel (Deliverability → IP pengirim) supaya warmup dan pemantauan blocklist berjalan. Catatan: Spamhaus menolak query dari resolver publik (Google/Cloudflare DNS); pakai resolver sendiri (misal unbound di server) atau Spamhaus DQS, kalau tidak hasilnya akan "unknown".

Cloudflare

Set CVRELAY_CLOUDFLARE_TOKEN dengan token API (izin Zone:Read dan DNS:Edit untuk zone klien) bila zone klien ada di akun Cloudflare Codeverta. Klien yang zone-nya di akun Cloudflare sendiri bisa diberi token per klien dari panel (halaman Domain & DNS); token itu diverifikasi ke Cloudflare lalu disimpan terenkripsi dan dipakai menggantikan token platform.

Token platform hanya bisa dipakai super admin dari panel, karena token itu menjangkau zone banyak klien (ADR 0005). API key klien hanya bisa memakai token Cloudflare milik klien itu sendiri. Zone platform tidak pernah diubah, dan zone yang sudah dipakai domain terverifikasi milik klien lain ditolak.

Aturan pemasangan: DKIM, return-path (CNAME, tanpa proxy) dan MX dibuat bila belum ada; SPF yang sudah ada digabung dengan include:_spf.<platform> sebelum mekanisme all (tidak peka huruf besar, menghormati modifier), tetapi dibatalkan sebagai conflict bila total DNS lookup (dihitung sampai include bertingkat) akan melebihi 10 atau SPF memakai redirect=; DMARC yang sudah ada dibiarkan; MX yang menunjuk ke penyedia lain (misal Google) dilaporkan sebagai conflict dan tidak diubah. Pakai jenis domain "Hanya kirim" bila mailbox klien tetap di penyedia lama.

DNS platform untuk bounce dan complaint

Return-path setiap pesan adalah b+<token>@bounce.<domain-klien>, dan domain klien memasang CNAME bounce.<domain-klien> ke bounce.<platform>. Supaya DSN sampai ke server ini, pasang sekali di zona platform:

bounce.relay.codeverta.com.   MX 10 mail.relay.codeverta.com.
bounce.relay.codeverta.com.   TXT "v=spf1 ip4:<IP-server> -all"
_spf.relay.codeverta.com.     TXT "v=spf1 ip4:<IP-server> -all"

Lalu daftarkan fbl@relay.codeverta.com ke feedback loop ISP (Yahoo CFL, Microsoft JMRP/SNDS). Port 25 masuk harus terbuka.

Laporan complaint hanya dipercaya bila bertanda tangan DKIM valid dari domain ISP di allowlist (default: Yahoo, AOL, Outlook/Hotmail, Microsoft, Comcast, Fastmail, Zoho, Mail.ru, Yandex), karena token di header pesan bisa dilihat dan disalin setiap penerima. Alamat VERP hanya diterima di bounce.<domain pengirim pesan>.

Kirim lewat SMTP relay

Aplikasi yang sudah memakai SMTP cukup ganti host dan kredensial. Password adalah API key dengan scope send. Alamat From di header harus dari domain terverifikasi milik tenant (bukan envelope sender).

# Laravel .env
MAIL_MAILER=smtp
MAIL_HOST=mail.relay.codeverta.com
MAIL_PORT=587
MAIL_ENCRYPTION=tls
MAIL_USERNAME=apikey
MAIL_PASSWORD=cv_live_...
MAIL_FROM_ADDRESS=no-reply@tokoabc.id
// Nodemailer
nodemailer.createTransport({ host: "mail.relay.codeverta.com", port: 587, auth: { user: "apikey", pass: process.env.CV_KEY } });

Header opsional (dibaca lalu dibuang sebelum dikirim): X-CV-Stream: broadcast, X-CV-Tags: invoice, bulanan, X-CV-Metadata: {"order_id":"12345"}. Penerima di envelope yang tidak ada di To/Cc dicatat sebagai bcc, dan header Bcc dibuang. Relay hanya berjalan jika CVRELAY_TLS_CERT/CVRELAY_TLS_KEY di-set (AUTH tanpa TLS ditolak).

Verifikasi webhook

Setiap POST membawa X-CV-Timestamp (unix) dan X-CV-Signature: v1=<hex>, di mana hex = HMAC-SHA256(secret, "<timestamp>.<body>"). Tolak jika timestamp lebih dari 5 menit.

import crypto from "node:crypto";
const expected = "v1=" + crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));

Pengembangan

Kebutuhan: Go 1.24+, Node 22+, MariaDB 10.6+ (11.4 disarankan).

# MariaDB lokal untuk tes integrasi: user butuh hak CREATE/DROP DATABASE
mysql -uroot -e "CREATE USER 'cvrelay'@'%' IDENTIFIED BY 'cvrelay'; GRANT ALL ON *.* TO 'cvrelay'@'%';"

make test        # go test -race ./... dengan MariaDB sungguhan
make build       # build React lalu binary bin/cvrelay
cd web && npm run dev   # UI dev server, /v1 diproksi ke :8080

Tes integrasi membuat database sekali pakai per tes (internal/testkit); tanpa CVRELAY_TEST_DSN tes tersebut dilewati.

Cara kode ini dibangun

Dibangun dengan skill mattpocock/skills:

  • domain-modeling: glosarium di CONTEXT.md dan keputusan di docs/adr/ (6 ADR). Pakai istilah di glosarium (Tenant, Recipient, Suppression, ...) di kode, tes, dan diskusi.
  • tdd: tes hanya di empat seam yang disepakati, lewat interface publik, bukan internals:
    1. REST API v1 (internal/api/*_test.go): httptest ke router asli + MariaDB asli.
    2. Delivery worker (internal/delivery/delivery_test.go): worker mengirim ke MX palsu (go-smtp in-process), DKIM diverifikasi independen.
    3. Modul murni: retry, bounce, webhook signer, dkim, mime, secret.
    4. Webhook dispatcher (internal/webhook/dispatcher_test.go): penerima httptest memverifikasi signature.
    5. SMTP relay (internal/submission/submission_test.go): klien net/smtp sungguhan (STARTTLS + AUTH) ke listener in-process; hasil dicek lewat REST API dan MX palsu.
    6. Inbound bounce/FBL (internal/inbound/inbound_test.go): MTA lain mengirim DSN/ARF ke listener port 25; hasil dicek lewat REST API (status, event, suppression, status tenant). Parser DSN/ARF juga punya fuzz test.
    7. Mailbox REST (internal/api/mailboxes_test.go): CRUD mailbox/alias, validasi, isolasi tenant.
    8. Port 25 ke IMAP (internal/imapd/*_test.go): klien net/smtp mengirim ke listener port 25, lalu klien IMAP sungguhan (go-imap/v2/imapclient, TLS) membaca: routing alias/catch-all, kuota, header jejak, flag, MOVE/COPY, dua perangkat + IDLE, keamanan login.

Struktur

cmd/cvrelay/        binary: serve, migrate, ctl
internal/
  app/              composition root (merakit semua layanan)
  api/              REST API v1 + tes seam API
  sending/          menerima permintaan kirim (REST + raw SMTP), log pengiriman
  submission/       SMTP relay 587/465
  inbound/          port 25: surat masuk mailbox, DSN bounce async, laporan FBL
  mailbox/          mailbox, alias, folder, pesan, kuota, penyapu blob
  imapd/            server IMAP 993/143
  reputation/       bounce/complaint rate 24 jam dan jeda otomatis
  delivery/         MTA keluar: klaim antrean, DKIM, SMTP, retry, bounce, unsubscribe
  webhook/          signer, CRUD webhook, dispatcher
  domain/  tenant/  template/  suppression/  events/
  dkim/  mime/  bounce/  retry/  secret/  blob/  dnsx/  store/  id/
  testkit/          DB sekali pakai, DNS palsu, MX palsu, jam palsu
migrations/         skema MariaDB (di-embed)
web/                React console (Vite + TanStack Query + Tailwind)
api/openapi.yaml
docs/adr/

Penyimpangan dari spesifikasi awal

  • MariaDB, bukan PostgreSQL (ADR 0001): tanpa RLS, jadi isolasi tenant ditegakkan di repository dan dites.
  • Satu SPA untuk admin + tenant console (ADR 0004).
  • Router chi ditulis tangan, belum di-generate dari OpenAPI; migrasi memakai runner kecil sendiri (goose terbaru butuh Go 1.26).
  • Event delivered belum dikirim terpisah; untuk SMTP keluar sent sudah berarti diterima MX tujuan.

Roadmap

Irisan berikut, kira-kira urut prioritas:

  1. Webmail ala Gmail, berurutan: webmail baca (sesi, HTML aman, thread, pencarian FULLTEXT, label), webmail tulis (editor, lampiran, draf, tanda tangan, balasan otomatis, branding, ID/EN), lalu admin klien (kelola mailbox/alias/kuota dalam batas paket, audit log). Rilis 2: aturan filter, kontak, kirim terjadwal, undo send, snooze, 2FA, proxy gambar, Web Push.
  2. Mail server lanjutan: import IMAP, POP3, ManageSieve, forward ke alamat luar (SRS), batas login per akun yang tersimpan di DB, modseq per pesan.
  3. Observability: /metrics Prometheus, OpenTelemetry, ACME/TLS bawaan.
  4. Multi-IP / IP pool per stream (struktur data sudah ada), MTA-STS/DANE saat mengirim.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages