Anda kirim 10.000 email setiap hari dari aplikasi. Lima di antaranya tidak sampai. Anda tidak punya cara tahu mana yang gagal, kenapa gagal, dan apakah kegagalan itu terjadi di sisi Anda atau di mail server tujuan. Masalahnya bukan SMTP – SMTP-nya jalan. Masalahnya: tidak ada correlation ID. Artikel ini menjelaskan cara menambahkan custom header ke setiap email, mencatatnya di database, dan memakainya untuk melacak satu transaksi email dari awal sampai selesai.
Email transaksional yang baik bukan hanya soal email terkirim. Anda perlu tahu email itu sampai di mana, kapan sampai, dan apakah sampai sama sekali. Tanpa sistem tracking, satu email yang hilang di antara 10.000 email yang berhasil akan butuh berhari-hari untuk ditemukan dan diagnosed.
Masalahnya begini: ketika Anda memanggil $mailer->send() di Laravel, yang Anda dapat adalah boolean true atau false. Kalau false, Anda tidak tahu apakah itu salah konfigurasi SMTP, limit rate, atau email sudah diterima mail server tujuan lalu di-silently dropped.
Di sinilah custom header SMTP berperan. Dengan menambahkan identifier unik ke setiap email, Anda bisa trace satu transaksi dari kode aplikasi sampai ke mail server tujuan.
Daftar Isi
Apa Itu Custom Header SMTP dan Kenapa Anda Membutuhkannya
Email punya struktur yang defined di RFC 5321 dan RFC 5322. Header email adalah bagian atas pesan yang berisi metadata: From, To, Subject, Date, Message-ID. Semua mail server di dunia memproses header ini sesuai standar.
Yang menarik adalah header dengan prefix “X-”. RFC 5321 secara eksplisit mengatakan mail server harus mengabaikan header yang tidak dikenal. Artinya: Anda bisa menambahkan header arbitrer seperti X-Transaction-ID: abc123, dan header itu akan sampai ke mailbox tujuan tanpa diganggu oleh mail server di tengah jalan.
Ini berarti Anda bisa menanamkan identifier unik di setiap email dan untuk correlating dengan log di sisi aplikasi Anda.
Contoh sederhana. Anda kirim email dengan header:
X-Transaction-ID: 550e8400-e29b-41d4-a716-446655440000
X-Tenant-ID: tenant_123
X-Event-Type: order_confirmation
Di database, Anda simpan record:
| transaction_id | tenant_id | event_type | smtp_response | status | sent_at |
|---|---|---|---|---|---|
| 550e8400… | tenant_123 | order_confirmation | 250 OK | delivered | 2026-05-02 10:00:00 |
Kalau kemudian ada complaint dari user, Anda langsung bisa trace email itu: kapan dikirim, response SMTP-nya apa, dan event apa yang trigger email tersebut.
Cara Menambahkan Custom Header di Laravel
Laravel Mail menyediakan dua cara untuk menambahkan custom header. Yang pertama lewat Mailable class:
class OrderConfirmation extends Mailable
{
use Queueable, SerializesModels;
public function build()
{
return $this->subject('Konfirmasi Pesanan #' . $this->orderId)
->view('emails.order-confirmation')
->withSwiftMessage(function ($message) {
$message->getHeaders()->addTextHeader(
'X-Transaction-ID',
$this->transactionId
);
$message->getHeaders()->addTextHeader(
'X-Tenant-ID',
$this->tenantId
);
});
}
}
Yang kedua, lebih clean, lewat Mail::raw atau Mail::send dengan header:
Mail::mailer('smtp')
->to($user->email)
->send(new OrderConfirmation($order));
Dengan Mailable, headers ditambahkan di method build(). Dengan approach ini, setiap email yang dikirim dari class ini otomatis punya transaction ID tanpa perlu manually menambahkan header di setiap call site.
Untuk UUID generation, pakai package ramsey/uuid yang sudah termasuk di Laravel:
use Ramsey\Uuid\Uuid;
$transactionId = Uuid::uuid4()->toString();
UUID lebih baik daripada sequential integer karena tidak bisa ditebak oleh pihak luar, dan distributed system tidak akan bentrok kalau dua server generate UUID secara bersamaan.
Menambahkan Custom Header di Node.js dengan Nodemailer
Nodemailer membuat penambahan header sangat straightforward:
const nodemailer = require('nodemailer');
async function sendOrderConfirmation(user, order) {
const transactionId = require('crypto').randomUUID();
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
const info = await transporter.sendMail({
from: '"App Name" <[email protected]>',
to: user.email,
subject: `Konfirmasi Pesanan #${order.id}`,
html: orderConfirmationTemplate(order),
headers: {
'X-Transaction-ID': transactionId,
'X-Tenant-ID': process.env.TENANT_ID,
'X-Event-Type': 'order_confirmation',
},
});
console.log('Message sent:', info.messageId);
// Simpan ke database: messageId, transactionId, smtpResponse
await db.emails.create({
message_id: info.messageId,
transaction_id: transactionId,
tenant_id: process.env.TENANT_ID,
event_type: 'order_confirmation',
smtp_response: info.response, // contoh: "250 OK"
status: 'sent',
});
return { transactionId, messageId: info.messageId };
}
Nodemailer juga mendukung DSN (Delivery Status Notification) yang bisa dikonfigurasi supaya mail server tujuan memberi tahu Anda kalau email gagal di-deliver. Tambahkan di options:
{
headers: { ... },
dsn: {
id: transactionId,
return: 'headers',
notify: ['failure', 'delay'],
recipient: process.env.DSN_RETURN_EMAIL,
},
}
Dengan DSN, Anda bisa menerima bounce notification secara asynchronous, bukan hanya rely pada SMTP response saat kirim.
Logging Custom Header ke Database
Header tanpa logging tidak berguna. Anda perlu menyimpan transaction ID dan metadata lainnya di database setiap kali kirim email, kemudian update statusnya ketika ada response dari mail server.
Struktur tabel minimal yang Anda butuhkan:
CREATE TABLE email_transactions (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
transaction_id VARCHAR(36) UNIQUE NOT NULL,
message_id VARCHAR(255),
tenant_id VARCHAR(100),
event_type VARCHAR(50),
recipient_email VARCHAR(255) NOT NULL,
smtp_response VARCHAR(255),
status ENUM('pending', 'sent', 'delivered', 'bounced', 'failed') DEFAULT 'pending',
sent_at TIMESTAMP NULL,
delivered_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_transaction_id (transaction_id),
INDEX idx_status (status),
INDEX idx_sent_at (sent_at)
);
Transaction ID di-generate di aplikasi sebelum kirim email. Simpan record ini di database yang terpisah dari tabel user atau order, supaya tidak lambat ketika volume email naik. Message ID dari SMTP response adalah identifier yang di-assign mail server pengirim. Dengan menyimpan keduanya, Anda bisa correlate log aplikasi dengan header yang sampai di mailbox penerima.
Kalau pakai Laravel, buat model dan job untuk async processing:
// App\Jobs\SendTransactionalEmail
class SendTransactionalEmail implements ShouldQueue
{
public function handle()
{
$transactionId = Uuid::uuid4()->toString();
// Insert record
$emailLog = EmailTransaction::create([
'transaction_id' => $transactionId,
'tenant_id' => $this->tenantId,
'event_type' => $this->eventType,
'recipient_email' => $this->to,
'status' => 'pending',
]);
try {
Mail::mailer('smtp')->to($this->to)->send(
new TransactionalMail($this->data, $transactionId)
);
$emailLog->update([
'status' => 'sent',
'sent_at' => now(),
]);
} catch (\Exception $e) {
$emailLog->update([
'status' => 'failed',
'smtp_response' => $e->getMessage(),
]);
}
}
}
Dengan queue, Anda tidak blocking HTTP request sambil menunggu SMTP response. Anda juga bisa retry kalau gagal, dan centralised log semua email yang pernah dikirim.
Cara Melacak Email Berdasarkan Header
Sekarang Anda punya transaction ID di header dan record di database. Bagaimana menggunakannya untuk debugging?
Scenario 1: User complaint – email tidak sampai
User: "Saya tidak pernah terima email konfirmasi"
Langkah:
- Cek database:
SELECT * FROM email_transactions WHERE recipient_email = '[email protected]' AND event_type = 'order_confirmation' ORDER BY sent_at DESC LIMIT 10 - Dapat record. Cek field
status: kalau sent tapi tidak delivered, kemungkinan besar email masuk spam atau di-silently dropped. Baca juga cara handle email bounce untuk perbedaan hard bounce dan soft bounce - Kalau failed, cek
smtp_responseuntuk tahu kenapa
Scenario 2: Email bounce
Kalau Anda terima bounce notification via webhook atau DSN, correlation dengan transaction ID di header memungkinkan Anda update record:
// Endpoint webhook dari mail server
public function handleBounce(Request $request)
{
$messageId = $request->input('Message-ID');
$bounceType = $request->input('bounce_type');
EmailTransaction::where('message_id', $messageId)->update([
'status' => $bounceType === 'permanent' ? 'bounced' : 'failed',
]);
}
Scenario 3: Debugging delay
Email yang delay bisa di-trace dengan membandingkan sent_at dengan delivered_at. Kalau delay lebih dari 5 menit untuk email transaksional, itu sudah tidak normal dan perlu dicek. Baca cara debug email tidak terkirim untuk detail troubleshooting (kalau Anda terima webhook delivery notification). Delay yang tidak biasa menunjukkan masalah di mail server atau jaringan.
Kapan Harus Pakai Dedicated Tracking vs Simple Header Logging
Custom header dengan database logging sudah cukup untuk sebagian besar aplikasi. Dengan ini Anda bisa:
- Correlate email dengan event di aplikasi
- Lihat SMTP response untuk setiap kirim
- Track bounce per recipient
- Debug complaint user
Yang tidak bisa dilakukan dengan custom header sederhana adalah tracking open dan click. Itu butuh pixel tracking atau link tracking, dan itu adalah fitur yang disediakan ESP seperti KIRIM.EMAIL sebagai bagian dari webhook real-time tracking.
Kombinasi yang powerful untuk sistem production: custom header untuk correlation ID di level aplikasi, webhook ESP untuk event-driven status tracking. Header memberi Anda visibility di sisi kode Anda. Webhook memberi Anda visibility di sisi mail server.
Kalau Anda pakai KIRIM.EMAIL Dev, Anda bisa memanfaatkan built-in tracking dengan custom header yang Anda tentukan sendiri, sehingga kedua sistem saling complement tanpa conflict.
Kombinasi yang powerful untuk sistem production: custom header untuk correlation ID di level aplikasi, webhook ESP untuk event-driven status tracking. Header memberi Anda visibility di sisi kode Anda. Webhook memberi Anda visibility di sisi mail server. KIRIM.EMAIL Dev menyediakan dedicated IP dengan infrastruktur email yang sepenuhnya di Indonesia, sehingga Anda punya kontrol penuh atas tracking dan reputasi tanpa bergantung pada shared infrastructure.
FAQ
Apa bedanya Message-ID bawaan mail server dengan custom header X-Transaction-ID?
Message-ID adalah identifier yang di-assign oleh mail server pengirim, bukan oleh aplikasi Anda. Formatnya juga berbeda antar mail server. Message-ID berguna untuk correlating dengan log mail server, tapi Anda tidak punya kontrol atas formatnya. Custom header X-Transaction-ID adalah identifier yang Anda tentukan sendiri sebelum email dikirim, sehingga Anda bisa langsung correlate dengan record di database aplikasi tanpa perlu bergantung pada mail server.
Apakah semua mail server meneruskan custom header sampai ke penerima?
Sebagian besar mail server corporate dan ESP (Gmail, Outlook, Yahoo, Mailgun, SendGrid) akan meneruskan custom header. Yang perlu diwaspadai: beberapa webmail client lama atau sistem keamanan email enterprise bisa menghapus atau mengubah header tertentu. Header dengan prefix X- yang sudah lama jadi standar de facto akan diteruskan. Untuk privacy dan security reasons, beberapa provider seperti Gmail sekarang bisa mengubah atau menghapus certain X- headers.
Bagaimana cara menambahkan custom header tanpa mengubah semua kode kirim email yang sudah ada?
Di Laravel, buat Mailable base class yang semua Mailable lain extends:
abstract class BaseMailable extends Mailable
{
public function build()
{
$this->withSwiftMessage(function ($message) {
if (!$message->getHeaders()->hasTextHeader('X-Transaction-ID')) {
$message->getHeaders()->addTextHeader(
'X-Transaction-ID',
Uuid::uuid4()->toString()
);
}
});
}
}
Dengan approach ini, semua email yang pakai Mailable system secara otomatis dapat transaction ID tanpa perlu ubah setiap call site.
Apakah custom header bisa dipakai untuk tracking bounce tanpa webhook?
Bisa, tapi terbatas. Anda bisa mendengarkan bounce via DSN (Delivery Status Notification) yang dikirim mail server tujuan kembali ke envelope sender. DSN akan include Message-ID, dan kalau Anda sudah menyimpan correlation antara Message-ID dan transaction ID di database, Anda bisa connect keduanya. Tapi DSN tidak always reliable: banyak mail server yang tidak kirim DSN untuk soft bounce, dan hard bounce kadang tidak pernah dilaporkan via DSN. Untuk reliability yang lebih tinggi, webhook lebih baik daripada DSN.
Berapa banyak custom header yang terlalu banyak?
Tidak ada batas resmi, tapi usahakan seminimal mungkin. Target tiga sampai lima header: transaction ID, tenant ID (kalau multi-tenant), event type, dan opcional lain seperti campaign ID atau priority level. Terlalu banyak header membuat email lebih besar dan bisa meningkatkan spam score di mata beberapa filter. Header yang tidak memberikan informasi actionable sebaiknya tidak ditambahkan.
Hasbi Putra adalah Head of Marketing di KIRIM.EMAIL, email delivery infrastructure untuk developer dan tim IT di Indonesia. KIRIM.EMAIL mengirim lebih dari 11 juta email per hari dengan server yang sepenuhnya berlokasi di Indonesia.
