Skip to content
36 changes: 36 additions & 0 deletions src/Migration/Destinations/Appwrite.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,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;
Expand Down Expand Up @@ -291,6 +292,7 @@ public static function getSupportedResources(): array
Resource::TYPE_PLATFORM,
Resource::TYPE_API_KEY,
Resource::TYPE_WEBHOOK,
Resource::TYPE_SMTP,

// Project
Resource::TYPE_PROJECT_VARIABLE,
Expand Down Expand Up @@ -3113,6 +3115,10 @@ public function importIntegrationsResource(Resource $resource): Resource
/** @var Webhook $resource */
$this->createWebhook($resource);
break;
case Resource::TYPE_SMTP:
/** @var SMTP $resource */
$this->createSMTP($resource);
break;
}

if ($resource->getStatus() !== Resource::STATUS_SKIPPED) {
Expand Down Expand Up @@ -3244,6 +3250,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();
Comment thread
premtsd-code marked this conversation as resolved.
$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', [
Expand Down
2 changes: 2 additions & 0 deletions src/Migration/Resource.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ abstract class Resource implements \JsonSerializable
public const TYPE_PLATFORM = 'platform';
public const TYPE_API_KEY = 'api-key';
public const TYPE_WEBHOOK = 'webhook';
public const TYPE_SMTP = 'smtp';

// Project (per-project singleton/settings resources)
public const TYPE_PROJECT_VARIABLE = 'project-variable';
Expand Down Expand Up @@ -129,6 +130,7 @@ abstract class Resource implements \JsonSerializable
self::TYPE_PLATFORM,
self::TYPE_API_KEY,
self::TYPE_WEBHOOK,
self::TYPE_SMTP,
self::TYPE_PROJECT_VARIABLE,
self::TYPE_PROJECT_PROTOCOLS,
self::TYPE_PROJECT_LABELS,
Expand Down
129 changes: 129 additions & 0 deletions src/Migration/Resources/Settings/SMTP.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php

namespace Utopia\Migration\Resources\Settings;

use Utopia\Migration\Resource;
use Utopia\Migration\Transfer;

/**
* Singleton resource representing the project's custom SMTP configuration.
* Password is not migrated — the source API never exposes it.
*/
class SMTP extends Resource
{
public function __construct(
string $id,
private readonly bool $enabled = false,
private readonly string $senderName = '',
private readonly string $senderEmail = '',
private readonly string $replyToName = '',
private readonly string $replyToEmail = '',
private readonly string $host = '',
private readonly int $port = 0,
private readonly string $username = '',
private readonly string $secure = '',
string $createdAt = '',
string $updatedAt = '',
) {
$this->id = $id;
$this->createdAt = $createdAt;
$this->updatedAt = $updatedAt;
}

/**
* @param array<string, mixed> $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<string, mixed>
*/
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_INTEGRATIONS;
}

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;
}
}
44 changes: 44 additions & 0 deletions src/Migration/Sources/Appwrite.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,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;
Expand Down Expand Up @@ -226,6 +227,7 @@ public static function getSupportedResources(): array
Resource::TYPE_PLATFORM,
Resource::TYPE_API_KEY,
Resource::TYPE_WEBHOOK,
Resource::TYPE_SMTP,

// Backups
Resource::TYPE_BACKUP_POLICY,
Expand Down Expand Up @@ -1658,6 +1660,7 @@ protected function exportGroupProjects(int $batchSize, array $resources): void
previous: $e
));
}

}

private function exportServices(): void
Expand Down Expand Up @@ -1714,6 +1717,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
*/
Expand Down Expand Up @@ -2630,6 +2655,11 @@ private function reportIntegrations(array $resources, array &$report, array $res
$report[Resource::TYPE_WEBHOOK] = 0;
}
}

if (\in_array(Resource::TYPE_SMTP, $resources)) {
// Singleton — one SMTP config per project.
$report[Resource::TYPE_SMTP] = 1;
}
}

/**
Expand Down Expand Up @@ -2703,6 +2733,20 @@ protected function exportGroupIntegrations(int $batchSize, array $resources): vo
));
}
}

try {
if (\in_array(Resource::TYPE_SMTP, $resources)) {
$this->exportSMTP();
}
} catch (\Throwable $e) {
$this->addError(new Exception(
Resource::TYPE_SMTP,
Transfer::GROUP_INTEGRATIONS,
message: $e->getMessage(),
code: (int) $e->getCode() ?: Exception::CODE_INTERNAL,
previous: $e
));
}
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/Migration/Transfer.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class Transfer
Resource::TYPE_PLATFORM,
Resource::TYPE_API_KEY,
Resource::TYPE_WEBHOOK,
Resource::TYPE_SMTP,
];
public const GROUP_DOCUMENTSDB_RESOURCES = [
Resource::TYPE_DATABASE_DOCUMENTSDB,
Expand Down Expand Up @@ -144,6 +145,7 @@ class Transfer
Resource::TYPE_PLATFORM,
Resource::TYPE_API_KEY,
Resource::TYPE_WEBHOOK,
Resource::TYPE_SMTP,

// Project
Resource::TYPE_PROJECT_VARIABLE,
Expand Down
1 change: 1 addition & 0 deletions tests/Migration/Unit/Adapters/MockDestination.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public static function getSupportedResources(): array
Resource::TYPE_MEMBERSHIP,
Resource::TYPE_PLATFORM,
Resource::TYPE_API_KEY,
Resource::TYPE_SMTP,
Resource::TYPE_PROJECT_VARIABLE,
Resource::TYPE_WEBHOOK,
Resource::TYPE_AUTH_METHODS,
Expand Down
1 change: 1 addition & 0 deletions tests/Migration/Unit/Adapters/MockSource.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ public static function getSupportedResources(): array
Resource::TYPE_MEMBERSHIP,
Resource::TYPE_PLATFORM,
Resource::TYPE_API_KEY,
Resource::TYPE_SMTP,
Resource::TYPE_PROJECT_VARIABLE,
Resource::TYPE_WEBHOOK,
Resource::TYPE_AUTH_METHODS,
Expand Down
Loading