Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
98 changes: 98 additions & 0 deletions apps/web/__tests__/e2e/outlook-draft-read-status.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* E2E test to verify our Outlook draft implementation doesn't mark emails as read
*
* Microsoft Graph's createReplyAll endpoint has an undocumented side effect:
* it marks the original message as read. Our implementation works around this
* by restoring the original read status after creating the draft.
*
* Usage: pnpm test-e2e outlook-draft-read-status
* Make sure TEST_OUTLOOK_EMAIL=you@email.com is set in .env.test
*/

import { beforeAll, describe, expect, test, vi } from "vitest";
import prisma from "@/utils/prisma";
import { createEmailProvider } from "@/utils/email/provider";
import { findOldMessage } from "@/__tests__/e2e/helpers";
import type { EmailProvider } from "@/utils/email/types";

const RUN_E2E_TESTS = process.env.RUN_E2E_TESTS;
const TEST_OUTLOOK_EMAIL = process.env.TEST_OUTLOOK_EMAIL;

vi.mock("server-only", () => ({}));

describe.skipIf(!RUN_E2E_TESTS)(
"Outlook Draft Read Status Preservation",
() => {
let provider: EmailProvider;
let emailAccountEmail: string;

beforeAll(async () => {
if (!TEST_OUTLOOK_EMAIL) {
console.warn("Set TEST_OUTLOOK_EMAIL env var to run these tests");
return;
}

const emailAccount = await prisma.emailAccount.findFirst({
where: {
email: TEST_OUTLOOK_EMAIL,
account: { provider: "microsoft" },
},
include: { account: true },
});

if (!emailAccount) {
throw new Error(`No Outlook account found for ${TEST_OUTLOOK_EMAIL}`);
}

provider = await createEmailProvider({
emailAccountId: emailAccount.id,
provider: "microsoft",
});

emailAccountEmail = emailAccount.email;
});

test("should preserve unread status when creating draft reply", async () => {
if (!provider) {
throw new Error("Email provider not initialized");
}

const testMessage = await findOldMessage(provider, 7);
const originalMessage = await provider.getMessage(testMessage.messageId);
const wasOriginallyUnread =
originalMessage.labelIds?.includes("UNREAD") ?? false;

let draftId: string | undefined;

try {
// Mark as unread for the test
await provider.markReadThread(testMessage.threadId, false);

// Verify unread status before creating draft
const beforeDraft = await provider.getMessage(testMessage.messageId);
expect(beforeDraft.labelIds).toContain("UNREAD");

// Create draft reply - our implementation should NOT mark the original as read
const draftResult = await provider.draftEmail(
beforeDraft,
{ content: "Test draft for read status verification" },
emailAccountEmail,
);
draftId = draftResult.draftId;

// Message should still be unread after draft creation
const afterDraft = await provider.getMessage(testMessage.messageId);
expect(afterDraft.labelIds).toContain("UNREAD");
} finally {
// Cleanup: restore original state
if (draftId) {
await provider.deleteDraft(draftId);
}
await provider.markReadThread(
testMessage.threadId,
!wasOriginallyUnread,
);
}
}, 30_000);
},
);
22 changes: 22 additions & 0 deletions apps/web/utils/outlook/mail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,17 @@ export async function draftEmail(
},
}));

// Get the original message's isRead status before creating the draft
// Microsoft Graph's createReplyAll automatically marks the original as read
const originalMessage: Message = await withOutlookRetry(() =>
client
.getClient()
.api(`/me/messages/${originalEmail.id}`)
.select("isRead")
.get(),
);
const wasUnread = originalMessage.isRead === false;

// Use createReplyAll endpoint to create a proper reply draft
// This ensures the draft is linked to the original message as a reply all
const replyDraft: Message = await withOutlookRetry(() =>
Expand Down Expand Up @@ -238,6 +249,17 @@ export async function draftEmail(
}),
);

// Restore the original message's unread status if it was unread before
// createReplyAll automatically marks the original message as read
if (wasUnread) {
await withOutlookRetry(() =>
client
.getClient()
.api(`/me/messages/${originalEmail.id}`)
.patch({ isRead: false }),
);
}

// Use the original replyDraft.id since that's the stable ID
// The PATCH response might not always include the full object?
return { ...updatedDraft, id: replyDraft.id };
Expand Down
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v2.21.62
v2.21.63
Loading