diff --git a/apps/web/utils/actions/admin.ts b/apps/web/utils/actions/admin.ts
index 60dbfce39a..577fa76528 100644
--- a/apps/web/utils/actions/admin.ts
+++ b/apps/web/utils/actions/admin.ts
@@ -10,7 +10,10 @@ import { syncStripeDataToDb } from "@/ee/billing/stripe/sync-stripe";
import { getStripe } from "@/ee/billing/stripe";
import { createEmailProvider } from "@/utils/email/provider";
import { hash } from "@/utils/hash";
-import { hashEmailBody } from "@/utils/actions/admin.validation";
+import {
+ hashEmailBody,
+ convertGmailUrlBody,
+} from "@/utils/actions/admin.validation";
export const adminProcessHistoryAction = adminActionClient
.metadata({ name: "adminProcessHistory" })
@@ -196,3 +199,59 @@ export const adminHashEmailAction = adminActionClient
const hashed = hash(email);
return { hash: hashed };
});
+
+export const adminConvertGmailUrlAction = adminActionClient
+ .metadata({ name: "adminConvertGmailUrl" })
+ .schema(convertGmailUrlBody)
+ .action(async ({ parsedInput: { rfc822MessageId, email } }) => {
+ // Clean up Message-ID (remove < > if present)
+ const cleanMessageId = rfc822MessageId.trim().replace(/^<|>$/g, "");
+
+ const emailAccount = await prisma.emailAccount.findUnique({
+ where: { email: email.toLowerCase() },
+ select: {
+ id: true,
+ account: {
+ select: {
+ provider: true,
+ },
+ },
+ },
+ });
+
+ if (!emailAccount) {
+ throw new SafeError("Email account not found");
+ }
+
+ const emailProvider = await createEmailProvider({
+ emailAccountId: emailAccount.id,
+ provider: emailAccount.account.provider,
+ });
+
+ const message =
+ await emailProvider.getMessageByRfc822MessageId(cleanMessageId);
+
+ if (!message) {
+ throw new SafeError(
+ `Could not find message with RFC822 Message-ID: ${cleanMessageId}`,
+ );
+ }
+
+ if (!message.threadId) {
+ throw new SafeError("Message does not have a thread ID");
+ }
+
+ const thread = await emailProvider.getThread(message.threadId);
+
+ if (!thread) {
+ throw new SafeError("Could not find thread for message");
+ }
+
+ const messageIds = thread.messages?.map((m) => m.id) || [];
+
+ return {
+ threadId: thread.id,
+ messageIds: messageIds,
+ rfc822MessageId: cleanMessageId,
+ };
+ });
diff --git a/apps/web/utils/actions/admin.validation.ts b/apps/web/utils/actions/admin.validation.ts
index ce705b974f..e8da18b4ec 100644
--- a/apps/web/utils/actions/admin.validation.ts
+++ b/apps/web/utils/actions/admin.validation.ts
@@ -4,3 +4,9 @@ export const hashEmailBody = z.object({
email: z.string().min(1, "Value is required"),
});
export type HashEmailBody = z.infer
;
+
+export const convertGmailUrlBody = z.object({
+ rfc822MessageId: z.string().min(1, "RFC822 Message-ID is required"),
+ email: z.string().email("Valid email address is required"),
+});
+export type ConvertGmailUrlBody = z.infer;
diff --git a/apps/web/utils/email/google.ts b/apps/web/utils/email/google.ts
index 27137aca1b..0e9f8cdb61 100644
--- a/apps/web/utils/email/google.ts
+++ b/apps/web/utils/email/google.ts
@@ -160,6 +160,14 @@ export class GmailProvider implements EmailProvider {
return parseMessage(message);
}
+ async getMessageByRfc822MessageId(
+ rfc822MessageId: string,
+ ): Promise {
+ const message = await getMessageByRfc822Id(rfc822MessageId, this.client);
+ if (!message) return null;
+ return parseMessage(message);
+ }
+
async getSentMessages(maxResults = 20): Promise {
return getSentMessages(this.client, maxResults);
}
diff --git a/apps/web/utils/email/microsoft.ts b/apps/web/utils/email/microsoft.ts
index 88f8310f39..cdf8c33d6f 100644
--- a/apps/web/utils/email/microsoft.ts
+++ b/apps/web/utils/email/microsoft.ts
@@ -6,6 +6,7 @@ import {
getMessages,
queryBatchMessages,
getFolderIds,
+ convertMessage,
} from "@/utils/outlook/message";
import {
getLabels,
@@ -151,6 +152,28 @@ export class OutlookProvider implements EmailProvider {
}
}
+ async getMessageByRfc822MessageId(
+ rfc822MessageId: string,
+ ): Promise {
+ const cleanMessageId = rfc822MessageId.trim().replace(/^<|>$/g, "");
+ const messageIdWithBrackets = `<${cleanMessageId}>`;
+
+ const response = await this.client
+ .getClient()
+ .api("/me/messages")
+ .filter(`internetMessageId eq '${messageIdWithBrackets}'`)
+ .top(1)
+ .get();
+
+ const message = response.value?.[0];
+ if (!message) {
+ return null;
+ }
+
+ const folderIds = await getFolderIds(this.client);
+ return convertMessage(message, folderIds);
+ }
+
private async getMessages({
searchQuery,
maxResults = 50,
diff --git a/apps/web/utils/email/types.ts b/apps/web/utils/email/types.ts
index d0b0911bea..782f7029bb 100644
--- a/apps/web/utils/email/types.ts
+++ b/apps/web/utils/email/types.ts
@@ -50,6 +50,9 @@ export interface EmailProvider {
getLabelByName(name: string): Promise;
getFolders(): Promise;
getMessage(messageId: string): Promise;
+ getMessageByRfc822MessageId(
+ rfc822MessageId: string,
+ ): Promise;
getMessagesByFields(options: {
froms?: string[];
tos?: string[];
diff --git a/apps/web/utils/gmail/gmail-id.ts b/apps/web/utils/gmail/gmail-id.ts
new file mode 100644
index 0000000000..d3008162e8
--- /dev/null
+++ b/apps/web/utils/gmail/gmail-id.ts
@@ -0,0 +1,14 @@
+/**
+ * Gmail ID utilities
+ */
+
+/**
+ * Extract Gmail ID from a Gmail URL
+ * @param url - Gmail URL
+ * @returns Gmail ID (either URL format or API format, depending on what's in the URL)
+ */
+export function extractGmailIdFromUrl(url: string): string | null {
+ // Match pattern: #label/ID or #search/query/ID
+ const match = url.match(/#[^/]+\/([A-Za-z0-9_-]+)/);
+ return match ? match[1] : null;
+}
diff --git a/version.txt b/version.txt
index 5f4b2ff1c8..c2be048130 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-v2.17.25
+v2.17.26
From af2e1a11ba937cc3b4b0c0766f80712ef190a6b1 Mon Sep 17 00:00:00 2001
From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com>
Date: Sun, 2 Nov 2025 23:42:36 +0200
Subject: [PATCH 2/7] fix mocl
---
apps/web/utils/__mocks__/email-provider.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/apps/web/utils/__mocks__/email-provider.ts b/apps/web/utils/__mocks__/email-provider.ts
index c149e793e8..3dd1e12f18 100644
--- a/apps/web/utils/__mocks__/email-provider.ts
+++ b/apps/web/utils/__mocks__/email-provider.ts
@@ -45,6 +45,8 @@ export const createMockEmailProvider = (
getLabels: vi.fn().mockResolvedValue([]),
getLabelById: vi.fn().mockResolvedValue(null),
getLabelByName: vi.fn().mockResolvedValue(null),
+ getMessageByRfc822MessageId: vi.fn().mockResolvedValue(null),
+ getFolders: vi.fn().mockResolvedValue([]),
getSignatures: vi.fn().mockResolvedValue([]),
getMessage: vi.fn().mockResolvedValue({
id: "msg1",
From 3f468785a409e848cfc299ff6c93d5742ed67335 Mon Sep 17 00:00:00 2001
From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com>
Date: Sun, 2 Nov 2025 23:44:25 +0200
Subject: [PATCH 3/7] delete unused file
---
apps/web/utils/gmail/gmail-id.ts | 14 --------------
1 file changed, 14 deletions(-)
delete mode 100644 apps/web/utils/gmail/gmail-id.ts
diff --git a/apps/web/utils/gmail/gmail-id.ts b/apps/web/utils/gmail/gmail-id.ts
deleted file mode 100644
index d3008162e8..0000000000
--- a/apps/web/utils/gmail/gmail-id.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-/**
- * Gmail ID utilities
- */
-
-/**
- * Extract Gmail ID from a Gmail URL
- * @param url - Gmail URL
- * @returns Gmail ID (either URL format or API format, depending on what's in the URL)
- */
-export function extractGmailIdFromUrl(url: string): string | null {
- // Match pattern: #label/ID or #search/query/ID
- const match = url.match(/#[^/]+\/([A-Za-z0-9_-]+)/);
- return match ? match[1] : null;
-}
From b11703d688c5a59ecf05c2b99e1816319485b3cc Mon Sep 17 00:00:00 2001
From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com>
Date: Mon, 3 Nov 2025 01:56:50 +0200
Subject: [PATCH 4/7] fixes
---
apps/web/utils/actions/admin.validation.ts | 4 ++--
apps/web/utils/email/microsoft.ts | 4 +++-
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/apps/web/utils/actions/admin.validation.ts b/apps/web/utils/actions/admin.validation.ts
index e8da18b4ec..107877d2a5 100644
--- a/apps/web/utils/actions/admin.validation.ts
+++ b/apps/web/utils/actions/admin.validation.ts
@@ -6,7 +6,7 @@ export const hashEmailBody = z.object({
export type HashEmailBody = z.infer;
export const convertGmailUrlBody = z.object({
- rfc822MessageId: z.string().min(1, "RFC822 Message-ID is required"),
- email: z.string().email("Valid email address is required"),
+ rfc822MessageId: z.string().trim().min(1, "RFC822 Message-ID is required"),
+ email: z.string().trim().email("Valid email address is required"),
});
export type ConvertGmailUrlBody = z.infer;
diff --git a/apps/web/utils/email/microsoft.ts b/apps/web/utils/email/microsoft.ts
index cdf8c33d6f..6ccb163614 100644
--- a/apps/web/utils/email/microsoft.ts
+++ b/apps/web/utils/email/microsoft.ts
@@ -161,7 +161,9 @@ export class OutlookProvider implements EmailProvider {
const response = await this.client
.getClient()
.api("/me/messages")
- .filter(`internetMessageId eq '${messageIdWithBrackets}'`)
+ .filter(
+ `internetMessageId eq '${escapeODataString(messageIdWithBrackets)}'`,
+ )
.top(1)
.get();
From 72eb58c435be3f4185e81b3bfc05cca50f47dbff Mon Sep 17 00:00:00 2001
From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com>
Date: Mon, 3 Nov 2025 01:57:15 +0200
Subject: [PATCH 5/7] ui
---
apps/web/components/NavUser.tsx | 8 ++++++++
apps/web/components/SideNav.tsx | 7 -------
apps/web/utils/actions/email-account-cookie.ts | 4 ++--
apps/web/utils/actions/user.ts | 11 +++++++----
apps/web/utils/cookies.server.ts | 7 +++++++
5 files changed, 24 insertions(+), 13 deletions(-)
create mode 100644 apps/web/utils/cookies.server.ts
diff --git a/apps/web/components/NavUser.tsx b/apps/web/components/NavUser.tsx
index bf42cb0aaf..67630d4b21 100644
--- a/apps/web/components/NavUser.tsx
+++ b/apps/web/components/NavUser.tsx
@@ -11,6 +11,7 @@ import {
PaletteIcon,
ChromeIcon,
Building2Icon,
+ CrownIcon,
} from "lucide-react";
import {
DropdownMenu,
@@ -174,6 +175,13 @@ export function NavUser() {
Usage
+
+
+
+
+ Premium
+
+
diff --git a/apps/web/components/SideNav.tsx b/apps/web/components/SideNav.tsx
index 6597a4ccb8..7ef61f2791 100644
--- a/apps/web/components/SideNav.tsx
+++ b/apps/web/components/SideNav.tsx
@@ -247,13 +247,6 @@ export function SideNav({ ...props }: React.ComponentProps) {
-
-
-
- Premium
-
-
-
diff --git a/apps/web/utils/actions/email-account-cookie.ts b/apps/web/utils/actions/email-account-cookie.ts
index 7dd262243b..a20b4a84a0 100644
--- a/apps/web/utils/actions/email-account-cookie.ts
+++ b/apps/web/utils/actions/email-account-cookie.ts
@@ -6,6 +6,7 @@ import {
LAST_EMAIL_ACCOUNT_COOKIE,
type LastEmailAccountCookieValue,
} from "@/utils/cookies";
+import { clearLastEmailAccountCookie } from "@/utils/cookies.server";
import { actionClientUser } from "@/utils/actions/safe-action";
/**
@@ -40,6 +41,5 @@ export const setLastEmailAccountAction = actionClientUser
export const clearLastEmailAccountAction = actionClientUser
.metadata({ name: "clearLastEmailAccount" })
.action(async () => {
- const cookieStore = await cookies();
- cookieStore.delete(LAST_EMAIL_ACCOUNT_COOKIE);
+ await clearLastEmailAccountCookie();
});
diff --git a/apps/web/utils/actions/user.ts b/apps/web/utils/actions/user.ts
index 47b8607b9c..c04f0910af 100644
--- a/apps/web/utils/actions/user.ts
+++ b/apps/web/utils/actions/user.ts
@@ -13,6 +13,7 @@ import {
saveAboutBody,
saveSignatureBody,
} from "@/utils/actions/user.validation";
+import { clearLastEmailAccountCookie } from "@/utils/cookies.server";
export const saveAboutAction = actionClient
.metadata({ name: "saveAbout" })
@@ -45,11 +46,13 @@ export const resetAnalyticsAction = actionClient
export const deleteAccountAction = actionClientUser
.metadata({ name: "deleteAccount" })
.action(async ({ ctx: { userId } }) => {
- try {
- await betterAuthConfig.api.signOut({
+ clearLastEmailAccountCookie().catch(() => {});
+
+ await betterAuthConfig.api
+ .signOut({
headers: await headers(),
- });
- } catch {}
+ })
+ .catch(() => {});
await deleteUser({ userId });
});
diff --git a/apps/web/utils/cookies.server.ts b/apps/web/utils/cookies.server.ts
new file mode 100644
index 0000000000..88dced2142
--- /dev/null
+++ b/apps/web/utils/cookies.server.ts
@@ -0,0 +1,7 @@
+import { cookies } from "next/headers";
+import { LAST_EMAIL_ACCOUNT_COOKIE } from "@/utils/cookies";
+
+export async function clearLastEmailAccountCookie() {
+ const cookieStore = await cookies();
+ cookieStore.delete(LAST_EMAIL_ACCOUNT_COOKIE);
+}
From 8840172c30245bc1c365083e91fc7faf8d25a35b Mon Sep 17 00:00:00 2001
From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com>
Date: Mon, 3 Nov 2025 02:02:48 +0200
Subject: [PATCH 6/7] fix
---
apps/web/utils/actions/user.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/web/utils/actions/user.ts b/apps/web/utils/actions/user.ts
index c04f0910af..1544b5e4bf 100644
--- a/apps/web/utils/actions/user.ts
+++ b/apps/web/utils/actions/user.ts
@@ -46,7 +46,7 @@ export const resetAnalyticsAction = actionClient
export const deleteAccountAction = actionClientUser
.metadata({ name: "deleteAccount" })
.action(async ({ ctx: { userId } }) => {
- clearLastEmailAccountCookie().catch(() => {});
+ await clearLastEmailAccountCookie().catch(() => {});
await betterAuthConfig.api
.signOut({
From 2d0dc3bf05d3131cda61fb775655e3a43c90a245 Mon Sep 17 00:00:00 2001
From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com>
Date: Mon, 3 Nov 2025 02:03:52 +0200
Subject: [PATCH 7/7] fix
---
apps/web/utils/actions/user.ts | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/apps/web/utils/actions/user.ts b/apps/web/utils/actions/user.ts
index 1544b5e4bf..373a64baaa 100644
--- a/apps/web/utils/actions/user.ts
+++ b/apps/web/utils/actions/user.ts
@@ -17,7 +17,7 @@ import { clearLastEmailAccountCookie } from "@/utils/cookies.server";
export const saveAboutAction = actionClient
.metadata({ name: "saveAbout" })
- .schema(saveAboutBody)
+ .inputSchema(saveAboutBody)
.action(async ({ parsedInput: { about }, ctx: { emailAccountId } }) => {
await prisma.emailAccount.update({
where: { id: emailAccountId },
@@ -27,7 +27,7 @@ export const saveAboutAction = actionClient
export const saveSignatureAction = actionClient
.metadata({ name: "saveSignature" })
- .schema(saveSignatureBody)
+ .inputSchema(saveSignatureBody)
.action(async ({ parsedInput: { signature }, ctx: { emailAccountId } }) => {
await prisma.emailAccount.update({
where: { id: emailAccountId },
@@ -45,20 +45,24 @@ export const resetAnalyticsAction = actionClient
export const deleteAccountAction = actionClientUser
.metadata({ name: "deleteAccount" })
- .action(async ({ ctx: { userId } }) => {
- await clearLastEmailAccountCookie().catch(() => {});
+ .action(async ({ ctx: { userId, logger } }) => {
+ await clearLastEmailAccountCookie().catch((error) => {
+ logger.error("Failed to clear last email account cookie", { error });
+ });
await betterAuthConfig.api
.signOut({
headers: await headers(),
})
- .catch(() => {});
+ .catch((error) => {
+ logger.error("Failed to sign out", { error });
+ });
await deleteUser({ userId });
});
export const deleteEmailAccountAction = actionClientUser
.metadata({ name: "deleteEmailAccount" })
- .schema(z.object({ emailAccountId: z.string() }))
+ .inputSchema(z.object({ emailAccountId: z.string() }))
.action(async ({ ctx: { userId }, parsedInput: { emailAccountId } }) => {
const emailAccount = await prisma.emailAccount.findUnique({
where: { id: emailAccountId, userId },