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.
@@ -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 = GmailClient.parseRawThreadListItem(rawThread as any)
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 = GmailClient.parseRawMessage(rawMessage as any)
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 = GmailClient.parseRawThreadListItem(rawThread as any)
62
+ const parsed = client.parseThreadListItem(rawThread as any)
58
63
  expect(parsed.snippet).toBe('A host sent you a message')
59
64
  })
@@ -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: GmailClient.parseRawThreadListItem(cached),
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 = GmailClient.parseRawThread(detail.data)
374
+ const parsed = this.parseThread(detail.data)
375
375
  await this.cacheThreadData(t.id, detail.data, parsed)
376
376
 
377
377
  return {
378
- parsed: GmailClient.parseRawThreadListItem(detail.data),
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: GmailClient.parseRawThread(cached), raw: cached }
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 = GmailClient.parseRawThread(res.data)
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<typeof GmailClient.parseRawLabels>; raw: gmail_v1.Schema$Label[] } | AuthError | ApiError> {
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: GmailClient.parseRawLabels(cached), raw: cached }
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: GmailClient.parseRawLabels(rawLabels), raw: rawLabels }
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
- // Static: parse raw Google API responses (used by cache readers)
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
- static parseRawThread(raw: gmail_v1.Schema$Thread): ThreadData {
1119
- const messages = (raw.messages ?? []).map((m) => GmailClient.parseRawMessage(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
- static parseRawMessage(message: gmail_v1.Schema$Message): ParsedMessage {
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 } = GmailClient.extractBodyStatic(message.payload ?? {})
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: GmailClient.extractAttachmentMetaStatic(message.payload?.parts ?? []),
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
- static parseRawThreadListItem(raw: gmail_v1.Schema$Thread): ThreadListItem {
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 latest =
1203
- messages.findLast((m) => !m.labelIds?.includes('DRAFT')) ?? messages[messages.length - 1]
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: parseFrom(getHeader('from') ?? ''),
1241
+ from: displayFrom,
1217
1242
  date: getHeader('date') ?? '',
1218
1243
  labelIds: allLabels,
1219
1244
  unread: allLabels.includes('UNREAD'),
1220
- messageCount: messages.filter((m) => !m.labelIds?.includes('DRAFT')).length,
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
- static parseRawLabels(rawLabels: gmail_v1.Schema$Label[]) {
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
- static parseRawLabelCounts(
1243
- rawLabels: gmail_v1.Schema$Label[],
1244
- archiveEstimate: number | null,
1245
- ) {
1246
- const result = rawLabels
1247
- .map((detail) => {
1248
- const labelName = (detail.name ?? detail.id ?? '').toLowerCase()
1249
- const isTotalLabel = labelName === 'draft' || labelName === 'sent'
1250
- return {
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 static: body/attachment extraction (for static parse methods)
1285
+ // Private: body/attachment extraction
1265
1286
  // =========================================================================
1266
1287
 
1267
- private static extractBodyStatic(payload: gmail_v1.Schema$MessagePart): {
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 = GmailClient.findBodyPartStatic(payload.parts, 'text/html')
1286
- const textData = GmailClient.findBodyPartStatic(payload.parts, 'text/plain')
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 = GmailClient.extractBodyStatic(part)
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 static findBodyPartStatic(parts: gmail_v1.Schema$MessagePart[], mimeType: string): string | null {
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 = GmailClient.findBodyPartStatic(part.parts, mimeType)
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 static extractAttachmentMetaStatic(parts: gmail_v1.Schema$MessagePart[]): AttachmentMeta[] {
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(...GmailClient.extractAttachmentMetaStatic(part.parts))
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
  // =========================================================================