@remit/storage-service 0.0.11 → 0.0.13

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.
@@ -1,761 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { PassThrough, type Readable } from "node:stream";
3
- import { pipeline } from "node:stream/promises";
4
- import { createGunzip, createGzip, gunzipSync, gzipSync } from "node:zlib";
5
- import {
6
- DeleteObjectCommand,
7
- DeleteObjectsCommand,
8
- GetObjectCommand,
9
- HeadObjectCommand,
10
- ListObjectsV2Command,
11
- PutObjectCommand,
12
- type S3Client,
13
- } from "@aws-sdk/client-s3";
14
- import { Upload } from "@aws-sdk/lib-storage";
15
- import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
16
- import { ContentEncoding, StorageType } from "@remit/domain-enums";
17
- import type {
18
- ExtractedTextListItem,
19
- OutboxAttachmentListItem,
20
- ParsedBody,
21
- StorageReference,
22
- StorageService,
23
- } from "../storage.js";
24
- import {
25
- buildBodyPartKey,
26
- buildDeduplicatedKey,
27
- buildExportArchiveKey,
28
- buildExtractedPrefix,
29
- buildExtractedSkippedKey,
30
- buildExtractedTextKey,
31
- buildMessageBodyKey,
32
- buildOutboxAttachmentKey,
33
- buildOutboxAttachmentPrefix,
34
- buildParsedBodyKey,
35
- computeChecksum,
36
- isStorageNotFoundError,
37
- } from "../storage.js";
38
- import { parseStorageUri } from "../uri.js";
39
-
40
- interface StoreParams {
41
- key: string;
42
- content: Buffer;
43
- contentType?: string;
44
- compress?: boolean;
45
- }
46
-
47
- export const createS3StorageService = (
48
- client: S3Client,
49
- bucketName: string,
50
- ): StorageService => {
51
- const storeInternal = async (
52
- params: StoreParams,
53
- ): Promise<StorageReference> => {
54
- const { key, content, contentType, compress = true } = params;
55
- const contentChecksumSha256 = computeChecksum(content);
56
- const contentEncoding = compress
57
- ? ContentEncoding.Gzip
58
- : ContentEncoding.None;
59
- const body = compress ? gzipSync(content) : content;
60
-
61
- await client.send(
62
- new PutObjectCommand({
63
- Bucket: bucketName,
64
- Key: key,
65
- Body: body,
66
- ContentType: contentType,
67
- ContentEncoding: compress ? "gzip" : undefined,
68
- }),
69
- );
70
-
71
- return {
72
- uri: `s3://${bucketName}/${key}`,
73
- storageType: StorageType.S3,
74
- storageLocation: bucketName,
75
- storageKey: key,
76
- sizeBytes: body.length,
77
- checksumSha256: contentChecksumSha256,
78
- contentEncoding,
79
- };
80
- };
81
-
82
- const storeMessageBody: StorageService["storeMessageBody"] = (params) => {
83
- const { accountConfigId, accountId, messageId, content } = params;
84
- return storeInternal({
85
- key: buildMessageBodyKey(accountConfigId, accountId, messageId),
86
- content,
87
- contentType: "message/rfc822",
88
- });
89
- };
90
-
91
- const storeMessageBodyStream: StorageService["storeMessageBodyStream"] =
92
- async (params) => {
93
- const { accountConfigId, accountId, messageId, content } = params;
94
- const key = buildMessageBodyKey(accountConfigId, accountId, messageId);
95
-
96
- // Hash the logical (pre-gzip) bytes as they flow, gzip them, and feed
97
- // the gzipped stream to a multipart Upload — the whole body is never
98
- // held in memory. `Upload` buffers only one part at a time.
99
- const hash = createHash("sha256");
100
- const hashTap = new PassThrough();
101
- hashTap.on("data", (chunk: Buffer) => hash.update(chunk));
102
-
103
- const gzip = createGzip();
104
- const uploadBody = new PassThrough();
105
-
106
- let storedBytes = 0;
107
- uploadBody.on("data", (chunk: Buffer) => {
108
- storedBytes += chunk.length;
109
- });
110
-
111
- const upload = new Upload({
112
- client,
113
- params: {
114
- Bucket: bucketName,
115
- Key: key,
116
- Body: uploadBody,
117
- ContentType: "message/rfc822",
118
- ContentEncoding: "gzip",
119
- },
120
- });
121
-
122
- const pumped = pipeline(content, hashTap, gzip, uploadBody);
123
- await Promise.all([upload.done(), pumped]).catch(async (error) => {
124
- // Abort the multipart upload so a failed pump doesn't leak parts.
125
- await upload.abort().catch(() => {});
126
- throw error;
127
- });
128
-
129
- return {
130
- uri: `s3://${bucketName}/${key}`,
131
- storageType: StorageType.S3,
132
- storageLocation: bucketName,
133
- storageKey: key,
134
- sizeBytes: storedBytes,
135
- checksumSha256: hash.digest("hex"),
136
- contentEncoding: ContentEncoding.Gzip,
137
- };
138
- };
139
-
140
- const storeBodyPart: StorageService["storeBodyPart"] = (params) => {
141
- const {
142
- accountConfigId,
143
- accountId,
144
- messageId,
145
- partPath,
146
- content,
147
- contentType,
148
- } = params;
149
- return storeInternal({
150
- key: buildBodyPartKey(accountConfigId, accountId, messageId, partPath),
151
- content,
152
- contentType,
153
- });
154
- };
155
-
156
- const bodyPartExists: StorageService["bodyPartExists"] = (
157
- accountConfigId,
158
- accountId,
159
- messageId,
160
- partPath,
161
- ) => {
162
- const key = buildBodyPartKey(
163
- accountConfigId,
164
- accountId,
165
- messageId,
166
- partPath,
167
- );
168
- return client
169
- .send(new HeadObjectCommand({ Bucket: bucketName, Key: key }))
170
- .then(() => true)
171
- .catch(() => false);
172
- };
173
-
174
- const retrieveBodyPart: StorageService["retrieveBodyPart"] = async (
175
- accountConfigId,
176
- accountId,
177
- messageId,
178
- partPath,
179
- ) => {
180
- const key = buildBodyPartKey(
181
- accountConfigId,
182
- accountId,
183
- messageId,
184
- partPath,
185
- );
186
-
187
- const response = await client
188
- .send(new GetObjectCommand({ Bucket: bucketName, Key: key }))
189
- .catch((error: unknown) => {
190
- if (isStorageNotFoundError(error)) return null;
191
- throw error;
192
- });
193
-
194
- if (!response) return null;
195
- if (!response.Body) {
196
- throw new Error(`Empty response body for body part: ${key}`);
197
- }
198
-
199
- const body = await response.Body.transformToByteArray();
200
- const buffer = Buffer.from(body);
201
- return response.ContentEncoding === "gzip" ? gunzipSync(buffer) : buffer;
202
- };
203
-
204
- const storeExtractedText: StorageService["storeExtractedText"] = (params) => {
205
- const { accountConfigId, accountId, messageId, partPath, text } = params;
206
- return storeInternal({
207
- key: buildExtractedTextKey(
208
- accountConfigId,
209
- accountId,
210
- messageId,
211
- partPath,
212
- ),
213
- content: Buffer.from(text, "utf8"),
214
- contentType: "text/plain; charset=utf-8",
215
- });
216
- };
217
-
218
- const storeExtractedSkipped: StorageService["storeExtractedSkipped"] = (
219
- params,
220
- ) => {
221
- const { accountConfigId, accountId, messageId, partPath, marker } = params;
222
- return storeInternal({
223
- key: buildExtractedSkippedKey(
224
- accountConfigId,
225
- accountId,
226
- messageId,
227
- partPath,
228
- ),
229
- content: Buffer.from(JSON.stringify(marker), "utf8"),
230
- contentType: "application/json",
231
- compress: false,
232
- });
233
- };
234
-
235
- const extractedResultExists: StorageService["extractedResultExists"] = async (
236
- accountConfigId,
237
- accountId,
238
- messageId,
239
- partPath,
240
- ) => {
241
- const textKey = buildExtractedTextKey(
242
- accountConfigId,
243
- accountId,
244
- messageId,
245
- partPath,
246
- );
247
- const skippedKey = buildExtractedSkippedKey(
248
- accountConfigId,
249
- accountId,
250
- messageId,
251
- partPath,
252
- );
253
-
254
- const headExists = (key: string): Promise<boolean> =>
255
- client
256
- .send(new HeadObjectCommand({ Bucket: bucketName, Key: key }))
257
- .then(() => true)
258
- .catch(() => false);
259
-
260
- const [textExists, skippedExists] = await Promise.all([
261
- headExists(textKey),
262
- headExists(skippedKey),
263
- ]);
264
- return textExists || skippedExists;
265
- };
266
-
267
- const retrieveExtractedText: StorageService["retrieveExtractedText"] = async (
268
- accountConfigId,
269
- accountId,
270
- messageId,
271
- partPath,
272
- ) => {
273
- const key = buildExtractedTextKey(
274
- accountConfigId,
275
- accountId,
276
- messageId,
277
- partPath,
278
- );
279
-
280
- const response = await client
281
- .send(new GetObjectCommand({ Bucket: bucketName, Key: key }))
282
- .catch((error: unknown) => {
283
- if (isStorageNotFoundError(error)) return null;
284
- throw error;
285
- });
286
-
287
- if (!response) return null;
288
- if (!response.Body) {
289
- throw new Error(`Empty response body for extracted text: ${key}`);
290
- }
291
-
292
- const body = await response.Body.transformToByteArray();
293
- const buffer = Buffer.from(body);
294
- const decoded =
295
- response.ContentEncoding === "gzip" ? gunzipSync(buffer) : buffer;
296
- return decoded.toString("utf8");
297
- };
298
-
299
- const listExtractedTexts: StorageService["listExtractedTexts"] = async (
300
- accountConfigId,
301
- accountId,
302
- messageId,
303
- ) => {
304
- const prefix = buildExtractedPrefix(accountConfigId, accountId, messageId);
305
- const suffix = ".txt.gz";
306
- const items: ExtractedTextListItem[] = [];
307
- let continuationToken: string | undefined;
308
-
309
- do {
310
- const response = await client.send(
311
- new ListObjectsV2Command({
312
- Bucket: bucketName,
313
- Prefix: prefix,
314
- ContinuationToken: continuationToken,
315
- }),
316
- );
317
- for (const object of response.Contents ?? []) {
318
- if (!object.Key || !object.Key.endsWith(suffix)) continue;
319
- items.push({
320
- partPath: object.Key.slice(prefix.length, -suffix.length),
321
- key: object.Key,
322
- });
323
- }
324
- continuationToken = response.IsTruncated
325
- ? response.NextContinuationToken
326
- : undefined;
327
- } while (continuationToken);
328
-
329
- return items;
330
- };
331
-
332
- const storeOutboxAttachment: StorageService["storeOutboxAttachment"] = (
333
- params,
334
- ) => {
335
- const {
336
- accountConfigId,
337
- accountId,
338
- outboxMessageId,
339
- outboxAttachmentId,
340
- content,
341
- } = params;
342
- // `application/octet-stream` rather than the uploader's declared type:
343
- // the object is only ever read back to build a MIME part, and a stored
344
- // `text/html` would be a type any future signed-URL read path could be
345
- // tricked into serving from the content origin.
346
- return storeInternal({
347
- key: buildOutboxAttachmentKey(
348
- accountConfigId,
349
- accountId,
350
- outboxMessageId,
351
- outboxAttachmentId,
352
- ),
353
- content,
354
- contentType: "application/octet-stream",
355
- compress: false,
356
- });
357
- };
358
-
359
- const listOutboxAttachments: StorageService["listOutboxAttachments"] = async (
360
- accountConfigId,
361
- accountId,
362
- outboxMessageId,
363
- ) => {
364
- const prefix = buildOutboxAttachmentPrefix(
365
- accountConfigId,
366
- accountId,
367
- outboxMessageId,
368
- );
369
- const items: OutboxAttachmentListItem[] = [];
370
- let continuationToken: string | undefined;
371
-
372
- do {
373
- const response = await client.send(
374
- new ListObjectsV2Command({
375
- Bucket: bucketName,
376
- Prefix: prefix,
377
- ContinuationToken: continuationToken,
378
- }),
379
- );
380
- for (const object of response.Contents ?? []) {
381
- if (!object.Key) continue;
382
- items.push({
383
- outboxAttachmentId: object.Key.slice(prefix.length),
384
- key: object.Key,
385
- sizeBytes: object.Size ?? 0,
386
- });
387
- }
388
- continuationToken = response.IsTruncated
389
- ? response.NextContinuationToken
390
- : undefined;
391
- } while (continuationToken);
392
-
393
- return items;
394
- };
395
-
396
- const listOutboxDraftsWithAttachments: StorageService["listOutboxDraftsWithAttachments"] =
397
- async (accountConfigId, accountId) => {
398
- const root = `accounts/${accountConfigId}/${accountId}/outbox/`;
399
- const drafts = new Set<string>();
400
- let continuationToken: string | undefined;
401
-
402
- // Delimited listing: S3 answers with the directory-like prefixes rather
403
- // than every object under them, so this stays one call per page of
404
- // drafts instead of one per object.
405
- do {
406
- const response = await client.send(
407
- new ListObjectsV2Command({
408
- Bucket: bucketName,
409
- Prefix: root,
410
- Delimiter: "/",
411
- ContinuationToken: continuationToken,
412
- }),
413
- );
414
- for (const entry of response.CommonPrefixes ?? []) {
415
- if (!entry.Prefix) continue;
416
- drafts.add(entry.Prefix.slice(root.length).replace(/\/$/, ""));
417
- }
418
- continuationToken = response.IsTruncated
419
- ? response.NextContinuationToken
420
- : undefined;
421
- } while (continuationToken);
422
-
423
- return [...drafts];
424
- };
425
-
426
- const deleteOutboxAttachment: StorageService["deleteOutboxAttachment"] =
427
- async (accountConfigId, accountId, outboxMessageId, outboxAttachmentId) => {
428
- const prefix = buildOutboxAttachmentPrefix(
429
- accountConfigId,
430
- accountId,
431
- outboxMessageId,
432
- );
433
- await client.send(
434
- new DeleteObjectCommand({
435
- Bucket: bucketName,
436
- Key: `${prefix}${outboxAttachmentId}`,
437
- }),
438
- );
439
- };
440
-
441
- const statOutboxAttachment: StorageService["statOutboxAttachment"] = async (
442
- accountConfigId,
443
- accountId,
444
- outboxMessageId,
445
- outboxAttachmentId,
446
- ) => {
447
- const head = await client
448
- .send(
449
- new HeadObjectCommand({
450
- Bucket: bucketName,
451
- Key: buildOutboxAttachmentKey(
452
- accountConfigId,
453
- accountId,
454
- outboxMessageId,
455
- outboxAttachmentId,
456
- ),
457
- }),
458
- )
459
- .catch((error: unknown) => {
460
- if (isStorageNotFoundError(error)) return null;
461
- // A HEAD on a missing key answers 404 with no NoSuchKey code, so the
462
- // SDK's NotFound is the same absence.
463
- if ((error as { name?: string })?.name === "NotFound") return null;
464
- throw error;
465
- });
466
- if (!head) return null;
467
- return { sizeBytes: head.ContentLength ?? 0 };
468
- };
469
-
470
- const retrieveOutboxAttachment: StorageService["retrieveOutboxAttachment"] =
471
- async (accountConfigId, accountId, outboxMessageId, outboxAttachmentId) => {
472
- const key = buildOutboxAttachmentKey(
473
- accountConfigId,
474
- accountId,
475
- outboxMessageId,
476
- outboxAttachmentId,
477
- );
478
- const response = await client
479
- .send(new GetObjectCommand({ Bucket: bucketName, Key: key }))
480
- .catch((error: unknown) => {
481
- if (isStorageNotFoundError(error)) return null;
482
- throw error;
483
- });
484
- if (!response) return null;
485
- if (!response.Body) {
486
- throw new Error(`Empty response body for outbox attachment: ${key}`);
487
- }
488
- const bytes = await response.Body.transformToByteArray();
489
- return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
490
- };
491
-
492
- // The browser PUTs straight to the bucket and these bytes never touch the
493
- // API. `ContentLength` on the command hoists `content-length` into
494
- // X-Amz-SignedHeaders, so S3 recomputes the signature over the length it was
495
- // sent: a request declaring any other size fails the signature check, and a
496
- // PUT is read for exactly that many bytes. The size is bound by S3 itself,
497
- // not by anything downstream trusting the client.
498
- const createOutboxAttachmentUploadUrl: StorageService["createOutboxAttachmentUploadUrl"] =
499
- async (params) => {
500
- const expiresIn = Math.max(
501
- 1,
502
- params.expiresAt - Math.floor(Date.now() / 1000),
503
- );
504
- const uploadUrl = await getSignedUrl(
505
- client,
506
- new PutObjectCommand({
507
- Bucket: bucketName,
508
- Key: buildOutboxAttachmentKey(
509
- params.accountConfigId,
510
- params.accountId,
511
- params.outboxMessageId,
512
- params.outboxAttachmentId,
513
- ),
514
- ContentLength: params.sizeBytes,
515
- }),
516
- { expiresIn },
517
- );
518
- return { uploadUrl };
519
- };
520
-
521
- const deleteOutboxAttachments: StorageService["deleteOutboxAttachments"] =
522
- async (accountConfigId, accountId, outboxMessageId) => {
523
- const prefix = buildOutboxAttachmentPrefix(
524
- accountConfigId,
525
- accountId,
526
- outboxMessageId,
527
- );
528
- let continuationToken: string | undefined;
529
-
530
- do {
531
- const response = await client.send(
532
- new ListObjectsV2Command({
533
- Bucket: bucketName,
534
- Prefix: prefix,
535
- ContinuationToken: continuationToken,
536
- }),
537
- );
538
- const keys = (response.Contents ?? [])
539
- .map((object) => object.Key)
540
- .filter((key): key is string => typeof key === "string");
541
-
542
- if (keys.length > 0) {
543
- await client.send(
544
- new DeleteObjectsCommand({
545
- Bucket: bucketName,
546
- Delete: { Objects: keys.map((Key) => ({ Key })) },
547
- }),
548
- );
549
- }
550
-
551
- continuationToken = response.IsTruncated
552
- ? response.NextContinuationToken
553
- : undefined;
554
- } while (continuationToken);
555
- };
556
-
557
- const storeDeduplicated: StorageService["storeDeduplicated"] = (params) => {
558
- const { accountConfigId, accountId, content, contentType } = params;
559
- const checksumSha256 = computeChecksum(content);
560
- return storeInternal({
561
- key: buildDeduplicatedKey(accountConfigId, accountId, checksumSha256),
562
- content,
563
- contentType,
564
- });
565
- };
566
-
567
- const storeParsedBody: StorageService["storeParsedBody"] = (params) => {
568
- const { accountConfigId, accountId, messageId, parsed } = params;
569
- const content = Buffer.from(JSON.stringify(parsed), "utf8");
570
- return storeInternal({
571
- key: buildParsedBodyKey(accountConfigId, accountId, messageId),
572
- content,
573
- contentType: "application/json",
574
- });
575
- };
576
-
577
- const retrieveParsedBody: StorageService["retrieveParsedBody"] = async (
578
- accountConfigId,
579
- accountId,
580
- messageId,
581
- ) => {
582
- const key = buildParsedBodyKey(accountConfigId, accountId, messageId);
583
-
584
- const response = await client
585
- .send(new GetObjectCommand({ Bucket: bucketName, Key: key }))
586
- .catch((error: unknown) => {
587
- if (isStorageNotFoundError(error)) return null;
588
- throw error;
589
- });
590
-
591
- if (!response) return null;
592
- if (!response.Body) {
593
- throw new Error(`Empty response body for parsed body: ${key}`);
594
- }
595
-
596
- const body = await response.Body.transformToByteArray();
597
- const buffer = Buffer.from(body);
598
- const decoded =
599
- response.ContentEncoding === "gzip" ? gunzipSync(buffer) : buffer;
600
- return JSON.parse(decoded.toString("utf8")) as ParsedBody;
601
- };
602
-
603
- const retrieve = async (uri: string): Promise<Buffer> => {
604
- const { storageKey } = parseStorageUri(uri);
605
-
606
- const response = await client.send(
607
- new GetObjectCommand({
608
- Bucket: bucketName,
609
- Key: storageKey,
610
- }),
611
- );
612
-
613
- if (!response.Body) {
614
- throw new Error(`Empty response body for: ${uri}`);
615
- }
616
-
617
- const body = await response.Body.transformToByteArray();
618
- const buffer = Buffer.from(body);
619
-
620
- if (response.ContentEncoding === "gzip") {
621
- return gunzipSync(buffer);
622
- }
623
-
624
- return buffer;
625
- };
626
-
627
- const retrieveMessageBody: StorageService["retrieveMessageBody"] = async (
628
- accountConfigId,
629
- accountId,
630
- messageId,
631
- ) => {
632
- const key = buildMessageBodyKey(accountConfigId, accountId, messageId);
633
-
634
- const response = await client
635
- .send(new GetObjectCommand({ Bucket: bucketName, Key: key }))
636
- .catch((error: unknown) => {
637
- if (isStorageNotFoundError(error)) return null;
638
- throw error;
639
- });
640
-
641
- if (!response) return null;
642
- if (!response.Body) {
643
- throw new Error(`Empty response body for message body: ${key}`);
644
- }
645
-
646
- const body = await response.Body.transformToByteArray();
647
- const buffer = Buffer.from(body);
648
- return response.ContentEncoding === "gzip" ? gunzipSync(buffer) : buffer;
649
- };
650
-
651
- const retrieveMessageBodyStream: StorageService["retrieveMessageBodyStream"] =
652
- async (accountConfigId, accountId, messageId) => {
653
- const key = buildMessageBodyKey(accountConfigId, accountId, messageId);
654
-
655
- const response = await client
656
- .send(new GetObjectCommand({ Bucket: bucketName, Key: key }))
657
- .catch((error: unknown) => {
658
- if (isStorageNotFoundError(error)) return null;
659
- throw error;
660
- });
661
-
662
- if (!response) return null;
663
- if (!response.Body) {
664
- throw new Error(`Empty response body for message body: ${key}`);
665
- }
666
-
667
- const source = response.Body as Readable;
668
- if (response.ContentEncoding === "gzip") {
669
- return source.pipe(createGunzip());
670
- }
671
- return source;
672
- };
673
-
674
- const exists = async (uri: string): Promise<boolean> => {
675
- const { storageKey } = parseStorageUri(uri);
676
-
677
- return client
678
- .send(
679
- new HeadObjectCommand({
680
- Bucket: bucketName,
681
- Key: storageKey,
682
- }),
683
- )
684
- .then(() => true)
685
- .catch(() => false);
686
- };
687
-
688
- const del = async (uri: string): Promise<void> => {
689
- const { storageKey } = parseStorageUri(uri);
690
-
691
- await client.send(
692
- new DeleteObjectCommand({
693
- Bucket: bucketName,
694
- Key: storageKey,
695
- }),
696
- );
697
- };
698
-
699
- const storeExportArchiveStream = async (
700
- accountConfigId: string,
701
- exportRequestId: string,
702
- body: Readable,
703
- ): Promise<string> => {
704
- const key = buildExportArchiveKey(accountConfigId, exportRequestId);
705
- const upload = new Upload({
706
- client,
707
- params: {
708
- Bucket: bucketName,
709
- Key: key,
710
- Body: body,
711
- ContentType: "application/zip",
712
- },
713
- });
714
- await upload.done().catch(async (error) => {
715
- await upload.abort().catch(() => {});
716
- throw error;
717
- });
718
- return key;
719
- };
720
-
721
- const getPresignedDownloadUrl = (
722
- key: string,
723
- expiresInSeconds: number,
724
- ): Promise<string> =>
725
- getSignedUrl(
726
- client,
727
- new GetObjectCommand({ Bucket: bucketName, Key: key }),
728
- { expiresIn: expiresInSeconds },
729
- );
730
-
731
- return {
732
- storeMessageBody,
733
- storeMessageBodyStream,
734
- storeBodyPart,
735
- bodyPartExists,
736
- retrieveBodyPart,
737
- storeOutboxAttachment,
738
- listOutboxAttachments,
739
- listOutboxDraftsWithAttachments,
740
- deleteOutboxAttachments,
741
- deleteOutboxAttachment,
742
- statOutboxAttachment,
743
- retrieveOutboxAttachment,
744
- createOutboxAttachmentUploadUrl,
745
- storeDeduplicated,
746
- storeParsedBody,
747
- retrieveParsedBody,
748
- storeExtractedText,
749
- storeExtractedSkipped,
750
- extractedResultExists,
751
- retrieveExtractedText,
752
- listExtractedTexts,
753
- retrieveMessageBody,
754
- retrieveMessageBodyStream,
755
- retrieve,
756
- exists,
757
- delete: del,
758
- storeExportArchiveStream,
759
- getPresignedDownloadUrl,
760
- };
761
- };