fix(mailboxes): give each personal mailbox its own name and avatar

Renaming any personal mailbox renamed all of them. The account profile and
every personal mailbox shared one identity: syncPersonalIdentity wrote the
profile name and avatar to every row matching (userId, type = 'personal'),
PATCH /api/mailboxes/[id] redirected a rename into that sync instead of the
mailbox row, and both the list response and the client overrode whatever was
stored with the account name. Outgoing mail and message lists did the same,
so a secondary mailbox also sent under the account owner's name.

Only the primary mailbox — the one at the account address — carries the
account identity now. Every other personal mailbox keeps the name and avatar
it was given. The rule lives in one place, lib/profile/identity-utils.ts, and
the API routes, the sender, and the client all go through it.

Existing secondary mailboxes keep whatever name the old blanket update left
on them until they are renamed; the rename now sticks.

Fixes #36

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Edgard Navarro-ClemonsandClaude Opus 5 committed 2026-09-11 16:26:48 -07:00
1 parent c5cfa1be6d
commit 9719ced1d3
11 files changed
+301 -51

No files matched your search

+22 -5
View File
@@ -1,7 +1,7 @@
import { eq } from "drizzle-orm";
import { NextResponse } from "next/server";
import { getDb } from "@/db";
import { mailboxes, users } from "@/db/schema";
import { domains, mailboxes, users } from "@/db/schema";
import { requireUser } from "@/lib/auth/cookies";
import { getEnv } from "@/lib/cloudflare";
import { getMailboxAccessLevel } from "@/lib/mailboxes/access";
@@ -11,6 +11,7 @@ import {
avatarKeyFor,
isUploadedAvatarFile,
} from "@/app/api/profile/avatar/utils";
import { tracksAccountIdentity } from "@/lib/profile/identity-utils";
import { syncPersonalIdentity } from "@/lib/profile/sync";
import type { MailboxAvatarRouteParams } from "./types";
import { mailboxAvatarKeyFor } from "./utils";
@@ -27,13 +28,19 @@ export async function GET(request: Request, { params }: MailboxAvatarRouteParams
.select({
avatarKey: mailboxes.avatarKey,
type: mailboxes.type,
localPart: mailboxes.localPart,
hostname: domains.hostname,
ownerEmail: users.email,
ownerAvatarKey: users.avatarKey,
})
.from(mailboxes)
.innerJoin(domains, eq(mailboxes.domainId, domains.id))
.innerJoin(users, eq(mailboxes.userId, users.id))
.where(eq(mailboxes.id, id))
.limit(1);
const avatarKey = mailbox?.type === "personal" ? mailbox.ownerAvatarKey : mailbox?.avatarKey;
const avatarKey = mailbox && tracksAccountIdentity(mailbox, mailbox.ownerEmail)
? mailbox.ownerAvatarKey
: mailbox?.avatarKey;
if (!avatarKey) return new Response("Not found", { status: 404 });
const object = await env.BUCKET.get(avatarKey);
@@ -75,18 +82,28 @@ export async function POST(request: Request, { params }: MailboxAvatarRouteParam
}
const [mailbox] = await db
.select({ userId: mailboxes.userId, type: mailboxes.type, ownerName: users.name })
.select({
userId: mailboxes.userId,
type: mailboxes.type,
localPart: mailboxes.localPart,
hostname: domains.hostname,
ownerName: users.name,
ownerEmail: users.email,
})
.from(mailboxes)
.innerJoin(domains, eq(mailboxes.domainId, domains.id))
.innerJoin(users, eq(mailboxes.userId, users.id))
.where(eq(mailboxes.id, id))
.limit(1);
if (!mailbox) return NextResponse.json({ error: "Mailbox not found" }, { status: 404 });
const key = mailbox.type === "personal" ? avatarKeyFor(mailbox.userId) : mailboxAvatarKeyFor(id);
// The primary mailbox shares the account avatar; every other mailbox stores its own.
const identity = tracksAccountIdentity(mailbox, mailbox.ownerEmail);
const key = identity ? avatarKeyFor(mailbox.userId) : mailboxAvatarKeyFor(id);
await env.BUCKET.put(key, await file.arrayBuffer(), {
httpMetadata: { contentType: file.type },
});
if (mailbox.type === "personal") {
if (identity) {
await syncPersonalIdentity(db, {
userId: mailbox.userId,
name: mailbox.ownerName,
+12 -9
View File
@@ -6,6 +6,7 @@ import { requireUser } from "@/lib/auth/cookies";
import { getEnv } from "@/lib/cloudflare";
import { getMailboxAccessLevel } from "@/lib/mailboxes/access";
import { ensureMailboxDomainRouting, removeMailboxDomainRouting } from "@/lib/mailboxes/domain-addresses";
import { isPrimaryMailbox, tracksAccountIdentity } from "@/lib/profile/identity-utils";
import { syncPersonalIdentity } from "@/lib/profile/sync";
import { updateMailboxSchema } from "@/lib/validators";
import type { MailboxRouteParams } from "./types";
@@ -26,15 +27,15 @@ export async function GET(request: Request, { params }: MailboxRouteParams) {
return NextResponse.json({ error: "Mailbox not found" }, { status: 404 });
}
const { avatarKey, ownerName, ownerAvatarKey, ...mailboxDetails } = mailbox;
const personal = mailbox.type === "personal";
const identity = tracksAccountIdentity(mailbox, user.email);
return NextResponse.json({
mailbox: {
...mailboxDetails,
displayName: personal ? ownerName : mailbox.displayName,
hasAvatar: personal ? !!ownerAvatarKey : !!avatarKey,
displayName: identity ? ownerName : mailbox.displayName,
hasAvatar: identity ? !!ownerAvatarKey : !!avatarKey,
permission: access.permission,
isPrimary: `${mailbox.localPart}@${mailbox.hostname}` === user.email,
isPrimary: isPrimaryMailbox(mailbox, user.email),
},
});
}
@@ -58,7 +59,9 @@ export async function PATCH(request: Request, { params }: MailboxRouteParams) {
}
const updateValues = getMailboxUpdateValues(parsed.data);
if (existing.type === "personal" && "displayName" in parsed.data) {
// Only the primary mailbox carries the account name; renaming any other
// mailbox writes to that mailbox alone.
if (tracksAccountIdentity(existing, user.email) && "displayName" in parsed.data) {
const name = parsed.data.displayName?.trim();
if (!name) {
return NextResponse.json({ error: "A valid account name is required" }, { status: 400 });
@@ -95,15 +98,15 @@ export async function PATCH(request: Request, { params }: MailboxRouteParams) {
const [mailbox] = await selectMailboxForUser(db, user.id, id);
const { avatarKey, ownerName, ownerAvatarKey, ...mailboxDetails } = mailbox!;
const personal = mailbox!.type === "personal";
const identity = tracksAccountIdentity(mailbox!, user.email);
return NextResponse.json({
mailbox: {
...mailboxDetails,
displayName: personal ? ownerName : mailbox!.displayName,
hasAvatar: personal ? !!ownerAvatarKey : !!avatarKey,
displayName: identity ? ownerName : mailbox!.displayName,
hasAvatar: identity ? !!ownerAvatarKey : !!avatarKey,
permission: access.permission,
isPrimary: `${mailbox!.localPart}@${mailbox!.hostname}` === user.email,
isPrimary: isPrimaryMailbox(mailbox!, user.email),
},
});
}
+2 -1
View File
@@ -6,6 +6,7 @@ import { domains, mailboxAliases, mailboxes, users } from "@/db/schema";
import { requireUser } from "@/lib/auth/cookies";
import { newId } from "@/lib/ids";
import { getLicenseEntitlements } from "@/lib/licenses/service";
import { tracksAccountIdentity } from "@/lib/profile/identity-utils";
import { mailboxSchema } from "@/lib/validators";
import { ensureMailboxDomainRouting, getMailboxDomainAddresses } from "@/lib/mailboxes/domain-addresses";
import { ensurePersonalMailbox } from "./utils";
@@ -19,7 +20,7 @@ export async function GET(request: Request) {
return NextResponse.json({
mailboxes: await Promise.all(rows.map(async (mailbox) => ({
...mailbox,
...(mailbox.userId === user.id && mailbox.type === "personal"
...(mailbox.userId === user.id && tracksAccountIdentity(mailbox, user.email)
? { displayName: user.name, hasAvatar: !!user.avatarKey }
: {}),
senderAddresses: await getMailboxDomainAddresses(db, mailbox),
+2 -1
View File
@@ -9,6 +9,7 @@ import { getContactDisplayNameMap } from "@/lib/contacts/service";
import { getFirstEmailAddressEntry, normalizeEmailAddress } from "@/lib/email/address";
import { buildSnippet } from "@/lib/email/parse";
import { getMailboxAccessLevel, listAccessibleMailboxes } from "@/lib/mailboxes/access";
import { tracksAccountIdentity } from "@/lib/profile/identity-utils";
export async function GET(request: Request) {
const env = getEnv();
@@ -175,7 +176,7 @@ export async function GET(request: Request) {
const mailboxNameMap = new Map(
accessibleMailboxes.map((mailbox) => [
mailbox.id,
mailbox.userId === user.id && mailbox.type === "personal"
mailbox.userId === user.id && tracksAccountIdentity(mailbox, user.email)
? user.name
: mailbox.displayName ?? mailbox.localPart,
]),
+8
View File
@@ -8,6 +8,14 @@ let mailboxesRequestSessionToken: string | null = null;
let cacheGeneration = 0;
export const SELECTED_MAILBOX_STORAGE_KEY = "selected-mailbox-id";
/**
* The primary mailbox is the one at the account address; it is the only mailbox
* whose name and avatar follow the profile.
*/
export function isIdentityMailbox(mailbox: Pick<MailboxOption, "type" | "isPrimary">): boolean {
return mailbox.type === "personal" && !!mailbox.isPrimary;
}
export function clearMailboxesCache() {
cacheGeneration += 1;
mailboxesCache = null;
+6 -4
View File
@@ -11,6 +11,7 @@ import type { ReactNode } from "react";
import {
clearMailboxesCache,
fetchMailboxOptions,
isIdentityMailbox,
SELECTED_MAILBOX_STORAGE_KEY,
} from "./mailbox-provider-utils";
import {
@@ -107,11 +108,12 @@ export function MailboxProvider({ children }: { children: ReactNode }) {
function updatePersonalMailboxNames(event: Event) {
const { name } = (event as CustomEvent<ProfileNameChangedDetail>).detail;
clearMailboxesCache();
// Only the primary mailbox follows the profile name; the rest keep their own.
setMailboxes((items) => items.map((mailbox) => (
mailbox.type === "personal" ? { ...mailbox, displayName: name } : mailbox
isIdentityMailbox(mailbox) ? { ...mailbox, displayName: name } : mailbox
)));
setSelectedMailboxState((mailbox) => (
mailbox?.type === "personal" ? { ...mailbox, displayName: name } : mailbox
mailbox && isIdentityMailbox(mailbox) ? { ...mailbox, displayName: name } : mailbox
));
}
@@ -123,10 +125,10 @@ export function MailboxProvider({ children }: { children: ReactNode }) {
function updatePersonalMailboxAvatars() {
clearMailboxesCache();
setMailboxes((items) => items.map((mailbox) => (
mailbox.type === "personal" ? { ...mailbox, hasAvatar: true } : mailbox
isIdentityMailbox(mailbox) ? { ...mailbox, hasAvatar: true } : mailbox
)));
setSelectedMailboxState((mailbox) => (
mailbox?.type === "personal" ? { ...mailbox, hasAvatar: true } : mailbox
mailbox && isIdentityMailbox(mailbox) ? { ...mailbox, hasAvatar: true } : mailbox
));
}
+2 -1
View File
@@ -5,6 +5,7 @@ import { usePathname, useRouter } from "next/navigation";
import { CalendarDays, Check, LogOut, Settings, ShieldCheck, UserRound, UsersRound } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { useSelectedMailbox } from "@/components/mailbox-provider";
import { isIdentityMailbox } from "@/components/mailbox-provider-utils";
import { useMessageCounts } from "@/hooks/use-message-counts";
import { authFetch } from "@/lib/auth/client";
import { logoutClientSession } from "@/lib/auth/logout";
@@ -146,7 +147,7 @@ export function MailboxSelector() {
const next = { ...current };
const version = Date.now();
for (const mailbox of mailboxes) {
if (mailbox.type === "personal") {
if (isIdentityMailbox(mailbox)) {
next[mailbox.id] = `/api/mailboxes/${mailbox.id}/avatar?v=${version}`;
}
}
+5 -1
View File
@@ -4,6 +4,7 @@ import { domains, mailboxes, users } from "@/db/schema";
import { getMailboxAccessLevel } from "@/lib/mailboxes/access";
import { formatEmailAddress, getEmailAddress } from "@/lib/email/address";
import { getMailboxDomainAddresses } from "@/lib/mailboxes/domain-addresses";
import { resolveMailboxDisplayName } from "@/lib/profile/identity-utils";
export async function getAuthorizedSenderAddress(
env: CloudflareEnv,
@@ -22,6 +23,7 @@ export async function getAuthorizedSenderAddress(
displayName: mailboxes.displayName,
type: mailboxes.type,
ownerName: users.name,
ownerEmail: users.email,
hostname: domains.hostname,
domainId: mailboxes.domainId,
useAllDomains: mailboxes.useAllDomains,
@@ -48,7 +50,9 @@ export async function getAuthorizedSenderAddress(
throw new Error("Sender address does not match the selected mailbox");
}
const senderAddress = requestedAddress.toLowerCase();
const senderName = mailbox.type === "personal" ? mailbox.ownerName : mailbox.displayName;
// The primary mailbox sends under the account name; every other mailbox
// sends under its own.
const senderName = resolveMailboxDisplayName(mailbox, mailbox.ownerEmail, mailbox.ownerName);
if (access.canSendAs) {
return {
+49
View File
@@ -0,0 +1,49 @@
/**
* A user's primary mailbox is the one at their account address. It carries the
* account identity: its name and avatar are the profile's, and editing either
* side keeps both in step. Every other personal mailbox owns its own display
* name and avatar, so renaming one never touches its siblings.
*/
export type MailboxIdentityScope = {
localPart: string;
hostname: string;
type?: string | null;
};
export function mailboxAddress(mailbox: Pick<MailboxIdentityScope, "localPart" | "hostname">): string {
return `${mailbox.localPart}@${mailbox.hostname}`.trim().toLowerCase();
}
export function isPrimaryMailbox(
mailbox: Pick<MailboxIdentityScope, "localPart" | "hostname">,
accountEmail: string | null | undefined,
): boolean {
if (!accountEmail) return false;
return mailboxAddress(mailbox) === accountEmail.trim().toLowerCase();
}
/** True when the mailbox's name and avatar come from the account profile. */
export function tracksAccountIdentity(
mailbox: MailboxIdentityScope,
accountEmail: string | null | undefined,
): boolean {
return mailbox.type === "personal" && isPrimaryMailbox(mailbox, accountEmail);
}
/** The name to show for a mailbox, before falling back to its local part. */
export function resolveMailboxDisplayName(
mailbox: MailboxIdentityScope & { displayName: string | null },
accountEmail: string | null | undefined,
accountName: string | null,
): string | null {
return tracksAccountIdentity(mailbox, accountEmail) ? accountName : mailbox.displayName;
}
/** The avatar key to serve for a mailbox. */
export function resolveMailboxAvatarKey(
mailbox: MailboxIdentityScope & { avatarKey: string | null },
accountEmail: string | null | undefined,
accountAvatarKey: string | null,
): string | null {
return tracksAccountIdentity(mailbox, accountEmail) ? accountAvatarKey : mailbox.avatarKey;
}
+44 -29
View File
@@ -1,8 +1,9 @@
import { and, eq } from "drizzle-orm";
import type { AppDatabase } from "@/db";
import { contacts, mailboxes, users } from "@/db/schema";
import { contacts, domains, mailboxes, users } from "@/db/schema";
import { getContactId } from "@/lib/contacts/utils";
import { getMailboxDomainAddresses } from "@/lib/mailboxes/domain-addresses";
import { tracksAccountIdentity } from "./identity-utils";
import type { PersonalIdentity } from "./sync-types";
export async function syncPersonalIdentity(
@@ -14,40 +15,54 @@ export async function syncPersonalIdentity(
.set({ name: identity.name, avatarKey: identity.avatarKey })
.where(eq(users.id, identity.userId));
await db
.update(mailboxes)
.set({ displayName: identity.name, avatarKey: identity.avatarKey })
.where(and(eq(mailboxes.userId, identity.userId), eq(mailboxes.type, "personal")));
const [account] = await db
.select({ email: users.email })
.from(users)
.where(eq(users.id, identity.userId))
.limit(1);
const personalMailboxes = await db
.select()
.select({ mailbox: mailboxes, hostname: domains.hostname })
.from(mailboxes)
.innerJoin(domains, eq(mailboxes.domainId, domains.id))
.where(and(eq(mailboxes.userId, identity.userId), eq(mailboxes.type, "personal")));
const addresses = new Set(
(await Promise.all(
personalMailboxes.map((mailbox) => getMailboxDomainAddresses(db, mailbox)),
)).flat(),
);
for (const email of addresses) {
await db
.insert(contacts)
.values({
id: getContactId(identity.userId, email),
userId: identity.userId,
email,
displayName: identity.name,
avatarKey: identity.avatarKey,
source: "manual",
})
.onConflictDoUpdate({
target: [contacts.userId, contacts.email],
set: {
displayName: identity.name,
avatarKey: identity.avatarKey,
for (const { mailbox, hostname } of personalMailboxes) {
// Only the primary mailbox mirrors the profile. Secondary mailboxes keep
// the name and avatar they were given.
const isIdentityMailbox = tracksAccountIdentity(
{ localPart: mailbox.localPart, hostname, type: mailbox.type },
account?.email,
);
if (isIdentityMailbox) {
await db
.update(mailboxes)
.set({ displayName: identity.name, avatarKey: identity.avatarKey })
.where(eq(mailboxes.id, mailbox.id));
}
const displayName = isIdentityMailbox ? identity.name : mailbox.displayName ?? identity.name;
const avatarKey = isIdentityMailbox ? identity.avatarKey : mailbox.avatarKey;
for (const email of await getMailboxDomainAddresses(db, mailbox)) {
await db
.insert(contacts)
.values({
id: getContactId(identity.userId, email),
userId: identity.userId,
email,
displayName,
avatarKey,
source: "manual",
},
});
})
.onConflictDoUpdate({
target: [contacts.userId, contacts.email],
set: {
displayName,
avatarKey,
source: "manual",
},
});
}
}
}
+149
View File
@@ -0,0 +1,149 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { DatabaseSync } from "node:sqlite";
import test from "node:test";
import { fileURLToPath } from "node:url";
import {
isPrimaryMailbox,
resolveMailboxAvatarKey,
resolveMailboxDisplayName,
tracksAccountIdentity,
} from "../src/lib/profile/identity-utils.ts";
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const read = (path) => readFileSync(join(root, path), "utf8");
const ACCOUNT_EMAIL = "sam@example.com";
const primary = { localPart: "sam", hostname: "example.com", type: "personal", displayName: "Sam Reyes", avatarKey: "avatars/user-1" };
const secondary = { localPart: "billing", hostname: "example.com", type: "personal", displayName: "Billing", avatarKey: "avatars/mbx-2" };
const shared = { localPart: "support", hostname: "example.com", type: "shared", displayName: "Support", avatarKey: "avatars/mbx-3" };
test("only the mailbox at the account address carries the account identity", () => {
assert.equal(isPrimaryMailbox(primary, ACCOUNT_EMAIL), true);
assert.equal(isPrimaryMailbox(secondary, ACCOUNT_EMAIL), false);
assert.equal(tracksAccountIdentity(primary, ACCOUNT_EMAIL), true);
assert.equal(tracksAccountIdentity(secondary, ACCOUNT_EMAIL), false);
assert.equal(tracksAccountIdentity(shared, ACCOUNT_EMAIL), false);
});
test("the address match ignores case and surrounding space", () => {
assert.equal(isPrimaryMailbox({ localPart: "Sam", hostname: "Example.com" }, " SAM@example.com "), true);
assert.equal(isPrimaryMailbox(primary, null), false);
assert.equal(isPrimaryMailbox(primary, undefined), false);
});
test("a secondary mailbox resolves to its own name and avatar", () => {
assert.equal(resolveMailboxDisplayName(primary, ACCOUNT_EMAIL, "Sam Reyes"), "Sam Reyes");
assert.equal(resolveMailboxDisplayName(secondary, ACCOUNT_EMAIL, "Sam Reyes"), "Billing");
assert.equal(resolveMailboxAvatarKey(primary, ACCOUNT_EMAIL, "avatars/user-1"), "avatars/user-1");
assert.equal(resolveMailboxAvatarKey(secondary, ACCOUNT_EMAIL, "avatars/user-1"), "avatars/mbx-2");
});
test("renaming the profile leaves sibling mailboxes untouched (issue #36)", () => {
const db = new DatabaseSync(":memory:");
db.exec(`
CREATE TABLE users (id TEXT PRIMARY KEY, email TEXT NOT NULL, name TEXT, avatar_key TEXT);
CREATE TABLE domains (id TEXT PRIMARY KEY, hostname TEXT NOT NULL);
CREATE TABLE mailboxes (
id TEXT PRIMARY KEY, user_id TEXT NOT NULL, domain_id TEXT NOT NULL,
local_part TEXT NOT NULL, display_name TEXT, avatar_key TEXT, type TEXT NOT NULL
);
INSERT INTO users VALUES ('u1', 'sam@example.com', 'Sam Reyes', 'avatars/user-1');
INSERT INTO domains VALUES ('d1', 'example.com');
INSERT INTO mailboxes VALUES
('m1', 'u1', 'd1', 'sam', 'Sam Reyes', 'avatars/user-1', 'personal'),
('m2', 'u1', 'd1', 'billing', 'Billing', 'avatars/mbx-2', 'personal'),
('m3', 'u1', 'd1', 'projects', 'Projects', NULL, 'personal');
`);
// What syncPersonalIdentity does now: load every personal mailbox with its
// hostname, then write only to the one that carries the account identity.
const account = db.prepare("SELECT email FROM users WHERE id = ?").get("u1");
const rows = db
.prepare(`
SELECT m.id, m.local_part, m.type, d.hostname
FROM mailboxes m JOIN domains d ON d.id = m.domain_id
WHERE m.user_id = ? AND m.type = 'personal'
`)
.all("u1");
const identityRows = rows.filter((row) =>
tracksAccountIdentity({ localPart: row.local_part, hostname: row.hostname, type: row.type }, account.email),
);
assert.deepEqual(identityRows.map((row) => row.id), ["m1"], "exactly one mailbox holds the account identity");
for (const row of identityRows) {
db.prepare("UPDATE mailboxes SET display_name = ?, avatar_key = ? WHERE id = ?")
.run("Sam R. Reyes", "avatars/user-2", row.id);
}
const named = db
.prepare("SELECT id, display_name, avatar_key FROM mailboxes ORDER BY id")
.all()
.map((row) => ({ ...row }));
assert.deepEqual(named, [
{ id: "m1", display_name: "Sam R. Reyes", avatar_key: "avatars/user-2" },
{ id: "m2", display_name: "Billing", avatar_key: "avatars/mbx-2" },
{ id: "m3", display_name: "Projects", avatar_key: null },
]);
});
test("the identity write is keyed by mailbox id, not by owner and type", () => {
const sync = read("src/lib/profile/sync.ts");
const updates = [...sync.matchAll(/\.update\(mailboxes\)[\s\S]*?\.where\(([\s\S]*?)\);/g)].map((m) => m[1]);
assert.ok(updates.length > 0, "no mailbox update found in syncPersonalIdentity");
for (const where of updates) {
assert.ok(
/eq\(mailboxes\.id,/.test(where),
`mailbox identity update must target a single mailbox id, got: ${where.trim()}`,
);
assert.ok(
!/eq\(mailboxes\.type, "personal"\)/.test(where),
"an update scoped by type fans the account name out to every personal mailbox",
);
}
assert.match(sync, /tracksAccountIdentity/);
});
test("the mailbox routes gate the account name on the identity check", () => {
const detail = read("src/app/api/mailboxes/[id]/route.ts");
assert.match(detail, /tracksAccountIdentity\(existing, user\.email\) && "displayName" in parsed\.data/);
assert.ok(
!/existing\.type === "personal" && "displayName" in parsed\.data/.test(detail),
"PATCH must not treat every personal mailbox as the account identity",
);
const list = read("src/app/api/mailboxes/route.ts");
assert.match(list, /tracksAccountIdentity\(mailbox, user\.email\)/);
assert.ok(
!/mailbox\.type === "personal"\n?\s*\? \{ displayName: user\.name/.test(list),
"the list response must not overwrite every personal mailbox name with the account name",
);
});
test("outgoing mail and message lists use the mailbox's own name", () => {
const sender = read("src/lib/email/sender.ts");
assert.match(sender, /resolveMailboxDisplayName\(mailbox, mailbox\.ownerEmail, mailbox\.ownerName\)/);
assert.ok(
!/mailbox\.type === "personal" \? mailbox\.ownerName/.test(sender),
"every personal mailbox would send under the account owner's name",
);
const messages = read("src/app/api/messages/route.ts");
assert.match(messages, /tracksAccountIdentity\(mailbox, user\.email\)/);
});
test("the client only repaints the primary mailbox on profile changes", () => {
const provider = read("src/components/mailbox-provider.tsx");
assert.match(provider, /isIdentityMailbox\(mailbox\)/);
assert.ok(
!/mailbox\.type === "personal" \? \{ \.\.\.mailbox, displayName: name \}/.test(provider),
"a profile rename must not relabel every personal mailbox in the sidebar",
);
const utils = read("src/components/mailbox-provider-utils.ts");
assert.match(utils, /export function isIdentityMailbox/);
});