/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ /* * 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 * * * Authors: * Ettore Perazzoli (ettore@ximian.com) * Jeffrey Stedfast (fejj@ximian.com) * Miguel de Icaza (miguel@ximian.com) * Radek Doulik (rodo@ximian.com) * * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) * */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "e-composer-private.h" typedef struct _AsyncContext AsyncContext; struct _AsyncContext { EActivity *activity; CamelMimeMessage *message; CamelDataWrapper *top_level_part; CamelDataWrapper *text_plain_part; EAccount *account; CamelSession *session; CamelInternetAddress *from; CamelTransferEncoding plain_encoding; GtkPrintOperationAction print_action; GPtrArray *recipients; guint skip_content : 1; guint need_thread : 1; guint pgp_sign : 1; guint pgp_encrypt : 1; guint smime_sign : 1; guint smime_encrypt : 1; }; /* Flags for building a message. */ typedef enum { COMPOSER_FLAG_HTML_CONTENT = 1 << 0, COMPOSER_FLAG_SAVE_OBJECT_DATA = 1 << 1, COMPOSER_FLAG_PRIORITIZE_MESSAGE = 1 << 2, COMPOSER_FLAG_REQUEST_READ_RECEIPT = 1 << 3, COMPOSER_FLAG_PGP_SIGN = 1 << 4, COMPOSER_FLAG_PGP_ENCRYPT = 1 << 5, COMPOSER_FLAG_SMIME_SIGN = 1 << 6, COMPOSER_FLAG_SMIME_ENCRYPT = 1 << 7 } ComposerFlags; enum { PROP_0, PROP_FOCUS_TRACKER, PROP_SHELL }; enum { PRESEND, SEND, SAVE_TO_DRAFTS, SAVE_TO_OUTBOX, PRINT, LAST_SIGNAL }; static guint signals[LAST_SIGNAL]; /* used by e_msg_composer_add_message_attachments () */ static void add_attachments_from_multipart (EMsgComposer *composer, CamelMultipart *multipart, gboolean just_inlines, gint depth); /* used by e_msg_composer_new_with_message () */ static void handle_multipart (EMsgComposer *composer, CamelMultipart *multipart, GCancellable *cancellable, gint depth); static void handle_multipart_alternative (EMsgComposer *composer, CamelMultipart *multipart, GCancellable *cancellable, gint depth); static void handle_multipart_encrypted (EMsgComposer *composer, CamelMimePart *multipart, GCancellable *cancellable, gint depth); static void handle_multipart_signed (EMsgComposer *composer, CamelMultipart *multipart, GCancellable *cancellable, gint depth); static void e_msg_composer_alert_sink_init (EAlertSinkInterface *interface); G_DEFINE_TYPE_WITH_CODE ( EMsgComposer, e_msg_composer, GTKHTML_TYPE_EDITOR, G_IMPLEMENT_INTERFACE ( E_TYPE_ALERT_SINK, e_msg_composer_alert_sink_init) G_IMPLEMENT_INTERFACE (E_TYPE_EXTENSIBLE, NULL)) static void async_context_free (AsyncContext *context) { if (context->activity != NULL) g_object_unref (context->activity); if (context->message != NULL) g_object_unref (context->message); if (context->top_level_part != NULL) g_object_unref (context->top_level_part); if (context->text_plain_part != NULL) g_object_unref (context->text_plain_part); if (context->account != NULL) g_object_unref (context->account); if (context->session != NULL) g_object_unref (context->session); if (context->from != NULL) g_object_unref (context->from); if (context->recipients != NULL) g_ptr_array_free (context->recipients, TRUE); g_slice_free (AsyncContext, context); } /** * emcu_part_to_html: * @part: * * Converts a mime part's contents into html text. If @credits is given, * then it will be used as an attribution string, and the * content will be cited. Otherwise no citation or attribution * will be performed. * * Return Value: The part in displayable html format. **/ static gchar * emcu_part_to_html (CamelMimePart *part, gssize *len, EMFormat *source, GCancellable *cancellable) { EMFormatQuote *emfq; CamelStreamMem *mem; GByteArray *buf; gchar *text; buf = g_byte_array_new (); mem = (CamelStreamMem *) camel_stream_mem_new (); camel_stream_mem_set_byte_array (mem, buf); emfq = em_format_quote_new (NULL, (CamelStream *) mem, EM_FORMAT_QUOTE_KEEP_SIG); em_format_set_composer ((EMFormat *) emfq, TRUE); if (source) { /* Copy over things we can, other things are internal. * XXX Perhaps need different api than 'clone'. */ if (em_format_get_default_charset (source)) em_format_set_default_charset ( (EMFormat *) emfq, em_format_get_default_charset (source)); if (em_format_get_charset (source)) em_format_set_charset ( (EMFormat *) emfq, em_format_get_charset (source)); } em_format_format_text (EM_FORMAT (emfq), CAMEL_STREAM (mem), CAMEL_DATA_WRAPPER (part), cancellable); g_object_unref (emfq); camel_stream_write((CamelStream *) mem, "", 1, cancellable, NULL); g_object_unref (mem); text = (gchar *) buf->data; if (len) *len = buf->len-1; g_byte_array_free (buf, FALSE); return text; } /* copy of mail_tool_remove_xevolution_headers */ static struct _camel_header_raw * emcu_remove_xevolution_headers (CamelMimeMessage *message) { struct _camel_header_raw *scan, *list = NULL; for (scan = ((CamelMimePart *) message)->headers; scan; scan = scan->next) if (!strncmp(scan->name, "X-Evolution", 11)) camel_header_raw_append (&list, scan->name, scan->value, scan->offset); for (scan = list; scan; scan = scan->next) camel_medium_remove_header ((CamelMedium *) message, scan->name); return list; } static EDestination ** destination_list_to_vector_sized (GList *list, gint n) { EDestination **destv; gint i = 0; if (n == -1) n = g_list_length (list); if (n == 0) return NULL; destv = g_new (EDestination *, n + 1); while (list != NULL && i < n) { destv[i] = E_DESTINATION (list->data); list->data = NULL; i++; list = g_list_next (list); } destv[i] = NULL; return destv; } static EDestination ** destination_list_to_vector (GList *list) { return destination_list_to_vector_sized (list, -1); } #define LINE_LEN 72 static gboolean text_requires_quoted_printable (const gchar *text, gsize len) { const gchar *p; gsize pos; if (!text) return FALSE; if (len == -1) len = strlen (text); if (len >= 5 && strncmp (text, "From ", 5) == 0) return TRUE; for (p = text, pos = 0; pos + 6 <= len; pos++, p++) { if (*p == '\n' && strncmp (p + 1, "From ", 5) == 0) return TRUE; } return FALSE; } static CamelTransferEncoding best_encoding (GByteArray *buf, const gchar *charset) { gchar *in, *out, outbuf[256], *ch; gsize inlen, outlen; gint status, count = 0; iconv_t cd; if (!charset) return -1; cd = camel_iconv_open (charset, "utf-8"); if (cd == (iconv_t) -1) return -1; in = (gchar *) buf->data; inlen = buf->len; do { out = outbuf; outlen = sizeof (outbuf); status = camel_iconv (cd, (const gchar **) &in, &inlen, &out, &outlen); for (ch = out - 1; ch >= outbuf; ch--) { if ((guchar) *ch > 127) count++; } } while (status == (gsize) -1 && errno == E2BIG); camel_iconv_close (cd); if (status == (gsize) -1 || status > 0) return -1; if ((count == 0) && (buf->len < LINE_LEN) && !text_requires_quoted_printable ( (const gchar *) buf->data, buf->len)) return CAMEL_TRANSFER_ENCODING_7BIT; else if (count <= buf->len * 0.17) return CAMEL_TRANSFER_ENCODING_QUOTEDPRINTABLE; else return CAMEL_TRANSFER_ENCODING_BASE64; } static gchar * best_charset (GByteArray *buf, const gchar *default_charset, CamelTransferEncoding *encoding) { const gchar *charset; /* First try US-ASCII */ *encoding = best_encoding (buf, "US-ASCII"); if (*encoding == CAMEL_TRANSFER_ENCODING_7BIT) return NULL; /* Next try the user-specified charset for this message */ *encoding = best_encoding (buf, default_charset); if (*encoding != -1) return g_strdup (default_charset); /* Now try the user's default charset from the mail config */ charset = e_composer_get_default_charset (); *encoding = best_encoding (buf, charset); if (*encoding != -1) return g_strdup (charset); /* Try to find something that will work */ charset = camel_charset_best ( (const gchar *) buf->data, buf->len); if (charset == NULL) { *encoding = CAMEL_TRANSFER_ENCODING_7BIT; return NULL; } *encoding = best_encoding (buf, charset); return g_strdup (charset); } static void clear_current_images (EMsgComposer *composer) { EMsgComposerPrivate *p = composer->priv; g_list_free (p->current_images); p->current_images = NULL; } void e_msg_composer_clear_inlined_table (EMsgComposer *composer) { EMsgComposerPrivate *p = composer->priv; g_hash_table_remove_all (p->inline_images); g_hash_table_remove_all (p->inline_images_by_url); } static void add_inlined_images (EMsgComposer *composer, CamelMultipart *multipart) { EMsgComposerPrivate *p = composer->priv; GList *d = p->current_images; GHashTable *added; added = g_hash_table_new (g_direct_hash, g_direct_equal); while (d) { CamelMimePart *part = d->data; if (!g_hash_table_lookup (added, part)) { camel_multipart_add_part (multipart, part); g_hash_table_insert (added, part, part); } d = d->next; } g_hash_table_destroy (added); } /* These functions builds a CamelMimeMessage for the message that the user has * composed in 'composer'. */ static void set_recipients_from_destv (CamelMimeMessage *msg, EDestination **to_destv, EDestination **cc_destv, EDestination **bcc_destv, gboolean redirect) { CamelInternetAddress *to_addr; CamelInternetAddress *cc_addr; CamelInternetAddress *bcc_addr; CamelInternetAddress *target; const gchar *text_addr, *header; gboolean seen_hidden_list = FALSE; gint i; to_addr = camel_internet_address_new (); cc_addr = camel_internet_address_new (); bcc_addr = camel_internet_address_new (); for (i = 0; to_destv != NULL && to_destv[i] != NULL; ++i) { text_addr = e_destination_get_address (to_destv[i]); if (text_addr && *text_addr) { target = to_addr; if (e_destination_is_evolution_list (to_destv[i]) && !e_destination_list_show_addresses (to_destv[i])) { target = bcc_addr; seen_hidden_list = TRUE; } if (camel_address_decode (CAMEL_ADDRESS (target), text_addr) <= 0) camel_internet_address_add (target, "", text_addr); } } for (i = 0; cc_destv != NULL && cc_destv[i] != NULL; ++i) { text_addr = e_destination_get_address (cc_destv[i]); if (text_addr && *text_addr) { target = cc_addr; if (e_destination_is_evolution_list (cc_destv[i]) && !e_destination_list_show_addresses (cc_destv[i])) { target = bcc_addr; seen_hidden_list = TRUE; } if (camel_address_decode (CAMEL_ADDRESS (target), text_addr) <= 0) camel_internet_address_add (target, "", text_addr); } } for (i = 0; bcc_destv != NULL && bcc_destv[i] != NULL; ++i) { text_addr = e_destination_get_address (bcc_destv[i]); if (text_addr && *text_addr) { if (camel_address_decode (CAMEL_ADDRESS (bcc_addr), text_addr) <= 0) camel_internet_address_add (bcc_addr, "", text_addr); } } if (redirect) header = CAMEL_RECIPIENT_TYPE_RESENT_TO; else header = CAMEL_RECIPIENT_TYPE_TO; if (camel_address_length (CAMEL_ADDRESS (to_addr)) > 0) { camel_mime_message_set_recipients (msg, header, to_addr); } else if (seen_hidden_list) { camel_medium_set_header ( CAMEL_MEDIUM (msg), header, "Undisclosed-Recipient:;"); } header = redirect ? CAMEL_RECIPIENT_TYPE_RESENT_CC : CAMEL_RECIPIENT_TYPE_CC; if (camel_address_length (CAMEL_ADDRESS (cc_addr)) > 0) { camel_mime_message_set_recipients (msg, header, cc_addr); } header = redirect ? CAMEL_RECIPIENT_TYPE_RESENT_BCC : CAMEL_RECIPIENT_TYPE_BCC; if (camel_address_length (CAMEL_ADDRESS (bcc_addr)) > 0) { camel_mime_message_set_recipients (msg, header, bcc_addr); } g_object_unref (to_addr); g_object_unref (cc_addr); g_object_unref (bcc_addr); } static void build_message_headers (EMsgComposer *composer, CamelMimeMessage *message, gboolean redirect) { EComposerHeaderTable *table; EComposerHeader *header; EAccount *account; const gchar *subject; const gchar *reply_to; g_return_if_fail (E_IS_MSG_COMPOSER (composer)); g_return_if_fail (CAMEL_IS_MIME_MESSAGE (message)); table = e_msg_composer_get_header_table (composer); /* Subject: */ subject = e_composer_header_table_get_subject (table); camel_mime_message_set_subject (message, subject); account = e_composer_header_table_get_account (table); if (account != NULL) { CamelMedium *medium; CamelInternetAddress *addr; const gchar *header_name; const gchar *name = account->id->name; const gchar *address = account->id->address; gchar *transport_uid; medium = CAMEL_MEDIUM (message); /* From: / Resent-From: */ addr = camel_internet_address_new (); camel_internet_address_add (addr, name, address); if (redirect) { gchar *value; value = camel_address_encode (CAMEL_ADDRESS (addr)); camel_medium_set_header (medium, "Resent-From", value); g_free (value); } else camel_mime_message_set_from (message, addr); g_object_unref (addr); /* X-Evolution-Account */ header_name = "X-Evolution-Account"; camel_medium_set_header (medium, header_name, account->uid); /* X-Evolution-Fcc */ header_name = "X-Evolution-Fcc"; camel_medium_set_header (medium, header_name, account->sent_folder_uri); /* X-Evolution-Transport */ header_name = "X-Evolution-Transport"; transport_uid = g_strconcat ( account->uid, "-transport", NULL); camel_medium_set_header (medium, header_name, transport_uid); g_free (transport_uid); } /* Reply-To: */ reply_to = e_composer_header_table_get_reply_to (table); if (reply_to != NULL && *reply_to != '\0') { CamelInternetAddress *addr; addr = camel_internet_address_new (); if (camel_address_unformat (CAMEL_ADDRESS (addr), reply_to) > 0) camel_mime_message_set_reply_to (message, addr); g_object_unref (addr); } /* To:, Cc:, Bcc: */ header = e_composer_header_table_get_header ( table, E_COMPOSER_HEADER_TO); if (e_composer_header_get_visible (header)) { EDestination **to, **cc, **bcc; to = e_composer_header_table_get_destinations_to (table); cc = e_composer_header_table_get_destinations_cc (table); bcc = e_composer_header_table_get_destinations_bcc (table); set_recipients_from_destv (message, to, cc, bcc, redirect); e_destination_freev (to); e_destination_freev (cc); e_destination_freev (bcc); } /* Date: */ camel_mime_message_set_date (message, CAMEL_MESSAGE_DATE_CURRENT, 0); /* X-Evolution-PostTo: */ header = e_composer_header_table_get_header ( table, E_COMPOSER_HEADER_POST_TO); if (e_composer_header_get_visible (header)) { CamelMedium *medium; const gchar *name = "X-Evolution-PostTo"; GList *list, *iter; medium = CAMEL_MEDIUM (message); camel_medium_remove_header (medium, name); list = e_composer_header_table_get_post_to (table); for (iter = list; iter != NULL; iter = iter->next) { gchar *folder = iter->data; camel_medium_add_header (medium, name, folder); g_free (folder); } g_list_free (list); } } static CamelCipherHash account_hash_algo_to_camel_hash (const gchar *hash_algo) { CamelCipherHash res = CAMEL_CIPHER_HASH_DEFAULT; if (hash_algo && *hash_algo) { if (g_ascii_strcasecmp (hash_algo, "sha1") == 0) res = CAMEL_CIPHER_HASH_SHA1; else if (g_ascii_strcasecmp (hash_algo, "sha256") == 0) res = CAMEL_CIPHER_HASH_SHA256; else if (g_ascii_strcasecmp (hash_algo, "sha384") == 0) res = CAMEL_CIPHER_HASH_SHA384; else if (g_ascii_strcasecmp (hash_algo, "sha512") == 0) res = CAMEL_CIPHER_HASH_SHA512; } return res; } static void composer_add_charset_filter (CamelStream *stream, const gchar *charset) { CamelMimeFilter *filter; filter = camel_mime_filter_charset_new ("UTF-8", charset); camel_stream_filter_add (CAMEL_STREAM_FILTER (stream), filter); g_object_unref (filter); } static void composer_add_quoted_printable_filter (CamelStream *stream) { CamelMimeFilter *filter; filter = camel_mime_filter_basic_new (CAMEL_MIME_FILTER_BASIC_QP_ENC); camel_stream_filter_add (CAMEL_STREAM_FILTER (stream), filter); g_object_unref (filter); filter = camel_mime_filter_canon_new (CAMEL_MIME_FILTER_CANON_FROM); camel_stream_filter_add (CAMEL_STREAM_FILTER (stream), filter); g_object_unref (filter); } /* Helper for composer_build_message_thread() */ static gboolean composer_build_message_pgp (AsyncContext *context, GCancellable *cancellable, GError **error) { CamelCipherContext *cipher; CamelDataWrapper *content; CamelMimePart *mime_part; const gchar *pgp_userid; gboolean have_pgp_key; /* Return silently if we're not signing or encrypting with PGP. */ if (!context->pgp_sign && !context->pgp_encrypt) return TRUE; have_pgp_key = (context->account != NULL) && (context->account->pgp_key != NULL) && (context->account->pgp_key[0] != '\0'); mime_part = camel_mime_part_new (); camel_medium_set_content ( CAMEL_MEDIUM (mime_part), context->top_level_part); if (context->top_level_part == context->text_plain_part) camel_mime_part_set_encoding ( mime_part, context->plain_encoding); g_object_unref (context->top_level_part); context->top_level_part = NULL; if (have_pgp_key) pgp_userid = context->account->pgp_key; else camel_internet_address_get ( context->from, 0, NULL, &pgp_userid); if (context->pgp_sign) { CamelMimePart *npart; gboolean success; npart = camel_mime_part_new (); cipher = camel_gpg_context_new (context->session); if (context->account != NULL) camel_gpg_context_set_always_trust ( CAMEL_GPG_CONTEXT (cipher), context->account->pgp_always_trust); success = camel_cipher_context_sign_sync ( cipher, pgp_userid, account_hash_algo_to_camel_hash ( (context->account != NULL) ? e_account_get_string (context->account, E_ACCOUNT_PGP_HASH_ALGORITHM) : NULL), mime_part, npart, cancellable, error); g_object_unref (cipher); g_object_unref (mime_part); if (!success) { g_object_unref (npart); return FALSE; } mime_part = npart; } if (context->pgp_encrypt) { CamelMimePart *npart; gboolean encrypt_to_self; gboolean success; encrypt_to_self = (context->account != NULL) && (context->account->pgp_encrypt_to_self) && (pgp_userid != NULL); npart = camel_mime_part_new (); /* Check to see if we should encrypt to self. * NB gets removed immediately after use */ if (encrypt_to_self) g_ptr_array_add ( context->recipients, g_strdup (pgp_userid)); cipher = camel_gpg_context_new (context->session); if (context->account != NULL) camel_gpg_context_set_always_trust ( CAMEL_GPG_CONTEXT (cipher), context->account->pgp_always_trust); success = camel_cipher_context_encrypt_sync ( cipher, pgp_userid, context->recipients, mime_part, npart, cancellable, error); g_object_unref (cipher); if (encrypt_to_self) g_ptr_array_set_size ( context->recipients, context->recipients->len - 1); g_object_unref (mime_part); if (!success) { g_object_unref (npart); return FALSE; } mime_part = npart; } content = camel_medium_get_content (CAMEL_MEDIUM (mime_part)); context->top_level_part = g_object_ref (content); g_object_unref (mime_part); return TRUE; } #ifdef HAVE_SSL static gboolean composer_build_message_smime (AsyncContext *context, GCancellable *cancellable, GError **error) { CamelCipherContext *cipher; CamelMimePart *mime_part; gboolean have_signing_certificate; gboolean have_encryption_certificate; /* Return silently if we're not signing or encrypting with S/MIME. */ if (!context->smime_sign && !context->smime_encrypt) return TRUE; have_signing_certificate = (context->account != NULL) && (context->account->smime_sign_key != NULL) && (context->account->smime_sign_key[0] != '\0'); have_encryption_certificate = (context->account != NULL) && (context->account->smime_encrypt_key != NULL) && (context->account->smime_encrypt_key[0] != '\0'); if (context->smime_sign && !have_signing_certificate) { g_set_error ( error, CAMEL_ERROR, CAMEL_ERROR_GENERIC, _("Cannot sign outgoing message: " "No signing certificate set for " "this account")); return FALSE; } if (context->smime_encrypt && !have_encryption_certificate) { g_set_error ( error, CAMEL_ERROR, CAMEL_ERROR_GENERIC, _("Cannot encrypt outgoing message: " "No encryption certificate set for " "this account")); return FALSE; } mime_part = camel_mime_part_new (); camel_medium_set_content ( CAMEL_MEDIUM (mime_part), context->top_level_part); if (context->top_level_part == context->text_plain_part) camel_mime_part_set_encoding ( mime_part, context->plain_encoding); g_object_unref (context->top_level_part); context->top_level_part = NULL; if (context->smime_sign) { CamelMimePart *npart; gboolean success; npart = camel_mime_part_new (); cipher = camel_smime_context_new (context->session); /* if we're also encrypting, envelope-sign rather than clear-sign */ if (context->smime_encrypt) { camel_smime_context_set_sign_mode ( (CamelSMIMEContext *) cipher, CAMEL_SMIME_SIGN_ENVELOPED); camel_smime_context_set_encrypt_key ( (CamelSMIMEContext *) cipher, TRUE, context->account->smime_encrypt_key); } else if (have_encryption_certificate) { camel_smime_context_set_encrypt_key ( (CamelSMIMEContext *) cipher, TRUE, context->account->smime_encrypt_key); } success = camel_cipher_context_sign_sync ( cipher, context->account->smime_sign_key, account_hash_algo_to_camel_hash ( (context->account != NULL) ? e_account_get_string (context->account, E_ACCOUNT_SMIME_HASH_ALGORITHM) : NULL), mime_part, npart, cancellable, error); g_object_unref (cipher); g_object_unref (mime_part); if (!success) { g_object_unref (npart); return FALSE; } mime_part = npart; } if (context->smime_encrypt) { gboolean success; /* check to see if we should encrypt to self, NB removed after use */ if (context->account->smime_encrypt_to_self) g_ptr_array_add ( context->recipients, g_strdup ( context->account->smime_encrypt_key)); cipher = camel_smime_context_new (context->session); camel_smime_context_set_encrypt_key ( (CamelSMIMEContext *) cipher, TRUE, context->account->smime_encrypt_key); success = camel_cipher_context_encrypt_sync ( cipher, NULL, context->recipients, mime_part, CAMEL_MIME_PART (context->message), cancellable, error); g_object_unref (cipher); if (!success) return FALSE; if (context->account->smime_encrypt_to_self) g_ptr_array_set_size ( context->recipients, context->recipients->len - 1); } /* we replaced the message directly, we don't want to do reparenting foo */ if (context->smime_encrypt) { context->skip_content = TRUE; } else { CamelDataWrapper *content; content = camel_medium_get_content ( CAMEL_MEDIUM (mime_part)); context->top_level_part = g_object_ref (content); } g_object_unref (mime_part); return TRUE; } #endif static void composer_build_message_thread (GSimpleAsyncResult *simple, EMsgComposer *composer, GCancellable *cancellable) { AsyncContext *context; GError *error = NULL; context = g_simple_async_result_get_op_res_gpointer (simple); /* Setup working recipient list if we're encrypting. */ if (context->pgp_encrypt || context->smime_encrypt) { gint ii, jj; const gchar *types[] = { CAMEL_RECIPIENT_TYPE_TO, CAMEL_RECIPIENT_TYPE_CC, CAMEL_RECIPIENT_TYPE_BCC }; context->recipients = g_ptr_array_new_with_free_func ( (GDestroyNotify) g_free); for (ii = 0; ii < G_N_ELEMENTS (types); ii++) { CamelInternetAddress *addr; const gchar *address; addr = camel_mime_message_get_recipients ( context->message, types[ii]); for (jj = 0; camel_internet_address_get (addr, jj, NULL, &address); jj++) g_ptr_array_add ( context->recipients, g_strdup (address)); } } if (!composer_build_message_pgp (context, cancellable, &error)) { g_simple_async_result_take_error (simple, error); return; } #if defined (HAVE_NSS) if (!composer_build_message_smime (context, cancellable, &error)) { g_simple_async_result_take_error (simple, error); return; } #endif /* HAVE_NSS */ } static void composer_add_evolution_format_header (CamelMedium *medium, ComposerFlags flags) { GString *string; string = g_string_sized_new (128); if (flags & COMPOSER_FLAG_HTML_CONTENT) g_string_append (string, "text/html"); else g_string_append (string, "text/plain"); if (flags & COMPOSER_FLAG_PGP_SIGN) g_string_append (string, ", pgp-sign"); if (flags & COMPOSER_FLAG_PGP_ENCRYPT) g_string_append (string, ", pgp-encrypt"); if (flags & COMPOSER_FLAG_SMIME_SIGN) g_string_append (string, ", smime-sign"); if (flags & COMPOSER_FLAG_SMIME_ENCRYPT) g_string_append (string, ", smime-encrypt"); camel_medium_add_header ( medium, "X-Evolution-Format", string->str); g_string_free (string, TRUE); } static void composer_build_message (EMsgComposer *composer, ComposerFlags flags, gint io_priority, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { EMsgComposerPrivate *priv; GSimpleAsyncResult *simple; AsyncContext *context; GtkhtmlEditor *editor; EAttachmentView *view; EAttachmentStore *store; EComposerHeaderTable *table; CamelDataWrapper *html; const gchar *iconv_charset = NULL; CamelMultipart *body = NULL; CamelContentType *type; CamelSession *session; CamelStream *stream; CamelStream *mem_stream; CamelMimePart *part; GByteArray *data; EAccount *account; gchar *charset; gint i; priv = composer->priv; editor = GTKHTML_EDITOR (composer); table = e_msg_composer_get_header_table (composer); account = e_composer_header_table_get_account (table); view = e_msg_composer_get_attachment_view (composer); store = e_attachment_view_get_store (view); session = e_msg_composer_get_session (composer); /* Do all the non-blocking work here, and defer * any blocking operations to a separate thread. */ context = g_slice_new0 (AsyncContext); context->account = g_object_ref (account); context->session = g_object_ref (session); context->from = e_msg_composer_get_from (composer); if (flags & COMPOSER_FLAG_PGP_SIGN) context->pgp_sign = TRUE; if (flags & COMPOSER_FLAG_PGP_ENCRYPT) context->pgp_encrypt = TRUE; if (flags & COMPOSER_FLAG_SMIME_SIGN) context->smime_sign = TRUE; if (flags & COMPOSER_FLAG_SMIME_ENCRYPT) context->smime_encrypt = TRUE; context->need_thread = context->pgp_sign || context->pgp_encrypt || context->smime_sign || context->smime_encrypt; simple = g_simple_async_result_new ( G_OBJECT (composer), callback, user_data, composer_build_message); g_simple_async_result_set_op_res_gpointer ( simple, context, (GDestroyNotify) async_context_free); /* If this is a redirected message, just tweak the headers. */ if (priv->redirect) { context->skip_content = TRUE; context->message = g_object_ref (priv->redirect); build_message_headers (composer, context->message, TRUE); g_simple_async_result_complete (simple); g_object_unref (simple); return; } context->message = camel_mime_message_new (); /* Explicitly generate a Message-ID header here so it's * consistent for all outbound streams (SMTP, Fcc, etc). */ camel_mime_message_set_message_id (context->message, NULL); build_message_headers (composer, context->message, FALSE); for (i = 0; i < priv->extra_hdr_names->len; i++) { camel_medium_add_header ( CAMEL_MEDIUM (context->message), priv->extra_hdr_names->pdata[i], priv->extra_hdr_values->pdata[i]); } /* Disposition-Notification-To */ if (flags & COMPOSER_FLAG_REQUEST_READ_RECEIPT) { gchar *mdn_address = account->id->reply_to; if (mdn_address == NULL || *mdn_address == '\0') mdn_address = account->id->address; camel_medium_add_header ( CAMEL_MEDIUM (context->message), "Disposition-Notification-To", mdn_address); } /* X-Priority */ if (flags & COMPOSER_FLAG_PRIORITIZE_MESSAGE) camel_medium_add_header ( CAMEL_MEDIUM (context->message), "X-Priority", "1"); /* Organization */ if (account != NULL && account->id->organization != NULL) { gchar *organization; organization = camel_header_encode_string ( (const guchar *) account->id->organization); camel_medium_set_header ( CAMEL_MEDIUM (context->message), "Organization", organization); g_free (organization); } /* X-Evolution-Format */ composer_add_evolution_format_header ( CAMEL_MEDIUM (context->message), flags); /* Build the text/plain part. */ if (priv->mime_body) { if (text_requires_quoted_printable (priv->mime_body, -1)) { context->plain_encoding = CAMEL_TRANSFER_ENCODING_QUOTEDPRINTABLE; } else { context->plain_encoding = CAMEL_TRANSFER_ENCODING_7BIT; for (i = 0; priv->mime_body[i]; i++) { if ((guchar) priv->mime_body[i] > 127) { context->plain_encoding = CAMEL_TRANSFER_ENCODING_QUOTEDPRINTABLE; break; } } } data = g_byte_array_new (); g_byte_array_append ( data, (const guint8 *) priv->mime_body, strlen (priv->mime_body)); type = camel_content_type_decode (priv->mime_type); } else { gchar *text; gsize length; data = g_byte_array_new (); text = gtkhtml_editor_get_text_plain (editor, &length); g_byte_array_append (data, (guint8 *) text, (guint) length); g_free (text); type = camel_content_type_new ("text", "plain"); charset = best_charset ( data, priv->charset, &context->plain_encoding); if (charset != NULL) { camel_content_type_set_param (type, "charset", charset); iconv_charset = camel_iconv_charset_name (charset); g_free (charset); } } mem_stream = camel_stream_mem_new_with_byte_array (data); stream = camel_stream_filter_new (mem_stream); g_object_unref (mem_stream); /* Convert the stream to the appropriate charset. */ if (iconv_charset && g_ascii_strcasecmp (iconv_charset, "UTF-8") != 0) composer_add_charset_filter (stream, iconv_charset); /* Encode the stream to quoted-printable if necessary. */ if (context->plain_encoding == CAMEL_TRANSFER_ENCODING_QUOTEDPRINTABLE) composer_add_quoted_printable_filter (stream); /* Construct the content object. This does not block since * we're constructing the data wrapper from a memory stream. */ context->top_level_part = camel_data_wrapper_new (); camel_data_wrapper_construct_from_stream_sync ( context->top_level_part, stream, NULL, NULL); g_object_unref (stream); context->text_plain_part = g_object_ref (context->top_level_part); /* Avoid re-encoding the data when adding it to a MIME part. */ if (context->plain_encoding == CAMEL_TRANSFER_ENCODING_QUOTEDPRINTABLE) context->top_level_part->encoding = context->plain_encoding; camel_data_wrapper_set_mime_type_field ( context->top_level_part, type); camel_content_type_unref (type); /* Build the text/html part, and wrap it and the text/plain part * in a multipart/alternative part. Additionally, if there are * inline images then wrap the multipart/alternative part along * with the images in a multipart/related part. * * So the structure of all this will be: * * multipart/related * multipart/alternative * text/plain * text/html * image/<> * image/<> * ... */ if (flags & COMPOSER_FLAG_HTML_CONTENT) { gchar *text; gsize length; gboolean pre_encode; clear_current_images (composer); if (flags & COMPOSER_FLAG_SAVE_OBJECT_DATA) gtkhtml_editor_run_command (editor, "save-data-on"); data = g_byte_array_new (); text = gtkhtml_editor_get_text_html (editor, &length); g_byte_array_append (data, (guint8 *) text, (guint) length); pre_encode = text_requires_quoted_printable (text, length); g_free (text); if (flags & COMPOSER_FLAG_SAVE_OBJECT_DATA) gtkhtml_editor_run_command (editor, "save-data-off"); mem_stream = camel_stream_mem_new_with_byte_array (data); stream = camel_stream_filter_new (mem_stream); g_object_unref (mem_stream); if (pre_encode) composer_add_quoted_printable_filter (stream); /* Construct the content object. This does not block since * we're constructing the data wrapper from a memory stream. */ html = camel_data_wrapper_new (); camel_data_wrapper_construct_from_stream_sync ( html, stream, NULL, NULL); g_object_unref (stream); camel_data_wrapper_set_mime_type ( html, "text/html; charset=utf-8"); /* Avoid re-encoding the data when adding it to a MIME part. */ if (pre_encode) html->encoding = CAMEL_TRANSFER_ENCODING_QUOTEDPRINTABLE; /* Build the multipart/alternative */ body = camel_multipart_new (); camel_data_wrapper_set_mime_type ( CAMEL_DATA_WRAPPER (body), "multipart/alternative"); camel_multipart_set_boundary (body, NULL); /* Add the text/plain part. */ part = camel_mime_part_new (); camel_medium_set_content ( CAMEL_MEDIUM (part), context->top_level_part); camel_mime_part_set_encoding (part, context->plain_encoding); camel_multipart_add_part (body, part); g_object_unref (part); /* Add the text/html part. */ part = camel_mime_part_new (); camel_medium_set_content (CAMEL_MEDIUM (part), html); camel_mime_part_set_encoding ( part, CAMEL_TRANSFER_ENCODING_QUOTEDPRINTABLE); camel_multipart_add_part (body, part); g_object_unref (part); g_object_unref (context->top_level_part); g_object_unref (html); /* If there are inlined images, construct a multipart/related * containing the multipart/alternative and the images. */ if (priv->current_images) { CamelMultipart *html_with_images; html_with_images = camel_multipart_new (); camel_data_wrapper_set_mime_type ( CAMEL_DATA_WRAPPER (html_with_images), "multipart/related; " "type=\"multipart/alternative\""); camel_multipart_set_boundary (html_with_images, NULL); part = camel_mime_part_new (); camel_medium_set_content ( CAMEL_MEDIUM (part), CAMEL_DATA_WRAPPER (body)); camel_multipart_add_part (html_with_images, part); g_object_unref (part); g_object_unref (body); add_inlined_images (composer, html_with_images); clear_current_images (composer); context->top_level_part = CAMEL_DATA_WRAPPER (html_with_images); } else context->top_level_part = CAMEL_DATA_WRAPPER (body); } /* If there are attachments, wrap what we've built so far * along with the attachments in a multipart/mixed part. */ if (e_attachment_store_get_num_attachments (store) > 0) { CamelMultipart *multipart = camel_multipart_new (); /* Generate a random boundary. */ camel_multipart_set_boundary (multipart, NULL); part = camel_mime_part_new (); camel_medium_set_content ( CAMEL_MEDIUM (part), context->top_level_part); if (context->top_level_part == context->text_plain_part) camel_mime_part_set_encoding ( part, context->plain_encoding); camel_multipart_add_part (multipart, part); g_object_unref (part); e_attachment_store_add_to_multipart ( store, multipart, priv->charset); g_object_unref (context->top_level_part); context->top_level_part = CAMEL_DATA_WRAPPER (multipart); } /* Run any blocking operations in a separate thread. */ if (context->need_thread) g_simple_async_result_run_in_thread ( simple, (GSimpleAsyncThreadFunc) composer_build_message_thread, io_priority, cancellable); else g_simple_async_result_complete (simple); g_object_unref (simple); } static CamelMimeMessage * composer_build_message_finish (EMsgComposer *composer, GAsyncResult *result, GError **error) { GSimpleAsyncResult *simple; AsyncContext *context; g_return_val_if_fail ( g_simple_async_result_is_valid ( result, G_OBJECT (composer), composer_build_message), NULL); simple = G_SIMPLE_ASYNC_RESULT (result); context = g_simple_async_result_get_op_res_gpointer (simple); if (g_simple_async_result_propagate_error (simple, error)) return NULL; /* Finalize some details before returning. */ if (!context->skip_content) camel_medium_set_content ( CAMEL_MEDIUM (context->message), context->top_level_part); if (context->top_level_part == context->text_plain_part) camel_mime_part_set_encoding ( CAMEL_MIME_PART (context->message), context->plain_encoding); return g_object_ref (context->message); } /* Signatures */ static gboolean is_top_signature (EMsgComposer *composer) { EShell *shell; EShellSettings *shell_settings; EMsgComposerPrivate *priv = E_MSG_COMPOSER_GET_PRIVATE (composer); g_return_val_if_fail (priv != NULL, FALSE); /* The composer had been created from a stored message, thus the * signature placement is either there already, or pt it at the * bottom regardless of a preferences (which is for reply anyway, * not for Edit as new) */ if (priv->is_from_message) return FALSE; shell = e_msg_composer_get_shell (composer); shell_settings = e_shell_get_shell_settings (shell); return e_shell_settings_get_boolean ( shell_settings, "composer-top-signature"); } static gboolean add_signature_delim (EMsgComposer *composer) { EShell *shell; EShellSettings *shell_settings; shell = e_msg_composer_get_shell (composer); shell_settings = e_shell_get_shell_settings (shell); return !e_shell_settings_get_boolean ( shell_settings, "composer-no-signature-delim"); } #define CONVERT_SPACES CAMEL_MIME_FILTER_TOHTML_CONVERT_SPACES #define NO_SIGNATURE_TEXT \ "" \ "
" static gchar * get_signature_html (EMsgComposer *composer) { EComposerHeaderTable *table; gchar *text = NULL, *html = NULL; ESignature *signature; gboolean format_html, add_delim; table = e_msg_composer_get_header_table (composer); signature = e_composer_header_table_get_signature (table); if (!signature) return NULL; add_delim = add_signature_delim (composer); if (!e_signature_get_autogenerated (signature)) { const gchar *filename; filename = e_signature_get_filename (signature); if (filename == NULL) return NULL; format_html = e_signature_get_is_html (signature); if (e_signature_get_is_script (signature)) text = e_run_signature_script (filename); else text = e_read_signature_file (signature, TRUE, NULL); } else { EAccount *account; EAccountIdentity *id; gchar *organization = NULL; gchar *address = NULL; gchar *name = NULL; account = e_composer_header_table_get_account (table); if (!account) return NULL; id = account->id; if (id->address != NULL) address = camel_text_to_html ( id->address, CONVERT_SPACES, 0); if (id->name != NULL) name = camel_text_to_html ( id->name, CONVERT_SPACES, 0); if (id->organization != NULL) organization = camel_text_to_html ( id->organization, CONVERT_SPACES, 0); text = g_strdup_printf ("%s%s%s%s%s%s%s%s%s", add_delim ? "-- \n
" : "", name ? name : "", (address && *address) ? " <" : "", address ? address : "", (address && *address) ? ">" : "", (organization && *organization) ? "
" : "", organization ? organization : ""); g_free (address); g_free (name); g_free (organization); format_html = TRUE; } /* printf ("text: %s\n", text); */ if (text) { gchar *encoded_uid = NULL; const gchar *sig_delim = format_html ? "-- \n
" : "-- \n"; const gchar *sig_delim_ent = format_html ? "\n-- \n
" : "\n-- \n"; if (signature != NULL) { const gchar *uid = e_signature_get_uid (signature); encoded_uid = e_composer_encode_clue_value (uid); } /* The signature dash convention ("-- \n") is specified * in the "Son of RFC 1036", section 4.3.2. * http://www.chemie.fu-berlin.de/outerspace/netnews/son-of-1036.html */ html = g_strdup_printf ( "" "" "
" "%s%s%s%s" "%s
", encoded_uid ? encoded_uid : "", format_html ? "" : "
\n",
			!add_delim ? "" :
				(!strncmp (
				sig_delim, text, strlen (sig_delim)) ||
				strstr (text, sig_delim_ent))
				? "" : sig_delim,
			text,
			format_html ? "" : "
\n", is_top_signature (composer) ? "
" : ""); g_free (text); g_free (encoded_uid); text = html; } return text; } static void set_editor_text (EMsgComposer *composer, const gchar *text, gboolean set_signature) { gchar *body = NULL; g_return_if_fail (E_IS_MSG_COMPOSER (composer)); g_return_if_fail (text != NULL); /* * * Keeping Signatures in the beginning of composer * ------------------------------------------------ * * Purists are gonna blast me for this. * But there are so many people (read Outlook users) who want this. * And Evo is an exchange-client, Outlook-replacement etc. * So Here it goes :( * * -- Sankar * */ if (is_top_signature (composer)) { /* put marker to the top */ body = g_strdup_printf ("
" NO_SIGNATURE_TEXT "%s", text); } else { /* no marker => to the bottom */ body = g_strdup_printf ("%s
", text); } gtkhtml_editor_set_text_html (GTKHTML_EDITOR (composer), body, -1); if (set_signature) e_msg_composer_show_sig_file (composer); g_free (body); } /* Miscellaneous callbacks. */ static void attachment_store_changed_cb (EMsgComposer *composer) { GtkhtmlEditor *editor; /* Mark the editor as changed so it prompts about unsaved * changes on close. */ editor = GTKHTML_EDITOR (composer); gtkhtml_editor_set_changed (editor, TRUE); } static void msg_composer_subject_changed_cb (EMsgComposer *composer) { EComposerHeaderTable *table; const gchar *subject; table = e_msg_composer_get_header_table (composer); subject = e_composer_header_table_get_subject (table); if (subject == NULL || *subject == '\0') subject = _("Compose Message"); gtk_window_set_title (GTK_WINDOW (composer), subject); } static void msg_composer_account_changed_cb (EMsgComposer *composer) { EMsgComposerPrivate *p = composer->priv; EComposerHeaderTable *table; GtkToggleAction *action; ESignature *signature; EAccount *account; gboolean active, can_sign; const gchar *uid; table = e_msg_composer_get_header_table (composer); account = e_composer_header_table_get_account (table); if (account == NULL) { e_msg_composer_show_sig_file (composer); return; } can_sign = (!account->pgp_no_imip_sign || p->mime_type == NULL || g_ascii_strncasecmp (p->mime_type, "text/calendar", 13) != 0); action = GTK_TOGGLE_ACTION (ACTION (PGP_SIGN)); active = account->pgp_always_sign && can_sign; gtk_toggle_action_set_active (action, active); action = GTK_TOGGLE_ACTION (ACTION (SMIME_SIGN)); active = account->smime_sign_default && can_sign; gtk_toggle_action_set_active (action, active); action = GTK_TOGGLE_ACTION (ACTION (SMIME_ENCRYPT)); active = account->smime_encrypt_default; gtk_toggle_action_set_active (action, active); uid = account->id->sig_uid; signature = uid ? e_get_signature_by_uid (uid) : NULL; e_composer_header_table_set_signature (table, signature); e_msg_composer_show_sig_file (composer); } static void msg_composer_paste_clipboard_targets_cb (GtkClipboard *clipboard, GdkAtom *targets, gint n_targets, EMsgComposer *composer) { GtkhtmlEditor *editor; gboolean html_mode; editor = GTKHTML_EDITOR (composer); html_mode = gtkhtml_editor_get_html_mode (editor); /* Order is important here to ensure common use cases are * handled correctly. See GNOME bug #603715 for details. */ if (gtk_targets_include_uri (targets, n_targets)) { e_composer_paste_uris (composer, clipboard); return; } /* Only paste HTML content in HTML mode. */ if (html_mode) { if (e_targets_include_html (targets, n_targets)) { e_composer_paste_html (composer, clipboard); return; } } if (gtk_targets_include_text (targets, n_targets)) { e_composer_paste_text (composer, clipboard); return; } if (gtk_targets_include_image (targets, n_targets, TRUE)) { e_composer_paste_image (composer, clipboard); return; } } static void msg_composer_paste_clipboard_cb (EWebViewGtkHTML *web_view, EMsgComposer *composer) { GtkClipboard *clipboard; clipboard = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD); gtk_clipboard_request_targets ( clipboard, (GtkClipboardTargetsReceivedFunc) msg_composer_paste_clipboard_targets_cb, composer); g_signal_stop_emission_by_name (web_view, "paste-clipboard"); } static void msg_composer_realize_gtkhtml_cb (GtkWidget *widget, EMsgComposer *composer) { EAttachmentView *view; GtkTargetList *target_list; GtkTargetEntry *targets; gint n_targets; /* XXX GtkHTML doesn't set itself up as a drag destination until * it's realized, and we need to amend to its target list so * it will accept the same drag targets as the attachment bar. * Do this any earlier and GtkHTML will just overwrite us. */ /* When redirecting a message, the message body is not * editable and therefore cannot be a drag destination. */ if (!e_web_view_gtkhtml_get_editable (E_WEB_VIEW_GTKHTML (widget))) return; view = e_msg_composer_get_attachment_view (composer); target_list = e_attachment_view_get_target_list (view); targets = gtk_target_table_new_from_list (target_list, &n_targets); target_list = gtk_drag_dest_get_target_list (widget); gtk_target_list_add_table (target_list, targets, n_targets); gtk_target_table_free (targets, n_targets); } static gboolean msg_composer_drag_motion_cb (GtkWidget *widget, GdkDragContext *context, gint x, gint y, guint time, EMsgComposer *composer) { EAttachmentView *view; view = e_msg_composer_get_attachment_view (composer); /* Stop the signal from propagating to GtkHtml. */ g_signal_stop_emission_by_name (widget, "drag-motion"); return e_attachment_view_drag_motion (view, context, x, y, time); } static void msg_composer_drag_data_received_cb (GtkWidget *widget, GdkDragContext *context, gint x, gint y, GtkSelectionData *selection, guint info, guint time, EMsgComposer *composer) { EAttachmentView *view; /* HTML mode has a few special cases for drops... */ if (gtkhtml_editor_get_html_mode (GTKHTML_EDITOR (composer))) { /* If we're receiving an image, we want the image to be * inserted in the message body. Let GtkHtml handle it. */ if (gtk_selection_data_targets_include_image (selection, TRUE)) return; /* If we're receiving URIs and -all- the URIs point to * image files, we want the image(s) to be inserted in * the message body. Let GtkHtml handle it. */ if (e_composer_selection_is_image_uris (composer, selection)) return; } view = e_msg_composer_get_attachment_view (composer); /* Forward the data to the attachment view. Note that calling * e_attachment_view_drag_data_received() will not work because * that function only handles the case where all the other drag * handlers have failed. */ e_attachment_paned_drag_data_received ( E_ATTACHMENT_PANED (view), context, x, y, selection, info, time); /* Stop the signal from propagating to GtkHtml. */ g_signal_stop_emission_by_name (widget, "drag-data-received"); } static void msg_composer_notify_header_cb (EMsgComposer *composer) { GtkhtmlEditor *editor; editor = GTKHTML_EDITOR (composer); gtkhtml_editor_set_changed (editor, TRUE); } static gboolean msg_composer_delete_event_cb (EMsgComposer *composer) { EShell *shell; GtkApplication *application; GList *windows; shell = e_msg_composer_get_shell (composer); /* If the "async" action group is insensitive, it means an * asynchronous operation is in progress. Block the event. */ if (!gtk_action_group_get_sensitive (composer->priv->async_actions)) return TRUE; application = GTK_APPLICATION (shell); windows = gtk_application_get_windows (application); if (g_list_length (windows) == 1) { /* This is the last watched window, use the quit * mechanism to have a draft saved properly */ e_shell_quit (shell, E_SHELL_QUIT_ACTION); } else { /* There are more watched windows opened, * invoke only a close action */ gtk_action_activate (ACTION (CLOSE)); } return TRUE; } static void msg_composer_prepare_for_quit_cb (EShell *shell, EActivity *activity, EMsgComposer *composer) { if (e_msg_composer_is_exiting (composer)) { /* needs save draft first */ g_object_ref (activity); g_object_weak_ref ( G_OBJECT (composer), (GWeakNotify) g_object_unref, activity); gtk_action_activate (ACTION (SAVE_DRAFT)); } } static void msg_composer_quit_requested_cb (EShell *shell, EShellQuitReason reason, EMsgComposer *composer) { if (e_msg_composer_is_exiting (composer)) { g_signal_handlers_disconnect_by_func ( shell, msg_composer_quit_requested_cb, composer); g_signal_handlers_disconnect_by_func ( shell, msg_composer_prepare_for_quit_cb, composer); } else if (!e_msg_composer_can_close (composer, FALSE) && !e_msg_composer_is_exiting (composer)) { e_shell_cancel_quit (shell); } } static void msg_composer_set_shell (EMsgComposer *composer, EShell *shell) { g_return_if_fail (E_IS_SHELL (shell)); g_return_if_fail (composer->priv->shell == NULL); composer->priv->shell = shell; g_object_add_weak_pointer ( G_OBJECT (shell), &composer->priv->shell); } static void msg_composer_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { switch (property_id) { case PROP_SHELL: msg_composer_set_shell ( E_MSG_COMPOSER (object), g_value_get_object (value)); return; } G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } static void msg_composer_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { switch (property_id) { case PROP_FOCUS_TRACKER: g_value_set_object ( value, e_msg_composer_get_focus_tracker ( E_MSG_COMPOSER (object))); return; case PROP_SHELL: g_value_set_object ( value, e_msg_composer_get_shell ( E_MSG_COMPOSER (object))); return; } G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); } static void msg_composer_finalize (GObject *object) { EMsgComposer *composer = E_MSG_COMPOSER (object); e_composer_private_finalize (composer); /* Chain up to parent's finalize() method. */ G_OBJECT_CLASS (e_msg_composer_parent_class)->finalize (object); } static void msg_composer_gallery_drag_data_get (GtkIconView *icon_view, GdkDragContext *context, GtkSelectionData *selection_data, guint target_type, guint time) { GtkTreePath *path; GtkCellRenderer *cell; GtkTreeModel *model; GtkTreeIter iter; GdkAtom target; gchar *str_data; if (!gtk_icon_view_get_cursor (icon_view, &path, &cell)) return; target = gtk_selection_data_get_target (selection_data); model = gtk_icon_view_get_model (icon_view); gtk_tree_model_get_iter (model, &iter, path); gtk_tree_model_get (model, &iter, 1, &str_data, -1); gtk_tree_path_free (path); /* only supports "text/uri-list" */ gtk_selection_data_set ( selection_data, target, 8, (guchar *) str_data, strlen (str_data)); g_free (str_data); } static void msg_composer_constructed (GObject *object) { EShell *shell; EShellSettings *shell_settings; GtkhtmlEditor *editor; EMsgComposer *composer; EAttachmentView *view; EAttachmentStore *store; EComposerHeaderTable *table; EWebViewGtkHTML *web_view; GtkUIManager *ui_manager; GtkToggleAction *action; const gchar *id; gboolean active; editor = GTKHTML_EDITOR (object); composer = E_MSG_COMPOSER (object); shell = e_msg_composer_get_shell (composer); shell_settings = e_shell_get_shell_settings (shell); if (e_shell_get_express_mode (shell)) { GtkWindow *parent = e_shell_get_active_window (shell); gtk_window_set_transient_for (GTK_WINDOW (composer), parent); } e_composer_private_constructed (composer); web_view = e_msg_composer_get_web_view (composer); ui_manager = gtkhtml_editor_get_ui_manager (editor); view = e_msg_composer_get_attachment_view (composer); table = E_COMPOSER_HEADER_TABLE (composer->priv->header_table); gtk_window_set_title (GTK_WINDOW (composer), _("Compose Message")); gtk_window_set_icon_name (GTK_WINDOW (composer), "mail-message-new"); gtk_window_set_default_size (GTK_WINDOW (composer), 600, 500); g_signal_connect ( object, "delete-event", G_CALLBACK (msg_composer_delete_event_cb), NULL); e_shell_adapt_window_size (shell, GTK_WINDOW (object)); gtk_application_add_window ( GTK_APPLICATION (shell), GTK_WINDOW (object)); g_signal_connect ( shell, "quit-requested", G_CALLBACK (msg_composer_quit_requested_cb), composer); g_signal_connect ( shell, "prepare-for-quit", G_CALLBACK (msg_composer_prepare_for_quit_cb), composer); /* Restore Persistent State */ e_restore_window ( GTK_WINDOW (composer), "/org/gnome/evolution/mail/composer-window/", E_RESTORE_WINDOW_SIZE); /* Honor User Preferences */ action = GTK_TOGGLE_ACTION (ACTION (REQUEST_READ_RECEIPT)); active = e_shell_settings_get_boolean ( shell_settings, "composer-request-receipt"); gtk_toggle_action_set_active (action, active); /* Clipboard Support */ g_signal_connect ( web_view, "paste-clipboard", G_CALLBACK (msg_composer_paste_clipboard_cb), composer); /* Drag-and-Drop Support */ g_signal_connect ( web_view, "realize", G_CALLBACK (msg_composer_realize_gtkhtml_cb), composer); g_signal_connect ( web_view, "drag-motion", G_CALLBACK (msg_composer_drag_motion_cb), composer); g_signal_connect ( web_view, "drag-data-received", G_CALLBACK (msg_composer_drag_data_received_cb), composer); g_signal_connect ( composer->priv->gallery_icon_view, "drag-data-get", G_CALLBACK (msg_composer_gallery_drag_data_get), NULL); /* Configure Headers */ e_composer_header_table_set_account_list ( table, e_get_account_list ()); e_composer_header_table_set_signature_list ( table, e_get_signature_list ()); g_signal_connect_swapped ( table, "notify::account", G_CALLBACK (msg_composer_account_changed_cb), composer); g_signal_connect_swapped ( table, "notify::destinations-bcc", G_CALLBACK (msg_composer_notify_header_cb), composer); g_signal_connect_swapped ( table, "notify::destinations-cc", G_CALLBACK (msg_composer_notify_header_cb), composer); g_signal_connect_swapped ( table, "notify::destinations-to", G_CALLBACK (msg_composer_notify_header_cb), composer); g_signal_connect_swapped ( table, "notify::reply-to", G_CALLBACK (msg_composer_notify_header_cb), composer); g_signal_connect_swapped ( table, "notify::signature", G_CALLBACK (e_msg_composer_show_sig_file), composer); g_signal_connect_swapped ( table, "notify::subject", G_CALLBACK (msg_composer_subject_changed_cb), composer); g_signal_connect_swapped ( table, "notify::subject", G_CALLBACK (msg_composer_notify_header_cb), composer); msg_composer_account_changed_cb (composer); /* Attachments */ store = e_attachment_view_get_store (view); g_signal_connect_swapped ( store, "row-deleted", G_CALLBACK (attachment_store_changed_cb), composer); g_signal_connect_swapped ( store, "row-inserted", G_CALLBACK (attachment_store_changed_cb), composer); /* Initialization may have tripped the "changed" state. */ gtkhtml_editor_set_changed (editor, FALSE); id = "org.gnome.evolution.composer"; e_plugin_ui_register_manager (ui_manager, id, composer); e_plugin_ui_enable_manager (ui_manager, id); e_extensible_load_extensions (E_EXTENSIBLE (composer)); /* Chain up to parent's constructed() method. */ G_OBJECT_CLASS (e_msg_composer_parent_class)->constructed (object); } static void msg_composer_dispose (GObject *object) { EMsgComposer *composer = E_MSG_COMPOSER (object); EShell *shell; if (composer->priv->address_dialog != NULL) { gtk_widget_destroy (composer->priv->address_dialog); composer->priv->address_dialog = NULL; } /* FIXME Our EShell is already unreferenced. */ shell = e_shell_get_default (); g_signal_handlers_disconnect_by_func ( shell, msg_composer_quit_requested_cb, composer); g_signal_handlers_disconnect_by_func ( shell, msg_composer_prepare_for_quit_cb, composer); e_composer_private_dispose (composer); /* Chain up to parent's dispose() method. */ G_OBJECT_CLASS (e_msg_composer_parent_class)->dispose (object); } static void msg_composer_map (GtkWidget *widget) { EComposerHeaderTable *table; GtkWidget *input_widget; const gchar *text; /* Chain up to parent's map() method. */ GTK_WIDGET_CLASS (e_msg_composer_parent_class)->map (widget); table = e_msg_composer_get_header_table (E_MSG_COMPOSER (widget)); /* If the 'To' field is empty, focus it. */ input_widget = e_composer_header_table_get_header ( table, E_COMPOSER_HEADER_TO)->input_widget; text = gtk_entry_get_text (GTK_ENTRY (input_widget)); if (gtk_widget_get_visible (input_widget) && (text == NULL || *text == '\0')) { gtk_widget_grab_focus (input_widget); return; } /* If not, check the 'Subject' field. */ input_widget = e_composer_header_table_get_header ( table, E_COMPOSER_HEADER_SUBJECT)->input_widget; text = gtk_entry_get_text (GTK_ENTRY (input_widget)); if (gtk_widget_get_visible (input_widget) && (text == NULL || *text == '\0')) { gtk_widget_grab_focus (input_widget); return; } /* Jump to the editor as a last resort. */ gtkhtml_editor_run_command (GTKHTML_EDITOR (widget), "grab-focus"); } static gboolean msg_composer_key_press_event (GtkWidget *widget, GdkEventKey *event) { EMsgComposer *composer = E_MSG_COMPOSER (widget); GtkWidget *input_widget; GtkhtmlEditor *editor; EWebViewGtkHTML *web_view; editor = GTKHTML_EDITOR (widget); composer = E_MSG_COMPOSER (widget); web_view = e_msg_composer_get_web_view (composer); input_widget = e_composer_header_table_get_header ( e_msg_composer_get_header_table (composer), E_COMPOSER_HEADER_SUBJECT)->input_widget; #ifdef HAVE_XFREE if (event->keyval == XF86XK_Send) { e_msg_composer_send (composer); return TRUE; } #endif /* HAVE_XFREE */ if (event->keyval == GDK_KEY_Escape) { gtk_action_activate (ACTION (CLOSE)); return TRUE; } if (event->keyval == GDK_KEY_Tab && gtk_widget_is_focus (input_widget)) { gtkhtml_editor_run_command (editor, "grab-focus"); return TRUE; } if (event->keyval == GDK_KEY_ISO_Left_Tab && gtk_widget_is_focus (GTK_WIDGET (web_view))) { gtk_widget_grab_focus (input_widget); return TRUE; } /* Chain up to parent's key_press_event() method. */ return GTK_WIDGET_CLASS (e_msg_composer_parent_class)-> key_press_event (widget, event); } static void msg_composer_cut_clipboard (GtkhtmlEditor *editor) { /* Do nothing. EFocusTracker handles this. */ } static void msg_composer_copy_clipboard (GtkhtmlEditor *editor) { /* Do nothing. EFocusTracker handles this. */ } static void msg_composer_paste_clipboard (GtkhtmlEditor *editor) { /* Do nothing. EFocusTracker handles this. */ } static void msg_composer_select_all (GtkhtmlEditor *editor) { /* Do nothing. EFocusTracker handles this. */ } static void msg_composer_command_before (GtkhtmlEditor *editor, const gchar *command) { EMsgComposer *composer; const gchar *data; composer = E_MSG_COMPOSER (editor); if (strcmp (command, "insert-paragraph") != 0) return; if (composer->priv->in_signature_insert) return; data = gtkhtml_editor_get_paragraph_data (editor, "orig"); if (data != NULL && *data == '1') { gtkhtml_editor_run_command (editor, "text-default-color"); gtkhtml_editor_run_command (editor, "italic-off"); return; }; data = gtkhtml_editor_get_paragraph_data (editor, "signature"); if (data != NULL && *data == '1') { gtkhtml_editor_run_command (editor, "text-default-color"); gtkhtml_editor_run_command (editor, "italic-off"); } } static void msg_composer_command_after (GtkhtmlEditor *editor, const gchar *command) { EMsgComposer *composer; const gchar *data; composer = E_MSG_COMPOSER (editor); if (strcmp (command, "insert-paragraph") != 0) return; if (composer->priv->in_signature_insert) return; gtkhtml_editor_run_command (editor, "italic-off"); data = gtkhtml_editor_get_paragraph_data (editor, "orig"); if (data != NULL && *data == '1') e_msg_composer_reply_indent (composer); gtkhtml_editor_set_paragraph_data (editor, "orig", "0"); data = gtkhtml_editor_get_paragraph_data (editor, "signature"); if (data == NULL || *data != '1') return; /* Clear the signature. */ if (gtkhtml_editor_is_paragraph_empty (editor)) gtkhtml_editor_set_paragraph_data (editor, "signature" ,"0"); else if (gtkhtml_editor_is_previous_paragraph_empty (editor) && gtkhtml_editor_run_command (editor, "cursor-backward")) { gtkhtml_editor_set_paragraph_data (editor, "signature", "0"); gtkhtml_editor_run_command (editor, "cursor-forward"); } gtkhtml_editor_run_command (editor, "text-default-color"); gtkhtml_editor_run_command (editor, "italic-off"); } static gchar * msg_composer_image_uri (GtkhtmlEditor *editor, const gchar *uri) { EMsgComposer *composer; GHashTable *hash_table; CamelMimePart *part; const gchar *cid; composer = E_MSG_COMPOSER (editor); hash_table = composer->priv->inline_images_by_url; part = g_hash_table_lookup (hash_table, uri); if (part == NULL && g_str_has_prefix (uri, "file:")) part = e_msg_composer_add_inline_image_from_file ( composer, uri + 5); if (part == NULL && g_str_has_prefix (uri, "cid:")) { hash_table = composer->priv->inline_images; part = g_hash_table_lookup (hash_table, uri); } if (part == NULL) return NULL; composer->priv->current_images = g_list_prepend (composer->priv->current_images, part); cid = camel_mime_part_get_content_id (part); if (cid == NULL) return NULL; return g_strconcat ("cid:", cid, NULL); } static void msg_composer_object_deleted (GtkhtmlEditor *editor) { const gchar *data; if (!gtkhtml_editor_is_paragraph_empty (editor)) return; data = gtkhtml_editor_get_paragraph_data (editor, "orig"); if (data != NULL && *data == '1') { gtkhtml_editor_set_paragraph_data (editor, "orig", "0"); gtkhtml_editor_run_command (editor, "indent-zero"); gtkhtml_editor_run_command (editor, "style-normal"); gtkhtml_editor_run_command (editor, "text-default-color"); gtkhtml_editor_run_command (editor, "italic-off"); gtkhtml_editor_run_command (editor, "insert-paragraph"); gtkhtml_editor_run_command (editor, "delete-back"); } data = gtkhtml_editor_get_paragraph_data (editor, "signature"); if (data != NULL && *data == '1') gtkhtml_editor_set_paragraph_data (editor, "signature", "0"); } static gboolean msg_composer_presend (EMsgComposer *composer) { /* This keeps the signal accumulator at TRUE. */ return TRUE; } static void msg_composer_submit_alert (EAlertSink *alert_sink, EAlert *alert) { EMsgComposerPrivate *priv; EAlertBar *alert_bar; GtkWidget *dialog; GtkWindow *parent; priv = E_MSG_COMPOSER_GET_PRIVATE (alert_sink); switch (e_alert_get_message_type (alert)) { case GTK_MESSAGE_INFO: case GTK_MESSAGE_WARNING: case GTK_MESSAGE_ERROR: alert_bar = E_ALERT_BAR (priv->alert_bar); e_alert_bar_add_alert (alert_bar, alert); break; default: parent = GTK_WINDOW (alert_sink); dialog = e_alert_dialog_new (parent, alert); gtk_dialog_run (GTK_DIALOG (dialog)); gtk_widget_destroy (dialog); break; } } static gboolean msg_composer_accumulator_false_abort (GSignalInvocationHint *ihint, GValue *return_accu, const GValue *handler_return, gpointer dummy) { gboolean v_boolean; v_boolean = g_value_get_boolean (handler_return); g_value_set_boolean (return_accu, v_boolean); /* FALSE means abort the signal emission. */ return v_boolean; } static void e_msg_composer_class_init (EMsgComposerClass *class) { GObjectClass *object_class; GtkWidgetClass *widget_class; GtkhtmlEditorClass *editor_class; g_type_class_add_private (class, sizeof (EMsgComposerPrivate)); object_class = G_OBJECT_CLASS (class); object_class->set_property = msg_composer_set_property; object_class->get_property = msg_composer_get_property; object_class->dispose = msg_composer_dispose; object_class->finalize = msg_composer_finalize; object_class->constructed = msg_composer_constructed; widget_class = GTK_WIDGET_CLASS (class); widget_class->map = msg_composer_map; widget_class->key_press_event = msg_composer_key_press_event; editor_class = GTKHTML_EDITOR_CLASS (class); editor_class->cut_clipboard = msg_composer_cut_clipboard; editor_class->copy_clipboard = msg_composer_copy_clipboard; editor_class->paste_clipboard = msg_composer_paste_clipboard; editor_class->select_all = msg_composer_select_all; editor_class->command_before = msg_composer_command_before; editor_class->command_after = msg_composer_command_after; editor_class->image_uri = msg_composer_image_uri; editor_class->link_clicked = NULL; /* EWebView handles this */ editor_class->object_deleted = msg_composer_object_deleted; class->presend = msg_composer_presend; g_object_class_install_property ( object_class, PROP_FOCUS_TRACKER, g_param_spec_object ( "focus-tracker", NULL, NULL, E_TYPE_FOCUS_TRACKER, G_PARAM_READABLE)); g_object_class_install_property ( object_class, PROP_SHELL, g_param_spec_object ( "shell", "Shell", "The EShell singleton", E_TYPE_SHELL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); signals[PRESEND] = g_signal_new ( "presend", G_OBJECT_CLASS_TYPE (class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (EMsgComposerClass, presend), msg_composer_accumulator_false_abort, NULL, e_marshal_BOOLEAN__VOID, G_TYPE_BOOLEAN, 0); signals[SEND] = g_signal_new ( "send", G_OBJECT_CLASS_TYPE (class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (EMsgComposerClass, send), NULL, NULL, e_marshal_VOID__OBJECT_OBJECT, G_TYPE_NONE, 2, CAMEL_TYPE_MIME_MESSAGE, E_TYPE_ACTIVITY); signals[SAVE_TO_DRAFTS] = g_signal_new ( "save-to-drafts", G_OBJECT_CLASS_TYPE (class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (EMsgComposerClass, save_to_drafts), NULL, NULL, e_marshal_VOID__OBJECT_OBJECT, G_TYPE_NONE, 2, CAMEL_TYPE_MIME_MESSAGE, E_TYPE_ACTIVITY); signals[SAVE_TO_OUTBOX] = g_signal_new ( "save-to-outbox", G_OBJECT_CLASS_TYPE (class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (EMsgComposerClass, save_to_outbox), NULL, NULL, e_marshal_VOID__OBJECT_OBJECT, G_TYPE_NONE, 2, CAMEL_TYPE_MIME_MESSAGE, E_TYPE_ACTIVITY); signals[PRINT] = g_signal_new ( "print", G_OBJECT_CLASS_TYPE (class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, e_marshal_VOID__ENUM_OBJECT_OBJECT, G_TYPE_NONE, 3, GTK_TYPE_PRINT_OPERATION_ACTION, CAMEL_TYPE_MIME_MESSAGE, E_TYPE_ACTIVITY); } static void e_msg_composer_alert_sink_init (EAlertSinkInterface *interface) { interface->submit_alert = msg_composer_submit_alert; } static void e_msg_composer_init (EMsgComposer *composer) { composer->priv = E_MSG_COMPOSER_GET_PRIVATE (composer); } /** * e_msg_composer_new: * @shell: an #EShell * * Create a new message composer widget. * * Returns: A pointer to the newly created widget **/ EMsgComposer * e_msg_composer_new (EShell *shell) { g_return_val_if_fail (E_IS_SHELL (shell), NULL); return g_object_new ( E_TYPE_MSG_COMPOSER, "html", e_web_view_gtkhtml_new (), "shell", shell, NULL); } EFocusTracker * e_msg_composer_get_focus_tracker (EMsgComposer *composer) { g_return_val_if_fail (E_IS_MSG_COMPOSER (composer), NULL); return composer->priv->focus_tracker; } static void e_msg_composer_set_pending_body (EMsgComposer *composer, gchar *text, gssize length) { g_object_set_data_full ( G_OBJECT (composer), "body:text", text, (GDestroyNotify) g_free); } static void e_msg_composer_flush_pending_body (EMsgComposer *composer) { const gchar *body; body = g_object_get_data (G_OBJECT (composer), "body:text"); if (body != NULL) set_editor_text (composer, body, FALSE); g_object_set_data (G_OBJECT (composer), "body:text", NULL); } static void add_attachments_handle_mime_part (EMsgComposer *composer, CamelMimePart *mime_part, gboolean just_inlines, gboolean related, gint depth) { CamelContentType *content_type; CamelDataWrapper *wrapper; if (!mime_part) return; content_type = camel_mime_part_get_content_type (mime_part); wrapper = camel_medium_get_content (CAMEL_MEDIUM (mime_part)); if (CAMEL_IS_MULTIPART (wrapper)) { /* another layer of multipartness... */ add_attachments_from_multipart ( composer, (CamelMultipart *) wrapper, just_inlines, depth + 1); } else if (just_inlines) { if (camel_mime_part_get_content_id (mime_part) || camel_mime_part_get_content_location (mime_part)) e_msg_composer_add_inline_image_from_mime_part ( composer, mime_part); } else if (related && camel_content_type_is (content_type, "image", "*")) { e_msg_composer_add_inline_image_from_mime_part (composer, mime_part); } else if (camel_content_type_is (content_type, "text", "*") && camel_mime_part_get_filename (mime_part) == NULL) { /* Do nothing if this is a text/anything without a * filename, otherwise attach it too. */ } else { e_msg_composer_attach (composer, mime_part); } } static void add_attachments_from_multipart (EMsgComposer *composer, CamelMultipart *multipart, gboolean just_inlines, gint depth) { /* find appropriate message attachments to add to the composer */ CamelMimePart *mime_part; gboolean related; gint i, nparts; related = camel_content_type_is ( CAMEL_DATA_WRAPPER (multipart)->mime_type, "multipart", "related"); if (CAMEL_IS_MULTIPART_SIGNED (multipart)) { mime_part = camel_multipart_get_part ( multipart, CAMEL_MULTIPART_SIGNED_CONTENT); add_attachments_handle_mime_part ( composer, mime_part, just_inlines, related, depth); } else if (CAMEL_IS_MULTIPART_ENCRYPTED (multipart)) { /* XXX What should we do in this case? */ } else { nparts = camel_multipart_get_number (multipart); for (i = 0; i < nparts; i++) { mime_part = camel_multipart_get_part (multipart, i); add_attachments_handle_mime_part ( composer, mime_part, just_inlines, related, depth); } } } /** * e_msg_composer_add_message_attachments: * @composer: the composer to add the attachments to. * @message: the source message to copy the attachments from. * @just_inlines: whether to attach all attachments or just add * inline images. * * Walk through all the mime parts in @message and add them to the composer * specified in @composer. */ void e_msg_composer_add_message_attachments (EMsgComposer *composer, CamelMimeMessage *message, gboolean just_inlines) { CamelDataWrapper *wrapper; wrapper = camel_medium_get_content (CAMEL_MEDIUM (message)); if (!CAMEL_IS_MULTIPART (wrapper)) return; add_attachments_from_multipart ( composer, (CamelMultipart *) wrapper, just_inlines, 0); } static void handle_multipart_signed (EMsgComposer *composer, CamelMultipart *multipart, GCancellable *cancellable, gint depth) { CamelContentType *content_type; CamelDataWrapper *content; CamelMimePart *mime_part; GtkToggleAction *action = NULL; const gchar *protocol; content = CAMEL_DATA_WRAPPER (multipart); content_type = camel_data_wrapper_get_mime_type_field (content); protocol = camel_content_type_param (content_type, "protocol"); if (protocol == NULL) action = NULL; else if (g_ascii_strcasecmp (protocol, "application/pgp-signature") == 0) action = GTK_TOGGLE_ACTION (ACTION (PGP_SIGN)); else if (g_ascii_strcasecmp (protocol, "application/x-pkcs7-signature") == 0) action = GTK_TOGGLE_ACTION (ACTION (SMIME_SIGN)); if (action) gtk_toggle_action_set_active (action, TRUE); mime_part = camel_multipart_get_part ( multipart, CAMEL_MULTIPART_SIGNED_CONTENT); if (mime_part == NULL) return; content_type = camel_mime_part_get_content_type (mime_part); content = camel_medium_get_content (CAMEL_MEDIUM (mime_part)); if (CAMEL_IS_MULTIPART (content)) { multipart = CAMEL_MULTIPART (content); /* Note: depth is preserved here because we're not * counting multipart/signed as a multipart, instead * we want to treat the content part as our mime part * here. */ if (CAMEL_IS_MULTIPART_SIGNED (content)) { /* Handle the signed content and configure * the composer to sign outgoing messages. */ handle_multipart_signed ( composer, multipart, cancellable, depth); } else if (CAMEL_IS_MULTIPART_ENCRYPTED (content)) { /* Decrypt the encrypted content and configure * the composer to encrypt outgoing messages. */ handle_multipart_encrypted ( composer, mime_part, cancellable, depth); } else if (camel_content_type_is (content_type, "multipart", "alternative")) { /* This contains the text/plain and text/html * versions of the message body. */ handle_multipart_alternative ( composer, multipart, cancellable, depth); } else { /* There must be attachments... */ handle_multipart ( composer, multipart, cancellable, depth); } } else if (camel_content_type_is (content_type, "text", "*")) { gchar *html; gssize length; html = emcu_part_to_html (mime_part, &length, NULL, cancellable); e_msg_composer_set_pending_body (composer, html, length); } else { e_msg_composer_attach (composer, mime_part); } } static void handle_multipart_encrypted (EMsgComposer *composer, CamelMimePart *multipart, GCancellable *cancellable, gint depth) { CamelContentType *content_type; CamelCipherContext *cipher; CamelDataWrapper *content; CamelMimePart *mime_part; CamelSession *session; CamelCipherValidity *valid; GtkToggleAction *action = NULL; const gchar *protocol; content_type = camel_mime_part_get_content_type (multipart); protocol = camel_content_type_param (content_type, "protocol"); if (protocol && g_ascii_strcasecmp (protocol, "application/pgp-encrypted") == 0) action = GTK_TOGGLE_ACTION (ACTION (PGP_ENCRYPT)); else if (content_type && ( camel_content_type_is (content_type, "application", "x-pkcs7-mime") || camel_content_type_is (content_type, "application", "pkcs7-mime"))) action = GTK_TOGGLE_ACTION (ACTION (SMIME_ENCRYPT)); if (action) gtk_toggle_action_set_active (action, TRUE); session = e_msg_composer_get_session (composer); cipher = camel_gpg_context_new (session); mime_part = camel_mime_part_new (); valid = camel_cipher_context_decrypt_sync ( cipher, multipart, mime_part, cancellable, NULL); g_object_unref (cipher); if (valid == NULL) return; camel_cipher_validity_free (valid); content_type = camel_mime_part_get_content_type (mime_part); content = camel_medium_get_content (CAMEL_MEDIUM (mime_part)); if (CAMEL_IS_MULTIPART (content)) { CamelMultipart *content_multipart = CAMEL_MULTIPART (content); /* Note: depth is preserved here because we're not * counting multipart/encrypted as a multipart, instead * we want to treat the content part as our mime part * here. */ if (CAMEL_IS_MULTIPART_SIGNED (content)) { /* Handle the signed content and configure the * composer to sign outgoing messages. */ handle_multipart_signed ( composer, content_multipart, cancellable, depth); } else if (CAMEL_IS_MULTIPART_ENCRYPTED (content)) { /* Decrypt the encrypted content and configure the * composer to encrypt outgoing messages. */ handle_multipart_encrypted ( composer, mime_part, cancellable, depth); } else if (camel_content_type_is (content_type, "multipart", "alternative")) { /* This contains the text/plain and text/html * versions of the message body. */ handle_multipart_alternative ( composer, content_multipart, cancellable, depth); } else { /* There must be attachments... */ handle_multipart ( composer, content_multipart, cancellable, depth); } } else if (camel_content_type_is (content_type, "text", "*")) { gchar *html; gssize length; html = emcu_part_to_html (mime_part, &length, NULL, cancellable); e_msg_composer_set_pending_body (composer, html, length); } else { e_msg_composer_attach (composer, mime_part); } g_object_unref (mime_part); } static void handle_multipart_alternative (EMsgComposer *composer, CamelMultipart *multipart, GCancellable *cancellable, gint depth) { /* Find the text/html part and set the composer body to it's contents */ CamelMimePart *text_part = NULL; gint i, nparts; nparts = camel_multipart_get_number (multipart); for (i = 0; i < nparts; i++) { CamelContentType *content_type; CamelDataWrapper *content; CamelMimePart *mime_part; mime_part = camel_multipart_get_part (multipart, i); if (!mime_part) continue; content_type = camel_mime_part_get_content_type (mime_part); content = camel_medium_get_content (CAMEL_MEDIUM (mime_part)); if (CAMEL_IS_MULTIPART (content)) { CamelMultipart *mp; mp = CAMEL_MULTIPART (content); if (CAMEL_IS_MULTIPART_SIGNED (content)) { /* Handle the signed content and configure * the composer to sign outgoing messages. */ handle_multipart_signed ( composer, mp, cancellable, depth + 1); } else if (CAMEL_IS_MULTIPART_ENCRYPTED (content)) { /* Decrypt the encrypted content and configure * the composer to encrypt outgoing messages. */ handle_multipart_encrypted ( composer, mime_part, cancellable, depth + 1); } else { /* Depth doesn't matter so long as we * don't pass 0. */ handle_multipart ( composer, mp, cancellable, depth + 1); } } else if (camel_content_type_is (content_type, "text", "html")) { /* text/html is preferable, so once we find it we're done... */ text_part = mime_part; break; } else if (camel_content_type_is (content_type, "text", "*")) { /* anyt text part not text/html is second rate so the first * text part we find isn't necessarily the one we'll use. */ if (!text_part) text_part = mime_part; } else { e_msg_composer_attach (composer, mime_part); } } if (text_part) { gchar *html; gssize length; html = emcu_part_to_html (text_part, &length, NULL, cancellable); e_msg_composer_set_pending_body (composer, html, length); } } static void handle_multipart (EMsgComposer *composer, CamelMultipart *multipart, GCancellable *cancellable, gint depth) { gint i, nparts; nparts = camel_multipart_get_number (multipart); for (i = 0; i < nparts; i++) { CamelContentType *content_type; CamelDataWrapper *content; CamelMimePart *mime_part; mime_part = camel_multipart_get_part (multipart, i); if (!mime_part) continue; content_type = camel_mime_part_get_content_type (mime_part); content = camel_medium_get_content (CAMEL_MEDIUM (mime_part)); if (CAMEL_IS_MULTIPART (content)) { CamelMultipart *mp; mp = CAMEL_MULTIPART (content); if (CAMEL_IS_MULTIPART_SIGNED (content)) { /* Handle the signed content and configure * the composer to sign outgoing messages. */ handle_multipart_signed ( composer, mp, cancellable, depth + 1); } else if (CAMEL_IS_MULTIPART_ENCRYPTED (content)) { /* Decrypt the encrypted content and configure * the composer to encrypt outgoing messages. */ handle_multipart_encrypted ( composer, mime_part, cancellable, depth + 1); } else if (camel_content_type_is ( content_type, "multipart", "alternative")) { handle_multipart_alternative ( composer, mp, cancellable, depth + 1); } else { /* Depth doesn't matter so long as we * don't pass 0. */ handle_multipart ( composer, mp, cancellable, depth + 1); } } else if (depth == 0 && i == 0) { gchar *html; gssize length; /* Since the first part is not multipart/alternative, * this must be the body. */ html = emcu_part_to_html ( mime_part, &length, NULL, cancellable); e_msg_composer_set_pending_body (composer, html, length); } else if (camel_mime_part_get_content_id (mime_part) || camel_mime_part_get_content_location (mime_part)) { /* special in-line attachment */ e_msg_composer_add_inline_image_from_mime_part ( composer, mime_part); } else { /* normal attachment */ e_msg_composer_attach (composer, mime_part); } } } static void set_signature_gui (EMsgComposer *composer) { GtkhtmlEditor *editor; EComposerHeaderTable *table; ESignature *signature = NULL; const gchar *data; gchar *decoded; editor = GTKHTML_EDITOR (composer); table = e_msg_composer_get_header_table (composer); if (!gtkhtml_editor_search_by_data (editor, 1, "ClueFlow", "signature", "1")) return; data = gtkhtml_editor_get_paragraph_data (editor, "signature_name"); if (g_str_has_prefix (data, "uid:")) { decoded = e_composer_decode_clue_value (data + 4); signature = e_get_signature_by_uid (decoded); g_free (decoded); } else if (g_str_has_prefix (data, "name:")) { decoded = e_composer_decode_clue_value (data + 5); signature = e_get_signature_by_name (decoded); g_free (decoded); } e_composer_header_table_set_signature (table, signature); } /** * e_msg_composer_new_with_message: * @shell: an #EShell * @message: The message to use as the source * * Create a new message composer widget. * * Note: Designed to work only for messages constructed using Evolution. * * Returns: A pointer to the newly created widget **/ EMsgComposer * e_msg_composer_new_with_message (EShell *shell, CamelMimeMessage *message, GCancellable *cancellable) { CamelInternetAddress *to, *cc, *bcc; GList *To = NULL, *Cc = NULL, *Bcc = NULL, *postto = NULL; const gchar *format, *subject; EDestination **Tov, **Ccv, **Bccv; GHashTable *auto_cc, *auto_bcc; CamelContentType *content_type; struct _camel_header_raw *headers; CamelDataWrapper *content; EAccount *account = NULL; gchar *account_name; EMsgComposer *composer; EMsgComposerPrivate *priv; EComposerHeaderTable *table; GtkToggleAction *action; struct _camel_header_raw *xev; gint len, i; g_return_val_if_fail (E_IS_SHELL (shell), NULL); headers = CAMEL_MIME_PART (message)->headers; while (headers != NULL) { gchar *value; if (strcmp (headers->name, "X-Evolution-PostTo") == 0) { value = g_strstrip (g_strdup (headers->value)); postto = g_list_append (postto, value); } headers = headers->next; } composer = e_msg_composer_new (shell); priv = E_MSG_COMPOSER_GET_PRIVATE (composer); table = e_msg_composer_get_header_table (composer); if (postto) { e_composer_header_table_set_post_to_list (table, postto); g_list_foreach (postto, (GFunc) g_free, NULL); g_list_free (postto); postto = NULL; } /* Restore the Account preference */ account_name = (gchar *) camel_medium_get_header ( CAMEL_MEDIUM (message), "X-Evolution-Account"); if (account_name) { account_name = g_strdup (account_name); g_strstrip (account_name); account = e_get_account_by_uid (account_name); if (account == NULL) /* XXX Backwards compatibility */ account = e_get_account_by_name (account_name); if (account != NULL) { g_free (account_name); account_name = g_strdup (account->name); } } if (postto == NULL) { auto_cc = g_hash_table_new_full ( camel_strcase_hash, camel_strcase_equal, (GDestroyNotify) g_free, (GDestroyNotify) NULL); auto_bcc = g_hash_table_new_full ( camel_strcase_hash, camel_strcase_equal, (GDestroyNotify) g_free, (GDestroyNotify) NULL); if (account) { CamelInternetAddress *iaddr; /* hash our auto-recipients for this account */ if (account->always_cc) { iaddr = camel_internet_address_new (); if (camel_address_decode (CAMEL_ADDRESS (iaddr), account->cc_addrs) != -1) { for (i = 0; i < camel_address_length (CAMEL_ADDRESS (iaddr)); i++) { const gchar *name, *addr; if (!camel_internet_address_get (iaddr, i, &name, &addr)) continue; g_hash_table_insert ( auto_cc, g_strdup (addr), GINT_TO_POINTER (TRUE)); } } g_object_unref (iaddr); } if (account->always_bcc) { iaddr = camel_internet_address_new (); if (camel_address_decode (CAMEL_ADDRESS (iaddr), account->bcc_addrs) != -1) { for (i = 0; i < camel_address_length (CAMEL_ADDRESS (iaddr)); i++) { const gchar *name, *addr; if (!camel_internet_address_get (iaddr, i, &name, &addr)) continue; g_hash_table_insert ( auto_bcc, g_strdup (addr), GINT_TO_POINTER (TRUE)); } } g_object_unref (iaddr); } } to = camel_mime_message_get_recipients (message, CAMEL_RECIPIENT_TYPE_TO); cc = camel_mime_message_get_recipients (message, CAMEL_RECIPIENT_TYPE_CC); bcc = camel_mime_message_get_recipients (message, CAMEL_RECIPIENT_TYPE_BCC); len = CAMEL_ADDRESS (to)->addresses->len; for (i = 0; i < len; i++) { const gchar *name, *addr; if (camel_internet_address_get (to, i, &name, &addr)) { EDestination *dest = e_destination_new (); e_destination_set_name (dest, name); e_destination_set_email (dest, addr); To = g_list_append (To, dest); } } Tov = destination_list_to_vector (To); g_list_free (To); len = CAMEL_ADDRESS (cc)->addresses->len; for (i = 0; i < len; i++) { const gchar *name, *addr; if (camel_internet_address_get (cc, i, &name, &addr)) { EDestination *dest = e_destination_new (); e_destination_set_name (dest, name); e_destination_set_email (dest, addr); if (g_hash_table_lookup (auto_cc, addr)) e_destination_set_auto_recipient (dest, TRUE); Cc = g_list_append (Cc, dest); } } Ccv = destination_list_to_vector (Cc); g_hash_table_destroy (auto_cc); g_list_free (Cc); len = CAMEL_ADDRESS (bcc)->addresses->len; for (i = 0; i < len; i++) { const gchar *name, *addr; if (camel_internet_address_get (bcc, i, &name, &addr)) { EDestination *dest = e_destination_new (); e_destination_set_name (dest, name); e_destination_set_email (dest, addr); if (g_hash_table_lookup (auto_bcc, addr)) e_destination_set_auto_recipient (dest, TRUE); Bcc = g_list_append (Bcc, dest); } } Bccv = destination_list_to_vector (Bcc); g_hash_table_destroy (auto_bcc); g_list_free (Bcc); } else { Tov = NULL; Ccv = NULL; Bccv = NULL; } subject = camel_mime_message_get_subject (message); e_composer_header_table_set_account_name (table, account_name); e_composer_header_table_set_destinations_to (table, Tov); e_composer_header_table_set_destinations_cc (table, Ccv); e_composer_header_table_set_destinations_bcc (table, Bccv); e_composer_header_table_set_subject (table, subject); g_free (account_name); e_destination_freev (Tov); e_destination_freev (Ccv); e_destination_freev (Bccv); /* Restore the format editing preference */ format = camel_medium_get_header ( CAMEL_MEDIUM (message), "X-Evolution-Format"); if (format != NULL) { gchar **flags; while (*format && camel_mime_is_lwsp (*format)) format++; flags = g_strsplit (format, ", ", 0); for (i = 0; flags[i]; i++) { if (g_ascii_strcasecmp (flags[i], "text/html") == 0) gtkhtml_editor_set_html_mode ( GTKHTML_EDITOR (composer), TRUE); else if (g_ascii_strcasecmp (flags[i], "text/plain") == 0) gtkhtml_editor_set_html_mode ( GTKHTML_EDITOR (composer), FALSE); else if (g_ascii_strcasecmp (flags[i], "pgp-sign") == 0) { action = GTK_TOGGLE_ACTION (ACTION (PGP_SIGN)); gtk_toggle_action_set_active (action, TRUE); } else if (g_ascii_strcasecmp (flags[i], "pgp-encrypt") == 0) { action = GTK_TOGGLE_ACTION (ACTION (PGP_ENCRYPT)); gtk_toggle_action_set_active (action, TRUE); } else if (g_ascii_strcasecmp (flags[i], "smime-sign") == 0) { action = GTK_TOGGLE_ACTION (ACTION (SMIME_SIGN)); gtk_toggle_action_set_active (action, TRUE); } else if (g_ascii_strcasecmp (flags[i], "smime-encrypt") == 0) { action = GTK_TOGGLE_ACTION (ACTION (SMIME_ENCRYPT)); gtk_toggle_action_set_active (action, TRUE); } } g_strfreev (flags); } /* Remove any other X-Evolution-* headers that may have been set */ xev = emcu_remove_xevolution_headers (message); camel_header_raw_clear (&xev); /* Check for receipt request */ if (camel_medium_get_header ( CAMEL_MEDIUM (message), "Disposition-Notification-To")) { action = GTK_TOGGLE_ACTION (ACTION (REQUEST_READ_RECEIPT)); gtk_toggle_action_set_active (action, TRUE); } /* Check for mail priority */ if (camel_medium_get_header (CAMEL_MEDIUM (message), "X-Priority")) { action = GTK_TOGGLE_ACTION (ACTION (PRIORITIZE_MESSAGE)); gtk_toggle_action_set_active (action, TRUE); } /* set extra headers */ headers = CAMEL_MIME_PART (message)->headers; while (headers) { if (g_ascii_strcasecmp (headers->name, "References") == 0 || g_ascii_strcasecmp (headers->name, "In-Reply-To") == 0) { g_ptr_array_add ( composer->priv->extra_hdr_names, g_strdup (headers->name)); g_ptr_array_add ( composer->priv->extra_hdr_values, g_strdup (headers->value)); } headers = headers->next; } /* Restore the attachments and body text */ content = camel_medium_get_content (CAMEL_MEDIUM (message)); if (CAMEL_IS_MULTIPART (content)) { CamelMimePart *mime_part; CamelMultipart *multipart; multipart = CAMEL_MULTIPART (content); mime_part = CAMEL_MIME_PART (message); content_type = camel_mime_part_get_content_type (mime_part); if (CAMEL_IS_MULTIPART_SIGNED (content)) { /* Handle the signed content and configure the * composer to sign outgoing messages. */ handle_multipart_signed ( composer, multipart, cancellable, 0); } else if (CAMEL_IS_MULTIPART_ENCRYPTED (content)) { /* Decrypt the encrypted content and configure the * composer to encrypt outgoing messages. */ handle_multipart_encrypted ( composer, mime_part, cancellable, 0); } else if (camel_content_type_is ( content_type, "multipart", "alternative")) { /* This contains the text/plain and text/html * versions of the message body. */ handle_multipart_alternative ( composer, multipart, cancellable, 0); } else { /* There must be attachments... */ handle_multipart ( composer, multipart, cancellable, 0); } } else { CamelMimePart *mime_part; gchar *html; gssize length; mime_part = CAMEL_MIME_PART (message); content_type = camel_mime_part_get_content_type (mime_part); if (content_type != NULL && ( camel_content_type_is ( content_type, "application", "x-pkcs7-mime") || camel_content_type_is ( content_type, "application", "pkcs7-mime"))) gtk_toggle_action_set_active ( GTK_TOGGLE_ACTION ( ACTION (SMIME_ENCRYPT)), TRUE); html = emcu_part_to_html ( CAMEL_MIME_PART (message), &length, NULL, cancellable); e_msg_composer_set_pending_body (composer, html, length); } priv->is_from_message = TRUE; /* We wait until now to set the body text because we need to * ensure that the attachment bar has all the attachments before * we request them. */ e_msg_composer_flush_pending_body (composer); set_signature_gui (composer); return composer; } /** * e_msg_composer_new_redirect: * @shell: an #EShell * @message: The message to use as the source * * Create a new message composer widget. * * Returns: A pointer to the newly created widget **/ EMsgComposer * e_msg_composer_new_redirect (EShell *shell, CamelMimeMessage *message, const gchar *resent_from, GCancellable *cancellable) { EMsgComposer *composer; EComposerHeaderTable *table; EWebViewGtkHTML *web_view; const gchar *subject; g_return_val_if_fail (E_IS_SHELL (shell), NULL); g_return_val_if_fail (CAMEL_IS_MIME_MESSAGE (message), NULL); composer = e_msg_composer_new_with_message ( shell, message, cancellable); table = e_msg_composer_get_header_table (composer); subject = camel_mime_message_get_subject (message); composer->priv->redirect = message; g_object_ref (message); e_composer_header_table_set_account_name (table, resent_from); e_composer_header_table_set_subject (table, subject); web_view = e_msg_composer_get_web_view (composer); e_web_view_gtkhtml_set_editable (web_view, FALSE); return composer; } /** * e_msg_composer_get_session: * @composer: an #EMsgComposer * * Returns the mail module's global #CamelSession instance. Calling * this function will load the mail module if it isn't already loaded. * * Returns: the mail module's #CamelSession **/ CamelSession * e_msg_composer_get_session (EMsgComposer *composer) { EShell *shell; EShellSettings *shell_settings; CamelSession *session; g_return_val_if_fail (E_IS_MSG_COMPOSER (composer), NULL); shell = e_msg_composer_get_shell (composer); shell_settings = e_shell_get_shell_settings (shell); session = e_shell_settings_get_pointer (shell_settings, "mail-session"); g_return_val_if_fail (CAMEL_IS_SESSION (session), NULL); return session; } /** * e_msg_composer_get_shell: * @composer: an #EMsgComposer * * Returns the #EShell that was passed to e_msg_composer_new(). * * Returns: the #EShell **/ EShell * e_msg_composer_get_shell (EMsgComposer *composer) { g_return_val_if_fail (E_IS_MSG_COMPOSER (composer), NULL); return E_SHELL (composer->priv->shell); } /** * e_msg_composer_get_web_view: * @composer: an #EMsgComposer * * Returns the #EWebView widget in @composer. * * Returns: the #EWebView **/ EWebViewGtkHTML * e_msg_composer_get_web_view (EMsgComposer *composer) { GtkHTML *html; GtkhtmlEditor *editor; g_return_val_if_fail (E_IS_MSG_COMPOSER (composer), NULL); /* This is a convenience function to avoid * repeating this awkwardness everywhere */ editor = GTKHTML_EDITOR (composer); html = gtkhtml_editor_get_html (editor); return E_WEB_VIEW_GTKHTML (html); } static void msg_composer_send_cb (EMsgComposer *composer, GAsyncResult *result, AsyncContext *context) { CamelMimeMessage *message; EAlertSink *alert_sink; GtkhtmlEditor *editor; GError *error = NULL; alert_sink = e_activity_get_alert_sink (context->activity); message = e_msg_composer_get_message_finish (composer, result, &error); if (e_activity_handle_cancellation (context->activity, error)) { g_warn_if_fail (message == NULL); async_context_free (context); g_error_free (error); gtk_window_present (GTK_WINDOW (composer)); return; } if (error != NULL) { g_warn_if_fail (message == NULL); e_alert_submit ( alert_sink, "mail-composer:no-build-message", error->message, NULL); async_context_free (context); g_error_free (error); gtk_window_present (GTK_WINDOW (composer)); return; } g_return_if_fail (CAMEL_IS_MIME_MESSAGE (message)); /* The callback can set editor 'changed' if anything failed. */ editor = GTKHTML_EDITOR (composer); gtkhtml_editor_set_changed (editor, FALSE); g_signal_emit ( composer, signals[SEND], 0, message, context->activity); g_object_unref (message); async_context_free (context); } /** * e_msg_composer_send: * @composer: an #EMsgComposer * * Send the message in @composer. **/ void e_msg_composer_send (EMsgComposer *composer) { AsyncContext *context; EAlertSink *alert_sink; EActivityBar *activity_bar; GCancellable *cancellable; gboolean proceed_with_send = TRUE; g_return_if_fail (E_IS_MSG_COMPOSER (composer)); /* This gives the user a chance to abort the send. */ g_signal_emit (composer, signals[PRESEND], 0, &proceed_with_send); if (!proceed_with_send) { gtk_window_present (GTK_WINDOW (composer)); return; } context = g_slice_new0 (AsyncContext); context->activity = e_composer_activity_new (composer); alert_sink = E_ALERT_SINK (composer); e_activity_set_alert_sink (context->activity, alert_sink); cancellable = camel_operation_new (); e_activity_set_cancellable (context->activity, cancellable); g_object_unref (cancellable); activity_bar = E_ACTIVITY_BAR (composer->priv->activity_bar); e_activity_bar_set_activity (activity_bar, context->activity); e_msg_composer_get_message ( composer, G_PRIORITY_DEFAULT, cancellable, (GAsyncReadyCallback) msg_composer_send_cb, context); } static void msg_composer_save_to_drafts_cb (EMsgComposer *composer, GAsyncResult *result, AsyncContext *context) { CamelMimeMessage *message; EAlertSink *alert_sink; GtkhtmlEditor *editor; GError *error = NULL; alert_sink = e_activity_get_alert_sink (context->activity); message = e_msg_composer_get_message_draft_finish ( composer, result, &error); if (e_activity_handle_cancellation (context->activity, error)) { g_warn_if_fail (message == NULL); async_context_free (context); g_error_free (error); return; } if (error != NULL) { g_warn_if_fail (message == NULL); e_alert_submit ( alert_sink, "mail-composer:no-build-message", error->message, NULL); async_context_free (context); g_error_free (error); return; } g_return_if_fail (CAMEL_IS_MIME_MESSAGE (message)); /* The callback can set editor 'changed' if anything failed. */ editor = GTKHTML_EDITOR (composer); gtkhtml_editor_set_changed (editor, FALSE); g_signal_emit ( composer, signals[SAVE_TO_DRAFTS], 0, message, context->activity); g_object_unref (message); async_context_free (context); } /** * e_msg_composer_save_to_drafts: * @composer: an #EMsgComposer * * Save the message in @composer to the selected account's Drafts folder. **/ void e_msg_composer_save_to_drafts (EMsgComposer *composer) { AsyncContext *context; EAlertSink *alert_sink; EActivityBar *activity_bar; GCancellable *cancellable; g_return_if_fail (E_IS_MSG_COMPOSER (composer)); context = g_slice_new0 (AsyncContext); context->activity = e_composer_activity_new (composer); alert_sink = E_ALERT_SINK (composer); e_activity_set_alert_sink (context->activity, alert_sink); cancellable = camel_operation_new (); e_activity_set_cancellable (context->activity, cancellable); g_object_unref (cancellable); activity_bar = E_ACTIVITY_BAR (composer->priv->activity_bar); e_activity_bar_set_activity (activity_bar, context->activity); e_msg_composer_get_message_draft ( composer, G_PRIORITY_DEFAULT, cancellable, (GAsyncReadyCallback) msg_composer_save_to_drafts_cb, context); } static void msg_composer_save_to_outbox_cb (EMsgComposer *composer, GAsyncResult *result, AsyncContext *context) { CamelMimeMessage *message; EAlertSink *alert_sink; GtkhtmlEditor *editor; GError *error = NULL; alert_sink = e_activity_get_alert_sink (context->activity); message = e_msg_composer_get_message_finish (composer, result, &error); if (e_activity_handle_cancellation (context->activity, error)) { g_warn_if_fail (message == NULL); async_context_free (context); g_error_free (error); return; } if (error != NULL) { g_warn_if_fail (message == NULL); e_alert_submit ( alert_sink, "mail-composer:no-build-message", error->message, NULL); async_context_free (context); g_error_free (error); return; } g_return_if_fail (CAMEL_IS_MIME_MESSAGE (message)); g_signal_emit ( composer, signals[SAVE_TO_OUTBOX], 0, message, context->activity); g_object_unref (message); async_context_free (context); /* XXX This should be elsewhere. */ editor = GTKHTML_EDITOR (composer); gtkhtml_editor_set_changed (editor, FALSE); } /** * e_msg_composer_save_to_outbox: * @composer: an #EMsgComposer * * Save the message in @composer to the local Outbox folder. **/ void e_msg_composer_save_to_outbox (EMsgComposer *composer) { AsyncContext *context; EAlertSink *alert_sink; EActivityBar *activity_bar; GCancellable *cancellable; gboolean proceed_with_save = TRUE; g_return_if_fail (E_IS_MSG_COMPOSER (composer)); /* This gives the user a chance to abort the save. */ g_signal_emit (composer, signals[PRESEND], 0, &proceed_with_save); if (!proceed_with_save) return; context = g_slice_new0 (AsyncContext); context->activity = e_composer_activity_new (composer); alert_sink = E_ALERT_SINK (composer); e_activity_set_alert_sink (context->activity, alert_sink); cancellable = camel_operation_new (); e_activity_set_cancellable (context->activity, cancellable); g_object_unref (cancellable); activity_bar = E_ACTIVITY_BAR (composer->priv->activity_bar); e_activity_bar_set_activity (activity_bar, context->activity); e_msg_composer_get_message ( composer, G_PRIORITY_DEFAULT, cancellable, (GAsyncReadyCallback) msg_composer_save_to_outbox_cb, context); } static void msg_composer_print_cb (EMsgComposer *composer, GAsyncResult *result, AsyncContext *context) { CamelMimeMessage *message; EAlertSink *alert_sink; GError *error = NULL; alert_sink = e_activity_get_alert_sink (context->activity); message = e_msg_composer_get_message_print_finish ( composer, result, &error); if (e_activity_handle_cancellation (context->activity, error)) { g_warn_if_fail (message == NULL); async_context_free (context); g_error_free (error); return; } if (error != NULL) { g_warn_if_fail (message == NULL); async_context_free (context); e_alert_submit ( alert_sink, "mail-composer:no-build-message", error->message, NULL); g_error_free (error); return; } g_return_if_fail (CAMEL_IS_MIME_MESSAGE (message)); g_signal_emit ( composer, signals[PRINT], 0, context->print_action, message, context->activity); g_object_unref (message); async_context_free (context); } /** * e_msg_composer_print: * @composer: an #EMsgComposer * @print_action: the print action to start * * Print the message in @composer. **/ void e_msg_composer_print (EMsgComposer *composer, GtkPrintOperationAction print_action) { AsyncContext *context; EAlertSink *alert_sink; EActivityBar *activity_bar; GCancellable *cancellable; g_return_if_fail (E_IS_MSG_COMPOSER (composer)); context = g_slice_new0 (AsyncContext); context->activity = e_composer_activity_new (composer); context->print_action = print_action; alert_sink = E_ALERT_SINK (composer); e_activity_set_alert_sink (context->activity, alert_sink); cancellable = camel_operation_new (); e_activity_set_cancellable (context->activity, cancellable); g_object_unref (cancellable); activity_bar = E_ACTIVITY_BAR (composer->priv->activity_bar); e_activity_bar_set_activity (activity_bar, context->activity); e_msg_composer_get_message_print ( composer, G_PRIORITY_DEFAULT, cancellable, (GAsyncReadyCallback) msg_composer_print_cb, context); } static GList * add_recipients (GList *list, const gchar *recips) { CamelInternetAddress *cia; const gchar *name, *addr; gint num, i; cia = camel_internet_address_new (); num = camel_address_decode (CAMEL_ADDRESS (cia), recips); for (i = 0; i < num; i++) { if (camel_internet_address_get (cia, i, &name, &addr)) { EDestination *dest = e_destination_new (); e_destination_set_name (dest, name); e_destination_set_email (dest, addr); list = g_list_append (list, dest); } } return list; } static gboolean list_contains_addr (const GList *list, EDestination *dest) { g_return_val_if_fail (dest != NULL, FALSE); while (list != NULL) { if (e_destination_equal (dest, list->data)) return TRUE; list = list->next; } return FALSE; } static void merge_cc_bcc (EDestination **addrv, GList **merge_into, const GList *to, const GList *cc, const GList *bcc) { gint ii; for (ii = 0; addrv && addrv[ii]; ii++) { if (!list_contains_addr (to, addrv[ii]) && !list_contains_addr (cc, addrv[ii]) && !list_contains_addr (bcc, addrv[ii])) *merge_into = g_list_append ( *merge_into, g_object_ref (addrv[ii])); } } static void merge_always_cc_and_bcc (EComposerHeaderTable *table, const GList *to, GList **cc, GList **bcc) { EDestination **addrv; g_return_if_fail (table != NULL); g_return_if_fail (cc != NULL); g_return_if_fail (bcc != NULL); addrv = e_composer_header_table_get_destinations_cc (table); merge_cc_bcc (addrv, cc, to, *cc, *bcc); e_destination_freev (addrv); addrv = e_composer_header_table_get_destinations_bcc (table); merge_cc_bcc (addrv, bcc, to, *cc, *bcc); e_destination_freev (addrv); } static const gchar *blacklist[] = { ".", "etc", ".." }; static gboolean file_is_blacklisted (gchar *filename) { gboolean blacklisted = FALSE; guint ii, jj, n_parts; gchar **parts; parts = g_strsplit (filename, G_DIR_SEPARATOR_S, -1); n_parts = g_strv_length (parts); for (ii = 0; ii < G_N_ELEMENTS (blacklist); ii++) { for (jj = 0; jj < n_parts; jj++) { if (g_str_has_prefix (parts[jj], blacklist[ii])) { blacklisted = TRUE; break; } } } g_strfreev (parts); return blacklisted; } static void handle_mailto (EMsgComposer *composer, const gchar *mailto) { EAttachmentView *view; EAttachmentStore *store; EComposerHeaderTable *table; GList *to = NULL, *cc = NULL, *bcc = NULL; EDestination **tov, **ccv, **bccv; gchar *subject = NULL, *body = NULL; gchar *header, *content, *buf; gsize nread, nwritten; const gchar *p; gint len, clen; table = e_msg_composer_get_header_table (composer); view = e_msg_composer_get_attachment_view (composer); store = e_attachment_view_get_store (view); buf = g_strdup (mailto); /* Parse recipients (everything after ':' until '?' or eos). */ p = buf + 7; len = strcspn (p, "?"); if (len) { content = g_strndup (p, len); camel_url_decode (content); to = add_recipients (to, content); g_free (content); } p += len; if (*p == '?') { p++; while (*p) { len = strcspn (p, "=&"); /* If it's malformed, give up. */ if (p[len] != '=') break; header = (gchar *) p; header[len] = '\0'; p += len + 1; clen = strcspn (p, "&"); content = g_strndup (p, clen); if (!g_ascii_strcasecmp (header, "to")) { camel_url_decode (content); to = add_recipients (to, content); } else if (!g_ascii_strcasecmp (header, "cc")) { camel_url_decode (content); cc = add_recipients (cc, content); } else if (!g_ascii_strcasecmp (header, "bcc")) { camel_url_decode (content); bcc = add_recipients (bcc, content); } else if (!g_ascii_strcasecmp (header, "subject")) { g_free (subject); camel_url_decode (content); if (g_utf8_validate (content, -1, NULL)) { subject = content; content = NULL; } else { subject = g_locale_to_utf8 (content, clen, &nread, &nwritten, NULL); if (subject) { subject = g_realloc (subject, nwritten + 1); subject[nwritten] = '\0'; } } } else if (!g_ascii_strcasecmp (header, "body")) { g_free (body); camel_url_decode (content); if (g_utf8_validate (content, -1, NULL)) { body = content; content = NULL; } else { body = g_locale_to_utf8 ( content, clen, &nread, &nwritten, NULL); if (body) { body = g_realloc (body, nwritten + 1); body[nwritten] = '\0'; } } } else if (!g_ascii_strcasecmp (header, "attach") || !g_ascii_strcasecmp (header, "attachment")) { EAttachment *attachment; camel_url_decode (content); if (file_is_blacklisted (content)) e_alert_submit ( E_ALERT_SINK (composer), "mail:blacklisted-file", content, NULL); if (g_ascii_strncasecmp (content, "file:", 5) == 0) attachment = e_attachment_new_for_uri (content); else attachment = e_attachment_new_for_path (content); e_attachment_store_add_attachment (store, attachment); e_attachment_load_async ( attachment, (GAsyncReadyCallback) e_attachment_load_handle_error, composer); g_object_unref (attachment); } else if (!g_ascii_strcasecmp (header, "from")) { /* Ignore */ } else if (!g_ascii_strcasecmp (header, "reply-to")) { /* ignore */ } else { /* add an arbitrary header? */ camel_url_decode (content); e_msg_composer_add_header (composer, header, content); } g_free (content); p += clen; if (*p == '&') { p++; if (!g_ascii_strncasecmp (p, "amp;", 4)) p += 4; } } } g_free (buf); merge_always_cc_and_bcc (table, to, &cc, &bcc); tov = destination_list_to_vector (to); ccv = destination_list_to_vector (cc); bccv = destination_list_to_vector (bcc); g_list_free (to); g_list_free (cc); g_list_free (bcc); e_composer_header_table_set_destinations_to (table, tov); e_composer_header_table_set_destinations_cc (table, ccv); e_composer_header_table_set_destinations_bcc (table, bccv); e_destination_freev (tov); e_destination_freev (ccv); e_destination_freev (bccv); e_composer_header_table_set_subject (table, subject); g_free (subject); if (body) { gchar *htmlbody; htmlbody = camel_text_to_html (body, CAMEL_MIME_FILTER_TOHTML_PRE, 0); set_editor_text (composer, htmlbody, TRUE); g_free (htmlbody); } } /** * e_msg_composer_new_from_url: * @shell: an #EShell * @url: a mailto URL * * Create a new message composer widget, and fill in fields as * defined by the provided URL. **/ EMsgComposer * e_msg_composer_new_from_url (EShell *shell, const gchar *url) { EMsgComposer *composer; g_return_val_if_fail (E_IS_SHELL (shell), NULL); g_return_val_if_fail (g_ascii_strncasecmp (url, "mailto:", 7) == 0, NULL); composer = e_msg_composer_new (shell); handle_mailto (composer, url); return composer; } /** * e_msg_composer_set_body_text: * @composer: a composer object * @text: the HTML text to initialize the editor with * @update_signature: whether update signature in the text after setting it; * Might be usually called with TRUE. * * Loads the given HTML text into the editor. **/ void e_msg_composer_set_body_text (EMsgComposer *composer, const gchar *text, gboolean update_signature) { g_return_if_fail (E_IS_MSG_COMPOSER (composer)); g_return_if_fail (text != NULL); set_editor_text (composer, text, update_signature); } /** * e_msg_composer_set_body: * @composer: a composer object * @body: the data to initialize the composer with * @mime_type: the MIME type of data * * Loads the given data into the composer as the message body. **/ void e_msg_composer_set_body (EMsgComposer *composer, const gchar *body, const gchar *mime_type) { EMsgComposerPrivate *p = composer->priv; EComposerHeaderTable *table; EWebViewGtkHTML *web_view; gchar *buff; g_return_if_fail (E_IS_MSG_COMPOSER (composer)); table = e_msg_composer_get_header_table (composer); buff = g_markup_printf_escaped ("%s", _("The composer contains a non-text " "message body, which cannot be edited.")); set_editor_text (composer, buff, FALSE); g_free (buff); gtkhtml_editor_set_html_mode (GTKHTML_EDITOR (composer), FALSE); web_view = e_msg_composer_get_web_view (composer); e_web_view_gtkhtml_set_editable (web_view, FALSE); g_free (p->mime_body); p->mime_body = g_strdup (body); g_free (p->mime_type); p->mime_type = g_strdup (mime_type); if (g_ascii_strncasecmp (p->mime_type, "text/calendar", 13) == 0) { EAccount *account; account = e_composer_header_table_get_account (table); if (account && account->pgp_no_imip_sign) { GtkToggleAction *action; action = GTK_TOGGLE_ACTION (ACTION (PGP_SIGN)); gtk_toggle_action_set_active (action, FALSE); action = GTK_TOGGLE_ACTION (ACTION (SMIME_SIGN)); gtk_toggle_action_set_active (action, FALSE); } } } /** * e_msg_composer_add_header: * @composer: an #EMsgComposer * @name: the header's name * @value: the header's value * * Adds a new custom header created from @name and @value. The header * is not shown in the user interface but will be added to the resulting * MIME message when sending or saving. **/ void e_msg_composer_add_header (EMsgComposer *composer, const gchar *name, const gchar *value) { EMsgComposerPrivate *priv; g_return_if_fail (E_IS_MSG_COMPOSER (composer)); g_return_if_fail (name != NULL); g_return_if_fail (value != NULL); priv = composer->priv; g_ptr_array_add (priv->extra_hdr_names, g_strdup (name)); g_ptr_array_add (priv->extra_hdr_values, g_strdup (value)); } /** * e_msg_composer_set_header: * @composer: an #EMsgComposer * @name: the header's name * @value: the header's value * * Replaces all custom headers matching @name that were added with * e_msg_composer_add_header() or e_msg_composer_set_header(), with * a new custom header created from @name and @value. The header is * not shown in the user interface but will be added to the resulting * MIME message when sending or saving. **/ void e_msg_composer_set_header (EMsgComposer *composer, const gchar *name, const gchar *value) { g_return_if_fail (E_IS_MSG_COMPOSER (composer)); g_return_if_fail (name != NULL); g_return_if_fail (value != NULL); e_msg_composer_remove_header (composer, name); e_msg_composer_add_header (composer, name, value); } /** * e_msg_composer_remove_header: * @composer: an #EMsgComposer * @name: the header's name * * Removes all custom headers matching @name that were added with * e_msg_composer_add_header() or e_msg_composer_set_header(). **/ void e_msg_composer_remove_header (EMsgComposer *composer, const gchar *name) { EMsgComposerPrivate *priv; guint ii; g_return_if_fail (E_IS_MSG_COMPOSER (composer)); g_return_if_fail (name != NULL); priv = composer->priv; for (ii = 0; ii < priv->extra_hdr_names->len; ii++) { if (g_strcmp0 (priv->extra_hdr_names->pdata[ii], name) == 0) { g_free (priv->extra_hdr_names->pdata[ii]); g_free (priv->extra_hdr_values->pdata[ii]); g_ptr_array_remove_index (priv->extra_hdr_names, ii); g_ptr_array_remove_index (priv->extra_hdr_values, ii); } } } /** * e_msg_composer_set_draft_headers: * @composer: an #EMsgComposer * @folder_uri: folder URI of the last saved draft * @message_uid: message UID of the last saved draft * * Add special X-Evolution-Draft headers to remember the most recently * saved draft message, even across Evolution sessions. These headers * can be used to mark the draft message for deletion after saving a * newer draft or sending the composed message. **/ void e_msg_composer_set_draft_headers (EMsgComposer *composer, const gchar *folder_uri, const gchar *message_uid) { const gchar *header_name; g_return_if_fail (E_IS_MSG_COMPOSER (composer)); g_return_if_fail (folder_uri != NULL); g_return_if_fail (message_uid != NULL); header_name = "X-Evolution-Draft-Folder"; e_msg_composer_set_header (composer, header_name, folder_uri); header_name = "X-Evolution-Draft-Message"; e_msg_composer_set_header (composer, header_name, message_uid); } /** * e_msg_composer_set_source_headers: * @composer: an #EMsgComposer * @folder_uri: folder URI of the source message * @message_uid: message UID of the source message * @flags: flags to set on the source message after sending * * Add special X-Evolution-Source headers to remember the message being * forwarded or replied to, even across Evolution sessions. These headers * can be used to set appropriate flags on the source message after sending * the composed message. **/ void e_msg_composer_set_source_headers (EMsgComposer *composer, const gchar *folder_uri, const gchar *message_uid, CamelMessageFlags flags) { GString *buffer; const gchar *header_name; g_return_if_fail (E_IS_MSG_COMPOSER (composer)); g_return_if_fail (folder_uri != NULL); g_return_if_fail (message_uid != NULL); buffer = g_string_sized_new (32); if (flags & CAMEL_MESSAGE_ANSWERED) g_string_append (buffer, "ANSWERED "); if (flags & CAMEL_MESSAGE_ANSWERED_ALL) g_string_append (buffer, "ANSWERED_ALL "); if (flags & CAMEL_MESSAGE_FORWARDED) g_string_append (buffer, "FORWARDED "); if (flags & CAMEL_MESSAGE_SEEN) g_string_append (buffer, "SEEN "); header_name = "X-Evolution-Source-Folder"; e_msg_composer_set_header (composer, header_name, folder_uri); header_name = "X-Evolution-Source-Message"; e_msg_composer_set_header (composer, header_name, message_uid); header_name = "X-Evolution-Source-Flags"; e_msg_composer_set_header (composer, header_name, buffer->str); g_string_free (buffer, TRUE); } /** * e_msg_composer_attach: * @composer: a composer object * @mime_part: the #CamelMimePart to attach * * Attaches @attachment to the message being composed in the composer. **/ void e_msg_composer_attach (EMsgComposer *composer, CamelMimePart *mime_part) { EAttachmentView *view; EAttachmentStore *store; EAttachment *attachment; g_return_if_fail (E_IS_MSG_COMPOSER (composer)); g_return_if_fail (CAMEL_IS_MIME_PART (mime_part)); view = e_msg_composer_get_attachment_view (composer); store = e_attachment_view_get_store (view); attachment = e_attachment_new (); e_attachment_set_mime_part (attachment, mime_part); e_attachment_store_add_attachment (store, attachment); e_attachment_load_async ( attachment, (GAsyncReadyCallback) e_attachment_load_handle_error, composer); g_object_unref (attachment); } /** * e_msg_composer_add_inline_image_from_file: * @composer: a composer object * @filename: the name of the file containing the image * * This reads in the image in @filename and adds it to @composer * as an inline image, to be wrapped in a multipart/related. * * Returns: the newly-created CamelMimePart (which must be reffed * if the caller wants to keep its own reference), or %NULL on error. **/ CamelMimePart * e_msg_composer_add_inline_image_from_file (EMsgComposer *composer, const gchar *filename) { gchar *mime_type, *cid, *url, *name, *dec_file_name; CamelStream *stream; CamelDataWrapper *wrapper; CamelMimePart *part; EMsgComposerPrivate *p = composer->priv; dec_file_name = g_strdup (filename); camel_url_decode (dec_file_name); if (!g_file_test (dec_file_name, G_FILE_TEST_IS_REGULAR)) return NULL; stream = camel_stream_fs_new_with_name ( dec_file_name, O_RDONLY, 0, NULL); if (!stream) return NULL; wrapper = camel_data_wrapper_new (); camel_data_wrapper_construct_from_stream_sync ( wrapper, stream, NULL, NULL); g_object_unref (CAMEL_OBJECT (stream)); mime_type = e_util_guess_mime_type (dec_file_name, TRUE); if (mime_type == NULL) mime_type = g_strdup ("application/octet-stream"); camel_data_wrapper_set_mime_type (wrapper, mime_type); g_free (mime_type); part = camel_mime_part_new (); camel_medium_set_content (CAMEL_MEDIUM (part), wrapper); g_object_unref (wrapper); cid = camel_header_msgid_generate (); camel_mime_part_set_content_id (part, cid); name = g_path_get_basename (dec_file_name); camel_mime_part_set_filename (part, name); g_free (name); camel_mime_part_set_encoding (part, CAMEL_TRANSFER_ENCODING_BASE64); url = g_strdup_printf ("file:%s", dec_file_name); g_hash_table_insert (p->inline_images_by_url, url, part); url = g_strdup_printf ("cid:%s", cid); g_hash_table_insert (p->inline_images, url, part); g_free (cid); g_free (dec_file_name); return part; } /** * e_msg_composer_add_inline_image_from_mime_part: * @composer: a composer object * @part: a CamelMimePart containing image data * * This adds the mime part @part to @composer as an inline image, to * be wrapped in a multipart/related. **/ void e_msg_composer_add_inline_image_from_mime_part (EMsgComposer *composer, CamelMimePart *part) { gchar *url; const gchar *location, *cid; EMsgComposerPrivate *p = composer->priv; cid = camel_mime_part_get_content_id (part); if (!cid) { camel_mime_part_set_content_id (part, NULL); cid = camel_mime_part_get_content_id (part); } url = g_strdup_printf ("cid:%s", cid); g_hash_table_insert (p->inline_images, url, part); g_object_ref (part); location = camel_mime_part_get_content_location (part); if (location != NULL) g_hash_table_insert ( p->inline_images_by_url, g_strdup (location), part); } static void composer_get_message_ready (EMsgComposer *composer, GAsyncResult *result, GSimpleAsyncResult *simple) { CamelMimeMessage *message; GError *error = NULL; message = composer_build_message_finish (composer, result, &error); if (message != NULL) g_simple_async_result_set_op_res_gpointer ( simple, message, (GDestroyNotify) g_object_unref); if (error != NULL) { g_warn_if_fail (message == NULL); g_simple_async_result_take_error (simple, error); } g_simple_async_result_complete (simple); g_object_unref (simple); } /** * e_msg_composer_get_message: * @composer: an #EMsgComposer * * Retrieve the message edited by the user as a #CamelMimeMessage. The * #CamelMimeMessage object is created on the fly; subsequent calls to this * function will always create new objects from scratch. **/ void e_msg_composer_get_message (EMsgComposer *composer, gint io_priority, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { GSimpleAsyncResult *simple; GtkAction *action; ComposerFlags flags = 0; g_return_if_fail (E_IS_MSG_COMPOSER (composer)); simple = g_simple_async_result_new ( G_OBJECT (composer), callback, user_data, e_msg_composer_get_message); if (gtkhtml_editor_get_html_mode (GTKHTML_EDITOR (composer))) flags |= COMPOSER_FLAG_HTML_CONTENT; action = ACTION (PRIORITIZE_MESSAGE); if (gtk_toggle_action_get_active (GTK_TOGGLE_ACTION (action))) flags |= COMPOSER_FLAG_PRIORITIZE_MESSAGE; action = ACTION (REQUEST_READ_RECEIPT); if (gtk_toggle_action_get_active (GTK_TOGGLE_ACTION (action))) flags |= COMPOSER_FLAG_REQUEST_READ_RECEIPT; action = ACTION (PGP_SIGN); if (gtk_toggle_action_get_active (GTK_TOGGLE_ACTION (action))) flags |= COMPOSER_FLAG_PGP_SIGN; action = ACTION (PGP_ENCRYPT); if (gtk_toggle_action_get_active (GTK_TOGGLE_ACTION (action))) flags |= COMPOSER_FLAG_PGP_ENCRYPT; #ifdef HAVE_NSS action = ACTION (SMIME_SIGN); if (gtk_toggle_action_get_active (GTK_TOGGLE_ACTION (action))) flags |= COMPOSER_FLAG_SMIME_SIGN; action = ACTION (SMIME_ENCRYPT); if (gtk_toggle_action_get_active (GTK_TOGGLE_ACTION (action))) flags |= COMPOSER_FLAG_SMIME_ENCRYPT; #endif composer_build_message ( composer, flags, io_priority, cancellable, (GAsyncReadyCallback) composer_get_message_ready, simple); } CamelMimeMessage * e_msg_composer_get_message_finish (EMsgComposer *composer, GAsyncResult *result, GError **error) { GSimpleAsyncResult *simple; CamelMimeMessage *message; g_return_val_if_fail ( g_simple_async_result_is_valid ( result, G_OBJECT (composer), e_msg_composer_get_message), NULL); simple = G_SIMPLE_ASYNC_RESULT (result); message = g_simple_async_result_get_op_res_gpointer (simple); if (g_simple_async_result_propagate_error (simple, error)) return NULL; g_return_val_if_fail (CAMEL_IS_MIME_MESSAGE (message), NULL); return g_object_ref (message); } void e_msg_composer_get_message_print (EMsgComposer *composer, gint io_priority, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { GSimpleAsyncResult *simple; ComposerFlags flags = 0; g_return_if_fail (E_IS_MSG_COMPOSER (composer)); simple = g_simple_async_result_new ( G_OBJECT (composer), callback, user_data, e_msg_composer_get_message_print); flags |= COMPOSER_FLAG_HTML_CONTENT; flags |= COMPOSER_FLAG_SAVE_OBJECT_DATA; composer_build_message ( composer, flags, io_priority, cancellable, (GAsyncReadyCallback) composer_get_message_ready, simple); } CamelMimeMessage * e_msg_composer_get_message_print_finish (EMsgComposer *composer, GAsyncResult *result, GError **error) { GSimpleAsyncResult *simple; CamelMimeMessage *message; g_return_val_if_fail ( g_simple_async_result_is_valid ( result, G_OBJECT (composer), e_msg_composer_get_message_print), NULL); simple = G_SIMPLE_ASYNC_RESULT (result); message = g_simple_async_result_get_op_res_gpointer (simple); if (g_simple_async_result_propagate_error (simple, error)) return NULL; g_return_val_if_fail (CAMEL_IS_MIME_MESSAGE (message), NULL); return g_object_ref (message); } void e_msg_composer_get_message_draft (EMsgComposer *composer, gint io_priority, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { GSimpleAsyncResult *simple; ComposerFlags flags = 0; g_return_if_fail (E_IS_MSG_COMPOSER (composer)); simple = g_simple_async_result_new ( G_OBJECT (composer), callback, user_data, e_msg_composer_get_message_draft); if (gtkhtml_editor_get_html_mode (GTKHTML_EDITOR (composer))) flags |= COMPOSER_FLAG_HTML_CONTENT; composer_build_message ( composer, flags, io_priority, cancellable, (GAsyncReadyCallback) composer_get_message_ready, simple); } CamelMimeMessage * e_msg_composer_get_message_draft_finish (EMsgComposer *composer, GAsyncResult *result, GError **error) { GSimpleAsyncResult *simple; CamelMimeMessage *message; g_return_val_if_fail ( g_simple_async_result_is_valid ( result, G_OBJECT (composer), e_msg_composer_get_message_draft), NULL); simple = G_SIMPLE_ASYNC_RESULT (result); message = g_simple_async_result_get_op_res_gpointer (simple); if (g_simple_async_result_propagate_error (simple, error)) return NULL; g_return_val_if_fail (CAMEL_IS_MIME_MESSAGE (message), NULL); return g_object_ref (message); } /** * e_msg_composer_show_sig: * @composer: A message composer widget * * Set a signature **/ void e_msg_composer_show_sig_file (EMsgComposer *composer) { GtkhtmlEditor *editor; gchar *html_text; g_return_if_fail (E_IS_MSG_COMPOSER (composer)); editor = GTKHTML_EDITOR (composer); if (composer->priv->redirect) return; composer->priv->in_signature_insert = TRUE; gtkhtml_editor_freeze (editor); gtkhtml_editor_run_command (editor, "cursor-position-save"); gtkhtml_editor_undo_begin (editor, "Set signature", "Reset signature"); /* Delete the old signature. */ gtkhtml_editor_run_command (editor, "block-selection"); gtkhtml_editor_run_command (editor, "cursor-bod"); if (gtkhtml_editor_search_by_data (editor, 1, "ClueFlow", "signature", "1")) { gtkhtml_editor_run_command (editor, "select-paragraph"); gtkhtml_editor_run_command (editor, "delete"); gtkhtml_editor_set_paragraph_data (editor, "signature", "0"); gtkhtml_editor_run_command (editor, "delete-back"); } gtkhtml_editor_run_command (editor, "unblock-selection"); html_text = get_signature_html (composer); if (html_text) { gtkhtml_editor_run_command (editor, "insert-paragraph"); if (!gtkhtml_editor_run_command (editor, "cursor-backward")) gtkhtml_editor_run_command (editor, "insert-paragraph"); else gtkhtml_editor_run_command (editor, "cursor-forward"); gtkhtml_editor_set_paragraph_data (editor, "orig", "0"); gtkhtml_editor_run_command (editor, "indent-zero"); gtkhtml_editor_run_command (editor, "style-normal"); gtkhtml_editor_insert_html (editor, html_text); g_free (html_text); } else if (is_top_signature (composer)) { /* insert paragraph after the signature ClueFlow things */ if (gtkhtml_editor_run_command (editor, "cursor-forward")) gtkhtml_editor_run_command (editor, "insert-paragraph"); } gtkhtml_editor_undo_end (editor); gtkhtml_editor_run_command (editor, "cursor-position-restore"); gtkhtml_editor_thaw (editor); composer->priv->in_signature_insert = FALSE; } CamelInternetAddress * e_msg_composer_get_from (EMsgComposer *composer) { CamelInternetAddress *address; EComposerHeaderTable *table; EAccount *account; g_return_val_if_fail (E_IS_MSG_COMPOSER (composer), NULL); table = e_msg_composer_get_header_table (composer); account = e_composer_header_table_get_account (table); if (account == NULL) return NULL; address = camel_internet_address_new (); camel_internet_address_add ( address, account->id->name, account->id->address); return address; } CamelInternetAddress * e_msg_composer_get_reply_to (EMsgComposer *composer) { CamelInternetAddress *address; EComposerHeaderTable *table; const gchar *reply_to; g_return_val_if_fail (E_IS_MSG_COMPOSER (composer), NULL); table = e_msg_composer_get_header_table (composer); reply_to = e_composer_header_table_get_reply_to (table); if (reply_to == NULL || *reply_to == '\0') return NULL; address = camel_internet_address_new (); if (camel_address_unformat (CAMEL_ADDRESS (address), reply_to) == -1) { g_object_unref (CAMEL_OBJECT (address)); return NULL; } return address; } /** * e_msg_composer_get_raw_message_text: * * Returns the text/plain of the message from composer **/ GByteArray * e_msg_composer_get_raw_message_text (EMsgComposer *composer) { GtkhtmlEditor *editor; GByteArray *array; gchar *text; gsize length; g_return_val_if_fail (E_IS_MSG_COMPOSER (composer), NULL); array = g_byte_array_new (); editor = GTKHTML_EDITOR (composer); text = gtkhtml_editor_get_text_plain (editor, &length); g_byte_array_append (array, (guint8 *) text, (guint) length); g_free (text); return array; } gboolean e_msg_composer_is_exiting (EMsgComposer *composer) { g_return_val_if_fail (composer != NULL, FALSE); return composer->priv->application_exiting; } void e_msg_composer_request_close (EMsgComposer *composer) { g_return_if_fail (composer != NULL); composer->priv->application_exiting = TRUE; } /* Returns whether can close the composer immediately. It will return FALSE * also when saving to drafts, but the e_msg_composer_is_exiting will return * TRUE for this case. can_save_draft means whether can save draft * immediately, or rather keep it on the caller (when FALSE). If kept on the * folder, then returns FALSE and sets interval variable to return TRUE in * e_msg_composer_is_exiting. */ gboolean e_msg_composer_can_close (EMsgComposer *composer, gboolean can_save_draft) { gboolean res = FALSE; GtkhtmlEditor *editor; EComposerHeaderTable *table; GdkWindow *window; GtkWidget *widget; const gchar *subject; gint response; editor = GTKHTML_EDITOR (composer); widget = GTK_WIDGET (composer); if (!gtkhtml_editor_get_changed (editor)) return TRUE; window = gtk_widget_get_window (widget); gdk_window_raise (window); table = e_msg_composer_get_header_table (composer); subject = e_composer_header_table_get_subject (table); if (subject == NULL || *subject == '\0') subject = _("Untitled Message"); response = e_alert_run_dialog_for_args ( GTK_WINDOW (composer), "mail-composer:exit-unsaved", subject, NULL); switch (response) { case GTK_RESPONSE_YES: gtk_widget_hide (widget); e_msg_composer_request_close (composer); if (can_save_draft) gtk_action_activate (ACTION (SAVE_DRAFT)); break; case GTK_RESPONSE_NO: res = TRUE; break; case GTK_RESPONSE_CANCEL: break; } return res; } void e_msg_composer_reply_indent (EMsgComposer *composer) { GtkhtmlEditor *editor; g_return_if_fail (E_IS_MSG_COMPOSER (composer)); editor = GTKHTML_EDITOR (composer); if (!gtkhtml_editor_is_paragraph_empty (editor)) { if (gtkhtml_editor_is_previous_paragraph_empty (editor)) gtkhtml_editor_run_command (editor, "cursor-backward"); else { gtkhtml_editor_run_command (editor, "text-default-color"); gtkhtml_editor_run_command (editor, "italic-off"); gtkhtml_editor_run_command (editor, "insert-paragraph"); return; } } gtkhtml_editor_run_command (editor, "style-normal"); gtkhtml_editor_run_command (editor, "indent-zero"); gtkhtml_editor_run_command (editor, "text-default-color"); gtkhtml_editor_run_command (editor, "italic-off"); } EComposerHeaderTable * e_msg_composer_get_header_table (EMsgComposer *composer) { g_return_val_if_fail (E_IS_MSG_COMPOSER (composer), NULL); return E_COMPOSER_HEADER_TABLE (composer->priv->header_table); } EAttachmentView * e_msg_composer_get_attachment_view (EMsgComposer *composer) { g_return_val_if_fail (E_IS_MSG_COMPOSER (composer), NULL); return E_ATTACHMENT_VIEW (composer->priv->attachment_paned); } GList * e_load_spell_languages (void) { GSettings *settings; GList *spell_languages = NULL; gchar **strv; gint ii; /* Ask GSettings for a list of spell check language codes. */ settings = g_settings_new ("org.gnome.evolution.mail"); strv = g_settings_get_strv (settings, "composer-spell-languages"); g_object_unref (settings); /* Convert the codes to spell language structs. */ for (ii = 0; strv[ii] != NULL; ii++) { gchar *language_code = strv[ii]; const GtkhtmlSpellLanguage *language; language = gtkhtml_spell_language_lookup (language_code); if (language != NULL) spell_languages = g_list_prepend ( spell_languages, (gpointer) language); } g_strfreev (strv); spell_languages = g_list_reverse (spell_languages); /* Pick a default spell language if it came back empty. */ if (spell_languages == NULL) { const GtkhtmlSpellLanguage *language; language = gtkhtml_spell_language_lookup (NULL); if (language) { spell_languages = g_list_prepend ( spell_languages, (gpointer) language); } } return spell_languages; } void e_save_spell_languages (GList *spell_languages) { GSettings *settings; GPtrArray *lang_array; /* Build a list of spell check language codes. */ lang_array = g_ptr_array_new (); while (spell_languages != NULL) { const GtkhtmlSpellLanguage *language; const gchar *language_code; language = spell_languages->data; language_code = gtkhtml_spell_language_get_code (language); g_ptr_array_add (lang_array, (gpointer) language_code); spell_languages = g_list_next (spell_languages); } g_ptr_array_add (lang_array, NULL); /* Save the language codes to GSettings. */ settings = g_settings_new ("org.gnome.evolution.mail"); g_settings_set_strv (settings, "composer-spell-languages", (const gchar * const *) lang_array->pdata); g_object_unref (settings); g_ptr_array_free (lang_array, TRUE); }