Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/microsoft

src/mail.ts

179 lines · 5.6 KB · TypeScript
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 }
}