diff --git a/docs/docs/reference/enum-types.mdx b/docs/docs/reference/enum-types.mdx index 99bef6b7..b12dc5da 100644 --- a/docs/docs/reference/enum-types.mdx +++ b/docs/docs/reference/enum-types.mdx @@ -6,6 +6,16 @@ sidebar_position: 32 These are the supported enum types and the possible values that can be used in the configuration. +## AttachmentOrderField + +Represents an attachment field to be ordered by for processing. + +| Value | Description | +|-------|-------------| +| `contentType` | Order by the content type of the attachment. | +| `hash` | Order by the hash of the attachment. | +| `name` | Order by the name of the attachment. | + ## ConflictStrategy Strategy that defines how to deal in case of conflicts with already existing files at the desired location in Google Drive. @@ -85,6 +95,17 @@ A flag to match messages with certain properties. | `unread` | Matches unread messages. | | `unstarred` | Matches un-starred messages. | +## MessageOrderField + +Represents a message field to be ordered by for processing. + +| Value | Description | +|-------|-------------| +| `date` | Order by the date of the message. | +| `from` | Order by the sender of the message. | +| `id` | Order by the ID of the message. | +| `subject` | Order by the subject of the message. | + ## MetaInfoType The type of meta information used for context substitution placeholders. @@ -97,6 +118,15 @@ The type of meta information used for context substitution placeholders. | `string` | A string data type. | | `variable` | A custom configuration variable. | +## OrderDirection + +Represents the direction a list should be ordered. + +| Value | Description | +|-------|-------------| +| `asc` | Order ascending. | +| `desc` | Order descending. | + ## PlaceholderModifierType The modifiers for placeholder expressions. @@ -148,3 +178,13 @@ The runtime mode in which processing takes place. | `dangerous` | This run-mode will execute all configured actions including possibly destructive actions like overwriting files or removing threads or messages.
ATTENTION: Use this only if you know exactly what you're doing and won't complain if something goes wrong! | | `dry-run` | This run-mode skips execution of writing actions. Use this for testing config changes or library upgrades. | | `safe-mode` | This run-mode can be used for normal operation but will skip possibly destructive actions like overwriting files or removing threads or messages. | + +## ThreadOrderField + +Represents a thread field to be ordered by for processing. + +| Value | Description | +|-------|-------------| +| `lastMessageDate` | Order by the date of the last message in the thread. | +| `id` | Order by the ID of the thread. | +| `firstMessageSubject` | Order by the subject of the first message in the thread. | diff --git a/src/lib/config/AttachmentConfig.ts b/src/lib/config/AttachmentConfig.ts index 809d879b..4fdba03f 100644 --- a/src/lib/config/AttachmentConfig.ts +++ b/src/lib/config/AttachmentConfig.ts @@ -12,6 +12,25 @@ import { essentialAttachmentMatchConfig, newAttachmentMatchConfig, } from "./AttachmentMatchConfig" +import { OrderDirection } from "./CommonConfig" + +/** + * Represents an attachment field to be ordered by for processing. + */ +export enum AttachmentOrderField { + /** + * Order by the content type of the attachment. + */ + CONTENT_TYPE = "contentType", + /** + * Order by the hash of the attachment. + */ + HASH = "hash", + /** + * Order by the name of the attachment. + */ + NAME = "name", +} /** * Represents a config to handle a certain GMail attachment @@ -39,6 +58,16 @@ export class AttachmentConfig { */ @Expose() name? = "" + /** + * The field to order attachments by for processing. + */ + @Expose() + orderBy?: AttachmentOrderField = undefined + /** + * The direction to order attachments for processing. + */ + @Expose() + orderDirection?: OrderDirection = undefined } export type RequiredAttachmentConfig = RequiredDeep diff --git a/src/lib/config/CommonConfig.ts b/src/lib/config/CommonConfig.ts new file mode 100644 index 00000000..5f8a6114 --- /dev/null +++ b/src/lib/config/CommonConfig.ts @@ -0,0 +1,18 @@ +/** + * Represents the direction a list should be ordered. + */ +export enum OrderDirection { + /** + * Order ascending. + */ + ASC = "asc", + /** + * Order descending. + */ + DESC = "desc", +} + +export type OrderableEntityConfig = { + orderBy: T + orderDirection: OrderDirection +} diff --git a/src/lib/config/MessageConfig.ts b/src/lib/config/MessageConfig.ts index 66ff69c4..9a16516c 100644 --- a/src/lib/config/MessageConfig.ts +++ b/src/lib/config/MessageConfig.ts @@ -12,12 +12,35 @@ import { essentialAttachmentConfig, normalizeAttachmentConfigs, } from "./AttachmentConfig" +import { OrderDirection } from "./CommonConfig" import { MessageMatchConfig, essentialMessageMatchConfig, newMessageMatchConfig, } from "./MessageMatchConfig" +/** + * Represents a message field to be ordered by for processing. + */ +export enum MessageOrderField { + /** + * Order by the date of the message. + */ + DATE = "date", + /** + * Order by the sender of the message. + */ + FROM = "from", + /** + * Order by the ID of the message. + */ + ID = "id", + /** + * Order by the subject of the message. + */ + SUBJECT = "subject", +} + /** * Represents a config to handle a certain GMail message */ @@ -50,6 +73,16 @@ export class MessageConfig { */ @Expose() name? = "" + /** + * The field to order messages by for processing. + */ + @Expose() + orderBy?: MessageOrderField = undefined + /** + * The direction to order messages for processing. + */ + @Expose() + orderDirection?: OrderDirection = undefined } export type RequiredMessageConfig = RequiredDeep diff --git a/src/lib/config/ThreadConfig.ts b/src/lib/config/ThreadConfig.ts index 8da0c910..1a786dd8 100644 --- a/src/lib/config/ThreadConfig.ts +++ b/src/lib/config/ThreadConfig.ts @@ -8,6 +8,7 @@ import { essentialThreadActionConfig, } from "./ActionConfig" import { AttachmentConfig, essentialAttachmentConfig } from "./AttachmentConfig" +import { OrderDirection } from "./CommonConfig" import { MessageConfig, essentialMessageConfig, @@ -19,6 +20,24 @@ import { newThreadMatchConfig, } from "./ThreadMatchConfig" +/** + * Represents a thread field to be ordered by for processing. + */ +export enum ThreadOrderField { + /** + * Order by the date of the last message in the thread. + */ + DATE = "lastMessageDate", + /** + * Order by the ID of the thread. + */ + ID = "id", + /** + * Order by the subject of the first message in the thread. + */ + SUBJECT = "firstMessageSubject", +} + /** * Represents a config handle a certain GMail thread */ @@ -57,6 +76,16 @@ export class ThreadConfig { */ @Expose() name? = "" + /** + * The field to order threads by for processing. + */ + @Expose() + orderBy?: ThreadOrderField = undefined + /** + * The direction to order threads for processing. + */ + @Expose() + orderDirection?: OrderDirection = undefined } export type RequiredThreadConfig = RequiredDeep diff --git a/src/lib/config/config-schema-v2.json b/src/lib/config/config-schema-v2.json index 4a5c12a9..50ca2ade 100644 --- a/src/lib/config/config-schema-v2.json +++ b/src/lib/config/config-schema-v2.json @@ -31,6 +31,25 @@ "description": "The unique name of the attachment config (will be generated if not set)", "title": "name", "type": "string" + }, + "orderBy": { + "description": "The field to order attachments by for processing.", + "enum": [ + "contentType", + "hash", + "name" + ], + "title": "orderBy", + "type": "string" + }, + "orderDirection": { + "description": "The direction to order attachments for processing.", + "enum": [ + "asc", + "desc" + ], + "title": "orderDirection", + "type": "string" } }, "title": "AttachmentConfig", @@ -1548,6 +1567,26 @@ "description": "The unique name of the message config (will be generated if not set)", "title": "name", "type": "string" + }, + "orderBy": { + "description": "The field to order messages by for processing.", + "enum": [ + "date", + "from", + "id", + "subject" + ], + "title": "orderBy", + "type": "string" + }, + "orderDirection": { + "description": "The direction to order messages for processing.", + "enum": [ + "asc", + "desc" + ], + "title": "orderDirection", + "type": "string" } }, "title": "MessageConfig", @@ -3046,6 +3085,25 @@ "description": "The unique name of the thread config (will be generated if not set)", "title": "name", "type": "string" + }, + "orderBy": { + "description": "The field to order threads by for processing.", + "enum": [ + "firstMessageSubject", + "id", + "lastMessageDate" + ], + "title": "orderBy", + "type": "string" + }, + "orderDirection": { + "description": "The direction to order threads for processing.", + "enum": [ + "asc", + "desc" + ], + "title": "orderDirection", + "type": "string" } }, "title": "ThreadConfig", diff --git a/src/lib/processors/AttachmentProcessor.spec.ts b/src/lib/processors/AttachmentProcessor.spec.ts index fccbb368..8e86a2da 100644 --- a/src/lib/processors/AttachmentProcessor.spec.ts +++ b/src/lib/processors/AttachmentProcessor.spec.ts @@ -1,12 +1,16 @@ import { GMailMocks } from "../../test/mocks/GMailMocks" import { MockFactory, Mocks } from "../../test/mocks/MockFactory" import { ProcessingStatus } from "../Context" -import { newAttachmentConfig } from "../config/AttachmentConfig" +import { + AttachmentOrderField, + newAttachmentConfig, +} from "../config/AttachmentConfig" import { AttachmentMatchConfig, RequiredAttachmentMatchConfig, newAttachmentMatchConfig, } from "../config/AttachmentMatchConfig" +import { OrderDirection } from "../config/CommonConfig" import { AttachmentProcessor } from "./AttachmentProcessor" let mocks: Mocks @@ -187,3 +191,42 @@ it("should process an attachment entity", () => { status: ProcessingStatus.OK, }) }) + +describe("order attachments", () => { + let attachments = [ + GMailMocks.newAttachmentMock({ + hash: "a1", + name: "2", + }), + GMailMocks.newAttachmentMock({ + hash: "a2", + name: "1", + }), + GMailMocks.newAttachmentMock({ + hash: "a3", + name: "3", + }), + ] + it("should order threads ascending", () => { + attachments = AttachmentProcessor.ordered( + attachments, + { + orderBy: AttachmentOrderField.NAME, + orderDirection: OrderDirection.ASC, + }, + AttachmentProcessor.orderRules, + ) + expect(attachments.map((t) => t.getHash())).toEqual(["a2", "a1", "a3"]) + }) + it("should order threads ascending", () => { + attachments = AttachmentProcessor.ordered( + attachments, + { + orderBy: AttachmentOrderField.NAME, + orderDirection: OrderDirection.DESC, + }, + AttachmentProcessor.orderRules, + ) + expect(attachments.map((t) => t.getHash())).toEqual(["a3", "a1", "a2"]) + }) +}) diff --git a/src/lib/processors/AttachmentProcessor.ts b/src/lib/processors/AttachmentProcessor.ts index 450aa1d7..bca2fb4d 100644 --- a/src/lib/processors/AttachmentProcessor.ts +++ b/src/lib/processors/AttachmentProcessor.ts @@ -1,9 +1,13 @@ import { ProcessingStage } from "../config/ActionConfig" -import { RequiredAttachmentConfig } from "../config/AttachmentConfig" +import { + AttachmentOrderField, + RequiredAttachmentConfig, +} from "../config/AttachmentConfig" import { AttachmentMatchConfig, RequiredAttachmentMatchConfig, } from "../config/AttachmentMatchConfig" +import { OrderableEntityConfig, OrderDirection } from "../config/CommonConfig" import { Attachment, AttachmentContext, @@ -226,6 +230,22 @@ export class AttachmentProcessor extends BaseProcessor { return result } + public static orderRules( + a: GoogleAppsScript.Gmail.GmailAttachment, + b: GoogleAppsScript.Gmail.GmailAttachment, + config: OrderableEntityConfig, + ): number { + return ( + { + [AttachmentOrderField.CONTENT_TYPE]: a + .getContentType() + .localeCompare(b.getContentType()), + [AttachmentOrderField.HASH]: a.getHash().localeCompare(b.getHash()), + [AttachmentOrderField.NAME]: a.getName().localeCompare(b.getName()), + }[config.orderBy] * (config.orderDirection == OrderDirection.ASC ? 1 : -1) + ) + } + public static processConfig( ctx: MessageContext, config: RequiredAttachmentConfig, @@ -245,7 +265,11 @@ export class AttachmentProcessor extends BaseProcessor { includeAttachments: matchConfig.includeAttachments, includeInlineImages: matchConfig.includeInlineImages, } - const attachments = ctx.message.object.getAttachments(opts) + const attachments = this.ordered( + ctx.message.object.getAttachments(opts), + config, + this.orderRules, + ) for (let index = 0; index < attachments.length; index++) { const attachment = attachments[index] if (!this.matches(ctx, matchConfig, attachment)) { diff --git a/src/lib/processors/BaseProcessor.ts b/src/lib/processors/BaseProcessor.ts index 5308339e..cb0b94e8 100644 --- a/src/lib/processors/BaseProcessor.ts +++ b/src/lib/processors/BaseProcessor.ts @@ -20,6 +20,7 @@ import { import { ActionArgsType, ActionReturnType } from "../actions/ActionRegistry" import { ActionConfig, ProcessingStage } from "../config/ActionConfig" import { AttachmentMatchConfig } from "../config/AttachmentMatchConfig" +import { OrderableEntityConfig } from "../config/CommonConfig" import { MessageMatchConfig } from "../config/MessageMatchConfig" import { ThreadMatchConfig } from "../config/ThreadMatchConfig" import { PatternUtil } from "../utils/PatternUtil" @@ -441,32 +442,14 @@ export abstract class BaseProcessor { return `${description}See [${title}](https://developers.google.com/apps-script/reference/gmail/gmail-${type}#${method}\\(\\)) reference docs.` } - protected static getConfigName( - // TODO: Remove, is obsolete now. - ctx: ThreadContext | MessageContext | AttachmentContext, - namePrefix = "", - ) { - let threadName = "" - let messageName = "" - let attachmentName = "" - if ((ctx as ThreadContext).thread) { - const threadCtx = ctx as ThreadContext - threadName = - threadCtx.thread.config.name ?? - `${threadCtx.thread.config.name}-${threadCtx.thread.configIndex}` - } - if ((ctx as MessageContext).message) { - const messageCtx = ctx as MessageContext - messageName = - messageCtx.message.config.name ?? - `-${messageCtx.message.config.name}-${messageCtx.message.configIndex}` - } - if ((ctx as AttachmentContext).message) { - const attachmentCtx = ctx as AttachmentContext - attachmentName = - attachmentCtx.attachment.config.name ?? - `-${attachmentCtx.attachment.config.name}-${attachmentCtx.attachment.configIndex}` + public static ordered( + entities: T[], + config: OrderableEntityConfig, + orderRulesFn: (a: T, b: T, config: OrderableEntityConfig) => number, + ): T[] { + if (config.orderBy && config.orderDirection) { + entities = entities.sort((a: T, b: T) => orderRulesFn(a, b, config)) } - return `${namePrefix}${threadName}${messageName}${attachmentName}` + return entities } } diff --git a/src/lib/processors/MessageProcessor.spec.ts b/src/lib/processors/MessageProcessor.spec.ts index 0552e712..9a096d2c 100644 --- a/src/lib/processors/MessageProcessor.spec.ts +++ b/src/lib/processors/MessageProcessor.spec.ts @@ -1,6 +1,7 @@ import { GMailMocks } from "../../test/mocks/GMailMocks" import { MockFactory, Mocks } from "../../test/mocks/MockFactory" -import { newMessageConfig } from "../config/MessageConfig" +import { OrderDirection } from "../config/CommonConfig" +import { MessageOrderField, newMessageConfig } from "../config/MessageConfig" import { MessageFlag } from "../config/MessageFlag" import { MessageMatchConfig } from "../config/MessageMatchConfig" import { MessageProcessor } from "./MessageProcessor" @@ -131,3 +132,36 @@ describe("processEntity()", () => { MessageProcessor.processEntity(ctx) }) }) + +describe("order messages", () => { + let messages = [ + GMailMocks.newMessageMock({ + id: "m1", + date: new Date(2024, 5, 10), + }), + GMailMocks.newMessageMock({ + id: "m2", + date: new Date(2024, 5, 9), + }), + GMailMocks.newMessageMock({ + id: "m3", + date: new Date(2024, 5, 11), + }), + ] + it("should order threads ascending", () => { + messages = MessageProcessor.ordered( + messages, + { orderBy: MessageOrderField.DATE, orderDirection: OrderDirection.ASC }, + MessageProcessor.orderRules, + ) + expect(messages.map((t) => t.getId())).toEqual(["m2", "m1", "m3"]) + }) + it("should order threads ascending", () => { + messages = MessageProcessor.ordered( + messages, + { orderBy: MessageOrderField.DATE, orderDirection: OrderDirection.DESC }, + MessageProcessor.orderRules, + ) + expect(messages.map((t) => t.getId())).toEqual(["m3", "m1", "m2"]) + }) +}) diff --git a/src/lib/processors/MessageProcessor.ts b/src/lib/processors/MessageProcessor.ts index 5ac701bb..79384cc9 100644 --- a/src/lib/processors/MessageProcessor.ts +++ b/src/lib/processors/MessageProcessor.ts @@ -11,7 +11,11 @@ import { newProcessingResult, } from "../Context" import { ProcessingStage } from "../config/ActionConfig" -import { RequiredMessageConfig } from "../config/MessageConfig" +import { OrderDirection, OrderableEntityConfig } from "../config/CommonConfig" +import { + MessageOrderField, + RequiredMessageConfig, +} from "../config/MessageConfig" import { MessageFlag } from "../config/MessageFlag" import { MessageMatchConfig, @@ -370,6 +374,23 @@ export class MessageProcessor extends BaseProcessor { return m } + public static orderRules( + a: GoogleAppsScript.Gmail.GmailMessage, + b: GoogleAppsScript.Gmail.GmailMessage, + config: OrderableEntityConfig, + ): number { + return ( + { + [MessageOrderField.DATE]: a.getDate().getTime() - b.getDate().getTime(), + [MessageOrderField.FROM]: a.getFrom().localeCompare(b.getFrom()), + [MessageOrderField.ID]: a.getId().localeCompare(b.getId()), + [MessageOrderField.SUBJECT]: a + .getSubject() + .localeCompare(b.getSubject()), + }[config.orderBy] * (config.orderDirection == OrderDirection.ASC ? 1 : -1) + ) + } + public static processConfig( ctx: ThreadContext, config: RequiredMessageConfig, @@ -380,7 +401,11 @@ export class MessageProcessor extends BaseProcessor { location: "MessageProcessor.processConfig()", message: `Processing message config '${configIndex}' started ...`, }) - const messages = ctx.thread.object.getMessages() + const messages = this.ordered( + ctx.thread.object.getMessages(), + config, + this.orderRules, + ) const matchConfig = this.buildMatchConfig( ctx, ctx.proc.config.global.message.match, diff --git a/src/lib/processors/ThreadProcessor.spec.ts b/src/lib/processors/ThreadProcessor.spec.ts index 9edd672a..16e0d021 100644 --- a/src/lib/processors/ThreadProcessor.spec.ts +++ b/src/lib/processors/ThreadProcessor.spec.ts @@ -1,10 +1,12 @@ import { ConfigMocks } from "../../test/mocks/ConfigMocks" import { ContextMocks } from "../../test/mocks/ContextMocks" +import { GMailMocks } from "../../test/mocks/GMailMocks" import { MockFactory, Mocks } from "../../test/mocks/MockFactory" import { ProcessingStatus } from "../Context" +import { OrderDirection } from "../config/CommonConfig" import { newConfig } from "../config/Config" import { MarkProcessedMethod } from "../config/SettingsConfig" -import { newThreadConfig } from "../config/ThreadConfig" +import { ThreadOrderField, newThreadConfig } from "../config/ThreadConfig" import { ThreadProcessor } from "./ThreadProcessor" let mocks: Mocks @@ -108,3 +110,40 @@ it("should process an thread entity", () => { status: ProcessingStatus.OK, }) }) + +describe("order threads", () => { + let threads: GoogleAppsScript.Gmail.GmailThread[] + beforeAll(() => { + jest.useRealTimers() + threads = [ + GMailMocks.newThreadMock({ + id: "t1", + lastMessageDate: new Date(2024, 5, 10), + }), + GMailMocks.newThreadMock({ + id: "t2", + lastMessageDate: new Date(2024, 5, 9), + }), + GMailMocks.newThreadMock({ + id: "t3", + lastMessageDate: new Date(2024, 5, 11), + }), + ] + }) + it("should order threads ascending", () => { + threads = ThreadProcessor.ordered( + threads, + { orderBy: ThreadOrderField.DATE, orderDirection: OrderDirection.ASC }, + ThreadProcessor.orderRules, + ) + expect(threads.map((t) => t.getId())).toEqual(["t2", "t1", "t3"]) + }) + it("should order threads ascending", () => { + threads = ThreadProcessor.ordered( + threads, + { orderBy: ThreadOrderField.DATE, orderDirection: OrderDirection.DESC }, + ThreadProcessor.orderRules, + ) + expect(threads.map((t) => t.getId())).toEqual(["t3", "t1", "t2"]) + }) +}) diff --git a/src/lib/processors/ThreadProcessor.ts b/src/lib/processors/ThreadProcessor.ts index d7445254..85fe880f 100644 --- a/src/lib/processors/ThreadProcessor.ts +++ b/src/lib/processors/ThreadProcessor.ts @@ -11,6 +11,7 @@ import { newProcessingResult, } from "../Context" import { ProcessingStage } from "../config/ActionConfig" +import { OrderDirection, OrderableEntityConfig } from "../config/CommonConfig" import { RequiredThreadConfig } from "../config/ThreadConfig" import { RequiredThreadMatchConfig, @@ -18,6 +19,7 @@ import { } from "../config/ThreadMatchConfig" import { PatternUtil } from "../utils/PatternUtil" import { RegexUtils } from "../utils/RegexUtils" +import { ThreadOrderField } from "./../config/ThreadConfig" import { BaseProcessor } from "./BaseProcessor" import { MessageProcessor } from "./MessageProcessor" @@ -350,6 +352,23 @@ export class ThreadProcessor extends BaseProcessor { return result } + public static orderRules( + a: GoogleAppsScript.Gmail.GmailThread, + b: GoogleAppsScript.Gmail.GmailThread, + config: OrderableEntityConfig, + ): number { + return ( + { + [ThreadOrderField.DATE]: + a.getLastMessageDate().getTime() - b.getLastMessageDate().getTime(), + [ThreadOrderField.ID]: a.getId().localeCompare(b.getId()), + [ThreadOrderField.SUBJECT]: a + .getFirstMessageSubject() + .localeCompare(b.getFirstMessageSubject()), + }[config.orderBy] * (config.orderDirection == OrderDirection.ASC ? 1 : -1) + ) + } + public static processConfig( ctx: ProcessingContext, config: RequiredThreadConfig, @@ -368,9 +387,13 @@ export class ThreadProcessor extends BaseProcessor { const query = PatternUtil.substitute(ctx, this.buildQuery(matchConfig)) ctx.log.info(`GMail search query: ${query}`) // Process all threads matching the search expression: - const threads = ctx.proc.gmailAdapter.search( - query, - ctx.proc.config.settings.maxBatchSize, + const threads = this.ordered( + ctx.proc.gmailAdapter.search( + query, + ctx.proc.config.settings.maxBatchSize, + ), + config, + this.orderRules, ) ctx.log.info(`-> got ${threads.length} threads`) for (let threadIndex = 0; threadIndex < threads.length; threadIndex++) { diff --git a/src/test/mocks/GMailMocks.ts b/src/test/mocks/GMailMocks.ts index 59c267f8..91ce9a2c 100644 --- a/src/test/mocks/GMailMocks.ts +++ b/src/test/mocks/GMailMocks.ts @@ -225,12 +225,14 @@ export class GMailMocks { isInTrash: data.isInTrash ?? false, isUnread: data.isUnread ?? true, labels: data.labels ?? [], - lastMessageDate: messages.reduce((lastMessage, currentMessage) => - lastMessage.date.getMilliseconds() < - currentMessage.date.getMilliseconds() - ? currentMessage - : lastMessage, - ).date, + lastMessageDate: + data.lastMessageDate ?? + messages.reduce((lastMessage, currentMessage) => + lastMessage.date.getMilliseconds() < + currentMessage.date.getMilliseconds() + ? currentMessage + : lastMessage, + ).date, messageCount: messages.length, messages: messages, permalink: data.permalink ?? "some-permalink-url", @@ -296,9 +298,11 @@ export class GMailMocks { const sampleContent = data.content ?? "Sample text content" const sampleData: RequiredAttachmentData = { contentType: data.contentType ?? "text/plain", - hash: crypto.createHash("sha1").update(sampleContent).digest("hex"), + hash: + data.hash ?? + crypto.createHash("sha1").update(sampleContent).digest("hex"), name: data.name ?? "attachment.txt", - size: this.lengthInUtf8Bytes(sampleContent), + size: data.size ?? this.lengthInUtf8Bytes(sampleContent), isGoogleType: data.isGoogleType ?? false, content: sampleContent, }