zele 0.3.17 → 0.3.21

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.
Files changed (64) hide show
  1. package/README.md +81 -12
  2. package/dist/api-utils.d.ts +10 -0
  3. package/dist/api-utils.js +14 -0
  4. package/dist/api-utils.js.map +1 -1
  5. package/dist/cli-types.d.ts +4 -0
  6. package/dist/cli-types.js +6 -0
  7. package/dist/cli-types.js.map +1 -0
  8. package/dist/cli.js +1 -5
  9. package/dist/cli.js.map +1 -1
  10. package/dist/commands/attachment.d.ts +2 -2
  11. package/dist/commands/attachment.js.map +1 -1
  12. package/dist/commands/auth-cmd.d.ts +2 -2
  13. package/dist/commands/auth-cmd.js +58 -52
  14. package/dist/commands/auth-cmd.js.map +1 -1
  15. package/dist/commands/calendar.d.ts +2 -2
  16. package/dist/commands/calendar.js +13 -14
  17. package/dist/commands/calendar.js.map +1 -1
  18. package/dist/commands/draft.d.ts +2 -2
  19. package/dist/commands/draft.js +62 -15
  20. package/dist/commands/draft.js.map +1 -1
  21. package/dist/commands/filter.d.ts +2 -2
  22. package/dist/commands/filter.js.map +1 -1
  23. package/dist/commands/label.d.ts +2 -2
  24. package/dist/commands/label.js +5 -6
  25. package/dist/commands/label.js.map +1 -1
  26. package/dist/commands/mail-actions.d.ts +2 -2
  27. package/dist/commands/mail-actions.js +290 -1
  28. package/dist/commands/mail-actions.js.map +1 -1
  29. package/dist/commands/mail.d.ts +2 -2
  30. package/dist/commands/mail.js +50 -10
  31. package/dist/commands/mail.js.map +1 -1
  32. package/dist/commands/profile.d.ts +2 -2
  33. package/dist/commands/profile.js.map +1 -1
  34. package/dist/commands/watch.d.ts +2 -2
  35. package/dist/commands/watch.js +2 -2
  36. package/dist/commands/watch.js.map +1 -1
  37. package/dist/gmail-client.d.ts +59 -3
  38. package/dist/gmail-client.js +119 -5
  39. package/dist/gmail-client.js.map +1 -1
  40. package/dist/imap-smtp-client.d.ts +75 -4
  41. package/dist/imap-smtp-client.js +131 -7
  42. package/dist/imap-smtp-client.js.map +1 -1
  43. package/dist/unsubscribe.d.ts +76 -0
  44. package/dist/unsubscribe.js +224 -0
  45. package/dist/unsubscribe.js.map +1 -0
  46. package/package.json +3 -2
  47. package/skills/zele/SKILL.md +32 -124
  48. package/src/api-utils.ts +14 -0
  49. package/src/cli-types.ts +8 -0
  50. package/src/cli.ts +2 -7
  51. package/src/commands/attachment.ts +2 -2
  52. package/src/commands/auth-cmd.ts +66 -56
  53. package/src/commands/calendar.ts +15 -16
  54. package/src/commands/draft.ts +71 -17
  55. package/src/commands/filter.ts +2 -2
  56. package/src/commands/label.ts +7 -8
  57. package/src/commands/mail-actions.ts +315 -4
  58. package/src/commands/mail.ts +54 -12
  59. package/src/commands/profile.ts +2 -2
  60. package/src/commands/watch.ts +4 -4
  61. package/src/gmail-client.ts +193 -6
  62. package/src/imap-smtp-client.ts +186 -7
  63. package/src/unsubscribe.test.ts +487 -0
  64. package/src/unsubscribe.ts +255 -0
@@ -58,6 +58,7 @@ export interface ParsedMessage {
58
58
  inReplyTo?: string
59
59
  references?: string
60
60
  listUnsubscribe?: string
61
+ listUnsubscribePost?: string
61
62
  body: string // decoded body (html preferred for rich rendering)
62
63
  mimeType: string // 'text/plain' or 'text/html'
63
64
  textBody: string | null // decoded text/plain body when available (for reply parsing)
@@ -102,6 +103,7 @@ export interface ThreadListItem {
102
103
  inReplyTo: string | null
103
104
  hasAttachments: boolean
104
105
  listUnsubscribe: string | null
106
+ listUnsubscribePost: string | null
105
107
  }
106
108
 
107
109
  export interface ThreadListResult {
@@ -715,7 +717,7 @@ export class GmailClient {
715
717
  return res.data
716
718
  }
717
719
 
718
- async getDraft({ draftId }: { draftId: string }): Promise<NotFoundError | AuthError | ApiError | { id: string; message: ParsedMessage; to: string[]; cc: string[]; bcc: string[] }> {
720
+ async getDraft({ draftId }: { draftId: string }): Promise<NotFoundError | AuthError | ApiError | { id: string; message: ParsedMessage; to: Sender[]; cc: Sender[]; bcc: Sender[] }> {
719
721
  const res = await gmailBoundary(this.account?.email ?? 'unknown', () =>
720
722
  withRetry(() =>
721
723
  this.gmail.users.drafts.get({
@@ -730,14 +732,13 @@ export class GmailClient {
730
732
  if (!res.data || !res.data.message) return new NotFoundError({ resource: `draft ${draftId}` })
731
733
 
732
734
  const message = this.parseMessage(res.data.message)
733
- const headers = res.data.message.payload?.headers ?? []
734
735
 
735
736
  return {
736
737
  id: res.data.id ?? draftId,
737
738
  message,
738
- to: this.getHeaderValues(headers, 'to'),
739
- cc: this.getHeaderValues(headers, 'cc'),
740
- bcc: this.getHeaderValues(headers, 'bcc'),
739
+ to: message.to,
740
+ cc: message.cc ?? [],
741
+ bcc: message.bcc,
741
742
  }
742
743
  }
743
744
 
@@ -816,6 +817,187 @@ export class GmailClient {
816
817
  )
817
818
  }
818
819
 
820
+ /**
821
+ * Update an existing draft. Gmail replaces the entire message content,
822
+ * so the caller must provide all fields (merge with existing draft before calling).
823
+ */
824
+ async updateDraft({
825
+ draftId,
826
+ to,
827
+ subject,
828
+ body,
829
+ cc,
830
+ bcc,
831
+ threadId,
832
+ fromEmail,
833
+ attachments,
834
+ }: {
835
+ draftId: string
836
+ to: Array<{ name?: string; email: string }>
837
+ subject: string
838
+ body: string
839
+ cc?: Array<{ name?: string; email: string }>
840
+ bcc?: Array<{ name?: string; email: string }>
841
+ threadId?: string
842
+ fromEmail?: string
843
+ attachments?: Array<{ filename: string; mimeType: string; content: Buffer }>
844
+ }) {
845
+ const raw = this.buildMimeMessage({ to, subject, body, cc, bcc, attachments, fromEmail })
846
+
847
+ const res = await gmailBoundary(this.account?.email ?? 'unknown', () =>
848
+ withRetry(() =>
849
+ this.gmail.users.drafts.update({
850
+ userId: 'me',
851
+ id: draftId,
852
+ requestBody: {
853
+ message: { raw, threadId },
854
+ },
855
+ }),
856
+ ),
857
+ )
858
+ if (res instanceof Error) return res
859
+
860
+ return res.data
861
+ }
862
+
863
+ /**
864
+ * Create a draft reply to a thread. Reuses the same reply-to resolution,
865
+ * reply-all CC computation, and In-Reply-To/References header logic as
866
+ * replyToThread(), but saves as a draft instead of sending.
867
+ */
868
+ async createDraftReply({
869
+ threadId,
870
+ body,
871
+ replyAll = false,
872
+ cc,
873
+ fromEmail,
874
+ }: {
875
+ threadId: string
876
+ body: string
877
+ replyAll?: boolean
878
+ cc?: Array<{ email: string }>
879
+ fromEmail?: string
880
+ }): Promise<EmptyThreadError | AuthError | ApiError | gmail_v1.Schema$Draft> {
881
+ const { parsed: thread } = await this.getThread({ threadId })
882
+ if (thread.messages.length === 0) {
883
+ return new EmptyThreadError({ threadId })
884
+ }
885
+
886
+ const lastMsg = thread.messages[thread.messages.length - 1]!
887
+
888
+ const replyTo = lastMsg.replyTo ?? lastMsg.from.email
889
+ const to = [{ email: replyTo }]
890
+
891
+ let resolvedCc: Array<{ email: string }> | undefined
892
+ if (replyAll) {
893
+ const profile = await this.getProfile()
894
+ if (profile instanceof Error) return profile
895
+ const myEmail = profile.emailAddress.toLowerCase()
896
+
897
+ const allRecipients = [
898
+ ...lastMsg.to.map((r) => r.email),
899
+ ...(lastMsg.cc?.map((r) => r.email) ?? []),
900
+ ]
901
+ .filter((e) => e.toLowerCase() !== myEmail)
902
+ .filter((e) => e.toLowerCase() !== replyTo.toLowerCase())
903
+
904
+ if (allRecipients.length > 0) {
905
+ resolvedCc = allRecipients.map((e) => ({ email: e }))
906
+ }
907
+ }
908
+
909
+ if (cc) {
910
+ resolvedCc = [...(resolvedCc ?? []), ...cc]
911
+ }
912
+
913
+ const refs = [lastMsg.references, lastMsg.messageId].filter(Boolean).join(' ')
914
+
915
+ const raw = this.buildMimeMessage({
916
+ to,
917
+ subject: lastMsg.subject.startsWith('Re:') ? lastMsg.subject : `Re: ${lastMsg.subject}`,
918
+ body,
919
+ cc: resolvedCc,
920
+ inReplyTo: lastMsg.messageId,
921
+ references: refs || undefined,
922
+ fromEmail,
923
+ })
924
+
925
+ const res = await gmailBoundary(this.account?.email ?? 'unknown', () =>
926
+ withRetry(() =>
927
+ this.gmail.users.drafts.create({
928
+ userId: 'me',
929
+ requestBody: {
930
+ message: { raw, threadId },
931
+ },
932
+ }),
933
+ ),
934
+ )
935
+ if (res instanceof Error) return res
936
+
937
+ return res.data
938
+ }
939
+
940
+ /**
941
+ * Create a draft forwarding a thread. Reuses the same forwarded-message body
942
+ * building logic as forwardThread(), but saves as a draft instead of sending.
943
+ */
944
+ async createDraftForward({
945
+ threadId,
946
+ to,
947
+ body,
948
+ fromEmail,
949
+ }: {
950
+ threadId: string
951
+ to: Array<{ email: string }>
952
+ body?: string
953
+ fromEmail?: string
954
+ }): Promise<EmptyThreadError | AuthError | ApiError | gmail_v1.Schema$Draft> {
955
+ const { parsed: thread } = await this.getThread({ threadId })
956
+ if (thread.messages.length === 0) {
957
+ return new EmptyThreadError({ threadId })
958
+ }
959
+
960
+ const lastMsg = thread.messages[thread.messages.length - 1]!
961
+ const renderedBody = renderEmailBody(lastMsg.body, lastMsg.mimeType)
962
+
963
+ const fromStr = lastMsg.from.name && lastMsg.from.name !== lastMsg.from.email
964
+ ? `${lastMsg.from.name} <${lastMsg.from.email}>`
965
+ : lastMsg.from.email
966
+
967
+ const fullBody = [
968
+ body ?? '',
969
+ '',
970
+ '---------- Forwarded message ----------',
971
+ `From: ${fromStr}`,
972
+ `Date: ${lastMsg.date}`,
973
+ `Subject: ${lastMsg.subject}`,
974
+ `To: ${lastMsg.to.map((t) => t.email).join(', ')}`,
975
+ '',
976
+ renderedBody,
977
+ ].join('\n')
978
+
979
+ const raw = this.buildMimeMessage({
980
+ to,
981
+ subject: `Fwd: ${lastMsg.subject}`,
982
+ body: fullBody,
983
+ fromEmail,
984
+ })
985
+
986
+ const res = await gmailBoundary(this.account?.email ?? 'unknown', () =>
987
+ withRetry(() =>
988
+ this.gmail.users.drafts.create({
989
+ userId: 'me',
990
+ requestBody: {
991
+ message: { raw, threadId },
992
+ },
993
+ }),
994
+ ),
995
+ )
996
+ if (res instanceof Error) return res
997
+
998
+ return res.data
999
+ }
1000
+
819
1001
  // =========================================================================
820
1002
  // Label mutations (read/unread, star, archive, trash, labels)
821
1003
  // =========================================================================
@@ -1332,6 +1514,7 @@ export class GmailClient {
1332
1514
  inReplyTo: getHeader('in-reply-to') ?? undefined,
1333
1515
  references: getHeader('references') ?? undefined,
1334
1516
  listUnsubscribe: getHeader('list-unsubscribe') ?? undefined,
1517
+ listUnsubscribePost: getHeader('list-unsubscribe-post') ?? undefined,
1335
1518
  body,
1336
1519
  mimeType,
1337
1520
  textBody,
@@ -1397,8 +1580,11 @@ export class GmailClient {
1397
1580
  // Check if any message has attachments (non-inline)
1398
1581
  const hasAttachments = messages.some((m) => this.hasNonInlineAttachments(m.payload))
1399
1582
 
1400
- // List-Unsubscribe from latest message
1583
+ // List-Unsubscribe / List-Unsubscribe-Post from latest message (RFC 2369 + RFC 8058).
1584
+ // Used by `mail list` to surface can_unsubscribe / one_click booleans and
1585
+ // by `mail unsubscribe` to plan the unsubscribe flow without re-fetching.
1401
1586
  const listUnsubscribe = getHeader('list-unsubscribe') ?? null
1587
+ const listUnsubscribePost = getHeader('list-unsubscribe-post') ?? null
1402
1588
 
1403
1589
  return {
1404
1590
  id: raw.id ?? '',
@@ -1418,6 +1604,7 @@ export class GmailClient {
1418
1604
  inReplyTo,
1419
1605
  hasAttachments,
1420
1606
  listUnsubscribe,
1607
+ listUnsubscribePost,
1421
1608
  }
1422
1609
  }
1423
1610
 
@@ -382,7 +382,11 @@ export class ImapSmtpClient {
382
382
  messageCount: 1,
383
383
  inReplyTo: env.inReplyTo ?? null,
384
384
  hasAttachments: this.hasAttachments(msg),
385
+ // IMAP list view uses envelope-only fetch, so raw headers aren't
386
+ // available. List-Unsubscribe stays null in list mode; it's
387
+ // resolved during getThread() where `source: true` is fetched.
385
388
  listUnsubscribe: null,
389
+ listUnsubscribePost: null,
386
390
  })
387
391
  }
388
392
  }
@@ -1086,9 +1090,9 @@ export class ImapSmtpClient {
1086
1090
  return {
1087
1091
  id: draftId,
1088
1092
  message: msg,
1089
- to: msg.to.map((t) => t.email),
1090
- cc: (msg.cc ?? []).map((c) => c.email),
1091
- bcc: msg.bcc.map((b) => b.email),
1093
+ to: msg.to,
1094
+ cc: msg.cc ?? [],
1095
+ bcc: msg.bcc,
1092
1096
  }
1093
1097
  }
1094
1098
 
@@ -1097,11 +1101,11 @@ export class ImapSmtpClient {
1097
1101
  const draft = await this.getDraft({ draftId })
1098
1102
 
1099
1103
  const result = await this.sendMessage({
1100
- to: draft.to.map((email) => ({ email })),
1104
+ to: draft.to,
1101
1105
  subject: draft.message.subject,
1102
1106
  body: draft.message.body,
1103
- cc: draft.cc.length > 0 ? draft.cc.map((email) => ({ email })) : undefined,
1104
- bcc: draft.bcc.length > 0 ? draft.bcc.map((email) => ({ email })) : undefined,
1107
+ cc: draft.cc.length > 0 ? draft.cc : undefined,
1108
+ bcc: draft.bcc.length > 0 ? draft.bcc : undefined,
1105
1109
  })
1106
1110
  if (result instanceof Error) return result
1107
1111
 
@@ -1124,6 +1128,168 @@ export class ImapSmtpClient {
1124
1128
  })
1125
1129
  }
1126
1130
 
1131
+ /**
1132
+ * Update an existing draft. IMAP has no native update — we delete the old
1133
+ * draft and APPEND a new message to the Drafts folder.
1134
+ */
1135
+ async updateDraft({
1136
+ draftId,
1137
+ to,
1138
+ subject,
1139
+ body,
1140
+ cc,
1141
+ bcc,
1142
+ fromEmail,
1143
+ }: {
1144
+ draftId: string
1145
+ to: Array<{ name?: string; email: string }>
1146
+ subject: string
1147
+ body: string
1148
+ cc?: Array<{ name?: string; email: string }>
1149
+ bcc?: Array<{ name?: string; email: string }>
1150
+ threadId?: string
1151
+ fromEmail?: string
1152
+ attachments?: Array<{ filename: string; mimeType: string; content: Buffer }>
1153
+ }) {
1154
+ // Delete old draft first — check for errors before creating replacement
1155
+ const deleted = await this.deleteDraft({ draftId })
1156
+ if (deleted instanceof Error) return deleted
1157
+
1158
+ // Create new draft with updated content
1159
+ return this.createDraft({ to, subject, body, cc, bcc, fromEmail })
1160
+ }
1161
+
1162
+ /**
1163
+ * Create a draft reply to a thread. Resolves reply-to, reply-all CCs,
1164
+ * and sets In-Reply-To/References headers, then appends to Drafts.
1165
+ */
1166
+ async createDraftReply({
1167
+ threadId,
1168
+ body,
1169
+ replyAll = false,
1170
+ cc,
1171
+ fromEmail,
1172
+ }: {
1173
+ threadId: string
1174
+ body: string
1175
+ replyAll?: boolean
1176
+ cc?: Array<{ email: string }>
1177
+ fromEmail?: string
1178
+ }): Promise<EmptyThreadError | AuthError | ApiError | { id: string; message: { id: string }; threadId: string }> {
1179
+ const thread = await this.getThread({ threadId })
1180
+ if (thread.parsed.messages.length === 0) {
1181
+ return new EmptyThreadError({ threadId })
1182
+ }
1183
+
1184
+ const lastMsg = thread.parsed.messages[thread.parsed.messages.length - 1]!
1185
+ const replyTo = lastMsg.replyTo ?? lastMsg.from.email
1186
+ const to = [{ email: replyTo }]
1187
+
1188
+ let resolvedCc: Array<{ email: string }> | undefined
1189
+ if (replyAll) {
1190
+ const myEmail = this.account.email.toLowerCase()
1191
+ const allRecipients = [
1192
+ ...lastMsg.to.map((r) => r.email),
1193
+ ...(lastMsg.cc?.map((r) => r.email) ?? []),
1194
+ ]
1195
+ .filter((e) => e.toLowerCase() !== myEmail)
1196
+ .filter((e) => e.toLowerCase() !== replyTo.toLowerCase())
1197
+
1198
+ if (allRecipients.length > 0) {
1199
+ resolvedCc = allRecipients.map((e) => ({ email: e }))
1200
+ }
1201
+ }
1202
+
1203
+ if (cc) {
1204
+ resolvedCc = [...(resolvedCc ?? []), ...cc]
1205
+ }
1206
+
1207
+ const refs = [lastMsg.references, lastMsg.messageId].filter(Boolean).join(' ')
1208
+ const subject = lastMsg.subject.startsWith('Re:') ? lastMsg.subject : `Re: ${lastMsg.subject}`
1209
+
1210
+ // Build MIME with reply headers
1211
+ const headers = [
1212
+ `From: ${fromEmail ?? this.account.email}`,
1213
+ `To: ${to.map((r) => r.email).join(', ')}`,
1214
+ `Subject: ${subject}`,
1215
+ `Date: ${new Date().toUTCString()}`,
1216
+ `MIME-Version: 1.0`,
1217
+ `Content-Type: text/plain; charset=utf-8`,
1218
+ ]
1219
+ if (resolvedCc && resolvedCc.length > 0) {
1220
+ headers.push(`Cc: ${resolvedCc.map((r) => r.email).join(', ')}`)
1221
+ }
1222
+ if (lastMsg.messageId) {
1223
+ headers.push(`In-Reply-To: ${lastMsg.messageId}`)
1224
+ }
1225
+ if (refs) {
1226
+ headers.push(`References: ${refs}`)
1227
+ }
1228
+
1229
+ const raw = headers.join('\r\n') + '\r\n\r\n' + body
1230
+ const rawBuffer = Buffer.from(raw)
1231
+
1232
+ const result = await this.withImap(async (client) => {
1233
+ const draftsPath = await this.resolveMailboxPath(client, 'drafts')
1234
+ const appendResult = await client.append(draftsPath, rawBuffer, ['\\Draft', '\\Seen'])
1235
+ const uid = appendResult && typeof appendResult === 'object' && 'uid' in appendResult ? appendResult.uid : undefined
1236
+ return {
1237
+ id: uid ? makeThreadId(draftsPath, uid) : 'unknown',
1238
+ message: { id: 'unknown' },
1239
+ threadId: uid ? makeThreadId(draftsPath, uid) : 'unknown',
1240
+ }
1241
+ })
1242
+ if (result instanceof Error) return result
1243
+ return result
1244
+ }
1245
+
1246
+ /**
1247
+ * Create a draft forwarding a thread. Builds the forwarded-message body
1248
+ * and appends to Drafts folder.
1249
+ */
1250
+ async createDraftForward({
1251
+ threadId,
1252
+ to,
1253
+ body,
1254
+ fromEmail,
1255
+ }: {
1256
+ threadId: string
1257
+ to: Array<{ email: string }>
1258
+ body?: string
1259
+ fromEmail?: string
1260
+ }): Promise<EmptyThreadError | AuthError | ApiError | { id: string; message: { id: string }; threadId: string }> {
1261
+ const thread = await this.getThread({ threadId })
1262
+ if (thread.parsed.messages.length === 0) {
1263
+ return new EmptyThreadError({ threadId })
1264
+ }
1265
+
1266
+ const lastMsg = thread.parsed.messages[thread.parsed.messages.length - 1]!
1267
+ const renderedBody = renderEmailBody(lastMsg.body, lastMsg.mimeType)
1268
+
1269
+ const fromStr = lastMsg.from.name && lastMsg.from.name !== lastMsg.from.email
1270
+ ? `${lastMsg.from.name} <${lastMsg.from.email}>`
1271
+ : lastMsg.from.email
1272
+
1273
+ const fullBody = [
1274
+ body ?? '',
1275
+ '',
1276
+ '---------- Forwarded message ----------',
1277
+ `From: ${fromStr}`,
1278
+ `Date: ${lastMsg.date}`,
1279
+ `Subject: ${lastMsg.subject}`,
1280
+ `To: ${lastMsg.to.map((t) => t.email).join(', ')}`,
1281
+ '',
1282
+ renderedBody,
1283
+ ].join('\n')
1284
+
1285
+ return this.createDraft({
1286
+ to,
1287
+ subject: `Fwd: ${lastMsg.subject}`,
1288
+ body: fullBody,
1289
+ fromEmail,
1290
+ })
1291
+ }
1292
+
1127
1293
  // =========================================================================
1128
1294
  // Folder listing (IMAP equivalent of labels)
1129
1295
  // =========================================================================
@@ -1207,6 +1373,8 @@ export class ImapSmtpClient {
1207
1373
  let body = ''
1208
1374
  let mimeType = 'text/plain'
1209
1375
  let textBody: string | null = null
1376
+ let listUnsubscribe: string | undefined
1377
+ let listUnsubscribePost: string | undefined
1210
1378
 
1211
1379
  if (msg.source) {
1212
1380
  const source = msg.source.toString('utf-8')
@@ -1214,6 +1382,16 @@ export class ImapSmtpClient {
1214
1382
  body = bodyResult.body
1215
1383
  mimeType = bodyResult.mimeType
1216
1384
  textBody = bodyResult.textBody
1385
+
1386
+ // Extract List-Unsubscribe / List-Unsubscribe-Post from raw MIME headers.
1387
+ // envelope-based fetches don't surface these, but getThread always fetches
1388
+ // source so it's available by the time we parse a full message.
1389
+ const headerEnd = source.indexOf('\r\n\r\n')
1390
+ const altEnd = source.indexOf('\n\n')
1391
+ const headerSplit = headerEnd !== -1 ? headerEnd : altEnd
1392
+ const headerText = headerSplit === -1 ? source : source.slice(0, headerSplit)
1393
+ listUnsubscribe = this.getHeader(headerText, 'list-unsubscribe')
1394
+ listUnsubscribePost = this.getHeader(headerText, 'list-unsubscribe-post')
1217
1395
  }
1218
1396
 
1219
1397
  // Extract attachments from bodyStructure
@@ -1240,7 +1418,8 @@ export class ImapSmtpClient {
1240
1418
  messageId: env.messageId ?? '',
1241
1419
  inReplyTo: env.inReplyTo,
1242
1420
  references: undefined,
1243
- listUnsubscribe: undefined,
1421
+ listUnsubscribe,
1422
+ listUnsubscribePost,
1244
1423
  body,
1245
1424
  mimeType,
1246
1425
  textBody,