notamitgamer

It's my profile man, there is ...
Log | Files | Refs | Activity | README

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:
Mpackage.json | 6++++--
Dpublic/icon.svg | 5-----
Dpublic/index.html | 155-------------------------------------------------------------------------------
Dpublic/manifest.json | 17-----------------
Dpublic/sw.js | 23-----------------------
Dscripts/backfill-from-resend.js | 82-------------------------------------------------------------------------------
Mserver.js | 366++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Dsrc/firebase.js | 27---------------------------
Dsrc/sseManager.js | 25-------------------------
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()">&larr; 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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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;"> + &copy; 2025-2026 Amit Dutta &bull; <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 };
© notamitgamer • Site Built: 2026-09-05 01:53:16 UTC • git-mirror commit: c170d72 [view raw info]
Originally created with stagit • modified by notamitgamer
Forked from github.com/notamitgamer/git-mirror