server/send-provider.test.ts

import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import nodemailer from "nodemailer";
import { ImapFlow } from "imapflow";

test("SMTP acceptance survives Sent-copy and connection cleanup failures", async (t) => {
  const directory = mkdtempSync(path.join(tmpdir(), "emc2-send-"));
  process.env.MAIL_DATA_DIR = directory;
  process.env.MAIL_DATA_KEY = Buffer.alloc(32, 7).toString("base64");
  const { put, closeStore } = await import("./store.js");
  const { sendMail } = await import("./mail-actions.js");
  put("account", "synthetic", {
    id: "synthetic",
    email: "sender@example.com",
    provider: "imap",
    credentials: { imapHost: "imap.example.com", smtpHost: "smtp.example.com" },
  });
  const input = {
    accountId: "synthetic",
    to: "recipient@example.com",
    subject: "Synthetic test",
    body: "Synthetic message",
  };
  try {
    for (const scenario of [
      { name: "accepted with copy and logout", copy: true, logout: true },
      { name: "Sent-copy failure", copy: false, logout: true },
      { name: "logout failure after saved copy", copy: true, logout: false },
      {
        name: "logout returns false after saved copy",
        copy: true,
        logout: true,
        logoutFalse: true,
      },
      { name: "copy and logout both fail", copy: false, logout: false },
      { name: "append returns false", copy: false, logout: true, appendFalse: true },
      { name: "Sent-copy connection fails", copy: false, logout: true, connect: false },
      { name: "connection and close both fail", copy: false, logout: true, connect: false, close: false },
    ]) {
      await t.test(scenario.name, async (t) => {
        let sends = 0;
        let logouts = 0;
        let closes = 0;
        t.mock.method(nodemailer, "createTransport", () => ({
          sendMail: async () => { sends++; return { accepted: [input.to] }; },
        }));
        t.mock.method(ImapFlow.prototype, "connect", async function (this: ImapFlow) {
          if (scenario.connect === false) throw new Error("Synthetic connection failure");
          this.usable = true;
        });
        t.mock.method(ImapFlow.prototype, "list", async () => [
          { path: "Sent", specialUse: "\\Sent" },
        ]);
        t.mock.method(ImapFlow.prototype, "append", async () => {
          if (scenario.appendFalse) return false;
          if (!scenario.copy) throw new Error("Synthetic append failure");
          return { uid: 1 };
        });
        t.mock.method(ImapFlow.prototype, "logout", async () => {
          logouts++;
          if (!scenario.logout) throw new Error("Synthetic logout failure");
          if (scenario.logoutFalse) return false;
        });
        t.mock.method(ImapFlow.prototype, "close", () => {
          closes++;
          if (scenario.close === false) throw new Error("Synthetic close failure");
        });
        const result = await sendMail(input);
        assert.equal(result.ok, true);
        assert.equal(sends, 1);
        assert.equal(logouts, scenario.connect === false ? 0 : 1);
        assert.equal(closes, scenario.connect === false ? 1 : 0);
        if (scenario.copy && scenario.logout && !scenario.logoutFalse)
          assert.equal(result.warning, undefined);
        else {
          assert.match(result.warning!, /Email was sent/);
          assert.match(result.warning!, /Do not resend/);
          if (!scenario.copy) assert.match(result.warning!, /Sent copy failed/);
          if (scenario.logoutFalse) assert.match(result.warning!, /connection failed/);
          if (!scenario.logout || scenario.close === false) assert.match(result.warning!, /connection failed/);
        }
      });
    }
    await t.test("SMTP rejection stays a failure and never opens IMAP", async (t) => {
      t.mock.method(nodemailer, "createTransport", () => ({
        sendMail: async () => { throw new Error("Synthetic SMTP rejection"); },
      }));
      const connect = t.mock.method(ImapFlow.prototype, "connect", async () => {});
      await assert.rejects(sendMail(input), /Synthetic SMTP rejection/);
      assert.equal(connect.mock.callCount(), 0);
    });
    await t.test("invalid input never submits to SMTP", async (t) => {
      const transport = t.mock.method(nodemailer, "createTransport", () => {
        throw new Error("Must not submit invalid input");
      });
      await assert.rejects(sendMail({ ...input, to: "invalid" }));
      assert.equal(transport.mock.callCount(), 0);
    });
  } finally {
    closeStore();
    rmSync(directory, { recursive: true, force: true });
  }
});