/**
 * Sync manager — extracted from android-bootstrap.ts for use in both
 * main-thread and Worker contexts.
 *
 * Platform-specific dependencies are injected:
 *   - emitEvent: posts events to the UI
 *   - vlog: verbose remote logging
 *   - createTcpTransport: factory for TCP transport (BridgeTcpTransport or WorkerTcpTransport)
 */
import { GmailApiWebProvider } from "./gmail-api-web.js";
import { ImapWebProvider } from "./imap-web-provider.js";
import { SmtpClient } from "@bobfrankston/smtp-direct";
import { storeBus } from "@bobfrankston/mailx-bus";
function toEmailAddress(addr) {
    return { name: addr?.name || "", address: addr?.address || "" };
}
export class SyncManager {
    db;
    bodyStore;
    providers = new Map();
    tokenProviders = new Map();
    deps;
    constructor(db, bodyStore, deps) {
        this.db = db;
        this.bodyStore = bodyStore;
        this.deps = deps;
    }
    on(_event, _handler) { }
    emit(event, ...args) { this.deps.emitEvent({ type: event, ...args[0] }); }
    async addAccount(account) {
        this.deps.vlog(`addAccount id=${account.id} email=${account.email} host=${account.imap?.host} auth=${account.imap?.auth}`);
        this.db.upsertAccount(account.id, account.name, account.email, JSON.stringify(account));
        if (this.isGmailAccount(account)) {
            const tokenProvider = this.tokenProviders.get(account.id);
            if (tokenProvider) {
                this.providers.set(account.id, new GmailApiWebProvider(tokenProvider));
                console.log(`[sync] ${account.id}: Gmail API provider registered`);
            }
            else {
                console.warn(`[sync] ${account.id}: no token provider`);
            }
        }
        else if (account.imap?.host && account.imap?.user) {
            try {
                const provider = new ImapWebProvider({
                    server: account.imap.host,
                    port: account.imap.port || 993,
                    username: account.imap.user,
                    password: account.imap.password,
                    inactivityTimeout: 300000,
                    fetchChunkSize: 10,
                    fetchChunkSizeMax: 100,
                }, this.deps.createTcpTransport);
                this.providers.set(account.id, provider);
                this.deps.vlog(`addAccount ${account.id}: IMAP provider registered (${account.imap.host}:${account.imap.port})`);
                console.log(`[sync] ${account.id}: IMAP provider registered (${account.imap.host})`);
            }
            catch (e) {
                this.deps.vlog(`addAccount ${account.id}: IMAP provider FAILED: ${e.message}`);
                console.error(`[sync] ${account.id}: IMAP provider failed: ${e.message}`);
            }
        }
        else {
            this.deps.vlog(`addAccount ${account.id}: no imap config, skipping`);
        }
    }
    setTokenProvider(accountId, provider) {
        this.tokenProviders.set(accountId, provider);
    }
    isGmailAccount(account) {
        return account.imap?.host?.includes("gmail") || account.email?.endsWith("@gmail.com") || false;
    }
    getProvider(accountId) {
        return this.providers.get(accountId) || null;
    }
    async syncAll() {
        const accounts = this.db.getAccounts();
        this.deps.vlog(`syncAll: ${accounts.length} accounts in DB: ${accounts.map((a) => a.id).join(",")}`);
        // Phase 1: Sync INBOX for every account first — user sees new mail fast.
        for (const account of accounts) {
            if (!this.providers.has(account.id))
                continue;
            try {
                const folders = await this.syncFolders(account.id);
                const inbox = folders.find((f) => f.specialUse === "inbox");
                if (inbox) {
                    await this.syncFolder(account.id, inbox.id);
                    this.deps.emitEvent({ type: "syncComplete", accountId: account.id });
                }
            }
            catch (e) {
                console.error(`[sync] ${account.id} inbox: ${e.message}`);
            }
        }
        // Phase 2: Remaining folders.
        for (const account of accounts) {
            if (!this.providers.has(account.id))
                continue;
            try {
                const folders = this.db.getFolders(account.id);
                const remaining = folders.filter((f) => f.specialUse !== "inbox");
                for (const folder of remaining) {
                    try {
                        await this.syncFolder(account.id, folder.id);
                    }
                    catch (e) {
                        console.error(`[sync] Skip ${folder.path}: ${e.message}`);
                    }
                }
                this.db.updateLastSync(account.id, Date.now());
                this.deps.emitEvent({ type: "syncComplete", accountId: account.id });
            }
            catch (e) {
                console.error(`[sync] ${account.id}: ${e.message}`);
                this.deps.vlog(`syncAll: ${account.id} ERROR: ${e.message}`);
                this.deps.emitEvent({ type: "syncError", accountId: account.id, error: e.message });
            }
        }
    }
    async syncAccount(accountId) {
        const folders = await this.syncFolders(accountId);
        for (const folder of folders) {
            try {
                await this.syncFolder(accountId, folder.id);
            }
            catch (e) {
                console.error(`[sync] Skip ${folder.path}: ${e.message}`);
            }
        }
        this.db.updateLastSync(accountId, Date.now());
        this.deps.emitEvent({ type: "syncComplete", accountId });
    }
    async syncFolders(accountId) {
        const provider = this.getProvider(accountId);
        if (!provider) {
            const existing = this.db.getFolders(accountId);
            this.deps.vlog(`syncFolders: ${accountId} no provider, returning ${existing.length} cached folders`);
            return existing;
        }
        this.deps.emitEvent({ type: "syncProgress", accountId, phase: "folders", progress: 0 });
        console.log(`[sync] ${accountId}: listing folders from provider`);
        let providerFolders = [];
        try {
            providerFolders = await provider.listFolders();
        }
        catch (e) {
            console.error(`[sync] ${accountId}: listFolders threw: ${e?.message || e}`);
            throw e;
        }
        console.log(`[sync] ${accountId}: provider returned ${providerFolders.length} folders` +
            (providerFolders.length > 0 ? ` (sample: ${providerFolders.slice(0, 3).map((f) => f.path || f.name).join(", ")})` : ""));
        for (const folder of providerFolders) {
            const flags = folder.flags || [];
            if (flags.some((f) => f.toLowerCase() === "\\noselect"))
                continue;
            this.db.upsertFolder(accountId, folder.path, folder.name, folder.specialUse, folder.delimiter);
        }
        this.deps.emitEvent({ type: "syncProgress", accountId, phase: "folders", progress: 100 });
        const dbFolders = this.db.getFolders(accountId);
        console.log(`[sync] ${accountId}: ${dbFolders.length} folders in db after upsert` +
            `, inbox=${dbFolders.some((f) => f.specialUse === "inbox") ? "yes" : "no"}`);
        this.deps.emitEvent({ type: "folderCountsChanged", accountId, counts: {} });
        return dbFolders;
    }
    async syncFolder(accountId, folderId) {
        const provider = this.getProvider(accountId);
        if (!provider)
            return;
        const folders = this.db.getFolders(accountId);
        const folder = folders.find((f) => f.id === folderId);
        if (!folder)
            return;
        this.deps.emitEvent({ type: "syncProgress", accountId, phase: `sync:${folder.path}`, progress: 0 });
        const highestUid = this.db.getHighestUid(accountId, folderId);
        const startDate = new Date(Date.now() - 30 * 86400000);
        let messages;
        if (highestUid > 0) {
            messages = await provider.fetchSince(folder.path, highestUid, { source: false });
            messages = messages.filter((m) => m.uid > highestUid);
        }
        else {
            const tomorrow = new Date(Date.now() + 86400000);
            messages = await provider.fetchByDate(folder.path, startDate, tomorrow, { source: false });
        }
        if (messages.length > 0) {
            console.log(`[sync] ${folder.path}: ${messages.length} messages`);
            this.storeProviderMessages(accountId, folderId, messages);
            this.db.recalcFolderCounts(folderId);
            this.deps.emitEvent({ type: "folderCountsChanged", accountId, counts: {} });
        }
        // Reconcile deletions
        try {
            const serverUidsArr = await provider.getUids(folder.path);
            const serverUids = new Set(serverUidsArr);
            const localUids = this.db.getUidsForFolder(accountId, folderId);
            if (serverUidsArr.length === 0 && localUids.length > 0) {
                console.log(`[sync] ${folder.path}: reconcile skipped — server returned empty but local has ${localUids.length}`);
            }
            else {
                const toDelete = localUids.filter((uid) => !serverUids.has(uid));
                const RECONCILE_DELETE_THRESHOLD = 0.5;
                if (localUids.length > 0 && toDelete.length / localUids.length > RECONCILE_DELETE_THRESHOLD) {
                    console.log(`[sync] ${folder.path}: reconcile refused — would delete ${toDelete.length}/${localUids.length}`);
                }
                else {
                    for (const uid of toDelete) {
                        this.db.deleteMessage(accountId, folderId, uid);
                        this.bodyStore.deleteMessage(accountId, folderId, uid).catch(() => { });
                    }
                    if (toDelete.length > 0) {
                        console.log(`[sync] ${folder.path}: reconciled ${toDelete.length} deletions`);
                        this.db.recalcFolderCounts(folderId);
                        this.deps.emitEvent({ type: "folderCountsChanged", accountId, counts: {} });
                    }
                }
            }
        }
        catch (e) {
            console.error(`[sync] ${folder.path}: reconcile error: ${e.message}`);
        }
        this.deps.emitEvent({ type: "folderSynced", accountId, entries: [{ folderId, syncedAt: Date.now() }] });
        this.deps.emitEvent({ type: "syncProgress", accountId, phase: `sync:${folder.path}`, progress: 100 });
    }
    storeProviderMessages(accountId, folderId, messages) {
        this.db.beginTransaction();
        try {
            for (const msg of messages) {
                const flags = [];
                if (msg.seen)
                    flags.push("\\Seen");
                if (msg.flagged)
                    flags.push("\\Flagged");
                if (msg.answered)
                    flags.push("\\Answered");
                if (msg.draft)
                    flags.push("\\Draft");
                const bodyPath = msg.providerId ? `gmail:${msg.providerId}` : "";
                // `messages.date` is NOT NULL. An unparseable envelope date
                // yields an *Invalid Date* — truthy, `instanceof Date`, but
                // `.getTime()` is NaN, which binds as NULL and fails the
                // constraint, aborting the whole batch. Default any non-finite
                // value to now.
                const dateRaw = msg.date instanceof Date ? msg.date.getTime()
                    : typeof msg.date === "number" ? msg.date
                        : NaN;
                const dateMs = Number.isFinite(dateRaw) ? dateRaw : Date.now();
                const sentRaw = msg.sentDate instanceof Date ? msg.sentDate.getTime()
                    : typeof msg.sentDate === "number" ? msg.sentDate
                        : NaN;
                this.db.upsertMessage({
                    accountId, folderId, uid: msg.uid,
                    messageId: msg.messageId || "", inReplyTo: "", references: [],
                    date: dateMs,
                    sentDate: Number.isFinite(sentRaw) ? sentRaw : undefined,
                    subject: msg.subject || "",
                    from: toEmailAddress(msg.from?.[0]),
                    to: msg.to.map((a) => toEmailAddress(a)),
                    cc: msg.cc.map((a) => toEmailAddress(a)),
                    flags, size: msg.size || 0, hasAttachments: false, preview: "", bodyPath,
                });
            }
            this.db.commitTransaction();
        }
        catch (e) {
            this.db.rollbackTransaction();
            console.error(`[sync] storeMessages error: ${e.message}`);
        }
    }
    async fetchMessageBody(accountId, folderId, uid) {
        const t0 = Date.now();
        // Cache first — IndexedDB lookup is O(1) and should always win on a
        // previously-fetched body. If this path misses on something the user
        // clearly fetched before, the cache is broken (wrong key shape, wiped
        // IndexedDB, account-id change) and needs investigation — log so it's
        // visible.
        if (await this.bodyStore.hasMessage(accountId, folderId, uid)) {
            // Cache hit is the common case on every body open — not worth a
            // log line each time. Only an actual fetch (below) is logged.
            return await this.bodyStore.getMessage(accountId, folderId, uid);
        }
        const provider = this.getProvider(accountId);
        if (!provider) {
            console.warn(`[fetchBody] no provider for ${accountId}`);
            return null;
        }
        const envelope = this.db.getMessageByUid(accountId, uid, folderId);
        const bp = envelope?.bodyPath || "";
        // Wall-clock timeout — without this, an IMAP provider that dangles
        // (Dovecot silently dropped the socket, BridgeTransport stalled) hung
        // the viewer forever. 60 s is generous for a single-message BODY[]
        // fetch on a phone connection; legit large bodies finish in seconds,
        // and anything longer means the socket is dead and retrying on a fresh
        // one is faster than waiting.
        const FETCH_TIMEOUT_MS = 60_000;
        const fetchPromise = (async () => {
            if (bp.startsWith("gmail:") && provider.fetchById) {
                const providerId = bp.substring(6);
                return provider.fetchById(providerId, { source: true });
            }
            const folders = this.db.getFolders(accountId);
            const folder = folders.find((f) => f.id === folderId);
            if (!folder)
                return null;
            return provider.fetchOne(folder.path, uid, { source: true });
        })();
        let msg = null;
        try {
            msg = await Promise.race([
                fetchPromise,
                new Promise((_, reject) => setTimeout(() => reject(new Error(`body-fetch timeout ${FETCH_TIMEOUT_MS / 1000}s (${accountId}/${folderId}/${uid})`)), FETCH_TIMEOUT_MS)),
            ]);
        }
        catch (e) {
            console.error(`[fetchBody] failed ${accountId}/${folderId}/${uid} after ${Date.now() - t0}ms: ${e?.message || e}`);
            throw e;
        }
        if (!msg?.source) {
            console.warn(`[fetchBody] No source returned for ${accountId}/${folderId}/${uid} (bp=${bp}, ${Date.now() - t0}ms)`);
            return null;
        }
        const raw = new TextEncoder().encode(msg.source);
        await this.bodyStore.putMessage(accountId, folderId, uid, raw);
        this.db.updateBodyPath(accountId, folderId, uid, `idb:${accountId}/${folderId}/${uid}`);
        console.log(`[fetchBody] fetched + cached ${accountId}/${folderId}/${uid} (${raw.byteLength} bytes, ${Date.now() - t0}ms)`);
        return raw;
    }
    /** Publish a Store event AND the legacy emitEvent() shape until every
     *  consumer subscribes via storeBus. Same shape as the desktop side —
     *  that's the load-bearing property for cross-platform code sharing. */
    publishStore(event) {
        storeBus.publish(event);
        // Bridge to existing emitEvent listeners. Drop this when all WebView
        // consumers switch to subscribeStore (mailx-bus → bridge → WebView).
        if (event.kind === "folderCountsChanged") {
            this.deps.emitEvent({ type: "folderCountsChanged", accountId: event.accountId, counts: {} });
        }
        else if (event.kind === "messageMoved") {
            this.deps.emitEvent({ type: "messageMoved", accountId: event.accountId, fromFolderId: event.folderId, toFolderId: event.targetFolderId, uid: event.uid });
        }
        else if (event.kind === "messageRemoved") {
            this.deps.emitEvent({ type: "messageDeleted", accountId: event.accountId, folderId: event.folderId, uid: event.uid });
        }
    }
    async updateFlagsLocal(accountId, uid, folderId, flags) {
        this.db.updateMessageFlags(accountId, folderId, uid, flags);
        this.db.recalcFolderCounts(folderId);
        this.db.queueSyncAction(accountId, "flags", uid, folderId, { flags });
        const env = this.db.getMessageByUid(accountId, uid, folderId);
        const msgUuid = env?.uuid;
        if (msgUuid)
            this.publishStore({ topic: `message:${msgUuid}`, kind: "flagsChanged", accountId, folderId, uid, msgUuid, flags });
        this.publishStore({ topic: `account:${accountId}`, kind: "folderCountsChanged", accountId, folderId });
    }
    async trashMessage(accountId, folderId, uid) {
        const env = this.db.getMessageByUid(accountId, uid, folderId);
        const msgUuid = env?.uuid;
        this.db.deleteMessage(accountId, folderId, uid);
        this.db.queueSyncAction(accountId, "trash", uid, folderId);
        if (msgUuid)
            this.publishStore({ topic: `message:${msgUuid}`, kind: "messageRemoved", accountId, folderId, uid, msgUuid });
        this.publishStore({ topic: `account:${accountId}`, kind: "folderCountsChanged", accountId, folderId });
    }
    async trashMessages(accountId, messages) {
        for (const m of messages)
            await this.trashMessage(accountId, m.folderId, m.uid);
    }
    async moveMessage(accountId, uid, folderId, targetFolderId) {
        const env = this.db.getMessageByUid(accountId, uid, folderId);
        const msgUuid = env?.uuid;
        this.db.queueSyncAction(accountId, "move", uid, folderId, { targetFolderId });
        if (msgUuid)
            this.publishStore({ topic: `message:${msgUuid}`, kind: "messageMoved", accountId, folderId, targetFolderId, uid, msgUuid });
    }
    async moveMessages(accountId, messages, targetFolderId) {
        for (const m of messages)
            await this.moveMessage(accountId, m.uid, m.folderId, targetFolderId);
    }
    async moveMessageCrossAccount() {
        throw new Error("Cross-account move not supported on mobile");
    }
    async undeleteMessage(accountId, uid, folderId) {
        this.db.queueSyncAction(accountId, "undelete", uid, folderId);
    }
    /** Q112: drain queued move/flag/trash actions to the provider. Android is
     *  standalone — it pushes state changes to Gmail (or other provider) the
     *  same way desktop does, so local actions propagate without needing a
     *  desktop to relay them. Called from android-bootstrap on startup and
     *  every 2-min sync tick. `send` actions are drained separately by
     *  processSendQueue. */
    async processSyncActions(accountId) {
        const provider = this.getProvider(accountId);
        if (!provider)
            return;
        const pending = this.db.getPendingSyncActions(accountId)
            .filter((a) => a.action !== "send");
        if (pending.length === 0)
            return;
        const folders = this.db.getFolders(accountId);
        const folderPath = (id) => {
            const f = folders.find((x) => x.id === id);
            return f?.path || null;
        };
        for (const p of pending) {
            const path = folderPath(p.folderId);
            if (!path) {
                this.db.failSyncActionByUid(accountId, p.action, p.uid, `unknown folder ${p.folderId}`);
                continue;
            }
            try {
                if (p.action === "flags" && typeof provider.setFlags === "function") {
                    await provider.setFlags(path, p.uid, Array.isArray(p.flags) ? p.flags : (p.flags ? [p.flags] : []));
                }
                else if (p.action === "trash" && typeof provider.trashMessage === "function") {
                    await provider.trashMessage(path, p.uid);
                }
                else if (p.action === "move" && typeof provider.moveMessage === "function") {
                    const toId = p.targetFolderId;
                    const toPath = folderPath(toId);
                    if (!toPath) {
                        this.db.failSyncActionByUid(accountId, p.action, p.uid, `unknown target folder ${toId}`);
                        continue;
                    }
                    await provider.moveMessage(path, p.uid, toPath);
                }
                else {
                    // Unsupported action for this provider — don't loop forever.
                    this.db.failSyncActionByUid(accountId, p.action, p.uid, `provider does not support ${p.action}`);
                    continue;
                }
                this.db.completeSyncActionByUid(accountId, p.action, p.uid);
            }
            catch (e) {
                const msg = e?.message || String(e);
                console.error(`[sync-action] ${accountId} ${p.action} uid=${p.uid}: ${msg}`);
                this.db.failSyncActionByUid(accountId, p.action, p.uid, msg);
            }
        }
    }
    async markFolderRead(folderId) {
        this.db.markFolderRead(folderId);
    }
    async emptyFolder(accountId, folderId) {
        const uids = this.db.getUidsForFolder(accountId, folderId);
        for (const uid of uids) {
            this.db.deleteMessage(accountId, folderId, uid);
            this.bodyStore.deleteMessage(accountId, folderId, uid).catch(() => { });
        }
        this.db.recalcFolderCounts(folderId);
        this.deps.emitEvent({ type: "folderCountsChanged", accountId, counts: {} });
    }
    queueOutgoingLocal(accountId, rawMessage) {
        const provider = this.getProvider(accountId);
        if (provider && typeof provider.sendRaw === "function") {
            provider.sendRaw(rawMessage)
                .then((result) => {
                console.log(`[send] ${accountId}: sent via Gmail API (id=${result.id})`);
                this.deps.emitEvent({ type: "sendComplete", accountId, messageId: result.id });
            })
                .catch((e) => {
                console.error(`[send] ${accountId}: Gmail send failed: ${e.message}`);
                this.deps.emitEvent({ type: "sendError", accountId, error: e.message });
            });
            return;
        }
        const accounts = this.db.getAccountConfigs();
        const row = accounts.find((a) => a.id === accountId);
        if (!row) {
            const e = "Unknown account";
            this.deps.emitEvent({ type: "sendError", accountId, error: e });
            throw new Error(e);
        }
        let account;
        try {
            account = JSON.parse(row.configJson);
        }
        catch {
            const e = "Account config malformed";
            this.deps.emitEvent({ type: "sendError", accountId, error: e });
            throw new Error(e);
        }
        if (!account.smtp) {
            const e = "No SMTP config for this account";
            this.deps.emitEvent({ type: "sendError", accountId, error: e });
            throw new Error(e);
        }
        this.sendViaSmtpDirect(accountId, account, rawMessage)
            .then((result) => {
            console.log(`[send] ${accountId}: sent via SMTP (${result.accepted.length} accepted, ${result.rejected.length} rejected)`);
            this.deps.emitEvent({ type: "sendComplete", accountId });
            // File a Sent copy. SMTP delivers but doesn't store anything —
            // without this APPEND a phone-sent message on an IMAP account
            // never appeared in Sent (Bob 2026-07-01, the 19:36 Screenshot).
            // Gmail's API path self-files, so only this branch needs it.
            // Best-effort: the message was delivered either way; failure is
            // logged, and the desktop's sent-sweep can repair later.
            const p = this.getProvider(accountId);
            if (p && typeof p.appendToSent === "function") {
                p.appendToSent(rawMessage)
                    .then((uid) => console.log(`[send] ${accountId}: Sent copy filed${uid ? ` (uid ${uid})` : ""}`))
                    .catch((e) => console.error(`[send] ${accountId}: Sent copy APPEND failed: ${e.message}`));
            }
        })
            .catch((e) => {
            console.error(`[send] ${accountId}: SMTP send failed: ${e.message}`);
            this.deps.emitEvent({ type: "sendError", accountId, error: e.message });
        });
    }
    async sendViaSmtpDirect(accountId, account, raw) {
        const smtp = account.smtp;
        const smtpPort = smtp.port || 587;
        const smtpHost = smtp.host || account.imap?.host;
        if (!smtpHost)
            throw new Error("No SMTP host");
        const smtpUser = smtp.user || account.imap?.user || account.email;
        const authType = smtp.auth || (account.imap?.password ? "password" : undefined);
        let auth;
        if (authType === "password") {
            const pass = smtp.password || account.imap?.password;
            if (!pass)
                throw new Error("SMTP password not configured");
            auth = { method: "PLAIN", user: smtpUser, pass };
        }
        else if (authType === "oauth2") {
            const tp = this.tokenProviders.get(accountId);
            if (!tp)
                throw new Error("OAuth token provider not registered");
            const token = await tp();
            auth = { method: "XOAUTH2", user: smtpUser, token };
        }
        const parseAddrs = (s) => s.match(/[\w.+-]+@[\w.-]+/g) || [];
        // HEADER SECTION only, unfolded — matching over the whole raw file
        // scraped Cc/Bcc lines out of the quoted reply chain in the body and
        // silently added those addresses to RCPT TO (desktop hit this
        // 2026-07-21; same code shape here). See mailx-imap sendRaw.
        const headerEnd = raw.search(/\r?\n\r?\n/);
        const headerSec = (headerEnd === -1 ? raw : raw.slice(0, headerEnd))
            .replace(/\r?\n[ \t]+/g, " ");
        const toMatch = headerSec.match(/^To:\s*(.+)$/mi);
        const ccMatch = headerSec.match(/^Cc:\s*(.+)$/mi);
        const bccMatch = headerSec.match(/^Bcc:\s*(.+)$/mi);
        const fromMatch = headerSec.match(/^From:\s*(.+)$/mi);
        const recipients = [
            ...(toMatch ? parseAddrs(toMatch[1]) : []),
            ...(ccMatch ? parseAddrs(ccMatch[1]) : []),
            ...(bccMatch ? parseAddrs(bccMatch[1]) : []),
        ];
        const sender = fromMatch ? (parseAddrs(fromMatch[1])[0] || account.email) : account.email;
        if (recipients.length === 0)
            throw new Error("No recipients");
        // Strip Bcc from the transmitted copy — headers only (a body line
        // that merely LOOKS like "Bcc:" in quoted text must not be eaten).
        // Folded continuation lines go with it. The `(\r?\n|$)` tail
        // consumes the line ending so no blank line is left mid-headers;
        // the final trim handles a Bcc sitting as the last header line.
        const stripBcc = (h) => h.replace(/^Bcc:[^\n]*(\r?\n[ \t][^\n]*)*(\r?\n|$)/mi, "").replace(/\r?\n$/, "");
        const rawToSend = headerEnd === -1
            ? stripBcc(raw)
            : stripBcc(raw.slice(0, headerEnd)) + raw.slice(headerEnd);
        const client = new SmtpClient({
            host: smtpHost,
            port: smtpPort,
            secure: smtpPort === 465,
            auth,
            localname: "mailx-android",
        }, this.deps.createTcpTransport);
        try {
            await client.connect();
            return await client.sendMail({ from: sender, to: recipients }, rawToSend);
        }
        finally {
            try {
                await client.quit();
            }
            catch { /* ignore */ }
        }
    }
    async saveDraft(_accountId, _raw, _prevUid, _draftId) {
        return null;
    }
    async deleteDraft(_accountId, _uid) { }
    async reauthenticate(_accountId) { return false; }
    async searchOnServer(_accountId, _query) { return []; }
    async syncAllContacts() { }
}
//# sourceMappingURL=sync-manager.js.map