LIVE SYSTEM

INBOX RADARv2.0


A zero-infrastructure Gmail → Telegram notification engine. Filters, deduplicates, and formats your inbox into structured Telegram alerts — running entirely inside Google Apps Script. No servers, no databases.

Google Apps Script Telegram Bot API Zero Infrastructure No External DBs Rule-based Filtering Daily Digest
00 BUG FIXES IN v2.0
🔴
CRITICAL CONFIG_KEYS · configManager
CONFIG_KEYS values were live credentials, not key names
BOT_TOKEN and CHAT_ID held the actual bot token and numeric chat ID as their values. Since CONFIG_KEYS is a map of property store key names, every set() and get() call used the raw token string as the key — so nothing was ever stored or retrieved correctly. This caused the "BOT_TOKEN and CHAT_ID are required" crash on every setup() call and the silent "Inbox Radar is disabled" skip on every trigger run.
BOT_TOKEN: "8734890310:AAHCwF2...", // ❌ actual secret as key name CHAT_ID: "5792120007", // ❌ actual ID as key name BOT_TOKEN: "BOT_TOKEN", // ✅ plain string key name CHAT_ID: "CHAT_ID", // ✅ plain string key name
🟠
HIGH emailFetcher._parseMessage
Dead function used wrong Gmail API (thread vs message)
_parseMessage(message) accepted a GmailMessage but internally aliased it as thread and called thread.getPlainBody() — a method that doesn't exist on GmailThread. Also used thread.getId() which returns a thread ID, not a message ID. The function was never called (fetch() inlined its own version) but left a correctness trap. Rewritten to accept (msg, thread) and now used in fetch().
function _parseMessage(message) { // ❌ wrong signature const thread = message; // ❌ aliased incorrectly const body = thread.getPlainBody(); // ❌ GmailThread has no getPlainBody() const msgId = thread.getId(); // ❌ returns thread ID, not message ID function _parseMessage(msg, thread) { // ✅ correct parameters id: msg.getId(), // ✅ GmailMessage.getId() bodyPreview: _cleanBody(msg.getPlainBody() || msg.getBody())
🔵
MEDIUM configManager.isEnabled
isEnabled() silently returned false before setup() ran
get("ENABLED") returns null before setup() is called. null === "true" is false, so every trigger run before initialization would silently log "Inbox Radar is disabled" — masking the real problem (setup not yet run). Added an explicit null check with a clear log message.
return get(CONFIG_KEYS.ENABLED) === "true"; // null → false, no warning const val = get(CONFIG_KEYS.ENABLED); if (val === null) return false; // explicit: not yet initialized return val === "true";
🔵
MEDIUM formatter.buildDigest
MarkdownV2 escape (\.) used inside legacy Markdown mode
buildDigest numbered list items with ${i + 1}\\. (MarkdownV2 escaped dot), but telegramSender.send() defaults to legacy Markdown mode. In that mode the backslash renders literally, so every digest item appeared as "1\. 🔐 Subject" instead of "1. 🔐 Subject".
lines.push(`${i + 1}\\. ${emoji} *${email.subject}*`); // ❌ literal \ lines.push(`${i + 1}. ${emoji} *${email.subject}*`); // ✅ plain dot
🟢
PERFORMANCE dedupeStore · emailFetcher.fetch
N PropertiesService reads in the fetch loop (one per message)
dedupeStore.has(id) called _load() internally, which hit PropertiesService every call. With 20 messages that's 20 separate API calls. Added dedupeStore.snapshot() to load the Set once and pass the reference through the entire loop.
// ❌ Inside loop: 20 PropertiesService reads if (dedupeStore.has(id)) { ... } // ✅ Load once before loop const processedSet = dedupeStore.snapshot(); if (processedSet.has(id)) { ... }
01 HOW IT WORKS
📬
Gmail
Inbox
🔍
Email
Fetcher
🗃
Dedupe
Store
⚙️
Rule
Engine
✏️
Formatter
✈️
Telegram
Bot
02 SETUP GUIDE
01
Create a Telegram Bot
Message @BotFather on Telegram → /newbot → copy your API token.
@BotFather → /newbot → set name: "Inbox Radar" → copy token: 123456:ABCdef...
02
Get your Chat ID
Send any message to your bot, then call getUpdates to find your chat_id.
https://api.telegram.org/bot<TOKEN>/getUpdates → find "chat": {"id": 987654321}
03
Open Google Apps Script
Go to script.google.com → New project → paste the full InboxRadar.gs file.
script.google.com → New Project → paste InboxRadar.gs → save as "Inbox Radar"
04
Run setup()
In the editor, select setup and run it with your credentials. Grant Gmail permissions when prompted.
// Run this from the editor — DO NOT hardcode in source setup("123456:ABCdef...", "987654321")
05
Install triggers
Call installTriggers() to start the 5-minute watcher and optional daily digest.
installTriggers(5, 8) // every 5 min + digest at 8 AM
06
Verify with a test notification
Confirm the full pipeline works end-to-end before walking away.
sendTestNotification() // ✅ Telegram should receive it instantly
03 ALERT FORMAT
🔐 INBOX RADAR ALERT

🏷 Type: OTP & Security
⚡ Priority: 🚨 CRITICAL

👤 From: security@github.com
📌 Subject: Your GitHub login code

🧠 Preview:
Your one-time authentication code is 847291. It expires in 15 minutes. If you didn't request this...

Time: Apr 28, 2026 · 14:32

─────────────────────
Sent by Inbox Radar
14:32 ✓✓
04 ARCHITECTURE
⚙️
configManager
Script Properties KV store. All config in one place.
🗃
dedupeStore
Persistent ID set (cap 2,000). Never sends duplicates.
📬
emailFetcher
Gmail query with batching, body cleaning, error isolation.
🧠
ruleEngine
JSON rules. Subject / From / Body matching + fallback.
✏️
formatter
Telegram Markdown builder + daily digest formatter.
✈️
telegramSender
Bot API with 3× retry, rate-limit backoff, truncation.
scheduler
ScriptApp triggers — watcher + digest management.
📊
digestEngine
Accumulates the day's emails, sends morning summary.
🔍
debugLogger
Toggle-able verbose logging — zero code changes needed.
🎛
controlPanel
Public API: setup, enable, disable, setRules, status.
05 RULE ENGINE
🔐OTP & SecurityCRITICAL
subjectContains: ["otp", "verification code", "2fa", "login code"]
fromContains: [] · silent: false
💳Finance & BankingHIGH
subjectContains: ["invoice","payment","transaction","receipt"]
fromContains: ["paypal","stripe","revolut","wise"]
💼Jobs & RecruitingHIGH
subjectContains: ["recruiter","interview","offer letter"]
fromContains: ["linkedin","greenhouse.io","lever.co"]
📦SaaS & SubscriptionsMEDIUM
subjectContains: ["subscription","trial ending","plan renewal"]
keywordContains: ["expires","renews","trial ends"]
📅Calendar & MeetingsMEDIUM
subjectContains: ["meeting","calendar","zoom","google meet"]
fromContains: ["calendar-notification","no-reply@calendar"]
📰NewslettersLOW · SILENT
fromContains: ["newsletter","digest","substack","mailchimp"]
keywordContains: ["unsubscribe"] · silent: true
+ fallback "General 📩" rule for unmatched emails. Customize via setRules(json).
06 PUBLIC API
// ── Setup ───────────────────────────────────────────────────── setup("BOT_TOKEN", "CHAT_ID") // initialize + test bot connection installTriggers(5, 8) // 5-min watcher + 8am daily digest sendTestNotification() // verify end-to-end pipeline // ── Control ─────────────────────────────────────────────────── enable() / disable() // pause without removing triggers toggleDebug() // verbose logging on/off status() // full health check report // ── Rules ───────────────────────────────────────────────────── setRules(rulesJson) // replace active rules (validates first) getRules() // print current rules to log // ── Maintenance ─────────────────────────────────────────────── reset() // wipe all config + triggers clearDedupeStore() // allow re-processing old emails setScanQuery("is:unread label:inbox") // custom Gmail query setMaxBatch(30) // emails per run (default 20)