-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Match on arbitrary header and value.
Based on Issue #36, allows matching on any header and value. ``` (header X-Custom-Header /value/) ``` As we use `GmailMessage.getHeader` function, the header name used in the condition must be case-sensitive. Overall flow: - Rules are parsed, and a list of condition-requested-headers is created. - ThreadData reads in the message data, and all the headers that were requested in the rules (we have to pass SessionData to ThreadData so it knows which headers to search). - Condition.match checks what header to match with, and compares the header's value with the rule's value. In order to get the SessionData.mock.ts to work, SessionData.labels had to be made public. This seems to be a limitation we will continue to hit in testing if we want to test on Sheet Apps, since we cannot use other better libraries.
- Loading branch information
Matt Diehl
committed
Aug 19, 2022
1 parent
4342985
commit 3e6fa8d
Showing
5 changed files
with
147 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,12 +15,14 @@ | |
*/ | ||
|
||
import {MessageData} from './ThreadData'; | ||
import {SessionData} from './SessionData'; | ||
import Mocks from './Mocks'; | ||
import Utils from './utils'; | ||
|
||
const RE_FLAG_PATTERN = /^\/(.*)\/([gimuys]*)$/; | ||
|
||
enum ConditionType { | ||
AND, OR, NOT, SUBJECT, FROM, TO, CC, BCC, LIST, SENDER, RECEIVER, BODY, | ||
AND, OR, NOT, SUBJECT, FROM, TO, CC, BCC, LIST, SENDER, RECEIVER, BODY, HEADER, | ||
} | ||
|
||
/** | ||
|
@@ -65,7 +67,7 @@ export default class Condition { | |
return pattern.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); | ||
} | ||
|
||
private static parseRegExp(pattern: string, condition_str: string, matching_address: boolean): RegExp { | ||
public static parseRegExp(pattern: string, condition_str: string, matching_address: boolean): RegExp { | ||
Utils.assert(pattern.length > 0, `Condition ${condition_str} should have value but not found`); | ||
const match = pattern.match(RE_FLAG_PATTERN); | ||
if (match !== null) { | ||
|
@@ -85,6 +87,7 @@ export default class Condition { | |
} | ||
|
||
private readonly type: ConditionType; | ||
private readonly subtype: string; | ||
private readonly regexp: RegExp; | ||
private readonly sub_conditions: Condition[]; | ||
|
||
|
@@ -94,8 +97,9 @@ export default class Condition { | |
`Condition ${condition_str} should be surrounded by ().`); | ||
const first_space = condition_str.indexOf(" "); | ||
const type_str = condition_str.substring(1, first_space).trim().toUpperCase(); | ||
const rest_str = condition_str.substring(first_space + 1, condition_str.length - 1).trim(); | ||
let rest_str = condition_str.substring(first_space + 1, condition_str.length - 1).trim(); | ||
this.type = ConditionType[type_str as keyof typeof ConditionType]; | ||
this.subtype = ""; | ||
switch (this.type) { | ||
case ConditionType.AND: | ||
case ConditionType.OR: { | ||
|
@@ -119,6 +123,13 @@ export default class Condition { | |
this.regexp = Condition.parseRegExp(rest_str, condition_str, true); | ||
break; | ||
} | ||
case ConditionType.HEADER: { | ||
const subtype_first_space = rest_str.indexOf(" "); | ||
this.subtype = rest_str.substring(0, subtype_first_space).trim(); | ||
rest_str = rest_str.substring(subtype_first_space + 1, rest_str.length - 1).trim(); | ||
this.regexp = Condition.parseRegExp(rest_str, condition_str, false); | ||
break; | ||
} | ||
case ConditionType.SUBJECT: | ||
case ConditionType.BODY: { | ||
this.regexp = Condition.parseRegExp(rest_str, condition_str, false); | ||
|
@@ -177,6 +188,13 @@ export default class Condition { | |
case ConditionType.BODY: { | ||
return this.regexp.test(message_data.body); | ||
} | ||
case ConditionType.HEADER: { | ||
const headerData = message_data.headers.get(this.subtype); | ||
if (headerData !== undefined) { | ||
return this.regexp.test(headerData); | ||
} | ||
return false; | ||
} | ||
} | ||
} | ||
|
||
|
@@ -191,6 +209,17 @@ export default class Condition { | |
return `(${type_str} ${regexp_str} ${sub_str})`; | ||
} | ||
|
||
getConditionHeaders(): string[] { | ||
const headers = []; | ||
if (this.type === ConditionType.HEADER) { | ||
headers.push(this.subtype); | ||
} | ||
this.sub_conditions?.forEach((sub_condition) => { | ||
headers.push(...sub_condition.getConditionHeaders()); | ||
}); | ||
return headers; | ||
} | ||
|
||
public static testRegex(it: Function, expect: Function) { | ||
|
||
function test_regexp(condition_str: string, target_str: string, is_address: boolean) { | ||
|
@@ -271,11 +300,18 @@ export default class Condition { | |
getSubject: () => '', | ||
getPlainBody: () => '', | ||
getRawContent: () => '', | ||
getHeader: (_name: string) => '', | ||
} as GoogleAppsScript.Gmail.GmailMessage; | ||
|
||
function test_cond(condition_str: string, message: Partial<GoogleAppsScript.Gmail.GmailMessage>): boolean { | ||
function test_cond( | ||
condition_str: string, | ||
message: Partial<GoogleAppsScript.Gmail.GmailMessage>, | ||
session_data: Partial<SessionData> = {}): boolean { | ||
const condition = new Condition(condition_str); | ||
const message_data = new MessageData(Object.assign({}, base_message, message)); | ||
const mock_session_data = Mocks.getMockSessionData(session_data); | ||
const message_data = new MessageData( | ||
mock_session_data, | ||
Object.assign({}, base_message, message)); | ||
return condition.match(message_data); | ||
} | ||
|
||
|
@@ -324,5 +360,56 @@ export default class Condition { | |
getTo: () => '[email protected]', | ||
})).toBe(true) | ||
}) | ||
|
||
it('Matches custom header with value', () => { | ||
expect(test_cond(`(header Sender [email protected])`, | ||
{ | ||
getHeader: (name: string) => { | ||
if (name === 'Sender') { | ||
return '[email protected]'; | ||
} | ||
return ''; | ||
}, | ||
}, | ||
{ | ||
requested_headers: ['Sender', 'List-Post'], | ||
})).toBe(true) | ||
}) | ||
it('Matches nested custom header with value', () => { | ||
expect(test_cond(`(and | ||
(from [email protected]) | ||
(and | ||
(header X-List mylist.gmail.com) | ||
(header Precedence /list/i)))`, | ||
{ | ||
getFrom: () => 'DDD EEE <[email protected]>', | ||
getHeader: (name: string) => { | ||
if (name === 'X-List') { | ||
return 'mylist.gmail.com'; | ||
} | ||
if (name === 'Precedence') { | ||
return 'bills list'; | ||
} | ||
return ''; | ||
}, | ||
}, | ||
{ | ||
requested_headers: ['X-List', 'Precedence'], | ||
})).toBe(true) | ||
}) | ||
it('Does not match custom header with incorrect data', () => { | ||
expect(test_cond(`(header MyHeader abc)`, | ||
{ | ||
getHeader: (name: string) => { | ||
if (name === 'MyHeader') { | ||
return 'xyz'; | ||
} | ||
return ''; | ||
}, | ||
}, | ||
{ | ||
requested_headers: ['MyHeader'], | ||
})).toBe(false) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,6 +35,10 @@ export class Rule { | |
return this.condition.toString(); | ||
} | ||
|
||
getConditionHeaders(): string[] { | ||
return this.condition.getConditionHeaders(); | ||
} | ||
|
||
private static parseBooleanValue(str: string): boolean { | ||
if (str.length === 0) { | ||
return false; | ||
|
@@ -86,6 +90,15 @@ export class Rule { | |
return result; | ||
} | ||
|
||
public static getConditionHeaders(rules: Rule[]): string[] { | ||
const headers: Set<string> = new Set<string>(); | ||
rules.forEach((rule) => { | ||
const rule_headers = rule.getConditionHeaders(); | ||
rule_headers.forEach(item => headers.add(item)) | ||
}); | ||
return Array.from(headers.values()) | ||
} | ||
|
||
public static parseRules(values: string[][]): Rule[] { | ||
const row_num = values.length; | ||
const column_num = values[0].length; | ||
|
@@ -206,10 +219,12 @@ export class Rule { | |
}]); | ||
|
||
const rules = Rule.parseRules(sheet); | ||
const condition_headers = Rule.getConditionHeaders(rules); | ||
|
||
expect(rules.length).toBe(1); | ||
expect(rules[0].stage).toBe(5); | ||
expect(rules[0].thread_action.label_names.size).toBe(2); | ||
expect(condition_headers).toEqual([]); | ||
}) | ||
|
||
it('Loaded Rules are sorted by stage', () => { | ||
|
@@ -239,5 +254,34 @@ export class Rule { | |
expect(rules[2].stage).toBe(15); | ||
}) | ||
|
||
it('Loads rules with Headers', () => { | ||
const sheet = Mocks.getMockTestSheet([ | ||
{ | ||
conditions: '(and (or (header Test1 /abc/i)' + | ||
' (header h2 [email protected]))' + | ||
' (and' + | ||
' (header X-List abcde)' + | ||
' (header h3 /abcde/)))', | ||
add_labels: 'def, uvw', | ||
stage: "10", | ||
}, | ||
{ | ||
conditions: '(header h5 /asdf/)', | ||
add_labels: 'abc', | ||
stage: "15", | ||
} | ||
]); | ||
|
||
const rules = Rule.parseRules(sheet); | ||
const condition_headers = Rule.getConditionHeaders(rules); | ||
|
||
expect(rules.length).toBe(2); | ||
expect(rules[0].stage).toBe(10); | ||
expect(rules[0].thread_action.label_names).toEqual(new Set(['def', 'uvw'])); | ||
expect(rules[1].stage).toBe(15); | ||
expect(rules[1].thread_action.label_names).toEqual(new Set(['abc'])); | ||
expect(condition_headers).toEqual( | ||
['Test1', 'h2', 'X-List', 'h3', 'h5']); | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters