feat: add 14 new smart folder categories (locale-agnostic)
New EmailCategory enum values: Travel, Subscriptions, Parcels, Recruitment, Events, SecurityAlerts, Healthcare, Education, NewsMedia, PropertyUtilities, Charity, Government, CryptoInvesting, FamilySchool. HeuristicClassifier: locale-agnostic rules using global brand domains + TLD patterns (.gov.*, .edu, .ac.*) + English subject keywords — works for international users without relying on country-specific domains (e.g. gov.uk). Priority order ensures SecurityAlerts and Government take precedence over Finance, and Travel/RideSharing/FoodDelivery fire before Finance to prevent receipt mis-classification. AnalyticsService: maps all 14 new slugs to category counts. client.js: folderToRequest() handles all 14 new slugs. Layout.jsx: SMART_FOLDERS extended with icons and labels. widgets.jsx: CAT_SLUG updated for dashboard drillthrough. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -98,8 +98,23 @@ function folderToRequest(slug, page, pageSize) {
|
|||||||
case 'sales': return { ...base, category: 'SeasonalSales' };
|
case 'sales': return { ...base, category: 'SeasonalSales' };
|
||||||
case 'ridesharing': return { ...base, category: 'RideSharing' };
|
case 'ridesharing': return { ...base, category: 'RideSharing' };
|
||||||
case 'food': return { ...base, category: 'FoodDelivery' };
|
case 'food': return { ...base, category: 'FoodDelivery' };
|
||||||
case 'wellness': return { ...base, category: 'Wellness' };
|
case 'wellness': return { ...base, category: 'Wellness' };
|
||||||
default: return { ...base };
|
// New categories
|
||||||
|
case 'travel': return { ...base, category: 'Travel' };
|
||||||
|
case 'subscriptions': return { ...base, category: 'Subscriptions' };
|
||||||
|
case 'parcels': return { ...base, category: 'Parcels' };
|
||||||
|
case 'recruitment': return { ...base, category: 'Recruitment' };
|
||||||
|
case 'events': return { ...base, category: 'Events' };
|
||||||
|
case 'security': return { ...base, category: 'SecurityAlerts' };
|
||||||
|
case 'healthcare': return { ...base, category: 'Healthcare' };
|
||||||
|
case 'education': return { ...base, category: 'Education' };
|
||||||
|
case 'news': return { ...base, category: 'NewsMedia' };
|
||||||
|
case 'property': return { ...base, category: 'PropertyUtilities' };
|
||||||
|
case 'charity': return { ...base, category: 'Charity' };
|
||||||
|
case 'government': return { ...base, category: 'Government' };
|
||||||
|
case 'crypto': return { ...base, category: 'CryptoInvesting' };
|
||||||
|
case 'family': return { ...base, category: 'FamilySchool' };
|
||||||
|
default: return { ...base };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,16 +32,31 @@ const FAV_SPECIALS = {
|
|||||||
const DEFAULT_FAV_SLUGS = ['inbox', 'unread', 'large', 'old', 'readlater'];
|
const DEFAULT_FAV_SLUGS = ['inbox', 'unread', 'large', 'old', 'readlater'];
|
||||||
|
|
||||||
const SMART_FOLDERS = [
|
const SMART_FOLDERS = [
|
||||||
{ slug: 'automated', icon: '🤖', label: 'Automated', countKey: 'automated' },
|
{ slug: 'automated', icon: '🤖', label: 'Automated', countKey: 'automated' },
|
||||||
{ slug: 'noreply', icon: '🔇', label: 'No-Reply', countKey: 'noreply' },
|
{ slug: 'noreply', icon: '🔇', label: 'No-Reply', countKey: 'noreply' },
|
||||||
{ slug: 'shopping', icon: '🛍️', label: 'Online Shopping', countKey: 'shopping' },
|
{ slug: 'shopping', icon: '🛍️', label: 'Online Shopping', countKey: 'shopping' },
|
||||||
{ slug: 'gaming', icon: '🎮', label: 'Gaming', countKey: 'gaming' },
|
{ slug: 'gaming', icon: '🎮', label: 'Gaming', countKey: 'gaming' },
|
||||||
{ slug: 'finance', icon: '💳', label: 'Finance & Insurance', countKey: 'finance' },
|
{ slug: 'finance', icon: '💳', label: 'Finance & Insurance', countKey: 'finance' },
|
||||||
{ slug: 'sales', icon: '🏷️', label: 'Seasonal Sales', countKey: 'sales' },
|
{ slug: 'sales', icon: '🏷️', label: 'Seasonal Sales', countKey: 'sales' },
|
||||||
{ slug: 'ridesharing', icon: '🚗', label: 'Ride Sharing', countKey: 'ridesharing' },
|
{ slug: 'ridesharing', icon: '🚗', label: 'Ride Sharing', countKey: 'ridesharing' },
|
||||||
{ slug: 'food', icon: '🍕', label: 'Food Delivery', countKey: 'food' },
|
{ slug: 'food', icon: '🍕', label: 'Food Delivery', countKey: 'food' },
|
||||||
{ slug: 'social', icon: '📱', label: 'Social Notifications',countKey: 'social' },
|
{ slug: 'social', icon: '📱', label: 'Social Notifications', countKey: 'social' },
|
||||||
{ slug: 'wellness', icon: '🏃', label: 'Wellness & Sport', countKey: 'wellness' },
|
{ slug: 'wellness', icon: '🏃', label: 'Wellness & Sport', countKey: 'wellness' },
|
||||||
|
// New categories
|
||||||
|
{ slug: 'travel', icon: '✈️', label: 'Travel', countKey: 'travel' },
|
||||||
|
{ slug: 'subscriptions',icon: '🔄', label: 'Subscriptions & SaaS', countKey: 'subscriptions' },
|
||||||
|
{ slug: 'parcels', icon: '📦', label: 'Parcels & Delivery', countKey: 'parcels' },
|
||||||
|
{ slug: 'recruitment', icon: '💼', label: 'Recruitment & Jobs', countKey: 'recruitment' },
|
||||||
|
{ slug: 'events', icon: '🎟️', label: 'Events & Tickets', countKey: 'events' },
|
||||||
|
{ slug: 'security', icon: '🔐', label: 'Security & Alerts', countKey: 'security' },
|
||||||
|
{ slug: 'healthcare', icon: '🏥', label: 'Healthcare', countKey: 'healthcare' },
|
||||||
|
{ slug: 'education', icon: '🎓', label: 'Education', countKey: 'education' },
|
||||||
|
{ slug: 'news', icon: '📰', label: 'News & Media', countKey: 'news' },
|
||||||
|
{ slug: 'property', icon: '🏠', label: 'Property & Utilities', countKey: 'property' },
|
||||||
|
{ slug: 'charity', icon: '❤️', label: 'Charities', countKey: 'charity' },
|
||||||
|
{ slug: 'government', icon: '🏛️', label: 'Government', countKey: 'government' },
|
||||||
|
{ slug: 'crypto', icon: '📈', label: 'Crypto & Investing', countKey: 'crypto' },
|
||||||
|
{ slug: 'family', icon: '👨👩👧', label: 'Family & School', countKey: 'family' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -105,20 +105,34 @@ const DOW = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|||||||
|
|
||||||
// Maps EmailCategory enum name → folder slug or search query
|
// Maps EmailCategory enum name → folder slug or search query
|
||||||
const CAT_SLUG = {
|
const CAT_SLUG = {
|
||||||
Finance: '/app/folder/finance',
|
Finance: '/app/folder/finance',
|
||||||
Social: '/app/folder/social',
|
Social: '/app/folder/social',
|
||||||
Notification: '/app/folder/automated',
|
Notification: '/app/folder/automated',
|
||||||
Promotional: '/app/folder/shopping',
|
Promotional: '/app/folder/shopping',
|
||||||
Shopping: '/app/folder/shopping',
|
Shopping: '/app/folder/shopping',
|
||||||
Gaming: '/app/folder/gaming',
|
Gaming: '/app/folder/gaming',
|
||||||
RideSharing: '/app/folder/ridesharing',
|
RideSharing: '/app/folder/ridesharing',
|
||||||
FoodDelivery: '/app/folder/food',
|
FoodDelivery: '/app/folder/food',
|
||||||
SeasonalSales:'/app/folder/sales',
|
SeasonalSales: '/app/folder/sales',
|
||||||
Wellness: '/app/folder/wellness',
|
Wellness: '/app/folder/wellness',
|
||||||
Spam: '/app/folder/spam',
|
Travel: '/app/folder/travel',
|
||||||
Newsletter: '/app/search?q=newsletter',
|
Subscriptions: '/app/folder/subscriptions',
|
||||||
Personal: '/app/search?q=is:unread',
|
Parcels: '/app/folder/parcels',
|
||||||
Unknown: '/app/folder/allmail',
|
Recruitment: '/app/folder/recruitment',
|
||||||
|
Events: '/app/folder/events',
|
||||||
|
SecurityAlerts: '/app/folder/security',
|
||||||
|
Healthcare: '/app/folder/healthcare',
|
||||||
|
Education: '/app/folder/education',
|
||||||
|
NewsMedia: '/app/folder/news',
|
||||||
|
PropertyUtilities:'/app/folder/property',
|
||||||
|
Charity: '/app/folder/charity',
|
||||||
|
Government: '/app/folder/government',
|
||||||
|
CryptoInvesting: '/app/folder/crypto',
|
||||||
|
FamilySchool: '/app/folder/family',
|
||||||
|
Spam: '/app/folder/spam',
|
||||||
|
Newsletter: '/app/search?q=newsletter',
|
||||||
|
Personal: '/app/search?q=is:unread',
|
||||||
|
Unknown: '/app/folder/allmail',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CategoryHeatmapWidget() {
|
export function CategoryHeatmapWidget() {
|
||||||
|
|||||||
@@ -16,7 +16,21 @@ public enum EmailCategory
|
|||||||
FoodDelivery = 10,
|
FoodDelivery = 10,
|
||||||
SeasonalSales = 11,
|
SeasonalSales = 11,
|
||||||
Wellness = 12,
|
Wellness = 12,
|
||||||
Shopping = 13
|
Shopping = 13,
|
||||||
|
Travel = 14,
|
||||||
|
Subscriptions = 15,
|
||||||
|
Parcels = 16,
|
||||||
|
Recruitment = 17,
|
||||||
|
Events = 18,
|
||||||
|
SecurityAlerts = 19,
|
||||||
|
Healthcare = 20,
|
||||||
|
Education = 21,
|
||||||
|
NewsMedia = 22,
|
||||||
|
PropertyUtilities = 23,
|
||||||
|
Charity = 24,
|
||||||
|
Government = 25,
|
||||||
|
CryptoInvesting = 26,
|
||||||
|
FamilySchool = 27
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>State machine for a sync run.</summary>
|
/// <summary>State machine for a sync run.</summary>
|
||||||
|
|||||||
@@ -167,16 +167,31 @@ public class AnalyticsService : IAnalyticsService
|
|||||||
|
|
||||||
var smartFolders = new Dictionary<string, int>
|
var smartFolders = new Dictionary<string, int>
|
||||||
{
|
{
|
||||||
["automated"] = Cat(EmailCategory.Notification),
|
["automated"] = Cat(EmailCategory.Notification),
|
||||||
["finance"] = Cat(EmailCategory.Finance),
|
["finance"] = Cat(EmailCategory.Finance),
|
||||||
["social"] = Cat(EmailCategory.Social),
|
["social"] = Cat(EmailCategory.Social),
|
||||||
["shopping"] = Cat(EmailCategory.Shopping) + Cat(EmailCategory.Promotional),
|
["shopping"] = Cat(EmailCategory.Shopping) + Cat(EmailCategory.Promotional),
|
||||||
["noreply"] = Cat(EmailCategory.Notification),
|
["noreply"] = Cat(EmailCategory.Notification),
|
||||||
["gaming"] = Cat(EmailCategory.Gaming),
|
["gaming"] = Cat(EmailCategory.Gaming),
|
||||||
["sales"] = Cat(EmailCategory.SeasonalSales),
|
["sales"] = Cat(EmailCategory.SeasonalSales),
|
||||||
["ridesharing"] = Cat(EmailCategory.RideSharing),
|
["ridesharing"] = Cat(EmailCategory.RideSharing),
|
||||||
["food"] = Cat(EmailCategory.FoodDelivery),
|
["food"] = Cat(EmailCategory.FoodDelivery),
|
||||||
["wellness"] = Cat(EmailCategory.Wellness),
|
["wellness"] = Cat(EmailCategory.Wellness),
|
||||||
|
// New categories
|
||||||
|
["travel"] = Cat(EmailCategory.Travel),
|
||||||
|
["subscriptions"] = Cat(EmailCategory.Subscriptions),
|
||||||
|
["parcels"] = Cat(EmailCategory.Parcels),
|
||||||
|
["recruitment"] = Cat(EmailCategory.Recruitment),
|
||||||
|
["events"] = Cat(EmailCategory.Events),
|
||||||
|
["security"] = Cat(EmailCategory.SecurityAlerts),
|
||||||
|
["healthcare"] = Cat(EmailCategory.Healthcare),
|
||||||
|
["education"] = Cat(EmailCategory.Education),
|
||||||
|
["news"] = Cat(EmailCategory.NewsMedia),
|
||||||
|
["property"] = Cat(EmailCategory.PropertyUtilities),
|
||||||
|
["charity"] = Cat(EmailCategory.Charity),
|
||||||
|
["government"] = Cat(EmailCategory.Government),
|
||||||
|
["crypto"] = Cat(EmailCategory.CryptoInvesting),
|
||||||
|
["family"] = Cat(EmailCategory.FamilySchool),
|
||||||
};
|
};
|
||||||
|
|
||||||
return new SidebarCountsDto(inbox, allMail, unread, starred, sent, drafts, trash, spam, large, old, smartFolders);
|
return new SidebarCountsDto(inbox, allMail, unread, starred, sent, drafts, trash, spam, large, old, smartFolders);
|
||||||
|
|||||||
@@ -4,66 +4,514 @@ using InboxIntel.Domain.Enums;
|
|||||||
namespace InboxIntel.Infrastructure.Sync;
|
namespace InboxIntel.Infrastructure.Sync;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fast, dependency-free first-pass classifier applied during sync. The AI
|
/// Fast, dependency-free first-pass classifier applied during sync. Every email
|
||||||
/// layer can later refine these labels, but this guarantees every email has a
|
/// gets a sensible category even when AI is disabled.
|
||||||
/// sensible category even when AI is disabled.
|
|
||||||
///
|
///
|
||||||
/// Priority order matters: more specific categories are checked first so a
|
/// Locale strategy:
|
||||||
/// food-delivery receipt doesn't fall through to Finance or Newsletter.
|
/// - Domain rules use globally-operating brands only (works in any country).
|
||||||
|
/// - TLD patterns (.gov.*, .edu, .ac.*) catch country-specific institutional domains
|
||||||
|
/// without enumerating them (e.g. .gov.uk, .gov.au, .govt.nz, skatteetaten.no won't
|
||||||
|
/// match .gov.* but SecurityAlerts + subject keywords cover it).
|
||||||
|
/// - Subject keywords use English terms, which most international transactional
|
||||||
|
/// systems use even when the UI is localised.
|
||||||
|
///
|
||||||
|
/// Priority order matters — more specific / higher-confidence rules run first.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class HeuristicClassifier
|
public static class HeuristicClassifier
|
||||||
{
|
{
|
||||||
// ── Domain hint lists ────────────────────────────────────────────────────
|
// ── Security & Alerts ────────────────────────────────────────────────────
|
||||||
private static readonly string[] RideSharingDomains = { "uber.com", "lyft.com", "bolt.eu", "freenow.com", "cabify.com", "grab.com", "gojek.com" };
|
// Checked first: a password-reset from a bank must not become Finance.
|
||||||
private static readonly string[] FoodDeliveryDomains = { "doordash.com", "ubereats.com", "justeat.com", "deliveroo.com", "grubhub.com", "seamless.com", "postmates.com", "foodpanda.com", "swiggy.com", "zomato.com" };
|
private static readonly string[] SecuritySubjectHints =
|
||||||
private static readonly string[] GamingDomains = { "steampowered.com", "epicgames.com", "ea.com", "xbox.com", "playstation.com", "nintendo.com", "blizzard.com", "riotgames.com", "ubisoft.com", "gog.com", "humblebundle.com", "bethesda.net" };
|
{
|
||||||
private static readonly string[] ShoppingDomains = { "amazon.com", "amazon.co.uk", "ebay.com", "ebay.co.uk", "etsy.com", "aliexpress.com", "asos.com", "shein.com", "zalando.com", "walmart.com", "target.com", "shopify.com" };
|
"sign-in attempt", "new sign-in", "new login", "sign in from",
|
||||||
private static readonly string[] WellnessDomains = { "strava.com", "garmin.com", "peloton.com", "myfitnesspal.com", "headspace.com", "calm.com", "nike.com", "adidas.com", "whoop.com", "fitbit.com", "zwift.com" };
|
"password reset", "reset your password", "change your password",
|
||||||
private static readonly string[] SocialDomains = { "facebook", "twitter", "linkedin", "instagram", "tiktok", "snapchat", "pinterest", "reddit", "discord", "slack", "teams" };
|
"unusual activity", "suspicious activity", "suspicious sign",
|
||||||
private static readonly string[] FinanceDomains = { "paypal.com", "stripe.com", "wise.com", "revolut.com", "monzo.com", "starling.com", "barclays.co.uk", "hsbc.co.uk", "lloydsbank.com", "natwest.com", "chase.com", "americanexpress.com" };
|
"security alert", "security notice", "account locked",
|
||||||
|
"verify your", "verification code", "two-factor", "2fa",
|
||||||
|
"one-time code", "one-time password", "your otp",
|
||||||
|
"unauthorized access", "we noticed a", "login attempt",
|
||||||
|
};
|
||||||
|
|
||||||
// ── Subject keyword lists ────────────────────────────────────────────────
|
// ── Government / Official ────────────────────────────────────────────────
|
||||||
private static readonly string[] FinanceSubjectHints = { "invoice", "receipt", "payment", "statement", "bank", "transaction", "billing", "refund", "order confirmation", "your order", "subscription renewal" };
|
// TLD-pattern matching covers .gov, .gov.uk, .gov.au, .govt.nz, .gov.ng etc.
|
||||||
private static readonly string[] SeasonalSalesHints = { "sale", "% off", "discount", "deal", "offer", "promo", "coupon", "black friday", "cyber monday", "flash sale", "clearance", "limited time" };
|
// Subject keywords catch country agencies that don't use .gov TLDs.
|
||||||
private static readonly string[] WellnessSubjectHints = { "workout", "run summary", "activity", "steps", "calories", "meditation", "streak", "fitness", "training plan", "race result" };
|
private static readonly string[] GovernmentSubjectHints =
|
||||||
private static readonly string[] NoReplyFromHints = { "noreply", "no-reply", "donotreply", "notifications", "mailer", "automated" };
|
{
|
||||||
|
"tax return", "tax refund", "self assessment", "national insurance",
|
||||||
|
"driving licence", "driving license", "vehicle registration",
|
||||||
|
"passport renewal", "visa application", "benefit payment",
|
||||||
|
"pension", "government gateway", "official notice",
|
||||||
|
"council tax", "property tax", "income tax",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Travel ───────────────────────────────────────────────────────────────
|
||||||
|
private static readonly string[] TravelDomains =
|
||||||
|
{
|
||||||
|
// Accommodation aggregators (global)
|
||||||
|
"booking.com", "airbnb.com", "expedia.com", "hotels.com",
|
||||||
|
"hostelworld.com", "agoda.com", "trivago.com", "vrbo.com",
|
||||||
|
"kayak.com", "skyscanner.com", "skyscanner.net",
|
||||||
|
"tripadvisor.com", "lastminute.com",
|
||||||
|
// Airlines — global + major European / Nordic
|
||||||
|
"ba.com", "britishairways.com", "easyjet.com", "ryanair.com",
|
||||||
|
"wizzair.com", "vueling.com", "iberia.com", "tap.pt",
|
||||||
|
"lufthansa.com", "airfrance.com", "klm.com",
|
||||||
|
"sas.se", "sas.no", "sas.dk", "sas.fi", // Scandinavian
|
||||||
|
"norwegian.com", "flyr.com", "finnair.com",
|
||||||
|
"delta.com", "united.com", "aa.com", "southwest.com", "jetblue.com",
|
||||||
|
"emirates.com", "etihad.com", "qatarairways.com",
|
||||||
|
"singaporeair.com", "cathaypackific.com",
|
||||||
|
"aircanada.com", "qantas.com", "virginatlantic.com",
|
||||||
|
"turkishairlines.com", "aerlingus.com",
|
||||||
|
// Rail
|
||||||
|
"eurostar.com", "trainline.com", "eurail.com", "amtrak.com",
|
||||||
|
"deutschebahn.com", "sbb.ch", "renfe.es",
|
||||||
|
// Car hire (global chains)
|
||||||
|
"hertz.com", "avis.com", "europcar.com", "enterprise.com", "sixt.com", "budget.com",
|
||||||
|
};
|
||||||
|
private static readonly string[] TravelSubjectHints =
|
||||||
|
{
|
||||||
|
"boarding pass", "check-in", "check in online",
|
||||||
|
"itinerary", "flight confirmation", "travel confirmation",
|
||||||
|
"hotel confirmation", "booking confirmation",
|
||||||
|
"your trip", "your reservation", "reservation confirmed",
|
||||||
|
"e-ticket", "travel itinerary", "trip summary",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Parcels & Delivery ────────────────────────────────────────────────────
|
||||||
|
private static readonly string[] ParcelDomains =
|
||||||
|
{
|
||||||
|
// Global carriers
|
||||||
|
"dhl.com", "dhl.de", "fedex.com", "ups.com", "tnt.com",
|
||||||
|
// Regional but widely used
|
||||||
|
"dpd.com", "dpd.co.uk", "dpd.de", "dpd.fr",
|
||||||
|
"gls-group.eu", "gls-group.com",
|
||||||
|
"royalmail.com", "parcelforce.com",
|
||||||
|
"hermes.com", "evri.com", "yodel.co.uk",
|
||||||
|
"posten.no", "postnord.com", "postnord.se", "postnord.dk",
|
||||||
|
"posti.fi", "bpost.be", "laposte.fr", "correos.es",
|
||||||
|
"poste.it", "deutschepost.de", "swiss-post.ch",
|
||||||
|
"usps.com", "canadapost.ca", "auspost.com.au", "nzpost.co.nz",
|
||||||
|
"amazon-logistics.com", "amzl.co.uk",
|
||||||
|
};
|
||||||
|
private static readonly string[] ParcelSubjectHints =
|
||||||
|
{
|
||||||
|
"your shipment", "shipment dispatched", "shipment update",
|
||||||
|
"your parcel", "parcel update", "package delivered",
|
||||||
|
"out for delivery", "delivery attempt", "delivery update",
|
||||||
|
"tracking number", "track your", "order dispatched",
|
||||||
|
"order shipped", "order is on its way", "estimated delivery",
|
||||||
|
"collected by courier", "left with neighbour",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Events & Tickets ─────────────────────────────────────────────────────
|
||||||
|
private static readonly string[] EventsDomains =
|
||||||
|
{
|
||||||
|
"eventbrite.com", "ticketmaster.com", "ticketmaster.co.uk",
|
||||||
|
"seetickets.com", "axs.com", "stubhub.com", "viagogo.com",
|
||||||
|
"skiddle.com", "meetup.com", "dice.fm", "bandsintown.com",
|
||||||
|
"wegottickets.com", "universe.com", "tixr.com",
|
||||||
|
};
|
||||||
|
private static readonly string[] EventsSubjectHints =
|
||||||
|
{
|
||||||
|
"your ticket", "your tickets", "ticket confirmation",
|
||||||
|
"event confirmation", "you're going", "see you there",
|
||||||
|
"ticket order", "e-ticket", "order confirmed",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Recruitment & Jobs ────────────────────────────────────────────────────
|
||||||
|
private static readonly string[] RecruitmentDomains =
|
||||||
|
{
|
||||||
|
"linkedin.com", "indeed.com", "glassdoor.com", "monster.com",
|
||||||
|
"reed.co.uk", "totaljobs.com", "cv-library.co.uk",
|
||||||
|
"workable.com", "greenhouse.io", "lever.co",
|
||||||
|
"smartrecruiters.com", "jobvite.com", "bamboohr.com",
|
||||||
|
"recruitee.com", "teamtailor.com",
|
||||||
|
"finn.no", // Norway
|
||||||
|
"jobindex.dk", // Denmark
|
||||||
|
"mol.fi", // Finland
|
||||||
|
"stepstone.de", // DACH
|
||||||
|
"xing.com", // DACH professional network
|
||||||
|
};
|
||||||
|
private static readonly string[] RecruitmentSubjectHints =
|
||||||
|
{
|
||||||
|
"application received", "your application", "job application",
|
||||||
|
"we received your application", "thank you for applying",
|
||||||
|
"interview invitation", "interview request",
|
||||||
|
"job offer", "offer letter", "we'd like to invite you",
|
||||||
|
"your cv", "your resume", "shortlisted",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Healthcare ────────────────────────────────────────────────────────────
|
||||||
|
private static readonly string[] HealthcareDomains =
|
||||||
|
{
|
||||||
|
// UK
|
||||||
|
"nhs.uk", "nhs.net",
|
||||||
|
// Global / multinational insurers & health platforms
|
||||||
|
"bupa.com", "axa.com", "axa-insurance.co.uk",
|
||||||
|
"cigna.com", "allianzcare.com",
|
||||||
|
"vitality.co.uk", "aviva.com",
|
||||||
|
"zocdoc.com", "doximity.com",
|
||||||
|
"babylon.com", "kry.se", "livi.co.uk",
|
||||||
|
// Norway
|
||||||
|
"helsenorge.no", "helse-bergen.no",
|
||||||
|
};
|
||||||
|
private static readonly string[] HealthcareSubjectHints =
|
||||||
|
{
|
||||||
|
"your appointment", "appointment reminder", "appointment confirmation",
|
||||||
|
"appointment booked", "medical appointment",
|
||||||
|
"prescription", "test results", "lab results",
|
||||||
|
"vaccination appointment", "vaccination reminder",
|
||||||
|
"health check", "clinical", "follow-up appointment",
|
||||||
|
"your referral", "specialist appointment",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Education ─────────────────────────────────────────────────────────────
|
||||||
|
// TLD-based: .edu (US/global), .ac.uk, .ac.nz, .edu.au, .edu.no etc.
|
||||||
|
private static readonly string[] EducationDomains =
|
||||||
|
{
|
||||||
|
"coursera.org", "udemy.com", "duolingo.com", "khanacademy.org",
|
||||||
|
"edx.org", "skillshare.com", "futurelearn.com",
|
||||||
|
"pluralsight.com", "masterclass.com",
|
||||||
|
"codecademy.com", "datacamp.com", "udacity.com",
|
||||||
|
"brilliant.org", "typeform.com", "quizlet.com",
|
||||||
|
};
|
||||||
|
private static readonly string[] EducationSubjectHints =
|
||||||
|
{
|
||||||
|
"assignment due", "assignment submitted", "your grade",
|
||||||
|
"course update", "lecture recording", "exam result",
|
||||||
|
"you earned a certificate", "certificate of completion",
|
||||||
|
"learning reminder", "course completion",
|
||||||
|
"term dates", "semester", "student portal",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── News & Media ──────────────────────────────────────────────────────────
|
||||||
|
private static readonly string[] NewsMediaDomains =
|
||||||
|
{
|
||||||
|
"substack.com", "medium.com",
|
||||||
|
"theguardian.com", "nytimes.com", "bbc.co.uk", "bbc.com",
|
||||||
|
"economist.com", "ft.com", "reuters.com", "apnews.com",
|
||||||
|
"morningbrew.com", "axios.com", "theatlantic.com",
|
||||||
|
"washingtonpost.com", "bloomberg.com",
|
||||||
|
"aftenposten.no", "dn.no", "vg.no", // Norwegian news
|
||||||
|
"politiken.dk", "berlingske.dk", // Danish
|
||||||
|
"hs.fi", "yle.fi", // Finnish
|
||||||
|
"svd.se", "dn.se", // Swedish
|
||||||
|
"spiegel.de", "faz.net", "zeit.de", // German
|
||||||
|
"lemonde.fr", "lefigaro.fr", // French
|
||||||
|
};
|
||||||
|
private static readonly string[] NewsMediaSubjectHints =
|
||||||
|
{
|
||||||
|
"morning briefing", "daily digest", "weekly digest",
|
||||||
|
"breaking news", "top stories", "today's headlines",
|
||||||
|
"newsletter", "this week in", "what you need to know",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Property & Utilities ──────────────────────────────────────────────────
|
||||||
|
private static readonly string[] PropertyDomains =
|
||||||
|
{
|
||||||
|
"rightmove.co.uk", "zoopla.co.uk", "onthemarket.com",
|
||||||
|
"finn.no", // also covers property in Norway
|
||||||
|
"hemnet.se", // Sweden
|
||||||
|
"boliga.dk", // Denmark
|
||||||
|
"zillow.com", "realtor.com", "redfin.com",
|
||||||
|
"airbnb.com", // already Travel, but utility overlap OK
|
||||||
|
};
|
||||||
|
private static readonly string[] PropertySubjectHints =
|
||||||
|
{
|
||||||
|
"your bill", "bill ready", "statement ready",
|
||||||
|
"meter reading", "direct debit", "standing order",
|
||||||
|
"payment due", "payment reminder",
|
||||||
|
"energy bill", "electricity bill", "gas bill", "water bill",
|
||||||
|
"broadband bill", "internet bill", "phone bill",
|
||||||
|
"rent payment", "rent reminder", "tenancy", "landlord",
|
||||||
|
"council tax", "property alert", "new property match",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Charities & Causes ────────────────────────────────────────────────────
|
||||||
|
// .org TLD is a strong signal but too broad on its own; combine with keywords.
|
||||||
|
private static readonly string[] CharitySubjectHints =
|
||||||
|
{
|
||||||
|
"donation receipt", "thank you for donating", "thank you for your gift",
|
||||||
|
"your donation", "monthly giving", "gift aid",
|
||||||
|
"fundraising", "charity appeal", "humanitarian",
|
||||||
|
"make a difference", "your support", "sponsor",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Crypto & Investing ────────────────────────────────────────────────────
|
||||||
|
private static readonly string[] CryptoDomains =
|
||||||
|
{
|
||||||
|
// Crypto exchanges (global)
|
||||||
|
"coinbase.com", "binance.com", "kraken.com", "crypto.com",
|
||||||
|
"gemini.com", "bitstamp.net", "bybit.com", "okx.com",
|
||||||
|
// Retail investing (global + regional)
|
||||||
|
"etoro.com", "robinhood.com",
|
||||||
|
"freetrade.com", "trading212.com", "ig.com",
|
||||||
|
"hargreaves.co.uk", "hl.co.uk",
|
||||||
|
"fidelity.com", "vanguard.com",
|
||||||
|
"nutmeg.com", "moneybox.com", "wealthsimple.com",
|
||||||
|
"nordnet.no", "nordnet.se", "nordnet.dk", "nordnet.fi", // Nordic investing
|
||||||
|
"avanza.se", // Sweden
|
||||||
|
"degiro.eu", // European
|
||||||
|
};
|
||||||
|
private static readonly string[] CryptoSubjectHints =
|
||||||
|
{
|
||||||
|
"trade executed", "order filled", "order confirmed",
|
||||||
|
"crypto", "bitcoin", "ethereum",
|
||||||
|
"portfolio update", "investment statement",
|
||||||
|
"dividend", "isa statement", "pension statement",
|
||||||
|
"fund performance", "capital gains",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Family & School ───────────────────────────────────────────────────────
|
||||||
|
// Intentionally subject-driven — school domains vary wildly by country.
|
||||||
|
private static readonly string[] FamilySchoolSubjectHints =
|
||||||
|
{
|
||||||
|
"parent evening", "parents evening", "open evening",
|
||||||
|
"school trip", "term dates", "half term",
|
||||||
|
"school report", "pupil", "student report",
|
||||||
|
"pta", "school newsletter", "class update",
|
||||||
|
"permission slip", "consent form",
|
||||||
|
"absence notification", "attendance",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Subscriptions & SaaS ─────────────────────────────────────────────────
|
||||||
|
private static readonly string[] SubscriptionDomains =
|
||||||
|
{
|
||||||
|
"spotify.com", "netflix.com", "disneyplus.com", "hulu.com",
|
||||||
|
"hbomax.com", "paramountplus.com", "appletv.com", "icloud.com",
|
||||||
|
"apple.com", "google.com", "microsoft.com",
|
||||||
|
"adobe.com", "dropbox.com", "notion.so",
|
||||||
|
"atlassian.com", "slack.com", "github.com",
|
||||||
|
"canva.com", "figma.com", "zoom.us",
|
||||||
|
"twitch.tv", "patreon.com",
|
||||||
|
"1password.com", "lastpass.com", "dashlane.com",
|
||||||
|
"nordvpn.com", "expressvpn.com", "privateinternetaccess.com",
|
||||||
|
};
|
||||||
|
private static readonly string[] SubscriptionSubjectHints =
|
||||||
|
{
|
||||||
|
"subscription renewed", "subscription renewal",
|
||||||
|
"your subscription", "membership renewed",
|
||||||
|
"payment receipt", "billing statement",
|
||||||
|
"thank you for subscribing", "your plan",
|
||||||
|
"invoice from", "receipt from",
|
||||||
|
"auto-renew", "automatic renewal",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Existing domain lists (retained) ────────────────────────────────────
|
||||||
|
private static readonly string[] RideSharingDomains =
|
||||||
|
{
|
||||||
|
"uber.com", "lyft.com", "bolt.eu", "bolt.com",
|
||||||
|
"freenow.com", "cabify.com", "grab.com", "gojek.com", "ola.com",
|
||||||
|
};
|
||||||
|
private static readonly string[] FoodDeliveryDomains =
|
||||||
|
{
|
||||||
|
"doordash.com", "ubereats.com", "justeat.com", "deliveroo.com",
|
||||||
|
"grubhub.com", "seamless.com", "postmates.com",
|
||||||
|
"foodpanda.com", "swiggy.com", "zomato.com",
|
||||||
|
"wolt.com", // popular in Nordics / Europe
|
||||||
|
"foodora.com", // Germany / Austria / Norway
|
||||||
|
};
|
||||||
|
private static readonly string[] GamingDomains =
|
||||||
|
{
|
||||||
|
"steampowered.com", "store.steampowered.com",
|
||||||
|
"epicgames.com", "ea.com", "xbox.com", "playstation.com",
|
||||||
|
"nintendo.com", "blizzard.com", "riotgames.com",
|
||||||
|
"ubisoft.com", "gog.com", "humblebundle.com",
|
||||||
|
"bethesda.net", "2k.com", "take2games.com",
|
||||||
|
"activision.com", "rockstargames.com",
|
||||||
|
};
|
||||||
|
private static readonly string[] ShoppingDomains =
|
||||||
|
{
|
||||||
|
"amazon.com", "amazon.co.uk", "amazon.de", "amazon.fr",
|
||||||
|
"amazon.es", "amazon.it", "amazon.nl", "amazon.se",
|
||||||
|
"ebay.com", "ebay.co.uk", "ebay.de",
|
||||||
|
"etsy.com", "aliexpress.com", "asos.com",
|
||||||
|
"shein.com", "zalando.com", "zalando.co.uk",
|
||||||
|
"walmart.com", "target.com", "shopify.com",
|
||||||
|
"about.you.de", "boozt.com", // European fashion
|
||||||
|
"elkjop.no", "elkjop.se", // Nordic electronics
|
||||||
|
"coolblue.nl", "coolblue.be", // Benelux electronics
|
||||||
|
};
|
||||||
|
private static readonly string[] WellnessDomains =
|
||||||
|
{
|
||||||
|
"strava.com", "garmin.com", "garmin.eu",
|
||||||
|
"peloton.com", "myfitnesspal.com",
|
||||||
|
"headspace.com", "calm.com",
|
||||||
|
"nike.com", "adidas.com",
|
||||||
|
"whoop.com", "fitbit.com", "zwift.com",
|
||||||
|
"noom.com", "flo.health",
|
||||||
|
};
|
||||||
|
private static readonly string[] FinanceDomains =
|
||||||
|
{
|
||||||
|
"paypal.com", "stripe.com", "wise.com", "transferwise.com",
|
||||||
|
"revolut.com", "monzo.com", "starling.co.uk", "starlingbank.com",
|
||||||
|
"n26.com", "bunq.com", // European digital banks
|
||||||
|
"barclays.co.uk", "hsbc.co.uk",
|
||||||
|
"lloydsbank.com", "natwest.com", "santander.co.uk",
|
||||||
|
"chase.com", "bankofamerica.com", "wellsfargo.com",
|
||||||
|
"americanexpress.com", "amex.co.uk",
|
||||||
|
"dnb.no", "sparebank1.no", // Norwegian banks
|
||||||
|
"seb.se", "handelsbanken.se", // Swedish
|
||||||
|
"nordea.com", // Nordic
|
||||||
|
"klarna.com", // BNPL (global)
|
||||||
|
"afterpay.com", "clearpay.co.uk",
|
||||||
|
};
|
||||||
|
private static readonly string[] SocialDomains =
|
||||||
|
{
|
||||||
|
"facebook", "twitter", "x.com", "linkedin",
|
||||||
|
"instagram", "tiktok", "snapchat", "pinterest",
|
||||||
|
"reddit", "discord", "slack", "teams",
|
||||||
|
"youtube.com", "mastodon",
|
||||||
|
};
|
||||||
|
private static readonly string[] SeasonalSalesHints =
|
||||||
|
{
|
||||||
|
" sale", "% off", "% discount", "save up to",
|
||||||
|
"flash sale", "clearance", "black friday",
|
||||||
|
"cyber monday", "prime day", "singles day",
|
||||||
|
"limited time offer", "today only", "exclusive deal",
|
||||||
|
"early access sale", "end of season",
|
||||||
|
};
|
||||||
|
private static readonly string[] WellnessSubjectHints =
|
||||||
|
{
|
||||||
|
"workout summary", "run summary", "activity summary",
|
||||||
|
"your weekly stats", "you hit your goal",
|
||||||
|
"calories burned", "steps today", "meditation streak",
|
||||||
|
"training plan", "race result", "new personal best",
|
||||||
|
};
|
||||||
|
private static readonly string[] NoReplyFromHints =
|
||||||
|
{
|
||||||
|
"noreply", "no-reply", "donotreply", "do-not-reply",
|
||||||
|
"notifications@", "mailer@", "automated@",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>True if the sender domain matches a .gov or .gov.XX TLD pattern.</summary>
|
||||||
|
private static bool IsGovDomain(string from)
|
||||||
|
{
|
||||||
|
// Covers: .gov .gov.uk .gov.au .govt.nz .gov.za .gov.ng etc.
|
||||||
|
return from.EndsWith(".gov") ||
|
||||||
|
from.Contains(".gov.") ||
|
||||||
|
from.Contains(".govt.") ||
|
||||||
|
from.Contains("government.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>True if sender domain is a recognised educational TLD.</summary>
|
||||||
|
private static bool IsEduDomain(string from)
|
||||||
|
{
|
||||||
|
// .edu .edu.au .edu.sg .ac.uk .ac.nz .ac.jp .edu.no etc.
|
||||||
|
return from.EndsWith(".edu") ||
|
||||||
|
from.Contains(".edu.") ||
|
||||||
|
from.Contains(".ac.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>True if sender domain is a .org (strong charity/NGO signal).</summary>
|
||||||
|
private static bool IsOrgDomain(string from) => from.EndsWith(".org") || from.Contains(".org.");
|
||||||
|
|
||||||
public static EmailCategory Classify(GmailMessageDetail d)
|
public static EmailCategory Classify(GmailMessageDetail d)
|
||||||
{
|
{
|
||||||
var subject = (d.Subject ?? string.Empty).ToLowerInvariant();
|
var subject = (d.Subject ?? string.Empty).ToLowerInvariant();
|
||||||
var from = d.FromAddress.ToLowerInvariant();
|
var from = d.FromAddress.ToLowerInvariant();
|
||||||
|
|
||||||
// Ride-sharing — check before finance so a trip receipt doesn't become Finance
|
// 1. Security alerts — highest priority, overrides everything
|
||||||
if (RideSharingDomains.Any(h => from.Contains(h))) return EmailCategory.RideSharing;
|
if (SecuritySubjectHints.Any(h => subject.Contains(h)))
|
||||||
|
return EmailCategory.SecurityAlerts;
|
||||||
|
|
||||||
// Food delivery — same reason
|
// 2. Government — TLD pattern + subject keywords
|
||||||
if (FoodDeliveryDomains.Any(h => from.Contains(h))) return EmailCategory.FoodDelivery;
|
if (IsGovDomain(from) || GovernmentSubjectHints.Any(h => subject.Contains(h)))
|
||||||
|
return EmailCategory.Government;
|
||||||
|
|
||||||
// Gaming
|
// 3. Travel (before Finance — a flight receipt is Travel, not Finance)
|
||||||
if (GamingDomains.Any(h => from.Contains(h))) return EmailCategory.Gaming;
|
if (TravelDomains.Any(h => from.Contains(h)) || TravelSubjectHints.Any(h => subject.Contains(h)))
|
||||||
|
return EmailCategory.Travel;
|
||||||
|
|
||||||
// Wellness
|
// 4. Ride-sharing (before Finance — receipt should stay RideSharing)
|
||||||
|
if (RideSharingDomains.Any(h => from.Contains(h)))
|
||||||
|
return EmailCategory.RideSharing;
|
||||||
|
|
||||||
|
// 5. Food delivery
|
||||||
|
if (FoodDeliveryDomains.Any(h => from.Contains(h)))
|
||||||
|
return EmailCategory.FoodDelivery;
|
||||||
|
|
||||||
|
// 6. Parcels (before Shopping — dispatch email is Parcels, not Shopping)
|
||||||
|
if (ParcelDomains.Any(h => from.Contains(h)) || ParcelSubjectHints.Any(h => subject.Contains(h)))
|
||||||
|
return EmailCategory.Parcels;
|
||||||
|
|
||||||
|
// 7. Events & Tickets
|
||||||
|
if (EventsDomains.Any(h => from.Contains(h)) || EventsSubjectHints.Any(h => subject.Contains(h)))
|
||||||
|
return EmailCategory.Events;
|
||||||
|
|
||||||
|
// 8. Healthcare
|
||||||
|
if (HealthcareDomains.Any(h => from.Contains(h)) || HealthcareSubjectHints.Any(h => subject.Contains(h)))
|
||||||
|
return EmailCategory.Healthcare;
|
||||||
|
|
||||||
|
// 9. Crypto & Investing (before Finance — a Coinbase email is Crypto)
|
||||||
|
if (CryptoDomains.Any(h => from.Contains(h)) || CryptoSubjectHints.Any(h => subject.Contains(h)))
|
||||||
|
return EmailCategory.CryptoInvesting;
|
||||||
|
|
||||||
|
// 10. Finance (domain + subject keywords)
|
||||||
|
if (FinanceDomains.Any(h => from.Contains(h)))
|
||||||
|
return EmailCategory.Finance;
|
||||||
|
if (new[] { "invoice", "receipt", "payment", "statement", "bank", "transaction", "billing", "refund" }
|
||||||
|
.Any(h => subject.Contains(h)))
|
||||||
|
return EmailCategory.Finance;
|
||||||
|
|
||||||
|
// 11. Gaming
|
||||||
|
if (GamingDomains.Any(h => from.Contains(h)))
|
||||||
|
return EmailCategory.Gaming;
|
||||||
|
|
||||||
|
// 12. Shopping (domain match)
|
||||||
|
if (ShoppingDomains.Any(h => from.Contains(h)))
|
||||||
|
return EmailCategory.Shopping;
|
||||||
|
|
||||||
|
// 13. Seasonal sales (subject-driven — catches cross-retailer promos)
|
||||||
|
if (SeasonalSalesHints.Any(h => subject.Contains(h)))
|
||||||
|
return EmailCategory.SeasonalSales;
|
||||||
|
|
||||||
|
// 14. Wellness
|
||||||
if (WellnessDomains.Any(h => from.Contains(h)) || WellnessSubjectHints.Any(h => subject.Contains(h)))
|
if (WellnessDomains.Any(h => from.Contains(h)) || WellnessSubjectHints.Any(h => subject.Contains(h)))
|
||||||
return EmailCategory.Wellness;
|
return EmailCategory.Wellness;
|
||||||
|
|
||||||
// Shopping / e-commerce domains
|
// 15. Recruitment
|
||||||
if (ShoppingDomains.Any(h => from.Contains(h))) return EmailCategory.Shopping;
|
if (RecruitmentDomains.Any(h => from.Contains(h)) || RecruitmentSubjectHints.Any(h => subject.Contains(h)))
|
||||||
|
return EmailCategory.Recruitment;
|
||||||
|
|
||||||
// Seasonal sales (subject-driven — catches cross-retailer promotions)
|
// 16. Education (TLD pattern + known platforms)
|
||||||
if (SeasonalSalesHints.Any(h => subject.Contains(h))) return EmailCategory.SeasonalSales;
|
if (IsEduDomain(from) || EducationDomains.Any(h => from.Contains(h)) || EducationSubjectHints.Any(h => subject.Contains(h)))
|
||||||
|
return EmailCategory.Education;
|
||||||
|
|
||||||
// Finance — domain first, then subject keywords
|
// 17. Family & School (subject-only — school domains are country-specific)
|
||||||
if (FinanceDomains.Any(h => from.Contains(h)) || FinanceSubjectHints.Any(h => subject.Contains(h)))
|
if (FamilySchoolSubjectHints.Any(h => subject.Contains(h)))
|
||||||
return EmailCategory.Finance;
|
return EmailCategory.FamilySchool;
|
||||||
|
|
||||||
// Social networks
|
// 18. Property & Utilities
|
||||||
if (SocialDomains.Any(h => from.Contains(h))) return EmailCategory.Social;
|
if (PropertyDomains.Any(h => from.Contains(h)) || PropertySubjectHints.Any(h => subject.Contains(h)))
|
||||||
|
return EmailCategory.PropertyUtilities;
|
||||||
|
|
||||||
// Newsletters (has unsubscribe header but didn't match any specific domain above)
|
// 19. Charities — .org TLD + subject keywords (either is sufficient)
|
||||||
if (d.HasListUnsubscribe) return EmailCategory.Newsletter;
|
if (CharitySubjectHints.Any(h => subject.Contains(h)) || IsOrgDomain(from))
|
||||||
|
return EmailCategory.Charity;
|
||||||
|
|
||||||
// Automated / notification senders
|
// 20. Subscriptions & SaaS
|
||||||
if (NoReplyFromHints.Any(h => from.Contains(h))) return EmailCategory.Notification;
|
if (SubscriptionDomains.Any(h => from.Contains(h)) || SubscriptionSubjectHints.Any(h => subject.Contains(h)))
|
||||||
|
return EmailCategory.Subscriptions;
|
||||||
|
|
||||||
|
// 21. News & Media
|
||||||
|
if (NewsMediaDomains.Any(h => from.Contains(h)) || NewsMediaSubjectHints.Any(h => subject.Contains(h)))
|
||||||
|
return EmailCategory.NewsMedia;
|
||||||
|
|
||||||
|
// 22. Social networks
|
||||||
|
if (SocialDomains.Any(h => from.Contains(h)))
|
||||||
|
return EmailCategory.Social;
|
||||||
|
|
||||||
|
// 23. Newsletter (generic — has unsubscribe header, didn't match anything above)
|
||||||
|
if (d.HasListUnsubscribe)
|
||||||
|
return EmailCategory.Newsletter;
|
||||||
|
|
||||||
|
// 24. Automated / no-reply senders
|
||||||
|
if (NoReplyFromHints.Any(h => from.Contains(h)))
|
||||||
|
return EmailCategory.Notification;
|
||||||
|
|
||||||
return EmailCategory.Personal;
|
return EmailCategory.Personal;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user