Tutorial
Generating Ical files with Phoenix and Swoosh
At some point, most apps need to send calendar events and you have probably noticed that there is a standard that all major calendar applications work with. That is the ical format. it is very common that you can receive such files with emails, and then be prompted, for example by Gmail, that you can add the event to the calendar. You might also be prevented with a link inside an application that makes prompts you do download a file with the same purpose.
iCal files, recognized by their .ics
extension, are a
standard format for calendar data exchange. They allow users to easily
import event details into their personal calendars, such as Google
Calendar, Apple Calendar, or Outlook. This feature enhances the user
experience by providing a seamless way to keep track of events and
appointments.
In this tutorial, I will go through how to generate the ical files and deliver them in two ways. Either by email or with a download link.
Step 1 - Generating the Event-schema
In the first step I need to generate the calendar events table. Note that I want all dates in utc_datetime. That also goes for the table-timesspamps, but since recent versions of Phoenix it is the default.
mix phx.gen.context Calendar Event calendar_events summary:text description:text start:utc_datetime end:utc_datetime location timezone
Remember to run the migrations when this is generated.
You can also in iex create a test event:
{:ok, event} = Tutorial.Calendar.create_event %{description: "Event description", summary: "Summary", location: "At the office", timezone: "Etc/UTC", start: ~U[2024-06-01 09:00:00Z], end: ~U[2024-06-01 10:00:00Z]}
Step 2 - Creating the iCal Renderer
In this section, we'll focus on creating the iCal renderer. It is basically a string with data interpolated from the event that we pass in. The core of this module is the render/1
function. This function takes a single argument, an Event
struct, and outputs a string formatted according to the iCal
specification. The goal here is to ensure that each piece of information
about the event (such as start time, end time, summary, and
description) is correctly placed in the iCal format.
defmodule Tutorial.Calendar.Ical do
@moduledoc """
This module is responible to render an ical compatible string
for an event.
"""
alias Tutorial.Calendar.Event
def render(%Event{} = event) do
"""
BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
BEGIN:VEVENT
DTSTART:#{format event.start}
DTEND:#{format event.end}
LOCATION:#{event.location}
SUMMARY:#{event.summary}
UID:#{event.id}
DTSTAMP:#{format event.updated_at}
DESCRIPTION:#{event.description}
CLASS:PUBLIC
END:VEVENT
END:VCALENDAR
"""
end
defp format(%DateTime{} = utc_datetime) do
Calendar.strftime(utc_datetime, "%Y%m%dT%H%M%SZ")
end
defp format(_utc_datetime), do: nil
end
The format/1
function is a private helper function used within our Ical
module. Its purpose is to correctly format DateTime
values into a string format that adheres to the iCal standard. This function takes a DateTime
struct and converts it into a string in the format YYYYMMDDTHHMMSSZ
, which is the standard format for date and time in iCal files.
Step 3 - Generating the notifier
mix phx.gen.notifier Calendar Event event
I will leave the notifier pretty much as it was generated. However, except for passing in a user, we also need to pass in an event. And notice how I added the attachment in the pipeline with a new function.
The create_ics_attachment/1
function is responsible for creating the iCal file attachment. It takes the event data, uses the Ical.render/1
function to generate the iCal formatted string, and then constructs a Swoosh attachment with this string.
This function ensures that the iCal file is correctly formatted and
attached to the email, ready to be sent to the user. When the recipient
receives the email, they can easily download and import the attached .ics
file into their preferred calendar application.
defmodule Tutorial.Calendar.EventNotifier do
import Swoosh.Email
alias Tutorial.Mailer
alias Tutorial.Calendar.Ical
def deliver_event(%{name: name, email: email}, event) do
new()
|> to({name, email})
|> from({"Phoenix Team", "team@example.com"})
|> subject("Welcome to Phoenix, #{name}!")
|> html_body("<h1>Hello, #{name}</h1>")
|> text_body("Hello, #{name}\n")
|> attachment(
create_ics_attachment(event)
)
|> Mailer.deliver()
end
defp create_ics_attachment(event) do
event_data = Ical.render(event)
%Swoosh.Attachment{filename: "event.ics", content_type: "text/calendar", data: event_data}
end
end
If I open up the http://localhost:4000/dev/mailbox I can see a sample of the sent email.
Notice the attachment in the bottom of the image.
Step 4 - Making a download link
The last step here is to make a downloadable link to an ical file. The code is similar to the one in the notifier. It is the same Ical.render/1
defmodule TutorialWeb.EventController do
use TutorialWeb, :controller
alias Tutorial.Calendar
alias Tutorial.Calendar.Ical
def show(conn, %{"id" => id}) do
event = Calendar.get_event!(id)
event_data = Ical.render(event)
send_download(
conn,
{:binary, event_data},
content_type: "text/calendar",
filename: "event.ics"
)
end
end
And after the controller is in place, I need to open up the router and add the route in a suitable place. Probably behind some authentication.
get "/event/:id", EventController, :show
With the route in place, I can just open the browser and visit the url and the browser will put the file in the download folder.