mail-notification

Fork of Jean-Yves Lefort's mail-notification, a tray icon to notify of new mail
git clone https://code.djc.id.au/git/mail-notification/

src/mn-message.gob (15132B) - raw

      1 /*
      2  * Mail Notification
      3  * Copyright (C) 2003-2008 Jean-Yves Lefort <jylefort@brutele.be>
      4  *
      5  * This program is free software; you can redistribute it and/or modify
      6  * it under the terms of the GNU General Public License as published by
      7  * the Free Software Foundation; either version 3 of the License, or
      8  * (at your option) any later version.
      9  *
     10  * This program is distributed in the hope that it will be useful,
     11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
     12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13  * GNU General Public License for more details.
     14  *
     15  * You should have received a copy of the GNU General Public License along
     16  * with this program; if not, write to the Free Software Foundation, Inc.,
     17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
     18  */
     19 
     20 %headertop{
     21 #include <time.h>
     22 #include "mn-mailbox.h"
     23 %}
     24 
     25 %h{
     26 typedef enum
     27 {
     28   MN_MESSAGE_NEW	= 1 << 0	/* unseen message */
     29 } MNMessageFlags;
     30 
     31 typedef struct _MNMessageAction MNMessageAction;
     32 
     33 typedef void (*MNMessageActionResultCallback) (MNMessageAction *action, GError *err, gpointer data);
     34 
     35 typedef struct
     36 {
     37   MNMessageAction		*action;
     38   MNMessage			*message;
     39   MNMessageActionResultCallback	callback;
     40   gpointer			data;
     41 } MNMessageActionRequest;
     42 
     43 struct _MNMessageAction
     44 {
     45   const char	*name;
     46   const char	*icon;
     47   const char	*label;
     48   const char	*error_message;
     49 
     50   gboolean (*can_perform) (MNMessage *message);
     51   void (*perform) (MNMessage *message, MNMessageActionRequest *request);
     52   void (*done) (MNMessage *message, GError *err);
     53 };
     54 
     55 #define MN_MESSAGE_ACTION_ERROR		(mn_message_action_error_quark())
     56 
     57 typedef enum
     58 {
     59   MN_MESSAGE_ACTION_ERROR_OTHER,
     60   MN_MESSAGE_ACTION_ERROR_CANCELLED
     61 } MNMessageActionError;
     62 %}
     63 
     64 %privateheader{
     65 #include "mn-xml.h"
     66 
     67 typedef enum
     68 {
     69   /* include in the XML summary and allow as a command format */
     70   MN_MESSAGE_PARAM_EXPORT	= MN_XML_PARAM_EXPORT,
     71 } MNMessageParamFlags;
     72 
     73 typedef GError *(*MNMessageActionPerformCallback) (MNMessage *message, gpointer data);
     74 %}
     75 
     76 %{
     77 #include <errno.h>
     78 #include <glib/gi18n.h>
     79 #include <gnome.h>
     80 #include <libgnomevfs/gnome-vfs.h>
     81 #include "mn-conf.h"
     82 #include "mn-util.h"
     83 
     84 typedef struct
     85 {
     86   MNMessageActionRequest		*request;
     87   MNMessageActionPerformCallback	callback;
     88   gpointer				user_data;
     89 } PerformInfo;
     90 %}
     91 
     92 %afterdecls{
     93 static const MNMessageAction message_actions[] = {
     94   {
     95     "open",
     96     "mail-open",
     97     /* translators: header capitalization */
     98     N_("Open"),
     99     N_("Unable to open message"),
    100     self_builtin_can_open,
    101     self_builtin_open,
    102     self_open_done
    103   },
    104   {
    105     "mark-as-read",
    106     "mark",
    107     /* translators: header capitalization */
    108     N_("Mark as Read"),
    109     N_("Unable to mark message as read"),
    110     self_builtin_can_mark_as_read,
    111     self_builtin_mark_as_read,
    112     self_mark_as_read_done
    113   },
    114   {
    115     "mark-as-spam",
    116     "spam",
    117     /* translators: header capitalization */
    118     N_("Mark as Spam"),
    119     N_("Unable to mark message as spam"),
    120     self_builtin_can_mark_as_spam,
    121     self_builtin_mark_as_spam,
    122     self_mark_as_spam_done
    123   },
    124   {
    125     "delete",
    126     "delete",
    127     /* translators: header capitalization */
    128     N_("Delete"),
    129     N_("Unable to mark message as spam"),
    130     self_builtin_can_delete,
    131     self_builtin_delete,
    132     self_delete_done
    133   }
    134 };
    135 %}
    136 
    137 class MN:Message from G:Object
    138 {
    139   /*
    140    * In order to not create reference cycles, we do not hold a
    141    * reference to the mailbox. The code is arranged so that a message
    142    * cannot survive its containing mailbox (whenever the mailbox is
    143    * removed, subsystems handle the messages-changed signal and
    144    * dereference the mailbox messages).
    145    */
    146   public MNMailbox *mailbox;
    147   property POINTER mailbox (flags = CONSTRUCT_ONLY, link, type = MNMailbox *);
    148 
    149   /* sent time, may be 0 */
    150   public time_t sent_time;
    151   property ULONG sent_time (link, flags = CONSTRUCT_ONLY | MN_MESSAGE_PARAM_EXPORT, link, type = time_t);
    152 
    153   /*
    154    * The application-wise message identifier. It is used by various
    155    * subsystems to test the equality of two messages (the MNMessage
    156    * instance pointers cannot be compared since they can change across
    157    * checks).
    158    *
    159    * Uniqueness is highly desired but not required. Nothing
    160    * catastrophical will happen if an ID clash occurs.
    161    *
    162    * This field is never NULL.
    163    */
    164   public char *id destroywith g_free;
    165   property STRING id (link, flags = CONSTRUCT_ONLY | MN_MESSAGE_PARAM_EXPORT);
    166 
    167   /*
    168    * The mailbox-wise message identifier. It is used by MNMailbox to
    169    * cache the message. It is not cached using the application-wise ID
    170    * because for most backends, retrieving that ID requires to read
    171    * the message, which obviously defeats the purpose of caching.
    172    *
    173    * If set, it should be unique across all the messages of the
    174    * containing mailbox, but nothing catastrophical will happen if an
    175    * ID clash occurs.
    176    *
    177    * If NULL, the message will not be cached.
    178    */
    179   public char *mid destroywith g_free;
    180   property STRING mid (link, flags = CONSTRUCT_ONLY);
    181 
    182   /* always set */
    183   public char *from destroywith g_free;
    184   property STRING from (link, flags = CONSTRUCT_ONLY | MN_MESSAGE_PARAM_EXPORT);
    185 
    186   /* always set */
    187   public char *subject destroywith g_free;
    188   property STRING subject (link, flags = CONSTRUCT_ONLY | MN_MESSAGE_PARAM_EXPORT);
    189 
    190   /* may be NULL */
    191   public char *uri destroywith g_free;
    192   property STRING uri (link, flags = CONSTRUCT_ONLY | MN_MESSAGE_PARAM_EXPORT);
    193 
    194   /* may be NULL */
    195   property STRING filename (flags = MN_MESSAGE_PARAM_EXPORT)
    196     get {
    197       g_value_take_string(VAL, self->uri ? gnome_vfs_get_local_path_from_uri(self->uri) : NULL);
    198     };
    199 
    200   public MNMessageFlags flags;
    201   property UINT flags (link, flags = CONSTRUCT_ONLY);
    202 
    203   public MNMessageAction *
    204     get_action (const char *name (check null))
    205   {
    206     static GHashTable *actions = NULL;
    207 
    208     if (! actions)
    209       {
    210 	int i;
    211 
    212 	actions = g_hash_table_new(g_str_hash, g_str_equal);
    213 
    214 	for (i = 0; i < G_N_ELEMENTS(message_actions); i++)
    215 	  {
    216 	    const MNMessageAction *action = &message_actions[i];
    217 
    218 	    g_hash_table_insert(actions, (gpointer) action->name, (gpointer) action);
    219 	  }
    220       }
    221 
    222     return g_hash_table_lookup(actions, name);
    223   }
    224 
    225   constructor (self)
    226   {
    227     g_assert(MN_IS_MAILBOX(self->mailbox));
    228 
    229     if (! self->id)
    230       {
    231 	GString *id;
    232 
    233 	/* no ID was provided, try to generate a persistent one */
    234 
    235 	id = g_string_new(NULL);
    236 
    237 	if (self->sent_time > 0)
    238 	  g_string_append_printf(id, ":sent-time:%i:", (int) self->sent_time);
    239 	if (self->from)
    240 	  g_string_append_printf(id, ":from:%s:", self->from);
    241 	if (self->subject)
    242 	  g_string_append_printf(id, ":subject:%s:", self->subject);
    243 
    244 	if (! *id->str)
    245 	  {
    246 	    static int unique = 0;
    247 
    248 	    /*
    249 	     * We could not generate a persistent ID. Fallback to a
    250 	     * non-persistent one.
    251 	     */
    252 
    253 	    g_string_append_printf(id, "%i", g_atomic_int_exchange_and_add(&unique, 1));
    254 	  }
    255 
    256 	self->id = g_string_free(id, FALSE);
    257       }
    258 
    259     /* these fields must only be filled after we have generated an ID */
    260 
    261     if (! self->from)
    262       self->from = g_strdup("");
    263     if (! self->subject)
    264       self->subject = g_strdup("");
    265   }
    266 
    267   private gboolean
    268     subst_command_cb (const char *name, char **value, gpointer data)
    269   {
    270     Self *self = data;
    271     GParamSpec **properties;
    272     unsigned int n_properties;
    273     gboolean status = FALSE;
    274     int i;
    275 
    276     properties = g_object_class_list_properties(G_OBJECT_GET_CLASS(self), &n_properties);
    277     for (i = 0; i < n_properties; i++)
    278       if ((properties[i]->flags & MN_MESSAGE_PARAM_EXPORT) != 0
    279 	  && ! strcmp(g_param_spec_get_name(properties[i]), name))
    280 	{
    281 	  GValue gvalue = { 0, };
    282 
    283 	  g_value_init(&gvalue, G_PARAM_SPEC_VALUE_TYPE(properties[i]));
    284 	  g_object_get_property(G_OBJECT(self), name, &gvalue);
    285 
    286 	  *value = mn_g_value_to_string(&gvalue);
    287 	  g_value_unset(&gvalue);
    288 
    289 	  status = TRUE;
    290 	  break;
    291 	}
    292     g_free(properties);
    293 
    294     return status;
    295   }
    296 
    297   private gboolean
    298     execute_command_real (self,
    299 			  const char *command (check null),
    300 			  GError **err)
    301   {
    302     char *subst;
    303     int status;
    304 
    305     subst = mn_subst_command(command, self_subst_command_cb, self, err);
    306     if (! subst)
    307       return FALSE;
    308 
    309     status = gnome_execute_shell(NULL, subst);
    310     g_free(subst);
    311 
    312     if (status < 0)
    313       {
    314 	g_set_error(err, 0, 0, "%s", g_strerror(errno));
    315 	return FALSE;
    316       }
    317 
    318     return TRUE;
    319   }
    320 
    321   /*
    322    * Returns TRUE if a custom action was found. Sets @err is the
    323    * execution of the custom action failed.
    324    */
    325   private gboolean
    326     execute_command (self, const char *id (check null), GError **err)
    327   {
    328     char *command;
    329     GError *tmp_err = NULL;
    330 
    331     command = mn_mailbox_get_command(self->mailbox, id);
    332     if (! command)
    333       return FALSE;
    334 
    335     if (! self_execute_command_real(self, command, &tmp_err))
    336       {
    337 	g_set_error(err, 0, 0, _("Unable to execute \"%s\": %s."), command, tmp_err->message);
    338 	g_error_free(tmp_err);
    339       }
    340 
    341     g_free(command);
    342     return TRUE;
    343   }
    344 
    345   public gboolean
    346     can_perform_action (self, MNMessageAction *action (check null))
    347   {
    348     return mn_mailbox_has_command(self->mailbox, action->name)
    349       || action->can_perform(self);
    350   }
    351 
    352   public void
    353     perform_action (self,
    354 		    MNMessageAction *action (check null),
    355 		    MNMessageActionResultCallback callback (check null),
    356 		    gpointer data)
    357   {
    358     GError *err = NULL;
    359 
    360     if (self_execute_command(self, action->name, &err))
    361       self_action_done_real(self, action, err, callback, data);
    362     else
    363       {
    364 	MNMessageActionRequest *request;
    365 
    366 	request = g_new0(MNMessageActionRequest, 1);
    367 	request->message = g_object_ref(self);
    368 	request->action = action;
    369 	request->callback = callback;
    370 	request->data = data;
    371 
    372 	action->perform(self, request);
    373       }
    374   }
    375 
    376   protected void
    377     perform_action_in_thread (MNMessageActionRequest *request (check null),
    378 			      MNMessageActionPerformCallback callback (check null),
    379 			      gpointer user_data)
    380   {
    381     PerformInfo *info;
    382 
    383     info = g_new0(PerformInfo, 1);
    384     info->request = request;
    385     info->callback = callback;
    386     info->user_data = user_data;
    387 
    388     g_object_ref(request->message);
    389     g_object_ref(request->message->mailbox);
    390 
    391     mn_thread_create((GThreadFunc) self_perform_action_in_thread_cb, info);
    392   }
    393 
    394   private void
    395     perform_action_in_thread_cb (PerformInfo *info)
    396   {
    397     GError *err;
    398 
    399     err = info->callback(info->request->message, info->user_data);
    400 
    401     GDK_THREADS_ENTER();
    402 
    403     self_action_done(info->request, err);
    404 
    405     g_object_unref(info->request->message->mailbox);
    406     g_object_unref(info->request->message);
    407 
    408     gdk_flush();
    409     GDK_THREADS_LEAVE();
    410   }
    411 
    412   private void
    413     action_done_real (self,
    414 		      MNMessageAction *action (check null),
    415 		      GError *err,
    416 		      MNMessageActionResultCallback callback,
    417 		      gpointer data)
    418   {
    419     action->done(self, err);
    420     callback(action, err, data);
    421   }
    422 
    423   protected void
    424     action_done (MNMessageActionRequest *request (check null), GError *err)
    425   {
    426     Self *self = request->message;
    427 
    428     self_action_done_real(self, request->action, err, request->callback, request->data);
    429 
    430     g_object_unref(request->message);
    431     g_free(request);
    432   }
    433 
    434   public GQuark
    435     action_error_quark (void)
    436   {
    437     return g_quark_from_static_string("mn-message-action-error");
    438   }
    439 
    440   virtual private gboolean
    441     builtin_can_open (self)
    442   {
    443     return self->uri != NULL;
    444   }
    445 
    446   virtual private void
    447     builtin_open (self, MNMessageActionRequest *request)
    448   {
    449     GError *err = NULL;
    450 
    451     gnome_url_show(self->uri, &err);
    452 
    453     self_action_done(request, err);
    454   }
    455 
    456   private void
    457     open_done (self, GError *err)
    458   {
    459     if (! err)
    460       self_consider_as_read(self); /* [1] */
    461   }
    462 
    463   virtual private gboolean
    464     builtin_can_mark_as_read (self)
    465   {
    466     return SELF_GET_CLASS(self)->builtin_mark_as_read != NULL;
    467   }
    468 
    469   virtual private void
    470     builtin_mark_as_read (self, MNMessageActionRequest *request);
    471 
    472   private void
    473     mark_as_read_done (self, GError *err)
    474   {
    475     if (! err)
    476       self_consider_as_read(self); /* [1] */
    477   }
    478 
    479   virtual private gboolean
    480     builtin_can_mark_as_spam (self)
    481   {
    482     return SELF_GET_CLASS(self)->builtin_mark_as_spam != NULL;
    483   }
    484 
    485   virtual private void
    486     builtin_mark_as_spam (self, MNMessageActionRequest *request);
    487 
    488   private void
    489     mark_as_spam_done (self, GError *err)
    490   {
    491     if (! err)
    492       self_consider_as_read(self); /* [1] */
    493   }
    494 
    495   virtual private gboolean
    496     builtin_can_delete (self)
    497   {
    498     return SELF_GET_CLASS(self)->builtin_delete != NULL;
    499   }
    500 
    501   virtual private void
    502     builtin_delete (self, MNMessageActionRequest *request);
    503 
    504   private void
    505     delete_done (self, GError *err)
    506   {
    507     if (! err)
    508       self_consider_as_read(self); /* [1] */
    509   }
    510 
    511   public void
    512     consider_as_read (self)
    513   {
    514     GSList *list;
    515     GSList *l;
    516     gboolean exists = FALSE;
    517 
    518     list = mn_conf_get_string_list(MN_CONF_MESSAGES_CONSIDERED_AS_READ);
    519 
    520     MN_LIST_FOREACH(l, list)
    521       {
    522 	const char *id = l->data;
    523 
    524 	if (! strcmp(id, self->id))
    525 	  {
    526 	    exists = TRUE;
    527 	    break;
    528 	  }
    529       }
    530 
    531     if (! exists)
    532       {
    533 	list = g_slist_prepend(list, g_strdup(self->id));
    534 
    535 	mn_conf_set_string_list(MN_CONF_MESSAGES_CONSIDERED_AS_READ, list);
    536       }
    537 
    538     mn_g_slist_free_deep(list);
    539   }
    540 
    541   /*
    542    * Atomically considers a list of messages as read, setting the
    543    * GConf list only once rather than for each message.
    544    */
    545   public void
    546     consider_as_read_list (GList *messages)
    547   {
    548     GHashTable *set;
    549     unsigned int old_size;
    550     GList *l;
    551 
    552     set = mn_conf_get_string_hash_set(MN_CONF_MESSAGES_CONSIDERED_AS_READ);
    553 
    554     old_size = g_hash_table_size(set);
    555 
    556     MN_LIST_FOREACH(l, messages)
    557       {
    558 	MNMessage *message = l->data;
    559 
    560 	g_hash_table_replace(set, g_strdup(message->id), GINT_TO_POINTER(TRUE));
    561       }
    562 
    563     if (g_hash_table_size(set) != old_size)
    564       mn_conf_set_string_hash_set(MN_CONF_MESSAGES_CONSIDERED_AS_READ, set);
    565 
    566     g_hash_table_destroy(set);
    567   }
    568 
    569   public MNMessage *
    570     new (MN:Mailbox *mailbox (check null type),
    571 	 time_t sent_time,
    572 	 const char *id,
    573 	 const char *mid,
    574 	 const char *from,
    575 	 const char *subject,
    576 	 const char *uri,
    577 	 MNMessageFlags flags)
    578   {
    579     return GET_NEW_VARG(MN_MESSAGE_PROP_MAILBOX(mailbox),
    580 			MN_MESSAGE_PROP_SENT_TIME(sent_time),
    581 			MN_MESSAGE_PROP_ID((char *) id),
    582 			MN_MESSAGE_PROP_MID((char *) mid),
    583 			MN_MESSAGE_PROP_FROM((char *) from),
    584 			MN_MESSAGE_PROP_SUBJECT((char *) subject),
    585 			MN_MESSAGE_PROP_URI((char *) uri),
    586 			MN_MESSAGE_PROP_FLAGS(flags),
    587 			NULL);
    588   }
    589 
    590   public xmlNode *
    591     xml_node_new (self)
    592   {
    593     xmlNode *node;
    594 
    595     node = xmlNewNode(NULL, "message");
    596 
    597     xmlSetProp(node, "mailbox", self->mailbox->runtime_name);
    598 
    599     if ((self->flags & MN_MESSAGE_NEW) != 0)
    600       xmlSetProp(node, "new", "true");
    601 
    602     mn_xml_export_properties(G_OBJECT(self), node);
    603 
    604     return node;
    605   }
    606 }
    607 
    608 /*
    609  * [1]: there can be a slight (or large if polling is in effect) delay
    610  * between executing an action which should cause a message to
    611  * disappear from MN (open it, mark it as read, etc) and having the
    612  * next mail check catch the change. By adding the message to the
    613  * considered-as-read GConf list, this delay is concealed from the
    614  * user.
    615  */