/* Notification plugin for Claws-Mail * Copyright (C) 2005-2007 Holger Berndt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "pluginconfig.h" #include "folder.h" #include "codeconv.h" #include "notification_prefs.h" #include "notification_core.h" #include "notification_banner.h" #include "notification_popup.h" #include "notification_command.h" #include "notification_lcdproc.h" #include "notification_trayicon.h" typedef struct { GSList *collected_msgs; GSList *folder_items; gboolean unread_also; gint max_msgs; gint num_msgs; } TraverseCollect; static gboolean notification_traverse_collect(GNode*, gpointer); static void notification_new_unnotified_do_msg(MsgInfo*); static gboolean notification_traverse_hash_startup(GNode*, gpointer); void notification_update_msg_counts(FolderItem *removed_item) { guint unread_msgs; guint new_msgs; guint unread_marked_msgs; guint marked_msgs; guint total_msgs; folder_count_total_msgs(&new_msgs, &unread_msgs, &unread_marked_msgs, &marked_msgs, &total_msgs); if(removed_item) { total_msgs -= removed_item->total_msgs; new_msgs -= removed_item->new_msgs; unread_msgs -= removed_item->unread_msgs; } #ifdef NOTIFICATION_LCDPROC notification_update_lcdproc(new_msgs, unread_msgs, total_msgs); #endif #ifdef NOTIFICATION_TRAYICON notification_update_trayicon(new_msgs, unread_msgs, unread_marked_msgs, marked_msgs, total_msgs); #endif } /* Replacement for the post-filtering hook: Pseudocode by Colin: hook on FOLDER_ITEM_UPDATE_HOOKLIST if hook flags & F_ITEM_UPDATE_MSGCOUNT scan mails (folder_item_get_msg_list) if MSG_IS_NEW(msginfo->flags) and not in hashtable notify() add to hashtable procmsg_msg_list_free hook on MSGINFO_UPDATE_HOOKLIST if hook flags & MSGINFO_UPDATE_FLAGS if !MSG_IS_NEW(msginfo->flags) remove from hashtable, it's now useless */ /* This hash table holds all mails that we already notified about, and that still are marked as "new". The keys are the msgid's, the values are just 1's stored in a pointer. */ static GHashTable *notified_hash = NULL; /* Remove message from the notified_hash if * - the message flags changed * - the message is not new * - the message is in the hash */ gboolean notification_notified_hash_msginfo_update(MsgInfoUpdate *msg_update) { g_return_val_if_fail(msg_update != NULL, FALSE); if((msg_update->flags & MSGINFO_UPDATE_FLAGS) && !MSG_IS_NEW(msg_update->msginfo->flags)) { MsgInfo *msg; gchar *msgid; msg = msg_update->msginfo; if(msg->msgid) msgid = msg->msgid; else { debug_print("Notification Plugin: Message has no message ID!\n"); msgid = ""; } g_return_val_if_fail(msg != NULL, FALSE); if(g_hash_table_lookup(notified_hash, msgid) != NULL) { debug_print("Notification Plugin: Removing message id %s from hash " "table\n", msgid); g_hash_table_remove(notified_hash, msgid); } } return FALSE; } /* On startup, mark all new mails as already notified * (by including them in the hash) */ void notification_notified_hash_startup_init(void) { GList *folder_list, *walk; Folder *folder; if(!notified_hash) { notified_hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); debug_print("Notification Plugin: Hash table created\n"); } folder_list = folder_get_list(); for(walk = folder_list; walk != NULL; walk = g_list_next(walk)) { folder = walk->data; g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1, notification_traverse_hash_startup, NULL); } } static gboolean notification_traverse_hash_startup(GNode *node, gpointer data) { GSList *walk; GSList *msg_list; FolderItem *item = (FolderItem*) node->data; gint new_msgs_left; if(!(item->new_msgs)) return FALSE; new_msgs_left = item->new_msgs; msg_list = folder_item_get_msg_list(item); for(walk = msg_list; walk; walk = g_slist_next(walk)) { MsgInfo *msg = (MsgInfo*) walk->data; if(MSG_IS_NEW(msg->flags)) { gchar *msgid; if(msg->msgid) msgid = msg->msgid; else { debug_print("Notification Plugin: Message has no message ID!\n"); msgid = ""; } /* If the message id is not yet in the hash, add it */ g_hash_table_insert(notified_hash, g_strdup(msgid), GINT_TO_POINTER(1)); debug_print("Notification Plugin: Init: Added msg id %s to the hash\n", msgid); /* Decrement left count and check if we're already done */ new_msgs_left--; if(new_msgs_left == 0) break; } } procmsg_msg_list_free(msg_list); return FALSE; } void notification_notified_hash_free(void) { if(notified_hash) { g_hash_table_destroy(notified_hash); notified_hash = NULL; debug_print("Notification Plugin: Hash table destroyed\n"); } } void notification_new_unnotified_msgs(FolderItemUpdateData *update_data) { GSList *msg_list, *walk; g_return_if_fail(notified_hash != NULL); msg_list = folder_item_get_msg_list(update_data->item); for(walk = msg_list; walk; walk = g_slist_next(walk)) { MsgInfo *msg; msg = (MsgInfo*) walk->data; if(MSG_IS_NEW(msg->flags)) { gchar *msgid; if(msg->msgid) msgid = msg->msgid; else { debug_print("Notification Plugin: Message has not message ID!\n"); msgid = ""; } debug_print("Notification Plugin: Found msg %s, " "checking if it is in hash...\n", msgid); /* Check if message is in hash table */ if(g_hash_table_lookup(notified_hash, msgid) != NULL) debug_print("yes.\n"); else { /* Add to hashtable */ g_hash_table_insert(notified_hash, g_strdup(msgid), GINT_TO_POINTER(1)); debug_print("no, added to table.\n"); /* Do the notification */ notification_new_unnotified_do_msg(msg); } } /* msg is 'new' */ } /* for all messages */ procmsg_msg_list_free(msg_list); } static void notification_new_unnotified_do_msg(MsgInfo *msg) { #ifdef NOTIFICATION_POPUP notification_popup_msg(msg); #endif #ifdef NOTIFICATION_COMMAND notification_command_msg(msg); #endif #ifdef NOTIFICATION_TRAYICON notification_trayicon_msg(msg); #endif } /* If folders is not NULL, then consider only those folder items * If max_msgs is not 0, stop after collecting msg_msgs messages */ GSList* notification_collect_msgs(gboolean unread_also, GSList *folder_items, gint max_msgs) { GList *folder_list, *walk; Folder *folder; TraverseCollect collect_data; collect_data.unread_also = unread_also; collect_data.collected_msgs = NULL; collect_data.folder_items = folder_items; collect_data.max_msgs = max_msgs; collect_data.num_msgs = 0; folder_list = folder_get_list(); for(walk = folder_list; walk != NULL; walk = g_list_next(walk)) { folder = walk->data; g_node_traverse(folder->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1, notification_traverse_collect, &collect_data); } return collect_data.collected_msgs; } void notification_collected_msgs_free(GSList *collected_msgs) { if(collected_msgs) { GSList *walk; for(walk = collected_msgs; walk != NULL; walk = g_slist_next(walk)) { CollectedMsg *msg = walk->data; if(msg->from) g_free(msg->from); if(msg->subject) g_free(msg->subject); if(msg->folderitem_name) g_free(msg->folderitem_name); g_free(msg); } g_slist_free(collected_msgs); } } static gboolean notification_traverse_collect(GNode *node, gpointer data) { TraverseCollect *cdata = data; FolderItem *item = node->data; gchar *folder_id_cur; /* Obey global folder type limitations */ if(!notify_include_folder_type(item->folder->klass->type, item->folder->klass->uistr)) return FALSE; /* If a folder_items list was given, check it first */ if((cdata->folder_items) && (item->path != NULL) && ((folder_id_cur = folder_item_get_identifier(item)) != NULL)) { FolderItem *list_item; GSList *walk; gchar *folder_id_list; gboolean eq; gboolean folder_in_list = FALSE; for(walk = cdata->folder_items; walk != NULL; walk = g_slist_next(walk)) { list_item = walk->data; folder_id_list = folder_item_get_identifier(list_item); eq = !strcmp2(folder_id_list,folder_id_cur); g_free(folder_id_list); if(eq) { folder_in_list = TRUE; break; } } g_free(folder_id_cur); if(!folder_in_list) return FALSE; } if(item->new_msgs || (cdata->unread_also && item->unread_msgs)) { GSList *msg_list = folder_item_get_msg_list(item); GSList *walk; for(walk = msg_list; walk != NULL; walk = g_slist_next(walk)) { MsgInfo *msg_info = walk->data; CollectedMsg *cmsg; if((cdata->max_msgs != 0) && (cdata->num_msgs >= cdata->max_msgs)) return FALSE; if(MSG_IS_NEW(msg_info->flags) || (MSG_IS_UNREAD(msg_info->flags) && cdata->unread_also)) { cmsg = g_new(CollectedMsg, 1); cmsg->from = g_strdup(msg_info->from ? msg_info->from : ""); cmsg->subject = g_strdup(msg_info->subject ? msg_info->subject : ""); if(msg_info->folder && msg_info->folder->name) cmsg->folderitem_name = g_strdup(msg_info->folder->path); else cmsg->folderitem_name = g_strdup(""); cdata->collected_msgs = g_slist_prepend(cdata->collected_msgs, cmsg); cdata->num_msgs++; } } procmsg_msg_list_free(msg_list); } return FALSE; } gboolean notify_include_folder_type(FolderType ftype, gchar *uistr) { gboolean retval; retval = FALSE; switch(ftype) { case F_MH: case F_MBOX: case F_MAILDIR: case F_IMAP: if(notify_config.include_mail) retval = TRUE; break; case F_NEWS: if(notify_config.include_news) retval = TRUE; break; case F_UNKNOWN: if(uistr == NULL) retval = FALSE; else if(!strcmp(uistr, "vCalendar")) { if(notify_config.include_calendar) retval = TRUE; } else if(!strcmp(uistr, "RSSyl")) { if(notify_config.include_rss) retval = TRUE; } else debug_print("Notification Plugin: Unknown folder type %d\n",ftype); break; default: debug_print("Notification Plugin: Unknown folder type %d\n",ftype); } return retval; } #ifdef HAVE_LIBNOTIFY #define STR_MAX_LEN 511 /* Returns a newly allocated string which needs to be freed */ gchar* notification_libnotify_sanitize_str(gchar *in) { gint i_out; gchar tmp_str[STR_MAX_LEN+1]; if(in == NULL) return NULL; i_out = 0; while(*in) { if(*in == '<') { if(i_out+3 >= STR_MAX_LEN) break; memcpy(&(tmp_str[i_out]),"<",4); in++; i_out += 4; } else if(*in == '>') { if(i_out+3 >= STR_MAX_LEN) break; memcpy(&(tmp_str[i_out]),">",4); in++; i_out += 4; } else if(*in == '&') { if(i_out+4 >= STR_MAX_LEN) break; memcpy(&(tmp_str[i_out]),"&",5); in++; i_out += 5; } else { if(i_out >= STR_MAX_LEN) break; tmp_str[i_out++] = *in++; } } tmp_str[i_out] = '\0'; return strdup(tmp_str); } gchar* notification_validate_utf8_str(gchar *text) { gchar *utf8_str = NULL; if(!g_utf8_validate(text, -1, NULL)) { debug_print("Notification plugin: String is not valid utf8, " "trying to fix it...\n"); /* fix it */ utf8_str = conv_codeset_strdup(text, conv_get_locale_charset_str_no_utf8(), CS_INTERNAL); /* check if the fix worked */ if(utf8_str == NULL || !g_utf8_validate(utf8_str, -1, NULL)) { debug_print("Notification plugin: String is still not valid utf8, " "sanitizing...\n"); utf8_str = g_malloc(strlen(text)*2+1); conv_localetodisp(utf8_str, strlen(text)*2+1, text); } } else { debug_print("Notification plugin: String is valid utf8\n"); utf8_str = g_strdup(text); } return utf8_str; } #endif