Skip to content
Merged
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
45 changes: 45 additions & 0 deletions src/client/wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { Session, Response, Invocation } from './types.ts'
import { MailboxService } from '../mailbox/service.ts'
import { EmailService } from '../email/service.ts'
import { EmailSubmissionService } from '../submission/service.ts'
import { IdentityService } from '../identity/service.ts'
import { JMAPLive, JMAPLiveWithConfig } from '../layers.ts'
import type { Id } from '../shared/common.ts'
import type {
Expand Down Expand Up @@ -54,6 +55,15 @@ import type {
EmailSubmissionChangesArguments,
EmailSubmissionChangesResponse,
} from '../submission/schema.ts'
import type {
IdentityObject as IdentityObjectSchema,
IdentityGetArguments,
IdentityGetResponse,
IdentitySetArguments,
IdentitySetResponse,
IdentityChangesArguments,
IdentityChangesResponse,
} from '../identity/schema.ts'

// Infer the actual TypeScript types from Schema types
type Mailbox = Schema.Schema.Type<typeof MailboxSchema>
Expand All @@ -71,6 +81,10 @@ type EmailSubmissionSetResponseResult = Schema.Schema.Type<typeof EmailSubmissio
type EmailSubmissionQueryResult = Schema.Schema.Type<typeof EmailSubmissionQueryResponse>
type EmailSubmissionQueryChangesResult = Schema.Schema.Type<typeof EmailSubmissionQueryChangesResponse>
type EmailSubmissionChangesResult = Schema.Schema.Type<typeof EmailSubmissionChangesResponse>
type IdentityObject = Schema.Schema.Type<typeof IdentityObjectSchema>
type IdentityGetResult = Schema.Schema.Type<typeof IdentityGetResponse>
type IdentitySetResult = Schema.Schema.Type<typeof IdentitySetResponse>
type IdentityChangesResult = Schema.Schema.Type<typeof IdentityChangesResponse>

/**
* Promise-based Mailbox namespace.
Expand Down Expand Up @@ -161,6 +175,20 @@ export interface SubmissionNamespace {
readonly getRecent: (limit?: number, accountId?: string) => Promise<readonly EmailSubmissionObject[]>
}

/**
* Promise-based Identity namespace.
*
* Core JMAP methods accept their full argument objects.
* Convenience methods accept an optional `accountId` override.
*/
export interface IdentityNamespace {
readonly get: (args: IdentityGetArguments) => Promise<IdentityGetResult>
readonly set: (args: IdentitySetArguments) => Promise<IdentitySetResult>
readonly changes: (args: IdentityChangesArguments) => Promise<IdentityChangesResult>
readonly getAll: (accountId?: string) => Promise<readonly IdentityObject[]>
readonly getDefault: (accountId?: string) => Promise<IdentityObject>
}

/**
* Promise-based JMAP client that hides Effect internals.
*
Expand Down Expand Up @@ -204,6 +232,8 @@ export interface JMAPClientWrapper {
readonly email: EmailNamespace
/** Email submission (sending) operations */
readonly submission: SubmissionNamespace
/** Identity operations */
readonly identity: IdentityNamespace
/** Dispose the underlying runtime and release resources */
readonly dispose: () => Promise<void>
}
Expand Down Expand Up @@ -395,13 +425,28 @@ const buildClient = async (
run(Effect.flatMap(EmailSubmissionService, svc => svc.getRecent(acct ?? accountId, limit))),
}

// --- Identity namespace ---
const identity: IdentityNamespace = {
get: (args) =>
run(Effect.flatMap(IdentityService, svc => svc.get(args))),
set: (args) =>
run(Effect.flatMap(IdentityService, svc => svc.set(args))),
changes: (args) =>
run(Effect.flatMap(IdentityService, svc => svc.changes(args))),
getAll: (acct?) =>
run(Effect.flatMap(IdentityService, svc => svc.getAll(acct ?? accountId))),
getDefault: (acct?) =>
run(Effect.flatMap(IdentityService, svc => svc.getDefault(acct ?? accountId))),
}

return {
accountId,
session,
batch,
mailbox,
email,
submission,
identity,
dispose: () => runtime.dispose(),
}
}
19 changes: 19 additions & 0 deletions src/identity/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Identity schema and types
export {
IdentityObject,
IdentityMutable,
IdentityGetArguments,
IdentityGetResponse,
IdentitySetArguments,
IdentitySetResponse,
IdentityChangesArguments,
IdentityChangesResponse,
IdentityHelpers,
} from './schema.ts'

// Identity service
export {
IdentityService,
IdentityServiceLive,
type IdentityServiceInterface,
} from './service.ts'
195 changes: 195 additions & 0 deletions src/identity/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { Schema } from 'effect'
import { Id, UnsignedInt, EmailAddress } from '../shared/common.ts'

/**
* JMAP Identity schemas - RFC 8621 Section 6
*/

/**
* Core Identity object (full)
* Per RFC 8621 Section 6: replyTo and bcc are nullable (Type|null)
* All nullable fields are also optional since servers may omit them entirely
*/
export const IdentityObject = Schema.Struct({
id: Id,
name: Schema.String,
email: Schema.String,
replyTo: Schema.optional(Schema.NullOr(Schema.Array(EmailAddress))),
bcc: Schema.optional(Schema.NullOr(Schema.Array(EmailAddress))),
textSignature: Schema.String,
htmlSignature: Schema.String,
mayDelete: Schema.Boolean
})

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

/**
* Identity properties that can be set during creation
* Per RFC 8621: email is creatable only; name, replyTo, bcc, textSignature,
* htmlSignature are creatable and updatable
*/
export const IdentityMutable = Schema.Struct({
name: Schema.optional(Schema.String),
email: Schema.optional(Schema.String),
replyTo: Schema.optional(Schema.NullOr(Schema.Array(EmailAddress))),
bcc: Schema.optional(Schema.NullOr(Schema.Array(EmailAddress))),
textSignature: Schema.optional(Schema.String),
htmlSignature: Schema.optional(Schema.String)
})

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

/**
* Arguments for Identity/get method
*/
export const IdentityGetArguments = Schema.Struct({
accountId: Schema.String,
ids: Schema.Union(Schema.Array(Id), Schema.Null),
properties: Schema.optional(Schema.Array(Schema.String))
})

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

/**
* Response for Identity/get method
*/
export const IdentityGetResponse = Schema.Struct({
accountId: Schema.String,
state: Schema.String,
list: Schema.Array(IdentityObject),
notFound: Schema.Array(Id)
})

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

/**
* Arguments for Identity/set method
*/
export const IdentitySetArguments = Schema.Struct({
accountId: Schema.String,
ifInState: Schema.optional(Schema.String),
create: Schema.optional(Schema.Record({
key: Schema.String,
value: IdentityMutable
})),
update: Schema.optional(Schema.Record({
key: Id,
value: Schema.Struct({
name: Schema.optional(Schema.String),
replyTo: Schema.optional(Schema.NullOr(Schema.Array(EmailAddress))),
bcc: Schema.optional(Schema.NullOr(Schema.Array(EmailAddress))),
textSignature: Schema.optional(Schema.String),
htmlSignature: Schema.optional(Schema.String)
})
})),
destroy: Schema.optional(Schema.Array(Id))
})

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

/**
* Response for Identity/set method
* All nullable fields are also optional since servers may omit them entirely
*/
export const IdentitySetResponse = Schema.Struct({
accountId: Schema.String,
oldState: Schema.String,
newState: Schema.String,
created: Schema.optional(Schema.NullOr(
Schema.Record({
key: Schema.String,
value: Schema.Struct({
id: Id,
mayDelete: Schema.optional(Schema.Boolean)
})
})
)),
updated: Schema.optional(Schema.NullOr(
Schema.Record({
key: Id,
value: Schema.NullOr(Schema.Any)
})
)),
destroyed: Schema.optional(Schema.NullOr(Schema.Array(Id))),
notCreated: Schema.optional(Schema.NullOr(
Schema.Record({
key: Schema.String,
value: Schema.Any
})
)),
notUpdated: Schema.optional(Schema.NullOr(
Schema.Record({
key: Id,
value: Schema.Any
})
)),
notDestroyed: Schema.optional(Schema.NullOr(
Schema.Record({
key: Id,
value: Schema.Any
})
))
})

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

/**
* Arguments for Identity/changes method
*/
export const IdentityChangesArguments = Schema.Struct({
accountId: Schema.String,
sinceState: Schema.String,
maxChanges: Schema.optional(UnsignedInt)
})

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

/**
* Response for Identity/changes method
*/
export const IdentityChangesResponse = 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 IdentityChangesResponse = Schema.Schema.Type<typeof IdentityChangesResponse>

/**
* Helper functions for working with identities
*/
export const IdentityHelpers = {
/**
* Check if an identity can be deleted
*/
canDelete: (identity: IdentityObject): boolean =>
identity.mayDelete,

/**
* Check if an identity has a custom reply-to address
*/
hasReplyTo: (identity: IdentityObject): boolean =>
identity.replyTo !== null && identity.replyTo !== undefined && identity.replyTo.length > 0,

/**
* Check if an identity has auto-bcc configured
*/
hasBcc: (identity: IdentityObject): boolean =>
identity.bcc !== null && identity.bcc !== undefined && identity.bcc.length > 0,

/**
* Check if an identity has a text signature
*/
hasTextSignature: (identity: IdentityObject): boolean =>
identity.textSignature.length > 0,

/**
* Check if an identity has an HTML signature
*/
hasHtmlSignature: (identity: IdentityObject): boolean =>
identity.htmlSignature.length > 0,
}
Loading