Gmail to Telegram: A Client Alert You Can Trace

Abstract tech illustration: Gmail to Telegram: A Client Alert You Can Trace

You're between client calls, you reopen Gmail to check whether one specific reply landed, and ten minutes later you're three threads deep in something unrelated. The fix isn't a smarter inbox. It's a narrow rule that matches one kind of email, a Telegram message that tells you to look, and a record you can audit when something goes wrong.

Here's exactly how I'd build that, including the two cases most demos skip: what happens when delivery fails, and what happens when the same email shows up twice.

Define the alert before you connect anything

The alert is two inspectable conditions: an expected sender and an expected subject marker. Nothing here replies to the client, changes a deal, or approves a payment. Notification and decision stay separate: the system tells you to look, and you decide.

For the test setup:

  1. In Gmail, create a label called Client Alerts.
  2. Create a filter for a test sender you control, with the subject containing ALERT-TEST, that applies the label.
  3. Send the test message from a different account, so the test looks like a real incoming message.

For a real client, the rule becomes a known client address plus an agreed subject phrase. Keep it boring on purpose. If you can't read the rule out loud in one sentence, it's too clever for a first version.

One caution: a sender address alone is not proof a message is trustworthy. Display names and addresses can be spoofed. So the alert text should say "Client email arrived, go look," never "invoice approved, pay now." The notification must never authorize a payment or a sensitive change.

There's no AI model in this version, and that's deliberate. A classifier adds another place for the workflow to be wrong before you've even proven the trigger, the destination, and the record. Add one later, once you can trace every alert end to end.

Connect Gmail with read-only access

The worker is a small Python script using the Gmail API, Telegram's Bot API, and a local SQLite file. If you already use a hosted connector instead, follow its own setup screens, because labels and auth flows differ. The Gmail-side facts below apply to any custom build.

The setup:

  • Enable the Gmail API in a Google Cloud project.
  • Create an OAuth client for a desktop application.
  • Configure the consent screen and add your own account as a test user while the app is in testing.
  • Download the client configuration to the machine that runs the worker.
  • Request the Gmail read-only scope.

On first authorization, sign into the exact mailbox you want monitored, and check the address before approving. A successful login to the wrong Google account gives you a perfectly working workflow watching the wrong inbox.

Read-only means the worker can't send or delete mail through that permission. It can still read mailbox data. The label filter does not narrow the OAuth permission to that label. Keep that distinction straight when you explain the setup to a client.

# Keep secrets out of the repo and out of shell history
export TELEGRAM_BOT_TOKEN="..."   # set via your secrets manager or a .env outside git
export TELEGRAM_CHAT_ID="..."
# Gmail token lives outside the repository, e.g. ~/.config/client-alerts/token.json

Don't paste credentials into an AI chat, and keep them off-screen if you're recording. For an unattended client deployment, review the Google application's publishing status and token behavior first. A test-mode authorization working today is not the same as production authentication that survives for months.

Poll or push: pick the failure mode you can live with

Gmail gives you two ways to notice new mail, and each fails differently.

Push runs through Cloud Pub/Sub. Gmail doesn't call your endpoint directly; you register the mailbox with the watch method, and you can filter on labels. Google's push notification guide says a watch has to be renewed at least every 7 days or updates stop, and recommends calling watch once per day. A successful call returns the current mailbox historyId and an expiration timestamp.

The Pub/Sub message itself is tiny: an emailAddress and a historyId. It's a watermark, not the email. You call users.history.list with your previous historyId to learn what changed, then fetch each message. One third-party guide (Unipile) warns that watch expiry is silent: if your renewal job fails, mail keeps arriving and your app receives nothing. It also says the Gmail service account needs the Pub/Sub Publisher role on the topic, or watch succeeds but nothing is delivered. That comes from a third-party guide rather than Google's page, so test it yourself.

Polling is simpler and fails more visibly. In n8n, for example, the Gmail Trigger has a single event, Message Received, and polls on a Poll Time you choose, so an alert can arrive up to one poll interval late. Max Emails per Poll defaults to 10 with a maximum of 50; extras queue for the next cycle. A third-party review (Manuel Gomes) notes that a message that arrives and is deleted before the next poll is never seen, and recommends a periodic reconciliation step.

For a first client alert, I'd poll. A few minutes of lag is acceptable for "a client replied," and a poller that errors loudly beats a push channel that goes quiet without telling you.

from googleapiclient.discovery import build

def find_candidates(creds):
    gmail = build("gmail", "v1", credentials=creds)
    # Same query syntax as the Gmail search box; a space means AND
    q = 'label:"Client Alerts" subject:ALERT-TEST is:unread'
    resp = gmail.users().messages().list(userId="me", q=q, maxResults=50).execute()
    return [m["id"] for m in resp.get("messages", [])]  # list returns only id + threadId

messages.list returns only id and threadId, so you need a separate messages.get call for the sender, subject, and date. The maxResults default is 100 and the cap is 500, per the Python client docs.

Build the Telegram destination and test it alone

Create a bot through Telegram's official BotFather account and store the token as TELEGRAM_BOT_TOKEN. Then open the new bot's chat and send it a start message. A bot can't begin a private conversation with an arbitrary user, so skip this step and you'll get nothing.

Read the incoming update through the Bot API to find your chat ID, and store it as TELEGRAM_CHAT_ID. Keep the token out of visible URLs, terminal history, and logs.

Then send a fixed test message before involving Gmail. If that doesn't arrive, fix the destination first. Debugging two connections at once makes a simple failure hard to isolate.

According to the Telegram Bot API, sendMessage requires chat_id and text, and the text must be 1-4096 characters after entities parsing. There's an optional disable_notification parameter that sends silently, which is useful if you later add a low-priority tier.

import os, requests

def send_telegram(text: str):
    url = f"api.telegram.org]}/sendMessage"
    r = requests.post(url, json={"chat_id": os.environ["TELEGRAM_CHAT_ID"], "text": text}, timeout=15)
    return r

Keep the payload small and scannable: a "Client email" label, the sender, the subject (with the test marker visible), and the received time. Don't dump the email body into chat. That makes the alert longer, leaks more data into a second system, and risks hitting the length limit.

That limit matters. Per Conferbot's summary, an over-limit message isn't truncated; the API rejects it with Bad Request: message is too long, so any splitting is your job. Truncating the subject yourself to a fixed length avoids the problem entirely.

Record every attempt, and decide what duplicates and failures mean

This is the part that makes the workflow traceable. The record is a SQLite table keyed on the Gmail message ID. Gmail message IDs are immutable, which makes the ID a stable deduplication key and delivery-record key.

CREATE TABLE IF NOT EXISTS alerts (
  gmail_message_id TEXT PRIMARY KEY,
  sender           TEXT,
  subject          TEXT,
  matched_at       TEXT,
  status           TEXT,     -- pending | sent | failed
  attempts         INTEGER DEFAULT 0,
  last_error       TEXT,
  sent_at          TEXT
);

The loop: insert the message ID as pending before sending. If the insert fails because the ID already exists, you've seen this message, so skip it. That's your duplicate guard, and it survives a worker restart. Then attempt the Telegram send and update the row to sent or failed.

import sqlite3, time

def process(db, msg):
    try:
        db.execute("INSERT INTO alerts (gmail_message_id, sender, subject, matched_at, status) "
                   "VALUES (?,?,?,datetime('now'),'pending')",
                   (msg["id"], msg["sender"], msg["subject"]))
        db.commit()
    except sqlite3.IntegrityError:
        return  # already seen: no second alert

    r = send_telegram(f"Client email\nFrom: {msg['sender']}\nSubject: {msg['subject'][:200]}")
    if r.status_code == 200:
        db.execute("UPDATE alerts SET status='sent', sent_at=datetime('now'), attempts=attempts+1 "
                   "WHERE gmail_message_id=?", (msg["id"],))
    elif r.status_code == 429:
        wait = r.json().get("parameters", {}).get("retry_after", 5)
        db.execute("UPDATE alerts SET status='failed', attempts=attempts+1, last_error=? "
                   "WHERE gmail_message_id=?", (f"429 retry_after={wait}", msg["id"]))
        time.sleep(wait)
    else:
        db.execute("UPDATE alerts SET status='failed', attempts=attempts+1, last_error=? "
                   "WHERE gmail_message_id=?", (r.text[:300], msg["id"]))
    db.commit()

On a 429, Telegram returns a retry_after value in seconds, and your bot should wait at least that long before retrying; retrying sooner can extend the penalty (Conferbot). The Bots FAQ says to avoid more than one message per chat per second, that short bursts are tolerated but eventually produce 429s, and that groups are capped at 20 messages per minute. For a single-owner alert bot you'll rarely get near that, but a backlog flush after downtime can, so sleep between sends.

Retry is a separate pass: select rows where status='failed' and attempts is under your cap, resend, and update. Anything past the cap stays failed with its last_error, and that's your dead-letter list. Review it daily at first.

Be clear about what this key does not cover. It dedupes the same Gmail message. If a client forwards or resends an email, it arrives with a new message ID and will alert again. Whether that's a bug or correct behavior depends on your rule. If you want to collapse resends, you'd need a second key, such as sender plus normalized subject within a time window. I'd start without it, because a second alert you can ignore is cheaper than a missed one.

Test it like it will break

Run these in order, and watch the table after each:

  • Match: send ALERT-TEST from the other account. Expect one Telegram message and one sent row, with the subject and marker visible in both.
  • Non-match: send a message without the marker. Expect no alert and no row.
  • Duplicate: re-run the worker against the same message. Expect no second alert.
  • Delivery failure: set a wrong bot token and send a match. Expect a failed row with a populated last_error; restore the token and confirm the retry pass delivers it.
  • Overlong subject: use a very long subject and confirm your truncation prevents a rejected send.

If you chose push instead of polling, add one more: let the watch lapse in a staging mailbox and confirm your renewal monitoring tells you before mail goes unseen. For polling, schedule a periodic reconciliation that searches the label for messages missing from the table. That catches anything a poll skipped.

Where this fits in client work

The pattern that holds up is always the same: narrow trigger, small payload, a log you can read at 6 p.m. when someone asks "did that alert go out?" From here, the natural next steps are routing different client categories to different chats, and adding a model-based classifier only once the plain rules and the delivery record have proven stable. bizflowai.io is where I publish practical AI automation for solopreneurs and small teams. The goal stays the same as in this tutorial: you decide, the system just makes sure you know.


Want more like this?

I publish practical AI automation tutorials for solopreneurs and small teams.

Planning an AI automation project or need a second opinion on your architecture?

Connect with me on LinkedIn: Lazar Milićević, senior engineer building practical AI automation.

Visit bizflowai.io for more on practical AI automation.

Frequently asked questions

What is an email-to-Telegram alert workflow for small businesses?

It is a small system that watches Gmail for one specific category of email and sends a Telegram notification when a match arrives. The tutorial builds it with a Python worker, the Gmail API, Telegram's bot API, and a local SQLite database. It only alerts you to look. It does not reply to clients, change deals, or approve payments.

How do I set up a Gmail alert condition for a Telegram notification?

Create a dedicated Gmail label called Client Alerts. Then create a Gmail filter for a test sender you control and a subject containing ALERT-TEST, and have the filter apply the label. Send the test message from a different account so you test a true incoming email. The condition stays simple: an expected sender plus an expected subject marker.

How do I connect a Python worker to Gmail securely?

Enable the Gmail API in a Google Cloud project and create an OAuth client for a desktop application. Configure the consent screen and add your account as a test user if the app is in testing. Request the Gmail read-only scope and sign into the exact mailbox you want monitored. Store the resulting token outside your repository and keep credentials hidden.

Does the Gmail read-only scope limit the worker to one label?

No. Read-only means the worker cannot send or delete email through that permission, but it can still read mailbox data. The label filter does not restrict the OAuth permission to that label. Treat the token as access to the whole mailbox, and protect it accordingly.

Why should a first email alert workflow avoid using an AI model?

A first version needs only a precise trigger, a destination, and a record of what happened. Adding an AI classifier before those basics are defined creates another place for the workflow to make mistakes. A simple sender-and-subject rule is easy to inspect and verify. The tutorial treats an explanation of an email and a system that notices its arrival as different things.