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