Skip to content

Commit 775f428

Browse files
Mihai - Alexandru ChindrișMihai - Alexandru Chindriș
authored andcommitted
Scaffold Mailbrain webhook receiver
0 parents  commit 775f428

13 files changed

Lines changed: 358 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: ["main"]
6+
pull_request:
7+
8+
jobs:
9+
build:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Checkout
13+
uses: actions/checkout@v4
14+
15+
- name: Setup Node
16+
uses: actions/setup-node@v4
17+
with:
18+
node-version: "20"
19+
cache: "npm"
20+
cache-dependency-path: apps/webhook-receiver/package-lock.json
21+
22+
- name: Install deps
23+
working-directory: apps/webhook-receiver
24+
run: npm install
25+
26+
- name: Build
27+
working-directory: apps/webhook-receiver
28+
run: npm run build

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules/
2+
dist/
3+
.env
4+
.DS_Store
5+
data/

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Mihai Codes
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Mailbrain
2+
3+
Mailbrain is a local-first mail intelligence system that ingests inbound email via MyMX webhooks, prioritizes what matters, and notifies you through Beeper/CodeBeep (later: ElevenLabs voice). The goal: you stop checking mail and only get alerts for high‑value messages.
4+
5+
## MVP goals
6+
- Receive inbound email webhooks (MyMX)
7+
- Verify webhook signatures
8+
- Normalize/store email payloads
9+
- Score/prioritize messages (people, AI news, waitlist acceptances, etc.)
10+
- Send concise notifications to Beeper via CodeBeep
11+
- Provide rules for allow/block/unsubscribe + safety flags
12+
13+
## Repository layout
14+
```
15+
apps/
16+
webhook-receiver/ # Node/TS webhook endpoint
17+
docs/
18+
vision.md # Product vision + use cases
19+
architecture.md # Data flow + components
20+
student-benefits.md # Free programs & credits
21+
beta-access-email.md # MyMX beta request email
22+
roadmap.md # Next steps
23+
```
24+
25+
## Quick start (dev)
26+
```bash
27+
cd apps/webhook-receiver
28+
npm install
29+
npm run dev
30+
```
31+
32+
## Environment
33+
See `.env.example` in `apps/webhook-receiver`.
34+
35+
## Notes
36+
- Webhook handler must be idempotent (same event ID can be retried).
37+
- MyMX signature verification is required in production.
38+
- Store secrets in env; never commit.
39+
40+
## References
41+
- MyMX docs: https://mymx.dev/docs
42+
- Webhook payload: https://mymx.dev/docs/webhook-payload

apps/webhook-receiver/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
PORT=4567
2+
MYMX_WEBHOOK_SECRET=replace_me
3+
EVENT_STORE=../../data/events.jsonl

apps/webhook-receiver/package.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "mailbrain-webhook-receiver",
3+
"version": "0.1.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"dev": "tsx watch src/server.ts",
8+
"start": "node --enable-source-maps dist/server.js",
9+
"build": "tsc -p tsconfig.json"
10+
},
11+
"dependencies": {
12+
"dotenv": "^16.4.5",
13+
"express": "^4.19.2",
14+
"mymx": "^1.0.0"
15+
},
16+
"devDependencies": {
17+
"@types/express": "^4.17.21",
18+
"tsx": "^4.19.2",
19+
"typescript": "^5.5.4"
20+
}
21+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import express from "express";
2+
import fs from "fs";
3+
import path from "path";
4+
import dotenv from "dotenv";
5+
import { handleWebhook, MyMXWebhookError, MYMX_CONFIRMED_HEADER } from "mymx";
6+
7+
dotenv.config();
8+
9+
const app = express();
10+
const port = Number(process.env.PORT || 4567);
11+
const secret = process.env.MYMX_WEBHOOK_SECRET || "";
12+
const eventStorePath = process.env.EVENT_STORE || "./events.jsonl";
13+
14+
if (!secret) {
15+
throw new Error("Missing MYMX_WEBHOOK_SECRET");
16+
}
17+
18+
// MyMX needs raw text body to verify signatures.
19+
app.use(
20+
express.text({
21+
type: "*/*",
22+
limit: "10mb",
23+
})
24+
);
25+
26+
function ensureStoreDir(filePath: string) {
27+
const dir = path.dirname(filePath);
28+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
29+
}
30+
31+
function appendEvent(line: string) {
32+
ensureStoreDir(eventStorePath);
33+
fs.appendFileSync(eventStorePath, line + "\n", "utf8");
34+
}
35+
36+
// Idempotency in MVP: keep last 10k IDs in memory.
37+
const seen = new Set<string>();
38+
const seenQueue: string[] = [];
39+
function remember(id: string) {
40+
if (seen.has(id)) return;
41+
seen.add(id);
42+
seenQueue.push(id);
43+
if (seenQueue.length > 10_000) {
44+
const old = seenQueue.shift();
45+
if (old) seen.delete(old);
46+
}
47+
}
48+
49+
app.post("/webhook/mymx", (req, res) => {
50+
const rawBody = req.body;
51+
const headers = Object.fromEntries(
52+
Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(",") : v])
53+
);
54+
55+
try {
56+
const event = handleWebhook({
57+
body: rawBody,
58+
headers,
59+
secret,
60+
});
61+
62+
if (seen.has(event.id)) {
63+
return res.status(200).set(MYMX_CONFIRMED_HEADER, "true").end();
64+
}
65+
66+
remember(event.id);
67+
68+
const record = {
69+
id: event.id,
70+
received_at: event.email.received_at,
71+
from: event.email.headers.from,
72+
subject: event.email.headers.subject,
73+
to: event.email.headers.to,
74+
body_text: event.email.parsed?.body_text || null,
75+
spam_score: event.email.analysis?.spamassassin?.score ?? null,
76+
raw_event: event,
77+
};
78+
79+
appendEvent(JSON.stringify(record));
80+
81+
return res.status(200).set(MYMX_CONFIRMED_HEADER, "true").end();
82+
} catch (err) {
83+
if (err instanceof MyMXWebhookError) {
84+
return res.status(400).json({ error: err.code });
85+
}
86+
console.error(err);
87+
return res.status(500).json({ error: "internal_error" });
88+
}
89+
});
90+
91+
app.get("/health", (_req, res) => {
92+
res.json({ ok: true });
93+
});
94+
95+
app.listen(port, () => {
96+
console.log(`Mailbrain webhook receiver listening on :${port}`);
97+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"module": "ES2022",
5+
"moduleResolution": "Bundler",
6+
"outDir": "dist",
7+
"rootDir": "src",
8+
"strict": true,
9+
"esModuleInterop": true,
10+
"skipLibCheck": true,
11+
"resolveJsonModule": true,
12+
"types": ["node"]
13+
},
14+
"include": ["src/**/*"]
15+
}

docs/architecture.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Architecture (MVP)
2+
3+
## Data flow
4+
1. **Inbound email** → MX records point to MyMX.
5+
2. **MyMX webhook** → POST JSON payload to our webhook receiver.
6+
3. **Verification** → verify MyMX signature + timestamp.
7+
4. **Normalization** → extract headers/body/attachments metadata.
8+
5. **Scoring** → priority classifier + rules engine.
9+
6. **Routing** → notify (Beeper via CodeBeep) and store summary.
10+
11+
## Components
12+
- **Webhook receiver (Node/TS)**
13+
- MyMX signature validation
14+
- Idempotent event handling
15+
- Normalization and storage
16+
17+
- **Storage (MVP)**
18+
- SQLite (local) or JSONL
19+
- Tables: events, emails, rules, notifications
20+
21+
- **Scoring engine (MVP)**
22+
- Rule-based scoring (VIP senders, keyword hits, waitlist signals)
23+
- Safety flags (phishing / social engineering / NSFW)
24+
25+
- **Notification adapter**
26+
- CodeBeep/Beeper for push
27+
- Later: ElevenLabs voice
28+
29+
## Future upgrades
30+
- ML-based classification
31+
- Active learning on user feedback
32+
- Attachment OCR and entity extraction
33+
- Auto-unsubscribe with vendor APIs or list-unsubscribe headers

docs/beta-access-email.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# MyMX Beta Access Email
2+
3+
**To:** beta@primitive.dev
4+
**Subject:** Request for MyMX beta access (student project)
5+
6+
Hi MyMX team,
7+
8+
I’m a student building a personal mail intelligence tool that ingests inbound email via webhooks to prioritize and notify me about important messages across providers (Gmail, iCloud, forwarded domains). I’d love beta access to MyMX to integrate inbound MX → webhook delivery and build with your SDK.
9+
10+
If you need any details about my use case or expected volume, I’m happy to share.
11+
12+
Thanks!
13+
— <Your Name>

0 commit comments

Comments
 (0)