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

System Mail Bundle Laravel Package

hautzi/system-mail-bundle

Symfony bundle for sending system-generated emails with configurable templates and mail transport integration. Use a simple service API to dispatch transactional notifications, confirmations, and other automated messages from your application.

View on GitHub
Deep Wiki
Context7

Getting Started

The first thing to internalise is that this is a Symfony bundle, not a Laravel package — there is no service provider, no facade, no config/system-mail.php. A Laravel developer's job here is usually one of two things: (a) you're being asked to work in a Symfony codebase that uses it, or (b) you're evaluating the pattern to port into a Laravel app. Both paths start in the same place.

Install and register (Symfony 3.x/4.x era):

composer require hautzi/system-mail-bundle
// app/AppKernel.php (SF3) or config/bundles.php (SF4)
$bundles[] = new Hautzi\SystemMailBundle\HautziSystemMailBundle();

Check the bundle's composer.json constraints before you run anything — the last tagged release is 2018-12-12, so expect PHP 7.x and Symfony 3/4 pins. On a modern Symfony 5/6/7 app composer require will fail resolution outright; that's the signal to stop and vendor the pattern instead of the package.

Where to look first, in order:

  1. README.md — the only real documentation; read the usage snippet line by line, it's short.
  2. DependencyInjection/ — the service ID you'll inject (something like hautzi.system_mail) and the config tree keys.
  3. Resources/config/ — the service definitions; this tells you what the mailer actually depends on (SwiftMailer mailer, translator, templating).
  4. Wherever the README says translation files live — the bundle's whole value proposition is that subject/body come from the translator, so the expected catalogue path and domain are the contract.

Your first use case is almost always a transactional mail you already have in a Laravel app: password reset, welcome, order confirmation. Pick one, define a translation key pair for it, and send it to yourself. The mental model is:

a mail type (a string key) + a recipient + parameters + a locale → a fully rendered, localised email.

If that framing sounds familiar, that's because it's Laravel's Mail::to($user)->locale($user->locale)->send(new WelcomeMail($user)) with the blade view replaced by translation strings. Keep that mapping in your head throughout; it makes the Symfony API legible.

Implementation Patterns

1. Mail types as translation keys, not classes. The bundle's design pushes you away from one Mailable per email and toward a flat registry of named mails. In Symfony you end up with something like:

// translations/system_mail.en.yml
welcome:
    subject: "Welcome, %name%"
    body: "Hi %name%, your account is ready. Confirm at %url%"
password_reset:
    subject: "Reset your password"
    body: "Use this link within 24h: %url%"
$systemMail->send('welcome', $user->getEmail(), [
    '%name%' => $user->getFirstName(),
    '%url%'  => $this->generateUrl('confirm', [], UrlGeneratorInterface::ABSOLUTE_URL),
], $user->getLocale());

This is a good pattern for non-developer-editable mail: copy changes go to a translator or PM editing YAML, not to a developer editing Twig. That's the trade-off versus Laravel Mailable classes — you lose per-mail logic and gain editorial control and uniform localisation.

2. Locale resolution is the interesting part. Decide deliberately between $request->getLocale() (sender's locale — almost always wrong for system mail) and the recipient's stored locale column. For a Laravel port this is exactly Mailable::locale() or Notification::locale(); in Symfony you pass it as an argument. Make the recipient locale the default in your own wrapper service so callers can't get it wrong:

final class AppMailer
{
    public function __construct(private SystemMail $inner) {}

    public function toUser(string $type, User $user, array $params = []): void
    {
        $this->inner->send($type, $user->getEmail(), $params, $user->getLocale());
    }
}

This thin wrapper is also where you'll later swap SwiftMailer for Symfony Mailer, add logging, or add a "do not send in dev to real customers" guard — all without touching call sites.

3. Keep HTML and plaintext as separate keys. The bundle renders whatever the catalogue gives it; if the body is HTML, the parameters are interpolated into HTML. Always provide a text variant and prefer %name%-style placeholders over raw concatenation so you have one place to escape.

4. Queueing via transport config, not the bundle. Because the bundle ultimately hands a message to the Symfony mailer, swiftmailer: { spool: { type: memory } } + a kernel.terminate flush, or a Messenger transport, gives you async delivery for free. Don't try to build queueing into the mail-type layer.

5. Testing. Point swiftmailer at MailHog/Mailcatcher in config_test.yml and assert on the profiler's mailer panel, or grab the spool directly in a kernel test. For the Laravel equivalent you get Mail::fake() + Mail::assertSent(WelcomeMail::class, fn ($m) => $m->locale === 'de') — strictly better ergonomics, and worth noting when you argue for porting rather than adopting.

6. Integration tips for a hybrid team. If Symfony and Laravel services coexist, don't share YAML/JSON catalogues by symlinking; export system_mail.*.yml as flat JSON and load it in Laravel with a custom FileLoader registered on the translator. Shared placeholder conventions (%name% vs :name) are the usual source of silent breakage — normalise at the boundary.

Gotchas and Tips

  • It is effectively unmaintained. Last release Dec 2018, 18 stars, 0 dependents on Packagist, MIT, essentially a single-author bundle. There is no community to catch a Symfony 5+ incompatibility for you. Treat it as reference source, not a dependency.
  • SwiftMailer is EOL. SwiftMailer reached end-of-life in Nov 2021 and was removed in Symfony 6. Any bundle built on it in 2018 has no forward path without a rewrite. This alone usually decides the build-vs-adopt question.
  • Placeholder interpolation into HTML is an injection surface. If any parameter is user-controlled (a display name, an order note), an unescaped %name% in an HTML body is stored XSS in an email — and worse, header injection if the bundle also interpolates the subject. Escape parameters yourself before passing them in; don't rely on the bundle.
  • Translation cache is the #1 debugging trap. Edit the YAML, see no change, assume the bundle is broken. It's the translator cache: bin/console cache:clear (and check translations/ is actually where the bundle's loader looks — a wrong directory fails silently, rendering the raw key or an empty body).
  • Missing keys often fail soft. A typo in the mail type or a missing locale produces a blank or key-as-body email rather than an exception. Add a startup/test assertion that every registered mail type exists in the fallback locale.
  • Fallback locale ordering. framework.default_locale + translator.fallbacks interact with the recipient locale in ways the README doesn't spell out; test a de_AT recipient explicitly rather than assuming de fallback.
  • Extension points are thin but real. You can decorate the mail service, override Resources/views/ templates via app/Resources/HautziSystemMailBundle/ (SF3) or templates/bundles/... (SF4+), and inject a custom translator. There's no event system for "before send", so add your own via the wrapper service above.
  • For a Laravel implementation, port the pattern in an afternoon. One SystemMail value object, a system_mail.php/JSON catalogue per locale, a SystemMailer service that composes a Mailable with ->locale($locale) and a generic Blade view doing __('system_mail.'.$type.'.body', $params). You get the same editorial workflow with maintained, first-party components — and Mail::fake() in tests. That's the recommendation unless you're specifically maintaining an existing Symfony 3/4 app.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource