diff --git a/src/email/index.ts b/src/email/index.ts index dd9a680..64b14a1 100644 --- a/src/email/index.ts +++ b/src/email/index.ts @@ -16,10 +16,15 @@ export { EmailQueryResponse, EmailQueryChangesArguments, EmailQueryChangesResponse, + EmailChangesArguments, + EmailChangesResponse, EmailCopyArguments, EmailCopyResponse, EmailImportArguments, EmailImportResponse, + EmailParseArguments, + EmailParseResponse, + ParsedEmail, EmailMutable, StandardProperties, EmailHelpers, diff --git a/src/email/schema.ts b/src/email/schema.ts index a2654bf..cc8c930 100644 --- a/src/email/schema.ts +++ b/src/email/schema.ts @@ -463,6 +463,121 @@ export const EmailImportResponse = Schema.Struct({ export type EmailImportResponse = Schema.Schema.Type +/** + * Arguments for Email/changes method + * Per RFC 8621 Section 4.6: Standard /changes method for tracking email state changes + */ +export const EmailChangesArguments = Schema.Struct({ + accountId: Schema.String, + sinceState: Schema.String, + maxChanges: Schema.optional(UnsignedInt) +}) + +export type EmailChangesArguments = Schema.Schema.Type + +/** + * Response for Email/changes method + * Per RFC 8621: Returns lists of created, updated, and destroyed email IDs + */ +export const EmailChangesResponse = Schema.Struct({ + accountId: Schema.String, + oldState: Schema.String, + newState: Schema.String, + hasMoreChanges: Schema.Boolean, + created: Schema.Array(Id), + updated: Schema.Array(Id), + destroyed: Schema.Array(Id) +}) + +export type EmailChangesResponse = Schema.Schema.Type + +/** + * Arguments for Email/parse method + * Per RFC 8621 Section 4.8: Parse blob data as RFC 5322 messages + */ +export const EmailParseArguments = Schema.Struct({ + accountId: Schema.String, + blobIds: Schema.Array(Schema.String), + properties: Schema.optional(Schema.Array(Schema.String)), + bodyProperties: Schema.optional(Schema.Array(Schema.String)), + fetchTextBodyValues: Schema.optional(Schema.Boolean), + fetchHTMLBodyValues: Schema.optional(Schema.Boolean), + fetchAllBodyValues: Schema.optional(Schema.Boolean), + maxBodyValueBytes: Schema.optional(UnsignedInt) +}) + +export type EmailParseArguments = Schema.Schema.Type + +/** + * Parsed email object returned by Email/parse + * This is similar to Email but with some differences: + * - id is the blobId that was parsed (not a real email ID) + * - Some server-computed fields may be missing + * Per RFC 8621: All nullable fields follow the same pattern as Email + */ +export const ParsedEmail = Schema.Struct({ + // The blobId that was parsed + blobId: Schema.optional(Schema.String), + // Size of the raw message + size: Schema.optional(UnsignedInt), + // Headers + headers: Schema.optional(Schema.Array(EmailHeader)), + // Message-ID header + messageId: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))), + // In-Reply-To header + inReplyTo: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))), + // References header + references: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))), + // Sender header + sender: Schema.optional(Schema.NullOr(Schema.Array(EmailAddress))), + // From header + from: Schema.optional(Schema.NullOr(Schema.Array(EmailAddress))), + // To header + to: Schema.optional(Schema.NullOr(Schema.Array(EmailAddress))), + // Cc header + cc: Schema.optional(Schema.NullOr(Schema.Array(EmailAddress))), + // Bcc header + bcc: Schema.optional(Schema.NullOr(Schema.Array(EmailAddress))), + // Reply-To header + replyTo: Schema.optional(Schema.NullOr(Schema.Array(EmailAddress))), + // Subject header + subject: Schema.optional(Schema.NullOr(Schema.String)), + // Date header parsed as date + sentAt: Schema.optional(Schema.NullOr(JMAPDate)), + // Body structure + bodyStructure: Schema.optional(EmailBodyPart), + // Text body parts + textBody: Schema.optional(Schema.Array(EmailBodyPart)), + // HTML body parts + htmlBody: Schema.optional(Schema.Array(EmailBodyPart)), + // Attachments + attachments: Schema.optional(Schema.Array(EmailAttachment)), + // Has attachment flag + hasAttachment: Schema.optional(Schema.Boolean), + // Preview text + preview: Schema.optional(Schema.String), + // Body values (content) + bodyValues: Schema.optional(EmailBodyValues) +}) + +export type ParsedEmail = Schema.Schema.Type + +/** + * Response for Email/parse method + * Per RFC 8621: Returns parsed email objects keyed by blob ID + */ +export const EmailParseResponse = Schema.Struct({ + accountId: Schema.String, + parsed: Schema.optional(Schema.NullOr(Schema.Record({ + key: Schema.String, + value: ParsedEmail + }))), + notParsable: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))), + notFound: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))) +}) + +export type EmailParseResponse = Schema.Schema.Type + /** * Standard email properties for convenience */ diff --git a/src/email/service.ts b/src/email/service.ts index b2bc610..5560613 100644 --- a/src/email/service.ts +++ b/src/email/service.ts @@ -24,10 +24,14 @@ import { EmailQueryResponse, EmailQueryChangesArguments, EmailQueryChangesResponse, + EmailChangesArguments, + EmailChangesResponse, EmailCopyArguments, EmailCopyResponse, EmailImportArguments, EmailImportResponse, + EmailParseArguments, + EmailParseResponse, EmailMutable, EmailFilterCondition, EmailHelpers, @@ -106,6 +110,30 @@ export interface EmailServiceInterface { JMAPClientService | HttpClient.HttpClient | IdGenerator >; + /** + * Get changes to emails since a state + * Per RFC 8621 Section 4.6: Standard /changes method + */ + readonly changes: ( + args: EmailChangesArguments, + ) => Effect.Effect< + Schema.Schema.Type, + JMAPMethodError | NetworkError | AuthenticationError | SessionError, + JMAPClientService | HttpClient.HttpClient | IdGenerator + >; + + /** + * Parse blob data as RFC 5322 messages + * Per RFC 8621 Section 4.8: Email/parse method + */ + readonly parse: ( + args: EmailParseArguments, + ) => Effect.Effect< + EmailParseResponse, + JMAPMethodError | NetworkError | AuthenticationError | SessionError, + JMAPClientService | HttpClient.HttpClient | IdGenerator + >; + /** * Get emails in a mailbox */ @@ -367,6 +395,42 @@ const makeEmailServiceLive = (): EmailServiceInterface => { ); }); + const changes: EmailServiceInterface["changes"] = (args) => + Effect.gen(function* () { + const client = yield* JMAPClientService; + const idGenerator = yield* IdGenerator; + const id = yield* idGenerator.generate; + const callId = `email-changes-${id}`; + + const methodCall: Invocation = ["Email/changes", args, callId]; + + const response = yield* client.batch([methodCall], [...CAPABILITY_SETS.MAIL]); + return yield* extractMethodResponse( + response, + "Email/changes", + callId, + EmailChangesResponse, + ); + }); + + const parse: EmailServiceInterface["parse"] = (args) => + Effect.gen(function* () { + const client = yield* JMAPClientService; + const idGenerator = yield* IdGenerator; + const id = yield* idGenerator.generate; + const callId = `email-parse-${id}`; + + const methodCall: Invocation = ["Email/parse", args, callId]; + + const response = yield* client.batch([methodCall], [...CAPABILITY_SETS.MAIL]); + return yield* extractMethodResponse( + response, + "Email/parse", + callId, + EmailParseResponse, + ); + }); + const getByMailbox: EmailServiceInterface["getByMailbox"] = ( accountId, mailboxId, @@ -662,8 +726,10 @@ const makeEmailServiceLive = (): EmailServiceInterface => { set, query, queryChanges, + changes, copy, import: emailImport, + parse, getByMailbox, search, getUnread, diff --git a/tests/config/capabilities.ts b/tests/config/capabilities.ts index d855ee4..c2d2662 100644 --- a/tests/config/capabilities.ts +++ b/tests/config/capabilities.ts @@ -35,10 +35,10 @@ export const JMAPCapabilities = { 'Email/set': true, 'Email/query': true, 'Email/queryChanges': true, - 'Email/changes': false, + 'Email/changes': true, 'Email/copy': true, 'Email/import': true, - 'Email/parse': false, + 'Email/parse': true, // SearchSnippet methods (RFC 8621 Section 5) 'SearchSnippet/get': false,