diff --git a/apps/api/src/modules/mail/admin/admin.controller.ts b/apps/api/src/modules/mail/admin/admin.controller.ts index 9da4b850a..8f5f2a023 100644 --- a/apps/api/src/modules/mail/admin/admin.controller.ts +++ b/apps/api/src/modules/mail/admin/admin.controller.ts @@ -765,6 +765,24 @@ export async function restartAllComponentsHandler(c: Context) { } } +export async function migrateToContainerHandler(c: Context) { + const guard = assertNotCloud(c); + if (guard) return guard; + const serverId = param(c, "serverId"); + await permission.assert(getRequestContext(c), { resourceType: "mail_server", resourceId: serverId, action: "admin" }); + const ctx = getRequestContext(c); + if (!(await isServerInOrg(ctx, serverId))) { + return c.json({ error: "Server not found" }, 404); + } + try { + const { migrateMailEngineToContainer } = await import("./migrate-engine.service"); + const result = await migrateMailEngineToContainer(serverId); + return c.json(result); + } catch (err) { + return errorJson(c, err); + } +} + export async function getComponentLogsHandler(c: Context) { const guard = assertNotCloud(c); if (guard) return guard; diff --git a/apps/api/src/modules/mail/admin/migrate-engine.service.ts b/apps/api/src/modules/mail/admin/migrate-engine.service.ts new file mode 100644 index 000000000..aab0155e4 --- /dev/null +++ b/apps/api/src/modules/mail/admin/migrate-engine.service.ts @@ -0,0 +1,41 @@ +/** + * Service to migrate a legacy host-native mail engine to a containerized engine. + */ + +import { detectMailEngine, ensureContainerMail } from "@repo/adapters"; +import { sshManager } from "../../../lib/ssh-manager"; +import { readState } from "../mail-state"; + +export async function migrateMailEngineToContainer( + serverId: string, +): Promise<{ migrated: boolean; message: string }> { + return sshManager.withExecutor(serverId, async (executor) => { + const probe = await detectMailEngine(executor); + + if (probe.flavor === "container") { + return { migrated: false, message: "Mail server is already running in container mode." }; + } + + const state = await readState(executor); + if (!state) { + throw new Error("Mail server state not found — cannot migrate."); + } + + // 1. Stop and disable legacy host-native mail daemons + await executor.exec("systemctl stop postfix dovecot amavis iredapd 2>/dev/null || true"); + await executor.exec("systemctl disable postfix dovecot amavis iredapd 2>/dev/null || true"); + + // 2. Provision and start the containerized mail engine + DB sidecar + const logs: string[] = []; + const result = await ensureContainerMail(executor, { + domain: state.domain, + secrets: state.secrets ?? {}, + onLog: (l) => logs.push(l.message), + }); + + return { + migrated: true, + message: `Mail server migrated successfully to container mode (image: ${result.image}).`, + }; + }); +} diff --git a/apps/api/src/modules/mail/mail.routes.ts b/apps/api/src/modules/mail/mail.routes.ts index fdd0fd27b..70db3a491 100644 --- a/apps/api/src/modules/mail/mail.routes.ts +++ b/apps/api/src/modules/mail/mail.routes.ts @@ -203,6 +203,11 @@ r.post( { tag: "mail_server:admin" }, admin.restartAllComponentsHandler, ); +r.post( + "/admin/:serverId/migrate-to-container", + { tag: "mail_server:admin" }, + admin.migrateToContainerHandler, +); r.post( "/admin/:serverId/components/:key/:action", { tag: "mail_server:admin" }, diff --git a/apps/dashboard/src/app/(dashboard)/emails/_components/admin/advanced-tab.tsx b/apps/dashboard/src/app/(dashboard)/emails/_components/admin/advanced-tab.tsx index 457839bf2..3440b1518 100644 --- a/apps/dashboard/src/app/(dashboard)/emails/_components/admin/advanced-tab.tsx +++ b/apps/dashboard/src/app/(dashboard)/emails/_components/admin/advanced-tab.tsx @@ -486,8 +486,8 @@ function MailStackToolsSection({ serverId }: { serverId: string }) {
-+ Convert a legacy host-native (bare-metal systemd) mail installation to a containerized Docker engine with Postgres sidecar without losing accounts or mail. +
+