commit e3de27d8ffcc5cd0d6c7375b2749caef874e0c5a
parent aca0080e875e823d8091dd94759c7247979a3913
Author: Amit Dutta <mail@amit.is-a.dev>
Date: Thu, 3 Sep 2026 07:31:49 +0530
Merge pull request #10 from notamitgamer/revert-5-feat/self-hosted-mailbox
Revert "feat: self-hosted PWA mailbox (Firestore + SSE) to replace Resend-only relay"
Diffstat:
9 files changed, 198 insertions(+), 508 deletions(-)
diff --git a/package.json b/package.json
@@ -9,9 +9,11 @@
"dependencies": {
"cors": "^2.8.5",
"express": "^4.19.0",
- "firebase-admin": "^12.1.0",
+ "mongoose": "^8.3.0",
"resend": "^3.2.0",
- "svix": "^1.24.0"
+ "svix": "^1.24.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
},
"engines": {
"node": ">=18.0.0"
diff --git a/public/icon.svg b/public/icon.svg
@@ -1,5 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
- <rect width="128" height="128" rx="28" fill="#0B57D0"/>
- <path d="M24 40h80a4 4 0 0 1 4 4v40a4 4 0 0 1-4 4H24a4 4 0 0 1-4-4V44a4 4 0 0 1 4-4z" fill="#FFFFFF"/>
- <path d="M24 40l40 30 40-30" fill="none" stroke="#0B57D0" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
-</svg>
diff --git a/public/index.html b/public/index.html
@@ -1,155 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-<meta charset="UTF-8">
-<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
-<title>Mailbox</title>
-<link rel="manifest" href="/manifest.json">
-<link rel="icon" href="/icon.svg">
-<meta name="theme-color" content="#0B57D0">
-<style>
- :root { color-scheme: light dark; }
- * { box-sizing: border-box; }
- body {
- margin: 0; font-family: Roboto, Arial, sans-serif;
- background: #F8F9FA; color: #1F1F1F;
- }
- @media (prefers-color-scheme: dark) {
- body { background: #131314; color: #E3E3E3; }
- .row { border-color: #333 !important; }
- .row:hover { background: #1E1F20 !important; }
- header { background: #1E1F20 !important; border-color: #333 !important; }
- }
- header {
- position: sticky; top: 0; z-index: 5;
- display: flex; align-items: center; gap: 12px;
- padding: 16px 20px; background: #fff; border-bottom: 1px solid #e3e3e3;
- }
- header h1 { font-size: 18px; margin: 0; font-weight: 500; }
- .badge {
- background: #0B57D0; color: #fff; border-radius: 100px;
- font-size: 12px; padding: 2px 10px; margin-left: auto;
- }
- #list { max-width: 720px; margin: 0 auto; }
- .row {
- display: flex; flex-direction: column; gap: 4px;
- padding: 14px 20px; border-bottom: 1px solid #eee; cursor: pointer;
- }
- .row:hover { background: #f0f4f9; }
- .row .top { display: flex; justify-content: space-between; gap: 8px; font-size: 14px; }
- .row .from { font-weight: 500; }
- .row .subject { font-size: 14px; color: #555; }
- .row.unread .subject, .row.unread .from { font-weight: 700; }
- .empty { text-align: center; padding: 60px 20px; color: #888; }
- #reader {
- display: none; max-width: 720px; margin: 0 auto; padding: 20px;
- }
- #reader.open { display: block; }
- #list.hidden { display: none; }
- .back { background: none; border: none; color: #0B57D0; font-size: 14px; cursor: pointer; padding: 8px 0; }
- #readerMeta { margin-bottom: 12px; }
- #readerMeta .subj { font-size: 20px; font-weight: 500; margin: 8px 0; }
- #readerMeta .from { font-size: 14px; color: #666; }
- iframe { width: 100%; min-height: 60vh; border: none; }
-</style>
-</head>
-<body>
-
-<header>
- <h1>📬 Mailbox</h1>
- <span class="badge" id="count" style="display:none;"></span>
-</header>
-
-<div id="list"><div class="empty">Loading…</div></div>
-
-<div id="reader">
- <button class="back" onclick="closeReader()">← Back to inbox</button>
- <div id="readerMeta"></div>
- <iframe id="readerFrame" sandbox=""></iframe>
-</div>
-
-<script>
-let mails = [];
-
-async function loadMails() {
- const res = await fetch('/api/mails');
- const data = await res.json();
- mails = data.mails || [];
- render();
-}
-
-function render() {
- const list = document.getElementById('list');
- const count = document.getElementById('count');
- const unread = mails.filter(m => !m.read).length;
- count.style.display = unread ? 'inline-block' : 'none';
- count.textContent = unread + ' new';
-
- if (!mails.length) {
- list.innerHTML = '<div class="empty">No mail yet. It will show up here the moment it arrives.</div>';
- return;
- }
-
- list.innerHTML = mails.map(m => `
- <div class="row ${m.read ? '' : 'unread'}" onclick="openMail('${m.id}')">
- <div class="top">
- <span class="from">${escapeHtml(m.from || 'Unknown sender')}</span>
- <span class="time">${m.receivedAt ? new Date(m.receivedAt).toLocaleString() : ''}</span>
- </div>
- <div class="subject">${escapeHtml(m.subject || '(no subject)')}</div>
- </div>
- `).join('');
-}
-
-async function openMail(id) {
- const res = await fetch('/api/mails/' + id);
- const mail = await res.json();
-
- document.getElementById('readerMeta').innerHTML = `
- <div class="from"><strong>From:</strong> ${escapeHtml(mail.from || '')}</div>
- <div class="from"><strong>To:</strong> ${escapeHtml(mail.to || '')}</div>
- <div class="subj">${escapeHtml(mail.subject || '(no subject)')}</div>
- `;
-
- const frame = document.getElementById('readerFrame');
- frame.srcdoc = mail.html || `<pre style="white-space:pre-wrap;font-family:Roboto,Arial,sans-serif;">${escapeHtml(mail.text || '(empty message)')}</pre>`;
-
- document.getElementById('list').classList.add('hidden');
- document.getElementById('reader').classList.add('open');
-
- const idx = mails.findIndex(m => m.id === id);
- if (idx > -1) mails[idx].read = true;
-}
-
-function closeReader() {
- document.getElementById('list').classList.remove('hidden');
- document.getElementById('reader').classList.remove('open');
- render();
-}
-
-function escapeHtml(str) {
- return String(str).replace(/[&<>"']/g, s => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[s]));
-}
-
-function connectStream() {
- const es = new EventSource('/api/stream');
- es.addEventListener('new_mail', (e) => {
- const mail = JSON.parse(e.data);
- mails.unshift({ ...mail, read: false });
- render();
- });
- es.onerror = () => {
- es.close();
- setTimeout(connectStream, 5000);
- };
-}
-
-if ('serviceWorker' in navigator) {
- navigator.serviceWorker.register('/sw.js').catch(() => {});
-}
-
-loadMails();
-connectStream();
-</script>
-</body>
-</html>
diff --git a/public/manifest.json b/public/manifest.json
@@ -1,17 +0,0 @@
-{
- "name": "Amit's Mailbox",
- "short_name": "Mailbox",
- "description": "Self-hosted inbox for mail@amit.is-a.dev",
- "start_url": "/",
- "display": "standalone",
- "background_color": "#F8F9FA",
- "theme_color": "#0B57D0",
- "icons": [
- {
- "src": "/icon.svg",
- "sizes": "any",
- "type": "image/svg+xml",
- "purpose": "any maskable"
- }
- ]
-}
diff --git a/public/sw.js b/public/sw.js
@@ -1,23 +0,0 @@
-const CACHE = 'mailbox-shell-v1';
-const SHELL_ASSETS = ['/', '/index.html', '/icon.svg', '/manifest.json'];
-
-self.addEventListener('install', (event) => {
- event.waitUntil(
- caches.open(CACHE).then((cache) => cache.addAll(SHELL_ASSETS))
- );
- self.skipWaiting();
-});
-
-self.addEventListener('activate', (event) => {
- event.waitUntil(self.clients.claim());
-});
-
-self.addEventListener('fetch', (event) => {
- const { request } = event;
- // Never cache API calls or the SSE stream — always go to network for those.
- if (request.url.includes('/api/')) return;
-
- event.respondWith(
- caches.match(request).then((cached) => cached || fetch(request))
- );
-});
diff --git a/scripts/backfill-from-resend.js b/scripts/backfill-from-resend.js
@@ -1,82 +0,0 @@
-/**
- * One-off backfill: pulls every email Resend has already received
- * (visible in the Resend dashboard under Emails > Receiving) and stores
- * it in Firestore using the same schema server.js writes on new mail.
- *
- * Resend keeps a log of received emails independent of your webhook, so
- * this recovers everything that arrived before this mailbox existed.
- *
- * Usage (from the repo root, with deps installed):
- * RESEND_API_KEY=re_xxx FIREBASE_SERVICE_ACCOUNT='{...}' node scripts/backfill-from-resend.js
- *
- * Safe to re-run: emails are stored under a deterministic Firestore doc ID
- * derived from the Resend email ID, so re-running just overwrites the same
- * docs instead of duplicating them.
- */
-
-const { Resend } = require('resend');
-const { db } = require('../src/firebase');
-
-const resend = new Resend(process.env.RESEND_API_KEY);
-const mailsCollection = db.collection('mails');
-
-async function backfill() {
- if (!process.env.RESEND_API_KEY) {
- console.error('Set RESEND_API_KEY before running this script.');
- process.exit(1);
- }
-
- let cursor;
- let total = 0;
- let page = 0;
-
- do {
- page += 1;
- const { data, error } = await resend.emails.receiving.list(
- cursor ? { before: cursor } : undefined
- );
-
- if (error) {
- console.error('Failed to list received emails:', error.message);
- process.exit(1);
- }
-
- const items = data?.data || [];
- console.log(`Page ${page}: ${items.length} emails`);
-
- for (const summary of items) {
- const { data: full, error: getError } = await resend.emails.receiving.get(summary.id);
- if (getError) {
- console.error(`Skipping ${summary.id}: ${getError.message}`);
- continue;
- }
-
- const docId = `resend_${summary.id}`;
- await mailsCollection.doc(docId).set({
- from: full.from,
- to: (full.to && full.to[0]) ? full.to[0].toLowerCase() : '',
- subject: full.subject || 'No Subject',
- html: full.html || null,
- text: full.text || null,
- resendEmailId: full.id,
- read: true, // treat backfilled mail as already-seen
- receivedAt: full.created_at ? new Date(full.created_at) : new Date(),
- backfilled: true,
- }, { merge: true });
-
- total += 1;
- process.stdout.write(` stored ${full.subject || '(no subject)'}\n`);
- }
-
- cursor = data?.has_more ? items[items.length - 1]?.id : null;
- } while (cursor);
-
- console.log(`\nDone. Backfilled ${total} email(s) into Firestore.`);
- console.log('Restart the server (or wait for its next cold start) to pick these up in the list cache.');
- process.exit(0);
-}
-
-backfill().catch((err) => {
- console.error(err);
- process.exit(1);
-});
diff --git a/server.js b/server.js
@@ -1,58 +1,13 @@
-const path = require('path');
const express = require('express');
const { Resend } = require('resend');
const { Webhook } = require('svix');
-const { db, admin } = require('./src/firebase');
-const sse = require('./src/sseManager');
-
const app = express();
const port = process.env.PORT || 3000;
const resend = new Resend(process.env.RESEND_API_KEY);
const webhookSecret = process.env.RESEND_WEBHOOK_SECRET;
-// Optional: still ping your personal Gmail when mail arrives. If unset, this
-// step is just skipped and the mailbox is the sole source of truth.
-const personalGmail = process.env.PERSONAL_GMAIL;
-const forwardingAddress = process.env.FORWARDING_BOT_ADDRESS;
-
-const mailsCollection = () => db.collection('mails');
-
-// --- In-memory cache ---
-// Once warm, list/detail reads are served straight from this process's
-// memory instead of hitting Firestore on every device/tab that opens the
-// mailbox. Firestore is only touched on a genuine cache miss (cold start,
-// after a Render restart) or a real write (new mail / delete).
-let mailListCache = null; // array of list-row objects, newest first
-const mailDetailCache = new Map(); // id -> full mail object
-
-function toListRow(id, d) {
- return {
- id,
- from: d.from,
- to: d.to,
- subject: d.subject,
- read: d.read,
- receivedAt: d.receivedAt ? d.receivedAt.toMillis() : null,
- };
-}
-
-function toDetail(id, d) {
- return {
- id,
- from: d.from,
- to: d.to,
- subject: d.subject,
- html: d.html,
- text: d.text,
- read: d.read,
- receivedAt: d.receivedAt ? d.receivedAt.toMillis() : null,
- };
-}
-
-app.use(express.static(path.join(__dirname, 'public')));
-
app.post('/api/incoming', express.text({ type: 'application/json' }), async (req, res) => {
try {
const rawBody = req.body;
@@ -64,7 +19,7 @@ app.post('/api/incoming', express.text({ type: 'application/json' }), async (req
const wh = new Webhook(webhookSecret);
let event;
-
+
try {
event = wh.verify(rawBody, headers);
} catch (err) {
@@ -78,158 +33,225 @@ app.post('/api/incoming', express.text({ type: 'application/json' }), async (req
const emailData = event.data;
const originalSender = emailData.from;
- const originalRecipient = (emailData.to && emailData.to[0] ? emailData.to[0] : '').toLowerCase();
+ const originalRecipient = emailData.to[0].toLowerCase();
const subject = emailData.subject || 'No Subject';
const allowedString = process.env.ALLOWED_ALIASES || '';
- const allowedAliases = allowedString.split(',').map(alias => alias.trim().toLowerCase()).filter(Boolean);
+ const allowedAliases = allowedString.split(',').map(alias => alias.trim().toLowerCase());
- if (allowedAliases.length && !allowedAliases.includes(originalRecipient)) {
+ if (!allowedAliases.includes(originalRecipient)) {
return res.status(200).json({ success: true, message: 'Alias ignored' });
}
- // --- 1. Store the full email as a real mailbox entry ---
- const mailDoc = {
- from: originalSender,
- to: originalRecipient,
- subject,
- html: emailData.html || null,
- text: emailData.text || null,
- resendEmailId: emailData.email_id || null,
- read: false,
- receivedAt: admin.firestore.FieldValue.serverTimestamp(),
- };
-
- const docRef = await mailsCollection().add(mailDoc);
-
- // Warm the caches with this new mail immediately — no Firestore
- // round-trip needed for the next read from any device.
- const cachedDoc = { ...mailDoc, receivedAt: { toMillis: () => Date.now() } };
- if (mailListCache) {
- mailListCache.unshift(toListRow(docRef.id, cachedDoc));
- }
- mailDetailCache.set(docRef.id, toDetail(docRef.id, cachedDoc));
-
- // Push live update to any open mailbox tab
- sse.broadcast('new_mail', {
- id: docRef.id,
- from: originalSender,
- to: originalRecipient,
- subject,
- receivedAt: Date.now(),
- });
-
- // --- 2. Optional: fire off a lightweight notification email ---
- if (personalGmail && forwardingAddress) {
- const notificationHtml = `
- <div style="font-family:Roboto,Arial,sans-serif;max-width:480px;margin:0 auto;padding:24px;border:1px solid #e3e3e3;border-radius:16px;">
- <h2 style="margin:0 0 8px;">New mail in your inbox</h2>
- <p style="margin:0 0 4px;color:#444;"><strong>From:</strong> ${originalSender}</p>
- <p style="margin:0 0 16px;color:#444;"><strong>Subject:</strong> ${subject}</p>
- <p style="margin:0;color:#747775;font-size:13px;">Open your mailbox app to read the full message.</p>
- </div>`;
-
- const { error } = await resend.emails.send({
- from: `Notifier Bot <${forwardingAddress}>`,
- to: [personalGmail],
- reply_to: originalSender,
- subject: `New mail: ${subject}`,
- html: notificationHtml,
- });
-
- if (error) console.error('Notification send failed:', error.message);
- }
-
- return res.status(200).json({ success: true, id: docRef.id });
-
- } catch (error) {
- console.error(error);
- return res.status(500).json({ error: 'Internal Server Error' });
- }
-});
-
-// --- Mailbox API (public, no auth by design) ---
-
-app.get('/api/mails', async (req, res) => {
- try {
- if (mailListCache) {
- return res.json({ mails: mailListCache, cached: true });
+ const personalGmail = process.env.PERSONAL_GMAIL;
+ const forwardingAddress = process.env.FORWARDING_BOT_ADDRESS;
+
+ // Premium Adaptive Google-style HTML Template
+ const notificationHtml = `
+<!DOCTYPE html>
+<html lang="en" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="color-scheme" content="light dark">
+ <meta name="supported-color-schemes" content="light dark">
+ <title>New Email Notification</title>
+
+ <style>
+ @import url('https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500&family=Roboto+Mono:wght@400;500&family=Roboto:wght@400;500&display=swap');
+
+ body {
+ margin: 0;
+ padding: 0;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
}
- const snapshot = await mailsCollection().orderBy('receivedAt', 'desc').limit(100).get();
- mailListCache = snapshot.docs.map(doc => toListRow(doc.id, doc.data()));
- res.json({ mails: mailListCache, cached: false });
- } catch (error) {
- console.error(error);
- res.status(500).json({ error: 'Failed to fetch mails' });
- }
-});
-
-app.get('/api/mails/:id', async (req, res) => {
- try {
- const { id } = req.params;
-
- let mail = mailDetailCache.get(id);
- let fromCache = true;
-
- if (!mail) {
- fromCache = false;
- const doc = await mailsCollection().doc(id).get();
- if (!doc.exists) return res.status(404).json({ error: 'Not found' });
- mail = toDetail(doc.id, doc.data());
- mailDetailCache.set(id, mail);
+ .btn-pill:hover {
+ opacity: 0.9;
+ transform: translateY(-1px);
+ transition: all 0.2s ease;
}
- if (!mail.read) {
- mail = { ...mail, read: true };
- mailDetailCache.set(id, mail);
- if (mailListCache) {
- const row = mailListCache.find(m => m.id === id);
- if (row) row.read = true;
+ @media (prefers-color-scheme: dark) {
+ .bg-outer { background-color: #131314 !important; }
+ .bg-card { background-color: #1E1F20 !important; }
+ .bg-tint { background-color: #282A2C !important; }
+ .text-primary { color: #E3E3E3 !important; }
+ .text-secondary { color: #C4C7C5 !important; }
+ .text-accent { color: #A8C7FA !important; }
+ .border-card { border-color: #444746 !important; }
+ .divider { background-color: #444746 !important; }
+ .btn-pill {
+ background-color: #A8C7FA !important;
+ color: #000000 !important;
}
- mailsCollection().doc(id).update({ read: true }).catch(() => {});
+ .icon-fill { fill: #8E918F !important; }
}
- res.json({ ...mail, cached: fromCache });
- } catch (error) {
- console.error(error);
- res.status(500).json({ error: 'Failed to fetch mail' });
- }
-});
+ /* Gmail Dark Mode Fixes */
+ [data-ogsb] .bg-outer { background-color: #131314 !important; }
+ [data-ogsb] .bg-card { background-color: #1E1F20 !important; }
+ [data-ogsb] .bg-tint { background-color: #282A2C !important; }
+ [data-ogsc] .text-primary { color: #E3E3E3 !important; }
+ [data-ogsc] .text-secondary { color: #C4C7C5 !important; }
+ [data-ogsc] .text-accent { color: #A8C7FA !important; }
+ [data-ogsb] .border-card { border-color: #444746 !important; }
+ [data-ogsb] .divider { background-color: #444746 !important; }
+ [data-ogsb] .btn-pill { background-color: #A8C7FA !important; }
+ [data-ogsc] .btn-pill { color: #000000 !important; }
+ [data-ogsc] .icon-fill { fill: #8E918F !important; }
+ </style>
+</head>
+
+<body class="bg-outer" style="margin: 0; padding: 0; background-color: #F8F9FA; font-family: 'Google Sans', Roboto, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased;">
+
+ <table width="100%" border="0" cellspacing="0" cellpadding="0" class="bg-outer" style="background-color: #F8F9FA; width: 100%; height: 100%;">
+ <tr>
+ <td align="center" style="padding: 40px 16px;">
+
+ <table border="0" cellspacing="0" cellpadding="0" class="bg-card border-card" style="background-color: #FFFFFF; border-radius: 28px; max-width: 600px; width: 100%; overflow: hidden; border: 1px solid #C4C7C5;">
+
+ <tr>
+ <td style="padding: 40px 32px 16px 32px;">
+ <h2 class="text-primary" style="margin: 0; font-family: 'Google Sans', Roboto, sans-serif; font-size: 22px; font-weight: 500; color: #1F1F1F; letter-spacing: -0.5px;">Incoming Message</h2>
+ <p class="text-secondary" style="margin: 8px 0 0 0; font-family: Roboto, Arial, sans-serif; font-size: 14px; font-weight: 400; color: #444746; line-height: 1.5;">A new email has been routed to your developer inbox.</p>
+ </td>
+ </tr>
+
+ <tr>
+ <td style="padding: 0 32px;">
+ <div class="divider" style="height: 1px; background-color: #E3E3E3; width: 100%; margin: 16px 0;"></div>
+ </td>
+ </tr>
+
+ <tr>
+ <td style="padding: 16px 32px;">
+
+ <div style="margin-bottom: 24px;">
+ <span class="text-accent" style="display: block; font-family: Roboto, Arial, sans-serif; font-size: 11px; font-weight: 500; color: #0B57D0; text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 6px;">Sender Email ID</span>
+ <span class="text-primary" style="display: block; font-family: Roboto, Arial, sans-serif; font-size: 16px; font-weight: 400; color: #1F1F1F; word-break: break-all; line-height: 1.4;">${originalSender}</span>
+ </div>
+
+ <div style="margin-bottom: 24px;">
+ <span class="text-accent" style="display: block; font-family: Roboto, Arial, sans-serif; font-size: 11px; font-weight: 500; color: #0B57D0; text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 6px;">Subject</span>
+ <span class="text-primary" style="display: block; font-family: 'Google Sans', Roboto, Arial, sans-serif; font-size: 18px; font-weight: 500; color: #1F1F1F; line-height: 1.4;">${subject}</span>
+ </div>
+
+ <div style="margin-bottom: 32px;">
+ <span class="text-accent" style="display: block; font-family: Roboto, Arial, sans-serif; font-size: 11px; font-weight: 500; color: #0B57D0; text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 8px;">Email Tracking ID</span>
+
+ <table border="0" cellspacing="0" cellpadding="0" style="width: 100%;">
+ <tr>
+ <td class="bg-tint" style="background-color: #F0F4F9; border-radius: 8px; padding: 12px 16px;">
+ <span class="text-secondary" style="display: block; font-family: 'Roboto Mono', monospace; font-size: 14px; font-weight: 500; color: #444746; word-break: break-all;">
+ ${emailData.email_id || 'N/A'}
+ </span>
+ </td>
+ </tr>
+ </table>
+ </div>
+
+ </td>
+ </tr>
+
+ <tr>
+ <td style="padding: 8px 32px 40px 32px;">
+ <table border="0" cellspacing="0" cellpadding="0">
+ <tr>
+ <td align="center" style="border-radius: 100px;">
+ <a href="https://resend.com/emails/${emailData.email_id}" target="_blank" class="btn-pill" style="display: inline-block; padding: 12px 32px; background-color: #0B57D0; font-family: 'Google Sans', Roboto, Arial, sans-serif; font-size: 14px; font-weight: 500; color: #FFFFFF; text-decoration: none; border-radius: 100px;">
+ Open Resend Dashboard
+ </a>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+
+ </table>
+
+ <table border="0" cellspacing="0" cellpadding="0" style="max-width: 600px; width: 100%; margin-top: 32px;">
+ <tr>
+ <td align="center" style="padding-bottom: 24px;">
+ <p class="text-secondary" style="margin: 0; font-family: Roboto, Arial, sans-serif; font-size: 12px; color: #747775; line-height: 1.6;">Automated alert by Notifier Bot.<br>Please do not reply to this unmonitored email.</p>
+ </td>
+ </tr>
+
+ <tr>
+ <td align="center" style="padding-bottom: 16px;">
+ <table border="0" cellspacing="0" cellpadding="0">
+ <tr>
+ <td style="padding: 0 12px;">
+ <a href="https://amit.is-a.dev" target="_blank" style="text-decoration: none;" title="Website">
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="#747775" class="icon-fill" xmlns="http://www.w3.org/2000/svg" style="display: block; border: 0;">
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>
+ </svg>
+ </a>
+ </td>
+ <td style="padding: 0 12px;">
+ <a href="https://github.com/notamitgamer" target="_blank" style="text-decoration: none;" title="GitHub">
+ <svg width="20" height="20" viewBox="0 0 16 16" fill="#747775" class="icon-fill" xmlns="http://www.w3.org/2000/svg" style="display: block; border: 0;">
+ <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
+ </svg>
+ </a>
+ </td>
+ <td style="padding: 0 12px;">
+ <a href="mailto:amitdutta4255@gmail.com" style="text-decoration: none;" title="Email">
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="#747775" class="icon-fill" xmlns="http://www.w3.org/2000/svg" style="display: block; border: 0;">
+ <path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/>
+ </svg>
+ </a>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+
+ <tr>
+ <td align="center">
+ <p class="text-secondary" style="margin: 0; font-family: Roboto, Arial, sans-serif; font-size: 12px; color: #747775;">
+ © 2025-2026 Amit Dutta • <a href="https://amit.is-a.dev" class="text-secondary" style="color: #747775; text-decoration: none;">amit.is-a.dev</a>
+ </p>
+ </td>
+ </tr>
+ </table>
+
+ </td>
+ </tr>
+ </table>
+
+</body>
+</html>
+ `;
+
+ const payload = {
+ from: `Notifier Bot <${forwardingAddress}>`,
+ to: [personalGmail],
+ reply_to: originalSender,
+ subject: subject,
+ html: notificationHtml
+ };
-app.delete('/api/mails/:id', async (req, res) => {
- try {
- const { id } = req.params;
- await mailsCollection().doc(id).delete();
+ const { data, error } = await resend.emails.send(payload);
- mailDetailCache.delete(id);
- if (mailListCache) {
- mailListCache = mailListCache.filter(m => m.id !== id);
+ if (error) {
+ console.error(error);
+ return res.status(500).json({ error: error.message });
}
- res.json({ success: true });
+ return res.status(200).json({ success: true, id: data?.id });
+
} catch (error) {
console.error(error);
- res.status(500).json({ error: 'Failed to delete mail' });
+ return res.status(500).json({ error: 'Internal Server Error' });
}
});
-// --- Live updates ---
-app.get('/api/stream', (req, res) => {
- res.writeHead(200, {
- 'Content-Type': 'text/event-stream',
- 'Cache-Control': 'no-cache',
- Connection: 'keep-alive',
- });
- res.write('\n');
- sse.addClient(res);
-});
-
app.get('/ping', (req, res) => {
res.status(200).send('Server is awake!');
});
app.listen(port, () => {
- sse.startHeartbeat();
- console.log(`Mailbox server listening on port ${port}`);
+ console.log(`Email router listening on port ${port}`);
});
diff --git a/src/firebase.js b/src/firebase.js
@@ -1,27 +0,0 @@
-const admin = require('firebase-admin');
-
-// --- FIREBASE SETUP ---
-// Same pattern as WhatsApp-Logger-Self-Hosted-: pass the full service account
-// JSON as a single env var on Render (FIREBASE_SERVICE_ACCOUNT), or fall back
-// to a local serviceAccountKey.json for local dev.
-let serviceAccount;
-try {
- if (process.env.FIREBASE_SERVICE_ACCOUNT) {
- serviceAccount = JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT);
- } else {
- serviceAccount = require('../serviceAccountKey.json');
- }
-
- admin.initializeApp({
- credential: admin.credential.cert(serviceAccount)
- });
- console.log('System: Firebase Admin initialized successfully.');
-} catch (error) {
- console.error('System Error: Failed to initialize Firebase. Make sure FIREBASE_SERVICE_ACCOUNT env var is set.');
- console.error(error.message);
- process.exit(1);
-}
-
-const db = admin.firestore();
-
-module.exports = { admin, db };
diff --git a/src/sseManager.js b/src/sseManager.js
@@ -1,25 +0,0 @@
-// --- SSE CONNECTION MANAGER ---
-// Mirrors the pattern used in WhatsApp-Logger-Self-Hosted-/src/sseManager.js
-const clients = new Set();
-
-function addClient(res) {
- clients.add(res);
- res.on('close', () => clients.delete(res));
-}
-
-function broadcast(event, data) {
- const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
- for (const res of clients) {
- try { res.write(payload); } catch (e) { /* client gone */ }
- }
-}
-
-function startHeartbeat() {
- setInterval(() => {
- for (const res of clients) {
- try { res.write(': ping\n\n'); } catch (e) { /* client gone */ }
- }
- }, 25000);
-}
-
-module.exports = { addClient, broadcast, startHeartbeat };