-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Fix outlook cold email #1002
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix outlook cold email #1002
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -150,4 +150,3 @@ try { | |
| console.error("Error running check:", error.message); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1182,22 +1182,66 @@ export class OutlookProvider implements EmailProvider { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| messageId: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }): Promise<boolean> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const response: { value: Message[] } = await this.client | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .getClient() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .api("/me/messages") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .filter( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `(toRecipients/any(a:a/emailAddress/address eq '${escapeODataString(options.from)}') and sentDateTime lt ${options.date.toISOString()}) or (from/emailAddress/address eq '${escapeODataString(options.from)}' and receivedDateTime lt ${options.date.toISOString()})`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .top(2) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .select("id") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .get(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const escapedFrom = escapeODataString(options.from); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const dateString = options.date.toISOString(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Split into two parallel queries to avoid OData "invalid nodes" error | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // when combining any() lambda with other filters. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const receivedFilter = `from/emailAddress/address eq '${escapedFrom}' and receivedDateTime lt ${dateString}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Use $search for sent messages as $filter on toRecipients is unreliable | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // We escape double quotes for the KQL search query | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const escapedSearchFrom = options.from | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .replace(/\\/g, "\\\\") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .replace(/"/g, '\\"'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const sentSearch = `"to:${escapedSearchFrom}"`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [sentResponse, receivedResponse] = await Promise.all([ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.client | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
macroscopeapp[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .getClient() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .api("/me/messages") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .search(sentSearch) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
elie222 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .top(5) // Increase top to account for potential future messages we filter out | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .select("id,sentDateTime") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .get() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .catch((error) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.logger.error("Error checking sent messages", { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| search: sentSearch, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { value: [] }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1199
to
+1213
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainCritical: The sent messages query fetches only the top 5 results without date filtering (since This breaks the cold email detection logic: the method would incorrectly return Apply this diff to significantly increase the limit and reduce false negative risk: - .top(5) // Increase top to account for potential future messages we filter out
+ .top(50) // Fetch more results to account for future messages filtered out client-sideNote: The ideal solution would be to use Verify whether 🌐 Web query: 💡 Result: Short answer: you can't reliably use $filter on toRecipients. Microsoft Graph does not support filtering on toRecipients/ccRecipients/bccRecipients (they're collection-of-complex-types), so use $search (e.g. ?$search="to:[[email protected]]" or ?$search="participants:[[email protected]]") or retrieve and filter client-side. [1][2][3] Notes/limits:
Sources:
Critical: The sent messages query fetches only the top 5 results using This breaks the cold email detection logic: the method would incorrectly return Apply this diff to significantly increase the limit and reduce false negative risk: - .top(5) // Increase top to account for potential future messages we filter out
+ .top(50) // Fetch more results to account for future messages filtered out client-sideNote: Microsoft Graph does not support 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.client | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .getClient() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .api("/me/messages") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .filter(receivedFilter) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .top(2) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .select("id") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .get() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .catch((error) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.logger.error("Error checking received messages", { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| filter: receivedFilter, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { value: [] }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1207
to
+1228
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Error handling silently masks failures, risking false negatives. Both queries return Consider re-throwing errors or logging warnings more prominently, so that failures are visible and can be investigated rather than silently treated as "no prior communication." Apply this diff to improve error visibility: .catch((error) => {
this.logger.error("Error checking sent messages", {
error,
search: sentSearch,
});
+ // Re-throw on non-transient errors to avoid silent false negatives
+ // For now, return empty to maintain existing behavior but consider revisiting
return { value: [] };
}),
this.client
.getClient()
.api("/me/messages")
.filter(receivedFilter)
.top(2)
.select("id")
.get()
.catch((error) => {
this.logger.error("Error checking received messages", {
error,
filter: receivedFilter,
});
+ // Re-throw on non-transient errors to avoid silent false negatives
+ // For now, return empty to maintain existing behavior but consider revisiting
return { value: [] };
}),Alternatively, add monitoring/alerting on these error logs to ensure failures are investigated. 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Filter sent messages by date since $search doesn't support date filtering well | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const validSentMessages = (sentResponse.value || []).filter( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (msg: Message) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!msg.sentDateTime) return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return new Date(msg.sentDateTime) < options.date; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // If we have any outgoing emails, or any incoming emails (excluding current message) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const hasPreviousEmail = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| response.value.length > 0 && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| response.value.some((message) => message.id !== options.messageId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const messages = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...validSentMessages, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...(receivedResponse.value || []), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return hasPreviousEmail; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return messages.some((message) => message.id !== options.messageId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.logger.error("Error checking previous communications", { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1 @@ | ||
| v2.20.18 | ||
| v2.20.19 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Verify KQL escaping completeness and "to:" syntax reliability.
The manual escaping for the KQL search query only handles backslashes and double quotes. KQL supports additional special characters and operators (parentheses, AND, OR, NOT, wildcards, etc.) that could appear in email addresses or display names. While wrapping the query in double quotes provides some protection, edge cases like
user(name)@example.comor emails containing KQL keywords might still cause unexpected behavior.Additionally, verify that the
to:property filter syntax is reliable for the Microsoft Graph $search API, as the previous comment mentions that$filterontoRecipientsis unreliable.Consider running this verification to check if the current escaping handles various email formats correctly:
Alternatively, search the web for Microsoft Graph KQL search syntax requirements:
🏁 Script executed:
Length of output: 3108
🌐 Web query:
💡 Result:
Short answer — follow the Graph docs: put each KQL clause inside double quotes, escape any double-quote or backslash characters with a backslash, and URL‑encode any other special characters in the clause (e.g. @ → %40, + → %2B). Also include ConsistencyLevel: eventual when needed. Example requests:
Single clause (URL-encoded @):
GET https://graph.microsoft.com/v1.0/me/messages?$search="from:someone%40contoso.com". (learn.microsoft.com)
If your clause contains a literal double-quote or backslash, escape them with a backslash inside the quoted clause (per docs). (learn.microsoft.com)
Notes:
If you want, tell me an example email or full request you’re trying and I’ll give the exact encoded $search string.
Citations:
Length of output: 34681
🏁 Script executed:
Length of output: 1784
🌐 Web query:
💡 Result:
Short answer: .search() does not perform URL percent-encoding for you — it just sets the $search value you pass onto the request. You must URL‑encode (and in some cases double‑encode) any reserved characters (e.g. & , spaces, parentheses, quotes) before calling .search(). See Microsoft Graph query-parameter encoding guidance and the client docs showing .search() simply adds the string. [1][2][3]
Quick guidance:
Sources:
[1] Microsoft Graph query-parameter encoding and $search notes.
[2] @microsoft/microsoft-graph-client npm docs (query / $search behavior).
[3] jsDocs for @microsoft/microsoft-graph-client showing .search() behavior.
URL-encode email addresses in KQL $search queries—current code will fail for all searches.
Per Microsoft Graph client documentation,
.search()does not perform URL percent-encoding automatically; you must URL-encode special characters like @ before passing the value to.search(). Email addresses always contain@, which must be encoded as%40.The current implementation (lines 1194–1197) only escapes backslashes and double quotes but does NOT URL-encode the email address. This will cause KQL syntax errors at runtime for every search, resulting in the error handler silently returning an empty array and causing
hasPreviousCommunicationsWithSenderOrDomainto incorrectly return false.Fix: Apply URL encoding (e.g.,
encodeURIComponent()) to the email address after escaping quotes and backslashes, before constructing the KQL search query.🤖 Prompt for AI Agents