From f947ee6f22371055801fa9cfc62e15df7851986c Mon Sep 17 00:00:00 2001 From: Dries Augustyns Date: Wed, 18 Feb 2026 16:24:55 +0100 Subject: [PATCH] feat: Add dedicated received type --- apps/api/src/controllers/Webhooks.ts | 2 +- apps/api/src/services/ActivityService.ts | 19 +++++++++++++++++++ apps/api/src/services/EmailService.ts | 4 +++- apps/web/src/components/ActivityItem.tsx | 15 +++++++++++++++ apps/web/src/pages/activity/index.tsx | 4 +++- .../migration.sql | 2 ++ packages/db/prisma/schema.prisma | 1 + packages/types/src/api/activity.ts | 1 + 8 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 packages/db/prisma/migrations/20260218151425_add_received_email_status/migration.sql diff --git a/apps/api/src/controllers/Webhooks.ts b/apps/api/src/controllers/Webhooks.ts index 8283af1..9242955 100644 --- a/apps/api/src/controllers/Webhooks.ts +++ b/apps/api/src/controllers/Webhooks.ts @@ -154,7 +154,7 @@ export class Webhooks { body: '', // Inbound emails don't have body content in our system from: recipientEmail, // The recipient address that received the email sourceType: EmailSourceType.INBOUND, - status: EmailStatus.DELIVERED, // Inbound emails are already delivered + status: EmailStatus.RECEIVED, // Inbound emails use RECEIVED status deliveredAt: new Date(body.mail?.timestamp || new Date()), }, }); diff --git a/apps/api/src/services/ActivityService.ts b/apps/api/src/services/ActivityService.ts index 2f162ec..9a66e38 100644 --- a/apps/api/src/services/ActivityService.ts +++ b/apps/api/src/services/ActivityService.ts @@ -376,6 +376,9 @@ export class ActivityService { if (!types || types.includes(ActivityType.EMAIL_DELIVERED)) { orConditions.push({deliveredAt: {not: null, ...dateFilter}}); } + if (!types || types.includes(ActivityType.EMAIL_RECEIVED)) { + orConditions.push({deliveredAt: {not: null, ...dateFilter}, sourceType: 'INBOUND'}); + } if (!types || types.includes(ActivityType.EMAIL_OPENED)) { orConditions.push({openedAt: {not: null, ...dateFilter}}); } @@ -485,6 +488,22 @@ export class ActivityService { }); } + if ( + email.deliveredAt && + email.sourceType === 'INBOUND' && + (!types || types.includes(ActivityType.EMAIL_RECEIVED)) && + isInDateRange(email.deliveredAt) + ) { + activities.push({ + id: `${email.id}_received`, + type: ActivityType.EMAIL_RECEIVED, + timestamp: email.deliveredAt, + contactEmail: email.contact.email, + contactId: email.contactId, + metadata: baseMetadata, + }); + } + if (email.openedAt && (!types || types.includes(ActivityType.EMAIL_OPENED)) && isInDateRange(email.openedAt)) { activities.push({ id: `${email.id}_opened`, diff --git a/apps/api/src/services/EmailService.ts b/apps/api/src/services/EmailService.ts index ef63f24..99dec73 100644 --- a/apps/api/src/services/EmailService.ts +++ b/apps/api/src/services/EmailService.ts @@ -585,10 +585,11 @@ export class EmailService { : {}), }; - const [total, sent, delivered, opened, clicked, bounced, failed] = await Promise.all([ + const [total, sent, delivered, received, opened, clicked, bounced, failed] = await Promise.all([ prisma.email.count({where}), prisma.email.count({where: {...where, status: EmailStatus.SENT}}), prisma.email.count({where: {...where, status: EmailStatus.DELIVERED}}), + prisma.email.count({where: {...where, status: EmailStatus.RECEIVED}}), prisma.email.count({where: {...where, status: EmailStatus.OPENED}}), prisma.email.count({where: {...where, status: EmailStatus.CLICKED}}), prisma.email.count({where: {...where, status: EmailStatus.BOUNCED}}), @@ -599,6 +600,7 @@ export class EmailService { total, sent, delivered, + received, opened, clicked, bounced, diff --git a/apps/web/src/components/ActivityItem.tsx b/apps/web/src/components/ActivityItem.tsx index 386d34d..9d90c98 100644 --- a/apps/web/src/components/ActivityItem.tsx +++ b/apps/web/src/components/ActivityItem.tsx @@ -9,6 +9,7 @@ import { CheckCircle, ChevronRight, Eye, + Inbox, MousePointerClick, Send, ShieldAlert, @@ -99,6 +100,7 @@ function isEmailActivity(type: string): boolean { return [ 'email.sent', 'email.delivered', + 'email.received', 'email.opened', 'email.clicked', 'email.bounced', @@ -181,6 +183,19 @@ function getActivityConfig(activity: Activity): ActivityConfig { }, }; + case 'email.received': + return { + icon: Inbox, + color: 'text-blue-600', + bgColor: 'bg-blue-100', + title: (typeof metadata.subject === 'string' ? metadata.subject : undefined) || 'Email received', + description: typeof metadata.from === 'string' ? `From: ${metadata.from}` : 'Inbound email', + badge: { + label: 'Received', + variant: 'default', + }, + }; + case 'email.opened': return { icon: Eye, diff --git a/apps/web/src/pages/activity/index.tsx b/apps/web/src/pages/activity/index.tsx index b07af1e..a3266bf 100644 --- a/apps/web/src/pages/activity/index.tsx +++ b/apps/web/src/pages/activity/index.tsx @@ -119,10 +119,12 @@ export default function ActivityPage() { All Activity Types Events - + Emails Emails Sent + Emails Delivered + Emails Received Emails Opened Emails Clicked Emails Bounced diff --git a/packages/db/prisma/migrations/20260218151425_add_received_email_status/migration.sql b/packages/db/prisma/migrations/20260218151425_add_received_email_status/migration.sql new file mode 100644 index 0000000..90b3f2e --- /dev/null +++ b/packages/db/prisma/migrations/20260218151425_add_received_email_status/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "EmailStatus" ADD VALUE 'RECEIVED'; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index bf364d8..f5dc63b 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -759,6 +759,7 @@ enum EmailStatus { SENDING // Currently being sent SENT // Successfully sent to provider DELIVERED // Confirmed delivered + RECEIVED // Received by platform (inbound) OPENED // Recipient opened email CLICKED // Recipient clicked link BOUNCED // Bounced (hard or soft) diff --git a/packages/types/src/api/activity.ts b/packages/types/src/api/activity.ts index edb4256..0069dbc 100644 --- a/packages/types/src/api/activity.ts +++ b/packages/types/src/api/activity.ts @@ -9,6 +9,7 @@ export enum ActivityType { EVENT_TRIGGERED = 'event.triggered', EMAIL_SENT = 'email.sent', EMAIL_DELIVERED = 'email.delivered', + EMAIL_RECEIVED = 'email.received', EMAIL_OPENED = 'email.opened', EMAIL_CLICKED = 'email.clicked', EMAIL_BOUNCED = 'email.bounced',