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

Php Aws Ses Laravel Package

daniel-zahariev/php-aws-ses

A lightweight, dependency-free PHP wrapper for Amazon SES. Send HTML and plain-text emails, handle attachments, bulk sending, and multiple recipients through the SES API without pulling in the full AWS SDK. Simple configuration with access keys and region.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a dependency-free wrapper around the SES Query (REST-like) API — no aws/aws-sdk-php, no credential chain, just cURL and three global classes. That is both its selling point (tiny footprint, works on locked-down hosts, drops into legacy PHP) and its limit.

composer require daniel-zahariev/php-aws-ses
php -m | grep curl   # ext-curl is the only hard requirement

Look here first: src/SimpleEmailService.php (sendEmail(), getErrors(), setRegion()) and src/SimpleEmailServiceMessage.php (setFrom, addTo, setSubject, setMessageHtml, setMessageText, addAttachment, addCustomHeader). Everything you will do day-to-day is those two files. The classes are global namespace (\SimpleEmailService, not DanielZahariev\...), so import with a leading backslash.

Minimal wiring in Laravel:

// config/services.php
'ses_rest' => [
    'key'    => env('AWS_ACCESS_KEY_ID'),
    'secret' => env('AWS_SECRET_ACCESS_KEY'),
    'region' => env('AWS_DEFAULT_REGION', 'eu-west-1'),
],

// AppServiceProvider::register()
$this->app->singleton(\SimpleEmailService::class, function ($app) {
    return (new \SimpleEmailService())
        ->setAccessKey(config('services.ses_rest.key'))
        ->setSecretKey(config('services.ses_rest.secret'))
        ->setRegion(config('services.ses_rest.region'))   // also selects SigV4
        ->setConnectTimeout(5)
        ->setTimeout(15);
});

First use case: password resets and order receipts, sent from a queued job, not inline in a request. Before anything else, verify your domain once (verifyDomain('example.com')) and confirm the identity shows up in the console — in the SES sandbox every recipient also needs verification, so test with your own address.

$m = new \SimpleEmailServiceMessage();
$m->setFrom('[email protected]', 'Example');
$m->addTo($user->email);
$m->setSubject('Your receipt');
$m->setMessageText($text);
$m->setMessageHtml($html);

$ses->clearErrors();
if (! $ses->sendEmail($m)) {
    throw new \RuntimeException(json_encode($ses->getErrors()));
}

Implementation Patterns

Wrap it behind your own interface, always. The package's boolean return is the single biggest day-to-day hazard, so the one thing you should never do is call $ses->sendEmail() from a controller. Put it behind App\Mail\SesRestMailer so you can swap in the official SDK later and so Mail::fake()-style tests remain possible.

interface TransactionalMailer { public function send(EmailMessage $msg): void; }

Custom Laravel mail transport (Laravel 9+). Register via Mail::extend('ses-rest', fn () => $transport) and return a Symfony\Component\Mailer\Transport\TransportInterface. Map the Symfony Email onto SimpleEmailServiceMessage: getFrom(), getTo()/getCc()/getBcc(), getSubject(), getHtmlBody(), getTextBody(), and iterate getAttachments() into addAttachment(). This keeps Mail::to($user)->send(new Receipt($order)) and all your existing Mailables/Blade views working. On Laravel 8 and below (SwiftMailer) the callback must return a fully-built Illuminate\Mail\Mailer with a Swift_Transport, which is more work — for old versions, skip the transport and use a job.

Render Mailables manually when you skip the transport. $mailable->render() gives HTML; use $mailable->assertHasSubject-friendly data and view() for the text alternative. Then feed the wrapper class from a queued job.

Queue + throttle, because the package does none. SES enforces a per-second send rate and a 24-hour quota. Parallel workers will blow straight through it.

public function middleware():.array {
    return [(new RateLimited('ses'))->dontReport()];
}
public $tries = 3;
public $backoff = [5, 30, 120];

Configure the ses limiter just under your actual SES rate. Retry connection errors and 5xx/Throttling; do not retry MessageRejected, InvalidParameterValue, or ConfigurationSetDoesNotExist — those will fail identically three times.

Configuration sets + custom headers for observability. Bounce/complaint handling is not optional with SES. Attach a config set to every message so events land in SNS/EventBridge:

$m->addCustomHeader('X-SES-CONFIGURATION-SET', 'transactional');

Same mechanism for List-Unsubscribe / List-Unsubscribe-Post on bulk mail — required by Gmail/Yahoo since 2024 and there is no first-class API for it here.

Build the message object per send, never reuse it. SimpleEmailServiceMessage accumulates recipients; a singleton message across a loop sends to everyone in one To: header. Also call clearErrors() before each send — the error array is append-only on the service instance.

Attachments: addAttachment($absolutePath, $displayName) for files on disk; addStringAttachment($bytes, $name) for generated PDFs. Read from Storage::path() in a job, not from a request-scoped temp file.

Quota monitoring as a scheduled command. getSendQuota() and getSendStatistics() are cheap and let you alert before you hit the ceiling — worth a daily Schedule::command() once you are past a few thousand a month.

Gotchas and Tips

  • Silent failures. sendEmail() returns false and stashes XML errors in getErrors(); it does not throw. A queued job that just calls it will "succeed" while the user gets nothing. Always convert a falsy return into an exception and log getResponseCode() + getErrors() together.
  • Call setRegion() even if you override the endpoint. Region is what flips the client to SigV4. Leave it at the us-east-1 default and sends to eu-west-1 identities fail with SignatureDoesNotMatch / InvalidSignatureException — an error that looks like bad credentials.
  • No credential chain. This package only accepts a static key/secret. On ECS, Lambda, or EC2 instance roles you must fetch STS/IMDS temporary credentials yourself and refresh them before expiry. If you are on AWS infrastructure, that alone usually justifies aws/aws-sdk-php instead.
  • Sandbox is the #1 first-day support ticket. MessageRejected: Email address is not verified means you are still sandboxed (200/day, 1/sec, verified recipients only) or your From domain isn't verified. Request production access early.
  • Header injection. Strip \r and \n from user-supplied values before they reach setSubject, setFrom, addTo, or addCustomHeader. MIME requires CRLF and the package will happily build a malformed message that SES rejects with a vague InvalidParameterValue.
  • Size limit. Keep the raw message under 10 MB including base64 attachments (~33% inflation), and remember 10 MB of PDF is only ~7.5 MB of file. Send links, not blobs.
  • Always set both a text and HTML body. HTML-only mail hurts deliverability and this package makes it easy to forget setMessageText().
  • Set Return-Path to a verified domain. Otherwise bounces route to your From: address and poison your sender reputation. Pair it with a config set that publishes bounces and complaints to SNS, and actually process the SNS topic.
  • Latency. One cURL round trip per message, no connection reuse — expect 100–300 ms per send. Never loop synchronously in a web request; one job per message or chunk your job and re-dispatch.
  • Don't send bulk in one message via To/Cc. Every recipient sees the others. Use per-recipient addBcc() at minimum, and keep the per-message recipient count low to avoid throttling bursts.
  • Timeouts matter in workers. Defaults are generous (30 s). Set setConnectTimeout(5)/setTimeout(15) so a hung SES call doesn't pin a queue worker; only disable setVerifyPeer() on local/dev boxes.
  • Mutable singleton state. Under Octane or long-running workers the singleton is fine only if you create a fresh SimpleEmailServiceMessage each send and clear errors. If you route across regions for data residency, build a separate client instance per region rather than mutating one.
  • Package age. Last tagged release is 2021. Method signatures (particularly the display-name argument on setFrom) vary slightly across 1.x tags — read the actual class in vendor/ rather than trusting docs. Pin the version in composer.json.
  • Debugging extension point. getRequest() and getResponse() expose the raw query params and XML. Log them at debug level when a send fails; the error objects alone often omit the offending parameter. Subclassing SimpleEmailService to override sendEmail() with logging and one retry is a legitimate, low-risk extension.
  • Have an exit plan. Keep the package behind your TransactionalMailer interface. When you outgrow it — IAM roles, SES v2 features, per-message event publishing, better error granularity — Symfony Mailer's SES transport (or the SDK's SesV2Client) drops in without touching a single call site.
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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings