Cameleer

Google Mail Stream component

The google-mail-stream component polls a Gmail mailbox and emits each new message that matches a Gmail search query, then marks it as read.

Scheme: google-mail-stream: · Crate: cameleer-cloud · Cargo feature: cloud · Consumer: yes · Producer: no · Apache Camel: Google Mail Stream

When to use it

  • Triage an inbox with an LLM: summarize or classify each new e-mail and route it to the right team, ticket system or chat channel.
  • Turn e-mails sent to a shared address (orders@, invoices@) into messages on a queue.
  • Extract data from automated e-mails (alerts, receipts) without running an IMAP client.

URI format

text
google-mail-stream://index[?options]

The path is a free name (Camel uses index); it is not used.

Example

Every minute, take the unread messages of the authenticated user, ask an LLM for a one-line summary and a label, and log the result. The route authenticates as the user with an OAuth refresh token. The OpenAI endpoint works with any OpenAI-compatible server (set OPENAI_BASE_URL, for example to an Ollama or vLLM server).

yaml example: google-mail-stream-llm-triage
- route:
    id: inbox-triage
    from:
      uri: "google-mail-stream://index?maxResults=20&clientId={{env:GOOGLE_CLIENT_ID}}&clientSecret=RAW({{env:GOOGLE_CLIENT_SECRET}})&refreshToken=RAW({{env:GOOGLE_REFRESH_TOKEN}})&delay=60000"
      steps:
        - setHeader:
            name: subject
            simple: "${header.CamelGoogleMailStreamSubject}"
        - to: "openai:chat-completion?model={{env:OPENAI_MODEL:gpt-4o-mini}}&baseUrl={{env:OPENAI_BASE_URL:https://api.openai.com/v1}}&temperature=0&systemMessage=Summarize this email in one sentence, then label it URGENT, ACTION or FYI."
        - log: "${header.subject}: ${body}"
        - to: "log:inbox-triage"

The body of each exchange is the plain-text part of the message (the raw RFC 822 bytes with raw=true). After the route processes a message successfully, the consumer removes its UNREAD label, so the default query is:unread does not return it again. If processing fails, the message stays unread and is not emitted again by this consumer while it keeps running.

Rust DSL

rust
ctx.add_routes(|b| {
    b.from("google-mail-stream://index?maxResults=20&clientId={{env:GOOGLE_CLIENT_ID}}&clientSecret=RAW({{env:GOOGLE_CLIENT_SECRET}})&refreshToken=RAW({{env:GOOGLE_REFRESH_TOKEN}})&delay=60000")
        .set_header("subject", simple("${header.CamelGoogleMailStreamSubject}"))
        .to("openai:chat-completion?model={{env:OPENAI_MODEL:gpt-4o-mini}}&baseUrl={{env:OPENAI_BASE_URL:https://api.openai.com/v1}}&temperature=0&systemMessage=Summarize this email in one sentence, then label it URGENT, ACTION or FYI.")
        .log("${header.subject}: ${body}")
        .to("log:inbox-triage");
})?;

How it polls

Each poll lists up to maxResults messages matching query and labels, fetches each one and emits them oldest first. Messages already emitted are remembered (in memory) while they still match the query, so a message is not emitted twice even with markAsRead=false. After a restart, messages that are still unread are emitted again.

Options

optiondefaultdescription
queryis:unreadGmail search query, for example is:unread label:invoices.
labelsComma separated label ids that messages must have (for example INBOX).
maxResults10Maximum messages per poll.
markAsReadtrueRemove the UNREAD label after a message is processed successfully.
rawfalseBody = the raw RFC 822 message bytes instead of the decoded text part.
userIdmeMailbox to read.
delay500Milliseconds between polls. Scheduled poll options such as initialDelay (default 1000), repeatCount and backoffMultiplier also apply.
clientId, clientSecret, refreshTokenOAuth user credentials (refresh-token flow). Wrap secrets in RAW(...).
serviceAccountKeyGOOGLE_APPLICATION_CREDENTIALSService account JSON key; needs delegate.
delegate (alias subject)User to impersonate with domain-wide delegation.
accessTokenA fixed OAuth access token.
tokenServerUrlhttps://oauth2.googleapis.com/tokenToken endpoint for the refresh-token flow (Cameleer).
scopeshttps://mail.google.com/Comma separated OAuth scopes.
uriEndpointOverride + overrideEndpoint=trueBase URL of an emulator or proxy.

useMetadataServer, authenticate, proxyHost, proxyPort and trustAllCertificates work as on the google-sheets page.

Message headers

headerdirectiondescription
CamelGoogleMailStreamIdoutMessage id.
CamelGoogleMailStreamThreadIdoutThread id.
CamelGoogleMailStreamSubjectoutSubject header.
CamelGoogleMailStreamFromoutFrom header.
CamelGoogleMailStreamTo, CamelGoogleMailStreamCc, CamelGoogleMailStreamBccoutRecipient headers.
CamelGoogleMailStreamReceivedDateoutGmail internalDate (milliseconds since the epoch).
CamelGoogleMailStreamLabelsoutList of label ids.

Run it with Docker

sh
docker run --rm \
  -e GOOGLE_CLIENT_ID -e GOOGLE_CLIENT_SECRET -e GOOGLE_REFRESH_TOKEN \
  -e OPENAI_API_KEY \
  -v "$PWD:/routes:ro" pmsudhi/cameleer run /routes/inbox-triage.camel.yaml

Get a refresh token once with the OAuth consent flow for the https://mail.google.com/ scope (for example with the OAuth 2.0 Playground and your own client id).

Common questions

How do I process new Gmail messages in a route?

Use from: google-mail-stream://index with credentials. Each new unread message arrives with its text as the body and its subject and sender in CamelGoogleMailStream* headers.

How do I read only one label?

Set query=label:my-label is:unread, or labels=<labelId> with a label id from google-mail://labels/list.

How do I keep messages unread?

Set markAsRead=false. The consumer still does not emit the same message twice while it runs.

Does it use Gmail push notifications?

No, it polls. For push, call google-mail://users/watch with a Pub/Sub topic and consume that topic with google-pubsub.

Found a problem?

If something on this page doesn't match what Cameleer does, or you hit a bug while building with it, please help fix it, whether you are a person or a coding agent: open an issue that describes what you saw, then fork the repository, fix it, add or update a test, run cargo test --workspace, and open a pull request. CONTRIBUTING.md explains the ground rules.

Edit this page on GitHub