Setelah memahami peta perjalanan email, saatnya kita turun ke mekanisme pengirimannya. PHP menyediakan fungsi bawaan mail() yang tampak sederhana, tetapi di baliknya ada protokol SMTP dan format pesan MIME yang perlu kita pahami. Chapter ini membongkar keduanya: kita mulai dari kelemahan mail(), membaca anatomi percakapan SMTP, lalu menyusun pesan MIME secara manual sebagai fungsi murni yang bisa diuji tanpa server.

Masalah dengan Fungsi mail() #

Fungsi mail() tidak mengirim email sendiri; fungsi ini hanya menyerahkan pesan ke program pengirim lokal yang dikonfigurasi lewat direktif sendmail_path di php.ini, lalu berharap ada MTA terpasang di mesin. Berikut wujud pemakaiannya:

php
<?php
$kepada = "[email protected]";
$subjek = "Pesanan #1234 Terkirim";
$isi = "Halo Budi, pesanan #1234 sudah kami kirim.";
$header = "From: Toko Example <[email protected]>\r\n"
    . "Reply-To: [email protected]\r\n"
    . "Content-Type: text/plain; charset=UTF-8";

$terkirim = mail($kepada, $subjek, $isi, $header);
// $terkirim bernilai true bila MTA lokal MENERIMA pesan,
// bukan jaminan pesan sampai ke kotak masuk penerima.

Contoh di atas memerlukan MTA lokal yang hidup, sehingga tidak bisa kita jalankan di lingkungan latihan ini — pada mesin pengembangan tanpa Postfix atau sendmail, mail() langsung mengembalikan false atau memicu warning. Kelemahan lainnya lebih fundamental: mail() tidak mengenal autentikasi SMTP, tidak mendukung TLS ke relay modern, dan perilakunya berbeda antara Linux dan Windows. Itulah alasan aplikasi produksi hampir selalu melewati mail() dan berbicara langsung ke server SMTP — sendiri-sendiri lewat socket, atau lewat library seperti yang akan kita bahas pada chapter berikutnya.

Anatomi Percakapan SMTP #

SMTP adalah protokol teks bergantian: klien mengirim perintah, server menjawab dengan kode tiga digit. Percakapan pengiriman satu email terlihat seperti ini:

text
S: 220 smtp.relay-example.com ESMTP siap
C: EHLO app.toko.example
S: 250-smtp.relay-example.com
S: 250-STARTTLS
S: 250 AUTH LOGIN PLAIN

C: STARTTLS
S: 220 siap memulai TLS
   ... percakapan selanjutnya sudah terenkripsi TLS ...

C: MAIL FROM:<[email protected]>
S: 250 OK
C: RCPT TO:<[email protected]>
S: 250 OK
C: DATA
S: 354 kirim isi pesan, akhiri dengan baris berisi satu titik
C: From: Toko Example <[email protected]>
C: To: [email protected]
C: Subject: Pesanan #1234 Terkirim
C: MIME-Version: 1.0
C: Content-Type: text/plain; charset=UTF-8
C:
C: Halo Budi, pesanan #1234 sudah kami kirim.
C: .
S: 250 OK: pesan masuk antrian
C: QUIT
S: 221 sampai jumpa

Baris berprefiks C dikirim klien, S dikirim server. Perintah EHLO memperkenalkan diri klien sekaligus menanyakan kemampuan server; MAIL FROM menetapkan amplop pengirim; RCPT TO menetapkan satu penerima per perintah; dan DATA membuka mode pengiriman isi pesan yang diakhiri satu baris berisi titik (.) semata. Kode jawaban 250 berarti sukses, 354 meminta kirim isi, dan 221 menutup sesi.

Menyusun Pesan MIME Secara Manual #

Isi perintah DATA adalah pesan MIME: blok header, satu baris kosong, lalu isi pesan. Karena tidak ada server SMTP di lingkungan latihan, kita bangun pesan tersebut sebagai fungsi murni yang mengembalikan string — pendekatan yang sekaligus mudah diuji:

php
<?php
function buatHeaderEmail(string $dari, string $kepada, string $subjek): string
{
    $baris = [
        "From: $dari",
        "To: $kepada",
        "Subject: $subjek",
        "MIME-Version: 1.0",
        "Content-Type: text/plain; charset=UTF-8",
    ];

    return implode("\r\n", $baris);
}

function buatPesanMime(string $dari, string $kepada, string $subjek, string $isi): string
{
    $isi = wordwrap($isi, 70, "\r\n");

    return buatHeaderEmail($dari, $kepada, $subjek) . "\r\n\r\n" . $isi . "\r\n";
}

$pesan = buatPesanMime(
    "Toko Example <[email protected]>",
    "[email protected]",
    "Pesanan #1234 Terkirim",
    "Halo Budi, pesanan #1234 sudah kami kirim hari ini. Paket berisi dua produk dan diperkirakan tiba dalam dua hari kerja. Anda bisa melacak statusnya kapan saja di halaman pesanan."
);

echo str_replace("\r\n", "\\r\\n\n", $pesan);

Output yang Dihasilkan:

text
From: Toko Example <[email protected]>\r\n
To: [email protected]\r\n
Subject: Pesanan #1234 Terkirim\r\n
MIME-Version: 1.0\r\n
Content-Type: text/plain; charset=UTF-8\r\n
\r\n
Halo Budi, pesanan #1234 sudah kami kirim hari ini. Paket berisi dua\r\n
produk dan diperkirakan tiba dalam dua hari kerja. Anda bisa melacak\r\n
statusnya kapan saja di halaman pesanan.\r\n

Mari kita bedah kode di atas:

  1. buatHeaderEmail(): merakit lima baris header lalu merekatkannya dengan implode("\r\n", ...) — MIME mewajibkan akhir baris bergaya CRLF (\r\n), bukan LF saja.
  2. wordwrap($isi, 70, "\r\n"): memotong isi pesan maksimal 70 kolom, jauh di bawah batas 998 karakter per baris pada RFC 5322, agar pesan nyaman dibaca di klien lawas.
  3. buatPesanMime(): merekatkan header dan isi dengan satu baris kosong (\r\n\r\n) sebagai pemisah wajib, lalu menambahkan CRLF penutup.
  4. str_replace("\r\n", "\\r\\n\n", $pesan): murni tampilan — mengubah setiap CRLF menjadi teks \r\n yang terlihat supaya kita bisa membaca struktur pesan tanpa karakter tak kasatmata.

String hasil buatPesanMime() inilah yang akan dikirim sebagai isi perintah DATA pada percakapan SMTP di atas. Struktur From, To, Subject, dan pemisah header-isi adalah kontrak yang sama dipakai semua library email.

Berbicara Langsung ke Server SMTP #

PHP dapat membuka koneksi TCP mentah dengan fsockopen(), lalu bertukar perintah SMTP memakai fwrite() dan fgets(). Kerangka dasarnya:

php
<?php
$socket = fsockopen("tcp://smtp.relay-example.com", 587, $kodeGalat, $pesanGalat, 10.0);

if ($socket === false) {
    throw new RuntimeException("Gagal terhubung: $pesanGalat ($kodeGalat)");
}

stream_set_timeout($socket, 10);
fwrite($socket, "EHLO app.toko.example\r\n");
$jawaban = fgets($socket, 1024); // misalnya "250-smtp.relay-example.com\r\n"

fclose($socket);

Contoh ini memerlukan server SMTP nyata sehingga kita lewati dari pengujian otomatis. Pada praktiknya, setelah menerima 250 STARTTLS kita naikkan koneksi menjadi TLS dengan stream_socket_enable_crypto(), mengulang EHLO, lalu mengirim autentikasi AUTH LOGIN dengan kredensial terenkode base64. Port yang dipakai: 587 untuk submission dengan STARTTLS, 465 untuk SMTPS (TLS langsung dari awal), dan 25 hanya untuk relay antar server.

Kredensial dari Environment Variable #

Kredensial SMTP tidak boleh ditulis langsung di dalam kode. Cara idiomatiknya adalah membaca dari environment variable:

php
<?php
$smtpHost = getenv("SMTP_HOST");
if ($smtpHost === false) {
    $smtpHost = "127.0.0.1";
}

$smtpPort = (int) (getenv("SMTP_PORT") ?: "25");

Karena getenv() bergantung pada keadaan lingkungan, kita pisahkan logika pembacaan menjadi fungsi murni yang menerima array env sehingga tetap dapat diuji:

php
<?php
function bacaKonfigurasiSmtp(array $env): array
{
    return [
        "host" => $env["SMTP_HOST"] ?? "127.0.0.1",
        "port" => (int) ($env["SMTP_PORT"] ?? "25"),
        "pengguna" => $env["SMTP_USER"] ?? "",
        "sandi" => $env["SMTP_PASS"] ?? "",
    ];
}

$konfigurasi = bacaKonfigurasiSmtp([
    "SMTP_HOST" => "smtp.relay-example.com",
    "SMTP_PORT" => "587",
    "SMTP_USER" => "[email protected]",
]);

foreach ($konfigurasi as $kunci => $nilai) {
    $tampilan = ($kunci === "sandi") ? "********" : (string) $nilai;
    echo $kunci . " = " . $tampilan . "\n";
}

Output yang Dihasilkan:

text
host = smtp.relay-example.com
port = 587
pengguna = [email protected]
sandi = ********

Fungsi bacaKonfigurasiSmtp() menerima salinan environment sebagai parameter, memberi nilai bawaan dengan operator ??, dan mengubah port menjadi integer. Dalam pengujian kita menyuntikkan array buatan; di produksi pemanggilannya cukup bacaKonfigurasiSmtp(getenv()). Perhatikan juga bahwa loop pencetakan menyamarkan nilai sandi — kebiasaan kecil yang mencegah kredensial bocor ke log.

Ringkasan #

mail() hanyalah pembungkus tipis ke MTA lokal sehingga tidak andal di luar mesin yang terpasang sempurna. Pengiriman yang sesungguhnya terjadi lewat percakapan SMTP — EHLO, MAIL FROM, RCPT TO, DATA — di atas koneksi ber-TLS dengan autentikasi, membawa pesan MIME berformat header dan isi terpisah baris kosong. Menulis semua itu tangan demi tangan menyakitkan, dan di situlah library email masuk: chapter berikutnya membandingkan Symfony Mailer, PHPMailer, dan Laminas Mail.

Referensi #