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