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