diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 6be885dbead43b7d687e21bec5678dbd52348a16..c92ee051393adf8bcdff7cb3402ced9eecea12dc 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -13,6 +13,7 @@ Added
 ~~~~~
 
 * Allow to disable exception mails to admins
+* Add possibility to create iCal feeds in all apps and dynamically create user-specific urls.
 
 Fixed
 ~~~~~
diff --git a/README.rst b/README.rst
index a6732de3b038f5d1d4f5b59d2c05f3c77840c2b9..290669c5779841ac7713f7f8004cc3141de0b1f9 100644
--- a/README.rst
+++ b/README.rst
@@ -64,7 +64,7 @@ Licence
   Copyright © 2017, 2018, 2019, 2020, 2021, 2022 Jonathan Weth <dev@jonathanweth.de>
   Copyright © 2017, 2018, 2019, 2020 Frank Poetzsch-Heffter <p-h@katharineum.de>
   Copyright © 2018, 2019, 2020, 2021, 2022 Hangzhi Yu <yuha@katharineum.de>
-  Copyright © 2018, 2019, 2020, 2021 Julian Leucker <leuckeju@katharineum.de>
+  Copyright © 2018, 2019, 2020, 2021, 2022 Julian Leucker <leuckeju@katharineum.de>
   Copyright © 2019, 2020, 2021, 2022 Dominik George <dominik.george@teckids.org>
   Copyright © 2019, 2020, 2021, 2022 Tom Teichler <tom.teichler@teckids.org>
   Copyright © 2019 mirabilos <thorsten.glaser@teckids.org>
diff --git a/aleksis/core/apps.py b/aleksis/core/apps.py
index 77e4b2a6327d9b88254b160375b1ce85be73ed68..08c965a02a7d6d0c4c53fb1bbcc5919ad35f7b7e 100644
--- a/aleksis/core/apps.py
+++ b/aleksis/core/apps.py
@@ -39,7 +39,7 @@ class CoreConfig(AppConfig):
         ([2017, 2018, 2019, 2020, 2021, 2022], "Jonathan Weth", "wethjo@katharineum.de"),
         ([2017, 2018, 2019, 2020], "Frank Poetzsch-Heffter", "p-h@katharineum.de"),
         ([2018, 2019, 2020, 2021, 2022], "Hangzhi Yu", "yuha@katharineum.de"),
-        ([2018, 2019, 2020, 2021], "Julian Leucker", "leuckeju@katharineum.de"),
+        ([2018, 2019, 2020, 2021, 2022], "Julian Leucker", "leuckeju@katharineum.de"),
         ([2019, 2020, 2021, 2022], "Dominik George", "dominik.george@teckids.org"),
         ([2019, 2020, 2021, 2022], "Tom Teichler", "tom.teichler@teckids.org"),
         ([2019], "mirabilos", "thorsten.glaser@teckids.org"),
diff --git a/aleksis/core/feeds.py b/aleksis/core/feeds.py
new file mode 100644
index 0000000000000000000000000000000000000000..fdfd6bc1f1123683f15e8b4baeada483892fa8f5
--- /dev/null
+++ b/aleksis/core/feeds.py
@@ -0,0 +1,80 @@
+from django.conf import settings
+from django.utils.formats import date_format
+from django.utils.functional import classproperty
+from django.utils.translation import gettext_lazy as _
+
+from django_ical.utils import build_rrule_from_text
+from django_ical.views import ICalFeed
+
+from aleksis.core import models
+from aleksis.core.util.core_helpers import get_site_preferences, queryset_rules_filter
+
+
+class PersonalICalFeedBase(ICalFeed):
+    """Base class for personal iCal feeds."""
+
+    @property
+    def product_id(self):
+        lang = self.request.LANGUAGE_CODE
+        title = get_site_preferences()["general__title"]
+        return f"-//AlekSIS//{title}//{lang}"
+
+    link = settings.BASE_URL
+    timezone = settings.TIME_ZONE
+    person = None
+    request = None
+
+    def get_object(self, request, *args, **kwargs):
+        if kwargs.get("person"):
+            self.person = kwargs.pop("person")
+        self.request = request
+        return super().get_object(request, *args, **kwargs)
+
+    @classproperty
+    def subclasses_list(cls):
+        return cls.__subclasses__()
+
+    @classproperty
+    def subclasses_dict(cls):
+        return {subclass.__name__: subclass for subclass in cls.subclasses_list}
+
+    @classproperty
+    def subclass_choices(cls):
+        return [
+            (subclass.__name__, f"{subclass.title} – {subclass.description}")
+            for subclass in cls.subclasses_list
+        ]
+
+
+class BirthdayFeed(PersonalICalFeedBase):
+    """Birthday calendar feed."""
+
+    title = _("Birthday Calendar")
+    description = _("A Calendar of Birthdays")
+    file_name = "birthdays.ics"
+
+    def items(self):
+        from aleksis.core.models import Person
+
+        return queryset_rules_filter(
+            obj=self.person.user,
+            perm="core.view_personal_details_rule",
+            queryset=Person.objects.filter(date_of_birth__isnull=False),
+        )
+
+    def item_title(self, item: "models.Person"):
+        return _("%(name)s's birthday") % {
+            "name": item.addressing_name,
+        }
+
+    def item_description(self, item: "models.Person"):
+        return _("%(name)s was born on %(birthday)s") % {
+            "name": item.addressing_name,
+            "birthday": date_format(item.date_of_birth),
+        }
+
+    def item_start_datetime(self, item):
+        return item.date_of_birth
+
+    def item_rrule(self, item):
+        return build_rrule_from_text("FREQ=YEARLY")
diff --git a/aleksis/core/menus.py b/aleksis/core/menus.py
index a714559e822ebe69e3e32e7c3d1bdd1791436a1d..58ffe5bcc421b738b367c0e04de76cf83fc2737f 100644
--- a/aleksis/core/menus.py
+++ b/aleksis/core/menus.py
@@ -315,6 +315,18 @@ MENUS = {
                 "aleksis.core.util.core_helpers.has_person",
             ],
         },
+        {
+            "name": _("Calendar Feeds"),
+            "url": "ical_feed_list",
+            "svg_icon": "mdi:calendar-multiple",
+            "validators": [
+                "menu_generator.validators.is_authenticated",
+                (
+                    "aleksis.core.util.predicates.permission_validator",
+                    "core.view_ical_rule",
+                ),
+            ],
+        },
         {
             "divider": True,
             "name": _("Logout"),
diff --git a/aleksis/core/migrations/0039_personal_ical_url.py b/aleksis/core/migrations/0039_personal_ical_url.py
new file mode 100644
index 0000000000000000000000000000000000000000..02d9cb3a9e3589563c7dd68ce211271264c5245e
--- /dev/null
+++ b/aleksis/core/migrations/0039_personal_ical_url.py
@@ -0,0 +1,31 @@
+# Generated by Django 3.2.12 on 2022-02-20 21:04
+
+from django.db import migrations, models
+import django.db.models.deletion
+import uuid
+
+from aleksis.core.feeds import PersonalICalFeedBase
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0038_notification_send_at'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='PersonalICalUrl',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True, verbose_name='UUID')),
+                ('name', models.CharField(max_length=255, verbose_name='Name')),
+                ('ical_feed', models.CharField(choices=PersonalICalFeedBase.subclass_choices, max_length=255, verbose_name='Selected ICal feed')),
+                ('person', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='calendar_urls', to='core.person', verbose_name='Person')),
+            ],
+            options={
+                'verbose_name': 'Personal Calendar URL',
+                'verbose_name_plural': 'Personal Calendar URLs',
+            },
+        ),
+    ]
diff --git a/aleksis/core/models.py b/aleksis/core/models.py
index 754c4fea9c076adfeb24f038b99d09960117c17b..963c60c99ef8e8b1a9b0ca7ebc31c7c8f298a2f5 100644
--- a/aleksis/core/models.py
+++ b/aleksis/core/models.py
@@ -1,6 +1,7 @@
 # flake8: noqa: DJ01
 import base64
 import hmac
+import uuid
 from datetime import date, datetime, timedelta
 from typing import Any, Iterable, List, Optional, Sequence, Union
 from urllib.parse import urlparse
@@ -52,6 +53,7 @@ from polymorphic.models import PolymorphicModel
 
 from aleksis.core.data_checks import BrokenDashboardWidgetDataCheck, DataCheck, DataCheckRegistry
 
+from .feeds import PersonalICalFeedBase
 from .managers import (
     CurrentSiteManagerWithoutMigrations,
     GroupManager,
@@ -1351,3 +1353,41 @@ class OAuthRefreshToken(AbstractRefreshToken):
     """Placeholder for customising the RefreshToken model."""
 
     pass
+
+
+class PersonalICalUrl(models.Model):
+    """Calendar URL for a person.
+
+    This is used to connect iCalendar subscriptions to a person. A person can have multiple
+    URLs. The URL is used to generate the iCalendar feed and have personalized results. It
+    is possible to create multiple URLs for the same person and the same iCal feed, e.g. to
+    allow a person to share and unshare their calendar with other people.
+    """
+
+    person = models.ForeignKey(
+        "Person",
+        on_delete=models.CASCADE,
+        related_name="calendar_urls",
+        verbose_name=_("Person"),
+    )
+    uuid = models.UUIDField(default=uuid.uuid4, editable=False, verbose_name=_("UUID"), unique=True)
+    name = models.CharField(max_length=255, verbose_name=_("Name"))
+    ical_feed = models.CharField(
+        max_length=255,
+        verbose_name=_("Selected ICal feed"),
+        choices=PersonalICalFeedBase.subclass_choices,
+    )
+
+    @property
+    def ical_feed_object(self):
+        return PersonalICalFeedBase.subclasses_dict.get(self.ical_feed)
+
+    class Meta:
+        verbose_name = _("Personal Calendar URL")
+        verbose_name_plural = _("Personal Calendar URLs")
+
+    def __str__(self):
+        return self.name
+
+    def get_absolute_url(self):
+        return reverse("ical_feed", kwargs={"slug": self.uuid})
diff --git a/aleksis/core/rules.py b/aleksis/core/rules.py
index 022fa11a00671a3acb0775c2229a378bd3981342..a6b478f26144b9fdea74804dccdb7a1913b01c30 100644
--- a/aleksis/core/rules.py
+++ b/aleksis/core/rules.py
@@ -7,6 +7,7 @@ from .util.predicates import (
     has_global_perm,
     has_object_perm,
     has_person,
+    is_assigned_to_current_person,
     is_current_person,
     is_group_owner,
     is_notification_recipient,
@@ -357,3 +358,16 @@ rules.add_perm("core.manage_permissions", manage_person_permissions_predicate)
 
 test_pdf_generation_predicate = has_person & has_global_perm("core.test_pdf")
 rules.add_perm("core.test_pdf_rule", test_pdf_generation_predicate)
+
+# Do CRUD on PersonalICalUrls
+view_ical_predicate = has_person
+rules.add_perm("core.view_ical_rule", view_ical_predicate)
+
+create_ical_predicate = view_ical_predicate
+rules.add_perm("core.create_ical_rule", create_ical_predicate)
+
+edit_ical_predicate = view_ical_predicate & is_assigned_to_current_person
+rules.add_perm("core.edit_ical_rule", edit_ical_predicate)
+
+delete_ical_predicate = edit_ical_predicate
+rules.add_perm("core.delete_ical_rule", delete_ical_predicate)
diff --git a/aleksis/core/static/js/copy_button.js b/aleksis/core/static/js/copy_button.js
new file mode 100644
index 0000000000000000000000000000000000000000..554f6230e4f9c8b9bb012875e31da25c4f61c2c6
--- /dev/null
+++ b/aleksis/core/static/js/copy_button.js
@@ -0,0 +1,16 @@
+$(".copy-button").click((e) => {
+    const target = $(e.currentTarget);
+    const input = $("#" + target.data("target"));
+    const copy_icon = target.children(".copy-icon-copy").first();
+    const check_icon = target.children(".copy-icon-success").first();
+
+    console.log("Copying to clipboard");
+    navigator.clipboard.writeText(input.val()).then(r => {
+        check_icon.show();
+        copy_icon.hide();
+        setTimeout(() => {
+            check_icon.hide();
+            copy_icon.show();
+        }, 1000);
+    });
+});
diff --git a/aleksis/core/static/public/style.scss b/aleksis/core/static/public/style.scss
index 832a80c4b2c5e4fd8395790284c468996a8d5c8e..5a35561c389f5656fd573e8f9b698ae456d42ad5 100644
--- a/aleksis/core/static/public/style.scss
+++ b/aleksis/core/static/public/style.scss
@@ -24,6 +24,10 @@ rect#background {
   background-color: lighten($primary-color, 5%);
 }
 
+.waves-effect.waves-secondary .waves-ripple {
+  background-color: lighten($secondary-color, 5%);
+}
+
 .success {
   @extend .light-green, .lighten-3
 }
@@ -974,3 +978,16 @@ svg.iconify {
     text-align: center;
   }
 }
+
+.btn-small-line-height {
+  line-height: $button-small-height;
+}
+
+.btn-smaller-padding {
+  padding: 0 8px;
+}
+
+p.ical-description {
+  margin: 0;
+  font-weight: 300;
+}
diff --git a/aleksis/core/templates/core/ical/ical_create.html b/aleksis/core/templates/core/ical/ical_create.html
new file mode 100644
index 0000000000000000000000000000000000000000..64ff80083ebab3e61cd108d46ee1bff158cdd236
--- /dev/null
+++ b/aleksis/core/templates/core/ical/ical_create.html
@@ -0,0 +1,20 @@
+{% extends 'core/base.html' %}
+{% load i18n material_form %}
+
+{% block page_title %}{% blocktrans %}Create iCal URL{% endblocktrans %}{% endblock page_title %}
+{% block browser_title %}{% blocktrans %}Create iCal URL{% endblocktrans %}{% endblock browser_title %}
+
+{% block content %}
+  <form method="post">
+    {% csrf_token %}
+
+    {% form form=form %}{% endform %}
+
+    {% include "core/partials/save_button.html" %}
+    <a href="{% url "ical_feed_list" %}" class="btn red">
+      <i class="material-icons iconify left" data-icon="mdi:close"></i>
+      {% blocktrans %}Cancel{% endblocktrans %}
+    </a>
+  </form>
+
+{% endblock content %}
diff --git a/aleksis/core/templates/core/ical/ical_edit.html b/aleksis/core/templates/core/ical/ical_edit.html
new file mode 100644
index 0000000000000000000000000000000000000000..cd46c475531bc1154f5f36c16ae1476b79e73137
--- /dev/null
+++ b/aleksis/core/templates/core/ical/ical_edit.html
@@ -0,0 +1,20 @@
+{% extends 'core/base.html' %}
+{% load i18n material_form %}
+
+{% block page_title %}{% blocktrans %}Edit iCal URL {{ object }}{% endblocktrans %}{% endblock page_title %}
+{% block browser_title %}{% blocktrans %}Edit iCal URL {{ object }}{% endblocktrans %}{% endblock browser_title %}
+
+{% block content %}
+  <form method="post">
+    {% csrf_token %}
+
+    {% form form=form %}{% endform %}
+
+    {% include "core/partials/save_button.html" %}
+    <a href="{% url "ical_feed_list" %}" class="btn red">
+      <i class="material-icons iconify left" data-icon="mdi:close"></i>
+      {% blocktrans %}Cancel{% endblocktrans %}
+    </a>
+  </form>
+
+{% endblock content %}
diff --git a/aleksis/core/templates/core/ical/ical_list.html b/aleksis/core/templates/core/ical/ical_list.html
new file mode 100644
index 0000000000000000000000000000000000000000..2388feb2daa9c4b31abb80bc830055e2d2d3efc6
--- /dev/null
+++ b/aleksis/core/templates/core/ical/ical_list.html
@@ -0,0 +1,38 @@
+{% extends 'core/base.html' %}
+{% load i18n msg_box static html_helpers %}
+
+{% block page_title %}{% trans "ICal Feeds" %}{% endblock page_title %}
+{% block browser_title %}{% trans "ICal Feeds" %}{% endblock browser_title %}
+
+{% block content %}
+  {% trans "These are URLs for different Calendar Feeds in the iCal (.ics) format. You can create as many as you want and import them in your calendar software." as msg %}
+  {% msg_box msg=msg status="info" %}
+  <a href="{% url "ical_feed_create" %}" class="btn green">
+  <i class="material-icons iconify left" data-icon="mdi:add"></i>
+  {% trans "Create iCal URL" %}
+  </a>
+  <h2>{% trans "Your iCal URLs" %}</h2>
+  <div class="collection">
+    {% for object in object_list %}
+      <div class="collection-item">
+        <span class="title btn-small-line-height">
+          {{ object }}
+          <a href="{% url "ical_feed_delete" object.pk %}"
+                  class="secondary-content btn-flat btn-small red-text btn-smaller-padding waves-effect waves-red">
+            <i class="material-icons iconify" data-icon="mdi:delete-outline"></i>
+          </a>
+          <a href="{% url "ical_feed_edit" object.pk %}"
+             class="secondary-content btn-flat btn-small primary-color-text btn-smaller-padding waves-effect waves-primary">
+            <i class="material-icons iconify" data-icon="mdi:pencil-outline"></i>
+          </a>
+          {% generate_random_id "ical-copy-" as id %}
+          {% include "core/partials/copy_button.html" with classes="secondary-content btn-flat btn-small secondary-color-text btn-smaller-padding" target=id %}
+        </span>
+        <p class="ical-description">{{ object.ical_feed_object.title }} – {{ object.ical_feed_object.description }}</p>
+        <input type="url" readonly value="https://{{ request.site }}{{ object.get_absolute_url }}"
+               id="{{ id }}">
+      </div>
+    {% endfor %}
+  </div>
+  <script src="{% static "js/copy_button.js" %}" type="text/javascript"></script>
+{% endblock content %}
diff --git a/aleksis/core/templates/core/partials/copy_button.html b/aleksis/core/templates/core/partials/copy_button.html
new file mode 100644
index 0000000000000000000000000000000000000000..2ca0168333bc7215a580b05316c4556db436bb12
--- /dev/null
+++ b/aleksis/core/templates/core/partials/copy_button.html
@@ -0,0 +1,5 @@
+<button type="button" data-target="{{ target }}"
+        class="{{ classes }} copy-button waves-effect waves-secondary">
+  <i class="material-icons iconify copy-icon-copy" data-icon="mdi:content-copy"></i>
+  <i class="material-icons iconify copy-icon-success" style="display: none" data-icon="mdi:check"></i>
+</button>
diff --git a/aleksis/core/templatetags/html_helpers.py b/aleksis/core/templatetags/html_helpers.py
index 5f78b1b195f9030cc97d96a0488c14845d450741..34066192527930c2a4096f49deb3d247f883c31e 100644
--- a/aleksis/core/templatetags/html_helpers.py
+++ b/aleksis/core/templatetags/html_helpers.py
@@ -1,3 +1,6 @@
+import random
+import string
+
 from django import template
 
 from bs4 import BeautifulSoup
@@ -22,3 +25,18 @@ def add_class_to_el(value: str, arg: str) -> str:
         el["class"] = el.get("class", []) + [cls]
 
     return str(soup)
+
+
+@register.simple_tag
+def generate_random_id(prefix: str, length: int = 10) -> str:
+    """Generate a random ID for templates.
+
+    :Example:
+
+    .. code-block::
+
+        {% generate_random_id "prefix-" %}
+    """
+    return prefix + "".join(
+        random.choice(string.ascii_lowercase) for i in range(length)  # noqa: S311
+    )
diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py
index f39808e0ef0ed22e9c3b94e91fc5c1161beccdb9..e76da89d18ed2078fc2405db11829aa344725fdf 100644
--- a/aleksis/core/urls.py
+++ b/aleksis/core/urls.py
@@ -320,6 +320,11 @@ urlpatterns = [
         name="assign_permission",
     ),
     path("pdfs/<int:pk>/", views.RedirectToPDFFile.as_view(), name="redirect_to_pdf_file"),
+    path("ical/", views.ICalFeedListView.as_view(), name="ical_feed_list"),
+    path("ical/create/", views.ICalFeedCreateView.as_view(), name="ical_feed_create"),
+    path("ical/<int:pk>/edit/", views.ICalFeedEditView.as_view(), name="ical_feed_edit"),
+    path("ical/<int:pk>/delete/", views.ICalFeedDeleteView.as_view(), name="ical_feed_delete"),
+    path("ical/<slug:slug>.ics", views.ICalFeedView.as_view(), name="ical_feed"),
     path("__icons__/", include("dj_iconify.urls")),
 ]
 
diff --git a/aleksis/core/util/predicates.py b/aleksis/core/util/predicates.py
index bcba7e8637d81aa9c554aef65cdb431b059753a9..5ba4271c08a4244ba5f359e9c46e01eff6c6d112 100644
--- a/aleksis/core/util/predicates.py
+++ b/aleksis/core/util/predicates.py
@@ -154,3 +154,9 @@ def contains_site_preference_value(section: str, pref: str, value: str):
 def has_activated_2fa(user: User) -> bool:
     """Check if the user has activated two-factor authentication."""
     return user_has_device(user)
+
+
+@predicate
+def is_assigned_to_current_person(user: User, obj: Model) -> bool:
+    """Check if the object is assigned to the current person."""
+    return getattr(obj, "person", None) == user.person
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index cb5a9169c5a9bb0048a998f289ee9a44432d06e9..ed20571dd83aee3bfc5d3bc4fd1dc2c8a7211b1d 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -102,6 +102,7 @@ from .models import (
     OAuthApplication,
     PDFFile,
     Person,
+    PersonalICalUrl,
     PersonInvitation,
     SchoolTerm,
     TaskUserAssignment,
@@ -1566,3 +1567,59 @@ class CustomAuthorizationView(AuthorizationView):
         context = super().get_context_data(**kwargs)
         context["no_menu"] = True
         return context
+
+
+class ICalFeedView(DetailView):
+    model = PersonalICalUrl
+    slug_field = "uuid"
+
+    def get(self, request, *args, **kwargs):
+        obj: PersonalICalUrl = self.get_object()
+        if obj.ical_feed_object:
+            kwargs["person"] = obj.person
+            return obj.ical_feed_object()(request, *args, **kwargs)
+        else:
+            return HttpResponse(status=410)
+
+
+class ICalFeedListView(PermissionRequiredMixin, ListView):
+    model = PersonalICalUrl
+    template_name = "core/ical/ical_list.html"
+    permission_required = "core.view_ical_rule"
+
+    def get_queryset(self):
+        return self.model.objects.filter(person=self.request.user.person)
+
+
+class ICalFeedEditView(PermissionRequiredMixin, AdvancedEditView):
+    model = PersonalICalUrl
+    template_name = "core/ical/ical_edit.html"
+    success_url = reverse_lazy("ical_feed_list")
+    success_message = _("ICal feed updated successfully")
+    permission_required = "core.edit_ical_rule"
+
+    fields = ["name", "ical_feed"]
+
+
+class ICalFeedDeleteView(PermissionRequiredMixin, AdvancedDeleteView):
+    model = PersonalICalUrl
+    template_name = "core/pages/delete.html"
+    success_url = reverse_lazy("ical_feed_list")
+    success_message = _("ICal feed deleted successfully")
+    permission_required = "core.delete_ical_rule"
+
+
+class ICalFeedCreateView(PermissionRequiredMixin, AdvancedCreateView):
+    model = PersonalICalUrl
+    template_name = "core/ical/ical_create.html"
+    success_url = reverse_lazy("ical_feed_list")
+    success_message = _("ICal feed created successfully")
+    permission_required = "core.create_ical_rule"
+
+    fields = ["name", "ical_feed"]
+
+    def form_valid(self, form):
+        obj = form.save(commit=False)
+        obj.person = self.request.user.person
+        obj.save()
+        return super().form_valid(form)
diff --git a/pyproject.toml b/pyproject.toml
index 01bf30832e0e8fea53a550b38864dabec66bf022..51f859a3a864acf61fc4755581242f751567d6dd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -126,6 +126,7 @@ python-gnupg = "^0.4.7"
 sentry-sdk = {version = "^1.4.3", optional = true}
 django-cte = "^1.1.5"
 pycountry = "^22.0.0"
+django-ical = "^1.8.3"
 django-iconify = "^0.2.0"
 customidenticon = "^0.1.5"