20 KiB
Known-Spam Fast Close Implementation Plan
Date: 2026-07-09
Project: /home/jason/projects/redmine
Primary tool: redmine_tickets.py
Target Redmine project: customer-service
Production base URL: https://rm.ldrprep.com
Agent write attribution: rebot / Harry Botter
Goal
Build a reliable two-prong production spam-cleanup workflow:
- Slow/reviewed path — the current classifier-driven
sync → spam-check → close-if-spamflow, used for discovering and validating new spam patterns. - Fast known-profile path — a verified-profile bulk closer that quickly locates all open
customer-servicetickets matching known spam sender/profile rules and closes them in one bounded, idempotent production operation.
The immediate business goal is to close known promotional/spam clutter quickly without risking customer tickets.
Current Reality
The slow path already works in production:
redmine_tickets.py synccaches enriched tickets usinginclude=journals,helpdesk.spam-checkcategorizes tickets into classes such as:spam-promotionalautomated-operationalunknown-review
close-if-spamcloses reviewedspam-promotionaltickets using--impersonate-user rebot.- Writes are verified by API as
status=Closedand close-note authorHarry B.. - Idempotency is recorded via local action markers.
- Customer-contact guard is active: before spam-close, the tool fetches the Helpdesk contact and skips if the contact has tag
Customer.
Validated production cleanup so far:
- First production pass closed 20 reviewed promotional-junk tickets.
- Current 200-ticket cache has a reviewed 27-ticket candidate set pending apply.
- Customer pre-tagging has been performed for confirmed customers including Cyberstorm, Esire-family contacts, Shinkiya, Bohoflow, BavariaTrachten, Solride, and u2wear.
Known safety rule:
Absence of
Customerdoes not imply spam. Presence ofCustomerblocks automated spam closure.
Desired Two-Prong Architecture
┌────────────────────────────┐
│ redmine_tickets.py substrate│
│ sync/cache/helpdesk/actions │
└──────────────┬─────────────┘
│
┌───────────────────────────┴───────────────────────────┐
│ │
Slow / reviewed path Fast / known profile path
discover new patterns close already-verified spam
│ │
sync → spam-check → review → close-if-spam known-spam-profile scan → dry-run → apply
│ │
produces new safe profiles uses frozen allowlisted rules
Slow path responsibilities
Use for:
- New sender types.
- Ambiguous business/vendor/customer cases.
- Classifier development.
- False-positive discovery.
- Contact/customer enrichment.
Fast path responsibilities
Use for:
- Previously reviewed spam senders/domains/campaigns.
- High-volume obvious junk such as known promo newsletters and sales blasts.
- Rapid production cleanup once a profile has been approved.
The fast path must not rediscover intent. It should only act on a curated profile registry.
Fast Known-Spam Profile Design
Risk Model Update — Closed Is Recoverable
Jason clarified an important Redmine/Helpdesk behavior:
- Closed tickets are not deleted.
- If the other party sends a new message to a closed ticket, Redmine reopens it and status changes to
New Message.
That means accidental closure is usually recoverable if the other party still cares enough to reply. The larger risk is re-closing a ticket after the sender already reopened it, because that means the system is ignoring renewed human intent.
Policy consequence:
- If a ticket is clearly
spam-promotionaland stale, especiallyupdated_onolder than 30 days, it is reasonable to close automatically once other safety gates pass. - If a ticket has evidence that it was previously closed and later reopened by a new Helpdesk message, do not close it automatically, even if it matches spam.
- Instead, escalate it for human attention: bump priority one level if possible and optionally set a due date 1–3 days out.
Terminology
A known spam profile is a conservative, reviewed rule bundle that identifies tickets we are comfortable closing automatically, subject to safety gates.
Example profiles:
profiles:
globalindustrial-promos:
senders:
- reply@globalindustrial.com
subject_patterns:
- "(?i)\\b(clearance|sale|save|% off|bestsellers|fans|blowers|cooling|storage|power tools|kee safety)\\b"
category: spam-promotional
confidence: high
notes: "Repeated promotional vendor emails; reviewed in LAN + prod broom passes."
vistaprint-promos:
senders:
- noreply@mail.vistaprint.com
subject_patterns:
- "(?i)\\b(sale|save|% off|email exclusive|ends|qualifying orders|favorite sale|customer faves)\\b"
category: spam-promotional
confidence: high
qnap-newsletters:
senders:
- enews-noreply@qnap.com
subject_patterns:
- "(?i)\\b(qnap|photodirector|nas|security advisory|product of the year|jc-star|file backup)\\b"
category: spam-promotional
confidence: high
business-journals-promos:
senders:
- replycincinnati@engaged.bizjournals.com
- replycincinnati@partner.bizjournals.com
- acollett@bizjournals.com
subject_patterns:
- "(?i)\\b(events|nominations|business summit|brand next to key business lists|chambercomp)\\b"
category: spam-promotional
confidence: high
Safety gates always applied before close
Fast path must still apply these universal gates:
- Ticket must be open.
- Ticket must be in
customer-serviceunless explicitly overridden. - Ticket Helpdesk contact must be fetched.
- Contact with tag
Customermust be skipped. - Optional: contact with tag
maybe-customershould be skipped by default unless--allow-maybe-customeris explicitly passed. - Ticket must not already have local
known-spam-close/close-if-spammarker. - If the ticket was previously closed and then reopened by a new Helpdesk message, skip spam-close and escalate instead.
- Apply writes must impersonate
rebot. - Apply must produce a verification report.
Reopened-ticket escalation behavior
Add a helper such as ticket_was_reopened_after_close(issue) that inspects journals/details for evidence of a transition from Closed/Resolved into New Message or another open status after a Helpdesk reply.
When true, do not close as spam. Instead, planned behavior should be:
{
"priority_id": "one level higher if known/mappable",
"due_date": "today + 1 to 3 days",
"notes": "Matched spam profile, but ticket appears to have been reopened after closure. Escalating for human review instead of auto-closing."
}
This should be its own idempotent action marker, e.g. spam-reopen-escalation, so the ticket is not bumped repeatedly.
Files to Modify
- Modify:
/home/jason/projects/redmine/redmine_tickets.py - Modify:
/home/jason/projects/redmine/tests/test_redmine_tickets.py - Create:
/home/jason/projects/redmine/profiles/known-spam/customer-service.json - Create:
/home/jason/projects/redmine/docs/rebot_legacy_spam_rules.md - Create/Update:
/home/jason/projects/redmine/docs/known_spam_fast_close_plan.md - Update:
/home/jason/projects/redmine/HANDOFF.md - Update:
/home/jason/projects/redmine/docs/ticket_spam_cleanup_handoff_2026-07-09.md - Optional later:
/home/jason/projects/redmine/docs/known_spam_profile_registry.md
Proposed CLI
Dry-run known-spam scan
cd /home/jason/projects/redmine
./redmine_tickets.py \
--base-url https://rm.ldrprep.com \
--project customer-service \
--cache-dir .cache/redmine_tickets_prod_known_spam_$(date +%Y%m%dT%H%M%SZ) \
known-spam-scan \
--profile profiles/known-spam/customer-service.yaml \
--status-id open \
--max-tickets 2000
Expected output:
Known-spam scan complete: seen=1732 matched=214 skipped_customer=3 skipped_maybe_customer=5 planned=206 apply=False
profile=globalindustrial-promos issue=... sender=... subject=...
...
Dry run only. Re-run known-spam-close --apply using this cache/profile after review.
Apply known-spam close
./redmine_tickets.py \
--base-url https://rm.ldrprep.com \
--project customer-service \
--cache-dir .cache/redmine_tickets_prod_known_spam_<timestamp> \
known-spam-close \
--profile profiles/known-spam/customer-service.yaml \
--close-status-id 5 \
--impersonate-user rebot \
--apply
Expected output:
Known-spam close complete: planned=206 closed=206 skipped_customer=0 skipped_existing_marker=0 apply=True
Verify apply
./redmine_tickets.py \
--base-url https://rm.ldrprep.com \
--project customer-service \
--cache-dir .cache/redmine_tickets_prod_known_spam_<timestamp> \
verify-known-spam-close
Expected output:
Verify known-spam close: checked=206 closed=206 author_harry_b=206 failures=0
Implementation Tasks
Task 1: Add profile loader
Objective: Load known-spam YAML profiles safely.
Files:
- Modify:
redmine_tickets.py - Test:
tests/test_redmine_tickets.py - Create:
profiles/known-spam/customer-service.yaml
Implementation notes:
- Use stdlib if PyYAML is not guaranteed? Existing environment may support YAML via project deps, but avoid installing packages.
- Safer first version: store profile as JSON instead of YAML if dependency uncertainty matters.
- Recommended file if avoiding deps:
profiles/known-spam/customer-service.json
Test cases:
- valid profile loads
- missing required fields fails
- invalid regex fails with helpful error
Acceptance:
python3 -m unittest discover -s tests -p 'test_redmine_tickets.py' -v
python3 -m py_compile redmine_tickets.py tests/test_redmine_tickets.py redmine_contacts.py
Task 2: Implement known-profile matcher
Objective: Determine whether a cached ticket matches a known spam profile.
Files:
- Modify:
redmine_tickets.py - Test:
tests/test_redmine_tickets.py
Rules:
A ticket matches when:
- sender matches one of profile senders/domains, and
- subject/body pattern matches if provided, and
- category is
spam-promotional, and - it does not match profile exclusions.
Test examples:
reply@globalindustrial.com+Clearance Sale: Top Steel Storage And Power Toolsmatchesglobalindustrial-promos.reply@globalindustrial.com+ a real invoice/order/customer thread does not match if exclusion pattern present.enews-noreply@qnap.com+QNAP Security Advisorymatches only if we decide QNAP security advisories are clutter; this profile should be explicit so no one later acts surprised.
Task 3: Add customer/maybe-customer universal skip gates
Objective: Ensure known-spam fast path has the same safety guard as slow path.
Files:
- Modify:
redmine_tickets.py - Test:
tests/test_redmine_tickets.py
Tests:
- contact tagged
Customeris skipped even if ticket matches known profile. - contact tagged
maybe-customeris skipped by default. --allow-maybe-customerpermits maybe-customer if explicitly requested.- absence of tags does not block known spam profile matching.
Acceptance:
Fast path must never bypass customer safety.
Task 3B: Detect reopened-after-close tickets and escalate instead of closing
Objective: Prevent the cleaner from repeatedly closing a ticket after the other party has reopened it with a new message.
Files:
- Modify:
redmine_tickets.py - Test:
tests/test_redmine_tickets.py
Implementation notes:
- Add
ticket_was_reopened_after_close(issue). - Inspect
issue['journals']and theirdetailsfor status changes. - Treat evidence of Closed/Resolved → New Message/open status as reopened-after-close.
- If exact status IDs are easier than names, map known status names/IDs from existing Redmine data before hardcoding.
- Do not rely on current status alone; the point is historical reopening.
Tests:
- Journal with status change Closed → New Message returns true.
- Journal with only New → Closed returns false.
- Known-spam close skips reopened-after-close tickets.
- Apply path records
spam-reopen-escalationinstead ofknown-spam-close.
Escalation behavior:
- Dry-run reports the escalation plan.
- Apply should update priority/due date only when explicit
--apply-escalationsor equivalent is provided; do not mix escalation writes into spam-close apply without a clear flag.
Task 4: Add known-spam-scan command
Objective: Sync/search open tickets and report matches without closing.
Files:
- Modify:
redmine_tickets.py - Test:
tests/test_redmine_tickets.py
Behavior:
- Fetch open
customer-servicetickets withinclude=journals,helpdesk. - Cache ticket payloads.
- Evaluate known spam profiles.
- Fetch contacts for matched tickets only.
- Report:
- matched profile
- issue ID
- sender
- subject
- contact ID
- age /
updated_on - skipped reason if skipped
- Mark tickets older than 30 days and matching a verified
spam-promotionalprofile as strong fast-close candidates. - Newer tickets may still be candidates, but the report should separate them from stale 30+ day candidates.
- Record local action marker:
known-spam-scan = matched|matched-stale|skipped-customer|skipped-maybe-customer|skipped-reopened|not-matched
Dry-run only: This command should never write Redmine.
Task 5: Add known-spam-close command
Objective: Close the previously scanned/planned known-spam tickets.
Files:
- Modify:
redmine_tickets.py - Test:
tests/test_redmine_tickets.py
Behavior:
- Reads cache + scan markers.
- Only closes tickets with marker
known-spam-scan = matched. - Re-checks contact tags before close. Do not rely only on stale scan state.
- Updates ticket with:
{
"status_id": 5,
"notes": "Closed as known promotional spam by redmine_tickets.py known-spam-close. Profile: <profile-name>."
}
- Writes as
rebot. - Records:
known-spam-close = closed- profile name
- close payload
- verification pending
Tests:
- apply closes only matched markers
- second apply closes zero
- customer tag recheck blocks closure even if scan marker was matched
- impersonation defaults to
rebot
Task 6: Add verification command/report
Objective: Verify production writes after apply.
Files:
- Modify:
redmine_tickets.py - Test:
tests/test_redmine_tickets.py
Behavior:
For every known-spam-close = closed marker:
- fetch issue with
include=journals,helpdesk - verify
status.name == Closed - verify close note exists
- verify note author is
Harry B. - print failures clearly
Output:
Verify known-spam close: checked=206 closed=206 author_harry_b=206 failures=0
Task 7: Seed first known-spam profile registry from verified production data
Objective: Convert already-validated spam senders into conservative profiles.
Initial profile groups:
- Global Industrial promos
- Vistaprint promos
- QNAP newsletters/promos
- Business Journals / BizJournals promo/events
- TikTok Shop seller promos
- Popmenu restaurant promos
- Samsung promo newsletters
- Cintas mop/product promos
- Wings and Rings restaurant promos
- SellerEssentials promos
Do not include yet without explicit decision:
sales@supermarketdistribution.com/ stock/MOQ updates — may be vendor-ish; review first.- QNAP security advisories — likely clutter, but security language can look operational; make it explicit in profile if included.
- Any sender with
Customerormaybe-customercontacts.
Task 8: Production rollout procedure
Objective: Define exact operator sequence for today.
Step A: Commit or checkpoint local code
Before production bulk close, ensure current code/tests/docs are checkpointed in git or at least git diff saved.
cd /home/jason/projects/redmine
git status --short
python3 -m unittest discover -s tests -p 'test_redmine_tickets.py' -v
python3 -m py_compile redmine_tickets.py tests/test_redmine_tickets.py redmine_contacts.py
Step B: First fast-path dry run
./redmine_tickets.py \
--base-url https://rm.ldrprep.com \
--project customer-service \
--cache-dir .cache/redmine_tickets_prod_known_spam_$(date +%Y%m%dT%H%M%SZ) \
known-spam-scan \
--profile profiles/known-spam/customer-service.json \
--status-id open \
--max-tickets 2000
Step C: Human review
Review summary counts and sample candidate rows. If any candidate looks like customer/prospect/operational context, do not apply; add exclusion/profile refinement.
Step D: Apply
./redmine_tickets.py \
--base-url https://rm.ldrprep.com \
--project customer-service \
--cache-dir <reviewed-cache> \
known-spam-close \
--profile profiles/known-spam/customer-service.json \
--close-status-id 5 \
--impersonate-user rebot \
--apply
Step E: Verify
./redmine_tickets.py \
--base-url https://rm.ldrprep.com \
--project customer-service \
--cache-dir <reviewed-cache> \
verify-known-spam-close
Step F: Handoff update
Update:
HANDOFF.mddocs/ticket_spam_cleanup_handoff_2026-07-09.md- optionally
docs/known_spam_profile_registry.md
Acceptance Criteria
The implementation is acceptable when:
- Slow path still works and tests pass.
- Fast path can dry-run all open production customer-service tickets without writes.
- Fast path can report exact match profile per candidate.
- Fast path skips contacts tagged
Customer. - Fast path skips contacts tagged
maybe-customerby default. - Fast path writes only when
--applyis passed. - Fast path writes as
rebot. - Second apply is idempotent.
- Verification command proves status closed + Harry B. note author.
- Profile registry is conservative and versioned in the repo.
Short-Term Recommendation for Today
Before building the full fast-profile command, there is still an immediate safe action available:
- Apply the currently reviewed 27-ticket production close set from:
.cache/redmine_tickets_prod_200_promotional_eval_1783612885
- Verify closed / Harry B. / idempotency.
- Then implement the fast known-spam profile path above.
This keeps the broom moving while the turbo-broom gets brakes installed. Very advanced broom science.
Legacy Rebot Rule Extraction Status
Completed source inspection of old Rebot spam/routing logic and captured it in:
docs/rebot_legacy_spam_rules.md
profiles/known-spam/customer-service.json
Validated profile seed:
JSON_OK
profiles 7
globalindustrial-promos spam-promotional 1 sender regex / 1 subject regex
vistaprint-promos spam-promotional 1 / 1
qnap-newsletters spam-promotional 1 / 1
business-journals-promos spam-promotional 3 / 1
legacy-rebot-promotional-senders spam-promotional 16 / 0
legacy-rebot-operational-clutter automated-operational 4 / 10
legacy-rebot-receipts-routing-reference routing-reference 4 / 1
Important split:
legacy-rebot-promotional-senderscan be considered for fast close after dry-run review.legacy-rebot-operational-clutterpreserves old Rebot close rules but should remain report-only until Jason explicitly approves operational clutter closing.legacy-rebot-receipts-routing-referenceshould not be closed as spam; it is future routing/reference material.