Allow attaching files from storage to emails
This commit is contained in:
parent
d3913c422b
commit
022c4678ec
|
@ -10,6 +10,7 @@ use Appwrite\Messaging\Status as MessageStatus;
|
||||||
use Appwrite\Network\Validator\Email;
|
use Appwrite\Network\Validator\Email;
|
||||||
use Appwrite\Permission;
|
use Appwrite\Permission;
|
||||||
use Appwrite\Role;
|
use Appwrite\Role;
|
||||||
|
use Appwrite\Utopia\Database\Validator\CompoundUID;
|
||||||
use Appwrite\Utopia\Database\Validator\CustomId;
|
use Appwrite\Utopia\Database\Validator\CustomId;
|
||||||
use Appwrite\Utopia\Database\Validator\Queries\Messages;
|
use Appwrite\Utopia\Database\Validator\Queries\Messages;
|
||||||
use Appwrite\Utopia\Database\Validator\Queries\Providers;
|
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('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('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('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('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('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)
|
->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('project')
|
||||||
->inject('queueForMessaging')
|
->inject('queueForMessaging')
|
||||||
->inject('response')
|
->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()'
|
$messageId = $messageId == 'unique()'
|
||||||
? ID::unique()
|
? ID::unique()
|
||||||
: $messageId;
|
: $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([
|
$message = $dbForProject->createDocument('messages', new Document([
|
||||||
'$id' => $messageId,
|
'$id' => $messageId,
|
||||||
'providerType' => MESSAGE_TYPE_EMAIL,
|
'providerType' => MESSAGE_TYPE_EMAIL,
|
||||||
|
@ -2628,6 +2653,7 @@ App::post('/v1/messaging/messages/email')
|
||||||
'html' => $html,
|
'html' => $html,
|
||||||
'cc' => $cc,
|
'cc' => $cc,
|
||||||
'bcc' => $bcc,
|
'bcc' => $bcc,
|
||||||
|
'attachments' => $attachments,
|
||||||
],
|
],
|
||||||
'status' => $status,
|
'status' => $status,
|
||||||
]));
|
]));
|
||||||
|
|
|
@ -34,6 +34,7 @@ use Utopia\Logger\Log;
|
||||||
use Utopia\Logger\Logger;
|
use Utopia\Logger\Logger;
|
||||||
use Utopia\Pools\Group;
|
use Utopia\Pools\Group;
|
||||||
use Utopia\Queue\Connection;
|
use Utopia\Queue\Connection;
|
||||||
|
use Utopia\Storage\Device\Local;
|
||||||
|
|
||||||
Authorization::disable();
|
Authorization::disable();
|
||||||
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
|
Runtime::enableCoroutine(SWOOLE_HOOK_ALL);
|
||||||
|
@ -209,6 +210,11 @@ Server::setResource('getCacheDevice', function () {
|
||||||
return getDevice(APP_STORAGE_CACHE . '/app-' . $projectId);
|
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');
|
$pools = $register->get('pools');
|
||||||
$platform = new Appwrite();
|
$platform = new Appwrite();
|
||||||
|
|
|
@ -2,11 +2,14 @@
|
||||||
|
|
||||||
namespace Appwrite\Platform\Workers;
|
namespace Appwrite\Platform\Workers;
|
||||||
|
|
||||||
|
use Appwrite\Auth\Auth;
|
||||||
use Appwrite\Event\Usage;
|
use Appwrite\Event\Usage;
|
||||||
use Appwrite\Extend\Exception;
|
use Appwrite\Extend\Exception;
|
||||||
use Appwrite\Messaging\Status as MessageStatus;
|
use Appwrite\Messaging\Status as MessageStatus;
|
||||||
use Utopia\App;
|
use Utopia\App;
|
||||||
use Utopia\CLI\Console;
|
use Utopia\CLI\Console;
|
||||||
|
use Utopia\Config\Config;
|
||||||
|
use Utopia\Database\Validator\Authorization;
|
||||||
use Utopia\DSN\DSN;
|
use Utopia\DSN\DSN;
|
||||||
use Utopia\Database\Database;
|
use Utopia\Database\Database;
|
||||||
use Utopia\Database\DateTime;
|
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\Twilio;
|
||||||
use Utopia\Messaging\Adapter\SMS\Vonage;
|
use Utopia\Messaging\Adapter\SMS\Vonage;
|
||||||
use Utopia\Messaging\Messages\Email;
|
use Utopia\Messaging\Messages\Email;
|
||||||
|
use Utopia\Messaging\Messages\Email\Attachment;
|
||||||
use Utopia\Messaging\Messages\Push;
|
use Utopia\Messaging\Messages\Push;
|
||||||
use Utopia\Messaging\Messages\SMS;
|
use Utopia\Messaging\Messages\SMS;
|
||||||
use Utopia\Platform\Action;
|
use Utopia\Platform\Action;
|
||||||
use Utopia\Queue\Message;
|
use Utopia\Queue\Message;
|
||||||
|
use Utopia\Storage\Device;
|
||||||
|
use Utopia\Storage\Storage;
|
||||||
|
|
||||||
use function Swoole\Coroutine\batch;
|
use function Swoole\Coroutine\batch;
|
||||||
|
|
||||||
|
@ -53,26 +59,38 @@ class Messaging extends Action
|
||||||
->inject('message')
|
->inject('message')
|
||||||
->inject('log')
|
->inject('log')
|
||||||
->inject('dbForProject')
|
->inject('dbForProject')
|
||||||
|
->inject('deviceFiles')
|
||||||
|
->inject('getLocalCache')
|
||||||
->inject('queueForUsage')
|
->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 Message $message
|
||||||
* @param Log $log
|
* @param Log $log
|
||||||
* @param Database $dbForProject
|
* @param Database $dbForProject
|
||||||
|
* @param Device $deviceFiles
|
||||||
|
* @param callable $getLocalCache
|
||||||
* @param Usage $queueForUsage
|
* @param Usage $queueForUsage
|
||||||
* @return void
|
* @return void
|
||||||
* @throws Exception
|
* @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() ?? [];
|
$payload = $message->getPayload() ?? [];
|
||||||
|
|
||||||
if (empty($payload)) {
|
if (empty($payload)) {
|
||||||
throw new Exception('Missing payload');
|
throw new Exception('Missing payload');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$project = new Document($payload['project'] ?? []);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!\is_null($payload['message'])
|
!\is_null($payload['message'])
|
||||||
|
@ -82,7 +100,7 @@ class Messaging extends Action
|
||||||
// Message was triggered internally
|
// Message was triggered internally
|
||||||
$this->processInternalSMSMessage(
|
$this->processInternalSMSMessage(
|
||||||
new Document($payload['message']),
|
new Document($payload['message']),
|
||||||
new Document($payload['project'] ?? []),
|
$project,
|
||||||
$payload['recipients'],
|
$payload['recipients'],
|
||||||
$queueForUsage,
|
$queueForUsage,
|
||||||
$log,
|
$log,
|
||||||
|
@ -90,12 +108,21 @@ class Messaging extends Action
|
||||||
} else {
|
} else {
|
||||||
$message = $dbForProject->getDocument('messages', $payload['messageId']);
|
$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', []);
|
$topicIds = $message->getAttribute('topics', []);
|
||||||
$targetIds = $message->getAttribute('targets', []);
|
$targetIds = $message->getAttribute('targets', []);
|
||||||
$userIds = $message->getAttribute('users', []);
|
$userIds = $message->getAttribute('users', []);
|
||||||
|
@ -199,8 +226,8 @@ class Messaging extends Action
|
||||||
/**
|
/**
|
||||||
* @var array<array> $results
|
* @var array<array> $results
|
||||||
*/
|
*/
|
||||||
$results = batch(\array_map(function ($providerId) use ($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) {
|
return function () use ($providerId, $identifiers, $providers, $fallback, $message, $dbForProject, $localCache, $deviceFiles) {
|
||||||
if (\array_key_exists($providerId, $providers)) {
|
if (\array_key_exists($providerId, $providers)) {
|
||||||
$provider = $providers[$providerId];
|
$provider = $providers[$providerId];
|
||||||
} else {
|
} else {
|
||||||
|
@ -226,8 +253,8 @@ class Messaging extends Action
|
||||||
$batches = \array_chunk($identifiers, $maxBatchSize);
|
$batches = \array_chunk($identifiers, $maxBatchSize);
|
||||||
$batchIndex = 0;
|
$batchIndex = 0;
|
||||||
|
|
||||||
return batch(\array_map(function ($batch) use ($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) {
|
return function () use ($batch, $message, $provider, $adapter, &$batchIndex, $dbForProject, $localCache, $deviceFiles) {
|
||||||
$deliveredTotal = 0;
|
$deliveredTotal = 0;
|
||||||
$deliveryErrors = [];
|
$deliveryErrors = [];
|
||||||
$messageData = clone $message;
|
$messageData = clone $message;
|
||||||
|
@ -236,7 +263,7 @@ class Messaging extends Action
|
||||||
$data = match ($provider->getAttribute('type')) {
|
$data = match ($provider->getAttribute('type')) {
|
||||||
MESSAGE_TYPE_SMS => $this->buildSMSMessage($messageData, $provider),
|
MESSAGE_TYPE_SMS => $this->buildSMSMessage($messageData, $provider),
|
||||||
MESSAGE_TYPE_PUSH => $this->buildPushMessage($messageData),
|
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)
|
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;
|
$fromName = $provider['options']['fromName'] ?? null;
|
||||||
$fromEmail = $provider['options']['fromEmail'] ?? null;
|
$fromEmail = $provider['options']['fromEmail'] ?? null;
|
||||||
$replyToEmail = $provider['options']['replyToEmail'] ?? null;
|
$replyToEmail = $provider['options']['replyToEmail'] ?? null;
|
||||||
|
@ -474,8 +506,9 @@ class Messaging extends Action
|
||||||
$bccTargets = $data['bcc'] ?? [];
|
$bccTargets = $data['bcc'] ?? [];
|
||||||
$cc = [];
|
$cc = [];
|
||||||
$bcc = [];
|
$bcc = [];
|
||||||
|
$attachments = $data['attachments'] ?? [];
|
||||||
|
|
||||||
if (\count($ccTargets) > 0) {
|
if (!empty($ccTargets)) {
|
||||||
$ccTargets = $dbForProject->find('targets', [
|
$ccTargets = $dbForProject->find('targets', [
|
||||||
Query::equal('$id', $ccTargets),
|
Query::equal('$id', $ccTargets),
|
||||||
Query::limit(\count($ccTargets)),
|
Query::limit(\count($ccTargets)),
|
||||||
|
@ -485,7 +518,7 @@ class Messaging extends Action
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (\count($bccTargets) > 0) {
|
if (!empty($bccTargets)) {
|
||||||
$bccTargets = $dbForProject->find('targets', [
|
$bccTargets = $dbForProject->find('targets', [
|
||||||
Query::equal('$id', $bccTargets),
|
Query::equal('$id', $bccTargets),
|
||||||
Query::limit(\count($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'];
|
$to = $message['to'];
|
||||||
$subject = $data['subject'];
|
$subject = $data['subject'];
|
||||||
$content = $data['content'];
|
$content = $data['content'];
|
||||||
$html = $data['html'] ?? false;
|
$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
|
private function buildSMSMessage(Document $message, Document $provider): SMS
|
||||||
|
@ -509,7 +594,11 @@ class Messaging extends Action
|
||||||
$content = $message['data']['content'];
|
$content = $message['data']['content'];
|
||||||
$from = $provider['options']['from'];
|
$from = $provider['options']['from'];
|
||||||
|
|
||||||
return new SMS($to, $content, $from);
|
return new SMS(
|
||||||
|
$to,
|
||||||
|
$content,
|
||||||
|
$from
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildPushMessage(Document $message): Push
|
private function buildPushMessage(Document $message): Push
|
||||||
|
@ -525,6 +614,17 @@ class Messaging extends Action
|
||||||
$tag = $message['data']['tag'] ?? null;
|
$tag = $message['data']['tag'] ?? null;
|
||||||
$badge = $message['data']['badge'] ?? 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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue