Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Raise PHPStan level to max and allow multiple mailers #23

Merged
merged 3 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/phpstan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ jobs:
name: phpstan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Checkout code
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
Expand All @@ -23,4 +24,4 @@ jobs:
uses: ramsey/composer-install@v3

- name: Run PHPStan
run: ./vendor/bin/phpstan --error-format=github
run: vendor/bin/phpstan --error-format=github
3 changes: 1 addition & 2 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ includes:
- phpstan-baseline.neon

parameters:
level: 8
level: max
paths:
- src
tmpDir: build/phpstan
checkOctaneCompatibility: true
checkModelProperties: true
checkMissingIterableValueType: false
checkGenericClassInNonGenericObjectType: false

14 changes: 14 additions & 0 deletions src/Exceptions/ConfigurationInvalid.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace InnoGE\LaravelMsGraphMail\Exceptions;

use Exception;

class ConfigurationInvalid extends Exception
{
public function __construct(string $key, mixed $value)
{
$invalidValue = var_export($value, true);
parent::__construct("Configuration key {$key} for microsoft-graph mailer has invalid value: {$invalidValue}.");
}
}
19 changes: 2 additions & 17 deletions src/Exceptions/ConfigurationMissing.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,8 @@

class ConfigurationMissing extends Exception
{
public static function tenantId(): self
public function __construct(string $key)
{
return new self('The tenant id is missing from the configuration file.');
}

public static function clientId(): self
{
return new self('The client id is missing from the configuration file.');
}

public static function clientSecret(): self
{
return new self('The client secret is missing from the configuration file.');
}

public static function fromAddress(): self
{
return new self('The mail from address is missing from the configuration file.');
parent::__construct("Configuration key {$key} for microsoft-graph mailer is missing.");
}
}
9 changes: 9 additions & 0 deletions src/Exceptions/InvalidResponse.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace InnoGE\LaravelMsGraphMail\Exceptions;

use Exception;

class InvalidResponse extends Exception
{
}
47 changes: 31 additions & 16 deletions src/LaravelMsGraphMailServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace InnoGE\LaravelMsGraphMail;

use Illuminate\Support\Facades\Mail;
use InnoGE\LaravelMsGraphMail\Exceptions\ConfigurationInvalid;
use InnoGE\LaravelMsGraphMail\Exceptions\ConfigurationMissing;
use InnoGE\LaravelMsGraphMail\Services\MicrosoftGraphApiService;
use Spatie\LaravelPackageTools\Package;
Expand All @@ -23,26 +24,40 @@ public function configurePackage(Package $package): void

public function boot(): void
{
$this->app->bind(MicrosoftGraphApiService::class, function () {
//throw exceptions when config is missing
throw_unless(filled(config('mail.mailers.microsoft-graph.tenant_id')), ConfigurationMissing::tenantId());
throw_unless(filled(config('mail.mailers.microsoft-graph.client_id')), ConfigurationMissing::clientId());
throw_unless(filled(config('mail.mailers.microsoft-graph.client_secret')), ConfigurationMissing::clientSecret());

return new MicrosoftGraphApiService(
tenantId: config('mail.mailers.microsoft-graph.tenant_id', ''),
clientId: config('mail.mailers.microsoft-graph.client_id', ''),
clientSecret: config('mail.mailers.microsoft-graph.client_secret', ''),
accessTokenTtl: config('mail.mailers.microsoft-graph.access_token_ttl', 3000),
);
});
Mail::extend('microsoft-graph', function (array $config): MicrosoftGraphTransport {
throw_if(blank($config['from']['address'] ?? []), new ConfigurationMissing('from.address'));

Mail::extend('microsoft-graph', function (array $config) {
throw_unless(filled($config['from']['address'] ?? []), ConfigurationMissing::fromAddress());
$accessTokenTtl = $config['access_token_ttl'] ?? 3000;
if (! is_int($accessTokenTtl)) {
throw new ConfigurationInvalid('access_token_ttl', $accessTokenTtl);
}

return new MicrosoftGraphTransport(
$this->app->make(MicrosoftGraphApiService::class)
new MicrosoftGraphApiService(
tenantId: $this->requireConfigString($config, 'tenant_id'),
clientId: $this->requireConfigString($config, 'client_id'),
clientSecret: $this->requireConfigString($config, 'client_secret'),
accessTokenTtl: $accessTokenTtl,
),
);
});
}

/**
* @param array<string, mixed> $config
* @return non-empty-string
*/
protected function requireConfigString(array $config, string $key): string
{
if (! array_key_exists($key, $config)) {
throw new ConfigurationMissing($key);
}

$value = $config[$key];
if (! is_string($value) || $value === '') {
throw new ConfigurationInvalid($key, $value);
}

return $value;
}
}
4 changes: 2 additions & 2 deletions src/MicrosoftGraphTransport.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ protected function prepareAttachments(Email $email, ?string $html): array
}

/**
* @param Collection<Address> $recipients
* @param Collection<array-key, Address> $recipients
*/
protected function transformEmailAddresses(Collection $recipients): array
{
Expand All @@ -101,7 +101,7 @@ protected function transformEmailAddress(Address $address): array
}

/**
* @return Collection<Address>
* @return Collection<array-key, Address>
*/
protected function getRecipients(Email $email, Envelope $envelope): Collection
{
Expand Down
9 changes: 8 additions & 1 deletion src/Services/MicrosoftGraphApiService.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use InnoGE\LaravelMsGraphMail\Exceptions\InvalidResponse;

class MicrosoftGraphApiService
{
Expand Down Expand Up @@ -48,7 +49,13 @@ protected function getAccessToken(): string

$response->throw();

return $response->json('access_token');
$accessToken = $response->json('access_token');
if (! is_string($accessToken)) {
$notString = var_export($accessToken, true);
throw new InvalidResponse("Expected response to contain key access_token of type string, got: {$notString}.");
}

return $accessToken;
});
}
}
160 changes: 113 additions & 47 deletions tests/MicrosoftGraphTransportTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use InnoGE\LaravelMsGraphMail\Exceptions\ConfigurationInvalid;
use InnoGE\LaravelMsGraphMail\Exceptions\ConfigurationMissing;
use InnoGE\LaravelMsGraphMail\Exceptions\InvalidResponse;
use InnoGE\LaravelMsGraphMail\Tests\Stubs\TestMail;
use InnoGE\LaravelMsGraphMail\Tests\Stubs\TestMailWithInlineImage;

Expand Down Expand Up @@ -219,69 +221,133 @@
->toBe('foo_access_token');
});

it('throws exceptions when config is missing', function (array $config, string $exceptionMessage) {
it('throws exceptions on invalid access token in response', function () {
Config::set('mail.mailers.microsoft-graph', [
'transport' => 'microsoft-graph',
'client_id' => 'foo_client_id',
'client_secret' => 'foo_client_secret',
'tenant_id' => 'foo_tenant_id',
'from' => [
'address' => '[email protected]',
'name' => 'Taylor Otwell',
],
]);
Config::set('mail.default', 'microsoft-graph');

Http::fake([
'https://login.microsoftonline.com/foo_tenant_id/oauth2/v2.0/token' => Http::response(['access_token' => 123]),
]);

expect(fn () => Mail::to('[email protected]')->send(new TestMail(false)))
->toThrow(InvalidResponse::class, 'Expected response to contain key access_token of type string, got: 123.');
});

it('throws exceptions when config is invalid', function (array $config, Exception $exception) {
Config::set('mail.mailers.microsoft-graph', $config);
Config::set('mail.default', 'microsoft-graph');

try {
Mail::to('[email protected]')
->send(new TestMail(false));
} catch (Exception $e) {
expect($e)
->toBeInstanceOf(ConfigurationMissing::class)
->getMessage()->toBe($exceptionMessage);
}
})->with(
expect(fn () => Mail::to('[email protected]')->send(new TestMail(false)))
->toThrow(get_class($exception), $exception->getMessage());
})->with([
[
[
[
'transport' => 'microsoft-graph',
'client_id' => 'foo_client_id',
'client_secret' => 'foo_client_secret',
'tenant_id' => '',
'from' => [
'address' => '[email protected]',
'name' => 'Taylor Otwell',
],
'transport' => 'microsoft-graph',
'client_id' => 'foo_client_id',
'client_secret' => 'foo_client_secret',
'from' => [
'address' => '[email protected]',
'name' => 'Taylor Otwell',
],
'The tenant id is missing from the configuration file.',
],
new ConfigurationMissing('tenant_id'),
],
[
[
[
'transport' => 'microsoft-graph',
'client_id' => '',
'client_secret' => 'foo_client_secret',
'tenant_id' => 'foo_tenant_id',
'from' => [
'address' => '[email protected]',
'name' => 'Taylor Otwell',
],
'transport' => 'microsoft-graph',
'tenant_id' => 123,
'client_id' => 'foo_client_id',
'client_secret' => 'foo_client_secret',
'from' => [
'address' => '[email protected]',
'name' => 'Taylor Otwell',
],
'The client id is missing from the configuration file.',
],
new ConfigurationInvalid('tenant_id', 123),
],
[
[
[
'transport' => 'microsoft-graph',
'client_id' => 'foo_client_id',
'client_secret' => '',
'tenant_id' => 'foo_tenant_id',
'from' => [
'address' => '[email protected]',
'name' => 'Taylor Otwell',
],
'transport' => 'microsoft-graph',
'tenant_id' => 'foo_tenant_id',
'client_secret' => 'foo_client_secret',
'from' => [
'address' => '[email protected]',
'name' => 'Taylor Otwell',
],
'The client secret is missing from the configuration file.',
],
new ConfigurationMissing('client_id'),
],
[
[
[
'transport' => 'microsoft-graph',
'client_id' => 'foo_client_id',
'client_secret' => 'foo_client_secret',
'tenant_id' => 'foo_tenant_id',
'transport' => 'microsoft-graph',
'tenant_id' => 'foo_tenant_id',
'client_id' => '',
'client_secret' => 'foo_client_secret',
'from' => [
'address' => '[email protected]',
'name' => 'Taylor Otwell',
],
'The mail from address is missing from the configuration file.',
],
]);
new ConfigurationInvalid('client_id', ''),
],
[
[
'transport' => 'microsoft-graph',
'tenant_id' => 'foo_tenant_id',
'client_id' => 'foo_client_id',
'from' => [
'address' => '[email protected]',
'name' => 'Taylor Otwell',
],
],
new ConfigurationMissing('client_secret'),
],
[
[
'transport' => 'microsoft-graph',
'tenant_id' => 'foo_tenant_id',
'client_id' => 'foo_client_id',
'client_secret' => null,
'from' => [
'address' => '[email protected]',
'name' => 'Taylor Otwell',
],
],
new ConfigurationInvalid('client_secret', null),
],
[
[
'transport' => 'microsoft-graph',
'tenant_id' => 'foo_tenant_id',
'client_id' => 'foo_client_id',
'client_secret' => 'foo_client_secret',
],
new ConfigurationMissing('from.address'),
],
[
[
'transport' => 'microsoft-graph',
'tenant_id' => 'foo_tenant_id',
'client_id' => 'foo_client_id',
'client_secret' => 'foo_client_secret',
'access_token_ttl' => false,
'from' => [
'address' => '[email protected]',
'name' => 'Taylor Otwell',
],
],
new ConfigurationInvalid('access_token_ttl', false),
],
]);

it('sends html mails with inline images with microsoft graph', function () {
Config::set('mail.mailers.microsoft-graph', [
Expand Down