diff --git a/src/cryptcontroller.cpp b/src/cryptcontroller.cpp index 1065f6e..5aa1022 100644 --- a/src/cryptcontroller.cpp +++ b/src/cryptcontroller.cpp @@ -1,1035 +1,1028 @@ /* @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 #include #include #include "common.h" #include #define DEBUG_RESOLVER 1 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) { log_debug ("%s:%s: CryptController ctor for %p encrypt %i sign %i inline %i.", SRCNAME, __func__, mail, encrypt, sign, mail->do_pgp_inline ()); m_recipient_addrs = mail->take_cached_recipients (); } CryptController::~CryptController() { log_debug ("%s:%s:%p", SRCNAME, __func__, m_mail); release_cArray (m_recipient_addrs); } int CryptController::collect_data () { /* Get the attachment info and the body. We need to do this before creating the engine's filter because sending the cancel to the engine with nothing for the engine to process. Will result in an error. This is actually a bug in our engine code but we better avoid triggering this bug because the engine sometimes hangs. Fixme: Needs a proper fix. */ /* 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->take_cached_plain_body (); if (body && !*body) { xfree (body); body = nullptr; } LPMESSAGE message = get_oom_base_message (m_mail->item ()); if (!message) { log_error ("%s:%s: Failed to get base message.", SRCNAME, __func__); } auto att_table = mapi_create_attach_table (message, 0); int n_att_usable = count_usable_attachments (att_table); if (!n_att_usable && !body) { gpgol_message_box (m_mail->get_window(), utf8_gettext ("Can't encrypt / sign an empty message."), utf8_gettext ("GpgOL"), MB_OK); gpgol_release (message); xfree (body); return -1; } bool do_inline = m_mail->do_pgp_inline (); if (n_att_usable && do_inline) { log_debug ("%s:%s: PGP Inline not supported for attachments." " Using PGP MIME", SRCNAME, __func__); do_inline = false; m_mail->set_do_pgp_inline (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 */ if (add_body_and_attachments (sink, message, att_table, m_mail, body, n_att_usable)) { log_error ("%s:%s: Collecting body and attachments failed.", SRCNAME, __func__); gpgol_release (message); return -1; } /* Message is no longer needed */ gpgol_release (message); /* Set the input buffer to start. */ m_input.seek (0, SEEK_SET); return 0; } int CryptController::lookup_fingerprints (const std::string &sigFpr, const std::vector recpFprs) { 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"); return -1; } ctx->setKeyListMode (GpgME::Local); GpgME::Error err; if (!sigFpr.empty()) { m_signer_key = ctx->key (sigFpr.c_str (), err, true); if (err || m_signer_key.isNull ()) { log_error ("%s:%s: failed to lookup key for '%s' with protocol '%s'", SRCNAME, __func__, sigFpr.c_str (), m_proto == GpgME::CMS ? "smime" : m_proto == GpgME::OpenPGP ? "openpgp" : "unknown"); return -1; } // reset context ctx = std::shared_ptr (GpgME::Context::createForProtocol (m_proto)); ctx->setKeyListMode (GpgME::Local); } if (!recpFprs.size()) { return 0; } // Convert recipient fingerprints char **cRecps = vector_to_cArray (recpFprs); err = ctx->startKeyListing (const_cast (cRecps)); if (err) { log_error ("%s:%s: failed to start recipient keylisting", SRCNAME, __func__); return -1; } do { m_recipients.push_back(ctx->nextKey(err)); } while (!err); m_recipients.pop_back(); release_cArray (cRecps); return 0; } int CryptController::parse_output (GpgME::Data &resolverOutput) { // Todo: Use Data::toString std::istringstream ss(resolverOutput.toString()); std::string line; std::string sigFpr; std::vector recpFprs; while (std::getline (ss, line)) { rtrim (line); if (line == "cancel") { log_debug ("%s:%s: resolver canceled", SRCNAME, __func__); return -2; } if (line == "unencrypted") { log_debug ("%s:%s: FIXME resolver wants unencrypted", SRCNAME, __func__); return -1; } std::istringstream lss (line); // First is sig or enc std::string what; std::string how; std::string fingerprint; std::getline (lss, what, ':'); std::getline (lss, how, ':'); std::getline (lss, fingerprint, ':'); if (m_proto == GpgME::UnknownProtocol) { m_proto = (how == "smime") ? GpgME::CMS : GpgME::OpenPGP; } if (what == "sig") { if (!sigFpr.empty ()) { log_error ("%s:%s: multiple signing keys not supported", SRCNAME, __func__); } sigFpr = fingerprint; continue; } if (what == "enc") { recpFprs.push_back (fingerprint); } } if (m_sign && sigFpr.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__); return -1; } return lookup_fingerprints (sigFpr, recpFprs); } int CryptController::resolve_keys_cached() { const auto cache = KeyCache::instance(); bool fallbackToSMIME = false; if (m_encrypt) { const auto cached_sender = m_mail->get_cached_sender (); auto recps = cArray_to_vector ((const char**) m_recipient_addrs); recps.push_back (cached_sender); m_recipients = cache->getEncryptionKeys(recps, GpgME::OpenPGP); m_proto = GpgME::OpenPGP; if (m_recipients.empty() && opt.enable_smime) { m_recipients = cache->getEncryptionKeys(recps, GpgME::CMS); fallbackToSMIME = true; m_proto = GpgME::CMS; } if (m_recipients.empty()) { log_debug ("%s:%s: Failed to resolve keys through cache", SRCNAME, __func__); m_proto = GpgME::UnknownProtocol; return 1; } } if (m_sign) { if (!fallbackToSMIME) { m_signer_key = cache->getSigningKey (m_mail->get_cached_sender ().c_str (), GpgME::OpenPGP); m_proto = GpgME::OpenPGP; } if (m_signer_key.isNull() && opt.enable_smime) { m_signer_key = cache->getSigningKey (m_mail->get_cached_sender ().c_str (), GpgME::CMS); m_proto = GpgME::CMS; } if (m_signer_key.isNull()) { log_debug ("%s:%s: Failed to resolve signer key through cache", SRCNAME, __func__); m_recipients.clear(); m_proto = GpgME::UnknownProtocol; return 1; } } return 0; } int CryptController::resolve_keys () { m_recipients.clear(); if (opt.autoresolve && !resolve_keys_cached ()) { log_debug ("%s:%s: resolved keys through the cache", SRCNAME, __func__); start_crypto_overlay(); return 0; } std::vector args; // Collect the arguments char *gpg4win_dir = get_gpg4win_dir (); if (!gpg4win_dir) { TRACEPOINT; return -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->get_window (); 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 (_("Resolving recipients..."))); } else if (m_sign) { args.push_back (std::string (_("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->get_cached_sender (); 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) { args.push_back (std::string ("--alwaysShow")); } if (m_encrypt) { args.push_back (std::string ("--encrypt")); // Get the recipients that are cached from OOM for (size_t i = 0; m_recipient_addrs && m_recipient_addrs[i]; i++) { args.push_back (GpgME::UserID::addrSpecFromString (m_recipient_addrs[i])); } } args.push_back (std::string ("--lang")); args.push_back (std::string (gettext_localename ())); // Args are prepared. Spawn the resolver. auto ctx = GpgME::Context::createForEngine (GpgME::SpawnEngine); if (!ctx) { // can't happen TRACEPOINT; return -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); #ifdef DEBUG_RESOLVER log_debug ("Spawning args:"); for (size_t i = 0; cargs && cargs[i]; i++) { log_debug (SIZE_T_FORMAT ": '%s'", i, cargs[i]); } #endif 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(); #ifdef DEBUG_RESOLVER log_debug ("Resolver stdout:\n'%s'", mystdout.toString ().c_str ()); log_debug ("Resolver stderr:\n'%s'", mystderr.toString ().c_str ()); #endif 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_debug ("Resolver stdout:\n'%s'", mystdout.toString ().c_str ()); log_debug ("Resolver stderr:\n'%s'", mystderr.toString ().c_str ()); return -1; } return ret; } int CryptController::do_crypto () { log_debug ("%s:%s", SRCNAME, __func__); /* Start a WKS check if necessary. */ WKSHelper::instance()->start_check (m_mail->get_cached_sender ()); int ret = resolve_keys (); if (ret == -1) { //error log_debug ("%s:%s: Failure to resolve keys.", SRCNAME, __func__); return -1; } if (ret == -2) { // Cancel return -2; } bool do_inline = m_mail->do_pgp_inline (); 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->set_do_pgp_inline (false); m_bodyInput = GpgME::Data(GpgME::Data::null); } auto ctx = std::shared_ptr (GpgME::Context::createForProtocol(m_proto)); if (!ctx) { log_error ("%s:%s: Failure to create context.", SRCNAME, __func__); gpgol_message_box (m_mail->get_window (), "Failure to create context.", utf8_gettext ("GpgOL"), MB_OK); return -1; } if (!m_signer_key.isNull()) { ctx->addSigningKey (m_signer_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_recipients, do_inline ? m_bodyInput : m_input, m_output, GpgME::Context::AlwaysTrust); if (result_pair.first.error() || result_pair.second.error()) { log_error ("%s:%s: Encrypt / Sign error %s %s.", SRCNAME, __func__, result_pair.first.error().asString(), result_pair.second.error().asString()); return -1; } if (result_pair.first.error().isCanceled() || result_pair.second.error().isCanceled()) { log_debug ("%s:%s: User cancled", SRCNAME, __func__); return -2; } } else if (m_encrypt && m_sign) { // First sign then encrypt const auto sigResult = ctx->sign (m_input, m_output, GpgME::Detached); if (sigResult.error()) { log_error ("%s:%s: Signing error %s.", SRCNAME, __func__, sigResult.error().asString()); return -1; } if (sigResult.error().isCanceled()) { log_debug ("%s:%s: User cancled", SRCNAME, __func__); return -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; return -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_recipients, multipart, m_output, GpgME::Context::AlwaysTrust); if (encResult.error()) { log_error ("%s:%s: Encryption error %s.", SRCNAME, __func__, encResult.error().asString()); return -1; } if (encResult.error().isCanceled()) { log_debug ("%s:%s: User cancled", SRCNAME, __func__); return -2; } // Now we have encrypted output just treat it like encrypted. } else if (m_encrypt) { const auto result = ctx->encrypt (m_recipients, do_inline ? m_bodyInput : m_input, m_output, GpgME::Context::AlwaysTrust); if (result.error()) { log_error ("%s:%s: Encryption error %s.", SRCNAME, __func__, result.error().asString()); return -1; } if (result.error().isCanceled()) { log_debug ("%s:%s: User cancled", SRCNAME, __func__); return -2; } } else if (m_sign) { const auto result = ctx->sign (do_inline ? m_bodyInput : m_input, m_output, do_inline ? GpgME::Clearsigned : GpgME::Detached); if (result.error()) { log_error ("%s:%s: Signing error %s.", SRCNAME, __func__, result.error().asString()); return -1; } if (result.error().isCanceled()) { log_debug ("%s:%s: User cancled", SRCNAME, __func__); return -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; return 0; } static int write_data (sink_t sink, GpgME::Data &data) { if (!sink || !sink->writefnc) { return -1; } char buf[4096]; size_t nread; data.seek (0, SEEK_SET); while ((nread = data.read (buf, 4096)) > 0) { sink->writefnc (sink, buf, nread); } return 0; } int create_sign_attach (sink_t sink, protocol_t protocol, GpgME::Data &signature, GpgME::Data &signedData, const char *micalg) { 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; return rc; } /* Write the boundary so that it is not included in the hashing. */ if ((rc = write_boundary (sink, boundary, 0))) { TRACEPOINT; return rc; } /* Write the signed mime structure */ if ((rc = write_data (sink, signedData))) { TRACEPOINT; return rc; } /* Write the signature attachment */ if ((rc = write_boundary (sink, boundary, 0))) { TRACEPOINT; return rc; } if (protocol == PROTOCOL_OPENPGP) { rc = write_string (sink, "Content-Type: application/pgp-signature\r\n"); } else { rc = write_string (sink, "Content-Transfer-Encoding: base64\r\n" "Content-Type: application/pkcs7-signature\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; return rc; } if ((rc = write_string (sink, "\r\n"))) { TRACEPOINT; return 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; return rc; } } else if ((rc = write_data (sink, signature))) { TRACEPOINT; return rc; } // Add an extra linefeed with should not harm. if ((rc = write_string (sink, "\r\n"))) { TRACEPOINT; return rc; } /* Write the final boundary. */ if ((rc = write_boundary (sink, boundary, 1))) { TRACEPOINT; return rc; } return rc; } static int create_encrypt_attach (sink_t sink, protocol_t protocol, GpgME::Data &encryptedData) { char boundary[BOUNDARYSIZE+1]; int rc = create_top_encryption_header (sink, protocol, boundary, false); // From here on use goto failure pattern. if (rc) { log_error ("%s:%s: Failed to create top header.", SRCNAME, __func__); return rc; } - if (protocol == PROTOCOL_OPENPGP) - { - rc = write_data (sink, encryptedData); - } - else - { - const auto encStr = encryptedData.toString(); - rc = write_b64 (sink, encStr.c_str(), encStr.size()); - } + rc = write_data (sink, encryptedData); + if (rc) { log_error ("%s:%s: Failed to create top header.", SRCNAME, __func__); return 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__); } return rc; } int CryptController::update_mail_mapi () { log_debug ("%s:%s", SRCNAME, __func__); if (m_mail->do_pgp_inline ()) { // Nothing to do for inline. log_debug ("%s:%s: Inline mail. No MAPI update.", SRCNAME, __func__); return 0; } LPMESSAGE message = get_oom_base_message (m_mail->item()); if (!message) { log_error ("%s:%s: Failed to obtain message.", SRCNAME, __func__); return -1; } mapi_attach_item_t *att_table = mapi_create_attach_table (message, 0); // 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 multipart/encrypted // content type. Otherwise newer (2016) exchange servers will throw // an M2MCVT.StorageError.Exeption (See GnuPG-Bug-Id: T3853 ) std::string overrideMimeTag; if (m_proto == GpgME::CMS && m_encrypt) { 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); return -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); } else if (m_encrypt) { rc = create_encrypt_attach (sink, protocol, m_output); } 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); } // only on error. if (rc) { cancel_mapi_attachment (&attach, sink); } // cleanup mapi_release_attach_table (att_table); gpgol_release (attach); gpgol_release (message); return rc; } std::string CryptController::get_inline_data () { std::string ret; if (!m_mail->do_pgp_inline ()) { return 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); } return ret; } void CryptController::parse_micalg (const GpgME::SigningResult &result) { if (result.isNull()) { TRACEPOINT; return; } const auto signature = result.createdSignature(0); if (signature.isNull()) { TRACEPOINT; return; } const char *hashAlg = signature.hashAlgorithmAsString (); if (!hashAlg) { TRACEPOINT; return; } 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 ()); } void CryptController::start_crypto_overlay () { auto wid = m_mail->get_window (); std::string text; if (m_encrypt) { text = _("Encrypting..."); } else if (m_sign) { text = _("Signing..."); } m_overlay = std::unique_ptr (new Overlay (wid, text)); } diff --git a/src/cryptcontroller.h b/src/cryptcontroller.h index 67b67c4..8bcf1d4 100644 --- a/src/cryptcontroller.h +++ b/src/cryptcontroller.h @@ -1,90 +1,96 @@ /* @file cryptcontroller.h * @brief Helper to handle sign and encrypt * * 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 . */ #ifndef CRYPTCONTROLLER_H #define CRYPTCONTROLLER_H #ifdef HAVE_CONFIG_H #include #endif #include class Mail; class Overlay; namespace GpgME { class SigningResult; } // namespace GpgME class CryptController { public: /** @brief Construct a Crypthelper for a Mail object. */ CryptController (Mail *mail, bool encrypt, bool sign, GpgME::Protocol proto); ~CryptController (); /** @brief Collect the data from the mail into the internal data structures. @returns 0 on success. */ int collect_data (); /** @brief Does the actual crypto work according to op. Can be called in a different thread then the UI Thread. @returns 0 on success. */ int do_crypto (); /** @brief Update the MAPI structure of the mail with the result. */ int update_mail_mapi (); /** @brief Get an inline body as std::string. */ std::string get_inline_data (); + /** @brief Get the protocol. Valid after do_crypto. */ + GpgME::Protocol get_protocol () const { return m_proto; } + + /** @brief check weather something was encrypted. */ + bool is_encrypter () const { return m_encrypt; } + private: int resolve_keys (); int resolve_keys_cached (); int parse_output (GpgME::Data &resolverOutput); int lookup_fingerprints (const std::string &sigFpr, const std::vector recpFprs); void parse_micalg (const GpgME::SigningResult &sResult); void start_crypto_overlay (); private: Mail *m_mail; GpgME::Data m_input, m_bodyInput, m_signedData, m_output; std::string m_micalg; bool m_encrypt, m_sign, m_crypto_success; GpgME::Protocol m_proto; GpgME::Key m_signer_key; std::vector m_recipients; std::unique_ptr m_overlay; char **m_recipient_addrs; }; #endif diff --git a/src/mail.cpp b/src/mail.cpp index df7d0f2..ea2f65a 100644 --- a/src/mail.cpp +++ b/src/mail.cpp @@ -1,2876 +1,2891 @@ /* @file mail.h * @brief High level class to work with Outlook Mailitems. * * Copyright (C) 2015, 2016 by Bundesamt für Sicherheit in der Informationstechnik * Software engineering by Intevation GmbH * * This file is part of GpgOL. * * GpgOL is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * GpgOL is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, see . */ #include "config.h" #include "dialogs.h" #include "common.h" #include "mail.h" #include "eventsinks.h" #include "attachment.h" #include "mapihelp.h" #include "mimemaker.h" #include "message.h" #include "revert.h" #include "gpgoladdin.h" #include "mymapitags.h" #include "parsecontroller.h" #include "cryptcontroller.h" #include "gpgolstr.h" #include "windowmessages.h" #include "mlang-charset.h" #include "wks-helper.h" #include "keycache.h" #include "cpphelp.h" #include #include #include #include #include #include #include #include #include #include #include #include #undef _ #define _(a) utf8_gettext (a) using namespace GpgME; static std::map s_mail_map; static std::map s_uid_map; static std::set uids_searched; static Mail *s_last_mail; #define COPYBUFSIZE (8 * 1024) Mail::Mail (LPDISPATCH mailitem) : m_mailitem(mailitem), m_processed(false), m_needs_wipe(false), m_needs_save(false), m_crypt_successful(false), m_is_smime(false), m_is_smime_checked(false), m_is_signed(false), m_is_valid(false), m_close_triggered(false), m_is_html_alternative(false), m_needs_encrypt(false), m_moss_position(0), m_crypto_flags(0), m_cached_html_body(nullptr), m_cached_plain_body(nullptr), m_cached_recipients(nullptr), m_type(MSGTYPE_UNKNOWN), m_do_inline(false), m_is_gsuite(false), m_crypt_state(NoCryptMail), m_window(nullptr), m_async_crypt_disabled(false), m_is_forwarded_crypto_mail(false) { if (get_mail_for_item (mailitem)) { log_error ("Mail object for item: %p already exists. Bug.", mailitem); return; } m_event_sink = install_MailItemEvents_sink (mailitem); if (!m_event_sink) { /* Should not happen but in that case we don't add us to the list and just release the Mail item. */ log_error ("%s:%s: Failed to install MailItemEvents sink.", SRCNAME, __func__); gpgol_release(mailitem); return; } s_mail_map.insert (std::pair (mailitem, this)); s_last_mail = this; } GPGRT_LOCK_DEFINE(dtor_lock); Mail::~Mail() { /* This should fix a race condition where the mail is deleted before the parser is accessed in the decrypt thread. The shared_ptr of the parser then ensures that the parser is alive even if the mail is deleted while parsing. */ gpgrt_lock_lock (&dtor_lock); log_oom_extra ("%s:%s: dtor: Mail: %p item: %p", SRCNAME, __func__, this, m_mailitem); std::map::iterator it; log_oom_extra ("%s:%s: Detaching event sink", SRCNAME, __func__); detach_MailItemEvents_sink (m_event_sink); gpgol_release(m_event_sink); log_oom_extra ("%s:%s: Erasing mail", SRCNAME, __func__); it = s_mail_map.find(m_mailitem); if (it != s_mail_map.end()) { s_mail_map.erase (it); } if (!m_uuid.empty()) { auto it2 = s_uid_map.find(m_uuid); if (it2 != s_uid_map.end()) { s_uid_map.erase (it2); } } log_oom_extra ("%s:%s: releasing mailitem", SRCNAME, __func__); gpgol_release(m_mailitem); xfree (m_cached_html_body); xfree (m_cached_plain_body); release_cArray (m_cached_recipients); if (!m_uuid.empty()) { log_oom_extra ("%s:%s: destroyed: %p uuid: %s", SRCNAME, __func__, this, m_uuid.c_str()); } else { log_oom_extra ("%s:%s: non crypto (or sent) mail: %p destroyed", SRCNAME, __func__, this); } log_oom_extra ("%s:%s: nulling shared pointer", SRCNAME, __func__); m_parser = nullptr; m_crypter = nullptr; gpgrt_lock_unlock (&dtor_lock); log_oom_extra ("%s:%s: returning", SRCNAME, __func__); } Mail * Mail::get_mail_for_item (LPDISPATCH mailitem) { if (!mailitem) { return NULL; } std::map::iterator it; it = s_mail_map.find(mailitem); if (it == s_mail_map.end()) { return NULL; } return it->second; } Mail * Mail::get_mail_for_uuid (const char *uuid) { if (!uuid) { return NULL; } auto it = s_uid_map.find(std::string(uuid)); if (it == s_uid_map.end()) { return NULL; } return it->second; } bool Mail::is_valid_ptr (const Mail *mail) { auto it = s_mail_map.begin(); while (it != s_mail_map.end()) { if (it->second == mail) return true; ++it; } return false; } int Mail::pre_process_message () { LPMESSAGE message = get_oom_base_message (m_mailitem); if (!message) { log_error ("%s:%s: Failed to get base message.", SRCNAME, __func__); return 0; } log_oom_extra ("%s:%s: GetBaseMessage OK 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) { gpgol_release (message); return 0; } /* Create moss attachments here so that they are properly hidden when the item is read into the model. */ m_moss_position = mapi_mark_or_create_moss_attach (message, m_type); if (!m_moss_position) { log_error ("%s:%s: Failed to find moss attachment.", SRCNAME, __func__); m_type = MSGTYPE_UNKNOWN; } gpgol_release (message); return 0; } static LPDISPATCH get_attachment (LPDISPATCH mailitem, int pos) { LPDISPATCH attachment; LPDISPATCH attachments = get_oom_object (mailitem, "Attachments"); if (!attachments) { log_debug ("%s:%s: Failed to get attachments.", SRCNAME, __func__); return NULL; } std::string item_str; int count = get_oom_int (attachments, "Count"); if (count < 1) { log_debug ("%s:%s: Invalid attachment count: %i.", SRCNAME, __func__, count); gpgol_release (attachments); return 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); return attachment; } /** Helper to check that all attachments are hidden, to be called before crypto. */ int Mail::check_attachments () const { LPDISPATCH attachments = get_oom_object (m_mailitem, "Attachments"); if (!attachments) { log_debug ("%s:%s: Failed to get attachments.", SRCNAME, __func__); return 1; } int count = get_oom_int (attachments, "Count"); if (!count) { gpgol_release (attachments); return 0; } std::string message; if (is_encrypted () && is_signed ()) { message += _("Not all attachments were encrypted or signed.\n" "The unsigned / unencrypted attachments are:\n\n"); } else if (is_signed ()) { message += _("Not all attachments were signed.\n" "The unsigned attachments are:\n\n"); } else if (is_encrypted ()) { message += _("Not all attachments were encrypted.\n" "The unencrypted attachments are:\n\n"); } else { gpgol_release (attachments); return 0; } bool foundOne = false; for (int i = 1; i <= count; i++) { std::string item_str; item_str = std::string("Item(") + std::to_string (i) + ")"; LPDISPATCH oom_attach = get_oom_object (attachments, item_str.c_str ()); if (!oom_attach) { log_error ("%s:%s: Failed to get attachment.", SRCNAME, __func__); continue; } VARIANT var; VariantInit (&var); if (get_pa_variant (oom_attach, PR_ATTACHMENT_HIDDEN_DASL, &var) || (var.vt == VT_BOOL && var.boolVal == VARIANT_FALSE)) { foundOne = true; char *dispName = get_oom_string (oom_attach, "DisplayName"); message += dispName ? dispName : "Unknown"; xfree (dispName); message += "\n"; } VariantClear (&var); gpgol_release (oom_attach); } gpgol_release (attachments); if (foundOne) { message += "\n"; message += _("Note: The attachments may be encrypted or signed " "on a file level but the GpgOL status does not apply to them."); wchar_t *wmsg = utf8_to_wchar (message.c_str ()); wchar_t *wtitle = utf8_to_wchar (_("GpgOL Warning")); MessageBoxW (get_active_hwnd (), wmsg, wtitle, MB_ICONWARNING|MB_OK); xfree (wmsg); xfree (wtitle); } return 0; } /** Get the cipherstream of the mailitem. */ static LPSTREAM get_attachment_stream (LPDISPATCH mailitem, int pos) { if (!pos) { log_debug ("%s:%s: Called with zero pos.", SRCNAME, __func__); return NULL; } LPDISPATCH attachment = get_attachment (mailitem, pos); 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__); return NULL; } hr = message->OpenProperty (PR_BODY_A, &IID_IStream, 0, 0, (LPUNKNOWN*)&stream); if (hr) { log_debug ("%s:%s: OpenProperty failed: hr=%#lx", SRCNAME, __func__, hr); return NULL; } return 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); return NULL; } if (FAILED (mapi_attachment->OpenProperty (PR_ATTACH_DATA_BIN, &IID_IStream, 0, MAPI_MODIFY, (LPUNKNOWN*) &stream))) { log_debug ("%s:%s: Failed to open stream for mapi_attachment: %p", SRCNAME, __func__, mapi_attachment); gpgol_release (mapi_attachment); } return stream; } #if 0 This should work. But Outlook says no. See the comment in set_pa_variant about this. I left the code here as an example how to work with safearrays and how this probably should work. static int copy_data_property(LPDISPATCH target, std::shared_ptr attach) { VARIANT var; VariantInit (&var); /* Get the size */ off_t size = attach->get_data ().seek (0, SEEK_END); attach->get_data ().seek (0, SEEK_SET); if (!size) { TRACEPOINT; return 1; } if (!get_pa_variant (target, PR_ATTACH_DATA_BIN_DASL, &var)) { log_debug("Have variant. type: %x", var.vt); } else { log_debug("failed to get variant."); } /* Set the type to an array of unsigned chars (OLE SAFEARRAY) */ var.vt = VT_ARRAY | VT_UI1; /* Set up the bounds structure */ SAFEARRAYBOUND rgsabound[1]; rgsabound[0].cElements = static_cast (size); rgsabound[0].lLbound = 0; /* Create an OLE SAFEARRAY */ var.parray = SafeArrayCreate (VT_UI1, 1, rgsabound); if (var.parray == NULL) { TRACEPOINT; VariantClear(&var); return 1; } void *buffer = NULL; /* Get a safe pointer to the array */ if (SafeArrayAccessData(var.parray, &buffer) != S_OK) { TRACEPOINT; VariantClear(&var); return 1; } /* Copy data to it */ size_t nread = attach->get_data ().read (buffer, static_cast (size)); if (nread != static_cast (size)) { TRACEPOINT; VariantClear(&var); return 1; } /*/ Unlock the variant data */ if (SafeArrayUnaccessData(var.parray) != S_OK) { TRACEPOINT; VariantClear(&var); return 1; } if (set_pa_variant (target, PR_ATTACH_DATA_BIN_DASL, &var)) { TRACEPOINT; VariantClear(&var); return 1; } VariantClear(&var); return 0; } #endif static int copy_attachment_to_file (std::shared_ptr att, HANDLE hFile) { char copybuf[COPYBUFSIZE]; size_t nread; /* Security considerations: Writing the data to a temporary file is necessary as neither MAPI manipulation works in the read event to transmit the data nor Property Accessor works (see above). From a security standpoint there is a short time where the temporary files are on disk. Tempdir should be protected so that only the user can read it. Thus we have a local attack that could also take the data out of Outlook. FILE_SHARE_READ is necessary so that outlook can read the file. A bigger concern is that the file is manipulated by another software to fake the signature state. So we keep the write exlusive to us. We delete the file before closing the write file handle. */ /* Make sure we start at the beginning */ att->get_data ().seek (0, SEEK_SET); while ((nread = att->get_data ().read (copybuf, COPYBUFSIZE))) { DWORD nwritten; if (!WriteFile (hFile, copybuf, nread, &nwritten, NULL)) { log_error ("%s:%s: Failed to write in tmp attachment.", SRCNAME, __func__); return 1; } if (nread != nwritten) { log_error ("%s:%s: Write truncated.", SRCNAME, __func__); return 1; } } return 0; } /** Sets some meta data on the last attachment added. The meta data is taken from the attachment object. */ static int fixup_last_attachment (LPDISPATCH mail, std::shared_ptr attachment) { /* Currently we only set content id */ if (attachment->get_content_id ().empty()) { log_debug ("%s:%s: Content id not found.", SRCNAME, __func__); return 0; } LPDISPATCH attach = get_attachment (mail, -1); if (!attach) { log_error ("%s:%s: No attachment.", SRCNAME, __func__); return 1; } int ret = put_pa_string (attach, PR_ATTACH_CONTENT_ID_DASL, attachment->get_content_id ().c_str()); gpgol_release (attach); return ret; } /** Helper to update the attachments of a mail object in oom. does not modify the underlying mapi structure. */ static int add_attachments(LPDISPATCH mail, std::vector > attachments) { int err = 0; for (auto att: attachments) { if (att->get_display_name().empty()) { log_error ("%s:%s: Ignoring attachment without display name.", SRCNAME, __func__); continue; } wchar_t* wchar_name = utf8_to_wchar (att->get_display_name().c_str()); HANDLE hFile; wchar_t* wchar_file = get_tmp_outfile (wchar_name, &hFile); if (copy_attachment_to_file (att, hFile)) { log_error ("%s:%s: Failed to copy attachment %s to temp file", SRCNAME, __func__, att->get_display_name().c_str()); err = 1; } if (add_oom_attachment (mail, wchar_file, wchar_name)) { log_error ("%s:%s: Failed to add attachment: %s", SRCNAME, __func__, att->get_display_name().c_str()); err = 1; } if (!DeleteFileW (wchar_file)) { log_error ("%s:%s: Failed to delete tmp attachment for: %s", SRCNAME, __func__, att->get_display_name().c_str()); err = 1; } xfree (wchar_file); xfree (wchar_name); err = fixup_last_attachment (mail, att); } return err; } GPGRT_LOCK_DEFINE(parser_lock); static DWORD WINAPI do_parsing (LPVOID arg) { gpgrt_lock_lock (&dtor_lock); /* We lock with mail dtors so we can be sure the mail->parser call is valid. */ Mail *mail = (Mail *)arg; if (!Mail::is_valid_ptr (mail)) { log_debug ("%s:%s: canceling parsing for: %p already deleted", SRCNAME, __func__, arg); gpgrt_lock_unlock (&dtor_lock); return 0; } /* This takes a shared ptr of parser. So the parser is still valid when the mail is deleted. */ auto parser = mail->parser(); gpgrt_lock_unlock (&dtor_lock); gpgrt_lock_lock (&parser_lock); gpgrt_lock_lock (&invalidate_lock); /* We lock the parser here to avoid too many decryption attempts if there are multiple mailobjects which might have already been deleted (e.g. by quick switches of the mailview.) Let's rather be a bit slower. */ log_debug ("%s:%s: preparing the parser for: %p", SRCNAME, __func__, arg); if (!Mail::is_valid_ptr (mail)) { log_debug ("%s:%s: cancel for: %p already deleted", SRCNAME, __func__, arg); gpgrt_lock_unlock (&invalidate_lock); gpgrt_lock_unlock (&parser_lock); return 0; } if (!parser) { log_error ("%s:%s: no parser found for mail: %p", SRCNAME, __func__, arg); gpgrt_lock_unlock (&invalidate_lock); gpgrt_lock_unlock (&parser_lock); return -1; } parser->parse(); do_in_ui_thread (PARSING_DONE, arg); gpgrt_lock_unlock (&invalidate_lock); gpgrt_lock_unlock (&parser_lock); return 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 NoCryptMail Needs Crypto ? (get_gpgol_draft_info_flags != 0) -> No: Pass send -> unencrypted mail. -> Yes: mail->update_oom_data State = Mail::NeedsFirstAfterWrite check_inline_response invoke_oom_method (m_object, "Save", NULL); > Write Event < Pass because is_crypto_mail is false (not a decrypted mail) > AfterWrite Event < | State NeedsFirstAfterWrite State = NeedsActualCrypo encrypt_sign_start collect_input_data -> Check if Inline PGP should be used do_crypt -> Resolve keys / do crypto State = NeedsUpdateInMAPI update_crypt_mapi crypter->update_mail_mapi if (inline) (Meaning PGP/Inline) <-- do nothing. else build MSOXSMIME attachment and clear body / attachments. State = NeedsUpdateInOOM <- Back to Send Event update_crypt_oom -> Cleans body or sets PGP/Inline body. (inline_body_to_body) State = WantsSendMIME or WantsSendInline -> Saftey check "has_crypted_or_empty_body" -> If MIME Mail do the T3656 check. Send. State order for "inline_response" (sync) Mails. NoCryptMail NeedsFirstAfterWrite NeedsActualCrypto NeedsUpdateInMAPI NeedsUpdateInOOM WantsSendMIME (or inline for PGP Inline) -> Send. State order for async Mails NoCryptMail NeedsFirstAfterWrite NeedsActualCrypto -> Cancel Send. Windowmessages -> Crypto Done NeedsUpdateInOOM NeedsSecondAfterWrite trigger Save. NeedsUpdateInMAPI WantsSendMIME trigger Send. */ static DWORD WINAPI do_crypt (LPVOID arg) { gpgrt_lock_lock (&dtor_lock); /* We lock with mail dtors so we can be sure the mail->parser call is valid. */ Mail *mail = (Mail *)arg; if (!Mail::is_valid_ptr (mail)) { log_debug ("%s:%s: canceling crypt for: %p already deleted", SRCNAME, __func__, arg); gpgrt_lock_unlock (&dtor_lock); return 0; } if (mail->crypt_state() != Mail::NeedsActualCrypt) { log_debug ("%s:%s: invalid state %i", SRCNAME, __func__, mail->crypt_state ()); mail->set_window_enabled (true); gpgrt_lock_unlock (&dtor_lock); return -1; } /* This takes a shared ptr of crypter. So the crypter is still valid when the mail is deleted. */ auto crypter = mail->crypter(); gpgrt_lock_unlock (&dtor_lock); if (!crypter) { log_error ("%s:%s: no crypter found for mail: %p", SRCNAME, __func__, arg); gpgrt_lock_unlock (&parser_lock); mail->set_window_enabled (true); return -1; } int rc = crypter->do_crypto(); gpgrt_lock_lock (&dtor_lock); if (!Mail::is_valid_ptr (mail)) { log_debug ("%s:%s: aborting crypt for: %p already deleted", SRCNAME, __func__, arg); gpgrt_lock_unlock (&dtor_lock); return 0; } if (rc == -1) { mail->reset_crypter (); crypter = nullptr; gpgol_bug (mail->get_window (), ERR_CRYPT_RESOLVER_FAILED); } mail->set_window_enabled (true); if (rc) { log_debug ("%s:%s: crypto failed for: %p with: %i", SRCNAME, __func__, arg, rc); mail->set_crypt_state (Mail::NoCryptMail); mail->reset_crypter (); crypter = nullptr; gpgrt_lock_unlock (&dtor_lock); return rc; } if (!mail->async_crypt_disabled ()) { mail->set_crypt_state (Mail::NeedsUpdateInOOM); gpgrt_lock_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 { mail->set_crypt_state (Mail::NeedsUpdateInMAPI); mail->update_crypt_mapi (); if (mail->crypt_state () == Mail::WantsSendMIME) { // For sync crypto we need to switch this. mail->set_crypt_state (Mail::NeedsUpdateInOOM); } else { // A bug! log_debug ("%s:%s: Resetting crypter because of state mismatch. %p", SRCNAME, __func__, arg); crypter = nullptr; mail->reset_crypter (); } gpgrt_lock_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); log_debug ("%s:%s: crypto thread for %p finished", SRCNAME, __func__, arg); return 0; } bool Mail::is_crypto_mail() const { if (m_type == MSGTYPE_UNKNOWN || m_type == MSGTYPE_GPGOL || m_type == MSGTYPE_SMIME) { /* Not a message for us. */ return false; } return true; } int Mail::decrypt_verify() { if (!is_crypto_mail()) { log_debug ("%s:%s: Decrypt Verify for non crypto mail: %p.", SRCNAME, __func__, m_mailitem); return 0; } if (m_needs_wipe) { log_error ("%s:%s: Decrypt verify called for msg that needs wipe: %p", SRCNAME, __func__, m_mailitem); return 1; } set_uuid (); m_processed = true; /* Insert placeholder */ char *placeholder_buf; if (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 (gpgrt_asprintf (&placeholder_buf, opt.prefer_html ? decrypt_template_html : decrypt_template, is_smime() ? "S/MIME" : "OpenPGP", _("Encrypted message"), _("Please wait while the message is being decrypted / verified...")) == -1) { log_error ("%s:%s: Failed to format placeholder.", SRCNAME, __func__); return 1; } if (opt.prefer_html) { m_orig_body = get_oom_string (m_mailitem, "HTMLBody"); if (put_oom_string (m_mailitem, "HTMLBody", placeholder_buf)) { log_error ("%s:%s: Failed to modify html body of item.", SRCNAME, __func__); } } else { m_orig_body = get_oom_string (m_mailitem, "Body"); if (put_oom_string (m_mailitem, "Body", placeholder_buf)) { log_error ("%s:%s: Failed to modify body of item.", SRCNAME, __func__); } } xfree (placeholder_buf); /* Do the actual parsing */ auto cipherstream = get_attachment_stream (m_mailitem, m_moss_position); if (m_type == MSGTYPE_GPGOL_WKS_CONFIRMATION) { WKSHelper::instance ()->handle_confirmation_read (this, cipherstream); return 0; } if (!cipherstream) { log_debug ("%s:%s: Failed to get cipherstream.", SRCNAME, __func__); return 1; } m_parser = std::shared_ptr (new ParseController (cipherstream, m_type)); m_parser->setSender(GpgME::UserID::addrSpecFromString(get_sender().c_str())); log_mime_parser ("%s:%s: Parser for \"%s\" is %p", SRCNAME, __func__, get_subject ().c_str(), m_parser.get()); gpgol_release (cipherstream); HANDLE parser_thread = CreateThread (NULL, 0, do_parsing, (LPVOID) this, 0, NULL); if (!parser_thread) { log_error ("%s:%s: Failed to create decrypt / verify thread.", SRCNAME, __func__); } CloseHandle (parser_thread); return 0; } void find_and_replace(std::string& source, const std::string &find, const std::string &replace) { for(std::string::size_type i = 0; (i = source.find(find, i)) != std::string::npos;) { source.replace(i, find.length(), replace); i += replace.length(); } } void Mail::update_body() { if (!m_parser) { TRACEPOINT; return; } const auto error = m_parser->get_formatted_error (); if (!error.empty()) { if (opt.prefer_html) { if (put_oom_string (m_mailitem, "HTMLBody", error.c_str ())) { log_error ("%s:%s: Failed to modify html body of item.", SRCNAME, __func__); } else { log_debug ("%s:%s: Set error html to: '%s'", SRCNAME, __func__, error.c_str ()); } } else { if (put_oom_string (m_mailitem, "Body", error.c_str ())) { log_error ("%s:%s: Failed to modify html body of item.", SRCNAME, __func__); } else { log_debug ("%s:%s: Set error plain to: '%s'", SRCNAME, __func__, error.c_str ()); } } return; } if (m_verify_result.error()) { log_error ("%s:%s: Verification failed. Restoring Body.", SRCNAME, __func__); if (opt.prefer_html) { if (put_oom_string (m_mailitem, "HTMLBody", m_orig_body.c_str ())) { log_error ("%s:%s: Failed to modify html body of item.", SRCNAME, __func__); } } else { if (put_oom_string (m_mailitem, "Body", m_orig_body.c_str ())) { log_error ("%s:%s: Failed to modify html body of item.", SRCNAME, __func__); } } return; } // No need to carry body anymore m_orig_body = std::string(); auto html = m_parser->get_html_body (); /** Outlook does not show newlines if \r\r\n is a newline. We replace these as apparently some other buggy MUA sends this. */ find_and_replace (html, "\r\r\n", "\r\n"); if (opt.prefer_html && !html.empty()) { char *converted = ansi_charset_to_utf8 (m_parser->get_html_charset().c_str(), html.c_str(), html.size()); int ret = put_oom_string (m_mailitem, "HTMLBody", converted ? converted : ""); xfree (converted); if (ret) { log_error ("%s:%s: Failed to modify html body of item.", SRCNAME, __func__); } return; } auto body = m_parser->get_body (); find_and_replace (body, "\r\r\n", "\r\n"); char *converted = ansi_charset_to_utf8 (m_parser->get_body_charset().c_str(), body.c_str(), body.size()); int ret = put_oom_string (m_mailitem, "Body", converted ? converted : ""); xfree (converted); if (ret) { log_error ("%s:%s: Failed to modify body of item.", SRCNAME, __func__); } return; } void Mail::parsing_done() { TRACEPOINT; log_oom_extra ("Mail %p Parsing done for parser: %p", this, m_parser.get()); if (!m_parser) { /* This should not happen but it happens when outlook sends multiple ItemLoad events for the same Mail Object. In that case it could happen that one parser was already done while a second is now returning for the wrong mail (as it's looked up by uuid.) We have a check in get_uuid that the uuid was not in the map before (and the parser is replaced). So this really really should not happen. We handle it anyway as we crash otherwise. It should not happen because the parser is only created in decrypt_verify which is called in the read event. And even in there we check if the parser was set. */ log_error ("%s:%s: No parser obj. For mail: %p", SRCNAME, __func__, this); return; } /* Store the results. */ m_decrypt_result = m_parser->decrypt_result (); m_verify_result = m_parser->verify_result (); m_crypto_flags = 0; if (!m_decrypt_result.isNull()) { m_crypto_flags |= 1; } if (m_verify_result.numSignatures()) { m_crypto_flags |= 2; } update_sigstate (); m_needs_wipe = true; TRACEPOINT; /* Set categories according to the result. */ update_categories(); TRACEPOINT; /* Update the body */ update_body(); TRACEPOINT; /* Check that there are no unsigned / unencrypted messages. */ check_attachments (); /* Update attachments */ if (add_attachments (m_mailitem, m_parser->get_attachments())) { log_error ("%s:%s: Failed to update attachments.", SRCNAME, __func__); } log_debug ("%s:%s: Delayed invalidate to update sigstate.", SRCNAME, __func__); CloseHandle(CreateThread (NULL, 0, delayed_invalidate_ui, (LPVOID) this, 0, NULL)); TRACEPOINT; return; } int Mail::encrypt_sign_start () { if (m_crypt_state != NeedsActualCrypt) { log_debug ("%s:%s: invalid state %i", SRCNAME, __func__, m_crypt_state); return -1; } int flags = 0; if (!needs_crypto()) { return 0; } LPMESSAGE message = get_oom_base_message (m_mailitem); if (!message) { log_error ("%s:%s: Failed to get base message.", SRCNAME, __func__); return -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); return -1; } } m_do_inline = 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. set_window_enabled (false); if (m_crypter->collect_data ()) { log_error ("%s:%s: Crypter for mail %p failed to collect data.", SRCNAME, __func__, this); set_window_enabled (true); return -1; } if (!m_async_crypt_disabled) { CloseHandle(CreateThread (NULL, 0, do_crypt, (LPVOID) this, 0, NULL)); } else { do_crypt (this); } return 0; } int Mail::needs_crypto () { LPMESSAGE message = get_oom_message (m_mailitem); bool ret; if (!message) { log_error ("%s:%s: Failed to get message.", SRCNAME, __func__); return false; } ret = get_gpgol_draft_info_flags (message); gpgol_release(message); return ret; } int Mail::wipe (bool force) { if (!m_needs_wipe && !force) { return 0; } log_debug ("%s:%s: Removing plaintext from mailitem: %p.", SRCNAME, __func__, m_mailitem); if (put_oom_string (m_mailitem, "HTMLBody", "")) { if (put_oom_string (m_mailitem, "Body", "")) { log_debug ("%s:%s: Failed to wipe mailitem: %p.", SRCNAME, __func__, m_mailitem); return -1; } return -1; } else { put_oom_string (m_mailitem, "Body", ""); } m_needs_wipe = false; return 0; } int Mail::update_oom_data () { char *buf = nullptr; log_debug ("%s:%s", SRCNAME, __func__); if (!is_crypto_mail()) { /* 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"); release_cArray (m_cached_recipients); m_cached_recipients = get_recipients (); } /* For some reason outlook may store the recipient address in the send using account field. If we have SMTP we prefer the SenderEmailAddress string. */ if (is_crypto_mail ()) { /* This is the case where we are reading a mail and not composing. When composing we need to use the SendUsingAccount because if you send from the folder of userA but change the from to userB outlook will keep the SenderEmailAddress of UserA. This is all so horrible. */ buf = get_sender_SenderEMailAddress (m_mailitem); if (!buf) { /* Try the sender Object */ buf = get_sender_Sender (m_mailitem); } } if (!buf) { buf = get_sender_SendUsingAccount (m_mailitem, &m_is_gsuite); } if (!buf && !is_crypto_mail ()) { /* Try the sender Object */ buf = get_sender_Sender (m_mailitem); } 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__); return -1; } m_sender = buf; xfree (buf); return 0; } std::string Mail::get_sender () { if (m_sender.empty()) update_oom_data(); return m_sender; } std::string Mail::get_cached_sender () { return m_sender; } int Mail::close_all_mails () { int err = 0; std::map::iterator it; TRACEPOINT; std::map mail_map_copy = s_mail_map; 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 (!is_valid_ptr (it->second)) { log_debug ("%s:%s: Already deleted mail for %p", SRCNAME, __func__, it->first); continue; } if (!it->second->is_crypto_mail()) { continue; } if (close_inspector (it->second) || close (it->second)) { log_error ("Failed to close mail: %p ", it->first); /* Should not happen */ if (is_valid_ptr (it->second) && it->second->revert()) { err++; } } } return err; } int Mail::revert_all_mails () { int err = 0; std::map::iterator it; for (it = s_mail_map.begin(); it != s_mail_map.end(); ++it) { if (it->second->revert ()) { log_error ("Failed to revert mail: %p ", it->first); err++; continue; } it->second->set_needs_save (true); if (!invoke_oom_method (it->first, "Save", NULL)) { log_error ("Failed to save reverted mail: %p ", it->second); err++; continue; } } return err; } int Mail::wipe_all_mails () { int err = 0; std::map::iterator it; for (it = s_mail_map.begin(); it != s_mail_map.end(); ++it) { if (it->second->wipe ()) { log_error ("Failed to wipe mail: %p ", it->first); err++; } } return err; } int Mail::revert () { int err = 0; if (!m_processed) { return 0; } err = gpgol_mailitem_revert (m_mailitem); if (err == -1) { log_error ("%s:%s: Message revert failed falling back to wipe.", SRCNAME, __func__); return wipe (); } /* We need to reprocess the mail next time around. */ m_processed = false; m_needs_wipe = false; return 0; } bool Mail::is_smime () { msgtype_t msgtype; LPMESSAGE message; if (m_is_smime_checked) { return m_is_smime; } message = get_oom_message (m_mailitem); if (!message) { log_error ("%s:%s: No message?", SRCNAME, __func__); return false; } msgtype = mapi_get_message_type (message); m_is_smime = msgtype == MSGTYPE_GPGOL_OPAQUE_ENCRYPTED || msgtype == MSGTYPE_GPGOL_OPAQUE_SIGNED; /* Check if it is an smime mail. Multipart signed can also be true. */ if (!m_is_smime && msgtype == MSGTYPE_GPGOL_MULTIPART_SIGNED) { char *proto; char *ct = mapi_get_message_content_type (message, &proto, NULL); if (ct && proto) { m_is_smime = (!strcmp (proto, "application/pkcs7-signature") || !strcmp (proto, "application/x-pkcs7-signature")); } else { log_error ("%s:%s: No protocol in multipart / signed mail.", SRCNAME, __func__); } xfree (proto); xfree (ct); } gpgol_release (message); m_is_smime_checked = true; return m_is_smime; } static std::string get_string (LPDISPATCH item, const char *str) { char *buf = get_oom_string (item, str); if (!buf) return std::string(); std::string ret = buf; xfree (buf); return ret; } std::string Mail::get_subject() const { return get_string (m_mailitem, "Subject"); } std::string Mail::get_body() const { return get_string (m_mailitem, "Body"); } std::string Mail::get_html_body() const { return get_string (m_mailitem, "HTMLBody"); } char ** Mail::get_recipients() const { LPDISPATCH recipients = get_oom_object (m_mailitem, "Recipients"); if (!recipients) { TRACEPOINT; return nullptr; } auto ret = get_oom_recipients (recipients); gpgol_release (recipients); return ret; } int Mail::close_inspector (Mail *mail) { LPDISPATCH inspector = get_oom_object (mail->item(), "GetInspector"); HRESULT hr; DISPID dispid; if (!inspector) { log_debug ("%s:%s: No inspector.", SRCNAME, __func__); return -1; } dispid = lookup_oom_dispid (inspector, "Close"); if (dispid != DISPID_UNKNOWN) { VARIANT aVariant[1]; DISPPARAMS dispparams; dispparams.rgvarg = aVariant; dispparams.rgvarg[0].vt = VT_INT; dispparams.rgvarg[0].intVal = 1; dispparams.cArgs = 1; dispparams.cNamedArgs = 0; hr = inspector->Invoke (dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, &dispparams, NULL, NULL, NULL); if (hr != S_OK) { log_debug ("%s:%s: Failed to close inspector: %#lx", SRCNAME, __func__, hr); gpgol_release (inspector); return -1; } } gpgol_release (inspector); return 0; } /* static */ int Mail::close (Mail *mail) { VARIANT aVariant[1]; DISPPARAMS dispparams; dispparams.rgvarg = aVariant; dispparams.rgvarg[0].vt = VT_INT; dispparams.rgvarg[0].intVal = 1; dispparams.cArgs = 1; dispparams.cNamedArgs = 0; log_oom_extra ("%s:%s: Invoking close for: %p", SRCNAME, __func__, mail->item()); mail->set_close_triggered (true); int rc = invoke_oom_method_with_parms (mail->item(), "Close", NULL, &dispparams); log_oom_extra ("%s:%s: Returned from close", SRCNAME, __func__); return rc; } void Mail::set_close_triggered (bool value) { m_close_triggered = value; } bool Mail::get_close_triggered () const { return m_close_triggered; } static const UserID get_uid_for_sender (const Key &k, const char *sender) { UserID ret; if (!sender) { return ret; } if (!k.numUserIDs()) { log_debug ("%s:%s: Key without uids", SRCNAME, __func__); return ret; } for (const auto uid: k.userIDs()) { if (!uid.email()) { log_error ("%s:%s: skipping uid without email.", SRCNAME, __func__); continue; } auto normalized_uid = uid.addrSpec(); auto normalized_sender = UserID::addrSpecFromString(sender); if (normalized_sender.empty() || normalized_uid.empty()) { log_error ("%s:%s: normalizing '%s' or '%s' failed.", SRCNAME, __func__, uid.email(), sender); continue; } if (normalized_sender == normalized_uid) { ret = uid; } } return ret; } void Mail::update_sigstate () { std::string sender = get_sender(); if (sender.empty()) { log_error ("%s:%s:%i", SRCNAME, __func__, __LINE__); return; } if (m_verify_result.isNull()) { log_debug ("%s:%s: No verify result.", SRCNAME, __func__); return; } if (m_verify_result.error()) { log_debug ("%s:%s: verify error.", SRCNAME, __func__); return; } for (const auto sig: m_verify_result.signatures()) { m_is_signed = true; m_uid = get_uid_for_sender (sig.key(), sender.c_str()); if (m_uid.isNull() || (sig.validity() != Signature::Validity::Marginal && sig.validity() != Signature::Validity::Full && sig.validity() != Signature::Validity::Ultimate)) { /* For our category we only care about trusted sigs. And the UID needs to match.*/ continue; } if (sig.validity() == Signature::Validity::Marginal) { const auto tofu = m_uid.tofuInfo(); if (!tofu.isNull() && (tofu.validity() != TofuInfo::Validity::BasicHistory && tofu.validity() != TofuInfo::Validity::LargeHistory)) { /* Marginal is only good enough without tofu. Otherwise we wait for basic trust. */ log_debug ("%s:%s: Discarding marginal signature." "With too little history.", SRCNAME, __func__); continue; } } log_debug ("%s:%s: Classified sender as verified uid validity: %i", SRCNAME, __func__, m_uid.validity()); m_sig = sig; m_is_valid = true; return; } log_debug ("%s:%s: No signature with enough trust. Using first", SRCNAME, __func__); m_sig = m_verify_result.signature(0); return; } bool Mail::is_valid_sig () { return m_is_valid; } void Mail::remove_categories () { const char *decCategory = _("GpgOL: Encrypted Message"); const char *verifyCategory = _("GpgOL: Trusted Sender Address"); remove_category (m_mailitem, decCategory); remove_category (m_mailitem, verifyCategory); } /* Now for some tasty hack: Outlook sometimes does not show the new categories properly but instead does some weird scrollbar thing. This can be avoided by resizing the message a bit. But somehow this only needs to be done once. Weird isn't it? But as this workaround worked let's do it programatically. Fun. Wan't some tomato sauce with this hack? */ static void resize_active_window () { HWND wnd = get_active_hwnd (); static std::vector resized_windows; if(std::find(resized_windows.begin(), resized_windows.end(), wnd) != resized_windows.end()) { /* We only need to do this once per window. XXX But sometimes we also need to do this once per view of the explorer. So for now this might break but we reduce the flicker. A better solution would be to find the current view and track that. */ return; } if (!wnd) { TRACEPOINT; return; } RECT oldpos; if (!GetWindowRect (wnd, &oldpos)) { TRACEPOINT; return; } if (!SetWindowPos (wnd, nullptr, (int)oldpos.left, (int)oldpos.top, /* Anything smaller then 19 was ignored when the window was * maximized on Windows 10 at least with a 1980*1024 * resolution. So I assume it's at least 1 percent. * This is all hackish and ugly but should work for 90%... * hopefully. */ (int)oldpos.right - oldpos.left - 20, (int)oldpos.bottom - oldpos.top, 0)) { TRACEPOINT; return; } if (!SetWindowPos (wnd, nullptr, (int)oldpos.left, (int)oldpos.top, (int)oldpos.right - oldpos.left, (int)oldpos.bottom - oldpos.top, 0)) { TRACEPOINT; return; } resized_windows.push_back(wnd); } void Mail::update_categories () { const char *decCategory = _("GpgOL: Encrypted Message"); const char *verifyCategory = _("GpgOL: Trusted Sender Address"); if (is_valid_sig()) { add_category (m_mailitem, verifyCategory); } else { remove_category (m_mailitem, verifyCategory); } if (!m_decrypt_result.isNull()) { add_category (m_mailitem, decCategory); } else { /* As a small safeguard against fakes we remove our categories */ remove_category (m_mailitem, decCategory); } resize_active_window(); return; } bool Mail::is_signed() const { return m_verify_result.numSignatures() > 0; } bool Mail::is_encrypted() const { return !m_decrypt_result.isNull(); } int Mail::set_uuid() { char *uuid; if (!m_uuid.empty()) { /* This codepath is reached by decrypt again after a close with discard changes. The close discarded the uuid on the OOM object so we have to set it again. */ log_debug ("%s:%s: Resetting uuid for %p to %s", SRCNAME, __func__, this, m_uuid.c_str()); uuid = get_unique_id (m_mailitem, 1, m_uuid.c_str()); } else { uuid = get_unique_id (m_mailitem, 1, nullptr); log_debug ("%s:%s: uuid for %p set to %s", SRCNAME, __func__, this, uuid); } if (!uuid) { log_debug ("%s:%s: Failed to get/set uuid for %p", SRCNAME, __func__, m_mailitem); return -1; } if (m_uuid.empty()) { m_uuid = uuid; Mail *other = get_mail_for_uuid (uuid); if (other) { /* According to documentation this should not happen as this means that multiple ItemLoad events occured for the same mailobject without unload / destruction of the mail. But it happens. If you invalidate the UI in the selection change event Outlook loads a new mailobject for the mail. Might happen in other surprising cases. We replace in that case as experiments have shown that the last mailobject is the one that is visible. Still troubling state so we log this as an error. */ log_error ("%s:%s: There is another mail for %p " "with uuid: %s replacing it.", SRCNAME, __func__, m_mailitem, uuid); delete other; } s_uid_map.insert (std::pair (m_uuid, this)); log_debug ("%s:%s: uuid for %p is now %s", SRCNAME, __func__, this, m_uuid.c_str()); } xfree (uuid); return 0; } /* Returns 2 if the userid is ultimately trusted. Returns 1 if the userid is fully trusted but has a signature by a key for which we have a secret and which is ultimately trusted. (Direct trust) 0 otherwise */ static int level_4_check (const UserID &uid) { if (uid.isNull()) { return 0; } if (uid.validity () == UserID::Validity::Ultimate) { return 2; } if (uid.validity () == UserID::Validity::Full) { for (const auto sig: uid.signatures ()) { const char *sigID = sig.signerKeyID (); if (sig.isNull() || !sigID) { /* should not happen */ TRACEPOINT; continue; } /* Direct trust information is not available through gnupg so we cached the keys with ultimate trust during parsing and now see if we find a direct trust path.*/ for (const auto secKey: ParseController::get_ultimate_keys ()) { /* Check that the Key id of the key matches */ const char *secKeyID = secKey.keyID (); if (!secKeyID || strcmp (secKeyID, sigID)) { continue; } /* Check that the userID of the signature is the ultimately trusted one. */ const char *sig_uid_str = sig.signerUserID(); if (!sig_uid_str) { /* should not happen */ TRACEPOINT; continue; } for (const auto signer_uid: secKey.userIDs ()) { if (signer_uid.validity() != UserID::Validity::Ultimate) { TRACEPOINT; continue; } const char *signer_uid_str = signer_uid.id (); if (!sig_uid_str) { /* should not happen */ TRACEPOINT; continue; } if (!strcmp(sig_uid_str, signer_uid_str)) { /* We have a match */ log_debug ("%s:%s: classified %s as ultimate because " "it was signed by uid %s of key %s", SRCNAME, __func__, signer_uid_str, sig_uid_str, secKeyID); return 1; } } } } } return 0; } std::string Mail::get_crypto_summary () { const int level = get_signature_level (); bool enc = is_encrypted (); if (level == 4 && enc) { return _("Security Level 4"); } if (level == 4) { return _("Trust Level 4"); } if (level == 3 && enc) { return _("Security Level 3"); } if (level == 3) { return _("Trust Level 3"); } if (level == 2 && enc) { return _("Security Level 2"); } if (level == 2) { return _("Trust Level 2"); } if (enc) { return _("Encrypted"); } if (is_signed ()) { /* Even if it is signed, if it is not validly signed it's still completly insecure as anyone could have signed this. So we avoid the label "signed" here as this word already implies some security. */ return _("Insecure"); } return _("Insecure"); } std::string Mail::get_crypto_one_line() { bool sig = is_signed (); bool enc = is_encrypted (); if (sig || enc) { if (sig && enc) { return _("Signed and encrypted message"); } else if (sig) { return _("Signed message"); } else if (enc) { return _("Encrypted message"); } } return _("Insecure message"); } std::string Mail::get_crypto_details() { std::string message; /* No signature with keys but error */ if (!is_encrypted() && !is_signed () && m_verify_result.error()) { message = _("You cannot be sure who sent, " "modified and read the message in transit."); message += "\n\n"; message += _("The message was signed but the verification failed with:"); message += "\n"; message += m_verify_result.error().asString(); return message; } /* No crypo, what are we doing here? */ if (!is_encrypted () && !is_signed ()) { return _("You cannot be sure who sent, " "modified and read the message in transit."); } /* Handle encrypt only */ if (is_encrypted() && !is_signed ()) { if (in_de_vs_mode ()) { if (m_sig.isDeVs()) { message += _("The encryption was VS-NfD-compliant."); } else { message += _("The encryption was not VS-NfD-compliant."); } } message += "\n\n"; message += _("You cannot be sure who sent the message because " "it is not signed."); return message; } bool keyFound = true; bool isOpenPGP = m_sig.key().isNull() ? !is_smime() : m_sig.key().protocol() == Protocol::OpenPGP; char *buf; bool hasConflict = false; int level = get_signature_level (); log_debug ("%s:%s: Formatting sig. Validity: %x Summary: %x Level: %i", SRCNAME, __func__, m_sig.validity(), m_sig.summary(), level); if (level == 4) { /* level 4 check for direct trust */ int four_check = level_4_check (m_uid); if (four_check == 2 && m_sig.key().hasSecret ()) { message = _("You signed this message."); } else if (four_check == 1) { message = _("The senders identity was certified by yourself."); } else if (four_check == 2) { message = _("The sender is allowed to certify identities for you."); } else { log_error ("%s:%s:%i BUG: Invalid sigstate.", SRCNAME, __func__, __LINE__); return message; } } else if (level == 3 && isOpenPGP) { /* Level three is only reachable through web of trust and no direct signature. */ message = _("The senders identity was certified by several trusted people."); } else if (level == 3 && !isOpenPGP) { /* Level three is the only level for trusted S/MIME keys. */ gpgrt_asprintf (&buf, _("The senders identity is certified by the trusted issuer:\n'%s'\n"), m_sig.key().issuerName()); message = buf; xfree (buf); } else if (level == 2 && m_uid.tofuInfo ().isNull ()) { /* Marginal trust through pgp only */ message = _("Some trusted people " "have certified the senders identity."); } else if (level == 2) { unsigned long first_contact = std::max (m_uid.tofuInfo().signFirst(), m_uid.tofuInfo().encrFirst()); char *time = format_date_from_gpgme (first_contact); /* i18n note signcount is always pulral because with signcount 1 we * would not be in this branch. */ gpgrt_asprintf (&buf, _("The senders address is trusted, because " "you have established a communication history " "with this address starting on %s.\n" "You encrypted %i and verified %i messages since."), time, m_uid.tofuInfo().encrCount(), m_uid.tofuInfo().signCount ()); xfree (time); message = buf; xfree (buf); } else if (level == 1) { /* This could be marginal trust through pgp, or tofu with little history. */ if (m_uid.tofuInfo ().signCount() == 1) { message += _("The senders signature was verified for the first time."); } else if (m_uid.tofuInfo ().validity() == TofuInfo::Validity::LittleHistory) { unsigned long first_contact = std::max (m_uid.tofuInfo().signFirst(), m_uid.tofuInfo().encrFirst()); char *time = format_date_from_gpgme (first_contact); gpgrt_asprintf (&buf, _("The senders address is not trustworthy yet because " "you only verified %i messages and encrypted %i messages to " "it since %s."), m_uid.tofuInfo().signCount (), m_uid.tofuInfo().encrCount (), time); xfree (time); message = buf; xfree (buf); } } else { /* Now we are in level 0, this could be a technical problem, no key or just unkown. */ message = is_encrypted () ? _("But the sender address is not trustworthy because:") : _("The sender address is not trustworthy because:"); message += "\n"; keyFound = !(m_sig.summary() & Signature::Summary::KeyMissing); bool general_problem = true; /* First the general stuff. */ if (m_sig.summary() & Signature::Summary::Red) { message += _("The signature is invalid: \n"); } else if (m_sig.summary() & Signature::Summary::SysError || m_verify_result.numSignatures() < 1) { message += _("There was an error verifying the signature.\n"); } else if (m_sig.summary() & Signature::Summary::SigExpired) { message += _("The signature is expired.\n"); } else { message += isOpenPGP ? _("The used key") : _("The used certificate"); message += " "; general_problem = false; } /* Now the key problems */ if ((m_sig.summary() & Signature::Summary::KeyMissing)) { message += _("is not available."); } else if ((m_sig.summary() & Signature::Summary::KeyRevoked)) { message += _("is revoked."); } else if ((m_sig.summary() & Signature::Summary::KeyExpired)) { message += _("is expired."); } else if ((m_sig.summary() & Signature::Summary::BadPolicy)) { message += _("is not meant for signing."); } else if ((m_sig.summary() & Signature::Summary::CrlMissing)) { message += _("could not be checked for revocation."); } else if ((m_sig.summary() & Signature::Summary::CrlTooOld)) { message += _("could not be checked for revocation."); } else if ((m_sig.summary() & Signature::Summary::TofuConflict) || m_uid.tofuInfo().validity() == TofuInfo::Conflict) { message += _("is not the same as the key that was used " "for this address in the past."); hasConflict = true; } else if (m_uid.isNull()) { gpgrt_asprintf (&buf, _("does not claim the address: \"%s\"."), get_sender().c_str()); message += buf; xfree (buf); } else if (((m_sig.validity() & Signature::Validity::Undefined) || (m_sig.validity() & Signature::Validity::Unknown) || (m_sig.summary() == Signature::Summary::None) || (m_sig.validity() == 0))&& !general_problem) { /* Bit of a catch all for weird results. */ if (isOpenPGP) { message += _("is not certified by any trustworthy key."); } else { message += _("is not certified by a trustworthy Certificate Authority or the Certificate Authority is unknown."); } } else if (m_uid.isRevoked()) { message += _("The sender marked this address as revoked."); } else if ((m_sig.validity() & Signature::Validity::Never)) { message += _("is marked as not trustworthy."); } } message += "\n\n"; if (in_de_vs_mode ()) { if (is_signed ()) { if (m_sig.isDeVs ()) { message += _("The signature is VS-NfD-compliant."); } else { message += _("The signature is not VS-NfD-compliant."); } message += "\n"; } if (is_encrypted ()) { if (m_decrypt_result.isDeVs ()) { message += _("The encryption is VS-NfD-compliant."); } else { message += _("The encryption is not VS-NfD-compliant."); } message += "\n\n"; } else { message += "\n"; } } if (hasConflict) { message += _("Click here to change the key used for this address."); } else if (keyFound) { message += isOpenPGP ? _("Click here for details about the key.") : _("Click here for details about the certificate."); } else { message += isOpenPGP ? _("Click here to search the key on the configured keyserver.") : _("Click here to search the certificate on the configured X509 keyserver."); } return message; } int Mail::get_signature_level () const { if (!m_is_signed) { return 0; } if (m_uid.isNull ()) { /* No m_uid matches our sender. */ return 0; } if (m_is_valid && (m_uid.validity () == UserID::Validity::Ultimate || (m_uid.validity () == UserID::Validity::Full && level_4_check (m_uid))) && (!in_de_vs_mode () || m_sig.isDeVs())) { return 4; } if (m_is_valid && m_uid.validity () == UserID::Validity::Full && (!in_de_vs_mode () || m_sig.isDeVs())) { return 3; } if (m_is_valid) { return 2; } if (m_sig.validity() == Signature::Validity::Marginal) { return 1; } if (m_sig.summary() & Signature::Summary::TofuConflict || m_uid.tofuInfo().validity() == TofuInfo::Conflict) { return 0; } return 0; } int Mail::get_crypto_icon_id () const { int level = get_signature_level (); int offset = is_encrypted () ? ENCRYPT_ICON_OFFSET : 0; return IDI_LEVEL_0 + level + offset; } const char* Mail::get_sig_fpr() const { if (!m_is_signed || m_sig.isNull()) { return nullptr; } return m_sig.fingerprint(); } /** Try to locate the keys for all recipients */ void Mail::locate_keys() { char ** recipients = get_recipients (); KeyCache::instance()->startLocate (recipients); KeyCache::instance()->startLocate (get_sender ().c_str ()); KeyCache::instance()->startLocateSecret (get_sender ().c_str ()); release_cArray (recipients); } bool Mail::is_html_alternative () const { return m_is_html_alternative; } char * Mail::take_cached_html_body () { char *ret = m_cached_html_body; m_cached_html_body = nullptr; return ret; } char * Mail::take_cached_plain_body () { char *ret = m_cached_plain_body; m_cached_plain_body = nullptr; return ret; } int Mail::get_crypto_flags () const { return m_crypto_flags; } void Mail::set_needs_encrypt (bool value) { m_needs_encrypt = value; } bool Mail::needs_encrypt() const { return m_needs_encrypt; } char ** Mail::take_cached_recipients() { char **ret = m_cached_recipients; m_cached_recipients = nullptr; return ret; } void Mail::append_to_inline_body (const std::string &data) { m_inline_body += data; } int Mail::inline_body_to_body() { if (!m_crypter) { log_error ("%s:%s: No crypter.", SRCNAME, __func__); return -1; } const auto body = m_crypter->get_inline_data (); if (body.empty()) { return 0; } int ret = put_oom_string (m_mailitem, "Body", body.c_str ()); return ret; } void Mail::update_crypt_mapi() { log_debug ("%s:%s: Update crypt mapi", SRCNAME, __func__); if (m_crypt_state != NeedsUpdateInMAPI) { log_debug ("%s:%s: invalid state %i", SRCNAME, __func__, m_crypt_state); return; } 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 = NoCryptMail; return; } } if (m_crypter->update_mail_mapi ()) { log_error ("%s:%s: Failed to update MAPI after crypt", SRCNAME, __func__); m_crypt_state = NoCryptMail; } else { m_crypt_state = WantsSendMIME; } /** If sync we need the crypter in update_crypt_oom */ if (!async_crypt_disabled ()) { // We don't need the crypter anymore. reset_crypter (); } } /** 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) { auto body = mail->get_body(); std::pair ret; ret.first = false; ret.second = false; ltrim (body); if (body.size() > 10 && !strncmp (body.c_str(), "-----BEGIN", 10)) { ret.first = true; return ret; } if (!body.size()) { ret.second = true; } else { log_mime_parser ("%s:%s: Body found in %p : \"%s\"", SRCNAME, __func__, mail, body.c_str ()); } return ret; } void Mail::update_crypt_oom() { log_debug ("%s:%s: Update crypt oom for %p", SRCNAME, __func__, this); if (m_crypt_state != NeedsUpdateInOOM) { log_debug ("%s:%s: invalid state %i", SRCNAME, __func__, m_crypt_state); reset_crypter (); return; } if (do_pgp_inline ()) { if (inline_body_to_body ()) { 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 = NoCryptMail; return; } } + + 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); + } + } + /** When doing async update_crypt_mapi follows and needs the crypter. */ if (async_crypt_disabled ()) { reset_crypter (); } 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 = WantsSendInline; return; } // We are in MIME land. Wipe the body. if (wipe (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 = NoCryptMail; return; } m_crypt_state = NeedsSecondAfterWrite; return; } void Mail::set_window_enabled (bool value) { if (!value) { m_window = get_active_hwnd (); } log_debug ("%s:%s: enable window %p %i", SRCNAME, __func__, m_window, value); EnableWindow (m_window, value ? TRUE : FALSE); } bool Mail::check_inline_response () { /* Async sending might lead to crashes when the send invocation is done. * For now we treat every mail as an inline response to disable async * encryption. :-( For more details see: T3838 */ #ifdef DO_ASYNC_CRYPTO m_async_crypt_disabled = false; LPDISPATCH app = GpgolAddin::get_instance ()->get_application (); if (!app) { TRACEPOINT; return false; } LPDISPATCH explorer = get_oom_object (app, "ActiveExplorer"); if (!explorer) { TRACEPOINT; return false; } LPDISPATCH inlineResponse = get_oom_object (explorer, "ActiveInlineResponse"); gpgol_release (explorer); if (!inlineResponse) { return 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); const auto subject = get_subject (); if (inlineResponse && !subject.empty() && !strcmp (subject.c_str (), inlineSubject)) { log_debug ("%s:%s: Detected inline response for '%p'", SRCNAME, __func__, this); m_async_crypt_disabled = true; } xfree (inlineSubject); #else m_async_crypt_disabled = true; #endif return m_async_crypt_disabled; } // static Mail * Mail::get_last_mail () { if (!s_last_mail || !is_valid_ptr (s_last_mail)) { s_last_mail = nullptr; } return s_last_mail; } // static void Mail::invalidate_last_mail () { s_last_mail = nullptr; } // static void Mail::locate_all_crypto_recipients() { if (!opt.autoresolve) { return; } std::map::iterator it; for (it = s_mail_map.begin(); it != s_mail_map.end(); ++it) { if (it->second->needs_crypto ()) { it->second->locate_keys (); } } } int Mail::remove_our_attachments () { LPDISPATCH attachments = get_oom_object (m_mailitem, "Attachments"); if (!attachments) { TRACEPOINT; return 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); } return ret; } /* We are very verbose because if we fail it might mean that we have leaked plaintext -> critical. */ bool Mail::has_crypted_or_empty_body () { 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); return true; } if (!pair.second) { log_debug ("%s:%s: Unexpected content detected. Return false %p.", SRCNAME, __func__, this); return 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); } return 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); return 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); return true; } xfree (mapi_body); log_debug ("%s:%s: Found mapi body. Return false. %p.", SRCNAME, __func__, this); return false; } diff --git a/src/mimemaker.cpp b/src/mimemaker.cpp index a7e9959..ed961f7 100644 --- a/src/mimemaker.cpp +++ b/src/mimemaker.cpp @@ -1,2434 +1,2445 @@ /* mimemaker.c - Construct MIME message out of a MAPI * Copyright (C) 2007, 2008 g10 Code GmbH * 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 . */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #define COBJMACROS #include #include #include "mymapi.h" #include "mymapitags.h" #include "common.h" #include "engine.h" #include "mapihelp.h" #include "mimemaker.h" #include "oomhelp.h" #include "gpgolstr.h" #include "mail.h" static const unsigned char oid_mimetag[] = {0x2A, 0x86, 0x48, 0x86, 0xf7, 0x14, 0x03, 0x0a, 0x04}; /* The base-64 list used for base64 encoding. */ static unsigned char bintoasc[64+1] = ("ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" "0123456789+/"); /* Object used to collect data in a memory buffer. */ struct databuf_s { size_t len; /* Used length. */ size_t size; /* Allocated length of BUF. */ char *buf; /* Malloced buffer. */ }; /*** local prototypes ***/ static int write_multistring (sink_t sink, const char *text1, ...) GPGOL_GCC_A_SENTINEL(0); /* Standard write method used with a sink_t object. */ int sink_std_write (sink_t sink, const void *data, size_t datalen) { HRESULT hr; LPSTREAM stream = static_cast(sink->cb_data); if (!stream) { log_error ("%s:%s: sink not setup for writing", SRCNAME, __func__); return -1; } if (!data) return 0; /* Flush - nothing to do here. */ hr = stream->Write(data, datalen, NULL); if (hr) { log_error ("%s:%s: Write failed: hr=%#lx", SRCNAME, __func__, hr); return -1; } return 0; } int sink_string_write (sink_t sink, const void *data, size_t datalen) { Mail *mail = static_cast(sink->cb_data); mail->append_to_inline_body (std::string((char*)data, datalen)); return 0; } /* Write method used with a sink_t that contains a file object. */ int sink_file_write (sink_t sink, const void *data, size_t datalen) { HANDLE hFile = sink->cb_data; DWORD written = 0; if (!hFile || hFile == INVALID_HANDLE_VALUE) { log_error ("%s:%s: sink not setup for writing", SRCNAME, __func__); return -1; } if (!data) return 0; /* Flush - nothing to do here. */ if (!WriteFile (hFile, data, datalen, &written, NULL)) { log_error ("%s:%s: Write failed: ", SRCNAME, __func__); return -1; } return 0; } /* Make sure that PROTOCOL is usable or return a suitable protocol. On error PROTOCOL_UNKNOWN is returned. */ static protocol_t check_protocol (protocol_t protocol) { switch (protocol) { case PROTOCOL_UNKNOWN: return PROTOCOL_UNKNOWN; case PROTOCOL_OPENPGP: case PROTOCOL_SMIME: return protocol; } log_error ("%s:%s: BUG", SRCNAME, __func__); return PROTOCOL_UNKNOWN; } /* Create a new MAPI attchment for MESSAGE which will be used to prepare the MIME message. On sucess the stream to write the data to is stored at STREAM and the attachment object itself is returned. The caller needs to call SaveChanges. Returns NULL on failure in which case STREAM will be set to NULL. */ LPATTACH create_mapi_attachment (LPMESSAGE message, sink_t sink, const char *overrideMimeTag) { HRESULT hr; ULONG pos; SPropValue prop; LPATTACH att = NULL; LPUNKNOWN punk; sink->cb_data = NULL; sink->writefnc = NULL; hr = message->CreateAttach(NULL, 0, &pos, &att); if (hr) { log_error ("%s:%s: can't create attachment: hr=%#lx\n", SRCNAME, __func__, hr); return NULL; } prop.ulPropTag = PR_ATTACH_METHOD; prop.Value.ul = ATTACH_BY_VALUE; hr = HrSetOneProp ((LPMAPIPROP)att, &prop); if (hr) { log_error ("%s:%s: can't set attach method: hr=%#lx\n", SRCNAME, __func__, hr); goto failure; } /* Mark that attachment so that we know why it has been created. */ if (get_gpgolattachtype_tag (message, &prop.ulPropTag) ) goto failure; prop.Value.l = ATTACHTYPE_MOSSTEMPL; hr = HrSetOneProp ((LPMAPIPROP)att, &prop); if (hr) { log_error ("%s:%s: can't set %s property: hr=%#lx\n", SRCNAME, __func__, "GpgOL Attach Type", hr); goto failure; } /* We better insert a short filename. */ prop.ulPropTag = PR_ATTACH_FILENAME_A; prop.Value.lpszA = strdup (MIMEATTACHFILENAME); hr = HrSetOneProp ((LPMAPIPROP)att, &prop); xfree (prop.Value.lpszA); if (hr) { log_error ("%s:%s: can't set attach filename: hr=%#lx\n", SRCNAME, __func__, hr); goto failure; } /* Even for encrypted messages we need to set the MAPI property to multipart/signed. This seems to be a part of the trigger which leads OL to process such a message in a special way. */ prop.ulPropTag = PR_ATTACH_TAG; prop.Value.bin.cb = sizeof oid_mimetag; prop.Value.bin.lpb = (LPBYTE)oid_mimetag; hr = HrSetOneProp ((LPMAPIPROP)att, &prop); if (!hr) { prop.ulPropTag = PR_ATTACH_MIME_TAG_A; prop.Value.lpszA = overrideMimeTag ? strdup (overrideMimeTag) : strdup ("multipart/signed"); if (overrideMimeTag) { log_debug ("%s:%s: using override mimetag: %s\n", SRCNAME, __func__, overrideMimeTag); } hr = HrSetOneProp ((LPMAPIPROP)att, &prop); xfree (prop.Value.lpszA); } if (hr) { log_error ("%s:%s: can't set attach mime tag: hr=%#lx\n", SRCNAME, __func__, hr); goto failure; } punk = NULL; hr = att->OpenProperty(PR_ATTACH_DATA_BIN, &IID_IStream, 0, (MAPI_CREATE|MAPI_MODIFY), &punk); if (FAILED (hr)) { log_error ("%s:%s: can't create output stream: hr=%#lx\n", SRCNAME, __func__, hr); goto failure; } sink->cb_data = (LPSTREAM)punk; sink->writefnc = sink_std_write; return att; failure: gpgol_release (att); return NULL; } /* Write data to a sink_t. */ int write_buffer (sink_t sink, const void *data, size_t datalen) { if (!sink || !sink->writefnc) { log_error ("%s:%s: sink not properly setup", SRCNAME, __func__); return -1; } return sink->writefnc (sink, data, datalen); } /* Same as above but used for passing as callback function. This fucntion does not return an error code but the number of bytes written. */ int write_buffer_for_cb (void *opaque, const void *data, size_t datalen) { sink_t sink = (sink_t) opaque; sink->enc_counter += datalen; return write_buffer (sink, data, datalen) ? -1 : datalen; } /* Write the string TEXT to the IStream STREAM. Returns 0 on sucsess, prints an error message and returns -1 on error. */ int write_string (sink_t sink, const char *text) { return write_buffer (sink, text, strlen (text)); } /* Write the string TEXT1 and all folloing arguments of type (const char*) to the SINK. The list of argumens needs to be terminated with a NULL. Returns 0 on sucsess, prints an error message and returns -1 on error. */ static int write_multistring (sink_t sink, const char *text1, ...) { va_list arg_ptr; int rc; const char *s; va_start (arg_ptr, text1); s = text1; do rc = write_string (sink, s); while (!rc && (s=va_arg (arg_ptr, const char *))); va_end (arg_ptr); return rc; } /* Helper to write a boundary to the output sink. The leading LF will be written as well. */ int write_boundary (sink_t sink, const char *boundary, int lastone) { int rc = write_string (sink, "\r\n--"); if (!rc) rc = write_string (sink, boundary); if (!rc) rc = write_string (sink, lastone? "--\r\n":"\r\n"); return rc; } /* Write DATALEN bytes of DATA to SINK in base64 encoding. This creates a complete Base64 chunk including the trailing fillers. */ int write_b64 (sink_t sink, const void *data, size_t datalen) { int rc; const unsigned char *p; unsigned char inbuf[4]; int idx, quads; char outbuf[2048]; size_t outlen; log_debug (" writing base64 of length %d\n", (int)datalen); idx = quads = 0; outlen = 0; for (p = (const unsigned char*)data; datalen; p++, datalen--) { inbuf[idx++] = *p; if (idx > 2) { /* We need space for a quad and a possible CR,LF. */ if (outlen+4+2 >= sizeof outbuf) { if ((rc = write_buffer (sink, outbuf, outlen))) return rc; outlen = 0; } outbuf[outlen++] = bintoasc[(*inbuf>>2)&077]; outbuf[outlen++] = bintoasc[(((*inbuf<<4)&060) |((inbuf[1] >> 4)&017))&077]; outbuf[outlen++] = bintoasc[(((inbuf[1]<<2)&074) |((inbuf[2]>>6)&03))&077]; outbuf[outlen++] = bintoasc[inbuf[2]&077]; idx = 0; if (++quads >= (64/4)) { quads = 0; outbuf[outlen++] = '\r'; outbuf[outlen++] = '\n'; } } } /* We need space for a quad and a final CR,LF. */ if (outlen+4+2 >= sizeof outbuf) { if ((rc = write_buffer (sink, outbuf, outlen))) return rc; outlen = 0; } if (idx) { outbuf[outlen++] = bintoasc[(*inbuf>>2)&077]; if (idx == 1) { outbuf[outlen++] = bintoasc[((*inbuf<<4)&060)&077]; outbuf[outlen++] = '='; outbuf[outlen++] = '='; } else { outbuf[outlen++] = bintoasc[(((*inbuf<<4)&060) |((inbuf[1]>>4)&017))&077]; outbuf[outlen++] = bintoasc[((inbuf[1]<<2)&074)&077]; outbuf[outlen++] = '='; } ++quads; } if (quads) { outbuf[outlen++] = '\r'; outbuf[outlen++] = '\n'; } if (outlen) { if ((rc = write_buffer (sink, outbuf, outlen))) return rc; } return 0; } /* Write DATALEN bytes of DATA to SINK in quoted-prinable encoding. */ static int write_qp (sink_t sink, const void *data, size_t datalen) { int rc; const unsigned char *p; char outbuf[80]; /* We only need 76 octect + 2 for the lineend. */ int outidx; /* Check whether the current character is followed by a line ending. Note that the end of the etxt also counts as a lineending */ #define nextlf_p() ((datalen > 2 && p[1] == '\r' && p[2] == '\n') \ || (datalen > 1 && p[1] == '\n') \ || datalen == 1 ) /* Macro to insert a soft line break if needed. */ # define do_softlf(n) \ do { \ if (outidx + (n) > 76 \ || (outidx + (n) == 76 && !nextlf_p())) \ { \ outbuf[outidx++] = '='; \ outbuf[outidx++] = '\r'; \ outbuf[outidx++] = '\n'; \ if ((rc = write_buffer (sink, outbuf, outidx))) \ return rc; \ outidx = 0; \ } \ } while (0) log_debug (" writing qp of length %d\n", (int)datalen); outidx = 0; for (p = (const unsigned char*) data; datalen; p++, datalen--) { if ((datalen > 1 && *p == '\r' && p[1] == '\n') || *p == '\n') { /* Line break. */ outbuf[outidx++] = '\r'; outbuf[outidx++] = '\n'; if ((rc = write_buffer (sink, outbuf, outidx))) return rc; outidx = 0; if (*p == '\r') { p++; datalen--; } } else if (*p == '\t' || *p == ' ') { /* Check whether tab or space is followed by a line break which forbids verbatim encoding. If we are already at the end of the buffer we take that as a line end too. */ if (nextlf_p()) { do_softlf (3); outbuf[outidx++] = '='; outbuf[outidx++] = tohex ((*p>>4)&15); outbuf[outidx++] = tohex (*p&15); } else { do_softlf (1); outbuf[outidx++] = *p; } } else if (!outidx && *p == '.' && nextlf_p () ) { /* We better protect a line with just a single dot. */ outbuf[outidx++] = '='; outbuf[outidx++] = tohex ((*p>>4)&15); outbuf[outidx++] = tohex (*p&15); } else if (!outidx && datalen >= 5 && !memcmp (p, "From ", 5)) { /* Protect the 'F' so that MTAs won't prefix the "From " with an '>' */ outbuf[outidx++] = '='; outbuf[outidx++] = tohex ((*p>>4)&15); outbuf[outidx++] = tohex (*p&15); } else if (*p >= '!' && *p <= '~' && *p != '=') { do_softlf (1); outbuf[outidx++] = *p; } else { do_softlf (3); outbuf[outidx++] = '='; outbuf[outidx++] = tohex ((*p>>4)&15); outbuf[outidx++] = tohex (*p&15); } } if (outidx) { outbuf[outidx++] = '\r'; outbuf[outidx++] = '\n'; if ((rc = write_buffer (sink, outbuf, outidx))) return rc; } # undef do_softlf # undef nextlf_p return 0; } /* Write DATALEN bytes of DATA to SINK in plain ascii encoding. */ static int write_plain (sink_t sink, const void *data, size_t datalen) { int rc; const unsigned char *p; char outbuf[100]; int outidx; log_debug (" writing ascii of length %d\n", (int)datalen); outidx = 0; for (p = (const unsigned char*) data; datalen; p++, datalen--) { if ((datalen > 1 && *p == '\r' && p[1] == '\n') || *p == '\n') { outbuf[outidx++] = '\r'; outbuf[outidx++] = '\n'; if ((rc = write_buffer (sink, outbuf, outidx))) return rc; outidx = 0; if (*p == '\r') { p++; datalen--; } } else if (!outidx && *p == '.' && ( (datalen > 2 && p[1] == '\r' && p[2] == '\n') || (datalen > 1 && p[1] == '\n') || datalen == 1)) { /* Better protect a line with just a single dot. We do this by adding a space. */ outbuf[outidx++] = *p; outbuf[outidx++] = ' '; } else if (outidx > 80) { /* We should never be called for too long lines - QP should have been used. */ log_error ("%s:%s: BUG: line longer than exepcted", SRCNAME, __func__); return -1; } else outbuf[outidx++] = *p; } if (outidx) { outbuf[outidx++] = '\r'; outbuf[outidx++] = '\n'; if ((rc = write_buffer (sink, outbuf, outidx))) return rc; } return 0; } /* Infer the conent type from the FILENAME. The return value is a static string there won't be an error return. In case Bae 64 encoding is required for the type true will be stored at FORCE_B64; however, this is only a shortcut and if that is not set, the caller should infer the encoding by other means. */ static const char * infer_content_type (const char * /*data*/, size_t /*datalen*/, const char *filename, int is_mapibody, int *force_b64) { static struct { char b64; const char *suffix; const char *ct; } suffix_table[] = { { 1, "3gp", "video/3gpp" }, { 1, "abw", "application/x-abiword" }, { 1, "ai", "application/postscript" }, { 1, "au", "audio/basic" }, { 1, "bin", "application/octet-stream" }, { 1, "class", "application/java-vm" }, { 1, "cpt", "application/mac-compactpro" }, { 0, "css", "text/css" }, { 0, "csv", "text/comma-separated-values" }, { 1, "deb", "application/x-debian-package" }, { 1, "dl", "video/dl" }, { 1, "doc", "application/msword" }, { 1, "dv", "video/dv" }, { 1, "dvi", "application/x-dvi" }, { 1, "eml", "message/rfc822" }, { 1, "eps", "application/postscript" }, { 1, "fig", "application/x-xfig" }, { 1, "flac", "application/x-flac" }, { 1, "fli", "video/fli" }, { 1, "gif", "image/gif" }, { 1, "gl", "video/gl" }, { 1, "gnumeric", "application/x-gnumeric" }, { 1, "hqx", "application/mac-binhex40" }, { 1, "hta", "application/hta" }, { 0, "htm", "text/html" }, { 0, "html", "text/html" }, { 0, "ics", "text/calendar" }, { 1, "jar", "application/java-archive" }, { 1, "jpeg", "image/jpeg" }, { 1, "jpg", "image/jpeg" }, { 1, "js", "application/x-javascript" }, { 1, "latex", "application/x-latex" }, { 1, "lha", "application/x-lha" }, { 1, "lzh", "application/x-lzh" }, { 1, "lzx", "application/x-lzx" }, { 1, "m3u", "audio/mpegurl" }, { 1, "m4a", "audio/mpeg" }, { 1, "mdb", "application/msaccess" }, { 1, "midi", "audio/midi" }, { 1, "mov", "video/quicktime" }, { 1, "mp2", "audio/mpeg" }, { 1, "mp3", "audio/mpeg" }, { 1, "mp4", "video/mp4" }, { 1, "mpeg", "video/mpeg" }, { 1, "mpega", "audio/mpeg" }, { 1, "mpg", "video/mpeg" }, { 1, "mpga", "audio/mpeg" }, { 1, "msi", "application/x-msi" }, { 1, "mxu", "video/vnd.mpegurl" }, { 1, "nb", "application/mathematica" }, { 1, "oda", "application/oda" }, { 1, "odb", "application/vnd.oasis.opendocument.database" }, { 1, "odc", "application/vnd.oasis.opendocument.chart" }, { 1, "odf", "application/vnd.oasis.opendocument.formula" }, { 1, "odg", "application/vnd.oasis.opendocument.graphics" }, { 1, "odi", "application/vnd.oasis.opendocument.image" }, { 1, "odm", "application/vnd.oasis.opendocument.text-master" }, { 1, "odp", "application/vnd.oasis.opendocument.presentation" }, { 1, "ods", "application/vnd.oasis.opendocument.spreadsheet" }, { 1, "odt", "application/vnd.oasis.opendocument.text" }, { 1, "ogg", "application/ogg" }, { 1, "otg", "application/vnd.oasis.opendocument.graphics-template" }, { 1, "oth", "application/vnd.oasis.opendocument.text-web" }, { 1, "otp", "application/vnd.oasis.opendocument.presentation-template"}, { 1, "ots", "application/vnd.oasis.opendocument.spreadsheet-template"}, { 1, "ott", "application/vnd.oasis.opendocument.text-template" }, { 1, "pdf", "application/pdf" }, { 1, "png", "image/png" }, { 1, "pps", "application/vnd.ms-powerpoint" }, { 1, "ppt", "application/vnd.ms-powerpoint" }, { 1, "prf", "application/pics-rules" }, { 1, "ps", "application/postscript" }, { 1, "qt", "video/quicktime" }, { 1, "rar", "application/rar" }, { 1, "rdf", "application/rdf+xml" }, { 1, "rpm", "application/x-redhat-package-manager" }, { 0, "rss", "application/rss+xml" }, { 1, "ser", "application/java-serialized-object" }, { 0, "sh", "application/x-sh" }, { 0, "shtml", "text/html" }, { 1, "sid", "audio/prs.sid" }, { 0, "smil", "application/smil" }, { 1, "snd", "audio/basic" }, { 0, "svg", "image/svg+xml" }, { 1, "tar", "application/x-tar" }, { 0, "texi", "application/x-texinfo" }, { 0, "texinfo", "application/x-texinfo" }, { 1, "tif", "image/tiff" }, { 1, "tiff", "image/tiff" }, { 1, "torrent", "application/x-bittorrent" }, { 1, "tsp", "application/dsptype" }, { 0, "vrml", "model/vrml" }, { 1, "vsd", "application/vnd.visio" }, { 1, "wp5", "application/wordperfect5.1" }, { 1, "wpd", "application/wordperfect" }, { 0, "xhtml", "application/xhtml+xml" }, { 1, "xlb", "application/vnd.ms-excel" }, { 1, "xls", "application/vnd.ms-excel" }, { 1, "xlt", "application/vnd.ms-excel" }, { 0, "xml", "application/xml" }, { 0, "xsl", "application/xml" }, { 0, "xul", "application/vnd.mozilla.xul+xml" }, { 1, "zip", "application/zip" }, { 0, NULL, NULL } }; int i; std::string suffix; *force_b64 = 0; if (filename) suffix = strrchr (filename, '.'); if (!suffix.empty()) { suffix.erase(0, 1); std::transform(suffix.begin(), suffix.end(), suffix.begin(), ::tolower); for (i=0; suffix_table[i].suffix; i++) { if (!strcmp (suffix_table[i].suffix, suffix.c_str())) { if (suffix_table[i].b64) *force_b64 = 1; return suffix_table[i].ct; } } } /* Not found via filename, look at the content. */ if (is_mapibody == 1) { return "text/plain"; } else if (is_mapibody == 2) { return "text/html"; } return "application/octet-stream"; } /* Figure out the best encoding to be used for the part. Return values are 0: Plain ASCII. 1: Quoted Printable 2: Base64 */ static int infer_content_encoding (const void *data, size_t datalen) { const unsigned char *p; int need_qp; size_t len, maxlen, highbin, lowbin, ntotal; ntotal = datalen; len = maxlen = lowbin = highbin = 0; need_qp = 0; for (p = (const unsigned char*) data; datalen; p++, datalen--) { len++; if ((*p & 0x80)) highbin++; else if ((datalen > 1 && *p == '\r' && p[1] == '\n') || *p == '\n') { len--; if (len > maxlen) maxlen = len; len = 0; } else if (*p == '\r') { /* CR not followed by a linefeed. */ lowbin++; } else if (*p == '\t' || *p == ' ' || *p == '\f') ; else if (*p < ' ' || *p == 127) lowbin++; else if (len == 1 && datalen > 2 && *p == '-' && p[1] == '-' && p[2] == ' ' && ( (datalen > 4 && p[3] == '\r' && p[4] == '\n') || (datalen > 3 && p[3] == '\n') || datalen == 3)) { /* This is a "-- \r\n" line, thus it indicates the usual signature line delimiter. We need to protect the trailing space. */ need_qp = 1; } else if (len == 1 && datalen > 5 && !memcmp (p, "--=-=", 5)) { /* This look pretty much like a our own boundary. We better protect it by forcing QP encoding. */ need_qp = 1; } else if (len == 1 && datalen >= 5 && !memcmp (p, "From ", 5)) { /* The usual From hack is required so that MTAs do not prefix it with an '>'. */ need_qp = 1; } } if (len > maxlen) maxlen = len; if (maxlen <= 76 && !lowbin && !highbin && !need_qp) return 0; /* Plain ASCII is sufficient. */ /* Somewhere in the Outlook documentation 20% is mentioned as discriminating value for Base64. Though our counting won't be identical we use that value to behave closely to it. */ if (ntotal && ((float)(lowbin+highbin))/ntotal < 0.20) return 1; /* Use quoted printable. */ return 2; /* Use base64. */ } /* Convert an utf8 input string to RFC2047 base64 encoding which is the subset of RFC2047 outlook likes. Return value needs to be freed. */ static char * utf8_to_rfc2047b (const char *input) { char *ret, *encoded; int inferred_encoding = 0; if (!input) { return NULL; } inferred_encoding = infer_content_encoding (input, strlen (input)); if (!inferred_encoding) { return xstrdup (input); } log_debug ("%s:%s: Encoding attachment filename. With: %s ", SRCNAME, __func__, inferred_encoding == 2 ? "Base64" : "QP"); if (inferred_encoding == 2) { encoded = b64_encode (input, strlen (input)); if (gpgrt_asprintf (&ret, "=?utf-8?B?%s?=", encoded) == -1) { log_error ("%s:%s: Error: %i", SRCNAME, __func__, __LINE__); xfree (encoded); return NULL; } } else { /* There is a Bug here. If you encode 4 Byte UTF-8 outlook can't handle it itself. And sends out a message with ?? inserted in that place. This triggers an invalid signature. */ encoded = qp_encode (input, strlen (input), NULL); if (gpgrt_asprintf (&ret, "=?utf-8?Q?%s?=", encoded) == -1) { log_error ("%s:%s: Error: %i", SRCNAME, __func__, __LINE__); xfree (encoded); return NULL; } } xfree (encoded); return ret; } /* Write a MIME part to SINK. First the BOUNDARY is written (unless it is NULL) then the DATA is analyzed and appropriate headers are written. If FILENAME is given it will be added to the part's header. IS_MAPIBODY should be passed as true if the data has been retrieved from the body property. */ static int write_part (sink_t sink, const char *data, size_t datalen, const char *boundary, const char *filename, int is_mapibody, const char *content_id = NULL) { int rc; const char *ct; int use_b64, use_qp, is_text; char *encoded_filename; if (filename) { /* If there is a filename strip the directory part. Take care that there might be slashes or backslashes. */ const char *s1 = strrchr (filename, '/'); const char *s2 = strrchr (filename, '\\'); if (!s1) s1 = s2; else if (s1 && s2 && s2 > s1) s1 = s2; if (s1) filename = s1; if (*filename && filename[1] == ':') filename += 2; if (!*filename) filename = NULL; } log_debug ("Writing part of length %d%s filename=`%s'\n", (int)datalen, is_mapibody? " (body)":"", filename?filename:"[none]"); ct = infer_content_type (data, datalen, filename, is_mapibody, &use_b64); use_qp = 0; if (!use_b64) { switch (infer_content_encoding (data, datalen)) { case 0: break; case 1: use_qp = 1; break; default: use_b64 = 1; break; } } is_text = !strncmp (ct, "text/", 5); if (boundary) if ((rc = write_boundary (sink, boundary, 0))) return rc; if ((rc=write_multistring (sink, "Content-Type: ", ct, (is_text || filename? ";\r\n" :"\r\n"), NULL))) return rc; /* OL inserts a charset parameter in many cases, so we do it right away for all text parts. We can assume us-ascii if no special encoding is required. */ if (is_text) if ((rc=write_multistring (sink, "\tcharset=\"", (!use_qp && !use_b64? "us-ascii" : "utf-8"), filename ? "\";\r\n" : "\"\r\n", NULL))) return rc; encoded_filename = utf8_to_rfc2047b (filename); if (encoded_filename) if ((rc=write_multistring (sink, "\tname=\"", encoded_filename, "\"\r\n", NULL))) return rc; /* Note that we need to output even 7bit because OL inserts that anyway. */ if ((rc = write_multistring (sink, "Content-Transfer-Encoding: ", (use_b64? "base64\r\n": use_qp? "quoted-printable\r\n":"7bit\r\n"), NULL))) return rc; if (content_id) { if ((rc=write_multistring (sink, "Content-ID: <", content_id, ">\r\n", NULL))) return rc; } else if (encoded_filename) if ((rc=write_multistring (sink, "Content-Disposition: attachment;\r\n" "\tfilename=\"", encoded_filename, "\"\r\n", NULL))) return rc; xfree(encoded_filename); /* Write delimiter. */ if ((rc = write_string (sink, "\r\n"))) return rc; /* Write the content. */ if (use_b64) rc = write_b64 (sink, data, datalen); else if (use_qp) rc = write_qp (sink, data, datalen); else rc = write_plain (sink, data, datalen); return rc; } /* Return the number of attachments in TABLE to be put into the MIME message. */ int count_usable_attachments (mapi_attach_item_t *table) { int idx, count = 0; if (table) for (idx=0; !table[idx].end_of_table; idx++) if (table[idx].attach_type == ATTACHTYPE_UNKNOWN && table[idx].method == ATTACH_BY_VALUE) count++; return count; } /* Write out all attachments from TABLE separated by BOUNDARY to SINK. This function needs to be syncronized with count_usable_attachments. If only_related is 1 only include attachments for multipart/related they are excluded otherwise. */ static int write_attachments (sink_t sink, LPMESSAGE message, mapi_attach_item_t *table, const char *boundary, int only_related) { int idx, rc; char *buffer; size_t buflen; if (table) for (idx=0; !table[idx].end_of_table; idx++) if (table[idx].attach_type == ATTACHTYPE_UNKNOWN && table[idx].method == ATTACH_BY_VALUE) { if (only_related && !table[idx].content_id) { continue; } else if (!only_related && table[idx].content_id) { continue; } buffer = mapi_get_attach (message, 0, table+idx, &buflen); if (!buffer) log_debug ("Attachment at index %d not found\n", idx); else log_debug ("Attachment at index %d: length=%d\n", idx, (int)buflen); if (!buffer) return -1; rc = write_part (sink, buffer, buflen, boundary, table[idx].filename, 0, table[idx].content_id); if (rc) { log_error ("Write part returned err: %i", rc); } xfree (buffer); } return 0; } /* Returns 1 if all attachments are related. 2 if there is a related and a mixed attachment. 0 if there are no other parts*/ static int is_related (Mail *mail, mapi_attach_item_t *table) { if (!mail || !mail->is_html_alternative () || !table) { return 0; } int related = 0; int mixed = 0; for (int idx = 0; !table[idx].end_of_table; idx++) { if (table[idx].content_id) { related = 1; } else { mixed = 1; } } return mixed + related; } /* Delete all attachments from TABLE except for the one we just created */ static int delete_all_attachments (LPMESSAGE message, mapi_attach_item_t *table) { HRESULT hr; int idx; if (table) for (idx=0; !table[idx].end_of_table; idx++) { if (table[idx].attach_type == ATTACHTYPE_MOSSTEMPL && table[idx].filename && !strcmp (table[idx].filename, MIMEATTACHFILENAME)) continue; hr = message->DeleteAttach (table[idx].mapipos, 0, NULL, 0); if (hr) { log_error ("%s:%s: DeleteAttach failed: hr=%#lx\n", SRCNAME, __func__, hr); return -1; } } return 0; } /* Commit changes to the attachment ATTACH and release the object. SINK needs to be passed as well and will also be closed. Note that the address of ATTACH is expected so that the fucntion can set it to NULL. */ int close_mapi_attachment (LPATTACH *attach, sink_t sink) { HRESULT hr; LPSTREAM stream = sink ? (LPSTREAM) sink->cb_data : NULL; if (!stream) { log_error ("%s:%s: sink not setup", SRCNAME, __func__); return -1; } hr = stream->Commit (0); if (hr) { log_error ("%s:%s: Commiting output stream failed: hr=%#lx", SRCNAME, __func__, hr); return -1; } gpgol_release (stream); sink->cb_data = NULL; hr = (*attach)->SaveChanges (0); if (hr) { log_error ("%s:%s: SaveChanges of the attachment failed: hr=%#lx\n", SRCNAME, __func__, hr); return -1; } gpgol_release ((*attach)); *attach = NULL; return 0; } /* Cancel changes to the attachment ATTACH and release the object. SINK needs to be passed as well and will also be closed. Note that the address of ATTACH is expected so that the fucntion can set it to NULL. */ void cancel_mapi_attachment (LPATTACH *attach, sink_t sink) { LPSTREAM stream = sink ? (LPSTREAM) sink->cb_data : NULL; if (stream) { stream->Revert(); gpgol_release (stream); sink->cb_data = NULL; } if (*attach) { /* Fixme: Should we try to delete it or is there a Revert method? */ gpgol_release ((*attach)); *attach = NULL; } } /* Do the final processing for a message. */ int finalize_message (LPMESSAGE message, mapi_attach_item_t *att_table, protocol_t protocol, int encrypt, bool is_inline) { HRESULT hr = 0; SPropValue prop; SPropTagArray proparray; /* Set the message class. */ prop.ulPropTag = PR_MESSAGE_CLASS_A; if (encrypt) { prop.Value.lpszA = strdup ("IPM.Note.InfoPathForm.GpgOL.SMIME.MultipartSigned"); } else { prop.Value.lpszA = strdup ("IPM.Note.InfoPathForm.GpgOLS.SMIME.MultipartSigned"); } if (!is_inline) { /* For inline we stick with IPM.Note because Exchange Online would error out if we tried our S/MIME conversion trick with a text plain message */ hr = message->SetProps(1, &prop, NULL); } xfree(prop.Value.lpszA); if (hr) { log_error ("%s:%s: error setting the message class: hr=%#lx\n", SRCNAME, __func__, hr); return -1; } /* Set a special property so that we are later able to identify messages signed or encrypted by us. */ if (mapi_set_sig_status (message, "@")) { log_error ("%s:%s: error setting sigstatus", SRCNAME, __func__); return -1; } /* We also need to set the message class into our custom property. This override is at least required for encrypted messages. */ if (is_inline && mapi_set_gpgol_msg_class (message, (encrypt? (protocol == PROTOCOL_SMIME? "IPM.Note.GpgOL.OpaqueEncrypted" : "IPM.Note.GpgOL.PGPMessage") : "IPM.Note.GpgOL.ClearSigned"))) { log_error ("%s:%s: error setting gpgol msgclass", SRCNAME, __func__); return -1; } if (!is_inline && mapi_set_gpgol_msg_class (message, (encrypt? (protocol == PROTOCOL_SMIME? "IPM.Note.GpgOL.OpaqueEncrypted" : "IPM.Note.GpgOL.MultipartEncrypted") : "IPM.Note.GpgOL.MultipartSigned"))) { log_error ("%s:%s: error setting gpgol msgclass", SRCNAME, __func__); return -1; } proparray.cValues = 1; proparray.aulPropTag[0] = PR_BODY; hr = message->DeleteProps (&proparray, NULL); if (hr) { log_debug_w32 (hr, "%s:%s: deleting PR_BODY failed", SRCNAME, __func__); } proparray.cValues = 1; proparray.aulPropTag[0] = PR_BODY_HTML; hr = message->DeleteProps (&proparray, NULL); if (hr) { log_debug_w32 (hr, "%s:%s: deleting PR_BODY_HTML failed", SRCNAME, __func__); } /* Now delete all parts of the MAPI message except for the one attachment we just created. */ if (delete_all_attachments (message, att_table)) { log_error ("%s:%s: error deleting attachments", SRCNAME, __func__); return -1; } /* Remove the draft info so that we don't leak the information on whether the message has been signed etc. */ mapi_set_gpgol_draft_info (message, NULL); if (mapi_save_changes (message, KEEP_OPEN_READWRITE|FORCE_SAVE)) { log_error ("%s:%s: error saving changes.", SRCNAME, __func__); return -1; } return 0; } /* Sink write method used by mime_sign. We write the data to the filter and also to the EXTRASINK but we don't pass a flush request to EXTRASINK. */ static int sink_hashing_write (sink_t hashsink, const void *data, size_t datalen) { int rc; engine_filter_t filter = (engine_filter_t) hashsink->cb_data; if (!filter || !hashsink->extrasink) { log_error ("%s:%s: sink not setup for writing", SRCNAME, __func__); return -1; } rc = engine_filter (filter, data, datalen); if (!rc && data && datalen) write_buffer (hashsink->extrasink, data, datalen); return rc; } /* This function is called by the filter to collect the output which is a detached signature. */ static int collect_signature (void *opaque, const void *data, size_t datalen) { struct databuf_s *db = (databuf_s *)opaque; if (db->len + datalen >= db->size) { db->size += datalen + 1024; db->buf = (char*) xrealloc (db->buf, db->size); } memcpy (db->buf + db->len, data, datalen); db->len += datalen; return datalen; } /* Helper to create the signing header. This includes enough space for later fixup of the micalg parameter. The MIME version is only written if FIRST is set. */ void create_top_signing_header (char *buffer, size_t buflen, protocol_t protocol, int first, const char *boundary, const char *micalg) { snprintf (buffer, buflen, "%s" "Content-Type: multipart/signed;\r\n" "\tprotocol=\"application/%s\";\r\n" "\tmicalg=%-15.15s;\r\n" "\tboundary=\"%s\"\r\n" "\r\n", first? "MIME-Version: 1.0\r\n":"", (protocol==PROTOCOL_OPENPGP? "pgp-signature":"pkcs7-signature"), micalg, boundary); } /* Add the body, either as multipart/alternative or just as the simple body part. Depending on the format set in outlook. To avoid memory duplication it takes the plain body as parameter. Boundary is the potential outer boundary of a multipart/mixed mail. If it is null we assume the multipart/alternative is the only part. return is zero on success. */ static int add_body (Mail *mail, const char *boundary, sink_t sink, const char *plain_body) { if (!plain_body) { return 0; } bool is_alternative = false; if (mail) { is_alternative = mail->is_html_alternative (); } int rc = 0; if (!is_alternative || !plain_body) { if (plain_body) { rc = write_part (sink, plain_body, strlen (plain_body), boundary, NULL, 1); } /* Just the plain body or no body. We are done. */ return rc; } /* Add a new multipart / mixed element. */ if (boundary && write_boundary (sink, boundary, 0)) { TRACEPOINT; return 1; } /* Now for the multipart/alternative part. We never do HTML only. */ char alt_boundary [BOUNDARYSIZE+1]; generate_boundary (alt_boundary); if ((rc=write_multistring (sink, "Content-Type: multipart/alternative;\r\n", "\tboundary=\"", alt_boundary, "\"\r\n", "\r\n", /* <-- extra line */ NULL))) { TRACEPOINT; return rc; } /* Now the plain body part */ if ((rc = write_part (sink, plain_body, strlen (plain_body), alt_boundary, NULL, 1))) { TRACEPOINT; return rc; } /* Now the html body. It is somehow not accessible through PR_HTML, OutlookSpy also shows MAPI Unsupported (but shows the data) strange. We just cache it. Memory is cheap :-) */ char *html_body = mail->take_cached_html_body(); if (!html_body) { log_error ("%s:%s: BUG: Body but no html body in alternative mail?", SRCNAME, __func__); return -1; } rc = write_part (sink, html_body, strlen (html_body), alt_boundary, NULL, 2); xfree (html_body); if (rc) { TRACEPOINT; return rc; } /* Finish our multipart */ return write_boundary (sink, alt_boundary, 1); } /* Add the body and attachments. Does multipart handling. */ int add_body_and_attachments (sink_t sink, LPMESSAGE message, mapi_attach_item_t *att_table, Mail *mail, const char *body, int n_att_usable) { int related = is_related (mail, att_table); int rc = 0; char inner_boundary[BOUNDARYSIZE+1]; char outer_boundary[BOUNDARYSIZE+1]; *outer_boundary = 0; *inner_boundary = 0; if (((body && n_att_usable) || n_att_usable > 1) && related == 1) { /* A body and at least one attachment or more than one attachment */ generate_boundary (outer_boundary); if ((rc=write_multistring (sink, "Content-Type: multipart/related;\r\n", "\tboundary=\"", outer_boundary, "\"\r\n", "\r\n", /* <--- Outlook adds an extra line. */ NULL))) return rc; } else if ((body && n_att_usable) || n_att_usable > 1) { generate_boundary (outer_boundary); if ((rc=write_multistring (sink, "Content-Type: multipart/mixed;\r\n", "\tboundary=\"", outer_boundary, "\"\r\n", "\r\n", /* <--- Outlook adds an extra line. */ NULL))) return rc; } /* Only one part. */ if (*outer_boundary && related == 2) { /* We have attachments that are related to the body and unrelated attachments. So we need another part. */ if ((rc=write_boundary (sink, outer_boundary, 0))) { return rc; } generate_boundary (inner_boundary); if ((rc=write_multistring (sink, "Content-Type: multipart/related;\r\n", "\tboundary=\"", inner_boundary, "\"\r\n", "\r\n", /* <--- Outlook adds an extra line. */ NULL))) { return rc; } } if ((rc=add_body (mail, *inner_boundary ? inner_boundary : *outer_boundary ? outer_boundary : NULL, sink, body))) { log_error ("%s:%s: Adding the body failed.", SRCNAME, __func__); return rc; } if (!rc && n_att_usable && related) { /* Write the related attachments. */ rc = write_attachments (sink, message, att_table, *inner_boundary? inner_boundary : *outer_boundary? outer_boundary : NULL, 1); if (rc) { return rc; } /* Close the related part if neccessary.*/ if (*inner_boundary && (rc=write_boundary (sink, inner_boundary, 1))) { return rc; } } /* Now write the other attachments */ if (!rc && n_att_usable) rc = write_attachments (sink, message, att_table, *outer_boundary? outer_boundary : NULL, 0); /* Finish the possible multipart/mixed. */ if (*outer_boundary && (rc = write_boundary (sink, outer_boundary, 1))) return rc; return rc; } /* Main body of mime_sign without the the code to delete the original attachments. On success the function returns the current attachment table at R_ATT_TABLE or sets this to NULL on error. If TMPSINK is set no attachment will be created but the output written to that sink. */ static int do_mime_sign (LPMESSAGE message, HWND hwnd, protocol_t protocol, mapi_attach_item_t **r_att_table, sink_t tmpsink, unsigned int session_number, const char *sender, Mail *mail) { int result = -1; int rc; LPATTACH attach = NULL; struct sink_s sinkmem; sink_t sink = &sinkmem; struct sink_s hashsinkmem; sink_t hashsink = &hashsinkmem; char boundary[BOUNDARYSIZE+1]; mapi_attach_item_t *att_table = NULL; char *body = NULL; int n_att_usable; char top_header[BOUNDARYSIZE+200]; engine_filter_t filter = NULL; struct databuf_s sigbuffer; char *my_sender = NULL; *r_att_table = NULL; memset (sink, 0, sizeof *sink); memset (hashsink, 0, sizeof *hashsink); memset (&sigbuffer, 0, sizeof sigbuffer); if (tmpsink) { attach = NULL; sink = tmpsink; } else { attach = create_mapi_attachment (message, sink); if (!attach) return -1; } /* 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. */ if (mail) { body = mail->take_cached_plain_body (); } /* Get the attachment info and the body. */ if (!body) { body = mapi_get_body (message, NULL); } if (body && !*body) { xfree (body); body = NULL; } att_table = mapi_create_attach_table (message, 0); n_att_usable = count_usable_attachments (att_table); if (!n_att_usable && !body) { log_debug ("%s:%s: can't sign an empty message\n", SRCNAME, __func__); result = gpg_error (GPG_ERR_NO_DATA); goto failure; } /* Prepare the signing. */ if (engine_create_filter (&filter, collect_signature, &sigbuffer)) goto failure; if (session_number) { engine_set_session_number (filter, session_number); { char *tmp = mapi_get_subject (message); engine_set_session_title (filter, tmp); xfree (tmp); } } if (sender) my_sender = xstrdup (sender); else my_sender = mapi_get_sender (message); if (engine_sign_start (filter, hwnd, protocol, my_sender, &protocol)) goto failure; protocol = check_protocol (protocol); if (protocol == PROTOCOL_UNKNOWN) { log_error ("%s:%s: no protocol selected", SRCNAME, __func__); goto failure; } /* Write the top header. */ generate_boundary (boundary); create_top_signing_header (top_header, sizeof top_header, protocol, 1, boundary, "xxx"); if ((rc = write_string (sink, top_header))) goto failure; /* Write the boundary so that it is not included in the hashing. */ if ((rc = write_boundary (sink, boundary, 0))) goto failure; /* Create a new sink for hashing and write/hash our content. */ hashsink->cb_data = filter; hashsink->extrasink = sink; hashsink->writefnc = sink_hashing_write; /* Add the plaintext */ if (add_body_and_attachments (hashsink, message, att_table, mail, body, n_att_usable)) goto failure; xfree (body); body = NULL; /* Here we are ready with the hashing. Flush the filter and wait for the signing process to finish. */ if ((rc = write_buffer (hashsink, NULL, 0))) goto failure; if ((rc = engine_wait (filter))) goto failure; filter = NULL; /* Not valid anymore. */ hashsink->cb_data = NULL; /* Not needed anymore. */ /* Write signature attachment. */ if ((rc = write_boundary (sink, boundary, 0))) goto failure; if (protocol == PROTOCOL_OPENPGP) { rc = write_string (sink, "Content-Type: application/pgp-signature\r\n"); } else { rc = write_string (sink, "Content-Transfer-Encoding: base64\r\n" "Content-Type: application/pkcs7-signature\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"); */ } /* About the above code: If we would add "Content-Transfer-Encoding: 7bit\r\n" to this attachment, Outlooks does not proceed with sending and even does not return any error. A wild guess is that while OL adds this header itself, it detects that it already exists and somehow gets into a problem. It is not a problem with the other parts, though. Hmmm, triggered by the top levels CT protocol parameter? Anyway, it is not required that we add it as we won't hash it. Note, that this only holds for OpenPGP; for S/MIME we need to set set CTE. We even write it before the CT because that is the same as Outlook would do it for a missing CTE. */ if (rc) goto failure; if ((rc = write_string (sink, "\r\n"))) goto failure; /* Write the signature. We add an extra CR,LF which should not harm and a terminating 0. */ collect_signature (&sigbuffer, "\r\n", 3); if ((rc = write_string (sink, sigbuffer.buf))) goto failure; /* Write the final boundary and finish the attachment. */ if ((rc = write_boundary (sink, boundary, 1))) goto failure; /* Fixup the micalg parameter. */ { HRESULT hr; LARGE_INTEGER off; LPSTREAM stream = (LPSTREAM) sink->cb_data; off.QuadPart = 0; hr = stream->Seek (off, STREAM_SEEK_SET, NULL); if (hr) { log_error ("%s:%s: seeking back to the begin failed: hr=%#lx", SRCNAME, __func__, hr); goto failure; } create_top_signing_header (top_header, sizeof top_header, protocol, 1, boundary, protocol == PROTOCOL_SMIME? "sha1":"pgp-sha1"); hr = stream->Write (top_header, strlen (top_header), NULL); if (hr) { log_error ("%s:%s: writing fixed micalg failed: hr=%#lx", SRCNAME, __func__, hr); goto failure; } /* Better seek again to the end. */ off.QuadPart = 0; hr = stream->Seek (off, STREAM_SEEK_END, NULL); if (hr) { log_error ("%s:%s: seeking back to the end failed: hr=%#lx", SRCNAME, __func__, hr); goto failure; } } if (attach) { if (close_mapi_attachment (&attach, sink)) goto failure; } result = 0; /* Everything is fine, fall through the cleanup now. */ failure: engine_cancel (filter); if (attach) cancel_mapi_attachment (&attach, sink); xfree (body); if (result) mapi_release_attach_table (att_table); else *r_att_table = att_table; xfree (sigbuffer.buf); xfree (my_sender); return result; } /* Sign the MESSAGE using PROTOCOL. If PROTOCOL is PROTOCOL_UNKNOWN the engine decides what protocol to use. On return MESSAGE is modified so that sending it will result in a properly MOSS (that is PGP or S/MIME) signed message. On failure the function tries to keep the original message intact but there is no 100% guarantee for it. */ int mime_sign (LPMESSAGE message, HWND hwnd, protocol_t protocol, const char *sender, Mail *mail) { int result = -1; mapi_attach_item_t *att_table; result = do_mime_sign (message, hwnd, protocol, &att_table, 0, engine_new_session_number (), sender, mail); if (!result) { if (!finalize_message (message, att_table, protocol, 0)) result = 0; } mapi_release_attach_table (att_table); return result; } /* Sink write method used by mime_encrypt. */ int sink_encryption_write (sink_t encsink, const void *data, size_t datalen) { engine_filter_t filter = (engine_filter_t) encsink->cb_data; if (!filter) { log_error ("%s:%s: sink not setup for writing", SRCNAME, __func__); return -1; } return engine_filter (filter, data, datalen); } #if 0 /* Not used. */ /* Sink write method used by mime_encrypt for writing Base64. */ static int sink_encryption_write_b64 (sink_t encsink, const void *data, size_t datalen) { engine_filter_t filter = encsink->cb_data; int rc; const unsigned char *p; unsigned char inbuf[4]; int idx, quads; char outbuf[6]; size_t outbuflen; if (!filter) { log_error ("%s:%s: sink not setup for writing", SRCNAME, __func__); return -1; } idx = encsink->b64.idx; assert (idx < 4); memcpy (inbuf, encsink->b64.inbuf, idx); quads = encsink->b64.quads; if (!data) /* Flush. */ { outbuflen = 0; if (idx) { outbuf[0] = bintoasc[(*inbuf>>2)&077]; if (idx == 1) { outbuf[1] = bintoasc[((*inbuf<<4)&060)&077]; outbuf[2] = '='; outbuf[3] = '='; } else { outbuf[1] = bintoasc[(((*inbuf<<4)&060)| ((inbuf[1]>>4)&017))&077]; outbuf[2] = bintoasc[((inbuf[1]<<2)&074)&077]; outbuf[3] = '='; } outbuflen = 4; quads++; } if (quads) { outbuf[outbuflen++] = '\r'; outbuf[outbuflen++] = '\n'; } if (outbuflen && (rc = engine_filter (filter, outbuf, outbuflen))) return rc; /* Send the flush command to the filter. */ if ((rc = engine_filter (filter, data, datalen))) return rc; } else { for (p = data; datalen; p++, datalen--) { inbuf[idx++] = *p; if (idx > 2) { idx = 0; outbuf[0] = bintoasc[(*inbuf>>2)&077]; outbuf[1] = bintoasc[(((*inbuf<<4)&060) |((inbuf[1] >> 4)&017))&077]; outbuf[2] = bintoasc[(((inbuf[1]<<2)&074) |((inbuf[2]>>6)&03))&077]; outbuf[3] = bintoasc[inbuf[2]&077]; outbuflen = 4; if (++quads >= (64/4)) { quads = 0; outbuf[4] = '\r'; outbuf[5] = '\n'; outbuflen += 2; } if ((rc = engine_filter (filter, outbuf, outbuflen))) return rc; } } } encsink->b64.idx = idx; memcpy (encsink->b64.inbuf, inbuf, idx); encsink->b64.quads = quads; return 0; } #endif /*Not used.*/ /* Helper from mime_encrypt. BOUNDARY is a buffer of at least BOUNDARYSIZE+1 bytes which will be set on return from that function. */ int create_top_encryption_header (sink_t sink, protocol_t protocol, char *boundary, bool is_inline) { int rc; if (is_inline) { *boundary = 0; rc = 0; /* This would be nice and worked for Google Sync but it failed for Microsoft Exchange Online *sigh* so we put the body instead into the oom body property and stick with IPM Note. rc = write_multistring (sink, "MIME-Version: 1.0\r\n" "Content-Type: text/plain;\r\n" "\tcharset=\"iso-8859-1\"\r\n" "Content-Transfer-Encoding: 7BIT\r\n" "\r\n", NULL); */ } else if (protocol == PROTOCOL_SMIME) { *boundary = 0; + rc = 0; + /* + For S/MIME encrypted mails we do not use the S/MIME conversion + code anymore. With Exchange 2016 this no longer works. Instead + we set an override mime tag, the extended headers in OOM in + Mail::update_crypt_oom and let outlook convert the attachment + to base64. + + A bit more details can be found in T3853 / T3884 + rc = write_multistring (sink, "Content-Type: application/pkcs7-mime; " "smime-type=enveloped-data;\r\n" "\tname=\"smime.p7m\"\r\n" "Content-Disposition: attachment; filename=\"smime.p7m\"\r\n" "Content-Transfer-Encoding: base64\r\n" "MIME-Version: 1.0\r\n" "\r\n", NULL); + */ } else { generate_boundary (boundary); rc = write_multistring (sink, "MIME-Version: 1.0\r\n" "Content-Type: multipart/encrypted;\r\n" "\tprotocol=\"application/pgp-encrypted\";\r\n", "\tboundary=\"", boundary, "\"\r\n", NULL); if (rc) return rc; /* Write the PGP/MIME encrypted part. */ rc = write_boundary (sink, boundary, 0); if (rc) return rc; rc = write_multistring (sink, "Content-Type: application/pgp-encrypted\r\n" "\r\n" "Version: 1\r\n", NULL); if (rc) return rc; /* And start the second part. */ rc = write_boundary (sink, boundary, 0); if (rc) return rc; rc = write_multistring (sink, "Content-Type: application/octet-stream\r\n" "\r\n", NULL); } return rc; } /* Encrypt the MESSAGE. */ int mime_encrypt (LPMESSAGE message, HWND hwnd, protocol_t protocol, char **recipients, const char *sender, Mail* mail) { int result = -1; int rc; LPATTACH attach = nullptr; struct sink_s sinkmem; sink_t sink = &sinkmem; struct sink_s encsinkmem; sink_t encsink = &encsinkmem; char boundary[BOUNDARYSIZE+1]; mapi_attach_item_t *att_table = NULL; char *body = NULL; int n_att_usable; engine_filter_t filter = NULL; char *my_sender = NULL; bool is_inline = mail && mail->do_pgp_inline (); memset (sink, 0, sizeof *sink); memset (encsink, 0, sizeof *encsink); /* Get the attachment info and the body. We need to do this before creating the engine's filter because sending the cancel to the engine with nothing for the engine to process. Will result in an error. This is actually a bug in our engine code but we better avoid triggering this bug because the engine sometimes hangs. Fixme: Needs a proper fix. */ /* 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. */ if (mail) { body = mail->take_cached_plain_body (); } if (!body) { body = mapi_get_body (message, NULL); } if (body && !*body) { xfree (body); body = NULL; } att_table = mapi_create_attach_table (message, 0); n_att_usable = count_usable_attachments (att_table); if (!n_att_usable && !body) { log_debug ("%s:%s: can't encrypt an empty message\n", SRCNAME, __func__); result = gpg_error (GPG_ERR_NO_DATA); goto failure; } if (n_att_usable && is_inline) { log_debug ("%s:%s: PGP Inline not supported for attachments. Using PGP MIME", SRCNAME, __func__); is_inline = false; } if (!is_inline || !mail) { attach = create_mapi_attachment (message, sink); if (!attach) return -1; } else { sink->cb_data = mail; sink->writefnc = sink_string_write; } /* Prepare the encryption. We do this early as it is quite common that some recipient keys are not available and thus the encryption will fail early. */ if (engine_create_filter (&filter, write_buffer_for_cb, sink)) goto failure; engine_set_session_number (filter, engine_new_session_number ()); { char *tmp = mapi_get_subject (message); engine_set_session_title (filter, tmp); xfree (tmp); } if (sender) my_sender = xstrdup (sender); else my_sender = mapi_get_sender (message); if (engine_encrypt_prepare (filter, hwnd, protocol, 0, my_sender, recipients, &protocol)) goto failure; if (engine_encrypt_start (filter, 0)) goto failure; protocol = check_protocol (protocol); if (protocol == PROTOCOL_UNKNOWN) goto failure; if (protocol != PROTOCOL_OPENPGP) { log_debug ("%s:%s: Inline not supported for S/MIME. Using MIME", SRCNAME, __func__); is_inline = false; } /* Write the top header. */ rc = create_top_encryption_header (sink, protocol, boundary, is_inline); if (rc) goto failure; /* Create a new sink for encrypting the following stuff. */ encsink->cb_data = filter; encsink->writefnc = sink_encryption_write; /* Add the plaintext */ if (is_inline && body) { if ((rc = write_multistring (encsink, body, NULL))) { log_error ("%s:%s: Adding the body failed.", SRCNAME, __func__); goto failure; } } else if (add_body_and_attachments (encsink, message, att_table, mail, body, n_att_usable)) { goto failure; } xfree (body); body = NULL; /* Flush the encryption sink and wait for the encryption to get ready. */ if ((rc = write_buffer (encsink, NULL, 0))) goto failure; if ((rc = engine_wait (filter))) goto failure; filter = NULL; /* Not valid anymore. */ encsink->cb_data = NULL; /* Not needed anymore. */ if (!sink->enc_counter) { log_debug ("%s:%s: nothing received from engine", SRCNAME, __func__); goto failure; } /* Write the final boundary (for OpenPGP) and finish the attachment. */ if (*boundary && (rc = write_boundary (sink, boundary, 1))) goto failure; if (attach && close_mapi_attachment (&attach, sink)) goto failure; if (finalize_message (message, att_table, protocol, 1, is_inline)) goto failure; result = 0; /* Everything is fine, fall through the cleanup now. */ failure: engine_cancel (filter); if (attach) { cancel_mapi_attachment (&attach, sink); } xfree (body); mapi_release_attach_table (att_table); xfree (my_sender); return result; } /* Sign and Encrypt the MESSAGE. */ int mime_sign_encrypt (LPMESSAGE message, HWND hwnd, protocol_t protocol, char **recipients, const char *sender, Mail *mail) { int result = -1; int rc = 0; HRESULT hr; LPATTACH attach; LPSTREAM tmpstream = NULL; struct sink_s sinkmem; sink_t sink = &sinkmem; struct sink_s encsinkmem; sink_t encsink = &encsinkmem; struct sink_s tmpsinkmem; sink_t tmpsink = &tmpsinkmem; char boundary[BOUNDARYSIZE+1]; mapi_attach_item_t *att_table = NULL; engine_filter_t filter = NULL; unsigned int session_number; char *my_sender = NULL; memset (sink, 0, sizeof *sink); memset (encsink, 0, sizeof *encsink); memset (tmpsink, 0, sizeof *tmpsink); attach = create_mapi_attachment (message, sink); if (!attach) return -1; /* First check that we are not trying to process an empty message which might lock up our engine. Unfortunately we need to duplicate the code we use in do_mime_sign here. FIXME: The engine should be fixed instead of using such a workaround. */ { char *body; body = mapi_get_body (message, NULL); if (body && !*body) { xfree (body); body = NULL; } att_table = mapi_create_attach_table (message, 0); if (!count_usable_attachments (att_table) && !body) result = gpg_error (GPG_ERR_NO_DATA); xfree (body); if (att_table) { mapi_release_attach_table (att_table); att_table = NULL; } if (gpg_err_code (result) == GPG_ERR_NO_DATA) { log_debug ("%s:%s: can't sign+encrypt an empty message\n", SRCNAME, __func__); goto failure; } } /* Create a temporary sink to construct the signed data. */ hr = OpenStreamOnFile (MAPIAllocateBuffer, MAPIFreeBuffer, (SOF_UNIQUEFILENAME | STGM_DELETEONRELEASE | STGM_CREATE | STGM_READWRITE), NULL, GpgOLStr("GPG"), &tmpstream); if (FAILED (hr)) { log_error ("%s:%s: can't create temp file: hr=%#lx\n", SRCNAME, __func__, hr); rc = -1; goto failure; } tmpsink->cb_data = tmpstream; tmpsink->writefnc = sink_std_write; /* Prepare the encryption. We do this early as it is quite common that some recipients are not available and thus the encryption will fail early. This is also required to allow the UIserver to figure out the protocol to use if we have not forced one. */ if (engine_create_filter (&filter, write_buffer_for_cb, sink)) goto failure; session_number = engine_new_session_number (); engine_set_session_number (filter, session_number); { char *tmp = mapi_get_subject (message); engine_set_session_title (filter, tmp); xfree (tmp); } if (sender) my_sender = xstrdup (sender); else my_sender = mapi_get_sender (message); if ((rc=engine_encrypt_prepare (filter, hwnd, protocol, ENGINE_FLAG_SIGN_FOLLOWS, my_sender, recipients, &protocol))) goto failure; protocol = check_protocol (protocol); if (protocol == PROTOCOL_UNKNOWN) goto failure; /* Now sign the message. This creates another attachment with the complete MIME object of the signed message. We can't do the encryption in streaming mode while running the encryption because we need to fix up that ugly micalg parameter after having created the signature. Note that the protocol to use is taken from the encryption operation. */ if (do_mime_sign (message, hwnd, protocol, &att_table, tmpsink, session_number, sender, mail)) goto failure; /* Now send the actual ENCRYPT command. This split up between prepare and start is necessary to help with the implementarion of the UI-server. If we would send the ENCRYPT command immediately the UI-server might block while reading from the input stream because we are first going to do a sign operation which in trun needs the attention of the UI server. A more robust but complicated approach to the UI-server would be to delay the reading (and thus the start of the underlying encrypt operation) until the first byte has been received. */ if ((rc=engine_encrypt_start (filter, 0))) goto failure; /* Write the top header. */ rc = create_top_encryption_header (sink, protocol, boundary); if (rc) goto failure; /* Create a new sink for encrypting the temporary attachment with the signed message. */ encsink->cb_data = filter; encsink->writefnc = sink_encryption_write; /* Copy the temporary stream to the encryption sink. */ { LARGE_INTEGER off; ULONG nread; char buffer[4096]; off.QuadPart = 0; hr = tmpstream->Seek (off, STREAM_SEEK_SET, NULL); if (hr) { log_error ("%s:%s: seeking back to the begin failed: hr=%#lx", SRCNAME, __func__, hr); rc = gpg_error (GPG_ERR_EIO); goto failure; } for (;;) { hr = tmpstream->Read (buffer, sizeof buffer, &nread); if (hr) { log_error ("%s:%s: IStream::Read failed: hr=%#lx", SRCNAME, __func__, hr); rc = gpg_error (GPG_ERR_EIO); goto failure; } if (!nread) break; /* EOF */ rc = write_buffer (encsink, buffer, nread); if (rc) { log_error ("%s:%s: writing tmpstream to encsink failed: %s", SRCNAME, __func__, gpg_strerror (rc)); goto failure; } } } /* Flush the encryption sink and wait for the encryption to get ready. */ if ((rc = write_buffer (encsink, NULL, 0))) goto failure; if ((rc = engine_wait (filter))) goto failure; filter = NULL; /* Not valid anymore. */ encsink->cb_data = NULL; /* Not needed anymore. */ if (!sink->enc_counter) { log_debug ("%s:%s: nothing received from engine", SRCNAME, __func__); goto failure; } /* Write the final boundary (for OpenPGP) and finish the attachment. */ if (*boundary && (rc = write_boundary (sink, boundary, 1))) goto failure; if (close_mapi_attachment (&attach, sink)) goto failure; if (finalize_message (message, att_table, protocol, 1)) goto failure; result = 0; /* Everything is fine, fall through the cleanup now. */ failure: if (result) log_debug ("%s:%s: failed rc=%d (%s) <%s>", SRCNAME, __func__, rc, gpg_strerror (rc), gpg_strsource (rc)); engine_cancel (filter); gpgol_release (tmpstream); mapi_release_attach_table (att_table); xfree (my_sender); return result; } int restore_msg_from_moss (LPMESSAGE message, LPDISPATCH moss_att, msgtype_t type, char *msgcls) { struct sink_s sinkmem; sink_t sink = &sinkmem; char *orig = NULL; int err = -1; char boundary[BOUNDARYSIZE+1]; (void)msgcls; LPATTACH new_attach = create_mapi_attachment (message, sink); log_debug ("Restore message from moss called."); if (!new_attach) { log_error ("%s:%s: Error: %i", SRCNAME, __func__, __LINE__); goto done; } // TODO MORE if (type == MSGTYPE_SMIME) { create_top_encryption_header (sink, PROTOCOL_SMIME, boundary); } else if (type == MSGTYPE_GPGOL_MULTIPART_ENCRYPTED) { create_top_encryption_header (sink, PROTOCOL_OPENPGP, boundary); } else { log_error ("%s:%s: Unsupported messagetype: %i", SRCNAME, __func__, type); goto done; } orig = get_pa_string (moss_att, PR_ATTACH_DATA_BIN_DASL); if (!orig) { log_error ("%s:%s: Error: %i", SRCNAME, __func__, __LINE__); goto done; } if (write_string (sink, orig)) { log_error ("%s:%s: Error: %i", SRCNAME, __func__, __LINE__); goto done; } if (*boundary && write_boundary (sink, boundary, 1)) { log_error ("%s:%s: Error: %i", SRCNAME, __func__, __LINE__); goto done; } if (close_mapi_attachment (&new_attach, sink)) { log_error ("%s:%s: Error: %i", SRCNAME, __func__, __LINE__); goto done; } /* Set a special property so that we are later able to identify messages signed or encrypted by us. */ if (mapi_set_sig_status (message, "@")) { log_error ("%s:%s: Error: %i", SRCNAME, __func__, __LINE__); goto done; } err = 0; done: xfree (orig); return err; } diff --git a/src/oomhelp.h b/src/oomhelp.h index cbc36ca..3bf86a6 100644 --- a/src/oomhelp.h +++ b/src/oomhelp.h @@ -1,348 +1,350 @@ /* oomhelp.h - Defs for helper functions for the Outlook Object Model * Copyright (C) 2009 g10 Code GmbH * 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 OOMHELP_H #define OOMHELP_H #include #include "mymapi.h" #include "myexchext.h" #define MSOCONTROLBUTTON 1 #define MSOCONTROLEDIT 2 #define MSOCONTROLDROPDOWN 3 #define MSOCONTROLCOMBOBOX 4 #define MSOCONTROLPOPUP 10 enum { msoButtonAutomatic = 0, msoButtonIcon = 1, msoButtonCaption = 2, msoButtonIconAndCaption = 3, msoButtonIconAndWrapCaption = 7, msoButtonIconAndCaptionBelow = 11, msoButtonWrapCaption = 14, msoButtonIconAndWrapCaptionBelow = 15 }; enum { msoButtonDown = -1, msoButtonUp = 0, msoButtonMixed = 2 }; DEFINE_GUID(GUID_NULL, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); DEFINE_GUID(IID_IConnectionPoint, 0xb196b286, 0xbab4, 0x101a, 0xb6, 0x9c, 0x00, 0xaa, 0x00, 0x34, 0x1d, 0x07); DEFINE_GUID(IID_IConnectionPointContainer, 0xb196b284, 0xbab4, 0x101a, 0xb6, 0x9c, 0x00, 0xaa, 0x00, 0x34, 0x1d, 0x07); DEFINE_GUID(IID_IPictureDisp, 0x7bf80981, 0xbf32, 0x101a, 0x8b, 0xbb, 0x00, 0xaa, 0x00, 0x30, 0x0c, 0xab); DEFINE_GUID(IID_ApplicationEvents, 0x0006304E, 0x0000, 0x0000, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); DEFINE_GUID(IID_ExplorerEvents, 0x0006300F, 0x0000, 0x0000, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); DEFINE_GUID(IID_ExplorersEvents, 0x00063078, 0x0000, 0x0000, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); DEFINE_GUID(IID_MailItemEvents, 0x0006302B, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); DEFINE_GUID(IID_MailItem, 0x00063034, 0x0000, 0x0000, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); DEFINE_GUID(IID_IMAPISecureMessage, 0x253cc320, 0xeab6, 0x11d0, 0x82, 0x22, 0, 0x60, 0x97, 0x93, 0x87, 0xea); DEFINE_OLEGUID(IID_IUnknown, 0x00000000, 0, 0); DEFINE_OLEGUID(IID_IDispatch, 0x00020400, 0, 0); DEFINE_OLEGUID(IID_IOleWindow, 0x00000114, 0, 0); #ifndef PR_SMTP_ADDRESS_DASL #define PR_SMTP_ADDRESS_DASL \ "http://schemas.microsoft.com/mapi/proptag/0x39FE001E" #endif #ifndef PR_EMS_AB_PROXY_ADDRESSES_DASL #define PR_EMS_AB_PROXY_ADDRESSES_DASL \ "http://schemas.microsoft.com/mapi/proptag/0x800F101E" #endif #ifndef PR_ATTACHMENT_HIDDEN_DASL #define PR_ATTACHMENT_HIDDEN_DASL \ "http://schemas.microsoft.com/mapi/proptag/0x7FFE000B" #endif #define PR_MESSAGE_CLASS_W_DASL \ "http://schemas.microsoft.com/mapi/proptag/0x001A001F" #define GPGOL_ATTACHTYPE_DASL \ "http://schemas.microsoft.com/mapi/string/" \ "{31805AB8-3E92-11DC-879C-00061B031004}/GpgOL Attach Type/0x00000003" #define GPGOL_UID_DASL \ "http://schemas.microsoft.com/mapi/string/" \ "{31805AB8-3E92-11DC-879C-00061B031004}/GpgOL UID/0x0000001F" #define PR_ATTACH_DATA_BIN_DASL \ "http://schemas.microsoft.com/mapi/proptag/0x37010102" #define PR_BODY_W_DASL \ "http://schemas.microsoft.com/mapi/proptag/0x1000001F" #define PR_ATTACHMENT_HIDDEN_DASL \ "http://schemas.microsoft.com/mapi/proptag/0x7FFE000B" #define PR_ATTACH_MIME_TAG_DASL \ "http://schemas.microsoft.com/mapi/proptag/0x370E001F" #define PR_ATTACH_CONTENT_ID_DASL \ "http://schemas.microsoft.com/mapi/proptag/0x3712001F" #define PR_TAG_SENDER_SMTP_ADDRESS \ "http://schemas.microsoft.com/mapi/proptag/0x5D01001F" #define PR_TAG_RECEIVED_REPRESENTING_SMTP_ADDRESS \ "http://schemas.microsoft.com/mapi/proptag/0x5D08001F" +#define PR_PIDNameContentType_DASL \ + "http://schemas.microsoft.com/mapi/string/{00020386-0000-0000-C000-000000000046}/content-type/0x0000001F" #ifdef __cplusplus extern "C" { #if 0 } #endif #endif /* Return the malloced name of an COM+ object. */ char *get_object_name (LPUNKNOWN obj); /* Helper to lookup a dispid. */ DISPID lookup_oom_dispid (LPDISPATCH pDisp, const char *name); /* Return the OOM object's IDispatch interface described by FULLNAME. */ LPDISPATCH get_oom_object (LPDISPATCH pStart, const char *fullname); /* Set the Icon of a CommandBarControl. */ int put_oom_icon (LPDISPATCH pDisp, int rsource_id, int size); /* Set the boolean property NAME to VALUE. */ int put_oom_bool (LPDISPATCH pDisp, const char *name, int value); /* Set the property NAME to VALUE. */ int put_oom_int (LPDISPATCH pDisp, const char *name, int value); /* Set the property NAME to STRING. */ int put_oom_string (LPDISPATCH pDisp, const char *name, const char *string); /* Set the property NAME to DISP. */ int put_oom_disp (LPDISPATCH pDisp, const char *name, LPDISPATCH value); /* Get the boolean property NAME of the object PDISP. */ int get_oom_bool (LPDISPATCH pDisp, const char *name); /* Get the integer property NAME of the object PDISP. */ int get_oom_int (LPDISPATCH pDisp, const char *name); /* Get the string property NAME of the object PDISP. */ char *get_oom_string (LPDISPATCH pDisp, const char *name); /* Get an IUnknown object from property NAME of PDISP. */ LPUNKNOWN get_oom_iunknown (LPDISPATCH pDisp, const char *name); /* Return the control object with tag property value TAG. */ LPDISPATCH get_oom_control_bytag (LPDISPATCH pObj, const char *tag); /* Add a new button to an object which supports the add method. Returns the new object or NULL on error. */ LPDISPATCH add_oom_button (LPDISPATCH pObj); /* Delete a button. */ void del_oom_button (LPDISPATCH button); /* Get the HWND of the active window in the current context */ HWND get_oom_context_window (LPDISPATCH context); /* Get the address of the recipients as string list */ char ** get_oom_recipients (LPDISPATCH recipients); /* Add an attachment to a dispatcher */ int add_oom_attachment (LPDISPATCH disp, const wchar_t* inFile, const wchar_t *displayName); /* Look up a string with the propertyAccessor interface */ char * get_pa_string (LPDISPATCH pDisp, const char *property); /* Look up a long with the propertyAccessor interface. returns -1 on error.*/ int get_pa_int (LPDISPATCH pDisp, const char *property, int *rInt); /* Set a variant with the propertyAccessor interface. This is tested to work at least vor BSTR variants. Trying to set PR_ATTACH_DATA_BIN_DASL with this failed with hresults 0x80020005 type mismatch or 0x80020008 vad variable type for: VT_ARRAY | VT_UI1 | VT_BYREF VT_SAFEARRAY | VT_UI1 | VT_BYREF VT_BSTR | VT_BYREF VT_BSTR VT_ARRAY | VT_UI1 VT_SAFEARRAY | VT_UI1 No idea whats wrong there. Needs more experiments. The Type is only documented as "Binary". Outlookspy also fails with the same error when trying to modify the property. */ int put_pa_string (LPDISPATCH pDisp, const char *dasl_id, const char *value); int put_pa_variant (LPDISPATCH pDisp, const char *dasl_id, VARIANT *value); int put_pa_int (LPDISPATCH pDisp, const char *dasl_id, int value); /* Look up a variant with the propertyAccessor interface */ int get_pa_variant (LPDISPATCH pDisp, const char *dasl_id, VARIANT *rVariant); /* Look up a LONG with the propertyAccessor interface */ LONG get_pa_long (LPDISPATCH pDisp, const char *dasl_id); /* Queries the interface of the dispatcher for the id id. Returns NULL on error. The returned Object must be released. Mainly useful to check if an object is what it appears to be. */ LPDISPATCH get_object_by_id (LPDISPATCH pDisp, REFIID id); /* Obtain the MAPI Message corresponding to the Mailitem. Returns NULL on error. The returned Message needs to be released by the caller */ LPMESSAGE get_oom_message (LPDISPATCH mailitem); /* Obtain the Base MAPI Message of a MailItem. The parameter should be a pointer to a MailItem. returns NULL on error. The returned Message needs to be released by the caller. */ LPMESSAGE get_oom_base_message (LPDISPATCH mailitem); /* Get a strong reference for a mail object by calling Application.GetObjectReference with type strong. The documentation is unclear what this acutally does. This function is left over from experiments about strong references. Maybe there is a use for them. The reference we use in the Mail object is documented as a Weak reference. But changing that does not appear to make a difference. */ LPDISPATCH get_strong_reference (LPDISPATCH mail); /* Invoke a method of an outlook object. returns true on success false otherwise. rVariant should either point to a propery initialized variant (initinalized wiht VariantInit) to hold the return value or a pointer to NULL. */ int invoke_oom_method (LPDISPATCH pDisp, const char *name, VARIANT *rVariant); /* Invoke a method of an outlook object. returns true on success false otherwise. rVariant should either point to a propery initialized variant (initinalized wiht VariantInit) to hold the return value or a pointer to NULL. parms can optionally be used to provide a DISPPARAMS structure with parameters for the function. */ int invoke_oom_method_with_parms (LPDISPATCH pDisp, const char *name, VARIANT *rVariant, DISPPARAMS *params); /* Try to obtain the mapisession through the Application. returns NULL on error.*/ LPMAPISESSION get_oom_mapi_session (void); /* Ensure a category of the name name exists in the session for the Mail mail. Creates the category with the specified color if required. returns 0 on success. */ void ensure_category_exists (LPDISPATCH mail, const char *category, int color); /* Add a category to a mail if it is not already added. */ int add_category (LPDISPATCH mail, const char *category); /* Remove a category from a mail if it was added. */ int remove_category (LPDISPATCH mail, const char *category); /* Get a unique identifier for a mail object. The uuid is a custom property. If create is set a new uuid will be added if none exists and the value of that uuid returned. The optinal uuid value can be set to be used as uuid instead of a generated one. Return value has to be freed by the caller. */ char * get_unique_id (LPDISPATCH mail, int create, const char* uuid); /* Uses the Application->ActiveWindow to determine the hwnd through FindWindow and the caption. Does not use IOleWindow because that was unreliable somhow. */ HWND get_active_hwnd (void); /* Create a new mailitem and return it */ LPDISPATCH create_mail (void); LPDISPATCH get_account_for_mail (const char *mbox); /* Sender fallbacks. All return either null or a malloced address. */ char *get_sender_CurrentUser (LPDISPATCH mailitem); char *get_sender_Sender (LPDISPATCH mailitem); char *get_sender_SenderEMailAddress (LPDISPATCH mailitem); /* Get the body of the active inline response */ char *get_inline_body (void); #ifdef __cplusplus char *get_sender_SendUsingAccount (LPDISPATCH mailitem, bool *r_is_GSuite); } #endif #endif /*OOMHELP_H*/