local cases = { { name = 'shipping', from = 'Amazon ', subject = 'Dispatched: your order', folder = 'notifications' }, { name = 'bare mixed-case sender', from = 'SHIPMENT-TRACKING@AMAZON.CO.UK', subject = 'Out for delivery', folder = 'notifications' }, { name = 'receipt beats mailing list', from = 'Apple ', subject = 'Your receipt from Apple.', ['list-unsubscribe'] = '', folder = 'notifications' }, { name = 'Stripe receipt', from = 'Example ', subject = 'Your Example receipt [#1234]', folder = 'notifications' }, { name = 'hosting payment', from = 'Host ', subject = 'Payment Confirmation : Invoice #123', folder = 'notifications' }, { name = 'support conversation', from = 'Host ', subject = '[Ticket ID: 123] Service Unavailable?' }, { name = 'bank statement', from = 'Monzo ', subject = 'Your September statement', folder = 'notifications' }, { name = 'bank account change', from = 'Monzo ', subject = 'Your account details changed' }, { name = 'Bandcamp release', from = 'Bandcamp ', subject = 'New release from an artist', folder = 'automated mail' }, { name = 'Bandcamp purchase', from = 'Bandcamp ', subject = 'Thank you!', folder = 'notifications' }, { name = 'Ableton beta', from = 'Live 12 ', subject = 'We have just released Live 12.5b1', folder = 'automated mail' }, { name = 'policy update', from = 'service@example.invalid', subject = 'Changes to our User Agreement', folder = 'automated mail' }, { name = 'mailing list', from = 'newsletter@example.invalid', subject = 'Weekly roundup', ['list-id'] = 'weekly.example.invalid', folder = 'automated mail' }, { name = 'bulk precedence', from = 'newsletter@example.invalid', subject = 'Monthly roundup', precedence = 'bulk', folder = 'automated mail' }, { name = 'non-bulk precedence', from = 'person@example.invalid', subject = 'Hello', precedence = 'normal' }, { name = 'login notification', from = 'service@example.invalid', subject = 'You signed in from a new device', folder = 'notifications' }, { name = 'password reset', from = 'Steam ', subject = 'Reset your password', ['list-unsubscribe'] = '' }, { name = 'verification code', from = 'Wise ', subject = 'Your verification code' }, { name = 'magic link', from = 'service@example.invalid', subject = 'Your secure link is here', ['list-id'] = 'service.example.invalid' }, { name = 'failed payment', from = 'Wise ', subject = 'Payment failed' }, { name = 'Stripe payment failure', from = 'Example ', subject = 'Payment failed for your invoice' }, { name = 'domain expiration', from = 'Cloudflare ', subject = 'Domain Expiration Notification', ['list-unsubscribe'] = '' }, { name = 'security warning', from = 'Steam ', subject = 'Suspicious activity: new device' }, { name = 'breach newsletter', from = 'service@example.invalid', subject = 'Notice of data breach', ['list-id'] = 'service.example.invalid' }, { name = 'flagged receipt', from = 'Apple ', subject = 'Your receipt from Apple.', flagged = true }, { name = 'confirmed phishing', from = 'Mail Delivery System ', subject = 'Verify your email or be blocked', ['list-unsubscribe'] = '', folder = 'Junk' }, { name = 'flagged spam', from = 'vada@servicedive.com', subject = 'Verify your email', flagged = true }, { name = 'sender suffix lookalike', from = 'orders@asos.com.attacker.invalid', subject = 'Your order' }, { name = 'sender prefix lookalike', from = 'not-orders@asos.com', subject = 'Your order' }, { name = 'unreviewed invoice', from = 'accounts@example.invalid', subject = 'Your invoice is available' }, { name = 'personal conversation', from = 'friend@example.invalid', subject = 'Want to catch up?' }, { name = 'rotating spam mailbox', from = 'New Person ', subject = 'Hello', folder = 'Junk' }, { name = 'spam subdomain', from = 'Campaign ', subject = 'Hello', folder = 'Junk' }, { name = 'bare mixed-case spam domain', from = 'RANDOM@DEVTHUSIASTAPP.COM', subject = 'Hello', folder = 'Junk' }, { name = 'other dedicated spam domain', from = 'new-person@sendnovada.com', subject = 'Hello', folder = 'Junk' }, { name = 'flagged spam domain', from = 'random@devthusiastapp.com', subject = 'Hello', flagged = true }, { name = 'domain suffix lookalike', from = 'person@devthusiastapp.com.example.invalid', subject = 'Hello' }, { name = 'domain prefix lookalike', from = 'person@notdevthusiastapp.com', subject = 'Hello' }, { name = 'domain in display name', from = '"random@devthusiastapp.com" ', subject = 'Hello' }, { name = 'domain in local part', from = 'devthusiastapp.com@example.invalid', subject = 'Hello' }, { name = 'exact sender in display name', from = '"vada@servicedive.com" ', subject = 'Hello' }, { name = 'unrelated Gmail sender', from = 'friend@gmail.com', subject = 'Hello' }, { name = 'unrelated Tiscali sender', from = 'friend@tiscali.it', subject = 'Hello' }, { name = 'unreviewed mailbox on possibly compromised domain', from = 'person@club-de-race.com', subject = 'Hello' }, { name = 'Devthusiast rotating domain', from = 'random@new-campaign.invalid', subject = 'Re: Gifted Invite Enclosed (Github Community Partnership)', body = 'Your lifetime membership to Devthusiast awaits.', folder = 'Junk' }, { name = 'campaign subject without brand', from = 'person@example.invalid', subject = 'Gifted Invite Enclosed (Github Community Partnership)', body = 'A different message.' }, { name = 'brand mention without campaign subject', from = 'friend@example.invalid', subject = 'Spam filters', body = 'Have you heard of devthusiast?' }, { name = 'generic partnership', from = 'person@example.invalid', subject = 'GitHub partnership', body = 'Would you like to collaborate?' }, { name = 'flagged campaign', from = 'random@new-campaign.invalid', subject = 'Gifted Invite Enclosed (Github Community Partnership)', body = 'Welcome to devthusiast.', flagged = true }, { name = 'decoded MIME subject', from = 'Windows ', subject = 'Action required: verify your account', raw_subject = '=?UTF-8?B?QWN0aW9uIHJlcXVpcmVkOiB2ZXJpZnkgeW91ciBhY2NvdW50?=' }, } for _, dry_run in ipairs({ false, true }) do local moved, preview = {}, {} local inbox = setmetatable({}, { __index = Mailbox }) function inbox:send_query(query, messages) local result = Set {} local field, value = query:match('^HEADER ([^ ]+) "(.*)"$') if not field then field, value = query:match('^(%u+) "(.*)"$') end for _, message in ipairs(messages or self:_send_query()) do local case = cases[message[2]] local matches = query == 'FLAGGED' and case.flagged if field then local header = case[field:lower()] matches = header and header:lower():find(value:lower(), 1, true) end if matches then result[#result + 1] = message end end return result end function inbox:_send_query() local result = Set {} for id in ipairs(cases) do result[#result + 1] = { self, id } end return result end function inbox:_fetch_fields(fields, messages) local result = {} for _, id in ipairs(messages) do local field = fields[1]:lower() local value = cases[id][field] if field == 'subject' then value = cases[id].raw_subject or value end if value then result[id] = fields[1] .. ': ' .. value .. '\r\n' end end return result end function inbox:move_messages(destination, messages) assert(not dry_run, 'Dry run attempted to move mail') for _, message in ipairs(messages) do local id = message[2] assert(not moved[id], 'Message filed twice: ' .. cases[id].name) moved[id] = destination.folder end return true end local account = setmetatable({ INBOX = inbox }, { __index = function(_, folder) return { folder = folder } end, }) local env = setmetatable({ IMAP = function() return account end, dofile = function() return { { username = 'test', password_env = 'TEST_PASSWORD' } } end, os = { getenv = function(name) if name == 'IMAPFILTER_DRY_RUN' then return dry_run and '1' or nil end if name == 'HOME' then return '/nonexistent' end if name == 'TEST_PASSWORD' then return 'not-a-real-password' end end }, print = function(line) local folder, id = line:match('^MATCH\ttest\t([^\t]+)\t(%d+)$') if id then id = tonumber(id) assert(not preview[id], 'Message previewed twice: ' .. cases[id].name) preview[id] = folder end end, }, { __index = _G }) assert(loadfile('config.lua', 't', env))() for id, case in ipairs(cases) do local actual = (dry_run and preview or moved)[id] assert(actual == case.folder, case.name .. ': expected ' .. (case.folder or 'INBOX') .. ', got ' .. (actual or 'INBOX')) end end print(#cases .. ' classification cases passed in live and dry-run modes (no network)')