# Multi Mail Accounts Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Replace the single Azure-account Email tab with a full multi-account mail management system supporting Microsoft 365 (Azure AD) and SMTP accounts. **Architecture:** New `mail_accounts` DB table with encrypted JSON config; `MailAccount` model with `buildTransport()` factory; `MailAccountController` for CRUD + test + toggle; `AppServiceProvider` registers all accounts as named Laravel mailers; Email tab in integrations view rebuilt as account list + AJAX modal. **Tech Stack:** Laravel 12, Alpine.js v3, `PromoSeven\AzureMailer` (local package), `Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport`, `Setting` model (existing). --- ## File Map | File | Change | |------|--------| | `database/migrations/2026_05_26_000001_create_mail_accounts_table.php` | Create | | `app/Models/MailAccount.php` | Create | | `app/Http/Controllers/MailAccountController.php` | Create | | `app/Providers/AppServiceProvider.php` | Add dynamic mailer registration | | `routes/web.php` | Add 7 new routes; remove 3 old azure routes | | `app/Http/Controllers/SettingsController.php` | Remove azure methods + `$azureSettings` | | `resources/views/settings/integrations.blade.php` | Replace Email tab with account list + modal | --- ### Task 1: Migration and MailAccount model **Files:** - Create: `database/migrations/2026_05_26_000001_create_mail_accounts_table.php` - Create: `app/Models/MailAccount.php` - [ ] **Step 1: Create the migration** ```php id(); $table->string('name', 100)->unique(); $table->string('label', 150); $table->enum('type', ['azure', 'smtp']); $table->string('from_address', 255); $table->string('from_name', 150)->nullable(); $table->text('config'); $table->boolean('enabled')->default(true); $table->timestamps(); }); } public function down(): void { Schema::dropIfExists('mail_accounts'); } }; ``` - [ ] **Step 2: Run the migration** ```bash php artisan migrate ``` Expected: `mail_accounts` table created with no errors. - [ ] **Step 3: Create `app/Models/MailAccount.php`** ```php 'encrypted:array', 'enabled' => 'boolean', ]; public function buildTransport(): TransportInterface { if ($this->type === 'azure') { $config = array_merge($this->config, [ 'from_address' => $this->from_address, 'graph_api_version' => 'v1.0', 'timeout' => 30, 'save_to_sent_items' => false, ]); return new AzureTransport( new GraphClient(new TokenManager($config), $config), $config ); } $cfg = $this->config; $tls = ($cfg['encryption'] ?? 'tls') === 'ssl'; $transport = new EsmtpTransport($cfg['host'], (int) ($cfg['port'] ?? 587), $tls); if (!empty($cfg['username'])) { $transport->setUsername($cfg['username']); $transport->setPassword($cfg['password'] ?? ''); } return $transport; } } ``` - [ ] **Step 4: Verify model is autoloaded** ```bash php artisan tinker --execute="echo App\Models\MailAccount::count();" ``` Expected: prints `0` with no errors. - [ ] **Step 5: Commit** ```bash git add database/migrations/2026_05_26_000001_create_mail_accounts_table.php app/Models/MailAccount.php git commit -m "feat: add mail_accounts migration and MailAccount model" ``` --- ### Task 2: MailAccountController and routes **Files:** - Create: `app/Http/Controllers/MailAccountController.php` - Modify: `routes/web.php` - [ ] **Step 1: Create `app/Http/Controllers/MailAccountController.php`** ```php get()->map(fn($a) => $this->accountData($a)); return response()->json(['accounts' => $accounts]); } public function show(MailAccount $mailAccount): JsonResponse { return response()->json([ 'account' => array_merge($this->accountData($mailAccount), ['config' => $mailAccount->config]), ]); } public function store(Request $request): JsonResponse { $data = $this->validated($request); $account = MailAccount::create($data); return response()->json(['success' => true, 'account' => $this->accountData($account)], 201); } public function update(Request $request, MailAccount $mailAccount): JsonResponse { $data = $this->validated($request, $mailAccount->id); $mailAccount->update($data); return response()->json(['success' => true, 'account' => $this->accountData($mailAccount->fresh())]); } public function destroy(MailAccount $mailAccount): JsonResponse { $mailAccount->delete(); return response()->json(['success' => true]); } public function testConnection(MailAccount $mailAccount): JsonResponse { try { if ($mailAccount->type === 'azure') { $config = array_merge($mailAccount->config, ['from_address' => $mailAccount->from_address]); (new TokenManager($config))->getToken(); } else { $cfg = $mailAccount->config; $host = $cfg['host'] ?? ''; $port = (int) ($cfg['port'] ?? 587); $socket = @fsockopen($host, $port, $errno, $errstr, 5); if (! $socket) { throw new \RuntimeException("Cannot connect to {$host}:{$port} — {$errstr}"); } fclose($socket); } return response()->json(['success' => true]); } catch (\Exception $e) { return response()->json(['success' => false, 'message' => $e->getMessage()]); } } public function toggleEnabled(MailAccount $mailAccount): JsonResponse { $mailAccount->update(['enabled' => ! $mailAccount->enabled]); return response()->json(['success' => true, 'enabled' => $mailAccount->fresh()->enabled]); } private function validated(Request $request, ?int $ignoreId = null): array { $nameUnique = 'unique:mail_accounts,name' . ($ignoreId ? ",{$ignoreId}" : ''); $rules = [ 'name' => ['required', 'string', 'max:100', 'regex:/^[a-z0-9\-]+$/', $nameUnique], 'label' => ['required', 'string', 'max:150'], 'type' => ['required', 'in:azure,smtp'], 'from_address' => ['required', 'email', 'max:255'], 'from_name' => ['nullable', 'string', 'max:150'], 'enabled' => ['boolean'], ]; if ($request->input('type') === 'azure') { $rules['config.tenant_id'] = ['required', 'string', 'max:100']; $rules['config.client_id'] = ['required', 'string', 'max:100']; $rules['config.client_secret'] = ['required', 'string', 'max:500']; } else { $rules['config.host'] = ['required', 'string', 'max:255']; $rules['config.port'] = ['required', 'integer', 'min:1', 'max:65535']; $rules['config.encryption'] = ['required', 'in:tls,ssl,none']; $rules['config.username'] = ['nullable', 'string', 'max:255']; $rules['config.password'] = ['nullable', 'string', 'max:500']; } $v = $request->validate($rules); return [ 'name' => $v['name'], 'label' => $v['label'], 'type' => $v['type'], 'from_address' => $v['from_address'], 'from_name' => $v['from_name'] ?? null, 'config' => $v['config'], 'enabled' => $v['enabled'] ?? true, ]; } private function accountData(MailAccount $account): array { return [ 'id' => $account->id, 'name' => $account->name, 'label' => $account->label, 'type' => $account->type, 'from_address' => $account->from_address, 'from_name' => $account->from_name, 'enabled' => $account->enabled, ]; } } ``` - [ ] **Step 2: Add the 7 new routes and remove the 3 old azure routes in `routes/web.php`** Find the block: ```php Route::post('settings/integrations/azure-mail', [SettingsController::class, 'updateAzureMail'])->name('settings.integrations.azure-mail'); Route::post('settings/integrations/test-azure-mail', [SettingsController::class, 'testAzureMailConnection'])->name('settings.integrations.test-azure-mail'); Route::post('settings/integrations/send-test-email', [SettingsController::class, 'sendTestEmail'])->name('settings.integrations.send-test-email'); ``` Replace it with: ```php Route::get('settings/integrations/mail-accounts', [MailAccountController::class, 'index'])->name('settings.mail-accounts.index'); Route::post('settings/integrations/mail-accounts', [MailAccountController::class, 'store'])->name('settings.mail-accounts.store'); Route::get('settings/integrations/mail-accounts/{mailAccount}', [MailAccountController::class, 'show'])->name('settings.mail-accounts.show'); Route::put('settings/integrations/mail-accounts/{mailAccount}', [MailAccountController::class, 'update'])->name('settings.mail-accounts.update'); Route::delete('settings/integrations/mail-accounts/{mailAccount}', [MailAccountController::class, 'destroy'])->name('settings.mail-accounts.destroy'); Route::post('settings/integrations/mail-accounts/{mailAccount}/test', [MailAccountController::class, 'testConnection'])->name('settings.mail-accounts.test'); Route::patch('settings/integrations/mail-accounts/{mailAccount}/toggle', [MailAccountController::class, 'toggleEnabled'])->name('settings.mail-accounts.toggle'); ``` Also add the `MailAccountController` use statement at the top of `routes/web.php` alongside the existing controller use statements: ```php use App\Http\Controllers\MailAccountController; ``` - [ ] **Step 3: Verify routes** ```bash php artisan route:list --name=settings.mail-accounts ``` Expected: 7 rows — index, store, show, update, destroy, test, toggle. - [ ] **Step 4: Commit** ```bash git add app/Http/Controllers/MailAccountController.php routes/web.php git commit -m "feat: add MailAccountController with CRUD + test + toggle routes" ``` --- ### Task 3: AppServiceProvider + SettingsController cleanup **Files:** - Modify: `app/Providers/AppServiceProvider.php` - Modify: `app/Http/Controllers/SettingsController.php` - [ ] **Step 1: Update `app/Providers/AppServiceProvider.php`** Replace the entire file content with: ```php Setting::get('ultramsg_instance_id', config('ultra-message.instance_id')), 'token' => Setting::get('ultramsg_token', config('ultra-message.token')), 'webhook_secret' => Setting::get('ultramsg_webhook_secret', config('ultra-message.webhook_secret')), 'webhook_path' => Setting::get('ultramsg_webhook_path', config('ultra-message.webhook_path', 'ultra-message/webhook')), 'timeout' => config('ultra-message.timeout', 30), 'enabled' => (bool) Setting::get('ultramsg_enabled', config('ultra-message.enabled', true)), ]; }); $this->callAfterResolving(MailManager::class, function (MailManager $manager) { try { foreach (MailAccount::all() as $account) { $manager->extend($account->name, fn () => $account->buildTransport()); } } catch (\Exception) { // DB not ready on fresh install — skip silently } }); } } ``` - [ ] **Step 2: Clean up `app/Http/Controllers/SettingsController.php`** Remove the `use Illuminate\Support\Facades\Mail;` and `use PromoSeven\AzureMailer\Graph\TokenManager;` use statements. Change `integrations()` to only pass `$whatsappSettings` (remove `$azureSettings`): ```php public function integrations(): View { $whatsappSettings = [ 'enabled' => Setting::get('ultramsg_enabled', false), 'instance_id' => Setting::get('ultramsg_instance_id', ''), 'token' => Setting::get('ultramsg_token', ''), 'webhook_secret' => Setting::get('ultramsg_webhook_secret', ''), 'webhook_path' => Setting::get('ultramsg_webhook_path', 'ultra-message/webhook'), ]; return view('settings.integrations', compact('whatsappSettings')); } ``` Delete the three azure methods entirely: `updateAzureMail()`, `testAzureMailConnection()`, `sendTestEmail()`. - [ ] **Step 3: Verify no parse errors** ```bash php artisan route:list --name=settings.integrations 2>&1 | tail -3 ``` Expected: shows 4 rows (GET + whatsapp POST + test-whatsapp POST + send-test-message POST), no errors. - [ ] **Step 4: Commit** ```bash git add app/Providers/AppServiceProvider.php app/Http/Controllers/SettingsController.php git commit -m "feat: register dynamic mailers in AppServiceProvider, remove single-azure methods from SettingsController" ``` --- ### Task 4: Rewrite Email tab in integrations.blade.php **Files:** - Modify: `resources/views/settings/integrations.blade.php` Replace the entire `{{-- ===== Email tab ===== --}}` section (and the old Email modal/accordion) with the following. - [ ] **Step 1: Replace the Email tab panel and add the modal** The complete replacement for everything from `{{-- ===== Email tab ===== --}}` to `{{-- end Email tab --}}` (inclusive), plus the new modal added just before the closing `` of the outer wrapper, plus the new JS block replacing the old azure JS functions: **Email tab panel** (replaces old email tab div): ```blade {{-- ===== Email tab ===== --}}
{{-- end Email tab --}} ``` **Modal** (add just before the closing `` of the outer Alpine wrapper, after the email tab div): ```blade {{-- Mail Accounts Modal --}} ``` - [ ] **Step 2: Update the Email tab button to also load accounts** In the pill tab row, change the Email tab button's `@click` from: ```blade