import { resolveMicrosoftIntegration, type MicrosoftAccountParams } from './accounts.ts'
import { graphItems, graphRequest, requireConfirmOrDryRun, type JsonObject } from './core.ts'
import { boundedTop } from './validation.ts'
export type MailListParams = MicrosoftAccountParams & {
folderId?: string
top?: number
skip?: number
filter?: string
search?: string
orderBy?: string
select?: string
}
export type MailGetParams = MicrosoftAccountParams & {
id: string
includeBody?: boolean
select?: string
}
export type MailSendParams = MicrosoftAccountParams & {
subject: string
body: string
to: string | string[]
cc?: string | string[]
bcc?: string | string[]
contentType?: 'text' | 'html'
saveToSentItems?: boolean
dryRun?: boolean
confirm?: boolean
}
export type MailReplyParams = MicrosoftAccountParams & {
messageId: string
comment?: string
dryRun?: boolean
confirm?: boolean
}
const MAIL_LIST_SELECT =
'id,subject,from,toRecipients,receivedDateTime,isRead,hasAttachments,bodyPreview,webLink,conversationId'
function recipientList(value: string | string[] | undefined): Array<{ emailAddress: { address: string } }> {
if (!value) return []
const addresses = Array.isArray(value) ? value : [value]
return addresses
.map((address) => address.trim())
.filter(Boolean)
.map((address) => ({ emailAddress: { address } }))
}
function mailPath(folderId?: string): string {
return folderId
? '/me/mailFolders/' + encodeURIComponent(folderId) + '/messages'
: '/me/messages'
}
/** List Outlook messages in the mailbox or a folder. */
export async function listMessages(params: MailListParams) {
const top = boundedTop(params.top, 10)
const select = params.select ?? MAIL_LIST_SELECT
const query: Record<string, string | number | undefined> = {
$top: top,
$select: select,
$orderby: params.orderBy ?? 'receivedDateTime desc',
$filter: params.filter,
$skip: params.skip,
}
if (params.search) query.$search = '"' + params.search.replace(/"/g, '\\"') + '"'
const data = await graphRequest<JsonObject>({
integration: resolveMicrosoftIntegration(params),
path: mailPath(params.folderId),
query,
})
return graphItems<JsonObject>(data)
}
/** Read one Outlook message. Omits body unless includeBody is true. */
export async function getMessage(params: MailGetParams) {
const id = params.id?.trim()
if (!id) throw new Error('getMessage requires id.')
const select =
params.select ??
(params.includeBody
? MAIL_LIST_SELECT + ',body,uniqueBody'
: MAIL_LIST_SELECT)
return graphRequest<JsonObject>({
integration: resolveMicrosoftIntegration(params),
path: '/me/messages/' + encodeURIComponent(id),
query: { $select: select },
})
}
/** List mail folders (Inbox, Sent Items, etc.). */
export async function listMailFolders(params: MicrosoftAccountParams & { top?: number } = {}) {
const data = await graphRequest<JsonObject>({
integration: resolveMicrosoftIntegration(params),
path: '/me/mailFolders',
query: {
$top: boundedTop(params.top, 20, 100),
$select: 'id,displayName,parentFolderId,totalItemCount,unreadItemCount,childFolderCount',
},
})
return graphItems<JsonObject>(data)
}
function sendMailBody(params: MailSendParams) {
const to = recipientList(params.to)
if (to.length === 0) throw new Error('sendMail requires at least one to address.')
const subject = params.subject?.trim()
if (!subject) throw new Error('sendMail requires subject.')
const body = params.body
if (typeof body !== 'string' || body.length === 0) throw new Error('sendMail requires body.')
return {
message: {
subject,
body: {
contentType: params.contentType === 'html' ? 'HTML' : 'Text',
content: body,
},
toRecipients: to,
ccRecipients: recipientList(params.cc),
bccRecipients: recipientList(params.bcc),
},
saveToSentItems: params.saveToSentItems !== false,
}
}
/** Preview or send an Outlook message. Sending requires confirm: true. */
export async function sendMail(params: MailSendParams) {
const payload = sendMailBody(params)
const mode = requireConfirmOrDryRun({
dryRun: params.dryRun,
confirm: params.confirm,
action: 'sendMail',
})
if (mode.dryRun) {
return { dryRun: true as const, method: 'POST', path: '/me/sendMail', body: payload }
}
await graphRequest({
integration: resolveMicrosoftIntegration(params),
method: 'POST',
path: '/me/sendMail',
body: payload,
})
return { sent: true as const, subject: payload.message.subject, to: params.to }
}
/** Preview or create a reply draft for an existing message. */
export async function createReplyDraft(params: MailReplyParams) {
const messageId = params.messageId?.trim()
if (!messageId) throw new Error('createReplyDraft requires messageId.')
const path = '/me/messages/' + encodeURIComponent(messageId) + '/createReply'
const body = params.comment ? { comment: params.comment } : {}
const mode = requireConfirmOrDryRun({
dryRun: params.dryRun,
confirm: params.confirm,
action: 'createReplyDraft',
})
if (mode.dryRun) {
return { dryRun: true as const, method: 'POST', path, body }
}
return graphRequest<JsonObject>({
integration: resolveMicrosoftIntegration(params),
method: 'POST',
path,
body,
})
}
/**
* Outlook mail helpers for Microsoft Graph.
* @example
* import mail from 'kody:@kody/microsoft/mail'
* const { items } = await mail().listMessages({ top: 5 })
*/
export default function mail() {
return { listMessages, getMessage, listMailFolders, sendMail, createReplyDraft }
}