Files
webmail/lib/demo/demo-data.ts
T
Linus Rath 4c0017d0c1 feat: rework the demo around one persona with two accounts and a shared mailbox
The demo user becomes Nora Lindqvist, engineering lead at Meridian Labs,
in the week of a product launch. The fixtures tell one story across mail,
contacts, calendar, tasks and files, and exercise the features the
screenshots and the public demo should show:

- a second, mail-only login (her personal domain) and the shared Support
  mailbox delegated to the work account, so the account switcher, the
  shared section and the merged inbox have real content
- raw headers the way Stalwart writes them: the sender check fails on
  the phishing mail, newsletters carry a one-click unsubscribe, one mail
  asks for a read receipt, and spam scores show in the details
- an iMIP invitation with a served .ics, an event waiting for the answer
  on the calendar, and RSVP patches applied in the demo client
- @mentions as mailto links in a received mail, a verification code
  twelve minutes old, nested folders, named tags on the mail, contact
  groups, a scheduled message and tasks switched on
- every attachment and file backed by real bytes under public/demo,
  generated by `scripts/demo-assets/generate.mjs`; contact photos are
  bundled portraits instead of requests to randomuser.me
- folder counts computed from the mail instead of hand-kept numbers

Demo login seeds the tags, templates and merged-inbox settings once per
device and switches the cross-account gate on for the session; "Reset"
restores them. `bulwark-demo-simulate=off` in localStorage stops the
simulated incoming mail for screenshot runs.
2026-10-18 19:48:11 +02:00

119 lines
5.5 KiB
TypeScript

import { cloneFixtures } from './demo-utils';
import { createDemoMailboxes, createDemoSupportMailboxes, createDemoPersonalMailboxes, SUPPORT_ACCOUNT_ID, SUPPORT_ACCOUNT_NAME } from './fixtures/mailboxes';
import { createDemoEmails } from './fixtures/emails';
import { createDemoSupportEmails } from './fixtures/support';
import { createDemoPersonalEmails, createDemoPersonalIdentities } from './fixtures/personal';
import { createDemoContacts, createDemoAddressBooks } from './fixtures/contacts';
import { createDemoCalendars, createDemoCalendarEvents } from './fixtures/calendars';
import { createDemoCalendarTasks } from './fixtures/tasks';
import { createDemoIdentities } from './fixtures/identities';
import { createDemoSieveScripts, createDemoSieveCapabilities, createDemoSieveContent } from './fixtures/filters';
import { createDemoFileNodes } from './fixtures/files';
import { createDemoVacationResponse } from './fixtures/vacation';
import { NORA, NORA_PERSONAL } from './fixtures/people';
import type { Email, EmailAddress, Mailbox, ContactCard, AddressBook, Calendar, CalendarEvent, CalendarTask, Identity, VacationResponse, FileNode, SharedAccount } from '@/lib/jmap/types';
import type { SieveScript, SieveCapabilities } from '@/lib/jmap/sieve-types';
/**
* The demo signs in to two accounts. "work" is the primary login with mail,
* contacts, calendars, files and filters, plus the shared Support mailbox
* delegated to it. "personal" is a mail-only second login, so the account
* switcher and the merged inbox have more than one source.
*/
export type DemoProfile = 'work' | 'personal';
export interface DemoData {
profile: DemoProfile;
/** JMAP account id of the login's primary account. */
accountId: string;
username: string;
/** The persona's own address in this account, used as From on new mail. */
self: EmailAddress;
mailboxes: Mailbox[];
/** Mailboxes of accounts shared with this login, keyed by their owner. */
sharedMailboxes: Record<string, { name: string; mailboxes: Mailbox[] }>;
emails: Email[];
contacts: ContactCard[];
addressBooks: AddressBook[];
calendars: Calendar[];
calendarEvents: CalendarEvent[];
calendarTasks: CalendarTask[];
identities: Identity[];
sieveScripts: SieveScript[];
sieveCapabilities: SieveCapabilities;
sieveContent: Record<string, string>;
fileNodes: FileNode[];
vacationResponse: VacationResponse;
quota: { used: number; total: number };
/** Ids of drafts that are scheduled to go out, with their send time. */
scheduled: Array<{ emailId: string; identityId: string; sendAt: string }>;
}
export const DEMO_WORK_ACCOUNT_ID = 'demo-account';
export const DEMO_PERSONAL_ACCOUNT_ID = 'demo-personal';
/** What `getSharedAccounts()` reports for a profile. */
export function demoSharedAccounts(profile: DemoProfile): SharedAccount[] {
if (profile === 'personal') {
return [{ id: DEMO_PERSONAL_ACCOUNT_ID, name: 'Personal', isPrimary: true, capabilities: { mail: true, sieve: false, calendars: false, contacts: false, filenode: false } }];
}
return [
{ id: DEMO_WORK_ACCOUNT_ID, name: 'Meridian Labs', isPrimary: true, capabilities: { mail: true, sieve: true, calendars: true, contacts: true, filenode: true } },
{ id: SUPPORT_ACCOUNT_ID, name: SUPPORT_ACCOUNT_NAME, isPrimary: false, capabilities: { mail: true, sieve: false, calendars: false, contacts: false, filenode: false } },
];
}
/** Return a fresh deep-cloned copy of all demo data for one profile. */
export function getDemoData(profile: DemoProfile = 'work'): DemoData {
if (profile === 'personal') {
return cloneFixtures({
profile,
accountId: DEMO_PERSONAL_ACCOUNT_ID,
username: NORA_PERSONAL.email,
self: { name: NORA_PERSONAL.name, email: NORA_PERSONAL.email },
mailboxes: createDemoPersonalMailboxes(),
sharedMailboxes: {},
emails: createDemoPersonalEmails(),
contacts: [],
addressBooks: [],
calendars: [],
calendarEvents: [],
calendarTasks: [],
identities: createDemoPersonalIdentities(),
sieveScripts: [],
sieveCapabilities: createDemoSieveCapabilities(),
sieveContent: {},
fileNodes: [],
vacationResponse: { id: 'singleton', isEnabled: false, fromDate: null, toDate: null, subject: '', textBody: '', htmlBody: null },
quota: { used: 1_814_000_000, total: 10_737_418_240 },
scheduled: [],
});
}
const scheduledAt = new Date();
scheduledAt.setHours(12, 0, 0, 0);
if (scheduledAt.getTime() < Date.now()) scheduledAt.setDate(scheduledAt.getDate() + 1);
return cloneFixtures({
profile,
accountId: DEMO_WORK_ACCOUNT_ID,
username: NORA.email,
self: { name: NORA.name, email: NORA.email },
mailboxes: createDemoMailboxes(),
sharedMailboxes: { [SUPPORT_ACCOUNT_ID]: { name: SUPPORT_ACCOUNT_NAME, mailboxes: createDemoSupportMailboxes() } },
emails: [...createDemoEmails(), ...createDemoSupportEmails()],
contacts: createDemoContacts(),
addressBooks: createDemoAddressBooks(),
calendars: createDemoCalendars(),
calendarEvents: createDemoCalendarEvents(),
calendarTasks: createDemoCalendarTasks(),
identities: createDemoIdentities(),
sieveScripts: createDemoSieveScripts(),
sieveCapabilities: createDemoSieveCapabilities(),
sieveContent: createDemoSieveContent(),
fileNodes: createDemoFileNodes(),
vacationResponse: createDemoVacationResponse(),
quota: { used: 6_442_450_944, total: 21_474_836_480 },
scheduled: [{ emailId: 'demo-email-scheduled-prep', identityId: 'demo-identity-primary', sendAt: scheduledAt.toISOString() }],
});
}