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;
}
}