import { createHash, randomBytes, randomUUID, scryptSync, timingSafeEqual } from "crypto";
import { Resolver } from "dns/promises";
import type { Pool, PoolConnection, ResultSetHeader, RowDataPacket } from "mysql2/promise";
import { after } from "next/server";
import { assertManagedMemberLoginAvailable } from "@/lib/billing-member-access";
import { ensureOfficialMailSchema, getDbPool } from "@/lib/db";
import { normalizeSupportedLocale, type SupportedLocale } from "@/lib/i18n-config";
import {
  resolveComposeAttachmentMode,
  resolveComposeSubjectFromKnownAttachments,
  type ComposeAttachmentMode,
} from "@/lib/mail-attachment-policy";
import { hasActiveGrowthPlanByOwnerEmail } from "@/lib/mail-billing-access";
import { PRIVACY_POLICY_VERSION, TERMS_VERSION } from "@/lib/legal-consent";
import { recordMarketingLifecycleEvent } from "@/lib/marketing-analytics";
import { renderFindIdEmail } from "@/lib/find-id-email";
import { getMailAbsoluteUrl } from "@/lib/mail-urls";
import {
  renderDomainConnectionCompletedEmail,
  renderSystemWelcomeEmail
} from "@/lib/system-lifecycle-email";
import { isValidDomainInput, getWwwDomainSuggestion, hasWwwDomainPrefix } from "@/lib/domain-input";
import {
  buildComposeRawAttachmentsForUserEmail,
  appendLargeComposeAttachmentLinks,
  cleanupComposeUploadArtifacts,
  finalizeComposeInlineImagesForUserEmail,
  getFirstComposeAttachmentFilenameForUserEmail,
  getLargeComposeAttachmentsForUserEmail,
  getLinkedComposeUploadAttachmentPayloadByMessageId,
  getLinkedComposeUploadAttachmentsByMessageId,
  getPublishedLargeComposeAttachmentsByTokens,
  linkComposeUploadIdsToMessageId,
  markComposeUploadsDeletedByMessageIds,
  relinkComposeUploadsToMessageId,
  storeLargeComposeAttachmentsForUserEmail,
  type ComposeUploadCleanupTarget,
  type ComposeUploadFinalizeProgressSnapshot,
  type ComposeRawAttachment
} from "@/lib/mail-compose-uploads";
import { buildMailboxHtmlDisplay, plainTextToHtml, sanitizeMailboxHtml } from "@/lib/mail-html";
import {
  getOfficialLargeAttachmentTokens,
  hasUnresolvedOfficialLargeAttachmentLinks,
  stripDisplayedLargeAttachmentSections,
} from "@/lib/mail-large-attachment-links";
import {
  detectMailboxSpamLanguage,
  MAILBOX_SPAM_LANGUAGES,
  normalizeMailboxSpamLanguages,
  type MailboxSpamLanguage
} from "@/lib/mail-spam-language";
import { isValidMailboxLocalPart, normalizeMailboxLocalPart } from "@/lib/mailbox-address";
import { createMailboxSnippet, shouldRepairMailboxSnippet } from "@/lib/mail-snippet";
import {
  createMailcowRepairPassword,
  isMailcowPasswordComplexityError,
} from "@/lib/mailbox-password-repair";
import { hasOperationalBenefitByOwnerUserId } from "@/lib/mail-operational-benefit";
import { getPublicMailTransportErrorMessage } from "@/lib/mail-error-messages";
import { triggerMailboxPostSyncByOwnerEmail } from "@/lib/mail-automation";
import {
  createInternalMailboxCredentialPassword,
  disableLegacyNativeAppCompatibility,
  disableMailboxExternalAccessByEmail,
  disableMailboxPop3AccessByEmail,
  revokeNativeMailboxAppPassword,
  synchronizeMailboxExternalAccessByEmail,
} from "@/lib/mail-external-access";
import {
  DEFAULT_MAIL_LAYOUT_PREFERENCES,
  normalizeMailLayoutPreferences,
  type MailListSortOrder,
  type MailLayoutPreferences
} from "@/lib/mail-layout";
import { getCachedMailboxAttachmentPayload, getMailboxMessageAttachmentMetadata } from "@/lib/mailbox-attachment-cache";
import { enqueueMailboxAttachmentStorageDeletesByMessageIds } from "@/lib/mailbox-file-drawer";
import { normalizeMailboxPageSize } from "@/lib/mailbox-page-size";
import {
  appendMessageToRemoteFolder,
  appendDraftMessage,
  appendSentMessage,
  classifyBodyInlineAttachments,
  createRemoteMailboxFolder,
  deleteRemoteMailboxFolder,
  decryptMailboxPassword,
  deleteRemoteMessages,
  encryptMailboxPassword,
  extractAttachmentPayloadsFromRawSource,
  fetchRemoteMailboxMessage,
  getRemoteMailboxFolderBackupItems,
  getRemoteMailboxFolderStatuses,
  getRemoteMailboxSyncLimit,
  renameRemoteMailboxFolder,
  moveRemoteMessage,
  sendSmtpMessage,
  syncRemoteMailbox,
  streamRemoteMailboxFolderBackupSources,
  updateRemoteMessageFlag,
  type RemoteMailboxAttachmentMeta,
  type RemoteMailboxAttachmentPayload,
  type RemoteMailboxBackupSource,
  type RemoteMailboxMessage,
  type RemoteMailboxMoveResult,
  type RemoteMailboxFolderStatus
} from "@/lib/mailbox-remote";
import {
  getPreloadedMailSendingServer,
  type PreloadedMailSendingServer,
} from "@/lib/mail-sending-servers";
import {
  assertMailcowPasswordPolicy,
  ensureMailcowDkimRecord,
  ensureMailcowDomain,
  ensureMailcowMailbox,
  ensureMailcowMailboxAccount,
  getMailcowMailboxQuota,
  ensureMailcowProvisioning,
  isMailcowProvisioned,
  regenerateMailcowDkim,
  type MailcowMailboxQuotaSnapshot
} from "@/lib/mailcow";

const MX_HOSTNAME = process.env.MX_HOSTNAME ?? "mx1.officialsite.kr";
const DMARC_RUA = process.env.DMARC_RUA ?? "mailto:dmarc@officialsite.kr";
const SPF_RECORD_VALUE = `v=spf1 include:${MX_HOSTNAME} -all`;
const DEFAULT_DOMAIN_VERIFICATION_DNS_SERVERS = ["1.1.1.1", "8.8.8.8"];
const configuredDomainVerificationDnsServers = (
  process.env.DOMAIN_VERIFICATION_DNS_SERVERS ?? DEFAULT_DOMAIN_VERIFICATION_DNS_SERVERS.join(",")
)
  .split(",")
  .map((server) => server.trim())
  .filter(Boolean);
const DOMAIN_VERIFICATION_DNS_SERVERS =
  configuredDomainVerificationDnsServers.length > 0
    ? configuredDomainVerificationDnsServers
    : DEFAULT_DOMAIN_VERIFICATION_DNS_SERVERS;
const MAX_COMPANY_NAME_LENGTH = 191;
const MAX_DOMAIN_LENGTH = 253;
const MAX_EMAIL_ADDRESS_LENGTH = 320;
const MAILBOX_AUTO_REFRESH_MAX_AGE_MS = 30_000;
const MAILBOX_FOLDER_STATUS_CACHE_TTL_MS = 45_000;
const MAILBOX_SYNC_WRITE_RETRY_DELAYS_MS = [120, 360, 900, 1_800] as const;
const MAILBOX_TRANSPORT_RETRY_DELAYS_MS = [500, 1_500] as const;
const MAILBOX_DISTRIBUTED_SYNC_LOCK_TIMEOUT_SECONDS = 0;
const MAILBOX_SYNC_OBSERVE_TIMEOUT_MS = 15_000;
const MAILBOX_SYNC_OBSERVE_INTERVAL_MS = 150;
const MAILBOX_HYDRATION_OBSERVE_TIMEOUT_MS = 60_000;
const MAILBOX_CONTENT_HYDRATION_LIMIT = getRemoteMailboxSyncLimit();
const MAILBOX_BACKUP_PART_SIZE_BYTES = 2 * 1024 * 1024 * 1024;
const FREE_PLAN_DAILY_SEND_LIMIT = 30;
const OUTBOUND_DELIVERY_LOCK_NAME = "official-mail-outbound-delivery";
const DEFAULT_OUTBOUND_DELIVERY_BATCH_SIZE = 3;
const GROWTH_PLAN_BURST_SEND_LIMIT = 10;
const GROWTH_PLAN_BURST_WINDOW_MS = 60 * 1000;
const GROWTH_PLAN_SEND_COOLDOWN_MS = 60 * 1000;
const SIGNUP_RECOVERY_EMAIL_CODE_TTL_MS = 3 * 60 * 1000;
const SIGNUP_RECOVERY_EMAIL_RESEND_COOLDOWN_MS = 10 * 1000;
const PASSWORD_RESET_TOKEN_TTL_MS = 5 * 60 * 1000;
const MAILBOX_PROVISIONING_RECOVERY_GRACE_MS = 2 * 60 * 1000;
const MAILBOX_PROVISIONING_RECOVERY_STALE_MINUTES = 10;
const MAILBOX_PROVISIONING_RECOVERY_MAX_BATCH_SIZE = 10;
const SIGNUP_RECOVERY_EMAIL_SUBJECT = "[오피셜메일] 보조 이메일 인증번호";
const PASSWORD_RESET_LINK_SUBJECT = "[오피셜메일] 비밀번호 재설정 링크";
const MAILCOW_QUOTA_COLD_WAIT_MS = Math.max(50, Number(process.env.MAILCOW_QUOTA_COLD_WAIT_MS ?? "200") || 200);
const MAILCOW_QUOTA_CACHE_TTL_MS = 60_000;
const ATTACHMENT_MESSAGE_REGEXP = "pdf|첨부|제안서|계약서|정리";
const LEGACY_SYSTEM_WELCOME_SUBJECT = "오피셜메일에 오신 것을 환영합니다";
const SYSTEM_DELIVERY_FAILURE_PREFIX = "[전송 실패]";
const CUSTOM_FOLDER_PREFIX = "custom__";
const MAILBOX_FOLDER_HIERARCHY_SEPARATOR = "/";
const LOCAL_MAILBOX_FOLDERS: LocalFolderDefinition[] = [
  { systemName: "inbox", displayName: "받은메일" },
  { systemName: "sent", displayName: "보낸메일" },
  { systemName: "drafts", displayName: "임시보관함" },
  { systemName: "spam", displayName: "스팸함" },
  { systemName: "trash", displayName: "휴지통" }
];

// A browser refresh, the page-load refresh, and IMAP IDLE can all request the
// same cache write. Keep each mailbox's database sync ordered in this process.
const mailboxSyncQueues = new Map<number, Promise<void>>();
type MailboxRemoteFolderStatusSnapshot = {
  changedFolders: string[];
  folders: RemoteMailboxFolderStatus[];
};
type MailboxFolderStatusCacheEntry = {
  expiresAt: number;
  inFlight: Promise<MailboxRemoteFolderStatusSnapshot> | null;
  value: MailboxRemoteFolderStatusSnapshot | null;
};
const mailboxFolderStatusCache = new Map<number, MailboxFolderStatusCacheEntry>();
type MailboxQuotaCacheEntry = {
  expiresAt: number;
  refresh: Promise<MailcowMailboxQuotaSnapshot | null> | null;
  value: MailcowMailboxQuotaSnapshot | null;
};
const mailboxQuotaCache = new Map<string, MailboxQuotaCacheEntry>();

async function getCachedMailcowMailboxQuota(email: string) {
  const normalizedEmail = email.trim().toLowerCase();
  const now = Date.now();
  let entry = mailboxQuotaCache.get(normalizedEmail);

  if (entry && entry.expiresAt > now) {
    return entry.value;
  }

  if (!entry) {
    entry = { expiresAt: 0, refresh: null, value: null };
    mailboxQuotaCache.set(normalizedEmail, entry);
  }

  if (!entry.refresh) {
    const target = entry;
    target.refresh = getMailcowMailboxQuota(normalizedEmail)
      .catch(() => null)
      .then((value) => {
        target.value = value;
        target.expiresAt = Date.now() + MAILCOW_QUOTA_CACHE_TTL_MS;
        return value;
      })
      .finally(() => {
        target.refresh = null;
      });
  }

  if (entry.expiresAt > 0) {
    return entry.value;
  }

  return Promise.race<MailcowMailboxQuotaSnapshot | null>([
    entry.refresh,
    new Promise<null>((resolve) => {
      setTimeout(() => resolve(null), MAILCOW_QUOTA_COLD_WAIT_MS);
    })
  ]);
}

export type SessionSnapshot = {
  email: string;
  companyName: string;
  displayName: string;
  mailConfigured: boolean;
  domain: string;
  mailbox: string;
  preferredLocale: SupportedLocale | null;
};

export type RecoveryEmailOverview = {
  email: string;
  verified: boolean;
};

export type SignupProvisioningStep = "account" | "domain" | "mailbox" | "dkim" | "welcome";

export type SignupProvisioningProgress = {
  description: string;
  status: "running" | "complete";
  step: SignupProvisioningStep;
  title: string;
};

export type PendingSignupRegistration = {
  companyName: string;
  displayName: string;
  domain: string;
  domainId: number;
  localPart: string;
  mailboxEmail: string;
  mailboxId: number;
  mailboxPassword: string;
  restoredFromCleanup: boolean;
  snapshot: SessionSnapshot;
  userId: number;
};

type MailboxProvisioningRecoveryJobRow = RowDataPacket & {
  attempt_count: number;
  company_name: string;
  display_name: string;
  domain: string;
  domain_id: number;
  job_id: number;
  local_part: string;
  mailbox_email: string;
  mailbox_id: number;
  mailcow_cleanup_at: Date | null;
  owner_user_id: number;
  password_ciphertext: string | null;
};

export type DnsRecordItem = {
  type: string;
  host: string;
  value: string;
  priority: number | null;
  status: "pending" | "verified" | "failed";
  lastCheckedAt: string | null;
};

export type MailSetupOverview = {
  domain: string;
  mailbox: string;
  localPart: string;
  status: "pending" | "verified" | "failed";
  mailConfigured: boolean;
  dkimEnabled: boolean;
  mxHostname: string;
  dkimSelector: string;
  dkimPublicKey: string;
  verifiedAt: string | null;
  dnsRecords: DnsRecordItem[];
};

export type MailFolderSummary = {
  id: number;
  kind: "custom" | "system";
  systemName: string;
  name: string;
  parentSystemName: string | null;
  count: number;
  unreadCount: number;
};

export type MailboxSenderMoveRule = {
  createdAt: string;
  id: number;
  senderAddress: string;
  targetFolderId: number;
  targetFolderName: string;
  targetFolderSystemName: string;
  updatedAt: string;
};

export type MailboxMoveRuleSettings = {
  folders: Array<Pick<MailFolderSummary, "id" | "name" | "systemName">>;
  limit: number;
  rules: MailboxSenderMoveRule[];
};

export type MailboxContact = {
  autoMoveFolderName: string | null;
  autoMoveFolderSystemName: string | null;
  companyName: string;
  createdAt: string;
  displayName: string;
  email: string;
  groupName: string;
  id: number | null;
  isSaved: boolean;
  isVip: boolean;
  memo: string;
  phone: string;
  updatedAt: string;
};

export type MailboxContactSettings = {
  contacts: MailboxContact[];
  folders: Array<Pick<MailFolderSummary, "id" | "name" | "systemName">>;
  groups: string[];
  limit: number;
};

export type MailboxContactInput = {
  companyName?: string;
  displayName?: string;
  email: string;
  groupName?: string;
  isVip?: boolean;
  memo?: string;
  phone?: string;
};

export type MailboxContactImportCandidate = MailboxContactInput & {
  interactionCount: number;
  lastInteractedAt: string;
};

export type MailboxContactDuplicateGroup = {
  contacts: MailboxContact[];
  key: string;
  reason: "name" | "phone";
};

export type MailboxSpamRuleAction = "allow" | "spam";
export type MailboxCustomSpamRuleField = "body" | "sender" | "sender_domain" | "subject";
export type { MailboxSpamLanguage } from "@/lib/mail-spam-language";

export type MailboxSenderDisposition = {
  action: MailboxSpamRuleAction;
  createdAt: string;
  id: number;
  senderAddress: string;
  updatedAt: string;
};

export type MailboxCustomSpamRule = {
  action: MailboxSpamRuleAction;
  createdAt: string;
  enabled: boolean;
  field: MailboxCustomSpamRuleField;
  id: number;
  priority: number;
  updatedAt: string;
  value: string;
};

export type MailboxSpamGeneralSettings = {
  autoMoveSpam: boolean;
  blockReportedSender: boolean;
  detectBounceSpoofing: boolean;
  detectMissingRecipient: boolean;
  detectSelfSpoofing: boolean;
  detectUnknownSender: boolean;
  expiredMessageAction: "delete" | "trash";
  hideRemoteContent: boolean;
  hideSpamFolder: boolean;
  languageFilterLanguages: MailboxSpamLanguage[];
  languageFilterMode: "all" | "selected";
  languageFilterEnabled: boolean;
  processPreviousReported: boolean;
  reportedMessageAction: "delete" | "spam";
  retentionDays: number;
};

export const DEFAULT_MAILBOX_SPAM_GENERAL_SETTINGS: MailboxSpamGeneralSettings = {
  autoMoveSpam: true,
  blockReportedSender: true,
  detectBounceSpoofing: false,
  detectMissingRecipient: false,
  detectSelfSpoofing: true,
  detectUnknownSender: false,
  expiredMessageAction: "delete",
  hideRemoteContent: true,
  hideSpamFolder: false,
  languageFilterLanguages: ["en"],
  languageFilterMode: "selected",
  languageFilterEnabled: false,
  processPreviousReported: true,
  reportedMessageAction: "spam",
  retentionDays: 30
};

export type MailboxSpamRuleSettings = {
  customRules: MailboxCustomSpamRule[];
  general: MailboxSpamGeneralSettings;
  limit: number;
  senderRules: MailboxSenderDisposition[];
};

export type MailboxSenderProfile = {
  contact: MailboxContact | null;
  disposition: MailboxSpamRuleAction | null;
  folders: Array<Pick<MailFolderSummary, "id" | "name" | "systemName">>;
  moveRule: MailboxSenderMoveRule | null;
  moveFolderCounts?: Array<{ folderSystemName: string; messageCount: number; unreadCount: number }>;
  senderAddress: string;
};

export type MailboxStorageFolder = {
  estimatedBytes: number;
  id: number;
  kind: "custom" | "system";
  messageCount: number;
  name: string;
  systemName: string;
  unreadCount: number;
};

export type MailboxStorageOverview = {
  folders: MailboxStorageFolder[];
  isUnlimited: boolean;
  limitBytes: number | null;
  usagePercent: number | null;
  usedBytes: number | null;
};

export type MailboxFolderBackupPart = {
  fileName: string;
  index: number;
  messageCount: number;
  sizeBytes: number;
};

export type MailboxFolderBackupManifest = {
  folderLabel: string;
  folderSystemName: string;
  messageCount: number;
  partSizeBytes: number;
  parts: MailboxFolderBackupPart[];
  totalBytes: number;
};

export type MailMessageFilter = "all" | "unread" | "important" | "attachments";
export type MailSearchField = "all" | "correspondent" | "from" | "to" | "subject" | "body";
export type MailAiSummaryStatus = "failed" | "sending" | "sent";

export type MailMessageFilterCounts = Record<MailMessageFilter, number>;

export type MailSearchSuggestion = {
  field: Exclude<MailSearchField, "all">;
  messageId: number;
  preview: string;
  receivedAt: string;
  value: string;
};

export type MailRecipientSuggestion = {
  displayName: string | null;
  email: string;
  kind: "history" | "member";
  label: string;
};

export type MailMessageSummary = {
  aiSummaryError: string | null;
  aiSummaryResponse: string | null;
  aiSummaryStatus: MailAiSummaryStatus | null;
  id: number;
  subject: string;
  fromName: string | null;
  fromAddress: string;
  toAddresses: string;
  senderContactName: string | null;
  senderIsVip: boolean;
  snippet: string;
  isRead: boolean;
  isStarred: boolean;
  receivedAt: string;
  direction: "inbound" | "outbound" | "draft";
  folderSystemName: string;
  folderName: string;
};

export type MailMessageDetail = MailMessageSummary & {
  attachments: MailMessageAttachment[];
  bodyHtml: string | null;
  bodyHtmlDisplay: string | null;
  bodyText: string;
  hasUnresolvedLargeAttachmentLinks: boolean;
  messageIdHeader: string | null;
  rawSourceAvailable: boolean;
  transportStatus: "saved" | "queued" | "sent" | "failed";
  transportResponse: string | null;
};

export type MailMessageAttachment = RemoteMailboxAttachmentMeta & {
  externalUrl?: string | null;
  isLargeAttachment?: boolean;
  isPendingPublication?: boolean;
};

export type MailDeliveryLog = {
  id: number;
  action: "send" | "draft" | "receive";
  status: "saved" | "queued" | "sent" | "failed";
  transport: string;
  sourceAddress: string;
  targetAddress: string;
  subject: string;
  messageIdHeader: string | null;
  responseText: string | null;
  createdAt: string;
};

export type MailMessageBodyView = {
  attachments: MailMessageAttachment[];
  bodyHtmlDisplay: string | null;
  bodyText: string;
  direction: "inbound" | "outbound" | "draft";
  fromAddress: string;
  fromName: string | null;
  id: number;
  receivedAt: string;
  subject: string;
  toAddresses: string;
};

export type MailAppOverview = {
  mailbox: string;
  mailboxQuotaBytes: number | null;
  mailboxQuotaIsUnlimited: boolean;
  mailboxQuotaUsagePercent: number | null;
  mailboxQuotaUsedBytes: number | null;
  domain: string;
  lastSyncAt: string | null;
  status: "pending_dns" | "active" | "disabled";
  syncError: string | null;
  spamSettings: MailboxSpamGeneralSettings;
  folders: MailFolderSummary[];
  currentFolder: string;
  messageFilter: MailMessageFilter;
  searchField: MailSearchField;
  searchQuery: string;
  sortOrder: MailListSortOrder;
  messageFilterCounts: MailMessageFilterCounts;
  currentPage: number;
  totalPages: number;
  totalMessages: number;
  pageSize: number;
  messages: MailMessageSummary[];
  selectedMessage: MailMessageDetail | null;
  selectedMessageLogs: MailDeliveryLog[];
};

export type MailboxFolderStatusSnapshot = {
  changedFolders: string[];
  currentFolderChanged: boolean;
  folders: Array<{
    count: number;
    name: string;
    systemName: string;
    unreadCount: number;
  }>;
  syncedFolders: string[];
};

type UserRow = RowDataPacket & {
  id: number;
  email: string;
  company_name: string;
  display_name: string;
  recovery_email: string | null;
  recovery_email_verified_at: Date | null;
  password_hash: string;
  mail_configured: number;
  mail_sidebar_width: number;
  mail_list_width: number;
  mail_list_height: number;
  mail_view_mode: string;
  mail_sort_order: string;
  preferred_locale: string | null;
};

type SignupRecoveryEmailVerificationRow = RowDataPacket & {
  id: number;
  email: string;
  code_hash: string;
  verification_token_hash: string | null;
  expires_at: Date;
  resend_available_at: Date;
  verified_at: Date | null;
  consumed_at: Date | null;
};

type PasswordResetTokenRow = RowDataPacket & {
  id: number;
  user_id: number;
  recovery_email: string;
  token_hash: string;
  expires_at: Date;
  used_at: Date | null;
};

type MailSetupRow = RowDataPacket & {
  domain_id: number;
  domain: string;
  domain_status: "pending" | "verified" | "failed";
  dkim_enabled: number;
  dkim_selector: string;
  dkim_public_key: string;
  mailcow_cleanup_at: Date | null;
  verified_at: Date | null;
  mailbox_email: string;
  local_part: string;
  mailbox_status: "pending_dns" | "active" | "disabled";
};

type DomainVerificationTargetRow = MailSetupRow & {
  owner_user_id: number;
};

type CleanedDomainSignupRow = RowDataPacket & {
  domain: string;
  domain_id: number;
  mailbox_email: string;
  mailbox_id: number;
  owner_email: string;
  owner_recovery_email: string | null;
  owner_user_id: number;
};

type RestorableManagedMemberRow = RowDataPacket & {
  display_name: string;
  email: string;
  local_part: string;
  password_ciphertext: string;
};

type DnsRecordRow = RowDataPacket & {
  record_type: string;
  host_name: string;
  value_text: string;
  priority: number | null;
  status: "pending" | "verified" | "failed";
  last_checked_at: Date | null;
};

type MailboxRow = RowDataPacket & PreloadedMailSendingServer & {
  id: number;
  email: string;
  last_sync_at: Date | null;
  last_sync_error: string | null;
  message_visibility_cutoff_at: Date | null;
  password_ciphertext: string | null;
  send_cooldown_until: Date | null;
  status: "pending_dns" | "active" | "disabled";
  domain: string;
};

type QueuedOutboundDeliveryRow = MailboxRow & {
  log_recipients: string;
  mailbox_message_id: number;
  message_id_header: string;
  raw_source: string;
  subject: string;
  visible_recipients: string;
};

type FolderSummaryRow = RowDataPacket & {
  id: number;
  last_synced_at: Date | null;
  system_name: string;
  name: string;
  remote_name: string | null;
  remote_total: number;
  remote_unseen: number;
};

type MailboxPendingMoveFolderRow = RowDataPacket & {
  message_count: number;
  requested_source_folder: string;
  source_folder: string;
  target_folder: string;
  unread_count: number;
};

type MailboxPendingMoveProjection = {
  affectedFolders: Set<string>;
  countDeltas: Map<string, { count: number; unreadCount: number }>;
};

const PENDING_MAILBOX_MOVE_EXCLUSION_SQL = `
  NOT EXISTS (
    SELECT 1
    FROM mailbox_message_move_job_items pending_move
    WHERE pending_move.mailbox_id = mm.mailbox_id
      AND pending_move.mailbox_message_id = mm.id
  )
`;

// Keep the remote identity unchanged while projecting durable queued moves
// into the destination folder, including across refreshes and other clients.
const PROJECTED_MAILBOX_FOLDER_JOIN_SQL = `
  LEFT JOIN mailbox_message_move_job_items projected_move_item
    ON projected_move_item.mailbox_id = mm.mailbox_id
    AND projected_move_item.mailbox_message_id = mm.id
  LEFT JOIN mailbox_message_move_jobs projected_move_job
    ON projected_move_job.id = projected_move_item.job_id
    AND projected_move_job.status IN ('pending', 'processing')
  INNER JOIN mailbox_folders original_folder ON original_folder.id = mm.folder_id
  INNER JOIN mailbox_folders f
    ON f.mailbox_id = mm.mailbox_id
    AND f.system_name = COALESCE(projected_move_job.target_folder, original_folder.system_name)
`;

const PROJECTED_MAILBOX_MESSAGE_VISIBLE_SQL = `
  NOT (
    projected_move_job.id IS NOT NULL
    AND projected_move_job.target_folder = 'trash'
    AND original_folder.system_name = 'trash'
  )
`;

type MailMessageCountRow = RowDataPacket & {
  all_count: number | null;
  unread_count: number | null;
  important_count: number | null;
  attachments_count: number | null;
};

type MailSearchSuggestionRow = RowDataPacket & {
  body_text: string;
  from_address: string;
  from_name: string | null;
  id: number;
  received_at: Date;
  snippet: string;
  subject: string;
  to_addresses: string;
};

type MailRecipientSuggestionRow = RowDataPacket & {
  from_address: string;
  from_name: string | null;
  received_at: Date;
  to_addresses: string;
};

type MailRecipientDirectoryRow = RowDataPacket & {
  display_name: string | null;
  email: string;
};

type MailMessageRow = RowDataPacket & {
  ai_summary_error: string | null;
  ai_summary_response: string | null;
  ai_summary_status: MailAiSummaryStatus | null;
  id: number;
  remote_folder: string | null;
  remote_uid: number | null;
  subject: string;
  from_name: string | null;
  from_address: string;
  to_addresses: string;
  snippet: string;
  body_text: string;
  body_html: string | null;
  message_id_header: string | null;
  raw_source: string | null;
  transport_status: "saved" | "queued" | "sent" | "failed";
  transport_response: string | null;
  remote_flags: string | null;
  is_read: number;
  is_starred: number;
  received_at: Date;
  direction: "inbound" | "outbound" | "draft";
  folder_system_name: string;
  folder_name: string;
  contact_display_name: string | null;
  sender_is_vip: number | null;
};

type MailMessageSummaryRow = RowDataPacket & {
  ai_summary_error: string | null;
  ai_summary_response: string | null;
  ai_summary_status: MailAiSummaryStatus | null;
  direction: "inbound" | "outbound" | "draft";
  folder_name: string;
  folder_system_name: string;
  from_address: string;
  from_name: string | null;
  id: number;
  is_read: number;
  is_starred: number;
  received_at: Date;
  remote_folder: string | null;
  remote_uid: number | null;
  snippet: string;
  snippet_body_html: string | null;
  snippet_body_text: string | null;
  subject: string;
  to_addresses: string;
  contact_display_name: string | null;
  sender_is_vip: number | null;
};

type MailMessageDetailRow = MailMessageRow & {
  raw_source_available: number;
};

type LocalFolderDefinition = {
  displayName: string;
  systemName: "inbox" | "sent" | "drafts" | "spam" | "trash";
};

type SupportedMailboxFolderSystemName = LocalFolderDefinition["systemName"];

type MailDeliveryLogRow = RowDataPacket & {
  id: number;
  action: "send" | "draft" | "receive";
  status: "saved" | "queued" | "sent" | "failed";
  transport: string;
  source_address: string;
  target_address: string;
  subject: string;
  message_id_header: string | null;
  response_text: string | null;
  created_at: Date;
};

type CountTotalRow = RowDataPacket & {
  total: number | null;
};

type MailboxSenderRuleRow = RowDataPacket & {
  created_at: Date;
  id: number;
  mailbox_id: number;
  sender_address: string;
  rule_action: MailboxSpamRuleAction;
  updated_at: Date;
};

type MailboxContactRow = RowDataPacket & {
  auto_move_folder_name: string | null;
  auto_move_folder_system_name: string | null;
  auto_move_rule_id?: number | null;
  company_name: string;
  created_at: Date;
  display_name: string;
  email: string;
  group_name: string;
  id: number;
  is_vip: number;
  memo: string | null;
  phone: string;
  updated_at: Date;
};

type MailboxCustomSpamRuleRow = RowDataPacket & {
  created_at: Date;
  enabled: number;
  id: number;
  match_field: MailboxCustomSpamRuleField;
  match_value: string;
  priority: number;
  rule_action: MailboxSpamRuleAction;
  updated_at: Date;
};

type MailboxSpamSettingsRow = RowDataPacket & {
  auto_move_spam: number;
  block_reported_sender: number;
  detect_bounce_spoofing: number;
  detect_missing_recipient: number;
  detect_self_spoofing: number;
  detect_unknown_sender: number;
  expired_message_action: "delete" | "trash";
  hide_remote_content: number;
  hide_spam_folder: number;
  language_filter_enabled: number;
  language_filter_languages: string;
  language_filter_mode: "all" | "selected";
  process_previous_reported: number;
  reported_message_action: "delete" | "spam";
  retention_days: number;
};

type MailboxStorageFolderRow = RowDataPacket & {
  estimated_bytes: number | string | null;
  id: number;
  message_count: number | string | null;
  name: string;
  system_name: string;
  unread_count: number | string | null;
};

type MailboxSenderMoveRuleRow = RowDataPacket & {
  created_at: Date;
  id: number;
  sender_address: string;
  target_folder_id: number;
  target_folder_name: string;
  target_folder_system_name: string;
  updated_at: Date;
};

type MailboxFolderRowBasic = RowDataPacket & {
  id: number;
  system_name: string;
  remote_name: string | null;
  name: string;
  sort_order: number;
};

type MailboxMoveMessageRow = RowDataPacket & {
  id: number;
  folder_id: number;
  direction: "draft" | "inbound" | "outbound";
  from_address: string;
  is_read?: number;
  remote_folder: string | null;
  remote_uid: number | null;
};

type MailboxSpamCandidateRow = MailboxMoveMessageRow & {
  body_text: string;
  subject: string;
  to_addresses: string;
};

type MailboxRemoteDuplicateRow = RowDataPacket & {
  id: number;
  folder_id: number;
};

type MailboxFolderMessageRow = RowDataPacket & {
  id: number;
  folder_id: number;
  remote_uid: number | null;
  remote_folder: string | null;
  direction: "draft" | "inbound" | "outbound";
  subject: string;
  from_name: string | null;
  from_address: string;
  to_addresses: string;
  snippet: string;
  body_text: string;
  body_html: string | null;
  message_id_header: string | null;
  raw_source: string | null;
  transport_status: "saved" | "queued" | "sent" | "failed";
  transport_response: string | null;
  remote_flags: string | null;
  is_read: number;
  is_starred: number;
  received_at: Date;
};

type MailboxMessageDeleteResult = {
  cleanupTargets: ComposeUploadCleanupTarget[];
  deletedCount: number;
  deletedMessageIds: number[];
  invalidatedFolderIds: number[];
};

type MailboxMoveActionResult = {
  deletedCount: number;
  deletedMessageIds: number[];
  movedCount: number;
  movedMessageIds: number[];
  spamSenderCount: number;
};

type MailboxMessageMoveJobRow = RowDataPacket & {
  attempt_count: number;
  completed_at: Date | null;
  email: string;
  id: number;
  last_error: string | null;
  mailbox_id: number;
  message_ids_json: string;
  release_spam: number;
  result_json: string | null;
  source_folder: string;
  status: "pending" | "processing" | "completed" | "failed";
  target_folder: string;
  undo_job_ids_json: string | null;
  undo_payload_json: string | null;
};

type MailboxMessageMoveUndoSnapshot = {
  messageId: number;
  sourceFolder: string;
};

export type MailboxMessageMoveJobStatus = {
  completedAt: string | null;
  error: string | null;
  id: number;
  result: MailboxMoveActionResult | null;
  status: "pending" | "processing" | "completed" | "failed";
};

function normalizeDate(value: Date | null) {
  return value ? value.toISOString() : null;
}

function isSystemMailboxFolderSystemName(value: string | undefined | null): value is SupportedMailboxFolderSystemName {
  return value === "inbox" || value === "sent" || value === "drafts" || value === "spam" || value === "trash";
}

function isCustomMailboxFolderSystemName(value: string | undefined | null): value is string {
  return typeof value === "string" && value.startsWith(CUSTOM_FOLDER_PREFIX);
}

function folderKindForSystemName(systemName: string) {
  return isCustomMailboxFolderSystemName(systemName) ? ("custom" as const) : ("system" as const);
}

function createCustomFolderSystemName() {
  return `${CUSTOM_FOLDER_PREFIX}${randomUUID()}`;
}

function deriveDisplayName(companyName: string, email: string) {
  return companyName.trim() || email.split("@")[0] || "대표";
}

function normalizeCustomFolderName(value: string) {
  return value.replace(/\s+/g, " ").trim().slice(0, 80);
}

function assertCustomFolderLeafName(value: string) {
  if (value.includes("/") || value.includes("\\")) {
    throw new Error("folder-name-invalid");
  }
}

function mailboxFolderRemoteName(folder: Pick<MailboxFolderRowBasic, "name" | "remote_name">) {
  return (folder.remote_name || folder.name).trim();
}

function isMailboxFolderDescendantRemoteName(remoteName: string, parentRemoteName: string) {
  return remoteName.toLowerCase().startsWith(
    `${parentRemoteName}${MAILBOX_FOLDER_HIERARCHY_SEPARATOR}`.toLowerCase(),
  );
}

function mailboxFolderParentSystemName(
  folder: Pick<MailboxFolderRowBasic, "name" | "remote_name" | "system_name">,
  folders: Array<Pick<MailboxFolderRowBasic, "name" | "remote_name" | "system_name">>,
) {
  if (!isCustomMailboxFolderSystemName(folder.system_name)) {
    return null;
  }

  const remoteName = mailboxFolderRemoteName(folder);
  const separatorIndex = remoteName.lastIndexOf(MAILBOX_FOLDER_HIERARCHY_SEPARATOR);

  if (separatorIndex <= 0) {
    return null;
  }

  const parentRemoteName = remoteName.slice(0, separatorIndex).toLowerCase();
  return folders.find(
    (candidate) =>
      candidate.system_name !== folder.system_name &&
      isCustomMailboxFolderSystemName(candidate.system_name) &&
      mailboxFolderRemoteName(candidate).toLowerCase() === parentRemoteName,
  )?.system_name ?? null;
}

type NormalizedSignupRegistrationInput = {
  companyName: string;
  domain: string;
  localPart: string;
  mailboxEmail: string;
  password: string;
  recoveryEmail: string;
  recoveryVerificationId: number;
  recoveryVerificationToken: string;
  termsAccepted: true;
};

function normalizeSignupRegistrationInput(input: {
  companyName: string;
  domain: string;
  localPart: string;
  password: string;
  recoveryEmail: string;
  recoveryVerificationId: number;
  recoveryVerificationToken: string;
  termsAccepted: boolean;
}): NormalizedSignupRegistrationInput {
  const companyName = input.companyName.trim();
  const domain = input.domain
    .trim()
    .toLowerCase()
    .replace(/^https?:\/\//, "")
    .replace(/\/$/, "");
  const localPart = normalizeMailboxLocalPart(input.localPart);
  const password = input.password.trim();
  const recoveryEmail = input.recoveryEmail.trim().toLowerCase();
  const recoveryVerificationId = Number(input.recoveryVerificationId);
  const recoveryVerificationToken = input.recoveryVerificationToken.trim();
  const mailboxEmail = `${localPart}@${domain}`;

  if (!companyName || !domain || !localPart || !password || !recoveryEmail) {
    throw new Error("required");
  }

  if (companyName.length > MAX_COMPANY_NAME_LENGTH) {
    throw new Error("company-name-too-long");
  }

  if (domain.length > MAX_DOMAIN_LENGTH || mailboxEmail.length > MAX_EMAIL_ADDRESS_LENGTH) {
    throw new Error("domain-too-long");
  }

  if (hasWwwDomainPrefix(input.domain)) {
    throw new Error(`domain-www-prefix:${getWwwDomainSuggestion(input.domain)}`);
  }

  if (!isValidDomainInput(input.domain)) {
    throw new Error("invalid-domain");
  }

  if (!isValidMailboxLocalPart(localPart)) {
    throw new Error("invalid-local-part");
  }

  if (recoveryEmail.length > MAX_EMAIL_ADDRESS_LENGTH || !RECIPIENT_ADDRESS_PATTERN.test(recoveryEmail)) {
    throw new Error("recovery-email-invalid");
  }

  if (!Number.isInteger(recoveryVerificationId) || recoveryVerificationId <= 0 || !recoveryVerificationToken) {
    throw new Error("recovery-email-unverified");
  }

  if (input.termsAccepted !== true) {
    throw new Error("terms-required");
  }

  return {
    companyName,
    domain,
    localPart,
    mailboxEmail,
    password,
    recoveryEmail,
    recoveryVerificationId,
    recoveryVerificationToken,
    termsAccepted: true
  };
}

function escapeHeaderValue(value: string) {
  return value.replace(/[\r\n]+/g, " ").trim();
}

function hasNonAscii(value: string) {
  return /[^\x20-\x7E]/.test(value);
}

function encodeMimeHeaderWord(value: string) {
  const safeValue = escapeHeaderValue(value);

  if (!safeValue) {
    return "";
  }

  if (!hasNonAscii(safeValue)) {
    return safeValue;
  }

  return `=?UTF-8?B?${Buffer.from(safeValue, "utf8").toString("base64")}?=`;
}

function formatMailboxHeader(name: string | null, address: string) {
  const safeAddress = escapeHeaderValue(address);

  if (!name?.trim()) {
    return `<${safeAddress}>`;
  }

  const safeName = escapeHeaderValue(name);

  if (hasNonAscii(safeName)) {
    return `${encodeMimeHeaderWord(safeName)} <${safeAddress}>`;
  }

  return `"${safeName.replace(/"/g, '\\"')}" <${safeAddress}>`;
}

function filterVisibleRemoteMailboxMessages(messages: RemoteMailboxMessage[], cutoffAt: Date | null) {
  if (!cutoffAt) {
    return messages;
  }

  const cutoffTime = cutoffAt.getTime();
  return messages.filter((message) => message.receivedAt.getTime() > cutoffTime);
}

function createMessageIdHeader(domain: string) {
  return `<${randomUUID()}@${domain}>`;
}

function formatMimeFilenameParameter(name: string, value: string) {
  const safeValue = escapeHeaderValue(value);

  if (!safeValue) {
    return null;
  }

  if (!hasNonAscii(safeValue)) {
    return `${name}="${safeValue.replace(/"/g, '\\"')}"`;
  }

  return `${name}*=UTF-8''${encodeURIComponent(safeValue)}`;
}

function encodeBase64Lines(buffer: Buffer) {
  return buffer.toString("base64").replace(/(.{76})/g, "$1\r\n");
}

function encodeBase64TextLines(value: string) {
  return encodeBase64Lines(Buffer.from(value, "utf8"));
}

function buildTextPlainPartLines(bodyText: string) {
  return [
    "Content-Type: text/plain; charset=UTF-8",
    "Content-Transfer-Encoding: base64",
    "",
    encodeBase64TextLines(bodyText)
  ];
}

function buildAlternativePartLines(bodyText: string, bodyHtml: string) {
  const boundary = `----=_OfficialMail_Alt_${randomUUID()}`;

  return [
    `Content-Type: multipart/alternative; boundary="${boundary}"`,
    "",
    `--${boundary}`,
    ...buildTextPlainPartLines(bodyText),
    "",
    `--${boundary}`,
    "Content-Type: text/html; charset=UTF-8",
    "Content-Transfer-Encoding: base64",
    "",
    encodeBase64TextLines(bodyHtml),
    "",
    `--${boundary}--`
  ];
}

function buildMimeAttachmentPartLines(attachment: ComposeRawAttachment) {
  const contentTypeParts = [attachment.contentType.trim() || "application/octet-stream"];
  const typeName = formatMimeFilenameParameter("name", attachment.filename);

  if (typeName) {
    contentTypeParts.push(typeName);
  }

  const dispositionParts: string[] = [attachment.contentDisposition];
  const fileName = formatMimeFilenameParameter("filename", attachment.filename);

  if (fileName) {
    dispositionParts.push(fileName);
  }

  return [
    `Content-Type: ${contentTypeParts.join("; ")}`,
    "Content-Transfer-Encoding: base64",
    `Content-Disposition: ${dispositionParts.join("; ")}`,
    attachment.contentId ? `Content-ID: <${attachment.contentId}>` : null,
    "",
    encodeBase64Lines(attachment.content)
  ].filter((line): line is string => line !== null);
}

function buildRawSource(input: {
  attachments?: ComposeRawAttachment[];
  bccAddresses?: string;
  bodyHtml?: string | null;
  bodyText: string;
  ccAddresses?: string;
  messageIdHeader: string;
  fromName: string | null;
  fromAddress: string;
  toAddresses: string;
  subject: string;
}) {
  const commonHeaders = [
    `Message-ID: ${input.messageIdHeader}`,
    `Date: ${new Date().toUTCString()}`,
    `From: ${formatMailboxHeader(input.fromName, input.fromAddress)}`,
    `To: ${escapeHeaderValue(input.toAddresses)}`,
    input.ccAddresses ? `Cc: ${escapeHeaderValue(input.ccAddresses)}` : null,
    input.bccAddresses ? `Bcc: ${escapeHeaderValue(input.bccAddresses)}` : null,
    `Subject: ${encodeMimeHeaderWord(input.subject)}`,
    "MIME-Version: 1.0"
  ].filter(Boolean);
  const normalizedText = input.bodyText.replace(/\r?\n/g, "\r\n");
  const normalizedHtml = input.bodyHtml?.trim() ? input.bodyHtml.replace(/\r?\n/g, "\r\n") : "";
  const attachments = input.attachments ?? [];
  const inlineAttachments = attachments.filter((attachment) => attachment.contentDisposition === "inline");
  const regularAttachments = attachments.filter((attachment) => attachment.contentDisposition !== "inline");
  const hasHtmlBody = normalizedHtml.length > 0;
  const bodyPartLines = hasHtmlBody
    ? buildAlternativePartLines(normalizedText, normalizedHtml)
    : buildTextPlainPartLines(normalizedText);

  if (attachments.length === 0) {
    if (!hasHtmlBody) {
      return [...commonHeaders, ...bodyPartLines].join("\r\n");
    }

    return [...commonHeaders, ...bodyPartLines].join("\r\n");
  }

  const mixedBoundary = `----=_OfficialMail_Mixed_${randomUUID()}`;
  const lines = [...commonHeaders, `Content-Type: multipart/mixed; boundary="${mixedBoundary}"`, ""];

  if (inlineAttachments.length > 0) {
    const relatedBoundary = `----=_OfficialMail_Related_${randomUUID()}`;
    lines.push(`--${mixedBoundary}`);
    lines.push(`Content-Type: multipart/related; boundary="${relatedBoundary}"`);
    lines.push("");
    lines.push(`--${relatedBoundary}`);
    lines.push(...bodyPartLines);
    lines.push("");

    for (const attachment of inlineAttachments) {
      lines.push(`--${relatedBoundary}`);
      lines.push(...buildMimeAttachmentPartLines(attachment));
      lines.push("");
    }

    lines.push(`--${relatedBoundary}--`);
    lines.push("");
  } else {
    lines.push(`--${mixedBoundary}`);
    lines.push(...bodyPartLines);
    lines.push("");
  }

  for (const attachment of regularAttachments) {
    lines.push(`--${mixedBoundary}`);
    lines.push(...buildMimeAttachmentPartLines(attachment));
    lines.push("");
  }

  lines.push(`--${mixedBoundary}--`);
  return lines.join("\r\n");
}

type SystemWelcomeMailboxConfig = {
  displayName: string;
  email: string;
  password: string;
};

function splitRecipientList(value: string) {
  const addresses = value.match(/[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+/gi);
  return [...new Set((addresses ?? []).map((address) => address.trim().toLowerCase()).filter(Boolean))];
}

function formatRecipientSuggestionLabel(email: string, displayName?: string | null) {
  const normalizedName = displayName?.trim() ?? "";
  return normalizedName ? `${normalizedName} <${email}>` : email;
}

function normalizeMailSearchField(value: string | undefined): MailSearchField {
  if (value === "correspondent" || value === "from" || value === "to" || value === "subject" || value === "body") {
    return value;
  }

  return "all";
}

function normalizeMailSearchQuery(value: string | undefined) {
  return (value ?? "").trim().replace(/\s+/g, " ").slice(0, 120);
}

function escapeSqlLike(value: string) {
  return value.replace(/[\\%_]/g, "\\$&");
}

function buildMailboxSearchSql(input: { field: MailSearchField; query: string }) {
  if (!input.query) {
    return {
      params: [] as string[],
      sql: ""
    };
  }

  const likeQuery = `%${escapeSqlLike(input.query.toLowerCase())}%`;

  if (input.field === "correspondent") {
    return {
      params: [input.query.toLowerCase(), likeQuery],
      sql: "AND (LOWER(mm.from_address) = ? OR LOWER(mm.to_addresses) LIKE ? ESCAPE '\\\\')"
    };
  }

  if (input.field === "from") {
    return {
      params: [likeQuery, likeQuery],
      sql: "AND (LOWER(COALESCE(mm.from_name, '')) LIKE ? ESCAPE '\\\\' OR LOWER(mm.from_address) LIKE ? ESCAPE '\\\\')"
    };
  }

  if (input.field === "to") {
    return {
      params: [likeQuery],
      sql: "AND LOWER(mm.to_addresses) LIKE ? ESCAPE '\\\\'"
    };
  }

  if (input.field === "subject") {
    return {
      params: [likeQuery],
      sql: "AND LOWER(mm.subject) LIKE ? ESCAPE '\\\\'"
    };
  }

  if (input.field === "body") {
    return {
      params: [likeQuery, likeQuery],
      sql: "AND (LOWER(mm.snippet) LIKE ? ESCAPE '\\\\' OR LOWER(mm.body_text) LIKE ? ESCAPE '\\\\')"
    };
  }

  return {
    params: [likeQuery, likeQuery, likeQuery, likeQuery, likeQuery],
    sql: `
      AND (
        LOWER(COALESCE(mm.from_name, '')) LIKE ? ESCAPE '\\\\'
        OR LOWER(mm.from_address) LIKE ? ESCAPE '\\\\'
        OR LOWER(mm.to_addresses) LIKE ? ESCAPE '\\\\'
        OR LOWER(mm.subject) LIKE ? ESCAPE '\\\\'
        OR LOWER(CONCAT_WS(' ', mm.snippet, mm.body_text)) LIKE ? ESCAPE '\\\\'
      )
    `
  };
}

const RECIPIENT_ADDRESS_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/i;

function normalizeEmailAddress(value: string) {
  return value.trim().toLowerCase();
}

function isValidEmailAddress(value: string) {
  return value.length <= MAX_EMAIL_ADDRESS_LENGTH && RECIPIENT_ADDRESS_PATTERN.test(value);
}

function createSha256Hex(value: string) {
  return createHash("sha256").update(value).digest("hex");
}

function createSixDigitVerificationCode() {
  return String(Math.floor(Math.random() * 1_000_000)).padStart(6, "0");
}

function maskEmailAddress(email: string) {
  const normalizedEmail = normalizeEmailAddress(email);
  const [localPart, domain] = normalizedEmail.split("@");

  if (!localPart || !domain) {
    return normalizedEmail;
  }

  const maskedLocalPart =
    localPart.length <= 2
      ? `${localPart[0] ?? "*"}*`
      : `${localPart.slice(0, 2)}${"*".repeat(Math.max(2, localPart.length - 3))}${localPart.slice(-1)}`;

  return `${maskedLocalPart}@${domain}`;
}

function normalizeRecipientList(value: string) {
  const recipients = splitRecipientList(value);
  const seen = new Set<string>();

  return recipients.filter((recipient) => {
    if (!isValidEmailAddress(recipient)) {
      throw new Error("invalid-recipient");
    }

    if (seen.has(recipient)) {
      return false;
    }

    seen.add(recipient);
    return true;
  });
}

function htmlToPlainText(value: string) {
  const withoutLineBreakTags = value
    .replace(/<(br|\/p|\/div|\/li|\/blockquote|\/h[1-6])\b[^>]*>/gi, "\n")
    .replace(/<(li)\b[^>]*>/gi, "• ");

  return withoutLineBreakTags
    .replace(/<style[\s\S]*?<\/style>/gi, "")
    .replace(/<script[\s\S]*?<\/script>/gi, "")
    .replace(/<[^>]+>/g, " ")
    .replace(/&nbsp;/gi, " ")
    .replace(/&amp;/gi, "&")
    .replace(/&lt;/gi, "<")
    .replace(/&gt;/gi, ">")
    .replace(/&quot;/gi, '"')
    .replace(/&#39;/gi, "'")
    .replace(/\r/g, "")
    .replace(/[ \t]+\n/g, "\n")
    .replace(/\n{3,}/g, "\n\n")
    .replace(/[ \t]{2,}/g, " ")
    .trim();
}

function getSystemWelcomeMailboxConfig(): SystemWelcomeMailboxConfig | null {
  const email = (process.env.SYSTEM_WELCOME_MAILBOX_EMAIL ?? "admin@officialsite.kr").trim().toLowerCase();
  const password = (
    process.env.SYSTEM_WELCOME_MAILBOX_PASSWORD ??
    process.env.MAILBOX_CREDENTIAL_SECRET ??
    process.env.DB_PASSWORD ??
    ""
  ).trim();
  const displayName = (process.env.SYSTEM_WELCOME_MAILBOX_NAME ?? "오피셜메일").trim() || "오피셜메일";

  if (!email || !password) {
    return null;
  }

  return { displayName, email, password };
}

function getSystemNoReplyMailboxConfig(): SystemWelcomeMailboxConfig | null {
  const email = (process.env.SYSTEM_NOREPLY_MAILBOX_EMAIL ?? "noreply@officialsite.kr").trim().toLowerCase();
  const password = (
    process.env.SYSTEM_NOREPLY_MAILBOX_PASSWORD ??
    process.env.SYSTEM_WELCOME_MAILBOX_PASSWORD ??
    process.env.MAILBOX_CREDENTIAL_SECRET ??
    process.env.DB_PASSWORD ??
    ""
  ).trim();
  const displayName = (process.env.SYSTEM_NOREPLY_MAILBOX_NAME ?? "오피셜메일").trim() || "오피셜메일";

  if (!email || !password) {
    return null;
  }

  return { displayName, email, password };
}

async function resolveSystemMailboxCredentials(mailbox: SystemWelcomeMailboxConfig): Promise<SystemWelcomeMailboxConfig> {
  const [rows] = await getDbPool().query<
    (RowDataPacket & { password_ciphertext: string | null; status: string })[]
  >(
    `
      SELECT password_ciphertext, status
      FROM mailboxes
      WHERE LOWER(email) = ?
      ORDER BY id DESC
      LIMIT 1
    `,
    [mailbox.email]
  );
  const storedMailbox = rows[0];

  if (!storedMailbox) {
    return mailbox;
  }

  if (storedMailbox.status === "disabled" || !storedMailbox.password_ciphertext) {
    throw new Error("system-mailbox-unavailable");
  }

  return {
    ...mailbox,
    password: decryptMailboxPassword(storedMailbox.password_ciphertext)
  };
}

async function sendSystemMailboxMessage(
  mailbox: SystemWelcomeMailboxConfig,
  input: {
    bodyHtml?: string | null;
    bodyText: string;
    recipients: string[];
    subject: string;
  }
) {
  const activeMailbox = await resolveSystemMailboxCredentials(mailbox);
  const [localPart, domain] = activeMailbox.email.split("@");

  if (!localPart || !domain) {
    throw new Error("system-mailbox-invalid");
  }

  await ensureMailcowMailboxAccount({
    displayName: activeMailbox.displayName,
    domain,
    localPart,
    mailboxPassword: activeMailbox.password
  });

  const messageIdHeader = createMessageIdHeader(domain);
  const rawSource = buildRawSource({
    bodyHtml: input.bodyHtml ?? (plainTextToHtml(input.bodyText) || null),
    bodyText: input.bodyText,
    messageIdHeader,
    fromAddress: activeMailbox.email,
    fromName: activeMailbox.displayName,
    toAddresses: input.recipients.join(", "),
    subject: input.subject
  });
  const credentials = {
    email: activeMailbox.email,
    password: activeMailbox.password
  };

  await sendSmtpMessage(credentials, {
    rawSource,
    recipients: input.recipients
  });

  try {
    await appendSentMessage(credentials, rawSource);
  } catch {
    // System mail sending should still succeed even if Sent append fails.
  }
}

export async function sendOfficialMailNotificationEmail(input: {
  bodyHtml?: string | null;
  bodyText: string;
  recipientEmail: string;
  subject: string;
}) {
  await ensureOfficialMailSchema();
  const senderEmail = (process.env.DOMAIN_ASSISTANCE_NOTIFICATION_SENDER_EMAIL ?? "admin@officialsite.kr")
    .trim()
    .toLowerCase();
  const [rows] = await getDbPool().query<
    (RowDataPacket & {
      domain: string;
      email: string;
      id: number;
      password_ciphertext: string;
    })[]
  >(
    `
      SELECT m.id, m.email, m.password_ciphertext, d.domain
      FROM mailboxes m
      INNER JOIN domains d ON d.id = m.domain_id
      WHERE LOWER(m.email) = ?
        AND m.status <> 'disabled'
      ORDER BY m.id DESC
      LIMIT 1
    `,
    [senderEmail]
  );
  const systemMailbox = rows[0];

  if (!systemMailbox) {
    throw new Error("notification-mailbox-missing");
  }

  const messageIdHeader = createMessageIdHeader(systemMailbox.domain);
  const bodyHtml = input.bodyHtml ?? (plainTextToHtml(input.bodyText) || null);
  const rawSource = buildRawSource({
    bodyHtml,
    bodyText: input.bodyText,
    fromAddress: systemMailbox.email,
    fromName: "오피셜메일",
    messageIdHeader,
    subject: input.subject,
    toAddresses: input.recipientEmail
  });
  const credentials = {
    email: systemMailbox.email,
    password: decryptMailboxPassword(systemMailbox.password_ciphertext)
  };

  const sendResult = await sendSmtpMessage(credentials, {
    rawSource,
    recipients: [input.recipientEmail]
  });

  let appendResult: Awaited<ReturnType<typeof appendSentMessage>> = null;

  try {
    appendResult = await appendSentMessage(credentials, rawSource);
  } catch {
    // The notification is delivered even if copying it to Sent fails.
  }

  if (appendResult) {
    try {
      await withRetryableMailboxTransaction(async (connection) => {
        await ensureLocalMailboxFolders(connection, systemMailbox.id);
        const [folderRows] = await connection.query<(RowDataPacket & { id: number; remote_name: string | null })[]>(
          "SELECT id, remote_name FROM mailbox_folders WHERE mailbox_id = ? AND system_name = 'sent' LIMIT 1",
          [systemMailbox.id]
        );
        const sentFolder = folderRows[0];

        if (!sentFolder) {
          throw new Error("notification-sent-folder-missing");
        }

        const remoteFolder = appendResult.destination || sentFolder.remote_name || "Sent";
        const sentAt = new Date();
        const mailboxMessageId = await upsertMailboxMessageRow(connection, {
          bodyHtml,
          bodyText: input.bodyText,
          direction: "outbound",
          folderId: sentFolder.id,
          fromAddress: systemMailbox.email,
          fromName: "오피셜메일",
          isRead: true,
          isStarred: false,
          mailboxId: systemMailbox.id,
          messageIdHeader,
          preserveExistingTransport: false,
          rawSource,
          receivedAt: sentAt,
          remoteFlags: "\\Seen",
          remoteFolder,
          remoteUid: appendResult.uid ?? null,
          snippet: input.bodyText.replace(/\s+/g, " ").slice(0, 140),
          subject: input.subject,
          toAddresses: input.recipientEmail,
          transportResponse: sendResult.response,
          transportStatus: "sent"
        });

        if (sentFolder.remote_name !== remoteFolder) {
          await connection.query("UPDATE mailbox_folders SET remote_name = ?, updated_at = NOW() WHERE id = ?", [
            remoteFolder,
            sentFolder.id
          ]);
        }

        await logMailboxDelivery(connection, {
          action: "send",
          mailboxId: systemMailbox.id,
          mailboxMessageId,
          messageIdHeader,
          responseText: sendResult.response,
          sourceAddress: systemMailbox.email,
          status: "sent",
          subject: input.subject,
          targetAddress: input.recipientEmail,
          transport: "smtp+imap"
        });
      });
    } catch (error) {
      console.warn("Notification Sent cache update failed", error);
    }
  }

  return { fromAddress: credentials.email };
}

function buildSignupRecoveryEmailBody(code: string) {
  return [
    "오피셜메일 회원가입 보조 이메일 인증번호입니다.",
    "",
    `인증번호: ${code}`,
    `유효시간: ${Math.floor(SIGNUP_RECOVERY_EMAIL_CODE_TTL_MS / 60_000)}분`,
    "",
    "직접 요청하지 않았다면 이 메일은 무시하셔도 됩니다.",
    "",
    "오피셜메일"
  ].join("\n");
}

function buildPasswordResetLinkBody(input: { companyName: string; resetUrl: string }) {
  const companyName = input.companyName.trim() || "회원";

  return [
    `${companyName} 계정의 비밀번호 재설정 링크입니다.`,
    "",
    input.resetUrl,
    "",
    `유효시간: ${Math.floor(PASSWORD_RESET_TOKEN_TTL_MS / 60_000)}분`,
    "링크에 접속하면 새 비밀번호를 바로 설정할 수 있습니다.",
    "",
    "직접 요청하지 않았다면 이 메일은 무시하셔도 됩니다.",
    "",
    "오피셜메일"
  ].join("\n");
}

function hashPassword(password: string) {
  const salt = randomBytes(16).toString("hex");
  const digest = scryptSync(password, salt, 64).toString("hex");
  return `${salt}:${digest}`;
}

function verifyPassword(password: string, storedHash: string) {
  const [salt, storedDigest] = storedHash.split(":");

  if (!salt || !storedDigest) {
    return false;
  }

  const computedDigest = scryptSync(password, salt, 64);
  const storedBuffer = Buffer.from(storedDigest, "hex");

  if (computedDigest.length !== storedBuffer.length) {
    return false;
  }

  return timingSafeEqual(computedDigest, storedBuffer);
}

async function withTransaction<T>(callback: (connection: PoolConnection) => Promise<T>) {
  const pool = getDbPool();
  const connection = await pool.getConnection();

  try {
    await connection.beginTransaction();
    const result = await callback(connection);
    await connection.commit();
    return result;
  } catch (error) {
    await connection.rollback();
    throw error;
  } finally {
    connection.release();
  }
}

function isRetryableMailboxSyncWriteError(error: unknown) {
  if (!error || typeof error !== "object") {
    return false;
  }

  const code = "code" in error ? String(error.code) : "";
  const errno = "errno" in error ? Number(error.errno) : 0;

  return (
    code === "ER_LOCK_DEADLOCK" ||
    code === "ER_LOCK_WAIT_TIMEOUT" ||
    code === "ER_LOCK_TABLE_FULL" ||
    errno === 1213 ||
    errno === 1205 ||
    errno === 1206
  );
}

function isRetryableMailboxTransportError(error: unknown) {
  const message = error instanceof Error ? error.message : String(error ?? "");
  const normalized = message.toLowerCase();

  if (!normalized.includes("mailbox-imap-failed:")) {
    return false;
  }

  return [
    "epipe",
    "econnreset",
    "etimedout",
    "esocket",
    "socket hang up",
    "socket timeout",
    "connection closed",
    "connection ended",
    "connection timeout",
    "unexpectedly closed",
    "write after end",
  ].some((token) => normalized.includes(token));
}

function waitForMailboxSyncRetry(delayMs: number) {
  return new Promise<void>((resolve) => {
    setTimeout(resolve, delayMs);
  });
}

async function withMailboxSyncQueue<T>(mailboxId: number, callback: () => Promise<T>) {
  const previous = mailboxSyncQueues.get(mailboxId) ?? Promise.resolve();
  let releaseCurrent!: () => void;
  const current = new Promise<void>((resolve) => {
    releaseCurrent = resolve;
  });
  const queue = previous.catch(() => undefined).then(() => current);

  mailboxSyncQueues.set(mailboxId, queue);
  await previous.catch(() => undefined);

  try {
    return await callback();
  } finally {
    releaseCurrent();

    if (mailboxSyncQueues.get(mailboxId) === queue) {
      mailboxSyncQueues.delete(mailboxId);
    }
  }
}

async function withDistributedMailboxSyncLock<T>(mailboxId: number, callback: () => Promise<T>) {
  const connection = await getDbPool().getConnection();
  const lockName = `official-mail:mailbox-sync:${mailboxId}`;
  let acquired = false;

  try {
    const [rows] = await connection.query<(RowDataPacket & { acquired: number | null })[]>(
      "SELECT GET_LOCK(?, ?) AS acquired",
      [lockName, MAILBOX_DISTRIBUTED_SYNC_LOCK_TIMEOUT_SECONDS]
    );
    acquired = Number(rows[0]?.acquired ?? 0) === 1;

    if (!acquired) {
      return { acquired: false as const, value: undefined };
    }

    return { acquired: true as const, value: await callback() };
  } finally {
    if (acquired) {
      await connection.query("SELECT RELEASE_LOCK(?)", [lockName]).catch(() => undefined);
    }

    connection.release();
  }
}

async function withExclusiveMailboxOperation<T>(mailboxId: number, callback: () => Promise<T>) {
  return withMailboxSyncQueue(mailboxId, async () => {
    const result = await withDistributedMailboxSyncLock(mailboxId, callback);

    if (!result.acquired) {
      throw new Error("mailbox-busy");
    }

    return result.value;
  });
}

async function waitForMailboxSyncCompletion(mailboxId: number, previousLastSyncAt: Date | null) {
  const deadline = Date.now() + MAILBOX_SYNC_OBSERVE_TIMEOUT_MS;
  const startedAt = Date.now();
  const previousTime = previousLastSyncAt?.getTime() ?? 0;
  const lockName = `official-mail:mailbox-sync:${mailboxId}`;

  while (Date.now() < deadline) {
    const [rows] = await getDbPool().query<(RowDataPacket & { is_free: number; last_sync_at: Date | null })[]>(
      `
        SELECT
          m.last_sync_at,
          IS_FREE_LOCK(?) AS is_free
        FROM mailboxes m
        WHERE m.id = ?
        LIMIT 1
      `,
      [lockName, mailboxId]
    );
    const lastSyncAt = rows[0]?.last_sync_at;

    if (
      (lastSyncAt && lastSyncAt.getTime() > previousTime) ||
      (Date.now() - startedAt >= MAILBOX_SYNC_OBSERVE_INTERVAL_MS && Number(rows[0]?.is_free ?? 0) === 1)
    ) {
      return true;
    }

    await waitForMailboxSyncRetry(MAILBOX_SYNC_OBSERVE_INTERVAL_MS);
  }

  return false;
}

async function waitForMailboxHydration(mailboxId: number, folderSystemName: string) {
  const deadline = Date.now() + MAILBOX_HYDRATION_OBSERVE_TIMEOUT_MS;

  while (Date.now() < deadline) {
    const [rows] = await getDbPool().query<(RowDataPacket & { pending_count: number })[]>(
      `
        SELECT COUNT(*) AS pending_count
        FROM (
          SELECT mm.raw_source
          FROM mailbox_messages mm
          INNER JOIN mailbox_folders mf ON mf.id = mm.folder_id
          WHERE mm.mailbox_id = ?
            AND mf.system_name = ?
            AND mm.remote_uid IS NOT NULL
          ORDER BY mm.received_at DESC, mm.remote_uid DESC
          LIMIT ?
        ) AS recent_messages
        WHERE recent_messages.raw_source IS NULL
      `,
      [mailboxId, folderSystemName, MAILBOX_CONTENT_HYDRATION_LIMIT]
    );

    if (Number(rows[0]?.pending_count ?? 0) === 0) {
      return;
    }

    await waitForMailboxSyncRetry(MAILBOX_SYNC_OBSERVE_INTERVAL_MS);
  }
}

async function retryMailboxSyncWrite<T>(callback: () => Promise<T>) {
  for (let attempt = 0; ; attempt += 1) {
    try {
      return await callback();
    } catch (error) {
      const retryDelay = MAILBOX_SYNC_WRITE_RETRY_DELAYS_MS[attempt];

      if (!isRetryableMailboxSyncWriteError(error) || retryDelay === undefined) {
        throw error;
      }

      await waitForMailboxSyncRetry(retryDelay);
    }
  }
}

async function retryMailboxTransport<T>(callback: () => Promise<T>) {
  for (let attempt = 0; ; attempt += 1) {
    try {
      return await callback();
    } catch (error) {
      const retryDelay = MAILBOX_TRANSPORT_RETRY_DELAYS_MS[attempt];

      if (!isRetryableMailboxTransportError(error) || retryDelay === undefined) {
        throw error;
      }

      console.warn(`[mailbox-sync] transient IMAP failure; retrying attempt=${attempt + 2}`);
      await waitForMailboxSyncRetry(retryDelay);
    }
  }
}

async function withRetryableMailboxTransaction<T>(callback: (connection: PoolConnection) => Promise<T>) {
  return retryMailboxSyncWrite(() => withTransaction(callback));
}

async function emitSignupProvisioningProgress(
  callback: ((progress: SignupProvisioningProgress) => void | Promise<void>) | undefined,
  progress: SignupProvisioningProgress
) {
  await callback?.(progress);
}

async function getUserByEmail(email: string) {
  await ensureOfficialMailSchema();
  const pool = getDbPool();
  const [rows] = await pool.query<UserRow[]>(
    `
      SELECT
        id,
        email,
        company_name,
        display_name,
        recovery_email,
        recovery_email_verified_at,
        password_hash,
        mail_configured,
        mail_sidebar_width,
        mail_list_width,
        mail_list_height,
        mail_view_mode,
        mail_sort_order,
        preferred_locale
      FROM users
      WHERE email = ?
        AND account_deleted_at IS NULL
      LIMIT 1
    `,
    [email]
  );

  return rows[0] ?? null;
}

async function getUserById(userId: number) {
  await ensureOfficialMailSchema();
  const pool = getDbPool();
  const [rows] = await pool.query<UserRow[]>(
    `
      SELECT
        id,
        email,
        company_name,
        display_name,
        recovery_email,
        recovery_email_verified_at,
        password_hash,
        mail_configured,
        mail_sidebar_width,
        mail_list_width,
        mail_list_height,
        mail_view_mode,
        mail_sort_order,
        preferred_locale
      FROM users
      WHERE id = ?
        AND account_deleted_at IS NULL
      LIMIT 1
    `,
    [userId]
  );

  return rows[0] ?? null;
}

async function getSignupRecoveryEmailVerificationById(
  verificationId: number,
  queryable: Pool | PoolConnection = getDbPool()
) {
  const [rows] = await queryable.query<SignupRecoveryEmailVerificationRow[]>(
    `
      SELECT
        id,
        email,
        code_hash,
        verification_token_hash,
        expires_at,
        resend_available_at,
        verified_at,
        consumed_at
      FROM signup_recovery_email_verifications
      WHERE id = ?
      LIMIT 1
    `,
    [verificationId]
  );

  return rows[0] ?? null;
}

async function getLatestSignupRecoveryEmailVerificationByEmail(email: string) {
  const [rows] = await getDbPool().query<SignupRecoveryEmailVerificationRow[]>(
    `
      SELECT
        id,
        email,
        code_hash,
        verification_token_hash,
        expires_at,
        resend_available_at,
        verified_at,
        consumed_at
      FROM signup_recovery_email_verifications
      WHERE email = ?
      ORDER BY id DESC
      LIMIT 1
    `,
    [email]
  );

  return rows[0] ?? null;
}

async function getPasswordResetTokenByHash(tokenHash: string) {
  const [rows] = await getDbPool().query<PasswordResetTokenRow[]>(
    `
      SELECT
        id,
        user_id,
        recovery_email,
        token_hash,
        expires_at,
        used_at
      FROM password_reset_tokens
      WHERE token_hash = ?
      LIMIT 1
    `,
    [tokenHash]
  );

  return rows[0] ?? null;
}

async function getLatestSetupByUserId(userId: number) {
  const pool = getDbPool();
  const [mailboxRows] = await pool.query<MailSetupRow[]>(
    `
      SELECT
        d.id AS domain_id,
        d.domain,
        d.status AS domain_status,
        d.dkim_enabled,
        d.dkim_selector,
        d.dkim_public_key,
        d.mailcow_cleanup_at,
        d.verified_at,
        m.email AS mailbox_email,
        m.local_part,
        m.status AS mailbox_status
      FROM mailboxes m
      INNER JOIN domains d ON d.id = m.domain_id
      WHERE m.user_id = ?
      ORDER BY m.id DESC
      LIMIT 1
    `,
    [userId]
  );

  if (mailboxRows[0]) {
    return mailboxRows[0];
  }

  const [rows] = await pool.query<MailSetupRow[]>(
    `
      SELECT
        d.id AS domain_id,
        d.domain,
        d.status AS domain_status,
        d.dkim_enabled,
        d.dkim_selector,
        d.dkim_public_key,
        d.mailcow_cleanup_at,
        d.verified_at,
        '' AS mailbox_email,
        '' AS local_part,
        'pending_dns' AS mailbox_status
      FROM domains d
      WHERE d.user_id = ?
      ORDER BY d.id DESC
      LIMIT 1
    `,
    [userId]
  );

  return rows[0] ?? null;
}

async function getMailSetupByDomainId(domainId: number) {
  const [rows] = await getDbPool().query<DomainVerificationTargetRow[]>(
    `
      SELECT
        d.id AS domain_id,
        d.domain,
        d.status AS domain_status,
        d.dkim_enabled,
        d.dkim_selector,
        d.dkim_public_key,
        d.mailcow_cleanup_at,
        d.verified_at,
        d.user_id AS owner_user_id,
        COALESCE(m.email, '') AS mailbox_email,
        COALESCE(m.local_part, '') AS local_part,
        COALESCE(m.status, 'pending_dns') AS mailbox_status
      FROM domains d
      LEFT JOIN mailboxes m
        ON m.domain_id = d.id
        AND m.user_id = d.user_id
      WHERE d.id = ?
      ORDER BY m.id DESC
      LIMIT 1
    `,
    [domainId]
  );

  return rows[0] ?? null;
}

async function getLatestMailboxByUserId(userId: number) {
  const pool = getDbPool();
  const [rows] = await pool.query<MailboxRow[]>(
    `
      SELECT
        m.id,
        m.email,
        m.password_ciphertext,
        m.last_sync_at,
        m.last_sync_error,
        m.message_visibility_cutoff_at,
        m.send_cooldown_until,
        m.status,
        d.domain,
        d.sending_server_key,
        sending_server.host AS sending_server_host,
        sending_server.port AS sending_server_port,
        sending_server.security_mode AS sending_server_security_mode,
        sending_server.tls_server_name AS sending_server_tls_server_name,
        sending_server.enabled AS sending_server_enabled
      FROM mailboxes m
      INNER JOIN domains d ON d.id = m.domain_id
      LEFT JOIN mail_sending_servers sending_server
        ON sending_server.server_key = d.sending_server_key
      WHERE m.user_id = ?
      ORDER BY m.id DESC
      LIMIT 1
    `,
    [userId]
  );

  return rows[0] ?? null;
}

async function getMailboxByUserIdAndId(userId: number, mailboxId: number) {
  if (!Number.isInteger(mailboxId) || mailboxId <= 0) {
    return null;
  }

  const [rows] = await getDbPool().query<MailboxRow[]>(
    `
      SELECT
        m.id,
        m.email,
        m.password_ciphertext,
        m.last_sync_at,
        m.last_sync_error,
        m.message_visibility_cutoff_at,
        m.send_cooldown_until,
        m.status,
        d.domain,
        d.sending_server_key,
        sending_server.host AS sending_server_host,
        sending_server.port AS sending_server_port,
        sending_server.security_mode AS sending_server_security_mode,
        sending_server.tls_server_name AS sending_server_tls_server_name,
        sending_server.enabled AS sending_server_enabled
      FROM mailboxes m
      INNER JOIN domains d ON d.id = m.domain_id
      LEFT JOIN mail_sending_servers sending_server
        ON sending_server.server_key = d.sending_server_key
      WHERE m.user_id = ?
        AND m.id = ?
      LIMIT 1
    `,
    [userId, mailboxId]
  );

  return rows[0] ?? null;
}

export async function createInternalMailboxNotification(input: {
  bodyText: string;
  fromAddress: string;
  fromName: string;
  notificationKey: string;
  recipientEmail: string;
  subject: string;
}) {
  await ensureOfficialMailSchema();

  const user = await getUserByEmail(input.recipientEmail);

  if (!user) {
    throw new Error("internal-notification-recipient-not-found");
  }

  const mailbox = await getLatestMailboxByUserId(user.id);

  if (!mailbox) {
    throw new Error("internal-notification-mailbox-not-found");
  }

  return withTransaction(async (connection) => {
    await ensureLocalMailboxFolders(connection, mailbox.id);

    const [existingRows] = await connection.query<(RowDataPacket & { id: number })[]>(
      `
        SELECT id
        FROM mailbox_messages
        WHERE mailbox_id = ?
          AND transport_response = ?
        LIMIT 1
      `,
      [mailbox.id, input.notificationKey]
    );

    if (existingRows[0]?.id) {
      return existingRows[0].id;
    }

    const [folderRows] = await connection.query<(RowDataPacket & { id: number })[]>(
      `
        SELECT id
        FROM mailbox_folders
        WHERE mailbox_id = ?
          AND system_name = 'inbox'
        LIMIT 1
      `,
      [mailbox.id]
    );
    const inboxFolderId = folderRows[0]?.id;

    if (!inboxFolderId) {
      throw new Error("internal-notification-inbox-not-found");
    }

    const bodyText = input.bodyText.trim();
    const [result] = await connection.query(
      `
        INSERT INTO mailbox_messages (
          mailbox_id,
          folder_id,
          remote_uid,
          remote_folder,
          direction,
          subject,
          from_name,
          from_address,
          to_addresses,
          snippet,
          body_text,
          body_html,
          message_id_header,
          raw_source,
          transport_status,
          transport_response,
          remote_flags,
          is_read,
          is_starred,
          received_at,
          remote_internal_date,
          synced_at
        ) VALUES (?, ?, NULL, NULL, 'inbound', ?, ?, ?, ?, ?, ?, ?, ?, NULL, 'saved', ?, NULL, 0, 0, NOW(), NOW(), NOW())
      `,
      [
        mailbox.id,
        inboxFolderId,
        input.subject,
        input.fromName,
        input.fromAddress,
        input.recipientEmail,
        bodyText.replace(/\s+/g, " ").slice(0, 140),
        bodyText,
        plainTextToHtml(bodyText),
        createMessageIdHeader(mailbox.domain),
        input.notificationKey
      ]
    );

    return Number((result as { insertId: number }).insertId);
  });
}

async function getDnsRecords(domainId: number) {
  const pool = getDbPool();
  const [rows] = await pool.query<DnsRecordRow[]>(
    `
      SELECT record_type, host_name, value_text, priority, status, last_checked_at
      FROM dns_records
      WHERE domain_id = ?
      ORDER BY FIELD(record_type, 'MX', 'TXT'), host_name
    `,
    [domainId]
  );

  return rows;
}

function buildSendCooldownDetail(until: Date) {
  const remainingMs = until.getTime() - Date.now();

  if (remainingMs <= 0) {
    return "연속 발송 제한이 해제되는 중입니다. 1초 후 다시 시도해주세요.";
  }

  const remainingSeconds = Math.max(1, Math.ceil(remainingMs / 1000));
  return `1분 내 연속 발송 제한이 적용되었습니다. ${remainingSeconds}초 후 다시 시도해주세요.`;
}

async function isGrowthPlanActiveByOwnerUserId(queryable: Pool | PoolConnection, ownerUserId: number) {
  const [ownerRows] = await queryable.query<
    (RowDataPacket & {
      effective_owner_email: string;
      effective_owner_user_id: number;
    })[]
  >(
    `
      SELECT
        COALESCE(mtm.owner_user_id, u.id) AS effective_owner_user_id,
        COALESCE(owner.email, u.email) AS effective_owner_email
      FROM users u
      LEFT JOIN managed_team_mailboxes mtm
        ON mtm.email = u.email
       AND mtm.status = 'active'
      LEFT JOIN users owner
        ON owner.id = mtm.owner_user_id
      WHERE u.id = ?
      LIMIT 1
    `,
    [ownerUserId]
  );
  const effectiveOwner = ownerRows[0];
  const effectiveOwnerUserId = effectiveOwner?.effective_owner_user_id ?? ownerUserId;

  if (await hasOperationalBenefitByOwnerUserId(effectiveOwnerUserId, queryable)) {
    return true;
  }
  const [rows] = await queryable.query<(RowDataPacket & { id: number })[]>(
    `
      SELECT id
      FROM mailbox_toss_pay_subscriptions
      WHERE owner_user_id = ?
        AND status = 'active'
      LIMIT 1
    `,
    [effectiveOwnerUserId]
  );

  return rows.length > 0;
}

async function countDistinctSendAttempts(queryable: Pool | PoolConnection, mailboxId: number, createdAfter: Date) {
  const [rows] = await queryable.query<CountTotalRow[]>(
    `
      SELECT COUNT(DISTINCT mailbox_message_id) AS total
      FROM mailbox_delivery_logs
      WHERE mailbox_id = ?
        AND action = 'send'
        AND created_at >= ?
    `,
    [mailboxId, createdAfter]
  );

  return Number(rows[0]?.total ?? 0);
}

async function assertMailboxSendAllowed(input: {
  activateCooldownOnBurst?: boolean;
  connection?: PoolConnection;
  enforceBurstCooldown?: boolean;
  mailboxId: number;
  ownerUserId: number;
  requestedSendCount?: number;
}) {
  const queryable = input.connection ?? getDbPool();
  const enforceBurstCooldown = input.enforceBurstCooldown !== false;
  const requestedSendCount = Math.max(1, Math.floor(input.requestedSendCount ?? 1));
  const mailboxLockClause = input.connection ? "FOR UPDATE" : "";
  const [mailboxRows] = await queryable.query<(RowDataPacket & { send_cooldown_until: Date | null })[]>(
    `
      SELECT send_cooldown_until
      FROM mailboxes
      WHERE id = ?
      LIMIT 1
      ${mailboxLockClause}
    `,
    [input.mailboxId]
  );
  const mailboxRow = mailboxRows[0];

  if (!mailboxRow) {
    throw new Error("mailbox-not-found");
  }

  const cooldownUntil = mailboxRow.send_cooldown_until;

  if (enforceBurstCooldown && cooldownUntil && cooldownUntil.getTime() > Date.now()) {
    throw new Error(`send-cooldown-active:${buildSendCooldownDetail(cooldownUntil)}`);
  }

  if (enforceBurstCooldown && cooldownUntil && cooldownUntil.getTime() <= Date.now()) {
    await queryable.query(
      `
        UPDATE mailboxes
        SET send_cooldown_until = NULL, updated_at = NOW()
        WHERE id = ?
      `,
      [input.mailboxId]
    );
  }

  const growthPlanActive = await isGrowthPlanActiveByOwnerUserId(queryable, input.ownerUserId);

  if (!growthPlanActive) {
    const dayStart = new Date();
    dayStart.setHours(0, 0, 0, 0);
    const todaySendCount = await countDistinctSendAttempts(queryable, input.mailboxId, dayStart);

    if (todaySendCount + requestedSendCount > FREE_PLAN_DAILY_SEND_LIMIT) {
      throw new Error("free-send-limit-daily");
    }

    return;
  }

  if (!enforceBurstCooldown || !input.activateCooldownOnBurst) {
    return;
  }

  const burstWindowStart = new Date(Date.now() - GROWTH_PLAN_BURST_WINDOW_MS);
  const burstSendCount = await countDistinctSendAttempts(queryable, input.mailboxId, burstWindowStart);

  if (burstSendCount + requestedSendCount < GROWTH_PLAN_BURST_SEND_LIMIT) {
    return;
  }

  const nextCooldownUntil = new Date(Date.now() + GROWTH_PLAN_SEND_COOLDOWN_MS);

  await queryable.query(
    `
      UPDATE mailboxes
      SET send_cooldown_until = ?, updated_at = NOW()
      WHERE id = ?
    `,
    [nextCooldownUntil, input.mailboxId]
  );
}

async function logMailboxDelivery(
  connection: PoolConnection,
  input: {
    mailboxMessageId: number;
    mailboxId: number;
    action: "send" | "draft" | "receive";
    status: "saved" | "queued" | "sent" | "failed";
    transport: string;
    sourceAddress: string;
    targetAddress: string;
    subject: string;
    messageIdHeader: string | null;
    responseText: string | null;
  }
) {
  await connection.query(
    `
      INSERT INTO mailbox_delivery_logs (
        mailbox_message_id,
        mailbox_id,
        action,
        status,
        transport,
        source_address,
        target_address,
        subject,
        message_id_header,
        response_text
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `,
    [
      input.mailboxMessageId,
      input.mailboxId,
      input.action,
      input.status,
      input.transport,
      input.sourceAddress,
      input.targetAddress,
      input.subject,
      input.messageIdHeader,
      input.responseText
    ]
  );
}

function getSystemNoticeSender() {
  return {
    email: (process.env.SYSTEM_NOTICE_EMAIL ?? process.env.SYSTEM_WELCOME_MAILBOX_EMAIL ?? "admin@officialsite.kr")
      .trim()
      .toLowerCase(),
    name: (process.env.SYSTEM_NOTICE_NAME ?? "오피셜메일 알림").trim() || "오피셜메일 알림"
  };
}

type MailboxQueuedSendTarget = {
  envelopeRecipients: string[];
  logRecipients: string;
  messageIdHeader: string;
  rawSource: string;
  visibleRecipients: string;
};

function buildQueuedSendTarget(input: {
  attachments: ComposeRawAttachment[];
  bccRecipients: string[];
  body: string;
  bodyHtml: string | null;
  ccRecipients: string[];
  fromAddress: string;
  fromName: string | null;
  subject: string;
  toRecipients: string[];
}) {
  const envelopeRecipients = [...input.toRecipients, ...input.ccRecipients, ...input.bccRecipients].filter(
    (recipient, index, array) => array.indexOf(recipient) === index
  );
  const visibleRecipients = [
    input.toRecipients.join(", "),
    input.ccRecipients.length > 0 ? `참조: ${input.ccRecipients.join(", ")}` : ""
  ]
    .filter(Boolean)
    .join(" / ");
  const messageIdHeader = createMessageIdHeader(input.fromAddress.split("@")[1] || "officialsite.kr");

  return {
    envelopeRecipients,
    logRecipients: envelopeRecipients.join(", "),
    messageIdHeader,
    rawSource: buildRawSource({
      attachments: input.attachments,
      bodyHtml: input.bodyHtml,
      bodyText: input.body,
      ccAddresses: input.ccRecipients.join(", "),
      messageIdHeader,
      fromAddress: input.fromAddress,
      fromName: input.fromName,
      subject: input.subject,
      toAddresses: input.toRecipients.join(", ")
    }),
    visibleRecipients: visibleRecipients || input.toRecipients.join(", ")
  } satisfies MailboxQueuedSendTarget;
}

function buildQueuedSendTargets(input: {
  attachments: ComposeRawAttachment[];
  bccRecipients: string[];
  body: string;
  bodyHtml: string | null;
  ccRecipients: string[];
  fromAddress: string;
  fromName: string | null;
  sendIndividually: boolean;
  subject: string;
  toRecipients: string[];
}) {
  if (input.sendIndividually && input.toRecipients.length > 1) {
    return input.toRecipients.map((recipient) =>
      buildQueuedSendTarget({
        attachments: input.attachments,
        bccRecipients: input.bccRecipients,
        body: input.body,
        bodyHtml: input.bodyHtml,
        ccRecipients: input.ccRecipients,
        fromAddress: input.fromAddress,
        fromName: input.fromName,
        subject: input.subject,
        toRecipients: [recipient]
      })
    );
  }

  return [
    buildQueuedSendTarget({
      attachments: input.attachments,
      bccRecipients: input.bccRecipients,
      body: input.body,
      bodyHtml: input.bodyHtml,
      ccRecipients: input.ccRecipients,
      fromAddress: input.fromAddress,
      fromName: input.fromName,
      subject: input.subject,
      toRecipients: input.toRecipients
    })
  ];
}

function normalizeDeliveryFailureReason(error: unknown) {
  const message = error instanceof Error ? error.message : "알 수 없는 전송 오류";

  return message
    .replace(/^mailbox-smtp-failed:/, "")
    .replace(/^mailbox-imap-failed:/, "")
    .replace(/\s+/g, " ")
    .trim()
    .slice(0, 500);
}

function buildDeliveryFailureNoticeBody(input: {
  mailboxAddress: string;
  reason: string;
  subject: string;
  targetAddress: string;
}) {
  return [
    "발송한 메일이 정상적으로 전달되지 않았습니다.",
    "",
    `제목: ${input.subject || "(제목 없음)"}`,
    `받는 사람: ${input.targetAddress}`,
    `보낸 계정: ${input.mailboxAddress}`,
    "",
    `사유: ${input.reason}`,
    "",
    "수신 주소와 메일 서버 설정을 다시 확인한 뒤 재전송해주세요."
  ].join("\n");
}

function buildSystemNoticeMessage(input: {
  bodyText: string;
  mailboxDomain: string;
  mailboxEmail: string;
  subject: string;
}) {
  const sender = getSystemNoticeSender();
  const messageIdHeader = createMessageIdHeader(input.mailboxDomain);
  const bodyHtml = plainTextToHtml(input.bodyText) || null;
  const rawSource = buildRawSource({
    bodyHtml,
    bodyText: input.bodyText,
    messageIdHeader,
    fromAddress: sender.email,
    fromName: sender.name,
    toAddresses: input.mailboxEmail,
    subject: input.subject
  });

  return {
    bodyHtml,
    bodyText: input.bodyText,
    fromAddress: sender.email,
    fromName: sender.name,
    messageIdHeader,
    rawSource,
    subject: input.subject
  };
}

async function insertSystemNotice(
  connection: PoolConnection,
  input: {
    bodyHtml: string | null;
    bodyText: string;
    fromAddress: string;
    fromName: string;
    messageIdHeader: string;
    mailboxEmail: string;
    mailboxId: number;
    rawSource: string;
    remoteFolder: string;
    remoteUid: number;
    subject: string;
  }
) {
  await ensureLocalMailboxFolders(connection, input.mailboxId);

  const [folderRows] = await connection.query<(RowDataPacket & { id: number })[]>(
    "SELECT id FROM mailbox_folders WHERE mailbox_id = ? AND system_name = 'inbox' LIMIT 1",
    [input.mailboxId]
  );
  const inboxFolderId = folderRows[0]?.id;

  if (!inboxFolderId) {
    return;
  }

  const mailboxMessageId = await upsertMailboxMessageRow(connection, {
    bodyHtml: input.bodyHtml,
    bodyText: input.bodyText,
    direction: "inbound",
    folderId: inboxFolderId,
    fromAddress: input.fromAddress,
    fromName: input.fromName,
    isRead: false,
    isStarred: false,
    mailboxId: input.mailboxId,
    messageIdHeader: input.messageIdHeader,
    preserveExistingTransport: false,
    rawSource: input.rawSource,
    receivedAt: new Date(),
    remoteFlags: null,
    remoteFolder: input.remoteFolder,
    remoteUid: input.remoteUid,
    snippet: input.bodyText.replace(/\s+/g, " ").slice(0, 140),
    subject: input.subject,
    toAddresses: input.mailboxEmail,
    transportResponse: "오피셜메일 시스템 알림",
    transportStatus: "sent"
  });

  await logMailboxDelivery(connection, {
    mailboxMessageId,
    mailboxId: input.mailboxId,
    action: "receive",
    status: "sent",
    transport: "system",
    sourceAddress: input.fromAddress,
    targetAddress: input.mailboxEmail,
    subject: input.subject,
    messageIdHeader: input.messageIdHeader,
    responseText: "오피셜메일 시스템 알림"
  });
}

async function hasStoredSystemWelcomeMail(input: { mailboxId: number; sourceAddress: string; subject: string }) {
  const [rows] = await getDbPool().query<(RowDataPacket & { exists_flag: number })[]>(
    `
      SELECT 1 AS exists_flag
      FROM mailbox_messages
      WHERE mailbox_id = ?
        AND from_address = ?
        AND (subject = ? OR subject = ?)
      LIMIT 1
    `,
    [input.mailboxId, input.sourceAddress, LEGACY_SYSTEM_WELCOME_SUBJECT, input.subject]
  );

  return rows.length > 0;
}

async function sendSystemWelcomeMail(input: { companyName: string; mailboxId: number; mailboxEmail: string }) {
  const systemMailbox = getSystemWelcomeMailboxConfig();
  const email = renderSystemWelcomeEmail({
    companyName: input.companyName,
    domainSetupUrl: getMailAbsoluteUrl("/mail?panel=setup&section=domain"),
    mailboxEmail: input.mailboxEmail
  });

  if (!systemMailbox) {
    return false;
  }

  if (
    await hasStoredSystemWelcomeMail({
      mailboxId: input.mailboxId,
      sourceAddress: systemMailbox.email,
      subject: email.subject
    })
  ) {
    return false;
  }

  const activeMailbox = await resolveSystemMailboxCredentials(systemMailbox);
  const [localPart, domain] = activeMailbox.email.split("@");

  if (!localPart || !domain) {
    return false;
  }

  await ensureMailcowMailboxAccount({
    displayName: activeMailbox.displayName,
    domain,
    localPart,
    mailboxPassword: activeMailbox.password
  });

  const messageIdHeader = createMessageIdHeader(domain);
  const rawSource = buildRawSource({
    bodyHtml: email.bodyHtml,
    bodyText: email.bodyText,
    messageIdHeader,
    fromAddress: activeMailbox.email,
    fromName: activeMailbox.displayName,
    toAddresses: input.mailboxEmail,
    subject: email.subject
  });
  const credentials = {
    email: activeMailbox.email,
    password: activeMailbox.password
  };
  await sendSmtpMessage(credentials, {
    rawSource,
    recipients: [input.mailboxEmail]
  });

  try {
    await appendSentMessage(credentials, rawSource);
  } catch {
    // The welcome mail should still be considered sent even if the sender's Sent append fails.
  }
  return true;
}

async function sendDomainConnectionCompletedMailOnce(input: {
  companyName: string;
  domain: string;
  domainId: number;
  mailboxEmail: string;
}) {
  const connection = await getDbPool().getConnection();
  const lockName = `officialmail:domain-connected-email:${input.domainId}`;
  let lockAcquired = false;

  try {
    const [lockRows] = await connection.query<(RowDataPacket & { acquired: number | null })[]>(
      "SELECT GET_LOCK(?, 10) AS acquired",
      [lockName]
    );
    lockAcquired = Number(lockRows[0]?.acquired ?? 0) === 1;

    if (!lockAcquired) {
      return false;
    }

    const [domainRows] = await connection.query<
      (RowDataPacket & { connection_completed_email_sent_at: Date | null })[]
    >(
      `
        SELECT connection_completed_email_sent_at
        FROM domains
        WHERE id = ?
        LIMIT 1
      `,
      [input.domainId]
    );

    if (!domainRows[0] || domainRows[0].connection_completed_email_sent_at) {
      return false;
    }

    const systemMailbox = getSystemWelcomeMailboxConfig();

    if (!systemMailbox) {
      return false;
    }

    const email = renderDomainConnectionCompletedEmail({
      companyName: input.companyName,
      domain: input.domain,
      mailboxEmail: input.mailboxEmail,
      mailboxUrl: getMailAbsoluteUrl("/mail?folder=inbox")
    });
    await sendSystemMailboxMessage(systemMailbox, {
      bodyHtml: email.bodyHtml,
      bodyText: email.bodyText,
      recipients: [input.mailboxEmail],
      subject: email.subject
    });
    const [updateResult] = await connection.query<ResultSetHeader>(
      `
        UPDATE domains
        SET connection_completed_email_sent_at = NOW(), updated_at = NOW()
        WHERE id = ?
          AND connection_completed_email_sent_at IS NULL
      `,
      [input.domainId]
    );

    return updateResult.affectedRows === 1;
  } finally {
    if (lockAcquired) {
      await connection.query("SELECT RELEASE_LOCK(?)", [lockName]).catch(() => undefined);
    }

    connection.release();
  }
}

async function finalizeQueuedMailboxSend(input: {
  logRecipients: string;
  mailbox: MailboxRow;
  mailboxMessageId: number;
  messageIdHeader: string;
  rawSource: string;
  recipients: string[];
  subject: string;
  visibleRecipients: string;
}) {
  const claimConnection = await getDbPool().getConnection();
  const claimLockName = `officialmail:outbound:${input.mailbox.id}:${input.mailboxMessageId}`;
  let claimLockAcquired = false;

  try {
    const [claimRows] = await claimConnection.query<(RowDataPacket & { acquired: number })[]>(
      "SELECT GET_LOCK(?, 20) AS acquired",
      [claimLockName]
    );
    claimLockAcquired = Number(claimRows[0]?.acquired ?? 0) === 1;

    if (!claimLockAcquired) {
      return { status: "skipped" as const };
    }

    const [messageRows] = await claimConnection.query<(RowDataPacket & { transport_status: string })[]>(
      `
        SELECT transport_status
        FROM mailbox_messages
        WHERE id = ? AND mailbox_id = ?
        LIMIT 1
      `,
      [input.mailboxMessageId, input.mailbox.id]
    );

    if (messageRows[0]?.transport_status !== "queued") {
      return { status: "skipped" as const };
    }

    let transport = "smtp";
    let transportResponse = "";
    let remoteFolderName = "Sent";
    let remoteUid: number | null = null;

    try {
      const credentials = toMailboxCredentials(input.mailbox);
      const sendResult = await sendSmtpMessage(credentials, {
        rawSource: input.rawSource,
        recipients: input.recipients,
        server: getPreloadedMailSendingServer(input.mailbox)
      });

      transportResponse = sendResult.response;

      try {
        const appendResult = await appendSentMessage(credentials, input.rawSource);
        remoteFolderName = appendResult?.destination ?? remoteFolderName;
        remoteUid = appendResult?.uid ?? null;

        if (!appendResult?.uid) {
          transportResponse = `${transportResponse} / Sent 폴더 UID 확인은 생략되었습니다.`;
        }
      } catch (error) {
        const appendError = error instanceof Error ? error.message : "sent-append-failed";
        transportResponse = `${transportResponse} / Sent 폴더 append 실패: ${appendError}`;
        transport = "smtp+imap";
      }

      await withRetryableMailboxTransaction(async (connection) => {
        const [folderRows] = await connection.query<(RowDataPacket & { id: number; remote_name: string | null })[]>(
          "SELECT id, remote_name FROM mailbox_folders WHERE mailbox_id = ? AND system_name = 'sent' LIMIT 1",
          [input.mailbox.id]
        );
        const sentFolderId = folderRows[0]?.id;

        if (sentFolderId && folderRows[0]?.remote_name !== remoteFolderName) {
          await connection.query(
            `
            UPDATE mailbox_folders
            SET remote_name = ?, updated_at = NOW()
            WHERE id = ?
          `,
            [remoteFolderName, sentFolderId]
          );
        }

        await connection.query(
          `
          UPDATE mailbox_messages
          SET
            remote_uid = ?,
            remote_folder = ?,
            transport_status = 'sent',
            transport_response = ?,
            remote_flags = '\\\\Seen',
            updated_at = NOW()
          WHERE id = ? AND mailbox_id = ?
        `,
          [remoteUid, remoteFolderName, transportResponse, input.mailboxMessageId, input.mailbox.id]
        );

        // A large link is public only after SMTP accepts at least one copy.
        // Match this message's plain-text links so individual sends can publish
        // shared uploads even when another recipient's queued copy failed.
        await connection.query(
          `UPDATE mailbox_uploaded_assets cua
           INNER JOIN mailbox_messages mm ON mm.id = ?
           INNER JOIN mailboxes mb ON mb.id = mm.mailbox_id
           SET cua.published_at = COALESCE(cua.published_at, NOW()),
               cua.updated_at = NOW()
           WHERE cua.owner_user_id = mb.user_id
             AND cua.large_attachment = 1
             AND cua.storage_status = 'finalized'
             AND mm.body_text LIKE CONCAT('%', cua.temp_token, '%')`,
          [input.mailboxMessageId]
        );

        await logMailboxDelivery(connection, {
          mailboxMessageId: input.mailboxMessageId,
          mailboxId: input.mailbox.id,
          action: "send",
          status: "sent",
          transport,
          sourceAddress: input.mailbox.email,
          targetAddress: input.logRecipients,
          subject: input.subject,
          messageIdHeader: input.messageIdHeader,
          responseText: transportResponse
        });
      });

      await updateMailboxSyncState(input.mailbox.id, {
        lastSyncAt: new Date(),
        lastSyncError: null
      });

      return {
        status: "sent" as const
      };
    } catch (error) {
      const reason = normalizeDeliveryFailureReason(error);
      const rawMessage = error instanceof Error ? error.message : "";
      const transportKind = rawMessage.startsWith("mailbox-imap-failed:") ? "imap" : "smtp";
      const publicReason = getPublicMailTransportErrorMessage(rawMessage || reason, transportKind);
      const errorMessage =
        transportKind === "imap" ? `mailbox-imap-failed:${publicReason}` : `mailbox-smtp-failed:${publicReason}`;

      const failureNotice = buildSystemNoticeMessage({
        bodyText: buildDeliveryFailureNoticeBody({
          mailboxAddress: input.mailbox.email,
          reason: publicReason,
          subject: input.subject,
          targetAddress: input.visibleRecipients || input.logRecipients
        }),
        mailboxDomain: input.mailbox.domain,
        mailboxEmail: input.mailbox.email,
        subject: `${SYSTEM_DELIVERY_FAILURE_PREFIX} ${input.subject || "(제목 없음)"}`
      });
      const credentials = toMailboxCredentials(input.mailbox);
      const [failedMessageAppend, failureNoticeAppend] = await Promise.allSettled([
        appendSentMessage(credentials, input.rawSource),
        appendMessageToRemoteFolder(credentials, "INBOX", failureNotice.rawSource)
      ]);
      const failedMessageRemote = failedMessageAppend.status === "fulfilled" ? failedMessageAppend.value : null;
      const failureNoticeRemote = failureNoticeAppend.status === "fulfilled" ? failureNoticeAppend.value : null;

      await withRetryableMailboxTransaction(async (connection) => {
        await connection.query(
          `
          UPDATE mailbox_messages
          SET
            remote_uid = COALESCE(?, remote_uid),
            remote_folder = COALESCE(?, remote_folder),
            transport_status = 'failed',
            transport_response = ?,
            updated_at = NOW()
          WHERE id = ? AND mailbox_id = ?
        `,
          [
            failedMessageRemote?.uid ?? null,
            failedMessageRemote?.destination ?? null,
            reason,
            input.mailboxMessageId,
            input.mailbox.id
          ]
        );

        await logMailboxDelivery(connection, {
          mailboxMessageId: input.mailboxMessageId,
          mailboxId: input.mailbox.id,
          action: "send",
          status: "failed",
          transport,
          sourceAddress: input.mailbox.email,
          targetAddress: input.logRecipients,
          subject: input.subject,
          messageIdHeader: input.messageIdHeader,
          responseText: reason
        });

        if (failureNoticeRemote?.uid) {
          await insertSystemNotice(connection, {
            ...failureNotice,
            mailboxEmail: input.mailbox.email,
            mailboxId: input.mailbox.id,
            remoteFolder: failureNoticeRemote.destination || "INBOX",
            remoteUid: failureNoticeRemote.uid
          });
        }
      });

      await updateMailboxSyncState(input.mailbox.id, {
        lastSyncAt: new Date(),
        lastSyncError: reason
      });

      return {
        errorMessage,
        reason: publicReason,
        status: "failed" as const
      };
    }
  } finally {
    if (claimLockAcquired) {
      await claimConnection.query("SELECT RELEASE_LOCK(?)", [claimLockName]).catch(() => undefined);
    }

    claimConnection.release();
  }
}

function normalizeOutboundDeliveryBatchSize(value?: number) {
  if (!Number.isFinite(value)) {
    return DEFAULT_OUTBOUND_DELIVERY_BATCH_SIZE;
  }

  return Math.min(5, Math.max(1, Math.trunc(value as number)));
}

export async function processQueuedMailboxDrafts() {
  await ensureOfficialMailSchema();
  const [rows] = await getDbPool().query<(MailboxRow & {
    draft_id: number; raw_source: string; message_id_header: string;
  })[]>(`
    SELECT mb.*, d.domain, mm.id AS draft_id, mm.raw_source, mm.message_id_header
    FROM mailbox_messages mm
    INNER JOIN mailboxes mb ON mb.id = mm.mailbox_id
    INNER JOIN domains d ON d.id = mb.domain_id
    INNER JOIN mailbox_folders f ON f.id = mm.folder_id
    WHERE mm.direction = 'draft' AND mm.transport_status = 'queued'
      AND f.system_name = 'drafts' AND mb.status = 'active'
      AND mm.raw_source IS NOT NULL
      AND mm.updated_at < DATE_SUB(NOW(), INTERVAL 5 SECOND)
    ORDER BY mm.updated_at ASC LIMIT 3
  `);
  let saved = 0;
  for (const row of rows) {
    const connection = await getDbPool().getConnection();
    const name = `official-mail:mailbox-sync:${row.id}`;
    let locked = false;
    try {
      const [locks] = await connection.query<(RowDataPacket & { acquired: number })[]>("SELECT GET_LOCK(?, 0) AS acquired", [name]);
      locked = Number(locks[0]?.acquired) === 1;
      if (!locked) continue;
      const [current] = await connection.query<RowDataPacket[]>("SELECT id FROM mailbox_messages WHERE id = ? AND direction = 'draft' AND transport_status = 'queued' AND message_id_header = ?", [row.draft_id, row.message_id_header]);
      if (!current.length) continue;
      const remote = await appendDraftMessage(toMailboxCredentials(row), row.raw_source, row.message_id_header);
      if (!remote?.uid) throw new Error("draft-append-unconfirmed");
      await connection.beginTransaction();
      await connection.query(
        "UPDATE mailbox_messages SET remote_uid = ?, remote_folder = ?, transport_status = 'saved', transport_response = ?, updated_at = NOW() WHERE id = ? AND message_id_header = ?",
        [remote.uid, remote.destination || "Drafts", "임시보관함에 임시저장을 완료했습니다.", row.draft_id, row.message_id_header],
      );
      await connection.query("UPDATE mailbox_delivery_logs SET status = 'saved' WHERE mailbox_message_id = ? AND action = 'draft'", [row.draft_id]);
      if ("totalCount" in remote) await connection.query(
        "UPDATE mailbox_folders SET remote_total = ?, updated_at = NOW() WHERE mailbox_id = ? AND system_name = 'drafts'",
        [remote.totalCount, row.id],
      );
      await connection.commit();
      saved++;
    } catch (error) {
      await connection.rollback().catch(() => undefined);
      // Keep the local draft visible and retry later, even after a deployment.
      await connection.query("UPDATE mailbox_messages SET transport_response = ?, updated_at = NOW() WHERE id = ? AND transport_status = 'queued'", ["임시저장 동기화 재시도 대기", row.draft_id]).catch(() => undefined);
      console.warn("[compose-draft] sync retry", { id: row.draft_id, message: error instanceof Error ? error.message : "unknown" });
    } finally {
      if (locked) await connection.query("SELECT RELEASE_LOCK(?)", [name]).catch(() => undefined);
      connection.release();
    }
  }
  return { attempted: rows.length, saved };
}

export async function processQueuedMailboxDeliveries(input?: { batchSize?: number }) {
  await ensureOfficialMailSchema();

  const batchSize = normalizeOutboundDeliveryBatchSize(input?.batchSize);
  const connection = await getDbPool().getConnection();
  let acquiredLock = false;

  try {
    const [lockRows] = await connection.query<(RowDataPacket & { acquired: number })[]>(
      "SELECT GET_LOCK(?, 0) AS acquired",
      [OUTBOUND_DELIVERY_LOCK_NAME]
    );
    acquiredLock = Number(lockRows[0]?.acquired ?? 0) === 1;

    if (!acquiredLock) {
      return {
        attempted: 0,
        busy: true,
        failed: 0,
        remaining: 0,
        sent: 0,
        skipped: 0
      };
    }

    const [queuedRows] = await connection.query<QueuedOutboundDeliveryRow[]>(
      `
        SELECT
          mb.id,
          mb.email,
          mb.last_sync_at,
          mb.last_sync_error,
          mb.message_visibility_cutoff_at,
          mb.password_ciphertext,
          mb.send_cooldown_until,
          mb.status,
          d.domain,
          d.sending_server_key,
          sending_server.host AS sending_server_host,
          sending_server.port AS sending_server_port,
          sending_server.security_mode AS sending_server_security_mode,
          sending_server.tls_server_name AS sending_server_tls_server_name,
          sending_server.enabled AS sending_server_enabled,
          message.id AS mailbox_message_id,
          message.message_id_header,
          message.raw_source,
          message.subject,
          message.to_addresses AS visible_recipients,
          COALESCE(
            (
              SELECT delivery.target_address
              FROM mailbox_delivery_logs AS delivery
              WHERE delivery.mailbox_message_id = message.id
                AND delivery.status = 'queued'
              ORDER BY delivery.id DESC
              LIMIT 1
            ),
            message.to_addresses
          ) AS log_recipients
        FROM mailbox_messages AS message
        INNER JOIN mailboxes AS mb ON mb.id = message.mailbox_id
        INNER JOIN domains AS d ON d.id = mb.domain_id
        LEFT JOIN mail_sending_servers AS sending_server
          ON sending_server.server_key = d.sending_server_key
        WHERE message.direction = 'outbound'
          AND message.transport_status = 'queued'
          AND message.raw_source IS NOT NULL
          AND message.message_id_header IS NOT NULL
          AND mb.status = 'active'
        ORDER BY message.id ASC
        LIMIT ${batchSize}
      `
    );

    const deliveryResults = await Promise.allSettled(
      queuedRows.map((row) =>
        finalizeQueuedMailboxSend({
          logRecipients: row.log_recipients,
          mailbox: row,
          mailboxMessageId: row.mailbox_message_id,
          messageIdHeader: row.message_id_header,
          rawSource: row.raw_source,
          recipients: normalizeRecipientList(row.log_recipients),
          subject: row.subject,
          visibleRecipients: row.visible_recipients
        })
      )
    );
    let sent = 0;
    let failed = 0;
    let skipped = 0;

    for (const result of deliveryResults) {
      if (result.status === "fulfilled" && result.value.status === "sent") {
        sent += 1;
      } else if (result.status === "fulfilled" && result.value.status === "skipped") {
        skipped += 1;
      } else {
        failed += 1;
      }
    }

    const [remainingRows] = await connection.query<(RowDataPacket & { remaining: number })[]>(
      `
        SELECT COUNT(*) AS remaining
        FROM mailbox_messages AS message
        INNER JOIN mailboxes AS mb ON mb.id = message.mailbox_id
        WHERE message.direction = 'outbound'
          AND message.transport_status = 'queued'
          AND message.raw_source IS NOT NULL
          AND message.message_id_header IS NOT NULL
          AND mb.status = 'active'
      `
    );

    return {
      attempted: queuedRows.length,
      busy: false,
      failed,
      remaining: Number(remainingRows[0]?.remaining ?? 0),
      sent,
      skipped
    };
  } finally {
    if (acquiredLock) {
      await connection.query("SELECT RELEASE_LOCK(?)", [OUTBOUND_DELIVERY_LOCK_NAME]).catch(() => undefined);
    }

    connection.release();
  }
}

function buildSessionSnapshot(user: UserRow, setup: MailSetupRow | null): SessionSnapshot {
  const hasActiveMailbox =
    !setup?.mailcow_cleanup_at && Boolean(setup?.mailbox_email) && setup?.mailbox_status === "active";

  return {
    email: user.email,
    companyName: user.company_name,
    displayName: user.display_name,
    mailConfigured: !setup?.mailcow_cleanup_at && (Boolean(user.mail_configured) || hasActiveMailbox),
    domain: setup?.domain ?? "",
    mailbox: setup?.mailbox_email ?? "대표@도메인",
    preferredLocale: normalizeSupportedLocale(user.preferred_locale)
  };
}

function buildMailLayoutPreferences(user: UserRow): MailLayoutPreferences {
  return normalizeMailLayoutPreferences({
    sidebarWidth: user.mail_sidebar_width ?? DEFAULT_MAIL_LAYOUT_PREFERENCES.sidebarWidth,
    listWidth: user.mail_list_width ?? DEFAULT_MAIL_LAYOUT_PREFERENCES.listWidth,
    listHeight: user.mail_list_height ?? DEFAULT_MAIL_LAYOUT_PREFERENCES.listHeight,
    viewMode:
      (user.mail_view_mode as MailLayoutPreferences["viewMode"] | undefined) ??
      DEFAULT_MAIL_LAYOUT_PREFERENCES.viewMode,
    sortOrder:
      (user.mail_sort_order as MailLayoutPreferences["sortOrder"] | undefined) ??
      DEFAULT_MAIL_LAYOUT_PREFERENCES.sortOrder
  });
}

function buildDnsTemplate(dkimSelector: string, dkimPublicKey: string) {
  return [
    {
      type: "MX",
      host: "@",
      value: `${MX_HOSTNAME}.`,
      priority: 10
    },
    {
      type: "TXT",
      host: "@",
      value: SPF_RECORD_VALUE,
      priority: null
    },
    {
      type: "TXT",
      host: `${dkimSelector}._domainkey`,
      value: `v=DKIM1; k=rsa; p=${dkimPublicKey}`,
      priority: null
    },
    {
      type: "TXT",
      host: "_dmarc",
      value: `v=DMARC1; p=none; rua=${DMARC_RUA}`,
      priority: null
    }
  ] satisfies Array<{
    type: string;
    host: string;
    value: string;
    priority: number | null;
  }>;
}

function buildPendingDnsTemplate() {
  return [
    {
      type: "MX",
      host: "@",
      value: `${MX_HOSTNAME}.`,
      priority: 10
    },
    {
      type: "TXT",
      host: "@",
      value: SPF_RECORD_VALUE,
      priority: null
    },
    {
      type: "TXT",
      host: "_dmarc",
      value: `v=DMARC1; p=none; rua=${DMARC_RUA}`,
      priority: null
    }
  ] satisfies Array<{
    type: string;
    host: string;
    value: string;
    priority: number | null;
  }>;
}

async function syncDnsRecords(
  connection: PoolConnection,
  domainId: number,
  records: ReturnType<typeof buildDnsTemplate>
) {
  await connection.query("DELETE FROM dns_records WHERE domain_id = ?", [domainId]);

  for (const record of records) {
    await connection.query(
      `
        INSERT INTO dns_records (
          domain_id,
          record_type,
          host_name,
          value_text,
          priority,
          status
        ) VALUES (?, ?, ?, ?, ?, 'pending')
      `,
      [domainId, record.type, record.host, record.value, record.priority]
    );
  }
}

async function ensureLocalMailboxFolders(connection: PoolConnection, mailboxId: number) {
  for (const [index, folder] of LOCAL_MAILBOX_FOLDERS.entries()) {
    const [existingRows] = await connection.query<(RowDataPacket & { id: number })[]>(
      `
        SELECT id
        FROM mailbox_folders
        WHERE mailbox_id = ?
          AND system_name = ?
        LIMIT 1
      `,
      [mailboxId, folder.systemName]
    );
    const existingId = existingRows[0]?.id;

    if (existingId) {
      await connection.query(
        `
          UPDATE mailbox_folders
          SET name = ?
          WHERE id = ?
            AND name <> ?
        `,
        [folder.displayName, existingId, folder.displayName]
      );
      continue;
    }

    await connection.query(
      `
        INSERT INTO mailbox_folders (
          mailbox_id,
          system_name,
          remote_name,
          name,
          remote_total,
          remote_unseen,
          last_synced_at,
          sort_order
        )
        VALUES (?, ?, NULL, ?, 0, 0, NULL, ?)
      `,
      [mailboxId, folder.systemName, folder.displayName, index + 1]
    );
  }
}

function normalizeFolderSystemName(folder: string | undefined | null): string {
  if (isSystemMailboxFolderSystemName(folder) || isCustomMailboxFolderSystemName(folder)) {
    return folder;
  }

  return "inbox";
}

function normalizeTargetFolderSystemName(folder: string | undefined | null): string {
  if (isSystemMailboxFolderSystemName(folder) || isCustomMailboxFolderSystemName(folder)) {
    return folder;
  }

  throw new Error("invalid-folder");
}

function resolveMailboxRefreshFolder(folder: string | undefined | null) {
  if (folder === "all") {
    return "inbox";
  }

  return normalizeFolderSystemName(folder);
}

function remoteFolderFallbackName(systemName: SupportedMailboxFolderSystemName) {
  switch (systemName) {
    case "sent":
      return "Sent";
    case "drafts":
      return "Drafts";
    case "spam":
      return "Junk";
    case "trash":
      return "Trash";
    default:
      return "INBOX";
  }
}

function normalizeMessageIds(messageIds: Array<number | string>) {
  return [...new Set(messageIds.map((value) => Number(value)).filter((value) => Number.isInteger(value) && value > 0))];
}

function normalizeSenderAddress(value: string) {
  return value.trim().toLowerCase();
}

function normalizeRemoteFlags(flags: string[]) {
  return flags.length > 0 ? flags.sort().join(" ") : null;
}

function hasRemoteFlag(remoteFlags: string[] | string | null | undefined, flag: string) {
  if (!remoteFlags) {
    return false;
  }

  const normalizedFlag = flag.trim().toLowerCase();
  const values = Array.isArray(remoteFlags) ? remoteFlags : remoteFlags.split(/\s+/).filter(Boolean);

  return values.some((value) => value.trim().toLowerCase() === normalizedFlag);
}

function applyRemoteFlagString(remoteFlags: string | null | undefined, flag: string, enabled: boolean) {
  const nextFlags = new Set(
    (remoteFlags ?? "")
      .split(/\s+/)
      .map((value) => value.trim())
      .filter(Boolean)
  );

  if (enabled) {
    nextFlags.add(flag);
  } else {
    nextFlags.delete(flag);
  }

  return [...nextFlags].sort().join(" ") || null;
}

function remoteFlagList(remoteFlags: string | null | undefined) {
  return [
    ...new Set(
      (remoteFlags ?? "")
        .split(/\s+/)
        .map((value) => value.trim())
        .filter(Boolean)
    )
  ];
}

function folderDisplayNameForAction(systemName: string, fallback: string) {
  if (systemName === "inbox") return "받은메일함";
  if (systemName === "sent") return "보낸메일함";
  if (systemName === "drafts") return "임시보관함";
  if (systemName === "spam") return "스팸메일함";
  if (systemName === "trash") return "휴지통";
  return fallback;
}

function padNumber(value: number) {
  return String(value).padStart(2, "0");
}

function formatFolderBackupName(sourceLabel: string) {
  const now = new Date();
  const dateStamp = `${now.getFullYear()}-${padNumber(now.getMonth() + 1)}-${padNumber(now.getDate())}`;
  const timeStamp = `${padNumber(now.getHours())}${padNumber(now.getMinutes())}`;

  return `${sourceLabel} 백업 ${dateStamp} ${timeStamp}`;
}

function normalizeRemoteTransportResponse(message: RemoteMailboxMessage) {
  return message.transportResponse?.trim() || "메일함과 동기화했습니다.";
}

function normalizeNullableText(value: string | null | undefined) {
  return value ?? null;
}

function normalizeNullableNumber(value: number | null | undefined) {
  return value === null || value === undefined ? null : Number(value);
}

function normalizeNullableDateTime(value: Date | string | null | undefined) {
  if (!value) {
    return null;
  }

  const date = value instanceof Date ? value : new Date(value);
  const time = date.getTime();
  return Number.isNaN(time) ? null : time;
}

function toMailboxCredentials(mailbox: MailboxRow) {
  if (!mailbox.password_ciphertext) {
    throw new Error("mailbox-auth-missing");
  }

  return {
    email: mailbox.email,
    password: decryptMailboxPassword(mailbox.password_ciphertext)
  };
}

async function updateMailboxSyncState(
  mailboxId: number,
  input: {
    lastSyncAt?: Date | null;
    lastSyncError?: string | null;
  }
) {
  const assignments: string[] = [];
  const values: Array<Date | number | string | null> = [];

  if (Object.hasOwn(input, "lastSyncAt")) {
    assignments.push("last_sync_at = ?");
    values.push(input.lastSyncAt ?? null);
  }

  if (Object.hasOwn(input, "lastSyncError")) {
    assignments.push("last_sync_error = ?");
    values.push(input.lastSyncError ?? null);
  }

  if (assignments.length === 0) {
    return;
  }

  values.push(mailboxId);
  await getDbPool().query(
    `
      UPDATE mailboxes
      SET
        ${assignments.join(",\n        ")},
        updated_at = NOW()
      WHERE id = ?
    `,
    values
  );
}

async function updateMailboxFolderContentSyncState(
  folderId: number,
  input: {
    lastContentSyncAt?: Date | null;
    lastContentSyncError?: string | null;
  }
) {
  const assignments: string[] = [];
  const values: Array<Date | number | string | null> = [];

  if (Object.hasOwn(input, "lastContentSyncAt")) {
    assignments.push("last_content_synced_at = ?");
    values.push(input.lastContentSyncAt ?? null);
  }

  if (Object.hasOwn(input, "lastContentSyncError")) {
    assignments.push("last_content_sync_error = ?");
    values.push(input.lastContentSyncError ?? null);
  }

  if (assignments.length === 0) {
    return;
  }

  values.push(folderId);
  await getDbPool().query(
    `
      UPDATE mailbox_folders
      SET
        ${assignments.join(",\n        ")},
        updated_at = NOW()
      WHERE id = ?
    `,
    values
  );
}

async function scheduleMailboxSyncRetry(mailboxId: number) {
  await getDbPool().query(
    `
      UPDATE mailboxes
      SET
        next_sync_retry_at = CASE
          WHEN next_sync_retry_at IS NULL OR next_sync_retry_at > DATE_ADD(NOW(), INTERVAL 5 MINUTE)
            THEN DATE_ADD(NOW(), INTERVAL 5 MINUTE)
          ELSE next_sync_retry_at
        END,
        updated_at = NOW()
      WHERE id = ?
    `,
    [mailboxId]
  );
}

async function clearMailboxSyncRetry(mailboxId: number) {
  await getDbPool().query(
    `
      UPDATE mailboxes
      SET
        sync_retry_attempts = 0,
        next_sync_retry_at = NULL,
        updated_at = NOW()
      WHERE id = ?
    `,
    [mailboxId]
  );
}

async function getMailboxSyncFailureState(mailboxId: number) {
  const [rows] = await getDbPool().query<
    (RowDataPacket & {
      content_error_count: number;
      last_sync_error: string | null;
    })[]
  >(
    `
      SELECT
        m.last_sync_error,
        COUNT(CASE
          WHEN NULLIF(TRIM(f.last_content_sync_error), '') IS NOT NULL THEN 1
          ELSE NULL
        END) AS content_error_count
      FROM mailboxes m
      LEFT JOIN mailbox_folders f ON f.mailbox_id = m.id
      WHERE m.id = ?
      GROUP BY m.id, m.last_sync_error
      LIMIT 1
    `,
    [mailboxId]
  );

  return {
    contentErrorCount: Number(rows[0]?.content_error_count ?? 0),
    metadataError: rows[0]?.last_sync_error?.trim() || null
  };
}

async function clearResolvedMailboxContentErrors(mailboxId: number) {
  await getDbPool().query(
    `
      UPDATE mailbox_folders f
      SET
        f.last_content_sync_error = NULL,
        f.last_content_synced_at = COALESCE(f.last_content_synced_at, NOW()),
        f.updated_at = NOW()
      WHERE f.mailbox_id = ?
        AND NULLIF(TRIM(f.last_content_sync_error), '') IS NOT NULL
        AND NOT EXISTS (
          SELECT 1
          FROM mailbox_messages mm
          WHERE mm.folder_id = f.id
            AND mm.remote_uid IS NOT NULL
            AND mm.raw_source IS NULL
        )
    `,
    [mailboxId]
  );
}

async function getFolderRowsByMailboxId(connection: PoolConnection, mailboxId: number) {
  const [rows] = await connection.query<
    (RowDataPacket & {
      id: number;
      system_name: string;
      remote_name: string | null;
    })[]
  >(
    `
      SELECT id, system_name, remote_name
      FROM mailbox_folders
      WHERE mailbox_id = ?
      ORDER BY sort_order ASC, id ASC
    `,
    [mailboxId]
  );

  return rows;
}

async function createCustomMailboxFolder(
  connection: PoolConnection,
  mailbox: MailboxRow,
  input: {
    name: string;
    insertAfterSystemName?: string | null;
    insertAtTop?: boolean;
    parentSystemName?: string | null;
  }
) {
  await ensureLocalMailboxFolders(connection, mailbox.id);

  const normalizedName = normalizeCustomFolderName(input.name);

  if (!normalizedName) {
    throw new Error("folder-name-required");
  }

  assertCustomFolderLeafName(normalizedName);

  const [existingRows] = await connection.query<MailboxFolderRowBasic[]>(
    `
      SELECT id, name, remote_name, system_name, sort_order
      FROM mailbox_folders
      WHERE mailbox_id = ?
      ORDER BY sort_order ASC, id ASC
    `,
    [mailbox.id]
  );

  const parentFolder = input.parentSystemName
    ? existingRows.find(
        (row) =>
          row.system_name === input.parentSystemName &&
          isCustomMailboxFolderSystemName(row.system_name),
      )
    : null;

  if (input.parentSystemName && !parentFolder) {
    throw new Error("folder-parent-not-found");
  }

  const parentRemoteName = parentFolder ? mailboxFolderRemoteName(parentFolder) : null;
  const remoteName = parentRemoteName
    ? `${parentRemoteName}${MAILBOX_FOLDER_HIERARCHY_SEPARATOR}${normalizedName}`
    : normalizedName;
  const duplicateFolder = existingRows.find(
    (row) => mailboxFolderRemoteName(row).toLowerCase() === remoteName.toLowerCase(),
  );

  if (duplicateFolder) {
    return {
      id: Number(duplicateFolder.id),
      name: duplicateFolder.name,
      parentSystemName: mailboxFolderParentSystemName(duplicateFolder, existingRows),
      systemName: duplicateFolder.system_name
    };
  }

  const credentials = mailbox.password_ciphertext ? toMailboxCredentials(mailbox) : null;

  if (credentials) {
    await createRemoteMailboxFolder(credentials, remoteName);
  }

  let nextSortOrder = 0;

  if (parentFolder && parentRemoteName) {
    const hierarchyRows = existingRows.filter((row) => {
      const candidateRemoteName = mailboxFolderRemoteName(row);
      return (
        candidateRemoteName.toLowerCase() === parentRemoteName.toLowerCase() ||
        isMailboxFolderDescendantRemoteName(candidateRemoteName, parentRemoteName)
      );
    });
    nextSortOrder = Math.max(...hierarchyRows.map((row) => Number(row.sort_order) || 0), 0) + 1;
  } else if (input.insertAtTop) {
    const [targetRows] = await connection.query<(RowDataPacket & { sort_order: number | null })[]>(
      `
        SELECT sort_order
        FROM mailbox_folders
        WHERE mailbox_id = ?
          AND system_name LIKE 'custom\\_\\_%' ESCAPE '\\\\'
        ORDER BY sort_order ASC, id ASC
        LIMIT 1
      `,
      [mailbox.id]
    );

    nextSortOrder = Number(targetRows[0]?.sort_order ?? 6);
  } else if (input.insertAfterSystemName) {
    const [targetRows] = await connection.query<(RowDataPacket & { sort_order: number | null })[]>(
      `
        SELECT sort_order
        FROM mailbox_folders
        WHERE mailbox_id = ?
          AND system_name = ?
        LIMIT 1
      `,
      [mailbox.id, input.insertAfterSystemName]
    );

    const afterSortOrder = Number(targetRows[0]?.sort_order ?? 0);
    nextSortOrder = afterSortOrder > 0 ? afterSortOrder + 1 : 0;
  }

  if (nextSortOrder <= 0) {
    const [maxSortRows] = await connection.query<(RowDataPacket & { max_sort_order: number | null })[]>(
      `
        SELECT MAX(sort_order) AS max_sort_order
        FROM mailbox_folders
        WHERE mailbox_id = ?
      `,
      [mailbox.id]
    );
    nextSortOrder = Number(maxSortRows[0]?.max_sort_order ?? 0) + 1;
  } else {
    await connection.query(
      `
        UPDATE mailbox_folders
        SET sort_order = sort_order + 1
        WHERE mailbox_id = ?
          AND sort_order >= ?
      `,
      [mailbox.id, nextSortOrder]
    );
  }

  const systemName = createCustomFolderSystemName();
  const [result] = await connection.query(
    `
      INSERT INTO mailbox_folders (
        mailbox_id,
        system_name,
        remote_name,
        name,
        remote_total,
        remote_unseen,
        last_synced_at,
        sort_order
      ) VALUES (?, ?, ?, ?, 0, 0, NULL, ?)
    `,
    [mailbox.id, systemName, remoteName, normalizedName, nextSortOrder]
  );

  return {
    id: Number((result as { insertId: number }).insertId),
    name: normalizedName,
    parentSystemName: parentFolder?.system_name ?? null,
    systemName
  };
}

async function syncRemoteFolderStateToDatabase(
  connection: PoolConnection,
  mailboxId: number,
  folderState: Awaited<ReturnType<typeof syncRemoteMailbox>>["folders"]
) {
  await ensureLocalMailboxFolders(connection, mailboxId);
  const folderRows = await getFolderRowsByMailboxId(connection, mailboxId);
  const folderIdBySystemName = new Map(folderRows.map((row) => [row.system_name, row.id]));

  for (const folder of folderState) {
    const folderId = folderIdBySystemName.get(folder.systemName);

    if (!folderId) {
      continue;
    }

    await connection.query(
      `
        UPDATE mailbox_folders
        SET
          remote_name = ?,
          remote_total = ?,
          remote_unseen = ?,
          last_synced_at = NOW(),
          updated_at = NOW()
        WHERE id = ?
          AND (
            NOT (remote_name <=> ?)
            OR remote_total <> ?
            OR remote_unseen <> ?
          )
      `,
      [
        folder.remoteName,
        folder.totalCount,
        folder.unreadCount,
        folderId,
        folder.remoteName,
        folder.totalCount,
        folder.unreadCount
      ]
    );
  }

  return folderIdBySystemName;
}

async function upsertMailboxMessageRow(
  connection: PoolConnection,
  input: {
    bodyHtml: string | null;
    bodyText: string;
    direction: "inbound" | "outbound" | "draft";
    folderId: number;
    fromAddress: string;
    fromName: string | null;
    isRead: boolean;
    isStarred: boolean;
    mailboxId: number;
    messageIdHeader: string | null;
    preserveExistingTransport: boolean;
    rawSource: string | null;
    receivedAt: Date;
    remoteInternalDate?: Date | null;
    remoteFlags: string | null;
    remoteFolder: string | null;
    remoteUid: number | null;
    snippet: string;
    subject: string;
    toAddresses: string;
    transportResponse: string | null;
    transportStatus: "saved" | "queued" | "sent" | "failed";
  },
  duplicateRetryRemaining = 1
) {
  const remoteInternalDate = input.remoteInternalDate ?? input.receivedAt;

  if (input.remoteFolder !== null && input.remoteUid !== null) {
    const [existingRows] = await connection.query<
      (RowDataPacket & {
        body_html: string | null;
        body_text: string;
        direction: "inbound" | "outbound" | "draft";
        folder_id: number;
        from_address: string;
        from_name: string | null;
        id: number;
        is_read: number;
        is_starred: number;
        message_id_header: string | null;
        raw_source: string | null;
        received_at: Date | string;
        remote_flags: string | null;
        remote_folder: string | null;
        remote_internal_date: Date | string | null;
        remote_uid: number | null;
        snippet: string;
        subject: string;
        to_addresses: string;
        transport_response: string | null;
        transport_status: "saved" | "queued" | "sent" | "failed";
      })[]
    >(
      `
        SELECT
          id,
          folder_id,
          remote_uid,
          remote_folder,
          direction,
          subject,
          from_name,
          from_address,
          to_addresses,
          snippet,
          body_text,
          body_html,
          message_id_header,
          raw_source,
          transport_status,
          transport_response,
          remote_flags,
          is_read,
          is_starred,
          received_at,
          remote_internal_date
        FROM mailbox_messages
        WHERE mailbox_id = ?
          AND remote_folder = ?
          AND remote_uid = ?
        LIMIT 1
        FOR UPDATE
      `,
      [input.mailboxId, input.remoteFolder, input.remoteUid]
    );
    const existingId = existingRows[0]?.id;

    if (existingId) {
      const existingRow = existingRows[0];
      const nextTransportStatus = input.preserveExistingTransport
        ? existingRow.transport_status
        : input.transportStatus;
      const nextTransportResponse = input.preserveExistingTransport
        ? (existingRow.transport_response ?? input.transportResponse)
        : input.transportResponse;
      const hasChanges =
        Number(existingRow.folder_id) !== input.folderId ||
        normalizeNullableNumber(existingRow.remote_uid) !== input.remoteUid ||
        normalizeNullableText(existingRow.remote_folder) !== input.remoteFolder ||
        existingRow.direction !== input.direction ||
        existingRow.subject !== input.subject ||
        normalizeNullableText(existingRow.from_name) !== input.fromName ||
        existingRow.from_address !== input.fromAddress ||
        existingRow.to_addresses !== input.toAddresses ||
        existingRow.snippet !== input.snippet ||
        existingRow.body_text !== input.bodyText ||
        normalizeNullableText(existingRow.body_html) !== input.bodyHtml ||
        normalizeNullableText(existingRow.message_id_header) !== input.messageIdHeader ||
        normalizeNullableText(existingRow.raw_source) !== input.rawSource ||
        existingRow.transport_status !== nextTransportStatus ||
        normalizeNullableText(existingRow.transport_response) !== nextTransportResponse ||
        normalizeNullableText(existingRow.remote_flags) !== input.remoteFlags ||
        Number(existingRow.is_read) !== (input.isRead ? 1 : 0) ||
        Number(existingRow.is_starred) !== (input.isStarred ? 1 : 0) ||
        normalizeNullableDateTime(existingRow.received_at) !== normalizeNullableDateTime(input.receivedAt) ||
        normalizeNullableDateTime(existingRow.remote_internal_date) !== normalizeNullableDateTime(remoteInternalDate);

      if (!hasChanges) {
        return Number(existingId);
      }

      const transportUpdate = input.preserveExistingTransport
        ? `
            transport_response = COALESCE(transport_response, ?),
        `
        : `
            transport_status = ?,
            transport_response = ?,
        `;
      const transportParams = input.preserveExistingTransport
        ? [input.transportResponse]
        : [input.transportStatus, input.transportResponse];

      await connection.query(
        `
          UPDATE mailbox_messages
          SET
            folder_id = ?,
            remote_uid = ?,
            remote_folder = ?,
            direction = ?,
            subject = ?,
            from_name = ?,
            from_address = ?,
            to_addresses = ?,
            snippet = ?,
            body_text = ?,
            body_html = ?,
            message_id_header = ?,
            attachments_indexed_at = NULL,
            raw_source = ?,
            ${transportUpdate}
            remote_flags = ?,
            is_read = ?,
            is_starred = ?,
            received_at = ?,
            remote_internal_date = ?,
            synced_at = NOW()
          WHERE id = ?
        `,
        [
          input.folderId,
          input.remoteUid,
          input.remoteFolder,
          input.direction,
          input.subject,
          input.fromName,
          input.fromAddress,
          input.toAddresses,
          input.snippet,
          input.bodyText,
          input.bodyHtml,
          input.messageIdHeader,
          input.rawSource,
          ...transportParams,
          input.remoteFlags,
          input.isRead ? 1 : 0,
          input.isStarred ? 1 : 0,
          input.receivedAt,
          remoteInternalDate,
          existingId
        ]
      );

      return Number(existingId);
    }

    if ((input.direction === "outbound" || input.direction === "draft") && input.messageIdHeader) {
      const [localOutboundRows] = await connection.query<(RowDataPacket & { id: number })[]>(
        `
          SELECT id
          FROM mailbox_messages
          WHERE mailbox_id = ?
            AND direction = ?
            AND message_id_header = ?
            AND remote_uid IS NULL
          ORDER BY id ASC
          LIMIT 1
          FOR UPDATE
        `,
        [input.mailboxId, input.direction, input.messageIdHeader]
      );
      const localOutboundId = localOutboundRows[0]?.id;

      if (localOutboundId) {
        try {
          await connection.query(
            `
              UPDATE mailbox_messages
              SET remote_uid = ?, remote_folder = ?, updated_at = NOW()
              WHERE id = ? AND remote_uid IS NULL
            `,
            [input.remoteUid, input.remoteFolder, localOutboundId]
          );
        } catch (error) {
          if (typeof error !== "object" || error === null || !("code" in error) || error.code !== "ER_DUP_ENTRY") {
            throw error;
          }
        }

        return upsertMailboxMessageRow(connection, input, duplicateRetryRemaining);
      }
    }
  }

  try {
    const [result] = await connection.query(
      `
        INSERT INTO mailbox_messages (
          mailbox_id,
          folder_id,
          remote_uid,
          remote_folder,
          direction,
          subject,
          from_name,
          from_address,
          to_addresses,
          snippet,
          body_text,
          body_html,
          message_id_header,
          raw_source,
          transport_status,
          transport_response,
          remote_flags,
          is_read,
          is_starred,
          received_at,
          remote_internal_date,
          synced_at
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
      `,
      [
        input.mailboxId,
        input.folderId,
        input.remoteUid,
        input.remoteFolder,
        input.direction,
        input.subject,
        input.fromName,
        input.fromAddress,
        input.toAddresses,
        input.snippet,
        input.bodyText,
        input.bodyHtml,
        input.messageIdHeader,
        input.rawSource,
        input.transportStatus,
        input.transportResponse,
        input.remoteFlags,
        input.isRead ? 1 : 0,
        input.isStarred ? 1 : 0,
        input.receivedAt,
        remoteInternalDate
      ]
    );

    return Number((result as { insertId: number }).insertId);
  } catch (error) {
    if (
      input.remoteFolder !== null &&
      input.remoteUid !== null &&
      typeof error === "object" &&
      error !== null &&
      "code" in error &&
      error.code === "ER_DUP_ENTRY"
    ) {
      if (duplicateRetryRemaining > 0) {
        return upsertMailboxMessageRow(connection, input, duplicateRetryRemaining - 1);
      }
    }

    throw error;
  }
}

async function permanentlyDeleteMailboxRows(
  connection: PoolConnection,
  mailbox: MailboxRow,
  rows: Array<Pick<MailboxMoveMessageRow, "folder_id" | "id" | "remote_folder" | "remote_uid">>
) {
  if (rows.length === 0) {
    return {
      cleanupTargets: [] as ComposeUploadCleanupTarget[],
      deletedCount: 0,
      deletedMessageIds: [] as number[],
      invalidatedFolderIds: [] as number[]
    } satisfies MailboxMessageDeleteResult;
  }

  const invalidatedFolderIds = new Set<number>();
  const credentials = mailbox.password_ciphertext ? toMailboxCredentials(mailbox) : null;
  const messageIds = rows.map((row) => row.id);
  const [readStateRows] = await connection.query<
    (RowDataPacket & { folder_id: number; id: number; is_read: number })[]
  >(
    `
      SELECT id, folder_id, is_read
      FROM mailbox_messages
      WHERE mailbox_id = ?
        AND id IN (${messageIds.map(() => "?").join(", ")})
    `,
    [mailbox.id, ...messageIds]
  );

  if (credentials) {
    const remoteFolderMap = new Map<string, number[]>();

    for (const row of rows) {
      if (!row.remote_folder || !row.remote_uid) {
        continue;
      }

      invalidatedFolderIds.add(row.folder_id);
      const bucket = remoteFolderMap.get(row.remote_folder) ?? [];
      bucket.push(row.remote_uid);
      remoteFolderMap.set(row.remote_folder, bucket);
    }

    for (const [remoteFolder, remoteUids] of remoteFolderMap) {
      const deleted = await deleteRemoteMessages(credentials, remoteFolder, remoteUids);

      if (!deleted) {
        throw new Error("mailbox-delete-failed");
      }
    }
  }

  const cleanupTargets = await markComposeUploadsDeletedByMessageIds(connection, messageIds);
  await enqueueMailboxAttachmentStorageDeletesByMessageIds(connection, messageIds);

  await connection.query(
    `
      DELETE FROM mailbox_delivery_logs
      WHERE mailbox_message_id IN (${messageIds.map(() => "?").join(", ")})
    `,
    messageIds
  );

  await connection.query(
    `
      DELETE FROM mailbox_messages
      WHERE mailbox_id = ?
        AND id IN (${messageIds.map(() => "?").join(", ")})
    `,
    [mailbox.id, ...messageIds]
  );

  const folderCountDeltas = new Map<number, { count: number; unreadCount: number }>();
  for (const row of readStateRows) {
    const current = folderCountDeltas.get(Number(row.folder_id)) ?? { count: 0, unreadCount: 0 };
    folderCountDeltas.set(Number(row.folder_id), {
      count: current.count - 1,
      unreadCount: current.unreadCount - (Boolean(row.is_read) ? 0 : 1)
    });
  }
  for (const [folderId, delta] of folderCountDeltas) {
    await connection.query(
      `
        UPDATE mailbox_folders
        SET
          remote_total = GREATEST(0, remote_total + ?),
          remote_unseen = GREATEST(0, remote_unseen + ?),
          last_synced_at = NULL,
          updated_at = NOW()
        WHERE id = ? AND mailbox_id = ?
      `,
      [delta.count, delta.unreadCount, folderId, mailbox.id]
    );
  }

  rows.forEach((row) => invalidatedFolderIds.add(row.folder_id));

  return {
    cleanupTargets,
    deletedCount: messageIds.length,
    deletedMessageIds: messageIds,
    invalidatedFolderIds: [...invalidatedFolderIds]
  } satisfies MailboxMessageDeleteResult;
}

async function removeReplacedDraftMessage(mailbox: MailboxRow, sourceDraftMessageId: number | null | undefined) {
  if (!sourceDraftMessageId) {
    return;
  }

  const cleanupTargets = await withTransaction(async (connection) => {
    const [rows] = await connection.query<MailboxMoveMessageRow[]>(
      `
        SELECT
          mm.id,
          mm.folder_id,
          mm.direction,
          mm.from_address,
          mm.remote_folder,
          mm.remote_uid
        FROM mailbox_messages mm
        INNER JOIN mailbox_folders mf ON mf.id = mm.folder_id
        WHERE mm.id = ?
          AND mm.mailbox_id = ?
          AND mm.direction = 'draft'
          AND mf.system_name = 'drafts'
        LIMIT 1
        FOR UPDATE
      `,
      [sourceDraftMessageId, mailbox.id]
    );

    if (rows.length === 0) {
      return [] as ComposeUploadCleanupTarget[];
    }

    const deleteResult = await permanentlyDeleteMailboxRows(connection, mailbox, rows);

    return deleteResult.cleanupTargets;
  });

  await cleanupComposeUploadArtifacts(getDbPool(), cleanupTargets).catch((error) => {
    console.error("Replaced draft upload cleanup failed", error);
  });
}

async function getReplacedDraftAttachments(mailbox: MailboxRow, sourceDraftMessageId: number | null | undefined) {
  if (!sourceDraftMessageId) {
    return [] satisfies ComposeRawAttachment[];
  }

  const [rows] = await getDbPool().query<(RowDataPacket & { raw_source: string | null })[]>(
    `
      SELECT mm.raw_source
      FROM mailbox_messages mm
      INNER JOIN mailbox_folders mf ON mf.id = mm.folder_id
      WHERE mm.id = ?
        AND mm.mailbox_id = ?
        AND mm.direction = 'draft'
        AND mf.system_name = 'drafts'
      LIMIT 1
    `,
    [sourceDraftMessageId, mailbox.id]
  );
  if (rows.length === 0) {
    throw new Error("draft-not-found");
  }
  const rawSource = rows[0].raw_source;

  if (!rawSource) {
    return [] satisfies ComposeRawAttachment[];
  }

  const attachments = await extractAttachmentPayloadsFromRawSource(rawSource);

  return attachments.map(
    (attachment) =>
      ({
        content: attachment.content,
        contentDisposition: attachment.contentDisposition,
        contentId: attachment.contentId,
        contentType: attachment.contentType,
        filename: attachment.filename
      }) satisfies ComposeRawAttachment
  );
}

async function replaceMailboxMessageAttachmentMetadata(
  connection: PoolConnection,
  messageId: number,
  attachments: RemoteMailboxAttachmentMeta[]
) {
  await enqueueMailboxAttachmentStorageDeletesByMessageIds(connection, [messageId]);
  await connection.query("DELETE FROM mailbox_message_attachments WHERE mailbox_message_id = ?", [messageId]);

  for (const attachment of attachments) {
    await connection.query(
      `
        INSERT INTO mailbox_message_attachments (
          mailbox_message_id,
          attachment_index,
          content_disposition,
          content_id,
          original_name,
          mime_type,
          size_bytes,
          storage_status
        ) VALUES (?, ?, ?, ?, ?, ?, ?, 'metadata')
      `,
      [
        messageId,
        attachment.index,
        attachment.contentDisposition,
        attachment.contentId,
        attachment.filename,
        attachment.contentType,
        attachment.sizeBytes
      ]
    );
  }

  await connection.query(
    `
      UPDATE mailbox_messages
      SET attachments_indexed_at = NOW()
      WHERE id = ?
    `,
    [messageId]
  );
}

async function replaceCurrentFolderMessages(
  connection: PoolConnection,
  mailboxId: number,
  folderId: number,
  remoteFolder: string,
  messages: RemoteMailboxMessage[],
  options?: {
    deleteMissing?: boolean;
  }
) {
  const identifiedMessages = messages.filter((message) => Number.isInteger(message.remoteUid) && message.remoteUid > 0);
  const remoteUids = identifiedMessages.map((message) => message.remoteUid);
  const preserveLocalOutboundSql =
    "remote_uid IS NULL AND direction IN ('outbound', 'draft') AND transport_status IN ('queued', 'sent', 'failed')";
  const deletedAssetTargets: ComposeUploadCleanupTarget[] = [];

  const [orphanedSnapshotRows] = await connection.query<(RowDataPacket & { id: number })[]>(
    `
      SELECT id
      FROM mailbox_messages
      WHERE mailbox_id = ?
        AND folder_id = ?
        AND remote_folder = ?
        AND remote_uid IS NULL
        AND raw_source IS NULL
        AND message_id_header IS NULL
        AND body_text = ''
        AND transport_response IN (
          '메일함과 동기화한 메일입니다.',
          '발송된 메일을 메일함과 동기화했습니다.'
        )
    `,
    [mailboxId, folderId, remoteFolder]
  );

  if (orphanedSnapshotRows.length > 0) {
    deletedAssetTargets.push(
      ...(await markComposeUploadsDeletedByMessageIds(
        connection,
        orphanedSnapshotRows.map((row) => row.id)
      ))
    );
    await enqueueMailboxAttachmentStorageDeletesByMessageIds(
      connection,
      orphanedSnapshotRows.map((row) => row.id)
    );
    await connection.query(
      `
        DELETE FROM mailbox_messages
        WHERE mailbox_id = ?
          AND folder_id = ?
          AND remote_folder = ?
          AND remote_uid IS NULL
          AND raw_source IS NULL
          AND message_id_header IS NULL
          AND body_text = ''
          AND transport_response IN (
            '메일함과 동기화한 메일입니다.',
            '발송된 메일을 메일함과 동기화했습니다.'
          )
      `,
      [mailboxId, folderId, remoteFolder]
    );
  }

  if (options?.deleteMissing && remoteUids.length > 0) {
    const placeholders = remoteUids.map(() => "?").join(", ");

    const [rowsToDelete] = await connection.query<(RowDataPacket & { id: number })[]>(
      `
        SELECT id
        FROM mailbox_messages
        WHERE mailbox_id = ?
          AND folder_id = ?
          AND remote_folder = ?
          AND (
            remote_uid NOT IN (${placeholders})
            OR (remote_uid IS NULL AND NOT (${preserveLocalOutboundSql}))
          )
      `,
      [mailboxId, folderId, remoteFolder, ...remoteUids]
    );

    deletedAssetTargets.push(
      ...(await markComposeUploadsDeletedByMessageIds(
        connection,
        rowsToDelete.map((row) => row.id)
      ))
    );
    await enqueueMailboxAttachmentStorageDeletesByMessageIds(
      connection,
      rowsToDelete.map((row) => row.id)
    );

    await connection.query(
      `
        DELETE FROM mailbox_messages
        WHERE mailbox_id = ?
          AND folder_id = ?
          AND remote_folder = ?
          AND (
            remote_uid NOT IN (${placeholders})
            OR (remote_uid IS NULL AND NOT (${preserveLocalOutboundSql}))
          )
      `,
      [mailboxId, folderId, remoteFolder, ...remoteUids]
    );
  } else if (options?.deleteMissing) {
    const [rowsToDelete] = await connection.query<(RowDataPacket & { id: number })[]>(
      `
        SELECT id
        FROM mailbox_messages
        WHERE mailbox_id = ?
          AND folder_id = ?
          AND remote_folder = ?
          AND NOT (${preserveLocalOutboundSql})
      `,
      [mailboxId, folderId, remoteFolder]
    );

    deletedAssetTargets.push(
      ...(await markComposeUploadsDeletedByMessageIds(
        connection,
        rowsToDelete.map((row) => row.id)
      ))
    );
    await enqueueMailboxAttachmentStorageDeletesByMessageIds(
      connection,
      rowsToDelete.map((row) => row.id)
    );

    await connection.query(
      `
        DELETE FROM mailbox_messages
        WHERE mailbox_id = ?
          AND folder_id = ?
          AND remote_folder = ?
          AND NOT (${preserveLocalOutboundSql})
      `,
      [mailboxId, folderId, remoteFolder]
    );
  }

  const [existingRemoteUidRows] = await connection.query<
    (RowDataPacket & {
      attachments_indexed_at: Date | null;
      id: number;
      remote_uid: number;
    })[]
  >(
    `
      SELECT id, remote_uid, attachments_indexed_at
      FROM mailbox_messages
      WHERE mailbox_id = ?
        AND folder_id = ?
        AND remote_folder = ?
        AND remote_uid IS NOT NULL
    `,
    [mailboxId, folderId, remoteFolder]
  );
  const existingMessagesByRemoteUid = new Map(
    existingRemoteUidRows.map((row) => [
      Number(row.remote_uid),
      {
        attachmentsIndexedAt: row.attachments_indexed_at,
        id: row.id
      }
    ])
  );

  for (const message of identifiedMessages) {
    if (!message.contentLoaded) {
      const normalizedRemoteFlags = normalizeRemoteFlags(message.remoteFlags);
      const existingMessage = existingMessagesByRemoteUid.get(message.remoteUid);

      if (!existingMessage) {
        const messageId = await upsertMailboxMessageRow(connection, {
          bodyHtml: null,
          bodyText: "",
          direction: message.direction,
          folderId,
          fromAddress: message.fromAddress,
          fromName: message.fromName,
          isRead: message.isRead,
          isStarred: hasRemoteFlag(message.remoteFlags, "\\Flagged"),
          mailboxId,
          messageIdHeader: message.messageIdHeader,
          preserveExistingTransport: true,
          rawSource: null,
          receivedAt: message.receivedAt,
          remoteInternalDate: message.remoteInternalDate,
          remoteFlags: normalizedRemoteFlags,
          remoteFolder: message.remoteFolder,
          remoteUid: message.remoteUid,
          snippet: "",
          subject: message.subject,
          toAddresses: message.toAddresses,
          transportResponse: normalizeRemoteTransportResponse(message),
          transportStatus: message.transportStatus
        });
        if (message.attachmentMetadataLoaded) {
          await replaceMailboxMessageAttachmentMetadata(connection, messageId, message.attachments);
        }
        existingMessagesByRemoteUid.set(message.remoteUid, {
          attachmentsIndexedAt: message.attachmentMetadataLoaded ? new Date() : null,
          id: messageId
        });
        continue;
      }

      await connection.query(
        `
          UPDATE mailbox_messages
          SET
            folder_id = ?,
            remote_flags = ?,
            is_read = ?,
            is_starred = ?,
            synced_at = NOW()
          WHERE mailbox_id = ?
            AND remote_folder = ?
            AND remote_uid = ?
            AND (
              folder_id <> ?
              OR NOT (remote_flags <=> ?)
              OR is_read <> ?
              OR is_starred <> ?
            )
        `,
        [
          folderId,
          normalizedRemoteFlags,
          message.isRead ? 1 : 0,
          hasRemoteFlag(message.remoteFlags, "\\Flagged") ? 1 : 0,
          mailboxId,
          message.remoteFolder,
          message.remoteUid,
          folderId,
          normalizedRemoteFlags,
          message.isRead ? 1 : 0,
          hasRemoteFlag(message.remoteFlags, "\\Flagged") ? 1 : 0
        ]
      );
      if (message.attachmentMetadataLoaded && !existingMessage.attachmentsIndexedAt) {
        await replaceMailboxMessageAttachmentMetadata(connection, existingMessage.id, message.attachments);
        existingMessage.attachmentsIndexedAt = new Date();
      }
      continue;
    }

    const messageId = await upsertMailboxMessageRow(connection, {
      bodyHtml: message.bodyHtml,
      bodyText: message.bodyText,
      direction: message.direction,
      folderId,
      fromAddress: message.fromAddress,
      fromName: message.fromName,
      isRead: message.isRead,
      isStarred: hasRemoteFlag(message.remoteFlags, "\\Flagged"),
      mailboxId,
      messageIdHeader: message.messageIdHeader,
      preserveExistingTransport: true,
      rawSource: message.rawSource,
      receivedAt: message.receivedAt,
      remoteInternalDate: message.remoteInternalDate,
      remoteFlags: normalizeRemoteFlags(message.remoteFlags),
      remoteFolder: message.remoteFolder,
      remoteUid: message.remoteUid,
      snippet: message.snippet,
      subject: message.subject,
      toAddresses: message.toAddresses,
      transportResponse: normalizeRemoteTransportResponse(message),
      transportStatus: message.transportStatus
    });
    await replaceMailboxMessageAttachmentMetadata(connection, messageId, message.attachments);
  }

  return deletedAssetTargets;
}

async function syncMailboxCacheByEmail(
  email: string,
  currentFolder?: string,
  options?: {
    deferHydration?: boolean;
    fullMetadata?: boolean;
    mailboxId?: number;
  }
) {
  const user = await getUserByEmail(email);

  if (!user) {
    throw new Error("user-not-found");
  }

  const mailbox = options?.mailboxId
    ? await getMailboxByUserIdAndId(user.id, options.mailboxId)
    : await getLatestMailboxByUserId(user.id);

  if (!mailbox) {
    throw new Error("mailbox-not-found");
  }

  const syncResult = await withMailboxSyncQueue(mailbox.id, async () => {
    const syncResult = await withDistributedMailboxSyncLock(mailbox.id, () =>
      syncMailboxCacheForMailbox(user, mailbox, currentFolder, {
        fullMetadata: options?.fullMetadata,
        metadataOnly: true
      })
    );

    if (!syncResult.acquired) {
      return null;
    }

    return syncResult.value;
  });

  const folderSystemName = normalizeFolderSystemName(currentFolder);
  const startHydrationTask = (hydrationFolderSystemName: string) =>
    hydrateMailboxContent(user, mailbox, hydrationFolderSystemName).catch((error) => {
      const message = error instanceof Error ? error.message : "Mailbox hydration failed";
      console.warn(`[mailbox-sync] hydration failed mailbox=${mailbox.email} message=${message}`);
    });

  if (!syncResult) {
    const observed = await waitForMailboxSyncCompletion(mailbox.id, mailbox.last_sync_at);

    return {
      // A competing request can finish a metadata-only sync and intentionally
      // defer hydration. Actively take over hydration instead of only waiting
      // for work that may never have been started.
      hydration: observed ? startHydrationTask(folderSystemName) : null,
      startHydration: null,
      updated: observed
    };
  }

  let hydration: Promise<void> | null = null;
  const startHydration = syncResult.pendingContent
    ? () => {
        if (hydration) {
          return hydration;
        }

        hydration = startHydrationTask(syncResult.folderSystemName);

        return hydration;
      }
    : null;

  if (startHydration && !options?.deferHydration) {
    hydration = startHydration();
  }

  if (!startHydration) {
    void triggerMailboxPostSyncByOwnerEmail(user.email, syncResult.folderSystemName);
  }

  return {
    hydration,
    startHydration,
    updated: true
  };
}

async function syncMailboxCacheForMailbox(
  user: UserRow,
  mailbox: MailboxRow,
  currentFolder?: string,
  options?: {
    fullMetadata?: boolean;
    metadataOnly?: boolean;
  }
) {
  const folderSystemName = normalizeFolderSystemName(currentFolder);
  await ensureOfficialMailSchema();
  const [folderRows] = await getDbPool().query<
    (RowDataPacket & {
      id: number;
      last_synced_at: Date | null;
      system_name: string;
      remote_name: string | null;
      remote_total: number;
      name: string;
    })[]
  >(
    `
      SELECT id, system_name, remote_name, remote_total, last_synced_at, name
      FROM mailbox_folders
      WHERE mailbox_id = ?
    `,
    [mailbox.id]
  );
  const requestedFolderRow =
    folderRows.find((row) => row.system_name === folderSystemName) ??
    folderRows.find((row) => row.system_name === "inbox") ??
    null;

  const [knownRemoteUidRows] = requestedFolderRow
    ? await getDbPool().query<(RowDataPacket & { content_loaded: number; remote_uid: number })[]>(
        `
          SELECT remote_uid, raw_source IS NOT NULL AS content_loaded
          FROM mailbox_messages
          WHERE mailbox_id = ?
            AND folder_id = ?
            AND remote_uid IS NOT NULL
        `,
        [mailbox.id, requestedFolderRow.id]
      )
    : [[]];
  const knownFolderRemoteUids = new Set(knownRemoteUidRows.map((row) => Number(row.remote_uid)));
  const knownRemoteUids = new Set(
    knownRemoteUidRows.filter((row) => Boolean(row.content_loaded)).map((row) => Number(row.remote_uid))
  );
  const [localRemoteCountRows] = requestedFolderRow
    ? await getDbPool().query<CountTotalRow[]>(
        `
          SELECT COUNT(*) AS total
          FROM mailbox_messages
          WHERE mailbox_id = ?
            AND folder_id = ?
            AND remote_uid IS NOT NULL
        `,
        [mailbox.id, requestedFolderRow.id]
      )
    : [[]];
  const localRemoteCount = Number(localRemoteCountRows[0]?.total ?? 0);
  const fullMetadataSnapshot =
    options?.metadataOnly === true &&
    (options.fullMetadata === true ||
      !requestedFolderRow?.last_synced_at ||
      (mailbox.message_visibility_cutoff_at === null && Number(requestedFolderRow.remote_total) !== localRemoteCount));

  try {
    const credentials = toMailboxCredentials(mailbox);
    const remoteSync = await retryMailboxTransport(() =>
      syncRemoteMailbox(
        credentials,
        isSystemMailboxFolderSystemName(requestedFolderRow?.system_name)
          ? requestedFolderRow.system_name
          : requestedFolderRow
            ? {
                name: requestedFolderRow.name,
                remoteName: requestedFolderRow.remote_name || requestedFolderRow.name
              }
            : "inbox",
        {
          fullMetadataSnapshot,
          knownRemoteUids: [...knownRemoteUids],
          loadMissingContent: !options?.metadataOnly
        }
      )
    );
    const visibleCurrentFolderMessages = filterVisibleRemoteMailboxMessages(
      remoteSync.currentFolderMessages,
      mailbox.message_visibility_cutoff_at
    );
    const newlyDiscoveredMessages = visibleCurrentFolderMessages.filter(
      (message) => !knownFolderRemoteUids.has(message.remoteUid)
    );
    const candidateMessageIdHeaders = [
      ...new Set(newlyDiscoveredMessages.map((message) => message.messageIdHeader?.trim() ?? "").filter(Boolean))
    ];
    const [previouslyKnownMessageRows] =
      candidateMessageIdHeaders.length > 0
        ? await getDbPool().query<(RowDataPacket & { message_id_header: string })[]>(
            `
              SELECT DISTINCT message_id_header
              FROM mailbox_messages
              WHERE mailbox_id = ?
                AND remote_folder IS NOT NULL
                AND remote_uid IS NOT NULL
                AND message_id_header IN (${candidateMessageIdHeaders.map(() => "?").join(", ")})
            `,
            [mailbox.id, ...candidateMessageIdHeaders]
          )
        : [[]];
    const previouslyKnownMessageIdHeaders = new Set(
      previouslyKnownMessageRows.map((row) => row.message_id_header.trim())
    );
    const newlyReceivedRemoteUids = new Set(
      newlyDiscoveredMessages
        .filter((message) => {
          const messageIdHeader = message.messageIdHeader?.trim() ?? "";
          return !messageIdHeader || !previouslyKnownMessageIdHeaders.has(messageIdHeader);
        })
        .map((message) => message.remoteUid)
    );
    const deletedAssetTargets = await retryMailboxSyncWrite(() =>
      withTransaction(async (connection) => {
        const attemptDeletedAssetTargets: ComposeUploadCleanupTarget[] = [];

        await ensureLocalMailboxFolders(connection, mailbox.id);
        const syncedFolderRows = await getFolderRowsByMailboxId(connection, mailbox.id);
        const activeFolderRow =
          syncedFolderRows.find((row) => row.system_name === (requestedFolderRow?.system_name ?? "inbox")) ??
          syncedFolderRows.find((row) => row.system_name === "inbox") ??
          null;
        const folderIdBySystemName = await syncRemoteFolderStateToDatabase(connection, mailbox.id, remoteSync.folders);
        const syncedFolder = remoteSync.currentFolderState;

        if (syncedFolder && activeFolderRow) {
          const folderId =
            folderIdBySystemName.get(activeFolderRow.system_name) ??
            (activeFolderRow.system_name === requestedFolderRow?.system_name ? activeFolderRow.id : undefined);

          if (folderId) {
            attemptDeletedAssetTargets.push(
              ...(await replaceCurrentFolderMessages(
                connection,
                mailbox.id,
                folderId,
                syncedFolder.remoteName,
                visibleCurrentFolderMessages,
                { deleteMissing: fullMetadataSnapshot }
              ))
            );

            if (activeFolderRow.system_name === "inbox") {
              const moveFolderRows = await getMailboxFolderRows(connection, mailbox.id);
              await applyMailboxSenderSpamRulesToInbox(connection, mailbox, moveFolderRows, newlyReceivedRemoteUids);
              await applyMailboxSenderMoveRulesToInbox(connection, mailbox, moveFolderRows, newlyReceivedRemoteUids);
            }

            await connection.query(
              `
                UPDATE mailbox_folders
                SET
                  remote_name = ?,
                  remote_total = ?,
                  remote_unseen = ?,
                  last_synced_at = NOW(),
                  updated_at = NOW()
                WHERE id = ?
                  AND (
                    NOT (remote_name <=> ?)
                    OR remote_total <> ?
                    OR remote_unseen <> ?
                  )
              `,
              [
                syncedFolder.remoteName,
                syncedFolder.totalCount,
                syncedFolder.unreadCount,
                folderId,
                syncedFolder.remoteName,
                syncedFolder.totalCount,
                syncedFolder.unreadCount
              ]
            );
          }
        }

        return attemptDeletedAssetTargets;
      })
    );
    await cleanupComposeUploadArtifacts(getDbPool(), deletedAssetTargets).catch(() => undefined);

    await retryMailboxSyncWrite(() =>
      updateMailboxSyncState(mailbox.id, {
        lastSyncAt: new Date(),
        lastSyncError: null
      })
    );
    if (!options?.metadataOnly && requestedFolderRow) {
      if (remoteSync.contentFailures.length > 0) {
        await retryMailboxSyncWrite(() =>
          updateMailboxFolderContentSyncState(requestedFolderRow.id, {
            lastContentSyncError: `원문 동기화 실패 UID: ${remoteSync.contentFailures.join(", ")}`.slice(0, 1000)
          })
        );
        await retryMailboxSyncWrite(() => scheduleMailboxSyncRetry(mailbox.id));
      } else {
        await retryMailboxSyncWrite(() =>
          updateMailboxFolderContentSyncState(requestedFolderRow.id, {
            lastContentSyncAt: new Date(),
            lastContentSyncError: null
          })
        );
      }
    }

    return {
      folderSystemName: requestedFolderRow?.system_name ?? "inbox",
      pendingContent:
        remoteSync.contentFailures.length > 0 ||
        visibleCurrentFolderMessages.some(
          (message) => !message.contentLoaded && !knownRemoteUids.has(message.remoteUid)
        )
    };
  } catch (error) {
    const message = error instanceof Error ? error.message : "Mailbox sync failed";

    if (options?.metadataOnly || !requestedFolderRow) {
      await retryMailboxSyncWrite(() =>
        updateMailboxSyncState(mailbox.id, {
          lastSyncError: message.slice(0, 1000)
        })
      ).catch(() => undefined);
    } else {
      await retryMailboxSyncWrite(() =>
        updateMailboxFolderContentSyncState(requestedFolderRow.id, {
          lastContentSyncError: message.slice(0, 1000)
        })
      ).catch(() => undefined);
    }
    await retryMailboxSyncWrite(() => scheduleMailboxSyncRetry(mailbox.id)).catch(() => undefined);

    throw error;
  }
}

async function hydrateMailboxContent(user: UserRow, mailbox: MailboxRow, folderSystemName: string) {
  const hydrationResult = await withMailboxSyncQueue(mailbox.id, async () => {
    const syncResult = await withDistributedMailboxSyncLock(mailbox.id, () =>
      syncMailboxCacheForMailbox(user, mailbox, folderSystemName)
    );

    return syncResult.acquired ? syncResult.value : null;
  });

  if (!hydrationResult) {
    await waitForMailboxHydration(mailbox.id, folderSystemName);
    return;
  }

  const postSync = triggerMailboxPostSyncByOwnerEmail(user.email, hydrationResult.folderSystemName);

  await Promise.race([postSync, waitForMailboxSyncRetry(MAILBOX_SYNC_OBSERVE_INTERVAL_MS)]);
  void postSync;
}

async function setMailboxMessageReadState(email: string, messageId: number | null, isRead: boolean) {
  if (!messageId) {
    return false;
  }

  const user = await getUserByEmail(email);

  if (!user) {
    return false;
  }

  const mailbox = await getLatestMailboxByUserId(user.id);

  if (!mailbox) {
    return false;
  }

  const [rows] = await getDbPool().query<
    (RowDataPacket & {
      id: number;
      is_read: number;
      remote_flags: string | null;
      remote_folder: string | null;
      remote_uid: number | null;
    })[]
  >(
    `
      SELECT id, is_read, remote_flags, remote_folder, remote_uid
      FROM mailbox_messages
      WHERE id = ? AND mailbox_id = ?
      LIMIT 1
    `,
    [messageId, mailbox.id]
  );

  const message = rows[0];

  if (!message) {
    return false;
  }

  if (Boolean(message.is_read) === isRead) {
    return false;
  }

  const nextRemoteFlags = applyRemoteFlagString(message.remote_flags, "\\Seen", isRead);

  await getDbPool().query(
    `
      UPDATE mailbox_messages
      SET is_read = ?, remote_flags = ?, updated_at = NOW()
      WHERE id = ?
    `,
    [isRead ? 1 : 0, nextRemoteFlags, message.id]
  );

  await updateMailboxSyncState(mailbox.id, {
    lastSyncAt: new Date(),
    lastSyncError: mailbox.last_sync_error
  });

  if (mailbox.password_ciphertext && message.remote_folder && message.remote_uid) {
    const credentials = toMailboxCredentials(mailbox);
    const syncErrorPrefix = isRead ? "mailbox-read-sync-failed" : "mailbox-unread-sync-failed";

    after(async () => {
      try {
        const updated = await updateRemoteMessageFlag(
          credentials,
          message.remote_folder!,
          message.remote_uid!,
          "\\Seen",
          isRead
        );

        if (!updated) {
          await updateMailboxSyncState(mailbox.id, {
            lastSyncAt: mailbox.last_sync_at,
            lastSyncError: `${syncErrorPrefix}:${message.id}`.slice(0, 1000)
          });
          return;
        }

        if (mailbox.last_sync_error?.startsWith(syncErrorPrefix)) {
          await updateMailboxSyncState(mailbox.id, {
            lastSyncAt: mailbox.last_sync_at,
            lastSyncError: null
          });
        }
      } catch (error) {
        const detail = error instanceof Error ? `${syncErrorPrefix}:${error.message}` : syncErrorPrefix;

        console.error("Mailbox read state sync failed", {
          email,
          isRead,
          messageId: message.id,
          error
        });

        await updateMailboxSyncState(mailbox.id, {
          lastSyncAt: mailbox.last_sync_at,
          lastSyncError: detail.slice(0, 1000)
        });
      }
    });
  }

  return true;
}

async function markMailboxMessageAsRead(email: string, messageId: number | null) {
  return setMailboxMessageReadState(email, messageId, true);
}

async function getMailboxMessageStarContext(email: string, messageId: number | null) {
  if (!messageId) {
    throw new Error("message-not-found");
  }

  const user = await getUserByEmail(email);

  if (!user) {
    throw new Error("user-not-found");
  }

  const mailbox = await getLatestMailboxByUserId(user.id);

  if (!mailbox) {
    throw new Error("mailbox-not-found");
  }

  const [rows] = await getDbPool().query<
    (RowDataPacket & {
      id: number;
      is_starred: number;
      remote_flags: string | null;
      remote_folder: string | null;
      remote_uid: number | null;
    })[]
  >(
    `
      SELECT id, is_starred, remote_flags, remote_folder, remote_uid
      FROM mailbox_messages
      WHERE id = ? AND mailbox_id = ?
      LIMIT 1
    `,
    [messageId, mailbox.id]
  );

  const message = rows[0];

  if (!message) {
    throw new Error("message-not-found");
  }

  return { mailbox, message };
}

async function persistMailboxMessageStarState(input: {
  mailbox: MailboxRow;
  message: {
    id: number;
    remote_flags: string | null;
    remote_folder: string | null;
    remote_uid: number | null;
  };
  nextStarred: boolean;
}) {
  const { mailbox, message, nextStarred } = input;

  if (mailbox.password_ciphertext && message.remote_folder && message.remote_uid) {
    const updated = await updateRemoteMessageFlag(
      toMailboxCredentials(mailbox),
      message.remote_folder,
      message.remote_uid,
      "\\Flagged",
      nextStarred
    );

    if (!updated) {
      throw new Error("mailbox-flag-failed");
    }
  }

  await getDbPool().query(
    `
      UPDATE mailbox_messages
      SET
        is_starred = ?,
        remote_flags = ?,
        updated_at = NOW()
      WHERE id = ?
    `,
    [nextStarred ? 1 : 0, applyRemoteFlagString(message.remote_flags, "\\Flagged", nextStarred), message.id]
  );

  return nextStarred;
}

export async function setMailboxMessageStarByEmail(email: string, messageId: number | null, nextStarred: boolean) {
  const { mailbox, message } = await getMailboxMessageStarContext(email, messageId);

  if (Boolean(message.is_starred) === nextStarred) {
    return nextStarred;
  }

  return persistMailboxMessageStarState({ mailbox, message, nextStarred });
}

export async function toggleMailboxMessageStarByEmail(email: string, messageId: number | null) {
  const { mailbox, message } = await getMailboxMessageStarContext(email, messageId);
  const nextStarred = !Boolean(message.is_starred);
  return persistMailboxMessageStarState({ mailbox, message, nextStarred });
}

export async function markMailboxMessagesAsReadByEmail(
  email: string,
  messageIds: Array<number | string>,
  isRead = true
) {
  const normalizedIds = normalizeMessageIds(messageIds);

  if (normalizedIds.length === 0) {
    return 0;
  }

  let updatedCount = 0;

  for (const messageId of normalizedIds) {
    if (await setMailboxMessageReadState(email, messageId, isRead)) {
      updatedCount += 1;
    }
  }

  return updatedCount;
}

async function getMailboxFolderRows(connection: PoolConnection, mailboxId: number) {
  const [folderRows] = await connection.query<MailboxFolderRowBasic[]>(
    `
      SELECT id, system_name, remote_name, name, sort_order
      FROM mailbox_folders
      WHERE mailbox_id = ?
      ORDER BY sort_order ASC, id ASC
    `,
    [mailboxId]
  );

  return folderRows;
}

async function getMailboxSenderDispositions(connection: PoolConnection, mailboxId: number) {
  const [rows] = await connection.query<MailboxSenderRuleRow[]>(
    `
      SELECT id, mailbox_id, sender_address, rule_action, created_at, updated_at
      FROM mailbox_sender_rules
      WHERE mailbox_id = ?
      ORDER BY updated_at DESC, id DESC
    `,
    [mailboxId]
  );

  return rows
    .map((row) => ({
      action: row.rule_action,
      createdAt: row.created_at.toISOString(),
      id: Number(row.id),
      senderAddress: normalizeSenderAddress(row.sender_address),
      updatedAt: row.updated_at.toISOString()
    }))
    .filter((row) => Boolean(row.senderAddress));
}

async function getMailboxCustomSpamRules(connection: PoolConnection, mailboxId: number) {
  const [rows] = await connection.query<MailboxCustomSpamRuleRow[]>(
    `
      SELECT
        id,
        match_field,
        match_value,
        rule_action,
        enabled,
        priority,
        created_at,
        updated_at
      FROM mailbox_custom_spam_rules
      WHERE mailbox_id = ?
      ORDER BY priority DESC, updated_at DESC, id DESC
    `,
    [mailboxId]
  );

  return rows.map((row) => ({
    action: row.rule_action,
    createdAt: row.created_at.toISOString(),
    enabled: Boolean(row.enabled),
    field: row.match_field,
    id: Number(row.id),
    priority: Number(row.priority),
    updatedAt: row.updated_at.toISOString(),
    value: row.match_value
  } satisfies MailboxCustomSpamRule));
}

function mapMailboxSpamGeneralSettings(row?: MailboxSpamSettingsRow): MailboxSpamGeneralSettings {
  if (!row) return { ...DEFAULT_MAILBOX_SPAM_GENERAL_SETTINGS };
  const languageFilterLanguages = normalizeMailboxSpamLanguages(row.language_filter_languages);
  return {
    autoMoveSpam: Boolean(row.auto_move_spam),
    blockReportedSender: Boolean(row.block_reported_sender),
    detectBounceSpoofing: Boolean(row.detect_bounce_spoofing),
    detectMissingRecipient: Boolean(row.detect_missing_recipient),
    detectSelfSpoofing: Boolean(row.detect_self_spoofing),
    detectUnknownSender: Boolean(row.detect_unknown_sender),
    expiredMessageAction: row.expired_message_action === "trash" ? "trash" : "delete",
    hideRemoteContent: Boolean(row.hide_remote_content),
    hideSpamFolder: Boolean(row.hide_spam_folder),
    languageFilterLanguages: languageFilterLanguages.length > 0 ? languageFilterLanguages : ["en"],
    languageFilterMode: row.language_filter_mode === "all" ? "all" : "selected",
    languageFilterEnabled: Boolean(row.language_filter_enabled),
    processPreviousReported: Boolean(row.process_previous_reported),
    reportedMessageAction: row.reported_message_action === "delete" ? "delete" : "spam",
    retentionDays: Math.max(1, Math.min(365, Number(row.retention_days) || 30))
  };
}

async function getMailboxSpamGeneralSettings(connection: PoolConnection, mailboxId: number) {
  const [rows] = await connection.query<MailboxSpamSettingsRow[]>(
    `
      SELECT
        auto_move_spam,
        detect_self_spoofing,
        detect_bounce_spoofing,
        detect_missing_recipient,
        detect_unknown_sender,
        language_filter_enabled,
        language_filter_mode,
        language_filter_languages,
        reported_message_action,
        block_reported_sender,
        process_previous_reported,
        hide_spam_folder,
        retention_days,
        expired_message_action,
        hide_remote_content
      FROM mailbox_spam_settings
      WHERE mailbox_id = ?
      LIMIT 1
    `,
    [mailboxId]
  );
  return mapMailboxSpamGeneralSettings(rows[0]);
}

async function getMailboxSenderMoveRules(connection: PoolConnection, mailboxId: number) {
  const [rows] = await connection.query<MailboxSenderMoveRuleRow[]>(
    `
      SELECT
        rule.id,
        rule.sender_address,
        rule.target_folder_id,
        target.name AS target_folder_name,
        target.system_name AS target_folder_system_name,
        rule.created_at,
        rule.updated_at
      FROM mailbox_sender_move_rules rule
      INNER JOIN mailbox_folders target ON target.id = rule.target_folder_id
      WHERE rule.mailbox_id = ?
    `,
    [mailboxId]
  );

  return rows
    .map((row) => ({
      createdAt: row.created_at.toISOString(),
      id: Number(row.id),
      senderAddress: normalizeSenderAddress(row.sender_address),
      targetFolderId: Number(row.target_folder_id),
      targetFolderName: row.target_folder_name,
      targetFolderSystemName: row.target_folder_system_name,
      updatedAt: row.updated_at.toISOString()
    }))
    .filter((row) => Boolean(row.senderAddress) && isCustomMailboxFolderSystemName(row.targetFolderSystemName));
}

function mapMailboxContactRow(row: MailboxContactRow): MailboxContact {
  return {
    autoMoveFolderName: row.auto_move_folder_name,
    autoMoveFolderSystemName: row.auto_move_folder_system_name,
    companyName: row.company_name,
    createdAt: row.created_at.toISOString(),
    displayName: row.display_name,
    email: normalizeSenderAddress(row.email),
    groupName: row.group_name,
    id: Number(row.id),
    isSaved: true,
    isVip: Boolean(row.is_vip),
    memo: row.memo ?? "",
    phone: row.phone,
    updatedAt: row.updated_at.toISOString()
  };
}

async function getMailboxContacts(connection: PoolConnection, mailboxId: number) {
  const [rows] = await connection.query<MailboxContactRow[]>(
    `
      SELECT
        contact.id,
        contact.email,
        contact.display_name,
        contact.company_name,
        contact.phone,
        contact.group_name,
        contact.memo,
        contact.is_vip,
        contact.created_at,
        contact.updated_at,
        folder.name AS auto_move_folder_name,
        folder.system_name AS auto_move_folder_system_name
      FROM mailbox_contacts contact
      LEFT JOIN mailbox_sender_move_rules move_rule ON move_rule.id = contact.auto_move_rule_id
      LEFT JOIN mailbox_folders folder ON folder.id = move_rule.target_folder_id
      WHERE contact.mailbox_id = ?
      ORDER BY contact.is_vip DESC, contact.display_name ASC, contact.email ASC
    `,
    [mailboxId]
  );

  return rows.map(mapMailboxContactRow);
}

async function findInboundSenderRows(
  connection: PoolConnection,
  mailboxId: number,
  senderAddresses: string[],
  options?: {
    excludePendingMoves?: boolean;
    releaseSpam?: boolean;
    sourceFolder?: string;
  }
) {
  const normalizedSenderAddresses = [
    ...new Set(senderAddresses.map((senderAddress) => normalizeSenderAddress(senderAddress)).filter(Boolean))
  ];

  if (normalizedSenderAddresses.length === 0) {
    return [] as MailboxMoveMessageRow[];
  }

  if (options?.sourceFolder) {
    const [rows] = await connection.query<MailboxMoveMessageRow[]>(
      `
        SELECT
          mm.id,
          mm.folder_id,
          mm.direction,
          mm.from_address,
          mm.remote_folder,
          mm.remote_uid
        FROM mailbox_messages mm
        INNER JOIN mailbox_folders f ON f.id = mm.folder_id
        WHERE mm.mailbox_id = ?
          AND mm.direction = 'inbound'
          AND f.system_name = ?
          AND LOWER(mm.from_address) IN (${normalizedSenderAddresses.map(() => "?").join(", ")})
          ${options.excludePendingMoves ? `AND ${PENDING_MAILBOX_MOVE_EXCLUSION_SQL}` : ""}
      `,
      [mailboxId, options.sourceFolder, ...normalizedSenderAddresses]
    );

    return rows;
  }

  const [rows] = await connection.query<MailboxMoveMessageRow[]>(
    `
      SELECT id, folder_id, direction, from_address, remote_folder, remote_uid
      FROM mailbox_messages
      WHERE mailbox_id = ?
        AND direction = 'inbound'
        AND LOWER(from_address) IN (${normalizedSenderAddresses.map(() => "?").join(", ")})
    `,
    [mailboxId, ...normalizedSenderAddresses]
  );

  return rows;
}

async function moveMailboxRowsToFolder(
  connection: PoolConnection,
  mailbox: MailboxRow,
  folderRows: MailboxFolderRowBasic[],
  rowsToMove: MailboxMoveMessageRow[],
  targetFolder: string
) {
  const targetFolderRow = folderRows.find((row) => row.system_name === targetFolder);

  if (!targetFolderRow) {
    throw new Error("folder-not-found");
  }

  if (targetFolder === "sent" && rowsToMove.some((message) => message.direction !== "outbound")) {
    throw new Error("sent-folder-only-outbound");
  }

  if (rowsToMove.length === 0) {
    return {
      deletedCount: 0,
      deletedMessageIds: [] as number[],
      movedCount: 0,
      movedMessageIds: [] as number[],
      spamSenderCount: 0
    } satisfies MailboxMoveActionResult;
  }

  const destinationRemoteFolder =
    targetFolderRow.remote_name ||
    (isSystemMailboxFolderSystemName(targetFolder) ? remoteFolderFallbackName(targetFolder) : targetFolderRow.name);
  const credentials = mailbox.password_ciphertext ? toMailboxCredentials(mailbox) : null;
  const invalidatedFolderIds = new Set<number>();
  const movedMessageIds: number[] = [];
  const [readStateRows] = await connection.query<(RowDataPacket & { id: number; is_read: number })[]>(
    `
      SELECT id, is_read
      FROM mailbox_messages
      WHERE mailbox_id = ?
        AND id IN (${rowsToMove.map(() => "?").join(", ")})
    `,
    [mailbox.id, ...rowsToMove.map((message) => message.id)]
  );
  const unreadMessageIds = new Set(
    readStateRows.filter((row) => !Boolean(row.is_read)).map((row) => Number(row.id))
  );
  const folderCountDeltas = new Map<number, { count: number; unreadCount: number }>();
  const addFolderCountDelta = (folderId: number, count: number, unreadCount: number) => {
    const current = folderCountDeltas.get(folderId) ?? { count: 0, unreadCount: 0 };
    folderCountDeltas.set(folderId, {
      count: current.count + count,
      unreadCount: current.unreadCount + unreadCount
    });
  };

  const mergeRemoteDuplicateRows = async (input: {
    keepMessageId: number;
    remoteFolder: string;
    remoteUid: number;
  }) => {
    const [duplicates] = await connection.query<MailboxRemoteDuplicateRow[]>(
      `
        SELECT id, folder_id
        FROM mailbox_messages
        WHERE mailbox_id = ?
          AND remote_folder = ?
          AND remote_uid = ?
          AND id <> ?
      `,
      [mailbox.id, input.remoteFolder, input.remoteUid, input.keepMessageId]
    );

    if (duplicates.length === 0) {
      return;
    }

    const duplicateIds = duplicates.map((row) => row.id);

    await connection.query(
      `
        UPDATE mailbox_delivery_logs
        SET mailbox_message_id = ?
        WHERE mailbox_message_id IN (${duplicateIds.map(() => "?").join(", ")})
      `,
      [input.keepMessageId, ...duplicateIds]
    );

    await connection.query(
      `
        UPDATE mailbox_ai_assist_threads
        SET source_message_id = ?
        WHERE source_message_id IN (${duplicateIds.map(() => "?").join(", ")})
          AND NOT EXISTS (
            SELECT 1
            FROM (
              SELECT source_message_id
              FROM mailbox_ai_assist_threads
              WHERE source_message_id = ?
            ) existing_thread
          )
      `,
      [input.keepMessageId, ...duplicateIds, input.keepMessageId]
    );

    await relinkComposeUploadsToMessageId(connection, duplicateIds, input.keepMessageId);
    await enqueueMailboxAttachmentStorageDeletesByMessageIds(connection, duplicateIds);

    duplicates.forEach((row) => invalidatedFolderIds.add(row.folder_id));

    await connection.query(
      `
        DELETE FROM mailbox_messages
        WHERE id IN (${duplicateIds.map(() => "?").join(", ")})
      `,
      duplicateIds
    );
  };

  for (const message of rowsToMove) {
    if (message.folder_id === targetFolderRow.id) {
      continue;
    }

    invalidatedFolderIds.add(message.folder_id);
    invalidatedFolderIds.add(targetFolderRow.id);

    let nextRemoteFolder = destinationRemoteFolder;
    let nextRemoteUid = message.remote_uid;

    if (message.direction === "draft" && !message.remote_uid) {
      const [pendingDraft] = await connection.query<RowDataPacket[]>(
        "SELECT id FROM mailbox_messages WHERE id = ? AND transport_status = 'queued'",
        [message.id],
      );
      // A queued move stays visibly projected, while the draft worker assigns
      // its remote UID before the move worker can act on that identity.
      if (pendingDraft.length) throw new Error("mailbox-busy");
    }

    if (credentials && message.remote_folder && message.remote_uid) {
      const moved: RemoteMailboxMoveResult | false = await moveRemoteMessage(
        credentials,
        message.remote_folder,
        message.remote_uid,
        destinationRemoteFolder
      );

      if (!moved) {
        throw new Error("mailbox-move-failed");
      }

      nextRemoteFolder = moved.destination || destinationRemoteFolder;
      nextRemoteUid = moved.remoteUid;
    }

    if (nextRemoteFolder && nextRemoteUid) {
      await mergeRemoteDuplicateRows({
        keepMessageId: message.id,
        remoteFolder: nextRemoteFolder,
        remoteUid: nextRemoteUid
      });
    }

    await connection.query(
      `
        UPDATE mailbox_messages
        SET
          folder_id = ?,
          remote_folder = ?,
          remote_uid = ?,
          updated_at = NOW()
        WHERE id = ?
      `,
      [targetFolderRow.id, nextRemoteFolder, nextRemoteUid, message.id]
    );

    movedMessageIds.push(message.id);
    const unreadDelta = unreadMessageIds.has(Number(message.id)) ? 1 : 0;
    addFolderCountDelta(Number(message.folder_id), -1, -unreadDelta);
    addFolderCountDelta(Number(targetFolderRow.id), 1, unreadDelta);
  }

  for (const [folderId, delta] of folderCountDeltas) {
    await connection.query(
      `
        UPDATE mailbox_folders
        SET
          remote_total = GREATEST(0, remote_total + ?),
          remote_unseen = GREATEST(0, remote_unseen + ?),
          last_synced_at = NULL,
          updated_at = NOW()
        WHERE id = ? AND mailbox_id = ?
      `,
      [delta.count, delta.unreadCount, folderId, mailbox.id]
    );
  }

  if (invalidatedFolderIds.size > 0) {
    await connection.query(
      `
        UPDATE mailbox_folders
        SET last_synced_at = NULL, updated_at = NOW()
        WHERE mailbox_id = ?
          AND id IN (${[...invalidatedFolderIds].map(() => "?").join(", ")})
      `,
      [mailbox.id, ...invalidatedFolderIds]
    );
  }

  if (isCustomMailboxFolderSystemName(targetFolder)) {
    await connection.query(
      `
        UPDATE mailbox_folders
        SET remote_name = COALESCE(remote_name, ?), updated_at = NOW()
        WHERE id = ?
      `,
      [destinationRemoteFolder, targetFolderRow.id]
    );
  }

  return {
    deletedCount: 0,
    deletedMessageIds: [] as number[],
    movedCount: movedMessageIds.length,
    movedMessageIds,
    spamSenderCount:
      targetFolder === "spam"
        ? new Set(rowsToMove.map((message) => normalizeSenderAddress(message.from_address)).filter(Boolean)).size
        : 0
  };
}

async function applyMailboxSenderSpamRulesToInbox(
  connection: PoolConnection,
  mailbox: MailboxRow,
  folderRows: MailboxFolderRowBasic[],
  newlyDiscoveredRemoteUids: Set<number>
) {
  if (newlyDiscoveredRemoteUids.size === 0) {
    return 0;
  }

  const [senderRules, customRules, generalSettings, contacts] = await Promise.all([
    getMailboxSenderDispositions(connection, mailbox.id),
    getMailboxCustomSpamRules(connection, mailbox.id),
    getMailboxSpamGeneralSettings(connection, mailbox.id),
    getMailboxContacts(connection, mailbox.id)
  ]);
  const enabledCustomRules = customRules.filter((rule) => rule.enabled && rule.value.trim());

  const hasAutomaticChecks =
    generalSettings.autoMoveSpam &&
    (generalSettings.detectSelfSpoofing ||
      generalSettings.detectBounceSpoofing ||
      generalSettings.detectMissingRecipient ||
      generalSettings.detectUnknownSender ||
      generalSettings.languageFilterEnabled);

  if (senderRules.length === 0 && enabledCustomRules.length === 0 && !hasAutomaticChecks) {
    return 0;
  }

  const inboxFolder = folderRows.find((folder) => folder.system_name === "inbox");
  if (!inboxFolder) {
    return 0;
  }

  const remoteUids = [...newlyDiscoveredRemoteUids];
  const [candidateRows] = await connection.query<MailboxSpamCandidateRow[]>(
    `
      SELECT
        id,
        folder_id,
        direction,
        from_address,
        to_addresses,
        subject,
        body_text,
        remote_folder,
        remote_uid
      FROM mailbox_messages
      WHERE mailbox_id = ?
        AND folder_id = ?
        AND direction = 'inbound'
        AND remote_uid IN (${remoteUids.map(() => "?").join(", ")})
    `,
    [mailbox.id, inboxFolder.id, ...remoteUids]
  );

  if (candidateRows.length === 0) {
    return 0;
  }

  const senderDispositionMap = new Map(
    senderRules.map((rule) => [rule.senderAddress, rule.action] as const)
  );
  const savedContactAddresses = new Set(contacts.map((contact) => contact.email));
  const mailboxAddress = normalizeSenderAddress(mailbox.email);
  const normalizeMatchText = (value: string) => value.trim().toLocaleLowerCase();
  const ruleMatches = (rule: MailboxCustomSpamRule, message: MailboxSpamCandidateRow) => {
    const expected = normalizeMatchText(rule.value);
    const sender = normalizeSenderAddress(message.from_address);
    const senderDomain = sender.split("@")[1] ?? "";
    const source =
      rule.field === "sender"
        ? sender
        : rule.field === "sender_domain"
          ? senderDomain
          : rule.field === "subject"
            ? normalizeMatchText(message.subject)
            : normalizeMatchText(message.body_text);

    return rule.field === "sender" || rule.field === "sender_domain"
      ? source === expected.replace(/^@/, "")
      : source.includes(expected);
  };

  const spamRows = candidateRows.filter((message) => {
    const sender = normalizeSenderAddress(message.from_address);
    const explicitDisposition = senderDispositionMap.get(sender);
    if (explicitDisposition) {
      return explicitDisposition === "spam";
    }

    const matchingCustomRule = enabledCustomRules.find((rule) => ruleMatches(rule, message));
    if (matchingCustomRule) {
      return matchingCustomRule.action === "spam";
    }

    if (!generalSettings.autoMoveSpam) return false;
    if (generalSettings.detectSelfSpoofing && sender === mailboxAddress) return true;
    if (
      generalSettings.detectBounceSpoofing &&
      /^(mailer-daemon|postmaster)@/i.test(sender) &&
      `${message.subject}\n${message.body_text}`.toLocaleLowerCase().includes(mailboxAddress)
    ) {
      return true;
    }
    if (
      generalSettings.detectMissingRecipient &&
      !splitRecipientList(message.to_addresses).some((recipient) => normalizeSenderAddress(recipient) === mailboxAddress)
    ) {
      return true;
    }
    if (generalSettings.languageFilterEnabled) {
      const detectedLanguage = detectMailboxSpamLanguage(message.subject, message.body_text);
      const blockedLanguages = generalSettings.languageFilterMode === "all"
        ? MAILBOX_SPAM_LANGUAGES
        : generalSettings.languageFilterLanguages;
      if (detectedLanguage && blockedLanguages.includes(detectedLanguage)) return true;
    }
    return generalSettings.detectUnknownSender && !savedContactAddresses.has(sender);
  });

  if (spamRows.length === 0) {
    return 0;
  }

  const moveResult = await moveMailboxRowsToFolder(connection, mailbox, folderRows, spamRows, "spam");

  return moveResult.movedCount;
}

async function applyMailboxSenderMoveRulesToInbox(
  connection: PoolConnection,
  mailbox: MailboxRow,
  folderRows: MailboxFolderRowBasic[],
  newlyDiscoveredRemoteUids: Set<number>
) {
  if (newlyDiscoveredRemoteUids.size === 0) {
    return 0;
  }

  const rules = await getMailboxSenderMoveRules(connection, mailbox.id);

  if (rules.length === 0) {
    return 0;
  }

  const inboxRows = await findInboundSenderRows(
    connection,
    mailbox.id,
    rules.map((rule) => rule.senderAddress),
    { excludePendingMoves: true, sourceFolder: "inbox" }
  );

  const newlyDiscoveredInboxRows = inboxRows.filter(
    (message) => message.remote_uid !== null && newlyDiscoveredRemoteUids.has(message.remote_uid)
  );

  if (newlyDiscoveredInboxRows.length === 0) {
    return 0;
  }

  const targetBySender = new Map(rules.map((rule) => [rule.senderAddress, rule.targetFolderSystemName]));
  const rowsByTarget = new Map<string, MailboxMoveMessageRow[]>();

  for (const message of newlyDiscoveredInboxRows) {
    const targetFolder = targetBySender.get(normalizeSenderAddress(message.from_address));

    if (!targetFolder) {
      continue;
    }

    const targetRows = rowsByTarget.get(targetFolder) ?? [];
    targetRows.push(message);
    rowsByTarget.set(targetFolder, targetRows);
  }

  let movedCount = 0;

  for (const [targetFolder, rows] of rowsByTarget) {
    const moveResult = await queueMailboxRowsToFolder(connection, mailbox.id, folderRows, rows, targetFolder);
    movedCount += moveResult.movedCount;
  }

  return movedCount;
}

export async function resolveMailboxMessageIdsByRemoteUids(
  email: string,
  remoteUidsInput: Array<number | string>,
  sourceFolderInput: string
) {
  const remoteUids = normalizeMessageIds(remoteUidsInput);

  if (remoteUids.length === 0) {
    return [] as number[];
  }

  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");

  const mailbox = await getLatestMailboxByUserId(user.id);
  if (!mailbox) throw new Error("mailbox-not-found");

  await ensureOfficialMailSchema();
  const sourceFolder = resolveMailboxRefreshFolder(sourceFolderInput);
  const [rows] = await getDbPool().query<(RowDataPacket & { id: number })[]>(
    `
      SELECT mm.id
      FROM mailbox_messages mm
      INNER JOIN mailbox_folders f ON f.id = mm.folder_id
      WHERE mm.mailbox_id = ?
        AND f.system_name = ?
        AND mm.remote_uid IN (${remoteUids.map(() => "?").join(", ")})
    `,
    [mailbox.id, sourceFolder, ...remoteUids]
  );

  return rows.map((row) => Number(row.id));
}

export async function getMailboxSenderMoveRuleForMessagesByEmail(email: string, messageIds: Array<number | string>) {
  const normalizedIds = normalizeMessageIds(messageIds);
  if (normalizedIds.length === 0) throw new Error("message-required");

  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");

  const mailbox = await getLatestMailboxByUserId(user.id);
  if (!mailbox) throw new Error("mailbox-not-found");

  await ensureOfficialMailSchema();
  return withTransaction(async (connection) => {
    const [selectedRows] = await connection.query<MailboxMoveMessageRow[]>(
      `
        SELECT id, folder_id, direction, from_address, remote_folder, remote_uid
        FROM mailbox_messages
        WHERE mailbox_id = ?
          AND id IN (${normalizedIds.map(() => "?").join(", ")})
      `,
      [mailbox.id, ...normalizedIds]
    );
    if (selectedRows.length !== normalizedIds.length) {
      throw new Error("message-not-found");
    }

    const senderAddresses = [
      ...new Set(
        selectedRows
          .filter((message) => message.direction === "inbound")
          .map((message) => normalizeSenderAddress(message.from_address))
          .filter(Boolean)
      )
    ];
    if (senderAddresses.length !== 1 || selectedRows.some((message) => message.direction !== "inbound")) {
      throw new Error("continue-move-requires-one-sender");
    }

    const rules = await getMailboxSenderMoveRules(connection, mailbox.id);
    return rules.find((rule) => rule.senderAddress === senderAddresses[0]) ?? null;
  });
}

export async function createMailboxSenderMoveRuleByEmail(
  email: string,
  messageIds: Array<number | string>,
  targetFolderInput: string,
  options?: {
    expectedExistingTargetFolder?: string;
    moveExistingMessages?: boolean;
    replaceExistingRule?: boolean;
    sourceFolder?: string;
  }
) {
  const normalizedIds = normalizeMessageIds(messageIds);
  if (normalizedIds.length === 0) throw new Error("message-required");

  const targetFolder = normalizeTargetFolderSystemName(targetFolderInput);
  const removesExistingRule = targetFolder === "inbox";
  if (!removesExistingRule && !isCustomMailboxFolderSystemName(targetFolder)) {
    throw new Error("continue-move-custom-folder-only");
  }

  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");

  const mailbox = await getLatestMailboxByUserId(user.id);
  if (!mailbox) throw new Error("mailbox-not-found");
  const expectedSourceFolder =
    options?.sourceFolder && options.sourceFolder !== "all"
      ? normalizeTargetFolderSystemName(options.sourceFolder)
      : null;

  await ensureOfficialMailSchema();
  return withRetryableMailboxTransaction(async (connection) => {
      await connection.query("SELECT id FROM mailboxes WHERE id = ? FOR UPDATE", [mailbox.id]);
      await ensureLocalMailboxFolders(connection, mailbox.id);
      const folderRows = await getMailboxFolderRows(connection, mailbox.id);
      const targetFolderRow = folderRows.find((folder) => folder.system_name === targetFolder);
      if (!targetFolderRow) throw new Error("folder-not-found");

      const [selectedRows] = await connection.query<MailboxMoveMessageRow[]>(
        `
          SELECT id, folder_id, direction, from_address, remote_folder, remote_uid
          FROM mailbox_messages
          WHERE mailbox_id = ?
            AND id IN (${normalizedIds.map(() => "?").join(", ")})
          FOR UPDATE
        `,
        [mailbox.id, ...normalizedIds]
      );
      if (selectedRows.length !== normalizedIds.length) {
        throw new Error("mailbox-state-changed");
      }

      if (expectedSourceFolder) {
        const expectedSourceFolderRow = folderRows.find((folder) => folder.system_name === expectedSourceFolder);
        if (
          !expectedSourceFolderRow ||
          selectedRows.some((message) => message.folder_id !== expectedSourceFolderRow.id)
        ) {
          throw new Error("mailbox-state-changed");
        }
      }

      const senderAddresses = [
        ...new Set(
          selectedRows
            .filter((message) => message.direction === "inbound")
            .map((message) => normalizeSenderAddress(message.from_address))
            .filter(Boolean)
        )
      ];
      if (senderAddresses.length !== 1 || selectedRows.some((message) => message.direction !== "inbound")) {
        throw new Error("continue-move-requires-one-sender");
      }

      const senderAddress = senderAddresses[0];
      const existingRules = await getMailboxSenderMoveRules(connection, mailbox.id);
      const existingRule = existingRules.find((rule) => rule.senderAddress === senderAddress);
      const expectedExistingTargetFolder = options?.expectedExistingTargetFolder
        ? normalizeTargetFolderSystemName(options.expectedExistingTargetFolder)
        : null;

      if (
        options?.replaceExistingRule &&
        expectedExistingTargetFolder &&
        existingRule?.targetFolderSystemName !== expectedExistingTargetFolder
      ) {
        throw new Error("mailbox-state-changed");
      }

      if (existingRule && existingRule.targetFolderSystemName !== targetFolder && !options?.replaceExistingRule) {
        throw new Error(`continue-move-rule-exists:${existingRule.targetFolderSystemName}`);
      }

      if (removesExistingRule) {
        if (!existingRule) {
          throw new Error("continue-move-rule-not-found");
        }

        await connection.query(
          `
            DELETE FROM mailbox_sender_move_rules
            WHERE mailbox_id = ?
              AND sender_address = ?
          `,
          [mailbox.id, senderAddress]
        );

        let rowsToMove = selectedRows;
        if (options?.moveExistingMessages) {
          const existingTargetRows = await findInboundSenderRows(connection, mailbox.id, [senderAddress], {
            sourceFolder: existingRule.targetFolderSystemName
          });
          rowsToMove = [
            ...new Map([...selectedRows, ...existingTargetRows].map((message) => [message.id, message])).values()
          ];
        }

        const moveResult = await queueMailboxRowsToFolder(connection, mailbox.id, folderRows, rowsToMove, targetFolder);

        return {
          ...moveResult,
          removedExistingRule: true,
          senderAddress,
          targetFolder
        };
      }

      if (!existingRule && existingRules.length >= 1_000) {
        throw new Error("continue-move-rule-limit");
      }

      await connection.query(
        `
          INSERT INTO mailbox_sender_move_rules (
            mailbox_id,
            sender_address,
            target_folder_id
          ) VALUES (?, ?, ?)
          ON DUPLICATE KEY UPDATE
            target_folder_id = VALUES(target_folder_id),
            updated_at = NOW()
        `,
        [mailbox.id, senderAddress, targetFolderRow.id]
      );

      let rowsToMove = selectedRows;
      if (options?.moveExistingMessages) {
        const existingInboxRows = await findInboundSenderRows(connection, mailbox.id, [senderAddress], {
          sourceFolder: "inbox"
        });
        const existingClassifiedRows = existingRule && options.replaceExistingRule
          ? await findInboundSenderRows(connection, mailbox.id, [senderAddress], { sourceFolder: existingRule.targetFolderSystemName })
          : [];
        rowsToMove = [
          ...new Map([...selectedRows, ...existingInboxRows, ...existingClassifiedRows].map((message) => [message.id, message])).values()
        ];
      }

      const moveResult = await queueMailboxRowsToFolder(connection, mailbox.id, folderRows, rowsToMove, targetFolder);

      return {
        ...moveResult,
        senderAddress,
        targetFolder
      };
    });
}

export async function getMailboxMoveRuleSettingsByEmail(email: string): Promise<MailboxMoveRuleSettings> {
  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");

  const mailbox = await getLatestMailboxByUserId(user.id);
  if (!mailbox) throw new Error("mailbox-not-found");

  await ensureOfficialMailSchema();
  return withTransaction(async (connection) => {
    await ensureLocalMailboxFolders(connection, mailbox.id);
    const [rules, folderRows] = await Promise.all([
      getMailboxSenderMoveRules(connection, mailbox.id),
      getMailboxFolderRows(connection, mailbox.id)
    ]);

    return {
      folders: folderRows
        .filter((folder) => isCustomMailboxFolderSystemName(folder.system_name))
        .map((folder) => ({
          id: Number(folder.id),
          name: folder.name,
          systemName: folder.system_name
        })),
      limit: 1_000,
      rules
    };
  });
}

export async function saveMailboxSenderMoveRuleByEmail(
  email: string,
  input: {
    moveExistingMessages?: boolean;
    ruleId?: number | null;
    senderAddress: string;
    targetFolder: string;
  }
) {
  const senderAddress = normalizeSenderAddress(input.senderAddress);
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(senderAddress)) {
    throw new Error("move-rule-invalid-sender");
  }

  const targetFolder = normalizeTargetFolderSystemName(input.targetFolder);
  if (!isCustomMailboxFolderSystemName(targetFolder)) {
    throw new Error("continue-move-custom-folder-only");
  }

  const ruleId = Number(input.ruleId ?? 0);
  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");

  const mailbox = await getLatestMailboxByUserId(user.id);
  if (!mailbox) throw new Error("mailbox-not-found");

  await ensureOfficialMailSchema();
  return withRetryableMailboxTransaction(async (connection) => {
      await connection.query("SELECT id FROM mailboxes WHERE id = ? FOR UPDATE", [mailbox.id]);
      await ensureLocalMailboxFolders(connection, mailbox.id);
      const folderRows = await getMailboxFolderRows(connection, mailbox.id);
      const targetFolderRow = folderRows.find((folder) => folder.system_name === targetFolder);
      if (!targetFolderRow) throw new Error("folder-not-found");

      const existingRules = await getMailboxSenderMoveRules(connection, mailbox.id);
      const duplicateSenderRule = existingRules.find(
        (rule) => rule.senderAddress === senderAddress && rule.id !== ruleId
      );
      if (duplicateSenderRule) throw new Error("move-rule-sender-exists");

      if (ruleId > 0) {
        const [result] = await connection.query<ResultSetHeader>(
          `
          UPDATE mailbox_sender_move_rules
          SET sender_address = ?, target_folder_id = ?, updated_at = NOW()
          WHERE id = ? AND mailbox_id = ?
        `,
          [senderAddress, targetFolderRow.id, ruleId, mailbox.id]
        );
        if (result.affectedRows === 0) throw new Error("move-rule-not-found");
      } else {
        if (existingRules.length >= 1_000) throw new Error("continue-move-rule-limit");
        await connection.query(
          `
          INSERT INTO mailbox_sender_move_rules (
            mailbox_id,
            sender_address,
            target_folder_id
          ) VALUES (?, ?, ?)
        `,
          [mailbox.id, senderAddress, targetFolderRow.id]
        );
      }

      let moveResult: Awaited<ReturnType<typeof queueMailboxRowsToFolder>> | null = null;
      if (input.moveExistingMessages) {
        const existingInboxRows = await findInboundSenderRows(connection, mailbox.id, [senderAddress], {
          sourceFolder: "inbox"
        });
        const previousRule = existingRules.find((rule) => rule.id === ruleId);
        const existingClassifiedRows = previousRule && previousRule.targetFolderSystemName !== targetFolder
          ? await findInboundSenderRows(connection, mailbox.id, [senderAddress], { sourceFolder: previousRule.targetFolderSystemName })
          : [];
        const rowsToMove = [...new Map([...existingInboxRows, ...existingClassifiedRows].map((message) => [message.id, message])).values()];
        if (rowsToMove.length > 0) {
          moveResult = await queueMailboxRowsToFolder(connection, mailbox.id, folderRows, rowsToMove, targetFolder);
        }
      }

      const nextRules = await getMailboxSenderMoveRules(connection, mailbox.id);
      const savedRule = nextRules.find((rule) => rule.senderAddress === senderAddress);
      if (!savedRule) throw new Error("move-rule-not-found");
      return { ...savedRule, moveResult };
    });
}

export async function deleteMailboxSenderMoveRulesByEmail(email: string, ruleIdsInput: Array<number | string>) {
  const ruleIds = normalizeMessageIds(ruleIdsInput);
  if (ruleIds.length === 0) return 0;

  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");

  const mailbox = await getLatestMailboxByUserId(user.id);
  if (!mailbox) throw new Error("mailbox-not-found");

  await ensureOfficialMailSchema();
  return withExclusiveMailboxOperation(mailbox.id, async () => {
    const [result] = await getDbPool().query<ResultSetHeader>(
      `
        DELETE FROM mailbox_sender_move_rules
        WHERE mailbox_id = ?
          AND id IN (${ruleIds.map(() => "?").join(", ")})
      `,
      [mailbox.id, ...ruleIds]
    );
    return result.affectedRows;
  });
}

export async function getMailboxContactSettingsByEmail(email: string): Promise<MailboxContactSettings> {
  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");

  const mailbox = await getLatestMailboxByUserId(user.id);
  if (!mailbox) throw new Error("mailbox-not-found");

  await ensureOfficialMailSchema();
  return withTransaction(async (connection) => {
    await ensureLocalMailboxFolders(connection, mailbox.id);
    const [savedContacts, folderRows, historyRows, groupRows, ignoredRows] = await Promise.all([
      getMailboxContacts(connection, mailbox.id),
      getMailboxFolderRows(connection, mailbox.id),
      connection.query<MailRecipientSuggestionRow[]>(
        `
          SELECT
            mm.from_name,
            mm.from_address,
            mm.to_addresses,
            mm.received_at
          FROM mailbox_messages mm
          WHERE mm.mailbox_id = ?
            AND mm.direction IN ('inbound', 'outbound')
          ORDER BY mm.received_at DESC, mm.id DESC
          LIMIT 5000
        `,
        [mailbox.id]
      ).then(([rows]) => rows),
      connection.query<(RowDataPacket & { name: string })[]>(
        `SELECT name FROM mailbox_contact_groups WHERE mailbox_id = ? ORDER BY name ASC`,
        [mailbox.id]
      ).then(([rows]) => rows),
      connection.query<(RowDataPacket & { email: string })[]>(
        `SELECT email FROM mailbox_contact_ignored_addresses WHERE mailbox_id = ?`,
        [mailbox.id]
      ).then(([rows]) => rows)
    ]);
    const contactsByEmail = new Map(savedContacts.map((contact) => [contact.email, contact]));
    const mailboxAddress = mailbox.email.trim().toLowerCase();
    const ignoredAddresses = new Set(ignoredRows.map((row) => normalizeSenderAddress(row.email)));

    const addHistoryContact = (contactEmail: string, lastInteractedAt: Date) => {
      const normalizedEmail = normalizeSenderAddress(contactEmail);
      if (
        !normalizedEmail ||
        normalizedEmail === mailboxAddress ||
        ignoredAddresses.has(normalizedEmail) ||
        contactsByEmail.has(normalizedEmail) ||
        !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail)
      ) {
        return;
      }

      contactsByEmail.set(normalizedEmail, {
        autoMoveFolderName: null,
        autoMoveFolderSystemName: null,
        companyName: "",
        createdAt: lastInteractedAt.toISOString(),
        displayName: "",
        email: normalizedEmail,
        groupName: "",
        id: null,
        isSaved: false,
        isVip: false,
        memo: "",
        phone: "",
        updatedAt: lastInteractedAt.toISOString()
      });
    };

    for (const row of historyRows) {
      if (row.from_address) addHistoryContact(row.from_address, row.received_at);
      for (const recipient of splitRecipientList(row.to_addresses)) {
        addHistoryContact(recipient, row.received_at);
      }
    }

    const contacts = [...contactsByEmail.values()].sort(
      (left, right) =>
        Number(right.isVip) - Number(left.isVip) ||
        (left.displayName || left.email).localeCompare(right.displayName || right.email, "ko")
    );

    return {
      contacts,
      folders: folderRows
        .filter((folder) => isCustomMailboxFolderSystemName(folder.system_name))
        .map((folder) => ({
          id: Number(folder.id),
          name: folder.name,
          systemName: folder.system_name
        })),
      groups: [...new Set([
        ...groupRows.map((row) => row.name.trim()),
        ...savedContacts.map((contact) => contact.groupName.trim())
      ].filter(Boolean))].sort((a, b) => a.localeCompare(b, "ko")),
      limit: 5_000
    };
  });
}

export async function createMailboxContactGroupByEmail(email: string, nameInput: string) {
  const name = nameInput.trim().slice(0, 120);
  if (!name) throw new Error("contact-group-name-required");
  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");
  const mailbox = await getLatestMailboxByUserId(user.id);
  if (!mailbox) throw new Error("mailbox-not-found");

  await ensureOfficialMailSchema();
  await withExclusiveMailboxOperation(mailbox.id, () =>
    withTransaction(async (connection) => {
      const [countRows] = await connection.query<CountTotalRow[]>(
        `SELECT COUNT(*) AS total FROM mailbox_contact_groups WHERE mailbox_id = ?`,
        [mailbox.id]
      );
      if (Number(countRows[0]?.total ?? 0) >= 200) throw new Error("contact-group-limit");
      await connection.query(
        `INSERT IGNORE INTO mailbox_contact_groups (mailbox_id, name) VALUES (?, ?)`,
        [mailbox.id, name]
      );
    })
  );
  return name;
}

export async function bulkUpdateMailboxContactsByEmail(
  email: string,
  input: {
    action: "delete" | "group" | "import" | "vip";
    contacts: MailboxContactInput[];
    duplicateMode?: "skip" | "update";
    groupName?: string;
    isVip?: boolean;
  }
) {
  const contacts = [...new Map(
    input.contacts
      .map((contact) => ({
        companyName: (contact.companyName ?? "").trim().slice(0, 191),
        displayName: (contact.displayName ?? "").trim().slice(0, 191),
        email: normalizeSenderAddress(contact.email),
        groupName: (contact.groupName ?? "").trim().slice(0, 120),
        isVip: Boolean(contact.isVip),
        memo: (contact.memo ?? "").trim().slice(0, 2_000),
        phone: (contact.phone ?? "").trim().slice(0, 40)
      }))
      .filter((contact) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(contact.email))
      .map((contact) => [contact.email, contact])
  ).values()].slice(0, 5_000);
  if (contacts.length === 0) throw new Error("contact-selection-required");

  const groupName = (input.groupName ?? "").trim().slice(0, 120);
  if (input.action === "group" && !groupName) throw new Error("contact-group-name-required");

  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");
  const mailbox = await getLatestMailboxByUserId(user.id);
  if (!mailbox) throw new Error("mailbox-not-found");

  await ensureOfficialMailSchema();
  return withExclusiveMailboxOperation(mailbox.id, () =>
    withTransaction(async (connection) => {
      const addresses = contacts.map((contact) => contact.email);
      const placeholders = addresses.map(() => "?").join(", ");

      if (input.action === "delete") {
        const [ruleRows] = await connection.query<(RowDataPacket & { auto_move_rule_id: number | null })[]>(
          `SELECT auto_move_rule_id FROM mailbox_contacts WHERE mailbox_id = ? AND email IN (${placeholders})`,
          [mailbox.id, ...addresses]
        );
        await connection.query(
          `DELETE FROM mailbox_contacts WHERE mailbox_id = ? AND email IN (${placeholders})`,
          [mailbox.id, ...addresses]
        );
        for (const address of addresses) {
          await connection.query(
            `INSERT IGNORE INTO mailbox_contact_ignored_addresses (mailbox_id, email) VALUES (?, ?)`,
            [mailbox.id, address]
          );
        }
        const ruleIds = ruleRows.map((row) => Number(row.auto_move_rule_id ?? 0)).filter(Boolean);
        if (ruleIds.length > 0) {
          await connection.query(
            `DELETE FROM mailbox_sender_move_rules WHERE mailbox_id = ? AND id IN (${ruleIds.map(() => "?").join(", ")})`,
            [mailbox.id, ...ruleIds]
          );
        }
        return { affectedCount: contacts.length };
      }

      const [countRows] = await connection.query<CountTotalRow[]>(
        `SELECT COUNT(*) AS total FROM mailbox_contacts WHERE mailbox_id = ?`,
        [mailbox.id]
      );
      const [existingRows] = await connection.query<(RowDataPacket & { email: string })[]>(
        `SELECT email FROM mailbox_contacts WHERE mailbox_id = ? AND email IN (${placeholders})`,
        [mailbox.id, ...addresses]
      );
      const existingAddresses = new Set(existingRows.map((row) => normalizeSenderAddress(row.email)));
      const newCount = contacts.filter((contact) => !existingAddresses.has(contact.email)).length;
      if (Number(countRows[0]?.total ?? 0) + newCount > 5_000) throw new Error("contact-limit");

      if (groupName) {
        await connection.query(
          `INSERT IGNORE INTO mailbox_contact_groups (mailbox_id, name) VALUES (?, ?)`,
          [mailbox.id, groupName]
        );
      }
      await connection.query(
        `DELETE FROM mailbox_contact_ignored_addresses WHERE mailbox_id = ? AND email IN (${placeholders})`,
        [mailbox.id, ...addresses]
      );

      let affectedCount = 0;
      for (const contact of contacts) {
        if (input.action === "import" && input.duplicateMode === "skip" && existingAddresses.has(contact.email)) {
          continue;
        }
        const nextGroupName = input.action === "group" ? groupName : contact.groupName;
        const nextVip = input.action === "vip" ? Boolean(input.isVip) : contact.isVip;
        const updateClause = input.action === "group"
          ? `group_name = VALUES(group_name), updated_at = NOW()`
          : input.action === "vip"
            ? `is_vip = VALUES(is_vip), updated_at = NOW()`
            : `
                display_name = IF(VALUES(display_name) <> '', VALUES(display_name), display_name),
                company_name = IF(VALUES(company_name) <> '', VALUES(company_name), company_name),
                phone = IF(VALUES(phone) <> '', VALUES(phone), phone),
                group_name = IF(VALUES(group_name) <> '', VALUES(group_name), group_name),
                memo = IF(VALUES(memo) <> '', VALUES(memo), memo),
                is_vip = GREATEST(is_vip, VALUES(is_vip)),
                updated_at = NOW()
              `;
        await connection.query(
          `
            INSERT INTO mailbox_contacts (
              mailbox_id, email, display_name, company_name, phone, group_name, memo, is_vip
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
            ON DUPLICATE KEY UPDATE ${updateClause}
          `,
          [
            mailbox.id,
            contact.email,
            contact.displayName,
            contact.companyName,
            contact.phone,
            nextGroupName,
            contact.memo || null,
            nextVip ? 1 : 0
          ]
        );
        affectedCount += 1;
      }
      return { affectedCount };
    })
  );
}

export async function getMailboxContactImportCandidatesByEmail(
  email: string,
  input?: { minCount?: number; months?: number }
): Promise<MailboxContactImportCandidate[]> {
  const months = Math.max(1, Math.min(120, Math.floor(input?.months ?? 6)));
  const minCount = Math.max(1, Math.min(100, Math.floor(input?.minCount ?? 2)));
  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");
  const mailbox = await getLatestMailboxByUserId(user.id);
  if (!mailbox) throw new Error("mailbox-not-found");

  await ensureOfficialMailSchema();
  return withTransaction(async (connection) => {
    const [rows] = await connection.query<MailRecipientSuggestionRow[]>(
      `
        SELECT mm.from_name, mm.from_address, mm.to_addresses, mm.received_at
        FROM mailbox_messages mm
        WHERE mm.mailbox_id = ?
          AND mm.direction IN ('inbound', 'outbound')
          AND mm.received_at >= DATE_SUB(NOW(), INTERVAL ${months} MONTH)
        ORDER BY mm.received_at DESC, mm.id DESC
        LIMIT 10000
      `,
      [mailbox.id]
    );
    const mailboxAddress = mailbox.email.trim().toLowerCase();
    const candidates = new Map<string, MailboxContactImportCandidate>();
    const addCandidate = (addressInput: string, displayName: string, receivedAt: Date) => {
      const address = normalizeSenderAddress(addressInput);
      if (!address || address === mailboxAddress || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(address)) return;
      const current = candidates.get(address);
      if (current) {
        current.interactionCount += 1;
        if (!current.displayName && displayName) current.displayName = displayName.trim().slice(0, 191);
        return;
      }
      candidates.set(address, {
        displayName: displayName.trim().slice(0, 191),
        email: address,
        interactionCount: 1,
        lastInteractedAt: receivedAt.toISOString()
      });
    };
    for (const row of rows) {
      const seen = new Set<string>();
      const fromAddress = normalizeSenderAddress(row.from_address ?? "");
      if (fromAddress && !seen.has(fromAddress)) {
        addCandidate(fromAddress, row.from_name ?? "", row.received_at);
        seen.add(fromAddress);
      }
      for (const recipient of splitRecipientList(row.to_addresses)) {
        const normalized = normalizeSenderAddress(recipient);
        if (!normalized || seen.has(normalized)) continue;
        addCandidate(normalized, "", row.received_at);
        seen.add(normalized);
      }
    }
    return [...candidates.values()]
      .filter((candidate) => candidate.interactionCount >= minCount)
      .sort((left, right) => right.interactionCount - left.interactionCount || left.email.localeCompare(right.email))
      .slice(0, 2_000);
  });
}

export async function getMailboxContactDuplicateGroupsByEmail(email: string): Promise<MailboxContactDuplicateGroup[]> {
  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");
  const mailbox = await getLatestMailboxByUserId(user.id);
  if (!mailbox) throw new Error("mailbox-not-found");
  await ensureOfficialMailSchema();
  return withTransaction(async (connection) => {
    const contacts = await getMailboxContacts(connection, mailbox.id);
    const groups = new Map<string, MailboxContactDuplicateGroup>();
    for (const contact of contacts) {
      const phone = contact.phone.replace(/\D/g, "");
      const normalizedName = contact.displayName.trim().toLocaleLowerCase("ko").replace(/\s+/g, "");
      const key = phone.length >= 7 ? `phone:${phone}` : normalizedName ? `name:${normalizedName}` : "";
      if (!key) continue;
      const current = groups.get(key) ?? {
        contacts: [],
        key,
        reason: key.startsWith("phone:") ? "phone" as const : "name" as const
      };
      current.contacts.push(contact);
      groups.set(key, current);
    }
    return [...groups.values()].filter((group) => group.contacts.length > 1);
  });
}

export async function mergeMailboxContactsByEmail(email: string, contactIdsInput: Array<number | string>) {
  const contactIds = normalizeMessageIds(contactIdsInput).slice(0, 100);
  if (contactIds.length < 2) throw new Error("contact-merge-selection-required");
  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");
  const mailbox = await getLatestMailboxByUserId(user.id);
  if (!mailbox) throw new Error("mailbox-not-found");
  await ensureOfficialMailSchema();
  return withExclusiveMailboxOperation(mailbox.id, () =>
    withTransaction(async (connection) => {
      const [rows] = await connection.query<MailboxContactRow[]>(
        `
          SELECT contact.*, NULL AS auto_move_folder_name, NULL AS auto_move_folder_system_name
          FROM mailbox_contacts contact
          WHERE contact.mailbox_id = ? AND contact.id IN (${contactIds.map(() => "?").join(", ")})
          FOR UPDATE
        `,
        [mailbox.id, ...contactIds]
      );
      const ordered = contactIds.map((id) => rows.find((row) => Number(row.id) === id)).filter(Boolean) as MailboxContactRow[];
      if (ordered.length < 2) throw new Error("contact-merge-selection-required");
      const primary = ordered[0];
      const firstValue = (selector: (row: MailboxContactRow) => string | null | undefined) =>
        ordered.map(selector).find((value) => String(value ?? "").trim()) ?? "";
      const retainedRuleId = ordered.map((row) => Number(row.auto_move_rule_id ?? 0)).find(Boolean) ?? null;
      const removed = ordered.slice(1);
      await connection.query(
        `
          UPDATE mailbox_contacts
          SET display_name = ?, company_name = ?, phone = ?, group_name = ?, memo = ?,
              is_vip = ?, auto_move_rule_id = ?, updated_at = NOW()
          WHERE mailbox_id = ? AND id = ?
        `,
        [
          firstValue((row) => row.display_name),
          firstValue((row) => row.company_name),
          firstValue((row) => row.phone),
          firstValue((row) => row.group_name),
          firstValue((row) => row.memo),
          ordered.some((row) => Boolean(row.is_vip)) ? 1 : 0,
          retainedRuleId,
          mailbox.id,
          primary.id
        ]
      );
      await connection.query(
        `DELETE FROM mailbox_contacts WHERE mailbox_id = ? AND id IN (${removed.map(() => "?").join(", ")})`,
        [mailbox.id, ...removed.map((row) => row.id)]
      );
      const obsoleteRuleIds = removed
        .map((row) => Number(row.auto_move_rule_id ?? 0))
        .filter((id) => id && id !== retainedRuleId);
      if (obsoleteRuleIds.length > 0) {
        await connection.query(
          `DELETE FROM mailbox_sender_move_rules WHERE mailbox_id = ? AND id IN (${obsoleteRuleIds.map(() => "?").join(", ")})`,
          [mailbox.id, ...obsoleteRuleIds]
        );
      }
      return { mergedCount: ordered.length };
    })
  );
}

export async function saveMailboxContactByEmail(
  email: string,
  input: {
    autoMoveFolder?: string | null;
    companyName?: string;
    contactId?: number | null;
    displayName?: string;
    email: string;
    groupName?: string;
    isVip?: boolean;
    memo?: string;
    moveExistingMessages?: boolean;
    phone?: string;
  }
) {
  const contactEmail = normalizeSenderAddress(input.email);
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(contactEmail)) {
    throw new Error("contact-invalid-email");
  }

  const contactId = Number(input.contactId ?? 0);
  const displayName = (input.displayName ?? "").trim().slice(0, 191);
  const companyName = (input.companyName ?? "").trim().slice(0, 191);
  const phone = (input.phone ?? "").trim().slice(0, 40);
  const groupName = (input.groupName ?? "").trim().slice(0, 120);
  const memo = (input.memo ?? "").trim().slice(0, 2_000);
  const autoMoveFolder = input.autoMoveFolder?.trim()
    ? normalizeTargetFolderSystemName(input.autoMoveFolder)
    : null;
  if (autoMoveFolder && !isCustomMailboxFolderSystemName(autoMoveFolder)) {
    throw new Error("contact-auto-folder-invalid");
  }

  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");
  const mailbox = await getLatestMailboxByUserId(user.id);
  if (!mailbox) throw new Error("mailbox-not-found");

  await ensureOfficialMailSchema();
  return withRetryableMailboxTransaction(async (connection) => {
      await connection.query("SELECT id FROM mailboxes WHERE id = ? FOR UPDATE", [mailbox.id]);
      await ensureLocalMailboxFolders(connection, mailbox.id);
      const folderRows = await getMailboxFolderRows(connection, mailbox.id);
      await connection.query(
        `DELETE FROM mailbox_contact_ignored_addresses WHERE mailbox_id = ? AND email = ?`,
        [mailbox.id, contactEmail]
      );
      if (groupName) {
        await connection.query(
          `INSERT IGNORE INTO mailbox_contact_groups (mailbox_id, name) VALUES (?, ?)`,
          [mailbox.id, groupName]
        );
      }
      const [existingRows] = await connection.query<
        (RowDataPacket & { auto_move_rule_id: number | null; email: string; id: number })[]
      >(
        `
          SELECT id, email, auto_move_rule_id
          FROM mailbox_contacts
          WHERE mailbox_id = ?
            AND id = ?
          LIMIT 1
        `,
        [mailbox.id, contactId]
      );
      const existing = existingRows[0] ?? null;
      if (contactId > 0 && !existing) throw new Error("contact-not-found");

      const [duplicateRows] = await connection.query<(RowDataPacket & { id: number })[]>(
        `
          SELECT id
          FROM mailbox_contacts
          WHERE mailbox_id = ?
            AND email = ?
            AND (? = 0 OR id <> ?)
          LIMIT 1
        `,
        [mailbox.id, contactEmail, contactId, contactId]
      );
      if (duplicateRows[0]) throw new Error("contact-email-exists");

      let autoMoveRuleId = existing?.auto_move_rule_id ?? null;
      if (autoMoveFolder) {
        const targetFolder = folderRows.find((folder) => folder.system_name === autoMoveFolder);
        if (!targetFolder) throw new Error("folder-not-found");

        const [senderRuleRows] = await connection.query<(RowDataPacket & { id: number })[]>(
          `
            SELECT id
            FROM mailbox_sender_move_rules
            WHERE mailbox_id = ? AND sender_address = ?
            LIMIT 1
          `,
          [mailbox.id, contactEmail]
        );
        const senderRuleId = senderRuleRows[0]?.id ? Number(senderRuleRows[0].id) : null;
        if (autoMoveRuleId && senderRuleId && Number(autoMoveRuleId) !== senderRuleId) {
          await connection.query(
            `DELETE FROM mailbox_sender_move_rules WHERE id = ? AND mailbox_id = ?`,
            [autoMoveRuleId, mailbox.id]
          );
        }
        const reusableRuleId = senderRuleId ?? autoMoveRuleId ?? null;

        if (reusableRuleId) {
          await connection.query(
            `
              UPDATE mailbox_sender_move_rules
              SET sender_address = ?, target_folder_id = ?, updated_at = NOW()
              WHERE id = ? AND mailbox_id = ?
            `,
            [contactEmail, targetFolder.id, reusableRuleId, mailbox.id]
          );
          autoMoveRuleId = Number(reusableRuleId);
        } else {
          const [ruleCountRows] = await connection.query<CountTotalRow[]>(
            `SELECT COUNT(*) AS total FROM mailbox_sender_move_rules WHERE mailbox_id = ?`,
            [mailbox.id]
          );
          if (Number(ruleCountRows[0]?.total ?? 0) >= 1_000) throw new Error("continue-move-rule-limit");
          const [insertRuleResult] = await connection.query<ResultSetHeader>(
            `
              INSERT INTO mailbox_sender_move_rules (mailbox_id, sender_address, target_folder_id)
              VALUES (?, ?, ?)
            `,
            [mailbox.id, contactEmail, targetFolder.id]
          );
          autoMoveRuleId = Number(insertRuleResult.insertId);
        }

        if (input.moveExistingMessages) {
          const inboxRows = await findInboundSenderRows(connection, mailbox.id, [contactEmail], {
            sourceFolder: "inbox"
          });
          if (inboxRows.length > 0) {
            await queueMailboxRowsToFolder(connection, mailbox.id, folderRows, inboxRows, autoMoveFolder);
          }
        }
      } else if (autoMoveRuleId) {
        await connection.query(
          `DELETE FROM mailbox_sender_move_rules WHERE id = ? AND mailbox_id = ?`,
          [autoMoveRuleId, mailbox.id]
        );
        autoMoveRuleId = null;
      }

      let savedContactId = contactId;
      if (existing) {
        await connection.query(
          `
            UPDATE mailbox_contacts
            SET
              email = ?, display_name = ?, company_name = ?, phone = ?, group_name = ?, memo = ?,
              is_vip = ?, auto_move_rule_id = ?, updated_at = NOW()
            WHERE id = ? AND mailbox_id = ?
          `,
          [
            contactEmail,
            displayName,
            companyName,
            phone,
            groupName,
            memo || null,
            input.isVip ? 1 : 0,
            autoMoveRuleId,
            contactId,
            mailbox.id
          ]
        );
      } else {
        const [insertResult] = await connection.query<ResultSetHeader>(
          `
            INSERT INTO mailbox_contacts (
              mailbox_id, email, display_name, company_name, phone, group_name, memo, is_vip, auto_move_rule_id
            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
          `,
          [
            mailbox.id,
            contactEmail,
            displayName,
            companyName,
            phone,
            groupName,
            memo || null,
            input.isVip ? 1 : 0,
            autoMoveRuleId
          ]
        );
        savedContactId = Number(insertResult.insertId);
      }

      const contacts = await getMailboxContacts(connection, mailbox.id);
      const savedContact = contacts.find((contact) => contact.id === savedContactId);
      if (!savedContact) throw new Error("contact-not-found");
      return savedContact;
    });
}

export async function deleteMailboxContactsByEmail(email: string, contactIdsInput: Array<number | string>) {
  const contactIds = normalizeMessageIds(contactIdsInput);
  if (contactIds.length === 0) return 0;

  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");
  const mailbox = await getLatestMailboxByUserId(user.id);
  if (!mailbox) throw new Error("mailbox-not-found");

  await ensureOfficialMailSchema();
  return withExclusiveMailboxOperation(mailbox.id, () =>
    withTransaction(async (connection) => {
      const [rows] = await connection.query<(RowDataPacket & { auto_move_rule_id: number | null; email: string })[]>(
        `
          SELECT auto_move_rule_id, email
          FROM mailbox_contacts
          WHERE mailbox_id = ? AND id IN (${contactIds.map(() => "?").join(", ")})
        `,
        [mailbox.id, ...contactIds]
      );
      const moveRuleIds = rows.map((row) => Number(row.auto_move_rule_id ?? 0)).filter((id) => id > 0);
      for (const row of rows) {
        await connection.query(
          `INSERT IGNORE INTO mailbox_contact_ignored_addresses (mailbox_id, email) VALUES (?, ?)`,
          [mailbox.id, normalizeSenderAddress(row.email)]
        );
      }
      const [result] = await connection.query<ResultSetHeader>(
        `
          DELETE FROM mailbox_contacts
          WHERE mailbox_id = ? AND id IN (${contactIds.map(() => "?").join(", ")})
        `,
        [mailbox.id, ...contactIds]
      );
      if (moveRuleIds.length > 0) {
        await connection.query(
          `
            DELETE FROM mailbox_sender_move_rules
            WHERE mailbox_id = ? AND id IN (${moveRuleIds.map(() => "?").join(", ")})
          `,
          [mailbox.id, ...moveRuleIds]
        );
      }
      return result.affectedRows;
    })
  );
}

export async function getMailboxSenderProfileByEmail(
  email: string,
  senderAddressInput: string
): Promise<MailboxSenderProfile> {
  const senderAddress = normalizeSenderAddress(senderAddressInput);
  if (!senderAddress) throw new Error("sender-address-required");

  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");
  const mailbox = await getLatestMailboxByUserId(user.id);
  if (!mailbox) throw new Error("mailbox-not-found");

  await ensureOfficialMailSchema();
  return withTransaction(async (connection) => {
    await ensureLocalMailboxFolders(connection, mailbox.id);
    const [contacts, dispositions, moveRules, folderRows, moveCounts] = await Promise.all([
      getMailboxContacts(connection, mailbox.id),
      getMailboxSenderDispositions(connection, mailbox.id),
      getMailboxSenderMoveRules(connection, mailbox.id),
      getMailboxFolderRows(connection, mailbox.id),
      connection.query<(RowDataPacket & { folder_system_name: string; message_count: number; unread_count: number })[]>(
        `
          SELECT f.system_name AS folder_system_name, COUNT(*) AS message_count,
            SUM(CASE WHEN mm.is_read = 0 THEN 1 ELSE 0 END) AS unread_count
          FROM mailbox_messages mm
          INNER JOIN mailbox_folders f ON f.id = mm.folder_id
          WHERE mm.mailbox_id = ? AND mm.direction = 'inbound' AND LOWER(mm.from_address) = ?
            AND ${PENDING_MAILBOX_MOVE_EXCLUSION_SQL}
          GROUP BY f.system_name
        `,
        [mailbox.id, senderAddress]
      ).then(([rows]) => rows)
    ]);
    return {
      contact: contacts.find((contact) => contact.email === senderAddress) ?? null,
      disposition: dispositions.find((rule) => rule.senderAddress === senderAddress)?.action ?? null,
      folders: folderRows
        .filter((folder) => isCustomMailboxFolderSystemName(folder.system_name))
        .map((folder) => ({ id: Number(folder.id), name: folder.name, systemName: folder.system_name })),
      moveRule: moveRules.find((rule) => rule.senderAddress === senderAddress) ?? null,
      moveFolderCounts: moveCounts.map((row) => ({ folderSystemName: row.folder_system_name, messageCount: Number(row.message_count), unreadCount: Number(row.unread_count) })),
      senderAddress
    };
  });
}

export async function getMailboxSpamRuleSettingsByEmail(email: string): Promise<MailboxSpamRuleSettings> {
  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");
  const mailbox = await getLatestMailboxByUserId(user.id);
  if (!mailbox) throw new Error("mailbox-not-found");

  await ensureOfficialMailSchema();
  return withTransaction(async (connection) => {
    const [senderRules, customRules, general] = await Promise.all([
      getMailboxSenderDispositions(connection, mailbox.id),
      getMailboxCustomSpamRules(connection, mailbox.id),
      getMailboxSpamGeneralSettings(connection, mailbox.id)
    ]);
    return { customRules, general, limit: 1_000, senderRules };
  });
}

export async function updateMailboxSpamGeneralSettingsByEmail(
  email: string,
  input: Partial<MailboxSpamGeneralSettings>
): Promise<MailboxSpamRuleSettings> {
  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");
  const mailbox = await getLatestMailboxByUserId(user.id);
  if (!mailbox) throw new Error("mailbox-not-found");
  const defaults = DEFAULT_MAILBOX_SPAM_GENERAL_SETTINGS;
  const booleanValue = (value: unknown, fallback: boolean) => typeof value === "boolean" ? value : fallback;
  const retentionDays = [7, 15, 30, 60, 90].includes(Number(input.retentionDays))
    ? Number(input.retentionDays)
    : defaults.retentionDays;
  const languageFilterLanguages = normalizeMailboxSpamLanguages(input.languageFilterLanguages);
  const settings: MailboxSpamGeneralSettings = {
    autoMoveSpam: booleanValue(input.autoMoveSpam, defaults.autoMoveSpam),
    blockReportedSender: booleanValue(input.blockReportedSender, defaults.blockReportedSender),
    detectBounceSpoofing: booleanValue(input.detectBounceSpoofing, defaults.detectBounceSpoofing),
    detectMissingRecipient: booleanValue(input.detectMissingRecipient, defaults.detectMissingRecipient),
    detectSelfSpoofing: booleanValue(input.detectSelfSpoofing, defaults.detectSelfSpoofing),
    detectUnknownSender: booleanValue(input.detectUnknownSender, defaults.detectUnknownSender),
    expiredMessageAction: input.expiredMessageAction === "trash" ? "trash" : "delete",
    hideRemoteContent: booleanValue(input.hideRemoteContent, defaults.hideRemoteContent),
    hideSpamFolder: booleanValue(input.hideSpamFolder, defaults.hideSpamFolder),
    languageFilterLanguages: languageFilterLanguages.length > 0 ? languageFilterLanguages : defaults.languageFilterLanguages,
    languageFilterMode: input.languageFilterMode === "all" ? "all" : "selected",
    languageFilterEnabled: booleanValue(input.languageFilterEnabled, defaults.languageFilterEnabled),
    processPreviousReported: booleanValue(input.processPreviousReported, defaults.processPreviousReported),
    reportedMessageAction: input.reportedMessageAction === "delete" ? "delete" : "spam",
    retentionDays
  };

  await ensureOfficialMailSchema();
  await getDbPool().query(
    `
      INSERT INTO mailbox_spam_settings (
        mailbox_id,
        auto_move_spam,
        detect_self_spoofing,
        detect_bounce_spoofing,
        detect_missing_recipient,
        detect_unknown_sender,
        language_filter_enabled,
        language_filter_mode,
        language_filter_languages,
        reported_message_action,
        block_reported_sender,
        process_previous_reported,
        hide_spam_folder,
        retention_days,
        expired_message_action,
        hide_remote_content
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
      ON DUPLICATE KEY UPDATE
        auto_move_spam = VALUES(auto_move_spam),
        detect_self_spoofing = VALUES(detect_self_spoofing),
        detect_bounce_spoofing = VALUES(detect_bounce_spoofing),
        detect_missing_recipient = VALUES(detect_missing_recipient),
        detect_unknown_sender = VALUES(detect_unknown_sender),
        language_filter_enabled = VALUES(language_filter_enabled),
        language_filter_mode = VALUES(language_filter_mode),
        language_filter_languages = VALUES(language_filter_languages),
        reported_message_action = VALUES(reported_message_action),
        block_reported_sender = VALUES(block_reported_sender),
        process_previous_reported = VALUES(process_previous_reported),
        hide_spam_folder = VALUES(hide_spam_folder),
        retention_days = VALUES(retention_days),
        expired_message_action = VALUES(expired_message_action),
        hide_remote_content = VALUES(hide_remote_content),
        updated_at = NOW()
    `,
    [
      mailbox.id,
      settings.autoMoveSpam ? 1 : 0,
      settings.detectSelfSpoofing ? 1 : 0,
      settings.detectBounceSpoofing ? 1 : 0,
      settings.detectMissingRecipient ? 1 : 0,
      settings.detectUnknownSender ? 1 : 0,
      settings.languageFilterEnabled ? 1 : 0,
      settings.languageFilterMode,
      settings.languageFilterLanguages.join(","),
      settings.reportedMessageAction,
      settings.blockReportedSender ? 1 : 0,
      settings.processPreviousReported ? 1 : 0,
      settings.hideSpamFolder ? 1 : 0,
      settings.retentionDays,
      settings.expiredMessageAction,
      settings.hideRemoteContent ? 1 : 0
    ]
  );
  return getMailboxSpamRuleSettingsByEmail(email);
}

export async function setMailboxSenderDispositionByEmail(
  email: string,
  senderAddressInput: string,
  action: MailboxSpamRuleAction | null,
  options?: { moveExistingMessages?: boolean; targetFolder?: string }
) {
  const senderAddress = normalizeSenderAddress(senderAddressInput);
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(senderAddress)) throw new Error("sender-address-required");
  if (action !== null && action !== "allow" && action !== "spam") throw new Error("spam-rule-invalid-action");

  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");
  const mailbox = await getLatestMailboxByUserId(user.id);
  if (!mailbox) throw new Error("mailbox-not-found");
  const targetFolder = options?.targetFolder ? normalizeTargetFolderSystemName(options.targetFolder) : "inbox";

  await ensureOfficialMailSchema();
  return withExclusiveMailboxOperation(mailbox.id, () =>
    withTransaction(async (connection) => {
      await ensureLocalMailboxFolders(connection, mailbox.id);
      const folderRows = await getMailboxFolderRows(connection, mailbox.id);
      if (action) {
        await connection.query(
          `
            INSERT INTO mailbox_sender_rules (mailbox_id, sender_address, rule_action)
            VALUES (?, ?, ?)
            ON DUPLICATE KEY UPDATE rule_action = VALUES(rule_action), updated_at = NOW()
          `,
          [mailbox.id, senderAddress, action]
        );
      } else {
        await connection.query(
          `DELETE FROM mailbox_sender_rules WHERE mailbox_id = ? AND sender_address = ?`,
          [mailbox.id, senderAddress]
        );
      }

      let movedCount = 0;
      if (action === "spam" && options?.moveExistingMessages !== false) {
        const rows = await findInboundSenderRows(connection, mailbox.id, [senderAddress]);
        movedCount = (await moveMailboxRowsToFolder(connection, mailbox, folderRows, rows, "spam")).movedCount;
      } else if (action === "allow" && options?.moveExistingMessages) {
        const rows = await findInboundSenderRows(connection, mailbox.id, [senderAddress], { sourceFolder: "spam" });
        movedCount = (await moveMailboxRowsToFolder(connection, mailbox, folderRows, rows, targetFolder)).movedCount;
      }
      return { action, movedCount, senderAddress };
    })
  );
}

export async function saveMailboxCustomSpamRuleByEmail(
  email: string,
  input: {
    action: MailboxSpamRuleAction;
    enabled?: boolean;
    field: MailboxCustomSpamRuleField;
    priority?: number;
    ruleId?: number | null;
    value: string;
  }
) {
  const allowedFields: MailboxCustomSpamRuleField[] = ["sender", "sender_domain", "subject", "body"];
  if (!allowedFields.includes(input.field)) throw new Error("spam-rule-invalid-field");
  if (input.action !== "allow" && input.action !== "spam") throw new Error("spam-rule-invalid-action");
  const value = input.value.trim().toLowerCase().slice(0, 500);
  if (!value) throw new Error("spam-rule-value-required");
  const priority = Math.max(0, Math.min(1_000_000, Math.floor(Number(input.priority ?? 0) || 0)));
  const ruleId = Number(input.ruleId ?? 0);

  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");
  const mailbox = await getLatestMailboxByUserId(user.id);
  if (!mailbox) throw new Error("mailbox-not-found");

  await ensureOfficialMailSchema();
  return withExclusiveMailboxOperation(mailbox.id, async () => {
    if (ruleId > 0) {
      const [result] = await getDbPool().query<ResultSetHeader>(
        `
          UPDATE mailbox_custom_spam_rules
          SET match_field = ?, match_value = ?, rule_action = ?, enabled = ?, priority = ?, updated_at = NOW()
          WHERE id = ? AND mailbox_id = ?
        `,
        [input.field, value, input.action, input.enabled === false ? 0 : 1, priority, ruleId, mailbox.id]
      );
      if (result.affectedRows === 0) throw new Error("spam-rule-not-found");
    } else {
      const [countRows] = await getDbPool().query<CountTotalRow[]>(
        `SELECT COUNT(*) AS total FROM mailbox_custom_spam_rules WHERE mailbox_id = ?`,
        [mailbox.id]
      );
      if (Number(countRows[0]?.total ?? 0) >= 1_000) throw new Error("spam-rule-limit");
      await getDbPool().query(
        `
          INSERT INTO mailbox_custom_spam_rules (
            mailbox_id, match_field, match_value, rule_action, enabled, priority
          ) VALUES (?, ?, ?, ?, ?, ?)
        `,
        [mailbox.id, input.field, value, input.action, input.enabled === false ? 0 : 1, priority]
      );
    }

    return getMailboxSpamRuleSettingsByEmail(email);
  });
}

export async function updateMailboxCustomSpamRuleEnabledByEmail(
  email: string,
  ruleIdInput: number | string,
  enabled: boolean
) {
  const ruleId = Number(ruleIdInput);
  if (!Number.isInteger(ruleId) || ruleId < 1) throw new Error("spam-rule-not-found");
  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");
  const mailbox = await getLatestMailboxByUserId(user.id);
  if (!mailbox) throw new Error("mailbox-not-found");
  await ensureOfficialMailSchema();
  const [result] = await getDbPool().query<ResultSetHeader>(
    `UPDATE mailbox_custom_spam_rules SET enabled = ?, updated_at = NOW() WHERE id = ? AND mailbox_id = ?`,
    [enabled ? 1 : 0, ruleId, mailbox.id]
  );
  if (result.affectedRows === 0) throw new Error("spam-rule-not-found");
  return enabled;
}

export async function deleteMailboxCustomSpamRulesByEmail(
  email: string,
  ruleIdsInput: Array<number | string>
) {
  const ruleIds = normalizeMessageIds(ruleIdsInput);
  if (ruleIds.length === 0) return 0;
  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");
  const mailbox = await getLatestMailboxByUserId(user.id);
  if (!mailbox) throw new Error("mailbox-not-found");
  await ensureOfficialMailSchema();
  const [result] = await getDbPool().query<ResultSetHeader>(
    `
      DELETE FROM mailbox_custom_spam_rules
      WHERE mailbox_id = ? AND id IN (${ruleIds.map(() => "?").join(", ")})
    `,
    [mailbox.id, ...ruleIds]
  );
  return result.affectedRows;
}

export async function moveAllMailboxMessagesFromSenderToTrashByEmail(email: string, senderAddressInput: string) {
  const senderAddress = normalizeSenderAddress(senderAddressInput);
  if (!senderAddress) throw new Error("sender-address-required");
  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");
  const mailbox = await getLatestMailboxByUserId(user.id);
  if (!mailbox) throw new Error("mailbox-not-found");

  await ensureOfficialMailSchema();
  return withRetryableMailboxTransaction(async (connection) => {
      await ensureLocalMailboxFolders(connection, mailbox.id);
      const folderRows = await getMailboxFolderRows(connection, mailbox.id);
      const trashFolder = folderRows.find((folder) => folder.system_name === "trash");
      if (!trashFolder) throw new Error("folder-not-found");
      const rows = (await findInboundSenderRows(connection, mailbox.id, [senderAddress])).filter(
        (row) => row.folder_id !== trashFolder.id
      );
      return queueMailboxRowsToFolder(connection, mailbox.id, folderRows, rows, "trash");
    });
}

type MailboxFolderBackupPlanPart = MailboxFolderBackupPart & {
  remoteUids: number[];
};

type MailboxFolderBackupPlan = {
  credentials: ReturnType<typeof toMailboxCredentials>;
  manifest: MailboxFolderBackupManifest;
  parts: MailboxFolderBackupPlanPart[];
  target: {
    name: string;
    remoteName: string | null;
    systemName: string;
  };
};

function sanitizeMailboxBackupFileSegment(value: string) {
  return value
    .normalize("NFKC")
    .replace(/[\\/:*?"<>|\u0000-\u001f]/g, "_")
    .replace(/\s+/g, " ")
    .trim()
    .slice(0, 80) || "메일함";
}

async function prepareMailboxFolderBackupByEmail(
  email: string,
  folderSystemNameInput: string
): Promise<MailboxFolderBackupPlan> {
  const folderSystemName = normalizeTargetFolderSystemName(folderSystemNameInput);
  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");
  const mailbox = await getLatestMailboxByUserId(user.id);
  if (!mailbox) throw new Error("mailbox-not-found");
  if (!mailbox.password_ciphertext) throw new Error("mailbox-auth-missing");

  await ensureOfficialMailSchema();
  await withTransaction((connection) => ensureLocalMailboxFolders(connection, mailbox.id));
  const [folderRows] = await getDbPool().query<
    (RowDataPacket & {
      name: string;
      remote_name: string | null;
      system_name: string;
    })[]
  >(
    `
      SELECT system_name, name, remote_name
      FROM mailbox_folders
      WHERE mailbox_id = ? AND system_name = ?
      LIMIT 1
    `,
    [mailbox.id, folderSystemName]
  );
  const folder = folderRows[0];
  if (!folder) throw new Error("folder-not-found");

  const target = {
    name: folder.name,
    remoteName: folder.remote_name,
    systemName: folder.system_name
  };
  const credentials = toMailboxCredentials(mailbox);
  const remoteItems = await getRemoteMailboxFolderBackupItems(credentials, target);
  const rawParts: Array<{ remoteUids: number[]; sizeBytes: number }> = [];

  for (const item of remoteItems) {
    const itemSize = Math.max(1, item.sizeBytes);
    let currentPart = rawParts.at(-1);

    if (!currentPart || (currentPart.remoteUids.length > 0 && currentPart.sizeBytes + itemSize > MAILBOX_BACKUP_PART_SIZE_BYTES)) {
      currentPart = { remoteUids: [], sizeBytes: 0 };
      rawParts.push(currentPart);
    }

    currentPart.remoteUids.push(item.remoteUid);
    currentPart.sizeBytes += itemSize;
  }

  const folderLabel = folderDisplayNameForAction(folder.system_name, folder.name);
  const localPart = sanitizeMailboxBackupFileSegment(mailbox.email.split("@")[0] || "officialmail");
  const safeFolderLabel = sanitizeMailboxBackupFileSegment(folderLabel);
  const parts = rawParts.map((part, partIndex) => {
    const index = partIndex + 1;
    const suffix = rawParts.length > 1 ? `_${String(index).padStart(2, "0")}` : "";

    return {
      fileName: `${localPart}_${safeFolderLabel}${suffix}.zip`,
      index,
      messageCount: part.remoteUids.length,
      remoteUids: part.remoteUids,
      sizeBytes: part.sizeBytes
    } satisfies MailboxFolderBackupPlanPart;
  });
  const manifest = {
    folderLabel,
    folderSystemName: folder.system_name,
    messageCount: remoteItems.length,
    partSizeBytes: MAILBOX_BACKUP_PART_SIZE_BYTES,
    parts: parts.map((part) => ({
      fileName: part.fileName,
      index: part.index,
      messageCount: part.messageCount,
      sizeBytes: part.sizeBytes
    })),
    totalBytes: remoteItems.reduce((total, item) => total + Math.max(0, item.sizeBytes), 0)
  } satisfies MailboxFolderBackupManifest;

  return { credentials, manifest, parts, target };
}

export async function getMailboxFolderBackupManifestByEmail(
  email: string,
  folderSystemName: string
): Promise<MailboxFolderBackupManifest> {
  return (await prepareMailboxFolderBackupByEmail(email, folderSystemName)).manifest;
}

export async function streamMailboxFolderBackupPartByEmail(
  email: string,
  folderSystemName: string,
  partIndex: number,
  onSource: (message: RemoteMailboxBackupSource) => Promise<void> | void
) {
  const plan = await prepareMailboxFolderBackupByEmail(email, folderSystemName);
  const part = plan.parts.find((candidate) => candidate.index === partIndex);
  if (!part) throw new Error("mailbox-folder-backup-part-not-found");

  const streamedCount = await streamRemoteMailboxFolderBackupSources(
    plan.credentials,
    plan.target,
    part.remoteUids,
    onSource
  );

  return {
    manifest: plan.manifest,
    part: plan.manifest.parts.find((candidate) => candidate.index === partIndex)!,
    streamedCount
  };
}

export async function getMailboxStorageOverviewByEmail(email: string): Promise<MailboxStorageOverview> {
  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");
  const mailbox = await getLatestMailboxByUserId(user.id);
  if (!mailbox) throw new Error("mailbox-not-found");

  await ensureOfficialMailSchema();
  await withTransaction((connection) => ensureLocalMailboxFolders(connection, mailbox.id));
  const [quota, folderRows] = await Promise.all([
    getMailcowMailboxQuota(mailbox.email).catch(() => getCachedMailcowMailboxQuota(mailbox.email)),
    getDbPool().query<MailboxStorageFolderRow[]>(
      `
        SELECT
          folder.id,
          folder.system_name,
          folder.name,
          folder.remote_total AS message_count,
          folder.remote_unseen AS unread_count,
          COALESCE(SUM(
            CASE
              WHEN message.raw_source IS NOT NULL THEN OCTET_LENGTH(message.raw_source)
              ELSE OCTET_LENGTH(COALESCE(message.body_html, ''))
                + OCTET_LENGTH(COALESCE(message.body_text, ''))
                + OCTET_LENGTH(COALESCE(message.subject, ''))
            END
          ), 0) AS estimated_bytes
        FROM mailbox_folders folder
        LEFT JOIN mailbox_messages message ON message.folder_id = folder.id
        WHERE folder.mailbox_id = ?
        GROUP BY
          folder.id,
          folder.system_name,
          folder.name,
          folder.remote_total,
          folder.remote_unseen,
          folder.sort_order
        ORDER BY folder.sort_order ASC, folder.id ASC
      `,
      [mailbox.id]
    ).then(([rows]) => rows)
  ]);

  return {
    folders: folderRows.map((folder) => ({
      estimatedBytes: Number(folder.estimated_bytes ?? 0),
      id: Number(folder.id),
      kind: isCustomMailboxFolderSystemName(folder.system_name) ? "custom" : "system",
      messageCount: Number(folder.message_count ?? 0),
      name: folder.name,
      systemName: folder.system_name,
      unreadCount: Number(folder.unread_count ?? 0)
    })),
    isUnlimited: quota?.isUnlimited ?? false,
    limitBytes: quota?.limitBytes ?? null,
    usagePercent: quota?.usagePercent ?? null,
    usedBytes: quota?.usedBytes ?? null
  };
}

export async function moveMailboxMessagesByEmail(
  email: string,
  messageIds: Array<number | string>,
  targetFolderInput: string,
  options?: {
    releaseSpam?: boolean;
    sourceFolder?: string;
  }
) {
  const normalizedIds = normalizeMessageIds(messageIds);

  if (normalizedIds.length === 0) {
    return {
      deletedCount: 0,
      deletedMessageIds: [] as number[],
      movedCount: 0,
      movedMessageIds: [] as number[],
      spamSenderCount: 0
    } satisfies MailboxMoveActionResult;
  }

  const targetFolder = normalizeTargetFolderSystemName(targetFolderInput);
  const user = await getUserByEmail(email);

  if (!user) {
    throw new Error("user-not-found");
  }

  const mailbox = await getLatestMailboxByUserId(user.id);

  if (!mailbox) {
    throw new Error("mailbox-not-found");
  }
  const expectedSourceFolder =
    options?.sourceFolder && options.sourceFolder !== "all"
      ? normalizeTargetFolderSystemName(options.sourceFolder)
      : null;

  const deletedAssetTargets: ComposeUploadCleanupTarget[] = [];
  const moveResult = await withExclusiveMailboxOperation(mailbox.id, () =>
    withTransaction(async (connection) => {
      await ensureLocalMailboxFolders(connection, mailbox.id);
      const folderRows = await getMailboxFolderRows(connection, mailbox.id);
      const spamSettings = await getMailboxSpamGeneralSettings(connection, mailbox.id);
      const [messageRows] = await connection.query<MailboxMoveMessageRow[]>(
        `
          SELECT id, folder_id, direction, from_address, remote_folder, remote_uid
          FROM mailbox_messages
          WHERE mailbox_id = ?
            AND id IN (${normalizedIds.map(() => "?").join(", ")})
          FOR UPDATE
        `,
        [mailbox.id, ...normalizedIds]
      );

      if (messageRows.length !== normalizedIds.length) {
        throw new Error("mailbox-state-changed");
      }

      if (expectedSourceFolder) {
        const expectedSourceFolderRow = folderRows.find((folder) => folder.system_name === expectedSourceFolder);
        if (
          !expectedSourceFolderRow ||
          messageRows.some((message) => message.folder_id !== expectedSourceFolderRow.id)
        ) {
          throw new Error("mailbox-state-changed");
        }
      }

      const selectedSenderAddresses = [
        ...new Set(
          messageRows
            .filter((message) => message.direction === "inbound")
            .map((message) => normalizeSenderAddress(message.from_address))
            .filter(Boolean)
        )
      ];

      if (options?.releaseSpam) {
        const spamFolderRow = folderRows.find((folder) => folder.system_name === "spam");

        if (
          targetFolder === "spam" ||
          !spamFolderRow ||
          messageRows.some((message) => message.folder_id !== spamFolderRow.id)
        ) {
          throw new Error("mailbox-state-changed");
        }

        if (selectedSenderAddresses.length > 0) {
          await connection.query(
            `
              DELETE FROM mailbox_sender_rules
              WHERE mailbox_id = ?
                AND sender_address IN (${selectedSenderAddresses.map(() => "?").join(", ")})
            `,
            [mailbox.id, ...selectedSenderAddresses]
          );
        }
      } else if (
        targetFolder === "spam" &&
        spamSettings.blockReportedSender &&
        selectedSenderAddresses.length > 0
      ) {
        await connection.query(
          `
            INSERT INTO mailbox_sender_rules (mailbox_id, sender_address, rule_action)
            VALUES ${selectedSenderAddresses.map(() => "(?, ?, 'spam')").join(", ")}
            ON DUPLICATE KEY UPDATE
              rule_action = VALUES(rule_action),
              updated_at = NOW()
          `,
          selectedSenderAddresses.flatMap((senderAddress) => [mailbox.id, senderAddress])
        );
      }

      let rowsToMove = messageRows;

      if (targetFolder === "spam" && spamSettings.processPreviousReported) {
        const senderRows = await findInboundSenderRows(
          connection,
          mailbox.id,
          messageRows.map((message) => message.from_address)
        );

        if (senderRows.length > 0) {
          rowsToMove = senderRows;
        }
      }

      if (targetFolder === "spam" && spamSettings.reportedMessageAction === "delete") {
        const deleteResult = await permanentlyDeleteMailboxRows(connection, mailbox, rowsToMove);
        deletedAssetTargets.push(...deleteResult.cleanupTargets);
        if (deleteResult.invalidatedFolderIds.length > 0) {
          await connection.query(
            `
              UPDATE mailbox_folders
              SET last_synced_at = NULL, updated_at = NOW()
              WHERE mailbox_id = ?
                AND id IN (${deleteResult.invalidatedFolderIds.map(() => "?").join(", ")})
            `,
            [mailbox.id, ...deleteResult.invalidatedFolderIds]
          );
        }
        return {
          deletedCount: deleteResult.deletedCount,
          deletedMessageIds: deleteResult.deletedMessageIds,
          movedCount: 0,
          movedMessageIds: [] as number[],
          spamSenderCount: selectedSenderAddresses.length
        } satisfies MailboxMoveActionResult;
      }

      if (targetFolder === "trash") {
        const trashFolderRow = folderRows.find((row) => row.system_name === "trash") ?? null;
        const rowsToDelete = trashFolderRow
          ? rowsToMove.filter((message) => message.folder_id === trashFolderRow.id)
          : [];
        const rowsToTrash = trashFolderRow
          ? rowsToMove.filter((message) => message.folder_id !== trashFolderRow.id)
          : rowsToMove;
        const deleteResult = await permanentlyDeleteMailboxRows(connection, mailbox, rowsToDelete);

        deletedAssetTargets.push(...deleteResult.cleanupTargets);

        if (deleteResult.invalidatedFolderIds.length > 0) {
          await connection.query(
            `
            UPDATE mailbox_folders
            SET last_synced_at = NULL, updated_at = NOW()
            WHERE mailbox_id = ?
              AND id IN (${deleteResult.invalidatedFolderIds.map(() => "?").join(", ")})
          `,
            [mailbox.id, ...deleteResult.invalidatedFolderIds]
          );
        }

        if (rowsToTrash.length === 0) {
          return {
            deletedCount: deleteResult.deletedCount,
            deletedMessageIds: deleteResult.deletedMessageIds,
            movedCount: 0,
            movedMessageIds: [] as number[],
            spamSenderCount: 0
          } satisfies MailboxMoveActionResult;
        }

        const movedResult = await moveMailboxRowsToFolder(connection, mailbox, folderRows, rowsToTrash, targetFolder);

        return {
          ...movedResult,
          deletedCount: deleteResult.deletedCount,
          deletedMessageIds: deleteResult.deletedMessageIds
        } satisfies MailboxMoveActionResult;
      }

      return moveMailboxRowsToFolder(connection, mailbox, folderRows, rowsToMove, targetFolder);
    })
  );
  mailboxFolderStatusCache.delete(mailbox.id);
  await cleanupComposeUploadArtifacts(getDbPool(), deletedAssetTargets).catch(() => undefined);

  return moveResult;
}

function parseMailboxMoveJobResult(value: string | null) {
  if (!value) return null;

  try {
    return JSON.parse(value) as MailboxMoveActionResult;
  } catch {
    return null;
  }
}

function parseMailboxMoveJobMessageIds(value: string) {
  try {
    const parsed = JSON.parse(value) as unknown;
    return Array.isArray(parsed) ? normalizeMessageIds(parsed as Array<number | string>) : [];
  } catch {
    return [];
  }
}

function parseMailboxMoveUndoSnapshots(value: string | null) {
  if (!value) return [];

  try {
    const parsed = JSON.parse(value) as unknown;
    if (!Array.isArray(parsed)) return [];

    return parsed.flatMap((item) => {
      if (!item || typeof item !== "object") return [];
      const candidate = item as Partial<MailboxMessageMoveUndoSnapshot>;
      const messageId = Number(candidate.messageId);
      const sourceFolder = String(candidate.sourceFolder ?? "").trim();
      if (!Number.isInteger(messageId) || messageId <= 0 || !sourceFolder) return [];
      return [{ messageId, sourceFolder }];
    });
  } catch {
    return [];
  }
}

function parseMailboxMoveUndoJobIds(value: string | null) {
  if (value === null) return null;
  return parseMailboxMoveJobMessageIds(value);
}

async function insertMailboxMessageMoveJob(
  connection: PoolConnection,
  mailboxId: number,
  folderRows: MailboxFolderRowBasic[],
  reservedRows: MailboxMoveMessageRow[],
  options: {
    releaseSpam: boolean;
    sourceFolder: string;
    targetFolder: string;
  }
) {
  const reservedIds = [...new Set(reservedRows.map((row) => Number(row.id)))];
  if (reservedIds.length === 0) throw new Error("message-required");

  const folderSystemNameById = new Map(folderRows.map((folder) => [Number(folder.id), folder.system_name]));
  const undoPayload = reservedRows.map(
    (row) =>
      ({
        messageId: Number(row.id),
        sourceFolder: folderSystemNameById.get(Number(row.folder_id)) ?? options.sourceFolder
      }) satisfies MailboxMessageMoveUndoSnapshot
  );
  const [jobResult] = await connection.query<ResultSetHeader>(
    `
      INSERT INTO mailbox_message_move_jobs (
        mailbox_id,
        source_folder,
        target_folder,
        message_ids_json,
        release_spam,
        status,
        process_after_at,
        undo_payload_json
      )
      VALUES (?, ?, ?, ?, ?, 'pending', NOW(3), ?)
    `,
    [
      mailboxId,
      options.sourceFolder,
      options.targetFolder,
      JSON.stringify(reservedIds),
      options.releaseSpam ? 1 : 0,
      JSON.stringify(undoPayload)
    ]
  );
  const jobId = Number(jobResult.insertId);

  await connection.query(
    `
      INSERT INTO mailbox_message_move_job_items (
        job_id,
        mailbox_id,
        mailbox_message_id
      )
      VALUES ${reservedIds.map(() => "(?, ?, ?)").join(", ")}
    `,
    reservedIds.flatMap((messageId) => [jobId, mailboxId, messageId])
  );

  return {
    id: jobId,
    messageIds: reservedIds,
    status: "pending" as const
  };
}

async function queueMailboxRowsToFolder(
  connection: PoolConnection,
  mailboxId: number,
  folderRows: MailboxFolderRowBasic[],
  rows: MailboxMoveMessageRow[],
  targetFolder: string
) {
  const target = folderRows.find((folder) => folder.system_name === targetFolder);
  if (!target) throw new Error("folder-not-found");
  if (targetFolder === "sent" && rows.some((row) => row.direction !== "outbound")) {
    throw new Error("sent-folder-only-outbound");
  }
  const ids = [...new Set(rows.filter((row) => Number(row.folder_id) !== Number(target.id)).map((row) => Number(row.id)))];
  const result = {
    deletedCount: 0,
    deletedMessageIds: [] as number[],
    jobId: null as number | null,
    movedCount: 0,
    movedMessageIds: [] as number[],
    queued: false,
    spamSenderCount: 0
  };
  if (ids.length === 0) return result;

  const [conflicts] = await connection.query<(RowDataPacket & { job_id: number })[]>(
    `
      SELECT item.job_id
      FROM mailbox_message_move_job_items item
      INNER JOIN mailbox_message_move_jobs job ON job.id = item.job_id
      WHERE item.mailbox_id = ?
        AND item.mailbox_message_id IN (${ids.map(() => "?").join(", ")})
        AND job.status IN ('pending', 'processing')
        AND job.target_folder <> ?
      LIMIT 1
    `,
    [mailboxId, ...ids, targetFolder]
  );
  if (conflicts.length > 0) throw new Error("mailbox-state-changed");

  const [reservedRows] = await connection.query<MailboxMoveMessageRow[]>(
    `
      SELECT mm.id, mm.folder_id, mm.direction, mm.from_address, mm.remote_folder, mm.remote_uid
      FROM mailbox_messages mm
      WHERE mm.mailbox_id = ?
        AND mm.id IN (${ids.map(() => "?").join(", ")})
        AND mm.folder_id <> ?
        AND ${PENDING_MAILBOX_MOVE_EXCLUSION_SQL}
      FOR UPDATE
    `,
    [mailboxId, ...ids, target.id]
  );
  if (reservedRows.length === 0) return result;
  const job = await insertMailboxMessageMoveJob(connection, mailboxId, folderRows, reservedRows, {
    releaseSpam: false,
    sourceFolder: "all",
    targetFolder
  });
  return { ...result, jobId: job.id, movedCount: job.messageIds.length, movedMessageIds: job.messageIds, queued: true };
}

export async function enqueueMailboxMessagesMoveByEmail(
  email: string,
  messageIds: Array<number | string>,
  targetFolderInput: string,
  options?: {
    releaseSpam?: boolean;
    sourceFolder?: string;
  }
) {
  const normalizedIds = normalizeMessageIds(messageIds);
  if (normalizedIds.length === 0) throw new Error("message-required");

  const targetFolder = normalizeTargetFolderSystemName(targetFolderInput);
  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");

  const mailbox = await getLatestMailboxByUserId(user.id);
  if (!mailbox) throw new Error("mailbox-not-found");

  const expectedSourceFolder =
    options?.sourceFolder && options.sourceFolder !== "all"
      ? normalizeTargetFolderSystemName(options.sourceFolder)
      : null;

  await ensureOfficialMailSchema();

  try {
    return await withTransaction(async (connection) => {
      await ensureLocalMailboxFolders(connection, mailbox.id);
      const folderRows = await getMailboxFolderRows(connection, mailbox.id);
      const spamSettings = await getMailboxSpamGeneralSettings(connection, mailbox.id);
      const [selectedRows] = await connection.query<MailboxMoveMessageRow[]>(
        `
            SELECT id, folder_id, direction, from_address, remote_folder, remote_uid
            FROM mailbox_messages
            WHERE mailbox_id = ?
              AND id IN (${normalizedIds.map(() => "?").join(", ")})
            FOR UPDATE
          `,
        [mailbox.id, ...normalizedIds]
      );

      if (selectedRows.length !== normalizedIds.length) {
        throw new Error("mailbox-state-changed");
      }

      if (expectedSourceFolder) {
        const sourceFolderRow = folderRows.find((folder) => folder.system_name === expectedSourceFolder);
        if (!sourceFolderRow || selectedRows.some((message) => message.folder_id !== sourceFolderRow.id)) {
          throw new Error("mailbox-state-changed");
        }
      }

      if (targetFolder === "sent" && selectedRows.some((row) => row.direction !== "outbound")) {
        throw new Error("sent-folder-only-outbound");
      }

      if (options?.releaseSpam) {
        const spamFolderRow = folderRows.find((folder) => folder.system_name === "spam");
        if (
          targetFolder === "spam" ||
          !spamFolderRow ||
          selectedRows.some((message) => message.folder_id !== spamFolderRow.id)
        ) {
          throw new Error("mailbox-state-changed");
        }
      }

      let reservedRows = selectedRows;
      if (
        targetFolder === "spam" &&
        !options?.releaseSpam &&
        spamSettings.processPreviousReported
      ) {
        const senderRows = await findInboundSenderRows(
          connection,
          mailbox.id,
          selectedRows.map((message) => message.from_address)
        );
        const targetFolderRow = folderRows.find((folder) => folder.system_name === targetFolder);
        const movableSenderRows = targetFolderRow
          ? senderRows.filter((message) => Number(message.folder_id) !== Number(targetFolderRow.id))
          : senderRows;
        if (movableSenderRows.length > 0) reservedRows = movableSenderRows;
      }

      return insertMailboxMessageMoveJob(connection, mailbox.id, folderRows, reservedRows, {
        releaseSpam: Boolean(options?.releaseSpam),
        sourceFolder: expectedSourceFolder ?? "all",
        targetFolder
      });
    });
  } catch (error) {
    const code = (error as { code?: string } | null)?.code;
    if (code === "ER_DUP_ENTRY") throw new Error("mailbox-state-changed");
    throw error;
  }
}

export async function undoMailboxMessageMoveJobByEmail(
  email: string,
  jobIdInput: number | string,
  restoreFolderInput?: string
) {
  const jobId = Number(jobIdInput);
  if (!Number.isInteger(jobId) || jobId <= 0) throw new Error("job-not-found");

  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");

  const mailbox = await getLatestMailboxByUserId(user.id);
  if (!mailbox) throw new Error("mailbox-not-found");

  const restoreFolder = restoreFolderInput ? normalizeTargetFolderSystemName(restoreFolderInput) : null;
  await ensureOfficialMailSchema();

  try {
    return await withExclusiveMailboxOperation(mailbox.id, () =>
      withTransaction(async (connection) => {
        const [rows] = await connection.query<MailboxMessageMoveJobRow[]>(
          `
            SELECT job.*, mailbox.email
            FROM mailbox_message_move_jobs job
            INNER JOIN mailboxes mailbox ON mailbox.id = job.mailbox_id
            WHERE job.id = ?
              AND job.mailbox_id = ?
              AND mailbox.user_id = ?
            LIMIT 1
            FOR UPDATE
          `,
          [jobId, mailbox.id, user.id]
        );
        const job = rows[0];
        if (!job) throw new Error("job-not-found");

        const existingUndoJobIds = parseMailboxMoveUndoJobIds(job.undo_job_ids_json);
        if (existingUndoJobIds !== null) {
          return {
            cancelled: existingUndoJobIds.length === 0,
            jobIds: existingUndoJobIds,
            queued: existingUndoJobIds.length > 0,
            retry: false
          };
        }

        if (job.status === "processing") {
          return {
            cancelled: false,
            jobIds: [] as number[],
            queued: false,
            retry: true
          };
        }

        if (job.status === "pending") {
          await connection.query(
            `
              UPDATE mailbox_message_move_jobs
              SET
                status = 'failed',
                completed_at = NOW(3),
                last_error = 'undone-by-user',
                undo_job_ids_json = '[]'
              WHERE id = ?
            `,
            [job.id]
          );
          await connection.query("DELETE FROM mailbox_message_move_job_items WHERE job_id = ?", [job.id]);
          return {
            cancelled: true,
            jobIds: [] as number[],
            queued: false,
            retry: false
          };
        }

        if (job.status !== "completed") {
          if (job.last_error === "undone-by-user") {
            return {
              cancelled: true,
              jobIds: [] as number[],
              queued: false,
              retry: false
            };
          }
          throw new Error("job-undo-unavailable");
        }

        const result = parseMailboxMoveJobResult(job.result_json);
        if (!result || result.deletedCount > 0 || result.movedMessageIds.length === 0) {
          throw new Error("job-undo-unavailable");
        }

        const movedMessageIds = normalizeMessageIds(result.movedMessageIds);
        const folderRows = await getMailboxFolderRows(connection, mailbox.id);
        const currentFolder = folderRows.find((folder) => folder.system_name === job.target_folder);
        if (!currentFolder) throw new Error("mailbox-state-changed");

        const [currentRows] = await connection.query<MailboxMoveMessageRow[]>(
          `
            SELECT id, folder_id, direction, from_address, remote_folder, remote_uid
            FROM mailbox_messages
            WHERE mailbox_id = ?
              AND id IN (${movedMessageIds.map(() => "?").join(", ")})
            FOR UPDATE
          `,
          [mailbox.id, ...movedMessageIds]
        );
        if (
          currentRows.length !== movedMessageIds.length ||
          currentRows.some((message) => Number(message.folder_id) !== Number(currentFolder.id))
        ) {
          throw new Error("mailbox-state-changed");
        }

        const snapshotByMessageId = new Map(
          parseMailboxMoveUndoSnapshots(job.undo_payload_json).map((snapshot) => [
            snapshot.messageId,
            snapshot.sourceFolder
          ])
        );
        const fallbackFolder = job.source_folder !== "all" ? job.source_folder : restoreFolder;
        const groupedRows = new Map<string, MailboxMoveMessageRow[]>();

        for (const message of currentRows) {
          const targetFolder = snapshotByMessageId.get(Number(message.id)) ?? fallbackFolder;
          if (!targetFolder || targetFolder === job.target_folder) throw new Error("job-undo-unavailable");
          if (!folderRows.some((folder) => folder.system_name === targetFolder)) {
            throw new Error("mailbox-state-changed");
          }
          groupedRows.set(targetFolder, [...(groupedRows.get(targetFolder) ?? []), message]);
        }

        const undoJobIds: number[] = [];
        for (const [targetFolder, messages] of groupedRows) {
          let reservedRows = messages;
          let sourceFolder = job.target_folder;

          if (targetFolder === "spam") {
            const senderRows = await findInboundSenderRows(
              connection,
              mailbox.id,
              messages.map((message) => message.from_address)
            );
            if (senderRows.length > 0) reservedRows = senderRows;
            sourceFolder = "all";
          }

          const undoJob = await insertMailboxMessageMoveJob(connection, mailbox.id, folderRows, reservedRows, {
            releaseSpam: job.target_folder === "spam" && !Boolean(job.release_spam),
            sourceFolder,
            targetFolder
          });
          undoJobIds.push(undoJob.id);
        }

        await connection.query("UPDATE mailbox_message_move_jobs SET undo_job_ids_json = ? WHERE id = ?", [
          JSON.stringify(undoJobIds),
          job.id
        ]);

        return {
          cancelled: false,
          jobIds: undoJobIds,
          queued: undoJobIds.length > 0,
          retry: false
        };
      })
    );
  } catch (error) {
    const code = (error as { code?: string } | null)?.code;
    if (code === "ER_DUP_ENTRY") throw new Error("mailbox-state-changed");
    throw error;
  }
}

export async function getMailboxMessageMoveJobByEmail(email: string, jobIdInput: number | string) {
  const jobId = Number(jobIdInput);
  if (!Number.isInteger(jobId) || jobId <= 0) throw new Error("job-not-found");

  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");

  await ensureOfficialMailSchema();
  const [rows] = await getDbPool().query<MailboxMessageMoveJobRow[]>(
    `
      SELECT
        job.*,
        mailbox.email
      FROM mailbox_message_move_jobs job
      INNER JOIN mailboxes mailbox ON mailbox.id = job.mailbox_id
      WHERE job.id = ?
        AND mailbox.user_id = ?
      LIMIT 1
    `,
    [jobId, user.id]
  );
  const row = rows[0];
  if (!row) throw new Error("job-not-found");

  return {
    completedAt: normalizeDate(row.completed_at),
    error: row.last_error,
    id: Number(row.id),
    result: parseMailboxMoveJobResult(row.result_json),
    status: row.status
  } satisfies MailboxMessageMoveJobStatus;
}

async function claimNextMailboxMessageMoveJob() {
  return withTransaction(async (connection) => {
    const [rows] = await connection.query<MailboxMessageMoveJobRow[]>(
      `
        SELECT job.*, mailbox.email
        FROM mailbox_message_move_jobs job
        INNER JOIN mailboxes mailbox ON mailbox.id = job.mailbox_id
        WHERE job.status = 'pending'
          AND job.process_after_at <= NOW(3)
        ORDER BY job.id ASC
        LIMIT 1
        FOR UPDATE SKIP LOCKED
      `
    );
    const row = rows[0];
    if (!row) return null;

    await connection.query(
      `
        UPDATE mailbox_message_move_jobs
        SET
          status = 'processing',
          attempt_count = attempt_count + 1,
          processing_started_at = NOW(3),
          last_error = NULL
        WHERE id = ?
      `,
      [row.id]
    );

    return { ...row, attempt_count: Number(row.attempt_count) + 1 };
  });
}

export async function processPendingMailboxMessageMoveJobs(options?: { limit?: number }) {
  await ensureOfficialMailSchema();
  const limit = Math.min(50, Math.max(1, Number(options?.limit ?? 10)));
  const pool = getDbPool();

  await pool.query(
    `
      DELETE FROM mailbox_message_move_jobs
      WHERE status IN ('completed', 'failed')
        AND completed_at < DATE_SUB(NOW(3), INTERVAL 30 DAY)
    `
  );

  await pool.query(
    `
      UPDATE mailbox_message_move_jobs
      SET
        status = 'pending',
        processing_started_at = NULL,
        process_after_at = NOW(3),
        last_error = '작업 프로세스가 중단되어 다시 시도합니다.'
      WHERE status = 'processing'
        AND processing_started_at < DATE_SUB(NOW(3), INTERVAL 5 MINUTE)
    `
  );

  const summary = { attempted: 0, completed: 0, failed: 0, retried: 0 };

  for (let index = 0; index < limit; index += 1) {
    const job = await claimNextMailboxMessageMoveJob();
    if (!job) break;
    summary.attempted += 1;

    const messageIds = parseMailboxMoveJobMessageIds(job.message_ids_json);
    try {
      if (messageIds.length === 0) throw new Error("message-required");

      const result = await moveMailboxMessagesByEmail(job.email, messageIds, job.target_folder, {
        releaseSpam: Boolean(job.release_spam),
        sourceFolder: job.source_folder
      });

      await withTransaction(async (connection) => {
        await connection.query(
          `
            UPDATE mailbox_message_move_jobs
            SET
              status = 'completed',
              completed_at = NOW(3),
              processing_started_at = NULL,
              result_json = ?,
              last_error = NULL
            WHERE id = ?
          `,
          [JSON.stringify(result), job.id]
        );
        await connection.query("DELETE FROM mailbox_message_move_job_items WHERE job_id = ?", [job.id]);
      });
      summary.completed += 1;
    } catch (error) {
      const message = error instanceof Error ? error.message.slice(0, 500) : "move-failed";
      const retryable =
        job.attempt_count < 5 &&
        (message === "mailbox-busy" ||
          message === "mailbox-move-failed" ||
          message === "mailbox-delete-failed" ||
          isRetryableMailboxSyncWriteError(error));

      if (retryable) {
        const retryAt = new Date(Date.now() + Math.min(30_000, 500 * 2 ** job.attempt_count));
        await pool.query(
          `
            UPDATE mailbox_message_move_jobs
            SET
              status = 'pending',
              processing_started_at = NULL,
              process_after_at = ?,
              last_error = ?
            WHERE id = ?
          `,
          [retryAt, message, job.id]
        );
        summary.retried += 1;
      } else {
        await withTransaction(async (connection) => {
          await connection.query(
            `
              UPDATE mailbox_message_move_jobs
              SET
                status = 'failed',
                completed_at = NOW(3),
                processing_started_at = NULL,
                last_error = ?
              WHERE id = ?
            `,
            [message, job.id]
          );
          await connection.query("DELETE FROM mailbox_message_move_job_items WHERE job_id = ?", [job.id]);
        });
        summary.failed += 1;
      }
    }
  }

  const [remainingRows] = await pool.query<(RowDataPacket & { count: number })[]>(
    `
      SELECT COUNT(*) AS count
      FROM mailbox_message_move_jobs
      WHERE status IN ('pending', 'processing')
    `
  );

  return { ...summary, remaining: Number(remainingRows[0]?.count ?? 0) };
}

export async function processExpiredMailboxSpamMessages(options?: {
  limitMailboxes?: number;
  limitMessagesPerMailbox?: number;
}) {
  await ensureOfficialMailSchema();
  const limitMailboxes = Math.min(10, Math.max(1, Number(options?.limitMailboxes ?? 1)));
  const limitMessages = Math.min(200, Math.max(1, Number(options?.limitMessagesPerMailbox ?? 50)));
  const [targets] = await getDbPool().query<
    (RowDataPacket & {
      expired_message_action: "delete" | "trash";
      mailbox_id: number;
      owner_email: string;
      retention_days: number;
    })[]
  >(
    `
      SELECT
        settings.mailbox_id,
        settings.retention_days,
        settings.expired_message_action,
        owner.email AS owner_email
      FROM mailbox_spam_settings settings
      INNER JOIN mailboxes mailbox ON mailbox.id = settings.mailbox_id
      INNER JOIN users owner ON owner.id = mailbox.user_id
      WHERE EXISTS (
        SELECT 1
        FROM mailbox_messages message
        INNER JOIN mailbox_folders folder ON folder.id = message.folder_id
        WHERE message.mailbox_id = settings.mailbox_id
          AND folder.system_name = 'spam'
          AND message.received_at < DATE_SUB(NOW(), INTERVAL settings.retention_days DAY)
      )
      ORDER BY settings.updated_at ASC, settings.mailbox_id ASC
      LIMIT ?
    `,
    [limitMailboxes]
  );
  const summary = { attemptedMailboxes: 0, deletedMessages: 0, movedMessages: 0 };

  for (const target of targets) {
    const [rows] = await getDbPool().query<(RowDataPacket & { id: number })[]>(
      `
        SELECT message.id
        FROM mailbox_messages message
        INNER JOIN mailbox_folders folder ON folder.id = message.folder_id
        WHERE message.mailbox_id = ?
          AND folder.system_name = 'spam'
          AND message.received_at < DATE_SUB(NOW(), INTERVAL ? DAY)
        ORDER BY message.received_at ASC, message.id ASC
        LIMIT ?
      `,
      [target.mailbox_id, target.retention_days, limitMessages]
    );
    const messageIds = rows.map((row) => Number(row.id));
    if (messageIds.length === 0) continue;
    summary.attemptedMailboxes += 1;

    if (target.expired_message_action === "trash") {
      const result = await moveMailboxMessagesByEmail(target.owner_email, messageIds, "trash", {
        sourceFolder: "spam"
      });
      summary.movedMessages += result.movedCount;
      summary.deletedMessages += result.deletedCount;
      continue;
    }

    const user = await getUserByEmail(target.owner_email);
    const mailbox = user ? await getMailboxByUserIdAndId(user.id, Number(target.mailbox_id)) : null;
    if (!mailbox) continue;
    let cleanupTargets: ComposeUploadCleanupTarget[] = [];
    const deletedCount = await withExclusiveMailboxOperation(mailbox.id, () =>
      withTransaction(async (connection) => {
        const [messageRows] = await connection.query<MailboxMoveMessageRow[]>(
          `
            SELECT message.id, message.folder_id, message.direction, message.from_address, message.remote_folder, message.remote_uid
            FROM mailbox_messages message
            INNER JOIN mailbox_folders folder ON folder.id = message.folder_id
            WHERE message.mailbox_id = ?
              AND folder.system_name = 'spam'
              AND message.id IN (${messageIds.map(() => "?").join(", ")})
            FOR UPDATE
          `,
          [mailbox.id, ...messageIds]
        );
        const result = await permanentlyDeleteMailboxRows(connection, mailbox, messageRows);
        cleanupTargets = result.cleanupTargets;
        if (result.invalidatedFolderIds.length > 0) {
          await connection.query(
            `
              UPDATE mailbox_folders
              SET last_synced_at = NULL, updated_at = NOW()
              WHERE mailbox_id = ?
                AND id IN (${result.invalidatedFolderIds.map(() => "?").join(", ")})
            `,
            [mailbox.id, ...result.invalidatedFolderIds]
          );
        }
        return result.deletedCount;
      })
    );
    await cleanupComposeUploadArtifacts(getDbPool(), cleanupTargets).catch(() => undefined);
    summary.deletedMessages += deletedCount;
  }

  return summary;
}

export async function markMailboxFolderReadByEmail(email: string, folderSystemNameInput: string) {
  const folderSystemName = normalizeTargetFolderSystemName(folderSystemNameInput);
  const user = await getUserByEmail(email);

  if (!user) {
    throw new Error("user-not-found");
  }

  const mailbox = await getLatestMailboxByUserId(user.id);

  if (!mailbox) {
    throw new Error("mailbox-not-found");
  }

  const [rows] = await getDbPool().query<(RowDataPacket & { id: number })[]>(
    `
      SELECT mm.id
      FROM mailbox_messages mm
      INNER JOIN mailbox_folders f ON f.id = mm.folder_id
      WHERE mm.mailbox_id = ?
        AND f.system_name = ?
        AND mm.is_read = 0
    `,
    [mailbox.id, folderSystemName]
  );

  return markMailboxMessagesAsReadByEmail(
    email,
    rows.map((row) => row.id),
    true
  );
}

export async function clearMailboxFolderByEmail(email: string, folderSystemNameInput: string) {
  const folderSystemName = normalizeTargetFolderSystemName(folderSystemNameInput);

  const user = await getUserByEmail(email);

  if (!user) {
    throw new Error("user-not-found");
  }

  const mailbox = await getLatestMailboxByUserId(user.id);

  if (!mailbox) {
    throw new Error("mailbox-not-found");
  }

  let deletedAssetTargets: ComposeUploadCleanupTarget[] = [];
  const deletedCount = await withTransaction(async (connection) => {
    await ensureLocalMailboxFolders(connection, mailbox.id);
    const [folderRows] = await connection.query<
      (RowDataPacket & {
        id: number;
        name: string;
        remote_name: string | null;
        system_name: string;
      })[]
    >(
      `
        SELECT id, name, remote_name, system_name
        FROM mailbox_folders
        WHERE mailbox_id = ?
          AND system_name = ?
        LIMIT 1
      `,
      [mailbox.id, folderSystemName]
    );

    const folderRow = folderRows[0];

    if (!folderRow) {
      throw new Error("folder-not-found");
    }

    const [messageRows] = await connection.query<MailboxFolderMessageRow[]>(
      `
        SELECT
          id,
          folder_id,
          remote_uid,
          remote_folder,
          direction,
          subject,
          from_name,
          from_address,
          to_addresses,
          snippet,
          body_text,
          body_html,
          message_id_header,
          raw_source,
          transport_status,
          transport_response,
          remote_flags,
          is_read,
          is_starred,
          received_at
        FROM mailbox_messages
        WHERE mailbox_id = ?
          AND folder_id = ?
        ORDER BY id ASC
      `,
      [mailbox.id, folderRow.id]
    );

    if (messageRows.length === 0) {
      return 0;
    }
    const deleteResult = await permanentlyDeleteMailboxRows(connection, mailbox, messageRows);
    deletedAssetTargets = deleteResult.cleanupTargets;

    await connection.query(
      `
        UPDATE mailbox_folders
        SET
          remote_total = 0,
          remote_unseen = 0,
          last_synced_at = NULL,
          updated_at = NOW()
        WHERE id = ?
      `,
      [folderRow.id]
    );

    return deleteResult.deletedCount;
  });
  await cleanupComposeUploadArtifacts(getDbPool(), deletedAssetTargets).catch(() => undefined);

  await updateMailboxSyncState(mailbox.id, {
    lastSyncAt: new Date(),
    lastSyncError: null
  });

  return deletedCount;
}

export async function enqueueMailboxTrashClearByEmail(email: string) {
  const user = await getUserByEmail(email);

  if (!user) {
    throw new Error("user-not-found");
  }

  const mailbox = await getLatestMailboxByUserId(user.id);

  if (!mailbox) {
    throw new Error("mailbox-not-found");
  }

  await ensureOfficialMailSchema();
  const [messageRows] = await getDbPool().query<(RowDataPacket & { id: number })[]>(
    `
      SELECT message.id
      FROM mailbox_messages message
      INNER JOIN mailbox_folders folder ON folder.id = message.folder_id
      WHERE message.mailbox_id = ?
        AND folder.system_name = 'trash'
        AND NOT EXISTS (
          SELECT 1
          FROM mailbox_message_move_job_items pending_move
          WHERE pending_move.mailbox_id = message.mailbox_id
            AND pending_move.mailbox_message_id = message.id
        )
      ORDER BY message.id ASC
    `,
    [mailbox.id]
  );
  const messageIds = messageRows.map((row) => Number(row.id));

  if (messageIds.length === 0) {
    return {
      jobId: null,
      queued: false,
      requestedCount: 0
    };
  }

  const job = await enqueueMailboxMessagesMoveByEmail(email, messageIds, "trash", {
    sourceFolder: "trash"
  });

  return {
    jobId: job.id,
    queued: true,
    requestedCount: job.messageIds.length
  };
}

export async function backupMailboxFolderByEmail(email: string, folderSystemNameInput: string) {
  const folderSystemName = normalizeTargetFolderSystemName(folderSystemNameInput);
  const user = await getUserByEmail(email);

  if (!user) {
    throw new Error("user-not-found");
  }

  const mailbox = await getLatestMailboxByUserId(user.id);

  if (!mailbox) {
    throw new Error("mailbox-not-found");
  }

  const result = await withTransaction(async (connection) => {
    await ensureLocalMailboxFolders(connection, mailbox.id);
    const [folderRows] = await connection.query<
      (RowDataPacket & {
        id: number;
        name: string;
        remote_name: string | null;
        system_name: string;
      })[]
    >(
      `
        SELECT id, name, remote_name, system_name
        FROM mailbox_folders
        WHERE mailbox_id = ?
          AND system_name = ?
        LIMIT 1
      `,
      [mailbox.id, folderSystemName]
    );

    const sourceFolder = folderRows[0];

    if (!sourceFolder) {
      throw new Error("folder-not-found");
    }

    const sourceLabel = folderDisplayNameForAction(sourceFolder.system_name, sourceFolder.name);
    const backupFolder = await createCustomMailboxFolder(connection, mailbox, {
      name: formatFolderBackupName(sourceLabel)
    });
    const [messageRows] = await connection.query<MailboxFolderMessageRow[]>(
      `
        SELECT
          id,
          folder_id,
          remote_uid,
          remote_folder,
          direction,
          subject,
          from_name,
          from_address,
          to_addresses,
          snippet,
          body_text,
          body_html,
          message_id_header,
          raw_source,
          transport_status,
          transport_response,
          remote_flags,
          is_read,
          is_starred,
          received_at
        FROM mailbox_messages
        WHERE mailbox_id = ?
          AND folder_id = ?
        ORDER BY received_at ASC, id ASC
      `,
      [mailbox.id, sourceFolder.id]
    );

    if (messageRows.length === 0) {
      return {
        copiedCount: 0,
        folderName: backupFolder.name,
        folderSystemName: backupFolder.systemName
      };
    }

    const credentials = mailbox.password_ciphertext ? toMailboxCredentials(mailbox) : null;
    let copiedCount = 0;

    for (const message of messageRows) {
      let remoteFolder = backupFolder.name;
      let remoteUid: number | null = null;

      if (credentials && message.raw_source) {
        const appendResult = await appendMessageToRemoteFolder(
          credentials,
          backupFolder.name,
          message.raw_source,
          remoteFlagList(message.remote_flags)
        );

        if (!appendResult) {
          throw new Error("mailbox-folder-backup-failed");
        }

        remoteFolder = appendResult.destination ?? backupFolder.name;
        remoteUid = appendResult.uid ?? null;
      }

      await upsertMailboxMessageRow(connection, {
        bodyHtml: message.body_html,
        bodyText: message.body_text,
        direction: message.direction,
        folderId: backupFolder.id,
        fromAddress: message.from_address,
        fromName: message.from_name,
        isRead: Boolean(message.is_read),
        isStarred: Boolean(message.is_starred),
        mailboxId: mailbox.id,
        messageIdHeader: message.message_id_header,
        preserveExistingTransport: false,
        rawSource: message.raw_source,
        receivedAt: message.received_at,
        remoteFlags: message.remote_flags,
        remoteFolder,
        remoteUid,
        snippet: message.snippet,
        subject: message.subject,
        toAddresses: message.to_addresses,
        transportResponse: message.transport_response,
        transportStatus: message.transport_status
      });
      copiedCount += 1;
    }

    await connection.query(
      `
        UPDATE mailbox_folders
        SET last_synced_at = NULL, updated_at = NOW()
        WHERE id = ?
      `,
      [backupFolder.id]
    );

    return {
      copiedCount,
      folderName: backupFolder.name,
      folderSystemName: backupFolder.systemName
    };
  });

  await updateMailboxSyncState(mailbox.id, {
    lastSyncAt: new Date(),
    lastSyncError: null
  });

  return result;
}

export async function updateMailboxSenderSpamRuleByEmail(
  email: string,
  senderAddressInput: string,
  options: {
    enabled: boolean;
    moveExistingMessages?: boolean;
    targetFolder?: string;
  }
) {
  return setMailboxSenderDispositionByEmail(email, senderAddressInput, options.enabled ? "spam" : "allow", {
    moveExistingMessages: options.enabled ? true : options.moveExistingMessages,
    targetFolder: options.targetFolder
  });
}

export async function createMailboxFolderByEmail(
  email: string,
  folderName: string,
  options?: {
    insertAfterSystemName?: string | null;
    insertAtTop?: boolean;
    parentSystemName?: string | null;
  }
) {
  const user = await getUserByEmail(email);

  if (!user) {
    throw new Error("user-not-found");
  }

  const mailbox = await getLatestMailboxByUserId(user.id);

  if (!mailbox) {
    throw new Error("mailbox-not-found");
  }

  return withTransaction(async (connection) =>
    createCustomMailboxFolder(connection, mailbox, {
      insertAfterSystemName: options?.insertAfterSystemName,
      insertAtTop: options?.insertAtTop,
      name: folderName,
      parentSystemName: options?.parentSystemName
    })
  );
}

export async function renameMailboxFolderByEmail(email: string, folderSystemNameInput: string, nextFolderName: string) {
  const user = await getUserByEmail(email);

  if (!user) {
    throw new Error("user-not-found");
  }

  const mailbox = await getLatestMailboxByUserId(user.id);

  if (!mailbox) {
    throw new Error("mailbox-not-found");
  }

  const folderSystemName = normalizeTargetFolderSystemName(folderSystemNameInput);
  const normalizedName = normalizeCustomFolderName(nextFolderName);

  if (!isCustomMailboxFolderSystemName(folderSystemName)) {
    throw new Error("custom-folder-only");
  }

  if (!normalizedName) {
    throw new Error("folder-name-required");
  }

  assertCustomFolderLeafName(normalizedName);

  return withTransaction(async (connection) => {
    await ensureLocalMailboxFolders(connection, mailbox.id);
    const folderRows = await getMailboxFolderRows(connection, mailbox.id);
    const targetFolder = folderRows.find((row) => row.system_name === folderSystemName);

    if (!targetFolder) {
      throw new Error("folder-not-found");
    }

    const currentRemoteName = targetFolder.remote_name || targetFolder.name;
    const separatorIndex = currentRemoteName.lastIndexOf(MAILBOX_FOLDER_HIERARCHY_SEPARATOR);
    const parentRemoteName = separatorIndex > 0 ? currentRemoteName.slice(0, separatorIndex) : "";
    const nextRemoteName = parentRemoteName
      ? `${parentRemoteName}${MAILBOX_FOLDER_HIERARCHY_SEPARATOR}${normalizedName}`
      : normalizedName;
    const duplicateFolder = folderRows.find(
      (row) =>
        row.system_name !== folderSystemName &&
        mailboxFolderRemoteName(row).toLowerCase() === nextRemoteName.toLowerCase(),
    );

    if (duplicateFolder) {
      throw new Error("folder-name-exists");
    }

    const credentials = mailbox.password_ciphertext ? toMailboxCredentials(mailbox) : null;

    if (credentials && currentRemoteName !== nextRemoteName) {
      await renameRemoteMailboxFolder(credentials, currentRemoteName, nextRemoteName);
    }

    const hierarchyRows = folderRows.filter((row) => {
      const remoteName = mailboxFolderRemoteName(row);
      return (
        row.id === targetFolder.id ||
        isMailboxFolderDescendantRemoteName(remoteName, currentRemoteName)
      );
    });

    for (const folder of hierarchyRows) {
      const currentFolderRemoteName = mailboxFolderRemoteName(folder);
      const nextFolderRemoteName =
        folder.id === targetFolder.id
          ? nextRemoteName
          : `${nextRemoteName}${currentFolderRemoteName.slice(currentRemoteName.length)}`;

      await connection.query(
        `
          UPDATE mailbox_folders
          SET
            name = CASE WHEN id = ? THEN ? ELSE name END,
            remote_name = ?,
            last_synced_at = NULL,
            updated_at = NOW()
          WHERE mailbox_id = ? AND id = ?
        `,
        [targetFolder.id, normalizedName, nextFolderRemoteName, mailbox.id, folder.id]
      );

      await connection.query(
        `
          UPDATE mailbox_messages
          SET remote_folder = ?, updated_at = NOW()
          WHERE mailbox_id = ? AND folder_id = ?
        `,
        [nextFolderRemoteName, mailbox.id, folder.id]
      );
    }

    return {
      id: targetFolder.id,
      name: normalizedName,
      parentSystemName: mailboxFolderParentSystemName(targetFolder, folderRows),
      systemName: folderSystemName
    };
  });
}

export async function moveMailboxFolderByEmail(
  email: string,
  folderSystemNameInput: string,
  parentSystemNameInput?: string | null,
) {
  const user = await getUserByEmail(email);

  if (!user) {
    throw new Error("user-not-found");
  }

  const mailbox = await getLatestMailboxByUserId(user.id);

  if (!mailbox) {
    throw new Error("mailbox-not-found");
  }

  const folderSystemName = normalizeTargetFolderSystemName(folderSystemNameInput);
  const parentSystemName = parentSystemNameInput
    ? normalizeTargetFolderSystemName(parentSystemNameInput)
    : null;

  if (
    !isCustomMailboxFolderSystemName(folderSystemName) ||
    (parentSystemName && !isCustomMailboxFolderSystemName(parentSystemName))
  ) {
    throw new Error("custom-folder-only");
  }

  if (parentSystemName === folderSystemName) {
    throw new Error("folder-parent-invalid");
  }

  return withTransaction(async (connection) => {
    await ensureLocalMailboxFolders(connection, mailbox.id);
    const folderRows = await getMailboxFolderRows(connection, mailbox.id);
    const sourceFolder = folderRows.find((row) => row.system_name === folderSystemName);
    const parentFolder = parentSystemName
      ? folderRows.find((row) => row.system_name === parentSystemName)
      : null;

    if (!sourceFolder || (parentSystemName && !parentFolder)) {
      throw new Error("folder-not-found");
    }

    const currentRemoteName = mailboxFolderRemoteName(sourceFolder);
    const parentRemoteName = parentFolder ? mailboxFolderRemoteName(parentFolder) : null;

    if (
      parentRemoteName &&
      (parentRemoteName.toLowerCase() === currentRemoteName.toLowerCase() ||
        isMailboxFolderDescendantRemoteName(parentRemoteName, currentRemoteName))
    ) {
      throw new Error("folder-parent-invalid");
    }

    const nextRemoteName = parentRemoteName
      ? `${parentRemoteName}${MAILBOX_FOLDER_HIERARCHY_SEPARATOR}${sourceFolder.name}`
      : sourceFolder.name;
    const collision = folderRows.find(
      (row) =>
        row.id !== sourceFolder.id &&
        mailboxFolderRemoteName(row).toLowerCase() === nextRemoteName.toLowerCase(),
    );

    if (collision) {
      throw new Error("folder-name-exists");
    }

    if (nextRemoteName !== currentRemoteName) {
      const credentials = mailbox.password_ciphertext ? toMailboxCredentials(mailbox) : null;

      if (credentials) {
        await renameRemoteMailboxFolder(credentials, currentRemoteName, nextRemoteName);
      }

      const hierarchyRows = folderRows.filter((row) => {
        const remoteName = mailboxFolderRemoteName(row);
        return (
          row.id === sourceFolder.id ||
          isMailboxFolderDescendantRemoteName(remoteName, currentRemoteName)
        );
      });

      for (const folder of hierarchyRows) {
        const currentFolderRemoteName = mailboxFolderRemoteName(folder);
        const nextFolderRemoteName =
          folder.id === sourceFolder.id
            ? nextRemoteName
            : `${nextRemoteName}${currentFolderRemoteName.slice(currentRemoteName.length)}`;

        await connection.query(
          `
            UPDATE mailbox_folders
            SET remote_name = ?, last_synced_at = NULL, updated_at = NOW()
            WHERE mailbox_id = ? AND id = ?
          `,
          [nextFolderRemoteName, mailbox.id, folder.id],
        );
        await connection.query(
          `
            UPDATE mailbox_messages
            SET remote_folder = ?, updated_at = NOW()
            WHERE mailbox_id = ? AND folder_id = ?
          `,
          [nextFolderRemoteName, mailbox.id, folder.id],
        );
      }
    }

    return {
      id: sourceFolder.id,
      name: sourceFolder.name,
      parentSystemName,
      systemName: sourceFolder.system_name,
    };
  });
}

export async function deleteMailboxFolderByEmail(email: string, folderSystemNameInput: string) {
  const user = await getUserByEmail(email);

  if (!user) {
    throw new Error("user-not-found");
  }

  const mailbox = await getLatestMailboxByUserId(user.id);

  if (!mailbox) {
    throw new Error("mailbox-not-found");
  }

  const folderSystemName = normalizeTargetFolderSystemName(folderSystemNameInput);

  if (!isCustomMailboxFolderSystemName(folderSystemName)) {
    throw new Error("custom-folder-only");
  }

  return withTransaction(async (connection) => {
    await ensureLocalMailboxFolders(connection, mailbox.id);
    const folderRows = await getMailboxFolderRows(connection, mailbox.id);
    const targetFolder = folderRows.find((row) => row.system_name === folderSystemName);

    if (!targetFolder) {
      throw new Error("folder-not-found");
    }

    const remoteFolderName = mailboxFolderRemoteName(targetFolder);
    const hierarchyFolders = folderRows.filter((folder) => {
      const candidateRemoteName = mailboxFolderRemoteName(folder);
      return (
        folder.id === targetFolder.id ||
        isMailboxFolderDescendantRemoteName(candidateRemoteName, remoteFolderName)
      );
    });
    const hierarchyFolderIds = hierarchyFolders.map((folder) => folder.id);

    const [folderMessages] = await connection.query<MailboxMoveMessageRow[]>(
      `
        SELECT
          id,
          folder_id,
          direction,
          from_address,
          remote_folder,
          remote_uid
        FROM mailbox_messages
        WHERE mailbox_id = ?
          AND folder_id IN (${hierarchyFolderIds.map(() => "?").join(", ")})
      `,
      [mailbox.id, ...hierarchyFolderIds]
    );

    const moveResult =
      folderMessages.length > 0
        ? await moveMailboxRowsToFolder(connection, mailbox, folderRows, folderMessages, "inbox")
        : { movedCount: 0 };

    const credentials = mailbox.password_ciphertext ? toMailboxCredentials(mailbox) : null;

    if (credentials && remoteFolderName) {
      for (const folder of [...hierarchyFolders].sort(
        (left, right) => mailboxFolderRemoteName(right).length - mailboxFolderRemoteName(left).length,
      )) {
        await deleteRemoteMailboxFolder(credentials, mailboxFolderRemoteName(folder));
      }
    }

    await connection.query(
      `
        DELETE FROM mailbox_folders
        WHERE mailbox_id = ?
          AND id IN (${hierarchyFolderIds.map(() => "?").join(", ")})
          AND system_name LIKE 'custom\\_\\_%' ESCAPE '\\\\'
      `,
      [mailbox.id, ...hierarchyFolderIds]
    );

    return {
      deletedFolderName: targetFolder.name,
      movedCount: moveResult.movedCount
    };
  });
}

export async function getSessionSnapshotByEmail(email: string) {
  try {
    const user = await getUserByEmail(email);

    if (!user) {
      return null;
    }

    const setup = await getLatestSetupByUserId(user.id);
    return buildSessionSnapshot(user, setup);
  } catch {
    return null;
  }
}

export async function updatePreferredLocaleByEmail(email: string, locale: SupportedLocale) {
  await ensureOfficialMailSchema();
  const normalizedEmail = email.trim().toLowerCase();
  const normalizedLocale = normalizeSupportedLocale(locale);

  if (!normalizedEmail || !normalizedLocale) {
    throw new Error("invalid-locale");
  }

  const [result] = await getDbPool().query(
    `
      UPDATE users
      SET preferred_locale = ?, updated_at = NOW()
      WHERE email = ?
    `,
    [normalizedLocale, normalizedEmail]
  );
  const affectedRows = Number((result as { affectedRows?: number }).affectedRows ?? 0);

  if (affectedRows === 0) {
    throw new Error("user-not-found");
  }

  return normalizedLocale;
}

export async function getPreferredNotificationEmailByEmail(email: string) {
  const normalizedEmail = normalizeEmailAddress(email);
  const user = await getUserByEmail(normalizedEmail);
  const recoveryEmail = normalizeEmailAddress(user?.recovery_email ?? "");

  return isValidEmailAddress(recoveryEmail) ? recoveryEmail : normalizedEmail;
}

export async function getRecoveryEmailOverviewByEmail(email: string): Promise<RecoveryEmailOverview> {
  const normalizedEmail = normalizeEmailAddress(email);
  const user = await getUserByEmail(normalizedEmail);
  const recoveryEmail = normalizeEmailAddress(user?.recovery_email ?? "");

  return {
    email: isValidEmailAddress(recoveryEmail) ? recoveryEmail : "",
    verified: Boolean(isValidEmailAddress(recoveryEmail) && user?.recovery_email_verified_at)
  };
}

export async function updateRecoveryEmailByEmail(input: {
  accountEmail: string;
  recoveryEmail: string;
  recoveryVerificationId: number;
  recoveryVerificationToken: string;
}) {
  await ensureOfficialMailSchema();
  const accountEmail = normalizeEmailAddress(input.accountEmail);
  const recoveryEmail = normalizeEmailAddress(input.recoveryEmail);
  const verificationId = Number(input.recoveryVerificationId);
  const verificationToken = input.recoveryVerificationToken.trim();

  if (!accountEmail) {
    throw new Error("user-not-found");
  }

  if (!recoveryEmail) {
    throw new Error("recovery-email-required");
  }

  if (!isValidEmailAddress(recoveryEmail)) {
    throw new Error("recovery-email-invalid");
  }

  if (accountEmail === recoveryEmail) {
    throw new Error("recovery-email-must-differ");
  }

  if (!Number.isInteger(verificationId) || verificationId <= 0 || !verificationToken) {
    throw new Error("recovery-email-unverified");
  }

  return withTransaction(async (connection) => {
    const [userRows] = await connection.query<(RowDataPacket & { id: number })[]>(
      `
        SELECT id
        FROM users
        WHERE email = ?
        LIMIT 1
        FOR UPDATE
      `,
      [accountEmail]
    );
    const user = userRows[0];

    if (!user) {
      throw new Error("user-not-found");
    }

    const verification = await getSignupRecoveryEmailVerificationById(verificationId, connection);

    if (
      !verification ||
      verification.email !== recoveryEmail ||
      verification.consumed_at ||
      !verification.verified_at ||
      !verification.verification_token_hash ||
      verification.expires_at.getTime() <= Date.now() ||
      verification.verification_token_hash !== createSha256Hex(verificationToken)
    ) {
      throw new Error("recovery-email-unverified");
    }

    const [consumeResult] = await connection.query<ResultSetHeader>(
      `
        UPDATE signup_recovery_email_verifications
        SET consumed_at = NOW(), updated_at = NOW()
        WHERE id = ?
          AND consumed_at IS NULL
      `,
      [verification.id]
    );

    if (consumeResult.affectedRows !== 1) {
      throw new Error("recovery-email-unverified");
    }

    await connection.query(
      `
        UPDATE users
        SET
          recovery_email = ?,
          recovery_email_verified_at = ?,
          updated_at = NOW()
        WHERE id = ?
      `,
      [recoveryEmail, verification.verified_at, user.id]
    );

    return {
      email: recoveryEmail,
      verified: true
    } satisfies RecoveryEmailOverview;
  });
}

async function getSessionSnapshotByUserId(userId: number) {
  try {
    const user = await getUserById(userId);

    if (!user) {
      return null;
    }

    const setup = await getLatestSetupByUserId(user.id);
    return buildSessionSnapshot(user, setup);
  } catch {
    return null;
  }
}

export async function getMailLayoutPreferencesByEmail(email: string) {
  const user = await getUserByEmail(email);

  if (!user) {
    return DEFAULT_MAIL_LAYOUT_PREFERENCES;
  }

  return buildMailLayoutPreferences(user);
}

export async function updateMailLayoutPreferencesByEmail(email: string, preferences: Partial<MailLayoutPreferences>) {
  await ensureOfficialMailSchema();
  const normalizedEmail = email.trim().toLowerCase();
  const user = await getUserByEmail(normalizedEmail);

  const normalizedPreferences = normalizeMailLayoutPreferences({
    sidebarWidth: preferences.sidebarWidth ?? user?.mail_sidebar_width ?? DEFAULT_MAIL_LAYOUT_PREFERENCES.sidebarWidth,
    listWidth: preferences.listWidth ?? user?.mail_list_width ?? DEFAULT_MAIL_LAYOUT_PREFERENCES.listWidth,
    listHeight: preferences.listHeight ?? user?.mail_list_height ?? DEFAULT_MAIL_LAYOUT_PREFERENCES.listHeight,
    viewMode:
      preferences.viewMode ??
      (user?.mail_view_mode as MailLayoutPreferences["viewMode"] | undefined) ??
      DEFAULT_MAIL_LAYOUT_PREFERENCES.viewMode,
    sortOrder:
      preferences.sortOrder ??
      (user?.mail_sort_order as MailLayoutPreferences["sortOrder"] | undefined) ??
      DEFAULT_MAIL_LAYOUT_PREFERENCES.sortOrder
  });

  await getDbPool().query(
    `
      UPDATE users
      SET
        mail_sidebar_width = ?,
        mail_list_width = ?,
        mail_list_height = ?,
        mail_view_mode = ?,
        mail_sort_order = ?,
        updated_at = NOW()
      WHERE email = ?
    `,
    [
      normalizedPreferences.sidebarWidth,
      normalizedPreferences.listWidth,
      normalizedPreferences.listHeight,
      normalizedPreferences.viewMode,
      normalizedPreferences.sortOrder,
      normalizedEmail
    ]
  );

  return normalizedPreferences;
}

export async function updateMailboxFolderOrderByEmail(email: string, folderIds: number[]) {
  await ensureOfficialMailSchema();
  const normalizedEmail = email.trim().toLowerCase();
  const user = await getUserByEmail(normalizedEmail);

  if (!user) {
    return [];
  }

  const mailbox = await getLatestMailboxByUserId(user.id);

  if (!mailbox) {
    return [];
  }

  const requestedIds = folderIds
    .map((value) => Number(value))
    .filter((value, index, array) => Number.isInteger(value) && value > 0 && array.indexOf(value) === index);

  return withTransaction(async (connection) => {
    await ensureLocalMailboxFolders(connection, mailbox.id);

    const [folderRows] = await connection.query<(RowDataPacket & { id: number })[]>(
      `
        SELECT id
        FROM mailbox_folders
        WHERE mailbox_id = ?
        ORDER BY sort_order ASC, id ASC
      `,
      [mailbox.id]
    );

    const validIds = folderRows.map((row) => Number(row.id));
    const nextOrder = requestedIds.filter((id) => validIds.includes(id));

    for (const id of validIds) {
      if (!nextOrder.includes(id)) {
        nextOrder.push(id);
      }
    }

    for (const [index, id] of nextOrder.entries()) {
      await connection.query(
        `
          UPDATE mailbox_folders
          SET sort_order = ?
          WHERE mailbox_id = ? AND id = ?
        `,
        [index + 1, mailbox.id, id]
      );
    }

    return nextOrder;
  });
}

export async function loginOrCreateUser(input: { email: string; password: string }) {
  const email = input.email.trim().toLowerCase();
  const password = input.password.trim();

  if (!email || !password) {
    throw new Error("required");
  }

  const existingUser = await getUserByEmail(email);

  if (!existingUser) {
    throw new Error("invalid-password");
  }

  if (!verifyPassword(password, existingUser.password_hash)) {
    throw new Error("invalid-password");
  }

  await assertManagedMemberLoginAvailable(email);

  const snapshot = await getSessionSnapshotByEmail(email);

  if (!snapshot) {
    throw new Error("user-load-failed");
  }

  return snapshot;
}

export async function registerUser(input: { email: string; companyName: string; password: string }) {
  const email = input.email.trim().toLowerCase();
  const companyName = input.companyName.trim();
  const password = input.password.trim();

  if (!email || !companyName || !password) {
    throw new Error("required");
  }

  if (!isValidEmailAddress(email)) {
    throw new Error("invalid-email");
  }

  if (companyName.length > MAX_COMPANY_NAME_LENGTH) {
    throw new Error("company-name-too-long");
  }

  const existingUser = await getUserByEmail(email);

  if (existingUser) {
    throw new Error("email-exists");
  }

  await withTransaction(async (connection) => {
    await connection.query(
      `
        INSERT INTO users (
          email,
          company_name,
          display_name,
          password_hash,
          mail_configured
        ) VALUES (?, ?, ?, ?, 0)
      `,
      [email, companyName, deriveDisplayName(companyName, email), hashPassword(password)]
    );
  });

  const snapshot = await getSessionSnapshotByEmail(email);

  if (!snapshot) {
    throw new Error("user-load-failed");
  }

  return snapshot;
}

async function getCleanedDomainSignupRecord(domain: string) {
  const [rows] = await getDbPool().query<CleanedDomainSignupRow[]>(
    `
      SELECT
        d.id AS domain_id,
        d.domain,
        d.user_id AS owner_user_id,
        u.email AS owner_email,
        u.recovery_email AS owner_recovery_email,
        m.id AS mailbox_id,
        m.email AS mailbox_email
      FROM domains d
      INNER JOIN users u ON u.id = d.user_id
      INNER JOIN mailboxes m
        ON m.domain_id = d.id
       AND m.user_id = d.user_id
      WHERE d.domain = ?
        AND d.mailcow_cleanup_at IS NOT NULL
      ORDER BY m.id ASC
      LIMIT 1
    `,
    [domain]
  );

  return rows[0] ?? null;
}

async function restoreCleanedDomainMembers(
  pending: Pick<PendingSignupRegistration, "domain" | "domainId" | "restoredFromCleanup">
) {
  if (!pending.restoredFromCleanup) {
    return;
  }

  const [memberRows] = await getDbPool().query<RestorableManagedMemberRow[]>(
    `
      SELECT
        display_name,
        email,
        local_part,
        password_ciphertext
      FROM managed_team_mailboxes
      WHERE domain_id = ?
        AND status = 'active'
      ORDER BY created_at ASC, id ASC
    `,
    [pending.domainId]
  );

  for (const member of memberRows) {
    await ensureMailcowMailboxAccount({
      displayName: member.display_name,
      domain: pending.domain,
      localPart: member.local_part,
      mailboxPassword: decryptMailboxPassword(member.password_ciphertext)
    });
  }

  await withTransaction(async (connection) => {
    await connection.query(
      `
        UPDATE domains
        SET mailcow_cleanup_at = NULL, updated_at = NOW()
        WHERE id = ?
          AND mailcow_cleanup_at IS NOT NULL
      `,
      [pending.domainId]
    );
    await connection.query(
      `
        UPDATE mailboxes m
        INNER JOIN managed_team_mailboxes mtm
          ON LOWER(mtm.email) = LOWER(m.email)
        SET
          m.status = 'active',
          m.last_sync_error = NULL,
          m.updated_at = NOW()
        WHERE m.domain_id = ?
          AND mtm.domain_id = ?
          AND mtm.status = 'active'
      `,
      [pending.domainId, pending.domainId]
    );
    await connection.query(
      `
        UPDATE users u
        INNER JOIN mailboxes m ON m.user_id = u.id
        INNER JOIN managed_team_mailboxes mtm
          ON LOWER(mtm.email) = LOWER(m.email)
        SET u.mail_configured = 1, u.updated_at = NOW()
        WHERE m.domain_id = ?
          AND mtm.domain_id = ?
          AND mtm.status = 'active'
      `,
      [pending.domainId, pending.domainId]
    );
  });
}

async function enqueueMailboxProvisioningRecoveryJob(
  connection: PoolConnection,
  input: { domainId: number; mailboxId: number; ownerUserId: number }
) {
  const processAfterAt = new Date(Date.now() + MAILBOX_PROVISIONING_RECOVERY_GRACE_MS);

  await connection.query(
    `
      INSERT INTO mailbox_provisioning_recovery_jobs (
        owner_user_id,
        domain_id,
        mailbox_id,
        status,
        attempt_count,
        process_after_at,
        processing_started_at,
        completed_at,
        last_error
      ) VALUES (?, ?, ?, 'pending', 0, ?, NULL, NULL, NULL)
      ON DUPLICATE KEY UPDATE
        owner_user_id = VALUES(owner_user_id),
        domain_id = VALUES(domain_id),
        status = 'pending',
        attempt_count = 0,
        process_after_at = VALUES(process_after_at),
        processing_started_at = NULL,
        completed_at = NULL,
        last_error = NULL,
        updated_at = NOW()
    `,
    [input.ownerUserId, input.domainId, input.mailboxId, processAfterAt]
  );
}

async function completeMailboxProvisioningRecoveryJob(mailboxId: number) {
  await getDbPool().query(
    `
      UPDATE mailbox_provisioning_recovery_jobs
      SET
        status = 'completed',
        processing_started_at = NULL,
        completed_at = NOW(),
        last_error = NULL,
        updated_at = NOW()
      WHERE mailbox_id = ?
    `,
    [mailboxId]
  );
}

export async function expediteMailboxProvisioningRecoveryJob(
  mailboxId: number,
  error: unknown,
) {
  await ensureOfficialMailSchema();
  const normalizedMailboxId = Number(mailboxId);

  if (!Number.isInteger(normalizedMailboxId) || normalizedMailboxId <= 0) {
    throw new Error("mailbox-provisioning-data-missing");
  }

  await getDbPool().query(
    `
      UPDATE mailbox_provisioning_recovery_jobs
      SET
        status = 'pending',
        process_after_at = NOW(),
        processing_started_at = NULL,
        last_error = ?,
        updated_at = NOW()
      WHERE mailbox_id = ?
        AND status <> 'completed'
    `,
    [normalizeMailboxProvisioningRecoveryError(error), normalizedMailboxId],
  );
}

export async function createPendingSignupRegistration(input: {
  companyName: string;
  domain: string;
  localPart: string;
  password: string;
  recoveryEmail: string;
  recoveryVerificationId: number;
  recoveryVerificationToken: string;
  termsAccepted: boolean;
}) {
  await ensureOfficialMailSchema();
  const normalized = normalizeSignupRegistrationInput(input);
  await assertMailcowPasswordPolicy(normalized.password);
  const mailboxPassword = createInternalMailboxCredentialPassword();
  const cleanedDomain = await getCleanedDomainSignupRecord(normalized.domain);
  const existingUser = await getUserByEmail(normalized.mailboxEmail);

  if (cleanedDomain && cleanedDomain.owner_email !== normalized.mailboxEmail) {
    throw new Error("domain-recovery-owner-mismatch");
  }

  if (existingUser && !cleanedDomain) {
    throw new Error("email-exists");
  }

  const pool = getDbPool();
  const [ownerRows] = await pool.query<(RowDataPacket & { user_id: number | null })[]>(
    "SELECT user_id FROM domains WHERE domain = ? LIMIT 1",
    [normalized.domain]
  );

  if (ownerRows[0]?.user_id && !cleanedDomain) {
    throw new Error("domain-taken");
  }

  const displayName = deriveDisplayName(normalized.companyName, normalized.mailboxEmail);
  let userId = 0;
  let domainId = 0;
  let mailboxId = 0;
  let restoredFromCleanup = false;

  await withTransaction(async (connection) => {
    const verification = await getSignupRecoveryEmailVerificationById(normalized.recoveryVerificationId, connection);

    if (
      !verification ||
      verification.email !== normalized.recoveryEmail ||
      verification.consumed_at ||
      !verification.verified_at ||
      !verification.verification_token_hash
    ) {
      throw new Error("recovery-email-unverified");
    }

    if (verification.expires_at.getTime() <= Date.now()) {
      throw new Error("recovery-email-expired");
    }

    if (verification.verification_token_hash !== createSha256Hex(normalized.recoveryVerificationToken)) {
      throw new Error("recovery-email-unverified");
    }

    const [lockedCleanedDomainRows] = await connection.query<CleanedDomainSignupRow[]>(
      `
        SELECT
          d.id AS domain_id,
          d.domain,
          d.user_id AS owner_user_id,
          u.email AS owner_email,
          u.recovery_email AS owner_recovery_email,
          m.id AS mailbox_id,
          m.email AS mailbox_email
        FROM domains d
        INNER JOIN users u ON u.id = d.user_id
        INNER JOIN mailboxes m
          ON m.domain_id = d.id
         AND m.user_id = d.user_id
        WHERE d.domain = ?
          AND d.mailcow_cleanup_at IS NOT NULL
        ORDER BY m.id ASC
        LIMIT 1
        FOR UPDATE
      `,
      [normalized.domain]
    );
    const lockedCleanedDomain = lockedCleanedDomainRows[0];

    if (lockedCleanedDomain) {
      if (lockedCleanedDomain.owner_email !== normalized.mailboxEmail) {
        throw new Error("domain-recovery-owner-mismatch");
      }

      if (lockedCleanedDomain.owner_recovery_email !== normalized.recoveryEmail) {
        throw new Error("domain-recovery-verification-required");
      }

      userId = lockedCleanedDomain.owner_user_id;
      domainId = lockedCleanedDomain.domain_id;
      mailboxId = lockedCleanedDomain.mailbox_id;
      restoredFromCleanup = true;

      await connection.query(
        `
          UPDATE users
          SET
            company_name = ?,
            display_name = ?,
            recovery_email = ?,
            recovery_email_verified_at = ?,
            password_hash = ?,
            mail_configured = 0,
            updated_at = NOW()
          WHERE id = ?
        `,
        [
          normalized.companyName,
          displayName,
          normalized.recoveryEmail,
          verification.verified_at,
          hashPassword(normalized.password),
          userId
        ]
      );
      await connection.query(
        `
          UPDATE domains
          SET
            status = 'pending',
            verified_at = NULL,
            dkim_selector = ?,
            dkim_public_key = '',
            dkim_private_key = '',
            updated_at = NOW()
          WHERE id = ?
        `,
        [process.env.DKIM_SELECTOR?.trim() || "dkim", domainId]
      );
      await connection.query(
        `
          UPDATE mailboxes
          SET
            local_part = ?,
            email = ?,
            password_ciphertext = ?,
            password_updated_at = NOW(),
            external_access_enabled = 0,
            last_sync_at = NULL,
            last_sync_error = NULL,
            status = 'pending_dns',
            updated_at = NOW()
          WHERE id = ?
            AND user_id = ?
            AND domain_id = ?
        `,
        [
          normalized.localPart,
          normalized.mailboxEmail,
          encryptMailboxPassword(mailboxPassword),
          mailboxId,
          userId,
          domainId
        ]
      );
      await syncDnsRecords(connection, domainId, buildPendingDnsTemplate());
      await ensureLocalMailboxFolders(connection, mailboxId);
    } else {
      const [duplicateUsers] = await connection.query<(RowDataPacket & { id: number })[]>(
        "SELECT id FROM users WHERE email = ? LIMIT 1",
        [normalized.mailboxEmail]
      );

      if (duplicateUsers[0]?.id) {
        throw new Error("email-exists");
      }

      const [duplicateDomains] = await connection.query<(RowDataPacket & { user_id: number | null })[]>(
        "SELECT user_id FROM domains WHERE domain = ? LIMIT 1",
        [normalized.domain]
      );

      if (duplicateDomains[0]?.user_id) {
        throw new Error("domain-taken");
      }

      const [userResult] = await connection.query(
        `
        INSERT INTO users (
          email,
          company_name,
          display_name,
          recovery_email,
          recovery_email_verified_at,
          password_hash,
          mail_configured
        ) VALUES (?, ?, ?, ?, ?, ?, 0)
      `,
        [
          normalized.mailboxEmail,
          normalized.companyName,
          displayName,
          normalized.recoveryEmail,
          verification.verified_at,
          hashPassword(normalized.password)
        ]
      );

      userId = Number((userResult as { insertId: number }).insertId);

      const [domainResult] = await connection.query(
        `
        INSERT INTO domains (
          user_id,
          domain,
          status,
          dkim_selector,
          dkim_public_key,
          dkim_private_key
        ) VALUES (?, ?, 'pending', ?, '', '')
      `,
        [userId, normalized.domain, process.env.DKIM_SELECTOR?.trim() || "dkim"]
      );

      domainId = Number((domainResult as { insertId: number }).insertId);

      const [mailboxResult] = await connection.query(
        `
        INSERT INTO mailboxes (
          user_id,
          domain_id,
          local_part,
          email,
          password_ciphertext,
          password_updated_at,
          external_access_enabled,
          last_sync_at,
          last_sync_error,
          status
        ) VALUES (?, ?, ?, ?, ?, NOW(), 0, NULL, NULL, 'pending_dns')
      `,
        [userId, domainId, normalized.localPart, normalized.mailboxEmail, encryptMailboxPassword(mailboxPassword)]
      );

      mailboxId = Number((mailboxResult as { insertId: number }).insertId);

      await syncDnsRecords(connection, domainId, buildPendingDnsTemplate());
      await ensureLocalMailboxFolders(connection, mailboxId);
    }
    await connection.query(
      `
        INSERT INTO user_legal_consents (
          user_id,
          terms_version,
          privacy_policy_version,
          consent_source,
          accepted_at
        ) VALUES (?, ?, ?, 'signup', NOW(3))
        ON DUPLICATE KEY UPDATE
          consent_source = VALUES(consent_source),
          accepted_at = VALUES(accepted_at)
      `,
      [userId, TERMS_VERSION, PRIVACY_POLICY_VERSION]
    );
    await enqueueMailboxProvisioningRecoveryJob(connection, {
      domainId,
      mailboxId,
      ownerUserId: userId
    });
    await connection.query(
      `
        UPDATE signup_recovery_email_verifications
        SET consumed_at = NOW(), updated_at = NOW()
        WHERE id = ?
      `,
      [verification.id]
    );
  });

  const snapshot = await getSessionSnapshotByEmail(normalized.mailboxEmail);

  if (!snapshot) {
    throw new Error("snapshot-load-failed");
  }

  return {
    companyName: normalized.companyName,
    displayName,
    domain: normalized.domain,
    domainId,
    localPart: normalized.localPart,
    mailboxEmail: normalized.mailboxEmail,
    mailboxId,
    mailboxPassword,
    restoredFromCleanup,
    snapshot,
    userId
  } satisfies PendingSignupRegistration;
}

export async function finalizePendingSignupRegistration(
  pending: PendingSignupRegistration,
  options?: {
    onProgress?: (progress: SignupProvisioningProgress) => void | Promise<void>;
  }
) {
  const onProgress = options?.onProgress;

  await emitSignupProvisioningProgress(onProgress, {
    step: "domain",
    status: "running",
    title: "도메인 등록 중",
    description: `${pending.domain} 도메인을 메일 서버에 등록하고 있습니다.`
  });
  await ensureMailcowDomain({
    allowExistingDomain: pending.restoredFromCleanup,
    domain: pending.domain,
    mailboxEmail: pending.mailboxEmail
  });
  await emitSignupProvisioningProgress(onProgress, {
    step: "domain",
    status: "complete",
    title: "도메인 등록 완료",
    description: `${pending.domain} 도메인 등록이 완료되었습니다.`
  });

  await emitSignupProvisioningProgress(onProgress, {
    step: "mailbox",
    status: "running",
    title: "대표 메일 생성 중",
    description: `${pending.mailboxEmail} 계정을 메일 서버에 생성하고 있습니다.`
  });
  await ensureMailcowMailbox({
    displayName: pending.displayName,
    domain: pending.domain,
    localPart: pending.localPart,
    mailboxPassword: pending.mailboxPassword
  });
  await synchronizeMailboxExternalAccessByEmail(pending.mailboxEmail);
  await emitSignupProvisioningProgress(onProgress, {
    step: "mailbox",
    status: "complete",
    title: "대표 메일 생성 완료",
    description: `${pending.mailboxEmail} 계정이 준비되었습니다.`
  });

  await emitSignupProvisioningProgress(onProgress, {
    step: "dkim",
    status: "running",
    title: "DNS 보안값 준비 중",
    description: "DKIM 공개키와 연결용 DNS 값을 정리하고 있습니다."
  });
  const dkimProvisioning = await ensureMailcowDkimRecord(pending.domain);
  await withTransaction(async (connection) => {
    await connection.query(
      `
        UPDATE domains
        SET
          dkim_selector = ?,
          dkim_public_key = ?,
          dkim_private_key = '',
          updated_at = NOW()
        WHERE id = ?
      `,
      [dkimProvisioning.dkimSelector, dkimProvisioning.dkimPublicKey, pending.domainId]
    );
    await syncDnsRecords(
      connection,
      pending.domainId,
      buildDnsTemplate(dkimProvisioning.dkimSelector, dkimProvisioning.dkimPublicKey)
    );
  });
  await restoreCleanedDomainMembers(pending);
  await completeMailboxProvisioningRecoveryJob(pending.mailboxId);
  await emitSignupProvisioningProgress(onProgress, {
    step: "dkim",
    status: "complete",
    title: "DNS 보안값 준비 완료",
    description: "MX, SPF, DKIM, DMARC 표가 모두 준비되었습니다."
  });

  await emitSignupProvisioningProgress(onProgress, {
    step: "welcome",
    status: "running",
    title: "초기 메일함 준비 중",
    description: "환영 메일과 기본 받은메일함을 마지막으로 정리하고 있습니다."
  });
  try {
    const welcomeSent = await sendSystemWelcomeMail({
      companyName: pending.companyName,
      mailboxEmail: pending.mailboxEmail,
      mailboxId: pending.mailboxId
    });

    if (welcomeSent) {
      await refreshMailboxByEmail(pending.mailboxEmail, { folder: "inbox" });
    }
  } catch (error) {
    console.error("System welcome mail send failed", error);
  }
  await emitSignupProvisioningProgress(onProgress, {
    step: "welcome",
    status: "complete",
    title: "초기 메일함 준비 완료",
    description: "가입 직후 확인할 수 있는 기본 메일함 상태까지 정리했습니다."
  });

  const snapshot = await getSessionSnapshotByUserId(pending.userId);

  if (!snapshot) {
    throw new Error("snapshot-load-failed");
  }

  return snapshot;
}

async function claimMailboxProvisioningRecoveryJobs(limit: number) {
  await ensureOfficialMailSchema();

  return withTransaction(async (connection) => {
    await connection.query(
      `
        UPDATE mailbox_provisioning_recovery_jobs
        SET
          status = 'pending',
          process_after_at = NOW(),
          processing_started_at = NULL,
          updated_at = NOW()
        WHERE status = 'processing'
          AND processing_started_at < DATE_SUB(
            NOW(),
            INTERVAL ${MAILBOX_PROVISIONING_RECOVERY_STALE_MINUTES} MINUTE
          )
      `
    );

    const [rows] = await connection.query<MailboxProvisioningRecoveryJobRow[]>(
      `
        SELECT
          j.id AS job_id,
          j.attempt_count,
          d.id AS domain_id,
          d.domain,
          d.mailcow_cleanup_at,
          u.company_name,
          u.display_name,
          u.id AS owner_user_id,
          m.id AS mailbox_id,
          m.email AS mailbox_email,
          m.local_part,
          m.password_ciphertext
        FROM mailbox_provisioning_recovery_jobs j
        INNER JOIN users u ON u.id = j.owner_user_id
        INNER JOIN domains d ON d.id = j.domain_id
        INNER JOIN mailboxes m ON m.id = j.mailbox_id
        WHERE j.status = 'pending'
          AND j.process_after_at <= NOW()
        ORDER BY j.process_after_at ASC, j.id ASC
        LIMIT ${limit}
        FOR UPDATE
      `
    );

    if (rows.length === 0) {
      return [];
    }

    const placeholders = rows.map(() => "?").join(", ");
    await connection.query(
      `
        UPDATE mailbox_provisioning_recovery_jobs
        SET
          status = 'processing',
          attempt_count = attempt_count + 1,
          processing_started_at = NOW(),
          updated_at = NOW()
        WHERE id IN (${placeholders})
          AND status = 'pending'
      `,
      rows.map((row) => row.job_id)
    );

    return rows;
  });
}

function normalizeMailboxProvisioningRecoveryError(error: unknown) {
  const message = error instanceof Error ? error.message : "unknown";
  return message.replace(/[\r\n\t]+/g, " ").slice(0, 500);
}

async function rescheduleMailboxProvisioningRecoveryJob(job: MailboxProvisioningRecoveryJobRow, error: unknown) {
  const errorMessage = normalizeMailboxProvisioningRecoveryError(error);
  const terminal =
    errorMessage === "mailbox-provisioning-domain-invalid" || errorMessage === "mailbox-provisioning-data-missing";
  const attemptNumber = Math.max(1, Number(job.attempt_count) + 1);
  const retryMinutes = Math.min(60, 2 ** Math.min(6, attemptNumber - 1));
  const processAfterAt = new Date(Date.now() + retryMinutes * 60 * 1000);

  await getDbPool().query(
    `
      UPDATE mailbox_provisioning_recovery_jobs
      SET
        status = ?,
        process_after_at = ?,
        processing_started_at = NULL,
        last_error = ?,
        updated_at = NOW()
      WHERE id = ?
    `,
    [terminal ? "failed" : "pending", processAfterAt, errorMessage, job.job_id]
  );

  return errorMessage;
}

async function repairMailboxProvisioningRecoveryJob(job: MailboxProvisioningRecoveryJobRow) {
  if (!isValidDomainInput(job.domain)) {
    throw new Error("mailbox-provisioning-domain-invalid");
  }

  if (!job.local_part || !job.mailbox_email || !job.password_ciphertext) {
    throw new Error("mailbox-provisioning-data-missing");
  }

  const savedMailboxPassword = decryptMailboxPassword(job.password_ciphertext);

  try {
    await ensureMailcowMailboxAccount({
      displayName: job.display_name,
      domain: job.domain,
      localPart: job.local_part,
      mailboxPassword: savedMailboxPassword
    });
  } catch (error) {
    if (!isMailcowPasswordComplexityError(error)) {
      throw error;
    }

    const repairPassword = createMailcowRepairPassword();

    await ensureMailcowMailboxAccount({
      displayName: job.display_name,
      domain: job.domain,
      localPart: job.local_part,
      mailboxPassword: repairPassword
    });
    await getDbPool().query(
      `
        UPDATE mailboxes
        SET
          password_ciphertext = ?,
          password_updated_at = NOW(),
          last_sync_error = NULL,
          updated_at = NOW()
        WHERE id = ?
      `,
      [encryptMailboxPassword(repairPassword), job.mailbox_id]
    );
  }

  const dkimProvisioning = await ensureMailcowDkimRecord(job.domain);
  await withTransaction(async (connection) => {
    await connection.query(
      `
        UPDATE domains
        SET
          dkim_selector = ?,
          dkim_public_key = ?,
          dkim_private_key = '',
          updated_at = NOW()
        WHERE id = ?
      `,
      [dkimProvisioning.dkimSelector, dkimProvisioning.dkimPublicKey, job.domain_id]
    );
    await syncDnsRecords(
      connection,
      job.domain_id,
      buildDnsTemplate(dkimProvisioning.dkimSelector, dkimProvisioning.dkimPublicKey)
    );
  });
  await restoreCleanedDomainMembers({
    domain: job.domain,
    domainId: job.domain_id,
    restoredFromCleanup: Boolean(job.mailcow_cleanup_at)
  });

  const welcomeSent = await sendSystemWelcomeMail({
    companyName: job.company_name,
    mailboxEmail: job.mailbox_email,
    mailboxId: job.mailbox_id
  });

  if (welcomeSent) {
    try {
      await refreshMailboxByEmail(job.mailbox_email, { folder: "inbox" });
    } catch (error) {
      // SMTP already accepted the welcome message. Do not retry the whole
      // recovery job and risk sending a duplicate only because sync lagged.
      console.error("Recovered mailbox welcome sync failed", error);
    }
  }

  await completeMailboxProvisioningRecoveryJob(job.mailbox_id);
  await recordMarketingLifecycleEvent({
    domainId: job.domain_id,
    eventType: "signup_completed",
    userId: job.owner_user_id
  });
}

export async function reconcilePendingMailboxProvisioningJobs(options?: { limit?: number }) {
  const requestedLimit = Number(options?.limit ?? 5);
  const limit = Math.max(
    1,
    Math.min(
      MAILBOX_PROVISIONING_RECOVERY_MAX_BATCH_SIZE,
      Number.isFinite(requestedLimit) ? Math.floor(requestedLimit) : 5
    )
  );
  const jobs = await claimMailboxProvisioningRecoveryJobs(limit);
  const repaired: string[] = [];
  const failed: Array<{ mailbox: string; error: string }> = [];

  for (const job of jobs) {
    try {
      await repairMailboxProvisioningRecoveryJob(job);
      repaired.push(job.mailbox_email);
    } catch (error) {
      failed.push({
        mailbox: job.mailbox_email,
        error: await rescheduleMailboxProvisioningRecoveryJob(job, error)
      });
    }
  }

  return {
    candidateCount: jobs.length,
    checkedCount: jobs.length,
    failed,
    repaired
  };
}

export async function registerUserWithMailboxSetup(input: {
  companyName: string;
  domain: string;
  localPart: string;
  password: string;
  recoveryEmail: string;
  recoveryVerificationId: number;
  recoveryVerificationToken: string;
  termsAccepted: boolean;
}) {
  const pending = await createPendingSignupRegistration(input);
  return finalizePendingSignupRegistration(pending);
}

export async function sendSignupRecoveryEmailVerification(input: {
  challengeId?: number | null;
  recoveryEmail: string;
}) {
  await ensureOfficialMailSchema();
  const recoveryEmail = normalizeEmailAddress(input.recoveryEmail);

  if (!recoveryEmail) {
    throw new Error("recovery-email-required");
  }

  if (!isValidEmailAddress(recoveryEmail)) {
    throw new Error("recovery-email-invalid");
  }

  const now = Date.now();
  const requestedChallengeId = Number(input.challengeId);
  let verification =
    Number.isInteger(requestedChallengeId) && requestedChallengeId > 0
      ? await getSignupRecoveryEmailVerificationById(requestedChallengeId)
      : null;

  if (verification && verification.email !== recoveryEmail) {
    verification = null;
  }

  if (!verification) {
    verification = await getLatestSignupRecoveryEmailVerificationByEmail(recoveryEmail);
  }

  if (verification && verification.resend_available_at.getTime() > now) {
    const secondsRemaining = Math.max(1, Math.ceil((verification.resend_available_at.getTime() - now) / 1000));
    throw new Error(`recovery-email-resend-cooldown:${secondsRemaining}`);
  }

  const code = createSixDigitVerificationCode();
  const expiresAt = new Date(now + SIGNUP_RECOVERY_EMAIL_CODE_TTL_MS);
  const resendAvailableAt = new Date(now + SIGNUP_RECOVERY_EMAIL_RESEND_COOLDOWN_MS);
  const codeHash = hashPassword(code);
  let verificationId = verification?.id ?? 0;

  if (verification && !verification.consumed_at) {
    await getDbPool().query(
      `
        UPDATE signup_recovery_email_verifications
        SET
          code_hash = ?,
          verification_token_hash = NULL,
          expires_at = ?,
          resend_available_at = ?,
          verified_at = NULL,
          updated_at = NOW()
        WHERE id = ?
      `,
      [codeHash, expiresAt, resendAvailableAt, verification.id]
    );
    verificationId = verification.id;
  } else {
    const [result] = await getDbPool().query(
      `
        INSERT INTO signup_recovery_email_verifications (
          email,
          code_hash,
          verification_token_hash,
          expires_at,
          resend_available_at,
          verified_at,
          consumed_at
        ) VALUES (?, ?, NULL, ?, ?, NULL, NULL)
      `,
      [recoveryEmail, codeHash, expiresAt, resendAvailableAt]
    );
    verificationId = Number((result as { insertId: number }).insertId);
  }

  const systemMailbox = getSystemNoReplyMailboxConfig();

  if (!systemMailbox) {
    throw new Error("system-mailbox-missing");
  }

  try {
    await sendSystemMailboxMessage(systemMailbox, {
      bodyText: buildSignupRecoveryEmailBody(code),
      recipients: [recoveryEmail],
      subject: SIGNUP_RECOVERY_EMAIL_SUBJECT
    });
  } catch (error) {
    console.error("Signup recovery email send failed", error);
    throw new Error("recovery-email-send-failed");
  }

  return {
    challengeId: verificationId,
    expiresAt: expiresAt.toISOString(),
    resendAvailableAt: resendAvailableAt.toISOString()
  };
}

export async function verifySignupRecoveryEmailCode(input: {
  challengeId: number;
  code: string;
  recoveryEmail: string;
}) {
  await ensureOfficialMailSchema();
  const challengeId = Number(input.challengeId);
  const code = input.code.trim();
  const recoveryEmail = normalizeEmailAddress(input.recoveryEmail);

  if (!challengeId || !code || !recoveryEmail) {
    throw new Error("required");
  }

  const verification = await getSignupRecoveryEmailVerificationById(challengeId);

  if (!verification || verification.email !== recoveryEmail || verification.consumed_at) {
    throw new Error("not-found");
  }

  if (verification.expires_at.getTime() <= Date.now()) {
    throw new Error("expired");
  }

  if (!verifyPassword(code, verification.code_hash)) {
    throw new Error("invalid-code");
  }

  const verificationToken = randomBytes(24).toString("base64url");
  await getDbPool().query(
    `
      UPDATE signup_recovery_email_verifications
      SET
        verification_token_hash = ?,
        verified_at = NOW(),
        updated_at = NOW()
      WHERE id = ?
    `,
    [createSha256Hex(verificationToken), verification.id]
  );

  return {
    challengeId: verification.id,
    expiresAt: verification.expires_at.toISOString(),
    verificationToken
  };
}

export async function requestPasswordResetLink(input: { companyName: string; email: string; mailAppUrl: string }) {
  await ensureOfficialMailSchema();
  const email = normalizeEmailAddress(input.email);
  const companyName = input.companyName.trim();
  const mailAppUrl = input.mailAppUrl.trim();

  if (!email || !companyName || !mailAppUrl) {
    throw new Error("required");
  }

  const user = await getUserByEmail(email);

  if (!user || user.company_name.trim() !== companyName) {
    throw new Error("not-found");
  }

  const recoveryEmail = normalizeEmailAddress(user.recovery_email ?? "");

  if (!recoveryEmail || !user.recovery_email_verified_at || !isValidEmailAddress(recoveryEmail)) {
    throw new Error("recovery-email-missing");
  }

  const token = randomBytes(32).toString("base64url");
  const tokenHash = createSha256Hex(token);
  const expiresAt = new Date(Date.now() + PASSWORD_RESET_TOKEN_TTL_MS);

  await withTransaction(async (connection) => {
    await connection.query(
      `
        UPDATE password_reset_tokens
        SET used_at = COALESCE(used_at, NOW()), updated_at = NOW()
        WHERE user_id = ? AND used_at IS NULL
      `,
      [user.id]
    );
    await connection.query(
      `
        INSERT INTO password_reset_tokens (
          user_id,
          recovery_email,
          token_hash,
          expires_at
        ) VALUES (?, ?, ?, ?)
      `,
      [user.id, recoveryEmail, tokenHash, expiresAt]
    );
  });

  const systemMailbox = getSystemNoReplyMailboxConfig();

  if (!systemMailbox) {
    throw new Error("system-mailbox-missing");
  }

  const resetUrl = new URL(
    `/reset-password/${encodeURIComponent(token)}`,
    `${mailAppUrl.replace(/\/$/, "")}/`
  ).toString();

  try {
    await sendSystemMailboxMessage(systemMailbox, {
      bodyText: buildPasswordResetLinkBody({
        companyName,
        resetUrl
      }),
      recipients: [recoveryEmail],
      subject: PASSWORD_RESET_LINK_SUBJECT
    });
  } catch (error) {
    console.error("Password reset email send failed", error);
    throw new Error("password-reset-send-failed");
  }

  return {
    maskedRecoveryEmail: maskEmailAddress(recoveryEmail)
  };
}

export type PasswordResetTokenStatus = "expired" | "invalid" | "used" | "valid";

export async function getPasswordResetTokenStatus(token: string) {
  await ensureOfficialMailSchema();
  const normalizedToken = token.trim();

  if (!normalizedToken) {
    return { status: "invalid" as const };
  }

  const resetToken = await getPasswordResetTokenByHash(createSha256Hex(normalizedToken));

  if (!resetToken) {
    return { status: "invalid" as const };
  }

  if (resetToken.used_at) {
    return { status: "used" as const };
  }

  if (resetToken.expires_at.getTime() <= Date.now()) {
    return { status: "expired" as const };
  }

  return { status: "valid" as const };
}

export async function resetPasswordByToken(input: { password: string; token: string }) {
  await ensureOfficialMailSchema();
  const password = input.password.trim();
  const token = input.token.trim();

  if (!password || !token) {
    throw new Error("required");
  }

  const resetToken = await getPasswordResetTokenByHash(createSha256Hex(token));

  if (!resetToken) {
    throw new Error("invalid-token");
  }

  if (resetToken.used_at) {
    throw new Error("used-token");
  }

  if (resetToken.expires_at.getTime() <= Date.now()) {
    throw new Error("expired-token");
  }

  const snapshot = await setRepresentativePasswordByUserId(resetToken.user_id, password);
  await getDbPool().query(
    `
      UPDATE password_reset_tokens
      SET used_at = NOW(), updated_at = NOW()
      WHERE id = ?
    `,
    [resetToken.id]
  );

  return snapshot;
}

export async function requestFindIdEmail(input: { recoveryEmail: string; loginUrl: string }) {
  const recoveryEmail = normalizeEmailAddress(input.recoveryEmail);
  const loginUrl = input.loginUrl.trim();

  if (!recoveryEmail || !loginUrl) {
    throw new Error("required");
  }

  if (!isValidEmailAddress(recoveryEmail)) {
    throw new Error("invalid-email");
  }

  await ensureOfficialMailSchema();
  const [rows] = await getDbPool().query<(RowDataPacket & { email: string })[]>(
    `
      SELECT DISTINCT email
      FROM users
      WHERE LOWER(recovery_email) = ?
        AND recovery_email_verified_at IS NOT NULL
        AND account_deleted_at IS NULL
      ORDER BY email ASC
      LIMIT 50
    `,
    [recoveryEmail]
  );
  const accountEmails = rows.map((row) => normalizeEmailAddress(row.email)).filter(isValidEmailAddress);

  if (accountEmails.length === 0) {
    throw new Error("not-found");
  }

  await withTransaction(async (connection) => {
    await connection.query(
      `
        INSERT IGNORE INTO find_id_email_request_limits (
          recovery_email,
          window_started_at,
          request_count
        ) VALUES (?, NOW(3), 0)
      `,
      [recoveryEmail]
    );

    const [rateLimitRows] = await connection.query<
      (RowDataPacket & {
        request_count: number;
        seconds_remaining: number;
        window_expired: number;
      })[]
    >(
      `
        SELECT
          request_count,
          window_started_at <= DATE_SUB(NOW(3), INTERVAL 5 SECOND) AS window_expired,
          GREATEST(
            1,
            CEIL(
              TIMESTAMPDIFF(
                MICROSECOND,
                NOW(3),
                DATE_ADD(window_started_at, INTERVAL 5 SECOND)
              ) / 1000000
            )
          ) AS seconds_remaining
        FROM find_id_email_request_limits
        WHERE recovery_email = ?
        FOR UPDATE
      `,
      [recoveryEmail]
    );
    const rateLimit = rateLimitRows[0];

    if (!rateLimit || Number(rateLimit.window_expired) === 1) {
      await connection.query(
        `
          UPDATE find_id_email_request_limits
          SET window_started_at = NOW(3), request_count = 1, updated_at = NOW(3)
          WHERE recovery_email = ?
        `,
        [recoveryEmail]
      );
      return;
    }

    if (Number(rateLimit.request_count) >= 2) {
      const secondsRemaining = Math.max(1, Math.ceil(Number(rateLimit.seconds_remaining) || 1));
      throw new Error(`find-id-rate-limit:${secondsRemaining}`);
    }

    await connection.query(
      `
        UPDATE find_id_email_request_limits
        SET request_count = request_count + 1, updated_at = NOW(3)
        WHERE recovery_email = ?
      `,
      [recoveryEmail]
    );
  });

  const systemMailbox = getSystemNoReplyMailboxConfig();

  if (!systemMailbox) {
    throw new Error("system-mailbox-missing");
  }

  const email = renderFindIdEmail({ accountEmails, loginUrl });

  after(async () => {
    try {
      await sendSystemMailboxMessage(systemMailbox, {
        bodyHtml: email.bodyHtml,
        bodyText: email.bodyText,
        recipients: [recoveryEmail],
        subject: email.subject
      });
    } catch (error) {
      console.error("Find ID email send failed", error);
    }
  });

  return {
    accountCount: accountEmails.length,
    maskedRecoveryEmail: maskEmailAddress(recoveryEmail)
  };
}

export async function resetUserPassword(input: { email: string; companyName: string; password: string }) {
  const email = input.email.trim().toLowerCase();
  const companyName = input.companyName.trim();
  const password = input.password.trim();

  if (!email || !companyName || !password) {
    throw new Error("required");
  }

  const existingUser = await getUserByEmail(email);

  if (!existingUser) {
    throw new Error("not-found");
  }

  if (existingUser.company_name.trim() !== companyName) {
    throw new Error("not-found");
  }

  return setRepresentativePasswordByUserId(existingUser.id, password);
}

async function setRepresentativePasswordByUserId(userId: number, nextPassword: string) {
  const normalizedPassword = nextPassword.trim();

  if (!Number.isInteger(userId) || userId <= 0 || !normalizedPassword) {
    throw new Error("required");
  }

  const user = await getUserById(userId);

  if (!user) {
    throw new Error("user-not-found");
  }

  const mailbox = await getLatestMailboxByUserId(user.id);
  if (mailbox) {
    const [rows] = await getDbPool().query<
      (RowDataPacket & {
        external_access_enabled: number | null;
        external_app_password_ciphertext: string | null;
        legacy_app_compat_enabled: number;
        pop3_access_enabled: number;
      })[]
    >(
      "SELECT external_access_enabled, external_app_password_ciphertext, legacy_app_compat_enabled, pop3_access_enabled FROM mailboxes WHERE id = ?",
      [mailbox.id],
    );
    // Legacy external access used the mailbox's primary credential. Revoke it
    // when changing the account password so the old account password cannot
    // remain valid at the IMAP endpoint.
    if (rows[0]?.legacy_app_compat_enabled === 1) {
      await disableLegacyNativeAppCompatibility(mailbox.email);
    }
    const externalPasswordConflicts = rows[0]?.external_app_password_ciphertext &&
      decryptMailboxPassword(rows[0].external_app_password_ciphertext) === normalizedPassword;
    if (externalPasswordConflicts ||
        (rows[0]?.external_access_enabled === 1 && !rows[0]?.external_app_password_ciphertext)) {
      await disableMailboxExternalAccessByEmail(mailbox.email);
    }
    // POP3's protocol-scoped secret is only stored by Mailcow. Revoke it when
    // the account password changes so the new password can never coincide
    // with an old POP3 app password without us being able to detect it.
    if (rows[0]?.pop3_access_enabled === 1) {
      await disableMailboxPop3AccessByEmail(mailbox.email);
    }
    await revokeNativeMailboxAppPassword(mailbox.email);
  }

  await withTransaction(async (connection) => {
    await connection.query(
      `
        UPDATE users
        SET password_hash = ?, updated_at = NOW()
        WHERE id = ?
      `,
      [hashPassword(normalizedPassword), user.id]
    );

    await connection.query(
      "UPDATE mailbox_app_sessions SET revoked_at = COALESCE(revoked_at, NOW()) WHERE user_id = ?",
      [user.id],
    );
  });

  const snapshot = await getSessionSnapshotByUserId(user.id);

  if (!snapshot) {
    throw new Error("snapshot-load-failed");
  }

  return snapshot;
}

export async function updateRepresentativePasswordByEmail(
  email: string,
  currentPassword: string,
  nextPassword: string
) {
  const normalizedEmail = email.trim().toLowerCase();
  const normalizedCurrentPassword = currentPassword.trim();
  const normalizedPassword = nextPassword.trim();

  if (!normalizedEmail || !normalizedCurrentPassword || !normalizedPassword) {
    throw new Error("required");
  }

  const user = await getUserByEmail(normalizedEmail);

  if (!user) {
    throw new Error("user-not-found");
  }

  if (!verifyPassword(normalizedCurrentPassword, user.password_hash)) {
    throw new Error("current-password-invalid");
  }

  return setRepresentativePasswordByUserId(user.id, normalizedPassword);
}

export async function updateMailboxDisplayNameByEmail(email: string, nextDisplayName: string) {
  const normalizedEmail = email.trim().toLowerCase();
  const displayName = nextDisplayName.replace(/\s+/g, " ").trim();

  if (!normalizedEmail || !displayName) {
    throw new Error("required");
  }

  if (displayName.length > 191) {
    throw new Error("display-name-too-long");
  }

  const user = await getUserByEmail(normalizedEmail);

  if (!user) {
    throw new Error("user-not-found");
  }

  const mailbox = await getLatestMailboxByUserId(user.id);

  if (mailbox?.password_ciphertext) {
    const [localPart, domain] = mailbox.email.split("@");

    if (localPart && domain) {
      await ensureMailcowMailbox({
        displayName,
        domain,
        localPart,
        mailboxPassword: decryptMailboxPassword(mailbox.password_ciphertext)
      });
    }
  }

  await withTransaction(async (connection) => {
    await connection.query(
      `
        UPDATE users
        SET display_name = ?, updated_at = NOW()
        WHERE id = ?
      `,
      [displayName, user.id]
    );

    // Team members are re-synced from this table during domain operations.
    await connection.query(
      `
        UPDATE managed_team_mailboxes
        SET display_name = ?, updated_at = NOW()
        WHERE LOWER(email) = ?
      `,
      [displayName, normalizedEmail]
    );
  });

  const snapshot = await getSessionSnapshotByUserId(user.id);

  if (!snapshot) {
    throw new Error("snapshot-load-failed");
  }

  return snapshot;
}

export async function provisionMailSetup(
  email: string,
  domain: string,
  localPart: string,
  mailboxPassword?: string,
  dkimKeySize: 1024 | 2048 = 2048
) {
  const normalizedEmail = email.trim().toLowerCase();
  const normalizedDomain = domain
    .trim()
    .toLowerCase()
    .replace(/^https?:\/\//, "")
    .replace(/\/$/, "");
  const normalizedLocalPart = normalizeMailboxLocalPart(localPart);
  const requestedMailboxPassword = mailboxPassword?.trim() ?? "";

  if (!normalizedEmail || !normalizedDomain || !normalizedLocalPart) {
    throw new Error("required");
  }

  if (
    normalizedDomain.length > MAX_DOMAIN_LENGTH ||
    `${normalizedLocalPart}@${normalizedDomain}`.length > MAX_EMAIL_ADDRESS_LENGTH
  ) {
    throw new Error("domain-too-long");
  }

  if (!isValidMailboxLocalPart(normalizedLocalPart)) {
    throw new Error("invalid-local-part");
  }

  const user = await getUserByEmail(normalizedEmail);

  if (!user) {
    throw new Error("user-not-found");
  }

  const existingSetup = await getLatestSetupByUserId(user.id);
  const existingMailbox = await getLatestMailboxByUserId(user.id);
  const mailboxEmail = `${normalizedLocalPart}@${normalizedDomain}`;
  const conflictingUser = await getUserByEmail(mailboxEmail);

  if (conflictingUser && conflictingUser.id !== user.id) {
    throw new Error("email-exists");
  }

  let resolvedMailboxPassword = requestedMailboxPassword
    ? requestedMailboxPassword
    : existingMailbox?.password_ciphertext
      ? decryptMailboxPassword(existingMailbox.password_ciphertext).trim()
      : "";

  // A domain reconfiguration must not carry a pre-v2 account password into
  // the new Mailcow mailbox. Web/native-app login uses the user password;
  // the direct server credential is always an independent internal secret.
  if (resolvedMailboxPassword && verifyPassword(resolvedMailboxPassword, user.password_hash)) {
    resolvedMailboxPassword = createInternalMailboxCredentialPassword();
  }

  if (!resolvedMailboxPassword) {
    throw new Error("mailbox-auth-missing");
  }

  const encryptedMailboxPassword = encryptMailboxPassword(resolvedMailboxPassword);
  const pool = getDbPool();
  const [ownerRows] = await pool.query<(RowDataPacket & { user_id: number | null })[]>(
    "SELECT user_id FROM domains WHERE domain = ? LIMIT 1",
    [normalizedDomain]
  );

  if (ownerRows[0]?.user_id && ownerRows[0].user_id !== user.id) {
    throw new Error("domain-taken");
  }

  const mailcowProvisioning = await ensureMailcowProvisioning({
    allowExistingDomain: existingSetup?.domain === normalizedDomain || existingMailbox?.email === mailboxEmail,
    dkimKeySize,
    displayName: user.display_name,
    domain: normalizedDomain,
    localPart: normalizedLocalPart,
    mailboxPassword: resolvedMailboxPassword
  });

  await withTransaction(async (connection) => {
    const [userRows] = await connection.query<UserRow[]>(
      `
        SELECT
          id,
          email,
          company_name,
          display_name,
          password_hash,
          mail_configured,
          mail_sidebar_width,
          mail_list_width,
          mail_list_height,
          mail_view_mode,
          mail_sort_order
        FROM users
        WHERE email = ?
        LIMIT 1
      `,
      [normalizedEmail]
    );

    const user = userRows[0];

    if (!user) {
      throw new Error("user-not-found");
    }

    const [ownerRows] = await connection.query<(RowDataPacket & { user_id: number | null })[]>(
      "SELECT user_id FROM domains WHERE domain = ? LIMIT 1",
      [normalizedDomain]
    );

    if (ownerRows[0]?.user_id && ownerRows[0].user_id !== user.id) {
      throw new Error("domain-taken");
    }

    await connection.query("DELETE FROM domains WHERE user_id = ?", [user.id]);

    const [domainResult] = await connection.query(
      `
        INSERT INTO domains (
          user_id,
          domain,
          status,
          dkim_selector,
          dkim_public_key,
          dkim_private_key
        ) VALUES (?, ?, 'pending', ?, ?, ?)
      `,
      [user.id, normalizedDomain, mailcowProvisioning.dkimSelector, mailcowProvisioning.dkimPublicKey, ""]
    );

    const domainId = Number((domainResult as { insertId: number }).insertId);
    const [mailboxResult] = await connection.query(
      `
        INSERT INTO mailboxes (
          user_id,
          domain_id,
          local_part,
          email,
          password_ciphertext,
          password_updated_at,
          last_sync_at,
          last_sync_error,
          status
        ) VALUES (?, ?, ?, ?, ?, NOW(), NULL, NULL, 'pending_dns')
      `,
      [user.id, domainId, normalizedLocalPart, mailboxEmail, encryptedMailboxPassword]
    );

    const mailboxId = Number((mailboxResult as { insertId: number }).insertId);

    await syncDnsRecords(
      connection,
      domainId,
      buildDnsTemplate(mailcowProvisioning.dkimSelector, mailcowProvisioning.dkimPublicKey)
    );
    await ensureLocalMailboxFolders(connection, mailboxId);

    await connection.query(
      `
        UPDATE users
        SET email = ?, mail_configured = 0, updated_at = NOW()
        WHERE id = ?
      `,
      [mailboxEmail, user.id]
    );
  });

  const snapshot = await getSessionSnapshotByUserId(user.id);

  if (!snapshot) {
    throw new Error("snapshot-load-failed");
  }

  await synchronizeMailboxExternalAccessByEmail(mailboxEmail);

  try {
    const latestUser = await getUserById(user.id);

    if (latestUser) {
      const latestMailbox = await getLatestMailboxByUserId(latestUser.id);

      if (latestMailbox) {
        const welcomeSent = await sendSystemWelcomeMail({
          companyName: user.company_name,
          mailboxEmail,
          mailboxId: latestMailbox.id
        });

        if (welcomeSent) {
          await refreshMailboxByEmail(mailboxEmail, { folder: "inbox" });
        }
      }
    }
  } catch (error) {
    console.error("System welcome mail send failed", error);
  }

  return snapshot;
}

export async function regenerateMailSetupDkim(email: string, dkimKeySize: 1024 | 2048) {
  const normalizedEmail = email.trim().toLowerCase();
  const user = await getUserByEmail(normalizedEmail);

  if (!user) {
    throw new Error("user-not-found");
  }

  const setup = await getLatestSetupByUserId(user.id);

  if (!setup) {
    throw new Error("setup-not-found");
  }

  const dkim = await regenerateMailcowDkim(setup.domain, dkimKeySize);

  await withTransaction(async (connection) => {
    await connection.query(
      `
        UPDATE domains
        SET
          dkim_selector = ?,
          dkim_public_key = ?,
          dkim_private_key = '',
          status = 'pending',
          updated_at = NOW()
        WHERE id = ? AND user_id = ?
      `,
      [dkim.dkimSelector, dkim.dkimPublicKey, setup.domain_id, user.id]
    );
    await connection.query(
      `
        DELETE FROM dns_records
        WHERE domain_id = ?
          AND record_type = 'TXT'
          AND host_name = ?
      `,
      [setup.domain_id, `${setup.dkim_selector}._domainkey`]
    );
    await connection.query(
      `
        INSERT INTO dns_records (
          domain_id,
          record_type,
          host_name,
          value_text,
          priority,
          status
        ) VALUES (?, 'TXT', ?, ?, NULL, 'pending')
      `,
      [setup.domain_id, `${dkim.dkimSelector}._domainkey`, `v=DKIM1; k=rsa; p=${dkim.dkimPublicKey}`]
    );
  });

  const snapshot = await getSessionSnapshotByUserId(user.id);

  if (!snapshot) {
    throw new Error("snapshot-load-failed");
  }

  return snapshot;
}

export async function regenerateMailSetupDkimByDomainId(domainId: number, dkimKeySize: 1024 | 2048) {
  if (!Number.isInteger(domainId) || domainId <= 0) {
    throw new Error("setup-not-found");
  }

  const setup = await getMailSetupByDomainId(domainId);

  if (!setup) {
    throw new Error("setup-not-found");
  }

  const dkim = await regenerateMailcowDkim(setup.domain, dkimKeySize);

  await withTransaction(async (connection) => {
    await connection.query(
      `
        UPDATE domains
        SET
          dkim_selector = ?,
          dkim_public_key = ?,
          dkim_private_key = '',
          status = 'pending',
          updated_at = NOW()
        WHERE id = ?
      `,
      [dkim.dkimSelector, dkim.dkimPublicKey, setup.domain_id]
    );
    await connection.query(
      `
        DELETE FROM dns_records
        WHERE domain_id = ?
          AND record_type = 'TXT'
          AND host_name = ?
      `,
      [setup.domain_id, `${setup.dkim_selector}._domainkey`]
    );
    await connection.query(
      `
        INSERT INTO dns_records (
          domain_id,
          record_type,
          host_name,
          value_text,
          priority,
          status
        ) VALUES (?, 'TXT', ?, ?, NULL, 'pending')
      `,
      [setup.domain_id, `${dkim.dkimSelector}._domainkey`, `v=DKIM1; k=rsa; p=${dkim.dkimPublicKey}`]
    );
  });

  return {
    dkimSelector: dkim.dkimSelector,
    domain: setup.domain,
    keySize: dkimKeySize
  };
}

async function updateMailSetupDkimUsage(input: {
  dkimEnabled: boolean;
  domain: string;
  domainId: number;
  mailboxEmail: string;
  userId: number;
}) {
  await getDbPool().query(
    `
      UPDATE domains
      SET
        dkim_enabled = ?,
        status = 'pending',
        updated_at = NOW()
      WHERE id = ?
    `,
    [input.dkimEnabled ? 1 : 0, input.domainId]
  );

  return verifyMailSetupTarget(input);
}

export async function updateMailSetupDkimUsageByEmail(email: string, dkimEnabled: boolean) {
  const user = await getUserByEmail(email.trim().toLowerCase());

  if (!user) {
    throw new Error("user-not-found");
  }

  const setup = await getLatestSetupByUserId(user.id);

  if (!setup?.mailbox_email) {
    throw new Error("setup-not-found");
  }

  await updateMailSetupDkimUsage({
    dkimEnabled,
    domain: setup.domain,
    domainId: setup.domain_id,
    mailboxEmail: setup.mailbox_email,
    userId: user.id
  });

  const snapshot = await getSessionSnapshotByUserId(user.id);

  if (!snapshot) {
    throw new Error("snapshot-load-failed");
  }

  return snapshot;
}

export async function updateMailSetupDkimUsageByDomainId(domainId: number, dkimEnabled: boolean) {
  if (!Number.isInteger(domainId) || domainId <= 0) {
    throw new Error("setup-not-found");
  }

  const setup = await getMailSetupByDomainId(domainId);

  if (!setup?.mailbox_email) {
    throw new Error("setup-not-found");
  }

  const result = await updateMailSetupDkimUsage({
    dkimEnabled,
    domain: setup.domain,
    domainId: setup.domain_id,
    mailboxEmail: setup.mailbox_email,
    userId: setup.owner_user_id
  });

  return {
    domain: setup.domain,
    dkimEnabled,
    verified: result.verified
  };
}

function toFqdn(domain: string, host: string) {
  if (host === "@") {
    return domain;
  }

  return `${host}.${domain}`;
}

function flattenTxt(rows: string[][]) {
  return rows.map((row) => row.join("")).join(" ");
}

function normalizeRecordValue(value: string) {
  return value.trim().replace(/\.$/, "").toLowerCase();
}

function normalizeTxtValue(value: string) {
  return value.replace(/\s+/g, "").trim();
}

async function resolveDomainDns<T>(resolve: (resolver: Resolver) => Promise<T>) {
  let lastError: unknown;

  for (const server of DOMAIN_VERIFICATION_DNS_SERVERS) {
    const resolver = new Resolver();
    resolver.setServers([server]);

    try {
      return await resolve(resolver);
    } catch (error) {
      lastError = error;
    }
  }

  throw lastError ?? new Error("domain-dns-resolver-unavailable");
}

async function verifyMx(domain: string, expectedValue: string) {
  const records = await resolveDomainDns((resolver) => resolver.resolveMx(domain));
  return records.some((record) => normalizeRecordValue(record.exchange) === normalizeRecordValue(expectedValue));
}

async function verifyTxt(fqdn: string, expectedValue: string) {
  const records = await resolveDomainDns((resolver) => resolver.resolveTxt(fqdn));

  return records.some((record) => {
    const value = flattenTxt([record]).replace(/\s+/g, " ").trim();

    if (expectedValue === SPF_RECORD_VALUE) {
      const mechanisms = value.toLowerCase().split(/\s+/);
      return mechanisms[0] === "v=spf1" && mechanisms.includes(`include:${normalizeRecordValue(MX_HOSTNAME)}`);
    }

    return normalizeTxtValue(value).includes(normalizeTxtValue(expectedValue));
  });
}

async function verifyMailSetupTarget(input: {
  dkimEnabled: boolean;
  domain: string;
  domainId: number;
  mailboxEmail: string;
  notifyConnectionCompleted?: boolean;
  userId: number;
}) {
  const dnsRecords = await getDnsRecords(input.domainId);
  const mailcowProvisioned = await isMailcowProvisioned(input.domain, input.mailboxEmail);

  const verificationResults = await Promise.all(
    dnsRecords.map(async (record) => {
      const fqdn = toFqdn(input.domain, record.host_name);
      const isDkimRecord = record.record_type === "TXT" && record.host_name.endsWith("._domainkey");

      if (isDkimRecord && !input.dkimEnabled) {
        return {
          ...record,
          isChecked: false,
          isRequired: false,
          isValid: false
        };
      }

      try {
        if (record.record_type === "MX") {
          return {
            ...record,
            isChecked: true,
            isRequired: true,
            isValid: await verifyMx(input.domain, record.value_text)
          };
        }

        return {
          ...record,
          isChecked: true,
          isRequired: !isDkimRecord,
          isValid: await verifyTxt(fqdn, record.value_text)
        };
      } catch {
        return {
          ...record,
          isChecked: true,
          isRequired: !isDkimRecord,
          isValid: false
        };
      }
    })
  );

  const dnsVerified = verificationResults.every((record) => !record.isRequired || record.isValid);
  const verified = dnsVerified && mailcowProvisioned;

  const newlyVerified = await withTransaction(async (connection) => {
    const [domainRows] = await connection.query<
      (RowDataPacket & {
        mailbox_status: "active" | "disabled" | "pending_dns" | null;
        verified_at: Date | null;
      })[]
    >(
      `
        SELECT
          d.verified_at,
          m.status AS mailbox_status
        FROM domains d
        LEFT JOIN mailboxes m
          ON m.domain_id = d.id
          AND m.user_id = ?
        WHERE d.id = ?
        LIMIT 1
        FOR UPDATE
      `,
      [input.userId, input.domainId]
    );
    const wasPreviouslyVerified = Boolean(domainRows[0]?.verified_at || domainRows[0]?.mailbox_status === "active");
    const mailboxShouldRemainActive = verified || wasPreviouslyVerified;

    for (const record of verificationResults) {
      await connection.query(
        `
          UPDATE dns_records
          SET status = ?, last_checked_at = NOW(), updated_at = NOW()
          WHERE domain_id = ? AND record_type = ? AND host_name = ?
        `,
        [
          !record.isChecked ? "pending" : record.isValid ? "verified" : "failed",
          input.domainId,
          record.record_type,
          record.host_name
        ]
      );
    }

    await connection.query(
      `
        UPDATE domains
        SET
          status = ?,
          verified_at = CASE
            WHEN ? = 1 AND verified_at IS NULL THEN NOW()
            ELSE verified_at
          END,
          updated_at = NOW()
        WHERE id = ?
      `,
      [verified ? "verified" : "failed", verified ? 1 : 0, input.domainId]
    );
    await connection.query(
      `
        UPDATE mailboxes
        SET status = ?, updated_at = NOW()
        WHERE domain_id = ? AND user_id = ?
      `,
      [mailboxShouldRemainActive ? "active" : "pending_dns", input.domainId, input.userId]
    );
    await connection.query(
      `
        UPDATE users
        SET mail_configured = ?, updated_at = NOW()
        WHERE id = ?
      `,
      [mailboxShouldRemainActive ? 1 : 0, input.userId]
    );

    return verified && !wasPreviouslyVerified;
  });

  if (newlyVerified) {
    await recordMarketingLifecycleEvent({
      domainId: input.domainId,
      eventType: "domain_verified",
      userId: input.userId
    });
  }

  if (verified && input.notifyConnectionCompleted) {
    after(async () => {
      try {
        const user = await getUserById(input.userId);

        if (user) {
          const sent = await sendDomainConnectionCompletedMailOnce({
            companyName: user.company_name,
            domain: input.domain,
            domainId: input.domainId,
            mailboxEmail: input.mailboxEmail
          });

          if (sent) {
            await refreshMailboxByEmail(input.mailboxEmail, { folder: "inbox" }).catch((error) => {
              console.warn("Domain connection completion mailbox refresh failed", error);
            });
          }
        }
      } catch (error) {
        // DNS verification must remain successful even if its informational email fails.
        console.error("Domain connection completion email send failed", error);
      }
    });
  }

  const dkimVerified =
    !input.dkimEnabled ||
    verificationResults.some(
      (record) => record.host_name.endsWith("._domainkey") && record.isValid,
    );

  return { dkimVerified, dnsVerified, mailcowProvisioned, verified };
}

export async function verifyMailSetup(email: string) {
  const snapshot = await getSessionSnapshotByEmail(email);

  if (!snapshot || !snapshot.domain) {
    throw new Error("setup-not-found");
  }

  const user = await getUserByEmail(snapshot.email);

  if (!user) {
    throw new Error("user-not-found");
  }

  const setup = await getLatestSetupByUserId(user.id);

  if (!setup?.mailbox_email) {
    throw new Error("setup-not-found");
  }

  await verifyMailSetupTarget({
    dkimEnabled: setup.dkim_enabled === 1,
    domain: setup.domain,
    domainId: setup.domain_id,
    mailboxEmail: setup.mailbox_email,
    notifyConnectionCompleted: true,
    userId: user.id
  });

  const updatedSnapshot = await getSessionSnapshotByEmail(email);

  if (!updatedSnapshot) {
    throw new Error("snapshot-load-failed");
  }

  return updatedSnapshot;
}

export async function verifyMailSetupByDomainId(domainId: number) {
  if (!Number.isInteger(domainId) || domainId <= 0) {
    throw new Error("setup-not-found");
  }

  const setup = await getMailSetupByDomainId(domainId);

  if (!setup?.mailbox_email) {
    throw new Error("setup-not-found");
  }

  const result = await verifyMailSetupTarget({
    dkimEnabled: setup.dkim_enabled === 1,
    domain: setup.domain,
    domainId: setup.domain_id,
    mailboxEmail: setup.mailbox_email,
    userId: setup.owner_user_id
  });

  return {
    dkimVerified: result.dkimVerified,
    dnsVerified: result.dnsVerified,
    domain: setup.domain,
    mailcowProvisioned: result.mailcowProvisioned,
    verified: result.verified
  };
}

type DomainVerificationSweepTargetRow = RowDataPacket & {
  active_mailbox_evidence: number;
  company_name: string | null;
  domain: string;
  domain_id: number;
  domain_status: "failed" | "pending" | "verified";
  external_inbound_evidence: number;
  owner_email: string;
  verified_at_evidence: number;
  verified_event_evidence: number;
};

type DomainVerificationSweepOutcome = {
  dnsVerified: boolean;
  error: string | null;
  mailcowProvisioned: boolean;
  target: DomainVerificationSweepTargetRow;
  verified: boolean;
};

async function mapDomainVerificationTargets(
  targets: DomainVerificationSweepTargetRow[],
  concurrency: number,
  callback: (target: DomainVerificationSweepTargetRow) => Promise<DomainVerificationSweepOutcome>
) {
  const results = new Array<DomainVerificationSweepOutcome>(targets.length);
  let cursor = 0;

  const workers = Array.from({ length: Math.min(concurrency, targets.length) }, async () => {
    while (cursor < targets.length) {
      const index = cursor;
      cursor += 1;
      results[index] = await callback(targets[index]);
    }
  });

  await Promise.all(workers);
  return results;
}

function getDomainVerificationTransition(
  outcome: DomainVerificationSweepOutcome,
  hadConnectionEvidence: boolean
) {
  if (outcome.error) {
    return "check_failed";
  }

  if (outcome.verified) {
    if (outcome.target.domain_status === "verified") {
      return "unchanged_verified";
    }

    return hadConnectionEvidence ? "reconnected" : "newly_verified";
  }

  if (!hadConnectionEvidence) {
    return "never_connected";
  }

  return outcome.target.domain_status === "verified"
    ? "newly_disconnected"
    : "still_disconnected";
}

export async function verifyAllActiveMailSetups() {
  await ensureOfficialMailSchema();

  const lockConnection = await getDbPool().getConnection();
  let lockAcquired = false;
  let runId: number | null = null;

  try {
    const [lockRows] = await lockConnection.query<(RowDataPacket & { acquired: number })[]>(
      "SELECT GET_LOCK('official_mail_domain_verification_sweep', 0) AS acquired"
    );
    lockAcquired = Number(lockRows[0]?.acquired ?? 0) === 1;

    if (!lockAcquired) {
      return {
        skipped: true,
        reason: "domain-verification-already-running"
      } as const;
    }

    const [runInsert] = await getDbPool().query<ResultSetHeader>(
      `
        INSERT INTO domain_verification_runs (status, started_at)
        VALUES ('running', NOW(3))
      `
    );
    runId = runInsert.insertId;

    const [targets] = await getDbPool().query<DomainVerificationSweepTargetRow[]>(
      `
        SELECT
          d.id AS domain_id,
          d.domain,
          d.status AS domain_status,
          u.email AS owner_email,
          u.company_name,
          IF(d.verified_at IS NOT NULL, 1, 0) AS verified_at_evidence,
          IF(m.status = 'active', 1, 0) AS active_mailbox_evidence,
          EXISTS(
            SELECT 1
            FROM marketing_funnel_events verification_event
            WHERE verification_event.domain_id = d.id
              AND verification_event.event_type = 'domain_verified'
          ) AS verified_event_evidence,
          EXISTS(
            SELECT 1
            FROM mailbox_messages inbound_message
            WHERE inbound_message.mailbox_id = m.id
              AND inbound_message.direction = 'inbound'
              AND LOWER(COALESCE(inbound_message.from_address, '')) NOT LIKE '%@officialsite.kr%'
          ) AS external_inbound_evidence
        FROM domains d
        INNER JOIN users u
          ON u.id = d.user_id
        INNER JOIN mailboxes m
          ON m.domain_id = d.id
         AND m.user_id = d.user_id
        WHERE d.mailcow_cleanup_at IS NULL
        ORDER BY d.id ASC
      `
    );

    const verifyTarget = async (target: DomainVerificationSweepTargetRow) => {
      try {
        const result = await verifyMailSetupByDomainId(target.domain_id);
        return {
          dnsVerified: result.dnsVerified,
          error: null,
          mailcowProvisioned: result.mailcowProvisioned,
          target,
          verified: result.verified
        } satisfies DomainVerificationSweepOutcome;
      } catch (error) {
        return {
          dnsVerified: false,
          error: error instanceof Error ? error.message : "domain-verification-failed",
          mailcowProvisioned: false,
          target,
          verified: false
        } satisfies DomainVerificationSweepOutcome;
      }
    };

    let outcomes = await mapDomainVerificationTargets(targets, 4, verifyTarget);
    const retryTargets = outcomes
      .filter((outcome) => outcome.error || !outcome.verified)
      .map((outcome) => outcome.target);

    if (retryTargets.length > 0) {
      await new Promise((resolve) => setTimeout(resolve, 2_500));
      const retryOutcomes = await mapDomainVerificationTargets(retryTargets, 3, verifyTarget);
      const retryByDomainId = new Map(
        retryOutcomes.map((outcome) => [outcome.target.domain_id, outcome])
      );
      outcomes = outcomes.map(
        (outcome) => retryByDomainId.get(outcome.target.domain_id) ?? outcome
      );
    }

    const resultRows = outcomes.map((outcome) => {
      const target = outcome.target;
      const hadConnectionEvidence = Boolean(
        target.verified_at_evidence ||
          target.verified_event_evidence ||
          target.active_mailbox_evidence ||
          target.external_inbound_evidence
      );
      const transitionKind = getDomainVerificationTransition(outcome, hadConnectionEvidence);

      return {
        currentStatus: outcome.error
          ? target.domain_status
          : outcome.verified
            ? "verified"
            : "failed",
        hadConnectionEvidence,
        outcome,
        transitionKind
      };
    });

    for (const row of resultRows) {
      const { outcome, transitionKind } = row;
      const target = outcome.target;
      await getDbPool().query(
        `
          INSERT INTO domain_verification_results (
            run_id,
            domain_id,
            domain_name,
            owner_email,
            company_name,
            previous_status,
            current_status,
            transition_kind,
            had_connection_evidence,
            evidence_verified_at,
            evidence_verified_event,
            evidence_active_mailbox,
            evidence_external_inbound,
            dns_verified,
            mailcow_provisioned,
            error_text,
            checked_at
          )
          VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(3))
        `,
        [
          runId,
          target.domain_id,
          target.domain,
          target.owner_email,
          target.company_name,
          target.domain_status,
          row.currentStatus,
          transitionKind,
          row.hadConnectionEvidence ? 1 : 0,
          target.verified_at_evidence,
          target.verified_event_evidence,
          target.active_mailbox_evidence,
          target.external_inbound_evidence,
          outcome.dnsVerified ? 1 : 0,
          outcome.mailcowProvisioned ? 1 : 0,
          outcome.error
        ]
      );
    }

    const disconnected = resultRows.filter(
      (row) => !row.outcome.error && !row.outcome.verified && row.hadConnectionEvidence
    );
    const neverConnected = resultRows.filter(
      (row) => !row.outcome.error && !row.outcome.verified && !row.hadConnectionEvidence
    );
    const newlyDisconnected = resultRows.filter(
      (row) => row.transitionKind === "newly_disconnected"
    );
    const reconnected = resultRows.filter((row) => row.transitionKind === "reconnected");
    const errors = resultRows.filter((row) => Boolean(row.outcome.error));
    const verifiedCount = resultRows.filter(
      (row) => !row.outcome.error && row.outcome.verified
    ).length;

    await getDbPool().query(
      `
        UPDATE domain_verification_runs
        SET
          status = ?,
          completed_at = NOW(3),
          total_count = ?,
          verified_count = ?,
          disconnected_count = ?,
          never_connected_count = ?,
          newly_disconnected_count = ?,
          reconnected_count = ?,
          error_count = ?
        WHERE id = ?
      `,
      [
        errors.length > 0 ? "completed_with_errors" : "completed",
        resultRows.length,
        verifiedCount,
        disconnected.length,
        neverConnected.length,
        newlyDisconnected.length,
        reconnected.length,
        errors.length,
        runId
      ]
    );

    return {
      disconnected: disconnected.map((row) => ({
        companyName: row.outcome.target.company_name,
        domain: row.outcome.target.domain,
        ownerEmail: row.outcome.target.owner_email,
        transitionKind: row.transitionKind
      })),
      errorCount: errors.length,
      newlyDisconnectedCount: newlyDisconnected.length,
      neverConnectedCount: neverConnected.length,
      reconnectedCount: reconnected.length,
      runId,
      skipped: false,
      totalCount: resultRows.length,
      verifiedCount
    } as const;
  } catch (error) {
    if (runId) {
      await getDbPool().query(
        `
          UPDATE domain_verification_runs
          SET status = 'failed', completed_at = NOW(3), error_text = ?
          WHERE id = ?
        `,
        [error instanceof Error ? error.message : "domain-verification-sweep-failed", runId]
      ).catch(() => undefined);
    }

    throw error;
  } finally {
    if (lockAcquired) {
      await lockConnection.query(
        "SELECT RELEASE_LOCK('official_mail_domain_verification_sweep')"
      ).catch(() => undefined);
    }
    lockConnection.release();
  }
}

export async function getMailSetupOverviewByEmail(email: string) {
  const user = await getUserByEmail(email);

  if (!user) {
    return null;
  }

  const setup = await getLatestSetupByUserId(user.id);

  if (!setup) {
    return null;
  }

  const dnsRecords = await getDnsRecords(setup.domain_id);

  return {
    domain: setup.domain,
    mailbox: setup.mailbox_email,
    localPart: setup.local_part,
    status: setup.domain_status,
    mailConfigured:
      Boolean(user.mail_configured) || (Boolean(setup.mailbox_email) && setup.mailbox_status === "active"),
    dkimEnabled: setup.dkim_enabled === 1,
    mxHostname: MX_HOSTNAME,
    dkimSelector: setup.dkim_selector,
    dkimPublicKey: setup.dkim_public_key,
    verifiedAt: normalizeDate(setup.verified_at),
    dnsRecords: dnsRecords.map((record) => ({
      type: record.record_type,
      host: record.host_name === "@" ? setup.domain : record.host_name,
      value: record.value_text,
      priority: record.priority,
      status: record.status,
      lastCheckedAt: normalizeDate(record.last_checked_at)
    }))
  } satisfies MailSetupOverview;
}

export const DKIM_ROTATION_NOTICE_SIGNUP_CUTOFF = "2026-08-18 21:17:36";

export async function shouldShowDkimRotationNoticeByEmail(email: string) {
  await ensureOfficialMailSchema();
  const normalizedEmail = normalizeEmailAddress(email);
  const [rows] = await getDbPool().query<(RowDataPacket & { eligible: number })[]>(
    `
      SELECT EXISTS(
        SELECT 1
        FROM users
        WHERE email = ?
          AND created_at <= ?
      ) AS eligible
    `,
    [normalizedEmail, DKIM_ROTATION_NOTICE_SIGNUP_CUTOFF]
  );

  if (rows[0]?.eligible !== 1) {
    return false;
  }

  const user = await getUserByEmail(normalizedEmail);

  if (!user) {
    return false;
  }

  const setup = await getLatestSetupByUserId(user.id);

  if (!setup || setup.dkim_enabled !== 1) {
    return false;
  }

  const dkimHost = `${setup.dkim_selector}._domainkey`;
  const currentDkimValue = `v=DKIM1; k=rsa; p=${setup.dkim_public_key}`;
  const dnsRecords = await getDnsRecords(setup.domain_id);
  const currentDkimVerified = dnsRecords.some(
    (record) =>
      record.record_type === "TXT" &&
      record.host_name === dkimHost &&
      record.value_text === currentDkimValue &&
      record.status === "verified" &&
      Boolean(record.last_checked_at)
  );

  return !currentDkimVerified;
}

async function getMailboxPendingMoveProjection(mailboxId: number): Promise<MailboxPendingMoveProjection> {
  const [rows] = await getDbPool().query<MailboxPendingMoveFolderRow[]>(
    `
      SELECT
        source_folder.system_name AS source_folder,
        move_job.source_folder AS requested_source_folder,
        move_job.target_folder,
        COUNT(*) AS message_count,
        SUM(CASE WHEN message.is_read = 0 THEN 1 ELSE 0 END) AS unread_count
      FROM mailbox_message_move_job_items move_item
      INNER JOIN mailbox_message_move_jobs move_job
        ON move_job.id = move_item.job_id
      INNER JOIN mailbox_messages message
        ON message.id = move_item.mailbox_message_id
        AND message.mailbox_id = move_item.mailbox_id
      INNER JOIN mailbox_folders source_folder
        ON source_folder.id = message.folder_id
      WHERE move_item.mailbox_id = ?
        AND move_job.status IN ('pending', 'processing')
      GROUP BY source_folder.system_name, move_job.source_folder, move_job.target_folder
    `,
    [mailboxId]
  );
  const affectedFolders = new Set<string>();
  const countDeltas = new Map<string, { count: number; unreadCount: number }>();
  const addDelta = (folder: string, count: number, unreadCount: number) => {
    const current = countDeltas.get(folder) ?? { count: 0, unreadCount: 0 };
    countDeltas.set(folder, {
      count: current.count + count,
      unreadCount: current.unreadCount + unreadCount
    });
  };

  for (const row of rows) {
    const messageCount = Number(row.message_count ?? 0);
    const unreadCount = Number(row.unread_count ?? 0);
    affectedFolders.add(row.source_folder);
    affectedFolders.add(row.target_folder);

    if (row.source_folder === row.target_folder) {
      if (row.target_folder === "trash" && row.requested_source_folder === "trash") {
        addDelta(row.source_folder, -messageCount, -unreadCount);
      }
      continue;
    }

    addDelta(row.source_folder, -messageCount, -unreadCount);
    addDelta(row.target_folder, messageCount, unreadCount);
  }

  const [drafts] = await getDbPool().query<(RowDataPacket & { count: number })[]>(
    `SELECT COUNT(*) AS count FROM mailbox_messages mm
     INNER JOIN mailbox_folders f ON f.id = mm.folder_id
     WHERE mm.mailbox_id = ? AND mm.direction = 'draft' AND mm.remote_uid IS NULL
       AND mm.transport_status = 'queued' AND f.system_name = 'drafts'`, [mailboxId],
  );
  const pendingDraftCount = Number(drafts[0]?.count ?? 0);
  if (pendingDraftCount) { affectedFolders.add("drafts"); addDelta("drafts", pendingDraftCount, 0); }

  return { affectedFolders, countDeltas };
}

async function getProjectedMailboxFolderStatuses(
  mailboxId: number,
  pendingProjection: MailboxPendingMoveProjection
) {
  const [folderRows] = await getDbPool().query<FolderSummaryRow[]>(
    `
      SELECT id, system_name, name, remote_name, remote_total, remote_unseen, last_synced_at
      FROM mailbox_folders
      WHERE mailbox_id = ?
      ORDER BY sort_order ASC, id ASC
    `,
    [mailboxId]
  );

  return folderRows.map((folder) => {
    const delta = pendingProjection.countDeltas.get(folder.system_name);
    return {
      count: Math.max(0, Number(folder.remote_total) + (delta?.count ?? 0)),
      name: folder.name,
      systemName: folder.system_name,
      unreadCount: Math.max(0, Number(folder.remote_unseen) + (delta?.unreadCount ?? 0))
    };
  });
}

async function loadRemoteMailboxFolderStatusSnapshot(mailbox: MailboxRow): Promise<MailboxRemoteFolderStatusSnapshot> {
  await ensureOfficialMailSchema();
  const [[folderRows], pendingProjection] = await Promise.all([
    getDbPool().query<
      (RowDataPacket & {
        id: number;
        name: string;
        remote_name: string | null;
        remote_total: number;
        remote_unseen: number;
        system_name: string;
      })[]
    >(
      `
        SELECT id, system_name, name, remote_name, remote_total, remote_unseen
        FROM mailbox_folders
        WHERE mailbox_id = ?
        ORDER BY sort_order ASC, id ASC
      `,
      [mailbox.id]
    ),
    getMailboxPendingMoveProjection(mailbox.id)
  ]);
  const statuses = await getRemoteMailboxFolderStatuses(
    toMailboxCredentials(mailbox),
    folderRows.map((folder) => ({
      name: folder.name,
      remoteName: folder.remote_name,
      systemName: folder.system_name
    }))
  );
  const folderBySystemName = new Map(folderRows.map((folder) => [folder.system_name, folder]));
  const changedFolders: string[] = [];

  for (const status of statuses) {
    const folder = folderBySystemName.get(status.systemName);

    if (!folder || pendingProjection.affectedFolders.has(status.systemName)) {
      continue;
    }

    const changed =
      (folder.remote_name ?? "") !== status.remoteName ||
      Number(folder.remote_total) !== status.totalCount ||
      Number(folder.remote_unseen) !== status.unreadCount;

    if (!changed) {
      continue;
    }

    changedFolders.push(status.systemName);
    await getDbPool().query(
      `
        UPDATE mailbox_folders
        SET
          remote_name = ?,
          remote_total = ?,
          remote_unseen = ?,
          last_synced_at = NULL,
          updated_at = NOW()
        WHERE id = ? AND mailbox_id = ?
      `,
      [status.remoteName, status.totalCount, status.unreadCount, folder.id, mailbox.id]
    );
  }

  return { changedFolders, folders: statuses };
}

async function getCachedRemoteMailboxFolderStatusSnapshot(mailbox: MailboxRow, force: boolean) {
  const now = Date.now();
  let entry = mailboxFolderStatusCache.get(mailbox.id);

  if (!entry) {
    entry = { expiresAt: 0, inFlight: null, value: null };
    mailboxFolderStatusCache.set(mailbox.id, entry);
  }

  if (entry.inFlight) {
    return entry.inFlight;
  }

  if (!force && entry.value && entry.expiresAt > now) {
    return entry.value;
  }

  const target = entry;
  target.inFlight = loadRemoteMailboxFolderStatusSnapshot(mailbox);

  try {
    const value = await target.inFlight;
    target.value = value;
    target.expiresAt = Date.now() + MAILBOX_FOLDER_STATUS_CACHE_TTL_MS;
    return value;
  } catch (error) {
    target.expiresAt = 0;
    throw error;
  } finally {
    target.inFlight = null;
  }
}

export async function refreshMailboxFolderStatusByEmail(
  email: string,
  options?: {
    force?: boolean;
    folder?: string;
  }
): Promise<MailboxFolderStatusSnapshot> {
  const user = await getUserByEmail(email);

  if (!user) {
    throw new Error("user-not-found");
  }

  const mailbox = await getLatestMailboxByUserId(user.id);

  if (!mailbox) {
    throw new Error("mailbox-not-found");
  }

  const requestedFolder = options?.folder === "all" ? "all" : normalizeFolderSystemName(options?.folder);
  let pendingProjection = await getMailboxPendingMoveProjection(mailbox.id);
  if (pendingProjection.affectedFolders.size > 0) {
    return {
      changedFolders: [],
      currentFolderChanged: false,
      folders: await getProjectedMailboxFolderStatuses(mailbox.id, pendingProjection),
      syncedFolders: []
    };
  }

  const snapshot = await getCachedRemoteMailboxFolderStatusSnapshot(mailbox, options?.force === true);
  pendingProjection = await getMailboxPendingMoveProjection(mailbox.id);
  if (pendingProjection.affectedFolders.size > 0) {
    return {
      changedFolders: [],
      currentFolderChanged: false,
      folders: await getProjectedMailboxFolderStatuses(mailbox.id, pendingProjection),
      syncedFolders: []
    };
  }
  const [pendingRows] = await getDbPool().query<(RowDataPacket & { system_name: string })[]>(
    `
      SELECT folder.system_name
      FROM mailbox_folders folder
      LEFT JOIN (
        SELECT folder_id, COUNT(remote_uid) AS remote_message_count
        FROM mailbox_messages
        WHERE mailbox_id = ?
        GROUP BY folder_id
      ) local_count ON local_count.folder_id = folder.id
      WHERE folder.mailbox_id = ?
        AND (
          folder.last_synced_at IS NULL
          OR COALESCE(local_count.remote_message_count, 0) <> folder.remote_total
        )
      ORDER BY folder.sort_order ASC, folder.id ASC
    `,
    [mailbox.id, mailbox.id]
  );
  const pendingFolders = pendingRows.map((row) => row.system_name);
  const foldersToSync =
    requestedFolder === "all"
      ? pendingFolders.filter((folder) => !pendingProjection.affectedFolders.has(folder))
      : pendingFolders.filter(
          (folder) => folder === requestedFolder && !pendingProjection.affectedFolders.has(folder)
        );
  const syncedFolders: string[] = [];

  for (const folder of foldersToSync) {
    try {
      const result = await refreshMailboxByEmail(email, {
        deferHydration: true,
        folder,
        mailboxId: mailbox.id
      });
      syncedFolders.push(folder);

      if (result.startHydration) {
        void result.startHydration();
      }
    } catch (error) {
      const message = error instanceof Error ? error.message : "folder-status-sync-failed";
      console.warn(`[mailbox-folder-status] sync failed mailbox=${mailbox.email} folder=${folder} message=${message}`);
    }
  }

  return {
    changedFolders: snapshot.changedFolders,
    currentFolderChanged:
      requestedFolder === "all" ? syncedFolders.length > 0 : syncedFolders.includes(requestedFolder),
    folders: snapshot.folders.map((folder) => {
      const delta = pendingProjection.countDeltas.get(folder.systemName);
      return {
        count: Math.max(0, folder.totalCount + (delta?.count ?? 0)),
        name: folder.name,
        systemName: folder.systemName,
        unreadCount: Math.max(0, folder.unreadCount + (delta?.unreadCount ?? 0))
      };
    }),
    syncedFolders
  };
}

export async function syncAllMailboxFoldersByEmail(
  email: string,
  options?: {
    awaitHydration?: boolean;
    mailboxId?: number;
  }
) {
  const user = await getUserByEmail(email);

  if (!user) {
    throw new Error("user-not-found");
  }

  const mailbox = options?.mailboxId
    ? await getMailboxByUserIdAndId(user.id, options.mailboxId)
    : await getLatestMailboxByUserId(user.id);

  if (!mailbox) {
    throw new Error("mailbox-not-found");
  }

  await ensureOfficialMailSchema();
  const [throttleResult] = await getDbPool().query<ResultSetHeader>(
    `
      UPDATE mailboxes
      SET full_sync_requested_at = NOW(3)
      WHERE id = ?
        AND (
          full_sync_requested_at IS NULL
          OR full_sync_requested_at <= DATE_SUB(NOW(3), INTERVAL 1 SECOND)
        )
    `,
    [mailbox.id]
  );

  if (throttleResult.affectedRows === 0) {
    return { accepted: false, failedFolders: [], syncedFolders: [] };
  }

  const syncedFolders: string[] = [];
  const failedFolders: string[] = [];
  const hydrationStarters: Array<() => Promise<void>> = [];

  mailboxFolderStatusCache.delete(mailbox.id);

  try {
    const result = await refreshMailboxByEmail(email, {
      deferHydration: true,
      folder: "inbox",
      fullMetadata: true,
      mailboxId: mailbox.id
    });
    syncedFolders.push("inbox");

    if (result.startHydration) {
      hydrationStarters.push(result.startHydration);
    }
  } catch (error) {
    failedFolders.push("inbox");
    const message = error instanceof Error ? error.message : "full-sync-failed";
    console.warn(`[mailbox-full-sync] failed mailbox=${mailbox.email} folder=inbox message=${message}`);
  }

  // The inbox pass also discovers folders created directly through IMAP.
  const [folderRows] = await getDbPool().query<(RowDataPacket & { system_name: string })[]>(
    `
      SELECT system_name
      FROM mailbox_folders
      WHERE mailbox_id = ? AND system_name <> 'inbox'
      ORDER BY sort_order ASC, id ASC
    `,
    [mailbox.id]
  );

  for (const row of folderRows) {
    try {
      const result = await refreshMailboxByEmail(email, {
        deferHydration: true,
        folder: row.system_name,
        fullMetadata: true,
        mailboxId: mailbox.id
      });
      syncedFolders.push(row.system_name);

      if (result.startHydration) {
        hydrationStarters.push(result.startHydration);
      }
    } catch (error) {
      failedFolders.push(row.system_name);
      const message = error instanceof Error ? error.message : "full-sync-failed";
      console.warn(`[mailbox-full-sync] failed mailbox=${mailbox.email} folder=${row.system_name} message=${message}`);
    }
  }

  for (const startHydration of hydrationStarters) {
    if (options?.awaitHydration) {
      await startHydration();
    } else {
      void startHydration();
    }
  }

  await retryMailboxSyncWrite(() => clearResolvedMailboxContentErrors(mailbox.id));
  await retryMailboxSyncWrite(() =>
    updateMailboxSyncState(mailbox.id, {
      lastSyncError:
        failedFolders.length > 0
          ? `메일함 동기화 실패: ${failedFolders.join(", ")}`.slice(0, 1000)
          : null
    })
  );
  const failureState = await getMailboxSyncFailureState(mailbox.id);

  if (failureState.metadataError || failureState.contentErrorCount > 0) {
    await scheduleMailboxSyncRetry(mailbox.id);
  } else {
    await clearMailboxSyncRetry(mailbox.id);
  }

  return {
    accepted: true,
    contentErrorCount: failureState.contentErrorCount,
    failedFolders,
    syncedFolders
  };
}

export async function retryFailedMailboxSynchronizations(options?: { limit?: number }) {
  await ensureOfficialMailSchema();
  const limit = Math.max(1, Math.min(5, Math.floor(options?.limit ?? 1)));
  const [rows] = await getDbPool().query<
    (RowDataPacket & {
      account_email: string;
      id: number;
      sync_retry_attempts: number;
    })[]
  >(
    `
      SELECT
        m.id,
        u.email AS account_email,
        m.sync_retry_attempts
      FROM mailboxes m
      INNER JOIN users u ON u.id = m.user_id
      WHERE m.status = 'active'
        AND m.password_ciphertext IS NOT NULL
        AND (
          NULLIF(TRIM(m.last_sync_error), '') IS NOT NULL
          OR EXISTS (
            SELECT 1
            FROM mailbox_folders f
            WHERE f.mailbox_id = m.id
              AND NULLIF(TRIM(f.last_content_sync_error), '') IS NOT NULL
          )
        )
        AND (m.next_sync_retry_at IS NULL OR m.next_sync_retry_at <= NOW())
      ORDER BY COALESCE(m.next_sync_retry_at, m.updated_at) ASC, m.id ASC
      LIMIT ?
    `,
    [limit]
  );
  const recovered: number[] = [];
  const failed: Array<{ mailboxId: number; message: string }> = [];
  const skipped: number[] = [];

  for (const row of rows) {
    const [claim] = await getDbPool().query<ResultSetHeader>(
      `
        UPDATE mailboxes
        SET
          sync_retry_attempts = sync_retry_attempts + 1,
          next_sync_retry_at = DATE_ADD(NOW(), INTERVAL 15 MINUTE),
          updated_at = NOW()
        WHERE id = ?
          AND (next_sync_retry_at IS NULL OR next_sync_retry_at <= NOW())
      `,
      [row.id]
    );

    if (claim.affectedRows === 0) {
      skipped.push(row.id);
      continue;
    }

    try {
      const result = await syncAllMailboxFoldersByEmail(row.account_email, {
        awaitHydration: true,
        mailboxId: row.id
      });

      if (!result.accepted) {
        await getDbPool().query(
          `UPDATE mailboxes SET next_sync_retry_at = DATE_ADD(NOW(), INTERVAL 1 MINUTE) WHERE id = ?`,
          [row.id]
        );
        skipped.push(row.id);
        continue;
      }

      const failureState = await getMailboxSyncFailureState(row.id);

      if (!failureState.metadataError && failureState.contentErrorCount === 0) {
        await clearMailboxSyncRetry(row.id);
        recovered.push(row.id);
        continue;
      }

      const nextAttempt = Number(row.sync_retry_attempts ?? 0) + 1;
      const retryDelayMinutes = Math.min(360, 5 * 2 ** Math.min(nextAttempt, 6));
      const retryAt = new Date(Date.now() + retryDelayMinutes * 60 * 1000);
      await getDbPool().query(
        `UPDATE mailboxes SET next_sync_retry_at = ?, updated_at = NOW() WHERE id = ?`,
        [retryAt, row.id]
      );
      failed.push({
        mailboxId: row.id,
        message: failureState.metadataError || `본문 동기화 오류 ${failureState.contentErrorCount}건`
      });
    } catch (error) {
      const message = error instanceof Error ? error.message : "mailbox-sync-recovery-failed";
      const nextAttempt = Number(row.sync_retry_attempts ?? 0) + 1;
      const retryDelayMinutes = Math.min(360, 5 * 2 ** Math.min(nextAttempt, 6));
      const retryAt = new Date(Date.now() + retryDelayMinutes * 60 * 1000);
      await getDbPool().query(
        `UPDATE mailboxes SET next_sync_retry_at = ?, updated_at = NOW() WHERE id = ?`,
        [retryAt, row.id]
      );
      failed.push({ mailboxId: row.id, message });
    }
  }

  return {
    failed,
    processed: rows.length,
    recovered,
    skipped
  };
}

export async function refreshMailboxByEmail(
  email: string,
  options?: {
    deferHydration?: boolean;
    folder?: string;
    fullMetadata?: boolean;
    mailboxId?: number;
  }
) {
  return syncMailboxCacheByEmail(email, options?.folder, {
    deferHydration: options?.deferHydration,
    fullMetadata: options?.fullMetadata,
    mailboxId: options?.mailboxId
  });
}

export async function refreshMailboxFileDrawerIndexByEmail(email: string) {
  const user = await getUserByEmail(email);
  if (!user) throw new Error("user-not-found");

  const mailbox = await getLatestMailboxByUserId(user.id);
  if (!mailbox) throw new Error("mailbox-not-found");

  await ensureOfficialMailSchema();
  const [rows] = await getDbPool().query<
    (RowDataPacket & {
      local_count: number;
      remote_total: number;
      system_name: string;
      unindexed_count: number;
    })[]
  >(
    `
      SELECT
        mf.system_name,
        mf.remote_total,
        COUNT(mm.id) AS local_count,
        COALESCE(SUM(mm.remote_uid IS NOT NULL AND mm.attachments_indexed_at IS NULL), 0) AS unindexed_count
      FROM mailbox_folders mf
      LEFT JOIN mailbox_messages mm ON mm.folder_id = mf.id AND mm.mailbox_id = mf.mailbox_id
      WHERE mf.mailbox_id = ?
        AND (mf.system_name IN ('inbox', 'spam') OR mf.system_name LIKE 'custom:%')
      GROUP BY mf.id, mf.system_name, mf.remote_total
      ORDER BY CASE mf.system_name WHEN 'inbox' THEN 0 WHEN 'spam' THEN 1 ELSE 2 END, mf.system_name
    `,
    [mailbox.id]
  );
  const foldersToSync = rows
    .map((row) => ({
      folder: row.system_name,
      fullMetadata:
        Number(row.unindexed_count) > 0 ||
        (mailbox.message_visibility_cutoff_at === null && Number(row.local_count) !== Number(row.remote_total)),
    }))
    .filter((row) => row.folder === 'inbox' || row.folder === 'spam' || row.fullMetadata);
  const failedFolders: string[] = [];
  const attachmentFingerprint = async () => {
    const [attachmentRows] = await getDbPool().query<
      (RowDataPacket & { attachment_count: number; latest_attachment_id: number })[]
    >(
      `SELECT COUNT(*) AS attachment_count, COALESCE(MAX(mma.id), 0) AS latest_attachment_id
       FROM mailbox_message_attachments mma
       INNER JOIN mailbox_messages mm ON mm.id = mma.mailbox_message_id
       WHERE mm.mailbox_id = ? AND mm.direction = 'inbound'
         AND mma.content_disposition = 'attachment'`,
      [mailbox.id]
    );
    return `${attachmentRows[0]?.attachment_count ?? 0}:${attachmentRows[0]?.latest_attachment_id ?? 0}`;
  };
  const beforeAttachments = await attachmentFingerprint();

  for (const { folder, fullMetadata } of foldersToSync) {
    try {
      await refreshMailboxByEmail(email, {
        deferHydration: true,
        folder,
        fullMetadata,
        mailboxId: mailbox.id
      });
    } catch (error) {
      failedFolders.push(folder);
      const message = error instanceof Error ? error.message : "file-drawer-index-failed";
      console.warn(
        `[mailbox-file-drawer] attachment index failed mailbox=${mailbox.email} folder=${folder} message=${message}`
      );
    }
  }

  const afterAttachments = await attachmentFingerprint();

  return {
    changed: beforeAttachments !== afterAttachments,
    failedFolders,
    syncedFolders: foldersToSync.map((row) => row.folder).filter((folder) => !failedFolders.includes(folder))
  };
}

export async function refreshMailboxIfStaleByEmail(
  email: string,
  options?: {
    folder?: string;
    mailboxId?: number;
    maxAgeMs?: number;
  }
) {
  const user = await getUserByEmail(email);

  if (!user) {
    return false;
  }

  const mailbox = options?.mailboxId
    ? await getMailboxByUserIdAndId(user.id, options.mailboxId)
    : await getLatestMailboxByUserId(user.id);

  if (!mailbox) {
    return false;
  }

  await ensureOfficialMailSchema();
  const requestedFolderKey = resolveMailboxRefreshFolder(options?.folder);
  const [rows] = await getDbPool().query<(RowDataPacket & { last_synced_at: Date | null; system_name: string })[]>(
    `
      SELECT system_name, last_synced_at
      FROM mailbox_folders
      WHERE mailbox_id = ? AND system_name = ?
      LIMIT 1
    `,
    [mailbox.id, requestedFolderKey]
  );

  const resolvedFolderKey = rows[0]?.system_name ?? "inbox";
  const lastSyncedAt = rows[0]?.last_synced_at ?? null;
  const maxAgeMs = options?.maxAgeMs ?? MAILBOX_AUTO_REFRESH_MAX_AGE_MS;

  if (lastSyncedAt && Date.now() - lastSyncedAt.getTime() < maxAgeMs) {
    return false;
  }

  await syncMailboxCacheByEmail(email, resolvedFolderKey, {
    mailboxId: mailbox.id
  });
  return true;
}

export async function saveMailboxCredentialsByEmail(
  email: string,
  password: string,
  options?: {
    folder?: string;
  }
) {
  const normalizedPassword = password.trim();

  if (!normalizedPassword) {
    throw new Error("mailbox-password-required");
  }

  const user = await getUserByEmail(email);

  if (!user) {
    throw new Error("user-not-found");
  }

  if (verifyPassword(normalizedPassword, user.password_hash)) {
    throw new Error("mailbox-password-must-differ");
  }

  const mailbox = await getLatestMailboxByUserId(user.id);

  if (!mailbox) {
    throw new Error("mailbox-not-found");
  }

  await getDbPool().query(
    `
      UPDATE mailboxes
      SET
        password_ciphertext = ?,
        password_updated_at = NOW(),
        last_sync_error = NULL,
        updated_at = NOW()
      WHERE id = ?
    `,
    [encryptMailboxPassword(normalizedPassword), mailbox.id]
  );

  await syncMailboxCacheByEmail(email, options?.folder);
}

async function getDeliveryLogsByMessageId(messageId: number) {
  const pool = getDbPool();
  const [rows] = await pool.query<MailDeliveryLogRow[]>(
    `
      SELECT
        id,
        action,
        status,
        transport,
        source_address,
        target_address,
        subject,
        message_id_header,
        response_text,
        created_at
      FROM mailbox_delivery_logs
      WHERE mailbox_message_id = ?
      ORDER BY created_at DESC, id DESC
    `,
    [messageId]
  );

  return rows.map((row) => ({
    id: row.id,
    action: row.action,
    status: row.status,
    transport: row.transport,
    sourceAddress: row.source_address,
    targetAddress: row.target_address,
    subject: row.subject,
    messageIdHeader: row.message_id_header,
    responseText: row.response_text,
    createdAt: row.created_at.toISOString()
  })) satisfies MailDeliveryLog[];
}

export async function getMailboxMessageBodyViewByEmail(
  email: string,
  messageId: number,
  options?: {
    mailboxId?: number;
  }
) {
  if (!Number.isInteger(messageId) || messageId <= 0) {
    return null;
  }

  const user = await getUserByEmail(email);

  if (!user) {
    return null;
  }

  const mailbox = options?.mailboxId
    ? await getMailboxByUserIdAndId(user.id, options.mailboxId)
    : await getLatestMailboxByUserId(user.id);

  if (!mailbox) {
    return null;
  }

  const [rows] = await getDbPool().query<
    (MailMessageRow & {
      folder_id: number;
      raw_source_available: number;
    })[]
  >(
    `
      SELECT
        mm.id,
        mm.folder_id,
        mm.remote_uid,
        mm.remote_folder,
        mm.subject,
        mm.from_name,
        mm.from_address,
        mm.to_addresses,
        mm.snippet,
        mm.body_text,
        mm.body_html,
        (mm.raw_source IS NOT NULL) AS raw_source_available,
        NULL AS raw_source,
        mm.message_id_header,
        mm.transport_status,
        mm.transport_response,
        mm.remote_flags,
        mm.is_read,
        mm.is_starred,
        mm.received_at,
        mm.direction,
        f.system_name AS folder_system_name,
        f.name AS folder_name
      FROM mailbox_messages mm
      INNER JOIN mailbox_folders f ON f.id = mm.folder_id
      WHERE mm.mailbox_id = ?
        AND mm.id = ?
      LIMIT 1
    `,
    [mailbox.id, messageId]
  );
  const row = rows[0];

  if (!row) {
    return null;
  }

  if (!Boolean(row.raw_source_available) && row.remote_folder && row.remote_uid) {
    try {
      const remoteMessage = await retryMailboxTransport(() =>
        fetchRemoteMailboxMessage(toMailboxCredentials(mailbox), {
          remoteFolder: row.remote_folder!,
          remoteUid: Number(row.remote_uid),
          systemName: isSystemMailboxFolderSystemName(row.folder_system_name) ? row.folder_system_name : "custom"
        })
      );

      if (remoteMessage) {
        await retryMailboxSyncWrite(() =>
          withTransaction(async (connection) => {
            const hydratedMessageId = await upsertMailboxMessageRow(connection, {
              bodyHtml: remoteMessage.bodyHtml,
              bodyText: remoteMessage.bodyText,
              direction: remoteMessage.direction,
              folderId: row.folder_id,
              fromAddress: remoteMessage.fromAddress,
              fromName: remoteMessage.fromName,
              isRead: remoteMessage.isRead,
              isStarred: hasRemoteFlag(remoteMessage.remoteFlags, "\\Flagged"),
              mailboxId: mailbox.id,
              messageIdHeader: remoteMessage.messageIdHeader,
              preserveExistingTransport: true,
              rawSource: remoteMessage.rawSource,
              receivedAt: remoteMessage.receivedAt,
              remoteInternalDate: remoteMessage.remoteInternalDate,
              remoteFlags: normalizeRemoteFlags(remoteMessage.remoteFlags),
              remoteFolder: remoteMessage.remoteFolder,
              remoteUid: remoteMessage.remoteUid,
              snippet: remoteMessage.snippet,
              subject: remoteMessage.subject,
              toAddresses: remoteMessage.toAddresses,
              transportResponse: normalizeRemoteTransportResponse(remoteMessage),
              transportStatus: remoteMessage.transportStatus
            });
            await replaceMailboxMessageAttachmentMetadata(connection, hydratedMessageId, remoteMessage.attachments);
          })
        );
        await retryMailboxSyncWrite(() =>
          updateMailboxFolderContentSyncState(row.folder_id, {
            lastContentSyncAt: new Date(),
            lastContentSyncError: null
          })
        );

        return getMailboxMessageBodyViewByEmail(email, messageId, options);
      }
    } catch (error) {
      const message = error instanceof Error ? error.message : "Mailbox message hydration failed";
      await retryMailboxSyncWrite(() =>
        updateMailboxFolderContentSyncState(row.folder_id, {
          lastContentSyncError: message.slice(0, 1000)
        })
      ).catch(() => undefined);
      await retryMailboxSyncWrite(() => scheduleMailboxSyncRetry(mailbox.id)).catch(() => undefined);
      console.warn(
        `[mailbox-sync] on-demand hydration failed mailbox=${mailbox.email} messageId=${messageId} message=${message}`
      );
    }
  }

  const [rawAttachments, linkedAttachments] = await Promise.all([
    getMailboxMessageAttachmentMetadata({
      mailboxId: mailbox.id,
      messageId: row.id
    }).catch(() => []),
    getLinkedComposeUploadAttachmentsByMessageId(row.id, {
      includeUnpublishedOwnerCopy: row.direction === "draft" ||
        (row.direction === "outbound" && row.transport_status === "queued")
    }).catch(() => [])
  ]);
  const largeTokens = getOfficialLargeAttachmentTokens(row.body_html, row.body_text);
  const largeAttachments = await getPublishedLargeComposeAttachmentsByTokens(largeTokens).catch(() => []);
  const linkedUrls = new Set(linkedAttachments.map((attachment) => attachment.externalUrl));
  const attachments: MailMessageAttachment[] = [
    ...classifyBodyInlineAttachments(rawAttachments, row.body_html).map((attachment) => ({
      ...attachment,
      externalUrl: null
    })),
    ...linkedAttachments,
    ...largeAttachments.filter((attachment) => !linkedUrls.has(attachment.externalUrl))
  ];
  const displayBody = stripDisplayedLargeAttachmentSections({
    bodyHtml: row.body_html,
    bodyText: row.body_text,
    availableTokens: attachments
      .filter((attachment) => attachment.isLargeAttachment && attachment.externalUrl)
      .map((attachment) => attachment.externalUrl!.slice(-48)),
  });

  return {
    attachments,
    bodyHtmlDisplay: buildMailboxHtmlDisplay({
      attachments,
      bodyHtml: displayBody.bodyHtml,
      bodyText: displayBody.bodyText,
      messageId: row.id
    }),
    bodyText: displayBody.bodyText ?? "",
    direction: row.direction,
    fromAddress: row.from_address,
    fromName: row.from_name,
    id: row.id,
    receivedAt: row.received_at.toISOString(),
    subject: row.subject,
    toAddresses: row.to_addresses
  } satisfies MailMessageBodyView;
}

export async function getMailboxMessageRawSourceByEmail(email: string, messageId: number) {
  if (!Number.isInteger(messageId) || messageId <= 0) {
    return null;
  }

  const user = await getUserByEmail(email.trim().toLowerCase());

  if (!user) {
    return null;
  }

  const mailbox = await getLatestMailboxByUserId(user.id);

  if (!mailbox) {
    return null;
  }

  const [rows] = await getDbPool().query<(RowDataPacket & { raw_source: string | null })[]>(
    `
      SELECT raw_source
      FROM mailbox_messages
      WHERE mailbox_id = ?
        AND id = ?
      LIMIT 1
    `,
    [mailbox.id, messageId]
  );

  return rows[0]?.raw_source ?? null;
}

export async function getMailboxDeliveryLogsByEmail(
  email: string,
  options?: {
    limit?: number;
  }
) {
  const user = await getUserByEmail(email);

  if (!user) {
    return [];
  }

  const mailbox = await getLatestMailboxByUserId(user.id);

  if (!mailbox) {
    return [];
  }

  const limit =
    options?.limit && Number.isFinite(options.limit) ? Math.max(1, Math.min(100, Math.floor(options.limit))) : 24;
  const pool = getDbPool();
  const [rows] = await pool.query<MailDeliveryLogRow[]>(
    `
      SELECT
        id,
        action,
        status,
        transport,
        source_address,
        target_address,
        subject,
        message_id_header,
        response_text,
        created_at
      FROM mailbox_delivery_logs
      WHERE mailbox_id = ?
      ORDER BY created_at DESC, id DESC
      LIMIT ?
    `,
    [mailbox.id, limit]
  );

  return rows.map((row) => ({
    id: row.id,
    action: row.action,
    status: row.status,
    transport: row.transport,
    sourceAddress: row.source_address,
    targetAddress: row.target_address,
    subject: row.subject,
    messageIdHeader: row.message_id_header,
    responseText: row.response_text,
    createdAt: row.created_at.toISOString()
  })) satisfies MailDeliveryLog[];
}

export async function getMailboxAttachmentByEmail(
  email: string,
  messageId: number,
  attachmentIndex: number,
  options?: {
    mailboxId?: number;
  }
): Promise<RemoteMailboxAttachmentPayload | null> {
  if (!Number.isInteger(messageId) || messageId < 1) {
    return null;
  }

  if (!Number.isInteger(attachmentIndex)) {
    return null;
  }

  const user = await getUserByEmail(email.trim().toLowerCase());

  if (!user) {
    return null;
  }

  const mailbox = options?.mailboxId
    ? await getMailboxByUserIdAndId(user.id, options.mailboxId)
    : await getLatestMailboxByUserId(user.id);

  if (!mailbox) {
    return null;
  }

  const [ownedMessageRows] = await getDbPool().query<
    (RowDataPacket & { content_loaded: number; id: number })[]
  >(
    `
      SELECT
        id,
        raw_source IS NOT NULL AS content_loaded
      FROM mailbox_messages
      WHERE mailbox_id = ?
        AND id = ?
      LIMIT 1
    `,
    [mailbox.id, messageId]
  );

  if (!ownedMessageRows[0]) {
    return null;
  }

  if (attachmentIndex < 0) {
    const linkedAttachment = await getLinkedComposeUploadAttachmentPayloadByMessageId(messageId, attachmentIndex);

    if (!linkedAttachment) {
      return null;
    }

    return {
      content: linkedAttachment.content,
      contentDisposition: linkedAttachment.contentDisposition,
      contentId: linkedAttachment.contentId,
      contentType: linkedAttachment.contentType,
      extension: linkedAttachment.extension,
      filename: linkedAttachment.filename,
      index: linkedAttachment.index,
      isInline: linkedAttachment.isInline,
      isPreviewable: linkedAttachment.isPreviewable,
      sizeBytes: linkedAttachment.sizeBytes
    } satisfies RemoteMailboxAttachmentPayload;
  }

  if (!Boolean(ownedMessageRows[0].content_loaded)) {
    await getMailboxMessageBodyViewByEmail(email, messageId, {
      mailboxId: mailbox.id
    });
  }

  return getCachedMailboxAttachmentPayload({
    attachmentIndex,
    mailboxId: mailbox.id,
    messageId
  });
}

export async function getMailAppOverviewByEmail(
  email: string,
  options?: {
    folder?: string;
    mailboxId?: number;
    messageId?: number | null;
    messageFilter?: MailMessageFilter;
    page?: number;
    pageSize?: number;
    includeOriginalBodyHtml?: boolean;
    markMessageRead?: boolean;
    searchField?: MailSearchField;
    searchQuery?: string;
    sortOrder?: MailListSortOrder;
  }
) {
  const user = await getUserByEmail(email);

  if (!user) {
    return null;
  }

  const mailbox = options?.mailboxId
    ? await getMailboxByUserIdAndId(user.id, options.mailboxId)
    : await getLatestMailboxByUserId(user.id);

  if (!mailbox) {
    return null;
  }

  await ensureOfficialMailSchema();

  const quotaPromise = getCachedMailcowMailboxQuota(mailbox.email);

  const pool = getDbPool();

  if (options?.markMessageRead && options?.messageId) {
    try {
      await markMailboxMessageAsRead(email, options.messageId);
    } catch {
      // Keep rendering the mailbox even if IMAP flag sync fails.
    }
  }

  const [folderRows] = await pool.query<FolderSummaryRow[]>(
    `
      SELECT
        f.id,
        f.system_name,
        f.name,
        f.remote_name,
        f.remote_total,
        f.remote_unseen,
        f.last_synced_at
      FROM mailbox_folders f
      WHERE f.mailbox_id = ?
      ORDER BY f.sort_order ASC, f.id ASC
    `,
    [mailbox.id]
  );
  const pendingMoveProjection = await getMailboxPendingMoveProjection(mailbox.id);

  const [spamSettingsRows] = await pool.query<MailboxSpamSettingsRow[]>(
    `
      SELECT
        auto_move_spam,
        detect_self_spoofing,
        detect_bounce_spoofing,
        detect_missing_recipient,
        detect_unknown_sender,
        language_filter_enabled,
        language_filter_mode,
        language_filter_languages,
        reported_message_action,
        block_reported_sender,
        process_previous_reported,
        hide_spam_folder,
        retention_days,
        expired_message_action,
        hide_remote_content
      FROM mailbox_spam_settings
      WHERE mailbox_id = ?
      LIMIT 1
    `,
    [mailbox.id]
  );
  const spamSettings = mapMailboxSpamGeneralSettings(spamSettingsRows[0]);

  const folders = folderRows.map((row) => {
    const delta = pendingMoveProjection.countDeltas.get(row.system_name);
    return {
      id: row.id,
      kind: folderKindForSystemName(row.system_name),
      systemName: row.system_name,
      name: row.name,
      parentSystemName: mailboxFolderParentSystemName(row, folderRows),
      count: Math.max(0, Number(row.remote_total ?? 0) + (delta?.count ?? 0)),
      unreadCount: Math.max(0, Number(row.remote_unseen ?? 0) + (delta?.unreadCount ?? 0))
    };
  });

  const defaultFolder =
    folders.find((folder) => folder.systemName === "inbox")?.systemName ?? folders[0]?.systemName ?? "inbox";
  const currentFolder =
    options?.folder === "all"
      ? "all"
      : (folders.find((folder) => folder.systemName === options?.folder)?.systemName ?? defaultFolder);
  const messageFilter: MailMessageFilter =
    options?.messageFilter === "unread" ||
    options?.messageFilter === "important" ||
    options?.messageFilter === "attachments"
      ? options.messageFilter
      : "all";
  const sortOrder: MailListSortOrder = options?.sortOrder === "oldest" ? "oldest" : "latest";
  const searchField = normalizeMailSearchField(options?.searchField);
  const searchQuery = normalizeMailSearchQuery(options?.searchQuery);
  const messageFolderFilterSql =
    currentFolder === "all"
      ? "mm.direction = 'inbound' AND f.system_name NOT IN ('sent', 'drafts', 'spam', 'trash')"
      : "f.system_name = ?";
  const messageFolderFilterParams = currentFolder === "all" ? [] : [currentFolder];
  const searchClause = buildMailboxSearchSql({
    field: searchField,
    query: searchQuery
  });
  const pageSize = normalizeMailboxPageSize(options?.pageSize);

  const importantFilterSql = "mm.is_starred = 1";
  const attachmentFilterSql = `LOWER(CONCAT_WS(' ', mm.subject, mm.snippet)) REGEXP '${ATTACHMENT_MESSAGE_REGEXP}'`;
  const messageFilterSql =
    messageFilter === "unread"
      ? "AND mm.is_read = 0"
      : messageFilter === "important"
        ? `AND ${importantFilterSql}`
        : messageFilter === "attachments"
          ? `AND ${attachmentFilterSql}`
          : "";

  const [messageCountRows] = await pool.query<MailMessageCountRow[]>(
    `
      SELECT
        COUNT(*) AS all_count,
        SUM(CASE WHEN mm.is_read = 0 THEN 1 ELSE 0 END) AS unread_count,
        SUM(CASE WHEN ${importantFilterSql} THEN 1 ELSE 0 END) AS important_count,
        SUM(CASE WHEN ${attachmentFilterSql} THEN 1 ELSE 0 END) AS attachments_count
      FROM mailbox_messages mm
      ${PROJECTED_MAILBOX_FOLDER_JOIN_SQL}
      WHERE mm.mailbox_id = ?
        AND ${messageFolderFilterSql}
        AND ${PROJECTED_MAILBOX_MESSAGE_VISIBLE_SQL}
        ${searchClause.sql}
    `,
    [mailbox.id, ...messageFolderFilterParams, ...searchClause.params]
  );

  const countRow = messageCountRows[0];
  const messageFilterCounts = {
    all: Number(countRow?.all_count ?? 0),
    unread: Number(countRow?.unread_count ?? 0),
    important: Number(countRow?.important_count ?? 0),
    attachments: Number(countRow?.attachments_count ?? 0)
  } satisfies MailMessageFilterCounts;
  const totalMessages = messageFilterCounts[messageFilter];
  const totalPages = Math.max(1, Math.ceil(totalMessages / pageSize));
  const requestedPage =
    options?.page && Number.isFinite(options.page) && options.page > 0 ? Math.floor(options.page) : 1;
  const currentPage = Math.max(1, Math.min(totalPages, requestedPage));
  const offset = (currentPage - 1) * pageSize;

  const [messageRows] = await pool.query<MailMessageSummaryRow[]>(
    `
      SELECT
        mm.id,
        mm.remote_uid,
        mm.remote_folder,
        mm.subject,
        mm.from_name,
        mm.from_address,
        contact.display_name AS contact_display_name,
        contact.is_vip AS sender_is_vip,
        mm.to_addresses,
        mm.snippet,
        CASE
          WHEN mm.snippet = ''
            OR LOCATE('http', LOWER(LEFT(mm.snippet, 220))) > 0
            OR LOCATE('email template', LOWER(LEFT(mm.snippet, 220))) > 0
          THEN mm.body_html
          ELSE NULL
        END AS snippet_body_html,
        CASE
          WHEN mm.snippet = ''
            OR LOCATE('http', LOWER(LEFT(mm.snippet, 220))) > 0
            OR LOCATE('email template', LOWER(LEFT(mm.snippet, 220))) > 0
          THEN mm.body_text
          ELSE NULL
        END AS snippet_body_text,
        mm.is_read,
        mm.is_starred,
        mm.received_at,
        mm.direction,
        mat.summary_status AS ai_summary_status,
        mat.summary_transport_response AS ai_summary_response,
        mat.last_error AS ai_summary_error,
        f.system_name AS folder_system_name,
        f.name AS folder_name
      FROM mailbox_messages mm
      ${PROJECTED_MAILBOX_FOLDER_JOIN_SQL}
      LEFT JOIN mailbox_ai_assist_threads mat ON mat.source_message_id = mm.id
      LEFT JOIN mailbox_contacts contact
        ON contact.mailbox_id = mm.mailbox_id
        AND contact.email = LOWER(mm.from_address)
      WHERE mm.mailbox_id = ?
        AND ${messageFolderFilterSql}
        AND ${PROJECTED_MAILBOX_MESSAGE_VISIBLE_SQL}
        ${searchClause.sql}
        ${messageFilterSql}
      ORDER BY mm.received_at ${sortOrder === "oldest" ? "ASC" : "DESC"}, mm.id ${sortOrder === "oldest" ? "ASC" : "DESC"}
      LIMIT ?
      OFFSET ?
    `,
    [mailbox.id, ...messageFolderFilterParams, ...searchClause.params, pageSize, offset]
  );

  const messages = messageRows.map((row) => ({
    aiSummaryError: row.ai_summary_error,
    aiSummaryResponse: row.ai_summary_response,
    aiSummaryStatus: row.ai_summary_status,
    id: row.id,
    remoteFolder: row.remote_folder,
    remoteUid: row.remote_uid,
    subject: row.subject,
    fromName: row.from_name,
    fromAddress: row.from_address,
    toAddresses: row.to_addresses,
    senderContactName: row.contact_display_name?.trim() || null,
    senderIsVip: Boolean(row.sender_is_vip),
    snippet: shouldRepairMailboxSnippet(row.snippet)
      ? createMailboxSnippet({
          bodyHtml: row.snippet_body_html,
          bodyText: row.snippet_body_text ?? row.snippet
        })
      : row.snippet,
    isRead: Boolean(row.is_read),
    isStarred: Boolean(row.is_starred),
    receivedAt: row.received_at.toISOString(),
    direction: row.direction,
    folderSystemName: row.folder_system_name,
    folderName: row.folder_name
  }));

  const selectedMessageId =
    options?.messageId && messages.some((message) => message.id === options.messageId) ? options.messageId : null;

  const [selectedMessageRows] = selectedMessageId
    ? await pool.query<MailMessageDetailRow[]>(
        `
          SELECT
            mm.id,
            mm.remote_uid,
            mm.remote_folder,
            mm.subject,
            mm.from_name,
            mm.from_address,
            contact.display_name AS contact_display_name,
            contact.is_vip AS sender_is_vip,
            mm.to_addresses,
            mm.snippet,
            mm.body_text,
            mm.body_html,
            mm.message_id_header,
            (mm.raw_source IS NOT NULL) AS raw_source_available,
            NULL AS raw_source,
            mm.transport_status,
            mm.transport_response,
            mm.remote_flags,
            mm.is_read,
            mm.is_starred,
            mm.received_at,
            mm.direction,
            mat.summary_status AS ai_summary_status,
            mat.summary_transport_response AS ai_summary_response,
            mat.last_error AS ai_summary_error,
            f.system_name AS folder_system_name,
            f.name AS folder_name
          FROM mailbox_messages mm
          ${PROJECTED_MAILBOX_FOLDER_JOIN_SQL}
          LEFT JOIN mailbox_ai_assist_threads mat ON mat.source_message_id = mm.id
          LEFT JOIN mailbox_contacts contact
            ON contact.mailbox_id = mm.mailbox_id
            AND contact.email = LOWER(mm.from_address)
          WHERE mm.mailbox_id = ?
            AND mm.id = ?
            AND ${messageFolderFilterSql}
            AND ${PROJECTED_MAILBOX_MESSAGE_VISIBLE_SQL}
          LIMIT 1
        `,
        [mailbox.id, selectedMessageId, ...messageFolderFilterParams]
      )
    : [[] as MailMessageDetailRow[]];
  const selectedMessageRow = selectedMessageRows[0] ?? null;

  const [selectedMessageRawAttachmentRows, selectedMessageLinkedAttachments, selectedMessageLogs] =
    selectedMessageRow
      ? await Promise.all([
          getMailboxMessageAttachmentMetadata({
            mailboxId: mailbox.id,
            messageId: selectedMessageRow.id
          }).catch(() => []),
          getLinkedComposeUploadAttachmentsByMessageId(selectedMessageRow.id, {
            includeUnpublishedOwnerCopy: selectedMessageRow.direction === "draft" ||
              (selectedMessageRow.direction === "outbound" && selectedMessageRow.transport_status === "queued"),
          }).catch(() => []),
          getDeliveryLogsByMessageId(selectedMessageRow.id).catch(() => [])
        ])
      : [[], [], []];
  const selectedMessageRawAttachments = selectedMessageRow
    ? classifyBodyInlineAttachments(
        selectedMessageRawAttachmentRows,
        selectedMessageRow.body_html
      )
    : [];
  const largeTokens = selectedMessageRow
    ? getOfficialLargeAttachmentTokens(selectedMessageRow.body_html, selectedMessageRow.body_text)
    : [];
  const detectedLargeAttachments = await getPublishedLargeComposeAttachmentsByTokens(largeTokens).catch(() => []);
  const linkedUrls = new Set(selectedMessageLinkedAttachments.map((attachment) => attachment.externalUrl));
  const selectedMessageAttachments: MailMessageAttachment[] = [
    ...selectedMessageRawAttachments.map((attachment) => ({
      ...attachment,
      externalUrl: null
    })),
    ...selectedMessageLinkedAttachments,
    ...detectedLargeAttachments.filter((attachment) => !linkedUrls.has(attachment.externalUrl)),
  ];
  const displayBody = selectedMessageRow
    ? stripDisplayedLargeAttachmentSections({
        bodyHtml: selectedMessageRow.body_html,
        bodyText: selectedMessageRow.body_text,
        availableTokens: selectedMessageAttachments
          .filter((attachment) => attachment.isLargeAttachment && attachment.externalUrl)
          .map((attachment) => attachment.externalUrl!.slice(-48)),
      })
    : null;

  const selectedMessage = selectedMessageRow
    ? {
        attachments: selectedMessageAttachments,
        aiSummaryError: selectedMessageRow.ai_summary_error,
        aiSummaryResponse: selectedMessageRow.ai_summary_response,
        aiSummaryStatus: selectedMessageRow.ai_summary_status,
        id: selectedMessageRow.id,
        subject: selectedMessageRow.subject,
        fromName: selectedMessageRow.from_name,
        fromAddress: selectedMessageRow.from_address,
        toAddresses: selectedMessageRow.to_addresses,
        senderContactName: selectedMessageRow.contact_display_name?.trim() || null,
        senderIsVip: Boolean(selectedMessageRow.sender_is_vip),
        snippet: selectedMessageRow.snippet,
        isRead: Boolean(selectedMessageRow.is_read),
        isStarred: Boolean(selectedMessageRow.is_starred),
        receivedAt: selectedMessageRow.received_at.toISOString(),
        direction: selectedMessageRow.direction,
        folderSystemName: selectedMessageRow.folder_system_name,
        folderName: selectedMessageRow.folder_name,
        bodyHtml: options?.includeOriginalBodyHtml
          ? sanitizeMailboxHtml(selectedMessageRow.body_html, selectedMessageRow.body_text)
          : null,
        bodyHtmlDisplay: buildMailboxHtmlDisplay({
          attachments: selectedMessageAttachments,
          bodyHtml: displayBody?.bodyHtml,
          bodyText: displayBody?.bodyText,
          messageId: selectedMessageRow.id
        }),
        bodyText: displayBody?.bodyText ?? "",
        hasUnresolvedLargeAttachmentLinks: hasUnresolvedOfficialLargeAttachmentLinks({
          attachments: selectedMessageAttachments,
          bodyHtml: selectedMessageRow.body_html,
          bodyText: selectedMessageRow.body_text,
        }),
        messageIdHeader: selectedMessageRow.message_id_header,
        rawSourceAvailable: Boolean(selectedMessageRow.raw_source_available),
        transportStatus: selectedMessageRow.transport_status,
        transportResponse: selectedMessageRow.transport_response
      }
    : null;
  const quota = await quotaPromise;

  return {
    mailbox: mailbox.email,
    mailboxQuotaBytes: quota?.limitBytes ?? null,
    mailboxQuotaIsUnlimited: quota?.isUnlimited ?? false,
    mailboxQuotaUsagePercent: quota?.usagePercent ?? null,
    mailboxQuotaUsedBytes: quota?.usedBytes ?? null,
    domain: mailbox.domain,
    lastSyncAt: normalizeDate(mailbox.last_sync_at),
    status: mailbox.status,
    syncError: mailbox.last_sync_error,
    spamSettings,
    folders,
    currentFolder,
    messageFilter,
    searchField,
    searchQuery,
    sortOrder,
    messageFilterCounts,
    currentPage,
    totalPages,
    totalMessages,
    pageSize,
    messages,
    selectedMessage,
    selectedMessageLogs
  } satisfies MailAppOverview;
}

type ForwardedMailboxInlineAttachment = {
  attachmentIndex: number;
  messageId: number;
  source: string;
};

type ResolvedForwardedMailboxInlineAttachment = ComposeRawAttachment & {
  contentId: string;
};

function extractForwardedMailboxInlineAttachments(bodyHtml: string | null) {
  if (!bodyHtml) {
    return [] satisfies ForwardedMailboxInlineAttachment[];
  }

  const references: ForwardedMailboxInlineAttachment[] = [];

  for (const match of bodyHtml.matchAll(/\bsrc\s*=\s*(["'])(\/api\/mailbox\/attachment\?[^"']+)\1/gi)) {
    const source = match[2];

    if (!source) {
      continue;
    }

    const parsed = new URL(source.replace(/&amp;/gi, "&"), "https://officialmail.invalid");
    const messageId = Number(parsed.searchParams.get("message"));
    const attachmentIndex = Number(parsed.searchParams.get("index"));

    if (
      parsed.pathname !== "/api/mailbox/attachment" ||
      parsed.searchParams.get("mode") !== "inline" ||
      !Number.isInteger(messageId) ||
      messageId < 1 ||
      !Number.isInteger(attachmentIndex)
    ) {
      continue;
    }

    references.push({ attachmentIndex, messageId, source });
  }

  return references;
}

async function resolveForwardedMailboxInlineAttachments(input: {
  bodyHtml: string;
  email: string;
  mailboxId: number;
}) {
  let bodyHtml = input.bodyHtml;
  const attachments: ComposeRawAttachment[] = [];
  const resolvedByAttachment = new Map<string, ResolvedForwardedMailboxInlineAttachment>();

  for (const reference of extractForwardedMailboxInlineAttachments(bodyHtml)) {
    const key = `${reference.messageId}:${reference.attachmentIndex}`;
    let attachment = resolvedByAttachment.get(key);

    if (!attachment) {
      const sourceAttachment = await getMailboxAttachmentByEmail(
        input.email,
        reference.messageId,
        reference.attachmentIndex,
        { mailboxId: input.mailboxId },
      );

      if (!sourceAttachment?.contentType.toLowerCase().startsWith("image/")) {
        continue;
      }

      attachment = {
        content: sourceAttachment.content,
        contentDisposition: "inline",
        contentId: `forwarded-${randomUUID()}@officialmail-inline`,
        contentType: sourceAttachment.contentType,
        filename: sourceAttachment.filename,
      };
      resolvedByAttachment.set(key, attachment);
      attachments.push(attachment);
    }

    bodyHtml = bodyHtml.split(reference.source).join(`cid:${attachment.contentId}`);
  }

  return { attachments, bodyHtml };
}

export async function createMailboxMessage(
  email: string,
  input: {
    attachmentUploadIds?: number[];
    largeAttachmentUploadIds?: number[];
    bcc?: string;
    bodyHtml?: string;
    cc?: string;
    inlineImageUploadIds?: number[];
    firstAttachmentFilename?: string;
    onIndividualQueueProgress?: (progress: { completedCount: number; totalCount: number }) => Promise<void> | void;
    onStageChange?: (stage: "sending" | "uploading") => Promise<void> | void;
    onUploadProgress?: (snapshot: ComposeUploadFinalizeProgressSnapshot) => Promise<void> | void;
    rawAttachmentFiles?: Array<{
      buffer: Buffer;
      contentType: string;
      filename: string;
      mode?: ComposeAttachmentMode;
      sizeBytes: number;
    }>;
    senderDisplayName?: string;
    sendIndividually?: boolean;
    sendSource?: "api" | "auto-send" | "compose";
    sourceDraftMessageId?: number | null;
    to: string;
    subject: string;
    body: string;
    intent: "send" | "draft";
    waitForDelivery?: boolean;
    deferredDraft?: boolean;
    messageIdHeader?: string;
    composeExitKey?: string;
  }
) {
  const user = await getUserByEmail(email);

  if (!user) {
    throw new Error("user-not-found");
  }

  const senderDisplayName = input.senderDisplayName?.trim().slice(0, 191) || user.display_name;

  const to = input.to.trim().toLowerCase();
  const cc = (input.cc ?? "").trim().toLowerCase();
  const bcc = (input.bcc ?? "").trim().toLowerCase();
  const htmlBody = (input.bodyHtml ?? "").trim();
  const attachmentUploadIds = (input.attachmentUploadIds ?? []).filter((value) => Number.isInteger(value) && value > 0);
  const largeAttachmentUploadIds = (input.largeAttachmentUploadIds ?? []).filter((value) => Number.isInteger(value) && value > 0);
  const rawAttachmentFiles = (input.rawAttachmentFiles ?? []).filter((file) => file.buffer.length > 0);
  const inlineImageUploadIds = (input.inlineImageUploadIds ?? []).filter(
    (value) => Number.isInteger(value) && value > 0
  );
  const toRecipients = normalizeRecipientList(to);
  const ccRecipients = cc ? normalizeRecipientList(cc) : [];
  const bccRecipients = bcc ? normalizeRecipientList(bcc) : [];
  const shouldSendIndividually = input.intent === "send" && input.sendIndividually === true && toRecipients.length > 1;
  const enforceBurstCooldown = input.sendSource !== "auto-send" && !shouldSendIndividually;
  const requestedSendCount = shouldSendIndividually ? toRecipients.length : 1;
  const recipients = [...toRecipients, ...ccRecipients, ...bccRecipients].filter(
    (recipient, index, array) => array.indexOf(recipient) === index
  );

  const mailbox = await getLatestMailboxByUserId(user.id);

  if (!mailbox) {
    throw new Error("mailbox-not-found");
  }

  if (input.deferredDraft && mailbox.status !== "active") throw new Error("mailbox-inactive");

  let body = input.body.trim() || htmlToPlainText(htmlBody);
  let storedBodyHtml = sanitizeMailboxHtml(htmlBody, body) ?? plainTextToHtml(body);
  let rawAttachments = await getReplacedDraftAttachments(mailbox, input.sourceDraftMessageId);
  const forwardedInlineAttachments = await resolveForwardedMailboxInlineAttachments({
    bodyHtml: storedBodyHtml,
    email,
    mailboxId: mailbox.id,
  });
  storedBodyHtml = forwardedInlineAttachments.bodyHtml;
  rawAttachments = [...rawAttachments, ...forwardedInlineAttachments.attachments];
  const preuploadedLargeAttachments = await getLargeComposeAttachmentsForUserEmail(email, largeAttachmentUploadIds);
  let finalizedLargeAttachmentIds: number[] = preuploadedLargeAttachments.map((upload) => upload.id);
  const firstUploadedFilename = attachmentUploadIds.length > 0
    ? await getFirstComposeAttachmentFilenameForUserEmail(email, attachmentUploadIds)
    : null;
  const subject = resolveComposeSubjectFromKnownAttachments({
    subject: input.subject,
    firstAttachmentFilename: input.firstAttachmentFilename,
    attachmentFilenames: [
      ...rawAttachments.map((attachment) => attachment.filename),
      ...(firstUploadedFilename ? [firstUploadedFilename] : []),
      ...rawAttachmentFiles.map((attachment) => attachment.filename),
      ...preuploadedLargeAttachments.map((attachment) => attachment.filename),
    ],
  });
  if (subject.length > 255) {
    throw new Error("compose-subject-too-long");
  }
  let finalizedInlineImageUploadIds: number[] = [];
  const hasPendingUploads =
    attachmentUploadIds.length > 0 || inlineImageUploadIds.length > 0 ||
    rawAttachmentFiles.length > 0 || rawAttachments.length > 0 || preuploadedLargeAttachments.length > 0;
  const hasInlineImageContent = Boolean(
    storedBodyHtml &&
    (/\bsrc=(['"])data:image\//i.test(storedBodyHtml) || /\/api\/mailbox\/uploads\/[a-f0-9]{48}/i.test(storedBodyHtml))
  );
  const hasBodyContent = Boolean(body.trim() || storedBodyHtml?.trim());

  if (
    (input.intent === "send" && recipients.length === 0) ||
    (input.intent === "send" && !subject) ||
    (input.intent === "send" && !hasBodyContent && !hasPendingUploads)
  ) {
    throw new Error("compose-required");
  }

  if (input.intent === "send") {
    await assertMailboxSendAllowed({
      enforceBurstCooldown,
      mailboxId: mailbox.id,
      ownerUserId: user.id,
      requestedSendCount
    });
  }

  if (inlineImageUploadIds.length > 0 || hasInlineImageContent) {
    const finalizedInlineImages = await finalizeComposeInlineImagesForUserEmail({
      bodyHtml: storedBodyHtml,
      email,
      inlineImageUploadIds
    });
    storedBodyHtml = finalizedInlineImages.bodyHtml ?? storedBodyHtml;
    finalizedInlineImageUploadIds = finalizedInlineImages.finalizedUploadIds;
  }

  if (attachmentUploadIds.length > 0 || rawAttachmentFiles.length > 0) {
    const rawUploadPayload = await buildComposeRawAttachmentsForUserEmail({
      attachmentUploadIds,
      bodyHtml: storedBodyHtml,
      email,
      inlineImageUploadIds: []
    });

    rawAttachments = [...rawAttachments, ...rawUploadPayload.attachments];
    storedBodyHtml = rawUploadPayload.bodyHtml ?? storedBodyHtml;
  }

  const growth = await hasActiveGrowthPlanByOwnerEmail(email);
  if (!growth && preuploadedLargeAttachments.length > 0) {
    throw new Error("attachment-growth-required");
  }
  const candidates = [
    ...rawAttachments.map((attachment) => ({ attachment, requestedMode: "regular" as ComposeAttachmentMode })),
    ...rawAttachmentFiles.map((file) => ({
      attachment: {
        content: file.buffer,
        contentDisposition: "attachment" as const,
        contentId: null,
        contentType: file.contentType || "application/octet-stream",
        filename: file.filename,
      } satisfies ComposeRawAttachment,
      requestedMode: file.mode ?? "regular" as ComposeAttachmentMode,
    })),
  ];
  const regularAttachments: ComposeRawAttachment[] = [];
  const largeAttachments: Array<{ buffer: Buffer; contentType: string; filename: string }> = [];
  let regularTotalBytes = 0;
  for (const { attachment, requestedMode } of candidates) {
    const mode = resolveComposeAttachmentMode({
      fileSizeBytes: attachment.content.length,
      regularTotalBytes,
      growth,
      requestedMode,
    });
    if (mode === "regular") {
      regularAttachments.push(attachment);
      regularTotalBytes += attachment.content.length;
    } else {
      largeAttachments.push({
        buffer: attachment.content,
        contentType: attachment.contentType,
        filename: attachment.filename,
      });
    }
  }
  rawAttachments = regularAttachments;
  let largeUploads = preuploadedLargeAttachments;
  if (largeAttachments.length > 0) {
    const newlyUploaded = await storeLargeComposeAttachmentsForUserEmail({
      email,
      files: largeAttachments,
    });
    largeUploads = [...largeUploads, ...newlyUploaded];
    finalizedLargeAttachmentIds = largeUploads.map((upload) => upload.id);
  }
  if (largeUploads.length > 0) {
    const linkedBody = appendLargeComposeAttachmentLinks({
      bodyHtml: storedBodyHtml,
      bodyText: body,
      uploads: largeUploads,
    });
    storedBodyHtml = linkedBody.bodyHtml ?? storedBodyHtml;
    body = linkedBody.bodyText;
  }

  const targetFolder = input.intent === "draft" ? "drafts" : "sent";
  const snippet = body.replace(/\s+/g, " ").slice(0, 140);
  const messageIdHeader = input.intent === "draft" && input.messageIdHeader
    ? input.messageIdHeader : createMessageIdHeader(mailbox.domain);
  const visibleRecipients = [toRecipients.join(", "), ccRecipients.length > 0 ? `참조: ${ccRecipients.join(", ")}` : ""]
    .filter(Boolean)
    .join(" / ");
  const logRecipients = recipients.join(", ");
  const rawSource = buildRawSource({
    attachments: rawAttachments,
    bccAddresses: input.intent === "draft" ? bccRecipients.join(", ") : "",
    bodyHtml: storedBodyHtml || null,
    bodyText: body,
    ccAddresses: ccRecipients.join(", "),
    messageIdHeader,
    fromName: senderDisplayName,
    fromAddress: mailbox.email,
    toAddresses: toRecipients.join(", "),
    subject
  });
  const visibleRecipientsLabel = visibleRecipients || toRecipients.join(", ");

  if (input.intent === "send") {
    await input.onStageChange?.("sending");
  }

  if (input.intent === "draft") {
    const credentials = toMailboxCredentials(mailbox);
    let remoteFolderName = "Drafts";
    let remoteUid: number | null = null;
    let transportResponse = "";
    const appendResult = input.deferredDraft ? null : await appendDraftMessage(credentials, rawSource);
    remoteFolderName = appendResult?.destination ?? remoteFolderName;
    remoteUid = appendResult?.uid ?? null;
    transportResponse = appendResult?.uid
      ? "임시보관함에 임시저장을 완료했습니다."
      : "임시보관함에 임시저장을 요청했습니다.";

    const result = await withRetryableMailboxTransaction(async (connection) => {
      await ensureLocalMailboxFolders(connection, mailbox.id);

      const [folderRows] = await connection.query<(RowDataPacket & { id: number; remote_name: string | null })[]>(
        "SELECT id, remote_name FROM mailbox_folders WHERE mailbox_id = ? AND system_name = ? LIMIT 1",
        [mailbox.id, targetFolder]
      );

      const folder = folderRows[0];
      const folderId = folder?.id;

      if (!folderId) {
        throw new Error("folder-not-found");
      }

      if (!folder.remote_name || folder.remote_name !== remoteFolderName) {
        await connection.query(
          `
            UPDATE mailbox_folders
            SET remote_name = ?, updated_at = NOW()
            WHERE id = ?
          `,
          [remoteFolderName, folderId]
        );
      }

      const mailboxMessageId = await upsertMailboxMessageRow(connection, {
        bodyHtml: storedBodyHtml || null,
        bodyText: body,
        direction: "draft",
        folderId,
        fromAddress: mailbox.email,
        fromName: senderDisplayName,
        isRead: true,
        isStarred: false,
        mailboxId: mailbox.id,
        messageIdHeader,
        preserveExistingTransport: false,
        rawSource,
        receivedAt: new Date(),
        remoteFlags: "\\Draft",
        remoteFolder: remoteFolderName,
        remoteUid,
        snippet,
        subject,
        toAddresses: visibleRecipientsLabel,
        transportResponse,
        transportStatus: input.deferredDraft ? "queued" : "saved"
      });

      await logMailboxDelivery(connection, {
        mailboxMessageId,
        mailboxId: mailbox.id,
        action: "draft",
        status: input.deferredDraft ? "queued" : "saved",
        transport: "imap",
        sourceAddress: mailbox.email,
        targetAddress: logRecipients,
        subject,
        messageIdHeader,
        responseText: transportResponse
      });

      await linkComposeUploadIdsToMessageId(connection, [...finalizedInlineImageUploadIds, ...finalizedLargeAttachmentIds], mailboxMessageId);

      if (input.composeExitKey) await connection.query(
        "INSERT INTO mailbox_compose_exit_receipts (owner_user_id, exit_key, mailbox_message_id) VALUES (?, ?, ?)",
        [user.id, input.composeExitKey, mailboxMessageId],
      );

      if (input.sourceDraftMessageId) {
        await relinkComposeUploadsToMessageId(connection, [input.sourceDraftMessageId], mailboxMessageId);
        if (input.deferredDraft) {
          // Reserve the replaced draft immediately so refresh/sync cannot show
          // both copies while the new draft's IMAP APPEND is pending.
          const [oldRows] = await connection.query<MailboxMoveMessageRow[]>(
            "SELECT id, folder_id, direction, from_address, remote_folder, remote_uid FROM mailbox_messages WHERE id = ? AND mailbox_id = ? FOR UPDATE",
            [input.sourceDraftMessageId, mailbox.id],
          );
          await queueMailboxRowsToFolder(connection, mailbox.id, await getMailboxFolderRows(connection, mailbox.id), oldRows, "trash");
        }
      }

      return { mailboxMessageId };
    });

    await updateMailboxSyncState(mailbox.id, {
      lastSyncAt: new Date(),
      lastSyncError: null
    }).catch((error) => {
      const code = error && typeof error === "object" && "code" in error
        ? String(error.code)
        : "unknown";
      console.warn("[mailbox-compose-draft] saved draft sync metadata update failed", { code });
    });

    if (!input.deferredDraft) await removeReplacedDraftMessage(mailbox, input.sourceDraftMessageId).catch((error) => {
      console.error("Replaced draft cleanup failed", error);
    });

    return {
      folder: targetFolder,
      messageId: result.mailboxMessageId,
      success: "draft" as const
    };
  }

  // Validate stored credentials before queueing.
  toMailboxCredentials(mailbox);

  const queuedResult = await withRetryableMailboxTransaction(async (connection) => {
    await ensureLocalMailboxFolders(connection, mailbox.id);
    await assertMailboxSendAllowed({
      activateCooldownOnBurst: true,
      connection,
      enforceBurstCooldown,
      mailboxId: mailbox.id,
      ownerUserId: user.id,
      requestedSendCount
    });

    const [folderRows] = await connection.query<(RowDataPacket & { id: number; remote_name: string | null })[]>(
      "SELECT id, remote_name FROM mailbox_folders WHERE mailbox_id = ? AND system_name = 'sent' LIMIT 1",
      [mailbox.id]
    );

    const folderId = folderRows[0]?.id;
    const remoteFolderName = folderRows[0]?.remote_name || "Sent";

    if (!folderId) {
      throw new Error("folder-not-found");
    }

    const sendTargets = buildQueuedSendTargets({
      attachments: rawAttachments,
      bccRecipients,
      body,
      bodyHtml: storedBodyHtml || null,
      ccRecipients,
      fromAddress: mailbox.email,
      fromName: senderDisplayName,
      sendIndividually: shouldSendIndividually,
      subject,
      toRecipients
    });
    const queuedEntries: Array<{
      envelopeRecipients: string[];
      logRecipients: string;
      mailboxMessageId: number;
      messageIdHeader: string;
      rawSource: string;
      visibleRecipients: string;
    }> = [];

    if (shouldSendIndividually) {
      await input.onIndividualQueueProgress?.({
        completedCount: 0,
        totalCount: sendTargets.length
      });
    }

    for (const [sendTargetIndex, sendTarget] of sendTargets.entries()) {
      const mailboxMessageId = await upsertMailboxMessageRow(connection, {
        bodyHtml: storedBodyHtml || null,
        bodyText: body,
        direction: "outbound",
        folderId,
        fromAddress: mailbox.email,
        fromName: senderDisplayName,
        isRead: true,
        isStarred: false,
        mailboxId: mailbox.id,
        messageIdHeader: sendTarget.messageIdHeader,
        preserveExistingTransport: false,
        rawSource: sendTarget.rawSource,
        receivedAt: new Date(),
        remoteFlags: "\\Seen",
        remoteFolder: remoteFolderName,
        remoteUid: null,
        snippet,
        subject,
        toAddresses: sendTarget.visibleRecipients,
        transportResponse: "메일 전송 대기열에 등록되었습니다.",
        transportStatus: "queued"
      });

      await logMailboxDelivery(connection, {
        mailboxMessageId,
        mailboxId: mailbox.id,
        action: "send",
        status: "queued",
        transport: "smtp",
        sourceAddress: mailbox.email,
        targetAddress: sendTarget.logRecipients,
        subject,
        messageIdHeader: sendTarget.messageIdHeader,
        responseText: "메일 전송 대기열에 등록되었습니다."
      });

      queuedEntries.push({
        envelopeRecipients: sendTarget.envelopeRecipients,
        logRecipients: sendTarget.logRecipients,
        mailboxMessageId,
        messageIdHeader: sendTarget.messageIdHeader,
        rawSource: sendTarget.rawSource,
        visibleRecipients: sendTarget.visibleRecipients
      });

      if (shouldSendIndividually) {
        await input.onIndividualQueueProgress?.({
          completedCount: sendTargetIndex + 1,
          totalCount: sendTargets.length
        });
      }
    }

    if (queuedEntries[0]) {
      await linkComposeUploadIdsToMessageId(
        connection,
        [...finalizedInlineImageUploadIds, ...finalizedLargeAttachmentIds],
        queuedEntries[0].mailboxMessageId
      );

      if (input.sourceDraftMessageId) {
        await relinkComposeUploadsToMessageId(
          connection,
          [input.sourceDraftMessageId],
          queuedEntries[0].mailboxMessageId
        );
      }
    }

    return { queuedEntries };
  });

  const shouldWaitForDelivery = input.waitForDelivery !== false;

  if (shouldWaitForDelivery) {
    let firstFailure: { errorMessage: string } | null = null;

    for (const queuedEntry of queuedResult.queuedEntries) {
      const deliveryResult = await finalizeQueuedMailboxSend({
        logRecipients: queuedEntry.logRecipients,
        mailbox,
        mailboxMessageId: queuedEntry.mailboxMessageId,
        messageIdHeader: queuedEntry.messageIdHeader,
        rawSource: queuedEntry.rawSource,
        recipients: queuedEntry.envelopeRecipients,
        subject,
        visibleRecipients: queuedEntry.visibleRecipients
      });

      if (deliveryResult.status === "failed" && !firstFailure) {
        firstFailure = {
          errorMessage: deliveryResult.errorMessage
        };
      }
    }

    if (firstFailure) {
      throw new Error(firstFailure.errorMessage);
    }
  }

  await updateMailboxSyncState(mailbox.id, {
    lastSyncAt: new Date(),
    lastSyncError: null
  }).catch((error) => {
    // The send is already committed to the durable queue. A sync timestamp
    // failure must not tell the caller it failed and invite a duplicate send.
    console.error("Queued mail sync-state update failed", error);
  });

  await removeReplacedDraftMessage(mailbox, input.sourceDraftMessageId).catch((error) => {
    console.error("Replaced draft cleanup failed", error);
  });

  return {
    folder: targetFolder,
    messageId:
      queuedResult.queuedEntries.length === 1 ? (queuedResult.queuedEntries[0]?.mailboxMessageId ?? null) : null,
    success: shouldSendIndividually
      ? (shouldWaitForDelivery ? "send-individual" : "send-individual-queued")
      : (shouldWaitForDelivery ? "send" : "send-queued")
  };
}

async function getRecentMailboxRecipientSuggestions(mailboxId: number, limit: number) {
  type RecentRecipientRow = RowDataPacket & { id: number; received_at: Date; to_addresses: string };
  const suggestions: MailRecipientSuggestion[] = [];
  const seen = new Set<string>();
  let cursor: { id: number; received_at: Date } | null = null;

  // Scan by newest sent message, not by a fixed history window: repeated
  // sends to one address must not hide older distinct recipients.
  while (suggestions.length < limit) {
    const result: [RecentRecipientRow[], unknown] = await getDbPool().query<RecentRecipientRow[]>(
      `
        SELECT mm.id, mm.received_at, mm.to_addresses
        FROM mailbox_messages mm
        WHERE mm.mailbox_id = ?
          AND mm.direction = 'outbound'
          AND mm.transport_status = 'sent'
          ${cursor ? "AND (mm.received_at < ? OR (mm.received_at = ? AND mm.id < ?))" : ""}
        ORDER BY mm.received_at DESC, mm.id DESC
        LIMIT 100
      `,
      cursor ? [mailboxId, cursor.received_at, cursor.received_at, cursor.id] : [mailboxId],
    );

    const rows: RecentRecipientRow[] = result[0];
    for (const row of rows) {
      for (const recipient of splitRecipientList(row.to_addresses)) {
        if (seen.has(recipient)) continue;
        seen.add(recipient);
        suggestions.push({ displayName: null, email: recipient, kind: "history", label: recipient });
        if (suggestions.length >= limit) return suggestions;
      }
    }

    if (rows.length < 100) break;
    const last: RecentRecipientRow = rows[rows.length - 1];
    cursor = { id: last.id, received_at: last.received_at };
  }

  return suggestions;
}

export async function getMailboxRecipientSuggestionsByEmail(
  email: string,
  input: {
    query: string;
    limit?: number;
  }
) {
  const user = await getUserByEmail(email);

  if (!user) {
    return [] satisfies MailRecipientSuggestion[];
  }

  const mailbox = await getLatestMailboxByUserId(user.id);

  if (!mailbox) {
    return [] satisfies MailRecipientSuggestion[];
  }

  const normalizedQuery = normalizeMailSearchQuery(input.query);
  const limit = Math.max(1, Math.min(10, input.limit ?? 10));

  if (!normalizedQuery) {
    return getRecentMailboxRecipientSuggestions(mailbox.id, limit);
  }

  const queryLower = normalizedQuery.toLowerCase();
  const likeQuery = `%${escapeSqlLike(queryLower)}%`;
  const pool = getDbPool();
  const [memberRowsResult, historyRowsResult] = await Promise.all([
    pool.query<MailRecipientDirectoryRow[]>(
      `
        SELECT u.display_name, m.email
        FROM mailboxes m
        INNER JOIN domains d ON d.id = m.domain_id
        INNER JOIN users u ON u.id = m.user_id
        WHERE d.domain = ?

        UNION ALL

        SELECT mtm.display_name, mtm.email
        FROM managed_team_mailboxes mtm
        INNER JOIN domains d ON d.id = mtm.domain_id
        WHERE d.domain = ?
          AND mtm.status = 'active'
      `,
      [mailbox.domain, mailbox.domain]
    ),
    pool.query<MailRecipientSuggestionRow[]>(
      `
        SELECT
          mm.from_name,
          mm.from_address,
          mm.to_addresses,
          mm.received_at
        FROM mailbox_messages mm
        WHERE mm.mailbox_id = ?
          AND (
            LOWER(COALESCE(mm.from_name, '')) LIKE ? ESCAPE '\\\\'
            OR LOWER(mm.from_address) LIKE ? ESCAPE '\\\\'
            OR LOWER(mm.to_addresses) LIKE ? ESCAPE '\\\\'
          )
        ORDER BY mm.received_at DESC, mm.id DESC
        LIMIT 60
      `,
      [mailbox.id, likeQuery, likeQuery, likeQuery]
    )
  ]);
  const [memberRows] = memberRowsResult;
  const [historyRows] = historyRowsResult;
  const suggestions: MailRecipientSuggestion[] = [];
  const seen = new Set<string>();

  const pushSuggestion = (suggestion: {
    displayName?: string | null;
    email: string;
    kind: MailRecipientSuggestion["kind"];
  }) => {
    const normalizedEmail = suggestion.email.trim().toLowerCase();

    if (!normalizedEmail || seen.has(normalizedEmail)) {
      return;
    }

    seen.add(normalizedEmail);
    suggestions.push({
      displayName: suggestion.displayName?.trim() ?? null,
      email: normalizedEmail,
      kind: suggestion.kind,
      label: formatRecipientSuggestionLabel(normalizedEmail, suggestion.displayName)
    });
  };

  for (const row of memberRows) {
    const displayName = row.display_name?.trim() ?? null;
    const memberEmail = row.email.trim().toLowerCase();

    if (!memberEmail) {
      continue;
    }

    if (!memberEmail.includes(queryLower) && !(displayName ?? "").toLowerCase().includes(queryLower)) {
      continue;
    }

    pushSuggestion({
      displayName,
      email: memberEmail,
      kind: "member"
    });

    if (suggestions.length >= limit) {
      return suggestions.slice(0, limit);
    }
  }

  for (const row of historyRows) {
    const senderName = row.from_name?.trim() ?? null;
    const senderAddress = row.from_address.trim().toLowerCase();

    if (
      senderAddress &&
      (senderAddress.includes(queryLower) || (senderName ?? "").toLowerCase().includes(queryLower))
    ) {
      pushSuggestion({
        displayName: senderName,
        email: senderAddress,
        kind: "history"
      });
    }

    if (suggestions.length >= limit) {
      break;
    }

    for (const recipient of splitRecipientList(row.to_addresses)) {
      if (!recipient.includes(queryLower)) {
        continue;
      }

      pushSuggestion({
        displayName: null,
        email: recipient,
        kind: "history"
      });

      if (suggestions.length >= limit) {
        break;
      }
    }

    if (suggestions.length >= limit) {
      break;
    }
  }

  return suggestions.slice(0, limit);
}

export async function getMailboxSearchSuggestionsByEmail(
  email: string,
  input: {
    folder?: string;
    query: string;
    limit?: number;
  }
) {
  const user = await getUserByEmail(email);

  if (!user) {
    return [] satisfies MailSearchSuggestion[];
  }

  const mailbox = await getLatestMailboxByUserId(user.id);

  if (!mailbox) {
    return [] satisfies MailSearchSuggestion[];
  }

  const normalizedQuery = normalizeMailSearchQuery(input.query);

  if (!normalizedQuery) {
    return [] satisfies MailSearchSuggestion[];
  }

  const currentFolder = input.folder?.trim() || "inbox";
  const messageFolderFilterSql =
    currentFolder === "all"
      ? "mm.direction = 'inbound' AND f.system_name NOT IN ('sent', 'drafts', 'spam', 'trash')"
      : "f.system_name = ?";
  const messageFolderFilterParams = currentFolder === "all" ? [] : [currentFolder];
  const searchClause = buildMailboxSearchSql({
    field: "all",
    query: normalizedQuery
  });
  const [rows] = await getDbPool().query<MailSearchSuggestionRow[]>(
    `
      SELECT
        mm.id,
        mm.subject,
        mm.from_name,
        mm.from_address,
        mm.to_addresses,
        mm.snippet,
        mm.body_text,
        mm.received_at
      FROM mailbox_messages mm
      INNER JOIN mailbox_folders f ON f.id = mm.folder_id
      WHERE mm.mailbox_id = ?
        AND ${messageFolderFilterSql}
        AND ${PENDING_MAILBOX_MOVE_EXCLUSION_SQL}
        ${searchClause.sql}
      ORDER BY mm.received_at DESC, mm.id DESC
      LIMIT 40
    `,
    [mailbox.id, ...messageFolderFilterParams, ...searchClause.params]
  );

  const limit = Math.max(1, Math.min(10, input.limit ?? 8));
  const suggestions: MailSearchSuggestion[] = [];
  const seen = new Set<string>();
  const queryLower = normalizedQuery.toLowerCase();

  const pushSuggestion = (suggestion: MailSearchSuggestion | null) => {
    if (!suggestion) {
      return;
    }

    const key = `${suggestion.field}:${suggestion.value.toLowerCase()}`;

    if (seen.has(key)) {
      return;
    }

    seen.add(key);
    suggestions.push(suggestion);
  };

  for (const row of rows) {
    const senderName = row.from_name?.trim() ?? "";
    const senderAddress = row.from_address.trim();
    const senderPreview = senderName ? `${senderName} <${senderAddress}>` : senderAddress;

    if (senderName.toLowerCase().includes(queryLower)) {
      pushSuggestion({
        field: "from",
        messageId: row.id,
        preview: senderPreview,
        receivedAt: row.received_at.toISOString(),
        value: senderName
      });
    }

    if (senderAddress.toLowerCase().includes(queryLower)) {
      pushSuggestion({
        field: "from",
        messageId: row.id,
        preview: senderPreview,
        receivedAt: row.received_at.toISOString(),
        value: senderAddress
      });
    }

    for (const recipient of splitRecipientList(row.to_addresses)) {
      if (!recipient.includes(queryLower)) {
        continue;
      }

      pushSuggestion({
        field: "to",
        messageId: row.id,
        preview: row.to_addresses,
        receivedAt: row.received_at.toISOString(),
        value: recipient
      });
    }

    const subject = row.subject.trim();

    if (subject.toLowerCase().includes(queryLower)) {
      pushSuggestion({
        field: "subject",
        messageId: row.id,
        preview: subject,
        receivedAt: row.received_at.toISOString(),
        value: subject
      });
    }

    const bodySource = `${row.snippet} ${row.body_text}`.trim();

    if (bodySource.toLowerCase().includes(queryLower)) {
      const matchIndex = bodySource.toLowerCase().indexOf(queryLower);
      const preview = bodySource.slice(Math.max(0, matchIndex - 24), matchIndex + 56).trim();

      pushSuggestion({
        field: "body",
        messageId: row.id,
        preview,
        receivedAt: row.received_at.toISOString(),
        value: normalizedQuery
      });
    }

    if (suggestions.length >= limit) {
      break;
    }
  }

  return suggestions.slice(0, limit);
}
