diff --git a/src/attachment.cpp b/src/attachment.cpp index 265483a..d259621 100644 --- a/src/attachment.cpp +++ b/src/attachment.cpp @@ -1,91 +1,240 @@ /* attachment.cpp - Functions for attachment handling * Copyright (C) 2005, 2007 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 . */ #include "config.h" #include "common_indep.h" #include "attachment.h" +#include "mymapitags.h" #include +#define COPYBUFSIZE (8 * 1024) + Attachment::Attachment() { memdbg_ctor ("Attachment"); } +#ifndef BUILD_TESTS +Attachment::Attachment(LPDISPATCH attach) +{ + memdbg_ctor ("Attachment"); + if (!attach) + { + STRANGEPOINT; + TRETURN; + } + int type = get_oom_int (attach, "Type"); + m_utf8DisplayName = get_oom_string (attach, "DisplayName"); + m_fileName = get_oom_string (attach, "FileName"); + + log_dbg ("Creating attachment obj for type: %i for %s", + type, anonstr (m_utf8DisplayName.c_str ())); + if (type == olByValue) + { + log_dbg ("Copying attachment by value"); + LPATTACH mapi_attachment = nullptr; + mapi_attachment = (LPATTACH) get_oom_iunknown (attach, + "MapiObject"); + if (!mapi_attachment) + { + log_debug ("%s:%s: Failed to get MapiObject of attachment: %p", + SRCNAME, __func__, attach); + TRETURN; + } + LPSTREAM stream = nullptr; + if (FAILED (gpgol_openProperty (mapi_attachment, PR_ATTACH_DATA_BIN, + &IID_IStream, 0, MAPI_MODIFY, + (LPUNKNOWN*) &stream))) + { + log_debug ("%s:%s: Failed to open stream for mapi_attachment: %p", + SRCNAME, __func__, mapi_attachment); + gpgol_release (mapi_attachment); + TRETURN; + } + gpgol_release (mapi_attachment); + char buf[COPYBUFSIZE]; + HRESULT hr; + ULONG bRead = 0; + while ((hr = stream->Read (buf, COPYBUFSIZE, &bRead)) == S_OK || + hr == S_FALSE) + { + if (!bRead) + { + // EOF + break; + } + m_data.write (buf, bRead); + } + gpgol_release (stream); + } + else + { + log_dbg ("Creating attachment for other type."); + HANDLE hTmpFile = INVALID_HANDLE_VALUE; + char *path = get_tmp_outfile_utf8 (m_fileName.c_str (), &hTmpFile); + if (!path || hTmpFile == INVALID_HANDLE_VALUE) + { + STRANGEPOINT; + TRETURN; + } + CloseHandle (hTmpFile); + if (oom_save_as_file (attach, path)) + { + log_err ("Failed to call SaveAsFile."); + TRETURN; + } + wchar_t *wname = utf8_to_wchar (path); + hTmpFile = CreateFileW (wname, + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_DELETE, + NULL, + OPEN_EXISTING, + FILE_FLAG_DELETE_ON_CLOSE, + NULL); + xfree (wname); + if (hTmpFile == INVALID_HANDLE_VALUE) + { + log_debug_w32 (-1, "%s:%s: Failed to open attachment file.", + SRCNAME, __func__); + } + + /* Read the attachment file into data */ + readFullFile (hTmpFile, m_data); + CloseHandle (hTmpFile); + } +} +#endif + Attachment::~Attachment() { memdbg_dtor ("Attachment"); log_debug ("%s:%s", SRCNAME, __func__); } std::string Attachment::get_display_name() const { return m_utf8DisplayName; } void Attachment::set_display_name(const char *name) { if (!name) { log_error ("%s:%s: Display name set to null.", SRCNAME, __func__); return; } m_utf8DisplayName = std::string(name); } +std::string +Attachment::get_file_name () const +{ + if (m_fileName.size ()) + { + return m_fileName; + } + return m_utf8DisplayName; +} + void Attachment::set_attach_type(attachtype_t type) { m_type = type; } GpgME::Data & Attachment::get_data() { return m_data; } void Attachment::set_content_id(const char *cid) { m_cid = cid; } std::string Attachment::get_content_id() const { return m_cid; } void Attachment::set_content_type(const char *ctype) { m_ctype = ctype; } std::string Attachment::get_content_type() const { return m_ctype; } + +int +Attachment::copy_to (HANDLE hFile) +{ + TSTART; + 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 */ + m_data.seek (0, SEEK_SET); + while ((nread = m_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__); + TRETURN 1; + } + if (nread != nwritten) + { + log_error ("%s:%s: Write truncated.", + SRCNAME, __func__); + TRETURN 1; + } + } + TRETURN 0; +} diff --git a/src/attachment.h b/src/attachment.h index 79f91df..a94bc17 100644 --- a/src/attachment.h +++ b/src/attachment.h @@ -1,61 +1,82 @@ /* attachment.h - Wrapper class for attachments * Copyright (C) 2005, 2007 g10 Code GmbH * Copyright (C) 2015, 2016 by Bundesamt für Sicherheit in der Informationstechnik * Software engineering by Intevation GmbH * * This file is part of GpgOL. * * GpgOL is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * GpgOL is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, see . */ #ifndef ATTACHMENT_H #define ATTACHMENT_H #include #include +#ifdef _WIN32 +# include "oomhelp.h" +#endif /** Helper class for attachment actions. */ class Attachment { public: + enum olAttachmentType + { + olByValue = 1, + olByReference = 4, + olEmbeddeditem = 5, + olOLE = 6, + }; /** Creates and opens a new in memory attachment. */ Attachment(); ~Attachment(); /** Set the display name */ void set_display_name(const char *name); std::string get_display_name() const; + /** Get file name, usually the same as display name */ + std::string get_file_name() const; + void set_attach_type(attachtype_t type); /* Content id */ void set_content_id (const char *cid); std::string get_content_id() const; /* Content type */ void set_content_type (const char *type); std::string get_content_type () const; /* get the underlying data structure */ GpgME::Data& get_data(); +#ifdef _WIN32 + /** Create a data struct from OOM attachment */ + Attachment (LPDISPATCH attach); + + /** Copy the attachment to a file handle */ + int copy_to (HANDLE hFile); +#endif private: GpgME::Data m_data; std::string m_utf8DisplayName; + std::string m_fileName; attachtype_t m_type; std::string m_cid; std::string m_ctype; }; #endif // ATTACHMENT_H diff --git a/src/common.cpp b/src/common.cpp index 7c20e4e..c44d8c6 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -1,1053 +1,1089 @@ /* common.c - Common routines used by GpgOL * Copyright (C) 2005, 2007, 2008 g10 Code GmbH * 2015, 2016, 2017 Bundesamt für Sicherheit in der Informationstechnik * Software engineering by Intevation GmbH * 2020 g10 Code 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 * 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 #define OEMRESOURCE /* Required for OBM_CHECKBOXES. */ #include #include #ifndef CSIDL_APPDATA #define CSIDL_APPDATA 0x001a #endif #ifndef CSIDL_LOCAL_APPDATA #define CSIDL_LOCAL_APPDATA 0x001c #endif #ifndef CSIDL_FLAG_CREATE #define CSIDL_FLAG_CREATE 0x8000 #endif #include #include #include #include "common.h" #include "dialogs.h" #include "cpphelp.h" #include #include #include #include #include +#define COPYBUFSIZE (8 * 1024) + HINSTANCE glob_hinst = NULL; void set_global_hinstance (HINSTANCE hinst) { glob_hinst = hinst; } void bring_to_front (HWND wid) { if (wid) { if (!SetForegroundWindow (wid)) { log_debug ("%s:%s: SetForegroundWindow failed", SRCNAME, __func__); /* Yet another fallback which will not work on some * versions and is not recommended by msdn */ if (!ShowWindow (wid, SW_SHOWNORMAL)) { log_debug ("%s:%s: ShowWindow failed.", SRCNAME, __func__); } } } log_debug ("%s:%s: done", SRCNAME, __func__); } void fatal_error (const char *format, ...) { va_list arg_ptr; char buf[512]; va_start (arg_ptr, format); vsnprintf (buf, sizeof buf -1, format, arg_ptr); buf[sizeof buf - 1] = 0; va_end (arg_ptr); MessageBox (NULL, buf, "Fatal Error", MB_OK); abort (); } /* Helper for read_w32_registry_string(). */ static HKEY get_root_key(const char *root) { HKEY root_key; if( !root ) root_key = HKEY_CURRENT_USER; else if( !strcmp( root, "HKEY_CLASSES_ROOT" ) ) root_key = HKEY_CLASSES_ROOT; else if( !strcmp( root, "HKEY_CURRENT_USER" ) ) root_key = HKEY_CURRENT_USER; else if( !strcmp( root, "HKEY_LOCAL_MACHINE" ) ) root_key = HKEY_LOCAL_MACHINE; else if( !strcmp( root, "HKEY_USERS" ) ) root_key = HKEY_USERS; else if( !strcmp( root, "HKEY_PERFORMANCE_DATA" ) ) root_key = HKEY_PERFORMANCE_DATA; else if( !strcmp( root, "HKEY_CURRENT_CONFIG" ) ) root_key = HKEY_CURRENT_CONFIG; else return NULL; return root_key; } #if defined(_WIN64) #define CROSS_ACCESS KEY_WOW64_32KEY #else #define CROSS_ACCESS KEY_WOW64_64KEY #endif std::string _readRegStr (HKEY root_key, const char *dir, const char *name, bool alternate) { #ifndef _WIN32 (void) root_key; (void)alternate; (void)dir; (void)name; return std::string(); #else HKEY key_handle; DWORD n1, nbytes, type; std::string ret; DWORD flags = KEY_READ; if (alternate) { flags |= CROSS_ACCESS; } if (RegOpenKeyExA(root_key, dir, 0, flags, &key_handle)) { return ret; } nbytes = 1; if (RegQueryValueExA(key_handle, name, 0, nullptr, nullptr, &nbytes)) { RegCloseKey (key_handle); return ret; } n1 = nbytes+1; char result[n1]; if (RegQueryValueExA(key_handle, name, 0, &type, (LPBYTE)result, &n1)) { RegCloseKey(key_handle); return ret; } RegCloseKey(key_handle); result[nbytes] = 0; /* make sure it is really a string */ ret = result; if (type == REG_EXPAND_SZ && strchr (result, '%')) { n1 += 1000; char tmp[n1 +1]; nbytes = ExpandEnvironmentStringsA(ret.c_str(), tmp, n1); if (nbytes && nbytes > n1) { n1 = nbytes; char tmp2[n1 +1]; nbytes = ExpandEnvironmentStringsA(result, tmp2, n1); if (nbytes && nbytes > n1) { /* oops - truncated, better don't expand at all */ return ret; } tmp2[nbytes] = 0; ret = tmp2; } else if (nbytes) { /* okay, reduce the length */ tmp[nbytes] = 0; ret = tmp; } } return ret; #endif } std::string readRegStr (const char *root, const char *dir, const char *name) { #ifndef _WIN32 (void)root; (void)dir; (void)name; return std::string(); #else HKEY root_key; std::string ret; if (!(root_key = get_root_key(root))) { return ret; } ret = _readRegStr (root_key, dir, name, false); if (ret.empty()) { // Try alternate as fallback ret = _readRegStr (root_key, dir, name, true); } if (ret.empty()) { // Try local machine as fallback. ret = _readRegStr (HKEY_LOCAL_MACHINE, dir, name, false); if (ret.empty()) { // Try alternative registry view as fallback ret = _readRegStr (HKEY_LOCAL_MACHINE, dir, name, true); } } return ret; #endif } /* Return a string from the Win32 Registry or NULL in case of error. Caller must release the return value. A NULL for root is an alias for HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE in turn. NOTE: The value is allocated with a plain xmalloc () - use xfree () and not the usual xfree(). */ char * read_w32_registry_string (const char *root, const char *dir, const char *name) { const auto ret = readRegStr (root, dir, name); if (ret.empty()) { return nullptr; } return xstrdup (ret.c_str ()); } /* Return the data dir used for forms etc. Returns NULL on error. */ char * get_data_dir (void) { char *instdir; char *p; char *dname; instdir = get_gpg4win_dir(); if (!instdir) return NULL; /* Build the key: "/share/gpgol". */ #define SDDIR "\\share\\gpgol" dname = (char*) xmalloc (strlen (instdir) + strlen (SDDIR) + 1); if (!dname) { xfree (instdir); return NULL; } p = dname; strcpy (p, instdir); p += strlen (instdir); strcpy (p, SDDIR); xfree (instdir); #undef SDDIR return dname; } /* Percent-escape the string STR by replacing colons with '%3a'. If EXTRA is not NULL all characters in it are also escaped. */ char * percent_escape (const char *str, const char *extra) { int i, j; char *ptr; if (!str) return NULL; for (i=j=0; str[i]; i++) if (str[i] == ':' || str[i] == '%' || (extra && strchr (extra, str[i]))) j++; ptr = (char *) xmalloc (i + 2 * j + 1); i = 0; while (*str) { /* FIXME: Work around a bug in Kleo. */ if (*str == ':') { ptr[i++] = '%'; ptr[i++] = '3'; ptr[i++] = 'a'; } else { if (*str == '%') { ptr[i++] = '%'; ptr[i++] = '2'; ptr[i++] = '5'; } else if (extra && strchr (extra, *str)) { ptr[i++] = '%'; ptr[i++] = tohex_lower ((*str >> 4) & 15); ptr[i++] = tohex_lower (*str & 15); } else ptr[i++] = *str; } str++; } ptr[i] = '\0'; return ptr; } /* Fix linebreaks. This replaces all consecutive \r or \n characters by a single \n. There can be extremly weird combinations of linebreaks like \r\r\n\r\r\n at the end of each line when getting the body of a mail message. */ void fix_linebreaks (char *str, int *len) { char *src; char *dst; src = str; dst = str; while (*src) { if (*src == '\r' || *src == '\n') { do src++; while (*src == '\r' || *src == '\n'); *(dst++) = '\n'; } else { *(dst++) = *(src++); } } *dst = '\0'; *len = dst - str; } /* Get a pretty name for the file at path path. File extension will be set to work for the protocol as provided in protocol and depends on the signature setting. Set signature to 0 if the extension should not be a signature extension. Returns NULL on success. Caller must free result. */ wchar_t * get_pretty_attachment_name (wchar_t *path, protocol_t protocol, int signature) { wchar_t* pretty; wchar_t* buf; if (!path || !wcslen (path)) { log_error("%s:%s: No path given", SRCNAME, __func__); return NULL; } pretty = (wchar_t*) xmalloc ((MAX_PATH + 1) * sizeof (wchar_t)); memset (pretty, 0, (MAX_PATH + 1) * sizeof (wchar_t)); buf = wcsrchr (path, '\\') + 1; if (!buf || !*buf) { log_error("%s:%s: No filename found in path", SRCNAME, __func__); xfree (pretty); return NULL; } wcscpy (pretty, buf); buf = pretty + wcslen(pretty); if (signature) { if (protocol == PROTOCOL_SMIME) { *(buf++) = '.'; *(buf++) = 'p'; *(buf++) = '7'; *(buf++) = 's'; } else { *(buf++) = '.'; *(buf++) = 's'; *(buf++) = 'i'; *(buf++) = 'g'; } } else { if (protocol == PROTOCOL_SMIME) { *(buf++) = '.'; *(buf++) = 'p'; *(buf++) = '7'; *(buf++) = 'm'; } else { *(buf++) = '.'; *(buf++) = 'g'; *(buf++) = 'p'; *(buf++) = 'g'; } } return pretty; } -static HANDLE +HANDLE CreateFileUtf8 (const char *utf8Name) { if (!utf8Name) { return INVALID_HANDLE_VALUE; } wchar_t *wname = utf8_to_wchar (utf8Name); if (!wname) { TRACEPOINT; return INVALID_HANDLE_VALUE; } auto ret = CreateFileW (wname, GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, NULL, CREATE_NEW, FILE_ATTRIBUTE_TEMPORARY, NULL); xfree (wname); return ret; } +int +readFullFile (HANDLE hFile, GpgME::Data &data) +{ + char buf[COPYBUFSIZE]; + DWORD bRead = 0; + BOOL ret; + while ((ret = ReadFile (hFile, buf, COPYBUFSIZE, &bRead, nullptr))) + { + if (!bRead) + { + // EOF + break; + } + data.write (buf, bRead); + } + if (!ret && bRead) + { + log_err ("Failed to read from file"); + TRETURN -1; + } + TRETURN 0; +} + static std::string getTmpPathUtf8 () { static std::string ret; if (!ret.empty()) { return ret; } wchar_t tmpPath[MAX_PATH + 2]; if (!GetTempPathW (MAX_PATH, tmpPath)) { log_error ("%s:%s: Could not get tmp path.", SRCNAME, __func__); return ret; } char *utf8Name = wchar_to_utf8 (tmpPath); if (!utf8Name) { TRACEPOINT; return ret; } ret = utf8Name; xfree (utf8Name); return ret; } /* Open a file in a temporary directory, take name as a suggestion and put the open Handle in outHandle. Returns the actually used file name in case there were other files with that name. */ wchar_t* get_tmp_outfile (const wchar_t *name, HANDLE *outHandle) { const auto utf8Name = wchar_to_utf8_string (name); const auto tmpPath = getTmpPathUtf8 (); if (utf8Name.empty() || tmpPath.empty()) { TRACEPOINT; return nullptr; } auto outName = tmpPath + utf8Name; log_data("%s:%s: Attachment candidate is %s", SRCNAME, __func__, outName.c_str ()); int tries = 1; while ((*outHandle = CreateFileUtf8 (outName.c_str ())) == INVALID_HANDLE_VALUE) { log_debug_w32 (-1, "%s:%s: Failed to open candidate '%s'", SRCNAME, __func__, anonstr (outName.c_str())); - char *outNameC = xstrdup (outName.c_str()); + char *outNameC = xstrdup ((tmpPath + utf8Name).c_str ()); const auto lastBackslash = strrchr (outNameC, '\\'); if (!lastBackslash) { /* This is an error because tmp name by definition contains one */ log_error ("%s:%s: No backslash in origname '%s'", SRCNAME, __func__, outNameC); xfree (outNameC); return NULL; } auto fileExt = strchr (lastBackslash, '.'); if (fileExt) { *fileExt = '\0'; ++fileExt; } // OutNameC is now without an extension and if // there is a file ext it now points to the extension. outName = outNameC + std::string("_") + std::to_string(tries++); if (fileExt) { outName += std::string(".") + fileExt; } xfree (outNameC); if (tries == 50) { /* Mmh fishy, maybe the name cannot be created on the file system. Let's switch to a generic name. */ log_dbg ("Can't find an attachment name. " "Switching over to a generic attachment name."); outName = tmpPath + "attachment"; if (fileExt) { outName += "."; outName += fileExt; } } if (tries > 100) { /* You have to know when to give up,.. */ log_error ("%s:%s: Could not get a name out of 100 tries", SRCNAME, __func__); return NULL; } } return utf8_to_wchar (outName.c_str ()); } +char * +get_tmp_outfile_utf8 (const char *name, HANDLE *outHandle) +{ + wchar_t *wname = utf8_to_wchar (name); + wchar_t *wret = get_tmp_outfile (wname, outHandle); + xfree (wname); + char *ret = wchar_to_utf8 (wret); + xfree (wret); + return ret; +} + /** Get the Gpg4win Install directory. * * Looks first for the Gpg4win 3.x registry key. Then for the Gpg4win * 2.x registry key. And checks that the directory can be read. * * @returns NULL if no dir could be found. Otherwise a malloced string. */ char * get_gpg4win_dir() { const char *g4win_keys[] = {GPG4WIN_REGKEY_3, GPG4WIN_REGKEY_2, NULL}; const char **key; for (key = g4win_keys; *key; key++) { char *tmp = read_w32_registry_string (NULL, *key, "Install Directory"); if (!tmp) { continue; } if (!access(tmp, R_OK)) { return tmp; } else { log_debug ("Failed to access: %s\n", tmp); xfree (tmp); } } return NULL; } static void epoch_to_file_time (unsigned long time, LPFILETIME pft) { LONGLONG ll; ll = Int32x32To64(time, 10000000) + 116444736000000000; pft->dwLowDateTime = (DWORD)ll; pft->dwHighDateTime = ll >> 32; } char * format_date_from_gpgme (unsigned long time) { wchar_t buf[256]; FILETIME ft; SYSTEMTIME st; epoch_to_file_time (time, &ft); FileTimeToSystemTime(&ft, &st); int ret = GetDateFormatEx (NULL, DATE_SHORTDATE, &st, NULL, buf, 256, NULL); if (ret == 0) { return NULL; } return wchar_to_utf8 (buf); } /* Return the name of the default UI server. This name is used to auto start an UI server if an initial connect failed. */ char * get_uiserver_name (void) { char *name = NULL; char *dir, *uiserver, *p; int extra_arglen = 9; const char * server_names[] = {"kleopatra.exe", "bin\\kleopatra.exe", "gpa.exe", "bin\\gpa.exe", NULL}; const char **tmp = NULL; dir = get_gpg4win_dir (); if (!dir) { log_error ("Failed to find gpg4win dir"); return NULL; } uiserver = read_w32_registry_string (NULL, GPG4WIN_REGKEY_3, "UI Server"); if (!uiserver) { uiserver = read_w32_registry_string (NULL, GPG4WIN_REGKEY_2, "UI Server"); } if (uiserver) { name = (char*) xmalloc (strlen (dir) + strlen (uiserver) + extra_arglen + 2); strcpy (stpcpy (stpcpy (name, dir), "\\"), uiserver); for (p = name; *p; p++) if (*p == '/') *p = '\\'; xfree (uiserver); } if (name && !access (name, F_OK)) { /* Set through registry and is accessible */ xfree(dir); return name; } /* Fallbacks */ for (tmp = server_names; *tmp; tmp++) { if (name) { xfree (name); } name = (char *) xmalloc (strlen (dir) + strlen (*tmp) + extra_arglen + 2); strcpy (stpcpy (stpcpy (name, dir), "\\"), *tmp); for (p = name; *p; p++) if (*p == '/') *p = '\\'; if (!access (name, F_OK)) { /* Found a viable candidate */ if (strstr (name, "kleopatra.exe")) { strcat (name, " --daemon"); } xfree (dir); return name; } } xfree (dir); log_error ("Failed to find a viable UIServer"); return NULL; } int has_high_integrity(HANDLE hToken) { PTOKEN_MANDATORY_LABEL integrity_label = NULL; DWORD integrity_level = 0, size = 0; if (hToken == NULL || hToken == INVALID_HANDLE_VALUE) { log_debug ("Invalid parameters."); return 0; } /* Get the required size */ if (!GetTokenInformation (hToken, TokenIntegrityLevel, NULL, 0, &size)) { if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { log_debug ("Failed to get required size.\n"); return 0; } } integrity_label = (PTOKEN_MANDATORY_LABEL) LocalAlloc(0, size); if (integrity_label == NULL) { log_debug ("Failed to allocate label. \n"); return 0; } if (!GetTokenInformation (hToken, TokenIntegrityLevel, integrity_label, size, &size)) { log_debug ("Failed to get integrity level.\n"); LocalFree(integrity_label); return 0; } /* Get the last integrity level */ integrity_level = *GetSidSubAuthority(integrity_label->Label.Sid, (DWORD)(UCHAR)(*GetSidSubAuthorityCount( integrity_label->Label.Sid) - 1)); LocalFree (integrity_label); return integrity_level >= SECURITY_MANDATORY_HIGH_RID; } int is_elevated() { int ret = 0; HANDLE hToken = NULL; if (OpenProcessToken (GetCurrentProcess(), TOKEN_QUERY, &hToken)) { DWORD elevation; DWORD cbSize = sizeof (DWORD); if (GetTokenInformation (hToken, TokenElevation, &elevation, sizeof (TokenElevation), &cbSize)) { ret = elevation; } } /* Elevation will be true and ElevationType TokenElevationTypeFull even if the token is a user token created by SAFER so we additionally check the integrity level of the token which will only be high in the real elevated process and medium otherwise. */ ret = ret && has_high_integrity (hToken); if (hToken) CloseHandle (hToken); return ret; } int gpgol_message_box (HWND parent, const char *utf8_text, const char *utf8_caption, UINT type) { wchar_t *w_text = utf8_to_wchar (utf8_text); wchar_t *w_caption = utf8_to_wchar (utf8_caption); int ret = 0; MSGBOXPARAMSW mbp; mbp.cbSize = sizeof (MSGBOXPARAMS); mbp.hwndOwner = parent; mbp.hInstance = glob_hinst; mbp.lpszText = w_text; mbp.lpszCaption = w_caption; mbp.dwStyle = type | MB_USERICON; mbp.dwLanguageId = MAKELANGID (LANG_NEUTRAL, SUBLANG_DEFAULT); mbp.lpfnMsgBoxCallback = NULL; mbp.dwContextHelpId = 0; mbp.lpszIcon = (LPCWSTR) MAKEINTRESOURCE (IDI_GPGOL_LOCK_ICON); ret = MessageBoxIndirectW (&mbp); xfree (w_text); xfree (w_caption); return ret; } void gpgol_bug (HWND parent, int code) { const char *bugmsg = utf8_gettext ("Operation failed.\n\n" "This is usually caused by a bug in GpgOL or an error in your setup.\n" "Please see https://www.gpg4win.org/reporting-bugs.html " "or ask your Administrator for support."); char *with_code; gpgrt_asprintf (&with_code, "%s\nCode: %i", bugmsg, code); memdbg_alloc (with_code); gpgol_message_box (parent, with_code, _("GpgOL Error"), MB_OK); xfree (with_code); return; } static int store_config_value (HKEY hk, const char *path, const char *key, const char *val) { HKEY h; int type; int ec; if (hk == NULL) { hk = HKEY_CURRENT_USER; } ec = RegCreateKeyEx (hk, path, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &h, NULL); if (ec != ERROR_SUCCESS) { log_debug_w32 (ec, "creating/opening registry key `%s' failed", path); return -1; } type = strchr (val, '%')? REG_EXPAND_SZ : REG_SZ; ec = RegSetValueEx (h, key, 0, type, (const BYTE*)val, strlen (val)); if (ec != ERROR_SUCCESS) { log_debug_w32 (ec, "saving registry key `%s'->`%s' failed", path, key); RegCloseKey(h); return -1; } RegCloseKey(h); return 0; } /* Store a key in the registry with the key given by @key and the value @value. */ int store_extension_value (const char *key, const char *val) { return store_config_value (HKEY_CURRENT_USER, GPGOL_REGPATH, key, val); } /* Load a key from the registry with the key given by @key. The value is returned in @val and needs to freed by the caller. */ int load_extension_value (const char *key, char **val) { if (!val) { STRANGEPOINT; return -1; } *val = read_w32_registry_string (nullptr, GPGOL_REGPATH, key); log_debug ("%s:%s: LoadReg '%s' val '%s'", SRCNAME, __func__, key ? key : "null", *val ? *val : "null"); return 0; } int store_extension_subkey_value (const char *subkey, const char *key, const char *val) { int ret; char *path; gpgrt_asprintf (&path, "%s\\%s", GPGOL_REGPATH, subkey); memdbg_alloc (path); ret = store_config_value (HKEY_CURRENT_USER, path, key, val); xfree (path); return ret; } bool in_de_vs_mode() { /* We cache the values only once. A change requires restart. This is because checking this is very expensive as gpgconf spawns each process to query the settings. */ static bool checked; static bool vs_mode; if (checked) { return vs_mode; } checked = true; GpgME::Error err; const auto components = GpgME::Configuration::Component::load (err); log_debug ("%s:%s: Checking for de-vs mode.", SRCNAME, __func__); if (err) { log_error ("%s:%s: Failed to get gpgconf components: %s", SRCNAME, __func__, err.asString ()); vs_mode = false; return vs_mode; } for (const auto &component: components) { if (component.name () && !strcmp (component.name (), "gpg")) { for (const auto &option: component.options ()) { if (option.name () && !strcmp (option.name (), "compliance") && option.currentValue ().stringValue () && #ifdef HAVE_W32_SYSTEM !stricmp (option.currentValue ().stringValue (), "de-vs")) #else !strcasecmp (option.currentValue ().stringValue (), "de-vs")) #endif { log_debug ("%s:%s: Detected de-vs mode", SRCNAME, __func__); vs_mode = true; return vs_mode; } } vs_mode = false; return vs_mode; } } vs_mode = false; return false; } const char * de_vs_name (bool isCompliant) { static std::string compName; static std::string uncompName; if (!compName.empty() && isCompliant) { return compName.c_str (); } if (!uncompName.empty() && !isCompliant) { return uncompName.c_str (); } TSTART; /* Only look once */ compName = _("VS-NfD compliant"); uncompName = _("not VS-NfD compliant"); /* Find the libkleopatrarc */ char *instdir = get_gpg4win_dir(); if (!instdir) { STRANGEPOINT; TRETURN isCompliant ? compName.c_str () : uncompName.c_str (); } std::string filename = instdir; filename += "\\share\\libkleopatrarc"; std::ifstream file(filename.c_str ()); if (!file.is_open ()) { log_err ("Failed to open '%s'", filename.c_str ()); TRETURN isCompliant ? compName.c_str () : uncompName.c_str (); } std::string line; bool in_de_vs_filter = false; bool in_not_de_vs_filter = false; const char *lname = gettext_localename (); /* lname is never null */ std::string localemain = gpgol_split (lname, '_')[0]; std::string idealname = std::string ("Name[") + localemain + std::string("]"); while (std::getline(file, line)) { if (starts_with (line, "[")) { in_de_vs_filter = false; in_not_de_vs_filter = false; continue; } if (starts_with (line, "id=de-vs-filter")) { in_de_vs_filter = true; continue; } if (starts_with (line, "id=not-de-vs-filter")) { in_not_de_vs_filter = true; continue; } if ((in_de_vs_filter || in_not_de_vs_filter) && starts_with (line, "Name")) { const auto split = gpgol_split (line, '='); if (split.size() != 2) { log_err ("Invalid libkleopatrarc line: %s", line.c_str()); continue; } if (split[0] == "Name") { if (in_de_vs_filter) { compName = split[1]; } else { uncompName = split[1]; } } if (split[0] == idealname) { log_dbg ("Found localized de-vs name %s", split[1].c_str ()); if (in_de_vs_filter) { compName = split[1]; } else { uncompName = split[1]; } } } } file.close(); TRETURN isCompliant ? compName.c_str () : uncompName.c_str (); } std::string compliance_string (bool forVerify, bool forDecrypt, bool isCompliant) { const char *comp_name = de_vs_name (isCompliant); if (forDecrypt && forVerify) { return asprintf_s (_("The message is %s."), comp_name); } if (forVerify) { /* TRANSLATORS %s is compliance name like VS-NfD */ return asprintf_s (_("The signature is %s."), comp_name); } if (forDecrypt) { /* TRANSLATORS %s is compliance name like VS-NfD */ return asprintf_s (_("The encryption is %s."), comp_name); } /* Should not happen */ log_err ("Invalid call to compliance string."); STRANGEPOINT; return std::string(); } diff --git a/src/common.h b/src/common.h index fe5d6a2..398f2f1 100644 --- a/src/common.h +++ b/src/common.h @@ -1,146 +1,157 @@ /* common.h - Common declarations for GpgOL * Copyright (C) 2004 Timo Schulz * Copyright (C) 2005, 2006, 2007, 2008 g10 Code GmbH * 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 * 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 GPGOL_COMMON_H #define GPGOL_COMMON_H #ifdef HAVE_CONFIG_H #include #endif #include #include "common_indep.h" #include /* i18n stuff */ #include "w32-gettext.h" #define _(a) gettext (a) #define N_(a) gettext_noop (a) /* Registry path to store plugin settings */ #define GPGOL_REGPATH "Software\\GNU\\GpgOL" #ifdef __cplusplus #include std::string readRegStr (const char *root, const char *dir, const char *name); +namespace GpgME +{ + class Data; +} // namespace GpgME + +/* Read a file into a GpgME::Data object returns 0 on success */ +int readFullFile (HANDLE hFile, GpgME::Data &data); + extern "C" { #if 0 } #endif #endif extern HINSTANCE glob_hinst; extern UINT this_dll; /*-- common.c --*/ void set_global_hinstance (HINSTANCE hinst); char *get_data_dir (void); char *get_gpg4win_dir (void); int store_extension_value (const char *key, const char *val); int store_extension_subkey_value (const char *subkey, const char *key, const char *val); int load_extension_value (const char *key, char **val); /* Get a temporary filename with and its name */ wchar_t *get_tmp_outfile (const wchar_t *name, HANDLE *outHandle); +char *get_tmp_outfile_utf8 (const char *name, HANDLE *outHandle); wchar_t *get_pretty_attachment_name (wchar_t *path, protocol_t protocol, int signature); /*-- verify-dialog.c --*/ int verify_dialog_box (gpgme_protocol_t protocol, gpgme_verify_result_t res, const char *filename); /*-- inspectors.cpp --*/ int initialize_inspectors (void); #if __GNUC__ >= 4 # define GPGOL_GCC_A_SENTINEL(a) __attribute__ ((sentinel(a))) #else # define GPGOL_GCC_A_SENTINEL(a) #endif /*-- common.c --*/ void fatal_error (const char *format, ...); char *read_w32_registry_string (const char *root, const char *dir, const char *name); char *percent_escape (const char *str, const char *extra); void fix_linebreaks (char *str, int *len); /* Format a date from gpgme (seconds since epoch) with windows system locale. */ char *format_date_from_gpgme (unsigned long time); /* Get the name of the uiserver */ char *get_uiserver_name (void); int is_elevated (void); /*-- main.c --*/ void read_options (void); int write_options (void); extern int g_ol_version_major; void bring_to_front (HWND wid); int gpgol_message_box (HWND parent, const char *utf8_text, const char *utf8_caption, UINT type); /* Show a bug message with the code. */ void gpgol_bug (HWND parent, int code); void i18n_init (void); #define ERR_CRYPT_RESOLVER_FAILED 1 #define ERR_WANTS_SEND_MIME_BODY 2 #define ERR_WANTS_SEND_INLINE_BODY 3 #define ERR_INLINE_BODY_TO_BODY 4 #define ERR_INLINE_BODY_INV_STATE 5 #define ERR_SEND_FALLBACK_FAILED 6 #define ERR_GET_BASE_MSG_FAILED 7 #ifdef __cplusplus } /* Check if we are in de_vs mode. */ bool in_de_vs_mode (); /* Get the name of the de_vs compliance mode as configured in libkleopatrarc */ const char *de_vs_name (bool isCompliant = false); /* Get a localized string of the compliance of an operation mode is the usual 1 encrypt, 2 sign. isCompliant if the value of the operation was compliant or not. */ std::string compliance_string (bool forVerify, bool forDecrypt, bool isCompliant); + +HANDLE CreateFileUtf8 (const char *utf8Name); #endif #endif /*GPGOL_COMMON_H*/ diff --git a/src/cryptcontroller.cpp b/src/cryptcontroller.cpp index bc149c9..32e5a34 100644 --- a/src/cryptcontroller.cpp +++ b/src/cryptcontroller.cpp @@ -1,1575 +1,1547 @@ /* @file cryptcontroller.cpp * @brief Helper to do crypto on a mail. * * Copyright (C) 2018 Intevation GmbH * * This file is part of GpgOL. * * GpgOL is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * GpgOL is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, see . */ #include "config.h" #include "common.h" #include "cpphelp.h" #include "cryptcontroller.h" #include "mail.h" #include "mapihelp.h" #include "mimemaker.h" #include "wks-helper.h" #include "overlay.h" #include "keycache.h" #include "mymapitags.h" #include "recipient.h" #include "windowmessages.h" #include #include #include #include "common.h" #include static int sink_data_write (sink_t sink, const void *data, size_t datalen) { GpgME::Data *d = static_cast(sink->cb_data); d->write (data, datalen); return 0; } static int create_sign_attach (sink_t sink, protocol_t protocol, GpgME::Data &signature, GpgME::Data &signedData, const char *micalg); /** We have some C Style cruft in here as this was historically how GpgOL worked directly in the MAPI data objects. To reduce the regression risk the new object oriented way for crypto reused as much as possible from this. */ CryptController::CryptController (Mail *mail, bool encrypt, bool sign, GpgME::Protocol proto): m_mail (mail), m_encrypt (encrypt), m_sign (sign), m_crypto_success (false), m_proto (proto) { TSTART; memdbg_ctor ("CryptController"); log_debug ("%s:%s: CryptController ctor for %p encrypt %i sign %i inline %i.", SRCNAME, __func__, mail, encrypt, sign, mail->getDoPGPInline ()); m_recipients = mail->getCachedRecipients (); m_signer_keys = mail->getSigningKeys (); m_sender = mail->getSender (); TRETURN; } CryptController::~CryptController() { TSTART; memdbg_dtor ("CryptController"); log_debug ("%s:%s:%p", SRCNAME, __func__, m_mail); TRETURN; } int CryptController::collect_data () { TSTART; - /* 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. */ - + int count = m_mail->plainAttachments ().size (); /* Take the Body from the mail if possible. This is a fix for GnuPG-Bug-ID: T3614 because the body is not always properly updated in MAPI when sending. */ char *body = m_mail->takeCachedPlainBody (); if (body && !*body) { xfree (body); body = nullptr; } - - LPMESSAGE message = m_mail->isCryptoMail() ? - get_oom_base_message (m_mail->item ()) : - get_oom_message (m_mail->item ()); - if (!message) - { - log_error ("%s:%s: Failed to get 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) + if (!count && !body) { if (!m_mail->isDraftEncrypt()) { gpgol_message_box (m_mail->getWindow (), utf8_gettext ("Can't encrypt / sign an empty message."), utf8_gettext ("GpgOL"), MB_OK); } - gpgol_release (message); - mapi_release_attach_table (att_table); xfree (body); TRETURN -1; } bool do_inline = m_mail->getDoPGPInline (); - if (n_att_usable && do_inline) + if (count && do_inline) { log_debug ("%s:%s: PGP Inline not supported for attachments." " Using PGP MIME", SRCNAME, __func__); do_inline = false; m_mail->setDoPGPInline (false); } else if (do_inline) { /* Inline. Use Body as input. We need to collect also our mime structure for S/MIME as we don't know yet if we are S/MIME or OpenPGP */ m_bodyInput.write (body, strlen (body)); log_debug ("%s:%s: Inline. Caching body.", SRCNAME, __func__); /* Set the input buffer to start. */ m_bodyInput.seek (0, SEEK_SET); } /* Set up the sink object to collect the mime structure */ struct sink_s sinkmem; sink_t sink = &sinkmem; memset (sink, 0, sizeof *sink); sink->cb_data = &m_input; sink->writefnc = sink_data_write; /* Collect the mime strucutre */ - int err = add_body_and_attachments (sink, message, att_table, m_mail, - body, n_att_usable); + int err = add_body_and_attachments (sink, m_mail, body); xfree (body); if (err) { log_error ("%s:%s: Collecting body and attachments failed.", SRCNAME, __func__); - gpgol_release (message); - mapi_release_attach_table (att_table); TRETURN -1; } - /* Message is no longer needed */ - gpgol_release (message); - - mapi_release_attach_table (att_table); - /* Set the input buffer to start. */ m_input.seek (0, SEEK_SET); TRETURN 0; } int CryptController::lookup_fingerprints (const std::vector &sigFprs, const std::vector > &recpFprs) { TSTART; auto ctx = std::shared_ptr (GpgME::Context::createForProtocol (m_proto)); if (!ctx) { log_error ("%s:%s: failed to create context with protocol '%s'", SRCNAME, __func__, m_proto == GpgME::CMS ? "smime" : m_proto == GpgME::OpenPGP ? "openpgp" : "unknown"); TRETURN -1; } ctx->setKeyListMode (GpgME::Local); GpgME::Error err; if (sigFprs.size()) { char **cSigners = vector_to_cArray (sigFprs); err = ctx->startKeyListing (const_cast (cSigners), true); if (err) { log_error ("%s:%s: failed to start signer keylisting", SRCNAME, __func__); release_cArray (cSigners); TRETURN -1; } do { m_signer_keys.push_back(ctx->nextKey(err)); } while (!err); release_cArray (cSigners); m_signer_keys.pop_back(); if (m_signer_keys.empty()) { log_error ("%s:%s: failed to lookup key for '%s' with protocol '%s'", SRCNAME, __func__, anonstr (sigFprs[0].c_str ()), m_proto == GpgME::CMS ? "smime" : m_proto == GpgME::OpenPGP ? "openpgp" : "unknown"); TRETURN -1; } // reset context ctx = std::shared_ptr (GpgME::Context::createForProtocol (m_proto)); ctx->setKeyListMode (GpgME::Local); } if (!recpFprs.size()) { TRETURN 0; } std::vector all_fingerprints; for (const auto &pair: recpFprs) { all_fingerprints.push_back (pair.second); } for (auto &recp: m_recipients) { std::vector fingerprintsToLookup; /* Find the matching fingerprints for the recpient. */ for (const auto &pair: recpFprs) { if (pair.first.empty()) { log_error ("%s:%s Recipient mbox is empty. Wrong Resolver Version?", SRCNAME, __func__); } /* We also accept the empty string here for backwards compatibility but we should still log an error to be clear. */ if (pair.first.empty() || pair.first == recp.mbox ()) { if (pair.second.empty ()) { log_err ("Would have added an empty string!"); continue; } fingerprintsToLookup.push_back (pair.second); all_fingerprints.erase(std::remove_if(all_fingerprints.begin(), all_fingerprints.end(), [&pair](const std::string &x){return x == pair.second;}), all_fingerprints.end()); } } if (!fingerprintsToLookup.size ()) { log_dbg ("No key selected for '%s'", anonstr (recp.mbox().c_str ())); continue; } // Convert recipient fingerprints char **cRecps = vector_to_cArray (fingerprintsToLookup); err = ctx->startKeyListing (const_cast (cRecps)); if (err) { log_error ("%s:%s: failed to start recipient keylisting", SRCNAME, __func__); release_cArray (cRecps); TRETURN -1; } std::vector keys; do { const auto key = ctx->nextKey (err); if (key.isNull () || err) { continue; } keys.push_back (key); log_dbg ("Adding '%s' as key for '%s", anonstr (key.primaryFingerprint ()), anonstr (recp.mbox ().c_str ())); } while (!err); keys.pop_back (); release_cArray (cRecps); recp.setKeys (keys); // reset context ctx = std::shared_ptr (GpgME::Context::createForProtocol (m_proto)); ctx->setKeyListMode (GpgME::Local); } if (!all_fingerprints.empty()) { log_error ("%s:%s: BUG: Not all fingerprints could be matched " "to a recipient.", SRCNAME, __func__); for (const auto &fpr: all_fingerprints) { log_debug ("%s:%s Failed to find: %s", SRCNAME, __func__, anonstr (fpr.c_str ())); } clear_keys (); TRETURN -1; } resolving_done (); TRETURN 0; } int CryptController::parse_output (GpgME::Data &resolverOutput) { TSTART; std::istringstream ss(resolverOutput.toString()); std::string line; std::vector sigFprs; std::vector> recpFprs; while (std::getline (ss, line)) { rtrim (line); if (line == "cancel") { log_debug ("%s:%s: resolver canceled", SRCNAME, __func__); TRETURN -2; } if (line == "unencrypted") { log_debug ("%s:%s: FIXME resolver wants unencrypted", SRCNAME, __func__); TRETURN -1; } std::istringstream lss (line); // First is sig or enc std::string what; std::string how; std::string fingerprint; std::string mbox; std::getline (lss, what, ':'); std::getline (lss, how, ':'); std::getline (lss, fingerprint, ':'); std::getline (lss, mbox, ':'); if (m_proto == GpgME::UnknownProtocol) { /* TODO: Allow mixed */ m_proto = (how == "smime") ? GpgME::CMS : GpgME::OpenPGP; } if (what == "sig") { sigFprs.push_back (fingerprint); continue; } if (what == "enc") { recpFprs.push_back(std::make_pair(mbox, fingerprint)); } } if (m_sign && sigFprs.empty()) { log_error ("%s:%s: Sign requested but no signing fingerprint - sending unsigned", SRCNAME, __func__); m_sign = false; } if (m_encrypt && !recpFprs.size()) { log_error ("%s:%s: Encrypt requested but no recipient fingerprints", SRCNAME, __func__); gpgol_message_box (m_mail->getWindow (), utf8_gettext ("No recipients for encryption selected."), _("GpgOL"), MB_OK); TRETURN -2; } TRETURN lookup_fingerprints (sigFprs, recpFprs); } /* Combines all recipient keys for this operation into a list and copys the resolved recipients back. */ void CryptController::resolving_done () { TSTART; m_enc_keys.clear (); for (const auto &recp: m_recipients) { const auto &keys = recp.keys (); m_enc_keys.insert (m_enc_keys.end (), keys.begin (), keys.end ()); } /* Copy the resolved recipients back to the mail */ m_mail->setRecipients (m_recipients); m_mail->setSigningKeys (m_signer_keys); TRETURN; } bool CryptController::resolve_through_protocol (GpgME::Protocol proto) { TSTART; const auto cache = KeyCache::instance (); if (m_encrypt) { for (auto &recp: m_recipients) { recp.setKeys(cache->getEncryptionKeys(recp.mbox (), proto)); } } if (m_sign) { if (m_sender.empty()) { log_error ("%s:%s: Asked to sign but sender is empty.", SRCNAME, __func__); return false; } const auto key = cache->getSigningKey (m_sender.c_str (), proto); if (m_signer_keys.empty ()) { m_signer_keys.push_back (key); } for (const auto &k: m_signer_keys) { if (k.protocol () != proto) { m_signer_keys.push_back (key); break; } } } TRETURN is_resolved (); } /* Check if we can be resolved by a single protocol and return it. */ GpgME::Protocol CryptController::get_resolved_protocol () const { TSTART; GpgME::Protocol ret = GpgME::UnknownProtocol; bool hasOpenPGPSignKey = false; bool hasSMIMESignKey = false; if (m_sign) { for (const auto &sig_key: m_signer_keys) { hasOpenPGPSignKey |= (!sig_key.isNull() && sig_key.protocol () == GpgME::OpenPGP); hasSMIMESignKey |= (!sig_key.isNull() && sig_key.protocol () == GpgME::CMS); } } if (m_encrypt) { for (const auto &recp: m_recipients) { if (!recp.keys ().size () || recp.keys ()[0].isNull ()) { /* No keys or the first key in the list is null. */ TRETURN GpgME::UnknownProtocol; } for (const auto &key: recp.keys()) { if (key.protocol () == GpgME::OpenPGP && (!m_sign || hasOpenPGPSignKey) && (ret == GpgME::UnknownProtocol || ret == GpgME::OpenPGP)) { ret = GpgME::OpenPGP; continue; } if (key.protocol () == GpgME::CMS && (!m_sign || hasSMIMESignKey) && (ret == GpgME::UnknownProtocol || ret == GpgME::CMS)) { ret = GpgME::CMS; continue; } // Unresolvable TRETURN GpgME::UnknownProtocol; } } } TRETURN ret; } /* Check that the crypt operation is resolved. This supports combined S/MIME and OpenPGP operations. */ bool CryptController::is_resolved () const { /* Check that we have signing keys if necessary and at least one encryption key for each recipient. */ bool hasOpenPGPSignKey = false; bool hasSMIMESignKey = false; if (m_sign) { for (const auto &sig_key: m_signer_keys) { hasOpenPGPSignKey |= (!sig_key.isNull() && sig_key.protocol () == GpgME::OpenPGP); hasSMIMESignKey |= (!sig_key.isNull() && sig_key.protocol () == GpgME::CMS); } } log_dbg ("Has OpenPGP Sig Key: %i SMIME: %i", hasOpenPGPSignKey, hasSMIMESignKey); if (m_encrypt) { Recipient::dump (m_recipients); for (const auto &recp: m_recipients) { if (!recp.keys ().size () || recp.keys ()[0].isNull ()) { /* No keys or the first key in the list is null. */ return false; } /* If we don't sign we need no more checks. */ if (!m_sign) { continue; } for (const auto &key: recp.keys()) { if (key.protocol () == GpgME::OpenPGP && !hasOpenPGPSignKey) { return false; } if (key.protocol () == GpgME::CMS && !hasSMIMESignKey) { return false; } } } } return true; } int CryptController::resolve_keys_cached() { TSTART; // Prepare variables const auto recps = m_mail->getCachedRecipientAddresses (); bool resolved = false; if (opt.enable_smime && opt.prefer_smime) { resolved = resolve_through_protocol (GpgME::CMS); if (resolved) { log_debug ("%s:%s: Resolved with CMS due to preference.", SRCNAME, __func__); m_proto = GpgME::CMS; } } if (!resolved) { resolved = resolve_through_protocol (GpgME::OpenPGP); if (resolved) { log_debug ("%s:%s: Resolved with OpenPGP.", SRCNAME, __func__); m_proto = GpgME::OpenPGP; } } if (!resolved && (opt.enable_smime && !opt.prefer_smime)) { resolved = resolve_through_protocol (GpgME::CMS); if (resolved) { log_debug ("%s:%s: Resolved with CMS as fallback.", SRCNAME, __func__); m_proto = GpgME::CMS; } } for (const auto &recp: m_recipients) { log_debug ("Enc Key for: '%s' Type: %i", anonstr (recp.mbox ().c_str ()), recp.type ()); for (const auto &key: recp.keys ()) { log_debug ("%s:%s", to_cstr (key.protocol ()), anonstr (key.primaryFingerprint ())); } } for (const auto &sig_key: m_signer_keys) { log_debug ("%s:%s: Signing key: %s:%s", SRCNAME, __func__, to_cstr (sig_key.protocol()), anonstr (sig_key.primaryFingerprint ())); } if (!resolved) { log_debug ("%s:%s: Failed to resolve through cache", SRCNAME, __func__); m_enc_keys.clear (); m_signer_keys.clear (); m_proto = GpgME::UnknownProtocol; TRETURN 1; } if (m_encrypt) { log_debug ("%s:%s: Encrypting with protocol %s to:", SRCNAME, __func__, to_cstr (m_proto)); } TRETURN 0; } void CryptController::clear_keys () { for (auto &recp: m_recipients) { recp.setKeys (std::vector ()); } m_signer_keys.clear (); } int CryptController::resolve_keys () { TSTART; m_proto = get_resolved_protocol (); if (m_proto != GpgME::UnknownProtocol) { log_debug ("%s:%s: Already resolved by %s Not resolving again.", SRCNAME, __func__, to_cstr (m_proto)); start_crypto_overlay(); resolving_done (); TRETURN 0; } m_enc_keys.clear(); if (m_mail->isDraftEncrypt() && opt.draft_key) { const auto key = KeyCache::instance()->getByFpr (opt.draft_key); if (key.isNull()) { const char *buf = utf8_gettext ("Failed to encrypt draft.\n\n" "The configured encryption key for drafts " "could not be found.\n" "Please check your configuration or " "turn off draft encryption in the settings."); gpgol_message_box (get_active_hwnd (), buf, _("GpgOL"), MB_OK); TRETURN -1; } log_debug ("%s:%s: resolved draft encryption key protocol is: %s", SRCNAME, __func__, to_cstr (key.protocol())); m_proto = key.protocol (); m_enc_keys.push_back (key); TRETURN 0; } if (!m_recipients.size()) { /* Should not happen. But we add it for better bug reports. */ const char *bugmsg = utf8_gettext ("Operation failed.\n\n" "This is usually caused by a bug in GpgOL or an error in your setup.\n" "Please see https://www.gpg4win.org/reporting-bugs.html " "or ask your Administrator for support."); char *buf; gpgrt_asprintf (&buf, "Failed to resolve recipients.\n\n%s\n", bugmsg); memdbg_alloc (buf); gpgol_message_box (get_active_hwnd (), buf, _("GpgOL"), MB_OK); xfree(buf); TRETURN -1; } if (opt.autoresolve && !opt.alwaysShowApproval && !resolve_keys_cached ()) { log_debug ("%s:%s: resolved keys through the cache", SRCNAME, __func__); start_crypto_overlay(); resolving_done (); TRETURN 0; } std::vector args; // Collect the arguments char *gpg4win_dir = get_gpg4win_dir (); if (!gpg4win_dir) { TRACEPOINT; TRETURN -1; } const auto resolver = std::string (gpg4win_dir) + "\\bin\\resolver.exe"; args.push_back (resolver); log_debug ("%s:%s: resolving keys with '%s'", SRCNAME, __func__, resolver.c_str ()); // We want debug output as OutputDebugString args.push_back (std::string ("--debug")); // Yes passing it as int is ok. auto wnd = m_mail->getWindow (); if (wnd) { // Pass the handle of the active window for raise / overlay. args.push_back (std::string ("--hwnd")); args.push_back (std::to_string ((int) (intptr_t) wnd)); } // Set the overlay caption args.push_back (std::string ("--overlayText")); if (m_encrypt) { args.push_back (std::string (utf8_gettext ("Resolving recipients..."))); } else if (m_sign) { args.push_back (std::string (utf8_gettext ("Resolving signers..."))); } if (!opt.enable_smime) { args.push_back (std::string ("--protocol")); args.push_back (std::string ("pgp")); } if (m_sign) { args.push_back (std::string ("--sign")); } const auto cached_sender = m_mail->getSender (); if (cached_sender.empty()) { log_error ("%s:%s: resolve keys without sender.", SRCNAME, __func__); } else { args.push_back (std::string ("--sender")); args.push_back (cached_sender); } if (!opt.autoresolve || opt.alwaysShowApproval) { args.push_back (std::string ("--alwaysShow")); } if (opt.prefer_smime) { args.push_back (std::string ("--preferred-protocol")); args.push_back (std::string ("cms")); } args.push_back (std::string ("--lang")); args.push_back (std::string (gettext_localename ())); bool has_smime_override = false; if (m_encrypt) { args.push_back (std::string ("--encrypt")); // Get the recipients that are cached from OOM for (const auto &recp: m_recipients) { const auto mbox = recp.mbox (); auto overrides = KeyCache::instance ()->getOverrides (mbox, GpgME::OpenPGP); const auto cms_overrides = KeyCache::instance ()->getOverrides (mbox, GpgME::CMS); overrides.insert(overrides.end(), cms_overrides.begin(), cms_overrides.end()); if (overrides.size()) { std::string overrideStr = mbox + ":"; for (const auto &key: overrides) { if (key.isNull()) { TRACEPOINT; continue; } has_smime_override |= key.protocol() == GpgME::CMS; overrideStr += key.primaryFingerprint(); overrideStr += ","; } overrideStr.erase(overrideStr.size() - 1, 1); args.push_back (std::string ("-o")); args.push_back (overrideStr); } args.push_back (mbox); } } if (!opt.prefer_smime && has_smime_override) { /* Prefer S/MIME if there was an S/MIME override */ args.push_back (std::string ("--preferred-protocol")); args.push_back (std::string ("cms")); } // Args are prepared. Spawn the resolver. auto ctx = GpgME::Context::createForEngine (GpgME::SpawnEngine); if (!ctx) { // can't happen TRACEPOINT; TRETURN -1; } // Convert our collected vector to c strings // It's a bit overhead but should be quick for such small // data. char **cargs = vector_to_cArray (args); log_data ("%s:%s: Spawn args:", SRCNAME, __func__); for (size_t i = 0; cargs && cargs[i]; i++) { log_data (SIZE_T_FORMAT ": '%s'", i, cargs[i]); } GpgME::Data mystdin (GpgME::Data::null), mystdout, mystderr; GpgME::Error err = ctx->spawn (cargs[0], const_cast (cargs), mystdin, mystdout, mystderr, (GpgME::Context::SpawnFlags) ( GpgME::Context::SpawnAllowSetFg | GpgME::Context::SpawnShowWindow)); // Somehow Qt messes up which window to bring back to front. // So we do it manually. bring_to_front (wnd); // We need to create an overlay while encrypting as pinentry can take a while start_crypto_overlay(); log_data ("Resolver stdout:\n'%s'", mystdout.toString ().c_str ()); log_data ("Resolver stderr:\n'%s'", mystderr.toString ().c_str ()); release_cArray (cargs); if (err) { log_debug ("%s:%s: Resolver spawn finished Err code: %i asString: %s", SRCNAME, __func__, err.code(), err.asString()); } int ret = parse_output (mystdout); if (ret == -1) { log_debug ("%s:%s: Failed to parse / resolve keys.", SRCNAME, __func__); log_data ("Resolver stdout:\n'%s'", mystdout.toString ().c_str ()); log_data ("Resolver stderr:\n'%s'", mystderr.toString ().c_str ()); TRETURN -1; } TRETURN ret; } int CryptController::do_crypto (GpgME::Error &err, std::string &r_diag) { TSTART; log_debug ("%s:%s", SRCNAME, __func__); if (m_mail->isDraftEncrypt ()) { log_debug ("%s:%s Disabling sign because of draft encrypt", SRCNAME, __func__); m_sign = false; } /* Start a WKS check if necessary. */ WKSHelper::instance()->start_check (m_mail->getSender ()); int ret = resolve_keys (); if (ret == -1) { //error log_debug ("%s:%s: Failure to resolve keys.", SRCNAME, __func__); TRETURN -1; } if (ret == -2) { // Cancel TRETURN -2; } /* If we need to send multiple emails we jump back from here into the main event loop. Copy the mail object and send it out mutiple times. */ if (opt.splitBCCMails) { bool foundOneNormalRecp = false; bool foundOneBCCRecp = false; for (const auto &recp: m_recipients) { if (foundOneBCCRecp && foundOneNormalRecp) { log_debug ("%s:%s: Have both BCC and normal recipients." " Need to send multiple mails.", SRCNAME, __func__); do_in_ui_thread_async (SEND_MULTIPLE_MAILS, m_mail); /* Cancel the crypto of this mail */ TRETURN -3; } if (recp.type() == Recipient::olCC || recp.type() == Recipient::olTo) { foundOneNormalRecp = true; } if (recp.type() == Recipient::olBCC) { foundOneBCCRecp = true; } } } bool do_inline = m_mail->getDoPGPInline (); if (m_proto == GpgME::CMS && do_inline) { log_debug ("%s:%s: Inline for S/MIME not supported. Switching to mime.", SRCNAME, __func__); do_inline = false; m_mail->setDoPGPInline (false); m_bodyInput = GpgME::Data(GpgME::Data::null); } auto ctx = GpgME::Context::create(m_proto); if (!ctx) { log_error ("%s:%s: Failure to create context.", SRCNAME, __func__); gpgol_message_box (m_mail->getWindow (), "Failure to create context.", utf8_gettext ("GpgOL"), MB_OK); TRETURN -1; } if (!m_signer_keys.empty ()) { for (const auto &key: m_signer_keys) { if (key.protocol () == m_proto) { ctx->addSigningKey (key); } } } ctx->setTextMode (m_proto == GpgME::OpenPGP); ctx->setArmor (m_proto == GpgME::OpenPGP); if (m_encrypt && m_sign && do_inline) { // Sign encrypt combined const auto result_pair = ctx->signAndEncrypt (m_enc_keys, do_inline ? m_bodyInput : m_input, m_output, GpgME::Context::AlwaysTrust); const auto err1 = result_pair.first.error(); const auto err2 = result_pair.second.error(); if (err1 || err2) { log_error ("%s:%s: Encrypt / Sign error %s %s.", SRCNAME, __func__, result_pair.first.error().asString(), result_pair.second.error().asString()); err = err1 ? err1 : err2; GpgME::Data log; const auto err3 = ctx->getAuditLog (log, GpgME::Context::DiagnosticAuditLog); if (!err3) { r_diag = log.toString(); } TRETURN -1; } if (err1.isCanceled() || err2.isCanceled()) { err = err1.isCanceled() ? err1 : err2; log_debug ("%s:%s: User cancled", SRCNAME, __func__); TRETURN -2; } } else if (m_encrypt && m_sign) { // First sign then encrypt const auto sigResult = ctx->sign (m_input, m_output, GpgME::Detached); err = sigResult.error(); if (err) { log_error ("%s:%s: Signing error %s.", SRCNAME, __func__, sigResult.error().asString()); GpgME::Data log; const auto err3 = ctx->getAuditLog (log, GpgME::Context::DiagnosticAuditLog); if (!err3) { r_diag = log.toString(); } TRETURN -1; } if (err.isCanceled()) { log_debug ("%s:%s: User cancled", SRCNAME, __func__); TRETURN -2; } parse_micalg (sigResult); // We now have plaintext in m_input // The detached signature in m_output // Set up the sink object to construct the multipart/signed GpgME::Data multipart; struct sink_s sinkmem; sink_t sink = &sinkmem; memset (sink, 0, sizeof *sink); sink->cb_data = &multipart; sink->writefnc = sink_data_write; if (create_sign_attach (sink, m_proto == GpgME::CMS ? PROTOCOL_SMIME : PROTOCOL_OPENPGP, m_output, m_input, m_micalg.c_str ())) { TRACEPOINT; TRETURN -1; } // Now we have the multipart throw away the rest. m_output = GpgME::Data (); m_input = GpgME::Data (); multipart.seek (0, SEEK_SET); const auto encResult = ctx->encrypt (m_enc_keys, multipart, m_output, GpgME::Context::AlwaysTrust); err = encResult.error(); if (err) { log_error ("%s:%s: Encryption error %s.", SRCNAME, __func__, err.asString()); GpgME::Data log; const auto err3 = ctx->getAuditLog (log, GpgME::Context::DiagnosticAuditLog); if (!err3) { r_diag = log.toString(); } TRETURN -1; } if (err.isCanceled()) { log_debug ("%s:%s: User cancled", SRCNAME, __func__); TRETURN -2; } // Now we have encrypted output just treat it like encrypted. } else if (m_encrypt) { const auto result = ctx->encrypt (m_enc_keys, do_inline ? m_bodyInput : m_input, m_output, GpgME::Context::AlwaysTrust); err = result.error(); if (err) { log_error ("%s:%s: Encryption error %s.", SRCNAME, __func__, err.asString()); GpgME::Data log; const auto err3 = ctx->getAuditLog (log, GpgME::Context::DiagnosticAuditLog); if (!err3) { r_diag = log.toString(); } TRETURN -1; } if (err.isCanceled()) { log_debug ("%s:%s: User cancled", SRCNAME, __func__); TRETURN -2; } } else if (m_sign) { const auto result = ctx->sign (do_inline ? m_bodyInput : m_input, m_output, do_inline ? GpgME::Clearsigned : GpgME::Detached); err = result.error(); if (err) { log_error ("%s:%s: Signing error %s.", SRCNAME, __func__, err.asString()); GpgME::Data log; const auto err3 = ctx->getAuditLog (log, GpgME::Context::DiagnosticAuditLog); if (!err3) { r_diag = log.toString(); } TRETURN -1; } if (err.isCanceled()) { log_debug ("%s:%s: User cancled", SRCNAME, __func__); TRETURN -2; } parse_micalg (result); } else { // ??? log_error ("%s:%s: unreachable code reached.", SRCNAME, __func__); } log_debug ("%s:%s: Crypto done sucessfuly.", SRCNAME, __func__); m_crypto_success = true; TRETURN 0; } static int write_data (sink_t sink, GpgME::Data &data) { TSTART; if (!sink || !sink->writefnc) { TRETURN -1; } char buf[4096]; size_t nread; data.seek (0, SEEK_SET); while ((nread = data.read (buf, 4096)) > 0) { sink->writefnc (sink, buf, nread); } TRETURN 0; } int create_sign_attach (sink_t sink, protocol_t protocol, GpgME::Data &signature, GpgME::Data &signedData, const char *micalg) { TSTART; char boundary[BOUNDARYSIZE+1]; char top_header[BOUNDARYSIZE+200]; int rc = 0; /* Write the top header. */ generate_boundary (boundary); create_top_signing_header (top_header, sizeof top_header, protocol, 1, boundary, micalg); if ((rc = write_string (sink, top_header))) { TRACEPOINT; TRETURN rc; } /* Write the boundary so that it is not included in the hashing. */ if ((rc = write_boundary (sink, boundary, 0))) { TRACEPOINT; TRETURN rc; } /* Write the signed mime structure */ if ((rc = write_data (sink, signedData))) { TRACEPOINT; TRETURN rc; } /* Write the signature attachment */ if ((rc = write_boundary (sink, boundary, 0))) { TRACEPOINT; TRETURN rc; } if (protocol == PROTOCOL_OPENPGP) { rc = write_string (sink, "Content-Type: application/pgp-signature;\r\n" "\tname=\"" OPENPGP_SIG_NAME "\"\r\n" "Content-Transfer-Encoding: 7Bit\r\n"); } else { rc = write_string (sink, "Content-Transfer-Encoding: base64\r\n" "Content-Type: application/pkcs7-signature\r\n" "Content-Disposition: inline;\r\n" "\tfilename=\"" SMIME_SIG_NAME "\"\r\n"); /* rc = write_string (sink, */ /* "Content-Type: application/x-pkcs7-signature\r\n" */ /* "\tname=\"smime.p7s\"\r\n" */ /* "Content-Transfer-Encoding: base64\r\n" */ /* "Content-Disposition: attachment;\r\n" */ /* "\tfilename=\"smime.p7s\"\r\n"); */ } if (rc) { TRACEPOINT; TRETURN rc; } if ((rc = write_string (sink, "\r\n"))) { TRACEPOINT; TRETURN rc; } // Write the signature data if (protocol == PROTOCOL_SMIME) { const std::string sigStr = signature.toString(); if ((rc = write_b64 (sink, (const void *) sigStr.c_str (), sigStr.size()))) { TRACEPOINT; TRETURN rc; } } else if ((rc = write_data (sink, signature))) { TRACEPOINT; TRETURN rc; } // Add an extra linefeed with should not harm. if ((rc = write_string (sink, "\r\n"))) { TRACEPOINT; TRETURN rc; } /* Write the final boundary. */ if ((rc = write_boundary (sink, boundary, 1))) { TRACEPOINT; TRETURN rc; } TRETURN rc; } static int create_encrypt_attach (sink_t sink, protocol_t protocol, GpgME::Data &encryptedData, int exchange_major_version) { TSTART; char boundary[BOUNDARYSIZE+1]; int rc = create_top_encryption_header (sink, protocol, boundary, false, exchange_major_version); // From here on use goto failure pattern. if (rc) { log_error ("%s:%s: Failed to create top header.", SRCNAME, __func__); TRETURN rc; } if (protocol == PROTOCOL_OPENPGP || exchange_major_version >= 15) { // With exchange 2016 we have to construct S/MIME // differently and write the raw data here. rc = write_data (sink, encryptedData); } else { const auto encStr = encryptedData.toString(); rc = write_b64 (sink, encStr.c_str(), encStr.size()); } if (rc) { log_error ("%s:%s: Failed to create top header.", SRCNAME, __func__); TRETURN rc; } /* Write the final boundary (for OpenPGP) and finish the attachment. */ if (*boundary && (rc = write_boundary (sink, boundary, 1))) { log_error ("%s:%s: Failed to write boundary.", SRCNAME, __func__); } TRETURN rc; } int CryptController::update_mail_mapi () { TSTART; log_debug ("%s:%s", SRCNAME, __func__); LPMESSAGE message = get_oom_base_message (m_mail->item()); if (!message) { log_error ("%s:%s: Failed to obtain message.", SRCNAME, __func__); TRETURN -1; } if (m_mail->getDoPGPInline ()) { // Nothing to do for inline. log_debug ("%s:%s: Inline mail. Setting encoding.", SRCNAME, __func__); SPropValue prop; prop.ulPropTag = PR_INTERNET_CPID; prop.Value.l = 65001; if (HrSetOneProp (message, &prop)) { log_error ("%s:%s: Failed to set CPID mapiprop.", SRCNAME, __func__); } TRETURN 0; } mapi_attach_item_t *att_table = mapi_create_attach_table (message, 0); /* When we forward e.g. a crypto mail we have sent the message has a MOSSTEMPL. We need to remove that. T4321 */ for (ULONG pos=0; att_table && !att_table[pos].end_of_table; pos++) { if (att_table[pos].attach_type == ATTACHTYPE_MOSSTEMPL) { log_debug ("%s:%s: Found existing moss attachment at " "pos %i removing it.", SRCNAME, __func__, att_table[pos].mapipos); if (message->DeleteAttach (att_table[pos].mapipos, 0, nullptr, 0) != S_OK) { log_error ("%s:%s: Failed to remove attachment.", SRCNAME, __func__); } } } // Set up the sink object for our MSOXSMIME attachment. struct sink_s sinkmem; sink_t sink = &sinkmem; memset (sink, 0, sizeof *sink); sink->cb_data = &m_input; sink->writefnc = sink_data_write; // For S/MIME encrypted mails we have to use the application/pkcs7-mime // content type. Otherwise newer (2016) exchange servers will throw // an M2MCVT.StorageError.Exeption (See GnuPG-Bug-Id: T3853 ) // This means that the conversion / build of the mime structure also // happens differently. int exchange_major_version = get_ex_major_version_for_addr ( m_mail->getSender ().c_str ()); std::string overrideMimeTag; if (m_proto == GpgME::CMS && m_encrypt && exchange_major_version >= 15) { log_debug ("%s:%s: CMS Encrypt with Exchange %i activating alternative.", SRCNAME, __func__, exchange_major_version); overrideMimeTag = "application/pkcs7-mime"; } LPATTACH attach = create_mapi_attachment (message, sink, overrideMimeTag.empty() ? nullptr : overrideMimeTag.c_str()); if (!attach) { log_error ("%s:%s: Failed to create moss attach.", SRCNAME, __func__); gpgol_release (message); TRETURN -1; } protocol_t protocol = m_proto == GpgME::CMS ? PROTOCOL_SMIME : PROTOCOL_OPENPGP; int rc = 0; /* Do we have override MIME ? */ const auto overrideMime = m_mail->get_override_mime_data (); if (!overrideMime.empty()) { rc = write_string (sink, overrideMime.c_str ()); } else if (m_sign && m_encrypt) { rc = create_encrypt_attach (sink, protocol, m_output, exchange_major_version); } else if (m_encrypt) { rc = create_encrypt_attach (sink, protocol, m_output, exchange_major_version); } else if (m_sign) { rc = create_sign_attach (sink, protocol, m_output, m_input, m_micalg.c_str ()); } // Close our attachment if (!rc) { rc = close_mapi_attachment (&attach, sink); } // Set message class etc. if (!rc) { rc = finalize_message (message, att_table, protocol, m_encrypt ? 1 : 0, false, m_mail->isDraftEncrypt (), exchange_major_version); } // only on error. if (rc) { cancel_mapi_attachment (&attach, sink); } // cleanup mapi_release_attach_table (att_table); gpgol_release (attach); gpgol_release (message); TRETURN rc; } std::string CryptController::get_inline_data () { TSTART; std::string ret; if (!m_mail->getDoPGPInline ()) { TRETURN ret; } m_output.seek (0, SEEK_SET); char buf[4096]; size_t nread; while ((nread = m_output.read (buf, 4096)) > 0) { ret += std::string (buf, nread); } TRETURN ret; } void CryptController::parse_micalg (const GpgME::SigningResult &result) { TSTART; if (result.isNull()) { TRACEPOINT; TRETURN; } const auto signature = result.createdSignature(0); if (signature.isNull()) { TRACEPOINT; TRETURN; } const char *hashAlg = signature.hashAlgorithmAsString (); if (!hashAlg) { TRACEPOINT; TRETURN; } if (m_proto == GpgME::OpenPGP) { m_micalg = std::string("pgp-") + hashAlg; } else { m_micalg = hashAlg; } std::transform(m_micalg.begin(), m_micalg.end(), m_micalg.begin(), ::tolower); log_debug ("%s:%s: micalg is: '%s'.", SRCNAME, __func__, m_micalg.c_str ()); TRETURN; } void CryptController::start_crypto_overlay () { TSTART; auto wid = m_mail->getWindow (); std::string text; if (m_encrypt) { text = utf8_gettext ("Encrypting..."); } else if (m_sign) { text = utf8_gettext ("Signing..."); } m_overlay = std::unique_ptr (new Overlay (wid, text)); TRETURN; } diff --git a/src/mail.cpp b/src/mail.cpp index d6dbaf6..6c46ace 100644 --- a/src/mail.cpp +++ b/src/mail.cpp @@ -1,5167 +1,5201 @@ /* @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 - * Copyright (C) 2019 g10code GmbH + * Copyright (C) 2019, 2020 g10code GmbH * * This file is part of GpgOL. * * GpgOL is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * GpgOL is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, see . */ #include "config.h" #include "categorymanager.h" #include "dialogs.h" #include "common.h" #include "mail.h" #include "eventsinks.h" #include "attachment.h" #include "mapihelp.h" #include "mimemaker.h" #include "revert.h" #include "gpgoladdin.h" #include "mymapitags.h" #include "parsecontroller.h" #include "cryptcontroller.h" #include "windowmessages.h" #include "mlang-charset.h" #include "wks-helper.h" #include "keycache.h" #include "cpphelp.h" #include "addressbook.h" #include "recipient.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #undef _ #define _(a) utf8_gettext (a) using namespace GpgME; static std::map s_mail_map; static std::map s_uid_map; static std::map s_folder_events_map; static std::set uids_searched; static std::set s_entry_ids_printing; GPGRT_LOCK_DEFINE (mail_map_lock); GPGRT_LOCK_DEFINE (uid_map_lock); static Mail *s_last_mail; Mail *g_mail_copy_triggerer = nullptr; #define COLOR_DARK_GREY "#f0f0f0" #define COLOR_LIGHT_GREY "#f8f8f8" const char *HTML_PREVIEW_PLACEHOLDER = "" "" "" "" "" "" "" "" "
" "

%s %s

" "

%s

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

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

\n")); } else if (m_type == MSGTYPE_GPGOL_MULTIPART_SIGNED && !m_orig_body.empty ()) { log_dbg ("Multipart signed inserting body in placeholder."); gpgrt_asprintf (&placeholder_buf, opt.prefer_html ? HTML_PREVIEW_PLACEHOLDER : TEXT_PREVIEW_PLACEHOLDER, isSMIME_m () ? "S/MIME" : "OpenPGP", _("message"), _("Please wait while the message is being verified..."), m_orig_body.c_str ()); } else { gpgrt_asprintf (&placeholder_buf, opt.prefer_html ? decrypt_template_html : decrypt_template, isSMIME_m () ? "S/MIME" : "OpenPGP", _("message"), _("Please wait while the message is being decrypted / verified...")); } if (opt.prefer_html) { put_oom_int (m_mailitem, "BodyFormat", 2); if (put_oom_string (m_mailitem, "HTMLBody", placeholder_buf)) { log_error ("%s:%s: Failed to modify html body of item.", SRCNAME, __func__); } } else { char *tmp = get_oom_string (m_mailitem, "Body"); if (!tmp) { TRACEPOINT; TRETURN 1; } m_orig_body = tmp; xfree (tmp); put_oom_int (m_mailitem, "BodyFormat", 1); if (put_oom_string (m_mailitem, "Body", placeholder_buf)) { log_error ("%s:%s: Failed to modify body of item.", SRCNAME, __func__); } } memdbg_alloc (placeholder_buf); xfree (placeholder_buf); if (m_type == MSGTYPE_GPGOL_WKS_CONFIRMATION) { if (m_header_info.boundary.empty()) { parseHeaders_m (); } WKSHelper::instance ()->handle_confirmation_read (this, cipherstream); TRETURN 0; } m_parser = std::shared_ptr (new ParseController (cipherstream, m_type)); m_parser->setSender(GpgME::UserID::addrSpecFromString(getSender_o ().c_str())); if (opt.autoimport) { /* Handle autocrypt header. As we want to have the import of the header in the same thread as the parser we leave it to the parser. */ if (m_header_info.boundary.empty()) { parseHeaders_m (); } if (m_header_info.acInfo.exists) { m_parser->setAutocryptInfo (m_header_info.acInfo); } } log_data ("%s:%s: Parser for \"%s\" is %p", SRCNAME, __func__, getSubject_o ().c_str(), m_parser.get()); gpgol_release (cipherstream); /* Printing happens in two steps. First a Mail is loaded after the BeforePrint event, then it is loaded a second time when the actual print happens. We have to catch both. */ if (!m_printing) { m_printing = checkIfMailMightBePrinting_o (); } else { /* Register us for printing. */ s_entry_ids_printing.insert (get_oom_string_s (m_mailitem, "EntryID")); } if (!opt.sync_dec && !m_printing) { HANDLE parser_thread = CreateThread (NULL, 0, do_parsing, (LPVOID) this, 0, NULL); if (!parser_thread) { log_error ("%s:%s: Failed to create decrypt / verify thread.", SRCNAME, __func__); } CloseHandle (parser_thread); TRETURN 0; } else { /* Parse synchronously */ do_parsing ((LPVOID) this); parsingDone_o (); TRETURN 0; } } int Mail::parseHeaders_m () { /* Parse the headers first so that the handler for confirmation read can access them. Should be put in its own function. */ auto message = MAKE_SHARED (get_oom_message (m_mailitem)); if (!message) { /* Hmmm */ STRANGEPOINT; TRETURN -1; } if (!mapi_get_header_info ((LPMESSAGE)message.get(), m_header_info)) { STRANGEPOINT; TRETURN -1; } TRETURN 0; } static void set_body (LPDISPATCH item, const std::string &plain, const std::string &html) { if (opt.prefer_html && !html.empty()) { if (put_oom_string (item, "HTMLBody", html.c_str ())) { log_error ("%s:%s: Failed to modify html body of item.", SRCNAME, __func__); if (!plain.empty ()) { put_oom_int (item, "BodyFormat", 1); if (put_oom_string (item, "Body", plain.c_str ())) { log_error ("%s:%s: Failed to put plaintext into body of item.", SRCNAME, __func__); } } else { put_oom_int (item, "BodyFormat", 2); if (put_oom_string (item, "HTMLBody", plain.c_str ())) { log_error ("%s:%s: Failed to put plaintext into html of item.", SRCNAME, __func__); } } } else { put_oom_int (item, "BodyFormat", 2); } } else if (!plain.empty ()) { put_oom_int (item, "BodyFormat", 1); if (put_oom_string (item, "Body", plain.c_str ())) { log_error ("%s:%s: Failed to put plaintext into body of item.", SRCNAME, __func__); } } } void Mail::updateBody_o (bool is_preview) { TSTART; if (!m_parser) { TRACEPOINT; TRETURN; } const auto error = m_parser->get_formatted_error (); if (!error.empty()) { set_body (m_mailitem, error, error); TRETURN; } if (m_verify_result.error() && !m_orig_body.empty ()) { log_error ("%s:%s: Verification failed. Restoring Body.", SRCNAME, __func__); set_body (m_mailitem, m_orig_body, m_orig_body); TRETURN; } // No need to carry body anymore if (!is_preview) { m_orig_body = std::string(); } auto html = m_parser->get_html_body (); auto body = m_parser->get_body (); /** Outlook does not show newlines if \r\r\n is a newline. We replace these as apparently some other buggy MUA sends this. */ find_and_replace (html, "\r\r\n", "\r\n"); if (opt.prefer_html && (!html.empty() || (is_preview && !m_block_html))) { if (!m_block_html) { auto charset = m_parser->get_html_charset(); int codepage = 0; if (charset.empty()) { codepage = get_oom_int (m_mailitem, "InternetCodepage"); log_debug ("%s:%s: Did not find html charset." " Using internet Codepage %i.", SRCNAME, __func__, codepage); } char *converted = nullptr; if (!html.empty () || !is_preview) { converted = ansi_charset_to_utf8 (charset.c_str(), html.c_str(), html.size(), codepage); } if (is_preview) { char *buf; if (!converted) { /* Convert plaintext to HTML for preview using outlook. */ charset = m_parser->get_body_charset (); converted = ansi_charset_to_utf8 (charset.c_str(), body.c_str(), body.size(), codepage); put_oom_string (m_mailitem, "Body", converted); xfree (converted); converted = get_oom_string (m_mailitem, "HTMLBody"); } if (converted) { gpgrt_asprintf (&buf, HTML_PREVIEW_PLACEHOLDER, isSMIME_m () ? "S/MIME" : "OpenPGP", _("message"), _("Please wait while the message is being verified..."), converted); memdbg_alloc (buf); xfree (converted); converted = buf; } } TRACEPOINT; put_oom_int (m_mailitem, "BodyFormat", 2); int ret = put_oom_string (m_mailitem, "HTMLBody", converted ? converted : ""); xfree (converted); TRACEPOINT; if (ret) { log_error ("%s:%s: Failed to modify html body of item.", SRCNAME, __func__); } TRETURN; } else if (!body.empty()) { /* We had a multipart/alternative mail but html should be blocked. So we prefer the text/plain part and warn once about this so that we hopefully don't get too many bugreports about this. */ if (!opt.smime_html_warn_shown) { std::string caption = _("GpgOL") + std::string (": ") + std::string (_("HTML display disabled.")); std::string buf = _("HTML content in unsigned S/MIME mails " "is insecure."); buf += "\n"; buf += _("GpgOL will only show such mails as text."); buf += "\n\n"; buf += _("This message is shown only once."); gpgol_message_box (getWindow (), buf.c_str(), caption.c_str(), MB_OK); opt.smime_html_warn_shown = true; write_options (); } } } if (body.empty () && m_block_html && !html.empty()) { #if 0 Sadly the following code still offers to load external references it might also be too dangerous if Outlook somehow autoloads the references as soon as the Body is put into HTML // Fallback to show HTML as plaintext if HTML display // is blocked. log_error ("%s:%s: No text body. Putting HTML into plaintext.", SRCNAME, __func__); char *converted = ansi_charset_to_utf8 (m_parser->get_html_charset().c_str(), html.c_str(), html.size()); int ret = put_oom_string (m_mailitem, "HTMLBody", converted ? converted : ""); xfree (converted); if (ret) { log_error ("%s:%s: Failed to modify html body of item.", SRCNAME, __func__); body = html; } else { char *plainBody = get_oom_string (m_mailitem, "Body"); if (!plainBody) { log_error ("%s:%s: Failed to obtain converted plain body.", SRCNAME, __func__); body = html; } else { ret = put_oom_string (m_mailitem, "HTMLBody", plainBody); xfree (plainBody); if (ret) { log_error ("%s:%s: Failed to put plain into html body of item.", SRCNAME, __func__); body = html; } else { TRETURN; } } } #endif body = html; std::string caption = _("GpgOL") + std::string (": ") + std::string (_("HTML display disabled.")); std::string buf = _("HTML content in unsigned S/MIME mails " "is insecure."); buf += "\n"; buf += _("GpgOL will only show such mails as text."); buf += "\n\n"; buf += _("Please ask the sender to sign the message or\n" "to send it with a plain text alternative."); gpgol_message_box (getWindow (), buf.c_str(), caption.c_str(), MB_OK); } find_and_replace (body, "\r\r\n", "\r\n"); const auto plain_charset = m_parser->get_body_charset(); int codepage = 0; if (plain_charset.empty()) { codepage = get_oom_int (m_mailitem, "InternetCodepage"); log_debug ("%s:%s: Did not find body charset. " "Using internet Codepage %i.", SRCNAME, __func__, codepage); } char *converted = ansi_charset_to_utf8 (plain_charset.c_str(), body.c_str(), body.size(), codepage); if (is_preview) { char *buf; gpgrt_asprintf (&buf, TEXT_PREVIEW_PLACEHOLDER, isSMIME_m () ? "S/MIME" : "OpenPGP", _("message"), _("Please wait while the message is being verified..."), converted); memdbg_alloc (buf); xfree (converted); converted = buf; } TRACEPOINT; put_oom_int (m_mailitem, "BodyFormat", 1); int ret = put_oom_string (m_mailitem, "Body", converted ? converted : ""); TRACEPOINT; xfree (converted); if (ret) { log_error ("%s:%s: Failed to modify body of item.", SRCNAME, __func__); } TRETURN; } void Mail::updateHeaders_o () { TSTART; if (!m_parser) { STRANGEPOINT; TRETURN; } const auto subject = m_parser->get_protected_header ("Subject"); if (!subject.empty ()) { put_oom_string (m_mailitem, "Subject", subject.c_str ()); } const auto to = m_parser->get_protected_header ("To"); if (!to.empty()) { put_oom_string (m_mailitem, "To", to.c_str ()); } const auto cc = m_parser->get_protected_header ("Cc"); if (!cc.empty()) { put_oom_string (m_mailitem, "CC", cc.c_str ()); } /* TODO: What about Date ? */ const auto reply_to = m_parser->get_protected_header ("Reply-To"); const auto followup_to = m_parser->get_protected_header ("Followup-To"); if (!reply_to.empty () || !followup_to.empty()) { auto recipients = MAKE_SHARED (get_oom_object (m_mailitem, "ReplyRecipents")); if (recipients) { if (!reply_to.empty ()) { invoke_oom_method_with_string (recipients.get (), "Add", reply_to.c_str ()); } if (!followup_to.empty ()) { invoke_oom_method_with_string (recipients.get (), "Add", followup_to.c_str ()); } } } const auto from = m_parser->get_protected_header ("From"); if (!from.empty ()) { LPDISPATCH sender = get_oom_object (m_mailitem, "Sender"); if (!sender) { log_debug ("%s:%s: Sender not found. From not set.", SRCNAME, __func__); TRETURN; } /* Declare that the address is SMTP */ put_oom_int (sender, "AddressEntryUserType", 30); const auto mail_start = from.find (" <"); if (mail_start == std::string::npos) { put_oom_string (sender, "Address", from.c_str ()); put_oom_string (sender, "Name", ""); } else { put_oom_string (sender, "Address", GpgME::UserID::addrSpecFromString (from.c_str ()).c_str ()); put_oom_string (sender, "Name", from.substr (0, mail_start).c_str ()); } gpgol_release (sender); } } static int parsed_count; void Mail::parsingDone_o (bool is_preview) { TSTART; TRACEPOINT; log_oom ("Mail %p Parsing done for parser num %i: %p", this, parsed_count++, m_parser.get()); if (!m_parser) { /* This should not happen but it happens when outlook sends multiple ItemLoad events for the same Mail Object. In that case it could happen that one parser was already done while a second is now returning for the wrong mail (as it's looked up by uuid.) We have a check in get_uuid that the uuid was not in the map before (and the parser is replaced). So this really really should not happen. We handle it anyway as we crash otherwise. It should not happen because the parser is only created in decrypt_verify which is called in the read event. And even in there we check if the parser was set. */ log_error ("%s:%s: No parser obj. For mail: %p", SRCNAME, __func__, this); TRETURN; } /* Store the results. */ m_decrypt_result = m_parser->decrypt_result (); if (is_preview) { log_dbg ("Parser is not completely done. In Preview mode."); } else { m_verify_result = m_parser->verify_result (); } /* Handle protected headers */ updateHeaders_o (); m_crypto_flags = 0; if (!m_decrypt_result.isNull()) { m_crypto_flags |= 1; } if (m_verify_result.numSignatures()) { m_crypto_flags |= 2; } TRACEPOINT; updateSigstate (); m_needs_wipe = !m_is_send_again; TRACEPOINT; /* Set categories according to the result. */ updateCategories_o (); TRACEPOINT; m_block_html = m_parser->shouldBlockHtml (); if (m_block_html) { // Just to be careful. setBlockStatus_m (); } TRACEPOINT; /* Update the body */ updateBody_o (is_preview); TRACEPOINT; m_dec_content_type = m_parser->get_content_type (); log_dbg ("Decrypted mail has content type: '%s'", m_dec_content_type.c_str ()); /* When printing we have already shown the warning. So we should not show it again but silently remove any attachments that are not hidden before our add_attachments. This also fixes an issue that when printing sometimes the child mails which are created for preview and print already have the decrypted attachments. */ checkAttachments_o (isPrint ()); /* Update attachments */ if (add_attachments_o (m_parser->get_attachments())) { log_error ("%s:%s: Failed to update attachments.", SRCNAME, __func__); } if (m_is_send_again) { log_debug ("%s:%s: I think that this is the send again of a crypto mail.", SRCNAME, __func__); /* We no longer want to be treated like a crypto mail. */ m_type = MSGTYPE_UNKNOWN; LPMESSAGE msg = get_oom_base_message (m_mailitem); if (!msg) { TRACEPOINT; } else { set_gpgol_draft_info_flags (msg, m_crypto_flags); gpgol_release (msg); } removeOurAttachments_o (); } installFolderEventHandler_o (); if (!is_preview) { log_debug ("%s:%s: Delayed invalidate to update sigstate.", SRCNAME, __func__); CloseHandle(CreateThread (NULL, 0, delayed_invalidate_ui, (LPVOID) 300, 0, NULL)); } TRACEPOINT; TRETURN; } int Mail::encryptSignStart_o () { TSTART; - if (m_crypt_state != NeedsActualCrypt) + if (m_crypt_state != DataCollected) { log_debug ("%s:%s: invalid state %i", SRCNAME, __func__, m_crypt_state); TRETURN -1; } int flags = 0; if (!needs_crypto_m ()) { TRETURN 0; } LPMESSAGE message = get_oom_base_message (m_mailitem); if (!message) { log_error ("%s:%s: Failed to get base message.", SRCNAME, __func__); TRETURN -1; } flags = get_gpgol_draft_info_flags (message); gpgol_release (message); const auto window = get_active_hwnd (); if (m_is_gsuite) { auto att_table = mapi_create_attach_table (message, 0); int n_att_usable = count_usable_attachments (att_table); mapi_release_attach_table (att_table); /* Check for attachments if we have some abort. */ if (n_att_usable) { wchar_t *w_title = utf8_to_wchar (_( "GpgOL: Oops, G Suite Sync account detected")); wchar_t *msg = utf8_to_wchar ( _("G Suite Sync breaks outgoing crypto mails " "with attachments.\nUsing crypto and attachments " "with G Suite Sync is not supported.\n\n" "See: https://dev.gnupg.org/T3545 for details.")); MessageBoxW (window, msg, w_title, MB_ICONINFORMATION|MB_OK); xfree (msg); xfree (w_title); TRETURN -1; } } m_do_inline = m_is_draft_encrypt ? false : m_is_gsuite ? true : opt.inline_pgp; GpgME::Protocol proto = opt.enable_smime ? GpgME::UnknownProtocol: GpgME::OpenPGP; m_crypter = std::shared_ptr (new CryptController (this, flags & 1, flags & 2, proto)); // Careful from here on we have to check every // error condition with window enabling again. disableWindow_o (); if (m_crypter->collect_data ()) { log_error ("%s:%s: Crypter for mail %p failed to collect data.", SRCNAME, __func__, this); enableWindow (); TRETURN -1; } if (!m_async_crypt_disabled) { CloseHandle(CreateThread (NULL, 0, do_crypt, (LPVOID) this, 0, NULL)); } else { log_debug ("%s:%s: Starting sync crypt", SRCNAME, __func__); do_crypt (this); } TRETURN 0; } int Mail::needs_crypto_m () const { TSTART; LPMESSAGE message = get_oom_message (m_mailitem); int ret; if (!message) { log_error ("%s:%s: Failed to get message.", SRCNAME, __func__); TRETURN false; } ret = get_gpgol_draft_info_flags (message); gpgol_release(message); TRETURN ret; } int Mail::wipe_o (bool force) { TSTART; if (!m_needs_wipe && !force) { TRETURN 0; } log_debug ("%s:%s: Removing plaintext from mailitem: %p.", SRCNAME, __func__, m_mailitem); if (put_oom_string (m_mailitem, "HTMLBody", "")) { if (put_oom_string (m_mailitem, "Body", "")) { log_debug ("%s:%s: Failed to wipe mailitem: %p.", SRCNAME, __func__, m_mailitem); TRETURN -1; } TRETURN -1; } else { put_oom_string (m_mailitem, "Body", ""); } m_needs_wipe = false; TRETURN 0; } int Mail::updateOOMData_o (bool for_encryption) { TSTART; char *buf = nullptr; - log_debug ("%s:%s", SRCNAME, __func__); + log_debug ("%s:%s: Called. For encryption: %i", SRCNAME, __func__, + for_encryption); for_encryption |= !isCryptoMail(); if (for_encryption) { + /* Write out the mail to a temporary file so that all the + properties are set correctly. */ + + HANDLE hTmpFile = INVALID_HANDLE_VALUE; + char *path = get_tmp_outfile_utf8 ("gpgol_enc.dat", &hTmpFile); + if (!path || hTmpFile == INVALID_HANDLE_VALUE) + { + STRANGEPOINT; + TRETURN -1; + } + CloseHandle (hTmpFile); + + m_pass_write = true; + if (oom_save_as (m_mailitem, path, olMSG)) + { + log_dbg ("Failed to call SaveAs."); + TRETURN -1; + } + + wchar_t *wpath = utf8_to_wchar (path); + if (FAILED (DeleteFileW (wpath))) + { + log_err ("Failed to delete file %S", wpath); + } + xfree (wpath); + + /* Clear any leftovers */ + m_plain_attachments.clear (); + + auto attachments = get_oom_object_s (m_mailitem, "Attachments"); + int count = attachments ? get_oom_int (attachments, "Count") : 0; + + for (int i = 1; i <= count; i++) + { + std::string item = asprintf_s ("Item(%i)", i); + LPDISPATCH attach = get_oom_object (attachments.get (), item.c_str ()); + if (!attach) + { + STRANGEPOINT; + continue; + } + m_plain_attachments.push_back (std::shared_ptr ( + new Attachment (attach))); + } + /* Update the body format. */ m_is_html_alternative = get_oom_int (m_mailitem, "BodyFormat") > 1; /* Store the body. It was not obvious for me (aheinecke) how to access this through MAPI. */ if (m_is_html_alternative) { log_debug ("%s:%s: Is html alternative mail.", SRCNAME, __func__); xfree (m_cached_html_body); m_cached_html_body = get_oom_string (m_mailitem, "HTMLBody"); } xfree (m_cached_plain_body); m_cached_plain_body = get_oom_string (m_mailitem, "Body"); if (!m_recipients_set) { m_cached_recipients = getRecipients_o (); } else { log_dbg ("Not updating cached recipients because recipients were " "set explicitly."); } } else { /* This is the case where we are reading a mail and not composing. When composing we need to use the SendUsingAccount because if you send from the folder of userA but change the from to userB outlook will keep the SenderEmailAddress of UserA. This is all so horrible. */ buf = get_sender_SenderEMailAddress (m_mailitem); if (!buf) { /* Try the sender Object */ buf = get_sender_Sender (m_mailitem); } /* We also want to cache sent representing email address so that we can use it for verification information. */ char *buf2 = get_sender_SentRepresentingAddress (m_mailitem); if (buf2) { m_sent_on_behalf = buf2; xfree (buf2); } } if (!buf) { buf = get_sender_SendUsingAccount (m_mailitem, &m_is_gsuite); } if (!isCryptoMail ()) { /* Try the sender Object and handle the case that someone changed the identity but not the account. */ char *buf2 = get_sender_Sender (m_mailitem); char *tmp = buf; if (buf && buf2) { if (*buf == '/' && *buf2 != '/' && *buf2) { log_dbg ("Send account could not be resolved. Using sender."); buf = buf2; xfree (tmp); } else if (*buf && *buf != '/' && *buf2 && *buf2 != '/') { log_dbg ("SendUsingAccount: '%s' Sender: '%s'", anonstr (buf), anonstr (buf2)); auto key = KeyCache::instance()->getSigningKey (buf2, GpgME::OpenPGP); if (!key.isNull()) { log_dbg ("Found OpenPGP Key for %s using that identity", anonstr (buf2)); buf = buf2; xfree (tmp); } else if (opt.enable_smime) { key = KeyCache::instance()->getSigningKey (buf2, GpgME::CMS); log_dbg ("Found S/MIME Key for %s using that identity", anonstr (buf2)); buf = buf2; xfree (tmp); } } } else if (!buf) { buf = buf2; } else if (buf2 && buf != buf2) { log_dbg ("Keeping SendUsingAccount %s", anonstr (buf)); xfree (buf2); } } if (!buf) { /* We don't have s sender object or SendUsingAccount, well, in that case fall back to the current user. */ buf = get_sender_CurrentUser (m_mailitem); } if (!buf) { log_debug ("%s:%s: All fallbacks failed.", SRCNAME, __func__); TRETURN -1; } m_sender = buf; if (for_encryption) { bool sender_is_recipient = false; for (const auto &recp: m_cached_recipients) { if (recp.mbox () == m_sender) { sender_is_recipient = true; log_debug ("%s:%s: Sender already recipient.", SRCNAME, __func__); break; } } if (!sender_is_recipient) { m_cached_recipients.push_back (Recipient (buf, Recipient::olOriginator)); } } xfree (buf); TRETURN 0; } std::string Mail::getSender_o () { TSTART; if (m_sender.empty()) updateOOMData_o (); TRETURN m_sender; } std::string Mail::getSender () const { TSTART; TRETURN m_sender; } int Mail::closeAllMails_o () { TSTART; int err = 0; /* Detach Folder sinks */ for (auto fit = s_folder_events_map.begin(); fit != s_folder_events_map.end(); ++fit) { detach_FolderEvents_sink (fit->second); gpgol_release (fit->second); } s_folder_events_map.clear(); std::map::iterator it; TRACEPOINT; gpgol_lock (&mail_map_lock); std::map mail_map_copy = s_mail_map; gpgol_unlock (&mail_map_lock); for (it = mail_map_copy.begin(); it != mail_map_copy.end(); ++it) { /* XXX For non racy code the is_valid_ptr check should not be necessary but we crashed sometimes closing a destroyed mail. */ if (!isValidPtr (it->second)) { log_debug ("%s:%s: Already deleted mail for %p", SRCNAME, __func__, it->first); continue; } if (!it->second->isCryptoMail ()) { continue; } bool close_failed = false; if (closeInspector_o (it->second)) { log_error ("%s:%s: Failed to close mail inspector: %p ", SRCNAME, __func__, it->first); close_failed = true; } if (isValidPtr (it->second)) { log_debug ("%s:%s: Inspector closed for %p closing object.", SRCNAME, __func__, it->first); if (it->second->close ()) { log_error ("%s:%s: Failed to close mail itself: %p ", SRCNAME, __func__, it->first); close_failed = true; } } else { log_debug ("%s:%s: Mail gone after inspector close.", SRCNAME, __func__); close_failed = false; } - /* Beware: The close code removes our Plaintext from the + /* Beware: The close code removes our NotStarted from the Outlook Object Model and temporary MAPI. If there - is an error we might put Plaintext into permanent + is an error we might put NotStarted into permanent storage and leak it to the server. So we have an extra safeguard below. The revert is likely to fail if close and closeInspector fails but to guard against a bug in our close code we try it anyway as revert will also try to remove the plaintext from memory and restore the original message. */ if (close_failed) { if (isValidPtr (it->second) && it->second->revert_o ()) { err++; } } } TRETURN err; } int Mail::revertAllMails_o () { TSTART; int err = 0; std::map::iterator it; gpgol_lock (&mail_map_lock); auto mail_map_copy = s_mail_map; gpgol_unlock (&mail_map_lock); for (it = mail_map_copy.begin(); it != mail_map_copy.end(); ++it) { if (it->second->revert_o ()) { log_error ("Failed to revert mail: %p ", it->first); err++; continue; } it->second->setNeedsSave (true); if (!invoke_oom_method (it->first, "Save", NULL)) { log_error ("Failed to save reverted mail: %p ", it->second); err++; continue; } } TRETURN err; } int Mail::wipeAllMails_o () { TSTART; int err = 0; std::map::iterator it; gpgol_lock (&mail_map_lock); auto mail_map_copy = s_mail_map; gpgol_unlock (&mail_map_lock); for (it = mail_map_copy.begin(); it != mail_map_copy.end(); ++it) { if (it->second->wipe_o ()) { log_error ("Failed to wipe mail: %p ", it->first); err++; } } TRETURN err; } int Mail::revert_o () { TSTART; int err = 0; if (!m_processed) { TRETURN 0; } m_disable_att_remove_warning = true; err = gpgol_mailitem_revert (m_mailitem); if (err == -1) { log_error ("%s:%s: Message revert failed falling back to wipe.", SRCNAME, __func__); TRETURN wipe_o (); } /* We need to reprocess the mail next time around. */ m_processed = false; m_needs_wipe = false; m_disable_att_remove_warning = false; TRETURN 0; } bool Mail::isSMIME () const { if (!m_is_smime_checked) { log_dbg ("WARNING: SMIME check before isSMIME_m was called."); return false; } return m_is_smime; } bool Mail::isSMIME_m () { TSTART; msgtype_t msgtype; LPMESSAGE message; if (m_is_smime_checked) { TRETURN m_is_smime; } message = get_oom_message (m_mailitem); if (!message) { log_error ("%s:%s: No message?", SRCNAME, __func__); TRETURN false; } msgtype = mapi_get_message_type (message); m_is_smime = msgtype == MSGTYPE_GPGOL_OPAQUE_ENCRYPTED || msgtype == MSGTYPE_GPGOL_OPAQUE_SIGNED || msgtype == MSGTYPE_SMIME; /* Check if it is an smime mail. Multipart signed can also be true. */ if (!m_is_smime && msgtype == MSGTYPE_GPGOL_MULTIPART_SIGNED) { char *proto; char *ct = mapi_get_message_content_type (message, &proto, NULL); if (ct && proto) { m_is_smime = (!strcmp (proto, "application/pkcs7-signature") || !strcmp (proto, "application/x-pkcs7-signature")); } else { log_error ("%s:%s: No protocol in multipart / signed mail.", SRCNAME, __func__); } xfree (proto); xfree (ct); } gpgol_release (message); m_is_smime_checked = true; log_debug ("%s:%s: Detected %s mail", SRCNAME, __func__, m_is_smime ? "S/MIME" : "not S/MIME"); TRETURN m_is_smime; } static std::string get_string_o (LPDISPATCH item, const char *str) { TSTART; char *buf = get_oom_string (item, str); if (!buf) { TRETURN std::string(); } std::string ret = buf; xfree (buf); TRETURN ret; } std::string Mail::getSubject_o () const { TSTART; TRETURN get_string_o (m_mailitem, "Subject"); } std::string Mail::getBody_o () const { TSTART; TRETURN get_string_o (m_mailitem, "Body"); } std::vector Mail::getRecipients_o () const { TSTART; LPDISPATCH recipients = get_oom_object (m_mailitem, "Recipients"); if (!recipients) { TRACEPOINT; std::vector(); } bool err = false; auto ret = get_oom_recipients (recipients, &err); gpgol_release (recipients); if (err) { log_debug ("%s:%s: Failed to resolve recipients at this time.", SRCNAME, __func__); } TRETURN ret; } int Mail::closeInspector_o (Mail *mail) { TSTART; LPDISPATCH inspector = get_oom_object (mail->item(), "GetInspector"); HRESULT hr; DISPID dispid; if (!inspector) { log_debug ("%s:%s: No inspector.", SRCNAME, __func__); TRETURN -1; } dispid = lookup_oom_dispid (inspector, "Close"); if (dispid != DISPID_UNKNOWN) { VARIANT aVariant[1]; DISPPARAMS dispparams; dispparams.rgvarg = aVariant; dispparams.rgvarg[0].vt = VT_INT; dispparams.rgvarg[0].intVal = 1; dispparams.cArgs = 1; dispparams.cNamedArgs = 0; hr = inspector->Invoke (dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, &dispparams, NULL, NULL, NULL); if (hr != S_OK) { log_debug ("%s:%s: Failed to close inspector: %#lx", SRCNAME, __func__, hr); gpgol_release (inspector); TRETURN -1; } } gpgol_release (inspector); TRETURN 0; } int Mail::close (bool restoreSMIMEClass) { TSTART; wm_after_move_data_t *move_data = nullptr; VARIANT aVariant[1]; DISPPARAMS dispparams; dispparams.rgvarg = aVariant; dispparams.rgvarg[0].vt = VT_INT; dispparams.rgvarg[0].intVal = 1; dispparams.cArgs = 1; dispparams.cNamedArgs = 0; /* Remove the mail from the print mail set. So that categories etc work again. */ const auto ourID = get_oom_string_s (m_mailitem, "EntryID"); if (!ourID.empty ()) { const auto it = s_entry_ids_printing.find (ourID); if (it != s_entry_ids_printing.end ()) { log_dbg ("Found %s in printing map. Removing it", ourID.c_str ()); s_entry_ids_printing.erase (it); } } if (isSMIME_m ()) { LPDISPATCH attachments = get_oom_object (m_mailitem, "Attachments"); LPMESSAGE mapi_msg = get_oom_message (m_mailitem); if (!mapi_msg) { log_error ("%s:%s:Failed to obtain mapi message for %p on close.", SRCNAME, __func__, m_mailitem); } if (m_gpgol_class.empty()) { log_debug ("%s:%s: GpgOL Class empty for S/MIME Mail.", SRCNAME, __func__); } /* This is strangely important. Because Outlook apparently treats * S/MIME Mails differently we need to change our message class * here again so that discard changes works properly. */ if (mapi_msg && !m_gpgol_class.empty ()) { char *current = mapi_get_message_class (mapi_msg); if (current && strcmp (current, m_gpgol_class.c_str ())) { log_debug ("%s:%s:Setting message class to %s on close.", SRCNAME, __func__, m_gpgol_class.c_str ()); mapi_set_mesage_class (mapi_msg, m_gpgol_class.c_str ()); if (restoreSMIMEClass) { /* After the close we need to change the message class back again so as to not break compatibility with other clients. */ move_data = (wm_after_move_data_t *) xmalloc (sizeof (wm_after_move_data_t)); size_t entryIDLen = 0; char *entryID = nullptr; entryID = mapi_get_binary_prop (mapi_msg, PR_ENTRYID, &entryIDLen); LPDISPATCH folder = get_oom_object (m_mailitem, "Parent"); if (folder) { char *name = get_object_name ((LPUNKNOWN) folder); if (!name || strcmp (name, "MAPIFolder")) { log_error ("%s:%s: Failed to obtain folder on close object is %s", SRCNAME, __func__, name ? name : "(null)"); } else { xfree (name); move_data->target_folder = (LPMAPIFOLDER) get_oom_iunknown ( folder, "MAPIOBJECT"); if (!move_data->target_folder) { log_error ("%s:%s: Failed to obtain target folder.", SRCNAME, __func__); xfree (entryID); xfree (current); xfree (move_data); move_data = nullptr; } else { memdbg_addRef (move_data->target_folder); } } } move_data->entry_id = entryID; move_data->entry_id_len = entryIDLen; move_data->old_class = current; } } } if (attachments) { int count = get_oom_int (attachments, "Count"); gpgol_release (attachments); if (count) { /* On exchange Outlook sometimes detects that an S/MIME mail * is an S/MIME Mail. When this mail is then modified by * us and the mail should be moved or closed Outlook will try * to save it. This fails and the user gets an error. * * So we save here, which should not be dangerous as we do not * put plaintext in mapi. * * Still better only do it if it is really * necessary as the changed message class can hurt. * * Tests show no plaintext leaks. The save saves the * message class and in that way outlook no longer * thinks the mails are S/MIME mails and we can * use our own handling. See T4525 */ removeCategories_o (); HRESULT hr = 0; if (mapi_msg) { log_debug ("%s:%s: MAPI Save for: %p", SRCNAME, __func__, m_mailitem); mapi_msg->SaveChanges (KEEP_OPEN_READWRITE); } if (!mapi_msg || hr) { log_error ("%s:%s: Failed to save mapi for %p hr=%#lx", SRCNAME, __func__, this, hr); } /* In case the mail is still visible in a different window */ updateCategories_o (); } } gpgol_release (mapi_msg); } log_oom ("%s:%s: Invoking close for: %p", SRCNAME, __func__, m_mailitem); setCloseTriggered (true); int rc = invoke_oom_method_with_parms (m_mailitem, "Close", nullptr, &dispparams); if (move_data) { log_debug ("%s:%s:Restoring message class to %s after close.", SRCNAME, __func__, move_data->old_class); do_in_ui_thread_async (AFTER_MOVE, move_data, 0); } setCloseTriggered (false); if (!rc) { /* Saveguard against oom writes when our data is in OOM. This * can happen when the mail was opened in a new window. But * also shown in the preview. * We get a close event and discard changes but the data is * still in oom because it is still visible in the opened * window. * * In that case we may not write! Otherwise the plaintext * might be leaked back to the server if the folder is synced. * */ char *body = get_oom_string (m_mailitem, "Body"); LPDISPATCH attachments = get_oom_object (m_mailitem, "Attachments"); if (body && strlen (body)) { log_debug ("%s:%s: Close successful. But body found. " "Mail still open.", SRCNAME, __func__); } else if (count_visible_attachments (attachments)) { log_debug ("%s:%s: Close successful. But attachments found. " "Mail still open.", SRCNAME, __func__); } else { setPassWrite (true); log_debug ("%s:%s: Close successful. Next write may pass.", SRCNAME, __func__); } gpgol_release (attachments); xfree (body); } log_oom ("%s:%s: returned from close", SRCNAME, __func__); TRETURN rc; } void Mail::setCloseTriggered (bool value) { TSTART; m_close_triggered = value; TRETURN; } bool Mail::getCloseTriggered () const { TSTART; TRETURN m_close_triggered; } static const UserID get_uid_for_sender (const Key &k, const char *sender) { TSTART; UserID ret; if (!sender) { TRETURN ret; } if (!k.numUserIDs()) { log_debug ("%s:%s: Key without uids", SRCNAME, __func__); TRETURN ret; } for (const auto uid: k.userIDs()) { if (!uid.email() || !*(uid.email())) { /* This happens for S/MIME a lot */ log_debug ("%s:%s: skipping uid without email.", SRCNAME, __func__); continue; } auto normalized_uid = uid.addrSpec(); auto normalized_sender = UserID::addrSpecFromString(sender); if (normalized_sender.empty() || normalized_uid.empty()) { log_error ("%s:%s: normalizing '%s' or '%s' failed.", SRCNAME, __func__, anonstr (uid.email()), anonstr (sender)); continue; } if (normalized_sender == normalized_uid) { ret = uid; } } TRETURN ret; } void Mail::updateSigstate () { TSTART; std::string sender = getSender (); if (sender.empty()) { log_error ("%s:%s:%i", SRCNAME, __func__, __LINE__); TRETURN; } if (m_verify_result.isNull()) { log_debug ("%s:%s: No verify result.", SRCNAME, __func__); TRETURN; } if (m_verify_result.error()) { log_debug ("%s:%s: verify error.", SRCNAME, __func__); TRETURN; } for (const auto sig: m_verify_result.signatures()) { m_is_signed = true; const auto key = KeyCache::instance ()->getByFpr (sig.fingerprint(), true); m_uid = get_uid_for_sender (key, sender.c_str()); if (m_uid.isNull() && !m_sent_on_behalf.empty ()) { m_uid = get_uid_for_sender (key, m_sent_on_behalf.c_str ()); if (!m_uid.isNull()) { log_debug ("%s:%s: Using sent on behalf '%s' instead of '%s'", SRCNAME, __func__, anonstr (m_sent_on_behalf.c_str()), anonstr (sender.c_str ())); } } /* Sigsum valid or green is somehow not set in this case. */ if (!sig.status() && m_uid.origin() == GpgME::Key::OriginWKD && (sig.validity() == Signature::Validity::Unknown || sig.validity() == Signature::Validity::Marginal)) { // WKD is a shortcut to Level 2 trust. log_debug ("%s:%s: Unknown or marginal from WKD -> Level 2", SRCNAME, __func__); } else if (m_uid.isNull() || (sig.validity() != Signature::Validity::Marginal && sig.validity() != Signature::Validity::Full && sig.validity() != Signature::Validity::Ultimate)) { /* For our category we only care about trusted sigs. And the UID needs to match.*/ continue; } else if (sig.validity() == Signature::Validity::Marginal) { const auto tofu = m_uid.tofuInfo(); if (!tofu.isNull() && (tofu.validity() != TofuInfo::Validity::BasicHistory && tofu.validity() != TofuInfo::Validity::LargeHistory)) { /* Marginal is only good enough without tofu. Otherwise we wait for basic trust. */ log_debug ("%s:%s: Discarding marginal signature." "With too little history.", SRCNAME, __func__); continue; } } log_debug ("%s:%s: Classified sender as verified uid validity: %i origin: %i", SRCNAME, __func__, m_uid.validity(), m_uid.origin()); m_sig = sig; m_is_valid = true; TRETURN; } log_debug ("%s:%s: No signature with enough trust. Using first", SRCNAME, __func__); m_sig = m_verify_result.signature(0); TRETURN; } bool Mail::isValidSig () const { TSTART; TRETURN m_is_valid; } void Mail::removeCategories_o () { TSTART; if (!m_store_id.empty () && !m_verify_category.empty ()) { log_oom ("%s:%s: Unreffing verify category", SRCNAME, __func__); CategoryManager::instance ()->removeCategory (this, m_verify_category); } if (!m_store_id.empty () && !m_decrypt_result.isNull()) { log_oom ("%s:%s: Unreffing dec category", SRCNAME, __func__); CategoryManager::instance ()->removeCategory (this, CategoryManager::getEncMailCategory ()); } if (m_is_junk) { log_oom ("%s:%s: Unreffing junk category", SRCNAME, __func__); CategoryManager::instance ()->removeCategory (this, CategoryManager::getJunkMailCategory ()); } TRETURN; } /* Now for some tasty hack: Outlook sometimes does not show the new categories properly but instead does some weird scrollbar thing. This can be avoided by resizing the message a bit. But somehow this only needs to be done once. Weird isn't it? But as this workaround worked let's do it programatically. Fun. Wan't some tomato sauce with this hack? */ static void resize_active_window () { TSTART; HWND wnd = get_active_hwnd (); static std::vector resized_windows; if(std::find(resized_windows.begin(), resized_windows.end(), wnd) != resized_windows.end()) { /* We only need to do this once per window. XXX But sometimes we also need to do this once per view of the explorer. So for now this might break but we reduce the flicker. A better solution would be to find the current view and track that. */ TRETURN; } if (!wnd) { TRACEPOINT; TRETURN; } RECT oldpos; if (!GetWindowRect (wnd, &oldpos)) { TRACEPOINT; TRETURN; } if (!SetWindowPos (wnd, nullptr, (int)oldpos.left, (int)oldpos.top, /* Anything smaller then 19 was ignored when the window was * maximized on Windows 10 at least with a 1980*1024 * resolution. So I assume it's at least 1 percent. * This is all hackish and ugly but should work for 90%... * hopefully. */ (int)oldpos.right - oldpos.left - 20, (int)oldpos.bottom - oldpos.top, 0)) { TRACEPOINT; TRETURN; } if (!SetWindowPos (wnd, nullptr, (int)oldpos.left, (int)oldpos.top, (int)oldpos.right - oldpos.left, (int)oldpos.bottom - oldpos.top, 0)) { TRACEPOINT; TRETURN; } resized_windows.push_back(wnd); TRETURN; } #if 0 static std::string pretty_id (const char *keyId) { /* Three spaces, four quads and a NULL */ char buf[20]; buf[19] = '\0'; if (!keyId) { return std::string ("null"); } size_t len = strlen (keyId); if (!len) { return std::string ("empty"); } if (len < 16) { return std::string (_("Invalid Key")); } const char *p = keyId + (len - 16); int j = 0; for (size_t i = 0; i < 16; i++) { if (i && i % 4 == 0) { buf[j++] = ' '; } buf[j++] = *(p + i); } return std::string (buf); } #endif void Mail::updateCategories_o () { TSTART; if (m_printing) { log_debug ("%s:%s: Not updating categories as we are printing.", SRCNAME, __func__); return; } auto mngr = CategoryManager::instance (); if (isValidSig ()) { char *buf; /* Resolve to the primary fingerprint */ #if 0 const auto sigKey = KeyCache::instance ()->getByFpr (m_sig.fingerprint (), true); const char *sigFpr; if (sigKey.isNull()) { sigFpr = m_sig.fingerprint (); } else { sigFpr = sigKey.primaryFingerprint (); } #endif /* If m_uid addrSpec would not return a result we would never * have gotten the UID. */ int lvl = get_signature_level (); /* TRANSLATORS: The first placeholder is for tranlsation of "Level". The second one is for the level number. The third is for the translation of "trust in" and the last one is for the mail address used for verification. The result is used as the text on the green bar for signed mails. e.g.: "GpgOL: Level 3 trust in 'john.doe@example.org'" */ gpgrt_asprintf (&buf, "GpgOL: %s %i %s '%s'", _("Level"), lvl, _("trust in"), m_uid.addrSpec ().c_str ()); memdbg_alloc (buf); int color = 0; if (lvl == 2) { color = 7; /* Olive */ } if (lvl == 3) { color = 5; /* Green */ } if (lvl == 4) { color = 20; /* Dark Green */ } m_store_id = mngr->addCategoryToMail (this, buf, color); m_verify_category = buf; xfree (buf); } else { remove_category (m_mailitem, "GpgOL: ", false); } if (!m_decrypt_result.isNull()) { const auto id = mngr->addCategoryToMail (this, CategoryManager::getEncMailCategory (), 8 /* Blue */); if (m_store_id.empty()) { m_store_id = id; } if (m_store_id != id) { log_error ("%s:%s unexpected store mismatch " "between '%s' and dec cat '%s'", SRCNAME, __func__, m_store_id.c_str(), id.c_str()); } } else { /* As a small safeguard against fakes we remove our categories */ remove_category (m_mailitem, CategoryManager::getEncMailCategory ().c_str (), true); } resize_active_window(); TRETURN; } bool Mail::isSigned () const { TSTART; TRETURN m_verify_result.numSignatures() > 0; } bool Mail::isEncrypted () const { TSTART; TRETURN !m_decrypt_result.isNull(); } int Mail::setUUID_o () { TSTART; 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); TRETURN -1; } if (m_uuid.empty()) { m_uuid = uuid; Mail *other = getMailForUUID (uuid); if (other) { /* According to documentation this should not happen as this means that multiple ItemLoad events occured for the same mailobject without unload / destruction of the mail. But it happens. If you invalidate the UI in the selection change event Outlook loads a new mailobject for the mail. Might happen in other surprising cases. We replace in that case as experiments have shown that the last mailobject is the one that is visible. Still troubling state so we log this as an error. */ log_error ("%s:%s: There is another mail for %p " "with uuid: %s replacing it.", SRCNAME, __func__, m_mailitem, uuid); delete other; } gpgol_lock (&uid_map_lock); s_uid_map.insert (std::pair (m_uuid, this)); gpgol_unlock (&uid_map_lock); log_debug ("%s:%s: uuid for %p is now %s", SRCNAME, __func__, this, m_uuid.c_str()); } xfree (uuid); TRETURN 0; } /* TRETURNs 2 if the userid is ultimately trusted. TRETURNs 1 if the userid is fully trusted but has a signature by a key for which we have a secret and which is ultimately trusted. (Direct trust) 0 otherwise */ static int level_4_check (const UserID &uid) { TSTART; if (uid.isNull()) { TRETURN 0; } if (uid.validity () == UserID::Validity::Ultimate) { TRETURN 2; } if (uid.validity () == UserID::Validity::Full) { const auto ultimate_keys = KeyCache::instance()->getUltimateKeys (); for (const auto sig: uid.signatures ()) { const char *sigID = sig.signerKeyID (); if (sig.isNull() || !sigID) { /* should not happen */ TRACEPOINT; continue; } /* Direct trust information is not available through gnupg so we cached the keys with ultimate trust during parsing and now see if we find a direct trust path.*/ for (const auto secKey: ultimate_keys) { /* Check that the Key id of the key matches */ const char *secKeyID = secKey.keyID (); if (!secKeyID || strcmp (secKeyID, sigID)) { continue; } /* Check that the userID of the signature is the ultimately trusted one. */ const char *sig_uid_str = sig.signerUserID(); if (!sig_uid_str) { /* should not happen */ TRACEPOINT; continue; } for (const auto signer_uid: secKey.userIDs ()) { if (signer_uid.validity() != UserID::Validity::Ultimate) { TRACEPOINT; continue; } const char *signer_uid_str = signer_uid.id (); if (!sig_uid_str) { /* should not happen */ TRACEPOINT; continue; } if (!strcmp(sig_uid_str, signer_uid_str)) { /* We have a match */ log_debug ("%s:%s: classified %s as ultimate because " "it was signed by uid %s of key %s", SRCNAME, __func__, anonstr (signer_uid_str), anonstr (sig_uid_str), anonstr (secKeyID)); TRETURN 1; } } } } } TRETURN 0; } std::string Mail::getCryptoSummary () const { TSTART; const int level = get_signature_level (); bool enc = isEncrypted (); if (level == 4 && enc) { TRETURN _("Security Level 4"); } if (level == 4) { TRETURN _("Trust Level 4"); } if (level == 3 && enc) { TRETURN _("Security Level 3"); } if (level == 3) { TRETURN _("Trust Level 3"); } if (level == 2 && enc) { TRETURN _("Security Level 2"); } if (level == 2) { TRETURN _("Trust Level 2"); } if (enc) { TRETURN _("Encrypted"); } if (isSigned ()) { /* Even if it is signed, if it is not validly signed it's still completly insecure as anyone could have signed this. So we avoid the label "signed" here as this word already implies some security. */ TRETURN _("Insecure"); } TRETURN _("Insecure"); } std::string Mail::getCryptoOneLine () const { TSTART; bool sig = isSigned (); bool enc = isEncrypted (); if (sig || enc) { if (sig && enc) { TRETURN _("Signed and encrypted message"); } else if (sig) { TRETURN _("Signed message"); } else if (enc) { TRETURN _("Encrypted message"); } } TRETURN _("Insecure message"); } std::string Mail::getCryptoDetails_o () { TSTART; std::string message; /* No signature with keys but error */ if (!isEncrypted () && !isSigned () && m_verify_result.error()) { message = _("You cannot be sure who sent, " "modified and read the message in transit."); message += "\n\n"; message += _("The message was signed but the verification failed with:"); message += "\n"; message += m_verify_result.error().asString(); TRETURN message; } /* No crypo, what are we doing here? */ if (!isEncrypted () && !isSigned ()) { TRETURN _("You cannot be sure who sent, " "modified and read the message in transit."); } /* Handle encrypt only */ if (isEncrypted () && !isSigned ()) { if (in_de_vs_mode ()) { message += compliance_string (false, true, m_decrypt_result.isDeVs ()); } message += "\n\n"; message += _("You cannot be sure who sent the message because " "it is not signed."); TRETURN message; } bool keyFound = true; const auto sigKey = KeyCache::instance ()->getByFpr (m_sig.fingerprint (), true); bool isOpenPGP = sigKey.isNull() ? !isSMIME_m () : sigKey.protocol() == Protocol::OpenPGP; char *buf; bool hasConflict = false; int level = get_signature_level (); log_debug ("%s:%s: Formatting sig. Validity: %x Summary: %x Level: %i", SRCNAME, __func__, m_sig.validity(), m_sig.summary(), level); if (level == 4) { /* level 4 check for direct trust */ int four_check = level_4_check (m_uid); if (four_check == 2 && sigKey.hasSecret ()) { message = _("You signed this message."); } else if (four_check == 1) { message = _("The senders identity was certified by yourself."); } else if (four_check == 2) { message = _("The sender is allowed to certify identities for you."); } else { log_error ("%s:%s:%i BUG: Invalid sigstate.", SRCNAME, __func__, __LINE__); TRETURN message; } } else if (level == 3 && isOpenPGP) { /* Level three is only reachable through web of trust and no direct signature. */ message = _("The senders identity was certified by several trusted people."); } else if (level == 3 && !isOpenPGP) { /* Level three is the only level for trusted S/MIME keys. */ gpgrt_asprintf (&buf, _("The senders identity is certified by the trusted issuer:\n'%s'\n"), sigKey.issuerName()); memdbg_alloc (buf); message = buf; xfree (buf); } else if (level == 2 && m_uid.origin () == GpgME::Key::OriginWKD) { message = _("The mail provider of the recipient served this key."); } else if (level == 2 && m_uid.tofuInfo ().isNull ()) { /* Marginal trust through pgp only */ message = _("Some trusted people " "have certified the senders identity."); } else if (level == 2) { unsigned long first_contact = std::max (m_uid.tofuInfo().signFirst(), m_uid.tofuInfo().encrFirst()); char *time = format_date_from_gpgme (first_contact); /* i18n note signcount is always pulral because with signcount 1 we * would not be in this branch. */ gpgrt_asprintf (&buf, _("The senders address is trusted, because " "you have established a communication history " "with this address starting on %s.\n" "You encrypted %i and verified %i messages since."), time, m_uid.tofuInfo().encrCount(), m_uid.tofuInfo().signCount ()); memdbg_alloc (buf); xfree (time); message = buf; xfree (buf); } else if (level == 1) { /* This could be marginal trust through pgp, or tofu with little history. */ if (m_uid.tofuInfo ().signCount() == 1) { message += _("The senders signature was verified for the first time."); } else if (m_uid.tofuInfo ().validity() == TofuInfo::Validity::LittleHistory) { unsigned long first_contact = std::max (m_uid.tofuInfo().signFirst(), m_uid.tofuInfo().encrFirst()); char *time = format_date_from_gpgme (first_contact); gpgrt_asprintf (&buf, _("The senders address is not trustworthy yet because " "you only verified %i messages and encrypted %i messages to " "it since %s."), m_uid.tofuInfo().signCount (), m_uid.tofuInfo().encrCount (), time); memdbg_alloc (buf); xfree (time); message = buf; xfree (buf); } } else { /* Now we are in level 0, this could be a technical problem, no key or just unkown. */ message = isEncrypted () ? _("But the sender address is not trustworthy because:") : _("The sender address is not trustworthy because:"); message += "\n"; keyFound = !(m_sig.summary() & Signature::Summary::KeyMissing); bool general_problem = true; /* First the general stuff. */ if (m_sig.summary() & Signature::Summary::Red) { message += _("The signature is invalid: \n"); if (m_sig.status().code() == GPG_ERR_BAD_SIGNATURE) { message += std::string("\n") + _("The signature does not match."); return message; } } else if (m_sig.summary() & Signature::Summary::SysError || m_verify_result.numSignatures() < 1) { message += _("There was an error verifying the signature.\n"); const auto err = m_sig.status (); if (err) { message += err.asString () + std::string ("\n"); } } else if (m_sig.summary() & Signature::Summary::SigExpired) { message += _("The signature is expired.\n"); } else { message += isOpenPGP ? _("The used key") : _("The used certificate"); message += " "; general_problem = false; } /* Now the key problems */ if ((m_sig.summary() & Signature::Summary::KeyMissing)) { message += _("is not available."); } else if ((m_sig.summary() & Signature::Summary::KeyRevoked)) { message += _("is revoked."); } else if ((m_sig.summary() & Signature::Summary::KeyExpired)) { message += _("is expired."); } else if ((m_sig.summary() & Signature::Summary::BadPolicy)) { message += _("is not meant for signing."); } else if ((m_sig.summary() & Signature::Summary::CrlMissing)) { message += _("could not be checked for revocation."); } else if ((m_sig.summary() & Signature::Summary::CrlTooOld)) { message += _("could not be checked for revocation."); } else if ((m_sig.summary() & Signature::Summary::TofuConflict) || m_uid.tofuInfo().validity() == TofuInfo::Conflict) { message += _("is not the same as the key that was used " "for this address in the past."); hasConflict = true; } else if (m_uid.isNull()) { gpgrt_asprintf (&buf, _("does not claim the address: \"%s\"."), getSender_o ().c_str()); memdbg_alloc (buf); message += buf; xfree (buf); } else if (((m_sig.validity() & Signature::Validity::Undefined) || (m_sig.validity() & Signature::Validity::Unknown) || (m_sig.summary() == Signature::Summary::None) || (m_sig.validity() == 0))&& !general_problem) { /* Bit of a catch all for weird results. */ if (isOpenPGP) { message += _("is not certified by any trustworthy key."); } else { message += _("is not certified by a trustworthy Certificate Authority or the Certificate Authority is unknown."); } } else if (m_uid.isRevoked()) { message += _("The sender marked this address as revoked."); } else if ((m_sig.validity() & Signature::Validity::Never)) { message += _("is marked as not trustworthy."); } } message += "\n\n"; if (in_de_vs_mode ()) { bool isBoth = isSigned () && m_sig.isDeVs() && isEncrypted () && m_decrypt_result.isDeVs (); if (isBoth) { message += compliance_string (true, true, true); } if (!isBoth && isSigned ()) { message += compliance_string (true, false, m_sig.isDeVs ()); message += "\n"; } if (!isBoth && isEncrypted ()) { message += compliance_string (false, true, m_decrypt_result.isDeVs ()); message += "\n"; message += "\n\n"; } else { message += "\n"; } } if (hasConflict) { message += _("Click here to change the key used for this address."); } else if (keyFound) { message += isOpenPGP ? _("Click here for details about the key.") : _("Click here for details about the certificate."); } else { message += isOpenPGP ? _("Click here to search the key on the configured keyserver.") : _("Click here to search the certificate on the configured X509 keyserver."); } TRETURN message; } int Mail::get_signature_level () const { TSTART; if (!m_is_signed) { TRETURN 0; } if (m_uid.isNull ()) { /* No m_uid matches our sender. */ TRETURN 0; } if (m_is_valid && (m_uid.validity () == UserID::Validity::Ultimate || (m_uid.validity () == UserID::Validity::Full && level_4_check (m_uid))) && (!in_de_vs_mode () || m_sig.isDeVs())) { TRETURN 4; } if (m_is_valid && m_uid.validity () == UserID::Validity::Full && (!in_de_vs_mode () || m_sig.isDeVs())) { TRETURN 3; } if (m_is_valid) { TRETURN 2; } if (m_sig.validity() == Signature::Validity::Marginal) { TRETURN 1; } if (m_sig.summary() & Signature::Summary::TofuConflict || m_uid.tofuInfo().validity() == TofuInfo::Conflict) { TRETURN 0; } TRETURN 0; } int Mail::getCryptoIconID () const { TSTART; int level = get_signature_level (); int offset = isEncrypted () ? ENCRYPT_ICON_OFFSET : 0; TRETURN IDI_LEVEL_0 + level + offset; } const char* Mail::getSigFpr () const { TSTART; if (!m_is_signed || m_sig.isNull()) { TRETURN nullptr; } TRETURN m_sig.fingerprint(); } /** Try to locate the keys for all recipients */ void Mail::locateKeys_o () { TSTART; if (m_locate_in_progress) { /** XXX The strangest thing seems to happen here: In get_recipients the lookup for "AddressEntry" on an unresolved address might cause network traffic. So Outlook somehow "detaches" this call and keeps processing window messages while the call is running. So our do_delayed_locate might trigger a second locate. If we access the OOM in this call while we access the same object in the blocked "detached" call we crash. (T3931) After the window message is handled outlook retunrs in the original lookup. A better fix here might be a non recursive lock of the OOM. But I expect that if we lock the handling of the Windowmessage we might deadlock. */ log_debug ("%s:%s: Locate for %p already in progress.", SRCNAME, __func__, this); TRETURN; } m_locate_in_progress = true; Addressbook::check_o (this); if (opt.autoresolve) { // First update oom data to have recipients and sender updated. updateOOMData_o (); KeyCache::instance()->startLocateSecret (getSender_o ().c_str (), this); KeyCache::instance()->startLocate (getSender_o ().c_str (), this); KeyCache::instance()->startLocate (getCachedRecipientAddresses (), this); } autosecureCheck (); m_locate_in_progress = false; TRETURN; } bool Mail::isHTMLAlternative () const { TSTART; TRETURN m_is_html_alternative; } char * Mail::takeCachedHTMLBody () { TSTART; char *ret = m_cached_html_body; m_cached_html_body = nullptr; TRETURN ret; } char * Mail::takeCachedPlainBody () { TSTART; char *ret = m_cached_plain_body; m_cached_plain_body = nullptr; TRETURN ret; } int Mail::getCryptoFlags () const { TSTART; TRETURN m_crypto_flags; } void Mail::setNeedsEncrypt (bool value) { TSTART; m_needs_encrypt = value; TRETURN; } bool Mail::getNeedsEncrypt () const { TSTART; TRETURN m_needs_encrypt; } std::vector Mail::getCachedRecipients () { TSTART; TRETURN m_cached_recipients; } void Mail::setRecipients (const std::vector &recps) { TSTART; m_recipients_set = !recps.empty (); m_cached_recipients = recps; TRETURN; } std::vector Mail::getCachedRecipientAddresses () { TSTART; std::vector ret; for (const auto &recp: m_cached_recipients) { ret.push_back (recp.mbox()); } return ret; } void Mail::appendToInlineBody (const std::string &data) { TSTART; m_inline_body += data; TRETURN; } int Mail::inlineBodyToBody_o () { TSTART; if (!m_crypter) { log_error ("%s:%s: No crypter.", SRCNAME, __func__); TRETURN -1; } const auto body = m_crypter->get_inline_data (); if (body.empty()) { TRETURN 0; } /* For inline we always work with UTF-8 */ put_oom_int (m_mailitem, "InternetCodepage", 65001); int ret = put_oom_string (m_mailitem, "Body", body.c_str ()); TRETURN ret; } void Mail::updateCryptMAPI_m () { TSTART; log_debug ("%s:%s: Update crypt mapi", SRCNAME, __func__); - if (m_crypt_state != NeedsUpdateInMAPI) + if (m_crypt_state != OOMSynced) { log_debug ("%s:%s: invalid state %i", SRCNAME, __func__, m_crypt_state); TRETURN; } if (!m_crypter) { if (!m_mime_data.empty()) { log_debug ("%s:%s: Have override mime data creating dummy crypter", SRCNAME, __func__); m_crypter = std::shared_ptr (new CryptController (this, false, false, GpgME::UnknownProtocol)); } else { log_error ("%s:%s: No crypter.", SRCNAME, __func__); - m_crypt_state = NoCryptMail; + m_crypt_state = NotStarted; TRETURN; } } if (m_crypter->update_mail_mapi ()) { log_error ("%s:%s: Failed to update MAPI after crypt", SRCNAME, __func__); - m_crypt_state = NoCryptMail; + m_crypt_state = NotStarted; } else { - m_crypt_state = WantsSendMIME; + m_crypt_state = CryptFinished; } /** If sync we need the crypter in update_crypt_oom */ if (!isAsyncCryptDisabled ()) { // We don't need the crypter anymore. resetCrypter (); } TRETURN; } /** Checks in OOM if the body is either empty or contains the -----BEGIN tag. pair.first -> true if body starts with -----BEGIN pair.second -> true if body is empty. */ static std::pair has_crypt_or_empty_body_oom (Mail *mail) { TSTART; auto body = mail->getBody_o (); std::pair ret; ret.first = false; ret.second = false; ltrim (body); if (body.size() > 10 && !strncmp (body.c_str(), "-----BEGIN", 10)) { ret.first = true; TRETURN ret; } if (!body.size()) { ret.second = true; } else { log_data ("%s:%s: Body found in %p : \"%s\"", SRCNAME, __func__, mail, body.c_str ()); } TRETURN ret; } void Mail::updateCryptOOM_o () { TSTART; log_debug ("%s:%s: Update crypt oom for %p", SRCNAME, __func__, this); - if (m_crypt_state != NeedsUpdateInOOM) + if (m_crypt_state != BackendDone) { log_debug ("%s:%s: invalid state %i", SRCNAME, __func__, m_crypt_state); resetCrypter (); resetRecipients (); TRETURN; } if (getDoPGPInline ()) { if (inlineBodyToBody_o ()) { log_error ("%s:%s: Inline body to body failed %p.", SRCNAME, __func__, this); gpgol_bug (get_active_hwnd(), ERR_INLINE_BODY_TO_BODY); - m_crypt_state = NoCryptMail; + m_crypt_state = NotStarted; TRETURN; } } if (m_crypter->get_protocol () == GpgME::CMS && m_crypter->is_encrypter ()) { /* We put the PIDNameContentType headers here for exchange because this is the only way we found to inject the smime-type. */ if (put_pa_string (m_mailitem, PR_PIDNameContentType_DASL, "application/pkcs7-mime;smime-type=\"enveloped-data\";name=smime.p7m")) { log_debug ("%s:%s: Failed to put PIDNameContentType for %p.", SRCNAME, __func__, this); } } if (get_oom_int (m_mailitem, "BodyFormat") == 3) { log_dbg ("Changing body format from rich text to HTML to " "prevent winmail.dat"); put_oom_int (m_mailitem, "BodyFormat", 2); } /** When doing async update_crypt_mapi follows and needs the crypter. */ if (isAsyncCryptDisabled ()) { resetCrypter (); resetRecipients (); } const auto pair = has_crypt_or_empty_body_oom (this); if (pair.first) { log_debug ("%s:%s: Looks like inline body. You can pass %p.", SRCNAME, __func__, this); - m_crypt_state = WantsSendInline; + m_crypt_state = CryptFinished; TRETURN; } /* Draft encryption we do not want to wipe the oom but we have to modify it to trigger the wirte / second after write. */ if (m_is_draft_encrypt) { char *subject = get_oom_string (m_mailitem, "Subject"); put_oom_string (m_mailitem, "Subject", subject ? subject : ""); xfree (subject); } // We are in MIME land. Wipe the body. if (!m_is_draft_encrypt && wipe_o (true)) { log_debug ("%s:%s: Cancel send for %p.", SRCNAME, __func__, this); wchar_t *title = utf8_to_wchar (_("GpgOL: Encryption not possible!")); wchar_t *msg = utf8_to_wchar (_( "Outlook returned an error when trying to send the encrypted mail.\n\n" "Please restart Outlook and try again.\n\n" "If it still fails consider using an encrypted attachment or\n" "switching to PGP/Inline in GpgOL's options.")); MessageBoxW (get_active_hwnd(), msg, title, MB_ICONERROR | MB_OK); xfree (msg); xfree (title); - m_crypt_state = NoCryptMail; + m_crypt_state = NotStarted; TRETURN; } - m_crypt_state = NeedsSecondAfterWrite; + m_crypt_state = OOMUpdated; TRETURN; } void Mail::enableWindow () { TSTART; if (!m_window) { log_error ("%s:%s:enable window which was not disabled", SRCNAME, __func__); } log_debug ("%s:%s: enable window %p", SRCNAME, __func__, m_window); EnableWindow (m_window, TRUE); TRETURN; } void Mail::disableWindow_o () { TSTART; m_window = get_active_hwnd (); log_debug ("%s:%s: disable window %p", SRCNAME, __func__, m_window); EnableWindow (m_window, FALSE); TRETURN; } bool Mail::isActiveInlineResponse_o () { const auto subject = getSubject_o (); bool ret = false; LPDISPATCH app = GpgolAddin::get_instance ()->get_application (); if (!app) { TRACEPOINT; TRETURN false; } LPDISPATCH explorer = get_oom_object (app, "ActiveExplorer"); if (!explorer) { TRACEPOINT; TRETURN false; } LPDISPATCH inlineResponse = get_oom_object (explorer, "ActiveInlineResponse"); gpgol_release (explorer); if (!inlineResponse) { TRETURN false; } // We have inline response // Check if we are it. It's a bit naive but meh. Worst case // is that we think inline response too often and do sync // crypt where we could do async crypt. char * inlineSubject = get_oom_string (inlineResponse, "Subject"); gpgol_release (inlineResponse); if (inlineResponse && !subject.empty() && !strcmp (subject.c_str (), inlineSubject)) { log_debug ("%s:%s: Detected inline response for '%p'", SRCNAME, __func__, this); ret = true; } xfree (inlineSubject); TRETURN ret; } bool Mail::checkSyncCrypto_o () { TSTART; /* Async sending is known to cause instabilities. So we keep a hidden option to disable it. */ if (opt.sync_enc) { m_async_crypt_disabled = true; TRETURN m_async_crypt_disabled; } m_async_crypt_disabled = false; + if (m_is_draft_encrypt) + { + log_dbg ("Disabling async crypt because of draft encrypt."); + m_async_crypt_disabled = true; + TRETURN m_async_crypt_disabled; + } + const auto subject = getSubject_o (); /* Check for an empty subject. Otherwise the question for it might be hidden behind our overlay. */ if (subject.empty()) { log_debug ("%s:%s: Detected empty subject. " "Disabling async crypt due to T4150.", SRCNAME, __func__); m_async_crypt_disabled = true; TRETURN m_async_crypt_disabled; } LPDISPATCH attachments = get_oom_object (m_mailitem, "Attachments"); - if (attachments) + if (/* attachments */ false) { /* This is horrible. But. For some kinds of attachments (we got reports about Office attachments the write in the send event triggered by our crypto done code fails with an exception. There does not appear to be a detectable pattern when this happens. As we can't be sure and do not know for which attachments this really happens we do not use async crypt for any mails with attachments. :-/ Better be save (not crash) instead of nice (async). TODO: Figure this out. The log goes like this. We pass the send event. That triggers a write, which we pass. And then that fails. So it looks like moving to Outbox fails. Because we can save as much as we like before that. Using the IMessage::SubmitMessage MAPI interface works, but as it is unstable in our current implementation we do not want to use it. mailitem-events.cpp:Invoke: Passing send event for mime-encrypted message 12B7C6E0. application-events.cpp:Invoke: Unhandled Event: f002 mailitem-events.cpp:Invoke: Write : 0ED4D058 mailitem-events.cpp:Invoke: Passing write event. oomhelp.cpp:invoke_oom_method_with_parms_type: Method 'Send' invokation failed: 0x80020009 oomhelp.cpp:dump_excepinfo: Exception: wCode: 0x1000 wReserved: 0x0 source: Microsoft Outlook desc: The operation failed. The messaging interfaces have returned an unknown error. If the problem persists, restart Outlook. help: null helpCtx: 0x0 deferredFill: 00000000 scode: 0x80040119 */ int count = get_oom_int (attachments, "Count"); gpgol_release (attachments); if (count) { m_async_crypt_disabled = true; log_debug ("%s:%s: Detected attachments. " "Disabling async crypt due to T4131.", SRCNAME, __func__); TRETURN m_async_crypt_disabled; } } if (isActiveInlineResponse_o ()) { m_async_crypt_disabled = true; } TRETURN m_async_crypt_disabled; } // static Mail * Mail::getLastMail () { TSTART; if (!s_last_mail || !isValidPtr (s_last_mail)) { s_last_mail = nullptr; } TRETURN s_last_mail; } // static void Mail::clearLastMail () { TSTART; s_last_mail = nullptr; TRETURN; } // static void Mail::locateAllCryptoRecipients_o () { TSTART; gpgol_lock (&mail_map_lock); std::map::iterator it; auto mail_map_copy = s_mail_map; gpgol_unlock (&mail_map_lock); for (it = mail_map_copy.begin(); it != mail_map_copy.end(); ++it) { if (it->second->needs_crypto_m ()) { it->second->locateKeys_o (); } } TRETURN; } int Mail::removeAllAttachments_o () { TSTART; int ret = 0; LPDISPATCH attachments = get_oom_object (m_mailitem, "Attachments"); if (!attachments) { TRACEPOINT; TRETURN 0; } int count = get_oom_int (attachments, "Count"); LPDISPATCH to_delete[count]; /* Populate the array so that we don't get in an index mess */ for (int i = 1; i <= count; i++) { auto item_str = std::string("Item(") + std::to_string (i) + ")"; to_delete[i-1] = get_oom_object (attachments, item_str.c_str()); } gpgol_release (attachments); /* Now delete all attachments */ for (int i = 0; i < count; i++) { LPDISPATCH attachment = to_delete[i]; if (!attachment) { log_error ("%s:%s: No such attachment %i", SRCNAME, __func__, i); ret = -1; } /* Delete the attachments that are marked to delete */ if (invoke_oom_method (attachment, "Delete", NULL)) { log_error ("%s:%s: Deleting attachment %i", SRCNAME, __func__, i); ret = -1; } gpgol_release (attachment); } TRETURN ret; } int Mail::removeOurAttachments_o () { TSTART; LPDISPATCH attachments = get_oom_object (m_mailitem, "Attachments"); if (!attachments) { TRACEPOINT; TRETURN 0; } int count = get_oom_int (attachments, "Count"); LPDISPATCH to_delete[count]; int del_cnt = 0; for (int i = 1; i <= count; i++) { auto item_str = std::string("Item(") + std::to_string (i) + ")"; LPDISPATCH attachment = get_oom_object (attachments, item_str.c_str()); if (!attachment) { TRACEPOINT; continue; } attachtype_t att_type; if (get_pa_int (attachment, GPGOL_ATTACHTYPE_DASL, (int*) &att_type)) { /* Not our attachment. */ gpgol_release (attachment); continue; } if (att_type == ATTACHTYPE_PGPBODY || att_type == ATTACHTYPE_MOSS || att_type == ATTACHTYPE_MOSSTEMPL) { /* One of ours to delete. */ to_delete[del_cnt++] = attachment; /* Dont' release yet */ continue; } gpgol_release (attachment); } gpgol_release (attachments); int ret = 0; for (int i = 0; i < del_cnt; i++) { LPDISPATCH attachment = to_delete[i]; /* Delete the attachments that are marked to delete */ if (invoke_oom_method (attachment, "Delete", NULL)) { log_error ("%s:%s: Error: deleting attachment %i", SRCNAME, __func__, i); ret = -1; } gpgol_release (attachment); } TRETURN ret; } /* We are very verbose because if we fail it might mean that we have leaked plaintext -> critical. */ bool Mail::hasCryptedOrEmptyBody_o () { TSTART; const auto pair = has_crypt_or_empty_body_oom (this); if (pair.first /* encrypted marker */) { log_debug ("%s:%s: Crypt Marker detected in OOM body. Return true %p.", SRCNAME, __func__, this); TRETURN true; } if (!pair.second) { log_debug ("%s:%s: Unexpected content detected. Return false %p.", SRCNAME, __func__, this); TRETURN false; } // Pair second == true (is empty) can happen on OOM error. LPMESSAGE message = get_oom_base_message (m_mailitem); if (!message && pair.second) { if (message) { gpgol_release (message); } TRETURN true; } size_t r_nbytes = 0; char *mapi_body = mapi_get_body (message, &r_nbytes); gpgol_release (message); if (!mapi_body || !r_nbytes) { // Body or bytes are null. we are empty. xfree (mapi_body); log_debug ("%s:%s: MAPI error or empty message. Return true. %p.", SRCNAME, __func__, this); TRETURN true; } if (r_nbytes > 10 && !strncmp (mapi_body, "-----BEGIN", 10)) { // Body is crypt. log_debug ("%s:%s: MAPI Crypt marker detected. Return true. %p.", SRCNAME, __func__, this); xfree (mapi_body); TRETURN true; } xfree (mapi_body); log_debug ("%s:%s: Found mapi body. Return false. %p.", SRCNAME, __func__, this); TRETURN false; } std::string Mail::getVerificationResultDump () { TSTART; std::stringstream ss; ss << m_verify_result; TRETURN ss.str(); } void Mail::setBlockStatus_m () { TSTART; SPropValue prop; LPMESSAGE message = get_oom_base_message (m_mailitem); prop.ulPropTag = PR_BLOCK_STATUS; prop.Value.l = 1; HRESULT hr = message->SetProps (1, &prop, NULL); if (hr) { log_error ("%s:%s: can't set block value: hr=%#lx\n", SRCNAME, __func__, hr); } gpgol_release (message); TRETURN; } void Mail::setBlockHTML (bool value) { TSTART; m_block_html = value; TRETURN; } void Mail::incrementLocateCount () { TSTART; m_locate_count++; TRETURN; } void Mail::decrementLocateCount () { TSTART; m_locate_count--; if (m_locate_count < 0) { log_error ("%s:%s: locate count mismatch.", SRCNAME, __func__); m_locate_count = 0; } if (!m_locate_count) { autosecureCheck (); } TRETURN; } void Mail::autosecureCheck () { TSTART; if (!opt.autosecure || !opt.autoresolve || m_manual_crypto_opts || m_locate_count) { TRETURN; } bool ret = KeyCache::instance()->isMailResolvable (this); log_debug ("%s:%s: status %i", SRCNAME, __func__, ret); /* As we are safe to call at any time, because we need * to be triggered by the locator threads finishing we * need to actually set the draft info flags in * the ui thread. */ do_in_ui_thread_async (ret ? DO_AUTO_SECURE : DONT_AUTO_SECURE, this); TRETURN; } void Mail::setDoAutosecure_m (bool value) { TSTART; TRACEPOINT; LPMESSAGE msg = get_oom_base_message (m_mailitem); if (!msg) { TRACEPOINT; TRETURN; } /* We need to set a uuid so that autosecure can be disabled manually */ setUUID_o (); int old_flags = get_gpgol_draft_info_flags (msg); if (old_flags && m_first_autosecure_check && /* Someone with always sign and autosecure active * will want to get autoencryption. */ !(old_flags == 2 && opt.sign_default)) { /* They were set explicily before us. This can be * because they were a draft (which is bad) or * because they are a reply/forward to a crypto mail * or because there are conflicting settings. */ log_debug ("%s:%s: Mail %p had already flags set.", SRCNAME, __func__, m_mailitem); m_first_autosecure_check = false; m_manual_crypto_opts = true; gpgol_release (msg); TRETURN; } m_first_autosecure_check = false; set_gpgol_draft_info_flags (msg, value ? 3 : opt.sign_default ? 2 : 0); gpgol_release (msg); gpgoladdin_invalidate_ui(); TRETURN; } bool Mail::decryptedSuccessfully () const { return m_decrypt_result.isNull() || !m_decrypt_result.error(); } void Mail::installFolderEventHandler_o() { TSTART; TRACEPOINT; LPDISPATCH folder = get_oom_object (m_mailitem, "Parent"); if (!folder) { TRACEPOINT; TRETURN; } char *objName = get_object_name (folder); if (!objName || strcmp (objName, "MAPIFolder")) { log_debug ("%s:%s: Mail %p parent is not a mapi folder.", SRCNAME, __func__, m_mailitem); xfree (objName); gpgol_release (folder); TRETURN; } xfree (objName); char *path = get_oom_string (folder, "FullFolderPath"); if (!path) { TRACEPOINT; path = get_oom_string (folder, "FolderPath"); } if (!path) { log_error ("%s:%s: Mail %p parent has no folder path.", SRCNAME, __func__, m_mailitem); gpgol_release (folder); TRETURN; } std::string strPath (path); xfree (path); if (s_folder_events_map.find (strPath) == s_folder_events_map.end()) { log_debug ("%s:%s: Install folder events watcher for %s.", SRCNAME, __func__, anonstr (strPath.c_str())); const auto sink = install_FolderEvents_sink (folder); s_folder_events_map.insert (std::make_pair (strPath, sink)); } /* Folder already registered */ gpgol_release (folder); TRETURN; } void Mail::refCurrentItem() { TSTART; if (m_currentItemRef) { log_debug ("%s:%s: Current item multi ref. Bug?", SRCNAME, __func__); TRETURN; } /* This prevents a crash in Outlook 2013 when sending a mail as it * would unload too early. * * As it didn't crash when the mail was opened in Outlook Spy this * mimics that the mail is inspected somewhere else. */ m_currentItemRef = get_oom_object (m_mailitem, "GetInspector.CurrentItem"); TRETURN; } void Mail::releaseCurrentItem() { TSTART; if (!m_currentItemRef) { TRETURN; } log_oom ("%s:%s: releasing CurrentItem ref %p", SRCNAME, __func__, m_currentItemRef); LPDISPATCH tmp = m_currentItemRef; m_currentItemRef = nullptr; /* This can cause our destruction */ gpgol_release (tmp); TRETURN; } void Mail::decryptPermanently_o() { TSTART; if (!m_needs_wipe) { log_debug ("%s:%s: Mail does not yet need wipe. Called to early?", SRCNAME, __func__); TRETURN; } if (!m_decrypt_result.isNull() && m_decrypt_result.error()) { log_debug ("%s:%s: Decrypt result had error. Can't decrypt permanently.", SRCNAME, __func__); TRETURN; } /* Remove the existing categories */ removeCategories_o (); /* Drop our state variables */ m_decrypt_result = GpgME::DecryptionResult(); m_verify_result = GpgME::VerificationResult(); m_needs_wipe = false; m_processed = false; m_is_smime = false; m_type = MSGTYPE_UNKNOWN; /* Remove our own attachments */ removeOurAttachments_o (); updateSigstate(); auto msg = MAKE_SHARED (get_oom_base_message (m_mailitem)); if (!msg) { STRANGEPOINT; TRETURN; } mapi_delete_gpgol_tags ((LPMESSAGE)msg.get()); /* The content type is wrong now. We remove it.*/ mapi_set_content_type ((LPMESSAGE)msg.get(), m_dec_content_type.empty () ? "text/plain" : m_dec_content_type.c_str ()); mapi_set_mesage_class ((LPMESSAGE)msg.get(), "IPM.Note"); if (invoke_oom_method (m_mailitem, "Save", NULL)) { log_error ("Failed to save decrypted mail: %p ", m_mailitem); } log_debug ("%s:%s: Delayed invalidate to update sigstate after perm dec.", SRCNAME, __func__); CloseHandle(CreateThread (NULL, 0, delayed_invalidate_ui, (LPVOID) 300, 0, NULL)); TRETURN; } int Mail::prepareCrypto_o () { TSTART; int olFlags = get_oom_crypto_flags (m_mailitem); log_dbg ("Outlook internal crypto flags are: %i", olFlags); if (olFlags) { std::string question = _("Should GpgOL override Outlook and continue " "to process the message?"); std::string vs_warning; if (in_de_vs_mode ()) { /* TRANSLATORS %s is compliance name like VS-NfD */ vs_warning = asprintf_s (_("Note: For %s communication you have to select Yes."), de_vs_name ()) + std::string ("\n\n"); } std::string msg = _("A crypto operation was selected both using " "Outlooks internal crypto and with GpgOL / GnuPG.") + std::string("\n\n") + vs_warning + question; int yesno = gpgol_message_box (get_active_hwnd (), msg.c_str (), _("Conflicting crypto settings"), MB_YESNO); if (yesno == IDYES) { log_dbg ("Overriding Outlook internal crypto."); } else { log_dbg ("Aborting send."); TRETURN -1; } } put_pa_int (m_mailitem, PR_SECURITY_FLAGS_DASL, 0); // Check inline response state to fill out asynccryptdisabled. checkSyncCrypto_o (); if (!isAsyncCryptDisabled()) { /* Obtain a reference of the current item. This prevents * an early unload which would crash Outlook 2013 * * As it didn't crash when the mail was opened in Outlook Spy this * mimics that the mail is inspected somewhere else. */ refCurrentItem (); } // First contact with a mail to encrypt update // state and oom data. updateOOMData_o (true); - setCryptState (Mail::NeedsFirstAfterWrite); + setCryptState (Mail::DataCollected); TRETURN 0; } /* Printing happens in two steps. First a Mail is loaded after the BeforePrint event, then it is loaded a second time when the actual print happens. We have to catch both. This functions looks over all mails and checks if one is currently printing. If so we compare our EntryID's and if they match. Bingo, we are printing, too.*/ bool Mail::checkIfMailMightBePrinting_o () { const auto ourID = get_oom_string_s (m_mailitem, "EntryID"); if (ourID.empty ()) { log_error ("%s:%s: Mail %p has no accessible EntryID", SRCNAME, __func__, this); return false; } if (s_entry_ids_printing.find (ourID) != s_entry_ids_printing.end ()) { log_dbg ("Found %s in printing map. This might be a printing child.", ourID.c_str ()); return true; } return false; } void Mail::setSigningKeys (const std::vector &keys) { m_resolved_signing_keys = keys; } std::vector Mail::getSigningKeys () const { return m_resolved_signing_keys; } LPDISPATCH Mail::copy () { TSTART; VARIANT result; VariantInit (&result); if (g_mail_copy_triggerer) { log_err ("BUG: Copy already in progress."); TRETURN nullptr; } g_mail_copy_triggerer = this; int err = invoke_oom_method (m_mailitem, "Copy", &result); if (err) { log_error ("%s:%s Failed to copy mail.", SRCNAME, __func__); TRETURN nullptr; } if (result.vt != VT_DISPATCH || !result.pdispVal) { log_error ("%s:%s Unexpected result type %x.", SRCNAME, __func__, result.vt); TRETURN nullptr; } return result.pdispVal; } static std::vector getKeysForProtocol (const std::vector keys, GpgME::Protocol proto) { std::vector ret; std::copy_if (keys.begin(), keys.end(), std::back_inserter (ret), [proto] (const auto &k) { return k.protocol () == proto; }); return ret; } /* A callback of a copied mail to update our Mail objects after the split */ void Mail::splitCopyMailCallback (Mail *copied_mail) { TSTART; log_dbg ("Copy mail callback reached with mail %p", copied_mail); g_mail_copy_triggerer = nullptr; copied_mail->setSplitCopy (true); std::vector newRecipientsForCopy; std::vector newRecipientsForUs; GpgME::Protocol copyProtocol = GpgME::UnknownProtocol; /* Now split out recipients */ bool bccFound = false; bool normalRecipientsFound = false; log_debug ("Dump before split."); Recipient::dump (m_cached_recipients); bool failureToRemoveBCC = false; for (const auto &recp: m_cached_recipients) { /* We want to always include the originator */ if (recp.type () == Recipient::olOriginator) { newRecipientsForCopy.push_back (recp); newRecipientsForUs.push_back (recp); } else if (recp.type () == Recipient::olBCC && !bccFound) { const auto bccKeys = recp.keys (); if (bccKeys.empty()) { log_err ("Empty keylist for recipient!"); continue; } const auto pgpKeys = getKeysForProtocol (bccKeys, GpgME::OpenPGP); const auto cmsKeys = getKeysForProtocol (bccKeys, GpgME::CMS); if (!pgpKeys.empty () && !cmsKeys.empty()) { log_dbg ("Detected BCC Recipient with both PGP and CMS keys." " splitting recipient."); /* Copy it and keep it as BCC recipient with only PGP keys */ auto splitRecipient = recp; splitRecipient.setKeys (pgpKeys); newRecipientsForUs.push_back (splitRecipient); /* For the copy use the CMS keys */ auto splitRecipient2 = recp; splitRecipient2.setKeys (cmsKeys); newRecipientsForCopy.push_back (splitRecipient2); copyProtocol = GpgME::CMS; } else { newRecipientsForCopy.push_back (recp); /* Our keylist is of a single protocol so we can take that. */ copyProtocol = bccKeys[0].protocol (); log_dbg ("Recipient '%s' is BCC. Gets its own mail with " "protocol: %s", anonstr (recp.mbox().c_str ()), to_cstr (copyProtocol)); } int err = remove_oom_recipient (m_mailitem, recp.mbox()); if (err) { log_dbg ("Failure to remove recipient '%s' possibly a group." "overwriting with our recipients.", anonstr (recp.mbox ().c_str ())); failureToRemoveBCC = true; } bccFound = true; } else { newRecipientsForUs.push_back (recp); } } if (failureToRemoveBCC) { set_oom_recipients (m_mailitem, newRecipientsForUs); } setRecipients(newRecipientsForUs); copied_mail->setRecipients(newRecipientsForCopy); set_oom_recipients (copied_mail->item(), newRecipientsForCopy); if (copyProtocol != GpgME::UnknownProtocol) { copied_mail->setSigningKeys (getKeysForProtocol (m_resolved_signing_keys, copyProtocol)); } else { copied_mail->setSigningKeys (m_resolved_signing_keys); } if (!bccFound) { /* TODO: Split more for S/MIME OpenPGP Mix. */ } if (bccFound && normalRecipientsFound) { /* Recurse */ log_dbg ("Both normal and hidden recipients found. " "Creating another copy."); copy (); TRETURN; } } void Mail::splitAndSend_o () { TSTART; log_dbg ("Split and send for: %p", this); LPDISPATCH copied_mailitem = copy (); if (!copied_mailitem) { log_err ("Failed to copy mail. Aboring."); TRETURN; } /* This triggers the copyMailCallback in the write event */ invoke_oom_method (copied_mailitem, "Send", nullptr); invoke_oom_method (m_mailitem, "Send", nullptr); TRETURN; } void Mail::resetCrypter () { m_crypter = nullptr; } void Mail::resetRecipients () { /* Clear recipient data */ m_cached_recipients.clear (); m_resolved_signing_keys.clear (); m_recipients_set = false; } void Mail::setSplitCopy (bool val) { m_is_split_copy = val; } bool Mail::isSplitCopy () const { return m_is_split_copy; } int Mail::buildProtectedHeaders_o () { TSTART; std::stringstream ss; std::vector toRecps; std::vector ccRecps; std::vector bccRecps; ss << "protected-headers=\"v1\"\r\n"; for (const auto &recp: m_cached_recipients) { if (recp.type() == Recipient::olCC) { ccRecps.push_back (recp.encodedDisplayName ()); } else if (recp.type() == Recipient::olTo) { toRecps.push_back (recp.encodedDisplayName ()); } else if (recp.type() == Recipient::olOriginator) { ss << "From: " << recp.encodedDisplayName () << "\r\n"; } } std::string buf; if (toRecps.size ()) { join(toRecps, ";", buf); ss << "To: " << buf << "\r\n"; } if (ccRecps.size ()) { join(toRecps, ";", buf); ss << "CC: " << buf << "\r\n"; } if (opt.encryptSubject) { char *subject = get_oom_string (m_mailitem, "Subject"); if (subject) { char *encodedSubject = utf8_to_rfc2047b (subject); if (encodedSubject) { ss << "Subject: " << encodedSubject << "\r\n"; } xfree (encodedSubject); } if (!put_oom_string (m_mailitem, "Subject", "...")) { STRANGEPOINT; TRETURN -1; } xfree (subject); } m_protected_headers = ss.str (); TRETURN 0; } void Mail::setProtectedHeaders (const std::string &hdr) { m_protected_headers = hdr; } std::string Mail::protectedHeaders () const { return m_protected_headers; } header_info_s Mail::headerInfo () const { return m_header_info; } const std::string& Mail::getOriginalBody () const { return m_orig_body; } + +int +Mail::isMultipartRelated () const +{ + int related = false; + int mixed = false; + for (const auto attach: m_plain_attachments) + { + if (attach->get_content_id ().size()) + { + related = 1; + } + else + { + mixed = 1; + } + } + return related + mixed; +} + +std::vector > +Mail::plainAttachments () const +{ + return m_plain_attachments; +} diff --git a/src/mail.h b/src/mail.h index a07277d..b0ff0e4 100644 --- a/src/mail.h +++ b/src/mail.h @@ -1,781 +1,789 @@ /* @file mail.h * @brief High level class to work with Outlook Mailitems. * * Copyright (C) 2015, 2016 by Bundesamt für Sicherheit in der Informationstechnik * Software engineering by Intevation GmbH * * This file is part of GpgOL. * * GpgOL is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * GpgOL is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, see . */ #ifndef MAIL_H #define MAIL_H #include "oomhelp.h" #include "mapihelp.h" #include "gpgme++/verificationresult.h" #include "gpgme++/decryptionresult.h" #include "gpgme++/key.h" #include class ParseController; class CryptController; class Attachment; class Recipient; /** @brief Data wrapper around a mailitem. * * This class is intended to bundle all that we know about * a Mail. Due to the restrictions in Outlook we sometimes may * need additional information that is not available at the time * like the sender address of an exchange account in the afterWrite * event. * * This class bundles such information and also provides a way to * access the event handler of a mail. * * Naming conventions of the suffixes: * _o functions that work on OOM and possibly also MAPI. * _m functions that work on MAPI. * _s functions that only work on internal data and are safe to call * from any thread. * * O and M functions _must_ only be called from the main thread. Use * a WindowMessage to signal the Main thread. But be wary. A WindowMessage * might be handled while an OOM call in the main thread waits for completion. * * An example for this is how update_oom_data can work: * * Main Thread: * call update_oom_data * └> internally invokes an OOM function that might do network access e.g. * to connect to the exchange server to fetch the address. * * Counterintutively the Main thread does not return from that function or * blocks for it's completion but handles windowmessages. * * After a windowmessage was handled and if the OOM invocation is * completed the invocation returns and normal execution continues. * * So if the window message handler's includes for example * also a call to lookup recipients we crash. Note that it's usually * safe to do OOM / MAPI calls from a window message. * * * While this seems impossible, remember that we do not work directly * with functions but everything is handled through COM. Without this * logic Outlook would probably become unusable because as any long running * call to the OOM would block it completely and freeze the UI. * (no windowmessages handled). * * So be wary when accessing the OOM from a Window Message. */ class Mail { public: enum CryptState { - NoCryptMail, - NeedsFirstAfterWrite, - NeedsActualCrypt, - NeedsUpdateInOOM, - NeedsSecondAfterWrite, - NeedsUpdateInMAPI, - WantsSendInline, - WantsSendMIME, + NotStarted, /* The mail has not been encrypted or signed by us */ + DataCollected, /* Data from the OOM / MAPI has been collected */ + BackendDone, /* GnuPG is finished */ + OOMUpdated, /* GnuPG Data has been written to OOM */ + OOMSynced, /* OOM has been synced internally but not MAPI */ + CryptFinished, /* The mail is signed and or encrypted and can be sent */ }; /** @brief Construct a mail object for the item. * * This also installs the event sink for this item. * * The mail object takes ownership of the mailitem * reference. Do not Release it! */ Mail (LPDISPATCH mailitem); ~Mail (); /** @brief looks for existing Mail objects for the OOM mailitem. @returns A reference to an existing mailitem or NULL in case none could be found. */ static Mail* getMailForItem (LPDISPATCH mailitem); /** @brief looks for existing Mail objects in the uuid map. Only objects for which set_uid has been called can be found in the uid map. Get the Unique ID of a mailitem thorugh get_unique_id @returns A reference to an existing mailitem or NULL in case none could be found. */ static Mail* getMailForUUID (const char *uuid); /** @brief Get the last created mail. @returns A reference to the last created mail or null. */ static Mail* getLastMail (); /** @brief voids the last mail variable. */ static void clearLastMail (); /** @brief Lock mail deletion. Mails are heavily accessed multi threaded. E.g. when locating keys. Due to bad timing it would be possible that between a check for "is_valid_ptr" to see if a map is still valid and the usage of the mail a delete would happen. This lock can be used to prevent that. Changes made to the mail will of course have no effect as the mail is already in the process of beeing unloaded. And calls that access MAPI or OOM still might crash. But this at least gurantees that the member variables of the mail exist while the lock is taken. Use it in your thread like: Mail::lockDelete (); Mail::isValidPtr (mail); mail->set_or_check_something (); Mail::unlockDelete (); Still be carefull when it is a mapi or oom function. */ static void lockDelete (); static void unlockDelete (); /** @brief looks for existing Mail objects. @returns A reference to an existing mailitem or NULL in case none could be found. Can be used to check if a mail object was destroyed. */ static bool isValidPtr (const Mail *mail); /** @brief wipe the plaintext from all known Mail objects. * * This is intended as a "cleanup" call to be done on unload * to avoid leaking plaintext in case we are deactivated while * some mails still have their plaintext inserted. * * @returns the number of errors that occured. */ static int wipeAllMails_o (); /** @brief revert all known Mail objects. * * Similar to wipe but works on MAPI to revert our attachment * dance and restore an original MIME mail. * * @returns the number of errors that occured. */ static int revertAllMails_o (); /** @brief close all known Mail objects. * * Close our mail with discard changes set to true. * This discards the plaintext / attachments. Afterwards * it calls save if neccessary to sync back the collected * property changes. * * This is the nicest of our three "Clean plaintext" * functions. Will fallback to revert if closing fails. * Closed mails are deleted. * * @returns the number of errors that occured. */ static int closeAllMails_o (); /** @brief closes the inspector for a mail * * @returns true on success. */ static int closeInspector_o (Mail *mail); /** Call close with discard changes to discard plaintext. returns the value of the oom close call. This may have delete the mail if the close triggers an unload. Set restoreSMIMEClass to resotre the S/MIME Message class IPM.Note.SMIME etc. for S/MIME mails. */ int close (bool restoreSMIMEClass = true); /** @brief locate recipients for all crypto mails * * To avoid lookups of recipients for non crypto mails we only * locate keys when a crypto action is already selected. * * As the user can do this after recipients were added but * we don't know for which mail the crypt button was triggered. * we march over all mails and if they are crypto mails we check * that the recipents were located. */ static void locateAllCryptoRecipients_o (); /** @brief Reference to the mailitem. Do not Release! */ LPDISPATCH item () { return m_mailitem; } /** @brief Pre process the message. Ususally to be called from BeforeRead. * * This function assumes that the base message interface can be accessed * and calles the MAPI Message handling which changes the message class * to enable our own handling. * * @returns 0 on success. */ int preProcessMessage_m (); /** @brief Decrypt / Verify the mail. * * Sets the needs_wipe and was_encrypted variable. * * @returns 0 on success. */ int decryptVerify_o (); /** @brief start crypto operations as selected by the user. * * Initiates the crypto operations according to the gpgol * draft info flags. * * @returns 0 on success. */ int encryptSignStart_o (); /** @brief Necessary crypto operations were completed successfully. */ bool wasCryptoSuccessful_m () { return m_crypt_successful || !needs_crypto_m (); } /** @brief Message should be encrypted and or signed. 0: No 1: Encrypt 2: Sign 3: Encrypt & Sign */ int needs_crypto_m () const; /** @brief wipe the plaintext from the message and encrypt attachments. * * @returns 0 on success; */ int wipe_o (bool force = false); /** @brief revert the message to the original mail before our changes. * * @returns 0 on success; */ int revert_o (); /** @brief update some data collected from the oom * * This updates cached values from the OOM that are not available * in MAPI events like after Write. * * For Exchange 2013 at least we don't have any other way to get the * senders SMTP address then through the object model. So we have to * store the sender address for later events that do not allow us to * access the OOM but enable us to work with the underlying MAPI structure. * * for_encryption can be used to override that data should be collected * for encryption. Otherwise it depends on "isCryptoMail" to decide which * data should be collected. * * It also updated the is_html_alternative value. * * @returns 0 on success */ int updateOOMData_o (bool for_encryption = false); /** @brief get sender SMTP address (UTF-8 encoded). * * If the sender address has not been set through update_sender this * calls update_sender before returning the sender. * * @returns A reference to the utf8 sender address. Or an empty string. */ std::string getSender_o (); /** @brief get sender SMTP address (UTF-8 encoded). * * Like get_sender but ensures not to touch oom or mapi * * @returns A reference to the utf8 sender address. Or an empty string. */ std::string getSender () const; /** @brief get the subject string (UTF-8 encoded). * * @returns the subject or an empty string. */ std::string getSubject_o () const; /** @brief Is this a crypto mail handled by gpgol. * * Calling this is only valid after a message has been processed. * * @returns true if the mail was either signed or encrypted and we processed * it. */ bool isCryptoMail () const; /** @brief This mail needs to be actually written. * * @returns true if the next write event should not be canceled. */ bool needsSave () { return m_needs_save; } /** @brief set the needs save state. */ void setNeedsSave (bool val) { m_needs_save = val; } /** @brief is this mail an S/MIME mail. * * @returns true for smime messages. */ bool isSMIME_m (); /* Same as above but does not touch MAPI. isSMIME_m needs to be called before this can return a true value. */ bool isSMIME () const; /** @brief get the associated parser. only valid while the actual parsing happens. */ std::shared_ptr parser () { return m_parser; } /** @brief get the associated cryptcontroller. only valid while the crypting happens. */ std::shared_ptr cryper () { return m_crypter; } /** To be called from outside once the paser was done. In Qt this would be a slot that is called once it is finished we hack around that a bit by calling it from our windowmessages handler. Parsing done can be called twice, once for the preview of a signed message and once when everything is finished. */ void parsingDone_o (bool is_preview = false); /** Returns true if the mail was verified and has at least one signature. Regardless of the validity of the mail */ bool isSigned () const; /** Returns true if the mail is encrypted to at least one recipient. Regardless if it could be decrypted. */ bool isEncrypted () const; /** Are we "green" */ bool isValidSig () const; /** Get UID gets UniqueID property of this mail. Returns an empty string if the uid was not set with set uid.*/ const std::string & getUUID () const { return m_uuid; } /** Returns 0 on success if the mail has a uid alrady or sets the uid. Setting only succeeds if the OOM is currently accessible. Returns -1 on error. */ int setUUID_o (); /** Returns a localized string describing in one or two words the crypto status of this mail. */ std::string getCryptoSummary () const; /** Returns a localized string describing the detailed crypto state of this mail. */ std::string getCryptoDetails_o (); /** Returns a localized string describing a one line summary of the crypto state. */ std::string getCryptoOneLine () const; /** Get the icon id of the appropiate icon for this mail */ int getCryptoIconID () const; /** Get the fingerprint of an associated signature or null if it is not signed. */ const char *getSigFpr () const; /** Remove all categories of this mail */ void removeCategories_o (); /** Get the body of the mail */ std::string getBody_o () const; /** Try to locate the keys for all recipients. This also triggers the Addressbook integration, which we treat as locate jobs. */ void locateKeys_o (); /** State variable to check if a close was triggerd by us. */ void setCloseTriggered (bool value); bool getCloseTriggered () const; /** Check if the mail should be sent as html alternative mail. Only valid if update_oom_data was called before. */ bool isHTMLAlternative () const; /** Get the html body. It is updated in update_oom_data. Caller takes ownership of the string and has to free it. */ char *takeCachedHTMLBody (); /** Get the plain body. It is updated in update_oom_data. Caller takes ownership of the string and has to free it. */ char *takeCachedPlainBody (); /** Get the cached recipients. It is updated in update_oom_data.*/ std::vector getCachedRecipients (); /** Only get the Mail addresses from the recipient objects. */ std::vector getCachedRecipientAddresses (); /** Set an override for recipients. If they have keys the keys will not be resolved again. */ void setRecipients (const std::vector &recps); /** Get the recipients. */ std::vector getRecipients_o () const; /** Returns 1 if the mail was encrypted, 2 if signed, 3 if both. Only valid after decrypt_verify. */ int getCryptoFlags () const; /** Returns true if the mail should be encrypted in the after write event. */ bool getNeedsEncrypt () const; void setNeedsEncrypt (bool val); /** Gets the level of the signature. See: https://wiki.gnupg.org/EasyGpg2016/AutomatedEncryption for a definition of the levels. */ int get_signature_level () const; /** Check if all attachments are hidden and show a warning message appropiate to the crypto state if necessary. If silent is true it will not show a warning but silently remove the bad attachments. */ int checkAttachments_o (bool silent); /** Check if the mail should be encrypted "inline" */ bool getDoPGPInline () const {return m_do_inline;} /** Check if the mail should be encrypted "inline" */ void setDoPGPInline (bool value) {m_do_inline = value;} /** Append data to a cached inline body. Helper to do this on MAPI level and later add it through OOM */ void appendToInlineBody (const std::string &data); /** Set the inline body as OOM body property. */ int inlineBodyToBody_o (); /** Get the crypt state */ CryptState cryptState () const {return m_crypt_state;} /** Set the crypt state */ void setCryptState (CryptState state) {m_crypt_state = state;} /** Update MAPI data after encryption. */ void updateCryptMAPI_m (); /** Update OOM data after encryption. Checks for plain text leaks and does not advance crypt state if body can't be cleaned. */ void updateCryptOOM_o (); /** Enable / Disable the window of this mail. When the window gets disabled the handle is stored for a later enable. */ void disableWindow_o (); void enableWindow (); /** Determine if the mail is an inline response. Call check_inline_response first to update the state from the OOM. We need synchronous encryption for inline responses. */ bool isAsyncCryptDisabled () { return m_async_crypt_disabled; } /** Check through OOM if the current mail is an inline response. Meaning editable in the message list. */ bool isActiveInlineResponse_o (); /* Check if we can't do async crypto. E.g. for inline responses. Caches the state which can then be queried through isAsyncCryptDisabled; */ bool checkSyncCrypto_o (); /** Get the window for the mail. Caution! This is only really valid in the time that the window is disabled. Use with care and can be null or invalid. */ HWND getWindow () { return m_window; } /** Cleanup any attached crypter objects. Useful on error. */ void resetCrypter (); /** Set special crypto mime data that should be used as the mime structure when sending. */ void setOverrideMIMEData (const std::string &data) {m_mime_data = data;} /** Get the mime data that should be used when sending. */ std::string get_override_mime_data () const { return m_mime_data; } bool hasOverrideMimeData() const { return !m_mime_data.empty(); } /** Set if this is a forward of a crypto mail. */ void setIsForwardedCryptoMail (bool value) { m_is_forwarded_crypto_mail = value; } bool is_forwarded_crypto_mail () { return m_is_forwarded_crypto_mail; } /** Remove the hidden GpgOL attachments. This is needed when forwarding without encryption so that our attachments are not included in the forward. Returns 0 on success. Works in OOM. */ int removeOurAttachments_o (); /** Remove all attachments. Including our own. This is needed for forwarding of unsigned S/MIME mails (Efail). Returns 0 on success. Works in OOM. */ int removeAllAttachments_o (); /** Check both OOM and MAPI if the body is either empty or encrypted. Won't abort on OOM or MAPI errors, so it can be used in both states. But will return false if a body was detected or in the OOM the MAPI Base Message. This is intended as a saveguard before sending a mail. This function should not be used to detected the necessity of encryption and is only an extra check to catch unexpected errors. */ bool hasCryptedOrEmptyBody_o (); void updateBody_o (bool is_preview); /** Update information from protected headers in OOM */ void updateHeaders_o (); /** Set if this mail looks like the send again of a crypto mail. This will mean that after it is decrypted it is treated like an unencrypted mail so that it can be encrypted again or sent unencrypted. */ void setIsSendAgain (bool value) { m_is_send_again = value; } /* Attachment removal state variables. */ bool attachmentRemoveWarningDisabled () { return m_disable_att_remove_warning; } /* Gets the string dump of the verification result. */ std::string getVerificationResultDump (); /* Block loading HTML content */ void setBlockHTML (bool value); bool isBlockHTML () const { return m_block_html; } /* Remove automatic loading of HTML references setting. */ void setBlockStatus_m (); /* Crypto options (sign/encrypt) have been set manually. */ void setCryptoSelectedManually (bool v) { m_manual_crypto_opts = v; } // bool is_crypto_selected_manually () const { return m_manual_crypto_opts; } /* Reference that a resolver thread is running for this mail. */ void incrementLocateCount (); /* To be called when a resolver thread is done. If there are no running resolver threads we can check the recipients to see if we should toggle / untoggle the secure state. */ void decrementLocateCount (); /* Check if the keys can be resolved automatically and trigger * setting the crypto flags accordingly. */ void autosecureCheck (); /* Set if a mail should be secured (encrypted and signed) * * Only save to call from a place that may access mapi. */ void setDoAutosecure_m (bool value); /* Install an event handler for the folder of this mail. */ void installFolderEventHandler_o (); /* Marker for a "Move" of this mail */ bool passWrite () { return m_pass_write; } void setPassWrite(bool value) { m_pass_write = value; } /* Releases the current item ref obtained in update oom data */ void releaseCurrentItem (); /* Gets an additional reference for GetInspector.CurrentItem */ void refCurrentItem (); /* Get the storeID for this mail */ std::string storeID() { return m_store_id; } /* Remove encryption permanently. */ void decryptPermanently_o (); /* Prepare for encrypt / sign. Updates data. */ int prepareCrypto_o (); /* State variable to check if we are about to encrypt a draft. */ void setIsDraftEncrypt (bool value) { m_is_draft_encrypt = value; } bool isDraftEncrypt () { return m_is_draft_encrypt; } /* Was this mail decrypted without error. Also returns true if the mail was not encrypted. */ bool decryptedSuccessfully () const; /* The mail should be decrypted again after the next * encryption. So that we can save it for example and * still work on it. */ void setDecryptAgain (bool value) { m_decrypt_again = value; } bool isDecryptAgain () const { return m_decrypt_again; } /* The type of the message. */ msgtype_t msgtype () const { return m_type; } /* The mail was loaded after we have seen a BeforePrint event. */ void setIsPrint (bool value) { m_printing = value; } bool isPrint () const { return m_printing; } /* Update the catgories (encrypted / trust in) based on the crypt results. */ void updateCategories_o (); /* Set the signing key for the mail. This overrides any other key resolution. */ void setSigningKeys (const std::vector &keys); std::vector getSigningKeys () const; /* Set the recipients for this mail. If they all have keys set then this overrides the cryptcontroller resolutions*/ void setRecipients (const std::vector &keys); /* Split a mail according to recipients, hidden, different protocols and so on and then send the multiple mails. */ void splitAndSend_o (); /* A callback for another mail that was created as a split from us. */ void splitCopyMailCallback (Mail *copy); /* Reset recipient and resolved key data. */ void resetRecipients (); /* Returns true if a mail is a copy that was split of a different mail. */ bool isSplitCopy () const; /* Setter for isSplitCopy */ void setSplitCopy (bool val); /* Set protected headers data */ void setProtectedHeaders (const std::string &hdrs); std::string protectedHeaders () const; /* Parse the headers so that the header_info_s becomes valid.*/ int parseHeaders_m (); /* Get the header info struct. Only valid afer parseHeaders_m */ header_info_s headerInfo () const; /* Get the original body of the mail. */ const std::string &getOriginalBody () const; + + /* Get the plain attachments for an encrypt / sign operation. Set through + updateOOMData_o */ + std::vector > plainAttachments() const; + + /* True if this is a multipart related mail, 1 if all attachments are + related, 2 if there is a related and a mixed attachment. */ + int isMultipartRelated () const; + private: /* Returns a copy of the mailitem object. This copy is sadly not the same as in the ItemLoad event so we cannot find it in our map of mailitems. */ LPDISPATCH copy (); bool checkIfMailMightBePrinting_o (); void updateSigstate (); int add_attachments_o (std::vector > attachments); int buildProtectedHeaders_o (); LPDISPATCH m_mailitem; LPDISPATCH m_event_sink; LPDISPATCH m_currentItemRef; bool m_processed, /* The message has been porcessed by us. */ m_needs_wipe, /* We have added plaintext to the mesage. */ m_needs_save, /* A property was changed but not by us. */ m_crypt_successful, /* We successfuly performed crypto on the item. */ m_is_smime, /* This is an smime mail. */ m_is_smime_checked, /* it was checked if this is an smime mail */ m_is_signed, /* Mail is signed */ m_is_valid, /* Mail is valid signed. */ m_close_triggered, /* We have programtically triggered a close */ m_is_html_alternative, /* Body Format is not plain text */ m_needs_encrypt; /* Send was triggered we want to encrypt. */ int m_moss_position; /* The number of the original message attachment. */ int m_crypto_flags; std::string m_sender; std::string m_sent_on_behalf; char *m_cached_html_body; /* Cached html body. */ char *m_cached_plain_body; /* Cached plain body. */ std::vector m_cached_recipients; msgtype_t m_type; /* Our messagetype as set in mapi */ std::shared_ptr m_parser; std::shared_ptr m_crypter; GpgME::VerificationResult m_verify_result; GpgME::DecryptionResult m_decrypt_result; GpgME::Signature m_sig; GpgME::UserID m_uid; std::string m_uuid; std::string m_orig_body; bool m_do_inline; bool m_is_gsuite; /* Are we on a gsuite account */ std::string m_inline_body; CryptState m_crypt_state; HWND m_window; bool m_async_crypt_disabled; std::string m_mime_data; bool m_is_forwarded_crypto_mail; /* Is this a forward of a crypto mail */ bool m_is_reply_crypto_mail; /* Is this a reply to a crypto mail */ bool m_is_send_again; /* Is this a send again of a crypto mail */ bool m_disable_att_remove_warning; /* Should not warn about attachment removal. */ bool m_block_html; /* Force blocking of html content. e.g for unsigned S/MIME mails. */ bool m_manual_crypto_opts; /* Crypto options (sign/encrypt) have been set manually. */ bool m_first_autosecure_check; /* This is the first autoresolve check */ int m_locate_count; /* The number of key locates pending for this mail. */ bool m_pass_write; /* Danger the next write will be passed. This is for closed mails */ bool m_locate_in_progress; /* Simplified state variable for locate */ std::string m_store_id; /* Store id for categories */ std::string m_verify_category; /* The category string for the verify result */ bool m_is_junk; /* Mail is in the junk folder */ bool m_is_draft_encrypt; /* Mail is a draft that should be encrypted */ bool m_decrypt_again; /* Mail should be decrypted again if it sees another beforeread */ bool m_printing; /* Mail is decrypted for printing */ std::string m_gpgol_class; /* The GpgOL Message class */ std::vector m_resolved_signing_keys; /* Prepared / resolved keys for signing. */ bool m_recipients_set; /* Recipients were explictly set. */ bool m_is_split_copy; /* Is the a copy mail that was part of a split. */ std::string m_protected_headers; header_info_s m_header_info; /* Information about the original headers */ bool m_attachs_added; /* State variable to track if we have added attachments to this mail. */ std::string m_dec_content_type; /* Top level content type of the decrypted mail. */ + std::vector > m_plain_attachments; /* Attachments to encrypt */ }; /* A state variable to capture which mail triggered a copy to use our own objects in the ItemLoad event. */ extern Mail *g_mail_copy_triggerer; #endif // MAIL_H diff --git a/src/mailitem-events.cpp b/src/mailitem-events.cpp index 670dcaa..4c72995 100644 --- a/src/mailitem-events.cpp +++ b/src/mailitem-events.cpp @@ -1,1066 +1,1055 @@ /* mailitem-events.h - Event handling for mails. * Copyright (C) 2015 by Bundesamt für Sicherheit in der Informationstechnik * Software engineering by Intevation GmbH + * Copyright (C) 2019, 2020 g10code GmbH * * This file is part of GpgOL. * * GpgOL is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * GpgOL is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, see . */ #include "config.h" #include "common.h" #include "eventsink.h" #include "eventsinks.h" #include "mymapi.h" #include "mymapitags.h" #include "oomhelp.h" #include "ocidl.h" #include "windowmessages.h" #include "mail.h" #include "mapihelp.h" #include "gpgoladdin.h" #include "wks-helper.h" #undef _ #define _(a) utf8_gettext (a) const wchar_t *prop_blacklist[] = { L"Body", L"HTMLBody", L"To", /* Somehow this is done when a mail is opened */ L"CC", /* Ditto */ L"BCC", /* Ditto */ L"Categories", L"UnRead", L"OutlookVersion", L"OutlookInternalVersion", L"ReceivedTime", L"InternetCodepage", L"ConversationIndex", L"Subject", L"SentOnBehalfOfName", L"MessageClass", L"BodyFormat", NULL }; typedef enum { AfterWrite = 0xFC8D, AttachmentAdd = 0xF00B, AttachmentRead = 0xF00C, AttachmentRemove = 0xFBAE, BeforeAttachmentAdd = 0xFBB0, BeforeAttachmentPreview = 0xFBAF, BeforeAttachmentRead = 0xFBAB, BeforeAttachmentSave = 0xF00D, BeforeAttachmentWriteToTempFile = 0xFBB2, BeforeAutoSave = 0xFC02, BeforeCheckNames = 0xF00A, BeforeDelete = 0xFA75, BeforeRead = 0xFC8C, Close = 0xF004, CustomAction = 0xF006, CustomPropertyChange = 0xF008, Forward = 0xF468, Open = 0xF003, PropertyChange = 0xF009, Read = 0xF001, ReadComplete = 0xFC8F, Reply = 0xF466, ReplyAll = 0xF467, Send = 0xF005, Unload = 0xFBAD, Write = 0xF002 } MailEvent; /* Mail Item Events */ BEGIN_EVENT_SINK(MailItemEvents, IDispatch) /* We are still in the class declaration */ private: Mail * m_mail; /* The mail object related to this mailitem */ }; MailItemEvents::MailItemEvents() : m_object(NULL), m_pCP(NULL), m_cookie(0), m_ref(1), m_mail(NULL) { } MailItemEvents::~MailItemEvents() { if (m_pCP) m_pCP->Unadvise(m_cookie); if (m_object) gpgol_release (m_object); } static bool propchangeWarnShown = false; static bool attachRemoveWarnShown = false; static bool addinsLogged = false; static DWORD WINAPI do_delayed_locate (LPVOID arg) { Sleep(100); do_in_ui_thread (RECIPIENT_ADDED, arg); return 0; } /* The main Invoke function. The return value of this function does not appear to have any effect on outlook although I have read in an example somewhere that you should return S_OK so that outlook continues to handle the event I have not yet seen any effect by returning error values here and no MSDN documentation about the return values. */ EVENT_SINK_INVOKE(MailItemEvents) { USE_INVOKE_ARGS TSTART; if (!m_mail) { m_mail = Mail::getMailForItem (m_object); if (!m_mail) { log_error ("%s:%s: mail event without mail object known. Bug.", SRCNAME, __func__); TRETURN S_OK; } } bool is_reply = false; switch(dispid) { case BeforeAutoSave: { log_oom ("%s:%s: BeforeAutoSave : %p", SRCNAME, __func__, m_mail); if (parms->cArgs != 1 || parms->rgvarg[0].vt != (VT_BOOL | VT_BYREF)) { /* This happens in the weird case */ log_debug ("%s:%s: Uncancellable BeforeAutoSave.", SRCNAME, __func__); TBREAK; } if (m_mail->isCryptoMail() && !m_mail->decryptedSuccessfully ()) { *(parms->rgvarg[0].pboolVal) = VARIANT_TRUE; log_debug ("%s:%s: Autosave for not successfuly decrypted mail." "Cancel it.", SRCNAME, __func__); TBREAK; } if (opt.draft_key && (m_mail->needs_crypto_m () & 1) && !m_mail->isDraftEncrypt()) { log_debug ("%s:%s: Draft encryption for autosave starting now.", SRCNAME, __func__); m_mail->setIsDraftEncrypt (true); m_mail->prepareCrypto_o (); } TRETURN S_OK; } case Open: { log_oom ("%s:%s: Open : %p", SRCNAME, __func__, m_mail); int draft_flags = 0; if (!opt.encrypt_default && !opt.sign_default) { TRETURN S_OK; } LPMESSAGE message = get_oom_base_message (m_object); if (!message) { log_error ("%s:%s: Failed to get message.", SRCNAME, __func__); TBREAK; } int old_flags = get_gpgol_draft_info_flags (message); if (old_flags) { log_dbg ("Open of already flagged mail. " "Not overwriting with settings."); gpgol_release (message); TBREAK; } if (opt.encrypt_default) { draft_flags = 1; } if (opt.sign_default) { draft_flags += 2; } set_gpgol_draft_info_flags (message, draft_flags); gpgol_release (message); TBREAK; } case BeforeRead: { log_oom ("%s:%s: BeforeRead : %p", SRCNAME, __func__, m_mail); if (GpgolAddin::get_instance ()->isShutdown()) { log_debug ("%s:%s: Ignoring read after shutdown.", SRCNAME, __func__); TBREAK; } if (m_mail->preProcessMessage_m ()) { log_error ("%s:%s: Pre process message failed.", SRCNAME, __func__); } if (m_mail->isDecryptAgain ()) { log_debug ("%s:%s: Decrypting after 500ms", SRCNAME, __func__); do_in_ui_thread_async (DECRYPT, m_mail, 500); } TBREAK; } case Read: { log_oom ("%s:%s: Read : %p", SRCNAME, __func__, m_mail); if (!addinsLogged) { // We do it here as this nearly always comes and we want to remove // as much as possible from the startup time. log_addins (); addinsLogged = true; } if (!m_mail->isCryptoMail ()) { log_debug ("%s:%s: Non crypto mail %p opened. Updating sigstatus.", SRCNAME, __func__, m_mail); /* Ensure that no wrong sigstatus is shown */ CloseHandle(CreateThread (NULL, 0, delayed_invalidate_ui, (LPVOID) 300, 0, NULL)); TBREAK; } if (m_mail->setUUID_o ()) { log_debug ("%s:%s: Failed to set uuid.", SRCNAME, __func__); delete m_mail; /* deletes this, too */ TRETURN S_OK; } if (m_mail->decryptVerify_o ()) { log_error ("%s:%s: Decrypt message failed.", SRCNAME, __func__); } if (!opt.enable_smime && m_mail->isSMIME_m ()) { /* We want to save the mail when it's an smime mail and smime is disabled to revert it. */ log_debug ("%s:%s: S/MIME mail but S/MIME is disabled." " Need save.", SRCNAME, __func__); m_mail->setNeedsSave (true); } TBREAK; } case PropertyChange: { if (!parms || parms->cArgs != 1 || parms->rgvarg[0].vt != VT_BSTR || !parms->rgvarg[0].bstrVal) { log_error ("%s:%s: Unexpected params.", SRCNAME, __func__); TBREAK; } const wchar_t *prop_name = parms->rgvarg[0].bstrVal; if (!m_mail->isCryptoMail ()) { if (m_mail->hasOverrideMimeData()) { /* This is a mail created by us. Ignore propchanges. */ TBREAK; } if (!wcscmp (prop_name, L"To") /* || !wcscmp (prop_name, L"BCC") || !wcscmp (prop_name, L"CC") Testing shows that Outlook always sends these three in a row */) { if (opt.autosecure || (m_mail->needs_crypto_m () & 1)) { /* XXX Racy race. This is a fix for crashes that happend if a resolved recipient is copied an pasted. If we then access the recipients object in the Property Change event we crash. Thus we do the delay dance. */ HANDLE thread = CreateThread (NULL, 0, do_delayed_locate, (LPVOID) m_mail, 0, NULL); CloseHandle(thread); } } TBREAK; } for (const wchar_t **cur = prop_blacklist; *cur; cur++) { if (!wcscmp (prop_name, *cur)) { log_oom ("%s:%s: Mail %p propchange: %ls no warning.", SRCNAME, __func__, m_mail, prop_name); TRETURN S_OK; } } log_oom ("%s:%s: Mail %p propchange: %ls.", SRCNAME, __func__, m_mail, prop_name); if (!wcscmp (prop_name, L"SendUsingAccount")) { bool sent = get_oom_bool (m_object, "Sent"); if (sent) { log_debug ("%s:%s: Ignoring SendUsingAccount change for sent %p ", SRCNAME, __func__, m_object); TRETURN S_OK; } log_debug ("%s:%s: Message %p looks like send again.", SRCNAME, __func__, m_object); m_mail->setIsSendAgain (true); TRETURN S_OK; } if (is_draft_mail (m_object)) { log_oom ("%s:%s: Change allowed for draft", SRCNAME, __func__); TRETURN S_OK; } /* We have tried several scenarios to handle propery changes. Only save the property in MAPI and call MAPI SaveChanges worked and did not leak plaintext but this caused outlook still to break the attachments of PGP/MIME Mails into two attachments and add them as winmail.dat so other clients are broken. Alternatively reverting the mail, saving the property and then decrypt again also worked a bit but there were some weird side effects and breakages. But this has the usual problem of a revert that the mail is created by outlook and e.g. multipart/signed signatures from most MUA's are broken. Some things to try out might be the close approach and then another open or a selection change. But for now we just warn. As a workardound a user should make property changes when the mail was not read by us. */ if (propchangeWarnShown) { TRETURN S_OK; } wchar_t *title = utf8_to_wchar (_("Sorry, that's not possible, yet")); char *fmt; gpgrt_asprintf (&fmt, _("GpgOL has prevented the change to the \"%s\" property.\n" "Property changes are not yet handled for crypto messages.\n\n" "To workaround this limitation please change the property when the " "message is not open in any window and not selected in the " "messagelist.\n\nFor example by right clicking but not selecting the message.\n"), wchar_to_utf8(prop_name)); memdbg_alloc (fmt); wchar_t *msg = utf8_to_wchar (fmt); xfree (fmt); MessageBoxW (get_active_hwnd(), msg, title, MB_ICONINFORMATION | MB_OK); xfree (msg); xfree (title); propchangeWarnShown = true; TRETURN S_OK; } case CustomPropertyChange: { log_oom ("%s:%s: CustomPropertyChange : %p", SRCNAME, __func__, m_mail); /* TODO */ TBREAK; } case Send: { /* This is the only event where we can cancel the send of a mailitem. But it is too early for us to encrypt as the MAPI structures are not yet filled. Crypto based on the Outlook Object Model data did not work as the messages were only sent out empty. See 2b376a48 for a try of this. This is why we store send_seen and invoke a save which may result in an error but only after triggering all the behavior we need -> filling mapi structures and invoking the AfterWrite handler where we encrypt. If this encryption is successful and we pass the send as then the encrypted data is sent. */ log_oom ("%s:%s: Send : %p", SRCNAME, __func__, m_mail); - if (!m_mail->needs_crypto_m () && m_mail->cryptState () == Mail::NoCryptMail) + if (!m_mail->needs_crypto_m () && m_mail->cryptState () == Mail::NotStarted) { if (m_mail->isCryptoMail ()) { log_debug ("%s:%s: Want to send crypto mail without crypto. " "Decrypting.", SRCNAME, __func__); m_mail->decryptPermanently_o (); TBREAK; } log_debug ("%s:%s: No crypto neccessary. Passing send for %p obj %p", SRCNAME, __func__, m_mail, m_object); TBREAK; } if (parms->cArgs != 1 || parms->rgvarg[0].vt != (VT_BOOL | VT_BYREF)) { log_debug ("%s:%s: Uncancellable send event.", SRCNAME, __func__); TBREAK; } - if (m_mail->cryptState () == Mail::NoCryptMail && + if (m_mail->cryptState () == Mail::NotStarted && m_mail->needs_crypto_m ()) { - log_debug ("%s:%s: Send event for crypto mail %p saving and starting.", - SRCNAME, __func__, m_mail); + log_dbg ("Send event for mail %p to be encrypted starting.", + m_mail); if (m_mail->prepareCrypto_o ()) { log_dbg ("Prepare crypto requested send abort."); *(parms->rgvarg[0].pboolVal) = VARIANT_TRUE; /* Reset the crypter state */ - m_mail->setCryptState (Mail::NoCryptMail); + m_mail->setCryptState (Mail::NotStarted); TBREAK; } m_mail->setIsDraftEncrypt (false); - // Save the Mail - invoke_oom_method (m_object, "Save", NULL); + /* Do the crypto operation */ + log_dbg ("Starting crypto."); + + if (m_mail->encryptSignStart_o ()) + { + log_debug ("%s:%s: Encrypt sign start failed.", + SRCNAME, __func__); + m_mail->setCryptState (Mail::NotStarted); + m_mail->releaseCurrentItem(); + } if (!m_mail->isAsyncCryptDisabled ()) { - // The afterwrite in the save should have triggered - // the encryption. We cancel send for our asyncness. - // Cancel send + /* Cancel sending. CRYPTO_DONE should trigger sending */ *(parms->rgvarg[0].pboolVal) = VARIANT_TRUE; TBREAK; } - else + /* Only reached for synchronous operation. Mail should now be + * encrypted and in a want's send state or the user aborted / + * error */ + if (m_mail->cryptState () == Mail::NotStarted) { - if (m_mail->cryptState () == Mail::NoCryptMail) - { - // Crypto failed or was canceled - log_debug ("%s:%s: Message %p mail %p cancelling send - " - "Crypto failed or canceled.", - SRCNAME, __func__, m_object, m_mail); - *(parms->rgvarg[0].pboolVal) = VARIANT_TRUE; - /* Reset the crypter state */ - m_mail->setCryptState (Mail::NoCryptMail); - TBREAK; - } - // For inline response we can't trigger send programatically - // so we do the encryption in sync. - if (m_mail->cryptState () == Mail::NeedsUpdateInOOM) - { - m_mail->updateCryptOOM_o (); - } - if (m_mail->cryptState () == Mail::NeedsSecondAfterWrite) - { - m_mail->setCryptState (Mail::WantsSendMIME); - } - if (m_mail->getDoPGPInline () && m_mail->cryptState () != Mail::WantsSendInline) - { - log_debug ("%s:%s: Message %p mail %p cancelling send - " - "Invalid state.", - SRCNAME, __func__, m_object, m_mail); - gpgol_bug (m_mail->getWindow (), - ERR_INLINE_BODY_INV_STATE); - *(parms->rgvarg[0].pboolVal) = VARIANT_TRUE; - TBREAK; - } + // Crypto failed or was canceled + log_debug ("%s:%s: Message %p mail %p cancelling send - " + "Crypto failed or canceled.", + SRCNAME, __func__, m_object, m_mail); + *(parms->rgvarg[0].pboolVal) = VARIANT_TRUE; + TBREAK; + } + if (m_mail->cryptState () == Mail::BackendDone) + { + m_mail->updateCryptOOM_o (); + } + if (m_mail->cryptState () == Mail::OOMUpdated) + { + m_mail->setCryptState (Mail::CryptFinished); } } - if (m_mail->cryptState () == Mail::WantsSendInline) + /* Check if updating the body worked for send inline */ + if (m_mail->cryptState () == Mail::CryptFinished && + m_mail->getDoPGPInline ()) { if (!m_mail->hasCryptedOrEmptyBody_o ()) { log_debug ("%s:%s: Message %p mail %p cancelling send - " "not encrypted or not empty body detected.", SRCNAME, __func__, m_object, m_mail); gpgol_bug (m_mail->getWindow (), ERR_WANTS_SEND_INLINE_BODY); - m_mail->setCryptState (Mail::NoCryptMail); + m_mail->setCryptState (Mail::NotStarted); *(parms->rgvarg[0].pboolVal) = VARIANT_TRUE; TBREAK; } log_debug ("%s:%s: Passing send event for no-mime message %p.", SRCNAME, __func__, m_object); WKSHelper::instance()->allow_notify (1000); TBREAK; } - - if (m_mail->cryptState () == Mail::WantsSendMIME) + else if (m_mail->cryptState () == Mail::CryptFinished) { if (!m_mail->hasCryptedOrEmptyBody_o ()) { /* The safety checks here trigger too often. Somehow for some users the body is not empty after the encryption but when it is sent it is still sent with the crypto content because the encrypted MIME Structure is used because it is correct in MAPI land. For safety reasons enabling the checks might be better but until we figure out why for some users the body replacement does not work we have to disable them. Otherwise GpgOL is unusuable for such users. GnuPG-Bug-Id: T3875 */ #define DISABLE_SAFTEY_CHECKS #ifndef DISABLE_SAFTEY_CHECKS gpgol_bug (m_mail->getWindow (), ERR_WANTS_SEND_MIME_BODY); log_debug ("%s:%s: Message %p mail %p cancelling send mime - " "not encrypted or not empty body detected.", SRCNAME, __func__, m_object, m_mail); - m_mail->setCryptState (Mail::NoCryptMail); + m_mail->setCryptState (Mail::NotStarted); *(parms->rgvarg[0].pboolVal) = VARIANT_TRUE; TBREAK; #else log_debug ("%s:%s: Message %p mail %p - " "not encrypted or not empty body detected - MIME.", SRCNAME, __func__, m_object, m_mail); #endif } /* Now we adress T3656 if Outlooks internal S/MIME is somehow * mixed in (even if it is enabled and then disabled) it might * cause strange behavior in that it sends the plain message * and not the encrypted message. Tests have shown that we can * bypass that by calling submit message on our base * message. * * We do this conditionally as our other way of using OOM * to send is proven to work and we don't want to mess * with it. */ // Get the Message class. HRESULT hr; LPSPropValue propval = NULL; // It's important we use the _not_ base message here. LPMESSAGE message = get_oom_message (m_object); hr = HrGetOneProp ((LPMAPIPROP)message, PR_MESSAGE_CLASS_A, &propval); gpgol_release (message); if (FAILED (hr)) { log_error ("%s:%s: HrGetOneProp() failed: hr=%#lx\n", SRCNAME, __func__, hr); TBREAK; } if (propval->Value.lpszA && !strstr (propval->Value.lpszA, "GpgOL") && strcmp (propval->Value.lpszA, "IPM.Note.SMIME.MultipartSigned") && strcmp (propval->Value.lpszA, "IPM.Note.SMIME")) { // Does not have a message class by us. log_debug ("%s:%s: Message %p - No GpgOL Message class " "after crypto. cls is: '%s'", SRCNAME, __func__, m_object, propval->Value.lpszA); log_debug ("%s:%s: Message %p - Activating T3656 Workaround", SRCNAME, __func__, m_object); message = get_oom_base_message (m_object); if (message) { // It's important we use the _base_ message here. mapi_save_changes (message, FORCE_SAVE); message->SubmitMessage(0); gpgol_release (message); // Close the composer and trigger unloads CloseHandle(CreateThread (NULL, 0, close_mail, (LPVOID) m_mail, 0, NULL)); } else { gpgol_bug (nullptr, ERR_GET_BASE_MSG_FAILED); } // Cancel send *(parms->rgvarg[0].pboolVal) = VARIANT_TRUE; } MAPIFreeBuffer (propval); if (*(parms->rgvarg[0].pboolVal) == VARIANT_TRUE) { TBREAK; } log_debug ("%s:%s: Passing send event for mime-encrypted message %p.", SRCNAME, __func__, m_object); WKSHelper::instance()->allow_notify (1000); TBREAK; } else { log_debug ("%s:%s: Message %p cancelling send - " - "crypto or second save failed.", - SRCNAME, __func__, m_object); + "crypto or second save failed state: %i.", + SRCNAME, __func__, m_object, + m_mail->cryptState ()); *(parms->rgvarg[0].pboolVal) = VARIANT_TRUE; } TRETURN S_OK; } case ReadComplete: { log_oom ("%s:%s: ReadComplete: %p", SRCNAME, __func__, m_mail); TRETURN S_OK; } case Write: { - log_oom ("%s:%s: Write : %p", - SRCNAME, __func__, m_mail); + log_oom ("%s:%s: Write : %p", SRCNAME, __func__, m_mail); /* This is a bit strange. We sometimes get multiple write events without a read in between. When we access the message in the second event it fails and if we cancel the event outlook crashes. So we have keep the m_needs_wipe state variable to keep track of that. */ if (parms->cArgs != 1 || parms->rgvarg[0].vt != (VT_BOOL | VT_BYREF)) { /* This happens in the weird case */ log_debug ("%s:%s: Uncancellable write event.", SRCNAME, __func__); TBREAK; } if (m_mail->passWrite()) { log_debug ("%s:%s: Passing write because passNextWrite was set for %p", SRCNAME, __func__, m_mail); m_mail->setPassWrite (false); TBREAK; } - if (m_mail->cryptState() == Mail::NeedsFirstAfterWrite) +#if 0 +TODO: Handle split copy in another way + if (m_mail->cryptState() == Mail::NeedsFirstAfterWrite) { log_debug ("%s:%s: Crypto mail needs first after write. " "Auto-Passing but removing our categories.", SRCNAME, __func__); + gpgol_message_box (nullptr, "NeedsFirstAfterWrite detected.", + "Write event wants first after write.", + MB_OK); m_mail->removeCategories_o (); if (g_mail_copy_triggerer) { log_dbg ("Needs first after write for copied mail."); g_mail_copy_triggerer->splitCopyMailCallback (m_mail); } TBREAK; } +#endif if (m_mail->isCryptoMail () && !m_mail->needsSave ()) { if (opt.draft_key && (m_mail->needs_crypto_m () & 1) && is_draft_mail (m_object) && m_mail->decryptedSuccessfully ()) { - if (m_mail->cryptState () == Mail::NeedsFirstAfterWrite || - m_mail->cryptState () == Mail::NeedsSecondAfterWrite) + if (m_mail->cryptState () == Mail::OOMUpdated) { log_debug ("%s:%s: re-encryption in progress. Passing.", SRCNAME, __func__); TBREAK; } /* This is the case for a modified draft */ log_debug ("%s:%s: Draft re-encryption starting now.", SRCNAME, __func__); m_mail->setIsDraftEncrypt (true); m_mail->prepareCrypto_o (); /* Passing write to trigger encrypt in after write */ TBREAK; } - if (m_mail->cryptState() == Mail::WantsSendMIME) + if (m_mail->cryptState() == Mail::CryptFinished) { log_debug ("%s:%s: Mail wants send mime. Passing.", SRCNAME, __func__); TBREAK; } Mail *last_mail = Mail::getLastMail (); if (Mail::isValidPtr (last_mail)) { /* We want to identify here if there was a mail created that should receive the contents of this mail. For this we check for a write in the same loop as a mail creation. Now when switching from one mail to another this is also what happens. The new mail is loaded and the old mail is written. To distinguish the two we check that the new mail does not have an entryID, a Subject and No Size. Maybe just size or entryID would be enough but better save then sorry. Security consideration: Worst case we pass the write here but an unload follows before we get the scheduled revert. This would leak plaintext. But does not happen in our tests. Similarly if we crash or Outlook is closed before we see this revert. But as we immediately revert after the write this should also not happen. */ const std::string lastSubject = last_mail->getSubject_o (); char *lastEntryID = get_oom_string (last_mail->item (), "EntryID"); int lastSize = get_oom_int (last_mail->item (), "Size"); std::string lastEntryStr; if (lastEntryID) { lastEntryStr = lastEntryID; xfree (lastEntryID); } if (!lastSize && !lastEntryStr.size () && !lastSubject.size ()) { log_debug ("%s:%s: Write in the same loop as empty load." " Pass but schedule revert.", SRCNAME, __func__); /* This might be a forward. So don't invalidate yet. */ // Mail::clearLastMail (); do_in_ui_thread_async (REVERT_MAIL, m_mail); TRETURN S_OK; } } /* We cancel the write event to stop outlook from excessively syncing our changes. if smime support is disabled and we still have an smime mail we also don't want to cancel the write event to enable reverting this mails. */ *(parms->rgvarg[0].pboolVal) = VARIANT_TRUE; log_debug ("%s:%s: Canceling write event.", SRCNAME, __func__); TRETURN S_OK; } if (m_mail->isCryptoMail () && m_mail->needsSave () && m_mail->revert_o ()) { /* An error cleaning the mail should not happen normally. But just in case there is an error we cancel the write here. */ log_debug ("%s:%s: Failed to remove plaintext. Canceling.", SRCNAME, __func__); *(parms->rgvarg[0].pboolVal) = VARIANT_TRUE; TRETURN S_OK; } if (!m_mail->isCryptoMail () && m_mail->is_forwarded_crypto_mail () && - !m_mail->needs_crypto_m () && m_mail->cryptState () == Mail::NoCryptMail) + !m_mail->needs_crypto_m () && m_mail->cryptState () == Mail::NotStarted) { /* We are sure now that while this is a forward of an encrypted * mail that the forward should not be signed or encrypted. So * it's not constructed by us. We need to remove our attachments * though so that they are not included in the forward. */ log_debug ("%s:%s: Writing unencrypted forward of crypt mail. " "Removing attachments. mail: %p item: %p", SRCNAME, __func__, m_mail, m_object); if (m_mail->removeOurAttachments_o ()) { // Worst case we forward some encrypted data here not // a security problem, so let it pass. log_error ("%s:%s: Failed to remove our attachments.", SRCNAME, __func__); } /* Remove marker because we did this now. */ m_mail->setIsForwardedCryptoMail (false); } if (m_mail->isDraftEncrypt () && - m_mail->cryptState () != Mail::NeedsFirstAfterWrite && - m_mail->cryptState () != Mail::NeedsSecondAfterWrite) + m_mail->cryptState () != Mail::OOMUpdated) { log_debug ("%s:%s: Canceling write because draft encrypt is in" " progress.", SRCNAME, __func__); *(parms->rgvarg[0].pboolVal) = VARIANT_TRUE; TRETURN S_OK; } if (opt.draft_key && (m_mail->needs_crypto_m () & 1) && - !m_mail->isDraftEncrypt() && - m_mail->cryptState() == Mail::NoCryptMail) + !m_mail->isDraftEncrypt () && + m_mail->cryptState () == Mail::NotStarted) { log_debug ("%s:%s: Draft encryption starting now.", SRCNAME, __func__); m_mail->setIsDraftEncrypt (true); m_mail->prepareCrypto_o (); + m_mail->encryptSignStart_o (); + /* Draft encryption happens synchronously so we pass it. */ + TRETURN S_OK; } log_debug ("%s:%s: Passing write event. %i %i", SRCNAME, __func__, m_mail->isDraftEncrypt(), m_mail->cryptState()); m_mail->setNeedsSave (false); TBREAK; } case AfterWrite: { log_oom ("%s:%s: AfterWrite : %p", SRCNAME, __func__, m_mail); - if (m_mail->cryptState () == Mail::NeedsFirstAfterWrite) + if (m_mail->cryptState () == Mail::OOMUpdated) { - /* Seen the first after write. Advance the state */ - m_mail->setCryptState (Mail::NeedsActualCrypt); - if (m_mail->encryptSignStart_o ()) + m_mail->setCryptState (Mail::OOMSynced); + m_mail->updateCryptMAPI_m (); + if (!m_mail->isAsyncCryptDisabled()) { - log_debug ("%s:%s: Encrypt sign start failed.", - SRCNAME, __func__); - m_mail->setCryptState (Mail::NoCryptMail); - if (!m_mail->isAsyncCryptDisabled()) - { - m_mail->releaseCurrentItem(); - } + m_mail->releaseCurrentItem(); } - TRETURN S_OK; - } - if (m_mail->cryptState () == Mail::NeedsSecondAfterWrite) - { - m_mail->setCryptState (Mail::NeedsUpdateInMAPI); - m_mail->updateCryptMAPI_m (); log_debug ("%s:%s: Second after write done.", SRCNAME, __func__); TRETURN S_OK; } TBREAK; } case Close: { log_oom ("%s:%s: Close : %p", SRCNAME, __func__, m_mail); if (m_mail->isCryptoMail ()) { if (is_draft_mail (m_object)) { /* In that case we want to ask the question to avoid data loss */ log_oom ("%s:%s: Passing close because of draft status: %p", SRCNAME, __func__, m_mail); m_mail->setDecryptAgain (true); TBREAK; } /* Close. This happens when an Opened mail is closed. To prevent the question of wether or not to save the changes (Which would save the decrypted data without an event to prevent it) we cancel the close and then either close it with discard changes or revert / save it. Contrary to documentation we can invoke close from close. */ if (parms->cArgs != 1 || parms->rgvarg[0].vt != (VT_BOOL | VT_BYREF)) { /* This happens in the weird case */ log_debug ("%s:%s: Uncancellable close event.", SRCNAME, __func__); TBREAK; } if (m_mail->getCloseTriggered ()) { /* Our close with discard changes, pass through */ m_mail->setCloseTriggered (false); TRETURN S_OK; } *(parms->rgvarg[0].pboolVal) = VARIANT_TRUE; log_oom ("%s:%s: Canceling close event.", SRCNAME, __func__); if (m_mail->close ()) { log_debug ("%s:%s: Close request failed.", SRCNAME, __func__); } } TRETURN S_OK; } case Unload: { log_oom ("%s:%s: Unload : %p", SRCNAME, __func__, m_mail); log_debug ("%s:%s: Removing Mail for message: %p.", SRCNAME, __func__, m_object); delete m_mail; log_oom ("%s:%s: deletion done", SRCNAME, __func__); memdbg_dump (); TRETURN S_OK; } case ReplyAll: case Reply: is_reply = true; /* fall through */ case Forward: { log_oom ("%s:%s: %s : %p", SRCNAME, __func__, is_reply ? "reply" : "forward", m_mail); int draft_flags = 0; if (opt.encrypt_default) { draft_flags = 1; } if (opt.sign_default) { draft_flags += 2; } bool is_crypto_mail = m_mail->isCryptoMail (); if (opt.reply_crypt && is_crypto_mail) { int crypto_flags = m_mail->getCryptoFlags (); if (crypto_flags) { if (opt.sign_default) { /* When default signing is on we also sign unsigned encrypted * mails. */ crypto_flags |= 2; /* reply / forward flags override the setting flags. */ } draft_flags = crypto_flags; } } /* If it is a crypto mail and the settings should not be taken * from the crypto mail and always encrypt / sign is on. Or * If it is not a crypto mail and we have automaticalls sign_encrypt. */ if (draft_flags) { /* Check if we can use the dispval */ if (parms->cArgs == 2 && parms->rgvarg[1].vt == (VT_DISPATCH) && parms->rgvarg[0].vt == (VT_BOOL | VT_BYREF)) { LPMESSAGE msg = get_oom_base_message (parms->rgvarg[1].pdispVal); if (msg) { set_gpgol_draft_info_flags (msg, draft_flags); gpgol_release (msg); } else { log_error ("%s:%s: Failed to get base message.", SRCNAME, __func__); } } else { log_error ("%s:%s: Unexpected parameters.", SRCNAME, __func__); } } if (!is_crypto_mail) { /* Replys to non crypto mails do not interest us anymore. */ TBREAK; } Mail *last_mail = Mail::getLastMail (); if (Mail::isValidPtr (last_mail)) { /* We want to identify here if there was a mail created that should receive the contents of this mail. For this we check for a forward in the same loop as a mail creation. We need to do it this complicated and can't just use get_mail_for_item because the mailitem pointer we get here is a different one then the one with which the mail was loaded. */ char *lastEntryID = get_oom_string (last_mail->item (), "EntryID"); int lastSize = get_oom_int (last_mail->item (), "Size"); std::string lastEntryStr; if (lastEntryID) { lastEntryStr = lastEntryID; xfree (lastEntryID); } if (!lastSize && !lastEntryStr.size ()) { if (!is_reply) { log_debug ("%s:%s: Forward in the same loop as empty " "load Marking %p (item %p) as forwarded.", SRCNAME, __func__, last_mail, last_mail->item ()); last_mail->setIsForwardedCryptoMail (true); } else { log_debug ("%s:%s: Reply in the same loop as empty " "load treating %p (item %p) as reply.", SRCNAME, __func__, last_mail, last_mail->item ()); } if (m_mail->isBlockHTML () && !opt.smime_insecure_reply_fw_allowed) { std::string buf; /** TRANSLATORS: Part of a warning dialog that disallows reply and forward with contents */ buf = is_reply ? _("You are replying to an unsigned S/MIME " "email.") : _("You are forwarding an unsigned S/MIME " "email."); buf +="\n\n"; buf += _("In this version of S/MIME an attacker could " "use the missing signature to have you " "decrypt contents from a different, otherwise " "completely unrelated email and place it in the " "quote so they can get hold of it.\n" "This is why we only allow quoting to be done manually."); buf += "\n\n"; buf += _("Please copy the relevant contents and insert " "them into the new email."); gpgol_message_box (get_active_hwnd (), buf.c_str(), _("GpgOL"), MB_OK); do_in_ui_thread_async (CLEAR_REPLY_FORWARD, last_mail, 1000); } } // We can now invalidate the last mail Mail::clearLastMail (); } TBREAK; } case AttachmentRemove: { log_oom ("%s:%s: AttachmentRemove: %p", SRCNAME, __func__, m_mail); if (!m_mail->isCryptoMail () || attachRemoveWarnShown || m_mail->attachmentRemoveWarningDisabled ()) { TRETURN S_OK; } gpgol_message_box (get_active_hwnd (), _("Attachments are part of the crypto message.\nThey " "can't be permanently removed and will be shown again the next " "time this message is opened."), _("Sorry, that's not possible, yet"), MB_OK); attachRemoveWarnShown = true; TRETURN S_OK; } default: log_oom ("%s:%s: Message:%p Unhandled Event: %lx \n", SRCNAME, __func__, m_object, dispid); } TRETURN S_OK; } END_EVENT_SINK(MailItemEvents, IID_MailItemEvents) diff --git a/src/main.c b/src/main.c index 92119bf..6e3c34a 100644 --- a/src/main.c +++ b/src/main.c @@ -1,419 +1,419 @@ /* main.c - DLL entry point * Copyright (C) 2005, 2007, 2008 g10 Code 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 * 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 #include #include #include #include #include #include "mymapi.h" #include "mymapitags.h" #include "common.h" #include "mymapi.h" /* Local function prototypes. */ static char *get_locale_dir (void); static void drop_locale_dir (char *locale_dir); /* The major version of Outlook we are attached to */ int g_ol_version_major; void i18n_init (void) { char *locale_dir; #ifdef ENABLE_NLS # ifdef HAVE_LC_MESSAGES setlocale (LC_TIME, ""); setlocale (LC_MESSAGES, ""); # else setlocale (LC_ALL, "" ); # endif #endif locale_dir = get_locale_dir (); if (locale_dir) { bindtextdomain (PACKAGE_GT, locale_dir); drop_locale_dir (locale_dir); } textdomain (PACKAGE_GT); } static char * get_gpgme_w32_inst_dir (void) { char *gpg4win_dir = get_gpg4win_dir (); char *tmp; gpgrt_asprintf (&tmp, "%s\\bin\\gpgme-w32spawn.exe", gpg4win_dir); memdbg_alloc (tmp); if (!access(tmp, R_OK)) { xfree (tmp); gpgrt_asprintf (&tmp, "%s\\bin", gpg4win_dir); memdbg_alloc (tmp); xfree (gpg4win_dir); return tmp; } xfree (tmp); gpgrt_asprintf (&tmp, "%s\\gpgme-w32spawn.exe", gpg4win_dir); memdbg_alloc (tmp); if (!access(tmp, R_OK)) { xfree (tmp); return gpg4win_dir; } OutputDebugString("Failed to find gpgme-w32spawn.exe!"); return NULL; } /* Entry point called by DLL loader. */ int WINAPI DllMain (HINSTANCE hinst, DWORD reason, LPVOID reserved) { (void)reserved; if (reason == DLL_PROCESS_ATTACH) { set_global_hinstance (hinst); gpg_err_init (); /* Set the installation directory for GpgME so that it can find tools like gpgme-w32-spawn correctly. */ char *instdir = get_gpgme_w32_inst_dir(); gpgme_set_global_flag ("w32-inst-dir", instdir); xfree (instdir); /* The next call initializes subsystems of gpgme and should be done as early as possible. The actual return value (the version string) is not used here. It may be called at any time later for this. */ gpgme_check_version (NULL); } else if (reason == DLL_PROCESS_DETACH) { gpg_err_deinit (0); } return TRUE; } static char * get_locale_dir (void) { char *instdir; char *p; char *dname; instdir = get_gpg4win_dir(); if (!instdir) return NULL; /* Build the key: "/share/locale". */ #define SLDIR "\\share\\locale" dname = xmalloc (strlen (instdir) + strlen (SLDIR) + 1); if (!dname) { xfree (instdir); return NULL; } p = dname; strcpy (p, instdir); p += strlen (instdir); strcpy (p, SLDIR); xfree (instdir); return dname; } static void drop_locale_dir (char *locale_dir) { xfree (locale_dir); } static int get_conf_bool (const char *name, int defaultVal) { char *val = NULL; int ret; load_extension_value (name, &val); ret = val == NULL ? defaultVal : *val != '1' ? 0 : 1; xfree (val); return ret; } static int dbg_compat (int oldval) { // We broke the debug levels at some point // This is cmpatibility code with the old // levels. #define DBG_MEMORY_OLD (1<<5) // 32 #define DBG_MIME_PARSER_OLD (1<<7) // 128 Unified as DBG_DATA #define DBG_MIME_DATA_OLD (1<<8) // 256 Unified in read_options #define DBG_OOM_OLD (1<<9) // 512 Unified as DBG_OOM #define DBG_OOM_EXTRA_OLD (1<<10)// 1024 Unified in read_options int new_dbg = oldval; if ((oldval & DBG_MEMORY_OLD)) { new_dbg |= DBG_MEMORY; new_dbg -= DBG_MEMORY_OLD; } if ((oldval & DBG_OOM_OLD)) { new_dbg |= DBG_OOM; new_dbg -= DBG_OOM_OLD; } if ((oldval & DBG_MIME_PARSER_OLD)) { new_dbg |= DBG_DATA; new_dbg -= DBG_MIME_PARSER_OLD; } if ((oldval & DBG_MIME_DATA_OLD)) { new_dbg |= DBG_DATA; new_dbg -= DBG_MIME_DATA_OLD; } if ((oldval & DBG_OOM_OLD)) { new_dbg |= DBG_OOM; new_dbg -= DBG_OOM_OLD; } if ((oldval & DBG_OOM_EXTRA_OLD)) { new_dbg |= DBG_OOM; new_dbg -= DBG_OOM_EXTRA_OLD; } #undef DBG_MEMORY_OLD #undef DBG_MIME_PARSER_OLD #undef DBG_MIME_DATA_OLD #undef DBG_OOM_OLD #undef DBG_OOM_EXTRA_OLD return new_dbg; } /* Read option settings from the Registry. */ void read_options (void) { char *val = NULL; /* Set the log file first so that output from this function is logged too. */ load_extension_value ("logFile", &val); set_log_file (val); xfree (val); val = NULL; /* Parse the debug flags. */ load_extension_value ("enableDebug", &val); opt.enable_debug = 0; if (val) { char *p, *pend; trim_spaces (val); for (p = val; p; p = pend) { pend = strpbrk (p, ", \t\n\r\f"); if (pend) { *pend++ = 0; pend += strspn (pend, ", \t\n\r\f"); } if (isascii (*p) && isdigit (*p)) { opt.enable_debug |= dbg_compat (strtoul (p, NULL, 0)); } else if (!strcmp (p, "memory")) opt.enable_debug |= DBG_MEMORY; else if (!strcmp (p, "mime-parser")) opt.enable_debug |= DBG_DATA; else if (!strcmp (p, "mime-data")) opt.enable_debug |= DBG_DATA; else if (!strcmp (p, "oom")) opt.enable_debug |= DBG_OOM; else if (!strcmp (p, "oom-extra")) opt.enable_debug |= DBG_OOM; else log_debug ("invalid debug flag `%s' ignored", p); } } else { /* To help the user enable debugging make sure that the registry key exists. Note that the other registry keys are stored after using the configuration dialog. */ store_extension_value ("enableDebug", "0"); } /* Yes we use free here because memtracing did not track the alloc as the option for debuging was not read before. */ free (val); val = NULL; if (opt.enable_debug) log_debug ("enabled debug flags:%s%s%s%s\n", (opt.enable_debug & DBG_MEMORY)? " memory":"", (opt.enable_debug & DBG_DATA)? " data":"", (opt.enable_debug & DBG_OOM)? " oom":"", (opt.enable_debug & DBG_TRACE)? " trace":"" ); opt.enable_smime = get_conf_bool ("enableSmime", 0); opt.encrypt_default = get_conf_bool ("encryptDefault", 0); opt.sign_default = get_conf_bool ("signDefault", 0); opt.inline_pgp = get_conf_bool ("inlinePGP", 0); opt.reply_crypt = get_conf_bool ("replyCrypt", 1); opt.prefer_smime = get_conf_bool ("preferSmime", 0); opt.autoresolve = get_conf_bool ("autoresolve", 1); opt.autoretrieve = get_conf_bool ("autoretrieve", 0); opt.automation = get_conf_bool ("automation", 1); opt.autosecure = get_conf_bool ("autosecure", 1); opt.autotrust = get_conf_bool ("autotrust", 0); opt.search_smime_servers = get_conf_bool ("searchSmimeServers", 0); opt.smime_html_warn_shown = get_conf_bool ("smimeHtmlWarnShown", 0); opt.smime_insecure_reply_fw_allowed = get_conf_bool ("smimeInsecureReplyAllowed", 0); opt.auto_unstrusted = get_conf_bool ("autoencryptUntrusted", 0); opt.autoimport = get_conf_bool ("autoimport", 0); opt.splitBCCMails = get_conf_bool ("splitBCCMails", 0); opt.combinedOpsEnabled = get_conf_bool ("combinedOpsEnabled", 0); opt.encryptSubject = get_conf_bool ("encryptSubject", 0); if (!opt.automation) { // Disabling automation is a shorthand to disable the // others, too. opt.autosecure = 0; opt.autoresolve = 0; opt.autotrust = 0; opt.autoretrieve = 0; opt.autoimport = 0; opt.auto_unstrusted = 0; } /* Draft encryption handling. */ if (get_conf_bool ("draftEnc", 0)) { load_extension_value ("draftKey", &val); if (val) { xfree (opt.draft_key); opt.draft_key = val; val = NULL; } } else { xfree (opt.draft_key); opt.draft_key = NULL; } opt.alwaysShowApproval = get_conf_bool ("alwaysShowApproval", 0); /* Hidden options */ - opt.sync_enc = 1; //get_conf_bool ("syncEnc", 0); + opt.sync_enc = get_conf_bool ("syncEnc", 0); /* Due to an issue where async encryption would leave unencrypted mails in the recently deleted folder on the server we block it. */ opt.sync_dec = get_conf_bool ("syncDec", 0); } /* Write current options back to the Registry. */ int write_options (void) { struct { const char *name; int mode; int value; char *s_val; } table[] = { {"smimeHtmlWarnShown", 0, opt.smime_html_warn_shown, NULL}, {NULL, 0, 0, NULL} }; char buf[32]; int rc, i; const char *string; for (i=0; table[i].name; i++) { switch (table[i].mode) { case 0: string = table[i].value? "1": "0"; log_debug ("storing option `%s' value=`%s'\n", table[i].name, string); rc = store_extension_value (table[i].name, string); break; case 1: sprintf (buf, "%d", table[i].value); log_debug ("storing option `%s' value=`%s'\n", table[i].name, buf); rc = store_extension_value (table[i].name, buf); break; case 2: string = table[i].s_val? table[i].s_val : ""; log_debug ("storing option `%s' value=`%s'\n", table[i].name, string); rc = store_extension_value (table[i].name, string); break; /* case 3: */ /* buf[0] = '0'; */ /* buf[1] = 0; */ /* switch (opt.default_protocol) */ /* { */ /* case PROTOCOL_UNKNOWN: buf[0] = '0'; /\* auto *\/ break; */ /* case PROTOCOL_OPENPGP: buf[0] = '1'; break; */ /* case PROTOCOL_SMIME: buf[0] = '2'; break; */ /* } */ /* log_debug ("storing option `%s' value=`%s'\n", */ /* table[i].name, buf); */ /* rc = store_extension_value (table[i].name, buf); */ /* break; */ case 4: sprintf (buf, "0x%x", table[i].value); log_debug ("storing option `%s' value=`%s'\n", table[i].name, buf); rc = store_extension_value (table[i].name, buf); break; default: rc = -1; break; } if (rc) log_error ("error storing option `%s': rc = %d\n", table[i].name, rc); } return 0; } diff --git a/src/mapihelp.cpp b/src/mapihelp.cpp index 18d07cc..3cd944f 100644 --- a/src/mapihelp.cpp +++ b/src/mapihelp.cpp @@ -1,4061 +1,4061 @@ /* mapihelp.cpp - Helper functions for MAPI * Copyright (C) 2005, 2007, 2008 g10 Code 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 "mymapi.h" #include "mymapitags.h" #include "common.h" #include "rfc822parse.h" #include "mapihelp.h" #include "parsetlv.h" #include "oomhelp.h" #include "cpphelp.h" #include #include #ifndef CRYPT_E_STREAM_INSUFFICIENT_DATA #define CRYPT_E_STREAM_INSUFFICIENT_DATA 0x80091011 #endif #ifndef CRYPT_E_ASN1_BADTAG #define CRYPT_E_ASN1_BADTAG 0x8009310B #endif static int get_attach_method (LPATTACH obj); static int has_smime_filename (LPATTACH obj); static char *get_attach_mime_tag (LPATTACH obj); /* Print a MAPI property to the log stream. */ void log_mapi_property (LPMESSAGE message, ULONG prop, const char *propname) { TSTART; HRESULT hr; LPSPropValue propval = NULL; size_t keylen; void *key; char *buf; if (!message) { TRETURN; /* No message: Nop. */ } hr = HrGetOneProp ((LPMAPIPROP)message, prop, &propval); if (FAILED (hr)) { log_debug ("%s:%s: HrGetOneProp(%s) failed: hr=%#lx\n", SRCNAME, __func__, propname, hr); TRETURN; } switch ( PROP_TYPE (propval->ulPropTag) ) { case PT_BINARY: keylen = propval->Value.bin.cb; key = propval->Value.bin.lpb; log_hexdump (key, keylen, "%s: %20s=", __func__, propname); break; case PT_UNICODE: buf = wchar_to_utf8 (propval->Value.lpszW); if (!buf) log_debug ("%s:%s: error converting to utf8\n", SRCNAME, __func__); else log_debug ("%s: %20s=`%s'", __func__, propname, buf); xfree (buf); break; case PT_STRING8: log_debug ("%s: %20s=`%s'", __func__, propname, propval->Value.lpszA); break; case PT_LONG: log_debug ("%s: %20s=%ld", __func__, propname, propval->Value.l); break; default: log_debug ("%s:%s: HrGetOneProp(%s) property type %lu not supported\n", SRCNAME, __func__, propname, PROP_TYPE (propval->ulPropTag) ); TRETURN; } MAPIFreeBuffer (propval); TRETURN; } /* Helper to create a named property. */ static ULONG create_gpgol_tag (LPMESSAGE message, const wchar_t *name, const char *func) { TSTART; HRESULT hr; LPSPropTagArray proparr = NULL; MAPINAMEID mnid, *pmnid; wchar_t *propname = xwcsdup (name); /* {31805ab8-3e92-11dc-879c-00061b031004}: GpgOL custom properties. */ GUID guid = {0x31805ab8, 0x3e92, 0x11dc, {0x87, 0x9c, 0x00, 0x06, 0x1b, 0x03, 0x10, 0x04}}; ULONG result; memset (&mnid, 0, sizeof mnid); mnid.lpguid = &guid; mnid.ulKind = MNID_STRING; mnid.Kind.lpwstrName = propname; pmnid = &mnid; hr = message->GetIDsFromNames (1, &pmnid, MAPI_CREATE, &proparr); xfree (propname); if (FAILED (hr)) proparr = NULL; if (FAILED (hr) || !(proparr->aulPropTag[0] & 0xFFFF0000) ) { log_error ("%s:%s: can't map GpgOL property: hr=%#lx\n", SRCNAME, func, hr); result = 0; } else result = (proparr->aulPropTag[0] & 0xFFFF0000); if (proparr) MAPIFreeBuffer (proparr); TRETURN result; } /* Return the property tag for GpgOL Msg Class. */ int get_gpgolmsgclass_tag (LPMESSAGE message, ULONG *r_tag) { TSTART; if (!(*r_tag = create_gpgol_tag (message, L"GpgOL Msg Class", __func__))) { TRETURN -1; } *r_tag |= PT_STRING8; TRETURN 0; } /* Return the property tag for GpgOL Old Msg Class. The Old Msg Class saves the message class as seen before we changed it the first time. */ int get_gpgololdmsgclass_tag (LPMESSAGE message, ULONG *r_tag) { TSTART; if (!(*r_tag = create_gpgol_tag (message, L"GpgOL Old Msg Class", __func__))) { TRETURN -1; } *r_tag |= PT_STRING8; TRETURN 0; } /* Return the property tag for GpgOL Attach Type. */ int get_gpgolattachtype_tag (LPMESSAGE message, ULONG *r_tag) { TSTART; if (!(*r_tag = create_gpgol_tag (message, L"GpgOL Attach Type", __func__))) { TRETURN -1; } *r_tag |= PT_LONG; TRETURN 0; } /* Return the property tag for GpgOL Protect IV. */ int get_gpgolprotectiv_tag (LPMESSAGE message, ULONG *r_tag) { TSTART; if (!(*r_tag = create_gpgol_tag (message, L"GpgOL Protect IV", __func__))) { TRETURN -1; } *r_tag |= PT_BINARY; TRETURN 0; } /* Return the property tag for GpgOL Last Decrypted. */ int get_gpgollastdecrypted_tag (LPMESSAGE message, ULONG *r_tag) { TSTART; if (!(*r_tag = create_gpgol_tag (message, L"GpgOL Last Decrypted",__func__))) { TRETURN -1; } *r_tag |= PT_BINARY; TRETURN 0; } /* Return the property tag for GpgOL MIME structure. */ int get_gpgolmimeinfo_tag (LPMESSAGE message, ULONG *r_tag) { TSTART; if (!(*r_tag = create_gpgol_tag (message, L"GpgOL MIME Info", __func__))) { TRETURN -1; } *r_tag |= PT_STRING8; TRETURN 0; } /* Return the property tag for GpgOL Charset. */ int get_gpgolcharset_tag (LPMESSAGE message, ULONG *r_tag) { TSTART; if (!(*r_tag = create_gpgol_tag (message, L"GpgOL Charset", __func__))) { TRETURN -1; } *r_tag |= PT_STRING8; TRETURN 0; } /* Return the property tag for GpgOL Draft Info. */ int get_gpgoldraftinfo_tag (LPMESSAGE message, ULONG *r_tag) { TSTART; if (!(*r_tag = create_gpgol_tag (message, L"GpgOL Draft Info", __func__))) { TRETURN -1; } *r_tag |= PT_STRING8; TRETURN 0; } /* Return the tag of the Internet Charset Body property which seems to hold the PR_BODY as received and thus before charset conversion. */ int get_internetcharsetbody_tag (LPMESSAGE message, ULONG *r_tag) { TSTART; HRESULT hr; LPSPropTagArray proparr = NULL; MAPINAMEID mnid, *pmnid; /* {4E3A7680-B77A-11D0-9DA5-00C04FD65685} */ GUID guid = {0x4E3A7680, 0xB77A, 0x11D0, {0x9D, 0xA5, 0x00, 0xC0, 0x4F, 0xD6, 0x56, 0x85}}; wchar_t propname[] = L"Internet Charset Body"; int result; memset (&mnid, 0, sizeof mnid); mnid.lpguid = &guid; mnid.ulKind = MNID_STRING; mnid.Kind.lpwstrName = propname; pmnid = &mnid; hr = message->GetIDsFromNames (1, &pmnid, 0, &proparr); if (FAILED (hr)) proparr = NULL; if (FAILED (hr) || !(proparr->aulPropTag[0] & 0xFFFF0000) ) { log_debug ("%s:%s: can't get the Internet Charset Body property:" " hr=%#lx\n", SRCNAME, __func__, hr); result = -1; } else { result = 0; *r_tag = ((proparr->aulPropTag[0] & 0xFFFF0000) | PT_BINARY); } if (proparr) MAPIFreeBuffer (proparr); TRETURN result; } /* Return the property tag for GpgOL UUID Info. */ static int get_gpgoluid_tag (LPMESSAGE message, ULONG *r_tag) { TSTART; if (!(*r_tag = create_gpgol_tag (message, L"GpgOL UID", __func__))) { TRETURN -1; } *r_tag |= PT_UNICODE; TRETURN 0; } char * mapi_get_uid (LPMESSAGE msg) { TSTART; /* If the UUID is not in OOM maybe we find it in mapi. */ if (!msg) { log_error ("%s:%s: Called without message", SRCNAME, __func__); TRETURN NULL; } ULONG tag; if (get_gpgoluid_tag (msg, &tag)) { log_debug ("%s:%s: Failed to get tag for '%p'", SRCNAME, __func__, msg); TRETURN NULL; } LPSPropValue propval = NULL; HRESULT hr = HrGetOneProp ((LPMAPIPROP)msg, tag, &propval); if (hr) { log_debug ("%s:%s: Failed to get prop for '%p'", SRCNAME, __func__, msg); TRETURN NULL; } char *ret = NULL; if (PROP_TYPE (propval->ulPropTag) == PT_UNICODE) { ret = wchar_to_utf8 (propval->Value.lpszW); log_debug ("%s:%s: Fund uuid in MAPI for %p", SRCNAME, __func__, msg); } else if (PROP_TYPE (propval->ulPropTag) == PT_STRING8) { ret = xstrdup (propval->Value.lpszA); log_debug ("%s:%s: Fund uuid in MAPI for %p", SRCNAME, __func__, msg); } MAPIFreeBuffer (propval); TRETURN ret; } /* A Wrapper around the SaveChanges method. This function should be called indirect through the mapi_save_changes macro. Returns 0 on success. */ int mapi_do_save_changes (LPMESSAGE message, ULONG flags, int only_del_body, const char *dbg_file, const char *dbg_func) { TSTART; HRESULT hr; SPropTagArray proparray; int any = 0; if (mapi_has_last_decrypted (message)) { 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", log_srcname (dbg_file), dbg_func); else any = 1; 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", log_srcname (dbg_file), dbg_func); else any = 1; } if (!only_del_body || any) { int i; for (i = 0, hr = 0; hr && i < 10; i++) { hr = message->SaveChanges (flags); if (hr) { log_debug ("%s:%s: Failed try to save.", SRCNAME, __func__); Sleep (1000); } } if (hr) { log_error ("%s:%s: SaveChanges(%lu) failed: hr=%#lx\n", log_srcname (dbg_file), dbg_func, (unsigned long)flags, hr); TRETURN -1; } } TRETURN 0; } /* Set an arbitary header in the message MSG with NAME to the value VAL. */ int mapi_set_header (LPMESSAGE msg, const char *name, const char *val) { HRESULT hr; LPSPropTagArray pProps = NULL; SPropValue pv; MAPINAMEID mnid, *pmnid; /* {00020386-0000-0000-C000-000000000046} -> GUID For X-Headers */ GUID guid = {0x00020386, 0x0000, 0x0000, {0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46} }; int result; if (!msg) { TRETURN -1; } memset (&mnid, 0, sizeof mnid); mnid.lpguid = &guid; mnid.ulKind = MNID_STRING; mnid.Kind.lpwstrName = utf8_to_wchar (name); pmnid = &mnid; hr = msg->GetIDsFromNames (1, &pmnid, MAPI_CREATE, &pProps); xfree (mnid.Kind.lpwstrName); if (FAILED (hr)) { pProps = NULL; log_error ("%s:%s: can't get mapping for header `%s': hr=%#lx\n", SRCNAME, __func__, name, hr); result = -1; } else { pv.ulPropTag = (pProps->aulPropTag[0] & 0xFFFF0000) | PT_STRING8; pv.Value.lpszA = (char *)val; hr = HrSetOneProp(msg, &pv); if (hr) { log_error ("%s:%s: can't set header `%s': hr=%#lx\n", SRCNAME, __func__, name, hr); result = -1; } else result = 0; } if (pProps) MAPIFreeBuffer (pProps); TRETURN result; } /* Return the data of the first attachment as string. Returns empty string on failure. */ static std::string mapi_get_first_attach_data (LPMESSAGE message) { TSTART; HRESULT hr; LPSTREAM stream = nullptr; ULONG bRead; std::string ret; if (!message) { STRANGEPOINT; TRETURN ret; } mapi_attach_item_t *table = mapi_create_attach_table (message, 0); if (!table) { log_debug ("%s:%s: Message has no attachments", SRCNAME, __func__); TRETURN ret; } if (table->end_of_table) { log_debug ("%s:%s: Message has no attachments", SRCNAME, __func__); mapi_release_attach_table (table); TRETURN ret; } stream = mapi_get_attach_as_stream (message, table, nullptr); mapi_release_attach_table (table); if (!stream) { log_debug ("%s:%s: Failed to get attachment as stream", SRCNAME, __func__); TRETURN ret; } char buf[8192]; while ((hr = stream->Read (buf, 8192, &bRead)) == S_OK || hr == S_FALSE) { if (!bRead) { // EOF break; } ret += std::string (buf, bRead); } gpgol_release (stream); TRETURN ret; } static int mapi_set_transport_headers (LPMESSAGE message, const char *data) { TSTART; if (!message) { STRANGEPOINT; TRETURN -1; } HRESULT hr; SPropValue prop; prop.ulPropTag = PR_TRANSPORT_MESSAGE_HEADERS; /* MAPI wants a non const value. So we better give it to avoid trouble */ prop.Value.lpszA = xstrdup (data); log_data ("Setting transport headers to:\n%s", data); hr = HrSetOneProp (message, &prop); xfree (prop.Value.lpszA); if (hr) { log_error ("%s:%s: can't set transport headerrs property: hr=%#lx\n", SRCNAME, __func__, hr); TRETURN -1; } TRETURN 0; } int mapi_set_content_type (LPMESSAGE message, const char *ct) { TSTART; if (!message) { STRANGEPOINT; TRETURN -1; } const auto headers = mapi_get_header (message); if (headers.empty ()) { log_err ("Failed to obtain original headers"); TRETURN -1; } std::stringstream newHeaders; bool inContentHeader = false; for (auto line: gpgol_split (headers, '\n')) { transform(line.begin(), line.end(), line.begin(), ::tolower); if (starts_with (line, "content-type")) { inContentHeader = true; if (ct) { newHeaders << "Content-type: " << ct << "\n"; } } else if (inContentHeader) { if (starts_with (line, " ") || starts_with (line, "\t")) { log_data ("Content header continuation %s ignored.", line.c_str()); continue; } else { inContentHeader = false; } } else if (starts_with (line, "content-")) { inContentHeader = true; continue; } else { newHeaders << line << "\n"; } } TRETURN mapi_set_transport_headers (message, newHeaders.str ().c_str ()); } /* Return the headers as ASCII string. Returns empty string on failure. */ std::string mapi_get_header (LPMESSAGE message) { TSTART; HRESULT hr; LPSTREAM stream = nullptr; ULONG bRead; std::string ret; if (!message) { TRETURN ret; } hr = gpgol_openProperty (message, PR_TRANSPORT_MESSAGE_HEADERS_A, &IID_IStream, 0, 0, (LPUNKNOWN*)&stream); if (hr) { log_debug ("%s:%s: OpenProperty failed: hr=%#lx", SRCNAME, __func__, hr); TRETURN ret; } char buf[8192]; while ((hr = stream->Read (buf, 8192, &bRead)) == S_OK || hr == S_FALSE) { if (!bRead) { // EOF break; } ret += std::string (buf, bRead); } gpgol_release (stream); TRETURN ret; } /* Return the body as a new IStream object. Returns NULL on failure. The stream Returns the body as an ASCII stream (Use mapi_get_body for an UTF-8 value). */ LPSTREAM mapi_get_body_as_stream (LPMESSAGE message) { TSTART; HRESULT hr; ULONG tag; LPSTREAM stream; if (!message) { TRETURN NULL; } if (!get_internetcharsetbody_tag (message, &tag) ) { /* The store knows about the Internet Charset Body property, thus try to get the body from this property if it exists. */ hr = gpgol_openProperty (message, tag, &IID_IStream, 0, 0, (LPUNKNOWN*)&stream); if (!hr) { TRETURN stream; } log_debug ("%s:%s: OpenProperty tag=%lx failed: hr=%#lx", SRCNAME, __func__, tag, hr); } /* We try to get it as an ASCII body. If this fails we would either need to implement some kind of stream filter to translated to utf-8 or read everyting into a memory buffer and [provide an istream from that memory buffer. */ hr = gpgol_openProperty (message, PR_BODY_A, &IID_IStream, 0, 0, (LPUNKNOWN*)&stream); if (hr) { log_debug ("%s:%s: OpenProperty failed: hr=%#lx", SRCNAME, __func__, hr); TRETURN NULL; } TRETURN stream; } /* Return the body of the message in an allocated buffer. The buffer is guaranteed to be Nul terminated. The actual length (ie. the strlen()) will be stored at R_NBYTES. The body will be returned in UTF-8 encoding. Returns NULL if no body is available. */ char * mapi_get_body (LPMESSAGE message, size_t *r_nbytes) { TSTART; HRESULT hr; LPSPropValue lpspvFEID = NULL; LPSTREAM stream; STATSTG statInfo; ULONG nread; char *body = NULL; if (r_nbytes) *r_nbytes = 0; hr = HrGetOneProp ((LPMAPIPROP)message, PR_BODY, &lpspvFEID); if (SUCCEEDED (hr)) /* Message is small enough to be retrieved directly. */ { switch ( PROP_TYPE (lpspvFEID->ulPropTag) ) { case PT_UNICODE: body = wchar_to_utf8 (lpspvFEID->Value.lpszW); if (!body) log_debug ("%s: error converting to utf8\n", __func__); break; case PT_STRING8: body = xstrdup (lpspvFEID->Value.lpszA); break; default: log_debug ("%s: proptag=0x%08lx not supported\n", __func__, lpspvFEID->ulPropTag); break; } MAPIFreeBuffer (lpspvFEID); } else /* Message is large; use an IStream to read it. */ { hr = gpgol_openProperty (message, PR_BODY, &IID_IStream, 0, 0, (LPUNKNOWN*)&stream); if (hr) { log_debug ("%s:%s: OpenProperty failed: hr=%#lx", SRCNAME, __func__, hr); TRETURN NULL; } hr = stream->Stat (&statInfo, STATFLAG_NONAME); if (hr) { log_debug ("%s:%s: Stat failed: hr=%#lx", SRCNAME, __func__, hr); gpgol_release (stream); TRETURN NULL; } /* Fixme: We might want to read only the first 1k to decide whether this is actually an OpenPGP message and only then continue reading. */ body = (char*)xmalloc ((size_t)statInfo.cbSize.QuadPart + 2); hr = stream->Read (body, (size_t)statInfo.cbSize.QuadPart, &nread); if (hr) { log_debug ("%s:%s: Read failed: hr=%#lx", SRCNAME, __func__, hr); xfree (body); gpgol_release (stream); TRETURN NULL; } body[nread] = 0; body[nread+1] = 0; if (nread != statInfo.cbSize.QuadPart) { log_debug ("%s:%s: not enough bytes returned\n", SRCNAME, __func__); xfree (body); gpgol_release (stream); TRETURN NULL; } gpgol_release (stream); { char *tmp; tmp = wchar_to_utf8 ((wchar_t*)body); if (!tmp) log_debug ("%s: error converting to utf8\n", __func__); else { xfree (body); body = tmp; } } } if (r_nbytes) *r_nbytes = strlen (body); TRETURN body; } /* Look at the body of the MESSAGE and try to figure out whether this is a supported PGP message. Returns the new message class or NULL if it does not look like a PGP message. If r_nobody is not null it is set to true if no body was found. */ static char * get_msgcls_from_pgp_lines (LPMESSAGE message, bool *r_nobody = nullptr) { TSTART; HRESULT hr; LPSTREAM stream; STATSTG statInfo; ULONG nread; size_t nbytes; char *body = NULL; char *p; char *msgcls = NULL; int is_wchar = 0; if (r_nobody) { *r_nobody = false; } stream = mapi_get_body_as_stream (message); if (!stream) { log_debug ("%s:%s: Failed to get body ASCII stream.", SRCNAME, __func__); hr = gpgol_openProperty (message, PR_BODY_W, &IID_IStream, 0, 0, (LPUNKNOWN*)&stream); if (hr) { log_error ("%s:%s: Failed to get w_body stream. : hr=%#lx", SRCNAME, __func__, hr); if (r_nobody) { *r_nobody = true; } TRETURN NULL; } else { is_wchar = 1; } } hr = stream->Stat (&statInfo, STATFLAG_NONAME); if (hr) { log_debug ("%s:%s: Stat failed: hr=%#lx", SRCNAME, __func__, hr); gpgol_release (stream); TRETURN NULL; } /* We read only the first 1k to decide whether this is actually an OpenPGP armored message . */ nbytes = (size_t)statInfo.cbSize.QuadPart; if (nbytes > 1024*2) nbytes = 1024*2; body = (char*)xmalloc (nbytes + 2); hr = stream->Read (body, nbytes, &nread); if (hr) { log_debug ("%s:%s: Read failed: hr=%#lx", SRCNAME, __func__, hr); xfree (body); gpgol_release (stream); TRETURN NULL; } body[nread] = 0; body[nread+1] = 0; if (nread != nbytes) { log_debug ("%s:%s: not enough bytes returned\n", SRCNAME, __func__); xfree (body); gpgol_release (stream); TRETURN NULL; } gpgol_release (stream); if (is_wchar) { char *tmp; tmp = wchar_to_utf8 ((wchar_t*)body); if (!tmp) log_debug ("%s: error converting to utf8\n", __func__); else { xfree (body); body = tmp; } } /* The first ~1k of the body of the message is now available in the utf-8 string BODY. Walk over it to figure out its type. */ for (p=body; p && *p; p = ((p=strchr (p+1, '\n')) ? (p+1) : NULL)) { if (!strncmp (p, "-----BEGIN PGP ", 15)) { /* Enabling clearsigned detection for Outlook 2010 and later would result in data loss as the signature is not reverted. */ if (!strncmp (p+15, "SIGNED MESSAGE-----", 19) && trailing_ws_p (p+15+19)) msgcls = xstrdup ("IPM.Note.GpgOL.ClearSigned"); else if (!strncmp (p+15, "MESSAGE-----", 12) && trailing_ws_p (p+15+12)) msgcls = xstrdup ("IPM.Note.GpgOL.PGPMessage"); break; } else if (!trailing_ws_p (p)) { /* We have text before the message. In that case we need to break because some bad MUA's like Outlook do not insert quote characters before a replied to message. In that case the reply to an inline Mail from an Outlook without GpgOL enabled could cause the behavior that we would detect the original message. */ log_debug ("%s:%s: Detected non whitespace %c before a PGP Marker", SRCNAME, __func__, *p); break; } } xfree (body); TRETURN msgcls; } /* Check whether the message is really a CMS encrypted message. We check here whether the message is really encrypted by looking at the object identifier inside the CMS data. Returns: -1 := Unknown message type, 0 := The message is signed, 1 := The message is encrypted. This function is required for two reasons: 1. Due to a bug in CryptoEx which sometimes assignes the *.CexEnc message class to signed messages and only updates the message class after accessing them. Thus in old stores there may be a lot of *.CexEnc message which are actually just signed. 2. If the smime-type parameter is missing we need another way to decide whether to decrypt or to verify. 3. Some messages lack a PR_TRANSPORT_MESSAGE_HEADERS and thus it is not possible to deduce the message type from the mail headers. This function may be used to identify the message anyway. */ static int is_really_cms_encrypted (LPMESSAGE message) { HRESULT hr; SizedSPropTagArray (1L, propAttNum) = { 1L, {PR_ATTACH_NUM} }; LPMAPITABLE mapitable; LPSRowSet mapirows; unsigned int pos, n_attach; int result = -1; /* Unknown. */ LPATTACH att = NULL; LPSTREAM stream = NULL; char buffer[24]; /* 24 bytes are more than enough to peek at. Cf. ksba_cms_identify() from the libksba package. */ const char *p; ULONG nread; size_t n; tlvinfo_t ti; hr = message->GetAttachmentTable (0, &mapitable); if (FAILED (hr)) { log_debug ("%s:%s: GetAttachmentTable failed: hr=%#lx", SRCNAME, __func__, hr); TRETURN -1; } memdbg_addRef (mapitable); hr = HrQueryAllRows (mapitable, (LPSPropTagArray)&propAttNum, NULL, NULL, 0, &mapirows); if (FAILED (hr)) { log_debug ("%s:%s: HrQueryAllRows failed: hr=%#lx", SRCNAME, __func__, hr); gpgol_release (mapitable); TRETURN -1; } n_attach = mapirows->cRows > 0? mapirows->cRows : 0; if (n_attach != 1) { FreeProws (mapirows); gpgol_release (mapitable); log_debug ("%s:%s: not just one attachment", SRCNAME, __func__); TRETURN -1; } pos = 0; if (mapirows->aRow[pos].cValues < 1) { log_error ("%s:%s: invalid row at pos %d", SRCNAME, __func__, pos); goto leave; } if (mapirows->aRow[pos].lpProps[0].ulPropTag != PR_ATTACH_NUM) { log_error ("%s:%s: invalid prop at pos %d", SRCNAME, __func__, pos); goto leave; } hr = message->OpenAttach (mapirows->aRow[pos].lpProps[0].Value.l, NULL, MAPI_BEST_ACCESS, &att); if (FAILED (hr)) { log_error ("%s:%s: can't open attachment %d (%ld): hr=%#lx", SRCNAME, __func__, pos, mapirows->aRow[pos].lpProps[0].Value.l, hr); goto leave; } memdbg_addRef (att); if (!has_smime_filename (att)) { log_debug ("%s:%s: no smime filename", SRCNAME, __func__); goto leave; } if (get_attach_method (att) != ATTACH_BY_VALUE) { log_debug ("%s:%s: wrong attach method", SRCNAME, __func__); goto leave; } hr = gpgol_openProperty (att, PR_ATTACH_DATA_BIN, &IID_IStream, 0, 0, (LPUNKNOWN*) &stream); if (FAILED (hr)) { log_error ("%s:%s: can't open data stream of attachment: hr=%#lx", SRCNAME, __func__, hr); goto leave; } memdbg_addRef (stream); hr = stream->Read (buffer, sizeof buffer, &nread); if ( hr != S_OK ) { log_error ("%s:%s: Read failed: hr=%#lx", SRCNAME, __func__, hr); goto leave; } if (nread < sizeof buffer) { log_error ("%s:%s: not enough bytes returned\n", SRCNAME, __func__); goto leave; } p = buffer; n = nread; if (parse_tlv (&p, &n, &ti)) goto leave; if (!(ti.cls == ASN1_CLASS_UNIVERSAL && ti.tag == ASN1_TAG_SEQUENCE && ti.is_cons) ) goto leave; if (parse_tlv (&p, &n, &ti)) goto leave; if (!(ti.cls == ASN1_CLASS_UNIVERSAL && ti.tag == ASN1_TAG_OBJECT_ID && !ti.is_cons && ti.length) || ti.length > n) goto leave; /* Now is this enveloped data (1.2.840.113549.1.7.3) or signed data (1.2.840.113549.1.7.2) ? */ if (ti.length == 9) { if (!memcmp (p, "\x2A\x86\x48\x86\xF7\x0D\x01\x07\x03", 9)) result = 1; /* Encrypted. */ else if (!memcmp (p, "\x2A\x86\x48\x86\xF7\x0D\x01\x07\x02", 9)) result = 0; /* Signed. */ } leave: if (stream) gpgol_release (stream); if (att) gpgol_release (att); FreeProws (mapirows); gpgol_release (mapitable); TRETURN result; } /* Return the content-type of the first and only attachment of MESSAGE or NULL if it does not exists. Caller must free. */ static char * get_first_attach_mime_tag (LPMESSAGE message) { HRESULT hr; SizedSPropTagArray (1L, propAttNum) = { 1L, {PR_ATTACH_NUM} }; LPMAPITABLE mapitable; LPSRowSet mapirows; unsigned int pos, n_attach; LPATTACH att = NULL; char *result = NULL; hr = message->GetAttachmentTable (0, &mapitable); if (FAILED (hr)) { log_debug ("%s:%s: GetAttachmentTable failed: hr=%#lx", SRCNAME, __func__, hr); TRETURN NULL; } memdbg_addRef (mapitable); hr = HrQueryAllRows (mapitable, (LPSPropTagArray)&propAttNum, NULL, NULL, 0, &mapirows); if (FAILED (hr)) { log_debug ("%s:%s: HrQueryAllRows failed: hr=%#lx", SRCNAME, __func__, hr); gpgol_release (mapitable); TRETURN NULL; } n_attach = mapirows->cRows > 0? mapirows->cRows : 0; if (n_attach < 1) { FreeProws (mapirows); gpgol_release (mapitable); log_debug ("%s:%s: less then one attachment", SRCNAME, __func__); TRETURN NULL; } pos = 0; if (mapirows->aRow[pos].cValues < 1) { log_error ("%s:%s: invalid row at pos %d", SRCNAME, __func__, pos); goto leave; } if (mapirows->aRow[pos].lpProps[0].ulPropTag != PR_ATTACH_NUM) { log_error ("%s:%s: invalid prop at pos %d", SRCNAME, __func__, pos); goto leave; } hr = message->OpenAttach (mapirows->aRow[pos].lpProps[0].Value.l, NULL, MAPI_BEST_ACCESS, &att); if (FAILED (hr)) { log_error ("%s:%s: can't open attachment %d (%ld): hr=%#lx", SRCNAME, __func__, pos, mapirows->aRow[pos].lpProps[0].Value.l, hr); goto leave; } memdbg_addRef (att); /* Note: We do not expect a filename. */ if (get_attach_method (att) != ATTACH_BY_VALUE) { log_debug ("%s:%s: wrong attach method", SRCNAME, __func__); goto leave; } result = get_attach_mime_tag (att); leave: if (att) gpgol_release (att); FreeProws (mapirows); gpgol_release (mapitable); TRETURN result; } /* Look at the first attachment's content type to determine the messageclass. */ static char * get_msgcls_from_first_attachment (LPMESSAGE message) { TSTART; char *ret = nullptr; char *attach_mime = get_first_attach_mime_tag (message); if (!attach_mime) { TRETURN nullptr; } if (!strcmp (attach_mime, "application/pgp-encrypted")) { ret = xstrdup ("IPM.Note.GpgOL.MultipartEncrypted"); xfree (attach_mime); } else if (!strcmp (attach_mime, "application/pgp-signature")) { ret = xstrdup ("IPM.Note.GpgOL.MultipartSigned"); xfree (attach_mime); } TRETURN ret; } /* Helper for mapi_change_message_class. Returns the new message class as an allocated string. Most message today are of the message class "IPM.Note". However a PGP/MIME encrypted message also has this class. We need to see whether we can detect such a mail right here and change the message class accordingly. */ static char * change_message_class_ipm_note (LPMESSAGE message) { TSTART; char *newvalue = NULL; char *ct, *proto; ct = mapi_get_message_content_type (message, &proto, NULL); log_debug ("%s:%s: content type is '%s'", SRCNAME, __func__, ct ? ct : "null"); /* First the simplest check */ if (ct && !strcmp (ct, "wks.confirmation.mail")) { log_dbg ("Setting WKSConfirmation"); newvalue = xstrdup ("IPM.Note.GpgOL.WKSConfirmation"); } else if (ct && proto) { log_debug ("%s:%s: protocol is '%s'", SRCNAME, __func__, proto); if (!strcmp (ct, "multipart/encrypted") && !strcmp (proto, "application/pgp-encrypted")) { newvalue = xstrdup ("IPM.Note.GpgOL.MultipartEncrypted"); } else if (!strcmp (ct, "multipart/signed") && !strcmp (proto, "application/pgp-signature")) { /* Sometimes we receive a PGP/MIME signed message with a class IPM.Note. */ newvalue = xstrdup ("IPM.Note.GpgOL.MultipartSigned"); } else if (!strcmp (ct, "multipart/mixed") && !strcmp (proto, "application/pgp-encrypted")) { /* This case happens if an encrypted mail is moved by outlook local filter rules. */ newvalue = xstrdup ("IPM.Note.GpgOL.MultipartEncrypted"); } xfree (proto); } else if (ct && !strcmp (ct, "application/ms-tnef")) { /* ms-tnef can either be inline PGP or PGP/MIME. First check for inline and then look at the attachments if they look like PGP /MIME .*/ newvalue = get_msgcls_from_pgp_lines (message); if (!newvalue) { /* So no PGP Inline. Lets look at the attachment. */ newvalue = get_msgcls_from_first_attachment (message); } } else if (!ct || !strcmp (ct, "text/plain") || !strcmp (ct, "multipart/mixed") || !strcmp (ct, "multipart/alternative") || !strcmp (ct, "multipart/related") || !strcmp (ct, "text/html")) { bool has_no_body = false; /* It is quite common to have a multipart/mixed or alternative mail with separate encrypted PGP parts. Look at the body to decide. */ newvalue = get_msgcls_from_pgp_lines (message, &has_no_body); if (!newvalue && has_no_body && ct && !strcmp (ct, "multipart/mixed")) { /* This is uncommon. But some Exchanges might break a PGP/MIME mail this way. Let's take a look at the attachments. Maybe it's a PGP/MIME mail. */ log_debug ("%s:%s: Multipart mixed without body found. Looking at attachments.", SRCNAME, __func__); newvalue = get_msgcls_from_first_attachment (message); } } xfree (ct); TRETURN newvalue; } /* Helper for mapi_change_message_class. Returns the new message class as an allocated string. This function is used for the message class "IPM.Note.SMIME". It indicates an S/MIME opaque encrypted or signed message. This may also be an PGP/MIME mail. */ static char * change_message_class_ipm_note_smime (LPMESSAGE message) { TSTART; char *newvalue = NULL; char *ct, *proto, *smtype; ct = mapi_get_message_content_type (message, &proto, &smtype); if (ct) { log_debug ("%s:%s: content type is '%s'", SRCNAME, __func__, ct); if (proto && !strcmp (ct, "multipart/signed") && !strcmp (proto, "application/pgp-signature")) { newvalue = xstrdup ("IPM.Note.GpgOL.MultipartSigned"); } else if (!opt.enable_smime) ; /* S/MIME not enabled; thus no further checks. */ else if (smtype) { log_debug ("%s:%s: smime-type is '%s'", SRCNAME, __func__, smtype); if (!strcmp (ct, "application/pkcs7-mime") || !strcmp (ct, "application/x-pkcs7-mime")) { if (!strcmp (smtype, "signed-data")) newvalue = xstrdup ("IPM.Note.GpgOL.OpaqueSigned"); else if (!strcmp (smtype, "enveloped-data")) newvalue = xstrdup ("IPM.Note.GpgOL.OpaqueEncrypted"); } } else if (proto && !strcmp (ct, "multipart/signed") && (!strcmp (proto, "application/pkcs7-signature") || !strcmp (proto, "application/x-pkcs7-signature"))) { log_debug ("%s:%s: protocol is '%s'", SRCNAME, __func__, proto); newvalue = xstrdup ("IPM.Note.GpgOL.SM.MultipartSigned"); } else { /* No smime type. The filename parameter is often not reliable, thus we better look into the message to see if it is encrypted and assume an opaque signed one if this is not the case. */ switch (is_really_cms_encrypted (message)) { case 0: newvalue = xstrdup ("IPM.Note.GpgOL.OpaqueSigned"); break; case 1: newvalue = xstrdup ("IPM.Note.GpgOL.OpaqueEncrypted"); break; } } xfree (smtype); xfree (proto); xfree (ct); } else { log_debug ("%s:%s: message has no content type", SRCNAME, __func__); /* CryptoEx (or the Toltec Connector) create messages without the transport headers property and thus we don't know the content type. We try to detect the message type anyway by looking into the first and only attachments. */ switch (is_really_cms_encrypted (message)) { case 0: newvalue = xstrdup ("IPM.Note.GpgOL.OpaqueSigned"); break; case 1: newvalue = xstrdup ("IPM.Note.GpgOL.OpaqueEncrypted"); break; default: /* Unknown. */ break; } } /* If we did not found anything but let's change the class anyway. */ if (!newvalue && opt.enable_smime) { log_debug ("%s:%s WARNING: Failed to detect smime. Fallback " "to OpaqueEncrypted", SRCNAME, __func__); newvalue = xstrdup ("IPM.Note.GpgOL.OpaqueEncrypted"); } TRETURN newvalue; } /* Helper for mapi_change_message_class. Returns the new message class as an allocated string. This function is used for the message class "IPM.Note.SMIME.MultipartSigned". This is an S/MIME message class but smime support is not enabled. We need to check whether this is actually a PGP/MIME message. */ static char * change_message_class_ipm_note_smime_multipartsigned (LPMESSAGE message) { TSTART; char *newvalue = NULL; char *ct, *proto; ct = mapi_get_message_content_type (message, &proto, NULL); if (ct) { log_debug ("%s:%s: content type is '%s'", SRCNAME, __func__, ct); if (proto && !strcmp (ct, "multipart/signed") && !strcmp (proto, "application/pgp-signature")) { newvalue = xstrdup ("IPM.Note.GpgOL.MultipartSigned"); } else if (!strcmp (ct, "wks.confirmation.mail")) { newvalue = xstrdup ("IPM.Note.GpgOL.WKSConfirmation"); } xfree (proto); xfree (ct); } else log_debug ("%s:%s: message has no content type", SRCNAME, __func__); TRETURN newvalue; } /* Helper for mapi_change_message_class. Returns the new message class as an allocated string. This function is used for the message classes "IPM.Note.Secure.CexSig" and "IPM.Note.Secure.Cexenc" (in the latter case IS_CEXSIG is true). These are CryptoEx generated signature or encryption messages. */ static char * change_message_class_ipm_note_secure_cex (LPMESSAGE message, int is_cexenc) { TSTART; char *newvalue = NULL; char *ct, *smtype, *proto; ct = mapi_get_message_content_type (message, &proto, &smtype); if (ct) { log_debug ("%s:%s: content type is '%s'", SRCNAME, __func__, ct); if (smtype) log_debug ("%s:%s: smime-type is '%s'", SRCNAME, __func__, smtype); if (proto) log_debug ("%s:%s: protocol is '%s'", SRCNAME, __func__, proto); if (smtype) { if (!strcmp (ct, "application/pkcs7-mime") || !strcmp (ct, "application/x-pkcs7-mime")) { if (!strcmp (smtype, "signed-data")) newvalue = xstrdup ("IPM.Note.GpgOL.OpaqueSigned"); else if (!strcmp (smtype, "enveloped-data")) newvalue = xstrdup ("IPM.Note.GpgOL.OpaqueEncrypted"); } } if (!newvalue && proto) { if (!strcmp (ct, "multipart/signed") && (!strcmp (proto, "application/pkcs7-signature") || !strcmp (proto, "application/x-pkcs7-signature"))) { newvalue = xstrdup ("IPM.Note.GpgOL.SM.MultipartSigned"); } else if (!strcmp (ct, "multipart/signed") && (!strcmp (proto, "application/pgp-signature"))) { newvalue = xstrdup ("IPM.Note.GpgOL.MultipartSigned"); } } if (!newvalue && (!strcmp (ct, "text/plain") || !strcmp (ct, "multipart/alternative") || !strcmp (ct, "multipart/mixed"))) { newvalue = get_msgcls_from_pgp_lines (message); } if (!newvalue) { switch (is_really_cms_encrypted (message)) { case 0: newvalue = xstrdup ("IPM.Note.GpgOL.OpaqueSigned"); break; case 1: newvalue = xstrdup ("IPM.Note.GpgOL.OpaqueEncrypted"); break; } } xfree (smtype); xfree (proto); xfree (ct); } else { log_debug ("%s:%s: message has no content type", SRCNAME, __func__); if (is_cexenc) { switch (is_really_cms_encrypted (message)) { case 0: newvalue = xstrdup ("IPM.Note.GpgOL.OpaqueSigned"); break; case 1: newvalue = xstrdup ("IPM.Note.GpgOL.OpaqueEncrypted"); break; } } else { char *mimetag; mimetag = get_first_attach_mime_tag (message); if (mimetag && !strcmp (mimetag, "multipart/signed")) newvalue = xstrdup ("IPM.Note.GpgOL.MultipartSigned"); xfree (mimetag); } if (!newvalue) { newvalue = get_msgcls_from_pgp_lines (message); } } if (!newvalue) newvalue = xstrdup ("IPM.Note.GpgOL.OpaqueEncrypted"); TRETURN newvalue; } static msgtype_t string_to_type (const char *s) { TSTART; if (!s || strlen (s) < 14) { TRETURN MSGTYPE_UNKNOWN; } if (!strncmp (s, "IPM.Note.GpgOL", 14) && (!s[14] || s[14] =='.')) { s += 14; if (!*s) { TRETURN MSGTYPE_GPGOL; } else if (!strcmp (s, ".SM.MultipartSigned")) { TRETURN MSGTYPE_GPGOL_MULTIPART_SIGNED; } else if (!strcmp (s, ".MultipartSigned")) { TRETURN MSGTYPE_GPGOL_MULTIPART_SIGNED; } else if (!strcmp (s, ".MultipartEncrypted")) { TRETURN MSGTYPE_GPGOL_MULTIPART_ENCRYPTED; } else if (!strcmp (s, ".OpaqueSigned")) { TRETURN MSGTYPE_GPGOL_OPAQUE_SIGNED; } else if (!strcmp (s, ".OpaqueEncrypted")) { TRETURN MSGTYPE_GPGOL_OPAQUE_ENCRYPTED; } else if (!strcmp (s, ".ClearSigned")) { TRETURN MSGTYPE_GPGOL_CLEAR_SIGNED; } else if (!strcmp (s, ".PGPMessage")) { TRETURN MSGTYPE_GPGOL_PGP_MESSAGE; } else if (!strcmp (s, ".WKSConfirmation")) { TRETURN MSGTYPE_GPGOL_WKS_CONFIRMATION; } else log_debug ("%s:%s: message class `%s' not supported", SRCNAME, __func__, s-14); } else if (!strncmp (s, "IPM.Note.SMIME", 14) && (!s[14] || s[14] =='.')) { TRETURN MSGTYPE_SMIME; } TRETURN MSGTYPE_UNKNOWN; } /* This function checks whether MESSAGE requires processing by us and adjusts the message class to our own. By passing true for SYNC_OVERRIDE the actual MAPI message class will be updated to our own message class overide. TRETURN true if the message was changed. */ int mapi_change_message_class (LPMESSAGE message, int sync_override, msgtype_t *r_type) { TSTART; HRESULT hr; ULONG tag; SPropValue prop; LPSPropValue propval = NULL; char *newvalue = NULL; int need_save = 0; int have_override = 0; if (!message) { TRETURN 0; /* No message: Nop. */ } if (get_gpgolmsgclass_tag (message, &tag) ) { - TRETURN 0; /* Ooops. */ + log_dbg ("Failed to create msgclass tag. Mail might be read only."); } hr = HrGetOneProp ((LPMAPIPROP)message, tag, &propval); if (FAILED (hr)) { hr = HrGetOneProp ((LPMAPIPROP)message, PR_MESSAGE_CLASS_A, &propval); if (FAILED (hr)) { log_error ("%s:%s: HrGetOneProp() failed: hr=%#lx\n", SRCNAME, __func__, hr); TRETURN 0; } } else { have_override = 1; log_debug ("%s:%s: have override message class\n", SRCNAME, __func__); } if ( PROP_TYPE (propval->ulPropTag) == PT_STRING8 ) { const char *s = propval->Value.lpszA; int cexenc = 0; log_debug ("%s:%s: checking message class `%s'", SRCNAME, __func__, s); if (!strcmp (s, "IPM.Note")) { newvalue = change_message_class_ipm_note (message); } else if (opt.enable_smime && (!strcmp (s, "IPM.Note.SMIME") || !strcmp (s, "IPM.Note.GpgOL"))) { newvalue = change_message_class_ipm_note_smime (message); } else if (opt.enable_smime && !strncmp (s, "IPM.Note.SMIME", 14) && (!s[14]||s[14] =='.')) { /* This is "IPM.Note.SMIME.foo" (where ".foo" is optional but the previous condition has already taken care of this). Note that we can't just insert a new part and keep the SMIME; we need to change the SMIME part of the class name so that Outlook does not process it as an SMIME message. */ char *tmp = change_message_class_ipm_note_smime_multipartsigned (message); /* This case happens even for PGP/MIME mails but that is ok as we later fiddle out the protocol. But we have to check if this is a WKS Mail now so that we can do the special handling for that. */ if (tmp && !strcmp (tmp, "IPM.Note.GpgOL.WKSConfirmation")) { newvalue = tmp; } else { xfree (tmp); gpgrt_asprintf (&newvalue, "IPM.Note.GpgOL.SM%s", s+14); memdbg_alloc (newvalue); } } else if (!strcmp (s, "IPM.Note.SMIME.MultipartSigned")) { /* This is an S/MIME message class but smime support is not enabled. We need to check whether this is actually a PGP/MIME message. */ newvalue = change_message_class_ipm_note_smime_multipartsigned (message); } else if (sync_override && have_override && !strncmp (s, "IPM.Note.GpgOL", 14) && (!s[14]||s[14] =='.')) { /* In case the original message class is not yet an GpgOL class we set it here. This is needed to convince Outlook not to do any special processing for IPM.Note.SMIME etc. */ LPSPropValue propval2 = NULL; hr = HrGetOneProp ((LPMAPIPROP)message, PR_MESSAGE_CLASS_A, &propval2); if (!SUCCEEDED (hr)) { log_debug ("%s:%s: Failed to get PR_MESSAGE_CLASS_A property.", SRCNAME, __func__); } else if (PROP_TYPE (propval2->ulPropTag) != PT_STRING8) { log_debug ("%s:%s: PR_MESSAGE_CLASS_A is not string.", SRCNAME, __func__); } else if (!propval2->Value.lpszA) { log_debug ("%s:%s: PR_MESSAGE_CLASS_A is null.", SRCNAME, __func__); } else if (!strcmp (propval2->Value.lpszA, s)) { log_debug ("%s:%s: PR_MESSAGE_CLASS_A is already the same.", SRCNAME, __func__); } else { newvalue = (char*)xstrdup (s); } MAPIFreeBuffer (propval2); } else if (opt.enable_smime && (!strcmp (s, "IPM.Note.Secure.CexSig") || (cexenc = !strcmp (s, "IPM.Note.Secure.CexEnc")))) { newvalue = change_message_class_ipm_note_secure_cex (message, cexenc); } if (r_type && !newvalue) { *r_type = string_to_type (s); } } if (!newvalue) { log_debug ("%s:%s Message is not a crypto message or already in the right class.", SRCNAME, __func__); } else { if (r_type) { *r_type = string_to_type (newvalue); } /* Save old message class if not yet done. (The second condition is just a failsafe check). */ if (!get_gpgololdmsgclass_tag (message, &tag) && PROP_TYPE (propval->ulPropTag) == PT_STRING8) { LPSPropValue propval2 = NULL; hr = HrGetOneProp ((LPMAPIPROP)message, tag, &propval2); if (!FAILED (hr)) MAPIFreeBuffer (propval2); else { /* No such property - save it. */ log_debug ("%s:%s: saving old message class\n", SRCNAME, __func__); prop.ulPropTag = tag; prop.Value.lpszA = propval->Value.lpszA; hr = message->SetProps (1, &prop, NULL); if (hr) { log_error ("%s:%s: can't save old message class: hr=%#lx\n", SRCNAME, __func__, hr); MAPIFreeBuffer (propval); TRETURN 0; } need_save = 1; } } /* Change message class. */ log_debug ("%s:%s: setting message class to `%s'\n", SRCNAME, __func__, newvalue); prop.ulPropTag = PR_MESSAGE_CLASS_A; prop.Value.lpszA = newvalue; hr = message->SetProps (1, &prop, NULL); xfree (newvalue); if (hr) { log_error ("%s:%s: can't set message class: hr=%#lx\n", SRCNAME, __func__, hr); MAPIFreeBuffer (propval); TRETURN 0; } need_save = 1; } MAPIFreeBuffer (propval); if (need_save) { if (mapi_save_changes (message, KEEP_OPEN_READWRITE|FORCE_SAVE)) { TRETURN 0; } } TRETURN 1; } /* Return the message class. This function will never return NULL so it is mostly useful for debugging. Caller needs to release the returned string. */ char * mapi_get_message_class (LPMESSAGE message) { TSTART; HRESULT hr; LPSPropValue propval = NULL; char *retstr; if (!message) { TRETURN xstrdup ("[No message]"); } hr = HrGetOneProp ((LPMAPIPROP)message, PR_MESSAGE_CLASS_A, &propval); if (FAILED (hr)) { log_error ("%s:%s: HrGetOneProp() failed: hr=%#lx\n", SRCNAME, __func__, hr); TRETURN xstrdup (hr == MAPI_E_NOT_FOUND? "[No message class property]": "[Error getting message class property]"); } if ( PROP_TYPE (propval->ulPropTag) == PT_STRING8 ) retstr = xstrdup (propval->Value.lpszA); else retstr = xstrdup ("[Invalid message class property]"); MAPIFreeBuffer (propval); TRETURN retstr; } /* Return the old message class. This function returns NULL if no old message class has been saved. Caller needs to release the returned string. */ char * mapi_get_old_message_class (LPMESSAGE message) { TSTART; HRESULT hr; ULONG tag; LPSPropValue propval = NULL; char *retstr; if (!message) { TRETURN NULL; } if (get_gpgololdmsgclass_tag (message, &tag)) { TRETURN NULL; } hr = HrGetOneProp ((LPMAPIPROP)message, tag, &propval); if (FAILED (hr)) { log_error ("%s:%s: HrGetOneProp() failed: hr=%#lx\n", SRCNAME, __func__, hr); TRETURN NULL; } if ( PROP_TYPE (propval->ulPropTag) == PT_STRING8 ) retstr = xstrdup (propval->Value.lpszA); else retstr = NULL; MAPIFreeBuffer (propval); TRETURN retstr; } /* Return the sender of the message. According to the specs this is an UTF-8 string; we rely on that the UI server handles internationalized domain names. */ char * mapi_get_sender (LPMESSAGE message) { TSTART; HRESULT hr; LPSPropValue propval = NULL; char *buf; char *p0, *p; if (!message) { TRETURN NULL; /* No message: Nop. */ } hr = HrGetOneProp ((LPMAPIPROP)message, PR_PRIMARY_SEND_ACCT, &propval); if (FAILED (hr)) { log_debug ("%s:%s: HrGetOneProp failed: hr=%#lx\n", SRCNAME, __func__, hr); TRETURN NULL; } if (PROP_TYPE (propval->ulPropTag) != PT_UNICODE) { log_debug ("%s:%s: HrGetOneProp Returns invalid type %lu\n", SRCNAME, __func__, PROP_TYPE (propval->ulPropTag) ); MAPIFreeBuffer (propval); TRETURN NULL; } buf = wchar_to_utf8 (propval->Value.lpszW); MAPIFreeBuffer (propval); if (!buf) { log_error ("%s:%s: error converting to utf8\n", SRCNAME, __func__); TRETURN NULL; } /* The PR_PRIMARY_SEND_ACCT property seems to be divided into fields using Ctrl-A as delimiter. The first field looks like the ascii formatted number of fields to follow, the second field like the email account and the third seems to be a textual description of that account. We return the second field. */ p = strchr (buf, '\x01'); if (!p) { log_error ("%s:%s: unknown format of the value `%s'\n", SRCNAME, __func__, anonstr (buf)); xfree (buf); TRETURN NULL; } for (p0=buf, p++; *p && *p != '\x01';) *p0++ = *p++; *p0 = 0; /* When using an Exchange account this is an X.509 address and not an SMTP address. We try to detect this here and extract only the CN RDN. Note that there are two CNs. This is just a simple approach and not a real parser. A better way to do this would be to ask MAPI to resolve the X.500 name to an SMTP name. */ if (strstr (buf, "/o=") && strstr (buf, "/ou=") && (p = strstr (buf, "/cn=Recipients")) && (p = strstr (p+1, "/cn="))) { log_debug ("%s:%s: orig address is `%s'\n", SRCNAME, __func__, anonstr (buf)); memmove (buf, p+4, strlen (p+4)+1); if (!strchr (buf, '@')) { /* Some Exchange accounts return only the accoutn name and no rfc821 mail address. Kleopatra chokes on that, thus we append a domain name. Thisis a bad hack. */ char *newbuf = (char *)xmalloc (strlen (buf) + 6 + 1); strcpy (stpcpy (newbuf, buf), "@local"); xfree (buf); buf = newbuf; } } log_debug ("%s:%s: address is `%s'\n", SRCNAME, __func__, anonstr (buf)); TRETURN buf; } static char * resolve_ex_from_address (LPMESSAGE message) { TSTART; HRESULT hr; char *sender_entryid; size_t entryidlen; LPMAPISESSION session; ULONG utype; LPUNKNOWN user; LPSPropValue propval = NULL; char *buf; if (g_ol_version_major < 14) { log_debug ("%s:%s: Not implemented for Ol < 14", SRCNAME, __func__); TRETURN NULL; } sender_entryid = mapi_get_binary_prop (message, PR_SENDER_ENTRYID, &entryidlen); if (!sender_entryid) { log_error ("%s:%s: Error: %i", SRCNAME, __func__, __LINE__); TRETURN NULL; } session = get_oom_mapi_session (); if (!session) { log_error ("%s:%s: Error: %i", SRCNAME, __func__, __LINE__); xfree (sender_entryid); TRETURN NULL; } hr = session->OpenEntry (entryidlen, (LPENTRYID)sender_entryid, &IID_IMailUser, MAPI_BEST_ACCESS | MAPI_CACHE_ONLY, &utype, (IUnknown**)&user); if (FAILED (hr)) { log_debug ("%s:%s: Failed to open cached entry. Fallback to uncached.", SRCNAME, __func__); hr = session->OpenEntry (entryidlen, (LPENTRYID)sender_entryid, &IID_IMailUser, MAPI_BEST_ACCESS, &utype, (IUnknown**)&user); } gpgol_release (session); if (FAILED (hr)) { log_error ("%s:%s: Error: %i", SRCNAME, __func__, __LINE__); TRETURN NULL; } hr = HrGetOneProp ((LPMAPIPROP)user, PR_SMTP_ADDRESS_W, &propval); if (FAILED (hr)) { log_error ("%s:%s: Error: %i", SRCNAME, __func__, __LINE__); TRETURN NULL; } if (PROP_TYPE (propval->ulPropTag) != PT_UNICODE) { log_debug ("%s:%s: HrGetOneProp Returns invalid type %lu\n", SRCNAME, __func__, PROP_TYPE (propval->ulPropTag) ); MAPIFreeBuffer (propval); TRETURN NULL; } buf = wchar_to_utf8 (propval->Value.lpszW); MAPIFreeBuffer (propval); TRETURN buf; } /* Return the from address of the message as a malloced UTF-8 string. returns NULL if that address is not available. */ char * mapi_get_from_address (LPMESSAGE message) { TSTART; HRESULT hr; LPSPropValue propval = NULL; char *buf; ULONG try_props[3] = {PidTagSenderSmtpAddress_W, PR_SENT_REPRESENTING_SMTP_ADDRESS_W, PR_SENDER_EMAIL_ADDRESS_W}; if (!message) { TRETURN xstrdup ("[no message]"); /* Ooops. */ } for (int i = 0; i < 3; i++) { /* We try to get different properties first as they contain the SMTP address of the sender. EMAIL address can be some LDAP stuff for exchange. */ hr = HrGetOneProp ((LPMAPIPROP)message, try_props[i], &propval); if (!FAILED (hr)) { break; } } /* This is the last result that should always work but not necessarily contain an SMTP Address. */ if (FAILED (hr)) { log_debug ("%s:%s: HrGetOneProp failed: hr=%#lx\n", SRCNAME, __func__, hr); TRETURN NULL; } if (PROP_TYPE (propval->ulPropTag) != PT_UNICODE) { log_debug ("%s:%s: HrGetOneProp Returns invalid type %lu\n", SRCNAME, __func__, PROP_TYPE (propval->ulPropTag) ); MAPIFreeBuffer (propval); TRETURN NULL; } buf = wchar_to_utf8 (propval->Value.lpszW); MAPIFreeBuffer (propval); if (!buf) { log_error ("%s:%s: error converting to utf8\n", SRCNAME, __func__); TRETURN NULL; } if (strstr (buf, "/o=")) { char *buf2; /* If both SMTP Address properties are not set we need to fallback to resolve the address through the address book */ log_debug ("%s:%s: resolving exchange address.", SRCNAME, __func__); buf2 = resolve_ex_from_address (message); if (buf2) { xfree (buf); TRETURN buf2; } } TRETURN buf; } /* Return the subject of the message as a malloced UTF-8 string. returns a replacement string if a subject is missing. */ char * mapi_get_subject (LPMESSAGE message) { TSTART; HRESULT hr; LPSPropValue propval = NULL; char *buf; if (!message) { TRETURN xstrdup ("[no message]"); /* Ooops. */ } hr = HrGetOneProp ((LPMAPIPROP)message, PR_SUBJECT_W, &propval); if (FAILED (hr)) { log_debug ("%s:%s: HrGetOneProp failed: hr=%#lx\n", SRCNAME, __func__, hr); TRETURN xstrdup (_("[no subject]")); } if (PROP_TYPE (propval->ulPropTag) != PT_UNICODE) { log_debug ("%s:%s: HrGetOneProp Returns invalid type %lu\n", SRCNAME, __func__, PROP_TYPE (propval->ulPropTag) ); MAPIFreeBuffer (propval); TRETURN xstrdup (_("[no subject]")); } buf = wchar_to_utf8 (propval->Value.lpszW); MAPIFreeBuffer (propval); if (!buf) { log_error ("%s:%s: error converting to utf8\n", SRCNAME, __func__); TRETURN xstrdup (_("[no subject]")); } TRETURN buf; } /* Return the message type. This function knows only about our own message types. Returns MSGTYPE_UNKNOWN for any MESSAGE we have no special support for. */ msgtype_t mapi_get_message_type (LPMESSAGE message) { TSTART; HRESULT hr; ULONG tag; LPSPropValue propval = NULL; msgtype_t msgtype = MSGTYPE_UNKNOWN; if (!message) { TRETURN msgtype; } if (get_gpgolmsgclass_tag (message, &tag) ) { TRETURN msgtype; /* Ooops */ } hr = HrGetOneProp ((LPMAPIPROP)message, tag, &propval); if (FAILED (hr)) { hr = HrGetOneProp ((LPMAPIPROP)message, PR_MESSAGE_CLASS_A, &propval); if (FAILED (hr)) { log_error ("%s:%s: HrGetOneProp(PR_MESSAGE_CLASS) failed: hr=%#lx\n", SRCNAME, __func__, hr); TRETURN msgtype; } } else log_debug ("%s:%s: have override message class\n", SRCNAME, __func__); if ( PROP_TYPE (propval->ulPropTag) == PT_STRING8 ) { msgtype = string_to_type (propval->Value.lpszA); } MAPIFreeBuffer (propval); TRETURN msgtype; } /* This function is pretty useless because IConverterSession won't take attachments into account. Need to write our own version. */ int mapi_to_mime (LPMESSAGE message, const char *filename) { TSTART; HRESULT hr; LPCONVERTERSESSION session; LPSTREAM stream; hr = CoCreateInstance (CLSID_IConverterSession, NULL, CLSCTX_INPROC_SERVER, IID_IConverterSession, (void **) &session); if (FAILED (hr)) { log_error ("%s:%s: can't create new IConverterSession object: hr=%#lx", SRCNAME, __func__, hr); TRETURN -1; } hr = OpenStreamOnFile (MAPIAllocateBuffer, MAPIFreeBuffer, (STGM_CREATE | STGM_READWRITE), (char*)filename, NULL, &stream); if (FAILED (hr)) { log_error ("%s:%s: can't create file `%s': hr=%#lx\n", SRCNAME, __func__, filename, hr); hr = -1; } else { hr = session->MAPIToMIMEStm (message, stream, CCSF_SMTP); if (FAILED (hr)) { log_error ("%s:%s: MAPIToMIMEStm failed: hr=%#lx", SRCNAME, __func__, hr); stream->Revert (); hr = -1; } else { stream->Commit (0); hr = 0; } gpgol_release (stream); } gpgol_release (session); TRETURN hr; } /* Return a binary property in a malloced buffer with its length stored at R_NBYTES. Returns NULL on error. */ char * mapi_get_binary_prop (LPMESSAGE message, ULONG proptype, size_t *r_nbytes) { TSTART; HRESULT hr; LPSPropValue propval = NULL; char *data; *r_nbytes = 0; hr = HrGetOneProp ((LPMAPIPROP)message, proptype, &propval); if (FAILED (hr)) { log_error ("%s:%s: error getting property %#lx: hr=%#lx", SRCNAME, __func__, proptype, hr); TRETURN NULL; } switch ( PROP_TYPE (propval->ulPropTag) ) { case PT_BINARY: /* This is a binary object but we know that it must be plain ASCII due to the armored format. */ data = (char*)xmalloc (propval->Value.bin.cb + 1); memcpy (data, propval->Value.bin.lpb, propval->Value.bin.cb); data[propval->Value.bin.cb] = 0; *r_nbytes = propval->Value.bin.cb; break; default: log_debug ("%s:%s: requested property %#lx has unknown tag %#lx\n", SRCNAME, __func__, proptype, propval->ulPropTag); data = NULL; break; } MAPIFreeBuffer (propval); TRETURN data; } /* Return an integer property at R_VALUE. On error the function Returns -1 and sets R_VALUE to 0, on success 0 is returned. */ int mapi_get_int_prop (LPMAPIPROP object, ULONG proptype, LONG *r_value) { TSTART; int rc = -1; HRESULT hr; LPSPropValue propval = NULL; *r_value = 0; hr = HrGetOneProp (object, proptype, &propval); if (FAILED (hr)) { log_error ("%s:%s: error getting property %#lx: hr=%#lx", SRCNAME, __func__, proptype, hr); TRETURN -1; } switch ( PROP_TYPE (propval->ulPropTag) ) { case PT_LONG: *r_value = propval->Value.l; rc = 0; break; default: log_debug ("%s:%s: requested property %#lx has unknown tag %#lx\n", SRCNAME, __func__, proptype, propval->ulPropTag); break; } MAPIFreeBuffer (propval); TRETURN rc; } /* Return the attachment method for attachment OBJ. In case of error we TRETURN 0 which happens not to be defined. */ static int get_attach_method (LPATTACH obj) { TSTART; HRESULT hr; LPSPropValue propval = NULL; int method ; hr = HrGetOneProp ((LPMAPIPROP)obj, PR_ATTACH_METHOD, &propval); if (FAILED (hr)) { log_error ("%s:%s: error getting attachment method: hr=%#lx", SRCNAME, __func__, hr); TRETURN 0; } /* We don't bother checking whether we really get a PT_LONG ulong back; if not the system is seriously damaged and we can't do further harm by returning a possible random value. */ method = propval->Value.l; MAPIFreeBuffer (propval); TRETURN method; } /* Return the filename from the attachment as a malloced string. The encoding we return will be UTF-8, however the MAPI docs declare that MAPI does only handle plain ANSI and thus we don't really care later on. In fact we would need to convert the filename back to wchar and use the Unicode versions of the file API. Returns NULL on error or if no filename is available. */ static char * get_attach_filename (LPATTACH obj) { TSTART; HRESULT hr; LPSPropValue propval; char *name = NULL; hr = HrGetOneProp ((LPMAPIPROP)obj, PR_ATTACH_LONG_FILENAME, &propval); if (FAILED(hr)) hr = HrGetOneProp ((LPMAPIPROP)obj, PR_ATTACH_FILENAME, &propval); if (FAILED(hr)) hr = HrGetOneProp ((LPMAPIPROP)obj, PR_DISPLAY_NAME_W, &propval); if (FAILED(hr)) { log_debug ("%s:%s: no filename property found", SRCNAME, __func__); TRETURN NULL; } switch ( PROP_TYPE (propval->ulPropTag) ) { case PT_UNICODE: name = wchar_to_utf8 (propval->Value.lpszW); if (!name) log_debug ("%s:%s: error converting to utf8\n", SRCNAME, __func__); break; case PT_STRING8: name = xstrdup (propval->Value.lpszA); break; default: log_debug ("%s:%s: proptag=%#lx not supported\n", SRCNAME, __func__, propval->ulPropTag); name = NULL; break; } MAPIFreeBuffer (propval); TRETURN name; } /* Return the content-id of the attachment OBJ or NULL if it does not exists. Caller must free. */ static char * get_attach_content_id (LPATTACH obj) { TSTART; HRESULT hr; LPSPropValue propval = NULL; char *name; hr = HrGetOneProp ((LPMAPIPROP)obj, PR_ATTACH_CONTENT_ID, &propval); if (FAILED (hr)) { if (hr != MAPI_E_NOT_FOUND) log_error ("%s:%s: error getting attachment's MIME tag: hr=%#lx", SRCNAME, __func__, hr); TRETURN NULL; } switch ( PROP_TYPE (propval->ulPropTag) ) { case PT_UNICODE: name = wchar_to_utf8 (propval->Value.lpszW); if (!name) log_debug ("%s:%s: error converting to utf8\n", SRCNAME, __func__); break; case PT_STRING8: name = xstrdup (propval->Value.lpszA); break; default: log_debug ("%s:%s: proptag=%#lx not supported\n", SRCNAME, __func__, propval->ulPropTag); name = NULL; break; } MAPIFreeBuffer (propval); TRETURN name; } /* Return the content-type of the attachment OBJ or NULL if it does not exists. Caller must free. */ static char * get_attach_mime_tag (LPATTACH obj) { TSTART; HRESULT hr; LPSPropValue propval = NULL; char *name; hr = HrGetOneProp ((LPMAPIPROP)obj, PR_ATTACH_MIME_TAG_A, &propval); if (FAILED (hr)) { if (hr != MAPI_E_NOT_FOUND) log_error ("%s:%s: error getting attachment's MIME tag: hr=%#lx", SRCNAME, __func__, hr); TRETURN NULL; } switch ( PROP_TYPE (propval->ulPropTag) ) { case PT_UNICODE: name = wchar_to_utf8 (propval->Value.lpszW); if (!name) log_debug ("%s:%s: error converting to utf8\n", SRCNAME, __func__); break; case PT_STRING8: name = xstrdup (propval->Value.lpszA); break; default: log_debug ("%s:%s: proptag=%#lx not supported\n", SRCNAME, __func__, propval->ulPropTag); name = NULL; break; } MAPIFreeBuffer (propval); TRETURN name; } /* Return the GpgOL Attach Type for attachment OBJ. Tag needs to be the tag of that property. */ attachtype_t get_gpgolattachtype (LPATTACH obj, ULONG tag) { TSTART; HRESULT hr; LPSPropValue propval = NULL; attachtype_t retval; hr = HrGetOneProp ((LPMAPIPROP)obj, tag, &propval); if (FAILED (hr)) { if (hr != MAPI_E_NOT_FOUND) log_error ("%s:%s: error getting GpgOL Attach Type: hr=%#lx", SRCNAME, __func__, hr); TRETURN ATTACHTYPE_UNKNOWN; } retval = (attachtype_t)propval->Value.l; MAPIFreeBuffer (propval); TRETURN retval; } /* Gather information about attachments and TRETURN a new table of attachments. Caller must release the returned table.s The routine will TRETURN NULL in case of an error or if no attachments are available. With FAST set only some information gets collected. */ mapi_attach_item_t * mapi_create_attach_table (LPMESSAGE message, int fast) { TSTART; HRESULT hr; SizedSPropTagArray (1L, propAttNum) = { 1L, {PR_ATTACH_NUM} }; LPMAPITABLE mapitable; LPSRowSet mapirows; mapi_attach_item_t *table; unsigned int pos, n_attach; ULONG moss_tag; if (get_gpgolattachtype_tag (message, &moss_tag) ) { TRETURN NULL; } /* Open the attachment table. */ hr = message->GetAttachmentTable (0, &mapitable); if (FAILED (hr)) { log_debug ("%s:%s: GetAttachmentTable failed: hr=%#lx", SRCNAME, __func__, hr); TRETURN NULL; } memdbg_addRef (mapitable); hr = HrQueryAllRows (mapitable, (LPSPropTagArray)&propAttNum, NULL, NULL, 0, &mapirows); if (FAILED (hr)) { log_debug ("%s:%s: HrQueryAllRows failed: hr=%#lx", SRCNAME, __func__, hr); gpgol_release (mapitable); TRETURN NULL; } n_attach = mapirows->cRows > 0? mapirows->cRows : 0; log_debug ("%s:%s: message has %u attachments\n", SRCNAME, __func__, n_attach); if (!n_attach) { FreeProws (mapirows); gpgol_release (mapitable); TRETURN NULL; } /* Allocate our own table. */ table = (mapi_attach_item_t *)xcalloc (n_attach+1, sizeof *table); for (pos=0; pos < n_attach; pos++) { LPATTACH att; if (mapirows->aRow[pos].cValues < 1) { log_error ("%s:%s: invalid row at pos %d", SRCNAME, __func__, pos); table[pos].mapipos = -1; continue; } if (mapirows->aRow[pos].lpProps[0].ulPropTag != PR_ATTACH_NUM) { log_error ("%s:%s: invalid prop at pos %d", SRCNAME, __func__, pos); table[pos].mapipos = -1; continue; } table[pos].mapipos = mapirows->aRow[pos].lpProps[0].Value.l; hr = message->OpenAttach (table[pos].mapipos, NULL, MAPI_BEST_ACCESS, &att); if (FAILED (hr)) { log_error ("%s:%s: can't open attachment %d (%d): hr=%#lx", SRCNAME, __func__, pos, table[pos].mapipos, hr); table[pos].mapipos = -1; continue; } memdbg_addRef (att); table[pos].method = get_attach_method (att); table[pos].filename = fast? NULL : get_attach_filename (att); table[pos].content_type = fast? NULL : get_attach_mime_tag (att); table[pos].content_id = fast? NULL : get_attach_content_id (att); if (table[pos].content_type) { char *p = strchr (table[pos].content_type, ';'); if (p) { *p++ = 0; trim_trailing_spaces (table[pos].content_type); while (strchr (" \t\r\n", *p)) p++; trim_trailing_spaces (p); table[pos].content_type_parms = p; } } table[pos].attach_type = get_gpgolattachtype (att, moss_tag); gpgol_release (att); } table[0].private_mapitable = mapitable; FreeProws (mapirows); table[pos].end_of_table = 1; mapitable = NULL; if (fast) { log_debug ("%s:%s: attachment info: not shown due to fast flag\n", SRCNAME, __func__); } else { log_debug ("%s:%s: attachment info:\n", SRCNAME, __func__); for (pos=0; !table[pos].end_of_table; pos++) { log_debug ("\t%d mt=%d fname=`%s' ct=`%s' ct_parms=`%s' method:%d\n", table[pos].mapipos, table[pos].attach_type, anonstr (table[pos].filename), table[pos].content_type, table[pos].content_type_parms, table[pos].method); } } TRETURN table; } /* Release a table as created by mapi_create_attach_table. */ void mapi_release_attach_table (mapi_attach_item_t *table) { TSTART; unsigned int pos; LPMAPITABLE mapitable; if (!table) { TRETURN; } mapitable = (LPMAPITABLE)table[0].private_mapitable; if (mapitable) gpgol_release (mapitable); for (pos=0; !table[pos].end_of_table; pos++) { xfree (table[pos].filename); xfree (table[pos].content_type); xfree (table[pos].content_id); } xfree (table); TRETURN; } /* Return an attachment as a new IStream object. Returns NULL on failure. If R_ATTACH is not NULL the actual attachment will not be released but stored at that address; the caller needs to release it in this case. */ LPSTREAM mapi_get_attach_as_stream (LPMESSAGE message, mapi_attach_item_t *item, LPATTACH *r_attach) { TSTART; HRESULT hr; LPATTACH att; LPSTREAM stream; if (r_attach) *r_attach = NULL; if (!item || item->end_of_table || item->mapipos == -1) { TRETURN NULL; } hr = message->OpenAttach (item->mapipos, NULL, MAPI_BEST_ACCESS, &att); if (FAILED (hr)) { log_error ("%s:%s: can't open attachment at %d: hr=%#lx", SRCNAME, __func__, item->mapipos, hr); TRETURN NULL; } memdbg_addRef (att); if (item->method != ATTACH_BY_VALUE) { log_error ("%s:%s: attachment: method not supported", SRCNAME, __func__); gpgol_release (att); TRETURN NULL; } hr = gpgol_openProperty (att, PR_ATTACH_DATA_BIN, &IID_IStream, 0, 0, (LPUNKNOWN*) &stream); if (FAILED (hr)) { log_error ("%s:%s: can't open data stream of attachment: hr=%#lx", SRCNAME, __func__, hr); gpgol_release (att); TRETURN NULL; } if (r_attach) *r_attach = att; else gpgol_release (att); TRETURN stream; } /* Return a malloced buffer with the content of the attachment. If R_NBYTES is not NULL the number of bytes will get stored there. ATT must have an attachment method of ATTACH_BY_VALUE. Returns NULL on error. If UNPROTECT is set and the appropriate crypto attribute is available, the function Returns the unprotected version of the atatchment. */ static char * attach_to_buffer (LPATTACH att, size_t *r_nbytes) { TSTART; HRESULT hr; LPSTREAM stream; STATSTG statInfo; ULONG nread; char *buffer; hr = gpgol_openProperty (att, PR_ATTACH_DATA_BIN, &IID_IStream, 0, 0, (LPUNKNOWN*) &stream); if (FAILED (hr)) { log_error ("%s:%s: can't open data stream of attachment: hr=%#lx", SRCNAME, __func__, hr); TRETURN NULL; } hr = stream->Stat (&statInfo, STATFLAG_NONAME); if ( hr != S_OK ) { log_error ("%s:%s: Stat failed: hr=%#lx", SRCNAME, __func__, hr); gpgol_release (stream); TRETURN NULL; } /* Allocate one byte more so that we can terminate the string. */ buffer = (char*)xmalloc ((size_t)statInfo.cbSize.QuadPart + 1); hr = stream->Read (buffer, (size_t)statInfo.cbSize.QuadPart, &nread); if ( hr != S_OK ) { log_error ("%s:%s: Read failed: hr=%#lx", SRCNAME, __func__, hr); xfree (buffer); gpgol_release (stream); TRETURN NULL; } if (nread != statInfo.cbSize.QuadPart) { log_error ("%s:%s: not enough bytes returned\n", SRCNAME, __func__); xfree (buffer); buffer = NULL; } gpgol_release (stream); /* Make sure that the buffer is a C string. */ if (buffer) buffer[nread] = 0; if (r_nbytes) *r_nbytes = nread; TRETURN buffer; } /* Return an attachment as a malloced buffer. The size of the buffer will be stored at R_NBYTES. If unprotect is true, the atatchment will be unprotected. Returns NULL on failure. */ char * mapi_get_attach (LPMESSAGE message, mapi_attach_item_t *item, size_t *r_nbytes) { TSTART; HRESULT hr; LPATTACH att; char *buffer; if (!item || item->end_of_table || item->mapipos == -1) { TRETURN NULL; } hr = message->OpenAttach (item->mapipos, NULL, MAPI_BEST_ACCESS, &att); if (FAILED (hr)) { log_error ("%s:%s: can't open attachment at %d: hr=%#lx", SRCNAME, __func__, item->mapipos, hr); TRETURN NULL; } memdbg_addRef (att); if (item->method != ATTACH_BY_VALUE) { log_error ("%s:%s: attachment: method not supported", SRCNAME, __func__); gpgol_release (att); TRETURN NULL; } buffer = attach_to_buffer (att, r_nbytes); gpgol_release (att); TRETURN buffer; } /* Mark this attachment as the original MOSS message. We set a custom property as well as the hidden flag. */ int mapi_mark_moss_attach (LPMESSAGE message, mapi_attach_item_t *item) { TSTART; int retval = -1; HRESULT hr; LPATTACH att; SPropValue prop; if (!item || item->end_of_table || item->mapipos == -1) { TRETURN -1; } log_debug ("%s:%s: Marking %i as MOSS attachment", SRCNAME, __func__, item->mapipos); hr = message->OpenAttach (item->mapipos, NULL, MAPI_BEST_ACCESS, &att); if (FAILED (hr)) { log_error ("%s:%s: can't open attachment at %d: hr=%#lx", SRCNAME, __func__, item->mapipos, hr); TRETURN -1; } memdbg_addRef (att); if (get_gpgolattachtype_tag (message, &prop.ulPropTag) ) goto leave; prop.Value.l = ATTACHTYPE_MOSS; hr = HrSetOneProp (att, &prop); if (hr) { log_error ("%s:%s: can't set %s property: hr=%#lx\n", SRCNAME, __func__, "GpgOL Attach Type", hr); TRETURN false; } prop.ulPropTag = PR_ATTACHMENT_HIDDEN; prop.Value.b = TRUE; hr = HrSetOneProp (att, &prop); if (hr) { log_error ("%s:%s: can't set hidden attach flag: hr=%#lx\n", SRCNAME, __func__, hr); goto leave; } hr = att->SaveChanges (KEEP_OPEN_READWRITE); if (hr) { log_error ("%s:%s: SaveChanges(attachment) failed: hr=%#lx\n", SRCNAME, __func__, hr); goto leave; } retval = 0; leave: gpgol_release (att); TRETURN retval; } /* If the hidden property has not been set on ATTACH, set it and save the changes. */ int mapi_set_attach_hidden (LPATTACH attach) { TSTART; int retval = -1; HRESULT hr; LPSPropValue propval; SPropValue prop; hr = HrGetOneProp ((LPMAPIPROP)attach, PR_ATTACHMENT_HIDDEN, &propval); if (SUCCEEDED (hr) && PROP_TYPE (propval->ulPropTag) == PT_BOOLEAN && propval->Value.b) { TRETURN 0;/* Already set to hidden. */ } prop.ulPropTag = PR_ATTACHMENT_HIDDEN; prop.Value.b = TRUE; hr = HrSetOneProp (attach, &prop); if (hr) { log_error ("%s:%s: can't set hidden attach flag: hr=%#lx\n", SRCNAME, __func__, hr); goto leave; } hr = attach->SaveChanges (KEEP_OPEN_READWRITE); if (hr) { log_error ("%s:%s: SaveChanges(attachment) failed: hr=%#lx\n", SRCNAME, __func__, hr); goto leave; } retval = 0; leave: TRETURN retval; } /* Returns true if ATTACH has the hidden flag set to true. */ int mapi_test_attach_hidden (LPATTACH attach) { TSTART; HRESULT hr; LPSPropValue propval = NULL; int result = 0; hr = HrGetOneProp ((LPMAPIPROP)attach, PR_ATTACHMENT_HIDDEN, &propval); if (FAILED (hr)) { TRETURN result; /* No. */ } if (PROP_TYPE (propval->ulPropTag) == PT_BOOLEAN && propval->Value.b) result = 1; /* Yes. */ MAPIFreeBuffer (propval); TRETURN result; } /* When sending a message we need to fake the message class so that OL processes it according to our needs. However, if we later try to get the message class from the sent message, OL still has the SMIME message class and tries to hide this by trying to decrypt the message and return the message class from the plaintext. To mitigate the problem we define our own msg class override property. */ int mapi_set_gpgol_msg_class (LPMESSAGE message, const char *name) { TSTART; HRESULT hr; SPropValue prop; if (get_gpgolmsgclass_tag (message, &prop.ulPropTag) ) { TRETURN -1; } prop.Value.lpszA = xstrdup (name); hr = HrSetOneProp (message, &prop); xfree (prop.Value.lpszA); if (hr) { log_error ("%s:%s: can't set %s property: hr=%#lx\n", SRCNAME, __func__, "GpgOL Msg Class", hr); TRETURN -1; } TRETURN 0; } /* Return the charset as assigned by GpgOL to an attachment. This may return NULL it is has not been assigned or is the standard (UTF-8). */ char * mapi_get_gpgol_charset (LPMESSAGE obj) { TSTART; HRESULT hr; LPSPropValue propval = NULL; ULONG tag; char *retstr; if (get_gpgolcharset_tag (obj, &tag) ) { TRETURN NULL; /* Error. */ } hr = HrGetOneProp ((LPMAPIPROP)obj, tag, &propval); if (FAILED (hr)) { TRETURN NULL; } if (PROP_TYPE (propval->ulPropTag) == PT_STRING8) { if (!strcmp (propval->Value.lpszA, "utf-8")) retstr = NULL; else retstr = xstrdup (propval->Value.lpszA); } else retstr = NULL; MAPIFreeBuffer (propval); TRETURN retstr; } /* Set the GpgOl charset property to an attachment. Note that this function does not call SaveChanges. */ int mapi_set_gpgol_charset (LPMESSAGE obj, const char *charset) { TSTART; HRESULT hr; SPropValue prop; char *p; /* Note that we lowercase the value and cut it to a max of 32 characters. The latter is required to make sure that HrSetOneProp will always work. */ if (get_gpgolcharset_tag (obj, &prop.ulPropTag) ) { TRETURN -1; } prop.Value.lpszA = xstrdup (charset); for (p=prop.Value.lpszA; *p; p++) *p = tolower (*(unsigned char*)p); if (strlen (prop.Value.lpszA) > 32) prop.Value.lpszA[32] = 0; hr = HrSetOneProp ((LPMAPIPROP)obj, &prop); xfree (prop.Value.lpszA); if (hr) { log_error ("%s:%s: can't set %s property: hr=%#lx\n", SRCNAME, __func__, "GpgOL Charset", hr); TRETURN -1; } TRETURN 0; } /* Return GpgOL's draft info string as an allocated string. If no draft info is available, NULL is returned. */ char * mapi_get_gpgol_draft_info (LPMESSAGE msg) { TSTART; HRESULT hr; LPSPropValue propval = NULL; ULONG tag; char *retstr; if (get_gpgoldraftinfo_tag (msg, &tag) ) { TRETURN NULL; } hr = HrGetOneProp ((LPMAPIPROP)msg, tag, &propval); if (FAILED (hr)) { TRETURN NULL; } if (PROP_TYPE (propval->ulPropTag) == PT_STRING8) retstr = xstrdup (propval->Value.lpszA); else retstr = NULL; MAPIFreeBuffer (propval); TRETURN retstr; } /* Set GpgOL's draft info string to STRING. This string is defined as: Character 1: 'E' = encrypt selected, 'e' = encrypt not selected. '-' = don't care Character 2: 'S' = sign selected, 's' = sign not selected. '-' = don't care Character 3: 'A' = Auto protocol 'P' = OpenPGP protocol 'X' = S/MIME protocol '-' = don't care If string is NULL, the property will get deleted. Note that this function does not call SaveChanges. */ int mapi_set_gpgol_draft_info (LPMESSAGE message, const char *string) { TSTART; HRESULT hr; SPropValue prop; SPropTagArray proparray; if (get_gpgoldraftinfo_tag (message, &prop.ulPropTag) ) { TRETURN -1; } if (string) { prop.Value.lpszA = xstrdup (string); hr = HrSetOneProp (message, &prop); xfree (prop.Value.lpszA); } else { proparray.cValues = 1; proparray.aulPropTag[0] = prop.ulPropTag; hr = message->DeleteProps (&proparray, NULL); } if (hr) { log_error ("%s:%s: can't %s %s property: hr=%#lx\n", SRCNAME, __func__, string?"set":"delete", "GpgOL Draft Info", hr); TRETURN -1; } TRETURN 0; } /* Return the MIME info as an allocated string. Will never return NULL. */ char * mapi_get_mime_info (LPMESSAGE msg) { TSTART; HRESULT hr; LPSPropValue propval = NULL; ULONG tag; char *retstr; if (get_gpgolmimeinfo_tag (msg, &tag) ) { TRETURN xstrdup ("[Error getting tag for MIME info]"); } hr = HrGetOneProp ((LPMAPIPROP)msg, tag, &propval); if (FAILED (hr)) { TRETURN xstrdup (""); } if (PROP_TYPE (propval->ulPropTag) == PT_STRING8) retstr = xstrdup (propval->Value.lpszA); else retstr = xstrdup ("[MIME info has an invalid type]"); MAPIFreeBuffer (propval); TRETURN retstr; } /* Helper around mapi_get_gpgol_draft_info to avoid the string handling. Return values are: 0 -> Do nothing 1 -> Encrypt 2 -> Sign 3 -> Encrypt & Sign*/ int get_gpgol_draft_info_flags (LPMESSAGE message) { TSTART; char *buf = mapi_get_gpgol_draft_info (message); int ret = 0; if (!buf) { TRETURN 0; } if (buf[0] == 'E') { ret |= 1; } if (buf[1] == 'S') { ret |= 2; } xfree (buf); TRETURN ret; } /* Sets the draft info flags. Protocol is always Auto. flags should be the same as defined by get_gpgol_draft_info_flags */ int set_gpgol_draft_info_flags (LPMESSAGE message, int flags) { TSTART; char buf[4]; buf[3] = '\0'; buf[2] = 'A'; /* Protocol */ buf[1] = flags & 2 ? 'S' : 's'; buf[0] = flags & 1 ? 'E' : 'e'; TRETURN mapi_set_gpgol_draft_info (message, buf); } /* Helper for mapi_get_msg_content_type() */ static int parse_headers_cb (void *dummy_arg, rfc822parse_event_t event, rfc822parse_t msg) { (void)dummy_arg; (void)msg; if (event == RFC822PARSE_T2BODY) { return 42; /* Hack to stop the parsing after having read the outer headers. */ } return 0; } /* Parses a std::string and returns an rfc822_parse_t with the result. Can return NULL on error. Caller must free the returned context. r_is_wks is set to true in case of a WKS Confirmation mail. */ static rfc822parse_t parse_header_data (const std::string &hdrStr, bool &r_is_wks) { TSTART; rfc822parse_t msg = nullptr; if (hdrStr.empty()) { log_error ("%s:%s: called with empty string", SRCNAME, __func__); TRETURN nullptr; } /* Read the headers into an rfc822 object. */ msg = rfc822parse_open (parse_headers_cb, NULL); if (!msg) { log_error ("%s:%s: rfc822parse_open failed", SRCNAME, __func__); TRETURN nullptr; } const char *s = nullptr; const char *header_lines = hdrStr.c_str(); size_t length; while ((s = strchr (header_lines, '\n'))) { length = (s - header_lines); if (length && s[-1] == '\r') length--; if (!strncmp ("Wks-Phase: confirm", header_lines, std::max (18, (int) length))) { log_debug ("%s:%s: detected wks confirmation mail", SRCNAME, __func__); rfc822parse_close (msg); r_is_wks = true; TRETURN nullptr; } rfc822parse_insert (msg, (const unsigned char*)header_lines, length); header_lines = s+1; } TRETURN msg; } /* Return Content-Type of the current message. This one is taken directly from the rfc822 header. If R_PROTOCOL is not NULL a string with the protocol parameter will be stored at this address, if no protocol is given NULL will be stored. If R_SMTYPE is not NULL a string with the smime-type parameter will be stored there. Caller must release all returned strings. */ char * mapi_get_message_content_type (LPMESSAGE message, char **r_protocol, char **r_smtype) { TSTART; const char *s; char *retstr = NULL; bool isWks = false; if (r_protocol) *r_protocol = NULL; if (r_smtype) *r_smtype = NULL; std::string hdrStr = mapi_get_header (message); if (hdrStr.empty()) { log_debug ("%s:%s: failed to get headers. Looking at first attach", SRCNAME, __func__); hdrStr = mapi_get_first_attach_data (message); if (hdrStr.empty()) { log_error ("%s:%s: failed to get headers. And attachment.", SRCNAME, __func__); TRETURN NULL; } } rfc822parse_t msg = parse_header_data (hdrStr, isWks); if (!msg) { if (isWks) { TRETURN xstrdup ("wks.confirmation.mail"); } log_error ("%s:%s: failed to parse headers", SRCNAME, __func__); TRETURN NULL; } /* Parse the content-type field. */ rfc822parse_field_t ctx = rfc822parse_parse_field (msg, "Content-Type", -1); if (ctx) { const char *s1, *s2; rfc822parse_t tnef_msg = nullptr; s1 = rfc822parse_query_media_type (ctx, &s2); if (s1) { retstr = (char*)xmalloc (strlen (s1) + 1 + strlen (s2) + 1); strcpy (stpcpy (stpcpy (retstr, s1), "/"), s2); if (!strcmp (retstr, "application/ms-tnef")) { char *attach_mime = get_first_attach_mime_tag (message); if (attach_mime && !strcmp (attach_mime, "multipart/signed")) { log_debug ("%s:%s: Found multipart signed ms-tnef mail.", SRCNAME, __func__); /* Let's look in the first attachment for the real message to figure out the real content type. */ const auto attach_data = mapi_get_first_attach_data (message); tnef_msg = parse_header_data (attach_data, isWks); if (tnef_msg) { rfc822parse_field_t ctx2; ctx2 = rfc822parse_parse_field (tnef_msg, "Content-Type", -1); if (ctx2) { s1 = rfc822parse_query_media_type (ctx2, &s2); if (s1) { xfree (retstr); retstr = (char*)xmalloc (strlen (s1) + 1 + strlen (s2) + 1); strcpy (stpcpy (stpcpy (retstr, s1), "/"), s2); rfc822parse_release_field (ctx); ctx = ctx2; } else { log_error ("%s:%s: Failed to find ct in ms-tnef.", SRCNAME, __func__); rfc822parse_release_field (ctx2); } } } else { log_error ("%s:%s: Failed to parse ms-tnef.", SRCNAME, __func__); if (isWks) { xfree (retstr); rfc822parse_close (msg); TRETURN xstrdup ("wks.confirmation.mail"); } } } xfree (attach_mime); } if (r_protocol) { s = rfc822parse_query_parameter (ctx, "protocol", 0); if (s) *r_protocol = xstrdup (s); } if (r_smtype) { s = rfc822parse_query_parameter (ctx, "smime-type", 0); if (s) *r_smtype = xstrdup (s); } } rfc822parse_release_field (ctx); if (tnef_msg) { rfc822parse_close (tnef_msg); } } rfc822parse_close (msg); TRETURN retstr; } /* Returns True if MESSAGE has a GpgOL Last Decrypted property with any value. This indicates that there should be no PR_BODY tag. */ int mapi_has_last_decrypted (LPMESSAGE message) { TSTART; HRESULT hr; LPSPropValue propval = NULL; ULONG tag; int yes = 0; if (get_gpgollastdecrypted_tag (message, &tag) ) { TRETURN 0; /* No. */ } hr = HrGetOneProp ((LPMAPIPROP)message, tag, &propval); if (FAILED (hr)) { TRETURN 0; /* No. */ } if (PROP_TYPE (propval->ulPropTag) == PT_BINARY) yes = 1; MAPIFreeBuffer (propval); TRETURN yes; } /* Helper to check whether the file name of OBJ is "smime.p7m". returns on true if so. */ static int has_smime_filename (LPATTACH obj) { TSTART; HRESULT hr; LPSPropValue propval; int yes = 0; hr = HrGetOneProp ((LPMAPIPROP)obj, PR_ATTACH_FILENAME, &propval); if (FAILED(hr)) { hr = HrGetOneProp ((LPMAPIPROP)obj, PR_ATTACH_LONG_FILENAME, &propval); if (FAILED(hr)) { TRETURN 0; } } if ( PROP_TYPE (propval->ulPropTag) == PT_UNICODE) { if (!wcscmp (propval->Value.lpszW, L"smime.p7m")) yes = 1; } else if ( PROP_TYPE (propval->ulPropTag) == PT_STRING8) { if (!strcmp (propval->Value.lpszA, "smime.p7m")) yes = 1; } MAPIFreeBuffer (propval); TRETURN yes; } /* Copy the MAPI body to a PGPBODY type attachment. */ int mapi_body_to_attachment (LPMESSAGE message) { TSTART; HRESULT hr; LPSTREAM instream; ULONG newpos; LPATTACH newatt = NULL; SPropValue prop; LPSTREAM outstream = NULL; LPUNKNOWN punk; char body_filename[] = PGPBODYFILENAME; instream = mapi_get_body_as_stream (message); if (!instream) { TRETURN -1; } log_debug ("%s:%s: Creating MOSS body attachment", SRCNAME, __func__); hr = message->CreateAttach (NULL, 0, &newpos, &newatt); if (hr) { log_error ("%s:%s: can't create attachment: hr=%#lx\n", SRCNAME, __func__, hr); goto leave; } prop.ulPropTag = PR_ATTACH_METHOD; prop.Value.ul = ATTACH_BY_VALUE; hr = HrSetOneProp ((LPMAPIPROP)newatt, &prop); if (hr) { log_error ("%s:%s: can't set attach method: hr=%#lx\n", SRCNAME, __func__, hr); goto leave; } /* Mark that attachment so that we know why it has been created. */ if (get_gpgolattachtype_tag (message, &prop.ulPropTag) ) goto leave; prop.Value.l = ATTACHTYPE_PGPBODY; hr = HrSetOneProp ((LPMAPIPROP)newatt, &prop); if (hr) { log_error ("%s:%s: can't set %s property: hr=%#lx\n", SRCNAME, __func__, "GpgOL Attach Type", hr); goto leave; } prop.ulPropTag = PR_ATTACHMENT_HIDDEN; prop.Value.b = TRUE; hr = HrSetOneProp ((LPMAPIPROP)newatt, &prop); if (hr) { log_error ("%s:%s: can't set hidden attach flag: hr=%#lx\n", SRCNAME, __func__, hr); goto leave; } prop.ulPropTag = PR_ATTACH_FILENAME_A; prop.Value.lpszA = body_filename; hr = HrSetOneProp ((LPMAPIPROP)newatt, &prop); if (hr) { log_error ("%s:%s: can't set attach filename: hr=%#lx\n", SRCNAME, __func__, hr); goto leave; } punk = (LPUNKNOWN)outstream; hr = gpgol_openProperty (newatt, 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 leave; } outstream = (LPSTREAM)punk; /* Insert a blank line so that our mime parser skips over the mail headers. */ hr = outstream->Write ("\r\n", 2, NULL); if (hr) { log_error ("%s:%s: Write failed: hr=%#lx", SRCNAME, __func__, hr); goto leave; } { ULARGE_INTEGER cb; cb.QuadPart = 0xffffffffffffffffll; hr = instream->CopyTo (outstream, cb, NULL, NULL); } if (hr) { log_error ("%s:%s: can't copy streams: hr=%#lx\n", SRCNAME, __func__, hr); goto leave; } hr = outstream->Commit (0); if (hr) { log_error ("%s:%s: Commiting output stream failed: hr=%#lx", SRCNAME, __func__, hr); goto leave; } gpgol_release (outstream); outstream = NULL; hr = newatt->SaveChanges (0); if (hr) { log_error ("%s:%s: SaveChanges of the attachment failed: hr=%#lx\n", SRCNAME, __func__, hr); goto leave; } gpgol_release (newatt); newatt = NULL; hr = mapi_save_changes (message, KEEP_OPEN_READWRITE); leave: if (outstream) { outstream->Revert (); gpgol_release (outstream); } if (newatt) gpgol_release (newatt); gpgol_release (instream); TRETURN hr? -1:0; } static int hide_attachment_mapipos (LPMESSAGE message, int pos) { LPATTACH att = nullptr; HRESULT hr = message->OpenAttach (pos, nullptr, MAPI_BEST_ACCESS, &att); if (hr != S_OK) { log_error ("%s:%s: can't open attachment at %d hr=%#lx", SRCNAME, __func__, pos, hr); TRETURN -1; } memdbg_addRef (att); log_debug ("%s:%s: Hiding attachment %i.", SRCNAME, __func__, pos); mapi_set_attach_hidden (att); gpgol_release (att); return 0; } int mapi_mark_or_create_moss_attach (LPMESSAGE message, LPMESSAGE parsed_message, msgtype_t msgtype) { TSTART; int i; if (msgtype == MSGTYPE_UNKNOWN || msgtype == MSGTYPE_GPGOL) { TRETURN 0; } /* First check if we already have one marked. */ mapi_attach_item_t *table = mapi_create_attach_table (message, 0); int part1 = 0, part2 = 0; int have_mosstempl = 0; for (i = 0; table && !table[i].end_of_table; i++) { have_mosstempl |= (table[i].attach_type == ATTACHTYPE_MOSSTEMPL); if (table[i].attach_type == ATTACHTYPE_PGPBODY || table[i].attach_type == ATTACHTYPE_MOSS || table[i].attach_type == ATTACHTYPE_MOSSTEMPL) { if (!part1) { part1 = i + 1; if (have_mosstempl) { /* If we have a MOSSTEMPL use that one. */ break; } } else if (!part2) { /* If we have two MOSS attachments we use the second one if the first one is not the octet-stream. */ if (table[part1 - 1].content_type && !strcmp (table[part1 - 1].content_type, "application/octet-stream")) { /* This is rare so debug it. Happend when drafts were encrypted / decrypted and another client was running */ log_debug ("%s:%s: Have two MOSS attachments but the " "first one is the octet stream", SRCNAME, __func__); int tmp = part1; part1 = i + 1; part2 = tmp; break; } part2 = i + 1; break; } } } if (have_mosstempl && parsed_message) { /* If we have a mosstempl. This means that we created this message. Sometimes we get the situation in sent mails that we have a mosstempl but the pgp version header and octet streams also attached in the parsed_message. We can't trigger a reread from the base message so in that case we want to hide everything as we rebuild the message from our MOSS. We don't delete here to avoid bugs where data loss might occur. See also: T4241 */ log_debug ("%s:%s: Found mosstempl. Hiding all other attachments.", SRCNAME, __func__); mapi_attach_item_t *table2 = mapi_create_attach_table (parsed_message, 0); for (i = 0; table2 && !table2[i].end_of_table; i++) { hide_attachment_mapipos (parsed_message, table2[i].mapipos); } if (table2) { mapi_release_attach_table (table2); /* GpgOL works on the OOM so we have to use the MOSS parts from the parsed message. */ int parsed_part = mapi_mark_or_create_moss_attach (parsed_message, nullptr, msgtype); if (parsed_part) { mapi_release_attach_table (table); TRETURN parsed_part; } } } if (part1 || part2) { /* Remark to ensure that it is hidden. As our revert code must unhide it so that it is not stored in winmail.dat but used as the mosstmpl. */ mapi_attach_item_t *item = table - 1 + (part2 ? part2 : part1); hide_attachment_mapipos (message, item->mapipos); mapi_release_attach_table (table); TRETURN part2 ? part2 : part1; } if (msgtype == MSGTYPE_GPGOL_CLEAR_SIGNED || msgtype == MSGTYPE_GPGOL_PGP_MESSAGE) { /* Inline message we need to create body attachment so that we are able to restore the content. */ if (mapi_body_to_attachment (message)) { log_error ("%s:%s: Failed to create body attachment.", SRCNAME, __func__); TRETURN 0; } log_debug ("%s:%s: Created body attachment. Repeating lookup.", SRCNAME, __func__); /* The position of the MOSS attach might change depending on the attachment count of the mail. So repeat the check to get the right position. */ mapi_release_attach_table (table); TRETURN mapi_mark_or_create_moss_attach (message, parsed_message, msgtype); } if (!table) { log_debug ("%s:%s: Neither pgp inline nor an attachment table.", SRCNAME, __func__); TRETURN 0; } /* MIME Mails check for S/MIME first. */ for (i = 0; !table[i].end_of_table; i++) { if (table[i].content_type && (!strcmp (table[i].content_type, "application/pkcs7-mime") || !strcmp (table[i].content_type, "application/x-pkcs7-mime")) && table[i].filename && !strcmp (table[i].filename, "smime.p7m")) break; } if (!table[i].end_of_table) { mapi_mark_moss_attach (message, table + i); mapi_release_attach_table (table); TRETURN i + 1; } /* PGP/MIME or S/MIME stuff. */ /* Multipart/encrypted message: We expect 2 attachments. The first one with the version number and the second one with the ciphertext. As we don't know wether we are called the first time, we first try to find these attachments by looking at all attachments. Only if this fails we identify them by their order (i.e. the first 2 attachments) and mark them as part1 and part2. */ for (i = 0; !table[i].end_of_table; i++); /* Count entries */ if (i >= 2) { int part1_idx = -1, part2_idx = -1; /* At least 2 attachments but none are marked. Thus we assume that this is the first time we see this message and we will set the mark now if we see appropriate content types. */ if (table[0].content_type && !strcmp (table[0].content_type, "application/pgp-encrypted")) part1_idx = 0; if (table[1].content_type && !strcmp (table[1].content_type, "application/octet-stream")) part2_idx = 1; if (part1_idx != -1 && part2_idx != -1) { mapi_mark_moss_attach (message, table+part1_idx); mapi_mark_moss_attach (message, table+part2_idx); mapi_release_attach_table (table); TRETURN 2; } } if (!table[0].end_of_table && table[1].end_of_table) { /* No MOSS flag found in the table but there is only one attachment. Due to the message type we know that this is the original MOSS message. We mark this attachment as hidden, so that it won't get displayed. We further mark it as our original MOSS attachment so that after parsing we have a mean to find it again (see above). */ mapi_mark_moss_attach (message, table + 0); mapi_release_attach_table (table); TRETURN 1; } mapi_release_attach_table (table); TRETURN 0; /* No original attachment - this should not happen. */ } static std::string ac_get_value (const char *header, const char *what) { TSTART; if (!header || !what) { STRANGEPOINT; TRETURN std::string(); } const char *s = strstr (header, what); if (!s) { log_debug ("%s:%s: could not find %s in header", SRCNAME, __func__, what); TRETURN std::string(); } /* As we found it we can be sure that this is not out of bounds. */ s += strlen (what); if (*s != '=') { log_debug ("%s:%s: No equal sign after %s in header %s", SRCNAME, __func__, what, s); TRETURN std::string(); } /* Move over the = sign. */ s++; /* Find the sep */ const char *s2 = strchr (s, ';'); if (!s2) { /* No seperator found. Assume the rest is the value */ TRETURN s; } /* From the equal to the ; is our value. */ TRETURN std::string (s, s2 - s); } static GpgME::Data prepare_key_data (const std::string &d) { TSTART; if (d.empty()) { STRANGEPOINT; TRETURN GpgME::Data(); } /* Prepare the keydata */ b64_state_t base64; /* The state of the Base-64 decoder. */ b64_init (&base64); /* strdup and not xstrdup as we want GpgME to take over */ char *b64decoded = strdup (d.c_str()); size_t len = b64_decode (&base64, b64decoded, strlen(b64decoded)); if (!len) { log_error ("%s:%s: Invalid base64 in %s", SRCNAME, __func__, b64decoded); xfree (b64decoded); TRETURN GpgME::Data(); } auto data = GpgME::Data (b64decoded, len, false /* take ownership */); TRETURN data; } bool mapi_get_header_info (LPMESSAGE message, header_info_s &r_header_info) { TSTART; rfc822parse_t msg; autocrypt_s r_autocrypt; r_autocrypt.exists = false; /* Read the headers into an rfc822 object. */ msg = rfc822parse_open (parse_headers_cb, NULL); if (!msg) { log_error ("%s:%s: rfc822parse_open failed", SRCNAME, __func__); TRETURN false; } const std::string hdrStr = mapi_get_header (message); if (hdrStr.empty()) { log_error ("%s:%s: failed to get headers", SRCNAME, __func__); rfc822parse_close (msg); TRETURN false; } size_t length; const char *header_lines = hdrStr.c_str(); const char *s; while ((s = strchr (header_lines, '\n'))) { length = (s - header_lines); if (length && s[-1] == '\r') length--; rfc822parse_insert (msg, (const unsigned char*)header_lines, length); header_lines = s+1; } const char *ac_field = rfc822parse_get_field (msg, "Autocrypt", -1, 0); if (ac_field) { r_autocrypt.exists = true; r_autocrypt.addr = ac_get_value (ac_field, "addr"); r_autocrypt.data = prepare_key_data (ac_get_value (ac_field, "keydata")); r_autocrypt.pref = ac_get_value (ac_field, "prefer-encrypt"); } rfc822parse_field_t field = rfc822parse_parse_field (msg, "Content-Type", -1); if (field) { const char *boundary = rfc822parse_query_parameter (field, "boundary", -1); if (boundary) { r_header_info.boundary = boundary; log_dbg ("Found Boundary. '%s'", boundary); } else { log_dbg ("Failed to find top level boundary."); } rfc822parse_release_field (field); } else { log_dbg ("Failed to get content type field."); } r_header_info.acInfo = r_autocrypt; rfc822parse_close (msg); TRETURN true; } void mapi_delete_gpgol_tags (LPMESSAGE message) { if (!message) { STRANGEPOINT; return; } ULONG tag; if (!get_gpgolmsgclass_tag (message, &tag)) { HRESULT hr; SPropTagArray proparray; proparray.cValues = 1; proparray.aulPropTag[0] = tag; hr = message->DeleteProps (&proparray, NULL); if (hr) { log_error ("%s:%s: deleteprops failed: hr=%#lx\n", SRCNAME, __func__, hr); } } } void mapi_set_mesage_class (LPMESSAGE message, const char *cls) { if (!message || !cls) { STRANGEPOINT; return; } HRESULT hr; SPropValue prop; prop.ulPropTag = PR_MESSAGE_CLASS_A; prop.Value.lpszA = xstrdup (cls); hr = HrSetOneProp (message, &prop); xfree (prop.Value.lpszA); if (hr) { log_error ("%s:%s: set msg class failed: hr=%#lx\n", SRCNAME, __func__, hr); } hr = mapi_save_changes (message, KEEP_OPEN_READWRITE); if (hr) { log_error ("%s:%s: save failed: hr=%#lx\n", SRCNAME, __func__, hr); } } diff --git a/src/mimedataprovider.cpp b/src/mimedataprovider.cpp index f1aa3ba..fd7ff0e 100644 --- a/src/mimedataprovider.cpp +++ b/src/mimedataprovider.cpp @@ -1,1264 +1,1268 @@ /* mimedataprover.cpp - GpgME dataprovider for mime data * Copyright (C) 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 "common_indep.h" #include "xmalloc.h" #include #include #include #include "mimedataprovider.h" #include "rfc822parse.h" #include "rfc2047parse.h" #include "attachment.h" #include "cpphelp.h" #ifndef HAVE_W32_SYSTEM #define stricmp strcasecmp #endif /* How much data is read at once in collect */ #define BUFSIZE 65536 /* The maximum length of a line we are able to process. RFC822 allows only for 1000 bytes; thus 2000 seemed to be a reasonable value until we worked with a MUA that sent whole HTML mails in a single line now we read more at once and allow the line to be the full size of our read. -1 */ #define LINEBUFSIZE (BUFSIZE - 1) #include /* To keep track of the MIME message structures we use a linked list with each item corresponding to one part. */ struct mimestruct_item_s; typedef struct mimestruct_item_s *mimestruct_item_t; struct mimestruct_item_s { mimestruct_item_t next; unsigned int level; /* Level in the hierarchy of that part. 0 indicates the outer body. */ char *filename; /* Malloced filename or NULL. */ char *cid; /* Malloced content id or NULL. */ char *charset; /* Malloced charset or NULL. */ char content_type[1]; /* String with the content type. */ }; /* The context object we use to track information. */ struct mime_context { rfc822parse_t msg; /* The handle of the RFC822 parser. */ int verify_mode; /* True if we want to verify a signature. */ int nesting_level; /* Current MIME nesting level. */ int in_data; /* We are currently in data (body or attachment). */ int body_seen; /* True if we have seen a part we consider the body of the message. */ std::shared_ptr current_attachment; /* A pointer to the current attachment */ int collect_body; /* True if we are collcting the body */ int collect_html_body; /* True if we are collcting the html body */ int collect_crypto_data; /* True if we are collecting the signed data. */ int collect_signature; /* True if we are collecting a signature. */ int pgp_marker_checked; /* Checked if the first body line is pgp marker*/ int is_encrypted; /* True if we are working on an encrypted mail. */ int start_hashing; /* Flag used to start collecting signed data. */ int hashing_level; /* MIME level where we started hashing. */ int is_qp_encoded; /* Current part is QP encoded. */ int is_base64_encoded; /* Current part is base 64 encoded. */ int is_body; /* The current part belongs to the body. */ protocol_t protocol; /* The detected crypto protocol. */ int part_counter; /* Counts the number of processed parts. */ int any_boundary; /* Indicates whether we have seen any boundary which means that we are actually working on a MIME message and not just on plain rfc822 message. */ int in_protected_headers; /* Indicates if we are in a mime part that was marked by a protected headers header. */ /* A linked list describing the structure of the mime message. This list gets build up while parsing the message. */ mimestruct_item_t mimestruct; mimestruct_item_t *mimestruct_tail; mimestruct_item_t mimestruct_cur; int any_attachments_created; /* True if we created a new atatchment. */ b64_state_t base64; /* The state of the Base-64 decoder. */ gpg_error_t parser_error; /* Indicates that we encountered a error from the parser. */ }; typedef struct mime_context *mime_context_t; /* Print the message event EVENT. */ static void debug_message_event (rfc822parse_event_t event) { const char *s; switch (event) { case RFC822PARSE_OPEN: s= "Open"; break; case RFC822PARSE_CLOSE: s= "Close"; break; case RFC822PARSE_CANCEL: s= "Cancel"; break; case RFC822PARSE_T2BODY: s= "T2Body"; break; case RFC822PARSE_FINISH: s= "Finish"; break; case RFC822PARSE_RCVD_SEEN: s= "Rcvd_Seen"; break; case RFC822PARSE_LEVEL_DOWN: s= "Level_Down"; break; case RFC822PARSE_LEVEL_UP: s= "Level_Up"; break; case RFC822PARSE_BOUNDARY: s= "Boundary"; break; case RFC822PARSE_LAST_BOUNDARY: s= "Last_Boundary"; break; case RFC822PARSE_BEGIN_HEADER: s= "Begin_Header"; break; case RFC822PARSE_PREAMBLE: s= "Preamble"; break; case RFC822PARSE_EPILOGUE: s= "Epilogue"; break; default: s= "[unknown event]"; break; } log_debug ("%s: rfc822 event %s\n", SRCNAME, s); } /* returns true if the BER encoded data in BUFFER is CMS signed data. LENGTH gives the length of the buffer, for correct detection LENGTH should be at least about 24 bytes. */ #if 0 static int is_cms_signed_data (const char *buffer, size_t length) { TSTART; const char *p = buffer; size_t n = length; tlvinfo_t ti; if (parse_tlv (&p, &n, &ti)) { TRETURN 0; } if (!(ti.cls == ASN1_CLASS_UNIVERSAL && ti.tag == ASN1_TAG_SEQUENCE && ti.is_cons) ) TRETURN 0; if (parse_tlv (&p, &n, &ti)) { TRETURN 0; } if (!(ti.cls == ASN1_CLASS_UNIVERSAL && ti.tag == ASN1_TAG_OBJECT_ID && !ti.is_cons && ti.length) || ti.length > n) TRETURN 0; if (ti.length == 9 && !memcmp (p, "\x2A\x86\x48\x86\xF7\x0D\x01\x07\x02", 9)) { TRETURN 1; } TRETURN 0; } #endif /* Process the transition to body event. This means we have received the empty line indicating the body and should now check the headers to see what to do about this part. This is mostly a C style function because it was based on the old c mimeparser. */ static int t2body (MimeDataProvider *provider, rfc822parse_t msg) { TSTART; rfc822parse_field_t field; mime_context_t ctx = provider->mime_context (); const char *ctmain, *ctsub; const char *s; size_t off; char *p; int is_text = 0; int is_text_attachment = 0; int is_protected_headers = 0; char *filename = NULL; char *cid = NULL; char *charset = NULL; bool ignore_cid = false; /* Figure out the encoding. */ ctx->is_qp_encoded = 0; ctx->is_base64_encoded = 0; p = rfc822parse_get_field (msg, "Content-Transfer-Encoding", -1, &off); if (p) { if (!stricmp (p+off, "quoted-printable")) ctx->is_qp_encoded = 1; else if (!stricmp (p+off, "base64")) { ctx->is_base64_encoded = 1; b64_init (&ctx->base64); } xfree (p); } /* Get the filename from the header. */ field = rfc822parse_parse_field (msg, "Content-Disposition", -1); if (field) { s = rfc822parse_query_parameter (field, "filename", 0); if (s) filename = rfc2047_parse (s); s = rfc822parse_query_parameter (field, NULL, 1); if (s && strstr (s, "attachment")) { log_debug ("%s:%s: Found Content-Disposition attachment." " Ignoring content-id to avoid hiding.", SRCNAME, __func__); ignore_cid = true; } /* This is a bit of a taste matter how to treat inline attachments. Outlook does not show them inline so we should not put it in the body either as we have no way to show that it was actually an attachment. For something like an inline patch it is better to add it as an attachment instead of just putting it in the body. The handling in the old parser was: if (s && strcmp (s, "inline")) not_inline_text = 1; */ if (ctx->body_seen) { /* Some MUA's like kontact e3.5 send the body as an inline text attachment. So if we have not seen the body yet we treat the first text/plain element as the body and not as an inline attachment. */ is_text_attachment = 1; } rfc822parse_release_field (field); } /* Process the Content-type and all its parameters. */ ctmain = ctsub = NULL; field = rfc822parse_parse_field (msg, "Content-Type", -1); if (field) ctmain = rfc822parse_query_media_type (field, &ctsub); if (!ctmain) { /* Either there is no content type field or it is faulty; in both cases we fall back to text/plain. */ ctmain = "text"; ctsub = "plain"; } log_debug ("%s:%s: ctx=%p, ct=`%s/%s'\n", SRCNAME, __func__, ctx, ctmain, ctsub); if (!ctx->nesting_level) { provider->set_content_type (ctmain, ctsub); } s = rfc822parse_query_parameter (field, "charset", 0); if (s) charset = xstrdup (s); if (!filename) { /* Check for Content-Type name if Content-Disposition filename was not found */ s = rfc822parse_query_parameter (field, "name", 0); if (s) filename = rfc2047_parse (s); } /* Parse a Content Id header */ if (!ignore_cid) { p = rfc822parse_get_field (msg, "Content-Id", -1, &off); if (p) { cid = xstrdup (p+off); xfree (p); } } /* Update our idea of the entire MIME structure. */ { mimestruct_item_t ms; ms = (mimestruct_item_t) xmalloc (sizeof *ms + strlen (ctmain) + 1 + strlen (ctsub)); ctx->mimestruct_cur = ms; *ctx->mimestruct_tail = ms; ctx->mimestruct_tail = &ms->next; ms->next = NULL; strcpy (stpcpy (stpcpy (ms->content_type, ctmain), "/"), ctsub); ms->level = ctx->nesting_level; ms->filename = filename; ms->cid = cid; filename = NULL; ms->charset = charset; charset = NULL; } + if (!strcmp (ctmain, "multipart") || !strcmp (ctmain, "text")) + { + s = rfc822parse_query_parameter (field, + "protected-headers", -1); + if (s) + { + log_data ("%s:%s: Found protected headers: '%s'", + SRCNAME, __func__, s); + if (!strncmp (s, "v", 1)) + { + provider->m_protected_headers_version = atoi (s + 1); + } + is_protected_headers = 1; + } + else + { + is_protected_headers = 0; + } + } + if (!strcmp (ctmain, "multipart")) { /* We don't care about the top level multipart layer but wait until it comes to the actual parts which then will get stored as attachments. For now encapsulated signed or encrypted containers are not processed in a special way as they should. Except for the simple verify mode. */ if (!provider->signature() && !strcmp (ctsub, "signed") && (s = rfc822parse_query_parameter (field, "protocol", 0))) { if (!strcmp (s, "application/pgp-signature")) ctx->protocol = PROTOCOL_OPENPGP; else if (!strcmp (s, "application/pkcs7-signature") || !strcmp (s, "application/x-pkcs7-signature")) ctx->protocol = PROTOCOL_SMIME; else ctx->protocol = PROTOCOL_UNKNOWN; /* Need to start the hashing after the next boundary. */ ctx->start_hashing = 1; } else if (!strcmp (ctsub, "encrypted") && (s = rfc822parse_query_parameter (field, "protocol", 0))) { if (!strcmp (s, "application/pgp-encrypted")) ctx->protocol = PROTOCOL_OPENPGP; /* We expect an encrypted mime part. */ ctx->is_encrypted = 1; } } else if (!strcmp (ctmain, "text")) { is_text = !strcmp (ctsub, "html")? 2:1; - s = rfc822parse_query_parameter (field, - "protected-headers", -1); - if (s) - { - log_data ("%s:%s: Found protected headers: '%s'", - SRCNAME, __func__, s); - if (!strncmp (s, "v", 1)) - { - provider->m_protected_headers_version = atoi (s + 1); - } - is_protected_headers = 1; - } - else - { - is_protected_headers = 0; - } } else if (ctx->nesting_level == 1 && !provider->signature() && !strcmp (ctmain, "application") && ((ctx->protocol == PROTOCOL_OPENPGP && !strcmp (ctsub, "pgp-signature")) || (ctx->protocol == PROTOCOL_SMIME && (!strcmp (ctsub, "pkcs7-signature") || !strcmp (ctsub, "x-pkcs7-signature"))))) { /* This is the second part of a MOSS signature. We only support here full messages thus checking the nesting level is sufficient. We do this only for the first signature (i.e. if sig_data has not been set yet). We also do this only while in verify mode because we don't want to write a full MUA. */ ctx->collect_signature = 1; log_debug ("%s:%s: Collecting signature.", SRCNAME, __func__); } else if (ctx->nesting_level == 1 && ctx->is_encrypted && !strcmp (ctmain, "application") && (ctx->protocol == PROTOCOL_OPENPGP && !strcmp (ctsub, "octet-stream"))) { log_debug ("%s:%s: Collecting encrypted PGP data.", SRCNAME, __func__); ctx->collect_crypto_data = 1; } else /* Other type. */ { /* Check whether this attachment is an opaque signed S/MIME part. We use a counter to later check that there is only one such part. */ if (!strcmp (ctmain, "application") && (!strcmp (ctsub, "pkcs7-mime") || !strcmp (ctsub, "x-pkcs7-mime"))) { log_debug ("%s:%s: Collecting crypted S/MIME data.", SRCNAME, __func__); ctx->collect_crypto_data = 1; } } /* Reset the in_data marker */ ctx->in_data = 1; ctx->in_protected_headers = is_protected_headers; log_debug ("%s:%s: this body: nesting=%d partno=%d is_text=%d" " charset=\"%s\"\n body_seen=%d is_text_attachment=%d" " is_protected_headers=%d", SRCNAME, __func__, ctx->nesting_level, ctx->part_counter, is_text, ctx->mimestruct_cur->charset?ctx->mimestruct_cur->charset:"", ctx->body_seen, is_text_attachment, is_protected_headers); /* If this is a text part, decide whether we treat it as one of our bodies. */ if ((is_text && !is_text_attachment)) { if (is_text == 2) { ctx->body_seen = 2; ctx->collect_html_body = 1; ctx->collect_body = 0; log_debug ("%s:%s: Collecting HTML body.", SRCNAME, __func__); /* We need this crutch because of one liner html mails which would not be collected by the line collector if they dont have a linefeed at the end. */ provider->set_has_html_body (true); } else { log_debug ("%s:%s: Collecting text body.", SRCNAME, __func__); ctx->body_seen = 1; ctx->collect_body = 1; ctx->collect_html_body = 0; } } else if (!ctx->collect_crypto_data) { bool isMultipart = (ctmain && !strcmp (ctmain, "multipart")); if (!ctx->nesting_level && !isMultipart) { log_dbg ("Data found that has no body. Treating it as attachment."); } else if (!ctx->nesting_level && isMultipart) { log_dbg ("Found first multipart transition"); } else if (ctx->nesting_level || !isMultipart) { /* Treat it as an attachment. */ ctx->current_attachment = provider->create_attachment(); ctx->collect_body = 0; ctx->collect_html_body = 0; log_debug ("%s:%s: Collecting attachment.", SRCNAME, __func__); } } else { log_dbg ("Don't know what to collect, invalid mail?."); } rfc822parse_release_field (field); /* (Content-type) */ TRETURN 0; } static int message_cb (void *opaque, rfc822parse_event_t event, rfc822parse_t msg) { int retval = 0; MimeDataProvider *provider = static_cast (opaque); mime_context_t ctx = provider->mime_context(); debug_message_event (event); if (event == RFC822PARSE_BEGIN_HEADER || event == RFC822PARSE_T2BODY) { /* We need to check here whether to start collecting signed data because attachments might come without header lines and thus we won't see the BEGIN_HEADER event. */ if (ctx->start_hashing == 1) { ctx->start_hashing = 2; ctx->hashing_level = ctx->nesting_level; ctx->collect_crypto_data = 1; } } switch (event) { case RFC822PARSE_T2BODY: retval = t2body (provider, msg); break; case RFC822PARSE_LEVEL_DOWN: ctx->nesting_level++; break; case RFC822PARSE_LEVEL_UP: if (ctx->nesting_level) ctx->nesting_level--; else { log_error ("%s: ctx=%p, invalid structure: bad nesting level\n", SRCNAME, ctx); ctx->parser_error = gpg_error (GPG_ERR_GENERAL); } break; case RFC822PARSE_BOUNDARY: case RFC822PARSE_LAST_BOUNDARY: ctx->any_boundary = 1; ctx->in_data = 0; ctx->collect_body = 0; if (ctx->start_hashing == 2 && ctx->hashing_level == ctx->nesting_level) { ctx->start_hashing = 3; /* Avoid triggering it again. */ ctx->collect_crypto_data = 0; } break; case RFC822PARSE_BEGIN_HEADER: ctx->part_counter++; break; default: /* Ignore all other events. */ break; } return retval; } MimeDataProvider::MimeDataProvider(bool no_headers) : m_protected_headers_version(0), m_signature(nullptr), m_has_html_body(false), m_collect_everything(no_headers) { TSTART; memdbg_ctor ("MimeDataProvider"); m_mime_ctx = (mime_context_t) xcalloc (1, sizeof *m_mime_ctx); m_mime_ctx->msg = rfc822parse_open (message_cb, this); m_mime_ctx->mimestruct_tail = &m_mime_ctx->mimestruct; TRETURN; } #ifdef HAVE_W32_SYSTEM MimeDataProvider::MimeDataProvider(LPSTREAM stream, bool no_headers): MimeDataProvider(no_headers) { TSTART; if (stream) { stream->AddRef (); memdbg_addRef (stream); } else { log_error ("%s:%s called without stream ", SRCNAME, __func__); TRETURN; } log_data ("%s:%s Collecting data.", SRCNAME, __func__); collect_data (stream); log_data ("%s:%s Data collected.", SRCNAME, __func__); gpgol_release (stream); TRETURN; } #endif MimeDataProvider::MimeDataProvider(FILE *stream, bool no_headers): MimeDataProvider(no_headers) { TSTART; log_data ("%s:%s Collecting data from file.", SRCNAME, __func__); collect_data (stream); log_data ("%s:%s Data collected.", SRCNAME, __func__); TRETURN; } MimeDataProvider::~MimeDataProvider() { TSTART; memdbg_dtor ("MimeDataProvider"); log_debug ("%s:%s", SRCNAME, __func__); while (m_mime_ctx->mimestruct) { mimestruct_item_t tmp = m_mime_ctx->mimestruct->next; xfree (m_mime_ctx->mimestruct->filename); xfree (m_mime_ctx->mimestruct->charset); xfree (m_mime_ctx->mimestruct->cid); xfree (m_mime_ctx->mimestruct); m_mime_ctx->mimestruct = tmp; } rfc822parse_close (m_mime_ctx->msg); m_mime_ctx->current_attachment = NULL; xfree (m_mime_ctx); if (m_signature) { delete m_signature; } TRETURN; } bool MimeDataProvider::isSupported(GpgME::DataProvider::Operation op) const { return op == GpgME::DataProvider::Read || op == GpgME::DataProvider::Seek || op == GpgME::DataProvider::Write || op == GpgME::DataProvider::Release; } ssize_t MimeDataProvider::read(void *buffer, size_t size) { log_data ("%s:%s: Reading: " SIZE_T_FORMAT "Bytes", SRCNAME, __func__, size); ssize_t bRead = m_crypto_data.read (buffer, size); if ((opt.enable_debug & DBG_DATA) && bRead) { std::string buf ((char *)buffer, bRead); if (!is_binary (buf)) { log_data ("%s:%s: Data: \n------\n%s\n------", SRCNAME, __func__, buf.c_str()); } else { log_data ("%s:%s: Hex Data: \n------\n%s\n------", SRCNAME, __func__, string_to_hex (buf).c_str ()); } } return bRead; } /* Split some raw data into lines and handle them accordingly. Returns the amount of bytes not taken from the input buffer. */ size_t MimeDataProvider::collect_input_lines(const char *input, size_t insize) { TSTART; char linebuf[LINEBUFSIZE]; const char *s = input; size_t pos = 0; size_t nleft = insize; size_t not_taken = nleft; size_t len = 0; /* Split the raw data into lines */ for (; nleft; nleft--, s++) { if (pos >= LINEBUFSIZE) { log_error ("%s:%s: rfc822 parser failed: line too long\n", SRCNAME, __func__); GpgME::Error::setSystemError (GPG_ERR_EIO); TRETURN not_taken; } if (*s != '\n') linebuf[pos++] = *s; else { /* Got a complete line. Remove the last CR. */ not_taken -= pos + 1; /* Pos starts at 0 so + 1 for it */ if (pos && linebuf[pos-1] == '\r') { pos--; } log_data ("%s:%s: Parsing line=`%.*s'\n", SRCNAME, __func__, (int)pos, linebuf); /* Check the next state */ if (rfc822parse_insert (m_mime_ctx->msg, (unsigned char*) linebuf, pos)) { log_error ("%s:%s: rfc822 parser failed: %s\n", SRCNAME, __func__, strerror (errno)); TRETURN not_taken; } /* Check if the first line of the body is actually a PGP Inline message. If so treat it as crypto data. */ if (!m_mime_ctx->pgp_marker_checked && m_mime_ctx->collect_body == 2) { m_mime_ctx->pgp_marker_checked = true; if (pos >= 27 && !strncmp ("-----BEGIN PGP MESSAGE-----", linebuf, 27)) { log_debug ("%s:%s: Found PGP Message in body.", SRCNAME, __func__); m_mime_ctx->collect_body = 0; m_mime_ctx->collect_crypto_data = 1; m_mime_ctx->start_hashing = 1; m_collect_everything = true; } } /* If we are currently in a collecting state actually collect that line */ if (m_mime_ctx->collect_crypto_data && m_mime_ctx->start_hashing) { /* Save the signed data. Note that we need to delay the CR/LF because the last line ending belongs to the next boundary. */ if (m_mime_ctx->collect_crypto_data == 2) { m_crypto_data.write ("\r\n", 2); } log_data ("Writing raw crypto data: %.*s", (int)pos, linebuf); m_crypto_data.write (linebuf, pos); m_mime_ctx->collect_crypto_data = 2; } if (m_mime_ctx->in_data && !m_mime_ctx->collect_signature && !m_mime_ctx->collect_crypto_data) { /* We are inside of a plain part. Write it out. */ if (m_mime_ctx->in_data == 1) /* Skip the first line. */ m_mime_ctx->in_data = 2; int slbrk = 0; if (m_mime_ctx->is_qp_encoded) len = qp_decode (linebuf, pos, &slbrk); else if (m_mime_ctx->is_base64_encoded) len = b64_decode (&m_mime_ctx->base64, linebuf, pos); else len = pos; if (m_mime_ctx->collect_body) { /* For protected headers to filter out the legacy display part we have to first collect it in its own buffer and then later decide if it should be hidden or not. Depending on the reset of the mime structure. The legacy display part must be either text/plain or text/rfc822-headers so we only have to handle this case and not the HTML case below. */ if (m_mime_ctx->collect_body == 2) { std::string *target_buf = (m_mime_ctx->in_protected_headers ? &m_ph_helpbuf : &m_body); *target_buf += std::string(linebuf, len); if (!m_mime_ctx->is_base64_encoded && !slbrk) { *target_buf += "\r\n"; } } if (m_body_charset.empty()) { m_body_charset = m_mime_ctx->mimestruct_cur->charset ? m_mime_ctx->mimestruct_cur->charset : ""; } m_mime_ctx->collect_body = 2; } else if (m_mime_ctx->collect_html_body) { if (m_mime_ctx->collect_html_body == 2) { m_html_body += std::string(linebuf, len); if (!m_mime_ctx->is_base64_encoded && !slbrk) { m_html_body += "\r\n"; } } if (m_html_charset.empty()) { m_html_charset = m_mime_ctx->mimestruct_cur->charset ? m_mime_ctx->mimestruct_cur->charset : ""; } m_mime_ctx->collect_html_body = 2; } else if (m_mime_ctx->current_attachment && len) { m_mime_ctx->current_attachment->get_data().write(linebuf, len); if (!m_mime_ctx->is_base64_encoded && !slbrk) { m_mime_ctx->current_attachment->get_data().write("\r\n", 2); } } else { log_debug ("%s:%s Collecting finished.", SRCNAME, __func__); } } else if (m_mime_ctx->in_data && m_mime_ctx->collect_signature) { /* We are inside of a signature attachment part. */ if (m_mime_ctx->collect_signature == 1) /* Skip the first line. */ m_mime_ctx->collect_signature = 2; else { int slbrk = 0; if (m_mime_ctx->is_qp_encoded) len = qp_decode (linebuf, pos, &slbrk); else if (m_mime_ctx->is_base64_encoded) len = b64_decode (&m_mime_ctx->base64, linebuf, pos); else len = pos; if (!m_signature) { m_signature = new GpgME::Data(); } if (len) m_signature->write(linebuf, len); if (!m_mime_ctx->is_base64_encoded && !slbrk) m_signature->write("\r\n", 2); } } else if (m_mime_ctx->in_data && !m_mime_ctx->start_hashing) { /* We are inside the data. That should be the actual ciphertext in the given encoding. */ int slbrk = 0; if (m_mime_ctx->is_qp_encoded) len = qp_decode (linebuf, pos, &slbrk); else if (m_mime_ctx->is_base64_encoded) len = b64_decode (&m_mime_ctx->base64, linebuf, pos); else len = pos; log_data ("Writing crypto data: %.*s", (int)pos, linebuf); if (len) m_crypto_data.write(linebuf, len); if (!m_mime_ctx->is_base64_encoded && !slbrk) m_crypto_data.write("\r\n", 2); } /* Continue with next line. */ pos = 0; } } TRETURN not_taken; } #ifdef HAVE_W32_SYSTEM void MimeDataProvider::collect_data(LPSTREAM stream) { TSTART; if (!stream) { TRETURN; } HRESULT hr; char buf[BUFSIZE]; ULONG bRead; bool first_read = true; bool is_pgp_message = false; size_t allRead = 0; while ((hr = stream->Read (buf, BUFSIZE, &bRead)) == S_OK || hr == S_FALSE) { if (!bRead) { log_debug ("%s:%s: Input stream at EOF.", SRCNAME, __func__); break; } log_debug ("%s:%s: Read %lu bytes.", SRCNAME, __func__, bRead); allRead += bRead; if (first_read) { if (bRead > 12 && strncmp ("MIME-Version", buf, 12) == 0) { /* Fun! In case we have exchange or sent messages created by us we get the mail attachment like it is before the MAPI to MIME conversion. So it has our MIME structure. In that case we have to expect MIME data even if the initial data check suggests that we don't. Checking if the content starts with MIME-Version appears to be a robust way to check if we try to parse MIME data. */ m_collect_everything = false; log_debug ("%s:%s: Found MIME-Version marker." "Expecting headers even if type suggested not to.", SRCNAME, __func__); } else if (bRead > 12 && !strncmp ("Content-Type:", buf, 13)) { /* Similar as above but we messed with the order of the headers for some s/mime mails. So also check for content type. Want some cheese with that hack? */ m_collect_everything = false; log_debug ("%s:%s: Found Content-Type header." "Expecting headers even if type suggested not to.", SRCNAME, __func__); } /* check for the PGP MESSAGE marker to see if we have it. */ if (bRead && m_collect_everything) { std::string tmp (buf, bRead); std::size_t found = tmp.find ("-----BEGIN PGP MESSAGE-----"); if (found != std::string::npos) { log_debug ("%s:%s: found PGP Message marker,", SRCNAME, __func__); is_pgp_message = true; } } } first_read = false; if (m_collect_everything) { /* For S/MIME, Clearsigned, PGP MESSAGES we just pass everything on. Only the Multipart classes need parsing. And the output of course. */ log_debug ("%s:%s: Just copying data.", SRCNAME, __func__); m_crypto_data.write ((void*)buf, (size_t) bRead); continue; } m_rawbuf += std::string (buf, bRead); size_t not_taken = collect_input_lines (m_rawbuf.c_str(), m_rawbuf.size()); if (not_taken == m_rawbuf.size()) { log_error ("%s:%s: Collect failed to consume anything.\n" "Buffer too small?", SRCNAME, __func__); break; } log_debug ("%s:%s: Consumed: " SIZE_T_FORMAT " bytes", SRCNAME, __func__, m_rawbuf.size() - not_taken); m_rawbuf.erase (0, m_rawbuf.size() - not_taken); } if (is_pgp_message && allRead < (1024 * 100)) { /* Sometimes received PGP Messsages contain extra whitespace / newlines. To also accept such messages we fix up pgp inline messages here. We only do this for messages which are smaller then a hundred KByte for performance. */ log_debug ("%s:%s: Fixing up a possible broken message.", SRCNAME, __func__); /* Copy crypto data to string */ std::string data = m_crypto_data.toString(); m_crypto_data = GpgME::Data(); std::istringstream iss (data); // Now parse it by line. std::string line; while (std::getline (iss, line)) { trim (line); if (line == "-----BEGIN PGP MESSAGE-----") { /* Finish an armor header */ line += "\n\n"; m_crypto_data.write (line.c_str (), line.size ()); continue; } /* Remove empty lines */ if (line.empty()) { continue; } if (line.find (':') != std::string::npos) { log_data ("%s:%s: Removing comment '%s'.", SRCNAME, __func__, line.c_str ()); continue; } line += '\n'; m_crypto_data.write (line.c_str (), line.size ()); } } TRETURN; } #endif void MimeDataProvider::collect_data(FILE *stream) { TSTART; if (!stream) { TRETURN; } char buf[BUFSIZE]; size_t bRead; while ((bRead = fread (buf, 1, BUFSIZE, stream)) > 0) { log_debug ("%s:%s: Read " SIZE_T_FORMAT " bytes.", SRCNAME, __func__, bRead); if (m_collect_everything) { /* For S/MIME, Clearsigned, PGP MESSAGES we just pass everything on. Only the Multipart classes need parsing. And the output of course. */ log_debug ("%s:%s: Making verbatim copy" SIZE_T_FORMAT " bytes.", SRCNAME, __func__, bRead); m_crypto_data.write ((void*)buf, bRead); continue; } m_rawbuf += std::string (buf, bRead); size_t not_taken = collect_input_lines (m_rawbuf.c_str(), m_rawbuf.size()); if (not_taken == m_rawbuf.size()) { log_error ("%s:%s: Collect failed to consume anything.\n" "Buffer too small?", SRCNAME, __func__); TRETURN; } log_debug ("%s:%s: Consumed: " SIZE_T_FORMAT " bytes", SRCNAME, __func__, m_rawbuf.size() - not_taken); m_rawbuf.erase (0, m_rawbuf.size() - not_taken); } TRETURN; } ssize_t MimeDataProvider::write(const void *buffer, size_t bufSize) { TSTART; if (m_collect_everything) { /* Writing with collect everything one means that we are outputprovider. In this case for inline messages we want to collect everything. */ log_debug ("%s:%s: Using complete input as body " SIZE_T_FORMAT " bytes.", SRCNAME, __func__, bufSize); m_body += std::string ((const char *) buffer, bufSize); TRETURN bufSize; } m_rawbuf += std::string ((const char*)buffer, bufSize); size_t not_taken = collect_input_lines (m_rawbuf.c_str(), m_rawbuf.size()); if (not_taken == m_rawbuf.size()) { log_error ("%s:%s: Write failed to consume anything.\n" "Buffer too small? or no newlines in text?", SRCNAME, __func__); TRETURN bufSize; } log_debug ("%s:%s: Write Consumed: " SIZE_T_FORMAT " bytes", SRCNAME, __func__, m_rawbuf.size() - not_taken); m_rawbuf.erase (0, m_rawbuf.size() - not_taken); TRETURN bufSize; } off_t MimeDataProvider::seek(off_t offset, int whence) { return m_crypto_data.seek (offset, whence); } GpgME::Data * MimeDataProvider::signature() const { TSTART; TRETURN m_signature; } std::shared_ptr MimeDataProvider::create_attachment() { TSTART; log_debug ("%s:%s: Creating attachment.", SRCNAME, __func__); auto attach = std::shared_ptr (new Attachment()); attach->set_attach_type (ATTACHTYPE_FROMMOSS); m_mime_ctx->any_attachments_created = 1; /* And now for the real name. We avoid storing the name "smime.p7m" because that one is used at several places in the mapi conversion functions. */ if (m_mime_ctx->mimestruct_cur && m_mime_ctx->mimestruct_cur->filename) { if (!strcmp (m_mime_ctx->mimestruct_cur->filename, "smime.p7m")) { attach->set_display_name ("x-smime.p7m"); } else { log_debug ("%s:%s: Attachment filename: %s", SRCNAME, __func__, anonstr (m_mime_ctx->mimestruct_cur->filename)); attach->set_display_name (m_mime_ctx->mimestruct_cur->filename); } } if (m_mime_ctx->mimestruct_cur && m_mime_ctx->mimestruct_cur->cid) { attach->set_content_id (m_mime_ctx->mimestruct_cur->cid); log_debug ("%s:%s: content-id: %s", SRCNAME, __func__, anonstr (m_mime_ctx->mimestruct_cur->cid)); } if (m_mime_ctx->mimestruct_cur && m_mime_ctx->mimestruct_cur->content_type) { attach->set_content_type (m_mime_ctx->mimestruct_cur->content_type); log_debug ("%s:%s: content-type: %s", SRCNAME, __func__, m_mime_ctx->mimestruct_cur->content_type); } m_attachments.push_back (attach); TRETURN attach; /* TODO handle encoding */ } static std::string get_header (rfc822parse_t msg, const std::string &which) { TSTART; const char *buf = rfc822parse_get_field (msg, which.c_str (), -1, nullptr); if (buf) { /* String is "which: " so the + 3 is the colon, the space and one extra to ensure that we do not construct a std::string from null below. */ if (strlen (buf) <= which.size() + 3) { log_error ("%s:%s: Invalid header value '%s'", SRCNAME, __func__, buf); TRETURN std::string (); } std::string ret = buf + which.size () + 2; if (ret.size()) { ret = rfc2047_parse (ret.c_str ()); } TRETURN ret; } TRETURN std::string (); } void MimeDataProvider::finalize () { TSTART; if (m_protected_headers_version) { static std::vector user_headers = {"Subject", "From", "To", "Cc", "Date", "Reply-To", "Followup-To"}; for (const auto &hdr: user_headers) { m_protected_headers.emplace (hdr, get_header (m_mime_ctx->msg, hdr)); } } /* Now check the mime strucutre for that legacy-display handling of protected headers. If the mime structure is: multipart/mixed and the first subpart is text/plain or text/rfc822-headers that we hide the first text part as we parsed the headers above from that part. */ if (m_protected_headers_version == 1 && m_ph_helpbuf.size () && m_mime_ctx->mimestruct && m_mime_ctx->mimestruct->content_type && !strcmp (m_mime_ctx->mimestruct->content_type, "multipart/mixed") && m_mime_ctx->mimestruct->next && m_mime_ctx->mimestruct->next->content_type && ( !strcmp (m_mime_ctx->mimestruct->next->content_type, "text/plain") || !strcmp (m_mime_ctx->mimestruct->next->content_type, "text/rfc822-headers"))) { log_debug ("%s:%s: Detected protected headers legacy part. It will be hidden.", SRCNAME, __func__); log_data ("%s:%s: PH legacy part: '%s'", SRCNAME, __func__, m_ph_helpbuf.c_str ()); } else if (m_ph_helpbuf.size ()) { log_debug ("%s:%s: Prepending protected headers part to buffer.", SRCNAME, __func__); m_body = m_ph_helpbuf + m_body; } if (m_rawbuf.size ()) { m_rawbuf += "\r\n"; size_t not_taken = collect_input_lines (m_rawbuf.c_str(), m_rawbuf.size()); m_rawbuf.erase (0, m_rawbuf.size() - not_taken); if (m_rawbuf.size ()) { log_error ("%s:%s: Collect left data in buffer.\n", SRCNAME, __func__); } } TRETURN; } const std::string &MimeDataProvider::get_body () { TSTART; TRETURN m_body; } const std::string &MimeDataProvider::get_html_body () { TSTART; TRETURN m_html_body; } const std::string &MimeDataProvider::get_html_charset() const { TSTART; TRETURN m_html_charset; } const std::string &MimeDataProvider::get_body_charset() const { TSTART; TRETURN m_body_charset; } std::string MimeDataProvider::get_protected_header (const std::string &which) const { TSTART; const auto it = m_protected_headers.find (which); if (it != m_protected_headers.end ()) { TRETURN it->second; } TRETURN std::string (); } std::string MimeDataProvider::get_content_type () const { return m_content_type; } void MimeDataProvider::set_content_type (const char *ctmain, const char *ctsub) { std::string main = ctmain ? std::string (ctmain) : std::string (); std::string sub = ctsub ? std::string ("/") + std::string (ctsub) : std::string (); m_content_type = main + sub; } diff --git a/src/mimemaker.cpp b/src/mimemaker.cpp index f8e4f48..bc1b2f2 100644 --- a/src/mimemaker.cpp +++ b/src/mimemaker.cpp @@ -1,1735 +1,1686 @@ /* 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 "mapihelp.h" #include "mimemaker.h" #include "oomhelp.h" #include "mail.h" +#include "attachment.h" +#include "cpphelp.h" #undef _ #define _(a) utf8_gettext (a) 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->appendToInlineBody (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; } /* 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); memdbg_addRef (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 = xstrdup (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 ? xstrdup (overrideMimeTag) : xstrdup ("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 = gpgol_openProperty (att, 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 content type from the FILENAME. The return value is a static string there won't be an error return. In case Base 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, "msg", "application/ms-tnef" }, { 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, "docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }, { 1, "dot", "application/msword" }, { 1, "dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template" }, { 1, "docm", "application/application/vnd.ms-word.document.macroEnabled.12" }, { 1, "dotm", "application/vnd.ms-word.template.macroEnabled.12" }, { 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, "pot", "application/vnd.ms-powerpoint" }, { 1, "ppa", "application/vnd.ms-powerpoint" }, { 1, "pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" }, { 1, "potx", "application/vnd.openxmlformats-officedocument.presentationml.template" }, { 1, "ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow" }, { 1, "ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12" }, { 1, "pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12" }, { 1, "potm", "application/vnd.ms-powerpoint.template.macroEnabled.12" }, { 1, "ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12" }, { 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, "xlsx", "application/vnd.ms-excel" }, { 1, "xlt", "application/vnd.ms-excel" }, { 1, "xla", "application/vnd.ms-excel" }, { 1, "xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template" }, { 1, "xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12" }, { 1, "xltm", "application/vnd.ms-excel.template.macroEnabled.12" }, { 1, "xlam", "application/vnd.ms-excel.addin.macroEnabled.12" }, { 1, "xlsb", "application/application/vnd.ms-excel.sheet.binary.macroEnabled.12" }, { 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) { const char *dot = strrchr (filename, '.'); if (dot) { suffix = dot; } } /* Check for at least one char after the dot. */ if (suffix.size() > 1) { /* Erase the dot */ 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. */ 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); } 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 = nullptr, const char *added_headers = nullptr) { 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 ? anonstr (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; /* Add any injected additional headers */ if (added_headers) { if ((rc = write_multistring (sink, added_headers, nullptr))) { 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 || table[idx].method == ATTACH_OLE || table[idx].method == ATTACH_EMBEDDED_MSG)) { /* OLE and embedded are usable becase we plan to add support later. First version only handled them with a warning in write attachments. */ 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. If only_related is 2 all attachments are included regardless of content-id. */ static int write_attachments (sink_t sink, - LPMESSAGE message, mapi_attach_item_t *table, + Mail *mail, const char *boundary, int only_related) { - int idx, rc; - char *buffer; - size_t buflen; + TSTART; + int rc; bool warning_shown = false; - - 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 == 1 && !table[idx].content_id) - { - continue; - } - else if (!only_related && table[idx].content_id) - { - continue; - } - buffer = mapi_get_attach (message, 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); - } - else if (!only_related && !warning_shown - && table[idx].attach_type == ATTACHTYPE_UNKNOWN - && (table[idx].method == ATTACH_OLE - || table[idx].method == ATTACH_EMBEDDED_MSG)) - { - char *fmt; - log_debug ("%s:%s: detected OLE attachment. Showing warning.", - SRCNAME, __func__); - gpgrt_asprintf (&fmt, _("The attachment '%s' is an Outlook item " - "which is currently unsupported in crypto mails."), - table[idx].filename ? - table[idx].filename : _("Unknown")); - std::string msg = fmt; - msg += "\n\n"; - xfree (fmt); - - gpgrt_asprintf (&fmt, _("Please encrypt '%s' with Kleopatra " - "and attach it as a file."), - table[idx].filename ? - table[idx].filename : _("Unknown")); - msg += fmt; - xfree (fmt); - - msg += "\n\n"; - msg += _("Send anyway?"); - warning_shown = true; - - if (gpgol_message_box (get_active_hwnd (), - msg.c_str (), - _("Sorry, that's not possible, yet"), - MB_APPLMODAL | MB_YESNO) == IDNO) - { - return -1; - } - } - else - { - log_debug ("%s:%s: Skipping unknown attachment at idx: %d type: %d" - " with method: %d", - SRCNAME, __func__, idx, table[idx].attach_type, - table[idx].method); - } - } - 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->isHTMLAlternative () || !table) + for (auto attach: mail->plainAttachments ()) { - return 0; - } - - int related = 0; - int mixed = 0; - for (int idx = 0; !table[idx].end_of_table; idx++) - { - if (table[idx].content_id) + std::string cid = attach->get_content_id (); + if (only_related == 1 && !cid.size ()) { - related = 1; + continue; } - else + else if (!only_related && cid.size ()) { - mixed = 1; + continue; + } + std::string buf = attach->get_data ().toString (); + const auto name = attach->get_file_name (); + if (!warning_shown && !only_related && !buf.size ()) + { + log_debug ("%s:%s: detected OLE attachment. Showing warning.", + SRCNAME, __func__); + std::string msg = asprintf_s (_("The attachment '%s' is an Outlook item " + "which is currently unsupported in crypto mails."), + name.size () ? + name.c_str (): _("Unknown")); + msg += "\n\n"; + msg += asprintf_s (_("Please encrypt '%s' with Kleopatra " + "and attach it as a file."), + name.size () ? + name.c_str (): _("Unknown")); + msg += "\n\n"; + msg += _("Send anyway?"); + warning_shown = true; + + if (gpgol_message_box (get_active_hwnd (), + msg.c_str (), + _("Sorry, that's not possible, yet"), + MB_APPLMODAL | MB_YESNO) == IDNO) + { + TRETURN -1; + } + } + rc = write_part (sink, buf.c_str (), buf.size (), boundary, + name.c_str (), 0, cid.size () ? cid.c_str () : nullptr); + if (rc) + { + log_error ("Write part returned err: %i", rc); } } - return mixed + related; + TRETURN 0; } - /* 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, bool is_draft, int exchange_major_version) { HRESULT hr = 0; SPropValue prop; SPropTagArray proparray; /* Set the message class. */ prop.ulPropTag = PR_MESSAGE_CLASS_A; if (protocol == PROTOCOL_SMIME) { /* When sending over exchange to the same server the recipient might see the message class we set here. So for S/MIME we keep the original. This makes the sent folder icon not immediately showing the GpgOL icon but gives other clients that do not have GpgOL installed a better chance to handle the mail. */ if (encrypt && exchange_major_version >= 15) { /* This only appears to work with later exchange versions */ prop.Value.lpszA = xstrdup ("IPM.Note.SMIME"); } else { prop.Value.lpszA = xstrdup ("IPM.Note.SMIME.MultipartSigned"); } } else if (encrypt) { prop.Value.lpszA = xstrdup ("IPM.Note.InfoPathForm.GpgOL.SMIME.MultipartSigned"); } else { prop.Value.lpszA = xstrdup ("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; } /* 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. when we send it. If it is a draft we are encrypting we want to keep them. To avoid confusion: draft_info for us means the state of the secure toggle button. */ if (!is_draft) { 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; } /* 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->isHTMLAlternative (); } int rc = 0; if (!is_alternative || !plain_body) { if (plain_body) { rc = write_part (sink, plain_body, strlen (plain_body), boundary, NULL, 1, nullptr, mail ? mail->protectedHeaders ().c_str () : nullptr); } /* 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, nullptr, mail ? mail->protectedHeaders().c_str() : nullptr))) { 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->takeCachedHTMLBody (); 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) +add_body_and_attachments (sink_t sink, Mail *mail, const char *body) { - int related = is_related (mail, att_table); + if (!mail) + { + STRANGEPOINT; + TRETURN -1; + } + int related = mail->isMultipartRelated (); int rc = 0; char inner_boundary[BOUNDARYSIZE+1]; char outer_boundary[BOUNDARYSIZE+1]; *outer_boundary = 0; *inner_boundary = 0; + int n_att_usable = mail->plainAttachments ().size (); 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, + rc = write_attachments (sink, mail, *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 we are multipart related the related attachments were already written above. If we are not related we pass 2 to the write_attachements function to force that even attachments with a content id are written out. This happens for example when forwarding a plain text mail with attachments. */ if (!rc && n_att_usable) { - rc = write_attachments (sink, message, att_table, + rc = write_attachments (sink, mail, *outer_boundary? outer_boundary : NULL, related ? 0 : 2); } if (rc) { return rc; } /* Finish the possible multipart/mixed. */ if (*outer_boundary && (rc = write_boundary (sink, outer_boundary, 1))) return rc; return rc; } /* 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 exchange_major_version) { 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; if (exchange_major_version >= 15) { /* 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 = 0; } else { 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" "Content-Disposition: inline;\r\n" "\tfilename=\"" OPENPGP_ENC_NAME "\"\r\n" "Content-Transfer-Encoding: 7Bit\r\n" "\r\n", NULL); } return rc; } 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; } err = 0; done: xfree (orig); return err; } diff --git a/src/mimemaker.h b/src/mimemaker.h index f92501b..2cb4115 100644 --- a/src/mimemaker.h +++ b/src/mimemaker.h @@ -1,104 +1,103 @@ /* mimemaker.h - Construct MIME from MAPI * Copyright (C) 2007, 2008 g10 Code GmbH * Copyright (C) 2015, 2016 by Bundesamt für Sicherheit in der Informationstechnik * Software engineering by Intevation GmbH * * This file is part of GpgOL. * * GpgOL is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * GpgOL is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, see . */ #ifndef MIMEMAKER_H #define MIMEMAKER_H #include "mapihelp.h" class Mail; #ifdef __cplusplus extern "C" { #if 0 } #endif #endif /* Names for our attachments */ #define OPENPGP_ENC_NAME "openpgp-encrypted-message.asc" #define OPENPGP_SIG_NAME "openpgp-digital-signature.asc" #define SMIME_SIG_NAME "smime.p7s" /* The object we use instead of IStream. It allows us to have a callback method for output and thus for processing stuff recursively. */ struct sink_s; typedef struct sink_s *sink_t; struct sink_s { void *cb_data; sink_t extrasink; int (*writefnc)(sink_t sink, const void *data, size_t datalen); unsigned long enc_counter; /* Used by write_buffer_for_cb. */ /* struct { */ /* int idx; */ /* unsigned char inbuf[4]; */ /* int quads; */ /* } b64; */ }; int sink_std_write (sink_t sink, const void *data, size_t datalen); int sink_file_write (sink_t sink, const void *data, size_t datalen); int sink_encryption_write (sink_t encsink, const void *data, size_t datalen); int write_buffer_for_cb (void *opaque, const void *data, size_t datalen); int write_buffer (sink_t sink, const void *data, size_t datalen); /** @brief Try to restore a message from the moss attachment. * * Try to turn the moss attachment back into a Mail that other * MUAs could handle. Uses all the tricks available to archive * that. Returns 0 on success. */ int restore_msg_from_moss (LPMESSAGE message, LPDISPATCH moss_att, msgtype_t type, char *msgcls); int count_usable_attachments (mapi_attach_item_t *table); -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 add_body_and_attachments (sink_t sink, Mail *mail, + const char *body); int create_top_encryption_header (sink_t sink, protocol_t protocol, char *boundary, bool is_inline = false, int exchange_major_version = -1); /* 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); LPATTACH create_mapi_attachment (LPMESSAGE message, sink_t sink, const char *overrideMimeTag = nullptr); int close_mapi_attachment (LPATTACH *attach, sink_t sink); int finalize_message (LPMESSAGE message, mapi_attach_item_t *att_table, protocol_t protocol, int encrypt, bool is_inline = false, bool is_draft = false, int exchange_major_version = -1); void cancel_mapi_attachment (LPATTACH *attach, sink_t sink); void create_top_signing_header (char *buffer, size_t buflen, protocol_t protocol, int first, const char *boundary, const char *micalg); int write_string (sink_t sink, const char *text); int write_b64 (sink_t sink, const void *data, size_t datalen); /* Encode an input string according to rfc2047 caller needs to free result. */ char *utf8_to_rfc2047b (const char *input); #ifdef __cplusplus } #endif #endif /*MIMEMAKER_H*/ diff --git a/src/oomhelp.cpp b/src/oomhelp.cpp index c975587..4f41013 100644 --- a/src/oomhelp.cpp +++ b/src/oomhelp.cpp @@ -1,3662 +1,3744 @@ /* oomhelp.cpp - 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 * 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 . */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include "common.h" #include "oomhelp.h" #include "cpphelp.h" #include "gpgoladdin.h" #include "categorymanager.h" #include "recipient.h" HRESULT gpgol_queryInterface (LPUNKNOWN pObj, REFIID riid, LPVOID FAR *ppvObj) { HRESULT ret = pObj->QueryInterface (riid, ppvObj); if (ret) { log_debug ("%s:%s: QueryInterface failed hr=%#lx", SRCNAME, __func__, ret); } else if ((opt.enable_debug & DBG_MEMORY) && *ppvObj) { memdbg_addRef (*ppvObj); } return ret; } HRESULT gpgol_openProperty (LPMAPIPROP obj, ULONG ulPropTag, LPCIID lpiid, ULONG ulInterfaceOptions, ULONG ulFlags, LPUNKNOWN FAR * lppUnk) { HRESULT ret = obj->OpenProperty (ulPropTag, lpiid, ulInterfaceOptions, ulFlags, lppUnk); if (ret) { log_debug ("%s:%s: OpenProperty failed hr=%#lx %s", SRCNAME, __func__, ret, mapi_err_to_string (ret)); } else if ((opt.enable_debug & DBG_MEMORY) && *lppUnk) { memdbg_addRef (*lppUnk); log_debug ("%s:%s: OpenProperty on %p prop %lx result %p", SRCNAME, __func__, obj, ulPropTag, *lppUnk); } return ret; } /* Return a malloced string with the utf-8 encoded name of the object or NULL if not available. */ char * get_object_name (LPUNKNOWN obj) { TSTART; HRESULT hr; LPDISPATCH disp = NULL; LPTYPEINFO tinfo = NULL; BSTR bstrname; char *name = NULL; if (!obj) goto leave; /* We can't use gpgol_queryInterface here to avoid recursion */ hr = obj->QueryInterface (IID_IDispatch, (void **)&disp); if (!disp || hr != S_OK) goto leave; disp->GetTypeInfo (0, 0, &tinfo); if (!tinfo) { log_debug ("%s:%s: no typeinfo found for object\n", SRCNAME, __func__); goto leave; } bstrname = NULL; hr = tinfo->GetDocumentation (MEMBERID_NIL, &bstrname, 0, 0, 0); if (hr || !bstrname) log_debug ("%s:%s: GetDocumentation failed: hr=%#lx\n", SRCNAME, __func__, hr); if (bstrname) { name = wchar_to_utf8 (bstrname); SysFreeString (bstrname); } leave: if (tinfo) tinfo->Release (); if (disp) disp->Release (); TRETURN name; } std::string get_object_name_s (LPUNKNOWN obj) { char *name = get_object_name (obj); std::string ret = "(null)"; if (name) { ret = name; } xfree (name); return ret; } std::string get_object_name_s (shared_disp_t obj) { return get_object_name_s (obj.get ()); } /* Lookup the dispid of object PDISP for member NAME. Returns DISPID_UNKNOWN on error. */ DISPID lookup_oom_dispid (LPDISPATCH pDisp, const char *name) { HRESULT hr; DISPID dispid; wchar_t *wname; if (!pDisp || !name) { TRETURN DISPID_UNKNOWN; /* Error: Invalid arg. */ } wname = utf8_to_wchar (name); if (!wname) { TRETURN DISPID_UNKNOWN;/* Error: Out of memory. */ } hr = pDisp->GetIDsOfNames (IID_NULL, &wname, 1, LOCALE_SYSTEM_DEFAULT, &dispid); xfree (wname); if (hr != S_OK || dispid == DISPID_UNKNOWN) log_debug ("%s:%s: error looking up dispid(%s)=%d: hr=0x%x\n", SRCNAME, __func__, name, (int)dispid, (unsigned int)hr); if (hr != S_OK) dispid = DISPID_UNKNOWN; return dispid; } static void init_excepinfo (EXCEPINFO *err) { if (!err) { TRETURN; } err->wCode = 0; err->wReserved = 0; err->bstrSource = nullptr; err->bstrDescription = nullptr; err->bstrHelpFile = nullptr; err->dwHelpContext = 0; err->pvReserved = nullptr; err->pfnDeferredFillIn = nullptr; err->scode = 0; } void dump_excepinfo (EXCEPINFO err) { log_oom ("%s:%s: Exception: \n" " wCode: 0x%x\n" " wReserved: 0x%x\n" " source: %S\n" " desc: %S\n" " help: %S\n" " helpCtx: 0x%x\n" " deferredFill: %p\n" " scode: 0x%x\n", SRCNAME, __func__, (unsigned int) err.wCode, (unsigned int) err.wReserved, err.bstrSource ? err.bstrSource : L"null", err.bstrDescription ? err.bstrDescription : L"null", err.bstrHelpFile ? err.bstrDescription : L"null", (unsigned int) err.dwHelpContext, err.pfnDeferredFillIn, (unsigned int) err.scode); } /* Return the OOM object's IDispatch interface described by FULLNAME. Returns NULL if not found. PSTART is the object where the search starts. FULLNAME is a dot delimited sequence of object names. If an object name has a "(foo)" suffix this passes it as a parameter to the invoke function (i.e. using (DISPATCH|PROPERTYGET)). Object names including the optional suffix are truncated at 127 byte. */ LPDISPATCH get_oom_object (LPDISPATCH pStart, const char *fullname) { TSTART; HRESULT hr; LPDISPATCH pObj = pStart; LPDISPATCH pDisp = NULL; log_oom ("%s:%s: looking for %p->`%s'", SRCNAME, __func__, pStart, fullname); while (pObj) { DISPPARAMS dispparams; VARIANT aVariant[4]; VARIANT vtResult; wchar_t *wname; char name[128]; int n_parms = 0; BSTR parmstr = NULL; INT parmint = 0; DISPID dispid; char *p, *pend; int dispmethod; unsigned int argErr = 0; EXCEPINFO execpinfo; init_excepinfo (&execpinfo); if (pDisp) { gpgol_release (pDisp); pDisp = NULL; } if (gpgol_queryInterface (pObj, IID_IDispatch, (LPVOID*)&pDisp) != S_OK) { log_error ("%s:%s Object does not support IDispatch", SRCNAME, __func__); if (pObj != pStart) gpgol_release (pObj); TRETURN NULL; } /* Confirmed through testing that the retval needs a release */ if (pObj != pStart) gpgol_release (pObj); pObj = NULL; if (!pDisp) { TRETURN NULL; /* The object has no IDispatch interface. */ } if (!*fullname) { if ((opt.enable_debug & DBG_MEMORY)) { pDisp->AddRef (); int ref = pDisp->Release (); log_oom ("%s:%s: got %p with %i refs", SRCNAME, __func__, pDisp, ref); } TRETURN pDisp; /* Ready. */ } /* Break out the next name part. */ { const char *dot; size_t n; dot = strchr (fullname, '.'); if (dot == fullname) { gpgol_release (pDisp); TRETURN NULL; /* Empty name part: error. */ } else if (dot) n = dot - fullname; else n = strlen (fullname); if (n >= sizeof name) n = sizeof name - 1; strncpy (name, fullname, n); name[n] = 0; if (dot) fullname = dot + 1; else fullname += strlen (fullname); } if (!strncmp (name, "get_", 4) && name[4]) { dispmethod = DISPATCH_PROPERTYGET; memmove (name, name+4, strlen (name+4)+1); } else if ((p = strchr (name, '('))) { *p++ = 0; pend = strchr (p, ')'); if (pend) *pend = 0; if (*p == ',' && p[1] != ',') { /* We assume this is "foo(,30007)". I.e. the frst arg is not given and the second one is an integer. */ parmint = (int)strtol (p+1, NULL, 10); n_parms = 4; } else { wname = utf8_to_wchar (p); if (wname) { parmstr = SysAllocString (wname); xfree (wname); } if (!parmstr) { gpgol_release (pDisp); TRETURN NULL; /* Error: Out of memory. */ } n_parms = 1; } dispmethod = DISPATCH_METHOD|DISPATCH_PROPERTYGET; } else dispmethod = DISPATCH_METHOD; /* Lookup the dispid. */ dispid = lookup_oom_dispid (pDisp, name); if (dispid == DISPID_UNKNOWN) { if (parmstr) SysFreeString (parmstr); gpgol_release (pDisp); TRETURN NULL; /* Name not found. */ } /* Invoke the method. */ dispparams.rgvarg = aVariant; dispparams.cArgs = 0; if (n_parms) { if (n_parms == 4) { dispparams.rgvarg[0].vt = VT_ERROR; dispparams.rgvarg[0].scode = DISP_E_PARAMNOTFOUND; dispparams.rgvarg[1].vt = VT_ERROR; dispparams.rgvarg[1].scode = DISP_E_PARAMNOTFOUND; dispparams.rgvarg[2].vt = VT_INT; dispparams.rgvarg[2].intVal = parmint; dispparams.rgvarg[3].vt = VT_ERROR; dispparams.rgvarg[3].scode = DISP_E_PARAMNOTFOUND; dispparams.cArgs = n_parms; } else if (n_parms == 1 && parmstr) { dispparams.rgvarg[0].vt = VT_BSTR; dispparams.rgvarg[0].bstrVal = parmstr; dispparams.cArgs++; } } dispparams.cNamedArgs = 0; VariantInit (&vtResult); hr = pDisp->Invoke (dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, dispmethod, &dispparams, &vtResult, &execpinfo, &argErr); if (parmstr) SysFreeString (parmstr); if (hr != S_OK || vtResult.vt != VT_DISPATCH) { log_debug ("%s:%s: failure: '%s' p=%p vt=%d hr=0x%x argErr=0x%x dispid=0x%x", SRCNAME, __func__, name, vtResult.pdispVal, vtResult.vt, (unsigned int)hr, (unsigned int)argErr, (unsigned int)dispid); dump_excepinfo (execpinfo); VariantClear (&vtResult); gpgol_release (pDisp); TRETURN NULL; /* Invoke failed. */ } pObj = vtResult.pdispVal; memdbg_addRef (pObj); } gpgol_release (pDisp); log_debug ("%s:%s: no object", SRCNAME, __func__); TRETURN NULL; } shared_disp_t get_oom_object_s (shared_disp_t pStart, const char *fullname) { return MAKE_SHARED (get_oom_object (pStart.get (), fullname)); } shared_disp_t get_oom_object_s (LPDISPATCH pStart, const char *fullname) { return MAKE_SHARED (get_oom_object (pStart, fullname)); } /* Helper for put_oom_icon. */ static int put_picture_or_mask (LPDISPATCH pDisp, int resource, int size, int is_mask) { TSTART; HRESULT hr; PICTDESC pdesc; LPDISPATCH pPict; DISPID dispid_put = DISPID_PROPERTYPUT; UINT fuload; DISPID dispid; DISPPARAMS dispparams; VARIANT aVariant[2]; /* When loading the mask we need to set the monochrome flag. We better create a DIB section to avoid possible rendering problems. */ fuload = LR_CREATEDIBSECTION | LR_SHARED; if (is_mask) fuload |= LR_MONOCHROME; memset (&pdesc, 0, sizeof pdesc); pdesc.cbSizeofstruct = sizeof pdesc; pdesc.picType = PICTYPE_BITMAP; pdesc.bmp.hbitmap = (HBITMAP) LoadImage (glob_hinst, MAKEINTRESOURCE (resource), IMAGE_BITMAP, size, size, fuload); if (!pdesc.bmp.hbitmap) { log_error_w32 (-1, "%s:%s: LoadImage(%d) failed\n", SRCNAME, __func__, resource); TRETURN -1; } /* Wrap the image into an OLE object. */ hr = OleCreatePictureIndirect (&pdesc, IID_IPictureDisp, TRUE, (void **) &pPict); if (hr != S_OK || !pPict) { log_error ("%s:%s: OleCreatePictureIndirect failed: hr=%#lx\n", SRCNAME, __func__, hr); TRETURN -1; } /* Store to the Picture or Mask property of the CommandBarButton. */ dispid = lookup_oom_dispid (pDisp, is_mask? "Mask":"Picture"); dispparams.rgvarg = aVariant; dispparams.rgvarg[0].vt = VT_DISPATCH; dispparams.rgvarg[0].pdispVal = pPict; dispparams.cArgs = 1; dispparams.rgdispidNamedArgs = &dispid_put; dispparams.cNamedArgs = 1; hr = pDisp->Invoke (dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_PROPERTYPUT, &dispparams, NULL, NULL, NULL); if (hr != S_OK) { log_debug ("%s:%s: Putting icon failed: %#lx", SRCNAME, __func__, hr); TRETURN -1; } TRETURN 0; } /* Update the icon of PDISP using the bitmap with RESOURCE ID. The function adds the system pixel size to the resource id to compute the actual icon size. The resource id of the mask is the N+1. */ int put_oom_icon (LPDISPATCH pDisp, int resource_id, int size) { TSTART; int rc; /* This code is only relevant for Outlook < 2010. Ideally it should grab the system pixel size and use an icon of the appropiate size (e.g. 32 or 64px) */ rc = put_picture_or_mask (pDisp, resource_id, size, 0); if (!rc) rc = put_picture_or_mask (pDisp, resource_id + 1, size, 1); TRETURN rc; } /* Set the boolean property NAME to VALUE. */ int put_oom_bool (LPDISPATCH pDisp, const char *name, int value) { TSTART; HRESULT hr; DISPID dispid_put = DISPID_PROPERTYPUT; DISPID dispid; DISPPARAMS dispparams; VARIANT aVariant[1]; dispid = lookup_oom_dispid (pDisp, name); if (dispid == DISPID_UNKNOWN) { TRETURN -1; } dispparams.rgvarg = aVariant; dispparams.rgvarg[0].vt = VT_BOOL; dispparams.rgvarg[0].boolVal = value? VARIANT_TRUE:VARIANT_FALSE; dispparams.cArgs = 1; dispparams.rgdispidNamedArgs = &dispid_put; dispparams.cNamedArgs = 1; hr = pDisp->Invoke (dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_PROPERTYPUT, &dispparams, NULL, NULL, NULL); if (hr != S_OK) { log_debug ("%s:%s: Putting '%s' failed: %#lx", SRCNAME, __func__, name, hr); TRETURN -1; } TRETURN 0; } /* Set the property NAME to VALUE. */ int put_oom_int (LPDISPATCH pDisp, const char *name, int value) { TSTART; HRESULT hr; DISPID dispid_put = DISPID_PROPERTYPUT; DISPID dispid; DISPPARAMS dispparams; VARIANT aVariant[1]; dispid = lookup_oom_dispid (pDisp, name); if (dispid == DISPID_UNKNOWN) { TRETURN -1; } dispparams.rgvarg = aVariant; dispparams.rgvarg[0].vt = VT_INT; dispparams.rgvarg[0].intVal = value; dispparams.cArgs = 1; dispparams.rgdispidNamedArgs = &dispid_put; dispparams.cNamedArgs = 1; hr = pDisp->Invoke (dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_PROPERTYPUT, &dispparams, NULL, NULL, NULL); if (hr != S_OK) { log_debug ("%s:%s: Putting '%s' failed: %#lx", SRCNAME, __func__, name, hr); TRETURN -1; } TRETURN 0; } /* Set the property NAME to VALUE. */ int put_oom_array (LPDISPATCH pDisp, const char *name, unsigned char *value, size_t size) { TSTART; HRESULT hr; DISPID dispid_put = DISPID_PROPERTYPUT; DISPID dispid; DISPPARAMS dispparams; VARIANT aVariant[1]; unsigned int argErr = 0; EXCEPINFO execpinfo; init_excepinfo (&execpinfo); VariantInit (aVariant); dispid = lookup_oom_dispid (pDisp, name); if (dispid == DISPID_UNKNOWN) { TRETURN -1; } /* Prepare the savearray */ SAFEARRAYBOUND saBound; saBound.lLbound = 0; saBound.cElements = size; SAFEARRAY* psa = SafeArrayCreate(VT_UI1, 1, &saBound); if (!psa) { log_err ("Failed to create SafeArray"); TRETURN -1; } hr = SafeArrayLock(psa); if (!SUCCEEDED (hr)) { log_err ("Failed to lock array."); } memcpy (psa->pvData, value, size); SafeArrayUnlock(psa); dispparams.rgvarg = aVariant; dispparams.rgvarg[0].vt = VT_ARRAY; dispparams.rgvarg[0].parray = psa; dispparams.cArgs = 1; dispparams.rgdispidNamedArgs = &dispid_put; dispparams.cNamedArgs = 1; hr = pDisp->Invoke (dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_PROPERTYPUT, &dispparams, NULL, &execpinfo, &argErr); SafeArrayDestroy(psa); if (hr != S_OK) { log_debug ("%s:%s: error: putting %s" " hr=0x%x argErr=0x%x", SRCNAME, __func__, name, (unsigned int)hr, (unsigned int)argErr); dump_excepinfo (execpinfo); TRETURN -1; } TRETURN 0; } /* Set the property NAME to STRING. */ int put_oom_string (LPDISPATCH pDisp, const char *name, const char *string) { TSTART; HRESULT hr; DISPID dispid_put = DISPID_PROPERTYPUT; DISPID dispid; DISPPARAMS dispparams; VARIANT aVariant[1]; BSTR bstring; EXCEPINFO execpinfo; init_excepinfo (&execpinfo); dispid = lookup_oom_dispid (pDisp, name); if (dispid == DISPID_UNKNOWN) { TRETURN -1; } { wchar_t *tmp = utf8_to_wchar (string); bstring = tmp? SysAllocString (tmp):NULL; xfree (tmp); if (!bstring) { log_error_w32 (-1, "%s:%s: SysAllocString failed", SRCNAME, __func__); TRETURN -1; } } dispparams.rgvarg = aVariant; dispparams.rgvarg[0].vt = VT_BSTR; dispparams.rgvarg[0].bstrVal = bstring; dispparams.cArgs = 1; dispparams.rgdispidNamedArgs = &dispid_put; dispparams.cNamedArgs = 1; hr = pDisp->Invoke (dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_PROPERTYPUT, &dispparams, NULL, &execpinfo, NULL); SysFreeString (bstring); if (hr != S_OK) { log_debug ("%s:%s: Putting '%s' failed: %#lx", SRCNAME, __func__, name, hr); dump_excepinfo (execpinfo); TRETURN -1; } TRETURN 0; } /* Set the property NAME to DISP. */ int put_oom_disp (LPDISPATCH pDisp, const char *name, LPDISPATCH disp) { TSTART; HRESULT hr; DISPID dispid_put = DISPID_PROPERTYPUT; DISPID dispid; DISPPARAMS dispparams; VARIANT aVariant[1]; EXCEPINFO execpinfo; init_excepinfo (&execpinfo); dispid = lookup_oom_dispid (pDisp, name); if (dispid == DISPID_UNKNOWN) { TRETURN -1; } dispparams.rgvarg = aVariant; dispparams.rgvarg[0].vt = VT_DISPATCH; dispparams.rgvarg[0].pdispVal = disp; dispparams.cArgs = 1; dispparams.rgdispidNamedArgs = &dispid_put; dispparams.cNamedArgs = 1; hr = pDisp->Invoke (dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_PROPERTYPUTREF, &dispparams, NULL, &execpinfo, NULL); if (hr != S_OK) { log_debug ("%s:%s: Putting '%s' failed: %#lx", SRCNAME, __func__, name, hr); dump_excepinfo (execpinfo); TRETURN -1; } TRETURN 0; } /* Get the boolean property NAME of the object PDISP. Returns False if not found or if it is not a boolean property. */ int get_oom_bool (LPDISPATCH pDisp, const char *name) { TSTART; HRESULT hr; int result = 0; DISPID dispid; dispid = lookup_oom_dispid (pDisp, name); if (dispid != DISPID_UNKNOWN) { DISPPARAMS dispparams = {NULL, NULL, 0, 0}; VARIANT rVariant; VariantInit (&rVariant); hr = pDisp->Invoke (dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_PROPERTYGET, &dispparams, &rVariant, NULL, NULL); if (hr != S_OK) log_debug ("%s:%s: Property '%s' not found: %#lx", SRCNAME, __func__, name, hr); else if (rVariant.vt != VT_BOOL) log_debug ("%s:%s: Property `%s' is not a boolean (vt=%d)", SRCNAME, __func__, name, rVariant.vt); else result = !!rVariant.boolVal; VariantClear (&rVariant); } TRETURN result; } /* Get the integer property NAME of the object PDISP. Returns 0 if not found or if it is not an integer property. */ int get_oom_int (LPDISPATCH pDisp, const char *name) { TSTART; HRESULT hr; int result = 0; DISPID dispid; dispid = lookup_oom_dispid (pDisp, name); if (dispid != DISPID_UNKNOWN) { DISPPARAMS dispparams = {NULL, NULL, 0, 0}; VARIANT rVariant; VariantInit (&rVariant); hr = pDisp->Invoke (dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_PROPERTYGET, &dispparams, &rVariant, NULL, NULL); if (hr != S_OK) log_debug ("%s:%s: Property '%s' not found: %#lx", SRCNAME, __func__, name, hr); else if (rVariant.vt != VT_INT && rVariant.vt != VT_I4) log_debug ("%s:%s: Property `%s' is not an integer (vt=%d)", SRCNAME, __func__, name, rVariant.vt); else result = rVariant.intVal; VariantClear (&rVariant); } TRETURN result; } int get_oom_int (shared_disp_t pDisp, const char *name) { return get_oom_int (pDisp.get (), name); } int get_oom_dirty (LPDISPATCH pDisp) { TSTART; HRESULT hr; DISPID dispid = DISPID_DIRTY_RAT; DISPPARAMS dispparams = {NULL, NULL, 0, 0}; VARIANT rVariant; VariantInit (&rVariant); hr = pDisp->Invoke (dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_PROPERTYGET | DISPATCH_METHOD, &dispparams, &rVariant, NULL, NULL); if (hr != S_OK) { log_debug ("%s:%s: Property dirty not found: %#lx", SRCNAME, __func__, hr); TRETURN -1; } return !!rVariant.bVal; } #if 0 int put_oom_dirty (LPDISPATCH pDisp, bool value) { TSTART; /* NOTE: I have found no scenario where this does not return the exception that the property is write protected. But we can never know when we need such an arcane function so I left it in. */ HRESULT hr; DISPID dispid_put = DISPID_PROPERTYPUT; DISPID dispid = DISPID_DIRTY_RAT; DISPPARAMS dispparams; VARIANT aVariant[1]; unsigned int argErr = 0; EXCEPINFO execpinfo; init_excepinfo (&execpinfo); dispparams.rgvarg = aVariant; dispparams.rgvarg[0].vt = VT_BOOL; dispparams.rgvarg[0].boolVal = value? VARIANT_TRUE:VARIANT_FALSE; dispparams.cArgs = 1; dispparams.rgdispidNamedArgs = &dispid_put; dispparams.cNamedArgs = 1; hr = pDisp->Invoke (dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_PROPERTYPUT | DISPATCH_METHOD, &dispparams, NULL, &execpinfo, &argErr); if (hr != S_OK) { log_debug ("%s:%s: error: invoking dirty p=%p vt=%d" " hr=0x%x argErr=0x%x", SRCNAME, __func__, nullptr, 0, (unsigned int)hr, (unsigned int)argErr); dump_excepinfo (execpinfo); TRETURN -1; } TRETURN 0; } #endif /* Get the string property NAME of the object PDISP. Returns NULL if not found or if it is not a string property. */ char * get_oom_string (LPDISPATCH pDisp, const char *name) { TSTART; HRESULT hr; char *result = NULL; DISPID dispid; dispid = lookup_oom_dispid (pDisp, name); if (dispid != DISPID_UNKNOWN) { DISPPARAMS dispparams = {NULL, NULL, 0, 0}; VARIANT rVariant; VariantInit (&rVariant); hr = pDisp->Invoke (dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_PROPERTYGET, &dispparams, &rVariant, NULL, NULL); if (hr != S_OK) log_debug ("%s:%s: Property '%s' not found: %#lx", SRCNAME, __func__, name, hr); else if (rVariant.vt != VT_BSTR) log_debug ("%s:%s: Property `%s' is not a string (vt=%d)", SRCNAME, __func__, name, rVariant.vt); else if (rVariant.bstrVal) result = wchar_to_utf8 (rVariant.bstrVal); VariantClear (&rVariant); } TRETURN result; } std::string get_oom_string_s (LPDISPATCH pDisp, const char *name) { char *ret_c = get_oom_string (pDisp, name); std::string ret; if (ret_c) { ret = ret_c; xfree (ret_c); } return ret; } std::string get_oom_string_s (shared_disp_t pDisp, const char *name) { return get_oom_string_s (pDisp.get (), name); } /* Get the object property NAME of the object PDISP. Returns NULL if not found or if it is not an object perty. */ LPUNKNOWN get_oom_iunknown (LPDISPATCH pDisp, const char *name) { TSTART; HRESULT hr; DISPID dispid; dispid = lookup_oom_dispid (pDisp, name); if (dispid != DISPID_UNKNOWN) { DISPPARAMS dispparams = {NULL, NULL, 0, 0}; VARIANT rVariant; VariantInit (&rVariant); hr = pDisp->Invoke (dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_PROPERTYGET, &dispparams, &rVariant, NULL, NULL); if (hr != S_OK) log_debug ("%s:%s: Property '%s' not found: %#lx", SRCNAME, __func__, name, hr); else if (rVariant.vt != VT_UNKNOWN) log_debug ("%s:%s: Property `%s' is not of class IUnknown (vt=%d)", SRCNAME, __func__, name, rVariant.vt); else { memdbg_addRef (rVariant.punkVal); TRETURN rVariant.punkVal; } VariantClear (&rVariant); } TRETURN NULL; } /* Return the control object described by the tag property with value TAG. The object POBJ must support the FindControl method. Returns NULL if not found. */ LPDISPATCH get_oom_control_bytag (LPDISPATCH pDisp, const char *tag) { TSTART; HRESULT hr; DISPID dispid; DISPPARAMS dispparams; VARIANT aVariant[4]; VARIANT rVariant; BSTR bstring; LPDISPATCH result = NULL; dispid = lookup_oom_dispid (pDisp, "FindControl"); if (dispid == DISPID_UNKNOWN) { log_debug ("%s:%s: Object %p has no FindControl method", SRCNAME, __func__, pDisp); TRETURN NULL; } { wchar_t *tmp = utf8_to_wchar (tag); bstring = tmp? SysAllocString (tmp):NULL; xfree (tmp); if (!bstring) { log_error_w32 (-1, "%s:%s: SysAllocString failed", SRCNAME, __func__); TRETURN NULL; } } dispparams.rgvarg = aVariant; dispparams.rgvarg[0].vt = VT_ERROR; /* Visible */ dispparams.rgvarg[0].scode = DISP_E_PARAMNOTFOUND; dispparams.rgvarg[1].vt = VT_BSTR; /* Tag */ dispparams.rgvarg[1].bstrVal = bstring; dispparams.rgvarg[2].vt = VT_ERROR; /* Id */ dispparams.rgvarg[2].scode = DISP_E_PARAMNOTFOUND; dispparams.rgvarg[3].vt = VT_ERROR;/* Type */ dispparams.rgvarg[3].scode = DISP_E_PARAMNOTFOUND; dispparams.cArgs = 4; dispparams.cNamedArgs = 0; VariantInit (&rVariant); hr = pDisp->Invoke (dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, &dispparams, &rVariant, NULL, NULL); SysFreeString (bstring); if (hr == S_OK && rVariant.vt == VT_DISPATCH && rVariant.pdispVal) { gpgol_queryInterface (rVariant.pdispVal, IID_IDispatch, (LPVOID*)&result); gpgol_release (rVariant.pdispVal); if (!result) log_debug ("%s:%s: Object with tag `%s' has no dispatch intf.", SRCNAME, __func__, tag); } else { log_debug ("%s:%s: No object with tag `%s' found: vt=%d hr=%#lx", SRCNAME, __func__, tag, rVariant.vt, hr); VariantClear (&rVariant); } TRETURN result; } /* 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) { TSTART; HRESULT hr; DISPID dispid; DISPPARAMS dispparams; VARIANT aVariant[5]; VARIANT rVariant; dispid = lookup_oom_dispid (pObj, "Add"); dispparams.rgvarg = aVariant; dispparams.rgvarg[0].vt = VT_BOOL; /* Temporary */ dispparams.rgvarg[0].boolVal = VARIANT_TRUE; dispparams.rgvarg[1].vt = VT_ERROR; /* Before */ dispparams.rgvarg[1].scode = DISP_E_PARAMNOTFOUND; dispparams.rgvarg[2].vt = VT_ERROR; /* Parameter */ dispparams.rgvarg[2].scode = DISP_E_PARAMNOTFOUND; dispparams.rgvarg[3].vt = VT_ERROR; /* Id */ dispparams.rgvarg[3].scode = DISP_E_PARAMNOTFOUND; dispparams.rgvarg[4].vt = VT_INT; /* Type */ dispparams.rgvarg[4].intVal = MSOCONTROLBUTTON; dispparams.cArgs = 5; dispparams.cNamedArgs = 0; VariantInit (&rVariant); hr = pObj->Invoke (dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, &dispparams, &rVariant, NULL, NULL); if (hr != S_OK || rVariant.vt != VT_DISPATCH || !rVariant.pdispVal) { log_error ("%s:%s: Adding Control failed: %#lx - vt=%d", SRCNAME, __func__, hr, rVariant.vt); VariantClear (&rVariant); TRETURN NULL; } TRETURN rVariant.pdispVal; } /* Add a new button to an object which supports the add method. Returns the new object or NULL on error. */ void del_oom_button (LPDISPATCH pObj) { TSTART; HRESULT hr; DISPID dispid; DISPPARAMS dispparams; VARIANT aVariant[5]; dispid = lookup_oom_dispid (pObj, "Delete"); dispparams.rgvarg = aVariant; dispparams.rgvarg[0].vt = VT_BOOL; /* Temporary */ dispparams.rgvarg[0].boolVal = VARIANT_FALSE; dispparams.cArgs = 1; dispparams.cNamedArgs = 0; hr = pObj->Invoke (dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, &dispparams, NULL, NULL, NULL); if (hr != S_OK) log_error ("%s:%s: Deleting Control failed: %#lx", SRCNAME, __func__, hr); TRETURN; } /* Gets the current contexts HWND. Returns NULL on error */ HWND get_oom_context_window (LPDISPATCH context) { TSTART; LPOLEWINDOW actExplorer; HWND ret = NULL; actExplorer = (LPOLEWINDOW) get_oom_object(context, "Application.ActiveExplorer"); if (actExplorer) actExplorer->GetWindow (&ret); else { log_debug ("%s:%s: Could not find active window", SRCNAME, __func__); } gpgol_release (actExplorer); TRETURN ret; } int put_pa_variant (LPDISPATCH pDisp, const char *dasl_id, VARIANT *value) { TSTART; LPDISPATCH propertyAccessor; VARIANT cVariant[2]; VARIANT rVariant; DISPID dispid; DISPPARAMS dispparams; HRESULT hr; EXCEPINFO execpinfo; BSTR b_property; wchar_t *w_property; unsigned int argErr = 0; init_excepinfo (&execpinfo); log_oom ("%s:%s: Looking up property: %s;", SRCNAME, __func__, dasl_id); propertyAccessor = get_oom_object (pDisp, "PropertyAccessor"); if (!propertyAccessor) { log_error ("%s:%s: Failed to look up property accessor.", SRCNAME, __func__); TRETURN -1; } dispid = lookup_oom_dispid (propertyAccessor, "SetProperty"); if (dispid == DISPID_UNKNOWN) { log_error ("%s:%s: could not find SetProperty DISPID", SRCNAME, __func__); TRETURN -1; } /* Prepare the parameter */ w_property = utf8_to_wchar (dasl_id); b_property = SysAllocString (w_property); xfree (w_property); /* Variant 0 carries the data. */ VariantInit (&cVariant[0]); if (VariantCopy (&cVariant[0], value)) { log_error ("%s:%s: Falied to copy value.", SRCNAME, __func__); TRETURN -1; } /* Variant 1 is the DASL as found out by experiments. */ VariantInit (&cVariant[1]); cVariant[1].vt = VT_BSTR; cVariant[1].bstrVal = b_property; dispparams.rgvarg = cVariant; dispparams.cArgs = 2; dispparams.cNamedArgs = 0; VariantInit (&rVariant); hr = propertyAccessor->Invoke (dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, &dispparams, &rVariant, &execpinfo, &argErr); VariantClear (&cVariant[0]); VariantClear (&cVariant[1]); gpgol_release (propertyAccessor); if (hr != S_OK) { log_debug ("%s:%s: failure: invoking SetProperty p=%p vt=%d" " hr=0x%x argErr=0x%x", SRCNAME, __func__, rVariant.pdispVal, rVariant.vt, (unsigned int)hr, (unsigned int)argErr); VariantClear (&rVariant); dump_excepinfo (execpinfo); TRETURN -1; } VariantClear (&rVariant); TRETURN 0; } int put_pa_string (LPDISPATCH pDisp, const char *dasl_id, const char *value) { TSTART; wchar_t *w_value = utf8_to_wchar (value); BSTR b_value = SysAllocString(w_value); xfree (w_value); VARIANT var; VariantInit (&var); var.vt = VT_BSTR; var.bstrVal = b_value; int ret = put_pa_variant (pDisp, dasl_id, &var); VariantClear (&var); TRETURN ret; } int put_pa_int (LPDISPATCH pDisp, const char *dasl_id, int value) { TSTART; VARIANT var; VariantInit (&var); var.vt = VT_I4; var.lVal = value; int ret = put_pa_variant (pDisp, dasl_id, &var); VariantClear (&var); TRETURN ret; } /* Get a MAPI property through OOM using the PropertyAccessor * interface and the DASL Uid. Returns -1 on error. * Variant has to be cleared with VariantClear. * rVariant must be a pointer to a Variant. */ int get_pa_variant (LPDISPATCH pDisp, const char *dasl_id, VARIANT *rVariant) { TSTART; LPDISPATCH propertyAccessor; VARIANT cVariant[1]; DISPID dispid; DISPPARAMS dispparams; HRESULT hr; EXCEPINFO execpinfo; BSTR b_property; wchar_t *w_property; unsigned int argErr = 0; init_excepinfo (&execpinfo); log_oom ("%s:%s: Looking up property: %s;", SRCNAME, __func__, dasl_id); propertyAccessor = get_oom_object (pDisp, "PropertyAccessor"); if (!propertyAccessor) { log_error ("%s:%s: Failed to look up property accessor.", SRCNAME, __func__); TRETURN -1; } dispid = lookup_oom_dispid (propertyAccessor, "GetProperty"); if (dispid == DISPID_UNKNOWN) { log_error ("%s:%s: could not find GetProperty DISPID", SRCNAME, __func__); TRETURN -1; } /* Prepare the parameter */ w_property = utf8_to_wchar (dasl_id); b_property = SysAllocString (w_property); xfree (w_property); cVariant[0].vt = VT_BSTR; cVariant[0].bstrVal = b_property; dispparams.rgvarg = cVariant; dispparams.cArgs = 1; dispparams.cNamedArgs = 0; VariantInit (rVariant); hr = propertyAccessor->Invoke (dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, &dispparams, rVariant, &execpinfo, &argErr); SysFreeString (b_property); gpgol_release (propertyAccessor); if (hr != S_OK && strcmp (GPGOL_UID_DASL, dasl_id)) { /* It often happens that mails don't have a uid by us e.g. if they are not crypto mails or just dont have one. This is not an error. */ log_debug ("%s:%s: error: invoking GetProperty p=%p vt=%d" " hr=0x%x argErr=0x%x", SRCNAME, __func__, rVariant->pdispVal, rVariant->vt, (unsigned int)hr, (unsigned int)argErr); dump_excepinfo (execpinfo); VariantClear (rVariant); TRETURN -1; } TRETURN 0; } /* Get a property string by using the PropertyAccessor of pDisp * Returns NULL on error or a newly allocated result. */ char * get_pa_string (LPDISPATCH pDisp, const char *property) { TSTART; VARIANT rVariant; char *result = NULL; if (get_pa_variant (pDisp, property, &rVariant)) { TRETURN NULL; } if (rVariant.vt == VT_BSTR && rVariant.bstrVal) { result = wchar_to_utf8 (rVariant.bstrVal); } else if (rVariant.vt & VT_ARRAY && !(rVariant.vt & VT_BYREF)) { LONG uBound, lBound; VARTYPE vt; char *data; SafeArrayGetVartype(rVariant.parray, &vt); if (SafeArrayGetUBound (rVariant.parray, 1, &uBound) != S_OK || SafeArrayGetLBound (rVariant.parray, 1, &lBound) != S_OK || vt != VT_UI1) { log_error ("%s:%s: Error: %i", SRCNAME, __func__, __LINE__); VariantClear (&rVariant); TRETURN NULL; } result = (char *)xmalloc (uBound - lBound + 1); data = (char *) rVariant.parray->pvData; memcpy (result, data + lBound, uBound - lBound); result[uBound - lBound] = '\0'; } else { log_debug ("%s:%s: Property `%s' is not a string (vt=%d)", SRCNAME, __func__, property, rVariant.vt); } VariantClear (&rVariant); TRETURN result; } int get_pa_int (LPDISPATCH pDisp, const char *property, int *rInt) { TSTART; VARIANT rVariant; if (get_pa_variant (pDisp, property, &rVariant)) { TRETURN -1; } if (rVariant.vt != VT_I4) { log_debug ("%s:%s: Property `%s' is not a int (vt=%d)", SRCNAME, __func__, property, rVariant.vt); TRETURN -1; } *rInt = rVariant.lVal; VariantClear (&rVariant); TRETURN 0; } /* Helper for exchange address lookup. */ static char * get_recipient_addr_entry_fallbacks_ex (LPDISPATCH addr_entry) { TSTART; /* Maybe check for type here? We are pretty sure that we are exchange */ /* According to MSDN Message Boards the PR_EMS_AB_PROXY_ADDRESSES_DASL is more avilable then the SMTP Address. */ char *ret = get_pa_string (addr_entry, PR_EMS_AB_PROXY_ADDRESSES_DASL); if (ret) { log_debug ("%s:%s: Found recipient through AB_PROXY: %s", SRCNAME, __func__, anonstr (ret)); char *smtpbegin = strstr(ret, "SMTP:"); if (smtpbegin == ret) { ret += 5; } TRETURN ret; } else { log_debug ("%s:%s: Failed AB_PROXY lookup.", SRCNAME, __func__); } LPDISPATCH ex_user = get_oom_object (addr_entry, "GetExchangeUser"); if (!ex_user) { log_debug ("%s:%s: Failed to find ExchangeUser", SRCNAME, __func__); TRETURN nullptr; } ret = get_oom_string (ex_user, "PrimarySmtpAddress"); gpgol_release (ex_user); if (ret) { log_debug ("%s:%s: Found recipient through exchange user primary smtp address: %s", SRCNAME, __func__, anonstr (ret)); TRETURN ret; } TRETURN nullptr; } /* Helper for additional fallbacks in recipient lookup */ static char * get_recipient_addr_fallbacks (LPDISPATCH recipient) { TSTART; if (!recipient) { TRETURN nullptr; } LPDISPATCH addr_entry = get_oom_object (recipient, "AddressEntry"); if (!addr_entry) { log_debug ("%s:%s: Failed to find AddressEntry", SRCNAME, __func__); TRETURN nullptr; } char *ret = get_recipient_addr_entry_fallbacks_ex (addr_entry); gpgol_release (addr_entry); TRETURN ret; } /* Try to resolve a recipient group and add it to the recipients vector. Returns true on success. */ static bool try_resolve_group (LPDISPATCH addrEntry, std::vector >&ret, int recipient_type) { TSTART; /* Get the name for debugging */ std::string name; char *cname = get_oom_string (addrEntry, "Name"); if (cname) { name = cname; } xfree (cname); int user_type = get_oom_int (addrEntry, "AddressEntryUserType"); if (user_type != DISTRIBUTION_LIST_ADDRESS_ENTRY_TYPE) { log_data ("%s:%s: type of %s is %i", SRCNAME, __func__, anonstr (name.c_str()), user_type); TRETURN false; } LPDISPATCH members = get_oom_object (addrEntry, "Members"); addrEntry = nullptr; if (!members) { TRACEPOINT; TRETURN false; } int count = get_oom_int (members, "Count"); if (!count) { TRACEPOINT; gpgol_release (members); TRETURN false; } bool foundOne = false; for (int i = 1; i <= count; i++) { auto item_str = std::string("Item(") + std::to_string (i) + ")"; auto entry = MAKE_SHARED (get_oom_object (members, item_str.c_str())); if (!entry) { TRACEPOINT; continue; } std::string entryName; char *entry_name = get_oom_string (entry.get(), "Name"); if (entry_name) { entryName = entry_name; xfree (entry_name); } int subType = get_oom_int (entry.get(), "AddressEntryUserType"); /* Resolve recursively, yeah fun. */ if (subType == DISTRIBUTION_LIST_ADDRESS_ENTRY_TYPE) { log_debug ("%s:%s: recursive address entry %s", SRCNAME, __func__, anonstr (entryName.c_str())); if (try_resolve_group (entry.get(), ret, recipient_type)) { foundOne = true; continue; } } std::pair element; element.second = entry; /* Resolve directly ? */ char *addrtype = get_pa_string (entry.get(), PR_ADDRTYPE_DASL); if (addrtype && !strcmp (addrtype, "SMTP")) { xfree (addrtype); char *resolved = get_pa_string (entry.get(), PR_EMAIL_ADDRESS_DASL); if (resolved) { element.first = Recipient (resolved, entryName.c_str (), recipient_type); ret.push_back (element); foundOne = true; continue; } } xfree (addrtype); /* Resolve through Exchange API */ char *ex_resolved = get_recipient_addr_entry_fallbacks_ex (entry.get()); if (ex_resolved) { element.first = Recipient (ex_resolved, entryName.c_str (), recipient_type); ret.push_back (element); foundOne = true; continue; } log_debug ("%s:%s: failed to resolve name %s", SRCNAME, __func__, anonstr (entryName.c_str())); } gpgol_release (members); if (!foundOne) { log_debug ("%s:%s: failed to resolve group %s", SRCNAME, __func__, anonstr (name.c_str())); } TRETURN foundOne; } /* Get the recipient mbox addresses with the addrEntry object corresponding to the resolved address. */ std::vector > get_oom_recipients_with_addrEntry (LPDISPATCH recipients, bool *r_err) { TSTART; int recipientsCnt = get_oom_int (recipients, "Count"); std::vector > ret; int i; if (!recipientsCnt) { TRETURN ret; } /* Get the recipients */ for (i = 1; i <= recipientsCnt; i++) { char buf[16]; LPDISPATCH recipient; snprintf (buf, sizeof (buf), "Item(%i)", i); recipient = get_oom_object (recipients, buf); if (!recipient) { /* Should be impossible */ log_error ("%s:%s: could not find Item %i;", SRCNAME, __func__, i); if (r_err) { *r_err = true; } break; } int recipient_type = get_oom_int (recipient, "Type"); std::string entryName; char *entry_name = get_oom_string (recipient, "Name"); if (entry_name) { entryName = entry_name; xfree (entry_name); } auto addrEntry = MAKE_SHARED (get_oom_object (recipient, "AddressEntry")); if (addrEntry && try_resolve_group (addrEntry.get (), ret, recipient_type)) { log_debug ("%s:%s: Resolved recipient group", SRCNAME, __func__); gpgol_release (recipient); continue; } std::pair entry; entry.second = addrEntry; char *resolved = get_pa_string (recipient, PR_SMTP_ADDRESS_DASL); if (resolved) { entry.first = Recipient (resolved, entryName.c_str (), recipient_type); entry.first.setIndex (i); xfree (resolved); gpgol_release (recipient); ret.push_back (entry); continue; } /* No PR_SMTP_ADDRESS first fallback */ resolved = get_recipient_addr_fallbacks (recipient); if (resolved) { entry.first = Recipient (resolved, entryName.c_str (), recipient_type); entry.first.setIndex (i); xfree (resolved); gpgol_release (recipient); ret.push_back (entry); continue; } char *address = get_oom_string (recipient, "Address"); gpgol_release (recipient); log_debug ("%s:%s: Failed to look up Address probably " "EX addr is returned", SRCNAME, __func__); if (address) { entry.first = Recipient (resolved, recipient_type); entry.first.setIndex (i); ret.push_back (entry); xfree (address); } else if (r_err) { *r_err = true; } } TRETURN ret; } /* Gets the resolved smtp addresses of the recpients. */ std::vector get_oom_recipients (LPDISPATCH recipients, bool *r_err) { TSTART; std::vector ret; for (const auto pair: get_oom_recipients_with_addrEntry (recipients, r_err)) { ret.push_back (pair.first); } TRETURN ret; } /* Add an attachment to the outlook dispatcher disp that has an Attachment property. inFile is the path to the attachment. Name is the name that should be used in outlook. */ int add_oom_attachment (LPDISPATCH disp, const wchar_t* inFileW, const wchar_t* displayName, std::string &r_error_str, int *r_err_code) { TSTART; LPDISPATCH attachments = get_oom_object (disp, "Attachments"); DISPID dispid; DISPPARAMS dispparams; VARIANT vtResult; VARIANT aVariant[4]; HRESULT hr; BSTR inFileB = nullptr, dispNameB = nullptr; unsigned int argErr = 0; EXCEPINFO execpinfo; init_excepinfo (&execpinfo); dispid = lookup_oom_dispid (attachments, "Add"); if (dispid == DISPID_UNKNOWN) { log_error ("%s:%s: could not find attachment dispatcher", SRCNAME, __func__); TRETURN -1; } if (inFileW) { inFileB = SysAllocString (inFileW); } if (displayName) { dispNameB = SysAllocString (displayName); } dispparams.rgvarg = aVariant; /* Contrary to the documentation the Source is the last parameter and not the first. Additionally DisplayName is documented but gets ignored by Outlook since Outlook 2003 */ dispparams.rgvarg[0].vt = VT_BSTR; /* DisplayName */ dispparams.rgvarg[0].bstrVal = dispNameB; dispparams.rgvarg[1].vt = VT_INT; /* Position */ dispparams.rgvarg[1].intVal = 1; dispparams.rgvarg[2].vt = VT_INT; /* Type */ dispparams.rgvarg[2].intVal = 1; dispparams.rgvarg[3].vt = VT_BSTR; /* Source */ dispparams.rgvarg[3].bstrVal = inFileB; dispparams.cArgs = 4; dispparams.cNamedArgs = 0; VariantInit (&vtResult); hr = attachments->Invoke (dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, &dispparams, &vtResult, &execpinfo, &argErr); if (hr != S_OK) { log_debug ("%s:%s: error: invoking Add p=%p vt=%d hr=0x%x argErr=0x%x", SRCNAME, __func__, vtResult.pdispVal, vtResult.vt, (unsigned int)hr, (unsigned int)argErr); dump_excepinfo (execpinfo); if (r_err_code) { *r_err_code = (int) execpinfo.scode; } if (execpinfo.bstrDescription) { char *utf8Err = wchar_to_utf8 (execpinfo.bstrDescription); if (utf8Err) { r_error_str = utf8Err; } xfree (utf8Err); } } if (inFileB) SysFreeString (inFileB); if (dispNameB) SysFreeString (dispNameB); VariantClear (&vtResult); gpgol_release (attachments); TRETURN hr == S_OK ? 0 : -1; } LPDISPATCH get_object_by_id (LPDISPATCH pDisp, REFIID id) { TSTART; LPDISPATCH disp = NULL; if (!pDisp) { TRETURN NULL; } if (gpgol_queryInterface(pDisp, id, (void **)&disp) != S_OK) { TRETURN NULL; } TRETURN disp; } LPDISPATCH get_strong_reference (LPDISPATCH mail) { TSTART; VARIANT var; VariantInit (&var); DISPPARAMS args; VARIANT argvars[2]; VariantInit (&argvars[0]); VariantInit (&argvars[1]); argvars[1].vt = VT_DISPATCH; argvars[1].pdispVal = mail; argvars[0].vt = VT_INT; argvars[0].intVal = 1; args.cArgs = 2; args.cNamedArgs = 0; args.rgvarg = argvars; LPDISPATCH ret = NULL; if (!invoke_oom_method_with_parms ( GpgolAddin::get_instance()->get_application(), "GetObjectReference", &var, &args)) { ret = var.pdispVal; log_oom ("%s:%s: Got strong ref %p for %p", SRCNAME, __func__, ret, mail); memdbg_addRef (ret); } else { log_error ("%s:%s: Failed to get strong ref.", SRCNAME, __func__); } VariantClear (&var); TRETURN ret; } LPMESSAGE get_oom_message (LPDISPATCH mailitem) { TSTART; LPUNKNOWN mapi_obj = get_oom_iunknown (mailitem, "MapiObject"); if (!mapi_obj) { log_error ("%s:%s: Failed to obtain MAPI Message.", SRCNAME, __func__); TRETURN NULL; } TRETURN (LPMESSAGE) mapi_obj; } static LPMESSAGE get_oom_base_message_from_mapi (LPDISPATCH mapi_message) { TSTART; HRESULT hr; LPDISPATCH secureItem = NULL; LPMESSAGE message = NULL; LPMAPISECUREMESSAGE secureMessage = NULL; secureItem = get_object_by_id (mapi_message, IID_IMAPISecureMessage); if (!secureItem) { log_error ("%s:%s: Failed to obtain SecureItem.", SRCNAME, __func__); TRETURN NULL; } secureMessage = (LPMAPISECUREMESSAGE) secureItem; /* The call to GetBaseMessage is pretty much a jump in the dark. So it would not be surprising to get crashes here in the future. */ log_oom("%s:%s: About to call GetBaseMessage.", SRCNAME, __func__); hr = secureMessage->GetBaseMessage (&message); memdbg_addRef (message); gpgol_release (secureMessage); if (hr != S_OK) { log_error_w32 (hr, "Failed to GetBaseMessage."); TRETURN NULL; } TRETURN message; } LPMESSAGE get_oom_base_message (LPDISPATCH mailitem) { TSTART; LPMESSAGE mapi_message = get_oom_message (mailitem); LPMESSAGE ret = NULL; if (!mapi_message) { log_error ("%s:%s: Failed to obtain mapi_message.", SRCNAME, __func__); TRETURN NULL; } ret = get_oom_base_message_from_mapi ((LPDISPATCH)mapi_message); gpgol_release (mapi_message); TRETURN ret; } static int invoke_oom_method_with_parms_type (LPDISPATCH pDisp, const char *name, VARIANT *rVariant, DISPPARAMS *params, int type) { TSTART; HRESULT hr; DISPID dispid; dispid = lookup_oom_dispid (pDisp, name); if (dispid != DISPID_UNKNOWN) { EXCEPINFO execpinfo; init_excepinfo (&execpinfo); DISPPARAMS dispparams = {NULL, NULL, 0, 0}; hr = pDisp->Invoke (dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, type, params ? params : &dispparams, rVariant, &execpinfo, NULL); if (hr != S_OK) { log_debug ("%s:%s: Method '%s' invokation failed: %#lx", SRCNAME, __func__, name, hr); dump_excepinfo (execpinfo); TRETURN -1; } } TRETURN 0; } int invoke_oom_method_with_parms (LPDISPATCH pDisp, const char *name, VARIANT *rVariant, DISPPARAMS *params) { TSTART; TRETURN invoke_oom_method_with_parms_type (pDisp, name, rVariant, params, DISPATCH_METHOD); } int invoke_oom_method (LPDISPATCH pDisp, const char *name, VARIANT *rVariant) { TSTART; TRETURN invoke_oom_method_with_parms (pDisp, name, rVariant, NULL); } LPMAPISESSION get_oom_mapi_session () { TSTART; LPDISPATCH application = GpgolAddin::get_instance ()->get_application (); LPDISPATCH oom_session = NULL; LPMAPISESSION session = NULL; LPUNKNOWN mapiobj = NULL; HRESULT hr; if (!application) { log_debug ("%s:%s: Not implemented for Ol < 14", SRCNAME, __func__); TRETURN NULL; } oom_session = get_oom_object (application, "Session"); if (!oom_session) { log_error ("%s:%s: session object not found", SRCNAME, __func__); TRETURN NULL; } mapiobj = get_oom_iunknown (oom_session, "MAPIOBJECT"); gpgol_release (oom_session); if (!mapiobj) { log_error ("%s:%s: error getting Session.MAPIOBJECT", SRCNAME, __func__); TRETURN NULL; } session = NULL; hr = gpgol_queryInterface (mapiobj, IID_IMAPISession, (void**)&session); gpgol_release (mapiobj); if (hr != S_OK || !session) { log_error ("%s:%s: error getting IMAPISession: hr=%#lx", SRCNAME, __func__, hr); TRETURN NULL; } TRETURN session; } int create_category (LPDISPATCH categories, const char *category, int color) { TSTART; VARIANT cVariant[3]; VARIANT rVariant; DISPID dispid; DISPPARAMS dispparams; HRESULT hr; EXCEPINFO execpinfo; BSTR b_name; wchar_t *w_name; unsigned int argErr = 0; init_excepinfo (&execpinfo); if (!categories || !category) { TRACEPOINT; TRETURN 1; } dispid = lookup_oom_dispid (categories, "Add"); if (dispid == DISPID_UNKNOWN) { log_error ("%s:%s: could not find Add DISPID", SRCNAME, __func__); TRETURN -1; } /* Do the string dance */ w_name = utf8_to_wchar (category); b_name = SysAllocString (w_name); xfree (w_name); /* Variants are in reverse order ShortcutKey -> 0 / Int Color -> 1 / Int Name -> 2 / Bstr */ VariantInit (&cVariant[2]); cVariant[2].vt = VT_BSTR; cVariant[2].bstrVal = b_name; VariantInit (&cVariant[1]); cVariant[1].vt = VT_INT; cVariant[1].intVal = color; VariantInit (&cVariant[0]); cVariant[0].vt = VT_INT; cVariant[0].intVal = 0; dispparams.cArgs = 3; dispparams.cNamedArgs = 0; dispparams.rgvarg = cVariant; hr = categories->Invoke (dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, &dispparams, &rVariant, &execpinfo, &argErr); SysFreeString (b_name); VariantClear (&cVariant[0]); VariantClear (&cVariant[1]); VariantClear (&cVariant[2]); if (hr != S_OK) { log_debug ("%s:%s: error: invoking Add p=%p vt=%d" " hr=0x%x argErr=0x%x", SRCNAME, __func__, rVariant.pdispVal, rVariant.vt, (unsigned int)hr, (unsigned int)argErr); dump_excepinfo (execpinfo); VariantClear (&rVariant); TRETURN -1; } VariantClear (&rVariant); log_oom ("%s:%s: Created category '%s'", SRCNAME, __func__, anonstr (category)); TRETURN 0; } LPDISPATCH get_store_for_id (const char *storeID) { TSTART; LPDISPATCH application = GpgolAddin::get_instance ()->get_application (); if (!application || !storeID) { TRACEPOINT; TRETURN nullptr; } LPDISPATCH stores = get_oom_object (application, "Session.Stores"); if (!stores) { log_error ("%s:%s: No stores found.", SRCNAME, __func__); TRETURN nullptr; } auto store_count = get_oom_int (stores, "Count"); for (int n = 1; n <= store_count; n++) { const auto store_str = std::string("Item(") + std::to_string(n) + ")"; LPDISPATCH store = get_oom_object (stores, store_str.c_str()); if (!store) { TRACEPOINT; continue; } char *id = get_oom_string (store, "StoreID"); if (id && !strcmp (id, storeID)) { gpgol_release (stores); xfree (id); return store; } xfree (id); gpgol_release (store); } gpgol_release (stores); TRETURN nullptr; } void ensure_category_exists (const char *category, int color) { TSTART; LPDISPATCH application = GpgolAddin::get_instance ()->get_application (); if (!application || !category) { TRACEPOINT; TRETURN; } log_oom ("%s:%s: Ensure category exists called for %s, %i", SRCNAME, __func__, category, color); LPDISPATCH stores = get_oom_object (application, "Session.Stores"); if (!stores) { log_error ("%s:%s: No stores found.", SRCNAME, __func__); TRETURN; } auto store_count = get_oom_int (stores, "Count"); for (int n = 1; n <= store_count; n++) { const auto store_str = std::string("Item(") + std::to_string(n) + ")"; LPDISPATCH store = get_oom_object (stores, store_str.c_str()); if (!store) { TRACEPOINT; continue; } LPDISPATCH categories = get_oom_object (store, "Categories"); gpgol_release (store); if (!categories) { categories = get_oom_object (application, "Session.Categories"); if (!categories) { TRACEPOINT; continue; } } auto count = get_oom_int (categories, "Count"); bool found = false; for (int i = 1; i <= count && !found; i++) { const auto item_str = std::string("Item(") + std::to_string(i) + ")"; LPDISPATCH category_obj = get_oom_object (categories, item_str.c_str()); if (!category_obj) { TRACEPOINT; gpgol_release (categories); break; } char *name = get_oom_string (category_obj, "Name"); if (name && !strcmp (category, name)) { log_oom ("%s:%s: Found category '%s'", SRCNAME, __func__, name); found = true; } /* We don't check the color here as the user may change that. */ gpgol_release (category_obj); xfree (name); } if (!found) { if (create_category (categories, category, color)) { log_oom ("%s:%s: Found category '%s'", SRCNAME, __func__, category); } } /* Otherwise we have to create the category */ gpgol_release (categories); } gpgol_release (stores); TRETURN; } int add_category (LPDISPATCH mail, const char *category) { TSTART; char *tmp = get_oom_string (mail, "Categories"); if (!tmp) { TRACEPOINT; TRETURN 1; } if (strstr (tmp, category)) { log_oom ("%s:%s: category '%s' already added.", SRCNAME, __func__, category); TRETURN 0; } std::string newstr (tmp); xfree (tmp); if (!newstr.empty ()) { newstr += CategoryManager::getSeperator () + std::string (" "); } newstr += category; TRETURN put_oom_string (mail, "Categories", newstr.c_str ()); } int remove_category (LPDISPATCH mail, const char *category, bool exactMatch) { TSTART; char *tmp = get_oom_string (mail, "Categories"); if (!tmp) { TRACEPOINT; TRETURN 1; } std::vector categories; std::istringstream f(tmp); std::string s; const std::string sep = CategoryManager::getSeperator(); while (std::getline(f, s, *(sep.c_str()))) { ltrim(s); categories.push_back(s); } xfree (tmp); const std::string categoryStr = category; categories.erase (std::remove_if (categories.begin(), categories.end(), [categoryStr, exactMatch] (const std::string &cat) { if (exactMatch) { return cat == categoryStr; } return cat.compare (0, categoryStr.size(), categoryStr) == 0; }), categories.end ()); std::string newCategories; std::string newsep = sep + " "; join (categories, newsep.c_str (), newCategories); TRETURN put_oom_string (mail, "Categories", newCategories.c_str ()); } static int _delete_category (LPDISPATCH categories, int idx) { TSTART; VARIANT aVariant[1]; DISPPARAMS dispparams; dispparams.rgvarg = aVariant; dispparams.rgvarg[0].vt = VT_INT; dispparams.rgvarg[0].intVal = idx; dispparams.cArgs = 1; dispparams.cNamedArgs = 0; TRETURN invoke_oom_method_with_parms (categories, "Remove", NULL, &dispparams); } int delete_category (LPDISPATCH store, const char *category) { TSTART; if (!store || !category) { TRETURN -1; } LPDISPATCH categories = get_oom_object (store, "Categories"); if (!categories) { categories = get_oom_object ( GpgolAddin::get_instance ()->get_application (), "Session.Categories"); if (!categories) { TRACEPOINT; TRETURN -1; } } auto count = get_oom_int (categories, "Count"); int ret = 0; for (int i = 1; i <= count; i++) { const auto item_str = std::string("Item(") + std::to_string(i) + ")"; LPDISPATCH category_obj = get_oom_object (categories, item_str.c_str()); if (!category_obj) { TRACEPOINT; gpgol_release (categories); break; } char *name = get_oom_string (category_obj, "Name"); gpgol_release (category_obj); if (name && !strcmp (category, name)) { if ((ret = _delete_category (categories, i))) { log_error ("%s:%s: Failed to delete category '%s'", SRCNAME, __func__, anonstr (category)); } else { log_debug ("%s:%s: Deleted category '%s'", SRCNAME, __func__, anonstr (category)); } xfree (name); break; } xfree (name); } gpgol_release (categories); TRETURN ret; } void delete_all_categories_starting_with (const char *string) { LPDISPATCH application = GpgolAddin::get_instance ()->get_application (); if (!application || !string) { TRACEPOINT; TRETURN; } log_oom ("%s:%s: Delete categories starting with: \"%s\"", SRCNAME, __func__, string); LPDISPATCH stores = get_oom_object (application, "Session.Stores"); if (!stores) { log_error ("%s:%s: No stores found.", SRCNAME, __func__); TRETURN; } auto store_count = get_oom_int (stores, "Count"); for (int n = 1; n <= store_count; n++) { const auto store_str = std::string("Item(") + std::to_string(n) + ")"; LPDISPATCH store = get_oom_object (stores, store_str.c_str()); if (!store) { TRACEPOINT; continue; } LPDISPATCH categories = get_oom_object (store, "Categories"); if (!categories) { categories = get_oom_object (application, "Session.Categories"); if (!categories) { TRACEPOINT; gpgol_release (store); continue; } } auto count = get_oom_int (categories, "Count"); std::vector to_delete; for (int i = 1; i <= count; i++) { const auto item_str = std::string("Item(") + std::to_string(i) + ")"; LPDISPATCH category_obj = get_oom_object (categories, item_str.c_str()); if (!category_obj) { TRACEPOINT; gpgol_release (categories); break; } char *name = get_oom_string (category_obj, "Name"); if (name && !strncmp (string, name, strlen (string))) { log_oom ("%s:%s: Found category for deletion '%s'", SRCNAME, __func__, anonstr(name)); to_delete.push_back (name); } /* We don't check the color here as the user may change that. */ gpgol_release (category_obj); xfree (name); } /* Do this one after another to avoid messing with indexes. */ for (const auto &str: to_delete) { delete_category (store, str.c_str ()); } gpgol_release (store); /* Otherwise we have to create the category */ gpgol_release (categories); } gpgol_release (stores); TRETURN; } static char * generate_uid () { TSTART; UUID uuid; UuidCreate (&uuid); unsigned char *str; UuidToStringA (&uuid, &str); char *ret = xstrdup ((char*)str); RpcStringFreeA (&str); TRETURN ret; } char * get_unique_id (LPDISPATCH mail, int create, const char *uuid) { TSTART; if (!mail) { TRETURN NULL; } /* Get the User Properties. */ if (!create) { char *uid = get_pa_string (mail, GPGOL_UID_DASL); if (!uid) { log_debug ("%s:%s: No uuid found in oom for '%p'", SRCNAME, __func__, mail); TRETURN NULL; } else { log_debug ("%s:%s: Found uid '%s' for '%p'", SRCNAME, __func__, uid, mail); TRETURN uid; } } char *newuid; if (!uuid) { newuid = generate_uid (); } else { newuid = xstrdup (uuid); } int ret = put_pa_string (mail, GPGOL_UID_DASL, newuid); if (ret) { log_debug ("%s:%s: failed to set uid '%s' for '%p'", SRCNAME, __func__, newuid, mail); xfree (newuid); TRETURN NULL; } log_debug ("%s:%s: '%p' has now the uid: '%s' ", SRCNAME, __func__, mail, newuid); TRETURN newuid; } HWND get_active_hwnd () { TSTART; LPDISPATCH app = GpgolAddin::get_instance ()->get_application (); if (!app) { TRACEPOINT; TRETURN nullptr; } LPDISPATCH activeWindow = get_oom_object (app, "ActiveWindow"); if (!activeWindow) { activeWindow = get_oom_object (app, "ActiveInspector"); if (!activeWindow) { activeWindow = get_oom_object (app, "ActiveExplorer"); if (!activeWindow) { TRACEPOINT; TRETURN nullptr; } } } /* Both explorer and inspector have this. */ char *caption = get_oom_string (activeWindow, "Caption"); gpgol_release (activeWindow); if (!caption) { TRACEPOINT; TRETURN nullptr; } /* Might not be completly true for multiple explorers on the same folder but good enugh. */ HWND hwnd = FindWindowExA(NULL, NULL, "rctrl_renwnd32", caption); xfree (caption); TRETURN hwnd; } LPDISPATCH create_mail () { TSTART; LPDISPATCH app = GpgolAddin::get_instance ()->get_application (); if (!app) { TRACEPOINT; TRETURN nullptr; } VARIANT var; VariantInit (&var); VARIANT argvars[1]; DISPPARAMS args; VariantInit (&argvars[0]); argvars[0].vt = VT_I2; argvars[0].intVal = 0; args.cArgs = 1; args.cNamedArgs = 0; args.rgvarg = argvars; LPDISPATCH ret = nullptr; if (invoke_oom_method_with_parms (app, "CreateItem", &var, &args)) { log_error ("%s:%s: Failed to create mailitem.", SRCNAME, __func__); TRETURN ret; } ret = var.pdispVal; TRETURN ret; } LPDISPATCH get_account_for_mail (const char *mbox) { TSTART; LPDISPATCH app = GpgolAddin::get_instance ()->get_application (); if (!app) { TRACEPOINT; TRETURN nullptr; } LPDISPATCH accounts = get_oom_object (app, "Session.Accounts"); if (!accounts) { TRACEPOINT; TRETURN nullptr; } int count = get_oom_int (accounts, "Count"); for (int i = 1; i <= count; i++) { std::string item = std::string ("Item(") + std::to_string (i) + ")"; LPDISPATCH account = get_oom_object (accounts, item.c_str ()); if (!account) { TRACEPOINT; continue; } char *smtpAddr = get_oom_string (account, "SmtpAddress"); if (!smtpAddr) { gpgol_release (account); TRACEPOINT; continue; } if (!stricmp (mbox, smtpAddr)) { gpgol_release (accounts); xfree (smtpAddr); TRETURN account; } gpgol_release (account); xfree (smtpAddr); } gpgol_release (accounts); log_error ("%s:%s: Failed to find account for '%s'.", SRCNAME, __func__, anonstr (mbox)); TRETURN nullptr; } char * get_sender_SendUsingAccount (LPDISPATCH mailitem, bool *r_is_GSuite) { TSTART; LPDISPATCH sender = get_oom_object (mailitem, "SendUsingAccount"); if (!sender) { TRETURN nullptr; } char *buf = get_oom_string (sender, "SmtpAddress"); char *dispName = get_oom_string (sender, "DisplayName"); gpgol_release (sender); /* Check for G Suite account */ if (dispName && !strcmp ("G Suite", dispName) && r_is_GSuite) { *r_is_GSuite = true; } xfree (dispName); if (buf && strlen (buf)) { log_debug ("%s:%s: found sender", SRCNAME, __func__); TRETURN buf; } xfree (buf); TRETURN nullptr; } char * get_sender_Sender (LPDISPATCH mailitem) { TSTART; LPDISPATCH sender = get_oom_object (mailitem, "Sender"); if (!sender) { TRETURN nullptr; } char *buf = get_pa_string (sender, PR_SMTP_ADDRESS_DASL); gpgol_release (sender); if (buf && strlen (buf)) { log_debug ("%s:%s Sender fallback 2", SRCNAME, __func__); TRETURN buf; } xfree (buf); /* We have a sender object but not yet an smtp address likely exchange. Try some more propertys of the message. */ buf = get_pa_string (mailitem, PR_TAG_SENDER_SMTP_ADDRESS); if (buf && strlen (buf)) { log_debug ("%s:%s Sender fallback 3", SRCNAME, __func__); TRETURN buf; } xfree (buf); buf = get_pa_string (mailitem, PR_TAG_RECEIVED_REPRESENTING_SMTP_ADDRESS); if (buf && strlen (buf)) { log_debug ("%s:%s Sender fallback 4", SRCNAME, __func__); TRETURN buf; } xfree (buf); TRETURN nullptr; } char * get_sender_CurrentUser (LPDISPATCH mailitem) { TSTART; LPDISPATCH sender = get_oom_object (mailitem, "Session.CurrentUser"); if (!sender) { TRETURN nullptr; } char *buf = get_pa_string (sender, PR_SMTP_ADDRESS_DASL); gpgol_release (sender); if (buf && strlen (buf)) { log_debug ("%s:%s Sender fallback 5", SRCNAME, __func__); TRETURN buf; } xfree (buf); TRETURN nullptr; } char * get_sender_SenderEMailAddress (LPDISPATCH mailitem) { TSTART; char *type = get_oom_string (mailitem, "SenderEmailType"); if (type && !strcmp ("SMTP", type)) { char *senderMail = get_oom_string (mailitem, "SenderEmailAddress"); if (senderMail) { log_debug ("%s:%s: Sender found", SRCNAME, __func__); xfree (type); TRETURN senderMail; } } xfree (type); TRETURN nullptr; } char * get_sender_SentRepresentingAddress (LPDISPATCH mailitem) { TSTART; char *buf = get_pa_string (mailitem, PR_SENT_REPRESENTING_EMAIL_ADDRESS_W_DASL); if (buf && strlen (buf)) { log_debug ("%s:%s Found sent representing address \"%s\"", SRCNAME, __func__, anonstr (buf)); TRETURN buf; } xfree (buf); TRETURN nullptr; } char * get_inline_body () { TSTART; LPDISPATCH app = GpgolAddin::get_instance ()->get_application (); if (!app) { TRACEPOINT; TRETURN nullptr; } LPDISPATCH explorer = get_oom_object (app, "ActiveExplorer"); if (!explorer) { TRACEPOINT; TRETURN nullptr; } LPDISPATCH inlineResponse = get_oom_object (explorer, "ActiveInlineResponse"); gpgol_release (explorer); if (!inlineResponse) { TRETURN nullptr; } char *body = get_oom_string (inlineResponse, "Body"); gpgol_release (inlineResponse); TRETURN body; } int get_ex_major_version_for_addr (const char *mbox) { TSTART; LPDISPATCH account = get_account_for_mail (mbox); if (!account) { TRACEPOINT; TRETURN -1; } char *version_str = get_oom_string (account, "ExchangeMailboxServerVersion"); gpgol_release (account); if (!version_str) { TRETURN -1; } log_debug ("%s:%s: Detected exchange major version: %s", SRCNAME, __func__, version_str); long int version = strtol (version_str, nullptr, 10); xfree (version_str); TRETURN (int) version; } int get_ol_ui_language () { TSTART; LPDISPATCH app = GpgolAddin::get_instance()->get_application(); if (!app) { TRACEPOINT; TRETURN 0; } LPDISPATCH langSettings = get_oom_object (app, "LanguageSettings"); if (!langSettings) { TRACEPOINT; TRETURN 0; } VARIANT var; VariantInit (&var); VARIANT aVariant[1]; DISPPARAMS dispparams; dispparams.rgvarg = aVariant; dispparams.rgvarg[0].vt = VT_INT; dispparams.rgvarg[0].intVal = 2; dispparams.cArgs = 1; dispparams.cNamedArgs = 0; int ret = invoke_oom_method_with_parms_type (langSettings, "LanguageID", &var, &dispparams, DISPATCH_PROPERTYGET); gpgol_release (langSettings); if (ret) { TRACEPOINT; TRETURN 0; } if (var.vt != VT_INT && var.vt != VT_I4) { TRACEPOINT; TRETURN 0; } int result = var.intVal; VariantClear (&var); TRETURN result; } void log_addins () { TSTART; LPDISPATCH app = GpgolAddin::get_instance ()->get_application (); if (!app) { TRACEPOINT; TRETURN; } LPDISPATCH addins = get_oom_object (app, "COMAddins"); if (!addins) { TRACEPOINT; TRETURN; } std::string activeAddins; int count = get_oom_int (addins, "Count"); for (int i = 1; i <= count; i++) { VARIANT aVariant[1]; VARIANT rVariant; VariantInit (&rVariant); DISPPARAMS dispparams; dispparams.rgvarg = aVariant; dispparams.rgvarg[0].vt = VT_INT; dispparams.rgvarg[0].intVal = i; dispparams.cArgs = 1; dispparams.cNamedArgs = 0; /* We need this instead of get_oom_object item(1) as usual becase the item method accepts a string or an int. String would be the ProgID and int is just the index. So Fun. */ if (invoke_oom_method_with_parms_type (addins, "Item", &rVariant, &dispparams, DISPATCH_METHOD | DISPATCH_PROPERTYGET)) { log_error ("%s:%s: Failed to invoke item func.", SRCNAME, __func__); continue; } if (rVariant.vt != (VT_DISPATCH)) { log_error ("%s:%s: Invalid ret val", SRCNAME, __func__); continue; } LPDISPATCH addin = rVariant.pdispVal; if (!addin) { TRACEPOINT; continue; } memdbg_addRef (addin); bool connected = get_oom_bool (addin, "Connect"); if (!connected) { gpgol_release (addin); continue; } char *progId = get_oom_string (addin, "ProgId"); gpgol_release (addin); if (!progId) { TRACEPOINT; continue; } activeAddins += std::string (progId) + "\n"; xfree (progId); } gpgol_release (addins); log_debug ("%s:%s:Active Addins:\n%s", SRCNAME, __func__, activeAddins.c_str ()); TRETURN; } bool is_preview_pane_visible (LPDISPATCH explorer) { TSTART; if (!explorer) { TRACEPOINT; TRETURN false; } VARIANT var; VariantInit (&var); VARIANT argvars[1]; DISPPARAMS args; VariantInit (&argvars[0]); argvars[0].vt = VT_INT; argvars[0].intVal = 3; args.cArgs = 1; args.cNamedArgs = 0; args.rgvarg = argvars; if (invoke_oom_method_with_parms (explorer, "IsPaneVisible", &var, &args)) { log_error ("%s:%s: Failed to check visibilty.", SRCNAME, __func__); TRETURN false; } if (var.vt != VT_BOOL) { TRACEPOINT; TRETURN false; } TRETURN !!var.boolVal; } static LPDISPATCH add_user_prop (LPDISPATCH user_props, const char *name) { TSTART; if (!user_props || !name) { TRACEPOINT; TRETURN nullptr; } wchar_t *w_name = utf8_to_wchar (name); BSTR b_name = SysAllocString (w_name); xfree (w_name); /* Args: 0: DisplayFormat int OlUserPropertyType 1: AddToFolderFields Bool Should the filed be added to the folder. 2: Type int OlUserPropertyType Type of the field. 3: Name Bstr Name of the field. Returns the added Property. */ VARIANT var; VariantInit (&var); DISPPARAMS args; VARIANT argvars[4]; VariantInit (&argvars[0]); VariantInit (&argvars[1]); VariantInit (&argvars[2]); VariantInit (&argvars[3]); argvars[0].vt = VT_INT; argvars[0].intVal = 1; // 1 means text. argvars[1].vt = VT_BOOL; argvars[1].boolVal = VARIANT_FALSE; argvars[2].vt = VT_INT; argvars[2].intVal = 1; argvars[3].vt = VT_BSTR; argvars[3].bstrVal = b_name; args.cArgs = 4; args.cNamedArgs = 0; args.rgvarg = argvars; int res = invoke_oom_method_with_parms (user_props, "Add", &var, &args); VariantClear (&argvars[0]); VariantClear (&argvars[1]); VariantClear (&argvars[2]); VariantClear (&argvars[3]); if (res) { log_oom ("%s:%s: Failed to add property %s.", SRCNAME, __func__, name); TRETURN nullptr; } if (var.vt != VT_DISPATCH) { TRACEPOINT; TRETURN nullptr; } LPDISPATCH ret = var.pdispVal; memdbg_addRef (ret); TRETURN ret; } LPDISPATCH find_user_prop (LPDISPATCH user_props, const char *name) { TSTART; if (!user_props || !name) { TRACEPOINT; TRETURN nullptr; } VARIANT var; VariantInit (&var); wchar_t *w_name = utf8_to_wchar (name); BSTR b_name = SysAllocString (w_name); xfree (w_name); /* Name -> 1 / Bstr Custom 0 -> Bool True for search in custom properties. False for builtin properties. */ DISPPARAMS args; VARIANT argvars[2]; VariantInit (&argvars[0]); VariantInit (&argvars[1]); argvars[1].vt = VT_BSTR; argvars[1].bstrVal = b_name; argvars[0].vt = VT_BOOL; argvars[0].boolVal = VARIANT_TRUE; args.cArgs = 2; args.cNamedArgs = 0; args.rgvarg = argvars; int res = invoke_oom_method_with_parms (user_props, "Find", &var, &args); VariantClear (&argvars[0]); VariantClear (&argvars[1]); if (res) { log_oom ("%s:%s: Failed to find property %s.", SRCNAME, __func__, name); TRETURN nullptr; } if (var.vt != VT_DISPATCH) { TRACEPOINT; TRETURN nullptr; } LPDISPATCH ret = var.pdispVal; memdbg_addRef (ret); TRETURN ret; } LPDISPATCH find_or_add_text_prop (LPDISPATCH user_props, const char *name) { TSTART; LPDISPATCH ret = find_user_prop (user_props, name); if (ret) { TRETURN ret; } ret = add_user_prop (user_props, name); TRETURN ret; } void release_disp (LPDISPATCH obj) { TSTART; gpgol_release (obj); TRETURN; } enum FolderID { olFolderCalendar = 9, olFolderConflicts = 19, olFolderContacts = 10, olFolderDeletedItems = 3, olFolderDrafts = 16, olFolderInbox = 6, olFolderJournal = 11, olFolderJunk = 23, olFolderLocalFailures = 21, olFolderManagedEmail = 29, olFolderNotes = 12, olFolderOutbox = 4, olFolderSentMail = 5, olFolderServerFailures = 22, olFolderSuggestedContacts = 30, olFolderSyncIssues = 20, olFolderTasks = 13, olFolderToDo = 28, olPublicFoldersAllPublicFolders = 18, olFolderRssFeeds = 25, }; static bool is_mail_in_folder (LPDISPATCH mailitem, int folder) { TSTART; if (!mailitem) { STRANGEPOINT; TRETURN false; } auto store = MAKE_SHARED (get_oom_object (mailitem, "Parent.Store")); if (!store) { log_debug ("%s:%s: Mail has no parent folder. Probably unsafed", SRCNAME, __func__); TRETURN false; } std::string tmp = std::string("GetDefaultFolder(") + std::to_string (folder) + std::string(")"); auto target_folder = MAKE_SHARED (get_oom_object (store.get(), tmp.c_str())); if (!target_folder) { STRANGEPOINT; TRETURN false; } auto mail_folder = MAKE_SHARED (get_oom_object (mailitem, "Parent")); if (!mail_folder) { STRANGEPOINT; TRETURN false; } char *target_id = get_oom_string (target_folder.get(), "entryID"); if (!target_id) { STRANGEPOINT; TRETURN false; } char *folder_id = get_oom_string (mail_folder.get(), "entryID"); if (!folder_id) { STRANGEPOINT; free (target_id); TRETURN false; } bool ret = !strcmp (target_id, folder_id); free (target_id); free (folder_id); TRETURN ret; } bool is_junk_mail (LPDISPATCH mailitem) { TSTART; TRETURN is_mail_in_folder (mailitem, FolderID::olFolderJunk); } bool is_draft_mail (LPDISPATCH mailitem) { TSTART; TRETURN is_mail_in_folder (mailitem, FolderID::olFolderDrafts); } void format_variant (std::stringstream &stream, VARIANT* var) { if (!var) { stream << " (null) "; } stream << "VT: " << std::hex << var->vt << " Value: "; VARTYPE vt = var->vt; if (vt == VT_BOOL) { stream << (var->boolVal == VARIANT_FALSE ? "false" : "true"); } else if (vt == (VT_BOOL | VT_BYREF)) { stream << (*(var->pboolVal) == VARIANT_FALSE ? "false" : "true"); } else if (vt == VT_BSTR) { char *buf = wchar_to_utf8 (var->bstrVal); stream << "BStr: " << buf; xfree (buf); } else if (vt == VT_INT || vt == VT_I4) { stream << var->intVal; } else if (vt == VT_DISPATCH) { char *buf = get_object_name ((LPUNKNOWN) var->pdispVal); stream << "IDispatch: " << buf; xfree (buf); } else if (vt == (VT_VARIANT | VT_BYREF)) { format_variant (stream, var->pvarVal); } else { stream << "?"; } stream << std::endl; } std::string format_dispparams (DISPPARAMS *p) { if (!p) { return "(null)"; } std::stringstream stream; stream << "Count: " << p->cArgs << " CNamed: " << p->cNamedArgs << std::endl; for (int i = 0; i < p->cArgs; i++) { format_variant (stream, p->rgvarg + i); } return stream.str (); } int count_visible_attachments (LPDISPATCH attachments) { int ret = 0; if (!attachments) { return 0; } int att_count = get_oom_int (attachments, "Count"); for (int i = 1; i <= att_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)) { /* SECURITY: Testing has shown that for all mail types GpgOL handles that might contain an unsigned attachment we always have the MAPIOBJECT / get the hidden state. Only the transient MIME attachments. The ones used for the MAPI to MIME conversion and which are hidden by GpgOL and handled by GpgOL will have no MAPIOBJECT when a mail is opened from file. So this will remove the warning that "smime.p7m" or "gpgol_mime_structure.txt" are unsigned and unencrypted attachments. */ log_dbg ("Failed to get hidden state."); LPUNKNOWN mapiobj = get_oom_iunknown (oom_attach, "MAPIOBJECT"); if (!mapiobj) { const auto dispName = get_oom_string_s (oom_attach, "DisplayName"); log_dbg ("Attachment: %s has no mapiobject. Ignoring it.", anonstr (dispName.c_str ())); } else { gpgol_release (mapiobj); const auto dispName = get_oom_string_s (oom_attach, "DisplayName"); log_dbg ("Attachment %s without hidden state but mapiobj. " "Count as visible.", anonstr (dispName.c_str ())); ret++; } } else if (var.vt == VT_BOOL && var.boolVal == VARIANT_FALSE) { ret++; } gpgol_release (oom_attach); VariantClear (&var); } return ret; } int invoke_oom_method_with_int (LPDISPATCH pDisp, const char *name, int arg, VARIANT *rVariant) { TSTART; DISPPARAMS parms; VARIANT argvars[1]; VariantInit (&argvars[0]); argvars[0].vt = VT_INT; argvars[0].intVal = arg; parms.cArgs = 1; parms.cNamedArgs = 0; parms.rgvarg = argvars; TRETURN invoke_oom_method_with_parms (pDisp, name, rVariant, &parms); } int invoke_oom_method_with_string (LPDISPATCH pDisp, const char *name, const char *arg, VARIANT *rVariant) { TSTART; if (!arg) { TRETURN 0; } wchar_t *warg = utf8_to_wchar (arg); if (!warg) { TRETURN 1; } VARIANT aVariant[1]; VariantInit (&aVariant[0]); aVariant[0].vt = VT_BSTR; aVariant[0].bstrVal = SysAllocString (warg); xfree (warg); DISPPARAMS dispparams; dispparams.rgvarg = aVariant; dispparams.cArgs = 1; dispparams.cNamedArgs = 0; int ret = invoke_oom_method_with_parms (pDisp, name, rVariant, &dispparams); VariantClear(&aVariant[0]); TRETURN ret; } int set_oom_recipients (LPDISPATCH item, const std::vector &recps) { if (!item) { STRANGEPOINT; TRETURN -1; } auto oom_recps = MAKE_SHARED (get_oom_object (item, "Recipients")); if (!oom_recps) { STRANGEPOINT; TRETURN -1; } /* First clear out the current recipients. */ int ret = invoke_oom_method (oom_recps.get (), "RemoveAll", nullptr); if (ret) { STRANGEPOINT; TRETURN ret; } for (const auto &recp: recps) { if (recp.type() == Recipient::olOriginator) { /* Skip the originator, we only add it internally but it does not need to be in OOM. */ continue; } VARIANT result; VariantInit (&result); ret = invoke_oom_method_with_string (oom_recps.get (), "Add", recp.mbox ().c_str (), &result); if (ret) { log_err ("Failed to add recipient."); TRETURN ret; } if (result.vt != VT_DISPATCH || !result.pdispVal) { log_err ("No recipient result."); continue; } if (put_oom_int (result.pdispVal, "Type", recp.type())) { log_err ("Failed to set recipient type."); } /* This releases the recipient. */ VariantClear (&result); } TRETURN 0; } int remove_oom_recipient (LPDISPATCH item, const std::string &mbox) { TSTART; if (!item) { STRANGEPOINT; TRETURN -1; } auto oom_recps = MAKE_SHARED (get_oom_object (item, "Recipients")); if (!oom_recps) { STRANGEPOINT; TRETURN -1; } bool r_err = false; const auto recps = get_oom_recipients (oom_recps.get (), &r_err); if (r_err) { log_debug ("Failure to lookup recipients via OOM"); TRETURN -1; } for (const auto &recp: recps) { if (recp.mbox () == mbox && recp.index () != -1) { TRETURN invoke_oom_method_with_int (oom_recps.get (), "Remove", recp.index (), nullptr); } } TRETURN -1; } void oom_dump_idispatch (LPDISPATCH obj) { log_dbg ("Start infos about %p", obj); if (!obj) { log_dbg ("It's NULL"); return; } log_dbg ("Name: '%s'", get_object_name_s (obj).c_str ()); LPTYPEINFO typeinfo = nullptr; HRESULT hr = obj->GetTypeInfo (0, 0, &typeinfo); if (!typeinfo || FAILED (hr)) { log_dbg ("No typeinfo."); return; } TYPEATTR* pta = NULL; hr = typeinfo->GetTypeAttr(&pta); if (!pta || FAILED (hr)) { log_dbg ("No type attr"); return; } /* First the IID to have it clear */ LPOLESTR lpsz = NULL; hr = StringFromIID(pta->guid, &lpsz); if(FAILED (hr)) { hr = StringFromCLSID (pta->guid, &lpsz); } if(SUCCEEDED (hr)) { log_dbg ("Interface: %S", lpsz); CoTaskMemFree(lpsz); } FUNCDESC *pfd = nullptr; /* Lets see what functions we have. */ for(int i = 0; i < pta->cFuncs; i++) { typeinfo->GetFuncDesc(i, &pfd); BSTR names[1]; unsigned int dumb; typeinfo->GetNames(pfd->memid, names, 1, &dumb); if (!names[0]) { typeinfo->ReleaseFuncDesc(pfd); continue; } log_dbg ("%i: %S id=0x%li With %d param(s)\n", i, (names[0]), pfd->memid, pfd->cParams); typeinfo->ReleaseFuncDesc(pfd); SysFreeString(names[0]); } typeinfo->ReleaseTypeAttr(pta); /* Now for some interesting object relations that many oom objecs have. */ const char * relations[] = { "Parent", "GetInspector", "Session", "Sender", nullptr }; for (int i = 0; relations [i]; i++) { LPDISPATCH rel = get_oom_object (obj, relations[i]); log_dbg ("%s: %s", relations [i], get_object_name_s (rel).c_str ()); gpgol_release (rel); } /* Now for some interesting string values. */ const char * stringVals[] = { "EntryID", "Subject", "MessageClass", "Body", nullptr }; for (int i = 0; stringVals[i]; i++) { const auto str = get_oom_string_s (obj, stringVals[i]); log_dbg ("%s: %s", stringVals[i], str.c_str ()); } log_dbg ("Object dump done"); return; } int get_oom_crypto_flags (LPDISPATCH mailitem) { TSTART; int r_val = 0; int err = get_pa_int (mailitem, PR_SECURITY_FLAGS_DASL, &r_val); if (err) { log_dbg ("Failed to get security flags."); TRETURN 0; } TRETURN r_val; } shared_disp_t show_folder_select () { TSTART; VARIANT var; VariantInit (&var); LPDISPATCH rVal; auto namespace_obj = get_oom_object_s (oApp (), "Session"); if (!namespace_obj) { STRANGEPOINT; TRETURN nullptr; } if (!invoke_oom_method (namespace_obj.get (), "PickFolder", &var)) { if (!(var.vt & VT_DISPATCH)) { log_dbg ("Failed to get disp obj. No folder selected?"); TRETURN nullptr; } rVal = var.pdispVal; log_oom ("%s:%s: Got folder ref %p", SRCNAME, __func__, rVal); memdbg_addRef (rVal); TRETURN MAKE_SHARED (rVal); } log_dbg ("No folder returned."); TRETURN nullptr; } -LPDISPATCH oApp () +LPDISPATCH +oApp () { return GpgolAddin::get_instance()->get_application(); } + +BSTR utf8_to_bstr (const char *string) +{ + wchar_t *tmp = utf8_to_wchar (string); + BSTR bstring = tmp ? SysAllocString (tmp) : NULL; + xfree (tmp); + if (!bstring) + { + log_error_w32 (-1, "%s:%s: SysAllocString failed", SRCNAME, __func__); + TRETURN nullptr; + } + TRETURN bstring; +} + +int +oom_save_as (LPDISPATCH obj, const char *path, oomSaveAsType type) +{ + if (!obj || !path) + { + /* invalid arguments */ + STRANGEPOINT; + TRETURN -1; + } + /* Params are first path and then type as optional. With + COM Marshalling this means that param 1 is the path + and 0 is the type. */ + VARIANT aVariant[2]; + VariantInit(aVariant); + VariantInit(aVariant + 1); + + DISPPARAMS dispparams; + dispparams.rgvarg = aVariant; + dispparams.rgvarg[0].vt = VT_INT; + dispparams.rgvarg[0].intVal = (int) type; + dispparams.rgvarg[1].vt = VT_BSTR; + dispparams.rgvarg[1].bstrVal = utf8_to_bstr (path); + dispparams.cArgs = 2; + dispparams.cNamedArgs = 0; + + int rc = invoke_oom_method_with_parms (obj, "SaveAs", nullptr, &dispparams); + + if (rc) + { + log_err ("Failed to call SaveAs"); + } + VariantClear(aVariant); + VariantClear(aVariant + 1); + + return rc; +} + +int +oom_save_as_file (LPDISPATCH obj, const char *path) +{ + if (!obj || !path) + { + /* invalid arguments */ + STRANGEPOINT; + TRETURN -1; + } + VARIANT aVariant[1]; + VariantInit(aVariant); + + DISPPARAMS dispparams; + dispparams.rgvarg = aVariant; + dispparams.rgvarg[0].vt = VT_BSTR; + dispparams.rgvarg[0].bstrVal = utf8_to_bstr (path); + dispparams.cArgs = 1; + dispparams.cNamedArgs = 0; + + int rc = invoke_oom_method_with_parms (obj, "SaveAsFile", + nullptr, &dispparams); + + if (rc) + { + log_err ("Failed to call SaveAsFile"); + } + VariantClear(aVariant); + + return rc; +} diff --git a/src/oomhelp.h b/src/oomhelp.h index 7766787..4ab6ea9 100644 --- a/src/oomhelp.h +++ b/src/oomhelp.h @@ -1,505 +1,531 @@ /* 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 "common.h" #include #include #include #define MSOCONTROLBUTTON 1 #define MSOCONTROLEDIT 2 #define MSOCONTROLDROPDOWN 3 #define MSOCONTROLCOMBOBOX 4 #define MSOCONTROLPOPUP 10 class Recipient; 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_FolderEvents, 0x000630F7, 0x0000, 0x0000, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); DEFINE_GUID(IID_ApplicationEvents, 0x0006304E, 0x0000, 0x0000, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46); DEFINE_GUID(IID_ApplicationEvents_11, 0x0006302C, 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 #ifndef PR_ADDRTYPE_DASL #define PR_ADDRTYPE_DASL \ "http://schemas.microsoft.com/mapi/proptag/0x3002001E" #endif #ifndef PR_EMAIL_ADDRESS_DASL #define PR_EMAIL_ADDRESS_DASL \ "http://schemas.microsoft.com/mapi/proptag/0x3003001E" #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_ATTACH_FLAGS_DASL \ "http://schemas.microsoft.com/mapi/proptag/0x37140003" #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" #define PR_BLOCK_STATUS_DASL \ "http://schemas.microsoft.com/mapi/proptag/0x10960003" #define PR_SENT_REPRESENTING_EMAIL_ADDRESS_W_DASL \ "http://schemas.microsoft.com/mapi/proptag/0x0065001F" #define PR_SENDER_NAME_W_DASL \ "http://schemas.microsoft.com/mapi/proptag/0x0C1A001F" #define PR_SENT_REPRESENTING_NAME_W_DASL \ "http://schemas.microsoft.com/mapi/proptag/0x0042001F" #define PR_SECURITY_FLAGS_DASL \ "http://schemas.microsoft.com/mapi/proptag/0x6E010003" #define DISTRIBUTION_LIST_ADDRESS_ENTRY_TYPE 11 #define DISPID_DIRTY_RAT 0xF024 typedef std::shared_ptr shared_disp_t; /* Function to contain the gpgol_release macro */ void release_disp (LPDISPATCH obj); #define MAKE_SHARED(X) shared_disp_t ((LPDISPATCH)X, &release_disp) /* Return the malloced name of an COM+ object. */ char *get_object_name (LPUNKNOWN obj); std::string get_object_name_s (LPUNKNOWN obj); std::string get_object_name_s (shared_disp_t 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); /* Do the same but with a shared disp return value */ shared_disp_t get_oom_object_s (LPDISPATCH pStart, const char *fullname); shared_disp_t get_oom_object_s (shared_disp_t 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); /* Set the byte array property NAME to VALUE. */ int put_oom_array (LPDISPATCH pDisp, const char *name, unsigned char *value, size_t size); /* 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); int get_oom_int (shared_disp_t pDisp, const char *name); /* Get the string property NAME of the object PDISP. */ char *get_oom_string (LPDISPATCH pDisp, const char *name); std::string get_oom_string_s (LPDISPATCH pDisp, const char *name); std::string get_oom_string_s (shared_disp_t 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. The second part of the pair returned is the recipient type which corresponds in value to Mail::recipientType. If r_err is not null it is set to true in case of an error. */ std::vector get_oom_recipients (LPDISPATCH recipients, bool *r_err = nullptr); /* Same as above but include the AddrEntry object in the result. Caller needs to release the AddrEntry. */ std::vector > get_oom_recipients_with_addrEntry (LPDISPATCH recipients, bool *r_err = nullptr); /* Add an attachment to a dispatcher */ int add_oom_attachment (LPDISPATCH disp, const wchar_t* inFile, const wchar_t *displayName, std::string &r_err_str, int *r_err_code); /* 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); /* Same as invoke oom method but do the string marshalling for arg */ int invoke_oom_method_with_string (LPDISPATCH pDisp, const char *name, const char *arg, VARIANT *rVariant = nullptr); /* Same as invoke oom method but with a single int argument */ int invoke_oom_method_with_int (LPDISPATCH pDisp, const char *name, int arg, VARIANT *rVariant = nullptr); /* 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. Creates the category with the specified color if required. returns 0 on success. */ void ensure_category_exists (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, bool exactMatch); /* Create the category */ int create_category (LPDISPATCH categories, const char *category, int color); /* Delete a category from the store. */ int delete_category (LPDISPATCH store, const char *category); /* Delete categories starting with "string" from all stores. */ void delete_all_categories_starting_with (const char *string); /* Iterate over application stores and return the one with the ID @storeID */ LPDISPATCH get_store_for_id (const char *storeID); /* 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); /* Print all active addins to log */ void log_addins (void); /* 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); /* Get the major version of the exchange server of the account for the mail address "mbox". Returns -1 if no version could be detected or exchange is not used.*/ int get_ex_major_version_for_addr (const char *mbox); /* Get the language code used for Outlooks UI */ int get_ol_ui_language (void); char *get_sender_SendUsingAccount (LPDISPATCH mailitem, bool *r_is_GSuite); /* Get the SentRepresentingAddress */ char *get_sender_SentRepresentingAddress (LPDISPATCH mailitem); /* memtracing query interface */ HRESULT gpgol_queryInterface (LPUNKNOWN pObj, REFIID riid, LPVOID FAR *ppvObj); HRESULT gpgol_openProperty (LPMAPIPROP obj, ULONG ulPropTag, LPCIID lpiid, ULONG ulInterfaceOptions, ULONG ulFlags, LPUNKNOWN FAR * lppUnk); /* Check if the preview pane in the explorer is visible */ bool is_preview_pane_visible (LPDISPATCH explorer); /* Find or add a text user property with that name. */ LPDISPATCH find_or_add_text_prop (LPDISPATCH props, const char *name); /* Find a user property and return it if found. */ LPDISPATCH find_user_prop (LPDISPATCH props, const char *name); /* Return true if this message is in the junk folder for this account */ bool is_junk_mail (LPDISPATCH mailitem); /* Return true if this message is in the draft folder for this account */ bool is_draft_mail (LPDISPATCH mailitem); /* Returns info about a dispparms variable for debugging. */ void format_variant (std::istringstream &stream, VARIANT *var); std::string format_dispparams (DISPPARAMS *p); /* Returns the count of attachments that are not hidden. */ int count_visible_attachments (LPDISPATCH attachments); /* Remove a recipient from the OOM. */ int remove_oom_recipient (LPDISPATCH item, const std::string &mbox); /* Remove all recipients and replace them with the list of our objects. */ int set_oom_recipients (LPDISPATCH item, const std::vector &recps); /* Print some introspective infos about the object. */ void oom_dump_idispatch (LPDISPATCH obj); /* Get the hidden dirty property of the object. */ int get_oom_dirty (LPDISPATCH pDisp); /* Get the Outlook crypto flags indicating in Outlook if the mail should be encrypted with S/MIME. */ int get_oom_crypto_flags (LPDISPATCH mailitem); /* Show a folder picker dialog and return a folder disp or NULL */ shared_disp_t show_folder_select (); /* Return the main Application object. Like qApp ;-) */ LPDISPATCH oApp (); /* Setter for dirty - Returns an error that the property is write protected on mails. But maybe we can use it somewhere else. Code is ifdeffed out. int put_oom_dirty (LPDISPATCH pDisp, bool val); */ + + + +/* Invoke ->SaveAs to a path, path is expected to be + in utf-8 returns 0 on success. */ +enum oomSaveAsType +{ + olDoc = 4, /* Microsoft Office Word format (.doc) */ + olHTML = 5, /* HTML format (.html) */ + olICal = 8, /* iCal format (.ics) */ + olMHTML = 10, /* MIME HTML format (.mht) */ + olMSG = 3, /* Outlook message format (.msg) */ + olMSGUnicode = 9, /* Outlook Unicode message format (.msg) */ + olRTF = 1, /* Rich Text format (.rtf) */ + olTemplate = 2, /* Microsoft Outlook template (.oft) */ + olTXT = 0, /* Text format (.txt) */ + olVCal = 7, /* VCal format (.vcs) */ + olVCard = 6, /* VCard format (.vcf) */ +}; +int oom_save_as (LPDISPATCH obj, const char *path, + oomSaveAsType type = olMSG); +int oom_save_as_file (LPDISPATCH obj, const char *path); + +/* Convert a utf8 value to a bstr allocated with SysAllocString. + Call SysFreeString on the allocated value. VariantClear on + a variant with VT_BSTR does this for you. */ +BSTR utf8_to_bstr (const char *val); #endif /*OOMHELP_H*/ diff --git a/src/windowmessages.cpp b/src/windowmessages.cpp index ec98e34..ec6abef 100644 --- a/src/windowmessages.cpp +++ b/src/windowmessages.cpp @@ -1,769 +1,769 @@ /* @file windowmessages.h * @brief Helper class to work with the windowmessage handler thread. * * Copyright (C) 2015, 2016 by Bundesamt für Sicherheit in der Informationstechnik * Software engineering by Intevation GmbH * * This file is part of GpgOL. * * GpgOL is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * GpgOL is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, see . */ #include "windowmessages.h" #include "common.h" #include "oomhelp.h" #include "mail.h" #include "gpgoladdin.h" #include "wks-helper.h" #include "addressbook.h" #include #define RESPONDER_CLASS_NAME "GpgOLResponder" /* Singleton window */ static HWND g_responder_window = NULL; static int invalidation_blocked = 0; LONG_PTR WINAPI gpgol_window_proc (HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { // log_debug ("WMG: %x", (unsigned int) message); if (message == WM_USER + 42) { TSTART; if (!lParam) { log_error ("%s:%s: Recieved user msg without lparam", SRCNAME, __func__); TRETURN DefWindowProc(hWnd, message, wParam, lParam); } wm_ctx_t *ctx = (wm_ctx_t *) lParam; log_debug ("%s:%s: Recieved user msg: %i", SRCNAME, __func__, ctx->wmsg_type); switch (ctx->wmsg_type) { case (PARSING_DONE): { auto mail = (Mail*) ctx->data; if (!Mail::isValidPtr (mail)) { log_debug ("%s:%s: Parsing done for mail which is gone.", SRCNAME, __func__); TBREAK; } mail->parsingDone_o (); TBREAK; } case (RECIPIENT_ADDED): { auto mail = (Mail*) ctx->data; if (!Mail::isValidPtr (mail)) { log_debug ("%s:%s: Recipient add for mail which is gone.", SRCNAME, __func__); TBREAK; } mail->locateKeys_o (); TBREAK; } case (REVERT_MAIL): { auto mail = (Mail*) ctx->data; if (!Mail::isValidPtr (mail)) { log_debug ("%s:%s: Revert mail for mail which is gone.", SRCNAME, __func__); TBREAK; } mail->setNeedsSave (true); /* Some magic here. Accessing any existing inline body cements it. Otherwise updating the body through the revert also changes the body of a inline mail. */ char *inlineBody = get_inline_body (); xfree (inlineBody); // Does the revert. log_debug ("%s:%s: Revert mail. Invoking save.", SRCNAME, __func__); invoke_oom_method (mail->item (), "Save", NULL); log_debug ("%s:%s: Revert mail. Save done. Updating body..", SRCNAME, __func__); mail->updateBody_o (false); log_debug ("%s:%s: Revert mail done.", SRCNAME, __func__); TBREAK; } case (INVALIDATE_UI): { if (!invalidation_blocked) { log_debug ("%s:%s: Invalidating UI", SRCNAME, __func__); gpgoladdin_invalidate_ui(); log_debug ("%s:%s: Invalidation done", SRCNAME, __func__); } else { log_debug ("%s:%s: Received invalidation msg while blocked." " Ignoring it", SRCNAME, __func__); } TBREAK; } case (INVALIDATE_LAST_MAIL): { log_debug ("%s:%s: clearing last mail", SRCNAME, __func__); Mail::clearLastMail (); TBREAK; } case (CLOSE): { auto mail = (Mail*) ctx->data; if (!Mail::isValidPtr (mail)) { log_debug ("%s:%s: Close for mail which is gone.", SRCNAME, __func__); TBREAK; } mail->refCurrentItem(); Mail::closeInspector_o (mail); TRACEPOINT; mail->close (); log_debug ("%s:%s: Close finished.", SRCNAME, __func__); mail->releaseCurrentItem(); TBREAK; } case (CRYPTO_DONE): { auto mail = (Mail*) ctx->data; if (!Mail::isValidPtr (mail)) { log_debug ("%s:%s: Crypto done for mail which is gone.", SRCNAME, __func__); TBREAK; } if (mail->isDraftEncrypt()) { mail->updateCryptOOM_o (); invoke_oom_method (mail->item (), "Save", NULL); log_debug ("%s:%s: Second save done for %p finished draft " "encryption.", SRCNAME, __func__, mail); mail->setIsDraftEncrypt (false); - mail->setCryptState (Mail::NoCryptMail); + mail->setCryptState (Mail::NotStarted); mail->releaseCurrentItem (); TBREAK; } // modify the mail. - if (mail->cryptState () == Mail::NeedsUpdateInOOM) + if (mail->cryptState () == Mail::BackendDone) { // Save the Mail log_debug ("%s:%s: Crypto done for %p updating oom.", SRCNAME, __func__, mail); mail->updateCryptOOM_o (); } - if (mail->cryptState () == Mail::NeedsSecondAfterWrite) + if (mail->cryptState () == Mail::OOMUpdated) { invoke_oom_method (mail->item (), "Save", NULL); log_debug ("%s:%s: Second save done for %p Invoking second send.", SRCNAME, __func__, mail); } // Finaly this should pass. if (invoke_oom_method (mail->item (), "Send", NULL)) { log_error ("%s:%s: Send failed for %p. " "Trying SubmitMessage instead.\n" "This will likely crash.", SRCNAME, __func__, mail); /* What we do here is similar to the T3656 workaround in mailitem-events.cpp. In our tests this works but is unstable. So we only use it as a very very last resort. */ auto mail_message = get_oom_base_message (mail->item()); if (!mail_message) { gpgol_bug (mail->getWindow (), ERR_GET_BASE_MSG_FAILED); TBREAK; } // It's important we use the _base_ message here. mapi_save_changes (mail_message, FORCE_SAVE); HRESULT hr = mail_message->SubmitMessage(0); gpgol_release (mail_message); if (hr == S_OK) { do_in_ui_thread_async (CLOSE, (LPVOID) mail); } else { log_error ("%s:%s: SubmitMessage Failed hr=0x%lx.", SRCNAME, __func__, hr); gpgol_bug (mail->getWindow (), ERR_SEND_FALLBACK_FAILED); } } else { mail->releaseCurrentItem (); } log_debug ("%s:%s: Send for %p completed.", SRCNAME, __func__, mail); TBREAK; } case (BRING_TO_FRONT): { HWND wnd = get_active_hwnd (); if (wnd) { log_debug ("%s:%s: Bringing window %p to front.", SRCNAME, __func__, wnd); bring_to_front (wnd); } else { log_debug ("%s:%s: No active window found for bring to front.", SRCNAME, __func__); } TBREAK; } case (WKS_NOTIFY): { WKSHelper::instance ()->notify ((const char *) ctx->data); xfree (ctx->data); TBREAK; } case (CLEAR_REPLY_FORWARD): { auto mail = (Mail*) ctx->data; if (!Mail::isValidPtr (mail)) { log_debug ("%s:%s: Clear reply forward for mail which is gone.", SRCNAME, __func__); TBREAK; } mail->wipe_o (true); mail->removeAllAttachments_o (); TBREAK; } case (DO_AUTO_SECURE): { auto mail = (Mail*) ctx->data; if (!Mail::isValidPtr (mail)) { log_debug ("%s:%s: DO_AUTO_SECURE for mail which is gone.", SRCNAME, __func__); TBREAK; } mail->setDoAutosecure_m (true); TBREAK; } case (DONT_AUTO_SECURE): { auto mail = (Mail*) ctx->data; if (!Mail::isValidPtr (mail)) { log_debug ("%s:%s: DO_AUTO_SECURE for mail which is gone.", SRCNAME, __func__); TBREAK; } mail->setDoAutosecure_m (false); TBREAK; } case (CONFIG_KEY_DONE): { log_debug ("%s:%s: Key configuration done.", SRCNAME, __func__); Addressbook::update_key_o (ctx->data); TBREAK; } case (DECRYPT): { auto mail = (Mail*) ctx->data; if (!Mail::isValidPtr (mail)) { log_debug ("%s:%s: DECRYPT for mail which is gone.", SRCNAME, __func__); TBREAK; } log_debug ("%s:%s: Decrypting %p again.", SRCNAME, __func__, mail); mail->preProcessMessage_m (); mail->decryptVerify_o (); TBREAK; } case (AFTER_MOVE): { /* What happens here is that after moving an S/MIME mail we restore the message class to the value it was before the close that initiated the move. See where this message is sent in folder events for more details. */ auto data = (wm_after_move_data_t *) ctx->data; log_debug ("%s:%s: AfterMove.", SRCNAME, __func__); /* Try to get the message object that was moved. */ LPMESSAGE mapi_message = nullptr; ULONG utype = 0; HRESULT hr; hr = data->target_folder->OpenEntry (data->entry_id_len, (LPENTRYID) data->entry_id, &IID_IMessage, MAPI_BEST_ACCESS, &utype, (IUnknown **) &mapi_message); if (FAILED (hr) || !mapi_message) { log_error ("%s:%s: Failed to open mail in target folder: hr=0x%lx", SRCNAME, __func__, hr); gpgol_release (data->target_folder); xfree (data->entry_id); xfree (data); TBREAK; } memdbg_addRef (mapi_message); log_debug ("%s:%s: Restoring message class after move to: %s", SRCNAME, __func__, data->old_class); mapi_set_mesage_class (mapi_message, data->old_class); hr = mapi_message->SaveChanges (FORCE_SAVE); if (FAILED (hr)) { log_error ("%s:%s: Failed to save mail in target folder: hr=0x%lx", SRCNAME, __func__, hr); } gpgol_release (mapi_message); gpgol_release (data->target_folder); xfree (data->old_class); xfree (data->entry_id); xfree (data); TBREAK; } case SEND_MULTIPLE_MAILS: { auto mail = (Mail*) ctx->data; if (!Mail::isValidPtr (mail)) { log_debug ("%s:%s: SEND_MULTIPLE_MAILS for mail which is gone.", SRCNAME, __func__); TBREAK; } log_debug ("%s:%s: Send multiple mails for %p.", SRCNAME, __func__, mail); mail->splitAndSend_o (); TBREAK; } case SEND: { auto mail = (Mail*) ctx->data; if (!Mail::isValidPtr (mail)) { log_debug ("%s:%s: SEND for mail %p which is gone.", SRCNAME, __func__, mail); TBREAK; } log_debug ("%s:%s: Send for %p.", SRCNAME, __func__, mail); invoke_oom_method (mail->item (), "Send", nullptr); TBREAK; } case SHOW_PREVIEW: { auto mail = (Mail*) ctx->data; if (!Mail::isValidPtr (mail)) { log_dbg ("SHOW_PREVIEW for mail %p which is gone.", mail); TBREAK; } log_dbg ("SHOW_PREVIEW for %p", mail); mail->parsingDone_o (true); TBREAK; } default: log_debug ("%s:%s: Unknown msg %x", SRCNAME, __func__, ctx->wmsg_type); } TRETURN 0; } else if (message == WM_USER) { switch (wParam) { case (EXT_API_CLOSE_ALL): { log_debug ("%s:%s: Closing all mails.", SRCNAME, __func__); Mail::closeAllMails_o (); TRETURN 0; } default: log_debug ("%s:%s: Unknown external msg %i", SRCNAME, __func__, (int) wParam); } } else if (message == WM_COPYDATA) { log_debug ("%s:%s Received copydata message.", SRCNAME, __func__); if (!lParam) { STRANGEPOINT; TRETURN DefWindowProc(hWnd, message, wParam, lParam); } const COPYDATASTRUCT *cds = (COPYDATASTRUCT*)lParam; if (cds->dwData == EXT_API_CLOSE) { log_debug ("%s:%s CopyData with external close. Payload: %.*s", SRCNAME, __func__, (int)cds->cbData, (char *) cds->lpData); std::string uid ((char*) cds->lpData, cds->cbData); auto mail = Mail::getMailForUUID (uid.c_str ()); if (!mail) { log_error ("%s:%s Failed to find mail for: %s", SRCNAME, __func__, uid.c_str() ); TRETURN DefWindowProc(hWnd, message, wParam, lParam); } mail->refCurrentItem (); Mail::closeInspector_o (mail); TRACEPOINT; mail->close (); log_debug ("%s:%s: Close for %p uid: %s finished.", SRCNAME, __func__, mail, uid.c_str ()); mail->releaseCurrentItem(); TRETURN 0; } else if (cds->dwData == EXT_API_DECRYPT) { log_debug ("%s:%s CopyData with external decrypt. Payload: %.*s", SRCNAME, __func__, (int)cds->cbData, (char *) cds->lpData); std::string uid ((char*) cds->lpData, cds->cbData); auto mail = Mail::getMailForUUID (uid.c_str ()); if (!mail) { log_error ("%s:%s Failed to find mail for: %s", SRCNAME, __func__, uid.c_str() ); TRETURN DefWindowProc(hWnd, message, wParam, lParam); } log_debug ("%s:%s: Decrypting %p again.", SRCNAME, __func__, mail); mail->preProcessMessage_m (); mail->decryptVerify_o (); TRETURN 0; } } return DefWindowProc(hWnd, message, wParam, lParam); } HWND create_responder_window () { TSTART; size_t cls_name_len = strlen(RESPONDER_CLASS_NAME) + 1; char cls_name[cls_name_len]; if (g_responder_window) { TRETURN g_responder_window; } /* Create Window wants a mutable string as the first parameter */ snprintf (cls_name, cls_name_len, "%s", RESPONDER_CLASS_NAME); WNDCLASS windowClass; windowClass.style = CS_GLOBALCLASS | CS_DBLCLKS; windowClass.lpfnWndProc = gpgol_window_proc; windowClass.cbClsExtra = 0; windowClass.cbWndExtra = 0; windowClass.hInstance = (HINSTANCE) GetModuleHandle(NULL); windowClass.hIcon = 0; windowClass.hCursor = 0; windowClass.hbrBackground = 0; windowClass.lpszMenuName = 0; windowClass.lpszClassName = cls_name; RegisterClass(&windowClass); g_responder_window = CreateWindow (cls_name, RESPONDER_CLASS_NAME, 0, 0, 0, 0, 0, 0, (HMENU) 0, (HINSTANCE) GetModuleHandle(NULL), 0); TRETURN g_responder_window; } static int send_msg_to_ui_thread (wm_ctx_t *ctx) { TSTART; size_t cls_name_len = strlen(RESPONDER_CLASS_NAME) + 1; char cls_name[cls_name_len]; snprintf (cls_name, cls_name_len, "%s", RESPONDER_CLASS_NAME); HWND responder = FindWindow (cls_name, RESPONDER_CLASS_NAME); if (!responder) { log_error ("%s:%s: Failed to find responder window.", SRCNAME, __func__); TRETURN -1; } SendMessage (responder, WM_USER + 42, 0, (LPARAM) ctx); TRETURN 0; } int do_in_ui_thread (gpgol_wmsg_type type, void *data) { TSTART; wm_ctx_t ctx = {NULL, UNKNOWN, 0, 0}; ctx.wmsg_type = type; ctx.data = data; log_debug ("%s:%s: Sending message of type %i", SRCNAME, __func__, type); if (send_msg_to_ui_thread (&ctx)) { TRETURN -1; } TRETURN ctx.err; } static DWORD WINAPI do_async (LPVOID arg) { TSTART; wm_ctx_t *ctx = (wm_ctx_t*) arg; log_debug ("%s:%s: Do async with type %i after %i ms", SRCNAME, __func__, ctx ? ctx->wmsg_type : -1, ctx->delay); if (ctx->delay) { Sleep (ctx->delay); } send_msg_to_ui_thread (ctx); xfree (ctx); TRETURN 0; } void do_in_ui_thread_async (gpgol_wmsg_type type, void *data, int delay) { TSTART; wm_ctx_t *ctx = (wm_ctx_t *) xcalloc (1, sizeof (wm_ctx_t)); ctx->wmsg_type = type; ctx->data = data; ctx->delay = delay; CloseHandle (CreateThread (NULL, 0, do_async, (LPVOID) ctx, 0, NULL)); TRETURN; } LRESULT CALLBACK gpgol_hook(int code, WPARAM wParam, LPARAM lParam) { /* Once we are in the close events we don't have enough control to revert all our changes so we have to do it with this nice little hack by catching the WM_CLOSE message before it reaches outlook. */ LPCWPSTRUCT cwp = (LPCWPSTRUCT) lParam; /* What we do here is that we catch all WM_CLOSE messages that get to Outlook. Then we check if the last open Explorer is the target of the close. In set case we start our shutdown routine before we pass the WM_CLOSE to outlook */ switch (cwp->message) { case WM_CLOSE: { HWND lastChild = NULL; log_debug ("%s:%s: Got WM_CLOSE", SRCNAME, __func__); if (!GpgolAddin::get_instance() || !GpgolAddin::get_instance ()->get_application()) { TRACEPOINT; TBREAK; } LPDISPATCH explorers = get_oom_object (GpgolAddin::get_instance ()->get_application(), "Explorers"); if (!explorers) { log_error ("%s:%s: No explorers object", SRCNAME, __func__); TBREAK; } int count = get_oom_int (explorers, "Count"); if (count != 1) { log_debug ("%s:%s: More then one explorer. Not shutting down.", SRCNAME, __func__); gpgol_release (explorers); TBREAK; } LPDISPATCH explorer = get_oom_object (explorers, "Item(1)"); gpgol_release (explorers); if (!explorer) { TRACEPOINT; TBREAK; } /* Casting to LPOLEWINDOW and calling GetWindow succeeded in Outlook 2016 but always TRETURNed the number 1. So we need this hack. */ char *caption = get_oom_string (explorer, "Caption"); gpgol_release (explorer); if (!caption) { log_debug ("%s:%s: No caption.", SRCNAME, __func__); TBREAK; } /* rctrl_renwnd32 is the window class of outlook. */ HWND hwnd = FindWindowExA(NULL, lastChild, "rctrl_renwnd32", caption); xfree (caption); lastChild = hwnd; if (hwnd == cwp->hwnd) { log_debug ("%s:%s: WM_CLOSE windowmessage for explorer. " "Shutting down.", SRCNAME, __func__); GpgolAddin::get_instance ()->shutdown(); TBREAK; } TBREAK; } case WM_SYSCOMMAND: /* This comes to often and when we are closed from the icon we also get WM_CLOSE if (cwp->wParam == SC_CLOSE) { log_debug ("%s:%s: SC_CLOSE syscommand. Closing all mails.", SRCNAME, __func__); GpgolAddin::get_instance ()->shutdown(); } */ break; default: // log_debug ("WM: %x", (unsigned int) cwp->message); break; } return CallNextHookEx (NULL, code, wParam, lParam); } /* Create the message hook for outlook's windowmessages we are especially interested in WM_QUIT to do cleanups and prevent the "Item has changed" question. */ HHOOK create_message_hook() { TSTART; TRETURN SetWindowsHookEx (WH_CALLWNDPROC, gpgol_hook, NULL, GetCurrentThreadId()); } GPGRT_LOCK_DEFINE (invalidate_lock); static bool invalidation_in_progress; DWORD WINAPI delayed_invalidate_ui (LPVOID minsleep) { TSTART; if (invalidation_in_progress) { log_debug ("%s:%s: Invalidation canceled as it is in progress.", SRCNAME, __func__); TRETURN 0; } TRACEPOINT; invalidation_in_progress = true; gpgol_lock(&invalidate_lock); int sleep_ms = (intptr_t)minsleep; Sleep (sleep_ms); int i = 0; while (invalidation_blocked) { i++; if (i % 10 == 0) { log_debug ("%s:%s: Waiting for invalidation.", SRCNAME, __func__); } Sleep (100); /* Do we need an abort statement here? */ } do_in_ui_thread (INVALIDATE_UI, nullptr); TRACEPOINT; invalidation_in_progress = false; gpgol_unlock(&invalidate_lock); TRETURN 0; } DWORD WINAPI close_mail (LPVOID mail) { TSTART; do_in_ui_thread (CLOSE, mail); TRETURN 0; } void blockInv() { TSTART; invalidation_blocked++; log_oom ("%s:%s: Invalidation block count %i", SRCNAME, __func__, invalidation_blocked); TRETURN; } void unblockInv() { TSTART; invalidation_blocked--; log_oom ("%s:%s: Invalidation block count %i", SRCNAME, __func__, invalidation_blocked); if (invalidation_blocked < 0) { log_error ("%s:%s: Invalidation block mismatch", SRCNAME, __func__); invalidation_blocked = 0; } TRETURN; } diff --git a/src/wks-helper.cpp b/src/wks-helper.cpp index e6dd369..322a6a9 100644 --- a/src/wks-helper.cpp +++ b/src/wks-helper.cpp @@ -1,868 +1,868 @@ /* wks-helper.cpp - Web Key Services for GpgOL * 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 "wks-helper.h" #include "common.h" #include "cpphelp.h" #include "oomhelp.h" #include "windowmessages.h" #include "mail.h" #include "mapihelp.h" #include "recipient.h" #include #include #include #include #include #include #include #include #define CHECK_MIN_INTERVAL (60 * 60 * 24 * 7) #define WKS_REG_KEY "webkey" #undef _ #define _(a) utf8_gettext (a) static std::map s_states; static std::map s_last_checked; static std::map > s_confirmation_cache; static WKSHelper* singleton = nullptr; GPGRT_LOCK_DEFINE (wks_lock); WKSHelper::WKSHelper() { load (); } WKSHelper::~WKSHelper () { // Ensure that we are not destroyed while // worker is running. gpgol_lock (&wks_lock); gpgol_unlock (&wks_lock); } const WKSHelper* WKSHelper::instance () { if (!singleton) { singleton = new WKSHelper (); } return singleton; } WKSHelper::WKSState WKSHelper::get_state (const std::string &mbox) const { gpgol_lock (&wks_lock); const auto it = s_states.find(mbox); const auto dataEnd = s_states.end(); gpgol_unlock (&wks_lock); if (it == dataEnd) { return NotChecked; } return it->second; } time_t WKSHelper::get_check_time (const std::string &mbox) const { gpgol_lock (&wks_lock); const auto it = s_last_checked.find(mbox); const auto dataEnd = s_last_checked.end(); gpgol_unlock (&wks_lock); if (it == dataEnd) { return 0; } return it->second; } std::pair WKSHelper::get_cached_confirmation (const std::string &mbox) const { gpgol_lock (&wks_lock); const auto it = s_confirmation_cache.find(mbox); const auto dataEnd = s_confirmation_cache.end(); if (it == dataEnd) { gpgol_unlock (&wks_lock); return std::make_pair (nullptr, nullptr); } auto ret = it->second; s_confirmation_cache.erase (it); gpgol_unlock (&wks_lock); return ret; } static std::string get_wks_client_path () { char *gpg4win_dir = get_gpg4win_dir (); if (!gpg4win_dir) { TRACEPOINT; return std::string (); } const auto ret = std::string (gpg4win_dir) + "\\..\\GnuPG\\bin\\gpg-wks-client.exe"; xfree (gpg4win_dir); if (!access (ret.c_str (), F_OK)) { return ret; } log_debug ("%s:%s: Failed to find wks-client in '%s'", SRCNAME, __func__, ret.c_str ()); return std::string (); } static bool check_published (const std::string &mbox) { const auto wksPath = get_wks_client_path (); if (wksPath.empty()) { return 0; } std::vector args; args.push_back (wksPath); args.push_back (std::string ("--status-fd")); args.push_back (std::string ("1")); args.push_back (std::string ("--check")); args.push_back (mbox); // Spawn the process auto ctx = GpgME::Context::createForEngine (GpgME::SpawnEngine); if (!ctx) { TRACEPOINT; return false; } GpgME::Data mystdin, mystdout, mystderr; char **cargs = vector_to_cArray (args); GpgME::Error err = ctx->spawn (cargs[0], const_cast (cargs), mystdin, mystdout, mystderr, GpgME::Context::SpawnNone); release_cArray (cargs); if (err) { log_debug ("%s:%s: WKS client spawn code: %i asString: %s", SRCNAME, __func__, err.code(), err.asString()); return false; } auto data = mystdout.toString (); rtrim (data); return data == "[GNUPG:] SUCCESS"; } static DWORD WINAPI do_check (LPVOID arg) { const auto wksPath = get_wks_client_path (); if (wksPath.empty()) { return 0; } std::vector args; const auto mbox = std::string ((char *) arg); xfree (arg); args.push_back (wksPath); args.push_back (std::string ("--status-fd")); args.push_back (std::string ("1")); args.push_back (std::string ("--supported")); args.push_back (mbox); // Spawn the process auto ctx = GpgME::Context::createForEngine (GpgME::SpawnEngine); if (!ctx) { TRACEPOINT; return 0; } GpgME::Data mystdin, mystdout, mystderr; char **cargs = vector_to_cArray (args); GpgME::Error err = ctx->spawn (cargs[0], const_cast (cargs), mystdin, mystdout, mystderr, GpgME::Context::SpawnNone); release_cArray (cargs); if (err) { log_debug ("%s:%s: WKS client spawn code: %i asString: %s", SRCNAME, __func__, err.code(), err.asString()); return 0; } auto data = mystdout.toString (); rtrim (data); bool success = data == "[GNUPG:] SUCCESS"; // TODO Figure out NeedsPublish state. auto state = success ? WKSHelper::NeedsPublish : WKSHelper::NotSupported; bool isPublished = false; if (success) { log_debug ("%s:%s: WKS client: '%s' is supported", SRCNAME, __func__, anonstr (mbox.c_str ())); isPublished = check_published (mbox); } if (isPublished) { log_debug ("%s:%s: WKS client: '%s' is published", SRCNAME, __func__, anonstr (mbox.c_str ())); state = WKSHelper::IsPublished; } WKSHelper::instance()->update_state (mbox, state, false); WKSHelper::instance()->update_last_checked (mbox, time (0)); return 0; } void WKSHelper::start_check (const std::string &mbox, bool forced) const { const auto state = get_state (mbox); if (!forced && (state != NotChecked && state != NotSupported)) { log_debug ("%s:%s: Check aborted because its neither " "not supported nor not checked.", SRCNAME, __func__); return; } auto lastTime = get_check_time (mbox); auto now = time (0); if (!forced && (state == NotSupported && lastTime && difftime (now, lastTime) < CHECK_MIN_INTERVAL)) { /* Data is new enough */ log_debug ("%s:%s: Check aborted because last checked is too recent.", SRCNAME, __func__); return; } if (mbox.empty()) { log_debug ("%s:%s: start check called without mbox", SRCNAME, __func__); } log_debug ("%s:%s: WKSHelper starting check", SRCNAME, __func__); /* Start the actual work that can be done in a background thread. */ CloseHandle (CreateThread (nullptr, 0, do_check, xstrdup (mbox.c_str ()), 0, nullptr)); return; } void WKSHelper::load () const { /* Map of mbox'es to states. states are ; */ const auto map = get_registry_subkeys (WKS_REG_KEY); for (const auto &pair: map) { const auto mbox = pair.first; const auto states = gpgol_split (pair.second, ';'); if (states.size() != 2) { log_error ("%s:%s: Invalid state '%s' for '%s'", SRCNAME, __func__, anonstr (mbox.c_str ()), anonstr (pair.second.c_str ())); continue; } WKSState state = (WKSState) strtol (states[0].c_str (), nullptr, 10); if (state == PublishInProgress) { /* Probably an error during the last publish. Let's start again. */ update_state (mbox, NotChecked, false); continue; } time_t update_time = (time_t) strtol (states[1].c_str (), nullptr, 10); update_state (mbox, state, false); update_last_checked (mbox, update_time, false); } } void WKSHelper::save () const { gpgol_lock (&wks_lock); for (const auto &pair: s_states) { auto state = std::to_string (pair.second) + ';'; const auto it = s_last_checked.find (pair.first); if (it != s_last_checked.end ()) { state += std::to_string (it->second); } else { state += '0'; } if (store_extension_subkey_value (WKS_REG_KEY, pair.first.c_str (), state.c_str ())) { log_error ("%s:%s: Failed to store state.", SRCNAME, __func__); } } gpgol_unlock (&wks_lock); } static DWORD WINAPI do_notify (LPVOID arg) { /** Wait till a message was sent */ std::pair *args = (std::pair *) arg; Sleep (args->second); do_in_ui_thread (WKS_NOTIFY, args->first); delete args; return 0; } void WKSHelper::allow_notify (int sleepTimeMS) const { gpgol_lock (&wks_lock); for (auto &pair: s_states) { if (pair.second == ConfirmationSeen || pair.second == NeedsPublish) { auto *args = new std::pair (xstrdup (pair.first.c_str()), sleepTimeMS); CloseHandle (CreateThread (nullptr, 0, do_notify, args, 0, nullptr)); break; } } gpgol_unlock (&wks_lock); } void WKSHelper::notify (const char *cBox) const { std::string mbox = cBox; const auto state = get_state (mbox); if (state == NeedsPublish) { char *buf; gpgrt_asprintf (&buf, _("A Pubkey directory is available for the address:\n\n" "\t%s\n\n" "Register your Pubkey in that directory to make\n" "it easy for others to send you encrypted mail.\n\n" "It's secure and free!\n\n" "Register automatically?"), mbox.c_str ()); memdbg_alloc (buf); if (gpgol_message_box (get_active_hwnd (), buf, _("GpgOL: Pubkey directory available!"), MB_YESNO) == IDYES) { start_publish (mbox); } else { update_state (mbox, PublishDenied); } xfree (buf); return; } if (state == ConfirmationSeen) { handle_confirmation_notify (mbox); return; } log_debug ("%s:%s: Unhandled notify state: %i for '%s'", SRCNAME, __func__, state, anonstr (cBox)); return; } void WKSHelper::start_publish (const std::string &mbox) const { log_debug ("%s:%s: Start publish for '%s'", SRCNAME, __func__, mbox.c_str ()); update_state (mbox, PublishInProgress); const auto key = GpgME::Key::locate (mbox.c_str ()); if (key.isNull ()) { MessageBox (get_active_hwnd (), "WKS publish failed to find key for mail address.", _("GpgOL"), MB_ICONINFORMATION|MB_OK); return; } const auto wksPath = get_wks_client_path (); if (wksPath.empty()) { TRACEPOINT; return; } std::vector args; args.push_back (wksPath); args.push_back (std::string ("--create")); args.push_back (std::string (key.primaryFingerprint ())); args.push_back (mbox); // Spawn the process auto ctx = GpgME::Context::createForEngine (GpgME::SpawnEngine); if (!ctx) { TRACEPOINT; return; } GpgME::Data mystdin, mystdout, mystderr; char **cargs = vector_to_cArray (args); GpgME::Error err = ctx->spawn (cargs[0], const_cast (cargs), mystdin, mystdout, mystderr, GpgME::Context::SpawnNone); release_cArray (cargs); if (err) { log_debug ("%s:%s: WKS client spawn code: %i asString: %s", SRCNAME, __func__, err.code(), err.asString()); return; } const auto data = mystdout.toString (); if (data.empty ()) { gpgol_message_box (get_active_hwnd (), mystderr.toString().c_str (), _("GpgOL: Directory request failed"), MB_OK); return; } log_data ("%s:%s: WKS client: returned '%s'", SRCNAME, __func__, data.c_str ()); if (!send_mail (data)) { gpgol_message_box (get_active_hwnd (), _("You might receive a confirmation challenge from\n" "your provider to finish the registration."), _("GpgOL: Registration request sent!"), MB_OK); } update_state (mbox, RequestSent); return; } void WKSHelper::update_state (const std::string &mbox, WKSState state, bool store) const { gpgol_lock (&wks_lock); auto it = s_states.find(mbox); if (it != s_states.end()) { it->second = state; } else { s_states.insert (std::make_pair (mbox, state)); } gpgol_unlock (&wks_lock); if (store) { save (); } } void WKSHelper::update_last_checked (const std::string &mbox, time_t time, bool store) const { gpgol_lock (&wks_lock); auto it = s_last_checked.find(mbox); if (it != s_last_checked.end()) { it->second = time; } else { s_last_checked.insert (std::make_pair (mbox, time)); } gpgol_unlock (&wks_lock); if (store) { save (); } } int WKSHelper::send_mail (const std::string &mimeData) const { std::istringstream ss(mimeData); std::string from; std::string to; std::string subject; std::string withoutHeaders; std::getline (ss, from); std::getline (ss, to); std::getline (ss, subject); if (from.compare (0, 6, "From: ") || to.compare (0, 4, "To: "), subject.compare (0, 9, "Subject: ")) { log_error ("%s:%s: Invalid mime data..", SRCNAME, __func__); return -1; } std::getline (ss, withoutHeaders, '\0'); from.erase (0, 6); to.erase (0, 4); subject.erase (0, 9); rtrim (from); rtrim (to); rtrim (subject); LPDISPATCH mail = create_mail (); if (!mail) { log_error ("%s:%s: Failed to create mail for request.", SRCNAME, __func__); return -1; } /* Now we have a problem. The created LPDISPATCH pointer has a different value then the one with which we saw the ItemLoad event. But we want to get the mail object. So,.. surpise a Hack! :-) */ auto last_mail = Mail::getLastMail (); if (!Mail::isValidPtr (last_mail)) { log_error ("%s:%s: Invalid last mail %p.", SRCNAME, __func__, last_mail); return -1; } /* Adding to / Subject etc already leads to changes and events so we set up the state before this. */ last_mail->setOverrideMIMEData (mimeData); - last_mail->setCryptState (Mail::NeedsSecondAfterWrite); + last_mail->setCryptState (Mail::OOMUpdated); if (put_oom_string (mail, "Subject", subject.c_str ())) { TRACEPOINT; gpgol_release (mail); return -1; } if (put_oom_string (mail, "To", to.c_str ())) { TRACEPOINT; gpgol_release (mail); return -1; } LPDISPATCH account = get_account_for_mail (from.c_str ()); if (account) { log_debug ("%s:%s: Found account to change for '%s'.", SRCNAME, __func__, anonstr (from.c_str ())); put_oom_disp (mail, "SendUsingAccount", account); } gpgol_release (account); if (invoke_oom_method (mail, "Save", nullptr)) { // Should not happen. log_error ("%s:%s: Failed to save mail.", SRCNAME, __func__); return -1; } if (invoke_oom_method (mail, "Send", nullptr)) { log_error ("%s:%s: Failed to send mail.", SRCNAME, __func__); return -1; } log_debug ("%s:%s: Done send mail.", SRCNAME, __func__); return 0; } static void copy_stream_to_data (LPSTREAM stream, GpgME::Data *data) { HRESULT hr; char buf[4096]; ULONG bRead; while ((hr = stream->Read (buf, 4096, &bRead)) == S_OK || hr == S_FALSE) { if (!bRead) { // EOF return; } data->write (buf, (size_t) bRead); } } void WKSHelper::handle_confirmation_notify (const std::string &mbox) const { auto pair = get_cached_confirmation (mbox); GpgME::Data *mimeData = pair.first; Mail *mail = pair.second; if (!mail && !mimeData) { log_debug ("%s:%s: Confirmation notify without cached data.", SRCNAME, __func__); /* This happens when we have seen a confirmation but have * not confirmed it and the state was saved. So we go back * to the confirmation sent state and wait until we see * the confirmation the next time. */ update_state (mbox, ConfirmationSent); return; } /* First ask the user if he wants to confirm */ if (gpgol_message_box (get_active_hwnd (), _("Confirm registration?"), _("GpgOL: Pubkey directory confirmation"), MB_YESNO) != IDYES) { log_debug ("%s:%s: User aborted confirmation.", SRCNAME, __func__); delete mimeData; /* Next time we read the confirmation we ask again. */ update_state (mbox, RequestSent); return; } /* Do the confirmation */ const auto wksPath = get_wks_client_path (); if (wksPath.empty()) { TRACEPOINT; return; } std::vector args; args.push_back (wksPath); args.push_back (std::string ("--receive")); // Spawn the process auto ctx = GpgME::Context::createForEngine (GpgME::SpawnEngine); if (!ctx) { TRACEPOINT; return; } GpgME::Data mystdout, mystderr; char **cargs = vector_to_cArray (args); GpgME::Error err = ctx->spawn (cargs[0], const_cast (cargs), *mimeData, mystdout, mystderr, GpgME::Context::SpawnNone); release_cArray (cargs); if (err) { log_debug ("%s:%s: WKS client spawn code: %i asString: %s", SRCNAME, __func__, err.code(), err.asString()); return; } const auto data = mystdout.toString (); if (data.empty ()) { gpgol_message_box (get_active_hwnd (), mystderr.toString().c_str (), _("GpgOL: Confirmation failed"), MB_OK); return; } log_data ("%s:%s: WKS client: returned '%s'", SRCNAME, __func__, data.c_str ()); if (!send_mail (data)) { gpgol_message_box (get_active_hwnd (), _("Your Pubkey can soon be retrieved from your domain."), _("GpgOL: Request confirmed!"), MB_OK); } if (mail && Mail::isValidPtr (mail)) { invoke_oom_method (mail->item(), "Delete", nullptr); } update_state (mbox, ConfirmationSent); } void WKSHelper::handle_confirmation_read (Mail *mail, LPSTREAM stream) const { /* We get the handle_confirmation in the Read event. To do sending etc. we have to move out of that event. For this we prepare the data for later usage. */ if (!mail || !stream) { TRACEPOINT; return; } /* Get the recipient of the confirmation mail */ const auto recipients = mail->getRecipients_o (); /* We assert that we have one recipient as the mail should have been sent by the wks-server. */ if (recipients.size() != 1) { log_error ("%s:%s: invalid recipients", SRCNAME, __func__); gpgol_release (stream); return; } std::string mbox = recipients[0].mbox (); /* Prepare stdin for the wks-client process */ /* First we need to write the headers */ LPMESSAGE message = get_oom_base_message (mail->item()); if (!message) { log_error ("%s:%s: Failed to obtain message.", SRCNAME, __func__); gpgol_release (stream); return; } const auto headers = mapi_get_header (message); gpgol_release (message); GpgME::Data *mystdin = new GpgME::Data(); mystdin->write (headers.c_str (), headers.size ()); /* Then the MIME data */ GpgME::Data mimeData; header_info_s mime_info = mail->headerInfo (); copy_stream_to_data (stream, &mimeData); gpgol_release (stream); auto stringData = mimeData.toString (); if (stringData.rfind ("-----BEGIN PGP", 0) == 0) { log_dbg ("Found PGP marker at the beginning. Adding Ct" "MIME"); std::stringstream ss; ss << "MIME-Version: 1.0\r\n\r\n\r\n" << "--" <write (stringData.c_str (), stringData.size ()); /* Then lets make sure its flushy */ mystdin->write (nullptr, 0); /* And reset it to start */ mystdin->seek (0, SEEK_SET); gpgol_lock (&wks_lock); s_confirmation_cache.insert (std::make_pair (mbox, std::make_pair (mystdin, mail))); gpgol_unlock (&wks_lock); update_state (mbox, ConfirmationSeen); /* Send the window message for notify. */ allow_notify (5000); }