From 3e6fa8d860390e60d56b34283612bb0e0b1cc3a9 Mon Sep 17 00:00:00 2001 From: Matt Diehl Date: Tue, 2 Aug 2022 22:31:59 -0700 Subject: [PATCH 1/2] 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. --- Condition.ts | 97 +++++++++++++++++++++++++++++++++++++++++++++++--- Mocks.ts | 1 + Rule.ts | 44 +++++++++++++++++++++++ SessionData.ts | 4 ++- ThreadData.ts | 9 +++-- 5 files changed, 147 insertions(+), 8 deletions(-) diff --git a/Condition.ts b/Condition.ts index c4324bb..05c1023 100644 --- a/Condition.ts +++ b/Condition.ts @@ -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): boolean { + function test_cond( + condition_str: string, + message: Partial, + session_data: Partial = {}): 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: () => 'abc+Def@bar.com', })).toBe(true) }) + + it('Matches custom header with value', () => { + expect(test_cond(`(header Sender abc@def.com)`, + { + getHeader: (name: string) => { + if (name === 'Sender') { + return 'abc@def.com'; + } + return ''; + }, + }, + { + requested_headers: ['Sender', 'List-Post'], + })).toBe(true) + }) + it('Matches nested custom header with value', () => { + expect(test_cond(`(and + (from abc@gmail.com) + (and + (header X-List mylist.gmail.com) + (header Precedence /list/i)))`, + { + getFrom: () => 'DDD EEE ', + 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) + }) } } diff --git a/Mocks.ts b/Mocks.ts index 63e32b4..9049115 100644 --- a/Mocks.ts +++ b/Mocks.ts @@ -24,6 +24,7 @@ export default class Mocks { config: Mocks.getMockConfig(), labels: {}, rules: [], + requested_headers: [], processing_start_time: new Date(12345), oldest_to_process: new Date(23456), getOrCreateLabel: () => ({} as GoogleAppsScript.Gmail.GmailLabel), diff --git a/Rule.ts b/Rule.ts index a8b9555..02b7291 100644 --- a/Rule.ts +++ b/Rule.ts @@ -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 = new Set(); + 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 xyz@gmail.com))' + + ' (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']); + }) } } diff --git a/SessionData.ts b/SessionData.ts index 5deb86f..a382e29 100644 --- a/SessionData.ts +++ b/SessionData.ts @@ -32,6 +32,7 @@ export class SessionData { public readonly config: Config; public readonly labels: { [key: string]: GoogleAppsScript.Gmail.GmailLabel }; public readonly rules: Rule[]; + public readonly requested_headers: string[]; public readonly processing_start_time: Date; public readonly oldest_to_process: Date; @@ -41,6 +42,7 @@ export class SessionData { this.config = Utils.withTimer("getConfigs", () => Config.getConfig()); this.labels = Utils.withTimer("getLabels", () => SessionData.getLabelMap()); this.rules = Utils.withTimer("getRules", () => Rule.getRules()); + this.requested_headers = Utils.withTimer("getHeaders", () => Rule.getConditionHeaders(this.rules)); this.processing_start_time = new Date(); // Check back two processing intervals to make sure we checked all messages in the thread @@ -48,7 +50,7 @@ export class SessionData { this.processing_start_time.getTime() - 2 * this.config.processing_frequency_in_minutes * 60 * 1000); } - getOrCreateLabel(name: string) { + getOrCreateLabel(name: string): GoogleAppsScript.Gmail.GmailLabel { name = name.trim(); Utils.assert(name.length > 0, "Can't get empty label!"); diff --git a/ThreadData.ts b/ThreadData.ts index dc5297e..4fd0bf9 100644 --- a/ThreadData.ts +++ b/ThreadData.ts @@ -68,8 +68,9 @@ export class MessageData { public readonly receivers: string[]; public readonly subject: string; public readonly body: string; + public readonly headers: Map; - constructor(message: GoogleAppsScript.Gmail.GmailMessage) { + constructor(session_data: SessionData, message: GoogleAppsScript.Gmail.GmailMessage) { this.from = message.getFrom(); this.to = MessageData.parseAddresses(message.getTo()); this.cc = MessageData.parseAddresses(message.getCc()); @@ -79,6 +80,10 @@ export class MessageData { this.sender = ([] as string[]).concat(this.from, this.reply_to); this.receivers = ([] as string[]).concat(this.to, this.cc, this.bcc, this.list); this.subject = message.getSubject(); + this.headers = new Map(); + session_data.requested_headers.forEach(header => { + this.headers.set(header, message.getHeader(header)); + }); // Potentially could be HTML, Plain, or RAW. But doesn't seem very useful other than Plain. let body = message.getPlainBody(); // Truncate and log long messages. @@ -111,7 +116,7 @@ export class ThreadData { if (newMessages.length === 0) { newMessages = [messages[messages.length - 1]]; } - this.message_data_list = newMessages.map(message => new MessageData(message)); + this.message_data_list = newMessages.map(message => new MessageData(session_data, message)); // Log if any dropped. const numDropped = messages.length - newMessages.length; From e01bfc7edb6b5aa3cef3d81505f93b78900a5057 Mon Sep 17 00:00:00 2001 From: Matt Diehl Date: Sat, 6 Aug 2022 13:27:20 -0700 Subject: [PATCH 2/2] Allow matching message thread details. Create rules that can match an email thread's details. Thread details include: - label - is_important - is_in_inbox - is_in_priority_inbox - is_in_spam - is_in_trash - is_starred - is_unread - first_message_subject For example, an email thread may already exist in the labels: - ABC - ABC/xyz - def When a new email is received that is for the same thread, the following rule can be used: (thread label ABC/xyz) Or you can check if an email thread is marked 'important': (thread is_important) Or you can check what the original thread subjectline was: (thread first_message_subject /This has no RE: in it/i) --- Condition.ts | 257 ++++++++++++++++++++++++++++++++++++++++++++------ ThreadData.ts | 22 +++++ 2 files changed, 252 insertions(+), 27 deletions(-) diff --git a/Condition.ts b/Condition.ts index 05c1023..df01008 100644 --- a/Condition.ts +++ b/Condition.ts @@ -22,16 +22,26 @@ import Utils from './utils'; const RE_FLAG_PATTERN = /^\/(.*)\/([gimuys]*)$/; enum ConditionType { - AND, OR, NOT, SUBJECT, FROM, TO, CC, BCC, LIST, SENDER, RECEIVER, BODY, HEADER, + AND, OR, NOT, SUBJECT, FROM, TO, CC, BCC, LIST, SENDER, RECEIVER, BODY, HEADER, THREAD +} + +enum ThreadSubType { + FIRST_MESSAGE_SUBJECT, LABEL, IS_STARRED, IS_IMPORTANT, IS_IN_INBOX, IS_IN_PRIORITY_INBOX, + IS_IN_SPAM, IS_IN_TRASH, IS_UNREAD } /** * S expression represents condition in rule. * * Syntax: - * CONDITION_EXP := (OPERATOR CONDITION_LIST) | (MATCHER STRING) + * CONDITION_EXP := (OPERATOR CONDITION_LIST) | (MATCHER STRING) | + * (MATCHER_SUBTYPE SUBTYPE_STRING STRING) | (MATCHER_SUBTYPE SUBTYPE_BOOL) * OPERATOR := and | or | not - * MATCHER := subject | from | to | cc | bcc | list | sender | receiver | content + * MATCHER := subject | from | to | cc | bcc | list | sender | receiver | body + * MATCHER_SUBTYPE := header | thread + * SUBTYPE_STRING := first_message_subject | label | STRING + * SUBTYPE_BOOL := is_starred | is_important | is_in_inbox | is_in_priority_inbox | + * is_in_spam | is_in_trash | is_unread * CONDITION_LIST := CONDITION_EXP | CONDITION_EXP CONDITION_LIST */ export default class Condition { @@ -87,7 +97,8 @@ export default class Condition { } private readonly type: ConditionType; - private readonly subtype: string; + private readonly header: string; + private readonly threadSubtype: ThreadSubType; private readonly regexp: RegExp; private readonly sub_conditions: Condition[]; @@ -99,7 +110,6 @@ export default class Condition { const type_str = condition_str.substring(1, first_space).trim().toUpperCase(); 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: { @@ -123,11 +133,27 @@ export default class Condition { this.regexp = Condition.parseRegExp(rest_str, condition_str, true); break; } - case ConditionType.HEADER: { + case ConditionType.HEADER: + case ConditionType.THREAD: { 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); + const subtype = subtype_first_space > 0 + ? rest_str.substring(0, subtype_first_space).trim() + : rest_str; + let matching_address = true; + if (this.type === ConditionType.HEADER) { + this.header = subtype; + } + if (this.type === ConditionType.THREAD) { + this.threadSubtype = ThreadSubType[subtype.toUpperCase() as keyof typeof ThreadSubType]; + if (this.threadSubtype === ThreadSubType.FIRST_MESSAGE_SUBJECT) { + matching_address = false; + } + if (this.threadSubtype === undefined) { + throw `Invalid 'thread' subtype: "${condition_str}"`; + } + } + rest_str = rest_str.substring(subtype_first_space + 1, rest_str.length).trim(); + this.regexp = Condition.parseRegExp(rest_str, condition_str, matching_address); break; } case ConditionType.SUBJECT: @@ -189,12 +215,43 @@ export default class Condition { return this.regexp.test(message_data.body); } case ConditionType.HEADER: { - const headerData = message_data.headers.get(this.subtype); + const headerData = message_data.headers.get(this.header); if (headerData !== undefined) { return this.regexp.test(headerData); } return false; } + case ConditionType.THREAD: { + switch (this.threadSubtype) { + case ThreadSubType.IS_IMPORTANT: { + return message_data.thread_is_important; + } + case ThreadSubType.IS_IN_INBOX: { + return message_data.thread_is_in_inbox; + } + case ThreadSubType.IS_IN_PRIORITY_INBOX: { + return message_data.thread_is_in_priority_inbox; + } + case ThreadSubType.IS_IN_SPAM: { + return message_data.thread_is_in_spam; + } + case ThreadSubType.IS_IN_TRASH: { + return message_data.thread_is_in_trash; + } + case ThreadSubType.IS_STARRED: { + return message_data.thread_is_starred; + } + case ThreadSubType.IS_UNREAD: { + return message_data.thread_is_unread; + } + case ThreadSubType.FIRST_MESSAGE_SUBJECT: { + return this.regexp.test(message_data.thread_first_message_subject); + } + case ThreadSubType.LABEL: { + return this.matchAddress(...message_data.thread_labels); + } + } + } } } @@ -212,7 +269,7 @@ export default class Condition { getConditionHeaders(): string[] { const headers = []; if (this.type === ConditionType.HEADER) { - headers.push(this.subtype); + headers.push(this.header); } this.sub_conditions?.forEach((sub_condition) => { headers.push(...sub_condition.getConditionHeaders()); @@ -291,27 +348,19 @@ export default class Condition { } public static testConditionParsing(it: Function, expect: Function) { - const base_message = { - getFrom: () => '', - getTo: () => '', - getCc: () => '', - getBcc: () => '', - getReplyTo: () => '', - getSubject: () => '', - getPlainBody: () => '', - getRawContent: () => '', - getHeader: (_name: string) => '', - } as GoogleAppsScript.Gmail.GmailMessage; function test_cond( condition_str: string, message: Partial, - session_data: Partial = {}): boolean { + thread: Partial = {}, + session_data: Partial = {}, + thread_labels: string[] = []): boolean { const condition = new Condition(condition_str); + const mock_message = Mocks.getMockMessage(message, thread, thread_labels); const mock_session_data = Mocks.getMockSessionData(session_data); const message_data = new MessageData( mock_session_data, - Object.assign({}, base_message, message)); + mock_message); return condition.match(message_data); } @@ -360,9 +409,163 @@ export default class Condition { getTo: () => 'abc+Def@bar.com', })).toBe(true) }) + it('Matches body using case-sensitivity', () => { + expect(test_cond(`(body with aSdF)`, + { + getPlainBody: () => 'Text with aSdF in it', + })).toBe(true) + }) + it('Does not match body using case-sensitivity', () => { + expect(test_cond(`(body asdf)`, + { + getPlainBody: () => 'Text with aSdF in it', + })).toBe(false) + }) + + function test_cond_labels( + condition_str: string, + thread_labels: string[]): boolean { + return test_cond(condition_str, {}, {}, {}, thread_labels); + } + + it('Matches email that is in label, case-insensitive', () => { + expect(test_cond_labels(`(thread label xyz)`, + ['ABC', 'XYZ', 'ABC/XYZ'])).toBe(true) + }) + it('Does not match email that is in label with partial name', () => { + expect(test_cond_labels(`(thread label XY)`, + ['ABC', 'XYZ', 'ABC/XYZ'])).toBe(false) + }) + it('Does not match email that is in label without full name', () => { + expect(test_cond_labels(`(thread label XYZ)`, + ['ABC/XYZ'])).toBe(false) + }) + it('Matches email that is in label with full name', () => { + expect(test_cond_labels(`(thread label ABC/XYZ)`, + ['ABC/XYZ'])).toBe(true) + }) + + function test_cond_thread( + condition_str: string, + thread: Partial): boolean { + return test_cond(condition_str, {}, thread); + } + + it('Throws exception if thread subtype is invalid', () => { + expect(() => {test_cond_thread(`(thread is_made_up)`, {})}).toThrow() + }) + it('Matches thread is_important if is', () => { + expect(test_cond_thread(`(thread is_important)`, + { + isImportant: () => true, + })).toBe(true) + }) + it('Does not match thread is_important if it is not', () => { + expect(test_cond_thread(`(thread is_important)`, + { + isImportant: () => false, + })).toBe(false) + }) + it('Matches thread is_in_inbox if is', () => { + expect(test_cond_thread(`(thread is_in_inbox)`, + { + isInInbox: () => true, + })).toBe(true) + }) + it('Does not match thread is_in_inbox if it is not', () => { + expect(test_cond_thread(`(thread is_in_inbox)`, + { + isInInbox: () => false, + })).toBe(false) + }) + it('Matches thread is_in_priority_inbox if is', () => { + expect(test_cond_thread(`(thread is_in_priority_inbox)`, + { + isInPriorityInbox: () => true, + })).toBe(true) + }) + it('Does not match thread is_in_priority_inbox if it is not', () => { + expect(test_cond_thread(`(thread is_in_priority_inbox)`, + { + isInPriorityInbox: () => false, + })).toBe(false) + }) + it('Matches thread is_in_spam if is', () => { + expect(test_cond_thread(`(thread is_in_spam)`, + { + isInSpam: () => true, + })).toBe(true) + }) + it('Does not match thread is_in_spam if it is not', () => { + expect(test_cond_thread(`(thread is_in_spam)`, + { + isInSpam: () => false, + })).toBe(false) + }) + it('Matches thread is_in_trash if it is', () => { + expect(test_cond_thread(`(thread is_in_trash)`, + { + isInTrash: () => true, + })).toBe(true) + }) + it('Does not match thread is_in_trash if it is not', () => { + expect(test_cond_thread(`(thread is_in_trash)`, + { + isInTrash: () => false, + })).toBe(false) + }) + it('Matches thread is_starred if it is', () => { + expect(test_cond_thread(`(thread is_starred)`, + { + hasStarredMessages: () => true, + })).toBe(true) + }) + it('Does not match thread is_starred if it is not', () => { + expect(test_cond_thread(`(thread is_starred)`, + { + hasStarredMessages: () => false, + })).toBe(false) + }) + it('Matches thread is_unread if it is', () => { + expect(test_cond_thread(`(thread is_unread)`, + { + isUnread: () => true, + })).toBe(true) + }) + it('Does not match thread is_unread if it is not', () => { + expect(test_cond_thread(`(thread is_unread)`, + { + isUnread: () => false, + })).toBe(false) + }) + it('Matches thread first_message_subject with case-sensitivity', () => { + expect(test_cond_thread(`(thread first_message_subject this is IN subject)`, + { + getFirstMessageSubject: () => 'subject this is IN subjects', + })).toBe(true) + }) + it('Does not match thread first_message_subject with case-sensitivity', () => { + expect(test_cond_thread(`(thread first_message_subject this is IN subject)`, + { + getFirstMessageSubject: () => 'subject this is in subjects', + })).toBe(false) + }) + it('Matches thread first_message_subject with regex', () => { + expect(test_cond_thread(`(thread first_message_subject /teST Regex/i)`, + { + getFirstMessageSubject: () => 'RE: test regex subjectline', + })).toBe(true) + }) + + function test_cond_headers( + condition_str: string, + message: Partial, + session_data: Partial): boolean { + return test_cond(condition_str, message, {}, session_data); + } it('Matches custom header with value', () => { - expect(test_cond(`(header Sender abc@def.com)`, + expect(test_cond_headers(`(header Sender abc@def.com)`, { getHeader: (name: string) => { if (name === 'Sender') { @@ -376,7 +579,7 @@ export default class Condition { })).toBe(true) }) it('Matches nested custom header with value', () => { - expect(test_cond(`(and + expect(test_cond_headers(`(and (from abc@gmail.com) (and (header X-List mylist.gmail.com) @@ -398,7 +601,7 @@ export default class Condition { })).toBe(true) }) it('Does not match custom header with incorrect data', () => { - expect(test_cond(`(header MyHeader abc)`, + expect(test_cond_headers(`(header MyHeader abc)`, { getHeader: (name: string) => { if (name === 'MyHeader') { diff --git a/ThreadData.ts b/ThreadData.ts index 4fd0bf9..d6f2da3 100644 --- a/ThreadData.ts +++ b/ThreadData.ts @@ -69,6 +69,15 @@ export class MessageData { public readonly subject: string; public readonly body: string; public readonly headers: Map; + public readonly thread_labels: string[]; + public readonly thread_is_important: boolean; + public readonly thread_is_in_inbox: boolean; + public readonly thread_is_in_priority_inbox: boolean; + public readonly thread_is_in_spam: boolean; + public readonly thread_is_in_trash: boolean; + public readonly thread_is_starred: boolean; + public readonly thread_is_unread: boolean; + public readonly thread_first_message_subject: string; constructor(session_data: SessionData, message: GoogleAppsScript.Gmail.GmailMessage) { this.from = message.getFrom(); @@ -84,6 +93,19 @@ export class MessageData { session_data.requested_headers.forEach(header => { this.headers.set(header, message.getHeader(header)); }); + this.thread_labels = []; + const thread = message.getThread(); + thread.getLabels().forEach(label => { + this.thread_labels.push(label.getName()); + }); + this.thread_is_important = thread.isImportant(); + this.thread_is_in_inbox = thread.isInInbox(); + this.thread_is_in_priority_inbox = thread.isInPriorityInbox(); + this.thread_is_in_spam = thread.isInSpam(); + this.thread_is_in_trash = thread.isInTrash(); + this.thread_is_starred = thread.hasStarredMessages(); + this.thread_is_unread = thread.isUnread(); + this.thread_first_message_subject = thread.getFirstMessageSubject(); // Potentially could be HTML, Plain, or RAW. But doesn't seem very useful other than Plain. let body = message.getPlainBody(); // Truncate and log long messages.