This post is designed to serve as a brief technical overview of a recent feature added to ServiceSpark, a community service management platform I develop as a volunteer for the United Way of Albany County.
ServiceSpark uses email to send email notifications to volunteers about new events and new comments on events that the volunteer is connected to. The email includes a link back to ServiceSpark.org, and encourages the user to RSVP and comment on the event. Unfortunately, however, this requires a click, a login, and users rarely follow through with the process.
Use email as an interface for ServiceSpark, allowing users to “reply” to an email to leave comments, or allow users to RSVP using their client’s native calendar support.
My implementation of this required some sort of way to generate unique reply email addresses for each email that was sent, and a way to make note of the reply address, so that replies, if any, can be processed.
Dealing with the reply is also problematic. Many replies include the chain of emails behind the reply, or signatures. These artifacts need to be stripped from the application, or else the comments will become cluttered.
Emails also need to be attached to the user’s ServiceSpark identity. Every message should come in and appear as if the volunteer logged into ServiceSpark and created the comment, or submitted an RSVP manually.
But, finding a way to receive email at any possible address seemed challenging. For starters, standardizing a way to communicate with an email platform is difficult. Hijacking qmail, or some other mail queue is just tedious and feels like a kludge.
Receiving emails turned out to be easy with Mandrill. Mandrill has an “incoming message API” that allows for your application to receive email via webhooks.
Briefly, this is how Mandrill works
- Set up a custom reply domain. This is a DNS MX entry that will set Mandrill as handler for a custom domain. This takes about 5 minutes.
- Set up a route from your Mandrill dashboard. This maps incoming messages to a webhook on your server. For mine, I set up a wildcard, so that all addresses get sent to the webhook.
- Emails received by Mandrill will be pushed as a JSON object to your server in nearly real time.
Since I’m using CakePHP, which is MVC, I set up a controller that is a singularized endpoint for all incoming webhooks. A token is used as a shared secret for the application and Mandrill.
- Application receives a POST at /webhooks/incoming/<token>
- Application fires an event `Webhook.Incoming.<token>`
- An event handler is set to listen to `Webhook.Incoming.<token>` and parse the incoming data.
Once the webserver was receiving messages from Mandrill, I began work to parse the messages. Dealing with the “junk” in email, like signatures, and threads was a huge requirement. Posting this information publicly would clutter the application greatly, and annoy users.
GitHub to the rescue–literally! GitHub wrote an email reply parsing library, and the library has been ported to PHP. Including this library and parsing the text from the Mandrill request was trivial.
Mapping the incoming email address to a specific action is accomplished by a database table that has fields for GUID, user_id, event_type, and event_data.
- Emails are generated with special GUID@myreplydomain.org Reply-to addresses.
- GUIDS and the corresponding event information are saved to the database.
- User receives an email. If they would like to respond, they may do so using their email client. Replies go to <guid>@myreplydomain.org.
- Emails are received by Mandrill, parsed and sent as a JSON object to a webhook on my web server.
- The server looks up the guid and processes the email appropriately.
At this point, email to comment is working beautifully. The next challenge is sending valid meeting requests and processing the responses into the application.
Standards are your friend.
The global standard for calendar data exchange is ICS aka ICAL. This text format specifies events, and recipients, and metadata to allow loosely-coupled applications to synchronize state (like meeting cancellations).
There is a protocol to using ICS/ICAL to exchange event information.
ICS/ICAL crash course
- Lines are allowed to be 75 characters long. If your line needs to wrap, the next line should start with a space. Ideally, you should construct your file, and then wrap the lines one at a time at the end.
- ICS files start with BEGIN:VCALENDAR and end with END:VCALENDAR (calendar boundaries)
- Key VCALENDAR fields are
- PRODID: a string describing application vendor and application that generated the ICS file. The format is -//vendor/product//LANGUAGE
- METHOD: a string describing the nature of the ICS/ICAL file. Common values include PUBLISH (for publishing event information), REQUEST (for requesting an RSVP), CANCEL (for cancelling an event)
- VERSION: the version of the ICS/ICAL standard used. This is commonly just 2.0.
- Events lie within calendar boundaries. The event boundaries are BEGIN:VEVENT and END:VEVENT
- Key VEVENT fields are
- SUMMARY: a title for your event
- DESCRIPTION: descriptive text about your event. Newlines should be replaced with the literal string “\n”
- DTSTART: the start of the event, ideally in UTC
- DTEND; the end of the event, ideally in UTC
- DTSTAMP: the time the ICS/ICAL file was generated
- UID: a unique identifier that can be used to reference the event in subsequent event updates or cancellations. This can be anything (URL, GUID, SHA-256 hash), but you need to record it, or you’re going to bungle the entire protocol.
- ATTENDEE: encoded metadata describing the recipient’s relation to the event. Subfields include…
- RSVP: true or false, depending on whether you want the recipient to respond (Google and Outlook will not show RSVP buttons without this)
- CN: the name of the recipient
- MAILTO: the email address of the recipient
- LOCATION: a string describing the location of the event
- URL: an absolute URL for the event. Subfields include…
- VALUE: the actual URL for the event
Example ICS/ICAL meeting invite file
BEGIN:VCALENDAR VERSION:2.0 PRODID:-//uwacwy/servicespark//EN METHOD:REQUEST BEGIN:VEVENT ATTENDEE;RSVP=TRUE;CN=Barack Obama:MAILTO:firstname.lastname@example.org UID:guid SUMMARY:Testing Markdown DESCRIPTION:#### Overview\nThis should print as markdown in emails.\n\n### # Subtext\nCurabitur cursus purus vestibulum\, venenatis nisi eu\, element um tortor. Sed at fringilla dolor\, at aliquam tortor. Morbi interdum lore m ipsum\, nec varius est porttitor tempus! Praesent sodales dolor sit amet feugiat accumsan. Suspendisse pulvinar convallis orci non semper. Nunc fa ucibus scelerisque metus\, vulputate eleifend ligula dictum at. LOCATION:no addresses specified ORGANIZER;CN=Contoso via My Service Site:MAILTO:email@example.com URL;VALUE=URI:http://1234testing.bradkovach.koding.io/servicespark/go/eve nts/view/7 DTSTART:20150905T235100Z DTEND:20150906T005100Z DTSTAMP:20150905T235307Z END:VEVENT END:VCALENDAR
Attaching a file like this to an outgoing message will cause (most) email clients to display RSVP buttons!
When an RSVP choice is selected, an ICS will be sent as a reply to the endpoint. The email address that sends the ICS response can not be treated as important, and should not be used to identify the recipient. For example, Google uses one notification endpoint for all of their users, and you will be unable to reliably determine who is RSVP’ing to the event. Instead, the UID should be used exclusively, or else updates to the event will cause duplicates, and all other manner of chaos.
Receiving the RSVP Reply
When Mandrill receives the reply, they will perform a POST to your specified webhook. The response will be an ICS file. The ICS file follows the same format as outlined above: it begins and ends with VCALENDAR boundaries, containing at least one VEVENT inside the VCALENDAR.
A number of parsers exist for robust ICS parsing, but we are not interested in anything beyond the latest response for the UID. When the ICS was sent, the UID and the incoming email address were saved. As a result, we can look up the UID based on the email that it came from. If an email is received, and there isn’t a valid link between that email and UID, then nothing will be done.
- Look up the UID based on email address. This returns the user, and the corresponding action (event RSVP modification, in this case). It is worth noting that these email events should expire eventually, so these email endpoints will automatically deactivate.
- Regex the incoming ICS response for the VEVENT region containing UID. This is not a multi-line regex. This will capture and return the entire VEVENT.
- Regex the VEVENT region for a valid going/not going/tentative response. Please note this is a multi-line regex. This will check each line and then capture and return the DECLINED, ACCEPTED, OR TENTATIVE state of the RSVP.
- Once we have determined the new state of the RSVP, update the user’s RSVP.
The pieces of this project demonstrate the beauty of event-driven programming. Using modern web development techniques like webhooks allow for decoupled applications to seamlessly interact with each other. Mandrill’s service integrates so smoothly with my application that the end result is an interface that is invisible, but robust. The end result is a incredibly rich interface to an application, where the user interacts and derives value from the application without even logging in.
Mandrill’s email service does not allow for the appropriate attachment headers (specifically `METHOD: REQUEST`) to be included in the message. As a result, Outlook (desktop and web) will not show RSVP buttons. Outlook (iOS and Android) perform according to specification and will present RSVP buttons.