diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index f0cb4dd96..8e217b25c 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -54,21 +54,38 @@ App::post('/v1/messaging/providers/mailgun') ->label('sdk.response.model', Response::MODEL_PROVIDER) ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('name', '', new Text(128), 'Provider name.') - ->param('from', '', new Email(), 'Sender email address.', true) + ->param('fromName', '', new Text(128), 'Sender Name.') + ->param('fromEmail', '', new Email(), 'Sender email address.') ->param('apiKey', '', new Text(0), 'Mailgun API Key.', true) ->param('domain', '', new Text(0), 'Mailgun Domain.', true) ->param('isEuRegion', null, new Boolean(), 'Set as EU region.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) + ->param('replyToName', '', new Text(128), 'Name set in the Reply To field for the mail. Default value is Sender Name. Reply to name must have reply to email as well.', true) + ->param('replyToEmail', '', new Text(128), 'Email set in the Reply To field for the mail. Default value is Sender Email. Reply to email must have reply to name as well.', true) + ->param('cc', [], new ArrayList(new Email()), 'Array of email addresses to be added as CC.', true) + ->param('bcc', [], new ArrayList(new Email()), 'Array of email addresses to be added as BCC.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, string $from, string $apiKey, string $domain, ?bool $isEuRegion, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, string $fromName, string $fromEmail, string $apiKey, string $domain, ?bool $isEuRegion, ?bool $enabled, string $replyToName, string $replyToEmail, array $cc, array $bcc, Event $queueForEvents, Database $dbForProject, Response $response) { $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; - $options = []; + $options = [ + 'fromName' => $fromName, + 'fromEmail' => $fromEmail, + ]; - if (!empty($from)) { - $options ['from'] = $from; + if (!empty($replyToName) && !empty($replyToEmail)) { + $options['replyToName'] = $replyToName; + $options['replyToEmail'] = $replyToEmail; + } + + if (!empty($cc)) { + $options['cc'] = $cc; + } + + if (!empty($bcc)) { + $options['bcc'] = $bcc; } $credentials = []; @@ -137,19 +154,36 @@ App::post('/v1/messaging/providers/sendgrid') ->label('sdk.response.model', Response::MODEL_PROVIDER) ->param('providerId', '', new CustomId(), 'Provider ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') ->param('name', '', new Text(128), 'Provider name.') - ->param('from', '', new Email(), 'Sender email address.', true) + ->param('fromName', '', new Text(128), 'Sender Name.') + ->param('fromEmail', '', new Email(), 'Sender email address.') ->param('apiKey', '', new Text(0), 'Sendgrid API key.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) + ->param('replyToName', '', new Text(128), 'Name set in the Reply To field for the mail. Default value is Sender Name.', true) + ->param('replyToEmail', '', new Text(128), 'Email set in the Reply To field for the mail. Default value is Sender Email.', true) + ->param('cc', [], new ArrayList(new Email()), 'Array of email addresses to be added as CC.', true) + ->param('bcc', [], new ArrayList(new Email()), 'Array of email addresses to be added as BCC.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, string $from, string $apiKey, ?bool $enabled, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, string $fromName, string $fromEmail, string $apiKey, ?bool $enabled, string $replyToName, string $replyToEmail, array $cc, array $bcc, Event $queueForEvents, Database $dbForProject, Response $response) { $providerId = $providerId == 'unique()' ? ID::unique() : $providerId; - $options = []; + $options = [ + 'fromName' => $fromName, + 'fromEmail' => $fromEmail, + ]; - if (!empty($from)) { - $options ['from'] = $from; + if (!empty($replyToName) && !empty($replyToEmail)) { + $options['replyToName'] = $replyToName; + $options['replyToEmail'] = $replyToEmail; + } + + if (!empty($cc)) { + $options['cc'] = $cc; + } + + if (!empty($bcc)) { + $options['bcc'] = $bcc; } $credentials = []; @@ -890,13 +924,18 @@ App::patch('/v1/messaging/providers/mailgun/:providerId') ->param('name', '', new Text(128), 'Provider name.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) ->param('isEuRegion', null, new Boolean(), 'Set as EU region.', true) - ->param('from', '', new Email(), 'Sender email address.', true) + ->param('fromName', '', new Text(128), 'Sender Name.', true) + ->param('fromEmail', '', new Email(), 'Sender email address.', true) + ->param('replyToName', '', new Text(128), 'Name set in the Reply To field for the mail. Default value is Sender Name.', true) + ->param('replyToEmail', '', new Text(128), 'Email set in the Reply To field for the mail. Default value is Sender Email.', true) + ->param('cc', [], new ArrayList(new Email()), 'Array of email addresses to be added as CC.', true) + ->param('bcc', [], new ArrayList(new Email()), 'Array of email addresses to be added as BCC.', true) ->param('apiKey', '', new Text(0), 'Mailgun API Key.', true) ->param('domain', '', new Text(0), 'Mailgun Domain.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, ?bool $enabled, ?bool $isEuRegion, string $from, string $apiKey, string $domain, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, ?bool $enabled, ?bool $isEuRegion, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, array $cc, array $bcc, string $apiKey, string $domain, Event $queueForEvents, Database $dbForProject, Response $response) { $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { @@ -912,12 +951,34 @@ App::patch('/v1/messaging/providers/mailgun/:providerId') $provider->setAttribute('name', $name); } - if (!empty($from)) { - $provider->setAttribute('options', [ - 'from' => $from, - ]); + $options = $provider->getAttribute('options'); + + if (!empty($fromName)) { + $options['fromName'] = $fromName; } + if (!empty($fromEmail)) { + $options['fromEmail'] = $fromEmail; + } + + if (!empty($replyToName)) { + $options['replyToName'] = $replyToName; + } + + if (!empty($replyToEmail)) { + $options['replyToEmail'] = $replyToEmail; + } + + if (\count($cc) > 0) { + $options['cc'] = $cc; + } + + if (\count($bcc) > 0) { + $options['bcc'] = $bcc; + } + + $provider->setAttribute('options', $options); + $credentials = $provider->getAttribute('credentials'); if ($isEuRegion === true || $isEuRegion === false) { @@ -976,11 +1037,16 @@ App::patch('/v1/messaging/providers/sendgrid/:providerId') ->param('name', '', new Text(128), 'Provider name.', true) ->param('enabled', null, new Boolean(), 'Set as enabled.', true) ->param('apiKey', '', new Text(0), 'Sendgrid API key.', true) - ->param('from', '', new Email(), 'Sender email address.', true) + ->param('fromName', '', new Text(128), 'Sender Name.', true) + ->param('fromEmail', '', new Email(), 'Sender email address.', true) + ->param('replyToName', '', new Text(128), 'Name set in the Reply To field for the mail. Default value is Sender Name.', true) + ->param('replyToEmail', '', new Text(128), 'Email set in the Reply To field for the mail. Default value is Sender Email.', true) + ->param('cc', [], new ArrayList(new Email()), 'Array of email addresses to be added as CC.', true) + ->param('bcc', [], new ArrayList(new Email()), 'Array of email addresses to be added as BCC.', true) ->inject('queueForEvents') ->inject('dbForProject') ->inject('response') - ->action(function (string $providerId, string $name, ?bool $enabled, string $apiKey, string $from, Event $queueForEvents, Database $dbForProject, Response $response) { + ->action(function (string $providerId, string $name, ?bool $enabled, string $apiKey, string $fromName, string $fromEmail, string $replyToName, string $replyToEmail, array $cc, array $bcc, Event $queueForEvents, Database $dbForProject, Response $response) { $provider = $dbForProject->getDocument('providers', $providerId); if ($provider->isEmpty()) { @@ -996,12 +1062,34 @@ App::patch('/v1/messaging/providers/sendgrid/:providerId') $provider->setAttribute('name', $name); } - if (!empty($from)) { - $provider->setAttribute('options', [ - 'from' => $from, - ]); + $options = $provider->getAttribute('options'); + + if (!empty($fromName)) { + $options['fromName'] = $fromName; } + if (!empty($fromEmail)) { + $options['fromEmail'] = $fromEmail; + } + + if (!empty($replyToName)) { + $options['replyToName'] = $replyToName; + } + + if (!empty($replyToEmail)) { + $options['replyToEmail'] = $replyToEmail; + } + + if (\count($cc) > 0) { + $options['cc'] = $cc; + } + + if (\count($bcc) > 0) { + $options['bcc'] = $bcc; + } + + $provider->setAttribute('options', $options); + if (!empty($apiKey)) { $provider->setAttribute('credentials', [ 'apiKey' => $apiKey, diff --git a/composer.json b/composer.json index a24feca83..9dbb59a94 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,7 @@ "utopia-php/image": "0.5.*", "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.3.*", - "utopia-php/messaging": "0.2.*", + "utopia-php/messaging": "0.6.*", "utopia-php/migration": "0.3.*", "utopia-php/orchestration": "0.9.*", "utopia-php/platform": "0.5.*", diff --git a/composer.lock b/composer.lock index 16f44a635..b865d57a8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "69bc2e21a65b78344393706b39d789b4", + "content-hash": "f5b11b64b696e0a17222fdaa81350c4c", "packages": [ { "name": "adhocore/jwt", @@ -402,16 +402,16 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.8.0", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9" + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/1110f66a6530a40fe7aea0378fe608ee2b2248f9", - "reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104", + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104", "shasum": "" }, "require": { @@ -426,11 +426,11 @@ "psr/http-client-implementation": "1.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.1", + "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", "php-http/message-factory": "^1.1", - "phpunit/phpunit": "^8.5.29 || ^9.5.23", + "phpunit/phpunit": "^8.5.36 || ^9.6.15", "psr/log": "^1.1 || ^2.0 || ^3.0" }, "suggest": { @@ -508,7 +508,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.8.0" + "source": "https://github.com/guzzle/guzzle/tree/7.8.1" }, "funding": [ { @@ -524,28 +524,28 @@ "type": "tidelift" } ], - "time": "2023-08-27T10:20:53+00:00" + "time": "2023-12-03T20:35:24+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.0.1", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "111166291a0f8130081195ac4556a5587d7f1b5d" + "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/111166291a0f8130081195ac4556a5587d7f1b5d", - "reference": "111166291a0f8130081195ac4556a5587d7f1b5d", + "url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223", + "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.1", - "phpunit/phpunit": "^8.5.29 || ^9.5.23" + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.36 || ^9.6.15" }, "type": "library", "extra": { @@ -591,7 +591,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.1" + "source": "https://github.com/guzzle/promises/tree/2.0.2" }, "funding": [ { @@ -607,20 +607,20 @@ "type": "tidelift" } ], - "time": "2023-08-03T15:11:55+00:00" + "time": "2023-12-03T20:19:20+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.6.1", + "version": "2.6.2", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727" + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/be45764272e8873c72dbe3d2edcfdfcc3bc9f727", - "reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221", + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221", "shasum": "" }, "require": { @@ -634,9 +634,9 @@ "psr/http-message-implementation": "1.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.1", + "bamarni/composer-bin-plugin": "^1.8.2", "http-interop/http-factory-tests": "^0.9", - "phpunit/phpunit": "^8.5.29 || ^9.5.23" + "phpunit/phpunit": "^8.5.36 || ^9.6.15" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" @@ -707,7 +707,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.6.1" + "source": "https://github.com/guzzle/psr7/tree/2.6.2" }, "funding": [ { @@ -723,7 +723,7 @@ "type": "tidelift" } ], - "time": "2023-08-27T10:13:57+00:00" + "time": "2023-12-03T20:05:35+00:00" }, { "name": "influxdb/influxdb-php", @@ -2270,16 +2270,16 @@ }, { "name": "utopia-php/messaging", - "version": "0.2.0", + "version": "0.6.0", "source": { "type": "git", "url": "https://github.com/utopia-php/messaging.git", - "reference": "2d0f474a106bb1da285f85e105c29b46085d3a43" + "reference": "a3ebf9d0714e760cfa36f9c5de75c7c0f31c4024" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/messaging/zipball/2d0f474a106bb1da285f85e105c29b46085d3a43", - "reference": "2d0f474a106bb1da285f85e105c29b46085d3a43", + "url": "https://api.github.com/repos/utopia-php/messaging/zipball/a3ebf9d0714e760cfa36f9c5de75c7c0f31c4024", + "reference": "a3ebf9d0714e760cfa36f9c5de75c7c0f31c4024", "shasum": "" }, "require": { @@ -2287,9 +2287,11 @@ "php": ">=8.0.0" }, "require-dev": { - "laravel/pint": "^1.2", + "ext-openssl": "*", + "laravel/pint": "1.13.*", "phpmailer/phpmailer": "6.8.*", - "phpunit/phpunit": "9.6.*" + "phpstan/phpstan": "1.10.*", + "phpunit/phpunit": "9.6.10" }, "type": "library", "autoload": { @@ -2312,9 +2314,9 @@ ], "support": { "issues": "https://github.com/utopia-php/messaging/issues", - "source": "https://github.com/utopia-php/messaging/tree/0.2.0" + "source": "https://github.com/utopia-php/messaging/tree/0.6.0" }, - "time": "2023-09-14T20:48:42+00:00" + "time": "2023-12-05T11:08:43+00:00" }, { "name": "utopia-php/migration", @@ -5823,5 +5825,5 @@ "platform-overrides": { "php": "8.0" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index 8f945e947..345da552a 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -13,22 +13,23 @@ use Utopia\Database\Database; use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Query; -use Utopia\Messaging\Adapters\SMS as SMSAdapter; -use Utopia\Messaging\Adapters\SMS\Mock; -use Utopia\Messaging\Adapters\SMS\Msg91; -use Utopia\Messaging\Adapters\SMS\Telesign; -use Utopia\Messaging\Adapters\SMS\Textmagic; -use Utopia\Messaging\Adapters\SMS\Twilio; -use Utopia\Messaging\Adapters\SMS\Vonage; -use Utopia\Messaging\Adapters\Push as PushAdapter; -use Utopia\Messaging\Adapters\Push\APNS; -use Utopia\Messaging\Adapters\Push\FCM; -use Utopia\Messaging\Adapters\Email as EmailAdapter; -use Utopia\Messaging\Adapters\Email\Mailgun; -use Utopia\Messaging\Adapters\Email\SendGrid; +use Utopia\Messaging\Adapter\Email as EmailAdapter; +use Utopia\Messaging\Adapter\Email\Mailgun; +use Utopia\Messaging\Adapter\Email\Sendgrid; +use Utopia\Messaging\Adapter\Push as PushAdapter; +use Utopia\Messaging\Adapter\Push\APNS; +use Utopia\Messaging\Adapter\Push\FCM; +use Utopia\Messaging\Adapter\SMS as SMSAdapter; +use Utopia\Messaging\Adapter\SMS\Mock; +use Utopia\Messaging\Adapter\SMS\Msg91; +use Utopia\Messaging\Adapter\SMS\Telesign; +use Utopia\Messaging\Adapter\SMS\Textmagic; +use Utopia\Messaging\Adapter\SMS\Twilio; +use Utopia\Messaging\Adapter\SMS\Vonage; use Utopia\Messaging\Messages\Email; use Utopia\Messaging\Messages\Push; use Utopia\Messaging\Messages\SMS; +use Utopia\Messaging\Response; use function Swoole\Coroutine\batch; @@ -169,8 +170,8 @@ class Messaging extends Action $batches = \array_chunk($identifiers, $maxBatchSize); $batchIndex = 0; - $results = batch(\array_map(function ($batch) use ($message, $provider, $adapter, $batchIndex) { - return function () use ($batch, $message, $provider, $adapter, $batchIndex) { + $results = batch(\array_map(function ($batch) use ($message, $provider, $adapter, $batchIndex, $dbForProject) { + return function () use ($batch, $message, $provider, $adapter, $batchIndex, $dbForProject) { $deliveredTotal = 0; $deliveryErrors = []; $messageData = clone $message; @@ -184,8 +185,24 @@ class Messaging extends Action }; try { - $adapter->send($data); - $deliveredTotal += \count($batch); + $response = new Response($provider->getAttribute('type')); + $response->fromArray(\json_decode($adapter->send($data))); + + $deliveredTotal += $response->getDeliveredTo(); + $details[] = $response->getDetails(); + foreach ($details as $detail) { + if ($detail['status'] === 'failure') { + $deliveryErrors[] = `Failed sending to target {$detail['recipient']} with error: {$detail['error']}`; + } + + // Deleting push targets when token has expired. + if ($detail['error'] === 'Expired token.') { + $target = $dbForProject->findOne('targets', [Query::equal('identifier', [$detail['recipient']])]); + if ($target instanceof Document && !$target->isEmpty()) { + $dbForProject->deleteDocument('targets', $target->getId()); + } + } + } } catch (\Exception $e) { $deliveryErrors[] = 'Failed sending to targets ' . $batchIndex + 1 . '-' . \count($batch) . ' with error: ' . $e->getMessage(); } finally { @@ -313,7 +330,7 @@ class Messaging extends Action 'twilio' => new Twilio($credentials['accountSid'], $credentials['authToken']), 'textmagic' => new Textmagic($credentials['username'], $credentials['apiKey']), 'telesign' => new Telesign($credentials['username'], $credentials['password']), - 'msg91' => new Msg91($credentials['senderId'], $credentials['authKey']), + 'msg91' => new Msg91($credentials['senderId'], $credentials['authKey'], $credentials['templateId']), 'vonage' => new Vonage($credentials['apiKey'], $credentials['apiSecret']), default => null }; @@ -323,6 +340,7 @@ class Messaging extends Action { $credentials = $provider->getAttribute('credentials'); return match ($provider->getAttribute('provider')) { + 'mock' => new Mock('username', 'password'), 'apns' => new APNS( $credentials['authKey'], $credentials['authKeyId'], @@ -339,21 +357,65 @@ class Messaging extends Action { $credentials = $provider->getAttribute('credentials'); return match ($provider->getAttribute('provider')) { + 'mock' => new Mock('username', 'password'), 'mailgun' => new Mailgun($credentials['apiKey'], $credentials['domain'], $credentials['isEuRegion']), - 'sendgrid' => new SendGrid($credentials['apiKey']), + 'sendgrid' => new Sendgrid($credentials['apiKey']), default => null }; } private function buildEmailMessage(Document $message, Document $provider): Email { - $from = $provider['options']['from']; + $fromName = $provider['options']['fromName']; + $fromEmail = $provider['options']['fromEmail']; + $replyToEmail = null; + $replyToName = null; + $cc = null; + $bcc = null; + + if (isset($provider['options']['replyToName']) && isset($provider['options']['replyToEmail'])) { + $replyToName = $provider['options']['replyToName']; + $replyToEmail = $provider['options']['replyToEmail']; + } + + if (isset($provider['options']['cc'])) { + foreach ($provider['options']['cc'] as $ccEmail) { + if (is_array($cc)) { + $cc[] = [ + 'email' => $ccEmail, + ]; + } else { + $cc = [ + [ + 'email' => $ccEmail, + ] + ]; + } + } + } + + if (isset($provider['options']['bcc'])) { + foreach ($provider['options']['bcc'] as $bccEmail) { + if (is_array($bcc)) { + $bcc[] = [ + 'email' => $bccEmail, + ]; + } else { + $bcc = [ + [ + 'email' => $bccEmail, + ] + ]; + } + } + } + $to = $message['to']; $subject = $message['data']['subject']; $content = $message['data']['content']; $html = $message['data']['html']; - return new Email($to, $subject, $content, $from, null, $html); + return new Email($to, $subject, $content, $fromName, $fromEmail, $replyToName, $replyToEmail, $cc, $bcc, html: $html); } private function buildSMSMessage(Document $message, Document $provider): SMS