letmesendemail/letmesendemail-laravel

letmesend.email for Laravel.

Maintainers

Package info

github.com/letmesendemail/letmesendemail-laravel

Homepage

pkg:composer/letmesendemail/letmesendemail-laravel

Transparency log

Statistics

Installs: 103

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.2.2 2026-07-14 12:37 UTC

This package is auto-updated.

Last update: 2026-07-14 12:39:49 UTC


README

Packagist Downloads Packagist Version License

The official Laravel package for the letmesend.email API.

Full Documentation

See the comprehensive user manual for complete documentation of every resource, configuration option, mail transport, webhooks, error handling, and detailed examples.

Requirements

  • PHP 8.1+
  • Laravel 10, 11, 12, or 13

Installation

composer require letmesendemail/letmesendemail-laravel

Configuration

Set your API key in .env:

LETMESENDEMAIL_API_KEY=lms_live_...

Optionally configure the base URL, timeout, and retries:

LETMESENDEMAIL_BASE_URL=https://letmesend.email/api/v1
LETMESENDEMAIL_TIMEOUT=30
LETMESENDEMAIL_RETRIES=3

Publish the config file (optional):

php artisan vendor:publish --tag=letmesendemail-config

Environment Variables

Variable Default Description
LETMESENDEMAIL_API_KEY Your letmesend.email API key
LETMESENDEMAIL_BASE_URL https://letmesend.email/api/v1 API base URL
LETMESENDEMAIL_TIMEOUT 30 Request timeout in seconds
LETMESENDEMAIL_RETRIES 0 Retry attempts for transient failures
LETMESENDEMAIL_WEBHOOK_SECRET Webhook signing secret
LETMESENDEMAIL_WEBHOOKS_ENABLED false Enable webhook route
LETMESENDEMAIL_WEBHOOK_PATH /webhooks/letmesendemail Webhook URI path

Explicit Configuration

For tests and multi-tenant applications, configure the client explicitly:

use LetMeSendEmail\Laravel\LetMeSendEmail;

$client = new LetMeSendEmail(
    apiKey: 'lms_live_...',
    baseUrl: 'https://letmesend.email/api/v1',
    timeout: 60,
    retries: 5,
);

You may also inject a preconfigured core Client or a TransportInterface for testing:

use LetMeSendEmail\Client;
use LetMeSendEmail\Configuration;
use LetMeSendEmail\Http\GuzzleTransport;
use GuzzleHttp\Client as GuzzleClient;

$httpClient = new LetMeSendEmail(
    client: new Client(
        new Configuration(apiKey: '...', retries: 5),
        new GuzzleTransport(new GuzzleClient()),
    ),
);

Usage

Facade

use LetMeSendEmail\Laravel\Facades\LetMeSendEmail;

Emails

// Send
$email = LetMeSendEmail::emails()->send(
    from: 'Acme <hello@acme.com>',
    to: ['person@example.com'],
    subject: 'Welcome',
    html: '<p>Hello from letmesend.email</p>',
);

echo $email->getId();

// Send with template
$email = LetMeSendEmail::emails()->sendWithTemplate(
    from: 'Acme <hello@acme.com>',
    to: ['person@example.com'],
    templateId: '01ARZ3NDEKTSV4RRFFQ69G5FAV',
    templateVariables: [
        ['key' => 'USER_NAME', 'type' => 'string', 'value' => 'John'],
    ],
);

// Verify email
$result = LetMeSendEmail::emails()->verify('person@example.com');
echo $result->getStatus();

// List emails (cursor-based pagination)
$list = LetMeSendEmail::emails()->list(perPage: 20);

foreach ($list->items() as $email) {
    echo $email->getId() . ' - ' . $email->getSubject();
}

echo $list->pagination()->hasMore(); // true

// Next page
$list = LetMeSendEmail::emails()->list(perPage: 20, after: 'cursor_from_previous_page');

// Get email
$email = LetMeSendEmail::emails()->get('01kvv5dv472evp42a60sy4p7zx');

Domains

$list = LetMeSendEmail::domains()->list();
$domain = LetMeSendEmail::domains()->get($id);
$result = LetMeSendEmail::domains()->verify('example.com');

Contacts

$contact = LetMeSendEmail::contacts()->create(
    email: 'john@example.com',
    firstName: 'John',
    lastName: 'Doe',
);

$list = LetMeSendEmail::contacts()->list();
$contact = LetMeSendEmail::contacts()->get($id);
$updated = LetMeSendEmail::contacts()->update($id, firstName: 'Jane');
$result = LetMeSendEmail::contacts()->delete($id);

Contact Categories

$category = LetMeSendEmail::contactCategories()->create(name: 'New Name');
$list = LetMeSendEmail::contactCategories()->list();
$category = LetMeSendEmail::contactCategories()->get($id);
$category = LetMeSendEmail::contactCategories()->update($id, name: 'Updated');
$result = LetMeSendEmail::contactCategories()->delete($id);

Email Topics

$topic = LetMeSendEmail::emailTopics()->create(
    name: 'Product Updates',
    slug: 'product-updates',
);

$list = LetMeSendEmail::emailTopics()->list();
$topic = LetMeSendEmail::emailTopics()->get($id);
$topic = LetMeSendEmail::emailTopics()->update($id, name: 'Updated');
$result = LetMeSendEmail::emailTopics()->delete($id);

Laravel Mail Transport

Send emails through Laravel's mail system using the letmesendemail mailer.

Configuration

Set your .env mailer:

MAIL_MAILER=letmesendemail

Or configure config/mail.php:

'mailers' => [
    'letmesendemail' => [
        'transport' => 'letmesendemail',
    ],
],

Sending a Mailable

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class WelcomeEmail extends Mailable
{
    use Queueable, SerializesModels;

    public function build(): static
    {
        return $this
            ->from('noreply@acme.com')
            ->subject('Welcome!')
            ->html('<h1>Welcome</h1>');
    }
}
Mail::to('user@example.com')->send(new WelcomeEmail());

Attachments

use Illuminate\Mail\Mailables\Attachment;

$this->attachFromStorage('/path/to/report.pdf');
// or
$this->attachData('file content', 'report.txt', ['mime' => 'text/plain']);

Structural MIME headers (From, To, Cc, Bcc, Reply-To, Subject, Content-Type, MIME-Version, Date, Message-ID, Sender, Return-Path) are automatically excluded from the API custom headers.

Idempotency

Set an Idempotency-Key header on the Mailable:

use Illuminate\Support\Facades\Mail;
use Symfony\Component\Mime\Email;

Mail::to('user@example.com')->send(
    (new WelcomeEmail())
        ->withSymfonyMessage(function (Email $message) {
            $message->getHeaders()->addTextHeader('Idempotency-Key', 'my-unique-key');
        }),
);

The SDK detects Idempotency-Key case-insensitively and passes it through the core API's idempotencyKey parameter.

Queue

Mail::to('user@example.com')->queue(new WelcomeEmail());

The transport's ApiException mapping to Symfony TransportException works in queued jobs.

Testing

use Illuminate\Support\Facades\Mail;

Mail::fake();

Mail::assertSent(WelcomeEmail::class);

Pagination

List endpoints return a response with cursor-based pagination:

$list = LetMeSendEmail::emails()->list(perPage: 10);

foreach ($list->items() as $email) {
    echo $email->getId();
}

$pag = $list->pagination();
$pag->hasMore();   // bool
$pag->getTotal();  // int
$pag->getPerPage(); // int

// Next page
$next = LetMeSendEmail::emails()->list(perPage: 10, after: 'cursor_value');

// Previous page
$prev = LetMeSendEmail::emails()->list(perPage: 10, before: 'cursor_value');

Error Handling

use LetMeSendEmail\Exceptions\ValidationError;
use LetMeSendEmail\Exceptions\AuthenticationError;
use LetMeSendEmail\Exceptions\RateLimitError;
use LetMeSendEmail\Exceptions\ApiException;

try {
    LetMeSendEmail::emails()->send(/* ... */);
} catch (ValidationError $e) {
    // field-level errors: $e->getValidationErrors()
} catch (AuthenticationError $e) {
    // check API key
} catch (RateLimitError $e) {
    // retry after $e->getRetryAfter()
} catch (ApiException $e) {
    // HTTP status: $e->getHttpStatus()
    // API code: $e->getApiCode()
}
Exception HTTP Status Description
ValidationError 400, 413, 422 Request validation failed
AuthenticationError 401 Invalid or missing API key
AuthorizationError 403 Insufficient permissions
NotFoundError 404 Resource not found
ConflictError 409 Resource conflict
RateLimitError 429 Rate limit exceeded
ApiError 500+ Server error
NetworkError Connection failed
TimeoutError Request timed out

Webhooks

Configuration

Enable webhooks in your .env:

LETMESENDEMAIL_WEBHOOKS_ENABLED=true
LETMESENDEMAIL_WEBHOOK_SECRET=whsec_your_signing_secret

The webhook route is registered at /webhooks/letmesendemail by default. It uses the VerifyWebhookSignature middleware (aliased as letmesendemail.webhook) which verifies the signature before the controller executes.

How it works

  1. The middleware reads the raw request body and webhook headers, calls WebhookSignature::verify(), and stores the parsed payload on the request.
  2. If the signature is invalid, the middleware returns a 400 response.
  3. The controller reads the verified payload from the request and dispatches LetMeSendEmail\Laravel\Events\WebhookReceived.

Listening for webhooks

namespace App\Listeners;

use LetMeSendEmail\Laravel\Events\WebhookReceived;

class HandleLetMeSendEmailWebhook
{
    public function handle(WebhookReceived $event): void
    {
        match ($event->payload['event'] ?? '') {
            'email.delivered' => // handle delivery
            'email.bounced'   => // handle bounce
            default           => // unknown event
        };
    }
}

Register the listener in EventServiceProvider:

protected $listen = [
    \LetMeSendEmail\Laravel\Events\WebhookReceived::class => [
        \App\Listeners\HandleLetMeSendEmailWebhook::class,
    ],
];

Timestamp tolerance

The default tolerance is 300 seconds (5 minutes). Configure via config:

// config/letmesendemail.php
'webhooks' => [
    'tolerance' => 300,
],

Testing

composer install
vendor/bin/pest

Mail::fake with the letmesendemail transport

use Illuminate\Support\Facades\Mail;

Mail::fake();

Mail::assertSent(WelcomeEmail::class);

Changelog

See CHANGELOG.md.