[evolution] Implement News & Blogs (RSS) reader



commit 854b61ecbf1aca722035f5cb41c29799d43d2648
Author: Milan Crha <mcrha redhat com>
Date:   Tue Jul 5 21:21:59 2022 +0200

    Implement News & Blogs (RSS) reader

 data/icons/CMakeLists.txt                          |    6 +-
 .../icons/hicolor_status_scalable_rss-symbolic.svg |   98 ++
 data/icons/hicolor_status_scalable_rss.svg         |  110 ++
 po/POTFILES.in                                     |    8 +
 src/modules/CMakeLists.txt                         |    1 +
 src/modules/rss/CMakeLists.txt                     |    2 +
 src/modules/rss/camel-rss-store-summary.c          |  852 ++++++++++++
 src/modules/rss/camel-rss-store-summary.h          |  116 ++
 src/modules/rss/camel/CMakeLists.txt               |   50 +
 src/modules/rss/camel/camel-rss-folder-summary.c   |  412 ++++++
 src/modules/rss/camel/camel-rss-folder-summary.h   |   69 +
 src/modules/rss/camel/camel-rss-folder.c           |  778 +++++++++++
 src/modules/rss/camel/camel-rss-folder.h           |   55 +
 src/modules/rss/camel/camel-rss-provider.c         |   94 ++
 src/modules/rss/camel/camel-rss-settings.c         |  301 ++++
 src/modules/rss/camel/camel-rss-settings.h         |   76 ++
 src/modules/rss/camel/camel-rss-store.c            |  397 ++++++
 src/modules/rss/camel/camel-rss-store.h            |   55 +
 src/modules/rss/camel/libcamelrss.urls             |    1 +
 src/modules/rss/e-rss-parser.c                     |  656 +++++++++
 src/modules/rss/e-rss-parser.h                     |   48 +
 src/modules/rss/evolution/CMakeLists.txt           |   32 +
 .../evolution/e-rss-folder-tree-model-extension.c  |  217 +++
 src/modules/rss/evolution/e-rss-preferences.c      | 1437 ++++++++++++++++++++
 src/modules/rss/evolution/e-rss-preferences.h      |   20 +
 src/modules/rss/evolution/e-rss-shell-extension.c  |  132 ++
 .../rss/evolution/e-rss-shell-view-extension.c     |  277 ++++
 src/modules/rss/evolution/module-rss.c             |   28 +
 src/modules/rss/evolution/module-rss.h             |   21 +
 29 files changed, 6347 insertions(+), 2 deletions(-)
---
diff --git a/data/icons/CMakeLists.txt b/data/icons/CMakeLists.txt
index ba48344de1..9aaaf4d37d 100644
--- a/data/icons/CMakeLists.txt
+++ b/data/icons/CMakeLists.txt
@@ -180,10 +180,12 @@ set(private_icons
        hicolor_places_24x24_mail-outbox.png
        hicolor_places_24x24_mail-sent.png
        hicolor_status_16x16_wrapped.png
-       hicolor_status_32x32_offline.png
-       hicolor_status_32x32_online.png
        hicolor_status_32x32_aspect-ratio-lock.png
        hicolor_status_32x32_aspect-ratio-unlock.png
+       hicolor_status_32x32_offline.png
+       hicolor_status_32x32_online.png
+       hicolor_status_scalable_rss.svg
+       hicolor_status_scalable_rss-symbolic.svg
 )
 
 # These icons were in gnome-icon-theme prior to GNOME 2.30.
diff --git a/data/icons/hicolor_status_scalable_rss-symbolic.svg 
b/data/icons/hicolor_status_scalable_rss-symbolic.svg
new file mode 100644
index 0000000000..965167b618
--- /dev/null
+++ b/data/icons/hicolor_status_scalable_rss-symbolic.svg
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="32"
+   height="32"
+   viewBox="0 0 8.4666665 8.4666669"
+   version="1.1"
+   id="svg5"
+   xmlns:xlink="http://www.w3.org/1999/xlink";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:svg="http://www.w3.org/2000/svg";>
+  <defs
+     id="defs2">
+    <linearGradient
+       id="grad">
+      <stop
+         style="stop-color:#f6af7a;stop-opacity:1;"
+         offset="0"
+         id="stop6983" />
+      <stop
+         style="stop-color:#d94027;stop-opacity:1;"
+         offset="1"
+         id="stop6985" />
+    </linearGradient>
+    <linearGradient
+       xlink:href="#grad"
+       id="linearGradient7143"
+       x1="-10.318749"
+       y1="8.4666662"
+       x2="-3.7041667"
+       y2="14.816667"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.9,0,0,0.9,10.424574,-6.2441669)" />
+    <filter
+       style="color-interpolation-filters:sRGB"
+       id="filter9649"
+       x="-0.66035874"
+       y="-0.66035874"
+       width="2.3207175"
+       height="2.3207175">
+      <feGaussianBlur
+         stdDeviation="0.053035311"
+         id="feGaussianBlur9651" />
+    </filter>
+    <filter
+       style="color-interpolation-filters:sRGB"
+       id="filter9641-6"
+       x="-0.12872887"
+       y="-0.12287756"
+       width="1.2574583"
+       height="1.2457551">
+      <feGaussianBlur
+         stdDeviation="0.053035311"
+         id="feGaussianBlur9643-7" />
+    </filter>
+    <filter
+       style="color-interpolation-filters:sRGB"
+       id="filter9641-6-3"
+       x="-0.19927554"
+       y="-0.19021757"
+       width="1.3985535"
+       height="1.3804351">
+      <feGaussianBlur
+         stdDeviation="0.053035311"
+         id="feGaussianBlur9643-7-5" />
+    </filter>
+    <linearGradient
+       xlink:href="#grad"
+       id="linearGradient10019"
+       x1="0.59531248"
+       y1="4.2333331"
+       x2="7.8713541"
+       y2="4.2333331"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.9,0,0,0.9,0.42332501,0.42333333)" />
+  </defs>
+  <g
+     id="layer1">
+    <path
+       
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.17593;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter9641-6)"
+       d="m -10.583333,-0.7937499 c 0.440964,0 0.8819353,0 1.4555821,0.13243357 0.5736469,0.13243356 
1.2783243,0.39668758 1.8519057,0.79389359 0.5735815,0.39720602 1.0143588,0.92613874 1.3670214,1.49931474 
0.3526627,0.5731761 0.6172246,1.190487 0.7494827,1.7638745 0.132258,0.5733874 0.1322581,1.1021462 
0.1322581,1.6313167"
+       id="path1371-5"
+       transform="matrix(0.729,0,0,0.729,9.8048219,2.7623248)" />
+    <path
+       
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.95988;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter9641-6-3)"
+       d="m -10.583333,-0.7937499 c 0.440964,0 0.8819353,0 1.4555821,0.13243357 0.5736469,0.13243356 
1.2783243,0.39668758 1.8519057,0.79389359 0.5735815,0.39720602 1.0143588,0.92613874 1.3670214,1.49931474 
0.3526627,0.5731761 0.6172246,1.190487 0.7494827,1.7638745 0.132258,0.5733874 0.1322581,1.1021462 
0.1322581,1.6313167"
+       id="path1371-5-6"
+       transform="matrix(0.4374,0,0,0.4374,6.6753693,4.2499621)" />
+    <circle
+       
style="fill:#000000;stroke:#000000;stroke-width:0.79375;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;paint-order:markers
 stroke fill;filter:url(#filter9649)"
+       id="path1690"
+       cx="1.911677"
+       cy="6.4105716"
+       r="0.39687499"
+       transform="matrix(0.81,0,0,0.81,0.74481282,0.91615312)" />
+  </g>
+</svg>
diff --git a/data/icons/hicolor_status_scalable_rss.svg b/data/icons/hicolor_status_scalable_rss.svg
new file mode 100644
index 0000000000..74d174fba6
--- /dev/null
+++ b/data/icons/hicolor_status_scalable_rss.svg
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="32"
+   height="32"
+   viewBox="0 0 8.4666665 8.4666669"
+   version="1.1"
+   id="svg5"
+   xmlns:xlink="http://www.w3.org/1999/xlink";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:svg="http://www.w3.org/2000/svg";>
+  <defs
+     id="defs2">
+    <linearGradient
+       id="grad">
+      <stop
+         style="stop-color:#f6af7a;stop-opacity:1;"
+         offset="0"
+         id="stop6983" />
+      <stop
+         style="stop-color:#d94027;stop-opacity:1;"
+         offset="1"
+         id="stop6985" />
+    </linearGradient>
+    <linearGradient
+       xlink:href="#grad"
+       id="linearGradient7143"
+       x1="-10.318749"
+       y1="8.4666662"
+       x2="-3.7041667"
+       y2="14.816667"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.9,0,0,0.9,10.424574,-6.2441669)" />
+    <filter
+       style="color-interpolation-filters:sRGB"
+       id="filter9649"
+       x="-0.66035874"
+       y="-0.66035874"
+       width="2.3207175"
+       height="2.3207175">
+      <feGaussianBlur
+         stdDeviation="0.053035311"
+         id="feGaussianBlur9651" />
+    </filter>
+    <filter
+       style="color-interpolation-filters:sRGB"
+       id="filter9641-6"
+       x="-0.12872887"
+       y="-0.12287756"
+       width="1.2574583"
+       height="1.2457551">
+      <feGaussianBlur
+         stdDeviation="0.053035311"
+         id="feGaussianBlur9643-7" />
+    </filter>
+    <filter
+       style="color-interpolation-filters:sRGB"
+       id="filter9641-6-3"
+       x="-0.19927554"
+       y="-0.19021757"
+       width="1.3985535"
+       height="1.3804351">
+      <feGaussianBlur
+         stdDeviation="0.053035311"
+         id="feGaussianBlur9643-7-5" />
+    </filter>
+    <linearGradient
+       xlink:href="#grad"
+       id="linearGradient10019"
+       x1="0.59531248"
+       y1="4.2333331"
+       x2="7.8713541"
+       y2="4.2333331"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.9,0,0,0.9,0.42332501,0.42333333)" />
+  </defs>
+  <g
+     id="layer1">
+    <g
+       id="g1263">
+      <rect
+         
style="opacity:1;fill:url(#linearGradient7143);fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient10019);stroke-width:0.47625;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers
 stroke fill"
+         id="rect6981"
+         width="6.1911001"
+         height="6.1912498"
+         x="1.1377"
+         y="1.1377083"
+         rx="1.1691136"
+         ry="1.1611228" />
+      <path
+         
style="fill:none;fill-rule:evenodd;stroke:#f9f9f9;stroke-width:1.17593;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter9641-6)"
+         d="m -10.583333,-0.7937499 c 0.440964,0 0.8819353,0 1.4555821,0.13243357 0.5736469,0.13243356 
1.2783243,0.39668758 1.8519057,0.79389359 0.5735815,0.39720602 1.0143588,0.92613874 1.3670214,1.49931474 
0.3526627,0.5731761 0.6172246,1.190487 0.7494827,1.7638745 0.132258,0.5733874 0.1322581,1.1021462 
0.1322581,1.6313167"
+         id="path1371-5"
+         transform="matrix(0.729,0,0,0.729,9.8048219,2.7623248)" />
+      <path
+         
style="fill:none;fill-rule:evenodd;stroke:#f9f9f9;stroke-width:1.95988;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter9641-6-3)"
+         d="m -10.583333,-0.7937499 c 0.440964,0 0.8819353,0 1.4555821,0.13243357 0.5736469,0.13243356 
1.2783243,0.39668758 1.8519057,0.79389359 0.5735815,0.39720602 1.0143588,0.92613874 1.3670214,1.49931474 
0.3526627,0.5731761 0.6172246,1.190487 0.7494827,1.7638745 0.132258,0.5733874 0.1322581,1.1021462 
0.1322581,1.6313167"
+         id="path1371-5-6"
+         transform="matrix(0.4374,0,0,0.4374,6.6753693,4.2499621)" />
+      <circle
+         
style="fill:#f9f9f9;stroke:#f9f9f9;stroke-width:0.79375;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;paint-order:markers
 stroke fill;filter:url(#filter9649)"
+         id="path1690"
+         cx="1.911677"
+         cy="6.4105716"
+         r="0.39687499"
+         transform="matrix(0.81,0,0,0.81,0.74481282,0.91615312)" />
+    </g>
+  </g>
+</svg>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index eed67030f2..89f510ae74 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -482,6 +482,14 @@ src/modules/prefer-plain/e-mail-display-popup-prefer-plain.c
 src/modules/prefer-plain/e-mail-parser-prefer-plain.c
 src/modules/prefer-plain/plugin/config-ui.c
 src/modules/prefer-plain/plugin/org-gnome-prefer-plain.eplug.xml
+src/modules/rss/camel/camel-rss-folder.c
+src/modules/rss/camel/camel-rss-folder-summary.c
+src/modules/rss/camel/camel-rss-provider.c
+src/modules/rss/camel/camel-rss-store.c
+src/modules/rss/e-rss-parser.c
+src/modules/rss/evolution/e-rss-preferences.c
+src/modules/rss/evolution/e-rss-shell-extension.c
+src/modules/rss/evolution/e-rss-shell-view-extension.c
 src/modules/spamassassin/evolution-spamassassin.c
 src/modules/spamassassin/org.gnome.Evolution-spamassassin.metainfo.xml.in
 src/modules/startup-wizard/e-mail-config-import-page.c
diff --git a/src/modules/CMakeLists.txt b/src/modules/CMakeLists.txt
index 66e835c2cf..d995f4dc60 100644
--- a/src/modules/CMakeLists.txt
+++ b/src/modules/CMakeLists.txt
@@ -85,6 +85,7 @@ add_subdirectory(offline-alert)
 add_subdirectory(plugin-lib)
 add_subdirectory(plugin-manager)
 add_subdirectory(prefer-plain)
+add_subdirectory(rss)
 add_subdirectory(settings)
 add_subdirectory(startup-wizard)
 add_subdirectory(vcard-inline)
diff --git a/src/modules/rss/CMakeLists.txt b/src/modules/rss/CMakeLists.txt
new file mode 100644
index 0000000000..b7442fd613
--- /dev/null
+++ b/src/modules/rss/CMakeLists.txt
@@ -0,0 +1,2 @@
+add_subdirectory(camel)
+add_subdirectory(evolution)
diff --git a/src/modules/rss/camel-rss-store-summary.c b/src/modules/rss/camel-rss-store-summary.c
new file mode 100644
index 0000000000..ec9a6bf431
--- /dev/null
+++ b/src/modules/rss/camel-rss-store-summary.c
@@ -0,0 +1,852 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-config.h"
+
+#include <errno.h>
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <libedataserver/libedataserver.h>
+
+#include "camel-rss-store-summary.h"
+
+struct _CamelRssStoreSummaryPrivate {
+       GRecMutex mutex;
+       gboolean dirty;
+       gchar *filename;
+       GHashTable *feeds; /* gchar *uid ~> RssFeed * */
+};
+
+enum {
+       FEED_CHANGED,
+       LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL];
+
+G_DEFINE_TYPE_WITH_PRIVATE (CamelRssStoreSummary, camel_rss_store_summary, G_TYPE_OBJECT)
+
+typedef struct _RssFeed {
+       guint index; /* to preserve order of adding */
+       gchar *href;
+       gchar *display_name;
+       gchar *icon_filename;
+       CamelRssContentType content_type;
+       guint32 total_count;
+       guint32 unread_count;
+       gint64 last_updated;
+} RssFeed;
+
+static void
+rss_feed_free (gpointer ptr)
+{
+       RssFeed *feed = ptr;
+
+       if (feed) {
+               g_free (feed->href);
+               g_free (feed->display_name);
+               g_free (feed->icon_filename);
+               g_free (feed);
+       }
+}
+
+typedef struct _EmitIdleData {
+       GWeakRef *weak_ref;
+       gchar *id;
+} EmitIdleData;
+
+static void
+emit_idle_data_free (gpointer ptr)
+{
+       EmitIdleData *eid = ptr;
+
+       if (eid) {
+               e_weak_ref_free (eid->weak_ref);
+               g_free (eid->id);
+               g_slice_free (EmitIdleData, eid);
+       }
+}
+
+static gboolean
+camel_rss_store_summary_emit_feed_changed_cb (gpointer user_data)
+{
+       EmitIdleData *eid = user_data;
+       CamelRssStoreSummary *self;
+
+       self = g_weak_ref_get (eid->weak_ref);
+       if (self) {
+               g_signal_emit (self, signals[FEED_CHANGED], 0, eid->id, NULL);
+               g_object_unref (self);
+       }
+
+       return G_SOURCE_REMOVE;
+}
+
+static void
+camel_rss_store_summary_schedule_feed_changed (CamelRssStoreSummary *self,
+                                              const gchar *id)
+{
+       EmitIdleData *eid;
+
+       eid = g_slice_new (EmitIdleData);
+       eid->weak_ref = e_weak_ref_new (self);
+       eid->id = g_strdup (id);
+
+       g_idle_add_full (G_PRIORITY_HIGH,
+               camel_rss_store_summary_emit_feed_changed_cb,
+               eid, emit_idle_data_free);
+}
+
+static void
+rss_store_summary_finalize (GObject *object)
+{
+       CamelRssStoreSummary *self = CAMEL_RSS_STORE_SUMMARY (object);
+
+       g_hash_table_destroy (self->priv->feeds);
+       g_free (self->priv->filename);
+
+       g_rec_mutex_clear (&self->priv->mutex);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (camel_rss_store_summary_parent_class)->finalize (object);
+}
+
+static void
+camel_rss_store_summary_class_init (CamelRssStoreSummaryClass *klass)
+{
+       GObjectClass *object_class;
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->finalize = rss_store_summary_finalize;
+
+       signals[FEED_CHANGED] = g_signal_new (
+               "feed-changed",
+               G_TYPE_FROM_CLASS (klass),
+               G_SIGNAL_RUN_LAST |
+               G_SIGNAL_ACTION,
+               0,
+               NULL, NULL, NULL,
+               G_TYPE_NONE, 1,
+               G_TYPE_STRING);
+}
+
+static void
+camel_rss_store_summary_init (CamelRssStoreSummary *self)
+{
+       self->priv = camel_rss_store_summary_get_instance_private (self);
+
+       self->priv->dirty = FALSE;
+       self->priv->feeds = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, rss_feed_free);
+
+       g_rec_mutex_init (&self->priv->mutex);
+}
+
+CamelRssStoreSummary *
+camel_rss_store_summary_new (const gchar *filename)
+{
+       CamelRssStoreSummary *self = g_object_new (CAMEL_TYPE_RSS_STORE_SUMMARY, NULL);
+
+       self->priv->filename = g_strdup (filename);
+
+       return self;
+}
+
+void
+camel_rss_store_summary_lock (CamelRssStoreSummary *self)
+{
+       g_return_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self));
+
+       g_rec_mutex_lock (&self->priv->mutex);
+}
+
+void
+camel_rss_store_summary_unlock (CamelRssStoreSummary *self)
+{
+       g_return_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self));
+
+       g_rec_mutex_unlock (&self->priv->mutex);
+}
+
+static gint
+compare_feeds_by_index (gconstpointer fd1,
+                       gconstpointer fd2)
+{
+       const RssFeed *feed1 = fd1, *feed2 = fd2;
+
+       if (!feed1 || !feed2)
+               return 0;
+
+       return feed1->index - feed2->index;
+}
+
+gboolean
+camel_rss_store_summary_load (CamelRssStoreSummary *self,
+                             GError **error)
+{
+       GKeyFile *key_file;
+       GError *local_error = NULL;
+       gboolean success;
+
+       g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), FALSE);
+
+       camel_rss_store_summary_lock (self);
+
+       g_hash_table_remove_all (self->priv->feeds);
+
+       key_file = g_key_file_new ();
+       success = g_key_file_load_from_file (key_file, self->priv->filename, G_KEY_FILE_NONE, &local_error);
+
+       if (success) {
+               GSList *feeds = NULL, *link;
+               gchar **groups;
+               guint ii;
+
+               groups = g_key_file_get_groups (key_file, NULL);
+
+               for (ii = 0; groups && groups[ii]; ii++) {
+                       const gchar *group = groups[ii];
+
+                       if (g_str_has_prefix (group, "feed:")) {
+                               RssFeed *feed;
+
+                               feed = g_new0 (RssFeed, 1);
+                               feed->href = g_key_file_get_string (key_file, group, "href", NULL);
+                               feed->display_name = g_key_file_get_string (key_file, group, "display-name", 
NULL);
+                               feed->icon_filename = g_key_file_get_string (key_file, group, 
"icon-filename", NULL);
+                               feed->content_type = g_key_file_get_integer (key_file, group, "content-type", 
NULL);
+                               feed->total_count = (guint32) g_key_file_get_uint64 (key_file, group, 
"total-count", NULL);
+                               feed->unread_count = (guint32) g_key_file_get_uint64 (key_file, group, 
"unread-count", NULL);
+                               feed->last_updated = g_key_file_get_int64 (key_file, group, "last-updated", 
NULL);
+                               feed->index = (gint) g_key_file_get_int64 (key_file, group, "index", NULL);
+
+                               if (feed->href && *feed->href && feed->display_name && *feed->display_name) {
+                                       if (feed->icon_filename && !*feed->icon_filename)
+                                               g_clear_pointer (&feed->icon_filename, g_free);
+
+                                       g_hash_table_insert (self->priv->feeds, g_strdup (group + 5 /* strlen 
("feed:") */), feed);
+
+                                       feeds = g_slist_prepend (feeds, feed);
+                               } else {
+                                       rss_feed_free (feed);
+                               }
+                       }
+               }
+
+               /* renumber indexes on load */
+               feeds = g_slist_sort (feeds, compare_feeds_by_index);
+
+               for (ii = 1, link = feeds; link; ii++, link = g_slist_next (link)) {
+                       RssFeed *feed = link->data;
+
+                       feed->index = ii;
+               }
+
+               g_slist_free (feeds);
+               g_strfreev (groups);
+       } else {
+               if (g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) {
+                       success = TRUE;
+                       g_clear_error (&local_error);
+               } else {
+                       g_propagate_error (error, local_error);
+               }
+       }
+
+       g_key_file_free (key_file);
+
+       self->priv->dirty = FALSE;
+
+       camel_rss_store_summary_unlock (self);
+
+       return success;
+}
+
+gboolean
+camel_rss_store_summary_save (CamelRssStoreSummary *self,
+                             GError **error)
+{
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), FALSE);
+
+       camel_rss_store_summary_lock (self);
+
+       if (self->priv->dirty) {
+               GKeyFile *key_file;
+               GHashTableIter iter;
+               gpointer key, value;
+
+               key_file = g_key_file_new ();
+
+               g_hash_table_iter_init (&iter, self->priv->feeds);
+
+               while (g_hash_table_iter_next (&iter, &key, &value)) {
+                       const gchar *id = key;
+                       const RssFeed *feed = value;
+                       gchar *group = g_strconcat ("feed:", id, NULL);
+
+                       g_key_file_set_string (key_file, group, "href", feed->href);
+                       g_key_file_set_string (key_file, group, "display-name", feed->display_name);
+                       g_key_file_set_string (key_file, group, "icon-filename", feed->icon_filename ? 
feed->icon_filename : "");
+                       g_key_file_set_integer (key_file, group, "content-type", feed->content_type);
+                       g_key_file_set_uint64 (key_file, group, "total-count", feed->total_count);
+                       g_key_file_set_uint64 (key_file, group, "unread-count", feed->unread_count);
+                       g_key_file_set_int64 (key_file, group, "last-updated", feed->last_updated);
+                       g_key_file_set_int64 (key_file, group, "index", feed->index);
+
+                       g_free (group);
+               }
+
+               success = g_key_file_save_to_file (key_file, self->priv->filename, error);
+
+               g_key_file_free (key_file);
+
+               self->priv->dirty = !success;
+       }
+
+       camel_rss_store_summary_unlock (self);
+
+       return success;
+}
+
+const gchar *
+camel_rss_store_summary_add (CamelRssStoreSummary *self,
+                            const gchar *href,
+                            const gchar *display_name,
+                            const gchar *icon_filename,
+                            CamelRssContentType content_type)
+{
+       RssFeed *feed;
+       gchar *id;
+       guint index = 1;
+
+       g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), NULL);
+       g_return_val_if_fail (href != NULL, NULL);
+       g_return_val_if_fail (display_name != NULL, NULL);
+
+       camel_rss_store_summary_lock (self);
+
+       self->priv->dirty = TRUE;
+
+       id = g_compute_checksum_for_string (G_CHECKSUM_SHA1, href, -1);
+
+       while (g_hash_table_contains (self->priv->feeds, id) && index != 0) {
+               gchar *tmp;
+
+               tmp = g_strdup_printf ("%s::%u", href, index);
+               g_free (id);
+               id = g_compute_checksum_for_string (G_CHECKSUM_SHA1, tmp, -1);
+               g_free (tmp);
+               index++;
+       }
+
+       feed = g_new0 (RssFeed, 1);
+       feed->href = g_strdup (href);
+       feed->display_name = g_strdup (display_name);
+       feed->icon_filename = g_strdup (icon_filename);
+       feed->content_type = content_type;
+       feed->index = g_hash_table_size (self->priv->feeds) + 1;
+
+       g_hash_table_insert (self->priv->feeds, id, feed);
+
+       camel_rss_store_summary_unlock (self);
+       camel_rss_store_summary_schedule_feed_changed (self, id);
+
+       return id;
+}
+
+static void
+camel_rss_store_summary_maybe_remove_filename (CamelRssStoreSummary *self,
+                                              const gchar *filename)
+{
+       if (filename && *filename) {
+               gchar *prefix, *dirsep;
+
+               prefix = g_strdup (self->priv->filename);
+               dirsep = strrchr (prefix, G_DIR_SEPARATOR);
+
+               if (dirsep) {
+                       dirsep[1] = '\0';
+
+                       if (g_str_has_prefix (filename, prefix) &&
+                           g_unlink (filename) == -1) {
+                               gint errn = errno;
+
+                               if (errn != ENOENT && camel_debug ("rss"))
+                                       g_printerr ("%s: Failed to delete '%s': %s", G_STRFUNC, filename, 
g_strerror (errn));
+                       }
+               }
+
+               g_free (prefix);
+       }
+}
+
+gboolean
+camel_rss_store_summary_remove (CamelRssStoreSummary *self,
+                               const gchar *id)
+{
+       RssFeed *feed;
+       gboolean result = FALSE;
+
+       g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), FALSE);
+       g_return_val_if_fail (id != NULL, FALSE);
+
+       camel_rss_store_summary_lock (self);
+
+       feed = g_hash_table_lookup (self->priv->feeds, id);
+
+       if (feed) {
+               guint removed_index = feed->index;
+
+               camel_rss_store_summary_maybe_remove_filename (self, feed->icon_filename);
+
+               result = g_hash_table_remove (self->priv->feeds, id);
+
+               /* Correct indexes of the left feeds */
+               if (result) {
+                       GHashTableIter iter;
+                       gpointer value;
+
+                       g_hash_table_iter_init (&iter, self->priv->feeds);
+                       while (g_hash_table_iter_next (&iter, NULL, &value)) {
+                               RssFeed *feed2 = value;
+
+                               if (feed2 && feed2->index > removed_index)
+                                       feed2->index--;
+                       }
+               }
+       }
+
+       if (result)
+               self->priv->dirty = TRUE;
+
+       camel_rss_store_summary_unlock (self);
+
+       if (result)
+               camel_rss_store_summary_schedule_feed_changed (self, id);
+
+       return result;
+}
+
+gboolean
+camel_rss_store_summary_contains (CamelRssStoreSummary *self,
+                                 const gchar *id)
+{
+       gboolean result = FALSE;
+
+       g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), FALSE);
+       g_return_val_if_fail (id != NULL, FALSE);
+
+       camel_rss_store_summary_lock (self);
+
+       result = g_hash_table_contains (self->priv->feeds, id);
+
+       camel_rss_store_summary_unlock (self);
+
+       return result;
+}
+
+static gint
+compare_ids_by_index (gconstpointer id1,
+                     gconstpointer id2,
+                     gpointer user_data)
+{
+       GHashTable *feeds = user_data;
+       RssFeed *feed1, *feed2;
+
+       feed1 = g_hash_table_lookup (feeds, id1);
+       feed2 = g_hash_table_lookup (feeds, id2);
+
+       if (!feed1 || !feed2)
+               return 0;
+
+       return feed1->index - feed2->index;
+}
+
+GSList * /* gchar *id */
+camel_rss_store_summary_dup_feeds (CamelRssStoreSummary *self)
+{
+       GSList *ids = NULL;
+       GHashTableIter iter;
+       gpointer key;
+
+       g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), NULL);
+
+       camel_rss_store_summary_lock (self);
+
+       g_hash_table_iter_init (&iter, self->priv->feeds);
+
+       while (g_hash_table_iter_next (&iter, &key, NULL)) {
+               ids = g_slist_prepend (ids, g_strdup (key));
+       }
+
+       ids = g_slist_sort_with_data (ids, compare_ids_by_index, self->priv->feeds);
+
+       camel_rss_store_summary_unlock (self);
+
+       return ids;
+}
+
+CamelFolderInfo *
+camel_rss_store_summary_dup_folder_info        (CamelRssStoreSummary *self,
+                                        const gchar *id)
+{
+       RssFeed *feed;
+       CamelFolderInfo *fi = NULL;
+
+       g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), NULL);
+       g_return_val_if_fail (id != NULL, NULL);
+
+       camel_rss_store_summary_lock (self);
+
+       feed = g_hash_table_lookup (self->priv->feeds, id);
+       if (feed) {
+               fi = camel_folder_info_new ();
+               fi->full_name = g_strdup (id);
+               fi->display_name = g_strdup (feed->display_name);
+               fi->flags = CAMEL_FOLDER_NOCHILDREN;
+               fi->unread = feed->unread_count;
+               fi->total = feed->total_count;
+       }
+
+       camel_rss_store_summary_unlock (self);
+
+       return fi;
+}
+
+CamelFolderInfo *
+camel_rss_store_summary_dup_folder_info_for_display_name (CamelRssStoreSummary *self,
+                                                         const gchar *display_name)
+{
+       CamelFolderInfo *fi = NULL;
+       GHashTableIter iter;
+       gpointer key, value;
+
+       g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), NULL);
+       g_return_val_if_fail (display_name != NULL, NULL);
+
+       camel_rss_store_summary_lock (self);
+
+       g_hash_table_iter_init (&iter, self->priv->feeds);
+
+       while (g_hash_table_iter_next (&iter, &key, &value)) {
+               const gchar *id = key;
+               RssFeed *feed = value;
+
+               if (g_strcmp0 (display_name, feed->display_name) == 0) {
+                       fi = camel_rss_store_summary_dup_folder_info (self, id);
+                       break;
+               }
+       }
+
+       camel_rss_store_summary_unlock (self);
+
+       return fi;
+}
+
+const gchar *
+camel_rss_store_summary_get_href (CamelRssStoreSummary *self,
+                                 const gchar *id)
+{
+       RssFeed *feed;
+       const gchar *result = NULL;
+
+       g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), NULL);
+       g_return_val_if_fail (id != NULL, NULL);
+
+       camel_rss_store_summary_lock (self);
+
+       feed = g_hash_table_lookup (self->priv->feeds, id);
+       if (feed)
+               result = feed->href;
+
+       camel_rss_store_summary_unlock (self);
+
+       return result;
+}
+
+const gchar *
+camel_rss_store_summary_get_display_name (CamelRssStoreSummary *self,
+                                         const gchar *id)
+{
+       RssFeed *feed;
+       const gchar *result = NULL;
+
+       g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), NULL);
+       g_return_val_if_fail (id != NULL, NULL);
+
+       camel_rss_store_summary_lock (self);
+
+       feed = g_hash_table_lookup (self->priv->feeds, id);
+       if (feed)
+               result = feed->display_name;
+
+       camel_rss_store_summary_unlock (self);
+
+       return result;
+}
+
+void
+camel_rss_store_summary_set_display_name (CamelRssStoreSummary *self,
+                                         const gchar *id,
+                                         const gchar *display_name)
+{
+       RssFeed *feed;
+       gboolean changed = FALSE;
+
+       g_return_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self));
+       g_return_if_fail (id != NULL);
+       g_return_if_fail (display_name && *display_name);
+
+       camel_rss_store_summary_lock (self);
+
+       feed = g_hash_table_lookup (self->priv->feeds, id);
+       if (feed) {
+               if (g_strcmp0 (feed->display_name, display_name) != 0) {
+                       g_free (feed->display_name);
+                       feed->display_name = g_strdup (display_name);
+                       self->priv->dirty = TRUE;
+                       changed = TRUE;
+               }
+       }
+
+       camel_rss_store_summary_unlock (self);
+
+       if (changed)
+               camel_rss_store_summary_schedule_feed_changed (self, id);
+}
+
+const gchar *
+camel_rss_store_summary_get_icon_filename (CamelRssStoreSummary *self,
+                                          const gchar *id)
+{
+       RssFeed *feed;
+       const gchar *result = NULL;
+
+       g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), NULL);
+       g_return_val_if_fail (id != NULL, NULL);
+
+       camel_rss_store_summary_lock (self);
+
+       feed = g_hash_table_lookup (self->priv->feeds, id);
+       if (feed)
+               result = feed->icon_filename;
+
+       camel_rss_store_summary_unlock (self);
+
+       return result;
+}
+
+void
+camel_rss_store_summary_set_icon_filename (CamelRssStoreSummary *self,
+                                          const gchar *id,
+                                          const gchar *icon_filename)
+{
+       RssFeed *feed;
+       gboolean changed = FALSE;
+
+       g_return_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self));
+       g_return_if_fail (id != NULL);
+
+       camel_rss_store_summary_lock (self);
+
+       feed = g_hash_table_lookup (self->priv->feeds, id);
+       if (feed) {
+               if (g_strcmp0 (feed->icon_filename, icon_filename) != 0) {
+                       camel_rss_store_summary_maybe_remove_filename (self, feed->icon_filename);
+                       g_free (feed->icon_filename);
+                       feed->icon_filename = g_strdup (icon_filename);
+                       self->priv->dirty = TRUE;
+                       changed = TRUE;
+               }
+       }
+
+       camel_rss_store_summary_unlock (self);
+
+       if (changed)
+               camel_rss_store_summary_schedule_feed_changed (self, id);
+}
+
+CamelRssContentType
+camel_rss_store_summary_get_content_type (CamelRssStoreSummary *self,
+                                         const gchar *id)
+{
+       RssFeed *feed;
+       CamelRssContentType result = CAMEL_RSS_CONTENT_TYPE_HTML;
+
+       g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), result);
+       g_return_val_if_fail (id != NULL, result);
+
+       camel_rss_store_summary_lock (self);
+
+       feed = g_hash_table_lookup (self->priv->feeds, id);
+       if (feed)
+               result = feed->content_type;
+
+       camel_rss_store_summary_unlock (self);
+
+       return result;
+}
+
+void
+camel_rss_store_summary_set_content_type (CamelRssStoreSummary *self,
+                                         const gchar *id,
+                                         CamelRssContentType content_type)
+{
+       RssFeed *feed;
+       gboolean changed = FALSE;
+
+       g_return_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self));
+       g_return_if_fail (id != NULL);
+
+       camel_rss_store_summary_lock (self);
+
+       feed = g_hash_table_lookup (self->priv->feeds, id);
+       if (feed) {
+               if (feed->content_type != content_type) {
+                       feed->content_type = content_type;
+                       self->priv->dirty = TRUE;
+                       changed = TRUE;
+               }
+       }
+
+       camel_rss_store_summary_unlock (self);
+
+       if (changed)
+               camel_rss_store_summary_schedule_feed_changed (self, id);
+}
+
+guint32
+camel_rss_store_summary_get_total_count (CamelRssStoreSummary *self,
+                                        const gchar *id)
+{
+       RssFeed *feed;
+       guint32 result = 0;
+
+       g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), 0);
+       g_return_val_if_fail (id != NULL, 0);
+
+       camel_rss_store_summary_lock (self);
+
+       feed = g_hash_table_lookup (self->priv->feeds, id);
+       if (feed)
+               result = feed->total_count;
+
+       camel_rss_store_summary_unlock (self);
+
+       return result;
+}
+
+void
+camel_rss_store_summary_set_total_count (CamelRssStoreSummary *self,
+                                        const gchar *id,
+                                        guint32 total_count)
+{
+       RssFeed *feed;
+
+       g_return_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self));
+       g_return_if_fail (id != NULL);
+
+       camel_rss_store_summary_lock (self);
+
+       feed = g_hash_table_lookup (self->priv->feeds, id);
+       if (feed) {
+               if (feed->total_count != total_count) {
+                       feed->total_count = total_count;
+                       self->priv->dirty = TRUE;
+               }
+       }
+
+       camel_rss_store_summary_unlock (self);
+}
+
+guint32
+camel_rss_store_summary_get_unread_count (CamelRssStoreSummary *self,
+                                         const gchar *id)
+{
+       RssFeed *feed;
+       guint32 result = 0;
+
+       g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), 0);
+       g_return_val_if_fail (id != NULL, 0);
+
+       camel_rss_store_summary_lock (self);
+
+       feed = g_hash_table_lookup (self->priv->feeds, id);
+       if (feed)
+               result = feed->unread_count;
+
+       camel_rss_store_summary_unlock (self);
+
+       return result;
+}
+
+void
+camel_rss_store_summary_set_unread_count (CamelRssStoreSummary *self,
+                                         const gchar *id,
+                                         guint32 unread_count)
+{
+       RssFeed *feed;
+
+       g_return_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self));
+       g_return_if_fail (id != NULL);
+
+       camel_rss_store_summary_lock (self);
+
+       feed = g_hash_table_lookup (self->priv->feeds, id);
+       if (feed) {
+               if (feed->unread_count != unread_count) {
+                       feed->unread_count = unread_count;
+                       self->priv->dirty = TRUE;
+               }
+       }
+
+       camel_rss_store_summary_unlock (self);
+}
+
+gint64
+camel_rss_store_summary_get_last_updated (CamelRssStoreSummary *self,
+                                         const gchar *id)
+{
+       RssFeed *feed;
+       gint64 result = 0;
+
+       g_return_val_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self), 0);
+       g_return_val_if_fail (id != NULL, 0);
+
+       camel_rss_store_summary_lock (self);
+
+       feed = g_hash_table_lookup (self->priv->feeds, id);
+       if (feed)
+               result = feed->last_updated;
+
+       camel_rss_store_summary_unlock (self);
+
+       return result;
+}
+
+void
+camel_rss_store_summary_set_last_updated (CamelRssStoreSummary *self,
+                                         const gchar *id,
+                                         gint64 last_updated)
+{
+       RssFeed *feed;
+
+       g_return_if_fail (CAMEL_IS_RSS_STORE_SUMMARY (self));
+       g_return_if_fail (id != NULL);
+
+       camel_rss_store_summary_lock (self);
+
+       feed = g_hash_table_lookup (self->priv->feeds, id);
+       if (feed) {
+               if (feed->last_updated != last_updated) {
+                       feed->last_updated = last_updated;
+                       self->priv->dirty = TRUE;
+               }
+       }
+
+       camel_rss_store_summary_unlock (self);
+}
diff --git a/src/modules/rss/camel-rss-store-summary.h b/src/modules/rss/camel-rss-store-summary.h
new file mode 100644
index 0000000000..9c1b2b36e0
--- /dev/null
+++ b/src/modules/rss/camel-rss-store-summary.h
@@ -0,0 +1,116 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef CAMEL_RSS_STORE_SUMMARY_H
+#define CAMEL_RSS_STORE_SUMMARY_H
+
+#include <camel/camel.h>
+
+/* Standard GObject macros */
+#define CAMEL_TYPE_RSS_STORE_SUMMARY \
+       (camel_rss_store_summary_get_type ())
+#define CAMEL_RSS_STORE_SUMMARY(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), CAMEL_TYPE_RSS_STORE_SUMMARY, CamelRssStoreSummary))
+#define CAMEL_RSS_STORE_SUMMARY_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), CAMEL_TYPE_RSS_STORE_SUMMARY, CamelRssStoreSummaryClass))
+#define CAMEL_IS_RSS_STORE_SUMMARY(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), CAMEL_TYPE_RSS_STORE_SUMMARY))
+#define CAMEL_IS_RSS_STORE_SUMMARY_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), CAMEL_TYPE_RSS_STORE_SUMMARY))
+#define CAMEL_RSS_STORE_SUMMARY_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), CAMEL_TYPE_RSS_STORE_SUMMARY, CamelRssStoreSummaryClass))
+
+G_BEGIN_DECLS
+
+typedef enum {
+       CAMEL_RSS_CONTENT_TYPE_HTML,
+       CAMEL_RSS_CONTENT_TYPE_PLAIN_TEXT,
+       CAMEL_RSS_CONTENT_TYPE_MARKDOWN
+} CamelRssContentType;
+
+typedef struct _CamelRssStoreSummary CamelRssStoreSummary;
+typedef struct _CamelRssStoreSummaryClass CamelRssStoreSummaryClass;
+typedef struct _CamelRssStoreSummaryPrivate CamelRssStoreSummaryPrivate;
+
+struct _CamelRssStoreSummary {
+       GObject object;
+       CamelRssStoreSummaryPrivate *priv;
+};
+
+struct _CamelRssStoreSummaryClass {
+       GObjectClass object_class;
+};
+
+GType          camel_rss_store_summary_get_type        (void);
+CamelRssStoreSummary *
+               camel_rss_store_summary_new             (const gchar *filename);
+void           camel_rss_store_summary_lock            (CamelRssStoreSummary *self);
+void           camel_rss_store_summary_unlock          (CamelRssStoreSummary *self);
+gboolean       camel_rss_store_summary_load            (CamelRssStoreSummary *self,
+                                                        GError **error);
+gboolean       camel_rss_store_summary_save            (CamelRssStoreSummary *self,
+                                                        GError **error);
+const gchar *  camel_rss_store_summary_add             (CamelRssStoreSummary *self,
+                                                        const gchar *href,
+                                                        const gchar *display_name,
+                                                        const gchar *icon_filename,
+                                                        CamelRssContentType content_type);
+gboolean       camel_rss_store_summary_remove          (CamelRssStoreSummary *self,
+                                                        const gchar *id);
+gboolean       camel_rss_store_summary_contains        (CamelRssStoreSummary *self,
+                                                        const gchar *id);
+GSList *       camel_rss_store_summary_dup_feeds       (CamelRssStoreSummary *self); /* gchar *id */
+CamelFolderInfo *
+               camel_rss_store_summary_dup_folder_info (CamelRssStoreSummary *self,
+                                                        const gchar *id);
+CamelFolderInfo *
+               camel_rss_store_summary_dup_folder_info_for_display_name
+                                                       (CamelRssStoreSummary *self,
+                                                        const gchar *display_name);
+const gchar *  camel_rss_store_summary_get_href        (CamelRssStoreSummary *self,
+                                                        const gchar *id);
+const gchar *  camel_rss_store_summary_get_display_name(CamelRssStoreSummary *self,
+                                                        const gchar *id);
+void           camel_rss_store_summary_set_display_name(CamelRssStoreSummary *self,
+                                                        const gchar *id,
+                                                        const gchar *display_name);
+const gchar *  camel_rss_store_summary_get_icon_filename
+                                                       (CamelRssStoreSummary *self,
+                                                        const gchar *id);
+void           camel_rss_store_summary_set_icon_filename
+                                                       (CamelRssStoreSummary *self,
+                                                        const gchar *id,
+                                                        const gchar *filename);
+CamelRssContentType
+               camel_rss_store_summary_get_content_type(CamelRssStoreSummary *self,
+                                                        const gchar *id);
+void           camel_rss_store_summary_set_content_type(CamelRssStoreSummary *self,
+                                                        const gchar *id,
+                                                        CamelRssContentType content_type);
+guint32                camel_rss_store_summary_get_total_count (CamelRssStoreSummary *self,
+                                                        const gchar *id);
+void           camel_rss_store_summary_set_total_count (CamelRssStoreSummary *self,
+                                                        const gchar *id,
+                                                        guint32 total_count);
+guint32                camel_rss_store_summary_get_unread_count(CamelRssStoreSummary *self,
+                                                        const gchar *id);
+void           camel_rss_store_summary_set_unread_count(CamelRssStoreSummary *self,
+                                                        const gchar *id,
+                                                        guint32 unread_count);
+gint64         camel_rss_store_summary_get_last_updated(CamelRssStoreSummary *self,
+                                                        const gchar *id);
+void           camel_rss_store_summary_set_last_updated(CamelRssStoreSummary *self,
+                                                        const gchar *id,
+                                                        gint64 last_updated);
+
+G_END_DECLS
+
+#endif /* CAMEL_RSS_STORE_SUMMARY_H */
diff --git a/src/modules/rss/camel/CMakeLists.txt b/src/modules/rss/camel/CMakeLists.txt
new file mode 100644
index 0000000000..1e8ceaff08
--- /dev/null
+++ b/src/modules/rss/camel/CMakeLists.txt
@@ -0,0 +1,50 @@
+pkg_check_modules(LIBEDATASERVER libedataserver-1.2)
+pkg_check_modules(CAMEL camel-1.2)
+pkg_check_variable(camel_providerdir camel-1.2 camel_providerdir)
+
+set(sources
+       camel-rss-folder.c
+       camel-rss-folder.h
+       camel-rss-folder-summary.c
+       camel-rss-folder-summary.h
+       camel-rss-provider.c
+       camel-rss-settings.c
+       camel-rss-settings.h
+       camel-rss-store.c
+       camel-rss-store.h
+       ../camel-rss-store-summary.c
+       ../camel-rss-store-summary.h
+       ../e-rss-parser.h
+       ../e-rss-parser.c
+)
+
+add_library(camelrss MODULE ${sources})
+
+target_compile_definitions(camelrss PRIVATE
+       -DG_LOG_DOMAIN=\"camel-rss-provider\"
+)
+
+target_compile_options(camelrss PUBLIC
+       ${CAMEL_CFLAGS}
+       ${LIBEDATASERVER_CFLAGS}
+)
+
+target_include_directories(camelrss PUBLIC
+       ${CMAKE_BINARY_DIR}
+       ${CMAKE_CURRENT_SOURCE_DIR}/..
+       ${CAMEL_INCLUDE_DIRS}
+       ${LIBEDATASERVER_INCLUDE_DIRS}
+)
+
+target_link_libraries(camelrss
+       ${CAMEL_LDFLAGS}
+       ${LIBEDATASERVER_LDFLAGS}
+)
+
+install(TARGETS camelrss
+       DESTINATION ${camel_providerdir}
+)
+
+install(FILES libcamelrss.urls
+       DESTINATION ${camel_providerdir}
+)
diff --git a/src/modules/rss/camel/camel-rss-folder-summary.c 
b/src/modules/rss/camel/camel-rss-folder-summary.c
new file mode 100644
index 0000000000..940765ed81
--- /dev/null
+++ b/src/modules/rss/camel/camel-rss-folder-summary.c
@@ -0,0 +1,412 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-config.h"
+
+#include <glib/gi18n-lib.h>
+
+#include <camel/camel.h>
+#include <libedataserver/libedataserver.h>
+
+#include "camel-rss-folder.h"
+#include "camel-rss-store.h"
+#include "camel-rss-folder-summary.h"
+
+struct _CamelRssFolderSummaryPrivate {
+       gulong saved_count_id;
+       gulong unread_count_id;
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (CamelRssFolderSummary, camel_rss_folder_summary, CAMEL_TYPE_FOLDER_SUMMARY)
+
+static void
+rss_folder_summary_sync_counts_cb (GObject *object,
+                                  GParamSpec *param,
+                                  gpointer user_data)
+{
+       CamelRssFolderSummary *self = CAMEL_RSS_FOLDER_SUMMARY (object);
+       CamelFolder *folder;
+       CamelStore *parent_store;
+       CamelRssStoreSummary *rss_store_summary;
+       const gchar *id;
+
+       folder = camel_folder_summary_get_folder (CAMEL_FOLDER_SUMMARY (self));
+       parent_store = camel_folder_get_parent_store (folder);
+
+       if (!parent_store)
+               return;
+
+       rss_store_summary = camel_rss_store_get_summary (CAMEL_RSS_STORE (parent_store));
+
+       if (!rss_store_summary)
+               return;
+
+       id = camel_rss_folder_get_id (CAMEL_RSS_FOLDER (folder));
+
+       if (g_strcmp0 (g_param_spec_get_name (param), "saved-count") == 0)
+               camel_rss_store_summary_set_total_count (rss_store_summary, id, 
camel_folder_summary_get_saved_count (CAMEL_FOLDER_SUMMARY (self)));
+       else if (g_strcmp0 (g_param_spec_get_name (param), "unread-count") == 0)
+               camel_rss_store_summary_set_unread_count (rss_store_summary, id, 
camel_folder_summary_get_unread_count (CAMEL_FOLDER_SUMMARY (self)));
+}
+
+static void
+rss_folder_summary_constructed (GObject *object)
+{
+       CamelRssFolderSummary *self = CAMEL_RSS_FOLDER_SUMMARY (object);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (camel_rss_folder_summary_parent_class)->constructed (object);
+
+       self->priv->saved_count_id = g_signal_connect (self, "notify::saved-count",
+               G_CALLBACK (rss_folder_summary_sync_counts_cb), NULL);
+
+       self->priv->unread_count_id = g_signal_connect (self, "notify::unread-count",
+               G_CALLBACK (rss_folder_summary_sync_counts_cb), NULL);
+}
+
+static void
+rss_folder_summary_dispose (GObject *object)
+{
+       CamelRssFolderSummary *self = CAMEL_RSS_FOLDER_SUMMARY (object);
+
+       if (self->priv->saved_count_id) {
+               g_signal_handler_disconnect (self, self->priv->saved_count_id);
+               self->priv->saved_count_id = 0;
+       }
+
+       if (self->priv->unread_count_id) {
+               g_signal_handler_disconnect (self, self->priv->unread_count_id);
+               self->priv->unread_count_id = 0;
+       }
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (camel_rss_folder_summary_parent_class)->dispose (object);
+}
+
+static void
+camel_rss_folder_summary_class_init (CamelRssFolderSummaryClass *class)
+{
+       GObjectClass *object_class;
+
+       object_class = G_OBJECT_CLASS (class);
+       object_class->constructed = rss_folder_summary_constructed;
+       object_class->dispose = rss_folder_summary_dispose;
+}
+
+static void
+camel_rss_folder_summary_init (CamelRssFolderSummary *rss_folder_summary)
+{
+       rss_folder_summary->priv = camel_rss_folder_summary_get_instance_private (rss_folder_summary);
+}
+
+CamelFolderSummary *
+camel_rss_folder_summary_new (CamelFolder *folder)
+{
+       return g_object_new (CAMEL_TYPE_RSS_FOLDER_SUMMARY, "folder", folder, NULL);
+}
+
+CamelMimeMessage *
+camel_rss_folder_summary_dup_message (CamelRssFolderSummary *self,
+                                     const gchar *uid,
+                                     CamelDataCache **out_rss_cache,
+                                     CamelRssContentType *out_content_type,
+                                     GCancellable *cancellable,
+                                     GError **error)
+{
+       CamelFolder *folder;
+       CamelDataCache *rss_cache;
+       CamelMimeMessage *message = NULL;
+       CamelRssStore *rss_store;
+       GIOStream *base_stream;
+
+       g_return_val_if_fail (CAMEL_IS_RSS_FOLDER_SUMMARY (self), NULL);
+       g_return_val_if_fail (uid != NULL, NULL);
+
+       folder = camel_folder_summary_get_folder (CAMEL_FOLDER_SUMMARY (self));
+       rss_store = CAMEL_RSS_STORE (camel_folder_get_parent_store (folder));
+
+       if (out_content_type) {
+               *out_content_type = camel_rss_store_summary_get_content_type (
+                       camel_rss_store_get_summary (rss_store),
+                       camel_rss_folder_get_id (CAMEL_RSS_FOLDER (folder)));
+       }
+
+       rss_cache = camel_rss_store_get_cache (rss_store);
+       base_stream = camel_data_cache_get (rss_cache, camel_rss_folder_get_id (CAMEL_RSS_FOLDER (folder)), 
uid, error);
+
+       if (base_stream) {
+               CamelStream *stream;
+
+               stream = camel_stream_new (base_stream);
+               g_object_unref (base_stream);
+
+               message = camel_mime_message_new ();
+               if (!camel_data_wrapper_construct_from_stream_sync (CAMEL_DATA_WRAPPER (message), stream, 
cancellable, error)) {
+                       g_object_unref (message);
+                       message = NULL;
+               }
+
+               g_object_unref (stream);
+       }
+
+       if (out_rss_cache)
+               *out_rss_cache = g_object_ref (rss_cache);
+
+       return message;
+}
+
+gboolean
+camel_rss_folder_summary_add_or_update_feed_sync (CamelRssFolderSummary *self,
+                                                 const gchar *href,
+                                                 ERssFeed *feed,
+                                                 GBytes *complete_article,
+                                                 CamelFolderChangeInfo **inout_changes,
+                                                 GCancellable *cancellable,
+                                                 GError **error)
+{
+       CamelDataCache *rss_cache = NULL;
+       CamelDataWrapper *body_wrapper;
+       CamelMimeMessage *message;
+       CamelRssContentType content_type = CAMEL_RSS_CONTENT_TYPE_HTML;
+       gchar *uid, *received, *received_tm;
+       GSList *link;
+       gboolean has_downloaded_eclosure = FALSE;
+       gboolean existing_message;
+       gboolean success = TRUE;
+
+       g_return_val_if_fail (CAMEL_IS_RSS_FOLDER_SUMMARY (self), FALSE);
+       g_return_val_if_fail (href != NULL, FALSE);
+       g_return_val_if_fail (feed != NULL, FALSE);
+       g_return_val_if_fail (feed->link != NULL, FALSE);
+       g_return_val_if_fail (inout_changes != NULL, FALSE);
+
+       uid = g_compute_checksum_for_string (G_CHECKSUM_SHA1, feed->id ? feed->id : feed->link, -1);
+       g_return_val_if_fail (uid != NULL, FALSE);
+
+       message = camel_rss_folder_summary_dup_message (self, uid, &rss_cache, &content_type, cancellable, 
NULL);
+       existing_message = camel_folder_summary_get_info_flags (CAMEL_FOLDER_SUMMARY (self), uid) != (~0);
+       if (!existing_message)
+               g_clear_object (&message);
+       if (!message) {
+               gchar *msg_id;
+
+               msg_id = g_strconcat (uid, "@localhost", NULL);
+
+               message = camel_mime_message_new ();
+
+               camel_mime_message_set_message_id (message, msg_id);
+               camel_mime_message_set_date (message, feed->last_modified, 0);
+               camel_medium_set_header (CAMEL_MEDIUM (message), "From", feed->author);
+               camel_medium_set_header (CAMEL_MEDIUM (message), "X-RSS-Feed", href);
+
+               g_free (msg_id);
+       }
+
+       camel_mime_message_set_subject (message, feed->title);
+
+       received_tm = camel_header_format_date (time (NULL), 0);
+       received = g_strconcat ("from ", href, " by localhost; ", received_tm, NULL);
+
+       camel_medium_add_header (CAMEL_MEDIUM (message), "Received", received);
+
+       for (link = feed->enclosures; link && !has_downloaded_eclosure; link = g_slist_next (link)) {
+               ERssEnclosure *enclosure = link->data;
+
+               if (enclosure->data && g_bytes_get_size (enclosure->data) > 0)
+                       has_downloaded_eclosure = TRUE;
+       }
+
+       body_wrapper = camel_data_wrapper_new ();
+
+       if (complete_article && g_bytes_get_size (complete_article) > 0) {
+               camel_data_wrapper_set_encoding (body_wrapper, CAMEL_TRANSFER_ENCODING_QUOTEDPRINTABLE);
+               camel_data_wrapper_set_mime_type (body_wrapper, "text/html; charset=utf-8");
+               success = camel_data_wrapper_construct_from_data_sync (body_wrapper, g_bytes_get_data 
(complete_article, NULL), g_bytes_get_size (complete_article), cancellable, error);
+       } else {
+               GString *body;
+               const gchar *ct;
+               gboolean first_enclosure = TRUE;
+
+               body = g_string_new (NULL);
+
+               if (content_type == CAMEL_RSS_CONTENT_TYPE_PLAIN_TEXT) {
+                       ct = "text/plain; charset=utf-8";
+
+                       g_string_append (body, feed->link);
+                       g_string_append_c (body, '\n');
+                       g_string_append_c (body, '\n');
+               } else if (content_type == CAMEL_RSS_CONTENT_TYPE_MARKDOWN) {
+                       ct = "text/markdown; charset=utf-8";
+               } else {
+                       ct = "text/html; charset=utf-8";
+               }
+
+               if (content_type != CAMEL_RSS_CONTENT_TYPE_PLAIN_TEXT) {
+                       gchar *tmp;
+
+                       tmp = g_markup_printf_escaped ("<h4><a href=\"%s\">%s</a></h4><div><br></div>", 
feed->link, feed->title);
+                       g_string_append (body, tmp);
+                       g_free (tmp);
+               }
+
+               if (feed->body)
+                       g_string_append (body, feed->body);
+
+               for (link = feed->enclosures; link; link = g_slist_next (link)) {
+                       ERssEnclosure *enclosure = link->data;
+                       gchar *tmp;
+
+                       if (enclosure->data && g_bytes_get_size (enclosure->data) > 0)
+                               continue;
+
+                       if (first_enclosure) {
+                               first_enclosure = FALSE;
+
+                               g_string_append (body, "<br><hr><br>\n");
+                               g_string_append (body, _("Enclosures:"));
+                               g_string_append (body, "<br>\n");
+                       }
+
+                       tmp = g_markup_printf_escaped ("<div><a href=\"%s\">%s</a></div>\n",
+                               enclosure->href, enclosure->title ? enclosure->title : enclosure->href);
+
+                       g_string_append (body, tmp);
+
+                       g_free (tmp);
+               }
+
+               camel_data_wrapper_set_encoding (body_wrapper, CAMEL_TRANSFER_ENCODING_QUOTEDPRINTABLE);
+               camel_data_wrapper_set_mime_type (body_wrapper, ct);
+               success = camel_data_wrapper_construct_from_data_sync (body_wrapper, body->str, body->len, 
cancellable, error);
+
+               g_string_free (body, TRUE);
+       }
+
+       if (success && has_downloaded_eclosure) {
+               CamelMultipart *mixed;
+               CamelMimePart *subpart;
+
+               mixed = camel_multipart_new ();
+               camel_data_wrapper_set_mime_type (CAMEL_DATA_WRAPPER (mixed), "multipart/mixed");
+               camel_multipart_set_boundary (mixed, NULL);
+
+               subpart = camel_mime_part_new ();
+               camel_medium_set_content (CAMEL_MEDIUM (subpart), body_wrapper);
+               camel_multipart_add_part (mixed, subpart);
+               g_object_unref (subpart);
+
+               for (link = feed->enclosures; link; link = g_slist_next (link)) {
+                       ERssEnclosure *enclosure = link->data;
+                       GUri *link_uri;
+
+                       if (!enclosure->data || !g_bytes_get_size (enclosure->data))
+                               continue;
+
+                       subpart = camel_mime_part_new ();
+                       camel_mime_part_set_content (subpart, (const gchar *) g_bytes_get_data 
(enclosure->data, NULL), g_bytes_get_size (enclosure->data),
+                               enclosure->content_type ? enclosure->content_type : 
"application/octet-stream");
+
+                       camel_mime_part_set_disposition (subpart, "inline");
+
+                       link_uri = g_uri_parse (enclosure->href, G_URI_FLAGS_PARSE_RELAXED, NULL);
+                       if (link_uri) {
+                               const gchar *path = g_uri_get_path (link_uri);
+                               const gchar *slash = path ? strrchr (path, '/') : NULL;
+
+                               if (slash && *slash && slash[1])
+                                       camel_mime_part_set_filename (subpart, slash + 1);
+
+                               g_uri_unref (link_uri);
+                       }
+
+                       camel_mime_part_set_encoding (subpart, CAMEL_TRANSFER_ENCODING_BASE64);
+
+                       camel_multipart_add_part (mixed, subpart);
+
+                       g_object_unref (subpart);
+               }
+
+               g_object_unref (body_wrapper);
+               body_wrapper = CAMEL_DATA_WRAPPER (mixed);
+       }
+
+       if (CAMEL_IS_MIME_PART (body_wrapper)) {
+               CamelDataWrapper *content;
+               CamelMedium *imedium, *omedium;
+               const CamelNameValueArray *headers;
+
+               imedium = CAMEL_MEDIUM (body_wrapper);
+               omedium = CAMEL_MEDIUM (message);
+
+               content = camel_medium_get_content (imedium);
+               camel_medium_set_content (omedium, content);
+               camel_data_wrapper_set_encoding (CAMEL_DATA_WRAPPER (omedium), 
camel_data_wrapper_get_encoding (CAMEL_DATA_WRAPPER (imedium)));
+
+               headers = camel_medium_get_headers (imedium);
+               if (headers) {
+                       gint ii, length;
+                       length = camel_name_value_array_get_length (headers);
+
+                       for (ii = 0; ii < length; ii++) {
+                               const gchar *header_name = NULL;
+                               const gchar *header_value = NULL;
+
+                               if (camel_name_value_array_get (headers, ii, &header_name, &header_value))
+                                       camel_medium_set_header (omedium, header_name, header_value);
+                       }
+               }
+       } else {
+               camel_medium_set_content (CAMEL_MEDIUM (message), body_wrapper);
+       }
+
+       if (success) {
+               CamelRssFolder *rss_folder;
+               GIOStream *io_stream;
+
+               rss_folder = CAMEL_RSS_FOLDER (camel_folder_summary_get_folder (CAMEL_FOLDER_SUMMARY (self)));
+               io_stream = camel_data_cache_add (rss_cache, camel_rss_folder_get_id (rss_folder), uid, 
error);
+               success = io_stream != NULL;
+
+               if (io_stream) {
+                       success = camel_data_wrapper_write_to_output_stream_sync (CAMEL_DATA_WRAPPER 
(message),
+                               g_io_stream_get_output_stream (io_stream), cancellable, error);
+               }
+
+               g_clear_object (&io_stream);
+       }
+
+       if (success) {
+               if (!*inout_changes)
+                       *inout_changes = camel_folder_change_info_new ();
+
+               if (existing_message) {
+                       camel_folder_change_info_change_uid (*inout_changes, uid);
+               } else {
+                       CamelFolderSummary *folder_summary = CAMEL_FOLDER_SUMMARY (self);
+                       CamelMessageInfo *info;
+
+                       info = camel_folder_summary_info_new_from_message (folder_summary, message);
+                       g_warn_if_fail (info != NULL);
+
+                       camel_message_info_set_uid (info, uid);
+                       camel_folder_summary_add (folder_summary, info, TRUE);
+
+                       g_clear_object (&info);
+
+                       camel_folder_change_info_add_uid (*inout_changes, uid);
+                       camel_folder_change_info_recent_uid (*inout_changes, uid);
+               }
+       }
+
+       g_clear_object (&rss_cache);
+       g_clear_object (&body_wrapper);
+       g_clear_object (&message);
+       g_free (received_tm);
+       g_free (received);
+       g_free (uid);
+
+       return success;
+}
diff --git a/src/modules/rss/camel/camel-rss-folder-summary.h 
b/src/modules/rss/camel/camel-rss-folder-summary.h
new file mode 100644
index 0000000000..df9ceab91d
--- /dev/null
+++ b/src/modules/rss/camel/camel-rss-folder-summary.h
@@ -0,0 +1,69 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef CAMEL_RSS_FOLDER_SUMMARY_H
+#define CAMEL_RSS_FOLDER_SUMMARY_H
+
+#include <camel/camel.h>
+
+#include "camel-rss-store-summary.h"
+#include "e-rss-parser.h"
+
+/* Standard GObject macros */
+#define CAMEL_TYPE_RSS_FOLDER_SUMMARY \
+       (camel_rss_folder_summary_get_type ())
+#define CAMEL_RSS_FOLDER_SUMMARY(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), CAMEL_TYPE_RSS_FOLDER_SUMMARY, CamelRssFolderSummary))
+#define CAMEL_RSS_FOLDER_SUMMARY_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), CAMEL_TYPE_RSS_FOLDER_SUMMARY, CamelRssFolderSummaryClass))
+#define CAMEL_IS_RSS_FOLDER_SUMMARY(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), CAMEL_TYPE_RSS_FOLDER_SUMMARY))
+#define CAMEL_IS_RSS_FOLDER_SUMMARY_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), CAMEL_TYPE_RSS_FOLDER_SUMMARY))
+#define CAMEL_RSS_FOLDER_SUMMARY_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), CAMEL_TYPE_RSS_FOLDER_SUMMARY, CamelRssFolderSummaryClass))
+
+G_BEGIN_DECLS
+
+typedef struct _CamelRssFolderSummary CamelRssFolderSummary;
+typedef struct _CamelRssFolderSummaryClass CamelRssFolderSummaryClass;
+typedef struct _CamelRssFolderSummaryPrivate CamelRssFolderSummaryPrivate;
+
+struct _CamelRssFolderSummary {
+       CamelFolderSummary parent;
+       CamelRssFolderSummaryPrivate *priv;
+};
+
+struct _CamelRssFolderSummaryClass {
+       CamelFolderSummaryClass parent_class;
+};
+
+GType          camel_rss_folder_summary_get_type               (void);
+CamelFolderSummary *
+               camel_rss_folder_summary_new                    (CamelFolder *folder);
+CamelMimeMessage *
+               camel_rss_folder_summary_dup_message            (CamelRssFolderSummary *self,
+                                                                const gchar *uid,
+                                                                CamelDataCache **out_rss_cache,
+                                                                CamelRssContentType *out_content_type,
+                                                                GCancellable *cancellable,
+                                                                GError **error);
+gboolean       camel_rss_folder_summary_add_or_update_feed_sync(CamelRssFolderSummary *self,
+                                                                const gchar *href,
+                                                                ERssFeed *feed,
+                                                                GBytes *complete_article,
+                                                                CamelFolderChangeInfo **inout_changes,
+                                                                GCancellable *cancellable,
+                                                                GError **error);
+
+G_END_DECLS
+
+#endif /* CAMEL_RSS_FOLDER_SUMMARY_H */
diff --git a/src/modules/rss/camel/camel-rss-folder.c b/src/modules/rss/camel/camel-rss-folder.c
new file mode 100644
index 0000000000..13a783efd7
--- /dev/null
+++ b/src/modules/rss/camel/camel-rss-folder.c
@@ -0,0 +1,778 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-config.h"
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+#include <libsoup/soup.h>
+
+#include "camel-rss-folder-summary.h"
+#include "camel-rss-settings.h"
+#include "camel-rss-store.h"
+#include "camel-rss-store-summary.h"
+
+#include "camel-rss-folder.h"
+
+struct _CamelRssFolderPrivate {
+       gboolean apply_filters;
+       CamelThreeState complete_articles;
+       CamelThreeState feed_enclosures;
+       gchar *id;
+};
+
+/* The custom property ID is a CamelArg artifact.
+ * It still identifies the property in state files. */
+enum {
+       PROP_0,
+       PROP_APPLY_FILTERS = 0x2501,
+       PROP_COMPLETE_ARTICLES = 0x2502,
+       PROP_FEED_ENCLOSURES = 0x2503
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (CamelRssFolder, camel_rss_folder, CAMEL_TYPE_FOLDER)
+
+static GPtrArray *
+rss_folder_search_by_expression (CamelFolder *folder,
+                                const gchar *expression,
+                                GCancellable *cancellable,
+                                GError **error)
+{
+       CamelFolderSearch *search;
+       GPtrArray *matches;
+
+       search = camel_folder_search_new ();
+       camel_folder_search_set_folder (search, folder);
+
+       matches = camel_folder_search_search (search, expression, NULL, cancellable, error);
+
+       g_clear_object (&search);
+
+       return matches;
+}
+
+static guint32
+rss_folder_count_by_expression (CamelFolder *folder,
+                               const gchar *expression,
+                               GCancellable *cancellable,
+                               GError **error)
+{
+       CamelFolderSearch *search;
+       guint32 count;
+
+       search = camel_folder_search_new ();
+       camel_folder_search_set_folder (search, folder);
+
+       count = camel_folder_search_count (search, expression, cancellable, error);
+
+       g_clear_object (&search);
+
+       return count;
+}
+
+static GPtrArray *
+rss_folder_search_by_uids (CamelFolder *folder,
+                          const gchar *expression,
+                          GPtrArray *uids,
+                          GCancellable *cancellable,
+                          GError **error)
+{
+       CamelFolderSearch *search;
+       GPtrArray *matches;
+
+       if (uids->len == 0)
+               return g_ptr_array_new ();
+
+       search = camel_folder_search_new ();
+       camel_folder_search_set_folder (search, folder);
+
+       matches = camel_folder_search_search (search, expression, uids, cancellable, error);
+
+       g_clear_object (&search);
+
+       return matches;
+}
+
+static void
+rss_folder_search_free (CamelFolder *folder,
+                       GPtrArray *result)
+{
+       camel_folder_search_free_result (NULL, result);
+}
+
+static gchar *
+rss_get_filename (CamelFolder *folder,
+                 const gchar *uid,
+                 GError **error)
+{
+       CamelStore *parent_store;
+       CamelDataCache *rss_cache;
+       CamelRssFolder *rss_folder;
+       CamelRssStore *rss_store;
+
+       parent_store = camel_folder_get_parent_store (folder);
+       rss_folder = CAMEL_RSS_FOLDER (folder);
+       rss_store = CAMEL_RSS_STORE (parent_store);
+       rss_cache = camel_rss_store_get_cache (rss_store);
+
+       return camel_data_cache_get_filename (rss_cache, rss_folder->priv->id, uid);
+}
+
+static gboolean
+rss_folder_append_message_sync (CamelFolder *folder,
+                               CamelMimeMessage *message,
+                               CamelMessageInfo *info,
+                               gchar **appended_uid,
+                               GCancellable *cancellable,
+                               GError **error)
+{
+       g_set_error (error, CAMEL_FOLDER_ERROR, CAMEL_FOLDER_ERROR_INVALID,
+               _("Cannot add message into News and Blogs folder"));
+
+       return FALSE;
+}
+
+static gboolean
+rss_folder_expunge_sync (CamelFolder *folder,
+                        GCancellable *cancellable,
+                        GError **error)
+{
+       CamelDataCache *cache;
+       CamelFolderSummary *summary;
+       CamelFolderChangeInfo *changes;
+       CamelRssFolder *rss_folder;
+       CamelStore *store;
+       GPtrArray *known_uids;
+       GList *to_remove = NULL;
+       guint ii;
+
+       summary = camel_folder_get_folder_summary (folder);
+       store = camel_folder_get_parent_store (folder);
+
+       if (!store)
+               return TRUE;
+
+       camel_folder_summary_prepare_fetch_all (summary, NULL);
+       known_uids = camel_folder_summary_get_array (summary);
+
+       if (known_uids == NULL)
+               return TRUE;
+
+       rss_folder = CAMEL_RSS_FOLDER (folder);
+       cache = camel_rss_store_get_cache (CAMEL_RSS_STORE (store));
+       changes = camel_folder_change_info_new ();
+
+       for (ii = 0; ii < known_uids->len; ii++) {
+               guint32 flags;
+               const gchar *uid;
+
+               uid = g_ptr_array_index (known_uids, ii);
+               flags = camel_folder_summary_get_info_flags (summary, uid);
+
+               if ((flags & CAMEL_MESSAGE_DELETED) != 0) {
+                       /* ignore cache removal error */
+                       camel_data_cache_remove (cache, rss_folder->priv->id, uid, NULL);
+
+                       camel_folder_change_info_remove_uid (changes, uid);
+                       to_remove = g_list_prepend (to_remove, (gpointer) camel_pstring_strdup (uid));
+               }
+       }
+
+       if (to_remove) {
+               camel_folder_summary_remove_uids (summary, to_remove);
+               camel_folder_summary_save (summary, NULL);
+               camel_folder_changed (folder, changes);
+
+               g_list_free_full (to_remove, (GDestroyNotify) camel_pstring_free);
+       }
+
+       camel_folder_change_info_free (changes);
+       camel_folder_summary_free_array (known_uids);
+
+       return TRUE;
+}
+
+static CamelMimeMessage *
+rss_folder_get_message_cached (CamelFolder *folder,
+                              const gchar *uid,
+                              GCancellable *cancellable)
+{
+       CamelRssFolderSummary *rss_summary;
+
+       g_return_val_if_fail (CAMEL_IS_RSS_FOLDER (folder), NULL);
+       g_return_val_if_fail (uid != NULL, NULL);
+
+       rss_summary = CAMEL_RSS_FOLDER_SUMMARY (camel_folder_get_folder_summary (folder));
+
+       return camel_rss_folder_summary_dup_message (rss_summary, uid, NULL, NULL, cancellable, NULL);
+}
+
+static CamelMimeMessage *
+rss_folder_get_message_sync (CamelFolder *folder,
+                            const gchar *uid,
+                            GCancellable *cancellable,
+                            GError **error)
+{
+       CamelMimeMessage *message;
+
+       message = rss_folder_get_message_cached (folder, uid, cancellable);
+
+       if (!message) {
+               g_set_error_literal (error, CAMEL_SERVICE_ERROR, CAMEL_SERVICE_ERROR_UNAVAILABLE,
+                       _("Message is not available"));
+       }
+
+       return message;
+}
+
+static gboolean
+rss_folder_refresh_info_sync (CamelFolder *folder,
+                             GCancellable *cancellable,
+                             GError **error)
+{
+       CamelRssFolder *self;
+       CamelRssFolderSummary *rss_folder_summary;
+       CamelRssStore *rss_store;
+       CamelRssStoreSummary *rss_store_summary;
+       CamelFolderChangeInfo *changes = NULL;
+       CamelSession *session;
+       gchar *href;
+       gint64 last_updated;
+       gboolean success = TRUE;
+
+       self = CAMEL_RSS_FOLDER (folder);
+       rss_store = CAMEL_RSS_STORE (camel_folder_get_parent_store (folder));
+       session = camel_service_ref_session (CAMEL_SERVICE (rss_store));
+
+       if (!session || !camel_session_get_online (session)) {
+               g_clear_object (&session);
+               return TRUE;
+       }
+
+       g_clear_object (&session);
+
+       rss_store_summary = camel_rss_store_get_summary (rss_store);
+       rss_folder_summary = CAMEL_RSS_FOLDER_SUMMARY (camel_folder_get_folder_summary (folder));
+
+       camel_rss_store_summary_lock (rss_store_summary);
+
+       href = g_strdup (camel_rss_store_summary_get_href (rss_store_summary, self->priv->id));
+       last_updated = camel_rss_store_summary_get_last_updated (rss_store_summary, self->priv->id);
+
+       camel_rss_store_summary_unlock (rss_store_summary);
+
+       if (href && *href) {
+               SoupSession *soup_session;
+               SoupMessage *message;
+               GBytes *bytes;
+
+               message = soup_message_new (SOUP_METHOD_GET, href);
+               if (!message) {
+                       g_set_error (error, CAMEL_FOLDER_ERROR, CAMEL_FOLDER_ERROR_INVALID, _("Invalid Feed 
URL “%s”."), href);
+                       g_free (href);
+
+                       return FALSE;
+               }
+
+               soup_session = soup_session_new_with_options (
+                       "timeout", 30,
+                       "user-agent", "Evolution/" VERSION,
+                       NULL);
+
+               if (camel_debug ("rss")) {
+                       SoupLogger *logger;
+
+                       logger = soup_logger_new (SOUP_LOGGER_LOG_BODY);
+                       soup_session_add_feature (soup_session, SOUP_SESSION_FEATURE (logger));
+                       g_object_unref (logger);
+               }
+
+               bytes = soup_session_send_and_read (soup_session, message, cancellable, error);
+
+               if (bytes) {
+                       GSList *feeds = NULL;
+
+                       success = SOUP_STATUS_IS_SUCCESSFUL (soup_message_get_status (message));
+
+                       if (success && e_rss_parser_parse ((const gchar *) g_bytes_get_data (bytes, NULL), 
g_bytes_get_size (bytes), NULL, NULL, NULL, NULL, &feeds)) {
+                               CamelSettings *settings;
+                               CamelRssSettings *rss_settings;
+                               gboolean download_complete_article;
+                               gboolean feed_enclosures, limit_feed_enclosure_size;
+                               guint32 max_feed_enclosure_size;
+                               gint64 max_last_modified = last_updated;
+                               GSList *link;
+
+                               settings = camel_service_ref_settings (CAMEL_SERVICE (rss_store));
+                               rss_settings = CAMEL_RSS_SETTINGS (settings);
+                               limit_feed_enclosure_size = camel_rss_settings_get_limit_feed_enclosure_size 
(rss_settings);
+                               max_feed_enclosure_size = camel_rss_settings_get_max_feed_enclosure_size 
(rss_settings);
+
+                               switch (self->priv->complete_articles) {
+                               case CAMEL_THREE_STATE_ON:
+                                       download_complete_article = TRUE;
+                                       break;
+                               case CAMEL_THREE_STATE_OFF:
+                                       download_complete_article = FALSE;
+                                       break;
+                               default:
+                                       download_complete_article = camel_rss_settings_get_complete_articles 
(rss_settings);
+                                       break;
+                               }
+
+                               switch (self->priv->feed_enclosures) {
+                               case CAMEL_THREE_STATE_ON:
+                                       feed_enclosures = TRUE;
+                                       break;
+                               case CAMEL_THREE_STATE_OFF:
+                                       feed_enclosures = FALSE;
+                                       break;
+                               default:
+                                       feed_enclosures = camel_rss_settings_get_feed_enclosures 
(rss_settings);
+                                       break;
+                               }
+
+                               g_clear_object (&settings);
+
+                               for (link = feeds; link && success; link = g_slist_next (link)) {
+                                       ERssFeed *feed = link->data;
+
+                                       if (feed->last_modified > last_updated) {
+                                               GBytes *complete_article = NULL;
+
+                                               if (max_last_modified < feed->last_modified)
+                                                       max_last_modified = feed->last_modified;
+
+                                               if (download_complete_article) {
+                                                       g_clear_object (&message);
+                                                       g_clear_pointer (&bytes, g_bytes_unref);
+
+                                                       message = soup_message_new (SOUP_METHOD_GET, 
feed->link);
+                                                       if (message) {
+                                                               complete_article = soup_session_send_and_read 
(soup_session, message, cancellable, NULL);
+
+                                                               if (!SOUP_STATUS_IS_SUCCESSFUL 
(soup_message_get_status (message)))
+                                                                       g_clear_pointer (&complete_article, 
g_bytes_unref);
+                                                       }
+                                               }
+
+                                               if (success && feed_enclosures && feed->enclosures) {
+                                                       GSList *elink;
+
+                                                       for (elink = feed->enclosures; elink && success; 
elink = g_slist_next (elink)) {
+                                                               ERssEnclosure *enclosure = elink->data;
+
+                                                               if (limit_feed_enclosure_size && 
enclosure->size > max_feed_enclosure_size)
+                                                                       continue;
+
+                                                               g_clear_object (&message);
+                                                               g_clear_pointer (&bytes, g_bytes_unref);
+
+                                                               message = soup_message_new (SOUP_METHOD_GET, 
enclosure->href);
+                                                               if (message) {
+                                                                       enclosure->data = 
soup_session_send_and_read (soup_session, message, cancellable, NULL);
+
+                                                                       if (!SOUP_STATUS_IS_SUCCESSFUL 
(soup_message_get_status (message)))
+                                                                               g_clear_pointer 
(&enclosure->data, g_bytes_unref);
+                                                               }
+                                                       }
+                                               }
+
+                                               success = success && 
camel_rss_folder_summary_add_or_update_feed_sync (rss_folder_summary, href, feed, complete_article, &changes, 
cancellable, error);
+
+                                               g_clear_pointer (&complete_article, g_bytes_unref);
+                                       }
+                               }
+
+                               if (success && max_last_modified != last_updated) {
+                                       camel_rss_store_summary_lock (rss_store_summary);
+                                       camel_rss_store_summary_set_last_updated (rss_store_summary, 
self->priv->id, max_last_modified);
+                                       camel_rss_store_summary_unlock (rss_store_summary);
+
+                                       success = camel_rss_store_summary_save (rss_store_summary, error);
+                               }
+                       }
+
+                       g_slist_free_full (feeds, e_rss_feed_free);
+               } else {
+                       success = FALSE;
+               }
+
+               g_clear_pointer (&bytes, g_bytes_unref);
+               g_clear_object (&soup_session);
+               g_clear_object (&message);
+       } else {
+               g_set_error (error, CAMEL_FOLDER_ERROR, CAMEL_FOLDER_ERROR_INVALID, _("Invalid Feed URL."));
+               success = FALSE;
+       }
+
+       g_free (href);
+
+       if (changes) {
+               GError *local_error = NULL;
+
+               if (!camel_folder_summary_save (CAMEL_FOLDER_SUMMARY (rss_folder_summary), (error && !*error) 
? error : &local_error)) {
+                       if (local_error)
+                               g_warning ("Failed to save RSS folder summary: %s", local_error->message);
+               }
+
+               g_clear_error (&local_error);
+
+               camel_folder_changed (folder, changes);
+               camel_folder_change_info_free (changes);
+       }
+
+       return success;
+}
+
+static void
+rss_unset_flagged_flag (const gchar *uid,
+                       CamelFolderSummary *summary)
+{
+       CamelMessageInfo *info;
+
+       info = camel_folder_summary_get (summary, uid);
+       if (info) {
+               camel_message_info_set_folder_flagged (info, FALSE);
+               g_clear_object (&info);
+       }
+}
+
+static gboolean
+rss_folder_synchronize_sync (CamelFolder *folder,
+                            gboolean expunge,
+                            GCancellable *cancellable,
+                            GError **error)
+{
+       CamelFolderSummary *summary;
+       GPtrArray *changed;
+
+       if (expunge) {
+               if (!camel_folder_expunge_sync (folder, cancellable, error))
+                       return FALSE;
+       }
+
+       summary = camel_folder_get_folder_summary (folder);
+       changed = camel_folder_summary_get_changed (summary);
+
+       if (changed) {
+               g_ptr_array_foreach (changed, (GFunc) rss_unset_flagged_flag, summary);
+               g_ptr_array_foreach (changed, (GFunc) camel_pstring_free, NULL);
+               camel_folder_summary_touch (summary);
+               g_ptr_array_free (changed, TRUE);
+       }
+
+       return camel_folder_summary_save (summary, error);
+}
+
+static void
+rss_folder_changed (CamelFolder *folder,
+                   CamelFolderChangeInfo *info)
+{
+       g_return_if_fail (CAMEL_IS_RSS_FOLDER (folder));
+
+       if (info && info->uid_removed && info->uid_removed->len) {
+               CamelDataCache *rss_cache;
+
+               rss_cache = camel_rss_store_get_cache (CAMEL_RSS_STORE (camel_folder_get_parent_store 
(folder)));
+
+               if (rss_cache) {
+                       CamelRssFolder *self = CAMEL_RSS_FOLDER (folder);
+                       guint ii;
+
+                       for (ii = 0; ii < info->uid_removed->len; ii++) {
+                               const gchar *message_uid = info->uid_removed->pdata[ii], *real_uid;
+
+                               if (!message_uid)
+                                       continue;
+
+                               real_uid = strchr (message_uid, ',');
+                               if (real_uid)
+                                       camel_data_cache_remove (rss_cache, self->priv->id, real_uid + 1, 
NULL);
+                       }
+               }
+       }
+
+       /* Chain up to parent's method. */
+       CAMEL_FOLDER_CLASS (camel_rss_folder_parent_class)->changed (folder, info);
+}
+
+static gboolean
+rss_folder_get_apply_filters (CamelRssFolder *folder)
+{
+       g_return_val_if_fail (CAMEL_IS_RSS_FOLDER (folder), FALSE);
+
+       return folder->priv->apply_filters;
+}
+
+static void
+rss_folder_set_apply_filters (CamelRssFolder *folder,
+                             gboolean apply_filters)
+{
+       g_return_if_fail (CAMEL_IS_RSS_FOLDER (folder));
+
+       if ((folder->priv->apply_filters ? 1 : 0) == (apply_filters ? 1 : 0))
+               return;
+
+       folder->priv->apply_filters = apply_filters;
+
+       g_object_notify (G_OBJECT (folder), "apply-filters");
+}
+
+static CamelThreeState
+rss_folder_get_complete_articles (CamelRssFolder *folder)
+{
+       g_return_val_if_fail (CAMEL_IS_RSS_FOLDER (folder), CAMEL_THREE_STATE_INCONSISTENT);
+
+       return folder->priv->complete_articles;
+}
+
+static void
+rss_folder_set_complete_articles (CamelRssFolder *folder,
+                                 CamelThreeState value)
+{
+       g_return_if_fail (CAMEL_IS_RSS_FOLDER (folder));
+
+       if (folder->priv->complete_articles == value)
+               return;
+
+       folder->priv->complete_articles = value;
+
+       g_object_notify (G_OBJECT (folder), "complete-articles");
+}
+
+static CamelThreeState
+rss_folder_get_feed_enclosures (CamelRssFolder *folder)
+{
+       g_return_val_if_fail (CAMEL_IS_RSS_FOLDER (folder), CAMEL_THREE_STATE_INCONSISTENT);
+
+       return folder->priv->feed_enclosures;
+}
+
+static void
+rss_folder_set_feed_enclosures (CamelRssFolder *folder,
+                               CamelThreeState value)
+{
+       g_return_if_fail (CAMEL_IS_RSS_FOLDER (folder));
+
+       if (folder->priv->feed_enclosures == value)
+               return;
+
+       folder->priv->feed_enclosures = value;
+
+       g_object_notify (G_OBJECT (folder), "feed-enclosures");
+}
+
+static void
+rss_folder_set_property (GObject *object,
+                        guint property_id,
+                        const GValue *value,
+                        GParamSpec *pspec)
+{
+       switch (property_id) {
+               case PROP_APPLY_FILTERS:
+                       rss_folder_set_apply_filters (CAMEL_RSS_FOLDER (object), g_value_get_boolean (value));
+                       return;
+               case PROP_COMPLETE_ARTICLES:
+                       rss_folder_set_complete_articles (CAMEL_RSS_FOLDER (object), g_value_get_enum 
(value));
+                       return;
+               case PROP_FEED_ENCLOSURES:
+                       rss_folder_set_feed_enclosures (CAMEL_RSS_FOLDER (object), g_value_get_enum (value));
+                       return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+rss_folder_get_property (GObject *object,
+                        guint property_id,
+                        GValue *value,
+                        GParamSpec *pspec)
+{
+       switch (property_id) {
+               case PROP_APPLY_FILTERS:
+                       g_value_set_boolean (value, rss_folder_get_apply_filters (CAMEL_RSS_FOLDER (object)));
+                       return;
+               case PROP_COMPLETE_ARTICLES:
+                       g_value_set_enum (value, rss_folder_get_complete_articles (CAMEL_RSS_FOLDER 
(object)));
+                       return;
+               case PROP_FEED_ENCLOSURES:
+                       g_value_set_enum (value, rss_folder_get_feed_enclosures (CAMEL_RSS_FOLDER (object)));
+                       return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+rss_folder_dispose (GObject *object)
+{
+       camel_folder_summary_save (camel_folder_get_folder_summary (CAMEL_FOLDER (object)), NULL);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (camel_rss_folder_parent_class)->dispose (object);
+}
+
+static void
+rss_folder_finalize (GObject *object)
+{
+       CamelRssFolder *self = CAMEL_RSS_FOLDER (object);
+
+       g_free (self->priv->id);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (camel_rss_folder_parent_class)->finalize (object);
+}
+
+static void
+camel_rss_folder_class_init (CamelRssFolderClass *class)
+{
+       GObjectClass *object_class;
+       CamelFolderClass *folder_class;
+
+       object_class = G_OBJECT_CLASS (class);
+       object_class->set_property = rss_folder_set_property;
+       object_class->get_property = rss_folder_get_property;
+       object_class->dispose = rss_folder_dispose;
+       object_class->finalize = rss_folder_finalize;
+
+       folder_class = CAMEL_FOLDER_CLASS (class);
+       folder_class->search_by_expression = rss_folder_search_by_expression;
+       folder_class->count_by_expression = rss_folder_count_by_expression;
+       folder_class->search_by_uids = rss_folder_search_by_uids;
+       folder_class->search_free = rss_folder_search_free;
+       folder_class->get_filename = rss_get_filename;
+       folder_class->append_message_sync = rss_folder_append_message_sync;
+       folder_class->expunge_sync = rss_folder_expunge_sync;
+       folder_class->get_message_cached = rss_folder_get_message_cached;
+       folder_class->get_message_sync = rss_folder_get_message_sync;
+       folder_class->refresh_info_sync = rss_folder_refresh_info_sync;
+       folder_class->synchronize_sync = rss_folder_synchronize_sync;
+       folder_class->changed = rss_folder_changed;
+
+       g_object_class_install_property (
+               object_class,
+               PROP_APPLY_FILTERS,
+               g_param_spec_boolean (
+                       "apply-filters",
+                       "Apply Filters",
+                       _("Apply message _filters to this folder"),
+                       FALSE,
+                       G_PARAM_READWRITE |
+                       G_PARAM_EXPLICIT_NOTIFY |
+                       G_PARAM_STATIC_STRINGS |
+                       CAMEL_PARAM_PERSISTENT));
+
+       g_object_class_install_property (
+               object_class,
+               PROP_COMPLETE_ARTICLES,
+               g_param_spec_enum (
+                       "complete-articles",
+                       "Complete Articles",
+                       _("_Download complete articles"),
+                       CAMEL_TYPE_THREE_STATE,
+                       CAMEL_THREE_STATE_INCONSISTENT,
+                       G_PARAM_READWRITE |
+                       G_PARAM_EXPLICIT_NOTIFY |
+                       CAMEL_PARAM_PERSISTENT));
+
+       g_object_class_install_property (
+               object_class,
+               PROP_FEED_ENCLOSURES,
+               g_param_spec_enum (
+                       "feed-enclosures",
+                       "Feed Enclosures",
+                       _("Download feed _enclosures"),
+                       CAMEL_TYPE_THREE_STATE,
+                       CAMEL_THREE_STATE_INCONSISTENT,
+                       G_PARAM_READWRITE |
+                       G_PARAM_EXPLICIT_NOTIFY |
+                       CAMEL_PARAM_PERSISTENT));
+}
+
+static void
+camel_rss_folder_init (CamelRssFolder *self)
+{
+       self->priv = camel_rss_folder_get_instance_private (self);
+       self->priv->complete_articles = CAMEL_THREE_STATE_INCONSISTENT;
+       self->priv->feed_enclosures = CAMEL_THREE_STATE_INCONSISTENT;
+}
+
+CamelFolder *
+camel_rss_folder_new (CamelStore *parent,
+                     const gchar *id,
+                     GCancellable *cancellable,
+                     GError **error)
+{
+       CamelFolder *folder;
+       CamelRssFolder *self;
+       CamelFolderSummary *folder_summary;
+       CamelRssStoreSummary *store_summary;
+       gchar *storage_path, *root;
+       CamelService *service;
+       CamelSettings *settings;
+       const gchar *user_data_dir;
+       gboolean filter_all = FALSE;
+
+       g_return_val_if_fail (id != NULL, NULL);
+
+       store_summary = camel_rss_store_get_summary (CAMEL_RSS_STORE (parent));
+       g_return_val_if_fail (store_summary != NULL, NULL);
+
+       service = CAMEL_SERVICE (parent);
+       user_data_dir = camel_service_get_user_data_dir (service);
+
+       settings = camel_service_ref_settings (service);
+
+       g_object_get (
+               settings,
+               "filter-all", &filter_all,
+               NULL);
+
+       g_object_unref (settings);
+
+       camel_rss_store_summary_lock (store_summary);
+
+       folder = g_object_new (
+               CAMEL_TYPE_RSS_FOLDER,
+               "display-name", camel_rss_store_summary_get_display_name (store_summary, id),
+               "full-name", id,
+               "parent-store", parent, NULL);
+
+       camel_rss_store_summary_unlock (store_summary);
+
+       self = CAMEL_RSS_FOLDER (folder);
+       self->priv->id = g_strdup (id);
+
+       camel_folder_set_flags (folder, camel_folder_get_flags (folder) | 
CAMEL_FOLDER_HAS_SUMMARY_CAPABILITY);
+
+       storage_path = g_build_filename (user_data_dir, id, NULL);
+       root = g_strdup_printf ("%s.cmeta", storage_path);
+       camel_object_set_state_filename (CAMEL_OBJECT (self), root);
+       camel_object_state_read (CAMEL_OBJECT (self));
+       g_free (root);
+       g_free (storage_path);
+
+       folder_summary = camel_rss_folder_summary_new (folder);
+
+       camel_folder_take_folder_summary (folder, folder_summary);
+
+       if (filter_all || rss_folder_get_apply_filters (self))
+               camel_folder_set_flags (folder, camel_folder_get_flags (folder) | CAMEL_FOLDER_FILTER_RECENT);
+
+       camel_folder_summary_load (folder_summary, NULL);
+
+       return folder;
+}
+
+const gchar *
+camel_rss_folder_get_id (CamelRssFolder *self)
+{
+       g_return_val_if_fail (CAMEL_IS_RSS_FOLDER (self), NULL);
+
+       return self->priv->id;
+}
diff --git a/src/modules/rss/camel/camel-rss-folder.h b/src/modules/rss/camel/camel-rss-folder.h
new file mode 100644
index 0000000000..633548a5ec
--- /dev/null
+++ b/src/modules/rss/camel/camel-rss-folder.h
@@ -0,0 +1,55 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef CAMEL_RSS_FOLDER_H
+#define CAMEL_RSS_FOLDER_H
+
+#include <camel/camel.h>
+
+/* Standard GObject macros */
+#define CAMEL_TYPE_RSS_FOLDER \
+       (camel_rss_folder_get_type ())
+#define CAMEL_RSS_FOLDER(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), CAMEL_TYPE_RSS_FOLDER, CamelRssFolder))
+#define CAMEL_RSS_FOLDER_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), CAMEL_TYPE_RSS_FOLDER, CamelRssFolderClass))
+#define CAMEL_IS_RSS_FOLDER(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), CAMEL_TYPE_RSS_FOLDER))
+#define CAMEL_IS_RSS_FOLDER_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), CAMEL_TYPE_RSS_FOLDER))
+#define CAMEL_RSS_FOLDER_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), CAMEL_TYPE_RSS_FOLDER, CamelRssFolderClass))
+
+G_BEGIN_DECLS
+
+typedef struct _CamelRssFolder CamelRssFolder;
+typedef struct _CamelRssFolderClass CamelRssFolderClass;
+typedef struct _CamelRssFolderPrivate CamelRssFolderPrivate;
+
+struct _CamelRssFolder {
+       CamelFolder parent;
+       CamelRssFolderPrivate *priv;
+};
+
+struct _CamelRssFolderClass {
+       CamelFolderClass parent;
+};
+
+GType          camel_rss_folder_get_type       (void);
+CamelFolder *  camel_rss_folder_new            (CamelStore *parent,
+                                                const gchar *id,
+                                                GCancellable *cancellable,
+                                                GError **error);
+const gchar *  camel_rss_folder_get_id         (CamelRssFolder *self);
+
+G_END_DECLS
+
+#endif /* CAMEL_RSS_FOLDER_H */
diff --git a/src/modules/rss/camel/camel-rss-provider.c b/src/modules/rss/camel/camel-rss-provider.c
new file mode 100644
index 0000000000..e6b05a33f3
--- /dev/null
+++ b/src/modules/rss/camel/camel-rss-provider.c
@@ -0,0 +1,94 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-config.h"
+
+#include <string.h>
+#include <glib/gi18n-lib.h>
+#include <camel/camel.h>
+
+#include "camel-rss-store.h"
+
+static CamelProvider rss_provider = {
+       "rss",
+       N_("News and Blogs"),
+
+       N_("This is a provider for reading RSS news and blogs."),
+
+       "rss",
+
+       CAMEL_PROVIDER_IS_LOCAL | CAMEL_PROVIDER_IS_SOURCE | CAMEL_PROVIDER_IS_STORAGE,
+
+       CAMEL_URL_NEED_HOST,
+
+       NULL, /* conf_entries */
+
+       NULL, /* port_entries */
+
+       /* ... */
+};
+
+static void
+add_hash (guint *hash,
+          gchar *s)
+{
+       if (s)
+               *hash ^= g_str_hash(s);
+}
+
+static guint
+rss_url_hash (gconstpointer key)
+{
+       const CamelURL *u = (CamelURL *) key;
+       guint hash = 0;
+
+       add_hash (&hash, u->user);
+       add_hash (&hash, u->host);
+       hash ^= u->port;
+
+       return hash;
+}
+
+static gint
+check_equal (gchar *s1,
+            gchar *s2)
+{
+       if (s1 == NULL) {
+               if (s2 == NULL)
+                       return TRUE;
+               else
+                       return FALSE;
+       }
+
+       if (s2 == NULL)
+               return FALSE;
+
+       return strcmp (s1, s2) == 0;
+}
+
+static gint
+rss_url_equal (gconstpointer a,
+              gconstpointer b)
+{
+       const CamelURL *u1 = a, *u2 = b;
+
+       return check_equal (u1->protocol, u2->protocol)
+               && check_equal (u1->user, u2->user)
+               && check_equal (u1->host, u2->host)
+               && u1->port == u2->port;
+}
+
+void
+camel_provider_module_init (void)
+{
+       rss_provider.object_types[CAMEL_PROVIDER_STORE] = camel_rss_store_get_type ();
+       rss_provider.url_hash = rss_url_hash;
+       rss_provider.url_equal = rss_url_equal;
+       rss_provider.authtypes = NULL;
+       rss_provider.translation_domain = GETTEXT_PACKAGE;
+
+       camel_provider_register (&rss_provider);
+}
diff --git a/src/modules/rss/camel/camel-rss-settings.c b/src/modules/rss/camel/camel-rss-settings.c
new file mode 100644
index 0000000000..cbcfef1876
--- /dev/null
+++ b/src/modules/rss/camel/camel-rss-settings.c
@@ -0,0 +1,301 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-config.h"
+
+#include <glib.h>
+
+#include "camel-rss-settings.h"
+
+struct _CamelRssSettingsPrivate {
+       gboolean filter_all;
+       gboolean complete_articles;
+       gboolean feed_enclosures;
+       gboolean limit_feed_enclosure_size;
+       guint32 max_feed_enclosure_size;
+};
+
+enum {
+       PROP_0,
+       PROP_FILTER_ALL,
+       PROP_COMPLETE_ARTICLES,
+       PROP_FEED_ENCLOSURES,
+       PROP_LIMIT_FEED_ENCLOSURE_SIZE,
+       PROP_MAX_FEED_ENCLOSURE_SIZE
+};
+
+G_DEFINE_TYPE_WITH_CODE (CamelRssSettings, camel_rss_settings, CAMEL_TYPE_OFFLINE_SETTINGS,
+       G_ADD_PRIVATE (CamelRssSettings))
+
+static void
+rss_settings_set_property (GObject *object,
+                          guint property_id,
+                          const GValue *value,
+                          GParamSpec *pspec)
+{
+       switch (property_id) {
+               case PROP_FILTER_ALL:
+                       camel_rss_settings_set_filter_all (
+                               CAMEL_RSS_SETTINGS (object),
+                               g_value_get_boolean (value));
+                       return;
+               case PROP_COMPLETE_ARTICLES:
+                       camel_rss_settings_set_complete_articles (
+                               CAMEL_RSS_SETTINGS (object),
+                               g_value_get_boolean (value));
+                       return;
+               case PROP_FEED_ENCLOSURES:
+                       camel_rss_settings_set_feed_enclosures (
+                               CAMEL_RSS_SETTINGS (object),
+                               g_value_get_boolean (value));
+                       return;
+               case PROP_LIMIT_FEED_ENCLOSURE_SIZE:
+                       camel_rss_settings_set_limit_feed_enclosure_size (
+                               CAMEL_RSS_SETTINGS (object),
+                               g_value_get_boolean (value));
+                       return;
+               case PROP_MAX_FEED_ENCLOSURE_SIZE:
+                       camel_rss_settings_set_max_feed_enclosure_size (
+                               CAMEL_RSS_SETTINGS (object),
+                               g_value_get_uint (value));
+                       return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+rss_settings_get_property (GObject *object,
+                          guint property_id,
+                          GValue *value,
+                          GParamSpec *pspec)
+{
+       switch (property_id) {
+               case PROP_FILTER_ALL:
+                       g_value_set_boolean (
+                               value,
+                               camel_rss_settings_get_filter_all (
+                               CAMEL_RSS_SETTINGS (object)));
+                       return;
+               case PROP_COMPLETE_ARTICLES:
+                       g_value_set_boolean (
+                               value,
+                               camel_rss_settings_get_complete_articles (
+                               CAMEL_RSS_SETTINGS (object)));
+                       return;
+               case PROP_FEED_ENCLOSURES:
+                       g_value_set_boolean (
+                               value,
+                               camel_rss_settings_get_feed_enclosures (
+                               CAMEL_RSS_SETTINGS (object)));
+                       return;
+               case PROP_LIMIT_FEED_ENCLOSURE_SIZE:
+                       g_value_set_boolean (
+                               value,
+                               camel_rss_settings_get_limit_feed_enclosure_size (
+                               CAMEL_RSS_SETTINGS (object)));
+                       return;
+               case PROP_MAX_FEED_ENCLOSURE_SIZE:
+                       g_value_set_uint (
+                               value,
+                               camel_rss_settings_get_max_feed_enclosure_size (
+                               CAMEL_RSS_SETTINGS (object)));
+                       return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+camel_rss_settings_class_init (CamelRssSettingsClass *class)
+{
+       GObjectClass *object_class;
+
+       object_class = G_OBJECT_CLASS (class);
+       object_class->set_property = rss_settings_set_property;
+       object_class->get_property = rss_settings_get_property;
+
+       g_object_class_install_property (
+               object_class,
+               PROP_FILTER_ALL,
+               g_param_spec_boolean (
+                       "filter-all",
+                       "Filter All",
+                       "Whether to apply filters in all folders",
+                       FALSE,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT |
+                       G_PARAM_EXPLICIT_NOTIFY |
+                       G_PARAM_STATIC_STRINGS));
+
+       g_object_class_install_property (
+               object_class,
+               PROP_COMPLETE_ARTICLES,
+               g_param_spec_boolean (
+                       "complete-articles",
+                       "Complete Articles",
+                       "Whether to download complete articles",
+                       FALSE,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT |
+                       G_PARAM_EXPLICIT_NOTIFY |
+                       G_PARAM_STATIC_STRINGS));
+
+       g_object_class_install_property (
+               object_class,
+               PROP_FEED_ENCLOSURES,
+               g_param_spec_boolean (
+                       "feed-enclosures",
+                       "Feed Enclosures",
+                       "Whether to download feed enclosures",
+                       FALSE,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT |
+                       G_PARAM_EXPLICIT_NOTIFY |
+                       G_PARAM_STATIC_STRINGS));
+
+       g_object_class_install_property (
+               object_class,
+               PROP_LIMIT_FEED_ENCLOSURE_SIZE,
+               g_param_spec_boolean (
+                       "limit-feed-enclosure-size",
+                       "Limit Feed Enclosure Size",
+                       "Whether to limit feed enclosure size",
+                       FALSE,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT |
+                       G_PARAM_EXPLICIT_NOTIFY |
+                       G_PARAM_STATIC_STRINGS));
+
+       g_object_class_install_property (
+               object_class,
+               PROP_MAX_FEED_ENCLOSURE_SIZE,
+               g_param_spec_uint (
+                       "max-feed-enclosure-size",
+                       "Max Feed Enclosure Size",
+                       "Max size, in kB, of feed enclosure to download",
+                       0, G_MAXUINT32, 0,
+                       G_PARAM_READWRITE |
+                       G_PARAM_CONSTRUCT |
+                       G_PARAM_EXPLICIT_NOTIFY |
+                       G_PARAM_STATIC_STRINGS));
+}
+
+static void
+camel_rss_settings_init (CamelRssSettings *settings)
+{
+       settings->priv = camel_rss_settings_get_instance_private (settings);
+}
+
+gboolean
+camel_rss_settings_get_filter_all (CamelRssSettings *settings)
+{
+       g_return_val_if_fail (CAMEL_IS_RSS_SETTINGS (settings), FALSE);
+
+       return settings->priv->filter_all;
+}
+
+void
+camel_rss_settings_set_filter_all (CamelRssSettings *settings,
+                                    gboolean filter_all)
+{
+       g_return_if_fail (CAMEL_IS_RSS_SETTINGS (settings));
+
+       if ((!settings->priv->filter_all) == (!filter_all))
+               return;
+
+       settings->priv->filter_all = filter_all;
+
+       g_object_notify (G_OBJECT (settings), "filter-all");
+}
+
+gboolean
+camel_rss_settings_get_complete_articles (CamelRssSettings *settings)
+{
+       g_return_val_if_fail (CAMEL_IS_RSS_SETTINGS (settings), FALSE);
+
+       return settings->priv->complete_articles;
+}
+
+void
+camel_rss_settings_set_complete_articles (CamelRssSettings *settings,
+                                         gboolean value)
+{
+       g_return_if_fail (CAMEL_IS_RSS_SETTINGS (settings));
+
+       if ((!settings->priv->complete_articles) == (!value))
+               return;
+
+       settings->priv->complete_articles = value;
+
+       g_object_notify (G_OBJECT (settings), "complete-articles");
+}
+
+gboolean
+camel_rss_settings_get_feed_enclosures (CamelRssSettings *settings)
+{
+       g_return_val_if_fail (CAMEL_IS_RSS_SETTINGS (settings), FALSE);
+
+       return settings->priv->feed_enclosures;
+}
+
+void
+camel_rss_settings_set_feed_enclosures (CamelRssSettings *settings,
+                                       gboolean value)
+{
+       g_return_if_fail (CAMEL_IS_RSS_SETTINGS (settings));
+
+       if ((!settings->priv->feed_enclosures) == (!value))
+               return;
+
+       settings->priv->feed_enclosures = value;
+
+       g_object_notify (G_OBJECT (settings), "feed-enclosures");
+}
+
+gboolean
+camel_rss_settings_get_limit_feed_enclosure_size (CamelRssSettings *settings)
+{
+       g_return_val_if_fail (CAMEL_IS_RSS_SETTINGS (settings), FALSE);
+
+       return settings->priv->limit_feed_enclosure_size;
+}
+
+void
+camel_rss_settings_set_limit_feed_enclosure_size (CamelRssSettings *settings,
+                                                 gboolean value)
+{
+       g_return_if_fail (CAMEL_IS_RSS_SETTINGS (settings));
+
+       if ((!settings->priv->limit_feed_enclosure_size) == (!value))
+               return;
+
+       settings->priv->limit_feed_enclosure_size = value;
+
+       g_object_notify (G_OBJECT (settings), "limit-feed-enclosure-size");
+}
+
+guint32
+camel_rss_settings_get_max_feed_enclosure_size (CamelRssSettings *settings)
+{
+       g_return_val_if_fail (CAMEL_IS_RSS_SETTINGS (settings), 0);
+
+       return settings->priv->max_feed_enclosure_size;
+}
+
+void
+camel_rss_settings_set_max_feed_enclosure_size (CamelRssSettings *settings,
+                                               guint32 value)
+{
+       g_return_if_fail (CAMEL_IS_RSS_SETTINGS (settings));
+
+       if (settings->priv->max_feed_enclosure_size == value)
+               return;
+
+       settings->priv->max_feed_enclosure_size = value;
+
+       g_object_notify (G_OBJECT (settings), "max-feed-enclosure-size");
+}
diff --git a/src/modules/rss/camel/camel-rss-settings.h b/src/modules/rss/camel/camel-rss-settings.h
new file mode 100644
index 0000000000..6874c42e0e
--- /dev/null
+++ b/src/modules/rss/camel/camel-rss-settings.h
@@ -0,0 +1,76 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef CAMEL_RSS_SETTINGS_H
+#define CAMEL_RSS_SETTINGS_H
+
+#include <camel/camel.h>
+
+/* Standard GObject macros */
+#define CAMEL_TYPE_RSS_SETTINGS \
+       (camel_rss_settings_get_type ())
+#define CAMEL_RSS_SETTINGS(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), CAMEL_TYPE_RSS_SETTINGS, CamelRssSettings))
+#define CAMEL_RSS_SETTINGS_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), CAMEL_TYPE_RSS_SETTINGS, CamelRssSettingsClass))
+#define CAMEL_IS_RSS_SETTINGS(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), CAMEL_TYPE_RSS_SETTINGS))
+#define CAMEL_IS_RSS_SETTINGS_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), CAMEL_TYPE_RSS_SETTINGS))
+#define CAMEL_RSS_SETTINGS_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), CAMEL_TYPE_RSS_SETTINGS, CamelRssSettingsClass))
+
+G_BEGIN_DECLS
+
+typedef struct _CamelRssSettings CamelRssSettings;
+typedef struct _CamelRssSettingsClass CamelRssSettingsClass;
+typedef struct _CamelRssSettingsPrivate CamelRssSettingsPrivate;
+
+struct _CamelRssSettings {
+       CamelOfflineSettings parent;
+       CamelRssSettingsPrivate *priv;
+};
+
+struct _CamelRssSettingsClass {
+       CamelOfflineSettingsClass parent_class;
+};
+
+GType          camel_rss_settings_get_type
+                                       (void) G_GNUC_CONST;
+gboolean       camel_rss_settings_get_filter_all
+                                       (CamelRssSettings *settings);
+void           camel_rss_settings_set_filter_all
+                                       (CamelRssSettings *settings,
+                                        gboolean filter_all);
+gboolean       camel_rss_settings_get_complete_articles
+                                       (CamelRssSettings *settings);
+void           camel_rss_settings_set_complete_articles
+                                       (CamelRssSettings *settings,
+                                        gboolean value);
+gboolean       camel_rss_settings_get_feed_enclosures
+                                       (CamelRssSettings *settings);
+void           camel_rss_settings_set_feed_enclosures
+                                       (CamelRssSettings *settings,
+                                        gboolean value);
+gboolean       camel_rss_settings_get_limit_feed_enclosure_size
+                                       (CamelRssSettings *settings);
+void           camel_rss_settings_set_limit_feed_enclosure_size
+                                       (CamelRssSettings *settings,
+                                        gboolean value);
+guint32                camel_rss_settings_get_max_feed_enclosure_size
+                                       (CamelRssSettings *settings);
+void           camel_rss_settings_set_max_feed_enclosure_size
+                                       (CamelRssSettings *settings,
+                                        guint32 value);
+
+G_END_DECLS
+
+#endif /* CAMEL_RSS_SETTINGS_H */
diff --git a/src/modules/rss/camel/camel-rss-store.c b/src/modules/rss/camel/camel-rss-store.c
new file mode 100644
index 0000000000..c430bb4ce4
--- /dev/null
+++ b/src/modules/rss/camel/camel-rss-store.c
@@ -0,0 +1,397 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-config.h"
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/types.h>
+
+#include <glib/gstdio.h>
+#include <glib/gi18n-lib.h>
+#include <libedataserver/libedataserver.h>
+
+#include "camel-rss-folder.h"
+#include "camel-rss-settings.h"
+#include "camel-rss-store-summary.h"
+#include "camel-rss-store.h"
+
+struct _CamelRssStorePrivate {
+       CamelDataCache *cache;
+       CamelRssStoreSummary *summary;
+};
+
+enum {
+       PROP_0,
+       PROP_SUMMARY
+};
+
+static GInitableIface *parent_initable_interface;
+
+/* Forward Declarations */
+static void camel_rss_store_initable_init (GInitableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (CamelRssStore, camel_rss_store, CAMEL_TYPE_STORE,
+       G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, camel_rss_store_initable_init)
+       G_ADD_PRIVATE (CamelRssStore))
+
+static gchar *
+rss_store_get_name (CamelService *service,
+                   gboolean brief)
+{
+       return g_strdup (_("News and Blogs"));
+}
+
+static gboolean
+rss_store_can_refresh_folder (CamelStore *store,
+                             CamelFolderInfo *info,
+                             GError **error)
+{
+       /* Any RSS folder can be refreshed */
+       return TRUE;
+}
+
+static CamelFolder *
+rss_store_get_folder_sync (CamelStore *store,
+                          const gchar *folder_name,
+                          CamelStoreGetFolderFlags flags,
+                          GCancellable *cancellable,
+                          GError **error)
+{
+       CamelRssStore *self = CAMEL_RSS_STORE (store);
+       CamelFolder *folder = NULL;
+
+       camel_rss_store_summary_lock (self->priv->summary);
+
+       /* The 'folder_name' is the folder ID */
+       if (camel_rss_store_summary_contains (self->priv->summary, folder_name)) {
+               folder = camel_rss_folder_new (store, folder_name, cancellable, error);
+       } else {
+               g_set_error (error, CAMEL_STORE_ERROR, CAMEL_STORE_ERROR_NO_FOLDER,
+                       _("Folder '%s' not found"), folder_name);
+       }
+
+       camel_rss_store_summary_unlock (self->priv->summary);
+
+       return folder;
+}
+
+static CamelFolderInfo *
+rss_store_get_folder_info_sync (CamelStore *store,
+                               const gchar *top,
+                               CamelStoreGetFolderInfoFlags flags,
+                               GCancellable *cancellable,
+                               GError **error)
+{
+       CamelRssStore *self = CAMEL_RSS_STORE (store);
+       CamelFolderInfo *fi = NULL, *first = NULL, *last = NULL;
+
+       if (!top || !*top) {
+               GSList *ids, *link;
+
+               ids = camel_rss_store_summary_dup_feeds (self->priv->summary);
+
+               for (link = ids; link; link = g_slist_next (link)) {
+                       const gchar *id = link->data;
+
+                       fi = camel_rss_store_summary_dup_folder_info (self->priv->summary, id);
+                       if (fi) {
+                               if (last) {
+                                       last->next = fi;
+                                       last = fi;
+                               } else {
+                                       first = fi;
+                                       last = first;
+                               }
+                       }
+               }
+
+               g_slist_free_full (ids, g_free);
+       } else {
+               first = camel_rss_store_summary_dup_folder_info (self->priv->summary, top);
+               if (!first)
+                       first = camel_rss_store_summary_dup_folder_info_for_display_name 
(self->priv->summary, top);
+
+               if (!first) {
+                       g_set_error (error, CAMEL_STORE_ERROR, CAMEL_STORE_ERROR_NO_FOLDER,
+                               _("Folder '%s' not found"), top);
+               }
+       }
+
+       return first;
+}
+
+static CamelFolderInfo *
+rss_store_create_folder_sync (CamelStore *store,
+                             const gchar *parent_name,
+                             const gchar *folder_name,
+                             GCancellable *cancellable,
+                             GError **error)
+{
+       g_set_error (error, CAMEL_STORE_ERROR, CAMEL_STORE_ERROR_INVALID,
+               _("Cannot create a folder in a News and Blogs store."));
+
+       return NULL;
+}
+
+static gboolean
+rss_store_rename_folder_sync (CamelStore *store,
+                             const gchar *old_name,
+                             const gchar *new_name_in,
+                             GCancellable *cancellable,
+                             GError **error)
+{
+       CamelRssStore *self = CAMEL_RSS_STORE (store);
+       gboolean success = FALSE;
+
+       camel_rss_store_summary_lock (self->priv->summary);
+
+       if (camel_rss_store_summary_contains (self->priv->summary, old_name)) {
+               const gchar *display_name;
+
+               success = TRUE;
+
+               display_name = camel_rss_store_summary_get_display_name (self->priv->summary, old_name);
+               if (g_strcmp0 (display_name, new_name_in) != 0) {
+                       camel_rss_store_summary_set_display_name (self->priv->summary, old_name, new_name_in);
+
+                       success = camel_rss_store_summary_save (self->priv->summary, error);
+
+                       if (success) {
+                               CamelFolderInfo *fi;
+
+                               fi = camel_rss_store_summary_dup_folder_info (self->priv->summary, old_name);
+                               camel_store_folder_renamed (store, old_name, fi);
+                               camel_folder_info_free (fi);
+                       }
+               }
+       } else {
+               g_set_error (error, CAMEL_STORE_ERROR, CAMEL_STORE_ERROR_NO_FOLDER,
+                       _("Folder '%s' not found"), old_name);
+       }
+
+       camel_rss_store_summary_unlock (self->priv->summary);
+
+       return success;
+}
+
+static gboolean
+rss_store_delete_folder_sync (CamelStore *store,
+                             const gchar *folder_name,
+                             GCancellable *cancellable,
+                             GError **error)
+{
+       CamelRssStore *self = CAMEL_RSS_STORE (store);
+       CamelFolderInfo *fi;
+       gboolean success = FALSE;
+
+       camel_rss_store_summary_lock (self->priv->summary);
+
+       /* The 'folder_name' is the folder ID */
+       fi = camel_rss_store_summary_dup_folder_info (self->priv->summary, folder_name);
+
+       if (camel_rss_store_summary_remove (self->priv->summary, folder_name)) {
+               GFile *file;
+               gchar *cmeta_filename;
+               GError *local_error = NULL;
+
+               file = g_file_new_build_filename (camel_data_cache_get_path (self->priv->cache), folder_name, 
NULL);
+
+               /* Ignore errors */
+               if (!e_file_recursive_delete_sync (file, cancellable, &local_error)) {
+                       if (camel_debug ("rss") &&
+                           !g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) &&
+                           !g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+                               g_printerr ("%s: Failed to delete cache directory '%s': %s", G_STRFUNC, 
g_file_peek_path (file), local_error ? local_error->message : "Unknown error");
+
+                       g_clear_error (&local_error);
+               }
+
+               g_clear_object (&file);
+
+               cmeta_filename = g_strdup_printf ("%s%c%s.cmeta", camel_data_cache_get_path 
(self->priv->cache), G_DIR_SEPARATOR, folder_name);
+               if (g_unlink (cmeta_filename)) {
+                       gint errn = errno;
+
+                       if (errn != ENOENT && camel_debug ("rss"))
+                               g_printerr ("%s: Failed to delete '%s': %s", G_STRFUNC, cmeta_filename, 
g_strerror (errn));
+               }
+
+               g_free (cmeta_filename);
+
+               camel_store_folder_deleted (store, fi);
+               success = camel_rss_store_summary_save (self->priv->summary, error);
+       } else {
+               g_set_error (error, CAMEL_STORE_ERROR, CAMEL_STORE_ERROR_NO_FOLDER,
+                       _("Folder '%s' not found"), folder_name);
+       }
+
+       camel_rss_store_summary_unlock (self->priv->summary);
+
+       if (fi)
+               camel_folder_info_free (fi);
+
+       return success;
+}
+
+static void
+rss_store_get_property (GObject *object,
+                       guint property_id,
+                       GValue *value,
+                       GParamSpec *pspec)
+{
+       switch (property_id) {
+               case PROP_SUMMARY:
+                       g_value_set_object (value,
+                               camel_rss_store_get_summary (
+                               CAMEL_RSS_STORE (object)));
+                       return;
+       }
+
+       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+}
+
+static void
+rss_store_dispose (GObject *object)
+{
+       CamelRssStore *self = CAMEL_RSS_STORE (object);
+
+       if (self->priv->summary) {
+               GError *local_error = NULL;
+
+               if (!camel_rss_store_summary_save (self->priv->summary, &local_error)) {
+                       g_warning ("%s: Failed to save RSS store summary: %s", G_STRFUNC, local_error ? 
local_error->message : "Unknown error");
+                       g_clear_error (&local_error);
+               }
+       }
+
+       g_clear_object (&self->priv->cache);
+       g_clear_object (&self->priv->summary);
+
+       /* Chain up to parent's method. */
+       G_OBJECT_CLASS (camel_rss_store_parent_class)->dispose (object);
+}
+
+static void
+camel_rss_store_class_init (CamelRssStoreClass *klass)
+{
+       GObjectClass *object_class;
+       CamelServiceClass *service_class;
+       CamelStoreClass *store_class;
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->get_property = rss_store_get_property;
+       object_class->dispose = rss_store_dispose;
+
+       service_class = CAMEL_SERVICE_CLASS (klass);
+       service_class->settings_type = CAMEL_TYPE_RSS_SETTINGS;
+       service_class->get_name = rss_store_get_name;
+
+       store_class = CAMEL_STORE_CLASS (klass);
+       store_class->can_refresh_folder = rss_store_can_refresh_folder;
+       store_class->get_folder_sync = rss_store_get_folder_sync;
+       store_class->get_folder_info_sync = rss_store_get_folder_info_sync;
+       store_class->create_folder_sync = rss_store_create_folder_sync;
+       store_class->delete_folder_sync = rss_store_delete_folder_sync;
+       store_class->rename_folder_sync = rss_store_rename_folder_sync;
+
+       g_object_class_install_property (
+               object_class,
+               PROP_SUMMARY,
+               g_param_spec_object (
+                       "summary", NULL, NULL,
+                       CAMEL_TYPE_RSS_STORE_SUMMARY,
+                       G_PARAM_READABLE |
+                       G_PARAM_STATIC_STRINGS));
+}
+
+static gboolean
+rss_store_initable_init (GInitable *initable,
+                        GCancellable *cancellable,
+                        GError **error)
+{
+       CamelDataCache *rss_cache;
+       CamelRssStore *self;
+       CamelStore *store;
+       CamelService *service;
+       const gchar *user_data_dir;
+       gchar *filename;
+
+       self = CAMEL_RSS_STORE (initable);
+       store = CAMEL_STORE (initable);
+       service = CAMEL_SERVICE (initable);
+
+       camel_store_set_flags (store, camel_store_get_flags (store) | CAMEL_STORE_VTRASH | CAMEL_STORE_VJUNK 
| CAMEL_STORE_IS_BUILTIN);
+
+       /* Chain up to parent method. */
+       if (!parent_initable_interface->init (initable, cancellable, error))
+               return FALSE;
+
+       service = CAMEL_SERVICE (initable);
+       user_data_dir = camel_service_get_user_data_dir (service);
+
+       if (g_mkdir_with_parents (user_data_dir, S_IRWXU) == -1) {
+               g_set_error_literal (
+                       error, G_FILE_ERROR,
+                       g_file_error_from_errno (errno),
+                       g_strerror (errno));
+               return FALSE;
+       }
+
+       filename = g_build_filename (user_data_dir, "rss.ini", NULL);
+       self->priv->summary = camel_rss_store_summary_new (filename);
+       g_free (filename);
+
+       if (!camel_rss_store_summary_load (self->priv->summary, error))
+               return FALSE;
+
+       /* setup store-wide cache */
+       rss_cache = camel_data_cache_new (user_data_dir, error);
+       if (rss_cache == NULL)
+               return FALSE;
+
+       /* Do not expire the cache */
+       camel_data_cache_set_expire_enabled (rss_cache, FALSE);
+
+       self->priv->cache = rss_cache;  /* takes ownership */
+
+       return TRUE;
+}
+
+static void
+camel_rss_store_initable_init (GInitableIface *iface)
+{
+       parent_initable_interface = g_type_interface_peek_parent (iface);
+
+       iface->init = rss_store_initable_init;
+}
+
+static void
+camel_rss_store_init (CamelRssStore *self)
+{
+       self->priv = camel_rss_store_get_instance_private (self);
+
+       camel_store_set_flags (CAMEL_STORE (self), 0);
+}
+
+CamelDataCache *
+camel_rss_store_get_cache (CamelRssStore *self)
+{
+       g_return_val_if_fail (CAMEL_IS_RSS_STORE (self), NULL);
+
+       return self->priv->cache;
+}
+
+CamelRssStoreSummary *
+camel_rss_store_get_summary (CamelRssStore *self)
+{
+       g_return_val_if_fail (CAMEL_IS_RSS_STORE (self), NULL);
+
+       return self->priv->summary;
+}
diff --git a/src/modules/rss/camel/camel-rss-store.h b/src/modules/rss/camel/camel-rss-store.h
new file mode 100644
index 0000000000..1ad34b2299
--- /dev/null
+++ b/src/modules/rss/camel/camel-rss-store.h
@@ -0,0 +1,55 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef CAMEL_RSS_STORE_H
+#define CAMEL_RSS_STORE_H
+
+#include <camel/camel.h>
+
+#include "camel-rss-store-summary.h"
+
+/* Standard GObject macros */
+#define CAMEL_TYPE_RSS_STORE \
+       (camel_rss_store_get_type ())
+#define CAMEL_RSS_STORE(obj) \
+       (G_TYPE_CHECK_INSTANCE_CAST \
+       ((obj), CAMEL_TYPE_RSS_STORE, CamelRssStore))
+#define CAMEL_RSS_STORE_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_CAST \
+       ((cls), CAMEL_TYPE_RSS_STORE, CamelRssStoreClass))
+#define CAMEL_IS_RSS_STORE(obj) \
+       (G_TYPE_CHECK_INSTANCE_TYPE \
+       ((obj), CAMEL_TYPE_RSS_STORE))
+#define CAMEL_IS_RSS_STORE_CLASS(cls) \
+       (G_TYPE_CHECK_CLASS_TYPE \
+       ((cls), CAMEL_TYPE_RSS_STORE))
+#define CAMEL_RSS_STORE_GET_CLASS(obj) \
+       (G_TYPE_INSTANCE_GET_CLASS \
+       ((obj), CAMEL_TYPE_RSS_STORE, CamelRssStoreClass))
+
+G_BEGIN_DECLS
+
+typedef struct _CamelRssStore CamelRssStore;
+typedef struct _CamelRssStoreClass CamelRssStoreClass;
+typedef struct _CamelRssStorePrivate CamelRssStorePrivate;
+
+struct _CamelRssStore {
+       CamelStore parent;
+       CamelRssStorePrivate *priv;
+};
+
+struct _CamelRssStoreClass {
+       CamelStoreClass parent_class;
+};
+
+GType          camel_rss_store_get_type        (void);
+CamelDataCache *camel_rss_store_get_cache      (CamelRssStore *self);
+CamelRssStoreSummary *
+               camel_rss_store_get_summary     (CamelRssStore *self);
+
+G_END_DECLS
+
+#endif /* CAMEL_RSS_STORE_H */
diff --git a/src/modules/rss/camel/libcamelrss.urls b/src/modules/rss/camel/libcamelrss.urls
new file mode 100644
index 0000000000..b60d988a7d
--- /dev/null
+++ b/src/modules/rss/camel/libcamelrss.urls
@@ -0,0 +1 @@
+rss
diff --git a/src/modules/rss/e-rss-parser.c b/src/modules/rss/e-rss-parser.c
new file mode 100644
index 0000000000..12736de9af
--- /dev/null
+++ b/src/modules/rss/e-rss-parser.c
@@ -0,0 +1,656 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-config.h"
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+
+#include <camel/camel.h>
+#include <libedataserver/libedataserver.h>
+
+#include "e-rss-parser.h"
+
+ERssEnclosure *
+e_rss_enclosure_new (void)
+{
+       return g_slice_new0 (ERssEnclosure);
+}
+
+void
+e_rss_enclosure_free (gpointer ptr)
+{
+       ERssEnclosure *enclosure = ptr;
+
+       if (enclosure) {
+               g_clear_pointer (&enclosure->data, g_bytes_unref);
+               g_free (enclosure->title);
+               g_free (enclosure->href);
+               g_free (enclosure->content_type);
+               g_slice_free (ERssEnclosure, enclosure);
+       }
+}
+
+ERssFeed *
+e_rss_feed_new (void)
+{
+       return g_slice_new0 (ERssFeed);
+}
+
+void
+e_rss_feed_free (gpointer ptr)
+{
+       ERssFeed *feed = ptr;
+
+       if (feed) {
+               g_free (feed->id);
+               g_free (feed->link);
+               g_free (feed->author);
+               g_free (feed->title);
+               g_free (feed->body);
+               g_slist_free_full (feed->enclosures, e_rss_enclosure_free);
+               g_slice_free (ERssFeed, feed);
+       }
+}
+
+static void
+e_rss_read_feed_person (xmlNodePtr author,
+                       xmlChar **out_name,
+                       xmlChar **out_email)
+{
+       xmlNodePtr node;
+
+       for (node = author->children; node && (!*out_name || !*out_email); node = node->next) {
+               if (g_strcmp0 ((const gchar *) node->name, "name") == 0) {
+                       g_clear_pointer (out_name, xmlFree);
+                       *out_name = xmlNodeGetContent (node);
+               } else if (g_strcmp0 ((const gchar *) node->name, "email") == 0) {
+                       g_clear_pointer (out_email, xmlFree);
+                       *out_email = xmlNodeGetContent (node);
+               } else if (g_strcmp0 ((const gchar *) node->name, "uri") == 0 &&
+                          (!*out_email || !**out_email)) {
+                       g_clear_pointer (out_email, xmlFree);
+                       *out_email = xmlNodeGetContent (node);
+               }
+       }
+
+       if (!*out_name && !*out_email) {
+               *out_name = xmlNodeGetContent (author);
+               if (*out_name && !**out_name)
+                       g_clear_pointer (&out_name, xmlFree);
+       }
+}
+
+static gchar *
+e_rss_parser_encode_address (xmlChar *name,
+                            xmlChar *email)
+{
+       gchar *address;
+
+       if (!name && !email)
+               return NULL;
+
+       address = camel_internet_address_format_address ((const gchar *) name,
+                                                        email ? (const gchar *) email : "");
+
+       if (address && (!email || !*email) && g_str_has_suffix (address, " <>")) {
+               /* avoid empty email in the string */
+               address[strlen (address) - 3] = '\0';
+       }
+
+       return address;
+}
+
+static ERssEnclosure *
+e_rss_read_enclosure (xmlNodePtr node)
+{
+       #define dup_attr(des, x) { \
+               xmlChar *attr = xmlGetProp (node, (const xmlChar *) x); \
+               if (attr && *attr) \
+                       des = g_strdup ((const gchar *) attr); \
+               else \
+                       des = NULL; \
+               g_clear_pointer (&attr, xmlFree); \
+       }
+
+       ERssEnclosure *enclosure;
+       xmlChar *length;
+       gchar *href;
+
+       dup_attr (href, "href");
+       if (!href)
+               dup_attr (href, "url");
+
+       if (!href || !*href) {
+               g_free (href);
+               return NULL;
+       }
+
+       enclosure = e_rss_enclosure_new ();
+
+       enclosure->href = href;
+
+       dup_attr (enclosure->title, "title");
+       dup_attr (enclosure->content_type, "type");
+
+       #undef dup_attr
+
+       length = xmlGetProp (node, (const xmlChar *) "length");
+       if (length && *length)
+               enclosure->size = g_ascii_strtoull ((const gchar *) length, NULL, 10);
+       g_clear_pointer (&length, xmlFree);
+
+       return enclosure;
+}
+
+typedef struct _FeedDefaults {
+       GUri *base_uri; /* 'base' as a GUri */
+       xmlChar *base;
+       xmlChar *author_name;
+       xmlChar *author_email;
+       gint64 publish_date;
+       xmlChar *link;
+       xmlChar *alt_link;
+       xmlChar *title;
+       xmlChar *icon;
+} FeedDefaults;
+
+static void
+e_rss_ensure_uri_absolute (GUri *base_uri,
+                          gchar **inout_uri)
+{
+       GUri *abs_uri;
+       const gchar *uri;
+
+       if (!base_uri || !inout_uri)
+               return;
+
+       uri = *inout_uri;
+
+       if (!uri || *uri != '/')
+               return;
+
+       abs_uri = g_uri_parse_relative (base_uri, uri,
+               G_URI_FLAGS_PARSE_RELAXED |
+               G_URI_FLAGS_HAS_PASSWORD |
+               G_URI_FLAGS_ENCODED_PATH |
+               G_URI_FLAGS_ENCODED_QUERY |
+               G_URI_FLAGS_ENCODED_FRAGMENT |
+               G_URI_FLAGS_SCHEME_NORMALIZE, NULL);
+
+       if (abs_uri) {
+               g_free (*inout_uri);
+               *inout_uri = g_uri_to_string_partial (abs_uri, G_URI_HIDE_PASSWORD);
+
+               g_uri_unref (abs_uri);
+       }
+}
+
+static void
+e_rss_read_item (xmlNodePtr item,
+                const FeedDefaults *defaults,
+                GSList **out_feeds)
+{
+       ERssFeed *feed = e_rss_feed_new ();
+       xmlNodePtr node;
+
+       for (node = item->children; node; node = node->next) {
+               xmlChar *value = NULL;
+
+               if (g_strcmp0 ((const gchar *) node->name, "title") == 0) {
+                       value = xmlNodeGetContent (node);
+                       g_clear_pointer (&feed->title, g_free);
+                       feed->title = g_strdup ((const gchar *) value);
+               } else if (g_strcmp0 ((const gchar *) node->name, "link") == 0) {
+                       xmlChar *rel = xmlGetProp (node, (const xmlChar *) "rel");
+                       if (!rel ||
+                           g_strcmp0 ((const gchar *) rel, "self") == 0 ||
+                           g_strcmp0 ((const gchar *) rel, "alternate") == 0) {
+                               value = xmlGetProp (node, (const xmlChar *) "href");
+                               if (!value)
+                                       value = xmlNodeGetContent (node);
+                               g_clear_pointer (&feed->link, g_free);
+                               feed->link = g_strdup ((const gchar *) value);
+
+                               /* Use full URI-s, not relative */
+                               if (feed->link && *feed->link == '/' && defaults->base_uri)
+                                       e_rss_ensure_uri_absolute (defaults->base_uri, &feed->link);
+                       } else if (g_strcmp0 ((const gchar *) rel, "enclosure") == 0) {
+                               ERssEnclosure *enclosure = e_rss_read_enclosure (node);
+
+                               if (enclosure)
+                                       feed->enclosures = g_slist_prepend (feed->enclosures, enclosure);
+                       }
+                       g_clear_pointer (&rel, xmlFree);
+               } else if (g_strcmp0 ((const gchar *) node->name, "id") == 0 ||
+                          g_strcmp0 ((const gchar *) node->name, "guid") == 0) {
+                       value = xmlNodeGetContent (node);
+                       g_clear_pointer (&feed->id, g_free);
+                       feed->id = g_strdup ((const gchar *) value);
+               } else if (g_strcmp0 ((const gchar *) node->name, "content") == 0) {
+                       value = xmlNodeGetContent (node);
+                       g_clear_pointer (&feed->body, g_free);
+                       feed->body = g_strdup ((const gchar *) value);
+               } else if (g_strcmp0 ((const gchar *) node->name, "description") == 0 ||
+                          g_strcmp0 ((const gchar *) node->name, "summary") == 0) {
+                       if (!feed->body || !*feed->body) {
+                               value = xmlNodeGetContent (node);
+                               g_clear_pointer (&feed->body, g_free);
+                               feed->body = g_strdup ((const gchar *) value);
+                       }
+               } else if (g_strcmp0 ((const gchar *) node->name, "enclosure") == 0) {
+                       ERssEnclosure *enclosure = e_rss_read_enclosure (node);
+
+                       if (enclosure)
+                               feed->enclosures = g_slist_prepend (feed->enclosures, enclosure);
+               } else if (g_strcmp0 ((const gchar *) node->name, "author") == 0) {
+                       xmlChar *name = NULL, *email = NULL;
+
+                       e_rss_read_feed_person (node, &name, &email);
+
+                       if (name || email) {
+                               g_clear_pointer (&feed->author, g_free);
+                               feed->author = e_rss_parser_encode_address (name, email);
+
+                               g_clear_pointer (&name, xmlFree);
+                               g_clear_pointer (&email, xmlFree);
+                       }
+               } else if (g_strcmp0 ((const gchar *) node->name, "pubDate") == 0) {
+                       value = xmlNodeGetContent (node);
+
+                       if (value && *value)
+                               feed->last_modified = camel_header_decode_date ((const gchar *) value, NULL);
+               } else if (g_strcmp0 ((const gchar *) node->name, "updated") == 0) {
+                       value = xmlNodeGetContent (node);
+
+                       if (value && *value) {
+                               GDateTime *dt;
+
+                               dt = g_date_time_new_from_iso8601 ((const gchar *) value, NULL);
+
+                               if (dt)
+                                       feed->last_modified = g_date_time_to_unix (dt);
+
+                               g_clear_pointer (&dt, g_date_time_unref);
+                       }
+               }
+
+               g_clear_pointer (&value, xmlFree);
+       }
+
+       if (feed->link && feed->title) {
+               if (!feed->author) {
+                       if (defaults->author_name || defaults->author_email) {
+                               feed->author = e_rss_parser_encode_address (defaults->author_name, 
defaults->author_email);
+                       } else {
+                               feed->author = g_strdup (_("Unknown author"));
+                       }
+               }
+
+               if (!feed->last_modified)
+                       feed->last_modified = defaults->publish_date;
+
+               feed->enclosures = g_slist_reverse (feed->enclosures);
+
+               *out_feeds = g_slist_prepend (*out_feeds, feed);
+       } else {
+               e_rss_feed_free (feed);
+       }
+}
+
+static void
+e_rss_read_defaults_rdf (xmlNodePtr root,
+                        FeedDefaults *defaults)
+{
+       xmlNodePtr node;
+
+       defaults->base = xmlGetProp (root, (const xmlChar *) "base");
+
+       for (node = root->children; node; node = node->next) {
+               if (g_strcmp0 ((const gchar *) node->name, "channel") == 0) {
+                       xmlNodePtr subnode;
+                       gboolean has_author = FALSE, has_link = FALSE, has_title = FALSE, has_image = FALSE;
+
+                       for (subnode = node->children; subnode && (!has_author || !has_link || !has_title || 
!has_image); subnode = subnode->next) {
+                               if (!has_author && g_strcmp0 ((const gchar *) subnode->name, "creator") == 0) 
{
+                                       g_clear_pointer (&defaults->author_name, xmlFree);
+                                       defaults->author_name = xmlNodeGetContent (subnode);
+                                       has_author = TRUE;
+                               } else if (!has_author && g_strcmp0 ((const gchar *) subnode->name, 
"publisher") == 0) {
+                                       g_clear_pointer (&defaults->author_name, xmlFree);
+                                       defaults->author_name = xmlNodeGetContent (subnode);
+                                       /* do not set has_author here, creator is more suitable */
+                               }
+
+                               if (!has_link && g_strcmp0 ((const gchar *) subnode->name, "link") == 0) {
+                                       defaults->link = xmlNodeGetContent (subnode);
+                                       has_link = TRUE;
+                               }
+
+                               if (!has_title && g_strcmp0 ((const gchar *) subnode->name, "title") == 0) {
+                                       defaults->title = xmlNodeGetContent (subnode);
+                                       has_title = TRUE;
+                               }
+
+                               if (!has_image && g_strcmp0 ((const gchar *) subnode->name, "image") == 0) {
+                                       defaults->icon = xmlGetProp (subnode, (const xmlChar *) "resource");
+                                       has_image = TRUE;
+                               }
+                       }
+
+                       break;
+               }
+       }
+}
+
+static void
+e_rss_read_rdf (xmlNodePtr node,
+               const FeedDefaults *defaults,
+               GSList **out_feeds)
+{
+       if (g_strcmp0 ((const gchar *) node->name, "item") == 0) {
+               e_rss_read_item (node, defaults, out_feeds);
+       }
+}
+
+static void
+e_rss_read_defaults_rss (xmlNodePtr root,
+                        FeedDefaults *defaults)
+{
+       xmlNodePtr channel_node;
+
+       defaults->base = xmlGetProp (root, (const xmlChar *) "base");
+
+       for (channel_node = root->children; channel_node; channel_node = channel_node->next) {
+               if (g_strcmp0 ((const gchar *) channel_node->name, "channel") == 0) {
+                       xmlNodePtr node;
+                       gboolean has_pubdate = FALSE, has_link = FALSE, has_title = FALSE, has_image = FALSE;
+
+                       for (node = channel_node->children; node && (!has_pubdate || !has_link || !has_title 
|| !has_image); node = node->next) {
+                               if (!has_pubdate && g_strcmp0 ((const gchar *) node->name, "pubDate") == 0) {
+                                       xmlChar *value = xmlNodeGetContent (node);
+
+                                       if (value && *value)
+                                               defaults->publish_date = camel_header_decode_date ((const 
gchar *) value, NULL);
+
+                                       g_clear_pointer (&value, xmlFree);
+
+                                       has_pubdate = TRUE;
+                               }
+
+                               if (!has_link && g_strcmp0 ((const gchar *) node->name, "link") == 0) {
+                                       xmlChar *value = xmlNodeGetContent (node);
+
+                                       if (value && *value) {
+                                               defaults->link = value;
+                                               has_link = TRUE;
+                                       } else {
+                                               g_clear_pointer (&value, xmlFree);
+                                       }
+                               }
+
+                               if (!has_title && g_strcmp0 ((const gchar *) node->name, "title") == 0) {
+                                       xmlChar *value = xmlNodeGetContent (node);
+
+                                       if (value && *value)
+                                               defaults->title = value;
+                                       else
+                                               g_clear_pointer (&value, xmlFree);
+
+                                       has_title = TRUE;
+                               }
+
+                               if (!has_image && g_strcmp0 ((const gchar *) node->name, "image") == 0) {
+                                       xmlNodePtr image_node;
+
+                                       for (image_node = node->children; image_node; image_node = 
image_node->next) {
+                                               if (g_strcmp0 ((const gchar *) image_node->name, "url") == 0) 
{
+                                                       xmlChar *value = xmlNodeGetContent (image_node);
+
+                                                       if (value && *value)
+                                                               defaults->icon = value;
+                                                       else
+                                                               g_clear_pointer (&value, xmlFree);
+                                                       break;
+                                               }
+                                       }
+
+                                       has_image = TRUE;
+                               }
+                       }
+                       /* read only the first channel */
+                       break;
+               }
+       }
+}
+
+static void
+e_rss_read_rss (xmlNodePtr node,
+               const FeedDefaults *defaults,
+               GSList **out_feeds)
+{
+       if (g_strcmp0 ((const gchar *) node->name, "channel") == 0) {
+               xmlNodePtr subnode;
+
+               for (subnode = node->children; subnode; subnode = subnode->next) {
+                       if (g_strcmp0 ((const gchar *) subnode->name, "item") == 0) {
+                               e_rss_read_item (subnode, defaults, out_feeds);
+                       }
+               }
+       }
+}
+
+static void
+e_rss_read_defaults_feed (xmlNodePtr root,
+                         FeedDefaults *defaults)
+{
+       xmlNodePtr node;
+       gboolean has_author = FALSE, has_published = FALSE, has_link = FALSE, has_alt_link = FALSE, has_title 
= FALSE, has_icon = FALSE;
+
+       defaults->base = xmlGetProp (root, (const xmlChar *) "base");
+       if (!defaults->base) {
+               for (node = root->children; node && !defaults->base; node = node->next) {
+                       if (g_strcmp0 ((const gchar *) node->name, "link") == 0)
+                               defaults->base = xmlGetProp (root, (const xmlChar *) "rel");
+                       else if (g_strcmp0 ((const gchar *) node->name, "alternate") == 0)
+                               defaults->base = xmlGetProp (root, (const xmlChar *) "href");
+               }
+       }
+
+       for (node = root->children; node && (!has_author || !has_published || !has_link || !has_alt_link || 
!has_title || !has_icon); node = node->next) {
+               if (!has_author && g_strcmp0 ((const gchar *) node->name, "author") == 0) {
+                       e_rss_read_feed_person (node, &defaults->author_name, &defaults->author_email);
+                       has_author = TRUE;
+               }
+
+               if (!has_published && (
+                   g_strcmp0 ((const gchar *) node->name, "published") == 0 ||
+                   g_strcmp0 ((const gchar *) node->name, "updated") == 0)) {
+                       xmlChar *value = xmlNodeGetContent (node);
+
+                       if (value && *value) {
+                               GDateTime *dt;
+
+                               dt = g_date_time_new_from_iso8601 ((const gchar *) value, NULL);
+
+                               if (dt) {
+                                       defaults->publish_date = g_date_time_to_unix (dt);
+                                       has_published = TRUE;
+                               }
+
+                               g_clear_pointer (&dt, g_date_time_unref);
+                       }
+
+                       g_clear_pointer (&value, xmlFree);
+               }
+
+               if ((!has_link || !has_alt_link) && g_strcmp0 ((const gchar *) node->name, "link") == 0) {
+                       xmlChar *rel, *href;
+
+                       rel = xmlGetProp (node, (const xmlChar *) "rel");
+                       href = xmlGetProp (node, (const xmlChar *) "href");
+
+                       if (!has_link && href && *href && g_strcmp0 ((const gchar *) rel, "self") == 0) {
+                               defaults->link = href;
+                               href = NULL;
+                               has_link = TRUE;
+                       }
+
+                       if (!has_alt_link && href && *href && g_strcmp0 ((const gchar *) rel, "alternate") == 
0) {
+                               defaults->alt_link = href;
+                               href = NULL;
+                               has_alt_link = TRUE;
+                       }
+
+                       g_clear_pointer (&rel, xmlFree);
+                       g_clear_pointer (&href, xmlFree);
+               }
+
+               if (!has_title && g_strcmp0 ((const gchar *) node->name, "title") == 0) {
+                       xmlChar *value = xmlNodeGetContent (node);
+
+                       if (value && *value)
+                               defaults->title = value;
+                       else
+                               g_clear_pointer (&value, xmlFree);
+
+                       has_title = TRUE;
+               }
+
+               if (!has_icon && (
+                   g_strcmp0 ((const gchar *) node->name, "icon") == 0 ||
+                   g_strcmp0 ((const gchar *) node->name, "logo") == 0)) {
+                       xmlChar *value = xmlNodeGetContent (node);
+
+                       if (value && *value) {
+                               g_clear_pointer (&defaults->icon, xmlFree);
+                               defaults->icon = value;
+                       } else {
+                               g_clear_pointer (&value, xmlFree);
+                       }
+
+                       /* Prefer "icon", but if not available, then use "logo" */
+                       has_icon = g_strcmp0 ((const gchar *) node->name, "icon") == 0;
+               }
+       }
+}
+
+static void
+e_rss_read_feed (xmlNodePtr node,
+                const FeedDefaults *defaults,
+                GSList **out_feeds)
+{
+       if (g_strcmp0 ((const gchar *) node->name, "entry") == 0) {
+               e_rss_read_item (node, defaults, out_feeds);
+       }
+}
+
+gboolean
+e_rss_parser_parse (const gchar *xml,
+                   gsize xml_len,
+                   gchar **out_link,
+                   gchar **out_alt_link,
+                   gchar **out_title,
+                   gchar **out_icon,
+                   GSList **out_feeds) /* ERssFeed * */
+{
+       xmlDoc *doc;
+       xmlNodePtr root;
+
+       g_return_val_if_fail (xml != NULL, FALSE);
+
+       if (out_feeds)
+               *out_feeds = NULL;
+
+       doc = e_xml_parse_data (xml, xml_len);
+
+       if (!doc)
+               return FALSE;
+
+       root = xmlDocGetRootElement (doc);
+       if (root) {
+               FeedDefaults defaults = { 0, };
+               void (*read_func) (xmlNodePtr node,
+                                  const FeedDefaults *defaults,
+                                  GSList **out_feeds) = NULL;
+
+               if (g_strcmp0 ((const gchar *) root->name, "rdf") == 0) {
+                       /* RSS 1.0 - https://web.resource.org/rss/1.0/ */
+                       e_rss_read_defaults_rdf (root, &defaults);
+                       read_func = e_rss_read_rdf;
+               } else if (g_strcmp0 ((const gchar *) root->name, "rss") == 0) {
+                       /* RSS 2.0 - https://www.rssboard.org/rss-specification */
+                       e_rss_read_defaults_rss (root, &defaults);
+                       read_func = e_rss_read_rss;
+               } else if (g_strcmp0 ((const gchar *) root->name, "feed") == 0) {
+                       /* Atom - https://validator.w3.org/feed/docs/atom.html */
+                       e_rss_read_defaults_feed (root, &defaults);
+                       read_func = e_rss_read_feed;
+               }
+
+               if (defaults.base || defaults.link || defaults.alt_link) {
+                       const gchar *base;
+
+                       base = (const gchar *) defaults.base;
+                       if (!base || *base == '/')
+                               base = (const gchar *) defaults.link;
+                       if (!base || *base == '/')
+                               base = (const gchar *) defaults.alt_link;
+
+                       if (base && *base != '/') {
+                               defaults.base_uri = g_uri_parse (base,
+                                       G_URI_FLAGS_PARSE_RELAXED |
+                                       G_URI_FLAGS_HAS_PASSWORD |
+                                       G_URI_FLAGS_ENCODED_PATH |
+                                       G_URI_FLAGS_ENCODED_QUERY |
+                                       G_URI_FLAGS_ENCODED_FRAGMENT |
+                                       G_URI_FLAGS_SCHEME_NORMALIZE, NULL);
+                       }
+               }
+
+               if (read_func && out_feeds) {
+                       xmlNodePtr node;
+
+                       for (node = root->children; node; node = node->next) {
+                               read_func (node, &defaults, out_feeds);
+                       }
+               }
+
+               if (out_link) {
+                       *out_link = g_strdup ((const gchar *) defaults.link);
+                       e_rss_ensure_uri_absolute (defaults.base_uri, out_link);
+               }
+
+               if (out_alt_link) {
+                       *out_alt_link = g_strdup ((const gchar *) defaults.alt_link);
+                       e_rss_ensure_uri_absolute (defaults.base_uri, out_alt_link);
+               }
+
+               if (out_title)
+                       *out_title = g_strdup ((const gchar *) defaults.title);
+
+               if (out_icon) {
+                       *out_icon = g_strdup ((const gchar *) defaults.icon);
+                       e_rss_ensure_uri_absolute (defaults.base_uri, out_icon);
+               }
+
+               g_clear_pointer (&defaults.base_uri, g_uri_unref);
+               g_clear_pointer (&defaults.base, xmlFree);
+               g_clear_pointer (&defaults.author_name, xmlFree);
+               g_clear_pointer (&defaults.author_email, xmlFree);
+               g_clear_pointer (&defaults.link, xmlFree);
+               g_clear_pointer (&defaults.alt_link, xmlFree);
+               g_clear_pointer (&defaults.title, xmlFree);
+               g_clear_pointer (&defaults.icon, xmlFree);
+
+               if (out_feeds)
+                       *out_feeds = g_slist_reverse (*out_feeds);
+       }
+
+       xmlFreeDoc (doc);
+
+       return TRUE;
+}
diff --git a/src/modules/rss/e-rss-parser.h b/src/modules/rss/e-rss-parser.h
new file mode 100644
index 0000000000..cf5f8fefa2
--- /dev/null
+++ b/src/modules/rss/e-rss-parser.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef E_RSS_PARSER_H
+#define E_RSS_PARSER_H
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+typedef struct _ERssEnclosure {
+       gchar *title;
+       gchar *href;
+       gchar *content_type;
+       guint64 size;
+       GBytes *data;
+} ERssEnclosure;
+
+typedef struct _ERssFeed {
+       gchar *id;
+       gchar *link;
+       gchar *author;
+       gchar *title;
+       gchar *body;
+       gint64 last_modified;
+       GSList *enclosures; /* ERssEnclosure * */
+} ERssFeed;
+
+ERssEnclosure *        e_rss_enclosure_new     (void);
+void           e_rss_enclosure_free    (gpointer ptr);
+
+ERssFeed *     e_rss_feed_new          (void);
+void           e_rss_feed_free         (gpointer ptr);
+
+gboolean       e_rss_parser_parse      (const gchar *xml,
+                                        gsize xml_len,
+                                        gchar **out_link,
+                                        gchar **out_alt_link,
+                                        gchar **out_title,
+                                        gchar **out_icon,
+                                        GSList **out_feeds); /* ERssFeed * */
+
+G_END_DECLS
+
+#endif /* E_RSS_PARSER_H */
diff --git a/src/modules/rss/evolution/CMakeLists.txt b/src/modules/rss/evolution/CMakeLists.txt
new file mode 100644
index 0000000000..b9cea767e2
--- /dev/null
+++ b/src/modules/rss/evolution/CMakeLists.txt
@@ -0,0 +1,32 @@
+set(extra_deps
+       evolution-mail
+       evolution-shell
+)
+set(sources
+       e-rss-preferences.c
+       e-rss-preferences.h
+       e-rss-folder-tree-model-extension.c
+       e-rss-shell-extension.c
+       e-rss-shell-view-extension.c
+       module-rss.c
+       module-rss.h
+       ../camel-rss-store-summary.c
+       ../camel-rss-store-summary.h
+       ../e-rss-parser.c
+       ../e-rss-parser.h
+)
+set(extra_defines)
+set(extra_cflags)
+set(extra_incdirs
+       ${CMAKE_CURRENT_SOURCE_DIR}/..
+)
+set(extra_ldflags)
+
+add_evolution_module(module-rss
+       sources
+       extra_deps
+       extra_defines
+       extra_cflags
+       extra_incdirs
+       extra_ldflags
+)
diff --git a/src/modules/rss/evolution/e-rss-folder-tree-model-extension.c 
b/src/modules/rss/evolution/e-rss-folder-tree-model-extension.c
new file mode 100644
index 0000000000..1b6ad5437f
--- /dev/null
+++ b/src/modules/rss/evolution/e-rss-folder-tree-model-extension.c
@@ -0,0 +1,217 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-config.h"
+
+#include <glib-object.h>
+
+#include "mail/em-folder-tree-model.h"
+#include "../camel-rss-store-summary.h"
+
+#include "module-rss.h"
+
+#define E_TYPE_RSS_FOLDER_TREE_MODEL_EXTENSION (e_rss_folder_tree_model_extension_get_type ())
+
+GType e_rss_folder_tree_model_extension_get_type (void);
+
+typedef struct _ERssFolderTreeModelExtension {
+       EExtension parent;
+       gboolean listens_feed_changed;
+} ERssFolderTreeModelExtension;
+
+typedef struct _ERssFolderTreeModelExtensionClass {
+       EExtensionClass parent_class;
+} ERssFolderTreeModelExtensionClass;
+
+G_DEFINE_DYNAMIC_TYPE (ERssFolderTreeModelExtension, e_rss_folder_tree_model_extension, E_TYPE_EXTENSION)
+
+static void
+e_rss_update_custom_icon (CamelRssStoreSummary *store_summary,
+                         const gchar *full_name,
+                         EMFolderTreeModel *model,
+                         GtkTreeIter *iter)
+{
+       const gchar *icon_filename;
+
+       icon_filename = camel_rss_store_summary_get_icon_filename (store_summary, full_name);
+
+       if (icon_filename && g_file_test (icon_filename, G_FILE_TEST_IS_REGULAR | G_FILE_TEST_EXISTS))
+               icon_filename = full_name;
+       else
+               icon_filename = "rss";
+
+       gtk_tree_store_set (GTK_TREE_STORE (model), iter,
+               COL_STRING_ICON_NAME, icon_filename,
+               -1);
+}
+
+static void
+e_rss_folder_custom_icon_feed_changed_cb (CamelRssStoreSummary *store_summary,
+                                         const gchar *feed_id,
+                                         EMFolderTreeModel *model)
+{
+       EMailSession *session;
+       CamelService *service;
+
+       if (!feed_id || !camel_rss_store_summary_contains (store_summary, feed_id))
+               return;
+
+       session = em_folder_tree_model_get_session (model);
+
+       if (!session)
+               return;
+
+       service = camel_session_ref_service (CAMEL_SESSION (session), "rss");
+
+       if (service) {
+               GtkTreeRowReference *row;
+
+               row = em_folder_tree_model_get_row_reference (model, CAMEL_STORE (service), feed_id);
+               if (row) {
+                       GtkTreePath *path;
+                       GtkTreeIter iter;
+
+                       path = gtk_tree_row_reference_get_path (row);
+                       gtk_tree_model_get_iter (GTK_TREE_MODEL (model), &iter, path);
+                       gtk_tree_path_free (path);
+
+                       e_rss_update_custom_icon (store_summary, feed_id, model, &iter);
+               }
+       }
+
+       g_clear_object (&service);
+}
+
+static void
+e_rss_folder_custom_icon_cb (EMFolderTreeModel *model,
+                            GtkTreeIter *iter,
+                            CamelStore *store,
+                            const gchar *full_name,
+                            ERssFolderTreeModelExtension *extension)
+{
+       CamelRssStoreSummary *store_summary = NULL;
+       const gchar *uid = camel_service_get_uid (CAMEL_SERVICE (store));
+
+       g_return_if_fail (extension != NULL);
+
+       if (g_strcmp0 (uid, "rss") != 0 || !full_name)
+               return;
+
+       if (g_strcmp0 (full_name, CAMEL_VJUNK_NAME) == 0 ||
+           g_strcmp0 (full_name, CAMEL_VTRASH_NAME) == 0)
+               return;
+
+       g_object_get (store, "summary", &store_summary, NULL);
+
+       if (!store_summary)
+               return;
+
+       if (!extension->listens_feed_changed) {
+               extension->listens_feed_changed = TRUE;
+
+               g_signal_connect_object (store_summary, "feed-changed",
+                       G_CALLBACK (e_rss_folder_custom_icon_feed_changed_cb), model, 0);
+       }
+
+       e_rss_update_custom_icon (store_summary, full_name, model, iter);
+
+       g_clear_object (&store_summary);
+}
+
+static gint
+e_rss_compare_folders_cb (EMFolderTreeModel *model,
+                         const gchar *store_uid,
+                         GtkTreeIter *iter1,
+                         GtkTreeIter *iter2)
+{
+       gint rv = -2;
+
+       /* Junk/Trash as the last, to not mix them with feed folders */
+       if (g_strcmp0 (store_uid, "rss") == 0) {
+               gboolean a_is_vfolder, b_is_vfolder;
+               guint32 flags_a, flags_b;
+
+               gtk_tree_model_get (
+                       GTK_TREE_MODEL (model), iter1,
+                       COL_UINT_FLAGS, &flags_a,
+                       -1);
+
+               gtk_tree_model_get (
+                       GTK_TREE_MODEL (model), iter2,
+                       COL_UINT_FLAGS, &flags_b,
+                       -1);
+
+               a_is_vfolder = (flags_a & CAMEL_FOLDER_TYPE_MASK) == CAMEL_FOLDER_TYPE_JUNK ||
+                              (flags_a & CAMEL_FOLDER_TYPE_MASK) == CAMEL_FOLDER_TYPE_TRASH;
+               b_is_vfolder = (flags_b & CAMEL_FOLDER_TYPE_MASK) == CAMEL_FOLDER_TYPE_JUNK ||
+                              (flags_b & CAMEL_FOLDER_TYPE_MASK) == CAMEL_FOLDER_TYPE_TRASH;
+
+               if ((a_is_vfolder || b_is_vfolder) && (!a_is_vfolder || !b_is_vfolder)) {
+                       if (a_is_vfolder)
+                               rv = 1;
+                       else
+                               rv = -1;
+               }
+       }
+
+       return rv;
+}
+
+static void
+e_rss_folder_tree_model_extension_constructed (GObject *object)
+{
+       static gboolean icon_dir_registered = FALSE;
+
+       /* Chain up to parent's method */
+       G_OBJECT_CLASS (e_rss_folder_tree_model_extension_parent_class)->constructed (object);
+
+       g_signal_connect_object (e_extension_get_extensible (E_EXTENSION (object)), "folder-custom-icon",
+               G_CALLBACK (e_rss_folder_custom_icon_cb), object, 0);
+
+       g_signal_connect_object (e_extension_get_extensible (E_EXTENSION (object)), "compare-folders",
+               G_CALLBACK (e_rss_compare_folders_cb), NULL, 0);
+
+       if (!icon_dir_registered) {
+               gchar *icon_dir;
+
+               icon_dir_registered = TRUE;
+
+               icon_dir = g_build_filename (e_get_user_data_dir (), "mail", "rss", NULL);
+
+               gtk_icon_theme_append_search_path (gtk_icon_theme_get_default (), icon_dir);
+
+               g_free (icon_dir);
+       }
+}
+
+static void
+e_rss_folder_tree_model_extension_class_init (ERssFolderTreeModelExtensionClass *klass)
+{
+       GObjectClass *object_class;
+       EExtensionClass *extension_class;
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->constructed = e_rss_folder_tree_model_extension_constructed;
+
+       extension_class = E_EXTENSION_CLASS (klass);
+       extension_class->extensible_type = EM_TYPE_FOLDER_TREE_MODEL;
+}
+
+static void
+e_rss_folder_tree_model_extension_class_finalize (ERssFolderTreeModelExtensionClass *klass)
+{
+}
+
+static void
+e_rss_folder_tree_model_extension_init (ERssFolderTreeModelExtension *extension)
+{
+}
+
+void
+e_rss_folder_tree_model_extension_type_register (GTypeModule *type_module)
+{
+       e_rss_folder_tree_model_extension_register_type (type_module);
+}
diff --git a/src/modules/rss/evolution/e-rss-preferences.c b/src/modules/rss/evolution/e-rss-preferences.c
new file mode 100644
index 0000000000..8543b0756c
--- /dev/null
+++ b/src/modules/rss/evolution/e-rss-preferences.c
@@ -0,0 +1,1437 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-config.h"
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+
+#include "e-util/e-util.h"
+#include "shell/e-shell.h"
+#include "camel-rss-store-summary.h"
+#include "e-rss-parser.h"
+
+#include "e-rss-preferences.h"
+
+enum {
+       COLUMN_STRING_ID = 0,
+       COLUMN_STRING_NAME,
+       COLUMN_STRING_HREF,
+       COLUMN_STRING_CONTENT_TYPE,
+       COLUMN_STRING_DESCRIPTION,
+       COLUMN_PIXBUF_ICON,
+       N_COLUMNS
+};
+
+static const gchar *
+e_rss_preferences_content_type_to_string (CamelRssContentType content_type)
+{
+       switch (content_type) {
+       case CAMEL_RSS_CONTENT_TYPE_HTML:
+               break;
+       case CAMEL_RSS_CONTENT_TYPE_PLAIN_TEXT:
+               return "text";
+       case CAMEL_RSS_CONTENT_TYPE_MARKDOWN:
+               return "markdown";
+       }
+
+       return "html";
+}
+
+static const gchar *
+e_rss_preferences_content_type_to_locale_string (CamelRssContentType content_type)
+{
+       switch (content_type) {
+       case CAMEL_RSS_CONTENT_TYPE_HTML:
+               break;
+       case CAMEL_RSS_CONTENT_TYPE_PLAIN_TEXT:
+               return _("Plain Text");
+       case CAMEL_RSS_CONTENT_TYPE_MARKDOWN:
+               return _("Markdown");
+       }
+
+       return _("HTML");
+}
+
+static CamelRssContentType
+e_rss_preferences_content_type_from_string (const gchar *str)
+{
+       if (g_strcmp0 (str, "text") == 0)
+               return CAMEL_RSS_CONTENT_TYPE_PLAIN_TEXT;
+
+       if (g_strcmp0 (str, "markdown") == 0)
+               return CAMEL_RSS_CONTENT_TYPE_MARKDOWN;
+
+       return CAMEL_RSS_CONTENT_TYPE_HTML;
+}
+
+static CamelService *
+e_rss_preferences_ref_store (EShell *shell)
+{
+       EShellBackend *shell_backend;
+       CamelSession *session = NULL;
+       CamelService *service;
+
+       g_return_val_if_fail (E_IS_SHELL (shell), NULL);
+
+       shell_backend = e_shell_get_backend_by_name (shell, "mail");
+       if (!shell_backend)
+               return NULL;
+
+       g_object_get (G_OBJECT (shell_backend), "session", &session, NULL);
+       if (!session)
+               return NULL;
+
+       service = camel_session_ref_service (session, "rss");
+
+       g_clear_object (&session);
+
+       return service;
+}
+
+static gchar *
+e_rss_preferences_describe_feed (const gchar *href,
+                                const gchar *name)
+{
+       return g_markup_printf_escaped ("%s\n<small>%s</small>", name, href);
+}
+
+static GdkPixbuf *
+e_rss_preferences_create_icon_pixbuf (const gchar *icon_filename)
+{
+       GdkPixbuf *pixbuf = NULL;
+
+       if (icon_filename && *icon_filename) {
+               GError *error = NULL;
+
+               pixbuf = gdk_pixbuf_new_from_file (icon_filename, &error);
+
+               if (!pixbuf)
+                       g_warning ("%s: Failed to load feed icon '%s': %s", G_STRFUNC, icon_filename, error ? 
error->message : "Unknown error");
+
+               g_clear_error (&error);
+       }
+
+       if (!pixbuf)
+               pixbuf = e_icon_factory_get_icon ("rss", GTK_ICON_SIZE_DIALOG);
+
+       return pixbuf;
+}
+
+static void
+e_rss_preferences_add_feed (GtkListStore *list_store,
+                           CamelRssStoreSummary *store_summary,
+                           const gchar *id)
+{
+       const gchar *href, *display_name, *icon_filename;
+       CamelRssContentType content_type;
+       gchar *description;
+       GdkPixbuf *pixbuf;
+       GtkTreeIter iter;
+
+       href = camel_rss_store_summary_get_href (store_summary, id);
+       display_name = camel_rss_store_summary_get_display_name (store_summary, id);
+       content_type = camel_rss_store_summary_get_content_type (store_summary, id);
+       description = e_rss_preferences_describe_feed (href, display_name);
+       icon_filename = camel_rss_store_summary_get_icon_filename (store_summary, id);
+       pixbuf = e_rss_preferences_create_icon_pixbuf (icon_filename);
+
+       gtk_list_store_append (list_store, &iter);
+       gtk_list_store_set (list_store, &iter,
+               COLUMN_STRING_ID, id,
+               COLUMN_STRING_NAME, display_name,
+               COLUMN_STRING_HREF, href,
+               COLUMN_STRING_CONTENT_TYPE, e_rss_preferences_content_type_to_locale_string (content_type),
+               COLUMN_STRING_DESCRIPTION, description,
+               COLUMN_PIXBUF_ICON, pixbuf,
+               -1);
+
+       g_clear_object (&pixbuf);
+       g_free (description);
+}
+
+static void
+e_rss_preferences_fill_list_store (GtkListStore *list_store,
+                                  CamelRssStoreSummary *store_summary)
+{
+       GSList *feeds, *link;
+
+       gtk_list_store_clear (list_store);
+
+       feeds = camel_rss_store_summary_dup_feeds (store_summary);
+
+       for (link = feeds; link; link = g_slist_next (link)) {
+               const gchar *id = link->data;
+
+               e_rss_preferences_add_feed (list_store, store_summary, id);
+       }
+
+       g_slist_free_full (feeds, g_free);
+}
+
+static void
+e_rss_preferences_source_written_cb (GObject *source_object,
+                                    GAsyncResult *result,
+                                    gpointer user_data)
+{
+       GError *error = NULL;
+
+       if (!e_source_write_finish (E_SOURCE (source_object), result, &error))
+               g_warning ("%s: Failed to save RSS changes: %s", G_STRFUNC, error ? error->message : "Unknown 
error");
+
+       g_clear_error (&error);
+}
+
+static void
+e_rss_preferences_source_changed_cb (ESource *source)
+{
+       e_source_write (source, NULL, e_rss_preferences_source_written_cb, NULL);
+}
+
+static void
+e_rss_preferences_three_state_toggled_cb (GtkToggleButton *widget,
+                                         gpointer user_data)
+{
+       gulong *phandler_id = user_data;
+
+       g_return_if_fail (GTK_IS_TOGGLE_BUTTON (widget));
+       g_return_if_fail (phandler_id != NULL);
+
+       g_signal_handler_block (widget, *phandler_id);
+
+       if (gtk_toggle_button_get_inconsistent (widget) &&
+           gtk_toggle_button_get_active (widget)) {
+               gtk_toggle_button_set_active (widget, FALSE);
+               gtk_toggle_button_set_inconsistent (widget, FALSE);
+       } else if (!gtk_toggle_button_get_active (widget)) {
+               gtk_toggle_button_set_inconsistent (widget, TRUE);
+               gtk_toggle_button_set_active (widget, FALSE);
+       }
+
+       g_signal_handler_unblock (widget, *phandler_id);
+}
+
+static GtkWidget *
+e_rss_preferences_new_three_state_check (const gchar *label)
+{
+       GtkWidget *widget;
+       gulong *phandler_id;
+
+       widget = gtk_check_button_new_with_mnemonic (label);
+
+       g_object_set (widget,
+               "inconsistent", TRUE,
+               "active", FALSE,
+               "visible", TRUE,
+               NULL);
+
+       phandler_id = g_new (gulong, 1);
+
+       *phandler_id = g_signal_connect_data (widget, "toggled",
+               G_CALLBACK (e_rss_preferences_three_state_toggled_cb),
+               phandler_id, (GClosureNotify) g_free, 0);
+
+       return widget;
+}
+
+static CamelThreeState
+e_rss_preferences_three_state_from_widget (GtkToggleButton *button)
+{
+       g_return_val_if_fail (GTK_IS_TOGGLE_BUTTON (button), CAMEL_THREE_STATE_INCONSISTENT);
+
+       if (gtk_toggle_button_get_inconsistent (button))
+               return CAMEL_THREE_STATE_INCONSISTENT;
+
+       if (gtk_toggle_button_get_active (button))
+               return CAMEL_THREE_STATE_ON;
+
+       return CAMEL_THREE_STATE_OFF;
+}
+
+static void
+e_rss_preferences_three_state_to_widget (GtkToggleButton *button,
+                                        CamelThreeState state)
+{
+       g_return_if_fail (GTK_IS_TOGGLE_BUTTON (button));
+
+       g_signal_handlers_block_matched (button, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, 
e_rss_preferences_three_state_toggled_cb, NULL);
+
+       if (state == CAMEL_THREE_STATE_INCONSISTENT) {
+               gtk_toggle_button_set_active (button, FALSE);
+               gtk_toggle_button_set_inconsistent (button, TRUE);
+       } else {
+               gtk_toggle_button_set_inconsistent (button, FALSE);
+               gtk_toggle_button_set_active (button, state == CAMEL_THREE_STATE_ON);
+       }
+
+       g_signal_handlers_unblock_matched (button, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, 
e_rss_preferences_three_state_toggled_cb, NULL);
+}
+
+typedef struct _FolderOpts {
+       CamelThreeState complete_articles;
+       CamelThreeState feed_enclosures;
+} FolderOpts;
+
+static void
+e_rss_properties_got_folder_to_save_cb (GObject *source_object,
+                                       GAsyncResult *result,
+                                       gpointer user_data)
+{
+       FolderOpts *fo = user_data;
+       CamelFolder *folder;
+       GError *error = NULL;
+
+       folder = camel_store_get_folder_finish (CAMEL_STORE (source_object), result, &error);
+
+       if (folder) {
+               g_object_set (folder,
+                       "complete-articles", fo->complete_articles,
+                       "feed-enclosures", fo->feed_enclosures,
+                       NULL);
+
+               g_object_unref (folder);
+       } else {
+               g_warning ("%s: Failed to get folder: %s", G_STRFUNC, error ? error->message : "Unknown 
error");
+       }
+
+       g_slice_free (FolderOpts, fo);
+}
+
+typedef struct _PopoverData {
+       gchar *id; /* can be NULL */
+       GtkEntry *href;
+       GtkWidget *fetch_button;
+       GtkEntry *name;
+       GtkButton *icon_button;
+       GtkImage *icon_image;
+       GtkComboBox *content_type;
+       GtkToggleButton *complete_articles;
+       GtkToggleButton *feed_enclosures;
+       GtkWidget *save_button;
+       gchar *icon_filename;
+       EActivityBar *activity_bar;
+       EActivity *activity;
+} PopoverData;
+
+static void
+popover_data_cancel_activity (PopoverData *pd)
+{
+       GCancellable *cancellable;
+
+       if (!pd || !pd->activity)
+               return;
+
+       cancellable = e_activity_get_cancellable (pd->activity);
+       g_cancellable_cancel (cancellable);
+
+       e_activity_set_state (pd->activity, E_ACTIVITY_CANCELLED);
+
+       g_clear_object (&pd->activity);
+}
+
+static void
+popover_data_free (gpointer ptr)
+{
+       PopoverData *pd = ptr;
+
+       if (pd) {
+               popover_data_cancel_activity (pd);
+
+               g_free (pd->id);
+               g_free (pd->icon_filename);
+               g_free (pd);
+       }
+}
+
+static void
+e_rss_preferences_entry_changed_cb (GtkEntry *entry,
+                                   gpointer user_data)
+{
+       GObject *popover = user_data;
+       PopoverData *pd;
+       const gchar *text;
+       gboolean sensitive;
+
+       pd = g_object_get_data (popover, "e-rss-popover-data");
+
+       text = gtk_entry_get_text (pd->href);
+       sensitive = text && *text;
+       gtk_widget_set_sensitive (pd->fetch_button, sensitive);
+
+       if (sensitive) {
+               text = gtk_entry_get_text (pd->name);
+               sensitive = text && *text;
+       }
+
+       gtk_widget_set_sensitive (pd->save_button, sensitive);
+}
+
+static void
+e_rss_preferences_feed_icon_ready_cb (GObject *source_object,
+                                     GAsyncResult *result,
+                                     gpointer user_data)
+{
+       GObject *popover = user_data;
+       GBytes *bytes;
+       GError *error = NULL;
+
+       bytes = soup_session_send_and_read_finish (SOUP_SESSION (source_object), result, &error);
+
+       if (bytes) {
+               PopoverData *pd = g_object_get_data (popover, "e-rss-popover-data");
+               SoupMessage *message = soup_session_get_async_result_message (SOUP_SESSION (source_object), 
result);
+               gboolean success = !error && g_bytes_get_size (bytes) > 0 && message &&
+                       SOUP_STATUS_IS_SUCCESSFUL (soup_message_get_status (message));
+
+               if (success) {
+                       gchar *tmp_file;
+
+                       tmp_file = e_mktemp ("rss-feed-XXXXXX.png");
+                       success = g_file_set_contents (tmp_file, (const gchar *) g_bytes_get_data (bytes, 
NULL), g_bytes_get_size (bytes), &error);
+
+                       if (success) {
+                               gtk_image_set_from_file (pd->icon_image, tmp_file);
+                               g_clear_pointer (&pd->icon_filename, g_free);
+                               pd->icon_filename = g_steal_pointer (&tmp_file);
+                       }
+
+                       g_free (tmp_file);
+               }
+
+               if (success) {
+                       e_activity_set_state (pd->activity, E_ACTIVITY_COMPLETED);
+                       g_clear_object (&pd->activity);
+               }
+       }
+
+       if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+               PopoverData *pd = g_object_get_data (popover, "e-rss-popover-data");
+               gchar *message;
+
+               message = g_strdup_printf (_("Failed to fetch feed icon: %s"), error->message);
+
+               e_activity_set_state (pd->activity, E_ACTIVITY_WAITING);
+               e_activity_set_text (pd->activity, message);
+
+               g_free (message);
+       }
+
+       g_clear_pointer (&bytes, g_bytes_unref);
+       g_clear_error (&error);
+}
+
+static void
+e_rss_preferences_feed_info_ready_cb (GObject *source_object,
+                                     GAsyncResult *result,
+                                     gpointer user_data)
+{
+       GObject *popover = user_data;
+       GBytes *bytes;
+       GError *error = NULL;
+
+       bytes = soup_session_send_and_read_finish (SOUP_SESSION (source_object), result, &error);
+
+       if (bytes) {
+               PopoverData *pd = g_object_get_data (popover, "e-rss-popover-data");
+               GCancellable *cancellable = e_activity_get_cancellable (pd->activity);
+               SoupMessage *message = soup_session_get_async_result_message (SOUP_SESSION (source_object), 
result);
+               gboolean success = !error && g_bytes_get_size (bytes) > 0 && message &&
+                       SOUP_STATUS_IS_SUCCESSFUL (soup_message_get_status (message));
+
+               if (success) {
+                       gchar *link = NULL, *alt_link = NULL, *title = NULL, *icon = NULL;
+
+                       success = e_rss_parser_parse ((const gchar *) g_bytes_get_data (bytes, NULL), 
g_bytes_get_size (bytes), &link, &alt_link, &title, &icon, NULL);
+                       if (success) {
+                               if ((link && camel_strstrcase (link, "gitlab")) ||
+                                   (alt_link && camel_strstrcase (alt_link, "gitlab")))
+                                       gtk_combo_box_set_active_id (pd->content_type, 
e_rss_preferences_content_type_to_string (CAMEL_RSS_CONTENT_TYPE_MARKDOWN));
+                               else
+                                       gtk_combo_box_set_active_id (pd->content_type, 
e_rss_preferences_content_type_to_string (CAMEL_RSS_CONTENT_TYPE_HTML));
+
+                               if (title && *title)
+                                       gtk_entry_set_text (pd->name, title);
+
+                               if (icon && *icon) {
+                                       SoupMessage *message;
+
+                                       e_activity_set_text (pd->activity, _("Fetching feed icon…"));
+
+                                       message = soup_message_new (SOUP_METHOD_GET, icon);
+                                       if (message) {
+                                               soup_session_send_and_read_async (SOUP_SESSION 
(source_object), message, G_PRIORITY_DEFAULT, cancellable,
+                                                       e_rss_preferences_feed_icon_ready_cb, popover);
+
+                                               g_object_unref (message);
+
+                                               /* Not as a problem, but as a flag to not complete the 
activity */
+                                               success = FALSE;
+                                       }
+                               }
+                       } else {
+                               g_set_error_literal (&error, G_IO_ERROR, G_IO_ERROR_FAILED, _("Failed to read 
feed information."));
+                       }
+
+                       g_free (link);
+                       g_free (alt_link);
+                       g_free (title);
+                       g_free (icon);
+               }
+
+               if (success) {
+                       e_activity_set_state (pd->activity, E_ACTIVITY_COMPLETED);
+                       g_clear_object (&pd->activity);
+               }
+       }
+
+       if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+               PopoverData *pd = g_object_get_data (popover, "e-rss-popover-data");
+               gchar *message;
+
+               message = g_strdup_printf (_("Failed to fetch feed information: %s"), error->message);
+
+               e_activity_set_state (pd->activity, E_ACTIVITY_WAITING);
+               e_activity_set_text (pd->activity, message);
+
+               g_free (message);
+       }
+
+       g_clear_pointer (&bytes, g_bytes_unref);
+       g_clear_error (&error);
+}
+
+static void
+e_rss_preferences_fetch_clicked_cb (GtkWidget *button,
+                                   gpointer user_data)
+{
+       GObject *popover = user_data;
+       SoupSession *session;
+       SoupMessage *message;
+       GCancellable *cancellable;
+       PopoverData *pd;
+
+       pd = g_object_get_data (popover, "e-rss-popover-data");
+       cancellable = g_cancellable_new ();
+
+       popover_data_cancel_activity (pd);
+
+       pd->activity = e_activity_new ();
+       e_activity_set_cancellable (pd->activity, cancellable);
+       e_activity_set_state (pd->activity, E_ACTIVITY_RUNNING);
+       e_activity_set_text (pd->activity, _("Fetching feed information…"));
+       e_activity_bar_set_activity (pd->activity_bar, pd->activity);
+
+       message = soup_message_new (SOUP_METHOD_GET, gtk_entry_get_text (pd->href));
+       if (!message) {
+               e_activity_set_text (pd->activity, _("Invalid Feed URL"));
+               e_activity_set_state (pd->activity, E_ACTIVITY_WAITING);
+               g_clear_object (&cancellable);
+
+               return;
+       }
+
+       session = soup_session_new_with_options (
+               "timeout", 30,
+               "user-agent", "Evolution/" VERSION,
+               NULL);
+
+       if (camel_debug ("rss")) {
+               SoupLogger *logger;
+
+               logger = soup_logger_new (SOUP_LOGGER_LOG_BODY);
+               soup_session_add_feature (session, SOUP_SESSION_FEATURE (logger));
+               g_object_unref (logger);
+       }
+
+       soup_session_send_and_read_async (session, message, G_PRIORITY_DEFAULT, cancellable,
+               e_rss_preferences_feed_info_ready_cb, popover);
+
+       g_clear_object (&message);
+       g_clear_object (&session);
+       g_clear_object (&cancellable);
+}
+
+static void
+e_rss_preferences_icon_clicked_cb (GtkWidget *button,
+                                  gpointer user_data)
+{
+       GObject *popover = user_data;
+       PopoverData *pd;
+       GtkWidget *dialog;
+       GtkWindow *parent;
+       GFile *file;
+
+       pd = g_object_get_data (popover, "e-rss-popover-data");
+
+       dialog = gtk_widget_get_toplevel (button);
+       parent = GTK_IS_WINDOW (dialog) ? GTK_WINDOW (dialog) : NULL;
+
+       dialog = e_image_chooser_dialog_new (_("Choose Feed Image"), parent);
+       file = e_image_chooser_dialog_run (E_IMAGE_CHOOSER_DIALOG (dialog));
+
+       g_clear_pointer (&pd->icon_filename, g_free);
+
+       if (G_IS_FILE (file)) {
+               pd->icon_filename = g_file_get_path (file);
+               gtk_image_set_from_file (pd->icon_image, pd->icon_filename);
+       } else {
+               gtk_image_set_from_icon_name (pd->icon_image, "rss", GTK_ICON_SIZE_DIALOG);
+       }
+
+       gtk_widget_destroy (dialog);
+}
+
+/* Copy icon to the private directory */
+static gchar *
+e_rss_preferences_maybe_copy_icon (const gchar *feed_id,
+                                  const gchar *icon_filename,
+                                  const gchar *user_data_dir)
+{
+       gchar *basename, *filename;
+       GFile *src, *des;
+       const gchar *ext;
+       GError *error = NULL;
+
+       if (!icon_filename || !*icon_filename || !user_data_dir || !*user_data_dir ||
+           g_str_has_prefix (icon_filename, user_data_dir))
+               return NULL;
+
+       basename = g_path_get_basename (icon_filename);
+       if (basename && *basename && (*basename == G_DIR_SEPARATOR || *basename == '.')) {
+               g_free (basename);
+               return NULL;
+       }
+
+       ext = strrchr (basename, '.');
+       if (!ext || !ext[1])
+               ext = ".png";
+
+       filename = g_strconcat (user_data_dir, G_DIR_SEPARATOR_S, feed_id, ext, NULL);
+
+       src = g_file_new_for_path (icon_filename);
+       des = g_file_new_for_path (filename);
+
+       if (g_file_copy (src, des, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, &error))
+               gtk_icon_theme_rescan_if_needed (gtk_icon_theme_get_default ());
+       else
+               g_warning ("Failed to copy icon file '%s' to '%s': %s", icon_filename, filename, error ? 
error->message : "Unknown error");
+
+       g_clear_error (&error);
+       g_clear_object (&src);
+       g_clear_object (&des);
+
+       g_free (basename);
+
+       return filename;
+}
+
+static void
+e_rss_preferences_save_clicked_cb (GtkWidget *button,
+                                  gpointer user_data)
+{
+       GObject *popover = user_data;
+       CamelService *service;
+       CamelRssStoreSummary *store_summary = NULL;
+       CamelRssContentType content_type;
+       FolderOpts *fo;
+       gchar *icon_filename;
+       const gchar *user_data_dir;
+       PopoverData *pd;
+       GError *error = NULL;
+
+       pd = g_object_get_data (popover, "e-rss-popover-data");
+
+       service = e_rss_preferences_ref_store (e_shell_get_default ());
+       if (!service) {
+               g_warn_if_reached ();
+               return;
+       }
+
+       g_object_get (service, "summary", &store_summary, NULL);
+
+       if (!store_summary) {
+               g_clear_object (&service);
+               g_warn_if_reached ();
+               return;
+       }
+
+       user_data_dir = camel_service_get_user_data_dir (service);
+       icon_filename = pd->icon_filename;
+       content_type = e_rss_preferences_content_type_from_string (gtk_combo_box_get_active_id 
(pd->content_type));
+
+       if (pd->id) {
+               const gchar *display_name;
+               gchar *old_display_name;
+               gchar *real_icon_filename;
+
+               old_display_name = g_strdup (camel_rss_store_summary_get_display_name (store_summary, 
pd->id));
+               display_name = gtk_entry_get_text (pd->name);
+
+               real_icon_filename = e_rss_preferences_maybe_copy_icon (pd->id, icon_filename, user_data_dir);
+
+               camel_rss_store_summary_set_display_name (store_summary, pd->id, display_name);
+               camel_rss_store_summary_set_icon_filename (store_summary, pd->id, real_icon_filename ? 
real_icon_filename : icon_filename);
+               camel_rss_store_summary_set_content_type (store_summary, pd->id, content_type);
+
+               if (camel_rss_store_summary_save (store_summary, &error) &&
+                   g_strcmp0 (old_display_name, display_name) != 0) {
+                       CamelFolderInfo *fi;
+
+                       fi = camel_rss_store_summary_dup_folder_info (store_summary, pd->id);
+
+                       camel_store_folder_renamed (CAMEL_STORE (service), pd->id, fi);
+
+                       camel_folder_info_free (fi);
+               }
+
+               g_free (real_icon_filename);
+               g_free (old_display_name);
+       } else {
+               const gchar *new_id;
+
+               new_id = camel_rss_store_summary_add (store_summary,
+                       gtk_entry_get_text (pd->href),
+                       gtk_entry_get_text (pd->name),
+                       icon_filename,
+                       content_type);
+
+               if (new_id) {
+                       gchar *real_icon_filename;
+
+                       pd->id = g_strdup (new_id);
+
+                       real_icon_filename = e_rss_preferences_maybe_copy_icon (pd->id, icon_filename, 
user_data_dir);
+                       if (real_icon_filename) {
+                               camel_rss_store_summary_set_icon_filename (store_summary, pd->id, 
real_icon_filename);
+                               g_free (real_icon_filename);
+                       }
+
+                       if (camel_rss_store_summary_save (store_summary, &error)) {
+                               CamelFolderInfo *fi;
+
+                               fi = camel_rss_store_summary_dup_folder_info (store_summary, pd->id);
+
+                               camel_store_folder_created (CAMEL_STORE (service), fi);
+
+                               camel_folder_info_free (fi);
+                       }
+               }
+       }
+
+       fo = g_slice_new0 (FolderOpts);
+       fo->complete_articles = e_rss_preferences_three_state_from_widget (pd->complete_articles);
+       fo->feed_enclosures = e_rss_preferences_three_state_from_widget (pd->feed_enclosures);
+
+       camel_store_get_folder (CAMEL_STORE (service), pd->id, CAMEL_STORE_FOLDER_NONE, G_PRIORITY_DEFAULT, 
NULL,
+               e_rss_properties_got_folder_to_save_cb, fo);
+
+       if (error) {
+               g_warning ("Failed to store RSS settings: %s", error->message);
+               g_clear_error (&error);
+       }
+
+       g_clear_object (&store_summary);
+       g_clear_object (&service);
+
+       gtk_widget_hide (GTK_WIDGET (popover));
+}
+
+static GtkPopover *
+e_rss_preferences_get_popover (GtkWidget *parent,
+                              GtkTreeView *tree_view,
+                              const gchar *id,
+                              PopoverData **out_pd)
+{
+       GtkPopover *popover;
+       PopoverData *pd;
+       GtkGrid *grid;
+       GtkWidget *widget, *label;
+
+       popover = g_object_get_data (G_OBJECT (tree_view), "e-rss-popover");
+
+       if (popover) {
+               pd = g_object_get_data (G_OBJECT (popover), "e-rss-popover-data");
+               gtk_popover_set_relative_to (popover, parent);
+               g_clear_pointer (&pd->id, g_free);
+               g_clear_pointer (&pd->icon_filename, g_free);
+               pd->id = g_strdup (id);
+
+               *out_pd = pd;
+
+               return popover;
+       }
+
+       pd = g_new0 (PopoverData, 1);
+       pd->id = g_strdup (id);
+
+       popover = GTK_POPOVER (gtk_popover_new (parent));
+
+       grid = GTK_GRID (gtk_grid_new ());
+       gtk_grid_set_column_spacing (grid, 6);
+       gtk_grid_set_row_spacing (grid, 6);
+
+       widget = gtk_button_new ();
+       g_object_set (G_OBJECT (widget),
+               "halign", GTK_ALIGN_START,
+               "valign", GTK_ALIGN_START,
+               NULL);
+       gtk_grid_attach (grid, widget, 0, 0, 1, 3);
+       pd->icon_button = GTK_BUTTON (widget);
+
+       widget = gtk_image_new_from_icon_name ("rss", GTK_ICON_SIZE_DIALOG);
+       gtk_container_add (GTK_CONTAINER (pd->icon_button), widget);
+       pd->icon_image = GTK_IMAGE (widget);
+
+       widget = gtk_label_new_with_mnemonic (_("Feed _URL:"));
+       gtk_widget_set_halign (widget, GTK_ALIGN_END);
+       gtk_grid_attach (grid, widget, 1, 0, 1, 1);
+       label = widget;
+
+       widget = gtk_entry_new ();
+       gtk_widget_set_size_request (widget, 250, -1);
+       gtk_widget_set_halign (widget, GTK_ALIGN_FILL);
+       gtk_entry_set_activates_default (GTK_ENTRY (widget), TRUE);
+       gtk_label_set_mnemonic_widget (GTK_LABEL (label), widget);
+       gtk_grid_attach (grid, widget, 2, 0, 1, 1);
+       pd->href = GTK_ENTRY (widget);
+
+       widget = gtk_button_new_with_mnemonic (_("_Fetch"));
+       gtk_grid_attach (grid, widget, 3, 0, 1, 1);
+       pd->fetch_button = widget;
+
+       widget = gtk_label_new_with_mnemonic (_("_Name:"));
+       gtk_widget_set_halign (widget, GTK_ALIGN_END);
+       gtk_grid_attach (grid, widget, 1, 1, 1, 1);
+       label = widget;
+
+       widget = gtk_entry_new ();
+       gtk_widget_set_halign (widget, GTK_ALIGN_FILL);
+       gtk_entry_set_activates_default (GTK_ENTRY (widget), TRUE);
+       gtk_label_set_mnemonic_widget (GTK_LABEL (label), widget);
+       gtk_grid_attach (grid, widget, 2, 1, 2, 1);
+       pd->name = GTK_ENTRY (widget);
+
+       widget = gtk_label_new_with_mnemonic (_("C_ontent:"));
+       gtk_widget_set_halign (widget, GTK_ALIGN_END);
+       gtk_grid_attach (grid, widget, 1, 2, 1, 1);
+       label = widget;
+
+       widget = gtk_combo_box_text_new ();
+       gtk_widget_set_size_request (widget, 250, -1);
+       gtk_label_set_mnemonic_widget (GTK_LABEL (label), widget);
+       gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "html", _("HTML"));
+       gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "text", _("Plain Text"));
+       gtk_combo_box_text_append (GTK_COMBO_BOX_TEXT (widget), "markdown", _("Markdown"));
+       gtk_grid_attach (grid, widget, 2, 2, 2, 1);
+       pd->content_type = GTK_COMBO_BOX (widget);
+
+       widget = e_rss_preferences_new_three_state_check (_("_Download complete articles"));
+       gtk_grid_attach (grid, widget, 2, 3, 2, 1);
+       pd->complete_articles = GTK_TOGGLE_BUTTON (widget);
+
+       widget = e_rss_preferences_new_three_state_check (_("Download feed _enclosures"));
+       gtk_grid_attach (grid, widget, 2, 4, 2, 1);
+       pd->feed_enclosures = GTK_TOGGLE_BUTTON (widget);
+
+       widget = gtk_button_new_with_mnemonic (_("_Save"));
+       gtk_widget_set_halign (widget, GTK_ALIGN_END);
+       gtk_grid_attach (grid, widget, 1, 5, 3, 1);
+       pd->save_button = widget;
+
+       gtk_widget_show_all (GTK_WIDGET (grid));
+
+       widget = e_activity_bar_new ();
+       gtk_grid_attach (grid, widget, 0, 6, 4, 1);
+       pd->activity_bar = E_ACTIVITY_BAR (widget);
+
+       gtk_popover_set_position (popover, GTK_POS_BOTTOM);
+       gtk_container_add (GTK_CONTAINER (popover), GTK_WIDGET (grid));
+       gtk_container_set_border_width (GTK_CONTAINER (popover), 6);
+
+       g_object_set_data_full (G_OBJECT (popover), "e-rss-popover-data", pd, popover_data_free);
+       g_object_set_data_full (G_OBJECT (tree_view), "e-rss-popover", g_object_ref_sink (popover), 
g_object_unref);
+
+       g_signal_connect_object (pd->href, "changed",
+               G_CALLBACK (e_rss_preferences_entry_changed_cb), popover, 0);
+
+       g_signal_connect_object (pd->name, "changed",
+               G_CALLBACK (e_rss_preferences_entry_changed_cb), popover, 0);
+
+       g_signal_connect_object (pd->fetch_button, "clicked",
+               G_CALLBACK (e_rss_preferences_fetch_clicked_cb), popover, 0);
+
+       g_signal_connect_object (pd->icon_button, "clicked",
+               G_CALLBACK (e_rss_preferences_icon_clicked_cb), popover, 0);
+
+       g_signal_connect_object (pd->save_button, "clicked",
+               G_CALLBACK (e_rss_preferences_save_clicked_cb), popover, 0);
+
+       e_rss_preferences_entry_changed_cb (pd->href, popover);
+
+       *out_pd = pd;
+
+       return popover;
+}
+
+static void
+e_rss_preferences_add_clicked_cb (GtkWidget *button,
+                                 GtkTreeView *tree_view)
+{
+       GtkPopover *popover;
+       PopoverData *pd = NULL;
+
+       popover = e_rss_preferences_get_popover (button, tree_view, NULL, &pd);
+
+       gtk_entry_set_text (pd->href, "");
+       gtk_entry_set_text (pd->name, "");
+       gtk_image_set_from_icon_name (pd->icon_image, "rss", GTK_ICON_SIZE_DIALOG);
+       gtk_combo_box_set_active_id (pd->content_type, "html");
+       e_rss_preferences_three_state_to_widget (pd->complete_articles, CAMEL_THREE_STATE_INCONSISTENT);
+       e_rss_preferences_three_state_to_widget (pd->feed_enclosures, CAMEL_THREE_STATE_INCONSISTENT);
+       g_clear_pointer (&pd->icon_filename, g_free);
+       g_clear_pointer (&pd->id, g_free);
+
+       gtk_widget_show (GTK_WIDGET (popover));
+}
+
+static gchar *
+e_rss_preferences_dup_selected_id (GtkTreeView *tree_view,
+                                  CamelStore **out_store)
+{
+       CamelService *service;
+       GtkTreeSelection *selection;
+       GtkTreeModel *model = NULL;
+       GtkTreeIter iter;
+       gchar *id = NULL;
+
+       if (out_store)
+               *out_store = NULL;
+
+       selection = gtk_tree_view_get_selection (tree_view);
+
+       if (!gtk_tree_selection_get_selected (selection, &model, &iter))
+               return NULL;
+
+       gtk_tree_model_get (model, &iter,
+               COLUMN_STRING_ID, &id,
+               -1);
+
+       if (!id)
+               return NULL;
+
+       service = e_rss_preferences_ref_store (e_shell_get_default ());
+       if (!service) {
+               g_warn_if_reached ();
+               g_free (id);
+               return NULL;
+       }
+
+       if (out_store)
+               *out_store = CAMEL_STORE (service);
+       else
+               g_object_unref (service);
+
+       return id;
+}
+
+static void
+e_rss_properties_got_folder_to_edit_cb (GObject *source_object,
+                                       GAsyncResult *result,
+                                       gpointer user_data)
+{
+       GtkTreeView *tree_view = user_data;
+       CamelFolder *folder;
+       GError *error = NULL;
+
+       folder = camel_store_get_folder_finish (CAMEL_STORE (source_object), result, &error);
+
+       if (folder) {
+               CamelRssStoreSummary *store_summary = NULL;
+               CamelThreeState state = CAMEL_THREE_STATE_INCONSISTENT;
+               GtkPopover *popover;
+               PopoverData *pd = NULL;
+               const gchar *icon_filename, *id;
+
+               id = camel_folder_get_full_name (folder);
+               g_object_get (source_object, "summary", &store_summary, NULL);
+               popover = g_object_get_data (G_OBJECT (tree_view), "e-rss-popover");
+               g_warn_if_fail (popover != NULL);
+               pd = g_object_get_data (G_OBJECT (popover), "e-rss-popover-data");
+               g_warn_if_fail (pd != NULL);
+               g_warn_if_fail (g_strcmp0 (id, pd->id) == 0);
+
+               icon_filename = camel_rss_store_summary_get_icon_filename (store_summary, id);
+
+               gtk_entry_set_text (pd->href, camel_rss_store_summary_get_href (store_summary, id));
+               gtk_entry_set_text (pd->name, camel_rss_store_summary_get_display_name (store_summary, id));
+
+               if (icon_filename && g_file_test (icon_filename, G_FILE_TEST_IS_REGULAR))
+                       gtk_image_set_from_file (pd->icon_image, icon_filename);
+               else
+                       gtk_image_set_from_icon_name (pd->icon_image, "rss", GTK_ICON_SIZE_DIALOG);
+
+               gtk_combo_box_set_active_id (pd->content_type, e_rss_preferences_content_type_to_string (
+                       camel_rss_store_summary_get_content_type (store_summary, id)));
+
+               g_clear_pointer (&pd->icon_filename, g_free);
+               pd->icon_filename = g_strdup (icon_filename);
+
+               g_object_get (folder, "complete-articles", &state, NULL);
+               e_rss_preferences_three_state_to_widget (pd->complete_articles, state);
+
+               g_object_get (folder, "feed-enclosures", &state, NULL);
+               e_rss_preferences_three_state_to_widget (pd->feed_enclosures, state);
+
+               gtk_widget_show (GTK_WIDGET (popover));
+
+               g_clear_object (&store_summary);
+               g_object_unref (folder);
+       } else {
+               g_warning ("%s: Failed to get folder: %s", G_STRFUNC, error ? error->message : "Unknown 
error");
+       }
+
+       g_clear_object (&tree_view);
+}
+
+static void
+e_rss_preferences_edit_clicked_cb (GtkWidget *button,
+                                  GtkTreeView *tree_view)
+{
+       CamelStore *store = NULL;
+       gchar *id;
+
+       id = e_rss_preferences_dup_selected_id (tree_view, &store);
+       if (id) {
+               PopoverData *pd = NULL;
+
+               /* prepare the popover */
+               g_warn_if_fail (e_rss_preferences_get_popover (button, tree_view, id, &pd) != NULL);
+
+               camel_store_get_folder (store, id, CAMEL_STORE_FOLDER_NONE, G_PRIORITY_DEFAULT, NULL,
+                       e_rss_properties_got_folder_to_edit_cb, g_object_ref (tree_view));
+       }
+
+       g_clear_object (&store);
+       g_free (id);
+}
+
+static void
+e_rss_preferences_delete_done_cb (GObject *source_object,
+                                 GAsyncResult *result,
+                                 gpointer user_data)
+{
+       GError *error = NULL;
+
+       if (!camel_store_delete_folder_finish (CAMEL_STORE (source_object), result, &error))
+               g_warning ("%s: Failed to delete folder: %s", G_STRFUNC, error ? error->message : "Unknown 
error");
+
+       g_clear_error (&error);
+}
+
+static void
+e_rss_preferences_remove_clicked_cb (GtkButton *button,
+                                    GtkTreeView *tree_view)
+{
+       CamelStore *store = NULL;
+       gchar *id;
+
+       id = e_rss_preferences_dup_selected_id (tree_view, &store);
+       if (id)
+               camel_store_delete_folder (store, id, G_PRIORITY_DEFAULT, NULL, 
e_rss_preferences_delete_done_cb, NULL);
+
+       g_clear_object (&store);
+       g_free (id);
+}
+
+static void
+e_rss_pereferences_selection_changed_cb (GtkTreeSelection *selection,
+                                        GtkWidget *button)
+{
+       gtk_widget_set_sensitive (button, gtk_tree_selection_get_selected (selection, NULL, NULL));
+}
+
+static void
+e_rss_preferences_map_cb (GtkTreeView *tree_view,
+                         gpointer user_data)
+{
+       CamelRssStoreSummary *store_summary = user_data;
+       GtkTreeModel *model;
+
+       model = gtk_tree_view_get_model (tree_view);
+
+       e_rss_preferences_fill_list_store (GTK_LIST_STORE (model), store_summary);
+}
+
+static void
+e_rss_preferences_feed_changed_cb (CamelRssStoreSummary *store_summary,
+                                  const gchar *id,
+                                  gpointer user_data)
+{
+       GtkTreeView *tree_view = user_data;
+       GtkTreeIter iter;
+       GtkTreeModel *model;
+       GtkListStore *list_store;
+       gboolean found;
+
+       if (!gtk_widget_get_mapped (GTK_WIDGET (tree_view)))
+               return;
+
+       model = gtk_tree_view_get_model (tree_view);
+       list_store = GTK_LIST_STORE (model);
+
+       found = gtk_tree_model_get_iter_first (model, &iter);
+       while (found) {
+               gchar *stored_id = NULL;
+
+               gtk_tree_model_get (model, &iter,
+                       COLUMN_STRING_ID, &stored_id,
+                       -1);
+
+               found = g_strcmp0 (id, stored_id) == 0;
+
+               g_free (stored_id);
+
+               if (found)
+                       break;
+
+               found = gtk_tree_model_iter_next (model, &iter);
+       }
+
+       if (found) {
+               if (camel_rss_store_summary_contains (store_summary, id)) {
+                       const gchar *href, *display_name, *icon_filename;
+                       CamelRssContentType content_type;
+                       gchar *description;
+                       GdkPixbuf *pixbuf;
+
+                       href = camel_rss_store_summary_get_href (store_summary, id);
+                       display_name = camel_rss_store_summary_get_display_name (store_summary, id);
+                       content_type = camel_rss_store_summary_get_content_type (store_summary, id);
+                       description = e_rss_preferences_describe_feed (href, display_name);
+                       icon_filename = camel_rss_store_summary_get_icon_filename (store_summary, id);
+                       pixbuf = e_rss_preferences_create_icon_pixbuf (icon_filename);
+
+                       gtk_list_store_set (list_store, &iter,
+                               COLUMN_STRING_NAME, display_name,
+                               COLUMN_STRING_HREF, href,
+                               COLUMN_STRING_CONTENT_TYPE, e_rss_preferences_content_type_to_locale_string 
(content_type),
+                               COLUMN_STRING_DESCRIPTION, description,
+                               COLUMN_PIXBUF_ICON, pixbuf,
+                               -1);
+
+                       g_clear_object (&pixbuf);
+                       g_free (description);
+               } else {
+                       gtk_list_store_remove (list_store, &iter);
+               }
+       } else if (camel_rss_store_summary_contains (store_summary, id)) {
+               e_rss_preferences_add_feed (list_store, store_summary, id);
+       }
+}
+
+static void
+e_rss_preferences_row_activated_cb (GtkTreeView *tree_view,
+                                   GtkTreePath *path,
+                                   GtkTreeViewColumn *column,
+                                   gpointer user_data)
+{
+       GtkWidget *button = user_data;
+
+       e_rss_preferences_edit_clicked_cb (button, tree_view);
+}
+
+static GtkWidget *
+e_rss_preferences_new (EPreferencesWindow *window)
+{
+       CamelService *service;
+       CamelSettings *settings;
+       CamelRssStoreSummary *store_summary = NULL;
+       EShell *shell;
+       ESource *source;
+       PangoAttrList *bold;
+       GtkGrid *grid;
+       GtkWidget *widget, *hbox, *spin, *scrolled_window, *button_box;
+       GtkListStore *list_store;
+       GtkTreeSelection *selection;
+       GtkTreeView *tree_view;
+       GtkTreeViewColumn *column;
+       GtkCellRenderer *cell_renderer;
+       gint row = 0;
+
+       shell = e_preferences_window_get_shell (window);
+       service = e_rss_preferences_ref_store (shell);
+       if (!service)
+               return NULL;
+
+       g_object_get (service, "summary", &store_summary, NULL);
+
+       if (!store_summary) {
+               g_clear_object (&service);
+               g_warn_if_reached ();
+               return NULL;
+       }
+
+       source = e_source_registry_ref_source (e_shell_get_registry (shell), "rss");
+       if (source) {
+               /* Auto-save changes */
+               g_signal_connect (source, "changed",
+                       G_CALLBACK (e_rss_preferences_source_changed_cb), NULL);
+               g_clear_object (&source);
+       } else {
+               g_warn_if_reached ();
+       }
+
+       settings = camel_service_ref_settings (service);
+
+       bold = pango_attr_list_new ();
+       pango_attr_list_insert (bold, pango_attr_weight_new (PANGO_WEIGHT_BOLD));
+
+       grid = GTK_GRID (gtk_grid_new ());
+       g_object_set (G_OBJECT (grid),
+               "halign", GTK_ALIGN_FILL,
+               "hexpand", TRUE,
+               "valign", GTK_ALIGN_FILL,
+               "vexpand", TRUE,
+               "border-width", 12,
+               NULL);
+
+       widget = gtk_label_new (_("General"));
+       g_object_set (G_OBJECT (widget),
+               "halign", GTK_ALIGN_START,
+               "hexpand", FALSE,
+               "attributes", bold,
+               NULL);
+
+       gtk_grid_attach (grid, widget, 0, row, 2, 1);
+       row++;
+
+       widget = gtk_check_button_new_with_mnemonic (_("_Download complete articles"));
+       g_object_set (G_OBJECT (widget),
+               "margin-start", 12,
+               NULL);
+
+       e_binding_bind_property (
+               settings, "complete-articles",
+               widget, "active",
+               G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
+
+       gtk_grid_attach (grid, widget, 0, row, 2, 1);
+       row++;
+
+       widget = gtk_check_button_new_with_mnemonic (_("_Download feed enclosures"));
+       g_object_set (G_OBJECT (widget),
+               "margin-start", 12,
+               NULL);
+
+       e_binding_bind_property (
+               settings, "feed-enclosures",
+               widget, "active",
+               G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
+
+       gtk_grid_attach (grid, widget, 0, row, 2, 1);
+       row++;
+
+       hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 4);
+       g_object_set (G_OBJECT (hbox),
+               "margin-start", 12,
+               NULL);
+
+       /* Translators: This is part of "Do not download enclosures larger than [ nnn ] KB" */
+       widget = gtk_check_button_new_with_mnemonic (_("Do not download enclosures larger than"));
+       gtk_box_pack_start (GTK_BOX (hbox), widget, FALSE, FALSE, 0);
+
+       spin = gtk_spin_button_new_with_range (1, 999999, 100);
+       gtk_box_pack_start (GTK_BOX (hbox), spin, FALSE, FALSE, 0);
+
+       e_binding_bind_property (
+               widget, "active",
+               spin, "sensitive",
+               G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE);
+
+       e_binding_bind_property (
+               settings, "limit-feed-enclosure-size",
+               widget, "active",
+               G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
+
+       e_binding_bind_property (
+               settings, "max-feed-enclosure-size",
+               spin, "value",
+               G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
+
+
+       /* Translators: This is part of "Do not download enclosures larger than [ nnn ] KB" */
+       widget = gtk_label_new (_("KB"));
+       gtk_box_pack_start (GTK_BOX (hbox), widget, FALSE, FALSE, 0);
+
+       gtk_grid_attach (grid, hbox, 0, row, 2, 1);
+       row++;
+
+       widget = gtk_label_new (_("Feeds"));
+       g_object_set (G_OBJECT (widget),
+               "halign", GTK_ALIGN_START,
+               "hexpand", FALSE,
+               "attributes", bold,
+               NULL);
+
+       gtk_grid_attach (grid, widget, 0, row, 2, 1);
+       row++;
+
+       scrolled_window = gtk_scrolled_window_new (NULL, NULL);
+       g_object_set (G_OBJECT (scrolled_window),
+               "halign", GTK_ALIGN_FILL,
+               "hexpand", TRUE,
+               "valign", GTK_ALIGN_FILL,
+               "vexpand", TRUE,
+               "margin-start", 12,
+               "hscrollbar-policy", GTK_POLICY_AUTOMATIC,
+               "vscrollbar-policy", GTK_POLICY_AUTOMATIC,
+               "shadow-type", GTK_SHADOW_IN,
+               NULL);
+
+       list_store = gtk_list_store_new (N_COLUMNS,
+               G_TYPE_STRING,          /* COLUMN_STRING_ID */
+               G_TYPE_STRING,          /* COLUMN_STRING_NAME */
+               G_TYPE_STRING,          /* COLUMN_STRING_HREF */
+               G_TYPE_STRING,          /* COLUMN_STRING_CONTENT_TYPE */
+               G_TYPE_STRING,          /* COLUMN_STRING_DESCRIPTION */
+               GDK_TYPE_PIXBUF);       /* COLUMN_PIXBUF_ICON */
+
+       widget = gtk_tree_view_new_with_model (GTK_TREE_MODEL (list_store));
+       g_object_set (G_OBJECT (widget),
+               "hexpand", TRUE,
+               "halign", GTK_ALIGN_FILL,
+               "vexpand", TRUE,
+               "valign", GTK_ALIGN_FILL,
+               "visible", TRUE,
+               NULL);
+       g_object_unref (list_store);
+       gtk_container_add (GTK_CONTAINER (scrolled_window), widget);
+
+       tree_view = GTK_TREE_VIEW (widget);
+
+       column = gtk_tree_view_column_new ();
+       gtk_tree_view_column_set_title (column, _("Name"));
+       gtk_tree_view_column_set_expand (column, TRUE);
+
+       cell_renderer = gtk_cell_renderer_pixbuf_new ();
+       gtk_tree_view_column_pack_start (column, cell_renderer, FALSE);
+
+       gtk_tree_view_column_set_attributes (column, cell_renderer,
+               "pixbuf", COLUMN_PIXBUF_ICON,
+               NULL);
+
+       cell_renderer = gtk_cell_renderer_text_new ();
+       g_object_set (cell_renderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
+       gtk_tree_view_column_pack_start (column, cell_renderer, FALSE);
+
+       gtk_tree_view_column_set_attributes (column, cell_renderer,
+               "markup", COLUMN_STRING_DESCRIPTION,
+               NULL);
+
+       gtk_tree_view_append_column (tree_view, column);
+
+       column = gtk_tree_view_column_new ();
+       gtk_tree_view_column_set_title (column, _("Content"));
+       gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_FIXED);
+       gtk_tree_view_column_set_fixed_width (column, 120);
+       gtk_tree_view_column_set_expand (column, FALSE);
+
+       cell_renderer = gtk_cell_renderer_text_new ();
+       g_object_set (cell_renderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
+       gtk_tree_view_column_pack_start (column, cell_renderer, FALSE);
+
+       gtk_tree_view_column_set_attributes (column, cell_renderer,
+               "text", COLUMN_STRING_CONTENT_TYPE,
+               NULL);
+
+       gtk_tree_view_append_column (tree_view, column);
+
+       g_signal_connect_object (tree_view, "map",
+               G_CALLBACK (e_rss_preferences_map_cb), store_summary, 0);
+
+       g_signal_connect_object (store_summary, "feed-changed",
+               G_CALLBACK (e_rss_preferences_feed_changed_cb), tree_view, 0);
+
+       selection = gtk_tree_view_get_selection (tree_view);
+       gtk_tree_selection_set_mode (selection, GTK_SELECTION_SINGLE);
+
+       button_box = gtk_button_box_new (GTK_ORIENTATION_VERTICAL);
+       g_object_set (G_OBJECT (button_box),
+               "layout-style", GTK_BUTTONBOX_START,
+               "margin-start", 6,
+               "spacing", 4,
+               NULL);
+
+       widget = e_dialog_button_new_with_icon ("list-add", _("_Add"));
+       gtk_container_add (GTK_CONTAINER (button_box), widget);
+
+       g_signal_connect_object (widget, "clicked",
+               G_CALLBACK (e_rss_preferences_add_clicked_cb), tree_view, 0);
+
+       widget = e_dialog_button_new_with_icon (NULL, _("_Edit"));
+       gtk_widget_set_sensitive (widget, FALSE);
+       gtk_container_add (GTK_CONTAINER (button_box), widget);
+
+       g_signal_connect_object (widget, "clicked",
+               G_CALLBACK (e_rss_preferences_edit_clicked_cb), tree_view, 0);
+
+       g_signal_connect_object (selection, "changed",
+               G_CALLBACK (e_rss_pereferences_selection_changed_cb), widget, 0);
+
+       g_signal_connect_object (tree_view, "row-activated",
+               G_CALLBACK (e_rss_preferences_row_activated_cb), widget, 0);
+
+       widget = e_dialog_button_new_with_icon ("edit-delete", _("_Remove"));
+       gtk_widget_set_sensitive (widget, FALSE);
+       gtk_container_add (GTK_CONTAINER (button_box), widget);
+
+       g_signal_connect_object (widget, "clicked",
+               G_CALLBACK (e_rss_preferences_remove_clicked_cb), tree_view, 0);
+
+       g_signal_connect_object (selection, "changed",
+               G_CALLBACK (e_rss_pereferences_selection_changed_cb), widget, 0);
+
+       gtk_grid_attach (grid, scrolled_window, 0, row, 1, 1);
+       gtk_grid_attach (grid, button_box, 1, row, 1, 1);
+       row++;
+
+       pango_attr_list_unref (bold);
+
+       widget = GTK_WIDGET (grid);
+       gtk_widget_show_all (widget);
+
+       g_clear_object (&store_summary);
+       g_clear_object (&service);
+       g_clear_object (&settings);
+
+       return widget;
+}
+
+void
+e_rss_preferences_init (EShell *shell)
+{
+       GtkWidget *preferences_window;
+       CamelService *service;
+
+       g_return_if_fail (E_IS_SHELL (shell));
+
+       service = e_rss_preferences_ref_store (shell);
+       if (!service)
+               return;
+
+       g_clear_object (&service);
+
+       preferences_window = e_shell_get_preferences_window (shell);
+
+       e_preferences_window_add_page (
+               E_PREFERENCES_WINDOW (preferences_window),
+               "e-rss-page",
+               "rss",
+               _("News and Blogs"),
+               NULL,
+               e_rss_preferences_new,
+               800);
+}
diff --git a/src/modules/rss/evolution/e-rss-preferences.h b/src/modules/rss/evolution/e-rss-preferences.h
new file mode 100644
index 0000000000..bce32615dd
--- /dev/null
+++ b/src/modules/rss/evolution/e-rss-preferences.h
@@ -0,0 +1,20 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef E_RSS_PREFERENCES_H
+#define E_RSS_PREFERENCES_H
+
+#include <glib.h>
+
+#include <shell/e-shell.h>
+
+G_BEGIN_DECLS
+
+void           e_rss_preferences_init  (EShell *shell);
+
+G_END_DECLS
+
+#endif /* E_RSS_PREFERENCES_H */
diff --git a/src/modules/rss/evolution/e-rss-shell-extension.c 
b/src/modules/rss/evolution/e-rss-shell-extension.c
new file mode 100644
index 0000000000..9e83c2b230
--- /dev/null
+++ b/src/modules/rss/evolution/e-rss-shell-extension.c
@@ -0,0 +1,132 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-config.h"
+
+#include <glib-object.h>
+#include <glib/gi18n-lib.h>
+
+#include "shell/e-shell.h"
+
+#include "e-rss-preferences.h"
+
+#include "module-rss.h"
+
+#define E_TYPE_RSS_SHELL_EXTENSION (e_rss_shell_extension_get_type ())
+
+GType e_rss_shell_extension_get_type (void);
+
+typedef struct _ERssShellExtension {
+       EExtension parent;
+} ERssShellExtension;
+
+typedef struct _ERssShellExtensionClass {
+       EExtensionClass parent_class;
+} ERssShellExtensionClass;
+
+G_DEFINE_DYNAMIC_TYPE (ERssShellExtension, e_rss_shell_extension, E_TYPE_EXTENSION)
+
+static void
+e_rss_ensure_esource (EShell *shell)
+{
+       ESourceRegistry *registry;
+       ESource *rss_source;
+
+       registry = e_shell_get_registry (shell);
+       rss_source = e_source_registry_ref_source (registry, "rss");
+
+       if (!rss_source) {
+               GError *error = NULL;
+
+               rss_source = e_source_new_with_uid ("rss", NULL, &error);
+
+               if (rss_source) {
+                       ESourceMailAccount *mail_account;
+
+                       mail_account = e_source_get_extension (rss_source, E_SOURCE_EXTENSION_MAIL_ACCOUNT);
+                       e_source_mail_account_set_builtin (mail_account, TRUE);
+                       e_source_backend_set_backend_name (E_SOURCE_BACKEND (mail_account), "rss");
+               } else {
+                       g_warning ("Failed to create RSS source: %s", error ? error->message : "Unknown 
error");
+               }
+
+               g_clear_error (&error);
+       }
+
+       if (rss_source) {
+               GError *error = NULL;
+
+               e_source_set_display_name (rss_source, _("News and Blogs"));
+
+               if (!e_source_registry_commit_source_sync (registry, rss_source, NULL, &error))
+                       g_warning ("Failed to commit RSS source: %s", error ? error->message : "Unknown 
error");
+
+               g_clear_error (&error);
+       }
+
+       g_clear_object (&rss_source);
+}
+
+static gboolean
+init_preferences_idle_cb (gpointer user_data)
+{
+       EShell *shell = g_weak_ref_get (user_data);
+
+       if (shell)
+               e_rss_preferences_init (shell);
+
+       g_clear_object (&shell);
+
+       return G_SOURCE_REMOVE;
+}
+
+static void
+e_rss_shell_ready_to_start_cb (EShell *shell)
+{
+       e_rss_ensure_esource (shell);
+
+       g_idle_add_full (G_PRIORITY_LOW, init_preferences_idle_cb,
+               e_weak_ref_new (shell), (GDestroyNotify) e_weak_ref_free);
+}
+
+static void
+e_rss_shell_extension_constructed (GObject *object)
+{
+       /* Chain up to parent's method */
+       G_OBJECT_CLASS (e_rss_shell_extension_parent_class)->constructed (object);
+
+       g_signal_connect_object (e_extension_get_extensible (E_EXTENSION (object)), "event::ready-to-start",
+               G_CALLBACK (e_rss_shell_ready_to_start_cb), NULL, 0);
+}
+
+static void
+e_rss_shell_extension_class_init (ERssShellExtensionClass *klass)
+{
+       GObjectClass *object_class;
+       EExtensionClass *extension_class;
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->constructed = e_rss_shell_extension_constructed;
+
+       extension_class = E_EXTENSION_CLASS (klass);
+       extension_class->extensible_type = E_TYPE_SHELL;
+}
+
+static void
+e_rss_shell_extension_class_finalize (ERssShellExtensionClass *klass)
+{
+}
+
+static void
+e_rss_shell_extension_init (ERssShellExtension *extension)
+{
+}
+
+void
+e_rss_shell_extension_type_register (GTypeModule *type_module)
+{
+       e_rss_shell_extension_register_type (type_module);
+}
diff --git a/src/modules/rss/evolution/e-rss-shell-view-extension.c 
b/src/modules/rss/evolution/e-rss-shell-view-extension.c
new file mode 100644
index 0000000000..ecb5ef6cdf
--- /dev/null
+++ b/src/modules/rss/evolution/e-rss-shell-view-extension.c
@@ -0,0 +1,277 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-config.h"
+
+#include <glib-object.h>
+#include <glib/gi18n-lib.h>
+
+#include "mail/e-mail-reader-utils.h"
+#include "mail/em-folder-tree.h"
+#include "shell/e-shell-content.h"
+#include "shell/e-shell-view.h"
+#include "shell/e-shell-window.h"
+
+#include "../camel-rss-store-summary.h"
+
+#include "module-rss.h"
+
+static const gchar *mail_ui_def =
+       "<popup name=\"mail-folder-popup\">\n"
+       "  <placeholder name=\"mail-folder-popup-actions\">\n"
+       "    <menuitem action=\"e-rss-mail-folder-reload-action\"/>\n"
+       "  </placeholder>\n"
+       "</popup>\n";
+
+#define E_TYPE_RSS_SHELL_VIEW_EXTENSION (e_rss_shell_view_extension_get_type ())
+
+GType e_rss_shell_view_extension_get_type (void);
+
+typedef struct _ERssShellViewExtension {
+       EExtension parent;
+       guint current_ui_id;
+       gboolean actions_added;
+} ERssShellViewExtension;
+
+typedef struct _ERssShellViewExtensionClass {
+       EExtensionClass parent_class;
+} ERssShellViewExtensionClass;
+
+G_DEFINE_DYNAMIC_TYPE (ERssShellViewExtension, e_rss_shell_view_extension, E_TYPE_EXTENSION)
+
+static gboolean
+e_rss_check_rss_folder_selected (EShellView *shell_view,
+                                CamelStore **pstore,
+                                gchar **pfolder_path)
+{
+       EShellSidebar *shell_sidebar;
+       EMFolderTree *folder_tree;
+       gchar *selected_path = NULL;
+       CamelStore *selected_store = NULL;
+       gboolean is_rss_folder = FALSE;
+
+       shell_sidebar = e_shell_view_get_shell_sidebar (shell_view);
+       g_object_get (shell_sidebar, "folder-tree", &folder_tree, NULL);
+       if (em_folder_tree_get_selected (folder_tree, &selected_store, &selected_path)) {
+               if (selected_store) {
+                       is_rss_folder = g_strcmp0 (camel_service_get_uid (CAMEL_SERVICE (selected_store)), 
"rss") == 0 &&
+                                       g_strcmp0 (selected_path, CAMEL_VJUNK_NAME) != 0 &&
+                                       g_strcmp0 (selected_path, CAMEL_VTRASH_NAME) != 0;
+
+                       if (is_rss_folder) {
+                               if (pstore)
+                                       *pstore = g_object_ref (selected_store);
+
+                               if (pfolder_path)
+                                       *pfolder_path = selected_path;
+                               else
+                                       g_free (selected_path);
+
+                               selected_path = NULL;
+                       }
+
+                       g_object_unref (selected_store);
+               }
+
+               g_free (selected_path);
+       }
+
+       g_object_unref (folder_tree);
+
+       return is_rss_folder;
+}
+
+static void
+e_rss_mail_folder_reload_got_folder_cb (GObject *source_object,
+                                       GAsyncResult *result,
+                                       gpointer user_data)
+{
+       EShellView *shell_view = user_data;
+       CamelFolder *folder;
+       GError *error = NULL;
+
+       folder = camel_store_get_folder_finish (CAMEL_STORE (source_object), result, &error);
+
+       if (folder) {
+               EShellContent *shell_content;
+
+               shell_content = e_shell_view_get_shell_content (shell_view);
+
+               e_mail_reader_refresh_folder (E_MAIL_READER (shell_content), folder);
+
+               g_object_unref (folder);
+       } else {
+               g_warning ("%s: Failed to get folder: %s", G_STRFUNC, error ? error->message : "Unknown 
error");
+       }
+}
+
+static void
+action_rss_mail_folder_reload_cb (GtkAction *action,
+                                 EShellView *shell_view)
+{
+       CamelStore *store = NULL;
+       CamelRssStoreSummary *store_summary = NULL;
+       gchar *folder_path = NULL;
+
+       g_return_if_fail (E_IS_SHELL_VIEW (shell_view));
+
+       if (!e_rss_check_rss_folder_selected (shell_view, &store, &folder_path))
+               return;
+
+       g_object_get (store, "summary", &store_summary, NULL);
+
+       camel_rss_store_summary_set_last_updated (store_summary, folder_path, 0);
+
+       camel_store_get_folder (store, folder_path, CAMEL_STORE_FOLDER_NONE, G_PRIORITY_DEFAULT, NULL,
+               e_rss_mail_folder_reload_got_folder_cb, shell_view);
+
+       g_clear_object (&store_summary);
+       g_clear_object (&store);
+       g_free (folder_path);
+}
+
+static void
+e_rss_shell_view_update_actions_cb (EShellView *shell_view,
+                                   GtkActionEntry *entries)
+{
+       CamelStore *store = NULL;
+       EShellWindow *shell_window;
+       GtkActionGroup *action_group;
+       GtkAction *action;
+       GtkUIManager *ui_manager;
+       gboolean is_rss_folder = FALSE;
+
+       is_rss_folder = e_rss_check_rss_folder_selected (shell_view, &store, NULL);
+
+       shell_window = e_shell_view_get_shell_window (shell_view);
+       ui_manager = e_shell_window_get_ui_manager (shell_window);
+       action_group = e_lookup_action_group (ui_manager, "mail");
+       action = gtk_action_group_get_action (action_group, "e-rss-mail-folder-reload-action");
+
+       if (action) {
+               gtk_action_set_visible (action, is_rss_folder);
+
+               if (store) {
+                       CamelSession *session;
+
+                       session = camel_service_ref_session (CAMEL_SERVICE (store));
+                       gtk_action_set_sensitive (action, session && camel_session_get_online (session));
+                       g_clear_object (&session);
+               } else {
+                       gtk_action_set_sensitive (action, FALSE);
+               }
+       }
+
+       g_clear_object (&store);
+}
+
+static void
+e_rss_shell_view_toggled_cb (EShellView *shell_view,
+                            ERssShellViewExtension *extension)
+{
+       EShellViewClass *shell_view_class;
+       EShellWindow *shell_window;
+       GtkUIManager *ui_manager;
+       gboolean is_active, need_update;
+       GError *error = NULL;
+
+       g_return_if_fail (E_IS_SHELL_VIEW (shell_view));
+       g_return_if_fail (extension != NULL);
+
+       shell_view_class = E_SHELL_VIEW_GET_CLASS (shell_view);
+       g_return_if_fail (shell_view_class != NULL);
+
+       shell_window = e_shell_view_get_shell_window (shell_view);
+       ui_manager = e_shell_window_get_ui_manager (shell_window);
+
+       need_update = extension->current_ui_id != 0;
+
+       if (extension->current_ui_id) {
+               gtk_ui_manager_remove_ui (ui_manager, extension->current_ui_id);
+               extension->current_ui_id = 0;
+       }
+
+       is_active = e_shell_view_is_active (shell_view);
+
+       if (!is_active || g_strcmp0 (shell_view_class->ui_manager_id, "org.gnome.evolution.mail") != 0) {
+               if (need_update)
+                       gtk_ui_manager_ensure_update (ui_manager);
+
+               return;
+       }
+
+       if (!extension->actions_added) {
+               GtkActionEntry mail_folder_context_entries[] = {
+                       { "e-rss-mail-folder-reload-action",
+                         NULL,
+                         N_("Re_load feed articles"),
+                         NULL,
+                         N_("Reload all feed articles from the server, updating existing and adding any 
missing"),
+                         G_CALLBACK (action_rss_mail_folder_reload_cb) }
+               };
+
+               GtkActionGroup *action_group;
+
+               action_group = e_shell_window_get_action_group (shell_window, "mail");
+
+               e_action_group_add_actions_localized (
+                       action_group, GETTEXT_PACKAGE,
+                       mail_folder_context_entries, G_N_ELEMENTS (mail_folder_context_entries), shell_view);
+
+               g_signal_connect (shell_view, "update-actions",
+                       G_CALLBACK (e_rss_shell_view_update_actions_cb), NULL);
+
+               extension->actions_added = TRUE;
+       }
+
+       extension->current_ui_id = gtk_ui_manager_add_ui_from_string (ui_manager, mail_ui_def, -1, &error);
+
+       if (error) {
+               g_warning ("%s: Failed to add ui definition: %s", G_STRFUNC, error->message);
+               g_error_free (error);
+       }
+
+       gtk_ui_manager_ensure_update (ui_manager);
+}
+
+static void
+e_rss_shell_view_extension_constructed (GObject *object)
+{
+       /* Chain up to parent's method */
+       G_OBJECT_CLASS (e_rss_shell_view_extension_parent_class)->constructed (object);
+
+       g_signal_connect_object (e_extension_get_extensible (E_EXTENSION (object)), "toggled",
+               G_CALLBACK (e_rss_shell_view_toggled_cb), object, 0);
+}
+
+static void
+e_rss_shell_view_extension_class_init (ERssShellViewExtensionClass *klass)
+{
+       GObjectClass *object_class;
+       EExtensionClass *extension_class;
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->constructed = e_rss_shell_view_extension_constructed;
+
+       extension_class = E_EXTENSION_CLASS (klass);
+       extension_class->extensible_type = E_TYPE_SHELL_VIEW;
+}
+
+static void
+e_rss_shell_view_extension_class_finalize (ERssShellViewExtensionClass *klass)
+{
+}
+
+static void
+e_rss_shell_view_extension_init (ERssShellViewExtension *extension)
+{
+}
+
+void
+e_rss_shell_view_extension_type_register (GTypeModule *type_module)
+{
+       e_rss_shell_view_extension_register_type (type_module);
+}
diff --git a/src/modules/rss/evolution/module-rss.c b/src/modules/rss/evolution/module-rss.c
new file mode 100644
index 0000000000..aa1c1a432d
--- /dev/null
+++ b/src/modules/rss/evolution/module-rss.c
@@ -0,0 +1,28 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "evolution-config.h"
+
+#include <glib-object.h>
+#include <gmodule.h>
+
+#include "module-rss.h"
+
+void e_module_load (GTypeModule *type_module);
+void e_module_unload (GTypeModule *type_module);
+
+G_MODULE_EXPORT void
+e_module_load (GTypeModule *type_module)
+{
+       e_rss_shell_extension_type_register (type_module);
+       e_rss_shell_view_extension_type_register (type_module);
+       e_rss_folder_tree_model_extension_type_register (type_module);
+}
+
+G_MODULE_EXPORT void
+e_module_unload (GTypeModule *type_module)
+{
+}
diff --git a/src/modules/rss/evolution/module-rss.h b/src/modules/rss/evolution/module-rss.h
new file mode 100644
index 0000000000..18434fd947
--- /dev/null
+++ b/src/modules/rss/evolution/module-rss.h
@@ -0,0 +1,21 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * SPDX-FileCopyrightText: (C) 2022 Red Hat (www.redhat.com)
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifndef MODULE_RSS_H
+#define MODULE_RSS_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+void           e_rss_shell_extension_type_register     (GTypeModule *type_module);
+void           e_rss_shell_view_extension_type_register(GTypeModule *type_module);
+void           e_rss_folder_tree_model_extension_type_register
+                                                       (GTypeModule *type_module);
+
+G_END_DECLS
+
+#endif /* MODULE_RSS_H */


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]