Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/email/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,15 @@ export {
EmailQueryResponse,
EmailQueryChangesArguments,
EmailQueryChangesResponse,
EmailChangesArguments,
EmailChangesResponse,
EmailCopyArguments,
EmailCopyResponse,
EmailImportArguments,
EmailImportResponse,
EmailParseArguments,
EmailParseResponse,
ParsedEmail,
EmailMutable,
StandardProperties,
EmailHelpers,
Expand Down
115 changes: 115 additions & 0 deletions src/email/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,121 @@ export const EmailImportResponse = Schema.Struct({

export type EmailImportResponse = Schema.Schema.Type<typeof EmailImportResponse>

/**
* 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<typeof EmailChangesArguments>

/**
* 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<typeof EmailChangesResponse>

/**
* 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<typeof EmailParseArguments>

/**
* 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<typeof ParsedEmail>

/**
* 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<typeof EmailParseResponse>

/**
* Standard email properties for convenience
*/
Expand Down
66 changes: 66 additions & 0 deletions src/email/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@ import {
EmailQueryResponse,
EmailQueryChangesArguments,
EmailQueryChangesResponse,
EmailChangesArguments,
EmailChangesResponse,
EmailCopyArguments,
EmailCopyResponse,
EmailImportArguments,
EmailImportResponse,
EmailParseArguments,
EmailParseResponse,
EmailMutable,
EmailFilterCondition,
EmailHelpers,
Expand Down Expand Up @@ -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<typeof EmailChangesResponse>,
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
*/
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -662,8 +726,10 @@ const makeEmailServiceLive = (): EmailServiceInterface => {
set,
query,
queryChanges,
changes,
copy,
import: emailImport,
parse,
getByMailbox,
search,
getUnread,
Expand Down
4 changes: 2 additions & 2 deletions tests/config/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down