Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ function OpenInGmailButton({

return (
<Link
href={getEmailUrlForMessage(messageId, threadId, userEmail, provider)}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modified this function signature to pass the entire message instead of just messageId and threadId.
Reason was that for Outlook, the link will be sourced from the message.webLink and gmail will use the message.id

href={getEmailUrlForMessage(messageId, threadId, provider, userEmail)}
target="_blank"
className="ml-2 text-muted-foreground hover:text-foreground"
>
Expand Down
20 changes: 20 additions & 0 deletions apps/web/app/api/resend/digest/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { extractNameFromEmail } from "../../../../utils/email";
import { RuleName } from "@/utils/rule/consts";
import { verifySignatureAppRouter } from "@upstash/qstash/nextjs";
import { camelCase } from "lodash";
import { getEmailUrlForMessage } from "@/utils/url";
import { createEmailProvider } from "@/utils/email/provider";
import { sleep } from "@/utils/sleep";

Expand Down Expand Up @@ -129,6 +130,7 @@ async function sendEmail({
items: {
select: {
messageId: true,
threadId: true,
content: true,
action: {
select: {
Expand Down Expand Up @@ -246,10 +248,28 @@ async function sendEmail({
storedDigestContentSchema.safeParse(parsedContent);

if (contentResult.success) {
// For Microsoft messages, use the weblink from Graph API if available
// Otherwise fall back to constructed URL
let emailUrl: string;
if (
emailAccount.account.provider === "microsoft" &&
message?.weblink
) {
emailUrl = message.weblink;
} else {
emailUrl = getEmailUrlForMessage(
item.messageId,
item.threadId,
emailAccount.account.provider,
emailAccount.email,
);
}

acc[ruleNameKey].push({
content: contentResult.data.content,
from: extractNameFromEmail(message?.headers?.from || ""),
subject: message?.headers?.subject || "",
emailUrl,
});
} else {
logger.warn("Failed to validate digest content structure", {
Expand Down
1 change: 1 addition & 0 deletions apps/web/app/api/resend/digest/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const digestItemSchema = z.object({
from: z.string(),
subject: z.string(),
content: z.string(),
emailUrl: z.string().optional(),
});

const digestSchema = z.record(z.string(), z.array(digestItemSchema).optional());
Expand Down
2 changes: 1 addition & 1 deletion apps/web/components/EmailMessageCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ export function EmailMessageCell({
href={getEmailUrlForMessage(
messageId,
threadId,
userEmail,
provider,
userEmail,
)}
target="_blank"
>
Expand Down
7 changes: 4 additions & 3 deletions apps/web/utils/outlook/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ export async function queryBatchMessages(
.getClient()
.api("/me/messages")
.select(
"id,conversationId,conversationIndex,subject,bodyPreview,from,sender,toRecipients,receivedDateTime,isDraft,isRead,body,categories,parentFolderId",
"id,conversationId,conversationIndex,subject,bodyPreview,from,sender,toRecipients,receivedDateTime,isDraft,isRead,body,categories,parentFolderId,webLink",
)
.top(maxResults);

Expand Down Expand Up @@ -347,7 +347,7 @@ export async function queryMessagesWithFilters(
.getClient()
.api("/me/messages")
.select(
"id,conversationId,conversationIndex,subject,bodyPreview,from,sender,toRecipients,receivedDateTime,isDraft,isRead,body,categories,parentFolderId",
"id,conversationId,conversationIndex,subject,bodyPreview,from,sender,toRecipients,receivedDateTime,isDraft,isRead,body,categories,parentFolderId,webLink",
)
.top(maxResults);

Expand Down Expand Up @@ -419,7 +419,7 @@ export async function getMessage(
.getClient()
.api(`/me/messages/${messageId}`)
.select(
"id,conversationId,conversationIndex,subject,bodyPreview,from,sender,toRecipients,receivedDateTime,isDraft,isRead,body,categories,parentFolderId",
"id,conversationId,conversationIndex,subject,bodyPreview,from,sender,toRecipients,receivedDateTime,isDraft,isRead,body,categories,parentFolderId,webLink",
)
.get();

Expand Down Expand Up @@ -487,5 +487,6 @@ export function convertMessage(
historyId: "",
inline: [],
conversationIndex: message.conversationIndex,
weblink: message.webLink,
};
}
1 change: 1 addition & 0 deletions apps/web/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export interface ParsedMessage {
date: string;
conversationIndex?: string | null;
internalDate?: string | null;
weblink?: string;
}

export interface Attachment {
Expand Down
116 changes: 116 additions & 0 deletions apps/web/utils/url.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { describe, it, expect } from "vitest";
import { getEmailUrlForMessage } from "./url";

describe("URL Generation", () => {
const testMessageId = "18c1234567890abcdef";
const testThreadId =
"AQQkADAwATZiZmYAZC02NgAyNC1mOGZmAC0wMAItMDAKABAAdO%2BkeNAzxk%2BrwljH9yJ17w%3D%3D";
const testEmail = "[email protected]";

it("should generate Gmail URL for Google provider", () => {
const url = getEmailUrlForMessage(
testMessageId,
testThreadId,
"google",
testEmail,
);

expect(url).toBe(
`https://mail.google.com/mail/?authuser=${encodeURIComponent(testEmail)}#all/${testMessageId}`,
);
});

it("should generate Outlook URL for Microsoft provider", () => {
const url = getEmailUrlForMessage(
testMessageId,
testThreadId,
"microsoft",
testEmail,
);

expect(url).toBe(
`https://outlook.live.com/mail/0/inbox/id/${testThreadId}`,
);
});

it("should generate Outlook URL defaulting to inbox folder", () => {
const url = getEmailUrlForMessage(
testMessageId,
testThreadId,
"microsoft",
testEmail,
);

expect(url).toBe(
`https://outlook.live.com/mail/0/inbox/id/${testThreadId}`,
);
});

// No folder argument supported for Outlook anymore; always defaults to inbox

it("should fallback to Gmail URL for unknown provider", () => {
const url = getEmailUrlForMessage(
testMessageId,
testThreadId,
"unknown",
testEmail,
);

expect(url).toBe(
`https://mail.google.com/mail/?authuser=${encodeURIComponent(testEmail)}#all/${testMessageId}`,
);
});

it("should generate different URLs for different providers", () => {
const gmailUrl = getEmailUrlForMessage(
testMessageId,
testThreadId,
"google",
testEmail,
);
const outlookUrl = getEmailUrlForMessage(
testMessageId,
testThreadId,
"microsoft",
testEmail,
);

expect(gmailUrl).not.toBe(outlookUrl);
expect(gmailUrl).toContain("mail.google.com");
expect(outlookUrl).toContain("outlook.live.com");
});

it("should handle empty email address", () => {
const url = getEmailUrlForMessage(
testMessageId,
testThreadId,
"google",
"",
);

expect(url).toContain("mail.google.com");
expect(url).toContain(testMessageId);
});

it("should encode special characters in email address", () => {
const url = getEmailUrlForMessage(
testMessageId,
testThreadId,
"google",
"[email protected]",
);

expect(url).toContain("test%2Btag%40example.com");
});

it("should default to inbox for Outlook when no folder is provided", () => {
const url = getEmailUrlForMessage(
testMessageId,
testThreadId,
"microsoft",
testEmail,
);

expect(url).toContain("/inbox/id/");
});
});
70 changes: 40 additions & 30 deletions apps/web/utils/url.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
function getGmailBaseUrl(emailAddress?: string | null) {
return `https://mail.google.com/mail/u/${emailAddress || 0}`;
function getGmailBaseUrl(emailAddress?: string) {
if (emailAddress) {
return `https://mail.google.com/mail/u/?email=${encodeURIComponent(emailAddress)}`;
}
return "https://mail.google.com/mail/u/0";
}

function getGmailMessageUrl(messageId: string, emailAddress?: string) {
if (emailAddress) {
return `https://mail.google.com/mail/?authuser=${encodeURIComponent(emailAddress)}#all/${messageId}`;
}
return `https://mail.google.com/mail/u/0/#all/${messageId}`;
}

function getOutlookBaseUrl() {
Expand All @@ -9,44 +19,49 @@ function getOutlookBaseUrl() {
const PROVIDER_CONFIG: Record<
string,
{
buildUrl: (
messageOrThreadId: string,
emailAddress?: string | null,
) => string;
buildUrl: (messageOrThreadId: string, emailAddress?: string) => string;
selectId: (messageId: string, threadId: string) => string;
}
> = {
microsoft: {
buildUrl: (messageOrThreadId: string, _emailAddress?: string | null) => {
// Outlook URL format: https://outlook.live.com/mail/0/inbox/id/ENCODED_MESSAGE_ID
// The message ID needs to be URL-encoded for Outlook
const encodedMessageId = encodeURIComponent(messageOrThreadId);
return `${getOutlookBaseUrl()}/inbox/id/${encodedMessageId}`;
buildUrl: (messageOrThreadId: string, _emailAddress?: string) => {
// Working Outlook URL format discovered from real URLs:
// https://outlook.live.com/mail/0/{FOLDER}/id/{MESSAGE_ID}
// Examples:
// - https://outlook.live.com/mail/0/inbox/id/...
// - https://outlook.live.com/mail/0/archive/id/...
// - https://outlook.live.com/mail/0/junkemail/id/...
// NOTE: Don't encode the messageId - it's already URL-encoded from Outlook
// We do not rely on folder names; default to inbox for fallback links
return `${getOutlookBaseUrl()}/inbox/id/${messageOrThreadId}`;
},
selectId: (_messageId: string, threadId: string) => threadId,
},
google: {
buildUrl: (messageOrThreadId: string, emailAddress?: string | null) =>
`${getGmailBaseUrl(emailAddress)}/#all/${messageOrThreadId}`,
buildUrl: (messageOrThreadId: string, emailAddress?: string) =>
getGmailMessageUrl(messageOrThreadId, emailAddress),
selectId: (messageId: string, _threadId: string) => messageId,
},
default: {
buildUrl: (messageOrThreadId: string, emailAddress?: string | null) =>
`${getGmailBaseUrl(emailAddress)}/#all/${messageOrThreadId}`,
selectId: (_messageId: string, threadId: string) => threadId,
buildUrl: (messageOrThreadId: string, emailAddress?: string) =>
getGmailMessageUrl(messageOrThreadId, emailAddress),
selectId: (messageId: string, _threadId: string) => messageId,
},
} as const;

function getProviderConfig(
provider?: string,
): (typeof PROVIDER_CONFIG)[keyof typeof PROVIDER_CONFIG] {
return PROVIDER_CONFIG[provider ?? "default"];
return (
PROVIDER_CONFIG[provider as keyof typeof PROVIDER_CONFIG] ??
PROVIDER_CONFIG.default
);
}

export function getEmailUrl(
messageOrThreadId: string,
emailAddress?: string | null,
provider?: string,
emailAddress?: string,
): string {
const config = getProviderConfig(provider);
return config.buildUrl(messageOrThreadId, emailAddress);
Expand All @@ -60,33 +75,28 @@ export function getEmailUrl(
export function getEmailUrlForMessage(
messageId: string,
threadId: string,
emailAddress?: string | null,
provider?: string,
emailAddress?: string,
) {
const config = getProviderConfig(provider);
const idToUse = config?.selectId(messageId, threadId);

return getEmailUrl(idToUse, emailAddress, provider);
return getEmailUrl(idToUse, provider, emailAddress);
}

// Keep the old function name for backward compatibility
export function getGmailUrl(
messageOrThreadId: string,
emailAddress?: string | null,
) {
return getEmailUrl(messageOrThreadId, emailAddress, "google");
export function getGmailUrl(messageOrThreadId: string, emailAddress?: string) {
return getEmailUrl(messageOrThreadId, "google", emailAddress);
}

export function getGmailSearchUrl(from: string, emailAddress?: string | null) {
export function getGmailSearchUrl(from: string, emailAddress?: string) {
return `${getGmailBaseUrl(
emailAddress,
)}/#advanced-search/from=${encodeURIComponent(from)}`;
}

export function getGmailBasicSearchUrl(emailAddress: string, query: string) {
return `${getGmailBaseUrl(emailAddress)}/#search/${encodeURIComponent(
query,
)}`;
return `${getGmailBaseUrl(emailAddress)}/#search/${encodeURIComponent(query)}`;
}

// export function getGmailCreateFilterUrl(
Expand All @@ -99,6 +109,6 @@ export function getGmailBasicSearchUrl(emailAddress: string, query: string) {
// )}/#create-filter/from=${encodeURIComponent(search)}`;
// }

export function getGmailFilterSettingsUrl(emailAddress?: string | null) {
export function getGmailFilterSettingsUrl(emailAddress?: string) {
return `${getGmailBaseUrl(emailAddress)}/#settings/filters`;
}
Loading
Loading