Skip to content

Conversation

@elie222
Copy link
Owner

@elie222 elie222 commented Nov 19, 2025

When someone emails us from the same domain, but under a different email, we skip the cold email check.

A better way to handle this is to only search for previous emails from the domain in question, that aren't labelled cold email. This PR does that.

Needs to be checked more before it can be merged in. And code may need clean up too.

Summary by CodeRabbit

  • Tests

    • Added comprehensive test coverage for cold email exclusion logic across multiple email providers and rule configurations, including edge cases and fallback scenarios.
  • Refactor

    • Enhanced cold email detection to support label and folder-based exclusion when filtering previous communications.
    • Updated email provider interfaces to accept custom label and folder exclusion parameters for improved filtering capabilities.

@vercel
Copy link

vercel bot commented Nov 19, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
inbox-zero Error Error Nov 19, 2025 2:46pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 19, 2025

Walkthrough

This PR enhances cold email filtering by introducing exclusion parameters (excludeLabel and excludeFolder) to the email provider interface. The feature allows the isColdEmail logic to exclude emails with specific labels or in specific folders when checking for previous communications with senders, improving cold email detection accuracy across Gmail and Outlook providers.

Changes

Cohort / File(s) Summary
Core cold-email logic
apps/web/utils/cold-email/is-cold-email.ts
Updated coldEmailRule parameter type from Pick<Rule, "instructions"> | null to ColdEmailRule | null. Added extraction of action label and move type to determine exclusion strategy (LABEL vs MOVE_FOLDER), computing labelOrFolderName with "Cold Email" fallback, and passing appropriate excludeLabel or excludeFolder to previous email checks.
Email provider interface
apps/web/utils/email/types.ts
Extended hasPreviousCommunicationsWithSenderOrDomain options with two new optional fields: excludeLabel (label/category name) and excludeFolder (folder name for Outlook).
Gmail implementation
apps/web/utils/gmail/message.ts
Added excludeLabel support to hasPreviousCommunicationWithSender and hasPreviousCommunicationsWithSenderOrDomain. Constructs exclusion query fragments (e.g., -label:"excludeLabel") appended to both incoming and outgoing email search queries.
Outlook implementation
apps/web/utils/email/microsoft.ts
Extended hasPreviousCommunicationsWithSenderOrDomain to handle excludeLabel (category-based) and excludeFolder (folder ID-based) exclusions. Resolves folder names to IDs with fallback logging and applies exclusion filters to Graph API queries fetching up to 2 results.
Cold-email exclusion tests
apps/web/utils/cold-email/exclude-cold-emails.test.ts, apps/web/utils/cold-email/is-cold-email-label-exclusion.test.ts
Comprehensive test suites covering isColdEmail label and folder exclusion scenarios for Gmail and Outlook, including default label fallback, rule configuration variations, and previous email detection with exclusion.
Gmail label-exclusion tests
apps/web/utils/gmail/message-label-exclusion.test.ts
Tests verifying Gmail label exclusion in search queries, validating correct query construction, domain vs. email-based searches, custom label names, and result filtering.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant isColdEmail
    participant EmailProvider
    participant ExternalAPI

    Client->>isColdEmail: isColdEmail(email, coldEmailRule)
    
    Note over isColdEmail: Extract action label/folder<br/>from coldEmailRule
    
    alt Has label (LABEL action)
        isColdEmail->>EmailProvider: hasPreviousEmail(...,<br/>excludeLabel: label)
    else Has folder (MOVE_FOLDER action)
        isColdEmail->>EmailProvider: hasPreviousEmail(...,<br/>excludeFolder: folder)
    else No rule
        isColdEmail->>EmailProvider: hasPreviousEmail(...,<br/>excludeLabel: "Cold Email")
    end
    
    Note over EmailProvider: Build filtered query<br/>(exclude label/folder)
    EmailProvider->>ExternalAPI: Search with exclusion filter
    ExternalAPI-->>EmailProvider: Previous emails (filtered)
    
    EmailProvider-->>isColdEmail: Has prior communication?
    
    alt Prior non-cold email found
        isColdEmail-->>Client: false (known sender)
    else No prior emails
        isColdEmail->>Client: Trigger AI classifier
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Moderate logic density in is-cold-email.ts (action type detection, exclusion parameter routing)
  • Multiple file modifications following a consistent pattern (exclusion plumbing across Gmail/Outlook)
  • Areas requiring extra attention:
    • Logic in is-cold-email.ts for extracting and routing exclusion parameters based on rule action type
    • Outlook folder ID resolution fallback behavior in microsoft.ts when folder name resolution fails
    • Consistency of excludeLabel vs. excludeFolder usage across Gmail and Outlook (note: Gmail does not use excludeFolder)
    • Test coverage for edge cases: null coldEmailRule, missing action data, folder resolution failures

Possibly related PRs

  • #983: Modifies sender normalization in is-cold-email.ts for detecting known cold senders; intersects with core cold-email detection flow
  • #987: Alters previous-email query construction in hasPreviousCommunicationsWithSenderOrDomain for Outlook; affects the same method signature and control flow
  • #603: Refactors isColdEmail to use generic EmailProvider interface and updates hasPreviousCommunicationsWithSenderOrDomain options; overlaps substantially in scope

Poem

🐰 Filters now dance with labels bright,
Cold emails hidden from our sight,
Gmail's tags and Outlook's fold,
Keep the warm senders from getting cold,
Exclusion logic, precise and tight!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title is vague and partially related to the changeset. While it mentions 'cold emails' and 'domains,' the WIP prefix and generic phrasing lack specificity about the actual implementation (exclusion logic for label/folder). Consider a more specific title like 'Add label/folder exclusion for cold email detection across domains' or 'Implement domain-aware cold email checks with label exclusion' to better convey the core change.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/cold-email-cross-domain

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/web/utils/email/microsoft.ts (1)

1212-1220: Complex query construction - consider readability improvements.

The filter query on line 1216 is quite long and combines multiple conditions:

  • Outgoing emails (toRecipients) with date filter
  • Incoming emails (from) with date filter
  • Exclude filter appended to both parts

While functionally correct, the query construction could be more readable. Consider extracting the query building into a helper function or breaking it into smaller parts to improve maintainability and debuggability.

Example refactor:

const outgoingFilter = `toRecipients/any(a:a/emailAddress/address eq '${escapeODataString(options.from)}') and sentDateTime lt ${options.date.toISOString()}${excludeFilter}`;
const incomingFilter = `from/emailAddress/address eq '${escapeODataString(options.from)}' and receivedDateTime lt ${options.date.toISOString()}${excludeFilter}`;

const response: { value: Message[] } = await this.client
  .getClient()
  .api("/me/messages")
  .filter(`(${outgoingFilter}) or (${incomingFilter})`)
  .top(2)
  .select("id")
  .get();
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8c7cf27 and 9df5a07.

📒 Files selected for processing (7)
  • apps/web/utils/cold-email/exclude-cold-emails.test.ts (1 hunks)
  • apps/web/utils/cold-email/is-cold-email-label-exclusion.test.ts (1 hunks)
  • apps/web/utils/cold-email/is-cold-email.ts (4 hunks)
  • apps/web/utils/email/microsoft.ts (1 hunks)
  • apps/web/utils/email/types.ts (1 hunks)
  • apps/web/utils/gmail/message-label-exclusion.test.ts (1 hunks)
  • apps/web/utils/gmail/message.ts (4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-17T04:19:57.099Z
Learnt from: edulelis
Repo: elie222/inbox-zero PR: 576
File: packages/resend/emails/digest.tsx:78-83
Timestamp: 2025-07-17T04:19:57.099Z
Learning: In packages/resend/emails/digest.tsx, the DigestEmailProps type uses `[key: string]: DigestItem[] | undefined | string | Date | undefined` instead of intersection types like `& Record<string, DigestItem[] | undefined>` due to implementation constraints. This was the initial implementation approach and cannot be changed to more restrictive typing.

Applied to files:

  • apps/web/utils/email/types.ts
🧬 Code graph analysis (5)
apps/web/utils/cold-email/is-cold-email.ts (2)
apps/web/utils/cold-email/cold-email-rule.ts (1)
  • ColdEmailRule (4-6)
apps/web/utils/email.ts (1)
  • extractEmailAddress (19-52)
apps/web/utils/gmail/message-label-exclusion.test.ts (2)
apps/web/utils/email/types.ts (1)
  • EmailProvider (45-252)
apps/web/utils/gmail/message.ts (1)
  • hasPreviousCommunicationsWithSenderOrDomain (244-268)
apps/web/utils/cold-email/is-cold-email-label-exclusion.test.ts (4)
apps/web/__tests__/helpers.ts (1)
  • getEmailAccount (26-46)
apps/web/utils/cold-email/cold-email-rule.ts (1)
  • ColdEmailRule (4-6)
apps/web/utils/types.ts (1)
  • EmailForLLM (120-134)
apps/web/utils/cold-email/is-cold-email.ts (1)
  • isColdEmail (20-99)
apps/web/utils/email/microsoft.ts (2)
apps/web/utils/outlook/folders.ts (1)
  • getOrCreateOutlookFolderIdByName (127-161)
apps/web/utils/outlook/odata-escape.ts (1)
  • escapeODataString (13-20)
apps/web/utils/cold-email/exclude-cold-emails.test.ts (3)
apps/web/utils/email/types.ts (1)
  • EmailProvider (45-252)
apps/web/__tests__/helpers.ts (1)
  • getEmailAccount (26-46)
apps/web/utils/cold-email/is-cold-email.ts (1)
  • isColdEmail (20-99)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: test
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: Jit Security
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (7)
apps/web/utils/cold-email/exclude-cold-emails.test.ts (1)

1-346: Excellent test coverage for label/folder exclusion logic.

The test suite is well-structured and covers all critical scenarios:

  • Gmail label exclusion with default and custom labels
  • Microsoft Outlook distinction between LABEL (category) and MOVE_FOLDER actions
  • Fallback to default "Cold Email" label when no rule is configured
  • Previous email detection with and without exclusions
  • Short-circuit behavior when date/id is missing

The tests effectively validate that the exclusion parameters are correctly passed through to the provider layer.

apps/web/utils/email/types.ts (1)

220-221: Well-documented interface extension.

The addition of excludeLabel and excludeFolder parameters is clearly documented with inline comments explaining provider-specific usage. The nullable types provide appropriate flexibility for different scenarios.

apps/web/utils/cold-email/is-cold-email-label-exclusion.test.ts (1)

1-323: Comprehensive test coverage for label exclusion.

The test suite thoroughly validates:

  • Default "Cold Email" label usage when rule is null or has no actions
  • Custom label name propagation
  • Correct parameter passing to the provider
  • Early return for known cold emailers (avoiding unnecessary provider calls)
  • Integration with previous email detection logic

The tests effectively ensure the label exclusion feature works correctly across different configurations.

apps/web/utils/gmail/message-label-exclusion.test.ts (1)

1-266: Thorough Gmail query construction tests.

Excellent test coverage for Gmail-specific label exclusion:

  • Validates correct query syntax (-label:"Label Name")
  • Tests domain-based searches for company emails vs full email for public domains
  • Verifies both incoming (from:) and outgoing (to:) query construction
  • Ensures current message is properly excluded from results

The tests effectively validate the Gmail implementation details.

apps/web/utils/cold-email/is-cold-email.ts (1)

59-74: Clean implementation of label/folder exclusion logic.

The logic correctly:

  • Extracts the action from the cold email rule
  • Determines whether to exclude by folder (MOVE_FOLDER) or label (LABEL)
  • Defaults to "Cold Email" when no action is configured
  • Passes appropriate parameters to the provider

The fallback to "Cold Email" (line 61) ensures consistent behavior even when rules are not fully configured, which aligns with the test expectations.

apps/web/utils/gmail/message.ts (2)

173-204: Clean implementation of Gmail label exclusion.

The implementation correctly:

  • Adds the excludeLabel parameter to findPreviousEmailsWithSender
  • Constructs the exclusion query only when excludeLabel is provided
  • Appends the exclusion to both incoming (from:) and outgoing (to:) searches
  • Maintains consistent query construction

The logic properly threads the exclusion through both email directions, ensuring comprehensive filtering.


244-268: Proper propagation of exclusion parameters.

The function correctly:

  • Accepts both excludeLabel and excludeFolder parameters (though Gmail doesn't use excludeFolder)
  • Determines whether to search by domain or full email based on public domain check
  • Passes excludeLabel through to hasPreviousCommunicationWithSender

The domain-based search logic for company emails vs. public email providers is well-implemented and will improve cold email detection across different sender types.

Comment on lines +1187 to +1210
let excludeFilter = "";

if (options.excludeFolder) {
// Exclude by folder (for MOVE_FOLDER action)
try {
const folderId = await getOrCreateOutlookFolderIdByName(
this.client,
options.excludeFolder,
);
if (folderId) {
excludeFilter = ` and parentFolderId ne '${folderId}'`;
}
} catch (error) {
// If folder doesn't exist or we can't get it, proceed without filtering
this.logger.info("Could not get folder ID for exclusion", {
folder: options.excludeFolder,
error,
});
}
} else if (options.excludeLabel) {
// Exclude by category (for LABEL action)
const categoryName = escapeODataString(options.excludeLabel);
excludeFilter = ` and not (categories/any(c: c eq '${categoryName}'))`;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

Verify the OData category exclusion syntax.

The category exclusion query uses:

excludeFilter = ` and not (categories/any(c: c eq '${categoryName}'))`;

This looks correct for OData, but please verify that the not (categories/any(...)) syntax is the standard approach for excluding messages with a specific category in Microsoft Graph API.

Also, the folder resolution silently continues without exclusion if it fails (lines 1200-1205). Consider whether this should be logged at a higher level (warn instead of info) or handled differently to ensure developers are aware when folder exclusion is skipped.


🌐 Web query:

Does Microsoft Graph API support "not (categories/any(c: c eq 'value'))" syntax for excluding emails with specific categories?

💡 Result:

Short answer: Yes — Microsoft Graph's $filter supports using not with the lambda any() operator (e.g. not categories/any(c:c eq 'Value')), but behavior can vary by resource and there are known limitations/quirks.

Evidence:

  • The Graph $filter docs show lambda operators and the not(...) pattern for collections (examples such as not(groupTypes/any(...))). [1]
  • A Microsoft Q&A post demonstrates using not categories/any() to return messages with no categories. [2]
  • However, community threads report inconsistent behavior for more complex category filters (for example AND between multiple any() expressions sometimes returns empty results), so expect edge cases and test your exact query. [3][4]

If you need, tell me the exact query and mailbox/folder you’re using and I’ll help test/adjust it.

Sources:
[1] Microsoft Graph: Use the $filter query parameter.
[2] Microsoft Q&A: Filter Emails That Do Not Have An Assigned Category Using Graph API.
[3] Microsoft Q&A / forum threads on filtering messages by categories.
[4] StackOverflow: querying mails without categories not supported.


OData category exclusion syntax is supported; consider edge case testing and document the graceful fallback behavior.

The not (categories/any(c: c eq '${categoryName}')) syntax is valid and documented in Microsoft Graph's $filter API. However, community reports note inconsistent behavior for more complex category filters, so test this query in your specific scenarios (especially if multiple categories or complex AND conditions are involved).

Regarding folder resolution failure (lines 1200-1205): The current approach logs at info level and gracefully continues without filtering. This seems intentional, but consider clarifying in a code comment why silent fallback is preferred here—specifically whether skipping exclusion is always safe or if certain failure modes should be treated more seriously. If folder IDs become stale or resolution failures indicate real data consistency issues, an info level might mask problems. Document the contract: "if folder doesn't exist, exclusion is skipped; caller must handle this."

🤖 Prompt for AI Agents
In apps/web/utils/email/microsoft.ts around lines 1187 to 1210, the OData
category exclusion syntax is correct but you must add explicit documentation +
handling for edge cases and clarify the folder-resolution fallback: add a
concise code comment explaining the contract ("if folder doesn't exist or cannot
be resolved, exclusion is skipped and caller must handle potential stale folder
IDs"), and annotate which failure modes should remain info vs escalate (e.g.,
transient network errors vs permission/consistency errors). Also add or update a
unit/integration test note to exercise category filters with multiple
categories/complex conditions and a TODO to verify Graph behavior in those
scenarios, and consider changing the logger level to warn or include error.code
when folder resolution fails for non-expected errors.

Comment on lines +182 to +184
const excludeLabelQuery = options.excludeLabel
? ` -label:"${options.excludeLabel}"`
: "";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Verify label name escaping in Gmail queries.

The label exclusion query is constructed using a template literal without escaping:

const excludeLabelQuery = options.excludeLabel
  ? ` -label:"${options.excludeLabel}"`
  : "";

If the label name contains special characters (like quotes, backslashes, or other Gmail search operators), this could:

  1. Break the query syntax
  2. Potentially allow query injection if label names come from untrusted sources

Please verify whether Gmail label names need escaping and, if so, implement appropriate escaping logic.


🏁 Script executed:

#!/bin/bash
# Search for existing escaping utilities in the Gmail codebase
echo "=== Searching for Gmail query escaping functions ==="
rg -n "escape.*gmail|gmail.*escape" --type ts -g '!node_modules' -g '!dist' -C 3

echo -e "\n=== Searching for label name sanitization ==="
rg -n "label.*escape|escape.*label|sanitize.*label" --type ts -g '!node_modules' -g '!dist' -C 3

echo -e "\n=== Check existing query construction patterns ==="
ast-grep --pattern 'query: `from:$_' -l ts

Length of output: 151


I need to investigate further to understand the actual risk. Let me search for how excludeLabel is used and sourced in the codebase:

Let me search for where the excludeLabel parameter comes from and how it's used:

Now let me search for where the excludeLabel parameter comes from and how it's validated:


🏁 Script executed:

#!/bin/bash
# Find where excludeLabel parameter is used and defined
echo "=== Searching for excludeLabel usage ==="
rg -n "excludeLabel" --type ts -C 5 -g '!node_modules' -g '!dist'

echo -e "\n=== Search for the function signature and options interface ==="
rg -n "excludeLabel\|interface.*options" --type ts -B 5 -A 10 -g '!node_modules' -g '!dist' | head -100

Length of output: 30993


Add escaping for label names in Gmail search queries.

While some tools restrict label names from containing special characters like forward slashes and backslashes, Gmail's own API doesn't enforce all these restrictions uniformly. The current code constructs label exclusion queries without escaping:

const excludeLabelQuery = options.excludeLabel
  ? ` -label:"${options.excludeLabel}"`
  : "";

If a label name contains quotation marks (e.g., My "Special" Label), the query breaks:

-label:"My "Special" Label"

becomes malformed at the first internal quote.

Additionally, apostrophes and special characters can be tricky in Gmail search and may require escaping with a backslash.

Implement escaping for label names to handle quotes and other special characters properly. The Outlook implementation in microsoft.ts shows that similar escaping logic exists elsewhere in the codebase (line 1208 uses escapeODataString), indicating this concern has been recognized for other providers.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 7 files

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants