@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 +95 -67
- package/package.json +1 -1
- package/src/channel.ts +12 -5
- package/src/inbound.ts +2 -2
- package/src/onboarding.ts +6 -6
- package/src/outbound.ts +2 -2
package/README.md
CHANGED
|
@@ -1,117 +1,145 @@
|
|
|
1
|
-
#
|
|
1
|
+
# openclaw-gmail
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
12
|
+
```bash
|
|
13
|
+
openclaw plugins install @mcinteerj/openclaw-gmail
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Or from a local clone:
|
|
8
17
|
|
|
9
18
|
```bash
|
|
10
|
-
|
|
19
|
+
openclaw plugins install --link /path/to/openclaw-gmail
|
|
11
20
|
```
|
|
12
21
|
|
|
13
|
-
|
|
22
|
+
Requires `openclaw >= 2026.1.0`.
|
|
14
23
|
|
|
15
|
-
|
|
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
|
-
|
|
26
|
+
### Option 1: API backend (recommended)
|
|
23
27
|
|
|
24
|
-
|
|
28
|
+
The API backend connects directly to Gmail — no gog CLI required.
|
|
25
29
|
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
71
|
+
### Option 2: gog backend
|
|
42
72
|
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
60
|
-
"email": "
|
|
104
|
+
"you@gmail.com": {
|
|
105
|
+
"email": "you@gmail.com",
|
|
61
106
|
"allowFrom": ["*"],
|
|
62
|
-
"
|
|
63
|
-
//
|
|
64
|
-
"allowOutboundTo": ["@
|
|
65
|
-
"threadReplyPolicy": "allowlist"
|
|
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
|
|
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
|
-
| `
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
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.
|
|
87
|
-
- **`allowlist`**: All thread participants
|
|
88
|
-
- **`sender-only`**: Only checks if the original thread sender is
|
|
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
|
-
|
|
95
|
-
# or
|
|
96
|
-
./node_modules/.bin/vitest run src/
|
|
140
|
+
npx vitest run
|
|
97
141
|
```
|
|
98
142
|
|
|
99
143
|
## Publishing
|
|
100
144
|
|
|
101
|
-
|
|
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
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 {
|
|
144
|
+
return { channel: "openclaw-gmail", messageId: "sent" };
|
|
145
145
|
}
|