|
<?php |
|
|
|
namespace App\Mail\Transport; |
|
|
|
use GuzzleHttp\ClientInterface; |
|
use Illuminate\Mail\Transport\Transport; |
|
use Illuminate\Support\Str; |
|
use JetBrains\PhpStorm\ArrayShape; |
|
use Psr\Http\Client\ClientExceptionInterface; |
|
use Psr\Log\LoggerInterface; |
|
use Swift_Mime_SimpleMessage; |
|
use Swift_Mime_SimpleMimeEntity; |
|
use Swift_TransportException; |
|
|
|
class SwisscomApiEmailMessagingTransport extends Transport |
|
{ |
|
/** |
|
* Guzzle client instance. |
|
*/ |
|
protected ClientInterface $client; |
|
/** |
|
* @var string Client ID for API Key |
|
*/ |
|
private string $clientId; |
|
private string $endpoint; |
|
private LoggerInterface $logger; |
|
private ?array $forceFrom = null; |
|
private array $clientIdMapByEmail; |
|
|
|
/** |
|
* Create a new SwisscomApiEmailMessagingTransport transport instance. |
|
*/ |
|
public function __construct(ClientInterface $client, array $clientIdMapByEmail, LoggerInterface $logger, string $endpoint = null) |
|
{ |
|
$this->client = $client; |
|
$this->endpoint = $endpoint === null ? 'https://api.swisscom.com' : $endpoint; |
|
$this->logger = $logger; |
|
$this->clientIdMapByEmail = $clientIdMapByEmail; |
|
} |
|
|
|
/** |
|
* Send the email |
|
*/ |
|
public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null) |
|
{ |
|
$data = [ |
|
"headers" => $this->getHeaders($message), |
|
"json" => $this->payload($message), |
|
'http_errors' => false, |
|
]; |
|
|
|
if ($this->forceFrom !== null) { |
|
$message->setFrom($this->forceFrom); |
|
} |
|
|
|
$this->beforeSendPerformed($message); |
|
|
|
try { |
|
$this->logger->debug("Send email:" . json_encode($data)); |
|
// https://digital.swisscom.com/products/email-messaging/documentation/free?v=2 |
|
$response = $this->client->request("POST", $this->endpoint . "/messaging/email", $data); |
|
} catch (ClientExceptionInterface $e) { |
|
$this->logger->warning("Sending email failure", ["exception" => $e]); |
|
throw new Swift_TransportException(sprintf('Sending mail via Swisscom-API failed. Code %s', $e->getCode()), 500, $e); |
|
} |
|
|
|
if ($response->getStatusCode() !== 200) { |
|
$content = $response->getBody()->getContents(); |
|
$this->logger->debug(sprintf("Unable to send email. Code %s", $response->getStatusCode()), |
|
[ |
|
"body" => $content, |
|
] |
|
); |
|
throw new Swift_TransportException(sprintf('Sending mail via Swisscom-API failed. Code %s', $response->getStatusCode())); |
|
} |
|
|
|
return $this->getRecipientCount($message); |
|
} |
|
|
|
/** |
|
* Get the number of recipients for a message |
|
*/ |
|
protected function getRecipientCount(Swift_Mime_SimpleMessage $message): int |
|
{ |
|
return count(array_merge( |
|
(array)$message->getTo(), |
|
(array)$message->getCc(), |
|
(array)$message->getBcc() |
|
)); |
|
} |
|
|
|
/** |
|
* Format email to "name <email>;name2 <email2>;email3" |
|
*/ |
|
protected function getEmailsString(array $contacts = null): ?string |
|
{ |
|
if ($contacts === null) { |
|
return null; |
|
} |
|
return collect($contacts)->map(function ($display, $address) { |
|
return $display && !preg_match("/^[0-9]+$/", $display) ? $display . " <$address>" : $address; |
|
})->values()->implode(';'); |
|
} |
|
|
|
/** |
|
* Gets MIME parts that match the message type. |
|
* Exclude parts of type \Swift_Mime_Attachment as those are handled later. |
|
* Inspired by https://github.com/wildbit/swiftmailer-postmark/blob/master/src/Postmark/Transport.php |
|
*/ |
|
protected function getMIMEPart(Swift_Mime_SimpleMessage $message, $mimeType): ?Swift_Mime_SimpleMimeEntity |
|
{ |
|
foreach ($message->getChildren() as $part) { |
|
if (str_starts_with($part->getContentType(), $mimeType) && !($part instanceof \Swift_Mime_Attachment)) { |
|
return $part; |
|
} |
|
} |
|
return null; |
|
} |
|
|
|
/** |
|
* Applies the message parts and attachments |
|
* into the API Payload. |
|
* Inspired by https://github.com/wildbit/swiftmailer-postmark/blob/master/src/Postmark/Transport.php |
|
*/ |
|
protected function processMessageParts(array &$payload, Swift_Mime_SimpleMessage $message): void |
|
{ |
|
//Get the primary message. |
|
switch ($message->getContentType()) { |
|
case 'text/html': |
|
case 'multipart/alternative': |
|
case 'multipart/mixed': |
|
$payload['html'] = $message->getBody(); |
|
break; |
|
default: |
|
$payload['text'] = $message->getBody(); |
|
break; |
|
} |
|
|
|
// Provide an alternate view from the secondary parts. |
|
if ($plain = $this->getMIMEPart($message, 'text/plain')) { |
|
$payload['text'] = $plain->getBody(); |
|
} |
|
if (!array_key_exists('html', $payload) && $html = $this->getMIMEPart($message, 'text/html')) { |
|
$payload['html'] = $html->getBody(); |
|
} |
|
|
|
// Process attachments |
|
if ($message->getChildren()) { |
|
$payload['attachments'] = array(); |
|
foreach ($message->getChildren() as $attachment) { |
|
if (is_object($attachment) and $attachment instanceof \Swift_Mime_Attachment) { |
|
$a = array( |
|
'fileName' => $attachment->getFilename(), |
|
'base64FileContent' => base64_encode($attachment->getBody()), |
|
// $attachment->getContentType() not supported by API. |
|
); |
|
$payload['attachments'][] = $a; |
|
} |
|
} |
|
} |
|
} |
|
|
|
private function payload(Swift_Mime_SimpleMessage $message): array |
|
{ |
|
$requiredPayload = [ |
|
"to" => $this->getEmailsString($message->getTo()), |
|
"subject" => $message->getSubject(), |
|
"text" => "", // Text is mandatory, but may be overridden by "processMessageParts" |
|
]; |
|
$optionalPayload = [ |
|
"replyTo" => $message->getReplyTo(), |
|
"cc" => $this->getEmailsString($message->getCc()), |
|
"bcc" => $this->getEmailsString($message->getBcc()), |
|
]; |
|
// Fill the keys: text, html, attachments |
|
$this->processMessageParts($optionalPayload, $message); |
|
|
|
$payload = array_filter($optionalPayload) + $requiredPayload; |
|
if ($payload["html"] ?? null) { |
|
$payload["html"] = base64_encode($payload["html"]); |
|
} |
|
return $payload; |
|
} |
|
|
|
#[ArrayShape(["client_id" => "string", "SCS-Request-ID" => "string", "SCS-Version" => "int", "Content-Type" => "string"])] |
|
private function getHeaders(Swift_Mime_SimpleMessage $message): array |
|
{ |
|
return [ |
|
"client_id" => $this->getClientId($message), |
|
"SCS-Request-ID" => Str::random(), |
|
"SCS-Version" => 2, |
|
"Content-Type" => 'application/json; charset=utf-8', |
|
]; |
|
} |
|
|
|
/** |
|
* @param array|null $forceFrom |
|
*/ |
|
public function setForceFrom(?array $forceFrom): void |
|
{ |
|
$this->forceFrom = $forceFrom; |
|
} |
|
|
|
private function getClientId(Swift_Mime_SimpleMessage $message): string |
|
{ |
|
$from = (string)array_key_first((array)$message->getFrom()); |
|
if (!in_array($from, array_keys($this->clientIdMapByEmail))) { |
|
throw new \RuntimeException("\"api_key_map\" is not configured for email " . $from); |
|
} |
|
|
|
return $this->clientIdMapByEmail[$from]; |
|
} |
|
|
|
} |