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