Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
pmdevelopment committed May 27, 2022
0 parents commit 90f8fda
Show file tree
Hide file tree
Showing 6 changed files with 265 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
vendor/
composer.lock
phpunit.xml
36 changes: 36 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Symfony Mailer for Microsoft

Use this bridge to send Emails with `symfony/mailer` and Microsoft Graph API. Supported Features:



| Field | Supported |
|-------------|-----------|
| Subject ||
| Return-Path ||
| Sender ||
| From ||
| Reply-To ||
| To ||
| Cc ||
| Bcc ||
| Priority ||
| Text ||
| HTML ||
| Attachments ||


## Configuration

Extend your `services.yaml`configuration

PMDevelopment\Mailer\Bridge\Microsoft\Transport\GraphTransportFactory:
tags:
- { name: mailer.transport_factory }


Add required Parameters to `.env`

MAILER_DSN=microsoft+graph://clientId:clientSecret@tenantId


46 changes: 46 additions & 0 deletions Transport/GraphClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace PMDevelopment\Mailer\Bridge\Microsoft\Transport;

use Microsoft\Graph\Graph;
use Symfony\Component\Mailer\Transport\Dsn;

class GraphClient
{
private Graph $graph;

private string $clientId;
private string $clientSecret;
private string $tenantId;

public function __construct(string $accessToken, Dsn $dsn)
{
$this->graph = new Graph();
$this->graph->setAccessToken($accessToken);

$this->clientId = $dsn->getUser();
$this->clientSecret = $dsn->getPassword();
$this->tenantId = $dsn->getHost();
}

public function getGraph(): Graph
{
return $this->graph;
}

public function getClientId(): string
{
return $this->clientId;
}

public function getClientSecret(): string
{
return $this->clientSecret;
}

public function getTenantId(): string
{
return $this->tenantId;
}

}
91 changes: 91 additions & 0 deletions Transport/GraphSendMailTransport.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

namespace PMDevelopment\Mailer\Bridge\Microsoft\Transport;

use GuzzleHttp\Client;
use Microsoft\Graph\Graph;
use Psr\Log\LoggerInterface;
use RuntimeException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mailer\Exception\HttpTransportException;
use Symfony\Component\Mailer\Exception\RuntimeException as MailerRuntimeException;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mailer\Transport\AbstractTransport;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\MessageConverter;
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Throwable;

class GraphSendMailTransport extends AbstractTransport
{
private GraphClient $client;

public function __construct(GraphClient $client, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
{
$this->client = $client;

parent::__construct($dispatcher, $logger);
}


protected function doSend(SentMessage $message): void
{
$graph = $this->client->getGraph();

try {
$email = MessageConverter::toEmail($message->getOriginalMessage());
} catch (Throwable $e) {
throw new MailerRuntimeException(sprintf('Unable to send message with the "%s" transport: ', __CLASS__) . $e->getMessage(), 0, $e);
}

$mailBody = [
'Message' => [
'attachments' => [],
'bccRecipients' => $this->getArrayFromAddresses($email->getBcc()),
'body' => [
'contentType' => 'text',
'content' => $email->getTextBody(),
],
'ccRecipients' => $this->getArrayFromAddresses($email->getCc()),
'subject' => $email->getSubject(),
'toRecipients' => $this->getArrayFromAddresses($email->getTo()),
],
];

foreach ($email->getAttachments() as $attachment) {
$mailBody['Message']['attachments'][] = [
'@odata.type' => '#microsoft.graph.fileAttachment',
'contentType' => 'text/plain',
'contentBytes' => base64_encode($attachment->getBody()),
'name' => $attachment->getPreparedHeaders()->getHeaderParameter('Content-Disposition', 'name'),
];
}


$result = $graph->createRequest(Request::METHOD_POST, sprintf('/users/%s/sendMail', $message->getEnvelope()->getSender()->getAddress()))
->attachBody($mailBody)
->execute();

if (Response::HTTP_ACCEPTED !== $result->getStatus()) {
throw new MailerRuntimeException(sprintf('Sending mail failed with status %d', $result->getStatus()));
}
}

public function __toString()
{
return sprintf('microsoft+graph://%s:%s@%s', $this->client->getClientId(), $this->client->getClientSecret(), $this->client->getTenantId());
}

private function getArrayFromAddresses(array $addresses)
{
return array_map(function (Address $address) {
return [
'emailAddress' => [
'address' => $address->getAddress(),
],
];
}, $addresses);
}
}
56 changes: 56 additions & 0 deletions Transport/GraphTransportFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace PMDevelopment\Mailer\Bridge\Microsoft\Transport;

use GuzzleHttp\Client;
use Microsoft\Graph\Graph;
use RuntimeException;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mailer\Exception\IncompleteDsnException;
use Symfony\Component\Mailer\Exception\UnsupportedSchemeException;
use Symfony\Component\Mailer\Transport\AbstractTransportFactory;
use Symfony\Component\Mailer\Transport\Dsn;
use Symfony\Component\Mailer\Transport\TransportInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

class GraphTransportFactory extends AbstractTransportFactory
{
public function __construct(EventDispatcherInterface $eventDispatcher, LoggerInterface $logger = null)
{
parent::__construct($eventDispatcher, null, $logger);
}


public function create(Dsn $dsn): TransportInterface
{
$client = new Client();

$loginUrl = sprintf('https://login.microsoftonline.com/%s/oauth2/v2.0/token', $dsn->getHost());

$loginFormParameters = [
'client_id' => $dsn->getUser(),
'client_secret' => $dsn->getPassword(),
'grant_type' => 'client_credentials',
'scope' => 'https://graph.microsoft.com/.default',
];

$request = $client->post($loginUrl, [
'form_params' => $loginFormParameters,
]);

$response = json_decode($request->getBody()->getContents(), true);
if (false === array_key_exists('access_token', $response)) {
throw new RuntimeException('Key "access_token" not found in %s', implode(',', array_keys($response)));
}

return new GraphSendMailTransport(new GraphClient($response['access_token'], $dsn), $this->dispatcher, $this->logger);
}


protected function getSupportedSchemes(): array
{
return ['microsoft+graph'];
}


}
33 changes: 33 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "pmdevelopment/microsoft-symfony-mailer",
"description": "Microsoft Graph API bridge for symfony mailer",
"keywords": [],
"homepage": "https://pmdevelopment.de",
"license": "MIT",
"authors": [
{
"name": "Sven Joder",
"email": "[email protected]"
}
],
"require": {
"php": "^7.4|^8.1",
"guzzlehttp/guzzle": "^6.0|^7.0",
"microsoft/microsoft-graph": "^1.66",
"symfony/event-dispatcher-contracts": "*",
"symfony/mailer": "^4.4.21|^5.2.6|^6.0",
"symfony/symfony": "^4.4|^5.4|^6.0"
},
"require-dev": {
"symfony/http-client": "^4.4|^5.0|^6.0"
},
"autoload": {
"psr-4": {
"PMDevelopment\\Mailer\\Bridge\\Microsoft\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev"
}

0 comments on commit 90f8fda

Please sign in to comment.