Skip to content

Commit a455737

Browse files
committed
wip
1 parent 78f08ad commit a455737

11 files changed

+317
-33
lines changed

README.md

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,39 +23,20 @@ You can install the package via composer:
2323
composer require innoge/laravel-msgraph-mail
2424
```
2525

26-
You can publish and run the migrations with:
27-
28-
```bash
29-
php artisan vendor:publish --tag="laravel-msgraph-mail-migrations"
30-
php artisan migrate
31-
```
32-
33-
You can publish the config file with:
34-
35-
```bash
36-
php artisan vendor:publish --tag="laravel-msgraph-mail-config"
37-
```
38-
39-
This is the contents of the published config file:
26+
First you need to add a new entry to the mail drivers array in your config/mail.php configuration file:
4027

4128
```php
42-
return [
43-
];
29+
'microsoft-graph' => [
30+
'transport' => 'microsoft-graph',
31+
'client_id' => env('MICROSOFT_GRAPH_CLIENT_ID'),
32+
'client_secret' => env('MICROSOFT_GRAPH_CLIENT_SECRET'),
33+
'tenant_id' => env('MICROSOFT_GRAPH_TENANT_ID'),
34+
'from' => [
35+
'address' => env('MAIL_FROM_ADDRESS'),
36+
'name' => env('MAIL_FROM_NAME'),
37+
],
38+
],
4439
```
45-
46-
Optionally, you can publish the views using
47-
48-
```bash
49-
php artisan vendor:publish --tag="laravel-msgraph-mail-views"
50-
```
51-
52-
## Usage
53-
54-
```php
55-
$laravelMsGraphMail = new InnoGE\LaravelMsGraphMail();
56-
echo $laravelMsGraphMail->echoPhrase('Hello, InnoGE!');
57-
```
58-
5940
## Testing
6041

6142
```bash

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
],
1818
"require": {
1919
"php": "^8.1",
20+
"illuminate/contracts": "^9.0",
2021
"spatie/laravel-package-tools": "^1.14.0",
21-
"illuminate/contracts": "^9.0"
22+
"symfony/mailer": "^6.0"
2223
},
2324
"require-dev": {
2425
"laravel/pint": "^1.0",

config/msgraph-mail.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22

33
// config for InnoGE/LaravelMsGraphMail
44
return [
5-
5+
'',
66
];

src/Dto/MicrosoftGraphMail.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace InnoGE\LaravelMsGraphMail\Dto;
4+
5+
class MicrosoftGraphMail
6+
{
7+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace InnoGE\LaravelMsGraphMail\Exceptions;
4+
5+
use Exception;
6+
7+
class ConfigurationMissing extends Exception
8+
{
9+
public static function tenantId(): self
10+
{
11+
return new static('The tenant id is missing from the configuration file.');
12+
}
13+
14+
public static function clientId(): self
15+
{
16+
return new static('The client id is missing from the configuration file.');
17+
}
18+
19+
public static function clientSecret(): self
20+
{
21+
return new static('The client secret is missing from the configuration file.');
22+
}
23+
}

src/LaravelMsGraphMailServiceProvider.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
namespace InnoGE\LaravelMsGraphMail;
44

5+
use Illuminate\Support\Facades\Mail;
6+
use InnoGE\LaravelMsGraphMail\Exceptions\ConfigurationMissing;
7+
use InnoGE\LaravelMsGraphMail\Services\MicrosoftGraphApiService;
58
use Spatie\LaravelPackageTools\Package;
69
use Spatie\LaravelPackageTools\PackageServiceProvider;
710

@@ -18,4 +21,28 @@ public function configurePackage(Package $package): void
1821
->name('laravel-msgraph-mail')
1922
->hasConfigFile();
2023
}
24+
25+
public function boot(): void
26+
{
27+
$this->app->bind(MicrosoftGraphApiService::class, function () {
28+
//throw exceptions when config is missing
29+
throw_unless(filled(config('mail.mailers.microsoft-graph.tenant_id')), ConfigurationMissing::tenantId());
30+
throw_unless(filled(config('mail.mailers.microsoft-graph.client_id')), ConfigurationMissing::clientId());
31+
throw_unless(filled(config('mail.mailers.microsoft-graph.client_secret')), ConfigurationMissing::clientSecret());
32+
33+
return new MicrosoftGraphApiService(
34+
tenantId: config('mail.mailers.microsoft-graph.tenant_id', ''),
35+
clientId: config('mail.mailers.microsoft-graph.client_id', ''),
36+
clientSecret: config('mail.mailers.microsoft-graph.client_secret', ''),
37+
accessTokenTtl: config('mail.mailers.microsoft-graph.access_token_ttl', 3000),
38+
);
39+
});
40+
41+
Mail::extend('microsoft-graph', function (array $config) {
42+
return new MicrosoftGraphTransport(
43+
app()->make(MicrosoftGraphApiService::class),
44+
$config['from']['address']
45+
);
46+
});
47+
}
2148
}

src/MicrosoftGraphTransport.php

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php
2+
3+
namespace InnoGE\LaravelMsGraphMail;
4+
5+
use Illuminate\Http\Client\RequestException;
6+
use Illuminate\Support\Collection;
7+
use InnoGE\LaravelMsGraphMail\Services\MicrosoftGraphApiService;
8+
use Psr\EventDispatcher\EventDispatcherInterface;
9+
use Psr\Log\LoggerInterface;
10+
use Symfony\Component\Mailer\Envelope;
11+
use Symfony\Component\Mailer\SentMessage;
12+
use Symfony\Component\Mailer\Transport\AbstractTransport;
13+
use Symfony\Component\Mime\Address;
14+
use Symfony\Component\Mime\Email;
15+
use Symfony\Component\Mime\MessageConverter;
16+
17+
class MicrosoftGraphTransport extends AbstractTransport
18+
{
19+
public function __construct(protected MicrosoftGraphApiService $microsoftGraphApiService, protected string $from, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
20+
{
21+
parent::__construct($dispatcher, $logger);
22+
}
23+
24+
public function __toString(): string
25+
{
26+
return sprintf('microsoft+graph+api://%s', $this->getEndpoint());
27+
}
28+
29+
/**
30+
* @throws RequestException
31+
*/
32+
protected function doSend(SentMessage $message): void
33+
{
34+
$email = MessageConverter::toEmail($message->getOriginalMessage());
35+
$envelope = $message->getEnvelope();
36+
37+
$payload = [
38+
'message' => [
39+
'subject' => $email->getSubject(),
40+
'body' => [
41+
'contentType' => $email->getHtmlBody() === null ? 'Text' : 'HTML',
42+
'content' => $email->getHtmlBody() ?: $email->getTextBody(),
43+
],
44+
'toRecipients' => $this->transformEmailAddresses($this->getRecipients($email, $envelope)),
45+
'ccRecipients' => $this->transformEmailAddresses(collect($email->getCc())),
46+
'bccRecipients' => $this->transformEmailAddresses(collect($email->getBcc())),
47+
'replyTo' => $this->transformEmailAddresses(collect($email->getReplyTo())),
48+
'sender' => $this->transformEmailAddress($envelope->getSender()),
49+
'attachments' => $this->getAttachments($email),
50+
],
51+
'saveToSentItems' => false,
52+
];
53+
54+
$this->microsoftGraphApiService->sendMail($this->from, $payload);
55+
}
56+
57+
/**
58+
* @param Collection<Address> $recipients
59+
* @return array
60+
*/
61+
protected function transformEmailAddresses(Collection $recipients): array
62+
{
63+
return $recipients
64+
->map(fn (Address $recipient) => $this->transformEmailAddress($recipient))
65+
->toArray();
66+
}
67+
68+
/**
69+
* @param Address $address
70+
* @return array
71+
*/
72+
protected function transformEmailAddress(Address $address): array
73+
{
74+
return [
75+
'emailAddress' => [
76+
'address' => $address->getAddress(),
77+
],
78+
];
79+
}
80+
81+
/**
82+
* @param Email $email
83+
* @param Envelope $envelope
84+
* @return Collection<Address>
85+
*/
86+
protected function getRecipients(Email $email, Envelope $envelope): Collection
87+
{
88+
return collect($envelope->getRecipients())
89+
->filter(fn (Address $address) => ! in_array($address, array_merge($email->getCc(), $email->getBcc()), true));
90+
}
91+
92+
protected function getAttachments(Email $email): array
93+
{
94+
$attachments = [];
95+
foreach ($email->getAttachments() as $attachment) {
96+
$attachments[] = [
97+
'@odata.type' => '#microsoft.graph.fileAttachment',
98+
'name' => $attachment->getName(),
99+
'contentType' => $attachment->getMediaType(),
100+
'contentBytes' => base64_encode($attachment->getBody()),
101+
];
102+
}
103+
104+
return $attachments;
105+
}
106+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace InnoGE\LaravelMsGraphMail\Services;
4+
5+
use Illuminate\Http\Client\PendingRequest;
6+
use Illuminate\Http\Client\RequestException;
7+
use Illuminate\Http\Client\Response;
8+
use Illuminate\Support\Facades\Cache;
9+
use Illuminate\Support\Facades\Http;
10+
11+
class MicrosoftGraphApiService
12+
{
13+
public function __construct(protected readonly string $tenantId,
14+
protected readonly string $clientId,
15+
protected readonly string $clientSecret,
16+
protected readonly int $accessTokenTtl
17+
) {
18+
}
19+
20+
/**
21+
* @throws RequestException
22+
*/
23+
public function sendMail(string $from, array $payload): Response
24+
{
25+
return $this->getBaseRequest()
26+
->post("/users/{$from}/sendMail", $payload)
27+
->throw();
28+
}
29+
30+
protected function getBaseRequest(): PendingRequest
31+
{
32+
return Http::withToken($this->getAccessToken())
33+
->baseUrl('https://graph.microsoft.com/v1.0');
34+
}
35+
36+
protected function getAccessToken(): string
37+
{
38+
return Cache::remember('microsoft-graph-api-access-token', $this->accessTokenTtl, function (): string {
39+
$response = Http::asForm()
40+
->post("https://login.microsoftonline.com/{$this->tenantId}/oauth2/v2.0/token",
41+
[
42+
'grant_type' => 'client_credentials',
43+
'client_id' => $this->clientId,
44+
'client_secret' => $this->clientSecret,
45+
'scope' => 'https://graph.microsoft.com/.default',
46+
]);
47+
48+
$response->throw();
49+
50+
return $response->json('access_token');
51+
});
52+
}
53+
}

tests/MicrosoftGraphTransportTest.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
use Illuminate\Contracts\Queue\Factory;
4+
use Illuminate\Support\Facades\Config;
5+
use Illuminate\Support\Facades\Http;
6+
use Illuminate\Support\Facades\Mail;
7+
8+
it('sends mails with microsoft graph', function () {
9+
Config::set('mail.mailers.microsoft-graph', [
10+
'transport' => 'microsoft-graph',
11+
'client_id' => 'foo_client_id',
12+
'client_secret' => 'foo_client_secret',
13+
'tenant_id' => 'foo_tenant_id',
14+
'from' => [
15+
'address' => '[email protected]',
16+
'name' => 'Taylor Otwell',
17+
],
18+
]);
19+
Http::fake();
20+
21+
Http::assertSent(function ($value) {
22+
dd($value);
23+
});
24+
});

tests/Stubs/DevTest.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
namespace InnoGE\LaravelMsGraphMail\Tests\Stubs;
4+
5+
use Illuminate\Bus\Queueable;
6+
use Illuminate\Mail\Attachment;
7+
use Illuminate\Mail\Mailable;
8+
use Illuminate\Mail\Mailables\Content;
9+
use Illuminate\Mail\Mailables\Envelope;
10+
use Illuminate\Queue\SerializesModels;
11+
12+
class DevTest extends Mailable
13+
{
14+
use Queueable, SerializesModels;
15+
16+
/**
17+
* Create a new message instance.
18+
*
19+
* @return void
20+
*/
21+
public function __construct()
22+
{
23+
//
24+
}
25+
26+
/**
27+
* Get the message envelope.
28+
*
29+
* @return \Illuminate\Mail\Mailables\Envelope
30+
*/
31+
public function envelope()
32+
{
33+
return new Envelope(
34+
subject: 'Dev Test',
35+
);
36+
}
37+
38+
/**
39+
* Get the message content definition.
40+
*
41+
* @return \Illuminate\Mail\Mailables\Content
42+
*/
43+
public function content()
44+
{
45+
return new Content(
46+
html: '<b>Test</b>',
47+
);
48+
}
49+
50+
/**
51+
* Get the attachments for the message.
52+
*
53+
* @return array
54+
*/
55+
public function attachments(): array
56+
{
57+
return [
58+
Attachment::fromPath(resource_path('pdf-test.pdf')),
59+
Attachment::fromPath(resource_path('pdf-test2.pdf')),
60+
];
61+
}
62+
}

0 commit comments

Comments
 (0)