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.

One session in the App Logs per email.

One session in the App Logs per email.

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: