7b5418213c7e7d91e40c30739a86928c4b40f3b6
[gpgol.git] / src / msgcache.c
1 /* msgcache.c - Implementation of a message cache.
2  *      Copyright (C) 2005 g10 Code GmbH
3  *
4  * This file is part of GPGol.
5  * 
6  * GPGol is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 2 of the License, or (at your option) any later version.
10  * 
11  * GPGol is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  * GNU Lesser General Public License for more details.
15  * 
16  * You should have received a copy of the GNU Lesser General Public
17  * License along with this program; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19  * 02110-1301, USA.
20  */
21
22 /* Due to some peculiarities of Outlook 2003 and possible also earlier
23    versions we need fixup the text of a reply before editing starts.
24    This is done using the Echache extension mechanism and this module
25    provides the means of caching and locating messages.  To be exact,
26    we don't cache entire messages but just the plaintext after
27    decryption.
28
29    What we do here is to save the plaintext in a list under a key
30    taken from the PR_CONVERSATION_INDEX property.  It seems that this
31    is a reliable key to match the message again after Reply has been
32    called.  The ConversationIndex as available as an OL item in the
33    SENDNOTEMESSAGE hook is 6 bytes longer that the one we get from
34    MAPI on the orginal message; thus we only compare up to the length
35    we have stored when caching the message.  OL2003 using POP3 uses a
36    ConversationIndex of 22/28 bytes.  Note that we could also read the
37    ConversationIndex from the OL object but it is easier in
38    msgcache_put to retrieve it direct from MAPI.
39
40    The obvious problem with the conversation index is that it does
41    only work with independent messages and fails badly when reading
42    and replying to several mails from one message thread.
43
44    The desitable soultion would be a way to know the rfc822 message-id
45    of the orignal message when entering the reply form.  I can't find
46    such a datum but it might be there anyway. I am here thinking of a
47    MS coder who wants a reply which indicates the message id and the
48    date of the message.
49
50    To keep the memory size at bay we but a limit on the maximum cache
51    size; thus depending on the total size of the messages the number
52    of open inspectors with decrypted messages which can be matched
53    against a reply template is limited. We try to make sure that there
54    is at least one message; this makes sure that in the most common
55    case the plaintext is always available.  We use a circular buffer
56    so that the oldest messages are flushed from the cache first.  I
57    don't think that it makes much sense to take the size of a message
58    into account here.
59 */
60    
61
62 #ifdef HAVE_CONFIG_H
63 #include <config.h>
64 #endif
65
66 #include <windows.h>
67 #include <assert.h>
68 #include <string.h>
69
70 #include "mymapi.h"
71 #include "mymapitags.h"
72
73 #include "msgcache.h"
74 #include "util.h"
75
76
77 /* We limit the size of the cache to this value. The cache might take
78    up more space temporary if a new message is larger than this values
79    or if several thereads are currently accessing the cache. */
80 #define MAX_CACHESIZE (512*1024)  /* 512k */
81
82
83 /* A Item to hold a cache message, i.e. the plaintext and a key. */
84 struct cache_item
85 {
86   struct cache_item *next;  
87
88   char *plaintext;  /* The malloced plaintext of the message.  This is
89                        assumed to be a valid C String, UTF8
90                        encoded. */
91   size_t length;    /* The length of that plaintext used to compute
92                        the total size of the cache. */
93
94   int ref;          /* Reference counter, indicating how many callers
95                        are currently accessing the plaintext of this
96                        object. */
97
98   /* The length of the key and the key itself.  The cache item is
99      dynamically allocated to fit the size of the key.  Note, that
100      the key is a binary blob. */
101   size_t keylen;
102   char key[1];
103 };
104 typedef struct cache_item *cache_item_t;
105
106
107 /* The actual cache is a simple list anchord at this global
108    variable. */
109 static cache_item_t the_cache;
110
111 /* Mutex used to serialize access to the cache. */
112 static HANDLE cache_mutex;
113
114
115
116 /* Initialize this module.  Called at a very early stage during DLL
117    loading.  Returns 0 on success. */
118 int
119 initialize_msgcache (void)
120 {
121   SECURITY_ATTRIBUTES sa;
122   
123   memset (&sa, 0, sizeof sa);
124   sa.bInheritHandle = FALSE;
125   sa.lpSecurityDescriptor = NULL;
126   sa.nLength = sizeof sa;
127   cache_mutex = CreateMutex (&sa, FALSE, NULL);
128   return cache_mutex? 0 : -1;
129 }
130
131
132 /* Acquire the mutex.  Returns 0 on success. */
133 static int 
134 lock_cache (void)
135 {
136   int code = WaitForSingleObject (cache_mutex, INFINITE);
137   if (code != WAIT_OBJECT_0)
138     log_error ("%s:%s: waiting on mutex failed: code=%#x\n",
139                __FILE__, __func__, code);
140   return code != WAIT_OBJECT_0;
141 }
142
143 /* Release the mutex.  No error is returned because this is a fatal
144    error anyway and there is no way to clean up. */
145 static void
146 unlock_cache (void)
147 {
148   if (!ReleaseMutex (cache_mutex))
149     log_error_w32 (-1, "%s:%s: ReleaseMutex failed", __FILE__, __func__);
150 }
151
152
153 /* Flush entries from the cache if it gets too large.  NOTE: This
154    function needs to be called with a locked cache.  NEWSIZE is the
155    size of the message we want to put in the cache later. */
156 static void
157 flush_if_needed (size_t newsize)
158 {
159   cache_item_t item, prev;
160   size_t total;
161   
162   for (total = newsize, item = the_cache; item; item = item->next)
163     total += item->length;
164
165   if (total <= MAX_CACHESIZE)
166     return;
167
168   /* Our algorithm to remove entries is pretty simple: We remove
169      entries from the end until we are below the maximum size. */
170  again:
171   for (item = the_cache, prev = NULL; item; prev = item, item = item->next)
172     if ( !item->next && !item->ref)
173       {
174         if (prev)
175           prev->next = NULL;
176         else
177           the_cache = NULL;
178         if (total > item->length)
179           total -= item->length;
180         else
181           total = 0;
182         xfree (item->plaintext);
183         xfree (item);
184         if (total > MAX_CACHESIZE)
185           goto again;
186         break;
187       }
188 }
189
190
191 /* Put the BODY of a message into the cache.  BODY should be a
192    malloced string, UTF8 encoded.  If TRANSFER is given as true, the
193    ownership of the malloced memory for BODY is transferred to this
194    module.  MESSAGE is the MAPI message object used to retrieve the
195    storarge key for the BODY. */
196 void
197 msgcache_put (char *body, int transfer, LPMESSAGE message)
198 {
199   HRESULT hr;
200   LPSPropValue lpspvFEID = NULL;
201   cache_item_t item;
202   size_t keylen;
203   void *key;
204   void *refhandle;
205
206   if (!message)
207     return; /* No message: Nop. */
208
209   hr = HrGetOneProp ((LPMAPIPROP)message, PR_CONVERSATION_INDEX, &lpspvFEID);
210   if (FAILED (hr))
211     {
212       log_debug ("%s: HrGetOneProp failed: hr=%#lx\n", __func__, hr);
213       return;
214     }
215     
216   if ( PROP_TYPE (lpspvFEID->ulPropTag) != PT_BINARY )
217     {
218       log_debug ("%s: HrGetOneProp returned unexpected property type\n",
219                  __func__);
220       MAPIFreeBuffer (lpspvFEID);
221       return;
222     }
223   keylen = lpspvFEID->Value.bin.cb;
224   key = lpspvFEID->Value.bin.lpb;
225
226   if (!keylen || !key || keylen > 100)
227     {
228       log_debug ("%s: malformed ConversationIndex\n", __func__);
229       MAPIFreeBuffer (lpspvFEID);
230       return;
231     }
232
233   if (msgcache_get (key, keylen, &refhandle) )
234     {
235       /* Update an existing cache item. We know that refhandle is
236          actually the item, thus we can simply change it here. The
237          reference counting makes sure that no other thread will steal
238          it while we are updating.  But we still need to lock. */
239       log_hexdump (key, keylen, "%s: updating key: ", __func__);
240       item = refhandle;
241       if (!lock_cache ())
242         {
243           xfree (item->plaintext);
244           item->plaintext = transfer? body : xstrdup (body);
245           item->length = strlen (body);
246         }
247       else /* Oops - locking failed:  Cleanup */
248         {
249           if (transfer)
250             xfree (body);
251         }
252
253       msgcache_unref (refhandle);
254       MAPIFreeBuffer (lpspvFEID);
255     }
256   else
257     {
258       /* Create a new cache entry. */
259       item = xmalloc (sizeof *item + keylen - 1);
260       item->next = NULL;
261       item->ref = 0;
262       item->plaintext = transfer? body : xstrdup (body);
263       item->length = strlen (body);
264       item->keylen = keylen;
265       memcpy (item->key, key, keylen);
266       log_hexdump (key, keylen, "%s: new cache key: ", __func__);
267       
268       MAPIFreeBuffer (lpspvFEID);
269       
270       if (!lock_cache ())
271         {
272           flush_if_needed (item->length);
273           item->next = the_cache;
274           the_cache = item;
275           unlock_cache ();
276         }
277     }
278 }
279
280
281
282 /* Locate a plaintext stored under KEY of length KEYLEN and return it.
283    The user must provide the address of a void pointer variable which
284    he later needs to pass to the msgcache_unref. Returns NULL if no
285    plaintext is available; msgcache_unref is then not required but
286    won't harm either. */
287 const char *
288 msgcache_get (const void *key, size_t keylen, void **refhandle)
289 {
290   cache_item_t item;
291   const char *result = NULL;
292
293   *refhandle = NULL;
294
295   if (!key || !keylen)
296     ; /* No key: Nop. */
297   else if (!the_cache)
298     ; /* No cache: avoid needless work. */
299   else if (!lock_cache ())
300     {
301       for (item = the_cache; item; item = item->next)
302         {
303           if (keylen >= item->keylen 
304               && !memcmp (key, item->key, item->keylen))
305             {
306               item->ref++;
307               result = item->plaintext; 
308               *refhandle = item;
309               break;
310             }
311         }
312       unlock_cache ();
313     }
314
315   log_hexdump (key, keylen, "%s: cache %s for key: ",
316                __func__, result? "hit":"miss");
317   return result;
318 }
319
320
321 /* Locate a plaintext stored for the mapi MESSSAGE and return it.  The
322    user must provide the address of a void pointer variable which he
323    later needs to pass to the msgcache_unref. Returns NULL if no
324    plaintext is available; msgcache_unref is then not required but
325    won't harm either. */
326 const char *
327 msgcache_get_from_mapi (LPMESSAGE message, void **refhandle)
328 {
329   HRESULT hr;
330   LPSPropValue lpspvFEID = NULL;
331   const char *result = NULL;
332
333   *refhandle = NULL;
334
335   if (!message)
336     return NULL; 
337
338   hr = HrGetOneProp ((LPMAPIPROP)message, PR_CONVERSATION_INDEX, &lpspvFEID);
339   if (FAILED (hr))
340     {
341       log_debug ("%s: HrGetOneProp failed: hr=%#lx\n", __func__, hr);
342       return NULL;
343     }
344     
345   if ( PROP_TYPE (lpspvFEID->ulPropTag) != PT_BINARY )
346     {
347       log_debug ("%s: HrGetOneProp returned unexpected property type\n",
348                  __func__);
349       MAPIFreeBuffer (lpspvFEID);
350       return NULL;
351     }
352   result = msgcache_get (lpspvFEID->Value.bin.lpb, lpspvFEID->Value.bin.cb,
353                          refhandle);
354   MAPIFreeBuffer (lpspvFEID);
355   return result;
356 }
357
358
359 /* Release access to a value returned by msgcache_get.  REFHANDLE is
360    the value as stored in the pointer variable by msgcache_get. */
361 void
362 msgcache_unref (void *refhandle)
363 {
364   cache_item_t item;
365
366   if (!refhandle)
367     return;
368
369   if (!lock_cache ())
370     {
371       for (item = the_cache; item; item = item->next)
372         {
373           if (item == refhandle)
374             {
375               if (item->ref < 1)
376                 log_error ("%s: zero reference count for item %p\n",
377                            __func__, item);
378               else
379                 item->ref--;
380               /* We could here check whether this one has been
381                  scheduled for removal.  However, I don't think this
382                  is really required, we just wait for the next new
383                  message which will then remove all pending ones. */
384               break;
385             }
386         }
387       unlock_cache ();
388       if (!item)
389         log_error ("%s: invalid reference handle %p detected\n",
390                    __func__, refhandle);
391     }
392 }