@novacraft-engineering/mailbox 0.4.18 → 0.4.19

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 (2) hide show
  1. package/lib/mailbox.ts +17 -9
  2. package/package.json +1 -1
package/lib/mailbox.ts CHANGED
@@ -593,7 +593,15 @@ export type RiskJudgement = { risk: Risk; reasons: string[]; score: number; quar
593
593
  export function judgeMessage(signals: RiskSignals, standing: SenderStanding): RiskJudgement {
594
594
  const reasons: string[] = []
595
595
  let score = 0
596
- const add = (weight: number, why: string) => { score += weight; reasons.push(why) }
596
+ // A finding is "telling" when it is hard to trip by accident. Failing an authentication
597
+ // check or shouting in the subject line is neither: ordinary mail does both. Holding a
598
+ // message back takes at least one finding of the first kind, however the weights add up.
599
+ let telling = 0
600
+ const add = (weight: number, why: string, isTelling = false) => {
601
+ score += weight
602
+ if (isTelling) telling += 1
603
+ reasons.push(why)
604
+ }
597
605
 
598
606
  if (/^fail$/i.test((signals.virus ?? '').trim())) {
599
607
  return { risk: 'virus', reasons: ['A virus scan failed on this message'], score: 100, quarantine: true }
@@ -604,12 +612,12 @@ export function judgeMessage(signals: RiskSignals, standing: SenderStanding): Ri
604
612
  const trusted = standing.replied > 0 && standing.markedSpam === 0
605
613
  if (standing.markedSpam > 0) {
606
614
  add(4 + Math.min(standing.markedSpam, 4),
607
- `You marked ${standing.markedSpam} earlier message${standing.markedSpam === 1 ? '' : 's'} from this sender as spam`)
615
+ `You marked ${standing.markedSpam} earlier message${standing.markedSpam === 1 ? '' : 's'} from this sender as spam`, true)
608
616
  } else if (standing.trashed >= 3 && standing.replied === 0) {
609
- add(3, `You have deleted ${standing.trashed} messages from this sender without ever replying`)
617
+ add(3, `You have deleted ${standing.trashed} messages from this sender without ever replying`, true)
610
618
  }
611
619
 
612
- if (/^fail$/i.test((signals.spam ?? '').trim())) add(4, 'The provider\u2019s spam filter flagged this message')
620
+ if (/^fail$/i.test((signals.spam ?? '').trim())) add(4, 'The provider\u2019s spam filter flagged this message', true)
613
621
 
614
622
  const authenticated = /^pass$/i.test((signals.dmarc ?? '').trim())
615
623
  // Heavy, but not enough on its own to hide a message: mail forwarded through a list
@@ -628,14 +636,14 @@ export function judgeMessage(signals: RiskSignals, standing: SenderStanding): Ri
628
636
  const free = freeProviders()
629
637
  const freeReply = replyDomains.find(entry => free.has(entry))
630
638
  if (freeReply && fromDomain && !free.has(fromDomain)) {
631
- add(4, `Replies to this message go to ${freeReply}, not to ${fromDomain}`)
639
+ add(4, `Replies to this message go to ${freeReply}, not to ${fromDomain}`, true)
632
640
  } else if (replyDomains.length) {
633
641
  add(1, `Replies go to ${replyDomains[0]} rather than ${fromDomain || 'the sender'}`)
634
642
  }
635
643
 
636
644
  const tld = fromDomain.split('.').pop() ?? ''
637
- if (throwawayTlds().has(tld)) add(2, `The sender\u2019s domain ends in .${tld}, which is cheap to register and often disposable`)
638
- if (/^\d{4,}$/.test(fromDomain.split('.')[0] ?? '')) add(2, 'The sender\u2019s domain name is just a string of digits')
645
+ if (throwawayTlds().has(tld)) add(2, `The sender\u2019s domain ends in .${tld}, which is cheap to register and often disposable`, true)
646
+ if (/^\d{4,}$/.test(fromDomain.split('.')[0] ?? '')) add(2, 'The sender\u2019s domain name is just a string of digits', true)
639
647
 
640
648
  const subject = (signals.subject ?? '').trim()
641
649
  const letters = subject.replace(/[^A-Za-z]/g, '')
@@ -643,7 +651,7 @@ export function judgeMessage(signals: RiskSignals, standing: SenderStanding): Ri
643
651
 
644
652
  const body = (signals.text ?? '').toLowerCase()
645
653
  const hits = scamPhrases().filter(phrase => body.includes(phrase))
646
- if (hits.length >= 2) add(3, `The wording follows a known advance-fee approach (${hits.slice(0, 3).join(', ')})`)
654
+ if (hits.length >= 2) add(3, `The wording follows a known advance-fee approach (${hits.slice(0, 3).join(', ')})`, true)
647
655
  else if (hits.length === 1) add(1, `Wording associated with advance-fee mail (${hits[0]})`)
648
656
 
649
657
  // Never heard from before is not suspicious by itself — everyone writes once for the
@@ -660,7 +668,7 @@ export function judgeMessage(signals: RiskSignals, standing: SenderStanding): Ri
660
668
  // time teaches the reader to ignore the warning.
661
669
  if (score < flagAt()) return { risk: 'clean', reasons: [], score, quarantine: false }
662
670
 
663
- const quarantine = score >= limit
671
+ const quarantine = score >= limit && telling > 0
664
672
  return { risk: quarantine ? 'spam' : 'suspicious', reasons, score, quarantine }
665
673
  }
666
674
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@novacraft-engineering/mailbox",
3
- "version": "0.4.18",
3
+ "version": "0.4.19",
4
4
  "description": "A shared webmail app: one codebase, one deployment per mailbox. Threads, a rich composer with signatures, attachments on S3-compatible storage, sharing links, full-text search, web push and PWA install.",
5
5
  "keywords": [
6
6
  "webmail",