laramailer / laravel
Laravel mail transport and PHP SDK for LaraMailer
Requires
- php: ^8.2
- guzzlehttp/guzzle: ^7.0
- illuminate/mail: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- symfony/mailer: ^7.0|^8.0
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0|^11.0
- phpunit/phpunit: ^11.0|^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Laravel mail transport + PHP SDK for a LaraMailer instance.
Installation
Requires PHP 8.2+ and Laravel 11, 12 or 13.
composer require laramailer/laravel
Configuration
config/mail.php:
'mailers' => [ 'laramailer' => ['transport' => 'laramailer'], ],
.env:
MAIL_MAILER=laramailer LARAMAILER_ENDPOINT=https://mail.example.com LARAMAILER_TOKEN=your_access_token LARAMAILER_ACCOUNT_ID=1 LARAMAILER_TRACKING_ENABLED=true
LARAMAILER_ACCOUNT_ID may be the numeric account id or the UUID shown in the dashboard.
Sending through Laravel Mail
Mail::to($supplier->email)->send( (new QuotationRequestMail($quotation)) ->metadata('contract_id', $contract->id) ->metadata('procedure_id', $procedure->id) ->metadata('user_id', auth()->id()) ->tag('quotation') );
-
metadata()values are stored on the LaraMailer task and are filterable. -
tag()values are joined intometadata.tags. -
Custom header
X-Idempotency-Keybecomes theIdempotency-Keyrequest header (safe retries). -
Custom header
X-Tracking-Enabled: falsedisables open/click tracking for that email. -
Custom header
X-Dry-Run: 1makes the send a dry run: the message is built and stored (and delivered to the server's dry-run sink such as Mailpit when one is configured) but never reaches the recipients. An account can also be switched to permanent dry run in the LaraMailer dashboard, which applies to every send regardless of this header. Explicit SDK:'dry_run' => truein$data. -
Mail::send()returns theSentMessagewhosegetMessageId()is the LaraMailer task id, so a caller can store it and follow the message later:$sent = Mail::mailer('laramailer')->to($supplier->email)->send(new QuotationRequestMail($request)); $request->update(['laramailer_task_id' => $sent?->getMessageId()]); // later, for monitoring $task = LaraMailer::mail()->getTask((int) $request->laramailer_task_id); $task['data']['status']; // pending|processing|completed|failed|cancelled|scheduled $task['data']['delivery_status']; // accepted|delivered|delayed|bounced $task['data']['message_id']; // the RFC Message-ID, once the send has actually run
The task id exists immediately; the RFC
Message-IDonly appears after the queued send runs, which is why the task id is the handle to store.Mail::queue()returns nothing, so useMail::send()(or the explicit SDK call, whose response carriesdata.id) when you need the id. -
Custom header
X-Send-At: 2026-09-10T08:00:00+01:00schedules the send (up to 30 days ahead); the task is created with statusscheduledand can be cancelled withLaraMailer::mail()->deleteTask($id)until it is dispatched. -
Mail::send()returns aSentMessage;getMessageId()is the LaraMailer task id. -
Embedded/inline images (
embed()) are not forwarded; use absolute image URLs in HTML. -
Emails are always sent from the account's own address. A From name in the mailable is kept as the display name; the From address itself is ignored.
Reading history and proof
use LaraMailer\Sdk\Facades\LaraMailer; $tasks = LaraMailer::mail()->listTasks(['metadata' => ['contract_id' => 42], 'status' => 'completed']); $task = LaraMailer::mail()->getTask($taskId); // $task['data']['sent_at'], ['smtp_response'], ['opened_at'], ['delivered_at'], ['tracking_events'], ['eml_url'] $eml = LaraMailer::mail()->downloadEml($taskId); // raw RFC 822 message as sent
Delivery status
getTask() (and each entry from listTasks()) returns delivery_status, one of accepted, delivered, delayed, or bounced — updated as the server reads bounce notifications from the account's mailbox. When it is bounced or delayed, the task also carries bounce_type, bounce_code, bounce_reason, bounce_recipient, bounced_at, and a bounce_events list; listTasks(['delivery_status' => 'bounced']) filters by it.
Explicit send
LaraMailer::mail()->send($accountId, [ 'to' => [['email' => 'supplier@example.com', 'name' => 'Supplier']], 'subject' => 'Quotation request', 'html_body' => '<p>...</p>', 'text_body' => '...', 'metadata' => ['contract_id' => 42], ], idempotencyKey: 'quotation-42-supplier-9');
Add send_at (ISO 8601 with offset) to schedule the send instead of sending immediately:
LaraMailer::mail()->send($accountId, [ 'to' => [['email' => 'supplier@example.com', 'name' => 'Supplier']], 'subject' => 'Quotation request', 'html_body' => '<p>...</p>', 'send_at' => '2026-09-10T08:00:00+01:00', ]);
Sending with a template
Templates are authored and published in the LaraMailer dashboard (per team). Send data, not HTML:
LaraMailer::mail()->sendTemplate( accountId: 1, template: 'quotation-request', variables: ['ref' => 'CP/2026/17', 'deadline' => '2026-09-20', 'items' => [['name' => 'Paper', 'qty' => 10]]], message: ['to' => [['email' => 'supplier@example.com']], 'metadata' => ['contract_id' => 42]], idempotencyKey: 'quotation-42-supplier-9', ); LaraMailer::templates()->list(); LaraMailer::templates()->preview(1, ['ref' => 'CP/2026/17']);
Templates use Mustache syntax ({{ref}}, {{#items}}…{{/items}}, helpers {{#date}}, {{#money}}, {{#upper}}). Variables are validated against the template's schema; the task records the exact template version used. The Laravel mail transport is not involved — it always carries fully rendered mail.
Accounts
// List all accounts $accounts = $client->accounts()->list(); // Get a specific account $account = $client->accounts()->get($accountId); // Create a new account $newAccount = $client->accounts()->create([ 'name' => 'My Account', 'email' => 'me@example.com', // ... other fields ]); // Update an account $client->accounts()->update($accountId, [ 'name' => 'Updated Name', ]); // Delete an account $client->accounts()->delete($accountId);
Attachments
Upload an attachment to be used in emails.
$attachment = $client->attachments()->upload('/path/to/image.png'); // Returns ['path' => '...', 'url' => '...']
OAuth2
// List OAuth2 configs $configs = $client->oauth2()->list(); // Initiate OAuth2 flow $url = $client->oauth2()->initiate($configId);
Testing
composer install
composer test
CI runs the suite on Orchestra Testbench across a matrix of Laravel 11, 12 and 13 (PHP 8.3 and 8.4) — see .github/workflows/tests.yml.