From 388c683959d43a26c2e36876a256b9044da9ea63 Mon Sep 17 00:00:00 2001 From: SMTP Relay & Email - Free Send <60409540+msgwing@users.noreply.github.com> Date: Mon, 17 Aug 2026 22:57:08 +0200 Subject: [PATCH] fix(outreach): stop queueing dead threads, our own replies, and abuse tools The queue of eight leads reviewed on 2026-08-17 contained no actionable work at all. Five threads were closed and resolved, the oldest since August 2024. Two were open but untouched since 2022. The eighth was genuinely live and a genuine fit - and the owner had already answered it six days earlier. Two of those eight were worse than useless: an email bomber and a phishing-simulation framework. Recommending a shared-domain relay as either one's sending path puts @msgwing.com behind their traffic, and every other user of that domain pays for it. Three gates the bot never had: * `is:open` on every GitHub query. Two of the three carried only `is:issue`. * A 120-day recency limit on GitHub results. Stack Exchange stays exempt - an answer there earns from search for years, so an old unanswered question is still worth answering. * `-commenter:`, so the bot stops re-queueing threads we have already replied to. That is the difference between a queue of work and a queue of things already done. Plus abuse tooling on the skip list: bomber, phishing, spammer, mail flood, brute force. The whole queue was closed with a reason each, so none of it returns. --- .github/workflows/outreach-watch.yml | 30 +++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/.github/workflows/outreach-watch.yml b/.github/workflows/outreach-watch.yml index f878217..0e2e76f 100644 --- a/.github/workflows/outreach-watch.yml +++ b/.github/workflows/outreach-watch.yml @@ -67,12 +67,23 @@ jobs: // provider feature request and an Angular boilerplate, neither of // which is anybody looking for a relay. Bare "smtp" plus "disabled" // is not a signal. What worked was the error code. + // `is:open` on every one of them. Two of these carried only + // `is:issue`, and the queue of 2026-08-17 was the result: five of + // the eight threads it had queued were closed and resolved, the + // oldest since August 2024. A comment on a finished issue reaches + // nobody and is exactly what gets an account flagged. const GH_QUERIES = [ - '5.7.139 in:title is:issue', - '"basic authentication is disabled" in:title is:issue', + '5.7.139 in:title is:issue is:open', + '"basic authentication is disabled" in:title is:issue is:open', 'smtp free relay in:title is:issue is:open', ]; + // A GitHub thread nobody has touched in four months is dead, even + // when it is open. Two of the eight were untouched since 2022. + // Stack Exchange is deliberately exempt: an answer there keeps + // earning from search for years. + const GH_MAX_IDLE_DAYS = 120; + // A candidate has to name a MAIL problem AND an AUTHENTICATION // problem. The first version was one flat list where any single // hit passed, and `basic authentication` on its own is not a mail @@ -115,6 +126,12 @@ jobs: 'bulk', 'newsletter', 'marketing', 'campaign', 'mailing list', 'spam filter', 'receive mail', 'imap', 'pop3', 'dovecot', 'mailbox migration', 'exchange 2013', 'exchange 2016 install', + // Abuse tooling. The queue of 2026-08-17 contained an email + // bomber and a phishing-simulation framework. Handing either a + // sending path puts the shared @msgwing.com domain behind their + // traffic, and every other user of that domain pays for it. + 'bomber', 'phishing', 'spammer', 'mass mail', 'mail flood', + 'email flood', 'brute force', ]; const since = Math.floor(Date.now() / 1000) - SE_LOOKBACK_DAYS * 86400; @@ -182,11 +199,18 @@ jobs: for (const q of GH_QUERIES) { attempted++; try { + // `-commenter:` excludes threads we have already replied to. + // Without it the bot re-queues its own finished work: the one + // genuinely live lead of 2026-08-17 had already been answered + // by the owner six days earlier, and was queued anyway. const res = await github.rest.search.issuesAndPullRequests({ - q: `${q} -repo:${context.repo.owner}/${context.repo.repo}`, + q: `${q} -repo:${context.repo.owner}/${context.repo.repo}` + + ` -commenter:${context.repo.owner}`, per_page: 10, }); + const ghIdle = Date.now() - GH_MAX_IDLE_DAYS * 86400_000; for (const item of res.data.items) { + if (new Date(item.updated_at).getTime() < ghIdle) continue; candidates.push({ id: `gh:${item.id}`, source: 'github',