@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 +1 -1
- package/src/accounts.ts +25 -1
- package/src/api-client.ts +29 -8
- package/src/outbound.ts +20 -3
package/package.json
CHANGED
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
|
|
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
|
-
//
|
|
90
|
-
const
|
|
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,
|
|
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
|
|
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
|
-
|
|
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 (
|
|
125
|
+
if (!seen.has(addr.email.toLowerCase())) {
|
|
105
126
|
ccCandidates.push(addr.name ? `${addr.name} <${addr.email}>` : addr.email);
|
|
106
|
-
|
|
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
|
-
|
|
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
|