mail

package
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 23, 2026 License: MIT Imports: 22 Imported by: 1

README

pkg/mail

Mesin pengiriman email sederhana dan minimal untuk Ziswapp dengan satu tanggung jawab:

  • Mengirim email dengan body text/html, text/plain, atau keduanya (multipart/alternative).
  • Mengirim lampiran (attachments) dengan multipart/mixed.
  • Menyisipkan header kustom untuk kebutuhan tracking/analytics.

Prioritas transport:

  • SMTP (STARTTLS/port 587, implicit TLS/port 465)
  • SES (AWS Simple Email Service)
  • Null/Log (untuk pengujian)

Semua pengiriman mengikuti kontrak yang kecil dan konsisten sehingga mudah diganti transport-nya tanpa mengubah kode pemanggil.


Fitur

  • API sangat kecil: Send(ctx, *Message) error
  • Factory: pilih transport via environment atau secara programatik
  • Implementasi SMTP dan Null/Log untuk pengujian
  • Body:
    • text/plain dan/atau text/html
    • multipart/alternative bila keduanya ada
  • Attachment:
    • multipart/mixed
    • base64 encoding, nama file aman (disanitasi)
  • Custom headers:
    • Header arbitrary (mis. X-Tracking-ID) disisipkan setelah sanitasi CR/LF
  • Reply-To, Message-ID, dan email tags (SES) untuk tracking/analytics lanjutan
  • Konteks:
    • Send menghormati context (timeout/cancel), termasuk saat dial dan TLS handshake
  • Tidak ada business logic; murni lapisan integrasi

Instalasi dan dependensi

Paket ini berada di dalam repository dan menggunakan standar library (SMTP via net/smtp).

Jika Anda baru menambahkan paket ini di aplikasi Anda, pastikan dependensi tersinkron (mis. jalankan go mod tidy di proyek Anda).


Variabel environment

Pemilihan transport

  • MAIL_TRANSPORT: smtp | ses | null | log | mock (default: null)

Pengirim default

  • MAIL_FROM_ADDRESS: alamat email default pengirim
  • MAIL_FROM_NAME: nama pengirim default (opsional)

SMTP

  • SMTP_HOST: host server SMTP (wajib untuk MAIL_TRANSPORT=smtp)
  • SMTP_PORT: port server SMTP (mis. 587 atau 465)
  • SMTP_USERNAME: username SMTP (opsional, tergantung server)
  • SMTP_PASSWORD: password SMTP
  • SMTP_FROM: alamat email default pengirim (fallback; gunakan MAIL_FROM_ADDRESS/MAIL_FROM_NAME sebagai sumber utama)

Contoh .env

# Transport email
MAIL_TRANSPORT=smtp

# Pengirim default (disarankan)
MAIL_FROM_ADDRESS=no-reply@ziswapp.org
MAIL_FROM_NAME=Ziswapp

# Konfigurasi SMTP
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USERNAME=apikey
SMTP_PASSWORD=secret
# SMTP_FROM=no-reply@ziswapp.org   # fallback (opsional)

# AWS SES (alternatif transport)
# MAIL_TRANSPORT=ses
#
# AWS_REGION=ap-southeast-1
# AWS_ACCESS_KEY_ID=AKIA...
# AWS_SECRET_ACCESS_KEY=...
#
# SES_CONFIGURATION_SET=optional-config-set
# SES_FROM=no-reply@ziswapp.org    # fallback (opsional)

# Pengujian (null/log)
# MAIL_TRANSPORT=null

API

Konstruktor

  • NewTransportFromEnv(ctx) (Mailer, error)
  • NewTransport(ctx, cfg *Config) (Mailer, error)
  • NewNullMailer() Mailer
  • NewSMTPMailer(host string, port int, username, password, defaultFrom string) (Mailer, error)
  • NewSESMailer(region, accessKeyID, secretAccessKey, defaultFrom, configurationSet string) (Mailer, error)

Antarmuka

  • Send(ctx context.Context, msg *Message) error

Tipe

  • Message:
    • From string
    • To []string
    • Cc []string
    • Bcc []string
    • Subject string
    • PlainText string
    • HTML string
    • ReplyTo []string
    • MessageID string
    • Headers map[string]string
    • Tags map[string]string
    • Attachments []Attachment
  • Attachment:
    • Filename string
    • ContentType string (default: application/octet-stream bila kosong)
    • Data []byte

Catatan internal (opsional)

  • BuildMime(from string, msg *Message) ([]byte, error) — membangun raw MIME (headers + body) yang reusable lintas transport.
  • Berbagai helper tersedia (sanitasi header/nama file, boundary generator, encoder).

Quick start

Inisialisasi dari environment

ctx := context.Background()

mailer, err := mail.NewTransportFromEnv(ctx)
if err != nil {
    // tangani error konfigurasi (mis. SMTP_HOST/PORT kosong)
}

Kirim email dasar

msg := &mail.Message{
    // From boleh kosong → fallback ke SMTP_FROM (untuk transport SMTP)
    To:        []string{"penerima@example.com"},
    Subject:   "Halo dari Ziswapp",
    PlainText: "Hai, ini body versi teks.",
    HTML:      "<h1>Hai</h1><p>Ini body versi <b>HTML</b>.</p>",
}

if err := mailer.Send(ctx, msg); err != nil {
    // tangani error pengiriman
}

Kirim email dengan attachment

pdf := []byte("%PDF-1.7 ...") // contoh data PDF

msg := &mail.Message{
    To:        []string{"penerima@example.com"},
    Subject:   "Invoice Bulan Ini",
    PlainText: "Silakan lihat lampiran.",
    HTML:      "<p>Silakan lihat lampiran.</p>",
    Attachments: []mail.Attachment{
        {
            Filename:    "invoice.pdf",
            ContentType: "application/pdf",
            Data:        pdf,
        },
    },
}

if err := mailer.Send(ctx, msg); err != nil {
    // tangani error
}

Menambahkan custom headers (tracking/analytics)

msg := &mail.Message{
    To:        []string{"penerima@example.com"},
    Subject:   "Kampanye Welcome",
    PlainText: "Selamat datang!",
    Headers: map[string]string{
        "X-Tracking-ID": "abc-123",
        "X-Campaign":    "welcome-2025",
    },
}

_ = mailer.Send(ctx, msg)

Konfigurasi programatik

cfg := &mail.Config{
    Transport: "smtp", // atau "null"/"log"/"mock"
}
cfg.SMTP.Host     = "smtp.example.com"
cfg.SMTP.Port     = 587
cfg.SMTP.Username = os.Getenv("SMTP_USERNAME")
cfg.SMTP.Password = os.Getenv("SMTP_PASSWORD")
cfg.SMTP.From     = "no-reply@ziswapp.org"

mailer, err := mail.NewTransport(context.Background(), cfg)
if err != nil {
    // tangani error
}

Praktik terbaik

  • Selalu gunakan context.WithTimeout untuk membatasi durasi pengiriman.
  • Isi SMTP_FROM dan biarkan Message.From kosong bila Anda ingin default konsisten.
  • Isi PlainText untuk kompatibilitas klien email yang tidak menampilkan HTML.
  • Gunakan header kustom untuk trace id kampanye atau audit non-PII.
  • Jangan menyertakan data sensitif dalam headers/body/log.

Contoh Penggunaan

Contoh lebih komprehensif yang mencakup:

  • Inisialisasi via ENV dengan timeout.
  • Pengisian To, Cc, Bcc.
  • Body gabungan text/plain + text/html.
  • Header kustom untuk tracking.
  • Beberapa lampiran (PDF dan CSV).
  • Preview raw MIME (opsional; untuk debugging/dev).
  • Fallback ke transport null untuk pengujian lokal.
package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "time"

    "github.com/ziswapp/ziswapp/pkg/mail"
)

func main() {
    // (Opsional) Set ENV secara programatik saat demo/pengujian
    os.Setenv("MAIL_TRANSPORT", "smtp")
    os.Setenv("SMTP_HOST", "smtp.example.com")
    os.Setenv("SMTP_PORT", "587")
    os.Setenv("SMTP_USERNAME", "apikey")
    os.Setenv("SMTP_PASSWORD", "secret")
    os.Setenv("SMTP_FROM", "no-reply@ziswapp.org")

    // Konteks dengan timeout (disarankan)
    ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
    defer cancel()

    // Inisialisasi mailer dari ENV
    mailer, err := mail.NewTransportFromEnv(ctx)
    if err != nil {
        log.Fatalf("mail init: %v", err)
    }

    // Siapkan lampiran contoh
    pdf := []byte("%PDF-1.7 ...binary content...")
    csv := []byte("name,email\nAlice,alice@example.com\nBob,bob@example.com\n")

    // Bangun pesan
    msg := &mail.Message{
        // From kosong → fallback ke SMTP_FROM
        To:      []string{"to1@example.com", "to2@example.com"},
        Cc:      []string{"cc@example.com"},
        Bcc:     []string{"internal-audit@example.com"},
        Subject: "Welcome to Ziswapp",
        PlainText: "Halo,\n\n" +
            "Selamat datang di Ziswapp.\n" +
            "Silakan lihat lampiran untuk informasi lebih lanjut.\n\n" +
            "Terima kasih.",
        HTML: `<h1>Halo</h1>
<p>Selamat datang di <b>Ziswapp</b>.</p>
<p>Silakan lihat <i>lampiran</i> untuk informasi lebih lanjut.</p>
<p>Terima kasih.</p>`,
        ReplyTo: []string{"support@ziswapp.org"},
        MessageID: "welcome-2025-0001@ziswapp.org",
        Headers: map[string]string{
            "X-Tracking-ID": "onboarding-2025-0001",
            "X-Campaign":    "onboarding-2025",
        },
        Tags: map[string]string{
            "campaign": "onboarding-2025",
            "segment":  "early-access",
        },
        Attachments: []mail.Attachment{
            {
                Filename:    "welcome.pdf",
                ContentType: "application/pdf",
                Data:        pdf,
            },
            {
                Filename:    "contacts.csv",
                ContentType: "text/csv",
                Data:        csv,
            },
        },
    }

    // (Opsional) Preview raw MIME untuk debugging (jangan dipakai di produksi)
    if raw, err := mail.BuildMime("no-reply@ziswapp.org", msg); err == nil {
        // Cetak sebagian untuk verifikasi
        n := 512
        if len(raw) < n {
            n = len(raw)
        }
        fmt.Printf("[DEBUG MIME PREVIEW]\n%s\n...\n\n", string(raw[:n]))
    } else {
        log.Printf("build mime failed: %v", err)
    }

    // Kirim
    if err := mailer.Send(ctx, msg); err != nil {
        log.Fatalf("send: %v", err)
    }
    log.Println("email terkirim")

    // (Opsional) Fallback ke transport null saat dev/testing
    os.Setenv("MAIL_TRANSPORT", "null")
    nullMailer, _ := mail.NewTransportFromEnv(context.Background())
    _ = nullMailer.Send(context.Background(), &mail.Message{
        To:        []string{"dev@example.com"},
        Subject:   "Test (Null Transport)",
        PlainText: "Ini hanya log, tidak terkirim.",
    })
}

Tips:

  • Pastikan MAIL_FROM_ADDRESS valid dan domain telah dikonfigurasi SPF/DKIM/DMARC agar deliverability baik.
  • Hindari melampirkan file besar; pertimbangkan tautan unduhan aman.

Rincian transport dan catatan

SMTP

  • STARTTLS vs implicit TLS:
    • Port 465: implicit TLS (TLS handshake sebelum perintah SMTP)
    • Port 587/25: plaintext TCP kemudian upgrade STARTTLS bila server mendukung
  • Autentikasi:
    • Jika SMTP_USERNAME diset, akan mencoba AUTH PLAIN bila server mendukung
  • Envelope & Header:
    • RCPT TO mencakup To, Cc, dan Bcc
    • Bcc tidak dituliskan ke header (sesuai standar)
  • Subject & karakter non-ASCII:
    • Subject di-encode MIME Q (utf-8) secara otomatis
  • Reply-To dan Message-ID:
    • Didukung via header MIME pada semua transport (SMTP, SES, Null/Log). Set melalui field ReplyTo dan MessageID pada Message.
  • Lampiran:
    • base64 dengan line wrap 76 karakter (sesuai rekomendasi MIME)
    • overhead ~33% dari ukuran asli
  • Kinerja & keandalan:
    • Send menghormati context; gunakan timeout yang realistis
    • Tangani error spesifik (auth gagal, STARTTLS gagal, RCPT gagal)

SES

  • Menggunakan AWS SDK v2 (SESv2) dengan SendEmail konten Raw MIME (dibangun via BuildMime)
  • Gunakan AWS_REGION; pengirim default berasal dari MAIL_FROM_ADDRESS/MAIL_FROM_NAME (fallback SES_FROM bila diisi).
  • Kredensial dapat memakai AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY atau default provider chain AWS.
  • Mendukung Destination (To/Cc/Bcc) dan header kustom di raw MIME
  • Mendukung SES_CONFIGURATION_SET (opsional) untuk integrasi eventing/tracking SES
  • Perhatikan batasan SES (sandbox mode, verified identity/domains, sending limits, region)

Null/Log

  • Tidak benar-benar mengirim email; mencetak detail (From/To/Cc/Bcc/Subject/Reply-To/Message-ID/Headers/Tags/Body/Attachments) ke stdout
  • Cocok untuk pengembangan/pengujian tanpa dependensi eksternal

Pengujian dengan transport null

Paksa transport null melalui environment

os.Setenv("MAIL_TRANSPORT", "null")

m, err := mail.NewTransportFromEnv(context.Background())
if err != nil {
    t.Fatalf("mail init: %v", err)
}

if err := m.Send(context.Background(), &mail.Message{
    From:      "sender@example.com",
    To:        []string{"recipient@example.com"},
    Subject:   "Test",
    PlainText: "Hello!",
}); err != nil {
    t.Fatalf("send: %v", err)
}

Buat langsung NullMailer

m := mail.NewNullMailer()
_ = m.Send(context.Background(), &mail.Message{
    To:        []string{"recipient@example.com"},
    Subject:   "Test",
    PlainText: "Hello!",
})

Catatan dan keterbatasan

  • Paket ini tidak melakukan DKIM/DMARC signing. Pastikan SPF/DKIM/DMARC domain dikonfigurasi di infrastruktur email Anda agar deliverability baik.
  • Header kustom akan disanitasi dari CR/LF untuk mencegah header injection.
  • Email yang sangat besar (banyak lampiran) dapat memperlambat pengiriman; pertimbangkan untuk memberi tautan unduhan aman alih-alih melampirkan file besar.
  • Validasi alamat email tidak dilakukan secara agresif; lakukan validasi di layer input bila perlu.
  • Logging bawaan transport null akan menulis isi email ke stdout—hindari menjalankannya di lingkungan produksi.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func BuildMime

func BuildMime(from string, msg *Message) ([]byte, error)

BuildMime membangun email MIME mentah (headers + body) dari informasi yang diberikan. Fungsi ini reusable untuk berbagai transport (SMTP, API provider, dsb).

Aturan: - Mengisi header standar: From, To, Cc, Subject (MIME-encoded), Date, MIME-Version - Header kustom dari msg.Headers akan disisipkan setelah disanitasi - Body mendukung text/plain dan/atau text/html, memakai multipart/alternative jika keduanya ada - Attachment didukung via multipart/mixed dengan base64 encoding - Bcc tidak dituliskan ke header (hanya digunakan di envelope oleh transport)

func GenerateBoundary

func GenerateBoundary() string

GenerateBoundary membuat boundary acak untuk multipart MIME.

func NormalizeEmails

func NormalizeEmails(in []string) []string

NormalizeEmails menormalkan daftar email dengan memangkas spasi dan menghapus entri kosong.

func SanitizeFilename

func SanitizeFilename(name string) string

SanitizeFilename membersihkan nama file lampiran dari karakter terlarang.

func SanitizeHeaderKey

func SanitizeHeaderKey(k string) string

SanitizeHeaderKey membersihkan key header dari karakter terlarang dan spasi.

func SanitizeHeaderValue

func SanitizeHeaderValue(v string) string

SanitizeHeaderValue membersihkan value header dari karakter terlarang dan spasi.

func WriteBase64

func WriteBase64(w io.Writer, data []byte)

WriteBase64 menuliskan data dalam encoding base64 dengan line wrap 76 karakter per baris sesuai rekomendasi MIME.

func WriteHeader

func WriteHeader(w io.Writer, key, value string)

WriteHeader menuliskan header email single-line dengan CRLF. Abaikan jika key atau value kosong.

func WriteIndented

func WriteIndented(b *strings.Builder, s string, indent int)

WriteIndented menuliskan teks dengan indentasi ke builder (untuk logging/presentation).

func WriteQuotedPrintable

func WriteQuotedPrintable(w io.Writer, s string) error

WriteQuotedPrintable menuliskan string menggunakan encoding quoted-printable ke writer yang diberikan, lalu menutup encoder.

Types

type Attachment

type Attachment struct {
	// Filename adalah nama file yang akan terlihat oleh penerima.
	Filename string
	// ContentType adalah MIME type dari lampiran (mis. "application/pdf").
	ContentType string
	// Data adalah isi file lampiran dalam bentuk byte slice.
	Data []byte
}

Attachment mewakili file lampiran email.

type Config

type Config struct {
	Transport string
	// FromAddress adalah alamat email default pengirim global (meng-override SMTP.From / SES.From).
	FromAddress string
	// FromName adalah nama pengirim default global. Jika diisi, header From akan berbentuk 'Name <address>'.
	FromName string

	SMTP struct {
		// Host adalah host SMTP server.
		Host string
		// Port adalah port SMTP server.
		Port int
		// Username adalah username untuk autentikasi SMTP.
		Username string
		// Password adalah password untuk autentikasi SMTP.
		Password string
		// From adalah alamat email default pengirim.
		From string
	}

	SES struct {
		// Region AWS (mis. ap-southeast-1)
		Region string
		// AccessKeyID untuk autentikasi AWS (opsional jika pakai default provider chain)
		AccessKeyID string
		// SecretAccessKey untuk autentikasi AWS (opsional jika pakai default provider chain)
		SecretAccessKey string
		// From adalah alamat email default pengirim.
		From string
		// ConfigurationSet opsional untuk SES v2
		ConfigurationSet string
	}
}

Config menyimpan konfigurasi mail. Gunakan FromEnv() untuk memuat dari variabel environment.

Transport: - "null" : Pengiriman in-memory/log (untuk pengujian); alias: "log", "mock" - "smtp" : Pengiriman melalui SMTP - "ses" : Pengiriman melalui AWS SES

func FromEnv

func FromEnv() *Config

FromEnv memuat nilai Config mail dari variabel environment.

Variabel: - MAIL_TRANSPORT: null | log | mock | smtp | ses (default: null) - MAIL_FROM_ADDRESS, MAIL_FROM_NAME - SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_FROM (fallback bila MAIL_FROM_* kosong) - AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY - SES_FROM, SES_CONFIGURATION_SET (SES_FROM fallback bila MAIL_FROM_* kosong)

type Mailer

type Mailer interface {
	// Send mengirim email dengan informasi yang diberikan dalam pesan.
	// Mengembalikan error jika pengiriman gagal.
	Send(ctx context.Context, msg *Message) error
}

Mailer adalah antarmuka untuk mengirim email. Implementasi bertanggung jawab untuk menangani retry, timeout, dan error handling.

func NewNullMailer

func NewNullMailer() Mailer

NewNullMailer membuat instance NullMailer.

func NewSESMailer

func NewSESMailer(region, accessKeyID, secretAccessKey, defaultFrom, configurationSet string) (Mailer, error)

NewSESMailer membuat Mailer SES baru menggunakan AWS SDK v2 (SESv2). region dan from wajib. Jika accessKeyID/secretAccessKey kosong, akan memakai default provider chain.

func NewSMTPMailer

func NewSMTPMailer(host string, port int, username, password, defaultFrom string) (Mailer, error)

NewSMTPMailer membuat Mailer SMTP baru.

func NewTransport

func NewTransport(ctx context.Context, cfg *Config) (Mailer, error)

NewTransport membuat Mailer berdasarkan Config yang diberikan.

func NewTransportFromEnv

func NewTransportFromEnv(ctx context.Context) (Mailer, error)

NewTransportFromEnv membuat Mailer dari variabel environment.

type Message

type Message struct {
	// From adalah alamat email pengirim.
	From string
	// To adalah daftar alamat email penerima.
	To []string
	// Subject adalah subjek email.
	Subject string
	// HTML adalah isi email dalam format HTML.
	HTML string
	// PlainText adalah isi email dalam format plaintext.
	PlainText string
	// Cc adalah daftar alamat email yang di-copy.
	Cc []string
	// Bcc adalah daftar alamat email yang di-hidden copy.
	Bcc []string
	// ReplyTo adalah daftar alamat email untuk header Reply-To.
	ReplyTo []string
	// MessageID adalah nilai untuk header Message-ID (opsional).
	// Jika kosong, transport dapat membiarkan server/pustaka menetapkan nilainya.
	MessageID string
	// Headers adalah header tambahan untuk tracking/analytic, dsb.
	Headers map[string]string
	// Tags adalah pasangan kunci-nilai untuk email tags (misalnya SES).
	// Implementasi transport dapat memetakan ke fitur tagging masing-masing provider.
	Tags map[string]string
	// Attachments adalah daftar lampiran yang akan dikirim bersama email.
	Attachments []Attachment
}

Message mewakili email yang akan dikirim.

type NullMailer

type NullMailer struct{}

NullMailer adalah implementasi Mailer yang tidak benar-benar mengirim email. Ini hanya mencetak isi email ke stdout untuk tujuan testing/logging.

func (*NullMailer) Send

func (n *NullMailer) Send(ctx context.Context, msg *Message) error

Send "mengirim" email dengan cara mencetak ke stdout. Mendukung headers dan attachments untuk verifikasi dalam pengujian.

type SESMailer

type SESMailer struct {
	// contains filtered or unexported fields
}

SESMailer adalah implementasi Mailer menggunakan AWS SES v2.

func (*SESMailer) Send

func (m *SESMailer) Send(ctx context.Context, msg *Message) error

Send mengirim email menggunakan AWS SES v2 dengan konten MIME mentah. To/Cc/Bcc akan diisi di Destination (envelope). Header kustom, body, dan attachments dibangun oleh BuilMime.

type SMTPMailer

type SMTPMailer struct {
	// contains filtered or unexported fields
}

SMTPMailer adalah implementasi Mailer menggunakan protokol SMTP.

func (*SMTPMailer) Send

func (m *SMTPMailer) Send(ctx context.Context, msg *Message) error

Send mengirim email menggunakan SMTP.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL