1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /*
3  * Pan - A Newsreader for Gtk+
4  * Copyright (C) 2002-2006  Charles Kerr <charles@rebelbase.com>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; version 2 of the License.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, see <http://www.gnu.org/licenses/>.
17  *
18  */
19 
20 #include <config.h>
21 
22 #include <errno.h>
23 #include <sys/types.h>
24 #include <sys/stat.h>
25 #include <unistd.h>
26 #include <dirent.h>
27 
28 #include <glib.h>
29 #include <glib/gi18n.h>
30 #include <gmime/gmime.h>
31 
32 #include <pan/general/debug.h>
33 #include <pan/general/file-util.h>
34 #include <pan/general/macros.h>
35 #include <pan/general/messages.h>
36 #include <pan/general/log.h>
37 #include <pan/general/string-view.h>
38 #include <pan/usenet-utils/mime-utils.h>
39 #include "article.h"
40 #include "article-cache.h"
41 
42 using namespace pan;
43 /**
44 * Message-IDs are transformed via message_id_to_filename()
45 * to play nicely with some filesystems, so to extract the Message-ID
46 * from a filename we need to reverse the transform.
47 *
48 * @return string length, or 0 on failure
49 */
50 
51 int
filename_to_message_id(char * buf,int len,const char * basename)52 ArticleCache :: filename_to_message_id (char * buf, int len, const char * basename)
53 {
54   const char * in;
55   char * out;
56   char * pch;
57   char tmp_basename[PATH_MAX];
58 
59   // sanity clause
60   pan_return_val_if_fail (basename && *basename, 0);
61   pan_return_val_if_fail (buf!=NULL, 0);
62   pan_return_val_if_fail (len>0, 0);
63 
64   // remove the trailing ".msg" or similar
65   g_strlcpy (tmp_basename, basename, sizeof(tmp_basename));
66 //  if ((pch = g_strrstr (tmp_basename, msg_extension.c_str())))
67 //     *pch = '\0';
68   if ((pch = g_strrstr (tmp_basename, ".")))
69      *pch = '\0';
70   g_strstrip (tmp_basename);
71 
72   // transform
73   out = buf;
74   *out++ = '<';
75   for (in=tmp_basename; *in; ++in) {
76      if (in[0]!='%' || !g_ascii_isxdigit(in[1]) || !g_ascii_isxdigit(in[2]))
77         *out++ = *in;
78      else {
79         char buf[3];
80         buf[0] = *++in;
81         buf[1] = *++in;
82         buf[2] = '\0';
83         *out++ = (char) strtoul (buf, NULL, 16);
84      }
85   }
86   *out++ = '>';
87   *out = '\0';
88 
89   return out - buf;
90 }
91 
92 /**
93 * Some characters in message-ids don't work well in filenames,
94 * so we transform them to a safer name.
95 */
96 char*
message_id_to_filename(char * buf,int len,const StringView & mid) const97 ArticleCache :: message_id_to_filename (char * buf, int len, const StringView& mid) const
98 {
99   // sanity clause
100   pan_return_val_if_fail (!mid.empty(), 0);
101   pan_return_val_if_fail (buf!=0, NULL);
102   pan_return_val_if_fail (len>0, NULL);
103 
104   // some characters in message-ids are illegal on older Windows boxes,
105   // so we transform those illegal characters using URL encoding
106   char * out = buf;
107   for (const char *in=mid.begin(), *end=mid.end(); in!=end; ++in) {
108      switch (*in) {
109         case '%': /* this is the escape character */
110         case '"': case '*': case '/': case ':': case '?': case '|':
111         case '\\': /* these are illegal on vfat, fat32 */
112            g_snprintf (out, len-(out-buf), "%%%02x", (int)*in);
113            out += 3;
114            break;
115         case '<': case '>': /* these are illegal too, but rather than encoding
116                                them, follow the convention of omitting them */
117            break;
118         default:
119            *out++ = *in;
120            break;
121      }
122   }
123 
124   // add the filename extension
125   g_snprintf (out, len-(out-buf), ".%s", msg_extension.c_str());
126 
127   return buf;
128 }
129 
ArticleCache(const StringView & path,const StringView & extension,size_t max_megs)130 ArticleCache :: ArticleCache (const StringView& path, const StringView& extension, size_t max_megs):
131    msg_extension(extension),
132    _path (path.str, path.len),
133    _max_megs (max_megs),
134    _current_bytes (0ul)
135 {
136 
137    GError * err = NULL;
138    GDir * dir = g_dir_open (_path.c_str(), 0, &err);
139    if (err != NULL)
140    {
141       Log::add_err_va (_("Error opening directory: “%s”: %s"), _path.c_str(), err->message);
142       g_clear_error (&err);
143    }
144    else
145    {
146       char filename[PATH_MAX];
147       const char * fname;
148       while ((fname = g_dir_read_name (dir)))
149       {
150          struct stat stat_p;
151          g_snprintf (filename, sizeof(filename), "%s%c%s", _path.c_str(), G_DIR_SEPARATOR, fname);
152          if (!stat (filename, &stat_p))
153          {
154             char str[2048];
155             const int len (filename_to_message_id (str, sizeof(str), fname));
156             if (len != 0)
157             {
158                MsgInfo info;
159                info._message_id = StringView (str, len);
160                info._size = stat_p.st_size;
161                info._date = stat_p.st_mtime;
162                _current_bytes += info._size;
163                _mid_to_info.insert (mid_to_info_t::value_type (info._message_id, info));
164             }
165          }
166       }
167       g_dir_close (dir);
168       debug ("loaded " << _mid_to_info.size() << " articles into cache from " << _path);
169    }
170 }
171 
~ArticleCache()172 ArticleCache :: ~ArticleCache ()
173 {
174 }
175 
176 /*****
177 ******
178 *****/
179 
180 void
fire_added(const Quark & mid)181 ArticleCache :: fire_added (const Quark& mid)
182 {
183   for (listeners_t::iterator it(_listeners.begin()), end(_listeners.end()); it!=end; )
184     (*it++)->on_cache_added (mid);
185 }
186 
187 void
fire_removed(const quarks_t & mids)188 ArticleCache :: fire_removed (const quarks_t& mids)
189 {
190   for (listeners_t::iterator it(_listeners.begin()), end(_listeners.end()); it!=end; )
191     (*it++)->on_cache_removed (mids);
192 }
193 
194 /*****
195 ******
196 *****/
197 
198 bool
contains(const Quark & mid) const199 ArticleCache :: contains (const Quark& mid) const
200 {
201   return _mid_to_info.find (mid) != _mid_to_info.end();
202 }
203 
204 char*
get_filename(char * buf,int buflen,const Quark & mid) const205 ArticleCache :: get_filename (char * buf, int buflen, const Quark& mid) const
206 {
207    char basename[PATH_MAX];
208    *buf = '\0';
209    message_id_to_filename (basename, sizeof(basename), mid.to_view());
210    g_snprintf (buf, buflen, "%s%c%s", _path.c_str(), G_DIR_SEPARATOR, basename);
211    return buf && *buf ? buf : 0;
212 };
213 
214 ArticleCache :: CacheResponse
add(const Quark & message_id,const StringView & article,const bool virtual_file)215 ArticleCache :: add (const Quark& message_id, const StringView& article, const bool virtual_file)
216 {
217   debug ("adding " << message_id << ", which is " << article.len << " bytes long");
218 
219   CacheResponse res;
220   res.type = CACHE_IO_ERR;
221 
222   pan_return_val_if_fail (!message_id.empty(), res);
223   pan_return_val_if_fail (!article.empty(), res);
224 
225   FILE * fp = 0;
226   char filename[PATH_MAX];
227   if (get_filename (filename, sizeof(filename), message_id))
228     fp = fopen (filename, "wb+");
229 
230   if (!fp)
231   {
232       Log::add_err_va (_("Unable to save “%s” %s"),
233                        filename, file::pan_strerror(errno));
234       res.type = CACHE_IO_ERR;
235   }
236   else
237   {
238     const size_t bytes_written (fwrite (article.str, sizeof(char), article.len, fp));
239     if (bytes_written < article.len)
240     {
241       Log::add_err_va (_("Unable to save “%s” %s"),
242                        filename, file::pan_strerror(errno));
243       if (errno ==  ENOSPC || errno == ENOMEM)
244       {
245           res.type = CACHE_DISK_FULL;
246       }
247     }
248     else
249     {
250       MsgInfo info;
251       info._message_id = message_id;
252       info._size = article.len;
253       info._date = time(0);
254       _mid_to_info.insert (mid_to_info_t::value_type (info._message_id, info));
255       fire_added (message_id);
256 
257       _current_bytes += info._size;
258       if (virtual_file) ++_locks[message_id];
259       resize ();
260       res.type = CACHE_OK;
261     }
262     fclose (fp);
263   }
264   return res;
265 }
266 
267 /***
268 ****
269 ***/
270 
271 void
reserve(const mid_sequence_t & mids)272 ArticleCache :: reserve (const mid_sequence_t& mids)
273 {
274   foreach_const (mid_sequence_t, mids, it)
275     ++_locks[*it];
276 }
277 
278 void
release(const mid_sequence_t & mids)279 ArticleCache :: release (const mid_sequence_t& mids)
280 {
281   foreach_const (mid_sequence_t, mids, it)
282     if (!--_locks[*it])
283       _locks.erase (*it);
284 }
285 
286 /***
287 ****
288 ***/
289 
290 void
resize()291 ArticleCache :: resize ()
292 {
293   // let's shrink it to 80% of the maximum size
294   const double buffer_zone (0.8);
295   guint64 max_bytes (_max_megs * 1024 * 1024);
296   max_bytes = (guint64) ((double)max_bytes * buffer_zone);
297   resize (max_bytes);
298 }
299 
300 void
clear()301 ArticleCache :: clear ()
302 {
303   resize (0);
304 }
305 
306 void
resize(guint64 max_bytes)307 ArticleCache :: resize (guint64 max_bytes)
308 {
309   quarks_t removed;
310   if (_current_bytes > max_bytes)
311   {
312     // sort from oldest to youngest
313     typedef std::set<MsgInfo, MsgInfoCompare> sorted_info_t;
314     sorted_info_t si;
315     for (mid_to_info_t::const_iterator it=_mid_to_info.begin(), end=_mid_to_info.end(); it!=end; ++it)
316       si.insert (it->second);
317 
318     // start blowing away files
319     for (sorted_info_t::const_iterator it=si.begin(), end=si.end(); _current_bytes>max_bytes && it!=end; ++it) {
320       const Quark& mid (it->_message_id);
321       if (_locks.find(mid) == _locks.end()) {
322         char buf[PATH_MAX];
323         get_filename (buf, sizeof(buf), mid);
324         unlink (buf);
325         _current_bytes -= it->_size;
326         removed.insert (mid);
327         debug ("removing [" << mid << "] as we resize the queue");
328         _mid_to_info.erase (mid);
329       }
330     }
331   }
332 
333   debug ("cache expired " << removed.size() << " articles, "
334          "has " << _mid_to_info.size() << " active "
335          "and " << _locks.size() << " locked.");
336 
337   if (!removed.empty())
338     fire_removed (removed);
339 }
340 
341 /****
342 *****
343 *****  Getting Messages
344 *****
345 ****/
346 
347 /*private*/ GMimeStream*
get_message_file_stream(const Quark & mid) const348 ArticleCache :: get_message_file_stream (const Quark& mid) const
349 {
350    GMimeStream * retval = NULL;
351 
352    /* open the file */
353    char filename[PATH_MAX];
354    if (get_filename (filename, sizeof(filename), mid))
355    {
356       errno = 0;
357       FILE * fp = fopen (filename, "rb");
358       if (!fp)
359          Log::add_err_va (_("Error opening file “%s” %s"), filename, file::pan_strerror(errno));
360       else {
361          GMimeStream * file_stream = g_mime_stream_file_new (fp);
362          retval = g_mime_stream_buffer_new (file_stream, GMIME_STREAM_BUFFER_BLOCK_READ);
363          g_object_unref (file_stream);
364       }
365    }
366 
367    debug ("file stream for " << mid << ": " << retval);
368    return retval;
369 }
370 
371 /*private*/ GMimeStream*
get_message_mem_stream(const Quark & mid) const372 ArticleCache :: get_message_mem_stream (const Quark& mid) const
373 {
374    debug ("mem stream got quark " << mid);
375    GMimeStream * retval (0);
376 
377    char filename[PATH_MAX];
378    if (get_filename (filename, sizeof(filename), mid))
379    {
380       debug ("mem stream loading filename " << filename);
381       gsize len (0);
382       char * buf (0);
383       GError * err (0);
384 
385       if (g_file_get_contents (filename, &buf, &len, &err)) {
386          debug ("got the contents, calling mem_new_with_buffer");
387          retval = g_mime_stream_mem_new_with_buffer (buf, len);
388          g_free (buf);
389       } else {
390          Log::add_err_va (_("Error reading file “%s”: %s"), filename, err->message);
391          g_clear_error (&err);
392       }
393    }
394 
395    debug ("mem stream for " << mid << ": " << retval);
396    return retval;
397 }
398 
399 #ifdef HAVE_GMIME_CRYPTO
400 GMimeMessage*
get_message(const mid_sequence_t & mids,GPGDecErr & err) const401 ArticleCache :: get_message (const mid_sequence_t& mids, GPGDecErr& err) const
402 #else
403 GMimeMessage*
404 ArticleCache :: get_message (const mid_sequence_t& mids) const
405 #endif
406 {
407    debug ("trying to get a message with " << mids.size() << " parts");
408    GMimeMessage * retval = NULL;
409 
410    // load the streams
411    typedef std::vector<GMimeStream*> streams_t;
412    streams_t streams;
413    //const bool in_memory (mids.size() <= 2u);
414    const bool in_memory (true); // workaround for bug #439841
415    foreach_const (mid_sequence_t, mids, it) {
416       const Quark mid (*it);
417       GMimeStream * stream (0);
418       if (this->contains (*it))
419         stream = in_memory
420           ? get_message_mem_stream (mid)
421           : get_message_file_stream (mid);
422       if (stream)
423         streams.push_back (stream);
424    }
425 
426 
427    // build the message
428    if (!streams.empty())
429 #ifdef HAVE_GMIME_CRYPTO
430      retval = mime :: construct_message (&streams.front(), streams.size(), err);
431 #else
432      retval = mime :: construct_message (&streams.front(), streams.size());
433 #endif
434    // cleanup
435    foreach (streams_t, streams, it)
436      g_object_unref (*it);
437 
438    return retval;
439 }
440 
441 ArticleCache :: strings_t
get_filenames(const mid_sequence_t & mids)442 ArticleCache :: get_filenames (const mid_sequence_t& mids)
443 {
444   strings_t ret;
445   char filename[PATH_MAX];
446   foreach_const (mid_sequence_t, mids, it)
447     if (get_filename (filename, sizeof(filename), *it))
448     {
449       ret.push_back (filename);
450     }
451   return ret;
452 }
453