1 # include <vector>
2 # include <iostream>
3 # include <atomic>
4 # include <fstream>
5 
6 # include <boost/filesystem.hpp>
7 
8 # include <glib.h>
9 # include <gmime/gmime.h>
10 # include "utils/gmime/gmime-compat.h"
11 # include "utils/gmime/gmime-filter-html-bq.h"
12 
13 # include "astroid.hh"
14 # include "message_thread.hh"
15 # include "chunk.hh"
16 # include "utils/utils.hh"
17 # include "utils/ustring_utils.hh"
18 # include "utils/vector_utils.hh"
19 # include "config.hh"
20 # include "crypto.hh"
21 
22 
23 namespace Astroid {
24 
25   std::atomic<uint> Chunk::nextid (0);
26 
Chunk(GMimeObject * mp,bool encrypted,bool _signed,refptr<Crypto> _cr)27   Chunk::Chunk (GMimeObject * mp, bool encrypted, bool _signed, refptr<Crypto> _cr) : mime_object (mp) {
28     id = nextid++;
29 
30     isencrypted = encrypted;
31     issigned    = _signed;
32     crypt       = _cr;
33 
34     ustring pts = astroid->config().get<std::string>("thread_view.preferred_type");
35     if (pts != "plain" && pts != "html") {
36       LOG (error) << "chunk: preferred type not 'html' or 'plain', setting to 'plain'.";
37       pts = "plain";
38     }
39     preferred_type = viewable_types[pts];
40 
41     if (mp == NULL) {
42       LOG (error) << "chunk (" << id << "): got NULL mime_object.";
43 
44       viewable   = true;
45       attachment = false;
46 
47     } else {
48       g_object_ref (mime_object);
49     }
50 
51     content_type = g_mime_object_get_content_type (mime_object);
52 
53     if (content_type) {
54       LOG (debug) << "chunk (" << id << "): content-type: " << g_mime_content_type_get_mime_type (content_type);
55     } else {
56       LOG (warn) << "chunk (" << id << "): content-type not specified, could be mime-message.";
57     }
58 
59     if (GMIME_IS_PART (mime_object)) {
60       // has no sub-parts
61 
62       std::string disposition = g_mime_object_get_disposition(mime_object) ? : std::string();
63       viewable = !(disposition == "attachment");
64 
65       const char * cid = g_mime_part_get_content_id ((GMimePart *) mime_object);
66       if (cid != NULL) {
67         content_id = ustring(cid);
68         LOG (debug) << "chunk: part, id: " << content_id;
69       }
70 
71       if (content_type != NULL) {
72         if (viewable) {
73           /* check if we can show this type */
74           viewable = false;
75 
76           for (auto &m : viewable_types) {
77             if (g_mime_content_type_is_type (content_type,
78                   g_mime_content_type_get_media_type (m.second),
79                   g_mime_content_type_get_media_subtype (m.second))) {
80 
81               viewable = true;
82               break;
83             }
84           }
85         }
86       } else {
87         viewable = false;
88       }
89 
90       attachment = !viewable;
91 
92       if (g_mime_content_type_is_type (content_type,
93           g_mime_content_type_get_media_type (preferred_type),
94           g_mime_content_type_get_media_subtype (preferred_type)))
95       {
96         LOG (debug) << "chunk: preferred.";
97         preferred = true;
98       }
99 
100       LOG (debug) << "chunk: is part (viewable: " << viewable << ", attachment: " << attachment << ") ";
101 
102       /* TODO: check for inline PGP encryption, though it may be unsafe:
103        *       https://dkg.fifthhorseman.net/notes/inline-pgp-harmful/
104        *
105        * One way to do this is by converting the inline PGP to PGP/MIME:
106        *
107        * Fetch the encrypted part out of the message, make a multipart and
108        * add the parts of the inline message there, making the encrypted part
109        * a multipartencrypted. Then add this multipart as child, and make this
110        * part unviwable and not attachment.
111        *
112        * That should preserve the information about what parts are encrypted,
113        * and which are not.
114        *
115        */
116 
117     } else if GMIME_IS_MESSAGE_PART (mime_object) {
118       LOG (debug) << "chunk: message part";
119 
120       /* contains a GMimeMessage with a potential substructure */
121       GMimeMessage * msg = g_mime_message_part_get_message ((GMimeMessagePart *) mime_object);
122       kids.push_back (refptr<Chunk>(new Chunk((GMimeObject *) msg)));
123 
124     } else if GMIME_IS_MESSAGE_PARTIAL (mime_object) {
125       LOG (debug) << "chunk: partial";
126 
127       GMimeMessage * msg = g_mime_message_partial_reconstruct_message (
128           (GMimeMessagePartial **) &mime_object,
129           g_mime_message_partial_get_total ((GMimeMessagePartial *) mime_object)
130           );
131 
132       kids.push_back (refptr<Chunk>(new Chunk((GMimeObject *) msg)));
133 
134 
135     } else if GMIME_IS_MULTIPART (mime_object) {
136       LOG (debug) << "chunk: multi part";
137 
138       int total = g_mime_multipart_get_count ((GMimeMultipart *) mime_object);
139 
140       if (GMIME_IS_MULTIPART_ENCRYPTED (mime_object) || GMIME_IS_MULTIPART_SIGNED (mime_object)) {
141 
142         /* inline PGP is handled in GMIME_IS_PART () above */
143 
144         ustring protocol = "";
145         const char * _protocol = g_mime_content_type_get_parameter (content_type, "protocol");
146         if (_protocol != NULL) protocol = _protocol;
147         crypt = refptr<Crypto> (new Crypto (protocol));
148         if (!crypt->ready) {
149           LOG (error) << "chunk: no crypto ready.";
150         }
151       }
152 
153       if (GMIME_IS_MULTIPART_ENCRYPTED (mime_object) && crypt->ready) {
154           LOG (warn) << "chunk: is encrypted.";
155           isencrypted = true;
156 
157           if (total != 2) {
158             LOG (error) << "chunk: encrypted message with not exactly 2 parts.";
159             return;
160           }
161 
162           GMimeObject * k = crypt->decrypt_and_verify (mime_object);
163 
164           if (k != NULL) {
165             auto c = refptr<Chunk>(new Chunk(k, true, crypt->verify_tried, crypt));
166             kids.push_back (c);
167           } else {
168             /* will be displayed as failed decrypted part */
169             viewable = true;
170             preferred = true;
171 
172           }
173 
174       } else if (GMIME_IS_MULTIPART_SIGNED (mime_object) && crypt->ready) {
175           LOG (warn) << "chunk: is signed.";
176           issigned = true;
177 
178           /* only show first part */
179           GMimeObject * mo = g_mime_multipart_get_part (
180               (GMimeMultipart *) mime_object,
181               0);
182 
183           crypt->verify_signature (mime_object);
184 
185           auto c = refptr<Chunk>(new Chunk(mo, false, true, crypt));
186           kids.push_back (c);
187 
188       } else {
189 
190         bool alternative = (g_mime_content_type_is_type (content_type, "multipart", "alternative"));
191         LOG (debug) << "chunk: alternative: " << alternative;
192 
193 
194         for (int i = 0; i < total; i++) {
195           GMimeObject * mo = g_mime_multipart_get_part (
196               (GMimeMultipart *) mime_object,
197               i);
198 
199           auto c = refptr<Chunk>(new Chunk(mo, isencrypted, issigned, crypt));
200           kids.push_back (c);
201         }
202 
203         if (alternative) {
204           for_each (
205               kids.begin(),
206               kids.end(),
207               [&] (refptr<Chunk> c) {
208                 for_each (
209                     kids.begin(),
210                     kids.end(),
211                     [&] (refptr<Chunk> cc) {
212                       if (c != cc) {
213                         LOG (debug) << "chunk: multipart: added sibling";
214                         c->siblings.push_back (cc);
215                       }
216                     }
217                   );
218 
219                 if (g_mime_content_type_is_type (c->content_type,
220                     g_mime_content_type_get_media_type (preferred_type),
221                     g_mime_content_type_get_media_subtype (preferred_type)))
222                 {
223                   LOG (debug) << "chunk: multipart: preferred.";
224                   c->preferred = true;
225                 }
226               }
227             );
228         }
229       }
230 
231       LOG (debug) << "chunk: multi part end";
232 
233     } else if GMIME_IS_MESSAGE (mime_object) {
234       LOG (debug) << "chunk: mime message";
235 
236       mime_message = true;
237     }
238 
239   }
240 
is_content_type(const char * major,const char * minor)241   bool Chunk::is_content_type (const char * major, const char * minor) {
242     return (mime_object != NULL) && g_mime_content_type_is_type (content_type, major, minor);
243   }
244 
viewable_text(bool html=true,bool verbose)245   ustring Chunk::viewable_text (bool html = true, bool verbose) {
246     if (isencrypted && !crypt->decrypted) {
247       if (verbose) {
248       /* replace newlines */
249       ustring err = UstringUtils::replace (crypt->decrypt_error, "\n", "<br />");
250 
251 
252       return "Failed decryption: <br /><br /><div class=\"gpg_error\">" + err + "</div>";
253 
254       } else {
255         return ""; // for reply
256       }
257     }
258 
259     GMimeStream * content_stream = NULL;
260 
261     if (mime_object != NULL && GMIME_IS_PART(mime_object)) {
262       LOG (debug) << "chunk: body: part";
263 
264 
265       if (is_content_type ("text", "plain")) {
266         LOG (debug) << "chunk: plain text (out html: " << html << ")";
267 
268         GMimeDataWrapper * content = g_mime_part_get_content (
269             (GMimePart *) mime_object);
270 
271         const char * charset = g_mime_object_get_content_type_parameter(GMIME_OBJECT(mime_object), "charset");
272         GMimeStream * stream = g_mime_data_wrapper_get_stream (content);
273 
274         GMimeStream * filter_stream = g_mime_stream_filter_new (stream);
275 
276         /* convert to html */
277         guint32 cite_color = 0x1e1e1e;
278 
279         /* other filters:
280          *
281          * GMIME_FILTER_HTML_PRE ||
282          */
283         guint32 html_filter_flags = GMIME_FILTER_HTML_CONVERT_NL |
284                                     GMIME_FILTER_HTML_CONVERT_SPACES |
285                                     GMIME_FILTER_HTML_CONVERT_URLS |
286                                     GMIME_FILTER_HTML_CONVERT_ADDRESSES |
287                                     GMIME_FILTER_HTML_BQ_BLOCKQUOTE_CITATION ;
288 
289         /* convert encoding */
290         GMimeContentEncoding enc = g_mime_data_wrapper_get_encoding (content);
291         if (enc) {
292           LOG (debug) << "enc: " << g_mime_content_encoding_to_string(enc);
293         }
294 
295         GMimeFilter * filter = g_mime_filter_basic_new(enc, false);
296         g_mime_stream_filter_add(GMIME_STREAM_FILTER(filter_stream), filter);
297         g_object_unref(filter);
298 
299         if (charset) {
300           LOG (debug) << "charset: " << charset;
301           if (std::string(charset) == "utf-8") {
302             charset = "UTF-8";
303           }
304 
305           GMimeFilter * filter = g_mime_filter_charset_new(charset, "UTF-8");
306           g_mime_stream_filter_add(GMIME_STREAM_FILTER(filter_stream), filter);
307           g_object_unref(filter);
308         } else {
309           LOG (warn) << "charset: not defined.";
310         }
311 
312         if (html) {
313 
314           GMimeFilter * html_filter;
315           html_filter = g_mime_filter_html_bq_new (html_filter_flags, cite_color);
316           g_mime_stream_filter_add (GMIME_STREAM_FILTER(filter_stream),
317                                   html_filter);
318           g_object_unref (html_filter);
319 
320         } else {
321 
322           /* CRLF to LF */
323           GMimeFilter * crlf_filter = g_mime_filter_dos2unix_new (false);
324           g_mime_stream_filter_add (GMIME_STREAM_FILTER (filter_stream),
325               crlf_filter);
326           g_object_unref (crlf_filter);
327 
328         }
329 
330         g_mime_stream_reset (stream);
331 
332         content_stream = filter_stream;
333 
334       } else if (is_content_type ("text", "html")) {
335         LOG (debug) << "chunk: html text";
336 
337         GMimeDataWrapper * content = g_mime_part_get_content (
338             (GMimePart *) mime_object);
339 
340         const char * charset = g_mime_object_get_content_type_parameter(GMIME_OBJECT(mime_object), "charset");
341         GMimeStream * stream = g_mime_data_wrapper_get_stream (content);
342 
343         GMimeStream * filter_stream = g_mime_stream_filter_new (stream);
344 
345         /* convert encoding */
346         GMimeContentEncoding enc = g_mime_data_wrapper_get_encoding (content);
347         if (enc) {
348           LOG (debug) << "enc: " << g_mime_content_encoding_to_string(enc);
349         }
350 
351         GMimeFilter * filter = g_mime_filter_basic_new(enc, false);
352         g_mime_stream_filter_add(GMIME_STREAM_FILTER(filter_stream), filter);
353         g_object_unref(filter);
354 
355         if (charset)
356         {
357           LOG (debug) << "charset: " << charset;
358           if (std::string(charset) == "utf-8") {
359             charset = "UTF-8";
360           }
361 
362           GMimeFilter * filter = g_mime_filter_charset_new(charset, "UTF-8");
363           g_mime_stream_filter_add(GMIME_STREAM_FILTER(filter_stream), filter);
364           g_object_unref(filter);
365         } else {
366           LOG (warn) << "charset: not defined";
367         }
368 
369 
370 
371         g_mime_stream_reset (stream);
372 
373         content_stream = filter_stream;
374       }
375     }
376 
377     if (content_stream != NULL) {
378       char buffer[4097];
379       ssize_t prevn = 1;
380       ssize_t n;
381       std::stringstream sstr;
382 
383       while ((n = g_mime_stream_read (content_stream, buffer, 4096), n) >= 0)
384       {
385         buffer[n] = 0;
386         sstr << buffer;
387 
388         if (n == 0 && prevn == 0) {
389           break;
390         }
391 
392         prevn = n;
393       }
394 
395       g_object_unref (content_stream);
396 
397       ustring b;
398       try {
399         b = sstr.str();
400       } catch (Glib::ConvertError &ex) {
401         LOG (error) << "could not convert chunk to utf-8, contents: " << sstr.str();
402         throw ex;
403       }
404 
405 
406       return b;
407     } else {
408       return ustring ("Error: Non-viewable part!");
409       LOG (error) << "chunk: tried to display non-viewable part.";
410       //throw runtime_error ("chunk: tried to display non-viewable part.");
411     }
412   }
413 
get_filename()414   ustring Chunk::get_filename () {
415     if (_fname.size () > 0) {
416       return _fname;
417     }
418 
419     if (GMIME_IS_PART (mime_object)) {
420       const char * s = g_mime_part_get_filename (GMIME_PART(mime_object));
421 
422       if (s != NULL) {
423         ustring fname (s);
424         _fname = fname;
425         return fname;
426       }
427     } else if (GMIME_IS_MESSAGE (mime_object)) {
428       const char * s = g_mime_message_get_subject (GMIME_MESSAGE (mime_object));
429 
430       if (s != NULL) {
431         ustring fname (s);
432         _fname = fname + ".eml";
433         return fname;
434       }
435     }
436     // no filename specified
437     return ustring ("");
438   }
439 
get_file_size()440   size_t Chunk::get_file_size () {
441     time_t t0 = clock ();
442 
443     // https://github.com/skx/lumail/blob/master/util/attachments.c
444 
445     refptr<Glib::ByteArray> cnt = contents ();
446     size_t sz = cnt->size ();
447 
448     LOG (info) << "chunk: file size: " << sz << " (time used to calculate: " << ( (clock () - t0) * 1000.0 / CLOCKS_PER_SEC ) << " s.)";
449 
450     return sz;
451   }
452 
contents()453   refptr<Glib::ByteArray> Chunk::contents () {
454     time_t t0 = clock ();
455 
456     // https://github.com/skx/lumail/blob/master/util/attachments.c
457 
458     GMimeStream * mem = g_mime_stream_mem_new ();
459 
460     if (GMIME_IS_PART (mime_object)) {
461 
462       GMimeDataWrapper * content = g_mime_part_get_content (GMIME_PART (mime_object));
463 
464       g_mime_data_wrapper_write_to_stream (content, mem);
465 
466     } else {
467 
468       g_mime_object_write_to_stream (mime_object, NULL, mem);
469       g_mime_stream_flush (mem);
470 
471     }
472 
473     GByteArray * res = g_mime_stream_mem_get_byte_array (GMIME_STREAM_MEM (mem));
474 
475     auto data = Glib::ByteArray::create ();
476     if (res != NULL) {
477       data->append (res->data, res->len);
478     }
479 
480     g_object_unref (mem);
481 
482     LOG (info) << "chunk: contents: loaded " << data->size () << " bytes in " << ( (clock () - t0) * 1000.0 / CLOCKS_PER_SEC ) << " ms.";
483 
484     return data;
485   }
486 
save_to(std::string filename,bool overwrite)487   bool Chunk::save_to (std::string filename, bool overwrite) {
488     /* saves chunk to file name, if filename is dir, own name */
489     using bfs::path;
490 
491     path to (filename.c_str());
492 
493     if (is_directory (to)) {
494       ustring fname = Utils::safe_fname (get_filename ());
495 
496       if (fname.size () == 0) {
497         if (content_id != "") {
498           fname = ustring::compose ("astroid-attachment-%1", content_id);
499         } else {
500           /* make up a name */
501           path new_to;
502 
503           do {
504             fname = ustring::compose ("astroid-attachment-%1", UstringUtils::random_alphanumeric (5));
505 
506             new_to = to / path(fname.c_str ());
507           } while (exists (new_to));
508         }
509       }
510 
511       to /= path (fname.c_str ());
512     }
513 
514     LOG (info) << "chunk: saving to: " << to;
515 
516     if (exists (to)) {
517       if (!overwrite) {
518         LOG (error) << "chunk: save: file already exists! not writing.";
519         return false;
520       } else {
521         LOG (warn) << "chunk: save: file already exists: overwriting.";
522       }
523     }
524 
525     if (!exists(to.parent_path ()) || !is_directory (to.parent_path())) {
526       LOG (error) << "chunk: save: parent path does not exist or is not a directory.";
527       return false;
528     }
529 
530     std::ofstream f (to.c_str (), std::ofstream::binary);
531 
532     auto data = contents ();
533     f.write (reinterpret_cast<char*>(data->get_data ()), data->size ());
534 
535     f.close ();
536 
537     return true;
538   }
539 
get_by_id(int _id,bool check_siblings)540   refptr<Chunk> Chunk::get_by_id (int _id, bool check_siblings) {
541     if (check_siblings) {
542       for (auto c : siblings) {
543         if (c->id == _id) {
544           return c;
545         } else {
546           auto kc = c->get_by_id (_id, false);
547           if (kc) return kc;
548         }
549       }
550     }
551 
552     for (auto c : kids) {
553       if (c->id == _id) {
554         return c;
555       } else {
556         auto kc = c->get_by_id (_id, true);
557         if (kc) return kc;
558       }
559     }
560 
561     return refptr<Chunk>();
562   }
563 
open()564   void Chunk::open () {
565     using bfs::path;
566     LOG (info) << "chunk: " << get_filename () << ", opening..";
567 
568     path tf = astroid->standard_paths().cache_dir;
569 
570     ustring tmp_fname = ustring::compose("%1-%2", UstringUtils::random_alphanumeric (10), Utils::safe_fname(get_filename ()));
571     tf /= path (tmp_fname.c_str());
572 
573     LOG (debug) << "chunk: saving to tmp path: " << tf.c_str();
574     save_to (tf.c_str());
575 
576     ustring tf_p (tf.c_str());
577 
578     Glib::Threads::Thread::create (
579         sigc::bind (
580           sigc::mem_fun (this, &Chunk::do_open),
581           tf_p ));
582   }
583 
do_open(ustring tf)584   void Chunk::do_open (ustring tf) {
585     ustring external_cmd = astroid->config().get<std::string> ("attachment.external_open_cmd");
586 
587     std::vector<std::string> args = { external_cmd.c_str(), tf.c_str () };
588     LOG (debug) << "chunk: spawning: " << args[0] << ", " << args[1];
589     std::string stdout;
590     std::string stderr;
591     int    exitcode;
592     try {
593       Glib::spawn_sync ("",
594                         args,
595                         Glib::SPAWN_DEFAULT | Glib::SPAWN_SEARCH_PATH,
596                         sigc::slot <void> (),
597                         &stdout,
598                         &stderr,
599                         &exitcode
600                         );
601 
602     } catch (Glib::SpawnError &ex) {
603       LOG (error) << "chunk: exception while opening attachment: " <<  ex.what ();
604       LOG (info) << "chunk: deleting tmp file: " << tf;
605       unlink (tf.c_str());
606     }
607 
608     ustring ustdout = ustring(stdout);
609     for (ustring &l : VectorUtils::split_and_trim (ustdout, ustring("\n"))) {
610 
611       LOG (debug) << l;
612     }
613 
614     ustring ustderr = ustring(stderr);
615     for (ustring &l : VectorUtils::split_and_trim (ustderr, ustring("\n"))) {
616 
617       LOG (debug) << l;
618     }
619 
620     if (exitcode != 0) {
621       LOG (error) << "chunk: chunk script exited with code: " << exitcode;
622     }
623 
624     LOG (info) << "chunk: deleting tmp file: " << tf;
625     unlink (tf.c_str());
626   }
627 
any_kids_viewable()628   bool Chunk::any_kids_viewable () {
629     if (viewable) return true;
630 
631     for (auto &k : kids) {
632       if (k->any_kids_viewable ()) return true;
633     }
634 
635     return false;
636   }
637 
any_kids_viewable_and_preferred()638   bool Chunk::any_kids_viewable_and_preferred () {
639     if (viewable && preferred) return true;
640 
641     for (auto &k : kids) {
642       if (k->any_kids_viewable_and_preferred ()) return true;
643     }
644 
645     return false;
646   }
647 
get_content_type()648   ustring Chunk::get_content_type () {
649     if (content_type == NULL) return "";
650     else return ustring (g_mime_content_type_get_mime_type (content_type));
651   }
652 
save()653   void Chunk::save () {
654     LOG (info) << "chunk: " << get_filename () << ", saving..";
655     Gtk::FileChooserDialog dialog ("Save attachment to folder..",
656         Gtk::FILE_CHOOSER_ACTION_SAVE);
657 
658     dialog.add_button ("_Cancel", Gtk::RESPONSE_CANCEL);
659     dialog.add_button ("_Select", Gtk::RESPONSE_OK);
660 
661     dialog.set_do_overwrite_confirmation (true);
662     dialog.set_current_name (Utils::safe_fname (get_filename ()));
663     dialog.set_current_folder (astroid->runtime_paths ().save_dir.c_str ());
664 
665     int result = dialog.run ();
666 
667     switch (result) {
668       case (Gtk::RESPONSE_OK):
669         {
670           std::string fname = dialog.get_filename ();
671           LOG (info) << "chunk: saving attachment to: " << fname;
672 
673           /* the dialog asks whether to overwrite or not */
674           save_to (fname, true);
675 
676           astroid->runtime_paths ().save_dir = bfs::path (dialog.get_current_folder ());
677 
678           break;
679         }
680 
681       default:
682         {
683           LOG (debug) << "chunk: save: cancelled.";
684         }
685     }
686   }
687 
get_mime_message()688   refptr<Message> Chunk::get_mime_message () {
689     if (!mime_message) {
690       LOG (error) << "chunk: this is not a mime message.";
691       throw std::runtime_error ("chunk: not a mime message");
692     }
693 
694     refptr<Message> m = refptr<Message> ( new Message (GMIME_MESSAGE(mime_object)) );
695 
696     return m;
697   }
698 
~Chunk()699   Chunk::~Chunk () {
700     LOG (debug) << "chunk: deconstruct.";
701     // these should not be unreffed.
702     if (mime_object) g_object_unref (mime_object);
703     // g_object_unref (content_type);
704   }
705 }
706 
707