npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@petter100/emai

v0.2.0

Published

AI-first unified email toolkit for agents — read, send, search, classify, extract, and manage email across any provider

Readme

Read, send, search, classify, extract, and manage email across any provider — with AI built in.

npm install @petter100/emai

Why emai?

AI agents need email capabilities — reading inboxes, understanding content, extracting data from attachments, composing replies, and managing messages. But today's email landscape is fragmented: Gmail API differs from Microsoft Graph differs from IMAP, and bolting on AI features means stitching together multiple libraries.

emai unifies everything into one toolkit:

| Capability | What it does | |------------|-------------| | Any provider | Gmail, Outlook, IMAP/SMTP through a single API | | AI-native | Classification, semantic search, smart compose, extraction, summarization, priority scoring | | Attachment intelligence | Parse PDFs, images (OCR + vision), Office docs, CSV, video | | Agent-ready | npm package, MCP server (37 tools), and CLI — agents can install and start using immediately | | Safety built in | PII scanning, credential detection, phishing analysis, human-in-the-loop approval | | Real-time | IMAP IDLE, webhooks, event streaming |


Table of Contents


Quick Start

import { Emai } from '@petter100/emai';

const emai = new Emai({
  provider: {
    type: 'imap',
    imap: { host: 'imap.gmail.com', port: 993, secure: true, auth: { user: '[email protected]', pass: 'app-password' } },
    smtp: { host: 'smtp.gmail.com', port: 465, secure: true, auth: { user: '[email protected]', pass: 'app-password' } },
  },
  ai: {
    adapter: 'openai',
    apiKey: process.env.OPENAI_API_KEY,
    model: 'gpt-4o',
  },
  search: { store: 'memory' },
});

await emai.connect();

// List recent emails
const { items } = await emai.emails.list({ limit: 10 });

// Classify an email
const classification = await emai.ai.classify(items[0]);
// → { category: 'support', confidence: 0.95, sentiment: 'negative', isUrgent: true }

// Semantic search
await emai.search.index(items);
const results = await emai.search.semantic('invoices from last quarter');

// Extract structured data
import { z } from 'zod';
const InvoiceSchema = z.object({
  invoiceNumber: z.string(),
  amount: z.number(),
  dueDate: z.string(),
  vendor: z.string(),
});
const extracted = await emai.ai.extract(items[0], InvoiceSchema);
// → { data: { invoiceNumber: 'INV-2026-001', amount: 1500, ... }, confidence: 0.92 }

// AI-compose a reply
const reply = await emai.ai.reply(items[0], {
  instructions: 'Thank them and confirm we received the invoice',
  tone: 'professional',
});

// Send
await emai.emails.send({
  to: '[email protected]',
  subject: reply.subject ?? 'Re: Invoice',
  text: reply.text,
});

await emai.disconnect();

Providers

Gmail API

const emai = new Emai({
  provider: {
    type: 'gmail',
    credentials: {
      clientId: process.env.GMAIL_CLIENT_ID,
      clientSecret: process.env.GMAIL_CLIENT_SECRET,
      refreshToken: process.env.GMAIL_REFRESH_TOKEN,
    },
  },
});

Requires the googleapis package: npm install googleapis

Microsoft Outlook

const emai = new Emai({
  provider: {
    type: 'outlook',
    credentials: {
      clientId: process.env.OUTLOOK_CLIENT_ID,
      clientSecret: process.env.OUTLOOK_CLIENT_SECRET,
      tenantId: process.env.OUTLOOK_TENANT_ID,     // optional, for multi-tenant
      refreshToken: process.env.OUTLOOK_REFRESH_TOKEN,
    },
  },
});

Requires: npm install @microsoft/microsoft-graph-client

IMAP/SMTP (any provider)

Works with any IMAP/SMTP-compatible email service — Gmail, Yahoo, Fastmail, self-hosted, etc.

const emai = new Emai({
  provider: {
    type: 'imap',
    imap: {
      host: 'imap.example.com',
      port: 993,
      secure: true,
      auth: { user: '[email protected]', pass: 'your-password' },
    },
    smtp: {
      host: 'smtp.example.com',
      port: 465,
      secure: true,
      auth: { user: '[email protected]', pass: 'your-password' },
    },
  },
});

OAuth2 is also supported:

auth: { user: '[email protected]', accessToken: 'oauth2-access-token' }

Requires: npm install imapflow nodemailer mailparser


AI Adapters

emai is LLM-agnostic. Pick any adapter — or bring your own:

| Adapter | Package | Default Model | Default Embedding Model | |---------|---------|---------------|------------------------| | openai | openai | gpt-4o | text-embedding-3-small | | anthropic | @anthropic-ai/sdk | claude-sonnet-4-20250514 | (native) | | google | @google/generative-ai | gemini-2.0-flash | text-embedding-004 | | ollama | ollama | llama3.1 | nomic-embed-text |

// OpenAI
ai: { adapter: 'openai', apiKey: '...', model: 'gpt-4o' }

// Anthropic
ai: { adapter: 'anthropic', apiKey: '...', model: 'claude-sonnet-4-20250514' }

// Google Gemini
ai: { adapter: 'google', apiKey: '...', model: 'gemini-2.0-flash' }

// Local via Ollama
ai: { adapter: 'ollama', model: 'llama3.1', baseUrl: 'http://localhost:11434' }

// Custom adapter (see Advanced section)
ai: { adapter: myCustomAdapter }

Full AI Configuration

ai: {
  adapter: 'openai',
  apiKey: process.env.OPENAI_API_KEY,
  model: 'gpt-4o',                    // Text/chat model
  embeddingModel: 'text-embedding-3-small',  // For semantic search
  baseUrl: 'https://your-proxy.com/v1',      // Custom endpoint
  temperature: 0.3,                    // 0-1, default 0.3
  maxTokens: 4096,                     // Default 4096
}

AI Features

Classification

Categorize emails into 15 categories with sentiment analysis and urgency detection.

const result = await emai.ai.classify(email);

Returns:

{
  category: 'support',           // 15 categories (see below)
  confidence: 0.95,              // 0-1
  sentiment: 'negative',         // 'positive' | 'negative' | 'neutral'
  isUrgent: true,
  isActionRequired: true,
  labels: ['customer-issue', 'billing'],
  reasoning: 'Customer reports billing discrepancy...'
}

Categories: primary, social, promotions, updates, forums, spam, phishing, support, sales, billing, newsletter, notification, personal, work, other

Batch classification:

const results = await emai.ai.classifyBatch(emails);
// → ClassificationResult[] — one per email

Summarization

Generate concise summaries with key points, action items, and sentiment.

const summary = await emai.ai.summarize(email);

Returns:

{
  summary: 'John requested the Q1 revenue report by Friday...',
  keyPoints: ['Q1 report needed', 'Deadline is Friday', 'Include regional breakdown'],
  participants: [{ name: 'John', address: '[email protected]' }],
  actionItems: [
    { description: 'Send Q1 report', assignee: 'you', dueDate: '2026-03-07', priority: 'high', status: 'pending' }
  ],
  sentiment: 'neutral',
  topicTags: ['reporting', 'finance', 'deadline']
}

Thread summarization:

const threadSummary = await emai.ai.summarizeThread(thread);
// Summarizes the entire conversation across all messages

const batchSummary = await emai.ai.summarizeBatch(emails);
// Digest-style summary of multiple emails

Structured Data Extraction

Extract typed, validated data from emails using Zod schemas.

import { z } from 'zod';

const OrderSchema = z.object({
  orderId: z.string(),
  items: z.array(z.object({
    name: z.string(),
    quantity: z.number(),
    price: z.number(),
  })),
  total: z.number(),
  shippingAddress: z.string(),
});

const { data, confidence, sources } = await emai.ai.extract(email, OrderSchema);
// data is fully typed as z.infer<typeof OrderSchema>

Returns:

{
  data: {
    orderId: 'ORD-2026-4521',
    items: [{ name: 'Widget Pro', quantity: 2, price: 49.99 }],
    total: 99.98,
    shippingAddress: '123 Main St, San Francisco, CA 94102'
  },
  confidence: 0.92,
  sources: [
    { field: 'orderId', source: 'body', span: 'Order #ORD-2026-4521' },
    { field: 'total', source: 'body', span: 'Total: $99.98' }
  ]
}

Smart Compose & Reply

AI-powered email composition with tone and length control.

// Compose a new email
const draft = await emai.ai.compose({
  context: 'Schedule a meeting to discuss Q1 results',
  tone: 'professional',
  length: 'short',
  language: 'en',
});
// → { subject: 'Q1 Results Discussion', text: '...', html: '...' }

// Reply to an email
const reply = await emai.ai.reply(email, {
  instructions: 'Decline politely, suggest next week instead',
  tone: 'friendly',
});
// → { subject: 'Re: Meeting Request', text: '...', html: '...' }

Tone options: professional, casual, friendly, formal, empathetic

Length options: short, medium, long

Tone rewriting & writing improvement:

const rewritten = await emai.ai.rewriteInTone(text, 'professional');
const improved = await emai.ai.improveWriting(text);

Priority Scoring

Score email importance on a 0-100 scale with contextual reasoning.

const priority = await emai.ai.prioritize(email, {
  userEmail: '[email protected]',
  vipList: ['[email protected]', '[email protected]'],
});

Returns:

{
  score: 85,                           // 0-100
  level: 'high',                       // 'critical' | 'high' | 'medium' | 'low' | 'none'
  reasoning: 'From VIP sender, contains deadline, requires action',
  suggestedResponseTime: '2 hours'     // 'immediate' | 'within 1 hour' | 'today' | 'this week' | 'when convenient'
}

Batch prioritization:

const results = await emai.ai.prioritizeBatch(emails, { userEmail: '[email protected]' });
// → Array<{ email: Email, priority: PriorityResult }>

Action Item Detection

Extract tasks, deadlines, requests, and follow-ups from emails.

const actions = await emai.ai.detectActions(email);

Returns:

[
  {
    description: 'Send Q1 report to finance team',
    assignee: 'you',
    dueDate: '2026-03-07',
    priority: 'high',
    status: 'pending'
  },
  {
    description: 'Schedule follow-up meeting',
    assignee: '[email protected]',
    dueDate: null,
    priority: 'medium',
    status: 'pending'
  }
]

Thread-level action detection:

const threadActions = await emai.ai.detectActionsInThread(thread);
// Detects actions across the entire conversation, deduplicating completed items

Email Operations

Reading Emails

// List emails with filtering
const { items, total, hasMore, nextCursor } = await emai.emails.list({
  folder: 'inbox',
  limit: 20,
  offset: 0,
  isRead: false,           // Unread only
  isStarred: true,         // Starred only
  hasAttachment: true,     // With attachments
  from: '[email protected]',
  subject: 'invoice',
  after: new Date('2026-01-01'),
  before: new Date('2026-03-01'),
  sortBy: 'date',
  sortOrder: 'desc',
});

// Pagination with cursor
const page2 = await emai.emails.list({ cursor: nextCursor, limit: 20 });

// Get a single email
const email = await emai.emails.get('email-id');

Email object shape:

interface Email {
  id: string;
  threadId?: string;
  provider: string;
  from: { name?: string; address: string };
  to: Array<{ name?: string; address: string }>;
  cc: Array<{ name?: string; address: string }>;
  bcc: Array<{ name?: string; address: string }>;
  subject: string;
  body: { text?: string; html?: string; markdown?: string };
  attachments: Attachment[];
  labels: string[];
  folder: string;
  date: Date;
  receivedDate: Date;
  isRead: boolean;
  isStarred: boolean;
  isDraft: boolean;
  snippet?: string;
  headers: Record<string, string>;
}

Sending Emails

const result = await emai.emails.send({
  to: '[email protected]',                    // string, array, or EmailAddress
  cc: ['[email protected]', '[email protected]'],
  bcc: '[email protected]',
  subject: 'Hello from emai',
  text: 'Plain text body',
  html: '<h1>HTML body</h1>',
  attachments: [
    { filename: 'report.pdf', content: pdfBuffer, contentType: 'application/pdf' }
  ],
  replyTo: '[email protected]',
  headers: { 'X-Custom-Header': 'value' },
});
// → { id: '...', threadId: '...', messageId: '<[email protected]>' }

Reply and forward:

// Reply
await emai.emails.reply('email-id', {
  text: 'Thanks for the update!',
  replyAll: true,          // Reply to all recipients
});

// Forward
await emai.emails.forward('email-id', {
  to: '[email protected]',
  text: 'FYI — see below.',
});

Drafts

const draft = await emai.emails.createDraft({
  to: '[email protected]',
  subject: 'Draft email',
  text: 'Work in progress...',
});

await emai.emails.updateDraft(draft.id, {
  text: 'Updated content',
});

await emai.emails.deleteDraft(draft.id);

Managing Emails

await emai.emails.markAsRead('email-id');
await emai.emails.markAsUnread('email-id');
await emai.emails.star('email-id');
await emai.emails.unstar('email-id');
await emai.emails.moveToFolder('email-id', 'archive');
await emai.emails.archive('email-id');
await emai.emails.delete('email-id');

Labels & Folders

// Labels
const labels = await emai.labels.list();
await emai.labels.add('email-id', 'important');
await emai.labels.remove('email-id', 'important');
await emai.labels.create('my-label', '#ff0000');   // with optional color
await emai.labels.delete('label-id');

// Folders
const folders = await emai.folders.list();
// → [{ id, name, path, type, unreadCount, totalCount, children }]
await emai.folders.create('Projects', parentId);
await emai.folders.delete('folder-id');

Threading

Automatic conversation thread detection using headers, subjects, and participant analysis.

// Get a thread by ID
const thread = await emai.threads.get('thread-id');
// → { id, subject, emails: [...], participants, lastDate, messageCount, labels }

// Detect threads from a list of emails
const threads = emai.threads.detect(emails);
// Groups emails into threads using:
// 1. In-Reply-To / References headers
// 2. Subject normalization (strips Re:/Fw:)
// 3. Participant overlap analysis

Search

Semantic Search

Find emails by meaning, not just keywords. Requires an AI adapter with embedding support.

// First, index your emails
await emai.search.index(emails);

// Then search by meaning
const results = await emai.search.semantic('complaints about shipping delays');
// Finds relevant emails even if they don't contain those exact words

Full-Text Search

Keyword search with operator support. Works without an AI adapter.

const results = await emai.search.fullText('from:john subject:invoice has:attachment');

Operators: from:, to:, subject:, has:attachment, is:unread, is:starred, after:, before:, label:

Hybrid Search

Combine semantic understanding with keyword precision:

const results = await emai.search.hybrid('quarterly revenue report', {
  alpha: 0.7,     // 1.0 = pure semantic, 0.0 = pure full-text
  limit: 10,
  minScore: 0.5,
});

Search Options

All search methods accept:

{
  limit?: number,        // Default: 10
  offset?: number,
  folder?: string,       // Filter by folder
  label?: string,        // Filter by label
  from?: string,         // Filter by sender
  after?: Date,          // After date
  before?: Date,         // Before date
  minScore?: number,     // Minimum relevance score
}

Vector Stores

Choose where to store search vectors:

| Store | Package | Best For | |-------|---------|----------| | memory | (built-in) | Development, small mailboxes | | sqlite | better-sqlite3 | Local persistent storage | | pgvector | pg | Production with PostgreSQL | | pinecone | @pinecone-database/pinecone | Managed cloud vector DB | | weaviate | weaviate-client | Hybrid search at scale | | chromadb | chromadb | Local/self-hosted vector DB |

// Memory (default, no persistence)
search: { store: 'memory' }

// SQLite (local file)
search: { store: 'sqlite', path: './emails.db' }

// PostgreSQL + pgvector
search: { store: 'pgvector', connectionString: 'postgresql://user:pass@localhost/emails' }

// Pinecone
search: { store: 'pinecone', apiKey: '...', indexName: 'emails', environment: 'us-east-1-aws' }

// Weaviate
search: { store: 'weaviate', url: 'http://localhost:8080', collectionName: 'emails' }

// ChromaDB
search: { store: 'chromadb', url: 'http://localhost:8000', collectionName: 'emails' }

Attachments

Parse any attachment format with configurable depth:

// Auto-detect and parse
const parsed = await emai.attachments.parse(attachment, { depth: 'deep' });
// → { text, markdown, metadata, pages, tables, images, structuredData }

// Get plain text from any attachment
const text = await emai.attachments.toText(attachment);

// OCR scanned documents and images
const ocrText = await emai.attachments.ocr(attachment);

// Vision AI description
const description = await emai.attachments.describe(attachment);

// Structured data extraction from attachments
const data = await emai.attachments.extract(attachment, InvoiceSchema);

// Download attachment content
const content = await emai.attachments.getContent('email-id', 'attachment-id');

Supported Formats

| Format | Extensions | Required Package | |--------|-----------|-----------------| | PDF | .pdf | pdf-parse | | Images | .jpg, .png, .gif, .webp | sharp (processing), tesseract.js (OCR) | | Word | .docx | mammoth | | Excel | .xlsx | (built-in) | | PowerPoint | .pptx | (built-in) | | CSV | .csv | papaparse | | Plain text | .txt, .md, .html | (built-in) |

Parse Depth Levels

| Depth | What It Does | |-------|-------------| | basic | Metadata and raw text extraction | | medium | Full text, tables, markdown conversion | | deep | OCR, vision AI analysis, image descriptions, table extraction |


Safety

Built-in security scanning for outbound emails:

const emai = new Emai({
  // ...provider, ai config...
  safety: {
    piiScanning: true,               // Detect PII (SSNs, credit cards, etc.)
    credentialScanning: true,         // Detect API keys, passwords, tokens
    humanApproval: 'high-risk',      // 'all' | 'high-risk' | 'none'
    blockedDomains: ['competitor.com'],
    allowedDomains: ['company.com'],
    maxRecipientsPerEmail: 50,
    onApprovalRequired: async (email, risks) => {
      console.log('Risks detected:', risks);
      return confirm('Send anyway?');
    },
  },
});

Automatic Scanning

When safety is configured, all outbound emails are automatically scanned:

await emai.emails.send({ to: '...', subject: '...', text: '...' });
// → Scans for PII, credentials, blocked domains before sending
// → If risks detected, triggers onApprovalRequired callback

Manual Scanning

const result = emai.safety.scan(email);
// → {
//   safe: false,
//   risks: [
//     { type: 'pii', severity: 'high', description: 'SSN detected', matched: '***-**-1234' }
//   ],
//   blocked: false,
//   requiresApproval: true
// }

What It Detects

| Type | Examples | |------|---------| | PII | Email addresses, phone numbers, SSNs, credit card numbers, dates of birth | | Credentials | API keys, passwords, private keys, JWT tokens, connection strings, AWS keys | | Phishing | Suspicious URLs, spoofed sender addresses, urgent payment requests | | Policy | Blocked domains, excessive recipients, custom policy violations |


Events & Real-Time

Events

Subscribe to email lifecycle events:

// Listen for specific events
emai.on('email:received', (email) => {
  console.log(`New email from ${email.from.address}: ${email.subject}`);
});

emai.on('email:sent', (result) => {
  console.log(`Email sent: ${result.id}`);
});

emai.on('safety:risk', ({ result }) => {
  console.log(`Risks detected:`, result.risks);
});

emai.on('error', (err) => {
  console.error('Error:', err.message);
});

// One-time listener
emai.once('email:received', (email) => { /* ... */ });

// Remove listener
const unsubscribe = emai.on('email:received', handler);
unsubscribe();  // or: emai.off('email:received', handler);

All events:

| Event | Payload | Description | |-------|---------|-------------| | email:received | Email | New email arrived | | email:sent | SendResult | Email sent successfully | | email:read | { emailId } | Email marked as read | | email:deleted | { emailId } | Email deleted | | email:moved | { emailId, folder } | Email moved to folder | | email:labeled | { emailId, label, action } | Label added/removed | | email:classified | { emailId, result } | Email classified by AI | | email:indexed | { emailId } | Email added to search index | | safety:risk | { emailId?, result } | Security risk detected | | safety:blocked | { emailId?, risks } | Email blocked by safety | | safety:approved | { emailId? } | Email approved after risk review | | watch:started | undefined | Watch started | | watch:stopped | undefined | Watch stopped | | watch:error | Error | Watch error occurred | | error | Error | General error |

Watch

Monitor for new emails in real-time:

// Start watching (uses IMAP IDLE when available, falls back to polling)
await emai.watch.start({
  folder: 'inbox',
  pollInterval: 30000,     // Poll every 30s (used when IDLE not available)
});

// Check status
emai.watch.isWatching();   // → true

// Stop watching
await emai.watch.stop();

Webhooks

Forward events to external URLs:

const webhookId = emai.webhooks.register(
  'https://your-app.com/webhook',
  ['email:received', 'email:sent'],
  {
    secret: 'your-webhook-secret',
    headers: { 'X-Custom': 'value' },
    retries: 3,
    retryDelay: 1000,
    timeout: 10000,
  }
);

// List registered webhooks
const hooks = emai.webhooks.list();

// Remove a webhook
emai.webhooks.unregister(webhookId);

MCP Server

Expose emai as an MCP server so AI agents (Claude, etc.) can use email tools directly:

npx emai mcp

Or programmatically:

import { startEmaiMcpServer } from '@petter100/emai/mcp';

await startEmaiMcpServer({
  provider: { type: 'imap', /* ... */ },
  ai: { adapter: 'openai', apiKey: '...' },
});

Available MCP Tools (37)

| Category | Tools | |----------|-------| | Email Reading | list_emails, get_email, get_thread, get_attachment | | Email Sending | send_email, reply_to_email, forward_email, create_draft, update_draft, delete_draft | | Email Management | mark_as_read, mark_as_unread, star_email, unstar_email, move_to_folder, delete_email, archive_email | | Labels & Folders | list_labels, add_label, remove_label, create_label, delete_label, list_folders, create_folder | | Search | search_semantic, search_fulltext, search_hybrid, index_emails | | AI | classify_email, summarize_email, summarize_thread, extract_data, compose_email, compose_reply, prioritize_emails, detect_actions | | Attachments | parse_attachment, ocr_attachment, describe_attachment | | Safety | scan_email | | Monitoring | start_watching, stop_watching |


CLI

emai includes a CLI for quick email operations:

npx emai init                          # Interactive setup — creates .emairc config

npx emai list                          # List recent emails
npx emai list --limit 20 --unread      # Filter options
npx emai read <id>                     # Read email content

npx emai send --to [email protected] --subject "Hello" --body "Hi there"
npx emai reply <id>                    # Reply to an email
npx emai forward <id> --to [email protected]

npx emai search "invoices" --type semantic
npx emai search "from:john" --type fulltext

npx emai classify <id>                 # AI classification
npx emai summarize <id>                # AI summary

npx emai star <id>                     # Star/unstar
npx emai delete <id>                   # Delete
npx emai move <id> --folder archive    # Move to folder

npx emai watch                         # Watch for new emails (real-time)
npx emai mcp                           # Start MCP server

npx emai <command> --json              # JSON output for any command
npx emai <command> --quiet             # Suppress info messages

Playground

emai ships with an interactive Playground — a web UI for testing every SDK feature against your real mailbox.

Getting Started

cd playground
npm install
npm run dev -- -p 4000

Open http://localhost:4000 and enter your email provider credentials + AI adapter config.

Features

The Playground includes:

  • Dashboard — One-click "Run All Tests" that exercises every SDK capability: email listing, classification, summarization, priority scoring, action detection, composition, extraction, search, and safety scanning
  • Email Browser — Gmail-like UI with AI indicators (category, priority, urgency badges)
  • AI Playground — Test individual AI features with custom parameters
  • Attachment Viewer — Parse, OCR, and extract data from attachments with configurable extraction fields
  • Search Console — Test semantic, full-text, and hybrid search with index management
  • Event Stream — Real-time SSE viewer showing all SDK events as they happen

Advanced

Custom LLM Adapter

Implement the LLMAdapter interface to use any LLM:

import type { LLMAdapter, CompletionOptions } from '@petter100/emai';
import { z } from 'zod';

const myAdapter: LLMAdapter = {
  name: 'my-llm',

  async complete(prompt: string, options?: CompletionOptions): Promise<string> {
    // Call your LLM and return the text response
    const response = await myLlm.chat(prompt, options);
    return response.text;
  },

  async completeJSON<T>(prompt: string, schema: z.ZodType<T>, options?: CompletionOptions): Promise<T> {
    // Call your LLM and return parsed + validated JSON
    const response = await myLlm.chat(prompt, { ...options, responseFormat: 'json' });
    return schema.parse(JSON.parse(response.text));
  },

  async embed(texts: string[]): Promise<number[][]> {
    // Generate embeddings for semantic search
    return myEmbeddingModel.embed(texts);
  },

  async vision(images: Array<{ data: Buffer; mimeType: string }>, prompt: string): Promise<string> {
    // Optional: describe images for attachment analysis
    return myVisionModel.describe(images, prompt);
  },
};

const emai = new Emai({
  provider: { /* ... */ },
  ai: { adapter: myAdapter },
});

Custom Vector Store

Implement the VectorStore interface for any vector database:

import type { VectorStore, VectorEntry, VectorSearchResult } from '@petter100/emai';

const myStore: VectorStore = {
  name: 'my-store',

  async initialize(dimensions: number): Promise<void> {
    // Create collection/index with the given vector dimensions
  },

  async upsert(vectors: VectorEntry[]): Promise<void> {
    // Insert or update vectors
    // Each entry: { id, vector: number[], metadata: Record<string, unknown>, content: string }
  },

  async search(vector: number[], limit: number, filter?: Record<string, unknown>): Promise<VectorSearchResult[]> {
    // Find nearest neighbors
    // Return: { id, score, metadata, content }[]
  },

  async delete(ids: string[]): Promise<void> {
    // Remove vectors by ID
  },

  async count(): Promise<number> {
    // Return total number of stored vectors
  },

  async close(): Promise<void> {
    // Cleanup connections
  },
};

const emai = new Emai({
  provider: { /* ... */ },
  search: { store: myStore },
});

Storage

Persist email data locally for offline access:

// In-memory (default, no persistence)
storage: { type: 'memory' }

// SQLite (persistent)
storage: { type: 'sqlite', path: './emai-storage.db' }

API Reference

Constructor

new Emai(config: EmaiConfig)

| Property | Type | Required | Description | |----------|------|----------|-------------| | provider | ProviderConfig | Yes | Email provider configuration | | ai | AiConfig | No | LLM adapter configuration (required for AI features) | | search | SearchConfig | No | Vector store configuration (required for semantic search) | | storage | StorageConfig | No | Local persistence configuration | | safety | SafetyConfig | No | Security scanning configuration |

Connection

| Method | Returns | Description | |--------|---------|-------------| | connect() | Promise<void> | Establish provider connection | | disconnect() | Promise<void> | Close all connections | | isConnected() | boolean | Check connection status |

Emails (emai.emails.*)

| Method | Returns | Description | |--------|---------|-------------| | list(options?) | Promise<ListResult<Email>> | List emails with filtering | | get(id) | Promise<Email> | Get a single email | | send(options) | Promise<SendResult> | Send an email | | reply(emailId, options) | Promise<SendResult> | Reply to an email | | forward(emailId, options) | Promise<SendResult> | Forward an email | | createDraft(options) | Promise<Email> | Create a draft | | updateDraft(draftId, options) | Promise<Email> | Update a draft | | deleteDraft(draftId) | Promise<void> | Delete a draft | | markAsRead(emailId) | Promise<void> | Mark as read | | markAsUnread(emailId) | Promise<void> | Mark as unread | | star(emailId) | Promise<void> | Star an email | | unstar(emailId) | Promise<void> | Unstar an email | | moveToFolder(emailId, folder) | Promise<void> | Move to folder | | archive(emailId) | Promise<void> | Archive an email | | delete(emailId) | Promise<void> | Delete an email |

AI (emai.ai.*)

| Method | Returns | Description | |--------|---------|-------------| | classify(email) | Promise<ClassificationResult> | Classify a single email | | classifyBatch(emails) | Promise<ClassificationResult[]> | Classify multiple emails | | summarize(email) | Promise<SummaryResult> | Summarize a single email | | summarizeThread(thread) | Promise<SummaryResult> | Summarize a thread | | summarizeBatch(emails) | Promise<string> | Digest summary of multiple emails | | extract(email, schema) | Promise<ExtractionResult<T>> | Extract structured data | | compose(options) | Promise<ComposeResult> | Compose a new email | | reply(email, options) | Promise<ComposeResult> | Compose a reply | | rewriteInTone(text, tone) | Promise<string> | Rewrite text in a given tone | | improveWriting(text) | Promise<string> | Improve writing quality | | prioritize(email, context?) | Promise<PriorityResult> | Score email priority | | prioritizeBatch(emails, context?) | Promise<Array<{ email, priority }>> | Batch prioritize | | detectActions(email) | Promise<ActionItem[]> | Detect action items | | detectActionsInThread(thread) | Promise<ActionItem[]> | Detect actions in thread |

Search (emai.search.*)

| Method | Returns | Description | |--------|---------|-------------| | semantic(query, options?) | Promise<SearchResult[]> | Semantic search | | fullText(query, options?) | Promise<SearchResult[]> | Full-text keyword search | | hybrid(query, options?) | Promise<SearchResult[]> | Combined search | | index(emails) | Promise<void> | Index emails for search | | indexEmail(email) | Promise<void> | Index a single email | | removeFromIndex(emailId) | Promise<void> | Remove from index | | getIndexedCount() | Promise<number> | Get indexed email count |

Attachments (emai.attachments.*)

| Method | Returns | Description | |--------|---------|-------------| | parse(attachment, options?) | Promise<ParsedAttachment> | Parse any attachment | | toText(attachment, options?) | Promise<string> | Extract plain text | | extract(attachment, schema) | Promise<ExtractionResult<T>> | Extract structured data | | describe(attachment) | Promise<string> | Vision AI description | | ocr(attachment) | Promise<string> | OCR text extraction | | getContent(emailId, attachmentId) | Promise<Buffer> | Download content |

Safety (emai.safety.*)

| Method | Returns | Description | |--------|---------|-------------| | scan(email) | ScanResult | Scan an email for risks | | scanOutbound(options) | Promise<ScanResult> | Scan before sending | | checkBeforeSend(options) | Promise<{ allowed, result }> | Check + approval flow |

Labels & Folders

| Method | Returns | Description | |--------|---------|-------------| | labels.list() | Promise<Label[]> | List all labels | | labels.add(emailId, label) | Promise<void> | Add label to email | | labels.remove(emailId, label) | Promise<void> | Remove label from email | | labels.create(name, color?) | Promise<Label> | Create a label | | labels.delete(labelId) | Promise<void> | Delete a label | | folders.list() | Promise<Folder[]> | List all folders | | folders.create(name, parentId?) | Promise<Folder> | Create a folder | | folders.delete(folderId) | Promise<void> | Delete a folder |

Threads (emai.threads.*)

| Method | Returns | Description | |--------|---------|-------------| | get(threadId) | Promise<Thread> | Get a thread | | detect(emails) | Thread[] | Detect threads from emails |

Watch & Webhooks

| Method | Returns | Description | |--------|---------|-------------| | watch.start(options?) | Promise<void> | Start watching for emails | | watch.stop() | Promise<void> | Stop watching | | watch.isWatching() | boolean | Check watch status | | webhooks.register(url, events, options?) | string | Register webhook | | webhooks.unregister(webhookId) | void | Remove webhook | | webhooks.list() | WebhookRegistration[] | List webhooks |

Events

| Method | Returns | Description | |--------|---------|-------------| | on(event, listener) | () => void | Subscribe (returns unsubscribe fn) | | once(event, listener) | () => void | One-time subscription | | off(event, listener) | void | Unsubscribe |


Optional Dependencies

emai keeps its core dependency-free. Install only what you need:

# AI adapters (pick one)
npm install openai                      # OpenAI
npm install @anthropic-ai/sdk           # Anthropic
npm install @google/generative-ai       # Google Gemini
npm install ollama                      # Ollama (local)

# Email providers
npm install googleapis                  # Gmail API
npm install @microsoft/microsoft-graph-client  # Outlook
npm install imapflow nodemailer mailparser     # IMAP/SMTP

# Vector stores (pick one)
npm install better-sqlite3              # SQLite
npm install pg                          # PostgreSQL + pgvector
npm install @pinecone-database/pinecone # Pinecone
npm install weaviate-client             # Weaviate
npm install chromadb                    # ChromaDB

# Attachment processing
npm install pdf-parse                   # PDF parsing
npm install tesseract.js                # OCR
npm install sharp                       # Image processing
npm install mammoth                     # Word documents
npm install papaparse                   # CSV parsing

# Other
npm install @modelcontextprotocol/sdk   # MCP server
npm install commander                   # CLI

License

MIT