add support for Ayatana indicator to Notification plugin
[claws.git] / src / plugins / notification / notification_core.c
blob5e28d0e357437ceb12e4a3299f695b8cf905eaed
1 /* Notification plugin for Claws Mail
2 * Copyright (C) 2005-2007 Holger Berndt
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.
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.
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/>.
18 #ifdef HAVE_CONFIG_H
19 # include "config.h"
20 # include "claws-features.h"
21 #endif
23 #include "folder.h"
24 #include "folderview.h"
25 #include "codeconv.h"
26 #include "gtk/gtkutils.h"
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"
38 #ifdef HAVE_LIBCANBERRA_GTK
39 # include <canberra-gtk.h>
40 #endif
41 #ifdef NOTIFICATION_AYATANA_INDICATOR
42 # include "notification_ayatana_indicator.h"
43 #endif
45 typedef struct {
46 GSList *collected_msgs;
47 GSList *folder_items;
48 gboolean unread_also;
49 gint max_msgs;
50 gint num_msgs;
51 } TraverseCollect;
53 static gboolean notification_traverse_collect(GNode*, gpointer);
54 static void notification_new_unnotified_do_msg(MsgInfo*);
55 static gboolean notification_traverse_hash_startup(GNode*, gpointer);
57 static GHashTable *msg_count_hash;
58 static NotificationMsgCount msg_count;
60 #ifdef HAVE_LIBCANBERRA_GTK
61 static gboolean canberra_new_email_is_playing = FALSE;
62 #endif
64 static void msg_count_hash_update_func(FolderItem*, gpointer);
65 static void msg_count_update_from_hash(gpointer, gpointer, gpointer);
66 static void msg_count_clear(NotificationMsgCount*);
67 static void msg_count_add(NotificationMsgCount*,NotificationMsgCount*);
68 static void msg_count_copy(NotificationMsgCount*,NotificationMsgCount*);
70 void notification_core_global_includes_changed(void)
72 #ifdef NOTIFICATION_BANNER
73 notification_update_banner();
74 #endif
76 if(msg_count_hash) {
77 g_hash_table_destroy(msg_count_hash);
78 msg_count_hash = NULL;
80 notification_update_msg_counts(NULL);
83 /* Hide/show main window */
84 void notification_toggle_hide_show_window(void)
86 MainWindow *mainwin;
87 GdkWindow *gdkwin;
89 if((mainwin = mainwindow_get_mainwindow()) == NULL)
90 return;
92 gdkwin = gtk_widget_get_window(GTK_WIDGET(mainwin->window));
93 if(gtk_widget_get_visible(GTK_WIDGET(mainwin->window))) {
94 if((gdk_window_get_state(gdkwin) & GDK_WINDOW_STATE_ICONIFIED)
95 || mainwindow_is_obscured()) {
96 notification_show_mainwindow(mainwin);
98 else {
99 main_window_hide(mainwin);
102 else {
103 notification_show_mainwindow(mainwin);
107 void notification_update_msg_counts(FolderItem *removed_item)
109 if(!msg_count_hash)
110 msg_count_hash = g_hash_table_new_full(g_str_hash,g_str_equal,
111 g_free,g_free);
113 folder_func_to_all_folders(msg_count_hash_update_func, msg_count_hash);
115 if(removed_item) {
116 gchar *identifier;
117 identifier = folder_item_get_identifier(removed_item);
118 if(identifier) {
119 g_hash_table_remove(msg_count_hash, identifier);
120 g_free(identifier);
123 msg_count_clear(&msg_count);
124 g_hash_table_foreach(msg_count_hash, msg_count_update_from_hash, NULL);
125 #ifdef NOTIFICATION_LCDPROC
126 notification_update_lcdproc();
127 #endif
128 #ifdef NOTIFICATION_TRAYICON
129 notification_update_trayicon();
130 #endif
131 #ifdef NOTIFICATION_AYATANA_INDICATOR
132 notification_update_ayatana_indicator();
133 #endif
134 #ifdef NOTIFICATION_INDICATOR
135 notification_update_indicator();
136 #endif
137 notification_update_urgency_hint();
140 static void msg_count_clear(NotificationMsgCount *count)
142 count->new_msgs = 0;
143 count->unread_msgs = 0;
144 count->unreadmarked_msgs = 0;
145 count->marked_msgs = 0;
146 count->total_msgs = 0;
149 /* c1 += c2 */
150 static void msg_count_add(NotificationMsgCount *c1,NotificationMsgCount *c2)
152 c1->new_msgs += c2->new_msgs;
153 c1->unread_msgs += c2->unread_msgs;
154 c1->unreadmarked_msgs += c2->unreadmarked_msgs;
155 c1->marked_msgs += c2->marked_msgs;
156 c1->total_msgs += c2->total_msgs;
159 /* c1 = c2 */
160 static void msg_count_copy(NotificationMsgCount *c1,NotificationMsgCount *c2)
162 c1->new_msgs = c2->new_msgs;
163 c1->unread_msgs = c2->unread_msgs;
164 c1->unreadmarked_msgs = c2->unreadmarked_msgs;
165 c1->marked_msgs = c2->marked_msgs;
166 c1->total_msgs = c2->total_msgs;
169 gboolean get_flat_gslist_from_nodes_traverse_func(GNode *node, gpointer data)
171 if(node->data) {
172 GSList **list = data;
173 *list = g_slist_prepend(*list, node->data);
175 return FALSE;
178 GSList* get_flat_gslist_from_nodes(GNode *node)
180 GSList *retval = NULL;
182 g_node_traverse(node, G_PRE_ORDER, G_TRAVERSE_ALL, -1, get_flat_gslist_from_nodes_traverse_func, &retval);
183 return retval;
186 void notification_core_get_msg_count_of_foldername(gchar *foldername, NotificationMsgCount *count)
188 GList *list;
189 GSList *f_list;
191 Folder *walk_folder;
192 Folder *folder = NULL;
194 for(list = folder_get_list(); list != NULL; list = list->next) {
195 walk_folder = list->data;
196 if(g_strcmp0(foldername, walk_folder->name) == 0) {
197 folder = walk_folder;
198 break;
201 if(!folder) {
202 debug_print("Notification plugin: Error: Could not find folder %s\n", foldername);
203 return;
206 msg_count_clear(count);
207 f_list = get_flat_gslist_from_nodes(folder->node);
208 notification_core_get_msg_count(f_list, count);
209 g_slist_free(f_list);
212 void notification_core_get_msg_count(GSList *folder_list,
213 NotificationMsgCount *count)
215 GSList *walk;
217 if(!folder_list)
218 msg_count_copy(count,&msg_count);
219 else {
220 msg_count_clear(count);
221 for(walk = folder_list; walk; walk = walk->next) {
222 gchar *identifier;
223 NotificationMsgCount *item_count;
224 FolderItem *item = (FolderItem*) walk->data;
225 identifier = folder_item_get_identifier(item);
226 if(identifier) {
227 item_count = g_hash_table_lookup(msg_count_hash,identifier);
228 g_free(identifier);
229 if(item_count)
230 msg_count_add(count, item_count);
236 static void msg_count_hash_update_func(FolderItem *item, gpointer data)
238 gchar *identifier;
239 NotificationMsgCount *count;
240 GHashTable *hash = data;
242 if(!notify_include_folder_type(item->folder->klass->type,
243 item->folder->klass->uistr))
244 return;
246 identifier = folder_item_get_identifier(item);
247 if(!identifier)
248 return;
250 count = g_hash_table_lookup(hash, identifier);
252 if(!count) {
253 count = g_new0(NotificationMsgCount,1);
254 g_hash_table_insert(hash, identifier, count);
256 else
257 g_free(identifier);
259 count->new_msgs = item->new_msgs;
260 count->unread_msgs = item->unread_msgs;
261 count->unreadmarked_msgs = item->unreadmarked_msgs;
262 count->marked_msgs = item->marked_msgs;
263 count->total_msgs = item->total_msgs;
266 static void msg_count_update_from_hash(gpointer key, gpointer value,
267 gpointer data)
269 NotificationMsgCount *count = value;
270 msg_count_add(&msg_count,count);
274 /* Replacement for the post-filtering hook:
275 Pseudocode by Colin:
276 hook on FOLDER_ITEM_UPDATE_HOOKLIST
277 if hook flags & F_ITEM_UPDATE_MSGCOUNT
278 scan mails (folder_item_get_msg_list)
279 if MSG_IS_NEW(msginfo->flags) and not in hashtable
280 notify()
281 add to hashtable
282 procmsg_msg_list_free
284 hook on MSGINFO_UPDATE_HOOKLIST
285 if hook flags & MSGINFO_UPDATE_FLAGS
286 if !MSG_IS_NEW(msginfo->flags)
287 remove from hashtable, it's now useless
290 /* This hash table holds all mails that we already notified about,
291 and that still are marked as "new". The keys are the msgid's,
292 the values are just 1's stored in a pointer. */
293 static GHashTable *notified_hash = NULL;
296 /* Remove message from the notified_hash if
297 * - the message flags changed
298 * - the message is not new
299 * - the message is in the hash
301 gboolean notification_notified_hash_msginfo_update(MsgInfoUpdate *msg_update)
303 g_return_val_if_fail(msg_update != NULL, FALSE);
305 if((msg_update->flags & MSGINFO_UPDATE_FLAGS) &&
306 !MSG_IS_NEW(msg_update->msginfo->flags)) {
308 MsgInfo *msg;
309 gchar *msgid;
311 msg = msg_update->msginfo;
312 if(msg->msgid)
313 msgid = msg->msgid;
314 else {
315 debug_print("Notification Plugin: Message has no message ID!\n");
316 msgid = "";
319 g_return_val_if_fail(msg != NULL, FALSE);
321 if(g_hash_table_lookup(notified_hash, msgid) != NULL) {
323 debug_print("Notification Plugin: Removing message id %s from hash "
324 "table\n", msgid);
325 g_hash_table_remove(notified_hash, msgid);
328 return FALSE;
331 /* On startup, mark all new mails as already notified
332 * (by including them in the hash) */
333 void notification_notified_hash_startup_init(void)
335 GList *folder_list, *walk;
336 Folder *folder;
338 if(!notified_hash) {
339 notified_hash = g_hash_table_new_full(g_str_hash, g_str_equal,
340 g_free, NULL);
341 debug_print("Notification Plugin: Hash table created\n");
344 folder_list = folder_get_list();
345 for(walk = folder_list; walk != NULL; walk = g_list_next(walk)) {
346 folder = walk->data;
348 g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
349 notification_traverse_hash_startup, NULL);
353 static gboolean notification_traverse_hash_startup(GNode *node, gpointer data)
355 GSList *walk;
356 GSList *msg_list;
357 FolderItem *item = (FolderItem*) node->data;
358 gint new_msgs_left;
360 if(!(item->new_msgs))
361 return FALSE;
363 new_msgs_left = item->new_msgs;
364 msg_list = folder_item_get_msg_list(item);
366 for(walk = msg_list; walk; walk = g_slist_next(walk)) {
367 MsgInfo *msg = (MsgInfo*) walk->data;
368 if(MSG_IS_NEW(msg->flags)) {
369 gchar *msgid;
371 if(msg->msgid)
372 msgid = msg->msgid;
373 else {
374 debug_print("Notification Plugin: Message has no message ID!\n");
375 msgid = "";
378 /* If the message id is not yet in the hash, add it */
379 g_hash_table_insert(notified_hash, g_strdup(msgid),
380 GINT_TO_POINTER(1));
381 debug_print("Notification Plugin: Init: Added msg id %s to the hash\n",
382 msgid);
383 /* Decrement left count and check if we're already done */
384 new_msgs_left--;
385 if(new_msgs_left == 0)
386 break;
389 procmsg_msg_list_free(msg_list);
390 return FALSE;
393 void notification_core_free(void)
395 if(notified_hash) {
396 g_hash_table_destroy(notified_hash);
397 notified_hash = NULL;
399 if(msg_count_hash) {
400 g_hash_table_destroy(msg_count_hash);
401 msg_count_hash = NULL;
403 debug_print("Notification Plugin: Freed internal data\n");
406 void notification_new_unnotified_msgs(FolderItemUpdateData *update_data)
408 GSList *msg_list, *walk;
410 g_return_if_fail(notified_hash != NULL);
412 msg_list = folder_item_get_msg_list(update_data->item);
414 for(walk = msg_list; walk; walk = g_slist_next(walk)) {
415 MsgInfo *msg;
416 msg = (MsgInfo*) walk->data;
418 if(MSG_IS_NEW(msg->flags)) {
419 gchar *msgid;
421 if(msg->msgid)
422 msgid = msg->msgid;
423 else {
424 debug_print("Notification Plugin: Message has not message ID!\n");
425 msgid = "";
428 debug_print("Notification Plugin: Found msg %s, "
429 "checking if it is in hash...\n", msgid);
430 /* Check if message is in hash table */
431 if(g_hash_table_lookup(notified_hash, msgid) != NULL)
432 debug_print("yes.\n");
433 else {
434 /* Add to hashtable */
435 g_hash_table_insert(notified_hash, g_strdup(msgid),
436 GINT_TO_POINTER(1));
437 debug_print("no, added to table.\n");
439 /* Do the notification */
440 notification_new_unnotified_do_msg(msg);
443 } /* msg is 'new' */
444 } /* for all messages */
445 procmsg_msg_list_free(msg_list);
448 #ifdef HAVE_LIBCANBERRA_GTK
449 static void canberra_finished_cb(ca_context *c, uint32_t id, int error, void *data)
451 canberra_new_email_is_playing = FALSE;
453 #endif
455 static void notification_new_unnotified_do_msg(MsgInfo *msg)
457 #ifdef NOTIFICATION_POPUP
458 notification_popup_msg(msg);
459 #endif
460 #ifdef NOTIFICATION_COMMAND
461 notification_command_msg(msg);
462 #endif
463 #ifdef NOTIFICATION_TRAYICON
464 notification_trayicon_msg(msg);
465 #endif
467 #ifdef HAVE_LIBCANBERRA_GTK
468 /* canberra */
469 if(notify_config.canberra_play_sounds && !canberra_new_email_is_playing) {
470 ca_proplist *proplist;
471 ca_proplist_create(&proplist);
472 ca_proplist_sets(proplist,CA_PROP_EVENT_ID ,"message-new-email");
473 canberra_new_email_is_playing = TRUE;
474 ca_context_play_full(ca_gtk_context_get(), 0, proplist, canberra_finished_cb, NULL);
475 ca_proplist_destroy(proplist);
477 #endif
480 /* If folders is not NULL, then consider only those folder items
481 * If max_msgs is not 0, stop after collecting msg_msgs messages
483 GSList* notification_collect_msgs(gboolean unread_also, GSList *folder_items,
484 gint max_msgs)
486 GList *folder_list, *walk;
487 Folder *folder;
488 TraverseCollect collect_data;
490 collect_data.unread_also = unread_also;
491 collect_data.collected_msgs = NULL;
492 collect_data.folder_items = folder_items;
493 collect_data.max_msgs = max_msgs;
494 collect_data.num_msgs = 0;
496 folder_list = folder_get_list();
497 for(walk = folder_list; walk != NULL; walk = g_list_next(walk)) {
498 folder = walk->data;
500 g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
501 notification_traverse_collect, &collect_data);
503 return collect_data.collected_msgs;
506 void notification_collected_msgs_free(GSList *collected_msgs)
508 if(collected_msgs) {
509 GSList *walk;
510 for(walk = collected_msgs; walk != NULL; walk = g_slist_next(walk)) {
511 CollectedMsg *msg = walk->data;
512 if(msg->from)
513 g_free(msg->from);
514 if(msg->subject)
515 g_free(msg->subject);
516 if(msg->folderitem_name)
517 g_free(msg->folderitem_name);
518 msg->msginfo = NULL;
519 g_free(msg);
521 g_slist_free(collected_msgs);
525 static gboolean notification_traverse_collect(GNode *node, gpointer data)
527 TraverseCollect *cdata = data;
528 FolderItem *item = node->data;
529 gchar *folder_id_cur;
531 /* Obey global folder type limitations */
532 if(!notify_include_folder_type(item->folder->klass->type,
533 item->folder->klass->uistr))
534 return FALSE;
536 /* If a folder_items list was given, check it first */
537 if((cdata->folder_items) && (item->path != NULL) &&
538 ((folder_id_cur = folder_item_get_identifier(item)) != NULL)) {
539 FolderItem *list_item;
540 GSList *walk;
541 gchar *folder_id_list;
542 gboolean eq;
543 gboolean folder_in_list = FALSE;
545 for(walk = cdata->folder_items; walk != NULL; walk = g_slist_next(walk)) {
546 list_item = walk->data;
547 folder_id_list = folder_item_get_identifier(list_item);
548 eq = !g_strcmp0(folder_id_list,folder_id_cur);
549 g_free(folder_id_list);
550 if(eq) {
551 folder_in_list = TRUE;
552 break;
555 g_free(folder_id_cur);
556 if(!folder_in_list)
557 return FALSE;
560 if(item->new_msgs || (cdata->unread_also && item->unread_msgs)) {
561 GSList *msg_list = folder_item_get_msg_list(item);
562 GSList *walk;
563 for(walk = msg_list; walk != NULL; walk = g_slist_next(walk)) {
564 MsgInfo *msg_info = walk->data;
565 CollectedMsg *cmsg;
567 if((cdata->max_msgs != 0) && (cdata->num_msgs >= cdata->max_msgs))
568 return FALSE;
570 if(MSG_IS_NEW(msg_info->flags) ||
571 (MSG_IS_UNREAD(msg_info->flags) && cdata->unread_also)) {
573 cmsg = g_new(CollectedMsg, 1);
574 cmsg->from = g_strdup(msg_info->from ? msg_info->from : "");
575 cmsg->subject = g_strdup(msg_info->subject ? msg_info->subject : "");
576 if(msg_info->folder && msg_info->folder->name)
577 cmsg->folderitem_name = g_strdup(msg_info->folder->path);
578 else
579 cmsg->folderitem_name = g_strdup("");
581 cmsg->msginfo = msg_info;
583 cdata->collected_msgs = g_slist_prepend(cdata->collected_msgs, cmsg);
584 cdata->num_msgs++;
587 procmsg_msg_list_free(msg_list);
590 return FALSE;
593 gboolean notify_include_folder_type(FolderType ftype, gchar *uistr)
595 gboolean retval;
597 retval = FALSE;
598 switch(ftype) {
599 case F_MH:
600 case F_MBOX:
601 case F_MAILDIR:
602 case F_IMAP:
603 if(notify_config.include_mail)
604 retval = TRUE;
605 break;
606 case F_NEWS:
607 if(notify_config.include_news)
608 retval = TRUE;
609 break;
610 case F_UNKNOWN:
611 if(uistr == NULL)
612 retval = FALSE;
613 else if(!strcmp(uistr, "vCalendar")) {
614 if(notify_config.include_calendar)
615 retval = TRUE;
617 else if(!strcmp(uistr, "RSSyl")) {
618 if(notify_config.include_rss)
619 retval = TRUE;
621 else
622 debug_print("Notification Plugin: Unknown folder type %d\n",ftype);
623 break;
624 default:
625 debug_print("Notification Plugin: Unknown folder type %d\n",ftype);
628 return retval;
631 static void fix_folderview_scroll(MainWindow *mainwin)
633 static gboolean fix_done = FALSE;
635 if (fix_done)
636 return;
638 gtk_widget_queue_resize(mainwin->folderview->ctree);
640 fix_done = TRUE;
643 void notification_show_mainwindow(MainWindow *mainwin)
645 gtk_window_deiconify(GTK_WINDOW(mainwin->window));
646 gtk_window_set_skip_taskbar_hint(GTK_WINDOW(mainwin->window), FALSE);
647 main_window_show(mainwin);
648 gtk_window_present(GTK_WINDOW(mainwin->window));
649 fix_folderview_scroll(mainwin);
652 #ifdef HAVE_LIBNOTIFY
653 #define STR_MAX_LEN 511
654 /* Returns a newly allocated string which needs to be freed */
655 gchar* notification_libnotify_sanitize_str(gchar *in)
657 gint out;
658 gchar tmp_str[STR_MAX_LEN+1];
660 if(in == NULL) return NULL;
662 out = 0;
663 while(*in) {
664 if(*in == '<') {
665 if(out+4 > STR_MAX_LEN) break;
666 memcpy(&(tmp_str[out]),"&lt;",4);
667 in++; out += 4;
669 else if(*in == '>') {
670 if(out+4 > STR_MAX_LEN) break;
671 memcpy(&(tmp_str[out]),"&gt;",4);
672 in++; out += 4;
674 else if(*in == '&') {
675 if(out+5 > STR_MAX_LEN) break;
676 memcpy(&(tmp_str[out]),"&amp;",5);
677 in++; out += 5;
679 else {
680 if(out+1 > STR_MAX_LEN) break;
681 tmp_str[out++] = *in++;
684 tmp_str[out] = '\0';
685 return strdup(tmp_str);
688 gchar* notification_validate_utf8_str(gchar *text)
690 gchar *utf8_str = NULL;
692 if(!g_utf8_validate(text, -1, NULL)) {
693 debug_print("Notification plugin: String is not valid utf8, "
694 "trying to fix it...\n");
695 /* fix it */
696 utf8_str = conv_codeset_strdup(text,
697 conv_get_locale_charset_str_no_utf8(),
698 CS_INTERNAL);
699 /* check if the fix worked */
700 if(utf8_str == NULL || !g_utf8_validate(utf8_str, -1, NULL)) {
701 debug_print("Notification plugin: String is still not valid utf8, "
702 "sanitizing...\n");
703 utf8_str = g_malloc(strlen(text)*2+1);
704 conv_localetodisp(utf8_str, strlen(text)*2+1, text);
707 else {
708 debug_print("Notification plugin: String is valid utf8\n");
709 utf8_str = g_strdup(text);
711 return utf8_str;
713 #endif