RSSyl: do not look at modified time when matching deleted items
[claws.git] / src / plugins / rssyl / rssyl_deleted.c
1 /*
2  * Claws-Mail-- a GTK+ based, lightweight, and fast e-mail client
3  * Copyright (C) 1999-2004 Hiroyuki Yamamoto
4  * This file (C) 2005 Andrej Kacian <andrej@kacian.sk>
5  *
6  * - handling of info about user-deleted feed items
7  *
8  * This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation; either version 2 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program; if not, write to the Free Software
20  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
21  */
22
23 #ifdef HAVE_CONFIG_H
24 #  include "config.h"
25 #endif
26
27 /* Global includes */
28 #include <glib.h>
29 #include <string.h>
30
31 /* Claws Mail includes */
32 #include <codeconv.h>
33 #include <common/utils.h>
34
35 /* Local includes */
36 #include "rssyl.h"
37 #include "parse822.h"
38 #include "strutils.h"
39
40 static RDeletedItem *_new_deleted_item()
41 {
42         RDeletedItem *ditem = g_new0(RDeletedItem, 1);
43
44         ditem->id = NULL;
45         ditem->title = NULL;
46         ditem->date_published = -1;
47
48         return ditem;
49 }
50
51 static void _free_deleted_item(gpointer d, gpointer user_data)
52 {
53         RDeletedItem *ditem = (RDeletedItem *)d;
54
55         if (ditem == NULL)
56                 return;
57
58         g_free(ditem->id);
59         g_free(ditem->title);
60         g_free(ditem);
61 }
62
63 void rssyl_deleted_free(GSList *deleted_items)
64 {
65         if (deleted_items != NULL) {
66                 debug_print("RSSyl: releasing list of deleted items\n");
67                 g_slist_foreach(deleted_items, _free_deleted_item, NULL);
68                 g_slist_free(deleted_items);
69                 deleted_items = NULL;
70         }
71 }
72
73 static gchar * _deleted_file_path(RFolderItem *ritem)
74 {
75         gchar *itempath, *deleted_file;
76
77         itempath = folder_item_get_path(&ritem->item);
78         deleted_file = g_strconcat(itempath, G_DIR_SEPARATOR_S, RSSYL_DELETED_FILE, NULL);
79         g_free(itempath);
80
81         return deleted_file;
82 }
83
84 /***************************************************************/
85 GSList *rssyl_deleted_update(RFolderItem *ritem)
86 {
87         gchar *deleted_file, *contents, **lines, **line;
88         GError *error = NULL;
89         guint i = 0;
90         RDeletedItem *ditem = NULL;
91         GSList *deleted_items = NULL;
92
93         g_return_val_if_fail(ritem != NULL, NULL);
94
95         deleted_file = _deleted_file_path(ritem);
96
97         debug_print("RSSyl: getting list of deleted items from '%s'\n", deleted_file);
98
99         if (!g_file_test(deleted_file, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
100                 debug_print("RSSyl: '%s' doesn't exist, ignoring\n", deleted_file);
101                 g_free(deleted_file);
102                 return NULL;
103         }
104
105         g_file_get_contents(deleted_file, &contents, NULL, &error);
106
107         if (error) {
108                 g_warning("GError: '%s'", error->message);
109                 g_error_free(error);
110         }
111
112         if (contents != NULL) {
113                 lines = strsplit_no_copy(contents, '\n');
114         } else {
115                 g_warning("Couldn't read '%s', ignoring", deleted_file);
116                 g_free(deleted_file);
117                 return NULL;
118         }
119
120         g_free(deleted_file);
121
122         while (lines[i]) {
123                 line = g_strsplit(lines[i], ": ", 2);
124                 if (line[0] && line[1] && strlen(line[0]) && strlen(line[1])) {
125                         if (!strcmp(line[0], "ID")) {
126                                 ditem = _new_deleted_item();
127                                 ditem->id = g_strdup(line[1]);
128                         } else if (ditem != NULL && !strcmp(line[0], "TITLE")) {
129                                 ditem->title = g_strdup(line[1]);
130                         } else if (ditem != NULL && !strcmp(line[0], "DPUB")) {
131                                 ditem->date_published = atoi(line[1]);
132                                 deleted_items = g_slist_prepend(deleted_items, ditem);
133                                 ditem = NULL;
134                         }
135                 }
136
137                 g_strfreev(line);
138                 i++;
139         }
140
141         g_free(lines);
142         g_free(contents);
143
144         debug_print("RSSyl: got %d deleted items\n", g_slist_length(deleted_items));
145         return deleted_items;
146 }
147
148 static void _store_one_deleted_item(gpointer data, gpointer user_data)
149 {
150         RDeletedItem *ditem = (RDeletedItem *)data;
151         FILE *f = (FILE *)user_data;
152         gboolean err = FALSE;
153
154         if (ditem == NULL || ditem->id == NULL)
155                 return;
156
157         err |= (fprintf(f,
158                         "ID: %s\n"
159                         "TITLE: %s\n"
160                         "DPUB: %lld\n",
161                         ditem->id, ditem->title,
162                         (long long)ditem->date_published) < 0);
163
164         if (err)
165                 debug_print("RSSyl: Error during writing deletion file.\n");
166 }
167
168 static void rssyl_deleted_store_internal(GSList *deleted_items, const gchar *deleted_file)
169 {
170         FILE *f;
171
172         if (g_file_test(deleted_file, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
173                 if (g_remove(deleted_file) != 0) {
174                         debug_print("RSSyl: Oops, couldn't delete '%s', bailing out\n",
175                                         deleted_file);
176                         return;
177                 }
178         }
179
180         if (g_slist_length(deleted_items) == 0)
181                 return;
182
183         if ((f = g_fopen(deleted_file, "w")) == NULL) {
184                 debug_print("RSSyl: Couldn't open '%s', bailing out.\n", deleted_file);
185                 return;
186         }
187
188         g_slist_foreach(deleted_items, (GFunc)_store_one_deleted_item,
189                         (gpointer)f);
190
191         fclose(f);
192         debug_print("RSSyl: written and closed deletion file\n");
193 }
194
195 void rssyl_deleted_store(RFolderItem *ritem)
196 {
197         gchar *path;
198
199         g_return_if_fail(ritem != NULL);
200
201         path = _deleted_file_path(ritem);
202         rssyl_deleted_store_internal(ritem->deleted_items, path);
203         g_free(path);
204 }
205
206
207 /* Creates a FeedItem from a message file and uses the data to add a item
208  * to the list of deleted stuff. */
209 void rssyl_deleted_add(RFolderItem *ritem, gchar *path)
210 {
211         FeedItem *fitem = NULL;
212         RDeletedItem *ditem = NULL;
213         GSList *deleted_items = rssyl_deleted_update(ritem);
214         gchar *deleted_file = NULL;
215
216         if (!(fitem = rssyl_parse_folder_item_file(path)))
217                 return;
218
219         ditem = _new_deleted_item();
220         ditem->id = g_strdup(feed_item_get_id(fitem));
221         ditem->title = conv_unmime_header(feed_item_get_title(fitem),
222                         CS_UTF_8, FALSE);
223         ditem->date_published = feed_item_get_date_published(fitem);
224
225         deleted_items = g_slist_prepend(deleted_items, ditem);
226
227         deleted_file = _deleted_file_path(ritem);
228         rssyl_deleted_store_internal(deleted_items, deleted_file);
229         g_free(deleted_file);
230
231         rssyl_deleted_free(deleted_items);
232
233         RFeedCtx *ctx = (RFeedCtx *)fitem->data;
234         g_free(ctx->path);
235         feed_item_free(fitem);
236 }
237
238 static gint _rssyl_deleted_check_func(gconstpointer a, gconstpointer b)
239 {
240         RDeletedItem *ditem = (RDeletedItem *)a;
241         FeedItem *fitem = (FeedItem *)b;
242         gchar *id;
243         gboolean id_match = FALSE;
244         gboolean title_match = FALSE;
245         gboolean pubdate_match = FALSE;
246
247         g_return_val_if_fail(ditem != NULL, -10);
248         g_return_val_if_fail(fitem != NULL, -20);
249
250         /* Following must match:
251          * ID, or if there is no ID, the URL, since that's
252          * what would have been stored in .deleted instead
253          * of ID... */
254         if ((id = feed_item_get_id(fitem)) == NULL)
255                 id = feed_item_get_url(fitem);
256
257         if (ditem->id && id &&
258                         !strcmp(ditem->id, id))
259                 id_match = TRUE;
260
261         /* title, ... */
262         if (ditem->title && feed_item_get_title(fitem) &&
263                         !strcmp(ditem->title, feed_item_get_title(fitem)))
264                 title_match = TRUE;
265
266         /* ...and time of publishing */
267         if (ditem->date_published == -1 ||
268                         ditem->date_published == feed_item_get_date_published(fitem))
269                 pubdate_match = TRUE;
270
271         /* if all three match, it's the same item */
272         if (id_match && title_match && pubdate_match) {
273                 return 0;
274         }
275
276         /* not our item */
277         return -1;
278 }
279
280 /* Returns TRUE if fitem is found among the deleted stuff. */
281 gboolean rssyl_deleted_check(GSList *deleted_items, FeedItem *fitem)
282 {
283         if (g_slist_find_custom(deleted_items, (gconstpointer)fitem,
284                                 _rssyl_deleted_check_func) != NULL)
285                 return TRUE;
286
287         return FALSE;
288 }
289
290 /******** Expiring ********/
291 struct _RDelExpireCtx {
292         RDeletedItem *ditem;
293         gboolean delete;
294 };
295
296 typedef struct _RDelExpireCtx RDelExpireCtx;
297
298 static void _rssyl_deleted_expire_func_f(gpointer data, gpointer user_data)
299 {
300         FeedItem *fitem = (FeedItem *)data;
301         RDelExpireCtx *ctx = (RDelExpireCtx *)user_data;
302         gchar *id;
303         gboolean id_match = FALSE;
304         gboolean title_match = FALSE;
305         gboolean pubdate_match = FALSE;
306
307         /* Following must match:
308          * ID, or if there is no ID, the URL, since that's
309          * what would have been stored in .deleted instead
310          * of ID... */
311         if ((id = feed_item_get_id(fitem)) == NULL)
312                 id = feed_item_get_url(fitem);
313
314         if (ctx->ditem->id && id &&
315                         !strcmp(ctx->ditem->id, id))
316                 id_match = TRUE;
317
318         /* title, ... */
319         if (ctx->ditem->title && feed_item_get_title(fitem) &&
320                         !strcmp(ctx->ditem->title, feed_item_get_title(fitem)))
321                 title_match = TRUE;
322
323         /* time of publishing, if set... */
324         if (ctx->ditem->date_published == -1 ||
325                         ctx->ditem->date_published == feed_item_get_date_published(fitem))
326                 pubdate_match = TRUE;
327
328         /* if it's our item, set to NOT delete, since it's obviously
329          * still in the feed */
330         if (id_match && title_match && pubdate_match)
331                 ctx->delete = FALSE;
332 }
333
334 /* Checks each item in deleted items list against feed and removes it if
335  * it is not found there anymore. */
336 void rssyl_deleted_expire(RFolderItem *ritem, Feed *feed)
337 {
338         GSList *d = NULL, *d2;
339         RDelExpireCtx *ctx = NULL;
340         RDeletedItem *ditem;
341
342         g_return_if_fail(ritem != NULL);
343         g_return_if_fail(feed != NULL);
344
345         ritem->deleted_items = rssyl_deleted_update(ritem);
346
347         /* Iterate over all items in the list */
348         d = ritem->deleted_items;
349         while (d) {
350                 ditem = (RDeletedItem *)d->data;
351                 ctx = g_new0(RDelExpireCtx, 1);
352                 ctx->ditem = ditem;
353                 ctx->delete = TRUE;
354
355                 /* Adjust ctx->delete accordingly */
356                 feed_foreach_item(feed, _rssyl_deleted_expire_func_f, (gpointer)ctx);
357
358                 /* Remove the item if necessary */
359                 if (ctx->delete) {
360                         debug_print("RSSyl: (DELETED) removing '%s' from list\n", ditem->title);
361                         d2 = d->next;
362                         ritem->deleted_items = g_slist_remove_link(ritem->deleted_items, d);
363                         d = d2;
364                         continue;
365                 } else {
366                         d = d->next;
367                 }
368
369                 g_free(ctx);
370         }
371
372         /* Write the new list to disk */
373         rssyl_deleted_store(ritem);
374
375         /* And clean up after myself */
376         rssyl_deleted_free(ritem->deleted_items);
377 }