import path from "node:path"; import { composeUp } from "./docker"; import { readEnvFile, readEnvValue, upsertEnvValues } from "./env-file"; import type { ResolvedEmailSettings } from "./global-settings"; import { listBusinessStacks, stackDir } from "./stacks"; export interface EmailRolloutResult { slug: string; ok: boolean; error?: string; } /** * Pushes global email settings into every business's own `.env` (the * same surgical, non-destructive `upsertEnvValues` helper * provisioning.ts's own updateBusiness already uses for name/domain * edits — never a full-file regenerate, so nothing else hand-edited into * a business's .env is touched), then force-recreates its container so * the change actually takes effect — a container only reads its `.env` * at creation (see docker.ts's composeUp), the same reason * updateBusiness recreates on a domain change. * * EMAIL_REPLY_TO is deliberately excluded from the "always overwrite" * set: unlike the SMTP transport itself (host/credentials/from-address), * a reply-to address is something a business plausibly wants to be its * own (e.g. its own support inbox) rather than a platform-wide value. * The global setting is only ever applied as a *fallback* — written the * first time (a fresh business's .env has it blank), left untouched * forever after a business (or a human editing its .env by hand) sets * its own. This mirrors updateBusiness()'s own "never clobber anything * hand-edited since provisioning" rule. * * Runs sequentially, not in parallel: this is an infrequent admin action * on a single-VPS, modest-business-count deployment, and sequential * keeps one slow/failing business from making the Docker daemon/MySQL * connection pool contend with N simultaneous `docker compose up`s. A * per-business result is returned so a partial failure (one business's * container already down, a stack in a broken state, etc.) is visible * rather than silently swallowed — the caller can offer a "Reapply" * action that just calls this again. */ export async function applyEmailSettingsToAllBusinesses(settings: ResolvedEmailSettings): Promise { const businesses = await listBusinessStacks(); const results: EmailRolloutResult[] = []; for (const business of businesses) { try { const dir = stackDir(business.slug); const envPath = path.join(dir, ".env"); const currentEnv = await readEnvFile(envPath); const currentReplyTo = currentEnv ? readEnvValue(currentEnv, "EMAIL_REPLY_TO") : null; const businessHasOwnReplyTo = Boolean(currentReplyTo && currentReplyTo.trim()); await upsertEnvValues(envPath, { EMAIL_PROVIDER: "brevo", EMAIL_SMTP_HOST: settings.host, EMAIL_SMTP_PORT: String(settings.port), EMAIL_SMTP_USERNAME: settings.username, EMAIL_SMTP_PASSWORD: settings.password, EMAIL_SMTP_ENCRYPTION: settings.encryption, EMAIL_FROM_ADDRESS: settings.fromAddress, EMAIL_FROM_NAME: settings.fromName, ...(businessHasOwnReplyTo ? {} : { EMAIL_REPLY_TO: settings.replyTo }), }); await composeUp(dir, { forceRecreate: true }); results.push({ slug: business.slug, ok: true }); } catch (error) { results.push({ slug: business.slug, ok: false, error: error instanceof Error ? error.message : String(error) }); } } return results; }