Fallback to MAPI SubmitMessage if OOM Send fails
[gpgol.git] / src / windowmessages.cpp
1 /* @file windowmessages.h
2  * @brief Helper class to work with the windowmessage handler thread.
3  *
4  * Copyright (C) 2015, 2016 by Bundesamt für Sicherheit in der Informationstechnik
5  * Software engineering by Intevation GmbH
6  *
7  * This file is part of GpgOL.
8  *
9  * GpgOL is free software; you can redistribute it and/or
10  * modify it under the terms of the GNU Lesser General Public
11  * License as published by the Free Software Foundation; either
12  * version 2.1 of the License, or (at your option) any later version.
13  *
14  * GpgOL is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17  * GNU Lesser General Public License for more details.
18  *
19  * You should have received a copy of the GNU Lesser General Public License
20  * along with this program; if not, see <http://www.gnu.org/licenses/>.
21  */
22 #include "windowmessages.h"
23
24 #include "common.h"
25 #include "oomhelp.h"
26 #include "mail.h"
27 #include "gpgoladdin.h"
28 #include "wks-helper.h"
29
30 #include <stdio.h>
31
32 #define RESPONDER_CLASS_NAME "GpgOLResponder"
33
34 /* Singleton window */
35 static HWND g_responder_window = NULL;
36 static int invalidation_blocked = 0;
37
38 LONG_PTR WINAPI
39 gpgol_window_proc (HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
40 {
41 //  log_debug ("WMG: %x", (unsigned int) message);
42   if (message == WM_USER + 42)
43     {
44       wm_ctx_t *ctx = (wm_ctx_t *) lParam;
45       log_debug ("%s:%s: Recieved user msg: %i",
46                  SRCNAME, __func__, ctx->wmsg_type);
47       switch (ctx->wmsg_type)
48         {
49           case (PARSING_DONE):
50             {
51               auto mail = (Mail*) ctx->data;
52               if (!Mail::isValidPtr (mail))
53                 {
54                   log_debug ("%s:%s: Parsing done for mail which is gone.",
55                              SRCNAME, __func__);
56                   break;
57                 }
58               mail->parsing_done();
59               break;
60             }
61           case (RECIPIENT_ADDED):
62             {
63               auto mail = (Mail*) ctx->data;
64               if (!Mail::isValidPtr (mail))
65                 {
66                   log_debug ("%s:%s: Recipient add for mail which is gone.",
67                              SRCNAME, __func__);
68                   break;
69                 }
70               mail->locateKeys_o ();
71               break;
72             }
73           case (REVERT_MAIL):
74             {
75               auto mail = (Mail*) ctx->data;
76               if (!Mail::isValidPtr (mail))
77                 {
78                   log_debug ("%s:%s: Revert mail for mail which is gone.",
79                              SRCNAME, __func__);
80                   break;
81                 }
82
83               mail->setNeedsSave (true);
84               /* Some magic here. Accessing any existing inline body cements
85                  it. Otherwise updating the body through the revert also changes
86                  the body of a inline mail. */
87               char *inlineBody = get_inline_body ();
88               xfree (inlineBody);
89
90               // Does the revert.
91               log_debug ("%s:%s: Revert mail. Invoking save.",
92                          SRCNAME, __func__);
93               invoke_oom_method (mail->item (), "Save", NULL);
94               log_debug ("%s:%s: Revert mail. Save done. Updating body..",
95                          SRCNAME, __func__);
96               mail->updateBody_o ();
97               log_debug ("%s:%s: Revert mail done.",
98                          SRCNAME, __func__);
99               break;
100             }
101           case (INVALIDATE_UI):
102             {
103               if (!invalidation_blocked)
104                 {
105                   log_debug ("%s:%s: Invalidating UI",
106                              SRCNAME, __func__);
107                   gpgoladdin_invalidate_ui();
108                   log_debug ("%s:%s: Invalidation done",
109                              SRCNAME, __func__);
110                 }
111               else
112                 {
113                   log_debug ("%s:%s: Received invalidation msg while blocked."
114                              " Ignoring it",
115                              SRCNAME, __func__);
116                 }
117               break;
118             }
119           case (INVALIDATE_LAST_MAIL):
120             {
121               log_debug ("%s:%s: clearing last mail",
122                          SRCNAME, __func__);
123               Mail::clearLastMail ();
124               break;
125             }
126           case (CLOSE):
127             {
128               auto mail = (Mail*) ctx->data;
129               if (!Mail::isValidPtr (mail))
130                 {
131                   log_debug ("%s:%s: Close for mail which is gone.",
132                              SRCNAME, __func__);
133                   break;
134                 }
135               mail->refCurrentItem();
136               Mail::close (mail);
137               log_debug ("%s:%s: Close finished.",
138                          SRCNAME, __func__);
139               mail->releaseCurrentItem();
140               break;
141             }
142           case (CRYPTO_DONE):
143             {
144               auto mail = (Mail*) ctx->data;
145               if (!Mail::isValidPtr (mail))
146                 {
147                   log_debug ("%s:%s: Crypto done for mail which is gone.",
148                              SRCNAME, __func__);
149                   break;
150                 }
151               // modify the mail.
152               if (mail->cryptState () == Mail::NeedsUpdateInOOM)
153                 {
154                   // Save the Mail
155                   log_debug ("%s:%s: Crypto done for %p updating oom.",
156                              SRCNAME, __func__, mail);
157                   mail->updateCryptOOM_o ();
158                 }
159               if (mail->cryptState () == Mail::NeedsSecondAfterWrite)
160                 {
161                   invoke_oom_method (mail->item (), "Save", NULL);
162                   log_debug ("%s:%s: Second save done for %p Invoking second send.",
163                              SRCNAME, __func__, mail);
164                 }
165               // Finaly this should pass.
166               if (invoke_oom_method (mail->item (), "Send", NULL))
167                 {
168                   log_error ("%s:%s: Send failed for %p. "
169                              "Trying SubmitMessage instead.",
170                              SRCNAME, __func__, mail);
171                   auto mail_message = get_oom_base_message (mail->item());
172                   // It's important we use the _base_ message here.
173                   mapi_save_changes (mail_message,
174                                      KEEP_OPEN_READWRITE | FORCE_SAVE);
175                   HRESULT hr = mail_message->SubmitMessage(0);
176                   gpgol_release (mail_message);
177
178                   if (hr == S_OK)
179                     {
180                       do_in_ui_thread_async (CLOSE, (LPVOID) mail);
181                     }
182                   else
183                     {
184                       log_error ("%s:%s: SubmitMessage Failed hr=0x%lx.",
185                                  SRCNAME, __func__, hr);
186                       gpgol_bug (mail->getWindow (),
187                                  ERR_SEND_FALLBACK_FAILED);
188                     }
189                 }
190               log_debug ("%s:%s:  Send for %p completed.",
191                          SRCNAME, __func__, mail);
192               mail->releaseCurrentItem ();
193               break;
194             }
195           case (BRING_TO_FRONT):
196             {
197               HWND wnd = get_active_hwnd ();
198               if (wnd)
199                 {
200                   log_debug ("%s:%s: Bringing window %p to front.",
201                              SRCNAME, __func__, wnd);
202                   bring_to_front (wnd);
203                 }
204               else
205                 {
206                   log_debug ("%s:%s: No active window found for bring to front.",
207                              SRCNAME, __func__);
208                 }
209               break;
210             }
211           case (WKS_NOTIFY):
212             {
213               WKSHelper::instance ()->notify ((const char *) ctx->data);
214               xfree (ctx->data);
215               break;
216             }
217           case (CLEAR_REPLY_FORWARD):
218             {
219               auto mail = (Mail*) ctx->data;
220               if (!Mail::isValidPtr (mail))
221                 {
222                   log_debug ("%s:%s: Clear reply forward for mail which is gone.",
223                              SRCNAME, __func__);
224                   break;
225                 }
226               mail->wipe_o (true);
227               mail->removeAllAttachments_o ();
228               break;
229             }
230           case (DO_AUTO_SECURE):
231             {
232               auto mail = (Mail*) ctx->data;
233               if (!Mail::isValidPtr (mail))
234                 {
235                   log_debug ("%s:%s: DO_AUTO_SECURE for mail which is gone.",
236                              SRCNAME, __func__);
237                   break;
238                 }
239               mail->setDoAutosecure_m (true);
240               break;
241             }
242           case (DONT_AUTO_SECURE):
243             {
244               auto mail = (Mail*) ctx->data;
245               if (!Mail::isValidPtr (mail))
246                 {
247                   log_debug ("%s:%s: DO_AUTO_SECURE for mail which is gone.",
248                              SRCNAME, __func__);
249                   break;
250                 }
251               mail->setDoAutosecure_m (false);
252               break;
253             }
254           default:
255             log_debug ("%s:%s: Unknown msg %x",
256                        SRCNAME, __func__, ctx->wmsg_type);
257         }
258         return 0;
259     }
260   return DefWindowProc(hWnd, message, wParam, lParam);
261 }
262
263 HWND
264 create_responder_window ()
265 {
266   size_t cls_name_len = strlen(RESPONDER_CLASS_NAME) + 1;
267   char cls_name[cls_name_len];
268   if (g_responder_window)
269     {
270       return g_responder_window;
271     }
272   /* Create Window wants a mutable string as the first parameter */
273   snprintf (cls_name, cls_name_len, "%s", RESPONDER_CLASS_NAME);
274
275   WNDCLASS windowClass;
276   windowClass.style = CS_GLOBALCLASS | CS_DBLCLKS;
277   windowClass.lpfnWndProc = gpgol_window_proc;
278   windowClass.cbClsExtra = 0;
279   windowClass.cbWndExtra = 0;
280   windowClass.hInstance = (HINSTANCE) GetModuleHandle(NULL);
281   windowClass.hIcon = 0;
282   windowClass.hCursor = 0;
283   windowClass.hbrBackground = 0;
284   windowClass.lpszMenuName  = 0;
285   windowClass.lpszClassName = cls_name;
286   RegisterClass(&windowClass);
287   g_responder_window = CreateWindow (cls_name, RESPONDER_CLASS_NAME, 0, 0, 0,
288                                      0, 0, 0, (HMENU) 0,
289                                      (HINSTANCE) GetModuleHandle(NULL), 0);
290   return g_responder_window;
291 }
292
293 static int
294 send_msg_to_ui_thread (wm_ctx_t *ctx)
295 {
296   size_t cls_name_len = strlen(RESPONDER_CLASS_NAME) + 1;
297   char cls_name[cls_name_len];
298   snprintf (cls_name, cls_name_len, "%s", RESPONDER_CLASS_NAME);
299
300   HWND responder = FindWindow (cls_name, RESPONDER_CLASS_NAME);
301   if (!responder)
302   {
303     log_error ("%s:%s: Failed to find responder window.",
304                SRCNAME, __func__);
305     return -1;
306   }
307   SendMessage (responder, WM_USER + 42, 0, (LPARAM) ctx);
308   return 0;
309 }
310
311 int
312 do_in_ui_thread (gpgol_wmsg_type type, void *data)
313 {
314   wm_ctx_t ctx = {NULL, UNKNOWN, 0, 0};
315   ctx.wmsg_type = type;
316   ctx.data = data;
317
318   log_debug ("%s:%s: Sending message of type %i",
319              SRCNAME, __func__, type);
320
321   if (send_msg_to_ui_thread (&ctx))
322     {
323       return -1;
324     }
325   return ctx.err;
326 }
327
328 static DWORD WINAPI
329 do_async (LPVOID arg)
330 {
331   wm_ctx_t *ctx = (wm_ctx_t*) arg;
332   log_debug ("%s:%s: Do async with type %i after %i ms",
333              SRCNAME, __func__, ctx ? ctx->wmsg_type : -1,
334              ctx->delay);
335   if (ctx->delay)
336     {
337       Sleep (ctx->delay);
338     }
339   send_msg_to_ui_thread (ctx);
340   xfree (ctx);
341   return 0;
342 }
343
344 void
345 do_in_ui_thread_async (gpgol_wmsg_type type, void *data, int delay)
346 {
347   wm_ctx_t *ctx = (wm_ctx_t *) xcalloc (1, sizeof (wm_ctx_t));
348   ctx->wmsg_type = type;
349   ctx->data = data;
350   ctx->delay = delay;
351
352   CloseHandle (CreateThread (NULL, 0, do_async, (LPVOID) ctx, 0, NULL));
353 }
354
355 LRESULT CALLBACK
356 gpgol_hook(int code, WPARAM wParam, LPARAM lParam)
357 {
358 /* Once we are in the close events we don't have enough
359    control to revert all our changes so we have to do it
360    with this nice little hack by catching the WM_CLOSE message
361    before it reaches outlook. */
362   LPCWPSTRUCT cwp = (LPCWPSTRUCT) lParam;
363
364   /* What we do here is that we catch all WM_CLOSE messages that
365      get to Outlook. Then we check if the last open Explorer
366      is the target of the close. In set case we start our shutdown
367      routine before we pass the WM_CLOSE to outlook */
368   switch (cwp->message)
369     {
370       case WM_CLOSE:
371       {
372         HWND lastChild = NULL;
373         log_debug ("%s:%s: Got WM_CLOSE",
374                    SRCNAME, __func__);
375         if (!GpgolAddin::get_instance() || !GpgolAddin::get_instance ()->get_application())
376           {
377             TRACEPOINT;
378             break;
379           }
380         LPDISPATCH explorers = get_oom_object (GpgolAddin::get_instance ()->get_application(),
381                                                "Explorers");
382
383         if (!explorers)
384           {
385             log_error ("%s:%s: No explorers object",
386                        SRCNAME, __func__);
387             break;
388           }
389         int count = get_oom_int (explorers, "Count");
390
391         if (count != 1)
392           {
393             log_debug ("%s:%s: More then one explorer. Not shutting down.",
394                        SRCNAME, __func__);
395             gpgol_release (explorers);
396             break;
397           }
398
399         LPDISPATCH explorer = get_oom_object (explorers, "Item(1)");
400         gpgol_release (explorers);
401
402         if (!explorer)
403           {
404             TRACEPOINT;
405             break;
406           }
407
408         /* Casting to LPOLEWINDOW and calling GetWindow
409            succeeded in Outlook 2016 but always returned
410            the number 1. So we need this hack. */
411         char *caption = get_oom_string (explorer, "Caption");
412         gpgol_release (explorer);
413         if (!caption)
414           {
415             log_debug ("%s:%s: No caption.",
416                        SRCNAME, __func__);
417             break;
418           }
419         /* rctrl_renwnd32 is the window class of outlook. */
420         HWND hwnd = FindWindowExA(NULL, lastChild, "rctrl_renwnd32",
421                                   caption);
422         xfree (caption);
423         lastChild = hwnd;
424         if (hwnd == cwp->hwnd)
425           {
426             log_debug ("%s:%s: WM_CLOSE windowmessage for explorer. "
427                        "Shutting down.",
428                        SRCNAME, __func__);
429             GpgolAddin::get_instance ()->shutdown();
430             break;
431           }
432         break;
433       }
434      case WM_SYSCOMMAND:
435         /*
436          This comes to often and when we are closed from the icon
437          we also get WM_CLOSE
438        if (cwp->wParam == SC_CLOSE)
439         {
440           log_debug ("%s:%s: SC_CLOSE syscommand. Closing all mails.",
441                      SRCNAME, __func__);
442           GpgolAddin::get_instance ()->shutdown();
443         } */
444        break;
445      default:
446 //       log_debug ("WM: %x", (unsigned int) cwp->message);
447        break;
448     }
449   return CallNextHookEx (NULL, code, wParam, lParam);
450 }
451
452 /* Create the message hook for outlook's windowmessages
453    we are especially interested in WM_QUIT to do cleanups
454    and prevent the "Item has changed" question. */
455 HHOOK
456 create_message_hook()
457 {
458   return SetWindowsHookEx (WH_CALLWNDPROC,
459                            gpgol_hook,
460                            NULL,
461                            GetCurrentThreadId());
462 }
463
464 GPGRT_LOCK_DEFINE (invalidate_lock);
465
466 static bool invalidation_in_progress;
467
468 DWORD WINAPI
469 delayed_invalidate_ui (LPVOID minsleep)
470 {
471   if (invalidation_in_progress)
472     {
473       log_debug ("%s:%s: Invalidation canceled as it is in progress.",
474                  SRCNAME, __func__);
475       return 0;
476     }
477   TRACEPOINT;
478   invalidation_in_progress = true;
479   gpgrt_lock_lock(&invalidate_lock);
480
481   int sleep_ms = (intptr_t)minsleep;
482   Sleep (sleep_ms);
483   int i = 0;
484   while (invalidation_blocked)
485     {
486       i++;
487       if (i % 10 == 0)
488         {
489           log_debug ("%s:%s: Waiting for invalidation.",
490                      SRCNAME, __func__);
491         }
492
493       Sleep (100);
494       /* Do we need an abort statement here? */
495     }
496   do_in_ui_thread (INVALIDATE_UI, nullptr);
497   TRACEPOINT;
498   invalidation_in_progress = false;
499   gpgrt_lock_unlock(&invalidate_lock);
500   return 0;
501 }
502
503 DWORD WINAPI
504 close_mail (LPVOID mail)
505 {
506   do_in_ui_thread (CLOSE, mail);
507   return 0;
508 }
509
510 void
511 blockInv()
512 {
513   invalidation_blocked++;
514   log_oom_extra ("%s:%s: Invalidation block count %i",
515                  SRCNAME, __func__, invalidation_blocked);
516 }
517
518 void
519 unblockInv()
520 {
521   invalidation_blocked--;
522   log_oom_extra ("%s:%s: Invalidation block count %i",
523                  SRCNAME, __func__, invalidation_blocked);
524
525   if (invalidation_blocked < 0)
526     {
527       log_error ("%s:%s: Invalidation block mismatch",
528                  SRCNAME, __func__);
529       invalidation_blocked = 0;
530     }
531 }