diff --git a/docs/superpowers/plans/2026-05-26-azure-mailer.md b/docs/superpowers/plans/2026-05-26-azure-mailer.md
new file mode 100644
index 0000000..5bc9f2c
--- /dev/null
+++ b/docs/superpowers/plans/2026-05-26-azure-mailer.md
@@ -0,0 +1,1353 @@
+# azure-mailer 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:** Build `promoseven/azure-mailer`, a Laravel mail transport driver that sends email via the Microsoft 365 Graph API using Azure AD Client Credentials.
+
+**Architecture:** Three focused classes — `TokenManager` (OAuth2 token + caching), `GraphClient` (HTTP calls to Graph API), `AzureTransport` (Symfony transport adapter) — wired together by `AzureMailerServiceProvider`. Dependencies flow one way: Transport → Client → TokenManager.
+
+**Tech Stack:** PHP 8.2, Laravel 11/12, Symfony Mailer 7, Laravel Http facade (Guzzle), Laravel Cache facade, Orchestra Testbench 10, PHPUnit 11.
+
+---
+
+## File Map
+
+| File | Action | Purpose |
+|------|--------|---------|
+| `C:\Users\IT Department\Desktop\azure-mailer\composer.json` | Create | Package manifest, autoloading, dependencies |
+| `src/Exceptions/AuthenticationException.php` | Create | Thrown when Azure AD token fetch fails |
+| `src/Exceptions/GraphApiException.php` | Create | Thrown when Graph API returns an error |
+| `src/Graph/TokenManager.php` | Create | Fetches + caches OAuth2 access token |
+| `src/Graph/GraphClient.php` | Create | POSTs mail payload to Graph API |
+| `src/Transport/AzureTransport.php` | Create | Symfony AbstractTransport — builds payload from Email object |
+| `src/AzureMailerServiceProvider.php` | Create | Registers `azure` transport with Laravel MailManager |
+| `config/azure-mailer.php` | Create | Publishable defaults (save_to_sent_items, timeout, graph_api_version) |
+| `tests/TestCase.php` | Create | Orchestra Testbench base with array cache driver |
+| `tests/Graph/TokenManagerTest.php` | Create | Token fetch, caching, invalidation, error handling |
+| `tests/Graph/GraphClientTest.php` | Create | Payload send, 401 retry, error handling |
+| `tests/Transport/AzureTransportTest.php` | Create | Payload building (HTML, text, CC/BCC/Reply-To, attachments) |
+| `C:\Users\IT Department\Desktop\OperationModule\composer.json` | Modify | Add path repository + require azure-mailer |
+| `C:\Users\IT Department\Desktop\OperationModule\config\mail.php` | Modify | Add azure mailer entry |
+| `C:\Users\IT Department\Desktop\OperationModule\.env.example` | Modify | Add AZURE_* env vars |
+
+---
+
+## Task 1: Package scaffold
+
+**Files:**
+- Create: `C:\Users\IT Department\Desktop\azure-mailer\composer.json`
+- Create: `C:\Users\IT Department\Desktop\azure-mailer\config\azure-mailer.php`
+
+- [ ] **Step 1: Create the package directory and composer.json**
+
+Create `C:\Users\IT Department\Desktop\azure-mailer\composer.json`:
+
+```json
+{
+ "name": "promoseven/azure-mailer",
+ "description": "Laravel mail transport for Microsoft 365 via Azure AD Graph API",
+ "type": "library",
+ "license": "MIT",
+ "require": {
+ "php": "^8.2",
+ "illuminate/support": "^11.0|^12.0",
+ "illuminate/http": "^11.0|^12.0",
+ "illuminate/mail": "^11.0|^12.0",
+ "symfony/mailer": "^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0",
+ "orchestra/testbench": "^10.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "PromoSeven\\AzureMailer\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "PromoSeven\\AzureMailer\\Tests\\": "tests/"
+ }
+ },
+ "extra": {
+ "laravel": {
+ "providers": [
+ "PromoSeven\\AzureMailer\\AzureMailerServiceProvider"
+ ]
+ }
+ },
+ "scripts": {
+ "test": "vendor/bin/phpunit"
+ },
+ "minimum-stability": "stable",
+ "prefer-stable": true
+}
+```
+
+- [ ] **Step 2: Create directory structure**
+
+Run from `C:\Users\IT Department\Desktop\azure-mailer`:
+
+```powershell
+New-Item -ItemType Directory -Force src/Exceptions, src/Graph, src/Transport, config, tests/Graph, tests/Transport
+```
+
+- [ ] **Step 3: Create the publishable config file**
+
+Create `C:\Users\IT Department\Desktop\azure-mailer\config\azure-mailer.php`:
+
+```php
+ false,
+ 'timeout' => 30,
+ 'graph_api_version' => 'v1.0',
+];
+```
+
+- [ ] **Step 4: Install dependencies**
+
+Run from `C:\Users\IT Department\Desktop\azure-mailer`:
+
+```powershell
+composer install
+```
+
+Expected: `vendor/` created, `vendor/bin/phpunit` exists.
+
+- [ ] **Step 5: Create phpunit.xml**
+
+Create `C:\Users\IT Department\Desktop\azure-mailer\phpunit.xml`:
+
+```xml
+
+
Hello
'], + 'toRecipients' => [['emailAddress' => ['address' => 'to@example.com', 'name' => '']]], + 'ccRecipients' => [], + 'bccRecipients' => [], + 'replyTo' => [], + 'attachments' => [], + ], + 'saveToSentItems' => false, + ]; + + private function makeTokenManager(string $token = 'fake-token'): TokenManager + { + Http::fake([ + 'login.microsoftonline.com/*' => Http::response([ + 'access_token' => $token, + 'expires_in' => 3600, + ], 200), + ]); + + return new TokenManager($this->config); + } + + public function test_sends_payload_with_bearer_token(): void + { + Http::fake([ + 'login.microsoftonline.com/*' => Http::response(['access_token' => 'tok', 'expires_in' => 3600], 200), + 'graph.microsoft.com/*' => Http::response('', 202), + ]); + + $client = new GraphClient(new TokenManager($this->config), $this->config); + $client->send($this->payload); + + Http::assertSent(function ($request) { + return str_contains($request->url(), 'graph.microsoft.com/v1.0/users/sender@example.com/sendMail') + && $request->hasHeader('Authorization', 'Bearer tok'); + }); + } + + public function test_retries_with_fresh_token_on_401(): void + { + Log::shouldReceive('warning') + ->once() + ->with('azure-mailer: 401 received, retrying with fresh token'); + + Http::fake([ + 'login.microsoftonline.com/*' => Http::sequence() + ->push(['access_token' => 'stale-token', 'expires_in' => 3600], 200) + ->push(['access_token' => 'fresh-token', 'expires_in' => 3600], 200), + 'graph.microsoft.com/*' => Http::sequence() + ->push('', 401) + ->push('', 202), + ]); + + $client = new GraphClient(new TokenManager($this->config), $this->config); + $client->send($this->payload); + + Http::assertSentCount(4); // 2 token fetches + 2 graph calls + } + + public function test_throws_graph_api_exception_on_second_401(): void + { + Log::shouldReceive('warning')->once(); + + Http::fake([ + 'login.microsoftonline.com/*' => Http::response(['access_token' => 'tok', 'expires_in' => 3600], 200), + 'graph.microsoft.com/*' => Http::response([ + 'error' => ['code' => 'InvalidAuthenticationToken', 'message' => 'Token is expired.'], + ], 401), + ]); + + $this->expectException(GraphApiException::class); + $this->expectExceptionMessage('InvalidAuthenticationToken'); + + $client = new GraphClient(new TokenManager($this->config), $this->config); + $client->send($this->payload); + } + + public function test_throws_graph_api_exception_on_other_error(): void + { + Http::fake([ + 'login.microsoftonline.com/*' => Http::response(['access_token' => 'tok', 'expires_in' => 3600], 200), + 'graph.microsoft.com/*' => Http::response([ + 'error' => ['code' => 'ErrorInvalidRecipients', 'message' => 'Recipient address is invalid.'], + ], 400), + ]); + + $this->expectException(GraphApiException::class); + $this->expectExceptionMessage('ErrorInvalidRecipients'); + + $client = new GraphClient(new TokenManager($this->config), $this->config); + $client->send($this->payload); + } + + public function test_uses_graph_api_version_from_config(): void + { + Http::fake([ + 'login.microsoftonline.com/*' => Http::response(['access_token' => 'tok', 'expires_in' => 3600], 200), + 'graph.microsoft.com/*' => Http::response('', 202), + ]); + + $config = array_merge($this->config, ['graph_api_version' => 'beta']); + $client = new GraphClient(new TokenManager($config), $config); + $client->send($this->payload); + + Http::assertSent(function ($request) { + return str_contains($request->url(), 'graph.microsoft.com/beta/'); + }); + } +} +``` + +- [ ] **Step 2: Run tests — expect failure** + +```powershell +vendor/bin/phpunit tests/Graph/GraphClientTest.php +``` + +Expected: FAIL with `Class "PromoSeven\AzureMailer\Graph\GraphClient" not found`. + +- [ ] **Step 3: Implement GraphClient** + +Create `C:\Users\IT Department\Desktop\azure-mailer\src\Graph\GraphClient.php`: + +```php +endpoint(); + + $response = Http::withToken($this->tokenManager->getToken()) + ->timeout($this->config['timeout'] ?? 30) + ->post($url, $payload); + + if ($response->status() === 401) { + Log::warning('azure-mailer: 401 received, retrying with fresh token'); + $this->tokenManager->invalidate(); + + $response = Http::withToken($this->tokenManager->getToken()) + ->timeout($this->config['timeout'] ?? 30) + ->post($url, $payload); + } + + if (! $response->successful()) { + $error = $response->json('error', []); + throw GraphApiException::fromResponse( + $error['code'] ?? (string) $response->status(), + $error['message'] ?? 'Unknown error' + ); + } + } + + private function endpoint(): string + { + $version = $this->config['graph_api_version'] ?? 'v1.0'; + $from = $this->config['from_address']; + + return "https://graph.microsoft.com/{$version}/users/{$from}/sendMail"; + } +} +``` + +- [ ] **Step 4: Run tests — expect all pass** + +```powershell +vendor/bin/phpunit tests/Graph/GraphClientTest.php +``` + +Expected: `5 tests` — all PASS. + +- [ ] **Step 5: Commit** + +```powershell +git add src/Graph/GraphClient.php tests/Graph/GraphClientTest.php +git commit -m "feat: add GraphClient with 401 retry and error handling" +``` + +--- + +## Task 5: AzureTransport + +**Files:** +- Create: `src/Transport/AzureTransport.php` +- Create: `tests/Transport/AzureTransportTest.php` + +- [ ] **Step 1: Write failing tests** + +Create `C:\Users\IT Department\Desktop\azure-mailer\tests\Transport\AzureTransportTest.php`: + +```php +createMock(GraphClient::class); + $client->method('send')->willReturnCallback(function (array $payload) use (&$captured) { + $captured[] = $payload; + }); + + return new AzureTransport($client, array_merge(['save_to_sent_items' => false], $config)); + } + + private function sendEmail(AzureTransport $transport, Email $email): array + { + $captured = []; + $client = $this->createMock(GraphClient::class); + $client->method('send')->willReturnCallback(function (array $payload) use (&$captured) { + $captured[] = $payload; + }); + + $transport = new AzureTransport($client, ['save_to_sent_items' => false]); + $envelope = Envelope::create($email); + $transport->send($email, $envelope); + + return $captured[0] ?? []; + } + + public function test_sends_html_body(): void + { + $captured = []; + $client = $this->createMock(GraphClient::class); + $client->expects($this->once()) + ->method('send') + ->willReturnCallback(function (array $payload) use (&$captured) { + $captured = $payload; + }); + + $email = (new Email()) + ->from('from@example.com') + ->to('to@example.com') + ->subject('Hello') + ->html('World
'); + + $transport = new AzureTransport($client, ['save_to_sent_items' => false]); + $transport->send($email, Envelope::create($email)); + + $this->assertSame('Hello', $captured['message']['subject']); + $this->assertSame('HTML', $captured['message']['body']['contentType']); + $this->assertSame('World
', $captured['message']['body']['content']); + $this->assertSame('to@example.com', $captured['message']['toRecipients'][0]['emailAddress']['address']); + $this->assertFalse($captured['saveToSentItems']); + } + + public function test_falls_back_to_text_body_when_no_html(): void + { + $captured = []; + $client = $this->createMock(GraphClient::class); + $client->method('send')->willReturnCallback(function ($p) use (&$captured) { $captured = $p; }); + + $email = (new Email()) + ->from('from@example.com') + ->to('to@example.com') + ->subject('Text only') + ->text('Plain text content'); + + $transport = new AzureTransport($client, ['save_to_sent_items' => false]); + $transport->send($email, Envelope::create($email)); + + $this->assertSame('Text', $captured['message']['body']['contentType']); + $this->assertSame('Plain text content', $captured['message']['body']['content']); + } + + public function test_maps_cc_bcc_and_reply_to(): void + { + $captured = []; + $client = $this->createMock(GraphClient::class); + $client->method('send')->willReturnCallback(function ($p) use (&$captured) { $captured = $p; }); + + $email = (new Email()) + ->from('from@example.com') + ->to('to@example.com') + ->cc('cc@example.com') + ->bcc('bcc@example.com') + ->replyTo('reply@example.com') + ->subject('Recipients test') + ->html('hi
'); + + $transport = new AzureTransport($client, ['save_to_sent_items' => false]); + $transport->send($email, Envelope::create($email)); + + $this->assertSame('cc@example.com', $captured['message']['ccRecipients'][0]['emailAddress']['address']); + $this->assertSame('bcc@example.com', $captured['message']['bccRecipients'][0]['emailAddress']['address']); + $this->assertSame('reply@example.com', $captured['message']['replyTo'][0]['emailAddress']['address']); + } + + public function test_encodes_attachments_as_base64(): void + { + $captured = []; + $client = $this->createMock(GraphClient::class); + $client->method('send')->willReturnCallback(function ($p) use (&$captured) { $captured = $p; }); + + $email = (new Email()) + ->from('from@example.com') + ->to('to@example.com') + ->subject('With attachment') + ->html('see attachment
') + ->attachData('PDF content here', 'report.pdf', ['mime' => 'application/pdf']); + + $transport = new AzureTransport($client, ['save_to_sent_items' => false]); + $transport->send($email, Envelope::create($email)); + + $attachment = $captured['message']['attachments'][0]; + $this->assertSame('#microsoft.graph.fileAttachment', $attachment['@odata.type']); + $this->assertSame('report.pdf', $attachment['name']); + $this->assertSame(base64_encode('PDF content here'), $attachment['contentBytes']); + $this->assertStringContainsString('application', $attachment['contentType']); + } + + public function test_save_to_sent_items_is_configurable(): void + { + $captured = []; + $client = $this->createMock(GraphClient::class); + $client->method('send')->willReturnCallback(function ($p) use (&$captured) { $captured = $p; }); + + $email = (new Email()) + ->from('from@example.com') + ->to('to@example.com') + ->subject('Save it') + ->html('keep
'); + + $transport = new AzureTransport($client, ['save_to_sent_items' => true]); + $transport->send($email, Envelope::create($email)); + + $this->assertTrue($captured['saveToSentItems']); + } + + public function test_to_string_returns_azure(): void + { + $client = $this->createMock(GraphClient::class); + $transport = new AzureTransport($client, []); + + $this->assertSame('azure', (string) $transport); + } +} +``` + +- [ ] **Step 2: Run tests — expect failure** + +```powershell +vendor/bin/phpunit tests/Transport/AzureTransportTest.php +``` + +Expected: FAIL with `Class "PromoSeven\AzureMailer\Transport\AzureTransport" not found`. + +- [ ] **Step 3: Implement AzureTransport** + +Create `C:\Users\IT Department\Desktop\azure-mailer\src\Transport\AzureTransport.php`: + +```php +getOriginalMessage(); + + if (! $email instanceof Email) { + return; + } + + $this->client->send($this->buildPayload($email)); + } + + public function __toString(): string + { + return 'azure'; + } + + private function buildPayload(Email $email): array + { + return [ + 'message' => [ + 'subject' => $email->getSubject() ?? '', + 'body' => [ + 'contentType' => $email->getHtmlBody() !== null ? 'HTML' : 'Text', + 'content' => $email->getHtmlBody() ?? $email->getTextBody() ?? '', + ], + 'toRecipients' => $this->mapAddresses($email->getTo()), + 'ccRecipients' => $this->mapAddresses($email->getCc()), + 'bccRecipients' => $this->mapAddresses($email->getBcc()), + 'replyTo' => $this->mapAddresses($email->getReplyTo()), + 'attachments' => $this->mapAttachments($email), + ], + 'saveToSentItems' => (bool) ($this->config['save_to_sent_items'] ?? false), + ]; + } + + private function mapAddresses(array $addresses): array + { + return array_map(fn ($addr) => [ + 'emailAddress' => [ + 'address' => $addr->getAddress(), + 'name' => $addr->getName() ?? '', + ], + ], $addresses); + } + + private function mapAttachments(Email $email): array + { + $result = []; + + foreach ($email->getAttachments() as $attachment) { + if (! $attachment instanceof DataPart) { + continue; + } + + $result[] = [ + '@odata.type' => '#microsoft.graph.fileAttachment', + 'name' => $attachment->getFilename() ?? 'attachment', + 'contentType' => $attachment->getMediaType() . '/' . $attachment->getMediaSubtype(), + 'contentBytes' => base64_encode($attachment->getBody()), + ]; + } + + return $result; + } +} +``` + +- [ ] **Step 4: Run tests — expect all pass** + +```powershell +vendor/bin/phpunit tests/Transport/AzureTransportTest.php +``` + +Expected: `6 tests` — all PASS. + +- [ ] **Step 5: Run full test suite** + +```powershell +vendor/bin/phpunit +``` + +Expected: All tests pass across all test files. + +- [ ] **Step 6: Commit** + +```powershell +git add src/Transport/AzureTransport.php tests/Transport/AzureTransportTest.php +git commit -m "feat: add AzureTransport with payload building and attachment support" +``` + +--- + +## Task 6: Service Provider + +**Files:** +- Create: `src/AzureMailerServiceProvider.php` + +- [ ] **Step 1: Create the service provider** + +Create `C:\Users\IT Department\Desktop\azure-mailer\src\AzureMailerServiceProvider.php`: + +```php +mergeConfigFrom(__DIR__ . '/../config/azure-mailer.php', 'azure-mailer'); + } + + public function boot(): void + { + $this->publishes([ + __DIR__ . '/../config/azure-mailer.php' => config_path('azure-mailer.php'), + ], 'azure-mailer-config'); + + $this->app->make(MailManager::class)->extend('azure', function (array $config) { + $merged = array_merge(config('azure-mailer', []), $config); + + return new AzureTransport( + new GraphClient(new TokenManager($merged), $merged), + $merged + ); + }); + } +} +``` + +- [ ] **Step 2: Write a smoke test for the service provider** + +Add this test to a new file `C:\Users\IT Department\Desktop\azure-mailer\tests\ServiceProviderTest.php`: + +```php +set('mail.mailers.azure', [ + 'transport' => 'azure', + 'tenant_id' => 'test-tenant', + 'client_id' => 'test-client-id', + 'client_secret' => 'test-secret', + 'from_address' => 'sender@example.com', + ]); + } + + public function test_azure_transport_is_registered_with_mail_manager(): void + { + $manager = $this->app->make(MailManager::class); + $transport = $manager->mailer('azure')->getSymfonyTransport(); + + $this->assertInstanceOf(AzureTransport::class, $transport); + $this->assertSame('azure', (string) $transport); + } +} +``` + +- [ ] **Step 3: Run the smoke test** + +```powershell +vendor/bin/phpunit tests/ServiceProviderTest.php +``` + +Expected: `1 test` — PASS. + +- [ ] **Step 4: Run full test suite to confirm nothing regressed** + +```powershell +vendor/bin/phpunit +``` + +Expected: All tests pass. + +- [ ] **Step 5: Commit** + +```powershell +git add src/AzureMailerServiceProvider.php tests/ServiceProviderTest.php +git commit -m "feat: add AzureMailerServiceProvider with transport registration and config publishing" +``` + +--- + +## Task 7: Wire into OperationModule + +**Files:** +- Modify: `C:\Users\IT Department\Desktop\OperationModule\composer.json` +- Modify: `C:\Users\IT Department\Desktop\OperationModule\config\mail.php` +- Modify: `C:\Users\IT Department\Desktop\OperationModule\.env.example` + +- [ ] **Step 1: Add path repository and require in OperationModule's composer.json** + +In `C:\Users\IT Department\Desktop\OperationModule\composer.json`, add to the `repositories` array: + +```json +{ + "type": "path", + "url": "../azure-mailer" +} +``` + +And add to `require`: + +```json +"promoseven/azure-mailer": "*" +``` + +The `repositories` array should look like: + +```json +"repositories": [ + { + "type": "path", + "url": "../ultra-message" + }, + { + "type": "path", + "url": "../azure-mailer" + } +] +``` + +- [ ] **Step 2: Run composer update** + +Run from `C:\Users\IT Department\Desktop\OperationModule`: + +```powershell +composer require promoseven/azure-mailer:* +``` + +Expected: Package installed, `vendor/promoseven/azure-mailer` symlink created. + +- [ ] **Step 3: Add the azure mailer to config/mail.php** + +In `C:\Users\IT Department\Desktop\OperationModule\config\mail.php`, inside the `'mailers'` array, add: + +```php +'azure' => [ + 'transport' => 'azure', + 'tenant_id' => env('AZURE_TENANT_ID'), + 'client_id' => env('AZURE_CLIENT_ID'), + 'client_secret' => env('AZURE_CLIENT_SECRET'), + 'from_address' => env('AZURE_MAIL_FROM_ADDRESS'), +], +``` + +- [ ] **Step 4: Add env vars to .env.example** + +Add to `C:\Users\IT Department\Desktop\OperationModule\.env.example`: + +```env +# Microsoft 365 / Azure AD Mail +AZURE_TENANT_ID= +AZURE_CLIENT_ID= +AZURE_CLIENT_SECRET= +AZURE_MAIL_FROM_ADDRESS= +``` + +- [ ] **Step 5: Verify package auto-discovery** + +Run from `C:\Users\IT Department\Desktop\OperationModule`: + +```powershell +php artisan package:discover --ansi +``` + +Expected output includes: `Discovered Package: promoseven/azure-mailer`. + +- [ ] **Step 6: Commit OperationModule changes** + +Run from `C:\Users\IT Department\Desktop\OperationModule`: + +```powershell +git add composer.json composer.lock config/mail.php .env.example +git commit -m "feat: integrate promoseven/azure-mailer as mail transport" +``` + +--- + +## Task 8: README + +**Files:** +- Create: `C:\Users\IT Department\Desktop\azure-mailer\README.md` + +- [ ] **Step 1: Write README** + +Create `C:\Users\IT Department\Desktop\azure-mailer\README.md`: + +```markdown +# promoseven/azure-mailer + +Laravel mail transport for Microsoft 365 via the Azure AD Graph API. +Replaces SMTP with a Client Credentials OAuth2 flow — drop-in compatible with +Laravel's `Mail` facade, Mailables, Notifications, and queued mail. + +## Requirements + +- PHP 8.2+ +- Laravel 11 or 12 +- An Azure AD App Registration with `Mail.Send` application permission + +## Installation + +```bash +composer require promoseven/azure-mailer +``` + +## Azure AD Setup + +1. Go to **Azure Portal → App Registrations → New registration** +2. Note the **Tenant ID**, **Client ID** +3. Under **Certificates & secrets**, create a new **Client secret** +4. Under **API permissions**, add **Microsoft Graph → Application permissions → Mail.Send** +5. Click **Grant admin consent** + +## Configuration + +Add to `config/mail.php` under `mailers`: + +```php +'azure' => [ + 'transport' => 'azure', + 'tenant_id' => env('AZURE_TENANT_ID'), + 'client_id' => env('AZURE_CLIENT_ID'), + 'client_secret' => env('AZURE_CLIENT_SECRET'), + 'from_address' => env('AZURE_MAIL_FROM_ADDRESS'), +], +``` + +Set `.env`: + +```env +MAIL_MAILER=azure + +AZURE_TENANT_ID=your-tenant-id +AZURE_CLIENT_ID=your-client-id +AZURE_CLIENT_SECRET=your-client-secret +AZURE_MAIL_FROM_ADDRESS=noreply@yourdomain.com +``` + +The `from_address` must be a mailbox in your Microsoft 365 tenant. + +## Advanced config (optional) + +Publish the config file to override defaults: + +```bash +php artisan vendor:publish --tag=azure-mailer-config +``` + +This creates `config/azure-mailer.php`: + +```php +return [ + 'save_to_sent_items' => false, // set true to keep copies in Sent folder + 'timeout' => 30, // HTTP timeout in seconds + 'graph_api_version' => 'v1.0', // or 'beta' +]; +``` + +## Usage + +No changes needed — use Laravel mail exactly as before: + +```php +Mail::to('user@example.com')->send(new OrderConfirmation($order)); +``` + +## License + +MIT +``` + +- [ ] **Step 2: Commit README** + +Run from `C:\Users\IT Department\Desktop\azure-mailer`: + +```powershell +git add README.md +git commit -m "docs: add README with installation and Azure AD setup guide" +``` + +--- + +## Final verification + +- [ ] **Run the full package test suite one last time** + +Run from `C:\Users\IT Department\Desktop\azure-mailer`: + +```powershell +vendor/bin/phpunit --testdox +``` + +Expected: All tests pass, output shows each test name. + +- [ ] **Verify OperationModule still boots** + +Run from `C:\Users\IT Department\Desktop\OperationModule`: + +```powershell +php artisan about +``` + +Expected: No errors, `promoseven/azure-mailer` visible in package list.