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/__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",
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..107877d2a5 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().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/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..373a64baaa 100644
--- a/apps/web/utils/actions/user.ts
+++ b/apps/web/utils/actions/user.ts
@@ -13,10 +13,11 @@ import {
saveAboutBody,
saveSignatureBody,
} from "@/utils/actions/user.validation";
+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 },
@@ -26,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 },
@@ -44,18 +45,24 @@ export const resetAnalyticsAction = actionClient
export const deleteAccountAction = actionClientUser
.metadata({ name: "deleteAccount" })
- .action(async ({ ctx: { userId } }) => {
- try {
- await betterAuthConfig.api.signOut({
+ .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((error) => {
+ logger.error("Failed to sign out", { error });
});
- } catch {}
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 },
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);
+}
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..6ccb163614 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,30 @@ 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 '${escapeODataString(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/version.txt b/version.txt
index 5f4b2ff1c8..c2be048130 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-v2.17.25
+v2.17.26