Ctrl K
ComunidadGuías…

lib/email-accounts.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Email account storage and CRUD for per-user email accounts.
// Each user's accounts stored at ~/.clay/email/{userId}.json
// Provides provider presets, connection testing, and account management.

var fs = require("fs");
var path = require("path");
var crypto = require("crypto");
var nodemailer = require("nodemailer");
var { CONFIG_DIR, chmodSafe } = require("./config");

var EMAIL_DIR = path.join(CONFIG_DIR, "email");

// --- Encryption key (derived from machine-specific secret) ---

var _encKey = null;
function getEncKey() {
  if (_encKey) return _encKey;
  // Derive a stable encryption key from CONFIG_DIR path + a salt file.
  // If the salt file does not exist, create one.
  var saltPath = path.join(CONFIG_DIR, ".email-salt");
  var salt;
  try {
    salt = fs.readFileSync(saltPath, "utf8").trim();
  } catch (e) {
    salt = crypto.randomBytes(32).toString("hex");
    fs.mkdirSync(CONFIG_DIR, { recursive: true });
    fs.writeFileSync(saltPath, salt);
    chmodSafe(saltPath, 0o600);
  }
  _encKey = crypto.scryptSync(salt, "clay-email-accounts", 32);
  return _encKey;
}

function encrypt(text) {
  var iv = crypto.randomBytes(16);
  var cipher = crypto.createCipheriv("aes-256-gcm", getEncKey(), iv);
  var encrypted = cipher.update(text, "utf8", "hex") + cipher.final("hex");
  var tag = cipher.getAuthTag().toString("hex");
  return iv.toString("hex") + ":" + tag + ":" + encrypted;
}

function decrypt(data) {
  var parts = data.split(":");
  if (parts.length !== 3) return data; // not encrypted (legacy), return as-is
  var iv = Buffer.from(parts[0], "hex");
  var tag = Buffer.from(parts[1], "hex");
  var encrypted = parts[2];
  var decipher = crypto.createDecipheriv("aes-256-gcm", getEncKey(), iv);
  decipher.setAuthTag(tag);
  return decipher.update(encrypted, "hex", "utf8") + decipher.final("utf8");
}

// --- Provider presets ---

var PROVIDER_PRESETS = {
  gmail: {
    label: "Gmail",
    imap: { host: "imap.gmail.com", port: 993, tls: true },
    smtp: { host: "smtp.gmail.com", port: 587 },
    helpUrl: "https://support.google.com/accounts/answer/185833",
  },
  outlook: {
    label: "Outlook",
    imap: { host: "outlook.office365.com", port: 993, tls: true },
    smtp: { host: "smtp.office365.com", port: 587 },
    helpUrl: "https://support.microsoft.com/en-us/account-billing/using-app-passwords-with-apps-that-don-t-support-two-step-verification-5896ed9b-4263-e681-128a-a6f2979a7944",
  },
  yahoo: {
    label: "Yahoo",
    imap: { host: "imap.mail.yahoo.com", port: 993, tls: true },
    smtp: { host: "smtp.mail.yahoo.com", port: 587 },
    helpUrl: "https://help.yahoo.com/kb/generate-manage-third-party-passwords-sln15241.html",
  },
};

// --- Storage helpers ---

function ensureEmailDir() {
  fs.mkdirSync(EMAIL_DIR, { recursive: true });
  chmodSafe(EMAIL_DIR, 0o700);
}

function userFilePath(userId) {
  return path.join(EMAIL_DIR, userId + ".json");
}

function loadUserAccounts(userId) {
  try {
    var raw = fs.readFileSync(userFilePath(userId), "utf8");
    var data = JSON.parse(raw);
    return data.accounts || [];
  } catch (e) {
    return [];
  }
}

function saveUserAccounts(userId, accounts) {
  ensureEmailDir();
  var filePath = userFilePath(userId);
  var tmpPath = filePath + ".tmp";
  fs.writeFileSync(tmpPath, JSON.stringify({ accounts: accounts }, null, 2));
  chmodSafe(tmpPath, 0o600);
  fs.renameSync(tmpPath, filePath);
  chmodSafe(filePath, 0o600);
}

// --- Account CRUD ---

function generateAccountId() {
  return "acc_" + crypto.randomBytes(8).toString("hex");
}

function listAccounts(userId) {
  var accounts = loadUserAccounts(userId);
  // Return accounts without exposing app passwords
  return accounts.map(function (acc) {
    return {
      id: acc.id,
      email: acc.email,
      provider: acc.provider,
      label: acc.label || "",
      imap: acc.imap,
      smtp: acc.smtp,
      addedAt: acc.addedAt,
    };
  });
}

function getAccount(userId, accountId) {
  var accounts = loadUserAccounts(userId);
  for (var i = 0; i < accounts.length; i++) {
    if (accounts[i].id === accountId) return accounts[i];
  }
  return null;
}

function getAccountByEmail(userId, email) {
  var accounts = loadUserAccounts(userId);
  for (var i = 0; i < accounts.length; i++) {
    if (accounts[i].email === email) return accounts[i];
  }
  return null;
}

function getAccountDecrypted(userId, accountId) {
  var acc = getAccount(userId, accountId);
  if (!acc) return null;
  return Object.assign({}, acc, { appPassword: decrypt(acc.appPassword) });
}

function getAccountByEmailDecrypted(userId, email) {
  var acc = getAccountByEmail(userId, email);
  if (!acc) return null;
  return Object.assign({}, acc, { appPassword: decrypt(acc.appPassword) });
}

function addAccount(userId, opts) {
  var accounts = loadUserAccounts(userId);

  // Check for duplicate email
  for (var i = 0; i < accounts.length; i++) {
    if (accounts[i].email === opts.email) {
      return { error: "Account with this email already exists" };
    }
  }

  var provider = opts.provider || "custom";
  var preset = PROVIDER_PRESETS[provider];

  var account = {
    id: generateAccountId(),
    email: opts.email,
    provider: provider,
    imap: opts.imap || (preset ? Object.assign({}, preset.imap) : { host: "", port: 993, tls: true }),
    smtp: opts.smtp || (preset ? Object.assign({}, preset.smtp) : { host: "", port: 587 }),
    appPassword: encrypt(opts.appPassword),
    addedAt: Date.now(),
    label: opts.label || (preset ? preset.label : "Custom"),
  };

  accounts.push(account);
  saveUserAccounts(userId, accounts);

  return {
    ok: true,
    account: {
      id: account.id,
      email: account.email,
      provider: account.provider,
      label: account.label,
      imap: account.imap,
      smtp: account.smtp,
      addedAt: account.addedAt,
    },
  };
}

function removeAccount(userId, accountId) {
  var accounts = loadUserAccounts(userId);
  var filtered = accounts.filter(function (acc) {
    return acc.id !== accountId;
  });
  if (filtered.length === accounts.length) {
    return { error: "Account not found" };
  }
  saveUserAccounts(userId, filtered);
  return { ok: true };
}

// --- Connection testing ---

function testSmtpConnection(account) {
  var password = account.appPassword;
  // If encrypted, decrypt
  if (password.indexOf(":") !== -1 && password.length > 50) {
    try { password = decrypt(password); } catch (e) {}
  }

  var transport = nodemailer.createTransport({
    host: account.smtp.host,
    port: account.smtp.port || 587,
    secure: false,
    auth: { user: account.email, pass: password },
    connectionTimeout: 10000,
    greetingTimeout: 10000,
  });

  return transport.verify().then(function () {
    try { transport.close(); } catch (e) {}
    return { ok: true };
  }).catch(function (err) {
    try { transport.close(); } catch (e) {}
    return { ok: false, error: err.message || "SMTP connection failed" };
  });
}

function testImapConnection(account) {
  var password = account.appPassword;
  if (password.indexOf(":") !== -1 && password.length > 50) {
    try { password = decrypt(password); } catch (e) {}
  }

  var ImapFlow;
  try {
    ImapFlow = require("imapflow").ImapFlow;
  } catch (e) {
    return Promise.resolve({ ok: false, error: "imapflow module not installed" });
  }

  var client = new ImapFlow({
    host: account.imap.host,
    port: account.imap.port || 993,
    secure: account.imap.tls !== false,
    auth: { user: account.email, pass: password },
    logger: false,
  });

  return client.connect().then(function () {
    return client.logout().then(function () {
      return { ok: true };
    });
  }).catch(function (err) {
    try { client.close(); } catch (e) {}
    return { ok: false, error: err.message || "IMAP connection failed" };
  });
}

function testConnection(account) {
  return Promise.all([
    testImapConnection(account),
    testSmtpConnection(account),
  ]).then(function (results) {
    var imap = results[0];
    var smtp = results[1];
    return {
      imap: imap,
      smtp: smtp,
      ok: imap.ok && smtp.ok,
    };
  });
}

// --- Exports ---

module.exports = {
  PROVIDER_PRESETS: PROVIDER_PRESETS,
  listAccounts: listAccounts,
  getAccount: getAccount,
  getAccountByEmail: getAccountByEmail,
  getAccountDecrypted: getAccountDecrypted,
  getAccountByEmailDecrypted: getAccountByEmailDecrypted,
  addAccount: addAccount,
  removeAccount: removeAccount,
  testConnection: testConnection,
  testImapConnection: testImapConnection,
  testSmtpConnection: testSmtpConnection,
  encrypt: encrypt,
  decrypt: decrypt,
};