From e5dc08619aae087c6d45768225ea8d1e55eff63e Mon Sep 17 00:00:00 2001 From: premtsd-code Date: Wed, 20 May 2026 14:27:52 +0100 Subject: [PATCH 1/3] Add SMTP migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a single SMTP settings resource carrying the project's custom SMTP configuration. Source reads via the typed Project model (Project::get()->smtp*); destination writes the smtp attribute on the project document directly, matching the pattern used by the other 5 settings resources. Password is intentionally not migrated — the source API only exposes smtpPassword as an empty string (write-only field). The destination's existing password is preserved via read-then-merge of the smtp map. --- src/Migration/Destinations/Appwrite.php | 36 ++++++ src/Migration/Resource.php | 2 + src/Migration/Resources/Settings/SMTP.php | 129 ++++++++++++++++++++++ src/Migration/Sources/Appwrite.php | 43 ++++++++ src/Migration/Transfer.php | 2 + 5 files changed, 212 insertions(+) create mode 100644 src/Migration/Resources/Settings/SMTP.php diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index 07f26dcb..24e8b61a 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -65,6 +65,7 @@ use Utopia\Migration\Resources\Settings\ProjectVariable; use Utopia\Migration\Resources\Settings\Protocols; use Utopia\Migration\Resources\Settings\Services as ServicesResource; +use Utopia\Migration\Resources\Settings\SMTP; use Utopia\Migration\Resources\Settings\Webhook; use Utopia\Migration\Resources\Sites\Deployment as SiteDeployment; use Utopia\Migration\Resources\Sites\EnvVar as SiteEnvVar; @@ -298,6 +299,7 @@ public static function getSupportedResources(): array Resource::TYPE_PROTOCOLS, Resource::TYPE_LABELS, Resource::TYPE_SERVICES, + Resource::TYPE_SMTP, // Backups Resource::TYPE_BACKUP_POLICY, @@ -3142,6 +3144,10 @@ public function importSettingsResource(Resource $resource): Resource /** @var ServicesResource $resource */ $this->createServices($resource); break; + case Resource::TYPE_SMTP: + /** @var SMTP $resource */ + $this->createSMTP($resource); + break; } if ($resource->getStatus() !== Resource::STATUS_SKIPPED) { @@ -3259,6 +3265,36 @@ protected function createServices(ServicesResource $resource): bool return true; } + /** + * Password is intentionally not copied — source API never exposes it. + * Read-then-merge preserves the destination's existing password. + */ + protected function createSMTP(SMTP $resource): bool + { + $project = $this->dbForPlatform->getDocument('projects', $this->projectId); + $smtp = $project->getAttribute('smtp', []); + + $smtp['enabled'] = $resource->getEnabled(); + $smtp['senderName'] = $resource->getSenderName(); + $smtp['senderEmail'] = $resource->getSenderEmail(); + $smtp['replyToName'] = $resource->getReplyToName(); + $smtp['replyToEmail'] = $resource->getReplyToEmail(); + $smtp['host'] = $resource->getHost(); + $smtp['port'] = $resource->getPort(); + $smtp['username'] = $resource->getUsername(); + $smtp['secure'] = $resource->getSecure(); + + $this->dbForPlatform->getAuthorization()->skip(fn () => $this->dbForPlatform->updateDocument( + 'projects', + $this->projectId, + new UtopiaDocument(['smtp' => $smtp]), + )); + + $this->dbForPlatform->purgeCachedDocument('projects', $this->projectId); + + return true; + } + protected function createWebhook(Webhook $resource): bool { $existing = $this->dbForPlatform->findOne('webhooks', [ diff --git a/src/Migration/Resource.php b/src/Migration/Resource.php index f77b3d8d..53020e1a 100644 --- a/src/Migration/Resource.php +++ b/src/Migration/Resource.php @@ -85,6 +85,7 @@ abstract class Resource implements \JsonSerializable public const TYPE_PROTOCOLS = 'protocols'; public const TYPE_LABELS = 'labels'; public const TYPE_SERVICES = 'services'; + public const TYPE_SMTP = 'smtp'; // Messaging public const TYPE_SUBSCRIBER = 'subscriber'; @@ -133,6 +134,7 @@ abstract class Resource implements \JsonSerializable self::TYPE_PROTOCOLS, self::TYPE_LABELS, self::TYPE_SERVICES, + self::TYPE_SMTP, self::TYPE_PROVIDER, self::TYPE_TOPIC, self::TYPE_SUBSCRIBER, diff --git a/src/Migration/Resources/Settings/SMTP.php b/src/Migration/Resources/Settings/SMTP.php new file mode 100644 index 00000000..9233225d --- /dev/null +++ b/src/Migration/Resources/Settings/SMTP.php @@ -0,0 +1,129 @@ +id = $id; + $this->createdAt = $createdAt; + $this->updatedAt = $updatedAt; + } + + /** + * @param array $array + */ + public static function fromArray(array $array): self + { + return new self( + $array['id'], + (bool) ($array['enabled'] ?? false), + (string) ($array['senderName'] ?? ''), + (string) ($array['senderEmail'] ?? ''), + (string) ($array['replyToName'] ?? ''), + (string) ($array['replyToEmail'] ?? ''), + (string) ($array['host'] ?? ''), + (int) ($array['port'] ?? 0), + (string) ($array['username'] ?? ''), + (string) ($array['secure'] ?? ''), + createdAt: $array['createdAt'] ?? '', + updatedAt: $array['updatedAt'] ?? '', + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'enabled' => $this->enabled, + 'senderName' => $this->senderName, + 'senderEmail' => $this->senderEmail, + 'replyToName' => $this->replyToName, + 'replyToEmail' => $this->replyToEmail, + 'host' => $this->host, + 'port' => $this->port, + 'username' => $this->username, + 'secure' => $this->secure, + 'createdAt' => $this->createdAt, + 'updatedAt' => $this->updatedAt, + ]; + } + + public static function getName(): string + { + return Resource::TYPE_SMTP; + } + + public function getGroup(): string + { + return Transfer::GROUP_SETTINGS; + } + + public function getEnabled(): bool + { + return $this->enabled; + } + + public function getSenderName(): string + { + return $this->senderName; + } + + public function getSenderEmail(): string + { + return $this->senderEmail; + } + + public function getReplyToName(): string + { + return $this->replyToName; + } + + public function getReplyToEmail(): string + { + return $this->replyToEmail; + } + + public function getHost(): string + { + return $this->host; + } + + public function getPort(): int + { + return $this->port; + } + + public function getUsername(): string + { + return $this->username; + } + + public function getSecure(): string + { + return $this->secure; + } +} diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index d54f935e..6c80209d 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -72,6 +72,7 @@ use Utopia\Migration\Resources\Settings\ProjectVariable; use Utopia\Migration\Resources\Settings\Protocols; use Utopia\Migration\Resources\Settings\Services as ServicesResource; +use Utopia\Migration\Resources\Settings\SMTP; use Utopia\Migration\Resources\Settings\Webhook; use Utopia\Migration\Resources\Sites\Deployment as SiteDeployment; use Utopia\Migration\Resources\Sites\EnvVar as SiteEnvVar; @@ -236,6 +237,7 @@ public static function getSupportedResources(): array Resource::TYPE_PROTOCOLS, Resource::TYPE_LABELS, Resource::TYPE_SERVICES, + Resource::TYPE_SMTP, ]; } @@ -1609,6 +1611,11 @@ private function reportSettings(array $resources, array &$report, array $resourc // Singleton — one services config per project. $report[Resource::TYPE_SERVICES] = 1; } + + if (\in_array(Resource::TYPE_SMTP, $resources)) { + // Singleton — one SMTP config per project. + $report[Resource::TYPE_SMTP] = 1; + } } /** @@ -1686,6 +1693,20 @@ protected function exportGroupSettings(int $batchSize, array $resources): void previous: $e )); } + + try { + if (\in_array(Resource::TYPE_SMTP, $resources)) { + $this->exportSMTP(); + } + } catch (\Throwable $e) { + $this->addError(new Exception( + Resource::TYPE_SMTP, + Transfer::GROUP_SETTINGS, + message: $e->getMessage(), + code: $e->getCode(), + previous: $e + )); + } } private function exportServices(): void @@ -1758,6 +1779,28 @@ private function exportProtocols(): void $this->callback([$protocols]); } + private function exportSMTP(): void + { + $project = $this->project->get(); + + $smtp = new SMTP( + $this->projectId, + $project->smtpEnabled, + $project->smtpSenderName, + $project->smtpSenderEmail, + $project->smtpReplyToName, + $project->smtpReplyToEmail, + $project->smtpHost, + $project->smtpPort, + $project->smtpUsername, + $project->smtpSecure, + createdAt: $project->createdAt, + updatedAt: $project->updatedAt, + ); + + $this->callback([$smtp]); + } + /** * @throws AppwriteException */ diff --git a/src/Migration/Transfer.php b/src/Migration/Transfer.php index c22d1c26..0ed5cb78 100644 --- a/src/Migration/Transfer.php +++ b/src/Migration/Transfer.php @@ -102,6 +102,7 @@ class Transfer Resource::TYPE_PROTOCOLS, Resource::TYPE_LABELS, Resource::TYPE_SERVICES, + Resource::TYPE_SMTP, ]; public const GROUP_BACKUPS_RESOURCES = [ @@ -150,6 +151,7 @@ class Transfer Resource::TYPE_PROTOCOLS, Resource::TYPE_LABELS, Resource::TYPE_SERVICES, + Resource::TYPE_SMTP, // legacy Resource::TYPE_DOCUMENT, From 529a3f933e6631193fcbf3d06fd43554ececad32 Mon Sep 17 00:00:00 2001 From: premtsd-code Date: Thu, 21 May 2026 09:59:53 +0100 Subject: [PATCH 2/3] chore: fix import ordering (Pint) --- src/Migration/Destinations/Appwrite.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Migration/Destinations/Appwrite.php b/src/Migration/Destinations/Appwrite.php index 24e8b61a..fc49a348 100644 --- a/src/Migration/Destinations/Appwrite.php +++ b/src/Migration/Destinations/Appwrite.php @@ -10,8 +10,8 @@ use Appwrite\Enums\Framework; use Appwrite\Enums\PasswordHash; use Appwrite\Enums\ProjectProtocolId; -use Appwrite\Enums\Runtime; use Appwrite\Enums\ProjectServiceId; +use Appwrite\Enums\Runtime; use Appwrite\Enums\SmtpEncryption; use Appwrite\InputFile; use Appwrite\Services\Functions; From e5fbe97d908fb5d50102fa8bc43d078fb9c2f72a Mon Sep 17 00:00:00 2001 From: Prem Palanisamy Date: Thu, 21 May 2026 14:29:03 +0100 Subject: [PATCH 3/3] Normalize error code for SMTP export handler --- src/Migration/Sources/Appwrite.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Migration/Sources/Appwrite.php b/src/Migration/Sources/Appwrite.php index b36ef3eb..9a31eedc 100644 --- a/src/Migration/Sources/Appwrite.php +++ b/src/Migration/Sources/Appwrite.php @@ -1703,7 +1703,7 @@ protected function exportGroupSettings(int $batchSize, array $resources): void Resource::TYPE_SMTP, Transfer::GROUP_SETTINGS, message: $e->getMessage(), - code: $e->getCode(), + code: (int) $e->getCode() ?: Exception::CODE_INTERNAL, previous: $e )); }