diff --git a/src/mail.cpp b/src/mail.cpp index d5b6610..00b6a3d 100644 --- a/src/mail.cpp +++ b/src/mail.cpp @@ -1,2222 +1,2235 @@ /* @file mail.h * @brief High level class to work with Outlook Mailitems. * * Copyright (C) 2015, 2016 by Bundesamt für Sicherheit in der Informationstechnik * Software engineering by Intevation GmbH * * This file is part of GpgOL. * * GpgOL 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.1 of the License, or (at your option) any later version. * * GpgOL 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 this program; if not, see . */ #include "config.h" #include "dialogs.h" #include "common.h" #include "mail.h" #include "eventsinks.h" #include "attachment.h" #include "mapihelp.h" #include "message.h" #include "revert.h" #include "gpgoladdin.h" #include "mymapitags.h" #include "parsecontroller.h" #include "gpgolstr.h" #include "windowmessages.h" #include "mlang-charset.h" #include #include #include #include #include #include #include #include #include #include #include #include #undef _ #define _(a) utf8_gettext (a) using namespace GpgME; static std::map g_mail_map; static std::map g_uid_map; static std::set uids_searched; static bool in_de_vs_mode() { /* We cache the values only once. A change requires restart. This is because checking this is very expensive as gpgconf spawns each process to query the settings. */ static bool checked; static bool vs_mode; if (checked) { return vs_mode; } Error err; const auto components = Configuration::Component::load (err); log_debug ("%s:%s: Checking for de-vs mode.", SRCNAME, __func__); if (err) { log_error ("%s:%s: Failed to get gpgconf components: %s", SRCNAME, __func__, err.asString ()); checked = true; vs_mode = false; return vs_mode; } for (const auto &component: components) { if (component.name () && !strcmp (component.name (), "gpg")) { for (const auto &option: component.options ()) { if (option.name () && !strcmp (option.name (), "compliance") && option.currentValue ().stringValue () && !stricmp (option.currentValue ().stringValue (), "de-vs")) { log_debug ("%s:%s: Detected de-vs mode", SRCNAME, __func__); checked = true; vs_mode = true; return vs_mode; } } checked = true; vs_mode = false; return vs_mode; } } checked = true; vs_mode = false; return false; } #define COPYBUFSIZE (8 * 1024) Mail::Mail (LPDISPATCH mailitem) : m_mailitem(mailitem), m_processed(false), m_needs_wipe(false), m_needs_save(false), m_crypt_successful(false), m_is_smime(false), m_is_smime_checked(false), m_is_signed(false), m_is_valid(false), m_close_triggered(false), m_is_html_alternative(false), m_needs_encrypt(false), m_moss_position(0), m_crypto_flags(0), m_type(MSGTYPE_UNKNOWN) { if (get_mail_for_item (mailitem)) { log_error ("Mail object for item: %p already exists. Bug.", mailitem); return; } m_event_sink = install_MailItemEvents_sink (mailitem); if (!m_event_sink) { /* Should not happen but in that case we don't add us to the list and just release the Mail item. */ log_error ("%s:%s: Failed to install MailItemEvents sink.", SRCNAME, __func__); gpgol_release(mailitem); return; } g_mail_map.insert (std::pair (mailitem, this)); } GPGRT_LOCK_DEFINE(dtor_lock); Mail::~Mail() { /* This should fix a race condition where the mail is deleted before the parser is accessed in the decrypt thread. The shared_ptr of the parser then ensures that the parser is alive even if the mail is deleted while parsing. */ gpgrt_lock_lock (&dtor_lock); std::map::iterator it; detach_MailItemEvents_sink (m_event_sink); gpgol_release(m_event_sink); it = g_mail_map.find(m_mailitem); if (it != g_mail_map.end()) { g_mail_map.erase (it); } if (!m_uuid.empty()) { auto it2 = g_uid_map.find(m_uuid); if (it2 != g_uid_map.end()) { g_uid_map.erase (it2); } } gpgol_release(m_mailitem); if (!m_uuid.empty()) { log_oom_extra ("%s:%s: destroyed: %p uuid: %s", SRCNAME, __func__, this, m_uuid.c_str()); } else { log_oom_extra ("%s:%s: non crypto mail: %p destroyed", SRCNAME, __func__, this); } gpgrt_lock_unlock (&dtor_lock); } Mail * Mail::get_mail_for_item (LPDISPATCH mailitem) { if (!mailitem) { return NULL; } std::map::iterator it; it = g_mail_map.find(mailitem); if (it == g_mail_map.end()) { return NULL; } return it->second; } Mail * Mail::get_mail_for_uuid (const char *uuid) { if (!uuid) { return NULL; } auto it = g_uid_map.find(std::string(uuid)); if (it == g_uid_map.end()) { return NULL; } return it->second; } bool Mail::is_valid_ptr (const Mail *mail) { auto it = g_mail_map.begin(); while (it != g_mail_map.end()) { if (it->second == mail) return true; ++it; } return false; } int Mail::pre_process_message () { LPMESSAGE message = get_oom_base_message (m_mailitem); if (!message) { log_error ("%s:%s: Failed to get base message.", SRCNAME, __func__); return 0; } log_oom_extra ("%s:%s: GetBaseMessage OK.", SRCNAME, __func__); /* Change the message class here. It is important that we change the message class in the before read event regardless if it is already set to one of GpgOL's message classes. Changing the message class (even if we set it to the same value again that it already has) causes Outlook to reconsider what it "knows" about a message and reread data from the underlying base message. */ mapi_change_message_class (message, 1); /* TODO: Unify this so mapi_change_message_class returns a useful value already. */ m_type = mapi_get_message_type (message); /* Create moss attachments here so that they are properly hidden when the item is read into the model. */ m_moss_position = mapi_mark_or_create_moss_attach (message, m_type); if (!m_moss_position) { log_error ("%s:%s: Failed to find moss attachment.", SRCNAME, __func__); m_type = MSGTYPE_UNKNOWN; } gpgol_release (message); return 0; } static LPDISPATCH get_attachment (LPDISPATCH mailitem, int pos) { LPDISPATCH attachment; LPDISPATCH attachments = get_oom_object (mailitem, "Attachments"); if (!attachments) { log_debug ("%s:%s: Failed to get attachments.", SRCNAME, __func__); return NULL; } std::string item_str; int count = get_oom_int (attachments, "Count"); if (pos > 0) { item_str = std::string("Item(") + std::to_string(pos) + ")"; } else { item_str = std::string("Item(") + std::to_string(count) + ")"; } if (count < 1) { log_debug ("%s:%s: Invalid attachment count: %i.", SRCNAME, __func__, count); gpgol_release (attachments); return NULL; } attachment = get_oom_object (attachments, item_str.c_str()); gpgol_release (attachments); return attachment; } /** Helper to check that all attachments are hidden, to be called before crypto. */ int Mail::check_attachments () const { LPDISPATCH attachments = get_oom_object (m_mailitem, "Attachments"); if (!attachments) { log_debug ("%s:%s: Failed to get attachments.", SRCNAME, __func__); return 1; } int count = get_oom_int (attachments, "Count"); if (!count) { gpgol_release (attachments); return 0; } std::string message; if (is_encrypted () && is_signed ()) { message += _("Not all attachments were encrypted or signed.\n" "The unsigned / unencrypted attachments are:\n\n"); } else if (is_signed ()) { message += _("Not all attachments were signed.\n" "The unsigned attachments are:\n\n"); } else if (is_encrypted ()) { message += _("Not all attachments were encrypted.\n" "The unencrypted attachments are:\n\n"); } else { gpgol_release (attachments); return 0; } bool foundOne = false; for (int i = 1; i <= count; i++) { std::string item_str; item_str = std::string("Item(") + std::to_string (i) + ")"; LPDISPATCH oom_attach = get_oom_object (attachments, item_str.c_str ()); if (!oom_attach) { log_error ("%s:%s: Failed to get attachment.", SRCNAME, __func__); continue; } VARIANT var; VariantInit (&var); if (get_pa_variant (oom_attach, PR_ATTACHMENT_HIDDEN_DASL, &var) || (var.vt == VT_BOOL && var.boolVal == VARIANT_FALSE)) { foundOne = true; message += get_oom_string (oom_attach, "DisplayName"); message += "\n"; } VariantClear (&var); gpgol_release (oom_attach); } gpgol_release (attachments); if (foundOne) { message += "\n"; message += _("Note: The attachments may be encrypted or signed " "on a file level but the GpgOL status does not apply to them."); wchar_t *wmsg = utf8_to_wchar (message.c_str ()); wchar_t *wtitle = utf8_to_wchar (_("GpgOL Warning")); MessageBoxW (get_active_hwnd (), wmsg, wtitle, MB_ICONWARNING|MB_OK); xfree (wmsg); xfree (wtitle); } return 0; } /** Get the cipherstream of the mailitem. */ static LPSTREAM get_attachment_stream (LPDISPATCH mailitem, int pos) { if (!pos) { log_debug ("%s:%s: Called with zero pos.", SRCNAME, __func__); return NULL; } LPDISPATCH attachment = get_attachment (mailitem, pos); LPATTACH mapi_attachment = NULL; LPSTREAM stream = NULL; mapi_attachment = (LPATTACH) get_oom_iunknown (attachment, "MapiObject"); gpgol_release (attachment); if (!mapi_attachment) { log_debug ("%s:%s: Failed to get MapiObject of attachment: %p", SRCNAME, __func__, attachment); return NULL; } if (FAILED (mapi_attachment->OpenProperty (PR_ATTACH_DATA_BIN, &IID_IStream, 0, MAPI_MODIFY, (LPUNKNOWN*) &stream))) { log_debug ("%s:%s: Failed to open stream for mapi_attachment: %p", SRCNAME, __func__, mapi_attachment); gpgol_release (mapi_attachment); } return stream; } #if 0 This should work. But Outlook says no. See the comment in set_pa_variant about this. I left the code here as an example how to work with safearrays and how this probably should work. static int copy_data_property(LPDISPATCH target, std::shared_ptr attach) { VARIANT var; VariantInit (&var); /* Get the size */ off_t size = attach->get_data ().seek (0, SEEK_END); attach->get_data ().seek (0, SEEK_SET); if (!size) { TRACEPOINT; return 1; } if (!get_pa_variant (target, PR_ATTACH_DATA_BIN_DASL, &var)) { log_debug("Have variant. type: %x", var.vt); } else { log_debug("failed to get variant."); } /* Set the type to an array of unsigned chars (OLE SAFEARRAY) */ var.vt = VT_ARRAY | VT_UI1; /* Set up the bounds structure */ SAFEARRAYBOUND rgsabound[1]; rgsabound[0].cElements = static_cast (size); rgsabound[0].lLbound = 0; /* Create an OLE SAFEARRAY */ var.parray = SafeArrayCreate (VT_UI1, 1, rgsabound); if (var.parray == NULL) { TRACEPOINT; VariantClear(&var); return 1; } void *buffer = NULL; /* Get a safe pointer to the array */ if (SafeArrayAccessData(var.parray, &buffer) != S_OK) { TRACEPOINT; VariantClear(&var); return 1; } /* Copy data to it */ size_t nread = attach->get_data ().read (buffer, static_cast (size)); if (nread != static_cast (size)) { TRACEPOINT; VariantClear(&var); return 1; } /*/ Unlock the variant data */ if (SafeArrayUnaccessData(var.parray) != S_OK) { TRACEPOINT; VariantClear(&var); return 1; } if (set_pa_variant (target, PR_ATTACH_DATA_BIN_DASL, &var)) { TRACEPOINT; VariantClear(&var); return 1; } VariantClear(&var); return 0; } #endif static int copy_attachment_to_file (std::shared_ptr att, HANDLE hFile) { char copybuf[COPYBUFSIZE]; size_t nread; /* Security considerations: Writing the data to a temporary file is necessary as neither MAPI manipulation works in the read event to transmit the data nor Property Accessor works (see above). From a security standpoint there is a short time where the temporary files are on disk. Tempdir should be protected so that only the user can read it. Thus we have a local attack that could also take the data out of Outlook. FILE_SHARE_READ is necessary so that outlook can read the file. A bigger concern is that the file is manipulated by another software to fake the signature state. So we keep the write exlusive to us. We delete the file before closing the write file handle. */ /* Make sure we start at the beginning */ att->get_data ().seek (0, SEEK_SET); while ((nread = att->get_data ().read (copybuf, COPYBUFSIZE))) { DWORD nwritten; if (!WriteFile (hFile, copybuf, nread, &nwritten, NULL)) { log_error ("%s:%s: Failed to write in tmp attachment.", SRCNAME, __func__); return 1; } if (nread != nwritten) { log_error ("%s:%s: Write truncated.", SRCNAME, __func__); return 1; } } return 0; } /** Sets some meta data on the last attachment atted. The meta data is taken from the attachment object. */ static int fixup_last_attachment (LPDISPATCH mail, std::shared_ptr attachment) { /* Currently we only set content id */ if (attachment->get_content_id ().empty()) { log_debug ("%s:%s: Content id not found.", SRCNAME, __func__); return 0; } LPDISPATCH attach = get_attachment (mail, -1); if (!attach) { log_error ("%s:%s: No attachment.", SRCNAME, __func__); return 1; } int ret = put_pa_string (attach, PR_ATTACH_CONTENT_ID_DASL, attachment->get_content_id ().c_str()); gpgol_release (attach); return ret; } /** Helper to update the attachments of a mail object in oom. does not modify the underlying mapi structure. */ static int add_attachments(LPDISPATCH mail, std::vector > attachments) { int err = 0; for (auto att: attachments) { if (att->get_display_name().empty()) { log_error ("%s:%s: Ignoring attachment without display name.", SRCNAME, __func__); continue; } wchar_t* wchar_name = utf8_to_wchar (att->get_display_name().c_str()); HANDLE hFile; wchar_t* wchar_file = get_tmp_outfile (GpgOLStr (att->get_display_name().c_str()), &hFile); if (copy_attachment_to_file (att, hFile)) { log_error ("%s:%s: Failed to copy attachment %s to temp file", SRCNAME, __func__, att->get_display_name().c_str()); err = 1; } if (add_oom_attachment (mail, wchar_file, wchar_name)) { log_error ("%s:%s: Failed to add attachment: %s", SRCNAME, __func__, att->get_display_name().c_str()); err = 1; } if (!DeleteFileW (wchar_file)) { log_error ("%s:%s: Failed to delete tmp attachment for: %s", SRCNAME, __func__, att->get_display_name().c_str()); err = 1; } xfree (wchar_file); xfree (wchar_name); err = fixup_last_attachment (mail, att); } return err; } GPGRT_LOCK_DEFINE(parser_lock); static DWORD WINAPI do_parsing (LPVOID arg) { gpgrt_lock_lock (&dtor_lock); /* We lock with mail dtors so we can be sure the mail->parser call is valid. */ Mail *mail = (Mail *)arg; if (!Mail::is_valid_ptr (mail)) { log_debug ("%s:%s: canceling parsing for: %p already deleted", SRCNAME, __func__, arg); gpgrt_lock_unlock (&dtor_lock); return 0; } /* This takes a shared ptr of parser. So the parser is still valid when the mail is deleted. */ auto parser = mail->parser(); gpgrt_lock_unlock (&dtor_lock); gpgrt_lock_lock (&parser_lock); /* We lock the parser here to avoid too many decryption attempts if there are multiple mailobjects which might have already been deleted (e.g. by quick switches of the mailview.) Let's rather be a bit slower. */ log_debug ("%s:%s: preparing the parser for: %p", SRCNAME, __func__, arg); if (!Mail::is_valid_ptr (mail)) { log_debug ("%s:%s: cancel for: %p already deleted", SRCNAME, __func__, arg); gpgrt_lock_unlock (&parser_lock); return 0; } if (!parser) { log_error ("%s:%s: no parser found for mail: %p", SRCNAME, __func__, arg); gpgrt_lock_unlock (&parser_lock); return -1; } parser->parse(); do_in_ui_thread (PARSING_DONE, arg); gpgrt_lock_unlock (&parser_lock); return 0; } bool Mail::is_crypto_mail() const { if (m_type == MSGTYPE_UNKNOWN || m_type == MSGTYPE_GPGOL || m_type == MSGTYPE_SMIME) { /* Not a message for us. */ return false; } return true; } int Mail::decrypt_verify() { if (!is_crypto_mail()) { return 0; } if (m_needs_wipe) { log_error ("%s:%s: Decrypt verify called for msg that needs wipe: %p", SRCNAME, __func__, m_mailitem); return 1; } set_uuid (); m_processed = true; /* Insert placeholder */ char *placeholder_buf; if (gpgrt_asprintf (&placeholder_buf, opt.prefer_html ? decrypt_template_html : decrypt_template, is_smime() ? "S/MIME" : "OpenPGP", _("Encrypted message"), _("Please wait while the message is being decrypted / verified...")) == -1) { log_error ("%s:%s: Failed to format placeholder.", SRCNAME, __func__); return 1; } if (opt.prefer_html) { m_orig_body = get_oom_string (m_mailitem, "HTMLBody"); if (put_oom_string (m_mailitem, "HTMLBody", placeholder_buf)) { log_error ("%s:%s: Failed to modify html body of item.", SRCNAME, __func__); } } else { m_orig_body = get_oom_string (m_mailitem, "Body"); if (put_oom_string (m_mailitem, "Body", placeholder_buf)) { log_error ("%s:%s: Failed to modify body of item.", SRCNAME, __func__); } } xfree (placeholder_buf); /* Do the actual parsing */ auto cipherstream = get_attachment_stream (m_mailitem, m_moss_position); if (!cipherstream) { log_debug ("%s:%s: Failed to get cipherstream.", SRCNAME, __func__); return 1; } m_parser = std::shared_ptr (new ParseController (cipherstream, m_type)); m_parser->setSender(GpgME::UserID::addrSpecFromString(get_sender().c_str())); log_mime_parser ("%s:%s: Parser for \"%s\" is %p", SRCNAME, __func__, get_subject ().c_str(), m_parser.get()); gpgol_release (cipherstream); HANDLE parser_thread = CreateThread (NULL, 0, do_parsing, (LPVOID) this, 0, NULL); if (!parser_thread) { log_error ("%s:%s: Failed to create decrypt / verify thread.", SRCNAME, __func__); } CloseHandle (parser_thread); return 0; } void find_and_replace(std::string& source, const std::string &find, const std::string &replace) { for(std::string::size_type i = 0; (i = source.find(find, i)) != std::string::npos;) { source.replace(i, find.length(), replace); i += replace.length(); } } void Mail::update_body() { const auto error = m_parser->get_formatted_error (); if (!error.empty()) { if (opt.prefer_html) { if (put_oom_string (m_mailitem, "HTMLBody", error.c_str ())) { log_error ("%s:%s: Failed to modify html body of item.", SRCNAME, __func__); } } else { if (put_oom_string (m_mailitem, "Body", error.c_str ())) { log_error ("%s:%s: Failed to modify html body of item.", SRCNAME, __func__); } } return; } if (m_verify_result.error()) { log_error ("%s:%s: Verification failed. Restoring Body.", SRCNAME, __func__); if (opt.prefer_html) { if (put_oom_string (m_mailitem, "HTMLBody", m_orig_body.c_str ())) { log_error ("%s:%s: Failed to modify html body of item.", SRCNAME, __func__); } } else { if (put_oom_string (m_mailitem, "Body", m_orig_body.c_str ())) { log_error ("%s:%s: Failed to modify html body of item.", SRCNAME, __func__); } } return; } // No need to carry body anymore m_orig_body = std::string(); auto html = m_parser->get_html_body (); /** Outlook does not show newlines if \r\r\n is a newline. We replace these as apparently some other buggy MUA sends this. */ find_and_replace (html, "\r\r\n", "\r\n"); if (opt.prefer_html && !html.empty()) { char *converted = ansi_charset_to_utf8 (m_parser->get_html_charset().c_str(), html.c_str(), html.size()); int ret = put_oom_string (m_mailitem, "HTMLBody", converted ? converted : ""); xfree (converted); if (ret) { log_error ("%s:%s: Failed to modify html body of item.", SRCNAME, __func__); } return; } auto body = m_parser->get_body (); find_and_replace (body, "\r\r\n", "\r\n"); char *converted = ansi_charset_to_utf8 (m_parser->get_body_charset().c_str(), body.c_str(), body.size()); int ret = put_oom_string (m_mailitem, "Body", converted ? converted : ""); xfree (converted); if (ret) { log_error ("%s:%s: Failed to modify body of item.", SRCNAME, __func__); } return; } void Mail::parsing_done() { TRACEPOINT; log_oom_extra ("Mail %p Parsing done for parser: %p", this, m_parser.get()); if (!m_parser) { /* This should not happen but it happens when outlook sends multiple ItemLoad events for the same Mail Object. In that case it could happen that one parser was already done while a second is now returning for the wrong mail (as it's looked up by uuid.) We have a check in get_uuid that the uuid was not in the map before (and the parser is replaced). So this really really should not happen. We handle it anyway as we crash otherwise. It should not happen because the parser is only created in decrypt_verify which is called in the read event. And even in there we check if the parser was set. */ log_error ("%s:%s: No parser obj. For mail: %p", SRCNAME, __func__, this); return; } /* Store the results. */ m_decrypt_result = m_parser->decrypt_result (); m_verify_result = m_parser->verify_result (); m_crypto_flags = 0; if (!m_decrypt_result.isNull()) { m_crypto_flags |= 1; } if (m_verify_result.numSignatures()) { m_crypto_flags |= 2; } update_sigstate (); m_needs_wipe = true; TRACEPOINT; /* Set categories according to the result. */ update_categories(); TRACEPOINT; /* Update the body */ update_body(); TRACEPOINT; /* Check that there are no unsigned / unencrypted messages. */ check_attachments (); /* Update attachments */ if (add_attachments (m_mailitem, m_parser->get_attachments())) { log_error ("%s:%s: Failed to update attachments.", SRCNAME, __func__); } /* Invalidate UI to set the correct sig status. */ m_parser = nullptr; gpgoladdin_invalidate_ui (); TRACEPOINT; return; } int Mail::encrypt_sign () { int err = -1, flags = 0; protocol_t proto = opt.enable_smime ? PROTOCOL_UNKNOWN : PROTOCOL_OPENPGP; if (!needs_crypto()) { return 0; } LPMESSAGE message = get_oom_base_message (m_mailitem); if (!message) { log_error ("%s:%s: Failed to get base message.", SRCNAME, __func__); return err; } flags = get_gpgol_draft_info_flags (message); const auto window = get_active_hwnd (); EnableWindow (window, FALSE); if (flags == 3) { log_debug ("%s:%s: Sign / Encrypting message", SRCNAME, __func__); err = message_sign_encrypt (message, proto, NULL, get_sender ().c_str (), this); } else if (flags == 2) { err = message_sign (message, proto, NULL, get_sender ().c_str (), this); } else if (flags == 1) { err = message_encrypt (message, proto, NULL, get_sender ().c_str (), this); } else { log_debug ("%s:%s: Unknown flags for crypto: %i", SRCNAME, __func__, flags); } log_debug ("%s:%s: Status: %i", SRCNAME, __func__, err); EnableWindow (window, TRUE); gpgol_release (message); m_crypt_successful = !err; return err; } int Mail::needs_crypto () { LPMESSAGE message = get_oom_message (m_mailitem); bool ret; if (!message) { log_error ("%s:%s: Failed to get message.", SRCNAME, __func__); return false; } ret = get_gpgol_draft_info_flags (message); gpgol_release(message); return ret; } int Mail::wipe (bool force) { if (!m_needs_wipe && !force) { return 0; } log_debug ("%s:%s: Removing plaintext from mailitem: %p.", SRCNAME, __func__, m_mailitem); if (put_oom_string (m_mailitem, "HTMLBody", "")) { if (put_oom_string (m_mailitem, "Body", "")) { log_debug ("%s:%s: Failed to wipe mailitem: %p.", SRCNAME, __func__, m_mailitem); return -1; } return -1; } m_needs_wipe = false; return 0; } int Mail::update_oom_data () { LPDISPATCH sender = NULL; log_debug ("%s:%s", SRCNAME, __func__); /* Update the body format. */ m_is_html_alternative = get_oom_int (m_mailitem, "BodyFormat") > 1; /* Store the body. It was not obvious for me (aheinecke) how to access this through MAPI. */ m_html_body = get_oom_string (m_mailitem, "HTMLBody"); /* For some reason outlook may store the recipient address in the send using account field. If we have SMTP we prefer the SenderEmailAddress string. */ if (is_crypto_mail ()) { /* This is the case where we are reading a mail and not composing. When composing we need to use the SendUsingAccount because if you send from the folder of userA but change the from to userB outlook will keep the SenderEmailAddress of UserA. This is all so horrible. */ char *type = get_oom_string (m_mailitem, "SenderEmailType"); if (type && !strcmp ("SMTP", type)) { char *senderMail = get_oom_string (m_mailitem, "SenderEmailAddress"); if (senderMail) { m_sender = senderMail; xfree (senderMail); xfree (type); return 0; } } xfree (type); } sender = get_oom_object (m_mailitem, "SendUsingAccount"); if (sender) { char *buf = get_oom_string (sender, "SmtpAddress"); m_sender = buf; xfree (buf); gpgol_release (sender); return 0; } /* Fallback to Sender object */ sender = get_oom_object (m_mailitem, "Sender"); if (sender) { char *buf = get_pa_string (sender, PR_SMTP_ADDRESS_DASL); m_sender = buf; xfree (buf); gpgol_release (sender); return 0; } /* We don't have s sender object or SendUsingAccount, well, in that case fall back to the current user. */ sender = get_oom_object (m_mailitem, "Session.CurrentUser"); if (sender) { char *buf = get_pa_string (sender, PR_SMTP_ADDRESS_DASL); m_sender = buf; xfree (buf); gpgol_release (sender); return 0; } log_debug ("%s:%s: All fallbacks failed.", SRCNAME, __func__); return -1; } std::string Mail::get_sender () { if (m_sender.empty()) update_oom_data(); return m_sender; } int Mail::close_all_mails () { int err = 0; std::map::iterator it; TRACEPOINT; std::map mail_map_copy = g_mail_map; for (it = mail_map_copy.begin(); it != mail_map_copy.end(); ++it) { if (!it->second->is_crypto_mail()) { continue; } if (close_inspector (it->second) || close (it->second)) { log_error ("Failed to close mail: %p ", it->first); /* Should not happen */ if (is_valid_ptr (it->second) && it->second->revert()) { err++; } } } return err; } int Mail::revert_all_mails () { int err = 0; std::map::iterator it; for (it = g_mail_map.begin(); it != g_mail_map.end(); ++it) { if (it->second->revert ()) { log_error ("Failed to revert mail: %p ", it->first); err++; continue; } it->second->set_needs_save (true); if (!invoke_oom_method (it->first, "Save", NULL)) { log_error ("Failed to save reverted mail: %p ", it->second); err++; continue; } } return err; } int Mail::wipe_all_mails () { int err = 0; std::map::iterator it; for (it = g_mail_map.begin(); it != g_mail_map.end(); ++it) { if (it->second->wipe ()) { log_error ("Failed to wipe mail: %p ", it->first); err++; } } return err; } int Mail::revert () { int err = 0; if (!m_processed) { return 0; } err = gpgol_mailitem_revert (m_mailitem); if (err == -1) { log_error ("%s:%s: Message revert failed falling back to wipe.", SRCNAME, __func__); return wipe (); } /* We need to reprocess the mail next time around. */ m_processed = false; m_needs_wipe = false; return 0; } bool Mail::is_smime () { msgtype_t msgtype; LPMESSAGE message; if (m_is_smime_checked) { return m_is_smime; } message = get_oom_message (m_mailitem); if (!message) { log_error ("%s:%s: No message?", SRCNAME, __func__); return false; } msgtype = mapi_get_message_type (message); m_is_smime = msgtype == MSGTYPE_GPGOL_OPAQUE_ENCRYPTED || msgtype == MSGTYPE_GPGOL_OPAQUE_SIGNED; /* Check if it is an smime mail. Multipart signed can also be true. */ if (!m_is_smime && msgtype == MSGTYPE_GPGOL_MULTIPART_SIGNED) { char *proto; char *ct = mapi_get_message_content_type (message, &proto, NULL); if (ct && proto) { m_is_smime = (!strcmp (proto, "application/pkcs7-signature") || !strcmp (proto, "application/x-pkcs7-signature")); } else { log_error ("Protocol in multipart signed mail."); } xfree (proto); xfree (ct); } gpgol_release (message); m_is_smime_checked = true; return m_is_smime; } static std::string get_string (LPDISPATCH item, const char *str) { char *buf = get_oom_string (item, str); if (!buf) return std::string(); std::string ret = buf; xfree (buf); return ret; } std::string Mail::get_subject() const { return get_string (m_mailitem, "Subject"); } std::string Mail::get_body() const { return get_string (m_mailitem, "Body"); } std::string Mail::get_html_body() const { return get_string (m_mailitem, "HTMLBody"); } char ** Mail::get_recipients() const { LPDISPATCH recipients = get_oom_object (m_mailitem, "Recipients"); if (!recipients) { TRACEPOINT; return nullptr; } auto ret = get_oom_recipients (recipients); gpgol_release (recipients); return ret; } int Mail::close_inspector (Mail *mail) { LPDISPATCH inspector = get_oom_object (mail->item(), "GetInspector"); HRESULT hr; DISPID dispid; if (!inspector) { log_debug ("%s:%s: No inspector.", SRCNAME, __func__); return -1; } dispid = lookup_oom_dispid (inspector, "Close"); if (dispid != DISPID_UNKNOWN) { VARIANT aVariant[1]; DISPPARAMS dispparams; dispparams.rgvarg = aVariant; dispparams.rgvarg[0].vt = VT_INT; dispparams.rgvarg[0].intVal = 1; dispparams.cArgs = 1; dispparams.cNamedArgs = 0; hr = inspector->Invoke (dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, &dispparams, NULL, NULL, NULL); if (hr != S_OK) { log_debug ("%s:%s: Failed to close inspector: %#lx", SRCNAME, __func__, hr); return -1; } } return 0; } /* static */ int Mail::close (Mail *mail) { VARIANT aVariant[1]; DISPPARAMS dispparams; dispparams.rgvarg = aVariant; dispparams.rgvarg[0].vt = VT_INT; dispparams.rgvarg[0].intVal = 1; dispparams.cArgs = 1; dispparams.cNamedArgs = 0; log_oom_extra ("%s:%s: Invoking close for: %p", SRCNAME, __func__, mail->item()); mail->set_close_triggered (true); int rc = invoke_oom_method_with_parms (mail->item(), "Close", NULL, &dispparams); log_debug ("returned from invoke"); return rc; } void Mail::set_close_triggered (bool value) { m_close_triggered = value; } bool Mail::get_close_triggered () const { return m_close_triggered; } static const UserID get_uid_for_sender (const Key &k, const char *sender) { UserID ret; if (!sender) { return ret; } if (!k.numUserIDs()) { log_debug ("%s:%s: Key without uids", SRCNAME, __func__); return ret; } for (const auto uid: k.userIDs()) { if (!uid.email()) { log_error ("%s:%s: skipping uid without email.", SRCNAME, __func__); continue; } auto normalized_uid = uid.addrSpec(); auto normalized_sender = UserID::addrSpecFromString(sender); if (normalized_sender.empty() || normalized_uid.empty()) { log_error ("%s:%s: normalizing '%s' or '%s' failed.", SRCNAME, __func__, uid.email(), sender); continue; } if (normalized_sender == normalized_uid) { ret = uid; } } return ret; } void Mail::update_sigstate () { std::string sender = get_sender(); if (sender.empty()) { log_error ("%s:%s:%i", SRCNAME, __func__, __LINE__); return; } if (m_verify_result.isNull()) { log_debug ("%s:%s: No verify result.", SRCNAME, __func__); return; } if (m_verify_result.error()) { log_debug ("%s:%s: verify error.", SRCNAME, __func__); return; } for (const auto sig: m_verify_result.signatures()) { m_is_signed = true; m_uid = get_uid_for_sender (sig.key(), sender.c_str()); if (m_uid.isNull() || (sig.validity() != Signature::Validity::Marginal && sig.validity() != Signature::Validity::Full && sig.validity() != Signature::Validity::Ultimate)) { /* For our category we only care about trusted sigs. And the UID needs to match.*/ continue; } if (sig.validity() == Signature::Validity::Marginal) { const auto tofu = m_uid.tofuInfo(); if (!tofu.isNull() && (tofu.validity() != TofuInfo::Validity::BasicHistory && tofu.validity() != TofuInfo::Validity::LargeHistory)) { /* Marginal is only good enough without tofu. Otherwise we wait for basic trust. */ log_debug ("%s:%s: Discarding marginal signature." "With too little history.", SRCNAME, __func__); continue; } } log_debug ("%s:%s: Classified sender as verified uid validity: %i", SRCNAME, __func__, m_uid.validity()); m_sig = sig; m_is_valid = true; return; } log_debug ("%s:%s: No signature with enough trust. Using first", SRCNAME, __func__); m_sig = m_verify_result.signature(0); return; } bool Mail::is_valid_sig () { return m_is_valid; } void Mail::remove_categories () { const char *decCategory = _("GpgOL: Encrypted Message"); const char *verifyCategory = _("GpgOL: Trusted Sender Address"); remove_category (m_mailitem, decCategory); remove_category (m_mailitem, verifyCategory); } /* Now for some tasty hack: Outlook sometimes does not show the new categories properly but instead does some weird scrollbar thing. This can be avoided by resizing the message a bit. But somehow this only needs to be done once. Weird isn't it? But as this workaround worked let's do it programatically. Fun. Wan't some tomato sauce with this hack? */ static void resize_active_window () { HWND wnd = get_active_hwnd (); static std::vector resized_windows; if(std::find(resized_windows.begin(), resized_windows.end(), wnd) != resized_windows.end()) { /* We only need to do this once per window. XXX But sometimes we also need to do this once per view of the explorer. So for now this might break but we reduce the flicker. A better solution would be to find the current view and track that. */ return; } if (!wnd) { TRACEPOINT; return; } RECT oldpos; if (!GetWindowRect (wnd, &oldpos)) { TRACEPOINT; return; } if (!SetWindowPos (wnd, nullptr, (int)oldpos.left, (int)oldpos.top, /* Anything smaller then 19 was ignored when the window was * maximized on Windows 10 at least with a 1980*1024 * resolution. So I assume it's at least 1 percent. * This is all hackish and ugly but should work for 90%... * hopefully. */ (int)oldpos.right - oldpos.left - 20, (int)oldpos.bottom - oldpos.top, 0)) { TRACEPOINT; return; } if (!SetWindowPos (wnd, nullptr, (int)oldpos.left, (int)oldpos.top, (int)oldpos.right - oldpos.left, (int)oldpos.bottom - oldpos.top, 0)) { TRACEPOINT; return; } resized_windows.push_back(wnd); } void Mail::update_categories () { const char *decCategory = _("GpgOL: Encrypted Message"); const char *verifyCategory = _("GpgOL: Trusted Sender Address"); if (is_valid_sig()) { add_category (m_mailitem, verifyCategory); } else { remove_category (m_mailitem, verifyCategory); } if (!m_decrypt_result.isNull()) { add_category (m_mailitem, decCategory); } else { /* As a small safeguard against fakes we remove our categories */ remove_category (m_mailitem, decCategory); } resize_active_window(); return; } bool Mail::is_signed() const { return m_verify_result.numSignatures() > 0; } bool Mail::is_encrypted() const { return !m_decrypt_result.isNull(); } int Mail::set_uuid() { char *uuid; if (!m_uuid.empty()) { /* This codepath is reached by decrypt again after a close with discard changes. The close discarded the uuid on the OOM object so we have to set it again. */ log_debug ("%s:%s: Resetting uuid for %p to %s", SRCNAME, __func__, this, m_uuid.c_str()); uuid = get_unique_id (m_mailitem, 1, m_uuid.c_str()); } else { uuid = get_unique_id (m_mailitem, 1, nullptr); log_debug ("%s:%s: uuid for %p set to %s", SRCNAME, __func__, this, uuid); } if (!uuid) { log_debug ("%s:%s: Failed to get/set uuid for %p", SRCNAME, __func__, m_mailitem); return -1; } if (m_uuid.empty()) { m_uuid = uuid; Mail *other = get_mail_for_uuid (uuid); if (other) { /* According to documentation this should not happen as this means that multiple ItemLoad events occured for the same mailobject without unload / destruction of the mail. But it happens. If you invalidate the UI in the selection change event Outlook loads a new mailobject for the mail. Might happen in other surprising cases. We replace in that case as experiments have shown that the last mailobject is the one that is visible. Still troubling state so we log this as an error. */ log_error ("%s:%s: There is another mail for %p " "with uuid: %s replacing it.", SRCNAME, __func__, m_mailitem, uuid); delete other; } g_uid_map.insert (std::pair (m_uuid, this)); log_debug ("%s:%s: uuid for %p is now %s", SRCNAME, __func__, this, m_uuid.c_str()); } xfree (uuid); return 0; } /* Returns 2 if the userid is ultimately trusted. Returns 1 if the userid is fully trusted but has a signature by a key for which we have a secret and which is ultimately trusted. (Direct trust) 0 otherwise */ static int level_4_check (const UserID &uid) { if (uid.isNull()) { return 0; } if (uid.validity () == UserID::Validity::Ultimate) { return 2; } if (uid.validity () == UserID::Validity::Full) { for (const auto sig: uid.signatures ()) { const char *sigID = sig.signerKeyID (); if (sig.isNull() || !sigID) { /* should not happen */ TRACEPOINT; continue; } /* Direct trust information is not available through gnupg so we cached the keys with ultimate trust during parsing and now see if we find a direct trust path.*/ for (const auto secKey: ParseController::get_ultimate_keys ()) { /* Check that the Key id of the key matches */ const char *secKeyID = secKey.keyID (); if (!secKeyID || strcmp (secKeyID, sigID)) { continue; } /* Check that the userID of the signature is the ultimately trusted one. */ const char *sig_uid_str = sig.signerUserID(); if (!sig_uid_str) { /* should not happen */ TRACEPOINT; continue; } for (const auto signer_uid: secKey.userIDs ()) { if (signer_uid.validity() != UserID::Validity::Ultimate) { TRACEPOINT; continue; } const char *signer_uid_str = signer_uid.id (); if (!sig_uid_str) { /* should not happen */ TRACEPOINT; continue; } if (!strcmp(sig_uid_str, signer_uid_str)) { /* We have a match */ log_debug ("%s:%s: classified %s as ultimate because " "it was signed by uid %s of key %s", SRCNAME, __func__, signer_uid_str, sig_uid_str, secKeyID); return 1; } } } } } return 0; } std::string Mail::get_crypto_summary () { const int level = get_signature_level (); bool enc = is_encrypted (); if (level == 4 && enc) { return _("Security Level 4"); } if (level == 4) { return _("Trust Level 4"); } if (level == 3 && enc) { return _("Security Level 3"); } if (level == 3) { return _("Trust Level 3"); } if (level == 2 && enc) { return _("Security Level 2"); } if (level == 2) { return _("Trust Level 2"); } if (enc) { return _("Encrypted"); } if (is_signed ()) { /* Even if it is signed, if it is not validly signed it's still completly insecure as anyone could have signed this. So we avoid the label "signed" here as this word already implies some security. */ return _("Insecure"); } return _("Insecure"); } std::string Mail::get_crypto_one_line() { bool sig = is_signed (); bool enc = is_encrypted (); if (sig || enc) { if (sig && enc) { return _("Signed and encrypted message"); } else if (sig) { return _("Signed message"); } else if (enc) { return _("Encrypted message"); } } return _("Insecure message"); } std::string Mail::get_crypto_details() { std::string message; - /* Handle encrypt only */ + + /* No signature with keys but error */ + if (!is_encrypted() && !is_signed () && m_verify_result.error()) + { + message = _("You cannot be sure who sent, " + "modified and read the message in transit."); + message += "\n\n"; + message += _("The message was signed but the verification failed with:"); + message += "\n"; + message += m_verify_result.error().asString(); + return message; + } + /* No crypo, what are we doing here? */ if (!is_encrypted () && !is_signed ()) { return _("You cannot be sure who sent, " "modified and read the message in transit."); } - else if (is_encrypted() && !is_signed ()) + /* Handle encrypt only */ + if (is_encrypted() && !is_signed ()) { if (in_de_vs_mode ()) { if (m_sig.isDeVs()) { message += _("The encryption was VS-NfD-compliant."); } else { message += _("The encryption was not VS-NfD-compliant."); } } message += "\n\n"; message += _("You cannot be sure who sent the message because " "it is not signed."); return message; } bool keyFound = true; bool isOpenPGP = m_sig.key().isNull() ? !is_smime() : m_sig.key().protocol() == Protocol::OpenPGP; char *buf; bool hasConflict = false; int level = get_signature_level (); log_debug ("%s:%s: Formatting sig. Validity: %x Summary: %x Level: %i", SRCNAME, __func__, m_sig.validity(), m_sig.summary(), level); if (level == 4) { /* level 4 check for direct trust */ int four_check = level_4_check (m_uid); if (four_check == 2 && m_sig.key().hasSecret ()) { message = _("You signed this message."); } else if (four_check == 1) { message = _("The senders identity was certified by yourself."); } else if (four_check == 2) { message = _("The sender is allowed to certify identities for you."); } else { log_error ("%s:%s:%i BUG: Invalid sigstate.", SRCNAME, __func__, __LINE__); return message; } } else if (level == 3 && isOpenPGP) { /* Level three is only reachable through web of trust and no direct signature. */ message = _("The senders identity was certified by several trusted people."); } else if (level == 3 && !isOpenPGP) { /* Level three is the only level for trusted S/MIME keys. */ gpgrt_asprintf (&buf, _("The senders identity is certified by the trusted issuer:\n'%s'\n"), m_sig.key().issuerName()); message = buf; xfree (buf); } else if (level == 2 && m_uid.tofuInfo ().isNull ()) { /* Marginal trust through pgp only */ message = _("Some trusted people " "have certified the senders identity."); } else if (level == 2) { unsigned long first_contact = std::max (m_uid.tofuInfo().signFirst(), m_uid.tofuInfo().encrFirst()); char *time = format_date_from_gpgme (first_contact); /* i18n note signcount is always pulral because with signcount 1 we * would not be in this branch. */ gpgrt_asprintf (&buf, _("The senders address is trusted, because " "you have established a communication history " "with this address starting on %s.\n" "You encrypted %i and verified %i messages since."), time, m_uid.tofuInfo().encrCount(), m_uid.tofuInfo().signCount ()); xfree (time); message = buf; xfree (buf); } else if (level == 1) { /* This could be marginal trust through pgp, or tofu with little history. */ if (m_uid.tofuInfo ().signCount() == 1) { message += _("The senders signature was verified for the first time."); } else if (m_uid.tofuInfo ().validity() == TofuInfo::Validity::LittleHistory) { unsigned long first_contact = std::max (m_uid.tofuInfo().signFirst(), m_uid.tofuInfo().encrFirst()); char *time = format_date_from_gpgme (first_contact); gpgrt_asprintf (&buf, _("The senders address is not trustworthy yet because " "you only verified %i messages and encrypted %i messages to " "it since %s."), m_uid.tofuInfo().signCount (), m_uid.tofuInfo().encrCount (), time); xfree (time); message = buf; xfree (buf); } } else { /* Now we are in level 0, this could be a technical problem, no key or just unkown. */ message = is_encrypted () ? _("But the sender address is not trustworthy because:") : _("The sender address is not trustworthy because:"); message += "\n"; keyFound = !(m_sig.summary() & Signature::Summary::KeyMissing); bool general_problem = true; /* First the general stuff. */ if (m_sig.summary() & Signature::Summary::Red) { message += _("The signature is invalid: \n"); } else if (m_sig.summary() & Signature::Summary::SysError || m_verify_result.numSignatures() < 1) { message += _("There was an error verifying the signature.\n"); } else if (m_sig.summary() & Signature::Summary::SigExpired) { message += _("The signature is expired.\n"); } else { message += isOpenPGP ? _("The used key") : _("The used certificate"); message += " "; general_problem = false; } /* Now the key problems */ if ((m_sig.summary() & Signature::Summary::KeyMissing)) { message += _("is not available."); } else if ((m_sig.summary() & Signature::Summary::KeyRevoked)) { message += _("is revoked."); } else if ((m_sig.summary() & Signature::Summary::KeyExpired)) { message += _("is expired."); } else if ((m_sig.summary() & Signature::Summary::BadPolicy)) { message += _("is not meant for signing."); } else if ((m_sig.summary() & Signature::Summary::CrlMissing)) { message += _("could not be checked for revocation."); } else if ((m_sig.summary() & Signature::Summary::CrlTooOld)) { message += _("could not be checked for revocation."); } else if ((m_sig.summary() & Signature::Summary::TofuConflict) || m_uid.tofuInfo().validity() == TofuInfo::Conflict) { message += _("is not the same as the key that was used " "for this address in the past."); hasConflict = true; } else if (m_uid.isNull()) { gpgrt_asprintf (&buf, _("does not claim the address: \"%s\"."), get_sender().c_str()); message += buf; xfree (buf); } else if (((m_sig.validity() & Signature::Validity::Undefined) || (m_sig.validity() & Signature::Validity::Unknown) || (m_sig.summary() == Signature::Summary::None) || (m_sig.validity() == 0))&& !general_problem) { /* Bit of a catch all for weird results. */ if (isOpenPGP) { message += _("is not certified by any trustworthy key."); } else { message += _("is not certified by a trustworthy Certificate Authority or the Certificate Authority is unknown."); } } else if (m_uid.isRevoked()) { message += _("The sender marked this address as revoked."); } else if ((m_sig.validity() & Signature::Validity::Never)) { message += _("is marked as not trustworthy."); } } message += "\n\n"; if (in_de_vs_mode ()) { if (is_signed ()) { if (m_sig.isDeVs ()) { message += _("The signature is VS-NfD-compliant."); } else { message += _("The signature is not VS-NfD-compliant."); } message += "\n"; } if (is_encrypted ()) { if (m_decrypt_result.isDeVs ()) { message += _("The encryption is VS-NfD-compliant."); } else { message += _("The encryption is not VS-NfD-compliant."); } message += "\n\n"; } else { message += "\n"; } } if (hasConflict) { message += _("Click here to change the key used for this address."); } else if (keyFound) { message += isOpenPGP ? _("Click here for details about the key.") : _("Click here for details about the certificate."); } else { message += isOpenPGP ? _("Click here to search the key on the configured keyserver.") : _("Click here to search the certificate on the configured X509 keyserver."); } return message; } int Mail::get_signature_level () const { if (!m_is_signed) { return 0; } if (m_uid.isNull ()) { /* No m_uid matches our sender. */ return 0; } if (m_is_valid && (m_uid.validity () == UserID::Validity::Ultimate || (m_uid.validity () == UserID::Validity::Full && level_4_check (m_uid))) && (!in_de_vs_mode () || m_sig.isDeVs())) { return 4; } if (m_is_valid && m_uid.validity () == UserID::Validity::Full && (!in_de_vs_mode () || m_sig.isDeVs())) { return 3; } if (m_is_valid) { return 2; } if (m_sig.validity() == Signature::Validity::Marginal) { return 1; } if (m_sig.summary() & Signature::Summary::TofuConflict || m_uid.tofuInfo().validity() == TofuInfo::Conflict) { return 0; } return 0; } int Mail::get_crypto_icon_id () const { int level = get_signature_level (); int offset = is_encrypted () ? ENCRYPT_ICON_OFFSET : 0; return IDI_LEVEL_0 + level + offset; } const char* Mail::get_sig_fpr() const { if (!m_is_signed || m_sig.isNull()) { return nullptr; } return m_sig.fingerprint(); } static DWORD WINAPI do_locate (LPVOID arg) { char *recipient = (char*) arg; log_debug ("%s:%s searching key for recipient: \"%s\"", SRCNAME, __func__, recipient); Context *ctx = Context::createForProtocol (OpenPGP); if (!ctx) { TRACEPOINT; return 0; } ctx->setKeyListMode (GpgME::Extern | GpgME::Local); ctx->startKeyListing (recipient, false); std::vector keys; Error err; do { keys.push_back (ctx->nextKey(err)); } while (!err); keys.pop_back (); ctx->endKeyListing (); delete ctx; if (keys.size ()) { log_debug ("%s:%s found key for recipient: \"%s\"", SRCNAME, __func__, recipient); } xfree (recipient); // do_in_ui_thread (UNKNOWN, NULL); return 0; } GPGRT_LOCK_DEFINE(uids_searched_lock); /** Try to locate the keys for all recipients */ void Mail::locate_keys() { gpgrt_lock_lock (&uids_searched_lock); char ** recipients = get_recipients (); if (!recipients) { TRACEPOINT; return; } for (int i = 0; recipients[i]; i++) { std::string recp = recipients[i]; if (uids_searched.find (recp) == uids_searched.end ()) { uids_searched.insert (recp); HANDLE thread = CreateThread (NULL, 0, do_locate, (LPVOID) strdup(recipients[i]), 0, NULL); CloseHandle (thread); } xfree (recipients[i]); } xfree (recipients); gpgrt_lock_unlock (&uids_searched_lock); } bool Mail::is_html_alternative () const { return m_is_html_alternative; } const std::string & Mail::get_cached_html_body () const { return m_html_body; } int Mail::get_crypto_flags () const { return m_crypto_flags; } void Mail::set_needs_encrypt (bool value) { m_needs_encrypt = value; } bool Mail::needs_encrypt() const { return m_needs_encrypt; } diff --git a/src/ribbon-callbacks.cpp b/src/ribbon-callbacks.cpp index 5af1215..8470db2 100644 --- a/src/ribbon-callbacks.cpp +++ b/src/ribbon-callbacks.cpp @@ -1,1756 +1,1756 @@ /* ribbon-callbacks.h - Callbacks for the ribbon extension interface * Copyright (C) 2013 Intevation GmbH * Software engineering by Intevation GmbH * * This file is part of GpgOL. * * GpgOL 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.1 of the License, or (at your option) any later version. * * GpgOL 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 this program; if not, see . */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include "ribbon-callbacks.h" #include "gpgoladdin.h" #include "common.h" #include "mymapi.h" #include "mymapitags.h" #include "myexchext.h" #include "common.h" #include "display.h" #include "msgcache.h" #include "engine.h" #include "engine-assuan.h" #include "mapihelp.h" #include "mimemaker.h" #include "filetype.h" #include "gpgolstr.h" #include "message.h" #include "mail.h" #include #include using namespace GpgME; #define OPAQUE_SIGNED_MARKER "-----BEGIN PGP MESSAGE-----" /* Gets the context of a ribbon control. And prints some useful debug output */ HRESULT getContext (LPDISPATCH ctrl, LPDISPATCH *context) { *context = get_oom_object (ctrl, "get_Context"); if (*context) { char *name = get_object_name (*context); log_debug ("%s:%s: contextObj: %s", SRCNAME, __func__, name); xfree (name); } return context ? S_OK : E_FAIL; } #define OP_ENCRYPT 1 /* Encrypt the data */ #define OP_SIGN 2 /* Sign the data */ #define OP_DECRYPT 1 /* Decrypt the data */ #define OP_VERIFY 2 /* Verify the data */ #define DATA_BODY 4 /* Use text body as data */ #define DATA_SELECTION 8 /* Use selection as data */ /* Read hfile in chunks of 4KB and writes them to the sink */ static int copyFileToSink (HANDLE hFile, sink_t sink) { char buf[4096]; DWORD bytesRead = 0; do { if (!ReadFile (hFile, buf, sizeof buf, &bytesRead, NULL)) { log_error ("%s:%s: Could not read source file.", SRCNAME, __func__); return -1; } if (write_buffer (sink, bytesRead ? buf : NULL, bytesRead)) { log_error ("%s:%s: Could not write out buffer", SRCNAME, __func__); return -1; } } while (bytesRead); return 0; } static int attachSignature (LPDISPATCH mailItem, char *subject, HANDLE hFileToSign, protocol_t protocol, unsigned int session_number, HWND curWindow, wchar_t *fileNameToSign, char *sender) { wchar_t *sigName = NULL; wchar_t *sigFileName = NULL; HANDLE hSigFile = NULL; int rc = 0; struct sink_s encsinkmem; sink_t encsink = &encsinkmem; struct sink_s sinkmem; sink_t sink = &sinkmem; engine_filter_t filter = NULL; memset (encsink, 0, sizeof *encsink); memset (sink, 0, sizeof *sink); /* Prepare a fresh filter */ if ((rc = engine_create_filter (&filter, write_buffer_for_cb, sink))) { goto failure; } encsink->cb_data = filter; encsink->writefnc = sink_encryption_write; engine_set_session_number (filter, session_number); engine_set_session_title (filter, subject ? subject :_("GpgOL")); if (engine_sign_start (filter, curWindow, protocol, sender, &protocol)) goto failure; sigName = get_pretty_attachment_name (fileNameToSign, protocol, 1); /* If we are unlucky the number of temporary file artifacts might differ for the signature and the encrypted file but we have to live with that. */ sigFileName = get_tmp_outfile (sigName, &hSigFile); sink->cb_data = hSigFile; sink->writefnc = sink_file_write; if (!sigFileName) { log_error ("%s:%s: Could not get a decent attachment name", SRCNAME, __func__); goto failure; } /* Reset the file to sign handle to the beginning of the file and copy it to the signature buffer */ SetFilePointer (hFileToSign, 0, NULL, 0); if ((rc=copyFileToSink (hFileToSign, encsink))) goto failure; /* Lets hope the user did not select a huge file. We are hanging here until encryption is completed.. */ if ((rc = engine_wait (filter))) goto failure; filter = NULL; /* Not valid anymore. */ encsink->cb_data = NULL; /* Not needed anymore. */ if (!sink->enc_counter) { log_error ("%s:%s: nothing received from engine", SRCNAME, __func__); goto failure; } /* Now we have an encrypted file behind encryptedFile. Let's add it */ add_oom_attachment (mailItem, sigFileName, nullptr); failure: xfree (sigFileName); xfree (sigName); if (hSigFile) { CloseHandle (hSigFile); DeleteFileW (sigFileName); } return rc; } /* do_composer_action Encrypts / Signs text in an IInspector context. Depending on the flags either the active selection or the full body is encrypted. Combine OP_ENCRYPT and OP_SIGN if you want both. */ HRESULT do_composer_action (LPDISPATCH ctrl, int flags) { LPDISPATCH context = NULL; LPDISPATCH selection = NULL; LPDISPATCH wordEditor = NULL; LPDISPATCH application = NULL; LPDISPATCH mailItem = NULL; LPDISPATCH sender = NULL; LPDISPATCH recipients = NULL; struct sink_s encsinkmem; sink_t encsink = &encsinkmem; struct sink_s sinkmem; sink_t sink = &sinkmem; char* senderAddr = NULL; char** recipientAddrs = NULL; LPSTREAM tmpstream = NULL; engine_filter_t filter = NULL; char* plaintext = NULL; int rc = 0; HRESULT hr; HWND curWindow; protocol_t protocol; unsigned int session_number; int i; STATSTG tmpStat; log_debug ("%s:%s: enter", SRCNAME, __func__); hr = getContext (ctrl, &context); if (FAILED(hr)) return hr; memset (encsink, 0, sizeof *encsink); memset (sink, 0, sizeof *sink); curWindow = get_oom_context_window (context); wordEditor = get_oom_object (context, "WordEditor"); application = get_oom_object (wordEditor, "get_Application"); selection = get_oom_object (application, "get_Selection"); mailItem = get_oom_object (context, "CurrentItem"); sender = get_oom_object (mailItem, "Session.CurrentUser"); recipients = get_oom_object (mailItem, "Recipients"); if (!wordEditor || !application || !selection || !mailItem || !sender || !recipients) { MessageBox (NULL, "Internal error in GpgOL.\n" "Could not find all objects.", _("GpgOL"), MB_ICONINFORMATION|MB_OK); log_error ("%s:%s: Could not find all objects.", SRCNAME, __func__); goto failure; } if (flags & DATA_SELECTION) { plaintext = get_oom_string (selection, "Text"); if (!plaintext || strlen (plaintext) <= 1) { MessageBox (NULL, _("Please select text to encrypt."), _("GpgOL"), MB_ICONINFORMATION|MB_OK); goto failure; } } else if (flags & DATA_BODY) { plaintext = get_oom_string (mailItem, "Body"); if (!plaintext || strlen (plaintext) <= 1) { MessageBox (NULL, _("Textbody empty."), _("GpgOL"), MB_ICONINFORMATION|MB_OK); goto failure; } } /* Create a temporary sink to construct the encrypted data. */ hr = OpenStreamOnFile (MAPIAllocateBuffer, MAPIFreeBuffer, (SOF_UNIQUEFILENAME | STGM_DELETEONRELEASE | STGM_CREATE | STGM_READWRITE), NULL, GpgOLStr("GPG"), &tmpstream); if (FAILED (hr)) { log_error ("%s:%s: can't create temp file: hr=%#lx\n", SRCNAME, __func__, hr); rc = -1; goto failure; } sink->cb_data = tmpstream; sink->writefnc = sink_std_write; /* Now lets prepare our encryption */ session_number = engine_new_session_number (); /* Prepare the encryption sink */ if (engine_create_filter (&filter, write_buffer_for_cb, sink)) { goto failure; } encsink->cb_data = filter; encsink->writefnc = sink_encryption_write; engine_set_session_number (filter, session_number); engine_set_session_title (filter, _("GpgOL")); senderAddr = get_pa_string (sender, PR_SMTP_ADDRESS_DASL); if (flags & OP_ENCRYPT) { recipientAddrs = get_oom_recipients (recipients); if (!recipientAddrs || !(*recipientAddrs)) { MessageBox (NULL, _("Please add at least one recipent."), _("GpgOL"), MB_ICONINFORMATION|MB_OK); goto failure; } if ((rc=engine_encrypt_prepare (filter, curWindow, PROTOCOL_UNKNOWN, (flags & OP_SIGN) ? ENGINE_FLAG_SIGN_FOLLOWS : 0, senderAddr, recipientAddrs, &protocol))) { log_error ("%s:%s: engine encrypt prepare failed : %s", SRCNAME, __func__, gpg_strerror (rc)); goto failure; } if ((rc=engine_encrypt_start (filter, 0))) { log_error ("%s:%s: engine encrypt start failed: %s", SRCNAME, __func__, gpg_strerror (rc)); goto failure; } } else { /* We could do some kind of clearsign / sign text as attachment here but it is error prone */ if ((rc=engine_sign_opaque_start (filter, curWindow, PROTOCOL_UNKNOWN, senderAddr, &protocol))) { log_error ("%s:%s: engine sign start failed: %s", SRCNAME, __func__, gpg_strerror (rc)); goto failure; } } /* Write the text in the encryption sink. */ rc = write_buffer (encsink, plaintext, strlen (plaintext)); if (rc) { log_error ("%s:%s: writing tmpstream to encsink failed: %s", SRCNAME, __func__, gpg_strerror (rc)); goto failure; } /* Flush the encryption sink and wait for the encryption to get ready. */ if ((rc = write_buffer (encsink, NULL, 0))) goto failure; if ((rc = engine_wait (filter))) goto failure; filter = NULL; /* Not valid anymore. */ encsink->cb_data = NULL; /* Not needed anymore. */ if (!sink->enc_counter) { log_debug ("%s:%s: nothing received from engine", SRCNAME, __func__); goto failure; } /* Check the size of the encrypted data */ tmpstream->Stat (&tmpStat, 0); if (tmpStat.cbSize.QuadPart > UINT_MAX) { log_error ("%s:%s: No one should write so large mails.", SRCNAME, __func__); goto failure; } /* Copy the encrypted stream to the message editor. */ { LARGE_INTEGER off; ULONG nread; char buffer[(unsigned int)tmpStat.cbSize.QuadPart + 1]; memset (buffer, 0, sizeof buffer); off.QuadPart = 0; hr = tmpstream->Seek (off, STREAM_SEEK_SET, NULL); if (hr) { log_error ("%s:%s: seeking back to the begin failed: hr=%#lx", SRCNAME, __func__, hr); rc = gpg_error (GPG_ERR_EIO); goto failure; } hr = tmpstream->Read (buffer, sizeof (buffer) - 1, &nread); if (hr) { log_error ("%s:%s: IStream::Read failed: hr=%#lx", SRCNAME, __func__, hr); rc = gpg_error (GPG_ERR_EIO); goto failure; } if (strlen (buffer) > 1) { if (flags & OP_SIGN) { /* When signing we append the signature after the body */ unsigned int combinedSize = strlen (buffer) + strlen (plaintext) + 5; char combinedBody[combinedSize]; memset (combinedBody, 0, combinedSize); snprintf (combinedBody, combinedSize, "%s\r\n\r\n%s", plaintext, buffer); if (flags & DATA_SELECTION) put_oom_string (selection, "Text", combinedBody); else if (flags & DATA_BODY) put_oom_string (mailItem, "Body", combinedBody); } else if (protocol == PROTOCOL_SMIME) { unsigned int enclosedSize = strlen (buffer) + 34 + 31 + 1; char enclosedData[enclosedSize]; snprintf (enclosedData, sizeof enclosedData, "-----BEGIN ENCRYPTED MESSAGE-----\r\n" "%s" "-----END ENCRYPTED MESSAGE-----\r\n", buffer); if (flags & DATA_SELECTION) put_oom_string (selection, "Text", enclosedData); else if (flags & DATA_BODY) put_oom_string (mailItem, "Body", enclosedData); } else { if (flags & DATA_SELECTION) put_oom_string (selection, "Text", buffer); else if (flags & DATA_BODY) { put_oom_string (mailItem, "Body", buffer); } } } else { /* Just to be save not to overwrite the selection with an empty buffer */ log_error ("%s:%s: unexpected problem ", SRCNAME, __func__); goto failure; } } failure: if (rc) log_debug ("%s:%s: failed rc=%d (%s) <%s>", SRCNAME, __func__, rc, gpg_strerror (rc), gpg_strsource (rc)); engine_cancel (filter); gpgol_release(wordEditor); gpgol_release(application); gpgol_release(selection); gpgol_release(sender); gpgol_release(recipients); gpgol_release(mailItem); gpgol_release(tmpstream); xfree (plaintext); xfree (senderAddr); if (recipientAddrs) { for (i=0; recipientAddrs && recipientAddrs[i]; i++) xfree (recipientAddrs[i]); xfree (recipientAddrs); } log_debug ("%s:%s: leave", SRCNAME, __func__); return S_OK; } HRESULT decryptAttachments (LPDISPATCH ctrl) { LPDISPATCH context = NULL; LPDISPATCH attachmentSelection; int attachmentCount; HRESULT hr = 0; int i = 0; HWND curWindow; int err; hr = getContext(ctrl, &context); attachmentSelection = get_oom_object (context, "AttachmentSelection"); if (!attachmentSelection) { /* We can be called from a context menu, in that case we directly have an AttachmentSelection context. Otherwise we have an Explorer context with an Attachment Selection property. */ attachmentSelection = context; } attachmentCount = get_oom_int (attachmentSelection, "Count"); curWindow = get_oom_context_window (context); { char *filenames[attachmentCount + 1]; filenames[attachmentCount] = NULL; /* Yes the items start at 1! */ for (i = 1; i <= attachmentCount; i++) { char buf[16]; char *filename; wchar_t *wcsOutFilename; DISPPARAMS saveParams; VARIANT aVariant[1]; LPDISPATCH attachmentObj; DISPID saveID; snprintf (buf, sizeof (buf), "Item(%i)", i); attachmentObj = get_oom_object (attachmentSelection, buf); if (!attachmentObj) { /* Should be impossible */ filenames[i-1] = NULL; log_error ("%s:%s: could not find Item %i;", SRCNAME, __func__, i); break; } filename = get_oom_string (attachmentObj, "FileName"); saveID = lookup_oom_dispid (attachmentObj, "SaveAsFile"); saveParams.rgvarg = aVariant; saveParams.rgvarg[0].vt = VT_BSTR; filenames[i-1] = get_save_filename (NULL, filename); xfree (filename); if (!filenames [i-1]) continue; wcsOutFilename = utf8_to_wchar2 (filenames[i-1], strlen(filenames[i-1])); saveParams.rgvarg[0].bstrVal = SysAllocString (wcsOutFilename); saveParams.cArgs = 1; saveParams.cNamedArgs = 0; hr = attachmentObj->Invoke (saveID, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, &saveParams, NULL, NULL, NULL); SysFreeString (saveParams.rgvarg[0].bstrVal); gpgol_release (attachmentObj); if (FAILED(hr)) { int j; log_debug ("%s:%s: Saving to file failed. hr: %x", SRCNAME, __func__, (unsigned int) hr); for (j = 0; j < i; j++) xfree (filenames[j]); gpgol_release (attachmentSelection); return hr; } } gpgol_release (attachmentSelection); err = op_assuan_start_decrypt_files (curWindow, filenames); for (i = 0; i < attachmentCount; i++) xfree (filenames[i]); } log_debug ("%s:%s: Leaving. Err: %i", SRCNAME, __func__, err); return S_OK; /* If we return an error outlook will show that our callback function failed in an ugly window. */ } /* MIME erify mail helper. Returns 0 if it was not called with a MIME crypto message or on error. */ static int verify_mime (LPDISPATCH mailitem) { int ret = 0; LPMESSAGE message = get_oom_base_message (mailitem); if (!message) { log_error ("%s:%s: Failed to get the base message", SRCNAME, __func__); return 0; } ret = message_incoming_handler (message, NULL, true /*force */); gpgol_release (message); return ret; } /* do_reader_action decrypts the content of an inspector. Controled by flags similary to the do_composer_action. */ HRESULT do_reader_action (LPDISPATCH ctrl, int flags) { LPDISPATCH context = NULL; LPDISPATCH selection = NULL; LPDISPATCH wordEditor = NULL; LPDISPATCH mailItem = NULL; LPDISPATCH wordApplication = NULL; struct sink_s decsinkmem; sink_t decsink = &decsinkmem; struct sink_s sinkmem; sink_t sink = &sinkmem; LPSTREAM tmpstream = NULL; engine_filter_t filter = NULL; HWND curWindow; char* encData = NULL; char* senderAddr = NULL; char* subject = NULL; int encDataLen = 0; int rc = 0; unsigned int session_number; HRESULT hr; STATSTG tmpStat; protocol_t protocol; hr = getContext (ctrl, &context); if (FAILED(hr)) return hr; memset (decsink, 0, sizeof *decsink); memset (sink, 0, sizeof *sink); curWindow = get_oom_context_window (context); if (!(flags & DATA_BODY)) { wordEditor = get_oom_object (context, "WordEditor"); wordApplication = get_oom_object (wordEditor, "get_Application"); selection = get_oom_object (wordApplication, "get_Selection"); } mailItem = get_oom_object (context, "CurrentItem"); if ((!wordEditor || !wordApplication || !selection || !mailItem) && !(flags & DATA_BODY)) { MessageBox (NULL, "Internal error in GpgOL.\n" "Could not find all objects.", _("GpgOL"), MB_ICONINFORMATION|MB_OK); log_error ("%s:%s: Could not find all objects.", SRCNAME, __func__); goto failure; } if (!mailItem) { /* This happens when we try to decrypt the body of a mail in the explorer context. */ mailItem = get_oom_object (context, "Selection.Item(1)"); if (!mailItem) { MessageBox (NULL, _("Please select a Mail."), _("GpgOL"), MB_ICONINFORMATION|MB_OK); goto failure; } } if (flags & DATA_SELECTION) { encData = get_oom_string (selection, "Text"); if (!encData || (encDataLen = strlen (encData)) <= 1) { MessageBox (NULL, _("Please select the data you wish to decrypt."), _("GpgOL"), MB_ICONINFORMATION|MB_OK); goto failure; } } else if (flags & DATA_BODY) { encData = get_oom_string (mailItem, "Body"); if (!encData || (encDataLen = strlen (encData)) <= 1) { MessageBox (NULL, _("Nothing to decrypt."), _("GpgOL"), MB_ICONINFORMATION|MB_OK); goto failure; } } fix_linebreaks (encData, &encDataLen); /* We check if the data we work on was opaque signed. This is true for signed stuff created by ribbon-callbacks and not a decent MIME implementation. So in that case we don't use verify_mime */ if (!strstr (encData, OPAQUE_SIGNED_MARKER) && verify_mime (mailItem)) { log_debug ("%s:%s: This was a mime message.", SRCNAME, __func__); if (flags & OP_DECRYPT) { MessageBox (NULL, "This message is in MIME format. Due to technical restrictions " "it can only be decrypted once per session. To decrypt it again " "please restart Outlook and open the message.", _("GpgOL"), MB_ICONINFORMATION|MB_OK); } goto failure; } subject = get_oom_string (mailItem, "Subject"); if (get_oom_bool (mailItem, "Sent")) { char *addrType = get_oom_string (mailItem, "SenderEmailType"); if (addrType && strcmp("SMTP", addrType) == 0) { senderAddr = get_oom_string (mailItem, "SenderEmailAddress"); } else { /* Not SMTP, fall back to try getting the property. */ LPDISPATCH sender = get_oom_object (mailItem, "Sender"); senderAddr = get_pa_string (sender, PR_SMTP_ADDRESS_DASL); gpgol_release (sender); } xfree (addrType); } else { /* If the message has not been sent we might be composing in this case use the current address */ LPDISPATCH sender = get_oom_object (mailItem, "Session.CurrentUser"); senderAddr = get_pa_string (sender, PR_SMTP_ADDRESS_DASL); gpgol_release (sender); } /* Determine the protocol based on the content */ protocol = is_cms_data (encData, encDataLen) ? PROTOCOL_SMIME : PROTOCOL_OPENPGP; hr = OpenStreamOnFile (MAPIAllocateBuffer, MAPIFreeBuffer, (SOF_UNIQUEFILENAME | STGM_DELETEONRELEASE | STGM_CREATE | STGM_READWRITE), NULL, GpgOLStr("GPG"), &tmpstream); if (FAILED (hr)) { log_error ("%s:%s: can't create temp file: hr=%#lx\n", SRCNAME, __func__, hr); rc = -1; goto failure; } sink->cb_data = tmpstream; sink->writefnc = sink_std_write; session_number = engine_new_session_number (); if (engine_create_filter (&filter, write_buffer_for_cb, sink)) goto failure; decsink->cb_data = filter; decsink->writefnc = sink_encryption_write; engine_set_session_number (filter, session_number); engine_set_session_title (filter, subject ? subject : _("GpgOL")); if (flags & OP_DECRYPT) { if ((rc=engine_decrypt_start (filter, curWindow, protocol, 1, NULL))) { log_error ("%s:%s: engine decrypt start failed: %s", SRCNAME, __func__, gpg_strerror (rc)); goto failure; } } else if (flags & OP_VERIFY) { log_debug ("Starting verify"); if ((rc=engine_verify_start (filter, curWindow, NULL, 0, protocol, senderAddr))) { log_error ("%s:%s: engine verify start failed: %s", SRCNAME, __func__, gpg_strerror (rc)); goto failure; } } /* Write the text in the decryption sink. */ rc = write_buffer (decsink, encData, encDataLen); /* Flush the decryption sink and wait for the decryption to get ready. */ if ((rc = write_buffer (decsink, NULL, 0))) goto failure; if ((rc = engine_wait (filter))) goto failure; filter = NULL; /* Not valid anymore. */ decsink->cb_data = NULL; /* Not needed anymore. */ if (!sink->enc_counter) { log_debug ("%s:%s: nothing received from engine", SRCNAME, __func__); goto failure; } /* Check the size of the decrypted data */ tmpstream->Stat (&tmpStat, 0); if (tmpStat.cbSize.QuadPart > UINT_MAX) { log_error ("%s:%s: No one should write so large mails.", SRCNAME, __func__); goto failure; } /* Copy the decrypted stream to the message editor. */ { LARGE_INTEGER off; ULONG nread; char buffer[(unsigned int)tmpStat.cbSize.QuadPart + 1]; memset (buffer, 0, sizeof buffer); off.QuadPart = 0; hr = tmpstream->Seek (off, STREAM_SEEK_SET, NULL); if (hr) { log_error ("%s:%s: seeking back to the begin failed: hr=%#lx", SRCNAME, __func__, hr); rc = gpg_error (GPG_ERR_EIO); goto failure; } hr = tmpstream->Read (buffer, sizeof (buffer) - 1, &nread); if (hr) { log_error ("%s:%s: IStream::Read failed: hr=%#lx", SRCNAME, __func__, hr); rc = gpg_error (GPG_ERR_EIO); goto failure; } if (strlen (buffer) > 1) { /* Now replace the crypto data with the decrypted data or show it somehow.*/ int err = 0; if (flags & DATA_SELECTION) { err = put_oom_string (selection, "Text", buffer); } else if (flags & DATA_BODY) { err = put_oom_string (mailItem, "Body", buffer); } if (err) { MessageBox (NULL, buffer, flags & OP_DECRYPT ? _("Plain text") : _("Signed text"), MB_ICONINFORMATION|MB_OK); } } else { /* Just to be save not to overwrite the selection with an empty buffer */ log_error ("%s:%s: unexpected problem ", SRCNAME, __func__); goto failure; } } failure: if (rc) log_debug ("%s:%s: failed rc=%d (%s) <%s>", SRCNAME, __func__, rc, gpg_strerror (rc), gpg_strsource (rc)); engine_cancel (filter); gpgol_release (mailItem); gpgol_release (selection); gpgol_release (wordEditor); gpgol_release (wordApplication); xfree (encData); xfree (senderAddr); xfree (subject); if (tmpstream) gpgol_release (tmpstream); return S_OK; } /* getIcon Loads a PNG image from the resurce converts it into a Bitmap and Wraps it in an PictureDispatcher that is returned as result. Based on documentation from: http://www.codeproject.com/Articles/3537/Loading-JPG-PNG-resources-using-GDI */ HRESULT getIcon (int id, VARIANT* result) { PICTDESC pdesc; LPDISPATCH pPict; HRESULT hr; Gdiplus::GdiplusStartupInput gdiplusStartupInput; Gdiplus::Bitmap* pbitmap; ULONG_PTR gdiplusToken; HRSRC hResource; DWORD imageSize; const void* pResourceData; HGLOBAL hBuffer; memset (&pdesc, 0, sizeof pdesc); pdesc.cbSizeofstruct = sizeof pdesc; pdesc.picType = PICTYPE_BITMAP; if (!result) { log_error ("getIcon called without result variant."); return E_POINTER; } /* Initialize GDI */ gdiplusStartupInput.DebugEventCallback = NULL; gdiplusStartupInput.SuppressBackgroundThread = FALSE; gdiplusStartupInput.SuppressExternalCodecs = FALSE; gdiplusStartupInput.GdiplusVersion = 1; GdiplusStartup (&gdiplusToken, &gdiplusStartupInput, NULL); /* Get the image from the resource file */ hResource = FindResource (glob_hinst, MAKEINTRESOURCE(id), RT_RCDATA); if (!hResource) { log_error ("%s:%s: failed to find image: %i", SRCNAME, __func__, id); return E_FAIL; } imageSize = SizeofResource (glob_hinst, hResource); if (!imageSize) return E_FAIL; pResourceData = LockResource (LoadResource(glob_hinst, hResource)); if (!pResourceData) { log_error ("%s:%s: failed to load image: %i", SRCNAME, __func__, id); return E_FAIL; } hBuffer = GlobalAlloc (GMEM_MOVEABLE, imageSize); if (hBuffer) { void* pBuffer = GlobalLock (hBuffer); if (pBuffer) { IStream* pStream = NULL; CopyMemory (pBuffer, pResourceData, imageSize); if (CreateStreamOnHGlobal (hBuffer, FALSE, &pStream) == S_OK) { pbitmap = Gdiplus::Bitmap::FromStream (pStream); gpgol_release (pStream); if (!pbitmap || pbitmap->GetHBITMAP (0, &pdesc.bmp.hbitmap)) { log_error ("%s:%s: failed to get PNG.", SRCNAME, __func__); } } } GlobalUnlock (pBuffer); } GlobalFree (hBuffer); Gdiplus::GdiplusShutdown (gdiplusToken); /* Wrap the image into an OLE object. */ hr = OleCreatePictureIndirect (&pdesc, IID_IPictureDisp, TRUE, (void **) &pPict); if (hr != S_OK || !pPict) { log_error ("%s:%s: OleCreatePictureIndirect failed: hr=%#lx\n", SRCNAME, __func__, hr); return -1; } result->pdispVal = pPict; result->vt = VT_DISPATCH; return S_OK; } /* Adds an encrypted attachment if the flag OP_SIGN is set a detached signature of the encrypted file is also added. */ static HRESULT attachEncryptedFile (LPDISPATCH ctrl, int flags) { LPDISPATCH context = NULL; LPDISPATCH mailItem = NULL; LPDISPATCH sender = NULL; LPDISPATCH recipients = NULL; HRESULT hr; char* senderAddr = NULL; char** recipientAddrs = NULL; char* subject = NULL; HWND curWindow; char *fileToEncrypt = NULL; wchar_t *fileToEncryptW = NULL; wchar_t *encryptedFile = NULL; wchar_t *attachName = NULL; HANDLE hFile = NULL; HANDLE hEncFile = NULL; unsigned int session_number; struct sink_s encsinkmem; sink_t encsink = &encsinkmem; struct sink_s sinkmem; sink_t sink = &sinkmem; engine_filter_t filter = NULL; protocol_t protocol; int rc = 0; int i = 0; memset (encsink, 0, sizeof *encsink); memset (sink, 0, sizeof *sink); hr = getContext (ctrl, &context); if (FAILED(hr)) return hr; /* First do the check for recipients as this is likely to fail */ mailItem = get_oom_object (context, "CurrentItem"); sender = get_oom_object (mailItem, "Session.CurrentUser"); recipients = get_oom_object (mailItem, "Recipients"); recipientAddrs = get_oom_recipients (recipients); if (!recipientAddrs || !(*recipientAddrs)) { MessageBox (NULL, _("Please add at least one recipent."), _("GpgOL"), MB_ICONINFORMATION|MB_OK); goto failure; } /* Get a file handle to read from */ fileToEncrypt = get_open_filename (NULL, _("Select file to encrypt")); if (!fileToEncrypt) { log_debug ("No file selected"); goto failure; } fileToEncryptW = utf8_to_wchar2 (fileToEncrypt, strlen(fileToEncrypt)); xfree (fileToEncrypt); hFile = CreateFileW (fileToEncryptW, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { /* Should not happen as the Open File dialog should have prevented this. Maybe this also happens when a file is not readable. In that case we might want to switch to a localized error naming the file. */ MessageBox (NULL, "Internal error in GpgOL.\n" "Could not open File.", _("GpgOL"), MB_ICONERROR|MB_OK); return S_OK; } /* Now do the encryption preperations */ if (!mailItem || !sender || !recipients) { MessageBox (NULL, "Internal error in GpgOL.\n" "Could not find all objects.", _("GpgOL"), MB_ICONERROR|MB_OK); log_error ("%s:%s: Could not find all objects.", SRCNAME, __func__); goto failure; } senderAddr = get_pa_string (sender, PR_SMTP_ADDRESS_DASL); curWindow = get_oom_context_window (context); session_number = engine_new_session_number (); subject = get_oom_string (mailItem, "Subject"); /* Prepare the encryption sink */ if ((rc = engine_create_filter (&filter, write_buffer_for_cb, sink))) { goto failure; } encsink->cb_data = filter; encsink->writefnc = sink_encryption_write; engine_set_session_number (filter, session_number); engine_set_session_title (filter, subject ? subject :_("GpgOL")); if ((rc=engine_encrypt_prepare (filter, curWindow, PROTOCOL_UNKNOWN, ENGINE_FLAG_BINARY_OUTPUT, senderAddr, recipientAddrs, &protocol))) { log_error ("%s:%s: engine encrypt prepare failed : %s", SRCNAME, __func__, gpg_strerror (rc)); goto failure; } attachName = get_pretty_attachment_name (fileToEncryptW, protocol, 0); if (!attachName) { log_error ("%s:%s: Could not get a decent attachment name", SRCNAME, __func__); goto failure; } encryptedFile = get_tmp_outfile (attachName, &hEncFile); sink->cb_data = hEncFile; sink->writefnc = sink_file_write; if ((rc=engine_encrypt_start (filter, 0))) { log_error ("%s:%s: engine encrypt start failed: %s", SRCNAME, __func__, gpg_strerror (rc)); goto failure; } if ((rc=copyFileToSink (hFile, encsink))) goto failure; /* Lets hope the user did not select a huge file. We are hanging here until encryption is completed.. */ if ((rc = engine_wait (filter))) goto failure; filter = NULL; /* Not valid anymore. */ encsink->cb_data = NULL; /* Not needed anymore. */ if (!sink->enc_counter) { log_error ("%s:%s: nothing received from engine", SRCNAME, __func__); goto failure; } /* Now we have an encrypted file behind encryptedFile. Let's add it */ add_oom_attachment (mailItem, encryptedFile, nullptr); if (flags & OP_SIGN) { attachSignature (mailItem, subject, hEncFile, protocol, session_number, curWindow, encryptedFile, senderAddr); } failure: if (filter) engine_cancel (filter); if (hEncFile) { CloseHandle (hEncFile); DeleteFileW (encryptedFile); } xfree (senderAddr); xfree (encryptedFile); xfree (fileToEncryptW); xfree (attachName); xfree (subject); gpgol_release (mailItem); gpgol_release (sender); gpgol_release (recipients); if (hFile) CloseHandle (hFile); if (recipientAddrs) { for (i=0; recipientAddrs && recipientAddrs[i]; i++) xfree (recipientAddrs[i]); xfree (recipientAddrs); } return S_OK; } HRESULT startCertManager (LPDISPATCH ctrl) { HRESULT hr; LPDISPATCH context; HWND curWindow; hr = getContext (ctrl, &context); if (FAILED(hr)) return hr; curWindow = get_oom_context_window (context); engine_start_keymanager (curWindow); return S_OK; } HRESULT decryptBody (LPDISPATCH ctrl) { return do_reader_action (ctrl, OP_DECRYPT | DATA_BODY); } HRESULT decryptSelection (LPDISPATCH ctrl) { return do_reader_action (ctrl, OP_DECRYPT | DATA_SELECTION); } HRESULT encryptBody (LPDISPATCH ctrl) { return do_composer_action (ctrl, OP_ENCRYPT | DATA_BODY); } HRESULT encryptSelection (LPDISPATCH ctrl) { return do_composer_action (ctrl, OP_ENCRYPT | DATA_SELECTION); } HRESULT addEncSignedAttachment (LPDISPATCH ctrl) { return attachEncryptedFile (ctrl, OP_SIGN); } HRESULT addEncAttachment (LPDISPATCH ctrl) { return attachEncryptedFile (ctrl, 0); } HRESULT signBody (LPDISPATCH ctrl) { return do_composer_action (ctrl, DATA_BODY | OP_SIGN); } HRESULT verifyBody (LPDISPATCH ctrl) { return do_reader_action (ctrl, DATA_BODY | OP_VERIFY); } HRESULT mark_mime_action (LPDISPATCH ctrl, int flags, bool is_explorer) { HRESULT hr; HRESULT rc = E_FAIL; LPDISPATCH context = NULL, mailitem = NULL; LPMESSAGE message = NULL; int oldflags, newflags; log_debug ("%s:%s: enter", SRCNAME, __func__); hr = getContext (ctrl, &context); if (FAILED(hr)) return hr; mailitem = get_oom_object (context, is_explorer ? "ActiveInlineResponse" : "CurrentItem"); if (!mailitem) { log_error ("%s:%s: Failed to get mailitem.", SRCNAME, __func__); goto done; } message = get_oom_base_message (mailitem); if (!message) { log_error ("%s:%s: Failed to get message.", SRCNAME, __func__); goto done; } oldflags = get_gpgol_draft_info_flags (message); newflags = oldflags xor flags; if (set_gpgol_draft_info_flags (message, newflags)) { log_error ("%s:%s: Failed to set draft flags.", SRCNAME, __func__); } rc = S_OK; /* We need to invalidate the UI to update the toggle states of the subbuttons and the top button. Yeah, we invalidate a lot *sigh* */ gpgoladdin_invalidate_ui (); done: gpgol_release (context); gpgol_release (mailitem); gpgol_release (message); return rc; } /* Get the state of encrypt / sign toggle buttons. flag values: 1 get the state of the encrypt button. 2 get the state of the sign button. If is_explorer is set to true we look at the inline response. */ HRESULT get_crypt_pressed (LPDISPATCH ctrl, int flags, VARIANT *result, bool is_explorer) { HRESULT hr; bool value; LPDISPATCH context = NULL, mailitem = NULL; LPMESSAGE message = NULL; result->vt = VT_BOOL | VT_BYREF; result->pboolVal = (VARIANT_BOOL*) xmalloc (sizeof (VARIANT_BOOL)); *(result->pboolVal) = VARIANT_FALSE; /* First the usual defensive check about our parameters */ if (!ctrl || !result) { log_error ("%s:%s:%i", SRCNAME, __func__, __LINE__); return E_FAIL; } hr = getContext (ctrl, &context); if (hr) { log_error ("%s:%s:%i : hresult %lx", SRCNAME, __func__, __LINE__, hr); return E_FAIL; } mailitem = get_oom_object (context, is_explorer ? "ActiveInlineResponse" : "CurrentItem"); if (!mailitem) { log_error ("%s:%s: Failed to get mailitem.", SRCNAME, __func__); goto done; } message = get_oom_base_message (mailitem); if (!message) { log_error ("%s:%s: No message found.", SRCNAME, __func__); goto done; } value = (get_gpgol_draft_info_flags (message) & flags) == flags; *(result->pboolVal) = value ? VARIANT_TRUE : VARIANT_FALSE; done: gpgol_release (context); gpgol_release (mailitem); gpgol_release (message); return S_OK; } static Mail * get_mail_from_control (LPDISPATCH ctrl, bool *none_selected) { HRESULT hr; LPDISPATCH context = NULL, mailitem = NULL; *none_selected = false; if (!ctrl) { log_error ("%s:%s:%i", SRCNAME, __func__, __LINE__); return NULL; } hr = getContext (ctrl, &context); if (hr) { log_error ("%s:%s:%i : hresult %lx", SRCNAME, __func__, __LINE__, hr); return NULL; } const auto ctx_name = std::string (get_object_name (context)); if (ctx_name.empty()) { log_error ("%s:%s: Failed to get context name", SRCNAME, __func__); gpgol_release (context); return NULL; } if (!strcmp (ctx_name.c_str(), "_Inspector")) { mailitem = get_oom_object (context, "CurrentItem"); } else if (!strcmp (ctx_name.c_str(), "_Explorer")) { LPDISPATCH selection = get_oom_object (context, "Selection"); if (!selection) { log_error ("%s:%s: Failed to get selection.", SRCNAME, __func__); gpgol_release (context); return NULL; } int count = get_oom_int (selection, "Count"); if (count == 1) { // If we call this on a selection with more items // Outlook sends an ItemLoad event for each mail // in that selection. mailitem = get_oom_object (selection, "Item(1)"); } gpgol_release (selection); if (!mailitem) { *none_selected = true; } else if (g_ol_version_major >= 16) { // Avoid showing wrong crypto state if we don't have a reading // pane. In that case the parser will finish for a mail which is gone // and the crypto state will not get updated. // // // Somehow latest Outlook 2016 crashes when accessing the current view // of the Explorer. This is even reproducible with // GpgOL disabled and only with Outlook Spy active. If you select // the explorer of an Outlook.com resource and then access // the CurrentView and close the CurrentView again in Outlook Spy // outlook crashes. LPDISPATCH prevEdit = get_oom_object (context, "PreviewPane.WordEditor"); gpgol_release (prevEdit); if (!prevEdit) { *none_selected = true; gpgol_release (mailitem); mailitem = nullptr; } } else { // Preview Pane is not available in older outlooks LPDISPATCH tableView = get_oom_object (context, "CurrentView"); if (!tableView) { // Woops, should not happen. TRACEPOINT; *none_selected = true; gpgol_release (mailitem); mailitem = nullptr; } else { int hasReadingPane = get_oom_bool (tableView, "ShowReadingPane"); gpgol_release (tableView); if (!hasReadingPane) { *none_selected = true; gpgol_release (mailitem); mailitem = nullptr; } } } } gpgol_release (context); if (!mailitem) { log_debug ("%s:%s: No mailitem. From %s", SRCNAME, __func__, ctx_name.c_str()); return NULL; } char *uid; /* Get the uid of this item. */ uid = get_unique_id (mailitem, 0, nullptr); if (!uid) { LPMESSAGE msg = get_oom_base_message (mailitem); uid = mapi_get_uid (msg); gpgol_release (msg); if (!uid) { log_debug ("%s:%s: Failed to get uid for %p", SRCNAME, __func__, mailitem); gpgol_release (mailitem); return NULL; } } auto ret = Mail::get_mail_for_uuid (uid); xfree (uid); if (!ret) { log_error ("%s:%s: Failed to find mail %p in map.", SRCNAME, __func__, mailitem); } gpgol_release (mailitem); return ret; } /* Helper to reduce code duplication.*/ #define MY_MAIL_GETTER \ if (!ctrl) \ { \ log_error ("%s:%s:%i", SRCNAME, __func__, __LINE__); \ return E_FAIL; \ } \ bool none_selected; \ const auto mail = get_mail_from_control (ctrl, &none_selected); \ (void)none_selected; \ if (!mail) \ { \ log_oom ("%s:%s:%i Failed to get mail", \ SRCNAME, __func__, __LINE__); \ } HRESULT get_is_details_enabled (LPDISPATCH ctrl, VARIANT *result) { MY_MAIL_GETTER result->vt = VT_BOOL | VT_BYREF; result->pboolVal = (VARIANT_BOOL*) xmalloc (sizeof (VARIANT_BOOL)); *(result->pboolVal) = none_selected ? VARIANT_FALSE : VARIANT_TRUE; return S_OK; } HRESULT get_sig_label (LPDISPATCH ctrl, VARIANT *result) { MY_MAIL_GETTER result->vt = VT_BSTR; wchar_t *w_result; if (!mail) { log_debug ("%s:%s: No mail.", SRCNAME, __func__); w_result = utf8_to_wchar (_("Insecure")); result->bstrVal = SysAllocString (w_result); xfree (w_result); return S_OK; } w_result = utf8_to_wchar (mail->get_crypto_summary ().c_str ()); result->bstrVal = SysAllocString (w_result); xfree (w_result); return S_OK; } HRESULT get_sig_ttip (LPDISPATCH ctrl, VARIANT *result) { MY_MAIL_GETTER result->vt = VT_BSTR; wchar_t *w_result; if (mail) { w_result = utf8_to_wchar (mail->get_crypto_one_line().c_str()); } else if (!none_selected) { w_result = utf8_to_wchar (_("Insecure message")); } else { w_result = utf8_to_wchar (_("No message selected")); } result->bstrVal = SysAllocString (w_result); xfree (w_result); return S_OK; } HRESULT get_sig_stip (LPDISPATCH ctrl, VARIANT *result) { MY_MAIL_GETTER result->vt = VT_BSTR; if (none_selected) { result->bstrVal = SysAllocString (L""); return S_OK; } - if (!mail || (!mail->is_signed () && !mail->is_encrypted ())) + if (!mail || !mail->is_crypto_mail ()) { wchar_t *w_result; w_result = utf8_to_wchar (utf8_gettext ("You cannot be sure who sent, " "modified and read the message in transit.\n\n" "Click here to learn more.")); result->bstrVal = SysAllocString (w_result); xfree (w_result); return S_OK; } const auto message = mail->get_crypto_details (); wchar_t *w_message = utf8_to_wchar (message.c_str()); result->bstrVal = SysAllocString (w_message); xfree (w_message); return S_OK; } HRESULT launch_cert_details (LPDISPATCH ctrl) { MY_MAIL_GETTER if (!mail || (!mail->is_signed () && !mail->is_encrypted ())) { ShellExecuteA(NULL, NULL, "https://emailselfdefense.fsf.org/infographic", 0, 0, SW_SHOWNORMAL); return S_OK; } if (!mail->is_signed () && mail->is_encrypted ()) { /* Encrypt only, no information but show something. because we want the button to be active. Aheinecke: I don't think we should show to which keys the message is encrypted here. This would confuse users if they see keyids of unknown keys and the information can't be "true" because the sender could have sent the same information to other people or used throw keyids etc. */ char * buf; gpgrt_asprintf (&buf, _("The message was not cryptographically signed.\n" "There is no additional information available if it " "was actually sent by '%s' or if someone faked the sender address."), mail->get_sender ().c_str()); MessageBox (NULL, buf, _("GpgOL"), MB_ICONINFORMATION|MB_OK); xfree (buf); return S_OK; } char *uiserver = get_uiserver_name (); bool showError = false; if (uiserver) { std::string path (uiserver); xfree (uiserver); if (path.find("kleopatra.exe") != std::string::npos) { size_t dpos; if ((dpos = path.find(" --daemon")) != std::string::npos) { path.erase(dpos, strlen(" --daemon")); } auto ctx = Context::createForEngine(SpawnEngine); if (!ctx) { log_error ("%s:%s: No spawn engine.", SRCNAME, __func__); } const char *argv[] = {path.c_str(), "--query", mail->get_sig_fpr(), NULL }; log_debug ("%s:%s: Starting %s %s %s", SRCNAME, __func__, path.c_str(), argv[1], argv[2]); Data d(Data::null); ctx->spawnAsync(path.c_str(), argv, d, d, d, Context::SpawnNone); } else { showError = true; } } else { showError = true; } if (showError) { MessageBox (NULL, _("Could not find Kleopatra.\n" "Please reinstall Gpg4win with the Kleopatra component enabled."), _("GpgOL"), MB_ICONINFORMATION|MB_OK); } return S_OK; } HRESULT get_crypto_icon (LPDISPATCH ctrl, VARIANT *result) { MY_MAIL_GETTER if (mail) { return getIcon (mail->get_crypto_icon_id (), result); } return getIcon (IDI_LEVEL_0, result); }