@mcinteerj/openclaw-gmail 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,117 +1,145 @@
1
- # Gmail Channel (Plugin)
1
+ # openclaw-gmail
2
2
 
3
- Connects Moltbot to Gmail via the `gog` CLI.
3
+ Gmail channel plugin for [OpenClaw](https://github.com/openclaw/openclaw). Supports two backends:
4
+
5
+ - **API** (recommended) — connects directly via the Gmail API using OAuth2. No external CLI needed.
6
+ - **gog** — shells out to the [gog CLI](https://github.com/jay/gog). The original backend, still fully supported.
7
+
8
+ Both backends coexist — you can run different accounts on different backends.
4
9
 
5
10
  ## Installation
6
11
 
7
- This is a plugin. To install from source:
12
+ ```bash
13
+ openclaw plugins install @mcinteerj/openclaw-gmail
14
+ ```
15
+
16
+ Or from a local clone:
8
17
 
9
18
  ```bash
10
- moltbot plugins install ./extensions/gmail
19
+ openclaw plugins install --link /path/to/openclaw-gmail
11
20
  ```
12
21
 
13
- ## Features
22
+ Requires `openclaw >= 2026.1.0`.
14
23
 
15
- - **Polling-based sync**: Robustly fetches new unread emails from Inbox.
16
- - **Circuit Breaker**: Handles API failures and rate limiting gracefully.
17
- - **Rich Text**: Markdown support for outbound emails.
18
- - **Threading**: Native Gmail thread support with quoted reply context.
19
- - **Archiving**: Automatically archives threads upon reply.
20
- - **Email Body Sanitisation**: Automatically cleans incoming email bodies for LLM consumption.
24
+ ## Setup
21
25
 
22
- ## Email Body Sanitisation
26
+ ### Option 1: API backend (recommended)
23
27
 
24
- Incoming emails are automatically sanitised to produce clean, readable text — no configuration needed.
28
+ The API backend connects directly to Gmail — no gog CLI required.
25
29
 
26
- ### What It Does
30
+ **If you have gog installed**, the onboarding flow will detect your existing OAuth client credentials from `~/.config/gogcli/credentials.json` and reuse them. You only need a one-time browser authorization to get a new refresh token.
27
31
 
28
- - **HTML-to-text conversion**: Strips tags, removes `<style>` and `<script>` blocks, filters out tracking pixels, and decodes HTML entities.
29
- - **Footer junk removal**: Strips common noise like unsubscribe links, "Sent from my iPhone", and confidentiality notices.
30
- - **Whitespace cleanup**: Collapses excessive blank lines and trims leading/trailing whitespace.
31
- - **Signature stripping**: Removes content below `-- ` signature separators by default.
32
+ **If you don't have gog**, you'll need to create a GCP OAuth client:
32
33
 
33
- ### Configurable Signature Stripping
34
+ 1. Go to [Google Cloud Console → Credentials](https://console.cloud.google.com/apis/credentials)
35
+ 2. Create a project (or use an existing one)
36
+ 3. Enable the Gmail API
37
+ 4. Create an OAuth 2.0 Client ID (type: **Desktop app**)
38
+ 5. Copy the Client ID and Client Secret
34
39
 
35
- Signature stripping is enabled by default. If you need to preserve content after `--` separators (e.g. for emails where dashes appear in the body), you can disable it programmatically:
40
+ Then run `openclaw configure`, select Gmail, and follow the prompts. The flow will:
41
+ - Ask for your email address
42
+ - Prompt for client credentials (or reuse gog's)
43
+ - Open a browser for OAuth consent
44
+ - Store the refresh token in your OpenClaw config
45
+ - Set `backend: "api"` on the account
36
46
 
37
- ```ts
38
- extractTextBody(html, plain, { stripSignature: false })
47
+ **Manual config** (if you prefer to skip the wizard):
48
+
49
+ ```json5
50
+ {
51
+ "channels": {
52
+ "openclaw-gmail": {
53
+ "accounts": {
54
+ "you@gmail.com": {
55
+ "email": "you@gmail.com",
56
+ "backend": "api",
57
+ "oauth": {
58
+ "clientId": "your-client-id.apps.googleusercontent.com",
59
+ "clientSecret": "your-client-secret",
60
+ "refreshToken": "your-refresh-token"
61
+ },
62
+ "allowFrom": ["*"],
63
+ "pollIntervalMs": 60000
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
39
69
  ```
40
70
 
41
- No plugin configuration is required — sanitisation runs automatically on every inbound message.
71
+ ### Option 2: gog backend
42
72
 
43
- ## Reply Behavior
73
+ Install the [gog CLI](https://github.com/jay/gog) (v1.2.0+), authorize it (`gog auth add you@gmail.com`), then run `openclaw configure`. The account will use gog by default (no `backend` field needed).
44
74
 
45
- - **Reply All**: When the bot replies to a thread, it uses "Reply All" to ensure all participants are included.
46
- - **Quoted Replies**: By default, replies include the full thread history as quoted text (standard Gmail format: "On [date], [author] wrote:"). This can be disabled per-account or globally.
47
- - **Allowlist Gatekeeping**: The bot only responds to emails from senders on the `allowFrom` list. However, if an allowed user includes others (CC) who are *not* on the allowlist, the bot will still "Reply All", including them in the conversation. This allows authorized users to bring others into the loop.
48
- - **Outbound Restrictions** (optional): Use `allowOutboundTo` and `threadReplyPolicy` to control who the bot can send emails to. By default, no outbound restrictions are applied (backwards compatible).
75
+ ### Upgrading from gog to API
49
76
 
50
- ## Configuration
77
+ Existing gog users upgrading the plugin will continue working with no changes — gog remains the default. To migrate an account to the API backend:
78
+
79
+ 1. Run `openclaw configure` → select Gmail
80
+ 2. The wizard detects your gog credentials and offers migration
81
+ 3. Authorize in the browser (one-time, ~10 seconds)
82
+ 4. Done — your account now uses the API directly
83
+
84
+ Your gog installation is not affected and other accounts can continue using it.
85
+
86
+ ## Features
51
87
 
52
- Add to `moltbot.json`:
88
+ - **Polling-based sync**: Fetches new unread emails from Inbox
89
+ - **Rich text**: Markdown responses are converted to HTML emails via `marked`
90
+ - **Threading**: Native Gmail thread support with quoted reply context
91
+ - **Reply All**: Replies include all thread participants
92
+ - **Archiving**: Automatically archives threads upon reply
93
+ - **Email body sanitization**: Cleans incoming HTML for LLM consumption
94
+ - **Circuit breaker** (gog backend): Handles API failures and rate limiting
95
+ - **MIME construction** (API backend): Builds RFC 2822 messages with proper threading headers
96
+
97
+ ## Configuration
53
98
 
54
99
  ```json5
55
100
  {
56
101
  "channels": {
57
- "gmail": {
102
+ "openclaw-gmail": {
58
103
  "accounts": {
59
- "main": {
60
- "email": "user@gmail.com",
104
+ "you@gmail.com": {
105
+ "email": "you@gmail.com",
61
106
  "allowFrom": ["*"],
62
- "includeQuotedReplies": true, // default: true
63
- // Optional: restrict who the bot can send emails TO
64
- "allowOutboundTo": ["@mycompany.com", "partner@example.com"],
65
- "threadReplyPolicy": "allowlist" // default: "open"
107
+ "pollIntervalMs": 60000,
108
+ "includeQuotedReplies": true, // default: true
109
+ "allowOutboundTo": ["@company.com"], // optional
110
+ "threadReplyPolicy": "allowlist" // default: "open"
66
111
  }
67
112
  },
68
113
  "defaults": {
69
- "includeQuotedReplies": true // global default
114
+ "includeQuotedReplies": true
70
115
  }
71
116
  }
72
117
  }
73
118
  }
74
119
  ```
75
120
 
76
- ### Configuration Options
77
-
78
121
  | Key | Type | Default | Description |
79
122
  |-----|------|---------|-------------|
80
- | `includeQuotedReplies` | boolean | `true` | Include thread history as quoted text in replies. Set to `false` for cleaner, shorter replies. |
81
- | `allowOutboundTo` | string[] | (falls back to `allowFrom`) | Restrict who the bot can send emails to. Supports exact emails and domain wildcards (e.g., `@company.com`). |
82
- | `threadReplyPolicy` | string | `"open"` | Controls thread reply behavior: `"open"` (no restrictions), `"allowlist"` (all recipients must be in `allowOutboundTo`), or `"sender-only"` (only original thread sender checked). |
123
+ | `backend` | `"api"` \| `"gog"` | `"gog"` | Which backend to use for this account |
124
+ | `oauth` | object | | OAuth credentials (required for API backend) |
125
+ | `allowFrom` | string[] | `[]` | Sender allowlist. `["*"]` allows all. |
126
+ | `pollIntervalMs` | number | `60000` | Polling interval in milliseconds |
127
+ | `includeQuotedReplies` | boolean | `true` | Include thread history as quoted text in replies |
128
+ | `allowOutboundTo` | string[] | (falls back to `allowFrom`) | Restrict who the bot can send to. Supports domain wildcards (`@company.com`). |
129
+ | `threadReplyPolicy` | `"open"` \| `"allowlist"` \| `"sender-only"` | `"open"` | Controls reply restrictions |
83
130
 
84
131
  ### Thread Reply Policies
85
132
 
86
- - **`open`** (default): No outbound restrictions. The bot can reply to any thread it was BCCd into. Backwards compatible.
87
- - **`allowlist`**: All thread participants (To, CC, From) must be in `allowOutboundTo`. Blocks replies if *any* recipient is not allowed.
88
- - **`sender-only`**: Only checks if the original thread sender is in `allowOutboundTo`. Useful when you want to reply to threads started by allowed users, even if they CC'd external parties.
133
+ - **`open`** (default): No outbound restrictions. Backwards compatible.
134
+ - **`allowlist`**: All thread participants must be in `allowOutboundTo`.
135
+ - **`sender-only`**: Only checks if the original thread sender is allowed.
89
136
 
90
137
  ## Development
91
138
 
92
- Run tests:
93
139
  ```bash
94
- npm test
95
- # or
96
- ./node_modules/.bin/vitest run src/
140
+ npx vitest run
97
141
  ```
98
142
 
99
143
  ## Publishing
100
144
 
101
- This repo includes a GitHub Actions workflow for npm publishing.
102
-
103
- ### Automatic (on release)
104
- Create a GitHub release → automatically publishes to npm.
105
-
106
- ### Manual (workflow dispatch)
107
- Go to Actions → "Publish to npm" → Run workflow.
108
-
109
- **Note:** Manual dispatch will:
110
- 1. Bump the version (patch by default, or specify major/minor)
111
- 2. Commit and push the version bump with a tag
112
- 3. Publish to npm
113
-
114
- ### Setup
115
- Add `NPM_TOKEN` secret to the repository:
116
- 1. Generate token at npmjs.com → Access Tokens → Classic Token (Automation)
117
- 2. Add to repo: Settings → Secrets → Actions → New repository secret
145
+ Create a GitHub release or run the "Publish to npm" workflow via Actions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcinteerj/openclaw-gmail",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Gmail channel plugin for OpenClaw - direct API or gog CLI",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/src/channel.ts CHANGED
@@ -27,7 +27,7 @@ import { createGmailClient, type GmailClient } from "./gmail-client.js";
27
27
  import crypto from "node:crypto";
28
28
 
29
29
  const meta = {
30
- id: "gmail",
30
+ id: "openclaw-gmail",
31
31
  label: "Gmail",
32
32
  selectionLabel: "Gmail",
33
33
  detailLabel: "Gmail",
@@ -71,8 +71,8 @@ function buildGmailMsgContext(
71
71
  ConversationLabel: threadLabel,
72
72
  SenderName: msg.sender.name,
73
73
  SenderId: msg.sender.id,
74
- Provider: "gmail" as const,
75
- Surface: "gmail" as const,
74
+ Provider: "openclaw-gmail" as const,
75
+ Surface: "openclaw-gmail" as const,
76
76
  MessageSid: msg.channelMessageId,
77
77
  ReplyToId: msg.channelMessageId,
78
78
  ThreadLabel: threadLabel,
@@ -83,7 +83,7 @@ function buildGmailMsgContext(
83
83
  MediaType: msg.mediaType,
84
84
  MediaUrl: msg.mediaUrl,
85
85
  CommandAuthorized: false,
86
- OriginatingChannel: "gmail" as const,
86
+ OriginatingChannel: "openclaw-gmail" as const,
87
87
  OriginatingTo: msg.threadId,
88
88
  });
89
89
 
@@ -105,7 +105,7 @@ async function dispatchGmailMessage(
105
105
 
106
106
  // Build the dispatch context
107
107
  const ctxPayload = buildGmailMsgContext(msg, account, cfg);
108
- const gmailCfg = cfg.channels?.gmail as GmailConfig | undefined;
108
+ const gmailCfg = cfg.channels?.["openclaw-gmail"] as GmailConfig | undefined;
109
109
 
110
110
  // Build reply dispatcher options using gateway's reply capability
111
111
  const deliver = async (payload: { text: string }) => {
@@ -255,6 +255,13 @@ export const gmailPlugin: ChannelPlugin<ResolvedGmailAccount> = {
255
255
  const client = (emailKey && activeClients.get(emailKey)) || createGmailClient(account, ctx.cfg);
256
256
  return sendGmailText({ ...ctx, client });
257
257
  },
258
+ sendMedia: (ctx: any) => {
259
+ const account = resolveGmailAccount(ctx.cfg, ctx.accountId);
260
+ const emailKey = account.email?.toLowerCase();
261
+ const client = (emailKey && activeClients.get(emailKey)) || createGmailClient(account, ctx.cfg);
262
+ const text = [ctx.text, ctx.mediaUrl].filter(Boolean).join("\n\n");
263
+ return sendGmailText({ ...ctx, text, client });
264
+ },
258
265
  resolveTarget: ({ to, allowFrom }) => {
259
266
  const trimmed = to?.trim() ?? "";
260
267
  const normalized = normalizeGmailTarget(trimmed);
package/src/inbound.ts CHANGED
@@ -76,7 +76,7 @@ export function parseInboundGmail(payload: GogPayload, accountId?: string): Inbo
76
76
  const fullText = `[Thread Context: ID=${payload.threadId}, Subject="${subject}"]\n\n${finalText}${attachmentContext}`;
77
77
 
78
78
  return {
79
- channelId: "gmail",
79
+ channelId: "openclaw-gmail",
80
80
  accountId,
81
81
  channelMessageId: payload.id,
82
82
  threadId: payload.threadId,
@@ -138,7 +138,7 @@ export function parseSearchGmail(msg: GogSearchMessage, accountId?: string, acco
138
138
  const fullText = `[Thread Context: ID=${msg.threadId}, Subject="${subject}"]\n\n${finalText}`;
139
139
 
140
140
  return {
141
- channelId: "gmail",
141
+ channelId: "openclaw-gmail",
142
142
  accountId,
143
143
  channelMessageId: msg.id,
144
144
  threadId: msg.threadId,
package/src/onboarding.ts CHANGED
@@ -7,7 +7,7 @@ import { readGogCredentials, runOAuthFlow, createOAuth2Client } from "./auth.js"
7
7
  import { ApiGmailClient } from "./api-client.js";
8
8
 
9
9
  const execAsync = promisify(exec);
10
- const channel = "gmail" as const;
10
+ const channel = "openclaw-gmail" as const;
11
11
 
12
12
  const MIN_GOG_VERSION = "1.2.0";
13
13
 
@@ -92,7 +92,7 @@ export const gmailOnboardingAdapter: ChannelOnboardingAdapter = {
92
92
  getStatus: async ({ cfg }: { cfg: OpenClawConfig }) => {
93
93
  const ids = listGmailAccountIds(cfg);
94
94
  const configured = ids.length > 0;
95
- const gmailConfig = (cfg.channels as any)?.gmail || {};
95
+ const gmailConfig = (cfg.channels as any)?.["openclaw-gmail"] || {};
96
96
  const accounts = gmailConfig.accounts || {};
97
97
  const backends = new Set(
98
98
  Object.values(accounts).map((a: any) => a.backend || "gog"),
@@ -128,7 +128,7 @@ export const gmailOnboardingAdapter: ChannelOnboardingAdapter = {
128
128
  }) => {
129
129
  // --- Account selection (unchanged) ---
130
130
  const existingIds = listGmailAccountIds(cfg);
131
- const gmailOverride = accountOverrides.gmail?.trim();
131
+ const gmailOverride = (accountOverrides["openclaw-gmail"] ?? accountOverrides.gmail)?.trim();
132
132
  const defaultAccountId = resolveDefaultGmailAccountId(cfg);
133
133
  let accountId = gmailOverride || defaultAccountId;
134
134
 
@@ -154,7 +154,7 @@ export const gmailOnboardingAdapter: ChannelOnboardingAdapter = {
154
154
  if (!email) throw new Error("Email required");
155
155
 
156
156
  // --- Detect available auth sources ---
157
- const gmailConfig = (cfg.channels as any)?.gmail || {};
157
+ const gmailConfig = (cfg.channels as any)?.["openclaw-gmail"] || {};
158
158
  const existingAccount = gmailConfig.accounts?.[email];
159
159
  const existingOAuth = existingAccount?.oauth;
160
160
  const gogInstalled = await checkGogInstalled();
@@ -330,7 +330,7 @@ export const gmailOnboardingAdapter: ChannelOnboardingAdapter = {
330
330
  ...cfg,
331
331
  channels: {
332
332
  ...cfg.channels,
333
- gmail: {
333
+ "openclaw-gmail": {
334
334
  dmPolicy: "allowlist",
335
335
  archiveOnReply: true,
336
336
  ...gmailConfig,
@@ -349,7 +349,7 @@ export const gmailOnboardingAdapter: ChannelOnboardingAdapter = {
349
349
  ...cfg,
350
350
  channels: {
351
351
  ...cfg.channels,
352
- gmail: { ...cfg.channels?.gmail, enabled: false },
352
+ "openclaw-gmail": { ...(cfg.channels as any)?.["openclaw-gmail"], enabled: false },
353
353
  },
354
354
  }),
355
355
  };
package/src/outbound.ts CHANGED
@@ -18,7 +18,7 @@ export interface GmailOutboundContext extends OutboundContext {
18
18
  export async function sendGmailText(ctx: GmailOutboundContext) {
19
19
  const { to, text, accountId, cfg, threadId, replyToId, subject: explicitSubject, client } = ctx;
20
20
  const account = resolveGmailAccount(cfg, accountId);
21
- const gmailCfg = cfg.channels?.gmail as GmailConfig | undefined;
21
+ const gmailCfg = cfg.channels?.["openclaw-gmail"] as GmailConfig | undefined;
22
22
 
23
23
  // Validate we have a target - prioritize threadId if it's valid
24
24
  const effectiveThreadId = isGmailThreadId(String(threadId)) ? String(threadId) : undefined;
@@ -141,5 +141,5 @@ export async function sendGmailText(ctx: GmailOutboundContext) {
141
141
  });
142
142
  }
143
143
 
144
- return { id: "sent" };
144
+ return { channel: "openclaw-gmail", messageId: "sent" };
145
145
  }