@ordinatio/email
v1.2.0
Published
Multi-provider email engine with OAEM protocol
Readme
@ordinatio/email
A standalone, multi-provider email engine with OAuth + IMAP/SMTP support, auto-discovery, templates, scheduled delivery, and OAEM protocol integration.
Built as a Canonical module (C-03) under the Ordinatio architecture. Zero app-layer dependencies — all side effects (activity logging, events, contact resolution) injected via callbacks.
Table of Contents
- Architecture
- Quick Start
- Providers
- Auto-Discovery
- Email Operations
- Templates
- Scheduled Emails
- OAEM Protocol Integration
- Callback Injection Pattern
- Error System
- Validation Schemas
- Testing
- API Reference
Architecture
┌──────────────────────────────────────────────────┐
│ @ordinatio/email │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │
│ │ Providers │ │ Services │ │ Discovery │ │
│ │ │ │ │ │ │ │
│ │ Gmail │ │ Account │ │ MX resolver │ │
│ │ Outlook │ │ Messages │ │ Autoconfig │ │
│ │ IMAP │ │ Sync │ │ SRV records │ │
│ │ │ │ Archive │ │ Port prober │ │
│ │ │ │ Content │ │ Intelligence │ │
│ │ │ │ Template │ │ │ │
│ │ │ │ Schedule │ │ │ │
│ └─────┬─────┘ └─────┬────┘ └───────┬───────┘ │
│ │ │ │ │
│ ┌─────┴──────────────┴───────────────┴───────┐ │
│ │ types / errors / schemas │ │
│ └─────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘
↕ PrismaClient ↕ Callbacks
(database injected) (side effects injected)Key design principles:
- Provider-agnostic — Gmail, Outlook, and any IMAP/SMTP server through a unified interface
- Callback injection — No imports from the host application; activity logging, event emission, contact resolution, and OAEM protocol support are injected at runtime
- Database-injected — Accepts a
PrismaClientinstance; works with any tenant database - Error-coded — 179 error codes with timestamped references, severity levels, and diagnosis steps (Rule 8)
- Schema-validated — 11 Zod schemas for all inputs
Quick Start
# Install (within pnpm workspace)
pnpm add @ordinatio/email
# Or reference as workspace dependency
# package.json: "@ordinatio/email": "workspace:*"import {
getProvider,
getActiveAccount,
syncEmails,
getInboxEmails,
replyToEmail,
discoverProvider,
} from '@ordinatio/email';
// Get the active email provider
const account = await getActiveAccount(db);
const provider = getProvider(account.provider.toLowerCase());
// Sync emails from provider
await syncEmails(db, {
onActivity: (action, desc, data) => console.log(action, desc),
onEmailSynced: async (email) => { /* extract context */ },
});
// Read inbox
const { emails, total } = await getInboxEmails(db, {
status: 'INBOX',
maxResults: 50,
});
// Auto-discover provider settings for a new email address
const discovery = await discoverProvider('[email protected]');
// → { domain: 'acmecorp.com', providers: [{ type: 'imap', settings: {...} }] }Providers
Three built-in providers, plus a registry for custom providers.
Gmail (OAuth)
import { GmailProvider } from '@ordinatio/email';
const gmail = new GmailProvider();
gmail.authType; // 'oauth'
gmail.getCapabilities(); // { supportsLabels: true, supportsNativeThreading: true, ... }
// OAuth flow
const authUrl = gmail.getAuthUrl('state-token');
const tokens = await gmail.exchangeCodeForTokens(code);
// Token refresh — use getValidAccessToken(db, accountId) instead of calling this directly.
// It handles the 5-minute buffer, token rotation capture, and TokenRevokedError.
const refreshed = await gmail.refreshAccessToken(tokens.refreshToken);
// Operations
const { messages, nextCursor } = await gmail.listMessages(tokens.accessToken, {
maxResults: 50,
after: new Date('2026-01-01'),
});
const full = await gmail.getMessage(tokens.accessToken, messageId);
await gmail.archiveMessage(tokens.accessToken, messageId);
await gmail.sendMessage(tokens.accessToken, { to, subject, bodyHtml });Environment variables:
GOOGLE_CLIENT_ID=xxx
GOOGLE_CLIENT_SECRET=xxx
GOOGLE_REDIRECT_URI=http://localhost:3000/api/email/callback/gmailOutlook (OAuth)
import { OutlookProvider } from '@ordinatio/email';
const outlook = new OutlookProvider();
outlook.authType; // 'oauth'
outlook.getCapabilities(); // { supportsFolders: true, maxAttachmentSize: 157286400, ... }
// OAuth flow (Microsoft MSAL)
const authUrl = outlook.getAuthUrl('state-token');
const tokens = await outlook.exchangeCodeForTokens(code);
// Operations use Microsoft Graph API
const { messages } = await outlook.listMessages(tokens.accessToken);
await outlook.archiveMessage(tokens.accessToken, messageId); // Moves to Archive folderEnvironment variables:
MICROSOFT_CLIENT_ID=xxx
MICROSOFT_CLIENT_SECRET=xxx
MICROSOFT_REDIRECT_URI=http://localhost:3000/api/email/callback/outlookIMAP/SMTP (Credentials)
Works with any email provider that supports IMAP and SMTP.
import { ImapSmtpProvider } from '@ordinatio/email';
import type { ImapSmtpCredentials } from '@ordinatio/email';
const imap = new ImapSmtpProvider();
imap.authType; // 'credentials'
// Test connection before saving
const credentials: ImapSmtpCredentials = {
imapHost: 'imap.fastmail.com',
imapPort: 993,
imapSecurity: 'ssl',
smtpHost: 'smtp.fastmail.com',
smtpPort: 465,
smtpSecurity: 'ssl',
username: '[email protected]',
password: 'app-password-here',
};
const test = await imap.testConnection(credentials);
// → { success: true, imapConnected: true, smtpConnected: true, folderCount: 12, messageCount: 1847 }
// Operations
const { messages } = await imap.listMessages(JSON.stringify(credentials));Custom Providers
Register your own provider implementation:
import { registerProvider } from '@ordinatio/email';
import type { EmailProvider } from '@ordinatio/email';
class ProtonMailProvider implements EmailProvider {
readonly providerId = 'protonmail';
readonly authType = 'credentials' as const;
// ... implement required methods
}
registerProvider('protonmail', () => new ProtonMailProvider());Provider Capabilities
Each provider declares its capabilities:
interface ProviderCapabilities {
supportsOAuth: boolean;
supportsPushNotifications: boolean;
supportsNativeArchive: boolean;
supportsNativeThreading: boolean;
supportsServerSearch: boolean;
supportsLabels: boolean; // Gmail-specific
supportsFolders: boolean; // IMAP-specific
archiveAction: 'label_remove' | 'move_folder' | 'flag';
maxAttachmentSize: number; // bytes
}| Capability | Gmail | Outlook | IMAP/SMTP |
|-----------|-------|---------|-----------|
| OAuth | Yes | Yes | No |
| Push notifications | Yes | Yes (webhooks) | No |
| Native threading | Yes (threadId) | Yes (conversationId) | No |
| Native archive | Yes (remove INBOX label) | Yes (move to Archive folder) | Flag-based |
| Server search | Yes (full-text) | Yes (OData filter) | IMAP SEARCH |
| Max attachment | 25 MB | 150 MB | Varies |
Auto-Discovery
Automatically detect email provider settings from an email address.
import { discoverProvider } from '@ordinatio/email';
const result = await discoverProvider('[email protected]', {
// Optional: query your own intelligence database
queryIntelligence: async (domain) => {
return db.emailProviderDiscovery.findFirst({ where: { domain } });
},
// Optional: record successful connections for future users
recordIntelligence: async (domain, settings) => {
await db.emailProviderDiscovery.upsert({ ... });
},
});
console.log(result);
// {
// domain: 'acmecorp.com',
// providers: [{
// type: 'imap',
// displayName: 'Acme Corp Mail',
// authMethod: 'password',
// settings: { imapHost: 'imap.acmecorp.com', imapPort: 993, ... },
// confidence: 0.9
// }],
// source: 'mozilla_autoconfig',
// durationMs: 340
// }Discovery Pipeline
Strategies are tried in order, stopping on first success:
| Priority | Strategy | Speed | Confidence |
|----------|----------|-------|------------|
| 1 | Provider Intelligence — learned from past connections | Instant | Very high |
| 2 | Known Provider Match — MX records (*google* = Gmail) | Fast | High |
| 3 | Mozilla Autoconfig — autoconfig.{domain}/mail/config-v1.1.xml | Medium | High |
| 4 | RFC 6186 SRV Records — _imaps._tcp.{domain} | Medium | Medium |
| 5 | Port Probing — TCP/TLS on ports 993, 143, 587, 465 | Slow | Low |
Provider Intelligence is cross-tenant shared — when one user connects to @acmecorp.com, all future users on that domain get instant detection.
Email Operations
Account Management
import { getActiveAccount, connectAccount, disconnectAccount, getValidAccessToken } from '@ordinatio/email';
// Get the currently active account
const account = await getActiveAccount(db);
// → { id, email, provider, isActive, lastSyncAt, ... }
// Connect a new account (OAuth providers)
const { id, email } = await connectAccount(db, 'gmail', code, '[email protected]', callbacks);
// Refresh an expired OAuth token
const freshToken = await getValidAccessToken(db, accountId);
// Disconnect and clean up
await disconnectAccount(db, accountId, callbacks);Inbox & Messages
import { getInboxEmails, getInboxThreads, getEmail, getClientEmails } from '@ordinatio/email';
// List inbox messages (paginated)
const { emails, total } = await getInboxEmails(db, {
status: 'INBOX', // or 'ARCHIVED'
maxResults: 50,
cursor: 'last-id', // cursor-based pagination
search: 'invoice', // full-text search
});
// Get threaded conversations
const threads = await getInboxThreads(db, { maxResults: 20 });
// Get all messages in a thread (for Gmail-style conversation view)
const threadMessages = await getThreadMessages(db, threadId);
// → messages ordered oldest-to-newest, with body content auto-fetched
// Get a single email with full body
const email = await getEmail(db, emailId);
// Get all emails linked to a client
const clientEmails = await getClientEmails(db, clientId);Sync
import { syncEmails, logSyncFailure } from '@ordinatio/email';
// Sync emails from all active accounts
await syncEmails(db, {
onActivity: (action, desc, data) => { /* log to activity feed */ },
onEvent: (event) => { /* emit automation trigger */ },
resolveContact: async (email, name) => { /* find or create contact */ },
onEmailSynced: async (emailData) => { /* extract context for LLM */ },
sanitizeHtml: (html) => { /* strip scripts */ },
oaem: {
parseCapsule: async (ctx) => { /* extract OAEM capsule */ },
onCapsuleVerified: async (result) => { /* persist to ledger */ },
},
});
// Log sync failures with error codes
await logSyncFailure(error, db);Archive
import { archiveEmail } from '@ordinatio/email';
await archiveEmail(db, emailId, userId, callbacks);
// Provider-aware: removes INBOX label (Gmail), moves to Archive folder (Outlook), flags (IMAP)
// Thread-aware: archives ALL messages in the thread, not just the selected oneOptimistic Archive Pattern
For instant UI feedback, navigate away immediately and archive in the background:
// 1. Navigate immediately — the user sees instant response
router.push('/inbox?archived=' + emailId);
// 2. Fire-and-forget: archive on server in background
fetch(`/api/email/messages/${emailId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'archive' }),
}).catch(() => {}); // Swallow errors — inbox page filters by archived param
// 3. On inbox page: track archived IDs client-side to hide them from cached list
const archivedIds = new Set<string>();
const archived = searchParams.get('archived');
if (archived) archivedIds.add(archived);
// Filter cached inbox data
const visibleEmails = emails.filter(e => !archivedIds.has(e.id));This gives sub-100ms perceived archive time regardless of server latency.
Reply & Compose
import { replyToEmail } from '@ordinatio/email';
await replyToEmail(db, {
emailId: 'msg-123',
bodyHtml: '<p>Thanks for your order!</p>',
accessToken: token,
}, callbacks);Content Fetching
import { fetchEmailContent } from '@ordinatio/email';
// Fetch full body + attachments (lazy-loaded on first access)
const content = await fetchEmailContent(db, emailId, {
sanitizeHtml: (html) => sanitize(html),
});Templates
Email templates with variable rendering, categories, and default seeding.
Template CRUD
import { createTemplate, updateTemplate, removeTemplate, listTemplates } from '@ordinatio/email';
// Create
const template = await createTemplate(db, {
name: 'Order Confirmation',
category: 'order',
subject: 'Your order {{orderNumber}} is confirmed',
bodyHtml: '<p>Dear {{clientName}}, your {{garmentType}} order is confirmed.</p>',
}, callbacks);
// List by category
const templates = await listTemplates(db, { category: 'order' });
// Update
await updateTemplate(db, template.id, { subject: 'Updated subject' }, callbacks);
// Delete (default templates protected)
await removeTemplate(db, template.id, callbacks);Variable Rendering
import { renderTemplate, extractPlaceholders, AVAILABLE_VARIABLES } from '@ordinatio/email';
// See all available variables
console.log(AVAILABLE_VARIABLES);
// [
// { key: 'clientName', label: 'Client Name', category: 'client' },
// { key: 'orderNumber', label: 'Order Number', category: 'order' },
// { key: 'clothierName', label: 'Clothier Name', category: 'clothier' },
// ...
// ]
// Extract placeholders from a template
const placeholders = extractPlaceholders('Hello {{clientName}}, your {{garmentType}} is ready.');
// → ['clientName', 'garmentType']
// Render with variables
const rendered = renderTemplate(
{ subject: 'Order {{orderNumber}}', bodyHtml: '<p>Hi {{clientName}}</p>' },
{ clientName: 'John Smith', orderNumber: 'ORD-001' }
);
// → { subject: 'Order ORD-001', bodyHtml: '<p>Hi John Smith</p>' }Default Templates
The engine ships with default templates that auto-seed on first access:
| Category | Templates |
|----------|-----------|
| fitting | Fitting appointment confirmation, fitting reminder |
| order | Order confirmation, order ready for pickup |
| fabric | Fabric arrived notification |
| welcome | New client welcome |
| followup | Post-delivery follow-up |
import { ensureDefaults, resetToDefaults } from '@ordinatio/email';
// Auto-seed defaults (idempotent)
await ensureDefaults(db);
// Reset all templates to factory defaults
await resetToDefaults(db, callbacks);Scheduled Emails
Queue emails for future delivery with retry logic.
import {
scheduleEmail,
cancelScheduledEmail,
getScheduledEmails,
getPendingToSend,
markAsProcessing,
markAsSent,
markAsFailed,
retryScheduledEmail,
} from '@ordinatio/email';
// Schedule for later
const scheduled = await scheduleEmail(db, {
toEmail: '[email protected]',
subject: 'Your fitting is tomorrow',
bodyHtml: '<p>Just a reminder...</p>',
scheduledFor: new Date('2026-03-10T09:00:00Z'),
}, callbacks);
// Cancel before sending
await cancelScheduledEmail(db, scheduled.id, callbacks);
// Worker polling: find emails due for delivery
const due = await getPendingToSend(db);
for (const email of due) {
await markAsProcessing(db, email.id);
try {
// ... send via provider ...
await markAsSent(db, email.id, callbacks);
} catch (err) {
await markAsFailed(db, email.id, err.message, callbacks);
}
}
// Retry a failed email
await retryScheduledEmail(db, failedEmailId);Status lifecycle: PENDING -> PROCESSING -> SENT | FAILED
OAEM Protocol Integration
The email engine integrates with the OAEM protocol layer (bundled at src/oaem/) via the OaemCallbacks interface. OAEM exports are available from the main @ordinatio/email entry point or via the @ordinatio/email/oaem subpath.
import type { OaemCallbacks } from '@ordinatio/email';
const oaemCallbacks: OaemCallbacks = {
// Outgoing: build and inject capsule before sending
buildCapsule: async (context) => {
const capsule = encodeCapsule({ intent: 'proposal_offer', ... });
const signed = await signCapsule(capsule, privateKey, options);
const augmented = embedCapsule(context.bodyHtml, capsule, { signature: signed });
return { bodyHtml: augmented, capsuleRaw: capsule };
},
// Incoming: parse and verify capsule from received email
parseCapsule: async (context) => {
const extracted = extractCapsule(context.bodyHtml);
if (!extracted?.found) return { found: false, trustTier: 0, verified: false };
const trust = await evaluateTrust(extracted.payload, extracted.signature, trustContext);
return { found: true, capsule: extracted.payload, trustTier: trust.tier, verified: trust.signatureValid };
},
// After verification: persist to thread ledger
onCapsuleVerified: async (result) => {
await db.oaemThreadLedger.upsert({ ... });
},
};Callback Injection Pattern
The engine has zero imports from any host application. All side effects are supplied via typed callback interfaces:
// Activity logging (e.g., to an activity feed)
interface ActivityLogger {
(action: EmailActivityAction, description: string, data?: EmailActivityData): Promise<void>;
}
// Event emission (e.g., to trigger automations)
interface EventEmitter {
(event: { type: string; data: Record<string, unknown> }): Promise<void>;
}
// Contact resolution (e.g., create CRM contacts from email senders)
// Used during sync to auto-link emails to contacts/clients
interface ContactResolver {
(db: PrismaClient, email: string, name?: string): Promise<{ id: string; clientId?: string } | null>;
}
// HTML sanitization (e.g., strip script tags from email body)
interface HtmlSanitizer {
(html: string): string;
}
// Context extraction (e.g., extract structured data for LLM context windows)
interface EmailContextExtractor {
(emailData: { subject: string; fromEmail: string; snippet: string; bodyHtml?: string }): Promise<void>;
}Auto-Contact from Email Senders
The resolveContact callback in EmailSyncCallbacks enables automatic contact creation from email senders. During sync, if an incoming email doesn't match any existing client, the engine calls resolveContact with the sender's email and name.
const syncCallbacks: EmailSyncCallbacks = {
resolveContact: async (db, fromEmail, fromName) => {
// Check if contact already exists
let contact = await db.contact.findFirst({ where: { email: fromEmail } });
if (!contact) {
// Auto-create contact from sender
contact = await db.contact.create({
data: {
email: fromEmail,
name: fromName || fromEmail.split('@')[0],
source: 'EMAIL_AUTO',
},
});
}
// If contact is linked to a client, return clientId too
return {
id: contact.id,
clientId: contact.clientId ?? undefined,
};
},
};The resolved contactId is stored on the email record, enabling:
- Sender name links to contact/client profile in inbox
- Client emails grouped under CRM records
- Contact-to-client conversion when upgrading a prospect
Recommended feature flag: AUTO_CONTACT_FROM_EMAIL — lets consumers toggle this behavior per-tenant.
Composing Callbacks
const callbacks: EmailMutationCallbacks = {
onActivity: async (action, description, data) => {
await createActivity(db, { action, description, metadata: data });
},
onEvent: async (event) => {
await emitTriggerEvent(event.type, event.data);
},
};
// Pass to any mutation
await connectAccount(db, 'gmail', code, email, callbacks);
await replyToEmail(db, input, callbacks);
await archiveEmail(db, emailId, userId, callbacks);Error System
179 error codes following the Rule 8 pattern. Every error includes a unique timestamped reference for debugging.
import { emailError, EMAIL_ENGINE_ERRORS } from '@ordinatio/email';
// Generate an error with ref
const { code, ref, timestamp, description, diagnosis } = emailError('EMAIL_101');
// → {
// code: 'EMAIL_101',
// ref: 'EMAIL_101-20260305T120000',
// description: 'OAuth token exchange failed',
// severity: 'error',
// recoverable: true,
// diagnosis: ['Check OAuth client credentials', 'Verify redirect URI matches', ...]
// }
// With runtime context
const error = emailError('EMAIL_601', { domain: 'acmecorp.com', source: 'mx_resolver' });Error Code Ranges
| Range | Module | Count |
|-------|--------|-------|
| EMAIL_100-104 | Account (OAuth, connect/disconnect) | 5 |
| EMAIL_200-206 | Messages (inbox, detail, reply, link) | 7 |
| EMAIL_300-310 | Attachments & transcription | 11 |
| EMAIL_400-421 | Categories & tasks (legacy) | 22 |
| EMAIL_430-502 | Scheduled, archive, drafts, templates, sync | 73 |
| EMAIL_600-650 | Discovery + multi-provider | 51 |
| TEMPLATE_100-109 | Email templates | 10 |
Each error entry includes:
{
file: string; // Source file
function: string; // Function name
httpStatus: number; // Suggested HTTP status code
severity: 'low' | 'medium' | 'error' | 'warn';
recoverable: boolean; // Can the user retry?
description: string; // Human-readable description
diagnosis: string[]; // Steps to investigate
}Validation Schemas
11 Zod schemas for runtime input validation:
import {
ConnectAccountSchema,
ScheduleEmailSchema,
CreateEmailTemplateSchema,
GetInboxMessagesQuerySchema,
// ...
} from '@ordinatio/email';
// Validate input
const result = ScheduleEmailSchema.safeParse(userInput);
if (!result.success) {
return { error: result.error.flatten().fieldErrors };
}| Schema | Used For |
|--------|----------|
| EmailProviderSchema | Provider type validation (gmail, outlook, imap) |
| ConnectAccountSchema | OAuth connection input |
| ScheduleEmailSchema | Schedule email input (to, subject, body, scheduledFor) |
| GetScheduledEmailsQuerySchema | Scheduled email list query params |
| GetInboxMessagesQuerySchema | Inbox query params (search, status, pagination) |
| CreateDraftSchema | Draft creation input |
| CreateEmailTemplateSchema | Template creation |
| UpdateEmailTemplateSchema | Template update |
| RenderEmailTemplateSchema | Template render (template + variables) |
| UseEmailTemplateSchema | Apply template to compose |
| ScheduledEmailStatusSchema | Status enum validation |
Testing
# Run all tests
pnpm --filter @ordinatio/email test:run
# Run with watch
pnpm --filter @ordinatio/email test
# TypeScript check
pnpm --filter @ordinatio/email exec tsc --noEmit529 tests across 19 test files (11 core + 8 OAEM):
| File | Tests | Coverage |
|------|-------|----------|
| account.test.ts | 11 | OAuth flow, token refresh, disconnection |
| templates.test.ts | 31 | CRUD, rendering, defaults, categories |
| template-renderer.test.ts | 18 | Variables, placeholders, edge cases |
| scheduled.test.ts | 24 | Scheduling, cancellation, retry, status |
| sync-service.test.ts | 39 | Multi-provider sync, failures, contacts |
| providers/gmail.test.ts | 42 | OAuth, messages, MIME, archive |
| providers/gmail-mime.test.ts | 29 | RFC 2822 MIME encoding, attachments |
| providers/outlook.test.ts | 31 | Graph API, OAuth, send, archive |
| providers/imap-smtp.test.ts | 26 | IMAP connect, SMTP, TLS, folders |
| discovery/discovery-service.test.ts | 25 | Multi-source pipeline, confidence |
| discovery/mx-resolver.test.ts | 16 | MX lookup, known provider matching |
| oaem/capsule/capsule.test.ts | — | CBOR encoding, dual-prefix embedding |
| oaem/signing/signing.test.ts | — | Ed25519 JWS, key rotation |
| oaem/trust/trust.test.ts | — | 3-tier trust evaluation |
| oaem/ledger/ledger.test.ts | — | Hash-chained thread state machine |
| oaem/oaem-security.test.ts | — | Replay protection, nonce tracking |
| oaem/oaem-invariants.test.ts | — | Protocol invariant checks |
| oaem/oaem-durability.test.ts | — | Durability under failure |
| oaem/oaem-extraction-torture.test.ts | — | Capsule extraction edge cases |
Plus 346 mob tests in the host application testing edge cases, fuzzing, concurrency, security, and state corruption scenarios.
API Reference
Providers
| Export | Type | Description |
|--------|------|-------------|
| getProvider(type) | (ProviderType) => EmailProvider | Factory — returns provider instance |
| isProviderSupported(type) | (string) => boolean | Type guard for valid providers |
| registerProvider(type, factory) | (string, () => EmailProvider) => void | Register a custom provider |
| GmailProvider | class | Gmail implementation (OAuth) |
| OutlookProvider | class | Outlook implementation (OAuth) |
| ImapSmtpProvider | class | Universal IMAP/SMTP implementation |
| buildMimeMessage(options) | (MimeMessageOptions) => string | Build raw MIME message |
Account
| Export | Type | Description |
|--------|------|-------------|
| getActiveAccount(db) | async | Get the currently active email account |
| getConnectUrl(provider) | (string) => string | Get OAuth authorization URL |
| getValidAccessToken(db, accountId) | async | Refresh token if expired, return valid token |
| updateSyncTimestamp(db, accountId) | async | Update lastSyncAt after sync |
| connectAccount(db, provider, code, email, callbacks?) | async | Exchange OAuth code, create account |
| disconnectAccount(db, accountId, callbacks?) | async | Delete account and associated data |
Messages
| Export | Type | Description |
|--------|------|-------------|
| getInboxEmails(db, options) | async | List inbox messages (paginated, searchable) |
| getInboxThreads(db, options) | async | List threaded conversations |
| getThreadMessages(db, threadId) | async | Get all messages in a thread (conversation view) |
| getEmail(db, emailId) | async | Get single email with full content |
| getClientEmails(db, clientId) | async | Get all emails linked to a client |
| replyToEmail(db, input, callbacks?) | async | Reply to an email |
| linkEmailToClient(db, emailId, clientId, userId, callbacks?) | async | Associate email with CRM client |
Sync & Archive
| Export | Type | Description |
|--------|------|-------------|
| syncEmails(db, callbacks?) | async | Sync emails from all active accounts |
| logSyncFailure(error, db?) | async | Log sync failure with error code |
| archiveEmail(db, emailId, userId, callbacks?) | async | Archive an email |
| fetchEmailContent(db, emailId, callbacks?) | async | Lazy-fetch full body + attachments |
Templates
| Export | Type | Description |
|--------|------|-------------|
| ensureDefaults(db) | async | Seed default templates (idempotent) |
| listTemplates(db, options?) | async | List templates by category |
| getTemplateById(db, id) | async | Get single template |
| getActiveByCategory(db, category) | async | Get active template for a category |
| createTemplate(db, input, callbacks?) | async | Create a new template |
| updateTemplate(db, id, input, callbacks?) | async | Update template |
| removeTemplate(db, id, callbacks?) | async | Delete template (default protected) |
| resetToDefaults(db, callbacks?) | async | Reset all templates to factory defaults |
| renderTemplate(template, variables) | sync | Render template with variables |
| extractPlaceholders(text) | sync | Extract {{variable}} placeholders |
| validateTemplate(template) | sync | Validate template structure |
| buildVariablesFromContext(client?, order?, clothier?) | sync | Build variables from context objects |
| AVAILABLE_VARIABLES | const | All available template variables |
| SAMPLE_VARIABLES | const | Sample data for template preview |
Scheduled
| Export | Type | Description |
|--------|------|-------------|
| getScheduledEmails(db, options?) | async | List scheduled emails |
| getScheduledEmail(db, id) | async | Get single scheduled email |
| getPendingToSend(db) | async | Get emails due for delivery |
| scheduleEmail(db, input, callbacks?) | async | Schedule email for future delivery |
| cancelScheduledEmail(db, id, callbacks?) | async | Cancel a pending email |
| markAsProcessing(db, id) | async | Mark as being sent |
| markAsSent(db, id, callbacks?) | async | Mark as successfully sent |
| markAsFailed(db, id, errorMessage, callbacks?) | async | Mark as failed with reason |
| retryScheduledEmail(db, id) | async | Reset failed email for retry |
Discovery
| Export | Type | Description |
|--------|------|-------------|
| discoverProvider(email, options?) | async | Auto-detect provider settings |
Error Registry
| Export | Type | Description |
|--------|------|-------------|
| emailError(code, context?) | sync | Generate error with ref |
| templateError(code, context?) | sync | Alias for template errors |
| EMAIL_ENGINE_ERRORS | Record | Full error registry |
| EMAIL_ERRORS | Record | Email-specific subset |
| TEMPLATE_ERRORS | Record | Template-specific subset |
Companion: @ordinatio/ui
The @ordinatio/ui package provides ready-made React components for building email UIs on top of this engine:
| Component | What It Does |
|-----------|-------------|
| <EmailBodyRenderer> | Iframe-isolated HTML rendering with <style> stripping, auto-resize |
| <EmailThreadView> | Gmail-style expand/collapse conversation from getThreadMessages() |
| useKeyboardShortcuts() | Declarative email triage shortcuts (R=reply, E=archive, etc.) |
| cachedFetch() | Stale-while-revalidate caching for inbox data |
| <ErrorBoundary> | Catch rendering crashes with copyable error ref codes |
Pugil Integration
This package includes a Pugil reporter that generates Council-consumable trial_report artifacts from test results.
# Normal test run (no Pugil overhead)
pnpm --filter @ordinatio/email test:run
# With Pugil trial report generation
PUGIL_ENABLED=true pnpm --filter @ordinatio/email test:run
# With Council cycle integration
PUGIL_ENABLED=true PUGIL_CYCLE_ID=cycle-email-v1 pnpm --filter @ordinatio/email test:run- Config:
src/pugil.config.ts— maps test files to categories (unit, integration, adversarial, chaos, concurrency) - Reporter:
src/pugil-reporter.ts— Vitest custom reporter, writes topugil-reports/ - Types:
PugilTestResult,PugilTestCategoryfrom@ordinatio/core
License
Private — part of the System 1701 monorepo.
