@rackbops/plugin-tracker 0.25.0 → 0.27.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.
Files changed (3) hide show
  1. package/README.md +12 -4
  2. package/dist/plugin.js +70 -13
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -494,16 +494,24 @@ eBay's saved-search alerts reach eBay watches by mail (0.25.0), while `TRACKER_M
494
494
  an outage, bounces to the mailbox that forwarded it; that day's alert is not retried. The Worker
495
495
  is deployed by pasting `mail-worker/worker.js` into the Cloudflare dashboard's Worker editor; it is
496
496
  not in the plugin's npm bundle.
497
- - **What a message does.** Gmail's forwarding confirmation (from `forwarding-noreply@google.com`):
498
- its code is DMed, to enter in Gmail's Forwarding and POP/IMAP settings. An eBay alert: the
497
+ - **What a message does.** Gmail's forwarding confirmation: its code is DMed, to enter in Gmail's
498
+ Forwarding and POP/IMAP settings. Since 0.26.0 it is any sender at Google naming forwarding. Since
499
+ 0.27.0 the code is a run of digits standing alone in the subject or shortly after the word
500
+ "code" in the body, and Gmail's verify link (a `/mail/vf-` address on `mail.google.com`,
501
+ `mail-settings.google.com` or a subdomain of either, never its cancel link) is DMed too; digits inside
502
+ an address are never the code. An eBay alert: the
499
503
  matches above "You might also like" (never eBay's suggestions) -- title, the item's bare
500
504
  `/itm/<number>` address, the price's lower end -- go into the inbox of each of the person's live
501
505
  eBay watches whose words are the saved search's (case and spacing aside), exactly as listings sent
502
506
  through the task API; the watch's next look DMs the new ones. Words match with case, spacing and
503
507
  the marks `` ` * _ ~ | < > @ # [ ] ( ) \ : / `` ignored. An alert no watch searches for gets a
504
508
  DM saying so, with the `/want` that would watch it, each time eBay sends it. Anything else is
505
- dropped. A message is never stored, only the listings read from it, and nothing from it is
506
- logged.
509
+ dropped. A message is never stored, only the listings read from it, and its words are never
510
+ logged: one that does nothing is logged only with why (the owner is not registered, or its
511
+ sender's domain -- a Google no-reply sender whole -- which bodies it had, and whether its
512
+ subject names forwarding; for Google's no-reply senders also its subject's shape, letters `a`,
513
+ digits `9` and each address `<addr>`, and counts from its body as read, plain text else the
514
+ HTML's text).
507
515
  - **Only eBay's own domains** (ebay.com, .co.uk, .de, .ca, .com.au and the rest of eBay's sites;
508
516
  never a lookalike such as `ebay.com.de`) count as eBay, for the sender and for every item link.
509
517
  The sender is its `From` address, not a checked signature: the private address is the guard, and
package/dist/plugin.js CHANGED
@@ -13598,7 +13598,6 @@ var MAIL_DOMAIN_KEY = "TRACKER_MAIL_DOMAIN";
13598
13598
  var MAIL_KEY_KEY = "TRACKER_MAIL_KEY";
13599
13599
  var MAIL_DOMAIN_FORMAT = "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$";
13600
13600
  var MIN_MAIL_KEY = 32;
13601
- var GMAIL_FORWARDING = "forwarding-noreply@google.com";
13602
13601
  var LOCAL = /^m-[0-9a-f]{24}$/;
13603
13602
  function parseMailConfig(env) {
13604
13603
  const domain = (env[MAIL_DOMAIN_KEY] ?? "").trim();
@@ -13644,13 +13643,63 @@ class MailAddresses {
13644
13643
  return row?.user_id ?? null;
13645
13644
  }
13646
13645
  }
13647
- function forwardingCode(mail) {
13648
- if (mail.from !== GMAIL_FORWARDING || !/forwarding confirmation/i.test(mail.subject))
13646
+ function fromGoogle(address) {
13647
+ const host = address?.slice(address.lastIndexOf("@") + 1) ?? "";
13648
+ return host === "google.com" || host.endsWith(".google.com");
13649
+ }
13650
+ var GMAIL_LINK = /https:\/\/(?:[a-z0-9-]{1,63}\.){0,4}(?:mail|mail-settings)\.google\.com\/[A-Za-z0-9\-._~%/?#&=+:@!$,;]{1,1000}/gi;
13651
+ function decodedHtmlLinks(html) {
13652
+ return html.slice(0, MAX_HTML_CHARS).replace(/&(?:amp|#0*38|#x0*26);/gi, "&");
13653
+ }
13654
+ function trimClosingMarks(s) {
13655
+ let end = s.length;
13656
+ while (end > 0 && ".,;:!?".includes(s.charAt(end - 1)))
13657
+ end--;
13658
+ return s.slice(0, end);
13659
+ }
13660
+ function gmailLink(s) {
13661
+ for (const l of s.match(GMAIL_LINK) ?? []) {
13662
+ const link = trimClosingMarks(l);
13663
+ if (/^https:\/\/[^/]+\/mail\/vf-/i.test(link))
13664
+ return link;
13665
+ }
13666
+ return null;
13667
+ }
13668
+ function withoutAddresses(s) {
13669
+ return s.split(/\s+/).filter((w) => !w.includes("@")).join(" ");
13670
+ }
13671
+ function bodyText(mail) {
13672
+ return mail.text || (mail.html !== null ? htmlText(mail.html.slice(0, MAX_HTML_CHARS)) : "");
13673
+ }
13674
+ function forwardingConfirmation(mail) {
13675
+ if (!fromGoogle(mail.from))
13649
13676
  return null;
13650
- const inSubject = /\(#(\d{6,12})\)/.exec(mail.subject)?.[1];
13651
- if (inSubject)
13652
- return inSubject;
13653
- return /confirmation code:\s*(\d{6,12})/i.exec(mail.text ?? mail.html ?? "")?.[1] ?? null;
13677
+ const leading = /^\(#(\d{6,12})\)/.exec(mail.subject)?.[1] ?? null;
13678
+ const body = bodyText(mail);
13679
+ if (!leading && !/forwarding/i.test(mail.subject) && !/forward/i.test(body))
13680
+ return null;
13681
+ const subject = withoutAddresses(mail.subject.slice(0, 500));
13682
+ const code = leading ?? /(?:^|[\s#(])(\d{6,12})(?=$|[\s).,:;])/.exec(subject)?.[1] ?? /\bcode\b\D{0,40}?(?<![\w.@/=%?&#:-])(\d{6,12})(?!\d)/i.exec(withoutAddresses(body))?.[1] ?? null;
13683
+ const link = gmailLink(mail.text ?? "") ?? (mail.html !== null ? gmailLink(decodedHtmlLinks(mail.html)) : null);
13684
+ return code || link ? { code, link } : null;
13685
+ }
13686
+ function shape(s) {
13687
+ return s.replace(/\p{L}/gu, "a").replace(/\p{N}/gu, "9").replace(/[^\x20-\x7e]/g, "?").slice(0, 160);
13688
+ }
13689
+ function subjectShape(subject) {
13690
+ return subject.slice(0, 500).split(/\s+/).map((w) => w.includes("@") ? "<addr>" : shape(w)).join(" ").slice(0, 160);
13691
+ }
13692
+ function ignoredWhy(mail) {
13693
+ const system = mail.from !== null && fromGoogle(mail.from) && /^[a-z-]*no-?reply$/.test(mail.from.slice(0, mail.from.lastIndexOf("@")));
13694
+ const domain = mail.from ? system ? mail.from : mail.from.slice(mail.from.lastIndexOf("@") + 1) : "none";
13695
+ const parts = [mail.html !== null ? "html" : "", mail.text !== null ? "text" : ""].filter(Boolean).join("+") || "no body";
13696
+ const base = `from ${domain.replace(/[^a-z0-9.@-]/g, "?").slice(0, 100)}, ${parts}, subject ${/forwarding/i.test(mail.subject) ? "names" : "does not name"} forwarding`;
13697
+ if (!system)
13698
+ return base;
13699
+ const body = bodyText(mail);
13700
+ const runs = (body.match(/\d{6,12}/g) ?? []).length;
13701
+ const links = (body.match(/https:\/\/[a-z0-9.-]{1,253}/gi) ?? []).length;
13702
+ return `${base}, subject shape "${subjectShape(mail.subject)}", body ${body.length} chars, ${runs} digit run(s), ${links} https link(s), says code: ${/code/i.test(body) ? "yes" : "no"}`;
13654
13703
  }
13655
13704
  function quoted(words) {
13656
13705
  return words.replace(SEARCH_MARKS, " ").replace(/\s+/g, " ").trim().slice(0, 100);
@@ -13678,15 +13727,19 @@ async function tell(d, userId, text) {
13678
13727
  async function deliverMail(d, userId, mail) {
13679
13728
  const user = await d.store.getUser(userId);
13680
13729
  if (!user || !d.admissions.isRegistered(user.id) || !d.inbox)
13681
- return { kind: "ignored" };
13682
- const code = forwardingCode(mail);
13683
- if (code) {
13684
- await tell(d, user.id, `Gmail asks to confirm forwarding to your alert address. The confirmation code is ${code}: enter it in Gmail's Settings, under Forwarding and POP/IMAP. If you did not ask for this, ignore it.`);
13730
+ return { kind: "ignored", why: "the owner is not registered" };
13731
+ const confirm = forwardingConfirmation(mail);
13732
+ if (confirm) {
13733
+ const how = [
13734
+ confirm.code ? `The confirmation code is ${confirm.code}: enter it in Gmail's Settings, under Forwarding and POP/IMAP.` : "",
13735
+ confirm.link ? `${confirm.code ? "Or open" : "Open"} Gmail's confirmation link: <${confirm.link}>` : ""
13736
+ ].filter(Boolean).join(" ");
13737
+ await tell(d, user.id, `Gmail asks to confirm forwarding to your alert address. ${how} If you did not ask for this, ignore it.`);
13685
13738
  return { kind: "confirmation" };
13686
13739
  }
13687
13740
  const alert = readEbayAlert(mail);
13688
13741
  if (!alert)
13689
- return { kind: "ignored" };
13742
+ return { kind: "ignored", why: ignoredWhy(mail) };
13690
13743
  const listings = [];
13691
13744
  for (const raw of alert.listings) {
13692
13745
  const l = acceptListing(raw);
@@ -13758,7 +13811,7 @@ function params(value) {
13758
13811
  return { value: first.trim().toLowerCase(), params: out };
13759
13812
  }
13760
13813
  function quotedPrintable(body) {
13761
- const s = body.replace(/=\r?\n/g, "").replace(/=([0-9A-Fa-f]{2})/g, (_, h) => String.fromCharCode(Number.parseInt(h, 16)));
13814
+ const s = body.replace(/=[ \t]*\r?\n/g, "").replace(/=([0-9A-Fa-f]{2})/g, (_, h) => String.fromCharCode(Number.parseInt(h, 16)));
13762
13815
  return bytesOf(s);
13763
13816
  }
13764
13817
  function base64(body) {
@@ -13884,8 +13937,12 @@ async function handleMailInbound(m, request) {
13884
13937
  const outcome = await deliverMail(d, userId, parseMail(raw));
13885
13938
  if (outcome.kind === "stored")
13886
13939
  d.log.info(`mail: an eBay alert gave ${outcome.listings} listing(s) to ${outcome.watches} watch(es)`);
13940
+ else if (outcome.kind === "ignored")
13941
+ d.log.info(`mail: one message ignored (${outcome.why})`);
13887
13942
  else
13888
13943
  d.log.info(`mail: one message ${outcome.kind}`);
13944
+ if (outcome.kind === "ignored")
13945
+ return answer(200, { result: "ignored" });
13889
13946
  const { kind, ...rest } = outcome;
13890
13947
  return answer(200, { result: kind, ...rest });
13891
13948
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rackbops/plugin-tracker",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "description": "The task tracker for Rackbops Clerk: reminders, renewals, price drops, one-off research and an interest scout (through city-hall, with their findings), a want-list watcher (listing pages you paste, BoardGameGeek through its API once its token is set, and eBay and BoardGameGeek listings you send in through the task API or that eBay's saved-search alert emails forwarded to your alert address carry, which the watcher never opens; with the model checking each new listing and its seller first, through city-hall) by slash command, delivered by DM with buttons, and a daily DM at each person's hour of what is due today and overdue, on docket's SQLite store (/allow, /register, /remind, /renewal, /price, /research, /scout, /want, /tasks, /task, /settings, /web), with a web area for your tasks, history and settings, a web editor to make, change, pause and delete your own tasks (research requests too, made but not changed, and scouts, made and changed), an admin view (with the DMs that did not arrive, raising a person's daily model ceiling, and daily model usage per person), an operator switch that turns the daily budgets off for the alpha, forget-me, and a JSON task API with personal API tokens",
5
5
  "type": "module",
6
6
  "main": "dist/plugin.js",