Bug #728973 - [IMAPX] Recover after store summary version mismatch
[platform/upstream/evolution-data-server.git] / camel / camel-store-summary.c
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
4  *
5  * Authors: Michael Zucchi <notzed@ximian.com>
6  *
7  * This library is free software you can redistribute it and/or modify it
8  * under the terms of the GNU Lesser General Public License as published by
9  * the Free Software Foundation.
10  *
11  * This library is distributed in the hope that it will be useful, but
12  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13  * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with this library; if not, see <http://www.gnu.org/licenses/>.
18  */
19
20 #ifdef HAVE_CONFIG_H
21 #include <config.h>
22 #endif
23
24 #include <ctype.h>
25 #include <errno.h>
26 #include <fcntl.h>
27 #include <stdlib.h>
28 #include <string.h>
29 #include <unistd.h>
30 #include <sys/stat.h>
31 #include <sys/types.h>
32
33 #include <glib/gstdio.h>
34
35 #include "camel-file-utils.h"
36 #include "camel-store-summary.h"
37 #include "camel-folder-summary.h"
38 #include "camel-url.h"
39 #include "camel-win32.h"
40
41 #define d(x)
42 #define io(x)                   /* io debug */
43
44 /* possible versions, for versioning changes */
45 #define CAMEL_STORE_SUMMARY_VERSION_0 (1)
46 #define CAMEL_STORE_SUMMARY_VERSION_2 (2)
47
48 /* current version */
49 #define CAMEL_STORE_SUMMARY_VERSION (2)
50
51 #define CAMEL_STORE_SUMMARY_GET_PRIVATE(obj) \
52         (G_TYPE_INSTANCE_GET_PRIVATE \
53         ((obj), CAMEL_TYPE_STORE_SUMMARY, CamelStoreSummaryPrivate))
54
55 struct _CamelStoreSummaryPrivate {
56         GRecMutex summary_lock; /* for the summary hashtable/array */
57         GRecMutex io_lock;      /* load/save lock, for access to saved_count, etc */
58
59         gboolean dirty;         /* summary has unsaved changes */
60
61         gchar *summary_path;
62
63         /* header info */
64         guint32 version;        /* version of base part of file */
65         guint32 count;          /* how many were saved/loaded */
66         time_t time;            /* timestamp for this summary (for implementors to use) */
67
68         GHashTable *folder_summaries; /* CamelFolderSummary->path; doesn't add reference to CamelFolderSummary */
69
70         guint scheduled_save_id;
71 };
72
73 G_DEFINE_TYPE (CamelStoreSummary, camel_store_summary, G_TYPE_OBJECT)
74
75 static void
76 store_summary_finalize (GObject *object)
77 {
78         CamelStoreSummary *summary = CAMEL_STORE_SUMMARY (object);
79         guint ii;
80
81         for (ii = 0; ii < summary->folders->len; ii++) {
82                 CamelStoreInfo *info;
83
84                 info = g_ptr_array_index (summary->folders, ii);
85                 camel_store_summary_info_unref (summary, info);
86         }
87
88         g_ptr_array_free (summary->folders, TRUE);
89         g_hash_table_destroy (summary->folders_path);
90         g_hash_table_destroy (summary->priv->folder_summaries);
91
92         g_free (summary->priv->summary_path);
93
94         g_rec_mutex_clear (&summary->priv->summary_lock);
95         g_rec_mutex_clear (&summary->priv->io_lock);
96
97         /* Chain up to parent's finalize() method. */
98         G_OBJECT_CLASS (camel_store_summary_parent_class)->finalize (object);
99 }
100
101 static void
102 store_summary_dispose (GObject *object)
103 {
104         CamelStoreSummary *summary = CAMEL_STORE_SUMMARY (object);
105
106         g_rec_mutex_lock (&summary->priv->summary_lock);
107
108         if (summary->priv->scheduled_save_id != 0) {
109                 g_source_remove (summary->priv->scheduled_save_id);
110                 summary->priv->scheduled_save_id = 0;
111                 camel_store_summary_save (summary);
112         }
113
114         g_rec_mutex_unlock (&summary->priv->summary_lock);
115
116         G_OBJECT_CLASS (camel_store_summary_parent_class)->dispose (object);
117 }
118
119 static gint
120 store_summary_summary_header_load (CamelStoreSummary *summary,
121                                    FILE *in)
122 {
123         gint32 version, flags, count;
124         time_t time;
125
126         fseek (in, 0, SEEK_SET);
127
128         io (printf ("Loading header\n"));
129
130         /* XXX The flags value is legacy; not used for anything. */
131         if (camel_file_util_decode_fixed_int32 (in, &version) == -1
132             || camel_file_util_decode_fixed_int32 (in, &flags) == -1
133             || camel_file_util_decode_time_t (in, &time) == -1
134             || camel_file_util_decode_fixed_int32 (in, &count) == -1) {
135                 return -1;
136         }
137
138         summary->priv->time = time;
139         summary->priv->count = count;
140         summary->priv->version = version;
141
142         if (version < CAMEL_STORE_SUMMARY_VERSION_0) {
143                 g_warning ("Store summary header version too low");
144                 return -1;
145         }
146
147         return 0;
148 }
149
150 static gint
151 store_summary_summary_header_save (CamelStoreSummary *summary,
152                                    FILE *out)
153 {
154         fseek (out, 0, SEEK_SET);
155
156         io (printf ("Savining header\n"));
157
158         /* always write latest version */
159         camel_file_util_encode_fixed_int32 (out, CAMEL_STORE_SUMMARY_VERSION);
160         camel_file_util_encode_fixed_int32 (out, 0);  /* flags (unused) */
161         camel_file_util_encode_time_t (out, summary->priv->time);
162
163         return camel_file_util_encode_fixed_int32 (
164                 out, camel_store_summary_count (summary));
165 }
166
167 static CamelStoreInfo *
168 store_summary_store_info_new (CamelStoreSummary *summary,
169                               const gchar *path)
170 {
171         CamelStoreInfo *info;
172
173         info = camel_store_summary_info_new (summary);
174
175         info->path = g_strdup (path);
176         info->unread = CAMEL_STORE_INFO_FOLDER_UNKNOWN;
177         info->total = CAMEL_STORE_INFO_FOLDER_UNKNOWN;
178
179         return info;
180 }
181
182 static CamelStoreInfo *
183 store_summary_store_info_load (CamelStoreSummary *summary,
184                                FILE *in)
185 {
186         CamelStoreInfo *info;
187
188         info = camel_store_summary_info_new (summary);
189
190         io (printf ("Loading folder info\n"));
191
192         if (camel_file_util_decode_string (in, &info->path) == -1 ||
193             camel_file_util_decode_uint32 (in, &info->flags) == -1 ||
194             camel_file_util_decode_uint32 (in, &info->unread) == -1 ||
195             camel_file_util_decode_uint32 (in, &info->total) == -1) {
196                 camel_store_summary_info_unref (summary, info);
197
198                 return NULL;
199         }
200
201         if (!ferror (in))
202                 return info;
203
204         camel_store_summary_info_unref (summary, info);
205
206         return NULL;
207 }
208
209 static gint
210 store_summary_store_info_save (CamelStoreSummary *summary,
211                                FILE *out,
212                                CamelStoreInfo *info)
213 {
214         io (printf ("Saving folder info\n"));
215
216         if (camel_file_util_encode_string (out, camel_store_info_path (summary, info)) == -1 ||
217             camel_file_util_encode_uint32 (out, info->flags) == -1 ||
218             camel_file_util_encode_uint32 (out, info->unread) == -1 ||
219             camel_file_util_encode_uint32 (out, info->total) == -1)
220                 return -1;
221
222         return ferror (out);
223 }
224
225 static void
226 store_summary_store_info_free (CamelStoreSummary *summary,
227                                CamelStoreInfo *info)
228 {
229         CamelStoreSummaryClass *class;
230
231         class = CAMEL_STORE_SUMMARY_GET_CLASS (summary);
232
233         g_free (info->path);
234         g_slice_free1 (class->store_info_size, info);
235 }
236
237 static void
238 store_summary_store_info_set_string (CamelStoreSummary *summary,
239                                      CamelStoreInfo *info,
240                                      gint type,
241                                      const gchar *str)
242 {
243         switch (type) {
244         case CAMEL_STORE_INFO_PATH:
245                 g_hash_table_remove (summary->folders_path, (gchar *) camel_store_info_path (summary, info));
246                 g_free (info->path);
247                 info->path = g_strdup (str);
248                 g_hash_table_insert (summary->folders_path, (gchar *) camel_store_info_path (summary, info), info);
249                 summary->priv->dirty = TRUE;
250                 break;
251         }
252 }
253
254 static void
255 camel_store_summary_class_init (CamelStoreSummaryClass *class)
256 {
257         GObjectClass *object_class;
258
259         g_type_class_add_private (class, sizeof (CamelStoreSummaryPrivate));
260
261         object_class = G_OBJECT_CLASS (class);
262         object_class->dispose = store_summary_dispose;
263         object_class->finalize = store_summary_finalize;
264
265         class->store_info_size = sizeof (CamelStoreInfo);
266         class->summary_header_load = store_summary_summary_header_load;
267         class->summary_header_save = store_summary_summary_header_save;
268         class->store_info_new = store_summary_store_info_new;
269         class->store_info_load = store_summary_store_info_load;
270         class->store_info_save = store_summary_store_info_save;
271         class->store_info_free = store_summary_store_info_free;
272         class->store_info_set_string = store_summary_store_info_set_string;
273 }
274
275 static void
276 camel_store_summary_init (CamelStoreSummary *summary)
277 {
278         summary->priv = CAMEL_STORE_SUMMARY_GET_PRIVATE (summary);
279
280         summary->priv->version = CAMEL_STORE_SUMMARY_VERSION;
281
282         summary->folders = g_ptr_array_new ();
283         summary->folders_path = g_hash_table_new (g_str_hash, g_str_equal);
284         summary->priv->folder_summaries = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, g_free);
285         summary->priv->scheduled_save_id = 0;
286
287         g_rec_mutex_init (&summary->priv->summary_lock);
288         g_rec_mutex_init (&summary->priv->io_lock);
289 }
290
291 /**
292  * camel_store_summary_new:
293  *
294  * Create a new #CamelStoreSummary object.
295  *
296  * Returns: a new #CamelStoreSummary object
297  **/
298 CamelStoreSummary *
299 camel_store_summary_new (void)
300 {
301         return g_object_new (CAMEL_TYPE_STORE_SUMMARY, NULL);
302 }
303
304 /**
305  * camel_store_summary_set_filename:
306  * @summary: a #CamelStoreSummary
307  * @filename: a filename
308  *
309  * Set the filename where the summary will be loaded to/saved from.
310  **/
311 void
312 camel_store_summary_set_filename (CamelStoreSummary *summary,
313                                   const gchar *name)
314 {
315         g_return_if_fail (CAMEL_IS_STORE_SUMMARY (summary));
316
317         g_rec_mutex_lock (&summary->priv->summary_lock);
318
319         g_free (summary->priv->summary_path);
320         summary->priv->summary_path = g_strdup (name);
321
322         g_rec_mutex_unlock (&summary->priv->summary_lock);
323 }
324
325 /**
326  * camel_store_summary_count:
327  * @summary: a #CamelStoreSummary object
328  *
329  * Get the number of summary items stored in this summary.
330  *
331  * Returns: the number of items gint he summary.
332  **/
333 gint
334 camel_store_summary_count (CamelStoreSummary *summary)
335 {
336         g_return_val_if_fail (CAMEL_IS_STORE_SUMMARY (summary), -1);
337
338         return summary->folders->len;
339 }
340
341 /**
342  * camel_store_summary_array:
343  * @summary: a #CamelStoreSummary object
344  *
345  * Obtain a copy of the summary array.  This is done atomically,
346  * so cannot contain empty entries.
347  *
348  * It must be freed using camel_store_summary_array_free().
349  *
350  * Returns: the summary array
351  **/
352 GPtrArray *
353 camel_store_summary_array (CamelStoreSummary *summary)
354 {
355         CamelStoreInfo *info;
356         GPtrArray *res;
357         gint i;
358
359         g_return_val_if_fail (CAMEL_IS_STORE_SUMMARY (summary), NULL);
360
361         g_rec_mutex_lock (&summary->priv->summary_lock);
362
363         res = g_ptr_array_sized_new (summary->folders->len);
364         for (i = 0; i < summary->folders->len; i++) {
365                 info = g_ptr_array_index (summary->folders, i);
366                 camel_store_summary_info_ref (summary, info);
367                 g_ptr_array_add (res, info);
368         }
369
370         g_rec_mutex_unlock (&summary->priv->summary_lock);
371
372         return res;
373 }
374
375 /**
376  * camel_store_summary_array_free:
377  * @summary: a #CamelStoreSummary object
378  * @array: the summary array as gotten from camel_store_summary_array()
379  *
380  * Free the folder summary array.
381  **/
382 void
383 camel_store_summary_array_free (CamelStoreSummary *summary,
384                                 GPtrArray *array)
385 {
386         gint i;
387
388         g_return_if_fail (CAMEL_IS_STORE_SUMMARY (summary));
389         g_return_if_fail (array != NULL);
390
391         for (i = 0; i < array->len; i++)
392                 camel_store_summary_info_unref (summary, array->pdata[i]);
393
394         g_ptr_array_free (array, TRUE);
395 }
396
397 /**
398  * camel_store_summary_path:
399  * @summary: a #CamelStoreSummary object
400  * @path: path to the item
401  *
402  * Retrieve a summary item by path name.
403  *
404  * The returned #CamelStoreInfo is referenced for thread-safety and should be
405  * unreferenced with camel_store_summary_info_unref() when finished with it.
406  *
407  * Returns: the summary item, or %NULL if the @path name is not
408  * available
409  **/
410 CamelStoreInfo *
411 camel_store_summary_path (CamelStoreSummary *summary,
412                           const gchar *path)
413 {
414         CamelStoreInfo *info;
415
416         g_return_val_if_fail (CAMEL_IS_STORE_SUMMARY (summary), NULL);
417         g_return_val_if_fail (path != NULL, NULL);
418
419         g_rec_mutex_lock (&summary->priv->summary_lock);
420
421         info = g_hash_table_lookup (summary->folders_path, path);
422
423         if (info != NULL)
424                 camel_store_summary_info_ref (summary, info);
425
426         g_rec_mutex_unlock (&summary->priv->summary_lock);
427
428         return info;
429 }
430
431 /**
432  * camel_store_summary_load:
433  * @summary: a #CamelStoreSummary object
434  *
435  * Load the summary off disk.
436  *
437  * Returns: %0 on success or %-1 on fail
438  **/
439 gint
440 camel_store_summary_load (CamelStoreSummary *summary)
441 {
442         CamelStoreSummaryClass *class;
443         CamelStoreInfo *info;
444         FILE *in;
445         gint i;
446
447         g_return_val_if_fail (CAMEL_IS_STORE_SUMMARY (summary), -1);
448         g_return_val_if_fail (summary->priv->summary_path != NULL, -1);
449
450         class = CAMEL_STORE_SUMMARY_GET_CLASS (summary);
451         g_return_val_if_fail (class->store_info_load != NULL, -1);
452
453         in = g_fopen (summary->priv->summary_path, "rb");
454         if (in == NULL)
455                 return -1;
456
457         g_rec_mutex_lock (&summary->priv->io_lock);
458
459         if (class->summary_header_load (summary, in) == -1)
460                 goto error;
461
462         /* now read in each message ... */
463         for (i = 0; i < summary->priv->count; i++) {
464                 info = class->store_info_load (summary, in);
465
466                 if (info == NULL)
467                         goto error;
468
469                 camel_store_summary_add (summary, info);
470         }
471
472         g_rec_mutex_unlock (&summary->priv->io_lock);
473
474         if (fclose (in) != 0)
475                 return -1;
476
477         summary->priv->dirty = FALSE;
478
479         return 0;
480
481 error:
482         i = ferror (in);
483         g_warning ("Cannot load summary file '%s': %s", summary->priv->summary_path, i == 0 ? "Unknown error" : g_strerror (i));
484         g_rec_mutex_unlock (&summary->priv->io_lock);
485         fclose (in);
486         summary->priv->dirty = FALSE;
487         errno = i;
488
489         return -1;
490 }
491
492 /**
493  * camel_store_summary_save:
494  * @summary: a #CamelStoreSummary object
495  *
496  * Writes the summary to disk.  The summary is only written if changes
497  * have occurred.
498  *
499  * Returns: %0 on succes or %-1 on fail
500  **/
501 gint
502 camel_store_summary_save (CamelStoreSummary *summary)
503 {
504         CamelStoreSummaryClass *class;
505         CamelStoreInfo *info;
506         FILE *out;
507         gint fd;
508         gint i;
509         guint32 count;
510
511         g_return_val_if_fail (CAMEL_IS_STORE_SUMMARY (summary), -1);
512         g_return_val_if_fail (summary->priv->summary_path != NULL, -1);
513
514         class = CAMEL_STORE_SUMMARY_GET_CLASS (summary);
515         g_return_val_if_fail (class->summary_header_save != NULL, -1);
516
517         io (printf ("** saving summary\n"));
518
519         if (!summary->priv->dirty) {
520                 io (printf ("**  summary clean no save\n"));
521                 return 0;
522         }
523
524         fd = g_open (
525                 summary->priv->summary_path,
526                 O_RDWR | O_CREAT | O_TRUNC | O_BINARY, 0600);
527         if (fd == -1) {
528                 io (printf ("**  open error: %s\n", g_strerror (errno)));
529                 return -1;
530         }
531
532         out = fdopen (fd, "wb");
533         if (out == NULL) {
534                 i = errno;
535                 printf ("**  fdopen error: %s\n", g_strerror (errno));
536                 close (fd);
537                 errno = i;
538                 return -1;
539         }
540
541         io (printf ("saving header\n"));
542
543         g_rec_mutex_lock (&summary->priv->io_lock);
544
545         if (class->summary_header_save (summary, out) == -1) {
546                 i = errno;
547                 fclose (out);
548                 g_rec_mutex_unlock (&summary->priv->io_lock);
549                 errno = i;
550                 return -1;
551         }
552
553         /* now write out each message ... */
554
555         /* FIXME: Locking? */
556
557         count = summary->folders->len;
558         for (i = 0; i < count; i++) {
559                 info = summary->folders->pdata[i];
560                 class->store_info_save (summary, out, info);
561         }
562
563         g_rec_mutex_unlock (&summary->priv->io_lock);
564
565         if (fflush (out) != 0 || fsync (fileno (out)) == -1) {
566                 i = errno;
567                 fclose (out);
568                 errno = i;
569                 return -1;
570         }
571
572         if (fclose (out) != 0)
573                 return -1;
574
575         summary->priv->dirty = FALSE;
576
577         return 0;
578 }
579
580 /**
581  * camel_store_summary_add:
582  * @summary: a #CamelStoreSummary object
583  * @info: a #CamelStoreInfo
584  *
585  * Adds a new @info record to the summary.  If @info->uid is %NULL,
586  * then a new uid is automatically re-assigned by calling
587  * camel_store_summary_next_uid_string().
588  *
589  * The @info record should have been generated by calling one of the
590  * info_new_*() functions, as it will be free'd based on the summary
591  * class.  And MUST NOT be allocated directly using malloc.
592  **/
593 void
594 camel_store_summary_add (CamelStoreSummary *summary,
595                          CamelStoreInfo *info)
596 {
597         g_return_if_fail (CAMEL_IS_STORE_SUMMARY (summary));
598
599         if (info == NULL)
600                 return;
601
602         if (camel_store_info_path (summary, info) == NULL) {
603                 g_warning ("Trying to add a folder info with missing required path name\n");
604                 return;
605         }
606
607         g_rec_mutex_lock (&summary->priv->summary_lock);
608
609         g_ptr_array_add (summary->folders, info);
610         g_hash_table_insert (summary->folders_path, (gchar *) camel_store_info_path (summary, info), info);
611         summary->priv->dirty = TRUE;
612
613         g_rec_mutex_unlock (&summary->priv->summary_lock);
614 }
615
616 /**
617  * camel_store_summary_add_from_path:
618  * @summary: a #CamelStoreSummary object
619  * @path: item path
620  *
621  * Build a new info record based on the name, and add it to the summary.
622  *
623  * Returns: the newly added record
624  **/
625 CamelStoreInfo *
626 camel_store_summary_add_from_path (CamelStoreSummary *summary,
627                                    const gchar *path)
628 {
629         CamelStoreInfo *info;
630
631         g_return_val_if_fail (CAMEL_IS_STORE_SUMMARY (summary), NULL);
632         g_return_val_if_fail (path != NULL, NULL);
633
634         g_rec_mutex_lock (&summary->priv->summary_lock);
635
636         info = g_hash_table_lookup (summary->folders_path, path);
637         if (info != NULL) {
638                 g_warning ("Trying to add folder '%s' to summary that already has it", path);
639                 info = NULL;
640         } else {
641                 CamelStoreSummaryClass *class;
642
643                 class = CAMEL_STORE_SUMMARY_GET_CLASS (summary);
644                 g_return_val_if_fail (class->store_info_new != NULL, NULL);
645
646                 info = class->store_info_new (summary, path);
647
648                 g_ptr_array_add (summary->folders, info);
649                 g_hash_table_insert (summary->folders_path, (gchar *) camel_store_info_path (summary, info), info);
650                 summary->priv->dirty = TRUE;
651         }
652
653         g_rec_mutex_unlock (&summary->priv->summary_lock);
654
655         return info;
656 }
657
658 /**
659  * camel_store_summary_info_ref:
660  * @summary: a #CamelStoreSummary object
661  * @info: a #CamelStoreInfo
662  *
663  * Add an extra reference to @info.
664  *
665  * Returns: the @info argument
666  **/
667 CamelStoreInfo *
668 camel_store_summary_info_ref (CamelStoreSummary *summary,
669                               CamelStoreInfo *info)
670 {
671         g_return_val_if_fail (info != NULL, NULL);
672         g_return_val_if_fail (info->refcount > 0, NULL);
673
674         g_atomic_int_inc (&info->refcount);
675
676         return info;
677 }
678
679 /**
680  * camel_store_summary_info_unref:
681  * @summary: a #CamelStoreSummary object
682  * @info: a #CamelStoreInfo
683  *
684  * Unref and potentially free @info, and all associated memory.
685  **/
686 void
687 camel_store_summary_info_unref (CamelStoreSummary *summary,
688                                 CamelStoreInfo *info)
689 {
690         g_return_if_fail (CAMEL_IS_STORE_SUMMARY (summary));
691         g_return_if_fail (info != NULL);
692         g_return_if_fail (info->refcount > 0);
693
694         if (g_atomic_int_dec_and_test (&info->refcount)) {
695                 CamelStoreSummaryClass *class;
696
697                 class = CAMEL_STORE_SUMMARY_GET_CLASS (summary);
698                 g_return_if_fail (class->store_info_free != NULL);
699
700                 class->store_info_free (summary, info);
701         }
702 }
703
704 /**
705  * camel_store_summary_touch:
706  * @summary: a #CamelStoreSummary object
707  *
708  * Mark the summary as changed, so that a save will force it to be
709  * written back to disk.
710  **/
711 void
712 camel_store_summary_touch (CamelStoreSummary *summary)
713 {
714         g_return_if_fail (CAMEL_IS_STORE_SUMMARY (summary));
715
716         g_rec_mutex_lock (&summary->priv->summary_lock);
717         summary->priv->dirty = TRUE;
718         g_rec_mutex_unlock (&summary->priv->summary_lock);
719 }
720
721 /**
722  * camel_store_summary_remove:
723  * @summary: a #CamelStoreSummary object
724  * @info: a #CamelStoreInfo
725  *
726  * Remove a specific @info record from the summary.
727  **/
728 void
729 camel_store_summary_remove (CamelStoreSummary *summary,
730                             CamelStoreInfo *info)
731 {
732         g_return_if_fail (CAMEL_IS_STORE_SUMMARY (summary));
733         g_return_if_fail (info != NULL);
734
735         g_rec_mutex_lock (&summary->priv->summary_lock);
736         g_hash_table_remove (summary->folders_path, camel_store_info_path (summary, info));
737         g_ptr_array_remove (summary->folders, info);
738         summary->priv->dirty = TRUE;
739         g_rec_mutex_unlock (&summary->priv->summary_lock);
740
741         camel_store_summary_info_unref (summary, info);
742 }
743
744 /**
745  * camel_store_summary_remove_path:
746  * @summary: a #CamelStoreSummary object
747  * @path: item path
748  *
749  * Remove a specific info record from the summary, by @path.
750  **/
751 void
752 camel_store_summary_remove_path (CamelStoreSummary *summary,
753                                  const gchar *path)
754 {
755         CamelStoreInfo *oldinfo;
756         gchar *oldpath;
757
758         g_return_if_fail (CAMEL_IS_STORE_SUMMARY (summary));
759         g_return_if_fail (path != NULL);
760
761         g_rec_mutex_lock (&summary->priv->summary_lock);
762         if (g_hash_table_lookup_extended (summary->folders_path, path, (gpointer) &oldpath, (gpointer) &oldinfo)) {
763                 /* make sure it doesn't vanish while we're removing it */
764                 camel_store_summary_info_ref (summary, oldinfo);
765                 g_rec_mutex_unlock (&summary->priv->summary_lock);
766                 camel_store_summary_remove (summary, oldinfo);
767                 camel_store_summary_info_unref (summary, oldinfo);
768         } else {
769                 g_rec_mutex_unlock (&summary->priv->summary_lock);
770         }
771 }
772
773 /**
774  * camel_store_summary_info_new:
775  * @summary: a #CamelStoreSummary object
776  *
777  * Allocate a new #CamelStoreInfo, suitable for adding to this
778  * summary.
779  *
780  * Returns: the newly allocated #CamelStoreInfo
781  **/
782 CamelStoreInfo *
783 camel_store_summary_info_new (CamelStoreSummary *summary)
784 {
785         CamelStoreSummaryClass *class;
786         CamelStoreInfo *info;
787
788         g_return_val_if_fail (CAMEL_IS_STORE_SUMMARY (summary), NULL);
789
790         class = CAMEL_STORE_SUMMARY_GET_CLASS (summary);
791         g_return_val_if_fail (class->store_info_size > 0, NULL);
792
793         info = g_slice_alloc0 (class->store_info_size);
794         info->refcount = 1;
795
796         return info;
797 }
798
799 /**
800  * camel_store_info_set_string:
801  * @summary: a #CamelStoreSummary object
802  * @info: a #CamelStoreInfo
803  * @type: specific string being set
804  * @value: string value to set
805  *
806  * Set a specific string on the @info.
807  **/
808 void
809 camel_store_info_set_string (CamelStoreSummary *summary,
810                              CamelStoreInfo *info,
811                              gint type,
812                              const gchar *value)
813 {
814         CamelStoreSummaryClass *class;
815
816         g_return_if_fail (CAMEL_IS_STORE_SUMMARY (summary));
817         g_return_if_fail (info != NULL);
818
819         class = CAMEL_STORE_SUMMARY_GET_CLASS (summary);
820         g_return_if_fail (class->store_info_set_string != NULL);
821
822         g_rec_mutex_lock (&summary->priv->summary_lock);
823
824         class->store_info_set_string (summary, info, type, value);
825
826         g_rec_mutex_unlock (&summary->priv->summary_lock);
827 }
828
829 /**
830  * camel_store_info_path:
831  * @summary: a #CamelStoreSummary
832  * @info: a #CamelStoreInfo
833  *
834  * Returns the path string from @info.
835  *
836  * Returns: the path string from @info
837  **/
838 const gchar *
839 camel_store_info_path (CamelStoreSummary *summary,
840                        CamelStoreInfo *info)
841 {
842         g_return_val_if_fail (CAMEL_IS_STORE_SUMMARY (summary), NULL);
843         g_return_val_if_fail (info != NULL, NULL);
844
845         /* XXX Not thread-safe; should return a duplicate. */
846         return info->path;
847 }
848
849 /**
850  * camel_store_info_name:
851  * @summary: a #CamelStoreSummary
852  * @info: a #CamelStoreInfo
853  *
854  * Returns the last segment of the path string from @info.
855  *
856  * Returns: the last segment of the path string from @info
857  **/
858 const gchar *
859 camel_store_info_name (CamelStoreSummary *summary,
860                        CamelStoreInfo *info)
861 {
862         const gchar *cp;
863
864         g_return_val_if_fail (CAMEL_IS_STORE_SUMMARY (summary), NULL);
865         g_return_val_if_fail (info != NULL, NULL);
866
867         cp = strrchr (info->path, '/');
868
869         /* XXX Not thread-safe; should return a duplicate. */
870         return (cp != NULL) ? cp + 1 : info->path;
871 }
872
873 static gboolean
874 store_summary_save_timeout (gpointer user_data)
875 {
876         CamelStoreSummary *summary = CAMEL_STORE_SUMMARY (user_data);
877
878         g_return_val_if_fail (summary != NULL, FALSE);
879
880         g_rec_mutex_lock (&summary->priv->summary_lock);
881
882         if (summary->priv->scheduled_save_id) {
883                 summary->priv->scheduled_save_id = 0;
884                 camel_store_summary_save (summary);
885         }
886
887         g_rec_mutex_unlock (&summary->priv->summary_lock);
888
889         return FALSE;
890 }
891
892 static void
893 store_summary_schedule_save (CamelStoreSummary *summary)
894 {
895         g_return_if_fail (CAMEL_IS_STORE_SUMMARY (summary));
896
897         if (summary->priv->scheduled_save_id != 0)
898                 g_source_remove (summary->priv->scheduled_save_id);
899
900         summary->priv->scheduled_save_id = g_timeout_add_seconds (
901                 5, store_summary_save_timeout, summary);
902         g_source_set_name_by_id (
903                 summary->priv->scheduled_save_id,
904                 "[camel] store_summary_save_timeout");
905 }
906
907 static void
908 store_summary_sync_folder_summary_count_cb (CamelFolderSummary *folder_summary,
909                                             GParamSpec *param,
910                                             CamelStoreSummary *summary)
911 {
912         gint new_count;
913         const gchar *path;
914         CamelStoreInfo *si;
915
916         g_return_if_fail (CAMEL_IS_FOLDER_SUMMARY (folder_summary));
917         g_return_if_fail (param != NULL);
918         g_return_if_fail (CAMEL_IS_STORE_SUMMARY (summary));
919
920         path = g_hash_table_lookup (summary->priv->folder_summaries, folder_summary);
921         g_return_if_fail (path != NULL);
922
923         g_rec_mutex_lock (&summary->priv->summary_lock);
924         si = camel_store_summary_path (summary, path);
925         if (!si) {
926                 g_rec_mutex_unlock (&summary->priv->summary_lock);
927                 g_warning ("%s: Store summary %p doesn't hold path '%s'", G_STRFUNC, summary, path);
928                 return;
929         }
930
931         if (g_strcmp0 (g_param_spec_get_name (param), "saved-count") == 0) {
932                 new_count = camel_folder_summary_get_saved_count (folder_summary);
933                 if (si->total != new_count) {
934                         si->total = new_count;
935                         camel_store_summary_touch (summary);
936                         store_summary_schedule_save (summary);
937                 }
938         } else if (g_strcmp0 (g_param_spec_get_name (param), "unread-count") == 0) {
939                 new_count = camel_folder_summary_get_unread_count (folder_summary);
940                 if (si->unread != new_count) {
941                         si->unread = new_count;
942                         camel_store_summary_touch (summary);
943                         store_summary_schedule_save (summary);
944                 }
945         } else {
946                 g_warn_if_reached ();
947         }
948
949         camel_store_summary_info_unref (summary, si);
950
951         g_rec_mutex_unlock (&summary->priv->summary_lock);
952 }
953
954 /**
955  * camel_store_summary_connect_folder_summary:
956  * @summary: a #CamelStoreSummary object
957  * @path: used path for @folder_summary
958  * @folder_summary: a #CamelFolderSummary object
959  *
960  * Connects listeners for count changes on @folder_summary to keep
961  * CamelStoreInfo.total and CamelStoreInfo.unread in sync transparently.
962  * The @folder_summary is stored in @summary as @path. Use
963  * camel_store_summary_disconnect_folder_summary() to disconnect from
964  * listening.
965  *
966  * Returns: Whether successfully connect callbacks for count change
967  * notifications.
968  *
969  * Since: 3.4
970  **/
971 gboolean
972 camel_store_summary_connect_folder_summary (CamelStoreSummary *summary,
973                                             const gchar *path,
974                                             CamelFolderSummary *folder_summary)
975 {
976         CamelStoreInfo *si;
977
978         g_return_val_if_fail (CAMEL_IS_STORE_SUMMARY (summary), FALSE);
979         g_return_val_if_fail (path != NULL, FALSE);
980         g_return_val_if_fail (CAMEL_IS_FOLDER_SUMMARY (folder_summary), FALSE);
981
982         g_rec_mutex_lock (&summary->priv->summary_lock);
983
984         si = camel_store_summary_path (summary, path);
985         if (!si) {
986                 g_rec_mutex_unlock (&summary->priv->summary_lock);
987                 g_warning ("%s: Store summary %p doesn't hold path '%s'", G_STRFUNC, summary, path);
988                 return FALSE;
989         }
990
991         camel_store_summary_info_unref (summary, si);
992
993         if (g_hash_table_lookup (summary->priv->folder_summaries, folder_summary)) {
994                 g_rec_mutex_unlock (&summary->priv->summary_lock);
995                 g_warning ("%s: Store summary %p already listens on folder summary %p", G_STRFUNC, summary, folder_summary);
996                 return FALSE;
997         }
998
999         g_hash_table_insert (summary->priv->folder_summaries, folder_summary, g_strdup (path));
1000         g_signal_connect (folder_summary, "notify::saved-count", G_CALLBACK (store_summary_sync_folder_summary_count_cb), summary);
1001         g_signal_connect (folder_summary, "notify::unread-count", G_CALLBACK (store_summary_sync_folder_summary_count_cb), summary);
1002
1003         g_rec_mutex_unlock (&summary->priv->summary_lock);
1004
1005         return TRUE;
1006 }
1007
1008 /**
1009  * camel_store_summary_disconnect_folder_summary:
1010  * @summary: a #CamelStoreSummary object
1011  * @folder_summary: a #CamelFolderSummary object
1012  *
1013  * Diconnects count change listeners previously connected
1014  * by camel_store_summary_connect_folder_summary().
1015  *
1016  * Returns: Whether such connection existed and whether was successfully
1017  * removed.
1018  *
1019  * Since: 3.4
1020  **/
1021 gboolean
1022 camel_store_summary_disconnect_folder_summary (CamelStoreSummary *summary,
1023                                                CamelFolderSummary *folder_summary)
1024 {
1025         g_return_val_if_fail (CAMEL_IS_STORE_SUMMARY (summary), FALSE);
1026         g_return_val_if_fail (CAMEL_IS_FOLDER_SUMMARY (folder_summary), FALSE);
1027
1028         g_rec_mutex_lock (&summary->priv->summary_lock);
1029
1030         if (!g_hash_table_lookup (summary->priv->folder_summaries, folder_summary)) {
1031                 g_rec_mutex_unlock (&summary->priv->summary_lock);
1032                 g_warning ("%s: Store summary %p is not connected to folder summary %p", G_STRFUNC, summary, folder_summary);
1033                 return FALSE;
1034         }
1035
1036         g_signal_handlers_disconnect_by_func (folder_summary, G_CALLBACK (store_summary_sync_folder_summary_count_cb), summary);
1037         g_hash_table_remove (summary->priv->folder_summaries, folder_summary);
1038
1039         if (summary->priv->scheduled_save_id != 0) {
1040                 g_source_remove (summary->priv->scheduled_save_id);
1041                 summary->priv->scheduled_save_id = 0;
1042         }
1043
1044         camel_store_summary_save (summary);
1045
1046         g_rec_mutex_unlock (&summary->priv->summary_lock);
1047
1048         return TRUE;
1049 }