From d099c018524e22b16b97395eaad4af654bf23536 Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Thu, 9 Jul 2026 13:02:38 -0700 Subject: [PATCH 1/7] Add Microsoft Graph token provider and tests Introduce MicrosoftGraphTokenProvider to obtain and cache OAuth2 client-credentials tokens from Microsoft Graph. The provider validates required credentials, posts to the tenant token endpoint via Guzzle, throws a RuntimeException on missing creds or HTTP errors (propagating server error messages), and caches the access token for ~50 minutes using a server-specific cache key. Includes unit tests that assert exception behavior for missing credentials and mock Guzzle to verify token request parameters and caching behavior. --- .../Mail/MicrosoftGraphTokenProvider.php | 61 ++++++++++++++++ .../Mail/MicrosoftGraphTokenProviderTest.php | 73 +++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 ProcessMaker/Mail/MicrosoftGraphTokenProvider.php create mode 100644 tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php diff --git a/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php b/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php new file mode 100644 index 0000000000..1926107806 --- /dev/null +++ b/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php @@ -0,0 +1,61 @@ +serverIndex ?: 'default'); + + return Cache::remember($cacheKey, now()->addMinutes(50), function () { + return $this->requestAccessToken(); + }); + } + + private function requestAccessToken(): string + { + $tenantId = $this->config['tenant_id'] ?? null; + $clientId = $this->config['key'] ?? null; + $clientSecret = $this->config['secret'] ?? null; + + if (!$tenantId || !$clientId || !$clientSecret) { + throw new RuntimeException('Microsoft Graph credentials are not configured.'); + } + + $client = new Client(); + $response = $client->post(sprintf(self::TOKEN_URL, $tenantId), [ + 'form_params' => [ + 'client_id' => $clientId, + 'client_secret' => $clientSecret, + 'scope' => self::DEFAULT_SCOPE, + 'grant_type' => 'client_credentials', + ], + 'http_errors' => false, + ]); + + $body = json_decode($response->getBody()->getContents(), true); + + if ($response->getStatusCode() >= 400) { + $message = $body['error_description'] ?? $body['error']['message'] ?? 'Unknown error'; + + throw new RuntimeException('Failed to get Microsoft Graph access token: ' . $message); + } + + return $body['access_token']; + } +} diff --git a/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php b/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php new file mode 100644 index 0000000000..51f434b75e --- /dev/null +++ b/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php @@ -0,0 +1,73 @@ + '', + 'key' => 'client-id', + 'secret' => 'client-secret', + ]); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Microsoft Graph credentials are not configured.'); + + $provider->getAccessToken(); + } + + public function testGetAccessTokenRequestsTokenFromMicrosoft() + { + Cache::flush(); + + $tokenResponseBody = Mockery::mock(); + $tokenResponseBody->shouldReceive('getContents') + ->andReturn(json_encode(['access_token' => 'token-from-azure'])); + + $tokenResponse = Mockery::mock(); + $tokenResponse->shouldReceive('getStatusCode')->andReturn(200); + $tokenResponse->shouldReceive('getBody')->andReturn($tokenResponseBody); + + $guzzle = Mockery::mock('overload:GuzzleHttp\Client'); + $guzzle->shouldReceive('post') + ->once() + ->with( + 'https://login.microsoftonline.com/tenant-id-123/oauth2/v2.0/token', + Mockery::on(function ($options) { + return $options['form_params']['client_id'] === 'client-id-123' + && $options['form_params']['client_secret'] === 'client-secret-123' + && $options['form_params']['scope'] === 'https://graph.microsoft.com/.default' + && $options['form_params']['grant_type'] === 'client_credentials' + && $options['http_errors'] === false; + }) + ) + ->andReturn($tokenResponse); + + $provider = new MicrosoftGraphTokenProvider([ + 'tenant_id' => 'tenant-id-123', + 'key' => 'client-id-123', + 'secret' => 'client-secret-123', + ], 1); + + $this->assertSame('token-from-azure', $provider->getAccessToken()); + $this->assertSame('token-from-azure', $provider->getAccessToken()); + } +} From 727173822d1bef83180ce54ed754d4127c07ba87 Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Thu, 9 Jul 2026 14:37:21 -0700 Subject: [PATCH 2/7] Inject HTTP client into token provider Add an optional Guzzle Client dependency to MicrosoftGraphTokenProvider so an HTTP client can be injected for testing. The constructor now accepts ?Client $httpClient and the provider uses $this->httpClient ?? new Client() when posting for tokens. Update the unit test to mock GuzzleHttp\Client (remove overload-based mock and separate-process annotations), import Client, and pass the mocked client into the provider. This enables dependency injection and simpler, more reliable tests. --- ProcessMaker/Mail/MicrosoftGraphTokenProvider.php | 5 +++-- .../Mail/MicrosoftGraphTokenProviderTest.php | 9 +++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php b/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php index 1926107806..913274bea6 100644 --- a/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php +++ b/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php @@ -14,7 +14,8 @@ class MicrosoftGraphTokenProvider public function __construct( private array $config, - private int|string $serverIndex = 0 + private int|string $serverIndex = 0, + private ?Client $httpClient = null, ) { } @@ -37,7 +38,7 @@ private function requestAccessToken(): string throw new RuntimeException('Microsoft Graph credentials are not configured.'); } - $client = new Client(); + $client = $this->httpClient ?? new Client(); $response = $client->post(sprintf(self::TOKEN_URL, $tenantId), [ 'form_params' => [ 'client_id' => $clientId, diff --git a/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php b/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php index 51f434b75e..4c5870934b 100644 --- a/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php +++ b/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php @@ -2,16 +2,13 @@ namespace Tests\Unit\ProcessMaker\Mail; +use GuzzleHttp\Client; use Illuminate\Support\Facades\Cache; use Mockery; use ProcessMaker\Mail\MicrosoftGraphTokenProvider; use RuntimeException; use Tests\TestCase; -/** - * @runTestsInSeparateProcesses - * @preserveGlobalState disabled - */ class MicrosoftGraphTokenProviderTest extends TestCase { protected function tearDown(): void @@ -46,7 +43,7 @@ public function testGetAccessTokenRequestsTokenFromMicrosoft() $tokenResponse->shouldReceive('getStatusCode')->andReturn(200); $tokenResponse->shouldReceive('getBody')->andReturn($tokenResponseBody); - $guzzle = Mockery::mock('overload:GuzzleHttp\Client'); + $guzzle = Mockery::mock(Client::class); $guzzle->shouldReceive('post') ->once() ->with( @@ -65,7 +62,7 @@ public function testGetAccessTokenRequestsTokenFromMicrosoft() 'tenant_id' => 'tenant-id-123', 'key' => 'client-id-123', 'secret' => 'client-secret-123', - ], 1); + ], 1, $guzzle); $this->assertSame('token-from-azure', $provider->getAccessToken()); $this->assertSame('token-from-azure', $provider->getAccessToken()); From b0d14ca501733e5af85d67b9b7f925ebdf62e8b3 Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:40:11 -0700 Subject: [PATCH 3/7] Use real PSR-7 Response in test Replace Mockery-based response mocks in MicrosoftGraphTokenProviderTest with an actual GuzzleHttp\Psr7\Response instance. Simplifies the test by providing a real PSR-7 response (200) with a JSON body and removes the need to mock getBody()/getContents() and getStatusCode(). Added the Response use statement. --- .../Mail/MicrosoftGraphTokenProviderTest.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php b/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php index 4c5870934b..a4f62d67d5 100644 --- a/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php +++ b/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php @@ -3,6 +3,7 @@ namespace Tests\Unit\ProcessMaker\Mail; use GuzzleHttp\Client; +use GuzzleHttp\Psr7\Response; use Illuminate\Support\Facades\Cache; use Mockery; use ProcessMaker\Mail\MicrosoftGraphTokenProvider; @@ -35,13 +36,7 @@ public function testGetAccessTokenRequestsTokenFromMicrosoft() { Cache::flush(); - $tokenResponseBody = Mockery::mock(); - $tokenResponseBody->shouldReceive('getContents') - ->andReturn(json_encode(['access_token' => 'token-from-azure'])); - - $tokenResponse = Mockery::mock(); - $tokenResponse->shouldReceive('getStatusCode')->andReturn(200); - $tokenResponse->shouldReceive('getBody')->andReturn($tokenResponseBody); + $tokenResponse = new Response(200, [], json_encode(['access_token' => 'token-from-azure'])); $guzzle = Mockery::mock(Client::class); $guzzle->shouldReceive('post') From 79347af0eac2e72941ce58c0067da34c44ae75eb Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:46:27 -0700 Subject: [PATCH 4/7] Add Microsoft Graph mail transport and config Introduce Microsoft Graph mail support: add MicrosoftGraphTransport and MicrosoftGraphMessageConverter to send emails via the Graph API and a MicrosoftGraphTokenProvider to obtain access tokens. Integrate the transport into OauthMailManager (createTransport/createMicrosoftGraphTransport) and register a 'microsoft_graph' mailer in config/mail.php. Update the admin settings UI to show the authorization button when the mail driver is set to 'microsoft_graph'. The transport posts a sendMail payload to the Graph endpoint and throws a TransportException on HTTP errors. --- .../Mail/MicrosoftGraphMessageConverter.php | 55 +++++++++++++++++++ .../Mail/MicrosoftGraphTokenProvider.php | 2 +- .../Transports/MicrosoftGraphTransport.php | 53 ++++++++++++++++++ ProcessMaker/Managers/OauthMailManager.php | 22 ++++++++ config/mail.php | 4 ++ .../settings/components/SettingsListing.vue | 4 +- 6 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 ProcessMaker/Mail/MicrosoftGraphMessageConverter.php create mode 100644 ProcessMaker/Mail/Transports/MicrosoftGraphTransport.php diff --git a/ProcessMaker/Mail/MicrosoftGraphMessageConverter.php b/ProcessMaker/Mail/MicrosoftGraphMessageConverter.php new file mode 100644 index 0000000000..e95a3ea281 --- /dev/null +++ b/ProcessMaker/Mail/MicrosoftGraphMessageConverter.php @@ -0,0 +1,55 @@ +getHtmlBody(); + $textBody = $email->getTextBody(); + + $message = [ + 'subject' => $email->getSubject() ?? '', + 'body' => [ + 'contentType' => $htmlBody !== null ? 'HTML' : 'Text', + 'content' => $htmlBody ?? $textBody ?? '', + ], + 'toRecipients' => self::convertAddresses($email->getTo()), + ]; + + $ccRecipients = self::convertAddresses($email->getCc()); + if ($ccRecipients) { + $message['ccRecipients'] = $ccRecipients; + } + + $bccRecipients = self::convertAddresses($email->getBcc()); + if ($bccRecipients) { + $message['bccRecipients'] = $bccRecipients; + } + + return [ + 'message' => $message, + 'saveToSentItems' => true, + ]; + } + + /** + * @param Address[] $addresses + */ + private static function convertAddresses(array $addresses): array + { + return array_map(function (Address $address) { + $emailAddress = ['address' => $address->getAddress()]; + + if ($address->getName()) { + $emailAddress['name'] = $address->getName(); + } + + return ['emailAddress' => $emailAddress]; + }, $addresses); + } +} diff --git a/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php b/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php index 913274bea6..1b97e5af05 100644 --- a/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php +++ b/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php @@ -15,7 +15,7 @@ class MicrosoftGraphTokenProvider public function __construct( private array $config, private int|string $serverIndex = 0, - private ?Client $httpClient = null, + private ?Client $httpClient = null ) { } diff --git a/ProcessMaker/Mail/Transports/MicrosoftGraphTransport.php b/ProcessMaker/Mail/Transports/MicrosoftGraphTransport.php new file mode 100644 index 0000000000..59ff0d1b96 --- /dev/null +++ b/ProcessMaker/Mail/Transports/MicrosoftGraphTransport.php @@ -0,0 +1,53 @@ +client = $client ?? new Client([ + 'base_uri' => 'https://graph.microsoft.com/v1.0/', + ]); + } + + protected function doSend(SentMessage $message): void + { + $email = MessageConverter::toEmail($message->getOriginalMessage()); + $payload = MicrosoftGraphMessageConverter::toSendMailPayload($email); + $token = $this->tokenProvider->getAccessToken(); + + $response = $this->client->post('users/' . rawurlencode($this->senderEmail) . '/sendMail', [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $token, + 'Content-Type' => 'application/json', + ], + 'json' => $payload, + 'http_errors' => false, + ]); + + if ($response->getStatusCode() >= 400) { + throw new TransportException( + 'Microsoft Graph send failed: ' . $response->getBody()->getContents() + ); + } + } + + public function __toString(): string + { + return 'microsoft-graph'; + } +} diff --git a/ProcessMaker/Managers/OauthMailManager.php b/ProcessMaker/Managers/OauthMailManager.php index 603b174970..58a7d08b12 100644 --- a/ProcessMaker/Managers/OauthMailManager.php +++ b/ProcessMaker/Managers/OauthMailManager.php @@ -7,6 +7,8 @@ use Google\Client as GoogleClient; use GuzzleHttp\Client; use Illuminate\Mail\MailManager; +use ProcessMaker\Mail\MicrosoftGraphTokenProvider; +use ProcessMaker\Mail\Transports\MicrosoftGraphTransport; use ProcessMaker\Models\EnvironmentVariable; use ProcessMaker\Models\Setting; use ProcessMaker\Packages\Connectors\Email\EmailConfig; @@ -77,6 +79,26 @@ protected function createSmtpTransport($config) return $transport; } + public function createTransport(array $config) + { + if ($this->app->config->get('mail.driver') === 'microsoft_graph') { + return $this->createMicrosoftGraphTransport($config); + } + + return parent::createTransport($config); + } + + protected function createMicrosoftGraphTransport(array $config) + { + return new MicrosoftGraphTransport( + new MicrosoftGraphTokenProvider( + $this->app->config->get('services.microsoft_graph', []), + $this->emailServerIndex ?? 0 + ), + $this->fromAddress + ); + } + public function checkForExpiredAccessToken() { switch ($this->authMethod) { diff --git a/config/mail.php b/config/mail.php index d78ea38d62..1b524c46c7 100644 --- a/config/mail.php +++ b/config/mail.php @@ -86,6 +86,10 @@ 'transport' => 'array', ], + 'microsoft_graph' => [ + 'transport' => 'microsoft_graph', + ], + 'failover' => [ 'transport' => 'failover', 'mailers' => [ diff --git a/resources/js/admin/settings/components/SettingsListing.vue b/resources/js/admin/settings/components/SettingsListing.vue index ecba515d51..01a3ae7e30 100644 --- a/resources/js/admin/settings/components/SettingsListing.vue +++ b/resources/js/admin/settings/components/SettingsListing.vue @@ -607,7 +607,9 @@ export default { filterEmailServerButtons(groupName, groupData, btn) { const authMethod = groupData.find(data => data.key.includes("EMAIL_CONNECTOR_MAIL_AUTH_METHOD")); const selectedAuthMethod = authMethod ? authMethod.ui.options[authMethod.config] : null; - const showAuthAccBtn = selectedAuthMethod && selectedAuthMethod !== 'standard' ? true : false; + const mailDriver = groupData.find(data => data.key.includes("EMAIL_CONNECTOR_MAIL_DRIVER")); + const selectedDriver = mailDriver ? mailDriver.ui.options[mailDriver.config] : null; + const showAuthAccBtn = (selectedAuthMethod && selectedAuthMethod !== 'standard') || selectedDriver === 'microsoft_graph'; if (groupName.includes('Email Server') && !showAuthAccBtn) { // Returns all 'top' position buttons except the '+ Mail Server' and 'Authorize Account' button for email server tabs From 91b80b6032cb45b106932fe3698e5733cdc95471 Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Wed, 15 Jul 2026 12:02:33 -0700 Subject: [PATCH 5/7] Remove authorization badge from microsoft_graph --- resources/js/admin/settings/components/SettingsListing.vue | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/resources/js/admin/settings/components/SettingsListing.vue b/resources/js/admin/settings/components/SettingsListing.vue index 01a3ae7e30..0d489764b3 100644 --- a/resources/js/admin/settings/components/SettingsListing.vue +++ b/resources/js/admin/settings/components/SettingsListing.vue @@ -607,9 +607,7 @@ export default { filterEmailServerButtons(groupName, groupData, btn) { const authMethod = groupData.find(data => data.key.includes("EMAIL_CONNECTOR_MAIL_AUTH_METHOD")); const selectedAuthMethod = authMethod ? authMethod.ui.options[authMethod.config] : null; - const mailDriver = groupData.find(data => data.key.includes("EMAIL_CONNECTOR_MAIL_DRIVER")); - const selectedDriver = mailDriver ? mailDriver.ui.options[mailDriver.config] : null; - const showAuthAccBtn = (selectedAuthMethod && selectedAuthMethod !== 'standard') || selectedDriver === 'microsoft_graph'; + const showAuthAccBtn = selectedAuthMethod && selectedAuthMethod !== 'standard'; if (groupName.includes('Email Server') && !showAuthAccBtn) { // Returns all 'top' position buttons except the '+ Mail Server' and 'Authorize Account' button for email server tabs From fc31cdb71c067702cd16e00a73c0b5ba069aec33 Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Thu, 16 Jul 2026 09:29:01 -0700 Subject: [PATCH 6/7] Remove caching from Microsoft Graph token provider Drop the use of Illuminate\Support\Facades\Cache and the Cache::remember call so getAccessToken always calls requestAccessToken. --- ProcessMaker/Mail/MicrosoftGraphTokenProvider.php | 9 ++------- .../Mail/MicrosoftGraphTokenProviderTest.php | 4 ---- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php b/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php index 1b97e5af05..7f2bf2fa85 100644 --- a/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php +++ b/ProcessMaker/Mail/MicrosoftGraphTokenProvider.php @@ -3,7 +3,6 @@ namespace ProcessMaker\Mail; use GuzzleHttp\Client; -use Illuminate\Support\Facades\Cache; use RuntimeException; class MicrosoftGraphTokenProvider @@ -15,17 +14,13 @@ class MicrosoftGraphTokenProvider public function __construct( private array $config, private int|string $serverIndex = 0, - private ?Client $httpClient = null + private ?Client $httpClient = null, ) { } public function getAccessToken(): string { - $cacheKey = 'microsoft_graph_access_token_' . ($this->serverIndex ?: 'default'); - - return Cache::remember($cacheKey, now()->addMinutes(50), function () { - return $this->requestAccessToken(); - }); + return $this->requestAccessToken(); } private function requestAccessToken(): string diff --git a/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php b/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php index a4f62d67d5..be23d292f6 100644 --- a/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php +++ b/tests/unit/ProcessMaker/Mail/MicrosoftGraphTokenProviderTest.php @@ -4,7 +4,6 @@ use GuzzleHttp\Client; use GuzzleHttp\Psr7\Response; -use Illuminate\Support\Facades\Cache; use Mockery; use ProcessMaker\Mail\MicrosoftGraphTokenProvider; use RuntimeException; @@ -34,8 +33,6 @@ public function testGetAccessTokenThrowsWhenCredentialsAreMissing() public function testGetAccessTokenRequestsTokenFromMicrosoft() { - Cache::flush(); - $tokenResponse = new Response(200, [], json_encode(['access_token' => 'token-from-azure'])); $guzzle = Mockery::mock(Client::class); @@ -60,6 +57,5 @@ public function testGetAccessTokenRequestsTokenFromMicrosoft() ], 1, $guzzle); $this->assertSame('token-from-azure', $provider->getAccessToken()); - $this->assertSame('token-from-azure', $provider->getAccessToken()); } } From 3b82c6141ef626cd767b7faa867d875b91cadeae Mon Sep 17 00:00:00 2001 From: sanja <52755494+sanjacornelius@users.noreply.github.com> Date: Thu, 16 Jul 2026 11:44:28 -0700 Subject: [PATCH 7/7] Include file attachments in Graph payload Add convertAttachments() to MicrosoftGraphMessageConverter to map Symfony Email attachments to Microsoft Graph fileAttachment objects (name, contentType, contentBytes base64) and include them in toSendMailPayload when present. --- .../Mail/MicrosoftGraphMessageConverter.php | 21 +++++++++ .../MicrosoftGraphMessageConverterTest.php | 43 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 tests/unit/ProcessMaker/Mail/MicrosoftGraphMessageConverterTest.php diff --git a/ProcessMaker/Mail/MicrosoftGraphMessageConverter.php b/ProcessMaker/Mail/MicrosoftGraphMessageConverter.php index e95a3ea281..a87205cdcc 100644 --- a/ProcessMaker/Mail/MicrosoftGraphMessageConverter.php +++ b/ProcessMaker/Mail/MicrosoftGraphMessageConverter.php @@ -31,6 +31,11 @@ public static function toSendMailPayload(Email $email): array $message['bccRecipients'] = $bccRecipients; } + $attachments = self::convertAttachments($email); + if ($attachments) { + $message['attachments'] = $attachments; + } + return [ 'message' => $message, 'saveToSentItems' => true, @@ -52,4 +57,20 @@ private static function convertAddresses(array $addresses): array return ['emailAddress' => $emailAddress]; }, $addresses); } + + private static function convertAttachments(Email $email): array + { + $attachments = []; + + foreach ($email->getAttachments() as $attachment) { + $attachments[] = [ + '@odata.type' => '#microsoft.graph.fileAttachment', + 'name' => $attachment->getFilename() ?: 'attachment', + 'contentType' => $attachment->getMediaType() . '/' . $attachment->getMediaSubtype(), + 'contentBytes' => base64_encode($attachment->getBody()), + ]; + } + + return $attachments; + } } diff --git a/tests/unit/ProcessMaker/Mail/MicrosoftGraphMessageConverterTest.php b/tests/unit/ProcessMaker/Mail/MicrosoftGraphMessageConverterTest.php new file mode 100644 index 0000000000..71d172a2ca --- /dev/null +++ b/tests/unit/ProcessMaker/Mail/MicrosoftGraphMessageConverterTest.php @@ -0,0 +1,43 @@ +to('recipient@example.com') + ->subject('With attachment') + ->html('
Hello
') + ->attach('file-contents', 'report.txt', 'text/plain'); + + $payload = MicrosoftGraphMessageConverter::toSendMailPayload($email); + + $this->assertSame('With attachment', $payload['message']['subject']); + $this->assertSame('HTML', $payload['message']['body']['contentType']); + $this->assertCount(1, $payload['message']['attachments']); + $this->assertSame([ + '@odata.type' => '#microsoft.graph.fileAttachment', + 'name' => 'report.txt', + 'contentType' => 'text/plain', + 'contentBytes' => base64_encode('file-contents'), + ], $payload['message']['attachments'][0]); + } + + public function testToSendMailPayloadOmitsAttachmentsKeyWhenNonePresent() + { + $email = (new Email()) + ->to('recipient@example.com') + ->subject('No attachment') + ->text('Hello'); + + $payload = MicrosoftGraphMessageConverter::toSendMailPayload($email); + + $this->assertArrayNotHasKey('attachments', $payload['message']); + } +}