vanio/easy-mailer
vanio/easy-mailer is a lightweight Laravel/PHP package that streamlines sending email through a simple, easy-to-use API, helping you configure mailers and dispatch messages with minimal boilerplate.
Verify the target framework before anything else. Vanio's packages are published as Symfony bundles/DI services, and this one is no exception in spirit — it wraps a mailer abstraction with a simpler send API. Open composer.json first and look at require: if you see symfony/mailer, symfony/dependency-injection, or twig/twig, you are not installing a Laravel package, you are installing a Symfony component wrapper. That single fact drives everything below.
Minimal steps:
composer require vanio/easy-mailer and immediately inspect vendor/vanio/easy-mailer/composer.json plus the src/ tree.services.xml / services.yaml) — it tells you the constructor arguments you'll need to build by hand in Laravel, and it's the closest thing to documentation this package has.AppServiceProvider and send one plain-text email through your configured transport before touching templates, attachments, or HTML.First use case: a single transactional email — password reset or order confirmation — rendered from a template, with one attachment, sent through SMTP. Do not start with bulk sending or queueing; you need to know whether the transport layer even resolves cleanly inside Laravel's container first.
// AppServiceProvider::register() — names illustrative, confirm against src/
$this->app->singleton(EasyMailer::class, function ($app) {
return new EasyMailer(
Transport::fromDsn(config('mail.dsn')), // build from your MAIL_* env
$app->make(RendererInterface::class),
);
});
If the package's mailer is a thin, injectable service with a transport and a renderer as constructor arguments, this works and you are done. If it reaches into a global container or expects Symfony's EventDispatcherInterface, stop and reassess — see Gotchas.
Wrap it, don't sprinkle it. The package's API surface is small and unversioned. Put it behind your own App\Mail\Mailer interface with a method per email type (sendPasswordReset, sendOrderConfirmation). When the package goes unmaintained — and with 0 dependents, assume it might — you swap one adapter class instead of grepping 40 call sites.
Treat message construction as a builder, sending as a separate step. The typical shape is a fluent builder returning a message object that a mailer service then sends. Keep those two phases in different methods on your adapter so you can snapshot/assert the message in tests without sending:
$message = $this->mailer->createMessage()
->to($user->email, $user->name)
->subject(__('mail.reset.subject'))
->html($this->blade->render('mail.reset', ['user' => $user]))
->attach($pdfPath, 'invoice.pdf');
$this->mailer->send($message);
Render with Blade, not Twig. If the package's renderer expects Twig, don't pull twig/twig + a Laravel Twig bridge into your stack just for email templates. Render with view()->render() and hand the package finished HTML. You lose the package's template-context convenience and keep your team's Blade knowledge, your existing layouts, and your @lang directives. This is the single highest-leverage decision in the whole integration.
Map your existing env to a DSN. Laravel developers have MAIL_MAILER, MAIL_HOST, MAIL_PORT, MAIL_USERNAME, MAIL_PASSWORD. Symfony's transport layer wants a DSN string. Build it once in a config file so local/staging/production differ by env only:
// config/mail.php
'easy_mailer_dsn' => env('MAIL_DSN', sprintf(
'smtp://%s:%s@%s:%s',
urlencode(env('MAIL_USERNAME')), urlencode(env('MAIL_PASSWORD')),
env('MAIL_HOST'), env('MAIL_PORT', 587),
)),
Note the urlencode — passwords with @, :, or / silently break DSN parsing otherwise.
Queue at the Laravel layer. The package will almost certainly not know about Laravel's queue. Make your adapter method synchronous, then wrap calls in a ShouldQueue job:
SendOrderConfirmation::dispatch($order)->onQueue('mail');
This gives you retries, backoff, failed_jobs, and Horizon visibility — all things you'd otherwise reimplement.
Test against a capturing transport, not the network. Point the DSN at a null/log transport in phpunit.xml and assert on the message object your adapter builds. Laravel's Mail::fake() will not see these emails — it only intercepts Illuminate\Mail\Mailable. Write a small FakeEasyMailer implementing your own adapter interface and assert against that; it's more valuable than spying on the package internals.
Multi-tenant / per-brand sending. If you send on behalf of tenants, resolve the transport per-tenant (from-address, DKIM credentials) at construction time in a scoped binding rather than mutating a singleton. Singletons that hold credentials are a classic source of "email sent as the wrong tenant" bugs.
php artisan vendor:publish to do anything. Everything is manual binding. Budget a half-day for this, not ten minutes.Mail component and Symfony's Mailer transport pool. They don't share config, connection pooling, or log context. Logs from one won't show up where you expect. Consider standardizing on one; running both long-term is how teams end up with "why did the invoice email go out twice."MessageBusInterface, pass a no-op implementation that sends immediately, and do your async in Laravel jobs as above.Symfony\Component\Mailer\Exception\TransportExceptionInterface; Laravel's mail throws its own. Catch the Symfony interface explicitly in your adapter and normalize to a domain exception, or your failed_jobs handler will miss real SMTP failures.smtp:// on 587 with STARTTLS, smtps:// on 465 with implicit TLS. Laravel's MAIL_ENCRYPTION=tls does not translate to smtps. Getting this wrong produces a 30-second hang, not an error — very easy to misdiagnose as a firewall issue.Log::channel('mail') works fine. Set it at debug level temporarily and you'll see the full SMTP conversation. Without it, failures look like generic "message not sent."TransportFactory-style resolver handles multi-tenant. Everything extensible lives in your App\Mail namespace.composer.json, and write an integration test that actually sends through a real SMTP sink (MailHog/Mailpit) in CI so a broken upgrade fails loudly rather than silently dropping mail.Mail + Mailable + Mail::fake() covers 95% of this with first-party support. Adopt this package only for a concrete win, and only behind your own adapter so reversing the decision costs an afternoon.How can I help you explore Laravel packages today?