Skip to main content

Chapter 20 - Sending Emails, Texts, and Push Notifications (Python)

Here's a concise but complete walkthrough of Chapter 20, following the chapter's structure with small examples.


Overview and safety warning

  • The chapter shows how to automate sending/reading Gmail, how to send SMS via email gateways, and how to send/receive push notifications with ntfy.
  • Strong recommendation: use a separate Gmail account for scripts to avoid bugs spamming/deleting from your real inbox.
  • For any new script, first do a dry run: comment out the send/delete calls and replace with print() so you can inspect what would happen.

Gmail API and EZGmail

Gmail's security/anti-spam makes plain smtplib/imaplib awkward; EZGmail wraps the official Gmail API to simplify Python access.

Enabling the API and init

Setup steps (same pattern as EZSheets):

  1. Have/create a Gmail account.
  2. In Google Cloud Console, create a project.
  3. Enable Gmail API instead of Sheets.
  4. On OAuth consent screen, add scope https://mail.google.com/ so scripts can read/send mail.
  5. Get credentials + token files, then in Python:
import ezgmail
ezgmail.init() # uses credentials, creates token, no error means success

You should treat the token file like a password; revoke compromised tokens by deleting the credential in Cloud Console and re-doing setup.

Sending mail

Basic send:

import ezgmail

ezgmail.send('recipient@example.com',
'Subject line',
'Body of the email')

With attachments:

ezgmail.send('recipient@example.com',
'Subject line',
'Body of the email',
['attachment1.jpg', 'attachment2.mp3'])

Notes:

  • Gmail may block repeated identical messages or .exe/.zip attachments.
  • Optional cc and bcc keyword arguments:
ezgmail.send('recipient@example.com',
'Subject line',
'Body',
cc='friend@example.com',
bcc='otherfriend@example.com,someoneelse@example.com')

Inspect which Gmail account the token is bound to:

import ezgmail
print(ezgmail.EMAIL_ADDRESS) # e.g. 'example@gmail.com'

Reading mail: threads and messages

  • Gmail groups related emails into threads.
  • EZGmail has GmailThread and GmailMessage objects.
  • GmailThread.messages is a list of GmailMessage objects.

Unread threads and summary:

import ezgmail

unread_threads = ezgmail.unread() # 25 most recent unread threads
ezgmail.summary(unread_threads)
# prints: "Al, Jon - Do you want to watch RoboCop this weekend? - Dec 09"

Accessing messages:

len(unread_threads)                    # e.g. 2
len(unread_threads[0].messages) # e.g. 2

msg = unread_threads[0].messages[0]
print(msg.subject) # 'RoboCop'
print(msg.body) # 'Do you want to watch RoboCop...'
print(msg.timestamp) # datetime(...)
print(msg.sender) # 'Al Sweigart <al@example.com>'
print(msg.recipient) # 'Jon Doe <jon@example.com>'

Get more than 25 unread threads:

threads = ezgmail.unread(maxResults=50)

Recent threads (read + unread):

recent_threads = ezgmail.recent()
recent_threads = ezgmail.recent(maxResults=100)

Searching mail

Use Gmail-style search queries with ezgmail.search().

result_threads = ezgmail.search('RoboCop')
ezgmail.summary(result_threads)

You can pass Gmail search operators:

  • 'label:UNREAD' – unread mail.
  • 'from:al@inventwithpython.com' – from specific address.
  • 'subject:hello' – subject contains "hello".
  • 'has:attachment' – messages with attachments.

Example:

threads = ezgmail.search('has:attachment')

Downloading attachments

Each GmailMessage has .attachments (list of filenames).

import ezgmail

threads = ezgmail.search('vacation photos')
msg = threads[0].messages[0]

print(msg.attachments)
# ['tulips.jpg', 'canal.jpg', 'bicycles.jpg']

msg.downloadAttachment('tulips.jpg') # to CWD
msg.downloadAllAttachments(downloadFolder='vacation2026')
# returns list of downloaded filenames

Existing files are overwritten by downloads.


SMS email gateways

  • SMS gateways let you send a text via email; format is number@gateway.
  • The gateway belongs to the recipient's carrier, e.g. 2125551234@vtext.com for Verizon.
  • Subject and body become the text body.
  • MMS gateways (different addresses) allow media and longer messages.

Some examples (from Table 20-1):

  • AT&T: number@txt.att.net (SMS), number@mms.att.net (MMS).
  • T-Mobile: number@tmomail.net (SMS/MMS).
  • Verizon: number@vtext.com (SMS), number@vzwpix.com (MMS).

Disadvantages:

  • No guarantee of timely delivery (or delivery at all).
  • No reliable failure notification.
  • Recipient can't reply back to your script.
  • Gateways may silently block you if you send "too many" messages.
  • A gateway that works today may stop tomorrow.

So SMS gateways are ok for occasional, non-urgent messages; for reliable/bulk SMS, use a paid service API (like Twilio), subject to local regulations.


Push notifications with ntfy

ntfy is a free HTTP pub-sub service for simple push notifications to phones/browsers.

  • Install ntfy app on your phone (Android/iOS) or open https://ntfy.sh/app to receive notifications.
  • Notifications are sent to topics (like chatroom names). Anyone can send/read for a topic.
  • Use a random, secret topic for your own notifications (treat it like a password); never send sensitive data (passwords, card numbers).

Sending notifications

Use requests.post():

import requests

requests.post('https://ntfy.sh/AlSweigartZPgxBQ42', 'Hello, world!')

Notes:

  • Use POST (not GET).
  • Messages are up to 4096 bytes.
  • Free accounts: 250 messages/day; flooding may get your IP temporarily blocked.
  • Paid accounts increase limits and allow reserved topics and posting restrictions.

Metadata: title, priority, tags

Send extra fields via HTTP headers:

import requests

requests.post(
'https://ntfy.sh/AlSweigartZPgxBQ42',
'The rent is too high!',
headers={
'Title': 'Important: Read this!',
'Tags': 'warning,neutral_face',
'Priority': '5',
},
)
  • Title – like an email subject.
  • Priority – 1–5 (default 3). It affects filtering, not delivery speed.
  • Tags – comma-separated keywords or emoji names (see ntfy docs for emoji list).

Receiving notifications in Python

Use requests.get() with /json?poll=1:

import requests

resp = requests.get('https://ntfy.sh/AlSweigartZPgxBQ42/json?poll=1')
print(resp.text)

Response is newline-separated JSON objects, not a single JSON array, so you must split lines and decode each with json.loads:

import json

notifications = []
for json_text in resp.text.splitlines():
notifications.append(json.loads(json_text))

print(notifications[0]['message']) # 'Hello, world!'
print(notifications[1]['message']) # 'The rent is too high!'

Important fields:

  • id – unique message ID.
  • time – Unix timestamp of creation.
  • expires – Unix timestamp when message will be deleted.
  • event'message', 'open', 'keepalive', or 'poll_request' (you usually care about 'message').
  • topic – topic name.
  • title – optional.
  • message – text content.
  • priority – 1–5 if present.
  • tags – list of strings if present.

time can be converted via datetime.datetime.fromtimestamp(...) (from Chapter 19).

You can add since parameter to filter messages:

  • since=10m – last 10 minutes (s, m, h allowed, e.g. 2h30m).
  • since=1737866912 – since given epoch timestamp.
  • since=wZ22cjyKXw1F – after message with that id.

Example URL:

https://ntfy.sh/AlSweigartZPgxBQ42/json?poll=1&since=10m

To reduce server load, poll at most once per minute or every few minutes; for real-time, use streaming (see ntfy docs).


Overall idea of the chapter

Chapter 20 covers automating communication: EZGmail wraps the Gmail API for sending/reading email (threads, messages, attachments, search operators), SMS email gateways provide free but unreliable texting via number@carrier, and ntfy offers simple HTTP-based push notifications (requests.post to send, requests.get with ?poll=1 to receive). Always use a dedicated script account, do dry runs before live sends, and treat tokens/topic names like passwords.