diff --git a/src/cryptcontroller.cpp b/src/cryptcontroller.cpp index 6e48c80..38da2fe 100644 --- a/src/cryptcontroller.cpp +++ b/src/cryptcontroller.cpp @@ -1,1566 +1,1567 @@ /* @file cryptcontroller.cpp * @brief Helper to do crypto on a mail. * * Copyright (C) 2018 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 "cpphelp.h" #include "cryptcontroller.h" #include "mail.h" #include "mapihelp.h" #include "mimemaker.h" #include "wks-helper.h" #include "overlay.h" #include "keycache.h" #include "mymapitags.h" #include "recipient.h" #include "recipientmanager.h" #include "windowmessages.h" #include #include #include #include "common.h" #include static int sink_data_write (sink_t sink, const void *data, size_t datalen) { GpgME::Data *d = static_cast(sink->cb_data); d->write (data, datalen); return 0; } static int create_sign_attach (sink_t sink, protocol_t protocol, GpgME::Data &signature, GpgME::Data &signedData, const char *micalg); /** We have some C Style cruft in here as this was historically how GpgOL worked directly in the MAPI data objects. To reduce the regression risk the new object oriented way for crypto reused as much as possible from this. */ CryptController::CryptController (Mail *mail, bool encrypt, bool sign, GpgME::Protocol proto): m_mail (mail), m_encrypt (encrypt), m_sign (sign), m_crypto_success (false), m_proto (proto) { TSTART; memdbg_ctor ("CryptController"); log_debug ("%s:%s: CryptController ctor for %p encrypt %i sign %i inline %i.", SRCNAME, __func__, mail, encrypt, sign, mail->getDoPGPInline ()); m_recipients = mail->getCachedRecipients (); m_signer_keys = mail->getSigningKeys (); m_sender = mail->getSender (); TRETURN; } CryptController::~CryptController() { TSTART; memdbg_dtor ("CryptController"); log_debug ("%s:%s:%p", SRCNAME, __func__, m_mail); TRETURN; } int CryptController::collect_data () { TSTART; int count = m_mail->plainAttachments ().size (); /* Take the Body from the mail if possible. This is a fix for GnuPG-Bug-ID: T3614 because the body is not always properly updated in MAPI when sending. */ char *body = m_mail->takeCachedPlainBody (); if (body && !*body) { xfree (body); body = nullptr; } if (!count && !body) { if (!m_mail->isDraftEncrypt()) { gpgol_message_box (m_mail->getWindow (), utf8_gettext ("Can't encrypt / sign an empty message."), utf8_gettext ("GpgOL"), MB_OK); } xfree (body); TRETURN -1; } bool do_inline = m_mail->getDoPGPInline (); if (count && do_inline) { log_debug ("%s:%s: PGP Inline not supported for attachments." " Using PGP MIME", SRCNAME, __func__); do_inline = false; m_mail->setDoPGPInline (false); } else if (do_inline) { /* Inline. Use Body as input. We need to collect also our mime structure for S/MIME as we don't know yet if we are S/MIME or OpenPGP */ m_bodyInput.write (body, strlen (body)); log_debug ("%s:%s: Inline. Caching body.", SRCNAME, __func__); /* Set the input buffer to start. */ m_bodyInput.seek (0, SEEK_SET); } /* Set up the sink object to collect the mime structure */ struct sink_s sinkmem; sink_t sink = &sinkmem; memset (sink, 0, sizeof *sink); sink->cb_data = &m_input; sink->writefnc = sink_data_write; /* Collect the mime strucutre */ int err = add_body_and_attachments (sink, m_mail, body); xfree (body); if (err) { log_error ("%s:%s: Collecting body and attachments failed.", SRCNAME, __func__); TRETURN -1; } /* Set the input buffer to start. */ m_input.seek (0, SEEK_SET); TRETURN 0; } int CryptController::lookup_fingerprints (const std::vector &sigFprs, const std::vector > &recpFprs) { TSTART; auto ctx = std::shared_ptr (GpgME::Context::createForProtocol (m_proto)); if (!ctx) { log_error ("%s:%s: failed to create context with protocol '%s'", SRCNAME, __func__, m_proto == GpgME::CMS ? "smime" : m_proto == GpgME::OpenPGP ? "openpgp" : "unknown"); TRETURN -1; } ctx->setKeyListMode (GpgME::Local); GpgME::Error err; if (sigFprs.size()) { char **cSigners = vector_to_cArray (sigFprs); err = ctx->startKeyListing (const_cast (cSigners), true); if (err) { log_error ("%s:%s: failed to start signer keylisting", SRCNAME, __func__); release_cArray (cSigners); TRETURN -1; } do { m_signer_keys.push_back(ctx->nextKey(err)); } while (!err); release_cArray (cSigners); m_signer_keys.pop_back(); if (m_signer_keys.empty()) { log_error ("%s:%s: failed to lookup key for '%s' with protocol '%s'", SRCNAME, __func__, anonstr (sigFprs[0].c_str ()), m_proto == GpgME::CMS ? "smime" : m_proto == GpgME::OpenPGP ? "openpgp" : "unknown"); TRETURN -1; } // reset context ctx = std::shared_ptr (GpgME::Context::createForProtocol (m_proto)); ctx->setKeyListMode (GpgME::Local); } if (!recpFprs.size()) { TRETURN 0; } std::vector all_fingerprints; for (const auto &pair: recpFprs) { all_fingerprints.push_back (pair.second); } for (auto &recp: m_recipients) { std::vector fingerprintsToLookup; /* Find the matching fingerprints for the recpient. */ for (const auto &pair: recpFprs) { if (pair.first.empty()) { log_error ("%s:%s Recipient mbox is empty. Wrong Resolver Version?", SRCNAME, __func__); } /* We also accept the empty string here for backwards compatibility but we should still log an error to be clear. */ if (pair.first.empty() || pair.first == recp.mbox ()) { if (pair.second.empty ()) { log_err ("Would have added an empty string!"); continue; } fingerprintsToLookup.push_back (pair.second); all_fingerprints.erase(std::remove_if(all_fingerprints.begin(), all_fingerprints.end(), [&pair](const std::string &x){return x == pair.second;}), all_fingerprints.end()); } } if (!fingerprintsToLookup.size ()) { log_dbg ("No key selected for '%s'", anonstr (recp.mbox().c_str ())); continue; } // Convert recipient fingerprints char **cRecps = vector_to_cArray (fingerprintsToLookup); err = ctx->startKeyListing (const_cast (cRecps)); if (err) { log_error ("%s:%s: failed to start recipient keylisting", SRCNAME, __func__); release_cArray (cRecps); TRETURN -1; } std::vector keys; do { const auto key = ctx->nextKey (err); if (key.isNull () || err) { continue; } keys.push_back (key); log_dbg ("Adding '%s' as key for '%s", anonstr (key.primaryFingerprint ()), anonstr (recp.mbox ().c_str ())); } while (!err); release_cArray (cRecps); recp.setKeys (keys); // reset context ctx = std::shared_ptr (GpgME::Context::createForProtocol (m_proto)); ctx->setKeyListMode (GpgME::Local); } if (!all_fingerprints.empty()) { log_error ("%s:%s: BUG: Not all fingerprints could be matched " "to a recipient.", SRCNAME, __func__); for (const auto &fpr: all_fingerprints) { log_debug ("%s:%s Failed to find: %s", SRCNAME, __func__, anonstr (fpr.c_str ())); } clear_keys (); TRETURN -1; } resolving_done (); TRETURN 0; } int CryptController::parse_output (GpgME::Data &resolverOutput) { TSTART; std::istringstream ss(resolverOutput.toString()); std::string line; std::vector sigFprs; std::vector> recpFprs; while (std::getline (ss, line)) { rtrim (line); if (line == "cancel") { log_debug ("%s:%s: resolver canceled", SRCNAME, __func__); TRETURN -2; } if (line == "unencrypted") { log_debug ("%s:%s: FIXME resolver wants unencrypted", SRCNAME, __func__); TRETURN -1; } std::istringstream lss (line); // First is sig or enc std::string what; std::string how; std::string fingerprint; std::string mbox; std::getline (lss, what, ':'); std::getline (lss, how, ':'); std::getline (lss, fingerprint, ':'); std::getline (lss, mbox, ':'); if (m_proto == GpgME::UnknownProtocol) { /* TODO: Allow mixed */ m_proto = (how == "smime") ? GpgME::CMS : GpgME::OpenPGP; } if (what == "sig") { sigFprs.push_back (fingerprint); continue; } if (what == "enc") { recpFprs.push_back(std::make_pair(mbox, fingerprint)); } } if (m_sign && sigFprs.empty()) { log_error ("%s:%s: Sign requested but no signing fingerprint - sending unsigned", SRCNAME, __func__); m_sign = false; } if (m_encrypt && !recpFprs.size()) { log_error ("%s:%s: Encrypt requested but no recipient fingerprints", SRCNAME, __func__); gpgol_message_box (m_mail->getWindow (), utf8_gettext ("No recipients for encryption selected."), _("GpgOL"), MB_OK); TRETURN -2; } TRETURN lookup_fingerprints (sigFprs, recpFprs); } /* Combines all recipient keys for this operation into a list and copys the resolved recipients back. */ void CryptController::resolving_done () { TSTART; m_enc_keys.clear (); for (const auto &recp: m_recipients) { const auto &keys = recp.keys (); m_enc_keys.insert (m_enc_keys.end (), keys.begin (), keys.end ()); } /* Copy the resolved recipients back to the mail */ m_mail->setRecipients (m_recipients); m_mail->setSigningKeys (m_signer_keys); TRETURN; } bool CryptController::resolve_through_protocol (GpgME::Protocol proto) { TSTART; const auto cache = KeyCache::instance (); if (m_encrypt) { for (auto &recp: m_recipients) { recp.setKeys(cache->getEncryptionKeys(recp.mbox (), proto)); } } if (m_sign) { if (m_sender.empty()) { log_error ("%s:%s: Asked to sign but sender is empty.", SRCNAME, __func__); return false; } const auto key = cache->getSigningKey (m_sender.c_str (), proto); if (m_signer_keys.empty ()) { m_signer_keys.push_back (key); } for (const auto &k: m_signer_keys) { if (k.protocol () != proto) { m_signer_keys.push_back (key); break; } } } TRETURN is_resolved (); } /* Check if we can be resolved by a single protocol and return it. */ GpgME::Protocol CryptController::get_resolved_protocol () const { TSTART; GpgME::Protocol ret = GpgME::UnknownProtocol; bool hasOpenPGPSignKey = false; bool hasSMIMESignKey = false; if (m_sign) { for (const auto &sig_key: m_signer_keys) { hasOpenPGPSignKey |= (!sig_key.isNull() && sig_key.protocol () == GpgME::OpenPGP); hasSMIMESignKey |= (!sig_key.isNull() && sig_key.protocol () == GpgME::CMS); } } if (m_encrypt) { for (const auto &recp: m_recipients) { if (!recp.keys ().size () || recp.keys ()[0].isNull ()) { /* No keys or the first key in the list is null. */ TRETURN GpgME::UnknownProtocol; } for (const auto &key: recp.keys()) { if (key.protocol () == GpgME::OpenPGP && (!m_sign || hasOpenPGPSignKey) && (ret == GpgME::UnknownProtocol || ret == GpgME::OpenPGP)) { ret = GpgME::OpenPGP; continue; } if (key.protocol () == GpgME::CMS && (!m_sign || hasSMIMESignKey) && (ret == GpgME::UnknownProtocol || ret == GpgME::CMS)) { ret = GpgME::CMS; continue; } // Unresolvable TRETURN GpgME::UnknownProtocol; } } } TRETURN ret; } /* Check that the crypt operation is resolved. This supports combined S/MIME and OpenPGP operations. */ bool CryptController::is_resolved () const { /* Check that we have signing keys if necessary and at least one encryption key for each recipient. */ bool hasOpenPGPSignKey = false; bool hasSMIMESignKey = false; if (m_sign) { for (const auto &sig_key: m_signer_keys) { hasOpenPGPSignKey |= (!sig_key.isNull() && sig_key.protocol () == GpgME::OpenPGP); hasSMIMESignKey |= (!sig_key.isNull() && sig_key.protocol () == GpgME::CMS); } } log_dbg ("Has OpenPGP Sig Key: %i SMIME: %i", hasOpenPGPSignKey, hasSMIMESignKey); if (m_encrypt) { Recipient::dump (m_recipients); for (const auto &recp: m_recipients) { if (!recp.keys ().size () || recp.keys ()[0].isNull ()) { /* No keys or the first key in the list is null. */ return false; } /* If we don't sign we need no more checks. */ if (!m_sign) { continue; } for (const auto &key: recp.keys()) { if (key.protocol () == GpgME::OpenPGP && !hasOpenPGPSignKey) { return false; } if (key.protocol () == GpgME::CMS && !hasSMIMESignKey) { return false; } } } } return true; } int CryptController::resolve_keys_cached() { TSTART; // Prepare variables const auto recps = m_mail->getCachedRecipientAddresses (); bool resolved = false; if (opt.enable_smime && opt.prefer_smime) { resolved = resolve_through_protocol (GpgME::CMS); if (resolved) { log_debug ("%s:%s: Resolved with CMS due to preference.", SRCNAME, __func__); m_proto = GpgME::CMS; } } if (!resolved) { resolved = resolve_through_protocol (GpgME::OpenPGP); if (resolved) { log_debug ("%s:%s: Resolved with OpenPGP.", SRCNAME, __func__); m_proto = GpgME::OpenPGP; } } if (!resolved && (opt.enable_smime && !opt.prefer_smime)) { resolved = resolve_through_protocol (GpgME::CMS); if (resolved) { log_debug ("%s:%s: Resolved with CMS as fallback.", SRCNAME, __func__); m_proto = GpgME::CMS; } } for (const auto &recp: m_recipients) { log_debug ("Enc Key for: '%s' Type: %i", anonstr (recp.mbox ().c_str ()), recp.type ()); for (const auto &key: recp.keys ()) { log_debug ("%s:%s", to_cstr (key.protocol ()), anonstr (key.primaryFingerprint ())); } } for (const auto &sig_key: m_signer_keys) { log_debug ("%s:%s: Signing key: %s:%s", SRCNAME, __func__, to_cstr (sig_key.protocol()), anonstr (sig_key.primaryFingerprint ())); } if (!resolved) { log_debug ("%s:%s: Failed to resolve through cache", SRCNAME, __func__); m_enc_keys.clear (); m_signer_keys.clear (); m_proto = GpgME::UnknownProtocol; TRETURN 1; } if (m_encrypt) { log_debug ("%s:%s: Encrypting with protocol %s to:", SRCNAME, __func__, to_cstr (m_proto)); } TRETURN 0; } void CryptController::clear_keys () { for (auto &recp: m_recipients) { recp.setKeys (std::vector ()); } m_signer_keys.clear (); } int CryptController::resolve_keys () { TSTART; m_proto = get_resolved_protocol (); if (m_proto != GpgME::UnknownProtocol) { log_debug ("%s:%s: Already resolved by %s Not resolving again.", SRCNAME, __func__, to_cstr (m_proto)); start_crypto_overlay(); resolving_done (); TRETURN 0; } m_enc_keys.clear(); if (m_mail->isDraftEncrypt() && opt.draft_key) { const auto key = KeyCache::instance()->getByFpr (opt.draft_key); if (key.isNull()) { const char *buf = utf8_gettext ("Failed to encrypt draft.\n\n" "The configured encryption key for drafts " "could not be found.\n" "Please check your configuration or " "turn off draft encryption in the settings."); gpgol_message_box (get_active_hwnd (), buf, _("GpgOL"), MB_OK); TRETURN -1; } log_debug ("%s:%s: resolved draft encryption key protocol is: %s", SRCNAME, __func__, to_cstr (key.protocol())); m_proto = key.protocol (); m_enc_keys.push_back (key); TRETURN 0; } if (!m_recipients.size()) { /* Should not happen. But we add it for better bug reports. */ const char *bugmsg = utf8_gettext ("Operation failed.\n\n" "This is usually caused by a bug in GpgOL or an error in your setup.\n" "Please see https://www.gpg4win.org/reporting-bugs.html " "or ask your Administrator for support."); char *buf; gpgrt_asprintf (&buf, "Failed to resolve recipients.\n\n%s\n", bugmsg); memdbg_alloc (buf); gpgol_message_box (get_active_hwnd (), buf, _("GpgOL"), MB_OK); xfree(buf); TRETURN -1; } if (opt.autoresolve && !opt.alwaysShowApproval && !resolve_keys_cached ()) { log_debug ("%s:%s: resolved keys through the cache", SRCNAME, __func__); start_crypto_overlay(); resolving_done (); TRETURN 0; } std::vector args; // Collect the arguments char *gpg4win_dir = get_gpg4win_dir (); if (!gpg4win_dir) { TRACEPOINT; TRETURN -1; } const auto resolver = std::string (gpg4win_dir) + "\\bin\\resolver.exe"; args.push_back (resolver); log_debug ("%s:%s: resolving keys with '%s'", SRCNAME, __func__, resolver.c_str ()); // We want debug output as OutputDebugString args.push_back (std::string ("--debug")); // Yes passing it as int is ok. auto wnd = m_mail->getWindow (); if (wnd) { // Pass the handle of the active window for raise / overlay. args.push_back (std::string ("--hwnd")); args.push_back (std::to_string ((int) (intptr_t) wnd)); } // Set the overlay caption args.push_back (std::string ("--overlayText")); if (m_encrypt) { args.push_back (std::string (utf8_gettext ("Resolving recipients..."))); } else if (m_sign) { args.push_back (std::string (utf8_gettext ("Resolving signers..."))); } if (!opt.enable_smime) { args.push_back (std::string ("--protocol")); args.push_back (std::string ("pgp")); } if (m_sign) { args.push_back (std::string ("--sign")); } const auto cached_sender = m_mail->getSender (); if (cached_sender.empty()) { log_error ("%s:%s: resolve keys without sender.", SRCNAME, __func__); } else { args.push_back (std::string ("--sender")); args.push_back (cached_sender); } if (!opt.autoresolve || opt.alwaysShowApproval) { args.push_back (std::string ("--alwaysShow")); } if (opt.prefer_smime) { args.push_back (std::string ("--preferred-protocol")); args.push_back (std::string ("cms")); } args.push_back (std::string ("--lang")); args.push_back (std::string (gettext_localename ())); bool has_smime_override = false; if (m_encrypt) { args.push_back (std::string ("--encrypt")); // Get the recipients that are cached from OOM for (const auto &recp: m_recipients) { const auto mbox = recp.mbox (); auto overrides = KeyCache::instance ()->getOverrides (mbox, GpgME::OpenPGP); const auto cms_overrides = KeyCache::instance ()->getOverrides (mbox, GpgME::CMS); overrides.insert(overrides.end(), cms_overrides.begin(), cms_overrides.end()); if (overrides.size()) { std::string overrideStr = mbox + ":"; for (const auto &key: overrides) { if (key.isNull()) { TRACEPOINT; continue; } has_smime_override |= key.protocol() == GpgME::CMS; overrideStr += key.primaryFingerprint(); overrideStr += ","; } overrideStr.erase(overrideStr.size() - 1, 1); args.push_back (std::string ("-o")); args.push_back (overrideStr); } args.push_back (mbox); } } if (!opt.prefer_smime && has_smime_override) { /* Prefer S/MIME if there was an S/MIME override */ args.push_back (std::string ("--preferred-protocol")); args.push_back (std::string ("cms")); } // Args are prepared. Spawn the resolver. auto ctx = GpgME::Context::createForEngine (GpgME::SpawnEngine); if (!ctx) { // can't happen TRACEPOINT; TRETURN -1; } // Convert our collected vector to c strings // It's a bit overhead but should be quick for such small // data. char **cargs = vector_to_cArray (args); log_data ("%s:%s: Spawn args:", SRCNAME, __func__); for (size_t i = 0; cargs && cargs[i]; i++) { log_data (SIZE_T_FORMAT ": '%s'", i, cargs[i]); } GpgME::Data mystdin (GpgME::Data::null), mystdout, mystderr; GpgME::Error err = ctx->spawn (cargs[0], const_cast (cargs), mystdin, mystdout, mystderr, (GpgME::Context::SpawnFlags) ( GpgME::Context::SpawnAllowSetFg | GpgME::Context::SpawnShowWindow)); // Somehow Qt messes up which window to bring back to front. // So we do it manually. bring_to_front (wnd); // We need to create an overlay while encrypting as pinentry can take a while start_crypto_overlay(); log_data ("Resolver stdout:\n'%s'", mystdout.toString ().c_str ()); log_data ("Resolver stderr:\n'%s'", mystderr.toString ().c_str ()); release_cArray (cargs); if (err) { log_debug ("%s:%s: Resolver spawn finished Err code: %i asString: %s", SRCNAME, __func__, err.code(), err.asString()); } int ret = parse_output (mystdout); if (ret == -1) { log_debug ("%s:%s: Failed to parse / resolve keys.", SRCNAME, __func__); log_data ("Resolver stdout:\n'%s'", mystdout.toString ().c_str ()); log_data ("Resolver stderr:\n'%s'", mystderr.toString ().c_str ()); TRETURN -1; } TRETURN ret; } int CryptController::do_crypto (GpgME::Error &err, std::string &r_diag) { TSTART; log_debug ("%s:%s", SRCNAME, __func__); if (m_mail->isDraftEncrypt ()) { log_debug ("%s:%s Disabling sign because of draft encrypt", SRCNAME, __func__); m_sign = false; } /* Start a WKS check if necessary. */ WKSHelper::instance()->start_check (m_mail->getSender ()); int ret = 0; - if (m_mail->isSplitCopy ()) + if (m_mail->copyParent ()) { /* Bypass resolving if we are working on a split mail */ m_recipients = m_mail->getCachedRecipients (); m_signer_keys = m_mail->getSigningKeys (); if (m_recipients.size () && m_recipients[0].keys ().size ()) { m_proto = m_recipients[0].keys ()[0].protocol (); } else if (m_signer_keys.size ()) { m_proto = m_signer_keys[0].protocol (); } m_enc_keys.clear (); for (const auto &recp: m_recipients) { const auto &keys = recp.keys (); m_enc_keys.insert (m_enc_keys.end (), keys.begin (), keys.end ()); } if ((opt.enable_debug & DBG_DATA)) { log_data ("Encrypting to: "); Recipient::dump (m_recipients); } } else { ret = resolve_keys (); } if (ret == -1) { //error log_debug ("%s:%s: Failure to resolve keys.", SRCNAME, __func__); TRETURN -1; } if (ret == -2) { // Cancel TRETURN -2; } - if (!m_mail->isSplitCopy ()) + if (!m_mail->copyParent ()) { RecipientManager mngr (m_recipients, m_signer_keys); if (mngr.getRequiredMails () > 1) { log_dbg ("More then one mail required for this recipient selection."); /* If we need to send multiple emails we jump back from here into the main event loop. Copy the mail object and send it out mutiple times. */ do_in_ui_thread_async (SEND_MULTIPLE_MAILS, m_mail); /* Cancel the crypto of this mail this continues in Mail::splitAndSend_o */ + wm_unregister_pending_op (m_mail); TRETURN -3; } } bool do_inline = m_mail->getDoPGPInline (); if (m_proto == GpgME::CMS && do_inline) { log_debug ("%s:%s: Inline for S/MIME not supported. Switching to mime.", SRCNAME, __func__); do_inline = false; m_mail->setDoPGPInline (false); m_bodyInput = GpgME::Data(GpgME::Data::null); } auto ctx = GpgME::Context::create(m_proto); if (!ctx) { log_error ("%s:%s: Failure to create context.", SRCNAME, __func__); gpgol_message_box (m_mail->getWindow (), "Failure to create context.", utf8_gettext ("GpgOL"), MB_OK); TRETURN -1; } if (!m_signer_keys.empty ()) { for (const auto &key: m_signer_keys) { if (key.protocol () == m_proto) { ctx->addSigningKey (key); } } } ctx->setTextMode (m_proto == GpgME::OpenPGP); ctx->setArmor (m_proto == GpgME::OpenPGP); if (m_encrypt && m_sign && do_inline) { // Sign encrypt combined const auto result_pair = ctx->signAndEncrypt (m_enc_keys, do_inline ? m_bodyInput : m_input, m_output, GpgME::Context::AlwaysTrust); const auto err1 = result_pair.first.error(); const auto err2 = result_pair.second.error(); if (err1 || err2) { log_error ("%s:%s: Encrypt / Sign error %s %s.", SRCNAME, __func__, result_pair.first.error().asString(), result_pair.second.error().asString()); err = err1 ? err1 : err2; GpgME::Data log; const auto err3 = ctx->getAuditLog (log, GpgME::Context::DiagnosticAuditLog); if (!err3) { r_diag = log.toString(); } TRETURN -1; } if (err1.isCanceled() || err2.isCanceled()) { err = err1.isCanceled() ? err1 : err2; log_debug ("%s:%s: User cancled", SRCNAME, __func__); TRETURN -2; } } else if (m_encrypt && m_sign) { // First sign then encrypt const auto sigResult = ctx->sign (m_input, m_output, GpgME::Detached); err = sigResult.error(); if (err) { log_error ("%s:%s: Signing error %s.", SRCNAME, __func__, sigResult.error().asString()); GpgME::Data log; const auto err3 = ctx->getAuditLog (log, GpgME::Context::DiagnosticAuditLog); if (!err3) { r_diag = log.toString(); } TRETURN -1; } if (err.isCanceled()) { log_debug ("%s:%s: User cancled", SRCNAME, __func__); TRETURN -2; } parse_micalg (sigResult); // We now have plaintext in m_input // The detached signature in m_output // Set up the sink object to construct the multipart/signed GpgME::Data multipart; struct sink_s sinkmem; sink_t sink = &sinkmem; memset (sink, 0, sizeof *sink); sink->cb_data = &multipart; sink->writefnc = sink_data_write; if (create_sign_attach (sink, m_proto == GpgME::CMS ? PROTOCOL_SMIME : PROTOCOL_OPENPGP, m_output, m_input, m_micalg.c_str ())) { TRACEPOINT; TRETURN -1; } // Now we have the multipart throw away the rest. m_output = GpgME::Data (); m_input = GpgME::Data (); multipart.seek (0, SEEK_SET); const auto encResult = ctx->encrypt (m_enc_keys, multipart, m_output, GpgME::Context::AlwaysTrust); err = encResult.error(); if (err) { log_error ("%s:%s: Encryption error %s.", SRCNAME, __func__, err.asString()); GpgME::Data log; const auto err3 = ctx->getAuditLog (log, GpgME::Context::DiagnosticAuditLog); if (!err3) { r_diag = log.toString(); } TRETURN -1; } if (err.isCanceled()) { log_debug ("%s:%s: User cancled", SRCNAME, __func__); TRETURN -2; } // Now we have encrypted output just treat it like encrypted. } else if (m_encrypt) { const auto result = ctx->encrypt (m_enc_keys, do_inline ? m_bodyInput : m_input, m_output, GpgME::Context::AlwaysTrust); err = result.error(); if (err) { log_error ("%s:%s: Encryption error %s.", SRCNAME, __func__, err.asString()); GpgME::Data log; const auto err3 = ctx->getAuditLog (log, GpgME::Context::DiagnosticAuditLog); if (!err3) { r_diag = log.toString(); } TRETURN -1; } if (err.isCanceled()) { log_debug ("%s:%s: User cancled", SRCNAME, __func__); TRETURN -2; } } else if (m_sign) { const auto result = ctx->sign (do_inline ? m_bodyInput : m_input, m_output, do_inline ? GpgME::Clearsigned : GpgME::Detached); err = result.error(); if (err) { log_error ("%s:%s: Signing error %s.", SRCNAME, __func__, err.asString()); GpgME::Data log; const auto err3 = ctx->getAuditLog (log, GpgME::Context::DiagnosticAuditLog); if (!err3) { r_diag = log.toString(); } TRETURN -1; } if (err.isCanceled()) { log_debug ("%s:%s: User cancled", SRCNAME, __func__); TRETURN -2; } parse_micalg (result); } else { // ??? log_error ("%s:%s: unreachable code reached.", SRCNAME, __func__); } log_debug ("%s:%s: Crypto done sucessfuly.", SRCNAME, __func__); m_crypto_success = true; TRETURN 0; } static int write_data (sink_t sink, GpgME::Data &data) { TSTART; if (!sink || !sink->writefnc) { TRETURN -1; } char buf[4096]; size_t nread; data.seek (0, SEEK_SET); while ((nread = data.read (buf, 4096)) > 0) { sink->writefnc (sink, buf, nread); } TRETURN 0; } int create_sign_attach (sink_t sink, protocol_t protocol, GpgME::Data &signature, GpgME::Data &signedData, const char *micalg) { TSTART; char boundary[BOUNDARYSIZE+1]; char top_header[BOUNDARYSIZE+200]; int rc = 0; /* Write the top header. */ generate_boundary (boundary); create_top_signing_header (top_header, sizeof top_header, protocol, 1, boundary, micalg); if ((rc = write_string (sink, top_header))) { TRACEPOINT; TRETURN rc; } /* Write the boundary so that it is not included in the hashing. */ if ((rc = write_boundary (sink, boundary, 0))) { TRACEPOINT; TRETURN rc; } /* Write the signed mime structure */ if ((rc = write_data (sink, signedData))) { TRACEPOINT; TRETURN rc; } /* Write the signature attachment */ if ((rc = write_boundary (sink, boundary, 0))) { TRACEPOINT; TRETURN rc; } if (protocol == PROTOCOL_OPENPGP) { rc = write_string (sink, "Content-Type: application/pgp-signature;\r\n" "\tname=\"" OPENPGP_SIG_NAME "\"\r\n" "Content-Transfer-Encoding: 7Bit\r\n"); } else { rc = write_string (sink, "Content-Transfer-Encoding: base64\r\n" "Content-Type: application/pkcs7-signature\r\n" "Content-Disposition: inline;\r\n" "\tfilename=\"" SMIME_SIG_NAME "\"\r\n"); /* rc = write_string (sink, */ /* "Content-Type: application/x-pkcs7-signature\r\n" */ /* "\tname=\"smime.p7s\"\r\n" */ /* "Content-Transfer-Encoding: base64\r\n" */ /* "Content-Disposition: attachment;\r\n" */ /* "\tfilename=\"smime.p7s\"\r\n"); */ } if (rc) { TRACEPOINT; TRETURN rc; } if ((rc = write_string (sink, "\r\n"))) { TRACEPOINT; TRETURN rc; } // Write the signature data if (protocol == PROTOCOL_SMIME) { const std::string sigStr = signature.toString(); if ((rc = write_b64 (sink, (const void *) sigStr.c_str (), sigStr.size()))) { TRACEPOINT; TRETURN rc; } } else if ((rc = write_data (sink, signature))) { TRACEPOINT; TRETURN rc; } // Add an extra linefeed with should not harm. if ((rc = write_string (sink, "\r\n"))) { TRACEPOINT; TRETURN rc; } /* Write the final boundary. */ if ((rc = write_boundary (sink, boundary, 1))) { TRACEPOINT; TRETURN rc; } TRETURN rc; } static int create_encrypt_attach (sink_t sink, protocol_t protocol, GpgME::Data &encryptedData, int exchange_major_version) { TSTART; char boundary[BOUNDARYSIZE+1]; int rc = create_top_encryption_header (sink, protocol, boundary, false, exchange_major_version); // From here on use goto failure pattern. if (rc) { log_error ("%s:%s: Failed to create top header.", SRCNAME, __func__); TRETURN rc; } if (protocol == PROTOCOL_OPENPGP || exchange_major_version >= 15) { // With exchange 2016 we have to construct S/MIME // differently and write the raw data here. rc = write_data (sink, encryptedData); } else { const auto encStr = encryptedData.toString(); rc = write_b64 (sink, encStr.c_str(), encStr.size()); } if (rc) { log_error ("%s:%s: Failed to create top header.", SRCNAME, __func__); TRETURN rc; } /* Write the final boundary (for OpenPGP) and finish the attachment. */ if (*boundary && (rc = write_boundary (sink, boundary, 1))) { log_error ("%s:%s: Failed to write boundary.", SRCNAME, __func__); } TRETURN rc; } int CryptController::update_mail_mapi () { TSTART; log_debug ("%s:%s", SRCNAME, __func__); LPMESSAGE message = get_oom_base_message (m_mail->item()); if (!message) { log_error ("%s:%s: Failed to obtain message.", SRCNAME, __func__); TRETURN -1; } if (m_mail->getDoPGPInline ()) { // Nothing to do for inline. log_debug ("%s:%s: Inline mail. Setting encoding.", SRCNAME, __func__); SPropValue prop; prop.ulPropTag = PR_INTERNET_CPID; prop.Value.l = 65001; if (HrSetOneProp (message, &prop)) { log_error ("%s:%s: Failed to set CPID mapiprop.", SRCNAME, __func__); } TRETURN 0; } mapi_attach_item_t *att_table = mapi_create_attach_table (message, 0); /* When we forward e.g. a crypto mail we have sent the message has a MOSSTEMPL. We need to remove that. T4321 */ for (ULONG pos=0; att_table && !att_table[pos].end_of_table; pos++) { if (att_table[pos].attach_type == ATTACHTYPE_MOSSTEMPL) { log_debug ("%s:%s: Found existing moss attachment at " "pos %i removing it.", SRCNAME, __func__, att_table[pos].mapipos); if (message->DeleteAttach (att_table[pos].mapipos, 0, nullptr, 0) != S_OK) { log_error ("%s:%s: Failed to remove attachment.", SRCNAME, __func__); } } } // Set up the sink object for our MSOXSMIME attachment. struct sink_s sinkmem; sink_t sink = &sinkmem; memset (sink, 0, sizeof *sink); sink->cb_data = &m_input; sink->writefnc = sink_data_write; // For S/MIME encrypted mails we have to use the application/pkcs7-mime // content type. Otherwise newer (2016) exchange servers will throw // an M2MCVT.StorageError.Exeption (See GnuPG-Bug-Id: T3853 ) // This means that the conversion / build of the mime structure also // happens differently. int exchange_major_version = get_ex_major_version_for_addr ( m_mail->getSender ().c_str ()); std::string overrideMimeTag; if (m_proto == GpgME::CMS && m_encrypt && exchange_major_version >= 15) { log_debug ("%s:%s: CMS Encrypt with Exchange %i activating alternative.", SRCNAME, __func__, exchange_major_version); overrideMimeTag = "application/pkcs7-mime"; } LPATTACH attach = create_mapi_attachment (message, sink, overrideMimeTag.empty() ? nullptr : overrideMimeTag.c_str()); if (!attach) { log_error ("%s:%s: Failed to create moss attach.", SRCNAME, __func__); gpgol_release (message); TRETURN -1; } protocol_t protocol = m_proto == GpgME::CMS ? PROTOCOL_SMIME : PROTOCOL_OPENPGP; int rc = 0; /* Do we have override MIME ? */ const auto overrideMime = m_mail->get_override_mime_data (); if (!overrideMime.empty()) { rc = write_string (sink, overrideMime.c_str ()); } else if (m_sign && m_encrypt) { rc = create_encrypt_attach (sink, protocol, m_output, exchange_major_version); } else if (m_encrypt) { rc = create_encrypt_attach (sink, protocol, m_output, exchange_major_version); } else if (m_sign) { rc = create_sign_attach (sink, protocol, m_output, m_input, m_micalg.c_str ()); } // Close our attachment if (!rc) { rc = close_mapi_attachment (&attach, sink); } // Set message class etc. if (!rc) { rc = finalize_message (message, att_table, protocol, m_encrypt ? 1 : 0, false, m_mail->isDraftEncrypt (), exchange_major_version); } // only on error. if (rc) { cancel_mapi_attachment (&attach, sink); } // cleanup mapi_release_attach_table (att_table); gpgol_release (attach); gpgol_release (message); TRETURN rc; } std::string CryptController::get_inline_data () { TSTART; std::string ret; if (!m_mail->getDoPGPInline ()) { TRETURN ret; } m_output.seek (0, SEEK_SET); char buf[4096]; size_t nread; while ((nread = m_output.read (buf, 4096)) > 0) { ret += std::string (buf, nread); } TRETURN ret; } void CryptController::parse_micalg (const GpgME::SigningResult &result) { TSTART; if (result.isNull()) { TRACEPOINT; TRETURN; } const auto signature = result.createdSignature(0); if (signature.isNull()) { TRACEPOINT; TRETURN; } const char *hashAlg = signature.hashAlgorithmAsString (); if (!hashAlg) { TRACEPOINT; TRETURN; } if (m_proto == GpgME::OpenPGP) { m_micalg = std::string("pgp-") + hashAlg; } else { m_micalg = hashAlg; } std::transform(m_micalg.begin(), m_micalg.end(), m_micalg.begin(), ::tolower); log_debug ("%s:%s: micalg is: '%s'.", SRCNAME, __func__, m_micalg.c_str ()); TRETURN; } void CryptController::start_crypto_overlay () { TSTART; auto wid = m_mail->getWindow (); std::string text; if (m_encrypt) { text = utf8_gettext ("Encrypting..."); } else if (m_sign) { text = utf8_gettext ("Signing..."); } m_overlay = std::unique_ptr (new Overlay (wid, text)); TRETURN; } diff --git a/src/mail.cpp b/src/mail.cpp index 135f172..c208679 100644 --- a/src/mail.cpp +++ b/src/mail.cpp @@ -1,5179 +1,5180 @@ /* @file mail.cpp * @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 * Copyright (C) 2019, 2020, 2021 g10code 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 "categorymanager.h" #include "dialogs.h" #include "common.h" #include "mail.h" #include "eventsinks.h" #include "attachment.h" #include "mapihelp.h" #include "mimemaker.h" #include "revert.h" #include "gpgoladdin.h" #include "mymapitags.h" #include "parsecontroller.h" #include "cryptcontroller.h" #include "windowmessages.h" #include "mlang-charset.h" #include "wks-helper.h" #include "keycache.h" #include "cpphelp.h" #include "addressbook.h" #include "recipient.h" #include "recipientmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #undef _ #define _(a) utf8_gettext (a) using namespace GpgME; static std::map s_mail_map; static std::map s_uid_map; static std::map s_folder_events_map; static std::set uids_searched; static std::set s_entry_ids_printing; GPGRT_LOCK_DEFINE (mail_map_lock); GPGRT_LOCK_DEFINE (uid_map_lock); static Mail *s_last_mail; #define COLOR_DARK_GREY "#f0f0f0" #define COLOR_LIGHT_GREY "#f8f8f8" const char *HTML_PREVIEW_PLACEHOLDER = "" "" "" "" "" "" "" "" "
" "

%s %s

" "

%s

" "%s" "
"; const char *TEXT_PREVIEW_PLACEHOLDER = "%s %s %s\n\n%s"; Mail::Mail (LPDISPATCH mailitem) : m_mailitem(mailitem), m_additional_item(nullptr), m_currentItemRef(nullptr), 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_cached_html_body(nullptr), m_cached_plain_body(nullptr), m_type(MSGTYPE_UNKNOWN), m_do_inline(false), m_is_gsuite(false), m_crypt_state(NotStarted), m_window(nullptr), m_async_crypt_disabled(false), m_is_forwarded_crypto_mail(false), m_is_send_again(false), m_disable_att_remove_warning(false), m_manual_crypto_opts(false), m_first_autosecure_check(true), m_locate_count(0), m_pass_write(false), m_locate_in_progress(false), m_is_junk(false), m_is_draft_encrypt(false), m_decrypt_again(false), m_printing(false), m_recipients_set(false), - m_is_split_copy(false), + m_copy_parent(nullptr), m_attachs_added(false) { TSTART; if (getMailForItem (mailitem)) { log_error ("Mail object for item: %p already exists. Bug.", mailitem); TRETURN; } 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); TRETURN; } gpgol_lock (&mail_map_lock); s_mail_map.insert (std::pair (mailitem, this)); gpgol_unlock (&mail_map_lock); s_last_mail = this; memdbg_ctor ("Mail"); TRETURN; } GPGRT_LOCK_DEFINE(dtor_lock); // static void Mail::lockDelete () { TSTART; gpgol_lock (&dtor_lock); TRETURN; } // static void Mail::unlockDelete () { TSTART; gpgol_unlock (&dtor_lock); TRETURN; } Mail::~Mail() { TSTART; /* 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. */ gpgol_lock (&dtor_lock); memdbg_dtor ("Mail"); log_oom ("%s:%s: dtor: Mail: %p item: %p", SRCNAME, __func__, this, m_mailitem); std::map::iterator it; log_oom ("%s:%s: Detaching event sink", SRCNAME, __func__); detach_MailItemEvents_sink (m_event_sink); gpgol_release(m_event_sink); log_oom ("%s:%s: Erasing mail", SRCNAME, __func__); gpgol_lock (&mail_map_lock); it = s_mail_map.find(m_mailitem); if (it != s_mail_map.end()) { s_mail_map.erase (it); } gpgol_unlock (&mail_map_lock); if (!m_uuid.empty()) { gpgol_lock (&uid_map_lock); auto it2 = s_uid_map.find(m_uuid); if (it2 != s_uid_map.end()) { s_uid_map.erase (it2); } gpgol_unlock (&uid_map_lock); } log_oom ("%s:%s: removing categories", SRCNAME, __func__); removeCategories_o (); log_oom ("%s:%s: releasing mailitem", SRCNAME, __func__); gpgol_release(m_mailitem); if (m_additional_item) { log_oom ("%s:%s: releasing addional reference", SRCNAME, __func__); gpgol_release (m_additional_item); } xfree (m_cached_html_body); xfree (m_cached_plain_body); if (!m_uuid.empty()) { log_oom ("%s:%s: destroyed: %p uuid: %s", SRCNAME, __func__, this, m_uuid.c_str()); } else { log_oom ("%s:%s: non crypto (or sent) mail: %p destroyed", SRCNAME, __func__, this); } log_oom ("%s:%s: nulling shared pointer", SRCNAME, __func__); m_parser = nullptr; m_crypter = nullptr; releaseCurrentItem(); gpgol_unlock (&dtor_lock); log_oom ("%s:%s: returning", SRCNAME, __func__); TRETURN; } //static Mail * Mail::getMailForItem (LPDISPATCH mailitem) { TSTART; if (!mailitem) { TRETURN NULL; } std::map::iterator it; gpgol_lock (&mail_map_lock); it = s_mail_map.find(mailitem); gpgol_unlock (&mail_map_lock); if (it == s_mail_map.end()) { TRETURN NULL; } TRETURN it->second; } //static Mail * Mail::getMailForUUID (const char *uuid) { TSTART; if (!uuid) { TRETURN NULL; } gpgol_lock (&uid_map_lock); auto it = s_uid_map.find(std::string(uuid)); gpgol_unlock (&uid_map_lock); if (it == s_uid_map.end()) { TRETURN NULL; } TRETURN it->second; } //static std::vector Mail::searchMailsByUUID (const std::string &uuid) { TSTART; std::vector ret; gpgol_lock (&mail_map_lock); auto it = s_mail_map.begin(); while (it != s_mail_map.end()) { if (get_unique_id_s (it->first, 0, nullptr) == uuid) { ret.push_back (it->second); } ++it; } gpgol_unlock (&mail_map_lock); TRETURN ret; } //static bool Mail::isValidPtr (const Mail *mail) { TSTART; gpgol_lock (&mail_map_lock); auto it = s_mail_map.begin(); while (it != s_mail_map.end()) { if (it->second == mail) { gpgol_unlock (&mail_map_lock); TRETURN true; } ++it; } gpgol_unlock (&mail_map_lock); TRETURN false; } int Mail::preProcessMessage_m () { TSTART; - if (m_is_split_copy) + if (m_copy_parent) { log_dbg ("Mail was created as a copy by gpgol. Addr: %p", this); TRETURN 0; } LPMESSAGE message = get_oom_base_message (m_mailitem); if (!message) { log_error ("%s:%s: Failed to get base message.", SRCNAME, __func__); TRETURN 0; } log_oom ("%s:%s: GetBaseMessage OK for %p.", SRCNAME, __func__, m_mailitem); /* 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, &m_type); if (m_type == MSGTYPE_UNKNOWN) { /* For unknown messages we still need to check for autocrypt headers. If the mails are crypto messages the autocrypt stuff is handled in the parsecontroller. */ autocrypt_s ac; parseHeaders_m (); if (m_header_info.acInfo.exists) { log_debug ("%s:%s: Importing autocrypt header from unencrypted " "mail.", SRCNAME, __func__); KeyCache::import_pgp_key_data (m_header_info.acInfo.data); } gpgol_release (message); TRETURN 0; } /* We could check PR_ACCESS here in MAPI to figure out if we can modify a mail or not. But this strangely does not fully tell us the truth. For example for a read only mail we can modify the body of the mail but "add oom attachments" will fail. Add OOM attachments has error handling and it will show the user that missing rights are an issue. */ /* Create moss attachments here so that they are properly hidden when the item is read into the model. */ LPMESSAGE parsed_message = get_oom_message (m_mailitem); m_moss_position = mapi_mark_or_create_moss_attach (message, parsed_message, m_type); gpgol_release (parsed_message); if (!m_moss_position) { log_error ("%s:%s: Failed to find moss attachment.", SRCNAME, __func__); m_type = MSGTYPE_UNKNOWN; } gpgol_release (message); TRETURN 0; } static LPDISPATCH get_attachment_o (LPDISPATCH mailitem, int pos) { TSTART; LPDISPATCH attachment; LPDISPATCH attachments = get_oom_object (mailitem, "Attachments"); if (!attachments) { log_debug ("%s:%s: Failed to get attachments.", SRCNAME, __func__); TRETURN NULL; } std::string item_str; int count = get_oom_int (attachments, "Count"); if (count < 1) { log_debug ("%s:%s: Invalid attachment count: %i.", SRCNAME, __func__, count); gpgol_release (attachments); TRETURN NULL; } if (pos > 0) { item_str = std::string("Item(") + std::to_string(pos) + ")"; } else { item_str = std::string("Item(") + std::to_string(count) + ")"; } attachment = get_oom_object (attachments, item_str.c_str()); gpgol_release (attachments); TRETURN attachment; } /** Helper to check that all attachments are hidden, to be called before crypto. */ int Mail::checkAttachments_o (bool silent) { TSTART; if (m_attachs_added) { log_dbg ("Not rechecking attachments as " "we already added them."); TRETURN 0; } LPDISPATCH attachments = get_oom_object (m_mailitem, "Attachments"); if (!attachments) { log_debug ("%s:%s: Failed to get attachments.", SRCNAME, __func__); TRETURN 1; } int count = count_visible_attachments (attachments); if (!count) { gpgol_release (attachments); TRETURN 0; } std::string message; if (isEncrypted () && isSigned ()) { message += _("Not all attachments were encrypted or signed.\n" "The unsigned / unencrypted attachments are:\n\n"); } else if (isSigned ()) { message += _("Not all attachments were signed.\n" "The unsigned attachments are:\n\n"); } else if (isEncrypted ()) { message += _("Not all attachments were encrypted.\n" "The unencrypted attachments are:\n\n"); } else { gpgol_release (attachments); TRETURN 0; } bool foundOne = false; std::vector to_delete; 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; char *dispName = get_oom_string (oom_attach, "DisplayName"); message += dispName ? dispName : "Unknown"; xfree (dispName); message += "\n"; to_delete.push_back (oom_attach); } else { gpgol_release (oom_attach); } VariantClear (&var); } /* In silent mode we remove the unencrypted attachments silently */ if (foundOne) { for (auto attachment: to_delete) { if (silent) { m_disable_att_remove_warning = true; log_debug ("%s:%s: Deleting bad attachment", SRCNAME, __func__); if (invoke_oom_method (attachment, "Delete", NULL)) { log_error ("%s:%s: Error deleting attachment: %i", SRCNAME, __func__, __LINE__); } m_disable_att_remove_warning = false; } gpgol_release (attachment); } } gpgol_release (attachments); if (foundOne && !silent) { 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); } TRETURN 0; } /** Get the cipherstream of the mailitem. */ static LPSTREAM get_attachment_stream_o (LPDISPATCH mailitem, int pos) { TSTART; if (!pos) { log_debug ("%s:%s: Called with zero pos.", SRCNAME, __func__); TRETURN NULL; } LPDISPATCH attachment = get_attachment_o (mailitem, pos); LPSTREAM stream = NULL; if (!attachment) { // For opened messages that have ms-tnef type we // create the moss attachment but don't find it // in the OOM. Try to find it through MAPI. HRESULT hr; log_debug ("%s:%s: Failed to find MOSS Attachment. " "Fallback to MAPI.", SRCNAME, __func__); LPMESSAGE message = get_oom_message (mailitem); if (!message) { log_debug ("%s:%s: Failed to get MAPI Interface.", SRCNAME, __func__); TRETURN NULL; } hr = gpgol_openProperty (message, PR_BODY_A, &IID_IStream, 0, 0, (LPUNKNOWN*)&stream); gpgol_release (message); if (hr) { log_debug ("%s:%s: OpenProperty failed: hr=%#lx", SRCNAME, __func__, hr); TRETURN NULL; } TRETURN stream; } LPATTACH mapi_attachment = 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); TRETURN NULL; } if (FAILED (gpgol_openProperty (mapi_attachment, 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); TRETURN nullptr; } gpgol_release (mapi_attachment); TRETURN 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) { TSTART; 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; TRETURN 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); TRETURN 1; } void *buffer = NULL; /* Get a safe pointer to the array */ if (SafeArrayAccessData(var.parray, &buffer) != S_OK) { TRACEPOINT; VariantClear(&var); TRETURN 1; } /* Copy data to it */ size_t nread = attach->get_data ().read (buffer, static_cast (size)); if (nread != static_cast (size)) { TRACEPOINT; VariantClear(&var); TRETURN 1; } /*/ Unlock the variant data */ if (SafeArrayUnaccessData(var.parray) != S_OK) { TRACEPOINT; VariantClear(&var); TRETURN 1; } if (set_pa_variant (target, PR_ATTACH_DATA_BIN_DASL, &var)) { TRACEPOINT; VariantClear(&var); TRETURN 1; } VariantClear(&var); TRETURN 0; } #endif /** Sets some meta data on the last attachment added. The meta data is taken from the attachment object. */ static int fixup_last_attachment_o (LPDISPATCH mail, std::shared_ptr attachment) { TSTART; /* Currently we only set content id */ std::string cid = attachment->get_content_id (); if (cid.empty() || cid == "<>") { log_debug ("%s:%s: Content id not found.", SRCNAME, __func__); TRETURN 0; } /* We add this safeguard here was we somehow can't trigger Outlook * not to hide attachments with content id (see below). So we * have to make double sure that the attachment is actually referenced * in the HTML before we hide it. In doubt it is better to "break" * the content id reference but show the Attachment as a hidden * attachment can appear to be data loss. See T4526 T4203. */ char *body = get_oom_string (mail, "HTMLBody"); if (!body) { log_debug ("%s:%s: No HTML Body.", SRCNAME, __func__); TRETURN 0; } if (cid.front () == '<') { cid.erase (0, 1); } if (cid.back() == '>') { cid.pop_back (); } char *cid_pos = strstr (body, cid.c_str ()); xfree (body); if (!cid_pos) { log_debug ("%s:%s: Failed to find cid: '%s' in body. Not setting cid.", SRCNAME, __func__, anonstr (cid.c_str ())); TRETURN 0; } LPDISPATCH attach = get_attachment_o (mail, -1); if (!attach) { log_error ("%s:%s: No attachment.", SRCNAME, __func__); TRETURN 1; } int ret = put_pa_string (attach, PR_ATTACH_CONTENT_ID_DASL, cid.c_str ()); log_debug ("%s:%s: Set attachment content id to: '%s'", SRCNAME, __func__, anonstr (cid.c_str())); if (ret) { log_error ("%s:%s: Failed.", SRCNAME, __func__); gpgol_release (attach); } #if 0 The following was an experiement to delete the ATTACH_FLAGS values so that we are not hiding attachments. LPATTACH mapi_attach = (LPATTACH) get_oom_iunknown (attach, "MAPIOBJECT"); if (mapi_attach) { SPropTagArray proparray; HRESULT hr; proparray.cValues = 1; proparray.aulPropTag[0] = 0x37140003; hr = mapi_attach->DeleteProps (&proparray, NULL); if (hr) { log_error ("%s:%s: can't delete property attach flags: hr=%#lx\n", SRCNAME, __func__, hr); ret = -1; } gpgol_release (mapi_attach); } else { log_error ("%s:%s: Failed to get mapi attachment.", SRCNAME, __func__); } #endif gpgol_release (attach); TRETURN ret; } /** Helper to update the attachments of a mail object in oom. does not modify the underlying mapi structure. */ int Mail::add_attachments_o (std::vector > attachments) { TSTART; if (m_attachs_added) { log_dbg ("Not adding attachments as they are already there."); TRETURN 0; } bool anyError = false; m_disable_att_remove_warning = true; std::string addErrStr; int addErrCode = 0; std::vector failedNames; for (auto att: attachments) { int err = 0; const auto dispName = att->get_display_name (); if (dispName.empty()) { log_error ("%s:%s: Ignoring attachment without display name.", SRCNAME, __func__); continue; } wchar_t* wchar_name = utf8_to_wchar (dispName.c_str()); if (!wchar_name) { log_error ("%s:%s: Failed to convert '%s' to wchar.", SRCNAME, __func__, anonstr (dispName.c_str())); continue; } HANDLE hFile; wchar_t* wchar_file = get_tmp_outfile (wchar_name, &hFile); if (!wchar_file) { log_error ("%s:%s: Failed to obtain a tmp filename for: %s", SRCNAME, __func__, anonstr (dispName.c_str())); err = 1; } if (!err && att->copy_to (hFile)) { log_error ("%s:%s: Failed to copy attachment %s to temp file", SRCNAME, __func__, anonstr (dispName.c_str())); err = 1; } if (!err && add_oom_attachment (m_mailitem, wchar_file, wchar_name, addErrStr, &addErrCode)) { log_error ("%s:%s: Failed to add attachment: %s", SRCNAME, __func__, anonstr (dispName.c_str())); failedNames.push_back (dispName); err = 1; } if (hFile && hFile != INVALID_HANDLE_VALUE) { CloseHandle (hFile); } if (wchar_file && !DeleteFileW (wchar_file)) { log_error ("%s:%s: Failed to delete tmp attachment for: %s", SRCNAME, __func__, anonstr (dispName.c_str())); err = 1; } xfree (wchar_file); xfree (wchar_name); if (!err) { log_debug ("%s:%s: Added attachment '%s'", SRCNAME, __func__, anonstr (dispName.c_str())); err = fixup_last_attachment_o (m_mailitem, att); } if (err) { anyError = true; } } if (anyError) { std::string msg = _("Not all attachments can be shown.\n\n" "The hidden attachments are:"); msg += "\n"; std::string filenames; join (failedNames, "\n", filenames); msg += filenames; msg += "\n\n"; if (addErrCode == 0x80004005) { msg += _("The mail exceeds the maximum size GpgOL " "can handle on this server."); } else { msg += _("Reason:"); msg += " " + addErrStr; } gpgol_message_box (getWindow (), msg.c_str (), _("GpgOL"), MB_OK); } m_disable_att_remove_warning = false; m_attachs_added = true; TRETURN anyError; } GPGRT_LOCK_DEFINE(parser_lock); static DWORD WINAPI do_parsing (LPVOID arg) { TSTART; gpgol_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::isValidPtr (mail)) { log_debug ("%s:%s: canceling parsing for: %p already deleted", SRCNAME, __func__, arg); gpgol_unlock (&dtor_lock); TRETURN 0; } blockInv (); /* This takes a shared ptr of parser. So the parser is still valid when the mail is deleted. */ auto parser = mail->parser (); gpgol_unlock (&dtor_lock); gpgol_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::isValidPtr (mail)) { log_debug ("%s:%s: cancel for: %p already deleted", SRCNAME, __func__, arg); gpgol_unlock (&parser_lock); unblockInv(); TRETURN 0; } if (!parser) { log_error ("%s:%s: no parser found for mail: %p", SRCNAME, __func__, arg); gpgol_unlock (&parser_lock); unblockInv(); TRETURN -1; } bool is_smime = mail->isSMIME (); std::vector senderKey; if (!is_smime) { senderKey = KeyCache::instance ()->getEncryptionKeys (mail->getSender (), GpgME::OpenPGP); } bool has_already_preview = (mail->msgtype () == MSGTYPE_GPGOL_MULTIPART_SIGNED && !mail->getOriginalBody ().empty ()); if (!has_already_preview && (senderKey.empty () || is_smime) && KeyCache::instance ()->protocolIsOnline (is_smime ? GpgME::CMS : GpgME::OpenPGP) && !opt.sync_dec) { log_dbg ("Op might be online. Doing two pass verify."); parser->parse (true); const auto verify_result = parser->verify_result (); if (verify_result.numSignatures ()) { log_dbg ("Have signature, needs second pass."); do_in_ui_thread (SHOW_PREVIEW, arg); if (!Mail::isValidPtr (mail)) { log_debug ("%s:%s: canceling parsing for: %p now deleted", SRCNAME, __func__, arg); gpgol_unlock (&parser_lock); unblockInv(); TRETURN 0; } log_dbg ("Preview updated."); /* Sleep (10000); */ parser->parse (false); log_dbg ("Second parse done."); } if (!opt.sync_dec) { do_in_ui_thread (PARSING_DONE, arg); } } else { parser->parse (false); if (!opt.sync_dec) { do_in_ui_thread (PARSING_DONE, arg); } } gpgol_unlock (&parser_lock); unblockInv(); TRETURN 0; } /* How encryption is done: There are two modes of encryption. Synchronous and Async. If async is used depends on the value of mail->async_crypt_disabled. Synchronous crypto: > Send Event < | State NotStarted Needs Crypto ? (get_gpgol_draft_info_flags != 0) -> No: Pass send -> unencrypted mail. -> Yes: mail->update_oom_data State = Mail::NeedsFirstAfterWrite checkSyncCrypto_o sync -> invoke_oom_method (m_object, "Save", NULL); > Write Event < Pass because is_crypto_mail is false (not a decrypted mail) > AfterWrite Event < | State NeedsFirstAfterWrite async -> skips ^ State = NeedsActualCrypo encrypt_sign_start collect_input_data -> Check if Inline PGP should be used do_crypt -> Resolve keys / do crypto State = OOMSynced update_crypt_mapi crypter->update_mail_mapi if (inline) (Meaning PGP/Inline) <-- do nothing. else build MSOXSMIME attachment and clear body / attachments. State = BackendDone <- Back to Send Event update_crypt_oom -> Cleans body or sets PGP/Inline body. (inline_body_to_body) State = CryptoFinished -> Saftey check "has_crypted_or_empty_body" -> If MIME Mail do the T3656 check. Send. State order for "inline_response" (sync) Mails. NotStarted NeedsFirstAfterWrite DataCollected OOMSynced BackendDone CryptFinished (or inline for PGP Inline) -> Send. State order for async Mails NotStarted NeedsFirstAfterWrite DataCollected -> Cancel Send. Windowmessages -> Crypto Done BackendDone OOMUpdated trigger Save. OOMSynced CryptFinished trigger Send. */ static DWORD WINAPI do_crypt (LPVOID arg) { TSTART; gpgol_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::isValidPtr (mail)) { log_debug ("%s:%s: canceling crypt for: %p already deleted", SRCNAME, __func__, arg); gpgol_unlock (&dtor_lock); TRETURN 0; } if (mail->cryptState () != Mail::DataCollected) { log_debug ("%s:%s: invalid state %i", SRCNAME, __func__, mail->cryptState ()); mail->enableWindow (); gpgol_unlock (&dtor_lock); TRETURN -1; } /* This takes a shared ptr of crypter. So the crypter is still valid when the mail is deleted. */ auto crypter = mail->cryper (); gpgol_unlock (&dtor_lock); if (!crypter) { log_error ("%s:%s: no crypter found for mail: %p", SRCNAME, __func__, arg); gpgol_unlock (&parser_lock); mail->enableWindow (); TRETURN -1; } GpgME::Error err; std::string diag; int rc = crypter->do_crypto(err, diag); gpgol_lock (&dtor_lock); if (!Mail::isValidPtr (mail)) { log_debug ("%s:%s: aborting crypt for: %p already deleted", SRCNAME, __func__, arg); + wm_abort_pending_ops (); gpgol_unlock (&dtor_lock); TRETURN 0; } mail->enableWindow (); if (rc == -1 || err) { mail->resetCrypter (); crypter = nullptr; if (err) { char *buf = nullptr; gpgrt_asprintf (&buf, _("Crypto operation failed:\n%s"), err.asString()); std::string msg = buf; memdbg_alloc (buf); xfree (buf); if (!diag.empty()) { msg += "\n\n"; msg += _("Diagnostics"); msg += ":\n"; msg += diag; } gpgol_message_box (mail->getWindow (), msg.c_str (), _("GpgOL"), MB_OK); } else { gpgol_bug (mail->getWindow (), ERR_CRYPT_RESOLVER_FAILED); } } if (rc || err.isCanceled()) { log_debug ("%s:%s: crypto failed for: %p with: %i err: %i", SRCNAME, __func__, arg, rc, err.code()); mail->setCryptState (Mail::NotStarted); mail->setIsDraftEncrypt (false); mail->resetCrypter (); crypter = nullptr; gpgol_unlock (&dtor_lock); if (rc != -3) { mail->resetRecipients (); wm_abort_pending_ops (); } TRETURN rc; } if (!mail->isAsyncCryptDisabled ()) { mail->setCryptState (Mail::BackendDone); gpgol_unlock (&dtor_lock); // This deletes the Mail in Outlook 2010 do_in_ui_thread (CRYPTO_DONE, arg); log_debug ("%s:%s: UI thread finished for %p", SRCNAME, __func__, arg); } else if (mail->isDraftEncrypt ()) { mail->setCryptState (Mail::OOMSynced); mail->updateCryptMAPI_m (); mail->setIsDraftEncrypt (false); mail->setCryptState (Mail::NotStarted); log_debug ("%s:%s: Synchronous draft encrypt finished for %p", SRCNAME, __func__, arg); gpgol_unlock (&dtor_lock); } else { mail->setCryptState (Mail::OOMSynced); mail->updateCryptMAPI_m (); if (mail->cryptState () == Mail::CryptFinished) { // For sync crypto we need to switch this. mail->setCryptState (Mail::BackendDone); } else { // A bug! log_debug ("%s:%s: Resetting crypter because of state mismatch. %p", SRCNAME, __func__, arg); crypter = nullptr; mail->resetCrypter (); } gpgol_unlock (&dtor_lock); } /* This works around a bug in pinentry that it might bring the wrong window to front. So after encryption / signing we bring outlook back to front. See GnuPG-Bug-Id: T3732 */ do_in_ui_thread_async (BRING_TO_FRONT, nullptr, 250); log_debug ("%s:%s: crypto thread for %p finished", SRCNAME, __func__, arg); TRETURN 0; } bool Mail::isCryptoMail () const { TSTART; if (m_type == MSGTYPE_UNKNOWN || m_type == MSGTYPE_GPGOL || m_type == MSGTYPE_SMIME) { /* Not a message for us. */ TRETURN false; } TRETURN true; } int Mail::decryptVerify_o () { TSTART; if (!isCryptoMail ()) { log_debug ("%s:%s: Decrypt Verify for non crypto mail: %p.", SRCNAME, __func__, m_mailitem); TRETURN 0; } m_decrypt_again = false; if (isSMIME_m ()) { LPMESSAGE oom_message = get_oom_message (m_mailitem); if (oom_message) { char *old_class = mapi_get_old_message_class (oom_message); char *current_class = mapi_get_message_class (oom_message); if (current_class) { /* Store our own class for an eventual close */ m_gpgol_class = current_class; xfree (current_class); current_class = nullptr; } if (old_class) { const char *new_class = old_class; /* Workaround that our own class might be the original */ if (!strcmp (old_class, "IPM.Note.GpgOL.OpaqueEncrypted")) { new_class = "IPM.Note.SMIME"; } else if (!strcmp (old_class, "IPM.Note.GpgOL.MultipartSigned")) { new_class = "IPM.Note.SMIME.MultipartSigned"; } log_debug ("%s:%s:Restoring message class to %s in decverify.", SRCNAME, __func__, new_class); put_oom_string (m_mailitem, "MessageClass", new_class); xfree (old_class); setPassWrite (true); /* Sync to MAPI */ invoke_oom_method (m_mailitem, "Save", nullptr); setPassWrite (false); } gpgol_release (oom_message); } } check_html_preferred (); auto cipherstream = get_attachment_stream_o (m_mailitem, m_moss_position); if (!cipherstream) { m_is_junk = is_junk_mail (m_mailitem); if (m_is_junk) { log_debug ("%s:%s: Detected: %p as junk", SRCNAME, __func__, m_mailitem); auto mngr = CategoryManager::instance (); m_store_id = mngr->addCategoryToMail (this, CategoryManager::getJunkMailCategory (), 3 /* peach */); installFolderEventHandler_o (); TRETURN 0; } log_debug ("%s:%s: Failed to get cipherstream. Aborting handling.", SRCNAME, __func__); m_type = MSGTYPE_UNKNOWN; TRETURN 1; } setUUID_o (); m_processed = true; m_pass_write = false; /* Insert placeholder */ m_orig_body = get_oom_string_s (m_mailitem, "Body"); rtrim (m_orig_body); if (m_orig_body.empty ()) { log_dbg ("Empty body."); /* We only need to check the HTML body if the plain body is not empty as outlook would convert the html to plain. */ } else if (opt.prefer_html) { m_orig_body = get_oom_string_s (m_mailitem, "HTMLBody"); } char *placeholder_buf = nullptr; if (m_type == MSGTYPE_GPGOL_WKS_CONFIRMATION) { gpgrt_asprintf (&placeholder_buf, opt.prefer_html ? decrypt_template_html : decrypt_template, "OpenPGP", _("Pubkey directory confirmation"), _("This is a confirmation request to publish your Pubkey in the " "directory for your domain.\n\n" "

If you did not request to publish your Pubkey in your providers " "directory, simply ignore this message.

\n")); } else if (m_type == MSGTYPE_GPGOL_MULTIPART_SIGNED && !m_orig_body.empty ()) { log_dbg ("Multipart signed inserting body in placeholder."); gpgrt_asprintf (&placeholder_buf, opt.prefer_html ? HTML_PREVIEW_PLACEHOLDER : TEXT_PREVIEW_PLACEHOLDER, isSMIME_m () ? "S/MIME" : "OpenPGP", _("message"), _("Please wait while the message is being verified..."), m_orig_body.c_str ()); } else { gpgrt_asprintf (&placeholder_buf, opt.prefer_html ? decrypt_template_html : decrypt_template, isSMIME_m () ? "S/MIME" : "OpenPGP", _("message"), _("Please wait while the message is being decrypted / verified...")); } if (opt.prefer_html) { put_oom_int (m_mailitem, "BodyFormat", 2); if (put_oom_string (m_mailitem, "HTMLBody", placeholder_buf)) { log_error ("%s:%s: Failed to modify html body of item.", SRCNAME, __func__); } } else { char *tmp = get_oom_string (m_mailitem, "Body"); if (!tmp) { TRACEPOINT; TRETURN 1; } m_orig_body = tmp; xfree (tmp); put_oom_int (m_mailitem, "BodyFormat", 1); if (put_oom_string (m_mailitem, "Body", placeholder_buf)) { log_error ("%s:%s: Failed to modify body of item.", SRCNAME, __func__); } } memdbg_alloc (placeholder_buf); xfree (placeholder_buf); if (m_type == MSGTYPE_GPGOL_WKS_CONFIRMATION) { if (m_header_info.boundary.empty()) { parseHeaders_m (); } WKSHelper::instance ()->handle_confirmation_read (this, cipherstream); TRETURN 0; } m_parser = std::shared_ptr (new ParseController (cipherstream, m_type)); m_parser->setSender(GpgME::UserID::addrSpecFromString(getSender_o ().c_str())); if (opt.autoimport) { /* Handle autocrypt header. As we want to have the import of the header in the same thread as the parser we leave it to the parser. */ if (m_header_info.boundary.empty()) { parseHeaders_m (); } if (m_header_info.acInfo.exists) { m_parser->setAutocryptInfo (m_header_info.acInfo); } } log_data ("%s:%s: Parser for \"%s\" is %p", SRCNAME, __func__, getSubject_o ().c_str(), m_parser.get()); gpgol_release (cipherstream); /* Printing happens in two steps. First a Mail is loaded after the BeforePrint event, then it is loaded a second time when the actual print happens. We have to catch both. */ if (!m_printing) { m_printing = checkIfMailMightBePrinting_o (); } else { /* Register us for printing. */ s_entry_ids_printing.insert (get_oom_string_s (m_mailitem, "EntryID")); } if (!opt.sync_dec && !m_printing) { 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); TRETURN 0; } else { /* Parse synchronously */ do_parsing ((LPVOID) this); parsingDone_o (); TRETURN 0; } } int Mail::parseHeaders_m () { /* Parse the headers first so that the handler for confirmation read can access them. Should be put in its own function. */ auto message = MAKE_SHARED (get_oom_message (m_mailitem)); if (!message) { /* Hmmm */ STRANGEPOINT; TRETURN -1; } if (!mapi_get_header_info ((LPMESSAGE)message.get(), m_header_info)) { STRANGEPOINT; TRETURN -1; } TRETURN 0; } static void set_body (LPDISPATCH item, const std::string &plain, const std::string &html) { if (opt.prefer_html && !html.empty()) { if (put_oom_string (item, "HTMLBody", html.c_str ())) { log_error ("%s:%s: Failed to modify html body of item.", SRCNAME, __func__); if (!plain.empty ()) { put_oom_int (item, "BodyFormat", 1); if (put_oom_string (item, "Body", plain.c_str ())) { log_error ("%s:%s: Failed to put plaintext into body of item.", SRCNAME, __func__); } } else { put_oom_int (item, "BodyFormat", 2); if (put_oom_string (item, "HTMLBody", plain.c_str ())) { log_error ("%s:%s: Failed to put plaintext into html of item.", SRCNAME, __func__); } } } else { put_oom_int (item, "BodyFormat", 2); } } else if (!plain.empty ()) { put_oom_int (item, "BodyFormat", 1); if (put_oom_string (item, "Body", plain.c_str ())) { log_error ("%s:%s: Failed to put plaintext into body of item.", SRCNAME, __func__); } } } void Mail::updateBody_o (bool is_preview) { TSTART; if (!m_parser) { TRACEPOINT; TRETURN; } const auto error = m_parser->get_formatted_error (); if (!error.empty()) { set_body (m_mailitem, error, error); TRETURN; } if (m_verify_result.error() && !m_orig_body.empty ()) { log_error ("%s:%s: Verification failed. Restoring Body.", SRCNAME, __func__); set_body (m_mailitem, m_orig_body, m_orig_body); TRETURN; } // No need to carry body anymore if (!is_preview) { m_orig_body = std::string(); } auto html = m_parser->get_html_body (); auto body = m_parser->get_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() || (is_preview && !m_block_html))) { if (!m_block_html) { auto charset = m_parser->get_html_charset(); int codepage = 0; if (charset.empty()) { codepage = get_oom_int (m_mailitem, "InternetCodepage"); log_debug ("%s:%s: Did not find html charset." " Using internet Codepage %i.", SRCNAME, __func__, codepage); } char *converted = nullptr; if (!html.empty () || !is_preview) { converted = ansi_charset_to_utf8 (charset.c_str(), html.c_str(), html.size(), codepage); } if (is_preview) { char *buf; if (!converted) { /* Convert plaintext to HTML for preview using outlook. */ charset = m_parser->get_body_charset (); converted = ansi_charset_to_utf8 (charset.c_str(), body.c_str(), body.size(), codepage); put_oom_string (m_mailitem, "Body", converted); xfree (converted); converted = get_oom_string (m_mailitem, "HTMLBody"); } if (converted) { gpgrt_asprintf (&buf, HTML_PREVIEW_PLACEHOLDER, isSMIME_m () ? "S/MIME" : "OpenPGP", _("message"), _("Please wait while the message is being verified..."), converted); memdbg_alloc (buf); xfree (converted); converted = buf; } } TRACEPOINT; put_oom_int (m_mailitem, "BodyFormat", 2); int ret = put_oom_string (m_mailitem, "HTMLBody", converted ? converted : ""); xfree (converted); TRACEPOINT; if (ret) { log_error ("%s:%s: Failed to modify html body of item.", SRCNAME, __func__); } TRETURN; } else if (!body.empty()) { /* We had a multipart/alternative mail but html should be blocked. So we prefer the text/plain part and warn once about this so that we hopefully don't get too many bugreports about this. */ if (!opt.smime_html_warn_shown) { std::string caption = _("GpgOL") + std::string (": ") + std::string (_("HTML display disabled.")); std::string buf = _("HTML content in unsigned S/MIME mails " "is insecure."); buf += "\n"; buf += _("GpgOL will only show such mails as text."); buf += "\n\n"; buf += _("This message is shown only once."); gpgol_message_box (getWindow (), buf.c_str(), caption.c_str(), MB_OK); opt.smime_html_warn_shown = true; write_options (); } } } if (body.empty () && m_block_html && !html.empty()) { #if 0 Sadly the following code still offers to load external references it might also be too dangerous if Outlook somehow autoloads the references as soon as the Body is put into HTML // Fallback to show HTML as plaintext if HTML display // is blocked. log_error ("%s:%s: No text body. Putting HTML into plaintext.", SRCNAME, __func__); 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__); body = html; } else { char *plainBody = get_oom_string (m_mailitem, "Body"); if (!plainBody) { log_error ("%s:%s: Failed to obtain converted plain body.", SRCNAME, __func__); body = html; } else { ret = put_oom_string (m_mailitem, "HTMLBody", plainBody); xfree (plainBody); if (ret) { log_error ("%s:%s: Failed to put plain into html body of item.", SRCNAME, __func__); body = html; } else { TRETURN; } } } #endif body = html; std::string caption = _("GpgOL") + std::string (": ") + std::string (_("HTML display disabled.")); std::string buf = _("HTML content in unsigned S/MIME mails " "is insecure."); buf += "\n"; buf += _("GpgOL will only show such mails as text."); buf += "\n\n"; buf += _("Please ask the sender to sign the message or\n" "to send it with a plain text alternative."); gpgol_message_box (getWindow (), buf.c_str(), caption.c_str(), MB_OK); } find_and_replace (body, "\r\r\n", "\r\n"); const auto plain_charset = m_parser->get_body_charset(); int codepage = 0; if (plain_charset.empty()) { codepage = get_oom_int (m_mailitem, "InternetCodepage"); log_debug ("%s:%s: Did not find body charset. " "Using internet Codepage %i.", SRCNAME, __func__, codepage); } char *converted = ansi_charset_to_utf8 (plain_charset.c_str(), body.c_str(), body.size(), codepage); if (is_preview) { char *buf; gpgrt_asprintf (&buf, TEXT_PREVIEW_PLACEHOLDER, isSMIME_m () ? "S/MIME" : "OpenPGP", _("message"), _("Please wait while the message is being verified..."), converted); memdbg_alloc (buf); xfree (converted); converted = buf; } TRACEPOINT; put_oom_int (m_mailitem, "BodyFormat", 1); int ret = put_oom_string (m_mailitem, "Body", converted ? converted : ""); TRACEPOINT; xfree (converted); if (ret) { log_error ("%s:%s: Failed to modify body of item.", SRCNAME, __func__); } TRETURN; } void Mail::updateHeaders_o () { TSTART; if (!m_parser) { STRANGEPOINT; TRETURN; } const auto subject = m_parser->get_protected_header ("Subject"); if (!subject.empty ()) { put_oom_string (m_mailitem, "Subject", subject.c_str ()); } const auto to = m_parser->get_protected_header ("To"); if (!to.empty()) { put_oom_string (m_mailitem, "To", to.c_str ()); } const auto cc = m_parser->get_protected_header ("Cc"); if (!cc.empty()) { put_oom_string (m_mailitem, "CC", cc.c_str ()); } /* TODO: What about Date ? */ const auto reply_to = m_parser->get_protected_header ("Reply-To"); const auto followup_to = m_parser->get_protected_header ("Followup-To"); if (!reply_to.empty () || !followup_to.empty()) { auto recipients = MAKE_SHARED (get_oom_object (m_mailitem, "ReplyRecipents")); if (recipients) { if (!reply_to.empty ()) { invoke_oom_method_with_string (recipients.get (), "Add", reply_to.c_str ()); } if (!followup_to.empty ()) { invoke_oom_method_with_string (recipients.get (), "Add", followup_to.c_str ()); } } } const auto from = m_parser->get_protected_header ("From"); if (!from.empty ()) { LPDISPATCH sender = get_oom_object (m_mailitem, "Sender"); if (!sender) { log_debug ("%s:%s: Sender not found. From not set.", SRCNAME, __func__); TRETURN; } /* Declare that the address is SMTP */ put_oom_int (sender, "AddressEntryUserType", 30); const auto mail_start = from.find (" <"); if (mail_start == std::string::npos) { put_oom_string (sender, "Address", from.c_str ()); put_oom_string (sender, "Name", ""); } else { put_oom_string (sender, "Address", GpgME::UserID::addrSpecFromString (from.c_str ()).c_str ()); put_oom_string (sender, "Name", from.substr (0, mail_start).c_str ()); } gpgol_release (sender); } } static int parsed_count; void Mail::parsingDone_o (bool is_preview) { TSTART; TRACEPOINT; log_oom ("Mail %p Parsing done for parser num %i: %p", this, parsed_count++, 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); TRETURN; } /* Store the results. */ m_decrypt_result = m_parser->decrypt_result (); if (is_preview) { log_dbg ("Parser is not completely done. In Preview mode."); } else { m_verify_result = m_parser->verify_result (); } /* Handle protected headers */ updateHeaders_o (); m_crypto_flags = 0; if (!m_decrypt_result.isNull()) { m_crypto_flags |= 1; } if (m_verify_result.numSignatures()) { m_crypto_flags |= 2; } TRACEPOINT; updateSigstate (); m_needs_wipe = !m_is_send_again; TRACEPOINT; /* Set categories according to the result. */ updateCategories_o (); TRACEPOINT; m_block_html = m_parser->shouldBlockHtml (); if (m_block_html) { // Just to be careful. setBlockStatus_m (); } TRACEPOINT; /* Update the body */ updateBody_o (is_preview); TRACEPOINT; m_dec_content_type = m_parser->get_content_type (); log_dbg ("Decrypted mail has content type: '%s'", m_dec_content_type.c_str ()); /* When printing we have already shown the warning. So we should not show it again but silently remove any attachments that are not hidden before our add_attachments. This also fixes an issue that when printing sometimes the child mails which are created for preview and print already have the decrypted attachments. */ checkAttachments_o (isPrint ()); /* Update attachments */ if (add_attachments_o (m_parser->get_attachments())) { log_error ("%s:%s: Failed to update attachments.", SRCNAME, __func__); } if (m_is_send_again) { log_debug ("%s:%s: I think that this is the send again of a crypto mail.", SRCNAME, __func__); /* We no longer want to be treated like a crypto mail. */ m_type = MSGTYPE_UNKNOWN; LPMESSAGE msg = get_oom_base_message (m_mailitem); if (!msg) { TRACEPOINT; } else { set_gpgol_draft_info_flags (msg, m_crypto_flags); gpgol_release (msg); } removeOurAttachments_o (); } installFolderEventHandler_o (); if (!is_preview) { log_debug ("%s:%s: Delayed invalidate to update sigstate.", SRCNAME, __func__); CloseHandle(CreateThread (NULL, 0, delayed_invalidate_ui, (LPVOID) 300, 0, NULL)); } TRACEPOINT; TRETURN; } int Mail::encryptSignStart_o () { TSTART; if (m_crypt_state != DataCollected) { log_debug ("%s:%s: invalid state %i", SRCNAME, __func__, m_crypt_state); TRETURN -1; } int flags = 0; if (!needs_crypto_m ()) { TRETURN 0; } LPMESSAGE message = get_oom_base_message (m_mailitem); if (!message) { log_error ("%s:%s: Failed to get base message.", SRCNAME, __func__); TRETURN -1; } flags = get_gpgol_draft_info_flags (message); gpgol_release (message); const auto window = get_active_hwnd (); if (m_is_gsuite) { auto att_table = mapi_create_attach_table (message, 0); int n_att_usable = count_usable_attachments (att_table); mapi_release_attach_table (att_table); /* Check for attachments if we have some abort. */ if (n_att_usable) { wchar_t *w_title = utf8_to_wchar (_( "GpgOL: Oops, G Suite Sync account detected")); wchar_t *msg = utf8_to_wchar ( _("G Suite Sync breaks outgoing crypto mails " "with attachments.\nUsing crypto and attachments " "with G Suite Sync is not supported.\n\n" "See: https://dev.gnupg.org/T3545 for details.")); MessageBoxW (window, msg, w_title, MB_ICONINFORMATION|MB_OK); xfree (msg); xfree (w_title); TRETURN -1; } } + wm_register_pending_op (this); m_do_inline = m_is_draft_encrypt ? false : m_is_gsuite ? true : opt.inline_pgp; GpgME::Protocol proto = opt.enable_smime ? GpgME::UnknownProtocol: GpgME::OpenPGP; m_crypter = std::shared_ptr (new CryptController (this, flags & 1, flags & 2, proto)); // Careful from here on we have to check every // error condition with window enabling again. disableWindow_o (); if (m_crypter->collect_data ()) { log_error ("%s:%s: Crypter for mail %p failed to collect data.", SRCNAME, __func__, this); enableWindow (); TRETURN -1; } if (!m_async_crypt_disabled) { CloseHandle(CreateThread (NULL, 0, do_crypt, (LPVOID) this, 0, NULL)); } else { log_debug ("%s:%s: Starting sync crypt", SRCNAME, __func__); do_crypt (this); } TRETURN 0; } int Mail::needs_crypto_m () const { TSTART; LPMESSAGE message = get_oom_message (m_mailitem); int ret; if (!message) { log_error ("%s:%s: Failed to get message.", SRCNAME, __func__); TRETURN false; } ret = get_gpgol_draft_info_flags (message); gpgol_release(message); TRETURN ret; } int Mail::wipe_o (bool force) { TSTART; if (!m_needs_wipe && !force) { TRETURN 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); TRETURN -1; } TRETURN -1; } else { put_oom_string (m_mailitem, "Body", ""); } m_needs_wipe = false; TRETURN 0; } int Mail::updateOOMData_o (bool for_encryption) { TSTART; char *buf = nullptr; log_debug ("%s:%s: Called. For encryption: %i", SRCNAME, __func__, for_encryption); for_encryption |= !isCryptoMail(); if (for_encryption) { /* Write out the mail to a temporary file so that all the properties are set correctly. */ HANDLE hTmpFile = INVALID_HANDLE_VALUE; char *path = get_tmp_outfile_utf8 ("gpgol_enc.dat", &hTmpFile); if (!path || hTmpFile == INVALID_HANDLE_VALUE) { STRANGEPOINT; TRETURN -1; } CloseHandle (hTmpFile); m_pass_write = true; if (oom_save_as (m_mailitem, path, olMSG)) { log_dbg ("Failed to call SaveAs."); TRETURN -1; } wchar_t *wpath = utf8_to_wchar (path); if (FAILED (DeleteFileW (wpath))) { log_err ("Failed to delete file %S", wpath); } xfree (wpath); /* Clear any leftovers */ m_plain_attachments.clear (); auto attachments = get_oom_object_s (m_mailitem, "Attachments"); int count = attachments ? get_oom_int (attachments, "Count") : 0; for (int i = 1; i <= count; i++) { std::string item = asprintf_s ("Item(%i)", i); LPDISPATCH attach = get_oom_object (attachments.get (), item.c_str ()); if (!attach) { STRANGEPOINT; continue; } m_plain_attachments.push_back (std::shared_ptr ( new Attachment (attach))); } /* 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. */ if (m_is_html_alternative) { log_debug ("%s:%s: Is html alternative mail.", SRCNAME, __func__); xfree (m_cached_html_body); m_cached_html_body = get_oom_string (m_mailitem, "HTMLBody"); } xfree (m_cached_plain_body); m_cached_plain_body = get_oom_string (m_mailitem, "Body"); if (!m_recipients_set) { m_cached_recipients = getRecipients_o (); } else { log_dbg ("Not updating cached recipients because recipients were " "set explicitly."); } } else { /* 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. */ buf = get_sender_SenderEMailAddress (m_mailitem); if (!buf) { /* Try the sender Object */ buf = get_sender_Sender (m_mailitem); } /* We also want to cache sent representing email address so that we can use it for verification information. */ char *buf2 = get_sender_SentRepresentingAddress (m_mailitem); if (buf2) { m_sent_on_behalf = buf2; xfree (buf2); } } if (!buf) { buf = get_sender_SendUsingAccount (m_mailitem, &m_is_gsuite); } if (!isCryptoMail ()) { /* Try the sender Object and handle the case that someone changed the identity but not the account. */ char *buf2 = get_sender_Sender (m_mailitem); char *tmp = buf; if (buf && buf2) { if (*buf == '/' && *buf2 != '/' && *buf2) { log_dbg ("Send account could not be resolved. Using sender."); buf = buf2; xfree (tmp); } else if (*buf && *buf != '/' && *buf2 && *buf2 != '/') { log_dbg ("SendUsingAccount: '%s' Sender: '%s'", anonstr (buf), anonstr (buf2)); auto key = KeyCache::instance()->getSigningKey (buf2, GpgME::OpenPGP); if (!key.isNull()) { log_dbg ("Found OpenPGP Key for %s using that identity", anonstr (buf2)); buf = buf2; xfree (tmp); } else if (opt.enable_smime) { key = KeyCache::instance()->getSigningKey (buf2, GpgME::CMS); log_dbg ("Found S/MIME Key for %s using that identity", anonstr (buf2)); buf = buf2; xfree (tmp); } } } else if (!buf) { buf = buf2; } else if (buf2 && buf != buf2) { log_dbg ("Keeping SendUsingAccount %s", anonstr (buf)); xfree (buf2); } } if (!buf) { /* We don't have s sender object or SendUsingAccount, well, in that case fall back to the current user. */ buf = get_sender_CurrentUser (m_mailitem); } if (!buf) { log_debug ("%s:%s: All fallbacks failed.", SRCNAME, __func__); TRETURN -1; } m_sender = buf; if (for_encryption) { bool sender_is_recipient = false; for (const auto &recp: m_cached_recipients) { if (recp.mbox () == m_sender) { sender_is_recipient = true; log_debug ("%s:%s: Sender already recipient.", SRCNAME, __func__); break; } } if (!sender_is_recipient) { m_cached_recipients.push_back (Recipient (buf, Recipient::olOriginator)); } } xfree (buf); TRETURN 0; } std::string Mail::getSender_o () { TSTART; if (m_sender.empty()) updateOOMData_o (); TRETURN m_sender; } std::string Mail::getSender () const { TSTART; TRETURN m_sender; } int Mail::closeAllMails_o () { TSTART; int err = 0; /* Detach Folder sinks */ for (auto fit = s_folder_events_map.begin(); fit != s_folder_events_map.end(); ++fit) { detach_FolderEvents_sink (fit->second); gpgol_release (fit->second); } s_folder_events_map.clear(); std::map::iterator it; TRACEPOINT; gpgol_lock (&mail_map_lock); std::map mail_map_copy = s_mail_map; gpgol_unlock (&mail_map_lock); for (it = mail_map_copy.begin(); it != mail_map_copy.end(); ++it) { /* XXX For non racy code the is_valid_ptr check should not be necessary but we crashed sometimes closing a destroyed mail. */ if (!isValidPtr (it->second)) { log_debug ("%s:%s: Already deleted mail for %p", SRCNAME, __func__, it->first); continue; } if (!it->second->isCryptoMail ()) { continue; } bool close_failed = false; if (closeInspector_o (it->second)) { log_error ("%s:%s: Failed to close mail inspector: %p ", SRCNAME, __func__, it->first); close_failed = true; } if (isValidPtr (it->second)) { log_debug ("%s:%s: Inspector closed for %p closing object.", SRCNAME, __func__, it->first); if (it->second->close ()) { log_error ("%s:%s: Failed to close mail itself: %p ", SRCNAME, __func__, it->first); close_failed = true; } } else { log_debug ("%s:%s: Mail gone after inspector close.", SRCNAME, __func__); close_failed = false; } /* Beware: The close code removes our NotStarted from the Outlook Object Model and temporary MAPI. If there is an error we might put NotStarted into permanent storage and leak it to the server. So we have an extra safeguard below. The revert is likely to fail if close and closeInspector fails but to guard against a bug in our close code we try it anyway as revert will also try to remove the plaintext from memory and restore the original message. */ if (close_failed) { if (isValidPtr (it->second) && it->second->revert_o ()) { err++; } } } TRETURN err; } int Mail::revertAllMails_o () { TSTART; int err = 0; std::map::iterator it; gpgol_lock (&mail_map_lock); auto mail_map_copy = s_mail_map; gpgol_unlock (&mail_map_lock); for (it = mail_map_copy.begin(); it != mail_map_copy.end(); ++it) { if (it->second->revert_o ()) { log_error ("Failed to revert mail: %p ", it->first); err++; continue; } it->second->setNeedsSave (true); if (!invoke_oom_method (it->first, "Save", NULL)) { log_error ("Failed to save reverted mail: %p ", it->second); err++; continue; } } TRETURN err; } int Mail::wipeAllMails_o () { TSTART; int err = 0; std::map::iterator it; gpgol_lock (&mail_map_lock); auto mail_map_copy = s_mail_map; gpgol_unlock (&mail_map_lock); for (it = mail_map_copy.begin(); it != mail_map_copy.end(); ++it) { if (it->second->wipe_o ()) { log_error ("Failed to wipe mail: %p ", it->first); err++; } } TRETURN err; } int Mail::revert_o () { TSTART; int err = 0; if (!m_processed) { TRETURN 0; } m_disable_att_remove_warning = true; err = gpgol_mailitem_revert (m_mailitem); if (err == -1) { log_error ("%s:%s: Message revert failed falling back to wipe.", SRCNAME, __func__); TRETURN wipe_o (); } /* We need to reprocess the mail next time around. */ m_processed = false; m_needs_wipe = false; m_disable_att_remove_warning = false; TRETURN 0; } bool Mail::isSMIME () const { if (!m_is_smime_checked) { log_dbg ("WARNING: SMIME check before isSMIME_m was called."); return false; } return m_is_smime; } bool Mail::isSMIME_m () { TSTART; msgtype_t msgtype; LPMESSAGE message; if (m_is_smime_checked) { TRETURN m_is_smime; } message = get_oom_message (m_mailitem); if (!message) { log_error ("%s:%s: No message?", SRCNAME, __func__); TRETURN false; } msgtype = mapi_get_message_type (message); m_is_smime = msgtype == MSGTYPE_GPGOL_OPAQUE_ENCRYPTED || msgtype == MSGTYPE_GPGOL_OPAQUE_SIGNED || msgtype == MSGTYPE_SMIME; /* 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 ("%s:%s: No protocol in multipart / signed mail.", SRCNAME, __func__); } xfree (proto); xfree (ct); } gpgol_release (message); m_is_smime_checked = true; log_debug ("%s:%s: Detected %s mail", SRCNAME, __func__, m_is_smime ? "S/MIME" : "not S/MIME"); TRETURN m_is_smime; } static std::string get_string_o (LPDISPATCH item, const char *str) { TSTART; char *buf = get_oom_string (item, str); if (!buf) { TRETURN std::string(); } std::string ret = buf; xfree (buf); TRETURN ret; } std::string Mail::getSubject_o () const { TSTART; TRETURN get_string_o (m_mailitem, "Subject"); } std::string Mail::getBody_o () const { TSTART; TRETURN get_string_o (m_mailitem, "Body"); } std::vector Mail::getRecipients_o () const { TSTART; LPDISPATCH recipients = get_oom_object (m_mailitem, "Recipients"); if (!recipients) { TRACEPOINT; std::vector(); } bool err = false; auto ret = get_oom_recipients (recipients, &err); gpgol_release (recipients); if (err) { log_debug ("%s:%s: Failed to resolve recipients at this time.", SRCNAME, __func__); } TRETURN ret; } int Mail::closeInspector_o (Mail *mail) { TSTART; LPDISPATCH inspector = get_oom_object (mail->item(), "GetInspector"); HRESULT hr; DISPID dispid; if (!inspector) { log_debug ("%s:%s: No inspector.", SRCNAME, __func__); TRETURN -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); gpgol_release (inspector); TRETURN -1; } } gpgol_release (inspector); TRETURN 0; } int Mail::close (bool restoreSMIMEClass) { TSTART; wm_after_move_data_t *move_data = nullptr; 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; /* Remove the mail from the print mail set. So that categories etc work again. */ const auto ourID = get_oom_string_s (m_mailitem, "EntryID"); if (!ourID.empty ()) { const auto it = s_entry_ids_printing.find (ourID); if (it != s_entry_ids_printing.end ()) { log_dbg ("Found %s in printing map. Removing it", ourID.c_str ()); s_entry_ids_printing.erase (it); } } if (isSMIME_m ()) { LPDISPATCH attachments = get_oom_object (m_mailitem, "Attachments"); LPMESSAGE mapi_msg = get_oom_message (m_mailitem); if (!mapi_msg) { log_error ("%s:%s:Failed to obtain mapi message for %p on close.", SRCNAME, __func__, m_mailitem); } if (m_gpgol_class.empty()) { log_debug ("%s:%s: GpgOL Class empty for S/MIME Mail.", SRCNAME, __func__); } /* This is strangely important. Because Outlook apparently treats * S/MIME Mails differently we need to change our message class * here again so that discard changes works properly. */ if (mapi_msg && !m_gpgol_class.empty ()) { char *current = mapi_get_message_class (mapi_msg); if (current && strcmp (current, m_gpgol_class.c_str ())) { log_debug ("%s:%s:Setting message class to %s on close.", SRCNAME, __func__, m_gpgol_class.c_str ()); mapi_set_mesage_class (mapi_msg, m_gpgol_class.c_str ()); if (restoreSMIMEClass) { /* After the close we need to change the message class back again so as to not break compatibility with other clients. */ move_data = (wm_after_move_data_t *) xmalloc (sizeof (wm_after_move_data_t)); size_t entryIDLen = 0; char *entryID = nullptr; entryID = mapi_get_binary_prop (mapi_msg, PR_ENTRYID, &entryIDLen); LPDISPATCH folder = get_oom_object (m_mailitem, "Parent"); if (folder) { char *name = get_object_name ((LPUNKNOWN) folder); if (!name || strcmp (name, "MAPIFolder")) { log_error ("%s:%s: Failed to obtain folder on close object is %s", SRCNAME, __func__, name ? name : "(null)"); } else { xfree (name); move_data->target_folder = (LPMAPIFOLDER) get_oom_iunknown ( folder, "MAPIOBJECT"); if (!move_data->target_folder) { log_error ("%s:%s: Failed to obtain target folder.", SRCNAME, __func__); xfree (entryID); xfree (current); xfree (move_data); move_data = nullptr; } else { memdbg_addRef (move_data->target_folder); } } } move_data->entry_id = entryID; move_data->entry_id_len = entryIDLen; move_data->old_class = current; } } } if (attachments) { int count = get_oom_int (attachments, "Count"); gpgol_release (attachments); if (count) { /* On exchange Outlook sometimes detects that an S/MIME mail * is an S/MIME Mail. When this mail is then modified by * us and the mail should be moved or closed Outlook will try * to save it. This fails and the user gets an error. * * So we save here, which should not be dangerous as we do not * put plaintext in mapi. * * Still better only do it if it is really * necessary as the changed message class can hurt. * * Tests show no plaintext leaks. The save saves the * message class and in that way outlook no longer * thinks the mails are S/MIME mails and we can * use our own handling. See T4525 */ removeCategories_o (); HRESULT hr = 0; if (mapi_msg) { log_debug ("%s:%s: MAPI Save for: %p", SRCNAME, __func__, m_mailitem); mapi_msg->SaveChanges (KEEP_OPEN_READWRITE); } if (!mapi_msg || hr) { log_error ("%s:%s: Failed to save mapi for %p hr=%#lx", SRCNAME, __func__, this, hr); } /* In case the mail is still visible in a different window */ updateCategories_o (); } } gpgol_release (mapi_msg); } log_oom ("%s:%s: Invoking close for: %p", SRCNAME, __func__, m_mailitem); setCloseTriggered (true); int rc = invoke_oom_method_with_parms (m_mailitem, "Close", nullptr, &dispparams); if (move_data) { log_debug ("%s:%s:Restoring message class to %s after close.", SRCNAME, __func__, move_data->old_class); do_in_ui_thread_async (AFTER_MOVE, move_data, 0); } setCloseTriggered (false); if (!rc) { /* Saveguard against oom writes when our data is in OOM. This * can happen when the mail was opened in a new window. But * also shown in the preview. * We get a close event and discard changes but the data is * still in oom because it is still visible in the opened * window. * * In that case we may not write! Otherwise the plaintext * might be leaked back to the server if the folder is synced. * */ char *body = get_oom_string (m_mailitem, "Body"); LPDISPATCH attachments = get_oom_object (m_mailitem, "Attachments"); if (body && strlen (body)) { log_debug ("%s:%s: Close successful. But body found. " "Mail still open.", SRCNAME, __func__); } else if (count_visible_attachments (attachments)) { log_debug ("%s:%s: Close successful. But attachments found. " "Mail still open.", SRCNAME, __func__); } else { setPassWrite (true); log_debug ("%s:%s: Close successful. Next write may pass.", SRCNAME, __func__); } gpgol_release (attachments); xfree (body); } log_oom ("%s:%s: returned from close", SRCNAME, __func__); TRETURN rc; } void Mail::setCloseTriggered (bool value) { TSTART; m_close_triggered = value; TRETURN; } bool Mail::getCloseTriggered () const { TSTART; TRETURN m_close_triggered; } static const UserID get_uid_for_sender (const Key &k, const char *sender) { TSTART; UserID ret; if (!sender) { TRETURN ret; } if (!k.numUserIDs()) { log_debug ("%s:%s: Key without uids", SRCNAME, __func__); TRETURN ret; } for (const auto uid: k.userIDs()) { if (!uid.email() || !*(uid.email())) { /* This happens for S/MIME a lot */ log_debug ("%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__, anonstr (uid.email()), anonstr (sender)); continue; } if (normalized_sender == normalized_uid) { ret = uid; } } TRETURN ret; } void Mail::updateSigstate () { TSTART; std::string sender = getSender (); if (sender.empty()) { log_error ("%s:%s:%i", SRCNAME, __func__, __LINE__); TRETURN; } if (m_verify_result.isNull()) { log_debug ("%s:%s: No verify result.", SRCNAME, __func__); TRETURN; } if (m_verify_result.error()) { log_debug ("%s:%s: verify error.", SRCNAME, __func__); TRETURN; } for (const auto sig: m_verify_result.signatures()) { m_is_signed = true; const auto key = KeyCache::instance ()->getByFpr (sig.fingerprint(), true); m_uid = get_uid_for_sender (key, sender.c_str()); if (m_uid.isNull() && !m_sent_on_behalf.empty ()) { m_uid = get_uid_for_sender (key, m_sent_on_behalf.c_str ()); if (!m_uid.isNull()) { log_debug ("%s:%s: Using sent on behalf '%s' instead of '%s'", SRCNAME, __func__, anonstr (m_sent_on_behalf.c_str()), anonstr (sender.c_str ())); } } /* Sigsum valid or green is somehow not set in this case. */ if (!sig.status() && m_uid.origin() == GpgME::Key::OriginWKD && (sig.validity() == Signature::Validity::Unknown || sig.validity() == Signature::Validity::Marginal)) { // WKD is a shortcut to Level 2 trust. log_debug ("%s:%s: Unknown or marginal from WKD -> Level 2", SRCNAME, __func__); } else 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; } else 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 origin: %i", SRCNAME, __func__, m_uid.validity(), m_uid.origin()); m_sig = sig; m_is_valid = true; TRETURN; } log_debug ("%s:%s: No signature with enough trust. Using first", SRCNAME, __func__); m_sig = m_verify_result.signature(0); TRETURN; } bool Mail::isValidSig () const { TSTART; TRETURN m_is_valid; } void Mail::removeCategories_o () { TSTART; if (!m_store_id.empty () && !m_verify_category.empty ()) { log_oom ("%s:%s: Unreffing verify category", SRCNAME, __func__); CategoryManager::instance ()->removeCategory (this, m_verify_category); } if (!m_store_id.empty () && !m_decrypt_result.isNull()) { log_oom ("%s:%s: Unreffing dec category", SRCNAME, __func__); CategoryManager::instance ()->removeCategory (this, CategoryManager::getEncMailCategory ()); } if (m_is_junk) { log_oom ("%s:%s: Unreffing junk category", SRCNAME, __func__); CategoryManager::instance ()->removeCategory (this, CategoryManager::getJunkMailCategory ()); } TRETURN; } /* 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 () { TSTART; 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. */ TRETURN; } if (!wnd) { TRACEPOINT; TRETURN; } RECT oldpos; if (!GetWindowRect (wnd, &oldpos)) { TRACEPOINT; TRETURN; } 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; TRETURN; } if (!SetWindowPos (wnd, nullptr, (int)oldpos.left, (int)oldpos.top, (int)oldpos.right - oldpos.left, (int)oldpos.bottom - oldpos.top, 0)) { TRACEPOINT; TRETURN; } resized_windows.push_back(wnd); TRETURN; } #if 0 static std::string pretty_id (const char *keyId) { /* Three spaces, four quads and a NULL */ char buf[20]; buf[19] = '\0'; if (!keyId) { return std::string ("null"); } size_t len = strlen (keyId); if (!len) { return std::string ("empty"); } if (len < 16) { return std::string (_("Invalid Key")); } const char *p = keyId + (len - 16); int j = 0; for (size_t i = 0; i < 16; i++) { if (i && i % 4 == 0) { buf[j++] = ' '; } buf[j++] = *(p + i); } return std::string (buf); } #endif void Mail::updateCategories_o () { TSTART; if (m_printing) { log_debug ("%s:%s: Not updating categories as we are printing.", SRCNAME, __func__); return; } auto mngr = CategoryManager::instance (); if (isValidSig ()) { char *buf; /* Resolve to the primary fingerprint */ #if 0 const auto sigKey = KeyCache::instance ()->getByFpr (m_sig.fingerprint (), true); const char *sigFpr; if (sigKey.isNull()) { sigFpr = m_sig.fingerprint (); } else { sigFpr = sigKey.primaryFingerprint (); } #endif /* If m_uid addrSpec would not return a result we would never * have gotten the UID. */ int lvl = get_signature_level (); /* TRANSLATORS: The first placeholder is for tranlsation of "Level". The second one is for the level number. The third is for the translation of "trust in" and the last one is for the mail address used for verification. The result is used as the text on the green bar for signed mails. e.g.: "GpgOL: Level 3 trust in 'john.doe@example.org'" */ gpgrt_asprintf (&buf, "GpgOL: %s %i %s '%s'", _("Level"), lvl, _("trust in"), m_uid.addrSpec ().c_str ()); memdbg_alloc (buf); int color = 0; if (lvl == 2) { color = 7; /* Olive */ } if (lvl == 3) { color = 5; /* Green */ } if (lvl == 4) { color = 20; /* Dark Green */ } m_store_id = mngr->addCategoryToMail (this, buf, color); m_verify_category = buf; xfree (buf); } else { remove_category (m_mailitem, "GpgOL: ", false); } if (!m_decrypt_result.isNull()) { const auto id = mngr->addCategoryToMail (this, CategoryManager::getEncMailCategory (), 8 /* Blue */); if (m_store_id.empty()) { m_store_id = id; } if (m_store_id != id) { log_error ("%s:%s unexpected store mismatch " "between '%s' and dec cat '%s'", SRCNAME, __func__, m_store_id.c_str(), id.c_str()); } } else { /* As a small safeguard against fakes we remove our categories */ remove_category (m_mailitem, CategoryManager::getEncMailCategory ().c_str (), true); } resize_active_window(); TRETURN; } bool Mail::isSigned () const { TSTART; TRETURN m_verify_result.numSignatures() > 0; } bool Mail::isEncrypted () const { TSTART; TRETURN !m_decrypt_result.isNull(); } int Mail::setUUID_o (bool reset) { TSTART; char *uuid; if (reset) { m_uuid = std::string (); uuid = reset_unique_id (m_mailitem); } else 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: Setting uuid for %p to %s again in MAPI", 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); TRETURN -1; } if (m_uuid.empty()) { m_uuid = uuid; Mail *other = getMailForUUID (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; } gpgol_lock (&uid_map_lock); s_uid_map.insert (std::pair (m_uuid, this)); gpgol_unlock (&uid_map_lock); log_debug ("%s:%s: uuid for %p is now %s", SRCNAME, __func__, this, m_uuid.c_str()); } xfree (uuid); TRETURN 0; } /* TRETURNs 2 if the userid is ultimately trusted. TRETURNs 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) { TSTART; if (uid.isNull()) { TRETURN 0; } if (uid.validity () == UserID::Validity::Ultimate) { TRETURN 2; } if (uid.validity () == UserID::Validity::Full) { const auto ultimate_keys = KeyCache::instance()->getUltimateKeys (); 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: 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__, anonstr (signer_uid_str), anonstr (sig_uid_str), anonstr (secKeyID)); TRETURN 1; } } } } } TRETURN 0; } std::string Mail::getCryptoSummary () const { TSTART; const int level = get_signature_level (); bool enc = isEncrypted (); if (level == 4 && enc) { TRETURN _("Security Level 4"); } if (level == 4) { TRETURN _("Trust Level 4"); } if (level == 3 && enc) { TRETURN _("Security Level 3"); } if (level == 3) { TRETURN _("Trust Level 3"); } if (level == 2 && enc) { TRETURN _("Security Level 2"); } if (level == 2) { TRETURN _("Trust Level 2"); } if (enc) { TRETURN _("Encrypted"); } if (isSigned ()) { /* 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. */ TRETURN _("Insecure"); } TRETURN _("Insecure"); } std::string Mail::getCryptoOneLine () const { TSTART; bool sig = isSigned (); bool enc = isEncrypted (); if (sig || enc) { if (sig && enc) { TRETURN _("Signed and encrypted message"); } else if (sig) { TRETURN _("Signed message"); } else if (enc) { TRETURN _("Encrypted message"); } } TRETURN _("Insecure message"); } std::string Mail::getCryptoDetails_o () { TSTART; std::string message; /* No signature with keys but error */ if (!isEncrypted () && !isSigned () && 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(); TRETURN message; } /* No crypo, what are we doing here? */ if (!isEncrypted () && !isSigned ()) { TRETURN _("You cannot be sure who sent, " "modified and read the message in transit."); } /* Handle encrypt only */ if (isEncrypted () && !isSigned ()) { if (in_de_vs_mode ()) { message += compliance_string (false, true, m_decrypt_result.isDeVs ()); } message += "\n\n"; message += _("You cannot be sure who sent the message because " "it is not signed."); TRETURN message; } bool keyFound = true; const auto sigKey = KeyCache::instance ()->getByFpr (m_sig.fingerprint (), true); bool isOpenPGP = sigKey.isNull() ? !isSMIME_m () : sigKey.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 && sigKey.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__); TRETURN 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"), sigKey.issuerName()); memdbg_alloc (buf); message = buf; xfree (buf); } else if (level == 2 && m_uid.origin () == GpgME::Key::OriginWKD) { message = _("The mail provider of the recipient served this key."); } 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 ()); memdbg_alloc (buf); 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); memdbg_alloc (buf); 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 = isEncrypted () ? _("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"); if (m_sig.status().code() == GPG_ERR_BAD_SIGNATURE) { message += std::string("\n") + _("The signature does not match."); return message; } } else if (m_sig.summary() & Signature::Summary::SysError || m_verify_result.numSignatures() < 1) { message += _("There was an error verifying the signature.\n"); const auto err = m_sig.status (); if (err) { message += err.asString () + std::string ("\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\"."), getSender_o ().c_str()); memdbg_alloc (buf); 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 ()) { bool isBoth = isSigned () && m_sig.isDeVs() && isEncrypted () && m_decrypt_result.isDeVs (); if (isBoth) { message += compliance_string (true, true, true); } if (!isBoth && isSigned ()) { message += compliance_string (true, false, m_sig.isDeVs ()); message += "\n"; } if (!isBoth && isEncrypted ()) { message += compliance_string (false, true, m_decrypt_result.isDeVs ()); message += "\n"; 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."); } TRETURN message; } int Mail::get_signature_level () const { TSTART; if (!m_is_signed) { TRETURN 0; } if (m_uid.isNull ()) { /* No m_uid matches our sender. */ TRETURN 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())) { TRETURN 4; } if (m_is_valid && m_uid.validity () == UserID::Validity::Full && (!in_de_vs_mode () || m_sig.isDeVs())) { TRETURN 3; } if (m_is_valid) { TRETURN 2; } if (m_sig.validity() == Signature::Validity::Marginal) { TRETURN 1; } if (m_sig.summary() & Signature::Summary::TofuConflict || m_uid.tofuInfo().validity() == TofuInfo::Conflict) { TRETURN 0; } TRETURN 0; } int Mail::getCryptoIconID () const { TSTART; int level = get_signature_level (); int offset = isEncrypted () ? ENCRYPT_ICON_OFFSET : 0; TRETURN IDI_LEVEL_0 + level + offset; } const char* Mail::getSigFpr () const { TSTART; if (!m_is_signed || m_sig.isNull()) { TRETURN nullptr; } TRETURN m_sig.fingerprint(); } /** Try to locate the keys for all recipients */ void Mail::locateKeys_o () { TSTART; if (m_locate_in_progress) { /** XXX The strangest thing seems to happen here: In get_recipients the lookup for "AddressEntry" on an unresolved address might cause network traffic. So Outlook somehow "detaches" this call and keeps processing window messages while the call is running. So our do_delayed_locate might trigger a second locate. If we access the OOM in this call while we access the same object in the blocked "detached" call we crash. (T3931) After the window message is handled outlook retunrs in the original lookup. A better fix here might be a non recursive lock of the OOM. But I expect that if we lock the handling of the Windowmessage we might deadlock. */ log_debug ("%s:%s: Locate for %p already in progress.", SRCNAME, __func__, this); TRETURN; } m_locate_in_progress = true; Addressbook::check_o (this); if (opt.autoresolve) { // First update oom data to have recipients and sender updated. updateOOMData_o (); KeyCache::instance()->startLocateSecret (getSender_o ().c_str (), this); KeyCache::instance()->startLocate (getSender_o ().c_str (), this); KeyCache::instance()->startLocate (getCachedRecipientAddresses (), this); } autosecureCheck (); m_locate_in_progress = false; TRETURN; } bool Mail::isHTMLAlternative () const { TSTART; TRETURN m_is_html_alternative; } char * Mail::takeCachedHTMLBody () { TSTART; char *ret = m_cached_html_body; m_cached_html_body = nullptr; TRETURN ret; } char * Mail::takeCachedPlainBody () { TSTART; char *ret = m_cached_plain_body; m_cached_plain_body = nullptr; TRETURN ret; } int Mail::getCryptoFlags () const { TSTART; TRETURN m_crypto_flags; } void Mail::setNeedsEncrypt (bool value) { TSTART; m_needs_encrypt = value; TRETURN; } bool Mail::getNeedsEncrypt () const { TSTART; TRETURN m_needs_encrypt; } std::vector Mail::getCachedRecipients () { TSTART; TRETURN m_cached_recipients; } void Mail::setRecipients (const std::vector &recps) { TSTART; m_recipients_set = !recps.empty (); m_cached_recipients = recps; TRETURN; } std::vector Mail::getCachedRecipientAddresses () { TSTART; std::vector ret; for (const auto &recp: m_cached_recipients) { ret.push_back (recp.mbox()); } return ret; } void Mail::appendToInlineBody (const std::string &data) { TSTART; m_inline_body += data; TRETURN; } int Mail::inlineBodyToBody_o () { TSTART; if (!m_crypter) { log_error ("%s:%s: No crypter.", SRCNAME, __func__); TRETURN -1; } const auto body = m_crypter->get_inline_data (); if (body.empty()) { TRETURN 0; } /* For inline we always work with UTF-8 */ put_oom_int (m_mailitem, "InternetCodepage", 65001); int ret = put_oom_string (m_mailitem, "Body", body.c_str ()); TRETURN ret; } void Mail::updateCryptMAPI_m () { TSTART; log_debug ("%s:%s: Update crypt mapi", SRCNAME, __func__); if (m_crypt_state != OOMSynced) { log_debug ("%s:%s: invalid state %i", SRCNAME, __func__, m_crypt_state); TRETURN; } if (!m_crypter) { if (!m_mime_data.empty()) { log_debug ("%s:%s: Have override mime data creating dummy crypter", SRCNAME, __func__); m_crypter = std::shared_ptr (new CryptController (this, false, false, GpgME::UnknownProtocol)); } else { log_error ("%s:%s: No crypter.", SRCNAME, __func__); m_crypt_state = NotStarted; TRETURN; } } if (m_crypter->update_mail_mapi ()) { log_error ("%s:%s: Failed to update MAPI after crypt", SRCNAME, __func__); m_crypt_state = NotStarted; } else { m_crypt_state = CryptFinished; } /** If sync we need the crypter in update_crypt_oom */ if (!isAsyncCryptDisabled ()) { // We don't need the crypter anymore. resetCrypter (); } TRETURN; } /** Checks in OOM if the body is either empty or contains the -----BEGIN tag. pair.first -> true if body starts with -----BEGIN pair.second -> true if body is empty. */ static std::pair has_crypt_or_empty_body_oom (Mail *mail) { TSTART; auto body = mail->getBody_o (); std::pair ret; ret.first = false; ret.second = false; ltrim (body); if (body.size() > 10 && !strncmp (body.c_str(), "-----BEGIN", 10)) { ret.first = true; TRETURN ret; } if (!body.size()) { ret.second = true; } else { log_data ("%s:%s: Body found in %p : \"%s\"", SRCNAME, __func__, mail, body.c_str ()); } TRETURN ret; } void Mail::updateCryptOOM_o () { TSTART; log_debug ("%s:%s: Update crypt oom for %p", SRCNAME, __func__, this); if (m_crypt_state != BackendDone) { log_debug ("%s:%s: invalid state %i", SRCNAME, __func__, m_crypt_state); resetCrypter (); resetRecipients (); TRETURN; } if (getDoPGPInline ()) { if (inlineBodyToBody_o ()) { log_error ("%s:%s: Inline body to body failed %p.", SRCNAME, __func__, this); gpgol_bug (get_active_hwnd(), ERR_INLINE_BODY_TO_BODY); m_crypt_state = NotStarted; TRETURN; } } if (m_crypter->get_protocol () == GpgME::CMS && m_crypter->is_encrypter ()) { /* We put the PIDNameContentType headers here for exchange because this is the only way we found to inject the smime-type. */ if (put_pa_string (m_mailitem, PR_PIDNameContentType_DASL, "application/pkcs7-mime;smime-type=\"enveloped-data\";name=smime.p7m")) { log_debug ("%s:%s: Failed to put PIDNameContentType for %p.", SRCNAME, __func__, this); } } if (get_oom_int (m_mailitem, "BodyFormat") == 3) { log_dbg ("Changing body format from rich text to HTML to " "prevent winmail.dat"); put_oom_int (m_mailitem, "BodyFormat", 2); } /** When doing async update_crypt_mapi follows and needs the crypter. */ if (isAsyncCryptDisabled ()) { resetCrypter (); resetRecipients (); } const auto pair = has_crypt_or_empty_body_oom (this); if (pair.first) { log_debug ("%s:%s: Looks like inline body. You can pass %p.", SRCNAME, __func__, this); m_crypt_state = CryptFinished; TRETURN; } /* Draft encryption we do not want to wipe the oom but we have to modify it to trigger the wirte / second after write. */ if (m_is_draft_encrypt) { char *subject = get_oom_string (m_mailitem, "Subject"); put_oom_string (m_mailitem, "Subject", subject ? subject : ""); xfree (subject); } // We are in MIME land. Wipe the body. if (!m_is_draft_encrypt && wipe_o (true)) { log_debug ("%s:%s: Cancel send for %p.", SRCNAME, __func__, this); wchar_t *title = utf8_to_wchar (_("GpgOL: Encryption not possible!")); wchar_t *msg = utf8_to_wchar (_( "Outlook returned an error when trying to send the encrypted mail.\n\n" "Please restart Outlook and try again.\n\n" "If it still fails consider using an encrypted attachment or\n" "switching to PGP/Inline in GpgOL's options.")); MessageBoxW (get_active_hwnd(), msg, title, MB_ICONERROR | MB_OK); xfree (msg); xfree (title); m_crypt_state = NotStarted; TRETURN; } m_crypt_state = OOMUpdated; TRETURN; } void Mail::enableWindow () { TSTART; if (!m_window) { log_error ("%s:%s:enable window which was not disabled", SRCNAME, __func__); } log_debug ("%s:%s: enable window %p", SRCNAME, __func__, m_window); EnableWindow (m_window, TRUE); TRETURN; } void Mail::disableWindow_o () { TSTART; m_window = get_active_hwnd (); log_debug ("%s:%s: disable window %p", SRCNAME, __func__, m_window); EnableWindow (m_window, FALSE); TRETURN; } bool Mail::isActiveInlineResponse_o () { const auto subject = getSubject_o (); bool ret = false; LPDISPATCH app = GpgolAddin::get_instance ()->get_application (); if (!app) { TRACEPOINT; TRETURN false; } LPDISPATCH explorer = get_oom_object (app, "ActiveExplorer"); if (!explorer) { TRACEPOINT; TRETURN false; } LPDISPATCH inlineResponse = get_oom_object (explorer, "ActiveInlineResponse"); gpgol_release (explorer); if (!inlineResponse) { TRETURN false; } // We have inline response // Check if we are it. It's a bit naive but meh. Worst case // is that we think inline response too often and do sync // crypt where we could do async crypt. char * inlineSubject = get_oom_string (inlineResponse, "Subject"); gpgol_release (inlineResponse); if (inlineResponse && !subject.empty() && !strcmp (subject.c_str (), inlineSubject)) { log_debug ("%s:%s: Detected inline response for '%p'", SRCNAME, __func__, this); ret = true; } xfree (inlineSubject); TRETURN ret; } bool Mail::checkSyncCrypto_o () { TSTART; /* Async sending is known to cause instabilities. So we keep a hidden option to disable it. */ if (opt.sync_enc) { m_async_crypt_disabled = true; TRETURN m_async_crypt_disabled; } m_async_crypt_disabled = false; if (m_is_draft_encrypt) { log_dbg ("Disabling async crypt because of draft encrypt."); m_async_crypt_disabled = true; TRETURN m_async_crypt_disabled; } const auto subject = getSubject_o (); /* Check for an empty subject. Otherwise the question for it might be hidden behind our overlay. */ if (subject.empty()) { log_debug ("%s:%s: Detected empty subject. " "Disabling async crypt due to T4150.", SRCNAME, __func__); m_async_crypt_disabled = true; TRETURN m_async_crypt_disabled; } LPDISPATCH attachments = get_oom_object (m_mailitem, "Attachments"); if (/* attachments */ false) { /* This is horrible. But. For some kinds of attachments (we got reports about Office attachments the write in the send event triggered by our crypto done code fails with an exception. There does not appear to be a detectable pattern when this happens. As we can't be sure and do not know for which attachments this really happens we do not use async crypt for any mails with attachments. :-/ Better be save (not crash) instead of nice (async). TODO: Figure this out. The log goes like this. We pass the send event. That triggers a write, which we pass. And then that fails. So it looks like moving to Outbox fails. Because we can save as much as we like before that. Using the IMessage::SubmitMessage MAPI interface works, but as it is unstable in our current implementation we do not want to use it. mailitem-events.cpp:Invoke: Passing send event for mime-encrypted message 12B7C6E0. application-events.cpp:Invoke: Unhandled Event: f002 mailitem-events.cpp:Invoke: Write : 0ED4D058 mailitem-events.cpp:Invoke: Passing write event. oomhelp.cpp:invoke_oom_method_with_parms_type: Method 'Send' invokation failed: 0x80020009 oomhelp.cpp:dump_excepinfo: Exception: wCode: 0x1000 wReserved: 0x0 source: Microsoft Outlook desc: The operation failed. The messaging interfaces have returned an unknown error. If the problem persists, restart Outlook. help: null helpCtx: 0x0 deferredFill: 00000000 scode: 0x80040119 */ int count = get_oom_int (attachments, "Count"); gpgol_release (attachments); if (count) { m_async_crypt_disabled = true; log_debug ("%s:%s: Detected attachments. " "Disabling async crypt due to T4131.", SRCNAME, __func__); TRETURN m_async_crypt_disabled; } } if (isActiveInlineResponse_o ()) { m_async_crypt_disabled = true; } TRETURN m_async_crypt_disabled; } // static Mail * Mail::getLastMail () { TSTART; if (!s_last_mail || !isValidPtr (s_last_mail)) { s_last_mail = nullptr; } TRETURN s_last_mail; } // static void Mail::clearLastMail () { TSTART; s_last_mail = nullptr; TRETURN; } // static void Mail::locateAllCryptoRecipients_o () { TSTART; gpgol_lock (&mail_map_lock); std::map::iterator it; auto mail_map_copy = s_mail_map; gpgol_unlock (&mail_map_lock); for (it = mail_map_copy.begin(); it != mail_map_copy.end(); ++it) { if (it->second->needs_crypto_m ()) { it->second->locateKeys_o (); } } TRETURN; } int Mail::removeAllAttachments_o () { TSTART; int ret = 0; LPDISPATCH attachments = get_oom_object (m_mailitem, "Attachments"); if (!attachments) { TRACEPOINT; TRETURN 0; } int count = get_oom_int (attachments, "Count"); LPDISPATCH to_delete[count]; /* Populate the array so that we don't get in an index mess */ for (int i = 1; i <= count; i++) { auto item_str = std::string("Item(") + std::to_string (i) + ")"; to_delete[i-1] = get_oom_object (attachments, item_str.c_str()); } gpgol_release (attachments); /* Now delete all attachments */ for (int i = 0; i < count; i++) { LPDISPATCH attachment = to_delete[i]; if (!attachment) { log_error ("%s:%s: No such attachment %i", SRCNAME, __func__, i); ret = -1; } /* Delete the attachments that are marked to delete */ if (invoke_oom_method (attachment, "Delete", NULL)) { log_error ("%s:%s: Deleting attachment %i", SRCNAME, __func__, i); ret = -1; } gpgol_release (attachment); } TRETURN ret; } int Mail::removeOurAttachments_o () { TSTART; LPDISPATCH attachments = get_oom_object (m_mailitem, "Attachments"); if (!attachments) { TRACEPOINT; TRETURN 0; } int count = get_oom_int (attachments, "Count"); LPDISPATCH to_delete[count]; int del_cnt = 0; for (int i = 1; i <= count; i++) { auto item_str = std::string("Item(") + std::to_string (i) + ")"; LPDISPATCH attachment = get_oom_object (attachments, item_str.c_str()); if (!attachment) { TRACEPOINT; continue; } attachtype_t att_type; if (get_pa_int (attachment, GPGOL_ATTACHTYPE_DASL, (int*) &att_type)) { /* Not our attachment. */ gpgol_release (attachment); continue; } if (att_type == ATTACHTYPE_PGPBODY || att_type == ATTACHTYPE_MOSS || att_type == ATTACHTYPE_MOSSTEMPL) { /* One of ours to delete. */ to_delete[del_cnt++] = attachment; /* Dont' release yet */ continue; } gpgol_release (attachment); } gpgol_release (attachments); int ret = 0; for (int i = 0; i < del_cnt; i++) { LPDISPATCH attachment = to_delete[i]; /* Delete the attachments that are marked to delete */ if (invoke_oom_method (attachment, "Delete", NULL)) { log_error ("%s:%s: Error: deleting attachment %i", SRCNAME, __func__, i); ret = -1; } gpgol_release (attachment); } TRETURN ret; } /* We are very verbose because if we fail it might mean that we have leaked plaintext -> critical. */ bool Mail::hasCryptedOrEmptyBody_o () { TSTART; const auto pair = has_crypt_or_empty_body_oom (this); if (pair.first /* encrypted marker */) { log_debug ("%s:%s: Crypt Marker detected in OOM body. Return true %p.", SRCNAME, __func__, this); TRETURN true; } if (!pair.second) { log_debug ("%s:%s: Unexpected content detected. Return false %p.", SRCNAME, __func__, this); TRETURN false; } // Pair second == true (is empty) can happen on OOM error. LPMESSAGE message = get_oom_base_message (m_mailitem); if (!message && pair.second) { if (message) { gpgol_release (message); } TRETURN true; } size_t r_nbytes = 0; char *mapi_body = mapi_get_body (message, &r_nbytes); gpgol_release (message); if (!mapi_body || !r_nbytes) { // Body or bytes are null. we are empty. xfree (mapi_body); log_debug ("%s:%s: MAPI error or empty message. Return true. %p.", SRCNAME, __func__, this); TRETURN true; } if (r_nbytes > 10 && !strncmp (mapi_body, "-----BEGIN", 10)) { // Body is crypt. log_debug ("%s:%s: MAPI Crypt marker detected. Return true. %p.", SRCNAME, __func__, this); xfree (mapi_body); TRETURN true; } xfree (mapi_body); log_debug ("%s:%s: Found mapi body. Return false. %p.", SRCNAME, __func__, this); TRETURN false; } std::string Mail::getVerificationResultDump () { TSTART; std::stringstream ss; ss << m_verify_result; TRETURN ss.str(); } void Mail::setBlockStatus_m () { TSTART; SPropValue prop; LPMESSAGE message = get_oom_base_message (m_mailitem); prop.ulPropTag = PR_BLOCK_STATUS; prop.Value.l = 1; HRESULT hr = message->SetProps (1, &prop, NULL); if (hr) { log_error ("%s:%s: can't set block value: hr=%#lx\n", SRCNAME, __func__, hr); } gpgol_release (message); TRETURN; } void Mail::setBlockHTML (bool value) { TSTART; m_block_html = value; TRETURN; } void Mail::incrementLocateCount () { TSTART; m_locate_count++; TRETURN; } void Mail::decrementLocateCount () { TSTART; m_locate_count--; if (m_locate_count < 0) { log_error ("%s:%s: locate count mismatch.", SRCNAME, __func__); m_locate_count = 0; } if (!m_locate_count) { autosecureCheck (); } TRETURN; } void Mail::autosecureCheck () { TSTART; if (!opt.autosecure || !opt.autoresolve || m_manual_crypto_opts || m_locate_count) { TRETURN; } bool ret = KeyCache::instance()->isMailResolvable (this); log_debug ("%s:%s: status %i", SRCNAME, __func__, ret); /* As we are safe to call at any time, because we need * to be triggered by the locator threads finishing we * need to actually set the draft info flags in * the ui thread. */ do_in_ui_thread_async (ret ? DO_AUTO_SECURE : DONT_AUTO_SECURE, this); TRETURN; } void Mail::setDoAutosecure_m (bool value) { TSTART; TRACEPOINT; LPMESSAGE msg = get_oom_base_message (m_mailitem); if (!msg) { TRACEPOINT; TRETURN; } /* We need to set a uuid so that autosecure can be disabled manually */ setUUID_o (); int old_flags = get_gpgol_draft_info_flags (msg); if (old_flags && m_first_autosecure_check && /* Someone with always sign and autosecure active * will want to get autoencryption. */ !(old_flags == 2 && opt.sign_default)) { /* They were set explicily before us. This can be * because they were a draft (which is bad) or * because they are a reply/forward to a crypto mail * or because there are conflicting settings. */ log_debug ("%s:%s: Mail %p had already flags set.", SRCNAME, __func__, m_mailitem); m_first_autosecure_check = false; m_manual_crypto_opts = true; gpgol_release (msg); TRETURN; } m_first_autosecure_check = false; set_gpgol_draft_info_flags (msg, value ? 3 : opt.sign_default ? 2 : 0); gpgol_release (msg); gpgoladdin_invalidate_ui(); TRETURN; } bool Mail::decryptedSuccessfully () const { return m_decrypt_result.isNull() || !m_decrypt_result.error(); } void Mail::installFolderEventHandler_o() { TSTART; TRACEPOINT; LPDISPATCH folder = get_oom_object (m_mailitem, "Parent"); if (!folder) { TRACEPOINT; TRETURN; } char *objName = get_object_name (folder); if (!objName || strcmp (objName, "MAPIFolder")) { log_debug ("%s:%s: Mail %p parent is not a mapi folder.", SRCNAME, __func__, m_mailitem); xfree (objName); gpgol_release (folder); TRETURN; } xfree (objName); char *path = get_oom_string (folder, "FullFolderPath"); if (!path) { TRACEPOINT; path = get_oom_string (folder, "FolderPath"); } if (!path) { log_error ("%s:%s: Mail %p parent has no folder path.", SRCNAME, __func__, m_mailitem); gpgol_release (folder); TRETURN; } std::string strPath (path); xfree (path); if (s_folder_events_map.find (strPath) == s_folder_events_map.end()) { log_debug ("%s:%s: Install folder events watcher for %s.", SRCNAME, __func__, anonstr (strPath.c_str())); const auto sink = install_FolderEvents_sink (folder); s_folder_events_map.insert (std::make_pair (strPath, sink)); } /* Folder already registered */ gpgol_release (folder); TRETURN; } void Mail::refCurrentItem() { TSTART; if (m_currentItemRef) { log_debug ("%s:%s: Current item multi ref. Bug?", SRCNAME, __func__); TRETURN; } /* This prevents a crash in Outlook 2013 when sending a mail as it * would unload too early. * * As it didn't crash when the mail was opened in Outlook Spy this * mimics that the mail is inspected somewhere else. */ m_currentItemRef = get_oom_object (m_mailitem, "GetInspector.CurrentItem"); TRETURN; } void Mail::releaseCurrentItem() { TSTART; if (!m_currentItemRef) { TRETURN; } log_oom ("%s:%s: releasing CurrentItem ref %p", SRCNAME, __func__, m_currentItemRef); LPDISPATCH tmp = m_currentItemRef; m_currentItemRef = nullptr; /* This can cause our destruction */ gpgol_release (tmp); TRETURN; } void Mail::decryptPermanently_o() { TSTART; if (!m_needs_wipe) { log_debug ("%s:%s: Mail does not yet need wipe. Called to early?", SRCNAME, __func__); TRETURN; } if (!m_decrypt_result.isNull() && m_decrypt_result.error()) { log_debug ("%s:%s: Decrypt result had error. Can't decrypt permanently.", SRCNAME, __func__); TRETURN; } /* Remove the existing categories */ removeCategories_o (); /* Drop our state variables */ m_decrypt_result = GpgME::DecryptionResult(); m_verify_result = GpgME::VerificationResult(); m_needs_wipe = false; m_processed = false; m_is_smime = false; m_type = MSGTYPE_UNKNOWN; /* Remove our own attachments */ removeOurAttachments_o (); updateSigstate(); auto msg = MAKE_SHARED (get_oom_base_message (m_mailitem)); if (!msg) { STRANGEPOINT; TRETURN; } mapi_delete_gpgol_tags ((LPMESSAGE)msg.get()); /* The content type is wrong now. We remove it.*/ mapi_set_content_type ((LPMESSAGE)msg.get(), m_dec_content_type.empty () ? "text/plain" : m_dec_content_type.c_str ()); mapi_set_mesage_class ((LPMESSAGE)msg.get(), "IPM.Note"); if (invoke_oom_method (m_mailitem, "Save", NULL)) { log_error ("Failed to save decrypted mail: %p ", m_mailitem); } log_debug ("%s:%s: Delayed invalidate to update sigstate after perm dec.", SRCNAME, __func__); CloseHandle(CreateThread (NULL, 0, delayed_invalidate_ui, (LPVOID) 300, 0, NULL)); TRETURN; } int Mail::prepareCrypto_o () { TSTART; int olFlags = get_oom_crypto_flags (m_mailitem); log_dbg ("Outlook internal crypto flags are: %i", olFlags); if (olFlags) { std::string question = _("Should GpgOL override Outlook and continue " "to process the message?"); std::string vs_warning; if (in_de_vs_mode ()) { /* TRANSLATORS %s is compliance name like VS-NfD */ vs_warning = asprintf_s (_("Note: For %s communication you have to select Yes."), de_vs_name ()) + std::string ("\n\n"); } std::string msg = _("A crypto operation was selected both using " "Outlooks internal crypto and with GpgOL / GnuPG.") + std::string("\n\n") + vs_warning + question; int yesno = gpgol_message_box (get_active_hwnd (), msg.c_str (), _("Conflicting crypto settings"), MB_YESNO); if (yesno == IDYES) { log_dbg ("Overriding Outlook internal crypto."); } else { log_dbg ("Aborting send."); TRETURN -1; } } put_pa_int (m_mailitem, PR_SECURITY_FLAGS_DASL, 0); // Check inline response state to fill out asynccryptdisabled. checkSyncCrypto_o (); if (!isAsyncCryptDisabled()) { /* Obtain a reference of the current item. This prevents * an early unload which would crash Outlook 2013 * * As it didn't crash when the mail was opened in Outlook Spy this * mimics that the mail is inspected somewhere else. */ refCurrentItem (); } // First contact with a mail to encrypt update // state and oom data. updateOOMData_o (true); setCryptState (Mail::DataCollected); TRETURN 0; } /* Printing happens in two steps. First a Mail is loaded after the BeforePrint event, then it is loaded a second time when the actual print happens. We have to catch both. This functions looks over all mails and checks if one is currently printing. If so we compare our EntryID's and if they match. Bingo, we are printing, too.*/ bool Mail::checkIfMailMightBePrinting_o () { const auto ourID = get_oom_string_s (m_mailitem, "EntryID"); if (ourID.empty ()) { log_error ("%s:%s: Mail %p has no accessible EntryID", SRCNAME, __func__, this); return false; } if (s_entry_ids_printing.find (ourID) != s_entry_ids_printing.end ()) { log_dbg ("Found %s in printing map. This might be a printing child.", ourID.c_str ()); return true; } return false; } void Mail::setSigningKeys (const std::vector &keys) { m_resolved_signing_keys = keys; } std::vector Mail::getSigningKeys () const { return m_resolved_signing_keys; } Mail * Mail::copy () { TSTART; VARIANT result; VariantInit (&result); int err = invoke_oom_method (m_mailitem, "Copy", &result); if (err) { log_error ("%s:%s Failed to copy mail.", SRCNAME, __func__); TRETURN nullptr; } if (result.vt != VT_DISPATCH || !result.pdispVal) { log_error ("%s:%s Unexpected result type %x.", SRCNAME, __func__, result.vt); TRETURN nullptr; } /* The returned dispatch interface has da different address then the one from the ItemLoad event. So we now need to find the mail object for the copied mail. The copied mail has an entry id but we are not assured that the current mail has an entry id because it might not have been saved *sigh* So we use the GpgOL UUID that is copied in MAPI to identify the copy. */ const auto id = get_unique_id_s (m_mailitem, 0, nullptr); const auto mails = searchMailsByUUID (id); log_dbg ("Found %i mails with id '%s'", (int) mails.size (), id.c_str ()); for (const auto it: mails) { if (it == this) { continue; } /* Transfer the refence from the copy IDispatch to the Mail object IDispatch. This is pretty strange, but doing a VariantClear on the result otherwise results in an unload of the object so we need to track that reference somehow. */ LPDISPATCH copyRef = result.pdispVal; memdbg_addRef (copyRef); it->setAdditionalReference (copyRef); /* Set a new UID for the copy and enter it in the map */ it->setUUID_o (true); TRETURN it; } log_err ("Failed to find copied mail!"); TRETURN nullptr; } void Mail::splitAndSend_o () { TSTART; log_dbg ("Split and send for: %p", this); RecipientManager mngr (m_cached_recipients, m_resolved_signing_keys); int count = mngr.getRequiredMails (); log_dbg ("Need to send %i mails", mngr.getRequiredMails ()); if (count == 1) { /* We have to bug out here because otherwise we would loop endlessly if a split is deteced but no split occurs */ gpgol_bug (get_active_hwnd(), ERR_SPLIT_UNEXPECTED); TRETURN; } for (int i = 0; i < count; i++) { Mail *copiedMail = copy (); if (!copiedMail) { log_err ("Failed to copy mail. Aboring."); TRETURN; } GpgME::Key sigKey; const RecpList recps = mngr.getRecipients (i, sigKey); copiedMail->setRecipients (recps); std::vector sigVec; sigVec.push_back (sigKey); copiedMail->setSigningKeys (sigVec); if (set_oom_recipients (copiedMail->item (), recps)) { log_err ("Failed to update recipients"); gpgol_bug (get_active_hwnd(), ERR_SPLIT_RECIPIENTS); TRETURN; } log_dbg ("Recipients for %i", i); Recipient::dump (mngr.getRecipients (i, sigKey)); - wm_register_pending_op (copiedMail); /* Now do the crypto */ - copiedMail->setSplitCopy (true); + copiedMail->setCopyParent (this); copiedMail->prepareCrypto_o (); copiedMail->encryptSignStart_o (); } /* This triggers the copyMailCallback in the write event */ // invoke_oom_method (copied_mailitem, "Send", nullptr); /* TODO: Close the mail after all others are sent successfully */ /* invoke_oom_method (m_mailitem, "Send", nullptr); */ TRETURN; } void Mail::resetCrypter () { m_crypter = nullptr; } void Mail::resetRecipients () { /* Clear recipient data */ m_cached_recipients.clear (); m_resolved_signing_keys.clear (); m_recipients_set = false; } void -Mail::setSplitCopy (bool val) +Mail::setCopyParent (Mail *val) { - m_is_split_copy = val; + m_copy_parent = val; } -bool -Mail::isSplitCopy () const +Mail * +Mail::copyParent() const { - return m_is_split_copy; + return m_copy_parent; } int Mail::buildProtectedHeaders_o () { TSTART; std::stringstream ss; std::vector toRecps; std::vector ccRecps; std::vector bccRecps; ss << "protected-headers=\"v1\"\r\n"; for (const auto &recp: m_cached_recipients) { if (recp.type() == Recipient::olCC) { ccRecps.push_back (recp.encodedDisplayName ()); } else if (recp.type() == Recipient::olTo) { toRecps.push_back (recp.encodedDisplayName ()); } else if (recp.type() == Recipient::olOriginator) { ss << "From: " << recp.encodedDisplayName () << "\r\n"; } } std::string buf; if (toRecps.size ()) { join(toRecps, ";", buf); ss << "To: " << buf << "\r\n"; } if (ccRecps.size ()) { join(toRecps, ";", buf); ss << "CC: " << buf << "\r\n"; } if (opt.encryptSubject) { char *subject = get_oom_string (m_mailitem, "Subject"); if (subject) { char *encodedSubject = utf8_to_rfc2047b (subject); if (encodedSubject) { ss << "Subject: " << encodedSubject << "\r\n"; } xfree (encodedSubject); } if (!put_oom_string (m_mailitem, "Subject", "...")) { STRANGEPOINT; TRETURN -1; } xfree (subject); } m_protected_headers = ss.str (); TRETURN 0; } void Mail::setProtectedHeaders (const std::string &hdr) { m_protected_headers = hdr; } std::string Mail::protectedHeaders () const { return m_protected_headers; } header_info_s Mail::headerInfo () const { return m_header_info; } const std::string& Mail::getOriginalBody () const { return m_orig_body; } int Mail::isMultipartRelated () const { int related = false; int mixed = false; for (const auto attach: m_plain_attachments) { if (attach->get_content_id ().size()) { related = 1; } else { mixed = 1; } } return related + mixed; } std::vector > Mail::plainAttachments () const { return m_plain_attachments; } void Mail::setAdditionalReference (LPDISPATCH ref) { m_additional_item = ref; } diff --git a/src/mail.h b/src/mail.h index 2d0ff1e..3ca7bec 100644 --- a/src/mail.h +++ b/src/mail.h @@ -1,797 +1,796 @@ /* @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 . */ #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 class ParseController; class CryptController; class Attachment; class Recipient; /** @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. * * Naming conventions of the suffixes: * _o functions that work on OOM and possibly also MAPI. * _m functions that work on MAPI. * _s functions that only work on internal data and are safe to call * from any thread. * * O and M functions _must_ only be called from the main thread. Use * a WindowMessage to signal the Main thread. But be wary. A WindowMessage * might be handled while an OOM call in the main thread waits for completion. * * An example for this is how update_oom_data can work: * * Main Thread: * call update_oom_data * └> internally invokes an OOM function that might do network access e.g. * to connect to the exchange server to fetch the address. * * Counterintutively the Main thread does not return from that function or * blocks for it's completion but handles windowmessages. * * After a windowmessage was handled and if the OOM invocation is * completed the invocation returns and normal execution continues. * * So if the window message handler's includes for example * also a call to lookup recipients we crash. Note that it's usually * safe to do OOM / MAPI calls from a window message. * * * While this seems impossible, remember that we do not work directly * with functions but everything is handled through COM. Without this * logic Outlook would probably become unusable because as any long running * call to the OOM would block it completely and freeze the UI. * (no windowmessages handled). * * So be wary when accessing the OOM from a Window Message. */ class Mail { public: enum CryptState { NotStarted, /* The mail has not been encrypted or signed by us */ DataCollected, /* Data from the OOM / MAPI has been collected */ BackendDone, /* GnuPG is finished */ OOMUpdated, /* GnuPG Data has been written to OOM */ OOMSynced, /* OOM has been synced internally but not MAPI */ CryptFinished, /* The mail is signed and or encrypted and can be sent */ }; /** @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* getMailForItem (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* getMailForUUID (const char *uuid); /** @brief Search for all Mail objects with the uuid. This does not use the UUID map as the uuid map assumes tht uuids are unique. The mails are searched in the general mail map. This can be used to find copies of the same mail. An empty uuid retuns all mails without a uuid set by us. @returns All mails with the same uuid. */ static std::vector searchMailsByUUID (const std::string &uuid); /** @brief Get the last created mail. @returns A reference to the last created mail or null. */ static Mail* getLastMail (); /** @brief voids the last mail variable. */ static void clearLastMail (); /** @brief Lock mail deletion. Mails are heavily accessed multi threaded. E.g. when locating keys. Due to bad timing it would be possible that between a check for "is_valid_ptr" to see if a map is still valid and the usage of the mail a delete would happen. This lock can be used to prevent that. Changes made to the mail will of course have no effect as the mail is already in the process of beeing unloaded. And calls that access MAPI or OOM still might crash. But this at least gurantees that the member variables of the mail exist while the lock is taken. Use it in your thread like: Mail::lockDelete (); Mail::isValidPtr (mail); mail->set_or_check_something (); Mail::unlockDelete (); Still be carefull when it is a mapi or oom function. */ static void lockDelete (); static void unlockDelete (); /** @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 isValidPtr (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 wipeAllMails_o (); /** @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 revertAllMails_o (); /** @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 closeAllMails_o (); /** @brief closes the inspector for a mail * * @returns true on success. */ static int closeInspector_o (Mail *mail); /** Call close with discard changes to discard plaintext. returns the value of the oom close call. This may have delete the mail if the close triggers an unload. Set restoreSMIMEClass to resotre the S/MIME Message class IPM.Note.SMIME etc. for S/MIME mails. */ int close (bool restoreSMIMEClass = true); /** @brief locate recipients for all crypto mails * * To avoid lookups of recipients for non crypto mails we only * locate keys when a crypto action is already selected. * * As the user can do this after recipients were added but * we don't know for which mail the crypt button was triggered. * we march over all mails and if they are crypto mails we check * that the recipents were located. */ static void locateAllCryptoRecipients_o (); /** @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 preProcessMessage_m (); /** @brief Decrypt / Verify the mail. * * Sets the needs_wipe and was_encrypted variable. * * @returns 0 on success. */ int decryptVerify_o (); /** @brief start crypto operations as selected by the user. * * Initiates the crypto operations according to the gpgol * draft info flags. * * @returns 0 on success. */ int encryptSignStart_o (); /** @brief Necessary crypto operations were completed successfully. */ bool wasCryptoSuccessful_m () { return m_crypt_successful || !needs_crypto_m (); } /** @brief Message should be encrypted and or signed. 0: No 1: Encrypt 2: Sign 3: Encrypt & Sign */ int needs_crypto_m () const; /** @brief wipe the plaintext from the message and encrypt attachments. * * @returns 0 on success; */ int wipe_o (bool force = false); /** @brief revert the message to the original mail before our changes. * * @returns 0 on success; */ int revert_o (); /** @brief update some data collected from the oom * * This updates cached values from the OOM that are not available * in MAPI events like after Write. * * 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. * * for_encryption can be used to override that data should be collected * for encryption. Otherwise it depends on "isCryptoMail" to decide which * data should be collected. * * It also updated the is_html_alternative value. * * @returns 0 on success */ int updateOOMData_o (bool for_encryption = false); /** @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 an empty string. */ std::string getSender_o (); /** @brief get sender SMTP address (UTF-8 encoded). * * Like get_sender but ensures not to touch oom or mapi * * @returns A reference to the utf8 sender address. Or an empty string. */ std::string getSender () const; /** @brief get the subject string (UTF-8 encoded). * * @returns the subject or an empty string. */ std::string getSubject_o () 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 isCryptoMail () const; /** @brief This mail needs to be actually written. * * @returns true if the next write event should not be canceled. */ bool needsSave () { return m_needs_save; } /** @brief set the needs save state. */ void setNeedsSave (bool val) { m_needs_save = val; } /** @brief is this mail an S/MIME mail. * * @returns true for smime messages. */ bool isSMIME_m (); /* Same as above but does not touch MAPI. isSMIME_m needs to be called before this can return a true value. */ bool isSMIME () const; /** @brief get the associated parser. only valid while the actual parsing happens. */ std::shared_ptr parser () { return m_parser; } /** @brief get the associated cryptcontroller. only valid while the crypting happens. */ std::shared_ptr cryper () { return m_crypter; } /** 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. Parsing done can be called twice, once for the preview of a signed message and once when everything is finished. */ void parsingDone_o (bool is_preview = false); /** Returns true if the mail was verified and has at least one signature. Regardless of the validity of the mail */ bool isSigned () const; /** Returns true if the mail is encrypted to at least one recipient. Regardless if it could be decrypted. */ bool isEncrypted () const; /** Are we "green" */ bool isValidSig () const; /** Get UID gets UniqueID property of this mail. Returns an empty string if the uid was not set with set uid.*/ const std::string & getUUID () const { return m_uuid; } /** Returns 0 on success if the mail has a uid alrady or sets the uid. If reset is true the mail will get a new UUID. Setting only succeeds if the OOM is currently accessible. Returns -1 on error. */ int setUUID_o (bool reset = false); /** Returns a localized string describing in one or two words the crypto status of this mail. */ std::string getCryptoSummary () const; /** Returns a localized string describing the detailed crypto state of this mail. */ std::string getCryptoDetails_o (); /** Returns a localized string describing a one line summary of the crypto state. */ std::string getCryptoOneLine () const; /** Get the icon id of the appropiate icon for this mail */ int getCryptoIconID () const; /** Get the fingerprint of an associated signature or null if it is not signed. */ const char *getSigFpr () const; /** Remove all categories of this mail */ void removeCategories_o (); /** Get the body of the mail */ std::string getBody_o () const; /** Try to locate the keys for all recipients. This also triggers the Addressbook integration, which we treat as locate jobs. */ void locateKeys_o (); /** State variable to check if a close was triggerd by us. */ void setCloseTriggered (bool value); bool getCloseTriggered () const; /** Check if the mail should be sent as html alternative mail. Only valid if update_oom_data was called before. */ bool isHTMLAlternative () const; /** Get the html body. It is updated in update_oom_data. Caller takes ownership of the string and has to free it. */ char *takeCachedHTMLBody (); /** Get the plain body. It is updated in update_oom_data. Caller takes ownership of the string and has to free it. */ char *takeCachedPlainBody (); /** Get the cached recipients. It is updated in update_oom_data.*/ std::vector getCachedRecipients (); /** Only get the Mail addresses from the recipient objects. */ std::vector getCachedRecipientAddresses (); /** Set an override for recipients. If they have keys the keys will not be resolved again. */ void setRecipients (const std::vector &recps); /** Get the recipients. */ std::vector getRecipients_o () const; /** Returns 1 if the mail was encrypted, 2 if signed, 3 if both. Only valid after decrypt_verify. */ int getCryptoFlags () const; /** Returns true if the mail should be encrypted in the after write event. */ bool getNeedsEncrypt () const; void setNeedsEncrypt (bool val); /** Gets the level of the signature. See: https://wiki.gnupg.org/EasyGpg2016/AutomatedEncryption for a definition of the levels. */ int get_signature_level () const; /** Check if all attachments are hidden and show a warning message appropiate to the crypto state if necessary. If silent is true it will not show a warning but silently remove the bad attachments. */ int checkAttachments_o (bool silent); /** Check if the mail should be encrypted "inline" */ bool getDoPGPInline () const {return m_do_inline;} /** Check if the mail should be encrypted "inline" */ void setDoPGPInline (bool value) {m_do_inline = value;} /** Append data to a cached inline body. Helper to do this on MAPI level and later add it through OOM */ void appendToInlineBody (const std::string &data); /** Set the inline body as OOM body property. */ int inlineBodyToBody_o (); /** Get the crypt state */ CryptState cryptState () const {return m_crypt_state;} /** Set the crypt state */ void setCryptState (CryptState state) {m_crypt_state = state;} /** Update MAPI data after encryption. */ void updateCryptMAPI_m (); /** Update OOM data after encryption. Checks for plain text leaks and does not advance crypt state if body can't be cleaned. */ void updateCryptOOM_o (); /** Enable / Disable the window of this mail. When the window gets disabled the handle is stored for a later enable. */ void disableWindow_o (); void enableWindow (); /** Determine if the mail is an inline response. Call check_inline_response first to update the state from the OOM. We need synchronous encryption for inline responses. */ bool isAsyncCryptDisabled () { return m_async_crypt_disabled; } /** Check through OOM if the current mail is an inline response. Meaning editable in the message list. */ bool isActiveInlineResponse_o (); /* Check if we can't do async crypto. E.g. for inline responses. Caches the state which can then be queried through isAsyncCryptDisabled; */ bool checkSyncCrypto_o (); /** Get the window for the mail. Caution! This is only really valid in the time that the window is disabled. Use with care and can be null or invalid. */ HWND getWindow () { return m_window; } /** Cleanup any attached crypter objects. Useful on error. */ void resetCrypter (); /** Set special crypto mime data that should be used as the mime structure when sending. */ void setOverrideMIMEData (const std::string &data) {m_mime_data = data;} /** Get the mime data that should be used when sending. */ std::string get_override_mime_data () const { return m_mime_data; } bool hasOverrideMimeData() const { return !m_mime_data.empty(); } /** Set if this is a forward of a crypto mail. */ void setIsForwardedCryptoMail (bool value) { m_is_forwarded_crypto_mail = value; } bool is_forwarded_crypto_mail () { return m_is_forwarded_crypto_mail; } /** Remove the hidden GpgOL attachments. This is needed when forwarding without encryption so that our attachments are not included in the forward. Returns 0 on success. Works in OOM. */ int removeOurAttachments_o (); /** Remove all attachments. Including our own. This is needed for forwarding of unsigned S/MIME mails (Efail). Returns 0 on success. Works in OOM. */ int removeAllAttachments_o (); /** Check both OOM and MAPI if the body is either empty or encrypted. Won't abort on OOM or MAPI errors, so it can be used in both states. But will return false if a body was detected or in the OOM the MAPI Base Message. This is intended as a saveguard before sending a mail. This function should not be used to detected the necessity of encryption and is only an extra check to catch unexpected errors. */ bool hasCryptedOrEmptyBody_o (); void updateBody_o (bool is_preview); /** Update information from protected headers in OOM */ void updateHeaders_o (); /** Set if this mail looks like the send again of a crypto mail. This will mean that after it is decrypted it is treated like an unencrypted mail so that it can be encrypted again or sent unencrypted. */ void setIsSendAgain (bool value) { m_is_send_again = value; } /* Attachment removal state variables. */ bool attachmentRemoveWarningDisabled () { return m_disable_att_remove_warning; } /* Gets the string dump of the verification result. */ std::string getVerificationResultDump (); /* Block loading HTML content */ void setBlockHTML (bool value); bool isBlockHTML () const { return m_block_html; } /* Remove automatic loading of HTML references setting. */ void setBlockStatus_m (); /* Crypto options (sign/encrypt) have been set manually. */ void setCryptoSelectedManually (bool v) { m_manual_crypto_opts = v; } // bool is_crypto_selected_manually () const { return m_manual_crypto_opts; } /* Reference that a resolver thread is running for this mail. */ void incrementLocateCount (); /* To be called when a resolver thread is done. If there are no running resolver threads we can check the recipients to see if we should toggle / untoggle the secure state. */ void decrementLocateCount (); /* Check if the keys can be resolved automatically and trigger * setting the crypto flags accordingly. */ void autosecureCheck (); /* Set if a mail should be secured (encrypted and signed) * * Only save to call from a place that may access mapi. */ void setDoAutosecure_m (bool value); /* Install an event handler for the folder of this mail. */ void installFolderEventHandler_o (); /* Marker for a "Move" of this mail */ bool passWrite () { return m_pass_write; } void setPassWrite(bool value) { m_pass_write = value; } /* Releases the current item ref obtained in update oom data */ void releaseCurrentItem (); /* Gets an additional reference for GetInspector.CurrentItem */ void refCurrentItem (); /* Get the storeID for this mail */ std::string storeID() { return m_store_id; } /* Remove encryption permanently. */ void decryptPermanently_o (); /* Prepare for encrypt / sign. Updates data. */ int prepareCrypto_o (); /* State variable to check if we are about to encrypt a draft. */ void setIsDraftEncrypt (bool value) { m_is_draft_encrypt = value; } bool isDraftEncrypt () { return m_is_draft_encrypt; } /* Was this mail decrypted without error. Also returns true if the mail was not encrypted. */ bool decryptedSuccessfully () const; /* The mail should be decrypted again after the next * encryption. So that we can save it for example and * still work on it. */ void setDecryptAgain (bool value) { m_decrypt_again = value; } bool isDecryptAgain () const { return m_decrypt_again; } /* The type of the message. */ msgtype_t msgtype () const { return m_type; } /* The mail was loaded after we have seen a BeforePrint event. */ void setIsPrint (bool value) { m_printing = value; } bool isPrint () const { return m_printing; } /* Update the catgories (encrypted / trust in) based on the crypt results. */ void updateCategories_o (); /* Set the signing key for the mail. This overrides any other key resolution. */ void setSigningKeys (const std::vector &keys); std::vector getSigningKeys () const; /* Set the recipients for this mail. If they all have keys set then this overrides the cryptcontroller resolutions*/ void setRecipients (const std::vector &keys); /* Split a mail according to recipients, hidden, different protocols and so on and then send the multiple mails. */ void splitAndSend_o (); /* Reset recipient and resolved key data. */ void resetRecipients (); - /* Returns true if a mail is a copy that was split of - a different mail. */ - bool isSplitCopy () const; + /* Returns the mail object of which this mail is a copy */ + Mail *copyParent () const; /* Setter for isSplitCopy */ - void setSplitCopy (bool val); + void setCopyParent (Mail *parent); /* Set protected headers data */ void setProtectedHeaders (const std::string &hdrs); std::string protectedHeaders () const; /* Parse the headers so that the header_info_s becomes valid.*/ int parseHeaders_m (); /* Get the header info struct. Only valid afer parseHeaders_m */ header_info_s headerInfo () const; /* Get the original body of the mail. */ const std::string &getOriginalBody () const; /* Get the plain attachments for an encrypt / sign operation. Set through updateOOMData_o */ std::vector > plainAttachments() const; /* True if this is a multipart related mail, 1 if all attachments are related, 2 if there is a related and a mixed attachment. */ int isMultipartRelated () const; /* Set an additional item ref to this object. Different API calls in outlook can return different LPDISPATCH pointer to the same OOM Mailitem. The additional ref will be released on delete. */ void setAdditionalReference (LPDISPATCH ref); private: /* Returns a copy of the mail object. */ Mail *copy (); bool checkIfMailMightBePrinting_o (); void updateSigstate (); int add_attachments_o (std::vector > attachments); int buildProtectedHeaders_o (); LPDISPATCH m_mailitem; LPDISPATCH m_additional_item; LPDISPATCH m_event_sink; LPDISPATCH m_currentItemRef; 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_close_triggered, /* We have programtically triggered a close */ m_is_html_alternative, /* Body Format is not plain text */ m_needs_encrypt; /* Send was triggered we want to encrypt. */ int m_moss_position; /* The number of the original message attachment. */ int m_crypto_flags; std::string m_sender; std::string m_sent_on_behalf; char *m_cached_html_body; /* Cached html body. */ char *m_cached_plain_body; /* Cached plain body. */ std::vector m_cached_recipients; msgtype_t m_type; /* Our messagetype as set in mapi */ std::shared_ptr m_parser; std::shared_ptr m_crypter; GpgME::VerificationResult m_verify_result; GpgME::DecryptionResult m_decrypt_result; GpgME::Signature m_sig; GpgME::UserID m_uid; std::string m_uuid; std::string m_orig_body; bool m_do_inline; bool m_is_gsuite; /* Are we on a gsuite account */ std::string m_inline_body; CryptState m_crypt_state; HWND m_window; bool m_async_crypt_disabled; std::string m_mime_data; bool m_is_forwarded_crypto_mail; /* Is this a forward of a crypto mail */ bool m_is_reply_crypto_mail; /* Is this a reply to a crypto mail */ bool m_is_send_again; /* Is this a send again of a crypto mail */ bool m_disable_att_remove_warning; /* Should not warn about attachment removal. */ bool m_block_html; /* Force blocking of html content. e.g for unsigned S/MIME mails. */ bool m_manual_crypto_opts; /* Crypto options (sign/encrypt) have been set manually. */ bool m_first_autosecure_check; /* This is the first autoresolve check */ int m_locate_count; /* The number of key locates pending for this mail. */ bool m_pass_write; /* Danger the next write will be passed. This is for closed mails */ bool m_locate_in_progress; /* Simplified state variable for locate */ std::string m_store_id; /* Store id for categories */ std::string m_verify_category; /* The category string for the verify result */ bool m_is_junk; /* Mail is in the junk folder */ bool m_is_draft_encrypt; /* Mail is a draft that should be encrypted */ bool m_decrypt_again; /* Mail should be decrypted again if it sees another beforeread */ bool m_printing; /* Mail is decrypted for printing */ std::string m_gpgol_class; /* The GpgOL Message class */ std::vector m_resolved_signing_keys; /* Prepared / resolved keys for signing. */ bool m_recipients_set; /* Recipients were explictly set. */ - bool m_is_split_copy; /* Is the a copy mail that was part of a split. */ + Mail *m_copy_parent; /* The mail object of which this mail is a copy */ std::string m_protected_headers; header_info_s m_header_info; /* Information about the original headers */ bool m_attachs_added; /* State variable to track if we have added attachments to this mail. */ std::string m_dec_content_type; /* Top level content type of the decrypted mail. */ std::vector > m_plain_attachments; /* Attachments to encrypt */ }; #endif // MAIL_H diff --git a/src/windowmessages.cpp b/src/windowmessages.cpp index 6a4bf09..0cd3cb5 100644 --- a/src/windowmessages.cpp +++ b/src/windowmessages.cpp @@ -1,820 +1,877 @@ /* @file windowmessages.h * @brief Helper class to work with the windowmessage handler thread. * * 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 "windowmessages.h" #include "common.h" #include "oomhelp.h" #include "mail.h" #include "gpgoladdin.h" #include "wks-helper.h" #include "addressbook.h" #include #define RESPONDER_CLASS_NAME "GpgOLResponder" /* Singleton window */ static HWND g_responder_window = NULL; static int invalidation_blocked = 0; static std::vector s_pending_ops; static std::vector s_ready_ops; GPGRT_LOCK_DEFINE (op_lock); +static void invoke_send_bottom (Mail *mail) +{ + TSTART; + // Finaly this should pass. + if (invoke_oom_method (mail->item (), "Send", NULL)) + { + log_error ("%s:%s: Send failed for %p. " + "Trying SubmitMessage instead.\n" + "This will likely crash.", + SRCNAME, __func__, mail); + + /* What we do here is similar to the T3656 workaround + in mailitem-events.cpp. In our tests this works but + is unstable. So we only use it as a very very last + resort. */ + auto mail_message = get_oom_base_message (mail->item()); + if (!mail_message) + { + gpgol_bug (mail->getWindow (), + ERR_GET_BASE_MSG_FAILED); + TRETURN; + } + // It's important we use the _base_ message here. + mapi_save_changes (mail_message, FORCE_SAVE); + HRESULT hr = mail_message->SubmitMessage(0); + gpgol_release (mail_message); + + if (hr == S_OK) + { + do_in_ui_thread_async (CLOSE, (LPVOID) mail); + } + else + { + log_error ("%s:%s: SubmitMessage Failed hr=0x%lx.", + SRCNAME, __func__, hr); + gpgol_bug (mail->getWindow (), + ERR_SEND_FALLBACK_FAILED); + } + } + else + { + mail->releaseCurrentItem (); + } + log_debug ("%s:%s: Send for %p completed.", + SRCNAME, __func__, mail); + TRETURN; +} + LONG_PTR WINAPI gpgol_window_proc (HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { // log_debug ("WMG: %x", (unsigned int) message); if (message == WM_USER + 42) { TSTART; if (!lParam) { log_error ("%s:%s: Recieved user msg without lparam", SRCNAME, __func__); TRETURN DefWindowProc(hWnd, message, wParam, lParam); } 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::isValidPtr (mail)) { log_debug ("%s:%s: Parsing done for mail which is gone.", SRCNAME, __func__); TBREAK; } mail->parsingDone_o (); TBREAK; } case (RECIPIENT_ADDED): { auto mail = (Mail*) ctx->data; if (!Mail::isValidPtr (mail)) { log_debug ("%s:%s: Recipient add for mail which is gone.", SRCNAME, __func__); TBREAK; } mail->locateKeys_o (); TBREAK; } case (REVERT_MAIL): { auto mail = (Mail*) ctx->data; if (!Mail::isValidPtr (mail)) { log_debug ("%s:%s: Revert mail for mail which is gone.", SRCNAME, __func__); TBREAK; } mail->setNeedsSave (true); /* Some magic here. Accessing any existing inline body cements it. Otherwise updating the body through the revert also changes the body of a inline mail. */ char *inlineBody = get_inline_body (); xfree (inlineBody); // Does the revert. log_debug ("%s:%s: Revert mail. Invoking save.", SRCNAME, __func__); invoke_oom_method (mail->item (), "Save", NULL); log_debug ("%s:%s: Revert mail. Save done. Updating body..", SRCNAME, __func__); mail->updateBody_o (false); log_debug ("%s:%s: Revert mail done.", SRCNAME, __func__); TBREAK; } case (INVALIDATE_UI): { if (!invalidation_blocked) { log_debug ("%s:%s: Invalidating UI", SRCNAME, __func__); gpgoladdin_invalidate_ui(); log_debug ("%s:%s: Invalidation done", SRCNAME, __func__); } else { log_debug ("%s:%s: Received invalidation msg while blocked." " Ignoring it", SRCNAME, __func__); } TBREAK; } case (INVALIDATE_LAST_MAIL): { log_debug ("%s:%s: clearing last mail", SRCNAME, __func__); Mail::clearLastMail (); TBREAK; } case (CLOSE): { auto mail = (Mail*) ctx->data; if (!Mail::isValidPtr (mail)) { log_debug ("%s:%s: Close for mail which is gone.", SRCNAME, __func__); TBREAK; } mail->refCurrentItem(); Mail::closeInspector_o (mail); TRACEPOINT; mail->close (); log_debug ("%s:%s: Close finished.", SRCNAME, __func__); mail->releaseCurrentItem(); TBREAK; } case (CRYPTO_DONE): { auto mail = (Mail*) ctx->data; if (!Mail::isValidPtr (mail)) { log_debug ("%s:%s: Crypto done for mail which is gone.", SRCNAME, __func__); TBREAK; } if (mail->isDraftEncrypt()) { mail->updateCryptOOM_o (); invoke_oom_method (mail->item (), "Save", NULL); log_debug ("%s:%s: Second save done for %p finished draft " "encryption.", SRCNAME, __func__, mail); mail->setIsDraftEncrypt (false); mail->setCryptState (Mail::NotStarted); mail->releaseCurrentItem (); TBREAK; } // modify the mail. if (mail->cryptState () == Mail::BackendDone) { // Save the Mail log_debug ("%s:%s: Crypto done for %p updating oom.", SRCNAME, __func__, mail); mail->updateCryptOOM_o (); } if (mail->cryptState () == Mail::OOMUpdated) { invoke_oom_method (mail->item (), "Save", NULL); log_debug ("%s:%s: Second save done for %p Invoking second send.", SRCNAME, __func__, mail); } - // Finaly this should pass. - if (invoke_oom_method (mail->item (), "Send", NULL)) + gpgrt_lock_lock (&op_lock); + /* Look for the Mail in the pending operations */ + auto it = std::find (s_pending_ops.begin (), + s_pending_ops.end (), mail); + if (it != s_pending_ops.end ()) { - log_error ("%s:%s: Send failed for %p. " - "Trying SubmitMessage instead.\n" - "This will likely crash.", - SRCNAME, __func__, mail); - - /* What we do here is similar to the T3656 workaround - in mailitem-events.cpp. In our tests this works but - is unstable. So we only use it as a very very last - resort. */ - auto mail_message = get_oom_base_message (mail->item()); - if (!mail_message) - { - gpgol_bug (mail->getWindow (), - ERR_GET_BASE_MSG_FAILED); - TBREAK; - } - // It's important we use the _base_ message here. - mapi_save_changes (mail_message, FORCE_SAVE); - HRESULT hr = mail_message->SubmitMessage(0); - gpgol_release (mail_message); - - if (hr == S_OK) - { - do_in_ui_thread_async (CLOSE, (LPVOID) mail); - } - else - { - log_error ("%s:%s: SubmitMessage Failed hr=0x%lx.", - SRCNAME, __func__, hr); - gpgol_bug (mail->getWindow (), - ERR_SEND_FALLBACK_FAILED); - } + s_pending_ops.erase (it); + log_dbg ("Removing %p from pending.", mail); } else { - mail->releaseCurrentItem (); + log_dbg ("Crypto op done which was not pending - ignoring."); + gpgrt_lock_unlock (&op_lock); + TBREAK; } - log_debug ("%s:%s: Send for %p completed.", - SRCNAME, __func__, mail); - TBREAK; - } - case (BRING_TO_FRONT): - { - HWND wnd = get_active_hwnd (); - if (wnd) + if (!s_pending_ops.empty()) { - log_debug ("%s:%s: Bringing window %p to front.", - SRCNAME, __func__, wnd); - bring_to_front (wnd); + log_dbg ("Have pending operations. Adding %p to ready.", mail); + s_ready_ops.push_back (mail); } else { - log_debug ("%s:%s: No active window found for bring to front.", - SRCNAME, __func__); + for (Mail *m: s_ready_ops) + { + log_dbg ("Sending out pending mail %p", m); + if (!Mail::isValidPtr (m)) + { + log_err ("Mail is gone!"); + continue; + } + invoke_send_bottom (m); + } + Mail *copyParent = mail->copyParent (); + invoke_send_bottom (mail); + s_pending_ops.clear (); + if (copyParent && !Mail::isValidPtr (copyParent)) + { + log_err ("Basis for copy mails is gone: %p", copyParent); + } + else if (copyParent) + { + copyParent->close (); + } } + + gpgrt_lock_unlock (&op_lock); TBREAK; } case (WKS_NOTIFY): { WKSHelper::instance ()->notify ((const char *) ctx->data); xfree (ctx->data); TBREAK; } case (CLEAR_REPLY_FORWARD): { auto mail = (Mail*) ctx->data; if (!Mail::isValidPtr (mail)) { log_debug ("%s:%s: Clear reply forward for mail which is gone.", SRCNAME, __func__); TBREAK; } mail->wipe_o (true); mail->removeAllAttachments_o (); TBREAK; } case (DO_AUTO_SECURE): { auto mail = (Mail*) ctx->data; if (!Mail::isValidPtr (mail)) { log_debug ("%s:%s: DO_AUTO_SECURE for mail which is gone.", SRCNAME, __func__); TBREAK; } mail->setDoAutosecure_m (true); TBREAK; } case (DONT_AUTO_SECURE): { auto mail = (Mail*) ctx->data; if (!Mail::isValidPtr (mail)) { log_debug ("%s:%s: DO_AUTO_SECURE for mail which is gone.", SRCNAME, __func__); TBREAK; } mail->setDoAutosecure_m (false); TBREAK; } case (CONFIG_KEY_DONE): { log_debug ("%s:%s: Key configuration done.", SRCNAME, __func__); Addressbook::update_key_o (ctx->data); TBREAK; } case (DECRYPT): { auto mail = (Mail*) ctx->data; if (!Mail::isValidPtr (mail)) { log_debug ("%s:%s: DECRYPT for mail which is gone.", SRCNAME, __func__); TBREAK; } log_debug ("%s:%s: Decrypting %p again.", SRCNAME, __func__, mail); mail->preProcessMessage_m (); mail->decryptVerify_o (); TBREAK; } case (AFTER_MOVE): { /* What happens here is that after moving an S/MIME mail we restore the message class to the value it was before the close that initiated the move. See where this message is sent in folder events for more details. */ auto data = (wm_after_move_data_t *) ctx->data; log_debug ("%s:%s: AfterMove.", SRCNAME, __func__); /* Try to get the message object that was moved. */ LPMESSAGE mapi_message = nullptr; ULONG utype = 0; HRESULT hr; hr = data->target_folder->OpenEntry (data->entry_id_len, (LPENTRYID) data->entry_id, &IID_IMessage, MAPI_BEST_ACCESS, &utype, (IUnknown **) &mapi_message); if (FAILED (hr) || !mapi_message) { log_error ("%s:%s: Failed to open mail in target folder: hr=0x%lx", SRCNAME, __func__, hr); gpgol_release (data->target_folder); xfree (data->entry_id); xfree (data); TBREAK; } memdbg_addRef (mapi_message); log_debug ("%s:%s: Restoring message class after move to: %s", SRCNAME, __func__, data->old_class); mapi_set_mesage_class (mapi_message, data->old_class); hr = mapi_message->SaveChanges (FORCE_SAVE); if (FAILED (hr)) { log_error ("%s:%s: Failed to save mail in target folder: hr=0x%lx", SRCNAME, __func__, hr); } gpgol_release (mapi_message); gpgol_release (data->target_folder); xfree (data->old_class); xfree (data->entry_id); xfree (data); TBREAK; } case SEND_MULTIPLE_MAILS: { auto mail = (Mail*) ctx->data; if (!Mail::isValidPtr (mail)) { log_debug ("%s:%s: SEND_MULTIPLE_MAILS for mail which is gone.", SRCNAME, __func__); TBREAK; } log_debug ("%s:%s: Send multiple mails for %p.", SRCNAME, __func__, mail); mail->splitAndSend_o (); TBREAK; } case SEND: { auto mail = (Mail*) ctx->data; if (!Mail::isValidPtr (mail)) { log_debug ("%s:%s: SEND for mail %p which is gone.", SRCNAME, __func__, mail); TBREAK; } log_debug ("%s:%s: Send for %p.", SRCNAME, __func__, mail); invoke_oom_method (mail->item (), "Send", nullptr); TBREAK; } case SHOW_PREVIEW: { auto mail = (Mail*) ctx->data; if (!Mail::isValidPtr (mail)) { log_dbg ("SHOW_PREVIEW for mail %p which is gone.", mail); TBREAK; } log_dbg ("SHOW_PREVIEW for %p", mail); mail->parsingDone_o (true); TBREAK; } default: log_debug ("%s:%s: Unknown msg %x", SRCNAME, __func__, ctx->wmsg_type); } TRETURN 0; } else if (message == WM_USER) { switch (wParam) { case (EXT_API_CLOSE_ALL): { log_debug ("%s:%s: Closing all mails.", SRCNAME, __func__); Mail::closeAllMails_o (); TRETURN 0; } default: log_debug ("%s:%s: Unknown external msg %i", SRCNAME, __func__, (int) wParam); } } else if (message == WM_COPYDATA) { log_debug ("%s:%s Received copydata message.", SRCNAME, __func__); if (!lParam) { STRANGEPOINT; TRETURN DefWindowProc(hWnd, message, wParam, lParam); } const COPYDATASTRUCT *cds = (COPYDATASTRUCT*)lParam; if (cds->dwData == EXT_API_CLOSE) { log_debug ("%s:%s CopyData with external close. Payload: %.*s", SRCNAME, __func__, (int)cds->cbData, (char *) cds->lpData); std::string uid ((char*) cds->lpData, cds->cbData); auto mail = Mail::getMailForUUID (uid.c_str ()); if (!mail) { log_error ("%s:%s Failed to find mail for: %s", SRCNAME, __func__, uid.c_str() ); TRETURN DefWindowProc(hWnd, message, wParam, lParam); } mail->refCurrentItem (); Mail::closeInspector_o (mail); TRACEPOINT; mail->close (); log_debug ("%s:%s: Close for %p uid: %s finished.", SRCNAME, __func__, mail, uid.c_str ()); mail->releaseCurrentItem(); TRETURN 0; } else if (cds->dwData == EXT_API_DECRYPT) { log_debug ("%s:%s CopyData with external decrypt. Payload: %.*s", SRCNAME, __func__, (int)cds->cbData, (char *) cds->lpData); std::string uid ((char*) cds->lpData, cds->cbData); auto mail = Mail::getMailForUUID (uid.c_str ()); if (!mail) { log_error ("%s:%s Failed to find mail for: %s", SRCNAME, __func__, uid.c_str() ); TRETURN DefWindowProc(hWnd, message, wParam, lParam); } log_debug ("%s:%s: Decrypting %p again.", SRCNAME, __func__, mail); mail->preProcessMessage_m (); mail->decryptVerify_o (); TRETURN 0; } } return DefWindowProc(hWnd, message, wParam, lParam); } HWND create_responder_window () { TSTART; size_t cls_name_len = strlen(RESPONDER_CLASS_NAME) + 1; char cls_name[cls_name_len]; if (g_responder_window) { TRETURN 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); TRETURN g_responder_window; } static int send_msg_to_ui_thread (wm_ctx_t *ctx) { TSTART; 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__); TRETURN -1; } SendMessage (responder, WM_USER + 42, 0, (LPARAM) ctx); TRETURN 0; } int do_in_ui_thread (gpgol_wmsg_type type, void *data) { TSTART; wm_ctx_t ctx = {NULL, UNKNOWN, 0, 0}; ctx.wmsg_type = type; ctx.data = data; log_debug ("%s:%s: Sending message of type %i", SRCNAME, __func__, type); if (send_msg_to_ui_thread (&ctx)) { TRETURN -1; } TRETURN ctx.err; } static DWORD WINAPI do_async (LPVOID arg) { TSTART; wm_ctx_t *ctx = (wm_ctx_t*) arg; log_debug ("%s:%s: Do async with type %i after %i ms", SRCNAME, __func__, ctx ? ctx->wmsg_type : -1, ctx->delay); if (ctx->delay) { Sleep (ctx->delay); } send_msg_to_ui_thread (ctx); xfree (ctx); TRETURN 0; } void do_in_ui_thread_async (gpgol_wmsg_type type, void *data, int delay) { TSTART; wm_ctx_t *ctx = (wm_ctx_t *) xcalloc (1, sizeof (wm_ctx_t)); ctx->wmsg_type = type; ctx->data = data; ctx->delay = delay; CloseHandle (CreateThread (NULL, 0, do_async, (LPVOID) ctx, 0, NULL)); TRETURN; } 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; /* What we do here is that we catch all WM_CLOSE messages that get to Outlook. Then we check if the last open Explorer is the target of the close. In set case we start our shutdown routine before we pass the WM_CLOSE to outlook */ switch (cwp->message) { case WM_CLOSE: { HWND lastChild = NULL; log_debug ("%s:%s: Got WM_CLOSE", SRCNAME, __func__); if (!GpgolAddin::get_instance() || !GpgolAddin::get_instance ()->get_application()) { TRACEPOINT; TBREAK; } LPDISPATCH explorers = get_oom_object (GpgolAddin::get_instance ()->get_application(), "Explorers"); if (!explorers) { log_error ("%s:%s: No explorers object", SRCNAME, __func__); TBREAK; } int count = get_oom_int (explorers, "Count"); if (count != 1) { log_debug ("%s:%s: More then one explorer. Not shutting down.", SRCNAME, __func__); gpgol_release (explorers); TBREAK; } LPDISPATCH explorer = get_oom_object (explorers, "Item(1)"); gpgol_release (explorers); if (!explorer) { TRACEPOINT; TBREAK; } /* Casting to LPOLEWINDOW and calling GetWindow succeeded in Outlook 2016 but always TRETURNed the number 1. So we need this hack. */ char *caption = get_oom_string (explorer, "Caption"); gpgol_release (explorer); if (!caption) { log_debug ("%s:%s: No caption.", SRCNAME, __func__); TBREAK; } /* 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. " "Shutting down.", SRCNAME, __func__); GpgolAddin::get_instance ()->shutdown(); TBREAK; } TBREAK; } 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__); GpgolAddin::get_instance ()->shutdown(); } */ break; default: // log_debug ("WM: %x", (unsigned int) cwp->message); 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() { TSTART; TRETURN SetWindowsHookEx (WH_CALLWNDPROC, gpgol_hook, NULL, GetCurrentThreadId()); } GPGRT_LOCK_DEFINE (invalidate_lock); static bool invalidation_in_progress; DWORD WINAPI delayed_invalidate_ui (LPVOID minsleep) { TSTART; if (invalidation_in_progress) { log_debug ("%s:%s: Invalidation canceled as it is in progress.", SRCNAME, __func__); TRETURN 0; } TRACEPOINT; invalidation_in_progress = true; gpgol_lock(&invalidate_lock); int sleep_ms = (intptr_t)minsleep; Sleep (sleep_ms); int i = 0; while (invalidation_blocked) { i++; if (i % 10 == 0) { log_debug ("%s:%s: Waiting for invalidation.", SRCNAME, __func__); } Sleep (100); /* Do we need an abort statement here? */ } do_in_ui_thread (INVALIDATE_UI, nullptr); TRACEPOINT; invalidation_in_progress = false; gpgol_unlock(&invalidate_lock); TRETURN 0; } DWORD WINAPI close_mail (LPVOID mail) { TSTART; do_in_ui_thread (CLOSE, mail); TRETURN 0; } void blockInv() { TSTART; invalidation_blocked++; log_oom ("%s:%s: Invalidation block count %i", SRCNAME, __func__, invalidation_blocked); TRETURN; } void unblockInv() { TSTART; invalidation_blocked--; log_oom ("%s:%s: Invalidation block count %i", SRCNAME, __func__, invalidation_blocked); if (invalidation_blocked < 0) { log_error ("%s:%s: Invalidation block mismatch", SRCNAME, __func__); invalidation_blocked = 0; } TRETURN; } void wm_register_pending_op (Mail *mail) { TSTART; gpgrt_lock_lock (&op_lock); const auto it = std::find (s_pending_ops.begin (), s_pending_ops.end (), mail); if (it != s_pending_ops.end ()) { log_err ("BUG: Double register for %p !!!", mail); gpgrt_lock_unlock (&op_lock); TRETURN; } + log_dbg ("Adding %p to pending operations.", mail); s_pending_ops.push_back (mail); gpgrt_lock_unlock (&op_lock); TRETURN; } +void +wm_unregister_pending_op (Mail *mail) +{ + TSTART; + gpgrt_lock_lock (&op_lock); + const auto it = std::find (s_pending_ops.begin (), s_pending_ops.end (), + mail); + if (it != s_pending_ops.end ()) + { + log_dbg ("Unregistering %p", mail); + s_pending_ops.erase (it); + } + else + { + log_err ("Failed to find %p as pending op.", mail); + } + gpgrt_lock_unlock (&op_lock); + TRETURN; +} + void wm_abort_pending_ops () { TSTART; gpgrt_lock_lock (&op_lock); log_dbg ("Aborting all pending and ready operations."); std::vector all_mails; all_mails.insert (all_mails.begin (), s_pending_ops.begin (), s_pending_ops.end ()); all_mails.insert (all_mails.begin (), s_ready_ops.begin (), s_ready_ops.end ()); for (Mail *mail: all_mails) { + log_dbg ("Removing %p from pending.", mail); if (!Mail::isValidPtr (mail)) { log_dbg ("Mail %p already gone", mail); continue; } - log_dbg ("Closing copy %p", mail); - if (mail->close ()) + if (mail->copyParent() && Mail::isValidPtr (mail->copyParent())) { - log_dbg ("Close failed"); + mail->copyParent ()->resetRecipients (); } } s_pending_ops.clear (); s_ready_ops.clear (); gpgrt_lock_unlock (&op_lock); TRETURN; } diff --git a/src/windowmessages.h b/src/windowmessages.h index 369c28e..913b1da 100644 --- a/src/windowmessages.h +++ b/src/windowmessages.h @@ -1,131 +1,134 @@ /* windowmessages.h - Helper functions for Window message exchange. * Copyright (C) 2015 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 . */ #ifndef WINDOWMESSAGES_H #define WINDOWMESSAGES_H #include #include "config.h" #include "mapihelp.h" #include class Mail; /** 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 = 1100, /* A large offset to avoid conflicts */ INVALIDATE_UI, /* The UI should be invalidated. */ PARSING_DONE, /* A mail was parsed. Data should be a pointer to the mail object. */ RECIPIENT_ADDED, /* A recipient was added. Data should be ptr to mail */ CLOSE, /* Close the message in the next event loop. */ CRYPTO_DONE, /* Sign / Encrypt done. */ WKS_NOTIFY, /* Show a WKS Notification. */ BRING_TO_FRONT, /* Bring the active Outlook window to the front. */ INVALIDATE_LAST_MAIL, REVERT_MAIL, CLEAR_REPLY_FORWARD, DO_AUTO_SECURE, DONT_AUTO_SECURE, CONFIG_KEY_DONE, DECRYPT, AFTER_MOVE, SEND_MULTIPLE_MAILS, SEND, SHOW_PREVIEW, /* Show mail contents before a verify is done */ /* External API, keep it stable! */ EXT_API_CLOSE = 1301, EXT_API_CLOSE_ALL = 1302, EXT_API_DECRYPT = 1303, } 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 */ int delay; } wm_ctx_t; typedef struct { LPMAPIFOLDER target_folder; char *entry_id; char *old_class; size_t entry_id_len; } wm_after_move_data_t; /** Create and register the responder window. The responder window should be */ HWND create_responder_window (); /** 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); /** Send a message to the UI thread but returns immediately without waiting for the execution. The delay is used in the detached thread to delay the sending of the actual message. */ void do_in_ui_thread_async (gpgol_wmsg_type type, void *data, int delay = 0); /** Create our filter before outlook Window Messages. */ HHOOK create_message_hook(); /** Block ui invalidation. The idea here is to further reduce UI invalidations because depending on timing they might crash. So we try to block invalidation for as long as it is a bad time for us. */ void blockInv (); /** Unblock ui invalidation */ void unblockInv (); DWORD WINAPI delayed_invalidate_ui (LPVOID minsleep_ms = 0); DWORD WINAPI close_mail (LPVOID); void wm_register_pending_op (Mail *mail); +void +wm_unregister_pending_op (Mail *mail); + void wm_abort_pending_ops (); #endif // WINDOWMESSAGES_H