บทความ

ทดสอบอีเมลใน Laravel ใช้เครื่องมืออะไรได้บ้าง ตั้งแต่ตอนพัฒนาจนขึ้น production

ตอนพัฒนาเราต้องดักอีเมลไว้ไม่ให้ถึงลูกค้า พอขึ้น production ต้องดูย้อนหลังได้ว่าระบบส่งอะไรถึงใคร โพสต์นี้รวมเครื่องมือของทั้งสองช่วง

เผยแพร่
เวลาอ่าน
7 นาที

ทดสอบอีเมล มีสองเรื่องที่ต้องแยกกัน

พอพูดว่าทดสอบอีเมล จริง ๆ เรามักหมายถึงสองเรื่อง

  1. ตอนพัฒนา เราอยากเห็นว่าอีเมลหน้าตาเป็นยังไง ลิงก์ถูกไหม โดยที่ลูกค้าไม่ได้รับไปด้วย
  2. ตอนระบบใช้งานจริง ลูกค้าโทรมาบอกว่าไม่ได้รับอีเมลยืนยันคำสั่งซื้อ เราต้องตอบให้ได้ว่าระบบส่งไปหรือยัง ส่งถึงใคร เมื่อไร ถ้าส่งไม่ผ่านก็ต้องรู้ว่าเพราะอะไร

เรื่องแรกต้องมีที่ดักอีเมลไว้ เรื่องที่สองต้องมีบันทึกการส่ง เครื่องมือของสองเรื่องนี้เลยต่างกัน

ทำไมไม่ควรทดสอบด้วย Gmail จริง

หลายทีมทดสอบด้วยการสร้าง App Password ของ Gmail แล้วใส่ใน .env ให้แอปส่งผ่าน SMTP ของ Gmail (smtp.gmail.com) SMTP คือช่องทางมาตรฐานที่โปรแกรมใช้ส่งอีเมลเข้า mail server วิธีนี้ใช้ได้ แต่มีเรื่องที่ต้องระวัง

  • อีเมลออกไปจริง ถ้าฐานข้อมูลในเครื่องดึงมาจาก production หรือมีอีเมลลูกค้าจริงอยู่ กดทดสอบครั้งเดียวก็อาจส่งใบแจ้งหนี้หรือลิงก์รีเซ็ตรหัสผ่านไปถึงลูกค้าตัวจริง
  • ติด limit Gmail ส่งได้ราว 500 ฉบับต่อวัน ถ้าเกิน Google จะระงับการส่งของบัญชีนั้นชั่วคราว ทดสอบงานที่ส่งอีเมลทีละหลายฉบับไม่กี่รอบก็เต็มแล้ว
  • รหัสหลุดได้ ต้องเปิด 2-Step Verification ก่อนถึงจะสร้าง App Password ได้ และรหัสนี้ใช้ส่งอีเมลในนามบัญชีนั้นได้ ถ้าไฟล์ .env หลุดไป รหัสก็หลุดไปด้วย
  • ดูยาก อีเมลกระจายไปอยู่ในกล่องของผู้รับแต่ละคน เราไม่มีที่ไหนให้ดูรวมว่าระบบส่งอะไรออกไปบ้าง

ตอนพัฒนาเราจึงควรใช้ที่ดักอีเมลแทน แอปจะส่งถึงที่อยู่ไหนก็ได้ อีเมลทุกฉบับจะมารวมอยู่ในเครื่องเรา

วิธีที่ง่ายที่สุดคือ MAIL_MAILER=log

Laravel เลือกวิธีส่งอีเมลจากค่า MAIL_MAILER ใน .env ซึ่งเรียกว่า mailer หรือ driver หนึ่งในนั้นคือ driver log ที่เขียนอีเมลลงไฟล์ log แทนการส่งจริง

MAIL_MAILER=log

อีเมลจะไปอยู่ใน storage/logs/laravel.log เป็นข้อความดิบทั้ง header และ HTML ใช้เช็กเร็ว ๆ ได้ว่าแอปส่งอีเมลไหม ส่งถึงใคร subject อะไร แต่เราจะไม่เห็นหน้าตาจริงของอีเมล และถ้าอีเมลยาวหรือมีไฟล์แนบ log ก็อ่านยาก

ดูหน้าตาอีเมลจริงด้วย Mailpit

Mailpit ทำตัวเป็น SMTP server ในเครื่องเรา รับอีเมลทุกฉบับไว้ แล้วแสดงในหน้าเว็บเหมือนกล่องจดหมาย Mailpit เป็น open source ใช้ได้โดยไม่มีค่าใช้จ่าย และเป็นตัวที่คนย้ายมาใช้แทน MailHog ซึ่งไม่มีใครพัฒนาต่อแล้ว (MailHog ออกเวอร์ชันล่าสุดปี 2020) ถ้าโปรเจกต์ไหนยังใช้ MailHog อยู่ ย้ายมาได้เลย พอร์ตก็เหมือนเดิม คือ 1025 สำหรับ SMTP และ 8025 สำหรับหน้าเว็บ

Mailpit เป็นโปรแกรมไฟล์เดียว ติดตั้งได้หลายทาง

brew install mailpit
brew services start mailpit

บน Windows ให้ดาวน์โหลดจากหน้า releases แตกไฟล์แล้วเปิด mailpit.exe ได้เลย

จากนั้นตั้งให้แอปส่งเข้า Mailpit

MAIL_MAILER=smtp
MAIL_HOST=127.0.0.1
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null

ถ้าใช้ Laravel Sail อยู่แล้ว จะมี Mailpit มาให้ ตั้ง MAIL_HOST=mailpit แล้วเปิดหน้าเว็บที่ http://localhost:8025

กล่องจดหมายของ Mailpit ที่ดักอีเมลจากแอปไว้ทั้งหมด

แอปส่งถึงใครก็ตาม อีเมลทุกฉบับมารวมอยู่ในกล่องเดียว

เปิดดูอีเมลยืนยันคำสั่งซื้อใน Mailpit

กดเข้าไปดูได้ทั้งหน้าตาจริง ข้อความล้วน header และไฟล์แนบ

นอกจากหน้าตาอีเมล Mailpit ยังมีแท็บ HTML Check ที่บอกว่าอีเมลของเราใช้ CSS หรือ tag ตัวไหนที่โปรแกรมอ่านอีเมลบางตัวไม่รองรับ และแท็บ Link Check ที่ลองเปิดทุกลิงก์ในอีเมลให้

แท็บ HTML Check ของ Mailpit

HTML Check บอกว่าโปรแกรมอ่านอีเมลรองรับ HTML ของเรากี่เปอร์เซ็นต์ และไล่ให้ดูทีละ property ว่าตัวไหนมีปัญหา

ใช้ XAMPP, Laragon หรือ MAMP อยู่ ใช้ Mailpit ได้ไหม

ได้ครับ นักพัฒนา PHP หลายคนเริ่มจากชุดโปรแกรมที่รวม Apache, PHP และ MySQL มาให้ หลายทีมยังใช้ XAMPP หรือ Laragon อยู่ และเรายังเจอ AppServ ในบทความสอนรุ่นเก่า แต่ละตัวมีเรื่องอีเมลแบบนี้

ชุดโปรแกรม มีอะไรเรื่องอีเมล ใช้กับ Laravel ยังไง
XAMPP รุ่น Windows มี Mercury Mail และโฟลเดอร์ sendmail มาด้วย แต่ต้องไปตั้งค่าใน php.ini เอง เปิด Mailpit คู่กัน แล้วตั้ง .env ตามข้างบน
Laragon รุ่นใหม่มี Mailpit มาในตัว ที่พอร์ต 1025 และ 8025 ตั้งแต่รุ่น 7 ถ้าใช้ทำงานเชิงพาณิชย์ต้องซื้อ license ตั้ง .env อย่างเดียว
MAMP รุ่น PRO บน Mac ตั้งค่า Postfix ให้ส่งออกผ่าน SMTP ข้างนอกได้ ซึ่งเป็นการส่งจริง ไม่ได้ดักไว้ เปิด Mailpit คู่กัน
AppServ ไม่มี mail server มาให้ ควรย้ายไปใช้ตัวอื่น

Mercury Mail กับ sendmail ของ XAMPP มีไว้ให้ฟังก์ชัน mail() ของ PHP ส่งอีเมลได้บน Windows แต่ Laravel ไม่ได้ใช้ mail() แอปส่งผ่าน SMTP ตามค่าใน .env เอง เราจึงไม่ต้องตั้งค่า sendmail หรือ Mercury เลย แค่เปิด Mailpit ไว้ในเครื่องก็พอ

ใครใช้ Laravel Herd อยู่ รุ่น Pro ก็มีที่ดักอีเมลในตัว เหมือนกัน

อยากให้ทีม QA หรือลูกค้าดูอีเมลด้วย

Mailpit อยู่ในเครื่องเรา คนอื่นเปิดดูไม่ได้ ถ้าอยากให้ทีม QA หรือลูกค้าดูอีเมลจาก server ทดสอบ (staging) ลองใช้ Mailtrap Email Sandbox ซึ่งเป็นกล่องจดหมายจำลองบนคลาวด์ แอปส่งเข้า SMTP ของ Mailtrap แล้วทุกคนในทีมเปิดดูผ่านเว็บได้ แพ็กเกจฟรีรับได้ 50 อีเมลทดสอบต่อเดือน ถ้าส่งมากกว่านั้นต้องอัปเกรด

ถ้า server ทดสอบต้องส่งอีเมลออกไปจริง เช่นเปิดให้ลูกค้าลองใช้ระบบ ใช้ Mail::alwaysTo() ให้ทุกฉบับไปที่อีเมลของทีมแทนผู้รับตัวจริง

public function boot(): void
{
    if ($this->app->environment('staging')) {
        Mail::alwaysTo('[email protected]');
    }
}

Telescope ช่วยอะไรได้บ้าง

ถ้าติดตั้ง Laravel Telescope ไว้ หน้า Mail ของ Telescope จะแสดงอีเมลที่แอปส่ง กดดูตัวอย่างและดาวน์โหลดเป็นไฟล์ .eml ได้ ข้อดีคือเราเห็นด้วยว่าอีเมลฉบับนั้นมาจาก request หรือ job ไหน แต่ Telescope ไม่ได้ดักอีเมล แอปยังส่งออกไปตาม mailer ที่ตั้งไว้ เราเลยยังต้องใช้คู่กับ log หรือ Mailpit อยู่ดี

เขียนเทสต์ด้วย Mail::fake()

ที่ผ่านมาเป็นเครื่องมือสำหรับดูด้วยตา ถ้าจะเขียนเทสต์อัตโนมัติ Laravel มี Mail::fake() ที่ไม่ส่งอะไรออกไปเลย แล้วให้เราเช็กว่าแอปส่ง Mailable ถึงผู้รับที่ถูกต้อง (Mailable คือคลาสที่แทนอีเมลหนึ่งแบบ เช่นอีเมลยืนยันคำสั่งซื้อ)

use App\Mail\OrderConfirmed;
use App\Models\Order;
use Illuminate\Support\Facades\Mail;

it('sends the order confirmation to the customer', function () {
    Mail::fake();

    $order = Order::factory()->create(['email' => '[email protected]']);

    $this->post(route('checkout.complete', $order));

    Mail::assertQueued(OrderConfirmed::class, fn ($mail) => $mail->hasTo('[email protected]'));
});

ถ้า Mailable ส่งผ่าน queue (คิวงานที่ทำอยู่เบื้องหลัง) ใช้ assertQueued ถ้าส่งทันทีใช้ assertSent

แล้วบน production ล่ะ

พอขึ้น production อีเมลต้องออกไปถึงลูกค้าจริง สิ่งที่เราต้องมีคือบันทึกที่ดูย้อนหลังได้ว่าระบบส่งอะไร ถึงใคร เมื่อไร และส่งไม่ผ่านเพราะอะไร

เครื่องมือแต่ละตัวเห็นอีเมลคนละช่วง ดูจากภาพนี้

flowchart LR
  A[แอป Laravel] -->|SMTP / API| B[ผู้ให้บริการส่งอีเมล]
  B --> C[mail server ปลายทาง]
  C --> D[inbox หรือ spam]
  A -.-> L1[บันทึกฝั่งแอป<br/>mail-log, laravel-mails, Nightwatch]
  B -.-> L2[log ของผู้ให้บริการ]
  C -.->|webhook| L3[ส่งถึง / ตีกลับ]

บันทึกฝั่งแอปเห็นแค่ตอนส่งออกจากแอป ส่วนอีเมลจะถึงหรือถูกตีกลับ ต้องรอข้อมูลจากผู้ให้บริการ

log ของผู้ให้บริการส่งอีเมล

ผู้ให้บริการอย่าง Postmark, Mailgun หรือ Amazon SES มีหน้า activity ให้ดูว่าอีเมลแต่ละฉบับไปถึงไหม ตีกลับ (bounce) เพราะอะไร ที่นี่ที่เดียวที่บอกได้ว่าอีเมลถึงผู้รับจริง แต่เก็บย้อนหลังได้ไม่นาน

ถ้าแอปส่งผ่าน SMTP ของ hosting ที่ใช้ cPanel จะมีหน้า Track Delivery ให้ดูว่าแต่ละฉบับส่งสำเร็จ รอส่งใหม่ หรือล้มเหลว ส่วนจะเก็บย้อนหลังกี่วันขึ้นกับที่ผู้ให้บริการ hosting ตั้งไว้ ถ้ายังส่งผ่าน Gmail บน production ก็ติดเพดานราว 500 ฉบับต่อวันแบบเดียวกับข้างบน

อีกเรื่องคือ log ของผู้ให้บริการไม่รู้ว่าอีเมลฉบับนั้นมาจากคำสั่งซื้อไหนในระบบเรา และคนที่ต้องตอบลูกค้ามักไม่มีสิทธิ์เข้าหน้าของผู้ให้บริการ

Telescope ไม่ได้ทำมาใช้บน production

Telescope มีหน้า Mail อยู่แล้ว ดูเหมือนจะใช้ได้ แต่นอกเครื่อง local ค่าเริ่มต้นของ Telescope เก็บแค่ exception, job ที่ล้มเหลว, scheduled task, slow query และ tag ที่เราเลือกไว้ Telescope จึงไม่เก็บอีเมลให้เรา แถมคำสั่ง prune ลบข้อมูลที่เก่ากว่า 24 ชั่วโมง ทิ้งด้วย ถ้าเราไปปลดข้อจำกัดพวกนี้ Telescope จะเก็บทุก request และ query ไปด้วย ตาราง telescope_entries จะโตเร็วมาก

Laravel Nightwatch

Laravel Nightwatch เป็นบริการ monitoring บนคลาวด์ของทีม Laravel แพ็กเกจเริ่มต้นไม่มีค่าใช้จ่าย ได้ 300k events และดูย้อนหลังได้ 14 วัน หน้า Mail ของ Nightwatch แสดง Mailable แต่ละตัว subject จำนวนผู้รับ to, cc, bcc จำนวนไฟล์แนบ และเวลาที่ใช้ส่ง ใช้ดูได้ว่าอีเมลแต่ละแบบส่งไปกี่ฉบับ ช้าไหม แต่ Nightwatch ไม่เก็บเนื้ออีเมล และจากโค้ดของแพ็กเกจ เก็บแค่จำนวนผู้รับ ไม่เก็บที่อยู่อีเมล เราเลยค้นด้วยอีเมลของลูกค้าไม่ได้

backstage/laravel-mails

backstage/laravel-mails เป็นแพ็กเกจ open source ที่เก็บอีเมลขาออกลงฐานข้อมูลของแอป ทั้งผู้รับและเนื้อหา จุดเด่นคือรับ webhook จาก Postmark และ Mailgun มาอัปเดตว่าอีเมลถึงผู้รับไหม ตีกลับไหม ผู้รับเปิดอ่านหรือกดลิงก์หรือยัง ถ้าใช้สองเจ้านี้อยู่และอยากเห็นสถานะพวกนี้ในแอปเลย ตัวนี้น่าลอง

mail-log-laravel

อีกตัวคือ mail-log-laravel แพ็กเกจ open source ที่ผมเขียนขึ้นเองและใช้กับเว็บนี้ แพ็กเกจนี้เก็บทุกอีเมลที่แอปส่งลงฐานข้อมูลของแอป ทั้ง Mailable และ Notification รวมอีเมลที่มาจาก template และ record เดียวกันไว้เป็นกลุ่มเดียว แล้วแสดงในหน้า /mail-log ว่าส่งถึงใคร เมื่อไร สำเร็จหรือล้มเหลว พร้อมข้อความ error จาก mail server เราค้นด้วยอีเมลของลูกค้าได้ และค่าเริ่มต้นเก็บย้อนหลัง 365 วัน วิธีติดตั้งและหน้าจอทั้งหมดอยู่ในโพสต์ของแพ็กเกจ

mail-log-laravel ไม่ได้รับ webhook จากผู้ให้บริการ ถ้าอยากรู้ว่าอีเมลถึง inbox หรือถูกตีกลับ ยังต้องดูที่ผู้ให้บริการควบคู่กัน

สรุป ใช้ตัวไหนเมื่อไร

ใช้ เมื่อ
MAIL_MAILER=log อยากเช็กเร็ว ๆ ว่าแอปส่งอีเมลไหม โดยไม่ต้องติดตั้งอะไร
Mailpit พัฒนาในเครื่องตัวเองและอยากเห็นหน้าตาอีเมลจริง ใช้ Sail หรือ Laragon ก็มีมาให้แล้ว
Mailtrap Email Sandbox อยากให้ QA หรือลูกค้าดูอีเมลจาก server ทดสอบ
Mail::alwaysTo() server ทดสอบต้องส่งอีเมลออกจริง แต่ไม่อยากให้ถึงผู้รับตัวจริง
Mail::fake() เขียนเทสต์ว่าแอปส่งอีเมลถูกฉบับและถูกคน
Telescope ไล่ดูอีเมลคู่กับ request หรือ job ในเครื่อง dev
log ของผู้ให้บริการ อยากรู้ว่าอีเมลถึง inbox หรือถูกตีกลับ ภายในช่วงที่เขาเก็บไว้
Nightwatch ใช้ Nightwatch อยู่แล้ว และอยากดูว่าอีเมลแต่ละแบบส่งไปกี่ฉบับ ช้าไหม
backstage/laravel-mails ใช้ Postmark หรือ Mailgun และอยากเห็นสถานะส่งถึงหรือตีกลับในแอป
mail-log-laravel อยากค้นย้อนหลังในแอปเองว่าระบบส่งอะไรถึงใคร เมื่อไร และส่งไม่ผ่านเพราะอะไร

ถ้าจะเริ่มทำแค่อย่างเดียว ผมแนะนำให้เลิกทดสอบด้วย Gmail จริง แล้วตั้ง Mailpit ไว้ในเครื่อง ตั้งครั้งเดียวก็ใช้ได้กับทุกโปรเจกต์

อ่านต่อ

ล่าสุด

ดูทั้งหมด →