a2zwebltd/laravel-email-sequences
Laravel Email Sequences helps you build and automate drip email campaigns directly in Laravel. Define multi-step sequences, schedule sends, manage subscribers, and track progress with a simple framework-native API—ideal for onboarding, follow-ups, and lifecycle messaging.
This release (v1.4.0) makes sequence emails locale-aware and lets you control Reply-To at two levels. Start here:
config/sequences.php. You'll find the shape you need for both new features:// config/sequences.php
return [
'reply_to' => env('EMAIL_SEQUENCES_REPLY_TO'), // global fallback
'sequences' => [
'onboarding' => [
'reply_to' => '[email protected]', // per-sequence override
'steps' => [ /* ... */ ],
],
],
];
Add EMAIL_SEQUENCES_REPLY_TO to .env — one line sets the default Reply-To for every sequence. Per-sequence reply_to wins over it.
Make your recipients locale-aware — implement Laravel's HasLocalePreference contract on the notifiable model (usually User):
use Illuminate\Contracts\Translation\HasLocalePreference;
class User extends Authenticatable implements HasLocalePreference
{
public function preferredLocale()
{
return $this->locale; // e.g. 'fr', 'de'
}
}
lang/ files, so create keys like lang/fr/sequences.php and reference them from the sequence definition.First use case: a multi-language onboarding drip where French users get French subject lines and replies land in the right support inbox per campaign. Previously you'd hand-roll locale switching and per-mailable replyTo() calls; now it's declarative.
Per-recipient locale resolution (the headline feature). The package reads preferredLocale() from the notifiable at send time. Because it's resolved per recipient, a single queued sequence can fan out to users in different locales without separate sequence definitions:
// lang/fr/sequences.php
return ['onboarding' => ['welcome' => 'Bienvenue !']];
// lang/en/sequences.php
return ['onboarding' => ['welcome' => 'Welcome!']];
Keep the subject in lang files and the body in your Mailable/Blade views — that's the split this release assumes.
Reply-To layering. Think of it as three tiers, most-specific wins:
| Tier | Source |
|---|---|
| Per-sequence | sequences.<code>.reply_to in config/sequences.php |
| Global | EMAIL_SEQUENCES_REPLY_TO env / sequences.reply_to |
| Framework default | Whatever your Mailable sets |
Use the global value for a shared support desk (support@) and override per-sequence where a campaign has a dedicated owner (billing@, events@). This is the pragmatic pattern: don't set replyTo() manually in every Mailable — let config drive it so marketing can change addresses without a deploy.
Multi-tenant / per-brand setups. If you already swap config at runtime, set config(['sequences.sequences.onboarding.reply_to' => $tenant->reply_to]) before dispatching. Same for locale: since resolution is per-recipient, tenant locale on the User model flows through automatically.
Testing pattern. Assert both behaviours in a feature test:
Mail::fake();
$user = User::factory()->create(['locale' => 'fr']);
dispatch sequence step...
Mail::assertSent(WelcomeMail::class, function ($mail) use ($user) {
return $mail->hasTo($user->email)
&& $mail->hasReplyTo('[email protected]')
&& $mail->subject === 'Bienvenue !';
});
Use Mail::assertSent with the locale set explicitly rather than relying on app()->getLocale(), since the new behaviour bypasses the app locale.
Integration tip: if you use a custom Mailable base class, keep the subject assignment in build() or envelope() — don't hardcode ->subject() in the constructor, or you'll shadow the translated subject.
HasLocalePreference must be implemented, not just have a locale column. The package checks the contract. A model with a locale attribute but no interface will silently fall back to app()->getLocale().lang/fr/sequences.php has no key for the step, Laravel returns the raw key string — your email subject literally becomes sequences.onboarding.welcome. Always ship a fallback in lang/en/ and consider Lang::has() assertions in tests.EMAIL_SEQUENCES_REPLY_TO may not behave like "unset" — prefer omitting the env var over setting it blank, and verify with Mail::assertNotSet style assertions.sequences.php, run php artisan config:clear (or config:cache) in local/staging. Per-sequence reply_to won't appear until the cache is rebuilt — a classic "why is it still using the global address?" moment.Lang::addLines() or a DB-backed translation loader (e.g. spatie/laravel-translation-loader) will work — handy for letting admins edit subject copy without deploys.Log::debug(app(LocalePreference::class)...) — or simpler, temporarily set MAIL_MAILER=log and inspect storage/logs/laravel.log; the rendered Subject: and Reply-To: headers tell you immediately which tier and locale won.How can I help you explore Laravel packages today?