Merge remote-tracking branch 'origin/feat-email-attachments' into feat-push-images

This commit is contained in:
Jake Barnby
2024-02-20 18:06:29 +13:00
6 changed files with 277 additions and 49 deletions
+147 -19
View File
@@ -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,35 @@ class Messaging extends Action
->inject('message')
->inject('log')
->inject('dbForProject')
->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, callable $getLocalCache, Usage $queueForUsage) => $this->action($message, $log, $dbForProject, $getLocalCache, $queueForUsage));
}
/**
* @param Message $message
* @param Log $log
* @param Database $dbForProject
* @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,
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 +97,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 +105,21 @@ class Messaging extends Action
} else {
$message = $dbForProject->getDocument('messages', $payload['messageId']);
$this->processMessage($dbForProject, $message);
$this->processMessage(
$dbForProject,
$message,
getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()),
$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 +223,8 @@ class Messaging extends Action
/**
* @var array<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 +250,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 +260,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)
};
@@ -310,6 +334,37 @@ class Messaging extends Action
$message->setAttribute('deliveredAt', DateTime::now());
$dbForProject->updateDocument('messages', $message->getId(), $message);
// Delete any attachments that were downloaded to the local cache
if ($provider->getAttribute('type') === MESSAGE_TYPE_EMAIL) {
if ($deviceFiles->getType() === Storage::DEVICE_LOCAL) {
return;
}
$data = $message->getAttribute('data');
$attachments = $data['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);
}
$path = $file->getAttribute('path', '');
if ($localCache->exists($path)) {
$localCache->delete($path);
}
}
}
}
private function processInternalSMSMessage(Document $message, Document $project, array $recipients, Usage $queueForUsage, Log $log): void
@@ -463,8 +518,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 +534,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 +546,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 +556,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 +622,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 +642,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
);
}
}
@@ -0,0 +1,58 @@
<?php
namespace Appwrite\Utopia\Database\Validator;
use Utopia\Database\Validator\UID;
use Utopia\Validator;
class CompoundUID extends Validator
{
public function getDescription(): string
{
return 'Must consist of multiple UIDs separated by a colon. Each UID must contain at most 36 chars. Valid chars are a-z, A-Z, 0-9, and underscore. Can\'t start with a special char.';
}
public function isArray(): bool
{
return false;
}
public function isValid($value): bool
{
if (!\is_string($value)) {
return false;
}
$ids = static::parse($value);
if (\count($ids) < 2) {
return false;
}
foreach ($ids as $id) {
$validator = new UID();
if (!$validator->isValid($id)) {
return false;
}
}
return true;
}
public function getType(): string
{
return self::TYPE_STRING;
}
public static function parse(string $key): array
{
$parts = \explode(':', $key);
$result = [];
foreach ($parts as $part) {
$result[] = $part;
}
return $result;
}
}