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-mailbox.gob (31247B) - 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 <stdarg.h>
     22 #include <time.h>
     23 #include <libxml/tree.h>
     24 #include "mn-decls.h"
     25 %}
     26 
     27 %h{
     28 #define MN_MAILBOX_MAX_TYPES	8
     29 extern GType mn_mailbox_types[MN_MAILBOX_MAX_TYPES + 1];
     30 
     31 typedef struct _MNMailboxConfiguration MNMailboxConfiguration;
     32 %}
     33 
     34 %privateheader{
     35 #include "mn-xml.h"
     36 
     37 typedef enum
     38 {
     39   /* load from mailboxes.xml */
     40   MN_MAILBOX_PARAM_LOAD		= MN_XML_PARAM_IMPORT,
     41 
     42   /* save to mailboxes.xml */
     43   MN_MAILBOX_PARAM_SAVE		= MN_XML_PARAM_EXPORT,
     44 
     45   /* load from and save to mailboxes.xml */
     46   MN_MAILBOX_PARAM_LOAD_SAVE	= MN_MAILBOX_PARAM_LOAD | MN_MAILBOX_PARAM_SAVE,
     47 
     48   /* ignore the case of a string property when checking if it has the default value */
     49   MN_MAILBOX_PARAM_IGNORE_CASE	= MN_XML_PARAM_IGNORE_CASE,
     50 
     51   /* require a string property to be non-empty */
     52   MN_MAILBOX_PARAM_REQUIRED	= 1 << (MN_XML_PARAM_USER_SHIFT + 0),
     53 } MNMailboxParamFlags;
     54 %}
     55 
     56 %{
     57 #include <glib/gi18n.h>
     58 #include <libgnomevfs/gnome-vfs.h>
     59 #if WITH_MBOX || WITH_MOZILLA || WITH_MH || WITH_MAILDIR || WITH_SYLPHEED
     60 #include "mn-system-vfs-mailbox.h"
     61 #include "mn-custom-vfs-mailbox.h"
     62 #endif
     63 #if WITH_POP3
     64 #include "mn-pop3-mailbox.h"
     65 #endif
     66 #if WITH_IMAP
     67 #include "mn-imap-mailbox.h"
     68 #endif
     69 #if WITH_GMAIL
     70 #include "mn-gmail-mailbox.h"
     71 #endif
     72 #if WITH_YAHOO
     73 #include "mn-yahoo-mailbox.h"
     74 #endif
     75 #if WITH_HOTMAIL
     76 #include "mn-hotmail-mailbox.h"
     77 #endif
     78 #if WITH_EVOLUTION
     79 #include "mn-evolution-mailbox.h"
     80 #endif
     81 #include "mn-util.h"
     82 #include "mn-message.h"
     83 #include "mn-conf.h"
     84 #include "mn-locked-callback.h"
     85 #include "mn-shell.h"
     86 
     87 struct _MNMailboxConfiguration
     88 {
     89   GType		type;
     90   unsigned int	n_parameters;
     91   GParameter	*parameters;
     92 };
     93 
     94 GType mn_mailbox_types[MN_MAILBOX_MAX_TYPES + 1];
     95 
     96 typedef struct
     97 {
     98   MNMailbox	*self;
     99   GHashTable	*messages;
    100   GHashTable	*messages_considered_as_read;
    101   gboolean	display_seen_mail;
    102 } FilterMessagesInfo;
    103 
    104 typedef struct
    105 {
    106   GHashTable	*other;
    107   gboolean	changed;
    108 } CompareMessagesInfo;
    109 
    110 static unsigned int cleanup_messages_considered_as_read_idle_id = 0;
    111 %}
    112 
    113 class MN:Mailbox from G:Object (abstract)
    114 {
    115   classwide const char *type;
    116   classwide int default_check_delay = -1;
    117 
    118   /*
    119    * If enabled, the base MNMailbox implementation of added() will
    120    * call mn_mailbox_enable_checking().
    121    */
    122   classwide gboolean enable_checking_when_added = TRUE;
    123 
    124   /*
    125    * Whether the mailbox is in the mailboxes list (a mailbox can be
    126    * removed from the mailboxes list without being finalized
    127    * immediately if a threaded check is in progress).
    128    *
    129    * Used for performance (testing it is faster than searching the
    130    * list) and for thread safety. Do not access directly, use the
    131    * accessors.
    132    */
    133   private gboolean _active;
    134 
    135   public gboolean
    136     get_active (self)
    137   {
    138     return g_atomic_int_get(&selfp->_active);
    139   }
    140 
    141   private void
    142     set_active (self, gboolean value)
    143   {
    144     g_atomic_int_set(&selfp->_active, value);
    145   }
    146 
    147   /**
    148    * added:
    149    * @self: the object which received the signal
    150    *
    151    * This signal gets emitted after the mailbox is added to the
    152    * mailboxes list.
    153    **/
    154   signal NONE (NONE)
    155     void added (self)
    156   {
    157     self_set_active(self, TRUE);
    158 
    159     if (SELF_GET_CLASS(self)->enable_checking_when_added)
    160       self_enable_checking(self);
    161   }
    162 
    163   /**
    164    * removed:
    165    * @self: the object which received the signal
    166    *
    167    * This signal gets emitted after the mailbox is removed from the
    168    * mailboxes list.
    169    **/
    170   signal NONE (NONE)
    171     void removed (self)
    172   {
    173     self_set_active(self, FALSE);
    174 
    175     mn_source_clear(&selfp->check_timeout_id);
    176 
    177     /*
    178      * Do not queue a cleanup of the messages-considered-as-read GConf
    179      * setting: it should not be done if the mailbox is only being
    180      * replaced (eg. from mn_mailbox_properties_dialog_apply()).
    181      *
    182      * If however the mailbox is being permanently removed, its
    183      * messages considered as read will be cleaned up the next time
    184      * another mailbox is checked, which is good enough.
    185      *
    186      * Note that we could queue a cleanup from here by adding and
    187      * testing a "gboolean replacing" signal parameter, but it is not
    188      * worth the effort.
    189      */
    190   }
    191 
    192   public char *runtime_name destroywith g_free;
    193 
    194   public char *name destroywith g_free;
    195   property STRING name (link, flags = MN_MAILBOX_PARAM_LOAD_SAVE);
    196 
    197   public char *open_command destroywith g_free;
    198   property STRING open_command (link, flags = MN_MAILBOX_PARAM_LOAD_SAVE);
    199 
    200   public char *mark_as_read_command destroywith g_free;
    201   property STRING mark_as_read_command (link, flags = MN_MAILBOX_PARAM_LOAD_SAVE);
    202 
    203   public char *mark_as_spam_command destroywith g_free;
    204   property STRING mark_as_spam_command (link, flags = MN_MAILBOX_PARAM_LOAD_SAVE);
    205 
    206   public char *delete_command destroywith g_free;
    207   property STRING delete_command (link, flags = MN_MAILBOX_PARAM_LOAD_SAVE);
    208 
    209   public char *stock_id destroywith g_free;
    210   property STRING stock_id (link, export);
    211 
    212   public char *format destroywith g_free;
    213   property STRING format (link, export);
    214 
    215   private unsigned int check_timeout_id;
    216 
    217   public int runtime_check_delay;
    218 
    219   public int check_delay;
    220   property INT check_delay (link,
    221 			    flags = CONSTRUCT | MN_MAILBOX_PARAM_LOAD_SAVE,
    222 			    default_value = -1);
    223 
    224   private gboolean poll = TRUE;
    225   property BOOLEAN poll (export)
    226     set
    227     {
    228       gboolean new_poll = g_value_get_boolean(VAL);
    229 
    230       /*
    231        * We do nothing unless the property has changed, because we do
    232        * not want to reset an already existing check timeout.
    233        */
    234       if (new_poll != selfp->poll)
    235 	{
    236 	  selfp->poll = new_poll;
    237 	  if (self_get_active(self) && selfp->checking_enabled)
    238 	    self_update_check_timeout(self);
    239 
    240 	  g_object_notify(G_OBJECT(self), "manually-checkable");
    241 	}
    242     }
    243     get
    244     {
    245       g_value_set_boolean(VAL, selfp->poll);
    246     };
    247 
    248   /*
    249    * Whether the user can initiate a check manually (for instance by
    250    * clicking on an Update menu item or by running mail-notification
    251    * --update).
    252    */
    253   property BOOLEAN manually_checkable (export)
    254     get
    255     {
    256       g_value_set_boolean(VAL, selfp->checking_enabled && selfp->poll);
    257     };
    258 
    259   private void
    260     update_check_timeout (self)
    261   {
    262     g_assert(self_get_active(self) == TRUE);
    263     g_assert(selfp->checking_enabled == TRUE);
    264 
    265     mn_source_clear(&selfp->check_timeout_id);
    266     if (selfp->poll && self->runtime_check_delay > 0)
    267       selfp->check_timeout_id = gdk_threads_add_timeout(self->runtime_check_delay * 1000, self_check_timeout_cb, self);
    268   }
    269 
    270   /* whether set_messages() has ever been called */
    271   private gboolean all_messages_set;
    272 
    273   /* all unread (unseen and seen) messages, indexed by id */
    274   private GHashTable *all_messages = {g_hash_table_new_full(g_str_hash, g_str_equal, NULL, (GDestroyNotify) g_object_unref)} destroywith g_hash_table_destroy;
    275 
    276   /* all_messages, indexed by mid (message references held by all_message) */
    277   private GHashTable *all_messages_by_mid = {g_hash_table_new(g_str_hash, g_str_equal)} destroywith g_hash_table_destroy;
    278 
    279   /* all_messages after applying the "seen messages" filter, indexed by id */
    280   public GHashTable *messages = {g_hash_table_new_full(g_str_hash, g_str_equal, NULL, (GDestroyNotify) g_object_unref)} destroywith g_hash_table_destroy;
    281 
    282   /* for performance */
    283   public time_t timestamp; /* timestamp of most recent message in self->messages */
    284 
    285   property POINTER messages (type = GHashTable *)
    286     get
    287     {
    288       g_value_set_pointer(VAL, self->messages);
    289     };
    290 
    291   protected void
    292     set_messages (self, GSList *messages)
    293   {
    294     GSList *l;
    295 
    296     selfp->all_messages_set = TRUE;
    297 
    298     g_hash_table_remove_all(selfp->all_messages);
    299     g_hash_table_remove_all(selfp->all_messages_by_mid);
    300 
    301     MN_LIST_FOREACH(l, messages)
    302       {
    303 	MNMessage *message = l->data;
    304 
    305 	g_hash_table_replace(selfp->all_messages, message->id, g_object_ref(message));
    306 
    307 	if (message->mid)
    308 	  /* do not ref message, it is owned by all_messages */
    309 	  g_hash_table_replace(selfp->all_messages_by_mid, message->mid, message);
    310       }
    311 
    312     /*
    313      * Some messages might be gone, queue a cleanup of the
    314      * considered-as-read GConf setting.
    315      */
    316     self_queue_cleanup_messages_considered_as_read();
    317 
    318     self_filter_messages(self);
    319   }
    320 
    321   /**
    322    * filter_messages:
    323    * @self: the mailbox to act upon
    324    *
    325    * Filters @self->all_messages with the "seen mail" filter and the
    326    * considered-as-read GConf list, and stores the resulting set in
    327    * @self->messages. Additionally, if @self->messages has changed,
    328    * emits the "messages-changed" signal.
    329    **/
    330   private void
    331     filter_messages (self)
    332   {
    333     FilterMessagesInfo info;
    334     gboolean changed = FALSE;
    335     gboolean has_new = FALSE;
    336 
    337     /* filter messages */
    338 
    339     self->timestamp = 0;
    340 
    341     info.self = self;
    342     info.messages = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, (GDestroyNotify) g_object_unref);
    343     info.messages_considered_as_read = mn_conf_get_string_hash_set(MN_CONF_MESSAGES_CONSIDERED_AS_READ);
    344     info.display_seen_mail = mn_conf_get_bool(MN_CONF_DISPLAY_SEEN_MAIL);
    345 
    346     g_hash_table_foreach(selfp->all_messages, (GHFunc) self_filter_messages_cb, &info);
    347 
    348     g_hash_table_destroy(info.messages_considered_as_read);
    349 
    350     /* detect changes */
    351 
    352     if (self_compare_messages(self->messages, info.messages))
    353       changed = TRUE;
    354     if (self_compare_messages(info.messages, self->messages))
    355       changed = has_new = TRUE;
    356 
    357     g_hash_table_destroy(self->messages);
    358     self->messages = info.messages;
    359 
    360     g_object_notify(G_OBJECT(self), "messages");
    361 
    362     if (changed)
    363       self_messages_changed(self, has_new);
    364   }
    365 
    366   private void
    367     filter_messages_cb (const char *id,
    368 			MNMessage *message,
    369 			FilterMessagesInfo *info)
    370   {
    371     Self *self = info->self;
    372 
    373     if (! info->display_seen_mail && (message->flags & MN_MESSAGE_NEW) == 0)
    374       return;
    375 
    376     if (g_hash_table_lookup(info->messages_considered_as_read, message->id))
    377       return;
    378 
    379     if (message->sent_time > self->timestamp)
    380       self->timestamp = message->sent_time;
    381 
    382     g_hash_table_replace(info->messages, message->id, g_object_ref(message));
    383   }
    384 
    385   /**
    386    * compare_messages:
    387    * @messages1: a %GHashTable containing %MNMessage objects
    388    * @messages2: another %GHashTable containing %MNMessage objects
    389    *
    390    * Compares @messages1 and @messages2.
    391    *
    392    * Return value: %TRUE if @messages1 contains messages not present
    393    * in @messages2, %FALSE otherwise.
    394    **/
    395   private gboolean
    396     compare_messages (GHashTable *messages1, GHashTable *messages2)
    397   {
    398     CompareMessagesInfo info;
    399 
    400     info.other = messages2;
    401     info.changed = FALSE;
    402     g_hash_table_foreach(messages1, (GHFunc) self_compare_messages_cb, &info);
    403 
    404     return info.changed;
    405   }
    406 
    407   private void
    408     compare_messages_cb (const char *id,
    409 			 MNMessage *message,
    410 			 CompareMessagesInfo *info)
    411   {
    412     if (! info->changed && ! g_hash_table_lookup(info->other, id))
    413       info->changed = TRUE;
    414   }
    415 
    416   private void
    417     queue_cleanup_messages_considered_as_read (void)
    418   {
    419     if (! cleanup_messages_considered_as_read_idle_id)
    420       cleanup_messages_considered_as_read_idle_id = gdk_threads_add_idle(self_cleanup_messages_considered_as_read_cb, NULL);
    421   }
    422 
    423   private gboolean
    424     cleanup_messages_considered_as_read_cb (gpointer data)
    425   {
    426     self_cleanup_messages_considered_as_read();
    427 
    428     cleanup_messages_considered_as_read_idle_id = 0;
    429     return FALSE;		/* remove source */
    430   }
    431 
    432   /*
    433    * Remove messages which no longer exist from the considered-as-read
    434    * GConf list.
    435    *
    436    * The primary goal is to ensure that a message which has been
    437    * opened or marked as read (these functions implicitly add the
    438    * message to the considered-as-read list) and then marked back as
    439    * unread in the mail reader will reappear in MN.
    440    *
    441    * The secondary goal is to ensure that the considered-as-list will
    442    * not grow forever.
    443    */
    444   private void
    445     cleanup_messages_considered_as_read (void)
    446   {
    447     GList *l;
    448     GHashTable *set;
    449 
    450     /*
    451      * If there is a mailbox which has not been successfully checked
    452      * yet, abort. Otherwise, the messages of that mailbox could be
    453      * mistakenly cleaned up.
    454      */
    455     MN_LIST_FOREACH(l, mn_shell->mailboxes->list)
    456       {
    457 	MNMailbox *mailbox = l->data;
    458 
    459 	if (! mailbox->_priv->all_messages_set)
    460 	  return;
    461       }
    462 
    463     set = mn_conf_get_string_hash_set(MN_CONF_MESSAGES_CONSIDERED_AS_READ);
    464 
    465     if (g_hash_table_foreach_remove(set, (GHRFunc) self_cleanup_messages_considered_as_read_remove_cb, NULL))
    466       /* one or more messages were removed, reflect the changes */
    467       mn_conf_set_string_hash_set(MN_CONF_MESSAGES_CONSIDERED_AS_READ, set);
    468 
    469     g_hash_table_destroy(set);
    470   }
    471 
    472   private gboolean
    473     cleanup_messages_considered_as_read_remove_cb (const char *id,
    474 						   gpointer value,
    475 						   gpointer user_data)
    476   {
    477     GList *l;
    478 
    479     MN_LIST_FOREACH(l, mn_shell->mailboxes->list)
    480       {
    481 	MNMailbox *mailbox = l->data;
    482 
    483 	if (g_hash_table_lookup(mailbox->_priv->all_messages, id))
    484 	  return FALSE;		/* message still exists, do not remove it */
    485       }
    486 
    487     return TRUE;		/* message no longer exists, remove it */
    488   }
    489 
    490   /**
    491    * messages-changed:
    492    * @self: the object which received the signal
    493    * @has_new: whether a new message has been received or not
    494    *
    495    * This signal gets emitted whenever the messages property changes.
    496    *
    497    * It is considered that the property changes if a new message
    498    * appears or if an old message disappears. If the contents of an
    499    * existing message change but the message keeps the same id, this
    500    * signal is not emitted (use the "notify::messages" signal if you
    501    * need notification of such events).
    502    **/
    503   signal private NONE (BOOLEAN)
    504     void messages_changed (self, gboolean has_new);
    505 
    506   public char *error destroywith g_free;
    507   property STRING error (link);
    508 
    509   protected void
    510     set_error (self, const char *format, ...)
    511     attr {G_GNUC_PRINTF(2, 3)}
    512   {
    513     char *error = NULL;
    514 
    515     if (format)
    516       MN_STRDUP_VPRINTF(error, format);
    517 
    518     g_object_set(G_OBJECT(self), MN_MAILBOX_PROP_ERROR(error), NULL);
    519     g_free(error);
    520   }
    521 
    522   public void
    523     init_types (void)
    524   {
    525     int i = 0;
    526 
    527 #if WITH_MBOX || WITH_MOZILLA || WITH_MH || WITH_MAILDIR || WITH_SYLPHEED
    528     /*
    529      * MNSystemVFSMailbox must be registered before
    530      * MNCustomVFSMailbox, because the latter's parse_uri() method
    531      * will accept any URI.
    532      */
    533     mn_mailbox_types[i++] = MN_TYPE_SYSTEM_VFS_MAILBOX;
    534     mn_mailbox_types[i++] = MN_TYPE_CUSTOM_VFS_MAILBOX;
    535 #endif
    536 #if WITH_POP3
    537     mn_mailbox_types[i++] = MN_TYPE_POP3_MAILBOX;
    538 #endif
    539 #if WITH_IMAP
    540     mn_mailbox_types[i++] = MN_TYPE_IMAP_MAILBOX;
    541 #endif
    542 #if WITH_GMAIL
    543     mn_mailbox_types[i++] = MN_TYPE_GMAIL_MAILBOX;
    544 #endif
    545 #if WITH_YAHOO
    546     mn_mailbox_types[i++] = MN_TYPE_YAHOO_MAILBOX;
    547 #endif
    548 #if WITH_HOTMAIL
    549     mn_mailbox_types[i++] = MN_TYPE_HOTMAIL_MAILBOX;
    550 #endif
    551 #if WITH_EVOLUTION
    552     mn_mailbox_types[i++] = MN_TYPE_EVOLUTION_MAILBOX;
    553 #endif
    554     mn_mailbox_types[i] = 0;
    555   }
    556 
    557   /* references the returned class */
    558   public MNMailboxClass *
    559     get_class_from_name (const char *type (check null))
    560   {
    561     int i;
    562 
    563     for (i = 0; mn_mailbox_types[i]; i++)
    564       {
    565 	SelfClass *class;
    566 
    567 	class = g_type_class_ref(mn_mailbox_types[i]);
    568 	if (! strcmp(class->type, type))
    569 	  return class;
    570 
    571 	g_type_class_unref(class);
    572       }
    573 
    574     return NULL;
    575   }
    576 
    577   public GType
    578     get_type_from_name (const char *type (check null))
    579   {
    580     SelfClass *class;
    581 
    582     class = self_get_class_from_name(type);
    583     if (class)
    584       {
    585 	GType gtype;
    586 
    587 	gtype = G_OBJECT_CLASS_TYPE(class);
    588 	g_type_class_unref(class);
    589 
    590 	return gtype;
    591       }
    592 
    593     return 0;
    594   }
    595 
    596   init (self)
    597   {
    598     mn_g_object_gconf_notifications_add_gdk_locked(self,
    599 						   MN_CONF_DISPLAY_SEEN_MAIL, self_notify_display_seen_messages_cb, self,
    600 						   MN_CONF_MESSAGES_CONSIDERED_AS_READ, self_notify_messages_considered_as_read_cb, self,
    601 						   NULL);
    602   }
    603 
    604   finalize (self)
    605   {
    606     /*
    607      * Even though we clear the source in removed(), it might have
    608      * been reinstalled afterwards (by a mn_mailbox_set_poll() call
    609      * from a check thread, etc).
    610      */
    611     if (selfp->check_timeout_id)
    612       g_source_remove(selfp->check_timeout_id);
    613   }
    614 
    615   private void
    616     notify_display_seen_messages_cb (GConfClient *client,
    617 				     unsigned int cnxn_id,
    618 				     GConfEntry *entry,
    619 				     gpointer user_data)
    620   {
    621     Self *self = user_data;
    622 
    623     self_filter_messages(self);
    624   }
    625 
    626   private void
    627     notify_messages_considered_as_read_cb (GConfClient *client,
    628 					   unsigned int cnxn_id,
    629 					   GConfEntry *entry,
    630 					   gpointer user_data)
    631   {
    632     Self *self = user_data;
    633 
    634     self_filter_messages(self);
    635   }
    636 
    637   public MNMailbox *
    638     new (const char *type (check null), ...)
    639     attr {G_GNUC_NULL_TERMINATED}
    640   {
    641     va_list args;
    642     GType type_id;
    643     const char *first_property_name;
    644     GObject *object;
    645 
    646     type_id = self_get_type_from_name(type);
    647     if (! type_id)
    648       return NULL;
    649 
    650     va_start(args, type);
    651     first_property_name = va_arg(args, const char *);
    652     object = g_object_new_valist(type_id, first_property_name, args);
    653     va_end(args);
    654 
    655     return SELF(object);
    656   }
    657 
    658   public MNMailbox *
    659     new_from_xml_node (xmlNode *node (check null), GError **err)
    660   {
    661     char *type;
    662     Self *self = NULL;
    663 
    664     type = xmlGetProp(node, "type");
    665     if (! type)
    666       {
    667 	g_set_error(err, 0, 0, _("\"type\" attribute missing"));
    668 	return NULL;
    669       }
    670 
    671     self = self_new(type, NULL);
    672     if (! self)
    673       {
    674 	g_set_error(err, 0, 0, _("unknown mailbox type \"%s\""), type);
    675 	goto end;
    676       }
    677 
    678     mn_xml_import_properties(G_OBJECT(self), node);
    679 
    680     if (! self_validate(self, err))
    681       {
    682 	g_object_unref(self);
    683 	self = NULL;
    684       }
    685 
    686   end:
    687     g_free(type);
    688     return self;
    689   }
    690 
    691   public xmlNode *
    692     xml_node_new (self)
    693   {
    694     xmlNode *node;
    695 
    696     node = xmlNewNode(NULL, "mailbox");
    697     xmlSetProp(node, "type", SELF_GET_CLASS(self)->type);
    698     mn_xml_export_properties(G_OBJECT(self), node);
    699 
    700     return node;
    701   }
    702 
    703   public MNMailbox *
    704     new_from_uri (const char *uri (check null))
    705   {
    706     int i;
    707 
    708     for (i = 0; mn_mailbox_types[i]; i++)
    709       {
    710 	MNMailboxClass *class;
    711 	MNMailbox *mailbox;
    712 
    713 	class = g_type_class_ref(mn_mailbox_types[i]);
    714 	mailbox = class->parse_uri ? class->parse_uri(NULL, uri) : NULL;
    715 	g_type_class_unref(class);
    716 
    717 	if (mailbox)
    718 	  return mailbox;
    719       }
    720 
    721     return NULL;
    722   }
    723 
    724   public MNMailbox *
    725     new_from_configuration (MNMailboxConfiguration *config (check null))
    726   {
    727     return g_object_newv(config->type, config->n_parameters, config->parameters);
    728   }
    729 
    730   public MNMailboxConfiguration *
    731     get_configuration (self)
    732   {
    733     GObject *object = G_OBJECT(self);
    734     GArray *parameters;
    735     GParamSpec **properties;
    736     unsigned int n_properties;
    737     int i;
    738     MNMailboxConfiguration *config;
    739 
    740     parameters = g_array_new(FALSE, FALSE, sizeof(GParameter));
    741 
    742     properties = g_object_class_list_properties(G_OBJECT_GET_CLASS(self), &n_properties);
    743     for (i = 0; i < n_properties; i++)
    744       if ((properties[i]->flags & MN_MAILBOX_PARAM_SAVE) != 0)
    745 	{
    746 	  GParameter parameter = { NULL, { 0, } };
    747 
    748 	  parameter.name = g_param_spec_get_name(properties[i]);
    749 
    750 	  g_value_init(&parameter.value, G_PARAM_SPEC_VALUE_TYPE(properties[i]));
    751 	  g_object_get_property(object, parameter.name, &parameter.value);
    752 
    753 	  g_array_append_val(parameters, parameter);
    754 	}
    755     g_free(properties);
    756 
    757     config = g_new0(MNMailboxConfiguration, 1);
    758     config->type = G_OBJECT_TYPE(self);
    759     config->n_parameters = parameters->len;
    760     config->parameters = (GParameter *) g_array_free(parameters, FALSE);
    761 
    762     return config;
    763   }
    764 
    765   public void
    766     configuration_free (MNMailboxConfiguration *config (check null))
    767   {
    768     int i;
    769 
    770     for (i = 0; i < config->n_parameters; i++)
    771       g_value_unset(&config->parameters[i].value);
    772 
    773     g_free(config->parameters);
    774     g_free(config);
    775   }
    776 
    777   public MNMailbox *
    778     new_from_obsolete_uri (const char *uri (check null))
    779   {
    780     char *real_uri;
    781     char *scheme;
    782     gboolean obsolete = FALSE;
    783     Self *self = NULL;
    784 
    785     real_uri = g_str_has_prefix(uri, "pop3:") /* also handle very old pop3 locators */
    786       ? g_strconcat("pop://", uri + 5, NULL)
    787       : g_strdup(uri);
    788 
    789     scheme = gnome_vfs_get_uri_scheme(real_uri);
    790     if (scheme)
    791       {
    792 	if (! strcmp(scheme, "pop") || ! strcmp(scheme, "pops")
    793 	    || ! strcmp(scheme, "imap") || ! strcmp(scheme, "imaps")
    794 	    || ! strcmp(scheme, "gmail"))
    795 	  obsolete = TRUE;
    796 	g_free(scheme);
    797       }
    798 
    799     self = obsolete ? self_parse_obsolete_uri(real_uri) : self_new_from_uri(real_uri);
    800     g_free(real_uri);
    801 
    802     if (self && ! self_validate(self, NULL))
    803       {
    804 	g_object_unref(self);
    805 	self = NULL;
    806       }
    807 
    808     return self;
    809   }
    810 
    811   private MNMailbox *
    812     parse_obsolete_uri (const char *uri (check null))
    813   {
    814     int len;
    815     int buflen;
    816     char *scheme;
    817     char *username;
    818     char *password;
    819     char *authmech;
    820     char *hostname;
    821     int port;
    822     char *path;
    823     char **queries;
    824     Self *self = NULL;
    825 
    826     len = strlen(uri);
    827     buflen = len + 1;
    828 
    829     {
    830       char *pat;
    831       char scheme_buf[buflen];
    832       char auth_buf[buflen];
    833       char location_buf[buflen];
    834       char username_buf[buflen];
    835       char password_buf[buflen];
    836       char authmech_buf[buflen];
    837       char hostname_buf[buflen];
    838       int _port;
    839       char path_buf[buflen];
    840       char queries_buf[buflen];
    841       gboolean has_location = FALSE;
    842       gboolean has_password = FALSE;
    843       gboolean has_authmech = FALSE;
    844       gboolean has_port = FALSE;
    845       gboolean has_path = FALSE;
    846       gboolean has_queries = FALSE;
    847       int n;
    848 
    849       /* split URI in 3 parts: scheme, auth and location */
    850 
    851       pat = g_strdup_printf("%%%i[^:]://%%%i[^@]@%%%is", len, len, len);
    852       n = sscanf(uri, pat, scheme_buf, auth_buf, location_buf);
    853       g_free(pat);
    854 
    855       if (n >= 2)
    856 	{
    857 	  if (n == 3)
    858 	    has_location = TRUE;
    859 	}
    860       else
    861 	return NULL;		/* unparsable */
    862 
    863       /* split auth part in 3 subparts: username, password and authmech */
    864 
    865       /*
    866        * For backward compatibility with previous versions of Mail
    867        * Notification, we also support ;auth= (in lowercase).
    868        */
    869 
    870       pat = g_strdup_printf("%%%i[^:]:%%%i[^;];%%*1[aA]%%*1[uU]%%*1[tT]%%*1[hH]=%%%is", len, len, len);
    871       n = sscanf(auth_buf, pat, username_buf, password_buf, authmech_buf);
    872       g_free(pat);
    873 
    874       if (n >= 2)
    875 	{
    876 	  has_password = TRUE;
    877 	  if (n == 3)
    878 	    has_authmech = TRUE;
    879 	}
    880       else
    881 	{
    882 	  pat = g_strdup_printf("%%%i[^;];%%*1[aA]%%*1[uU]%%*1[tT]%%*1[hH]=%%%is", len, len);
    883 	  n = sscanf(auth_buf, pat, username_buf, authmech_buf);
    884 	  g_free(pat);
    885 
    886 	  if (n >= 1)
    887 	    {
    888 	      if (n == 2)
    889 		has_authmech = TRUE;
    890 	    }
    891 	  else
    892 	    return NULL;	/* unparsable */
    893 	}
    894 
    895       if (has_location)
    896 	{
    897 	  char hostport_buf[buflen];
    898 
    899 	  /* split location part in 3 subparts: hostport, path and queries */
    900 
    901 	  pat = g_strdup_printf("%%%i[^/]/%%%i[^?]?%%%is", len, len, len);
    902 	  n = sscanf(location_buf, pat, hostport_buf, path_buf, queries_buf);
    903 	  g_free(pat);
    904 
    905 	  if (n >= 2)
    906 	    {
    907 	      has_path = TRUE;
    908 	      if (n == 3)
    909 		has_queries = TRUE;
    910 	    }
    911 	  else
    912 	    {
    913 	      pat = g_strdup_printf("%%%i[^?]?%%%is", len, len);
    914 	      n = sscanf(location_buf, pat, hostport_buf, queries_buf);
    915 	      g_free(pat);
    916 
    917 	      if (n == 2)
    918 		has_queries = TRUE;
    919 	    }
    920 
    921 	  /* split hostport in 2 subparts: host and port */
    922 
    923 	  pat = g_strdup_printf("[%%%i[^]]]:%%u", len);
    924 	  n = sscanf(hostport_buf, pat, hostname_buf, &_port);
    925 	  g_free(pat);
    926 
    927 	  if (n < 1)
    928 	    {
    929 	      pat = g_strdup_printf("%%%i[^:]:%%u", len);
    930 	      n = sscanf(hostport_buf, pat, hostname_buf, &_port);
    931 	      g_free(pat);
    932 	    }
    933 
    934 	  if (n == 2)
    935 	    has_port = TRUE;
    936 	}
    937 
    938       scheme = gnome_vfs_unescape_string(scheme_buf, NULL);
    939       username = gnome_vfs_unescape_string(username_buf, NULL);
    940       password = has_password ? gnome_vfs_unescape_string(password_buf, NULL) : NULL;
    941       authmech = has_authmech ? gnome_vfs_unescape_string(authmech_buf, NULL) : NULL;
    942       hostname = has_location ? gnome_vfs_unescape_string(hostname_buf, NULL) : NULL;
    943       port = has_port ? _port : 0;
    944       path = has_path ? gnome_vfs_unescape_string(path_buf, NULL) : NULL;
    945       if (has_queries)
    946 	{
    947 	  int i;
    948 
    949 	  queries = g_strsplit(queries_buf, "&", 0);
    950 	  for (i = 0; queries[i]; i++)
    951 	    {
    952 	      char *unescaped;
    953 
    954 	      unescaped = gnome_vfs_unescape_string(queries[i], NULL);
    955 
    956 	      g_free(queries[i]);
    957 	      queries[i] = unescaped;
    958 	    }
    959 	}
    960       else
    961 	queries = NULL;
    962     }
    963 
    964     if (! strcmp(scheme, "pop") || ! strcmp(scheme, "pops"))
    965       {
    966 #if WITH_POP3
    967 	MNPIMailboxConnectionType connection_type;
    968 
    969 	if (queries && mn_strv_find(queries, "STLS") != -1)
    970 	  connection_type = MN_PI_MAILBOX_CONNECTION_TYPE_INBAND_SSL;
    971 	else
    972 	  connection_type = ! strcmp(scheme, "pops")
    973 	    ? MN_PI_MAILBOX_CONNECTION_TYPE_SSL
    974 	    : MN_PI_MAILBOX_CONNECTION_TYPE_NORMAL;
    975 
    976 	self = self_new("pop3",
    977 			"connection-type", connection_type,
    978 			"username", username,
    979 			"password", password,
    980 			"authmech", authmech,
    981 			"hostname", hostname,
    982 			"port", port,
    983 			NULL);
    984 #endif
    985       }
    986     else if (! strcmp(scheme, "imap") || ! strcmp(scheme, "imaps"))
    987       {
    988 #if WITH_IMAP
    989 	MNPIMailboxConnectionType connection_type;
    990 
    991 	if (queries && mn_strv_find(queries, "STARTTLS") != -1)
    992 	  connection_type = MN_PI_MAILBOX_CONNECTION_TYPE_INBAND_SSL;
    993 	else
    994 	  connection_type = ! strcmp(scheme, "imaps")
    995 	    ? MN_PI_MAILBOX_CONNECTION_TYPE_SSL
    996 	    : MN_PI_MAILBOX_CONNECTION_TYPE_NORMAL;
    997 
    998 	self = self_new("imap",
    999 			"connection-type", connection_type,
   1000 			"username", username,
   1001 			"password", password,
   1002 			"authmech", authmech,
   1003 			"hostname", hostname,
   1004 			"port", port,
   1005 			NULL);
   1006 
   1007 	if (path)
   1008 	  g_object_set(self, MN_IMAP_MAILBOX_PROP_MAILBOX(path), NULL);
   1009 
   1010 	if (queries && mn_strv_find(queries, "noidle") != -1)
   1011 	  g_object_set(G_OBJECT(self), MN_IMAP_MAILBOX_PROP_USE_IDLE_EXTENSION(MN_IMAP_MAILBOX_USE_IDLE_NEVER), NULL);
   1012 #endif
   1013       }
   1014     else if (! strcmp(scheme, "gmail"))
   1015       {
   1016 #if WITH_GMAIL
   1017 	self = self_new("gmail",
   1018 			"username", username,
   1019 			"password", password,
   1020 			NULL);
   1021 #endif
   1022       }
   1023 
   1024     g_free(scheme);
   1025     g_free(username);
   1026     g_free(password);
   1027     g_free(authmech);
   1028     g_free(hostname);
   1029     g_free(path);
   1030     g_strfreev(queries);
   1031 
   1032     return self;
   1033   }
   1034 
   1035   private gboolean
   1036     check_timeout_cb (gpointer data)
   1037   {
   1038     Self *self = data;
   1039 
   1040     self_check(self);
   1041 
   1042     return TRUE;		/* continue */
   1043   }
   1044 
   1045   private gboolean
   1046     validate (self, GError **err)
   1047   {
   1048     GParamSpec **properties;
   1049     unsigned int n_properties;
   1050     int i;
   1051     gboolean status = TRUE;
   1052 
   1053     properties = g_object_class_list_properties(G_OBJECT_GET_CLASS(self), &n_properties);
   1054     for (i = 0; i < n_properties; i++)
   1055       if ((properties[i]->flags & MN_MAILBOX_PARAM_REQUIRED) != 0)
   1056 	{
   1057 	  GValue value = { 0, };
   1058 	  const char *str;
   1059 	  gboolean is_empty;
   1060 
   1061 	  g_assert(G_IS_PARAM_SPEC_STRING(properties[i]));
   1062 
   1063 	  g_value_init(&value, G_TYPE_STRING);
   1064 	  g_object_get_property(G_OBJECT(self), g_param_spec_get_name(properties[i]), &value);
   1065 
   1066 	  str = g_value_get_string(&value);
   1067 	  is_empty = ! str || ! *str;
   1068 
   1069 	  g_value_unset(&value);
   1070 
   1071 	  if (is_empty)
   1072 	    {
   1073 	      g_set_error(err, 0, 0, _("property \"%s\" has no value"), g_param_spec_get_name(properties[i]));
   1074 	      status = FALSE;
   1075 	      break;
   1076 	    }
   1077 	}
   1078     g_free(properties);
   1079 
   1080     return status;
   1081   }
   1082 
   1083   /**
   1084    * seal:
   1085    * @self: a mailbox
   1086    *
   1087    * Seals the mailbox before it is made operational by being added to
   1088    * the mailboxes list. The point of this function is to allow
   1089    * subclasses to perform initialization which needs to consult the
   1090    * value of properties loaded from mailboxes.xml or set by the
   1091    * properties dialog. That would not be possible from init(), since
   1092    * these properties are only set after the mailbox is constructed.
   1093    **/
   1094   virtual public void
   1095     seal (self)
   1096   {
   1097     if (self->name)
   1098       {
   1099 	g_free(self->runtime_name);
   1100 	self->runtime_name = g_strdup(self->name);
   1101       }
   1102 
   1103     self->runtime_check_delay = self->check_delay != -1
   1104       ? self->check_delay
   1105       : SELF_GET_CLASS(self)->default_check_delay;
   1106   }
   1107 
   1108   //[G_GNUC_UNUSED]              /* invoked via the class pointer */
   1109   virtual private MNMailbox *
   1110     parse_uri (self, const char *uri (check null));
   1111 
   1112   virtual public void
   1113     check (self)
   1114   {
   1115     g_assert(self_get_active(self) == TRUE);
   1116     g_assert(selfp->checking_enabled == TRUE);
   1117   }
   1118 
   1119   /*
   1120    * Mailboxes start with this property disabled. As long as this
   1121    * property is disabled, the Update menu items are insensitive, the
   1122    * periodic poll timeout is not installed, and mn_mailbox_check() is
   1123    * not called. This allows subclasses to prevent checks while they
   1124    * are saving the password, etc.
   1125    */
   1126   private gboolean checking_enabled;
   1127 
   1128   protected void
   1129     enable_checking (self)
   1130   {
   1131     selfp->checking_enabled = TRUE;
   1132     g_object_notify(G_OBJECT(self), "manually-checkable");
   1133 
   1134     self_update_check_timeout(self);
   1135     self_check(self);
   1136   }
   1137 
   1138   protected void
   1139     notice (self, const char *format (check null), ...)
   1140     attr {G_GNUC_PRINTF(2, 3)}
   1141   {
   1142     char *message;
   1143 
   1144     /*
   1145      * Disregard messages sent by a threaded check still in progress
   1146      * after the mailbox has been removed.
   1147      */
   1148     if (! self_get_active(self))
   1149       return;
   1150 
   1151     MN_STRDUP_VPRINTF(message, format);
   1152     mn_info(_("%s: %s"), self->runtime_name, message);
   1153     g_free(message);
   1154   }
   1155 
   1156   protected void
   1157     warning (self, const char *format (check null), ...)
   1158     attr {G_GNUC_PRINTF(2, 3)}
   1159   {
   1160     char *message;
   1161 
   1162     /*
   1163      * Disregard messages sent by a threaded check still in progress
   1164      * after the mailbox has been removed.
   1165      */
   1166     if (! self_get_active(self))
   1167       return;
   1168 
   1169     MN_STRDUP_VPRINTF(message, format);
   1170     g_warning(_("%s: %s"), self->runtime_name, message);
   1171     g_free(message);
   1172   }
   1173 
   1174   protected MNMessage *
   1175     get_message_from_mid (self, const char *mid (check null))
   1176   {
   1177     return g_hash_table_lookup(selfp->all_messages_by_mid, mid);
   1178   }
   1179 
   1180   public char *
   1181     get_command (self, const char *id (check null))
   1182   {
   1183     char *prop;
   1184     char *command;
   1185 
   1186     prop = g_strconcat(id, "-command", NULL);
   1187     g_object_get(self, prop, &command, NULL);
   1188     g_free(prop);
   1189 
   1190     if (command && ! *command)
   1191       {
   1192 	g_free(command);
   1193 	return NULL;
   1194       }
   1195 
   1196     return command;
   1197   }
   1198 
   1199   public gboolean
   1200     has_command (self, const char *id (check null))
   1201   {
   1202     char *command;
   1203     gboolean has;
   1204 
   1205     command = self_get_command(self, id);
   1206     has = command != NULL;
   1207     g_free(command);
   1208 
   1209     return has;
   1210   }
   1211 }