zele 0.3.12 → 0.3.14
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 +8 -1
- package/dist/auth.js +1 -1
- package/dist/auth.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +10 -8
- package/dist/cli.js.map +1 -1
- package/dist/commands/attachment.js +2 -3
- package/dist/commands/attachment.js.map +1 -1
- package/dist/commands/auth-cmd.js +1 -2
- package/dist/commands/auth-cmd.js.map +1 -1
- package/dist/commands/calendar.js +4 -6
- package/dist/commands/calendar.js.map +1 -1
- package/dist/commands/draft.js +2 -3
- package/dist/commands/draft.js.map +1 -1
- package/dist/commands/label.js +4 -5
- package/dist/commands/label.js.map +1 -1
- package/dist/commands/mail.js +24 -21
- package/dist/commands/mail.js.map +1 -1
- package/dist/gmail-client.d.ts +11 -12
- package/dist/gmail-client.js +55 -40
- package/dist/gmail-client.js.map +1 -1
- package/dist/mail-tui.js +165 -187
- package/dist/mail-tui.js.map +1 -1
- package/dist/output.d.ts +3 -1
- package/dist/output.js +7 -2
- package/dist/output.js.map +1 -1
- package/package.json +7 -5
- package/src/auth.ts +1 -1
- package/src/cli.ts +31 -30
- package/src/commands/attachment.ts +2 -4
- package/src/commands/auth-cmd.ts +1 -2
- package/src/commands/calendar.ts +4 -7
- package/src/commands/draft.ts +2 -3
- package/src/commands/label.ts +4 -4
- package/src/commands/mail.ts +24 -19
- package/src/gmail-client.test.ts +8 -3
- package/src/gmail-client.ts +64 -58
- package/src/mail-tui.tsx +419 -418
- package/src/output.ts +8 -3
- package/bin/zele +0 -27
package/src/gmail-client.test.ts
CHANGED
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
// Captures entity/encoding regressions in snippet fields from Gmail metadata responses.
|
|
3
3
|
|
|
4
4
|
import { expect, test } from 'vitest'
|
|
5
|
+
import { OAuth2Client } from 'google-auth-library'
|
|
5
6
|
import { GmailClient } from './gmail-client.js'
|
|
6
7
|
|
|
8
|
+
// Create a real client instance for testing (no account context needed for parsing tests)
|
|
9
|
+
const auth = new OAuth2Client()
|
|
10
|
+
const client = new GmailClient({ auth })
|
|
11
|
+
|
|
7
12
|
test('thread list snippet decodes HTML entities for TUI preview', () => {
|
|
8
13
|
const rawThread = {
|
|
9
14
|
id: 'thread_1',
|
|
@@ -16,7 +21,7 @@ test('thread list snippet decodes HTML entities for TUI preview', () => {
|
|
|
16
21
|
],
|
|
17
22
|
}
|
|
18
23
|
|
|
19
|
-
const parsed =
|
|
24
|
+
const parsed = client.parseThreadListItem(rawThread as any)
|
|
20
25
|
expect(parsed.snippet).toBe("It's ready & waiting")
|
|
21
26
|
})
|
|
22
27
|
|
|
@@ -38,7 +43,7 @@ test('message snippet decodes HTML entities for detail preview', () => {
|
|
|
38
43
|
labelIds: ['INBOX'],
|
|
39
44
|
}
|
|
40
45
|
|
|
41
|
-
const parsed =
|
|
46
|
+
const parsed = client.parseMessage(rawMessage as any)
|
|
42
47
|
expect(parsed.snippet).toBe("Built with Opus [4.6](https://4.6): you're in")
|
|
43
48
|
})
|
|
44
49
|
|
|
@@ -54,6 +59,6 @@ test('thread list snippet strips zero-width and preheader garbage', () => {
|
|
|
54
59
|
],
|
|
55
60
|
}
|
|
56
61
|
|
|
57
|
-
const parsed =
|
|
62
|
+
const parsed = client.parseThreadListItem(rawThread as any)
|
|
58
63
|
expect(parsed.snippet).toBe('A host sent you a message')
|
|
59
64
|
})
|
package/src/gmail-client.ts
CHANGED
|
@@ -353,7 +353,7 @@ export class GmailClient {
|
|
|
353
353
|
const cached = await this.getCachedThread(t.id)
|
|
354
354
|
if (cached && (!t.historyId || !cached.historyId || t.historyId === cached.historyId)) {
|
|
355
355
|
return {
|
|
356
|
-
parsed:
|
|
356
|
+
parsed: this.parseThreadListItem(cached),
|
|
357
357
|
raw: cached,
|
|
358
358
|
}
|
|
359
359
|
}
|
|
@@ -371,11 +371,11 @@ export class GmailClient {
|
|
|
371
371
|
if (detail instanceof AuthError) return detail
|
|
372
372
|
if (detail instanceof Error) return null
|
|
373
373
|
|
|
374
|
-
const parsed =
|
|
374
|
+
const parsed = this.parseThread(detail.data)
|
|
375
375
|
await this.cacheThreadData(t.id, detail.data, parsed)
|
|
376
376
|
|
|
377
377
|
return {
|
|
378
|
-
parsed:
|
|
378
|
+
parsed: this.parseThreadListItem(detail.data),
|
|
379
379
|
raw: detail.data,
|
|
380
380
|
}
|
|
381
381
|
})
|
|
@@ -397,7 +397,7 @@ export class GmailClient {
|
|
|
397
397
|
// Check cache
|
|
398
398
|
const cached = await this.getCachedThread(threadId)
|
|
399
399
|
if (cached) {
|
|
400
|
-
return { parsed:
|
|
400
|
+
return { parsed: this.parseThread(cached), raw: cached }
|
|
401
401
|
}
|
|
402
402
|
|
|
403
403
|
const res = await withRetry(() =>
|
|
@@ -408,7 +408,7 @@ export class GmailClient {
|
|
|
408
408
|
}),
|
|
409
409
|
)
|
|
410
410
|
|
|
411
|
-
const parsed =
|
|
411
|
+
const parsed = this.parseThread(res.data)
|
|
412
412
|
const result: ThreadResult = { parsed, raw: res.data }
|
|
413
413
|
|
|
414
414
|
// Write cache
|
|
@@ -909,11 +909,11 @@ export class GmailClient {
|
|
|
909
909
|
// Labels CRUD
|
|
910
910
|
// =========================================================================
|
|
911
911
|
|
|
912
|
-
async listLabels(): Promise<{ parsed: ReturnType<
|
|
912
|
+
async listLabels(): Promise<{ parsed: ReturnType<GmailClient['parseLabels']>; raw: gmail_v1.Schema$Label[] } | AuthError | ApiError> {
|
|
913
913
|
// Check cache
|
|
914
914
|
const cached = await this.getCachedLabels()
|
|
915
915
|
if (cached) {
|
|
916
|
-
return { parsed:
|
|
916
|
+
return { parsed: this.parseLabels(cached), raw: cached }
|
|
917
917
|
}
|
|
918
918
|
|
|
919
919
|
const res = await gmailBoundary(this.account?.email ?? 'unknown', () =>
|
|
@@ -928,7 +928,7 @@ export class GmailClient {
|
|
|
928
928
|
// Write cache
|
|
929
929
|
await this.cacheLabelsData(rawLabels)
|
|
930
930
|
|
|
931
|
-
return { parsed:
|
|
931
|
+
return { parsed: this.parseLabels(rawLabels), raw: rawLabels }
|
|
932
932
|
}
|
|
933
933
|
|
|
934
934
|
async getLabel({ labelId }: { labelId: string }) {
|
|
@@ -1111,12 +1111,12 @@ export class GmailClient {
|
|
|
1111
1111
|
}
|
|
1112
1112
|
|
|
1113
1113
|
// =========================================================================
|
|
1114
|
-
//
|
|
1114
|
+
// Parsing: raw Google API responses → typed objects
|
|
1115
1115
|
// =========================================================================
|
|
1116
1116
|
|
|
1117
1117
|
/** Parse a raw gmail_v1.Schema$Thread (format: full) into ThreadData. */
|
|
1118
|
-
|
|
1119
|
-
const messages = (raw.messages ?? []).map((m) =>
|
|
1118
|
+
parseThread(raw: gmail_v1.Schema$Thread): ThreadData {
|
|
1119
|
+
const messages = (raw.messages ?? []).map((m) => this.parseMessage(m))
|
|
1120
1120
|
|
|
1121
1121
|
if (messages.length === 0) {
|
|
1122
1122
|
return {
|
|
@@ -1151,7 +1151,7 @@ export class GmailClient {
|
|
|
1151
1151
|
}
|
|
1152
1152
|
|
|
1153
1153
|
/** Parse a raw gmail_v1.Schema$Message into ParsedMessage. */
|
|
1154
|
-
|
|
1154
|
+
parseMessage(message: gmail_v1.Schema$Message): ParsedMessage {
|
|
1155
1155
|
const headers = message.payload?.headers ?? []
|
|
1156
1156
|
const labelIds = message.labelIds ?? []
|
|
1157
1157
|
|
|
@@ -1165,7 +1165,7 @@ export class GmailClient {
|
|
|
1165
1165
|
.map((h) => h.value ?? '')
|
|
1166
1166
|
.filter((v) => v.length > 0)
|
|
1167
1167
|
|
|
1168
|
-
const { body, mimeType, textBody } =
|
|
1168
|
+
const { body, mimeType, textBody } = this.extractBody(message.payload ?? {})
|
|
1169
1169
|
|
|
1170
1170
|
return {
|
|
1171
1171
|
id: message.id ?? '',
|
|
@@ -1192,15 +1192,16 @@ export class GmailClient {
|
|
|
1192
1192
|
body,
|
|
1193
1193
|
mimeType,
|
|
1194
1194
|
textBody,
|
|
1195
|
-
attachments:
|
|
1195
|
+
attachments: this.extractAttachmentMeta(message.payload?.parts ?? []),
|
|
1196
1196
|
}
|
|
1197
1197
|
}
|
|
1198
1198
|
|
|
1199
|
-
/** Parse raw gmail_v1.Schema$Thread (format: metadata) into ThreadListItem.
|
|
1200
|
-
|
|
1199
|
+
/** Parse raw gmail_v1.Schema$Thread (format: metadata) into ThreadListItem.
|
|
1200
|
+
* Shows the other party in conversations where user sent the latest message. */
|
|
1201
|
+
parseThreadListItem(raw: gmail_v1.Schema$Thread): ThreadListItem {
|
|
1201
1202
|
const messages = raw.messages ?? []
|
|
1202
|
-
const
|
|
1203
|
-
|
|
1203
|
+
const nonDraftMessages = messages.filter((m) => !m.labelIds?.includes('DRAFT'))
|
|
1204
|
+
const latest = nonDraftMessages[nonDraftMessages.length - 1] ?? messages[messages.length - 1]
|
|
1204
1205
|
|
|
1205
1206
|
const headers = latest?.payload?.headers ?? []
|
|
1206
1207
|
const allLabels = [...new Set(messages.flatMap((m) => m.labelIds ?? []))]
|
|
@@ -1208,21 +1209,45 @@ export class GmailClient {
|
|
|
1208
1209
|
const getHeader = (name: string) =>
|
|
1209
1210
|
headers.find((h) => h.name?.toLowerCase() === name.toLowerCase())?.value ?? null
|
|
1210
1211
|
|
|
1212
|
+
// Determine display sender — show other party when user sent the latest message
|
|
1213
|
+
let displayFrom = parseFrom(getHeader('from') ?? '')
|
|
1214
|
+
const latestIsFromUser = latest?.labelIds?.includes('SENT') ?? false
|
|
1215
|
+
|
|
1216
|
+
if (latestIsFromUser && this.account?.email) {
|
|
1217
|
+
const getMsgHeader = (msg: gmail_v1.Schema$Message, name: string) =>
|
|
1218
|
+
msg.payload?.headers?.find((h) => h.name?.toLowerCase() === name.toLowerCase())?.value ?? null
|
|
1219
|
+
|
|
1220
|
+
// Find most recent message NOT from the user
|
|
1221
|
+
const otherPartyMsg = nonDraftMessages.findLast((m) => !m.labelIds?.includes('SENT'))
|
|
1222
|
+
if (otherPartyMsg) {
|
|
1223
|
+
const fromHeader = getMsgHeader(otherPartyMsg, 'from')
|
|
1224
|
+
if (fromHeader) displayFrom = parseFrom(fromHeader)
|
|
1225
|
+
} else {
|
|
1226
|
+
// All messages from user — show first recipient
|
|
1227
|
+
const firstMsg = nonDraftMessages[0] ?? messages[0]
|
|
1228
|
+
if (firstMsg) {
|
|
1229
|
+
const toHeader = getMsgHeader(firstMsg, 'to') ?? ''
|
|
1230
|
+
const recipients = parseAddressList(toHeader)
|
|
1231
|
+
if (recipients[0]) displayFrom = recipients[0]
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1211
1236
|
return {
|
|
1212
1237
|
id: raw.id ?? '',
|
|
1213
1238
|
historyId: raw.historyId ?? null,
|
|
1214
1239
|
snippet: sanitizeSnippet(latest?.snippet ?? ''),
|
|
1215
1240
|
subject: (getHeader('subject') ?? '(no subject)').replace(/"/g, '').trim(),
|
|
1216
|
-
from:
|
|
1241
|
+
from: displayFrom,
|
|
1217
1242
|
date: getHeader('date') ?? '',
|
|
1218
1243
|
labelIds: allLabels,
|
|
1219
1244
|
unread: allLabels.includes('UNREAD'),
|
|
1220
|
-
messageCount:
|
|
1245
|
+
messageCount: nonDraftMessages.length,
|
|
1221
1246
|
}
|
|
1222
1247
|
}
|
|
1223
1248
|
|
|
1224
1249
|
/** Parse raw gmail_v1.Schema$Label[] from labels.list into our label objects. */
|
|
1225
|
-
|
|
1250
|
+
parseLabels(rawLabels: gmail_v1.Schema$Label[]) {
|
|
1226
1251
|
return rawLabels.map((label) => ({
|
|
1227
1252
|
id: label.id ?? '',
|
|
1228
1253
|
name: label.name ?? '',
|
|
@@ -1239,19 +1264,15 @@ export class GmailClient {
|
|
|
1239
1264
|
}
|
|
1240
1265
|
|
|
1241
1266
|
/** Parse raw gmail_v1.Schema$Label[] (with counts) into label count objects. */
|
|
1242
|
-
|
|
1243
|
-
rawLabels
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
label: labelName === 'draft' ? 'drafts' : labelName,
|
|
1252
|
-
count: Number(isTotalLabel ? detail.threadsTotal : detail.threadsUnread) || 0,
|
|
1253
|
-
}
|
|
1254
|
-
})
|
|
1267
|
+
parseLabelCounts(rawLabels: gmail_v1.Schema$Label[], archiveEstimate: number | null) {
|
|
1268
|
+
const result = rawLabels.map((detail) => {
|
|
1269
|
+
const labelName = (detail.name ?? detail.id ?? '').toLowerCase()
|
|
1270
|
+
const isTotalLabel = labelName === 'draft' || labelName === 'sent'
|
|
1271
|
+
return {
|
|
1272
|
+
label: labelName === 'draft' ? 'drafts' : labelName,
|
|
1273
|
+
count: Number(isTotalLabel ? detail.threadsTotal : detail.threadsUnread) || 0,
|
|
1274
|
+
}
|
|
1275
|
+
})
|
|
1255
1276
|
|
|
1256
1277
|
if (archiveEstimate !== null) {
|
|
1257
1278
|
result.push({ label: 'archive', count: archiveEstimate })
|
|
@@ -1261,10 +1282,10 @@ export class GmailClient {
|
|
|
1261
1282
|
}
|
|
1262
1283
|
|
|
1263
1284
|
// =========================================================================
|
|
1264
|
-
// Private
|
|
1285
|
+
// Private: body/attachment extraction
|
|
1265
1286
|
// =========================================================================
|
|
1266
1287
|
|
|
1267
|
-
private
|
|
1288
|
+
private extractBody(payload: gmail_v1.Schema$MessagePart): {
|
|
1268
1289
|
body: string
|
|
1269
1290
|
mimeType: string
|
|
1270
1291
|
textBody: string | null
|
|
@@ -1282,8 +1303,8 @@ export class GmailClient {
|
|
|
1282
1303
|
return { body: '', mimeType: 'text/plain', textBody: null }
|
|
1283
1304
|
}
|
|
1284
1305
|
|
|
1285
|
-
const htmlData =
|
|
1286
|
-
const textData =
|
|
1306
|
+
const htmlData = this.findBodyPart(payload.parts, 'text/html')
|
|
1307
|
+
const textData = this.findBodyPart(payload.parts, 'text/plain')
|
|
1287
1308
|
const textBody = textData ? decodeBase64Url(textData) : null
|
|
1288
1309
|
|
|
1289
1310
|
if (htmlData) {
|
|
@@ -1296,7 +1317,7 @@ export class GmailClient {
|
|
|
1296
1317
|
|
|
1297
1318
|
for (const part of payload.parts) {
|
|
1298
1319
|
if (part.parts) {
|
|
1299
|
-
const nested =
|
|
1320
|
+
const nested = this.extractBody(part)
|
|
1300
1321
|
if (nested.body) return nested
|
|
1301
1322
|
}
|
|
1302
1323
|
}
|
|
@@ -1304,20 +1325,20 @@ export class GmailClient {
|
|
|
1304
1325
|
return { body: '', mimeType: 'text/plain', textBody: null }
|
|
1305
1326
|
}
|
|
1306
1327
|
|
|
1307
|
-
private
|
|
1328
|
+
private findBodyPart(parts: gmail_v1.Schema$MessagePart[], mimeType: string): string | null {
|
|
1308
1329
|
for (const part of parts) {
|
|
1309
1330
|
if (part.mimeType === mimeType && part.body?.data) {
|
|
1310
1331
|
return part.body.data
|
|
1311
1332
|
}
|
|
1312
1333
|
if (part.parts) {
|
|
1313
|
-
const found =
|
|
1334
|
+
const found = this.findBodyPart(part.parts, mimeType)
|
|
1314
1335
|
if (found) return found
|
|
1315
1336
|
}
|
|
1316
1337
|
}
|
|
1317
1338
|
return null
|
|
1318
1339
|
}
|
|
1319
1340
|
|
|
1320
|
-
private
|
|
1341
|
+
private extractAttachmentMeta(parts: gmail_v1.Schema$MessagePart[]): AttachmentMeta[] {
|
|
1321
1342
|
const results: AttachmentMeta[] = []
|
|
1322
1343
|
|
|
1323
1344
|
for (const part of parts) {
|
|
@@ -1338,7 +1359,7 @@ export class GmailClient {
|
|
|
1338
1359
|
}
|
|
1339
1360
|
|
|
1340
1361
|
if (part.parts) {
|
|
1341
|
-
results.push(...
|
|
1362
|
+
results.push(...this.extractAttachmentMeta(part.parts))
|
|
1342
1363
|
}
|
|
1343
1364
|
}
|
|
1344
1365
|
|
|
@@ -1543,21 +1564,6 @@ export class GmailClient {
|
|
|
1543
1564
|
}
|
|
1544
1565
|
}
|
|
1545
1566
|
|
|
1546
|
-
// =========================================================================
|
|
1547
|
-
// Private: message parsing (delegates to static methods)
|
|
1548
|
-
// =========================================================================
|
|
1549
|
-
|
|
1550
|
-
private parseMessage(message: gmail_v1.Schema$Message): ParsedMessage {
|
|
1551
|
-
return GmailClient.parseRawMessage(message)
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
private parseThreadListItem(
|
|
1555
|
-
threadId: string,
|
|
1556
|
-
thread: gmail_v1.Schema$Thread,
|
|
1557
|
-
): ThreadListItem {
|
|
1558
|
-
return GmailClient.parseRawThreadListItem({ ...thread, id: threadId })
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
1567
|
// =========================================================================
|
|
1562
1568
|
// Private: MIME message construction
|
|
1563
1569
|
// =========================================================================
|