Skip to content

feat: Gmail integration - #124

Open
claude-for-timatic[bot] wants to merge 10 commits into
feat/51-customer-email-domainsfrom
feat/52-gmail-integration
Open

claude-for-timatic[bot] wants to merge 10 commits into
feat/51-customer-email-domainsfrom
feat/52-gmail-integration

Conversation

@claude-for-timatic

Copy link
Copy Markdown
Contributor

New integrations/gmail package (Saloon, OAuth like google-calendar). It turns the emails users send and the drafts they save into events without a duration (email_sent, email_draft_saved). The customer comes from the recipients' email domains (#51), and the budget from the existing previous-booking logic.

Closes #52

Stacked on #101 (#51). Merge that first; GitHub then retargets this PR to main.

Decisions

  • Polling the Gmail History API every 15 minutes, not Pub/Sub push. Push needs a GCP topic, a public endpoint and a watch renewal every 7 days. A delay of minutes is fine for suggestions, and polling matches the calendar sync.
  • gmail.metadata scope: headers only, never bodies. Only To, Cc, Bcc and Subject are requested (metadataHeaders). The subject is stored as the event title, and recipients are only used to resolve the customer.
  • The first sync starts from now. Old mail is not read back, and the user instructions say so. When a history id has expired (404), the sync also restarts from now.
  • Message failures: a 404 on a message skips it, because Gmail replaces a draft's message on every save. Any other failure (429, 5xx, 401) stops without advancing history_id, so nothing is lost.
  • external_id = Gmail thread id. Replies in one thread form one sequence and learn the customer and budget from earlier bookings (Looking up previous bookings of recurring events breaks after event pruning #35).
  • Dedupe on (source, thread, user, type, ended_at), because events has no column for a message id. Two mails in one thread within the same second collapse into one event, which is harmless for 15-minute estimates.
  • Drafts: one event per draft message that still exists at sync time. A draft sent before the next sync only becomes email_sent.
  • Weights: email sent 40 (written work at a known moment, like a PR comment). Draft saved 45, slightly weaker because drafts may be abandoned.
  • Customer: recipients in To, then Cc, then Bcc order go to App\Integrations\Suggestions\CustomerEmailDomainResolver. The resolver moved there in feat: email domains on customers #101, so no architecture exemption is needed. Mail sent only to own-organisation domains gets no customer.
  • No overlapping syncs: WithoutOverlapping per connection (dontRelease, 600s expiry), so two runs can't race on history_id.
  • One Google client, shared with Google Calendar (GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET). There is a new GMAIL_REDIRECT_URI.

Setup (see integrations/gmail/README.md)

  • Enable the Gmail API.
  • Add the gmail.metadata scope under Data Access. It is a restricted scope: use an Internal audience, or external users see an unverified-app warning until Google verifies the app.
  • Add https://<api-domain>/gmail/callback as a redirect URI.
  • Still to do by hand: add GMAIL_REDIRECT_URI= to .env.example. My permissions don't allow touching .env* files.

Review

A code-review agent and a UX-review agent reviewed this PR. Applied:

  • Only a 404 skips a message.
  • Removed the architecture exemption.
  • Tests for pagination, a message that is gone, failed history lists and a 429 on a message, and incoming mail.
  • metadataHeaders.
  • WithoutOverlapping.
  • Admin callout copy about what is read and the customer email domains.
  • README restricted-scope setup.
  • User instructions note that only mail from after connecting is read.

Left out: skipping message ids deleted later in the same history. It only saves a request per stale draft save.

Docs to update after #96

  • suggestions/events.md:
    • Add the weights: email sent 40, email draft saved 45.
    • A mail thread is a sequence.
    • An email's customer comes from the recipients' email domains, and own-organisation domains never resolve.
    • Gmail reads from the moment of connecting.

Verification

Full Pest suite green (659), PHPStan 0 errors, Pint applied.

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.

1 participant