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 */