* Builds a Gmail search query string from the source config. * Combines the user's custom query with the label and date range filters. * When multiple labels are provided, they are OR-joined: `(label:A OR label:B)`.
(sourceConfig: Record<string, unknown>)
| 65 | * When multiple labels are provided, they are OR-joined: `(label:A OR label:B)`. |
| 66 | */ |
| 67 | function buildSearchQuery(sourceConfig: Record<string, unknown>): string { |
| 68 | const parts: string[] = [] |
| 69 | |
| 70 | const labelNames = parseMultiValue(sourceConfig.label) |
| 71 | if (labelNames.length === 1) { |
| 72 | const token = formatLabelToken(labelNames[0]) |
| 73 | if (token) parts.push(token) |
| 74 | } else if (labelNames.length > 1) { |
| 75 | const tokens = labelNames.map(formatLabelToken).filter(Boolean) |
| 76 | if (tokens.length === 1) { |
| 77 | parts.push(tokens[0]) |
| 78 | } else if (tokens.length > 1) { |
| 79 | parts.push(`(${tokens.join(' OR ')})`) |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | const dateRange = (sourceConfig.dateRange as string) || 'all' |
| 84 | const now = new Date() |
| 85 | switch (dateRange) { |
| 86 | case '7d': |
| 87 | parts.push(`after:${formatGmailDate(daysAgo(now, 7))}`) |
| 88 | break |
| 89 | case '30d': |
| 90 | parts.push(`after:${formatGmailDate(daysAgo(now, 30))}`) |
| 91 | break |
| 92 | case '90d': |
| 93 | parts.push(`after:${formatGmailDate(daysAgo(now, 90))}`) |
| 94 | break |
| 95 | case '6m': |
| 96 | parts.push(`after:${formatGmailDate(daysAgo(now, 180))}`) |
| 97 | break |
| 98 | case '1y': |
| 99 | parts.push(`after:${formatGmailDate(daysAgo(now, 365))}`) |
| 100 | break |
| 101 | } |
| 102 | |
| 103 | const excludePromotions = sourceConfig.excludePromotions !== 'false' |
| 104 | if (excludePromotions) { |
| 105 | parts.push('-category:promotions') |
| 106 | } |
| 107 | |
| 108 | const excludeSocial = sourceConfig.excludeSocial !== 'false' |
| 109 | if (excludeSocial) { |
| 110 | parts.push('-category:social') |
| 111 | } |
| 112 | |
| 113 | const customQuery = sourceConfig.query as string | undefined |
| 114 | const trimmedCustom = customQuery?.trim() |
| 115 | if (trimmedCustom) { |
| 116 | /** |
| 117 | * Wrap the user-supplied query in parentheses whenever it contains an OR |
| 118 | * so it's AND-joined as a single clause with the preceding label / category |
| 119 | * / date filters. Always wrap (rather than try to detect existing outer |
| 120 | * parens) because a regex like /^\(.*\)$/ misclassifies inputs such as |
| 121 | * `(from:alice) OR (from:bob)` where the parens don't bracket the whole |
| 122 | * expression. Double-wrapping is a no-op in Gmail search syntax. |
| 123 | */ |
| 124 | const needsGroup = /\bOR\b/i.test(trimmedCustom) |
no test coverage detected