2a96d5583622e2710f4480ed47e91e0803f1bf34
[claws.git] / src / plugins / notification / notification_core.c
1 /* Notification plugin for Claws-Mail
2  * Copyright (C) 2005-2007 Holger Berndt
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program. If not, see <http://www.gnu.org/licenses/>.
16  */
17
18 #ifdef HAVE_CONFIG_H
19 #  include "config.h"
20 #  include "claws-features.h"
21 #endif
22
23 #include "folder.h"
24 #include "folderview.h"
25 #include "codeconv.h"
26 #include "gtk/gtkutils.h"
27
28 #include "notification_core.h"
29 #include "notification_plugin.h"
30 #include "notification_prefs.h"
31 #include "notification_banner.h"
32 #include "notification_popup.h"
33 #include "notification_command.h"
34 #include "notification_lcdproc.h"
35 #include "notification_trayicon.h"
36 #include "notification_indicator.h"
37
38 #ifdef HAVE_LIBCANBERRA_GTK
39 # include <canberra-gtk.h>
40 #endif
41
42 typedef struct {
43   GSList *collected_msgs;
44   GSList *folder_items;
45   gboolean unread_also;
46   gint max_msgs;
47   gint num_msgs;
48 } TraverseCollect;
49
50 static gboolean notification_traverse_collect(GNode*, gpointer);
51 static void     notification_new_unnotified_do_msg(MsgInfo*);
52 static gboolean notification_traverse_hash_startup(GNode*, gpointer);
53
54 static GHashTable *msg_count_hash;
55 static NotificationMsgCount msg_count;
56
57 #ifdef HAVE_LIBCANBERRA_GTK
58 static gboolean canberra_new_email_is_playing = FALSE;
59 #endif
60
61 static void msg_count_hash_update_func(FolderItem*, gpointer);
62 static void msg_count_update_from_hash(gpointer, gpointer, gpointer);
63 static void msg_count_clear(NotificationMsgCount*);
64 static void msg_count_add(NotificationMsgCount*,NotificationMsgCount*);
65 static void msg_count_copy(NotificationMsgCount*,NotificationMsgCount*);
66
67 void notification_core_global_includes_changed(void)
68 {
69 #ifdef NOTIFICATION_BANNER
70   notification_update_banner();
71 #endif
72
73   if(msg_count_hash) {
74     g_hash_table_destroy(msg_count_hash);
75     msg_count_hash = NULL;
76   }
77   notification_update_msg_counts(NULL);
78 }
79
80 /* Hide/show main window */
81 void notification_toggle_hide_show_window(void)
82 {
83   MainWindow *mainwin;
84   if((mainwin = mainwindow_get_mainwindow()) == NULL)
85     return;
86
87   if(gtk_widget_get_visible(GTK_WIDGET(mainwin->window))) {
88     if((gdk_window_get_state(GTK_WIDGET(mainwin->window)->window)&GDK_WINDOW_STATE_ICONIFIED)
89        || mainwindow_is_obscured()) {
90       notification_show_mainwindow(mainwin);
91     }
92     else {
93       main_window_hide(mainwin);
94     }
95   }
96   else {
97     notification_show_mainwindow(mainwin);
98   }
99 }
100
101 void notification_update_msg_counts(FolderItem *removed_item)
102 {
103   if(!msg_count_hash)
104     msg_count_hash = g_hash_table_new_full(g_str_hash,g_str_equal,
105                                            g_free,g_free);
106
107   folder_func_to_all_folders(msg_count_hash_update_func, msg_count_hash);
108
109   if(removed_item) {
110     gchar *identifier;
111     identifier = folder_item_get_identifier(removed_item);
112     if(identifier) {
113       g_hash_table_remove(msg_count_hash, identifier);
114       g_free(identifier);
115     }
116   }
117   msg_count_clear(&msg_count);
118   g_hash_table_foreach(msg_count_hash, msg_count_update_from_hash, NULL);
119 #ifdef NOTIFICATION_LCDPROC
120   notification_update_lcdproc();
121 #endif
122 #ifdef NOTIFICATION_TRAYICON
123   notification_update_trayicon();
124 #endif
125 #ifdef NOTIFICATION_INDICATOR
126   notification_update_indicator();
127 #endif
128   notification_update_urgency_hint();
129 }
130
131 static void msg_count_clear(NotificationMsgCount *count)
132 {
133   count->new_msgs          = 0;
134   count->unread_msgs       = 0;
135   count->unreadmarked_msgs = 0;
136   count->marked_msgs       = 0;
137   count->total_msgs        = 0;
138 }
139
140 /* c1 += c2 */
141 static void msg_count_add(NotificationMsgCount *c1,NotificationMsgCount *c2)
142 {
143   c1->new_msgs          += c2->new_msgs;
144   c1->unread_msgs       += c2->unread_msgs;
145   c1->unreadmarked_msgs += c2->unreadmarked_msgs;
146   c1->marked_msgs       += c2->marked_msgs;
147   c1->total_msgs        += c2->total_msgs;
148 }
149
150 /* c1 = c2 */
151 static void msg_count_copy(NotificationMsgCount *c1,NotificationMsgCount *c2)
152 {
153   c1->new_msgs          = c2->new_msgs;
154   c1->unread_msgs       = c2->unread_msgs;
155   c1->unreadmarked_msgs = c2->unreadmarked_msgs;
156   c1->marked_msgs       = c2->marked_msgs;
157   c1->total_msgs        = c2->total_msgs;
158 }
159
160 gboolean get_flat_gslist_from_nodes_traverse_func(GNode *node, gpointer data)
161 {
162   if(node->data) {
163     GSList **list = data;
164     *list = g_slist_prepend(*list, node->data);
165   }
166   return FALSE;
167 }
168
169 GSList* get_flat_gslist_from_nodes(GNode *node)
170 {
171   GSList *retval = NULL;
172
173   g_node_traverse(node, G_PRE_ORDER, G_TRAVERSE_ALL, -1, get_flat_gslist_from_nodes_traverse_func, &retval);
174   return retval;
175 }
176
177 void notification_core_get_msg_count_of_foldername(gchar *foldername, NotificationMsgCount *count)
178 {
179   GList *list;
180   Folder *walk_folder;
181   Folder *folder = NULL;
182
183   for(list = folder_get_list(); list != NULL; list = list->next) {
184     walk_folder = list->data;
185     if(strcmp2(foldername, walk_folder->name) == 0) {
186       folder = walk_folder;
187       break;
188     }
189   }
190   if(!folder) {
191     debug_print("Notification plugin: Error: Could not find folder %s\n", foldername);
192     return;
193   }
194
195   msg_count_clear(count);
196   notification_core_get_msg_count(get_flat_gslist_from_nodes(folder->node), count);
197 }
198
199 void notification_core_get_msg_count(GSList *folder_list,
200                                      NotificationMsgCount *count)
201 {
202   GSList *walk;
203
204   if(!folder_list)
205     msg_count_copy(count,&msg_count);
206   else {
207     msg_count_clear(count);
208     for(walk = folder_list; walk; walk = walk->next) {
209       gchar *identifier;
210       NotificationMsgCount *item_count;
211       FolderItem *item = (FolderItem*) walk->data;
212       identifier = folder_item_get_identifier(item);
213       if(identifier) {
214         item_count = g_hash_table_lookup(msg_count_hash,identifier);
215         g_free(identifier);
216         if(item_count)
217           msg_count_add(count, item_count);
218       }
219     }
220   }
221 }
222
223 static void msg_count_hash_update_func(FolderItem *item, gpointer data)
224 {
225   gchar *identifier;
226   NotificationMsgCount *count;
227   GHashTable *hash = data;
228
229   if(!notify_include_folder_type(item->folder->klass->type,
230                                  item->folder->klass->uistr))
231     return;
232
233   identifier = folder_item_get_identifier(item);
234   if(!identifier)
235     return;
236
237   count = g_hash_table_lookup(hash, identifier);
238
239   if(!count) {
240     count = g_new0(NotificationMsgCount,1);
241     g_hash_table_insert(hash, identifier, count);
242   }
243   else
244     g_free(identifier);
245
246   count->new_msgs          = item->new_msgs;
247   count->unread_msgs       = item->unread_msgs;
248   count->unreadmarked_msgs = item->unreadmarked_msgs;
249   count->marked_msgs       = item->marked_msgs;
250   count->total_msgs        = item->total_msgs;
251 }
252
253 static void msg_count_update_from_hash(gpointer key, gpointer value,
254                                        gpointer data)
255 {
256   NotificationMsgCount *count = value;
257   msg_count_add(&msg_count,count);
258 }
259
260
261 /* Replacement for the post-filtering hook:
262    Pseudocode by Colin:
263 hook on FOLDER_ITEM_UPDATE_HOOKLIST
264  if hook flags & F_ITEM_UPDATE_MSGCOUNT
265   scan mails (folder_item_get_msg_list)
266    if MSG_IS_NEW(msginfo->flags) and not in hashtable
267     notify()
268     add to hashtable
269    procmsg_msg_list_free
270
271 hook on MSGINFO_UPDATE_HOOKLIST
272  if hook flags & MSGINFO_UPDATE_FLAGS
273   if !MSG_IS_NEW(msginfo->flags)
274    remove from hashtable, it's now useless
275 */
276
277 /* This hash table holds all mails that we already notified about,
278    and that still are marked as "new". The keys are the msgid's,
279    the values are just 1's stored in a pointer. */
280 static GHashTable *notified_hash = NULL;
281
282
283 /* Remove message from the notified_hash if
284  *  - the message flags changed
285  *  - the message is not new
286  *  - the message is in the hash
287 */
288 gboolean notification_notified_hash_msginfo_update(MsgInfoUpdate *msg_update)
289 {
290   g_return_val_if_fail(msg_update != NULL, FALSE);
291
292   if((msg_update->flags & MSGINFO_UPDATE_FLAGS) &&
293      !MSG_IS_NEW(msg_update->msginfo->flags)) {
294
295     MsgInfo *msg;
296     gchar *msgid;
297
298     msg = msg_update->msginfo;
299     if(msg->msgid)
300       msgid = msg->msgid;
301     else {
302       debug_print("Notification Plugin: Message has no message ID!\n");
303       msgid = "";
304     }
305
306     g_return_val_if_fail(msg != NULL, FALSE);
307
308     if(g_hash_table_lookup(notified_hash, msgid) != NULL) {
309
310       debug_print("Notification Plugin: Removing message id %s from hash "
311                   "table\n", msgid);
312       g_hash_table_remove(notified_hash, msgid);
313     }
314   }
315   return FALSE;
316 }
317
318 /* On startup, mark all new mails as already notified
319  * (by including them in the hash) */
320 void notification_notified_hash_startup_init(void)
321 {
322   GList *folder_list, *walk;
323   Folder *folder;
324
325   if(!notified_hash) {
326     notified_hash = g_hash_table_new_full(g_str_hash, g_str_equal,
327                                           g_free, NULL);
328     debug_print("Notification Plugin: Hash table created\n");
329   }
330
331   folder_list = folder_get_list();
332   for(walk = folder_list; walk != NULL; walk = g_list_next(walk)) {
333     folder = walk->data;
334
335     g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
336                     notification_traverse_hash_startup, NULL);
337   }
338 }
339
340 static gboolean notification_traverse_hash_startup(GNode *node, gpointer data)
341 {
342   GSList *walk;
343   GSList *msg_list;
344   FolderItem *item = (FolderItem*) node->data;
345   gint new_msgs_left;
346
347   if(!(item->new_msgs))
348     return FALSE;
349
350   new_msgs_left = item->new_msgs;
351   msg_list = folder_item_get_msg_list(item);
352
353   for(walk = msg_list; walk; walk = g_slist_next(walk)) {
354     MsgInfo *msg = (MsgInfo*) walk->data;
355     if(MSG_IS_NEW(msg->flags)) {
356       gchar *msgid;
357
358       if(msg->msgid)
359         msgid = msg->msgid;
360       else {
361         debug_print("Notification Plugin: Message has no message ID!\n");
362         msgid = "";
363       }
364
365       /* If the message id is not yet in the hash, add it */
366       g_hash_table_insert(notified_hash, g_strdup(msgid),
367                           GINT_TO_POINTER(1));
368       debug_print("Notification Plugin: Init: Added msg id %s to the hash\n",
369                   msgid);
370       /* Decrement left count and check if we're already done */
371       new_msgs_left--;
372       if(new_msgs_left == 0)
373         break;
374     }
375   }
376   procmsg_msg_list_free(msg_list);
377   return FALSE;
378 }
379
380 void notification_core_free(void)
381 {
382   if(notified_hash) {
383     g_hash_table_destroy(notified_hash);
384     notified_hash = NULL;
385   }
386   if(msg_count_hash) {
387     g_hash_table_destroy(msg_count_hash);
388     msg_count_hash = NULL;
389   }
390   debug_print("Notification Plugin: Freed internal data\n");
391 }
392
393 void notification_new_unnotified_msgs(FolderItemUpdateData *update_data)
394 {
395   GSList *msg_list, *walk;
396
397   g_return_if_fail(notified_hash != NULL);
398
399   msg_list = folder_item_get_msg_list(update_data->item);
400
401   for(walk = msg_list; walk; walk = g_slist_next(walk)) {
402     MsgInfo *msg;
403     msg = (MsgInfo*) walk->data;
404
405     if(MSG_IS_NEW(msg->flags)) {
406       gchar *msgid;
407
408       if(msg->msgid)
409         msgid = msg->msgid;
410       else {
411         debug_print("Notification Plugin: Message has not message ID!\n");
412         msgid = "";
413       }
414
415       debug_print("Notification Plugin: Found msg %s, "
416                   "checking if it is in hash...\n", msgid);
417       /* Check if message is in hash table */
418       if(g_hash_table_lookup(notified_hash, msgid) != NULL)
419         debug_print("yes.\n");
420       else {
421         /* Add to hashtable */
422         g_hash_table_insert(notified_hash, g_strdup(msgid),
423                             GINT_TO_POINTER(1));
424         debug_print("no, added to table.\n");
425
426         /* Do the notification */
427         notification_new_unnotified_do_msg(msg);
428       }
429
430     } /* msg is 'new' */
431   } /* for all messages */
432   procmsg_msg_list_free(msg_list);
433 }
434
435 #ifdef HAVE_LIBCANBERRA_GTK
436 static void canberra_finished_cb(ca_context *c, uint32_t id, int error, void *data)
437 {
438   canberra_new_email_is_playing = FALSE;
439 }
440 #endif
441
442 static void notification_new_unnotified_do_msg(MsgInfo *msg)
443 {
444 #ifdef NOTIFICATION_POPUP
445   notification_popup_msg(msg);
446 #endif
447 #ifdef NOTIFICATION_COMMAND
448   notification_command_msg(msg);
449 #endif
450 #ifdef NOTIFICATION_TRAYICON
451   notification_trayicon_msg(msg);
452 #endif
453
454 #ifdef HAVE_LIBCANBERRA_GTK
455   /* canberra */
456   if(notify_config.canberra_play_sounds && !canberra_new_email_is_playing) {
457     MainWindow *mainwin;
458     ca_proplist *proplist;
459     mainwin = mainwindow_get_mainwindow();
460     ca_proplist_create(&proplist);
461     ca_proplist_sets(proplist,CA_PROP_EVENT_ID ,"message-new-email");
462     canberra_new_email_is_playing = TRUE;
463     ca_context_play_full(ca_gtk_context_get(), 0, proplist, canberra_finished_cb, NULL);
464     ca_proplist_destroy(proplist);
465   }
466 #endif
467 }
468
469 /* If folders is not NULL, then consider only those folder items
470  * If max_msgs is not 0, stop after collecting msg_msgs messages
471  */
472 GSList* notification_collect_msgs(gboolean unread_also, GSList *folder_items,
473                                   gint max_msgs)
474 {
475   GList *folder_list, *walk;
476   Folder *folder;
477   TraverseCollect collect_data;
478
479   collect_data.unread_also = unread_also;
480   collect_data.collected_msgs = NULL;
481   collect_data.folder_items = folder_items;
482   collect_data.max_msgs = max_msgs;
483   collect_data.num_msgs = 0;
484
485   folder_list = folder_get_list();
486   for(walk = folder_list; walk != NULL; walk = g_list_next(walk)) {
487     folder = walk->data;
488
489     g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
490                     notification_traverse_collect, &collect_data);
491   }
492   return collect_data.collected_msgs;
493 }
494
495 void notification_collected_msgs_free(GSList *collected_msgs)
496 {
497   if(collected_msgs) {
498     GSList *walk;
499     for(walk = collected_msgs; walk != NULL; walk = g_slist_next(walk)) {
500       CollectedMsg *msg = walk->data;
501       if(msg->from)
502                                 g_free(msg->from);
503       if(msg->subject)
504                                 g_free(msg->subject);
505       if(msg->folderitem_name)
506                                 g_free(msg->folderitem_name);
507                         msg->msginfo = NULL;
508       g_free(msg);
509     }
510     g_slist_free(collected_msgs);
511   }
512 }
513
514 static gboolean notification_traverse_collect(GNode *node, gpointer data)
515 {
516   TraverseCollect *cdata = data;
517   FolderItem *item = node->data;
518   gchar *folder_id_cur;
519
520   /* Obey global folder type limitations */
521   if(!notify_include_folder_type(item->folder->klass->type,
522                                  item->folder->klass->uistr))
523     return FALSE;
524
525   /* If a folder_items list was given, check it first */
526   if((cdata->folder_items) && (item->path != NULL) &&
527      ((folder_id_cur  = folder_item_get_identifier(item)) != NULL)) {
528     FolderItem *list_item;
529     GSList *walk;
530     gchar *folder_id_list;
531     gboolean eq;
532     gboolean folder_in_list = FALSE;
533
534     for(walk = cdata->folder_items; walk != NULL; walk = g_slist_next(walk)) {
535       list_item = walk->data;
536       folder_id_list = folder_item_get_identifier(list_item);
537       eq = !strcmp2(folder_id_list,folder_id_cur);
538       g_free(folder_id_list);
539       if(eq) {
540         folder_in_list = TRUE;
541         break;
542       }
543     }
544     g_free(folder_id_cur);
545     if(!folder_in_list)
546       return FALSE;
547   }
548
549   if(item->new_msgs || (cdata->unread_also && item->unread_msgs)) {
550     GSList *msg_list = folder_item_get_msg_list(item);
551     GSList *walk;
552     for(walk = msg_list; walk != NULL; walk = g_slist_next(walk)) {
553       MsgInfo *msg_info = walk->data;
554       CollectedMsg *cmsg;
555
556       if((cdata->max_msgs != 0) && (cdata->num_msgs >= cdata->max_msgs))
557         return FALSE;
558
559       if(MSG_IS_NEW(msg_info->flags) ||
560                                  (MSG_IS_UNREAD(msg_info->flags) && cdata->unread_also)) {
561
562                                 cmsg = g_new(CollectedMsg, 1);
563                                 cmsg->from = g_strdup(msg_info->from ? msg_info->from : "");
564                                 cmsg->subject = g_strdup(msg_info->subject ? msg_info->subject : "");
565                                 if(msg_info->folder && msg_info->folder->name)
566                                         cmsg->folderitem_name = g_strdup(msg_info->folder->path);
567                                 else
568                                         cmsg->folderitem_name = g_strdup("");
569
570                                 cmsg->msginfo = msg_info;
571
572                                 cdata->collected_msgs = g_slist_prepend(cdata->collected_msgs, cmsg);
573                                 cdata->num_msgs++;
574       }
575     }
576     procmsg_msg_list_free(msg_list);
577   }
578
579   return FALSE;
580 }
581
582 gboolean notify_include_folder_type(FolderType ftype, gchar *uistr)
583 {
584   gboolean retval;
585
586   retval = FALSE;
587   switch(ftype) {
588   case F_MH:
589   case F_MBOX:
590   case F_MAILDIR:
591   case F_IMAP:
592     if(notify_config.include_mail)
593       retval = TRUE;
594     break;
595   case F_NEWS:
596     if(notify_config.include_news)
597       retval = TRUE;
598     break;
599   case F_UNKNOWN:
600     if(uistr == NULL)
601       retval = FALSE;
602     else if(!strcmp(uistr, "vCalendar")) {
603       if(notify_config.include_calendar)
604         retval = TRUE;
605     }
606     else if(!strcmp(uistr, "RSSyl")) {
607       if(notify_config.include_rss)
608         retval = TRUE;
609     }
610     else
611       debug_print("Notification Plugin: Unknown folder type %d\n",ftype);
612     break;
613   default:
614     debug_print("Notification Plugin: Unknown folder type %d\n",ftype);
615   }
616
617   return retval;
618 }
619
620 static void fix_folderview_scroll(MainWindow *mainwin)
621 {
622         static gboolean fix_done = FALSE;
623
624         if (fix_done)
625                 return;
626
627         gtk_widget_queue_resize(mainwin->folderview->ctree);
628
629         fix_done = TRUE;
630 }
631
632 void notification_show_mainwindow(MainWindow *mainwin)
633 {
634       gtk_window_deiconify(GTK_WINDOW(mainwin->window));
635       gtk_window_set_skip_taskbar_hint(GTK_WINDOW(mainwin->window), FALSE);
636       main_window_show(mainwin);
637       gtk_window_present(GTK_WINDOW(mainwin->window));
638       fix_folderview_scroll(mainwin);
639 }
640
641 #ifdef HAVE_LIBNOTIFY
642 #define STR_MAX_LEN 511
643 /* Returns a newly allocated string which needs to be freed */
644 gchar* notification_libnotify_sanitize_str(gchar *in)
645 {
646   gint i_out;
647   gchar tmp_str[STR_MAX_LEN+1];
648
649   if(in == NULL) return NULL;
650
651   i_out = 0;
652   while(*in) {
653     if(*in == '<') {
654       if(i_out+3 >= STR_MAX_LEN) break;
655       memcpy(&(tmp_str[i_out]),"&lt;",4);
656       in++; i_out += 4;
657     }
658     else if(*in == '>') {
659       if(i_out+3 >= STR_MAX_LEN) break;
660       memcpy(&(tmp_str[i_out]),"&gt;",4);
661       in++; i_out += 4;
662     }
663     else if(*in == '&') {
664       if(i_out+4 >= STR_MAX_LEN) break;
665       memcpy(&(tmp_str[i_out]),"&amp;",5);
666       in++; i_out += 5;
667     }
668     else {
669       if(i_out >= STR_MAX_LEN) break;
670       tmp_str[i_out++] = *in++;
671     }
672   }
673   tmp_str[i_out] = '\0';
674   return strdup(tmp_str);
675 }
676
677 gchar* notification_validate_utf8_str(gchar *text)
678 {
679   gchar *utf8_str = NULL;
680
681   if(!g_utf8_validate(text, -1, NULL)) {
682     debug_print("Notification plugin: String is not valid utf8, "
683                 "trying to fix it...\n");
684     /* fix it */
685     utf8_str = conv_codeset_strdup(text,
686                                    conv_get_locale_charset_str_no_utf8(),
687                                    CS_INTERNAL);
688     /* check if the fix worked */
689     if(utf8_str == NULL || !g_utf8_validate(utf8_str, -1, NULL)) {
690       debug_print("Notification plugin: String is still not valid utf8, "
691                   "sanitizing...\n");
692       utf8_str = g_malloc(strlen(text)*2+1);
693       conv_localetodisp(utf8_str, strlen(text)*2+1, text);
694     }
695   }
696   else {
697     debug_print("Notification plugin: String is valid utf8\n");
698     utf8_str = g_strdup(text);
699   }
700   return utf8_str;
701 }
702 #endif