Creating an iCalendar event with HTML content in Kotlin / JVM
Tags: kotlin, html, icalendar
Published: 2023-01-25 19:33:37


## Introduction

The [iCalendar](https://en.wikipedia.org/wiki/ICalendar) format allows to define calendar events in a simple text file. The most common file extension of an iCalendar file is .ics and is supported by many apps, such as Outlook.

An iCalendar can define many types of information, such as events and to-dos, using many standard properties. Among these properties, an important one is missing for standard events, which is to define the event description in HTML. Indeed, the default description property `DESCRIPTION` takes only plain text values. Fortunately, there is an experimental property `X-ALT-DESC` that can supports HTML markup.

In this post, we'll use a library called [biweekly](https://github.com/mangstadt/biweekly) in order to programmatically create an iCalendar event with an HTML description. But first let's start with some manually created files.

## Manually created iCalendar with an X-ALT-DESC property

The following file illustrates a basic iCalendar event that contains both `DESCRIPTION` and `X-ALT-DESC`.

```ics
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//xyz Corp//NONSGML PDA Calendar Version 1.0//EN
BEGIN:VEVENT
UID:uid1@awesome.com
ORGANIZER:mailto:organizer@awesome.com
DTSTAMP:20230101T100000Z
DTSTART:20230101T100000Z
DTEND:20230102T113000Z
SUMMARY:Awesome Event
DESCRIPTION:Please join this awesome event
X-ALT-DESC;FMTTYPE=text/html:<!doctype html><html><body><h1 style="color:blue">Awesome event</h1><p>Please join</p></body></html>
END:VEVENT
END:VCALENDAR
```

Setting both `DESCRIPTION` and `X-ALT-DESC` properties in the same event is valid and works on any tool that is compatible with iCalendar. In fact, if the tool does not support `X-ALT-DESC`, then `DESCRIPTION` will be used instead. Tools that do support `X-ALT-DESC` can ignore the `DESCRIPTION` field and use `X-ALT-DESC` instead. That's what Outlook does as we'll see later.

You can test iCalendar by first putting this content in a text file and changing its extension to `.ics`. Next you can just open it if you have a calendar app installed (such as Outlook) or import it into a Calendar app (such as Google Calendar). You can also [download this ics file](/assets/files/posts/html-icalendar/awesome-event.ics) which already contains the above text.

This is how Outlook renders the ics file:

![manual ics outlook](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n4zebnesjvcl4rc5ccui.png)

We can note that Outlook ignores `DESCRIPTION` when `X-ALT-DESC` is available.

And here is the rendering of Google Calendar. We can note that it just ignores `X-ALT-DESC` and uses `DESCRIPTION` instead.

![manual ics calendar](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kr1faa2srcmx18zjb73t.png)

## Creating an iCalendar with an X-ALT-DESC property with biweekly

[biweekly](https://github.com/mangstadt/biweekly) is an iCalendar library which supports the JVM and Android. This means that it can target a wide range of applications. In this chapter, we'll use this library to create and ics file in a Kotlin project. We'll use IntelliJ IDEA community edition because it's free and provide powerful Kotlin support.

So, let's create a new project by choosing **Kotlin** as language:

![new project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y5ullxao166rda9uy1fc.png)

After that, we need to add biweekly as a _dependency_ in `build.gradle.kts`:

```kt
dependencies {
    ...
    implementation("net.sf.biweekly:biweekly:0.6.6") // add this line
    ...
}
```

Next comes the fun part where we write some code. The following snippet creates an iCalendar event and prints it to the console and writes it into a file.

```kt
val event = VEvent()
// set start and end dates
val startDateTime = LocalDateTime.of(2022, 1, 1, 10, 0)
event.setDateStart(Date.from(startDateTime.toInstant(ZoneOffset.UTC)), true)
val endDateTime = startDateTime.plusDays(2).plusHours(1).plusMinutes(30)
event.setDateEnd(Date.from(endDateTime.toInstant(ZoneOffset.UTC)), true)
// set summary, description and organizer
event.setSummary("Awesome event")
event.setDescription("Please join this awesome event")
event.setOrganizer("organizer@awesome.com")
// set the HTML content to X-ALT-DESC. We also need to set FMTTYPE parameter to text/html
event.setExperimentalProperty(
    "X-ALT-DESC",
    "<!doctype html><html><body>" +
            "<h1 style='color:blue'>Awesome event</h1>" +
            "<p>Please join</p></body></html>"
).setParameter("FMTTYPE", "text/html")
// create the iCalendar
val ical = ICalendar()
ical.addEvent(event)
// Print it on the console
print(Biweekly.write(ical).go())
// Write to file
val file = File("awesome-event.ics")
Biweekly.write(ical).go(file)
```

The generated ics is as follows:

```ics
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Michael Angstadt//biweekly 0.6.6//EN
BEGIN:VEVENT
UID:4a945aec-3b58-437d-90fe-7ded8e352ec9
DTSTAMP:20230125T184903Z
DTSTART:20220101T100000Z
DTEND:20220103T113000Z
SUMMARY:Awesome event
DESCRIPTION:Please join this awesome event
ORGANIZER:mailto:organizer@awesome.com
X-ALT-DESC;FMTTYPE=text/html:<!doctype html><html><body><h1 style='color:bl
 ue'>Awesome event</h1><p>Please join</p></body></html>
END:VEVENT
END:VCALENDAR
```

We can note that biweekly automatically adds the VERSION, PRODID and UID fields, which is always nice.


Opening the ics with outlook show this.

![biweekly ics outlook](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/50wwn1qhm57y9i5bm1mk.png)

We can go even further by embedding an image using a base64 src. Here is the improved code that embeds an image in the ics.

```ics
val event = VEvent()
val startDateTime = LocalDateTime.of(2023, 1, 1, 10, 0)
event.setDateStart(Date.from(startDateTime.toInstant(ZoneOffset.UTC)), true)
val endDateTime = startDateTime.plusDays(2).plusHours(1).plusMinutes(30)
event.setDateEnd(Date.from(endDateTime.toInstant(ZoneOffset.UTC)), true)
event.setSummary("Awesome event")
event.setDescription("Please join this awesome event")
event.setOrganizer("organizer@awesome.com")
event.setExperimentalProperty(
    "X-ALT-DESC",
    "<!doctype html><html><body>" +
            "<h1 style='color:blue'>Awesome event</h1>" +
            "<p>Please join</p>" +
            "<img src=\"data:image/jpeg;base64,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\" alt=\"\" />" +
            "</body></html>"
).setParameter("FMTTYPE", "text/html")
val ical = ICalendar()
ical.addEvent(event)
print(Biweekly.write(ical).go())
Biweekly.write(ical).go(File("awesome-event-with-image.ics"))
```

The ics should render as follows in Outlook.

![biweekly ics image outlook](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4xkho77fqjk0ybtdqdyj.png)


## Conclusion

In this post, we have created some iCalendar files by hand and by code using biweekly. We also took advantage of the experimental property `X-ALT-DESC` to display a more fun and colorful description thanks to the power of HTML.

You can find the source code of the [project here](https://github.com/wlybe/biweekly-kotlin-demo).

Happy coding.