Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
mailtrap-development
mailtrap-development
darvis/mailtrap
claude-code
email
laravel
webhook
mailtrap
email-validation
livewire
mail-log
laravel-boost

Work with darvis/mailtrap. Use it to tag outgoing mail with a model, query mail logs, validate or block email addresses, handle blocked sends, and test the Mailtrap webhook.

Install
php artisan boost:add-skill darvis/mailtrap

Save this content to: .claude/skills/mailtrap-development/SKILL.md

# darvis/mailtrap development

## When to use this skill

Use this skill when code sends mail in an application that has `darvis/mailtrap` installed, when you query `mail_logs` or `email_validations`, or when you work on Mailtrap webhook handling or its tests.

## How a send is processed

1. On `MessageSending`, every To, Cc and Bcc address runs through `EmailValidation::validateEmail()`: a format check, then the MX record and whether it resolves to an IP. A missing MX record or a Null MX blocks the address; a DNS lookup that fails (timeout, SERVFAIL) lets it through without storing a verdict. The DNS lookups are skipped when another address on the domain is already valid. Set `validation.enabled` to false to skip validation altogether.
2. All recipients are checked before any row is written. A `blocked` address aborts the send: every recipient of that mail gets a `MailLog` row with status `550` (the others with `Not sent: … is blocked` as error), and a `TransportException` is thrown.
3. Otherwise one `MailLog` row per recipient is created with `status_code = null`. Every row of the mail shares the `X-Message-ID` header.
4. On `MessageSent` those rows become `200`. Mailtrap webhook events later update the row for that recipient. The row is found through the `x_message_id` custom variable (the `X-MT-Custom-Variables` header), otherwise through Mailtrap's `message_id`.

A failure to write the log is reported to the exception handler and never stops the mail. Addresses are trimmed and compared in lower case in `email_validations` and in `MailLog::toRecipient()`.

## Tagging mail with a model

Set `X-Mail-Type`, `X-Mail-Model` (a class name or morph alias) and `X-Mail-Model-ID` as headers on the Mailable. They are copied to `type`, `model` and `model_id`, and `$log->related` resolves the model.

## Querying logs

Use the scopes instead of comparing status codes:

```php
use Darvis\Mailtrap\Models\MailLog;

MailLog::successful();   // status_code "200"
MailLog::pending();      // not yet confirmed as sent
MailLog::failed();       // any status other than 200
MailLog::blocked();      // stopped before sending (550)
MailLog::delivered();    // delivered_at is set by a delivery event
MailLog::opened();       // opened_at is set by an open event
MailLog::clicked();      // clicked_at is set by a click event
MailLog::forModel(Order::class, $order->id)->toRecipient($email)->latest()->get();
```

`delivered_at`, `opened_at` and `clicked_at` hold the first time Mailtrap reported that event; a later open does not move `opened_at`. Open and click events only arrive when tracking is switched on for the sending domain in Mailtrap, and an open is a hint, not proof: mail clients that load images on their own count as opened, and clients that block images never do. `status_code` stays `"200"` for these events, so use the scopes or the columns, not the status, to tell an opened mail from a sent one.

Logs older than `logging.cleanup_after_days` are pruned by `php artisan model:prune --model="Darvis\Mailtrap\Models\MailLog"`. Schedule that command, because Laravel only discovers models in `app/Models` by itself.

## Address verdicts

| Status | Set by | Stops sending |
| --- | --- | --- |
| `EmailValidation::VALID` | local checks, delivery/open/click events, `markAsValid()` | no |
| `EmailValidation::INVALID` | bounce/spam/reject events, `markAsInvalid()` | no |
| `EmailValidation::BLOCKED` | failed format or MX check, `markAsBlocked()` | yes |

- Use `markAsBlocked($email, $reason)` only to halt mail to that one address, for example after a complaint. It never affects other addresses on the domain.
- Blocks from a local check (reasons in `EmailValidation::LOCAL_CHECK_REASONS`) expire after `validation.cache_duration` seconds and are then checked again. Manual blocks never expire.
- To check many addresses without DNS lookups, use `EmailValidation::bulkValidationStatus($emails)`. To validate the addresses that have no verdict yet, use `bulkValidationWithCheck($emails, true)`.

## Reacting to events

```php
use Darvis\Mailtrap\Events\MailBlocked;
use Darvis\Mailtrap\Events\MailtrapEventReceived;

Event::listen(function (MailtrapEventReceived $event): void {
    // $event->type, $event->email, $event->payload (raw Mailtrap event), $event->mailLog (null for ignored types)
    if ($event->type === 'unsubscribe') {
        // ...
    }
});

Event::listen(function (MailBlocked $event): void {
    // $event->email, $event->reason, $event->message (Symfony Email), $event->mailLog
});
```

`MailtrapEventReceived` also fires for `unsubscribe`, `soft bounce` and `suspension`, which the package does not act on. A listener that throws does not break the webhook batch.

## Inbox access

The inbox route ends with `Darvis\Mailtrap\Http\Middleware\AuthorizeInbox`, which aborts with 403 unless `Gate::allows('viewMailtrap')`. The package default allows the `local` environment only; a gate of the host application replaces it. The component repeats the check in `boot()`, `render()` and every action. In a host test that opens the inbox, define the gate first: `Gate::define('viewMailtrap', fn ($user = null) => true);`.

## Testing

- Use the `array` mailer and give recipients a verdict up front with `EmailValidation::markAsValid()`. Otherwise tests run real DNS lookups.
- Webhook requests must be signed. Sign the raw JSON body with HMAC-SHA256 and send it in the `Mailtrap-Signature` header:

```php
use Darvis\Mailtrap\Support\MailtrapConfig;

$body = json_encode(['events' => [[
    'event' => 'bounce', 'email' => '[email protected]', 'message_id' => 'abc', 'response_code' => 550,
]]]);

// Set manta_mailtrap.webhook.secret in the test first; without a secret every call gets 403.
$this->call('POST', '/api/webhooks/mailtrap', server: [
    'CONTENT_TYPE' => 'application/json',
    'HTTP_ACCEPT' => 'application/json',
    'HTTP_MAILTRAP_SIGNATURE' => hash_hmac('sha256', $body, MailtrapConfig::webhookSecret()),
], content: $body)->assertOk();
```

The event names in the payload are `delivery`, `open`, `click`, `bounce`, `spam` and `reject`. `soft bounce`, `unsubscribe` and `suspension` are accepted but not acted on.

darvis/mailtrap development

When to use this skill

Use this skill when code sends mail in an application that has darvis/mailtrap installed, when you query mail_logs or email_validations, or when you work on Mailtrap webhook handling or its tests.

How a send is processed

  1. On MessageSending, every To, Cc and Bcc address runs through EmailValidation::validateEmail(): a format check, then the MX record and whether it resolves to an IP. A missing MX record or a Null MX blocks the address; a DNS lookup that fails (timeout, SERVFAIL) lets it through without storing a verdict. The DNS lookups are skipped when another address on the domain is already valid. Set validation.enabled to false to skip validation altogether.
  2. All recipients are checked before any row is written. A blocked address aborts the send: every recipient of that mail gets a MailLog row with status 550 (the others with Not sent: … is blocked as error), and a TransportException is thrown.
  3. Otherwise one MailLog row per recipient is created with status_code = null. Every row of the mail shares the X-Message-ID header.
  4. On MessageSent those rows become 200. Mailtrap webhook events later update the row for that recipient. The row is found through the x_message_id custom variable (the X-MT-Custom-Variables header), otherwise through Mailtrap's message_id.

A failure to write the log is reported to the exception handler and never stops the mail. Addresses are trimmed and compared in lower case in email_validations and in MailLog::toRecipient().

Tagging mail with a model

Set X-Mail-Type, X-Mail-Model (a class name or morph alias) and X-Mail-Model-ID as headers on the Mailable. They are copied to type, model and model_id, and $log->related resolves the model.

Querying logs

Use the scopes instead of comparing status codes:

use Darvis\Mailtrap\Models\MailLog;

MailLog::successful();   // status_code "200"
MailLog::pending();      // not yet confirmed as sent
MailLog::failed();       // any status other than 200
MailLog::blocked();      // stopped before sending (550)
MailLog::delivered();    // delivered_at is set by a delivery event
MailLog::opened();       // opened_at is set by an open event
MailLog::clicked();      // clicked_at is set by a click event
MailLog::forModel(Order::class, $order->id)->toRecipient($email)->latest()->get();

delivered_at, opened_at and clicked_at hold the first time Mailtrap reported that event; a later open does not move opened_at. Open and click events only arrive when tracking is switched on for the sending domain in Mailtrap, and an open is a hint, not proof: mail clients that load images on their own count as opened, and clients that block images never do. status_code stays "200" for these events, so use the scopes or the columns, not the status, to tell an opened mail from a sent one.

Logs older than logging.cleanup_after_days are pruned by php artisan model:prune --model="Darvis\Mailtrap\Models\MailLog". Schedule that command, because Laravel only discovers models in app/Models by itself.

Address verdicts

Status Set by Stops sending
EmailValidation::VALID local checks, delivery/open/click events, markAsValid() no
EmailValidation::INVALID bounce/spam/reject events, markAsInvalid() no
EmailValidation::BLOCKED failed format or MX check, markAsBlocked() yes
  • Use markAsBlocked($email, $reason) only to halt mail to that one address, for example after a complaint. It never affects other addresses on the domain.
  • Blocks from a local check (reasons in EmailValidation::LOCAL_CHECK_REASONS) expire after validation.cache_duration seconds and are then checked again. Manual blocks never expire.
  • To check many addresses without DNS lookups, use EmailValidation::bulkValidationStatus($emails). To validate the addresses that have no verdict yet, use bulkValidationWithCheck($emails, true).

Reacting to events

use Darvis\Mailtrap\Events\MailBlocked;
use Darvis\Mailtrap\Events\MailtrapEventReceived;

Event::listen(function (MailtrapEventReceived $event): void {
    // $event->type, $event->email, $event->payload (raw Mailtrap event), $event->mailLog (null for ignored types)
    if ($event->type === 'unsubscribe') {
        // ...
    }
});

Event::listen(function (MailBlocked $event): void {
    // $event->email, $event->reason, $event->message (Symfony Email), $event->mailLog
});

MailtrapEventReceived also fires for unsubscribe, soft bounce and suspension, which the package does not act on. A listener that throws does not break the webhook batch.

Inbox access

The inbox route ends with Darvis\Mailtrap\Http\Middleware\AuthorizeInbox, which aborts with 403 unless Gate::allows('viewMailtrap'). The package default allows the local environment only; a gate of the host application replaces it. The component repeats the check in boot(), render() and every action. In a host test that opens the inbox, define the gate first: Gate::define('viewMailtrap', fn ($user = null) => true);.

Testing

  • Use the array mailer and give recipients a verdict up front with EmailValidation::markAsValid(). Otherwise tests run real DNS lookups.
  • Webhook requests must be signed. Sign the raw JSON body with HMAC-SHA256 and send it in the Mailtrap-Signature header:
use Darvis\Mailtrap\Support\MailtrapConfig;

$body = json_encode(['events' => [[
    'event' => 'bounce', 'email' => '[email protected]', 'message_id' => 'abc', 'response_code' => 550,
]]]);

// Set manta_mailtrap.webhook.secret in the test first; without a secret every call gets 403.
$this->call('POST', '/api/webhooks/mailtrap', server: [
    'CONTENT_TYPE' => 'application/json',
    'HTTP_ACCEPT' => 'application/json',
    'HTTP_MAILTRAP_SIGNATURE' => hash_hmac('sha256', $body, MailtrapConfig::webhookSecret()),
], content: $body)->assertOk();

The event names in the payload are delivery, open, click, bounce, spam and reject. soft bounce, unsubscribe and suspension are accepted but not acted on.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core