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):
- Have/create a Gmail account.
- In Google Cloud Console, create a project.
- Enable Gmail API instead of Sheets.
- On OAuth consent screen, add scope
https://mail.google.com/so scripts can read/send mail. - 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/.zipattachments. - Optional
ccandbcckeyword 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
GmailThreadandGmailMessageobjects. GmailThread.messagesis a list ofGmailMessageobjects.
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.comfor 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(notGET). - 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,hallowed, e.g.2h30m).since=1737866912– since given epoch timestamp.since=wZ22cjyKXw1F– after message with thatid.
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.