diff --git a/aleksis/core/frontend/components/calendar/CalendarOverview.vue b/aleksis/core/frontend/components/calendar/CalendarOverview.vue
index 9c88be6adabbfb3036171ecf5c96fd16d47fb6af..f5e19bd02210c52ae14bd0067775091efaa9201a 100644
--- a/aleksis/core/frontend/components/calendar/CalendarOverview.vue
+++ b/aleksis/core/frontend/components/calendar/CalendarOverview.vue
@@ -1,7 +1,9 @@
 <template>
   <div class="mt-4 mb-4">
     <v-skeleton-loader
-      v-if="$apollo.queries.calendarFeeds.loading && calendarFeeds.length === 0"
+      v-if="
+        $apollo.queries.calendar.loading && calendar.calendarFeeds.length === 0
+      "
       type="date-picker-options, actions"
     />
     <div v-else>
@@ -47,7 +49,7 @@
           >
             <calendar-select
               v-model="selectedCalendarFeedNames"
-              :calendar-feeds="calendarFeeds"
+              :calendar-feeds="calendar.calendarFeeds"
             />
           </button-menu>
         </v-col>
@@ -87,16 +89,21 @@
               {{ $t("calendar.my_calendars") }}
             </v-subheader>
             <calendar-select
+              class="mb-4"
               v-model="selectedCalendarFeedNames"
-              :calendar-feeds="calendarFeeds"
+              :calendar-feeds="calendar.calendarFeeds"
             />
+            <v-btn depressed block v-if="calendar" :href="calendar.allFeedsUrl">
+              <v-icon left>mdi-download-outline</v-icon>
+              {{ $t("calendar.download_all") }}
+            </v-btn>
           </v-list>
         </v-col>
         <v-col lg="9" xl="10" :style="{ 'z-index': '20' }">
           <v-sheet height="600">
             <v-expand-transition>
               <v-progress-linear
-                v-if="$apollo.queries.calendarFeeds.loading"
+                v-if="$apollo.queries.calendar.loading"
                 indeterminate
               />
             </v-expand-transition>
@@ -123,7 +130,7 @@
               </template>
             </v-calendar>
             <component
-              v-if="calendarFeeds && selectedEvent"
+              v-if="calendar && calendar.calendarFeeds && selectedEvent"
               :is="detailComponentForFeed(selectedEvent.calendarFeedName)"
               v-model="selectedOpen"
               :selected-element="selectedElement"
@@ -158,7 +165,9 @@ export default {
   data() {
     return {
       calendarFocus: "",
-      calendarFeeds: [],
+      calendar: {
+        calendarFeeds: [],
+      },
       selectedCalendarFeedNames: [],
       currentCalendarType: "week",
       selectedEvent: {},
@@ -185,14 +194,14 @@ export default {
     };
   },
   apollo: {
-    calendarFeeds: {
+    calendar: {
       query: gqlCalendarOverview,
       skip: true,
     },
   },
   computed: {
     events() {
-      return this.calendarFeeds
+      return this.calendar.calendarFeeds
         .filter((c) => this.selectedCalendarFeedNames.includes(c.name))
         .flatMap((cf) =>
           cf.feed.events.map((event) => ({
@@ -236,7 +245,7 @@ export default {
     },
     detailComponentForFeed(feedName) {
       if (
-        this.calendarFeeds &&
+        this.calendar.calendarFeeds &&
         feedName &&
         Object.keys(calendarFeedDetailComponents).includes(feedName + "details")
       ) {
@@ -246,7 +255,7 @@ export default {
     },
     eventBarComponentForFeed(feedName) {
       if (
-        this.calendarFeeds &&
+        this.calendar.calendarFeeds &&
         feedName &&
         Object.keys(calendarFeedEventBarComponents).includes(
           feedName + "eventbar"
@@ -267,15 +276,15 @@ export default {
       let olderStart = extendedStart < this.fetchedDateRange.start;
       let youngerEnd = extendedEnd > this.fetchedDateRange.end;
 
-      if (this.calendarFeeds.length === 0) {
+      if (this.calendar.calendarFeeds.length === 0) {
         // No calendar feeds have been fetched yet,
         // so fetch all events in the current date range
 
-        this.$apollo.queries.calendarFeeds.setVariables({
+        this.$apollo.queries.calendar.setVariables({
           start: extendedStart,
           end: extendedEnd,
         });
-        this.$apollo.queries.calendarFeeds.skip = false;
+        this.$apollo.queries.calendar.skip = false;
         this.fetchedDateRange = { start: extendedStart, end: extendedEnd };
       } else if (olderStart || youngerEnd) {
         // Define newly fetched date range
@@ -286,14 +295,14 @@ export default {
         let fetchStart = olderStart ? extendedStart : this.fetchedDateRange.end;
         let fetchEnd = youngerEnd ? extendedEnd : this.fetchedDateRange.start;
 
-        this.$apollo.queries.calendarFeeds.fetchMore({
+        this.$apollo.queries.calendar.fetchMore({
           variables: {
             start: fetchStart,
             end: fetchEnd,
           },
           updateQuery: (previousResult, { fetchMoreResult }) => {
-            let previousCalendarFeeds = previousResult.calendarFeeds;
-            let newCalendarFeeds = fetchMoreResult.calendarFeeds;
+            let previousCalendarFeeds = previousResult.calendar.calendarFeeds;
+            let newCalendarFeeds = fetchMoreResult.calendar.calendarFeeds;
 
             previousCalendarFeeds.forEach((calendarFeed, i, calendarFeeds) => {
               // Get all events except those that are updated
@@ -308,7 +317,9 @@ export default {
               ];
             });
             return {
-              calendarFeeds: previousCalendarFeeds,
+              calendar: {
+                calendarFeeds: previousCalendarFeeds,
+              },
             };
           },
         });
diff --git a/aleksis/core/frontend/components/calendar/calendarOverview.graphql b/aleksis/core/frontend/components/calendar/calendarOverview.graphql
index 7e09f896fd3601f8891ef54ca2ccb08ddc756bdc..4d55a10ad313b3561a4ec0b9a077b485d9400e23 100644
--- a/aleksis/core/frontend/components/calendar/calendarOverview.graphql
+++ b/aleksis/core/frontend/components/calendar/calendarOverview.graphql
@@ -1,22 +1,25 @@
 query ($start: Date, $end: Date) {
-  calendarFeeds {
-    name
-    verboseName
-    description
-    url
-    color
-    feed {
-      events(start: $start, end: $end) {
-        name
-        start
-        end
-        color
-        description
-        location
-        uid
-        allDay
-        status
-        meta
+  calendar {
+    allFeedsUrl
+    calendarFeeds {
+      name
+      verboseName
+      description
+      url
+      color
+      feed {
+        events(start: $start, end: $end) {
+          name
+          start
+          end
+          color
+          description
+          location
+          uid
+          allDay
+          status
+          meta
+        }
       }
     }
   }
diff --git a/aleksis/core/frontend/messages/en.json b/aleksis/core/frontend/messages/en.json
index f9fcdc789dd80596ed55ab4c4a087484144e742b..c700f281d1c0ef0b2f9a0c12a69bb8a5fd47f8ad 100644
--- a/aleksis/core/frontend/messages/en.json
+++ b/aleksis/core/frontend/messages/en.json
@@ -256,7 +256,8 @@
     "ics_to_clipboard": "Copy link to calendar ICS to clipboard",
     "cancelled": "Cancelled",
     "download_ics": "Download ICS",
-    "my_calendars": "My Calendars"
+    "my_calendars": "My Calendars",
+    "download_all": "Download all"
   },
   "status": {
     "changes": "You have unsaved changes.",
diff --git a/aleksis/core/schema/__init__.py b/aleksis/core/schema/__init__.py
index b3c2d6400e1eb58a338d0d5b6889d515cf2ef831..0ce00ad587d2f43b0ed2776f9c598073c5ae401f 100644
--- a/aleksis/core/schema/__init__.py
+++ b/aleksis/core/schema/__init__.py
@@ -9,7 +9,6 @@ from haystack.inputs import AutoQuery
 from haystack.query import SearchQuerySet
 from haystack.utils.loading import UnifiedIndex
 
-from ..mixins import CalendarEventMixin
 from ..models import (
     CustomMenu,
     DynamicRoute,
@@ -21,7 +20,7 @@ from ..models import (
 )
 from ..util.apps import AppConfig
 from ..util.core_helpers import get_allowed_object_ids, get_app_module, get_app_packages, has_person
-from .calendar import CalendarType
+from .calendar import CalendarBaseType
 from .celery_progress import CeleryProgressFetchedMutation, CeleryProgressType
 from .custom_menu import CustomMenuType
 from .dynamic_routes import DynamicRouteType
@@ -72,9 +71,7 @@ class Query(graphene.ObjectType):
 
     oauth_access_tokens = graphene.List(OAuthAccessTokenType)
 
-    calendar_feeds = graphene.List(CalendarType)
-
-    calendar_feeds_by_names = graphene.List(CalendarType, names=graphene.List(graphene.String))
+    calendar = graphene.Field(CalendarBaseType)
 
     def resolve_ping(root, info, payload) -> str:
         return payload
@@ -178,11 +175,8 @@ class Query(graphene.ObjectType):
     def resolve_oauth_access_tokens(root, info, **kwargs):
         return OAuthAccessToken.objects.filter(user=info.context.user)
 
-    def resolve_calendar_feeds(root, info, **kwargs):
-        return CalendarEventMixin.valid_feeds
-
-    def resolve_calendar_feeds_by_names(root, info, names, **kwargs):
-        return [CalendarEventMixin.get_object_by_name(name) for name in names]
+    def resolve_calendar(self, info, **kwargs):
+        return True
 
 
 class Mutation(graphene.ObjectType):
diff --git a/aleksis/core/schema/calendar.py b/aleksis/core/schema/calendar.py
index e918bd0759de481d491e138f69c08e7fee6bc3d1..22f8c0687d7bc6dfc626ad75d8c784b497f0b2db 100644
--- a/aleksis/core/schema/calendar.py
+++ b/aleksis/core/schema/calendar.py
@@ -5,6 +5,8 @@ from django.urls import reverse
 import graphene
 from graphene import ObjectType
 
+from aleksis.core.mixins import CalendarEventMixin
+
 
 class CalendarEventType(ObjectType):
     name = graphene.String()
@@ -83,3 +85,20 @@ class CalendarType(ObjectType):
 
     def resolve_color(root, info, **kwargs):
         return root.get_color(info.context)
+
+
+class CalendarBaseType(ObjectType):
+    calendar_feeds = graphene.List(CalendarType)
+
+    calendar_feeds_by_names = graphene.List(CalendarType, names=graphene.List(graphene.String))
+
+    all_feeds_url = graphene.String()
+
+    def resolve_calendar_feeds(root, info, **kwargs):
+        return CalendarEventMixin.valid_feeds
+
+    def resolve_calendar_feeds_by_names(root, info, names, **kwargs):
+        return [CalendarEventMixin.get_object_by_name(name) for name in names]
+
+    def resolve_all_feeds_url(root, info, **kwargs):
+        return info.context.build_absolute_uri(reverse("all_calendar_feeds"))
diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py
index 887a384247207c391f46b9e3eabfec870f863e9c..d959ad27a46ab662d9c63fddfa5bfd11fc4884b1 100644
--- a/aleksis/core/urls.py
+++ b/aleksis/core/urls.py
@@ -398,6 +398,7 @@ urlpatterns = [
                     name="assign_permission",
                 ),
                 path("feeds/<str:name>.ics", views.ICalFeedView.as_view(), name="calendar_feed"),
+                path("feeds.ics", views.ICalAllFeedsView.as_view(), name="all_calendar_feeds"),
             ]
         ),
     ),
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index 62f095450ea01e9295b97cfba2427683b7bb466a..9d71ff52378b605b45be8bf686e378986d29b382 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -1606,3 +1606,16 @@ class ICalFeedView(PermissionRequiredMixin, View):
             feed.write(response, "utf-8")
             return response
         raise Http404
+
+
+class ICalAllFeedsView(PermissionRequiredMixin, View):
+    """View to generate an iCal feed for all calendars."""
+
+    permission_required = "core.view_calendar_feed_rule"
+
+    def get(self, request, *args, **kwargs):
+        response = HttpResponse(content_type="text/calendar")
+        for calendar in CalendarEventMixin.valid_feeds:
+            feed = calendar.create_feed(request)
+            feed.write(response, "utf-8")
+        return response