supercontest.comms.email
Email-related utilities. These functions all respect user notification preferences.
Built with the standard library’s email.message and delivered by Amazon SES
– the v2 SendEmail call, with the finished MIME as raw content – from the
league’s own domain, which SES signs with DKIM. SES authenticates the caller by
its IAM role, so there is no mail credential anywhere: not in the settings, not
in Secrets Manager. Every message names a configuration set, which is what
routes bounces and complaints to their SNS topic.
One message per recipient, each addressed To that recipient alone. SES caps
a message at 50 recipients, and mail addressed to its reader is what the
mailbox providers’ bulk-sender rules expect; a Bcc blast is neither.
boto3 blocks, so the async senders below hand send_mail() to a worker
thread; the sync ones are called from one.
A BROADCAST CAN OUTLAST ITS REQUEST. SES accepts this account’s mail at 14
messages a second (aws sesv2 get-account, MaxSendRate), and sends are
paced under that (SEND_RATE_PER_SECOND), so a full list – 655
addresses at the most, in September 2026 – takes about 55 seconds however
many are in flight, and the relays reach the app through CloudFront, which
gives up on an answer after 60. So each broadcast takes defer: the
route passes its BackgroundTasks.add_task, the recipients are read and the
once-only claim is committed in the request, and the send runs after the
response (202). Without defer – a script, a test – it sends in the call.
Module Attributes
A link in a message body, which |
|
How the SES client waits and retries. |
|
How many messages are in flight to SES at once, so one slow reply does not hold up the rest. |
|
Messages started per second, by every send in the process. |
|
What a redirected message's subject starts with ( |
|
Where the HTML part's notice goes: just inside its |
|
Runs a function after the response: FastAPI's |
Functions
One finished message, addressed |
|
Ends the read before a broadcast, claiming it first when it must go out only once. |
|
Sends each message to the one address in its |
|
Emails an image of the table of all the league picks for the week. |
|
Tells an address that registered again that it already has an account. |
|
Sends the address-confirmation link. |
|
Emails a user a list of teams. |
|
Emails all subscribed users that lockdown is about to happen. |
|
Emails the league's subscribers that new lines have been posted. |
|
Sends the password-reset link. |
|
The footer every notification carries. |
|
The line a redirected message starts with. |
|
Sends one message to each recipient. |
|
The SES client in the region the sending identity is verified in. |
Classes
Hands out start times at most |
Exceptions
Not one message of a send was delivered: the transport itself is failing. |
- supercontest.comms.email.BODY_TAG = re.compile('<body\\b[^>]*>', re.IGNORECASE)
Where the HTML part’s notice goes: just inside its
<body>, if it has one.
- supercontest.comms.email.Defer
Runs a function after the response: FastAPI’s
BackgroundTasks.add_task.alias of
Callable[[…],Any]
- supercontest.comms.email.LINK = re.compile('https?://\\S+')
A link in a message body, which
send_mail()logs in dev instead of sending.
- exception supercontest.comms.email.MailDeliveryError
Bases:
ExceptionNot one message of a send was delivered: the transport itself is failing.
- class supercontest.comms.email.Pacer(rate: float, clock: ~collections.abc.Callable[[], float] = <built-in function monotonic>, sleep: ~collections.abc.Callable[[float], None] = <built-in function sleep>)
Bases:
objectHands out start times at most
ratea second, across threads.Each
wait()takes the next free slot – now, or one interval after the last slot taken – and sleeps until it, outside the lock, so the threads wait in parallel and start in turn.- wait() None
Blocks until this caller’s slot.
- supercontest.comms.email.REDIRECT_SUBJECT_PREFIX = '[us-west-2 shadow] '
What a redirected message’s subject starts with (
Settings.mail_redirect_to).
- supercontest.comms.email.SEND_CONCURRENCY = 8
How many messages are in flight to SES at once, so one slow reply does not hold up the rest. How many START each second is
SEND_RATE_PER_SECOND.
- supercontest.comms.email.SEND_RATE_PER_SECOND = 12
Messages started per second, by every send in the process. SES refuses calls past the account’s
MaxSendRate– 14 a second (aws sesv2 get-account) – and the client’s five attempts, all spent against a steady excess, would leave recipients unsent. 12 leaves room for the one-off mails (a confirmation, a reset) sent meanwhile; the retries stay, for what pacing cannot see (another process, SES’s own jitter).
- supercontest.comms.email.SES_CLIENT_CONFIG = <botocore.config.Config object>
How the SES client waits and retries.
standardretries throttling, which is what exceeding the account’s per-second send rate returns – one call per recipient can reach it – and gives up after five attempts.
- supercontest.comms.email.build_message(settings: Settings, subject: str, body: str, recipient: str, png_attachment: tuple[str, bytes] | None, html: str | None) EmailMessage
One finished message, addressed
Toone recipient.- Parameters:
settings – The sender.
subject – The subject.
body – The plain text.
recipient – The one address.
png_attachment – A PNG to attach: its file name and its bytes.
html – The HTML alternative, if any.
- Returns:
The message.
- async supercontest.comms.email.claim_then_release(session: AsyncSession, job: Literal['picks_open', 'picks_closing', 'all_picks'], season: int, week: int, recipients: int, once: bool) bool
Ends the read before a broadcast, claiming it first when it must go out only once.
The claim is committed before anything is sent, so a relay’s retry that arrives while the first attempt is still sending finds it and stops. A failed send is not retried: better one week without a reminder than the league mailed twice.
- Parameters:
session – The session the recipients were read through.
job – Which broadcast.
season – Season year.
week – Week number.
recipients – How many addresses it is going to.
once – False to send regardless – an admin resending by hand.
- Returns:
Whether to send.
- supercontest.comms.email.deliver(settings: Settings, messages: list[EmailMessage]) int
Sends each message to the one address in its
Toheader, through the one client,SEND_CONCURRENCYat a time and no faster thanSEND_RATE_PER_SECOND.A message SES refuses is logged and skipped, so one bad address cannot stop the rest of a broadcast; the caller learns only if nothing got through.
- Parameters:
settings – The sender, the region and the configuration set.
messages – The finished messages, one per recipient.
- Raises:
MailDeliveryError – If every message failed.
- Returns:
How many SES accepted.
- async supercontest.comms.email.email_all_picks(session: AsyncSession, settings: Settings, season: int, week: int, png: bytes, defer: Callable[[...], Any] | None = None) bool
Emails an image of the table of all the league picks for the week.
- Parameters:
session – The session, for the subscribers.
settings – The app’s settings.
season – Goes in the subject of the email.
week – Goes in the subject of the email.
png – The image, as PNG bytes.
defer – Send after the response through this; see the module docstring.
- Returns:
Whether it was sent, or queued to be; False if this week’s already went out.
- supercontest.comms.email.email_already_registered(settings: Settings, recipient: str, link: str) None
Tells an address that registered again that it already has an account.
- Parameters:
settings – The app’s settings.
recipient – The address that registered.
link – A password-reset link, for an owner who has forgotten theirs.
- supercontest.comms.email.email_confirm_link(settings: Settings, recipient: str, link: str) None
Sends the address-confirmation link.
- Parameters:
settings – The app’s settings.
recipient – The address being confirmed.
link – The confirmation link.
- async supercontest.comms.email.email_picks(session: AsyncSession, settings: Settings, season: int, week: int, teams: list[str], emails: list[str], email_admin: bool = True) None
Emails a user a list of teams.
This is usually to notify the single user that their late picks have been submitted.
- Parameters:
session – The session, for the admins’ addresses.
settings – The app’s settings.
season – Goes in the subject of the email.
week – Goes in the subject of the email.
teams – The names of the teams to send.
emails – The regular recipients of the email.
email_admin – If
True, sends an identical email to notify the admin.
- async supercontest.comms.email.email_picks_closing(session: AsyncSession, settings: Settings, season: int, week: int, unfinished_pickers: list[str], defer: Callable[[...], Any] | None = None) bool
Emails all subscribed users that lockdown is about to happen.
- Parameters:
session – The session, for the subscribers and the admins.
settings – The app’s settings.
season – Goes in the subject of the email.
week – Goes in the subject of the email.
unfinished_pickers – All users who have not submitted 5 picks. This function does not calculate it, of course - it’s just passed.
defer – Send after the response through this; see the module docstring.
- Returns:
Whether it was sent, or queued to be; False if this week’s already went out.
- async supercontest.comms.email.email_picks_open(session: AsyncSession, settings: Settings, season: int, week: int, once: bool = True, defer: Callable[[...], Any] | None = None) bool
Emails the league’s subscribers that new lines have been posted.
The league, here and in the other two broadcasts, is
queries.get_users_with_email_pref()’s.- Parameters:
session – The session, for the subscribers.
settings – The app’s settings.
season – Goes in the subject of the email.
week – Goes in the subject of the email.
once – Send only if this week’s has not gone out; False to resend.
defer – Send after the response through this; see the module docstring.
- Returns:
Whether it was sent, or queued to be.
- supercontest.comms.email.email_reset_link(settings: Settings, recipient: str, link: str) None
Sends the password-reset link.
- Parameters:
settings – The app’s settings.
recipient – The address whose password is being reset.
link – The reset link.
The footer every notification carries.
- Parameters:
settings – The app’s settings, for the link.
- Returns:
Common email footer for the app.
- supercontest.comms.email.redirect_notice(recipients: int) str
The line a redirected message starts with.
- Parameters:
recipients – How many addresses the send was for.
- Returns:
The line.
- supercontest.comms.email.send_mail(settings: Settings, subject: str, body: str, recipients: list[str], png_attachment: tuple[str, bytes] | None = None, html: str | None = None) None
Sends one message to each recipient.
With
html, the message ismultipart/alternative:bodyfirst, as the plain text every client can show, then the HTML (email_html).With
Settings.mail_redirect_toset, the whole send is ONE message, to that address, its subject prefixedREDIRECT_SUBJECT_PREFIXand both its parts opened byredirect_notice(). This is the one function that reaches SES (tests/tooling/test_mail_redirect.py), so nothing bypasses it.- Parameters:
settings – The app’s settings. Nothing is sent in dev, nor in tests.
subject – The subject of the email.
body – The body of the email, as plain text.
recipients – Email addresses of all desired recipients. Duplicates get one copy.
png_attachment – A PNG to attach, if desired: its file name and its bytes.
html – The same message as HTML, if it has one.
- supercontest.comms.email.ses_client(settings: Settings) SESV2Client
The SES client in the region the sending identity is verified in.
The region is the settings’, never the environment’s: an identity, its production access and its configuration set exist in one region only.
- Parameters:
settings – The app’s settings.
- Returns:
The client.