feat: add demo data for emails, files, filters, identities, mailboxes, vacation responses, and JMAP client interface

- Created demo emails with various states (inbox, sent, drafts, trash, etc.) in `emails.ts`.
- Added demo file nodes representing directories and files in `files.ts`.
- Implemented demo Sieve capabilities and scripts in `filters.ts`.
- Defined demo identities for users in `identities.ts`.
- Established demo mailboxes with permissions and counts in `mailboxes.ts`.
- Created a demo vacation response in `vacation.ts`.
- Introduced a comprehensive JMAP client interface in `client-interface.ts` to standardize interactions with the JMAP API.
This commit is contained in:
Linus Rath
2026-03-21 01:38:42 +01:00
parent a8be40579e
commit 2547c10060
53 changed files with 4036 additions and 110 deletions
+274
View File
@@ -0,0 +1,274 @@
import type { Calendar, CalendarEvent } from '@/lib/jmap/types';
import { demoDate, demoISODate } from '../demo-utils';
export function createDemoCalendars(): Calendar[] {
return [
{
id: 'demo-calendar-personal',
name: 'Personal',
description: null,
color: '#3b82f6',
sortOrder: 1,
isSubscribed: true,
isVisible: true,
isDefault: true,
includeInAvailability: 'all',
defaultAlertsWithTime: null,
defaultAlertsWithoutTime: null,
timeZone: null,
shareWith: null,
myRights: { mayReadFreeBusy: true, mayReadItems: true, mayWriteAll: true, mayWriteOwn: true, mayUpdatePrivate: true, mayRSVP: true, mayAdmin: true, mayDelete: false },
},
{
id: 'demo-calendar-work',
name: 'Work',
description: null,
color: '#22c55e',
sortOrder: 2,
isSubscribed: true,
isVisible: true,
isDefault: false,
includeInAvailability: 'all',
defaultAlertsWithTime: null,
defaultAlertsWithoutTime: null,
timeZone: null,
shareWith: null,
myRights: { mayReadFreeBusy: true, mayReadItems: true, mayWriteAll: true, mayWriteOwn: true, mayUpdatePrivate: true, mayRSVP: true, mayAdmin: true, mayDelete: true },
},
{
id: 'demo-calendar-birthdays',
name: 'Birthdays',
description: null,
color: '#eab308',
sortOrder: 3,
isSubscribed: true,
isVisible: true,
isDefault: false,
includeInAvailability: 'none',
defaultAlertsWithTime: null,
defaultAlertsWithoutTime: null,
timeZone: null,
shareWith: null,
myRights: { mayReadFreeBusy: true, mayReadItems: true, mayWriteAll: true, mayWriteOwn: true, mayUpdatePrivate: true, mayRSVP: true, mayAdmin: true, mayDelete: true },
},
];
}
export function createDemoCalendarEvents(): CalendarEvent[] {
const baseEvent = {
'@type': 'Event' as const,
descriptionContentType: 'text/plain',
isDraft: false,
isOrigin: true,
sequence: 0,
status: 'confirmed' as const,
freeBusyStatus: 'busy' as const,
privacy: 'public' as const,
color: null,
keywords: null,
categories: null,
locale: null,
replyTo: null,
organizerCalendarAddress: null,
participants: null,
mayInviteSelf: false,
mayInviteOthers: false,
hideAttendees: false,
recurrenceId: null,
recurrenceIdTimeZone: null,
recurrenceRules: null,
recurrenceOverrides: null,
excludedRecurrenceRules: null,
useDefaultAlerts: true,
alerts: null,
locations: null,
virtualLocations: null,
links: null,
relatedTo: null,
};
return [
// ── Personal calendar ──────────────────────────────────────
{
...baseEvent,
id: 'demo-event-1',
calendarIds: { 'demo-calendar-personal': true },
uid: 'demo-event-1@example.com',
title: 'Dentist Appointment',
description: 'Regular checkup at Dr. Smith\'s office',
created: demoDate(-7),
updated: demoDate(-7),
start: demoISODate(2, 10, 0),
utcStart: demoDate(2, 10),
utcEnd: demoDate(2, 11),
duration: 'PT1H',
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
showWithoutTime: false,
locations: { loc1: { '@type': 'Location', name: 'Dr. Smith Dental Clinic', description: '123 Medical Plaza', locationTypes: null, coordinates: null, timeZone: null, links: null, relativeTo: null } },
},
{
...baseEvent,
id: 'demo-event-2',
calendarIds: { 'demo-calendar-personal': true },
uid: 'demo-event-2@example.com',
title: 'Birthday Party',
description: 'Emma\'s birthday celebration',
created: demoDate(-10),
updated: demoDate(-10),
start: demoISODate(5),
utcStart: demoDate(5),
utcEnd: demoDate(6),
duration: 'P1D',
timeZone: null,
showWithoutTime: true,
freeBusyStatus: 'free' as const,
},
{
...baseEvent,
id: 'demo-event-3',
calendarIds: { 'demo-calendar-personal': true },
uid: 'demo-event-3@example.com',
title: 'Weekend Trip',
description: 'Road trip to the mountains',
created: demoDate(-5),
updated: demoDate(-5),
start: demoISODate(8),
utcStart: demoDate(8),
utcEnd: demoDate(10),
duration: 'P2D',
timeZone: null,
showWithoutTime: true,
freeBusyStatus: 'busy' as const,
},
// ── Work calendar ──────────────────────────────────────────
{
...baseEvent,
id: 'demo-event-4',
calendarIds: { 'demo-calendar-work': true },
uid: 'demo-event-4@example.com',
title: 'Weekly Standup',
description: 'Team sync-up meeting',
created: demoDate(-30),
updated: demoDate(-1),
start: demoISODate(1, 9, 30),
utcStart: demoDate(1, 9, 30),
utcEnd: demoDate(1, 10, 0),
duration: 'PT30M',
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
showWithoutTime: false,
recurrenceRules: [{
'@type': 'RecurrenceRule',
frequency: 'weekly',
interval: 1,
rscale: 'gregorian',
skip: 'omit',
firstDayOfWeek: 'mo',
byDay: [{ day: 'mo' }],
byMonthDay: null,
byMonth: null,
byYearDay: null,
byWeekNo: null,
byHour: null,
byMinute: null,
bySecond: null,
bySetPosition: null,
count: null,
until: null,
}],
virtualLocations: { vl1: { '@type': 'VirtualLocation', name: 'Zoom', uri: 'https://zoom.example/123456', description: 'Weekly standup room', features: null } },
},
{
...baseEvent,
id: 'demo-event-5',
calendarIds: { 'demo-calendar-work': true },
uid: 'demo-event-5@example.com',
title: 'Quarterly Review',
description: 'Q4 performance review and planning session',
created: demoDate(-14),
updated: demoDate(-3),
start: demoISODate(4, 14, 0),
utcStart: demoDate(4, 14),
utcEnd: demoDate(4, 16),
duration: 'PT2H',
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
showWithoutTime: false,
participants: {
p1: {
'@type': 'Participant', name: 'Demo User', email: 'demo@example.com', calendarAddress: null, description: null, sendTo: null,
kind: 'individual', roles: { attendee: true }, participationStatus: 'accepted', participationComment: null,
expectReply: false, scheduleAgent: 'server', scheduleForceSend: false, scheduleId: null, scheduleSequence: 0,
scheduleStatus: null, scheduleUpdated: null, invitedBy: null, delegatedTo: null, delegatedFrom: null, memberOf: null,
locationId: null, language: null, links: null,
},
p2: {
'@type': 'Participant', name: 'Alice Johnson', email: 'alice.johnson@example.com', calendarAddress: null, description: null, sendTo: null,
kind: 'individual', roles: { owner: true }, participationStatus: 'accepted', participationComment: null,
expectReply: false, scheduleAgent: 'server', scheduleForceSend: false, scheduleId: null, scheduleSequence: 0,
scheduleStatus: null, scheduleUpdated: null, invitedBy: null, delegatedTo: null, delegatedFrom: null, memberOf: null,
locationId: null, language: null, links: null,
},
p3: {
'@type': 'Participant', name: 'Bob Chen', email: 'bob.chen@example.com', calendarAddress: null, description: null, sendTo: null,
kind: 'individual', roles: { attendee: true }, participationStatus: 'tentative', participationComment: null,
expectReply: true, scheduleAgent: 'server', scheduleForceSend: false, scheduleId: null, scheduleSequence: 0,
scheduleStatus: null, scheduleUpdated: null, invitedBy: null, delegatedTo: null, delegatedFrom: null, memberOf: null,
locationId: null, language: null, links: null,
},
},
},
{
...baseEvent,
id: 'demo-event-6',
calendarIds: { 'demo-calendar-work': true },
uid: 'demo-event-6@example.com',
title: 'Lunch Meeting with Sarah',
description: 'Design review over lunch',
created: demoDate(-3),
updated: demoDate(-3),
start: demoISODate(3, 12, 0),
utcStart: demoDate(3, 12),
utcEnd: demoDate(3, 13),
duration: 'PT1H',
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
showWithoutTime: false,
locations: { loc1: { '@type': 'Location', name: 'The Garden Bistro', description: '123 Oak Street', locationTypes: null, coordinates: null, timeZone: null, links: null, relativeTo: null } },
},
// ── Birthdays calendar ─────────────────────────────────────
{
...baseEvent,
id: 'demo-event-7',
calendarIds: { 'demo-calendar-birthdays': true },
uid: 'demo-event-7@example.com',
title: 'Alice Johnson\'s Birthday',
description: '',
created: demoDate(-30),
updated: demoDate(-30),
start: demoISODate(12),
utcStart: demoDate(12),
utcEnd: demoDate(13),
duration: 'P1D',
timeZone: null,
showWithoutTime: true,
freeBusyStatus: 'free' as const,
},
{
...baseEvent,
id: 'demo-event-8',
calendarIds: { 'demo-calendar-birthdays': true },
uid: 'demo-event-8@example.com',
title: 'Carlos Rivera\'s Birthday',
description: '',
created: demoDate(-30),
updated: demoDate(-30),
start: demoISODate(-3),
utcStart: demoDate(-3),
utcEnd: demoDate(-2),
duration: 'P1D',
timeZone: null,
showWithoutTime: true,
freeBusyStatus: 'free' as const,
},
];
}
+194
View File
@@ -0,0 +1,194 @@
import type { ContactCard, AddressBook } from '@/lib/jmap/types';
export function createDemoAddressBooks(): AddressBook[] {
return [
{
id: 'demo-addressbook-personal',
name: 'Personal',
isDefault: true,
isSubscribed: true,
sortOrder: 1,
myRights: { mayRead: true, mayWrite: true, mayShare: true, mayDelete: false },
},
{
id: 'demo-addressbook-work',
name: 'Work',
isDefault: false,
isSubscribed: true,
sortOrder: 2,
myRights: { mayRead: true, mayWrite: true, mayShare: true, mayDelete: true },
},
];
}
export function createDemoContacts(): ContactCard[] {
return [
// ── Personal address book ──────────────────────────────────
{
id: 'demo-contact-1',
addressBookIds: { 'demo-addressbook-personal': true },
kind: 'individual',
name: { components: [{ kind: 'given', value: 'Alice' }, { kind: 'surname', value: 'Johnson' }] },
emails: { e1: { address: 'alice.johnson@example.com', contexts: { work: true }, pref: 1 } },
phones: { p1: { number: '+1-555-0101', features: { voice: true }, contexts: { work: true } } },
organizations: { o1: { name: 'Acme Corp', units: [{ name: 'Engineering' }] } },
titles: { t1: { name: 'Senior Engineer', kind: 'title' } },
anniversaries: { a1: { kind: 'birth', date: { year: 1990, month: 3, day: 15 } } },
},
{
id: 'demo-contact-2',
addressBookIds: { 'demo-addressbook-personal': true },
kind: 'individual',
name: { components: [{ kind: 'given', value: 'Bob' }, { kind: 'surname', value: 'Chen' }] },
emails: {
e1: { address: 'bob.chen@example.com', contexts: { work: true }, pref: 1 },
e2: { address: 'bob.personal@email.example', contexts: { private: true } },
},
phones: {
p1: { number: '+1-555-0102', features: { voice: true }, contexts: { work: true } },
p2: { number: '+1-555-0103', features: { cell: true }, contexts: { private: true } },
},
organizations: { o1: { name: 'Acme Corp', units: [{ name: 'Backend Team' }] } },
titles: { t1: { name: 'Staff Engineer', kind: 'title' } },
},
{
id: 'demo-contact-3',
addressBookIds: { 'demo-addressbook-personal': true },
kind: 'individual',
name: { components: [{ kind: 'given', value: 'Sarah' }, { kind: 'surname', value: 'Kim' }] },
emails: { e1: { address: 'sarah.kim@example.com', pref: 1 } },
phones: { p1: { number: '+1-555-0104', features: { voice: true } } },
organizations: { o1: { name: 'DesignCo' } },
titles: { t1: { name: 'UX Designer', kind: 'title' } },
},
{
id: 'demo-contact-4',
addressBookIds: { 'demo-addressbook-personal': true },
kind: 'individual',
name: { components: [{ kind: 'given', value: 'Carlos' }, { kind: 'surname', value: 'Rivera' }] },
emails: { e1: { address: 'carlos.rivera@example.com', pref: 1 } },
phones: { p1: { number: '+1-555-0105', features: { cell: true } } },
notes: { n1: { note: 'Met at the DevConf 2024 conference' } },
},
{
id: 'demo-contact-5',
addressBookIds: { 'demo-addressbook-personal': true },
kind: 'individual',
name: { components: [{ kind: 'given', value: 'Emma' }, { kind: 'surname', value: 'Wilson' }] },
emails: { e1: { address: 'emma.wilson@example.com', pref: 1 } },
addresses: {
a1: {
components: [
{ kind: 'number', value: '456' },
{ kind: 'name', value: 'Elm Street' },
{ kind: 'locality', value: 'Springfield' },
{ kind: 'region', value: 'IL' },
{ kind: 'postcode', value: '62701' },
],
contexts: { private: true },
},
},
anniversaries: { a1: { kind: 'birth', date: { month: 7, day: 22 } } },
},
{
id: 'demo-contact-6',
addressBookIds: { 'demo-addressbook-personal': true },
kind: 'individual',
name: { components: [{ kind: 'given', value: 'David' }, { kind: 'surname', value: 'Park' }] },
emails: { e1: { address: 'david.park@example.com', pref: 1 } },
phones: { p1: { number: '+82-10-1234-5678', features: { cell: true } } },
},
{
id: 'demo-contact-7',
addressBookIds: { 'demo-addressbook-personal': true },
kind: 'org',
name: { components: [{ kind: 'surname', value: 'Local Coffee Shop' }] },
emails: { e1: { address: 'hello@localcoffee.example', pref: 1 } },
phones: { p1: { number: '+1-555-0200', features: { voice: true } } },
addresses: {
a1: {
components: [
{ kind: 'number', value: '789' },
{ kind: 'name', value: 'Main Street' },
{ kind: 'locality', value: 'Anytown' },
{ kind: 'region', value: 'CA' },
{ kind: 'postcode', value: '90210' },
],
},
},
},
{
id: 'demo-contact-8',
addressBookIds: { 'demo-addressbook-personal': true },
kind: 'individual',
name: { components: [{ kind: 'given', value: 'Lisa' }, { kind: 'surname', value: 'Tanaka' }] },
emails: { e1: { address: 'lisa.tanaka@example.com', pref: 1 } },
},
// ── Work address book ──────────────────────────────────────
{
id: 'demo-contact-9',
addressBookIds: { 'demo-addressbook-work': true },
kind: 'individual',
name: { components: [{ kind: 'given', value: 'Michael' }, { kind: 'surname', value: 'Torres' }] },
emails: { e1: { address: 'michael.torres@company.example', contexts: { work: true }, pref: 1 } },
phones: { p1: { number: '+1-555-0301', features: { voice: true }, contexts: { work: true } } },
organizations: { o1: { name: 'Company Inc', units: [{ name: 'Product' }] } },
titles: { t1: { name: 'Product Manager', kind: 'title' } },
},
{
id: 'demo-contact-10',
addressBookIds: { 'demo-addressbook-work': true },
kind: 'individual',
name: { components: [{ kind: 'given', value: 'Rachel' }, { kind: 'surname', value: 'Green' }] },
emails: { e1: { address: 'rachel.green@company.example', contexts: { work: true }, pref: 1 } },
organizations: { o1: { name: 'Company Inc', units: [{ name: 'Marketing' }] } },
titles: { t1: { name: 'Marketing Lead', kind: 'title' } },
},
{
id: 'demo-contact-11',
addressBookIds: { 'demo-addressbook-work': true },
kind: 'individual',
name: { components: [{ kind: 'given', value: 'James' }, { kind: 'surname', value: 'Miller' }] },
emails: { e1: { address: 'james.miller@company.example', contexts: { work: true }, pref: 1 } },
organizations: { o1: { name: 'Company Inc', units: [{ name: 'Engineering' }] } },
titles: { t1: { name: 'CTO', kind: 'title' } },
},
{
id: 'demo-contact-12',
addressBookIds: { 'demo-addressbook-work': true },
kind: 'individual',
name: { components: [{ kind: 'given', value: 'Priya' }, { kind: 'surname', value: 'Sharma' }] },
emails: { e1: { address: 'priya.sharma@company.example', contexts: { work: true }, pref: 1 } },
organizations: { o1: { name: 'Company Inc', units: [{ name: 'QA' }] } },
titles: { t1: { name: 'QA Engineer', kind: 'title' } },
},
{
id: 'demo-contact-13',
addressBookIds: { 'demo-addressbook-work': true },
kind: 'individual',
name: { components: [{ kind: 'given', value: 'Ahmed' }, { kind: 'surname', value: 'Hassan' }] },
emails: { e1: { address: 'ahmed.hassan@company.example', contexts: { work: true }, pref: 1 } },
organizations: { o1: { name: 'Company Inc', units: [{ name: 'DevOps' }] } },
titles: { t1: { name: 'DevOps Engineer', kind: 'title' } },
},
{
id: 'demo-contact-14',
addressBookIds: { 'demo-addressbook-work': true },
kind: 'individual',
name: { components: [{ kind: 'given', value: 'Maria' }, { kind: 'surname', value: 'Lopez' }] },
emails: { e1: { address: 'maria.lopez@company.example', contexts: { work: true }, pref: 1 } },
organizations: { o1: { name: 'Company Inc', units: [{ name: 'HR' }] } },
titles: { t1: { name: 'HR Business Partner', kind: 'title' } },
},
{
id: 'demo-contact-15',
addressBookIds: { 'demo-addressbook-work': true },
kind: 'individual',
name: { components: [{ kind: 'given', value: 'Wei' }, { kind: 'surname', value: 'Zhang' }] },
emails: { e1: { address: 'wei.zhang@company.example', contexts: { work: true }, pref: 1 } },
organizations: { o1: { name: 'Company Inc', units: [{ name: 'Data Science' }] } },
titles: { t1: { name: 'Data Scientist', kind: 'title' } },
},
];
}
+364
View File
@@ -0,0 +1,364 @@
import type { Email } from '@/lib/jmap/types';
import { demoDate } from '../demo-utils';
export function createDemoEmails(): Email[] {
const now = new Date();
return [
// ── Inbox ───────────────────────────────────────────────────
{
id: 'demo-email-1',
threadId: 'demo-thread-1',
mailboxIds: { 'demo-mailbox-inbox': true },
keywords: {},
size: 4200,
receivedAt: demoDate(0, -2),
from: [{ name: 'Bulwark Team', email: 'welcome@bulwark.email' }],
to: [{ name: 'Demo User', email: 'demo@example.com' }],
subject: 'Welcome to Bulwark Mail!',
sentAt: demoDate(0, -2),
preview: 'Thanks for trying out Bulwark Mail. This is a demo environment where you can explore all features...',
hasAttachment: false,
textBody: [{ partId: '1', blobId: 'blob-1', size: 350, type: 'text/plain' }],
htmlBody: [{ partId: '2', blobId: 'blob-2', size: 800, type: 'text/html' }],
bodyValues: {
'1': { value: 'Thanks for trying out Bulwark Mail!\n\nThis is a demo environment where you can explore all features without connecting to a real server. All data stays on your device.\n\nFeel free to:\n- Read, compose, and organize emails\n- Manage contacts and calendars\n- Configure filters and settings\n- Try keyboard shortcuts (press ? to see them)\n\nEnjoy exploring!' },
'2': { value: '<div><h2>Welcome to Bulwark Mail!</h2><p>Thanks for trying out Bulwark Mail!</p><p>This is a demo environment where you can explore all features without connecting to a real server. <strong>All data stays on your device.</strong></p><p>Feel free to:</p><ul><li>Read, compose, and organize emails</li><li>Manage contacts and calendars</li><li>Configure filters and settings</li><li>Try keyboard shortcuts (press <kbd>?</kbd> to see them)</li></ul><p>Enjoy exploring!</p></div>' },
},
messageId: '<welcome@demo.bulwark.email>',
},
{
id: 'demo-email-2',
threadId: 'demo-thread-2',
mailboxIds: { 'demo-mailbox-inbox': true },
keywords: { $seen: true },
size: 18500,
receivedAt: demoDate(-1, -5),
from: [{ name: 'TechDigest Weekly', email: 'newsletter@techdigest.example' }],
to: [{ name: 'Demo User', email: 'demo@example.com' }],
subject: 'This Week in Tech: AI Developments & Open Source Updates',
sentAt: demoDate(-1, -5),
preview: 'Your weekly roundup of the most important technology news and open source developments...',
hasAttachment: false,
textBody: [{ partId: '1', blobId: 'blob-3', size: 2400, type: 'text/plain' }],
htmlBody: [{ partId: '2', blobId: 'blob-4', size: 5200, type: 'text/html' }],
bodyValues: {
'1': { value: 'This Week in Tech\n\n1. AI-Powered Code Review Tools\nNew tools are making code reviews faster and more thorough...\n\n2. Open Source Licensing Update\nThe OSI has published new guidelines for AI-generated code...\n\n3. WebAssembly 2.0 Draft\nThe W3C has released the first draft of WebAssembly 2.0...\n\nRead more at techdigest.example' },
'2': { value: '<div style="max-width:600px;margin:0 auto;"><h1>This Week in Tech</h1><h3>1. AI-Powered Code Review Tools</h3><p>New tools are making code reviews faster and more thorough, with several open-source options gaining traction.</p><h3>2. Open Source Licensing Update</h3><p>The OSI has published new guidelines for AI-generated code contributions to open source projects.</p><h3>3. WebAssembly 2.0 Draft</h3><p>The W3C has released the first draft of WebAssembly 2.0, promising improved memory management.</p></div>' },
},
messageId: '<weekly-42@techdigest.example>',
},
// Thread: Project discussion (3 emails in same thread)
{
id: 'demo-email-3a',
threadId: 'demo-thread-3',
mailboxIds: { 'demo-mailbox-inbox': true },
keywords: { $seen: true },
size: 3100,
receivedAt: demoDate(-3, -10),
from: [{ name: 'Alice Johnson', email: 'alice.johnson@example.com' }],
to: [{ name: 'Demo User', email: 'demo@example.com' }, { name: 'Bob Chen', email: 'bob.chen@example.com' }],
subject: 'Q4 Project Timeline',
sentAt: demoDate(-3, -10),
preview: 'Hi team, I wanted to share the updated timeline for our Q4 deliverables...',
hasAttachment: false,
textBody: [{ partId: '1', blobId: 'blob-5', size: 450, type: 'text/plain' }],
htmlBody: [{ partId: '2', blobId: 'blob-6', size: 650, type: 'text/html' }],
bodyValues: {
'1': { value: 'Hi team,\n\nI wanted to share the updated timeline for our Q4 deliverables:\n\n- Phase 1: Design review — Oct 15\n- Phase 2: Development — Nov 1-30\n- Phase 3: Testing — Dec 1-15\n- Phase 4: Launch — Dec 20\n\nPlease review and let me know if you see any conflicts.\n\nBest,\nAlice' },
'2': { value: '<p>Hi team,</p><p>I wanted to share the updated timeline for our Q4 deliverables:</p><ul><li>Phase 1: Design review — Oct 15</li><li>Phase 2: Development — Nov 1-30</li><li>Phase 3: Testing — Dec 1-15</li><li>Phase 4: Launch — Dec 20</li></ul><p>Please review and let me know if you see any conflicts.</p><p>Best,<br>Alice</p>' },
},
messageId: '<q4-timeline-1@example.com>',
},
{
id: 'demo-email-3b',
threadId: 'demo-thread-3',
mailboxIds: { 'demo-mailbox-inbox': true },
keywords: { $seen: true },
size: 3500,
receivedAt: demoDate(-2, -8),
from: [{ name: 'Bob Chen', email: 'bob.chen@example.com' }],
to: [{ name: 'Alice Johnson', email: 'alice.johnson@example.com' }, { name: 'Demo User', email: 'demo@example.com' }],
subject: 'Re: Q4 Project Timeline',
sentAt: demoDate(-2, -8),
preview: 'Looks good to me! One concern: the testing window might be tight given the holidays...',
hasAttachment: false,
textBody: [{ partId: '1', blobId: 'blob-7', size: 520, type: 'text/plain' }],
bodyValues: {
'1': { value: 'Looks good to me! One concern: the testing window might be tight given the holidays. Could we start testing a few days earlier?\n\nAlso, should we set up a shared doc for tracking blockers?\n\n— Bob' },
},
messageId: '<q4-timeline-2@example.com>',
inReplyTo: ['<q4-timeline-1@example.com>'],
references: ['<q4-timeline-1@example.com>'],
},
{
id: 'demo-email-3c',
threadId: 'demo-thread-3',
mailboxIds: { 'demo-mailbox-inbox': true },
keywords: {},
size: 3800,
receivedAt: demoDate(-1, -3),
from: [{ name: 'Alice Johnson', email: 'alice.johnson@example.com' }],
to: [{ name: 'Bob Chen', email: 'bob.chen@example.com' }, { name: 'Demo User', email: 'demo@example.com' }],
subject: 'Re: Q4 Project Timeline',
sentAt: demoDate(-1, -3),
preview: 'Great point Bob. Let\'s move testing to Nov 28. I\'ll create the shared doc today...',
hasAttachment: false,
textBody: [{ partId: '1', blobId: 'blob-8', size: 400, type: 'text/plain' }],
bodyValues: {
'1': { value: 'Great point Bob. Let\'s move testing to Nov 28. I\'ll create the shared doc today and share the link.\n\nUpdated timeline:\n- Design review: Oct 15\n- Development: Nov 1-27\n- Testing: Nov 28 - Dec 15\n- Launch: Dec 20\n\n— Alice' },
},
messageId: '<q4-timeline-3@example.com>',
inReplyTo: ['<q4-timeline-2@example.com>'],
references: ['<q4-timeline-1@example.com>', '<q4-timeline-2@example.com>'],
},
// Email with attachments
{
id: 'demo-email-4',
threadId: 'demo-thread-4',
mailboxIds: { 'demo-mailbox-inbox': true },
keywords: {},
size: 245000,
receivedAt: demoDate(0, -6),
from: [{ name: 'Sarah Kim', email: 'sarah.kim@example.com' }],
to: [{ name: 'Demo User', email: 'demo@example.com' }],
subject: 'Invoice #2024-089 & Project Screenshot',
sentAt: demoDate(0, -6),
preview: 'Hi, please find attached the invoice for October and a screenshot of the latest prototype...',
hasAttachment: true,
textBody: [{ partId: '1', blobId: 'blob-9', size: 280, type: 'text/plain' }],
bodyValues: {
'1': { value: 'Hi,\n\nPlease find attached the invoice for October and a screenshot of the latest prototype.\n\nLet me know if you have any questions.\n\nBest regards,\nSarah' },
},
attachments: [
{ partId: 'att-1', blobId: 'demo-blob-att-1', size: 145000, name: 'Invoice-2024-089.pdf', type: 'application/pdf' },
{ partId: 'att-2', blobId: 'demo-blob-att-2', size: 89000, name: 'prototype-v3.png', type: 'image/png' },
],
messageId: '<invoice-089@example.com>',
},
// Starred email
{
id: 'demo-email-5',
threadId: 'demo-thread-5',
mailboxIds: { 'demo-mailbox-inbox': true },
keywords: { $seen: true, $flagged: true },
size: 2800,
receivedAt: demoDate(-2, -1),
from: [{ name: 'Carlos Rivera', email: 'carlos.rivera@example.com' }],
to: [{ name: 'Demo User', email: 'demo@example.com' }],
subject: 'Reminder: Team Dinner Friday',
sentAt: demoDate(-2, -1),
preview: 'Hey! Just a reminder about our team dinner this Friday at 7 PM at The Garden Bistro...',
hasAttachment: false,
textBody: [{ partId: '1', blobId: 'blob-10', size: 320, type: 'text/plain' }],
bodyValues: {
'1': { value: 'Hey!\n\nJust a reminder about our team dinner this Friday at 7 PM at The Garden Bistro. I\'ve made a reservation for 8 people.\n\nAddress: 123 Oak Street\n\nLet me know if you can make it!\n\nCheers,\nCarlos' },
},
messageId: '<dinner-reminder@example.com>',
},
// ── Sent ────────────────────────────────────────────────────
{
id: 'demo-email-6',
threadId: 'demo-thread-6',
mailboxIds: { 'demo-mailbox-sent': true },
keywords: { $seen: true },
size: 2100,
receivedAt: demoDate(-1, -4),
from: [{ name: 'Demo User', email: 'demo@example.com' }],
to: [{ name: 'Alice Johnson', email: 'alice.johnson@example.com' }],
subject: 'Updated Requirements Document',
sentAt: demoDate(-1, -4),
preview: 'Hi Alice, I\'ve updated the requirements document with the changes we discussed...',
hasAttachment: false,
textBody: [{ partId: '1', blobId: 'blob-11', size: 290, type: 'text/plain' }],
bodyValues: {
'1': { value: 'Hi Alice,\n\nI\'ve updated the requirements document with the changes we discussed in yesterday\'s meeting. The main updates are in sections 3 and 5.\n\nLet me know if you have any questions.\n\nBest,\nDemo User' },
},
messageId: '<sent-1@example.com>',
},
{
id: 'demo-email-7',
threadId: 'demo-thread-7',
mailboxIds: { 'demo-mailbox-sent': true },
keywords: { $seen: true },
size: 1800,
receivedAt: demoDate(-4, -2),
from: [{ name: 'Demo User', email: 'demo@example.com' }],
to: [{ name: 'Sarah Kim', email: 'sarah.kim@example.com' }],
subject: 'Re: Design Feedback',
sentAt: demoDate(-4, -2),
preview: 'Thanks Sarah! The new color scheme looks great. I especially like the contrast improvements...',
hasAttachment: false,
textBody: [{ partId: '1', blobId: 'blob-12', size: 250, type: 'text/plain' }],
bodyValues: {
'1': { value: 'Thanks Sarah! The new color scheme looks great. I especially like the contrast improvements for accessibility.\n\nLet\'s go with Option B for the navigation.\n\nBest,\nDemo User' },
},
messageId: '<sent-2@example.com>',
},
// ── Drafts ──────────────────────────────────────────────────
{
id: 'demo-email-8',
threadId: 'demo-thread-8',
mailboxIds: { 'demo-mailbox-drafts': true },
keywords: { $seen: true, $draft: true },
size: 900,
receivedAt: demoDate(0, -1),
from: [{ name: 'Demo User', email: 'demo@example.com' }],
to: [{ name: 'Bob Chen', email: 'bob.chen@example.com' }],
subject: 'Meeting Notes - Draft',
sentAt: demoDate(0, -1),
preview: 'Here are the notes from today\'s standup...',
hasAttachment: false,
textBody: [{ partId: '1', blobId: 'blob-13', size: 180, type: 'text/plain' }],
bodyValues: {
'1': { value: 'Here are the notes from today\'s standup:\n\n- API integration on track\n- Need to resolve the caching issue\n- ' },
},
messageId: '<draft-1@example.com>',
},
// ── Trash ───────────────────────────────────────────────────
{
id: 'demo-email-9',
threadId: 'demo-thread-9',
mailboxIds: { 'demo-mailbox-trash': true },
keywords: { $seen: true },
size: 15200,
receivedAt: demoDate(-5, -3),
from: [{ name: 'Promo Store', email: 'deals@promostore.example' }],
to: [{ name: 'Demo User', email: 'demo@example.com' }],
subject: '🎉 Flash Sale: 50% Off Everything!',
sentAt: demoDate(-5, -3),
preview: 'Limited time offer! Get 50% off all items in our store...',
hasAttachment: false,
textBody: [{ partId: '1', blobId: 'blob-14', size: 400, type: 'text/plain' }],
bodyValues: {
'1': { value: 'Limited time offer! Get 50% off all items in our store. Use code FLASH50 at checkout.' },
},
messageId: '<promo-1@promostore.example>',
},
{
id: 'demo-email-10',
threadId: 'demo-thread-10',
mailboxIds: { 'demo-mailbox-trash': true },
keywords: { $seen: true },
size: 2300,
receivedAt: demoDate(-7, 0),
from: [{ name: 'System Notification', email: 'noreply@service.example' }],
to: [{ name: 'Demo User', email: 'demo@example.com' }],
subject: 'Your password was changed',
sentAt: demoDate(-7, 0),
preview: 'Your account password was successfully changed on...',
hasAttachment: false,
textBody: [{ partId: '1', blobId: 'blob-15', size: 200, type: 'text/plain' }],
bodyValues: {
'1': { value: 'Your account password was successfully changed. If you did not make this change, please contact support immediately.' },
},
messageId: '<notification-1@service.example>',
},
// ── Projects ────────────────────────────────────────────────
{
id: 'demo-email-11',
threadId: 'demo-thread-11',
mailboxIds: { 'demo-mailbox-projects': true },
keywords: { $seen: true, $flagged: true },
size: 4500,
receivedAt: demoDate(-2, -7),
from: [{ name: 'Alice Johnson', email: 'alice.johnson@example.com' }],
to: [{ name: 'Demo User', email: 'demo@example.com' }],
subject: '[Project] Sprint Planning Agenda',
sentAt: demoDate(-2, -7),
preview: 'Here\'s the agenda for next week\'s sprint planning session...',
hasAttachment: false,
textBody: [{ partId: '1', blobId: 'blob-16', size: 600, type: 'text/plain' }],
bodyValues: {
'1': { value: 'Hi team,\n\nHere\'s the agenda for next week\'s sprint planning:\n\n1. Review previous sprint velocity\n2. Discuss tech debt items\n3. Prioritize backlog\n4. Assign story points\n5. Capacity planning\n\nPlease come prepared with your updates.\n\nThanks,\nAlice' },
},
messageId: '<project-1@example.com>',
},
{
id: 'demo-email-12',
threadId: 'demo-thread-12',
mailboxIds: { 'demo-mailbox-projects': true },
keywords: {},
size: 3200,
receivedAt: demoDate(0, -8),
from: [{ name: 'Bob Chen', email: 'bob.chen@example.com' }],
to: [{ name: 'Demo User', email: 'demo@example.com' }],
subject: '[Project] API Rate Limiting Discussion',
sentAt: demoDate(0, -8),
preview: 'I\'ve been thinking about our rate limiting approach and wanted to propose a few changes...',
hasAttachment: false,
textBody: [{ partId: '1', blobId: 'blob-17', size: 480, type: 'text/plain' }],
bodyValues: {
'1': { value: 'Hey,\n\nI\'ve been thinking about our rate limiting approach and wanted to propose:\n\n1. Token bucket algorithm instead of fixed window\n2. Per-endpoint limits rather than global\n3. Graduated response (warn → throttle → block)\n\nThoughts? I can put together a more detailed RFC if we agree on the direction.\n\n— Bob' },
},
messageId: '<project-2@example.com>',
},
// ── Archive ─────────────────────────────────────────────────
{
id: 'demo-email-13',
threadId: 'demo-thread-13',
mailboxIds: { 'demo-mailbox-archive': true },
keywords: { $seen: true },
size: 2600,
receivedAt: demoDate(-14, -6),
from: [{ name: 'HR Department', email: 'hr@company.example' }],
to: [{ name: 'Demo User', email: 'demo@example.com' }],
subject: 'Updated PTO Policy - Effective January 1',
sentAt: demoDate(-14, -6),
preview: 'Please review the updated PTO policy that takes effect January 1st...',
hasAttachment: false,
textBody: [{ partId: '1', blobId: 'blob-18', size: 380, type: 'text/plain' }],
bodyValues: {
'1': { value: 'Dear team,\n\nPlease review the updated PTO policy effective January 1st. Key changes include:\n\n- Increased annual allowance from 20 to 25 days\n- Flexible half-day options\n- Rollover limit increased to 10 days\n\nPlease acknowledge receipt.\n\nBest,\nHR Department' },
},
messageId: '<hr-policy-1@company.example>',
},
// ── Receipts ────────────────────────────────────────────────
{
id: 'demo-email-14',
threadId: 'demo-thread-14',
mailboxIds: { 'demo-mailbox-receipts': true },
keywords: { $seen: true },
size: 5200,
receivedAt: demoDate(-3, -12),
from: [{ name: 'Cloud Services', email: 'billing@cloudprovider.example' }],
to: [{ name: 'Demo User', email: 'demo@example.com' }],
subject: 'Payment Receipt - Invoice #INV-2024-1042',
sentAt: demoDate(-3, -12),
preview: 'Your payment of $49.99 has been processed successfully...',
hasAttachment: false,
textBody: [{ partId: '1', blobId: 'blob-19', size: 350, type: 'text/plain' }],
bodyValues: {
'1': { value: 'Payment Confirmation\n\nAmount: $49.99\nDate: Processing date\nInvoice: INV-2024-1042\nService: Cloud Hosting (Standard Plan)\n\nThank you for your payment.' },
},
messageId: '<receipt-1@cloudprovider.example>',
},
// ── Spam ────────────────────────────────────────────────────
{
id: 'demo-email-15',
threadId: 'demo-thread-15',
mailboxIds: { 'demo-mailbox-junk': true },
keywords: {},
size: 8900,
receivedAt: demoDate(-1, -9),
from: [{ name: 'Prize Center', email: 'winner@totallylegit.example' }],
to: [{ name: 'Demo User', email: 'demo@example.com' }],
subject: 'Congratulations! You Won $1,000,000!!!',
sentAt: demoDate(-1, -9),
preview: 'Dear lucky winner, you have been selected to receive one million dollars...',
hasAttachment: false,
textBody: [{ partId: '1', blobId: 'blob-20', size: 500, type: 'text/plain' }],
bodyValues: {
'1': { value: 'Dear lucky winner,\n\nYou have been selected to receive ONE MILLION DOLLARS! Click below to claim your prize immediately.\n\n[This is a demo spam email]' },
},
messageId: '<spam-1@totallylegit.example>',
},
];
}
+94
View File
@@ -0,0 +1,94 @@
import type { FileNode } from '@/lib/jmap/types';
import { demoDate } from '../demo-utils';
export function createDemoFileNodes(): FileNode[] {
return [
// Root-level directories
{
id: 'demo-file-documents',
parentId: null,
name: 'Documents',
type: 'd',
blobId: null,
size: 0,
created: demoDate(-30),
updated: demoDate(-2),
},
{
id: 'demo-file-photos',
parentId: null,
name: 'Photos',
type: 'd',
blobId: null,
size: 0,
created: demoDate(-30),
updated: demoDate(-5),
},
// Documents contents
{
id: 'demo-file-meeting-notes',
parentId: 'demo-file-documents',
name: 'meeting-notes.md',
type: 'text/markdown',
blobId: 'demo-blob-file-1',
size: 2150,
created: demoDate(-7),
updated: demoDate(-2),
},
{
id: 'demo-file-quarterly-report',
parentId: 'demo-file-documents',
name: 'quarterly-report.pdf',
type: 'application/pdf',
blobId: 'demo-blob-file-2',
size: 148480,
created: demoDate(-14),
updated: demoDate(-14),
},
{
id: 'demo-file-todo',
parentId: 'demo-file-documents',
name: 'todo.txt',
type: 'text/plain',
blobId: 'demo-blob-file-3',
size: 410,
created: demoDate(-3),
updated: demoDate(-1),
},
// Photos contents
{
id: 'demo-file-vacation',
parentId: 'demo-file-photos',
name: 'vacation.jpg',
type: 'image/jpeg',
blobId: 'demo-blob-file-4',
size: 1258291,
created: demoDate(-10),
updated: demoDate(-10),
},
{
id: 'demo-file-team-photo',
parentId: 'demo-file-photos',
name: 'team-photo.png',
type: 'image/png',
blobId: 'demo-blob-file-5',
size: 911360,
created: demoDate(-21),
updated: demoDate(-21),
},
// Root-level file
{
id: 'demo-file-budget',
parentId: null,
name: 'budget.xlsx',
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
blobId: 'demo-blob-file-6',
size: 68608,
created: demoDate(-5),
updated: demoDate(-1),
},
];
}
+48
View File
@@ -0,0 +1,48 @@
import type { SieveScript, SieveCapabilities } from '@/lib/jmap/sieve-types';
export function createDemoSieveCapabilities(): SieveCapabilities {
return {
implementation: 'Demo Sieve Engine',
maxSizeScript: 65536,
sieveExtensions: ['fileinto', 'reject', 'vacation', 'imap4flags', 'comparator-i;ascii-casemap', 'body', 'envelope'],
notificationMethods: [],
externalLists: [],
};
}
export function createDemoSieveScripts(): SieveScript[] {
return [
{
id: 'demo-sieve-1',
name: 'Default Filters',
blobId: 'demo-sieve-blob-1',
isActive: true,
},
];
}
// Sieve script content keyed by blobId
export function createDemoSieveContent(): Record<string, string> {
return {
'demo-sieve-blob-1': [
'require ["fileinto", "imap4flags"];',
'',
'# Newsletters to Receipts',
'if address :contains "from" "newsletter@" {',
' fileinto "Receipts";',
' stop;',
'}',
'',
'# Flag emails from boss',
'if address :is "from" "alice.johnson@example.com" {',
' addflag "\\\\Flagged";',
'}',
'',
'# Move project updates',
'if header :contains "subject" "[Project]" {',
' fileinto "Projects";',
' stop;',
'}',
].join('\n'),
};
}
+22
View File
@@ -0,0 +1,22 @@
import type { Identity } from '@/lib/jmap/types';
export function createDemoIdentities(): Identity[] {
return [
{
id: 'demo-identity-primary',
name: 'Demo User',
email: 'demo@example.com',
textSignature: 'Best regards,\nDemo User\nBulwark Mail Demo',
htmlSignature: '<p>Best regards,<br><b>Demo User</b><br>Bulwark Mail Demo</p>',
mayDelete: false,
},
{
id: 'demo-identity-alias',
name: 'Demo User',
email: 'demo+newsletter@example.com',
textSignature: '',
htmlSignature: '',
mayDelete: true,
},
];
}
+17
View File
@@ -0,0 +1,17 @@
import type { Mailbox } from '@/lib/jmap/types';
const RIGHTS_SYSTEM = { mayReadItems: true, mayAddItems: true, mayRemoveItems: true, maySetSeen: true, maySetKeywords: true, mayCreateChild: true, mayRename: false, mayDelete: false, maySubmit: true };
const RIGHTS_CUSTOM = { ...RIGHTS_SYSTEM, mayRename: true, mayDelete: true };
export function createDemoMailboxes(): Mailbox[] {
return [
{ id: 'demo-mailbox-inbox', name: 'Inbox', role: 'inbox', sortOrder: 1, totalEmails: 12, unreadEmails: 5, totalThreads: 10, unreadThreads: 4, myRights: RIGHTS_SYSTEM, isSubscribed: true },
{ id: 'demo-mailbox-sent', name: 'Sent', role: 'sent', sortOrder: 2, totalEmails: 8, unreadEmails: 0, totalThreads: 8, unreadThreads: 0, myRights: RIGHTS_SYSTEM, isSubscribed: true },
{ id: 'demo-mailbox-drafts', name: 'Drafts', role: 'drafts', sortOrder: 3, totalEmails: 1, unreadEmails: 0, totalThreads: 1, unreadThreads: 0, myRights: RIGHTS_SYSTEM, isSubscribed: true },
{ id: 'demo-mailbox-trash', name: 'Trash', role: 'trash', sortOrder: 5, totalEmails: 2, unreadEmails: 0, totalThreads: 2, unreadThreads: 0, myRights: RIGHTS_SYSTEM, isSubscribed: true },
{ id: 'demo-mailbox-archive', name: 'Archive', role: 'archive', sortOrder: 4, totalEmails: 4, unreadEmails: 0, totalThreads: 4, unreadThreads: 0, myRights: RIGHTS_SYSTEM, isSubscribed: true },
{ id: 'demo-mailbox-junk', name: 'Spam', role: 'junk', sortOrder: 6, totalEmails: 3, unreadEmails: 1, totalThreads: 3, unreadThreads: 1, myRights: RIGHTS_SYSTEM, isSubscribed: true },
{ id: 'demo-mailbox-projects', name: 'Projects', sortOrder: 10, totalEmails: 5, unreadEmails: 2, totalThreads: 5, unreadThreads: 2, myRights: RIGHTS_CUSTOM, isSubscribed: true },
{ id: 'demo-mailbox-receipts', name: 'Receipts', sortOrder: 11, totalEmails: 3, unreadEmails: 0, totalThreads: 3, unreadThreads: 0, myRights: RIGHTS_CUSTOM, isSubscribed: true },
];
}
+13
View File
@@ -0,0 +1,13 @@
import type { VacationResponse } from '@/lib/jmap/types';
export function createDemoVacationResponse(): VacationResponse {
return {
id: 'singleton',
isEnabled: false,
fromDate: null,
toDate: null,
subject: 'Out of Office',
textBody: 'Thank you for your email. I am currently out of the office and will return on Monday. For urgent matters, please contact support@example.com.',
htmlBody: '<p>Thank you for your email. I am currently out of the office and will return on Monday.</p><p>For urgent matters, please contact <a href="mailto:support@example.com">support@example.com</a>.</p>',
};
}