Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 42 additions & 9 deletions tools/v1/individual/spam-risk-checker/README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,48 @@
# Spam Risk Checker
# Spam Risk Checker (V1)

This folder is the isolated workspace for the Spam Risk Checker tool.
**Release Tier:** V1
**Audience:** Individual
**Labels:** GrantFox OSS, Maybe Rewarded, Official Campaign, Tooling Ecosystem, V1 Launch Tool, Individual Tool

## Ownership Boundary
## Overview

All work for this tool must stay inside:
The Spam Risk Checker is a small, isolated heuristic tool for reviewing incoming message content and producing a lightweight risk score. It is intentionally self-contained so contributors can validate it without touching the main mail application.

`text
.\tools\v1\individual\spam-risk-checker\
`
## Setup

Do not wire this tool into the main app, routing, inbox architecture, wallet core, Stellar core, database schema, or existing design system unless a future integration issue explicitly allows it.
1. From the repository root, ensure dependencies are installed.
2. Run the local test suite for this tool:
`npx vitest run tools/v1/individual/spam-risk-checker/tests/spamRiskChecker.test.ts`

See specs.md for the issue categories and contributor expectations.
## Usage

```ts
import { analyzeSpamRisk } from "./services/spamRiskChecker";

const result = analyzeSpamRisk({
subject: "Urgent update",
body: "Verify your account now and click the link.",
});

console.log(result.level, result.score, result.reasons);
```

## Fixtures

The local fixtures live in [tests/fixtures.ts](tests/fixtures.ts) and cover:

- a benign message that should score as low risk
- a suspicious message with urgency and bait language
- a mixed message that should score as medium risk

## Known Limitations

- The current implementation uses simple deterministic heuristics rather than machine learning.
- It evaluates message text only; it does not inspect headers, attachments, or sender reputation.
- The scoring is intentionally conservative so contributors can review and extend it easily.

## OSS Contributor Review Notes

- Review the service in [services/spamRiskChecker.ts](services/spamRiskChecker.ts) first.
- Validate the behavior in [tests/spamRiskChecker.test.ts](tests/spamRiskChecker.test.ts).
- Keep future inbox integration work as a separate follow-up issue.
16 changes: 16 additions & 0 deletions tools/v1/individual/spam-risk-checker/REVIEW_NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Review Notes

This folder is meant to be reviewed as a self-contained mini-product.

## What to validate

- The scoring logic lives in [services/spamRiskChecker.ts](services/spamRiskChecker.ts) and uses local heuristics only.
- The tests in [tests/spamRiskChecker.test.ts](tests/spamRiskChecker.test.ts) cover low, medium, and high-risk cases without depending on the main app.
- The fixture content in [tests/fixtures.ts](tests/fixtures.ts) stays local to the tool and can be extended without touching app-wide fixtures.
- The documentation in [README.md](README.md) explains setup, usage, and current limitations.

## Isolation checklist

- No imports from the app shell, dashboard, routing, wallet, or database layers were added.
- The tool can be reviewed and tested from this folder alone.
- Any future integration with the inbox experience should be tracked as a separate follow-up issue.
100 changes: 100 additions & 0 deletions tools/v1/individual/spam-risk-checker/services/spamRiskChecker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
export type SpamRiskLevel = "low" | "medium" | "high";

export interface SpamRiskAnalysis {
score: number;
level: SpamRiskLevel;
reasons: string[];
summary: string;
}

export interface SpamRiskInput {
subject?: string;
body?: string;
}

const URGENCY_PATTERN = /\b(urgent|immediately|act now|click now|limited time|final notice)\b/i;
const SPAM_BAIT_PATTERN =
/\b(free|winner|prize|cash|guaranteed|claim now|verify your account|reset your password)\b/i;
const FINANCE_PATTERN = /\b(crypto|bitcoin|investment|make money|earn money)\b/i;
const SHORTENED_LINK_PATTERN = /\b(bit\.ly|tinyurl|t\.co|ow\.ly)\b/i;
const EXTERNAL_LINK_PATTERN = /https?:\/\/\S+/i;

function normalizeText(value: string | undefined): string {
return (value ?? "").trim();
}

function dedupe(values: string[]): string[] {
return Array.from(new Set(values));
}

function classifyScore(score: number): SpamRiskLevel {
if (score >= 5) {
return "high";
}

if (score >= 2) {
return "medium";
}

return "low";
}

export function analyzeSpamRisk(input: string | SpamRiskInput): SpamRiskAnalysis {
const text =
typeof input === "string" ? input : [input.subject, input.body].map(normalizeText).join("\n");
const trimmedText = normalizeText(text);

if (!trimmedText) {
return {
score: 0,
level: "low",
reasons: [],
summary: "No content was provided for review.",
};
}

const reasons: string[] = [];
let score = 0;

if (URGENCY_PATTERN.test(trimmedText)) {
score += 2;
reasons.push("Urgency language");
}

if (SPAM_BAIT_PATTERN.test(trimmedText)) {
score += 2;
reasons.push("Common spam bait phrases");
}

if (FINANCE_PATTERN.test(trimmedText)) {
score += 2;
reasons.push("Financial promises");
}

if (SHORTENED_LINK_PATTERN.test(trimmedText) || EXTERNAL_LINK_PATTERN.test(trimmedText)) {
score += 1;
reasons.push("External links");
}

if (/!{2,}/.test(trimmedText)) {
score += 1;
reasons.push("Excessive punctuation");
}

const uppercaseWords = trimmedText.match(/\b[A-Z]{4,}\b/g) ?? [];
if (uppercaseWords.length > 0) {
score += Math.min(uppercaseWords.length, 2);
reasons.push("All-caps emphasis");
}

const level = classifyScore(score);
const verb =
level === "high" ? "looks suspicious" : level === "medium" ? "needs caution" : "looks ordinary";

return {
score,
level,
reasons: dedupe(reasons),
summary: `This message ${verb} based on ${reasons.length || 1} signal${reasons.length === 1 ? "" : "s"}.`,
};
}
43 changes: 12 additions & 31 deletions tools/v1/individual/spam-risk-checker/specs.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,22 @@
# Spam Risk Checker

Risk scoring for incoming mail.

## Scope

- Release tier: $(System.Collections.Hashtable.Tier.ToUpperInvariant())
- Audience: $(System.Collections.Hashtable.Audience)
- Folder ownership: $dir/

This is a self-contained tooling workspace. Do not wire this tool into the main app, routing, inbox architecture, wallet core, Stellar core, or design system unless a future integration issue explicitly allows it.

Recommended internal structure:

- components/
- services/
- hooks/
- ests/
- docs/
"@ | Set-Content -Path "tools/v1/individual/spam-risk-checker/README.md"
@"

# Spam Risk Checker Specs

## Purpose

Risk scoring for incoming mail.
Provide a lightweight, folder-local risk scoring helper for incoming message content.

## Contributor boundary

All work for this tool should stay in:
All work for this tool should stay inside this folder so it remains isolated from the main application shell and inbox architecture.

## Recommended structure

$dir/
- services/ for the scoring logic
- tests/ for local Vitest coverage
- fixtures for sample inputs
- docs for setup and review notes

## Required issue categories
## Review focus

- Architecture
- Feature
- UI and accessibility
- Security and performance
- Testing and documentation
- Keep the implementation deterministic and easy to reason about
- Prefer local fixtures over app-wide test data
- Avoid integration work unless a dedicated follow-up issue is created
17 changes: 17 additions & 0 deletions tools/v1/individual/spam-risk-checker/tests/fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const benignFixture = [
"Hi Maya,",
"I wanted to confirm our lunch plans for Friday.",
"Please let me know if 2 PM works for you.",
].join("\n");

export const suspiciousFixture = [
"URGENT! Verify your account now!!!",
"Click now to claim your free prize before midnight.",
"https://bit.ly/claim-now",
].join("\n");

export const mixedFixture = [
"Quick question about the invoice.",
"Please review the proposal before the meeting now!!!",
"https://example.com/review",
].join("\n");
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";

import { analyzeSpamRisk } from "../services/spamRiskChecker";
import { benignFixture, mixedFixture, suspiciousFixture } from "./fixtures";

describe("Spam Risk Checker", () => {
it("returns a low-risk assessment for ordinary mail", () => {
const result = analyzeSpamRisk(benignFixture);

expect(result.level).toBe("low");
expect(result.score).toBe(0);
expect(result.reasons).toEqual([]);
expect(result.summary).toContain("ordinary");
});

it("flags urgent, bait-heavy content as high risk", () => {
const result = analyzeSpamRisk(suspiciousFixture);

expect(result.level).toBe("high");
expect(result.score).toBeGreaterThanOrEqual(6);
expect(result.reasons).toEqual(
expect.arrayContaining(["Urgency language", "Common spam bait phrases", "External links"]),
);
});

it("uses the object form for subject and body input", () => {
const result = analyzeSpamRisk({
subject: "Quick question",
body: mixedFixture,
});

expect(result.level).toBe("medium");
expect(result.score).toBeGreaterThan(0);
expect(result.reasons).toContain("External links");
});

it("returns a neutral result for empty input", () => {
const result = analyzeSpamRisk("");

expect(result.level).toBe("low");
expect(result.score).toBe(0);
expect(result.summary).toContain("No content");
});
});