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