From 239092da52f87e3917819828b3e1250d770018fe Mon Sep 17 00:00:00 2001 From: yeziR4 Date: Mon, 13 Jul 2026 13:31:46 +0100 Subject: [PATCH 1/2] feat: add Gmail receipts connector --- README.md | 1 + SCOPES.md | 1 + .../gmail-playwright-1.0.0.tgz | Bin 0 -> 7104 bytes connector-index.json | 28 + .../google/fixtures/gmail.receipts.empty.json | 1 + .../google/fixtures/gmail.receipts.small.json | 23 + connectors/google/gmail-playwright.js | 500 ++++++++++++++++++ connectors/google/gmail-playwright.json | 35 ++ connectors/google/icons/gmail.svg | 7 + connectors/google/schemas/gmail.receipts.json | 38 ++ fixture-index.json | 26 + registry.json | 16 + scope-catalog.json | 32 ++ scopes/web-capabilities.json | 1 + 14 files changed, 709 insertions(+) create mode 100644 artifacts/gmail-playwright/gmail-playwright-1.0.0.tgz create mode 100644 connectors/google/fixtures/gmail.receipts.empty.json create mode 100644 connectors/google/fixtures/gmail.receipts.small.json create mode 100644 connectors/google/gmail-playwright.js create mode 100644 connectors/google/gmail-playwright.json create mode 100644 connectors/google/icons/gmail.svg create mode 100644 connectors/google/schemas/gmail.receipts.json diff --git a/README.md b/README.md index 4ff9929..d02e1c6 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ The catalog separates two collection paths. **Vana Desktop (local)** runs regist | `claude` | `claude.conversations`, `claude.projects` | claude-export-playwright (experimental) | | `doordash` | `doordash.orders` | doordash-playwright (beta) | | `github` | `github.contributions`, `github.events`, `github.history`, `github.profile`, `github.repositories`, `github.starred` | github-playwright (stable) | +| `gmail` | `gmail.receipts` | gmail-playwright (experimental) | | `heb` | `heb.nutrition`, `heb.orders`, `heb.profile` | heb-playwright (experimental) | | `icloud_notes` | `icloud_notes.folders`, `icloud_notes.notes` | icloud-notes-playwright (experimental) | | `instagram` | `instagram.ads`, `instagram.following`, `instagram.posts`, `instagram.profile` | instagram-playwright (stable); instagram-ads-playwright (beta) | diff --git a/SCOPES.md b/SCOPES.md index 4f65ae5..310221d 100644 --- a/SCOPES.md +++ b/SCOPES.md @@ -31,6 +31,7 @@ Every immutable `connectors-` GitHub release includes `scope-catalog.j | github | `github.profile` | Account profile information such as name, bio, follower counts, pinned repositories, organization memberships, and achievement badges | [JSON Schema](connectors/github/schemas/github.profile.json) | Yes | Yes | None | github-playwright (stable) | | github | `github.repositories` | Your repository list with metadata like language, stars, and update time | [JSON Schema](connectors/github/schemas/github.repositories.json) | No | Yes | None | github-playwright (stable) | | github | `github.starred` | Repositories you have starred on GitHub | [JSON Schema](connectors/github/schemas/github.starred.json) | No | Yes | None | github-playwright (stable) | +| gmail | `gmail.receipts` | Recent Gmail messages matching purchases, flights, or tickets, including sender, subject, date, and preview text. | [JSON Schema](connectors/google/schemas/gmail.receipts.json) | No | Yes | Vana Desktop (local; gmail-playwright): The Desktop connector returns at most the 100 most recent Gmail rows matching the purchase and travel search. | gmail-playwright (experimental) | | heb | `heb.nutrition` | Nutrition facts (calories, macros, sodium, fiber, vitamins) for each product ordered. Aligned to Schema.org NutritionInformation. | [JSON Schema](connectors/heb/schemas/heb.nutrition.json) | No | Yes | None | heb-playwright (experimental) | | heb | `heb.orders` | Curbside and delivery orders with item names, quantities, prices, and order dates | [JSON Schema](connectors/heb/schemas/heb.orders.json) | No | Yes | None | heb-playwright (experimental) | | heb | `heb.profile` | Your name, email, phone number, and saved delivery addresses | [JSON Schema](connectors/heb/schemas/heb.profile.json) | No | Yes | None | heb-playwright (experimental) | diff --git a/artifacts/gmail-playwright/gmail-playwright-1.0.0.tgz b/artifacts/gmail-playwright/gmail-playwright-1.0.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..037406ff074aee43e7621ef80d76e165d9d66a19 GIT binary patch literal 7104 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", + "subject": "Your order is confirmed", + "snippet": "Thanks for your purchase. Your synthetic order will be dispatched shortly." + }, + { + "id": "synthetic-flight-2026-002", + "date": "Jul 7, 2026, 4:15 PM", + "sender": "Example Airways ", + "subject": "Flight booking confirmation", + "snippet": "Your synthetic itinerary from Lagos to Accra is confirmed for August 18." + }, + { + "id": "synthetic-ticket-2026-003", + "date": "Jul 3, 2026, 8:05 AM", + "sender": "Example Tickets ", + "subject": "Your event tickets", + "snippet": "Your two synthetic admission tickets are attached and ready to use." + } +] diff --git a/connectors/google/gmail-playwright.js b/connectors/google/gmail-playwright.js new file mode 100644 index 0000000..a87c8e7 --- /dev/null +++ b/connectors/google/gmail-playwright.js @@ -0,0 +1,500 @@ +/** + * Gmail Receipts Connector (Playwright) + * + * Exports the 100 most recent Gmail rows matching purchases, flights, or + * tickets. Google authentication is always completed by the user in a visible + * browser; credentials and message data remain in the local connector runtime. + * + * Extraction method: authenticated Gmail search + DOM extraction. + */ + +const PLATFORM = "gmail"; +const VERSION = "1.0.0-playwright"; +const SCOPE = "gmail.receipts"; +const CANONICAL_SCOPES = [SCOPE]; +const MAX_MESSAGES = 100; +const MAX_PAGES = 10; +const SEARCH_URL = + "https://mail.google.com/mail/u/0/#search/category%3Apurchases+OR+flight+OR+ticket"; + +const makeConnectorError = ( + errorClass, + reason, + disposition, + extras = {}, +) => ({ + errorClass, + reason, + disposition, + ...extras, +}); + +const makeFatalRunError = (errorClass, reason, phase = "collect") => { + const error = new Error(reason); + error.telemetryError = makeConnectorError(errorClass, reason, "fatal", { + phase, + }); + return error; +}; + +const inferErrorClass = (message, fallback = "runtime_error") => { + const text = String(message || "").toLowerCase(); + if (text.includes("auth") || text.includes("login")) return "auth_failed"; + if (text.includes("timeout") || text.includes("timed out")) return "timeout"; + if (text.includes("navigate") || text.includes("navigation")) { + return "navigation_error"; + } + if (text.includes("selector") || text.includes("message row")) { + return "selector_error"; + } + return fallback; +}; + +const buildResult = ({ requestedScopes, messages, errors }) => { + const result = { + requestedScopes: [...requestedScopes], + timestamp: new Date().toISOString(), + version: VERSION, + platform: PLATFORM, + exportSummary: { + count: messages?.length || 0, + label: messages?.length === 1 ? "message" : "messages", + details: { + messages: messages?.length || 0, + limit: MAX_MESSAGES, + query: "category:purchases OR flight OR ticket", + }, + }, + errors, + }; + + if (Array.isArray(messages) && requestedScopes.includes(SCOPE)) { + Object.assign(result, { "gmail.receipts": messages }); + } + + return result; +}; + +const resolveRequestedScopes = () => { + const raw = + typeof page.requestedScopes === "function" ? page.requestedScopes() : null; + + if (raw == null) return [...CANONICAL_SCOPES]; + if (!Array.isArray(raw) || raw.length === 0) { + throw makeFatalRunError( + "protocol_violation", + "Gmail connector received an empty or invalid requestedScopes array.", + "init", + ); + } + + const deduped = Array.from(new Set(raw)); + const unsupported = deduped.filter( + (scope) => !CANONICAL_SCOPES.includes(scope), + ); + if (unsupported.length > 0) { + throw makeFatalRunError( + "protocol_violation", + `Gmail connector received unsupported requestedScopes: ${unsupported.join(", ")}.`, + "init", + ); + } + + return deduped; +}; + +const safeGoto = async (url, attempts = 3) => { + for (let attempt = 1; attempt <= attempts; attempt += 1) { + try { + await page.goto(url, { timeout: 60000 }); + return true; + } catch (_error) { + if (attempt < attempts) await page.sleep(1500); + } + } + return false; +}; + +const checkLoginStatus = async () => { + try { + return await page.evaluate(` + (() => { + if (window.location.hostname !== 'mail.google.com') return false; + if (document.querySelector('input[type="password"]')) return false; + return !!document.querySelector( + '[aria-label="Search mail"], [role="main"]' + ); + })() + `); + } catch (_error) { + return false; + } +}; + +const inspectSearchState = async () => { + try { + return await page.evaluate(` + (() => { + const main = document.querySelector('[role="main"]'); + const rowSelector = + 'tr[data-legacy-thread-id], tr[data-thread-id]'; + const rows = main ? main.querySelectorAll(rowSelector) : []; + const text = (main?.innerText || '').replace(/\\s+/g, ' ').trim(); + const empty = /no (?:emails|messages).*match|no conversations in this view/i.test(text); + return { + authenticated: window.location.hostname === 'mail.google.com' && + !!document.querySelector('[aria-label="Search mail"], [role="main"]'), + rowCount: rows.length, + empty, + }; + })() + `); + } catch (_error) { + return { authenticated: false, rowCount: 0, empty: false }; + } +}; + +const waitForSearchResults = async () => { + for (let attempt = 0; attempt < 20; attempt += 1) { + const state = await inspectSearchState(); + if (state.rowCount > 0 || state.empty) return state; + await page.sleep(1000); + } + return inspectSearchState(); +}; + +const extractVisibleMessages = async () => { + try { + const value = await page.evaluate(` + (() => { + const clean = (value) => String(value || '') + .replace(/[\\u200B-\\u200D\\uFEFF]/g, '') + .replace(/\\s+/g, ' ') + .trim(); + + const fallbackId = (parts) => { + const input = parts.join('\\u001f'); + let hash = 0x811c9dc5; + for (let index = 0; index < input.length; index += 1) { + hash ^= input.charCodeAt(index); + hash = Math.imul(hash, 0x01000193); + } + return 'gmail-' + (hash >>> 0).toString(16).padStart(8, '0'); + }; + + const main = document.querySelector('[role="main"]'); + if (!main) return { messages: [], hasNext: false, selectorFound: false }; + + let rows = Array.from(main.querySelectorAll( + 'tr[data-legacy-thread-id], tr[data-thread-id]' + )); + + // Some Gmail rollouts omit thread attributes. Restrict the fallback to + // table rows containing Gmail's semantic sender email attribute. + if (rows.length === 0) { + rows = Array.from(main.querySelectorAll('tr')).filter( + (row) => row.querySelector('[email]') + ); + } + + const messages = rows.map((row, rowIndex) => { + try { + const senderNode = row.querySelector('[email]'); + const senderName = clean( + senderNode?.getAttribute('name') || senderNode?.textContent + ); + const senderEmail = clean(senderNode?.getAttribute('email')); + const sender = senderName && senderEmail && + senderName.toLowerCase() !== senderEmail.toLowerCase() + ? senderName + ' <' + senderEmail + '>' + : (senderName || senderEmail); + + // Gmail does not expose semantic subject/snippet attributes. These + // short class names are used only after the stable row/sender gate, + // with structural fallbacks for alternate rollouts. + const subjectNode = row.querySelector('.bog') || + row.querySelector('[data-subject]') || + Array.from(row.querySelectorAll('span')).find((span) => { + const candidate = clean(span.textContent); + return candidate && span !== senderNode && candidate.length > 2; + }); + const snippetNode = row.querySelector('.y2') || + row.querySelector('[data-snippet]'); + const dateNode = row.querySelector( + 'td:last-of-type [title], td:last-of-type time, td:last-of-type span' + ); + + const subject = clean( + subjectNode?.getAttribute('data-subject') || subjectNode?.textContent + ); + const snippet = clean( + snippetNode?.getAttribute('data-snippet') || snippetNode?.textContent + ).replace(/^[-–—]\\s*/, ''); + const date = clean( + dateNode?.getAttribute('title') || + dateNode?.getAttribute('datetime') || + dateNode?.textContent + ); + + const id = clean( + row.getAttribute('data-legacy-last-message-id') || + row.getAttribute('data-legacy-thread-id') || + row.getAttribute('data-thread-id') || + row.id + ) || fallbackId([date, sender, subject, snippet, String(rowIndex)]); + + return { id, date, sender, subject, snippet }; + } catch (_error) { + return null; + } + }).filter(Boolean); + + const next = Array.from(document.querySelectorAll( + '[aria-label="Older"], [aria-label="Next page"], ' + + '[data-tooltip="Older"], [data-tooltip="Next page"]' + )).find((element) => { + const disabled = element.getAttribute('aria-disabled') === 'true' || + element.hasAttribute('disabled'); + const visible = !!(element.offsetWidth || element.offsetHeight || + element.getClientRects().length); + return visible && !disabled; + }); + + return { + messages, + hasNext: !!next, + selectorFound: rows.length > 0, + firstId: messages[0]?.id || '', + }; + })() + `); + + return value || { + messages: [], + hasNext: false, + selectorFound: false, + firstId: "", + }; + } catch (_error) { + return { + messages: [], + hasNext: false, + selectorFound: false, + firstId: "", + }; + } +}; + +const clickNextPage = async () => { + try { + return await page.evaluate(` + (() => { + const next = Array.from(document.querySelectorAll( + '[aria-label="Older"], [aria-label="Next page"], ' + + '[data-tooltip="Older"], [data-tooltip="Next page"]' + )).find((element) => { + const disabled = element.getAttribute('aria-disabled') === 'true' || + element.hasAttribute('disabled'); + const visible = !!(element.offsetWidth || element.offsetHeight || + element.getClientRects().length); + return visible && !disabled; + }); + if (!next) return false; + next.click(); + return true; + })() + `); + } catch (_error) { + return false; + } +}; + +const waitForPageChange = async (previousFirstId) => { + for (let attempt = 0; attempt < 15; attempt += 1) { + await page.sleep(1000); + const current = await extractVisibleMessages(); + if (current.firstId && current.firstId !== previousFirstId) return true; + } + return false; +}; + +const collectMessages = async () => { + const messages = []; + const seenIds = new Set(); + let paginationFailed = false; + let selectorFound = false; + + for (let pageNumber = 1; pageNumber <= MAX_PAGES; pageNumber += 1) { + const pageData = await extractVisibleMessages(); + selectorFound = selectorFound || pageData.selectorFound; + + for (const message of pageData.messages || []) { + if (!message?.id || seenIds.has(message.id)) continue; + seenIds.add(message.id); + messages.push(message); + if (messages.length >= MAX_MESSAGES) break; + } + + await page.setProgress({ + phase: { step: 2, total: 2, label: "Collecting confirmations" }, + message: `Collected ${messages.length} of up to ${MAX_MESSAGES} messages...`, + count: messages.length, + }); + + if (messages.length >= MAX_MESSAGES || !pageData.hasNext) break; + if (!(await clickNextPage())) { + paginationFailed = true; + break; + } + if (!(await waitForPageChange(pageData.firstId))) { + paginationFailed = true; + break; + } + await page.sleep(500); + } + + return { + messages: messages.slice(0, MAX_MESSAGES), + paginationFailed, + selectorFound, + }; +}; + +(async () => { + let requestedScopes = [...CANONICAL_SCOPES]; + + try { + requestedScopes = resolveRequestedScopes(); + + await page.setProgress({ + phase: { step: 1, total: 2, label: "Signing in" }, + message: "Checking your saved Gmail session...", + }); + + if (!(await safeGoto(SEARCH_URL))) { + throw makeFatalRunError( + "navigation_error", + "Could not reach Gmail after multiple attempts.", + "auth", + ); + } + await page.sleep(2500); + + let loggedIn = await checkLoginStatus(); + if (!loggedIn) { + const browserResult = await page.showBrowser(SEARCH_URL); + if (!browserResult?.headed) { + throw makeFatalRunError( + "auth_failed", + "Gmail login requires a visible browser session.", + "auth", + ); + } + + await page.setData( + "status", + "Sign in to Google in the browser. Your credentials remain on this device.", + ); + await page.promptUser( + "Complete Google sign-in, including any two-factor verification. Continue when Gmail is open.", + async () => await checkLoginStatus(), + 2000, + ); + loggedIn = await checkLoginStatus(); + } + + if (!loggedIn) { + throw makeFatalRunError( + "auth_failed", + "Gmail login could not be confirmed.", + "auth", + ); + } + + await page.goHeadless(); + if (!(await safeGoto(SEARCH_URL))) { + throw makeFatalRunError( + "navigation_error", + "Gmail opened, but the purchase and travel search could not be loaded.", + ); + } + + await page.setProgress({ + phase: { step: 2, total: 2, label: "Collecting confirmations" }, + message: "Loading purchase and travel confirmation messages...", + count: 0, + }); + + const searchState = await waitForSearchResults(); + if (!searchState.authenticated) { + throw makeFatalRunError( + "auth_failed", + "The Gmail session expired before collection completed.", + "collect", + ); + } + + if (searchState.empty) { + const result = buildResult({ requestedScopes, messages: [], errors: [] }); + await page.setData("result", result); + await page.setData("status", "Complete! No matching Gmail messages found."); + return result; + } + + const collection = await collectMessages(); + const errors = []; + + if (!collection.selectorFound && collection.messages.length === 0) { + errors.push( + makeConnectorError( + "selector_error", + "Gmail loaded, but no recognizable message rows were found.", + "omitted", + { scope: SCOPE, phase: "collect" }, + ), + ); + } else if (collection.paginationFailed) { + errors.push( + makeConnectorError( + "navigation_error", + "Gmail pagination stopped before all available matching rows could be read.", + "degraded", + { scope: SCOPE, phase: "collect" }, + ), + ); + } + + const producedMessages = collection.selectorFound + ? collection.messages + : null; + const result = buildResult({ + requestedScopes, + messages: producedMessages, + errors, + }); + + await page.setData("result", result); + await page.setData( + "status", + `Complete! Exported ${collection.messages.length} Gmail messages.`, + ); + return result; + } catch (error) { + const telemetryError = + error?.telemetryError || + makeConnectorError( + inferErrorClass(error?.message || String(error)), + error?.message || String(error), + "fatal", + { phase: "collect" }, + ); + const result = buildResult({ + requestedScopes, + messages: null, + errors: [telemetryError], + }); + await page.setData("result", result); + await page.setData("error", telemetryError.reason); + return result; + } +})(); diff --git a/connectors/google/gmail-playwright.json b/connectors/google/gmail-playwright.json new file mode 100644 index 0000000..71bb6b0 --- /dev/null +++ b/connectors/google/gmail-playwright.json @@ -0,0 +1,35 @@ +{ + "manifest_version": "1.1", + "connector_id": "gmail-playwright", + "source_id": "gmail", + "version": "1.0.0", + "name": "Gmail Receipts", + "company": "Google", + "description": "Exports up to 100 recent Gmail purchase receipts and travel confirmations using a local authenticated browser session.", + "runtime": "playwright", + "page_api_version": 1, + "connect_url": "https://mail.google.com/mail/u/0/#search/category%3Apurchases+OR+flight+OR+ticket", + "connect_selector": "[aria-label=\"Search mail\"], [role=\"main\"]", + "icon": "icons/gmail.svg", + "scopes": [ + { + "scope": "gmail.receipts", + "label": "Your purchase and travel confirmations", + "description": "Recent Gmail messages matching purchases, flights, or tickets, including sender, subject, date, and preview text.", + "limits": [ + { + "type": "maxItems", + "value": 100, + "unit": "messages", + "description": "The Desktop connector returns at most the 100 most recent Gmail rows matching the purchase and travel search." + } + ] + } + ], + "capabilities": ["headed-takeover"], + "id": "gmail-playwright", + "connectURL": "https://mail.google.com/mail/u/0/#search/category%3Apurchases+OR+flight+OR+ticket", + "connectSelector": "[aria-label=\"Search mail\"], [role=\"main\"]", + "iconURL": "icons/gmail.svg", + "exportFrequency": "weekly" +} diff --git a/connectors/google/icons/gmail.svg b/connectors/google/icons/gmail.svg new file mode 100644 index 0000000..ff594b3 --- /dev/null +++ b/connectors/google/icons/gmail.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/connectors/google/schemas/gmail.receipts.json b/connectors/google/schemas/gmail.receipts.json new file mode 100644 index 0000000..16a028e --- /dev/null +++ b/connectors/google/schemas/gmail.receipts.json @@ -0,0 +1,38 @@ +{ + "name": "Gmail Receipts and Travel Confirmations", + "version": "1.0.0", + "scope": "gmail.receipts", + "dialect": "json", + "description": "A chronologically ordered list of up to 100 recent Gmail rows matching purchase receipts, flight confirmations, or ticket messages.", + "schema": { + "type": "array", + "maxItems": 100, + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique Gmail message or thread-row identifier exposed by the authenticated mailbox." + }, + "date": { + "type": "string", + "description": "Gmail's displayed received date or timestamp for the message row." + }, + "sender": { + "type": "string", + "description": "Sender name, email address, or both when Gmail exposes both values." + }, + "subject": { + "type": "string", + "description": "Subject line displayed in the Gmail search result." + }, + "snippet": { + "type": "string", + "description": "Short preview text displayed alongside the message subject." + } + }, + "required": ["id", "date", "sender", "subject", "snippet"], + "additionalProperties": false + } + } +} diff --git a/fixture-index.json b/fixture-index.json index 56f33f5..5bdefa6 100644 --- a/fixture-index.json +++ b/fixture-index.json @@ -4,6 +4,32 @@ "sourceRepo": "https://github.com/vana-com/data-connectors", "generatedAt": "2026-07-13T00:00:00Z", "fixtures": [ + { + "id": "gmail-playwright.gmail.receipts.empty", + "connectorId": "gmail-playwright", + "sourceId": "gmail", + "scope": "gmail.receipts", + "scenario": "empty", + "recordCount": 0, + "path": "connectors/google/fixtures/gmail.receipts.empty.json", + "rawUrl": "https://raw.githubusercontent.com/vana-com/data-connectors/main/connectors/google/fixtures/gmail.receipts.empty.json", + "schemaPath": "connectors/google/schemas/gmail.receipts.json", + "sizeBytes": 3, + "sha256": "sha256:37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570" + }, + { + "id": "gmail-playwright.gmail.receipts.small", + "connectorId": "gmail-playwright", + "sourceId": "gmail", + "scope": "gmail.receipts", + "scenario": "small", + "recordCount": 3, + "path": "connectors/google/fixtures/gmail.receipts.small.json", + "rawUrl": "https://raw.githubusercontent.com/vana-com/data-connectors/main/connectors/google/fixtures/gmail.receipts.small.json", + "schemaPath": "connectors/google/schemas/gmail.receipts.json", + "sizeBytes": 834, + "sha256": "sha256:9ba2cde62c7ca80b5c7717954d0573786c21e68634cae443cbf4cccf331784db" + }, { "id": "instagram-playwright.instagram.profile.empty", "connectorId": "instagram-playwright", diff --git a/registry.json b/registry.json index 27fdab1..2b52747 100644 --- a/registry.json +++ b/registry.json @@ -373,6 +373,22 @@ "script": "sha256:ab465031ce09187c70890f4498fb0d650d0ea0dd7a11d558496d834a00076188", "metadata": "sha256:1b43c2ab74a7dc8bb2271b5be9d6f1d1e64d3f636604332f67439663bfd62bce" } + }, + { + "id": "gmail-playwright", + "company": "google", + "version": "1.0.0", + "name": "Gmail Receipts", + "status": "experimental", + "description": "Exports up to 100 recent Gmail purchase receipts and travel confirmations using a local authenticated browser session.", + "files": { + "script": "google/gmail-playwright.js", + "metadata": "google/gmail-playwright.json" + }, + "checksums": { + "script": "sha256:e65b818375945ed41687e6bbfe5c83b5f6a564f53c3424e50dc91f666c5e914d", + "metadata": "sha256:b3f80f45f9c38e0580099bfc612a847592f34af8815b0657f38333c017a663c4" + } } ] } diff --git a/scope-catalog.json b/scope-catalog.json index 49b5657..7989aa7 100644 --- a/scope-catalog.json +++ b/scope-catalog.json @@ -19,6 +19,7 @@ "connectors/apple/icloud-notes-playwright.json", "connectors/doordash/doordash-playwright.json", "connectors/github/github-playwright.json", + "connectors/google/gmail-playwright.json", "connectors/google/youtube-playwright.json", "connectors/heb/heb-playwright.json", "connectors/linkedin/linkedin-playwright.json", @@ -364,6 +365,37 @@ } } }, + { + "sourceId": "gmail", + "scopeId": "gmail.receipts", + "description": "Recent Gmail messages matching purchases, flights, or tickets, including sender, subject, date, and preview text.", + "schema": { + "path": "connectors/google/schemas/gmail.receipts.json" + }, + "maturity": "experimental", + "fulfillment": { + "desktop": { + "status": "supported", + "connectors": [ + { + "id": "gmail-playwright", + "status": "experimental", + "limits": [ + { + "type": "maxItems", + "value": 100, + "unit": "messages", + "description": "The Desktop connector returns at most the 100 most recent Gmail rows matching the purchase and travel search." + } + ] + } + ] + }, + "web": { + "status": "unsupported" + } + } + }, { "sourceId": "heb", "scopeId": "heb.nutrition", diff --git a/scopes/web-capabilities.json b/scopes/web-capabilities.json index 6d3e826..715b919 100644 --- a/scopes/web-capabilities.json +++ b/scopes/web-capabilities.json @@ -31,6 +31,7 @@ { "scopeId": "github.profile", "status": "supported" }, { "scopeId": "github.repositories", "status": "unsupported" }, { "scopeId": "github.starred", "status": "unsupported" }, + { "scopeId": "gmail.receipts", "status": "unsupported" }, { "scopeId": "heb.nutrition", "status": "unsupported" }, { "scopeId": "heb.orders", "status": "unsupported" }, { "scopeId": "heb.profile", "status": "unsupported" }, From d343bc46c8d1e156440f2198ea72399970ba4617 Mon Sep 17 00:00:00 2001 From: yeziR4 Date: Mon, 13 Jul 2026 14:26:03 +0100 Subject: [PATCH 2/2] fix: support current Gmail date cells --- .../gmail-playwright-1.0.0.tgz | Bin 7104 -> 7119 bytes connector-index.json | 10 +++++----- connectors/google/gmail-playwright.js | 1 + registry.json | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/artifacts/gmail-playwright/gmail-playwright-1.0.0.tgz b/artifacts/gmail-playwright/gmail-playwright-1.0.0.tgz index 037406ff074aee43e7621ef80d76e165d9d66a19..edb30071e3a112480bd416779aea9b8c4032964c 100644 GIT binary patch literal 7119 zcmV;=8!+S_iwFP!000001MEC&cN52w`RrfO*3HHeNTZi+h(U0S!HIVPbL@odUZXf| zO-mXW&1mOg;R`xY=`2PVlH7)z$T`>K-}m@4on_1D|_a zTl5z`#lI!b+ucs@JGS-ZQTy(@G){N~OMZ>D{)8W=?Tx}9{-RQFbN=7!b+^9p|7(0S z|0gr<`A&R3`2uSoQ@1xatN7oA|ML9rLestP*jM4nzc~LN0?XO^nIFW1+B8Y#JMH#j zv2YfBCyXZT9)s#&3=DNPBL@k6b4hCOSE2R`|Pwfo%^kxv-!M>h-$&*4nHm7cKE)k27uq}|JV3@0CQ5)B5-;xqSy1KR2-(!NZ3PUdk$j9>B!cA&jdD+ zt6)cBB)oYN>-A=H9$Zj&7*2d4n_Ur)A{eEBJ$pY7qa_szYC6t9oRU1Z>Q}}`!BKJu!T0_ zF$trKf9vn+J;iqqkM53rToj)G*LNbx@#-`dKCuj!IIb7oW%#5_qO5N4jRSr{h_gqW0qzT`9mB|0M(jw|DYls>0s5V=%O!!`YF zC?$^1`I+Z?i6>>EHWl0zZY$yML}U4Od6tB|2ZkLUwQ5b}NIDsI{ z_b_KFU9xKL3_kv=9eE5qX&cgS07?7=8!)WOicoKpeF>}2bBLY~CSal8$2w5@) z@K5+`&c?I>VfF*NsK81Fb+^Flm;f-;i6#?7Mee#0EKI_37A8}+03s<0O9IBSog%!r z3NsmI-Uc-Z0W={H7TvsnXe05F(Gk!xPW_|`lfawL#jP-zLY$L}k}Z6=9|n^cge=E0 zwf&Nq(x6tuz{vw$r$`dCpsA#+Y53aA=-A9QGBgl?RURVA{a3bfjkzBS6S`jhS);M9 z|H=HnsRPM|&lmu#_W#|j&b@m@|G#Kh+ z#3%c-XVTsmYH(>JvB!fjz!-vQnCSZpLq!&Hg?zn z_=!0>i-4lVtVW`iw#qqko+Kh7=)a$IT0ai&zIo%whwr7Ex-8?%&2j(956>u;O=WPF zA;p1EW0XkGg=oW>IBWw2i%%jxvjlOY3Txi_8{(_}wuW~Kh}(79n__*O@H3En;HR68 zk`|yzRWpKEhVO>MUZ9o)A!yo^2jKMiC<@~kq7h$UeTFT-GK6WuxaI~%nI25WH5ndj zU2mhOOB?-k!P+ZI2&>0p2G5NpVJeDolNI$xmJdlKv|y^9_@ckY6;pC+kJH)^~^3Zk{OF{q+>HL@Mh@@6QmsW zD(AeCSup>(2qwvN2|{*Ia0?)LdfWm~b|`Qq5D^ePPI9LQSRM{M-{<%kh-48)?^+{1 zPtr)#hGoV`oh4kk9QZoOcCjaoyh=%U4*7P#+6=>?g#CK-+~iz+!R^5SL(K$LQ~0X$Rx8(inZkMq=pc~36CNQJeCoxC7hv)jg)=hw z|9*VScn zv`@vz=aKYy3M9HbU=8doftc_p1^*Y}eF`B1NA!JT!7+egu1ElQA^WPReyd9tx2R4& zQP(y=ys=Vh?Of+9c*ZdEQb<|KE2Kf_-ln$wLMl0X31FDX4CK;}2E9(zvQ|L&>5IQI zKulntF=}P`_3GlOj{Wkd{#OMnRa&a0W9;)uTnM59AzVwUX8Z#9NY(GGWCMX<7-NW* zVS`XEm>W_O8mDtSkGEv5BtojBtZ@N0t&5R)juB`}T4RxnriJ#xorqi~LV|3$6^H(L z0p1vr>f&{fcjDqKhQrEn9QGe#E=zW2eDrPsn`e~!B_44d*q!2FmwWyNMm_2}4#q!WkWArm zc*SnUp77lVES?JAZ^d&l^2Xi>cQ%3uqsbPKv$I(!^FXkfEgHyB&%qAIcsR$|I3+t* zEFU?=5*Cum`ExH4v6&Wlw$7W6T; zD$510$TsqshRTVCxj}Y(mOa}JdpTjvU}Odr&(yEG(-oC+J=aQ8>yLN3n4fmBG>T=z zJ0GFxqz6wQv&Op(Q&nu_Gxg;RHsCj$`x{6g=?-O!2%~Uz(A( z{;e|(nc6yLuXJ9F*vv#S4c(o>6{*~%o4f4k;R~!is5uCwy|zZ#7kl-5_m};{qZe$T zUyyj9dR{*}IzBjji5+y4&b}e7#~kk+zItY+X7}oJ|K9G)!F7g zEGqDM&*yQB&L|Q*hCCXc@UBr9lVifKMDZBrd;p)@V1q}j{^9zw9mkOXnroMh98UWn zFFs_T1|gQ+J}zzg`fH^GZ%WdKzt#%2Ifl3v5j`V`r4fy?qm z%)wdNBGyI6PwZrNG1>M0t)! zdbY+T2LR$<(4#`5O0CQ_&K**Mr$E>`3dzCop^~459H>Ix9c>Y1t3H3+F(y>D%Y5)S zoy~Z3kwwX)5af2p?B}uLQ6#P*}0A+nf7A$)m<&i)U zElM9>kQ)MNg0wCi(!CSEedTh~h-rLOZs$Oe-T+{fv;7y8_~t;O`6eESlHx+KlPw9n=V z-j4^9PKOOeW-_Y7+T~9=KBI3Lb0mRX;ihu{J798ec*s#lbv+gdZIC9&v1y!6S7HCb%N1ug|e^e6E-p!I#`~HKC(c91q>R3E# zTsnWOrm7MnMI2k^V?Gu?0iHlQ9$y3_*mUGK8D{k}488}fZ-gJKWp#*rbuUI}56qW` z12ZdYybD7Ny`l?ar1*k+iByOQtS%S*z?3K4VcVE{DW$cX4OVkx(I_3l=uqu^gI`K(yjL>f)S=pEfjIZk+z#u>2;ZL-ZNAl7;U4`I>I z{Go8HXY(?o7j%^CU=Bdx`V=x5yqCn*L3=j}f$m15fUFJ44M!=SFF9nwb)~t!LACf3 zG+Be%9E|58jNICAqfrG*wXUyM0JLg18}|E)_Unt;+l%z_A}nWS8RlL#>J57QLrQEp zYl}Eu=AuZfD<*PX0m2izXhpFCc>zF)VNMvrqcECNw6bRDNZ2CEo|=of4PPqMMm0JjB^ z&q&nUZ{Ear+mj~SV9*Ky)2zO)0xUy09Nca%VC)FSBzcKA>ay0M_f28-L|( zv}%&(Eo-foCA4BR*3mma$BVKkTWKrCY*Cw>xNPM+%F0{zy?ot_6sl(OS{8F&=n!(1 zMUfNfvW?l^jSS1k7aT21onX)qm5xZDQVlJ+$u#oh&6~8>={#x4zfa-M{V>5grC2)PHy5M&cm6rlI_ zyWP=`?r6&%U?dF`TD+HXCLbTlRg^O6R$JQihZgvKpvFV?677Yq*iGs*C5I2w*u21d z-`*_s>)6l)>^dk!-5>k8@%C|ylB*5*lL8y;E~B~Z(IX5OA-Pnn>~1%lId_loR%`t} zP`_i-j1`iX`DNS^<3YF7%^rcB(Lg4QZzY{ca|o29~_Jqd>P5#oiH_|B(&klmkDS? z>eLR4!}=}I7+egipTWfV85h*mm4*gObFdTSbGFs6D8jOpaL^zxsK#G8`)^AfCNCS9sTe_Zh=CK!K|VA z>Li8w*35iw0mx%}#k*k1AEM#0*M-JM8--artZq#+B%}$p&fy32M5>${g23P%Ci#A` zr62O`*S31~HK&HZOhw@iCcum7RoxMldXbG-6`%tYS6-&)V~~D%ebK0BTag%Rpv@vc ze1`|dM(GIb9Nywm`Yx}0aG!2oCc;<^r;LSi7uAyNoP`q-xB@ZDgq}>Af;cRjWLbix z`D6&GJ*<2J^m}W^Jkpr5|n5+cS zD+0VMR}Co@E9iRByBS#}zzUOsI99GzSYjh_o%g@76MBskeGz@d@`HXxYG8bHcX{Rx zh;Pyg$1O~yZ5a6u4K3eF+$|cYYN4#Oj+x9Bt(7f-%1qjWZ)PWzVpn02akv}{*{`s$ z2UlW|#i+lZwEpq;|M|z?|91${>YX+PvsHYrLQu;@5j)~pne0_fz;NzWDTY|Xf zR-ujZTS871M3FoxM-E|Al?qh@FcH@crd~P6bMsq9mS=fG{Y3tr+0xIPDXD1cbjO4t z!y?g4D2s1qZ7&bni{Wvus&MGL z^c!U?*NSmE<&xNww@1n>CV*Aq&20erb_;|ZVk-|r=e^)1$`d-*i_sCIsH%Yl=^$M7 zC=d>+vtXfiqbMhOJ|rKubeX9SmKN79+6~zHy2jH9$KzNezj>}a&&oIdEa+USVr9U5 z&-dWz2)Gf~8_K2SR#xJmmj(~Gu2Hn&YFh+Kic;nP(`qw0*ZMk&i``X7zwDIa5zGBP z-`I=d zFtF?SzBhV@P+#Gx_Lr#?f3pbxdKN)HQ~d=6pz34=`y7%&yL!HK$n`mnMpRuFC4Eo+ z41$q&`o*L$jrXO(T)W2G-72r~Ru^AQu98L(-ptb_((+2HT?JM%9HrEhkQACQWm(!v zftD*z_vBTn6?NXF(uq7i(FFhz9JsN%RD}oBgg(*_NZ|!^46#o)hG2~05%%Y-Sk`H? z0IB&hot;sMq5Hu8{&0|8mdkZ4*WI!6Dc(1{9^s34<-dc5X?RYqNBI}RJx4O8u1L@n zLq>peGRz;`uB%S%(G+dbKIn@g(9>v8(asApJ<$WX>)Hc#xA`@_c^Xgk0Mm;rn-SXW z4szFG8te?h-*@`BcuCwv@+t}^_`7U%y&t*&hp)TFiJ0%O9;AX{!hL#DSAPDly(`#l z8-}6xeuZQ7-~gG?4cH|+^w7%&47+SQ4rtoAS%5AMmb66|JYIO+ANVbkK8E_O6J->K&AQz^pp9dGwOy?UKQ0!o!6PNiaG zbA(l(i$<|yX{HvY@|{|trNpdTjmN3$GvkCiL1B7**N%UyVuQ#*yyQt8m-aRuC!MMA zrK`GfUs<|^?8QVgzaQTbD)s1uR7j)0mDfGRW1v6d`Rg3d zKQ9-4gMGQmF}u71eFc2}48^!Nvqz?7##-lf&iI$SeOLbS>5p`LK!(TT4** z961VCr&lJSC2G*4)RI5;d;3J0$=qt(C62_`FKbL3<*Vm|hApIER-}QV9lgLd7V}9G z`h2zfylJ_VB&^CLX~7+^?%sG4@~#4cdi_Bg;4ukS-WMxdi!<54|p8EFr6Sg7&MKqmMdNxeztlpwofe-y`s8CkV1$021t+aQz5Sd z>e}OmurL~TAbpfdSHo}Zbl&7>Qtr=&xOsRu<~bu6a~ntHO%vwMW`4x*)8lW^zIVL7 z2ah)H&RKuev*7zcM>+RjxV6uL3i$G+p{xUcRDF()EgqeVeeLz*+~CDv-N7mRhD@Bazf7;yf%2#ajk&70;qD-IZh*zEHl^~Iw1Yszn3s|)v25w!v^vfTBvfPVJ@zg+p3N)a&4`?B_YKGlm379FSYfZwK zkL=^Q!(&awq5%bnUx6p%=$Q3ob{TMu(K6|Y&QrDPbsA)|dl!T~x@*u%fovq{06%#H z#a3q8QuAi<6RdqYQ*p9ldCF$aHWT`YM|Zi*v5!8RvO|fXVIN159Q~A+oSI+@84iB7z_r3!C){L3OCc&3vIkIV&EL(~0EXyS&-n*5! z%7_?}h(UmdhiI*>s{Idlzi@xab@x0l00By}Y;UgCOvM7{(bLoK?itW&fA_^d9r$c* zZqi@)6#teyKkRmT-?7avkJ@+NrE$U|Sn_ME^#}YoZEq9?@fVeX+w*^`*WLWa|F7}U z{GZIY=R5Jm#%;0^)_lO3VkuCc{Awdk;hv;KNtR>_7jRy z?dck0Pv$(Cva#nw>z_7yk2d#UmOH2&^w@^8$sV#TXX9StSg+M}9{#1)UV_-)*m<vs^cJ(;XG>foQ+pq9N%XTokz~Y z9n%eX_b)2rtKnW5iLCyi2 zDUSuUB5cM3mn9Lu5I%z)jlF2b5f(s*y6LH4po<+&7O~tb`@^K!z+8BH-MO_oxrUM(N@9nhRY5yq}9JbI# zJSJgu`EUIly{GuW;n9P!kBj0H;QCG^IbNN`!Y7vDQYV&gKCK-SScdDI)`m@XLQeb? zM8FH6L4v`M5T_PGL&fP)I2SQ=pTIZ9KG46q-EiNDY^KrxLIDT-7syJ+p(@5?6AP?A zvKc)C&H&A0*ikZ?A}0-*noJT29>R!8GQqPKjQrHak+BF|5j9zyp8pQyYBCpy-6VwO zk-&(DCE|VJSX=OBUXtzI-aqvxmy!fC{(e6ZvuqF4d%^t_yFprW?P=g8G)wIzKdZ3& zU#5b+5b?VtoHL`EAm&LLfiUxg&B8cgAjG5;^d+YmDA5_Qa9kNDr1Uv8gUFS78m{SY zLn(25&d)vHOFStPwW;8)a9as~CqhtynjE&ubf{hYdi3hcYsT^26+_`!svCl=(q1I~ zI~Bo*Y}i7Gcm8EYGTrzw!7tJu>@IocipXwN6i{8;RJ#( z-@gRs4PF|8G2a7U7LKbzzPw{w3IKI9kPkAf(7=Qej@}9C1)wB7yz%Wkiuk2+(V3&S zyn4-3EhaS5Q$va~vT$M^0kI-!)ZGEEV*kaN`qPr11Ar3ogzulf~JzPrr~QdqhmAM$k0FlR(Xgd_utscHRgUSOz3*`M~%k5 z{wMSQrVb<na3elW+PzM% z)9Ut=%t$}^z0B?O#E37PiI+^%^E4I_INJpL31x5>Jm4*S0F}lEmAj(kdBI9ClnW(Z z;*)*aHEHjG8eAGl?D8NCFos|nCi;FzaKVD2KPjN$u$K_hT(bLCf=a7np&iT^2nyPF zkDL>q0M3+Wdofd(DlC_Mea@`uxS>#SQ=vny*rwXD(wb^0%%SMb%$LP8_0^_`YSP6k z^>wLWl}Ci?RfQ%~T+;Z-f{-Z*lteXU6~?9LsNi;r?0hR{VgyK3H%yh}XI=t`jUBcR zeqxT!BA{q7tC6Ust#ZzsCy9s%`d?2vtsjREPEQ^A@V#_XS7m&;J?=mG;W@>!sSM6C zq&N_2j1uX&5N$XUhpnSv@kzvImLRTIVa;2ALwwcW*6@A-al0;iTda>0eh!ik{B+Y% z(gHN8YDN&t@LhM<0cuGQf~HM*08XEeqA-pj8u10zXV?NPLzpIvYi?ka>A_@Nli{(} z%{F?rw9!u&tlgr7uzDA2deU6WTNETuAt~KKG zB#lIESZ0jWS;Do;fv!1- zQK^r|$4mnS1UT5nm|b|WhsG!jz=~4(g{WbOfxUh>nNT8Bz)>A!+}0wz!Q=HLq$Nfs zipZoK!JuStEor9IH>YLXrTknquV1WC3Q@he{7;sWhDChJ zYbL5!m!FCq*KPBo|Oz^DkWf3d^ zUdntmCMNZ~3SvV{sot@=j3NjOa_5wFt)P~E1RZ#UY z--i-6ob!j35Y&NSDPB1bR0*LVGdBQK0;d%C^%a&Jh--zhP-e~bU9YkQ!+kiHn1Rhj zO-7@Q@n!e!dIQQTDk7)6xKsP`+@YG@CdQs3olN*0F%ckoRP`@ z_v6FY>{vp??=tM0^GiSEu1OYSoh>>`s+s#GlrR$LdsHJAq_(JF176gspRZ6fMF&xkV`)u^g30`S^?pwFaE{= zF@b%?sFmf{>x-v4_RF98Ul*)YX{nZuvCk)QA&3fua4o5t@k`(%Rll>64FrN=j3HWv z4MMqOZb(UJoX+t)-jcbJ2&s~?#s%24E=J}#MxZTejYTq=7TODUB66Jw39{u@9Qqdp zcwA_Xc*J#JcZzFfg@p1<<@gqf z%1zPJdCHw}%x;$488-xVcYHpjU{pmWDx2LIrRB>tH_xS5(UNTq{kjKilqNe%i&-D3%TH ze}tx!9z1=_8Xq)FRk4xJ)R%(+!#P>ZUl|g7J4O9}|H1QLv>(pz@fTZT_4#jqb8Bm} zc>izf;pVsV->>m$-@m`c?z6(puk1*qo@Ema7>Hx_)%%;?F)qK5jgZuxY=_B<{`T!7 z%vvCaNV0oMvha&;N)?6?oJt%YL((tkh7%YmI*!p-Q1HYTFvas`eQ8GC z`nS$FWNPb}z0!FxVlxxTG<3HMSEO>6ZXU1~hX+`DP;(GUdu@%fFZSlu&M$k1M+a=6 zUyyjLdfvV~I^I8gjU9B8&b}e7#~kk-zIkb;X7}oJ|L)G~!`J(}JFm`Y`gI?pbKH+E91(!xuIS3w zfE`o7t>?3kA6c!|aFXy#Eu^B*=q}On>K1DWM;HrEP zb8uF+h;`9%c@;ggu!ic^JhXX!o_fA}M0Wy_-yf(Z22rMPk11$vn%l2UDKIezQJ&+G zo~?1o0f6`y^r+CNQY&+fbDNal1rWB5Lb88+sN|<12da>FTU$igs?Q&{jR}?QG9Nrn zXEPpMW>NAe1i9Td`+4U0ROLn*o7ZmWbf#p?U;q}7Ju|Q3)L7YTIj(@5!;kI2T#vbC zU050OjP0B;&N_j3xRaW;pxefNvcn^Ne;J;v7h9}EL;aELBQpT(my)-G^*G0i>95w- zxWRt-p~%Nf1j&0C(L0ovH^E+blb|kTp@|0z1%H$gKv|)ywM;VQG+QpA1bV;EoZ^XaaI1R}A)_{>^qX0ArV(R;bJOq3|ZN@~PI2WDjP@9W; zd4j@x(qX*&W=T><6EB(Kt?MO^QrGz*WP?cv?&Ivj3w>_g)?)fjzI_QpU6STt+Gq0w z@5h5lr^ALKGa1!k?eZrbpV7CBIg-GxaML+}9Wc2!JmjdOx*m&!Hb@iX*fda;RT{5m zI%Cg=yi{(gPPs7g@LrLMnTW|jh>!6S7HCb%r=Nw8zf}^_-p!I#`~HKC(eI%b)UkNf zxN?45O;sgEia55+$9yb)0z83qJiZJ@u<6KeGR*2{7<>;{-v~ce%jyvM>Ryb{9-A*u z24+^)_yC3&dPSGUNbv>t5~&aqSY0mqfhkY8%^qUzrIglkHdxJ(O;>D$Aw8>0x6?3? zk#7bigHgj?G4_R+*SnjYj)H%+r=>N@Lm$T2im({2z1vQ1!QeVZa7Nue90jbt}D&;b*jam zpvfB4=3qP*VdU0^>y0W{s`cKz3V>GaX5D^&(SCg~dwY>yUWDb$EW_NZM!i9gze$NL zXKfM3t6UU`b;U%ki#!saokcnjhg1!$Oa+DWJ8e8J%m#a*$14A!sFy@1bOBhMi>S9_ zJQs+kyRx?R60kAF>44ia`jf|N$M@^j(&%wchD9JMhOXoEXJ8cr5&eQq;7Qik8{oDe z@)?PG`}8z^(4I8eI)hdSm}d2*ZAwotVC^7e^=I4k+DiPA7sg|lrCxrg zESRoWiACFkL?liqOl>TQq+Nu}rNG;)QjG>>Y6L+?uAW&b>$ec_hF#nRy6?&u5V@qh zh&_Oal{H*_l2UvqQh~IjX|3LA%9T`?T*p=+)P=n;k~@>Zd6}In@c|vn2e4kJ-1sYR zqg9hMZ&_=#ETI*nv5wvWI$o4T*-Be6W{cY7#APerQC8lv@8#=eq);`Jx3ZXXphL)2 z7DY~^%Qj|vw=ygvUvRW6b%H@dR5~JoN;S0PCez51)6=xq={#@Azc1j=-pjqcA!%vF z99xU+0a~F8*y${8A49DAoJZ(UbGy(Z$;ZGT8Y4rLbwH@o>5grC2)PHy5M&cm6rlHy zy4}%_?r753Rg^O6R$JQihZgwjK#hm&CE5*Lv6IwkN)8{Uu{pqd z-`*_s>)6l)>^dk!-5>k8@%C|ylB;$3lLG7P0i(I>=~E0BA-Pnn>^^KbbM7AFt=9S@ zpnk`u$<-3sKh@h%R)Eco%s^yG;R#qQK<_n(xiJ(viQfZVb1fT@rHaD%87m|&_cfpH z*|ONcmce$L9fvbv>bbrTF=EU@NM6;8q-b&??>tTLJ~$XJ_%f2eJ7H=_Nod29FB8y) z)Ttd7hxJ>aF}N(WrRqmH^oy5z4JSuv%dY1WEhQT_BpOf(BC%ZFqCdC9l%2%mh#K50c!vF*6s1|^9@w~7WUQF~lHnH;k;e)&B!3;Z)@ zB9a|V{rWn(^L5H`?J*cB?8-O1a*$Rm|C0Es*RNiXHnQGW0SSOuJNn^=+yaFdgIPoK z)kzBVt(p1W0+46+iVwh&KS9G|uM3S&*9)_@S>2juNJtZGox>04iBvf^1cAXjO!EC= zOF!h>uWj|}YfcS+nTo<4On?{DtGXj9^&%UwDnJJ)uDndo#~}Uk`l35ChTx}4 z5vl>0xak&Cubtz$`7I;Mv*^EmB7c=^>Bq{HR5W$cVuFcbk!U6$#dkJWL(7t3{8U!_ za&ToS-I|q!LMFI+9)`%SA+y;t9!+<@J?r}1>c@i-RApFLNeO68k>7IeB)u`*!3>wEBY1l)-04ds|}D=Tr( zOM@S~r%|+`V_O7DN=xP#(rPoA)4h8rE_PQTiLw)er!4m?dt)z(lYKWkpE~IbpMmbl zH2=D4kX`vP)d7unF3(#l;4HJfwNi9*eFnjrI!m~^K@sX}U|?6}eQ)#*p}xVh=`U07 z{bmvV^(=yZrYZ^wK-KvO_BkYlb`5;#sOobZaH#4mO8Tz+F#{v-^t(r48t+MkxpAeo zyIEeTtuCROj3bRAykDmao#pjXyOyhFI7+E0ttd2M;<2=q0xegb(a8%>E2_0i1rT`% zqSJU0?7OkL8imKqgg(*_NZ|!^n6O9peqfB@5%%Y-Sk`H?0IB&pot;ymp!?YV{$!9{ zdCPSySI@EY1>PFG8R3g~<-dc5X?RYqNBI}RJx4O8E;`T@Lq>peGRz;p-cz00qbb^= zebCoEpr_HG(wi4#dZGt%*R==gZu6UX^E9670j3vMHY2p#9po;zG}t+Wzwh*+?~=HS z%4k92QVu=r1ti1;I3m&A4yDr4spvkmRQ>P7v6I-I%(SQr5|Y0Y z?7R}kaqQ%?OE)_;y4b7c9#TQUi#>1mA$7e@NBxRhB+iziXLEv7V2H+XU}>cmqVj`U zp`^sBTTLcu=ri+#8$ls@{nn0st73!5K%DcXj!AnR4~0g`ed%(i+#_oj$X-sRDiIw_ zwPf0zc|?8!OI2x;!}%a;j6uKMm`bnTcRE;oda^y)Ox%{OSUcy*xr5K@nagN zUzijSJ`Bvn7wZkL@V?kQ6?cyszpBlkS@JS1k+n2L{TS z|H8#SN9x$iPD619-s9dL{tM@%eD7K#3KsYL9s>7AO68ASDia_4YeCK8i@u#i3~Km^6{+>M2@ba)d(w@H^L?8TP+-LZ%J>nQ8B#3rL+2?hto=k;k)}P;*cl~$ zh!2m4_eiRSf3tWsV=7C0eL1D?4Bi{8X)200A^vfS(vJ8q{@z_9_2^64s4=@_;)C#-DLM}5*x|)VLAGMEX08g|k77g$?d<33^ zqkY!% { const snippetNode = row.querySelector('.y2') || row.querySelector('[data-snippet]'); const dateNode = row.querySelector( + 'td.xW [title], td.xW time, td.xW span, ' + 'td:last-of-type [title], td:last-of-type time, td:last-of-type span' ); diff --git a/registry.json b/registry.json index 2b52747..cc39dd5 100644 --- a/registry.json +++ b/registry.json @@ -386,7 +386,7 @@ "metadata": "google/gmail-playwright.json" }, "checksums": { - "script": "sha256:e65b818375945ed41687e6bbfe5c83b5f6a564f53c3424e50dc91f666c5e914d", + "script": "sha256:21d7e4853f41a86a6de47c7dd833f0ba21dce22d9ed7fd51548f7b33c27b7279", "metadata": "sha256:b3f80f45f9c38e0580099bfc612a847592f34af8815b0657f38333c017a663c4" } }