mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-16 06:19:39 +02:00
Closes the last plan milestone. Users can verify their sending-domain setup without leaving the broadcast settings page. Server (mana-mail) - services/dns-check.ts: parseSpf / parseDkim / parseDmarc are pure functions. SPF accepts include:<mailDomain>, flags weak (+all) and wrong (include missing) and multi-record (RFC 7208 §3.2). DKIM needs v=DKIM1 + a p= public-key segment. DMARC requires v=DMARC1, flags p=none as weak (monitoring only), ok on quarantine/reject. All three are case-insensitive. - lookupTxt(): DNS-over-HTTPS against Cloudflare 1.1.1.1 — avoids the Bun/container udp-resolver flakiness and works everywhere. Multi-string TXT (`"a" "b"`) get concatenated before parsing. - checkDomain(): one call, three parallel DoH lookups, returns a structured result with suggested copy-paste records scoped to the user's actual mail domain from config. - Route: GET /v1/mail/dns-check?domain=&selector= (JWT auth). Zod validates the domain looks sensible before hitting DoH. - 16 unit tests covering all three parsers + multi-record edge case. Client - api.ts: runDnsCheck(domain, selector?) helper with typed result. - components/DnsCheckBanner.svelte: derives domain from the default from-email (after @), calls the check on-demand, renders per-record status chips (ok / weak / wrong / missing) with messages, exposes copy-pasteable SPF + DMARC records when anything's off. DKIM setup is provider-specific so we show a hint rather than a canned record. Last-check timestamp persists to settings.dnsCheck so the banner survives a reload without re-hitting the API. - Wired into SettingsForm between Impressum and Standard-Footer — where the user is already thinking about "what's required to actually send". All checks clean: - webapp pnpm check: 0 broadcast errors (4 pre-existing articles errors from parallel Spaces work, unrelated) - mana-mail tests: 36/36 across tracking-token + link-rewriter + dns-check - mana-mail build: 2.51 MB (+8 KB for juice — dns-check itself is ~3 KB) Plan: docs/plans/broadcast-module.md §M8. All 10 milestones now done. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
100 lines
3.9 KiB
TypeScript
100 lines
3.9 KiB
TypeScript
/**
|
|
* mana-mail — Mail service for the Mana ecosystem.
|
|
*
|
|
* Hono + Bun runtime. Provides JMAP-based email access to Stalwart,
|
|
* account provisioning (@mana.how addresses), and mail API for the frontend.
|
|
*/
|
|
|
|
import { Hono } from 'hono';
|
|
import { cors } from 'hono/cors';
|
|
import { loadConfig } from './config';
|
|
import { getDb } from './db/connection';
|
|
import { serviceErrorHandler as errorHandler } from '@mana/shared-hono';
|
|
import { jwtAuth } from './middleware/jwt-auth';
|
|
import { serviceAuth } from './middleware/service-auth';
|
|
import { JmapClient } from './services/jmap-client';
|
|
import { AccountService } from './services/account-service';
|
|
import { MailService } from './services/mail-service';
|
|
import { BroadcastOrchestrator } from './services/broadcast-orchestrator';
|
|
import { healthRoutes } from './routes/health';
|
|
import { createThreadRoutes } from './routes/threads';
|
|
import { createMessageRoutes } from './routes/messages';
|
|
import { createSendRoutes } from './routes/send';
|
|
import { createLabelRoutes } from './routes/labels';
|
|
import { createAccountRoutes } from './routes/accounts';
|
|
import { createInternalRoutes } from './routes/internal';
|
|
import { createBroadcastSendRoutes } from './routes/broadcast-send';
|
|
import { createBroadcastTrackRoutes } from './routes/broadcast-track';
|
|
import { createBroadcastStatsRoutes } from './routes/broadcast-stats';
|
|
import { createBroadcastDnsRoutes } from './routes/broadcast-dns';
|
|
|
|
// ─── Bootstrap ──────────────────────────────────────────────
|
|
|
|
const config = loadConfig();
|
|
const db = getDb(config.databaseUrl);
|
|
|
|
// Instantiate services
|
|
const jmapClient = new JmapClient(config.stalwart);
|
|
const accountService = new AccountService(db, config.stalwart);
|
|
const mailService = new MailService(db, jmapClient, accountService);
|
|
const broadcastOrchestrator = new BroadcastOrchestrator(
|
|
db,
|
|
jmapClient,
|
|
accountService,
|
|
config.broadcast.trackingSecret,
|
|
config.baseUrl,
|
|
config.broadcast.sendThrottleMs
|
|
);
|
|
|
|
// ─── App ────────────────────────────────────────────────────
|
|
|
|
const app = new Hono();
|
|
|
|
// Global middleware
|
|
app.onError(errorHandler);
|
|
app.use(
|
|
'*',
|
|
cors({
|
|
origin: config.cors.origins,
|
|
credentials: true,
|
|
})
|
|
);
|
|
|
|
// Health check (no auth)
|
|
app.route('/health', healthRoutes);
|
|
|
|
// Public tracking routes — NO auth. Recipients click these from
|
|
// emails without being logged in. Mounted under /api/v1/track/* so
|
|
// they sit outside the /api/v1/mail/* JWT middleware. Registered
|
|
// BEFORE the JWT middleware to avoid middleware leakage.
|
|
app.route(
|
|
'/api/v1/track',
|
|
createBroadcastTrackRoutes(db, config.broadcast.trackingSecret, config.baseUrl)
|
|
);
|
|
|
|
// User-facing routes (JWT auth)
|
|
app.use('/api/v1/mail/*', jwtAuth(config.manaAuthUrl));
|
|
app.route('/api/v1/mail', createThreadRoutes(mailService));
|
|
app.route('/api/v1/mail', createSendRoutes(mailService));
|
|
app.route(
|
|
'/api/v1/mail',
|
|
createBroadcastSendRoutes(broadcastOrchestrator, config.broadcast.maxRecipientsPerCampaign)
|
|
);
|
|
app.route('/api/v1/mail', createBroadcastStatsRoutes(db));
|
|
app.route('/api/v1/mail', createBroadcastDnsRoutes(config.stalwart.domain));
|
|
app.route('/api/v1/mail', createLabelRoutes(mailService));
|
|
app.route('/api/v1/mail', createAccountRoutes(accountService));
|
|
app.route('/api/v1/mail/messages', createMessageRoutes(mailService));
|
|
|
|
// Service-to-service routes (X-Service-Key auth)
|
|
app.use('/api/v1/internal/*', serviceAuth(config.serviceKey));
|
|
app.route('/api/v1/internal', createInternalRoutes(accountService));
|
|
|
|
// ─── Start ──────────────────────────────────────────────────
|
|
|
|
console.log(`mana-mail starting on port ${config.port}...`);
|
|
|
|
export default {
|
|
port: config.port,
|
|
fetch: app.fetch,
|
|
};
|