diff --git a/src/mail.cpp b/src/mail.cpp index 25d851a..b294f16 100644 --- a/src/mail.cpp +++ b/src/mail.cpp @@ -1,1595 +1,1601 @@ /* @file mail.h * @brief High level class to work with Outlook Mailitems. * * Copyright (C) 2015, 2016 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 #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; #define COPYBUFSIZE (8 * 1024) /* Our own basic trust level for tofu. GnuPG's can't be trusted. See comment in get_valid_sig why.*/ #define GPGOL_BASIC_TOFU_TRUST 10 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_moss_position(0), m_sender(NULL), 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); } } xfree (m_sender); 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; } const auto item_str = std::string("Item(") + std::to_string(pos) + ")"; int count = get_oom_int (attachments, "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; } /** 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; } /** 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); } 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 (!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) { if (put_oom_string (m_mailitem, "HTMLBody", placeholder_buf)) { log_error ("%s:%s: Failed to modify html body of item.", SRCNAME, __func__); } } else { 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)); 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 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; } const auto html = m_parser->get_html_body(); 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; } const auto body = m_parser->get_body(); 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 (); update_sigstate (); m_needs_wipe = true; TRACEPOINT; /* Set categories according to the result. */ update_categories(); TRACEPOINT; /* Update the body */ update_body(); TRACEPOINT; /* 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); if (flags == 3) { log_debug ("%s:%s: Sign / Encrypting message", SRCNAME, __func__); err = message_sign_encrypt (message, proto, NULL, get_sender (), this); } else if (flags == 2) { err = message_sign (message, proto, NULL, get_sender (), this); } else if (flags == 1) { err = message_encrypt (message, proto, NULL, get_sender (), this); } else { log_debug ("%s:%s: Unknown flags for crypto: %i", SRCNAME, __func__, flags); } log_debug ("%s:%s: Status: %i", SRCNAME, __func__, err); 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 () { if (!m_needs_wipe) { 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_sender () { LPDISPATCH sender = NULL; /* For some reason outlook my store the recipient address in the send using account field. If we have SMTP we prefer the SenderEmailAddress string. */ char *type = get_oom_string (m_mailitem, "SenderEmailType"); if (type && !strcmp ("SMTP", type)) { xfree (type); char *senderMail = get_oom_string (m_mailitem, "SenderEmailAddress"); if (senderMail) { xfree (m_sender); m_sender = senderMail; return 0; } } xfree (type); sender = get_oom_object (m_mailitem, "SendUsingAccount"); xfree (m_sender); m_sender = NULL; if (sender) { m_sender = get_oom_string (sender, "SmtpAddress"); gpgol_release (sender); return 0; } /* Fallback to Sender object */ sender = get_oom_object (m_mailitem, "Sender"); if (sender) { m_sender = get_pa_string (sender, PR_SMTP_ADDRESS_DASL); 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) { m_sender = get_pa_string (sender, PR_SMTP_ADDRESS_DASL); gpgol_release (sender); return 0; } log_error ("%s:%s: All fallbacks failed.", SRCNAME, __func__); return -1; } const char * Mail::get_sender () { if (!m_sender) { update_sender(); } return m_sender; } int Mail::close_all_mails () { int err = 0; std::map::iterator it; TRACEPOINT; - for (it = g_mail_map.begin(); it != g_mail_map.end(); ++it) + 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 (it->second->close ()) + if (close_inspector (it->second) || close (it->second)) { log_error ("Failed to close mail: %p ", it->first); /* Should not happen */ - if (it->second->revert()) + 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; } return get_oom_recipients (recipients); } int -Mail::close_inspector () +Mail::close_inspector (Mail *mail) { - LPDISPATCH inspector = get_oom_object (m_mailitem, "GetInspector"); + 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::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__, this); - int rc = invoke_oom_method_with_parms (m_mailitem, "Close", - NULL, &dispparams); - - /* Reset the uuid after discarding all changes in the oom - so that we can still find ourself. */ - set_uuid (); + SRCNAME, __func__, mail->item()); + mail->set_close_triggered (true); + int rc = invoke_oom_method_with_parms (mail->item(), "Close", + NULL, &dispparams); - if (!rc) - { - /* Now that we have closed it with discard changes we no - longer need to wipe the mail because the plaintext was - discarded. */ - m_needs_wipe = false; - } + 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()) { 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 () { const char *sender = get_sender(); if (!sender) { 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; if (sig.validity() != Signature::Validity::Marginal && sig.validity() != Signature::Validity::Full && sig.validity() != Signature::Validity::Ultimate) { /* For our category we only care about trusted sigs. */ continue; } const auto uid = get_uid_for_sender (sig.key(), sender); if (sig.validity() == Signature::Validity::Marginal) { const auto tofu = uid.tofuInfo(); if (tofu.isNull() || (tofu.validity() != TofuInfo::Validity::LittleHistory && tofu.validity() != TofuInfo::Validity::BasicHistory && tofu.validity() != TofuInfo::Validity::LargeHistory)) { /* Marginal is not good enough without tofu. We also wait for basic trust. */ log_debug ("%s:%s: Discarding marginal signature.", SRCNAME, __func__); continue; } /* GnuPG uses the encrypt count to determine validity. This does not make sense for us. E.g. Drafts may have been encrypted and encryption is no communication so it does not track communication history or consistency. So basically "our" tofu validity is that more then 10 messages have been exchanged. Which was the original code in GnuPG */ if (!tofu.isNull() && tofu.signCount() <= GPGOL_BASIC_TOFU_TRUST) { log_debug ("%s:%s: Tofu signcount too small.", SRCNAME, __func__); continue; } } log_debug ("%s:%s: Classified sender as verified", SRCNAME, __func__); m_sig = sig; m_uid = uid; 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; } const std::pair Mail::get_valid_sig () { std::pair ret; if (!m_is_valid) { return ret; } return std::pair (m_sig, m_uid); } bool Mail::is_valid_sig () { return m_is_valid; } void Mail::remove_categories () { const char *decCategory = _("GpgOL: Encrypted Message"); const char *verifyCategory = _("GpgOL: Verified Sender"); remove_category (m_mailitem, decCategory); remove_category (m_mailitem, verifyCategory); } void Mail::update_categories () { const char *decCategory = _("GpgOL: Encrypted Message"); const char *verifyCategory = _("GpgOL: Verified Sender"); if (m_decrypt_result.numRecipients()) { /* We use the number of recipients as we don't care if decryption was successful or not for this category */ add_category (m_mailitem, decCategory); } else { /* As a small safeguard against fakes we remove our categories */ remove_category (m_mailitem, decCategory); } if (is_valid_sig()) { add_category (m_mailitem, verifyCategory); } else { remove_category (m_mailitem, verifyCategory); } return; } bool Mail::is_signed() { return m_verify_result.numSignatures() > 0; } 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; } std::string Mail::get_signature_status() { std::string message; if (!is_signed()) { message =_("This message is not signed.\n"); message += _("You cannot be sure who wrote the message."); return message; } const auto pair = get_valid_sig (); bool keyFound = false; char *buf; bool isOpenPGP = pair.first.key().protocol() == Protocol::OpenPGP; if (!pair.first.isNull () && !pair.second.isNull ()) { const auto sig = pair.first; const auto uid = pair.second; /* We are valid */ keyFound = true; gpgrt_asprintf (&buf, _("The sender is verified because:\n\n%s %s"), isOpenPGP ? _("The used key") : _(" The used certificate"), sig.validity() == Signature::Validity::Ultimate ? _("is marked as your own.") : sig.validity() == Signature::Validity::Full && isOpenPGP ? _("was certified by enough trusted keys.") : ""); message += buf; xfree (buf); if (sig.validity() == Signature::Validity::Full && !isOpenPGP) { gpgrt_asprintf (&buf, _("is cerified by the trusted issuer:\n'%s'\n"), sig.key().issuerName()); message += buf; xfree (buf); } else if (sig.validity() == Signature::Validity::Marginal) { char *time = format_date_from_gpgme (uid.tofuInfo().signFirst()); gpgrt_asprintf (&buf, _("was consistently used for %i messages since %s."), uid.tofuInfo().signCount (), time); xfree (time); message += buf; xfree (buf); } } else { if (m_verify_result.numSignatures() > 1) { log_debug ("%s:%s: More then one signature found on %p", SRCNAME, __func__, m_mailitem); } /* We only handle the first signature. */ const auto sig = m_verify_result.signature (0); isOpenPGP = sig.key().protocol() == Protocol::OpenPGP; keyFound = !(sig.summary() & Signature::Summary::KeyMissing); log_debug ("%s:%s: Formatting sig. Validity: %x Summary: %x", SRCNAME, __func__, sig.validity(), sig.summary()); /* There is a signature but we don't accepted it as fully valid. */ message += _("The sender is not verified because:\n\n"); /* First the general stuff. */ if (sig.summary() & Signature::Summary::Red) { message += _("The signature is invalid.\n"); message += _("You cannot be sure who wrote the message."); } else if (sig.summary() & Signature::Summary::SysError || m_verify_result.numSignatures() < 1) { message += _("There was an error verifying the signature.\n"); message += _("You cannot be sure who wrote the message."); } else if (sig.summary() & Signature::Summary::SigExpired) { message += _("The signature is expired.\n"); } else { message += isOpenPGP ? _("The used key") : _("The used certificate") + std::string(" "); } const auto uid = get_uid_for_sender (sig.key(), get_sender()); /* Now the key problems */ if ((sig.summary() & Signature::Summary::KeyMissing)) { message += _("is not in your keyring."); } else if ((sig.summary() & Signature::Summary::KeyRevoked)) { message += _("is revoked."); } else if ((sig.summary() & Signature::Summary::KeyExpired)) { message += _("is expired."); } else if ((sig.summary() & Signature::Summary::BadPolicy)) { message += _("is not meant for signing."); } else if ((sig.summary() & Signature::Summary::CrlMissing)) { message += _("could not be checked for revocation."); } else if ((sig.summary() & Signature::Summary::CrlTooOld)) { message += _("could not be checked for revocation."); } else if ((sig.summary() & Signature::Summary::TofuConflict)) { message += _("conflicts with another key that was used in the past by the sender."); } else if (uid.isNull()) { gpgrt_asprintf (&buf, _("does not claim the address: \"%s\"."), get_sender()); message += buf; xfree (buf); } else if ((sig.validity() & Signature::Validity::Marginal)) { const auto tofuInfo = uid.tofuInfo(); if (tofuInfo.isNull() || !tofuInfo.signCount()) { message += _("is not certified by enough trusted keys."); } else { message += _("does not have enough history for basic trust."); } } else if ((sig.validity() & Signature::Validity::Undefined) || (sig.validity() & Signature::Validity::Unknown) || (sig.summary() == Signature::Summary::None) || (sig.validity() == 0)) { /* Bit of a catch all for weird results. */ message += _("is not certified by any trusted key."); } else if ((sig.validity() & Signature::Validity::Never)) { message += _("is explicitly marked as invalid."); } } message += "\n\n"; if (keyFound) { message += isOpenPGP ? _("Click here for details about the key.") : _("Click here for details about the key."); } 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_icon_id () const { if (!m_is_signed) { return IDI_EMBLEM_INFORMATION_64_PNG; } if (m_is_valid) { return IDI_EMBLEM_SUCCESS_64_PNG; } const auto sig = m_verify_result.signature (0); if ((sig.summary() & Signature::Summary::KeyMissing)) { return IDI_EMBLEM_QUESTION_64_PNG; } /* Maybe warning for unsigned and invalid sigs? */ return IDI_EMBLEM_INFORMATION_64_PNG; } 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; } /** Try to locate the keys for all recipients */ void Mail::locate_keys() { 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); } diff --git a/src/mail.h b/src/mail.h index b0d5c59..3ca144a 100644 --- a/src/mail.h +++ b/src/mail.h @@ -1,309 +1,316 @@ /* @file mail.h * @brief High level class to work with Outlook Mailitems. * * Copyright (C) 2015, 2016 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 . */ #ifndef MAIL_H #define MAIL_H #include "oomhelp.h" #include "mapihelp.h" #include "gpgme++/verificationresult.h" #include "gpgme++/decryptionresult.h" #include "gpgme++/key.h" #include #include class ParseController; /** @brief Data wrapper around a mailitem. * * This class is intended to bundle all that we know about * a Mail. Due to the restrictions in Outlook we sometimes may * need additional information that is not available at the time * like the sender address of an exchange account in the afterWrite * event. * * This class bundles such information and also provides a way to * access the event handler of a mail. */ class Mail { public: /** @brief Construct a mail object for the item. * * This also installs the event sink for this item. * * The mail object takes ownership of the mailitem * reference. Do not Release it! */ Mail (LPDISPATCH mailitem); ~Mail (); /** @brief looks for existing Mail objects for the OOM mailitem. @returns A reference to an existing mailitem or NULL in case none could be found. */ static Mail* get_mail_for_item (LPDISPATCH mailitem); /** @brief looks for existing Mail objects in the uuid map. Only objects for which set_uid has been called can be found in the uid map. Get the Unique ID of a mailitem thorugh get_unique_id @returns A reference to an existing mailitem or NULL in case none could be found. */ static Mail* get_mail_for_uuid (const char *uuid); /** @brief looks for existing Mail objects. @returns A reference to an existing mailitem or NULL in case none could be found. Can be used to check if a mail object was destroyed. */ static bool is_valid_ptr (const Mail *mail); /** @brief wipe the plaintext from all known Mail objects. * * This is intended as a "cleanup" call to be done on unload * to avoid leaking plaintext in case we are deactivated while * some mails still have their plaintext inserted. * * @returns the number of errors that occured. */ static int wipe_all_mails (); /** @brief revert all known Mail objects. * * Similar to wipe but works on MAPI to revert our attachment * dance and restore an original MIME mail. * * @returns the number of errors that occured. */ static int revert_all_mails (); /** @brief close all known Mail objects. * * Close our mail with discard changes set to true. * This discards the plaintext / attachments. Afterwards * it calls save if neccessary to sync back the collected * property changes. * * This is the nicest of our three "Clean plaintext" * functions. Will fallback to revert if closing fails. * Closed mails are deleted. * * @returns the number of errors that occured. */ static int close_all_mails (); /** @brief Reference to the mailitem. Do not Release! */ LPDISPATCH item () { return m_mailitem; } /** @brief Pre process the message. Ususally to be called from BeforeRead. * * This function assumes that the base message interface can be accessed * and calles the MAPI Message handling which changes the message class * to enable our own handling. * * @returns 0 on success. */ int pre_process_message (); /** @brief Decrypt / Verify the mail. * * Sets the needs_wipe and was_encrypted variable. * * @returns 0 on success. */ int decrypt_verify (); /** @brief do crypto operations as selected by the user. * * Initiates the crypto operations according to the gpgol * draft info flags. * * @returns 0 on success. */ int encrypt_sign (); /** @brief Necessary crypto operations were completed successfully. */ bool crypto_successful () { return !needs_crypto() || m_crypt_successful; } /** @brief Message should be encrypted and or signed. 0: No 1: Encrypt 2: Sign 3: Encrypt & Sign */ int needs_crypto (); /** @brief wipe the plaintext from the message and encrypt attachments. * * @returns 0 on success; */ int wipe (); /** @brief revert the message to the original mail before our changes. * * @returns 0 on success; */ int revert (); /** @brief update the sender address. * * For Exchange 2013 at least we don't have any other way to get the * senders SMTP address then through the object model. So we have to * store the sender address for later events that do not allow us to * access the OOM but enable us to work with the underlying MAPI structure. * * @returns 0 on success */ int update_sender (); /** @brief get sender SMTP address (UTF-8 encoded). * * If the sender address has not been set through update_sender this * calls update_sender before returning the sender. * * @returns A reference to the utf8 sender address. Or NULL. */ const char *get_sender (); /** @brief get the subject string (UTF-8 encoded). * * @returns the subject or an empty string. */ std::string get_subject () const; /** @brief Is this a crypto mail handled by gpgol. * * Calling this is only valid after a message has been processed. * * @returns true if the mail was either signed or encrypted and we processed * it. */ bool is_crypto_mail () const; /** @brief This mail needs to be actually written. * * @returns true if the next write event should not be canceled. */ bool needs_save () { return m_needs_save; } /** @brief set the needs save state. */ void set_needs_save (bool val) { m_needs_save = val; } /** @brief is this mail an S/MIME mail. * * @returns true for smime messages. */ bool is_smime (); - /** @brief closes the inspector for this mail + /** @brief closes the inspector for a mail * * @returns true on success. */ - int close_inspector (); + static int close_inspector (Mail *mail); /** @brief get the associated parser. only valid while the actual parsing happens. */ std::shared_ptr parser () { return m_parser; } /** To be called from outside once the paser was done. In Qt this would be a slot that is called once it is finished we hack around that a bit by calling it from our windowmessages handler. */ void parsing_done (); /** Returns true if the mail was verified and has at least one signature. Regardless of the validity of the mail */ bool is_signed (); /** Returns a non null signature / uid if the mail was verified and the validity was high enough that we treat it as "verified sender" in the UI. The signature / uid pair returned is the signature that was used to determine the verified sender in that case. */ const std::pair get_valid_sig (); /** Small helper to check if get_valid_sig returns non null results. */ bool is_valid_sig (); /** Get UID gets UniqueID property of this mail. Returns an empty string if the uid was not set with set uid.*/ const std::string & get_uuid () const { return m_uuid; } /** Returns 0 on success if the mail has a uid alrady or sets the uid. Setting only succeeds if the OOM is currently accessible. Returns -1 on error. */ int set_uuid (); /** Returns a localized string describing the signature state of this mail. */ std::string get_signature_status (); /** Get the icon id of the appropiate icon for this mail */ int get_signature_icon_id () const; /** Get the fingerprint of an associated signature or null if it is not signed. */ const char *get_sig_fpr() const; /** Remove all categories of this mail */ void remove_categories (); /** Get the body of the mail */ std::string get_body () const; /** Get the html of the mail */ std::string get_html_body () const; /** Get the recipients recipients is a null terminated array of strings. Needs to be freed by the caller. */ char ** get_recipients () const; /** Call close with discard changes to discard plaintext. returns the value of the oom close - call. */ - int close (); + call. This may have delete the mail if the close + triggers an unload. + */ + static int close (Mail *mail); /** Try to locate the keys for all recipients */ void locate_keys(); + + /** State variable to check if a close was triggerd by us. */ + void set_close_triggered (bool value); + bool get_close_triggered () const; private: void update_categories (); void update_body (); void update_sigstate (); LPDISPATCH m_mailitem; LPDISPATCH m_event_sink; bool m_processed, /* The message has been porcessed by us. */ m_needs_wipe, /* We have added plaintext to the mesage. */ m_needs_save, /* A property was changed but not by us. */ m_crypt_successful, /* We successfuly performed crypto on the item. */ m_is_smime, /* This is an smime mail. */ m_is_smime_checked, /* it was checked if this is an smime mail */ m_is_signed, /* Mail is signed */ - m_is_valid; /* Mail is valid signed. */ + m_is_valid, /* Mail is valid signed. */ + m_close_triggered; /* We have programtically triggered a close */ int m_moss_position; /* The number of the original message attachment. */ char *m_sender; msgtype_t m_type; /* Our messagetype as set in mapi */ std::shared_ptr m_parser; GpgME::VerificationResult m_verify_result; GpgME::DecryptionResult m_decrypt_result; GpgME::Signature m_sig; GpgME::UserID m_uid; std::string m_uuid; }; #endif // MAIL_H diff --git a/src/mailitem-events.cpp b/src/mailitem-events.cpp index 4091f06..33b48e9 100644 --- a/src/mailitem-events.cpp +++ b/src/mailitem-events.cpp @@ -1,519 +1,425 @@ /* mailitem-events.h - Event handling for mails. * Copyright (C) 2015 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 "common.h" #include "eventsink.h" #include "eventsinks.h" #include "mymapi.h" #include "oomhelp.h" #include "ocidl.h" #include "windowmessages.h" #include "mail.h" #include "mapihelp.h" #undef _ #define _(a) utf8_gettext (a) const wchar_t *prop_blacklist[] = { L"Body", L"HTMLBody", L"To", /* Somehow this is done when a mail is opened */ L"CC", /* Ditto */ L"BCC", /* Ditto */ L"Categories", L"UnRead", NULL }; typedef enum { AfterWrite = 0xFC8D, AttachmentAdd = 0xF00B, AttachmentRead = 0xF00C, AttachmentRemove = 0xFBAE, BeforeAttachmentAdd = 0xFBB0, BeforeAttachmentPreview = 0xFBAF, BeforeAttachmentRead = 0xFBAB, BeforeAttachmentSave = 0xF00D, BeforeAttachmentWriteToTempFile = 0xFBB2, BeforeAutoSave = 0xFC02, BeforeCheckNames = 0xF00A, BeforeDelete = 0xFA75, BeforeRead = 0xFC8C, Close = 0xF004, CustomAction = 0xF006, CustomPropertyChange = 0xF008, Forward = 0xF468, Open = 0xF003, PropertyChange = 0xF009, Read = 0xF001, ReadComplete = 0xFC8F, Reply = 0xFC8F, ReplyAll = 0xF467, Send = 0xF005, Unload = 0xFBAD, Write = 0xF002 } MailEvent; /* Mail Item Events */ BEGIN_EVENT_SINK(MailItemEvents, IDispatch) /* We are still in the class declaration */ private: Mail * m_mail; /* The mail object related to this mailitem */ bool m_send_seen; /* The message is about to be submitted */ - bool m_decrypt_after_write; - bool m_ignore_unloads; - bool m_ignore_next_unload; }; MailItemEvents::MailItemEvents() : m_object(NULL), m_pCP(NULL), m_cookie(0), m_ref(1), m_mail(NULL), - m_send_seen (false), - m_decrypt_after_write(false), - m_ignore_unloads(false), - m_ignore_next_unload(false) + m_send_seen (false) { } MailItemEvents::~MailItemEvents() { if (m_pCP) m_pCP->Unadvise(m_cookie); if (m_object) gpgol_release (m_object); } -static DWORD WINAPI -request_decrypt (LPVOID arg) -{ - log_debug ("%s:%s: requesting decrypt again for: %p", - SRCNAME, __func__, arg); - if (do_in_ui_thread (REQUEST_DECRYPT, arg)) - { - log_debug ("%s:%s: second decrypt failed for: %p", - SRCNAME, __func__, arg); - } - return 0; -} - -static DWORD WINAPI -request_close (LPVOID arg) -{ - log_debug ("%s:%s: requesting close for: %s", - SRCNAME, __func__, (char*) arg); - if (do_in_ui_thread (REQUEST_CLOSE, arg)) - { - log_debug ("%s:%s: close request failed for: %s", - SRCNAME, __func__, (char*) arg); - } - return 0; -} - static bool propchangeWarnShown = false; /* The main Invoke function. The return value of this function does not appear to have any effect on outlook although I have read in an example somewhere that you should return S_OK so that outlook continues to handle the event I have not yet seen any effect by returning error values here and no MSDN documentation about the return values. */ EVENT_SINK_INVOKE(MailItemEvents) { USE_INVOKE_ARGS if (!m_mail) { m_mail = Mail::get_mail_for_item (m_object); if (!m_mail) { log_error ("%s:%s: mail event without mail object known. Bug.", SRCNAME, __func__); return S_OK; } } switch(dispid) { case Open: { log_oom_extra ("%s:%s: Open : %p", SRCNAME, __func__, m_mail); LPMESSAGE message; int draft_flags = 0; if (!opt.encrypt_default && !opt.sign_default) { return S_OK; } message = get_oom_base_message (m_object); if (!message) { log_error ("%s:%s: Failed to get message.", SRCNAME, __func__); break; } if (opt.encrypt_default) { draft_flags = 1; } if (opt.sign_default) { draft_flags += 2; } set_gpgol_draft_info_flags (message, draft_flags); gpgol_release (message); - - if (m_mail->is_crypto_mail()) - { - m_ignore_unloads = true; - } + break; } case BeforeRead: { log_oom_extra ("%s:%s: BeforeRead : %p", SRCNAME, __func__, m_mail); if (m_mail->pre_process_message ()) { log_error ("%s:%s: Pre process message failed.", SRCNAME, __func__); } break; } case Read: { log_oom_extra ("%s:%s: Read : %p", SRCNAME, __func__, m_mail); if (!m_mail->is_crypto_mail()) { /* Not for us. */ break; } if (m_mail->set_uuid ()) { log_debug ("%s:%s: Failed to set uuid.", SRCNAME, __func__); delete m_mail; /* deletes this, too */ return S_OK; } if (m_mail->decrypt_verify ()) { log_error ("%s:%s: Decrypt message failed.", SRCNAME, __func__); } if (!opt.enable_smime && m_mail->is_smime ()) { /* We want to save the mail when it's an smime mail and smime is disabled to revert it. */ m_mail->set_needs_save (true); } break; } case PropertyChange: { if (!parms || parms->cArgs != 1 || parms->rgvarg[0].vt != VT_BSTR || !parms->rgvarg[0].bstrVal) { log_error ("%s:%s: Unexpected params.", SRCNAME, __func__); break; } const wchar_t *prop_name = parms->rgvarg[0].bstrVal; if (!m_mail->is_crypto_mail ()) { if (!opt.autoresolve) { break; } if (!wcscmp (prop_name, L"To") || !wcscmp (prop_name, L"BCC") || !wcscmp (prop_name, L"CC")) { if ((m_mail->needs_crypto() & 1)) { m_mail->locate_keys(); } } break; } for (const wchar_t **cur = prop_blacklist; *cur; cur++) { if (!wcscmp (prop_name, *cur)) { log_oom ("%s:%s: Message %p propchange: %ls discarded.", SRCNAME, __func__, m_object, prop_name); return S_OK; } } log_oom ("%s:%s: Message %p propchange: %ls.", SRCNAME, __func__, m_object, prop_name); /* We have tried several scenarios to handle propery changes. Only save the property in MAPI and call MAPI SaveChanges worked and did not leak plaintext but this caused outlook still to break the attachments of PGP/MIME Mails into two attachments and add them as winmail.dat so other clients are broken. Alternatively reverting the mail, saving the property and then decrypt again also worked a bit but there were some weird side effects and breakages. But this has the usual problem of a revert that the mail is created by outlook and e.g. multipart/signed signatures from most MUA's are broken. - Close -> discard changes -> then setting the property and - then saving also works but then the mail is closed / unloaded - and we can't decrypt again. - Some things to try out might be the close approach and then another open or a selection change. But for now we just warn. As a workardound a user should make property changes when the mail was not read by us. */ if (propchangeWarnShown) { return S_OK; } wchar_t *title = utf8_to_wchar (_("Sorry, that's not possible, yet")); char *fmt; gpgrt_asprintf (&fmt, _("GpgOL has prevented the change to the \"%s\" property.\n" "Property changes are not yet handled for crypto messages.\n\n" "To workaround this limitation please change the property when the " "message is not open in any window and not selected in the " "messagelist.\n\nFor example by right clicking but not selecting the message.\n"), wchar_to_utf8(prop_name)); wchar_t *msg = utf8_to_wchar (fmt); xfree (fmt); MessageBoxW (get_active_hwnd(), msg, title, MB_ICONINFORMATION | MB_OK); xfree (msg); xfree (title); propchangeWarnShown = true; return S_OK; } case CustomPropertyChange: { log_oom_extra ("%s:%s: CustomPropertyChange : %p", SRCNAME, __func__, m_mail); /* TODO */ break; } case Send: { /* This is the only event where we can cancel the send of an mailitem. But it is too early for us to encrypt as the MAPI structures are not yet filled. Crypto based on the Outlook Object Model data did not work as the messages were only sent out empty. See 2b376a48 for a try of this. The we store send_seend and invoke a save which will result in an error but only after triggering all the behavior we need -> filling mapi structures and invoking the AfterWrite handler where we encrypt. If this encryption is successful and we pass the send as then the encrypted data is sent. */ log_oom_extra ("%s:%s: Send : %p", SRCNAME, __func__, m_mail); if (parms->cArgs != 1 || parms->rgvarg[0].vt != (VT_BOOL | VT_BYREF)) { log_debug ("%s:%s: Uncancellable send event.", SRCNAME, __func__); break; } m_mail->update_sender (); m_send_seen = true; invoke_oom_method (m_object, "Save", NULL); if (m_mail->crypto_successful ()) { log_debug ("%s:%s: Passing send event for message %p.", SRCNAME, __func__, m_object); m_send_seen = false; break; } else { log_debug ("%s:%s: Message %p cancelling send - crypto failed.", SRCNAME, __func__, m_object); *(parms->rgvarg[0].pboolVal) = VARIANT_TRUE; } return S_OK; } case Write: { log_oom_extra ("%s:%s: Write : %p", SRCNAME, __func__, m_mail); /* This is a bit strange. We sometimes get multiple write events without a read in between. When we access the message in the second event it fails and if we cancel the event outlook crashes. So we have keep the m_needs_wipe state variable to keep track of that. */ if (parms->cArgs != 1 || parms->rgvarg[0].vt != (VT_BOOL | VT_BYREF)) { /* This happens in the weird case */ log_debug ("%s:%s: Uncancellable write event.", SRCNAME, __func__); break; } if (m_mail->is_crypto_mail () && !m_mail->needs_save ()) { /* We cancel the write event to stop outlook from excessively syncing our changes. if smime support is disabled and we still have an smime mail we also don't want to cancel the write event to enable reverting this mails. */ *(parms->rgvarg[0].pboolVal) = VARIANT_TRUE; log_debug ("%s:%s: Canceling write event.", SRCNAME, __func__); return S_OK; } log_debug ("%s:%s: Passing write event.", SRCNAME, __func__); m_mail->set_needs_save (false); break; } case AfterWrite: { log_oom_extra ("%s:%s: AfterWrite : %p", SRCNAME, __func__, m_mail); if (m_send_seen) { m_send_seen = false; m_mail->encrypt_sign (); return S_OK; } - else if (m_decrypt_after_write) - { - char *uuid = strdup (m_mail->get_uuid ().c_str()); - HANDLE thread = CreateThread (NULL, 0, request_decrypt, - (LPVOID) uuid, 0, NULL); - CloseHandle (thread); - m_decrypt_after_write = false; - } break; } case Close: { log_oom_extra ("%s:%s: Close : %p", SRCNAME, __func__, m_mail); if (m_mail->is_crypto_mail ()) { /* Close. This happens when an Opened mail is closed. To prevent the question of wether or not to save the changes (Which would save the decrypted data without an event to prevent it) we cancel the close and then either close it with discard changes or revert / save it. - This happens with a window message as we can't invoke close from + Contrary to documentation we can invoke close from close. - - But as a side effect the mail, if opened in the explorer still will - be reverted, too. So shown as empty. To prevent that - we request a decrypt in the AfterWrite event which checks if the - message is opened in the explorer. If not it destroys the mail. - - Evil Hack: Outlook sends an Unload event after the message is closed - This is not true our Internal Object is kept alive if it is opened - in the explorer. So we ignore the unload event and then check in - the window message handler that checks for decrypt again if the - mail is currently open in the active explorer. If not we delete our - Mail object so that the message is released. */ if (parms->cArgs != 1 || parms->rgvarg[0].vt != (VT_BOOL | VT_BYREF)) { /* This happens in the weird case */ log_debug ("%s:%s: Uncancellable close event.", SRCNAME, __func__); break; } + if (m_mail->get_close_triggered ()) + { + /* Our close with discard changes, pass through */ + m_mail->set_close_triggered (false); + return S_OK; + } *(parms->rgvarg[0].pboolVal) = VARIANT_TRUE; log_oom_extra ("%s:%s: Canceling close event.", SRCNAME, __func__); - m_decrypt_after_write = true; - m_ignore_unloads = false; - m_ignore_next_unload = true; - - char *uuid = strdup (m_mail->get_uuid ().c_str()); - HANDLE thread = CreateThread (NULL, 0, request_close, - (LPVOID) uuid, 0, NULL); - CloseHandle (thread); + if (Mail::close(m_mail)) + { + log_debug ("%s:%s: Close request failed.", + SRCNAME, __func__); + } } + return S_OK; } case Unload: { log_oom_extra ("%s:%s: Unload : %p", SRCNAME, __func__, m_mail); - /* Unload. Experiments have shown that this does not - mean a mail is actually unloaded in Outlook. E.g. - If it was open in an inspector and then closed we - see an unload event but the mail is still shown in - the explorer. Fun. On the other hand if a message - was opened and the explorer selection changes - we also get an unload but the mail is still open. - - Really we still get events after the unload and - can make changes to the object. - - In case the mail was opened m_ignore_unloads is set - to true so the mail is not removed when the message - selection changes. As close invokes decrypt_again - the mail object is removed there when the explorer - selection changed. - - In case the mail was closed m_ignore_next_unload - is set so only the Unload thad follows the canceled - close is ignored and not the unload that comes from - our then triggered close (save / discard). - - - This is horribly hackish and feels wrong. But it - works. - */ - if (m_ignore_unloads || m_ignore_next_unload) - { - if (m_ignore_next_unload) - { - m_ignore_next_unload = false; - } - log_debug ("%s:%s: Ignoring unload for message: %p.", - SRCNAME, __func__, m_object); - } - else - { - log_debug ("%s:%s: Removing Mail for message: %p.", - SRCNAME, __func__, m_object); - delete m_mail; - } + log_debug ("%s:%s: Removing Mail for message: %p.", + SRCNAME, __func__, m_object); + delete m_mail; return S_OK; } default: log_oom_extra ("%s:%s: Message:%p Unhandled Event: %lx \n", SRCNAME, __func__, m_object, dispid); } return S_OK; } END_EVENT_SINK(MailItemEvents, IID_MailItemEvents) diff --git a/src/windowmessages.cpp b/src/windowmessages.cpp index 64e4ef1..a4bf62c 100644 --- a/src/windowmessages.cpp +++ b/src/windowmessages.cpp @@ -1,279 +1,220 @@ /* @file windowmessages.h * @brief Helper class to work with the windowmessage handler thread. * * Copyright (C) 2015, 2016 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 "windowmessages.h" #include "common.h" #include "oomhelp.h" #include "mail.h" #include "gpgoladdin.h" #include #define RESPONDER_CLASS_NAME "GpgOLResponder" /* Singleton window */ static HWND g_responder_window = NULL; LONG_PTR WINAPI gpgol_window_proc (HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { if (message == WM_USER + 1) { wm_ctx_t *ctx = (wm_ctx_t *) lParam; log_debug ("%s:%s: Recieved user msg: %i", SRCNAME, __func__, ctx->wmsg_type); switch (ctx->wmsg_type) { case (PARSING_DONE): { auto mail = (Mail*) ctx->data; if (!Mail::is_valid_ptr (mail)) { log_debug ("%s:%s: Parsing done for mail which is gone.", SRCNAME, __func__); break; } mail->parsing_done(); break; } - case (REQUEST_DECRYPT): - { - char *uuid = (char *) ctx->data; - auto mail = Mail::get_mail_for_uuid (uuid); - if (!mail) - { - log_debug ("%s:%s: Decrypt again for uuid which is gone.", - SRCNAME, __func__); - xfree (uuid); - break; - } - /* Check if we are still in the active explorer. */ - LPDISPATCH mailitem = get_oom_object (GpgolAddin::get_instance()->get_application (), - "ActiveExplorer.Selection.Item(1)"); - if (!mailitem) - { - log_debug ("%s:%s: Decrypt again but no selected mailitem.", - SRCNAME, __func__); - xfree (uuid); - delete mail; - break; - } - - char *active_uuid = get_unique_id (mailitem, 0, nullptr); - if (!active_uuid || strcmp (active_uuid, uuid)) - { - log_debug ("%s:%s: UUID mismatch", - SRCNAME, __func__); - xfree (uuid); - delete mail; - break; - } - log_debug ("%s:%s: Decrypting %s again", - SRCNAME, __func__, uuid); - xfree (uuid); - xfree (active_uuid); - - mail->decrypt_verify (); - break; - } - case (REQUEST_CLOSE): - { - char *uuid = (char *) ctx->data; - auto mail = Mail::get_mail_for_uuid (uuid); - if (!mail) - { - log_debug ("%s:%s: Close request for uuid which is gone.", - SRCNAME, __func__); - break; - } - if (mail->close()) - { - log_debug ("%s:%s: Close request failed.", - SRCNAME, __func__); - } - ctx->wmsg_type = REQUEST_DECRYPT; - gpgol_window_proc (hWnd, message, wParam, (LPARAM) ctx); - break; - } case (INVALIDATE_UI): { log_debug ("%s:%s: Invalidating UI", SRCNAME, __func__); gpgoladdin_invalidate_ui(); log_debug ("%s:%s: Invalidation done", SRCNAME, __func__); break; } default: log_debug ("Unknown msg"); } return DefWindowProc(hWnd, message, wParam, lParam); } return DefWindowProc(hWnd, message, wParam, lParam); } HWND create_responder_window () { size_t cls_name_len = strlen(RESPONDER_CLASS_NAME) + 1; char cls_name[cls_name_len]; if (g_responder_window) { return g_responder_window; } /* Create Window wants a mutable string as the first parameter */ snprintf (cls_name, cls_name_len, "%s", RESPONDER_CLASS_NAME); WNDCLASS windowClass; windowClass.style = CS_GLOBALCLASS | CS_DBLCLKS; windowClass.lpfnWndProc = gpgol_window_proc; windowClass.cbClsExtra = 0; windowClass.cbWndExtra = 0; windowClass.hInstance = (HINSTANCE) GetModuleHandle(NULL); windowClass.hIcon = 0; windowClass.hCursor = 0; windowClass.hbrBackground = 0; windowClass.lpszMenuName = 0; windowClass.lpszClassName = cls_name; RegisterClass(&windowClass); g_responder_window = CreateWindow (cls_name, RESPONDER_CLASS_NAME, 0, 0, 0, 0, 0, 0, (HMENU) 0, (HINSTANCE) GetModuleHandle(NULL), 0); return g_responder_window; } int send_msg_to_ui_thread (wm_ctx_t *ctx) { size_t cls_name_len = strlen(RESPONDER_CLASS_NAME) + 1; char cls_name[cls_name_len]; snprintf (cls_name, cls_name_len, "%s", RESPONDER_CLASS_NAME); HWND responder = FindWindow (cls_name, RESPONDER_CLASS_NAME); if (!responder) { log_error ("%s:%s: Failed to find responder window.", SRCNAME, __func__); return -1; } SendMessage (responder, WM_USER + 1, 0, (LPARAM) ctx); return 0; } int do_in_ui_thread (gpgol_wmsg_type type, void *data) { wm_ctx_t ctx = {NULL, UNKNOWN, 0}; ctx.wmsg_type = type; ctx.data = data; if (send_msg_to_ui_thread (&ctx)) { return -1; } return ctx.err; } static std::vector explorers; void add_explorer (LPDISPATCH explorer) { explorers.push_back (explorer); } void remove_explorer (LPDISPATCH explorer) { explorers.erase(std::remove(explorers.begin(), explorers.end(), explorer), explorers.end()); } LRESULT CALLBACK gpgol_hook(int code, WPARAM wParam, LPARAM lParam) { /* Once we are in the close events we don't have enough control to revert all our changes so we have to do it with this nice little hack by catching the WM_CLOSE message before it reaches outlook. */ LPCWPSTRUCT cwp = (LPCWPSTRUCT) lParam; switch (cwp->message) { case WM_CLOSE: { HWND lastChild = NULL; for (const auto explorer: explorers) { /* Casting to LPOLEWINDOW and calling GetWindow succeeded in Outlook 2016 but always returned the number 1. So we need this hack. */ char *caption = get_oom_string (explorer, "Caption"); if (!caption) { log_debug ("%s:%s: No caption.", SRCNAME, __func__); continue; } /* rctrl_renwnd32 is the window class of outlook. */ HWND hwnd = FindWindowExA(NULL, lastChild, "rctrl_renwnd32", caption); xfree (caption); lastChild = hwnd; if (hwnd == cwp->hwnd) { log_debug ("%s:%s: WM_CLOSE windowmessage for explorer. " "Closing all mails.", SRCNAME, __func__); Mail::close_all_mails(); break; } } break; } case WM_SYSCOMMAND: /* This comes to often and when we are closed from the icon we also get WM_CLOSE if (cwp->wParam == SC_CLOSE) { log_debug ("%s:%s: SC_CLOSE syscommand. Closing all mails.", SRCNAME, __func__); Mail::close_all_mails(); } */ break; default: break; } return CallNextHookEx (NULL, code, wParam, lParam); } /* Create the message hook for outlook's windowmessages we are especially interested in WM_QUIT to do cleanups and prevent the "Item has changed" question. */ HHOOK create_message_hook() { return SetWindowsHookEx (WH_CALLWNDPROC, gpgol_hook, NULL, GetCurrentThreadId()); } diff --git a/src/windowmessages.h b/src/windowmessages.h index 7c07ae3..4dbd711 100644 --- a/src/windowmessages.h +++ b/src/windowmessages.h @@ -1,76 +1,73 @@ /* windowmessages.h - Helper functions for Window message exchange. * Copyright (C) 2015 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 . */ #ifndef WINDOWMESSAGES_H #define WINDOWMESSAGES_H #include /** Window Message handling for GpgOL. In Outlook only one thread has access to the Outlook Object model and this is the UI Thread. We can work in other threads but to do something with outlooks data we neet to be in the UI Thread. So we create a hidden Window in this thread and use the fact that SendMessage handles Window messages in the thread where the Window was created. This way we can go back to interactct with the Outlook from another thread without working with COM Multithreading / Marshaling. The Responder Window should be initalized on startup. */ typedef enum _gpgol_wmsg_type { UNKNOWN = 0, INVALIDATE_UI = 1, /* The UI should be invalidated. */ PARSING_DONE = 2, /* A mail was parsed. Data should be a pointer to the mail object. */ - REQUEST_DECRYPT = 3, - REQUEST_CLOSE = 4 /* Request the mail to be closed with discard - changes set to true */ } gpgol_wmsg_type; typedef struct { void *data; /* Pointer to arbitrary data depending on msg type */ gpgol_wmsg_type wmsg_type; /* Type of the msg. */ int err; /* Set to true on error */ } wm_ctx_t; /** Create and register the responder window. The responder window should be */ HWND create_responder_window (); /** Send a message to the UI thread through the responder Window. Returns 0 on success. */ int send_msg_to_ui_thread (wm_ctx_t *ctx); /** Uses send_msg_to_ui_thread to execute the request in the ui thread. Returns the result. */ int do_in_ui_thread (gpgol_wmsg_type type, void *data); /** Create our filter before outlook Window Messages. */ HHOOK create_message_hook(); void add_explorer (LPDISPATCH explorer); void remove_explorer (LPDISPATCH explorer); #endif // WINDOWMESSAGES_H