@@ -19,17 +19,18 @@ describe.skipIf(!process.env.MAILFLOW_TEST_DATABASE_URL)('threaded Inbox message
1919 await database . connect ( ) ;
2020 await database . query ( `
2121 CREATE TEMP TABLE email_accounts (id uuid, user_id text, enabled boolean,
22- include_in_unified_inbox boolean, name text, email_address text, color text);
23- CREATE TEMP TABLE folders (account_id uuid, path text, total_count integer DEFAULT 0,
24- unread_count integer DEFAULT 0);
22+ include_in_unified_inbox boolean, name text, email_address text, color text,
23+ folder_mappings jsonb);
24+ CREATE TEMP TABLE folders (account_id uuid, path text, name text, delimiter text,
25+ special_use text, total_count integer DEFAULT 0, unread_count integer DEFAULT 0);
2526 CREATE TEMP TABLE messages (id uuid PRIMARY KEY, account_id uuid, folder text,
2627 uid bigint, message_id text, in_reply_to text, thread_references text,
2728 thread_id text, thread_key text, date timestamptz, is_deleted boolean DEFAULT false,
2829 subject text, from_name text, from_email text, to_addresses jsonb, cc_addresses jsonb,
2930 reply_to jsonb, snippet text, is_read boolean DEFAULT false, is_starred boolean DEFAULT false,
3031 has_attachments boolean DEFAULT false, category text, list_unsubscribe text,
3132 list_unsubscribe_post text, delivery_addresses jsonb, spam_verdict text,
32- spam_user_override text, spam_score_ml real);
33+ spam_user_override text, spam_score_ml real, flags jsonb );
3334 CREATE TEMP TABLE contacts (id uuid, user_id text, primary_email text, photo_data text)` ) ;
3435 query . mockImplementation ( ( sql , values ) => database . query ( sql , values ) ) ;
3536 } ) ;
@@ -39,15 +40,19 @@ describe.skipIf(!process.env.MAILFLOW_TEST_DATABASE_URL)('threaded Inbox message
3940 beforeEach ( async ( ) => {
4041 await database . query ( 'TRUNCATE messages, folders, email_accounts, contacts' ) ;
4142 await database . query (
42- "INSERT INTO email_accounts VALUES ($1,'owner',true,true,'Work','me@example.com','#123456')" ,
43+ "INSERT INTO email_accounts VALUES ($1,'owner',true,true,'Work','me@example.com','#123456',NULL )" ,
4344 [ account ] ,
4445 ) ;
4546 } ) ;
4647
47- const insert = ( folder , messageId , threadKey , date , subject ) => database . query (
48- `INSERT INTO messages(id,account_id,folder,message_id,thread_id,thread_key,date,subject)
49- VALUES ($1,$2,$3,$4,$5,$5,$6,$7)` ,
50- [ id ( ) , account , folder , messageId , threadKey , date , subject ] ,
48+ const insert = ( folder , messageId , threadKey , date , subject , flags = null ) => database . query (
49+ `INSERT INTO messages(id,account_id,folder,message_id,thread_id,thread_key,date,subject,flags)
50+ VALUES ($1,$2,$3,$4,$5,$5,$6,$7,$8)` ,
51+ [ id ( ) , account , folder , messageId , threadKey , date , subject , flags && JSON . stringify ( flags ) ] ,
52+ ) ;
53+ const folder = ( path , { name = path , specialUse = null , delimiter = '/' } = { } ) => database . query (
54+ 'INSERT INTO folders(account_id,path,name,delimiter,special_use) VALUES ($1,$2,$3,$4,$5)' ,
55+ [ account , path , name , delimiter , specialUse ] ,
5156 ) ;
5257
5358 const inboxRows = async ( accountId ) => {
@@ -95,6 +100,43 @@ describe.skipIf(!process.env.MAILFLOW_TEST_DATABASE_URL)('threaded Inbox message
95100 } ) ;
96101 }
97102
103+ // A count of 2 makes the row a conversation, and deleting a conversation expunges its drafts:
104+ // a saved reply must not turn a lone message into one.
105+ it ( 'does not count a saved reply draft, by its \\Draft flag' , async ( ) => {
106+ await insert ( 'INBOX' , '<a@ext>' , '<a@ext>' , '2026-10-01T10:00Z' , 'Question' ) ;
107+ await insert ( 'Drafts' , '<d@me>' , '<a@ext>' , '2026-10-01T11:00Z' , 'Re: Question' , [ '\\Draft' , '\\Seen' ] ) ;
108+ expect ( ( await inboxRows ( account ) ) [ '<a@ext>' ] . message_count ) . toBe ( 1 ) ;
109+ expect ( ( await inboxRows ( undefined ) ) [ '<a@ext>' ] . message_count ) . toBe ( 1 ) ;
110+ } ) ;
111+
112+ it ( 'does not count an unflagged draft in the special-use, stock-named or mapped Drafts folder' , async ( ) => {
113+ await folder ( '[Gmail]/Entwürfe' , { name : 'Entwürfe' , specialUse : '\\Drafts' } ) ;
114+ await folder ( 'Drafts' ) ;
115+ await folder ( 'Brouillons' ) ;
116+ await database . query ( `UPDATE email_accounts SET folder_mappings = '{"drafts":"Brouillons"}'` ) ;
117+ await insert ( 'INBOX' , '<a@ext>' , '<a@ext>' , '2026-10-01T10:00Z' , 'Question' ) ;
118+ await insert ( '[Gmail]/Entwürfe' , '<d1@me>' , '<a@ext>' , '2026-10-01T11:00Z' , 'Re: Question' ) ;
119+ await insert ( 'Drafts' , '<d2@me>' , '<a@ext>' , '2026-10-01T11:01Z' , 'Re: Question' ) ;
120+ await insert ( 'Brouillons' , '<d3@me>' , '<a@ext>' , '2026-10-01T11:02Z' , 'Re: Question' ) ;
121+ expect ( ( await inboxRows ( account ) ) [ '<a@ext>' ] . message_count ) . toBe ( 1 ) ;
122+ } ) ;
123+
124+ it ( 'still counts a message in a user folder that merely has "drafts" in its name' , async ( ) => {
125+ await folder ( 'Clients/Drafts' , { name : 'Drafts' } ) ;
126+ await folder ( 'Blog drafts' ) ;
127+ await insert ( 'INBOX' , '<a@ext>' , '<a@ext>' , '2026-10-01T10:00Z' , 'Question' ) ;
128+ await insert ( 'Clients/Drafts' , '<b@ext>' , '<a@ext>' , '2026-10-01T11:00Z' , 'Re: Question' ) ;
129+ await insert ( 'Blog drafts' , '<c@ext>' , '<a@ext>' , '2026-10-01T12:00Z' , 'Re: Question' ) ;
130+ expect ( ( await inboxRows ( account ) ) [ '<a@ext>' ] . message_count ) . toBe ( 3 ) ;
131+ } ) ;
132+
133+ it ( 'counts the Sent reply but not the draft beside it' , async ( ) => {
134+ await insert ( 'INBOX' , '<a@ext>' , '<a@ext>' , '2026-10-01T10:00Z' , 'Question' ) ;
135+ await insert ( 'Sent' , '<b@me>' , '<a@ext>' , '2026-10-01T11:00Z' , 'Re: Question' , [ '\\Seen' ] ) ;
136+ await insert ( 'Drafts' , '<d@me>' , '<a@ext>' , '2026-10-01T12:00Z' , 'Re: Question' , [ '\\Draft' ] ) ;
137+ expect ( ( await inboxRows ( account ) ) [ '<a@ext>' ] . message_count ) . toBe ( 2 ) ;
138+ } ) ;
139+
98140 it ( 'does not count deleted messages' , async ( ) => {
99141 await insert ( 'INBOX' , '<a@ext>' , '<a@ext>' , '2026-10-01T10:00Z' , 'Question' ) ;
100142 await insert ( 'Sent' , '<b@me>' , '<a@ext>' , '2026-10-01T11:00Z' , 'Re: Question' ) ;
0 commit comments