diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index 8e6c73f3b..c2818a8b1 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -10,6 +10,7 @@ use Appwrite\Messaging\Status as MessageStatus; use Appwrite\Network\Validator\Email; use Appwrite\Permission; use Appwrite\Role; +use Appwrite\Utopia\Database\Validator\CompoundUID; use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Database\Validator\Queries\Messages; use Appwrite\Utopia\Database\Validator\Queries\Providers; @@ -2573,6 +2574,7 @@ App::post('/v1/messaging/messages/email') ->param('targets', [], new ArrayList(new UID()), 'List of Targets IDs.', true) ->param('cc', [], new ArrayList(new UID()), 'Array of target IDs to be added as CC.', true) ->param('bcc', [], new ArrayList(new UID()), 'Array of target IDs to be added as BCC.', true) + ->param('attachments', [], new ArrayList(new CompoundUID()), 'Array of compound bucket IDs to file IDs to be attached to the email.', true) ->param('status', MessageStatus::DRAFT, new WhiteList([MessageStatus::DRAFT, MessageStatus::SCHEDULED, MessageStatus::PROCESSING]), 'Message Status. Value must be one of: ' . implode(', ', [MessageStatus::DRAFT, MessageStatus::SCHEDULED, MessageStatus::PROCESSING]) . '.', true) ->param('html', false, new Boolean(), 'Is content of type HTML', true) ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) @@ -2582,7 +2584,7 @@ App::post('/v1/messaging/messages/email') ->inject('project') ->inject('queueForMessaging') ->inject('response') - ->action(function (string $messageId, string $subject, string $content, array $topics, array $users, array $targets, array $cc, array $bcc, string $status, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForConsole, Document $project, Messaging $queueForMessaging, Response $response) { + ->action(function (string $messageId, string $subject, string $content, array $topics, array $users, array $targets, array $cc, array $bcc, array $attachments, string $status, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForConsole, Document $project, Messaging $queueForMessaging, Response $response) { $messageId = $messageId == 'unique()' ? ID::unique() : $messageId; @@ -2615,6 +2617,29 @@ App::post('/v1/messaging/messages/email') } } + if (!empty($attachments)) { + foreach ($attachments as &$attachment) { + [$bucketId, $fileId] = CompoundUID::parse($attachment); + + $bucket = $dbForProject->getDocument('buckets', $bucketId); + + if ($bucket->isEmpty()) { + throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); + } + + $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); + + if ($file->isEmpty()) { + throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); + } + + $attachment = [ + 'bucketId' => $bucketId, + 'fileId' => $fileId, + ]; + } + } + $message = $dbForProject->createDocument('messages', new Document([ '$id' => $messageId, 'providerType' => MESSAGE_TYPE_EMAIL, @@ -2628,6 +2653,7 @@ App::post('/v1/messaging/messages/email') 'html' => $html, 'cc' => $cc, 'bcc' => $bcc, + 'attachments' => $attachments, ], 'status' => $status, ])); diff --git a/app/worker.php b/app/worker.php index 2080970ac..f432adac3 100644 --- a/app/worker.php +++ b/app/worker.php @@ -34,6 +34,7 @@ use Utopia\Logger\Log; use Utopia\Logger\Logger; use Utopia\Pools\Group; use Utopia\Queue\Connection; +use Utopia\Storage\Device\Local; Authorization::disable(); Runtime::enableCoroutine(SWOOLE_HOOK_ALL); @@ -209,6 +210,11 @@ Server::setResource('getCacheDevice', function () { return getDevice(APP_STORAGE_CACHE . '/app-' . $projectId); }; }); +Server::setResource('getLocalCache', function () { + return function (string $projectId) { + return new Local(APP_STORAGE_CACHE . '/app-' . $projectId); + }; +}); $pools = $register->get('pools'); $platform = new Appwrite(); diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index 083eae4e0..4f20139a6 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -2,11 +2,14 @@ namespace Appwrite\Platform\Workers; +use Appwrite\Auth\Auth; use Appwrite\Event\Usage; use Appwrite\Extend\Exception; use Appwrite\Messaging\Status as MessageStatus; use Utopia\App; use Utopia\CLI\Console; +use Utopia\Config\Config; +use Utopia\Database\Validator\Authorization; use Utopia\DSN\DSN; use Utopia\Database\Database; use Utopia\Database\DateTime; @@ -29,10 +32,13 @@ 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\Email\Attachment; use Utopia\Messaging\Messages\Push; use Utopia\Messaging\Messages\SMS; use Utopia\Platform\Action; use Utopia\Queue\Message; +use Utopia\Storage\Device; +use Utopia\Storage\Storage; use function Swoole\Coroutine\batch; @@ -53,26 +59,38 @@ class Messaging extends Action ->inject('message') ->inject('log') ->inject('dbForProject') + ->inject('deviceFiles') + ->inject('getLocalCache') ->inject('queueForUsage') - ->callback(fn(Message $message, Log $log, Database $dbForProject, Usage $queueForUsage) => $this->action($message, $log, $dbForProject, $queueForUsage)); + ->callback(fn(Message $message, Log $log, Database $dbForProject, Device $deviceFiles, callable $getLocalCache, Usage $queueForUsage) => $this->action($message, $log, $dbForProject, $deviceFiles, $getLocalCache, $queueForUsage)); } /** * @param Message $message * @param Log $log * @param Database $dbForProject + * @param Device $deviceFiles + * @param callable $getLocalCache * @param Usage $queueForUsage * @return void * @throws Exception + * @throws \Utopia\Database\Exception */ - public function action(Message $message, Log $log, Database $dbForProject, Usage $queueForUsage): void - { + public function action( + Message $message, + Log $log, + Database $dbForProject, + Device $deviceFiles, + callable $getLocalCache, + Usage $queueForUsage + ): void { $payload = $message->getPayload() ?? []; if (empty($payload)) { throw new Exception('Missing payload'); } + $project = new Document($payload['project'] ?? []); if ( !\is_null($payload['message']) @@ -82,7 +100,7 @@ class Messaging extends Action // Message was triggered internally $this->processInternalSMSMessage( new Document($payload['message']), - new Document($payload['project'] ?? []), + $project, $payload['recipients'], $queueForUsage, $log, @@ -90,12 +108,21 @@ class Messaging extends Action } else { $message = $dbForProject->getDocument('messages', $payload['messageId']); - $this->processMessage($dbForProject, $message); + $this->processMessage( + $dbForProject, + $message, + $deviceFiles, + $getLocalCache($project->getId()) + ); } } - private function processMessage(Database $dbForProject, Document $message): void - { + private function processMessage( + Database $dbForProject, + Document $message, + Device $deviceFiles, + Device $localCache, + ): void { $topicIds = $message->getAttribute('topics', []); $targetIds = $message->getAttribute('targets', []); $userIds = $message->getAttribute('users', []); @@ -199,8 +226,8 @@ class Messaging extends Action /** * @var array $results */ - $results = batch(\array_map(function ($providerId) use ($identifiers, $providers, $fallback, $message, $dbForProject) { - return function () use ($providerId, $identifiers, $providers, $fallback, $message, $dbForProject) { + $results = batch(\array_map(function ($providerId) use ($identifiers, $providers, $fallback, $message, $dbForProject, $localCache, $deviceFiles) { + return function () use ($providerId, $identifiers, $providers, $fallback, $message, $dbForProject, $localCache, $deviceFiles) { if (\array_key_exists($providerId, $providers)) { $provider = $providers[$providerId]; } else { @@ -226,8 +253,8 @@ class Messaging extends Action $batches = \array_chunk($identifiers, $maxBatchSize); $batchIndex = 0; - return batch(\array_map(function ($batch) use ($message, $provider, $adapter, &$batchIndex, $dbForProject) { - return function () use ($batch, $message, $provider, $adapter, &$batchIndex, $dbForProject) { + return batch(\array_map(function ($batch) use ($message, $provider, $adapter, &$batchIndex, $dbForProject, $localCache, $deviceFiles) { + return function () use ($batch, $message, $provider, $adapter, &$batchIndex, $dbForProject, $localCache, $deviceFiles) { $deliveredTotal = 0; $deliveryErrors = []; $messageData = clone $message; @@ -236,7 +263,7 @@ class Messaging extends Action $data = match ($provider->getAttribute('type')) { MESSAGE_TYPE_SMS => $this->buildSMSMessage($messageData, $provider), MESSAGE_TYPE_PUSH => $this->buildPushMessage($messageData), - MESSAGE_TYPE_EMAIL => $this->buildEmailMessage($dbForProject, $messageData, $provider), + MESSAGE_TYPE_EMAIL => $this->buildEmailMessage($dbForProject, $messageData, $provider, $deviceFiles, $localCache), default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE) }; @@ -463,8 +490,13 @@ class Messaging extends Action }; } - private function buildEmailMessage(Database $dbForProject, Document $message, Document $provider): Email - { + private function buildEmailMessage( + Database $dbForProject, + Document $message, + Document $provider, + Device $deviceFiles, + Device $localCache, + ): Email { $fromName = $provider['options']['fromName'] ?? null; $fromEmail = $provider['options']['fromEmail'] ?? null; $replyToEmail = $provider['options']['replyToEmail'] ?? null; @@ -474,8 +506,9 @@ class Messaging extends Action $bccTargets = $data['bcc'] ?? []; $cc = []; $bcc = []; + $attachments = $data['attachments'] ?? []; - if (\count($ccTargets) > 0) { + if (!empty($ccTargets)) { $ccTargets = $dbForProject->find('targets', [ Query::equal('$id', $ccTargets), Query::limit(\count($ccTargets)), @@ -485,7 +518,7 @@ class Messaging extends Action } } - if (\count($bccTargets) > 0) { + if (!empty($bccTargets)) { $bccTargets = $dbForProject->find('targets', [ Query::equal('$id', $bccTargets), Query::limit(\count($bccTargets)), @@ -495,12 +528,64 @@ class Messaging extends Action } } + if (!empty($attachments)) { + foreach ($attachments as &$attachment) { + $bucketId = $attachment['bucketId']; + $fileId = $attachment['fileId']; + + $bucket = $dbForProject->getDocument('buckets', $bucketId); + if ($bucket->isEmpty()) { + throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); + } + + $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); + if ($file->isEmpty()) { + throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); + } + + $mimes = Config::getParam('storage-mimes'); + $path = $file->getAttribute('path', ''); + + if (!$deviceFiles->exists($path)) { + throw new Exception(Exception::STORAGE_FILE_NOT_FOUND, 'File not found in ' . $path); + } + + $contentType = 'text/plain'; + + if (\in_array($file->getAttribute('mimeType'), $mimes)) { + $contentType = $file->getAttribute('mimeType'); + } + + if ($deviceFiles->getType() !== Storage::DEVICE_LOCAL) { + $deviceFiles->transfer($path, $path, $localCache); + } + + $attachment = new Attachment( + $file->getAttribute('name'), + $path, + $contentType + ); + } + } + $to = $message['to']; $subject = $data['subject']; $content = $data['content']; $html = $data['html'] ?? false; - return new Email($to, $subject, $content, $fromName, $fromEmail, $replyToName, $replyToEmail, $cc, $bcc, null, $html); + return new Email( + $to, + $subject, + $content, + $fromName, + $fromEmail, + $replyToName, + $replyToEmail, + $cc, + $bcc, + $attachments, + $html + ); } private function buildSMSMessage(Document $message, Document $provider): SMS @@ -509,7 +594,11 @@ class Messaging extends Action $content = $message['data']['content']; $from = $provider['options']['from']; - return new SMS($to, $content, $from); + return new SMS( + $to, + $content, + $from + ); } private function buildPushMessage(Document $message): Push @@ -525,6 +614,17 @@ class Messaging extends Action $tag = $message['data']['tag'] ?? null; $badge = $message['data']['badge'] ?? null; - return new Push($to, $title, $body, $data, $action, $sound, $icon, $color, $tag, $badge); + return new Push( + $to, + $title, + $body, + $data, + $action, + $sound, + $icon, + $color, + $tag, + $badge + ); } }