/* * evolution-mdn.c * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with the program; if not, see * */ #include #include #include #include #include #include #include #include #include #include #define MDN_USER_FLAG "receipt-handled" #define E_TYPE_MDN (e_mdn_get_type ()) #define E_MDN(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), E_TYPE_MDN, EMdn)) #define E_IS_MDN(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), E_TYPE_MDN)) typedef struct _EMdn EMdn; typedef struct _EMdnClass EMdnClass; struct _EMdn { EExtension parent; gpointer alert; /* weak pointer */ }; struct _EMdnClass { EExtensionClass parent_class; }; typedef struct _MdnContext MdnContext; struct _MdnContext { ESource *source; EMailReader *reader; CamelFolder *folder; CamelMessageInfo *info; CamelMimeMessage *message; gchar *notify_to; }; typedef enum { MDN_ACTION_MODE_MANUAL, MDN_ACTION_MODE_AUTOMATIC } MdnActionMode; typedef enum { MDN_SENDING_MODE_MANUAL, MDN_SENDING_MODE_AUTOMATIC } MdnSendingMode; /* Module Entry Points */ void e_module_load (GTypeModule *type_module); void e_module_unload (GTypeModule *type_module); /* Forward Declarations */ GType e_mdn_get_type (void); G_DEFINE_DYNAMIC_TYPE (EMdn, e_mdn, E_TYPE_EXTENSION) static void mdn_context_free (MdnContext *context) { if (context->info != NULL) camel_folder_free_message_info ( context->folder, context->info); g_object_unref (context->source); g_object_unref (context->reader); g_object_unref (context->folder); g_object_unref (context->message); g_free (context->notify_to); g_slice_free (MdnContext, context); } static void mdn_remove_alert (EMdn *mdn) { g_return_if_fail (E_IS_MDN (mdn)); if (mdn->alert != NULL) e_alert_response (mdn->alert, GTK_RESPONSE_OK); } static void mdn_submit_alert (EMdn *mdn, EMailReader *reader, EAlert *alert) { EPreviewPane *preview_pane; g_return_if_fail (E_IS_MDN (mdn)); mdn_remove_alert (mdn); g_return_if_fail (mdn->alert == NULL); /* Make sure alerts are shown in the preview pane and not * wherever e_mail_reader_get_alert_sink() might show it. */ preview_pane = e_mail_reader_get_preview_pane (reader); e_alert_sink_submit_alert (E_ALERT_SINK (preview_pane), alert); mdn->alert = alert; g_object_add_weak_pointer (G_OBJECT (mdn->alert), &mdn->alert); } static gchar * mdn_get_notify_to (CamelMimeMessage *message) { CamelMedium *medium; const gchar *address; const gchar *header_name; medium = CAMEL_MEDIUM (message); header_name = "Disposition-Notification-To"; address = camel_medium_get_header (medium, header_name); /* TODO Should probably decode/format the address, * since it could be in RFC 2047 format. */ if (address != NULL) while (camel_mime_is_lwsp (*address)) address++; return g_strdup (address); } static gchar * mdn_get_disposition (MdnActionMode action_mode, MdnSendingMode sending_mode) { GString *string; string = g_string_sized_new (64); switch (action_mode) { case MDN_ACTION_MODE_MANUAL: g_string_append (string, "manual-action"); break; case MDN_ACTION_MODE_AUTOMATIC: g_string_append (string, "automatic-action"); break; default: g_warn_if_reached (); } g_string_append_c (string, '/'); switch (sending_mode) { case MDN_SENDING_MODE_MANUAL: g_string_append (string, "MDN-sent-manually"); break; case MDN_SENDING_MODE_AUTOMATIC: g_string_append (string, "MDN-sent-automatically"); break; default: g_warn_if_reached (); } g_string_append (string, ";displayed"); return g_string_free (string, FALSE); } static void mdn_receipt_done (EMailSession *session, GAsyncResult *result, gpointer user_data) { GError *error = NULL; e_mail_session_append_to_local_folder_finish ( session, result, NULL, &error); if (error == NULL) { mail_send (session); } else { /* FIXME Poor error handling. */ g_warning ("%s: %s", G_STRFUNC, error->message); g_error_free (error); } } static void mdn_notify_sender (ESource *identity_source, EMailReader *reader, CamelFolder *folder, CamelMimeMessage *message, CamelMessageInfo *info, const gchar *notify_to, MdnActionMode action_mode, MdnSendingMode sending_mode) { /* See RFC 3798 for a description of message receipts. */ CamelMimeMessage *receipt; CamelMultipart *body; CamelMimePart *part; CamelMedium *medium; CamelDataWrapper *receipt_text, *receipt_data; CamelContentType *type; CamelInternetAddress *address; CamelStream *stream; CamelMessageInfo *receipt_info; EMailBackend *backend; EMailSession *session; ESourceExtension *extension; const gchar *message_id; const gchar *message_date; const gchar *message_subject; const gchar *extension_name; const gchar *transport_uid; const gchar *self_address; const gchar *sent_folder_uri; gchar *fake_msgid; gchar *hostname; gchar *receipt_subject; gchar *disposition; gchar *recipient; gchar *content; gchar *ua; backend = e_mail_reader_get_backend (reader); session = e_mail_backend_get_session (backend); /* Tag the message immediately even though we haven't actually sent * the read receipt yet. Not a big deal if we fail to send it, and * we don't want to keep badgering the user about it. */ camel_message_info_set_user_flag (info, MDN_USER_FLAG, TRUE); medium = CAMEL_MEDIUM (message); message_id = camel_medium_get_header (medium, "Message-ID"); message_date = camel_medium_get_header (medium, "Date"); message_subject = camel_mime_message_get_subject (message); if (message_id == NULL) message_id = ""; if (message_date == NULL) message_date = ""; /* Collect information for the receipt. */ g_return_if_fail (identity_source != NULL); extension_name = E_SOURCE_EXTENSION_MAIL_IDENTITY; extension = e_source_get_extension (identity_source, extension_name); self_address = e_source_mail_identity_get_address ( E_SOURCE_MAIL_IDENTITY (extension)); extension_name = E_SOURCE_EXTENSION_MAIL_SUBMISSION; extension = e_source_get_extension (identity_source, extension_name); sent_folder_uri = e_source_mail_submission_get_sent_folder ( E_SOURCE_MAIL_SUBMISSION (extension)); transport_uid = e_source_mail_submission_get_transport_uid ( E_SOURCE_MAIL_SUBMISSION (extension)); /* We use camel_header_msgid_generate() to get a canonical * hostname, then skip the part leading to '@' */ fake_msgid = camel_header_msgid_generate (); hostname = strchr (fake_msgid, '@'); g_return_if_fail (hostname != NULL); hostname++; /* Create toplevel container. */ body = camel_multipart_new (); camel_data_wrapper_set_mime_type ( CAMEL_DATA_WRAPPER (body), "multipart/report;" "report-type=\"disposition-notification\""); camel_multipart_set_boundary (body, NULL); /* Create textual receipt. */ receipt_text = camel_data_wrapper_new (); type = camel_content_type_new ("text", "plain"); camel_content_type_set_param (type, "format", "flowed"); camel_content_type_set_param (type, "charset", "UTF-8"); camel_data_wrapper_set_mime_type_field (receipt_text, type); camel_content_type_unref (type); content = g_strdup_printf ( /* Translators: First %s is an email address, second %s * is the subject of the email, third %s is the date. */ _("Your message to %s about \"%s\" on %s has been read."), self_address, message_subject, message_date); stream = camel_stream_mem_new (); camel_stream_write_string (stream, content, NULL, NULL); camel_data_wrapper_construct_from_stream_sync ( receipt_text, stream, NULL, NULL); g_object_unref (stream); g_free (content); part = camel_mime_part_new (); camel_medium_set_content (CAMEL_MEDIUM (part), receipt_text); camel_mime_part_set_encoding ( part, CAMEL_TRANSFER_ENCODING_QUOTEDPRINTABLE); camel_multipart_add_part (body, part); g_object_unref (part); g_object_unref (receipt_text); /* Create the machine-readable receipt. */ receipt_data = camel_data_wrapper_new (); ua = g_strdup_printf ( "%s; %s", hostname, "Evolution " VERSION SUB_VERSION " " VERSION_COMMENT); recipient = g_strdup_printf ("rfc822; %s", self_address); disposition = mdn_get_disposition (action_mode, sending_mode); type = camel_content_type_new ("message", "disposition-notification"); camel_data_wrapper_set_mime_type_field (receipt_data, type); camel_content_type_unref (type); content = g_strdup_printf ( "Reporting-UA: %s\n" "Final-Recipient: %s\n" "Original-Message-ID: %s\n" "Disposition: %s\n", ua, recipient, message_id, disposition); stream = camel_stream_mem_new (); camel_stream_write_string (stream, content, NULL, NULL); camel_data_wrapper_construct_from_stream_sync ( receipt_data, stream, NULL, NULL); g_object_unref (stream); g_free (content); g_free (ua); g_free (recipient); g_free (fake_msgid); g_free (disposition); part = camel_mime_part_new (); camel_medium_set_content (CAMEL_MEDIUM (part), receipt_data); camel_mime_part_set_encoding (part, CAMEL_TRANSFER_ENCODING_7BIT); camel_multipart_add_part (body, part); g_object_unref (part); g_object_unref (receipt_data); /* Finish creating the message. */ receipt = camel_mime_message_new (); camel_medium_set_content ( CAMEL_MEDIUM (receipt), CAMEL_DATA_WRAPPER (body)); g_object_unref (body); receipt_subject = g_strdup_printf ( /* Translators: %s is the subject of the email message. */ _("Delivery Notification for \"%s\""), message_subject); camel_mime_message_set_subject (receipt, receipt_subject); g_free (receipt_subject); address = camel_internet_address_new (); camel_address_decode (CAMEL_ADDRESS (address), self_address); camel_mime_message_set_from (receipt, address); g_object_unref (address); address = camel_internet_address_new (); camel_address_decode (CAMEL_ADDRESS (address), notify_to); camel_mime_message_set_recipients ( receipt, CAMEL_RECIPIENT_TYPE_TO, address); g_object_unref (address); camel_medium_set_header ( CAMEL_MEDIUM (receipt), "Return-Path", "<>"); camel_medium_set_header ( CAMEL_MEDIUM (receipt), "X-Evolution-Identity", e_source_get_uid (identity_source)); camel_medium_set_header ( CAMEL_MEDIUM (receipt), "X-Evolution-Transport", transport_uid); camel_medium_set_header ( CAMEL_MEDIUM (receipt), "X-Evolution-Fcc", sent_folder_uri); /* RFC 3834, Section 5 describes this header. */ camel_medium_set_header ( CAMEL_MEDIUM (receipt), "Auto-Submitted", "auto-replied"); /* Send the receipt. */ receipt_info = camel_message_info_new (NULL); camel_message_info_set_flags ( receipt_info, CAMEL_MESSAGE_SEEN, CAMEL_MESSAGE_SEEN); /* FIXME Pass a GCancellable. */ e_mail_session_append_to_local_folder ( session, E_MAIL_LOCAL_FOLDER_OUTBOX, receipt, receipt_info, G_PRIORITY_DEFAULT, NULL, (GAsyncReadyCallback) mdn_receipt_done, g_object_ref (session)); camel_message_info_free (receipt_info); } static void mdn_notify_action_cb (GtkAction *action, MdnContext *context) { mdn_notify_sender ( context->source, context->reader, context->folder, context->message, context->info, context->notify_to, MDN_ACTION_MODE_MANUAL, MDN_SENDING_MODE_MANUAL); /* Make sure the newly-added user flag gets saved. */ camel_folder_free_message_info (context->folder, context->info); context->info = NULL; } static void mdn_mail_reader_changed_cb (EMailReader *reader, EMdn *mdn) { MessageList *message_list; g_return_if_fail (E_IS_MAIL_READER (reader)); message_list = MESSAGE_LIST (e_mail_reader_get_message_list (reader)); if (!message_list || message_list_selected_count (message_list) != 1) mdn_remove_alert (mdn); } static void mdn_message_loaded_cb (EMailReader *reader, const gchar *message_uid, CamelMimeMessage *message, EMdn *mdn) { EAlert *alert; ESource *source; ESourceMDN *extension; ESourceRegistry *registry; EMailBackend *backend; EMailSession *session; CamelFolder *folder; CamelMessageInfo *info; EMdnResponsePolicy response_policy; const gchar *extension_name; gchar *notify_to = NULL; backend = e_mail_reader_get_backend (reader); session = e_mail_backend_get_session (backend); registry = e_mail_session_get_registry (session); folder = e_mail_reader_ref_folder (reader); mdn_remove_alert (mdn); info = camel_folder_get_message_info (folder, message_uid); if (info == NULL) goto exit; if (camel_message_info_user_flag (info, MDN_USER_FLAG)) { alert = e_alert_new ("mdn:sender-notified", NULL); mdn_submit_alert (mdn, reader, alert); g_object_unref (alert); goto exit; } notify_to = mdn_get_notify_to (message); if (notify_to == NULL) goto exit; /* Do not show the notice in special folders. */ if (em_utils_folder_is_drafts (registry, folder)) goto exit; if (em_utils_folder_is_templates (registry, folder)) goto exit; if (em_utils_folder_is_sent (registry, folder)) goto exit; if (em_utils_folder_is_outbox (registry, folder)) goto exit; /* This returns a new ESource reference. */ source = em_utils_guess_mail_identity_with_recipients ( registry, message, folder, message_uid); if (source == NULL) goto exit; extension_name = E_SOURCE_EXTENSION_MDN; extension = e_source_get_extension (source, extension_name); response_policy = e_source_mdn_get_response_policy (extension); if (response_policy == E_MDN_RESPONSE_POLICY_ASK) { MdnContext *context; GtkAction *action; gchar *tooltip; context = g_slice_new0 (MdnContext); context->source = g_object_ref (source); context->reader = g_object_ref (reader); context->folder = g_object_ref (folder); context->message = g_object_ref (message); context->info = camel_message_info_ref (info); context->notify_to = notify_to; notify_to = NULL; tooltip = g_strdup_printf ( _("Send a read receipt to '%s'"), context->notify_to); action = gtk_action_new ( "notify-sender", /* name doesn't matter */ _("_Notify Sender"), tooltip, NULL); g_signal_connect_data ( action, "activate", G_CALLBACK (mdn_notify_action_cb), context, (GClosureNotify) mdn_context_free, (GConnectFlags) 0); alert = e_alert_new ("mdn:notify-sender", NULL); e_alert_add_action (alert, action, GTK_RESPONSE_APPLY); mdn_submit_alert (mdn, reader, alert); g_object_unref (alert); g_object_unref (action); g_free (tooltip); } g_object_unref (source); exit: if (info != NULL) camel_folder_free_message_info (folder, info); g_clear_object (&folder); g_free (notify_to); } static void mdn_message_seen_cb (EMailReader *reader, const gchar *message_uid, CamelMimeMessage *message) { ESource *source; ESourceMDN *extension; ESourceRegistry *registry; EMailBackend *backend; EMailSession *session; CamelFolder *folder; CamelMessageInfo *info; EMdnResponsePolicy response_policy; const gchar *extension_name; gchar *notify_to = NULL; backend = e_mail_reader_get_backend (reader); session = e_mail_backend_get_session (backend); registry = e_mail_session_get_registry (session); folder = e_mail_reader_ref_folder (reader); info = camel_folder_get_message_info (folder, message_uid); if (info == NULL) goto exit; if (camel_message_info_user_flag (info, MDN_USER_FLAG)) goto exit; notify_to = mdn_get_notify_to (message); if (notify_to == NULL) goto exit; /* This returns a new ESource reference. */ source = em_utils_guess_mail_identity_with_recipients ( registry, message, folder, message_uid); if (source == NULL) goto exit; extension_name = E_SOURCE_EXTENSION_MDN; extension = e_source_get_extension (source, extension_name); response_policy = e_source_mdn_get_response_policy (extension); if (response_policy == E_MDN_RESPONSE_POLICY_ALWAYS) mdn_notify_sender ( source, reader, folder, message, info, notify_to, MDN_ACTION_MODE_AUTOMATIC, MDN_SENDING_MODE_AUTOMATIC); g_object_unref (source); exit: if (info != NULL) camel_folder_free_message_info (folder, info); g_clear_object (&folder); g_free (notify_to); } static void mdn_constructed (GObject *object) { EExtension *extension; EExtensible *extensible; extension = E_EXTENSION (object); extensible = e_extension_get_extensible (extension); g_return_if_fail (E_IS_MAIL_READER (extensible)); g_signal_connect ( extensible, "changed", G_CALLBACK (mdn_mail_reader_changed_cb), extension); g_signal_connect ( extensible, "message-loaded", G_CALLBACK (mdn_message_loaded_cb), extension); g_signal_connect ( extensible, "message-seen", G_CALLBACK (mdn_message_seen_cb), NULL); /* Chain up to parent's constructed() method. */ G_OBJECT_CLASS (e_mdn_parent_class)->constructed (object); } static void mdn_dispose (GObject *object) { mdn_remove_alert (E_MDN (object)); /* Chain up to parent's dispose() method. */ G_OBJECT_CLASS (e_mdn_parent_class)->dispose (object); } static void e_mdn_class_init (EMdnClass *class) { GObjectClass *object_class; EExtensionClass *extension_class; object_class = G_OBJECT_CLASS (class); object_class->constructed = mdn_constructed; object_class->dispose = mdn_dispose; extension_class = E_EXTENSION_CLASS (class); extension_class->extensible_type = E_TYPE_MAIL_READER; } static void e_mdn_class_finalize (EMdnClass *class) { } static void e_mdn_init (EMdn *extension) { } G_MODULE_EXPORT void e_module_load (GTypeModule *type_module) { e_mdn_register_type (type_module); } G_MODULE_EXPORT void e_module_unload (GTypeModule *type_module) { }