4.4 KiB
Mailship
A Nostr email notification server. Receives events pushed from relays via NIP-9a, stores them, and sends daily or weekly digest emails.
Architecture
Flotilla ──HTTP──▶ Mailship (PUT /subscription/email)
│ NIP-98 auth
│ returns {key, callback}
│
└──kind 30390──▶ relay or NPB
│
event matches filter
│
▼
POST /notify/:subId ──▶ Mailship
{id, relay} │
UUID in path ├── fetch event from relay
is the auth ├── store in SQLite (dedup)
│
cron fires ──▶ render digest ──▶ SMTP ──▶ email
Configuration
| Variable | Required | Description |
|---|---|---|
MAILSHIP_SECRET |
✓ | A nostr private key hex string for the server's identity |
MAILSHIP_NAME |
✓ | Name of this Mailship instance |
MAILSHIP_URL |
✓ | Public URL of this instance |
BASE_URL |
✓ | Base URL for callback URLs (same as MAILSHIP_URL typically) |
SMTP_HOST |
✓ | SMTP server hostname |
SMTP_PORT |
✓ | SMTP server port |
SMTP_USER |
✓ | SMTP username |
SMTP_PASSWORD |
✓ | SMTP password |
SMTP_FROM |
✓ | From email address for outgoing mail |
EVENT_VIEWER_URL |
Base URL of the app event links open in (defaults to Flotilla at https://app.flotilla.social, or anything handling the same /spaces/<relay>/<hash> and /<nevent> URL shapes) |
|
BRAND_NAME |
Name used in email branding (default: Flotilla) |
|
BRAND_ACCENT |
Accent color string used in email branding (default: #7161FF) |
|
BRAND_LOGO |
URL of the logo image shown in the email header. Defaults to <EVENT_VIEWER_URL>/logo.png; if empty/unset, a colored brand name is shown instead |
|
DEFAULT_RELAYS |
✓ | Comma-separated list of default relays |
INDEXER_RELAYS |
✓ | Comma-separated list of indexer relays |
SEARCH_RELAYS |
✓ | Comma-separated list of search relays |
PORT |
Port to run on (default: 4738) |
API
PUT /subscription/email
Idempotently register or update an email subscription. Re-sends the confirmation
email only when the subscription is new or the email address changed; a frequency
change keeps the existing confirmation. The pubkey is extracted from the NIP-98
Authorization header — the body does not include a pubkey field.
Body: { email, frequency }
Auth: NIP-98 (Nostr <base64> Authorization header)
Response: { key, callback }
GET /subscription/email
Look up an existing subscription for the authenticated pubkey, so clients can avoid re-registering (and re-confirming) when settings haven't changed. Returns 404 if none exists.
Auth: NIP-98 (Nostr <base64> Authorization header)
Response: { key, callback, email, frequency, confirmed }
DELETE /subscription/:key
Unsubscribe. Verifies the NIP-98 auth pubkey matches the subscription owner.
Auth: NIP-98 (Nostr <base64> Authorization header)
Response: { ok: true }
POST /notify/:id
NIP-9a relay push callback. Called by relays or NPB when matching events are found.
Body: { id, relay }
Response: { ok: true, stored: boolean }
Returns 404 if subscription not found or inactive.
GET /confirm?token=...
Confirm email address via link from confirmation email.
GET /unsubscribe?token=...
Unsubscribe via link from digest email.
Development
pnpm install
pnpm run build
pnpm run start
Previewing the digest email
To iterate on the email template, render it with sample data and open the result in your browser:
pnpm run preview:digest
open digest-preview.html
Docker
Quick start
cp .env.template .env
# Fill in your secrets in .env
docker compose up -d
Build and run manually
docker build -t mailship .
docker run -d \
-p 4738:4738 \
-v mailship-data:/data \
--env-file .env \
mailship
Tests
pnpm test:unit # Run unit tests (vitest)
pnpm test # Run integration tests (bash E2E)
pnpm test:server # Start server for manual testing
Forked from Anchor
Mailship is a fork of Anchor, stripped of push notification support and adapted for NIP-9a relay push event intake.