@@ -22,16 +22,26 @@ import Utils from './utils';
2222const RE_FLAG_PATTERN = / ^ \/ ( .* ) \/ ( [ g i m u y s ] * ) $ / ;
2323
2424enum ConditionType {
25- AND , OR , NOT , SUBJECT , FROM , TO , CC , BCC , LIST , SENDER , RECEIVER , BODY , HEADER ,
25+ AND , OR , NOT , SUBJECT , FROM , TO , CC , BCC , LIST , SENDER , RECEIVER , BODY , HEADER , THREAD
26+ }
27+
28+ enum ThreadSubType {
29+ FIRST_MESSAGE_SUBJECT , LABEL , IS_STARRED , IS_IMPORTANT , IS_IN_INBOX , IS_IN_PRIORITY_INBOX ,
30+ IS_IN_SPAM , IS_IN_TRASH , IS_UNREAD
2631}
2732
2833/**
2934 * S expression represents condition in rule.
3035 *
3136 * Syntax:
32- * CONDITION_EXP := (OPERATOR CONDITION_LIST) | (MATCHER STRING)
37+ * CONDITION_EXP := (OPERATOR CONDITION_LIST) | (MATCHER STRING) |
38+ * (MATCHER_SUBTYPE SUBTYPE_STRING STRING) | (MATCHER_SUBTYPE SUBTYPE_BOOL)
3339 * OPERATOR := and | or | not
34- * MATCHER := subject | from | to | cc | bcc | list | sender | receiver | content
40+ * MATCHER := subject | from | to | cc | bcc | list | sender | receiver | body
41+ * MATCHER_SUBTYPE := header | thread
42+ * SUBTYPE_STRING := first_message_subject | label | STRING
43+ * SUBTYPE_BOOL := is_starred | is_important | is_in_inbox | is_in_priority_inbox |
44+ * is_in_spam | is_in_trash | is_unread
3545 * CONDITION_LIST := CONDITION_EXP | CONDITION_EXP CONDITION_LIST
3646 */
3747export default class Condition {
@@ -87,7 +97,8 @@ export default class Condition {
8797 }
8898
8999 private readonly type : ConditionType ;
90- private readonly subtype : string ;
100+ private readonly header : string ;
101+ private readonly threadSubtype : ThreadSubType ;
91102 private readonly regexp : RegExp ;
92103 private readonly sub_conditions : Condition [ ] ;
93104
@@ -99,7 +110,6 @@ export default class Condition {
99110 const type_str = condition_str . substring ( 1 , first_space ) . trim ( ) . toUpperCase ( ) ;
100111 let rest_str = condition_str . substring ( first_space + 1 , condition_str . length - 1 ) . trim ( ) ;
101112 this . type = ConditionType [ type_str as keyof typeof ConditionType ] ;
102- this . subtype = "" ;
103113 switch ( this . type ) {
104114 case ConditionType . AND :
105115 case ConditionType . OR : {
@@ -123,11 +133,27 @@ export default class Condition {
123133 this . regexp = Condition . parseRegExp ( rest_str , condition_str , true ) ;
124134 break ;
125135 }
126- case ConditionType . HEADER : {
136+ case ConditionType . HEADER :
137+ case ConditionType . THREAD : {
127138 const subtype_first_space = rest_str . indexOf ( " " ) ;
128- this . subtype = rest_str . substring ( 0 , subtype_first_space ) . trim ( ) ;
129- rest_str = rest_str . substring ( subtype_first_space + 1 , rest_str . length - 1 ) . trim ( ) ;
130- this . regexp = Condition . parseRegExp ( rest_str , condition_str , false ) ;
139+ const subtype = subtype_first_space > 0
140+ ? rest_str . substring ( 0 , subtype_first_space ) . trim ( )
141+ : rest_str ;
142+ let matching_address = true ;
143+ if ( this . type === ConditionType . HEADER ) {
144+ this . header = subtype ;
145+ }
146+ if ( this . type === ConditionType . THREAD ) {
147+ this . threadSubtype = ThreadSubType [ subtype . toUpperCase ( ) as keyof typeof ThreadSubType ] ;
148+ if ( this . threadSubtype === ThreadSubType . FIRST_MESSAGE_SUBJECT ) {
149+ matching_address = false ;
150+ }
151+ if ( this . threadSubtype === undefined ) {
152+ throw `Invalid 'thread' subtype: "${ condition_str } "` ;
153+ }
154+ }
155+ rest_str = rest_str . substring ( subtype_first_space + 1 , rest_str . length ) . trim ( ) ;
156+ this . regexp = Condition . parseRegExp ( rest_str , condition_str , matching_address ) ;
131157 break ;
132158 }
133159 case ConditionType . SUBJECT :
@@ -189,12 +215,43 @@ export default class Condition {
189215 return this . regexp . test ( message_data . body ) ;
190216 }
191217 case ConditionType . HEADER : {
192- const headerData = message_data . headers . get ( this . subtype ) ;
218+ const headerData = message_data . headers . get ( this . header ) ;
193219 if ( headerData !== undefined ) {
194220 return this . regexp . test ( headerData ) ;
195221 }
196222 return false ;
197223 }
224+ case ConditionType . THREAD : {
225+ switch ( this . threadSubtype ) {
226+ case ThreadSubType . IS_IMPORTANT : {
227+ return message_data . thread_is_important ;
228+ }
229+ case ThreadSubType . IS_IN_INBOX : {
230+ return message_data . thread_is_in_inbox ;
231+ }
232+ case ThreadSubType . IS_IN_PRIORITY_INBOX : {
233+ return message_data . thread_is_in_priority_inbox ;
234+ }
235+ case ThreadSubType . IS_IN_SPAM : {
236+ return message_data . thread_is_in_spam ;
237+ }
238+ case ThreadSubType . IS_IN_TRASH : {
239+ return message_data . thread_is_in_trash ;
240+ }
241+ case ThreadSubType . IS_STARRED : {
242+ return message_data . thread_is_starred ;
243+ }
244+ case ThreadSubType . IS_UNREAD : {
245+ return message_data . thread_is_unread ;
246+ }
247+ case ThreadSubType . FIRST_MESSAGE_SUBJECT : {
248+ return this . regexp . test ( message_data . thread_first_message_subject ) ;
249+ }
250+ case ThreadSubType . LABEL : {
251+ return this . matchAddress ( ...message_data . thread_labels ) ;
252+ }
253+ }
254+ }
198255 }
199256 }
200257
@@ -212,7 +269,7 @@ export default class Condition {
212269 getConditionHeaders ( ) : string [ ] {
213270 const headers = [ ] ;
214271 if ( this . type === ConditionType . HEADER ) {
215- headers . push ( this . subtype ) ;
272+ headers . push ( this . header ) ;
216273 }
217274 this . sub_conditions ?. forEach ( ( sub_condition ) => {
218275 headers . push ( ...sub_condition . getConditionHeaders ( ) ) ;
@@ -291,27 +348,19 @@ export default class Condition {
291348 }
292349
293350 public static testConditionParsing ( it : Function , expect : Function ) {
294- const base_message = {
295- getFrom : ( ) => '' ,
296- getTo : ( ) => '' ,
297- getCc : ( ) => '' ,
298- getBcc : ( ) => '' ,
299- getReplyTo : ( ) => '' ,
300- getSubject : ( ) => '' ,
301- getPlainBody : ( ) => '' ,
302- getRawContent : ( ) => '' ,
303- getHeader : ( _name : string ) => '' ,
304- } as GoogleAppsScript . Gmail . GmailMessage ;
305351
306352 function test_cond (
307353 condition_str : string ,
308354 message : Partial < GoogleAppsScript . Gmail . GmailMessage > ,
309- session_data : Partial < SessionData > = { } ) : boolean {
355+ thread : Partial < GoogleAppsScript . Gmail . GmailThread > = { } ,
356+ session_data : Partial < SessionData > = { } ,
357+ thread_labels : string [ ] = [ ] ) : boolean {
310358 const condition = new Condition ( condition_str ) ;
359+ const mock_message = Mocks . getMockMessage ( message , thread , thread_labels ) ;
311360 const mock_session_data = Mocks . getMockSessionData ( session_data ) ;
312361 const message_data = new MessageData (
313362 mock_session_data ,
314- Object . assign ( { } , base_message , message ) ) ;
363+ mock_message ) ;
315364 return condition . match ( message_data ) ;
316365 }
317366
@@ -360,9 +409,163 @@ export default class Condition {
360409 getTo : ( ) => 'abc+Def@bar.com' ,
361410 } ) ) . toBe ( true )
362411 } )
412+ it ( 'Matches body using case-sensitivity' , ( ) => {
413+ expect ( test_cond ( `(body with aSdF)` ,
414+ {
415+ getPlainBody : ( ) => 'Text with aSdF in it' ,
416+ } ) ) . toBe ( true )
417+ } )
418+ it ( 'Does not match body using case-sensitivity' , ( ) => {
419+ expect ( test_cond ( `(body asdf)` ,
420+ {
421+ getPlainBody : ( ) => 'Text with aSdF in it' ,
422+ } ) ) . toBe ( false )
423+ } )
424+
425+ function test_cond_labels (
426+ condition_str : string ,
427+ thread_labels : string [ ] ) : boolean {
428+ return test_cond ( condition_str , { } , { } , { } , thread_labels ) ;
429+ }
430+
431+ it ( 'Matches email that is in label, case-insensitive' , ( ) => {
432+ expect ( test_cond_labels ( `(thread label xyz)` ,
433+ [ 'ABC' , 'XYZ' , 'ABC/XYZ' ] ) ) . toBe ( true )
434+ } )
435+ it ( 'Does not match email that is in label with partial name' , ( ) => {
436+ expect ( test_cond_labels ( `(thread label XY)` ,
437+ [ 'ABC' , 'XYZ' , 'ABC/XYZ' ] ) ) . toBe ( false )
438+ } )
439+ it ( 'Does not match email that is in label without full name' , ( ) => {
440+ expect ( test_cond_labels ( `(thread label XYZ)` ,
441+ [ 'ABC/XYZ' ] ) ) . toBe ( false )
442+ } )
443+ it ( 'Matches email that is in label with full name' , ( ) => {
444+ expect ( test_cond_labels ( `(thread label ABC/XYZ)` ,
445+ [ 'ABC/XYZ' ] ) ) . toBe ( true )
446+ } )
447+
448+ function test_cond_thread (
449+ condition_str : string ,
450+ thread : Partial < GoogleAppsScript . Gmail . GmailThread > ) : boolean {
451+ return test_cond ( condition_str , { } , thread ) ;
452+ }
453+
454+ it ( 'Throws exception if thread subtype is invalid' , ( ) => {
455+ expect ( ( ) => { test_cond_thread ( `(thread is_made_up)` , { } ) } ) . toThrow ( )
456+ } )
457+ it ( 'Matches thread is_important if is' , ( ) => {
458+ expect ( test_cond_thread ( `(thread is_important)` ,
459+ {
460+ isImportant : ( ) => true ,
461+ } ) ) . toBe ( true )
462+ } )
463+ it ( 'Does not match thread is_important if it is not' , ( ) => {
464+ expect ( test_cond_thread ( `(thread is_important)` ,
465+ {
466+ isImportant : ( ) => false ,
467+ } ) ) . toBe ( false )
468+ } )
469+ it ( 'Matches thread is_in_inbox if is' , ( ) => {
470+ expect ( test_cond_thread ( `(thread is_in_inbox)` ,
471+ {
472+ isInInbox : ( ) => true ,
473+ } ) ) . toBe ( true )
474+ } )
475+ it ( 'Does not match thread is_in_inbox if it is not' , ( ) => {
476+ expect ( test_cond_thread ( `(thread is_in_inbox)` ,
477+ {
478+ isInInbox : ( ) => false ,
479+ } ) ) . toBe ( false )
480+ } )
481+ it ( 'Matches thread is_in_priority_inbox if is' , ( ) => {
482+ expect ( test_cond_thread ( `(thread is_in_priority_inbox)` ,
483+ {
484+ isInPriorityInbox : ( ) => true ,
485+ } ) ) . toBe ( true )
486+ } )
487+ it ( 'Does not match thread is_in_priority_inbox if it is not' , ( ) => {
488+ expect ( test_cond_thread ( `(thread is_in_priority_inbox)` ,
489+ {
490+ isInPriorityInbox : ( ) => false ,
491+ } ) ) . toBe ( false )
492+ } )
493+ it ( 'Matches thread is_in_spam if is' , ( ) => {
494+ expect ( test_cond_thread ( `(thread is_in_spam)` ,
495+ {
496+ isInSpam : ( ) => true ,
497+ } ) ) . toBe ( true )
498+ } )
499+ it ( 'Does not match thread is_in_spam if it is not' , ( ) => {
500+ expect ( test_cond_thread ( `(thread is_in_spam)` ,
501+ {
502+ isInSpam : ( ) => false ,
503+ } ) ) . toBe ( false )
504+ } )
505+ it ( 'Matches thread is_in_trash if it is' , ( ) => {
506+ expect ( test_cond_thread ( `(thread is_in_trash)` ,
507+ {
508+ isInTrash : ( ) => true ,
509+ } ) ) . toBe ( true )
510+ } )
511+ it ( 'Does not match thread is_in_trash if it is not' , ( ) => {
512+ expect ( test_cond_thread ( `(thread is_in_trash)` ,
513+ {
514+ isInTrash : ( ) => false ,
515+ } ) ) . toBe ( false )
516+ } )
517+ it ( 'Matches thread is_starred if it is' , ( ) => {
518+ expect ( test_cond_thread ( `(thread is_starred)` ,
519+ {
520+ hasStarredMessages : ( ) => true ,
521+ } ) ) . toBe ( true )
522+ } )
523+ it ( 'Does not match thread is_starred if it is not' , ( ) => {
524+ expect ( test_cond_thread ( `(thread is_starred)` ,
525+ {
526+ hasStarredMessages : ( ) => false ,
527+ } ) ) . toBe ( false )
528+ } )
529+ it ( 'Matches thread is_unread if it is' , ( ) => {
530+ expect ( test_cond_thread ( `(thread is_unread)` ,
531+ {
532+ isUnread : ( ) => true ,
533+ } ) ) . toBe ( true )
534+ } )
535+ it ( 'Does not match thread is_unread if it is not' , ( ) => {
536+ expect ( test_cond_thread ( `(thread is_unread)` ,
537+ {
538+ isUnread : ( ) => false ,
539+ } ) ) . toBe ( false )
540+ } )
541+ it ( 'Matches thread first_message_subject with case-sensitivity' , ( ) => {
542+ expect ( test_cond_thread ( `(thread first_message_subject this is IN subject)` ,
543+ {
544+ getFirstMessageSubject : ( ) => 'subject this is IN subjects' ,
545+ } ) ) . toBe ( true )
546+ } )
547+ it ( 'Does not match thread first_message_subject with case-sensitivity' , ( ) => {
548+ expect ( test_cond_thread ( `(thread first_message_subject this is IN subject)` ,
549+ {
550+ getFirstMessageSubject : ( ) => 'subject this is in subjects' ,
551+ } ) ) . toBe ( false )
552+ } )
553+ it ( 'Matches thread first_message_subject with regex' , ( ) => {
554+ expect ( test_cond_thread ( `(thread first_message_subject /teST Regex/i)` ,
555+ {
556+ getFirstMessageSubject : ( ) => 'RE: test regex subjectline' ,
557+ } ) ) . toBe ( true )
558+ } )
559+
560+ function test_cond_headers (
561+ condition_str : string ,
562+ message : Partial < GoogleAppsScript . Gmail . GmailMessage > ,
563+ session_data : Partial < SessionData > ) : boolean {
564+ return test_cond ( condition_str , message , { } , session_data ) ;
565+ }
363566
364567 it ( 'Matches custom header with value' , ( ) => {
365- expect ( test_cond ( `(header Sender abc@def.com)` ,
568+ expect ( test_cond_headers ( `(header Sender abc@def.com)` ,
366569 {
367570 getHeader : ( name : string ) => {
368571 if ( name === 'Sender' ) {
@@ -376,7 +579,7 @@ export default class Condition {
376579 } ) ) . toBe ( true )
377580 } )
378581 it ( 'Matches nested custom header with value' , ( ) => {
379- expect ( test_cond ( `(and
582+ expect ( test_cond_headers ( `(and
380583 (from abc@gmail.com)
381584 (and
382585 (header X-List mylist.gmail.com)
@@ -398,7 +601,7 @@ export default class Condition {
398601 } ) ) . toBe ( true )
399602 } )
400603 it ( 'Does not match custom header with incorrect data' , ( ) => {
401- expect ( test_cond ( `(header MyHeader abc)` ,
604+ expect ( test_cond_headers ( `(header MyHeader abc)` ,
402605 {
403606 getHeader : ( name : string ) => {
404607 if ( name === 'MyHeader' ) {
0 commit comments