argorant 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -0
- package/bin/argorant.js +356 -3
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -282,6 +282,10 @@ argorant campaigns create --name "Q4 CFO outreach" [--daily-limit 40] [--timezon
|
|
|
282
282
|
argorant campaigns emails set <campaign> --step 1 --subject "Quick question, {{first_name}}" --body-file ./step1.txt
|
|
283
283
|
argorant campaigns emails set <campaign> --step 2 --body "Bumping this." --delay-days 3
|
|
284
284
|
argorant campaigns emails set <campaign> --file emails.json # whole sequence at once
|
|
285
|
+
argorant campaigns subsequences list <campaign> # reply-keyword follow-ups
|
|
286
|
+
argorant campaigns subsequences set <campaign> --file subsequences.json # up to 5, replaces the set
|
|
287
|
+
# keywords also match in capitals, as a plural, with a small typo or inside a short sentence;
|
|
288
|
+
# "not interested" and replies that ask to stop never start a subsequence
|
|
285
289
|
argorant campaigns leads add <campaign> --list <list-id> # a saved list
|
|
286
290
|
argorant campaigns leads add <campaign> --csv leads.csv # email, first_name, last_name, company, title
|
|
287
291
|
argorant campaigns senders set <campaign> --emails a@x.com,b@y.com # or --all
|
|
@@ -302,6 +306,40 @@ argorant inboxes connect-google --admin admin@yourdomain.com # lists mailb
|
|
|
302
306
|
argorant inboxes connect-google --admin admin@yourdomain.com --emails a@x.com,b@x.com # or --all
|
|
303
307
|
```
|
|
304
308
|
|
|
309
|
+
### Mailboxes from any email provider (SMTP and IMAP)
|
|
310
|
+
|
|
311
|
+
Zoho, Fastmail, Yahoo, iCloud, IONOS, your web host or any other provider connect with their server settings.
|
|
312
|
+
Argorant signs in twice before it saves anything: once to send (SMTP) and once to read replies (IMAP). A mailbox
|
|
313
|
+
connects only when both work. The password is stored safely and never shown again; many providers need an app
|
|
314
|
+
password instead of the normal one. Google and Microsoft mailboxes connect with their own sign-in instead.
|
|
315
|
+
|
|
316
|
+
```
|
|
317
|
+
argorant inboxes presets # provider keys for --preset
|
|
318
|
+
argorant inboxes test-smtp --email anna@acme.com --preset zoho # checks sending and reading replies, saves nothing
|
|
319
|
+
argorant inboxes connect-smtp --email anna@acme.com --preset zoho --name "Anna Weber"
|
|
320
|
+
argorant inboxes connect-smtp --email anna@acme.com --smtp-host mail.acme.com --smtp-port 587 --imap-host mail.acme.com
|
|
321
|
+
argorant inboxes connect-smtp --csv mailboxes.csv # many at once, a result per row
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
The password is never taken on the command line. Put it in an environment variable and name it with
|
|
325
|
+
`--password-env NAME`, set `ARGORANT_MAILBOX_PASSWORD`, or type it when asked. `--imap-password-env NAME` sets a
|
|
326
|
+
different password for reading replies. Connecting the same address again replaces its settings, which is how a
|
|
327
|
+
mailbox is reconnected after a password change.
|
|
328
|
+
|
|
329
|
+
CSV columns: `email, sender_name, preset, smtp_host, smtp_port, smtp_security, smtp_username, smtp_password,
|
|
330
|
+
imap_host, imap_port, imap_security, imap_username, imap_password, daily_limit`. Each row needs `email`,
|
|
331
|
+
`smtp_password` (or `password`) and either `preset` or `smtp_host` + `smtp_port` + `imap_host`. Empty columns take
|
|
332
|
+
the defaults: the username is the email address, reading replies uses the sending sign-in, port 993, 25 emails a
|
|
333
|
+
day. Security is `ssl`, `starttls` or `none`. Use port 587 for sending; if you give 465, Argorant tries 587 and
|
|
334
|
+
saves 587 when it works. Rows go up 25 at a time; the command exits 1 when any row did not
|
|
335
|
+
connect.
|
|
336
|
+
|
|
337
|
+
```
|
|
338
|
+
email,sender_name,preset,smtp_password
|
|
339
|
+
anna@acme.com,Anna Weber,zoho,app-password-1
|
|
340
|
+
ben@acme.com,Ben Hill,zoho,app-password-2
|
|
341
|
+
```
|
|
342
|
+
|
|
305
343
|
New mailboxes on their own new domains, ordered from the terminal. The quote shows every domain with its
|
|
306
344
|
first-year and renewal price, the mailboxes, the total and how it is paid. When your workspace saved its card
|
|
307
345
|
for API and agent purchases (you ticked that box when you paid an earlier order, and the setting in
|
package/bin/argorant.js
CHANGED
|
@@ -1460,6 +1460,89 @@ async function campaignsEmails(argv) {
|
|
|
1460
1460
|
console.log(green("✓") + ` Saved ${(r.emails || emails).length} email(s) for ${dim(campaignId)}`);
|
|
1461
1461
|
}
|
|
1462
1462
|
|
|
1463
|
+
// subsequences: follow-ups a reply keyword starts (GET / POST /api/v1/campaigns/{id}/subsequences).
|
|
1464
|
+
// The server replaces the whole set: subsequences left out are deleted with anyone still in them,
|
|
1465
|
+
// so `set` asks before it drops one that still has people in progress (-y skips the question).
|
|
1466
|
+
const SUBSEQUENCES_USAGE =
|
|
1467
|
+
"usage: argorant campaigns subsequences list <campaign>\n" +
|
|
1468
|
+
" or: argorant campaigns subsequences set <campaign> --file subsequences.json [-y]\n" +
|
|
1469
|
+
" or: argorant campaigns subsequences clear <campaign> [-y]\n" +
|
|
1470
|
+
' subsequences.json: [{"name":"Interested","keywords":["interested","yes"],"emails":[{"body":"...","delay_days":0},{"body":"...","delay_days":3}]}]\n' +
|
|
1471
|
+
" Up to 5 subsequences, 1 to 10 keywords and 1 to 8 emails each. Email 1 defaults to the subject 'Re: {{thread_subject}}'.\n" +
|
|
1472
|
+
" Keywords also count in capitals, as a plural, with a small typo or inside a short sentence.\n" +
|
|
1473
|
+
" 'not interested' and replies that ask to stop never start one.";
|
|
1474
|
+
|
|
1475
|
+
function readSubsequencesFile(text, source) {
|
|
1476
|
+
let data;
|
|
1477
|
+
try { data = JSON.parse(text); } catch (e) { die(`${source}: not valid JSON (${e.message})`); }
|
|
1478
|
+
const items = Array.isArray(data) ? data : data && Array.isArray(data.subsequences) ? data.subsequences : null;
|
|
1479
|
+
if (!items) die(`${source}: expected a list of subsequences (or {"subsequences": [...]})`);
|
|
1480
|
+
if (items.length > 5) die(`${source}: a campaign can have up to 5 subsequences (got ${items.length})`);
|
|
1481
|
+
return items.map((item, i) => {
|
|
1482
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) die(`${source}: subsequence ${i + 1} is not an object`);
|
|
1483
|
+
const keywords = typeof item.keywords === "string" ? item.keywords.split(",") : item.keywords;
|
|
1484
|
+
if (!Array.isArray(keywords) || !keywords.some((k) => String(k || "").trim())) die(`${source}: subsequence ${i + 1} needs at least one keyword`);
|
|
1485
|
+
const emails = item.emails || item.steps;
|
|
1486
|
+
if (!Array.isArray(emails) || !emails.length) die(`${source}: subsequence ${i + 1} needs at least one email`);
|
|
1487
|
+
const out = { name: item.name, keywords: keywords.map((k) => String(k).trim()).filter(Boolean), emails };
|
|
1488
|
+
if (item.id) out.id = item.id;
|
|
1489
|
+
return out;
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function printSubsequences(r) {
|
|
1494
|
+
const subs = r.subsequences || [];
|
|
1495
|
+
console.log(dim(`Stop on reply: ${r.stop_on_reply === false ? "off" : "on"}`));
|
|
1496
|
+
if (!subs.length) return console.log(dim("No subsequences. Add them with `argorant campaigns subsequences set <campaign> --file subsequences.json`."));
|
|
1497
|
+
subs.forEach((s, i) => {
|
|
1498
|
+
const st = s.stats || {};
|
|
1499
|
+
console.log(`${bold(`${i + 1}. ${s.name}`)} ${dim(s.id || "")} ${s.status || ""}`);
|
|
1500
|
+
console.log(` keywords: ${(s.keywords || []).join(", ")}`);
|
|
1501
|
+
console.log(` emails: ${(s.steps || []).length} ${dim(`in progress ${st.in_progress || 0} replied ${st.replied || 0} finished ${st.finished || 0} sent ${st.emails_sent || 0}`)}`);
|
|
1502
|
+
for (const step of s.steps || []) {
|
|
1503
|
+
const preview = String(step.body || "").replace(/\s+/g, " ").slice(0, 70);
|
|
1504
|
+
console.log(dim(` ${step.step_number}. +${step.delay_days || 0}d ${step.subject || "(same thread)"} ${preview}`));
|
|
1505
|
+
}
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
async function campaignsSubsequences(argv) {
|
|
1510
|
+
const sub = argv[0];
|
|
1511
|
+
if (!["list", "get", "set", "clear"].includes(sub)) die(SUBSEQUENCES_USAGE);
|
|
1512
|
+
const key = requireKey();
|
|
1513
|
+
const args = readFlags(argv.slice(1), { "--file": "file" });
|
|
1514
|
+
const identifier = args._[0];
|
|
1515
|
+
if (!identifier) die(SUBSEQUENCES_USAGE);
|
|
1516
|
+
const campaignId = await resolveCampaign(args.base, key, identifier);
|
|
1517
|
+
const route = `/api/v1/campaigns/${campaignId}/subsequences`;
|
|
1518
|
+
if (sub === "list" || sub === "get") {
|
|
1519
|
+
const r = need(await request("GET", args.base, route, { key }), "campaigns subsequences");
|
|
1520
|
+
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
1521
|
+
return printSubsequences(r);
|
|
1522
|
+
}
|
|
1523
|
+
let items = [];
|
|
1524
|
+
if (sub === "set") {
|
|
1525
|
+
if (!args.file) die(SUBSEQUENCES_USAGE);
|
|
1526
|
+
const text = args.file === "-" ? await readStdin() : (() => { try { return fs.readFileSync(args.file, "utf8"); } catch { die(`cannot read file: ${args.file}`); } })();
|
|
1527
|
+
items = readSubsequencesFile(text, args.file === "-" ? "stdin" : args.file);
|
|
1528
|
+
}
|
|
1529
|
+
// What the replace would drop, and who is still inside it.
|
|
1530
|
+
const current = need(await request("GET", args.base, route, { key }), "campaigns subsequences");
|
|
1531
|
+
const keptIds = new Set(items.map((i) => i.id).filter(Boolean));
|
|
1532
|
+
const dropped = (current.subsequences || []).filter((s) => !keptIds.has(s.id));
|
|
1533
|
+
const busy = dropped.filter((s) => ((s.stats || {}).in_progress || 0) > 0);
|
|
1534
|
+
if (dropped.length && !args.yes) {
|
|
1535
|
+
const who = busy.length ? ` (${busy.map((s) => `${s.name}: ${s.stats.in_progress} in progress`).join(", ")})` : "";
|
|
1536
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1537
|
+
const ok = await new Promise((r) => rl.question(`This removes ${dropped.length} subsequence(s)${who}, with anyone still in them. Continue? [y/N] `, (a) => { rl.close(); r(/^y/i.test(a)); }));
|
|
1538
|
+
if (!ok) return console.log(dim("aborted"));
|
|
1539
|
+
}
|
|
1540
|
+
const r = need(await request("POST", args.base, route, { key, body: { subsequences: items } }), "campaigns subsequences set");
|
|
1541
|
+
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
1542
|
+
console.log(green("✓") + ` Saved ${(r.subsequences || []).length} subsequence(s) for ${dim(campaignId)}`);
|
|
1543
|
+
printSubsequences(r);
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1463
1546
|
// senders: which connected mailboxes send this campaign.
|
|
1464
1547
|
async function campaignsSenders(argv) {
|
|
1465
1548
|
const sub = argv[0];
|
|
@@ -1912,6 +1995,9 @@ ${bold("USAGE")}
|
|
|
1912
1995
|
--gap 60-120 --unsubscribe|--no-unsubscribe --start YYYY-MM-DD --tags a,b --name "<n>"
|
|
1913
1996
|
${p} emails set <campaign> --file emails.json
|
|
1914
1997
|
${p} emails set <campaign> --step <n> --subject "..." (--body-file <path> | --body <text|->) [--delay-days <d>]
|
|
1998
|
+
${p} subsequences list <campaign>
|
|
1999
|
+
${p} subsequences set <campaign> --file subsequences.json [-y] ${dim("reply keywords start follow-ups in the same thread")}
|
|
2000
|
+
${p} subsequences clear <campaign> [-y]
|
|
1915
2001
|
${p} leads add <campaign> --list <list-id> | --csv <file> [--include-catch-all]
|
|
1916
2002
|
${p} leads list <campaign> [--status replied] [--q text] [-n 50]
|
|
1917
2003
|
${p} leads remove <campaign> <email>
|
|
@@ -1929,6 +2015,10 @@ ${bold("USAGE")}
|
|
|
1929
2015
|
${bold("argorant inboxes")} set <mailbox> [--daily-limit 40] [--warmup|--no-warmup] [--active|--inactive] [--timezone <tz>]
|
|
1930
2016
|
${bold("argorant inboxes")} disconnect <mailbox>
|
|
1931
2017
|
${bold("argorant inboxes")} connect-google --admin <admin@yourdomain.com> [--emails a,b | --all]
|
|
2018
|
+
${bold("argorant inboxes")} connect-smtp --email <address> --preset <key> [--name "Sender"] [--password-env NAME] ${dim("any provider; --smtp-host/--imap-host without a preset")}
|
|
2019
|
+
${bold("argorant inboxes")} connect-smtp --csv mailboxes.csv ${dim("many at once, 25 per request, a result per row")}
|
|
2020
|
+
${bold("argorant inboxes")} test-smtp --email <address> --preset <key> ${dim("checks sending and reading replies, saves nothing")}
|
|
2021
|
+
${bold("argorant inboxes")} presets ${dim("provider keys for --preset (zoho, fastmail, yahoo, icloud, ...)")}
|
|
1932
2022
|
${bold("argorant inboxes")} endings ${dim("the domain endings on offer (com, net, org, co, sbs, biz, info)")}
|
|
1933
2023
|
${bold("argorant inboxes")} suggest <website|brand> [--ending com] [-n 20] ${dim("free domain names with prices")}
|
|
1934
2024
|
${bold("argorant inboxes")} quote --mailboxes 100 [--company acme.com] [--domains a.com,b.com] [--currency usd|eur] [--billing month|quarter]
|
|
@@ -1976,6 +2066,8 @@ async function cmdCampaigns(argv) {
|
|
|
1976
2066
|
replies: campaignsReplies,
|
|
1977
2067
|
analytics: campaignsAnalytics,
|
|
1978
2068
|
emails: campaignsEmails,
|
|
2069
|
+
subsequences: campaignsSubsequences,
|
|
2070
|
+
subsequence: campaignsSubsequences,
|
|
1979
2071
|
steps: campaignsSteps,
|
|
1980
2072
|
senders: campaignsSenders,
|
|
1981
2073
|
inboxes: campaignsInboxes,
|
|
@@ -2000,7 +2092,7 @@ async function cmdInboxes(argv) {
|
|
|
2000
2092
|
const r = await fetchPages(args.base, key, "/api/v1/inboxes", { limit: 100 }, "inboxes", "inboxes list", true);
|
|
2001
2093
|
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
2002
2094
|
const rows = r.inboxes || [];
|
|
2003
|
-
if (!rows.length) return console.log(dim("No mailboxes connected yet. Connect Google Workspace with `argorant inboxes connect-google --admin
|
|
2095
|
+
if (!rows.length) return console.log(dim("No mailboxes connected yet. Connect Google Workspace with `argorant inboxes connect-google --admin ...`, any other provider with `argorant inboxes connect-smtp --email ... --preset ...`, or Microsoft 365 in the app."));
|
|
2004
2096
|
for (const i of rows) {
|
|
2005
2097
|
console.log(`${bold(i.email)} ${dim(i.name || "")}`);
|
|
2006
2098
|
console.log(dim(` ${i.provider} · ${i.status}${i.health ? ` · ${i.health}` : ""} · today ${i.sent_today || 0}/${i.daily_limit || "?"}`));
|
|
@@ -2207,7 +2299,267 @@ async function cmdInboxes(argv) {
|
|
|
2207
2299
|
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
2208
2300
|
return console.log(green("✓") + ` Bundle ${target} cancelled${r.existing ? dim(" (already)") : ""}`);
|
|
2209
2301
|
}
|
|
2210
|
-
|
|
2302
|
+
if (sub === "presets") {
|
|
2303
|
+
const args = readFlags(argv.slice(1));
|
|
2304
|
+
const r = need(await request("GET", args.base, "/api/v1/inboxes/smtp/presets", { key }), "inboxes presets");
|
|
2305
|
+
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
2306
|
+
for (const p of r.presets || []) {
|
|
2307
|
+
const out = p.smtp_host ? `${p.smtp_host}:${p.smtp_port} ${p.smtp_secure}` : "you enter the servers";
|
|
2308
|
+
const inn = p.imap_host ? `${p.imap_host}:${p.imap_port} ${p.imap_secure}` : "";
|
|
2309
|
+
console.log(`${bold(p.key.padEnd(14))} ${p.label}`);
|
|
2310
|
+
console.log(dim(` sending ${out}${inn ? ` · reading replies ${inn}` : ""}`));
|
|
2311
|
+
if (p.hint) console.log(dim(` ${p.hint}`));
|
|
2312
|
+
}
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
if (sub === "test-smtp" || sub === "connect-smtp") return cmdSmtpMailbox(sub, argv.slice(1), key);
|
|
2316
|
+
die(`unknown inboxes subcommand: ${sub} (list, set, disconnect, connect-google, connect-smtp, test-smtp, presets, endings, suggest, quote, order, status, renewal, cancel-bundle)`);
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
// ---- mailboxes from any email provider (SMTP sending + IMAP replies), 0.14.0 ----
|
|
2320
|
+
// The password is never taken on the command line (shell history): --password-env NAME, the
|
|
2321
|
+
// ARGORANT_MAILBOX_PASSWORD variable, a hidden prompt, or the CSV file for many mailboxes.
|
|
2322
|
+
const SMTP_FLAGS = {
|
|
2323
|
+
"--email": "email", "--name": "name", "--preset": "preset",
|
|
2324
|
+
"--smtp-host": "smtpHost", "--smtp-port": "smtpPort", "--smtp-security": "smtpSecurity", "--smtp-user": "smtpUser",
|
|
2325
|
+
"--imap-host": "imapHost", "--imap-port": "imapPort", "--imap-security": "imapSecurity", "--imap-user": "imapUser",
|
|
2326
|
+
"--daily-limit": "dailyLimit", "--password-env": "passwordEnv", "--imap-password-env": "imapPasswordEnv", "--csv": "csv",
|
|
2327
|
+
};
|
|
2328
|
+
const SMTP_BULK_CHUNK = 25;
|
|
2329
|
+
const SMTP_USAGE = `usage: argorant inboxes connect-smtp --email <address> (--preset <key> | --smtp-host <h> --smtp-port <p> --imap-host <h>)
|
|
2330
|
+
[--name "Sender Name"] [--smtp-security starttls|ssl|none] [--smtp-user <u>] [--imap-port 993]
|
|
2331
|
+
[--imap-security ssl|starttls|none] [--imap-user <u>] [--daily-limit 25] [--password-env NAME] [--imap-password-env NAME]
|
|
2332
|
+
argorant inboxes connect-smtp --csv mailboxes.csv
|
|
2333
|
+
argorant inboxes test-smtp (same flags as connect-smtp for one mailbox; saves nothing)
|
|
2334
|
+
argorant inboxes presets (the provider keys for --preset)`;
|
|
2335
|
+
// CSV header -> field (lower case, spaces and dashes as underscores).
|
|
2336
|
+
const SMTP_CSV_COLUMNS = {
|
|
2337
|
+
email: "email", sender_name: "sender_name", name: "sender_name", display_name: "sender_name", preset: "preset",
|
|
2338
|
+
smtp_host: "smtp_host", smtp_port: "smtp_port", smtp_security: "smtp_security", smtp_secure: "smtp_security",
|
|
2339
|
+
smtp_username: "smtp_username", smtp_user: "smtp_username", smtp_password: "smtp_password", password: "smtp_password",
|
|
2340
|
+
imap_host: "imap_host", imap_port: "imap_port", imap_security: "imap_security", imap_secure: "imap_security",
|
|
2341
|
+
imap_username: "imap_username", imap_user: "imap_username", imap_password: "imap_password", daily_limit: "daily_limit",
|
|
2342
|
+
};
|
|
2343
|
+
|
|
2344
|
+
function normalizeSecurity(value, what) {
|
|
2345
|
+
if (value === undefined || value === null || String(value).trim() === "") return undefined;
|
|
2346
|
+
const v = String(value).trim().toLowerCase().replace(/[\s_-]+/g, "");
|
|
2347
|
+
if (["ssl", "ssl/tls", "tls", "ssltls", "implicit"].includes(v)) return "ssl";
|
|
2348
|
+
if (["starttls", "start"].includes(v)) return "starttls";
|
|
2349
|
+
if (["none", "no", "off", "plain"].includes(v)) return "none";
|
|
2350
|
+
throw new Error(`${what} must be ssl, starttls or none`);
|
|
2351
|
+
}
|
|
2352
|
+
function parsePort(value, what) {
|
|
2353
|
+
if (value === undefined || value === null || String(value).trim() === "") return undefined;
|
|
2354
|
+
const n = Number(String(value).trim());
|
|
2355
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535) throw new Error(`${what} must be a port number from 1 to 65535`);
|
|
2356
|
+
return n;
|
|
2357
|
+
}
|
|
2358
|
+
function parseDailyLimit(value) {
|
|
2359
|
+
if (value === undefined || value === null || String(value).trim() === "") return undefined;
|
|
2360
|
+
const n = Number(String(value).trim());
|
|
2361
|
+
if (!Number.isInteger(n) || n < 1 || n > 10000) throw new Error("daily limit must be a whole number from 1 to 10000");
|
|
2362
|
+
return n;
|
|
2363
|
+
}
|
|
2364
|
+
// One mailbox in the API's field names; empty values are left out (the server fills the defaults).
|
|
2365
|
+
function mailboxBody(f) {
|
|
2366
|
+
const body = {};
|
|
2367
|
+
const put = (k, v) => { if (v !== undefined && v !== null && String(v) !== "") body[k] = v; };
|
|
2368
|
+
put("email", f.email ? String(f.email).trim().toLowerCase() : undefined);
|
|
2369
|
+
put("display_name", f.sender_name ? String(f.sender_name).trim() : undefined);
|
|
2370
|
+
put("preset", f.preset ? String(f.preset).trim().toLowerCase() : undefined);
|
|
2371
|
+
put("smtp_host", f.smtp_host ? String(f.smtp_host).trim() : undefined);
|
|
2372
|
+
put("smtp_port", parsePort(f.smtp_port, "smtp port"));
|
|
2373
|
+
put("smtp_secure", normalizeSecurity(f.smtp_security, "smtp security"));
|
|
2374
|
+
put("smtp_username", f.smtp_username ? String(f.smtp_username).trim() : undefined);
|
|
2375
|
+
put("smtp_password", f.smtp_password);
|
|
2376
|
+
put("imap_host", f.imap_host ? String(f.imap_host).trim() : undefined);
|
|
2377
|
+
put("imap_port", parsePort(f.imap_port, "imap port"));
|
|
2378
|
+
put("imap_secure", normalizeSecurity(f.imap_security, "imap security"));
|
|
2379
|
+
put("imap_username", f.imap_username ? String(f.imap_username).trim() : undefined);
|
|
2380
|
+
put("imap_password", f.imap_password);
|
|
2381
|
+
put("daily_limit", parseDailyLimit(f.daily_limit));
|
|
2382
|
+
if (!body.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(body.email)) throw new Error("needs a valid email address");
|
|
2383
|
+
if (!body.smtp_password) throw new Error("needs a password");
|
|
2384
|
+
if (!body.preset && !(body.smtp_host && body.smtp_port && body.imap_host)) {
|
|
2385
|
+
throw new Error("needs a preset, or the sending server, its port and the server for reading replies");
|
|
2386
|
+
}
|
|
2387
|
+
return body;
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
// RFC 4180 CSV: quoted fields, doubled quotes, commas and line breaks inside quotes, CRLF, a BOM.
|
|
2391
|
+
function parseCsv(text) {
|
|
2392
|
+
const rows = [];
|
|
2393
|
+
let row = [], field = "", quoted = false, i = 0;
|
|
2394
|
+
const src = String(text || "").replace(/^\uFEFF/, "");
|
|
2395
|
+
while (i < src.length) {
|
|
2396
|
+
const ch = src[i];
|
|
2397
|
+
if (quoted) {
|
|
2398
|
+
if (ch === '"') {
|
|
2399
|
+
if (src[i + 1] === '"') { field += '"'; i += 2; continue; }
|
|
2400
|
+
quoted = false; i += 1; continue;
|
|
2401
|
+
}
|
|
2402
|
+
field += ch; i += 1; continue;
|
|
2403
|
+
}
|
|
2404
|
+
if (ch === '"' && field === "") { quoted = true; i += 1; continue; }
|
|
2405
|
+
if (ch === ",") { row.push(field); field = ""; i += 1; continue; }
|
|
2406
|
+
if (ch === "\r" && src[i + 1] === "\n") { i += 1; continue; }
|
|
2407
|
+
if (ch === "\n" || ch === "\r") { row.push(field); rows.push(row); row = []; field = ""; i += 1; continue; }
|
|
2408
|
+
field += ch; i += 1;
|
|
2409
|
+
}
|
|
2410
|
+
if (field !== "" || row.length) { row.push(field); rows.push(row); }
|
|
2411
|
+
return rows.filter((r) => r.some((v) => String(v).trim() !== ""));
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
// CSV text -> { rows: [{ line, body }], invalid: [{ line, email, message }], ignored: [column] }
|
|
2415
|
+
function mailboxesFromCsv(text) {
|
|
2416
|
+
const table = parseCsv(text);
|
|
2417
|
+
if (!table.length) throw new Error("the file is empty");
|
|
2418
|
+
const header = table[0].map((h) => String(h).trim().toLowerCase().replace(/[\s-]+/g, "_"));
|
|
2419
|
+
const fields = header.map((h) => SMTP_CSV_COLUMNS[h] || null);
|
|
2420
|
+
if (!fields.includes("email")) throw new Error("the file needs an email column (see: argorant inboxes connect-smtp --help)");
|
|
2421
|
+
const ignored = header.filter((h, i) => h && !fields[i]);
|
|
2422
|
+
const rows = [], invalid = [];
|
|
2423
|
+
table.slice(1).forEach((cells, index) => {
|
|
2424
|
+
const line = index + 2;
|
|
2425
|
+
const f = {};
|
|
2426
|
+
fields.forEach((name, i) => { if (name && cells[i] !== undefined && String(cells[i]).trim() !== "") f[name] = name.endsWith("password") ? String(cells[i]) : String(cells[i]).trim(); });
|
|
2427
|
+
try {
|
|
2428
|
+
rows.push({ line, body: mailboxBody(f) });
|
|
2429
|
+
} catch (e) {
|
|
2430
|
+
invalid.push({ line, email: f.email || "", status: "invalid", message: e.message });
|
|
2431
|
+
}
|
|
2432
|
+
});
|
|
2433
|
+
return { rows, invalid, ignored };
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
function promptSecret(question) {
|
|
2437
|
+
return new Promise((resolve) => {
|
|
2438
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
2439
|
+
let muted = false;
|
|
2440
|
+
rl._writeToOutput = (str) => { if (!muted) rl.output.write(str); };
|
|
2441
|
+
rl.question(question, (answer) => { rl.close(); process.stdout.write("\n"); resolve(answer); });
|
|
2442
|
+
muted = true;
|
|
2443
|
+
});
|
|
2444
|
+
}
|
|
2445
|
+
async function mailboxPassword(args, which) {
|
|
2446
|
+
const envFlag = which === "imap" ? args.imapPasswordEnv : args.passwordEnv;
|
|
2447
|
+
if (envFlag) {
|
|
2448
|
+
const v = process.env[envFlag];
|
|
2449
|
+
if (!v) die(`the environment variable ${envFlag} is empty or not set.`);
|
|
2450
|
+
return v;
|
|
2451
|
+
}
|
|
2452
|
+
if (which === "imap") return undefined; // the server uses the sending password
|
|
2453
|
+
if (process.env.ARGORANT_MAILBOX_PASSWORD) return process.env.ARGORANT_MAILBOX_PASSWORD;
|
|
2454
|
+
if (process.stdin.isTTY) {
|
|
2455
|
+
const v = await promptSecret("Mailbox password or app password (not shown): ");
|
|
2456
|
+
if (!v) die("no password given.");
|
|
2457
|
+
return v;
|
|
2458
|
+
}
|
|
2459
|
+
die("give the mailbox password with --password-env NAME or the ARGORANT_MAILBOX_PASSWORD variable.");
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
function printSignInTest(t) {
|
|
2463
|
+
if (!t || typeof t !== "object") return;
|
|
2464
|
+
for (const [leg, label] of [["smtp", "sending"], ["imap", "reading replies"]]) {
|
|
2465
|
+
const r = t[leg];
|
|
2466
|
+
if (!r) continue;
|
|
2467
|
+
console.log(` ${r.works ? green("✓") : red("✗")} ${label.padEnd(16)}${r.message || r.result || ""}`);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
function printBulkResults(results) {
|
|
2471
|
+
const w = Math.min(40, Math.max(5, ...results.map((r) => String(r.email || "").length)));
|
|
2472
|
+
for (const r of results) {
|
|
2473
|
+
const mark = r.status === "connected" ? green("✓ connected") : r.status === "skipped" ? yellow("- skipped ") : red("✗ " + (r.status === "invalid" ? "invalid " : "failed "));
|
|
2474
|
+
console.log(`${String(r.line).padStart(4)} ${String(r.email || "").padEnd(w)} ${mark} ${r.status === "connected" ? "" : (r.message || "")}`);
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
async function cmdSmtpMailbox(sub, argv, key) {
|
|
2479
|
+
if (argv.includes("--password") || argv.some((a) => a.startsWith("--password="))) {
|
|
2480
|
+
die("for safety the password is never taken on the command line. Use --password-env NAME, the ARGORANT_MAILBOX_PASSWORD variable, or type it when asked.");
|
|
2481
|
+
}
|
|
2482
|
+
if (argv.includes("--help") || argv.includes("-h")) return console.log(SMTP_USAGE + `
|
|
2483
|
+
|
|
2484
|
+
CSV columns: email, sender_name, preset, smtp_host, smtp_port, smtp_security, smtp_username, smtp_password,
|
|
2485
|
+
imap_host, imap_port, imap_security, imap_username, imap_password, daily_limit
|
|
2486
|
+
Each row needs email, smtp_password (or password), and a preset or smtp_host + smtp_port + imap_host.
|
|
2487
|
+
Empty columns take the defaults: username = email, reading replies uses the sending sign-in, port 993, 25 a day.
|
|
2488
|
+
Send on port 587 (starttls). If you give 465, Argorant tries 587 and saves 587 when it works.
|
|
2489
|
+
Rows go up 25 at a time; each row says whether it connected or what to fix. Exit 1 when any row did not connect.`);
|
|
2490
|
+
const args = readFlags(argv, SMTP_FLAGS);
|
|
2491
|
+
if (args.csv) {
|
|
2492
|
+
if (sub !== "connect-smtp") die("--csv works with connect-smtp.");
|
|
2493
|
+
let text;
|
|
2494
|
+
try { text = fs.readFileSync(args.csv, "utf8"); } catch (e) { die(`cannot read ${args.csv}: ${e.message}`); }
|
|
2495
|
+
let parsed;
|
|
2496
|
+
try { parsed = mailboxesFromCsv(text); } catch (e) { die(`${args.csv}: ${e.message}`); }
|
|
2497
|
+
if (parsed.ignored.length && !args.json) warn(`ignoring unknown column${parsed.ignored.length === 1 ? "" : "s"}: ${parsed.ignored.join(", ")}`);
|
|
2498
|
+
const results = [...parsed.invalid];
|
|
2499
|
+
const totals = { connected: 0, failed: 0, skipped: 0, invalid: parsed.invalid.length };
|
|
2500
|
+
for (let i = 0; i < parsed.rows.length; i += SMTP_BULK_CHUNK) {
|
|
2501
|
+
const chunk = parsed.rows.slice(i, i + SMTP_BULK_CHUNK);
|
|
2502
|
+
if (!args.json) process.stderr.write(dim(`checking ${i + 1} to ${i + chunk.length} of ${parsed.rows.length}...\n`));
|
|
2503
|
+
const res = await request("POST", args.base, "/api/v1/inboxes/smtp/bulk", { key, body: { mailboxes: chunk.map((r) => r.body) } });
|
|
2504
|
+
if (res.status >= 400) {
|
|
2505
|
+
if (results.length && !args.json) printBulkResults(results.sort((a, b) => a.line - b.line));
|
|
2506
|
+
need(res, "inboxes connect-smtp --csv");
|
|
2507
|
+
}
|
|
2508
|
+
const r = res.json || {};
|
|
2509
|
+
for (const item of r.results || []) {
|
|
2510
|
+
const source = chunk[Number(item.row) - 1];
|
|
2511
|
+
results.push({ line: source ? source.line : null, email: item.email || (source && source.body.email), status: item.status, result: item.result, message: item.message });
|
|
2512
|
+
if (item.status in totals) totals[item.status] += 1;
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
results.sort((a, b) => (a.line || 0) - (b.line || 0));
|
|
2516
|
+
if (totals.failed || totals.invalid || totals.skipped) process.exitCode = 1;
|
|
2517
|
+
if (args.json) return console.log(JSON.stringify({ ...totals, results }, null, 2));
|
|
2518
|
+
printBulkResults(results);
|
|
2519
|
+
const parts = [`${totals.connected} connected`];
|
|
2520
|
+
if (totals.failed) parts.push(`${totals.failed} could not sign in`);
|
|
2521
|
+
if (totals.skipped) parts.push(`${totals.skipped} skipped`);
|
|
2522
|
+
if (totals.invalid) parts.push(`${totals.invalid} with missing or wrong columns`);
|
|
2523
|
+
return console.log(`\n${parts.join(", ")}.`);
|
|
2524
|
+
}
|
|
2525
|
+
if (!args.email) die(SMTP_USAGE);
|
|
2526
|
+
const password = await mailboxPassword(args, "smtp");
|
|
2527
|
+
const imapPassword = await mailboxPassword(args, "imap");
|
|
2528
|
+
let body;
|
|
2529
|
+
try {
|
|
2530
|
+
body = mailboxBody({
|
|
2531
|
+
email: args.email, sender_name: args.name, preset: args.preset, smtp_host: args.smtpHost, smtp_port: args.smtpPort,
|
|
2532
|
+
smtp_security: args.smtpSecurity, smtp_username: args.smtpUser, smtp_password: password, imap_host: args.imapHost,
|
|
2533
|
+
imap_port: args.imapPort, imap_security: args.imapSecurity, imap_username: args.imapUser, imap_password: imapPassword,
|
|
2534
|
+
daily_limit: args.dailyLimit,
|
|
2535
|
+
});
|
|
2536
|
+
} catch (e) {
|
|
2537
|
+
die(`${e.message}.\n${SMTP_USAGE}`);
|
|
2538
|
+
}
|
|
2539
|
+
if (sub === "test-smtp") {
|
|
2540
|
+
delete body.display_name; delete body.daily_limit;
|
|
2541
|
+
const r = need(await request("POST", args.base, "/api/v1/inboxes/smtp/test", { key, body }), "inboxes test-smtp");
|
|
2542
|
+
if (!r.works) process.exitCode = 1;
|
|
2543
|
+
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
2544
|
+
console.log(`${r.works ? green("✓") : red("✗")} ${body.email} ${r.message || ""}`);
|
|
2545
|
+
printSignInTest(r);
|
|
2546
|
+
if (r.works) console.log(dim(`Connect it: argorant inboxes connect-smtp ${process.argv.slice(4).join(" ")}`.trim()));
|
|
2547
|
+
return;
|
|
2548
|
+
}
|
|
2549
|
+
const res = await request("POST", args.base, "/api/v1/inboxes/smtp", { key, body });
|
|
2550
|
+
if (res.status === 422 && errorCode(res.json) === "sign_in_failed") {
|
|
2551
|
+
const details = (res.json && res.json.error && res.json.error.details) || {};
|
|
2552
|
+
process.exitCode = 1;
|
|
2553
|
+
if (args.json) return console.log(JSON.stringify({ connected: false, message: errorMessage(res.json), test: details.test || null }, null, 2));
|
|
2554
|
+
console.log(`${red("✗")} ${body.email} was not connected. ${errorMessage(res.json) || ""}`);
|
|
2555
|
+
printSignInTest(details.test);
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2558
|
+
const r = need(res, "inboxes connect-smtp");
|
|
2559
|
+
if (args.json) return console.log(JSON.stringify(r, null, 2));
|
|
2560
|
+
const i = r.inbox || {};
|
|
2561
|
+
console.log(green("✓") + ` Connected ${bold(i.email || body.email)}${i.daily_limit ? dim(` ${i.daily_limit} a day`) : ""}`);
|
|
2562
|
+
console.log(dim("It is in `argorant inboxes list` now and can be picked as a sender for any campaign."));
|
|
2211
2563
|
}
|
|
2212
2564
|
|
|
2213
2565
|
// =============================================================================
|
|
@@ -3369,7 +3721,7 @@ ${bold("COMMANDS")}
|
|
|
3369
3721
|
${cyan("find")} status | download | resume | cancel <job_id> Manage a find job ${dim("(free)")}
|
|
3370
3722
|
${cyan("find pricing")} Finder prices, balance, limits ${dim("(free)")}
|
|
3371
3723
|
${cyan("campaigns")} ... Email outreach: create, write, enroll, launch ${dim("(argorant campaigns help)")}
|
|
3372
|
-
${cyan("inboxes")} list | connect-google
|
|
3724
|
+
${cyan("inboxes")} list | connect-google | connect-smtp | test-smtp Connected mailboxes; connect Google Workspace or any provider
|
|
3373
3725
|
${cyan("inboxes")} quote | order | status | renewal New mailboxes on new domains ${dim("(saved card or payment link)")}
|
|
3374
3726
|
${cyan("inbox")} list | read | reply | forward | classify Replies across campaigns; answer in-thread or forward
|
|
3375
3727
|
${cyan("blocklist")} list | add | remove Addresses and domains never contacted
|
|
@@ -3524,5 +3876,6 @@ if (require.main === module) {
|
|
|
3524
3876
|
parseArgs, readFlags, flagValue, parseLimit, takeIdempotencyFlag, nextIdempotencyKey, isV1Write, IDEM,
|
|
3525
3877
|
errorMessage, errorCode, requestIdOf, detailMsg, need, EXIT, request, pageQuery, pickItems, fetchPages, retryWaitSeconds, RATE_RETRY,
|
|
3526
3878
|
companyFilters, numericId, fmtPrice, packLine, quoteLines, LOOKUP_KINDS, OWN_FLAG_COMMANDS, VERSION,
|
|
3879
|
+
parseCsv, mailboxesFromCsv, mailboxBody, normalizeSecurity, SMTP_BULK_CHUNK,
|
|
3527
3880
|
};
|
|
3528
3881
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "argorant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Search, count, reveal, export, and verify B2B contacts from the Argorant database, and find work emails by name and company domain, from your terminal, scripts, or coding agent.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"argorant": "bin/argorant.js"
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"url": "https://argorant.com/docs/cli"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
|
-
"prepublishOnly": "node scripts/prepublish-guard.js"
|
|
37
|
+
"prepublishOnly": "node scripts/prepublish-guard.js",
|
|
38
|
+
"test": "node --test test/*.test.js"
|
|
38
39
|
}
|
|
39
40
|
}
|