[evolution] Implement News & Blogs (RSS) reader
- From: Milan Crha <mcrha src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [evolution] Implement News & Blogs (RSS) reader
- Date: Tue, 5 Jul 2022 19:25:11 +0000 (UTC)
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]