@mcinteerj/openclaw-gmail 1.5.0 → 1.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcinteerj/openclaw-gmail",
3
- "version": "1.5.0",
3
+ "version": "1.6.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/accounts.ts CHANGED
@@ -18,12 +18,36 @@ export interface ResolvedGmailAccount extends ResolvedChannelAccount {
18
18
  };
19
19
  }
20
20
 
21
+ /**
22
+ * Normalize an account key the same way the gateway's routing layer does
23
+ * (replace non-alphanumeric chars with hyphens). This allows matching
24
+ * "honk-keithy-gmail-com" back to "honk.keithy@gmail.com".
25
+ */
26
+ function canonicalizeKey(value: string): string {
27
+ return value.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+/, "").replace(/-+$/, "");
28
+ }
29
+
21
30
  export function resolveGmailAccount(
22
31
  cfg: ChannelConfig<GmailConfig>,
23
32
  accountId?: string,
24
33
  ): ResolvedGmailAccount {
25
34
  const resolvedId = accountId || DEFAULT_ACCOUNT_ID;
26
- const account = cfg.channels?.['openclaw-gmail']?.accounts?.[resolvedId];
35
+ const accounts = cfg.channels?.['openclaw-gmail']?.accounts;
36
+ let account = accounts?.[resolvedId];
37
+
38
+ // If direct lookup fails, try matching against canonicalized account keys.
39
+ // The gateway's routing layer normalizes email-format accountIds (e.g.
40
+ // "honk.keithy@gmail.com" -> "honk-keithy-gmail-com"), so we need to
41
+ // reverse-match by canonicalizing each config key the same way.
42
+ if (!account && accounts && resolvedId !== DEFAULT_ACCOUNT_ID) {
43
+ const canonicalizedId = canonicalizeKey(resolvedId);
44
+ for (const key of Object.keys(accounts)) {
45
+ if (canonicalizeKey(key) === canonicalizedId) {
46
+ account = accounts[key];
47
+ break;
48
+ }
49
+ }
50
+ }
27
51
 
28
52
  if (!account) {
29
53
  // Graceful fallback for UI logic that queries 'default' on unconfigured channels
package/src/api-client.ts CHANGED
@@ -85,25 +85,46 @@ export class ApiGmailClient implements GmailClient {
85
85
  if (!thread || thread.messages.length === 0) return null;
86
86
 
87
87
  const lastMsg = thread.messages[thread.messages.length - 1];
88
+ const selfLower = selfEmail.toLowerCase();
88
89
 
89
- // Resolve To: reply to the sender of the last message
90
- const to = lastMsg.from;
90
+ // Determine if the last message is from self
91
+ const fromAddresses = parseEmailAddresses(lastMsg.from);
92
+ const lastMsgIsFromSelf = fromAddresses.some(
93
+ (a) => a.email.toLowerCase() === selfLower,
94
+ );
95
+
96
+ // Resolve To:
97
+ // - If last message is from someone else: reply to that sender (standard reply)
98
+ // - If last message is from self: reply to the recipients of that message (matches Gmail client)
99
+ let to: string;
100
+ if (lastMsgIsFromSelf) {
101
+ to = lastMsg.to || "";
102
+ } else {
103
+ to = lastMsg.from;
104
+ }
91
105
 
92
- // For reply-all, Cc = everyone from To + Cc minus self and the new To
106
+ // For reply-all, build CC from the last message's To + CC, minus self and the new To
93
107
  let cc: string | undefined;
94
108
  if (replyAll) {
95
- const selfLower = selfEmail.toLowerCase();
96
109
  const toAddresses = parseEmailAddresses(to);
97
- const toLower = new Set(toAddresses.map((a) => a.email.toLowerCase()));
110
+ const seen = new Set(toAddresses.map((a) => a.email.toLowerCase()));
111
+ seen.add(selfLower); // always exclude self
98
112
 
99
113
  const ccCandidates: string[] = [];
100
- for (const field of [lastMsg.to, lastMsg.cc]) {
114
+
115
+ // When replying to own message: lastMsg.to is already the new To, so CC from lastMsg.cc only
116
+ // When replying to other's message: lastMsg.from is the new To, so CC from lastMsg.to + lastMsg.cc
117
+ const ccSources = lastMsgIsFromSelf
118
+ ? [lastMsg.cc]
119
+ : [lastMsg.to, lastMsg.cc];
120
+
121
+ for (const field of ccSources) {
101
122
  if (!field) continue;
102
123
  const addresses = parseEmailAddresses(field);
103
124
  for (const addr of addresses) {
104
- if (addr.email.toLowerCase() !== selfLower && !toLower.has(addr.email.toLowerCase())) {
125
+ if (!seen.has(addr.email.toLowerCase())) {
105
126
  ccCandidates.push(addr.name ? `${addr.name} <${addr.email}>` : addr.email);
106
- toLower.add(addr.email.toLowerCase()); // dedupe
127
+ seen.add(addr.email.toLowerCase()); // dedupe
107
128
  }
108
129
  }
109
130
  }
package/src/outbound.ts CHANGED
@@ -43,10 +43,26 @@ export async function sendGmailText(ctx: GmailOutboundContext) {
43
43
  ?? gmailCfg?.defaults?.threadReplyPolicy
44
44
  ?? "open"; // Default: open for backwards compatibility
45
45
 
46
- const subject = explicitSubject || "(no subject)";
47
46
  const isThread = isGmailThreadId(toValue);
48
47
  let quotedContent: QuotedContent | null = null;
49
48
 
49
+ // Resolve subject: use explicit, or look up from thread, or fallback
50
+ let subject = explicitSubject;
51
+ if (!subject && isThread) {
52
+ try {
53
+ const thread = await client.getThread(toValue, { full: false });
54
+ if (thread && thread.messages.length > 0) {
55
+ const origSubject = thread.messages[0].subject;
56
+ if (origSubject) {
57
+ subject = origSubject.toLowerCase().startsWith("re:") ? origSubject : `Re: ${origSubject}`;
58
+ }
59
+ }
60
+ } catch {
61
+ // Non-fatal: fall through to default
62
+ }
63
+ }
64
+ subject = subject || "(no subject)";
65
+
50
66
  // Validate outbound recipients
51
67
  if (isThread && threadReplyPolicy !== "open" && account.email) {
52
68
  const validation = await validateThreadReply(
@@ -119,7 +135,7 @@ export async function sendGmailText(ctx: GmailOutboundContext) {
119
135
  }
120
136
  }
121
137
 
122
- await client.send({
138
+ const sendParams = {
123
139
  account: account.email,
124
140
  to: isThread ? undefined : toValue,
125
141
  subject,
@@ -128,7 +144,8 @@ export async function sendGmailText(ctx: GmailOutboundContext) {
128
144
  threadId: isThread ? toValue : undefined,
129
145
  replyToMessageId: replyToId ? String(replyToId) : undefined,
130
146
  replyAll: isThread,
131
- });
147
+ };
148
+ await client.send(sendParams);
132
149
 
133
150
  // Archive if it was a thread (Reply = Archive), unless disabled by config
134
151
  const archiveOnReply = accountCfg?.archiveOnReply