Anvil Advent Calendar
Build a web app every day until Christmas, with nothing but Python!
Dear Santa…
Remember the wishlist app from earlier? You can create a Christmas present wishlist at
https://christmalist.anvil.app
Once you’ve logged in, you can add items to your wishlist and share it with your friends, who then buy you all the things and check them off the list.
Today we’re going to make it a little more convenient. We’re going to make it possible to add to your wishlist by email.
Unfortunately, you need to log in to add to your list, but you can’t ’log in’ when you send an email. Spoiler: we find a way to ensure the user is who they claim to be, and it’s called DKIM.
What it does
First, you send an email with your desired item in the subject line:
The item shows up at the bottom of your wishlist:
And the app sends you a reply:
How it works
Receiving emails
We created a function to handle the messages using the @anvil.email.handle_message
decorator:
@anvil.email.handle_message(require_dkim=True)
def message_handler(msg):
for to_address in msg.addressees.to_addresses:
if to_address.address.split('@')[0] == 'add':
# Messages to add@christmalist.anvil.app go to the 'add item' handler
add_item_by_email(msg)
break
else:
raise anvil.email.DeliveryFailure("That email address is not recognised.")
All emails that are sent to christmalist.anvil.app
get handled by this function. We’re only interested in emails
to add@christmalist.anvil.app
, so we check the part before the @
and if it’s add
, we run add_item_by_email
.
If someone sends an email to <anything else>@christmalist.anvil.app
, we raise a DeliveryFailure
, which lets the user
know this address is not valid.
‘Logging in’
So how do we log the user in? We can’t show the user a login form if they’re interacting with the app via email. The email might appear to come from a particular address, but that doesn’t tell us anything - a malicious grinch might spoof the ‘from’ address to add items to other peoples’ lists!
Luckily, modern email systems sign emails using a DKIM signature. The DKIM
signature ensures that the email was signed by the owner of the domain - for example, if the
email comes from santa@gmail.com
and we’ve checked the DKIM signature, we know that Gmail sent the email, so
the legitimate owner of santa@gmail.com
must have been logged into Gmail to send it.
We’ve enabled DKIM checking by using require_dkim=True
in the anvil.email.handle_message
decorator. Now we can confidently
add items to our users’ wishlists as if they’re logged in.
@anvil.email.handle_message(require_dkim=True)
def message_handler(msg):
# ...
Processing the message
We’re keeping the message processing simple - the user writes what they want in the subject line of their email,
and we store that in their wishlist. The subject line is available as msg.subject
, so we just add that straight
into the database.
def add_item_by_email(msg):
print(f'Email submission received from: {msg.addressees.from_address.raw_value}')
user = app_tables.users.get(email=msg.addressees.from_address.address)
if user:
app_tables.wishlists.add_row(item=msg.subject, url=None, owner=user, purchaser=None, purchased=False)
msg.reply(
text="Thanks! That's been added to your wishlist.",
html="<h1>Thanks!</h1>That's been added to your wishlist.",
)
else:
msg.reply(
text=f"You don't currently have a wishlist on Christmalist. Sign up at {anvil.server.get_app_origin()}",
html=f'You don\'t currently have a wishlist on Christmalist.<br><br><a href="{anvil.server.get_app_origin()}">Sign up here!</a>',
)
We could parse the body of the email if we wanted to make things more complex - we have the html
and text
of the email in the msg
object.
The print
statement at the top prints a message to App Logs. We always get a session in the App Logs for each
email that’s sent to the app, so this is not required, but it’s nice to know that the control flow reached this function.
Exceptions show up in the App Logs too, which is really useful for debugging your email handler.
Clone it and build on it
This is a very flexible app design. It’s an example of a CRUD app - it Creates, Reads, Updates and Deletes items in a database. You just need to change a few names and forms to convert it to a range of uses - it could be an inventory tracker, a double-entry bookkeeping app, or a news aggregator.
Click this button to clone the source code.
Give the Gift of Python
Share this post: