mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
fix: show replies sent in demo mode in the conversation they answer
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { DemoJMAPClient } from '@/lib/demo/demo-client';
|
||||
import { computeReplyThreadingHeaders } from '@/lib/email-threading';
|
||||
|
||||
// A server threads a reply by In-Reply-To and References. The demo gave every
|
||||
// sent message a thread of its own, so a (quick) reply said "sent" but never
|
||||
// showed up in the conversation it answered.
|
||||
describe('DemoJMAPClient reply threading', () => {
|
||||
async function inboxMessage(client: DemoJMAPClient) {
|
||||
const inbox = (await client.getMailboxes()).find((m) => m.role === 'inbox')!;
|
||||
const { emails } = await client.getEmails(inbox.id);
|
||||
return emails.find((e) => e.messageId)!;
|
||||
}
|
||||
|
||||
it('puts a sent reply into the thread of the message it answers', async () => {
|
||||
const client = new DemoJMAPClient();
|
||||
const original = await inboxMessage(client);
|
||||
const threading = computeReplyThreadingHeaders(original)!;
|
||||
|
||||
const { emailId } = await client.sendEmail(
|
||||
[original.from![0].email], `Re: ${original.subject}`, 'Thanks!',
|
||||
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
|
||||
threading.inReplyTo, threading.references,
|
||||
);
|
||||
|
||||
const sent = await client.getEmail(emailId!);
|
||||
expect(sent?.threadId).toBe(original.threadId);
|
||||
expect((await client.getThreadEmails(original.threadId)).map((e) => e.id)).toContain(emailId);
|
||||
});
|
||||
|
||||
it('keeps a saved reply draft in the thread too', async () => {
|
||||
const client = new DemoJMAPClient();
|
||||
const original = await inboxMessage(client);
|
||||
const threading = computeReplyThreadingHeaders(original)!;
|
||||
|
||||
const draftId = await client.createDraft(
|
||||
[original.from![0].email], `Re: ${original.subject}`, 'Draft',
|
||||
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
|
||||
threading.inReplyTo, threading.references,
|
||||
);
|
||||
|
||||
const draft = await client.getEmail(draftId);
|
||||
expect(draft?.threadId).toBe(original.threadId);
|
||||
expect(draft?.inReplyTo).toEqual(threading.inReplyTo);
|
||||
});
|
||||
|
||||
it('starts a new thread for a message that answers nothing', async () => {
|
||||
const client = new DemoJMAPClient();
|
||||
const original = await inboxMessage(client);
|
||||
|
||||
const { emailId } = await client.sendEmail(['a@example.com'], original.subject ?? 'Hello', 'New message');
|
||||
|
||||
const sent = await client.getEmail(emailId!);
|
||||
expect(sent?.threadId).toBeTruthy();
|
||||
expect(sent?.threadId).not.toBe(original.threadId);
|
||||
});
|
||||
});
|
||||
+20
-2
@@ -13,6 +13,7 @@ import { withBasePath } from '@/lib/browser-navigation';
|
||||
import { toInertBlob } from '@/lib/file-preview';
|
||||
import type { StorageQuota } from "@/lib/quota";
|
||||
import { buildDemoEml, demoEmlBlobId, demoEmlEmailId } from './demo-eml';
|
||||
import { stripMessageIdBrackets } from '@/lib/email-threading';
|
||||
|
||||
// Fixture blobs with real bytes behind them, served from public/demo/ (see
|
||||
// fixtures/assets.ts for the list and the licences). Any other blob id falls
|
||||
@@ -791,13 +792,15 @@ export class DemoJMAPClient implements IJMAPClient {
|
||||
attachments?: Array<{ blobId: string; name: string; type: string; size: number; disposition?: 'attachment' | 'inline'; cid?: string }>,
|
||||
_fromName?: string,
|
||||
htmlBody?: string,
|
||||
inReplyTo?: string[],
|
||||
references?: string[],
|
||||
): Promise<string> {
|
||||
const draftsMb = this.data.mailboxes.find(m => m.role === 'drafts');
|
||||
const id = draftId || generateDemoId('email');
|
||||
const existing = draftId ? this.data.emails.findIndex(e => e.id === draftId) : -1;
|
||||
|
||||
const email: Email = {
|
||||
id, threadId: generateDemoId('thread'),
|
||||
id, threadId: this.threadFor(inReplyTo, references),
|
||||
mailboxIds: { [draftsMb?.id || this.data.mailboxes[0].id]: true },
|
||||
keywords: { $seen: true, $draft: true },
|
||||
size: body.length,
|
||||
@@ -819,6 +822,8 @@ export class DemoJMAPClient implements IJMAPClient {
|
||||
: { '1': { value: body } },
|
||||
attachments: attachments?.map(a => ({ ...a, partId: generateDemoId('part') })),
|
||||
messageId: `<${id}@demo.example.com>`,
|
||||
inReplyTo: inReplyTo?.length ? inReplyTo : undefined,
|
||||
references: references?.length ? references : undefined,
|
||||
};
|
||||
|
||||
if (existing >= 0) {
|
||||
@@ -853,7 +858,7 @@ export class DemoJMAPClient implements IJMAPClient {
|
||||
}
|
||||
const sentMb = this.data.mailboxes.find(m => m.role === 'sent');
|
||||
const email: Email = {
|
||||
id: generateDemoId('email'), threadId: generateDemoId('thread'),
|
||||
id: generateDemoId('email'), threadId: this.threadFor(inReplyTo, references),
|
||||
mailboxIds: { [delayedUntil ? (this.data.mailboxes.find(m => m.role === 'drafts')?.id || this.data.mailboxes[0].id) : (sentMb?.id || this.data.mailboxes[0].id)]: true },
|
||||
keywords: delayedUntil ? { $seen: true, $draft: true } : { $seen: true },
|
||||
size: body.length + (htmlBody?.length || 0),
|
||||
@@ -893,6 +898,19 @@ export class DemoJMAPClient implements IJMAPClient {
|
||||
: { scheduled: false, emailId: email.id };
|
||||
}
|
||||
|
||||
/**
|
||||
* The thread a new message joins. A server threads a reply by In-Reply-To
|
||||
* and References, so the reply shows up in the conversation it answers;
|
||||
* without a known parent the message starts a thread of its own.
|
||||
*/
|
||||
private threadFor(inReplyTo?: string[], references?: string[]): string {
|
||||
const wanted = new Set([...(inReplyTo ?? []), ...(references ?? [])].map(stripMessageIdBrackets));
|
||||
const parent = wanted.size > 0
|
||||
? this.data.emails.find(e => e.messageId && wanted.has(stripMessageIdBrackets(e.messageId)))
|
||||
: undefined;
|
||||
return parent?.threadId ?? generateDemoId('thread');
|
||||
}
|
||||
|
||||
/** The From address of new mail: the chosen identity, else the account's own address. */
|
||||
private fromAddress(identityId?: string, fromEmail?: string, fromName?: string): EmailAddress {
|
||||
const identity = identityId ? this.data.identities.find(i => i.id === identityId) : undefined;
|
||||
|
||||
Reference in New Issue
Block a user