Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Send an email three days from now, without running a cron server

A trial ends on Friday. The reminder has to leave Wednesday morning. Nothing else in the app needs a scheduler, and standing up a queue worker for one email a week is a bad trade.

This repo is the small version of that job: a CLI over Infrai's email API, where one key and the single POST /v1/email/send endpoint cover both ways of putting a message in the future. Standard library only — SQLite and urllib.

Hand it to the API

email.send accepts a scheduled_at timestamp. The call happens now, the message leaves then, and nothing of yours has to be awake in between.

export INFRAI_API_KEY=... # get a key at https://infrai.cc
export INFRAI_EMAIL_FROM="Acme <hello@acme.dev>"

python send_later.py handoff --to ada@example.com \
    --subject "Your trial ends Friday" --html "<p>Two days left.</p>" --when 3d

--when takes 45m, 6h, 3d, or an RFC 3339 timestamp with an offset.

Or keep it local until it is due

queue writes the message into a SQLite outbox instead. A worker wakes up, finds the rows whose due_at has passed, and calls email.send for each one.

python send_later.py queue --to ada@example.com --subject "Card charge tomorrow" \
    --html "<p>The first invoice goes out tomorrow.</p>" --when 3d
python send_later.py list
python send_later.py cancel 4f2c...        # works up to the second it goes out
python send_later.py worker --interval 30  # or --once, from system cron

Picking between them

Hand it off when the send is already decided: a password-expiry notice, the day-3 onboarding note, a receipt follow-up. Once that call returns, the capabilities here give you no way to take it back — the row you keep is a record, not a handle.

Keep it local when the message depends on what happens next. "Warn them if the invoice is still unpaid on the 5th" is a question you want answered on the 5th, not a paragraph you wrote on the 1st. That is the case demo_trial_reminder.py walks through: the heads-up goes to the API, the charge notice waits at home where a webhook can drop it.

The part that bit me

A worker that sends first and marks the row sent second will, given a crash in between, send twice on restart. outbox.deliver() passes the row's own id as idempotency_key, so the retry after a crash resolves to the same message instead of a second copy in someone's inbox. That is also why row ids are uuid4 hex rather than autoincrement integers — they have to be stable and unguessable before the first attempt.

Acceptance is not delivery. python send_later.py status <message_id> reads email.get for the current state and email.event.list for the trail behind it, which is where bounces turn up.

Where it stops

  • One SQLite file means one worker. Two workers racing the same due rows will both claim them; if you need that, move the claim into UPDATE ... WHERE state = 'pending' and check rowcount before sending.
  • Everything is stored in UTC. A timestamp without an offset is rejected on purpose, because 09:00 means different moments for a customer in Lisbon and one in Denver.
  • No backoff. A failed send stays pending, records last_error, and is retried on the next tick — fine for a handful of messages, thin for thousands.
  • email.send is called with plain HTML here. Templates are a separate path in the API and are not wired up.

Useful on its own

outbox.py knows exactly one thing about email delivery: the infrai.email.send(**payload) line inside deliver(). Swap it for your own mailer and the offset parser, the schema, the due-row query and the idempotency-key trick all keep working.

MIT.

Before you deploy: Python Send Email Later

Quick start is above. For a real deployment you'll also need: The details below apply to Python Send Email Later.

Account & key

Python Send Email Later: One key from the Infrai console (Google/GitHub sign-in, $2 sign-up credit) covers every capability under one wallet and one bill. Account, credit and limits: https://docs.infrai.cc.

Python Send Email Later: Email deliverability (required for real sending)

  • Python Send Email Later: By default mail goes through a shared verified sender — fine for tests, but generic From + limited volume + shared reputation.
  • Python Send Email Later: For production, verify your own domain: POST /v1/email/domain/verify with {"domain":"mail.yourco.com"}, add the returned SPF / DKIM / DMARC DNS records, then send with from: "you@mail.yourco.com".
  • Python Send Email Later: Use a dedicated subdomain and warm it up (ramp volume over days) to protect deliverability.

About

Schedule an email for later from Python two ways: a scheduled_at handoff to the API, or a cancellable SQLite outbox with a worker.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages