lacodix/laravel-scoped-mail-config
Send emails with dynamic, per-scope mailer settings in Laravel. Provide SMTP/from config via any model or class implementing HasMailConfig—ideal for multi-tenancy (e.g., Spatie) or user/team-specific mail configurations.
Install the Package
composer require lacodix/laravel-scoped-mail-config
Publish the config (if needed):
php artisan vendor:publish --provider="Lacodix\LaravelScopedMailConfig\ScopedMailConfigServiceProvider"
Implement HasMailConfig Interface
Add the interface to your model (e.g., Tenant, User, or Team):
use Lacodix\LaravelScopedMailConfig\Concerns\HasMailConfig;
class Tenant extends Model implements HasMailConfig
{
public function getMailConfig(string $name): array
{
return [
'transport' => 'smtp',
'host' => 'smtp.example.com',
'port' => 587,
'encryption' => 'tls',
'username' => 'user@example.com',
'password' => 'password',
'from' => ['address' => 'no-reply@example.com', 'name' => 'App Name'],
];
}
}
Resolve the Scope in AppServiceProvider
Configure how the package resolves the scope (e.g., current tenant or user):
use Lacodix\LaravelScopedMailConfig\Facades\ScopedMail;
public function boot(): void
{
ScopedMail::resolveScopeUsing(fn () => Tenant::getCurrentTenant());
}
Send Scoped Emails
Use the ScopedMail facade like Laravel’s native Mail facade:
ScopedMail::to('user@example.com')->send(new OrderShipped($order));
HasMailConfig to the Tenant model.Tenant::getCurrentTenant().ScopedMail, and the package will dynamically apply the tenant’s SMTP settings.Dynamic SMTP per Tenant
HasMailConfig on the Tenant model to define SMTP settings.AppServiceProvider to fetch the current tenant.ScopedMail, and the package will override Laravel’s default mail config.User-Specific Email Signatures
HasMailConfig on the User model to define from address and name.ScopedMail::resolveScopeUsing(fn () => Auth::user());
ScopedMail will use the user’s signature.Team-Based Mail Configs
HasMailConfig to a Team model to define shared SMTP settings for team members.Middleware for Scope Resolution Dynamically resolve the scope based on request context (e.g., subdomain or API token):
public function handle(Request $request, Closure $next)
{
$tenant = Tenant::findBySubdomain($request->getHost());
ScopedMail::resolveScopeUsing(fn () => $tenant);
return $next($request);
}
Fallback to Global Config
If no scope is resolved, the package falls back to Laravel’s default config/mail.php. Test this behavior:
// Test with no scope resolved
ScopedMail::fake();
ScopedMail::to('user@example.com')->send(new TestMail());
Testing Scoped Emails
Use the fake() method to mock scoped emails in tests:
public function test_scoped_email()
{
ScopedMail::fake();
ScopedMail::to('user@example.com')->send(new TestMail());
ScopedMail::assertSent(TestMail::class, function ($mail) {
return $mail->hasTo('user@example.com');
});
}
Queue Workers
Scoped mail configs work seamlessly with Laravel queues. Ensure the scope is resolved before the job runs (e.g., in the job’s handle method or via middleware).
Scope Resolution Timing
handle method:
public function handle()
{
$tenant = Tenant::find($this->tenantId);
ScopedMail::resolveScopeUsing(fn () => $tenant);
Mail::to($this->email)->send(new TestMail());
}
Config Caching
php artisan config:cache, scoped mail configs may not update dynamically. The package reads configs at runtime, but cached configs might persist stale values.Missing getMailConfig Method
getMailConfig on the scoped model will cause runtime errors.HasMailConfig include the method. Use IDE hints or static analysis tools to catch missing implementations.Laravel 12+ Compatibility
Inspect Active Scope Use the facade to debug the current scope:
$scope = ScopedMail::getScope();
dd($scope); // Outputs the resolved model (e.g., Tenant instance)
Check Mail Config Overrides Log the resolved mail config to verify overrides:
$config = ScopedMail::getMailConfig();
\Log::info('Scoped Mail Config:', $config);
Test with fake()
Use ScopedMail::fake() to isolate scoped email logic in tests and verify configs:
ScopedMail::fake();
ScopedMail::to('user@example.com')->send(new TestMail());
$mail = ScopedMail::assertSent(TestMail::class);
dd($mail->mailables[0]->config); // Inspect the applied config
Custom Scope Resolvers
Extend the package by creating a custom resolver. Override the resolveScopeUsing method in a service provider:
ScopedMail::resolveScopeUsing(function () {
return Tenant::where('id', request()->tenant_id)->first();
});
Dynamic Config Merging
Merge scoped configs with global configs in getMailConfig:
public function getMailConfig(string $name): array
{
$globalConfig = config('mail.default');
$scopedConfig = [
'from' => ['address' => 'tenant@example.com'],
];
return array_merge($globalConfig, $scopedConfig);
}
Event-Based Scoping
Trigger scope resolution via events (e.g., TenantSwitched):
event(new TenantSwitched($tenant));
// In a listener:
ScopedMail::resolveScopeUsing(fn () => $event->tenant);
API for External Configs Fetch scoped configs via an API endpoint for admin dashboards:
Route::get('/mail-config', function () {
$tenant = Tenant::getCurrentTenant();
return response()->json($tenant->getMailConfig('default'));
});
Use config/mail.php as a Fallback
Define default mail settings in config/mail.php and override them per scope. This ensures emails are always deliverable, even if a scope fails to resolve.
Environment-Specific Scopes Combine scoped configs with environment variables for flexibility:
public function getMailConfig(string $name): array
{
return [
'host' => env('SMTP_HOST', 'default.smtp.com'),
'port' => env('SMTP_PORT', 587),
];
}
Validate SMTP Credentials
Add validation to getMailConfig to catch misconfigurations early:
public function getMailConfig(string $name): array
{
$config = [
'host' => 'smtp.example.com',
'username' => 'user@example.com',
'password
How can I help you explore Laravel packages today?