P
imvdmolen.nl
Blog

Laravel Herd's binnenkomende mailverkeer testen met MailHog integratie

Werken met applicaties die binnenkomende e-mails verwerken brengt altijd uitdagingen met zich mee tijdens ontwikkeling. Laravel Herd levert standaard MailHog voor uitgaande mails, maar wat als je applicatie reageert op inkomende berichten via webhooks of IMAP-verbindingen? Dan loop je tegen het probleem aan dat je lokaal geen echte mailserver hebt draaien die post kan ontvangen en doorsturen naar je applicatie.

MailHog configureren voor inkomende post

MailHog kan meer dan alleen uitgaande mails opvangen. Door de SMTP-server functionaliteit in te schakelen, transformeer je het van een simpele mail-catcher naar een volwaardige lokale mailserver. Deze setup vereist wel wat handmatige configuratie die Herd niet automatisch voor je regelt.

Start MailHog met aangepaste parameters via de terminal. In plaats van de standaard configuratie die Herd hanteert, roep je MailHog direct aan met specifieke poorten voor zowel SMTP als de webinterface:

mailhog -smtp-bind-addr 127.0.0.1:1025 -ui-bind-addr 127.0.0.1:8025 -api-bind-addr 127.0.0.1:8025

Deze configuratie zorgt ervoor dat MailHog luistert op poort 1025 voor SMTP-verkeer en de webinterface beschikbaar maakt op poort 8025. Het voordeel van deze aanpak is dat je nu daadwerkelijk e-mails naar deze server kunt sturen vanaf externe bronnen of via lokale scripts.

Voor permanente configuratie maak je een configuratiebestand aan in je home directory. MailHog leest automatisch een bestand genaamd .mailhog als het bestaat:

echo "smtp-bind-addr = 127.0.0.1:1025
ui-bind-addr = 127.0.0.1:8025
api-bind-addr = 127.0.0.1:8025
storage = maildir
maildir-path = /tmp/mailhog" > ~/.mailhog

Webhook endpoints bouwen voor mailverwerking

Laravel applicaties die reageren op inkomende e-mails hebben meestal een webhook endpoint nodig die de mailinhoud ontvangt en verwerkt. MailHog ondersteunt dit via zijn API, waardoor je programmatisch toegang krijgt tot alle ontvangen berichten.

Bouw een controller die de MailHog API uitleest en nieuwe berichten verwerkt. Deze controller kan je periodiek aanroepen via een scheduled command of handmatig triggeren tijdens ontwikkeling:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use App\Services\IncomingMailProcessor;

class MailProcessorController extends Controller
{
    public function processIncoming()
    {
        $response = Http::get('http://127.0.0.1:8025/api/v2/messages');
        
        if ($response->failed()) {
            return response()->json(['error' => 'Could not fetch messages'], 500);
        }
        
        $messages = $response->json()['items'] ?? [];
        
        foreach ($messages as $message) {
            $this->processMessage($message);
        }
        
        return response()->json(['processed' => count($messages)]);
    }
    
    private function processMessage(array $messageData)
    {
        $processor = new IncomingMailProcessor();
        
        $emailData = [
            'from' => $messageData['From']['Mailbox'] . '@' . $messageData['From']['Domain'],
            'subject' => $messageData['Content']['Headers']['Subject'][0] ?? '',
            'body' => $messageData['Content']['Body'],
            'received_at' => $messageData['Created']
        ];
        
        $processor->handle($emailData);
        
        // Verwijder bericht na verwerking
        Http::delete("http://127.0.0.1:8025/api/v1/messages/{$messageData['ID']}");
    }
}

De service klasse die de eigenlijke mailverwerking doet, houdt zich bezig met de businesslogic. Hier parseer je de e-mailinhoud, extract je relevante informatie en sla je alles op in de database:

<?php

namespace App\Services;

class IncomingMailProcessor
{
    public function handle(array $emailData)
    {
        // Parse onderwerp voor actietype
        $actionType = $this->extractActionFromSubject($emailData['subject']);
        
        // Valideer afzender
        if (!$this->isValidSender($emailData['from'])) {
            \Log::warning('Invalid sender attempted mail processing', $emailData);
            return;
        }
        
        // Verwerk op basis van actietype
        switch ($actionType) {
            case 'support_request':
                $this->createSupportTicket($emailData);
                break;
            case 'order_confirmation':
                $this->processOrderConfirmation($emailData);
                break;
            default:
                $this->handleUnknownAction($emailData);
        }
    }
    
    private function extractActionFromSubject(string $subject): string
    {
        if (str_contains(strtolower($subject), 'support')) {
            return 'support_request';
        }
        
        if (str_contains(strtolower($subject), 'order')) {
            return 'order_confirmation';
        }
        
        return 'unknown';
    }
    
    private function isValidSender(string $email): bool
    {
        $allowedDomains = ['trusted-partner.com', 'internal.company.com'];
        
        $domain = substr(strrchr($email, '@'), 1);
        
        return in_array($domain, $allowedDomains);
    }
}

Testscripts schrijven voor mailflows

Handmatig e-mails versturen naar je lokale MailHog server wordt snel vervelend. Artisan commands die verschillende e-mailscenario's simuleren versnellen je development workflow aanzienlijk.

Maak een command die verschillende typen inkomende berichten genereert. Deze command stuurt direct naar de SMTP-poort van MailHog en simuleert realistische e-mailinhoud:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Swift_Mailer;
use Swift_SmtpTransport;
use Swift_Message;

class SimulateIncomingMail extends Command
{
    protected $signature = 'mail:simulate {type} {--count=1}';
    protected $description = 'Simulate incoming emails for testing';

    public function handle()
    {
        $type = $this->argument('type');
        $count = (int) $this->option('count');
        
        $transport = (new Swift_SmtpTransport('127.0.0.1', 1025));
        $mailer = new Swift_Mailer($transport);
        
        for ($i = 0; $i < $count; $i++) {
            $message = $this->createMessageForType($type, $i);
            $result = $mailer->send($message);
            
            if ($result) {
                $this->info("Sent {$type} email #{$i}");
            } else {
                $this->error("Failed to send {$type} email #{$i}");
            }
        }
    }
    
    private function createMessageForType(string $type, int $index): Swift_Message
    {
        switch ($type) {
            case 'support':
                return $this->createSupportMessage($index);
            case 'order':
                return $this->createOrderMessage($index);
            default:
                return $this->createGenericMessage($index);
        }
    }
    
    private function createSupportMessage(int $index): Swift_Message
    {
        $message = (new Swift_Message())
            ->setSubject("Support Request #{$index} - Login Issues")
            ->setFrom(['[email protected]' => 'John Doe'])
            ->setTo(['[email protected]'])
            ->setBody("Hi,\n\nI'm having trouble logging into my account. Can you help?\n\nThanks,\nJohn");
            
        return $message;
    }
    
    private function createOrderMessage(int $index): Swift_Message
    {
        $orderId = 1000 + $index;
        
        $message = (new Swift_Message())
            ->setSubject("Order Confirmation #{$orderId}")
            ->setFrom(['[email protected]' => 'Partner System'])
            ->setTo(['[email protected]'])
            ->setBody("Order #{$orderId} has been confirmed.\n\nTotal: €99.99\nItems: 3");
            
        return $message;
    }
}

Doordat je verschillende scenario's kunt simuleren met één commando, test je snel edge cases en verschillende mailformaten. Het commando php artisan mail:simulate support --count=5 genereert vijf support-gerelateerde e-mails die je applicatie kan verwerken.

Integratie met Laravel's Mail facade

Laravel's ingebouwde mail functionaliteit werkt naadloos samen met deze setup. Door de juiste configuratie in je .env bestand kunnen zowel uitgaande als inkomende mails via dezelfde MailHog instantie lopen.

Pas je mail configuratie aan om de lokale SMTP-server te targetten. In plaats van de standaard Herd configuratie overschrijf je de SMTP instellingen:

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

Deze configuratie zorgt ervoor dat alle uitgaande mails via Mail::send() ook in MailHog terechtkomen, naast de inkomende berichten die je via de webhook verwerkt. Het resultaat is een complete mail-testomgeving waarin je bidirectionele e-mailcommunicatie kunt simuleren en debuggen.

Zelf heb ik deze setup vaak ingezet voor projecten waarbij klanten via e-mail opdrachten doorsturen of statusupdates ontvangen. Door alles lokaal te kunnen testen zonder externe mailservers, voorkom je dat testberichten bij echte klanten terechtkomen en kun je snel itereren op de mailverwerkingslogica. Het scheelt enorm veel tijd in vergelijking met het constant moeten wisselen tussen verschillende omgevingen of het opzetten van aparte staging-mailservers.