-
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 5, 2022
1 parent
dc4ba36
commit 42b0114
Showing
7 changed files
with
225 additions
and
16 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 |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import {SessionData} from "SessionData"; | ||
import {Config} from "Config"; | ||
|
||
|
||
export default class Mocks { | ||
|
||
private static base_config: Config = { | ||
auto_labeling_parent_label: "", | ||
go_link: "", | ||
hour_of_day_to_run_sanity_checking: 0, | ||
max_threads: 50, | ||
processed_label: "myProcessed", | ||
processing_failed_label: "zFailed", | ||
processing_frequency_in_minutes: 5, | ||
unprocessed_label: "myUnprocessed", | ||
}; | ||
|
||
public static getMockConfig = (overrides: Partial<Config> = {}) => ( | ||
Object.assign({}, Mocks.base_config, overrides) | ||
); | ||
|
||
private static base_session_data: SessionData = { | ||
user_email: "[email protected]", | ||
config: Mocks.getMockConfig(), | ||
labels: {}, | ||
rules: [], | ||
requested_headers: [], | ||
processing_start_time: new Date(12345), | ||
oldest_to_process: new Date(23456), | ||
getOrCreateLabel: () => ({} as GoogleAppsScript.Gmail.GmailLabel), | ||
}; | ||
|
||
public static getMockSessionData = (overrides: Partial<SessionData> = {}) => ( | ||
Object.assign({}, Mocks.base_session_data, overrides) | ||
); | ||
} |
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 |
---|---|---|
|
@@ -34,6 +34,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; | ||
|
@@ -85,6 +89,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()) | ||
} | ||
|
||
private static parseRules(values: string[][]): Rule[] { | ||
const row_num = values.length; | ||
const column_num = values[0].length; | ||
|
@@ -150,6 +163,9 @@ export class Rule { | |
public static getRules(): Rule[] { | ||
const values: string[][] = Utils.withTimer("GetRuleValues", () => { | ||
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('rules'); | ||
if(sheet === null) { | ||
throw "Active sheet 'rules' not found"; | ||
} | ||
const column_num = sheet.getLastColumn(); | ||
const row_num = sheet.getLastRow(); | ||
return sheet.getRange(1, 1, row_num, column_num) | ||
|
@@ -216,21 +232,81 @@ export class Rule { | |
}) | ||
|
||
it('Loads Simple Rule', () => { | ||
const row: string[] = new_row({ | ||
conditions: '(body /to: me/i)', | ||
add_labels: 'abc, xyz', | ||
stage: "5", | ||
}); | ||
const sheet: string[][] = [ | ||
headers, | ||
row, | ||
new_row({ | ||
conditions: '(body /to: me/i)', | ||
add_labels: 'abc, xyz', | ||
stage: "5", | ||
}), | ||
] | ||
|
||
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', () => { | ||
const sheet: string[][] = [ | ||
headers, | ||
new_row({ | ||
conditions: '(body /to: me/i)', | ||
add_labels: 'abc, xyz', | ||
stage: "5", | ||
}), | ||
new_row({ | ||
conditions: '(body /to: me/i)', | ||
add_labels: 'abc, xyz', | ||
stage: "15", | ||
}), | ||
new_row({ | ||
conditions: '(body /to: me/i)', | ||
add_labels: 'abc, xyz', | ||
stage: "1", | ||
}), | ||
] | ||
|
||
const rules = Rule.parseRules(sheet); | ||
|
||
expect(rules.length).toBe(3); | ||
expect(rules[0].stage).toBe(1); | ||
expect(rules[1].stage).toBe(5); | ||
expect(rules[2].stage).toBe(15); | ||
}) | ||
|
||
it('Loads rules with Headers', () => { | ||
const sheet: string[][] = [ | ||
headers, | ||
new_row({ | ||
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", | ||
}), | ||
new_row({ | ||
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
Oops, something went wrong.