diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js index cebe4cc..f7a2b95 100644 --- a/frontend/src/api/client.js +++ b/frontend/src/api/client.js @@ -92,8 +92,13 @@ function folderToRequest(slug, page, pageSize) { case 'automated': return { ...base, category: 'Notification' }; case 'finance': return { ...base, category: 'Finance' }; case 'social': return { ...base, category: 'Social' }; - case 'shopping': return { ...base, category: 'Promotional' }; + case 'shopping': return { ...base, category: 'Shopping' }; case 'noreply': return { ...base, query: 'from:noreply' }; + case 'gaming': return { ...base, category: 'Gaming' }; + case 'sales': return { ...base, category: 'SeasonalSales' }; + case 'ridesharing': return { ...base, category: 'RideSharing' }; + case 'food': return { ...base, category: 'FoodDelivery' }; + case 'wellness': return { ...base, category: 'Wellness' }; default: return { ...base }; } } diff --git a/frontend/src/components/widgets.jsx b/frontend/src/components/widgets.jsx index 7b79e88..69f57d2 100644 --- a/frontend/src/components/widgets.jsx +++ b/frontend/src/components/widgets.jsx @@ -109,6 +109,12 @@ const CAT_SLUG = { Social: '/app/folder/social', Notification: '/app/folder/automated', Promotional: '/app/folder/shopping', + Shopping: '/app/folder/shopping', + Gaming: '/app/folder/gaming', + RideSharing: '/app/folder/ridesharing', + FoodDelivery: '/app/folder/food', + SeasonalSales:'/app/folder/sales', + Wellness: '/app/folder/wellness', Spam: '/app/folder/spam', Newsletter: '/app/search?q=newsletter', Personal: '/app/search?q=is:unread', diff --git a/src/InboxIntel.Domain/Enums/Enums.cs b/src/InboxIntel.Domain/Enums/Enums.cs index 1b616d7..4776b95 100644 --- a/src/InboxIntel.Domain/Enums/Enums.cs +++ b/src/InboxIntel.Domain/Enums/Enums.cs @@ -10,7 +10,13 @@ public enum EmailCategory Spam = 4, Promotional = 5, Social = 6, - Notification = 7 + Notification = 7, + Gaming = 8, + RideSharing = 9, + FoodDelivery = 10, + SeasonalSales = 11, + Wellness = 12, + Shopping = 13 } /// State machine for a sync run. diff --git a/src/InboxIntel.Infrastructure/Analytics/AnalyticsService.cs b/src/InboxIntel.Infrastructure/Analytics/AnalyticsService.cs index 017e6cb..6242a01 100644 --- a/src/InboxIntel.Infrastructure/Analytics/AnalyticsService.cs +++ b/src/InboxIntel.Infrastructure/Analytics/AnalyticsService.cs @@ -167,16 +167,16 @@ public class AnalyticsService : IAnalyticsService var smartFolders = new Dictionary { - ["automated"] = Cat(EmailCategory.Notification), - ["finance"] = Cat(EmailCategory.Finance), - ["social"] = Cat(EmailCategory.Social), - ["shopping"] = Cat(EmailCategory.Promotional), - ["noreply"] = 0, - ["gaming"] = 0, - ["sales"] = 0, - ["ridesharing"] = 0, - ["food"] = 0, - ["wellness"] = 0, + ["automated"] = Cat(EmailCategory.Notification), + ["finance"] = Cat(EmailCategory.Finance), + ["social"] = Cat(EmailCategory.Social), + ["shopping"] = Cat(EmailCategory.Shopping) + Cat(EmailCategory.Promotional), + ["noreply"] = Cat(EmailCategory.Notification), + ["gaming"] = Cat(EmailCategory.Gaming), + ["sales"] = Cat(EmailCategory.SeasonalSales), + ["ridesharing"] = Cat(EmailCategory.RideSharing), + ["food"] = Cat(EmailCategory.FoodDelivery), + ["wellness"] = Cat(EmailCategory.Wellness), }; return new SidebarCountsDto(inbox, allMail, unread, starred, sent, drafts, trash, spam, large, old, smartFolders); diff --git a/src/InboxIntel.Infrastructure/Sync/HeuristicClassifier.cs b/src/InboxIntel.Infrastructure/Sync/HeuristicClassifier.cs index 7e98032..b21ff2b 100644 --- a/src/InboxIntel.Infrastructure/Sync/HeuristicClassifier.cs +++ b/src/InboxIntel.Infrastructure/Sync/HeuristicClassifier.cs @@ -7,22 +7,64 @@ namespace InboxIntel.Infrastructure.Sync; /// Fast, dependency-free first-pass classifier applied during sync. The AI /// layer can later refine these labels, but this guarantees every email has a /// sensible category even when AI is disabled. +/// +/// Priority order matters: more specific categories are checked first so a +/// food-delivery receipt doesn't fall through to Finance or Newsletter. /// public static class HeuristicClassifier { - private static readonly string[] FinanceHints = { "invoice", "receipt", "payment", "statement", "bank", "transaction", "billing" }; - private static readonly string[] SocialHints = { "facebook", "twitter", "linkedin", "instagram", "tiktok" }; - private static readonly string[] NoReplyHints = { "noreply", "no-reply", "donotreply", "newsletter", "mailer", "notifications" }; + // ── Domain hint lists ──────────────────────────────────────────────────── + private static readonly string[] RideSharingDomains = { "uber.com", "lyft.com", "bolt.eu", "freenow.com", "cabify.com", "grab.com", "gojek.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" }; + 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" }; + 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" }; + private static readonly string[] SocialDomains = { "facebook", "twitter", "linkedin", "instagram", "tiktok", "snapchat", "pinterest", "reddit", "discord", "slack", "teams" }; + 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" }; + + // ── Subject keyword lists ──────────────────────────────────────────────── + private static readonly string[] FinanceSubjectHints = { "invoice", "receipt", "payment", "statement", "bank", "transaction", "billing", "refund", "order confirmation", "your order", "subscription renewal" }; + private static readonly string[] SeasonalSalesHints = { "sale", "% off", "discount", "deal", "offer", "promo", "coupon", "black friday", "cyber monday", "flash sale", "clearance", "limited time" }; + private static readonly string[] WellnessSubjectHints = { "workout", "run summary", "activity", "steps", "calories", "meditation", "streak", "fitness", "training plan", "race result" }; + private static readonly string[] NoReplyFromHints = { "noreply", "no-reply", "donotreply", "notifications", "mailer", "automated" }; public static EmailCategory Classify(GmailMessageDetail d) { 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 + if (RideSharingDomains.Any(h => from.Contains(h))) return EmailCategory.RideSharing; + + // Food delivery — same reason + if (FoodDeliveryDomains.Any(h => from.Contains(h))) return EmailCategory.FoodDelivery; + + // Gaming + if (GamingDomains.Any(h => from.Contains(h))) return EmailCategory.Gaming; + + // Wellness + if (WellnessDomains.Any(h => from.Contains(h)) || WellnessSubjectHints.Any(h => subject.Contains(h))) + return EmailCategory.Wellness; + + // Shopping / e-commerce domains + if (ShoppingDomains.Any(h => from.Contains(h))) return EmailCategory.Shopping; + + // Seasonal sales (subject-driven — catches cross-retailer promotions) + if (SeasonalSalesHints.Any(h => subject.Contains(h))) return EmailCategory.SeasonalSales; + + // Finance — domain first, then subject keywords + if (FinanceDomains.Any(h => from.Contains(h)) || FinanceSubjectHints.Any(h => subject.Contains(h))) + return EmailCategory.Finance; + + // Social networks + if (SocialDomains.Any(h => from.Contains(h))) return EmailCategory.Social; + + // Newsletters (has unsubscribe header but didn't match any specific domain above) if (d.HasListUnsubscribe) return EmailCategory.Newsletter; - if (FinanceHints.Any(h => subject.Contains(h))) return EmailCategory.Finance; - if (SocialHints.Any(h => from.Contains(h))) return EmailCategory.Social; - if (NoReplyHints.Any(h => from.Contains(h))) return EmailCategory.Notification; + + // Automated / notification senders + if (NoReplyFromHints.Any(h => from.Contains(h))) return EmailCategory.Notification; + return EmailCategory.Personal; } }