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.