src/Service/Mail/GraphApiTransport.php line 27

Open in your IDE?
  1. <?php
  2. namespace App\Service\Mail;
  3. use Psr\EventDispatcher\EventDispatcherInterface;
  4. use Psr\Log\LoggerInterface;
  5. use Symfony\Component\Mailer\Envelope;
  6. use Symfony\Component\Mailer\Exception\HttpTransportException;
  7. use Symfony\Component\Mailer\SentMessage;
  8. use Symfony\Component\Mailer\Transport\AbstractApiTransport;
  9. use Symfony\Component\Mime\Address;
  10. use Symfony\Component\Mime\Email;
  11. use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
  12. use Symfony\Contracts\HttpClient\HttpClientInterface;
  13. use Symfony\Contracts\HttpClient\ResponseInterface;
  14. /**
  15. * @author Sjoerd Adema <vitrus@gmail.com>
  16. */
  17. class GraphApiTransport extends AbstractApiTransport
  18. {
  19. private string $graphTentantId;
  20. private string $graphClientId;
  21. private string $graphClientSecret;
  22. private ?string $accessToken = null;
  23. public function __construct(
  24. string $graphTentantId,
  25. string $graphClientId,
  26. string $graphClientSecret,
  27. HttpClientInterface $client = null,
  28. EventDispatcherInterface $dispatcher = null,
  29. LoggerInterface $logger = null
  30. ) {
  31. $this->graphTentantId = $graphTentantId;
  32. $this->graphClientId = $graphClientId;
  33. $this->graphClientSecret = $graphClientSecret;
  34. parent::__construct($client, $dispatcher, $logger);
  35. }
  36. public function __toString(): string
  37. {
  38. return sprintf('microsoft-graph-api://%s:{SECRET}@%s', $this->graphClientId, $this->graphTentantId);
  39. }
  40. protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface
  41. {
  42. if (null === $this->accessToken) {
  43. $this->requestAccessToken();
  44. }
  45. $response = $this->client->request('POST', $this->getEndpoint($sentMessage), [
  46. 'json' => $this->normalizeEmail($email, $envelope),
  47. 'auth_bearer' => $this->accessToken,
  48. ]);
  49. try {
  50. $statusCode = $response->getStatusCode();
  51. } catch (TransportExceptionInterface $e) {
  52. throw new HttpTransportException('Could not reach Microsoft Graph API.', $response, 0, $e);
  53. }
  54. if (202 !== $statusCode) {
  55. throw new HttpTransportException('Unable to sent e-mail using Graph API', $response);
  56. }
  57. return $response;
  58. }
  59. private function normalizeEmail(Email $email, Envelope $envelope): array
  60. {
  61. $payload = [
  62. 'message' => [
  63. 'subject' => $email->getSubject(),
  64. 'toRecipients' => $this->normalizeAddresses($envelope->getRecipients() ?? $email->getTo()),
  65. 'ccRecipients' => $this->normalizeAddresses($envelope->getRecipients() ? [] : $email->getCc()),
  66. 'bccRecipients' => $this->normalizeAddresses($envelope->getRecipients() ? [] : $email->getBcc()),
  67. 'replyTo' => $this->normalizeAddresses($email->getReplyTo()),
  68. 'body' => $this->normalizeBody($email),
  69. 'attachments' => $this->normalizeAttachments($email),
  70. ],
  71. 'saveToSentItems' => $this->normalizeSaveToSentItems($email),
  72. ];
  73. return $payload;
  74. }
  75. private function normalizeAddress(Address $address): array
  76. {
  77. $addressArray = [
  78. 'emailAddress' => [
  79. 'address' => $address->getAddress(),
  80. ],
  81. ];
  82. if ($address->getName()) {
  83. $addressArray['emailAddress']['name'] = $address->getName();
  84. }
  85. return $addressArray;
  86. }
  87. /**
  88. * @param Address[] $addresses
  89. */
  90. private function normalizeAddresses(array $addresses): array
  91. {
  92. $addressesArray = [];
  93. foreach ($addresses as $address) {
  94. $addressesArray[] = $this->normalizeAddress($address);
  95. }
  96. return $addressesArray;
  97. }
  98. private function normalizeBody(Email $email): array
  99. {
  100. // prefer html body
  101. if (null !== $htmlContent = $email->getHtmlBody()) {
  102. return [
  103. 'contentType' => 'html',
  104. 'content' => $htmlContent,
  105. ];
  106. }
  107. // fallback on textBody
  108. if (null !== $textContent = $email->getTextBody()) {
  109. return [
  110. 'contentType' => 'text',
  111. 'content' => $textContent,
  112. ];
  113. }
  114. return [];
  115. }
  116. private function normalizeAttachments(Email $email): array
  117. {
  118. $attachments = [];
  119. foreach ($email->getAttachments() as $attachment) {
  120. $headers = $attachment->getPreparedHeaders();
  121. $filename = $headers->getHeaderParameter('Content-Disposition', 'filename');
  122. $attachments[] = [
  123. '@odata.type' => '#microsoft.graph.fileAttachment',
  124. 'contentType' => $headers->get('Content-Type')->getBody(),
  125. 'contentBytes' => base64_encode($attachment->getBody()),
  126. 'name' => $filename,
  127. ];
  128. }
  129. return $attachments;
  130. }
  131. private function normalizeSaveToSentItems($email): bool
  132. {
  133. $saveToSentItems = true;
  134. $saveToSentHeader = $email->getHeaders()->get('X-Save-To-Sent-Items');
  135. if ($saveToSentHeader !== null) {
  136. if (strtolower($saveToSentHeader->getBodyAsString()) === 'false') {
  137. $saveToSentItems = false;
  138. }
  139. }
  140. return $saveToSentItems;
  141. }
  142. private function requestAccessToken(): void
  143. {
  144. $url = 'https://login.microsoftonline.com/' . $this->graphTentantId . '/oauth2/v2.0/token';
  145. $response = $this->client->request('POST', $url, [
  146. 'body' => [
  147. 'client_id' => $this->graphClientId,
  148. 'client_secret' => $this->graphClientSecret,
  149. 'scope' => 'https://graph.microsoft.com/.default',
  150. 'grant_type' => 'client_credentials',
  151. ],
  152. ]);
  153. $token = json_decode($response->getContent(), null, 512, JSON_THROW_ON_ERROR);
  154. $this->accessToken = $token->access_token;
  155. }
  156. private function getEndpoint(SentMessage $sentMessage): string
  157. {
  158. $senderAddress = $sentMessage->getEnvelope()->getSender()->getAddress();
  159. return sprintf('https://graph.microsoft.com/v1.0/users/%s/sendMail', $senderAddress);
  160. }
  161. }