Abstract / Overview
Objective: forward Gmail messages with a chosen label into a Telegram chat. Delivery: Telegram Bot API. Orchestration: Make.com scenario for no-code, plus a Google Apps Script option.
Assumption: single Gmail account, one target Telegram chat (personal or group). Messages are forwarded as plain text with minimal formatting. Attachments and inline images are optional extensions described later.
Outcome: a reliable email-to-chat bridge with filters, deduplication, and simple rate control.
Flow
![Telegram with make]()
Conceptual Background
- Gmail labels: Labels act as selectors. A filter applies labels on arrival. Automations watch that label, not the entire inbox.
- Telegram bot: A bot created via BotFather exposes
sendMessage
and related endpoints. Each destination chat has a numeric chat_id
.
- Polling vs push: Gmail push to third parties is constrained. Most practical flows poll: Make.com “Watch emails” or Apps Script time-driven triggers.
- Idempotency: Use immutable identifiers such as
Message-Id
header or Gmail thread/message IDs to avoid duplicate forwards.
- Formatting: Telegram supports MarkdownV2 and HTML subsets. Escape characters to prevent formatting errors.
- Privacy: Email may contain PII. Forward only fields required for operations. Avoid forwarding signatures or footers if not needed.
- Quotas and budgets: Gmail and Apps Script have daily quotas. Make.com is billed per operation. Telegram Bot API is free but rate-limited. Use a budget guard.
Step-by-Step Walkthrough
1. Prerequisites
- Gmail account with permission to create filters and labels.
- Telegram account.
- Make.com account (for the no-code path).
- Optional: Google Apps Script access for the code path.
2. Create the Telegram bot and capture chat ID
- Open Telegram. Start a chat with @BotFather.
- Run
/newbot
. Provide a name and a unique username ending in bot
.
- Copy the bot token. Format:
1234567890:AA...
. Save as TELEGRAM_BOT_TOKEN
.
- Start a chat with your bot. Send a simple
hi
.
- Obtain your
chat_id
. Two common paths:
- Add
https://api.telegram.org/bot<TELEGRAM_BOT_TOKEN>/getUpdates
in a browser and read the message.chat.id
.
- Or add the bot to a group and run the same
getUpdates
after posting a message in that group. Record the group’s chat.id
.
3. Prepare Gmail: label and filter
- In Gmail, create a label. Example:
ForwardToTelegram
.
- Create a filter: search criteria such as
from:[email protected]
or subject keywords.
- In filter actions, check Apply the label and select
ForwardToTelegram
. Optionally, check Never send it to Spam.
- Apply to matching conversations. New arrivals will carry the label.
4. No-code path with Make.com
- Create a scenario
- Module 1: Gmail → Watch Emails.
- Search:
Label
equals ForwardToTelegram
.
- Set Include to
Unread only
if you want single-pass behavior.
- Choose an interval. Example: every minute.
- Add a transformer
- Clean subject and body. Truncate body to a safe length, e.g., 3,000 characters.
- Optionally strip quoted text using a regex.
- Add a deduplication step
- Use Make Data Store with key
${messageId}
.
- If key exists, skip. If not, continue and store the key after send.
- Add a budget guard
- Data Store counter
tg_sends_monthly
.
- If
counter >= LIMIT
, route to an email or do nothing. Reset monthly.
- Send to Telegram
- Mark processed (optional)
- Add Gmail → Modify a Message. Mark as Read or add a
TelegramSent
label.
5. Code path with Google Apps Script
Use a time-based trigger. The script scans the label and posts new messages to Telegram. It stores processed Gmail message IDs in Script Properties to prevent duplicates.
Steps
- In Google Drive, create an Apps Script project.
- Paste the code from Section 4.2.
- Set
TELEGRAM_BOT_TOKEN
, CHAT_ID
, and GMAIL_LABEL
.
- Add a time-driven trigger. Example: every 5 minutes.
- Run once to grant permissions.
6. Handling attachments and rich content
- Start simple: body text only.
- For attachments, iterate
getAttachments()
and upload with sendDocument
or sendPhoto
.
- For HTML mail, convert to text or use
parse_mode: HTML
after sanitization.
7. Hardening
- Retries: Retry on HTTP 429 or 5xx with exponential backoff.
- Observability: Log each send with timestamp, thread ID, and Telegram response.
- Security: Store the bot token in Make Secrets or Apps Script Properties Service.
- Access control: Telegram bots cannot read private messages sent by others unless they initiate. For groups, ensure the bot is added and has permission to post.
Code / JSON Snippets
All snippets are minimal and ready to adapt.
1. Telegram sendMessage
with cURL
Use for quick validation.
# Send a test message
export TELEGRAM_BOT_TOKEN="YOUR_TELEGRAM_BOT_TOKEN"
export CHAT_ID="YOUR_CHAT_ID"
curl -sS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-H "Content-Type: application/json" \
-d "{\"chat_id\":\"${CHAT_ID}\",\"text\":\"Gmail → Telegram test\",\"disable_web_page_preview\":true}"
2. Google Apps Script: poll a Gmail label and forward to Telegram
Stores processed message IDs to avoid duplicates and escapes MarkdownV2.
/**
* Gmail label → Telegram forwarder
* Install a time-driven trigger: every 5 minutes.
*/
const TELEGRAM_BOT_TOKEN = 'YOUR_TELEGRAM_BOT_TOKEN';
const CHAT_ID = 'YOUR_CHAT_ID';
const GMAIL_LABEL = 'ForwardToTelegram';
const MAX_BODY_CHARS = 3000;
function escapeMdV2(s) {
return String(s || '').replace(/[_*[\]()~`>#+\-=|{}.!]/g, '\\$&');
}
function forwardLabeledEmails() {
const props = PropertiesService.getScriptProperties();
const seen = new Set((props.getProperty('SEEN_IDS') || '').split(',').filter(Boolean));
const label = GmailApp.getUserLabelByName(GMAIL_LABEL) || GmailApp.createLabel(GMAIL_LABEL);
const threads = label.getThreads(0, 50); // recent threads first
const sentIds = [];
threads.forEach(thread => {
const msgs = thread.getMessages();
msgs.forEach(msg => {
const id = msg.getId();
if (seen.has(id)) return;
const from = msg.getFrom();
const subject = msg.getSubject() || '(no subject)';
const plain = msg.getPlainBody() || msg.getBody().replace(/<[^>]+>/g, ' ');
const body = plain.trim().slice(0, MAX_BODY_CHARS);
const text =
'*New labeled email*\n' +
'From: ' + escapeMdV2(from) + '\n' +
'Subj: ' + escapeMdV2(subject) + '\n' +
'Label: ' + escapeMdV2(GMAIL_LABEL) + '\n' +
'---\n' +
escapeMdV2(body) + '\n' +
'Link: https://mail.google.com/mail/u/0/#inbox/' + thread.getId();
const url = 'https://api.telegram.org/bot' + TELEGRAM_BOT_TOKEN + '/sendMessage';
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify({
chat_id: CHAT_ID,
text: text,
parse_mode: 'MarkdownV2',
disable_web_page_preview: true
}),
muteHttpExceptions: true
};
const resp = UrlFetchApp.fetch(url, options);
if (resp.getResponseCode() < 300) {
sentIds.push(id);
} else {
console.error('Telegram error', resp.getResponseCode(), resp.getContentText());
}
});
});
if (sentIds.length) {
const updated = new Set([...seen, ...sentIds]);
const trimmed = Array.from(updated).slice(-4000); // avoid unbounded growth
props.setProperty('SEEN_IDS', trimmed.join(','));
}
}
3. Make.com HTTP module body template (MarkdownV2)
Use when building the sendMessage request in Make.
{
"chat_id": "{{CHAT_ID}}",
"text": "*New labeled email*\nFrom: {{escape mdv2(from)}}\nSubj: {{escape mdv2(subject)}}\nLabel: ForwardToTelegram\n---\n{{escape mdv2(body_excerpt)}}\nLink: https://mail.google.com/mail/u/0/#inbox/{{gmailThreadId}}",
"parse_mode": "MarkdownV2",
"disable_web_page_preview": true
}
4. Minimal sample workflow JSON code
Portable spec for documentation and reproducibility.
{
"name": "gmail-label-to-telegram",
"assumptions": {
"single_label": "ForwardToTelegram",
"one_destination_chat": true,
"timezone": "UTC"
},
"trigger": {
"type": "gmail_watch_label",
"label": "ForwardToTelegram",
"interval_seconds": 60
},
"steps": [
{
"id": "dedupe",
"type": "datastore_check",
"key": "{{email.messageId}}",
"store": "seen_messages",
"on_exists": "abort"
},
{
"id": "compose",
"type": "template",
"format": "MarkdownV2",
"value": "*New labeled email*\\nFrom: {{escape mdv2 email.from}}\\nSubj: {{escape mdv2 email.subject}}\\nLabel: ForwardToTelegram\\n---\\n{{escape mdv2 email.body_excerpt}}\\nLink: https://mail.google.com/mail/u/0/#inbox/{{email.threadId}}"
},
{
"id": "budget_guard",
"type": "counter_check",
"counter": "tg_sends_monthly",
"limit": 500,
"window": "calendar_month",
"on_exceed": "route:drop"
},
{
"id": "send",
"type": "http_post",
"url": "https://api.telegram.org/bot{{secrets.TELEGRAM_BOT_TOKEN}}/sendMessage",
"json": {
"chat_id": "{{secrets.CHAT_ID}}",
"text": "{{steps.compose.output}}",
"parse_mode": "MarkdownV2",
"disable_web_page_preview": true
},
"retry": { "retries": 3, "backoff_seconds": 5 }
},
{
"id": "mark_seen",
"type": "datastore_put",
"store": "seen_messages",
"key": "{{email.messageId}}"
}
],
"secrets": {
"TELEGRAM_BOT_TOKEN": "YOUR_TOKEN",
"CHAT_ID": "YOUR_CHAT_ID"
}
}
5. Optional. attachments as Telegram documents in Apps Script
function forwardAttachments(msg) {
const atts = msg.getAttachments({includeInlineImages: false, includeAttachments: true});
atts.forEach(att => {
const url = 'https://api.telegram.org/bot' + TELEGRAM_BOT_TOKEN + '/sendDocument';
const options = {
method: 'post',
payload: {
chat_id: CHAT_ID,
caption: 'Attachment: ' + att.getName(),
document: Utilities.newBlob(att.getBytes(), att.getContentType(), att.getName())
},
muteHttpExceptions: true
};
UrlFetchApp.fetch(url, options);
});
}
Use Cases / Scenarios
- On-call alerts: Forward vendor or monitoring alerts labeled
P1
into an on-call Telegram group.
- Sales leads: Route lead capture emails from forms to a sales chat for faster response.
- Service desk triage: Convert labeled ticket notifications into a channel where agents already work.
- Compliance inbox: Forward only filtered subjects or senders. Keep the raw email in Gmail for audit.
- Executive digest: Label curated messages and forward summaries into a private Telegram chat.
Limitations / Considerations
- Gmail API and Apps Script quotas: Time-based triggers and UrlFetch calls have daily caps. Space polling to fit limits.
- Make.com operation costs: Each poll and HTTP call consumes operations. Set polling frequency to match urgency.
- Markdown escaping: Unescaped characters break messages. Always escape
_*[]()~\
>#+-=|{}.!` when using MarkdownV2.
- Attachment size: Telegram has file size limits. Large attachments will fail. Prefer links to the Gmail thread.
- Privacy and compliance: Remove signatures and disclaimers if not needed. Mask confidential data.
- Threading: Telegram chats are linear. Do not expect email-like threading. Use short prefixes for categorization.
- Reliability Add retries on 429 and transient failures. Log responses for audit.
- Group permissions. For groups, ensure the bot can post. Check slow mode and anti-spam settings.
- HTML emails: Rich HTML may not render well. Prefer plain-text excerpts.
Conclusion
A label-driven filter in Gmail and a Telegram bot produce a simple email-to-chat workflow. Make.com offers a fast, no-code path with dedupe and budgets. Apps Script provides a lightweight code solution inside Google Workspace. Start with plain text and a small polling interval. Add attachments and formatting only after the core path is stable.
Fixes (common pitfalls with solutions and troubleshooting tips)
- No Telegram delivery. Verify
chat_id
by calling getUpdates
after messaging the bot. Confirm the bot is added to the group.
- HTTP 401/403. Token is wrong or bot lacks permission. Regenerate the BotFather token and update secrets.
- Duplicate forwards. Add a Data Store or Script Properties set keyed by Gmail
Message-Id
. Do not rely on the subject line.
- Gmail label not found. Create the label exactly. Names are case sensitive in your script.
- Markdown errors like “can’t parse entities”. Escape all special characters or switch to
parse_mode: "HTML"
after sanitizing tags.
- Over-polling. Increase the interval. Batch process recent threads in pages.
- Large emails. Truncate the body to a fixed length and include the Gmail thread link.
- Attachments failing. Check Telegram file size. Use
sendDocument
and blobs in Apps Script.
- Make.com scenario idle. Check the connection, reauthorize Gmail, and ensure the label has new messages since the last run.
- Apps Script trigger not firing. Use a time-driven trigger, not an on-open trigger. Grant permissions on first run.