1 # include <iostream> 2 # include <vector> 3 # include <algorithm> 4 # include <random> 5 # include <ctime> 6 # include <memory> 7 8 # include <gtkmm.h> 9 10 # include <boost/filesystem.hpp> 11 12 # include "astroid.hh" 13 # include "config.hh" 14 # include "account_manager.hh" 15 # include "edit_message.hh" 16 # include "compose_message.hh" 17 # include "db.hh" 18 # include "thread_view/thread_view.hh" 19 # include "raw_message.hh" 20 # include "main_window.hh" 21 # include "message_thread.hh" 22 # include "chunk.hh" 23 # include "utils/utils.hh" 24 # include "utils/ustring_utils.hh" 25 # include "utils/resource.hh" 26 # include "utils/gmime/gmime-compat.h" 27 # include "actions/onmessage.hh" 28 29 # include "editor/plugin.hh" 30 # include "editor/external.hh" 31 32 using namespace std; 33 using namespace boost::filesystem; 34 35 namespace Astroid { 36 int EditMessage::edit_id = 0; 37 EditMessage(MainWindow * mw,ustring _to,ustring _from,ustring _cc,ustring _bcc,ustring _subject,ustring _body)38 EditMessage::EditMessage (MainWindow * mw, ustring _to, ustring _from, ustring _cc, ustring _bcc, ustring _subject, ustring _body) : 39 EditMessage (mw, false) { // {{{ 40 41 in_read = false; 42 to = _to; 43 cc = _cc; 44 bcc = _bcc; 45 subject = _subject; 46 body = _body; 47 if (!_from.empty ()) { 48 set_from (Address (_from)); 49 } 50 51 /* reload message */ 52 prepare_message (); 53 read_edited_message (); 54 55 edit_when_ready (); 56 } 57 EditMessage(MainWindow * mw,refptr<Message> msg)58 EditMessage::EditMessage (MainWindow * mw, refptr<Message> msg) : 59 EditMessage (mw, false) { 60 /* load draft */ 61 LOG (info) << "em: loading draft from: " << msg->fname; 62 63 /* if this is a previously saved draft: use it, otherwise make new draft 64 * based on original message */ 65 66 if (any_of (Db::draft_tags.begin (), 67 Db::draft_tags.end (), 68 [&](ustring t) { 69 return has (msg->tags, t); 70 })) 71 { 72 draft_msg = msg; 73 msg_id = msg->mid; 74 } 75 76 for (auto &c : msg->mime_messages_and_attachments ()) { 77 add_attachment (new ComposeMessage::Attachment (c)); 78 } 79 80 inreplyto = msg->inreplyto; 81 references = msg->references; 82 83 /* write msg to new tmpfile */ 84 msg->save_to (tmpfile_path.c_str ()); 85 86 /* reload message */ 87 read_edited_message (); 88 89 /* TODO: read encryption / signing / signature state from message properties */ 90 91 edit_when_ready (); 92 } 93 EditMessage(MainWindow * mw,bool _edit_when_ready)94 EditMessage::EditMessage (MainWindow * mw, bool _edit_when_ready) : Mode (mw) { 95 editor_config = astroid->config ("editor"); 96 in_read = false; 97 98 # ifndef DISABLE_EMBEDDED 99 embed_editor = !editor_config.get<bool> ("external_editor"); 100 # endif 101 save_draft_on_force_quit = editor_config.get <bool> ("save_draft_on_force_quit"); 102 103 ustring attachment_words_s = editor_config.get<string> ("attachment_words"); 104 attachment_words = VectorUtils::split_and_trim (attachment_words_s.lowercase (), ","); 105 sort (attachment_words.begin (), attachment_words.end ()); 106 107 tmpfile_path = astroid->standard_paths ().runtime_dir; 108 109 gpgenabled = astroid->config ("crypto").get<bool> ("gpg.enabled"); 110 111 set_label ("New message"); 112 113 path ui = Resource (false, "ui/edit-message.glade").get_path (); 114 115 Glib::RefPtr<Gtk::Builder> builder = Gtk::Builder::create_from_file(ui.c_str(), "box_message"); 116 117 builder->get_widget ("box_message", box_message); 118 119 builder->get_widget ("from_combo", from_combo); 120 builder->get_widget ("switch_encrypt", switch_encrypt); 121 builder->get_widget ("switch_sign", switch_sign); 122 builder->get_widget ("reply_mode_combo", reply_mode_combo); 123 builder->get_widget ("fields_revealer", fields_revealer); 124 builder->get_widget ("reply_revealer", reply_revealer); 125 builder->get_widget ("encryption_revealer", encryption_revealer); 126 127 builder->get_widget ("editor_box", editor_box); 128 builder->get_widget ("switch_signature", switch_signature); 129 builder->get_widget ("switch_markdown", switch_markdown); 130 /* 131 builder->get_widget ("editor_rev", editor_rev); 132 builder->get_widget ("thread_rev", thread_rev); 133 */ 134 135 pack_start (*box_message, true, 5); 136 137 /* set up message id and random server name for editor */ 138 id = edit_id++; 139 140 ustring _mid = ""; 141 msg_time = time(0); 142 143 # ifndef DISABLE_PLUGINS 144 if (!astroid->plugin_manager->astroid_extension->generate_mid (_mid)) { 145 # endif 146 147 char _hostname[1024]; 148 _hostname[1023] = 0; 149 gethostname (_hostname, 1023); 150 151 char _domainname[1024]; 152 _domainname[1023] = 0; 153 if (getdomainname (_domainname, 1023) < 0) { 154 *_domainname = '\0'; 155 } 156 157 ustring hostname = astroid->config ().get <string> ("mail.message_id_fqdn"); 158 UstringUtils::trim (hostname); 159 if (hostname.empty ()) { 160 if (*_hostname != 0) { 161 hostname = _hostname; 162 163 if (*_domainname != 0) { 164 ustring d (_domainname); 165 d = UstringUtils::replace (d, "(", ""); // often (none) is returned 166 d = UstringUtils::replace (d, ")", ""); 167 hostname += "."; 168 hostname += d; 169 } 170 171 if (hostname.find (".", 0) == std::string::npos) { 172 /* add a top level domain */ 173 hostname += ".none"; 174 } 175 } else { 176 hostname = UstringUtils::random_alphanumeric (10) + ".none"; 177 } 178 } 179 180 181 ustring user = astroid->config ().get<string> ("mail.message_id_user"); 182 UstringUtils::trim (user); 183 if (user.empty ()) user = "astroid"; 184 185 _mid = UstringUtils::random_alphanumeric (10); 186 187 _mid = ustring::compose ("%1.%2.%3@%4", msg_time, _mid, user, hostname); 188 189 # ifndef DISABLE_PLUGINS 190 } 191 # endif 192 193 if (msg_id == "") { 194 msg_id = _mid; 195 } 196 197 LOG (info) << "em: msg id: " << msg_id; 198 199 make_tmpfile (); 200 201 # ifndef DISABLE_EMBEDDED 202 if (embed_editor) { 203 editor = new Plugin (this, _mid); 204 205 //editor_rev->add (*editor_socket); 206 editor_box->pack_start (dynamic_cast<Plugin *> (editor)->bin, false, false, 2); 207 208 } else { 209 # endif 210 editor = new External (this); 211 # ifndef DISABLE_EMBEDDED 212 } 213 # endif 214 215 thread_view = Gtk::manage(new ThreadView(main_window, true)); 216 //thread_rev->add (*thread_view); 217 editor_box->pack_start (*thread_view, false, false, 2); 218 219 thread_view->signal_ready().connect ( 220 sigc::mem_fun (this, &EditMessage::on_tv_ready)); 221 222 thread_view->signal_element_action().connect ( 223 sigc::mem_fun (this, &EditMessage::on_element_action)); 224 225 /* defaults */ 226 accounts = astroid->accounts; 227 228 /* from combobox */ 229 from_store = Gtk::ListStore::create (from_columns); 230 from_combo->set_model (from_store); 231 232 account_no = 0; 233 for (Account &a : accounts->accounts) { 234 auto row = *(from_store->append ()); 235 row[from_columns.name_and_address] = a.full_address(); 236 row[from_columns.account] = &a; 237 238 if (a.isdefault) { 239 from_combo->set_active (account_no); 240 } 241 242 account_no++; 243 } 244 245 from_combo->pack_start (from_columns.name_and_address); 246 /* 247 from_combo->signal_key_press_event ().connect ( 248 sigc::mem_fun (this, &EditMessage::on_from_combo_key_press)); 249 */ 250 251 252 253 show_all_children (); 254 255 prepare_message (); 256 257 sending_in_progress.store (false); 258 259 if (!embed_editor) { 260 thread_view->show (); 261 gtk_box_set_child_packing (editor_box->gobj (), GTK_WIDGET(thread_view->gobj ()), true, true, 5, GTK_PACK_START); 262 grab_modal (); 263 thread_view->grab_focus (); 264 } 265 266 editor_toggle (false); 267 268 from_combo->signal_changed().connect ( 269 sigc::mem_fun (this, &EditMessage::on_from_combo_changed)); 270 271 /* editor->start_editor_when_ready = true; */ 272 273 switch_signature->property_active().signal_changed ().connect ( 274 sigc::mem_fun (*this, &EditMessage::switch_signature_set)); 275 276 switch_markdown->property_active().signal_changed ().connect ( 277 sigc::mem_fun (*this, &EditMessage::switch_signature_set)); 278 279 switch_encrypt->property_active().signal_changed ().connect ( 280 sigc::mem_fun (*this, &EditMessage::switch_signature_set)); 281 282 switch_sign->property_active().signal_changed ().connect ( 283 sigc::mem_fun (*this, &EditMessage::switch_signature_set)); 284 285 reset_signature (); 286 287 switch_markdown->set_active (astroid->config ().get<bool> ("editor.markdown_on")); 288 289 /* register keys {{{ */ 290 keys.title = "Edit mode"; 291 keys.register_key (Key (GDK_KEY_Return), { Key (GDK_KEY_KP_Enter) }, 292 "edit_message.edit", 293 "Edit message in editor", 294 [&] (Key) { 295 if (!editor_active && !message_sent && !sending_in_progress.load()) { 296 editor_toggle (true); 297 } 298 return true; 299 }); 300 301 keys.register_key ("y", "edit_message.send", 302 "Send message", 303 [&] (Key) { 304 if (!message_sent && !sending_in_progress.load()) { 305 if (editor_active) { 306 set_warning ("Cannot send message when editing."); 307 308 return true; 309 } 310 311 if (!check_fields ()) { 312 /* warning str is set in check_fields () */ 313 LOG (error) << "em: error problem with some of the input fields.."; 314 315 return true; 316 } 317 318 ask_yes_no ("Really send message?", [&](bool yes){ if (yes) send_message (); }); 319 } 320 return true; 321 }); 322 323 keys.register_key ("C-c", "edit_message.cancel", 324 "Cancel sending message (unreliable)", 325 [&] (Key) { 326 if (!message_sent && sending_in_progress.load ()) { 327 sending_message->cancel_sending (); 328 329 /* send_message_finished will still be called to clean up sending_message */ 330 } 331 332 return true; 333 }); 334 335 keys.register_key ("V", "edit_message.view_raw", 336 "View raw message", 337 [&] (Key) { 338 /* view raw source of to be sent message */ 339 auto c = make_message (); 340 341 GMimeStream * m = g_mime_stream_mem_new (); 342 assert (g_mime_stream_mem_get_owner (GMIME_STREAM_MEM(m)) == true); 343 c->write (m); 344 345 main_window->add_mode (new RawMessage (main_window, refptr<Message>(new UnprocessedMessage(m)))); 346 347 g_object_unref (m); 348 349 return true; 350 }); 351 352 keys.register_key ("f", "edit_message.cycle_from", 353 "Cycle through From selector", 354 [&] (Key) { 355 356 if (editor_active) { 357 set_warning ("Cannot change from address when editing."); 358 359 return true; 360 } 361 362 /* cycle through from combo box */ 363 if (!message_sent && !sending_in_progress.load()) { 364 if (from_store->children().size() > 1) { 365 int i = from_combo->get_active_row_number (); 366 if (i >= (static_cast<int>(from_store->children().size())-1)) i = 0; 367 else i++; 368 from_combo->set_active (i); 369 reset_signature (); 370 } 371 } 372 373 return true; 374 }); 375 376 keys.register_key ("a", "edit_message.attach", 377 "Attach file", 378 [&] (Key) { 379 attach_file (); 380 return true; 381 }); 382 383 keys.register_key ("A", "edit_messsage.attach_mids", 384 "Attach messages by mids", 385 [&] (Key) { 386 main_window->enable_command (CommandBar::CommandMode::AttachMids, 387 "Attach mids:", "", [&] (ustring mids) 388 { 389 auto midsv = VectorUtils::split_and_trim (mids, ","); 390 if (!midsv.empty ()) { 391 Db db; 392 393 for (auto mid : midsv) { 394 db.on_message (mid, [&] (notmuch_message_t * msg) { 395 if (msg != NULL) { 396 LOG (debug) << "em: attaching: " << mid; 397 398 refptr<Message> mmsg = refptr<Message> (new UnprocessedMessage (msg, 0)); 399 add_attachment (new ComposeMessage::Attachment (mmsg)); 400 401 } else { 402 LOG (warn) << "em: could not find and attach mid: " << mid; 403 } 404 }); 405 } 406 prepare_message (); 407 read_edited_message (); 408 } 409 }); 410 411 return true; 412 }); 413 414 keys.register_key ("s", "edit_message.save_draft", 415 "Save draft", 416 [&] (Key) { 417 418 if (editor_active) { 419 set_warning ("Cannot save draft when editing."); 420 421 return true; 422 } 423 424 if (sending_in_progress.load ()) { 425 /* block closing the window while sending */ 426 LOG (error) << "em: message is being sent, it cannot be saved as draft anymore."; 427 } else { 428 429 bool r; 430 431 r = save_draft (); 432 433 if (!r) { 434 on_tv_ready (); 435 } else { 436 close (true); 437 } 438 } 439 return true; 440 }); 441 442 keys.register_key ("D", "edit_message.delete_draft", 443 "Delete draft", 444 [&] (Key) { 445 if (!draft_msg) { 446 LOG (debug) << "em: not a draft."; 447 return true; 448 } 449 450 if (sending_in_progress.load ()) { 451 /* block closing the window while sending */ 452 LOG (error) << "em: message is being sent, cannot delete draft now. it will be deleted upon successfully sent message."; 453 } else if (!message_sent) { 454 ask_yes_no ("Do you want to delete this draft and close it? (any changes will be lost)", 455 [&](bool yes) { 456 if (yes) { 457 delete_draft (); 458 close (true); 459 } 460 }); 461 } else { 462 // message has been sent successfully, no need to complain. 463 close (); 464 } 465 return true; 466 }); 467 468 469 keys.register_key ("S", "edit_message.toggle_signature", 470 "Toggle signature", 471 [&] (Key) { 472 auto iter = from_combo->get_active (); 473 auto row = *iter; 474 Account * a = row[from_columns.account]; 475 if (a->has_signature) { 476 switch_signature->set_active (!switch_signature->get_active ()); 477 } 478 return true; 479 }); 480 481 keys.register_key ("M", "edit_message.toggle_markdown", 482 "Toggle markdown", 483 [&] (Key) { 484 switch_markdown->set_active (!switch_markdown->get_active ()); 485 486 return true; 487 }); 488 489 keys.register_key ("E", "edit_message.toggle_encrypt", 490 "Toggle encryption and signature", 491 [&] (Key) { 492 493 auto iter = from_combo->get_active (); 494 if (!iter) { 495 return true; 496 } 497 498 auto row = *iter; 499 Account * a = row[from_columns.account]; 500 501 if (gpgenabled && a->has_gpg) { 502 if (!switch_encrypt->get_active () && !switch_sign->get_active ()) { 503 switch_encrypt->set_active (false); 504 switch_sign->set_active (true); 505 } else if (!switch_encrypt->get_active() && switch_sign->get_active ()) { 506 switch_encrypt->set_active (true); 507 switch_sign->set_active (true); 508 } else if (switch_encrypt->get_active () && switch_sign->get_active ()) { 509 switch_encrypt->set_active (true); 510 switch_sign->set_active (false); 511 } else { 512 switch_encrypt->set_active (false); 513 switch_sign->set_active (false); 514 } 515 } 516 517 return true; 518 }); 519 520 // }}} 521 522 if (_edit_when_ready) edit_when_ready (); 523 } // }}} 524 edit_when_ready()525 void EditMessage::edit_when_ready () { 526 if (!embed_editor) { 527 editor_toggle (true); 528 } else { 529 if (!editor->ready ()) editor->start_editor_when_ready = true; 530 else editor_toggle (true); 531 } 532 } 533 ~EditMessage()534 EditMessage::~EditMessage () { 535 LOG (debug) << "em: deconstruct."; 536 537 if (status_icon_visible) { 538 main_window->notebook.remove_widget (&message_sending_status_icon); 539 } 540 541 if (editor->started ()) { 542 editor->stop (); 543 } 544 545 if (is_regular_file (tmpfile_path)) { 546 boost::filesystem::remove (tmpfile_path); 547 } 548 } 549 pre_close()550 void EditMessage::pre_close () { 551 ((Mode*) thread_view)->pre_close (); 552 } 553 close(bool force)554 void EditMessage::close (bool force) { 555 if (sending_in_progress.load ()) { 556 /* block closing the window while sending */ 557 } else if (!force && !message_sent && !draft_saved) { 558 559 if (editor->started ()) { 560 set_warning ("Cannot close when editing."); 561 562 return; 563 } 564 565 ask_yes_no ("Do you want to close this message? (unsaved changes will be lost)", [&](bool yes){ if (yes) { Mode::close (force); } }); 566 567 } else if (force && !message_sent && !draft_saved && save_draft_on_force_quit) { 568 569 LOG (warn) << "em: force quit, trying to save draft.."; 570 bool r = save_draft (); 571 if (!r) { 572 LOG (error) << "em: cannot save draft! check account config. changes will be lost."; 573 } 574 Mode::close (force); 575 576 } else { 577 // message has been sent successfully, no need to complain. 578 Mode::close (force); 579 } 580 } 581 582 /* drafts */ save_draft()583 bool EditMessage::save_draft () { 584 LOG (info) << "em: saving draft.."; 585 draft_saved = false; 586 auto c = make_draft_message (); 587 ustring fname; 588 589 bool add_to_notmuch = false; 590 591 if (!draft_msg) { 592 /* make new message */ 593 594 path ddir = c->account->save_drafts_to; 595 if (!is_directory(ddir)) { 596 LOG (error) << "em: no draft directory specified!"; 597 set_warning ("draft could not be saved, no suitable draft directory for account specified."); 598 return false; 599 600 } else { 601 /* msg_id might come from external client or server */ 602 ddir = ddir / path(Utils::safe_fname (msg_id)); 603 fname = ddir.c_str (); 604 605 add_to_notmuch = true; 606 } 607 } else { 608 fname = draft_msg->fname; // overwrite 609 } 610 611 c->write (fname); 612 613 LOG (info) << "em: saved draft to: " << fname; 614 615 if (add_to_notmuch) { 616 astroid->actions->doit (refptr<Action> ( 617 new AddDraftMessage (fname))); 618 } 619 620 draft_saved = true; 621 return true; 622 } 623 delete_draft()624 void EditMessage::delete_draft () { 625 if (draft_msg) { 626 627 delete_draft (draft_msg); 628 629 draft_msg = refptr<Message> (); 630 draft_saved = true; /* avoid saving on exit */ 631 632 } else { 633 LOG (warn) << "em: not a draft, not deleting."; 634 } 635 } 636 delete_draft(refptr<Message> draft_msg)637 void EditMessage::delete_draft (refptr<Message> draft_msg) { 638 LOG (info) << "em: deleting draft."; 639 path fname = path(draft_msg->fname.c_str()); 640 641 if (is_regular_file (fname)) { 642 boost::filesystem::remove (fname); 643 644 /* first remove tag in case it has been sent */ 645 astroid->actions->doit (refptr<Action>( 646 new OnMessageAction (draft_msg->mid, draft_msg->tid, 647 648 [fname] (Db * db, notmuch_message_t * msg) { 649 for (ustring t : Db::draft_tags) { 650 notmuch_message_remove_tag (msg, 651 t.c_str ()); 652 } 653 654 bool persists = !db->remove_message (fname.c_str ()); 655 656 if (persists && db->maildir_synchronize_flags) { 657 /* sync in case there are other copies of the message */ 658 notmuch_message_tags_to_maildir_flags (msg); 659 } 660 }))); 661 } 662 663 } 664 665 /* edit / read message cycling {{{ */ on_from_combo_changed()666 void EditMessage::on_from_combo_changed () { 667 /* this will be called when the From: field has been changed 668 * manually in the e-mail as well. this check prevents the 669 * message currently being read from the edited draft to be 670 * overwritten before it is read. */ 671 if (!in_read) { 672 prepare_message (); 673 read_edited_message (); 674 } 675 } 676 prepare_message()677 void EditMessage::prepare_message () { 678 LOG (debug) << "em: preparing message from fields.."; 679 680 if (in_read) { 681 LOG (error) << "em: preparing message while in read"; 682 throw std::logic_error ("em: preparing message while in read"); 683 } 684 685 auto iter = from_combo->get_active (); 686 if (!iter) { 687 LOG (warn) << "em: error: no from account selected."; 688 return; 689 } 690 691 auto row = *iter; 692 Account * a = row[from_columns.account]; 693 auto from_ia = internet_address_mailbox_new (a->name.c_str(), a->email.c_str()); 694 ustring from = internet_address_to_string (from_ia, NULL, true); 695 696 tmpfile.open (tmpfile_path.c_str(), std::fstream::out); 697 tmpfile << "From: " << from << endl; 698 tmpfile << "To: " << to << endl; 699 tmpfile << "Cc: " << cc << endl; 700 if (bcc.size() > 0) { 701 tmpfile << "Bcc: " << bcc << endl; 702 } 703 tmpfile << "Subject: " << subject << endl; 704 tmpfile << endl; 705 tmpfile << body.raw(); 706 707 tmpfile.close (); 708 LOG (debug) << "em: prepare message done."; 709 } 710 set_from(Address a)711 bool EditMessage::set_from (Address a) { 712 if (!accounts->is_me (a)) { 713 LOG (error) << "em: from address is not a defined account."; 714 } 715 716 return set_from (accounts->get_account_for_address (a)); 717 } 718 set_from(Account * a)719 bool EditMessage::set_from (Account * a) { 720 int rn = from_combo->get_active_row_number (); 721 722 account_no = 0; 723 for (Gtk::TreeRow row : from_store->children ()) { 724 if (row[from_columns.account] == a) { 725 from_combo->set_active (row); 726 break; 727 } 728 account_no ++; 729 } 730 731 bool same_account = (rn == from_combo->get_active_row_number ()); 732 LOG (debug) << "same account: " << same_account; 733 if (!same_account) { 734 reset_signature (); 735 } 736 737 return same_account; 738 } 739 reset_signature()740 void EditMessage::reset_signature () { 741 /* should not be run unless the account has been changed */ 742 auto it = from_combo->get_active (); 743 Account * a = (*it)[from_columns.account]; 744 745 switch_signature->set_sensitive (a->has_signature); 746 switch_signature->set_active (a->has_signature && a->signature_default_on); 747 748 encryption_revealer->set_reveal_child (gpgenabled && a->has_gpg); 749 switch_sign->set_sensitive (gpgenabled && a->has_gpg); 750 switch_encrypt->set_sensitive (gpgenabled && a->has_gpg); 751 752 switch_encrypt->set_active (false); 753 switch_sign->set_active (gpgenabled && a->has_gpg && a->always_gpg_sign); 754 } 755 switch_signature_set()756 void EditMessage::switch_signature_set () { 757 LOG (debug) << "got sig: " << switch_signature->get_active (); 758 if (!in_read) { 759 prepare_message (); 760 read_edited_message (); 761 } 762 } 763 read_edited_message()764 void EditMessage::read_edited_message () { 765 LOG (debug) << "em: reading edited message.."; 766 std::lock_guard<std::mutex> lk (message_draft_m); 767 768 if (in_read) { 769 LOG (error) << "em: read while already reading!"; 770 throw std::logic_error ("read while already reading"); 771 } 772 773 in_read = true; 774 775 draft_saved = false; // we expect changes to have been made 776 set_warning (""); 777 778 if (!editor_active) { 779 if (astroid->hint_level () < 1) { 780 set_info ("Edit message with 'Enter'."); 781 } else { 782 set_info (""); 783 } 784 } 785 786 /* make message */ 787 auto c = setup_message (); 788 789 /* set account selector to from address email */ 790 set_from (c->account); 791 792 /* build message */ 793 finalize_message (c); 794 795 if (c->markdown && !c->markdown_success) { 796 set_warning ("Failed processing markdown: " + UstringUtils::replace (c->markdown_error, "\n", "<br />")); 797 } 798 799 if (c->encrypt || c->sign) { 800 if (!c->encryption_success) { 801 set_warning ("Failed encrypting: " + UstringUtils::replace (c->encryption_error, "\n", "<br />")); 802 } 803 } 804 805 to = c->to; 806 cc = c->cc; 807 bcc = c->bcc; 808 references = c->references; 809 inreplyto = c->inreplyto; 810 subject = c->subject; 811 body = ustring(c->body.str()); 812 813 GMimeStream * m = g_mime_stream_mem_new (); 814 assert (g_mime_stream_mem_get_owner (GMIME_STREAM_MEM(m)) == true); 815 c->write (m); 816 817 auto msgt = refptr<MessageThread>(new MessageThread()); 818 msgt->add_message (refptr<Message>(new UnprocessedMessage(m))); 819 thread_view->load_message_thread (msgt); 820 821 g_object_unref (m); 822 823 in_read = false; 824 } 825 826 /* }}} */ 827 set_info(ustring msg)828 void EditMessage::set_info (ustring msg) { 829 info_str = msg; 830 LOG (debug) << "em: set info (ready: " << thread_view->ready << "): " << msg; 831 if (thread_view->ready) { 832 if (info_str.length () > 0) { 833 thread_view->set_info (thread_view->focused_message, info_str); 834 } else { 835 thread_view->hide_info (thread_view->focused_message); 836 } 837 } 838 } 839 set_warning(ustring msg)840 void EditMessage::set_warning (ustring msg) { 841 warning_str = msg; 842 LOG (debug) << "em: set warning (ready: " << thread_view->ready << "): " << msg; 843 if (thread_view->ready) { 844 if (warning_str.length () > 0) { 845 thread_view->set_warning (thread_view->focused_message, warning_str); 846 } else { 847 thread_view->hide_warning (thread_view->focused_message); 848 } 849 } 850 } 851 on_tv_ready()852 void EditMessage::on_tv_ready () { 853 LOG (debug) << "em: got tv ready."; 854 855 set_info (info_str); 856 set_warning (warning_str); 857 } 858 859 /* swapping between edit and read mode {{{ */ fields_hide()860 void EditMessage::fields_hide () { 861 fields_revealer->set_reveal_child (false); 862 } 863 fields_show()864 void EditMessage::fields_show () { 865 fields_revealer->set_reveal_child (true); 866 } 867 868 /* turn on or off the editor or set up for the editor 869 * only run for embedded editor */ editor_toggle(bool on)870 void EditMessage::editor_toggle (bool on) { 871 LOG (debug) << "em: editor toggle: " << on; 872 873 # ifndef DISABLE_EMBEDDED 874 if (embed_editor) { 875 if (on) { 876 prepare_message (); 877 878 editor_active = true; 879 880 /* 881 editor_rev->set_reveal_child (true); 882 thread_rev->set_reveal_child (false); 883 */ 884 885 dynamic_cast<Plugin *> (editor)->bin.show (); 886 thread_view->hide (); 887 888 gtk_box_set_child_packing (editor_box->gobj (), GTK_WIDGET(dynamic_cast<Plugin *> (editor)->bin.gobj ()), true, true, 5, GTK_PACK_START); 889 gtk_box_set_child_packing (editor_box->gobj (), GTK_WIDGET(thread_view->gobj ()), false, false, 5, GTK_PACK_START); 890 891 /* future Gtk 892 editor_box->set_child_packing (editor_rev, true, true, 2); 893 editor_box->set_child_packing (thread_rev, false, false, 2); 894 */ 895 896 fields_hide (); 897 898 editor->start (); 899 900 } else { 901 if (editor->started ()) { 902 editor->stop (); 903 } 904 905 /* 906 editor_rev->set_reveal_child (false); 907 thread_rev->set_reveal_child (true); 908 */ 909 dynamic_cast<Plugin *> (editor)->bin.hide (); 910 thread_view->show (); 911 912 gtk_box_set_child_packing (editor_box->gobj (), GTK_WIDGET(dynamic_cast<Plugin *>(editor)->bin.gobj ()), false, false, 5, GTK_PACK_START); 913 gtk_box_set_child_packing (editor_box->gobj (), GTK_WIDGET(thread_view->gobj ()), true, true, 5, GTK_PACK_START); 914 915 /* future Gtk 916 editor_box->set_child_packing (editor_rev, false, false, 2); 917 editor_box->set_child_packing (thread_rev, true, true, 2); 918 */ 919 920 fields_show (); 921 922 if (editor_active) 923 read_edited_message (); 924 925 editor_active = false; 926 927 grab_modal (); 928 thread_view->grab_focus (); 929 } 930 931 } else { 932 # endif 933 if (on && !editor->started ()) { 934 /* start editor */ 935 editor_active = true; 936 937 prepare_message (); 938 read_edited_message (); 939 940 editor->start (); 941 942 info_str = "Editing.."; 943 944 } else { 945 /* return from editor */ 946 set_info (""); 947 948 if (editor_active) { 949 read_edited_message (); 950 } 951 952 editor_active = false; 953 } 954 # ifndef DISABLE_EMBEDDED 955 } 956 # endif 957 } 958 activate_editor()959 void EditMessage::activate_editor () { 960 /* used by Plugin */ 961 LOG (debug) << "em: activate editor."; 962 963 editor_active = true; 964 965 LOG (debug) << "em: activate editor."; 966 967 if (!editor->ready ()) { 968 LOG (warn) << "em: activate editor: not ready."; 969 return; 970 } 971 972 /* 973 * https://bugzilla.gnome.org/show_bug.cgi?id=729248 974 */ 975 976 release_modal (); 977 978 if (editor->ready ()) { 979 980 editor->focus (); 981 982 } else { 983 984 LOG (warn) << "em: activate editor, editor not yet started!"; 985 986 } 987 } 988 989 /* }}} */ 990 on_element_action(int id,ThreadView::ElementAction action)991 void EditMessage::on_element_action (int id, ThreadView::ElementAction action) { 992 993 if (sending_in_progress.load ()) return; 994 995 if (action == ThreadView::ElementAction::EDelete) { 996 997 /* delete attachment */ 998 auto e = thread_view->state[thread_view->focused_message].elements[id]; 999 1000 if (e.type == ThreadView::MessageState::ElementType::Attachment || 1001 e.type == ThreadView::MessageState::ElementType::MimeMessage ) 1002 { 1003 LOG (info) << "em: remove attachment: " << id << ", cid: " << e.id; 1004 1005 /* find attachment */ 1006 unsigned int attachment = 0; 1007 1008 for (unsigned int i = 0; 1009 i < thread_view->state[thread_view->focused_message].elements.size (); 1010 i++) 1011 { 1012 auto xe = thread_view->state[thread_view->focused_message].elements[i]; 1013 if (xe.type == ThreadView::MessageState::ElementType::Attachment || 1014 xe.type == ThreadView::MessageState::ElementType::MimeMessage ) 1015 { 1016 1017 if (i == (unsigned int) id) { 1018 attachments.erase (attachments.begin () + attachment); 1019 break; 1020 } 1021 1022 attachment++; 1023 } 1024 } 1025 1026 prepare_message (); 1027 read_edited_message (); 1028 } 1029 } 1030 } 1031 1032 /* }}} */ 1033 1034 /* send message {{{ */ check_fields()1035 bool EditMessage::check_fields () { 1036 if (to.empty () && cc.empty () && bcc.empty ()) { 1037 set_warning ("No recipients defined!"); 1038 1039 on_tv_ready (); 1040 1041 return false; 1042 } 1043 1044 /* check if attachments are mentioned */ 1045 if (attachments.empty () && !attachment_words.empty ()) { 1046 ustring bl = body.lowercase (); 1047 1048 /* strip quoted lines */ 1049 ostringstream nonquoted; 1050 stringstream sstr (bl); 1051 while (sstr.good()) { 1052 string line; 1053 getline (sstr, line); 1054 1055 if (line[0] != '>') 1056 nonquoted << line << endl; 1057 } 1058 ustring nqb = ustring(nonquoted.str()); 1059 1060 if (any_of (attachment_words.begin (), 1061 attachment_words.end (), 1062 [&] (ustring w) { 1063 return nqb.find (w) != string::npos; 1064 })) 1065 { 1066 set_warning ("Attachments have been mentioned in the message, but none are attached, do you still want to send?"); 1067 1068 on_tv_ready (); 1069 } 1070 } 1071 1072 return true; 1073 } 1074 send_message()1075 bool EditMessage::send_message () { 1076 LOG (info) << "em: sending message.."; 1077 1078 /* load body */ 1079 editor_toggle (false); // resets warning and info 1080 1081 set_warning (""); 1082 set_info ("sending message.."); 1083 1084 on_tv_ready (); 1085 1086 auto c = make_message (); 1087 1088 if (c == NULL) return false; 1089 1090 if (c->markdown && !c->markdown_success) { 1091 set_warning ("Cannot send, failed processing markdown: " + UstringUtils::replace (c->markdown_error, "\n", "<br />")); 1092 return false; 1093 } 1094 1095 if (c->encrypt || c->sign) { 1096 if (!c->encryption_success) { 1097 set_warning ("Cannot send, failed encrypting: " + UstringUtils::replace (c->encryption_error, "\n", "<br />")); 1098 return false; 1099 } 1100 } 1101 1102 c->message_sent().connect ( 1103 sigc::mem_fun (this, &EditMessage::send_message_finished)); 1104 c->message_send_status ().connect ( 1105 sigc::mem_fun (this, &EditMessage::update_send_message_status)); 1106 1107 fields_hide (); 1108 sending_in_progress.store (true); 1109 1110 /* set message sending icon */ 1111 status_icon_visible = true; 1112 Glib::RefPtr<Gtk::IconTheme> theme = Gtk::IconTheme::get_default(); 1113 Glib::RefPtr<Gdk::Pixbuf> pixbuf = theme->load_icon ( 1114 "mail-send", 1115 Notebook::icon_size, 1116 Gtk::ICON_LOOKUP_USE_BUILTIN | Gtk::ICON_LOOKUP_FORCE_SIZE); 1117 message_sending_status_icon.set (pixbuf); 1118 1119 main_window->notebook.add_widget (&message_sending_status_icon); 1120 1121 c->send_threaded (); 1122 1123 sending_message = std::move (c); 1124 1125 return true; 1126 } 1127 update_send_message_status(bool warn,ustring msg)1128 void EditMessage::update_send_message_status (bool warn, ustring msg) { 1129 if (warn) { 1130 set_info (""); 1131 set_warning (msg); 1132 } else { 1133 set_info (msg); 1134 set_warning (""); 1135 } 1136 1137 on_tv_ready (); 1138 } 1139 send_message_finished(bool result_from_sender)1140 void EditMessage::send_message_finished (bool result_from_sender) { 1141 LOG (info) << "em: message sending done."; 1142 status_icon_visible = true; 1143 1144 Glib::RefPtr<Gtk::IconTheme> theme = Gtk::IconTheme::get_default(); 1145 Glib::RefPtr<Gdk::Pixbuf> pixbuf; 1146 1147 if (result_from_sender) { 1148 lock_message_after_send (); 1149 1150 pixbuf = theme->load_icon ( 1151 "gtk-apply", 1152 Notebook::icon_size, 1153 Gtk::ICON_LOOKUP_USE_BUILTIN | Gtk::ICON_LOOKUP_FORCE_SIZE); 1154 1155 /* delete draft */ 1156 if (draft_msg) { 1157 LOG (info) << "em: deleting draft: " << draft_msg->fname; 1158 delete_draft (); 1159 } 1160 1161 } else { 1162 fields_show (); 1163 1164 pixbuf = theme->load_icon ( 1165 "dialog-error", 1166 Notebook::icon_size, 1167 Gtk::ICON_LOOKUP_USE_BUILTIN | Gtk::ICON_LOOKUP_FORCE_SIZE); 1168 } 1169 1170 message_sending_status_icon.set (pixbuf); 1171 sending_in_progress.store (false); 1172 1173 sending_message.reset(); 1174 1175 emit_message_sent_attempt (result_from_sender); 1176 1177 if (result_from_sender && (astroid->config().get<bool> ("mail.close_on_success"))) { 1178 LOG (info) << "cm: sending successful, auto-closing window"; 1179 close (true); 1180 } 1181 } 1182 lock_message_after_send()1183 void EditMessage::lock_message_after_send () { 1184 message_sent = true; 1185 1186 fields_hide (); 1187 } 1188 setup_message()1189 std::unique_ptr<ComposeMessage> EditMessage::setup_message () { 1190 auto c = std::unique_ptr<ComposeMessage>(new ComposeMessage ()); 1191 1192 c->load_message (msg_id, tmpfile_path.c_str()); 1193 1194 c->set_references (references); 1195 c->set_inreplyto (inreplyto); 1196 1197 for (shared_ptr<ComposeMessage::Attachment> a : attachments) { 1198 c->add_attachment (a); 1199 } 1200 1201 return c; 1202 } 1203 finalize_message(std::unique_ptr<ComposeMessage> & c)1204 void EditMessage::finalize_message (std::unique_ptr<ComposeMessage> &c) { 1205 /* these options are not known before setup_message is done, and the 1206 * new account information has been applied to the editor */ 1207 if (c->account->has_signature && switch_signature->get_active ()) { 1208 c->include_signature = true; 1209 } else { 1210 c->include_signature = false; 1211 } 1212 c->markdown = switch_markdown->get_active (); 1213 1214 if (gpgenabled && c->account->has_gpg) { 1215 c->encrypt = switch_encrypt->get_active (); 1216 c->sign = switch_sign->get_active (); 1217 } 1218 1219 c->build (); 1220 c->finalize (); 1221 } 1222 make_message()1223 std::unique_ptr<ComposeMessage> EditMessage::make_message () { 1224 return make_message (false); 1225 } 1226 make_draft_message()1227 std::unique_ptr<ComposeMessage> EditMessage::make_draft_message () { 1228 return make_message (true); 1229 } 1230 make_message(bool draft=false)1231 std::unique_ptr<ComposeMessage> EditMessage::make_message (bool draft = false) { 1232 auto c = setup_message (); 1233 bool sigstate = c->account->has_signature; 1234 1235 if (draft) { 1236 /* Do not save signature in a draft */ 1237 c->account->has_signature = false; 1238 } 1239 finalize_message (c); 1240 c->account->has_signature = sigstate; 1241 1242 return c; 1243 } 1244 1245 /* }}} */ 1246 make_tmpfile()1247 void EditMessage::make_tmpfile () { 1248 tmpfile_path = tmpfile_path / path(msg_id); 1249 1250 LOG (info) << "em: tmpfile: " << tmpfile_path; 1251 1252 if (is_regular_file (tmpfile_path)) { 1253 LOG (error) << "em: error: tmpfile already exists!"; 1254 throw runtime_error ("em: tmpfile already exists!"); 1255 } 1256 1257 tmpfile.open (tmpfile_path.c_str(), std::fstream::out); 1258 1259 if (tmpfile.fail()) { 1260 LOG (error) << "em: error: could not create tmpfile!"; 1261 throw runtime_error ("em: could not create tmpfile!"); 1262 } 1263 1264 tmpfile.close (); 1265 } 1266 attach_file()1267 void EditMessage::attach_file () { 1268 LOG (info) << "em: attach file.."; 1269 1270 if (message_sent) { 1271 LOG (debug) << "em: message already sent."; 1272 return; 1273 } 1274 1275 Gtk::FileChooserDialog dialog ("Choose file to attach..", 1276 Gtk::FILE_CHOOSER_ACTION_OPEN); 1277 1278 dialog.add_button ("_Cancel", Gtk::RESPONSE_CANCEL); 1279 dialog.add_button ("_Attach", Gtk::RESPONSE_OK); 1280 dialog.set_select_multiple (true); 1281 dialog.set_current_folder (astroid->runtime_paths ().attach_dir.c_str ()); 1282 1283 int result = dialog.run (); 1284 1285 switch (result) { 1286 case (Gtk::RESPONSE_OK): 1287 { 1288 vector<string> fnames = dialog.get_filenames (); 1289 astroid->runtime_paths ().attach_dir = bfs::path (dialog.get_current_folder ()); 1290 for (string &fname : fnames) { 1291 path p (fname.c_str()); 1292 1293 if (!is_regular (p)) { 1294 LOG (error) << "em: attach: file is not regular: " << p.c_str(); 1295 } else { 1296 LOG (info) << "em: attaching file: " << p.c_str(); 1297 add_attachment (new ComposeMessage::Attachment (p)); 1298 } 1299 } 1300 1301 prepare_message (); 1302 read_edited_message (); 1303 1304 break; 1305 } 1306 1307 default: 1308 { 1309 LOG (debug) << "em: attach: cancelled."; 1310 } 1311 } 1312 } 1313 add_attachment(ComposeMessage::Attachment * a)1314 void EditMessage::add_attachment (ComposeMessage::Attachment * a) { 1315 if (a->valid) { 1316 attachments.push_back (shared_ptr<ComposeMessage::Attachment> (a)); 1317 } else { 1318 LOG (error) << "em: invalid attachment, not adding: " << a->name; 1319 delete a; 1320 } 1321 } 1322 grab_modal()1323 void EditMessage::grab_modal () { 1324 if (!embed_editor || !editor_active) add_modal_grab (); 1325 } 1326 release_modal()1327 void EditMessage::release_modal () { 1328 remove_modal_grab (); 1329 } 1330 1331 /* message sent attempt signal */ 1332 EditMessage::type_message_sent_attempt message_sent_attempt()1333 EditMessage::message_sent_attempt () 1334 { 1335 return m_message_sent_attempt; 1336 } 1337 emit_message_sent_attempt(bool res)1338 void EditMessage::emit_message_sent_attempt (bool res) { 1339 m_message_sent_attempt.emit (res); 1340 } 1341 1342 /* from combo box {{{ */ 1343 /* 1344 bool EditMessage::on_from_combo_key_press (GdkEventKey * event) { 1345 1346 switch (event->keyval) { 1347 case GDK_KEY_j: 1348 { 1349 from_combo->set_active (from_combo->get_active_row_number()+1); 1350 return true; 1351 } 1352 } 1353 1354 return false; 1355 } 1356 */ 1357 1358 // }}} 1359 1360 } 1361