fix(gmail): remove inbox-only sync restriction to enable sent-mail re… (#1902)

This commit is contained in:
ARYASH GUPTA
2026-05-16 01:36:54 -07:00
committed by GitHub
parent df542e7268
commit 8f93ca0cae
3 changed files with 87 additions and 2 deletions
@@ -36,6 +36,20 @@ use crate::openhuman::composio::providers::{
const ACTION_GET_PROFILE: &str = "GMAIL_GET_PROFILE";
const ACTION_FETCH_EMAILS: &str = "GMAIL_FETCH_EMAILS";
/// Base Gmail search query used on every sync pass.
///
/// Excludes spam and trash but intentionally does NOT restrict to `in:inbox` —
/// that restriction (issue #1713) prevented sent emails from ever being ingested.
/// Exported `pub(super)` so `tests.rs` can assert against the canonical value
/// rather than a duplicated literal.
pub(super) const BASE_QUERY: &str = "-in:spam -in:trash";
/// Gmail search query strings that retrieve sent mail.
///
/// Any of these can be passed as the `query` parameter to `GMAIL_FETCH_EMAILS`
/// to fetch outbound messages. Exported `pub(super)` for use in regression tests.
pub(super) const SENT_QUERIES: &[&str] = &["from:me", "label:SENT", "in:sent"];
/// Page size per API call. Kept moderate so each call is fast and we
/// get frequent checkpoints for the daily budget.
const PAGE_SIZE: u32 = 25;
@@ -283,7 +297,14 @@ impl ComposioProvider for GmailProvider {
// so same-day re-ticks do not re-fetch a whole day's
// window every time. Fall back to the day filter only when
// the cursor cannot be parsed as a timestamp.
let mut query = "in:inbox -in:spam -in:trash".to_string();
//
// NOTE: We intentionally do NOT restrict to `in:inbox` here.
// The original query `in:inbox -in:spam -in:trash` meant sent
// emails (label:SENT) were never fetched and therefore the
// agent could not answer questions about outbound mail (issue #1713).
// Removing `in:inbox` lets Gmail return both inbox and sent
// messages while still excluding spam and trash.
let mut query = BASE_QUERY.to_string();
if let Some(ref cursor) = state.cursor {
if let Some(epoch_filter) = sync::cursor_to_gmail_after_epoch_filter(cursor) {
query.push_str(&format!(" after:{epoch_filter}"));
@@ -1,5 +1,6 @@
//! Unit tests for the Gmail provider.
use super::provider::{BASE_QUERY, SENT_QUERIES};
use super::sync::{
cursor_to_gmail_after_epoch_filter, cursor_to_gmail_after_filter, extract_messages,
extract_page_token, now_ms, parse_cursor_to_epoch_secs,
@@ -238,3 +239,63 @@ fn extract_messages_handles_deep_nesting() {
// live `ComposioClient` (HTTP) plus the global `MemoryClient` singleton.
// Those go through the integration test suite. Here we just lock in
// the provider's identity surface and helpers.
// ── Regression tests for issue #1713: sent-mail retrieval ───────────────────
//
// Before the fix the sync query was `in:inbox -in:spam -in:trash`, which meant
// sent emails (label:SENT) were never fetched or ingested into the memory tree.
// The fix removes `in:inbox` so both inbox and sent mail are fetched.
/// Guard: the provider source must NOT contain the `in:inbox` restriction.
/// If this test fails, someone reintroduced the inbox-only query that caused
/// issue #1713.
#[test]
fn provider_source_does_not_restrict_to_inbox() {
let source = include_str!("provider.rs");
// The old restriction started with `"in:inbox`. The double-quote is part
// of the Rust string literal, so this catches the exact regression pattern.
assert!(
!source.contains("\"in:inbox"),
"provider.rs sync query must NOT start with 'in:inbox' — this \
restriction prevents sent emails from being ingested (issue #1713). \
Use '-in:spam -in:trash' to allow both inbox and sent mail."
);
}
/// The base sync query must exclude spam and trash but NOT restrict to inbox.
/// Asserts against the canonical `BASE_QUERY` constant from provider.rs so
/// any change to the production value is caught immediately.
#[test]
fn sync_base_query_excludes_spam_and_trash_without_inbox_restriction() {
assert!(
BASE_QUERY.contains("-in:spam"),
"BASE_QUERY must exclude spam (got: {BASE_QUERY:?})"
);
assert!(
BASE_QUERY.contains("-in:trash"),
"BASE_QUERY must exclude trash (got: {BASE_QUERY:?})"
);
assert!(
!BASE_QUERY.contains("in:inbox"),
"BASE_QUERY must NOT restrict to inbox — omitting this allows sent \
mail to be fetched and ingested (issue #1713). Got: {BASE_QUERY:?}"
);
}
/// Sent-mail query strings must be non-empty and must not restrict to inbox.
/// Iterates `SENT_QUERIES` from provider.rs — the canonical list of query
/// strings a user or agent can pass to GMAIL_FETCH_EMAILS for sent mail.
#[test]
fn sent_mail_query_strings_are_well_formed() {
assert!(!SENT_QUERIES.is_empty(), "SENT_QUERIES must not be empty");
for q in SENT_QUERIES {
assert!(
!q.is_empty(),
"sent-mail query must not be empty, got empty string in SENT_QUERIES"
);
assert!(
!q.starts_with("in:inbox"),
"sent-mail query '{q}' must not restrict to inbox"
);
}
}
+4 -1
View File
@@ -499,7 +499,10 @@ impl Tool for ComposioTool {
fn description(&self) -> &str {
"Execute actions on 1000+ apps via Composio (Gmail, Notion, GitHub, Slack, etc.). \
Use action='list' to see available actions, action='execute' with action_name/tool_slug, params, and optional connected_account_id, \
or action='connect' with app/auth_config_id to get OAuth URL."
or action='connect' with app/auth_config_id to get OAuth URL. \
For Gmail: GMAIL_FETCH_EMAILS supports standard Gmail search syntax in the 'query' param — \
use query='from:me' or query='label:SENT' to retrieve sent emails, query='label:INBOX' for inbox, \
query='is:unread' for unread mail, etc. Sent mail is synced and searchable."
}
fn parameters_schema(&self) -> serde_json::Value {