diff --git a/resources/Scribe.lr8 b/resources/Scribe.lr8 --- a/resources/Scribe.lr8 +++ b/resources/Scribe.lr8 @@ -1,5231 +1,5231 @@ - + diff --git a/resources/resdefs.h b/resources/resdefs.h --- a/resources/resdefs.h +++ b/resources/resdefs.h @@ -1,1284 +1,1284 @@ // Generated by LgiRes // This file generated by LgiRes #define L_FUI_NEW -912 #define L_FUI_LEGEND -911 #define L_FUI_NOT -910 #define L_FUI_OPTIONS -909 #define L_FUI_CONFIGURE -908 #define L_FUI_MOVE_DOWN -907 #define L_FUI_MOVE_UP -906 #define L_FUI_DELETE -905 #define L_FUI_OR -904 #define L_FUI_AND -903 #define L_FUI_NEW_OR -902 #define L_FUI_NEW_AND -901 #define L_FUI_NEW_CONDITION -900 #define L_STORE_RESTART -803 #define L_STORE_MISMATCH -802 #define L_STORE_OS_ERR -801 #define L_STORE_WRITE_ERR -800 #define L_TOOLBAR_SHOW_TEXT -700 #define L_FR_SELECTION_ONLY -608 #define L_FR_REPLACE_WITH -607 #define L_FR_REPLACE_ALL -606 #define L_FR_REPLACE -605 #define L_FR_MATCH_CASE -604 #define L_FR_MATCH_WORD -603 #define L_FR_FIND_NEXT -602 #define L_FR_FIND_WHAT -601 #define L_FR_FIND -600 #define L_COLOUR_NONE -550 #define L_CHANGE_CHARSET -505 #define L_VIEW_IMAGES -504 #define L_VIEW_IN_DEFAULT_BROWSER -503 #define L_COPY_SOURCE -502 #define L_VIEW_SOURCE -501 #define L_COPY_LINK_LOCATION -500 #define L_FONTUI_UNDERLINE -407 #define L_FONTUI_TITLE -406 #define L_FONTUI_STYLE -405 #define L_FONTUI_PTSIZE -404 #define L_FONTUI_PREVIEW -403 #define L_FONTUI_ITALIC -402 #define L_FONTUI_FACE -401 #define L_FONTUI_BOLD -400 #define L_TEXTCTRL_TAB_SIZE -214 #define L_TEXTCTRL_INDENT_SIZE -213 #define L_TEXTCTRL_HARD_TABS -212 #define L_TEXTCTRL_SHOW_WHITESPACE -211 #define L_TEXTCTRL_UNDO -210 #define L_TEXTCTRL_REDO -209 #define L_TEXTCTRL_PASTE -208 #define L_TEXTCTRL_OPENURL -207 #define L_TEXTCTRL_GOTO_LINE -206 #define L_TEXTCTRL_FIXED -205 #define L_TEXTCTRL_EMAIL_TO -204 #define L_TEXTCTRL_CUT -203 #define L_TEXTCTRL_COPYLINK -202 #define L_TEXTCTRL_COPY -201 #define L_TEXTCTRL_AUTO_INDENT -200 #define IDS_ERROR_ESMTP_UNSUPPORTED_AUTHS -101 #define IDS_ERROR_ESMTP_NO_AUTHS -100 #define L_BTN_CANCEL -51 #define L_BTN_OK -50 #define FIELD_FLAGS 1 #define FIELD_TO 2 #define FIELD_CC 3 #define FIELD_FROM 4 #define FIELD_REPLY 5 #define FIELD_SUBJECT 6 #define FIELD_TEXT 7 #define FIELD_MESSAGE_ID 8 #define FIELD_DATE_RECEIVED 9 #define FIELD_INTERNET_HEADER 10 #define FIELD_FIRST_NAME 11 #define FIELD_LAST_NAME 12 #define FIELD_EMAIL 13 #define FIELD_HOME_STREET 14 #define FIELD_HOME_SUBURB 15 #define FIELD_HOME_POSTCODE 16 #define FIELD_HOME_STATE 17 #define FIELD_HOME_COUNTRY 18 #define FIELD_WORK_PHONE 19 #define FIELD_HOME_PHONE 20 #define FIELD_HOME_MOBILE 21 #define FIELD_HOME_IM 22 #define FIELD_HOME_FAX 23 #define FIELD_HOME_WEBPAGE 24 #define FIELD_NICK 25 #define FIELD_SPOUSE 26 #define FIELD_NOTE 27 #define FIELD_PLUGIN_ASSOC 28 #define FIELD_SIZE 29 #define FIELD_DATE_SENT 30 #define FIELD_COLUMN 31 #define FIELD_BCC 32 #define FIELD_MIME_TYPE 33 #define FIELD_PRIORITY 34 #define FIELD_FOLDER_OPEN 35 #define FIELD_CODE_PAGE 36 #define FIELD_MARK_COLOUR 37 #define FIELD_ALTERNATE_HTML 38 #define FIELD_CONTENT_ID 39 #define FIELD_FILTER_NAME 40 #define FIELD_CONDITION 41 #define FIELD_ACTION 42 #define FIELD_COND_FIELD 43 #define FIELD_COND_OPERATOR 44 #define FIELD_COND_VALUE 45 #define FIELD_ACT_TYPE 46 #define FIELD_ACT_ARG 47 #define FIELD_DIGEST_INDEX 48 #define FIELD_COMBINE_OP 49 #define FIELD_FILTER_INDEX 50 #define FIELD_FILTER_INCOMING 55 #define FIELD_FILTER_OUTGOING 56 #define FIELD_FILTER_INTERNAL 59 #define FIELD_CAL_SUBJECT 62 #define FIELD_CAL_LOCATION 63 #define FIELD_CAL_REMINDER_TIME 64 #define FIELD_CAL_REMINDER_ACTION_dep 65 #define FIELD_CAL_REMINDER_ARG_dep 66 #define FIELD_CAL_SHOW_TIME_AS 67 #define FIELD_CAL_RECUR_FREQ 68 #define FIELD_CAL_RECUR_INTERVAL 69 #define FIELD_CAL_RECUR_FILTER_DAYS 70 #define FIELD_CAL_RECUR_FILTER_MONTHS 71 #define FIELD_CAL_RECUR_FILTER_YEARS 72 #define FIELD_CAL_NOTES 73 #define FIELD_CAL_START_UTC 76 #define FIELD_CAL_END_UTC 77 #define FIELD_CAL_RECUR_FILTER_POS 78 #define FIELD_CAL_RECUR_END_DATE 79 #define FIELD_CAL_RECUR_END_COUNT 80 #define FIELD_CAL_RECUR_END_TYPE 81 #define FIELD_CAL_RECUR 82 #define IDC_COLOUR 83 #define FIELD_ATTENDEE_NAME 85 #define FIELD_ATTENDEE_EMAIL 86 #define FIELD_ATTENDEE_ATTENDENCE 87 #define FIELD_ATTENDEE_NOTE 88 #define FIELD_ATTENDEE_RESPONSE 89 #define FIELD_WORK_STREET 90 #define FIELD_WORK_SUBURB 91 #define FIELD_WORK_POSTCODE 92 #define FIELD_WORK_STATE 93 #define FIELD_WORK_COUNTRY 94 #define FIELD_WORK_MOBILE 95 #define FIELD_WORK_IM 96 #define FIELD_WORK_FAX 97 #define FIELD_WORK_WEBPAGE 98 #define FIELD_COMPANY 99 #define FIELD_LABEL 110 #define IDM_SAVE 113 #define FIELD_TITLE 114 #define FIELD_SERVER_UID 117 #define FIELD_GROUP_NAME 120 #define FIELD_GROUP_LIST 123 #define FIELD_FROM_CONTACT_NAME 126 #define IDS_ENCRYPTION_SUPPORT 150 #define FIELD_DATE_MODIFIED 162 #define FIELD_IMAP_SEQ 167 #define FIELD_RECEIVED_DOMAIN 170 #define IDD_FILTER_ITEMS 175 #define IDC_SIGNATURE_TAB 184 #define IDC_RESET_REPLY 186 #define IDC_SIG 187 #define IDC_RESET_FORWARD 189 #define IDS_205 205 #define IDS_SAVE_ITEM 208 #define IDS_DESCRIPTIVE_NAME 209 #define IDS_ERROR_SOFTWARE_UPDATE 210 #define IDS_211 211 #define IDS_235 235 #define IDC_SUB_FOLDERS 275 #define IDM_FEEDBACK 287 #define IDM_SCRIBE_LINK 291 #define IDM_INSCRIBE_LINK 292 #define IDM_FAQ 293 #define IDC_READ 316 #define IDD_SUB_FOLDERS 317 #define IDC_HAS_TEMPLATES 322 #define IDC_INBOX 324 #define IDC_OUTBOX 325 #define IDM_PAGE_SETUP 326 #define IDC_TRASH 327 #define IDC_CONTACT_FLD 328 #define IDS_DEBUG 329 #define IDC_SET_INBOX 330 #define IDC_SET_OUTBOX 331 #define IDC_SET_SENT 332 #define IDC_SET_TRASH 333 #define IDC_SET_CONTACTS 334 #define IDC_SET_FILTERS 335 #define IDC_MAX_SIZE 342 #define IDC_CONFIRM_DEL 343 #define IDS_CONTENT_ID 344 #define IDC_BOLD_UNREAD 346 #define IDC_GRID_LINES 347 #define IDC_TITLE 348 #define IDC_PREVIEW_LINES 349 #define IDC_SET_TEMPLATES 350 #define IDC_351 351 #define IDC_SMTP_AUTH 353 #define IDC_FILTERS_FLD 354 #define IDS_RETRY 355 #define IDS_FOLDER_PROPERTIES_COMPACT 356 #define IDC_HAS_SPAM 357 #define IDS_CALENDAR 359 #define IDS_NO_LOG 360 #define IDC_REC_TYPE 362 #define IDS_FIND 363 #define IDS_NO_ITEMS 364 #define IDS_RECEIVE_ALL_ACCOUNTS 365 #define IDS_NO_ITEMS_IN_FOLDER 366 #define IDS_PREVIEW_ON_SERVER 367 #define IDS_RECEIVE_MAIL 368 #define IDS_NO_TEMPLATES 369 #define IDC_STOP_FILTERING 370 #define IDS_NEW_EMAIL 371 #define IDS_EXIT 372 #define IDS_MARK_ALL_SEND 373 #define IDS_MARK 374 #define IDS_SELECT_MARKED 375 #define IDS_PRIORITY 376 #define IDS_HIGH 377 #define IDS_NORMAL 378 #define IDS_LOW 379 #define IDS_NEW_CONTACT 380 #define IDS_RECEIVE 381 #define IDC_LIMIT_TO 382 #define IDS_PRINT 383 #define IDS_CAL_SDAY_SUN 384 #define IDS_SAVE_CLOSE 385 #define IDS_FILTER 386 #define IDS_PREV_MSG 387 #define IDS_NEXT_MSG 388 #define IDS_CONDITIONS 390 #define IDS_ACTIONS 391 #define IDS_DETAIL 392 #define IDS_FIELDS 393 #define IDS_FILE_NAME 394 #define IDS_MIME_TYPE 395 #define IDS_PREVIEW 396 #define IDS_ERROR_FOLDERS_VERSION 397 #define IDS_ERROR_FOLDERS_STATUS 398 #define IDS_EXPORT 399 #define IDS_STATUS 400 #define IDD_STATUS 401 #define IDS_402 402 #define IDC_ACC_LIST 403 #define IDC_404 404 #define IDC_IN_FOLDER 406 #define IDS_407 407 #define IDS_408 408 #define IDC_STATUS_TXT 409 #define IDC_OP_TXT 410 #define IDS_411 411 #define IDC_EMAIL_TXT 412 #define IDC_EMAIL_PROG 413 #define IDD_MAIL_PROPERTIES 414 #define IDC_STOP 415 #define IDC_SEND_LOG 416 #define IDC_OP_PROG 417 #define IDC_SET_IN 418 #define IDS_HIDE_GNUPG 419 #define IDS_RESIZE 420 #define IDS_COPY_FOLDER 421 #define IDS_GNUPG_ATTACH_PUB_KEY 422 #define IDS_GNUPG_SIGN 423 #define IDS_GNUPG_ENCRYPT 424 #define IDS_GNUPG_DECRYPT 425 #define IDS_GNUPG_ERR_NOMSG 426 #define IDC_CLEAR_KEYWORDS 436 #define IDS_MONTH 437 #define IDC_BAYES_TABLE 439 #define IDS_HIGH_PRIORITY 440 #define IDD_ACCOUNT 441 #define IDS_443 443 #define IDS_444 444 #define IDS_445 445 #define IDC_MAIL_DESCRIPTION 448 #define IDC_SENT 449 #define IDC_RECEIVED 450 #define IDC_FORWARDED 451 #define IDC_REPLIED 452 #define IDC_HAS_ATTACH 453 #define IDC_MARKED 455 #define IDC_READY_SEND 456 #define IDC_OPEN_INSPECTOR 457 #define IDC_CREATED 459 #define IDD_NO_FOLDER 460 #define IDC_MESSAGE 462 #define IDS_470 470 #define IDC_CREATE_FOLDER 471 #define IDS_472 472 #define IDS_475 475 #define IDC_ACCOUNT 476 #define IDC_ACCOUNT_NAME 477 #define IDC_SMTP_PASSWORD 478 #define IDC_EXISTING 486 #define IDC_FOLDER_HISTORY 487 #define IDS_488 488 #define IDS_ASK_TNEF_DECODE 495 #define IDC_RECEIVE_TABLE 500 #define IDC_SEND_SSL 501 #define IDS_OK 502 #define IDC_NEW_FOLDER 503 #define IDC_EXISTING_FOLDER 504 #define IDC_BROWSE_NEW 505 #define IDC_BROWSE_EXISTING 506 #define IDS_YEAR 507 #define IDC_TEXT 508 #define IDC_SEARCH 509 #define IDS_510 510 #define IDC_BAYES_MODE 511 #define IDC_REPEATS 512 #define IDS_YEARS 513 #define IDS_CANCEL 514 #define IDS_SAVE 515 #define IDS_EMAIL 516 #define IDS_REPLY 517 #define IDS_REPLYALL 518 #define IDS_FORWARD 519 #define IDC_SIG_HTML 520 #define IDS_TO 521 #define IDC_DEFAULT_ALT 522 #define IDS_COMPACT_TESTING_DLG 523 #define IDS_FOLDER_COMPACT_ERR_DLG 524 #define IDS_COULDNT_OPEN 525 #define IDC_SEARCH_SUB 526 #define IDC_FIND_CASE 527 #define IDC_FIND_WORD 528 #define IDS_ATTACH_FILE 529 #define IDC_CTRLS_TABLE 530 #define IDC_MAIL_FIELD 531 #define IDC_CONTACT 532 #define IDM_BAYES_STATS 533 #define IDS_EDIT 534 #define IDC_ID_TABLE 535 #define IDC_CONTACT_FIELD 536 #define IDC_ACTION 537 #define IDS_REMOVE 538 #define IDM_UNIT_TESTS 539 #define IDC_LAYOUT 540 #define IDC_TABS 541 #define IDC_275 542 #define IDC_FIRST 543 #define IDS_544 544 #define IDC_OUT_FOLDER 545 #define IDC_SET_OUT 546 #define IDD_PAGE_SETUP 547 #define IDC_PREVIEW 548 #define IDS_ATTACH_WARNING_DLG 549 #define IDC_BROWSE_FONT 550 #define IDS_MAIL_PROPS_DLG 551 #define IDS_552 552 #define IDS_553 553 #define IDS_554 554 #define IDS_555 555 #define IDC_LEFT 556 #define IDC_TOP 557 #define IDC_RIGHT 558 #define IDC_BOTTOM 559 #define IDS_MAIL_PROPS 560 #define IDC_CUSTOM_FIELDS 561 #define IDS_ADD_CONTACTS 562 #define IDS_SUBFLD_NAME_CLASH 563 #define IDC_TREE 564 #define IDS_ERROR_NO_RECIPIENTS 565 #define IDC_USER_PASS_CONFIRM 566 #define IDS_SAVE_ATTACHMENT_AS 567 #define IDS_NO_CONNECTION_TO_SERVER 568 #define IDS_SERVER_ERROR_MSG 569 #define IDS_NO_SMTP_SERVER_SETTINGS 570 #define IDS_ASK_ABOUT_OPTIONS 571 #define IDS_THIS_WEEK 572 #define IDS_UPDATE_PERIOD 573 #define IDS_HEX_LOG 574 #define IDS_OPEN 575 #define IDS_BYTE_LOG 576 #define IDC_CALENDER 577 #define IDS_WINDOWS_SSL_INSTALL 578 #define IDC_KEYWORDS 579 #define IDC_PATH 580 #define IDC_UNREAD 581 #define IDC_LOCATION 582 #define IDS_ADDRESS 583 #define IDS_POPUP 584 #define IDC_PARAM_LABEL 585 #define IDC_DIR 586 #define IDS_LOW_PRIORITY 587 #define IDD_CAL 588 #define IDD_CAL_REMOTE 589 #define IDC_590 590 #define IDS_ADD_CAL 591 #define IDS_ADD_CAL_POPUP 592 #define IDS_SET_READ 593 #define IDS_SET_UNREAD 594 #define IDS_PROPERTIES 595 #define IDS_SUBJECT 596 #define IDS_OPTIONS 597 #define IDS_SMTP 598 #define IDS_SIZE 599 #define IDS_DATE 600 #define IDS_SAVEAS 601 #define IDS_EMPTY 602 #define IDS_ERROR_FOLDERS_DONT_EXIST 603 #define IDS_ERROR_CANT_OPEN_FOLDERS 604 #define IDS_ENTER_FILE_NAME_FOR_FOLDERS 605 #define IDS_SEND_MAIL 606 #define IDS_NEW 607 #define IDS_APPOINTMENT 608 #define IDS_CAL_EVENT 609 #define IDS_CONTACT 610 #define IDS_CREATESUBFOLDER 611 #define IDS_612 612 #define IDS_CAL_VIEW 613 #define IDS_MINUTES 614 #define IDS_HOUR 615 #define IDS_HOURS 616 #define IDS_DAY 617 #define IDS_DAYS 618 #define IDS_FREE 619 #define IDS_TENTATIVE 620 #define IDS_BUSY 621 #define IDS_OUT_OF_OFFICE 622 #define IDS_DOWNLOAD 623 #define IDS_READ_RECEIPT 624 #define IDS_CAL_SDAY_MON 625 #define IDS_RENAMEFOLDER 626 #define IDC_FOLDER_MSG 627 #define IDC_PASS 628 #define IDS_629 629 #define IDS_630 630 #define IDS_631 631 #define IDD_FOLDER_PROPS 632 #define IDS_FIRST 633 #define IDS_LOCAL_CONTACTS 634 #define IDC_ACCOUNT_TBL 635 #define IDS_MOVE_ERROR 636 #define IDS_MESSAGE 637 #define IDS_ACTION 638 #define IDS_MSGS_ERROR 639 #define IDS_CAL_LMONTH_OCT 640 #define IDC_ACCOUNTS_READ 641 #define IDS_CONFIGURE 642 #define IDS_ERROR_FILE_OVERWRITE 643 #define IDC_INTERNAL_FILTERING 644 #define IDC_STORE2_KEEP 645 #define IDS_ERROR_CANT_READ 646 #define IDC_FOLDER_WRITE 647 #define IDS_ERROR_FILE_EXISTS 648 #define IDD_BAYES_SETTINGS 649 #define IDC_USER_LEVEL_PSW 650 #define IDC_USER_PASS 651 #define IDS_ERROR_FOLDERS_ALREADY_EXIST 652 #define IDC_APW_USER 653 #define IDC_COMPACT_MS 654 #define IDD_FIND 655 #define IDC_DEF_SEND 656 #define IDC_TOOLBAR_TEXT 657 #define IDS_ADD 658 #define IDM_SAVEAS 659 #define IDC_URL 660 #define IDC_SUBJECT 661 #define IDD_MAIL_FIELDS 662 #define IDD_KEY 663 #define IDC_LABEL 664 #define IDS_147 665 #define IDC_SENT_DATE 666 #define IDS_148 667 #define IDC_RECEIVED_DATE 668 #define IDD_FILTER 669 #define IDC_PRIVACY_TYPE 670 #define IDS_671 671 #define IDM_NO_IDENTITIES 672 #define IDD_FILTER_ACTION 673 #define IDD_NEWFOLDER_F 674 #define IDD_FILES_IMPORT 675 #define IDS_ADD_LOCAL_CAL_FOLDER 676 #define IDC_UI_FNT_SIZE 677 #define IDC_UI_LANG 678 #define IDS_ERROR_LR8_FAILURE 679 #define IDC_REC_8BIT_CS 680 #define IDC_CLIENT 681 #define IDC_BLINK 682 #define IDC_FOLDER_READ 683 #define IDC_ACCOUNTS_WRITE 684 #define IDC_CUSTOM_TABLE 685 #define IDC_APR_USER 686 #define IDC_NAME_TABLE 687 #define IDC_USER 688 #define IDC_APR_ADMIN 689 #define IDC_APW_NONE 690 #define IDS_ADD_CAL_URL 691 #define IDC_REMINDER_VALUE 692 #define IDS_LIKE 693 #define IDC_PREVIEW_READ 694 #define IDS_ASK_USER_PASS 695 #define IDD_FILTER_SAVE_ATTACH 696 #define IDM_HELP 697 #define IDC_TYPES 698 #define IDC_OTHER_TABLE 699 #define IDC_BROWSE_DIR 700 #define IDC_ONLY_SEND_THIS 701 #define IDC_GROUPS_FLD 702 #define IDC_SET_GROUPS 703 #define IDS_CC 704 #define IDS_RECIPIENTS 705 #define IDS_TEXT 706 #define IDS_ATTACHMENTS 707 #define IDS_INTERNETHEADER 708 #define IDS_FOLDER_PROPERTIES_DLG 709 #define IDS_TOMORROW 710 #define IDS_NEXT_WEEK 711 #define IDC_SRC 712 #define IDC_SET_DEST 713 #define IDC_USAGE 714 #define IDC_START_TIME 715 #define IDS_ABOUT 716 #define IDC_COLLECT_SUB_MAIL 717 #define IDS_NO_MAIL_TO_FILTER 718 #define IDS_FORWARD_PREFIX 719 #define IDS_REPLY_PREFIX 720 #define IDS_SPAM 721 #define IDS_SEARCH 722 #define IDS_ITEM_FILTER 723 #define IDD_WEBDAV_PROPS 724 #define IDC_725 725 #define IDM_IMPORT_EML 726 #define IDC_CONTACTS_URL 727 #define IDS_DELETE_FOLDER_DLG 728 #define IDS_DISCONNECT 729 #define IDS_SET_DEFAULT 730 #define IDC_HTML_FONT 731 #define IDC_SET_HTML_FONT 732 #define IDS_APPEARANCE 733 #define IDS_UPDATE 734 #define IDC_EDIT_REPLY 735 #define IDC_EDIT_FORWARD 736 #define IDC_CALENDAR_URL 737 #define IDC_APW_ADMIN 738 #define IDS_ERROR_EXE_FILE 739 #define IDC_USERNAME 740 #define IDS_FROM 741 #define IDC_ADD 742 #define IDC_DATE_FORMAT 743 #define IDS_EXITING 744 #define IDS_SORT_SUBFOLDERS 745 #define IDC_DESC 746 #define IDC_948 747 #define IDS_LOADING 748 #define IDS_MOVE_FOLDER 749 #define IDC_FILTER_INDEX 750 #define IDC_FILTER_UP 751 #define IDC_FILTER_DOWN 752 #define IDM_SCRIPT_BREAK_ON_WARN 753 #define IDS_MOVE_ATTACH 754 #define IDS_ERROR_WONT_OVERWRITE_FOLDERS 755 #define IDS_NEXT_FOLDER 756 #define IDC_BAYES_READ 757 #define IDM_DELETE 758 #define IDS_SUB_FOLDER 759 #define IDS_WEEK 760 #define IDS_ERROR_READONLY_FOLDERS 761 #define IDC_TIMEZONE 762 #define IDC_LOCALTIME 763 #define IDC_HIDE_ID 764 #define IDS_SOFTWARE_UPDATE_DOWNLOAD 765 #define IDD_FILTER_SCRIPT 766 #define IDC_SCRIPT 767 #define IDS_ASK_DUPE_CONTACT 768 #define IDS_PERM_DEL_ATTACHMENTS 769 #define IDC_HTTP_PROXY 770 #define IDC_PICK_TZ 771 #define IDS_THREAD 772 #define IDS_SOFTWARE_CURRENT 773 #define IDS_TRANSLATION_AUTHORS 774 #define IDD_CSV_IMPORT 775 #define IDC_THEME 776 #define IDS_BCC 777 #define IDS_CLICK_TO_ADD 778 #define IDS_779 779 #define IDC_BROWSE_THEMES 780 #define IDD_OUTLOOK_EXPORT 781 #define IDS_SUMMARY 782 #define IDC_BROWSE_FOLDER 783 #define IDS_MERGE_TEMPLATE 784 #define IDS_MERGE_FILE 785 #define IDS_COPY 786 #define IDD_SETTINGS 787 #define IDS_DELETE_SYS_FOLDER 788 #define IDS_789 789 #define IDS_CAL_SDAY_TUE 790 #define IDS_791 791 #define IDS_792 792 #define IDS_793 793 #define IDS_794 794 #define IDS_MBOX_IMPORT 795 #define IDC_SET_READ 796 #define IDS_CAL_SDAY_WED 797 #define IDS_CAL_SDAY_THU 798 #define IDS_CAL_SDAY_FRI 799 #define IDS_CAL_SDAY_SAT 800 #define IDS_CAL_LDAY_SUN 801 #define IDS_CAL_LDAY_MON 802 #define IDS_CAL_LDAY_TUE 803 #define IDS_CAL_LDAY_WED 804 #define IDS_CAL_LDAY_THU 805 #define IDS_CAL_LDAY_FRI 806 #define IDS_CAL_LDAY_SAT 807 #define IDS_CAL_SMONTH_JAN 808 #define IDS_CAL_SMONTH_FEB 809 #define IDS_CAL_SMONTH_MAR 810 #define IDS_CAL_SMONTH_APR 811 #define IDS_CHANGED 812 #define IDS_CAL_SMONTH_MAY 813 #define IDS_CAL_SMONTH_JUN 814 #define IDS_CAL_SMONTH_JUL 815 #define IDS_CAL_SMONTH_AUG 816 #define IDS_CAL_SMONTH_SEP 817 #define IDS_CAL_SMONTH_OCT 818 #define IDS_CAL_SMONTH_NOV 819 #define IDS_CAL_SMONTH_DEC 820 #define IDS_CAL_LMONTH_JAN 821 #define IDS_EVENT_DUE 822 #define IDC_BAYES_THRESHOLD 823 #define IDS_CAL_LMONTH_FEB 824 #define IDS_CAL_LMONTH_MAR 825 #define IDS_CAL_LMONTH_APR 826 #define IDS_CAL_LMONTH_MAY 827 #define IDS_CAL_LMONTH_JUN 828 #define IDS_CAL_LMONTH_JUL 829 #define IDS_CAL_LMONTH_AUG 830 #define IDC_PREVIEW_SECONDS 831 #define IDS_CAL_LMONTH_SEP 832 #define IDC_SOCKS5_PASSWORD 833 #define IDS_CAL_LMONTH_NOV 834 #define IDS_CAL_LMONTH_DEC 835 #define IDC_RECEIVE_SSL 836 #define IDC_SEND_CHARSET1 837 #define IDC_SEND_CHARSET2 838 #define IDS_DELETE_ASK 839 #define IDS_DONT_ADJUST_TZ 840 #define IDC_DELETE_AFTER 841 #define IDC_HTML_IMAGES 842 #define IDC_AUTO_DELETE_EXE 843 #define IDD_GROUP 844 #define IDC_SEND_AUTH_TYPE 845 #define IDM_HOMEPAGE 846 #define IDC_GROUP_NAME 847 #define IDC_GROUP_LIST 848 #define IDC_START_DATE 849 #define IDC_850 850 #define IDS_TODAY 851 #define IDC_REC_ASCII_CP 852 #define IDS_FOLDER_OUTBOX 853 #define IDS_FOLDER_SENT 854 #define IDS_780 855 #define IDS_FOLDER_FILTERS 856 #define IDS_FOLDER_CONTACTS 857 #define IDS_ATTACHMENTS_NAME 858 #define IDS_ATTACHMENTS_DATA 859 #define IDS_ANYWHERE 860 #define IDS_TODO 861 #define IDS_FOLDER_TEMPLATES 862 #define IDS_FOLDER_GROUPS 863 #define IDS_FOLDER_CALENDAR 864 #define IDS_DEFAULT_CHARSET_RECEIVE 865 #define IDS_DUE 866 #define IDS_CLICK_TO_CREATE 867 #define IDC_REPEAT 868 #define IDS_1101 869 #define IDS_ERROR_ALREADY_ONLINE 870 #define IDS_NOT_LOADED 871 #define IDC_SHOW_LOCATION 872 #define IDS_ERROR_MAPI_NOT_INSTALLED 873 #define IDC_CALENDAR 874 #define IDS_PLUGINS_NO_CONF 875 #define IDC_TAB1 876 #define IDC_TAB2 877 #define IDC_CHECK_EVERY 878 #define IDS_1116 879 #define IDC_FILTER_ACTIONS 880 #define IDC_LOGIN 881 #define IDC_MSG_STORE 882 #define IDC_SRC_FOLDERS 883 #define IDC_FOLDER 884 #define IDS_ADD_ALL_CONTACTS 885 #define IDS_MAPI_LOGIN_FAILED 886 #define IDS_ACCSTAT_IDLE 887 #define IDS_BROWSE_FOLDER 888 #define IDS_3 889 #define IDS_ASK_ADMIN_PASS 890 #define IDS_LINKS 891 #define IDC_EXTRA_HEADERS 892 #define IDC_FORMAT 893 #define IDM_TABLE2 894 #define IDC_RECEIVE_AUTH_TYPE 895 #define IDS_896 896 #define IDC_SUSPECT_FOLDER 897 #define IDM_SET_UNREAD 898 #define IDS_HTML_TAGS 899 #define IDC_PROGRESS_TBL 900 #define IDS_DELETE 901 #define IDC_SET_SUSPECT_FOLDER 902 #define IDC_SEND_TABLE 903 #define IDS_838 904 #define IDC_TEMPLATE 905 #define IDS_1082 906 #define IDC_TXT_DAYS 907 #define IDC_FOLDERS 908 #define IDD_SECURITY 909 #define IDS_9 910 #define IDS_REMOVE_DUPES 911 #define IDC_SHOW_TOTALS 912 #define IDC_913 913 #define IDC_REMINDER_TYPE 914 #define IDS_ALWAYS_SHOW_REMOTE_CONTENT 915 #define IDC_TXT_SIZE 916 #define IDS_ERROR_FILE_EMPTY 917 #define IDC_REMINDER_PARAM 918 #define IDC_REMINDER_UNIT 919 #define IDD_EML_IMPORT 920 #define IDC_HOME_TABLE 921 #define IDC_APR_NONE 922 #define IDS_OPEN_CONTACT 923 #define IDS_THREAD_SELECT 924 #define IDS_THREAD_DELETE 925 #define IDS_THREAD_IGNORE 926 #define IDS_NONE 927 #define IDS_ALL 928 #define IDS_CONVERT_SELECTION 929 #define IDS_MULTIPLE_ITEMS 930 #define IDC_GROUP 931 #define IDC_MAPPING 932 #define IDS_933 933 #define IDS_934 934 #define IDC_TAB 935 #define IDC_HAS_FILTERS 936 #define IDC_MODE 937 #define IDC_MERGE 938 #define IDC_APPEND 939 #define IDC_CHECK_DEFAULT 940 #define IDC_EVERY 941 #define IDC_LOAD 942 #define IDS_ERROR_CANT_WRITE 943 #define IDS_944 944 #define IDS_SAVE_TO_OUTBOX 945 #define IDS_INC_BETA 946 #define IDS_947 947 #define IDC_TIME_TYPE 948 #define IDS_FOLDER_TRASH 949 #define IDD_FILES_EXPORT 950 #define IDS_WAITING_FOR 951 #define IDC_END_DATE 952 #define IDC_HAM 953 #define IDC_SPAM 954 #define IDC_FALSE_NEG 955 #define IDC_FALSE_POS 956 #define IDC_EFFICIENCY 957 #define IDS_RECEIPT_ASK 958 #define IDS_ERROR_NO_HELP 959 #define IDC_END_TIME 960 #define IDC_869 961 #define IDS_ERROR_CONNECTION 962 #define IDS_CREATE_FOLDER 963 #define IDC_ALL_DAY 964 #define IDC_DEF_REPLYALL_SETTING 965 #define IDS_DEFAULT_CHARSET_SEND 966 #define IDC_WEDNESDAY 967 #define IDS_WARN_NO_SECURITY 968 #define IDC_969 969 #define IDC_LAUNCH_HELP 970 #define IDC_THURSDAY 971 #define IDD_LANG 972 #define IDC_LANG 973 #define IDC_FRIDAY 974 #define IDS_975 975 #define IDS_976 976 #define IDD_FILTER_REPLY 977 #define IDD_FILTER_FORWARD 978 #define IDC_SATURDAY 979 #define IDC_SUNDAY 980 #define IDC_REC_PORT 981 #define IDS_INSPECT 982 #define IDS_FOLDER_INBOX 983 #define IDC_OCCURRENCES 984 #define IDC_CONFIGURE_REMOTE_CONTENT 985 #define IDC_TABLE3 986 #define IDC_BLACKLIST 987 #define IDC_ATTACHMENTS 988 #define IDS_GNUPG_ERR_NO_BOUNDARY 989 #define IDS_GNUPG_ERR_NO_MIME 990 #define IDS_ASK_FORWARD_ATTACHMENTS 991 #define IDC_ALL 992 #define IDC_MARK_REPLIED 993 #define IDC_MARK_FORWARDED 994 #define IDS_BOUNCE 995 #define IDS_REMOVE_WHITELIST 996 #define IDC_UNDELETE 997 #define IDS_ERROR_NO_CONFIG_SEND 998 #define IDS_ERROR_NO_CONFIG_RECEIVE 999 #define IDC_MAIL 1000 #define IDC_CONTACTS 1001 #define IDC_NAME 1002 #define IDC_TYPE 1003 #define IDC_EMAIL 1004 #define IDS_SEND 1005 #define IDC_REPLY_TO 1006 #define IDC_NICK 1007 #define IDM_SCRIBE_FAQ 1008 #define IDC_SPOUSE 1009 #define IDC_HAS_CAL_EVENTS 1010 #define IDC_QUOTE_STR 1011 #define IDC_WRAP_COLS 1012 #define IDS_ASK_DELETE_DUPES 1013 #define IDS_GNUPG_ERR_NO_RECIP 1014 #define IDC_START_IN 1015 #define IDC_SET_START_IN 1016 #define IDM_LAYOUT4 1017 #define IDC_DOWN 1018 #define IDM_REFRESH 1019 #define IDS_ERROR_REG_WRITE 1020 #define IDS_ERROR_NEED_INSTALL 1021 #define IDC_SMTP_SERVER 1022 #define IDC_SMTP_NAME 1023 #define IDC_REC_SERVER 1024 #define IDC_REC_NAME 1025 #define IDC_REC_PASSWORD 1026 #define IDC_REC_CHECK 1027 #define IDC_POP3_LEAVE 1028 #define IDC_LAST 1030 #define IDC_QUOTE 1031 #define IDC_HOME_FAX 1032 #define IDC_HOME_STREET 1033 #define IDC_ACCOUNTS 1034 #define IDC_HOME_SUBURB 1035 #define IDC_FONT 1036 #define IDC_DESCRIPTION 1037 #define IDC_NEW_MAIL_SOUND 1038 #define IDS_MAIL_MESSAGE 1039 #define IDC_SMTP_DOMAIN 1040 #define IDC_HOME_STATE 1041 #define IDC_REPLYWITHSIG 1042 #define IDC_HOME_COUNTRY 1043 #define IDS_POP3 1044 #define IDC_HOME_PHONE 1045 #define IDC_HOME_MOBILE 1046 #define IDC_HOME_IM 1047 #define IDC_HOME_WEBPAGE 1048 #define IDC_NOTE 1049 #define IDC_DEFAULT 1050 #define IDC_TRAY 1051 #define IDS_DELETEFOLDER 1052 #define IDC_CHECKDIALUP 1053 #define IDC_SET_FONT 1054 #define IDC_POP3_ON_START 1055 #define IDC_LIST 1056 #define IDC_START_WEEK 1057 #define IDC_REFRESH 1058 #define IDS_COPY_PATH 1059 #define IDC_SET_SOUND 1060 #define IDS_OE_IMPORT 1061 #define IDS_1062 1062 #define IDM_MENU_1063 1063 #define IDC_DEL_SRC_FOLDER 1064 #define IDC_PICK_FOLDER 1065 #define IDC_WRAP 1066 #define IDC_NO_SPAM_TRASH 1067 #define IDC_MSG 1069 #define IDC_KEY 1070 #define IDC_DONT_WARN 1071 #define ID_YES 1072 #define ID_NO 1073 #define IDC_REGISTER_CLIENT 1074 #define IDC_SOCKS5_SERVER 1075 #define IDC_SOCKS5_USER 1076 #define IDC_REMINDER_ADD 1077 #define IDC_USE_TEMPLATE 1078 #define IDC_SET_FOLDER 1079 #define IDC_REMINDERS 1080 #define IDC_AVAILABLE_TYPE 1081 #define IDC_AVAILABLE 1082 #define IDS_ERROR_NO_SPACE 1083 #define IDC_TEMPLATES 1084 #define IDS_RECEIPT_BODY 1085 #define IDS_SPAM_PROB 1086 #define IDS_IS_WHITELIST 1087 #define IDC_BUSY 1088 #define IDS_ERROR_MAPI_DEFAULT 1089 #define IDS_ERROR_SOFTWARE_KEY 1090 #define IDM_MANAGE_MAIL_STORES 1091 #define IDC_PUBLIC 1092 #define IDC_PRIVATE 1093 #define IDC_NEW_FILTER_ACTION 1094 #define IDC_DELETE_FILTER_ACTION 1095 #define IDS_MAIL 1096 #define IDC_RADIO1 1097 #define IDC_RADIO2 1098 #define IDC_DEF_ACTION 1099 #define IDC_WORK_COUNTRY 1100 #define IDC_WORK_PHONE 1101 #define IDC_WORK_MOBILE 1102 #define IDC_WORK_IM 1103 #define IDC_WORK_STATE 1104 #define IDC_WORK_POSTCODE 1105 #define IDC_WORK_STREET 1106 #define IDC_WORK_FAX 1107 #define IDC_WORK_SUBURB 1108 #define IDC_HOME_POSTCODE 1109 #define IDC_COMPANY 1110 #define IDC_WORK_WEBPAGE 1111 #define IDC_SOCKS5 1112 #define IDC_DELETE 1113 #define IDS_MAIL_MERGE_Q 1114 #define IDC_ADD_SRC_FOLDER 1115 #define IDC_PROPERTIES 1116 #define IDD_OUTLOOK_ADD_FLD 1117 #define IDS_ASK_FOLDER_PASS 1118 #define IDC_DEF_SEND_TXT 1119 #define IDS_ERROR_SCRIPT_COMPILE 1120 #define IDS_ANY 1121 #define IDS_ERROR_NOTHING_TO_UPGRADE 1122 #define IDC_PAGE_ALL 1123 #define IDC_SAVE 1124 #define IDC_PAGE_RANGES 1125 #define IDS_PASSWORD_SAVED 1126 #define IDS_RESIZE_JPEG_QUAL 1127 #define IDC_TXT_MIN 1128 #define IDC_TXT_DOWNLOAD 1129 #define IDC_TXT_KBS 1130 #define IDM_SCRIPTING_CONSOLE 1131 #define IDD_PRINT_PREVIEW 1132 #define IDC_SAVE_IMG 1133 #define IDS_ACCSTAT_SETUP 1134 #define IDS_ACCSTAT_CONNECT 1135 #define IDS_ACCSTAT_TRANS 1136 #define IDS_ACCSTAT_WAIT 1137 #define IDS_ACCSTAT_DELETING 1138 #define IDS_ACCSTAT_FINISHED 1139 #define IDS_ACCSTAT_CANCELLED 1140 #define IDS_ERROR 1141 #define IDS_ENTER_KEY 1142 #define IDM_SCRIPT_DEBUG 1143 #define IDC_UPDATE 1144 #define IDC_1145 1145 #define IDS_CONTAINS 1146 #define IDS_STARTS_WITH 1147 #define IDS_ENDS_WITH 1148 #define IDS_SERVER 1149 #define IDS_1150 1150 #define IDM_MENU_1151 1151 #define IDS_TIME 1152 #define IDM_HTML_EDITOR 1153 #define IDS_ERROR_MAPI_INIT_FAILED 1154 #define IDD_CAL_RECUR 1155 #define IDS_WEEKS 1156 #define IDC_DEL 1157 #define IDC_SHOW_EXTRA 1158 #define IDC_IMAGE 1159 #define IDC_GUEST_ENTRY 1160 #define IDC_ADD_GUEST 1161 #define IDS_MISSING_PARENT 1162 #define IDS_ORIGINAL_MESSAGE 1163 #define IDS_PORTABLE_Q 1164 #define IDC_TABLE 1165 #define IDS_NO_EVENTS 1166 #define IDS_UNREAD_MAIL 1167 #define IDS_1168 1168 #define IDC_SHOW_ADDR 1169 #define IDS_AND_OPERATOR 1170 #define IDS_OR_OPERATOR 1171 #define IDS_MEMBER_OF_GROUP 1172 #define IDS_ACTION_MOVE_FOLDER 1173 #define IDS_ACTION_SOUND 1174 #define IDS_ACTION_OPEN 1175 #define IDS_ACTION_EXECUTE 1176 #define IDS_ACTION_SET_COLOUR 1177 #define IDS_ACTION_SET_LABEL 1178 #define IDS_ACTION_EMPTY_FOLDER 1179 #define IDS_ACTION_MARK_SPAM 1180 #define IDS_ACTION_SAVE_ATTACHMENTS 1181 #define IDS_ACTION_DELETE_ATTACHMENTS 1182 #define IDS_ACTION_CREATE_FOLDER 1183 #define IDS_SELECT_FOLDER 1184 #define IDC_BAYES_INCREMENTAL 1185 #define IDC_BAYES_DEBUG 1186 #define IDC_IMG_NOTES_TBL 1187 #define IDC_WORK_TABLE 1188 #define IDC_OUTGOING 1189 #define IDS_ERROR_FILE_DOESNT_EXIST 1190 #define IDS_ERROR_FOLDER_DOESNT_EXIST 1191 #define IDS_IMPORT 1192 #define IDD_SCRIBE_EXPORT 1193 #define IDC_INCOMING 1194 #define IDM_MENU_1195 1195 #define IDC_DEST 1196 #define IDC_HAS_GROUPS 1198 #define IDC_CAL_URL 1199 #define IDC_SPAM_FLD 1200 #define IDC_SET_SPAM 1201 #define IDC_SPAM_FOLDER 1202 #define IDC_SET_SPAM_FOLDER 1203 #define IDC_PASSWORD 1204 #define IDS_ERROR_SERVER_CONNECT 1205 #define IDC_DST 1206 #define IDS_ERROR_PRINT_FAILED 1214 #define IDS_ASK_ACCOUNT_PASSWORD 1215 #define IDS_SELECT_IO 1216 #define IDC_GUESTS 1217 #define IDS_DONT_SHOW_AGAIN 1218 #define IDS_INSTALL 1219 #define IDC_1220 1220 #define IDS_EDIT_MAIL_STORES 1224 #define IDS_ERROR_CANT_CREATE_FOLDER 1225 #define IDS_ERROR_CANT_CREATE_DB 1226 #define IDS_SHOW_CONSOLE 1227 #define IDC_STATUS 1228 #define IDS_SELECT_ACCOUNT 1229 #define IDS_1235 1235 #define IDC_MONDAY 1238 #define IDC_TUESDAY 1239 #define IDS_MONTHS 1246 #define IDC_NEVER 1249 #define IDC_1250 1250 #define IDC_AFTER 1251 #define IDC_AFTER_COUNT 1252 #define IDC_1254 1254 #define IDC_ON 1255 #define IDC_ON_DATE 1256 #define IDC_SUMMARY 1258 #define IDS_ALWAYS_SHOW 1260 #define IDS_OPEN_SOURCE 1262 #define IDS_DEFAULT 1263 #define IDS_IMPORT_SETTINGS_Q 1264 #define IDS_IMPORT_SETTINGS_DONE 1265 #define IDS_DELETE_ATTACHMENTS 1266 #define IDS_LANGUAGE 1267 #define IDS_1268 1268 #define IDS_SHOW_REMOTE_CONTENT 1274 #define IDC_DELETE_DAYS 1275 #define IDS_RESIZE_IMGS 1277 #define IDD_REMOTE_CONTENT 1279 #define IDC_WHITELIST 1282 #define IDS_REMOTE_CONTENT_MSG 1287 #define IDS_GNUPG_ERR_SAVE_FAILED 1291 #define IDS_MESSAGES_SENT 1293 #define IDS_MAIL_MERGE_EMPTY 1294 #define IDS_GNUPG_ERR_NO_OUTPUT 1295 #define IDS_FILTER_MAILINGLIST 1299 #define IDC_ADVANCED 1300 #define IDS_ACTION_COPY 1301 #define IDC_PAGE_PARTIAL 1302 #define IDD_CSV_EXPORT 1304 #define IDS_1305 1305 #define IDS_EXPORT_MSG 1312 #define IDS_MARK_ALL_READ 1313 #define IDD_MANAGE_FOLDERS 1314 #define IDC_MS_TBL 1315 #define IDC_OPEN_MS 1316 #define IDC_CLOSE_MS 1317 #define IDC_CREATE_MS 1318 #define IDC_CONVERT_MS 1320 #define IDC_REPAIR_MS 1321 #define IDC_MAIL_STORES 1324 #define IDS_1325 1325 #define IDS_1326 1326 #define IDC_MSG_ID 1327 #define IDC_FILTER 1328 #define IDS_GNUPG_ERR_WRONG_SEGS 1329 #define IDS_RELATIVE_TIMES 1333 #define IDS_1335 1335 #define IDS_CHECKING_OBJECTS 1336 #define IDS_ERROR_FOLDER_NOT_LOADED 1337 #define IDS_1338 1338 #define IDS_1339 1339 #define IDS_1340 1340 #define IDS_1341 1341 #define IDD_REPLICATE 1342 #define IDM_CHECK_UPDATE 1350 #define IDS_1353 1353 #define IDS_1354 1354 #define IDS_ERROR_FONT_SETTINGS 1356 #define IDC_SEC_AUTH 1357 #define IDC_EDIT_CONTROL 1359 #define IDC_DELETE_LARGER 1360 #define IDC_DELETE_SIZE 1361 #define IDC_CLEAR 1364 #define IDS_ADD_TO_CAL 1367 #define IDS_ADD_TO_DICTIONARY 1368 #define IDS_ERROR_CANT_ADD_DICT 1370 #define IDS_SPELL_CHECK 1371 #define IDS_SPELL_DICT 1372 #define IDS_MAILSTORE_UPGRADE_Q 1373 #define IDS_ERROR_IMPORT 1374 #define IDS_ERROR_REPLICATION_FAILED 1375 #define IDS_ERROR_IMPORT_COUNT 1388 #define IDS_GNUPG_ERR_NOT_INSTALLED 1458 #define IDD_FOLDER_SELECT 1469 #define IDS_REPARSE 1471 #define IDD_FOLDER_FORMAT 1472 #define IDS_GNUPG_PSW_PROMPT 1473 #define IDC_V1 1481 #define IDC_V2 1482 #define IDC_UP 1483 #define IDS_GROWL_SUPPORT 1485 #define IDS_GROWL 1486 #define IDC_MSG_DEL_ACTION 1487 #define IDC_MSG_DEL_NEXT 1488 #define IDC_MSG_DEL_CLOSE 1489 #define IDC_MSG_DEL_PREV 1490 #define IDS_KILL 1497 #define IDC_RECEIVE_LOG 1514 #define IDS_EMAIL_PROGRESS 1516 #define IDS_COPY_LOG_TO_CLIP 1517 #define IDC_SMTP_PORT 1519 #define IDS_RESIZE_MAX_PX 1522 #define IDS_RESIZING 1523 #define IDS_RESIZE_MAX_SIZE 1524 #define IDS_UPGRADE_KEY 1525 #define IDC_UPGRADE 1526 #define IDS_MAIL2_DEPRECATED 1527 #define IDS_SSL_DEBUG 1528 #define IDS_CONSOLE 1529 #define IDC_SPELL_LANG 1531 #define IDC_WIDTH 1535 #define IDC_SLIDE 1537 #define IDC_BAYES_DELETE_ON_SERVER 1541 #define IDC_BAYES_DELETE_ATTACHMENTS 1543 #define IDC_TENTATIVE 1548 #define IDS_DELETING 1549 #define IDS_DONT_SHOW_EMOJI 1550 #define IDS_HELP 1551 #define IDS_DESKTOP 1552 #define IDS_PORTABLE 1553 #define IDC_BTNS_TABLE 1556 #define IDS_GNUPG_CHECKING 1558 #define IDS_GNUPG_ERR_NO_SIGN_ENC 1561 #define IDS_GNUPG_ERR_CANT_START 1565 #define IDS_GNUPG_ERR_CODE 1568 #define IDS_GNUPG_ERR_NO_CONTENT 1570 #define IDS_GNUPG_ERR_READ_FAIL 1572 #define IDS_GNUPG_ERR_NO_MAIL 1574 #define IDS_GNUPG_ERR_NO_ROOT 1578 #define IDS_GNUPG_ERR_WRONG_MIME 1580 #define IDS_GNUPG_ERR_NO_ENC_MIME 1581 #define IDS_GNUPG_ERR_NO_ID 1582 #define IDS_GNUPG_DECRYPT_CANCEL 1583 #define IDS_GNUPG_ERR_NO_DATA 1584 #define IDS_GNUPG_ERR_DECRYPT_FAIL 1585 #define IDS_GNUPG_ERR_NO_SENDER_EMAIL 1586 #define IDS_GNUPG_ERR_NO_PRIV_KEY 1587 #define IDS_GNUPG_ERR_NO_PUB_KEY 1588 #define IDS_GNUPG_ERR_IMPORT_FAIL 1589 #define IDS_GNUPG_ERR_SIGN_ENC_CANCEL 1590 #define IDS_GNUPG_ERR_TEMP_WRITE 1591 #define IDS_GNUPG_ERR_EXPORT_TEMP 1592 #define IDS_GNUPG_ERR_NEW_ATTACH_FAIL 1593 #define IDS_GNUPG_ERR_REPARENT 1594 #define IDS_GNUPG_ERR_RECIP_NO_EMAIL 1595 #define IDS_GNUPG_ERR_WRITE 1596 #define IDS_GNUPG_ERR_ENCRYPT_FAIL 1597 #define IDS_GNUPG_ERR_ONE_OR_MORE 1598 #define IDS_GNUPG_DECRYPT_OK 1599 #define IDS_GNUPG_ENCRYPTED 1600 #define IDS_GNUPG_ENCRYPTED_AND_SIGNED 1601 #define IDS_GNUPG_SIGNED 1602 #define IDS_GNUPG_GOOD_SIG 1603 #define IDS_GNUPG_BAD_SIG 1604 #define IDS_GNUPG_SIG_MSG 1605 #define IDS_GNUPG_SIG_NO_ID 1606 #define IDS_GNUPG_ERR_INVALID_DECRYPTION 1607 #define IDS_GNUPG_CHECK_MSG 1608 #define IDS_SHOW_SCRIPT_DEBUGGER 1609 #define IDC_ACC_LOG 1613 #define IDC_1615 1615 #define IDC_1616 1616 #define IDS_SHOW_SIZE_IN_KIB 1617 #define IDS_GNUPG_GET_GPG 1619 #define IDC_START_REL 1620 #define IDC_END_REL 1621 #define IDS_YESTERDAY 1622 #define IDC_1626 1626 #define IDS_MBOX_SELECT_FOLDER 2000 #define IDS_MBOX_READING 2001 #define IDS_MBOX_EXPORT 2002 #define IDS_MBOX_EXPORT_FOLDER 2003 #define IDS_MBOX_WRITING 2004 #define IDS_STORE2_ERROR_COMPACT 2005 #define IDS_STORE2_DISCARD_FIX 2006 #define IDS_STORE2_SAVE_DEBUG 2007 #define IDS_NAME 2008 #define IDS_NO_MAIL_TO_SEND 2009 #define IDS_NO_OUTGOING_FOLDER 2010 #define IDS_ERROR_NO_NAME_EMAIL 2011 #define IDS_APPNAME 2012 #define IDS_SURNAME 2013 #define IDC_SET_CALENDER 2014 #define IDC_NEW_MAIL_NOTIFY 2015 #define IDD_NEW_MAIL_NOTIFY 2016 #define IDC_NEW_MAIL 2017 #define IDC_REMEMBER_PSW 2020 #define IDS_36 2021 #define IDS_37 2022 #define IDC_OPEN 2023 #define IDC_GLYPH_SUB 2031 #define IDC_FILTERS 2032 #define IDD_OUTLOOK_IMPORT 2040 #define IDD_WARN_DEFAULT 2041 #define IDC_POP_RECIP 2042 #define IDD_ADDCONTACT 2043 #define IDD_CONTACT 2044 #define IDD_FOLDER_NAME 2045 #define IDD_POPVIEW 2047 #define IDC_FPR_NONE 3000 #define IDC_FPR_USER 3001 #define IDC_FPR_ADMIN 3002 #define IDC_FPW_NONE 3003 #define IDC_FPW_USER 3004 #define IDC_FPW_ADMIN 3005 #define IDM_BOUNCE 40000 #define IDM_SOFTWARE_KEY 40001 #define IDM_UPGRADE_MAIL_STORES 40002 #define IDM_OPTIONS 40004 #define IDM_PRINT 40005 #define IDM_PRINTSETUP 40006 #define IDM_EXIT 40007 #define IDM_NEW_EMAIL 40008 #define IDM_REPLY 40009 #define IDM_REPLY_ALL 40010 #define IDM_FORWARD 40011 #define IDM_SEND_MAIL 40012 #define IDM_NEW_CONTACT 40014 #define IDM_ABOUT 40019 #define IDM_IMPORT_NS_CONTACTS 40020 #define IDM_IMPORT_OUTLOOK_PAB 40025 #define IDM_IMPORT_OUTLOOK_ITEMS 40027 #define IDM_NEW_FILTER 40038 #define IDM_IMPORT_TEXT_MBOX 40047 #define IDM_EXPORT_TEXT_MBOX 40048 #define IDM_IMP_MBX_EMAIL 40049 #define IDM_IMP_DBX_EMAIL 40050 #define IDM_NEW_DRAFT 40060 #define IDM_NO_TEMPLATES 40061 #define IDM_FILTER_CURRENT_FOLDER 41010 #define IDM_IMP_EUDORA_ADDR 41043 #define IDM_IMP_MOZILLA_ADDR 41045 #define IDM_FIND 41055 #define IDM_IMPORT_CSV 41065 #define IDM_WORK_OFFLINE 41075 #define IDM_EXPORT_CSV 41085 #define IDM_NEW_GROUP 41095 #define IDM_SECURITY 41105 #define IDM_BAYES_SETTINGS 41116 #define IDM_BAYES_CHECK 41117 #define IDM_BUILD_BAYES_DB 41118 #define IDM_LOGOUT 41129 #define IDM_FOLDERS_DOWN_LEFT 41142 #define IDM_PREVIEW_ALONG_BOTTOM 41143 #define IDM_LAYOUT1 41144 #define IDM_LAYOUT2 41145 #define IDM_LAYOUT3 41146 #define IDM_EXPORT_OUTLOOK_ITEMS 41147 #define IDM_CRASH 41148 #define IDM_TUTORIALS 41149 #define IDM_DEBUG_INFO 41150 #define IDM_VERSION_HISTORY 41151 #define IDM_MEMECODE 41152 #define IDM_SET_READ 41155 #define IDM_IMP_MOZILLA_MAIL 41156 #define IDM_DEBUG_FILTERS 41157 #define IDM_FILTERS_DISABLE 41158 #define IDM_DUMP_MEM 41159 #define IDM_FILTER_SELECTION 41160 #define IDM_EXPORT_SCRIBE 41161 #define IDM_TOOLS_MENU 41163 #define IDM_REPLICATE 41164 #define IDM_EXPORT 41165 -#define IDM_COPY 'copy' -#define IDM_CUT 'cut ' -#define IDM_PASTE 'past' +#define IDM_COPY Lgi4CC("copy") +#define IDM_CUT Lgi4CC("cut ") +#define IDM_PASTE Lgi4CC("past") diff --git a/src/BayesianFilter.cpp b/src/BayesianFilter.cpp --- a/src/BayesianFilter.cpp +++ b/src/BayesianFilter.cpp @@ -1,1855 +1,1855 @@ #include #include "Scribe.h" #include "resdefs.h" #include "BayesianFilter.h" #include "lgi/common/LgiRes.h" #include "lgi/common/SpellCheck.h" #define SECONDS(n) ((n) * 1000) #define TIMEOUT_BAYES_LOAD SECONDS(10) #define TIMEOUT_SPELL_CHECK SECONDS(3) #define TIMEOUT_UPDATE_REBUILD SECONDS(2) #define TIMEOUT_BAYES_IDLE (50) // ms, out of 100ms idle timer. #define WHITELIST_MY_EMAIL 0 #define WHITELIST_CONTACTS 0 #define STORE_SIZE 16 #define WORD_INTEREST_CENTER 0.5 static const char HamWordsFile[] = "hamwords.idx"; static const char SpamWordsFile[] = "spamwords.idx"; static const char WhiteListFile[] = "whitelist.idx"; void ProcessWords(LString Words, std::function Callback) { char *end = Words.Get() + Words.Length(); if (!Callback) { LAssert(0); return; } for (char *s = Words; s < end; ) { char *e = s; while (*e && *e != ' ' && e < end) e++; if (*e == ' ') *e = 0; Callback(s); s = e + 1; } } double WordInterest(double d) { d -= WORD_INTEREST_CENTER; if (d < 0) d *= -1; return d; } class Token { public: LString Word; double Prob = 0.0; double Interest = 0.0; }; class TokenStore { public: constexpr static int StoreSize = STORE_SIZE; int Used; Token *Tok[StoreSize]; TokenStore() { Used = 0; ZeroObj(Tok); } ~TokenStore() { for (int i=0; i 0) return Tok[Used-1]->Interest; return 0; } double Prob(LStream *Log) { int i; if (Log) { Log->Print("%s\n", LLoadString(IDS_SPAM_PROB)); for (i=0; iPrint("\t[%i] '%s'=%f\n", i, Tok[i]->Word.Get(), Tok[i]->Prob); } } // ab //------------------- //ab + (1 - a)(1 - b) if (Used == 0) return 0.5; double top, bottom; top = Tok[0]->Prob; bottom = 1 - Tok[0]->Prob; for (i=1; iProb; bottom *= 1 - Tok[i]->Prob; } double Result = top / (top + bottom); if (Log) Log->Print("\nResult=%f\n", Result); return Result; } bool CanInsert(double i) { return Used < CountOf(Tok) || i >= Min(); } bool Insert(Token *t) { bool Status = false; if (t) { for (int i=0; iWord, t->Word) == 0) { Status = true; DeleteObj(t); goto End; } else if (t->Interest > Tok[i]->Interest) { if (Used >= CountOf(Tok)) { DeleteObj(Tok[Used-1]); memmove(Tok + i + 1, Tok + i, sizeof(Tok[0]) * (Used - i - 1)); } else { memmove(Tok + i + 1, Tok + i, sizeof(Tok[0]) * (Used - i)); Used++; } Tok[i] = t; Status = true; goto End; } } if (Used < CountOf(Tok)) { Tok[Used++] = t; Status = true; goto End; } else { DeleteObj(t); } } End: return Status; } }; class BayesianThread : public LThread, public LMutex, public LEventTargetI, public LMappedEventSink { public: enum ThreadState { BayesLoading, BayesReady, BayesExiting, }; struct Build { /// Empty all the word databases first... otherwise append new words on top of old ones bool ResetDb; LHashTbl,int> WhiteList; int HamEmailCount; LHashTbl,int> HamWords; int SpamEmailCount; LHashTbl,int> SpamWords; Build(bool Reset) : WhiteList(0, -1), HamWords(0, -1), SpamWords(0, -1) { ResetDb = Reset; HamEmailCount = 0; SpamEmailCount = 0; HamWords.SetMaxSize(HamWords.Unlimited); SpamWords.SetMaxSize(SpamWords.Unlimited); } void InsertWhiteList(const char *Word) { int c = WhiteList.Find(Word); WhiteList.Add(Word, c < 0 ? 1 : c + 1); } void InsertHamWords(const char *Word) { int c = HamWords.Find(Word); HamWords.Add(Word, c < 0 ? 1 : c + 1); } void InsertSpamWords(const char *Word) { int c = SpamWords.Find(Word); SpamWords.Add(Word, c < 0 ? 1 : c + 1); } }; /// This is used when the GUI thread requests an email to be /// tested by the Bayesian Classifier. Initially the GUI thread /// fills out this structure and passes it over to the worker /// thread and it then does the calculation and passes it back /// to the GUI thread for actioning. struct Test { bool Analyse = false; LStringPipe Log; LString FromAddr; LString MsgRef; LString Words; double Score = 0.0; bool WhiteListed = false; }; /// This is a change of classification container for /// when the email changes from ham to spam or the reverse. struct Change { LString Words; LString Str; ScribeMailType OldType = BayesMailUnknown; ScribeMailType NewType = BayesMailUnknown; bool RemoveWhite = false; bool IncrementWhite = false; }; private: ScribeWnd *App; LAutoPtr WhiteList; LAutoPtr Ham; LAutoPtr Spam; ThreadState State; LArray< LAutoPtr > Work; LArray< LAutoPtr > Tests; LArray< LAutoPtr > Results; LArray< LAutoPtr > Changes; LAutoPtr Tokens; uint64_t CheckTextTs = 0; LAutoPtr CheckText; void Empty() { WhiteList.Reset(new LWordStore); Spam.Reset(new LWordStore); Ham.Reset(new LWordStore); #define SetListFile(list, file) \ if (!list->GetFile()) \ { \ if (auto s = FindWordDb(file)) \ { \ list->SetFile(s); \ } \ } SetListFile(WhiteList, WhiteListFile); SetListFile(Spam, SpamWordsFile); SetListFile(Ham, HamWordsFile); // empty the word lists WhiteList->Empty(); WhiteList->Serialize(0, true); Spam->Empty(); Spam->Serialize(0, true); Ham->Empty(); Ham->Serialize(0, true); } public: BayesianThread(ScribeWnd *app) : LThread("BayesianThread.Thread"), LMutex ("BayesianThread.Mutex") { App = app; State = BayesLoading; Run(); } ~BayesianThread() { State = BayesExiting; int64 Start = LCurrentTime(); while (!IsExited()) { LSleep(20); if (LCurrentTime()-Start > 5000) { Terminate(); break; } } } LMessage::Result OnEvent(LMessage *Msg) override { switch (Msg->Msg()) { case M_CHECK_TEXT: CheckTextTs = LCurrentTime(); CheckText.Reset((LSpellCheck::CheckText*)Msg->A()); break; } return 0; } bool PostEvent(int Cmd, LMessage::Param a = 0, LMessage::Param b = 0, int64_t TimeoutMs = -1) override { LMessage m(Cmd, a, b); OnEvent(&m); return true; } void Add(LAutoPtr b) { if (Lock(_FL)) { Work.New() = b; Unlock(); } } void Add(LAutoPtr t) { if (Lock(_FL)) { Tests.New() = t; Unlock(); } } void Add(LAutoPtr c) { if (Lock(_FL)) { Changes.New() = c; Unlock(); } } bool GetResults(LArray< LAutoPtr > &results) { if (Lock(_FL)) { results = Results; Unlock(); } Results.Length(0); return results.Length() > 0; } ThreadState GetState() { return State; } void SetStore(LAutoPtr &s, LWordStore *ws) { if (ws && Lock(_FL)) { s.Reset(ws); Unlock(); } } LString FindWordDb(const char *Name) { LString OptPath; auto Opts = App->GetOptions(); // Look in the same folder as the options file: if (Opts && Opts->GetFile()) { LFile::Path p(Opts->GetFile()); p--; OptPath = p.GetFull(); p += Name; if (p.IsFile()) return p.GetFull(); } // Check the install folder too: LFile::Path p(LSP_APP_INSTALL); p += Name; if (p.IsFile()) return p.GetFull(); // No existing file found, so create a path using the options location: p = OptPath; p += Name; return p.GetFull(); } void OnCheckText(LSpellCheck::CheckText *Ct) { auto p = Ct->Errors.Length() ? 0.3 : 0.7; auto i = WordInterest(p); if (Tokens->CanInsert(i)) { Token *t = new Token; if (t) { t->Word = Ct->Text; t->Prob = p; t->Interest = i; Tokens->Insert(t); } } } double IsSpam(Test *t) { // Check the auto white list if (WhiteList) { long Count = WhiteList->GetWordCount(t->FromAddr); if (Count > 0) { // It's from someone we've accepted mail from before if (t->Analyse) t->Log.Print("%s\n", LLoadString(IDS_IS_WHITELIST)); t->WhiteListed = true; return 0.0; } } bool Analyse = t->Analyse; LAutoPtr Spell(App->CreateSpellObject()); Tokens.Reset(new TokenStore); if (!Ham || !Spam) { LgiTrace("%s:%i - No Ham/Spam DB loaded?\n", _FL); return 0.0; } ssize_t HamItems = Ham->Length(); ssize_t SpamItems = Spam->Length(); if (Analyse) LgiTrace("HamItems=" LPrintfSSizeT " SpamItems=" LPrintfSSizeT "\n"); if (Spell) { Spell->Check(GetHandle(), t->Words, 0, t->Words.Length()); auto Start = LCurrentTime(); while (!CheckText) // Wait for the spell check to complete { if (LCurrentTime() - Start > TIMEOUT_SPELL_CHECK) { LgiTrace("%s:%i - Bayesian filter didn't get response from spell check, continuing without.\n", _FL); break; } LSleep(10); } if (CheckText) LgiTrace("%s:%i - SpellCheck took %ims\n", _FL, (int)(LCurrentTime()-CheckTextTs)); } struct Entry { LString word; ssize_t spam = 0, ham = 0; bool spellErr = false; int Compare(Entry *e) { auto i = (spam+ham) - (e->spam+e->ham); if (i < 0) return -1; return i > 0 ? 1 : 0; } }; LArray entries; ProcessWords(t->Words, [this, t, &entries](auto w) { size_t nextErr = 0; Entry &e = entries.New(); e.word = w; e.spam = Spam->GetWordCount(w); e.ham = Ham->GetWordCount(w); size_t Cur = w - t->Words.Get(); // bool hasAt = Strchr(w, '@') != NULL; if (CheckText) { while (nextErr < CheckText->Errors.Length()) { auto &errs = CheckText->Errors[nextErr]; if (errs.Overlap(Cur)) { e.spellErr = true; nextErr++; break; } if (errs.Start < (ssize_t)Cur) nextErr++; else break; } } }); entries.Sort([](auto a, auto b) { return a->Compare(b); }); for (auto &e: entries) { double s, p, i; if (e.spam && e.ham) { s = (double) e.spam / SpamItems; p = s / ( ((double) e.ham / HamItems) + s); } else if (e.spam) p = 0.99; else if (e.ham) p = e.spellErr ? 0.6 : 0.01; else p = e.spellErr ? 0.88 : 0.4; i = WordInterest(p); if (Tokens->CanInsert(i)) { Token *t = new Token; if (t) { t->Word = e.word; t->Prob = p; t->Interest = i; Tokens->Insert(t); } } if (Analyse) LgiTrace(" %s, " LPrintfSSizeT ", " LPrintfSSizeT ", %i, %g\n", e.word.Get(), e.spam, e.ham, e.spellErr, p); } auto result = Tokens->Prob(&t->Log); if (Analyse) LgiTrace(" result=%g\n", result); return result; } void ConvertHashToBtree(LWordStore *Ws, LHashTbl,int> &Hash, int EmailCount, bool Append, LStream *Debug = NULL) { auto Items = Hash.Length(); int64 Start = LCurrentTime(); if (Debug) Debug->Print("ConvertHashToBtree(%s, %i words, %i emails)\n", Ws->GetFile(), Items, EmailCount); Ws->Empty(); for (auto i: Hash) { ssize_t Result; if (Append) { ssize_t Old = Ws->GetWordCount(i.key); Result = Ws->SetWordCount(i.key, Old + i.value); } else { Result = Ws->SetWordCount(i.key, i.value); } if (!Result) { if (Debug) Debug->Print("SetWordCount(%s, %i) failed\n", i.key, i.value); LAssert(0); return; } } if (EmailCount) { Ws->SetItems(EmailCount); } Hash.Empty(); LgiTrace("ConvertHashToBtree(%s) took %.1f sec, for " LPrintfInt64 " items.\n", Ws->GetFile(), ((double)((int64)LCurrentTime()-Start))/1000.0, Items); } void ApplyChange(Change *c, LWordStore *Ws, bool Add) { bool status = false; if (!Ws || !c) { LgiTrace("%s:%i - Invalid param: %p, %p\n", _FL, c, Ws); return; } ProcessWords(c->Words, [this, Ws, Add, &status](auto w) { ssize_t c = Ws->GetWordCount(w); if (Add) status = Ws->SetWordCount(w, c + 1) > 0; else status = Ws->SetWordCount(w, c > 0 ? c - 1 : 0) > 0; LAssert(status); }); ssize_t Total = Ws->GetItems(); status = Ws->SetItems((int)Total + (Add ? 1 : -1)); LAssert(status); } int Main() override { if (auto s = FindWordDb(HamWordsFile)) SetStore(Ham, new LWordStore(s)); if (auto s = FindWordDb(SpamWordsFile)) SetStore(Spam, new LWordStore(s)); if (auto s = FindWordDb(WhiteListFile)) SetStore(WhiteList, new LWordStore(s)); State = BayesReady; bool Notify = false; while (State == BayesReady) { LAutoPtr b; LAutoPtr t; LAutoPtr c; if (Lock(_FL)) { if (Work.Length()) { b = Work[0]; Work.DeleteAt(0, true); } else if (Tests.Length()) { t = Tests[0]; Tests.DeleteAt(0, true); } else if (Changes.Length()) { c = Changes[0]; Changes.DeleteAt(0, true); } Unlock(); } if (b) { if (b->ResetDb) Empty(); ConvertHashToBtree(Spam, b->SpamWords, b->SpamEmailCount, b->ResetDb == false); ConvertHashToBtree(Ham, b->HamWords, b->HamEmailCount, b->ResetDb == false); ConvertHashToBtree(WhiteList, b->WhiteList, 0, b->ResetDb == false); } else if (t) { t->Score = IsSpam(t); if (Lock(_FL)) { Results.New() = t; Notify = true; Unlock(); } } else if (c) { switch (c->OldType) { default: break; case BayesMailHam: ApplyChange(c, Ham, false); break; case BayesMailSpam: ApplyChange(c, Spam, false); break; } switch (c->NewType) { default: break; case BayesMailHam: ApplyChange(c, Ham, true); break; case BayesMailSpam: ApplyChange(c, Spam, true); break; } if (c->Str) { if (!WhiteList) { LgiTrace("Missing whitelist obj.\n"); } else if (c->NewType == BayesMailSpam || c->RemoveWhite) { // Make sure the email address is not in the white list... WhiteList->DeleteWord(c->Str); LAssert(WhiteList->GetWordCount(c->Str) == 0); } else if (c->IncrementWhite) { auto i = WhiteList->GetWordCount(c->Str); WhiteList->SetWordCount(c->Str, i + 1); } } } else { // No work to do... if (Notify) { App->PostEvent(M_SCRIBE_BAYES_RESULT); Notify = false; } LSleep(20); } } return 0; } }; struct BayesEvent { Mail *m; bool Loading, Done; ScribeMailType OldType, NewType; }; class BuildSpamDB { int FolderLoads = 0; int MailLoads = 0; public: ScribeWnd *App; BayesianFilter *Filter; LAutoPtr Prog; LAutoPtr Debug; // Processing part 1... load the folders: LArray Folders; // Processing part 2... convert the mail to words: struct BuildItem { Mail *m = NULL; bool loading = false; ScribeMailType type = BayesMailUnknown; void Set(Mail *mail, ScribeMailType Type) { m = mail; m->IncRef(); type = Type; loading = false; } }; LArray Items; int HamCount = 0; int SpamCount = 0; int FalsePositives = 0; int FalseNegatives = 0; int LoadFailures = 0; LAutoPtr b; BuildSpamDB(ScribeWnd *app); ~BuildSpamDB(); /// \returns true when the processing is finished bool Process(); void ProcessMail(Mail *m, ScribeMailType type); void AbortProcess(); void AddFolder(ScribeFolder *f) { Folders.Add(f); } bool IsCancelled() { return Prog ? Prog->IsCancelled() : true; } }; class BayesianFilterPriv : public LMutex { LAutoPtr Thread; public: ScribeWnd *App; uint64_t Ts; uint64_t WorkStartTs = 0; LArray Work; LAutoPtr Prog; LAutoPtr Build; BayesianFilterPriv(ScribeWnd *app) : LMutex("BayesianFilterPriv") { Ts = 0; App = app; } BayesianThread *GetThread() { if (!Thread) Thread.Reset(new BayesianThread(App)); return Thread; } bool IsCancelled() { return Build ? Build->IsCancelled() : true; } }; BuildSpamDB::BuildSpamDB(ScribeWnd *app) : App(app), Filter(app) { App->OnFolderTask(Filter->d->GetThread(), true); b.Reset(new BayesianThread::Build(true)); if (Prog.Reset(new LProgressDlg(App))) Prog->SetDescription("Scanning folders..."); LVariant i; if (App->GetOptions()->GetValue(OPT_BayesDebug, i)) { if (Debug.Reset(new LFile)) { char s[MAX_PATH_LEN]; LMakePath(s, sizeof(s), LGetExePath(), "Bayes.txt"); if (Debug->Open(s, O_WRITE)) Debug->SetSize(0); else LgiTrace("%s:%i - Couldn't open '%s'\n", _FL, s); } } } BuildSpamDB::~BuildSpamDB() { // Save stats... LVariant i; auto opts = App->GetOptions(); opts->SetValue(OPT_BayesHam, i = HamCount); opts->SetValue(OPT_BayesSpam, i = SpamCount); opts->SetValue(OPT_BayesFalsePositives, i = FalsePositives); opts->SetValue(OPT_BayesFalseNegitives, i = FalseNegatives); App->OnFolderTask(Filter->d->GetThread(), false); } void BuildSpamDB::AbortProcess() { Folders.Length(0); Items.Length(0); } bool BuildSpamDB::Process() { if (IsCancelled()) AbortProcess(); // This should execute for only a small time slice... if (Folders.Length() || FolderLoads) { if (!Folders.Length()) return false; // Just wait for them... auto f = Folders[0]; Folders.DeleteAt(0); if (f) { auto Path = f->GetPath(); ScribeMailType Type = Filter->BayesTypeFromPath(Path); if (Debug) Debug->Print("%s:%i - add folder '%s', Type=%i\n", _FL, Path.Get(), Type); auto Parent = f->GetParent(); if (Type != BayesMailUnknown && Parent) { FolderLoads++; f->WhenLoaded(_FL, [this, f, Type, Path]() { LgiTrace("%s:%i - Scanning '%s' got %i items, FolderLoads=%i\n", _FL, Path.Get(), (int)f->Items.Length(), FolderLoads); for (auto i: f->Items) { auto m = i->IsMail(); if (!m) continue; // Add item to the work queue... Items.New().Set(m, Type); } FolderLoads--; (*Prog)++; }); // FIXME: does this need to do something on callback? f->LoadThings(); } else { // LgiTrace("%s:%i - Unknown folder '%s'\n", _FL, Path.Get()); (*Prog)++; } } if (Folders.Length() == 0 && FolderLoads == 0) { Prog->SetDescription("Processing mail..."); Prog->SetRange(Items.Length()); Prog->Value(0); } return false; } if (Items.Length() || MailLoads) { if (Items.Length() && MailLoads < 100) { auto Start = LCurrentTime(); while ( (LCurrentTime() - Start) < TIMEOUT_BAYES_IDLE && Items.Length()) { // Process mail items... auto &i = Items[0]; // Operate on read mail only... auto flags = i.m->GetFlags(); if (TestFlag(flags, MAIL_READ)) { MailLoads++; // auto loaded = i.m->GetLoaded(); i.m->WhenLoaded(_FL, [this, mail = i.m, type = i.type] { ProcessMail(mail, type); MailLoads--; (*Prog)++; }); i.m->SetLoaded(); } Items.DeleteAt(0); } } return false; } // We're done... return true; } void BuildSpamDB::ProcessMail(Mail *m, ScribeMailType Type) { auto LoadState = m->GetLoaded(); if (LoadState != Store3Loaded) { LAssert(!"Should only be called on loaded email..."); return; } const char *email; if (Type == BayesMailHam && (email = m->GetFromStr(FIELD_EMAIL))) { b->InsertWhiteList(email); } LString Words; auto Status = Filter->MakeMailWordList(m, Words); if (Status != Store3Success) { LAssert(!"MakeMailWordList failed."); return; } ProcessWords(Words, [this, Type](auto w) { if (Type == BayesMailSpam) b->InsertSpamWords(w); else if (Type == BayesMailHam) b->InsertHamWords(w); // else do nothing }); auto flags = m->GetFlags(); // Remove the bayes DB flags... flags &= ~(MAIL_HAM_DB|MAIL_SPAM_DB); if (Type == BayesMailSpam) { b->SpamEmailCount++; flags |= MAIL_SPAM_DB; } else if (Type == BayesMailHam) { b->HamEmailCount++; flags |= MAIL_HAM_DB; } if (flags & MAIL_BAYES_HAM) // originally classified as ham.. { if (flags & MAIL_SPAM_DB) // but now is spam... FalseNegatives++; else if (Type == BayesMailHam) HamCount++; } else if (flags & MAIL_BAYES_SPAM) // originally classified as spam.. { if (flags & MAIL_SPAM_DB) SpamCount++; else if (Type == BayesMailHam) // but now is ham... FalsePositives++; } // Update the object.. m->SetFlags(flags); // Delete our reference... m->DecRef(); } #ifdef _DEBUG bool HashSerialize(LHashTbl,uint32_t> &h, char *file, bool write) { bool Status = false; struct Field { uint32_t Value; uint16_t Len; char Str[1]; }; LFile f; if (f.Open(file, write?O_WRITE:O_READ)) { Field *fld = (Field*)malloc(64<<10); if (fld) { int header = 6; if (write) { f.SetSize(0); // char *key; // for (int i=h.First(&key); i; i=h.Next(&key)) for (auto i : h) { fld->Value = (uint32_t)i.value; fld->Len = (uint16_t)strlen(i.key); memcpy(fld->Str, i.key, fld->Len + 1); Status = f.Write(fld, header + fld->Len) == (header + fld->Len); } } else { h.Empty(); while (!f.Eof()) { if (f.Read(fld, header) == header) { if (f.Read(fld->Str, fld->Len) == fld->Len) { fld->Str[fld->Len] = 0; Status = h.Add(fld->Str, fld->Value); } else break; } else break; } } free(fld); } } return Status; } #endif BayesianFilter::BayesianFilter(ScribeWnd *app) { d = new BayesianFilterPriv(app); App = app; } BayesianFilter::~BayesianFilter() { DeleteObj(d); } void BayesianFilter::AddFolderToSpamDb(ScribeFolder *f) { if (d->IsCancelled()) return; d->Build->AddFolder(f); for (auto c = f->GetChildFolder(); c; c = c->GetNextFolder()) AddFolderToSpamDb(c); } /* static size_t FolderCount(ScribeFolder *f) { ssize_t len = f->Length(); auto items = f->GetItems(); auto n = MAX(len, items); for (auto c = f->GetChildFolder(); c; c = c->GetNextFolder()) n += FolderCount(c); return n; } */ void BayesianFilter::BuildStats() { auto prob = App->GetFolder("/Mail3/Spam/Probably"); auto inbox = App->GetFolder("/IMAP/INBOX"); if (!prob || !inbox) return; prob->LoadThings(); inbox->LoadThings(); } bool BayesianFilter::BuildSpamDb() { if (!d->GetThread()) return false; // scan folders LVariant MoveTo; if (!App->GetOptions()->GetValue(OPT_BayesMoveTo, MoveTo)) MoveTo = "/Spam/Probably"; if (!d->Build.Reset(new BuildSpamDB(App))) return false; // Recurse over the folders for (auto &s: App->GetStorageFolders()) { if (s.Root) AddFolderToSpamDb(s.Root); } for (auto a : *App->GetAccounts()) { if (a->Receive.GetRootFolder()) AddFolderToSpamDb(a->Receive.GetRootFolder()); } d->Build->Prog->SetRange(d->Build->Folders.Length()); return true; } #define IsCJK(c) \ ( \ ((c)>=0x4E00 && (c)<=0x9FFF) || \ ((c)>=0x3400 && (c)<=0x4DFF) || \ ((c)>=0x20000 && (c)<=0x2A6DF)|| \ ((c)>=0xF900 && (c)<=0xFAFF) || \ ((c)>=0x2F800 && (c)<=0x2FA1F) \ ) bool IsUriChar(int32 ch) { if (!ch) return false; if (IsDigit(ch) || IsAlpha(ch)) return true; if (strchr("-._~:/?#[]@!$&\'()*+,;%=", ch)) return true; return false; } typedef LHashTbl,bool> TokenMap; void TokeniseText(const char *Source, bool *Lut, LString::Array &Blocks, TokenMap *Ignore = NULL) { if (!Source || !Lut) return; char buf[16 << 10]; ssize_t used = 0; auto oldWarn = LUtf8Ptr::Warn; LUtf8Ptr::Warn = false; auto emitBuf = [&Blocks, &used, &buf]() { if (used > 0) Blocks.New().Set(buf, used); used = 0; }; for (auto *s = Source; *s;) { int32 ch; LUtf8Ptr start(s); // Skip non-word while ( (ch = start) && ch < 256 && !Lut[ch] ) { start++; } // Seek end of word LUtf8Ptr end(start); while ( (ch = end) && ( ch >= 256 || Lut[ch] ) ) { if (end > start && IsCJK(ch)) break; end++; } auto len = end - start; // Is the word something that looks like a URI? bool isUrl = (len == 4 && !Strnicmp((const char*)start.GetPtr(), "http", 4)) || (len == 5 && !Strnicmp((const char*)start.GetPtr(), "https", 5)); if (isUrl) { // Seek to the end of the URI while ((ch = end) && IsUriChar(ch) && (end - start) < sizeof(buf) - 10) end++; isUrl = true; len = end - start; } if (len > 0) { ch = start; if (ch != '*' || len != 10 || Strnicmp((const char*)start.GetPtr(), "***SPAM***", len)) { if (used + len >= sizeof(buf) - 2) emitBuf(); auto word = buf + used; if (isUrl) { LUri uri(LString((char*)start.GetPtr(), len)); if (uri.sHost) { memcpy(word, uri.sHost.Get(), uri.sHost.Length()); used += uri.sHost.Length(); buf[used++] = ' '; } } else { memcpy(word, start.GetPtr(), len); used += len; if (Ignore) { buf[used] = 0; if (Ignore->Find(buf+used-len)) used -= len; // undo adding word else buf[used++] = ' '; // convert to space delimit } else { buf[used++] = ' '; } } } } else { if (ch) LgiTrace("%s:%i - Invalid utf-8, aborting parse...\n", _FL); break; } s = (const char*)end.GetPtr(); } emitBuf(); LUtf8Ptr::Warn = oldWarn; } Store3Status BayesianFilter::MakeMailWordList(Mail *m, LString &out) { LString::Array Blocks; if (!m || !m->GetObject()) return Store3Error; static bool Processing = false; if (!Processing) { Processing = true; // create word lut for deciding whether a char is part of a word or not bool Lut[256]; ZeroObj(Lut); memset(Lut + 'a', true, 'z'-'a'+1); memset(Lut + 'A', true, 'Z'-'A'+1); Lut[(int)'-'] = true; // Lut[(int)'!'] = true; Lut[(int)'$'] = true; bool Email[256]; memcpy(Email, Lut, sizeof(Email)); Email[(int)'@'] = true; Email[(int)'.'] = true; memset(Lut + 0x80, true, 128); // create ignored words list LString::Array Temp; TokenMap Ignore; for (auto a: *App->GetAccounts()) { auto s = a->Identity.Name(); if (ValidStr(s.Str())) TokeniseText(s.Str(), Lut, Temp); s = a->Identity.Email(); if (ValidStr(s.Str())) TokeniseText(s.Str(), Email, Temp); } ProcessWords(LString("").Join(Temp), [&Ignore](auto w) { Ignore.Add(w, true); }); // process various parts of the email TokeniseText(m->GetSubject(), Lut, Blocks, &Ignore); TokeniseText(m->GetFromStr(FIELD_EMAIL), Email, Blocks, &Ignore); TokeniseText(m->GetFromStr(FIELD_NAME), Lut, Blocks, &Ignore); LVariant Body; Store3State Loaded = (Store3State)m->GetObject()->GetInt(FIELD_LOADED); if (Loaded < Store3Loaded) { m->GetBody(); Loaded = (Store3State)m->GetObject()->GetInt(FIELD_LOADED); if (Loaded < Store3Loaded) { Processing = false; return Store3Delayed; } // else continue... } auto Req = m->GetValue("BodyAsText", Body); if (Req) { // auto id = m->GetMessageId(); TokeniseText(Body.Str(), Lut, Blocks, &Ignore); } else { LString path = "(nullFolder)"; auto fld = m->GetFolder(); if (fld) path = fld->GetPath(); LgiTrace("%s:%i - couldn't get body for %s/%s\n", _FL, path.Get(), m->GetMessageId()); /* Technically not an error... body can be blank. Processing = false; return Store3Error; */ } out = LString("").Join(Blocks); Processing = false; return Store3Success; } else { LAssert(!"Recursion."); return Store3Error; } } Store3Status BayesianFilter::IsSpam(double &Result, Mail *m, bool Analyse) { if (!m) { Result = 0.0; return Store3Error; } Store3Status Status = Store3NotImpl; LAutoPtr t(new BayesianThread::Test); const char *FromAddr; if ((FromAddr = m->GetFromStr(FIELD_EMAIL))) { #if WHITELIST_MY_EMAIL // Check if from yourself... if (App->IsMyEmail(FromAddr)) goto OnWhiteListed; #endif // Check the user white list LVariant Wl; if (App->GetOptions()->GetValue(OPT_BayesUserWhiteList, Wl)) { auto w = Wl.LStr().SplitDelimit("\r\n\t "); bool IsWhite = false; for (unsigned i=0; i Contacts; App->GetContacts(Contacts); for (auto c: Contacts) { if (c->HasEmail(FromAddr)) goto OnWhiteListed; } #endif t->FromAddr = FromAddr; } // Tokenise the mail... Status = MakeMailWordList(m, t->Words); if (Status == Store3Error) return Status; if (Status == Store3Delayed) { // Not loaded yet, retry it later... m->WhenLoaded(_FL, [this, Analyse, m]() { double r; IsSpam(r, m, Analyse); }); return Store3Delayed; } // Pass it over to the thread t->Analyse = Analyse; t->MsgRef = m->GetMailRef(); d->GetThread()->Add(t); return Store3Delayed; OnWhiteListed: if (Analyse) OnBayesAnalyse(LLoadString(IDS_IS_WHITELIST), FromAddr); Result = 0.0; return Store3Success; } ScribeMailType BayesianFilter::BayesTypeFromPath(LString Path) { if (!Path) return BayesMailUnknown; auto spam = App->GetFolder(FOLDER_SPAM); auto folder = App->GetFolder(Path); if (spam && folder) { if (folder == spam) return BayesMailSpam; // If folder is a child of the spam folder, it's NOT ham but "unknown". // Typically a staging ground for "possible" spam. So it should not contribute to // bayes word counts. for (auto p = folder->GetParent(); p; p = p->GetParent()) if (p == spam) return BayesMailUnknown; } else { auto t = Path.SplitDelimit("/"); ssize_t spamIdx = -1; for (size_t i=0; i spamIdx + 1 ? BayesMailUnknown : BayesMailSpam; } return BayesMailHam; } ScribeMailType BayesianFilter::BayesTypeFromPath(Mail *m) { ScribeMailType Status = BayesMailUnknown; if (m) { ScribeFolder *f = m->GetFolder(); if (f) Status = BayesTypeFromPath(f->GetPath()); } return Status; } void BayesianFilter::WhiteListIncrement(const char *Word) { if (!Word) return; LAutoPtr c(new BayesianThread::Change); c->IncrementWhite = true; c->Str = Word; d->GetThread()->Add(c); } bool BayesianFilter::RemoveFromWhitelist(const char *Email) { if (!Email) return false; LAutoPtr c(new BayesianThread::Change); c->Str = Email; c->RemoveWhite = true; d->GetThread()->Add(c); return true; } Store3Status BayesianFilter::OnBayesianMailEvent(Mail *m, ScribeMailType OldType, ScribeMailType NewType) { if (!m) return Store3Error; auto flags = m->GetFlags(); if (NewType == BayesMailHam) { if (TestFlag(flags, MAIL_HAM_DB)) return Store3Success; if (!TestFlag(flags, MAIL_BAYES_HAM|MAIL_BAYES_SPAM)) flags |= MAIL_BAYES_HAM; } if (NewType == BayesMailSpam) { if (TestFlag(flags, MAIL_SPAM_DB)) return Store3Success; if (!TestFlag(flags, MAIL_BAYES_HAM|MAIL_BAYES_SPAM)) flags |= MAIL_BAYES_SPAM; } m->SetFlags(flags); if (d->Work.Length() == 0) d->WorkStartTs = LCurrentTime(); auto &w = d->Work.New(); w.m = m; w.m->IncRef(); w.Loading = false; w.Done = false; w.OldType = OldType; w.NewType = NewType; return Store3Delayed; } void BayesianFilter::OnEvent(LMessage *Msg) { switch (Msg->Msg()) { case M_SCRIBE_IDLE: { // LProfile prof("M_SCRIBE_IDLE", 15); if (d->Build) { if (d->Build->Process()) { // Give the hash tables to the worker thread to convert to disk: d->GetThread()->Add(d->Build->b); // And finish doing the build processing: d->Build.Reset(); } break; } auto EmptyWorkQueue = [this]() { for (auto j: d->Work) { if (!j.Done && j.m) { j.m->DecRef(); j.m = NULL; } } d->Work.Length(0); }; // Look for something we can do... auto StartTs = LCurrentTime(); size_t i, processedCount = 0; for (i=0; iWork.Length(); i++) { auto &job = d->Work[i]; if (job.Done) continue; if (!job.m->GetObject()) { // Why? LgiTrace("%s:%i - Error: object not found.\n", _FL); job.Done = true; // We can't continue anyway continue; } if (job.Loading) { // prof.Add("loading"); auto loaded = job.m->GetLoaded(); if (loaded < Store3Loaded) continue; // Still hasn't loaded.. } LString words; // prof.Add("MakeMailWordList"); auto Status = MakeMailWordList(job.m, words); if (Status == Store3Error) { // Remove the work from the list... it's probably going to keep failing d->Work.DeleteAt(i--); continue; } else if (Status == Store3Delayed) { if (job.Loading) LAssert(!"Already waiting for load."); else job.Loading = true; continue; } // prof.Add("change"); LAutoPtr c(new BayesianThread::Change); c->Str = job.m->GetFromStr(FIELD_EMAIL); c->Words = words; c->OldType = job.OldType; c->NewType = job.NewType; d->GetThread()->Add(c); // Update the spam/ham flags... auto flags = job.m->GetFlags(); flags &= ~(MAIL_BAYES_SPAM|MAIL_BAYES_HAM); if (job.NewType == BayesMailHam) flags |= MAIL_BAYES_HAM; else if (job.NewType == BayesMailSpam) flags |= MAIL_BAYES_SPAM; job.m->SetFlags(flags); job.m->DecRef(); job.m = NULL; job.Done = true; processedCount++; if (LCurrentTime()-StartTs >= TIMEOUT_BAYES_IDLE) break; } // prof.Add("post"); if (d->Work.Length() > 0) { if (!processedCount) { EmptyWorkQueue(); } else { auto Now = LCurrentTime(); LAssert(d->WorkStartTs); if (Now - d->WorkStartTs > 300 && !d->Prog) { d->Prog.Reset(new LProgressDlg(App, 500)); d->Prog->SetDescription("Bayesian filtering..."); } if (d->Prog) { d->Prog->SetRange(d->Work.Length()); d->Prog->Value(i); if (d->Prog->IsCancelled()) { // Dump the work queue EmptyWorkQueue(); } } } } else { d->Prog.Reset(); d->WorkStartTs = 0; } break; } case M_SCRIBE_BAYES_RESULT: { LArray< LAutoPtr > Results; if (!d->GetThread()->GetResults(Results)) break; for (auto t: Results) { if (!t) { LAssert(!"Null ptr in Results"); continue; } if (t->Analyse) { LArray a; int Size = (int) t->Log.GetSize(); a.Length(Size+1); t->Log.Read(&a[0], Size); a[Size] = 0; - OnBayesAnalyse(&a[0], t->WhiteListed ? t->FromAddr : NULL); + OnBayesAnalyse(&a[0], t->WhiteListed ? t->FromAddr : LString()); } else if (t->MsgRef) { OnBayesResult(t->MsgRef, t->Score); } else LAssert(!"There should always be a msg ref"); } break; } } } diff --git a/src/Exp_Scribe.cpp b/src/Exp_Scribe.cpp --- a/src/Exp_Scribe.cpp +++ b/src/Exp_Scribe.cpp @@ -1,906 +1,906 @@ #include "Scribe.h" #include "lgi/common/List.h" #include "lgi/common/ProgressDlg.h" #include "lgi/common/Store3.h" #include "lgi/common/LgiRes.h" #include "lgi/common/FileSelect.h" #include "ScribeFolderSelect.h" #include "resdefs.h" #include "FolderTask.h" #define OnError(...) \ { \ Errors.Add(in->Type(), Errors.Find(in->Type()) + 1); \ LgiTrace(__VA_ARGS__); \ break; \ } #define OnSkip() \ { \ Skipped.Add(in->Type(), Skipped.Find(in->Type()) + 1); \ break; \ } #define OnCreate() \ { \ Created.Add(in->Type(), Created.Find(in->Type()) + 1); \ } struct ExportParams { bool AllFolders = false; bool ExceptTrashSpam = false; LString DestFolders; // Mail3 path for dest folders; LString DestPath; // Sub folder path for export LString::Array SrcPaths; }; struct Mail3Folders { ScribeWnd *App = NULL; LAutoPtr Store; LAutoPtr Root; Mail3Folders(ScribeWnd *app) : App(app) { } Mail3Folders(Mail3Folders &src) { App = src.App; if (src) { Store = src.Store; Root = src.Root; } else LAssert(!"Src not loaded."); } operator bool() { return Store != NULL && Root != NULL; } void LoadFolders(const char *FilePath) { if (!Store) Store.Reset(App->CreateDataStore(FilePath, true)); if (Store && !Root) { if (Root.Reset(new ScribeFolder)) { Root->App = App; Root->SetObject(Store->GetRoot(), false, _FL); } } } void Unload() { Root.Reset(); Store.Reset(); } bool CheckDirty(ScribeFolder *f) { if (f->GetDirty()) return true; for (auto c = f->GetChildFolder(); c; c = c->GetNextFolder()) if (CheckDirty(c)) return true; return false; } bool IsDirty() { if (!Root) return false; return CheckDirty(Root); } }; struct ScribeExportTask : public FolderTask { int FolderLoadErrors = 0; LHashTbl,int> Created, Errors, Skipped; LMailStore *SrcStore = NULL; // Source data store Mail3Folders Dst; ScribeFolder *Spam = NULL; ScribeFolder *Trash = NULL; ExportParams Params; // Working state, used to iterate over the exporting process while // being able to yield to the OS at regular intervals. enum ExportState { ExpNone, ExpGetNext, ExpLoadFolders, ExpItems, ExpFinished, ExpCleanup } State = ExpNone; LString::Array InputPaths; ScribeFolder *SrcFolder = NULL; LArray SrcItems; ScribeFolder *DstFolder = NULL; LDataFolderI *DstObj = NULL; LDataStoreI *DstStore = NULL; LHashTbl,LDataI*> DstObjMap; ScribeExportTask(struct ScribeExportDlg *dlg); LString ContactKey(Contact *c); bool TimeSlice(); bool CopyAttachments(LDataI *outMail, LDataPropI *outSeg, LDataPropI *inSeg, LString &err); LString MakePath(LString path) { LString sep = "/"; auto p = Params.DestPath.SplitDelimit(sep); p += path.Strip(sep).SplitDelimit(sep).Slice(1); return sep + sep.Join(p); } void CollectPaths(ScribeFolder *f, LString::Array &paths) { paths.Add(f->GetPath()); for (auto c = f->GetChildFolder(); c; c = c->GetNextFolder()) CollectPaths(c, paths); } ScribeFolder *GetFolder(LString Path, Store3ItemTypes CreateItemType = MAGIC_NONE) { if (!Dst.Root) { LAssert(!"No root loaded."); return NULL; } Dst.Root->LoadFolders(); bool Create = false; auto parts = Path.SplitDelimit("/"); ScribeFolder *f = Dst.Root; for (auto p: parts) { auto c = f->GetSubFolder(p); if (!c) { if (CreateItemType != MAGIC_NONE) { c = f->CreateSubFolder(p, CreateItemType); if (!c) { Errors.Add(MAGIC_FOLDER, Errors.Find(MAGIC_FOLDER)+1); return NULL; } Create = true; } else return NULL; } f = c; } if (Create) Created.Add(MAGIC_FOLDER, Created.Find(MAGIC_FOLDER)+1); else Skipped.Add(MAGIC_FOLDER, Skipped.Find(MAGIC_FOLDER)+1); return f; } void OnComplete() { Store3ItemTypes types[] = { MAGIC_FOLDER, MAGIC_MAIL, MAGIC_CONTACT, MAGIC_CALENDAR, MAGIC_GROUP, MAGIC_FILTER }; LStringPipe html; html.Print("\n" "
Mail export complete.
\n" "
\n" "\n" "\n"); for (int i=0; i\n", name.Get(), created, errStyle, errors, skipped); } html.Print("
Type Created Errors Skipped
%s %i %i %i
\n" "
\n" "Created: new item created in destination store.
\n" "Error: there was an error replicating item.
\n" "Skipped: the item already existed in the destination store.
\n" ); LHtmlMsg(NULL, App, html.NewLStr(), "Export", MB_OK); } LString GetOrCreateMessageId(LDataI &obj) { auto msgId = obj.GetStr(FIELD_MESSAGE_ID); if (msgId) return msgId; // Check the headers: auto hdrs = obj.GetStr(FIELD_INTERNET_HEADER); if (hdrs) { LAutoString Header(InetGetHeaderField(hdrs, "Message-ID")); if (Header) { auto ids = ParseIdList(Header); auto id = ids[0]; obj.SetStr(FIELD_MESSAGE_ID, id); obj.Save(); return id; } } // Msg has no ID and no header... create one. auto from = obj.GetObj(FIELD_FROM); if (!from) { LgiTrace("%s:%i - No from for email: %p\n", _FL, &obj); return LString(); } auto fromEmail = from->GetStr(FIELD_EMAIL); LVariant Email; const char *At = fromEmail ? strchr(fromEmail, '@') : NULL; if (!At) { if (App->GetOptions()->GetValue(OPT_Email, Email) && Email.Str()) At = strchr(Email.Str(), '@'); else At = "@domain.com"; } if (!At) { LgiTrace("%s:%i - No at in email: %p\n", _FL, &obj); return LString(); } char m[96], a[32], b[32]; Base36(a, LCurrentTime()); Base36(b, LRand(RAND_MAX)); sprintf_s(m, sizeof(m), "<%s.%i%s%s>", a, LRand(RAND_MAX), b, At); obj.SetStr(FIELD_MESSAGE_ID, m); obj.Save(); return m; } LString ObjToId(LDataI &obj) { switch (obj.Type()) { case MAGIC_MAIL: { return obj.GetStr(FIELD_MESSAGE_ID); } case MAGIC_CONTACT: { auto fn = obj.GetStr(FIELD_FIRST_NAME); auto ln = obj.GetStr(FIELD_LAST_NAME); auto em = obj.GetStr(FIELD_EMAIL); LString s; s.Printf("%s,%s,%s", fn, ln, em); return s; } case MAGIC_CALENDAR: { auto sub = obj.GetStr(FIELD_CAL_SUBJECT); auto start = obj.GetDate(FIELD_CAL_START_UTC); LString s; s.Printf("%s," LPrintfInt64, sub, start?start->Ts():0); return s; break; } case MAGIC_GROUP: { return obj.GetStr(FIELD_GROUP_NAME); } case MAGIC_FILTER: { return obj.GetStr(FIELD_FILTER_NAME); } default: { LAssert(!"Impl me."); break; } } return LString(); } bool CheckModified(LDataI *in, LDataI *out) { if (!out) // No existing object return true; auto inMod = in->GetDate(FIELD_DATE_MODIFIED); auto outMod = in->GetDate(FIELD_DATE_MODIFIED); if (!inMod || !outMod || !inMod->IsValid() || !outMod->IsValid()) return true; // Can't tell... no dates stored. bool mod = *inMod > *outMod; return mod; } void MakeDstObjMap() { DstObjMap.Empty(); if (!DstFolder || !DstObj) return; auto &c = DstObj->Children(); for (auto t = c.First(); t; t = c.Next()) { auto Id = ObjToId(*t); if (Id) DstObjMap.Add(Id, t); } } }; struct ScribeExportDlg : public LDialog, public LDataEventsI { ScribeWnd *App = NULL; LMailStore *SrcStore = NULL; LList *Lst = NULL; ExportParams Params; Mail3Folders Dst; ScribeExportDlg(ScribeWnd *app, LMailStore *srcStore) : SrcStore(srcStore), Dst(app) { SetParent(App = app); if (!SrcStore) SrcStore = App->GetDefaultMailStore(); if (LoadFromResource(IDD_SCRIBE_EXPORT)) { MoveToCenter(); // EnableCtrls(false); GetViewById(IDC_SRC_FOLDERS, Lst); LVariant s; if (Lst && App->GetOptions()->GetValue(OPT_ScribeExpSrcPaths, s) && s.Str()) { Params.SrcPaths = LString(s.Str()).SplitDelimit(":"); for (auto p: Params.SrcPaths) Lst->Insert(new LListItem(p)); Lst->ResizeColumnsToContent(); } if (App->GetOptions()->GetValue(OPT_ScribeExpDstPath, s) && ValidStr(s.Str())) SetCtrlName(IDC_FOLDER, s.Str()); else SetCtrlName(IDC_FOLDER, "/"); LVariant n; if (App->GetOptions()->GetValue(OPT_ScribeExpAll, n)) SetCtrlValue(IDC_ALL, n.CastInt32()); if (App->GetOptions()->GetValue(OPT_ScribeExpExclude, n)) SetCtrlValue(IDC_NO_SPAM_TRASH, n.CastInt32()); else SetCtrlValue(IDC_NO_SPAM_TRASH, true); if (App->GetOptions()->GetValue(OPT_ScribeExpFolders, s) && s.Str()) SetCtrlName(IDC_DEST, s.Str()); OnAll(); } } void OnNew(LDataFolderI *parent, LArray &new_items, int pos, bool is_new) { } bool OnDelete(LDataFolderI *parent, LArray &items) { return true; } bool OnMove(LDataFolderI *new_parent, LDataFolderI *old_parent, LArray &items) { return true; } bool OnChange(LArray &items, int FieldHint) { return true; } void OnAll() { SetCtrlEnabled(IDC_SRC_FOLDERS, !GetCtrlValue(IDC_ALL)); SetCtrlEnabled(IDC_ADD_SRC_FOLDER, !GetCtrlValue(IDC_ALL)); SetCtrlEnabled(IDC_DEL_SRC_FOLDER, !GetCtrlValue(IDC_ALL)); } int OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDC_ALL: { OnAll(); break; } case IDC_SET_DEST: { auto s = new LFileSelect(this); s->Type("Scribe Folders", "*.mail3"); s->Type("All Files", LGI_ALL_FILES); s->Open([this](auto dlg, auto status) { if (status) SetCtrlName(IDC_DEST, dlg->Name()); delete dlg; }); break; } case IDC_ADD_SRC_FOLDER: { if (!Lst) break; auto s = new FolderDlg(this, App); s->DoModal([this, s](auto dlg, auto status) { if (status && ValidStr(s->Get())) { bool Has = false; for (auto n: *Lst) { if (Stricmp(n->GetText(), s->Get()) == 0) { Has = true; break; } } if (!Has) { Lst->Insert(new LListItem(s->Get())); Lst->ResizeColumnsToContent(); } } delete dlg; }); break; } case IDC_DEL_SRC_FOLDER: { if (Lst) { List i; if (Lst->GetSelection(i)) { i.DeleteObjects(); } } break; } case IDC_SET_FOLDER: { if (!Dst) Dst.LoadFolders(GetCtrlName(IDC_DEST)); if (Dst.Root) { auto s = new FolderDlg(this, App, MAGIC_NONE, Dst.Root); s->DoModal([this, s](auto dlg, auto ctrlId) { if (ctrlId) SetCtrlName(IDC_FOLDER, s->Get()); delete dlg; }); } else LgiMsg(this, "Couldn't load mail3 store.", AppName); break; } case IDOK: { Params.AllFolders = GetCtrlValue(IDC_ALL) != 0; Params.ExceptTrashSpam = GetCtrlValue(IDC_NO_SPAM_TRASH) != 0; Params.DestPath = GetCtrlName(IDC_FOLDER); Params.DestFolders = GetCtrlName(IDC_DEST); if (Lst) { Params.SrcPaths.Empty(); Params.SrcPaths.SetFixedLength(false); for (auto i: *Lst) Params.SrcPaths.Add(i->GetText()); } if (!Dst) Dst.LoadFolders(Params.DestFolders); // fall through } case IDCANCEL: { LVariant v; if (Lst) { LStringPipe p; int n=0; for (auto i : *Lst) { p.Print("%s%s", n ? (char*)":" : (char*) "", i->GetText(0)); } char *s = p.NewStr(); if (s) { App->GetOptions()->SetValue(OPT_ScribeExpSrcPaths, v = s); DeleteArray(s); } } App->GetOptions()->SetValue(OPT_ScribeExpDstPath, v = GetCtrlName(IDC_FOLDER)); App->GetOptions()->SetValue(OPT_ScribeExpAll, v = (int)GetCtrlValue(IDC_ALL)); App->GetOptions()->SetValue(OPT_ScribeExpExclude, v = (int)GetCtrlValue(IDC_NO_SPAM_TRASH)); App->GetOptions()->SetValue(OPT_ScribeExpFolders, v = GetCtrlName(IDC_DEST)); EndModal(c->GetId() == IDOK); } } return 0; } }; ScribeExportTask::ScribeExportTask(ScribeExportDlg *dlg) : - FolderTask(dlg->Dst.Root, LAutoPtr(NULL), NULL, NULL), + FolderTask(dlg->Dst.Root, LAutoPtr(NULL), LString(), NULL), Dst(dlg->Dst), SrcStore(dlg->SrcStore) { SetDescription("Initializing..."); SetType("Folders"); LAssert(SrcStore); Params = dlg->Params; if (Params.ExceptTrashSpam) { Spam = App->GetFolder("/Spam"); Trash = App->GetFolder(FOLDER_TRASH); } // Work out the folders we need to operate on: if (Params.AllFolders) CollectPaths(SrcStore->Root, InputPaths); else InputPaths = Params.SrcPaths; SetRange(InputPaths.Length()); // Kick off the processing... State = ExpGetNext; SetPulse(PULSE_MS); SetAlwaysOnTop(true); } LString ScribeExportTask::ContactKey(Contact *c) { LString p; const char *f = 0, *l = 0; auto e = c->GetAddrAt(0); c->Get(OPT_First, f); c->Get(OPT_Last, l); p.Printf("%s,%s,%s", e.Get(), f, l); return p; } #define ExportFolderStatus(b) \ { if (onStatus) onStatus(b); \ return; } bool ScribeExportTask::TimeSlice() { if (IsCancelled()) return false; switch (State) { case ExpGetNext: { if (InputPaths.Length() == 0) { State = ExpFinished; break; } // Load up the next SrcFolder... auto src = InputPaths[0]; InputPaths.DeleteAt(0, true); auto dst = MakePath(src); SrcFolder = App->GetFolder(src, SrcStore); if (!SrcFolder) { FolderLoadErrors++; return true; } DstStore = NULL; DstFolder = GetFolder(dst, SrcFolder->GetItemType()); if (!DstFolder) { return true; } State = ExpLoadFolders; SrcFolder->LoadThings(this, [this](auto status) { if (status == Store3Success) { // Load a list of things to process... SrcItems.Empty(); auto SrcObj = dynamic_cast(SrcFolder->GetObject()); if (SrcObj) { auto &c = SrcObj->Children(); for (auto t = c.First(); t; t = c.Next()) SrcItems.Add(t); } DstFolder->LoadThings(this, [this](auto status) { if (status == Store3Success) { State = ExpItems; SetDescription(SrcFolder->GetPath()); if (DstFolder->GetObject()) DstObj = dynamic_cast(DstFolder->GetObject()); else LAssert(!"No object?"); if (DstObj) DstStore = DstObj->GetStore(); else LAssert(!"No object?"); MakeDstObjMap(); } else State = ExpGetNext; }); } else { State = ExpGetNext; } }); break; } case ExpLoadFolders: { // No-op, but we should probably time out... break; } case ExpItems: { if (!SrcFolder || !DstFolder || !DstStore) { State = ExpGetNext; break; } auto Trans = DstStore->StartTransaction(); auto StartTs = LCurrentTime(); int Processed = 0; while ( SrcItems.Length() > 0 && LCurrentTime() - StartTs < WORK_SLICE_MS) { auto in = SrcItems[0]; SrcItems.DeleteAt(0); switch (in->Type()) { case MAGIC_MAIL: { auto Id = GetOrCreateMessageId(*in); if (!Id) OnError("%s:%i - Email %p has no MsgId\n", _FL, in) if (DstObjMap.Find(Id)) OnSkip() // Create new mail... auto outMail = DstStore->Create(MAGIC_MAIL); outMail->CopyProps(*in); // Now create all the attachments auto inSeg = dynamic_cast(in->GetObj(FIELD_MIME_SEG)); LDataI *outSeg = NULL; if (inSeg) { outSeg = DstStore->Create(MAGIC_ATTACHMENT); if (!outSeg) OnError("%s:%i - Failed to create attachment\n", _FL) else { outSeg->CopyProps(*inSeg); auto outMime = outSeg->GetStr(FIELD_MIME_TYPE); if (!outMime) { auto hdrs = outSeg->GetStr(FIELD_INTERNET_HEADER); if (!hdrs) { // This is going to cause an assert later outSeg->SetStr(FIELD_MIME_TYPE, sAppOctetStream); // LgiTrace("%s:%i - Setting default mime on %p\n", _FL, outSeg); } } if (outMail->SetObj(FIELD_MIME_SEG, outSeg) < Store3Delayed) OnError("%s:%i - Failed to attach seg to mail.\n", _FL) else { LString err; if (!CopyAttachments(outMail, outSeg, inSeg, err)) OnError("%s:%i - CopyAttachments failed\n", _FL) else OnCreate() } } } else OnCreate() outMail->Save(DstFolder->GetObject()); break; } default: { // Is the object already in the dst map? auto Id = ObjToId(*in); auto existing = DstObjMap.Find(Id); if (!CheckModified(in, existing)) OnSkip(); auto outObj = DstStore->Create(in->Type()); if (!outObj) { OnError("%s:%i - %s failed to create %s\n", _FL, DstStore->GetStr(FIELD_STORE_TYPE), Store3ItemTypeName((Store3ItemTypes)in->Type())) } if (!outObj->CopyProps(*in)) OnError("%s:%i - CopyProps failed.\n", _FL) if (outObj->Save(DstFolder->GetObject()) < Store3Delayed) OnError("%s:%i - Save failed\n", _FL) OnCreate(); break; } } Processed++; } if (SrcItems.Length() == 0) { SrcFolder = NULL; DstFolder = NULL; DstStore = NULL; State = ExpGetNext; (*this)++; // move progress... } // LgiTrace("Processed: %i\n", Processed); break; } case ExpFinished: { OnComplete(); State = ExpCleanup; return true; } case ExpCleanup: { if (Dst.IsDirty()) return true; // We're done... return false; } default: { LAssert(!"Not impl."); return false; } } return true; } // This should copy all the child objects of 'inSeg' to new child objects of 'outSeg' bool ScribeExportTask::CopyAttachments(LDataI *outMail, LDataPropI *outSeg, LDataPropI *inSeg, LString &err) { #define ERR(str) \ { err = str; return false; } if (!outMail || !outSeg || !inSeg) ERR("param error"); auto children = inSeg->GetList(FIELD_MIME_SEG); if (!children) return true; // Nothing to copy... for (auto i = children->First(); i; i = children->Next()) { auto inMime = i->GetStr(FIELD_MIME_TYPE); if (!inMime) continue; auto o = outMail->GetStore()->Create(MAGIC_ATTACHMENT); if (!o) ERR("couldn't create attachment"); if (!o->CopyProps(*i)) ERR("copy attachment properties failed"); auto outData = dynamic_cast(outSeg); if (!outData) ERR("outSeg isn't a LDataI object"); if (!o->Save(outData)) ERR("failed to save attachment to output mail"); if (!CopyAttachments(outMail, o, i, err)) return false; // but leave the error message untouched. } return true; } void ExportScribe(ScribeWnd *App, LMailStore *Store) { auto Dlg = new ScribeExportDlg(App, Store); Dlg->DoModal([Dlg, App](auto dlg, auto ctrlId) { if (ctrlId && Dlg->Dst) { new ScribeExportTask(Dlg); } delete dlg; }); } diff --git a/src/ScribeApp.cpp b/src/ScribeApp.cpp --- a/src/ScribeApp.cpp +++ b/src/ScribeApp.cpp @@ -1,13284 +1,13280 @@ /* ** FILE: ScribeApp.cpp ** AUTHOR: Matthew Allen ** DATE: 22/10/1998 ** DESCRIPTION: Scribe email application ** ** Copyright (C) 1998, Matthew Allen ** fret@memecode.com */ // Debug defines // #define PRINT_OUT_STORAGE_TREE // #define TEST_OBJECT_SIZE #define USE_SPELLCHECKER 1 #define USE_INTERNAL_BROWSER 1 // for help #define RUN_STARTUP_SCRIPTS 1 #define PROFILE_ON_PULSE 0 #define TRAY_CONTACT_BASE 1000 #define TRAY_MAIL_BASE 10000 // Includes #include #include #include #include #include #include "Scribe.h" #include "lgi/common/StoreConvert1To2.h" #include "lgi/common/ProgressDlg.h" #include "lgi/common/TextLabel.h" #include "lgi/common/Button.h" #include "lgi/common/CheckBox.h" #include "lgi/common/OpenSSLSocket.h" #include "lgi/common/SoftwareUpdate.h" #include "lgi/common/Html.h" #include "lgi/common/TextView3.h" #include "lgi/common/RichTextEdit.h" #include "lgi/common/Browser.h" #include "lgi/common/ClipBoard.h" #include "lgi/common/Store3.h" #include "lgi/common/Growl.h" #include "lgi/common/Edit.h" #include "lgi/common/Box.h" #include "lgi/common/LgiRes.h" #include "lgi/common/SpellCheck.h" #include "lgi/common/SubProcess.h" #include "lgi/common/CssTools.h" #include "lgi/common/Map.h" #include "lgi/common/Charset.h" #include "lgi/common/RefCount.h" #include "ScribePrivate.h" #include "PreviewPanel.h" #include "ScribeStatusPanel.h" #include "ScribeFolderDlg.h" #include "ScribePageSetup.h" #include "Calendar.h" #include "CalendarView.h" #include "ScribeSpellCheck.h" #include "Store3Common.h" #include "PrintContext.h" #include "resource.h" #include "ManageMailStores.h" #include "ReplicateDlg.h" #include "ScribeAccountPreview.h" #include "Encryption/GnuPG.h" #include "Store3Webdav/WebdavStore.h" #include "resdefs.h" #include "../unittests/UnitTest.h" #include "../src/common/Coding/ScriptingPriv.h" #define DEBUG_STORE_EVENTS 0 #if DEBUG_STORE_EVENTS #define LOG_STORE(...) LgiTrace(__VA_ARGS__) #else #define LOG_STORE(...) #endif #define IDM_LOAD_MSG 2000 #define RAISED_LOOK 0 #define SUNKEN_LOOK false #ifdef MAC #define SUNKEN_CTRL false #else #define SUNKEN_CTRL true #endif #if LGI_CARBON #define TRAY_ICON_NONE -1 #define TRAY_ICON_NORMAL -1 #define TRAY_ICON_MAIL 0 #define TRAY_ICON_ERROR 1 #elif defined(WIN32) #define TRAY_ICON_NORMAL 0 #define TRAY_ICON_ERROR 1 #define TRAY_ICON_MAIL 2 #define TRAY_ICON_NONE 3 #else #define TRAY_ICON_NONE -1 #define TRAY_ICON_NORMAL 0 #define TRAY_ICON_ERROR 1 #define TRAY_ICON_MAIL 2 #endif char ScribeThingList[] = "com.memecode.ThingList"; ScribeClipboardFmt *ScribeClipboardFmt::Alloc(bool ForFolders, size_t Size) { ScribeClipboardFmt *obj = (ScribeClipboardFmt*) calloc(sizeof(ScribeClipboardFmt)+((Size-1)*sizeof(Thing*)), 1); if (obj) { memcpy(obj->Magic, ForFolders ? ScribeFolderMagic : ScribeThingMagic, sizeof(obj->Magic)); obj->ProcessId = LAppInst->GetProcessId(); obj->Len = (uint32_t)Size; } return obj; } ScribeClipboardFmt *ScribeClipboardFmt::Alloc(List &Lst) { ScribeClipboardFmt *Fmt = Alloc(false, Lst.Length()); for (unsigned i=0; iThingAt(i, Lst[i]); return Fmt; } ScribeClipboardFmt *ScribeClipboardFmt::Alloc(LArray &Arr) { ScribeClipboardFmt *Fmt = Alloc(false, Arr.Length()); for (unsigned i=0; iThingAt(i, Arr[i]); return Fmt; } bool ScribeClipboardFmt::Is(const char *Type, void *Ptr, size_t Bytes) { // Do we have the minimum bytes for the structure? if (Bytes >= sizeof(ScribeClipboardFmt) && Ptr != NULL) { ScribeClipboardFmt *This = (ScribeClipboardFmt*)Ptr; // Check the magic is the right value if (memcmp(This->Magic, Type, 4) != 0) return false; // Check it's from this process if (This->ProcessId != LAppInst->GetProcessId()) return false; return true; } return false; } Thing *ScribeClipboardFmt::ThingAt(size_t Idx, Thing *Set) { if (memcmp(Magic, ScribeThingMagic, 4)) return NULL; if (Idx >= Len) return NULL; if (Set) Things[Idx] = Set; return Things[Idx]; } ScribeFolder *ScribeClipboardFmt::FolderAt(size_t Idx, ScribeFolder *Set) { if (memcmp(Magic, ScribeFolderMagic, 4)) return NULL; if (Idx >= Len) return NULL; if (Set) Folders[Idx] = Set; return Folders[Idx]; } size_t ScribeClipboardFmt::Sizeof() { return sizeof(*this) + ((Len - 1) * sizeof(Thing*)); } bool OptionSizeInKiB = false; bool ShowRelativeDates = false; const char *MailAddressDelimiters = "\t\r\n;,"; char16 SpellDelim[] = { ' ', '\t', '\r', '\n', ',', ',', '.', ':', ';', '{', '}', '[', ']', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '-', '+', '=', '|', '\\', '/', '?', '\"', 0 }; const char *DefaultRfXml = "---------- %s ----------\n" "%s: ()\n" "%s: ()\n" "%s: \n" "%s: \n" "\n" "\n" "\n" "\n"; uchar DateTimeFormats[] = { GDTF_DEFAULT, GDTF_DAY_MONTH_YEAR | GDTF_12HOUR, GDTF_MONTH_DAY_YEAR | GDTF_12HOUR, GDTF_YEAR_MONTH_DAY | GDTF_12HOUR, GDTF_DAY_MONTH_YEAR | GDTF_24HOUR, GDTF_MONTH_DAY_YEAR | GDTF_24HOUR, GDTF_YEAR_MONTH_DAY | GDTF_24HOUR }; SystemFolderInfo SystemFolders[] = { {FOLDER_INBOX, OPT_Inbox, NULL}, {FOLDER_OUTBOX, OPT_Outbox, NULL}, {FOLDER_SENT, OPT_Sent, NULL}, {FOLDER_CONTACTS, OPT_Contacts, NULL}, {FOLDER_TRASH, OPT_Trash, NULL}, {FOLDER_CALENDAR, OPT_Calendar, OPT_HasCalendar}, {FOLDER_TEMPLATES, OPT_Templates, OPT_HasTemplates}, {FOLDER_FILTERS, OPT_Filters, OPT_HasFilters}, {FOLDER_GROUPS, OPT_Groups, OPT_HasGroups}, {FOLDER_SPAM, OPT_SpamFolder, OPT_HasSpam}, {-1, 0, 0} }; ScribeBehaviour *ScribeBehaviour::New(ScribeWnd *app) { return 0; } void ScribeOptionsDefaults(LOptionsFile *f) { if (!f) return; f->CreateTag("Accounts"); f->CreateTag("CalendarUI"); f->CreateTag("CalendarUI.Sources"); f->CreateTag("MailUI"); f->CreateTag("ScribeUI"); f->CreateTag("Plugins"); f->CreateTag("Print"); #define DefaultIntOption(opt, def) { LVariant v; if (!f->GetValue(opt, v)) \ f->SetValue(opt, v = (int)def); } #define DefaultStrOption(opt, def) { LVariant v; if (!f->GetValue(opt, v)) \ f->SetValue(opt, v = def); } DefaultIntOption(OPT_DefaultAlternative, 1); DefaultIntOption(OPT_BoldUnread, 1); DefaultIntOption(OPT_PreviewLines, 1); DefaultIntOption(OPT_AutoDeleteExe, 1); DefaultIntOption(OPT_DefaultReplyAllSetting, MAIL_ADDR_BCC); DefaultIntOption(OPT_BlinkNewMail, 1); DefaultIntOption(OPT_MarkReadAfterSeconds, 5); DefaultStrOption(OPT_BayesThreshold, "0.9"); DefaultIntOption(OPT_SoftwareUpdate, 1); DefaultIntOption(OPT_ResizeImgAttachments, false); DefaultIntOption(OPT_ResizeJpegQual, 80); DefaultIntOption(OPT_ResizeMaxPx, 1024); DefaultIntOption(OPT_ResizeMaxKb, 200); DefaultIntOption(OPT_RegisterWindowsClient, 1); DefaultIntOption(OPT_HasTemplates, 0); DefaultIntOption(OPT_HasCalendar, 1); DefaultIntOption(OPT_HasGroups, 1); DefaultIntOption(OPT_HasFilters, 1); DefaultIntOption(OPT_HasSpam, 0); } const char *Store3ItemTypeName(Store3ItemTypes t) { switch (t) { case MAGIC_NONE: return "MAGIC_NONE"; case MAGIC_BASE: return "MAGIC_BASE"; case MAGIC_MAIL: return "MAGIC_MAIL"; case MAGIC_CONTACT: return "MAGIC_CONTACT"; // case MAGIC_FOLDER: return "MAGIC_FOLDER"; case MAGIC_MAILBOX: return "MAGIC_MAILBOX"; case MAGIC_ATTACHMENT: return "MAGIC_ATTACHMENT"; case MAGIC_ANY: return "MAGIC_ANY"; case MAGIC_FILTER: return "MAGIC_FILTER"; case MAGIC_FOLDER: return "MAGIC_FOLDER"; case MAGIC_CONDITION: return "MAGIC_CONDITION"; case MAGIC_ACTION: return "MAGIC_ACTION"; case MAGIC_CALENDAR: return "MAGIC_CALENDAR"; case MAGIC_ATTENDEE: return "MAGIC_ATTENDEE"; case MAGIC_GROUP: return "MAGIC_GROUP"; default: LAssert(0); break; } return "(error)"; } void SetRecipients(ScribeWnd *App, char *Start, LDataIt l, EmailAddressType CC) { while (Start && *Start) { LString Str; char *End = strchr(Start, ','); if (End) { Str.Set(Start, End-Start); Start = End + 1; } else { Str = Start; Start = 0; } if (Str) { ListAddr *a = new ListAddr(App); if (a) { a->CC = CC; if (_strnicmp(Str, "mailto:", 7) == 0) a->sAddr = Str(7,-1); else a->sAddr = Str; l->Insert(a); } } } } static char SoftwareUpdateUri[] = "http://www.memecode.com/update.php"; enum SoftwareStatus { SwError, SwCancel, SwOutOfDate, SwUpToDate }; static LString ExtractVer(const char *s) { char Buf[256], *Out = Buf; for (const char *In = s; *In && Out < Buf + sizeof(Buf) - 1; In++) { if (*In == ' ') break; if (IsDigit(*In) || *In == '.') *Out++ = *In; } *Out++ = 0; return LString(Buf); } void IsSoftwareUpToDate(ScribeWnd *Parent, bool WithUI, bool IncBetas, std::function callback) { // LSoftwareUpdate::UpdateInfo Info // Software update? auto Proxy = Parent->GetHttpProxy(); auto Update = new LSoftwareUpdate(AppName, SoftwareUpdateUri, Proxy); Update->CheckForUpdate( [WithUI, Parent, callback, Update](auto Info, auto errorMsg) { if (Info) { auto LocalVer = LString(ScribeVer).SplitDelimit("."); LString BuildVer = ExtractVer(Info->Build); auto OnlineVer = BuildVer.SplitDelimit("."); if (OnlineVer.Length() != LocalVer.Length()) { LgiTrace("%s:%i - Invalid online version number \"%s\"\n", _FL, Info->Version.Get()); if(callback) callback(SwError, Info); return; } unsigned i; for(i = 0; i < OnlineVer.Length(); i++) { auto l = Atoi(LocalVer[i].Get()); auto o = Atoi(OnlineVer[i].Get()); if(l < o) { if(callback) callback(SwOutOfDate, Info); return; } if(l > o) { if(callback) callback(SwUpToDate, Info); return; } } LDateTime Compile; auto Date = LString(__DATE__).SplitDelimit(" "); Compile.Month(LDateTime::MonthFromName(Date[0])); Compile.Day(atoi(Date[1])); Compile.Year(atoi(Date[2])); Compile.SetTime(__TIME__); bool DateGreaterThenCompile = Info->Date > Compile; if (callback) callback(DateGreaterThenCompile ? SwOutOfDate : SwUpToDate, Info); return; } else if (WithUI) { if (callback) callback(SwCancel, NULL); LgiMsg(Parent, LLoadString(IDS_ERROR_SOFTWARE_UPDATE), AppName, MB_OK, errorMsg); } if (callback) callback(SwError, NULL); }, WithUI ? Parent : NULL, IncBetas); } bool UpgradeSoftware(const LSoftwareUpdate::UpdateInfo *Info, ScribeWnd *Parent, bool WithUI) { bool DownloadUpdate = true; if (WithUI) { char Ds[64]; Info->Date.Get(Ds, sizeof(Ds)); DownloadUpdate = LgiMsg(Parent, LLoadString(IDS_SOFTWARE_UPDATE_DOWNLOAD), AppName, MB_YESNO, Info->Build.Get(), Info->Uri.Get(), Ds) == IDYES; } if (!DownloadUpdate) return false; LAutoString Proxy = Parent->GetHttpProxy(); LSoftwareUpdate Update(AppName, SoftwareUpdateUri, Proxy, ScribeTempPath()); // FIXME: return false; // Update.ApplyUpdate(Info, false, Parent); } void SoftwareUpdate(ScribeWnd *Parent, bool WithUI, bool IncBetas, std::function callback) { // Software update? IsSoftwareUpToDate(Parent, WithUI, IncBetas, [WithUI, Parent, callback](auto s, auto Info) { if (s == SwUpToDate) { if (WithUI) LgiMsg(Parent, LLoadString(IDS_SOFTWARE_CURRENT), AppName, MB_OK); if (callback) callback(false); // we're up to date } else if (s == SwOutOfDate) { auto status = UpgradeSoftware(Info, Parent, WithUI); if (callback) callback(status); // update is going to happen } }); } const char *AppName = "Scribe"; char HelpFile[] = "index.html"; const char OptionsFileName[] = "ScribeOptions"; const char AuthorEmailAddr[] = "fret@memecode.com"; const char AuthorHomepage[] = "http://www.memecode.com"; const char ApplicationHomepage[] = "http://www.memecode.com/scribe.php"; const char CommercialHomepage[] = "http://www.memecode.com/inscribe.php"; const char FaqHomepage[] = "http://www.memecode.com/scribe/faq.php"; const char *DefaultFolderNames[16]; Store3ItemTypes DefaultFolderTypes[] = { MAGIC_MAIL, // Inbox MAGIC_MAIL, // Outbox MAGIC_MAIL, // Sent MAGIC_ANY, // Trash MAGIC_CONTACT, // Contacts MAGIC_MAIL, // Templates MAGIC_FILTER, // Filters MAGIC_CALENDAR, // Calendar Events MAGIC_GROUP, // Groups MAGIC_MAIL, // Spam MAGIC_NONE, MAGIC_NONE, MAGIC_NONE, MAGIC_NONE }; extern void Log(char *File, char *Str, ...); ////////////////////////////////////////////////////////////////////////////// void LogMsg(char *str, ...) { #ifdef _DEBUG char f[256]; LMakePath(f, sizeof(f), LGetExePath(), "log.txt"); if (str) { char buffer[256]; va_list arg; va_start(arg ,str); vsprintf_s(buffer, sizeof(buffer), str, arg); va_end(arg); LFile File; while (!File.Open(f, O_WRITE)) { LSleep(5); } File.Seek(File.GetSize(), SEEK_SET); File.Write(buffer, strlen(buffer)); } else { FileDev->Delete(f, false); } #endif } LString GetFullAppName(bool Platform) { LString Ret = AppName; if (Platform) { LString s; const char *Build = #ifndef _DEBUG "Release"; #else "Debug"; #endif LArray Ver; int Os = LGetOs(&Ver); const char *OsName = LGetOsName(); if (Os == LGI_OS_WIN9X) { switch (Ver[1]) { case 0: OsName = "Win95"; break; case 10: OsName = "Win98"; break; case 90: OsName = "WinME"; break; } } else if (Os == LGI_OS_WIN32 || Os == LGI_OS_WIN64) { if (Ver[0] < 5) { OsName = "WinNT"; } else if (Ver[0] == 5) { if (Ver[1] == 0) OsName = "Win2k"; else OsName = "WinXP"; } else if (Ver[0] == 6) { if (Ver[1] == 0) OsName = "Vista"; else if (Ver[1] == 1) OsName = "Win7"; else if (Ver[1] == 2) OsName = "Win8"; else if (Ver[1] == 3) OsName = "Win8.1"; } else if (Ver[0] == 10) { OsName = "Win10"; } else if (Ver[0] == 11) { // What's the chances eh? OsName = "Win11"; } } s.Printf(" v%s (%s v", ScribeVer, OsName); Ret += s; for (unsigned i=0; iId); Ret += s; } s.Printf(")"); Ret += s; } return Ret; } bool MatchWord(char *Str, char *Word) { bool Status = false; if (Str && Word) { #define IsWord(c) ( IsDigit(c) || IsAlpha(c) ) for (char *s=stristr(Str, Word); s; s=stristr(s+1, Word)) { char *e = s + strlen(Word); if ( (s<=Str || !IsWord(s[-1]) ) && (e[0] == 0 || !IsWord(e[0])) ) { return true; } } } return Status; } ////////////////////////////////////////////////////////////////////////////// ScribePanel::ScribePanel(ScribeWnd *app, const char *name, int size, bool open) : LPanel(name, size, open) { App = app; } bool ScribePanel::Pour(LRegion &r) { if (App) { SetClosedSize(App->GetToolbarHeight()); } return LPanel::Pour(r); } ////////////////////////////////////////////////////////////////////////////// class NoContactType : public Contact { LString NoFace80Path; LString NoFace160Path; public: NoContactType(ScribeWnd *wnd) : Contact(wnd) { } Thing &operator =(Thing &c) override { return *this; } bool GetVariant(const char *Name, LVariant &Value, const char *Array) override { ScribeDomType Fld = StrToDom(Name); int Px = Array ? atoi(Array) : 80; LString &Str = Px == 160 ? NoFace160Path : NoFace80Path; if (!Str) { LString f; f.Printf("NoFace%i.png", Px); Str = LFindFile(f); LAssert(Str != NULL); // This should always resolve. } if (!Str) return false; if (Fld == SdImageHtml) { LString html; html.Printf("\n", Str.Get()); Value = html; return true; } else if (Fld == SdImage) { Value = Str; return true; } return false; } }; class ScribeWndPrivate : public LBrowser::LBrowserEvents, public LVmCallback, public LHtmlStaticInst { LOptionsFile::PortableType InstallMode = LOptionsFile::UnknownMode; public: ScribeWnd *App; uint64 LastTs = 0; int ClipboardFormat = 0; LFont *PreviewFont = NULL; int PrintMaxPages = -1; int NewMailTimeout = -1; bool SendAfterReceive = false; bool IngoreOnClose = false; LAutoString UiTags; LAutoPtr Growl; LArray TrayMenuContacts; bool ExitAfterSend = false; LToolButton *ShowConsoleBtn = NULL; LString MulPassword; LString CalendarSummary; LBox *SubSplit = NULL, *SearchSplit = NULL; LArray ThingSources; int LastLayout = 0; LMenuItem *DisableUserFilters = NULL; LAutoPtr Options; HttpImageThread *ImageLoader = NULL; int LastMinute = -1, LastHour = -1; LArray Store3EventCallbacks; LAutoPtr PrintOptions; LHashTbl, LString> ResFiles; // These are for the LDataEventsI callbacks to store source context // Mainly for debugging where various events came from. const char *CtxFile = NULL; int CtxLine = 0; // Contact no face images LAutoRefPtr NoContact; // Remote content white/blacklists bool RemoteContent_Init = false; LString::Array RemoteWhiteLst, RemoteBlackLst; // Spell checking int AppWndHnd; LAutoPtr SpellerThread; // Missing caps LCapabilityTarget::CapsHash MissingCaps; MissingCapsBar *Bar = NULL; LString ErrSource; // Script file that has an error. Filter *ErrFilter = NULL; // Filter that has scripting error. // Load state bool FoldersLoaded = false; // Bayesian filter LStringPipe BayesLog; // Thread item processing LArray Transfers; // Scripting... LAutoPtr Engine; LArray Scripts; LArray CurrentScripts; LScript *CurrentScript() { return CurrentScripts.Length() ? CurrentScripts.Last() : NULL; } int NextToolMenuId = IDM_TOOL_SCRIPT_BASE; LAutoPtr ScriptToolbar; LArray OnSecondTimerCallbacks; // Encryption LAutoPtr GpgInst; // Unit tests LAutoPtr UnitTestServer; class ScribeTextControlFactory : public LViewFactory { ScribeWnd *Wnd; LView *NewView(const char *Class, LRect *Pos, const char *Text) { if (!_stricmp(Class, "ScribeTextView")) return Wnd->CreateTextControl(-1, 0, true); return NULL; } public: ScribeTextControlFactory(ScribeWnd *wnd) { Wnd = wnd; } } TextControlFactory; ScribeWndPrivate(ScribeWnd *app) : App(app), TextControlFactory(app) { NoContact = new NoContactType(app); NoContact->DecRef(); // 2->1 AppWndHnd = LEventSinkMap::Dispatch.AddSink(App); #ifdef WIN32 ClipboardFormat = RegisterClipboardFormat( #ifdef UNICODE L"Scribe.Item" #else "Scribe.Item" #endif ); #endif LScribeScript::Inst = new LScribeScript(App); if (Engine.Reset(new LScriptEngine(App, LScribeScript::Inst, this))) Engine->SetConsole(LScribeScript::Inst->GetLog()); } ~ScribeWndPrivate() { // Why do we need this? ~LView will take care of it? // LEventSinkMap::Dispatch.RemoveSink(App); Options.Reset(); Scripts.DeleteObjects(); DeleteObj(ImageLoader); Engine.Reset(); DeleteObj(LScribeScript::Inst); } LGrowl *GetGrowl() { if (!Growl && Growl.Reset(new LGrowl)) { LAutoPtr r(new LGrowl::LRegister); r->App = "Scribe"; r->IconUrl = "http://memecode.com/images/scribe/growl-app.png"; LGrowl::LNotifyType &NewMail = r->Types.New(); NewMail.Name = "new-mail"; NewMail.IconUrl = "http://memecode.com/images/scribe/growl-new-mail.png"; NewMail.Enabled = true; LGrowl::LNotifyType &Cal = r->Types.New(); Cal.Name = "calendar"; Cal.IconUrl = "http://memecode.com/images/scribe/growl-calendar.png"; Cal.Enabled = true; LGrowl::LNotifyType &Debug = r->Types.New(); Debug.Name = "debug"; Debug.IconUrl = "http://memecode.com/images/scribe/growl-bug.png"; Debug.Enabled = false; LGrowl::LNotifyType &Info = r->Types.New(); Info.IconUrl = "http://memecode.com/images/scribe/growl-note.png"; Info.Name = "info"; Info.Enabled = true; Growl->Register(r); } return Growl; } LVmDebugger *AttachVm(LVirtualMachine *OriginalVm, LCompiledCode *Code, const char *Assembly) { if (!OriginalVm || !Code) return NULL; LVariant v; if (Options) Options->GetValue(OPT_ScriptDebugger, v); if (!v.CastInt32()) return NULL; LAutoPtr CopiedVm(new LVirtualMachine(OriginalVm)); LAutoPtr CopiedCode(new LCompiledCode(*Code)); return new LVmDebuggerWnd(App, this, CopiedVm, CopiedCode, Assembly); } bool CallCallback(LVirtualMachine &Vm, LString CallbackName, LScriptArguments &Args) { for (auto s: Scripts) { if (!s->Code) continue; auto Method = s->Code->GetMethod(CallbackName); if (!Method) continue; auto Status = Vm.ExecuteFunction(s->Code, Method, Args); return Status > ScriptError; } Vm.SetDebuggerEnabled(true); // Lets show the UI when we throw the callback not found error. Args.Throw(_FL, "There is no function '%s' for callback.", CallbackName.Get()); return false; } bool CompileScript(LAutoPtr &Output, const char *FileName, const char *Source) { LCompiler c; return c.Compile(Output, Engine->GetSystemContext(), LScribeScript::Inst, FileName, Source, NULL); } bool OnSearch(LBrowser *br, const char *txt) { char Path[256]; if (!App->GetHelpFilesPath(Path, sizeof(Path))) return false; auto Terms = LString(txt).SplitDelimit(", "); LStringPipe p; p.Print("\n

Search Results

\n
    \n"); LDirectory Dir; for (int b = Dir.First(Path, "*.html"); b; b = Dir.Next()) { if (!Dir.IsDir()) { char Path[256]; Dir.Path(Path, sizeof(Path)); LFile f; if (f.Open(Path, O_READ)) { LXmlTree t(GXT_NO_DOM); LXmlTag r; if (t.Read(&r, &f)) { char *PrevName = 0; char PrevUri[256] = ""; for (auto c: r.Children) { if (c->IsTag("a")) { char *Name = c->GetAttr("name"); if (Name) { PrevName = Name; } } else if (c->GetContent()) { bool Hit = false; for (unsigned i=0; !Hit && iGetContent(), Terms[i]) != 0; } if (Hit) { LStringPipe Uri(256); char *Leaf = strrchr(Path, DIR_CHAR); Leaf = Leaf ? Leaf + 1 : Path; Uri.Print("file://%s", Path); if (PrevName) Uri.Print("#%s", PrevName); LAutoString UriStr(Uri.NewStr()); if (_stricmp(UriStr, PrevUri)) { p.Print("
  • %s", UriStr.Get(), Leaf); if (PrevName) p.Print("#%s", PrevName); p.Print("\n"); strcpy_s(PrevUri, sizeof(PrevUri), UriStr); } } } } } } } } p.Print("
\n\n\n"); LAutoString Html(p.NewStr()); br->SetHtml(Html); return true; } void AskUserForInstallMode(std::function callback) { auto Dlg = new LAlert(App, AppName, LLoadString(IDS_PORTABLE_Q), LLoadString(IDS_HELP), LLoadString(IDS_DESKTOP), LLoadString(IDS_PORTABLE)); Dlg->SetButtonCallback(1, [this](auto idx) { App->LaunchHelp("install.html"); }); Dlg->DoModal([callback](auto dlg, auto Btn) { if (Btn == 1) { // Help LAssert(!"Help btn should use callback."); } else if (Btn == 2) { // Desktop if (callback) callback(LOptionsFile::DesktopMode); } else if (Btn == 3) { // Portable if (callback) callback(LOptionsFile::PortableMode); } else { delete dlg; LAppInst->Exit(1); } delete dlg; }); } LOptionsFile::PortableType GetInstallMode() { if (InstallMode == LOptionsFile::UnknownMode) { if (LAppInst->GetOption("portable")) { InstallMode = LOptionsFile::PortableMode; LgiTrace("Selecting portable mode based on -portable switch.\n"); } else if (LAppInst->GetOption("desktop")) { InstallMode = LOptionsFile::DesktopMode; LgiTrace("Selecting portable mode based on -desktop switch.\n"); } } if (InstallMode == LOptionsFile::UnknownMode) { bool PortableIsPossible = true; char Inst[MAX_PATH_LEN] = ""; LGetSystemPath(LSP_APP_INSTALL, Inst, sizeof(Inst)); // Do write check char Wr[MAX_PATH_LEN]; LMakePath(Wr, sizeof(Wr), Inst, "_write_test.txt"); LFile f; if (f.Open(Wr, O_WRITE)) { // Clean up f.Close(); FileDev->Delete(Wr, false); } else { // No write perms PortableIsPossible = false; } if (PortableIsPossible && LAppInst->IsElevated()) { // Check if the install is in some read only location: // e.g. c:\Program Files char Pm[MAX_PATH_LEN]; if (LGetSystemPath(LSP_USER_APPS, Pm, sizeof(Pm))) { size_t n = strlen(Pm); PortableIsPossible = _strnicmp(Pm, Inst, n) != 0; // LgiMsg(App, "%i\n%s\n%s", AppName, MB_OK, PortableIsPossible, Pm, Inst); } else LgiTrace("%s:%i - Failed to get paths.", _FL); } if (PortableIsPossible) { // Basically "ask the user" here... return LOptionsFile::UnknownMode; } else { InstallMode = LOptionsFile::DesktopMode; LgiTrace("Selecting Desktop based on lack of write permissions to install folder.\n"); } } return InstallMode; } void SetInstallMode(LOptionsFile::PortableType t) { InstallMode = t; } void DeleteCallbacks(LArray &Callbacks) { for (unsigned i=0; iGetMenu()->FindItem(Callbacks[i].Param); if (it) { it->Remove(); DeleteObj(it); } } } } }; ////////////////////////////////////////////////////////////////////////////// void UpgradeRfOption(ScribeWnd *App, const char *New, const char *Old, const char *Default) { LVariant v; /* App->GetOptions()->GetValue(New, v); if (v.Str()) { ScribePath *Path = new ScribePath(App, Old); if (Path) { char *Xml = LReadTextFile(*Path); if (Xml) { App->GetOptions()->SetValue(New, v = Xml); DeleteArray(Xml); } App->GetOptions()->DeleteValue(Old); DeleteObj(Path); } } */ if (Default && !App->GetOptions()->GetValue(New, v)) { App->GetOptions()->SetValue(New, v = Default); } } //////////////////////////////////////////////////////////////////////////// ScribeWnd::AppState ScribeWnd::ScribeState = ScribeConstructing; /* * This constructor is a little convoluted, but the basic idea is this: * * - Do some basic init. * - Attempt to load the options (could make portable/desktop mode clear) * - If the portable/desktop mode is unclear ask the user. * - Call AppConstruct1. * - If the UI language is not known, ask the user. * - Call AppConstruct2. * * Each time a dialog is needed the rest of the code needs to be in a callable function. * * It's important to note that the ScribeWnd::OnCreate method needs to be called after * the system Handle() is created, and after any dialogs in the ScribeWnd::ScribeWnd * constructor have finished. */ ScribeWnd::ScribeWnd() : BayesianFilter(this), CapabilityInstaller("Scribe", ScribeVer, "http://memecode.com/components/lookup.php", ScribeTempPath()), TrayIcon(this) { if (_Lock) _Lock->SetName("ScribeWnd"); // init some variables LApp::ObjInstance()->AppWnd = this; LCharsetSystem::Inst()->DetectCharset = ::DetectCharset; d = new ScribeWndPrivate(this); ScribeIpc = new LSharedMemory("Scribe", SCRIBE_INSTANCE_MAX * sizeof(ScribeIpcInstance)); if (ScribeIpc && ScribeIpc->GetPtr()) { ScribeIpcInstance *InstLst = (ScribeIpcInstance*) ScribeIpc->GetPtr(); for (int i=0; iMagic = SCRIBE_INSTANCE_MAGIC; ThisInst->Pid = LProcessId(); // LgiTrace("Install Scribe pid=%i to pos=%i\n", LProcessId(), i); } } } else DeleteObj(ScribeIpc); #ifndef WIN32 printf("%s\n", GetFullAppName(true).Get()); #endif auto Type = d->GetInstallMode(); if (Type == LOptionsFile::UnknownMode) { // This may make the mode more clear... if (LoadOptions()) Type = d->GetInstallMode(); } if (Type == LOptionsFile::UnknownMode) { d->AskUserForInstallMode([this](auto selectedMode) { d->SetInstallMode(selectedMode); if (!d->Options) d->Options.Reset(new LOptionsFile(selectedMode, OptionsFileName)); Construct1(); }); } else Construct1(); } void ScribeWnd::Construct1() { if (!d->Options && !LoadOptions()) { ScribeState = ScribeExiting; return; } ScribeOptionsDefaults(d->Options); LVariant GlyphSub; if (GetOptions()->GetValue(OPT_GlyphSub, GlyphSub)) { bool UseGlyphSub = GlyphSub.CastInt32() != 0; LSysFont->SubGlyphs(UseGlyphSub); LSysBold->SubGlyphs(UseGlyphSub); LFontSystem::Inst()->SetDefaultGlyphSub(UseGlyphSub); } else { GetOptions()->SetValue(OPT_GlyphSub, GlyphSub = LFontSystem::Inst()->GetDefaultGlyphSub()); } { // Limit the size of the 'Scribe.txt' log file if (auto p = LgiTraceGetFilePath()) { int64 Sz = LFileSize(p); #define MiB * 1024 * 1024 if (Sz > (3 MiB)) FileDev->Delete(p); } } // Process pre-UI options LVariant SizeAdj; int SzAdj = SizeAdj.CastInt32(); if (GetOptions()->GetValue(OPT_UiFontSize, SizeAdj) && (SzAdj = SizeAdj.CastInt32()) >= 0 && SzAdj < 5) { SzAdj -= 2; if (SzAdj) { int Pt = LSysFont->PointSize(); LSysFont->PointSize(Pt + SzAdj); LSysFont->Create(); LSysBold->PointSize(Pt + SzAdj); LSysBold->Create(); LFont *m = LMenu::GetFont(); if (m) { m->PointSize(m->PointSize() + SzAdj); m->Create(); } } } else { GetOptions()->SetValue(OPT_UiFontSize, SizeAdj = 2); } // Resources and languages SetLanguage(); // If no language set... LVariant LangId; if (!GetOptions()->GetValue(OPT_UiLanguage, LangId)) { // Ask the user... auto Dlg = new LanguageDlg(this); if (!Dlg->Ok) { delete Dlg; LgiMsg(this, "Failed to create language selection dialog.", "Scribe Error"); ScribeState = ScribeExiting; LCloseApp(); } else { Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) { // Set the language in the options file LVariant v; GetOptions()->SetValue(OPT_UiLanguage, v = Dlg->Lang.Get()); // Reload the resource file... to get the new lang. LResources *Cur = LgiGetResObj(false); DeleteObj(Cur); SetLanguage(); Construct2(); } else // User canceled { ScribeState = ScribeExiting; LCloseApp(); } delete dlg; }); } } else Construct2(); } void ScribeWnd::Construct2() { #if 1 auto CurRes = LgiGetResObj(false); LVariant Theme; if (CurRes && GetOptions()->GetValue(OPT_Theme, Theme)) { auto Paths = ScribeThemePaths(); auto NoTheme = LLoadString(IDS_DEFAULT); if (Theme.Str() && Stricmp(NoTheme, Theme.Str())) { for (auto p: Paths) { LFile::Path Inst(p); Inst += Theme.Str(); if (Inst.Exists()) { CurRes->SetThemeFolder(Inst); d->Static->OnSystemColourChange(); break; } } } } #endif LoadCalendarStringTable(); ZeroObj(DefaultFolderNames); DefaultFolderNames[FOLDER_INBOX] = LLoadString(IDS_FOLDER_INBOX, "Inbox"); DefaultFolderNames[FOLDER_OUTBOX] = LLoadString(IDS_FOLDER_OUTBOX, "Outbox"); DefaultFolderNames[FOLDER_SENT] = LLoadString(IDS_FOLDER_SENT, "Sent"); DefaultFolderNames[FOLDER_TRASH] = LLoadString(IDS_FOLDER_TRASH, "Trash"); DefaultFolderNames[FOLDER_CONTACTS] = LLoadString(IDS_FOLDER_CONTACTS, "Contacts"); DefaultFolderNames[FOLDER_TEMPLATES] = LLoadString(IDS_FOLDER_TEMPLATES, "Templates"); DefaultFolderNames[FOLDER_FILTERS] = LLoadString(IDS_FOLDER_FILTERS, "Filters"); DefaultFolderNames[FOLDER_CALENDAR] = LLoadString(IDS_FOLDER_CALENDAR, "Calendar"); DefaultFolderNames[FOLDER_GROUPS] = LLoadString(IDS_FOLDER_GROUPS, "Groups"); DefaultFolderNames[FOLDER_SPAM] = LLoadString(IDS_SPAM, "Spam"); LStringPipe RfXml; RfXml.Print(DefaultRfXml, LLoadString(IDS_ORIGINAL_MESSAGE), LLoadString(FIELD_TO), LLoadString(FIELD_FROM), LLoadString(FIELD_SUBJECT), LLoadString(IDS_DATE)); { LAutoString Xml(RfXml.NewStr()); UpgradeRfOption(this, OPT_TextReplyFormat, "ReplyXml", Xml); UpgradeRfOption(this, OPT_TextForwardFormat, "ForwardXml", Xml); } LFontType t; if (t.GetSystemFont("small")) { d->PreviewFont = t.Create(); if (d->PreviewFont) { #if defined WIN32 d->PreviewFont->PointSize(8); #endif } } MoveOnScreen(); // Load global graphics LoadImageResources(); // Load time threads // Window name Name(AppName); SetSnapToEdge(true); ClearTempPath(); #if WINNATIVE SetStyle(GetStyle() & ~WS_VISIBLE); SetExStyle(GetExStyle() & ~WS_EX_ACCEPTFILES); CreateClassW32(AppName, LoadIcon(LProcessInst(), MAKEINTRESOURCE(IDI_APP))); #endif #if defined LINUX SetIcon("About64px.png"); LFinishXWindowsStartup(this); #endif ScribeState = ScribeConstructed; OnCreate(); } void ScribeWnd::Construct3() { if (ScribeState == ScribeConstructing) { // Constructor is still running, probably showing some UI. // Don't complete setup at this point. return; } // Load the styles LResources::StyleElement(this); // Main menu Menu = new LMenu(AppName); if (Menu) { Menu->Attach(this); if (Menu->Load(this, "ID_MENU", GetUiTags())) { LAssert(ImageList != NULL); Menu->SetImageList(ImageList, false); auto IdentityItem = Menu->FindItem(IDM_NO_IDENTITIES); if (IdentityItem) { IdentityMenu = IdentityItem->GetParent(); } CmdSend.MenuItem = Menu->FindItem(IDM_SEND_MAIL); auto NewMailMenu = Menu->FindItem(IDM_NEW_EMAIL); if (NewMailMenu) { MailMenu = NewMailMenu->GetParent(); } LVariant v; WorkOffline = Menu->FindItem(IDM_WORK_OFFLINE); if (WorkOffline && GetOptions()->GetValue(OPT_WorkOffline, v)) { WorkOffline->Checked(v.CastInt32() != 0); } if ((d->DisableUserFilters = Menu->FindItem(IDM_FILTERS_DISABLE))) { if (GetOptions()->GetValue(OPT_DisableUserFilters, v)) { d->DisableUserFilters->Checked(v.CastInt32() != 0); } } #if RUN_STARTUP_SCRIPTS // Run scripts in './Scripts' folder char s[MAX_PATH_LEN]; LMakePath(s, sizeof(s), ScribeResourcePath(), "scripts"); if (!LDirExists(s)) LMakePath(s, sizeof(s), LGetSystemPath(LSP_APP_INSTALL), #if defined(LINUX) || defined(WINDOWS) "..\\" #endif "scripts"); if (!LDirExists(s)) LgiTrace("%s:%i - Error: the scripts folder '%s' doesn't exist.\n", _FL, s); else { bool ErrorDsp = false; LDirectory Dir; for (int b = Dir.First(s); b; b = Dir.Next()) { if (Dir.IsDir()) continue; char *Ext = LGetExtension(Dir.GetName()); if (!Ext || _stricmp(Ext, "script") != 0) continue; Dir.Path(s, sizeof(s)); LStringPipe Log; char *Source = LReadTextFile(s); if (Source) { LScript *Cur = new LScript; if (Cur) { char Msg[256]; d->CurrentScripts.Add(Cur); LScribeScript::Inst->GetLog()->Write(Msg, sprintf_s(Msg, sizeof(Msg), "Compiling '%s'...\n", Dir.GetName())); LCompiler c; if (c.Compile( Cur->Code, d->Engine->GetSystemContext(), LScribeScript::Inst, s, Source, NULL)) { LFunctionInfo *Main = Cur->Code->GetMethod("Main"); if (Main) { LVirtualMachine Vm(d); LScriptArguments Args(&Vm); Args.New() = new LVariant((LDom*)this); d->Scripts.Add(Cur); if (Vm.ExecuteFunction( Cur->Code, Main, Args, LScribeScript::Inst->GetLog()) && Args.GetReturn()->CastInt32()) { d->CurrentScripts.Delete(Cur, true); Cur = NULL; } else { LgiTrace("Error: Script's main failed (%s)\n", Cur->Code->GetFileName()); if (Cur->Callbacks.Length()) d->DeleteCallbacks(Cur->Callbacks); d->Scripts.Delete(Cur); } Args.DeleteObjects(); } } else if (!ErrorDsp) { ErrorDsp = true; OnScriptCompileError(Source, NULL); } if (Cur) { d->CurrentScripts.Delete(Cur, true); DeleteObj(Cur); } } DeleteArray(Source); } } } #endif #define EnableItem(id, en) { auto i = Menu->FindItem(id); if (i) i->Enabled(en); } #define SetMenuIcon(id, ico) { auto i = Menu->FindItem(id); if (i) i->Icon(ico); } EnableItem(IDM_IMPORT_OUTLOOK_ITEMS, true); // SetMenuIcon(IDM_OPEN_FOLDERS, ICON_OPEN_FOLDER); SetMenuIcon(IDM_OPTIONS, ICON_OPTIONS); SetMenuIcon(IDM_SECURITY, ICON_LOCK); SetMenuIcon(IDM_CUT, ICON_CUT); SetMenuIcon(IDM_COPY, ICON_COPY); SetMenuIcon(IDM_PASTE, ICON_PASTE); SetMenuIcon(IDM_LAYOUT1, ICON_LAYOUT1); SetMenuIcon(IDM_LAYOUT2, ICON_LAYOUT2); SetMenuIcon(IDM_LAYOUT3, ICON_LAYOUT3); SetMenuIcon(IDM_LAYOUT4, ICON_LAYOUT4); SetMenuIcon(IDM_NEW_EMAIL, ICON_UNSENT_MAIL); SetMenuIcon(IDM_SET_READ, ICON_READ_MAIL); SetMenuIcon(IDM_SET_UNREAD, ICON_UNREAD_MAIL); SetMenuIcon(IDM_NEW_CONTACT, ICON_CONTACT); SetMenuIcon(IDM_NEW_GROUP, ICON_CONTACT_GROUP); SetMenuIcon(IDM_REPLY, ICON_FLAGS_REPLY); SetMenuIcon(IDM_REPLY_ALL, ICON_FLAGS_REPLY); SetMenuIcon(IDM_FORWARD, ICON_FLAGS_FORWARD); SetMenuIcon(IDM_BOUNCE, ICON_FLAGS_BOUNCE); SetMenuIcon(IDM_NEW_FILTER, ICON_FILTER); SetMenuIcon(IDM_FILTER_CURRENT_FOLDER, ICON_FOLDER_FILTERS); SetMenuIcon(IDM_MEMECODE, ICON_LINK); SetMenuIcon(IDM_HOMEPAGE, ICON_LINK); SetMenuIcon(IDM_SCRIBE_FAQ, ICON_LINK); SetMenuIcon(IDM_INSCRIBE_LINK, ICON_LINK); SetMenuIcon(IDM_VERSION_HISTORY, ICON_LINK); SetMenuIcon(IDM_DEBUG_INFO, ICON_LINK); SetMenuIcon(IDM_TUTORIALS, ICON_LINK); SetMenuIcon(IDM_FEEDBACK, ICON_UNREAD_MAIL); SetMenuIcon(IDM_HELP, ICON_HELP); LMenuItem *mi; if ( GetOptions()->GetValue(OPT_EditControl, v) && (mi = Menu->FindItem(IDM_HTML_EDITOR)) ) mi->Checked(v.CastInt32() != 0); Menu->SetPrefAndAboutItems(IDM_OPTIONS, IDM_ABOUT); } } // Initialize user interface SetupUi(); // Get some of the base submenu pointers. These are needed // for SetupAccounts to work correctly, e.g. populate the // send/receive/preview submenus. Folders need to be loaded // before this for the templates folder BuildDynMenus(); // Load accounts SetupAccounts(); // Recursively load folder tree LoadFolders([this](auto status) { // Redo it for the templates... now that load folders has completed. BuildDynMenus(); if (ScribeState == ScribeExiting) return; // Process command line OnCommandLine(); // Update the templates sub-menu now that the folders are loaded BuildDynMenus(); // Check registry settings SetDefaultHandler(); // Run on load scripts... LArray OnLoadCallbacks; if (GetScriptCallbacks(LOnLoad, OnLoadCallbacks)) { for (auto r: OnLoadCallbacks) { LVirtualMachine Vm; LScriptArguments Args(&Vm); Args.New() = new LVariant(this); ExecuteScriptCallback(*r, Args); Args.DeleteObjects(); } } ScribeState = ScribeRunning; }); } void ScribeWnd::SetLanguage() { LVariant LangId; if (GetOptions()->GetValue(OPT_UiLanguage, LangId)) { // Set the language to load... LAppInst->SetConfig("Language", LangId.Str()); } LResources::SetLoadStyles(true); // Load the resources (with the current lang) if (!LgiGetResObj(true, "Scribe")) { LgiMsg(NULL, "The resource file 'Scribe.lr8' is missing.", AppName); ScribeState = ScribeExiting; LCloseApp(); } } ScribeWnd::~ScribeWnd() { LAppInst->AppWnd = 0; SearchView = NULL; ScribeState = ScribeExiting; LScribeScript::Inst->ShowScriptingWindow(false); // Other cleanup... ClearTempPath(); ShutdownIpc(); SetPulse(); // Save anything thats still dirty in the folders... // just in case we crash during the shutdown phase. ScribeFolder *Cur = GetCurrentFolder(); if (Cur) Cur->SerializeFieldWidths(); SaveDirtyObjects(5000); // Tell the UI not to reference anything in the folders if (PreviewPanel) { PreviewPanel->OnThing(0, false); } Mail::NewMailLst.Empty(); // ~AccountStatusItem references the account list... must be before we // delete the accounts. DeleteObj(StatusPanel); // ~Accountlet needs to reference the root container... so // it has to go before unloading of folders. Accounts.DeleteObjects(); UnLoadFolders(); DeleteObj(PreviewPanel); SaveOptions(); DeleteObj(Commands); DeleteObj(d->PreviewFont); DeleteObj(d->SubSplit); DeleteObj(Splitter); MailList = NULL; CmdSend.ToolButton = NULL; CmdReceive.ToolButton = NULL; CmdPreview.ToolButton = NULL; CmdSend.MenuItem = NULL; CmdReceive.MenuItem = NULL; CmdPreview.MenuItem = NULL; // This could be using the OpenSSL library for HTTPS connections. So // close it before calling EndSSL. DeleteObj(d->ImageLoader); // This has to be after we close all the accounts... otherwise // they might still be using SSL functions, e.g. an IMAP/SSL connect. EndSSL(); DeleteObj(d); } LString ScribeWnd::GetResourceFile(SribeResourceType Type) { auto File = d->ResFiles.Find(Type); if (!File) LgiTrace("%s:%i - No file for resource type %i\n", _FL, Type); return File; } void ScribeWnd::LoadImageResources() { auto Res = LgiGetResObj(); LString::Array Folders; if (Res) { auto p = Res->GetThemeFolder(); if (p) Folders.Add(p); } Folders.Add(ScribeResourcePath()); for (auto p: Folders) { LDirectory Dir; LgiTrace("%s:%i - Loading resource folder '%s'\n", _FL, p.Get()); for (auto b = Dir.First(p); b; b = Dir.Next()) { if (Dir.IsDir()) continue; auto Name = Dir.GetName(); if (MatchStr("Toolbar-*.png", Name)) { if (!d->ResFiles.Find(ResToolbarFile)) d->ResFiles.Add(ResToolbarFile, Dir.FullPath()); } else if (MatchStr("xgate-icons-*.png", Name)) d->ResFiles.Add(ResToolbarFile, Dir.FullPath()); else if (MatchStr("Icons-*.png", Name)) d->ResFiles.Add(ResIconsFile, Dir.FullPath()); } } ToolbarImgs.Reset(LLoadImageList(GetResourceFile(ResToolbarFile))); ImageList.Reset(LLoadImageList(GetResourceFile(ResIconsFile))); if (!ImageList) LgiTrace("%s:%i - Failed to load toolbar image ('xgate-icons-32.png' or 'Toolbar-24.png')\n", _FL); } int ScribeWnd::GetEventHandle() { return d->AppWndHnd; } void ScribeWnd::OnCloseInstaller() { d->Bar = NULL; if (InThread()) { PourAll(); } else LAssert(0); } void ScribeWnd::OnInstall(CapsHash *Caps, bool Status) { } bool ScribeWnd::NeedsCapability(const char *Name, const char *Param) { #if DEBUG_CAPABILITIES LgiTrace("ScribeWnd::NeedsCapability(%s, %s)\n", Name, Param); #endif if (!InThread()) { #if DEBUG_CAPABILITIES LgiTrace("%s:%i - Posting M_NEEDS_CAP\n", _FL); #endif PostEvent(M_NEEDS_CAP, (LMessage::Param)NewStr(Name), (LMessage::Param)NewStr(Param)); } else { if (!Name) return false; if (d->MissingCaps.Find(Name)) { #if DEBUG_CAPABILITIES LgiTrace("%s:%i - Already in MissingCaps\n", _FL); #endif return true; } d->MissingCaps.Add(Name, true); LStringPipe MsgBuf(256); int i = 0; // const char *k; // for (bool b=d->MissingCaps.First(&k); b; b=d->MissingCaps.Next(&k), i++) for (auto k : d->MissingCaps) { MsgBuf.Print("%s%s", i?", ":"", k.key); } LVariant Actions; if (stristr(Name, "OpenSSL")) { MsgBuf.Print(LLoadString(IDS_ERROR_SERVER_CONNECT)); if (Param) MsgBuf.Print("\n%s", Param); Actions.Add(new LVariant(LLoadString(IDS_INSTALL))); } else if (stristr(Name, "Registry")) { MsgBuf.Print(LLoadString(IDS_ERROR_REG_WRITE)); Actions.Add(new LVariant(LLoadString(IDS_DONT_SHOW_AGAIN))); } else if (stristr(Name, "SpellingDictionary")) { MsgBuf.Print(LLoadString(IDS_ERROR_NEED_INSTALL), Param); Actions.Add(new LVariant(LLoadString(IDS_DOWNLOAD))); } Actions.Add(new LVariant(LLoadString(IDS_OK))); #if DEBUG_CAPABILITIES LgiTrace("%s:%i - Actions.Length()=%i, Bar=%p\n", _FL, Actions.Length(), d->Bar); #endif if (Actions.Length()) { LAutoString Msg(MsgBuf.NewStr()); // Check the script hook here... bool ShowInstallBar = true; LArray Callbacks; if (GetScriptCallbacks(LBeforeInstallBar, Callbacks)) { for (unsigned i=0; iCastInt32()) ShowInstallBar = false; else Msg.Reset(TheMsg.ReleaseStr()); } } } // Now create the capability install bar... if (!d->Bar && ShowInstallBar && Actions.Type == GV_LIST) { // FYI Capabilities are handled in ScribeWnd::StartAction. LArray Act; for (auto v : *Actions.Value.Lst) Act.Add(v->Str()); d->Bar = new MissingCapsBar(this, &d->MissingCaps, Msg, this, Act); AddView(d->Bar, 2); AttachChildren(); OnPosChange(); } } } return true; } LAutoString ScribeWnd::GetDataFolder() { LVariant v; GetOptions()->GetValue(OPT_IsPortableInstall, v); char p[MAX_PATH_LEN]; if (LGetSystemPath(v.CastInt32() ? LSP_APP_INSTALL : LSP_APP_ROOT, p, sizeof(p))) { if (!LDirExists(p)) FileDev->CreateFolder(p); return LAutoString(NewStr(p)); } else LgiTrace("%s:%i - LgiGetSystemPath failed (portable=%i).\n", _FL, v.CastInt32()); return LAutoString(); } LScriptEngine *ScribeWnd::GetScriptEngine() { return d->Engine; } LScriptCallback ScribeWnd::GetCallback(const char *CallbackMethodName) { LScriptCallback Cb; auto Cur = d->CurrentScript(); if (Cur && Cur->Code) { Cb.Script = Cur; Cb.Func = Cur->Code->GetMethod(CallbackMethodName); } if (!Cb.Func) { for (auto s: d->Scripts) { Cb.Script = s; if ((Cb.Func = s->Code->GetMethod(CallbackMethodName))) break; } } return Cb; } bool ScribeWnd::RegisterCallback(LScriptCallbackType Type, LScriptArguments &Args) { if (!d->CurrentScript()) { LgiTrace("%s:%i - No current script.\n", _FL); return false; } char *Fn = Args[1]->Str(); LScriptCallback Cb = GetCallback(Fn); if (!Cb.Func) { LgiTrace("%s:%i - No callback '%s'.\n", _FL, Fn); return false; } switch (Type) { case LToolsMenu: { char *Menu = Args[0]->Str(); auto Cur = d->CurrentScript(); if (!Menu || !Fn || !Cur) { LgiTrace("%s:%i - menu=%s, fn=%s.\n", _FL, Menu, Fn); return false; } LScriptCallback &c = Cur->Callbacks.New(); c = Cb; c.Type = Type; c.Param = d->NextToolMenuId; LMenuItem *Tools = GetMenu()->FindItem(IDM_TOOLS_MENU); auto ToolSub = Tools ? Tools->Sub() : 0; if (ToolSub) { if (d->NextToolMenuId == IDM_TOOL_SCRIPT_BASE) { ToolSub->AppendSeparator(); } ToolSub->AppendItem(Menu, c.Param, true); d->NextToolMenuId++; } break; } case LThingContextMenu: case LFolderContextMenu: case LThingUiToolbar: case LMailOnBeforeSend: case LMailOnAfterReceive: case LApplicationToolbar: case LBeforeInstallBar: case LInstallComponent: case LOnTimer: case LRenderMail: case LOnLoad: { auto Cur = d->CurrentScript(); LAssert(d->Scripts.HasItem(Cur)); LScriptCallback &c = Cur->Callbacks.New(); c = Cb; c.Type = Type; if (Args.Length() > 2) c.Data = *Args[2]; break; } default: { LAssert(!"Not a known callback type"); return false; } } return true; } bool ScribeWnd::GetScriptCallbacks(LScriptCallbackType Type, LArray &Callbacks) { for (auto s: d->Scripts) { for (auto &c: s->Callbacks) { if (c.Type == Type) Callbacks.Add(&c); } } return Callbacks.Length() > 0; } bool ScribeWnd::ExecuteScriptCallback(LScriptCallback &c, LScriptArguments &Args, bool ReturnArgs) { if (!c.Func || !c.Script) return false; // Setup LVirtualMachine Vm(d); Vm.SetDebuggerEnabled(true); d->CurrentScripts.Add(c.Script); // Call the method bool Status = Vm.ExecuteFunction( c.Script->Code, c.Func, Args, LScribeScript::Inst->GetLog(), ReturnArgs ? &Args : NULL) != ScriptError; // Cleanup d->CurrentScripts.PopLast(); return Status; } LStream *ScribeWnd::ShowScriptingConsole() { auto Item = Menu->FindItem(IDM_SCRIPTING_CONSOLE); if (Item) { Item->Checked(!Item->Checked()); LScribeScript::Inst->ShowScriptingWindow(Item->Checked()); LVariant v; GetOptions()->SetValue(OPT_ShowScriptConsole, v = Item->Checked()); } return LScribeScript::Inst->GetLog(); } LOptionsFile::PortableType ScribeWnd::GetPortableType() { return d->GetInstallMode(); } void ScribeWnd::RemoteContent_AddSender(const char *Addr, bool WhiteList) { if (!Addr) return; auto Opt = WhiteList ? OPT_RemoteContentWhiteList : OPT_RemoteContentBlackList; LVariant v; GetOptions()->GetValue(Opt, v); // Not an error if not there... auto existing = LString(v.Str()).SplitDelimit(" ,\r\n"); for (auto p: existing) { if (MatchStr(p, Addr)) { LgiTrace("%s:%i - '%s' is already in '%s'\n", _FL, Addr, Opt); return; // Already in list... } } existing.SetFixedLength(false); existing.Add(Addr); auto updated = LString("\n").Join(existing); GetOptions()->SetValue(Opt, v = updated.Get()); LgiTrace("%s:%i - Added '%s' to '%s'\n", _FL, Addr, Opt); d->RemoteContent_Init = false; } ScribeRemoteContent ScribeWnd::RemoteContent_GetSenderStatus(const char *Addr) { if (!d->RemoteContent_Init) { LVariant v; if (GetOptions()->GetValue(OPT_RemoteContentWhiteList, v)) d->RemoteWhiteLst = LString(v.Str()).SplitDelimit(" ,\r\n"); if (GetOptions()->GetValue(OPT_RemoteContentBlackList, v)) d->RemoteBlackLst = LString(v.Str()).SplitDelimit(" ,\r\n"); d->RemoteContent_Init = true; } for (auto p: d->RemoteWhiteLst) if (MatchStr(p, Addr)) return RemoteAlwaysLoad; for (auto p: d->RemoteBlackLst) if (MatchStr(p, Addr)) return RemoteNeverLoad; return RemoteDefault; } void ScribeWnd::RemoteContent_ClearCache() { d->RemoteWhiteLst.Empty(); d->RemoteBlackLst.Empty(); d->RemoteContent_Init = false; } void ScribeWnd::OnSpellerSettingChange() { // Kill the current thread d->SpellerThread.Reset(); // Setup the new thread LSpellCheck *t = GetSpellThread(); if (t) { // Trigger an install if needed t->Check(d->AppWndHnd, "thisisamispeltword", 0, 18); } } bool ScribeWnd::SetSpellThreadParams(LSpellCheck *Thread) { if (!Thread) return false; LVariant Lang, Dict; GetOptions()->GetValue(OPT_SpellCheckLanguage, Lang); GetOptions()->GetValue(OPT_SpellCheckDictionary, Dict); LAutoPtr Params(new LSpellCheck::Params); if (!Params) return false; Params->IsPortable = GetPortableType(); Params->OptionsPath = GetOptions()->GetFile(); Params->Lang = Lang.Str(); Params->Dict = Dict.Str(); Params->CapTarget = this; Thread->SetParams(Params); return true; } LSpellCheck *ScribeWnd::CreateSpellObject() { LVariant PrefAspell; GetOptions()->GetValue(OPT_PreferAspell, PrefAspell); LAutoPtr Obj; if (PrefAspell.CastInt32()) Obj = CreateAspellObject(); #if defined(MAC) if (!Obj) Obj = CreateAppleSpellCheck(); #elif defined(WINDOWS) LArray Ver; int Os = LGetOs(&Ver); if ( !Obj && (Os == LGI_OS_WIN32 || Os == LGI_OS_WIN64) && ( Ver.Length() > 1 && ( Ver[0] > 6 || (Ver[0] == 6 && Ver[1] > 1) ) ) ) Obj = CreateWindowsSpellCheck(); #endif if (!Obj) Obj = CreateAspellObject(); SetSpellThreadParams(Obj); return Obj.Release(); } LSpellCheck *ScribeWnd::GetSpellThread(bool OverrideOpt) { LVariant Use; if (OverrideOpt) Use = true; else GetOptions()->GetValue(OPT_SpellCheck, Use); #if USE_SPELLCHECKER if ((Use.CastInt32() != 0) ^ (d->SpellerThread.Get() != 0)) d->SpellerThread.Reset(Use.CastInt32() ? CreateSpellObject() : NULL); #endif return d->SpellerThread; } LAutoString ScribeWnd::GetHttpProxy() { LAutoString Proxy; LVariant v; if (GetOptions()->GetValue(OPT_HttpProxy, v) && ValidStr(v.Str())) { Proxy.Reset(v.ReleaseStr()); } else { LProxyUri p; if (p.sHost) Proxy.Reset(NewStr(p.ToString())); } return Proxy; } InstallProgress *ScribeWnd::StartAction(MissingCapsBar *Bar, LCapabilityTarget::CapsHash *Components, const char *ActionParam) { if (!ActionParam) { LgiTrace("%s:%i - No action supplied.\n", _FL); return NULL; } LArray Callbacks; LVariant Action(ActionParam); if (GetScriptCallbacks(LInstallComponent, Callbacks)) { bool StartInstall = true; for (unsigned i=0; iCastInt32()) StartInstall = false; } } if (!Action.Str()) { LgiTrace("%s:%i - GInstallComponent removed action name.\n", _FL); return NULL; } if (!StartInstall) { LgiTrace("%s:%i - GInstallComponent script canceled install of '%s'.\n", _FL, ActionParam); return NULL; } } if (!_stricmp(Action.Str(), LLoadString(IDS_OK))) { // Do nothing d->MissingCaps.Empty(); } else if (!_stricmp(Action.Str(), LLoadString(IDS_DONT_SHOW_AGAIN))) { // Turn off registering as a client. LVariant No(false); GetOptions()->SetValue(OPT_RegisterWindowsClient, No); GetOptions()->SetValue(OPT_CheckDefaultEmail, No); } else if (!_stricmp(Action.Str(), LLoadString(IDS_INSTALL))) { #ifdef WINDOWS bool IsSsl = false; for (auto c: *Components) { if (!_stricmp(c.key, "openssl")) { IsSsl = true; break; } } if (IsSsl) { LString s; s.Printf(LLoadString(IDS_WINDOWS_SSL_INSTALL), LGetOsName()); auto q = new LAlert(this, AppName, s, "Open Website", LLoadString(IDS_CANCEL)); q->DoModal([this, q](auto dlg, auto id) { switch (id) { case 1: LExecute("https://slproweb.com/products/Win32OpenSSL.html"); break; default: break; } delete dlg; }); return NULL; } #endif return CapabilityInstaller::StartAction(Bar, Components, Action.Str()); } else if (!_stricmp(Action.Str(), LLoadString(IDS_SHOW_CONSOLE))) { ShowScriptingConsole(); } else if (!_stricmp(Action.Str(), LLoadString(IDS_OPEN_SOURCE))) { if (d->ErrSource) LExecute(d->ErrSource); else if (d->ErrFilter) d->ErrFilter->DoUI(); d->ErrSource.Empty(); d->ErrFilter = NULL; } else if ( !Stricmp(Action.Str(), LLoadString(IDS_SHOW_REMOTE_CONTENT)) || !Stricmp(Action.Str(), LLoadString(IDS_ALWAYS_SHOW_REMOTE_CONTENT))) { auto c = Components->begin(); LWindow *w = Bar->GetWindow(); if ((*c).key && !Stricmp((*c).key, "RemoteContent") && w) { LScriptArguments Args(NULL); Args[0] = new LVariant(!Stricmp(Action.Str(), LLoadString(IDS_ALWAYS_SHOW_REMOTE_CONTENT))); w->CallMethod(DomToStr(SdShowRemoteContent), Args); } } else if (!_stricmp(Action.Str(), LLoadString(IDS_DOWNLOAD))) { auto t = GetSpellThread(); if (t) t->InstallDictionary(); else LgiTrace("%s:%i - No spell thread.\n", _FL); } else LAssert(!"Unknown action."); return NULL; } HttpImageThread *ScribeWnd::GetImageLoader() { if (!d->ImageLoader) { LAutoString Proxy = GetHttpProxy(); d->ImageLoader = new HttpImageThread(this, Proxy, 0); } return d->ImageLoader; } char *ScribeWnd::GetUiTags() { if (!d->UiTags) { char UiTags[256] = "inscribe" #if defined WIN32 " win32" #elif defined LINUX " linux" #elif defined MAC " mac" #endif ; LVariant Tags; if (!GetOptions()) { LAssert(!"Where is the options?"); } else if (GetOptions()->GetValue("tags", Tags)) { size_t Len = strlen(UiTags); sprintf_s(UiTags+Len, sizeof(UiTags)-Len, " %s", Tags.Str()); } d->UiTags.Reset(NewStr(UiTags)); } return d->UiTags; } void ScribeWnd::OnCreate() { // LgiTrace("ScribeWnd::OnCreate. ScribeState=%i\n", ScribeState); if (IsAttached() && ScribeState == ScribeConstructed) { ScribeState = ScribeInitializing; Construct3(); } } ScribeAccount *ScribeWnd::GetAccountByEmail(const char *Email) { if (!Email) return NULL; for (auto a : *GetAccounts()) { LVariant e = a->Identity.Email(); if (e.Str() && !_stricmp(e.Str(), Email)) { return a; } } return 0; } ScribeAccount *ScribeWnd::GetAccountById(int Id) { for (auto a : *GetAccounts()) { if (a->Receive.Id() == Id) { return a; } } return 0; } const char *ScribeWnd::EditCtrlMimeType() { LVariant Html; GetOptions()->GetValue(OPT_EditControl, Html); return Html.CastInt32() ? sTextHtml : sTextPlain; } LAutoString ScribeWnd::GetReplyXml(const char *MimeType) { bool IsHtml = MimeType && !_stricmp(MimeType, sTextHtml); LVariant s; GetOptions()->GetValue(IsHtml ? OPT_HtmlReplyFormat : OPT_TextReplyFormat, s); return LAutoString(s.ReleaseStr()); } LAutoString ScribeWnd::GetForwardXml(const char *MimeType) { bool IsHtml = MimeType && !_stricmp(MimeType, sTextHtml); LVariant s; GetOptions()->GetValue(IsHtml ? OPT_HtmlForwardFormat : OPT_TextForwardFormat, s); return LAutoString(s.ReleaseStr()); } LVmCallback *ScribeWnd::GetDebuggerCallback() { return d; } GpgConnector *ScribeWnd::GetGpgConnector() { if (!d->GpgInst) { if (!GpgConnector::IsInstalled()) return NULL; d->GpgInst.Reset(new GpgConnector()); } return d->GpgInst; } LFont *ScribeWnd::GetPreviewFont() { return d->PreviewFont; } bool ScribeWnd::IsValid() { #if 0 try { for (ScribeAccount *a = Accounts.First(); a; a = Accounts.Next()) { } } catch(...) { return false; } #endif return true; } bool ScribeWnd::ShutdownIpc() { // Remove our instance from the shared memory if (ScribeIpc && ScribeIpc->GetPtr()) { ScribeIpcInstance *InstLst = (ScribeIpcInstance*) ScribeIpc->GetPtr(); if (ThisInst) memset(ThisInst, 0, sizeof(*ThisInst)); int c = 0; for (int i=0; iDestroy(); } ThisInst = 0; DeleteObj(ScribeIpc); return true; } bool ScribeWnd::GetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { case SdQuote: // Type: String { return GetOptions()->GetValue(OPT_QuoteReplyStr, Value); } case SdName: // Type: String { Value = AppName; break; } case SdHome: // Type: String { Value = LGetExePath().Get(); break; } case SdNow: // Type: String { LDateTime Now; Now.SetNow(); char s[64]; Now.Get(s, sizeof(s)); Value = s; break; } case SdFolder: // Type: ScribeFolder[] // Pass system folder index or string as array parameter. { ScribeFolder *f = 0; if (!Array) return false; if (IsDigit(*Array)) { f = GetFolder(atoi(Array)); } else { f = GetFolder(Array); } if (!f) return false; Value = (LDom*)f; break; } case SdAppName: // Type: String { Value = AppName; break; } case SdCalendarToday: // Type: String { Calendar::SummaryOfToday(this, [this](auto s) { d->CalendarSummary = s; }); return d->CalendarSummary; } case SdInboxSummary: { LStringPipe p; // Iterate through the mail stores for (auto m: Folders) { if (!m.Store) continue; auto Inbox = m.Store->GetObj(FIELD_INBOX); if (!Inbox) continue; auto Unread = Inbox->GetInt(FIELD_UNREAD); auto Name = Inbox->GetStr(FIELD_FOLDER_NAME); LString Path; Path.Printf("/%s/%s", m.Name.Get(), Name); if (Unread) p.Print("%s: %i unread
", m.Name.Get(), Path.Get(), Unread); else p.Print("%s: 0 unread
", m.Name.Get()); } // And also the IMAP full folders for (auto a: Accounts) { ScribeProtocol Protocol = a->Receive.ProtocolType(); if (Protocol == ProtocolImapFull) { LDataStoreI *Store = a->Receive.GetDataStore(); if (Store) { auto Inbox = Store->GetObj(FIELD_INBOX); if (Inbox) { auto Unread = Inbox->GetInt(FIELD_UNREAD); auto Name = Inbox->GetStr(FIELD_FOLDER_NAME); auto m = a->Receive.Name(); if (m.Str()) { LString Path; Path.Printf("/%s/%s", m.Str(), Name); if (Unread) p.Print("%s: %i unread
", m.Str(), Path.Get(), Unread); else p.Print("%s: 0 unread
", m.Str()); } } } } } Value = p.NewLStr().Get(); break; } case SdExecute: // Type: String { if (!Array) return false; const char *s = Array; char *Exe = LTokStr(s); if (!Exe) return false; while (*s && *s == ' ') s++; LStringPipe Out; LSubProcess p(Exe, (char*)s); if (p.Start()) { p.Communicate(&Out); LAutoString o(Out.NewStr()); LAutoString t(TrimStr(o)); Value = t; } DeleteArray(Exe); break; } case SdBuildType: // Type: String { #ifdef _DEBUG Value = "Debug"; #else Value = "Release"; #endif break; } case SdPlatform: // Type: String { LArray Ver; LGetOs(&Ver); LString::Array Va; for (auto i: Ver) Va.New().Printf("%i", i); #if defined __GTK_H__ auto Api = "GTK3"; #elif LGI_SDL auto Api = "SDL"; #elif LGI_COCOA auto Api = "Cocoa"; #elif LGI_CARBON auto Api = "Carbon"; #elif defined WIN32 auto Api = "WinApi"; #else #error "Impl me." auto Api = "#err"; #endif LString s; s.Printf("%s, v%s, %s", LGetOsName(), LString(".").Join(Va).Get(), Api); Value = s.Get(); break; } case SdVersion: // Type: String { char Ver[32]; sprintf_s(Ver, sizeof(Ver), "v%s", ScribeVer); Value = Ver; break; } case SdBuild: // Type: String { char s[128]; sprintf_s(s, sizeof(s), "%s, %s, %ibit", __DATE__, __TIME__, (int)(sizeof(NativeInt)*8)); Value = s; break; } case SdLanguage: // Type: String { LLanguage *l = LGetLanguageId(); if (!l) return false; char s[256]; sprintf_s(s, sizeof(s), "%s \"%s\"", l->Name, l->Id); Value = s; break; } case SdString: // Type: String { if (!Array) { LAssert(!"Missing string ID"); return false; } int Id = atoi(Array); Value = LLoadString(Id); break; } case SdCurrentFolder: // Type: ScribeFolder { Value = (LDom*) GetCurrentFolder(); break; } case SdView: // Type: LView { Value = (LView*)this; break; } case SdNoContact: // Type: Contact { Value = (NoContactType*)d->NoContact; break; } case SdAccounts: { if (Array) { if (IsDigit(*Array)) { auto i = atoi(Array); if (i >= 0 && i < (ssize_t)Accounts.Length()) { Value = (LDom*)Accounts[i]; } else return false; } else { for (auto a : Accounts) { LVariant nm = a->Send.Name(); if (nm.Str() && !_stricmp(nm.Str(), Array)) { Value = (LDom*)a; break; } } } } else { Value = (int32)Accounts.Length(); } break; } case SdOptions: { Value = GetOptions(); break; } case SdMailStorePaths: { if (!Value.SetList()) return false; for (auto Ms : Folders) Value.Add(new LVariant(Ms.Path)); break; } case SdRootFolders: { if (!Value.SetList() || !Tree) return false; for (auto *i = Tree->GetChild(); i; i = i->GetNext()) { ScribeFolder *c = dynamic_cast(i); if (c) { auto p = c->GetPath(); Value.Add(new LVariant(p)); } } break; } default: { return false; } } return true; } bool ScribeWnd::CallMethod(const char *MethodName, LScriptArguments &Args) { ScribeDomType m = StrToDom(MethodName); switch (m) { case SdGrowlOnMail: // Type: (Mail Obj) { if (Args.Length() != 1) { LgiTrace("%s:%i - Wrong arg count: %i.\n", _FL, (int)Args.Length()); return false; } Mail *m = dynamic_cast(Args[0]->CastDom()); if (!m) { LgiTrace("%s:%i - Invalid object.\n", _FL); return false; } GrowlOnMail(m); break; } case SdGrowlInfo: { auto Title = Args.Length() > 0 ? Args[0] : NULL; auto Text = Args.Length() > 1 ? Args[1] : NULL; GrowlInfo(Title ? Title->Str() : NULL, Text ? Text->Str() : NULL); break; } case SdGetClipboardText: // Type: () { LClipBoard c(this); *Args.GetReturn() = c.Text(); break; } case SdSetClipboardText: // Type: (String Text) { if (Args.Length() != 1) { LgiTrace("%s:%i - Wrong arg count: %i.\n", _FL, (int)Args.Length()); return false; } char *Str = Args[0]->CastString(); LClipBoard c(this); if (ValidStr(Str)) *Args.GetReturn() = c.Text(Str); else *Args.GetReturn() = c.Empty(); break; } case SdLookupContactGroup: // Type: (String GroupName) { if (Args.Length() != 1) { LgiTrace("%s:%i - Wrong arg count: %i.\n", _FL, (int)Args.Length()); return false; } ContactGroup *Grp = LookupContactGroup(this, Args[0]->Str()); *Args.GetReturn() = dynamic_cast(Grp); break; } case SdAskUserString: // Type: (LView ParentView, String Callback, String PromptMessage[, Bool ObsurePassword[, String DefaultValue]]) { auto Vm = dynamic_cast(Args.GetVm()); if (!Vm) return false; LVirtualMachine::Context Ctx = Vm->SaveContext(); if (!Ctx || Args.Length() < 3) { *Args.GetReturn() = false; return true; } LView *View = Args[0]->CastView(); auto CallbackName = Args[1]->Str(); auto Prompt = Args[2]->CastString(); bool IsPassword = Args.Length() > 3 ? Args[3]->CastInt32() != 0 : false; auto Default = Args.Length() > 4 ? Args[4]->Str() : NULL; auto i = new LInput(View ? View : this, Default, Prompt, AppName, IsPassword); i->DoModal([Ctx, i, CallbackName=LString(CallbackName)](auto dlg, auto id) { if (id) { LScriptArguments Args(NULL); Args.Add(new LVariant(i->GetStr())); Ctx.Call(CallbackName, Args); Args.DeleteObjects(); } delete dlg; }); *Args.GetReturn() = true; break; } case SdCreateAccount: // Type: () { ScribeAccount *a = new ScribeAccount(this, (int)Accounts.Length()); if (a) { *Args.GetReturn() = (LDom*)a; Accounts.Insert(a); a->Create(); } else return false; break; } case SdDeleteAccount: // Type: (ScribeAccount AccountToDelete) { if (Args.Length() != 1) return false; ScribeAccount *a = dynamic_cast(Args[0]->CastDom()); if (!a) { *Args.GetReturn() = false; } else { int Idx = (int)Accounts.IndexOf(a); if (Idx < 0 || a->IsOnline()) { *Args.GetReturn() = false; } else { Accounts.Delete(a); a->Delete(); delete a; // delete actual account object // Reindex remaining items so their are no gaps int i=0; auto it = Accounts.begin(); for (a = *it; a; a = *++it, i++) { a->ReIndex(i); } } } break; } case SdShowRemoteContent: { if (PreviewPanel) return PreviewPanel->CallMethod(MethodName, Args); else return false; break; } case SdSearchHtml: // Type(Html, SearchExp, ResultExp) { if (Args.Length() != 3) { LgiTrace("%s:%i - SearchHtml requires 3 arguments.\n", _FL); *Args.GetReturn() = false; return true; } auto Html = Args[0]->Str(); auto SearchExp = Args[1]->Str(); auto ResultExp = Args[2]->Str(); if (!Html || !SearchExp || !ResultExp) { LgiTrace("%s:%i - SearchHtml got non-string argument.\n", _FL); *Args.GetReturn() = false; return true; } SearchHtml(Args.GetReturn(), Html, SearchExp, ResultExp); return true; } case SdGetUri: // Type(UriToDownload, CallbackName) { if (Args.Length() < 2) { LgiTrace("%s:%i - GetUri requires at least 2 arguments.\n", _FL); *Args.GetReturn() = false; return true; } auto Uri = Args[0]->Str(); auto Callback = Args[1]->Str(); LVariant *UserData = Args.Length() > 2 ? Args[2] : NULL; new ScriptDownloadContentThread(this, Uri, Callback, UserData); *Args.GetReturn() = true; return true; } default: { LAssert(!"Unsupported method."); return false; } } return true; } LOptionsFile *ScribeWnd::GetOptions(bool Create) { if (!d->Options && Create) { LAssert(!"Not here... do it in LoadOptions."); return NULL; } return d->Options; } int OptionsFileCmp(OptionsInfo *a, OptionsInfo *b) { int64 Diff = b->Mod - a->Mod; return Diff < 0 ? -1 : (Diff > 0 ? 1 : 0); } OptionsInfo::OptionsInfo() { Score = 0; Mod = 0; Leaf = NULL; Usual = false; } OptionsInfo &OptionsInfo::operator =(char *p) { File = p; Leaf = LGetLeaf(File); if (Leaf) { char n[64]; sprintf_s(n, sizeof(n), "%s.xml", OptionsFileName); Usual = !_stricmp(n, Leaf); } return *this; } LAutoPtr OptionsInfo::Load() { // Read the file... size_t Count = 0; LXmlTag *Stores = NULL; LXmlTag *Acc = NULL; LAutoPtr Of(new LOptionsFile(File)); if (!Of) return Of; if (!Of->SerializeFile(false)) goto OnError; // Sanity check the options... Acc = Of->LockTag(OPT_Accounts, _FL); if (!Acc) goto OnError; Of->Unlock(); Stores = Of->LockTag(OPT_MailStores, _FL); if (!Stores) goto OnError; Count = Stores->Children.Length(); Of->Unlock(); if (Count == 0) goto OnError; return Of; OnError: Of.Reset(); return Of; } #define DEBUG_OPTS_SCAN 0 bool ScribeWnd::ScanForOptionsFiles(LArray &Files, LSystemPath PathType) { LString Root = LGetSystemPath(PathType); LDirectory Dir; char p[MAX_PATH_LEN]; if (IsUnitTest) Root += ".unittest"; #if DEBUG_OPTS_SCAN LgiTrace("%s:%i - Root='%s'\n", _FL, Root.Get()); #endif for (int b = Dir.First(Root); b; b = Dir.Next()) { if ( !Dir.IsDir() && Dir.Path(p, sizeof(p)) ) { LResolveShortcut(p, p, sizeof(p)); char *Ext = LGetExtension(Dir.GetName()); if (stristr(Dir.GetName(), OptionsFileName) != NULL && Ext && (!_stricmp(Ext, "xml") || !_stricmp(Ext, "bak"))) { OptionsInfo &i = Files.New(); i = p; i.Mod = Dir.GetLastWriteTime(); #if DEBUG_OPTS_SCAN LgiTrace("%s:%i - File='%s'\n", _FL, p); #endif } } } Files.Sort(OptionsFileCmp); // Scan through the results and pick out the normal file for (unsigned i=0; iOptions; i++) { if (Files[i].Usual) { d->Options = Files[i].Load(); #if DEBUG_OPTS_SCAN LgiTrace("%s:%i - Attempt '%s' = %p\n", _FL, Files[i].File.Get(), d->Options); #endif } } if (!d->Options) { // Scan through the alternative files and look for something // we can use. #if DEBUG_OPTS_SCAN LgiTrace("%s:%i - Scanning backups\n", _FL); #endif for (unsigned i=0; iOptions; i++) { if (!Files[i].Usual) { d->Options = Files[i].Load(); if (d->Options) { // Lets rename this baby back to the real filename LString Xml = OptionsFileName; Xml += ".xml"; LFile::Path Normal(Root, Xml); if (LFileExists(Normal)) FileDev->Delete(Normal); d->Options->SetFile(Normal); d->Options->SerializeFile(true); // sets to clean after changing filename. } } } } if (d->Options) { // Load OK: Clear out any old options files... #if DEBUG_OPTS_SCAN LgiTrace("%s:%i - Files.len=" LPrintfSizeT "\n", _FL, Files.Length()); #endif while (Files.Length() > 6) { auto Idx = Files.Length() - 1; auto &f = Files[Idx]; #if DEBUG_OPTS_SCAN LgiTrace("%s:%i - Delete '%s'\n", _FL, f.File.Get()); #endif FileDev->Delete(f.File); Files.DeleteAt(Idx); } } return d->Options != NULL; } bool ScribeWnd::IsUnitTest = false; bool ScribeWnd::LoadOptions() { bool Load = false; // Check if we are running unit tests... if ((IsUnitTest = LAppInst->GetOption("unittest"))) { d->UnitTestServer.Reset(new LUnitTestServer(this)); } // Look in the XGate folder #if WINNATIVE && !defined(_DEBUG) LRegKey xgate(false, "HKEY_CURRENT_USER\\Software\\XGate"); if (xgate.IsOk()) { char *Spool = xgate.GetStr("SpoolDir"); if (LDirExists(Spool)) { char File[MAX_PATH_LEN]; LMakePath(File, sizeof(File), Spool, "spool"); LMakePath(File, sizeof(File), File, OptionsFileName); strcat_s(File, sizeof(File), ".xml"); if (LFileExists(File)) { d->SetInstallMode(LOptionsFile::DesktopMode); LgiTrace("Selecting xgate mode based on options file path.\n"); d->Options.Reset(new LOptionsFile(File)); } } } #endif // Now look in the application install folder LArray Files; if (!d->Options && ScanForOptionsFiles(Files, LSP_APP_INSTALL)) { // File is in the install folder... d->SetInstallMode(LOptionsFile::PortableMode); LgiTrace("Selecting portable mode based on options file path.\n"); } // Look in the app root if ( !d->Options && ScanForOptionsFiles(Files, LSP_APP_ROOT) ) { // Desktop mode d->SetInstallMode(LOptionsFile::DesktopMode); LgiTrace("Selecting desktop mode based on options file path.\n"); } // Do multi-instance stuff if (d->Options && d->Options->GetFile()) { // Search for other instances of Scribe if (ScribeIpc) { int i; ScribeIpcInstance *InstLst = (ScribeIpcInstance*) ScribeIpc->GetPtr(); ScribeIpcInstance *Mul = 0; for (i=0; iMulPassword = Mul->Password; break; } } for (i=0; iIsScribe(), InstLst->Magic, InstLst->Pid); if (InstLst->IsScribe() && InstLst != ThisInst) { // LgiTrace("%s, %s\n", InstLst->OptionsPath, d->Options->GetFile()); if (Mul || _stricmp(InstLst->OptionsPath, d->Options->GetFile()) == 0) { int Pid = InstLst->Pid; OsAppArguments *Args = LAppInst->GetAppArgs(); if (!LIsProcess(Pid)) { continue; } char *Utf8 = 0; #if WINNATIVE Utf8 = WideToUtf8(Args->lpCmdLine); #else LStringPipe p; for (int i=1; iArgs; i++) { if (i > 1) p.Push(" "); char *Sp = strchr(Args->Arg[i], ' '); if (Sp) p.Push("\""); p.Push(Args->Arg[i]); if (Sp) p.Push("\""); } Utf8 = p.NewStr(); #endif if (Utf8) { size_t Len = strlen(Utf8); if (Len > 255) { InstLst->Flags |= SCRIBE_IPC_LONG_ARGS; InstLst->Flags &= ~SCRIBE_IPC_CONTINUE_ARGS; for (char *u = Utf8; Len > 0; u += 255) { ssize_t Part = MIN(sizeof(InstLst->Args)-1, Len); memcpy(InstLst->Args, u, Part); Len -= Part; int64 Start = LCurrentTime(); while (LCurrentTime() - Start < 60000) { if (TestFlag(InstLst->Flags, SCRIBE_IPC_CONTINUE_ARGS) && InstLst->Args[0] == 0) { Start = 0; break; } LSleep(10); } if (Start) { LgiTrace("%s:%i - SendLA timed out.\n", _FL); break; } } InstLst->Flags &= ~(SCRIBE_IPC_CONTINUE_ARGS | SCRIBE_IPC_LONG_ARGS); } else { strcpy_s(InstLst->Args, sizeof(InstLst->Args), Utf8); } DeleteArray(Utf8); ShutdownIpc(); LgiTrace("Passed args to the other running instance of Scribe (pid=%i)\n", Pid); LCloseApp(); return false; } else LgiTrace("%s:%i - No arguments to pass.\n", _FL); } } } if (Mul && LFileExists(Mul->OptionsPath)) { // No instance of Scribe is running, but MUL may be keeping // the previously run instance around. So we should run that // by using the options file and password from MUL's instance // record. d->Options.Reset(new LOptionsFile(Mul->OptionsPath)); } } // Insert ourselves into the instance list if (ThisInst) { strcpy_s(ThisInst->OptionsPath, sizeof(ThisInst->OptionsPath), d->Options->GetFile()); } } // Open file and load.. if (!Load && d->Options) { LOptionsFile *Opts = GetOptions(); Load = Opts->SerializeFile(false); if (Load) { LVariant v = d->GetInstallMode() == LOptionsFile::PortableMode; GetOptions()->SetValue(OPT_IsPortableInstall, v); } else { auto err = GetOptions()->GetError(); LgiMsg( this, LLoadString(IDS_ERROR_LR8_FAILURE), AppName, MB_OK, err); } } if (!d->Options) { // d->Options = new LOptionsFile(d->GetInstallMode(), OptionsFileName); return false; } if (d->Options) { LVariant v; if (!d->Options->GetValue(OPT_IsPortableInstall, v) && d->GetInstallMode() != LOptionsFile::UnknownMode) { v = d->GetInstallMode() == LOptionsFile::PortableMode; d->Options->SetValue(OPT_IsPortableInstall, v); } ScribeOptionsDefaults(d->Options); if (Load) { if (GetOptions()->GetValue(OPT_PrintSettings, v)) { auto *p = GetPrinter(); if (p) { LString s = v.Str(); p->Serialize(s, false); } } } if (d->Options->GetValue(OPT_PreviewLines, v)) { Mail::PreviewLines = v.CastInt32() != 0; } // upgrade smtp password const char *Pw = "SmtpPsw"; if (!GetOptions()->GetValue(OPT_EncryptedSmtpPassword, v)) { // no encrypted password, look for unencrypted password if (GetOptions()->GetValue(Pw, v)) { LPassword p; p.Set(v.Str()); p.Serialize(GetOptions(), OPT_EncryptedSmtpPassword, true); } } // if old un-encrypted password exists... // delete the key, we are now storing an encrypted // password if (GetOptions()->GetValue(Pw, v)) GetOptions()->DeleteValue(Pw); if (GetOptions()->GetValue(OPT_AdjustDateTz, v)) Mail::AdjustDateTz = !v.CastInt32(); if (!GetOptions()->GetValue(OPT_ConfirmDelete, v)) GetOptions()->SetValue(OPT_ConfirmDelete, v = true); if (!GetOptions()->GetValue(OPT_DelDirection, v)) GetOptions()->SetValue(OPT_DelDirection, v = DeleteActionPrev); if (GetOptions()->GetValue(OPT_SizeInKiB, v)) OptionSizeInKiB = v.CastInt32() != 0; if (GetOptions()->GetValue(OPT_RelativeDates, v)) ShowRelativeDates = v.CastInt32() != 0; // date format if (GetOptions()->GetValue(OPT_DateFormat, v)) { int Idx = v.CastInt32(); if (Idx >= 0 && Idx < CountOf(DateTimeFormats)) LDateTime::SetDefaultFormat(DateTimeFormats[Idx]); } // SSL debug logging if (GetOptions()->GetValue(OPT_DebugSSL, v)) SslSocket::DebugLogging = v.CastInt32() != 0; // Growl if (GetOptions()->GetValue(OPT_GrowlEnabled, v) && v.CastInt32()) { LVariant Ver, Bld; GetVariant(DomToStr(SdVersion), Ver); GetVariant("Build", Bld); LString n; n.Printf("%s\n%s", Ver.Str(), Bld.Str()); GrowlInfo("Scribe has started up...", n); } } #if LGI_EXCEPTIONS try { #endif // Default the font settings to the system font // if they don't already exist const char *OptFont[] = { OPT_EditorFont, OPT_PrintFont, OPT_HtmlFont, 0 }; int Index = 0; for (const char **Opt=OptFont; *Opt; Opt++, Index++) { LVariant v; if (!GetOptions()->GetValue(*Opt, v)) { LFontType Type; if (Type.GetSystemFont("System")) { if (Index == 2) { int Pt = Type.GetPointSize(); Type.SetPointSize(Pt+3); } Type.Serialize(GetOptions(), *Opt, true); } } } #if LGI_EXCEPTIONS } catch (...) { LgiMsg( this, LLoadString(IDS_ERROR_FONT_SETTINGS), AppName, MB_OK); } #endif return true; } bool ScribeWnd::SaveOptions() { LStringPipe Log(256); bool Status = false; bool WriteFailed = false; bool WndStateSet = false; RestartSave: if (d->Options && !d->Options->GetFile()) { bool PortableOk = true; char Path[MAX_PATH_LEN]; char Leaf[32]; sprintf_s(Leaf, sizeof(Leaf), "%s.xml", OptionsFileName); Log.Print("No current path for '%s', creating...\n", Leaf); LVariant v; GetOptions()->GetValue(OPT_IsPortableInstall, v); if (v.CastInt32()) { if (!LGetSystemPath(LSP_APP_INSTALL, Path, sizeof(Path))) { PortableOk = false; Log.Print("Error: LgiGetSystemPath(LSP_APP_INSTALL) failed.\n"); } else { LMakePath(Path, sizeof(Path), Path, Leaf); // Do write test to confirm we are good to go LFile f; if (f.Open(Path, O_WRITE)) { f.Close(); FileDev->Delete(Path, false); d->Options->SetFile(Path); } else { PortableOk = false; Log.Print("Warning: '%s' is not writable.\n", Path); } } } if (!v.CastInt32() || !PortableOk) { // Desktop mode then. if (v.CastInt32()) { const char *Msg = "Switching to desktop mode because the install folder is not writable."; Log.Print("%s\n", Msg); LgiMsg(this, Msg, AppName, MB_OK); GetOptions()->SetValue(OPT_IsPortableInstall, v = false); } if (!LGetSystemPath(LSP_APP_ROOT, Path, sizeof(Path))) { Log.Print("Error: LgiGetSystemPath(LSP_APP_ROOT) failed.\n"); } else { LMakePath(Path, sizeof(Path), Path, LAppInst->LBase::Name()); if (!LDirExists(Path)) { if (!FileDev->CreateFolder(Path)) { Log.Print("Error: CreateFolder('%s') failed.\n", Path); } } LMakePath(Path, sizeof(Path), Path, Leaf); // Do write test to confirm we are good to go LFile f; if (f.Open(Path, O_WRITE)) { f.Close(); FileDev->Delete(Path, false); d->Options->SetFile(Path); } else { Log.Print("Error: '%s' is not writable.\n", Path); } } } } if (d->Options && d->Options->GetFile() && d->Options->IsValid()) { // Backup options file char Backup[MAX_PATH_LEN]; strcpy_s(Backup, sizeof(Backup), d->Options->GetFile()); char *Ext = LGetExtension(Backup); if (Ext) { *--Ext = 0; LString s; for (int i=1; i<100; i++) { s.Printf("%s_%i.bak", Backup, i); if (!LFileExists(s)) break; } if (!LFileExists(s)) FileDev->Move(d->Options->GetFile(), s); } // Update some settings... #if LGI_VIEW_HANDLE if (Handle()) #endif WndStateSet = SerializeState(GetOptions(), OPT_ScribeWndPos, false); LVariant v; if (Splitter) GetOptions()->SetValue(OPT_SplitterPos, v = (int)Splitter->Value()); if (d->SubSplit) { auto First = d->SubSplit->GetViewAt(0); if (First == (LViewI*)SearchView) { auto Lst = (SearchView) ? d->SubSplit->GetViewAt(1) : NULL; if (Lst) GetOptions()->SetValue(OPT_SubSplitPos, v = (int)Lst->GetPos().Y()); } else GetOptions()->SetValue(OPT_SubSplitPos, v = (int)d->SubSplit->Value()); } // Write them... if (GetOptions()->SerializeFile(true)) { Status = true; } else { // We probably don't have write permissions to the install folder... Log.Print("Error: Options.Serialize failed.\n"); if (!WriteFailed) { // This blocks any possibility of an infinite loop WriteFailed = true; d->Options->SetFile(NULL); // Set desktop mode explicitly LVariant v; GetOptions()->GetValue(OPT_IsPortableInstall, v = false); Log.Print("Restarting save after setting desktop mode...\n"); goto RestartSave; } } } if (!Status) { LString a = Log.NewLStr(); LgiMsg(this, "Saving options failed:\n%s", AppName, MB_OK, a.Get()); } if (!WndStateSet) { LRect r(10, 10, 790, 590); SetPos(r); MoveToCenter(); } return Status; } // // Command Line Options: // // -m, -t : To recipient(s) // -f : The filename of the attachment // -b : Attach as a binary // -c : CC'd recipient(s) // -s : Subject for the email // -n : Send now... else UI is shown // -p : Print the file // -upgrade_folders : trigger a folder upgrade // -o : Load the following options file // -u : Load the following URL/file // void ScribeWnd::OnCommandLine() { // check command line args LString Str, File; bool CreateMail = false; CreateMail = LAppInst->GetOption("m", Str); if (!CreateMail) CreateMail = LAppInst->GetOption("t", Str); bool HasFile = LAppInst->GetOption("f", File); if (!CreateMail) CreateMail = HasFile; LString OpenArg; if (LAppInst->GetOption("u", OpenArg)) { LUri u(OpenArg); if (u.sProtocol) { OnUrl(OpenArg); } else if (LFileExists(OpenArg)) { LArray Files; Files.Add(OpenArg); OnReceiveFiles(Files); } } Mail *NewEmail = 0; if (CreateMail && Str) { // strip off quotes if needed char *In = Str, *Out = Str; for (; In && *In; In++) { if (!strchr("\'\"", *In)) { *Out++ = *In; } } *Out++ = 0; // create object NewEmail = dynamic_cast(CreateItem(MAGIC_MAIL, NULL, false)); if (NewEmail) { Mailto mt(this, Str); mt.Apply(NewEmail); // cc's? if (LAppInst->GetOption("c", Str)) { SetRecipients(this, Str, NewEmail->GetObject()->GetList(FIELD_TO), MAIL_ADDR_CC); } // attach a file? if (File) { if (LAppInst->GetOption("b")) { // attach as a binary file NewEmail->AttachFile(this, &File[0]); } else { // insert as the body LAutoString b(LReadTextFile(&File[0])); if (b) { NewEmail->SetBody(b); } } } // subject? if (LAppInst->GetOption("s", Str)) { NewEmail->SetSubject(Str); } // Send now or later? if (LAppInst->GetOption("n")) { // Check for exit after send option d->ExitAfterSend = LAppInst->GetOption("exit"); // now NewEmail->SetFlags(MAIL_CREATED | MAIL_READY_TO_SEND | NewEmail->GetFlags()); NewEmail->Save(); OnCommand(IDM_SEND_MAIL, 0, #ifndef __GTK_H__ Handle() #else NULL #endif ); } else { // later NewEmail->DoUI(); } } } // Pop3 on startup option LVariant n; if (GetOptions()->GetValue(OPT_Pop3OnStart, n) && n.CastInt32()) { OnCommand(IDM_RECEIVE_MAIL, 0, NULL); } } void ScribeWnd::SetCurrentIdentity(int i) { LVariant v = i; GetOptions()->SetValue(OPT_CurrentIdentity, v); if (DefaultIdentityItem) DefaultIdentityItem->Checked(i < 0); for (auto a: Accounts) { a->SetCheck(i == a->GetIndex()); } } ScribeAccount *ScribeWnd::GetCurrentAccount() { auto Idx = GetCurrentIdentity(); ScribeAccount *a = (Idx >= 0 && Idx < (ssize_t)Accounts.Length()) ? Accounts.ItemAt(Idx) : NULL; bool ValidId = a != NULL && a->IsValid(); if (!ValidId) { LAssert(!"No current identity?"); // Find a valid account to be the identity... for (auto a : Accounts) { if (!a->Send.Disabled() && a->Identity.IsValid()) { break; } } } return a; } int ScribeWnd::GetCurrentIdentity() { LVariant i; if (GetOptions()->GetValue(OPT_CurrentIdentity, i)) return i.CastInt32(); else if (ScribeState != ScribeInitializing) LgiTrace("%s:%i - No OPT_CurrentIdentity set.\n", _FL); return -1; } void ScribeWnd::SetupAccounts() { int i, CurrentIdentity = GetCurrentIdentity(); if (StatusPanel) { StatusPanel->Empty(); } #if !defined(COCOA) // FIXME LAssert(ReceiveMenu && PreviewMenu); #endif if (SendMenu) SendMenu->Empty(); if (ReceiveMenu) ReceiveMenu->Empty(); if (PreviewMenu) PreviewMenu->Empty(); if (IdentityMenu) { IdentityMenu->Empty(); } static bool Startup = true; bool ResetDefault = false; LArray Enabled; for (i=0; true; i++) { // char *s = 0; ScribeAccount *a = Startup ? new ScribeAccount(this, i) : Accounts[i]; if (a) { if (i == 0) { a->Create(); } a->Register(this); LVariant ReceiveName = a->Receive.Name(); LVariant ReceiveServer = a->Receive.Server(); LVariant SendServer = a->Send.Server(); if (i == 0 || ValidStr(ReceiveName.Str()) || ValidStr(ReceiveServer.Str()) || ValidStr(SendServer.Str()) ) { a->Send.SendItem = SendItem; a->Receive.ReceiveItem = ReceiveItem; a->Receive.PreviewItem = PreviewItem; if (!Accounts.HasItem(a)) { Accounts.Insert(a); } if (i) a->Create(); a->InitMenus(); // Identity Menu Item LVariant IdEmail = a->Identity.Email(); LVariant IdName = a->Identity.Name(); if (IdentityMenu && ValidStr(IdEmail.Str())) { char s[256]; if (IdName.Str()) sprintf_s(s, sizeof(s), "%s <%s>", IdName.Str(), IdEmail.Str()); else sprintf_s(s, sizeof(s), "<%s>", IdEmail.Str()); a->SetMenuItem(IdentityMenu->AppendItem(s, IDM_IDENTITY_BASE+i+1, !a->Send.Disabled())); if (a->Send.Disabled()) { a->SetCheck(false); if (i == CurrentIdentity) ResetDefault = true; } else { a->SetCheck(i == CurrentIdentity); Enabled[i] = a; } } } else { Accounts.Delete(a); DeleteObj(a); } } if (!a) break; } if ((ResetDefault || CurrentIdentity < 0) && Enabled.Length()) { for (unsigned i=0; iSetCheck(true); LVariant v; GetOptions()->SetValue(OPT_CurrentIdentity, v = (int)i); break; } } } Startup = false; if (ReceiveMenu && i == 0) { ReceiveMenu->AppendItem(LLoadString(IDS_NO_ITEMS), 0, false); } if (StatusPanel) { StatusPanel->OnAccountListChange(); } SetPulse(100); } ////////////////////////////////////////////////////////////////////////////// class LShutdown : public LDialog { LTableLayout *Tbl = NULL; LTextLabel *Msg = NULL; LButton *ActionBtn = NULL; LButton *CancelBtn = NULL; bool Disconnected = false; uint64_t WaitTs = 0; public: ScribeAccount *Waiting = NULL; LArray Accounts; LShutdown(LArray accounts) { Accounts = accounts; LRect r( 0, 0, 320 + LAppInst->GetMetric(LGI_MET_DECOR_X), 70 + LAppInst->GetMetric(LGI_MET_DECOR_Y)); SetPos(r); MoveToCenter(); LString Str; Str.Printf("%s %s", AppName, LLoadString(IDS_EXITING)); LView::Name(Str); AddView(Tbl = new LTableLayout(IDC_TABLE)); auto c = Tbl->GetCell(0, 0); c->Add(Msg = new LTextLabel(-1, 10, 10, 300, -1, LLoadString(IDS_NONE))); c = Tbl->GetCell(0, 1); c->TextAlign(LCss::AlignCenter); c->Width("100%"); c->Add(ActionBtn = new LButton(IDC_KILL, 0, 0, -1, -1, LLoadString(IDS_DISCONNECT))); c->Add(CancelBtn = new LButton(IDCANCEL, 0, 0, -1, -1, LLoadString(IDS_CANCEL))); if (ActionBtn) ActionBtn->Enabled(false); } ~LShutdown() { int asd=0; } void OnCreate() { SetPulse(100); } void OnPulse() { if (Accounts.Length()) { LArray Remove; for (auto a: Accounts) { if (!a->IsOnline()) { Remove.Add(a); if (a == Waiting) Waiting = NULL; } else if (!Waiting) { Waiting = a; LString s; LVariant v = a->Receive.Name(); s.Printf(LLoadString(IDS_WAITING_FOR), v.Str() ? v.Str() : LLoadString(IDS_NONE)); Msg->Name(s); WaitTs = LCurrentTime(); Disconnected = false; ActionBtn->Name(LLoadString(IDS_DISCONNECT)); ActionBtn->Enabled(true); } } for (auto r: Remove) Accounts.Delete(r); } else { SetPulse(); EndModal(false); } } int OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDC_KILL: { if (Waiting) { WaitTs = LCurrentTime(); if (!Disconnected) { Disconnected = true; ActionBtn->Name(LLoadString(IDS_KILL)); Waiting->Disconnect(); } else { Waiting->Kill(); ActionBtn->Enabled(false); } } else { ActionBtn->Enabled(false); } break; } case IDCANCEL: { EndModal(false); break; } } return 0; } }; bool ScribeWnd::OnRequestClose(bool OsShuttingDown) { if (FolderTasks.Length() > 0) { LgiTrace("%s:%i - %i folder tasks still busy...\n", _FL, FolderTasks.Length()); return false; } LString OnClose = LAppInst->GetConfig("Scribe.OnClose"); if (!d->IngoreOnClose && !OsShuttingDown && !Stricmp(OnClose.Get(), "minimize")) { SetZoom(LZoomMin); return false; } Visible(false); if (ScribeState != ScribeRunning) { // Inside a folder load/unload or initialization // Tell the loader to quit out... ScribeState = ScribeExiting; // Leave now, we can exit when we're ready return false; } else if (IsSending() || GetActiveThreads() > 0) { // whack up a shutdown window LArray Online; for (auto i: Accounts) { i->OnEndSession(); if (i->IsOnline()) Online.Add(i); } LAssert(Online.Length() > 0); auto Dlg = new LShutdown(Online); Dlg->DoModal([this](auto dlg, auto id) { if (id) { ScribeState = ScribeExiting; LCloseApp(); } else { ScribeState = ScribeRunning; Visible(true); } delete dlg; }); return false; // At the very minimum the app has to wait for the user to respond. } else { // End all sessions if any... for (auto i: Accounts) { i->OnEndSession(); } } // close all the other top level windows while (ThingUi::All.Length() > 0) { ThingUi *Ui = ThingUi::All.First(); if (!Ui->OnRequestClose(OsShuttingDown)) { ScribeState = ScribeRunning; Visible(true); return false; } size_t Start = ThingUi::All.Length(); Ui->Quit(); if (ThingUi::All.Length() >= Start) { LAssert(0); break; } } SerializeState(GetOptions(), OPT_ScribeWndPos, false); LCloseApp(); return LWindow::OnRequestClose(OsShuttingDown); } void ScribeWnd::DoOnTimer(LScriptCallback *c) { if (!c) return; auto Now = LCurrentTime(); if (c->PrevTs) { auto Since = Now - c->PrevTs; double Sec = (double)Since / 1000.0; if (Sec >= c->fParam) { // Call the function c->PrevTs = Now; LVirtualMachine Vm; LScriptArguments Args(&Vm); LVariant This((LDom*)this); Args.Add(&This); ExecuteScriptCallback(*c, Args); } } else { c->PrevTs = Now; } } void ScribeWnd::OnMinute() { if (Folders.Length() == 0) return; // Check for calendar event alarms... Calendar::CheckReminders(); // Check for any outgoing email that should be re-attempted... ScribeFolder *Outbox = GetFolder(FOLDER_OUTBOX); if (Outbox) { bool Resend = false; for (auto t : Outbox->Items) { Mail *m = t->IsMail(); if (m && !TestFlag(m->GetFlags(), MAIL_SENT) && TestFlag(m->GetFlags(), MAIL_READY_TO_SEND) && m->SendAttempts > 0) { Resend = true; break; } } if (Resend) Send(); } LArray Cb; if (GetScriptCallbacks(LOnTimer, Cb)) { for (auto c: Cb) { if (!c->Func) continue; if (c->fParam == 0.0) { // Work out the period from 'Data' char *s = c->Data.Str(); while (*s && IsWhiteSpace(*s)) s++; char *u = s; while (*u && !IsAlpha(*u)) u++; double v = atof(s); switch (*u) { case 's': case 'S': // seconds c->fParam = v; break; case 'm': case 'M': // mins c->fParam = v * LDateTime::MinuteLength; break; case 'h': case 'H': // hours c->fParam = v * LDateTime::HourLength; break; case 'd': case 'D': // days c->fParam = v * LDateTime::DayLength; break; default: { LgiTrace("%s:%i - Couldn't understand period '%s'\n", _FL, c->Data.Str()); c->Data.Empty(); break; } } if ((c->OnSecond = c->fParam < 60.0)) { d->OnSecondTimerCallbacks.Add(c); } } if (!c->OnSecond) DoOnTimer(c); } } } void ScribeWnd::OnHour() { // Force time zone update in case of daylight savings change. LDateTime::SystemTimeZone(true); // Check if we need should be doing a software update check static bool InSoftwareCheck = false; if (!InSoftwareCheck) { char s[64]; InSoftwareCheck = true; LVariant v; if (GetOptions()->GetValue(OPT_SoftwareUpdate, v) && v.CastInt32()) { LDateTime Now, Last; Now.SetFormat(GDTF_YEAR_MONTH_DAY); Last.SetFormat(Now.GetFormat()); Now.SetNow(); if (!GetOptions()->GetValue(OPT_SoftwareUpdateLast, v) || !Last.Set(v.Str())) { // Record now as the last check point Now.Get(s, sizeof(s)); GetOptions()->SetValue(OPT_SoftwareUpdateLast, v = s); } else if (GetOptions()->GetValue(OPT_SoftwareUpdateTime, v)) { // Valid last check date/time. switch (v.CastInt32()) { case 0: // Week Last.AddDays(7); break; case 1: // Month Last.AddMonths(1); break; case 2: // Year Last.AddMonths(12); break; default: LgiTrace("%s:%i - The option '%s' is not valid\n", _FL, OPT_SoftwareUpdateTime); return; } if (Last < Now) { // Save the last date for next time... Now.Get(s, sizeof(s)); GetOptions()->SetValue(OPT_SoftwareUpdateLast, v = s); // Check for update now... GetOptions()->GetValue(OPT_SoftwareUpdateIncBeta, v); IsSoftwareUpToDate( this, false, v.CastInt32() != 0, [this](auto s, auto Info) { if (s == SwOutOfDate) if (UpgradeSoftware(Info, this, true)) LCloseApp(); }); } } } InSoftwareCheck = false; } } bool ScribeWnd::SaveDirtyObjects(int TimeLimitMs) { bool Status = false; if (Thing::DirtyThings.Length() > 0) { static bool SavingObjects = false; if (!SavingObjects) { SavingObjects = true; LArray WriteTimes; // ssize_t StartDirty = Thing::DirtyThings.Length(); uint64 Start = LCurrentTime(); for (unsigned i=0; iSave(NULL)) { WriteTimes.Add((int) (LCurrentTime() - WriteStart)); LAssert(!ThingType::DirtyThings.HasItem(t)); Status = true; } else { LgiTrace("Failed to save thing type 0x%x\n", t->Type()); FailedWrites++; if (FailedWrites > 2) { while (ThingType::DirtyThings.Length()) ThingType::DirtyThings[0]->SetDirty(false); FailedWrites = 0; } } } } SavingObjects = false; /* if (Status) { LStringPipe p; p.Print("WriteTimes: "); for (unsigned i=0; iLastTs >= 1000) { d->LastTs = Now; OnPulseSecond(); } } } void ScribeWnd::OnPulseSecond() { #if PROFILE_ON_PULSE LProfile Prof("NewMailLst handling"); Prof.HideResultsIfBelow(50); #endif if (Mail::NewMailLst.Length() > 0) { LVariant Blink; if (GetOptions()->GetValue(OPT_BlinkNewMail, Blink) && Blink.CastInt32()) { TrayIcon.Value((TrayIcon.Value() == TRAY_ICON_MAIL) ? TRAY_ICON_NONE : TRAY_ICON_MAIL); } } else { bool Err = false; for (auto a: Accounts) { if (!a->Receive.GetStatus() || !a->Send.GetStatus()) { Err = true; } } TrayIcon.Value(Err ? TRAY_ICON_ERROR : TRAY_ICON_NORMAL); } #if PROFILE_ON_PULSE Prof.Add("StatusPanel handling"); #endif if (StatusPanel) { StatusPanel->OnPulse(); } #if PROFILE_ON_PULSE Prof.Add("OnXXXX handling"); #endif LDateTime Now; Now.SetNow(); if (d->LastMinute != Now.Minutes()) // Check every minute... { d->LastMinute = Now.Minutes(); OnMinute(); } if (d->LastHour != Now.Hours()) // Check every hour... { d->LastHour = Now.Hours(); OnHour(); } { // These timers need to be checked every second... for (auto c: d->OnSecondTimerCallbacks) DoOnTimer(c); } #if PROFILE_ON_PULSE Prof.Add("Instance handling"); #endif if (ThisInst && ValidStr(ThisInst->Args)) { LStringPipe p; p.Push(ThisInst->Args); if (ThisInst->Flags & SCRIBE_IPC_LONG_ARGS) { ThisInst->Flags |= SCRIBE_IPC_CONTINUE_ARGS; int64 Start = LCurrentTime(); while ( TestFlag(ThisInst->Flags, SCRIBE_IPC_LONG_ARGS) && LCurrentTime() - Start < 60000) { ZeroObj(ThisInst->Args); while ( TestFlag(ThisInst->Flags, SCRIBE_IPC_LONG_ARGS) && !ThisInst->Args[0] && LCurrentTime() - Start < 60000) { LSleep(10); } p.Push(ThisInst->Args); } } ZeroObj(ThisInst->Args); LAutoString Arg(p.NewStr()); if (Arg) { OsAppArguments AppArgs(0, 0); LgiTrace("Received cmd line: %s\n", Arg.Get()); AppArgs.Set(Arg); LAppInst->SetAppArgs(AppArgs); if (LAppInst->GetOption("m") && LAppInst->GetOption("f")) ; else LAppInst->OnCommandLine(); OnCommandLine(); if (GetZoom() == LZoomMin) SetZoom(LZoomNormal); Visible(true); } } #if PROFILE_ON_PULSE Prof.Add("PreviewPanel handling"); #endif if (PreviewPanel) { PreviewPanel->OnPulse(); } } void ScribeWnd::AddFolderToMru(char *FileName) { if (FileName) { // read MRU List Files; int i; for (i=0; i<10; i++) { char Key[32]; LVariant f; sprintf_s(Key, sizeof(Key), "FolderMru.%i", i); if (GetOptions()->GetValue(Key, f)) { Files.Insert(NewStr(f.Str())); GetOptions()->DeleteValue(Key); } } // remove FileName if present for (auto f: Files) { if (_stricmp(f, FileName) == 0) { Files.Delete(f); DeleteArray(f); break; } } // insert FileName at the start of the list Files.Insert(NewStr(FileName)); // write MRU for (i=0; i<10; i++) { char *n = Files.ItemAt(i); if (n) { char Key[32]; sprintf_s(Key, sizeof(Key), "FolderMru.%i", i); LVariant f; GetOptions()->SetValue(Key, f = n); } else break; } // Clean up Files.DeleteArrays(); } } bool ScribeWnd::CleanFolders(ScribeFolder *f) { if (!f) return false; if (f->Select()) { f->SerializeFieldWidths(); } for (ScribeFolder *c = f->GetChildFolder(); c; c = c->GetNextFolder()) { CleanFolders(c); } return true; } void ScribeWnd::OnFolderChanged(LDataFolderI *folder) { } bool ScribeWnd::OnFolderTask(LEventTargetI *Ptr, bool Add) { if (Add) { if (FolderTasks.HasItem(Ptr)) { LAssert(!"Can't add task twice."); return false; } FolderTasks.Add(Ptr); return true; } else { if (!FolderTasks.HasItem(Ptr)) { LAssert(!"Item not part of task list."); return false; } FolderTasks.Delete(Ptr); return true; } } LMailStore *ScribeWnd::GetDefaultMailStore() { LMailStore *Def = 0; for (unsigned i=0; i Def->Priority()) { Def = &Folders[i]; } } } } return Def; } bool HasMailStore(LXmlTag *MailStores, char *Name) { for (auto t : MailStores->Children) { char *StoreName = t->GetAttr(OPT_MailStoreName); if (StoreName && Name && !_stricmp(StoreName, Name)) return true; } return false; } LDataStoreI *ScribeWnd::CreateDataStore(const char *_Full, bool CreateIfMissing) { LString Full(_Full); auto Ext = LGetExtension(Full); if (Ext) { if (!_stricmp(Ext, "mail2")) { LgiMsg(this, LLoadString(IDS_MAIL2_DEPRECATED), AppName, MB_OK, Full.Get()); } else if (!_stricmp(Ext, "mail3")) { return OpenMail3(Full, this, CreateIfMissing); } else if (!_stricmp(Ext, "sqlite")) { LTrimDir(Full); return OpenMail3(Full, this, CreateIfMissing); } else { LgiTrace("%s:%i - Not a valid mail store extension: %s\n", _FL, Full.Get()); LAssert(!"Not a valid mail store extension."); } } else LgiTrace("%s:%i - No extension for CreateDataStore: %s\n", _FL, Full.Get()); return NULL; } class MailStoreUpgrade : public LProgressDlg, public LDataPropI { public: ScribeWnd *App = NULL; LDataStoreI *Ds = NULL; int Status = -1; LString Error; MailStoreUpgrade(ScribeWnd *app, LDataStoreI *ds) : LProgressDlg(app, -1) { SetParent(App = app); Ds = ds; SetCanCancel(false); SetDescription("Upgrading mail store..."); SetPulse(1000); Ds->Upgrade(this, this, [this](auto status) { Status = status; }); } ~MailStoreUpgrade() { } void OnPulse() override { if (Status >= 0) { EndModal(0); return; } return LProgressDlg::OnPulse(); } LDataPropI &operator =(LDataPropI &p) { LAssert(0); return *this; } Store3Status SetStr(int id, const char *str) override { switch (id) { case Store3UiError: Error = str; break; default: LAssert(!"Impl me."); return Store3Error; break; } return Store3Success; } }; bool ScribeWnd::ProcessFolder(LDataStoreI *&Store, int StoreIdx, char *StoreName) { if (Store->GetInt(FIELD_VERSION) == 0) { // version error LgiMsg(this, LLoadString(IDS_ERROR_FOLDERS_VERSION), AppName, MB_OK, 0, Store->GetInt(FIELD_VERSION)); return false; } if (Store->GetInt(FIELD_READONLY)) { LgiMsg(this, LLoadString(IDS_ERROR_READONLY_FOLDERS), AppName); } // get root item LDataFolderI *Root = Store->GetRoot(); if (!Root) return false; ScribeFolder *&Mailbox = Folders[StoreIdx].Root; Mailbox = new ScribeFolder; if (Mailbox) { Mailbox->App = this; Mailbox->SetObject(Root, false, _FL); Root->SetStr(FIELD_FOLDER_NAME, StoreName); Root->SetInt(FIELD_FOLDER_TYPE, MAGIC_NONE); } #ifdef TEST_OBJECT_SIZE // debug/repair code if (Root->StoreSize != Root->Sizeof()) { SizeErrors[0]++; Root->StoreSize = Root->Sizeof(); if (Root->Object) { Root->Object->StoreDirty = true; } } #endif // Insert the root object and then... Tree->Insert(Mailbox); // Recursively load the rest of the tree { // LProfile p("Loadfolders"); Mailbox->LoadFolders(); } // This forces a re-pour to re-order the folders according to their // sort settings. Tree->UpdateAllItems(); if (ScribeState != ScribeExiting) { // Show the tree Mailbox->Expanded(Folders[StoreIdx].Expanded); // Checks the folders for a number of required objects // and creates them if required auto StoreType = Store->GetInt(FIELD_STORE_TYPE); if (StoreType == Store3Sqlite) Validate(&Folders[StoreIdx]); else if (StoreType < 0) LAssert(!"Make sure you impl the FIELD_STORE_TYPE field in the store."); // FIXME // AddFolderToMru(Full); } return true; } struct LoadMailStoreState : public LView::ViewEventTarget { typedef std::function BoolCb; typedef std::function IntCb; ScribeWnd *App; BoolCb Callback; // Final cb for entire LoadMailStoreState execution LOptionsFile *Options = NULL; LXmlTag *MailStores = NULL; LArray Que; int StoreIdx = 0; bool OptionsDirty = false; bool Status = false; std::function ReturnWithEvent; std::function ReturnOnDialog; std::function AskStoreUpgrade; LoadMailStoreState(ScribeWnd *app, std::function callback) : ViewEventTarget(app, M_LOAD_NEXT_MAIL_STORE), App(app), Callback(callback) { Options = App->GetOptions(); ReturnWithEvent = [this](bool s) { PostEvent(M_LOAD_NEXT_MAIL_STORE); return s; }; ReturnOnDialog = [this](bool s) { return s; }; AskStoreUpgrade = [this](auto FolderPath, auto Details, auto cb) { auto result = LgiMsg(App, LLoadString(IDS_MAILSTORE_UPGRADE_Q), AppName, MB_YESNO, FolderPath, ValidStr(Details) ? Details : "n/a"); cb(result); }; MailStores = Options->LockTag(OPT_MailStores, _FL); // Load up some work in the queue and start the iteration... if (MailStores) Que = MailStores->Children; } void Start() { PostEvent(M_LOAD_NEXT_MAIL_STORE); } bool OnStatus(bool b) { if (MailStores) Options->Unlock(); if (OptionsDirty) App->SaveOptions(); if (Callback) Callback(b); delete this; return b; } LMessage::Result OnEvent(LMessage *Msg) override { if (Msg->Msg() == M_LOAD_NEXT_MAIL_STORE) { if (!MailStores) OnStatus(false); else Iterate(); } return 0; } // This will process one item off the Que, // Or if no work is available call PostIterate to // finish the process. // // In most cases this function should complete with // ReturnWithEvent to trigger the next iteration... bool Iterate() { // No more work, so do the completion step: if (Que.Length() == 0) return PostIterate(); // There are 2 exits modes for this function: // // 1) Normal exit where we should post a M_LOAD_NEXT_MAIL_STORE to ourselves to // start the next iteration by calling ReturnWithEvent. // // 2) A dialog was launched and we should NOT post a M_LOAD_NEXT_MAIL_STORE event // by calling ReturnOnDialog. The dialog's callback will do that later. // Get the next Xml tag off the queue: auto MailStore = Que[0]; Que.DeleteAt(0, true); if (!MailStore->IsTag(OPT_MailStore)) return ReturnWithEvent(false); // Read the folders.. auto Path = MailStore->GetAttr(OPT_MailStoreLocation); auto ContactUrl = MailStore->GetAttr(OPT_MailStoreContactUrl); auto CalUrl = MailStore->GetAttr(OPT_MailStoreCalendarUrl); if (!Path && !ContactUrl && !CalUrl) { LgiTrace("%s:%i - No mail store path (%i).\n", _FL, StoreIdx); return ReturnWithEvent(false); } // If disabled, skip: if (MailStore->GetAsInt(OPT_MailStoreDisable) > 0) return ReturnWithEvent(false); // Check and validate the folder name: auto StoreName = MailStore->GetAttr(OPT_MailStoreName); if (!StoreName) { char Tmp[256]; for (int i=1; true; i++) { sprintf_s(Tmp, sizeof(Tmp), "Folders%i", i); if (!HasMailStore(MailStores, Tmp)) break; } MailStore->SetAttr(OPT_MailStoreName, Tmp); StoreName = MailStore->GetAttr(OPT_MailStoreName); OptionsDirty = true; } // Build a LMailStore entry in App->Folders: auto &Folder = App->Folders[StoreIdx]; Folder.Name = StoreName; if (Path) { // Mail3 folders on disk... LFile::Path p; if (LIsRelativePath(Path)) { p = Options->GetFile(); p--; p += Path; } else p = Path; auto Full = p.GetFull(); LVariant CreateFoldersIfMissing; Options->GetValue(OPT_CreateFoldersIfMissing, CreateFoldersIfMissing); // Sniff type... auto Ext = LGetExtension(Full); if (!Ext) return ReturnWithEvent(false); if (!Folder.Store) Folder.Store = App->CreateDataStore(Full, CreateFoldersIfMissing.CastInt32() != 0); if (!Folder.Store) { LgiTrace("%s:%i - Failed to create data store for '%s'\n", _FL, Full.Get()); return ReturnWithEvent(false); } Folder.Path = Full; } else if (ContactUrl || CalUrl) { // Remove Webdav folders... Folder.Store = new WebdavStore(App, App, StoreName); } else { return ReturnWithEvent(false); } LDataStoreI *&Store = Folder.Store; auto ex = MailStore->GetAsInt(OPT_MailStoreExpanded); if (ex >= 0) Folder.Expanded = ex != 0; // Check if the mail store requires upgrading... auto MsState = (Store3Status)Store->GetInt(FIELD_STATUS); if (MsState == Store3UpgradeRequired) { LgiTrace("%s:%i - this=%p\n", _FL, this); auto Details = Store->GetStr(FIELD_STATUS); AskStoreUpgrade(Folder.Path.Get(), ValidStr(Details) ? Details : "n/a", [this, Store, MailStore](auto result) { if (result == IDYES) { auto Prog = new MailStoreUpgrade(App, Store); Prog->DoModal([this, MailStore](auto dlg, auto code) { // Upgrade complete, finish the iteration.. Iterate2(MailStore); delete dlg; }); return; } // Upgrade not allowed, so do next iteration... ReturnWithEvent(false); }); return ReturnOnDialog(true); } else if (MsState == Store3Error) { auto ErrMsg = Store->GetStr(FIELD_ERROR); auto a = new LAlert(App, AppName, ErrMsg ? ErrMsg : LLoadString(IDS_ERROR_FOLDERS_STATUS), LLoadString(IDS_EDIT_MAIL_STORES), LLoadString(IDS_OK)); a->DoModal([this](auto dlg, auto code) { delete dlg; if (code == 1) { App->PostEvent(M_COMMAND, IDM_MANAGE_MAIL_STORES); // This fails the whole LoadMailStores process, because the user // is going to edit the mail store list via the dialog; OnStatus(false); } else { // We can't load this mail store, so do the next iteration... ReturnWithEvent(true); } }); return ReturnOnDialog(false); } // No upgrade or error, just keep going. return Iterate2(MailStore); } // This also should complete by calling ReturnWithEvent/ReturnOnDialog. bool Iterate2(LXmlTag *MailStore) { auto &Folder = App->Folders[StoreIdx]; auto StoreName = MailStore->GetAttr(OPT_MailStoreName); // check password LString FolderPsw; if ((FolderPsw = Folder.Store->GetStr(FIELD_STORE_PASSWORD))) { bool Verified = false; if (ValidStr(App->d->MulPassword)) { Verified = App->d->MulPassword.Equals(FolderPsw, false); App->d->MulPassword.Empty(); } if (!Verified) { auto Dlg = new LInput(App, "", LLoadString(IDS_ASK_FOLDER_PASS), AppName, true); Dlg->DoModal([this, Dlg, FolderPsw, StoreName](auto dlg, auto id) { auto psw = Dlg->GetStr(); delete dlg; if (id == IDOK) { auto &Folder = App->Folders[StoreIdx]; if (psw == FolderPsw) { Status |= App->ProcessFolder(Folder.Store, StoreIdx, StoreName); } else { // Clear the folder and don't increment the StoreIdx... Folder.Empty(); App->Folders.PopLast(); ReturnWithEvent(false); return; } } Iterate3(); }); return ReturnOnDialog(true); } } else { Status |= App->ProcessFolder(Folder.Store, StoreIdx, StoreName); } return Iterate3(); } bool Iterate3() { StoreIdx++; return ReturnWithEvent(true); } bool PostIterate() { if (Status) { // Force load some folders... ScribeFolder *Folder = App->GetFolder(FOLDER_CALENDAR); if (Folder) Folder->LoadThings(); Folder = App->GetFolder(FOLDER_FILTERS); if (Folder) Folder->LoadThings(); for (auto ms: App->Folders) { if (!ms.Root) continue; for (auto c = ms.Root->GetChildFolder(); c; c = c->GetNextFolder()) { if (c->GetItemType() == MAGIC_CONTACT || c->GetItemType() == MAGIC_FILTER) c->LoadThings(); } } List c; App->GetContacts(c); // Set selected folder to Inbox by default // if the user hasn't selected a folder already if (App->ScribeState != ScribeWnd::ScribeExiting && App->Tree && !App->Tree->Selection()) { LVariant StartInFolder; Options->GetValue(OPT_StartInFolder, StartInFolder); ScribeFolder *Start = NULL; if (ValidStr(StartInFolder.Str())) { Start = App->GetFolder(StartInFolder.Str()); } if (!Start) { Start = App->GetFolder(FOLDER_INBOX); } if (Start && App->Tree) { App->Tree->Select(Start); } } } Options->DeleteValue(OPT_CreateFoldersIfMissing); // Set system folders ScribeFolder *f = App->GetFolder(FOLDER_INBOX); if (f) f->SetSystemFolderType(Store3SystemInbox); f = App->GetFolder(FOLDER_OUTBOX); if (f) f->SetSystemFolderType(Store3SystemOutbox); f = App->GetFolder(FOLDER_SENT); if (f) f->SetSystemFolderType(Store3SystemSent); f = App->GetFolder(FOLDER_SPAM); if (f) f->SetSystemFolderType(Store3SystemSpam); return OnStatus(Status); } }; void ScribeWnd::LoadMailStores(std::function Callback) { if (auto s = new LoadMailStoreState(this, Callback)) s->Start(); } void ScribeWnd::LoadFolders(std::function Callback) { AppState PrevState = ScribeState; ScribeState = ScribeLoadingFolders; // Setup Mailstores tag { LXmlTag *MailStores = GetOptions()->LockTag(OPT_MailStores, _FL); if (!MailStores) { // Check if we can upgrade the old folder tag char n[32]; sprintf_s(n, sizeof(n), "%s-Folders", LGetOsName()); LVariant OldFolders; GetOptions()->GetValue(n, OldFolders); // Create mail store element.. GetOptions()->CreateTag(OPT_MailStores); if ((MailStores = GetOptions()->LockTag(OPT_MailStores, _FL))) { if (OldFolders.Str()) { LXmlTag *Store = MailStores->CreateTag(OPT_MailStore); if (Store) { char Opts[MAX_PATH_LEN]; LMakePath(Opts, sizeof(Opts), GetOptions()->GetFile(), ".."); auto Rel = LMakeRelativePath(Opts, OldFolders.Str()); Store->SetAttr(OPT_MailStoreLocation, Rel ? Rel.Get() : OldFolders.Str()); // No need to ask the user for a store name, it'll be // asked later in this method anyway... // Leave the old folder tag in the xml in case the user // downgrades to v1.xx } } } } GetOptions()->Unlock(); if (!MailStores) { if (Callback) Callback(false); return; } } // Set loading flags CmdSend.Enabled(false); CmdReceive.Enabled(false); CmdPreview.Enabled(false); LoadMailStores([this, PrevState, Callback](auto Status) { if (Tree) { for (auto a: Accounts) { if (!a->Receive.Disabled() && a->Receive.IsPersistant()) a->Receive.Connect(0, false); } } using BoolFn = std::function; auto FinishLoad = new BoolFn ( [this, PrevState, Callback](bool Status) { if (ScribeState == ScribeExiting) { LCloseApp(); } else { d->FoldersLoaded = true; PostEvent(M_SCRIBE_LOADED); } if (ScribeState == ScribeExiting) LCloseApp(); ScribeState = PrevState; if (Callback) Callback(Status); } ); if (Folders.Length() == 0) { auto Dlg = new ScribeFolderDlg(this); Dlg->DoModal([this, Dlg, FinishLoad, Callback, &Status](auto dlg, auto id) { if (id == IDOK) { bool CreateMailStore = false; if (Dlg->Create) { // create folders if (LFileExists(Dlg->FolderFile)) { if (LgiMsg(this, LLoadString(IDS_ERROR_FOLDERS_ALREADY_EXIST), AppName, MB_YESNO) == IDYES) CreateMailStore = true; else LgiMsg(this, LLoadString(IDS_ERROR_WONT_OVERWRITE_FOLDERS), AppName); } else if ((Status = CreateFolders(Dlg->FolderFile))) CreateMailStore = true; } else CreateMailStore = true; if (CreateMailStore) { LXmlTag *MailStores = GetOptions()->LockTag(OPT_MailStores, _FL); if (MailStores) { LXmlTag *Store = MailStores->CreateTag(OPT_MailStore); if (Store) { char p[MAX_PATH_LEN]; LMakePath(p, sizeof(p), GetOptions()->GetFile(), ".."); auto RelPath = LMakeRelativePath(p, Dlg->FolderFile); Store->SetAttr(OPT_MailStoreLocation, RelPath ? RelPath.Get() : Dlg->FolderFile.Get()); } GetOptions()->Unlock(); LoadMailStores(NULL); } } } if (id) (*FinishLoad)(Status); else if (Callback) Callback(false); delete FinishLoad; delete dlg; }); } else { (*FinishLoad)(Status); delete FinishLoad; } }); } bool ScribeWnd::UnLoadFolders() { if (FolderTasks.Length() > 0 || ScribeState == ScribeLoadingFolders) { // Um, we can't unload folders right now // something is already happening... return false; } AppState PrevState = ScribeState; ScribeState = ScribeUnloadingFolders; OnSelect(); if (MailList) { ScribeFolder *Container = MailList->GetContainer(); if (Container) { // save folder settings Container->SerializeFieldWidths(); } MailList->SetContainer(NULL); MailList->RemoveAll(); } int Error = 0; while (Thing::DirtyThings.Length() > 0) { if (!SaveDirtyObjects()) { Error++; LgiTrace("%s:%i - SaveDirtyObjects failed.\n", _FL); if (Error > 100) { // I think we're stuck... return false; } } } // Unload IMAP folders... for (auto a: Accounts) { if (!a->Receive.Disabled() && a->Receive.IsPersistant()) { a->Receive.Disconnect(); } } if (GetOptions()) { // Unload local folders... LXmlTag *MailStores = GetOptions()->LockTag(OPT_MailStores, _FL); for (size_t i=0; iExpanded(); for (auto ms: MailStores->Children) { char *StoreName = ms->GetAttr(OPT_MailStoreName); if (Folders[i].Name.Equals(StoreName)) { ms->SetAttr(OPT_MailStoreExpanded, Expanded); break; } } } DeleteObj(Folders[i].Root); DeleteObj(Folders[i].Store); } if (MailStores) { GetOptions()->Unlock(); MailStores = NULL; } } Folders.Length(0); d->FoldersLoaded = false; if (ScribeState == ScribeExiting) LCloseApp(); ScribeState = PrevState; return true; } void ScribeWnd::BuildDynMenus() { if (MailMenu) { LString SendMail = LLoadString(IDS_SEND_MAIL); LString ReceiveMail = LLoadString(IDS_RECEIVE_MAIL); LString PreviewMail = LLoadString(IDS_PREVIEW_ON_SERVER); auto ReceiveAll = LLoadString(IDS_RECEIVE_ALL_ACCOUNTS); if (!CmdReceive.MenuItem && ReceiveAll) CmdReceive.MenuItem = MailMenu->AppendItem(ReceiveAll, IDM_RECEIVE_ALL, true); if (!SendMenu && SendMail) { auto s = SendMail.SplitDelimit("\t"); SendMenu = MailMenu->AppendSub(s[0]); } if (!ReceiveMenu && ReceiveMail) { auto s = ReceiveMail.SplitDelimit("\t"); ReceiveMenu = MailMenu->AppendSub(s[0]); } if (!PreviewMenu && PreviewMail) { auto s = PreviewMail.SplitDelimit("\t"); PreviewMenu = MailMenu->AppendSub(s[0]); } } if (!NewTemplateMenu) { auto i = Menu->FindItem(IDM_NO_TEMPLATES); if (i) { NewTemplateMenu = i->GetParent(); } } if (NewTemplateMenu) { NewTemplateMenu->Empty(); int d = 0; ScribeFolder *Templates = GetFolder(FOLDER_TEMPLATES, NULL, true); if (Templates) { Templates->LoadThings(); for (auto i: Templates->Items) { Mail *m = i->IsMail(); if (m) { NewTemplateMenu->AppendItem(m->GetSubject() ? m->GetSubject() : (char*)"(no subject)", IDM_NEW_FROM_TEMPLATE + d, true); d++; } } if (d == 0) { NewTemplateMenu->AppendItem(LLoadString(IDS_NO_ITEMS_IN_FOLDER), -1, false); } } else { NewTemplateMenu->AppendItem(LLoadString(IDS_NO_TEMPLATES), -1, false); } } } int ScribeWnd::GetToolbarHeight() { return (Commands) ? MAX(Commands->Y()-1, 20) : 20; } LToolBar *ScribeWnd::LoadToolbar(LViewI *Parent, const char *File, LAutoPtr &Img) { if (!Img) Img.Reset(LLoadImageList(File)); if (!Img) { LAssert(!"Missing image resource."); return NULL; } LToolBar *Tools = NULL; if (Img) { Tools = new LToolBar; if (Tools) Tools->SetImageList(Img, Img->TileX(), Img->TileY(), false); } else { Tools = LgiLoadToolbar(Parent, File); } if (Tools) { LVariant SizeAdj; LFont *f = Tools->GetFont(); if (f) { if (GetOptions()->GetValue(OPT_UiFontSize, SizeAdj)) { SizeAdj.Cast(GV_INT32); SizeAdj.Value.Int -= 2; f->PointSize(f->PointSize()+SizeAdj.Value.Int); } } Tools->GetCss(true)->BorderSpacing(LCss::Len(LCss::LenPx, SCRIBE_TOOLBAR_BORDER_SPACING_PX)); Tools->TextLabels(ShowToolbarText()); } return Tools; } void ScribeWnd::SetListPane(LView *v) { ThingList *ThingLst = dynamic_cast(v); DynamicHtml *Html = dynamic_cast(v); if (!ThingLst) { DeleteObj(SearchView); if (MailList) MailList->RemoveAll(); } v->Sunken(SUNKEN_CTRL); if ((MailList = ThingLst)) { DeleteObj(TitlePage); if (GetCtrlValue(IDM_ITEM_FILTER)) { OnCommand(IDM_ITEM_FILTER, 0, NULL); } } else { TitlePage = Html; } SetLayout(); } bool ScribeWnd::SetItemPreview(LView *v) { v->Sunken(SUNKEN_CTRL); if (d->SubSplit->IsAttached()) { if (d->LastLayout == 2) { Splitter->SetViewAt(1, v); } else { d->SubSplit->SetViewAt(1, v); } return true; } return false; } ScribeWnd::LayoutMode ScribeWnd::GetEffectiveLayoutMode() { LVariant Mode; GetOptions()->GetValue(OPT_LayoutMode, Mode); ScribeFolder *Cur = GetCurrentFolder(); if (Cur && Cur->IsRoot()) { Mode = FoldersAndList; } if (Mode.CastInt32() == 0) { Mode = FoldersListAndPreview; } return (LayoutMode) Mode.CastInt32(); } void ScribeWnd::SetLayout(LayoutMode Mode) { // int TreeWidth = Tree ? Tree->X() : 200; // int PreviewHt = PreviewPanel ? PreviewPanel->Y() : 200; if (Mode > 0) { LVariant v; GetOptions()->SetValue(OPT_LayoutMode, v = (int)Mode); } Mode = GetEffectiveLayoutMode(); if (!Splitter) return; bool JustPreviewPane = (Mode == FoldersAndList && d->LastLayout == FoldersListAndPreview) || (Mode == FoldersListAndPreview && d->LastLayout == FoldersAndList); LView *Content = NULL; if (TitlePage) Content = TitlePage; else if (MailList) Content = MailList; if (JustPreviewPane) { // Optimized path for hide/show the preview pane that doesn't destroy the tree // control and cause it to lose focus... otherwise we can't set it's focus due // to some weird windows interaction. switch (Mode) { default: case FoldersListAndPreview: { if (Content) Content->Sunken(SUNKEN_CTRL); if (PreviewPanel) PreviewPanel->Sunken(SUNKEN_CTRL); Splitter->SetViewAt(1, d->SubSplit); d->SubSplit->SetVertical(true); int Idx = 0; if (SearchView) d->SubSplit->SetViewAt(Idx++, SearchView); d->SubSplit->SetViewAt(Idx++, Content); d->SubSplit->SetViewAt(Idx++, PreviewPanel); break; } case FoldersAndList: { if (Content) Content->Sunken(SUNKEN_CTRL); if (SearchView) { #if LGI_VIEW_HANDLE if (!d->SubSplit->Handle()) Splitter->SetViewAt(1, d->SubSplit); #endif d->SubSplit->SetVertical(true); Splitter->SetViewAt(1, d->SubSplit); int Idx = 0; if (SearchView) d->SubSplit->SetViewAt(Idx++, SearchView); d->SubSplit->SetViewAt(Idx++, Content); } else { d->SubSplit->Detach(); Splitter->SetViewAt(1, Content); } break; } } } else { if (Tree) Tree->Sunken(SUNKEN_CTRL); if (Content) Content->Sunken(SUNKEN_CTRL); if (PreviewPanel) PreviewPanel->Sunken(SUNKEN_CTRL); switch (Mode) { default: case FoldersListAndPreview: { Splitter->SetVertical(false); d->SubSplit->SetVertical(true); Splitter->SetViewAt(0, Tree); Splitter->SetViewAt(1, d->SubSplit); int Idx = 0; if (SearchView) d->SubSplit->SetViewAt(Idx++, SearchView); d->SubSplit->SetViewAt(Idx++, Content); d->SubSplit->SetViewAt(Idx++, PreviewPanel); DeleteObj(d->SearchSplit); break; } case PreviewOnBottom: { Splitter->SetVertical(true); d->SubSplit->SetVertical(false); Splitter->SetViewAt(0, d->SubSplit); Splitter->SetViewAt(1, PreviewPanel); d->SubSplit->SetViewAt(0, Tree); if (SearchView) { if (!d->SearchSplit) d->SearchSplit = new LBox; d->SubSplit->SetViewAt(1, d->SearchSplit); d->SearchSplit->SetVertical(true); d->SearchSplit->SetViewAt(0, SearchView); d->SearchSplit->SetViewAt(1, Content); } else { d->SubSplit->SetViewAt(1, Content); DeleteObj(d->SearchSplit); } break; } case FoldersAndList: { Splitter->SetVertical(false); Splitter->SetViewAt(0, Tree); if (SearchView) { d->SubSplit->SetVertical(true); Splitter->SetViewAt(1, d->SubSplit); d->SubSplit->SetViewAt(0, SearchView); d->SubSplit->SetViewAt(1, Content); } else { d->SubSplit->Detach(); Splitter->SetViewAt(1, Content); } DeleteObj(d->SearchSplit); break; } case ThreeColumn: { Splitter->SetVertical(false); Splitter->SetViewAt(0, Tree); if (SearchView) { d->SubSplit->SetVertical(true); Splitter->SetViewAt(1, d->SubSplit); d->SubSplit->SetViewAt(0, SearchView); d->SubSplit->SetViewAt(1, Content); } else { d->SubSplit->Detach(); Splitter->SetViewAt(1, Content); } Splitter->SetViewAt(2, PreviewPanel); DeleteObj(d->SearchSplit); break; } } } if (!SearchView) { LVariant Pos = 200; GetOptions()->GetValue(OPT_SplitterPos, Pos); if (Pos.CastInt32() < 10) Pos = 200; Splitter->Value(Pos.CastInt32()); LRect r = Splitter->GetPos(); r.x2++; Splitter->SetPos(r); if (d->SubSplit->IsAttached()) { Pos = 200; GetOptions()->GetValue(OPT_SubSplitPos, Pos); if (Pos.CastInt32() < 10) Pos = 200; d->SubSplit->Value(Pos.CastInt32()); } } PourAll(); - #ifdef LINUX - LYield(); - #endif - d->LastLayout = Mode; } void ScribeWnd::SetupUi() { // Show the window if (!SerializeState(GetOptions(), OPT_ScribeWndPos, true)) { LRect r(0, 0, 1023, 767); SetPos(r); MoveToCenter(); } Visible(true); // Main toolbar Commands = LoadToolbar(this, GetResourceFile(ResToolbarFile), ToolbarImgs); if (Commands) { Commands->Attach(this); #ifdef MAC Commands->Raised(false); #else Commands->Raised(RAISED_LOOK); #endif Commands->AppendButton(RemoveAmp(LLoadString(IDS_NEW_EMAIL)), IDM_NEW_EMAIL, TBT_PUSH, true, IMG_NEW_MAIL); Commands->AppendButton(RemoveAmp(LLoadString(IDS_NEW_CONTACT)), IDM_NEW_CONTACT, TBT_PUSH, true, IMG_NEW_CONTACT); Commands->AppendSeparator(); CmdSend.ToolButton = Commands->AppendButton(RemoveAmp(LLoadString(IDS_SEND)), IDM_SEND_MAIL, TBT_PUSH, true, IMG_SEND); Commands->AppendButton("+", IDM_RECEIVE_AND_SEND, TBT_PUSH, true, -2); CmdReceive.ToolButton = Commands->AppendButton(RemoveAmp(LLoadString(IDS_RECEIVE)), IDM_RECEIVE_MAIL, TBT_PUSH, true, IMG_RECEIVE); CmdPreview.ToolButton = Commands->AppendButton(RemoveAmp(LLoadString(IDS_PREVIEW)), IDM_PREVIEW_POP3, TBT_PUSH, true, IMG_PREVIEW); Commands->AppendSeparator(); Commands->AppendButton(RemoveAmp(LLoadString(IDS_DELETE)), IDM_DELETE, TBT_PUSH, true, IMG_TRASH); Commands->AppendButton(RemoveAmp(LLoadString(IDS_SPAM)), IDM_DELETE_AS_SPAM, TBT_PUSH, true, IMG_DELETE_SPAM); Commands->AppendSeparator(); Commands->AppendButton(RemoveAmp(LLoadString(IDS_REPLY)), IDM_REPLY, TBT_PUSH, true, IMG_REPLY); Commands->AppendButton(RemoveAmp(LLoadString(IDS_REPLYALL)), IDM_REPLY_ALL, TBT_PUSH, true, IMG_REPLY_ALL); Commands->AppendButton(RemoveAmp(LLoadString(IDS_FORWARD)), IDM_FORWARD, TBT_PUSH, true, IMG_FORWARD); Commands->AppendButton(RemoveAmp(LLoadString(IDS_BOUNCE)), IDM_BOUNCE, TBT_PUSH, true, IMG_BOUNCE); Commands->AppendSeparator(); Commands->AppendButton(RemoveAmp(LLoadString(IDS_PRINT)), IDM_PRINT, TBT_PUSH, true, IMG_PRINT); Commands->AppendButton(RemoveAmp(LLoadString(IDS_CALENDAR)), IDM_CALENDAR, TBT_PUSH, true, IMG_CALENDAR); Commands->AppendButton(RemoveAmp(LLoadString(IDS_ITEM_FILTER)), IDM_ITEM_FILTER, TBT_TOGGLE, true, IMG_SEARCH); Commands->AppendButton(RemoveAmp(LLoadString(IDS_THREAD)), IDM_THREAD, TBT_TOGGLE, true, IMG_THREADS); d->ShowConsoleBtn = Commands->AppendButton(RemoveAmp(LLoadString(IDS_SHOW_CONSOLE)), IDM_SHOW_CONSOLE, TBT_PUSH, true, IMG_CONSOLE_NOMSG); Commands->AppendSeparator(); Commands->AppendButton(RemoveAmp(LLoadString(IDS_HELP)), IDM_HELP, TBT_PUSH, true, IMG_HELP); Commands->Customizable(GetOptions(), OPT_ScribeWndToolbar); if (d->ScriptToolbar.Reset(new LScriptUi(Commands))) d->ScriptToolbar->SetupCallbacks(this, 0, 0, LApplicationToolbar); PourAll(); } CmdSend.Enabled(false); CmdReceive.Enabled(false); // Preview and status windows PreviewPanel = new LPreviewPanel(this); StatusPanel = new AccountStatusPanel(this, ImageList); if (PreviewPanel && StatusPanel) { #ifdef MAC StatusPanel->Raised(false); #else StatusPanel->Raised(RAISED_LOOK); #endif StatusPanel->Attach(this); PourAll(); } // Splitter window, for folders and item list Tree = new MailTree(this); if (ImageList) { Tree->AskImage(true); Tree->SetImageList(ImageList, false); } d->SubSplit = new LBox; Splitter = new LBox; if (Splitter) { Splitter->Attach(this); #ifdef MAC Splitter->Raised(false); #else Splitter->Raised(RAISED_LOOK); #endif SetLayout(); } #if WINNATIVE TrayIcon.Load(MAKEINTRESOURCE(IDI_SMALL)); TrayIcon.Load(MAKEINTRESOURCE(IDI_ERR)); TrayIcon.Load(MAKEINTRESOURCE(IDI_MAIL)); TrayIcon.Load(MAKEINTRESOURCE(IDI_BLANK)); #else TrayIcon.Load(_T("tray_small.png")); TrayIcon.Load(_T("tray_error.png")); TrayIcon.Load(_T("tray_mail.png")); #endif LStringPipe s(256); LVariant UserName; s.Print("%s", AppName); if (GetOptions()->GetValue(OPT_UserName, UserName)) { s.Print(" [%s]", UserName.Str()); } auto AppTitle = s.NewLStr(); TrayIcon.Name(AppTitle); Name(AppTitle); TrayIcon.Value(TRAY_ICON_NORMAL); TrayIcon.Visible(true); auto Item = Menu->FindItem(IDM_SCRIPTING_CONSOLE); if (Item) { LVariant v; if (GetOptions()->GetValue(OPT_ShowScriptConsole, v)) { Item->Checked(v.CastInt32() != 0); LScribeScript::Inst->ShowScriptingWindow(Item->Checked()); } } if (Tree) { Tree->Focus(true); } } Thing *ScribeWnd::CreateItem(int Type, ScribeFolder *Folder, bool Ui) { auto FolderStore = Folder && Folder->GetObject() ? Folder->GetObject()->GetStore() : NULL; auto DefaultStore = GetDefaultMailStore(); auto Store = FolderStore ? FolderStore : (DefaultStore ? DefaultStore->Store : NULL); if (!Store) { LAssert(!"no store"); LgiTrace("%s:%i - No store for creating calendar object.\n", _FL); return NULL; } auto Obj = Store->Create(Type); if (!Obj) { LAssert(!"create failed"); LgiTrace("%s:%i - store failed to create object.\n", _FL); return NULL; } #define HANDLE_CREATE_ITEM(Magic, Type) \ case Magic: \ { \ auto o = new Type(this, Obj); \ if (!o) \ { \ LgiTrace("%s:%i - Alloc failed.\n", _FL); \ break; \ } \ if (Folder) o->SetParentFolder(Folder); \ if (Ui) o->DoUI(); \ return o; \ } switch ((uint32_t)Type) { case MAGIC_MAIL: { // create a new mail message Mail *m = new Mail(this, Obj); if (!m) { LgiTrace("%s:%i - Alloc failed.\n", _FL); break; } if (!m->GetObject()) { m->DecRef(); return 0; } m->OnCreate(); if (Folder) m->SetParentFolder(Folder); if (Ui) m->DoUI(); return m; } HANDLE_CREATE_ITEM(MAGIC_CONTACT, Contact) HANDLE_CREATE_ITEM(MAGIC_CALENDAR, Calendar) HANDLE_CREATE_ITEM(MAGIC_FILTER, Filter) HANDLE_CREATE_ITEM(MAGIC_GROUP, ContactGroup) default: LAssert(!"Unhandled object type."); break; } return NULL; } void ScribeWnd::OnPaint(LSurface *pDC) { #if 0 pDC->Colour(LColour::Red); pDC->Rectangle(); #else LCssTools Tools(this); auto c = GetClient(); // c.Offset(0, -26); Tools.PaintContent(pDC, c); #endif } int CompareContacts(Contact **a, Contact **b) { if (a && b) { auto A = (*a)->GetFirst(); auto B = (*b)->GetFirst(); if (A && B) return _stricmp(A, B); } return 0; } bool ScribeWnd::OpenAMail(ScribeFolder *Folder) { if (Folder && Tree) { Folder->LoadThings(); for (auto i: Folder->Items) { Mail *m = i->IsMail(); if (m && !(m->GetFlags() & MAIL_READ)) { Tree->Select(Folder); m->DoUI(); return true; } } for (auto *t=Folder->GetChild(); t; t=t->GetNext()) { ScribeFolder *f = dynamic_cast(t); if (OpenAMail(f)) { return true; } } } return false; } void AddContactToMenu(LSubMenu *Menu, Contact *c, ssize_t Index) { if (!c || Index < 0) return; auto Email = c->GetEmail(); if (!Email) return; // has an email, list it auto First = c->GetFirst(); auto Last = c->GetLast(); if (First || Last) { char Buf[256]; sprintf_s(Buf, sizeof(Buf), "%s %s", (First)?First:"", (Last)?Last:""); auto Item = Menu->AppendItem(Buf, TRAY_CONTACT_BASE + (int)Index, true); if (Item) Item->Icon(ICON_CONTACT); } } void ScribeWnd::AddContactsToMenu(LSubMenu *Menu) { if (!Menu) return; d->TrayMenuContacts.Sort(CompareContacts); if (((ssize_t)d->TrayMenuContacts.Length() << 4) > GdcD->Y() - 200) { // Group contacts by starting letter LArray Alpha[26]; LArray Other; for (auto c: d->TrayMenuContacts) { auto First = c->GetFirst(); auto Last = c->GetLast(); auto Email = c->GetEmail(); if (Email) { // has an email, list it if (First || Last) { auto Name = First?First:Last; if ( (*Name >= 'a' && *Name <= 'z') || (*Name >= 'A' && *Name <= 'Z') ) { int Ind = tolower(*Name) - 'a'; if (Ind >= 0 && Ind < CountOf(Alpha)) Alpha[Ind].Add(c); else Other.Add(c); } else Other.Add(c); } } } for (int i=0; i 0) { char Group[64]; sprintf_s(Group, sizeof(Group), "%c...", 'a' + i); auto Sub = Menu->AppendSub(Group); if (Sub) { for (auto c: Alpha[i]) AddContactToMenu(Sub, c, d->TrayMenuContacts.IndexOf(c)); } } } if (Other.Length()) { auto Sub = Menu->AppendSub("Other..."); if (Sub) { for (auto c: Other) AddContactToMenu(Sub, c, d->TrayMenuContacts.IndexOf(c)); } } } else { // Display all... for (size_t i=0; iTrayMenuContacts.Length(); i++) { AddContactToMenu(Menu, d->TrayMenuContacts[i], i); } } } void ScribeWnd::OnUrl(const char *Url) { LUri u(Url); if (u.sProtocol && !_stricmp(u.sProtocol, "mailto")) { CreateMail(0, Url, 0); } } void ScribeWnd::OnReceiveFiles(LArray &Files) { int UnknownFormats = 0; int64 Period = LCurrentTime() - LastDrop; if (Period > 500) // Lock out drops within 500ms of an LGI drop { LString sSend, sPages; bool HasSend = LAppInst->GetOption("send", sSend); bool HasPrint = LAppInst->GetOption("p", sPages); LArray NewMail; for (unsigned i=0; i str(new LFile); if (str->Open(f, O_READ)) { if (t->Import(t->AutoCast(str), MimeType)) { if (HasSend) { Mail *m = t->IsMail(); if (m) { NewMail.Add(m); m->SetFlags(m->GetFlags() | MAIL_CREATED | MAIL_READ); } } else { t->DoUI(); } } } } } else UnknownFormats++; } bool SendNow = sSend ? atoi(sSend) != 0 : false; for (unsigned i=0; iSetFlags(m->GetFlags() | MAIL_READY_TO_SEND); m->Save(); } else if (HasPrint) { ThingPrint(NULL, m, GetPrinter(), 0, sPages ? atoi(sPages) : 0); } else { m->DoUI(); } } if (SendNow) Send(); } } void ScribeWnd::OnTrayClick(LMouse &m) { if (m.Down()) { #ifndef MAC // No support for different mouse button info so the default // action is to show the sub-menu of contacts and actions. if (m.IsContextMenu()) #endif { LWindow::OnTrayClick(m); } #ifndef MAC else if (m.Left()) { if (m.Double()) { if (Mail::NewMailLst.Length() > 0) { ScribeFolder *InBox = GetFolder(FOLDER_INBOX); OpenAMail(InBox); } } else { if (GetZoom() == LZoomMin) { SetZoom(LZoomNormal); } else { if (Obscured()) SetZoom(LZoomNormal); // Bounce in front, first. else SetZoom(LZoomMin); } Visible(true); MoveOnScreen(); Raise(); if (MailList) { MailList->Focus(true); } } } else if (m.Middle()) { Mail::NewMailLst.Empty(); } #endif } } void ScribeWnd::OnTrayMenu(LSubMenu &m) { m.SetImageList(ImageList, false); d->TrayMenuContacts.Length(0); #ifdef MAC m.AppendItem(LLoadString(IDS_OPEN), IDM_OPEN); m.AppendSeparator(); #endif LHashTbl,Contact*> Added; LArray Srcs = GetThingSources(MAGIC_CONTACT); for (auto c: Srcs) { c->LoadThings(); for (auto i: c->Items) { Contact *c = i->IsContact(); if (!c) continue; bool IsAdded = false; auto Emails = c->GetEmails(); for (auto e: Emails) { if (Added.Find(e)) IsAdded = true; } if (Emails.Length() && !IsAdded) { for (auto e: Emails) Added.Add(e, c); d->TrayMenuContacts.Add(c); } } } AddContactsToMenu(&m); m.AppendSeparator(); if (Mail::NewMailLst.Length() > 0) { int i=0; for (auto ml: Mail::NewMailLst) { LStringPipe p; // This code figures out how many UTF characters to print. We // can't split a UTF character because downstream conversions // will fail. const char *Subj = ml->GetSubject(); if (!Subj) Subj = "(No Subject)"; LUtf8Ptr u((uint8_t*)Subj); while ((int32)u && u.GetPtr() - (uchar*)Subj < 64) u++; ssize_t Bytes = u.GetPtr() - (uchar*)Subj; p.Print("%.*s, %s <%s>", Bytes, Subj?Subj:(char*)"(No Subject)", ml->GetFromStr(FIELD_NAME), ml->GetFromStr(FIELD_EMAIL)); LAutoString a(p.NewStr()); LAssert(LIsUtf8(a)); auto Item = m.AppendItem(a, TRAY_MAIL_BASE+i++, true); if (Item) { Item->Icon(ICON_UNREAD_MAIL); } } m.AppendSeparator(); } if (GetZoom() == LZoomMin) { m.AppendItem(LLoadString(IDS_OPEN), IDM_OPEN, true); } auto NewMail = m.AppendItem(LLoadString(IDS_NEW_EMAIL), IDM_NEW_EMAIL); if (NewMail) NewMail->Icon(ICON_UNSENT_MAIL); m.AppendItem(LLoadString(IDS_EXIT), IDM_EXIT); } void ScribeWnd::OnTrayMenuResult(int MenuId) { switch (MenuId) { case IDM_OPEN: { if (GetZoom() == LZoomMin) { SetZoom(LZoomNormal); } Visible(true); Raise(); if (MailList) { MailList->Focus(true); } break; } case IDM_NEW_EMAIL: { CreateMail(); break; } case IDM_EXIT: { d->IngoreOnClose = true; PostEvent(M_CLOSE); break; } default: { auto i = MenuId - TRAY_CONTACT_BASE; Contact *c = d->TrayMenuContacts.IdxCheck(i) ? d->TrayMenuContacts[i] : NULL; if (c) { CreateMail(c); } Mail *m = Mail::NewMailLst[MenuId - TRAY_MAIL_BASE]; if (m) { Mail::NewMailLst.Delete(m); m->DoUI(); } break; } } } void ScribeWnd::OnZoom(LWindowZoom Action) { if (Action == LZoomMin) { LVariant i; if (GetOptions()->GetValue(OPT_MinimizeToTray, i) && i.CastInt32()) { Visible(false); } } } struct UserInput { std::function Callback; LView *Parent; LString Msg; bool Password; UserInput() { Password = false; } }; void ScribeWnd::GetUserInput(LView *Parent, LString Msg, bool Password, std::function Callback) { if (InThread()) { auto Inp = new LInput(Parent ? Parent : this, "", Msg, AppName, Password); Inp->DoModal([this, Inp, Callback](auto dlg, auto id) { if (Callback) Callback(id ? Inp->GetStr() : LString()); delete dlg; }); } else { auto i = new UserInput; i->Parent = Parent; i->Msg = Msg; i->Password = Password; i->Callback = Callback; if (!PostEvent(M_GET_USER_INPUT, (LMessage::Param)i)) { LAssert(!"PostEvent failed."); if (Callback) Callback(LString()); } } } LMessage::Result ScribeWnd::OnEvent(LMessage *Msg) { TrayIcon.OnEvent(Msg); BayesianFilter::OnEvent(Msg); switch (Msg->Msg()) { case M_UNIT_TEST: { LAutoPtr j((LJson*)Msg->A()); if (!j) break; auto cmd = j->Get("cmd"); if (!cmd) break; if (cmd.Equals("somecmd")) { } break; } case M_CALENDAR_SOURCE_EVENT: { CalendarSource *cs = (CalendarSource*)Msg->A(); LAutoPtr m((LMessage*)Msg->B()); if (cs && m) cs->OnEvent(m); break; } case M_GET_USER_INPUT: { LAutoPtr i((UserInput*)Msg->A()); LAssert(i); LAssert(InThread()); // Least we get stuck in an infinite loop GetUserInput(i->Parent, i->Msg, i->Password, i->Callback); break; } case M_SET_HTML: { LAutoPtr Html((LString*)Msg->A()); if (PreviewPanel && Html) { LScriptArguments Arg(NULL); Arg.Add(new LVariant(Html->Get())); PreviewPanel->CallMethod(DomToStr(SdSetHtml), Arg); } break; } case M_NEW_CONSOLE_MSG: { if (d->ShowConsoleBtn) d->ShowConsoleBtn->Image(IDM_CONSOLE_MSGS); break; } case M_NEEDS_CAP: { LAutoString c((char*)Msg->A()); LAutoString param((char*)Msg->B()); NeedsCapability(c, param); return 0; break; } case M_STORAGE_EVENT: { LDataStoreI *Store = LDataStoreI::Map.Find((int)Msg->A()); if (Store) Store->OnEvent((void*)Msg->B()); break; } case M_SCRIBE_SET_MSG_FLAG: { LAutoString p((char*)Msg->A()); if (p) { char *d = strrchr(p, '/'); if (d) { *d++ = 0; ScribeFolder *f = GetFolder(p); if (f) { LUri u; LString a = u.DecodeStr(d); f->GetMessageById(a, [this, NewFlag=(int)Msg->B()](auto r) { if (r) { int ExistingFlags = r->GetFlags(); r->SetFlags(ExistingFlags | NewFlag); } }); } } } break; } case M_SCRIBE_DEL_THING: { Thing *t = (Thing*)Msg->A(); DeleteObj(t); break; } case M_SCRIBE_LOADED: { if (d->FoldersLoaded) { // Ok let the user in CmdSend.Enabled(true); CmdReceive.Enabled(true); CmdPreview.Enabled(true); } break; } case M_SCRIBE_THREAD_DONE: { // Finialize connection AccountThread *Thread = (AccountThread*) Msg->A(); Accountlet *Acc = (Accountlet*) Msg->B(); if (Thread && Acc) { OnAfterConnect(Acc->GetAccount(), Acc->IsReceive()); d->NewMailTimeout = 2; Acc->OnThreadDone(); } else { LAssert(0); } break; } case M_SCRIBE_MSG: { char *m = (char*)Msg->A(); if (m) { if (Msg->B()) { if (LgiMsg(this, m, AppName, MB_YESNO) == IDYES) { PostEvent(M_COMMAND, IDM_OPTIONS, 0); } } else { LgiMsg(this, "%s", AppName, MB_OK, m); } DeleteArray(m); } break; } /* case M_SCRIBE_LOG_MSG: { List *Log = (List*)Msg->A(); LAutoPtr Entry((LogEntry*)Msg->B()); if (ScribeState != ScribeExiting && Log && Entry) { Log->Insert(Entry.Release()); // Trim long list... while (Log->Length() > 1024) { LogEntry *e = Log->First(); Log->Delete(e); DeleteObj(e); } } break; } */ case M_SCRIBE_NEW_MAIL: { if (Lock(_FL)) { if (NewMailDlg) { NewMailDlg->AddThings(&Mail::NewMailLst); } Unlock(); } break; } case M_SCRIBE_OPEN_THING: { Thing *t = (Thing*) Msg->A(); if (t) { t->DoUI(); } break; } case M_SCRIBE_ITEM_SELECT: { if (!MailList || !IsAttached()) break; List Things; MailList->GetSelection(Things); OnSelect(&Things); break; } case M_URL: { LAutoPtr Url((LString*)Msg->A()); if (Url) { LUri u(*Url); if (u.sProtocol && !_stricmp(u.sProtocol, "mailto")) { Mailto mt(this, *Url); if (mt.To.Length() > 0) { Thing *t = CreateItem(MAGIC_MAIL, NULL, false); if (t) { Mail *m = t->IsMail(); if (m) { mt.Apply(m); m->DoUI(); } else DeleteObj(t); } } } } break; } } return LWindow::OnEvent(Msg); } bool ScribeWnd::IsMyEmail(const char *Email) { if (Email) { LVariant e; for (auto a : *GetAccounts()) { LVariant e = a->Identity.Email(); if (e.Str() && _stricmp(Email, e.Str()) == 0) { return true; } } } return false; } int ScribeWnd::GetMaxPages() { return d->PrintMaxPages; } void ScribeWnd::ThingPrint(std::function Callback, ThingType *m, LPrinter *Printer, LView *Parent, int MaxPages) { d->PrintMaxPages = MaxPages; if (!Printer) Printer = GetPrinter(); if (!Printer) { if (Callback) Callback(false); return; } Thing *t = dynamic_cast(m); if (!t) { if (Callback) Callback(false); return; } auto Events = new ScribePrintContext(this, t); Printer->Print( Events, [this, Events, Parent, Printer, Callback](auto pages) { if (pages == Events->OnBeginPrintError) { LgiMsg(Parent, "Printing failed: %s", AppName, MB_OK, Printer->GetErrorMsg().Get()); if (Callback) Callback(false); } else if (Callback) { Callback(true); } delete Events; }, AppName, -1, Parent ? Parent : this); } bool ScribeWnd::MailReplyTo(Mail *m, bool All) { bool Status = false; if (m) { LDataStoreI *Store = m->GetObject() ? m->GetObject()->GetStore() : NULL; LDataI *NewMailObj = Store && Store->GetInt(FIELD_STORE_TYPE) == Store3Sqlite ? Store->Create(MAGIC_MAIL) : NULL; Mail *NewMail = new Mail(this, NewMailObj); if (NewMail) { if (NewMail->GetObject()) { NewMail->OnReply(m, All, true); LView *w = NewMail->DoUI(); if (w) { LViewI *t = w->FindControl(IDC_TEXT); if (t) { t->Focus(true); } } Status = true; } else DeleteObj(NewMail); } } return Status; } bool ScribeWnd::MailForward(Mail *m) { bool Status = false; if (m) { Mail *NewMail = new Mail(this); if (NewMail) { if (NewMail->OnForward(m, true)) { NewMail->DoUI(); Status = true; } else { NewMail->DecRef(); } } } return Status; } bool ScribeWnd::MailBounce(Mail *m) { bool Status = false; if (m) { Mail *NewMail = new Mail(this); if (NewMail) { if (NewMail->OnBounce(m, true)) { NewMail->DoUI(); Status = true; } else { DeleteObj(NewMail); } } } return Status; } Mail *ScribeWnd::CreateMail(Contact *c, const char *Email, const char *Name) { Mail *m = dynamic_cast(CreateItem(MAGIC_MAIL, NULL, false)); if (m) { bool IsMailTo = false; if (Email) { IsMailTo = _strnicmp(Email, "mailto:", 7) == 0; if (IsMailTo) { Mailto mt(this, Email); mt.Apply(m); } } MailUi *UI = dynamic_cast(m->DoUI()); if (UI) { if (c) { UI->AddRecipient(c); } if (Email && !IsMailTo) { UI->AddRecipient(Email, Name); } } } return m; } Mail *ScribeWnd::LookupMailRef(const char *MsgRef, bool TraceAllUids) { if (!MsgRef) return 0; LAutoString p(NewStr(MsgRef)); char *RawUid = strrchr(p, '/'); if (RawUid) { *RawUid++ = 0; LUri u; LString Uid = u.DecodeStr(RawUid); // Try the mail message map first... Mail *m = Mail::GetMailFromId(Uid); if (m) return m; // Ok, not found, so look in last known folder... ScribeFolder *f = GetFolder(p); if (f) { for (auto t : f->Items) { Mail *m = t->IsMail(); if (m) { auto s = m->GetMessageId(); if (s && !strcmp(s, Uid)) { return m; } if (TraceAllUids) LgiTrace("\t%s\n", s); } } } } return 0; } void ScribeWnd::OnBayesAnalyse(const char *Msg, const char *WhiteListEmail) { LString s, q; s.Printf("
%s
", Msg); if (WhiteListEmail) { q.Printf(LLoadString(IDS_REMOVE_WHITELIST), WhiteListEmail); s += LString("
") + q; } s += ""; LHtmlMsg([this, WhiteListEmail=LString(WhiteListEmail)](auto result) { if (result == IDYES) RemoveFromWhitelist(WhiteListEmail); }, this, s, AppName, WhiteListEmail ? MB_YESNO : MB_OK); } bool ScribeWnd::OnBayesResult(const char *MailRef, double Rating) { Mail *m = LookupMailRef(MailRef); if (m) return OnBayesResult(m, Rating); #ifdef _DEBUG else { LgiTrace("%s:%i - error finding mail ref: %s\n", _FL, MailRef); LookupMailRef(MailRef, true); LAssert(!"We should always be able to resolve the reference, unless m is completely deleted"); } #endif return false; } bool ScribeWnd::OnBayesResult(Mail *m, double Rating) { if (!m) return false; LVariant v; GetOptions()->GetValue(OPT_BayesThreshold, v); double BayesThresh = v.CastDouble(); if (BayesThresh < 0.1) BayesThresh = 0.1; if (BayesThresh > 1.0) BayesThresh = 1.0; if (Rating < BayesThresh) { // Not spam, so we continue new mail processing if (m->NewEmail == Mail::NewEmailBayes) { List Nm; Nm.Insert(m); m->NewEmail = Mail::NewEmailGrowl; OnNewMail(&Nm, true); } return false; } // Spam is pink! m->SetMarkColour(Rgb32(255, 0, 0)); m->SetFlags(m->GetFlags() | MAIL_BAYES_SPAM); ScribeBayesianFilterMode FilterMode = BayesOff; if (GetOptions()->GetValue(OPT_BayesFilterMode, v)) FilterMode = (ScribeBayesianFilterMode)v.CastInt32(); if (FilterMode == BayesTrain) { // Move to folder LVariant MoveToPath; if (!GetOptions()->GetValue(OPT_BayesMoveTo, MoveToPath)) { MoveToPath = "/Spam/Probably"; } ScribeFolder *f = GetFolder(MoveToPath.Str()); if (f) { LArray Items; Items.Add(m); f->MoveTo(Items, false, [this, m](auto result, auto status) { List obj; obj.Insert(m); OnNewMail(&obj, false); }); } } else { m->DeleteAsSpam(this); } return true; } static int AccountCmp(ScribeAccount *a, ScribeAccount *b, int Data) { return a->Identity.Sort() - b->Identity.Sort(); } class ScribePasteState : public LProgressDlg { ScribeWnd *App = NULL; ScribeFolder *Folder = NULL; LString Data; LDataStoreI::StoreTrans Trans; LProgressPane *LoadPane = NULL, *SavePane = NULL; ScribeClipboardFmt *tl = NULL; uint32_t Errors = 0; ssize_t Idx = 0; enum PasteState { LoadingThings, SavingThings, } State = LoadingThings; public: ScribePasteState(ScribeWnd *app, ScribeFolder *folder, LString data) : LProgressDlg(app), App(app), Folder(folder), Data(data) { // Paste 'ScribeThingList' tl = (ScribeClipboardFmt*)Data.Get(); Trans = Folder->GetObject()->GetStore()->StartTransaction(); LoadPane = ItemAt(0); LoadPane->SetDescription("Loading objects..."); LoadPane->SetRange(tl->Length()); SavePane = Push(); SavePane->SetRange(tl->Length()); SavePane->SetDescription("Saving: No errors..."); // LProgressDlg will do a SetPulse in it's OnCreate } void OnPulse() { auto Start = LCurrentTime(); static int TimeSlice = 300; //ms if (State == LoadingThings) { while ( Idx < tl->Length() && !IsCancelled() && LCurrentTime() - Start < TimeSlice) { Thing *t = tl->ThingAt(Idx++); if (!t) continue; auto Obj = t->GetObject(); if (Obj->GetInt(FIELD_LOADED) < Store3Loaded) Obj->SetInt(FIELD_LOADED, Store3Loaded); } Value(Idx); if (Idx >= tl->Length()) { State = SavingThings; Idx = 0; } } else if (State == SavingThings) { while ( Idx < tl->Length() && !IsCancelled() && LCurrentTime() - Start < TimeSlice) { Thing *t = tl->ThingAt(Idx++); if (!t) continue; auto Obj = t->GetObject(); LAssert(Obj->GetInt(FIELD_LOADED) == Store3Loaded); // Load loop should have done this already Thing *Dst = App->CreateItem(Obj->Type(), Folder, false); if (Dst) { *Dst = *t; Dst->Update(); if (!Dst->Save(Folder)) { LString s; s.Printf("Saving: " LPrintfSSizeT " error(s)", ++Errors); SetDescription(s); } } else Errors++; } SavePane->Value(Idx); if (Idx >= tl->Length()) { if (Errors > 0) LgiMsg(this, "Failed to save %i of %i objects.", AppName, MB_OK, Errors, tl->Length()); Quit(); return; } } LProgressDlg::OnPulse(); } }; int ScribeWnd::OnCommand(int Cmd, int Event, OsView WndHandle) { // Send mail multi-menu if (Cmd >= IDM_SEND_FROM && Cmd <= IDM_SEND_FROM + (ssize_t)Accounts.Length()) { Send(Cmd - IDM_SEND_FROM); return 0; } // Receive mail multi-menu if (Cmd >= IDM_RECEIVE_FROM && Cmd < IDM_RECEIVE_FROM + (ssize_t)Accounts.Length()) { Receive(Cmd - IDM_RECEIVE_FROM); return 0; } // Preview mail multi-menu if (Cmd >= IDM_PREVIEW_FROM && Cmd < IDM_PREVIEW_FROM + (ssize_t)Accounts.Length()) { Preview(Cmd - IDM_PREVIEW_FROM); return 0; } // Identity multi-menu if (Cmd >= IDM_IDENTITY_BASE && Cmd <= IDM_IDENTITY_BASE + (ssize_t)Accounts.Length()) { SetCurrentIdentity(Cmd - IDM_IDENTITY_BASE - 1); return 0; } // Is this a script tool? if (LScribeScript::Inst && Cmd >= IDM_TOOL_SCRIPT_BASE && Cmd < IDM_TOOL_SCRIPT_BASE + (int)d->Scripts.Length()) { // Do tools menu callback... find the right callback.... LArray c; if (GetScriptCallbacks(LToolsMenu, c)) { for (unsigned i=0; iFunc && c[i]->Param == Cmd) { // Call the callback char Msg[MAX_PATH_LEN]; LScribeScript::Inst->GetLog()->Write ( Msg, sprintf_s(Msg, sizeof(Msg), "\n\nRunning tool script '%s'...\n", c[i]->Script->Code->GetFileName()) ); // Setup the arguments... LScriptArguments Args(NULL); Args.New() = new LVariant((LDom*)this); Args.New() = new LVariant(Cmd); // Call the method ExecuteScriptCallback(*c[i], Args); // Cleanup Args.DeleteObjects(); break; } } } return 0; } // New from template multi-menu if (Cmd >= IDM_NEW_FROM_TEMPLATE && Cmd < IDM_NEW_FROM_TEMPLATE + 100) { int Index = Cmd - IDM_NEW_FROM_TEMPLATE; ScribeFolder *Templates = GetFolder(FOLDER_TEMPLATES); if (Templates) { Templates->LoadThings(); for (auto i: Templates->Items) { Mail *m = i->IsMail(); if (m) { if (Index == 0) { Thing *t = CreateItem(MAGIC_MAIL, 0, false); // GetFolder(FOLDER_OUTBOX) Mail *NewMail = IsMail(t); if (NewMail) { *NewMail = (Thing&)*m; NewMail->DoUI(); break; } } Index--; } } } return 0; } switch (Cmd) { // File menu case IDM_MANAGE_MAIL_STORES: { auto Dlg = new ManageMailStores(this); Dlg->DoModal([this, Dlg](auto dlg, auto id) { LAutoPtr mem(dlg); if (id) { SaveOptions(); if (!UnLoadFolders()) return; LXmlTag *Ms = GetOptions()->LockTag(OPT_MailStores, _FL); if (Ms) { while (Ms->Children.Length()) delete Ms->Children[0]; LXmlTag *t = Dlg->Options.GetChildTag(OPT_MailStores); if (t) { for (auto c: t->Children) { LXmlTag *n = new LXmlTag; n->Copy(*c, true); Ms->InsertTag(n); } } GetOptions()->Unlock(); } LVariant v; GetOptions()->SetValue(OPT_CreateFoldersIfMissing, v = true); if (!Dlg->Options.GetValue(OPT_StartInFolder, v)) v.Empty(); GetOptions()->SetValue(OPT_StartInFolder, v); LoadFolders(NULL); } }); break; } case IDM_REPLICATE: { auto Dlg = new ReplicateDlg(this); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) { UnLoadFolders(); Dlg->StartProcess(); // Don't delete dialog... let it run } else delete dlg; }); break; } case IDM_SECURITY: { // Check for user perm password... // No point allow any old one to edit the security settings. auto ShowDialog = [this]() { auto Dlg = new SecurityDlg(this); Dlg->DoModal(NULL); }; LPassword p; if (p.Serialize(GetOptions(), OPT_UserPermPassword, false)) { GetAccessLevel(this, PermRequireUser, "Security Settings", [ShowDialog](bool Allow) { if (Allow) ShowDialog(); }); } else { ShowDialog(); } break; } case IDM_OPTIONS: { LVariant ShowTotals; GetOptions()->GetValue(OPT_ShowFolderTotals, ShowTotals); // do the dialog auto Dlg = new OptionsDlg(this); Dlg->DoModal([this, Dlg, ShowTotals](auto dlg, auto id) { if (id) { // set up the POP3 accounts SetupAccounts(); SaveOptions(); // close any IMAP accounts that are now disabled. for (auto a : Accounts) { if (a->Receive.IsConfigured() && a->Receive.IsPersistant()) { if (a->Receive.Disabled()) a->Receive.Disconnect(); else Receive(a->GetIndex()); } } // List/Tree view options update LVariant i; if (GetOptions()->GetValue(OPT_ShowFolderTotals, i) && i.CastInt32() != ShowTotals.CastInt32()) { Tree->UpdateAllItems(); } if (GetOptions()->GetValue(OPT_PreviewLines, i)) { Mail::PreviewLines = i.CastInt32() != 0; } if (MailList) { if (GetOptions()->GetValue(OPT_GridLines, i)) { MailList->DrawGridLines(i.CastInt32() != 0); } MailList->Invalidate(); } // date formats if (GetOptions()->GetValue(OPT_DateFormat, i)) { int Idx = i.CastInt32(); if (Idx >= 0 && Idx < CountOf(DateTimeFormats)) { LDateTime::SetDefaultFormat(DateTimeFormats[Idx]); } } if (GetOptions()->GetValue(OPT_AdjustDateTz, i)) Mail::AdjustDateTz = i.CastInt32() == 0; // SSL debug logging if (GetOptions()->GetValue(OPT_DebugSSL, i)) SslSocket::DebugLogging = i.CastInt32() != 0; // Html edit menu if (GetOptions()->GetValue(OPT_EditControl, i)) { auto mi = Menu->FindItem(IDM_HTML_EDITOR); if (mi) mi->Checked(i.CastInt32() != 0); } } delete dlg; }); break; } case IDM_WORK_OFFLINE: { if (WorkOffline) { WorkOffline->Checked(!WorkOffline->Checked()); LVariant v; GetOptions()->SetValue(OPT_WorkOffline, v = WorkOffline->Checked()); if (!WorkOffline->Checked()) { // Offline -> Online transition. // Check if any pending messages are in the Outbox ScribeFolder *Outbox = GetFolder(FOLDER_OUTBOX); if (Outbox) { bool HasMailToSend = false; for (auto t: Outbox->Items) { Mail *m = t->IsMail(); if (m) { if (TestFlag(m->GetFlags(), MAIL_READY_TO_SEND)) { HasMailToSend = true; break; } } } if (HasMailToSend) { PostEvent(M_COMMAND, IDM_SEND_MAIL, #ifndef __GTK_H__ (LMessage::Param)Handle() #else 0 #endif ); } } } } break; } case IDM_ITEM_FILTER: { if (GetCtrlValue(IDM_ITEM_FILTER)) { if ((SearchView = new LSearchView(this))) { SearchView->Focus(true); SetLayout(); } } else { DeleteObj(SearchView); } ScribeFolder *Folder = GetCurrentFolder(); if (Folder) { Folder->Populate(MailList); } break; } case IDM_PRINT: { if (MailList) { List Sel; if (MailList->LList::GetSelection(Sel)) { for (auto i: Sel) { ThingType *t = dynamic_cast(i); ThingPrint(NULL, t); } } } break; } case IDM_PRINTSETUP: { auto *p = GetPrinter(); if (p && p->Browse(this)) { LString Str; if (p->Serialize(Str, true)) { LVariant v; GetOptions()->SetValue(OPT_PrintSettings, v = Str); } } break; } case IDM_PAGE_SETUP: { auto Dlg = new ScribePageSetup(this, GetOptions()); Dlg->DoModal(NULL); break; } case IDM_EXIT: { LMouse m; GetMouse(m); d->IngoreOnClose = m.Ctrl(); LCloseApp(); break; } // Edit menu case IDM_FIND: { auto v = GetFocus(); LDocView *doc = dynamic_cast(v); if (doc) { doc->DoFind(NULL); } else { ScribeFolder *Folder = GetCurrentFolder(); OpenFinder(this, Folder); } break; } case IDM_COPY: { if (MailList && MailList->Focus()) { List Lst; if (!MailList->GetSelection(Lst)) break; // Copy 'ScribeThingList' ScribeClipboardFmt *tl = ScribeClipboardFmt::Alloc(Lst); if (!tl) break; LClipBoard Clip(this); if (Clip.IsOpen()) { if (!Clip.Binary(d->ClipboardFormat, (uchar*)tl, tl->Sizeof(), true)) { LgiMsg(this, "Couldn't set the clipboard data.", AppName, MB_OK); } } else { LgiMsg(this, "Couldn't open the clipboard.", AppName, MB_OK); } free(tl); } else { LViewI *v = LAppInst->GetFocus(); if (v) v->PostEvent(M_COPY); } break; } case IDM_PASTE: { LViewI *v = LAppInst->GetFocus(); if (v && v->GetWindow() != (LWindow*)this) { v->PostEvent(M_PASTE); break; } if (!MailList->Focus() && !Tree->Focus()) { LgiTrace("%s:%i - List/Tree doesn't have focus.\n"); break; } ScribeFolder *Folder = dynamic_cast(Tree->Selection()); if (!Folder || !Folder->GetObject()) { LgiMsg(this, "No current folder.", AppName, MB_OK); break; } LClipBoard Clip(this); if (!Clip.IsOpen()) { LgiMsg(this, "Couldn't open the clipboard.", AppName, MB_OK); break; } Clip.Binary(d->ClipboardFormat, [this, Folder](auto Data, auto Err) { if (Data) { if (ScribeClipboardFmt::IsThing(Data.Get(), Data.Length())) { new ScribePasteState(this, Folder, Data); } } else { LgiMsg(this, "Couldn't get the clipboard data: %s", AppName, MB_OK, Err.Get()); } }); break; } case IDM_DELETE: { LViewI *f = LAppInst->GetFocus(); LEdit *e = dynamic_cast(f); if (e) { // This handles the case where on a mac the menu eats the delete key, even // when the edit control needs it LKey k(LK_DELETE, 0); k.Down(true); f->OnKey(k); k.Down(false); f->OnKey(k); } else { OnDelete(); } break; } case IDM_DELETE_AS_SPAM: { if (MailList) { List Sel; MailList->GetSelection(Sel); int Index = -1; for (auto i: Sel) { Mail *m = IsMail(i); if (m) { if (Index < 0) { Index = MailList->IndexOf(i); } m->DeleteAsSpam(this); } } if (Index >= 0) { LListItem *i = MailList->ItemAt(Index); if (!i) i = MailList->ItemAt(MailList->Length()-1); if (i) i->Select(true); } } break; } case IDM_REFRESH: { ScribeFolder *f = GetCurrentFolder(); if (!f) break; const char *s = DomToStr(SdRefresh); f->GetFldObj()->OnCommand(s); break; } // Mail menu case IDM_NEW_EMAIL: { CreateMail(); break; } case IDM_SET_READ: case IDM_SET_UNREAD: { ScribeFolder *f = GetCurrentFolder(); if (!f) break; bool SetRead = Cmd == IDM_SET_READ; f->LoadThings(); LArray Change; for (auto t: f->Items) { Mail *m = t->IsMail(); if (m && m->Select()) Change.Add(m->GetObject()); } LVariant v = MAIL_READ; LDataStoreI *Store = f->GetObject()->GetStore(); if (Store->Change(Change, FIELD_FLAGS, v, SetRead ? OpPlusEquals : OpMinusEquals) == Store3Error) { for (auto t : f->Items) { Mail *m = t->IsMail(); if (!m) continue; if (!m->Select()) continue; if (SetRead) m->SetFlags(m->GetFlags() | MAIL_READ); else m->SetFlags(m->GetFlags() & ~MAIL_READ); } } break; } case IDM_REPLY: case IDM_REPLY_ALL: { if (MailList) MailReplyTo(IsMail(MailList->GetSelected()), (Cmd == IDM_REPLY_ALL)); break; } case IDM_FORWARD: { if (MailList) MailForward(IsMail(MailList->GetSelected())); break; } case IDM_BOUNCE: { if (MailList) MailBounce(IsMail(MailList->GetSelected())); break; } case IDM_SEND_MAIL: { Send(); break; } case IDM_RECEIVE_AND_SEND: { d->SendAfterReceive = true; PostEvent(M_COMMAND, IDM_RECEIVE_MAIL, (LMessage::Param)FindControl(IDM_RECEIVE_MAIL)); break; } case IDM_THREAD: { if (MailList) { ScribeFolder *f = GetCurrentFolder(); if (f) { f->SetThreaded(!f->GetThreaded()); f->Populate(MailList); } } break; } case IDM_RECEIVE_ALL: { #define LOG_RECEIVE_ALL 0 int i = 0; Accounts.Sort(AccountCmp); for (auto a : Accounts) { #if LOG_RECEIVE_ALL auto name = a->Identity.Name(); auto email = a->Identity.Email(); LString desc; desc.Printf("%s/%s", name.Str(), email.Str()); #endif if (!a->Receive.IsConfigured()) { #if LOG_RECEIVE_ALL LgiTrace("%s:%i - %i/%s not configured.\n", _FL, a->GetIndex(), desc.Get()); #endif } else if (a->Receive.Disabled() > 0) { #if LOG_RECEIVE_ALL LgiTrace("%s:%i - %i/%s is disabled.\n", _FL, a->GetIndex(), desc.Get()); #endif } else { #if LOG_RECEIVE_ALL LgiTrace("%s:%i - %i/%s will connect.\n", _FL, a->GetIndex(), desc.Get()); #endif Receive(a->GetIndex()); } i++; } break; } case IDM_RECEIVE_MAIL: { LVariant Def; if (GetOptions()->GetValue(OPT_Pop3DefAction, Def) && Def.CastInt32() == 0) return OnCommand(IDM_RECEIVE_ALL, 0, NULL); Receive(0); break; } case IDM_PREVIEW_POP3: { LArray Account; Accounts.Sort(AccountCmp); for (auto a: Accounts) { if (!a->Receive.IsConfigured()) continue; auto Protocol = ProtocolStrToEnum(a->Receive.Protocol().Str()); if (Protocol == ProtocolPop3) { Account.Add(a); break; } } if (Account.Length() == 1) OpenPopView(this, Account); break; } case IDM_CALENDAR: { extern void OpenCalender(ScribeFolder *folder); ScribeFolder *Folder = GetFolder(FOLDER_CALENDAR); if (Folder) { OpenCalender(Folder); } break; } // Contact menu case IDM_NEW_CONTACT: { CreateItem(MAGIC_CONTACT, NULL); break; } case IDM_NEW_GROUP: { CreateItem(MAGIC_GROUP, NULL); break; } // Filter menu case IDM_NEW_FILTER: { Thing *t = CreateItem(MAGIC_FILTER, NULL, false); if (t) { t->IsFilter()->SetIncoming(true); t->DoUI(); } break; } case IDM_FILTER_CURRENT_FOLDER: { ScribeFolder *Folder = GetCurrentFolder(); if (Folder) { List Filters; GetFilters(Filters, false, false, true); List Src; for (auto i: Folder->Items) { if (i->IsMail()) { Src.Insert(i->IsMail()); } } if (!Src[0]) { LgiMsg(this, LLoadString(IDS_NO_MAIL_TO_FILTER), AppName); } else { Filter::ApplyFilters(this, Filters, Src); } } break; } case IDM_FILTER_SELECTION: { ScribeFolder *Folder = GetCurrentFolder(); if (Folder) { List Filters; GetFilters(Filters, false, false, true); List Src; for (auto i: Folder->Items) { if (i->IsMail() && i->Select()) { Src.Insert(i->IsMail()); } } if (Src.Length()) { Filter::ApplyFilters(this, Filters, Src); } } break; } case IDM_DEBUG_FILTERS: { auto i = Menu->FindItem(IDM_DEBUG_FILTERS); if (i) { i->Checked(!i->Checked()); } break; } case IDM_HTML_EDITOR: { auto i = Menu->FindItem(IDM_HTML_EDITOR); if (i) { i->Checked(!i->Checked()); LVariant v; GetOptions()->SetValue(OPT_EditControl, v = i->Checked() ? 1 : 0); } break; } case IDM_FILTERS_DISABLE: { if (d->DisableUserFilters) { d->DisableUserFilters->Checked(!d->DisableUserFilters->Checked()); LVariant v; GetOptions()->SetValue(OPT_DisableUserFilters, v = d->DisableUserFilters->Checked()); } break; } case IDM_BUILD_BAYES_DB: { BuildSpamDb(); break; } case IDM_BAYES_STATS: { BuildStats(); break; } case IDM_BAYES_SETTINGS: { auto Dlg = new BayesDlg(this); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) { LVariant i; if (GetOptions()->GetValue(OPT_BayesFilterMode, i)) { ScribeBayesianFilterMode m = ((ScribeBayesianFilterMode)i.CastInt32()); if (m != BayesOff) { LVariant SpamPath, ProbablyPath; GetOptions()->GetValue(OPT_SpamFolder, SpamPath); GetOptions()->GetValue(OPT_BayesMoveTo, ProbablyPath); if (m == BayesFilter) { ScribeFolder *Spam = GetFolder(SpamPath.Str()); if (!Spam) { LMailStore *RelevantStore = GetMailStoreForPath(SpamPath.Str()); if (RelevantStore) { LString p = SpamPath.Str(); LString::Array a = p.SplitDelimit("/"); Spam = RelevantStore->Root; for (unsigned i=1; iGetSubFolder(a[i]); if (!c) c = Spam->CreateSubFolder(a[i], MAGIC_MAIL); Spam = c; } } } if (Spam) { LVariant v; GetOptions()->SetValue(OPT_HasSpam, v = 1); } } else if (m == BayesTrain) { ScribeFolder *Probably = GetFolder(ProbablyPath.Str()); if (!Probably) { LgiMsg(this, "Couldn't find the folder '%s'", AppName, MB_OK, ProbablyPath.Str()); } } } } } delete dlg; }); break; } case IDM_BAYES_CHECK: { List Sel; if (MailList) MailList->GetSelection(Sel); for (auto i: Sel) { Thing *t = dynamic_cast(i); if (t) { Mail *m = t->IsMail(); if (m) { d->BayesLog.Empty(); double SpamRating = 0.0; IsSpam(SpamRating, m, true); break; } } } break; } // Tools menu case IDM_SCRIPTING_CONSOLE: case IDM_SHOW_CONSOLE: { ShowScriptingConsole(); if (d->ShowConsoleBtn) d->ShowConsoleBtn->Image(IMG_CONSOLE_NOMSG); break; } case IDM_EXPORT_TEXT_MBOX: { Export_UnixMBox(this); break; } case IDM_IMPORT_CSV: { ImportCsv(this); break; } case IDM_EXPORT_CSV: { ExportCsv(this); break; } case IDM_IMPORT_EML: { ImportEml(this); break; } case IDM_EXPORT_SCRIBE: { ExportScribe(this, NULL/* default mail store */); break; } case IDM_IMPORT_TEXT_MBOX: { Import_UnixMBox(this); break; } case IDM_IMP_EUDORA_ADDR: { Import_EudoraAddressBook(this); break; } case IDM_IMP_MOZILLA_ADDR: { Import_MozillaAddressBook(this); break; } case IDM_IMP_MOZILLA_MAIL: { Import_MozillaMail(this); break; } #if WINNATIVE case IDM_IMPORT_OUTLOOK_PAB: { Import_OutlookContacts(this); break; } case IDM_IMPORT_OUTLOOK_ITEMS: { Import_Outlook(this, IMP_OUTLOOK); break; } case IDM_EXPORT_OUTLOOK_ITEMS: { Export_Outlook(this); break; } #endif case IDM_IMP_MBX_EMAIL: { Import_OutlookExpress(this, false); // v4 break; } case IDM_IMP_DBX_EMAIL: { Import_OutlookExpress(this); // v5 break; } case IDM_IMPORT_NS_CONTACTS: { Import_NetscapeContacts(this); break; } case IDM_CHECK_UPDATE: { LVariant v; GetOptions()->GetValue(OPT_SoftwareUpdateIncBeta, v); SoftwareUpdate(this, true, v.CastInt32() != 0, [](auto goingToUpdate) { if (goingToUpdate) LCloseApp(); }); break; } case IDM_LOGOUT: { CurrentAuthLevel = PermRequireNone; auto i = Menu->FindItem(IDM_LOGOUT); if (i) i->Enabled(false); break; } case IDM_LAYOUT1: { LVariant v; int TwoThirds = GetClient().Y() >> 1; GetOptions()->SetValue(OPT_SplitterPos, v = 200); GetOptions()->SetValue(OPT_SubSplitPos, v = TwoThirds); SetLayout(FoldersListAndPreview); break; } case IDM_LAYOUT2: { LVariant v; int TwoThirds = GetClient().Y() >> 1; GetOptions()->SetValue(OPT_SplitterPos, v = TwoThirds); GetOptions()->SetValue(OPT_SubSplitPos, v = 200); SetLayout(PreviewOnBottom); break; } case IDM_LAYOUT3: { LVariant v; GetOptions()->SetValue(OPT_SplitterPos, v = 200); SetLayout(FoldersAndList); break; } case IDM_LAYOUT4: { LVariant v; GetOptions()->SetValue(OPT_SplitterPos, v = 200); GetOptions()->SetValue(OPT_SubSplitPos, v); SetLayout(ThreeColumn); break; } case IDM_CRASH: { int *Crash = 0; *Crash = true; break; } case IDM_DUMP_MEM: { LDumpMemoryStats(0); break; } case IDM_SCRIPT_DEBUG: { LVariant v; if (GetOptions()) GetOptions()->SetValue(OPT_ScriptDebugger, v = true); LVirtualMachine *vm = new LVirtualMachine(d); if (!vm) break; LVmDebugger *dbg = vm->OpenDebugger(); if (!dbg) break; break; } case IDM_SCRIPT_BREAK_ON_WARN: { auto mi = GetMenu()->FindItem(IDM_SCRIPT_BREAK_ON_WARN); if (!mi) break; LVirtualMachine::BreakOnWarning = !mi->Checked(); mi->Checked(LVirtualMachine::BreakOnWarning); break; } case IDM_UNIT_TESTS: { #ifdef _DEBUG UnitTests([this](auto ok) { LgiMsg(this, "UnitTest status: %i", AppName, MB_OK, ok); }); #endif break; } // Help menu case IDM_HELP: { LaunchHelp("index.html"); // LgiMsg(this, LLoadString(IDS_ERROR_NO_HELP), AppName, MB_OK); break; } case IDM_FEEDBACK: { LVariant e; if (GetOptions()->GetValue("author", e)) CreateMail(0, e.Str()); else CreateMail(0, AuthorEmailAddr); break; } case IDM_MEMECODE: { LExecute(AuthorHomepage); break; } case IDM_HOMEPAGE: { LVariant hp; if (GetOptions()->GetValue("homepage", hp)) LExecute(hp.Str()); else LExecute(ApplicationHomepage); break; } case IDM_VERSION_HISTORY: { LExecute("http://www.memecode.com/site/ver.php?id=445"); break; } case IDM_DEBUG_INFO: { char s[256]; sprintf_s(s, sizeof(s), "%s#debug", ApplicationHomepage); LExecute(s); break; } case IDM_TUTORIALS: { LExecute("http://www.memecode.com/scribe/tutorials"); break; } case IDM_INSCRIBE_LINK: { LExecute(CommercialHomepage); break; } case IDM_SCRIBE_FAQ: { LExecute(FaqHomepage); break; } case IDM_ABOUT: { extern void ScribeAbout(ScribeWnd *Parent); ScribeAbout(this); break; } default: { if (d->ScriptToolbar) d->ScriptToolbar->ExecuteCallbacks(this, 0, 0, Cmd); break; } } return 0; } void ScribeWnd::OnDelete() { LVariant ConfirmDelete; GetOptions()->GetValue(OPT_ConfirmDelete, ConfirmDelete); if (!ConfirmDelete.CastInt32() || LgiMsg(this, LLoadString(IDS_DELETE_ASK), AppName, MB_YESNO) == IDYES) { LArray Del; if (Tree && Tree->Focus()) { ScribeFolder *Item = dynamic_cast(Tree->Selection()); if (Item) { Tree->OnDelete(Item, false); } } else if (MailList #ifdef MAC && MailList->Focus() #endif ) { List Sel; MailList->GetSelection(Sel); for (auto i: Sel) { Thing *t = dynamic_cast(i); if (t) Del.Add(t->GetObject()); } if (Del.Length()) { auto Store = Del[0]->GetStore(); Store->Delete(Del, true); } else LgiTrace("%s:%i - Nothing to delete\n", _FL); #ifndef MAC MailList->Focus(true); #endif } } } int ScribeWnd::OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDC_THING_LIST: { if (n.Type == LNotifyReturnKey) { LListItem *i = MailList ? MailList->GetSelected() : 0; Thing *t = dynamic_cast(i); if (t) { t->DoUI(); } } else if (n.Type == LNotifyDeleteKey) { /* This is now handled by the menu OnDelete(); return true; */ } if (SearchView && MailList) { SearchView->OnNotify(Ctrl, n); } break; } case IDC_TEXT: { if (PreviewPanel) { PreviewPanel->OnNotify(Ctrl, n); } break; } } return 0; } void ScribeWnd::AddThingSrc(ScribeFolder *src) { if (!d->ThingSources.HasItem(src)) d->ThingSources.Add(src); } void ScribeWnd::RemoveThingSrc(ScribeFolder *src) { d->ThingSources.Delete(src); } LArray ScribeWnd::GetThingSources(Store3ItemTypes Type) { LArray a; for (auto f: d->ThingSources) { if (f->GetItemType() == Type && !f->IsInTrash()) { a.Add(f); } } return a; } bool ScribeWnd::LogFilterActivity() { auto i = Menu->FindItem(IDM_DEBUG_FILTERS); return i ? i->Checked() : false; } bool ScribeWnd::CreateFolders(LAutoString &FileName) { bool Status = false; if (FileName) { char *Ext = LGetExtension(FileName); if (!Ext) { char File[300]; strcpy_s(File, sizeof(File), FileName); strcat(File, ".mail3"); FileName.Reset(NewStr(File)); } // Create objects, and then close the file.. it'll be reloaded later LAutoPtr m(CreateDataStore(FileName, true)); if (m) { m->GetRoot(true); Status = true; } else LgiTrace("%s:%i - CreateDataStore failed.\n", _FL); } else LgiTrace("%s:%i - No file name for CreateFolder.\n", _FL); return Status; } bool ScribeWnd::CompactFolders(LMailStore &Store, bool Interactive) { if (!Store.Store) return false; auto Dlg = new Store3Progress(this, Interactive); Dlg->SetDescription(LLoadString(IDS_CHECKING_OBJECTS)); bool Offline = false; if (WorkOffline) { Offline = WorkOffline->Checked(); WorkOffline->Checked(true); } Store.Store->Compact(this, Dlg, [this, Offline, Dlg](auto status) { LAssert(InThread()); if (WorkOffline) WorkOffline->Checked(Offline); delete Dlg; }); return true; } CalendarSource *CalendarSource::Create(ScribeWnd *App, const char *ObjName, const char *Id) { if (!Stricmp(ObjName, "RemoteCalendarSource")) return new RemoteCalendarSource(App, Id); return new FolderCalendarSource(App, Id); } int ScribeWnd::GetCalendarSources(LArray &Out) { static bool Loaded = false; if (!Loaded) { Loaded = true; CalendarSource::SetCreateIn(NULL); LVariant Create; GetOptions()->GetValue(OPT_CalendarCreateIn, Create); // This should be a list of all calendar folders in ANY mail store... auto CalFlds = GetThingSources(MAGIC_CALENDAR); LXmlTag *t = GetOptions()->LockTag(OPT_CalendarSources, _FL); if (t) { bool AutoPopulate = t->Children.Length() == 0; for (auto c: t->Children) { auto s = CalendarSource::Create(this, c->GetAttr(CalendarSource::OptObject), c->GetTag()); if (s && s->Read()) { // Add known source... if (!Stricmp(Create.Str(), c->GetTag())) { CalendarSource::SetCreateIn(s); } // Remove from CalFlds FolderCalendarSource *Fcs = dynamic_cast(c); if (Fcs) { auto Path = Fcs->GetPath(); for (auto c: CalFlds) { if (c->GetPath().Equals(Path)) { CalFlds.Delete(c); break; } } } } } if (AutoPopulate) { // Now CalFlds should be a list of all calendar folders NOT in the source XML tag for (auto c: CalFlds) { FolderCalendarSource *s = new FolderCalendarSource(this); if (s) { // So add an entry to track it... auto Path = c->GetPath(); s->SetPath(Path); s->SetDisplay(true); s->SetColour(CalendarSource::FindUnusedColour()); s->Write(); } } } GetOptions()->Unlock(); } if (!CalendarSource::GetCreateIn() && CalendarSource::GetSources().Length()) { CalendarSource::SetCreateIn(CalendarSource::GetSources().ItemAt(0)); } } for (unsigned i=0; iPathOption; fi++) { bool Check = true; if (fi->HasOption) { LVariant v; if (GetOptions()->GetValue(fi->HasOption, v)) Check = v.CastInt32() != 0; } if (Check) { ScribeFolder *c = GetFolder(fi->Id); if (c == f) return fi->Id; } } return -1; } ScribeFolder *ScribeWnd::GetCurrentFolder() { if (Tree) { auto *Item = Tree->Selection(); if (Item) { return dynamic_cast(Item); } } return 0; } bool ScribeWnd::GetSystemPath(int Folder, LVariant &Path) { char KeyName[64]; sprintf_s(KeyName, sizeof(KeyName), "Folder-%i", Folder); return GetOptions()->GetValue(KeyName, Path); } LMailStore *ScribeWnd::GetMailStoreForIdentity(const char *IdEmail) { LVariant Tmp; if (!IdEmail) { // Get current identity ScribeAccount *Cur = GetCurrentAccount(); if (Cur) { Tmp = Cur->Identity.Email(); IdEmail = Tmp.Str(); } } if (!IdEmail) return NULL; ScribeAccount *a = NULL; for (auto Acc : Accounts) { LVariant e = Acc->Identity.Email(); if (e.Str() && !_stricmp(e.Str(), IdEmail)) { a = Acc; break; } } if (!a) return NULL; LVariant DestPath = a->Receive.DestinationFolder(); if (!DestPath.Str()) return NULL; return GetMailStoreForPath(DestPath.Str()); } ScribeFolder *ScribeWnd::GetFolder(int Id, LDataI *s) { if (s) { for (auto &f: Folders) if (s->GetStore() == f.Store) return GetFolder(Id, &f); } return GetFolder(Id); } ScribeFolder *ScribeWnd::GetFolder(int Id, LMailStore *Store, bool Quiet) { char KeyName[64]; sprintf_s(KeyName, sizeof(KeyName), "Folder-%i", Id); LVariant FolderName; bool NoOption = false; if (GetOptions()->GetValue(KeyName, FolderName)) { if (ValidStr(FolderName.Str()) && strlen(FolderName.Str()) > 0) { ScribeFolder *c = GetFolder(FolderName.Str(), Store); if (c) { return c; } else if (!Quiet) { LgiTrace("%s:%i - '%s' doesn't exist.\n", _FL, FolderName.Str()); } } } else if (!Quiet) { // LgiTrace("%s:%i - No option '%s'\n", _FL, KeyName); NoOption = true; } switch (Id) { case FOLDER_INBOX: case FOLDER_OUTBOX: case FOLDER_SENT: case FOLDER_TRASH: case FOLDER_CONTACTS: case FOLDER_TEMPLATES: case FOLDER_FILTERS: case FOLDER_CALENDAR: case FOLDER_GROUPS: case FOLDER_SPAM: { ScribeFolder *c = GetFolder(DefaultFolderNames[Id], Store); if (!c) { // if (!Quiet) // LgiTrace("%s:%i - Default folder '%s' doesn't exist.\n", _FL, DefaultFolderNames[Id]); } else if (NoOption) { auto p = c->GetPath(); GetOptions()->SetValue(KeyName, FolderName = p.Get()); } return c; } } return NULL; } bool ScribeWnd::OnMailStore(LMailStore **MailStore, bool Add) { if (!MailStore) { LAssert(!"No mail store pointer?"); return false; } if (Add) { *MailStore = &Folders.New(); if (*MailStore) return true; } else { ssize_t Idx = *MailStore - &Folders[0]; if (Idx >= 0 && Idx < (ssize_t)Folders.Length()) { Folders.DeleteAt(Idx, true); *MailStore = NULL; return true; } else { LAssert(!"Index out of range."); } } return false; } LMailStore *ScribeWnd::GetMailStoreForPath(const char *Path) { if (!Path) return NULL; auto t = LString(Path).SplitDelimit("/"); if (t.Length() > 0) { const char *First = t[0]; // Find the mail store that that t[0] refers to for (unsigned i=0; iGetText(); if (RootStr && !_stricmp(RootStr, First)) { return &Folders[i]; } } } } return NULL; } ScribeFolder *ScribeWnd::GetFolder(const char *Name, LMailStore *s) { ScribeFolder *Folder = 0; if (ValidStr(Name)) { LString Sep("/"); auto t = LString(Name).Split(Sep); LMailStore tmp; LString TmpName; if (t.Length() > 0) { if (!s) { s = GetMailStoreForPath(Name); if (!s) { // IMAP folders? for (auto a: Accounts) { ScribeProtocol Proto = a->Receive.ProtocolType(); if (Proto == ProtocolImapFull) { ScribeFolder *Root = a->Receive.GetRootFolder(); if (Root) { const char *RootStr = Root->GetText(); if (RootStr && a->Receive.GetDataStore() && !_stricmp(RootStr, t[0])) { tmp.Root = Root; tmp.Store = a->Receive.GetDataStore(); s = &tmp; break; } } } } } if (s) { if (*Name == '/') Name++; Name = strchr(Name, '/'); if (!Name) Name = "/"; } } else if (s->Root) { // Check if the store name is on the start of the folder auto RootName = s->Root->GetName(true); if (RootName.Equals(t[0])) { LString::Array a; for (unsigned i=1; iRoot; Folder = s->Root ? s->Root->GetSubFolder(Name) : 0; } } return Folder; } void ScribeWnd::Update(int What) { if (What & UPDATE_TREE) { Tree->Invalidate(); return; } if (What & UPDATE_LIST) { if (MailList) MailList->Invalidate(); return; } } void ScribeWnd::DoDebug(char *s) { } Thing *ScribeWnd::CreateThingOfType(Store3ItemTypes Type, LDataI *obj) { Thing *t = NULL; switch (Type) { case MAGIC_CONTACT: { t = new Contact(this, obj); break; } case MAGIC_MAIL: { t = new Mail(this, obj); break; } case MAGIC_ATTACHMENT: { t = new Attachment(this, obj); break; } case MAGIC_FILTER: { t = new Filter(this, obj); break; } case MAGIC_CALENDAR: { t = new Calendar(this, obj); break; } case MAGIC_GROUP: { t = new ContactGroup(this, obj); break; } default: break; } if (t) { t->App = this; } return t; } void ScribeWnd::GetFilters(List &Filters, bool JustIn, bool JustOut, bool JustInternal) { auto Srcs = GetThingSources(MAGIC_FILTER); for (auto f: Srcs) { for (auto t: f->Items) { Filter *Ftr = t->IsFilter(); if (Ftr) { if (JustIn && !Ftr->GetIncoming()) continue; if (JustOut && !Ftr->GetOutgoing()) continue; if (JustInternal && !Ftr->GetInternal()) continue; Filters.Insert(Ftr); } } } extern int FilterCompare(Filter *a, Filter *b, NativeInt Data); Filters.Sort(FilterCompare); } bool ScribeWnd::ShowToolbarText() { LVariant i; if (GetOptions()->GetValue(OPT_ToolbarText, i)) { return i.CastInt32() != 0; } GetOptions()->SetValue(OPT_ToolbarText, i = true); return true; } void ScribeWnd::HashContacts(LHashTbl,Contact*> &Contacts, ScribeFolder *Folder, bool Deep) { if (!Folder) { // Default item is the contacts folder Folder = GetFolder(FOLDER_CONTACTS); // Also look at all the contact sources... auto Srcs = GetThingSources(MAGIC_CONTACT); for (auto Src: Srcs) { for (auto t: Src->Items) { Contact *c = t->IsContact(); if (!c) continue; auto emails = c->GetEmails(); for (auto e: emails) { if (!Contacts.Find(e)) Contacts.Add(e, c); } } } } // recurse through each folder and make a list // of every contact object we find. if (Folder) { Folder->LoadThings(); for (auto t: Folder->Items) { Contact *c = t->IsContact(); if (c) { auto Emails = c->GetEmails(); for (auto e: Emails) if (e && !Contacts.Find(e)) Contacts.Add(e, c); } } for (auto f = Folder->GetChildFolder(); Deep && f; f = f->GetNextFolder()) { HashContacts(Contacts, f, Deep); } } } List *ScribeWnd::GetEveryone() { return &Contact::Everyone; } bool ScribeWnd::GetContacts(List &Contacts, ScribeFolder *Folder, bool Deep) { LArray Folders; if (!Folder) { Folders = GetThingSources(MAGIC_CONTACT); auto f = GetFolder(FOLDER_CONTACTS); if (f && !Folders.HasItem(f)) Folders.Add(f); } else Folders.Add(Folder); if (!Folders.Length()) return false; for (auto f: Folders) { // recurse through each folder and make a list // of every contact object we find. ScribePerm Perm = f->GetFolderPerms(ScribeReadAccess); bool Safe = CurrentAuthLevel >= Perm; if (Safe) { f->LoadThings(); for (auto t: f->Items) { Contact *c = t->IsContact(); if (c) Contacts.Insert(c); } for (ScribeFolder *c = f->GetChildFolder(); Deep && c; c = c->GetNextFolder()) GetContacts(Contacts, c, Deep); } } return true; } /* This function goes through the database and checks for some basic requirements and fixes things up if they aren't ok. */ bool ScribeWnd::ValidateFolder(LMailStore *s, int Id) { char OptName[32]; sprintf_s(OptName, sizeof(OptName), "Folder-%i", Id); LVariant Path; if (!GetOptions()->GetValue(OptName, Path)) { char Opt[256]; sprintf_s(Opt, sizeof(Opt), "/%s", DefaultFolderNames[Id]); GetOptions()->SetValue(OptName, Path = Opt); } // If the path name has the store name at the start, strip that off... LString Sep("/"); LString::Array Parts = LString(Path.Str()).Split(Sep); if (Parts.Length() > 1) { if (Parts[0].Equals(s->Name)) { Parts.DeleteAt(0, true); Path = Sep.Join(Parts); } else { LMailStore *ms = GetMailStoreForPath(Path.Str()); if (ms) { s = ms; } else { // Most likely the user has renamed something and broken the // path. Lets just error out instead of creating the wrong folder return false; } } } // Now resolve the path... ScribeFolder *Folder = GetFolder(Path.Str(), s); if (!Folder) { char *p = Path.Str(); if (_strnicmp(p, "/IMAP ", 6) != 0) { LAssert(DefaultFolderTypes[Id] != MAGIC_NONE); Folder = s->Root->CreateSubFolder(*p=='/'?p+1:p, DefaultFolderTypes[Id]); } } if (!Folder) return false; Folder->SetDefaultFields(); return true; } void ScribeWnd::Validate(LMailStore *s) { // Check for all the basic folders int Errors = 0; for (SystemFolderInfo *fi = SystemFolders; fi->PathOption; fi++) { bool Check = true; if (fi->HasOption) { LVariant v; if (GetOptions()->GetValue(fi->HasOption, v)) Check = v.CastInt32() != 0; } if (Check) { if (!ValidateFolder(s, fi->Id)) Errors++; } } if (Errors && LgiMsg(this, "There were errors validating the system folders." "Would you like to review the mail store's system folder paths?", AppName, MB_YESNO) == IDYES) { PostEvent(M_COMMAND, IDM_MANAGE_MAIL_STORES); } } ThingFilter *ScribeWnd::GetThingFilter() { return SearchView; } ScribeAccount *ScribeWnd::GetSendAccount() { LVariant DefSendAcc = 0; if (!GetOptions()->GetValue(OPT_DefaultSendAccount, DefSendAcc)) { for (auto a : Accounts) if (a->Send.Server().Str()) return a; } ScribeAccount *i = Accounts.ItemAt(DefSendAcc.CastInt32()); if (i && i->Send.Server().Str()) return i; return NULL; } LPrinter *ScribeWnd::GetPrinter() { if (!d->PrintOptions) d->PrintOptions.Reset(new LPrinter); return d->PrintOptions; } int ScribeWnd::GetActiveThreads() { int Status = 0; for (auto i: Accounts) { if (i->IsOnline()) { Status++; } } return Status; } class DefaultClientDlg : public LDialog { public: bool DontWarn; DefaultClientDlg(LView *parent) { DontWarn = false; SetParent(parent); LoadFromResource(IDD_WARN_DEFAULT); MoveToCenter(); } int OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case ID_YES: case ID_NO: { LCheckBox *DW; if (GetViewById(IDC_DONT_WARN, DW)) { DontWarn = DW->Value() != 0; } EndModal(Ctrl->GetId() == ID_YES); break; } } return 0; } }; #if WINNATIVE struct DefaultClient { char DefIcon[MAX_PATH_LEN]; char CmdLine[MAX_PATH_LEN]; char DllPath[MAX_PATH_LEN]; DefaultClient() { auto Exe = LGetExeFile(); sprintf_s(DefIcon, sizeof(DefIcon), "%s,1", Exe.Get()); sprintf_s(CmdLine, sizeof(CmdLine), "\"%s\" /m \"%%1\"", Exe.Get()); LMakePath(DllPath, sizeof(DllPath), Exe, "../ScribeMapi.dll"); } bool IsWindowsXp() { LArray Ver; int Os = LGetOs(&Ver); if ( ( Os == LGI_OS_WIN32 || Os == LGI_OS_WIN64 ) && Ver.Length() > 1 && Ver[0] == 5 && Ver[1] == 1 ) return true; return false; } bool InstallMailto(bool Write) { LAutoPtr mailto = CheckKey(Write, "HKCR\\mailto"); if (!mailto) return false; if (!CheckString(Write, mailto, NULL, "URL:MailTo Protocol")) return false; LAutoPtr deficon = CheckKey(Write, "HKCR\\mailto\\DefaultIcon"); if (!deficon) return false; if (!CheckString(Write, deficon, NULL, DefIcon)) return false; LAutoPtr shell = CheckKey(Write, "HKCR\\mailto\\shell"); if (!shell) return false; if (!CheckString(Write, shell, NULL, "open")) return false; LAutoPtr cmd = CheckKey(Write, "HKCR\\mailto\\shell\\open\\command"); if (!cmd) return false; if (!CheckString(Write, cmd, NULL, CmdLine)) return false; return true; } LAutoPtr CheckKey(bool Write, const char *Key, ...) const { char Buffer[512]; va_list Arg; va_start(Arg, Key); vsprintf_s(Buffer, sizeof(Buffer), Key, Arg); va_end(Arg); LAutoPtr k(new LRegKey(Write, Buffer)); if (k && Write && !k->IsOk()) { if (!k->Create()) { k.Reset(); LgiTrace("%s:%i - Failed to create '%s'\n", _FL, Buffer); } } return k; } bool CheckInt(bool Write, LRegKey *k, const char *Name, uint32_t Value) { if (!k) { LgiTrace("%s:%i - No key: '%s'\n", _FL, Name); return false; } uint32_t Cur; if (!k->GetInt(Name, Cur)) Cur = Value + 1; if (Cur == Value) return true; if (Write) { bool Status = k->SetInt(Name, Value); if (!Status) LgiTrace("%s:%i - Failed to set key '%s': '%s' to %i\n", _FL, k->Name(), Name, Value); return Status; } return false; } bool CheckString(bool Write, LRegKey *k, const char *StrName, const char *StrValue) { if (!k) { LgiTrace("%s:%i - No key: '%s' to '%s'\n", _FL, StrName, StrValue); return false; } LString v; if (k->GetStr(StrName, v)) { bool Same = Stricmp(v.Get(), StrValue) == 0; if (Write && !Same) { bool Status = k->SetStr(StrName, StrValue); if (!Status) LgiTrace("%s:%i - Failed to set key '%s': '%s' to '%s'\n", _FL, k->Name(), StrName, StrValue); return Status; } return Same; } else if (Write) { bool Status = k->SetStr(StrName, StrValue); if (!Status) LgiTrace("%s:%i - Failed to set key '%s': '%s' to '%s'\n", _FL, k->Name(), StrName, StrValue); return Status; } return false; } bool IsDefault() { LAutoPtr mail = CheckKey(false, "HKCU\\Software\\Clients\\Mail"); if (!mail) return false; LString v; if (!mail->GetStr(NULL, v)) return false; return !_stricmp(v, "Scribe"); } bool SetDefault() const { LAutoPtr mail = CheckKey(true, "HKCU\\Software\\Clients\\Mail"); if (!mail) return false; // Set the default client in the current user tree. mail->SetStr(NULL, "Scribe"); // Configure the mailto handler const char *Base = "HKEY_ROOT"; bool Error = false; LRegKey Mt(true, "%s\\mailto", Base); if (Mt.IsOk() || Mt.Create()) { if (!Mt.SetStr(0, "URL:MailTo Protocol") || !Mt.SetStr("URL Protocol", "")) Error = true; } else { LgiTrace("%s:%i - Couldn't open/create registry key (err=%i).\n", _FL, GetLastError()); Error = true; } LRegKey Di(true, "%s\\mailto\\DefaultIcon", Base); if (Di.IsOk() || Di.Create()) { if (!Di.SetStr(0, DefIcon)) Error = true; } else { LgiTrace("%s:%i - Couldn't open/create registry key (err=%i).\n", _FL, GetLastError()); Error = true; } LRegKey c(true, "%s\\mailto\\shell\\open\\command", Base); if (c.IsOk() || c.Create()) { if (!c.SetStr(NULL, CmdLine)) Error = true; } else { LgiTrace("%s:%i - Couldn't open/create registry key (err=%i).\n", _FL, GetLastError()); Error = true; } return Error; } bool InstallAsClient(char *Base, bool Write) { // Create software client entry, to put Scribe in the Internet Options for mail clients. LAutoPtr mail = CheckKey(Write, "%s\\Software\\Clients\\Mail", Base); if (!mail) return false; LAutoPtr app = CheckKey(Write, "%s\\Software\\Clients\\Mail\\Scribe", Base); if (!app) return false; if (!CheckString(Write, app, NULL, AppName)) return false; if (!CheckString(Write, app, "DllPath", DllPath)) return false; LAutoPtr shell = CheckKey(Write, "%s\\Software\\Clients\\Mail\\Scribe\\shell\\open\\command", Base); if (!shell) return false; if (!CheckString(Write, shell, NULL, CmdLine)) return false; LAutoPtr icon = CheckKey(Write, "%s\\Software\\Clients\\Mail\\Scribe\\DefaultIcon", Base); if (!icon) return false; if (!CheckString(Write, icon, NULL, DefIcon)) return false; LAutoPtr proto = CheckKey(Write, "%s\\Software\\Classes\\Protocol\\mailto", Base); if (!proto) return false; if (!CheckString(Write, proto, NULL, "URL:MailTo Protocol")) return false; if (!CheckString(Write, proto, "URL Protocol", "")) return false; if (!CheckInt(Write, proto, "EditFlags", 0x2)) return false; LAutoPtr proto_cmd = CheckKey(Write, "%s\\Software\\Classes\\Protocol\\mailto\\shell\\open\\command", Base); if (!proto_cmd) return false; if (!CheckString(Write, proto_cmd, NULL, CmdLine)) return false; return true; } struct FileType { char *Name; char *Desc; int Icon; }; static FileType FileTypes[]; bool Win7Install(bool Write) { // http://msdn.microsoft.com/en-us/library/windows/desktop/cc144154%28v=vs.85%29.aspx LArray Ver; int Os = LGetOs(&Ver); if ( ( Os == LGI_OS_WIN32 || Os == LGI_OS_WIN64 ) && Ver[0] >= 6) { char Path[MAX_PATH_LEN]; auto Exe = LGetExeFile(); for (int i=0; FileTypes[i].Name; i++) { LAutoPtr base = CheckKey(Write, "HKEY_CLASSES_ROOT\\%s", FileTypes[i].Name); if (!base) return false; if (!CheckString(Write, base, NULL, FileTypes[i].Desc)) return false; LAutoPtr r = CheckKey(Write, "HKEY_CLASSES_ROOT\\%s\\shell\\Open\\command", FileTypes[i].Name); if (!r) return false; sprintf_s(Path, sizeof(Path), "\"%s\" -u \"%%1\"", Exe.Get()); if (!CheckString(Write, r, NULL, Path)) return false; LAutoPtr ico = CheckKey(Write, "HKEY_CLASSES_ROOT\\%s\\DefaultIcon", FileTypes[i].Name); if (!ico) return false; sprintf_s(Path, sizeof(Path), "%s,%i", Exe.Get(), FileTypes[i].Icon); if (!CheckString(Write, ico, NULL, Path)) return false; } LAutoPtr r = CheckKey(Write, "HKEY_LOCAL_MACHINE\\SOFTWARE\\Clients\\Mail\\Scribe\\Capabilities"); if (!r) return false; if (!CheckString(Write, r, "ApplicationDescription", "Scribe is a small lightweight email client.") && !CheckString(Write, r, "ApplicationName", "Scribe") && !CheckString(Write, r, "ApplicationIcon", DefIcon)) return false; LAutoPtr as = CheckKey(Write, "HKEY_LOCAL_MACHINE\\SOFTWARE\\Clients\\Mail\\Scribe\\Capabilities\\FileAssociations"); if (!as) return false; if (!CheckString(Write, as, ".eml", "Scribe.Email") && !CheckString(Write, as, ".msg", "Scribe.Email") && !CheckString(Write, as, ".mbox", "Scribe.Folder") && !CheckString(Write, as, ".mbx", "Scribe.Folder") && !CheckString(Write, as, ".ics", "Scribe.Calendar") && !CheckString(Write, as, ".vcs", "Scribe.Calendar") && !CheckString(Write, as, ".vcf", "Scribe.Contact") && !CheckString(Write, as, ".mail3", "Scribe.MailStore")) return false; LAutoPtr ua = CheckKey(Write, "HKEY_LOCAL_MACHINE\\SOFTWARE\\Clients\\Mail\\Scribe\\Capabilities\\UrlAssociations"); if (!ua) return false; if (!CheckString(Write, ua, "mailto", "Scribe.Mailto")) return false; LAutoPtr a = CheckKey(Write, "HKEY_LOCAL_MACHINE\\SOFTWARE\\RegisteredApplications"); if (!a) return false; if (!CheckString(Write, a, "Scribe", "SOFTWARE\\Clients\\Mail\\Scribe\\Capabilities")) return false; } return true; } void Win7Uninstall() { for (int i=0; FileTypes[i].Name; i++) { LRegKey base(true, "HKEY_CLASSES_ROOT\\%s", FileTypes[i].Name); base.DeleteKey(); } } }; DefaultClient::FileType DefaultClient::FileTypes[] = { { "Scribe.Email", "Email", 2 }, { "Scribe.Folder", "Mailbox", 0 }, { "Scribe.Calendar", "Calendar Event", 6 }, { "Scribe.Contact", "Contact", 4 }, { "Scribe.MailStore", "Mail Store", 0 }, { "Scribe.Mailto", "Mailto Protocol", 0 }, { 0, 0 } }; #endif void ScribeWnd::SetDefaultHandler() { #if WINNATIVE if (LAppInst->GetOption("noreg")) return; LVariant RegisterClient; if (!GetOptions()->GetValue(OPT_RegisterWindowsClient, RegisterClient)) RegisterClient = true; if (!RegisterClient.CastInt32()) return; // Create IE mail client entries for local machine and current user DefaultClient Def; bool OldAssert = LRegKey::AssertOnError; LRegKey::AssertOnError = false; bool RegistryOk = ( !Def.IsWindowsXp() || Def.InstallMailto(true) ) && Def.InstallAsClient("HKLM", true) && Def.Win7Install(true); LRegKey::AssertOnError = OldAssert; if (!RegistryOk) { // Need write permissions to fix up the registry? NeedsCapability("RegistryWritePermissions"); return; } // Check if the user wants us to be the default client LVariant n = true; GetOptions()->GetValue(OPT_CheckDefaultEmail, n); if (n.CastInt32()) { // HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\mailto\UserChoice LRegKey::AssertOnError = false; bool IsDef = Def.IsDefault(); if (!IsDef) { // Ask the user... auto Dlg = new DefaultClientDlg(this); Dlg->DoModal([this, Dlg, Def, OldAssert](auto dlg, auto id) { if (id) { auto Error = !Def.SetDefault(); LVariant v; GetOptions()->SetValue(OPT_CheckDefaultEmail, v = (int) (!Dlg->DontWarn)); OnSetDefaultHandler(Error, OldAssert); } delete dlg; }); } else OnSetDefaultHandler(false, OldAssert); } #endif } void ScribeWnd::OnSetDefaultHandler(bool Error, bool OldAssert) { #if WINDOWS LRegKey::AssertOnError = OldAssert; #endif if (Error) NeedsCapability("RegistryWritePermissions"); } void ScribeWnd::OnSelect(List *l, bool ChangeEvent) { Mail *m = (l && l->Length() == 1) ? (*l)[0]->IsMail() : 0; if (Commands) { bool NotCreated = m && !TestFlag(m->GetFlags(), MAIL_CREATED); Commands->SetCtrlEnabled(IDM_DELETE, l && l->Length() > 0); Commands->SetCtrlEnabled(IDM_DELETE_AS_SPAM, l && l->Length() > 0); Commands->SetCtrlEnabled(IDM_PRINT, l && l->Length() == 1); Commands->SetCtrlEnabled(IDM_REPLY, NotCreated); Commands->SetCtrlEnabled(IDM_REPLY_ALL, NotCreated); Commands->SetCtrlEnabled(IDM_FORWARD, m != 0); Commands->SetCtrlEnabled(IDM_BOUNCE, m != 0); } if (PreviewPanel && GetEffectiveLayoutMode() != 3) { if (!PreviewPanel->IsAttached()) { SetItemPreview(PreviewPanel); } Thing *t = (l && l->Length() == 1) ? (*l)[0] : 0; PreviewPanel->OnThing(t, ChangeEvent); /* if (d->Debug) d->Debug->OnThing(t); */ } } class SpellErrorInst { public: int Id; LString Word; LString::Array Suggestions; SpellErrorInst(int id) { Id = id; // Decor = LCss::TextDecorSquiggle; // DecorColour.Rgb(255, 0, 0); } ~SpellErrorInst() { } bool OnMenu(LSubMenu *m) { if (Suggestions.Length()) { for (unsigned i=0; iAppendItem(Suggestions[i], 100 + i, true); } m->AppendSeparator(); } char Buf[256]; sprintf_s(Buf, sizeof(Buf), LLoadString(IDS_ADD_TO_DICTIONARY, "Add '%s' to dictionary"), Word.Get()); m->AppendItem(Buf, 1, true); return true; } void OnMenuClick(int i) { if (i == 1) { // Add to dictionary... /* if (PostThreadEvent(SpellHnd, M_ADD_WORD, (LMessage::Param) new LString(Word))) { // FIXME LAssert(!"Impl me."); // View->PostEvent(M_DELETE_STYLE, (LMessage::Param) dynamic_cast(this)); } */ } else if (i >= 100 && i < 100 + (int)Suggestions.Length()) { // Change spelling.. char *Replace = Suggestions[i - 100]; if (Replace) { char16 *w = Utf8ToWide(Replace); if (w) { /* int NewLen = StrlenW(w); if (NewLen > Len) { // Bigger... memcpy(View->NameW() + Start, w, Len * sizeof(char16)); View->Insert(Start + Len, w + Len, NewLen - Len); } else if (NewLen < Len) { // Smaller... memcpy(View->NameW() + Start, w, NewLen * sizeof(char16)); View->Delete(Start + NewLen, Len - NewLen); } else { // Just copy... memcpy(View->NameW() + Start, w, Len * sizeof(char16)); RefreshLayout(Start, Len); } */ DeleteArray(w); } } } } }; class MailTextView : public LTextView3 { ScribeWnd *App; LSpellCheck *Thread; LColour c[8]; LHashTbl, SpellErrorInst*> ErrMap; SpellErrorInst *NewErrorInst() { int Id; while (ErrMap.Find(Id = LRand(10000))) ; SpellErrorInst *Inst = new SpellErrorInst(Id); if (!Inst) return NULL; ErrMap.Add(Id, Inst); return Inst; } public: MailTextView(ScribeWnd *app, int Id, int x, int y, int cx, int cy, LFontType *FontType) : LTextView3(Id, x, y, cx, cy, FontType) { App = app; Thread = 0; int i=0; c[i++].Rgb(0x80, 0, 0); c[i++].Rgb(0, 0x80, 0); c[i++].Rgb(0, 0, 0x80); c[i++].Rgb(0x80, 0x80, 0); c[i++].Rgb(0x80, 0, 0x80); c[i++].Rgb(0, 0x80, 0x80); c[i++].Rgb(0x80, 0x80, 0x80); c[i++].Rgb(0xc0, 0xc0, 0xc0); for (i=0; i 0 && !StrchrW(SpellDelim, Text[Start-1])) Start--; if (Len > 0) { // Text being added Len += Origin - Start; while ((ssize_t)Start + Len < Size && !StrchrW(SpellDelim, Text[Start + Len])) Len++; } else if (Len < 0) { // Text being deleted Len = Origin - Start; while ((ssize_t)Start + Len < Size && !StrchrW(SpellDelim, Text[Start + Len])) Len++; } if (!Thread) Thread = App->GetSpellThread(); if (Thread && Len > 0) { LString Str(Text+Start, Len); LArray Params; Thread->Check(AddDispatch(), Str, Start, Len, &Params); } // Adjust all the positions of the styles after this. for (auto s = Style.begin(); s != Style.end(); ) { if (s->Start >= Origin && s->Owner == 1) { if (Length < 0 && s->Start < Origin - Length) { // In the deleted text... Style.Delete(s); continue; } // After the deleted text s->Start += Length; LAssert(s->Start >= 0); } s++; } } } void PourText(size_t Start, ssize_t Len) { LTextView3::PourText(Start, Len); for (auto l: Line) { int n=0; char16 *t = Text + l->Start; char16 *e = t + l->Len; while ((*t == ' ' || *t == '>') && t < e) if (*t++ == '>') n++; if (n > 0) l->c = c[(n-1)%CountOf(c)]; } } LMessage::Result OnEvent(LMessage *m) { switch (m->Msg()) { case M_CHECK_TEXT: { LAutoPtr Ct((LSpellCheck::CheckText*)m->A()); if (!Ct || !Thread) break; // Clear existing spelling error styles ssize_t Start = Ct->Start; ssize_t End = Start + Ct->Len; for (auto i = Style.begin(); i != Style.end(); ) { if (i->End() < (size_t)Start || i->Start >= End) { // Outside the area we are re-styling. i++; } else { if (i->Owner == STYLE_SPELLING) { // Existing error style inside the area Style.Delete(i); } else { // Existing non-error style... i++; } } } // Insert the new styles for (auto Ct: Ct->Errors) { SpellErrorInst *ErrInst = NewErrorInst(); LAutoPtr Style(new LTextView3::LStyle(STYLE_SPELLING)); if (Style && ErrInst) { Style->View = this; Style->Start = Ct.Start; Style->Len = Ct.Len; Style->Font = GetFont(); Style->Data = ErrInst->Id; Style->DecorColour = LColour::Red; Style->Decor = LCss::TextDecorSquiggle; ErrInst->Word = LString(Text + Style->Start, Style->End()); ErrInst->Suggestions = Ct.Suggestions; InsertStyle(Style); } } // Update the screen... Invalidate(); break; } case M_DELETE_STYLE: { /* LTextView3::LStyle *s = (LTextView3::LStyle*)m->A(); if (s && Style.HasItem(s)) { Style.Delete(s); Invalidate(); } else LAssert(0); */ break; } } return LTextView3::OnEvent(m); } bool OnStyleClick(LStyle *style, LMouse *m) { switch (style->Owner) { case STYLE_URL: { if (m->Left() && m->Down() && m->Double()) { LString s(Text + style->Start, style->Len); LUri u(s); if ( (u.sProtocol && !_stricmp(u.sProtocol, "mailto")) || LIsValidEmail(s) ) { Mailto m(App, s); Mail *email = App->CreateMail(); if (email) { m.Apply(email); email->DoUI(); return true; } } else { // Web link? LExecute(s); return true; } } break; } default: break; } return false; } }; LDocView *ScribeWnd::CreateTextControl(int Id, const char *MimeType, bool Editor, Mail *m) { LDocView *Ctrl = 0; // Get the default font LFontType FontType; bool UseFont = FontType.Serialize(GetOptions(), OPT_EditorFont, false); if (Editor) { if (!Stricmp(MimeType, sTextHtml)) { // Use the built in html editor LRichTextEdit *Rte; if ((Ctrl = Rte = new LRichTextEdit(Id))) { if (UseFont) Ctrl->SetFont(FontType.Create(), true); // Give the control the speller settings: LVariant Check, Lang, Dict; if (GetOptions()->GetValue(OPT_SpellCheck, Check) && Check.CastInt32() != 0) { if (GetOptions()->GetValue(OPT_SpellCheckLanguage, Lang)) Rte->SetValue(LDomPropToString(SpellCheckLanguage), Lang); if (GetOptions()->GetValue(OPT_SpellCheckDictionary, Dict)) Rte->SetValue(LDomPropToString(SpellCheckDictionary), Dict); // Set the spell thread: LSpellCheck *t = GetSpellThread(); if (t) Rte->SetSpellCheck(t); } } } else { // Use the built in plain text editor Ctrl = new MailTextView(this, Id, 0, 0, 200, 200, (UseFont) ? &FontType : 0); } } else { // Create a view only control for the mime type: LDocView *HtmlCtrl = NULL; if (!MimeType || !Stricmp(MimeType, sTextPlain) || !Stricmp(MimeType, sMultipartEncrypted)) { Ctrl = new MailTextView(this, Id, 0, 0, 200, 200, (UseFont) ? &FontType : 0); } else { HtmlCtrl = Ctrl = new Html1::LHtml(Id, 0, 0, 200, 200); } if (HtmlCtrl && UseFont) { LVariant LoadImg; if (GetOptions()->GetValue(OPT_HtmlLoadImages, LoadImg)) HtmlCtrl->SetLoadImages(LoadImg.CastInt32() != 0); HtmlCtrl->SetFont(FontType.Create(), true); } } if (Ctrl) { Ctrl->SetUrlDetect(true); Ctrl->SetAutoIndent(true); LVariant WrapOption; if (GetOptions()->GetValue(OPT_WordWrap, WrapOption)) { if (WrapOption.CastInt32()) { LVariant WrapCols = 80; GetOptions()->GetValue(OPT_WrapAtColumn, WrapCols); Ctrl->SetWrapAtCol(WrapCols.CastInt32()); } else { Ctrl->SetWrapAtCol(0); } } } return Ctrl; } void ScribeWnd::GrowlInfo(LString title, LString text) { LGrowl *g = d->GetGrowl(); if (!g) return; LAutoPtr n(new LGrowl::LNotify); n->Name = "info"; n->Title = title; n->Text = text; g->Notify(n); } void ScribeWnd::GrowlOnMail(Mail *m) { LVariant v; LAutoPtr n(new LGrowl::LNotify); n->Name = "new-mail"; n->Title = m->GetSubject(); int Len = 64; char sLen[16]; sprintf_s(sLen, sizeof(sLen), "%i", Len); if (m->GetVariant("BodyAsText", v, sLen)) { char *s = v.Str(); if (s) { int Words = 0; bool Lut[256]; memset(Lut, 0, sizeof(Lut)); Lut[(int)' '] = Lut[(int)'\t'] = Lut[(int)'\r'] = Lut[(int)'\n'] = true; char *c; for (c = s; *c && Words < 30; ) { while (*c && Lut[(int)*c]) c++; while (*c && !Lut[(int)*c]) c++; Words++; } n->Text.Set(s, c - s); } } LGrowl *g = d->GetGrowl(); if (g) { g->Notify(n); m->NewEmail = Mail::NewEmailTray; } } void ScribeWnd::OnNewMailSound() { static uint64 PrevTs = 0; auto Now = LCurrentTime(); if (Now - PrevTs > 30000) { PrevTs = Now; LVariant v; if (GetOptions()->GetValue(OPT_NewMailSoundFile, v) && LFileExists(v.Str())) { LPlaySound(v.Str(), SND_ASYNC); } } } void ScribeWnd::OnFolderSelect(ScribeFolder *f) { if (SearchView) SearchView->OnFolder(); } void ScribeWnd::OnNewMail(List *MailObjs, bool Add) { if (!MailObjs) return; LVariant v; bool ShowDetail = MailObjs->Length() < 5; List NeedsFiltering; LArray NeedsBayes; LArray NeedsGrowl; LArray Resort; for (auto m: *MailObjs) { if (Add) { #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail t=%p, uid=%s, mode=%s\n", _FL, (Thing*)m, m->GetServerUid().ToString().Get(), toString(m->NewEmail)); #endif switch (m->NewEmail) { case Mail::NewEmailNone: { auto Loaded = m->GetLoaded(); #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail.GetLoaded=%i uid=%s\n", _FL, (int)Loaded, m->GetServerUid().ToString().Get()); #endif if (Loaded != Store3Loaded) { LOG_STORE("\tOnNewMail calling SetLoaded.\n"); m->SetLoaded(); m->NewEmail = Mail::NewEmailLoading; } else { m->NewEmail = Mail::NewEmailFilter; LOG_STORE("\tOnNewMail none->NeedsFiltering.\n"); NeedsFiltering.Insert(m); } break; } case Mail::NewEmailLoading: { auto Loaded = m->GetLoaded(); if (Loaded == Store3Loaded) { m->NewEmail = Mail::NewEmailFilter; NeedsFiltering.Insert(m); if (m->GetFolder() && !Resort.HasItem(m->GetFolder())) { Resort.Add(m->GetFolder()); } } break; } case Mail::NewEmailFilter: { NeedsFiltering.Insert(m); break; } case Mail::NewEmailBayes: { NeedsBayes.Add(m); break; } case Mail::NewEmailGrowl: { if (d->Growl) { NeedsGrowl.Add(m); break; } else { m->NewEmail = Mail::NewEmailTray; // no Growl loaded so fall through to new tray mail } } case Mail::NewEmailTray: { LAssert(m->GetObject()); Mail::NewMailLst.Insert(m); OnNewMailSound(); break; } default: { LAssert(!"Hmmm what happen?"); break; } } } else { #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail.RemoveNewMail t=%p, uid=%s\n", _FL, (Thing*)m, m->GetServerUid().ToString().Get()); #endif Mail::NewMailLst.Delete(m); if (m->NewEmail == Mail::NewEmailFilter) m->NewEmail = Mail::NewEmailNone; } } if (Add) { // Do filtering if (NeedsFiltering.Length()) { List Filters; if (!GetOptions()->GetValue(OPT_DisableUserFilters, v) || !v.CastInt32()) { GetFilters(Filters, true, false, false); } if (Filters.Length() > 0) { // Run the filters #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail.Filtering %i mail through %i filters\n", _FL, (int)NeedsFiltering.Length(), (int)Filters.Length()); #endif Filter::ApplyFilters(NULL, Filters, NeedsFiltering); // All the email not filtered now needs to be sent to the bayes filter. for (auto m: NeedsFiltering) { if (m->NewEmail == Mail::NewEmailBayes) { #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail.NeedsBayes t=%p, msgid=%s\n", _FL, (Thing*)m, m->GetMessageId()); #endif NeedsBayes.Add(m); } else if (m->NewEmail == Mail::NewEmailGrowl) { #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail.NeedsGrowl t=%p, msgid=%s\n", _FL, (Thing*)m, m->GetMessageId()); #endif NeedsGrowl.Add(m); } } } } // Do bayes if (NeedsBayes.Length()) { ScribeBayesianFilterMode FilterMode = BayesOff; if (GetOptions()->GetValue(OPT_BayesFilterMode, v)) FilterMode = (ScribeBayesianFilterMode)v.CastInt32(); for (unsigned i=0; iMailMessageIdMap(); // Start the Bayesian rating process off Store3Status Status = IsSpam(Rating, m); if (Status == Store3Success) { // Bayes done... this stops OnBayesResult from passing it back to OnNewMail m->NewEmail = Mail::NewEmailGrowl; // Process bayes result if (!OnBayesResult(m, Rating)) { // Not spam... so on to growl #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.Bayes.NeedsGrowl t=%p, msgid=%s\n", _FL, (Thing*)m, m->GetMessageId()); #endif NeedsGrowl.Add(m); } else { // Is spam... do nothing... #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NEW_MAIL: Bayes->IsSpam t=%p, msgid=%s\n", _FL, (Thing*)m, m->GetMessageId()); #endif m->NewEmail = Mail::NewEmailNone; m = 0; } } else { // Didn't get classified immediately, so it'll be further // processed when OnBayesResult gets called later. } } else { // Bayes filter not active... move it to growl m->NewEmail = Mail::NewEmailGrowl; NeedsGrowl.Add(m); } } } if (NeedsGrowl.Length()) { if (d->Growl) { if (!ShowDetail) { LAutoPtr n(new LGrowl::LNotify); n->Name = "new-mail"; n->Title = "New Mail"; n->Text.Printf("%i new messages", (int)MailObjs->Length()); d->Growl->Notify(n); } else { for (unsigned i=0; iGetLoaded(); LAssert(state == Store3Loaded); // If loaded then notify GrowlOnMail(m); } } } for (unsigned i=0; iNewEmail = Mail::NewEmailTray; #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNewMail.Growl->Tray t=%p, msgid=%s\n", _FL, (Thing*)m, m->GetMessageId()); #endif LAssert(m->GetObject()); Mail::NewMailLst.Insert(m); OnNewMailSound(); } } if (GetOptions()->GetValue(OPT_NewMailNotify, v) && v.CastInt32()) { PostEvent(M_SCRIBE_NEW_MAIL); } for (unsigned i=0; iGetPath(); LgiTrace("%s:%i - NewMail.OnNewMail.Resort=%s\n", _FL, Path.Get()); #endif Resort[i]->ReSort(); } } } LColour ScribeWnd::GetColour(int i) { static LColour MailPreview; static LColour UnreadCount; #define ReadColDef(Var, Tag, Default) \ case Tag: \ { \ if (!Var.IsValid()) \ { \ Var = Default; \ LColour::GetConfigColour("Colour."#Tag, Var); \ } \ return Var; \ break; \ } switch (i) { ReadColDef(MailPreview, L_MAIL_PREVIEW, LColour(0, 0, 255)); ReadColDef(UnreadCount, L_UNREAD_COUNT, LColour(0, 0, 255)); default: { return LColour((LSystemColour)i); break; } } return LColour(); } bool WriteXmlTag(LStream &p, LXmlTag *t) { const char *Tag = t->GetTag(); bool ValidTag = ValidStr(Tag) && !IsDigit(Tag[0]); if (ValidTag) p.Print("<%s", Tag); else { LAssert(0); return false; } LXmlTree Tree; static const char *EncodeEntitiesAttr = "\'<>\"\n"; for (unsigned i=0; iAttr.Length(); i++) { auto &a = t->Attr[i]; // Write the attribute name p.Print(" %s=\"", a.GetName()); // Encode the value if (!Tree.EncodeEntities(&p, a.GetValue(), -1, EncodeEntitiesAttr)) { LAssert(0); return false; } // Write the delimiter p.Write((void*)"\"", 1); if (iAttr.Length()-1 /*&& TestFlag(d->Flags, GXT_PRETTY_WHITESPACE)*/) { p.Write((void*)"\n", 1); } } p.Write(">", 1); return true; } LString ScribeWnd::ProcessReplyForwardTemplate(Mail *m, Mail *r, char *Xml, int &Cursor, const char *MimeType) { LStringPipe p(256); if (m && r && Xml) { bool IsHtml = MimeType && !_stricmp(MimeType, sTextHtml); LMemStream mem(Xml, strlen(Xml)); LXmlTag x; LXmlTree t(GXT_KEEP_WHITESPACE | GXT_NO_DOM); if (t.Read(&x, &mem, 0)) { ScribeDom Dom(this); Dom.Email = m; if (IsHtml) { const char *EncodeEntitiesContent = "\'<>\""; for (auto Tag: x.Children) { if (!WriteXmlTag(p, Tag)) { break; } for (const char *c = Tag->GetContent(); c; ) { const char *s = strstr(c, "") : NULL; if (s && e) { if (s > c) { t.EncodeEntities(&p, (char*)c, s - c, EncodeEntitiesContent); } s += 2; LString Var = LString(s, e - s).Strip(); LVariant v; if (Var) { LString::Array parts = Var.SplitDelimit(" "); if (parts.Length() > 0) { if (Dom.GetValue(parts[0], v)) { for (unsigned mod = 1; mod < parts.Length(); mod++) { LString::Array m = parts[mod].SplitDelimit("=", 1); if (m.Length() == 2) { if (m[0].Equals("quote")) { LVariant Quote; if (Dom.GetValue(m[1], Quote)) { LVariant WrapColumn; if (!GetOptions()->GetValue(OPT_WrapAtColumn, WrapColumn) || WrapColumn.CastInt32() <= 0) WrapColumn = 76; WrapAndQuote(p, Quote.Str(), WrapColumn.CastInt32(), v.Str(), NULL, MimeType); v.Empty(); } } } } switch (v.Type) { case GV_STRING: { p.Push(v.Str()); break; } case GV_DATETIME: { p.Push(v.Value.Date->Get()); break; } case GV_NULL: break; default: { LAssert(!"Unsupported type."); break; } } } } } c = e + 2; } else { p.Print("%s", c); break; } } } } else { LArray Tags; Tags.Add(&x); for (auto Tag: x.Children) { Tags.Add(Tag); } for (unsigned i=0; iGetTag() && Dom.GetValue(Tag->GetTag(), v)) { char *s = v.Str(); if (s) { const char *Quote; if ((Quote = Tag->GetAttr("quote"))) { LVariant q, IsQuote; GetOptions()->GetValue(OPT_QuoteReply, IsQuote); if (r->GetValue(Quote, q)) { Quote = q.Str(); } else { Quote = "> "; } if (Quote && IsQuote.CastInt32()) { LVariant WrapColumn; if (!GetOptions()->GetValue(OPT_WrapAtColumn, WrapColumn) || WrapColumn.CastInt32() <= 0) WrapColumn = 76; WrapAndQuote(p, Quote, WrapColumn.CastInt32(), s); } else { p.Push(s); } } else { p.Push(s); } } else if (v.Type == GV_DATETIME && v.Value.Date) { char b[64]; v.Value.Date->Get(b, sizeof(b)); p.Push(b); } } else if (Tag->IsTag("cursor")) { int Size = (int)p.GetSize(); char *Buf = new char[Size+1]; if (Buf) { p.Peek((uchar*)Buf, Size); Buf[Size] = 0; RemoveReturns(Buf); Cursor = LCharLen(Buf, "utf-8"); DeleteArray(Buf); } } if (Tag->GetContent()) { p.Push(Tag->GetContent()); } } } } } return p.NewLStr(); } LAutoString ScribeWnd::ProcessSig(Mail *m, char *Xml, const char *MimeType) { LStringPipe p; if (!m || !Xml) return LAutoString(); if (MimeType && !_stricmp(MimeType, sTextHtml)) p.Write(Xml, strlen(Xml)); else { LMemStream mem(Xml, strlen(Xml)); LXmlTag x; LXmlTree t(GXT_KEEP_WHITESPACE|GXT_NO_DOM); if (t.Read(&x, &mem, 0)) { for (auto Tag: x.Children) { if (Tag->IsTag("random-line")) { char *FileName = 0; if ((FileName = Tag->GetAttr("Filename"))) { LFile f(FileName); if (f) { auto Lines = f.Read().SplitDelimit("\r\n"); char *RandomLine = Lines[LRand((unsigned)Lines.Length())]; if (RandomLine) { p.Push(RandomLine); } } } } else if (Tag->IsTag("random-paragraph")) { char *FileName = 0; if ((FileName = Tag->GetAttr("Filename"))) { char *File = LReadTextFile(FileName); if (File) { List Para; for (char *f=File; f && *f; ) { // skip whitespace while (strchr(" \t\r\n", *f)) f++; if (*f) { char *Start = f; char *n; while ((n = strchr(f, '\n'))) { f = n + 1; if (f[1] == '\n' || (f[1] == '\r' && f[2] == '\n')) { break; } } if (f == Start) f += strlen(f); Para.Insert(NewStr(Start, f-Start)); } } DeleteArray(File); char *RandomPara = Para.ItemAt(LRand((int)Para.Length())); if (RandomPara) { p.Push(RandomPara); } Para.DeleteArrays(); } } } else if (Tag->IsTag("include-file")) { char *FileName = 0; if ((FileName = Tag->GetAttr("filename"))) { char *File = LReadTextFile(FileName); if (File) { p.Push(File); DeleteArray(File); } } } else if (Tag->IsTag("quote-file")) { char *FileName = 0; char *QuoteStr = 0; if ((FileName = Tag->GetAttr("filename")) && (QuoteStr = Tag->GetAttr("Quote"))) { } } else { p.Push(Tag->GetContent()); } } } } return LAutoString(p.NewStr()); } // Get the effective permissions for a resource. // // This method can be used by both sync and async code: // In sync mode, don't supply a callback (ie = NULL) and the return value will be: // Store3Error - no access // Store3Delayed - no access, asking the user for password // Store3Success - allow immediate access // // In async mode, supply a callback and wait for the response. // callback(false) - no access // callback(true) - allow immediate access // in this mode the same return values as sync mode are used. Store3Status ScribeWnd::GetAccessLevel(LViewI *Parent, ScribePerm Required, const char *ResourceName, std::function Callback) { if (Required >= CurrentAuthLevel) { if (Callback) Callback(true); return Store3Success; } if (!Parent) Parent = this; switch (Required) { default: break; case PermRequireUser: { LPassword p; if (!p.Serialize(GetOptions(), OPT_UserPermPassword, false)) { if (Callback) Callback(true); return Store3Success; } char Msg[256]; sprintf_s(Msg, sizeof(Msg), LLoadString(IDS_ASK_USER_PASS), ResourceName); auto d = new LInput(Parent, "", Msg, AppName, true); d->DoModal([this, d, p, Callback](auto dlg, auto id) { if (id && d->GetStr()) { char Pass[256]; p.Get(Pass); bool Status = strcmp(Pass, d->GetStr()) == 0; if (Status) { CurrentAuthLevel = PermRequireUser; auto i = Menu->FindItem(IDM_LOGOUT); if (i) i->Enabled(true); if (Callback) Callback(true); } else { if (Callback) Callback(false); } } delete dlg; }); return Store3Delayed; } case PermRequireAdmin: { LString Key; Key.Printf("Scribe.%s", OPT_AdminPassword); auto Hash = LAppInst->GetConfig(Key); if (ValidStr(Hash)) { if (Callback) Callback(false); return Store3Error; } uchar Bin[256]; ssize_t BinLen = 0; if ((BinLen = ConvertBase64ToBinary(Bin, sizeof(Bin), Hash, strlen(Hash))) != 16) { LgiMsg(Parent, "Admin password not correctly encoded.", AppName); if (Callback) Callback(false); return Store3Error; } auto d = new LInput(Parent, "", LLoadString(IDS_ASK_ADMIN_PASS), AppName, true); d->DoModal([this, d, Bin, Callback](auto dlg, auto id) { if (id && d->GetStr()) { unsigned char Digest[16]; char Str[256]; sprintf_s(Str, sizeof(Str), "%s admin", d->GetStr().Get()); MDStringToDigest(Digest, Str); if (memcmp(Bin, Digest, 16) == 0) { CurrentAuthLevel = PermRequireAdmin; auto i = Menu->FindItem(IDM_LOGOUT); if (i) i->Enabled(true); if (Callback) Callback(true); } else { if (Callback) Callback(false); } } delete dlg; }); return Store3Delayed; } } if (Callback) Callback(true); return Store3Success; } void ScribeWnd::GetAccountSettingsAccess(LViewI *Parent, ScribeAccessType AccessType, std::function Callback) { LVariant Level = (int)PermRequireNone; // Check if user level access is required char *Opt = (char*)(AccessType == ScribeReadAccess ? OPT_AccPermRead : OPT_AccPermWrite); GetOptions()->GetValue(Opt, Level); /* // Check if admin access is required char *Admin = GetScribeAccountPerm((char*) (AccessType == ScribeReadAccess ? "Read" : "Write")); if (Admin && _stricmp(Admin, "Admin") == 0) { Level = PermRequireAdmin; } */ GetAccessLevel(Parent ? Parent : this, (ScribePerm)Level.CastInt32(), "Account Settings", Callback); } LMutex *ScribeWnd::GetLock() { return _Lock; } void ScribeWnd::OnBeforeConnect(ScribeAccount *Account, bool Receive) { if (Receive) { Account->Receive.Enabled(false); } else { Account->Send.Enabled(true); } if (StatusPanel) { StatusPanel->Invalidate(); } } void ScribeWnd::OnAfterConnect(ScribeAccount *Account, bool Receive) { if (Account) { SaveOptions(); if (ScribeState == ScribeExiting) { LCloseApp(); } } if (Receive) { Account->Receive.Enabled(true); } else { Account->Send.Enabled(true); } if (StatusPanel) { StatusPanel->Invalidate(); } if (d->SendAfterReceive) { bool Online = false; for (auto a: Accounts) { bool p = a->Receive.IsPersistant(); bool o = a->Receive.IsOnline(); if (!p && o) { Online = true; break; } } if (!Online) { Send(-1, true); d->SendAfterReceive = false; } } } void ScribeWnd::Send(int Which, bool Quiet) { if (ScribeState == ScribeExiting) return; if (Which < 0) { LVariant v; if (GetOptions()->GetValue(OPT_DefaultSendAccount, v)) Which = v.CastInt32(); } LArray Outboxes; unsigned i; for (i=0; iLoadThings(); Outboxes.Add(OutBox); } } if (Outboxes.Length() < 1) { LgiMsg(this, LLoadString(IDS_NO_OUTGOING_FOLDER), AppName, MB_OK); return; } int MailToSend = 0; List Acc; SendAccountlet *Default = 0; { // Create list of accounts for (auto a: Accounts) { Acc.Insert(&a->Send); a->Send.Outbox.DeleteObjects(); if (Which < 0 || a->GetIndex() == Which) Default = &a->Send; } // If the default it not in the list try the first one.. if (!Default) Default = Acc[0]; } for (i=0; iItems) { Mail *m = t->IsMail(); if (!m) continue; uint32_t Flags = m->GetFlags(); if (!TestFlag(Flags, MAIL_SENT) && TestFlag(Flags, MAIL_READY_TO_SEND)) { LDataIt To = m->GetObject()->GetList(FIELD_TO); if (To && To->Length()) { LAutoPtr Out(new ScribeEnvelope); if (Out) { if (m->OnBeforeSend(Out)) { SendAccountlet *Send = 0; for (auto a: Acc) { LVariant Ie = a->GetAccount()->Identity.Email(); if (ValidStr(Ie.Str()) && a->OnlySendThroughThisAccount()) { if (Ie.Str() && m->GetFromStr(FIELD_EMAIL) && _stricmp(Ie.Str(), m->GetFromStr(FIELD_EMAIL)) == 0) { Send = a; break; } } } if (!Send) { Send = Default; } if (Send) { LAssert(Out->To.Length() > 0); Out->SourceFolder = OutBox->GetPath(); Send->Outbox.Add(Out.Release()); MailToSend++; } } } } else { LgiMsg( this, LLoadString(IDS_ERROR_NO_RECIPIENTS), AppName, MB_OK, m->GetSubject() ? m->GetSubject() : (char*)"(none)"); } } } } if (MailToSend) { for (auto a: Acc) { if (a->Outbox.Length() > 0 && !a->IsOnline()) { if (a->IsConfigured()) { a->Connect(0, Quiet); } else { auto d = new LAlert(this, AppName, LLoadString(IDS_ERROR_NO_CONFIG_SEND), LLoadString(IDS_CONFIGURE), LLoadString(IDS_CANCEL)); d->DoModal([this, d, a](auto dlg, auto id) { if (id == 1) a->GetAccount()->InitUI(this, 1, NULL); delete dlg; }); } } } } else { LgiMsg(this, LLoadString(IDS_NO_MAIL_TO_SEND), AppName, MB_OK, Outboxes[0]->GetText()); } } void ScribeWnd::Receive(int Which) { #define LOG_RECEIVE 0 if (ScribeState == ScribeExiting) { LgiTrace("%s:%i - Won't receive, is trying to exit.\n", _FL); return; } for (ScribeAccount *i: Accounts) { if (i->GetIndex() != Which) continue; if (i->Receive.IsOnline()) { #if LOG_RECEIVE LgiTrace("%s:%i - %i already online.\n", _FL, Which); #endif } else if (i->Receive.Disabled() > 0) { #if LOG_RECEIVE LgiTrace("%s:%i - %i is disabled.\n", _FL, Which); #endif } else if (!i->Receive.IsConfigured()) { #if LOG_RECEIVE LgiTrace("%s:%i - %i is not configured.\n", _FL, Which); #endif auto a = new LAlert(this, AppName, LLoadString(IDS_ERROR_NO_CONFIG_RECEIVE), LLoadString(IDS_CONFIGURE), LLoadString(IDS_CANCEL)); a->DoModal([this, a, i](auto dlg, auto id) { if (id == 1) i->InitUI(this, 2, NULL); delete dlg; }); } else { i->Receive.Connect(0, false); } break; } } bool ScribeWnd::GetHelpFilesPath(char *Path, int PathSize) { const char *Index = "index.html"; char Install[MAX_PATH_LEN]; strcpy_s(Install, sizeof(Install), ScribeResourcePath()); for (int i=0; i<5; i++) { char p[MAX_PATH_LEN]; LMakePath(p, sizeof(p), Install, "help"); LMakePath(p, sizeof(p), p, Index); LgiTrace("Trying '%s'\n", p); if (LFileExists(p)) { LTrimDir(p); strcpy_s(Path, PathSize, p); return true; } #ifdef MAC LMakePath(p, sizeof(p), Install, "Resources/Help"); LMakePath(p, sizeof(p), p, Index); // LgiTrace("Trying '%s'\n", p); if (LFileExists(p)) { LTrimDir(p); strcpy_s(Path, PathSize, p); return true; } #endif LTrimDir(Install); // Try all the parent folders... } LArray Ext; LArray Help; Ext.Add("index.html"); LMakePath(Install, sizeof(Install), ScribeResourcePath(), ".."); LRecursiveFileSearch(Install, &Ext, &Help); for (unsigned i=0; iAddPath(ScribeResourcePath()); Browse->SetEvents(d); return Browse->SetUri(Path); } #else #ifdef MAC if (LExecute(Path)) return true; #else if (Hash) *Hash = '#'; // Get browser... char Browser[256]; if (!LGetAppForMimeType("application/browser", Browser, sizeof(Browser))) { LgiTrace("%s:%i - LGetAppForMimeType('text/html') failed.\n", _FL); goto HelpError; } // Execute browser to view help... char Uri[256]; sprintf_s(Uri, sizeof(Uri), "\"file://%s\"", Path); #ifdef WIN32 char *c; while (c = strchr(Uri, '\\')) *c = '/'; #endif LgiTrace("LaunchHelp('%s','%s').\n", Browser, Uri); if (!LExecute(Browser, Uri)) { LgiTrace("%s:%i - LExecute('%s','%s') failed.\n", _FL, Browser, Uri); goto HelpError; } return true; #endif #endif HelpError: LgiMsg(this, LLoadString(IDS_ERROR_NO_HELP), AppName, MB_OK); } return false; } void ScribeWnd::Preview(int Which) { LArray a; a.Add(Accounts[Which]); OpenPopView(this, a); } bool MergeSegments(LDataPropI *DstProp, LDataPropI *SrcProp, LDom *Dom) { LDataI *Src = dynamic_cast(SrcProp); LDataI *Dst = dynamic_cast(DstProp); if (!Dst || !Src) return false; Store3MimeType Mt(Src->GetStr(FIELD_MIME_TYPE)); if (Mt.IsText()) { // Set the headers... Dst->SetStr(FIELD_INTERNET_HEADER, Src->GetStr(FIELD_INTERNET_HEADER)); // Do mail merge of data part... LAutoStreamI Data = Src->GetStream(_FL); if (Data) { // Read data out into a string string... LAutoString Str(new char[(int)Data->GetSize()+1]); Data->Read(Str, (int)Data->GetSize()); Str[Data->GetSize()] = 0; // Do field insert and save result to segment char *Merged = ScribeInsertFields(Str, Dom); LAutoStreamI Mem(new LMemStream(Merged, strlen(Merged))); Dst->SetStream(Mem); } } else { // Straight copy... Dst->CopyProps(*Src); } // Merge children segments as well LDataIt Sc = Src->GetList(FIELD_MIME_SEG); LDataIt Dc = Dst->GetList(FIELD_MIME_SEG); if (Dc && Sc) { for (unsigned i=0; iLength(); i++) { // Create new dest child, and merge the source child across LDataPropI *NewDestChild = Dc->Create(Dst->GetStore()); if (!MergeSegments(NewDestChild, (*Sc)[i], Dom)) return false; LDataI *NewDest = dynamic_cast(NewDestChild); if (NewDest) NewDest->Save(Dst); } } return true; } // Either 'FileName' or 'Source' will be valid void ScribeWnd::MailMerge(LArray &Contacts, const char *FileName, Mail *Source) { ScribeFolder *Outbox = GetFolder(FOLDER_OUTBOX); if (Outbox && Contacts.Length() && (FileName || Source)) { LAutoPtr ImportEmail; if (FileName) { LAutoPtr File(new LFile); if (File->Open(FileName, O_READ)) { Thing *t = CreateItem(MAGIC_MAIL, Outbox, false); ImportEmail.Reset(Source = t->IsMail()); if (!Source->Import(Source->AutoCast(File), sMimeMessage)) { Source = 0; } } } if (Source) { List Msgs; ScribeDom Dom(this); // Do the merging of the document with the database for (unsigned i=0; iGetContact() : 0; Contact *temp = new Contact(this); if (!c) { temp->SetFirst(la->sName); temp->SetEmail(la->sAddr); c = temp; } Dom.Con = c; Thing *t = CreateItem(MAGIC_MAIL, Outbox, false); if (t) { Dom.Email = t->IsMail(); LAutoString s; if (s.Reset(ScribeInsertFields(Source->GetSubject(), &Dom))) Dom.Email->SetSubject(s); LDataPropI *Recip = Dom.Email->GetTo()->Create(Dom.Email->GetObject()->GetStore()); if (Recip) { Recip->CopyProps(*Dom.Con->GetObject()); Recip->SetInt(FIELD_CC, 0); Dom.Email->GetTo()->Insert(Recip); } MergeSegments( Dom.Email->GetObject()->GetObj(FIELD_MIME_SEG), Source->GetObject()->GetObj(FIELD_MIME_SEG), &Dom); Msgs.Insert(Dom.Email); } temp->DecRef(); } // Ask user what to do if (Msgs[0]) { char Msg[256]; sprintf_s(Msg, sizeof(Msg), LLoadString(IDS_MAIL_MERGE_Q), Msgs.Length()); auto Ask = new LAlert(this, AppName, Msg, LLoadString(IDS_SAVE_TO_OUTBOX), LLoadString(IDS_CANCEL)); Ask->DoModal([this, Msgs, Outbox](auto dlg, auto id) { switch (id) { case 1: // Save To Outbox { for (size_t i=0; iSave(Outbox); } break; } case 2: // Cancel { for (size_t i=0; iOnDelete(); } break; } } delete dlg; }); } else { LgiMsg(this, LLoadString(IDS_MAIL_MERGE_EMPTY), AppName); } } } } ScribeFolder *CastFolder(LDataI *f) { if (!f) { LAssert(!"Null pointer"); return 0; } if (f->Type() == MAGIC_FOLDER) { return (ScribeFolder*)f->UserData; } return 0; } Thing *CastThing(LDataI *t) { if (!t) { LAssert(!"Null pointer"); return 0; } if (t->Type() == MAGIC_FOLDER) { LAssert(!"Shouldn't be a folder"); return 0; } return (Thing*)t->UserData; } LString _GetUids(LArray &items) { LString::Array a; for (auto i: items) a.New().Printf(LPrintfInt64, i->GetInt(FIELD_SERVER_UID)); return LString(",").Join(a); } /// Received new items from a storage backend. void ScribeWnd::OnNew( /// The parent folder of the new item LDataFolderI *Parent, /// All the new items LArray &NewItems, /// The position in the parent folder or -1 int Pos, /// Non-zero if the object is a new email. bool IsNew) { ScribeFolder *Fld = CastFolder(Parent); int UnreadDiff = 0; if (Stricmp(Parent->GetStr(FIELD_FOLDER_NAME), "Contacts") && Stricmp(Parent->GetStr(FIELD_FOLDER_NAME), "Calendar")) { LOG_STORE("OnNew(%s, %s, %i, %i)\n", Parent->GetStr(FIELD_FOLDER_NAME), _GetUids(NewItems).Get(), Pos, IsNew); } if (!Fld) { // When this happens the parent hasn't been loaded by the UI thread // yet, so if say the IMAP back end notices a new sub-folder then we // can safely ignore it until such time as the UI loads the parent // folder. At which point the child we got told about here will be // loaded anyway. #if DEBUG_NEW_MAIL LDataFolderI *p = dynamic_cast(Parent); LgiTrace("%s:%i - NewMail.OnNew no UI object for '%s'\n", _FL, p ? p->GetStr(FIELD_FOLDER_NAME) : NULL); #endif return; } if (!Parent || !NewItems.Length()) { LAssert(!"Param error"); return; } for (auto cb: d->Store3EventCallbacks) cb->OnNew(Parent, NewItems, Pos, IsNew); List NewMail; for (auto Item: NewItems) { if (Item->Type() == MAGIC_FOLDER) { // Insert new folder into the right point on the tree... LDataFolderI *SubObject = dynamic_cast(Item); if (!SubObject) { LAssert(!"Not a valid folder."); continue; } ScribeFolder *Sub = CastFolder(SubObject); // LgiTrace("OnNew '%s', Sub=%p Children=%i\n", SubObject->GetStr(FIELD_FOLDER_NAME), Sub, SubObject->SubFolders().Length()); if (!Sub) { // New folder... if ((Sub = new ScribeFolder)) { Sub->App = this; Sub->SetObject(SubObject, false, _FL); Fld->Insert(Sub); } } else { // Existing folder... Fld->Insert(Sub, Pos); } } else { Thing *t = CastThing(Item); if (!t) { // Completely new thing... t = CreateThingOfType((Store3ItemTypes) Item->Type(), Item); } if (t) { if (t->DeleteOnAdd.Obj) { // Complete a delayed move auto OldFolder = t->App->GetFolder(t->DeleteOnAdd.Path); if (!OldFolder) { LgiTrace("%s:%i - Couldn't resolve old folder '%s'\n", _FL, t->DeleteOnAdd.Path.Get()); } else { auto OldItem = t->DeleteOnAdd.Obj; if (!OldFolder->Items.HasItem(OldItem)) LgiTrace("%s:%i - Couldn't find old obj.\n", _FL); else OldItem->OnDelete(); } } t->SetParentFolder(Fld); Fld->Update(); if (Fld->Select()) { // Existing thing... t->SetFieldArray(Fld->GetFieldArray()); ThingFilter *Filter = GetThingFilter(); if (!Filter || Filter->TestThing(t)) { MailList->Insert(t, -1, false); } } Mail *m = t->IsMail(); if (m) { // LgiTrace("OnNew %p\n", m->GetObject()); UnreadDiff += TestFlag(m->GetFlags(), MAIL_READ) ? 0 : 1; #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnNew t=%p uid=%s IsNew=%i\n", _FL, t, m->GetServerUid().ToString().Get(), IsNew); #endif if (IsNew) { LAssert(!NewMail.HasItem(m)); NewMail.Insert(m); } } t->Update(); } } } if (UnreadDiff) Fld->OnUpdateUnRead(UnreadDiff, false); if (MailList && Fld->Select()) MailList->Sort(ListItemCompare, (NativeInt)Fld); if (NewMail.Length()) OnNewMail(&NewMail); } void ScribeWnd::OnPropChange(LDataStoreI *store, int Prop, LVariantType Type) { switch (Prop) { case FIELD_IS_ONLINE: { // This is in case we receive a message after the app has shutdown and // deleted 'this'. if (ScribeState > ScribeRunning) break; if (StatusPanel) StatusPanel->Invalidate(); for (auto a : Accounts) { if (a->Receive.GetDataStore() == store) { int64 Online = store->GetInt(FIELD_IS_ONLINE); auto Old = ScribeState; // This prevents the folders unloading during this call. // Which causes crashes. ScribeState = ScribeLoadingFolders; a->Receive.OnOnlineChange(Online != 0); ScribeState = Old; break; } } break; } } } void ScribeWnd::SetContext(const char *file, int line) { d->CtxFile = file; d->CtxLine = line; } bool ScribeWnd::OnChange(LArray &items, int FieldHint) { bool UpdateSelection = false; List NewMail; ScribeFolder *Parent = 0; // LOG_STORE("OnChange(%i, %i)\n", (int)items.Length(), FieldHint); LAssert(d->CtxFile != NULL); for (unsigned c=0; cStore3EventCallbacks.Length(); c++) { d->Store3EventCallbacks[c]->OnChange(items, FieldHint); } for (unsigned i=0; iType() == MAGIC_FOLDER) { auto ItemFolder = dynamic_cast(Item); ScribeFolder *fld = CastFolder(ItemFolder); if (fld) { if (FieldHint == FIELD_STATUS) { // This is the delayed folder load case: fld->IsLoaded(true); if (fld->Select()) fld->Populate(GetMailList()); } else { fld->Update(); } } else { // LAssert(!"Can't cast to folder?"); } } else if ((t = CastThing(Item))) { ThingUi *Ui = t->GetUI(); if (Ui) Ui->OnChange(); Mail *m = t->IsMail(); if (m) { auto StoreFlags = Item->GetInt(FIELD_FLAGS); #if 0 LgiTrace("App.OnChange(%i) handler %p: %s -> %s\n", FieldHint, m, EmailFlagsToStr(m->FlagsCache).Get(), EmailFlagsToStr(StoreFlags).Get()); #endif if (TestFlag(m->FlagsCache, MAIL_NEW) && !TestFlag(StoreFlags, MAIL_NEW)) { Mail::NewMailLst.Delete(m); Parent = m->GetFolder(); } if (m->FlagsCache != StoreFlags) { // LgiTrace("%s:%i - OnChange mail flags changed.\n", _FL); m->SetFlagsCache(StoreFlags, false, false); Parent = m->GetFolder(); } if (m->NewEmail == Mail::NewEmailLoading) { auto Loaded = m->GetLoaded(); if (Loaded < Store3Loaded) { #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnChange.GetBody t=%p, uid=%s, mode=%s, loaded=%s (%s:%i)\n", _FL, (Thing*)m, m->GetServerUid().ToString().Get(), toString(m->NewEmail), toString(Loaded), d->CtxFile, d->CtxLine); #endif m->GetBody(); } else { #if DEBUG_NEW_MAIL LgiTrace("%s:%i - NewMail.OnChange.NewMail t=%p, uid=%s, mode=%s, loaded=%s (%s:%i)\n", _FL, (Thing*)m, m->GetServerUid().ToString().Get(), toString(m->NewEmail), toString(Loaded), d->CtxFile, d->CtxLine); #endif NewMail.Insert(m); } } } else if (t->IsCalendar()) { for (auto cv: CalendarView::CalendarViews) cv->OnContentsChanged(); } #ifdef _DEBUG if (FieldHint == FIELD_MIME_SEG) { // This was a hack to fix a dumb bug in a dev build... so so dumb. t->SetDirty(); } else #endif if (FieldHint != FIELD_FLAGS) { // Call the on load handler... t->IsLoaded(true); } if (t->GetList()) { t->Update(); if (FieldHint != FIELD_FLAGS) UpdateSelection |= t->Select(); } } } if (MailList && UpdateSelection) { List Sel; if (MailList->GetSelection(Sel)) { OnSelect(&Sel, true); } } if (Parent) Parent->OnUpdateUnRead(0, true); if (NewMail.Length()) OnNewMail(&NewMail); d->CtxFile = NULL; d->CtxLine = 0; return true; } ContactGroup *ScribeWnd::FindGroup(char *SearchName) { ScribeFolder *g = GetFolder(FOLDER_GROUPS); if (!g || !SearchName) return 0; g->LoadThings(); for (Thing *t: g->Items) { ContactGroup *Grp = t->IsGroup(); if (!Grp) continue; auto Name = Grp->GetObject()->GetStr(FIELD_GROUP_NAME); if (Name) { if (!_stricmp(Name, SearchName)) { return Grp; } } } return 0; } bool ScribeWnd::Match(LDataStoreI *Store, LDataPropI *Address, int ObjType, LArray &Matches) { if (!Store || !Address) return 0; // char *Addr = Address->GetStr(ObjType == MAGIC_CONTACT ? FIELD_EMAIL : FIELD_NAME); auto Addr = Address->GetStr(FIELD_EMAIL); if (!Addr) return 0; List *l = GetEveryone(); if (!l) return 0; Matches.Length(0); if (ObjType == MAGIC_CONTACT) { for (auto c: *l) { if (strchr(Addr, '@')) { auto Emails = c->GetEmails(); for (auto e: Emails) { if (e.Equals(Addr)) { Matches.Add(c); break; } } } } } else if (ObjType == MAGIC_GROUP) { ScribeFolder *g = GetFolder(FOLDER_GROUPS); if (!g) return false; g->LoadThings(); for (auto t: g->Items) { ContactGroup *Grp = t->IsGroup(); if (!Grp) continue; List GrpAddr; if (!Grp->GetAddresses(GrpAddr)) continue; for (auto a: GrpAddr) { if (_stricmp(a, Addr) == 0) { Matches.Add(t); break; } } GrpAddr.DeleteArrays(); } } return Matches.Length() > 0; } bool ScribeWnd::OnMove(LDataFolderI *new_parent, LDataFolderI *old_parent, LArray &items) { if (!new_parent || items.Length() == 0) return false; bool Status = false; for (auto cb: d->Store3EventCallbacks) cb->OnMove(new_parent, old_parent, items); ScribeFolder *New = CastFolder(new_parent); ScribeFolder *Old = old_parent ? CastFolder(old_parent) : NULL; if (New) { ssize_t SelIdx = -1; for (unsigned n=0; nType()) { case MAGIC_FOLDER: { ScribeFolder *i = CastFolder(items[n]); if (i) { i->Detach(); New->Insert(i); Status = true; } break; } default: { Thing *t = CastThing(items[n]); if (t) { int UnreadMail = t->IsMail() ? !TestFlag(t->IsMail()->GetFlags(), MAIL_READ) : false; if (Old && UnreadMail) Old->OnUpdateUnRead(-1, false); if (t->GetList()) { if (t->Select() && SelIdx < 0) SelIdx = t->GetList()->IndexOf(t); t->GetList()->Remove(t); } // This closes the user interface if the object is being moved to the trash... if (New->GetSystemFolderType() == Store3SystemTrash) t->SetUI(); t->SetParentFolder(New); if (New->Select()) { MailList->Insert(t); MailList->ReSort(); } if (UnreadMail) New->OnUpdateUnRead(1, false); if (New->GetItemType() == MAGIC_ANY && t->IsMail()) { Mail::NewMailLst.Delete(t->IsMail()); } Status = true; } break; } } } if (MailList && SelIdx >= 0 && MailList->Length() > 0) { if (SelIdx >= (ssize_t)MailList->Length()) SelIdx = MailList->Length()-1; MailList->Value(SelIdx); } } return Status; } bool ScribeWnd::OnDelete(LDataFolderI *Parent, LArray &Items) { int UnreadAdjust = 0; int SelectIdx = -1; LOG_STORE("OnDelete(%s, %i)\n", Parent->GetStr(FIELD_FOLDER_NAME), (int)Items.Length()); if (!Items.Length()) return false; for (unsigned c=0; cStore3EventCallbacks.Length(); c++) { d->Store3EventCallbacks[c]->OnDelete(Parent, Items); } ScribeFolder *Fld = NULL; for (unsigned i=0; iType() == MAGIC_FOLDER) { // Insert new folder into the right point on the tree... ScribeFolder *Sub = CastFolder(Item); if (!Sub) return true; LgiTrace("OnDelete folder %p (%i)\n", (ThingType*)Sub, ThingType::DirtyThings.HasItem(Sub)); // Remove the deleted object from the dirty queue... ThingType::DirtyThings.Delete(Sub); // And make sure it can't get dirty again... Sub->SetWillDirty(false); // Remove all the children LDataIterator &SubItems = Sub->GetFldObj()->Children(); LArray Children; for (LDataI *c = SubItems.First(); c; c = SubItems.Next()) { Children.Add(c); } OnDelete(Sub->GetFldObj(), Children); // Remove the UI element Sub->Remove(); // Free the memory for the object DeleteObj(Sub); } else { Fld = Parent ? CastFolder(Parent) : 0; if (!Fld) return false; Thing *t = CastThing(Item); if (!t) { // This happens when the item moves from one store to another // of a different type and a copy of the old object is made. The // 'Thing' is detached from 'Item' and attached to the new LDataI // object. // However if the object is an unread email... we should still decrement the // folder's unread count. if (Item->Type() == MAGIC_MAIL && !(Item->GetInt(FIELD_FLAGS) & MAIL_READ)) { Fld->OnUpdateUnRead(-1, false); } } else { LAssert(!Fld || Fld == t->GetFolder()); // LgiTrace("OnDelete thing %p (%i)\n", (ThingType*)t, ThingType::DirtyThings.HasItem(t)); // Remove the deleted object from the dirty queue... ThingType::DirtyThings.Delete(t); // And make sure it can't get dirty again... t->SetWillDirty(false); // Was the thing currently being previewed? if (PreviewPanel && PreviewPanel->GetCurrent() == t) PreviewPanel->OnThing(0, false); // Was the object selected in the thing list? if (Fld && Fld->Select()) { // If so, select an adjacent item. if (SelectIdx < 0 && t->Select()) SelectIdx = MailList->IndexOf(t); MailList->Remove(t); } // Was the object an unread email? Mail *m = t->IsMail(); if (m) { if (!TestFlag(m->GetFlags(), MAIL_READ) && Fld) UnreadAdjust--; Mail::NewMailLst.Delete(m); } t->SetUI(); t->SetParentFolder(NULL); t->SetObject(NULL, false, _FL); if (Fld) Fld->Update(); if (!t->DecRef()) { t->SetDirty(false); t->SetWillDirty(false); } } } } if (Fld) Fld->OnUpdateUnRead(UnreadAdjust, false); if (SelectIdx >= 0) { LListItem *i = MailList->ItemAt(SelectIdx); if (!i && MailList->Length() > 0) i = MailList->ItemAt(MailList->Length()-1); if (i) i->Select(true); } return true; } bool ScribeWnd::AddStore3EventHandler(LDataEventsI *callback) { if (!d->Store3EventCallbacks.HasItem(callback)) { d->Store3EventCallbacks.Add(callback); } return true; } bool ScribeWnd::RemoveStore3EventHandler(LDataEventsI *callback) { d->Store3EventCallbacks.Delete(callback); return true; } bool ScribeWnd::OnMailTransferEvent(MailTransferEvent *t) { if (!Lock(_FL)) return false; LAssert(t); d->Transfers.Add(t); Unlock(); return true; } bool ScribeWnd::OnTransfer() { LVariant v; LArray Local; // Lock the transfer list if (Lock(_FL)) { // Take out a bunch of emails... for (int i=0; i<5 && d->Transfers.Length() > 0; i++) { auto t = d->Transfers[0]; LAssert(t); d->Transfers.DeleteAt(0, true); // Save them to a local array Local.Add(t); } Unlock(); if (Local.Length() == 0) // Didn't get any return false; } LArray Trans; for (auto &f: Folders) { if (f.Store) Trans.New() = f.Store->StartTransaction(); } for (auto Transfer: Local) { ReceiveStatus NewStatus = MailReceivedError; if (!Transfer) { LAssert(0); continue; } // We have to set Transfer->Status to something other than "waiting" // in all branches of this loop. Otherwise the account thread will hang. Accountlet *Acc = Transfer->Account; #if DEBUG_NEW_MAIL LgiTrace( "%s:%i - NewMail.OnTransfer t=%p receive=%i act=%i\n", _FL, Transfer, Acc->IsReceive(), Transfer->Action); #endif if (Acc->IsReceive()) { switch (Transfer->Action) { default: break; case MailDownload: case MailDownloadAndDelete: { // Import newly received mail from file ReceiveAccountlet *Receive = dynamic_cast(Acc); if (Receive) { LVariant Path = Receive->DestinationFolder(); ScribeFolder *Inbox = Path.Str() ? GetFolder(Path.Str()) : NULL; if (!Inbox) Inbox = GetFolder(FOLDER_INBOX); if (Inbox) { Mail *m = new Mail(this, Inbox->GetObject()->GetStore()->Create(MAGIC_MAIL)); if (m) { // Just in case m->SetParentFolder(Inbox); // Set the new flag... m->SetFlags(m->GetFlags() | MAIL_NEW, true, false); // Set the account to, which is used to map the email back // to the incoming account in the case that the headers // don't have the correct "To" information. m->SetAccountId(Receive->Id()); // Decode the email m->OnAfterReceive(Transfer->Rfc822Msg); LVariant v; m->SetServerUid(v = Transfer->Uid); // Save to permanent storage if (m->Save(Inbox)) { m->SetDirty(false); NewStatus = MailReceivedOk; // Only after we've safely stored the email can we // actually mark it as downloaded. if (Transfer->Uid) { Receive->AddMsg(Transfer->Uid); } } else LgiTrace("%s:%i - Error: Couldn't save mail to folders.\n", _FL); } else LgiTrace("%s:%i - Error: Memory alloc failed.\n", _FL); } else LgiTrace("%s:%i - Error: No Inbox.\n", _FL); } else LgiTrace("%s:%i - Error: Bad ptr.\n", _FL); break; } case MailHeaders: { LList *Lst; if (Transfer->Msg && (Lst = Transfer->GetList())) { //LgiTrace("Using Lst=%p\n", Lst); Lst->Insert(Transfer->Msg); NewStatus = MailReceivedOk; } break; } } } else if (Transfer->Send) { ScribeFolder *Outbox = GetFolder(Transfer->Send->SourceFolder); if (!Outbox) Outbox = GetFolder(FOLDER_OUTBOX); if (!Outbox) break; Outbox->GetMessageById(Transfer->Send->MsgId, [this, Transfer](auto m) { if (!m) { LAssert(!"Where is the email?"); LgiTrace("%s:%i - Can't find outbox for msg id '%s'\n", _FL, Transfer->Send->MsgId.Get()); } else { if (Transfer->OutgoingHeaders) { m->SetInternetHeader(Transfer->OutgoingHeaders); DeleteArray(Transfer->OutgoingHeaders); } m->OnAfterSend(); m->Save(); // Do filtering LVariant DisableFilters; GetOptions()->GetValue(OPT_DisableUserFilters, DisableFilters); if (!DisableFilters.CastInt32()) { // Run the filters List Filters; GetFilters(Filters, false, true, false); if (Filters[0]) { List In; In.Insert(m); Filter::ApplyFilters(0, Filters, In); } } // Add to bayesian spam whitelist... LVariant v; ScribeBayesianFilterMode FilterMode = BayesOff; GetOptions()->GetValue(OPT_BayesFilterMode, v); FilterMode = (ScribeBayesianFilterMode) v.CastInt32(); if (FilterMode != BayesOff && m->GetObject()) { LDataIt To = m->GetObject()->GetList(FIELD_TO); if (To) { for (LDataPropI *a = To->First(); a; a = To->Next()) { if (a->GetStr(FIELD_EMAIL)) WhiteListIncrement(a->GetStr(FIELD_EMAIL)); } } } // FIXME: // NewStatus = MailReceivedOk; } }); } #if DEBUG_NEW_MAIL LgiTrace( "%s:%i - NewMail.OnTransfer t=%p NewStatus=%i\n", _FL, Transfer, NewStatus); #endif if (NewStatus != MailReceivedOk) { // So tell the thread not to delete it from the server LgiTrace("%s:%i - Mail[%i] error: %s\n", _FL, Transfer->Index, ReceiveStatusName(Transfer->Status)); Transfer->Action = MailNoop; } Transfer->Status = NewStatus; } return Local.Length() > 0; } bool ScribeWnd::OnIdle() { bool Status = false; for (auto a : Accounts) Status |= a->Receive.OnIdle(); Status |= OnTransfer(); LMessage m(M_SCRIBE_IDLE); BayesianFilter::OnEvent(&m); SaveDirtyObjects(); #ifdef _DEBUG static uint64_t LastTs = 0; auto Now = LCurrentTime(); if (Now - LastTs >= 1000) { LastTs = Now; if (Thing::DirtyThings.Length() > 0) LgiTrace("%s:%i - Thing::DirtyThings=" LPrintfInt64 "\n", _FL, Thing::DirtyThings.Length()); } #endif return Status; } void ScribeWnd::OnScriptCompileError(const char *Source, Filter *f) { const char *CompileMsg = LLoadString(IDS_ERROR_SCRIPT_COMPILE); LArray Actions; Actions.Add(LLoadString(IDS_SHOW_CONSOLE)); Actions.Add(LLoadString(IDS_OPEN_SOURCE)); Actions.Add(LLoadString(IDS_OK)); if (!d->Bar) { // FYI Capabilities are handled in ScribeWnd::StartAction. d->ErrSource = Source; d->ErrFilter = f; d->Bar = new MissingCapsBar(this, &d->MissingCaps, CompileMsg, this, Actions); AddView(d->Bar, 2); AttachChildren(); OnPosChange(); } } #ifdef _DEBUG #include "Store3Mail3/Mail3.h" class NoSaveOptions : public LOptionsFile { bool Serialize(bool Write) { return true; } public: NoSaveOptions(LOptionsFile *opts) : LOptionsFile(PortableMode, AppName) { } }; struct ScribeUnitTest { uint64_t StartTs = 0; int Timeout = 5000; virtual ~ScribeUnitTest() {} virtual void OnTimeout() {} virtual void Run(std::function Callback) = 0; virtual void OnPulse() { if (StartTs != 0 && LCurrentTime() - StartTs >= Timeout) { LgiTrace("%s:%i - UnitTest timed out.\n", _FL); StartTs = 0; OnTimeout(); } } }; struct UnitTestState : public LView::ViewEventTarget, public LThread, public LCancel { ScribeWnd *App = NULL; NoSaveOptions *Opts = NULL; std::function Callback; LArray Tests; LAutoPtr t; bool Status = true; bool IsFinished = false; // Old app state.. LAutoPtr OldOpts; LArray OldFolders; UnitTestState(ScribeWnd *app, std::function callback); ~UnitTestState(); LMessage::Result OnEvent(LMessage *Msg) override; bool Iterate(); bool Done(); int Main() override; LDataStoreI *CreateTestMail3(); // Wrappers for protected elements: LArray &GetFolders() { return App->Folders; } void UnLoadFolders() { App->UnLoadFolders(); } }; #define UnitTestFail() { if (Callback) Callback(false); return; } #define UnitTestPass() { if (Callback) Callback(true); return; } // Basic load mail3 folder... struct LoadMailStore1 : public ScribeUnitTest { UnitTestState *s; LString folderPath; bool GotCallback = false; std::function Callback; LoadMailStore1(UnitTestState *state) : s(state) { } ~LoadMailStore1() { if (folderPath) { s->UnLoadFolders(); LFile::Path p = folderPath; p--; FileDev->RemoveFolder(p, true); } LAssert(GotCallback); } // Do operations on mail store before we try and load it... virtual void OnMailStore(LDataStoreI *store) {} virtual void ConfigureLoadMailStoreState(LoadMailStoreState *state) {} void OnTimeout() { GotCallback = true; Callback(false); // This should delete 'this' } void Run(std::function cb) { Callback = cb; if (auto store = s->CreateTestMail3()) { folderPath = store->GetStr(FIELD_NAME); OnMailStore(store); delete store; } // Setup the options saying what folders to load... s->Opts->CreateTag(OPT_MailStores); auto MailStores = s->Opts->LockTag(OPT_MailStores, _FL); if (MailStores == NULL) UnitTestFail(); auto ms = MailStores->CreateTag(OPT_MailStore); ms->SetAttr(OPT_MailStoreLocation, folderPath); ms->SetAttr(OPT_MailStoreDisable, "0"); ms->SetAttr(OPT_MailStoreName, "testing"); s->Opts->Unlock(); if (!ms) UnitTestFail(); // Try the load... if (auto state = new LoadMailStoreState(s->App, [this](auto status) { GotCallback = true; auto &Folders = s->GetFolders(); if (Folders.Length() == 0) UnitTestFail() bool found = false; for (auto &f: Folders) { if (f.Path == folderPath && f.Store != NULL) { found = true; break; } } if (!found) UnitTestFail(); Callback(status); }) ) { ConfigureLoadMailStoreState(state); state->Start(); } } }; // This tests the DB schema upgrade functionality of the mail3 store. struct LoadMailStore2 : public LoadMailStore1 { LoadMailStore2(UnitTestState *state) : LoadMailStore1(state) { } void OnMailStore(LDataStoreI *store) override { auto ms3 = dynamic_cast(store); if (!ms3) UnitTestFail(); // Delete a field from the database so that we force an "upgrade" LMail3Store::LStatement s(ms3, "alter table Calendar rename column DateModified to DateMod"); if (!s.Exec()) UnitTestFail(); } // This is a simple placeholder dialog to simulate asking the user to // upgrade the mail store. It then returns a 'Yes' to the callback after // a few seconds. struct UpgradeYes : public LWindow { UnitTestState *s; LoadMailStoreState::IntCb Cb; UpgradeYes(UnitTestState *state, LoadMailStoreState::IntCb cb) : s(state), Cb(cb) { Name("Upgrade Dlg"); // Just needs to be open long enough to run the msg loop a bit. SetPulse(500); LRect r(0, 0, 100, 100); SetPos(r); MoveSameScreen(s->App); if (Attach(NULL)) Visible(true); } void OnPulse() { Cb(IDYES); Quit(); } }; void ConfigureLoadMailStoreState(LoadMailStoreState *state) override { state->AskStoreUpgrade = [this](auto path, auto detail, auto cb) { new UpgradeYes(s, cb); }; } }; UnitTestState::UnitTestState(ScribeWnd *app, std::function callback) : LView::ViewEventTarget(app, M_UNIT_TEST_TICK), LThread("UnitTestState"), App(app), Callback(callback) { OldOpts = App->d->Options; OldFolders.Swap(App->Folders); App->d->Options.Reset(Opts = new NoSaveOptions(OldOpts)); Tests.Add(new LoadMailStore1(this)); Tests.Add(new LoadMailStore2(this)); LgiTrace("%s:%i - Starting with " LPrintfInt64 " unit tests.\n", _FL, Tests.Length()); Run(); } UnitTestState::~UnitTestState() { Cancel(); // Restore app state... App->d->Options = OldOpts; OldFolders.Swap(App->Folders); WaitForExit(); } LMessage::Result UnitTestState::OnEvent(LMessage *Msg) { if (!IsFinished && Msg->Msg() == M_UNIT_TEST_TICK) { if (t) t->OnPulse(); else Iterate(); } return 0; } bool UnitTestState::Iterate() { if (Tests.Length() == 0) return Done(); t.Reset(Tests[0]); Tests.DeleteAt(0, true); t->StartTs = LCurrentTime(); LgiTrace("%s:%i - Running unit test..\n", _FL); t->Run([this](auto ok) { LgiTrace("%s:%i - Unit test status: %i\n", _FL, ok); Status &= ok; t.Reset(); // Reset for the next unit test. }); return true; } bool UnitTestState::Done() { // Stop more tick events.. IsFinished = true; Cancel(); if (Callback) Callback(Status); delete this; return true; } int UnitTestState::Main() { while (!IsCancelled()) { PostEvent(M_UNIT_TEST_TICK); LSleep(500); } return 0; } LDataStoreI *UnitTestState::CreateTestMail3() { LFile::Path p(ScribeTempPath()); p += "UnitTesting"; if (!p.Exists()) FileDev->CreateFolder(p); p += "Folders.mail3"; return App->CreateDataStore(p, true); } void ScribeWnd::UnitTests(std::function Callback) { UnLoadFolders(); new UnitTestState(this, Callback); } #endif diff --git a/src/ScribeFilter.cpp b/src/ScribeFilter.cpp --- a/src/ScribeFilter.cpp +++ b/src/ScribeFilter.cpp @@ -1,4213 +1,4214 @@ /* ** FILE: ScribeFilter.cpp ** AUTHOR: Matthew Allen ** DATE: 29/10/1999 ** DESCRIPTION: Scribe filters ** ** Copyright (C) 1999-2022, Matthew Allen ** fret@memecode.com ** */ #include #include #include #include #include #include "Scribe.h" #include "lgi/common/Combo.h" #include "lgi/common/Edit.h" #include "lgi/common/Button.h" #include "lgi/common/ProgressDlg.h" #include "lgi/common/FilterUi.h" #include "lgi/common/XmlTree.h" #include "lgi/common/ClipBoard.h" #include "lgi/common/TabView.h" #include "lgi/common/Printer.h" #include "lgi/common/LgiRes.h" #include "lgi/common/FileSelect.h" #include "lgi/common/Charset.h" #include "resdefs.h" #include "resource.h" #define COMBINE_OP_AND 0 #define COMBINE_OP_OR 1 #define IDM_TRUE 500 #define IDM_FALSE 501 #define IDM_NOTNEW 502 #define IDM_LOCAL 503 #define IDM_SERVER 504 #define IDM_LOCAL_AND_SERVER 505 const char *ATTR_NOT = "Not"; const char *ATTR_FIELD = "Field"; const char *ATTR_OP = "Op"; const char *ATTR_VALUE = "Value"; const char *ELEMENT_AND = "And"; const char *ELEMENT_OR = "Or"; const char *ELEMENT_CONDITION = "Condition"; const char *ELEMENT_CONDITIONS = "Conditions"; const char *ELEMENT_ACTION = "Action"; #define SkipWs(s) while ((*s) && strchr(WhiteSpace, *s)) s++; ////////////////////////////////////////////////////////////// class FilterPrivate { public: bool *Stop; LStream *Log; FilterPrivate() { Stop = 0; Log = 0; } }; ////////////////////////////////////////////////////////////// // Filter field definitions ItemFieldDef FilterFieldDefs[] = { {"Name", SdName, GV_STRING, FIELD_FILTER_NAME}, {"Index", SdIndex, GV_INT32, FIELD_FILTER_INDEX}, {"Incoming", SdIncoming, GV_INT32, FIELD_FILTER_INCOMING}, {"Outgoing", SdOutgoing, GV_INT32, FIELD_FILTER_OUTGOING}, {"Internal", SdInternal, GV_INT32, FIELD_FILTER_INTERNAL}, {0} }; int DefaultFilterFields[] = { FIELD_FILTER_NAME, FIELD_FILTER_INDEX, FIELD_FILTER_INCOMING, FIELD_FILTER_OUTGOING, FIELD_FILTER_INTERNAL, 0 }; #define ForCondField(Macro, Value) \ switch (Value) \ { \ case 0: Macro("To"); break; \ case 1: Macro("From"); break; \ case 2: Macro("Subject"); break; \ case 3: Macro("Size"); break; \ case 4: Macro("DateReceived"); break; \ case 5: Macro("DateSent"); break; \ case 6: Macro("Body"); break; \ case 7: Macro("InternetHeaders"); break; \ case 8: Macro("MessageID"); break; \ case 9: Macro("Priority"); break; \ case 10: /* flags */ break; \ case 11: Macro("Html"); break; \ case 12: Macro("Label"); break; \ case 13: Macro("From.Contact"); break; \ case 14: Macro("ImapCacheFile"); break; \ case 15: Macro("ImapFlags"); break; \ case 16: Macro("Attachments"); break; \ case 17: Macro("AttachmentNames"); break; \ case 18: Macro("From.Groups"); break; \ case 19: Macro("*"); break; \ } // Macro("", FIELD_FLAGS) struct ActionName { int Id; const char *Default; }; ActionName ActionNames[] = { {IDS_ACTION_MOVE_FOLDER, "Move to Folder"}, {IDC_DELETE, "Delete"}, {IDS_PRINT, "Print"}, {IDS_ACTION_SOUND, "Play Sound"}, {IDS_ACTION_OPEN, "Open Email"}, {IDS_ACTION_EXECUTE, "Execute Process"}, {IDS_ACTION_SET_COLOUR, "Set Colour"}, {IDS_SET_READ, "Set Read"}, {IDS_ACTION_SET_LABEL, "Set Label"}, {IDS_ACTION_EMPTY_FOLDER, "Empty Folder"}, {IDS_ACTION_MARK_SPAM, "Mark As Spam"}, {IDS_REPLY, "Reply"}, {IDS_FORWARD, "Forward"}, {IDS_BOUNCE, "Bounce"}, {IDS_ACTION_SAVE_ATTACHMENTS, "Save Attachment(s)"}, {IDS_ACTION_DELETE_ATTACHMENTS, "Delete Attachments(s)"}, {L_CHANGE_CHARSET, "Change Charset"}, {IDS_ACTION_COPY, "Copy to Folder"}, {IDS_EXPORT, "Export"}, {0, 0}, {IDS_ACTION_CREATE_FOLDER, "Create Folder"}, {0, 0} }; // These are the english names used for storing XML const char *OpNames[] = { "=", "!=", "<", "<=", ">=", ">", "Like", // LLoadString(IDS_LIKE), "Contains", // LLoadString(IDS_CONTAINS), "Starts With", // LLoadString(IDS_STARTS_WITH), "Ends With", // LLoadString(IDS_ENDS_WITH), 0 }; const char *TranslatedOpNames[] = { "=", "!=", "<", "<=", ">=", ">", 0, // like 0, // contains 0, // starts with 0, // ends with 0 }; const char **GetOpNames(bool Translated) { if (Translated) { if (TranslatedOpNames[6] == NULL) { TranslatedOpNames[6] = LLoadString(IDS_LIKE); TranslatedOpNames[7] = LLoadString(IDS_CONTAINS); TranslatedOpNames[8] = LLoadString(IDS_STARTS_WITH); TranslatedOpNames[9] = LLoadString(IDS_ENDS_WITH); } if (TranslatedOpNames[6]) { return TranslatedOpNames; } } return OpNames; } ////////////////////////////////////////////////////////////// void SkipSep(const char *&s) { while (s && *s && strchr(" \t,", *s)) s++; } LCombo *LoadTemplates(ScribeWnd *App, LView *Wnd, List &MsgIds, int Ctrl, char *Template) { LCombo *Temp; if (Wnd->GetViewById(IDC_TEMPLATE, Temp)) { ScribeFolder *Templates = App->GetFolder(FOLDER_TEMPLATES); if (Templates) { int n=0; for (auto t: Templates->Items) { Mail *m = t->IsMail(); if (m) { auto Id = m->GetMessageId(true); if (Id) { MsgIds.Insert(NewStr(Id)); Temp->Insert(m->GetSubject() ? m->GetSubject() : (char*)"(no subject)"); if (Template && strcmp(Template, Id) == 0) { Temp->Value(n); } } } n++; } } } return Temp; } class BrowseReply : public LDialog { List MsgIds; LCombo *Temp; public: LString Arg; BrowseReply(ScribeWnd *App, LView *Parent, const char *arg) { SetParent(Parent); LoadFromResource(IDD_FILTER_REPLY); MoveToCenter(); char *Template = LTokStr(arg); SkipSep(arg); char *All = LTokStr(arg); SkipSep(arg); char *MarkReplied = LTokStr(arg); if (All) { SetCtrlValue(IDC_ALL, atoi(All)); } if (MarkReplied) { SetCtrlValue(IDC_MARK_REPLIED, atoi(MarkReplied)); } Temp = LoadTemplates(App, this, MsgIds, IDC_TEMPLATE, Template); DeleteArray(Template); DeleteArray(MarkReplied); DeleteArray(All); } ~BrowseReply() { MsgIds.DeleteArrays(); } int OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDOK: { if (Temp) { char *Template = MsgIds[(int)Temp->Value()]; int All = (int)GetCtrlValue(IDC_ALL); int MarkReplied = (int)GetCtrlValue(IDC_MARK_REPLIED); char s[256]; sprintf_s(s, sizeof(s), "\"%s\" %i %i", Template?Template:(char*)"", All, MarkReplied); Arg = s; } // Fall thru } case IDCANCEL: { EndModal(c->GetId() == IDOK); break; } } return 0; } }; class BrowseForward : public LDialog { List MsgIds; ScribeWnd *App; LCombo *Temp; bool UseTemplate; public: LString Arg; BrowseForward(ScribeWnd *app, LView *parent, const char *arg, bool temp) { UseTemplate = temp; App = app; Arg = 0; Temp = 0; SetParent(parent); LoadFromResource(IDD_FILTER_FORWARD); MoveToCenter(); char *Template = 0; if (UseTemplate) { Template = LTokStr(arg); SkipSep(arg); } char *Email = LTokStr(arg); SkipSep(arg); char *Forward = LTokStr(arg); SkipSep(arg); char *MarkForwarded = LTokStr(arg); if (UseTemplate) { bool HasTemplate = ValidStr(Template) ? strlen(Template) > 1 : 0; SetCtrlValue(IDC_USE_TEMPLATE, HasTemplate); Temp = LoadTemplates(App, this, MsgIds, IDC_TEMPLATE, HasTemplate ? Template : 0); } else { LViewI *v = FindControl(IDC_USE_TEMPLATE); if (v) { int y1 = v->GetPos().y1; Children.Delete(v); DeleteObj(v); v = FindControl(IDC_TEMPLATE); if (v) { int y2 = v->GetPos().y2; Children.Delete(v); DeleteObj(v); int Sub = y1 - y2 - 10; for (auto v: Children) { LRect r = v->GetPos(); r.Offset(0, Sub); v->SetPos(r); } LRect r = GetPos(); r.y2 += Sub; SetPos(r); } } } SetCtrlName(IDC_EMAIL, Email); if (Forward) SetCtrlValue(IDC_ATTACHMENTS, atoi(Forward)); if (MarkForwarded) SetCtrlValue(IDC_MARK_FORWARDED, atoi(MarkForwarded)); DeleteArray(Template); DeleteArray(Email); DeleteArray(Forward); DeleteArray(MarkForwarded); } ~BrowseForward() { DeleteArray(Arg); } int OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDOK: { char s[256]; bool HasTemplate = GetCtrlValue(IDC_USE_TEMPLATE) != 0; char *MsgId = HasTemplate ? MsgIds[(int)GetCtrlValue(IDC_TEMPLATE)] : 0; const char *Email = GetCtrlName(IDC_EMAIL); int Attachments = (int)GetCtrlValue(IDC_ATTACHMENTS); int MarkForwarded = (int)GetCtrlValue(IDC_MARK_FORWARDED); if (UseTemplate) { sprintf_s(s, sizeof(s), "\"%s\" \"%s\" %i %i", MsgId, Email?Email:(char*)"", Attachments, MarkForwarded); } else { sprintf_s(s, sizeof(s), "\"%s\" %i %i", Email?Email:(char*)"", Attachments, MarkForwarded); } Arg = s; // Fall thru } case IDCANCEL: { EndModal(c->GetId() == IDOK); } } return 0; } }; class BrowseSaveAttach : public LDialog { ScribeWnd *App; public: char *Arg; BrowseSaveAttach(ScribeWnd *app, LView *parent, const char *arg) { Arg = 0; App = app; SetParent(parent); if (LoadFromResource(IDD_FILTER_SAVE_ATTACH)) { MoveToCenter(); char *Dir = LTokStr(arg); SkipSep(arg); char *Types = LTokStr(arg); SetCtrlName(IDC_DIR, Dir); SetCtrlName(IDC_TYPES, Types); DeleteArray(Dir); DeleteArray(Types); } } ~BrowseSaveAttach() { DeleteArray(Arg); } int OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDC_BROWSE_DIR: { auto s = new LFileSelect(this); s->Name(GetCtrlName(IDC_DIR)); s->OpenFolder([this](auto s, auto status) { if (status) SetCtrlName(IDC_DIR, s->Name()); delete s; }); break; } case IDOK: { char s[512]; const char *Dir = GetCtrlName(IDC_DIR); const char *Types = GetCtrlName(IDC_TYPES); sprintf_s(s, sizeof(s), "\"%s\",\"%s\"", Dir?Dir:(char*)"", Types?Types:(char*)""); Arg = NewStr(s); // Fall thru } case IDCANCEL: { EndModal(c->GetId() == IDOK); } } return 0; } }; bool LgiCreateTempFileName(char *Path, int PathLen) { if (Path) { #if defined WIN32 int Len = GetTempPathA(PathLen, Path); #else strcpy(Path, "/tmp"); int Len = (int)strlen(Path); #endif if (Path[Len-1] != DIR_CHAR) strcat(Path, DIR_STR); int len = (int) strlen(Path); sprintf_s(Path+len, PathLen-len, "~%i.txt", LRand(10000)); return true; } return false; } ////////////////////////////////////////////////////////////// FilterCondition::FilterCondition() { Op = 0; Not = false; } FilterCondition &FilterCondition::operator=(FilterCondition &c) { Source.Reset(NewStr(c.Source)); Op = c.Op; Not = c.Not; Value.Reset(NewStr(c.Value)); return *this; } bool FilterCondition::Test(Filter *F, Mail *m, LStream *Log) { if (Log) Log->Print("\tCondition.Test Fld='%s'\n", (char*)Source); if (ValidStr(Source)) { int Flds = 0; for (; MailFieldDefs[Flds].FieldId; Flds++) { } LVariant v; // Get data if (_stricmp(Source, "mail.attachments") == 0) { // attachment(s) data List Attachments; if (m->GetAttachments(&Attachments)) { for (auto a: Attachments) { char *Data; ssize_t Length; LDateTime Temp; if (a->Get(&Data, &Length)) { // Zero terminate the string LVariant v; v.SetBinary(Length, Data); // Test the file if (TestData(F, v, Log)) { return true; } } } } return false; } else if (_stricmp(Source, "mail.attachmentnames") == 0) { // attachment(s) name List Attachments; // ItemFieldDef AttachType = {"Attachment(s) Name", SdAttachmentNames, GV_STRING}; LDateTime Temp; if (m->GetAttachments(&Attachments)) { for (auto a: Attachments) { LVariant v = a->GetName(); if (TestData(F, v, Log)) { return true; } } } return false; } else { bool Status = false; ItemFieldDef *f = 0; if (_stricmp(Source, "mail.*") == 0) { ItemFieldDef *Start = MailFieldDefs; ItemFieldDef *End = MailFieldDefs + Flds - 1; for (f = Start; f <= End && !Status; f++) { switch (f->FieldId) { case FIELD_TO: { for (LDataPropI *a = m->GetTo()->First(); a; a = m->GetTo()->Next()) { char Data[256]; sprintf_s(Data, sizeof(Data), "%s <%s>", a->GetStr(FIELD_NAME), a->GetStr(FIELD_EMAIL)); LVariant v(Data); if (TestData(F, v, Log)) { Status |= true; } } continue; break; } case FIELD_FROM: { char Data[256]; sprintf_s(Data, sizeof(Data), "%s <%s>", m->GetFrom()->GetStr(FIELD_NAME), m->GetFrom()->GetStr(FIELD_EMAIL)); v = Data; break; } case FIELD_REPLY: { char Data[256]; sprintf_s(Data, sizeof(Data), "%s <%s>", m->GetReply()->GetStr(FIELD_NAME), m->GetReply()->GetStr(FIELD_EMAIL)); v = Data; break; } case FIELD_SUBJECT: v = m->GetSubject(); break; case FIELD_SIZE: v = m->TotalSizeof(); break; case FIELD_DATE_RECEIVED: v = m->GetDateReceived(); break; case FIELD_DATE_SENT: v = m->GetDateSent(); break; case FIELD_TEXT: v = m->GetBody(); break; case FIELD_INTERNET_HEADER: v = m->GetInternetHeader(); break; case FIELD_MESSAGE_ID: v = m->GetMessageId(); break; case FIELD_PRIORITY: v = m->GetPriority(); break; case FIELD_ALTERNATE_HTML: v = m->GetHtml(); break; case FIELD_LABEL: v = m->GetLabel(); break; } } } else { if (F) { F->GetValue(Source, v); } else if (Log) { Log->Print("%s:%i - Error: No filter to query value.\n", __FILE__, __LINE__); } } if (v.Type) { // Test data Status |= TestData(F, v, Log); } else if (Log) { Log->Print("%s:%i - Error: Variant doesn't have type!\n", __FILE__, __LINE__); } return Status; } } return false; } char *LogPreview(char *s) { LStringPipe p(1 << 10); if (s) { char *c; for (c = s; *c && c - s < 200; c++) { switch (*c) { case '\n': p.Push("\\n"); break; case '\r': p.Push("\\r"); break; case '\t': p.Push("\\t"); break; default: { p.Push(c, 1); } } } if (*c) { p.Push("..."); } } return p.NewStr(); } bool FilterCondition::TestData(Filter *F, LVariant &Var, LStream *Log) { // Do DOM lookup on the Value LVariant Val; if (F && F->Evaluate(Value, Val)) { // Compare using type switch (Var.Type) { case GV_LIST: { for (auto v: *Var.Value.Lst) { if (TestData(F, *v, Log)) { return true; } } break; } case GV_DOM: { // Probably an address field LVariant n; if (Var.Value.Dom->GetValue("Name", n)) { if (TestData(F, n, Log)) { return true; } } if (Var.Value.Dom->GetValue("Email", n)) { if (TestData(F, n, Log)) { return true; } } break; } case GV_STRING: { char *sVar = Var.Str(); char *sVal = Val.Str(); bool IsStr = ValidStr(sVar); bool IsVal = ValidStr(sVal); char *VarLog = Log ? LogPreview(sVar) : 0; bool m = false; switch (Op) { case OP_EQUAL: { if (!IsStr && !IsVal) { m = true; } else if (IsStr && IsVal) { m = _stricmp(sVal, sVar) == 0; } if (Log) Log->Print("\t\t\t'%s' == '%s' = %i\n", VarLog, sVal, m); break; } case OP_LIKE: { m = MatchStr(sVal, sVar); if (Log) Log->Print("\t\t\t'%s' like '%s' = %i\n", VarLog, sVal, m); break; } case OP_CONTAINS: { if (IsVal && IsStr) { m = stristr(sVar, sVal) != 0; if (Log) Log->Print("\t\t\t'%s' contains '%s' = %i\n", VarLog, sVal, m); } break; } case OP_STARTS_WITH: { if (IsVal && IsStr) { size_t Len = strlen(sVal); m = _strnicmp(sVar, sVal, Len) == 0; if (Log) Log->Print("\t\t\t'%s' starts with '%s' = %i\n", VarLog, sVal, m); } break; } case OP_ENDS_WITH: { if (IsVal && IsStr) { size_t SLen = strlen(sVar); size_t VLen = strlen(sVal); if (SLen >= VLen) { m = _strnicmp(sVar + SLen - VLen, sVal, VLen) == 0; if (Log) Log->Print("\t\t\t'%s' ends with '%s' = %i\n", VarLog, sVal, m); } else { if (Log) Log->Print("\t\t\tEnds With Error: '%s' is shorter than '%s'\n", sVar, sVal); } } else { if (Log) Log->Print("\t\t\tEnds With Error: invalid arguments\n"); } break; } } DeleteArray(VarLog); return m; break; } case GV_INT32: { // Convert Val to int int Int = 0; if (Val.Str()) { Int = atoi(Val.Str()); } else if (Val.Type == GV_INT32) { Int = Val.Value.Int; } int IntVal = Var.Value.Int; switch (Op) { case OP_LIKE: // for lack anything better case OP_EQUAL: { bool m = Int == IntVal; if (Log) Log->Print("\t\t\t%i == %i = %i\n", Int, IntVal, m); return m; } case OP_NOT_EQUAL: { bool m = Int != IntVal; if (Log) Log->Print("\t\t\t%i != %i = %i\n", Int, IntVal, m); return m; } case OP_LESS_THAN: { bool m = Int < IntVal; if (Log) Log->Print("\t\t\t%i < %i = %i\n", Int, IntVal, m); return m; } case OP_LESS_THAN_OR_EQUAL: { bool m = Int <= IntVal; if (Log) Log->Print("\t\t\t%i <= %i = %i\n", Int, IntVal, m); return m; } case OP_GREATER_THAN: { bool m = Int > IntVal; if (Log) Log->Print("\t\t\t%i > %i = %i\n", Int, IntVal, m); return m; } case OP_GREATER_THAN_OR_EQUAL: { bool m = Int >= IntVal; if (Log) Log->Print("\t\t\t%i >= %i = %i\n", Int, IntVal, m); return m; } } break; } case GV_DATETIME: { LDateTime Temp; LDateTime *DVal; if (Val.Type == GV_DATETIME) { DVal = Val.Value.Date; } else if (Val.Type == GV_STRING) { Temp.Set(Val.Str()); DVal = &Temp; } else break; LDateTime *DVar = Var.Value.Date; if (DVal && DVar) { bool Less = *DVar < *DVal; bool Greater = *DVar > *DVal; bool Equal = !Less && !Greater; char LogVal[64]; char LogVar[64]; if (Log) { DVal->Get(LogVal, sizeof(LogVal)); DVar->Get(LogVar, sizeof(LogVar)); } switch (Op) { case OP_LIKE: case OP_EQUAL: { if (Log) Log->Print("\t\t\t%s = %s == %i\n", LogVar, LogVal, Equal); return Equal; } case OP_NOT_EQUAL: { if (Log) Log->Print("\t\t\t%s != %s == %i\n", LogVar, LogVal, !Equal); return !Equal; } case OP_LESS_THAN: { if (Log) Log->Print("\t\t\t%s <= %s == %i\n", LogVar, LogVal, Less); return Less; } case OP_LESS_THAN_OR_EQUAL: { if (Log) Log->Print("\t\t\t%s < %s == %i\n", LogVar, LogVal, Less || Equal); return Less || Equal; } case OP_GREATER_THAN: { if (Log) Log->Print("\t\t\t%s > %s == %i\n", LogVar, LogVal, Greater); return Greater; } case OP_GREATER_THAN_OR_EQUAL: { if (Log) Log->Print("\t\t\t%s >= %s == %i\n", LogVar, LogVal, Greater || Equal); return Greater || Equal; } } } break; } default: { if (Log) Log->Print("\t\t\tUnknown data type %i.\n", Var.Type); break; } } } return false; } ThingUi *FilterCondition::DoUI(MailContainer *c) { return NULL; } ////////////////////////////////////////////////////////////// // #define OPT_Action "Action" #define OPT_Type "Type" #define OPT_Arg1 "Arg1" #define IDC_TYPE_CBO 2000 #define IDC_ARG_EDIT 2001 #define IDC_BROWSE_ARG 2002 FilterAction::FilterAction(LDataStoreI *Store) { Type = ACTION_MOVE_TO_FOLDER; TypeCbo = 0; ArgEdit = 0; Btn = 0; } FilterAction::~FilterAction() { DeleteObj(TypeCbo); DeleteObj(ArgEdit); DeleteObj(Btn); } int FilterAction::OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDC_TYPE_CBO: { Type = (FilterActionTypes) c->Value(); break; } case IDC_ARG_EDIT: { Arg1 = c->Name(); break; } case IDC_BROWSE_ARG: { if (ArgEdit) ArgEdit->Name(Arg1); break; } } return 0; } void FilterAction::OnMeasure(LPoint *Info) { LListItem::OnMeasure(Info); if (Select()) Info->y += 2; } bool FilterAction::Select() { return LListItem::Select(); } void FilterAction::OnPaintColumn(LItem::ItemPaintCtx &Ctx, int i, LItemColumn *c) { LListItem::OnPaintColumn(Ctx, i, c); if (LListItem::Select() && !TypeCbo && !ArgEdit) Select(true); else if (i == 0 && TypeCbo) TypeCbo->SetPos(*GetPos(i)); else if (i == 1 && ArgEdit) ArgEdit->SetPos(*GetPos(i)); else if (i == 2 && Btn) Btn->SetPos(*GetPos(i)); } void FilterAction::Select(bool b) { LListItem::Select(b); if (b) { LList *Lst = LListItem::GetList(); if (Lst && Lst->IsAttached()) { LRect *r = GetPos(0); if (!TypeCbo) { TypeCbo = new LCombo(IDC_TYPE_CBO, r->x1, r->y1, r->X(), r->Y(), 0); for (int i=0; ActionNames[i].Id; i++) TypeCbo->Insert(LLoadString(ActionNames[i].Id)); TypeCbo->Attach(Lst); } TypeCbo->Value(Type); TypeCbo->SetPos(*r); r = GetPos(1); if (!ArgEdit) { ArgEdit = new LEdit(IDC_ARG_EDIT, r->x1, r->y1, r->X(), r->Y(), 0); ArgEdit->Attach(Lst); } ArgEdit->Name(Arg1); ArgEdit->SetPos(*r); r = GetPos(2); if (!Btn) { Btn = new LButton(IDC_BROWSE_ARG, r->x1, r->y1, r->X(), r->Y(), "..."); Btn->Attach(Lst); } Btn->SetPos(*r); } } else { DeleteObj(TypeCbo); DeleteObj(ArgEdit); DeleteObj(Btn); } } const char *FilterAction::GetText(int Col) { switch (Col) { case 0: return (char*)LLoadString(ActionNames[Type].Id); break; case 1: return Arg1; break; case 2: break; } return 0; } bool FilterAction::Get(LXmlTag *t) { if (!t) return false; // Obj -> XML t->SetAttr(OPT_Type, Type); t->SetAttr(OPT_Arg1, Arg1); return true; } bool FilterAction::Set(LXmlTag *t) { if (!t) return false; // XML -> Obj Type = (FilterActionTypes) t->GetAsInt(OPT_Type); Arg1 = t->GetAttr(OPT_Arg1); return true; } LDataPropI &FilterAction::operator =(LDataPropI &p) { FilterAction *c = dynamic_cast(&p); if (c) { Type = c->Type; Arg1 = c->Arg1; } return *this; } ThingUi *FilterAction::DoUI(MailContainer *c) { return 0; } const char *GetFileName(const char *Path) { if (Path) { auto d = strrchr(Path, DIR_CHAR); if (d) return d + 1; else return Path; } return 0; } bool CollectAttachmentsByPattern(Mail *m, LString Pattern, List &Files) { Files.Empty(); if (m) { List Attachments; if (m->GetAttachments(&Attachments)) { auto p = Pattern.SplitDelimit(" ,;"); for (auto a: Attachments) { bool Match = true; for (unsigned i=0; Match && iGetName()); if (d) Match = MatchStr(p[i], d); } if (Match) Files.Insert(a); } } } return Files[0] != 0; } Mail *GetTemplateMail(ScribeWnd *App, char *TemplateMsgId) { ScribeFolder *Templates = App->GetFolder(FOLDER_TEMPLATES); if (Templates) { // Mail *Template = 0; for (auto t: Templates->Items) { Mail *m = t->IsMail(); if (m) { auto MsgId = m->GetMessageId(); if (MsgId && strcmp(MsgId, TemplateMsgId) == 0) { return m; break; } } } } return 0; } class FilterScribeDom : public ScribeDom { public: FilterScribeDom(ScribeWnd *a) : ScribeDom(a) { } bool GetVariant(const char *Name, LVariant &Value, const char *Array = 0) { if (!Name) return false; if (!_stricmp(Name, "file")) { LVariant v; if (!GetValue(Array, v)) return false; char p[MAX_PATH_LEN], fn[32]; do { sprintf_s(fn, sizeof(fn), "file_%d.tmp", LRand()); LMakePath(p, sizeof(p), ScribeTempPath(), fn); } while (LFileExists(p)); LFile f; if (f.Open(p, O_WRITE)) { switch (v.Type) { case GV_INT32: f.Print("%i", v.Value.Int); break; case GV_INT64: f.Print(LPrintfInt64, v.Value.Int64); break; case GV_BOOL: f.Print("%s", v.Value.Bool ? "true" : "false"); break; case GV_DOUBLE: f.Print("%f", v.Value.Dbl); break; case GV_STRING: case GV_WSTRING: f.Print("%s", v.Str()); break; case GV_BINARY: f.Write(v.Value.Binary.Data, v.Value.Binary.Length); break; case GV_DATETIME: v.Value.Date->Get(fn, sizeof(fn)); f.Write(fn, strlen(fn)); break; default: f.Print("Unsupported type."); break; } f.Close(); Value = p; return true; } } return ScribeDom::GetVariant(Name, Value, Array); } }; bool FilterAction::Do(Filter *F, ScribeWnd *App, Mail *&m, LStream *Log) { bool Status = false; if (!F || !App || !m) { LAssert(!"Param error."); return false; } switch (Type) { case ACTION_MOVE_TO_FOLDER: { ScribeFolder *Folder = App->GetFolder(Arg1); if (Folder) { LArray Items; Items.Add(m); Folder->MoveTo(Items, false, [this, Log](auto result, auto status) { if (Log) Log->Print("\tACTION_MOVE_TO_FOLDER(%s) = %i.\n", Arg1.Get(), result); }); Status = true; } else if (Log) { Log->Print("\tACTION_MOVE_TO_FOLDER(%s) failed, folder missing.\n", Arg1.Get()); } break; } case ACTION_COPY: { ScribeFolder *Folder = App->GetFolder(Arg1); if (Folder) { LArray Items; Items.Add(m); Folder->MoveTo(Items, true, [this, Log](auto result, auto status) { if (Log) Log->Print("\tACTION_COPY(%s) = %i.\n", Arg1.Get(), result); }); Status = true; } else if (Log) { Log->Print("\tACTION_COPY(%s) failed, folder missing.\n", Arg1.Get()); } break; } case ACTION_EXPORT: { LFile::Path p(Arg1); if (!p.IsFolder()) { if (Log) Log->Print("\tACTION_EXPORT(%s) failed, folder missing.\n", Arg1.Get()); break; } auto Fn = m->GetDropFileName(); p += LGetLeaf(Fn); LAutoPtr out(new LFile); if (!out->Open(p, O_WRITE)) { if (Log) Log->Print("\tACTION_EXPORT(%s) failed: couldn't open file: %s.\n", Arg1.Get(), p.GetFull().Get()); break; } if (!m->Export(m->AutoCast(out), sMimeMessage)) { if (Log) Log->Print("\tACTION_EXPORT(%s) failed: couldn't export file.\n", Arg1.Get()); } break; } case ACTION_DELETE: { bool Local = ValidStr(Arg1) ? stristr(Arg1, "local") != 0 : true; bool Server = stristr(Arg1, "server") != 0; if (Server) { ScribeAccount *a = m->GetAccountSentTo(); if (!a) break; auto Uid = m->GetServerUid(); if (Uid.Str()) { if (Log) Log->Print("\tACTION_DELETE - Setting '%s' to be deleted on the server (Uid=%s)\n", m->GetSubject(), Uid.Str()); a->Receive.DeleteAsSpam(Uid.Str()); - m->SetServerUid(Uid = NULL); + Uid.Empty(); + m->SetServerUid(Uid); } else { LVariant Uid; if (m->GetValue("InternetHeader[X-UIDL]", Uid)) { if (Log) Log->Print("\tACTION_DELETE - Setting '%s' to be deleted on the server (Uid=%s)\n", m->GetSubject(), Uid.Str()); a->Receive.DeleteAsSpam(Uid.Str()); } } } if (Local) { bool DeleteStatus = m->OnDelete(); if (Log) Log->Print("\tACTION_DELETE(%s) status %i.\n", Arg1.Get(), DeleteStatus); /* ScribeFolder *Folder = App->GetFolder(FOLDER_TRASH); if (Folder) { Thing *t = m; Status = Folder->MoveTo(t); m = t->IsMail(); if (Log) Log->Print("\tACTION_DELETE(%s) = %i.\n", Arg1.Get(), Status); } else if (Log) { Log->Print("\tACTION_DELETE(%s) failed, trash missing.\n", Arg1.Get()); } */ } break; } case ACTION_SET_READ: { bool Read = true; bool NotNew = false; if (ValidStr(Arg1)) { if (IsDigit(*Arg1)) { // boolean number Read = Arg1.Int() != 0; } else if (stristr(Arg1, "true")) { Read = true; } else if (stristr(Arg1, "false")) { Read = false; } if (stristr(Arg1, "notnew")) { NotNew = true; } } int f = m->GetFlags(); if (Read) SetFlag(f, MAIL_READ); else ClearFlag(f, MAIL_READ); m->SetFlags(f); if (Log) Log->Print("\tACTION_SET_READ(%s)\n", Arg1.Get()); if (NotNew) { List Objs; Objs.Insert(m); App->OnNewMail(&Objs, false); } break; } case ACTION_LABEL: { LVariant v; if (!F || !F->GetValue(Arg1, v)) v = Arg1; m->SetVariant("Label", v); break; } case ACTION_EMPTY_FOLDER: { if (F->App && Arg1) { ScribeFolder *Folder = F->App->GetFolder(Arg1); if (Folder) { Folder->LoadThings(); List m; Thing *t; while ((t = Folder->Items[0])) { if (t->IsMail()) { m.Insert(t->IsMail()); } if (Folder->DeleteThing(t)) { DeleteObj(t); } } F->App->OnNewMail(&m, false); Folder->OnUpdateUnRead(0, true); if (Log) Log->Print("\tACTION_EMPTY_FOLDER(%s)\n", Arg1.Get()); } } break; } case ACTION_MARK_AS_SPAM: { m->DeleteAsSpam(App); break; } case ACTION_PRINT: { LPrinter Info; #ifdef _MSC_VER #pragma message ("Warning: ACTION_PRINT not implemented.") #endif /* FIXME if (Info.Serialize(Arg1, false)) { App->ThingPrint(m, &Info); } */ break; } case ACTION_PLAY_SOUND: { LPlaySound(Arg1, true); break; } case ACTION_EXECUTE: { FilterScribeDom dom(App); dom.Email = m; dom.Fil = F; LAutoString cmd(ScribeInsertFields(Arg1, &dom)); if (cmd) { const char *s = cmd; LAutoString exe(LTokStr(s)); LExecute(exe, s); } break; } case ACTION_OPEN: { m->DoUI(); break; } case ACTION_MARK: { if (Stricmp(Arg1.Get(), "false") == 0) { // unmark the item... m->SetMarkColour(0); } else { // parse out RGB auto T = Arg1.SplitDelimit(","); if (T.Length() == 3) { // we have an RGB, so set it baby uint32_t c = Rgb32(atoi(T[0]), atoi(T[1]), atoi(T[2])); m->SetMarkColour(c); } else { uint32_t c = Rgb32(0, 0, 255); m->SetMarkColour(c); } } break; } case ACTION_REPLY: { if (Arg1) { const char *s = Arg1; char *TemplateMsgId = LTokStr(s); SkipSep(s); char *ReplyAll = LTokStr(s); SkipSep(s); char *MarkReplied = LTokStr(s); Mail *Template = GetTemplateMail(App, TemplateMsgId); if (Template) { Thing *t = App->CreateThingOfType(MAGIC_MAIL); if (t) { Mail *n = t->IsMail(); if (n) { bool MarkOriginal = MarkReplied && atoi(MarkReplied); // Prepare mail... n->OnReply(m, ValidStr(ReplyAll)?atoi(ReplyAll)!=0:false, MarkOriginal); n->SetFlags(m->GetFlags() | MAIL_READY_TO_SEND); if (ValidStr(Template->GetSubject())) { n->SetSubject(Template->GetSubject()); } n->SetBody(ScribeInsertFields(Template->GetBody(), F)); // Save it... n->Save(0); // Send it... if not offline. LVariant Offline; App->GetOptions()->GetValue(OPT_WorkOffline, Offline); if (!Offline.CastInt32()) { App->PostEvent(M_COMMAND, IDM_SEND_MAIL, 0); } } else DeleteObj(t); } } DeleteArray(TemplateMsgId); DeleteArray(ReplyAll); DeleteArray(MarkReplied); } break; } case ACTION_FORWARD: { const char *s = Arg1; char *TemplateMsgId = LTokStr(s); SkipSep(s); char *Email = LTokStr(s); SkipSep(s); char *Attach = LTokStr(s); SkipSep(s); char *MarkForwarded = LTokStr(s); bool Attachments = Attach ? atoi(Attach)!=0 : true; if (ValidStr(Email)) { Thing *t = App->CreateThingOfType(MAGIC_MAIL); if (t) { Mail *n = t->IsMail(); if (n) { bool MarkOriginal = MarkForwarded && atoi(MarkForwarded); // Setup email... Mail *Template = TemplateMsgId ? GetTemplateMail(App, TemplateMsgId) : 0; if (Template) { n->SetSubject(Template->GetSubject()); if (ValidStr(Template->GetBody())) { n->SetBody(ScribeInsertFields(Template->GetBody(), F)); } if (Attachments) { List Att; m->GetAttachments(&Att); for (auto a: Att) { n->AttachFile(new Attachment(App, a)); } } } else { n->OnForward(m, MarkOriginal, Attachments); } n->SetFlags(m->GetFlags() | MAIL_READY_TO_SEND); LDataPropI *Addr = n->GetTo()->Create(n->GetObject()->GetStore()); if (Addr) { LVariant v; if (F->GetValue(Email, v)) { Addr->SetStr(FIELD_EMAIL, v.Str()); } else { Addr->SetStr(FIELD_EMAIL, Email); } n->GetTo()->Insert(Addr); } // Save it... n->Save(0); // Send it... if not offline. LVariant Offline; App->GetOptions()->GetValue(OPT_WorkOffline, Offline); if (!Offline.CastInt32()) { App->PostEvent(M_COMMAND, IDM_SEND_MAIL, 0); } } else DeleteObj(t); } } DeleteArray(Email); DeleteArray(Attach); DeleteArray(MarkForwarded); break; } case ACTION_BOUNCE: { const char *s = Arg1; char *Email = LTokStr(s); SkipSep(s); char *Attach = LTokStr(s); SkipSep(s); char *Mark = LTokStr(s); if (ValidStr(Email)) { Thing *t = App->CreateThingOfType(MAGIC_MAIL); if (t) { Mail *n = t->IsMail(); if (n) { bool Attachments = Attach && atoi(Attach); bool MarkOriginal = Mark && atoi(Mark); // Setup email... n->OnBounce(m, MarkOriginal, Attachments); n->SetFlags(m->GetFlags() | MAIL_READY_TO_SEND); LDataPropI *Addr = n->GetTo()->Create(n->GetObject()->GetStore()); if (Addr) { LVariant v; if (F->GetValue(Email, v)) { Addr->SetStr(FIELD_EMAIL, v.Str()); } else { Addr->SetStr(FIELD_EMAIL, Email); } n->GetTo()->Insert(Addr); } // Save it... n->Save(0); // Send it... if not offline. LVariant Offline; App->GetOptions()->GetValue(OPT_WorkOffline, Offline); if (!Offline.CastInt32()) { App->PostEvent(M_COMMAND, IDM_SEND_MAIL, 0); } } else DeleteObj(t); } } DeleteArray(Email); DeleteArray(Attach); DeleteArray(Mark); break; } case ACTION_SAVE_ATTACHMENTS: { const char *arg = Arg1; char *Dir = LTokStr(arg); SkipSep(arg); char *Types = LTokStr(arg); List Files; if (CollectAttachmentsByPattern(m, Types, Files)) { for (auto a: Files) { auto d = GetFileName(a->GetName()); if (d) { char Path[256]; LMakePath(Path, sizeof(Path), Dir, d); a->SaveTo(Path); } } } DeleteArray(Dir); DeleteArray(Types); break; } case ACTION_DELETE_ATTACHMENTS: { List Files; if (CollectAttachmentsByPattern(m, Arg1, Files)) { for (auto a: Files) { m->DeleteAttachment(a); } } break; } case ACTION_CHANGE_CHARSET: { m->SetBodyCharset(Arg1); break; } } return Status; } int CsCmp(LCharset **a, LCharset **b) { return _stricmp((*a)->Charset, (*b)->Charset); } void FilterAction::DescribeHtml(Filter *Flt, LStream &s) { s.Print("%s ", GetText(0)); switch (Type) { case ACTION_MOVE_TO_FOLDER: { ScribeFolder *Folder = Flt->App->GetFolder(Arg1); if (Folder) s.Print("\"%s\"\n", Arg1.Get()); else s.Print("\"%s\"\n", Arg1.Get()); break; } case ACTION_DELETE: break; case ACTION_PRINT: break; case ACTION_PLAY_SOUND: break; case ACTION_OPEN: break; case ACTION_EXECUTE: break; case ACTION_MARK: break; case ACTION_SET_READ: break; case ACTION_LABEL: break; case ACTION_EMPTY_FOLDER: break; case ACTION_MARK_AS_SPAM: break; case ACTION_REPLY: break; case ACTION_FORWARD: break; case ACTION_BOUNCE: break; case ACTION_SAVE_ATTACHMENTS: break; case ACTION_DELETE_ATTACHMENTS: break; case ACTION_CHANGE_CHARSET: break; case ACTION_COPY: break; case ACTION_EXPORT: break; } } void FilterAction::Browse(ScribeWnd *App, LView *Parent) { if (!Parent) return; switch (Type) { default: LAssert(0); break; case ACTION_MOVE_TO_FOLDER: case ACTION_COPY: case ACTION_EMPTY_FOLDER: { auto Dlg = new FolderDlg(Parent, App, MAGIC_MAIL); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) Arg1 = Dlg->Get(); delete dlg; }); break; } case ACTION_EXPORT: { auto s = new LFileSelect(Parent); s->OpenFolder([this](auto s, auto status) { if (status) Arg1 = s->Name(); delete s; }); break; } case ACTION_DELETE: { auto RClick = new LSubMenu; if (RClick) { RClick->AppendItem("Local (default)", IDM_LOCAL, true); RClick->AppendItem("From Server", IDM_SERVER, true); RClick->AppendItem("Local and from Server", IDM_LOCAL_AND_SERVER, true); LMouse m; if (Parent->GetMouse(m, true)) { switch (RClick->Float(Parent, m.x, m.y)) { case IDM_LOCAL: { Arg1 = "local"; break; } case IDM_SERVER: { Arg1 = "server"; break; } case IDM_LOCAL_AND_SERVER: { Arg1 = "local,server"; break; } } } DeleteObj(RClick); } break; } case ACTION_OPEN: { // no configuration break; } case ACTION_SET_READ: { auto RClick = new LSubMenu; if (RClick) { RClick->AppendItem("Read", IDM_TRUE, true); RClick->AppendItem("Unread", IDM_FALSE, true); RClick->AppendItem("Unread But Not New", IDM_NOTNEW, true); LMouse m; if (Parent->GetMouse(m, true)) { switch (RClick->Float(Parent, m.x, m.y)) { case IDM_TRUE: { Arg1 = "true"; break; } case IDM_FALSE: { Arg1 = "false"; break; } case IDM_NOTNEW: { Arg1 = "false,notnew"; break; } } } DeleteObj(RClick); } break; } case ACTION_MARK: { auto RClick = new LSubMenu; if (RClick) { BuildMarkMenu(RClick, MS_One, 0); LMouse m; if (Parent->GetMouse(m, true)) { int Result = RClick->Float(Parent, m.x, m.y); if (Result == IDM_UNMARK) { Arg1 = "False"; } else if (Result >= IDM_MARK_BASE) { char s[32]; sprintf_s(s, sizeof(s), "%i,%i,%i", R32(MarkColours32[Result-IDM_MARK_BASE]), G32(MarkColours32[Result-IDM_MARK_BASE]), B32(MarkColours32[Result-IDM_MARK_BASE])); Arg1 = s; } } } break; } case ACTION_PRINT: { LPrinter Info; #ifdef _MSC_VER #pragma message ("Warning: ACTION_PRINT not implemented.") #endif /* Info.Serialize(Arg1, false); if (Info.Browse(Parent)) { Info.Serialize(Arg1, true); } */ break; } case ACTION_PLAY_SOUND: case ACTION_EXECUTE: { auto Select = new LFileSelect(Parent); Select->Parent(Parent); if (Type == ACTION_PLAY_SOUND) { Select->Type("Wave files", "*.wav"); } else { Select->Type("Executables", "*.exe"); Select->Type("All Files", LGI_ALL_FILES); } Select->Name(Arg1); Select->Open([this](auto s, auto ok) { if (ok) Arg1 = s->Name(); delete s; }); break; } case ACTION_REPLY: { auto Dlg = new BrowseReply(App, Parent, Arg1); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) Arg1 = Dlg->Arg; delete dlg; }); break; } case ACTION_FORWARD: { auto Dlg = new BrowseForward(App, Parent, Arg1, true); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) Arg1 = Dlg->Arg; delete dlg; }); break; } case ACTION_BOUNCE: { auto Dlg = new BrowseForward(App, Parent, Arg1, false); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) Arg1 = Dlg->Arg; delete dlg; }); break; } case ACTION_SAVE_ATTACHMENTS: { auto Dlg = new BrowseSaveAttach(App, Parent, Arg1); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id) Arg1 = Dlg->Arg; delete dlg; }); break; } case ACTION_CHANGE_CHARSET: { auto s = new LSubMenu; if (s) { LArray Cs; for (LCharset *c = LGetCsList(); c->Charset; c++) { Cs.Add(c); } Cs.Sort(CsCmp); for (unsigned i=0; iCharset[0] != Cs[i]->Charset[0]) break; n++; } if (n > 1) { char a[64]; char *One = LSeekUtf8(Cs[i]->Charset, 1); ssize_t Len = One - Cs[i]->Charset; memcpy(a, Cs[i]->Charset, Len); strcpy_s(a + Len, sizeof(a) - Len, "..."); auto Sub = s->AppendSub(a); if (Sub) { for (unsigned k=0; kAppendItem(Cs[i+k]->Charset, i+k+1, Cs[i+k]->IsAvailable()); } i += n - 1; } } else { s->AppendItem(Cs[i]->Charset, i+1, Cs[i]->IsAvailable()); } } LMouse m; Parent->GetMouse(m, true); int Result = s->Float(Parent, m.x, m.y, true); if (Result) { Result--; if (Result >= 0 && Result < (int)Cs.Length()) { Arg1 = Cs[Result]->Charset; } } DeleteObj(s); } break; } } } /////////////////////////////////////////////////////////////// int Filter::MaxIndex = -1; Filter::Filter(ScribeWnd *window, LDataI *object) : Thing(window, object) { DefaultObject(object); d = new FilterPrivate; Ui = 0; Current = 0; IgnoreCheckEvents = true; ChkIncoming = new LListItemCheckBox(this, 2, GetIncoming()!=0); ChkOutgoing = new LListItemCheckBox(this, 3, GetOutgoing()!=0); ChkInternal = new LListItemCheckBox(this, 4, GetInternal()!=0); IgnoreCheckEvents = false; } Filter::~Filter() { Empty(); DeleteObj(d); } bool Filter::GetFormats(bool Export, LString::Array &MimeTypes) { MimeTypes.Add(sTextXml); return MimeTypes.Length() > 0; } char *Filter::GetDropFileName() { if (!DropFileName) { auto Nm = GetName(); char f[256]; if (Nm) { LUtf8Ptr o(f); for (LUtf8Ptr i(Nm); (uint32_t)i; i++) { if (!strchr(LGI_IllegalFileNameChars, (uint32_t)i)) o.Add(i); } o.Add(0); } else strcpy_s(f, sizeof(f), "Filter"); strcat_s(f, sizeof(f), ".xml"); DropFileName.Reset(NewStr(f)); } return DropFileName; } bool Filter::GetDropFiles(LString::Array &Files) { char Tmp[MAX_PATH_LEN]; LMakePath(Tmp, sizeof(Tmp), ScribeTempPath(), GetDropFileName()); LAutoPtr Out(new LFile); if (!Out->Open(Tmp, O_WRITE)) return false; if (!Export(AutoCast(Out), sTextXml)) return false; Files.Add(Tmp); return true; } Thing::IoProgress Filter::Import(IoProgressImplArgs) { if (Stricmp(mimeType, sTextXml) && Stricmp(mimeType, sMimeXml)) IoProgressNotImpl(); LXmlTree Tree; LXmlTag r; if (!Tree.Read(&r, stream)) IoProgressError("Xml parse error."); if (!r.IsTag("Filter")) IoProgressError("No filter tag."); Empty(); LXmlTag *t = r.GetChildTag("Name"); if (t && t->GetContent()) SetName(t->GetContent()); SetIndex(r.GetAsInt("index")); if ((t = r.GetChildTag(ELEMENT_CONDITIONS))) { LStringPipe p; if (Tree.Write(t, &p)) { LAutoString s(p.NewStr()); ConditionsCache.Reset(); SetConditionsXml(s); } if ((t = r.GetChildTag("Actions"))) { LStringPipe p; if (Tree.Write(t, &p)) { LAutoString s(p.NewStr()); SetActionsXml(s); } else IoProgressError("Xml write failed."); } } IoProgressSuccess(); } Thing::IoProgress Filter::Export(IoProgressImplArgs) { if (Stricmp(mimeType, sMimeXml)) IoProgressNotImpl(); LXmlTag r("Filter"); LXmlTag *t; if ((t = r.CreateTag("Name"))) t->SetContent(GetName()); r.SetAttr("index", GetIndex()); LAutoPtr Cond = Parse(false); r.InsertTag(Cond.Release()); LAutoPtr Act = Parse(true); r.InsertTag(Act.Release()); LXmlTree tree; if (!tree.Write(&r, stream)) IoProgressError("Failed to write xml."); IoProgressSuccess(); } /// This filters a list of email. The email will have it's NewEmail state set to /// one of three things: /// If the email is not filtered then: /// Mail::NewEmailBayes /// If the email is filtered but not set to !NEW then: /// Mail::NewEmailGrowl /// If the email is filtered AND set to !NEW then: /// Mail::NewMailNone int Filter::ApplyFilters(LView *Parent, List &Filters, List &Email) { int Status = 0; ScribeWnd *App = Filters.Length() > 0 ? Filters[0]->App : NULL; if (!App) return 0; bool Logging = App->LogFilterActivity(); LStream *LogStream = NULL; if (Logging) LogStream = App->ShowScriptingConsole(); LAutoPtr Prog; if (Parent && Prog.Reset(new LProgressDlg(Parent))) { Prog->SetRange(Email.Length()); Prog->SetDescription("Filtering..."); Prog->SetType("email"); } for (auto m: Email) { bool Act = false; bool Stop = false; m->IncRef(); for (auto f: Filters) { if (Stop) break; if (f->Test(m, Stop, LogStream)) { f->DoActions(m, Stop, LogStream); Act = true; } } if (Act) { Status++; if (m && m->NewEmail == Mail::NewEmailFilter) { m->NewEmail = Mail::NewEmailGrowl; } } else if (m && m->NewEmail == Mail::NewEmailFilter) { m->NewEmail = Mail::NewEmailBayes; } m->DecRef(); m = NULL; if (Prog) { Prog->Value(Prog->Value() + 1); if (Prog->IsCancelled()) break; } } return Status; } Filter *Filter::GetFilterAt(int Index) { ScribeFolder *f = GetFolder(); if (f) { for (auto t: f->Items) { Filter *f = t->IsFilter(); if (f && f->GetIndex() == Index) { return f; } } } return 0; } enum TermType { TermString, TermVariant }; class ExpTerm { public: TermType Type; LVariant Value; ExpTerm(TermType t) { Type = t; } }; bool Filter::Evaluate(char *str, LVariant &v) { char *BufStr = NewStr(str); char *s = BufStr; if (s) { List Terms; const char *Ws = " \t\r\n"; while (s && *s) { while (*s && strchr(Ws, *s)) s++; if (*s && *s == '\"') { char *Start = ++s; char *In = s; char *Out = s; while (*In) { if (In[0] == '\"') { if (In[1] == '\"') { // Quote *Out++ = '\"'; In += 2; } else { // End of string In++; break; } } else { *Out++ = *In++; } } *Out++ = 0; s = In; ExpTerm *t; Terms.Insert(t = new ExpTerm(TermString)); if (t) { t->Value = Start; } } else { char *Start = s; while (*s && !strchr(Ws, *s)) s++; while (*s && strchr(Ws, *s)) s++; char *Src = NewStr(Start, s-Start); if (Src) { ExpTerm *t; Terms.Insert(t = new ExpTerm(TermVariant)); if (t) { if (GetValue(Start, t->Value)) { switch (t->Value.Type) { default: break; case GV_BINARY: case GV_LIST: case GV_DOM: case GV_VOID_PTR: { v = t->Value; DeleteArray(BufStr); DeleteArray(Src); Terms.DeleteObjects(); return true; } } } else { t->Type = TermString; t->Value = Src; } } DeleteArray(Src); } } } LStringPipe Out; auto It = Terms.begin(); ExpTerm *t = *It; if (t) { if (Terms.Length() > 1) { // Collapse terms for (; t; t=*(++It)) { char Buf[128]; switch (t->Value.Type) { default: break; case GV_INT32: { sprintf_s(Buf, sizeof(Buf), "%i", t->Value.Value.Int); Out.Push(Buf); break; } case GV_INT64: { sprintf_s(Buf, sizeof(Buf), LPrintfInt64, t->Value.Value.Int64); Out.Push(Buf); break; } case GV_BOOL: { sprintf_s(Buf, sizeof(Buf), "%i", (int)t->Value.Value.Bool); Out.Push(Buf); break; } case GV_DOUBLE: { sprintf_s(Buf, sizeof(Buf), "%g", t->Value.Value.Dbl); Out.Push(Buf); break; } case GV_STRING: { if (t->Value.Str()) Out.Push(t->Value.Str()); break; } case GV_DATETIME: { t->Value.Value.Date->Get(Buf, sizeof(Buf)); Out.Push(Buf); break; } case GV_BINARY: case GV_LIST: case GV_DOM: case GV_NULL: case GV_VOID_PTR: { break; } } } v.OwnStr(Out.NewStr()); } else { v = t->Value; } } Terms.DeleteObjects(); } DeleteArray(BufStr); return true; } bool Filter::SetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Field = StrToDom(Name); switch (Field) { case SdName: SetName(Value.Str()); break; case SdConditionsXml: ConditionsCache.Reset(); SetConditionsXml(Value.Str()); break; case SdActionsXml: SetActionsXml(Value.Str()); break; case SdDateModified: return SetDateField(FIELD_DATE_MODIFIED, Value); default: return false; } return true; } bool Filter::CallMethod(const char *MethodName, LScriptArguments &Args) { ScribeDomType Method = StrToDom(MethodName); switch (Method) { case SdAddCondition: // Type: (String Feild, String Op, String Value) { if (Args.Length() != 3) { LgiTrace("%s:%i - SdAddCondition: wrong number of parameters %i (expecting 3)\n", _FL, Args.Length()); break; } const char *Field = Args[0]->Str(); const char *Op = Args[1]->Str(); const char *Value = Args[2]->Str(); if (!Field || !Op || !Value) { LgiTrace("%s:%i - SdAddCondition: Missing values.\n", _FL); break; } LAutoPtr t = Parse(false); LXmlTag *Cond; if (!t || !t->IsTag(ELEMENT_CONDITIONS) || (Cond = t->Children[0]) == NULL) { LgiTrace("%s:%i - SdAddCondition: Failed to parse conditions.\n", _FL); break; } if (!Cond->IsTag(ELEMENT_AND) && !Cond->IsTag(ELEMENT_OR)) { LgiTrace("%s:%i - SdAddCondition: Unexpected root operator.\n", _FL); break; } // Check that the condition doesn't already exist... for (auto c : Cond->Children) { if (c->IsTag(ELEMENT_CONDITION)) { char *CFeild = c->GetAttr(ATTR_FIELD); char *COp = c->GetAttr(ATTR_OP); char *CVal = c->GetAttr(ATTR_VALUE); if (!Stricmp(Field, CFeild) && !Stricmp(Op, COp) && !Stricmp(Value, CVal)) { return true; } } } LXmlTag *n = new LXmlTag(ELEMENT_CONDITION); if (!n) { LgiTrace("%s:%i - SdAddCondition: Alloc failed.\n", _FL); break; } n->SetAttr(ATTR_FIELD, Field); n->SetAttr(ATTR_OP, Op); n->SetAttr(ATTR_VALUE, Value); Cond->InsertTag(n); LXmlTree tree; LStringPipe p; if (!tree.Write(t, &p)) { LgiTrace("%s:%i - SdAddCondition: Failed to write XML.\n", _FL); break; } LAutoString a(p.NewStr()); ConditionsCache.Reset(); SetConditionsXml(a); SetDirty(true); return true; } case SdAddAction: // Type: (String ActionName, String Value) { if (Args.Length() != 2) { LgiTrace("%s:%i - SdAddAction: wrong number of parameters %i (expecting 3)\n", _FL, Args.Length()); break; } LString Action = Args[0]->CastString(); if (!Action) { LgiTrace("%s:%i - SdAddAction: Missing action name.\n", _FL); break; } FilterAction a(GetObject()->GetStore()); for (ActionName *an = ActionNames; an->Id; an++) { if (Action.Equals(an->Default)) { a.Type = (FilterActionTypes) (an - ActionNames); a.Arg1 = Args[1]->CastString(); AddAction(&a); return true; } } LgiTrace("%s:%i - SdAddAction: Action '%s' not found.\n", _FL, Action.Get()); return false; } case SdStopFiltering: // Type: () { if (d->Stop) { *d->Stop = true; return true; } else LgiTrace("%s:%i - No stop parameter to set.\n", _FL); return true; } case SdDoActions: // Type: (Mail Object) { if (Args.Length() == 1) { LDom *d = Args[0]->CastDom(); Mail *m = dynamic_cast(d); if (m) { bool Stop = false; return DoActions(m, Stop); } else LgiTrace("%s:%i - DoActions: failed to cast arg1 to Mail object.\n", _FL); } else LgiTrace("%s:%i - DoActions is expecting 1 argument, not %i.\n", _FL, Args.Length()); return true; } default: break; } return Thing::CallMethod(MethodName, Args); } bool Filter::GetVariant(const char *Var, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Var); switch (Fld) { case SdMail: // Type: Mail { if (!Current) return false; Value = *Current; break; } case SdScribe: // Type: ScribeWnd { Value = (LDom*)App; break; } case SdName: // Type: String { Value = GetName(); break; } case SdTestConditions: // Type: Bool { if (Current && *Current) { bool s; bool &Stop = d->Stop ? *d->Stop : s; Value = EvaluateXml(*Current, Stop, d->Log); } else return false; break; } case SdType: // Type: Int32 { Value = GetObject()->Type(); break; } case SdConditionsXml: // Type: String { Value = GetConditionsXml(); break; } case SdActionsXml: // Type: String { Value = GetActionsXml(); break; } case SdIndex: // Type: Int32 { Value = GetIndex(); break; } case SdDateModified: // Type: DateTime { return GetDateField(FIELD_DATE_MODIFIED, Value); } default: { return false; } } return true; } Thing &Filter::operator =(Thing &t) { Filter *f = t.IsFilter(); if (f) { if (GetObject() && f->GetObject()) { GetObject()->CopyProps(*f->GetObject()); } } return *this; } int Filter::Compare(LListItem *Arg, ssize_t Field) { Filter *a = dynamic_cast(Arg); if (a) { switch (Field) { case FIELD_FILTER_NAME: return a && GetName() ? _stricmp(GetName(), a->GetName()) : -1; case FIELD_FILTER_INDEX: return GetIndex() - a->GetIndex(); case FIELD_FILTER_INCOMING: return GetIncoming() - a->GetIncoming(); case FIELD_FILTER_OUTGOING: return GetOutgoing() - a->GetOutgoing(); } } return 0; } int Filter::GetImage(int Flags) { return ICON_FILTER; } void DisplayXml(LStream &s, char *xml) { char *start = xml; char *e; for (e = xml; *e; e++) { if (*e == '<') { s.Write(start, e - start); s.Print("<"); start = e + 1; } } s.Write(start, e - start); } void DescribeCondition(LStream &s, LXmlTag *t) { int i = 0; if (t->IsTag(ELEMENT_AND) || t->IsTag(ELEMENT_OR)) { for (auto c: t->Children) { LStringPipe p; DescribeCondition(p, c); LAutoString a(p.NewStr()); if (i) s.Print(" %s ", t->GetTag()); s.Print("(%s)", a.Get()); i++; } } else { // Condition char *f = t->GetAttr(ATTR_FIELD); char *v = t->GetAttr(ATTR_VALUE); int Not = t->GetAsInt(ATTR_NOT) > 0; const char *o = t->GetAttr(ATTR_OP); if (o && IsDigit(*o)) o = OpNames[atoi(o)]; s.Print("%s%s %s \"%s\"", Not ? "!" : "", f, o, v); } } LAutoString Filter::DescribeHtml() { LStringPipe p(256); p.Print("<style>\n" ".error { color: red; font-weight: bold; }\n" ".op { color: blue; }\n" ".var { color: #800; }\n" "pre { color: green; }\n" "</style>\n" "\n" "Name: %s
\n", GetName()); p.Print("Incoming: %i
\n", GetIncoming()); p.Print("Outgoing: %i
\n", GetOutgoing()); if (GetConditionsXml()) { LAutoPtr r = Parse(false); if (r && r->Children.Length()) { p.Print("Conditions:
    \n"); for (auto c: r->Children) { p.Print("
  • "); DescribeCondition(p, c); } p.Print("
\n"); } } if (GetActionsXml()) { LAutoPtr r = Parse(true); if (r && r->Children.Length()) { p.Print("Actions:
    \n"); for (auto c: r->Children) { if (!c->IsTag(ELEMENT_ACTION)) continue; LAutoPtr a(new FilterAction(GetObject()->GetStore())); if (a->Set(c)) { p.Print("
  • "); a->DescribeHtml(this, p); } } p.Print("
\n"); } } if (GetScript()) { LXmlTree t; LAutoString e(t.EncodeEntities(GetScript(), -1, "<>")); p.Print("Script:
%s
\n", e.Get()); } p.Print("\n"); return LAutoString(p.NewStr()); } void Filter::Empty() { if (GetObject()) { ConditionsCache.Reset(); SetConditionsXml(0); SetName(0); } } bool Filter::EvaluateTree(LXmlTag *n, Mail *m, bool &Stop, LStream *Log) { bool Status = false; if (n && n->GetTag() && m && m->GetObject()) { if (n->IsTag(ELEMENT_AND)) { if (Log) Log->Print("\tAnd {\n"); for (auto c: n->Children) { if (!EvaluateTree(c, m, Stop, Log)) { if (Log) Log->Print("\t} (false)\n"); return false; } } if (Log) Log->Print("\t} (true)\n"); Status = true; } else if (n->IsTag(ELEMENT_OR)) { if (Log) Log->Print("\tOr {\n"); for (auto c: n->Children) { if (EvaluateTree(c, m, Stop, Log)) { if (Log) Log->Print("\t} (true)\n"); return true; } } if (Log) Log->Print("\t} (false)\n"); } else if (n->IsTag(ELEMENT_CONDITION)) { FilterCondition *c = new FilterCondition; if (c) { if (!c->Set(n)) { LAssert(0); } else { Status = c->Test(this, m, Log); if (c->Not) Status = !Status; if (Log) { Log->Print("\tResult=%i (not=%i)\n", Status, c->Not); } } DeleteObj(c); } } else LAssert(0); } else LAssert(0); return Status; } bool FilterCondition::Set(LXmlTag *t) { if (!t) return false; Source.Reset(NewStr(t->GetAttr(ATTR_FIELD))); Value.Reset(NewStr(t->GetAttr(ATTR_VALUE))); Not = t->GetAsInt(ATTR_NOT) > 0; char *o = t->GetAttr(ATTR_OP); if (o) { if (IsDigit(*o)) Op = atoi(o); else { for (int i=0; OpNames[i]; i++) { if (!_stricmp(OpNames[i], o)) { Op = i; break; } } } } return true; } bool Filter::EvaluateXml(Mail *m, bool &Stop, LStream *Log) { bool Status = false; if (ValidStr(GetConditionsXml())) { if (!ConditionsCache) ConditionsCache = Parse(false); if (ConditionsCache && ConditionsCache->Children.Length()) Status = EvaluateTree(ConditionsCache->Children[0], m, Stop, Log); } return Status; } bool Filter::Test(Mail *m, bool &Stop, LStream *Log) { bool Status = false; if (Log) Log->Print("Filter.Test '%s':\n", GetName()); Current = &m; if (m && m->GetObject()) { d->Stop = &Stop; d->Log = Log; if (ValidStr(GetScript())) { OnFilterScript(this, m, GetScript()); } else if (ValidStr(GetConditionsXml())) { Status = EvaluateXml(m, Stop, Log); } else LAssert(0); d->Stop = 0; d->Log = 0; } Current = 0; return Status; } bool Filter::DoActions(Mail *&m, bool &Stop, LStream *Log) { if (!App || !m) return false; Current = &m; LAutoPtr r = Parse(true); if (r) { LArray Act; for (auto c: r->Children) { if (c->IsTag(ELEMENT_ACTION)) { FilterAction *a = new FilterAction(GetObject()->GetStore()); if (a) { if (a->Set(c)) Act.Add(a); else LAssert(!"Can't convert xml to action."); } } } for (unsigned i=0; iGetObject(); i++) { FilterAction *a = Act[i]; a->Do(this, App, m, Log); } Act.DeleteObjects(); } Current = 0; if (GetStopFiltering()) { Stop = true; } return true; } ThingUi *Filter::DoUI(MailContainer *c) { if (!Ui) { MaxIndex = MAX(MaxIndex, GetIndex()); if (GetIndex() < 0) { SetIndex(++MaxIndex); } Ui = new FilterUi(this); } #if WINNATIVE if (Ui) SetForegroundWindow(Ui->Handle()); #endif return Ui; } LAutoPtr Filter::Parse(bool Actions) { LAutoPtr Ret; auto RawXml = Actions ? GetActionsXml() : GetConditionsXml(); if (!RawXml) return Ret; LMemStream Xml(RawXml, strlen(RawXml), false); LXmlTree t; Ret.Reset(new LXmlTag); if (!t.Read(Ret, &Xml)) Ret.Reset(); return Ret; } void Filter::AddAction(FilterAction *Action) { if (!Action) return; LAutoPtr a = Parse(true); if (!a) a.Reset(new LXmlTag("Actions")); LAutoPtr n(new LXmlTag(ELEMENT_ACTION)); if (!Action->Get(n)) return; a->InsertTag(n.Release()); LXmlTree t; LStringPipe p; if (!t.Write(a, &p)) return; LAutoString Xml(p.NewStr()); SetActionsXml(Xml); SetDirty(true); } bool Filter::Save(ScribeFolder *Into) { bool Status = false; if (!Into) { Into = GetFolder(); if (!Into) { Into = App->GetFolder(FOLDER_FILTERS); } } if (Into) { SetParentFolder(Into); if (ChkIncoming) SetIncoming(ChkIncoming->Value()!=0); if (ChkOutgoing) SetOutgoing(ChkOutgoing->Value()!=0); if (ChkInternal) SetInternal(ChkInternal->Value()!=0); LDateTime Now; GetObject()->SetDate(FIELD_DATE_MODIFIED, &Now.SetNow()); Store3Status s = Into->WriteThing(this); Status = s != Store3Error; if (Status) SetDirty(false); } return Status; } void Filter::OnPaint(ItemPaintCtx &Ctx) { LListItem::OnPaint(Ctx); } void Filter::OnMouseClick(LMouse &m) { LListItem::OnMouseClick(m); if (m.Double()) { // open the UI for the Item DoUI(); } else if (m.Right()) { // open the right click menu auto RClick = new LSubMenu; if (RClick) { RClick->AppendItem(LLoadString(IDS_OPEN), IDM_OPEN); RClick->AppendItem(LLoadString(IDS_DELETE), IDM_DELETE); RClick->AppendItem(LLoadString(IDS_EXPORT), IDM_EXPORT); if (Parent->GetMouse(m, true)) { switch (RClick->Float(Parent, m.x, m.y)) { case IDM_OPEN: { DoUI(); break; } case IDM_DELETE: { LVariant ConfirmDelete; App->GetOptions()->GetValue(OPT_ConfirmDelete, ConfirmDelete); if (!ConfirmDelete.CastInt32() || LgiMsg(GetList(), LLoadString(IDS_DELETE_ASK), AppName, MB_YESNO) == IDYES) { List Del; LList *ParentList = LListItem::Parent; if (ParentList && ParentList->GetSelection(Del)) { for (auto m: Del) { auto f = dynamic_cast(m); if (f) f->OnDelete(); } } } break; } case IDM_EXPORT: { ExportAll(GetList(), sTextXml, NULL); break; } } } DeleteObj(RClick); } } } int *Filter::GetDefaultFields() { static int Def[] = { FIELD_FILTER_NAME, 0, 0, 0 }; return Def; } const char *Filter::GetFieldText(int Field) { switch (Field) { case FIELD_FILTER_NAME: return GetName(); case FIELD_FILTER_CONDITIONS_XML: return GetConditionsXml(); case FIELD_FILTER_ACTIONS_XML: return GetActionsXml(); case FIELD_FILTER_SCRIPT: return GetScript(); } return NULL; } void Filter::OnColumnNotify(int Col, int64 Data) { if (!IgnoreCheckEvents) { switch (Col) { case 2: case 3: case 4: { SetDirty(true); break; } } } } const char *Filter::GetText(int i) { int Field = 0; if (FieldArray.Length()) { if (i >= 0 && i < (int)FieldArray.Length()) Field = FieldArray[i]; } else if (i >= 0 && i < CountOf(DefaultFilterFields)) { Field = DefaultFilterFields[i]; } switch (Field) { case FIELD_FILTER_NAME: { return GetName(); break; } case FIELD_FILTER_INDEX: { static char i[16]; sprintf_s(i, sizeof(i), "%i", GetIndex()); return i; break; } } return 0; } int FilterIndexCmp(Filter **a, Filter **b) { int ai = (*a)->GetIndex(); int bi = (*b)->GetIndex(); return ai - bi; } void Filter::Reindex(ScribeFolder *Folder) { if (!Folder) return; LArray Zero; LArray Sort; for (auto t : Folder->Items) { Filter *f = t->IsFilter(); if (f) { int Idx = f->GetIndex(); if (Idx > 0) { Sort.Add(f); } else { Zero.Add(f); } } } Sort.Sort(FilterIndexCmp); LArray Map; for (unsigned n=0; nGetIndex(); if (Idx != i + 1) { Sort[i]->SetIndex(i + 1); Sort[i]->SetDirty(); Sort[i]->Update(); Changed = true; } } if (Changed) Folder->ReSort(); } ////////////////////////////////////////////////////////// enum ConditionOptions { FIELD_ANYWHERE = FIELD_MAX }; int FilterCallback( LFilterView *View, LFilterItem *Item, LFilterMenu Menu, LRect &r, LArray *GetList, void *Data) { int Status = -1; if (!View) return Status; switch (Menu) { case FMENU_FIELD: { LSubMenu s; LString::Array Names; ItemFieldDef *FieldDefs = MailFieldDefs; for (ItemFieldDef *i = FieldDefs; i->DisplayText; i++) { const char *Trans = LLoadString(i->FieldId); auto idx = (i - FieldDefs) + 1; Names[idx] = DomToStr(i->Dom); s.AppendItem(Trans ? Trans : i->DisplayText, (int)idx, true); } s.AppendItem(LLoadString(IDS_ATTACHMENTS_DATA), FIELD_ATTACHMENTS_DATA, true); s.AppendItem(LLoadString(IDS_ATTACHMENTS_NAME), FIELD_ATTACHMENTS_NAME, true); s.AppendItem(LLoadString(IDS_MEMBER_OF_GROUP), FIELD_MEMBER_OF_GROUP, true); s.AppendItem(LLoadString(IDS_ANYWHERE), FIELD_ANYWHERE, true); LPoint p(r.x1, r.y2 + 1); View->PointToScreen(p); int Cmd = s.Float(View, p.x, p.y, true); switch (Cmd) { case FIELD_ATTACHMENTS_DATA: Item->SetField("mail.Attachments"); break; case FIELD_ATTACHMENTS_NAME: Item->SetField("mail.AttachmentNames"); break; case FIELD_MEMBER_OF_GROUP: Item->SetField("mail.From.Groups"); break; case FIELD_ANYWHERE: Item->SetField("mail.*"); break; default: if (Cmd > 0 && Cmd < Names.Length()) Item->SetField(LString("mail.") + Names[Cmd]); break; } break; } case FMENU_OP: { if (GetList) { for (const char **o = GetOpNames(true); *o; o++) { GetList->Add(NewStr(*o)); } Status = true; } /* else { auto s = new LSubMenu; if (s) { int n = 1; for (char **o = GetOpNames(true); *o; o++) { s->AppendItem(*o, n++, true); } LPoint p(r.x1, r.y2 + 1); View->PointToScreen(p); int Cmd = s->Float(View, p.x, p.y, true); if (Cmd > 0) { Item->SetOp(OpNames[Cmd - 1]); } DeleteObj(s); } } */ break; } case FMENU_VALUE: { break; } } return Status; } ////////////////////////////////////////////////////////// struct FilterUiPriv { }; FilterUi::FilterUi(Filter *item) : ThingUi(item, "Filter") { d = new FilterUiPriv; Item = item; if (!(Item && Item->App)) { return; } Script = 0; Tab = 0; Actions = 0; Conditions = 0; LRect r(100, 100, 800, 600); SetPos(r); MoveSameScreen(item->App); // Create window #if WINNATIVE CreateClassW32("FilterUi", LoadIcon(LProcessInst(), MAKEINTRESOURCE(IDI_FILTER))); #endif if (Attach(0)) { // Setup UI Commands.Toolbar = Item->App->LoadToolbar(this, Item->App->GetResourceFile(ResToolbarFile), Item->App->GetToolbarImgList()); if (Commands.Toolbar) { Commands.Toolbar->Attach(this); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_SAVE)), IDM_SAVE, TBT_PUSH, true, IMG_SAVE); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_SAVE_CLOSE)), IDM_SAVE_CLOSE, TBT_PUSH, true, IMG_SAVE_AND_CLOSE); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_DELETE)), IDM_DELETE, TBT_PUSH, true, IMG_TRASH); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_HELP)), IDM_HELP, TBT_PUSH, true, IMG_HELP); Commands.SetupCallbacks(GetItem()->App, this, GetItem(), LThingUiToolbar); } LTabPage *Cond = 0; LTabPage *Act = 0; Tab = new LTabView(91, 0, 0, 1000, 1000, 0); if (Tab) { Tab->Attach(this); Tab->SetPourChildren(true); LTabPage *Filter = Tab->Append(LLoadString(IDS_FILTER)); if (Filter) { #ifdef _DEBUG auto Status = #endif Filter->LoadFromResource(IDD_FILTER); LAssert(Status); Name(Filter->Name()); } Cond = Tab->Append(LLoadString(IDS_CONDITIONS)); if (Cond) { Conditions = new LFilterView(FilterCallback, Item); if (Conditions) { Cond->Append(Conditions); Conditions->SetPourLargest(true); } } Act = Tab->Append(LLoadString(IDS_ACTIONS)); if (Act) { #ifdef _DEBUG auto Status = #endif Act->LoadFromResource(IDD_FILTER_ACTION); LAssert(Status); if (GetViewById(IDC_FILTER_ACTIONS, Actions)) Actions->MultiSelect(false); } LTabPage *ScriptTab = Tab->Append(""); if (ScriptTab && ScriptTab->LoadFromResource(IDD_FILTER_SCRIPT)) { if (GetViewById(IDC_SCRIPT, Script)) { Script->SetWrapType(L_WRAP_NONE); Script->Sunken(true); Script->SetPourLargest(true); } else LAssert(0); } } // Show window Visible(true); if (Cond && Item) { LCombo *Cbo; if (GetViewById(IDC_ACTION, Cbo)) { for (ActionName *o = ActionNames; o->Id; o++) { const char *s = LLoadString(o->Id, o->Default); Cbo->Insert(s); } } } OnLoad(); } RegisterHook(this, LKeyEvents); } FilterUi::~FilterUi() { if (Item) Item->Ui = 0; DeleteObj(d); } bool FilterUi::OnViewKey(LView *v, LKey &k) { if (k.CtrlCmd()) { switch (k.c16) { case 's': case 'S': { if (k.Down()) OnSave(); return true; } case 'w': case 'W': { if (k.Down()) { OnSave(); Quit(); } return true; } } } return false; } int FilterUi::OnNotify(LViewI *Col, LNotification n) { int Reindex = 0; int InsertOffset = 0; switch (Col->GetId()) { case IDC_TYPE_CBO: case IDC_ARG_EDIT: { if (Actions) { List Sel; if (Actions->GetSelection(Sel)) { for (auto a: Sel) a->OnNotify(Col, n); } } break; } case IDC_BROWSE_ARG: { if (Actions) { List Sel; if (Actions->GetSelection(Sel)) { FilterAction *a = Sel[0]; if (a) { a->Browse(Item->App, this); a->OnNotify(Col, n); } } } break; } case IDC_UP: InsertOffset = -1; // fall thru case IDC_DOWN: { if (!InsertOffset) InsertOffset = 1; if (!Actions) break; List Items; if (!Actions->GetSelection(Items) && Items[0]) break; int Idx = Actions->IndexOf(Items[0]) + InsertOffset; FilterAction *Last = 0; for (auto a: Items) { Actions->Remove(a); Actions->Insert(a, Idx++); Last = a; } if (Last) { Actions->Focus(true); Last->Select(true); } break; } case IDC_NEW_FILTER_ACTION: { if (Actions) { FilterAction *n = new FilterAction(Item->GetObject()->GetStore()); Actions->Insert(n); Actions->Select(n); Actions->Focus(true); } break; } case IDC_DELETE_FILTER_ACTION: { if (Actions) { List Items; if (Actions->GetSelection(Items) && Items[0]) { int Idx = Actions->IndexOf(Items[0]); Items.DeleteObjects(); if (Idx >= (int)Actions->Length()) Idx = (int)Actions->Length() - 1; Actions->Select(Actions->ItemAt(Idx)); Actions->Focus(true); } } break; } case IDC_LAUNCH_HELP: { switch (Tab->Value()) { case 0: // Name/Index default: { Item->App->LaunchHelp("filters.html"); break; } case 1: // Conditions { Item->App->LaunchHelp("filters.html#cond"); break; } case 2: // Actions { Item->App->LaunchHelp("filters.html#actions"); break; } case 3: // Script { Item->App->LaunchHelp("filters.html#script"); break; } } break; } case IDC_FILTER_UP: { Reindex = 1; break; } case IDC_FILTER_DOWN: { Reindex = -1; break; } } if (Reindex) { // Remove holes in the indexing ScribeFolder *Folder = Item->GetFolder(); if (Folder) { Folder->ReSort(); } // Swap entries int i = (int)GetCtrlValue(IDC_FILTER_INDEX); if (i >= 0) { Filter *f = Item->GetFilterAt(i - Reindex); if (f) { int n = f->GetIndex(); f->SetIndex(Item->GetIndex()); f->SetDirty(); Item->SetIndex(n); Item->SetDirty(); f->Save(); Item->Save(); SetCtrlValue(IDC_FILTER_INDEX, Item->GetIndex()); Item->App->GetItemList()->ReSort(); Item->Update(); f->Update(); } } } return 0; } LMessage::Result FilterUi::OnEvent(LMessage *Msg) { switch (Msg->Msg()) { #ifdef WIN32 case WM_CLOSE: { Quit(); return 0; } #endif } return LWindow::OnEvent(Msg); } void LoadTree(LFilterView *v, LXmlTag *t, LTreeNode *i) { int idx = 0; for (auto c: t->Children) { if (c->GetTag()) { LFilterItem *n = 0; bool Cond = false; if (c->IsTag(ELEMENT_AND)) n = v->Create(LNODE_AND); else if (c->IsTag(ELEMENT_OR)) n = v->Create(LNODE_OR); else if (c->IsTag(ELEMENT_CONDITION)) { Cond = true; n = v->Create(LNODE_COND); } if (n) { if (Cond) { n->SetNot(c->GetAsInt(ATTR_NOT) > 0); n->SetField(c->GetAttr(ATTR_FIELD)); n->SetValue(c->GetAttr(ATTR_VALUE)); char *o = c->GetAttr(ATTR_OP); if (o) { if (IsDigit(*o)) n->SetOp(atoi(o)); else { for (int i=0; OpNames[i]; i++) { if (!_stricmp(OpNames[i], o)) { n->SetOp(i); break; } } } } } i->Insert(n, idx++); LoadTree(v, c, n); } } } } void FilterUi::OnLoad() { if (Item) { LAutoPtr r = Item->Parse(true); if (r && Actions) { for (auto c: r->Children) { FilterAction *a = new FilterAction(Item->GetObject()->GetStore()); if (a) { if (a->Set(c)) { Actions->Insert(a); } else LAssert(!"Can't convert xml to action."); } } } auto FilterName = Item->GetName(); SetCtrlName(IDC_NAME, FilterName); SetCtrlValue(IDC_FILTER_INDEX, Item->GetIndex()); SetCtrlName(IDC_SCRIPT, Item->GetScript()); SetCtrlValue(IDC_STOP_FILTERING, Item->GetStopFiltering()); SetCtrlValue(IDC_INCOMING, Item->GetIncoming()); SetCtrlValue(IDC_OUTGOING, Item->GetOutgoing()); SetCtrlValue(IDC_INTERNAL_FILTERING, Item->GetInternal()); auto Xml = Item->GetConditionsXml(); if (Conditions && Xml) { LAutoPtr x(new LXmlTag); if (x) { LMemStream p(Xml, strlen(Xml)); LXmlTree t; if (t.Read(x, &p, 0)) { Conditions->Empty(); LoadTree(Conditions, x, Conditions->GetRootNode()); if (!Conditions->GetRootNode()->GetChild()) { Conditions->SetDefault(); } } } } if (ValidStr(FilterName)) { LString s; s.Printf("%s - %s", LLoadString(IDS_FILTER), FilterName); Name(s); } } } void SaveTree(LXmlTag *t, LTreeNode *i) { for (LTreeNode *c = i->GetChild(); c; c = c->GetNext()) { LFilterItem *fi = dynamic_cast(c); if (fi) { const char *Tag = 0; bool Cond = false; switch (fi->GetNode()) { default: break; case LNODE_AND: Tag = ELEMENT_AND; break; case LNODE_OR: Tag = ELEMENT_OR; break; case LNODE_COND: Cond = true; Tag = ELEMENT_CONDITION; break; } if (Tag) { LXmlTag *n = new LXmlTag(Tag); if (n) { if (Cond) { n->SetAttr(ATTR_NOT, fi->GetNot()); n->SetAttr(ATTR_FIELD, fi->GetField()); n->SetAttr(ATTR_OP, fi->GetOp()); n->SetAttr(ATTR_VALUE, fi->GetValue()); } t->InsertTag(n); SaveTree(n, fi); } } } } } void FilterUi::OnSave() { if (Item) { Item->SetName(GetCtrlName(IDC_NAME)); Item->SetScript(GetCtrlName(IDC_SCRIPT)); Item->SetStopFiltering(GetCtrlValue(IDC_STOP_FILTERING)!=0); Item->SetIncoming(GetCtrlValue(IDC_INCOMING)!=0); Item->SetOutgoing(GetCtrlValue(IDC_OUTGOING)!=0); Item->ChkIncoming->Value(GetCtrlValue(IDC_INCOMING)); Item->ChkOutgoing->Value(GetCtrlValue(IDC_OUTGOING)); Item->ChkInternal->Value(GetCtrlValue(IDC_INTERNAL_FILTERING)); if (Conditions) { LXmlTag *x = new LXmlTag(ELEMENT_CONDITIONS); if (x) { SaveTree(x, Conditions->GetRootNode()); LXmlTree t; LStringPipe p; if (t.Write(x, &p)) { LAutoString a(p.NewStr()); Item->ConditionsCache.Reset(); Item->SetConditionsXml(a); } DeleteObj(x); } } LXmlTag x("Actions"); List Act; Actions->GetAll(Act); for (size_t i=0; i c(new LXmlTag("Action")); if (a->Get(c)) { x.InsertTag(c.Release()); } } LXmlTree t; LStringPipe p; t.Write(&x, &p); LAutoString s(p.NewStr()); Item->SetActionsXml(s); if (Item->Save()) { Item->Reindex(Item->GetFolder()); } } } int FilterUi::OnCommand(int Cmd, int Event, OsView Window) { switch (Cmd) { case IDM_SAVE: { OnSave(); break; } case IDM_SAVE_CLOSE: { OnSave(); // fall thru } case IDM_CLOSE: { Quit(); break; } case IDM_DELETE: { Item->Ui = 0; Item->OnDelete(); Item = 0; Quit(); break; } case IDM_HELP: { App->LaunchHelp("filters.html"); break; } case IDC_NEW_FILTER_ACTION: { LList *l; if (GetViewById(IDC_FILTER_ACTIONS, l)) { } break; } case IDC_DELETE_FILTER_ACTION: { break; } default: { Commands.ExecuteCallbacks(GetItem()->App, this, GetItem(), Cmd); break; } } return 0; } diff --git a/src/ScribeFolder.cpp b/src/ScribeFolder.cpp --- a/src/ScribeFolder.cpp +++ b/src/ScribeFolder.cpp @@ -1,4108 +1,4108 @@ /* ** FILE: ScribeFolder.cpp ** AUTHOR: Matthew Allen ** DATE: 17/1/2000 ** DESCRIPTION: Scribe folder's ** ** Copyright (C) 2000-2002, Matthew Allen ** fret@memecode.com */ // Includes #include "Scribe.h" #include "lgi/common/DropFiles.h" #include "lgi/common/ProgressDlg.h" #include "lgi/common/QuickSort.h" #include "lgi/common/DisplayString.h" #include "lgi/common/TextFile.h" #include "lgi/common/LgiRes.h" #include "lgi/common/FileSelect.h" #include "lgi/common/ClipBoard.h" #include "Calendar.h" #include "resdefs.h" #include "ReplicateDlg.h" #include "FolderTask.h" ////////////////////////////////////////////////////////////////////////////// #if WINNATIVE #include "lgi/common/Com.h" #endif class LDndFilePromise #if defined(WINDOWS) #elif defined(__GTK_H__) #elif defined(MAC) #endif { LStream *Src; #if WINNATIVE FILEGROUPDESCRIPTOR Gd; #elif defined(__GTK_H__) #elif defined(MAC) #endif public: LDndFilePromise(LDragData &dd, LStream *src, LString FileName) { Src = src; #if WINNATIVE ZeroObj(Gd); Gd.cItems = 1; // CLSID Fd.clsid; // SIZEL Fd.sizel; // POINTL Fd.pointl; // FILETIME Fd.ftCreationTime; // FILETIME Fd.ftLastAccessTime; // FILETIME Fd.ftLastWriteTime; FILEDESCRIPTOR &Fd = Gd.fgd[0]; Fd.dwFlags = FD_ATTRIBUTES | FD_PROGRESSUI; Fd.dwFileAttributes = FILE_ATTRIBUTE_COMPRESSED | FILE_ATTRIBUTE_NORMAL; int64 Sz = Src->GetSize(); if (Sz >= 0) { Fd.dwFlags |= FD_FILESIZE; Fd.nFileSizeHigh = Sz >> 32; Fd.nFileSizeLow = Sz & 0xffffffff; } auto Leaf = FileName.RFind(DIR_STR); LAutoWString FnW(Utf8ToWide(Leaf >= 0 ? FileName(Leaf+1,-1) : FileName)); Strcpy(Fd.cFileName, CountOf(Fd.cFileName), FnW.Get()); LVariant &v = dd.Data[0]; v.SetBinary(sizeof(Gd), &Gd); #elif defined(__GTK_H__) LAssert(!"Not impl."); #elif LGI_COCOA LAssert(!"Not impl."); #elif LGI_CARBON // See Apple: Technical Note TN1085 // https://web.archive.org/web/20080725134839/http://developer.apple.com/technotes/tn/tn1085.html // flavorTypePromiseHFS = 'phfs' PromiseHFSFlavor Promise; ZeroObj(Promise); Promise.fileType = 'mbox'; Promise.fileCreator = '****'; Promise.promisedFlavor = kDragPromisedFlavor; // 'fssP' int Sz = sizeof(Promise); LVariant &v1 = dd.Data[0]; v1.SetBinary(sizeof(Promise), &Promise, false); // The Dnd code will add the 2nd part of the promise // There isn't the visibility into that API at this level #else #error "Impl me." #endif } ~LDndFilePromise() { #if defined(WINDOWS) #elif defined(__GTK_H__) #elif defined(MAC) #endif } #if defined(WINDOWS) #elif defined(__GTK_H__) #elif defined(MAC) #endif int AddContents(LDragData &dd) { dd.Data[0] = Src; return 1; } }; ////////////////////////////////////////////////////////////////////////////// #define PROFILE_POPULATE 0 #define PROFILE_LOAD_THINGS 0 int FoldersCurrentlyLoading = 0; char ScribeFolderObject[] = "com.memecode.Folder"; class ScribeFolderPriv { public: int8 IsInbox = -1; bool InUpdateUnread = false; LAutoPtr DsBase; LAutoPtr DsUnread; LAutoPtr FilePromise; }; ////////////////////////////////////////////////////////////////////////////// ScribeFolder::ScribeFolder() { d = new ScribeFolderPriv; } ScribeFolder::~ScribeFolder() { bool IsRoot = !GetParent(); if (CurState != FldState_Idle) { // int Cur = FoldersCurrentlyLoading; #ifdef _DEBUG LAssert(!"Can't delete folder while it's busy."); #else LgiMsg( App, "Can't unload folder while busy. Please report what\n" "you were attempting to do to fret@memecode.com", "Scribe Error", MB_OK); #endif return; } switch (GetItemType()) { case MAGIC_CALENDAR: CalendarSource::FolderDelete(this); // fall through case MAGIC_CONTACT: case MAGIC_FILTER: case MAGIC_GROUP: App->RemoveThingSrc(this); break; default: break; } if (View()) { bool IsMe = View()->GetContainer() == this; // bool IsSelect = Select(); View()->DeletePlaceHolders(); if (IsMe) { View()->SetContainer(0); View()->RemoveAll(); } } Thing *t; int i = 0; while ((t = Items[i])) { if (!t->DecRef()) i++; } Update(); EmptyFieldList(); DeleteObj(d); ScribeFolder *f = GetChildFolder(); DeleteObj(f); if (!IsRoot) { f = GetNextFolder(); DeleteObj(f); } // Don't delete 'Object' here, it's owned by the backend storage object } bool ScribeFolder::SetObject(LDataI *o, bool InDestructor, const char *File, int Line) { if (CurState != FldState_Idle) { LAssert(!"Can't set object while folder is not idle."); return false; } return LDataUserI::SetObject(o, InDestructor, File, Line); } void ScribeFolder::UpdateOsUnread() { #ifdef WIN32 if (App->GetFolder(FOLDER_INBOX) == this) { LHashTbl, int> Un(0, -1); // Pre-populate 'Un' with our email accounts List *Acc = App->GetAccounts(); if (Acc) { for (auto a: *Acc) { LVariant e = a->Identity.Email(); if (e.Str()) Un.Add(e.Str(), 0); } } // Scan folder for unread email... for (auto t : Items) { Mail *m = t->IsMail(); if (m) { if (!TestFlag(m->GetFlags(), MAIL_READ)) { ScribeAccount *a = m->GetAccountSentTo(); if (a) { LVariant Email; Email = a->Identity.Email(); if (Email.Str()) { int Cur = Un.Find(Email.Str()); Un.Add(Email.Str(), Cur + 1); } } } } } #if 0 // Set system email status LArray Ver; int Os = LGetOs(&Ver); if ((Os == LGI_OS_WIN32 || Os == LGI_OS_WIN64) && (Ver[0] > 5 || (Ver[0] == 5 && Ver[1] > 0))) { char e[256]; LgiGetExeFile(e, sizeof(e)); typedef HRESULT (__stdcall *pSHSetUnreadMailCount)(LPCWSTR pszMailAddress, DWORD dwCount,LPCWSTR pszShellExecuteCommand); LLibrary Shell32("shell32"); pSHSetUnreadMailCount SHSetUnreadMailCount = (pSHSetUnreadMailCount) Shell32.GetAddress("SHSetUnreadMailCountW"); if (SHSetUnreadMailCount) { LVariant Exe; Exe = e; char *k; for (int u=Un.First(&k); u>=0; u=Un.Next(&k)) { LVariant Email = k; SHSetUnreadMailCount(Email.WStr(), u, Exe.WStr()); } } } #endif } #endif } void ScribeFolder::SetLoadOnDemand() { if ((LoadOnDemand = new LTreeItem)) { Expanded(false); LoadOnDemand->SetText((char*)LLoadString(IDS_LOADING)); Insert(LoadOnDemand); } } void ScribeFolder::GetMessageById(const char *Id, std::function Callback) { if (!Id) { if (Callback) Callback(NULL); return; } LoadThings(NULL, [this, Id=LString(Id), Callback](auto s) { if (s < Store3Delayed) { if (Callback) Callback(NULL); return; } for (auto t : Items) { Mail *r = t->IsMail(); if (!r) continue; auto rid = r->GetMessageId(); if (!Stricmp(rid, Id.Get())) { if (Callback) Callback(r); return; } } }); } bool ScribeFolder::InsertThing(Thing *t) { bool Status = false; if (t && Select() && View()) { // Filter ThingFilter *Filter = App->GetThingFilter(); t->SetFieldArray(FieldArray); if (!Filter || Filter->TestThing(t)) { if (t->GetList() != View()) { View()->Insert(t); #if WINNATIVE UpdateWindow(View()->Handle()); #endif } } Status = true; } return Status; } bool ScribeFolder::GetThreaded() { return GetObject() ? GetObject()->GetInt(FIELD_FOLDER_THREAD) != 0 : false; } void ScribeFolder::SetThreaded(bool t) { if (GetObject() && GetThreaded() ^ t) { GetObject()->SetInt(FIELD_FOLDER_THREAD, t); SetDirty(); } } ThingList *ScribeFolder::View() { return App ? App->GetMailList() : 0; } bool ScribeFolder::HasFieldId(int Id) { auto o = GetFldObj(); if (o) { for (LDataPropI *f = o->Fields().First(); f; f = o->Fields().Next()) { if (Id == f->GetInt(FIELD_ID)) { return true; } } } return false; } void ScribeFolder::SetFolderPerms(LView *Parent, ScribeAccessType Access, ScribePerm Perm, std::function Callback) { ScribePerm Current = GetFolderPerms(Access); int Field = (Access == ScribeReadAccess) ? FIELD_FOLDER_PERM_READ : FIELD_FOLDER_PERM_WRITE; if (GetObject() && Current != Perm) { App->GetAccessLevel(Parent, Current, GetPath(), [this, Field, Perm, Callback](auto Allow) { if (Allow) { GetObject()->SetInt(Field, Perm); SetDirty(); } if (Callback) Callback(Allow); }); } else { if (Callback) Callback(true); // i.e. not changing } } ScribePerm ScribeFolder::GetFolderPerms(ScribeAccessType Access) { int Field = (Access == ScribeReadAccess) ? FIELD_FOLDER_PERM_READ : FIELD_FOLDER_PERM_WRITE; return GetObject() ? (ScribePerm)GetObject()->GetInt(Field) : PermRequireNone; } Store3Status ScribeFolder::CopyTo(ScribeFolder *NewParent, int NewIndex) { Store3Status Copied = Store3Error; if (NewParent == GetParent()) { NewParent->GetObject()->GetStore(); } else if (GetObject() && GetObject()->GetStore() && NewParent->GetObject() && NewParent->GetObject()->GetStore()) { // Find or create the destination folder LDataFolderI *Dst = 0; auto Name = GetObject()->GetStr(FIELD_FOLDER_NAME); for (ScribeFolder *f = NewParent->GetChildFolder(); f; f = f->GetNextFolder()) { auto n = f->GetObject()->GetStr(FIELD_FOLDER_NAME); if (n && !_stricmp(n, Name)) { Dst = f->GetFldObj(); } } if (!Dst) { // Create sub-folder if ((Dst = dynamic_cast(NewParent->GetObject()->GetStore()->Create(GetObject()->Type())))) { Dst->CopyProps(*GetObject()); if (Dst->Save(NewParent->GetObject()) == Store3Error) return Store3Error; } else return Store3Error; } if (Dst) { Copied = Store3ReplicateFolders(App, Dst, GetFldObj(), true, false, 0); } } else LAssert(!"Pointer error"); return Copied; } Store3Status ScribeFolder::SetFolder(ScribeFolder *f, int Param) { Store3Status Moved = Store3Error; if (f == this) { LAssert(0); } else if (f && GetObject() && GetObject()->GetStore() && f->GetObject() && f->GetObject()->GetStore()) { if (GetObject()->GetStore() == f->GetObject()->GetStore()) { // Simple in storage movement LArray Mv; Mv.Add(GetObject()); Moved = GetObject()->GetStore()->Move(f->GetFldObj(), Mv); if (Moved && Param >= 0) { GetObject()->SetInt(FIELD_FOLDER_INDEX, Param); } } else { // Cross storage movement... // Find or create the destinate folder LDataFolderI *Dst = 0; int Idx = 0; auto Name = GetObject()->GetStr(FIELD_FOLDER_NAME); for (ScribeFolder *c = f->GetChildFolder(); c; c = c->GetNextFolder(), Idx++) { auto n = c->GetObject()->GetStr(FIELD_FOLDER_NAME); if (n && !_stricmp(n, Name)) { Dst = c->GetFldObj(); break; } } if (!Dst) { Dst = dynamic_cast(f->GetObject()->GetStore()->Create(f->Type())); } else { ScribeFolder *c = CastFolder(dynamic_cast(Dst)); if (c) { c->Remove(); Param = Idx; DeleteObj(c); } } if (Dst) { if (View()) View()->RemoveAll(); // Clean up all the items and sub-folders, they will be created by the // replication code calling "OnNew". Thing *t; while ((t = Items[0])) { t->DecRef(); Items.Delete(t); } // Delete the child folders... ScribeFolder *Cf; while ((Cf = GetChildFolder())) { DeleteObj(Cf); } // Copy ourself over... Dst->CopyProps(*GetObject()); LDataFolderI *Old = GetFldObj(); SetObject(Dst, false, _FL); // Save the object to the new store... Store3Status s = GetObject()->Save(f->GetObject()); if (s != Store3Error) { // And replicate all the children objects... Moved = Store3ReplicateFolders(App, GetFldObj(), Old, true, true, 0); } } else LAssert(!"Not a valid folder"); } if (Moved == Store3Success) { // Move LTreeItem node... f->Insert(this, Param); Select(true); LoadFolders(); // Adjust all the other indexes so that it's remembered ScribeFolder *p = GetFolder(); if (p) { int Idx = 0; for (p = p->GetChildFolder(); p; p = p->GetNextFolder()) { p->SetSortIndex(Idx++); p->SetDirty(); } } } } else LAssert(!"Pointer error"); return Moved; } Store3Status ScribeFolder::DeleteAllThings(std::function Callback) { if (!GetFldObj()) return Store3Error; Store3Status r = GetFldObj()->DeleteAllChildren(); if (r == Store3Error) { LAssert(!"DeleteAllChildren failed."); } else if (r == Store3Success) { LAssert(Items.Length() == 0); OnUpdateUnRead(0, true); Update(); } return r; } Store3Status ScribeFolder::DeleteThing(Thing *t, std::function Callback) { Store3Status Status = Store3Error; if (t && t->GetObject()) { if (t->IsAttachment()) { #ifdef _DEBUG Mail *m = #endif t->IsAttachment()->GetOwner(); LAssert(m && Items.HasItem(m)); } if (t->GetObject()->Delete()) { t->SetParentFolder(NULL); if (View()) View()->Remove(t); } } return Status; } Store3Status ScribeFolder::WriteThing(Thing *t, std::function Callback) { if (!t) { if (Callback) Callback(Store3Error); return Store3Error; } auto ParentFolder = GetFolder(); - auto Path = ParentFolder ? ParentFolder->GetPath() : NULL; + auto Path = ParentFolder ? ParentFolder->GetPath() : LString(); auto OnAllow = [this, t, Callback]() { // Generic thing storage.. bool Create = !t->GetObject(); if (Create) t->SetObject(GetObject()->GetStore()->Create(t->Type()), false, _FL); if (!t->GetObject()) { LAssert(!"No object?"); LgiTrace("%s:%i - No object to save.\n", _FL); if (Callback) Callback(Store3Error); return; } // saving a thing that already has an item on disk auto Obj = GetObject(); auto Status = t->GetObject()->Save(Obj); if (Status != Store3Error) { // The ScribeWnd::OnNew will take care of inserting the item into the // right folder, updating the unread count, any filtering etc. t->OnSerialize(true); if (Callback) Callback(Status); } else { if (Create) t->SetObject(NULL, false, _FL); LgiTrace("%s:%i - Object->Save returned %i.\n", _FL, Status); if (Callback) Callback(Store3Error); } }; if (App) App->GetAccessLevel(App, GetWriteAccess(), Path, [OnAllow](auto Allow) { if (Allow) OnAllow(); }); else OnAllow(); return Store3Success; } int ThingContainerNameCmp(LTreeItem *a, LTreeItem *b, NativeInt d) { ScribeFolder *A = dynamic_cast(a); ScribeFolder *B = dynamic_cast(b); if (A && B) { const char *s1 = A->GetText(); const char *s2 = B->GetText(); const char *Empty = ""; return _stricmp(s1?s1:Empty, s2?s2:Empty); } else LAssert(!"Invalid objects."); return 0; } int ThingContainerIdxCmp(LTreeItem *a, LTreeItem *b, NativeInt d) { ScribeFolder *A = dynamic_cast(a); ScribeFolder *B = dynamic_cast(b); if (A && B) { int Aidx = A->GetSortIndex(); int Bidx = B->GetSortIndex(); if (Aidx >= 0 || Bidx >= 0) { return Aidx - Bidx; } } else LAssert(!"Invalid objects."); return 0; } void ScribeFolder::SortSubfolders() { int i = 0; ScribeFolder *c; LTreeItem::Items.Sort(ThingContainerNameCmp); for (c = GetChildFolder(); c; c = c->GetNextFolder()) { c->SetSortIndex(i++); c->SetDirty(); } GetTree()->UpdateAllItems(); GetTree()->Invalidate(); } void ScribeFolder::DoContextMenu(LMouse &m) { MailTree *mt = dynamic_cast(GetTree()); LScriptUi s(new LSubMenu); List Templates; bool ForceDelete = m.Shift(); bool IsTrash = false; bool IsSystem = false; if (!s.Sub) return; IsTrash = GetItemType() == MAGIC_ANY; IsSystem = App->GetFolderType(this) >= 0; if (App->GetFolderType(this) == FOLDER_OUTBOX) { s.Sub->AppendItem(LLoadString(IDS_MARK_ALL_SEND), IDM_MARK_SEND, true); } if (IsTrash) { s.Sub->AppendItem(LLoadString(IDS_EMPTY), IDM_EMPTY, true); s.Sub->AppendItem(LLoadString(IDS_MARK_ALL_READ), IDM_MARK_ALL_READ, true); } else if (!IsRoot()) { auto *LiteralNew = LLoadString(IDS_NEW); const char *Type = 0; switch (GetItemType()) { case MAGIC_MAIL: Type = LLoadString(IDS_MAIL); break; case MAGIC_CONTACT: Type = LLoadString(IDS_CONTACT); break; case MAGIC_CALENDAR: Type = LLoadString(IDS_CAL_EVENT); break; case MAGIC_FILTER: Type = LLoadString(IDS_FILTER); break; default: break; } if (Type) { char Str[256]; sprintf_s(Str, sizeof(Str), "%s %s", LiteralNew, Type); s.Sub->AppendItem(Str, IDM_NEW_EMAIL, true); } } switch (GetItemType()) { case MAGIC_CONTACT: { char Str[256]; const char *LiteralEmail = LLoadString(IDS_EMAIL); sprintf_s(Str, sizeof(Str), "%s '%s'", LiteralEmail, GetText()); s.Sub->AppendItem(Str, IDM_EMAIL_GROUP, true); ScribeFolder *f = App->GetFolder(FOLDER_TEMPLATES); if (f) { // FIXME f->LoadThings(); auto Merge = s.Sub->AppendSub(LLoadString(IDS_MERGE_TEMPLATE)); if (Merge) { int n = 0; for (auto t: f->Items) { Mail *m = t->IsMail(); if (m) { Templates.Insert(m); Merge->AppendItem(m->GetSubject()?m->GetSubject():(char*)"(no subject)", IDM_MERGE_TEMPLATE_BASE+n++, true); } } } } else { s.Sub->AppendItem(LLoadString(IDS_MERGE_TEMPLATE), 0, false); } s.Sub->AppendItem(LLoadString(IDS_MERGE_FILE), IDM_MERGE_FILE, true); break; } case MAGIC_MAIL: { if (!IsRoot()) { s.Sub->AppendItem(LLoadString(IDC_COLLECT_SUB_MAIL), IDM_COLLECT_MAIL, true); s.Sub->AppendItem(LLoadString(IDS_MARK_ALL_READ), IDM_MARK_ALL_READ, true); if (GetObject() && GetObject()->GetInt(FIELD_STORE_TYPE) == Store3Imap) { s.Sub->AppendItem(LLoadString(IDC_UNDELETE), IDM_UNDELETE, true); s.Sub->AppendItem("Expunge", IDM_EXPUNGE, true); s.Sub->AppendItem(LLoadString(IDC_REFRESH), IDM_REFRESH, true); } } break; } default: break; } if (s.Sub->ItemAt(0)) { s.Sub->AppendSeparator(); } s.Sub->AppendItem(LLoadString(IDS_FIND), IDM_FIND, true); s.Sub->AppendSeparator(); bool HasFolderTask = App->HasFolderTasks(); bool FolderOp = !HasFolderTask && !IsRoot(); s.Sub->AppendItem(LLoadString(IDS_DELETEFOLDER), IDM_DELETE, FolderOp && ((!IsTrash && !IsSystem) || ForceDelete)); s.Sub->AppendItem(LLoadString(IDS_CREATESUBFOLDER), IDM_CREATE_SUB, !HasFolderTask && CanHaveSubFolders(GetItemType())); s.Sub->AppendItem(LLoadString(IDS_RENAMEFOLDER), IDM_RENAME, FolderOp); auto ExportMimeType = GetStorageMimeType(); s.Sub->AppendItem(LLoadString(IDS_EXPORT), IDM_EXPORT, FolderOp && ExportMimeType != NULL); s.Sub->AppendSeparator(); s.Sub->AppendItem(LLoadString(IDS_SORT_SUBFOLDERS), IDM_SORT_SUBFOLDERS, true); s.Sub->AppendItem(LLoadString(IDS_COPY_PATH), IDM_COPY_PATH, true); s.Sub->AppendItem(LLoadString(IDS_PROPERTIES), IDM_PROPERTIES, true); LArray Callbacks; if (App->GetScriptCallbacks(LFolderContextMenu, Callbacks)) { LScriptArguments Args(NULL); Args[0] = new LVariant(App); Args[1] = new LVariant(this); Args[2] = new LVariant(&s); for (auto c: Callbacks) App->ExecuteScriptCallback(*c, Args); Args.DeleteObjects(); } m.ToScreen(); LPoint Scr = _ScrollPos(); m.x -= Scr.x; m.y -= Scr.y; int Msg = s.Sub->Float(GetTree(), m.x, m.y); switch (Msg) { case IDM_NEW_EMAIL: { switch (GetItemType()) { case MAGIC_MAIL: case MAGIC_CONTACT: case MAGIC_CALENDAR: case MAGIC_FILTER: { if (App) App->CreateItem(GetItemType(), this); break; } default: break; } break; } case IDM_MARK_SEND: { for (Thing *i: Items) { Mail *m = i->IsMail(); if (m) { m->SetFlags(MAIL_READY_TO_SEND | MAIL_READ | MAIL_CREATED); m->SetDirty(); m->Update(); } } break; } case IDM_EMAIL_GROUP: { if (App) { Mail *m = dynamic_cast(App->CreateItem(MAGIC_MAIL, NULL, false)); if (m) { MailUi *UI = dynamic_cast(m->DoUI()); if (UI) { List Cts; App->GetContacts(Cts, this); for (auto c: Cts) { UI->AddRecipient(c); } } } } break; } case IDM_FIND: { ScribeFolder *Folder = (ScribeFolder*) GetTree()->Selection(); OpenFinder(App, Folder); break; } case IDM_CREATE_SUB: { if (mt) mt->OnCreateSubDirectory(this); break; } case IDM_DELETE: { if (mt) mt->OnDelete(this, ForceDelete); break; } case IDM_RENAME: { auto Dlg = new FolderNameDlg(mt, GetName(true)); Dlg->DoModal([this, Dlg, mt](auto dlg, auto id) { if (id && ValidStr(Dlg->Name)) { // check for folder name conflicts... ScribeFolder *ParentFolder = GetFolder(); LString Path; if (ParentFolder) Path = ParentFolder->GetPath(); if (Path) { char s[256]; sprintf_s(s, sizeof(s), "%s/%s", Path.Get(), Dlg->Name); if (App->GetFolder(s)) { LgiMsg(mt, LLoadString(IDS_SUBFLD_NAME_CLASH), AppName, MB_OK); return; } } // change the folders name... OnRename(Dlg->Name); } delete dlg; }); break; } case IDM_EXPORT: { LString DropName = LGetLeaf(GetDropFileName()); if (!DropName) { LgiTrace("%s:%i - Failed to create folder name.\n", _FL); break; } auto s = new LFileSelect(mt); s->Name(DropName); s->Save([this, ExportMimeType](auto s, auto ok) { LAutoPtr mem(s); if (ok) { if (LFileExists(s->Name())) { LString a, b; a.Printf(LLoadString(IDS_ERROR_FILE_EXISTS), s->Name()); b.Printf("\n%s\n", LLoadString(IDS_ERROR_FILE_OVERWRITE)); if (LgiMsg(GetTree(), a + b, AppName, MB_YESNO) == IDNO) return; } LAutoPtr f(new LFile); if (!f || !f->Open(s->Name(), O_WRITE)) { LgiTrace("%s:%i - Failed to open '%s' for writing.\n", _FL, s->Name()); return; } f->SetSize(0); LAutoPtr str(f.Release()); ExportAsync(str, ExportMimeType); } }); break; } case IDM_EMPTY: { if (App->GetMailList()) { App->GetMailList()->RemoveAll(); LArray Del; for (ScribeFolder *c = GetChildFolder(); c; c = c->GetNextFolder()) Del.Add(c->GetObject()); if (Del.Length()) GetObject()->GetStore()->Delete(Del, false); DeleteAllThings([mt](auto status) { mt->Invalidate(); }); } break; } case IDM_SORT_SUBFOLDERS: { SortSubfolders(); break; } case IDM_PROPERTIES: { mt->OnProperties(this); break; } case IDM_COPY_PATH: { LClipBoard c(GetTree()); #ifdef WINDOWS LAutoWString w(Utf8ToWide(GetPath())); c.TextW(w); #else c.Text(GetPath()); #endif break; } case IDM_COLLECT_MAIL: { CollectSubFolderMail(); break; } case IDM_MARK_ALL_READ: { LArray Change; for (auto c : Items) { Mail *m = c->IsMail(); if (m) { if (!TestFlag(m->GetFlags(), MAIL_READ)) Change.Add(m->GetObject()); } } LVariant v = MAIL_READ; if (GetObject()->GetStore()->Change(Change, FIELD_FLAGS, v, OpPlusEquals) == Store3Error) { LProgressDlg Prog(GetTree(), 500); Prog.SetRange(Change.Length()); // FIXME!! // Prog.SetYieldTime(200); Prog.SetDescription("Marking email..."); for (auto c : Change) { auto t = CastThing(c); Mail *m = t ? t->IsMail() : NULL; if (m) m->SetFlags(m->GetFlags() | MAIL_READ, false, false); Prog++; if (Prog.IsCancelled()) break; } OnUpdateUnRead(0, true); } break; } case IDM_MERGE_FILE: { auto s = new LFileSelect(mt); s->Type("Email Template", "*.txt;*.eml"); s->Open([this](auto dlg, auto id) { if (id) { LArray Recip; for (auto i: Items) { Recip.Add(new ListAddr(i->IsContact())); } App->MailMerge(Recip, dlg->Name(), 0); Recip.DeleteObjects(); } delete dlg; }); break; } case IDM_UNDELETE: { const char *s = DomToStr(SdUndelete); GetFldObj()->OnCommand(s); break; } case IDM_EXPUNGE: { const char *s = DomToStr(SdExpunge); GetFldObj()->OnCommand(s); break; } case IDM_REFRESH: { const char *s = DomToStr(SdRefresh); GetFldObj()->OnCommand(s); break; } default: { Mail *Template = Templates[Msg - IDM_MERGE_TEMPLATE_BASE]; if (Template) { LArray Recip; for (auto i: Items) { Recip.Add(new ListAddr(i->IsContact())); } App->MailMerge(Recip, 0, Template); Recip.DeleteObjects(); } else { // Handle any installed callbacks for menu items for (unsigned i=0; iExecuteScriptCallback(Cb, Args); } } } break; } } } bool ScribeFolder::IsInTrash() { ScribeFolder *p = this; while ((p = p->GetFolder())) { if (p->GetItemType() == MAGIC_ANY) return true; } return false; } /// This just adds certain folder types as a group ware source at the app level void ScribeFolder::OnItemType() { switch (GetItemType()) { case MAGIC_CONTACT: case MAGIC_FILTER: case MAGIC_CALENDAR: case MAGIC_GROUP: App->AddThingSrc(this); break; default: break; } } bool ScribeFolder::LoadFolders() { // static int64 Last = 0; auto FldObj = GetFldObj(); if (!FldObj) return true; CurState = FldState_Loading; FoldersCurrentlyLoading++; LHashTbl, ScribeFolder*> Loaded; for (ScribeFolder *c = GetChildFolder(); c; c = c->GetNextFolder()) { Loaded.Add(c->GetObject(), c); } LDataFolderI *s; auto &Sub = FldObj->SubFolders(); for (unsigned sIdx = 0; Sub.GetState() == Store3Loaded && sIdx < Sub.Length(); sIdx++) { if (!(s = Sub[sIdx])) break; if (!Loaded.Find(s)) { ScribeFolder *n = new ScribeFolder; if (n) { n->App = App; n->SetObject(s, false, _FL); Insert(n); SetWillDirty(false); Expanded(GetOpen() != 0); SetWillDirty(true); n->LoadFolders(); n->OnItemType(); } #if 0 int64 Now = LCurrentTime(); if (Now - Last > 500) { LYield(); Last = Now; } #endif } } LTreeItem::Items.Sort(ThingContainerIdxCmp); CurState = FldState_Idle; FoldersCurrentlyLoading--; return true; } class LoadProgressItem : public LListItem { int n; int Total; int64 Last; public: LoadProgressItem(int t) { n = 0; Total = t; Last = LCurrentTime(); } void SetPos(int i) { n = i; if (n % 32 == 0) { int64 Now = LCurrentTime(); if (Now > Last + 500) { if (Parent) { Parent->Invalidate(&Pos, true); #ifdef MAC LYield(); #endif } Last = Now; } LSleep(0); } } void OnPaint(ItemPaintCtx &Ctx) { int x = n * 150 / Total; LRect &r = Ctx; Ctx.pDC->Colour(L_FOCUS_SEL_BACK); Ctx.pDC->Rectangle(r.x1, r.y1, r.x1 + x, r.y2); Ctx.pDC->Colour(L_MED); Ctx.pDC->Rectangle(r.x1 + x + 1, r.y1, r.x1 + 150, r.y2); Ctx.pDC->Colour(L_WORKSPACE); Ctx.pDC->Rectangle(r.x1 + 151, r.y1, r.x2, r.y2); } }; class LoadingItem : public LListItem { LDataIterator *Iter; LString s; public: LoadingItem(LDataIterator *it) { _UserPtr = NULL; if ((Iter = it)) { Iter->SetProgressFn([this](ssize_t pos, ssize_t sz) { this->s.Printf("%.1f%%", (double)pos * 100 / sz); this->Update(); }); } } ~LoadingItem() { if (Iter) Iter->SetProgressFn(NULL); } bool SetText(const char *str, int i) { s = str; Update(); return true; } void OnPaint(LItem::ItemPaintCtx &Ctx) { LString msg; auto loading = LLoadString(IDS_LOADING); if (s) msg.Printf("%s %s", loading, s.Get()); else msg = loading; LDisplayString d(LSysFont, msg); LSysFont->Colour(L_TEXT, L_WORKSPACE); LSysFont->Transparent(false); d.Draw(Ctx.pDC, (Ctx.X()-d.X())/2, Ctx.y1, &Ctx); } void Select(bool b) { } }; bool ScribeFolder::UnloadThings() { bool Status = true; if (IsLoaded()) { // LgiTrace("Unloading %s, Items=%i\n", GetPath(), Items.Length()); // Emptying the item list, leave the store nodes around though Thing *t; while ((t = Items[0])) { if (t->GetDirty()) { t->Save(0); } t->SetObject(NULL, false, _FL); t->DecRef(); } Items.Empty(); IsLoaded(false); GetObject()->SetInt(FIELD_LOADED, false); } return Status; } #define DEBUG_PROF_LOAD_THINGS 0 #if DEBUG_PROF_LOAD_THINGS #define PROFILE(str) prof.Add("Add"); .Add(str) #else #define PROFILE(str) #endif void ScribeFolder::ContinueLoading(int OldUnread, std::function Callback) { auto FldObj = GetFldObj(); WhenLoaded(_FL, [this, OldUnread, Callback, FldObj]() { // This is called when all the Store3 objects are loaded int Unread = OldUnread; if (Unread < 0) Unread = GetUnRead(); Loading.Reset(); auto &Children = GetFldObj()->Children(); if (Children.GetState() != Store3Loaded) { LAssert(!"Really should be loaded by now."); return; } for (auto c = Children.First(); c; c = Children.Next()) { auto t = CastThing(c); if (t) { // LAssert(Items.HasItem(t)); } else if ((t = App->CreateThingOfType((Store3ItemTypes) c->Type(), c))) { t->SetObject(c, false, _FL); t->SetParentFolder(this); t->OnSerialize(false); } } int NewUnRead = 0; for (auto t: Items) { if (t->GetFolder() != this) { #ifdef _DEBUG char s[256]; sprintf_s(s, sizeof(s), "%s:%i - Error, thing not parented correctly: this='%s', child='%x'\n", _FL, GetText(0), t->GetObject() ? t->GetObject()->Type() : 0); printf("%s", s); LgiMsg(App, s, AppName); #endif t->SetFolder(this); } Mail *m = t->IsMail(); if (m) NewUnRead += (m->GetFlags() & MAIL_READ) ? 0 : 1; t->SetFieldArray(FieldArray); } if (Unread != NewUnRead) OnUpdateUnRead(NewUnRead - Unread, false); Update(); if (d->IsInbox < 0 && App) { d->IsInbox = App->GetFolder(FOLDER_INBOX) == this; if (d->IsInbox > 0) UpdateOsUnread(); } if (Callback) Callback(Store3Success); }, 0); if (!IsLoaded()) { bool Ui = Tree ? Tree->InThread() : false; if (Ui) Tree->Capture(false); auto &Children = FldObj->Children(); auto Status = Children.GetState(); if (Status != Store3Loaded) { if (View() && Loading.Reset(Ui ? new LoadingItem(&Children) : NULL)) View()->Insert(Loading); return; // Ie deferred or error... } IsLoaded(true); } } Store3Status ScribeFolder::LoadThings(LViewI *Parent, std::function Callback) { int OldUnRead = GetUnRead(); auto FldObj = GetFldObj(); if (!FldObj) { LgiTrace("%s:%i - No folder object.\n", _FL); return Store3Error; } if (!Parent) Parent = App; auto Path = GetPath(); if (!App || !Path) { LAssert(!"We should probably always have an 'App' and 'Path' ptrs..."); return Store3Error; } auto Access = App->GetAccessLevel( Parent, GetReadAccess(), Path, [this, OldUnRead, Callback](auto access) { if (access) ContinueLoading(OldUnRead, Callback); else if (Callback) Callback(Store3NoPermissions); }); if (Access == Store3Error) { // No read access: LgiTrace("%s:%i - Folder read access denied.\n", _FL); // Emptying the item list, leave the store nodes around though for (auto t: Items) t->SetObject(NULL, false, _FL); Items.Empty(); IsLoaded(false); Update(); } return Access; } void ScribeFolder::OnRename(char *NewName) { if (!NewName) return; int FolderType = App->GetFolderType(this); SetName(NewName, true); // Calls update too.. SetDirty(); if (FolderType >= 0) { // it's a system folder so reflect the changes in // the system folder tracking options. char KeyName[32]; sprintf_s(KeyName, sizeof(KeyName), "Folder-%i", FolderType); auto Path = GetPath(); if (Path) { LVariant v = Path.Get(); App->GetOptions()->SetValue(KeyName, v); } } } void ScribeFolder::OnDelete() { if (GetObject()) { char Msg[256]; sprintf_s(Msg, sizeof(Msg), LLoadString(IDS_DELETE_FOLDER_DLG), GetText()); int Result = LgiMsg(App, Msg, AppName, MB_YESNO); if (Result == IDYES) { if (App->GetMailList()) App->GetMailList()->RemoveAll(); ScribeFolder *Parent = GetFolder(); LArray Del; Del.Add(GetObject()); if (GetObject()->GetStore()->Delete(Del, true)) { if (Parent) Parent->Select(true); } } } } bool ScribeFolder::ReindexField(int OldIndex, int NewIndex) { if (!GetFldObj()) return false; auto &Flds = GetFldObj()->Fields(); if (GetFldObj()->Fields().Length() <= 0) return false; LDataPropI *f = Flds[OldIndex]; if (!f) return false; Flds.Delete(f); Flds.Insert(f, OldIndex < NewIndex ? NewIndex - 1 : NewIndex); int i=0; FieldArray.Length(0); for (f = Flds.First(); f; f = Flds.Next()) { auto Id = f->GetInt(FIELD_ID); FieldArray[i++] = (int)Id; } for (auto t: Items) t->SetFieldArray(FieldArray); SetDirty(); return true; } int ScribeFolder::_UnreadChildren() { int Status = 0; for (ScribeFolder *f=GetChildFolder(); f; f=f->GetNextFolder()) { int u = f->GetUnRead(); if (u > 0) Status += u; Status += f->_UnreadChildren(); } return Status; } void ScribeFolder::_PourText(LPoint &Size) { if (!d) return; const char *Text = GetText(); Size.x = Size.y = 0; if (Text && !d->DsBase) { if (GetUnRead() > 0 || ChildUnRead) { const char *StartCount = strrchr(Text, '('); ssize_t NameLen = StartCount ? StartCount-Text : strlen(Text); LFont *b = (App->GetBoldFont()) ? App->GetBoldFont() : GetTree()->GetFont(); d->DsBase.Reset(new LDisplayString(b, Text, NameLen)); d->DsUnread.Reset(new LDisplayString(GetTree()->GetFont(), StartCount)); } else { d->DsBase.Reset(new LDisplayString(GetTree()->GetFont(), Text)); } } if (d->DsBase) Size.x += d->DsBase->X(); if (d->DsUnread) Size.x += d->DsUnread->X(); Size.x += 4; Size.y = MAX(16, LSysFont->GetHeight()); } void ScribeFolder::_PaintText(LItem::ItemPaintCtx &Ctx) { if (!d) return; LRect *_Text = _GetRect(TreeItemText); if (d->DsBase) { LFont *f = d->DsBase->GetFont(); int Tab = f->TabSize(); f->TabSize(0); f->Transparent(false); f->Colour(Ctx.Fore, Ctx.TxtBack); d->DsBase->Draw(Ctx.pDC, _Text->x1 + 2, _Text->y1 + 1, _Text); if (d->DsUnread) { f = d->DsUnread->GetFont(); f->Transparent(true); LColour UnreadCol(App->GetColour(L_UNREAD_COUNT)); if ( abs ( (UnreadCol.GetGray() - Ctx.Back.GetGray()) ) < 80 ) { // too close.. use fore f->Colour(Ctx.Fore, Ctx.TxtBack); } else { // contrast ok.. use unread f->Colour(UnreadCol, Ctx.TxtBack); } d->DsUnread->Draw(Ctx.pDC, _Text->x1 + 2 + d->DsBase->X(), _Text->y1 + 1); } f->TabSize(Tab); if (Ctx.x2 > _Text->x2) { Ctx.pDC->Colour(Ctx.Back); Ctx.pDC->Rectangle(_Text->x2 + 1, Ctx.y1, Ctx.x2, Ctx.y2); } } else { Ctx.pDC->Colour(Ctx.Back); Ctx.pDC->Rectangle(&Ctx); } } bool ScribeFolder::Save(ScribeFolder *Into) { bool Status = false; if (GetObject()) { // saving a disk object Store3Status s = GetObject()->Save(); if (s != Store3Error) { Status = true; SetDirty(false); } else { LAssert(0); } } return Status; } void ScribeFolder::OnExpand(bool b) { if (b && LoadOnDemand) { DeleteObj(LoadOnDemand); if (App->ScribeState != ScribeWnd::ScribeExiting) { ScribeWnd::AppState Prev = App->ScribeState; App->ScribeState = ScribeWnd::ScribeLoadingFolders; LoadFolders(); if (App->ScribeState == ScribeWnd::ScribeExiting) { LCloseApp(); return; } App->ScribeState = Prev; } } OnUpdateUnRead(0, false); SetDirty(((uchar)b) != GetOpen()); SetOpen(b); } bool ScribeFolder::OnKey(LKey &k) { #ifndef WINDOWS // This is being done by the VK_APPS key on windows... if (k.IsContextMenu()) { if (k.Down()) { LMouse m; m.x = 5; m.y = 5; m.ViewCoords = true; m.Target = GetTree(); DoContextMenu(m); } return true; } #endif return false; } #define DefField(id, wid) \ { \ LDataPropI *Fld = Fields.Create(GetFldObj()->GetStore()); \ if (Fld) \ { \ Fld->SetInt(FIELD_ID, id); \ Fld->SetInt(FIELD_WIDTH, wid); \ Fields.Insert(Fld); \ } \ } void ScribeFolder::SetDefaultFields(bool Force) { if (!GetFldObj()) return; if (Force || GetFldObj()->Fields().Length() <= 0) { LDataIterator &Fields = GetFldObj()->Fields(); Fields.DeleteObjects(); int FolderType = App ? App->GetFolderType(this) : -1; if (FolderType == FOLDER_OUTBOX || FolderType == FOLDER_SENT) { DefField(FIELD_PRIORITY, 10); DefField(FIELD_FLAGS, 15); DefField(FIELD_TO, 150); DefField(FIELD_SUBJECT, 250); DefField(FIELD_SIZE, 80); DefField(FIELD_DATE_SENT, 100); } else { switch (GetItemType()) { case MAGIC_MAIL: { DefField(FIELD_PRIORITY, 10); DefField(FIELD_FLAGS, 10); DefField(FIELD_FROM, 150); DefField(FIELD_SUBJECT, 250); DefField(FIELD_SIZE, 80); DefField(FIELD_DATE_SENT, 100); break; } case MAGIC_CONTACT: { DefField(FIELD_FIRST_NAME, 200); DefField(FIELD_LAST_NAME, 200); DefField(FIELD_EMAIL, 300); break; } case MAGIC_CALENDAR: { DefField(FIELD_CAL_SUBJECT, 200); DefField(FIELD_CAL_START_UTC, 150); DefField(FIELD_CAL_END_UTC, 150); break; } case MAGIC_FILTER: { for (int i=0; DefaultFilterFields[i]; i++) { DefField(DefaultFilterFields[i], i == 0 ? 200 : 100); } break; } case MAGIC_GROUP: { DefField(FIELD_GROUP_NAME, 300); break; } default: break; } } // set for save SetDirty(Fields.Length() > 0); } else { // already has fields, so don't interfere with them } } LString ScribeFolder::GetPath() { LString::Array p; ScribeFolder *f = this; while (f) { auto name = f->GetName(true); if (name) p.Add(name); f = dynamic_cast(f->GetParent()); } LString dir = "/"; return dir + dir.Join(p.Reverse()); } void ScribeFolder::SetName(const char *Name, bool Encode) { if (Name) { d->DsBase.Reset(); d->DsUnread.Reset(); NameCache.Reset(); if (GetObject()) GetObject()->SetStr(FIELD_FOLDER_NAME, Name); SetText(Name); // Calls update } } LString ScribeFolder::GetName(bool Decode) { if (!GetObject()) - return NULL; + return LString(); auto In = GetObject()->GetStr(FIELD_FOLDER_NAME); if (!In) - return NULL; + return LString(); if (Decode) return LUrlDecode(In); else return In; } void ScribeFolder::Update() { if (Tree && !Tree->InThread()) { // LgiTrace("%s:%i - Update not in thread?\n", _FL); return; } d->DsBase.Reset(); d->DsUnread.Reset(); NameCache.Reset(); LTreeItem::Update(); } const char *ScribeFolder::GetText(int i) { if (!NameCache) { LString Name = GetName(true); if (!Name) return "...loading..."; size_t NameLen = strlen(Name) + 40; NameCache.Reset(new char[NameLen]); if (NameCache) { bool ShowTotals = false; LVariant v; if (App->GetOptions()->GetValue(OPT_ShowFolderTotals, v)) ShowTotals = v.CastInt32() != 0; int c = sprintf_s(NameCache, NameLen, "%s", Name.Get()); auto ItemType = GetItemType(); if (ItemType && (ShowTotals || GetUnRead() || ChildUnRead)) { c += sprintf_s(NameCache+c, NameLen-c, " ("); if (ItemType == MAGIC_MAIL || ItemType == MAGIC_ANY) { const char *Ch = ChildUnRead ? "..." : ""; if (ShowTotals) c += sprintf_s(NameCache+c, NameLen-c, "%i%s/", GetUnRead(), Ch); else if (GetUnRead()) c += sprintf_s(NameCache+c, NameLen-c, "%i%s", GetUnRead(), Ch); else if (ChildUnRead) c += sprintf_s(NameCache+c, NameLen-c, "..."); } if (ShowTotals) { if (IsLoaded()) c += sprintf_s(NameCache+c, NameLen-c, "%zi", Items.Length()); else c += sprintf_s(NameCache+c, NameLen-c, "?"); } c += sprintf_s(NameCache+c, NameLen-c, ")"); } } } return NameCache; } int ScribeFolder::GetImage(int Flags) { if (GetItemType() == MAGIC_ANY) { return ICON_TRASH; } if (Flags) { return ICON_OPEN_FOLDER; } switch (GetItemType()) { case MAGIC_MAIL: return ICON_FOLDER_MAIL; case MAGIC_CONTACT: return ICON_FOLDER_CONTACTS; case MAGIC_FILTER: return ICON_FOLDER_FILTERS; case MAGIC_CALENDAR: return ICON_FOLDER_CALENDAR; case MAGIC_NONE: return ICON_MAILBOX; default: return ICON_CLOSED_FOLDER; } } class NullMail : public Mail { Thing &operator =(Thing &c) override { return *this; } public: NullMail(ScribeWnd *App, MContainer *c, ScribeFolder *f) : Mail(App) { SetFlags(MAIL_READ | MAIL_CREATED); Container = c; SetParentFolder(f); SetSubject((char*)LLoadString(IDS_MISSING_PARENT)); } ~NullMail() { SetParentFolder(NULL); } bool IsPlaceHolder() override { return true; } const char *GetText(int i) override { // Clear all the fields return 0; } int Compare(LListItem *Arg, ssize_t Field) override { // Use the first mail to sort into position if (Container) { MContainer *c = Container->Children.Length() ? Container->Children[0] : 0; if (c && c->Message) { return c->Message->Compare(Arg, Field); } } return -1; } void OnMouseClick(LMouse &m) override { // Disable the mouse click } bool SetDirty(bool b = true) override { // Don't set it to dirty, otherwise the dirty object // clean up code will save it into the outbox. return true; } }; template int TrashCompare(T *pa, T *pb, NativeInt Data) { ScribeFolder *f = (ScribeFolder*)Data; Thing *a = dynamic_cast(pa); Thing *b = dynamic_cast(pb); if (!a || !b) return 0; int type = a->Type() - b->Type(); if (type) return type; int col = f->GetSortCol(); int *defs = a->GetDefaultFields(); if (!defs || !defs[col]) return 0; return (f->GetSortAscend() ? 1 : -1) * a->Compare(b, defs[col]); } template int TrashCompare(LListItem *pa, LListItem *pb, NativeInt Data); int ListItemCompare(LListItem *a, LListItem *b, NativeInt Data) { ScribeFolder *f = (ScribeFolder*)Data; return (f->GetSortAscend() ? 1 : -1) * a->Compare(b, f->GetSortField()); } int ThingCompare(Thing *a, Thing *b, NativeInt Data) { ScribeFolder *f = (ScribeFolder*)Data; return (f->GetSortAscend() ? 1 : -1) * a->Compare(b, f->GetSortField()); } int ThingSorter(Thing *a, Thing *b, ThingSortParams *Params) { return (Params->SortAscend ? 1 : -1) * a->Compare(b, Params->SortField); } bool ScribeFolder::Thread() { bool Status = false; if (GetItemType() == MAGIC_MAIL) { int Thread = GetThreaded(); ThingList *Lst = View(); List m; for (auto t: Items) { Mail *e = t->IsMail(); if (e) { if (Thread) { m.Insert(e); } else if (e->Container) { DeleteObj(e->Container); } } } if (Lst && m[0]) { // Thread LArray Containers; MContainer::Thread(m, Containers); LAssert(m.Length() == Items.Length()); // Insert blank items for missing thread parents for (unsigned i=0; iMessage) { LAssert(c->Children.Length() > 1); c->Message = new NullMail(App, c, this); if (c->Message) { Lst->PlaceHolders.Insert(c->Message); if (View() && c->Message) { c->Message->SetFieldArray(FieldArray); Items.Insert(c->Message); } } } } // Sort root list LArray Containers2 = Containers; ThingSortParams Params; Params.SortAscend = GetSortAscend(); Params.SortField = GetSortField(); #if 0 for (int i=0; iMessage ? Containers[i]->Message->GetFieldText(Params.SortField) : NULL; LgiTrace("%p(%s)\n", Containers[i], Str1); } Containers.Sort(ContainerCompare); LQuickSort(Base, Containers2.Length(), ContainerSorter, &Params); for (int i=0; iMessage ? Containers[i]->Message->GetFieldText(Params.SortField) : NULL; char *Str2 = Containers2[i]->Message ? Containers2[i]->Message->GetFieldText(Params.SortField) : NULL; LgiTrace("%p(%s), %p(%s) - %s\n", Containers[i], Str1, Containers2[i], Str2, Containers[i]!=Containers2[i]?"DIFFERENT":""); } #else MContainer **Base = &Containers[0]; if (Containers.Length() > 1) LQuickSort(Base, Containers.Length(), ContainerSorter, &Params); /* for (int i=0; iMessage ? Containers[i]->Message->GetFieldText(Params.SortField) : NULL; LgiTrace("%p(%s)\n", Containers[i]->Message, Str1); } LgiTrace("\n"); */ #endif // Position and index the thread tree int Index = 0; for (unsigned i=0; iPour(Index, 0, 0, i < Containers.Length()-1, &Params); } // Sort all the items by index Items.Sort(ContainerIndexer); Status = true; /* int Idx = 0; for (Thing *t = Items.First(); t; t = Items.Next(), Idx++) { Mail *m = t->IsMail(); if (m) { char *Str = m->GetFieldText(Params.SortField); LgiTrace("%i,%i %p %s\n", Idx, m->Container ? m->Container->Index : -1, m, Str); } } */ } } else { for (auto t: Items) { Mail *e = t->IsMail(); if (e) DeleteObj(e->Container); } } return Status; } struct SortPairInt { Thing *t; uint64 ts; void SetDate(Thing *th, int sf) { t = th; auto obj = th->GetObject(); // Store3State loaded = (Store3State)obj->GetInt(FIELD_LOADED); auto dt = obj->GetDate(sf); ts = dt && dt->Year() ? dt->Ts() : 0; } void SetInt(Thing *th, int sf) { t = th; auto obj = th->GetObject(); // Store3State loaded = (Store3State)obj->GetInt(FIELD_LOADED); ts = obj->GetInt(sf); } static int Compare(SortPairInt *a, SortPairInt *b) { int64_t diff = (int64_t)a->ts - (int64_t)b->ts; if (diff < 0) return -1; return diff > 0 ? 1 : 0; } }; struct SortPairStr { Thing *t; const char *ts; void SetStr(Thing *th, int sf) { t = th; ts = th->GetObject()->GetStr(sf); } static int Compare(SortPairStr *a, SortPairStr *b) { return stricmp(a->ts ? a->ts : "", b->ts ? b->ts : ""); } }; bool ScribeFolder::SortItems() { int sf = GetSortField(); LVariantType type = GV_NULL; auto StartTs = LCurrentTime(); const static int TimeOut = 2/*sec*/ * 1000; switch (sf) { case FIELD_DATE_SENT: case FIELD_DATE_RECEIVED: type = GV_DATETIME; break; case FIELD_SUBJECT: type = GV_STRING; break; default: return false; } LArray intPairs; LArray strPairs; bool intType = type == GV_DATETIME || type == GV_INT32 || type == GV_INT64; if (intType) { intPairs.Length(Items.Length()); int n = 0; auto s = intPairs.AddressOf(); if (type == GV_DATETIME) { for (auto i: Items) { s[n++].SetDate(i, sf); if (n % 50 == 0 && LCurrentTime() - StartTs >= TimeOut) return false; } } else { for (auto i: Items) { s[n++].SetInt(i, sf); if (n % 50 == 0 && LCurrentTime() - StartTs >= TimeOut) return false; } } intPairs.Sort(SortPairInt::Compare); } else if (type == GV_STRING) { strPairs.Length(Items.Length()); int n = 0; auto s = strPairs.AddressOf(); for (auto i: Items) s[n++].SetStr(i, sf); strPairs.Sort(SortPairStr::Compare); } Items.Empty(); if (intType) { if (GetSortAscend()) for (auto i: intPairs) Items.Add(i.t); else for (auto it = intPairs.rbegin(); it != intPairs.end(); it--) Items.Add((*it).t); } else { if (GetSortAscend()) for (auto i: strPairs) Items.Add(i.t); else for (auto it = strPairs.rbegin(); it != strPairs.end(); it--) Items.Add((*it).t); } return true; } void ScribeFolder::Populate(ThingList *list) { LProfile Prof("ScribeFolder::Populate", 1000); App->OnSelect(); if (!GetFldObj() || !list) return; CurState = FldState_Populating; ScribeFolder *Prev = list->GetContainer(); bool Refresh = Prev == this; // Remove old items from list Prof.Add("Delete Placeholders"); list->DeletePlaceHolders(); list->RemoveAll(); if (!Refresh || list->GetColumns() == 0 || GetItemType() == MAGIC_FILTER) { if (Prev) { // save previous folders settings Prev->SerializeFieldWidths(); } Prof.Add("Empty cols"); LVariant GridLines; if (App->GetOptions()->GetValue(OPT_GridLines, GridLines)) { list->DrawGridLines(GridLines.CastInt32() != 0); } list->EmptyColumns(); Prof.Add("Set def fields"); bool ForceDefaultFields = GetItemType() == MAGIC_FILTER; if (GetFldObj()->Fields().Length() <= 0 || ForceDefaultFields) { SetDefaultFields(ForceDefaultFields); } // Add fields to list view int n = 0; LArray Empty; for (auto t: Items) { t->SetFieldArray(Empty); } LRect *Bounds = 0; if (App->GetIconImgList()) { Bounds = App->GetIconImgList()->GetBounds(); } switch (GetItemType()) { case MAGIC_ANY: { list->AddColumn("", 170); list->AddColumn("", 170); list->AddColumn("", 170); list->AddColumn("", 170); break; } default: { n = 0; FieldArray.Length(0); for (LDataPropI *i = GetFldObj()->Fields().First(); i; i = GetFldObj()->Fields().Next()) { int FieldId = (int)i->GetInt(FIELD_ID); const char *FName = LLoadString(FieldId); int Width = (int)i->GetInt(FIELD_WIDTH); const char *FieldText = FName ? FName : i->GetStr(FIELD_NAME); LAssert(FieldText != NULL); LItemColumn *c = list->AddColumn(FieldText, Width); if (c) { switch (i->GetInt(FIELD_ID)) { case FIELD_PRIORITY: { int x = 12; if (Bounds) { x = Bounds[ICON_PRIORITY_BLACK].X() + 7; } c->Width(x); c->Image(ICON_PRIORITY_BLACK); c->Resizable(false); break; } case FIELD_FLAGS: { int x = 14; if (Bounds) { x = Bounds[ICON_FLAGS_BLACK].X() + 7; } c->Width(x); c->Image(ICON_FLAGS_BLACK); c->Resizable(false); break; } case FIELD_SIZE: { c->TextAlign(LCss::Len(LCss::AlignRight)); break; } } } FieldArray[n++] = (int)i->GetInt(FIELD_ID); } break; } } // Add all items to list if (View()) { View()->SetContainer(this); // tell the list who we are if (GetSortCol() >= 0) { // set current sort settings View()->SetSort(GetSortCol(), GetSortAscend()); } } Prof.Add("Load things"); // FIXME: LoadThings(); } // Filter List Is; // Do any threading/sorting static LString SortMsg; if (!Thread() && GetSortField()) { SortMsg.Printf("Sorting " LPrintfInt64 " items", Items.Length()); Prof.Add(SortMsg); if (GetItemType() == MAGIC_ANY) { Items.Sort(TrashCompare, (NativeInt)this); } else { // Sort.. // if (!SortItems()) Items.Sort(ThingCompare, (NativeInt)this); } } // Do any filtering... Prof.Add("Filtering"); ThingFilter *Filter = App->GetThingFilter(); auto FilterStart = LCurrentTime(); size_t Pos = 0; for (auto t: Items) { t->SetFieldArray(FieldArray); if (!Filter || Filter->TestThing(t)) { // Add anyway... because all items are not part of list Is.Insert(t); } if ((LCurrentTime()-FilterStart) > 300) { if (!Loading && Loading.Reset(new LoadingItem(NULL))) list->Insert(Loading, 0); if (Loading) { LString s; s.Printf(LPrintfInt64 " of " LPrintfInt64 ", %.1f%%", Pos, Items.Length(), (double)Pos * 100 / Items.Length()); Loading->SetText(s); } FilterStart = LCurrentTime(); } Pos++; } Prof.Add("Inserting"); if (View() && Is[0]) { View()->Insert(Is, -1, true); } Prof.Add("Deleting"); list->DeletePlaceHolders(); Loading.Reset(); Prof.Add("OnSelect"); GetFldObj()->OnSelect(true); CurState = FldState_Idle; } void ScribeFolder::OnUpdateUnRead(int Offset, bool ScanItems) { if (!d->InUpdateUnread) { d->InUpdateUnread = true; int OldUnRead = GetUnRead(); d->DsBase.Reset(); d->DsUnread.Reset(); NameCache.Reset(); if (ScanItems) { if (GetItemType() == MAGIC_MAIL || GetItemType() == MAGIC_ANY) { size_t Count = 0; for (auto t: Items) { Mail *m = t->IsMail(); if (m && !TestFlag(m->GetFlags(), MAIL_READ)) Count++; } SetUnRead((int32_t)Count); } } else if (Offset != 0) { SetUnRead(GetUnRead() + Offset); } if (GetUnRead() < 0) SetUnRead(0); if (OldUnRead != GetUnRead()) { for (LTreeItem *i = GetParent(); i; i = i->GetParent()) { ScribeFolder *tc = dynamic_cast(i); if (tc && tc->GetParent()) tc->OnUpdateUnRead(0, false); } SetDirty(); if (d->IsInbox > 0) UpdateOsUnread(); } ChildUnRead = Expanded() ? 0 : _UnreadChildren(); Update(); d->InUpdateUnread = false; } } void ScribeFolder::EmptyFieldList() { if (GetFldObj()) GetFldObj()->Fields().DeleteObjects(); } void ScribeFolder::SerializeFieldWidths(bool Write) { if (GetFldObj() && View()) { // LAssert(View()->GetColumns() == Object->Fields().Length()); int Cols = MIN(View()->GetColumns(), (int)GetFldObj()->Fields().Length()); for (int i=0; iColumnAt(i); LDataPropI *f = GetFldObj()->Fields()[i]; if (c && f) { if (f->GetInt(FIELD_WIDTH) != c->Width()) { if (Write) { c->Width((int)f->GetInt(FIELD_WIDTH)); } else { f->SetInt(FIELD_WIDTH, c->Width()); SetDirty(); } } } else LAssert(0); } } } void ScribeFolder::OnProperties(int Tab) { if (!GetObject()) return; SerializeFieldWidths(); if (View()) { SetSort(View()->GetSortCol(), View()->GetSortAscending()); } OpenFolderProperties(this, Tab, [this](auto repop) { if (repop) { SetDirty(); SerializeFieldWidths(true); Populate(View()); } }); } ScribeFolder *ScribeFolder::GetSubFolder(const char *Path) { if (!Path) return NULL; if (*Path == '/') Path++; ScribeFolder *Folder = NULL; char Name[256]; auto Sep = strchr((char*)Path, '/'); if (Sep) { ZeroObj(Name); memcpy(Name, Path, Sep-Path); } else { strcpy_s(Name, sizeof(Name), Path); } LTreeItem *Starts[2] = { GetChild(), IsRoot() ? GetNext() : 0 }; for (int s=0; !Folder && sGetNext()) { ScribeFolder *f = dynamic_cast(i); if (f) { auto n = f->GetName(true); if (n.Equals(Name)) { if (Sep) Folder = f->GetSubFolder(Sep+1); else Folder = f; break; } } } } return Folder; } ScribeFolder *ScribeFolder::CreateSubFolder(const char *Name, int Type) { ScribeFolder *NewFolder = 0; auto ThisObj = dynamic_cast(GetObject()); if (Name && ThisObj && ThisObj->GetStore()) { LDataI *Fld = GetObject()->GetStore()->Create(MAGIC_FOLDER); if (Fld) { LDataFolderI *Obj = dynamic_cast(Fld); if (Obj) { NewFolder = new ScribeFolder; if (NewFolder) { NewFolder->App = App; NewFolder->SetObject(Obj, false, _FL); // Set name and type NewFolder->SetName(Name, true); NewFolder->GetObject()->SetInt(FIELD_FOLDER_TYPE, Type); ThisObj->SubFolders(); if (NewFolder->GetObject()->Save(ThisObj)) { Insert(NewFolder); NewFolder->SetDefaultFields(); NewFolder->OnItemType(); } else { DeleteObj(NewFolder); } } } } } return NewFolder; } bool ScribeFolder::Delete(LArray &Items, bool ToTrash) { if (!App) return false; List NotNew; LArray Del; LDataStoreI *Store = NULL; for (auto i: Items) { if (i->IsPlaceHolder()) { i->DecRef(); } else { Mail *m = i->IsMail(); if (m) NotNew.Insert(m); auto ObjStore = i->GetObject() ? i->GetObject()->GetStore() : NULL; if (!Store) Store = ObjStore; if (Store == ObjStore) Del.Add(i->GetObject()); else LAssert(!"All objects must have the same store."); } } if (NotNew.Length()) App->OnNewMail(&NotNew, false); if (Del.Length() == 0) return true; if (!Store) { LgiTrace("%s:%i - No Store?\n", _FL); return false; } if (!Store->Delete(Del, ToTrash)) { LgiTrace("%s:%i - Store.Delete failed.\n", _FL); return false; } return true; } class MoveToState { ScribeWnd *App = NULL; // Input ScribeFolder *Folder = NULL; LArray Items; bool CopyOnly; std::function&)> Callback; // Output bool Result = false; // Overall success/failure LArray Status; // Per item status // State LArray InStoreMove; // Object in the same store... LDataI *FolderObj = NULL; LDataStoreI *FolderStore = NULL; ScribeMailType NewBayesType = BayesMailUnknown; LHashTbl, int> Map; int NewFolderType = -1; bool BuildDynMenus = false; bool BayesInc = false; size_t Moves = 0; // Returns true if the object is deleted. bool SetStatus(int i, Store3Status s) { LAssert(Status[i] == Store3NotImpl); Status[i] = s; LAssert(Moves > 0); Moves--; if (Moves > 0) return false; OnMovesDone(); return true; } public: MoveToState(ScribeFolder *folder, LArray &items, bool copyOnly, std::function&)> callback) : Folder(folder), Items(items), CopyOnly(copyOnly), Callback(callback) { // Validate parameters if (Folder && (App = Folder->App)) { LVariant v; if (App->GetOptions()->GetValue(OPT_BayesIncremental, v)) BayesInc = v.CastInt32() != 0; } else { delete this; return; } if ((FolderObj = Folder->GetObject())) { FolderStore = Folder->GetObject()->GetStore(); } Status.Length(Moves = Items.Length()); for (auto &s: Status) s = Store3NotImpl; auto FolderItemType = Folder->GetItemType(); auto ThisFolderPath = Folder->GetPath(); NewBayesType = App->BayesTypeFromPath(ThisFolderPath); NewFolderType = App->GetFolderType(Folder); ScribeFolder *TemplatesFolder = App->GetFolder(FOLDER_TEMPLATES); BuildDynMenus = Folder == TemplatesFolder; for (unsigned i=0; iGetObject()) { if (SetStatus(i, Store3Error)) return; continue; } auto ThingItemType = t->Type(); if (FolderItemType != ThingItemType && FolderItemType != MAGIC_ANY) { if (SetStatus(i, Store3Error)) return; continue; } ScribeFolder *Old = t->GetFolder(); LString Path; if (Old) { Path = Old->GetPath(); if (Old == TemplatesFolder) // Moving to or from the templates folder... update the menu BuildDynMenus = true; } if (Old && Path) { bool IsDeleted = false; App->GetAccessLevel(App, Old->GetFolderPerms(ScribeWriteAccess), Path, [this, i, t, &IsDeleted](bool Allow) { if (Allow) IsDeleted = Move(i, t); else IsDeleted = SetStatus(i, Store3NoPermissions); }); // If the callback has already been executed and the object is deleted, exit immediately. if (IsDeleted) return; } else { if (Move(i, t)) return; } } } // This must call SetStatus once and only once for each item it's called with. // Returns true if the SetStatus call indicates deletion. // 'this' will be invalid after SetStatus returns true. bool Move(int i, Thing *t) { ScribeMailType OldBayesType = BayesMailUnknown; if (BayesInc && t->IsMail() && TestFlag(t->IsMail()->GetFlags(), MAIL_READ)) { OldBayesType = App->BayesTypeFromPath(t->IsMail()); } ScribeFolder *Old = t->GetFolder(); Store3Status r = Store3NotImpl; int OldFolderType = Old ? App->GetFolderType(Old) : -1; if ( (OldFolderType == FOLDER_TRASH || OldFolderType == FOLDER_SENT) && NewFolderType == FOLDER_TRASH) { // Delete for good r = Old ? Old->DeleteThing(t, NULL) : Store3Error; } else { // If this folder is currently selected... if (Folder->Select()) { // Insert item into list t->SetFieldArray(Folder->FieldArray); } if (CopyOnly) { LDataI *NewT = FolderStore->Create(t->Type()); if (NewT) { NewT->CopyProps(*t->GetObject()); r = NewT->Save(Folder->GetObject()); } else { r = Store3Error; } } else { if (NewFolderType != FOLDER_TRASH && OldBayesType != NewBayesType) { App->OnBayesianMailEvent(t->IsMail(), OldBayesType, NewBayesType); } // Move to this folder auto o = t->GetObject(); if (o && o->GetStore() == FolderStore) { InStoreMove.Add(o); Map.Add(t, i); r = Store3Delayed; } else { // Out of store more... use the old single object method... for the moment.. r = t->SetFolder(Folder); if (r == Store3Success) { // Remove from the list.. if (Old && Old->Select() && App->GetMailList()) App->GetMailList()->Remove(t); } } } } if (r == Store3Success) t->OnMove(); return SetStatus(i, r); } void OnMovesDone() { if (InStoreMove.Length()) { Store3Status s = Store3NotImpl; auto Fld = dynamic_cast(Folder->GetObject()); if (!Fld) s = Store3Error; else s = FolderStore->Move(Fld, InStoreMove); Result = s >= Store3Delayed; for (auto p: Map) { Status[p.value] = s; if (s == Store3Success) { LAssert(p.key->GetFolder() == Folder); LAssert(Items.HasItem(p.key)); p.key->OnMove(); } } } if (BuildDynMenus) // Moving to or from the templates folder... update the menu App->BuildDynMenus(); if (Callback) Callback(Result, Status); delete this; } }; void ScribeFolder::MoveTo(LArray &Items, bool CopyOnly, std::function&)> Callback) { if (Items.Length() == 0) return; if (!GetObject() || !App) return; new MoveToState(this, Items, CopyOnly, Callback); } int ThingFilterCompare(Thing *a, Thing *b, NativeInt Data) { Filter *A = a->IsFilter(); Filter *B = b->IsFilter(); return (A && B) ? A->GetIndex() - B->GetIndex() : 0; } void ScribeFolder::ReSort() { if (View() && Select()) { View()->SetSort(GetSortCol(), GetSortAscend()); } } void ScribeFolder::SetSort(int Col, bool Ascend, bool CanDirty) { if (GetItemType() == MAGIC_FILTER) { // Remove any holes in the indexing int i = 1; Items.Sort(ThingFilterCompare); for (auto t : Items) { Filter *f = t->IsFilter(); if (f) { if (f->GetIndex() != i) { f->SetIndex(i); } i++; } } } if (GetSortCol() != Col || GetSortAscend() != (uchar)Ascend) { GetObject()->SetInt(FIELD_SORT, (Col + 1) * (Ascend ? 1 : -1)); if (CanDirty) SetDirty(); } } int ScribeFolder::GetSortField() { int Status = 0; int Col = GetSortCol(); if (Col >= 0 && Col < (int)FieldArray.Length()) Status = FieldArray[Col]; return Status; } class FolderStream : public LStream { ScribeFolder *f; // Current index into the thing array int Idx; // Total known size of stream... int64 Sz; // A memory buffer containing just the encoded 'Thing' LMemStream Mem; LString FolderMime; public: FolderStream(ScribeFolder *folder) : Mem(512 << 10) { f = folder; Idx = 0; Sz = 0; switch (f->GetItemType()) { case MAGIC_MAIL: FolderMime = sMimeMbox; break; case MAGIC_CONTACT: FolderMime = sMimeVCard; break; case MAGIC_CALENDAR: FolderMime = sMimeVCalendar; break; default: LAssert(!"Need a mime type?"); break; } } int GetIndex() { return Idx; } int Open(const char *Str = 0,int Int = 0) { return true; } bool IsOpen() { return true; } int Close() { return 0; } int64 GetSize() { return -1; } int64 SetSize(int64 Size) { return -1; } int64 GetPos() { return -1; } int64 SetPos(int64 pos) { // This means that IStream::Seek return E_NOTIMPL, which is important // for windows to support copies of unknown size. return -1; } ssize_t Read(void *Buffer, ssize_t Size, int Flags = 0) { // Read from stream.. int64 Remaining = Mem.GetSize() - Mem.GetPos(); if (Remaining <= 0) { Thing *t = Idx < (ssize_t)f->Items.Length() ? f->Items[Idx++] : NULL; if (t) { Mem.SetSize(0); // We can't allow Export to delete the Mem object, we own it. // So create a proxy object for it. LAutoPtr cp(new LProxyStream(&Mem)); if (t->Export(cp, FolderMime)) Sz += Mem.GetSize(); else LAssert(0); Mem.SetPos(0); } else return 0; } Remaining = Mem.GetSize() - Mem.GetPos(); if (Remaining > 0) { int Common = (int)MIN(Size, Remaining); ssize_t Rd = Mem.Read(Buffer, Common); if (Rd > 0) return Rd; else LAssert(0); } return 0; } ssize_t Write(const void *Buffer, ssize_t Size, int Flags = 0) { LAssert(0); return 0; } LStreamI *Clone() { LAssert(0); return NULL; } }; // ScribeFolder drag'n'drop bool ScribeFolder::GetFormats(LDragFormats &Formats) { if (GetItemType() == MAGIC_MAIL || GetItemType() == MAGIC_CONTACT || GetItemType() == MAGIC_CALENDAR) { Formats.SupportsFileStreams(); } Formats.Supports(ScribeFolderObject); return Formats.Length() > 0; } bool ScribeFolder::OnBeginDrag(LMouse &m) { if (GetParent()) Drag(Tree, m.Event, DROPEFFECT_MOVE | DROPEFFECT_COPY); return true; } void ScribeFolder::OnEndData() { DropFileName.Empty(); } LString ScribeFolder::GetDropFileName() { LAutoString Fn(MakeFileName(GetObject()->GetStr(FIELD_FOLDER_NAME), 0)); DropFileName = Fn; if (GetItemType() == MAGIC_MAIL) DropFileName += ".mbx"; else if (GetItemType() == MAGIC_CONTACT) DropFileName += ".vcf"; else if (GetItemType() == MAGIC_CALENDAR) DropFileName += ".ics"; else if (GetItemType() == MAGIC_FILTER) DropFileName += ".xml"; return DropFileName; } bool ScribeFolder::GetData(LArray &Data) { ssize_t DataSet = 0; for (unsigned idx=0; idxSetPos(0); auto Fn = GetDropFileName(); auto MimeType = sMimeMbox; auto r = dd.AddFileStream(LGetLeaf(Fn), MimeType, s); LAssert(r); } DataSet = dd.Data.Length(); } else if (dd.IsFormat(LGI_FileDropFormat)) { LMouse m; if (App->GetMouse(m, true)) { LString::Array Files; if (GetDropFileName()) { LAutoPtr f(new LFile); if (f->Open(DropFileName, O_WRITE)) { if (GetItemType() == MAGIC_MAIL) Export(AutoCast(f), sMimeMbox); else if (GetItemType() == MAGIC_CONTACT) Export(AutoCast(f), sMimeVCard); else if (GetItemType() == MAGIC_CALENDAR) Export(AutoCast(f), sMimeVCalendar); } } if (DropFileName) { Files.Add(DropFileName.Get()); if (CreateFileDrop(&dd, m, Files)) { DataSet++; } else LgiTrace("%s:%i - CreateFileDrop failed.\n", _FL); } else LgiTrace("%s:%i - No drop file name.\n", _FL); } else LgiTrace("%s:%i - GetMouse failed.\n", _FL); } else if (dd.IsFormat(ScribeFolderObject)) { ScribeClipboardFmt *Fmt = ScribeClipboardFmt::Alloc(true, 1); if (Fmt) { Fmt->FolderAt(0, this); dd.Data[0].SetBinary(Fmt->Sizeof(), Fmt); DataSet++; free(Fmt); } } } return DataSet > 0; } void ScribeFolder::CollectSubFolderMail(ScribeFolder *To) { if (!To) To = this; LoadThings(NULL, [this, To](auto Status) { LArray Items; for (auto Item: Items) { if (To != this && Item->IsMail()) Items.Add(Item); } To->MoveTo(Items, false, NULL); for (ScribeFolder *f = GetChildFolder(); f; f = f->GetNextFolder()) { f->CollectSubFolderMail(To); } }); } void ScribeFolder::OnReceiveFiles(LArray &Files) { if (Files.Length()) { for (unsigned i=0; i f(new LTextFile); if (f->Open(File, O_READ)) Import(AutoCast(f), MimeType); } } } // Import/export bool ScribeFolder::GetFormats(bool Export, LString::Array &MimeTypes) { MimeTypes.Add(sMimeMbox); if (!Export) MimeTypes.Add(sMimeMessage); return MimeTypes.Length() > 0; } ThingType::IoProgress ScribeFolder::Import(IoProgressImplArgs) { if (Stricmp(mimeType, sMimeMbox) == 0 || Stricmp(mimeType, "text/x-mail") == 0) { // Mail box format... ThingType::IoProgress p(Store3Delayed); p.prog = new ImportFolderTask(this, stream, mimeType, cb); return p; } else if (Stricmp(mimeType, sMimeVCard) == 0) { VCard Io; Thing *t; bool Error = false; int Imported = 0; int Idx = 0; while ((t = App->CreateItem(GetItemType(), 0, false))) { Contact *c = t->IsContact(); if (!c) { t->DecRef(); Error = true; break; } if (Io.Import(c->GetObject(), stream)) { const char *First = 0, *Last = 0; c->GetField(FIELD_FIRST_NAME, First); c->GetField(FIELD_LAST_NAME, Last); LgiTrace("Import %i %s %s\n", Idx, First, Last); if (t->Save(this)) { Imported++; } else { Error = true; break; } } else { t->DecRef(); break; } Idx++; } if (Error) { LgiMsg( App, LLoadString(IDS_ERROR_IMPORT_COUNT), AppName, MB_OK, LLoadString(IDC_CONTACTS), Imported); IoProgressError("Contact import error."); } IoProgressSuccess(); } else if (Stricmp(mimeType, sMimeVCalendar) == 0) { VCal Io; Thing *t; while ((t = App->CreateItem(GetItemType(), this, false))) { if (Io.Import(t->GetObject(), stream)) { if (!t->Save(this)) IoProgressError("Contact save failed."); } else { t->DecRef(); break; } } IoProgressSuccess(); } else if (GetObject()) { Thing *t = App->CreateThingOfType(GetItemType(), GetObject()->GetStore()->Create(GetItemType())); if (!t) IoProgressError("Failed to create contact"); if (!t->Import(stream, mimeType)) { t->DecRef(); IoProgressError("Contact import failed."); } if (!t->Save(this)) { t->DecRef(); IoProgressError("Contact save failed."); } IoProgressSuccess(); } IoProgressNotImpl(); } class FolderExportTask : public LProgressDlg { LAutoPtr Out; ScribeFolder *Folder; LString MimeType; int Idx; bool HasError = false; public: // Minimum amount of time to do work. constexpr static int WORK_SLICE_MS = 130; // This should be larger then WORK_SLICE_MS to allow message loop to process constexpr static int PULSE_MS = 200; FolderExportTask(LAutoPtr out, ScribeFolder *folder, LString mimeType) : LProgressDlg(folder->App) { Out = out; Folder = folder; MimeType = mimeType; Idx = 0; Ts = LCurrentTime(); Folder->App->OnFolderTask(this, true); bool Mbox = _stricmp(MimeType, sMimeMbox) == 0; // Clear the files contents Out->SetSize(0); // Setup progress UI SetDescription(Mbox ? LLoadString(IDS_MBOX_WRITING) : (char*)"Writing..."); SetRange(Folder->Items.Length()); switch (Folder->GetItemType()) { case MAGIC_MAIL: SetType(LLoadString(IDS_EMAIL)); break; case MAGIC_CALENDAR: SetType(LLoadString(IDS_CALENDAR)); break; case MAGIC_CONTACT: SetType(LLoadString(IDS_CONTACT)); break; case MAGIC_GROUP: SetType("Groups"); break; default: SetType("Objects"); break; } SetPulse(PULSE_MS); SetAlwaysOnTop(true); } ~FolderExportTask() { Folder->App->OnFolderTask(this, false); } bool OnRequestClose(bool OsClose) { return true; } void OnPulse() { LProgressDlg::OnPulse(); PostEvent(M_EXPORT_NEXT); } LMessage::Result OnEvent(LMessage *Msg) { if (Msg->Msg() == M_EXPORT_NEXT) { auto StartTs = LCurrentTime(); while ( !IsCancelled() && (LCurrentTime() - StartTs) < WORK_SLICE_MS) { if (Idx >= (ssize_t)Folder->Items.Length()) { Quit(); break; } // Process all the container's items Thing *t = Folder->Items[Idx++]; if (t) { if (t->Export(Out, MimeType, NULL)) { Value(Idx); } else { HasError = true; LgiMsg(this, "Error exporting items.", AppName); Quit(); } } } return 0; } return LProgressDlg::OnEvent(Msg); } }; // This is the mime type used to storage objects on disk const char *ScribeFolder::GetStorageMimeType() { auto Type = GetItemType(); switch (Type) { case MAGIC_MAIL: return sMimeMbox; case MAGIC_CALENDAR: return sMimeVCalendar; case MAGIC_CONTACT: return sMimeVCard; case MAGIC_FILTER: return sMimeXml; default: LgiTrace("%s:%i - Unsupported storage type: %s\n", _FL, Store3ItemTypeName(Type)); break; } return NULL; } void ScribeFolder::ExportAsync(LAutoPtr f, const char *MimeType, std::function Callback) { if (!MimeType) { LAssert(!"No Mimetype"); if (Callback) Callback(NULL); return; } LoadThings( NULL, [ this, Str = f.Release(), MimeType = LString(MimeType), Callback ] (auto Status) { LAutoPtr f(Str); FolderExportTask *Task = NULL; if (Status == Store3Success) Task = new FolderExportTask(f, this, MimeType); if (Callback) Callback(Task); }); } ThingType::IoProgress ScribeFolder::Export(IoProgressImplArgs) { IoProgress ErrStatus(Store3Error); if (!mimeType) { ErrStatus.errMsg = "No mimetype."; if (cb) cb(&ErrStatus, NULL); return ErrStatus; } if (!LoadThings()) { ErrStatus.errMsg = "Failed to load things."; if (cb) cb(&ErrStatus, NULL); return ErrStatus; } IoProgress Status(Store3Delayed); Status.prog = new ExportFolderTask(this, stream, mimeType, cb); return Status; } size_t ScribeFolder::Length() { if (GetItemType() == MAGIC_MAIL) { ThingList *v = View(); if (v) return v->Length(); } if (IsLoaded()) return Items.Length(); return GetItems(); } ssize_t ScribeFolder::IndexOf(Mail *m) { if (GetItemType() == MAGIC_MAIL) { ThingList *v = View(); if (v) return v->IndexOf(m); return Items.IndexOf(m); } return -1; } Mail *ScribeFolder::operator [](size_t i) { if (GetItemType() == MAGIC_MAIL) { ThingList *v = View(); if (v) return dynamic_cast(v->ItemAt(i)); Thing *t = Items[i]; if (t) return t->IsMail(); } return NULL; } bool ScribeFolder::GetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { case SdType: // Type: Int32 { Value = GetObject()->Type(); break; } case SdName: // Type: String { Value = GetName(true).Get(); break; } case SdPath: // Type: String { auto p = GetPath(); if (p) Value = p; else return false; break; } case SdUnread: // Type: Int32 { Value = GetUnRead(); break; } case SdLength: // Type: Int32 { Value = (int32)Items.Length(); break; } case SdItem: // Type: Thing[] { Value.Empty(); LoadThings(); // Use in sync mode, no callback // This call back HAS to set value one way or another... if (Array) { bool IsNumeric = true; for (auto *v = Array; *v; v++) { if (!IsDigit(*v)) { IsNumeric = false; break; } } if (IsNumeric) { int Idx = atoi(Array); if (Idx >= 0 && Idx < (ssize_t)Items.Length()) { Value = (LDom*) Items[Idx]; return true; } } else // Is message ID? { for (auto t : Items) { Mail *m = t->IsMail(); if (!m) break; auto Id = m->GetMessageId(); if (Id && !strcmp(Id, Array)) { Value = (LDom*)t; return true; } } } } else if (Value.SetList()) { for (auto t : Items) Value.Value.Lst->Insert(new LVariant((LDom*)t)); return true; } break; } case SdItemType: // Type: Int32 { Value = GetItemType(); break; } case SdScribe: // Type: ScribeWnd { Value = (LDom*)App; break; } case SdChild: // Type: ScribeFolder { Value = (LDom*)GetChildFolder(); break; } case SdNext: // Type: ScribeFolder { Value = (LDom*)GetNextFolder(); break; } case SdSelected: // Type: Thing[] { if (!Select() || !Value.SetList()) return false; List a; if (!App->GetMailList()->GetSelection(a)) return false; for (auto t: a) { Value.Value.Lst->Insert(new LVariant(t)); } break; } case SdExpanded: // Type: Boolean { Value = Expanded(); break; } default: { return false; } } return true; } bool ScribeFolder::SetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { case SdName: // Type: String { char *s = Value.CastString(); if (ValidStr(s)) OnRename(s); else return false; break; } case SdExpanded: // Type: Boolean { Expanded(Value.CastInt32() != 0); break; } default: return false; } return true; } bool ScribeFolder::CallMethod(const char *MethodName, LScriptArguments &Args) { ScribeDomType m = StrToDom(MethodName); switch (m) { case SdLoad: // Type: () { LoadThings(App); // FIXME: Callback for status? return true; } case SdSelect: // Type: () { Select(true); return true; } case SdImport: // Type: (String FileName, String MimeType) { *Args.GetReturn() = false; if (Args.Length() != 2) LgiTrace("%s:%i - Error: expecting 2 arguments to 'Import'.\n", _FL); else { auto FileName = Args[0]->Str(); LAutoPtr f(new LFile); if (f->Open(FileName, O_READ)) { auto p = Import(AutoCast(f), Args[1]->Str()); *Args.GetReturn() = p.status; } else LgiTrace("%s:%i - Error: Can't open '%s' for reading.\n", _FL, FileName); } break; } case SdExport: // Type: (String FileName, String MimeType) { *Args.GetReturn() = false; if (Args.Length() != 2) LgiTrace("%s:%i - Error: expecting 2 arguments to 'Export'.\n", _FL); else { auto FileName = Args[0]->Str(); LAutoPtr f(new LFile); if (f->Open(FileName, O_WRITE)) { auto p = Export(AutoCast(f), Args[1]->Str()); *Args.GetReturn() = p.status; } else LgiTrace("%s:%i - Error: Can't open '%s' for writing.\n", _FL, FileName); } break; } default: break; } return false; } void ScribeFolder::OnRethread() { if (GetThreaded()) { Thread(); } } diff --git a/src/ScribeMail.cpp b/src/ScribeMail.cpp --- a/src/ScribeMail.cpp +++ b/src/ScribeMail.cpp @@ -1,10308 +1,10304 @@ /* ** FILE: ScribeMail.cpp ** AUTHOR: Matthew Allen ** DATE: 11/11/98 ** DESCRIPTION: Scribe Mail Object and UI ** ** Copyright (C) 1998-2003, Matthew Allen ** fret@memecode.com */ #include #include #include #include #include #include #include "Scribe.h" #include "ScribePageSetup.h" #include "lgi/common/Popup.h" #include "lgi/common/ColourSelect.h" #include "lgi/common/TextView3.h" #include "lgi/common/Html.h" #include "lgi/common/Combo.h" #include "lgi/common/Edit.h" #include "lgi/common/Button.h" #include "lgi/common/TextLabel.h" #include "lgi/common/CheckBox.h" #include "lgi/common/TabView.h" #include "lgi/common/Input.h" #include "lgi/common/ClipBoard.h" #include "lgi/common/TableLayout.h" #include "lgi/common/DisplayString.h" #include "lgi/common/ThreadEvent.h" #include "lgi/common/GdcTools.h" #include "lgi/common/Charset.h" #include "../src/common/Coding/ScriptingPriv.h" #include "PrintPreview.h" #include "ScribeListAddr.h" #include "PrintContext.h" #include "lgi/common/LgiRes.h" #include "Encryption/GnuPG.h" #include "ObjectInspector.h" #include "lgi/common/EventTargetThread.h" #include "Store3Common.h" #include "Tables.h" #include "Calendar.h" #include "CalendarView.h" #include "AddressSelect.h" #include "Store3Imap/ScribeImap.h" #include "lgi/common/TextConvert.h" #include "lgi/common/FileSelect.h" #include "resdefs.h" #include "resource.h" #include "lgi/common/Printer.h" #include "lgi/common/SubProcess.h" #define SAVE_HEADERS 0 static char MsgIdEncodeChars[] = "%/"; static char ScribeReplyClass[] = "scribe_reply"; static char ScribeReplyStyles[] = "margin-left: 0.5em;\n" "padding-left: 0.5em;\n" "border-left: 1px solid #ccc;"; char DefaultTextReplyTemplate[] = { "---------- Original Message ----------\n" "To: <>\n" "From: <>\n" "Subject: \n" "Date: \n" "\n" "\n" "\n" "\n" }; char DefaultHtmlReplyTemplate[] = { "\n" "\n" "\n" "\n" "\n" "

---------- Original Message ----------
\n" " To: <?mail.tohtml?>
\n" " From: <?mail.fromhtml?>
\n" " Subject: <?mail.subject?>
\n" " Date: <?mail.datesent?>
\n" "
\n" " <?mail.bodyastext quote=scribe.quote?>
\n" " <?cursor?>
\n" " <?mail.sig[html]?>

\n" "\n" "\n" }; class DepthCheck { int &i; public: constexpr static int MaxDepth = 5; DepthCheck(int &val) : i(val) { i++; if (!*this) LAssert(!"Recursion?"); } ~DepthCheck() { i--; } operator bool() const { return i < MaxDepth; } }; void CollectAttachments(LArray *Attachments, LArray *Related, LDataI **Text, LDataI **Html, LDataPropI *d, Store3MimeType *ParentMime = NULL) { if (!d) return; Store3MimeType Mt(d->GetStr(FIELD_MIME_TYPE)); auto FileName = d->GetStr(FIELD_NAME); if (!Mt) Mt = "text/plain"; // printf("Collect %s %s\n", (char*)Mt, FileName); auto Att = dynamic_cast(d); if (ParentMime && Att && ParentMime->IsRelated() && Related) { auto Id = d->GetStr(FIELD_CONTENT_ID); if (ValidStr(Id)) { if (Related) Related->Add(Att); Att = NULL; } else if (Mt.IsHtml() && Html) { *Html = Att; Att = NULL; } } if (Att) { if (ValidStr(FileName)) { if (Attachments) Attachments->Add(Att); } else if (Mt.IsHtml()) { if (Html) *Html = Att; } else if (Mt.IsPlainText()) { if (Text) *Text = Att; } else if (!Mt.IsMultipart()) { if (Attachments) Attachments->Add(Att); } /* if (d->GetInt(FIELD_SIZE) < (512 << 10)) a.Add(dynamic_cast(d)); */ } auto It = d->GetList(FIELD_MIME_SEG); if (It) { for (auto i = It->First(); i; i = It->Next()) CollectAttachments(Attachments, Related, Text, Html, i, Mt.IsMultipart() ? &Mt : NULL); } } void RemoveReturns(char *s) { // Delete out the '\r' chars. char *In = s; char *Out = s; while (*In) { if (*In != '\r') { *Out++ = *In; } In++; } *Out++ = 0; } class XmlSaveStyles : public LXmlTree { void OnParseComment(LXmlTag *Ref, const char *Comment, ssize_t Bytes) { if (Ref && Ref->IsTag("style")) { Ref->SetContent(Comment, Bytes); } } public: XmlSaveStyles(int flags) : LXmlTree(flags) { } }; bool ExtractHtmlContent(LString &OutHtml, LString &Charset, LString &Styles, const char *InHtml) { if (!InHtml) return false; XmlSaveStyles t(GXT_NO_ENTITIES | GXT_NO_DOM | GXT_NO_HEADER); LXmlTag r; LMemStream mem(InHtml, strlen(InHtml), false); if (!t.Read(&r, &mem)) return false; bool InHead = false; LStringPipe Style; r.Children.SetFixedLength(false); for (auto It = r.Children.begin(); It != r.Children.end(); ) { LXmlTag *c = *It; if (c->IsTag("style")) { if (ValidStr(c->GetContent())) Style.Print("%s\n", c->GetContent()); c->Parent = NULL; r.Children.Delete(It); DeleteObj(c); } else if (c->IsTag("/head")) { InHead = false; c->Parent = NULL; r.Children.Delete(It); DeleteObj(c); } else if (c->IsTag("body")) { // We remove this tag, but KEEP the content... if any if (ValidStr(c->GetContent())) { c->SetTag(NULL); It++; } else { // No content, remove entirely. c->Parent = NULL; r.Children.Delete(It); DeleteObj(c); } } else if (InHead || c->IsTag("html") || c->IsTag("/html") || c->IsTag("/body") || c->IsTag("/style")) { c->Parent = NULL; r.Children.Delete(It); DeleteObj(c); } else if (c->IsTag("head")) { InHead = true; c->Parent = NULL; r.Children.Delete(It); DeleteObj(c); } else It++; } LStringPipe p; t.Write(&r, &p); OutHtml = p.NewLStr(); Styles = Style.NewLStr(); #if 0 LgiTrace("InHtml=%s\n", InHtml); LgiTrace("OutHtml=%s\n", OutHtml.Get()); LgiTrace("Styles=%s\n", Styles.Get()); #endif return true; } ////////////////////////////////////////////////////////////////////////////// char MailToStr[] = "mailto:"; char SubjectStr[] = "subject="; char ContentTypeDefault[] = "Content-type: text/plain; charset=us-ascii"; extern LString HtmlToText(const char *Html, const char *InitialCharSet); extern LString TextToHtml(const char *Txt, const char *Charset); class ImageResizeThread : public LEventTargetThread { LOptionsFile *Opts; public: class Job { #ifdef __GTK_H__ /* This object may not exist when the worker is finished. However LAppInst->PostEvent can handle that so we'll allow it, so long as it's never used in the Sink->PostEvent form. */ LViewI *Sink; #else OsView Sink; #endif public: LString FileName; LAutoStreamI Data; void SetSink(LViewI *v) { #if !LGI_VIEW_HANDLE Sink = v; #else Sink = v->Handle(); #endif } bool PostEvent(int Msg, LMessage::Param a = 0, LMessage::Param b = 0) { #ifdef __GTK_H__ return LAppInst->PostEvent(Sink, Msg, a, b); #else return LPostEvent(Sink, Msg, a, b); #endif } }; ImageResizeThread(LOptionsFile *opts) : LEventTargetThread("ImageResize") { Opts = opts; } void Resize(LAutoPtr &Job) { LVariant Qual = 80, Px = 1024, SizeLimit = 200, v; Opts->GetValue(OPT_ResizeJpegQual, Qual); Opts->GetValue(OPT_ResizeMaxPx, Px); Opts->GetValue(OPT_ResizeMaxKb, SizeLimit); LAutoStreamI Input = Job->Data; LAutoStreamI MemBuf(new LMemFile(4 << 10)); int64 FileSize = Input->GetSize(); LStream *sImg = dynamic_cast(Input.Get()); LAutoPtr Img(GdcD->Load(sImg, Job->FileName)); if (Img) { int iPx = Px.CastInt32(); int iKb = SizeLimit.CastInt32(); if (Img->X() > iPx || Img->Y() > iPx || FileSize >= iKb << 10) { // Create a JPEG filter auto Jpeg = LFilterFactory::New(".jpg", FILTER_CAP_WRITE, NULL); if (Jpeg) { // Re-sample the image... double XScale = (double) Img->X() / iPx; double YScale = (double) Img->Y() / iPx; // double Aspect = (double) Img->X() / Img->Y(); double Scale = XScale > YScale ? XScale : YScale; if (Scale > 1.0) { int Nx = (int)(Img->X() / Scale + 0.001); int Ny = (int)(Img->Y() / Scale + 0.001); LAutoPtr ResizedImg(new LMemDC(Nx, Ny, Img->GetColourSpace())); if (ResizedImg) { if (ResampleDC(ResizedImg, Img)) { Img = ResizedImg; } } } // Compress the image.. LXmlTag Props; Props.SetValue(LGI_FILTER_QUALITY, Qual); Props.SetValue(LGI_FILTER_SUBSAMPLE, v = 1); // 2x2 Jpeg->Props = &Props; if (Jpeg->WriteImage(dynamic_cast(MemBuf.Get()), Img) == LFilter::IoSuccess) { Job->Data = MemBuf; } } } } Job->PostEvent(M_RESIZE_IMAGE, (LMessage::Param)Job.Get()); Job.Release(); // Do this after the post event... so the deref doesn't crash. } LMessage::Result OnEvent(LMessage *Msg) { switch (Msg->Msg()) { case M_RESIZE_IMAGE: { LAutoPtr j((Job*)Msg->A()); if (j) Resize(j); break; } } return 0; } }; #include "lgi/common/RichTextEdit.h" #include "../src/common/Widgets/Editor/RichTextEditPriv.h" class MailRendererScript : public LThread, public LCancel, public LDom { Mail *m; LScriptCallback *cb; public: MailRendererScript(Mail *ml, LScriptCallback *script) : m(ml), cb(script), LThread("MailRendererScript") { Run(); } ~MailRendererScript() { Cancel(true); while (!IsExited()) LSleep(1); } int Main() { LScriptArguments Args(NULL); Args.New() = new LVariant(m->App); Args.New() = new LVariant(m); Args.New() = new LVariant((LDom*)this); m->App->ExecuteScriptCallback(*cb, Args); return 0; } bool CallMethod(const char *MethodName, LScriptArguments &Args) { ScribeDomType Method = StrToDom(MethodName); *Args.GetReturn() = false; switch (Method) { case SdGetTemp: { *Args.GetReturn() = ScribeTempPath(); break; } case SdExecute: { *Args.GetReturn() = false; if (Args.Length() >= 3) { auto Dir = Args[0]->Str(); auto Exe = Args[1]->Str(); auto Arg = Args[2]->Str(); if (Dir && Exe && Arg) { LSubProcess p(Exe, Arg); p.SetInitFolder(Dir); if (p.Start()) { char Buf[256]; LStringPipe Out; Out.Print("%s %s\n", Exe, Arg); ssize_t Rd; while (p.IsRunning() && !IsCancelled()) { Rd = p.Read(Buf, sizeof(Buf)); if (Rd < 0) break; if (Rd == 0) LSleep(1); else Out.Write(Buf, Rd); } while (!IsCancelled() && (Rd = p.Read(Buf, sizeof(Buf))) > 0) Out.Write(Buf, Rd); // LgiTrace("%s:%i - Process:\n%s\n", _FL, Out.NewLStr().Get()); if (p.IsRunning()) p.Kill(); else *Args.GetReturn() = true; } } } break; } case SdSetHtml: { if (!IsCancelled() && Args.Length() > 0) { LView *Ui = m->GetUI(); if (!Ui) { // Maybe hasn't finished opening the UI? LSleep(100); Ui = m->GetUI(); } if (!Ui) Ui = m->App; if (Ui) Ui->PostEvent(M_SET_HTML, (LMessage::Param)new LString(Args[0]->Str())); } break; } default: LAssert(!"Unsupported call."); return false; } return true; } }; class MailPrivate { LAutoPtr Resizer; Mail *m; public: struct HtmlBody { LString Html, Charset, Styles; }; LAutoPtr Body; LAutoPtr Renderer; LString MsgIdCache; LString DomainCache; int InSetFlags = 0; int InSetFlagsCache = 0; MailPrivate(Mail *mail) { m = mail; } void OnSave() { Resizer.Reset(); } HtmlBody *GetBody() { if (!Body && !Body.Reset(new HtmlBody)) return NULL; if (ValidStr(m->GetHtml())) { ExtractHtmlContent( Body->Html, Body->Charset, Body->Styles, m->GetHtml()); } else if (ValidStr(m->GetBody())) { Body->Charset = m->GetCharSet(); Body->Html = TextToHtml(m->GetBody(), Body->Charset); } return Body; } bool AddResizeImage(Attachment *File) { if (!File || !m->GetUI()) return false; if (!Resizer) Resizer.Reset(new ImageResizeThread(m->App->GetOptions())); if (!Resizer) return false; LAutoStreamI Obj = File->GetObject()->GetStream(_FL); if (!Obj) return false; ImageResizeThread::Job *j = new ImageResizeThread::Job; if (!j) return false; // The user interface will handle the response... j->SetSink(m->GetUI()); j->FileName = File->GetName(); // Make a complete copy of the stream... j->Data.Reset(new LMemStream(Obj, 0, -1)); // Post the work over to the thread... Resizer->PostEvent(M_RESIZE_IMAGE, (LMessage::Param)j); // Mark the image resizing File->SetIsResizing(true); return true; } }; bool Mail::ResizeImage(Attachment *a) { return d->AddResizeImage(a); } AttachmentList::AttachmentList(int id, int x, int y, int cx, int cy, MailUi *ui) : LList(id, x, y, cx, cy, 0) { Ui = ui; } AttachmentList::~AttachmentList() { RemoveAll(); } void AttachmentList::OnItemClick(LListItem *Item, LMouse &m) { LList::OnItemClick(Item, m); if (!Item && m.IsContextMenu() && Ui) { LSubMenu RClick; RClick.AppendItem(LLoadString(IDS_ATTACH_FILE), IDM_OPEN, true); switch (RClick.Float(this, m)) { case IDM_OPEN: { Ui->PostEvent(M_COMMAND, IDM_ATTACH_FILE, 0); break; } } } } bool AttachmentList::OnKey(LKey &k) { if (k.vkey == LK_DELETE) { if (k.Down()) { List s; if (GetSelection(s)) { Attachment *a = dynamic_cast(s[0]); if (a) { a->OnDeleteAttachment(this, true); } } } return true; } return LList::OnKey(k); } ////////////////////////////////////////////////////////////////////////////// int Strnlen(const char *s, int n) { int i = 0; if (s) { if (n < 0) { while (*s++) { i++; } } else { while (*s++ && n-- > 0) { i++; } } } return i; } ////////////////////////////////////////////////////////////////////////////// MailContainer::~MailContainer() { MailContainerIter *i; while ((i = Iters[0])) { Iters.Delete(i); if (i->Container) { i->Container = 0; } } } MailContainerIter::MailContainerIter() { Container = 0; } MailContainerIter::~MailContainerIter() { if (Container) { Container->Iters.Delete(this); } } void MailContainerIter::SetContainer(MailContainer *c) { Container = c; if (Container) { Container->Iters.Insert(this); } } ////////////////////////////////////////////////////////////////////////////// uint32_t MarkColours32[IDM_MARK_MAX] = { Rgb32(255, 0, 0), // red Rgb32(255, 166, 0), // orange Rgb32(255, 222, 0), // yellow Rgb32(0, 0xa0, 0), // green Rgb32(0, 0xc0, 255),// cyan Rgb32(0, 0, 255), // blue Rgb32(192, 0, 255), // purple Rgb32(128, 128, 128) // grey }; ItemFieldDef MailFieldDefs[] = { {"To", SdTo, GV_STRING, FIELD_TO}, {"From", SdFrom, GV_STRING, FIELD_FROM}, {"Subject", SdSubject, GV_STRING, FIELD_SUBJECT}, {"Size", SdSize, GV_INT64, FIELD_SIZE}, {"Received Date", SdReceivedDate, GV_DATETIME, FIELD_DATE_RECEIVED}, {"Send Date", SdSendDate, GV_DATETIME, FIELD_DATE_SENT}, {"Body", SdBody, GV_STRING, FIELD_TEXT}, {"Internet Header", SdInternetHeader, GV_STRING, FIELD_INTERNET_HEADER, IDC_INTERNET_HEADER}, {"Message ID", SdMessageId, GV_STRING, FIELD_MESSAGE_ID}, {"Priority", SdPriority, GV_INT32, FIELD_PRIORITY}, {"Flags", SdFlags, GV_INT32, FIELD_FLAGS}, {"Html", SdHtml, GV_STRING, FIELD_ALTERNATE_HTML}, {"Label", SdLabel, GV_STRING, FIELD_LABEL}, {"From Contact", SdContact, GV_STRING, FIELD_FROM_CONTACT_NAME}, {"File", SdFile, GV_STRING, FIELD_CACHE_FILENAME}, {"ImapFlags", SdImapFlags, GV_STRING, FIELD_CACHE_FLAGS}, {"ImapSeq", SdFile, GV_INT32, FIELD_IMAP_SEQ}, {"ImapUid", SdImapFlags, GV_INT32, FIELD_SERVER_UID}, {"ReceivedDomain", SdReceivedDomain, GV_STRING, FIELD_RECEIVED_DOMAIN}, {"MessageId", SdMessageId, GV_STRING, FIELD_MESSAGE_ID}, {0} }; ////////////////////////////////////////////////////////////////////////////// class LIdentityItem : public LListItem { ScribeWnd *App; ScribeAccount *Acc; char *Txt; public: LIdentityItem(ScribeWnd *app, ScribeAccount *acc) { App = app; Acc = acc; Txt = 0; LVariant e, n; if (Acc) { n = Acc->Identity.Name(); e = Acc->Identity.Email(); } else { LAssert(!"No account specified"); } if (e.Str() && n.Str()) { char t[256]; sprintf_s(t, sizeof(t), "%s <%s>", n.Str(), e.Str()); Txt = NewStr(t); } else if (e.Str()) { Txt = NewStr(e.Str()); } else if (n.Str()) { Txt = NewStr(n.Str()); } else { Txt = NewStr("(error)"); } } ~LIdentityItem() { DeleteArray(Txt); } ScribeAccount *GetAccount() { return Acc; } const char *GetText(int i) { switch (i) { case 0: { return Txt; break; } } return 0; } }; class LIdentityDropDrop : public LPopup { ScribeWnd *App; Mail *Email; LList *Lst; public: LIdentityDropDrop(ScribeWnd *app, Mail *mail, LView *owner) : LPopup(owner) { App = app; Email = mail; LRect r(0, 0, 300, 100); SetPos(r); Children.Insert(Lst = new LList(IDC_LIST, 2, 2, X()-4, Y()-4)); if (Lst) { Lst->SetParent(this); Lst->AddColumn("Identity", Lst->GetClient().X()); Lst->MultiSelect(false); if (App) { Lst->Insert(new LIdentityItem(App, 0)); for (auto a : *App->GetAccounts()) { if (a->Identity.Name().Str()) { Lst->Insert(new LIdentityItem(App, a)); } } /* for (LListItem *i = List->First(); i; i = List->Next()) { LIdentityItem *Item = dynamic_cast(i); if (Item) { char *IdEmail = a->Send.IdentityEmail(); if (Email && IdEmail && Email->From->Addr && _stricmp(Email->From->Addr, IdEmail) == 0) { Item->Select(true); } } } */ } } } void OnPaint(LSurface *pDC) { LRect r(GetClient()); LWideBorder(pDC, r, DefaultRaisedEdge); } int OnNotify(LViewI *c, LNotification n) { switch (c->GetId()) { case IDC_LIST: { if (n.Type == LNotifyItemClick) { Visible(false); LIdentityItem *NewFrom = dynamic_cast(Lst->GetSelected()); if (Email && NewFrom) { // ScribeAccount *a = NewFrom->GetAccount(); if (Email->GetUI()) { LList *FromList; if (GetViewById(IDC_FROM, FromList)) { // Change item data /* FIXME DeleteArray(Email->From->Name); DeleteArray(Email->From->Addr); DeleteArray(Email->Reply->Name); DeleteArray(Email->Reply->Addr); if (a) { Email->From->Addr = NewStr(a->Send.IdentityEmail().Str()); Email->From->Name = NewStr(a->Send.IdentityName().Str()); Email->Reply->Addr = NewStr(a->Send.IdentityReplyTo().Str()); if (Email->Reply->Addr) { Email->Reply->Name = NewStr(Email->From->Name); } } else { LVariant e, n, r; App->GetOptions()->GetValue(OPT_EmailAddr, e); App->GetOptions()->GetValue(OPT_UserName, n); App->GetOptions()->GetValue(OPT_ReplyToEmail, n); Email->From->Addr = NewStr(e.Str()); Email->From->Name = NewStr(n.Str()); Email->Reply->Addr = NewStr(r.Str()); if (Email->Reply->Addr) { Email->Reply->Name = NewStr(Email->From->Name); } } // Change UI FromList->Empty(); LDataPropI *na = new LDataPropI(Email->From); if (na) { na->CC = MAIL_ADDR_FROM; FromList->Insert(na); } */ } } } } break; } } return 0; } }; ////////////////////////////////////////////////////////////////////////////// LSubMenu* BuildMarkMenu( LSubMenu *MarkMenu, MarkedState MarkState, uint32_t SelectedMark, bool None, bool All, bool Select) { int SelectedIndex = -1; // Build image list LImageList *ImgLst = new LImageList(16, 16); if (ImgLst && ImgLst->Create(16 * CountOf(MarkColours32), 16, System32BitColourSpace)) { // ImgLst->Colour(1); ImgLst->Colour(L_MED); ImgLst->Rectangle(); for (int i=0; iColour(L_LOW); ImgLst->Box(i*16+1, 0, i*16+15, 14); SelectedIndex = i; } ImgLst->Colour(MarkColours32[i], 32); ImgLst->Rectangle(i*16+3, 2, i*16+13, 12); } } // Build Submenu if (MarkMenu && ImgLst) { ImgLst->Update(-1); MarkMenu->SetImageList(ImgLst); LMenuItem *Item = NULL; if (None) { Item = MarkMenu->AppendItem(LLoadString(IDS_NONE), Select ? IDM_SELECT_NONE : IDM_UNMARK, MarkState != MS_None); } if (All) { Item = MarkMenu->AppendItem(LLoadString(IDS_ALL), Select ? IDM_SELECT_ALL : IDM_MARK_ALL, true); } if (Item) { MarkMenu->AppendSeparator(); } for (int i=0; iAppendItem(s, ((Select) ? IDM_MARK_SELECT_BASE : IDM_MARK_BASE) + i, (MarkState != 1) || (i != SelectedIndex)); if (Item) { Item->Icon(i); } } } return MarkMenu; } ////////////////////////////////////////////////////////////////////////////// char *WrapLines(char *Str, int Len, int WrapColumn) { if (Str && Len > 0 && WrapColumn > 0) { LMemQueue Temp; int LastWhite = -1; int StartLine = 0; int XPos = 0; int i; for (i=0; Str[i] && i= WrapColumn && Len > 0) { Temp.Write((uchar*) Str+StartLine, Len); Temp.Write((uchar*) "\n", 1); XPos = 0; StartLine = StartLine + Len + 1; LastWhite = -1; } else { LastWhite = i; XPos++; } } else if (Str[i] == '\t') { XPos = ((XPos + 7) / 8) * 8; } else if (Str[i] == '\n') { Temp.Write((uchar*) Str+StartLine, i - StartLine + 1); XPos = 0; StartLine = i+1; } else { XPos++; } } Temp.Write((uchar*) Str+StartLine, i - StartLine + 1); int WrapLen = (int)Temp.GetSize(); char *Wrapped = new char[WrapLen+1]; if (Wrapped) { Temp.Read((uchar*) Wrapped, WrapLen); Wrapped[WrapLen] = 0; return Wrapped; } } return Str; } char *DeHtml(const char *Str) { char *r = 0; if (Str) { LMemQueue Buf; char Buffer[256]; auto s = Str; while (s && *s) { // Search for start of next tag const char *Start = s; const char *End = Start; for (; *End && *End != '<'; End++); // Push pre-tag data onto pipe size_t Len = End-Start; for (size_t i=0; i, ItemFieldDef*> Lut(256); if (Lut.Length() == 0) { for (ItemFieldDef **l=FieldLists; *l; l++) { for (ItemFieldDef *i = *l; i->FieldId; i++) { if (i->Option) Lut.Add(i->Option, i); } } } return (ItemFieldDef*) Lut.Find(Name); } return 0; } static bool FieldLutInit = false; static LArray IdToFieldLut; ItemFieldDef *GetFieldDefById(int Id) { if (!FieldLutInit) { FieldLutInit = true; for (ItemFieldDef **l=FieldLists; *l; l++) { for (ItemFieldDef *i = *l; i->FieldId; i++) { IdToFieldLut[i->FieldId] = i; } } } if (Id >= 0 && Id < (int)IdToFieldLut.Length()) return IdToFieldLut[Id]; return 0; } ////////////////////////////////////////////////////////////////////////////// void Log(char *File, char *Str, ...) { #if defined WIN32 const char *DefFile = "c:\\temp\\list.txt"; #else const char *DefFile = "/home/list.txt"; #endif if (Str) { LFile f; if (f.Open((File) ? File : DefFile, O_WRITE)) { char Buf[1024]; va_list Arg; va_start(Arg, Str); vsprintf_s(Buf, sizeof(Buf), Str, Arg); va_end(Arg); f.Seek(0, SEEK_END); f.Write(Buf, (int)strlen(Buf)); } } else { LFile f; if (f.Open((File) ? File : DefFile, O_WRITE)) { f.SetSize(0); } } } char *NewPropStr(LOptionsFile *Options, char *Name) { LVariant n; if (Options->GetValue(Name, n) && n.Str() && strlen(n.Str()) > 0) { return NewStr(n.Str()); } return 0; } ////////////////////////////////////////////////////////////////////////////// // Columns of controls #define MAILUI_Y 0 #define IDM_REMOVE_GRTH 1000 #define IDM_REMOVE_GRTH_SP 1001 #define IDM_REMOVE_HTML 1002 #define IDM_CONVERT_B64_TO_BIN 1003 #define IDM_CONVERT_BIN_TO_B64 1004 #define RECIP_SX 500 #define ADD_X (RECIP_X + RECIP_SX + 10) #ifdef MAC #define ADD_RECIP_BTN_X 36 #else #define ADD_RECIP_BTN_X 20 #endif #define CONTENT_BORDER 3 #if defined WIN32 #define DLG_X 15 #define DLG_Y 30 #else #define DLG_X 6 #define DLG_Y 6 #endif MailUi::MailUi(Mail *item, MailContainer *container) : ThingUi(item, LLoadString(IDS_MAIL_MESSAGE)) { // Init everything to 0 Container = container; if (!item || !item->App) { LAssert(!"Invalid ptrs"); return; } // This allows us to hook iconv conversion events LFontSystem::Inst()->Register(this); // This allows us to hook missing image library events GdcD->Register(this); // Read/Write access bool ReadOnly = !TestFlag(GetItem()->GetFlags(), MAIL_CREATED | MAIL_BOUNCE); int MinButY = 0; // Get position LRect r(150, 150, 800, 750); SetPos(r); int FontHeight = GetFont()->GetHeight(); LVariant v; LOptionsFile *Options = App ? App->GetOptions() : 0; if (Options) { if (Options->GetValue("MailUI.Pos", v)) { r.SetStr(v.Str()); } } SetPos(r); MoveSameScreen(App); Name(LLoadString(IDS_MAIL_MESSAGE)); #if WINNATIVE CreateClassW32("Scribe::MailUi", LoadIcon(LProcessInst(), MAKEINTRESOURCE(IDI_MAIL))); #endif bool IsCreated = TestFlag(GetItem()->GetFlags(), MAIL_CREATED); if (Attach(0)) { DropTarget(true); // Setup main toolbar Commands.Toolbar = App->LoadToolbar(this, App->GetResourceFile(ResToolbarFile), App->GetToolbarImgList()); if (Commands.Toolbar) { Commands.Toolbar->Raised(false); Commands.Toolbar->Attach(this); BtnSend = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_SEND)), IDM_SEND_MSG, TBT_PUSH, !ReadOnly, IMG_SEND); Commands.Toolbar->AppendSeparator(); BtnSave = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_SAVE)), IDM_SAVE, TBT_PUSH, !ReadOnly, IMG_SAVE); BtnSaveClose = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_SAVE_CLOSE)), IDM_SAVE_CLOSE, TBT_PUSH, !ReadOnly, IMG_SAVE_AND_CLOSE); Commands.Toolbar->AppendSeparator(); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_DELETE)), IDM_DELETE_MSG, TBT_PUSH, true, IMG_TRASH); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_SPAM)), IDM_DELETE_AS_SPAM, TBT_PUSH, true, IMG_DELETE_SPAM); BtnAttach = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_ATTACH_FILE)), IDM_ATTACH_FILE, TBT_PUSH, !ReadOnly, IMG_ATTACH_FILE); Commands.Toolbar->AppendSeparator(); BtnReply = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_REPLY)), IDM_REPLY, TBT_PUSH, ReadOnly, IMG_REPLY); BtnReplyAll = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_REPLYALL)), IDM_REPLY_ALL, TBT_PUSH, ReadOnly, IMG_REPLY_ALL); BtnForward = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_FORWARD)), IDM_FORWARD, TBT_PUSH, ReadOnly, IMG_FORWARD); BtnBounce = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_BOUNCE)), IDM_BOUNCE, TBT_PUSH, ReadOnly, IMG_BOUNCE); Commands.Toolbar->AppendSeparator(); BtnPrev = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_PREV_MSG)), IDM_PREV_MSG, TBT_PUSH, true, IMG_PREV_ITEM); BtnNext = Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_NEXT_MSG)), IDM_NEXT_MSG, TBT_PUSH, true, IMG_NEXT_ITEM); Commands.Toolbar->AppendSeparator(); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_HIGH_PRIORITY)), IDM_HIGH_PRIORITY, TBT_TOGGLE, true, IMG_HIGH_PRIORITY); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_LOW_PRIORITY)), IDM_LOW_PRIORITY, TBT_TOGGLE, true, IMG_LOW_PRIORITY); Commands.Toolbar->AppendSeparator(); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_READ_RECEIPT)), IDM_READ_RECEIPT, TBT_TOGGLE, true, IMG_READ_RECEIPT); Commands.Toolbar->AppendSeparator(); Commands.Toolbar->AppendButton(RemoveAmp(LLoadString(IDS_PRINT)), IDM_PRINT, TBT_PUSH, true, IMG_PRINT); for (LViewI *w: Commands.Toolbar->IterateViews()) MinButY = MAX(MinButY, w->Y()); Commands.Toolbar->Customizable(App->GetOptions(), "MailWindowToolbar"); Commands.SetupCallbacks(App, this, GetItem(), LThingUiToolbar); } // Setup to/from panels LVariant AlwaysShowFrom; if (IsCreated) App->GetOptions()->GetValue(OPT_MailShowFrom, AlwaysShowFrom); bool ShowFrom = !IsCreated && !TestFlag(GetItem()->GetFlags(), MAIL_BOUNCE); LDisplayString Recip(LSysFont, LLoadString(IDS_RECIPIENTS)); LAutoPtr ReplyChk(new LCheckBox(IDC_USE_REPLY_TO, 21, #ifdef MAC 1, #else 6, #endif -1, -1, "Reply To")); int ReplyChkPx = ReplyChk ? ReplyChk->X() : 0; int RECIP_X = 20 + MAX(ReplyChkPx, Recip.X()) + 10; int EditHeight = FontHeight + 6; LVariant HideGpg; if (!item->App->GetOptions()->GetValue(OPT_HideGnuPG, HideGpg) || HideGpg.CastInt32() == 0) { GpgUi = new MailUiGpg( App, this, 21, RECIP_X, !ReadOnly && !TestFlag(GetItem()->GetFlags(), MAIL_SENT)); if (GpgUi) GpgUi->Attach(this); } ToPanel = new LPanel(LLoadString(IDS_TO), FontHeight * 8, !ShowFrom); if (ToPanel) { int Cy = 4; ToPanel->AddView(SetTo = new LCombo(IDC_SET_TO, 20, Cy, 60, EditHeight, 0)); if (SetTo) { SetTo->Insert(LLoadString(IDS_TO)); SetTo->Insert(LLoadString(IDS_CC)); SetTo->Insert(LLoadString(IDS_BCC)); if (SetTo->GetPos().x2 + 10 > RECIP_X) RECIP_X = SetTo->GetPos().x2 + 10; } ToPanel->AddView(Entry = new LEdit(IDC_ENTRY, RECIP_X, Cy, RECIP_SX, EditHeight, "")); Cy = SetTo->GetPos().y2 + 5; ToPanel->AddView(To = new AddressList(App, IDC_TO, RECIP_X, Cy, RECIP_SX, ToPanel->GetOpenSize() - Cy - 6)); if (To) To->SetImageList(App->GetIconImgList(), false); ToPanel->AddView(new LTextLabel(IDC_STATIC, 20, Cy, -1, -1, LLoadString(IDS_RECIPIENTS))); ToPanel->Raised(false); ToPanel->Attach(this); } FromPanel = new LPanel(LLoadString(IDS_FROM), FontHeight + 13, AlwaysShowFrom.CastInt32() || ShowFrom); if (FromPanel) { FromPanel->AddView(new LTextLabel(IDC_STATIC, 21, #ifdef MAC 8, #else 4, #endif -1, -1, LLoadString(IDS_FROM))); if (ShowFrom) { FromPanel->AddView(FromList = new AddressList(App, IDC_FROM, RECIP_X, 2, RECIP_SX, EditHeight)); } else { FromPanel->AddView(FromCbo = new LCombo(IDC_FROM, RECIP_X, 2, RECIP_SX, EditHeight, 0)); } if (IsCreated) { LCheckBox *Chk; auto Label = LLoadString(IDS_ALWAYS_SHOW); FromPanel->AddView(Chk = new LCheckBox(IDC_SHOW_FROM, ADD_X, #ifdef MAC 1, #else 6, #endif -1, -1, Label)); if (Chk) Chk->Value(AlwaysShowFrom.CastInt32()); } FromPanel->Raised(false); FromPanel->Attach(this); } ReplyToPanel = new LPanel("Reply To:", FontHeight + 13, false); if (ReplyToPanel) { ReplyToPanel->Raised(false); ReplyToPanel->AddView(ReplyToChk = ReplyChk.Release()); ReplyToPanel->AddView(ReplyToCbo = new LCombo(IDC_REPLY_TO_ADDR, RECIP_X, 2, RECIP_SX, EditHeight, 0)); ReplyToPanel->Attach(this); } SubjectPanel = new LPanel(LLoadString(IDS_SUBJECT), -(LSysFont->GetHeight() + 16)); if (SubjectPanel) { SubjectPanel->Raised(false); SubjectPanel->AddView( new LTextLabel(IDC_STATIC, 21, 8, -1, -1, LLoadString(IDS_SUBJECT))); SubjectPanel->AddView(Subject = new LEdit(IDC_SUBJECT, RECIP_X, 4, RECIP_SX, EditHeight, "")); SubjectPanel->Attach(this); } CalendarPanel = new LPanel(LLoadString(IDC_CALENDAR), -(LSysFont->GetHeight() + 16), false); if (CalendarPanel) { CalendarPanel->Raised(false); LTableLayout *t = new LTableLayout(IDC_TABLE); if (t) { t->GetCss(true)->Margin(LCss::Len(LCss::LenPx, LTableLayout::CellSpacing)); t->GetCss()->Width(LCss::LenAuto); t->GetCss()->Height(LCss::LenAuto); CalendarPanel->AddView(t); auto c = t->GetCell(0, 0); c->Width(LCss::Len(LCss::LenPx, RECIP_X - (LTableLayout::CellSpacing * 2))); c->PaddingLeft(LCss::Len(LCss::LenPx, 21 - LTableLayout::CellSpacing)); c->Debug = true; c->Add(new LTextLabel(IDC_STATIC, 0, 0, -1, -1, LLoadString(IDS_CALENDAR))); c = t->GetCell(1, 0); c->Add(new LButton(IDC_ADD_CAL_EVENT, 0, 0, -1, -1, LLoadString(IDS_ADD_CAL))); c = t->GetCell(2, 0); c->Add(new LButton(IDC_ADD_CAL_EVENT_POPUP, 0, 0, -1, -1, LLoadString(IDS_ADD_CAL_POPUP))); c = t->GetCell(3, 0); c->Add(CalPanelStatus = new LTextLabel(IDC_STATIC, 0, 0, -1, -1, NULL)); } CalendarPanel->Attach(this); } Tab = new LTabView(IDC_MAIL_UI_TABS, 0, 0, 1000, 1000, 0); if (Tab) { Tab->GetCss(true)->PaddingTop("3px"); // Don't attach text and html controls here, because OnLoad will do it later... TabText = Tab->Append(LLoadString(IDS_TEXT)); TabHtml = Tab->Append("HTML"); TabAttachments = Tab->Append(LLoadString(IDS_ATTACHMENTS)); TabHeader = Tab->Append(LLoadString(IDS_INTERNETHEADER)); if (TabAttachments) { TabAttachments->Append(Attachments = new AttachmentList(IDC_ATTACHMENTS, CONTENT_BORDER, CONTENT_BORDER, 200, 200, this)); if (Attachments) { Attachments->AddColumn(LLoadString(IDS_FILE_NAME), 160); Attachments->AddColumn(LLoadString(IDS_SIZE), 80); Attachments->AddColumn(LLoadString(IDS_MIME_TYPE), 100); Attachments->AddColumn(LLoadString(IDS_CONTENT_ID), 250); } } if (TabHeader) { TabHeader->Append(Header = new LTextView3( IDC_INTERNET_HEADER, CONTENT_BORDER, CONTENT_BORDER, 100, 20)); if (Header) { Header->Sunken(true); } } LTabPage *Fields = Tab->Append(LLoadString(IDS_FIELDS)); if (Fields) { Fields->LoadFromResource(IDD_MAIL_FIELDS); LTableLayout *l; if (GetViewById(IDC_TABLE, l)) { l->SetPourLargest(false); } } Tab->Attach(this); // Colours LColourSelect *Colour; if (GetViewById(IDC_COLOUR, Colour)) { LArray c32; for (int i=0; iSetColourList(&c32); } else LAssert(!"No colour control?"); } PourAll(); if (Commands.Toolbar) { if (ToPanel) ToPanel->SetClosedSize(Commands.Toolbar->Y()-1); if (FromPanel) FromPanel->SetClosedSize(Commands.Toolbar->Y()-1); if (ReplyToPanel) ReplyToPanel->SetClosedSize(Commands.Toolbar->Y()-1); } SetIcon("About64px.png"); OnLoad(); _Running = true; Visible(true); RegisterHook(this, LKeyEvents); LResources::StyleElement(this); } } MailUi::~MailUi() { DeleteObj(TextView); if (Attachments) { Attachments->RemoveAll(); } LOptionsFile *Options = (GetItem() && App) ? App->GetOptions() : 0; if (Options) { LRect p = GetPos(); if (p.x1 >= 0 && p.y1 >= 0) { LVariant v = p.GetStr(); Options->SetValue("MailUI.Pos", v); } } Tab = 0; if (GetItem()) { GetItem()->Ui = NULL; } else LgiTrace("%s:%i - Error: no item to clear UI ptr?\n", _FL); // We delete the LView here because objects // need to have their virtual tables intact _Delete(); } Mail *MailUi::GetItem() { return _Item ? _Item->IsMail() : 0; } void MailUi::SetItem(Mail *m) { if (_Item) { Mail *old = _Item->IsMail(); if (old) old->Ui = NULL; else LAssert(0); _Item = NULL; } if (m) { _Item = m; m->Ui = this; } } bool MailUi::SetDirty(bool Dirty, bool Ui) { bool b = ThingUi::SetDirty(Dirty, Ui); if (MetaFieldsDirty && !Dirty) { Mail *m = GetItem(); if (m) { LColourSelect *Colour; if (GetViewById(IDC_COLOUR, Colour)) { uint32_t Col = (uint32_t)Colour->Value(); m->SetMarkColour(Col); } else LgiTrace("%s:%i - Can't find IDC_COLOUR\n", _FL); auto s = GetCtrlName(IDC_LABEL); m->SetLabel(s); if (m->GetObject()->GetInt(FIELD_STORE_TYPE) != Store3Imap) // Imap knows to save itself. m->SetDirty(); m->Update(); } MetaFieldsDirty = false; } return b; } #define IDM_FILTER_BASE 2000 #define IDM_CHARSET_BASE 3000 void AddActions(LSubMenu *Menu, List &Filters, LArray Folders) { if (!Menu) return; auto StartLen = Menu->Length(); for (auto Folder: Folders) { for (ScribeFolder *f=Folder->GetChildFolder(); f; f=f->GetNextFolder()) { auto Sub = Menu->AppendSub(f->GetName(true)); if (Sub) { auto Item = Sub->GetParent(); if (Item) Item->Icon(ICON_CLOSED_FOLDER); Sub->SetImageList(Menu->GetImageList(), false); f->LoadThings(); AddActions(Sub, Filters, {f}); } } List a; Folder->LoadThings(); for (auto t: Folder->Items) { a.Insert(t->IsFilter()); } a.Sort(FilterCompare); for (auto i: a) { LVariant Name; if (i->GetVariant("Name", Name) && Name.Str()) { char n[256]; strcpy_s(n, sizeof(n), Name.Str()); for (char *s=n; *s; s++) { if (*s == '&') { memmove(s + 1, s, strlen(s)+1); s++; } } auto Item = Menu->AppendItem(n, (int) (IDM_FILTER_BASE + Filters.Length()), true); if (Item) { Item->Icon(ICON_FILTER); Filters.Insert(i); } } } } if (StartLen == Menu->Length()) { char s[64]; sprintf_s(s, sizeof(s), "(%s)", LLoadString(IDS_EMPTY)); Menu->AppendItem(s, 0, false); } } bool MailUi::OnViewKey(LView *v, LKey &k) { if (k.Down() && k.CtrlCmd() && !k.Alt()) { switch (k.vkey) { case LK_RETURN: { PostEvent(M_COMMAND, IDM_SEND_MSG); return true; } case LK_UP: { SeekMsg(-1); return true; } case LK_DOWN: { SeekMsg(1); return true; } } switch (k.c16) { case 'p': case 'P': { App->ThingPrint(NULL, GetItem(), NULL, this); break; } case 'f': case 'F': { if (Tab->Value() == 0) { if (TextView) TextView->DoFind(NULL); } else if (Tab->Value() == 1) { if (HtmlView) HtmlView->DoFind(NULL); } return true; } case 'r': case 'R': { OnCommand(IDM_REPLY, 0, NULL); return true; } case 'w': case 'W': { if (OnRequestClose(false)) Quit(); return true; } case 's': case 'S': { if (IsDirty()) { SetDirty(false); } return true; } case '1': { Tab->Value(0); if (TextView) TextView->Focus(true); return true; } case '2': { Tab->Value(1); if (HtmlView) HtmlView->Focus(true); return true; } } } return ThingUi::OnViewKey(v, k); } void MailUi::SerializeText(bool FromCtrl) { if (GetItem() && TextView) { if (FromCtrl) { GetItem()->SetBody(TextView->Name()); } else { TextView->Name(GetItem()->GetBody()); } } } const char *FilePart(const char *Uri) { const char *Dir = Uri + strlen(Uri) - 1; while (Dir > Uri && Dir[-1] != '/' && Dir[-1] != '\\') { Dir--; } return Dir; } void MailUi::OnAttachmentsChange() { Attachments->ResizeColumnsToContent(); if (GetItem()) { TabAttachments->GetCss(true)->FontBold(GetItem()->Attachments.Length() > 0); TabAttachments->OnStyleChange(); } } bool MailUi::NeedsCapability(const char *Name, const char *Param) { if (!InThread()) { PostEvent(M_NEEDS_CAP, (LMessage::Param)NewStr(Name)); } else { if (!Name) return false; if (Caps.Find(Name)) return true; Caps.Add(Name, true); char msg[256]; LArray Actions; LAutoPtr Back; if (!_stricmp(Name, "RemoteContent")) { Actions.Add(LLoadString(IDS_ALWAYS_SHOW_REMOTE_CONTENT)); Actions.Add(LLoadString(IDS_SHOW_REMOTE_CONTENT)); Back.Reset(new LColour(L_LOW)); strcpy_s(msg, sizeof(msg), LLoadString ( IDS_REMOTE_CONTENT_MSG, "To protect your privacy Scribe has blocked the remote content in this message." )); } else { Actions.Add(LLoadString(IDS_INSTALL)); int ch = 0; for (auto k : Caps) ch += sprintf_s(msg+ch, sizeof(msg)-ch, "%s%s", ch?", ":"", k.key); ch += sprintf_s(msg+ch, sizeof(msg)-ch, " is required to display this content."); } if (!MissingCaps) { MissingCaps = new MissingCapsBar(this, &Caps, msg, App, Actions, Back); auto c = IterateViews(); auto Idx = c.IndexOf(GpgUi); AddView(MissingCaps, (int)Idx+1); AttachChildren(); OnPosChange(); } else { MissingCaps->SetMsg(msg); } } return true; } void MailUi::OnCloseInstaller() { if (MissingCaps) { DeleteObj(MissingCaps); PourAll(); } } void MailUi::OnInstall(LCapabilityTarget::CapsHash *Caps, bool Status) { } void MailUi::OnChildrenChanged(LViewI *Wnd, bool Attaching) { if (Wnd == (LViewI*)MissingCaps && !Attaching) { MissingCaps = NULL; PourAll(); } } LDocView *MailUi::GetDoc(const char *MimeType) { if (!MimeType) { MimeType = sTextPlain; LVariant v; if (App->GetOptions()->GetValue(OPT_DefaultAlternative, v) && v.CastInt32() > 0) MimeType = sTextHtml; } LAssert(MimeType != NULL); if (!_stricmp(MimeType, sTextPlain)) return TextView; else if (!_stricmp(MimeType, sTextHtml)) return HtmlView; else LAssert(!"Invalid mime type."); return NULL; } bool MailUi::SetDoc(LDocView *v, const char *MimeType) { if (!MimeType) { MimeType = sTextPlain; LVariant v; if (App->GetOptions()->GetValue(OPT_DefaultAlternative, v) && v.CastInt32() > 0) MimeType = sTextHtml; } LAssert(MimeType != NULL); if (!_stricmp(MimeType, sTextPlain)) { LTextView3 *Txt = static_cast(v); if (Txt) { if (Txt != TextView) { DeleteObj(TextView); TextView = Txt; if (TabText && !TextView->IsAttached()) TabText->Append(TextView); } TextLoaded = true; if (_Running) TabText->Select(); } else { LAssert(!"Invalid ctrl."); return false; } } else if (!_stricmp(MimeType, sTextHtml)) { LDocView *Html = dynamic_cast(v); if (Html) { if (Html != HtmlView) { DeleteObj(HtmlView); HtmlView = Html; LCapabilityClient *cc = dynamic_cast(v); if (cc) cc->Register(this); if (TabHtml && !HtmlView->IsAttached()) TabHtml->Append(HtmlView); } HtmlLoaded = true; if (_Running) TabHtml->Select(); } else { LAssert(!"Invalid ctrl."); return false; } } else { LAssert(!"Invalid mime type."); return false; } return true; } bool MailUi::IsWorking(int Set) { if (!GetItem()) return false; // Are any of the attachments busy doing something? LDataI *AttachPoint = GetItem() && Set >= 0 ? GetItem()->GetFileAttachPoint() : NULL; List Attachments; if (GetItem()->GetAttachments(&Attachments)) { // Attachment *Match = NULL; for (auto a: Attachments) { if (Set >= 0) { a->SetIsResizing(Set != 0); if (AttachPoint) { a->GetObject()->Save(AttachPoint); } } else if (a->GetIsResizing()) { return true; } } } // Check rich text control as well... auto Rte = dynamic_cast(HtmlView); if (Rte) { if (Rte->IsBusy()) { return true; } } return false; } class BusyPanel : public LPanel { public: BusyPanel() : LPanel("Working...", LSysFont->GetHeight() << 1) { LViewI *v; LCss::ColorDef Bk(LColour(255, 128, 0)); GetCss(true)->BackgroundColor(Bk); AddView(v = new LTextLabel(IDC_STATIC, 30, 5, -1, -1, "Still resizing images...")); v->GetCss(true)->BackgroundColor(Bk); AddView(v = new LButton(IDCANCEL, 200, 3, -1, -1, LLoadString(IDS_CANCEL))); v->GetCss(true)->NoPaintColor(Bk); } }; void MailUi::SetCmdAfterResize(int Cmd) { if (CmdAfterResize == 0) { CmdAfterResize = Cmd; if (!WorkingDlg) { WorkingDlg = new BusyPanel; if (WorkingDlg) { AddView(WorkingDlg, 1); AttachChildren(); OnPosChange(); } } } } bool MailUi::OnRequestClose(bool OsClose) { bool Working = IsWorking(); if (Working) { SetCmdAfterResize(IDM_SAVE_CLOSE); return false; } return ThingUi::OnRequestClose(OsClose); } void MailUi::OnChange() { if (!IsDirty()) OnLoad(); } struct MailUiNameAddr { LString Name, Addr; MailUiNameAddr(const char *name = 0, const char *addr = 0) { Name = name; Addr = addr; } }; void MailUi::OnLoad() { bool Edit = false; bool ReadOnly = true; Mail *Item = GetItem(); _Running = false; if (Item && Item->App) { Edit = TestFlag(Item->GetFlags(), MAIL_CREATED); ReadOnly = !TestFlag(Item->GetFlags(), MAIL_CREATED | MAIL_BOUNCE); if (Entry) { Entry->Name(""); } if (To) { To->OnInit(Item->GetTo()); } if (FromCbo) { LHashTbl, MailUiNameAddr*> ReplyToAddrs; const char *Template = "%s <%s>"; FromCbo->Empty(); int Idx = -1; LVariant DefName, DefAddr; // LOptionsFile *Opts = Item->Window->GetOptions(); for (auto a : *Item->App->GetAccounts()) { LVariant Name = a->Identity.Name(); LVariant Addr = a->Identity.Email(); LVariant ReplyTo = a->Identity.ReplyTo(); if (!a->IsValid() || a->Send.Disabled()) continue; if (ReplyTo.Str()) { if (!ReplyToAddrs.Find(ReplyTo.Str())) ReplyToAddrs.Add(ReplyTo.Str(), new MailUiNameAddr(Name.Str(), ReplyTo.Str())); } else if (Addr.Str()) { if (!ReplyToAddrs.Find(Addr.Str())) ReplyToAddrs.Add(Addr.Str(), new MailUiNameAddr(Name.Str(), Addr.Str())); } if (Name.Str() && Addr.Str()) { if (!DefAddr.Str() || _stricmp(DefAddr.Str(), Addr.Str())) { auto FromAddr = Item->GetFromStr(FIELD_EMAIL); if (FromAddr && _stricmp(Addr.Str(), FromAddr) == 0) { Idx = (int)FromCbo->Length(); } LString p; p.Printf(Template, Name.Str(), Addr.Str()); int Id = a->Receive.Id(); int CurLen = (int)FromCbo->Length(); LAssert(Id != 0); FromAccountId[CurLen] = Id; FromCbo->Insert(p); } } } if (Idx < 0) { auto FromName = Item->GetFromStr(FIELD_NAME); auto FromAddr = Item->GetFromStr(FIELD_EMAIL); if (FromAddr) { LStringPipe p; if (FromName) p.Print(Template, FromName, FromAddr); else p.Print("<%s>", FromAddr); LAutoString s(p.NewStr()); FromAccountId[FromCbo->Length()] = -1; Idx = (int)FromCbo->Length(); FromCbo->Insert(s); FromCbo->Value(Idx); } } else { FromCbo->Value(Idx); } if (ReplyToAddrs.Length() > 0 && ReplyToCbo) { auto CurAddr = Item->GetReply() ? Item->GetReply()->GetStr(FIELD_EMAIL) : NULL; int CurIdx = -1; // for (MailUiNameAddr *na = ReplyToAddrs.First(); na; na = ReplyToAddrs.Next()) for (auto na : ReplyToAddrs) { char s[256]; sprintf_s(s, sizeof(s), Template, na.value->Name.Get(), na.value->Addr.Get()); if (CurAddr && !_stricmp(na.value->Addr, CurAddr)) CurIdx = (int)ReplyToCbo->Length(); ReplyToCbo->Insert(s); } if (CurIdx >= 0) { ReplyToCbo->Value(CurIdx); ReplyToChk->Value(true); } else { ReplyToChk->Value(false); } } ReplyToAddrs.DeleteObjects(); } else if (FromList) { FromList->Empty(); ListAddr *na = new ListAddr(App, Item->GetFrom()); if (na) { na->CC = MAIL_ADDR_FROM; na->OnFind(); FromList->Insert(na); } } if (Subject) { Subject->Name(Item->GetSubject()); char Title[140]; auto Subj = Item->GetSubject(); if (Subj) sprintf_s(Title, sizeof(Title), "%s - %.100s", LLoadString(IDS_MAIL_MESSAGE), Subj); else sprintf_s(Title, sizeof(Title), "%s", LLoadString(IDS_MAIL_MESSAGE)); Title[sizeof(Title)-1] = 0; for (char *s = Title; *s; s++) if (*s == '\n' || *s == '\r') *s = ' '; Name(Title); } int64_t Rgb32 = Item->GetMarkColour(); SetCtrlValue(IDC_COLOUR, Rgb32 > 0 ? Rgb32 : -1); SetCtrlName(IDC_LABEL, Item->GetLabel()); char Date[256]; Item->GetDateReceived()->Get(Date, sizeof(Date)); SetCtrlName(IDC_RECEIVED_DATE, Date); Item->GetDateSent()->Get(Date, sizeof(Date)); SetCtrlName(IDC_SENT_DATE, Date); TextLoaded = false; HtmlLoaded = false; auto TextContent = Item->GetBody(); auto HtmlContent = Item->GetHtml(); Sx = Sy = -1; LDocView *DocView = NULL; if (TabText && (DocView = Item->CreateView(this, sTextPlain, true, -1, !Edit))) { if (DocView == HtmlView) { // CreateView converted the text to HTML to embed Emojis. If we have // actual HTML content it'll overwrite the text portion, so we need // to move the HTML control to the text tab to leave room for actual HTML. DeleteObj(TextView); TextView = HtmlView; HtmlView = NULL; TextView->Detach(); TabText->Append(TextView); TextLoaded = true; HtmlLoaded = false; } if (!TextView && Edit) { // What the? Force creation of control... LAssert(!"Must have an edit control."); LDocView *Dv = App->CreateTextControl(IDC_TEXT_VIEW, sTextPlain, Edit, GetItem()); if (Dv) SetDoc(Dv, sTextPlain); LAssert(TextView != NULL); } if (TextView) { TextView->Visible(true); // This needs to be below the resize of the control so that // any wrapping has already been done and thus the scroll // bar is laid out already. if (Item->Cursor > 0) TextView->SetCaret(Item->Cursor, false); TabText->GetCss(true)->FontBold(ValidStr(TextContent)); TabText->OnStyleChange(); } } bool ValidHtml = ValidStr(HtmlContent); if (TabHtml && Item->CreateView(this, sTextHtml, true, -1, !Edit) && HtmlView) { HtmlView->Visible(true); if (Item->Cursor > 0) HtmlView->SetCaret(Item->Cursor, false); TabHtml->GetCss(true)->FontBold(ValidHtml); TabHtml->OnStyleChange(); } LVariant DefTab; Item->App->GetOptions()->GetValue(Edit ? OPT_EditControl : OPT_DefaultAlternative, DefTab); CurrentEditCtrl = (Edit || ValidHtml) && DefTab.CastInt32(); Tab->Value(CurrentEditCtrl); HtmlCtrlDirty = !ValidHtml; TextCtrlDirty = !ValidStr(TextContent); if (CalendarPanel) { auto CalEvents = GetItem()->GetCalendarAttachments(); CalendarPanel->Open(CalEvents.Length() > 0); } OnPosChange(); if (Attachments) { Attachments->RemoveAll(); List Files; if (Item->GetAttachments(&Files)) { for (auto a: Files) Attachments->Insert(a); } OnAttachmentsChange(); } if (Header) { LAutoString Utf((char*)LNewConvertCp("utf-8", Item->GetInternetHeader(), "iso-8859-1")); Header->Name(Utf); Header->SetEnv(Item); } bool Update = (Item->GetFlags() & MAIL_READ) == 0 && (Item->GetFlags() & MAIL_CREATED) == 0; if (Update) { Item->SetFlags(Item->GetFlags() | MAIL_READ); } if (Commands.Toolbar) { int p = Item->GetPriority(); Commands.Toolbar->SetCtrlValue(IDM_HIGH_PRIORITY, p < MAIL_PRIORITY_NORMAL); Commands.Toolbar->SetCtrlValue(IDM_LOW_PRIORITY, p > MAIL_PRIORITY_NORMAL); Commands.Toolbar->SetCtrlValue(IDM_READ_RECEIPT, TestFlag(Item->GetFlags(), MAIL_READ_RECEIPT)); } } if (Item->GetFlags() & (MAIL_CREATED | MAIL_BOUNCE)) { if (Entry) Entry->Focus(true); } else { if (TextView) TextView->Focus(true); } if (BtnPrev && BtnNext) { if (Item && Container) { /* int Items = Item->GetList()->Length(); int i = Item->GetList()->IndexOf(Item); */ auto Items = Container->Length(); auto i = Container->IndexOf(Item); BtnPrev->Enabled(i < (ssize_t)Items - 1); BtnNext->Enabled(i > 0); } else { BtnPrev->Enabled(false); BtnNext->Enabled(false); } } if (BtnSend) BtnSend->Enabled(!ReadOnly); if (BtnSave) BtnSave->Enabled(!ReadOnly); if (BtnSaveClose) BtnSaveClose->Enabled(!ReadOnly); if (BtnAttach) BtnAttach->Enabled(!ReadOnly); if (BtnReply) BtnReply->Enabled(ReadOnly); if (BtnReplyAll) BtnReplyAll->Enabled(ReadOnly); if (BtnForward) BtnForward->Enabled(true); if (BtnBounce) BtnBounce->Enabled(ReadOnly); if (Commands.Toolbar) { Commands.Toolbar->SetCtrlEnabled(IDM_HIGH_PRIORITY, Edit); Commands.Toolbar->SetCtrlEnabled(IDM_LOW_PRIORITY, Edit); Commands.Toolbar->SetCtrlEnabled(IDM_READ_RECEIPT, Edit); } _Running = true; } void MailUi::OnSave() { if (!GetItem()) return; if (GetItem()->GetFlags() & MAIL_SENT) { // Save a copy instead of over writing the original sent email Mail *Copy = new Mail(App, GetItem()->GetObject()->GetStore()->Create(MAGIC_MAIL)); if (Copy) { *Copy = *_Item; Copy->SetFlags(MAIL_READ | MAIL_CREATED, true); Copy->SetFolder(GetItem()->GetFolder()); Copy->SetDateSent(0); SetItem(Copy); } } Mail *Item = GetItem(); if (To) { To->OnSave(Item->GetObject()->GetStore(), Item->GetTo()); } LMailStore *AccountMailStore = NULL; if (FromCbo && Item->GetFrom() && FromAccountId.Length() > 0) { int64 CboVal = FromCbo->Value(); LAssert(CboVal < (ssize_t)FromAccountId.Length()); int AccountId = FromAccountId[(int)CboVal]; LDataPropI *Frm = Item->GetFrom(); if (AccountId < 0) { // From is a literal address, not an account ID. This can happen when bouncing email. Mailto mt(App, FromCbo->Name()); if (mt.To.Length() == 1) { AddressDescriptor *a = mt.To[0]; if (a) { Frm->SetStr(FIELD_NAME, a->sName); Frm->SetStr(FIELD_EMAIL, a->sAddr); } } else LAssert(0); } else if (AccountId > 0) { ScribeAccount *a = Item->App->GetAccountById(AccountId); if (a) { Frm->SetStr(FIELD_NAME, a->Identity.Name().Str()); Frm->SetStr(FIELD_EMAIL, a->Identity.Email().Str()); } else LAssert(!"From account missing."); // Find the associated mail store for this account. Hopefully we can put any new // mail into the mail store that the account is using. // // Check for IMAP mail store? AccountMailStore = a->Receive.GetMailStore(); if (!AccountMailStore) { // Nope... what about a receive path? LVariant DestFolder = a->Receive.DestinationFolder(); if (ValidStr(DestFolder.Str())) { AccountMailStore = Item->App->GetMailStoreForPath(DestFolder.Str()); } } } else LAssert(!"No account id."); } LDataPropI *ReplyObj = Item->GetReply(); if (ReplyToCbo != NULL && ReplyObj != NULL && ReplyToChk != NULL && ReplyToChk->Value()) { Mailto mt(App, ReplyToCbo->Name()); if (mt.To.Length() == 1) { AddressDescriptor *a = mt.To[0]; if (a && a->sAddr) { if (a->sName) ReplyObj->SetStr(FIELD_NAME, a->sName); ReplyObj->SetStr(FIELD_EMAIL, a->sAddr); } } } Item->SetSubject(Subject->Name()); Item->SetLabel(GetCtrlName(IDC_LABEL)); auto c32 = GetCtrlValue(IDC_COLOUR); Item->SetMarkColour(c32); LDocView *Ctrl = CurrentEditCtrl ? HtmlView : TextView; if (Ctrl) { // Delete all existing data... Item->SetBody(0); Item->SetBodyCharset(0); Item->SetHtml(0); Item->SetHtmlCharset(0); const char *MimeType = Ctrl->GetMimeType(); const char *Charset = Ctrl->GetCharset(); if (!_stricmp(MimeType, sTextHtml)) { LArray Media; // Set the HTML part LString HtmlFormat; if (!Ctrl->GetFormattedContent("text/html", HtmlFormat, &Media)) HtmlFormat = Ctrl->Name(); Item->SetHtml(HtmlFormat); Item->SetHtmlCharset(Charset); // Also set a text version for the alternate LString TxtFormat; if (!Ctrl->GetFormattedContent(sTextPlain, TxtFormat)) { TxtFormat = HtmlToText(Item->GetHtml(), Charset); } if (TxtFormat) { Item->SetBody(TxtFormat); Item->SetBodyCharset(Charset); } auto Obj = Item->GetObject(); // This clears any existing multipart/related objects... Obj->SetObj(FIELD_HTML_RELATED, NULL); if (Media.Length() > 0) { // Make a table of existing attachments so that we don't duplicate // these new ones. LArray Objs; LHashTbl,LDataI*> Map; if (GetItem()->GetAttachmentObjs(Objs)) { for (auto i : Objs) { auto Cid = i->GetStr(FIELD_CONTENT_ID); if (Cid) Map.Add(Cid, i); } } // If there are media attachments, splice them into the MIME tree. // This should go after setting the text part so that the right // MIME alternative structure is generated. auto Store = Obj->GetStore(); for (auto &Cm : Media) { LDataI *a = Store->Create(MAGIC_ATTACHMENT); if (a) { LAssert(Cm.Valid()); auto Existing = Map.Find(Cm.Id); if (Existing) { // Delete the existing attachment Thing *t = CastThing(Existing); Attachment *a = t ? t->IsAttachment() : NULL; if (a) { // Delete both the Attachment and it's store object... auto it = GetItem(); it->DeleteAttachment(a); } else { // There is Attachment object for the LDataI.... but we can // still delete it from the store. LArray del; del.Add(Existing); Existing->GetStore()->Delete(del, false); } } LgiTrace("Adding related: %s %s " LPrintfInt64 "\n", Cm.FileName.Get(), Cm.MimeType.Get(), Cm.Stream->GetSize()); a->SetStr(FIELD_CONTENT_ID, Cm.Id); a->SetStr(FIELD_NAME, Cm.FileName); a->SetStr(FIELD_MIME_TYPE, Cm.MimeType); a->SetStream(Cm.Stream); Obj->SetObj(FIELD_HTML_RELATED, a); } } } } else { auto Text = Ctrl->Name(); Item->SetBody(Text); Item->SetBodyCharset(Charset); } } #if SAVE_HEADERS char *Headers = Header ? Header->Name() : 0; if (Headers) { DeleteArray(Item->InternetHeader); Item->InternetHeader = NewStr(Headers); } #endif Item->GetMessageId(true); Item->CreateMailHeaders(); Item->Update(); ScribeFolder *Folder = Item->GetFolder(); // Now get the associated outbox for this mail ScribeFolder *Outbox = Item->App->GetFolder(FOLDER_OUTBOX, AccountMailStore); auto Fld = Folder ? Folder : Outbox; LAssert(Fld != NULL); bool Status = Fld ? Item->Save(Fld) : false; if (Status) { LArray c; c.Add(Item->GetObject()); Item->App->SetContext(_FL); Item->App->OnChange(c, 0); } } bool MailUi::AddRecipient(AddressDescriptor *Addr) { if (Addr && To) { ListAddr *La = dynamic_cast(Addr); if (La) { To->Insert(La); return true; } } return false; } bool MailUi::AddRecipient(Contact *c) { ListAddr *La = new ListAddr(c); if (La) { To->Insert(La); return true; } return false; } bool MailUi::AddRecipient(const char *Email, const char *Name) { ListAddr *La = new ListAddr(App, Email, Name); if (La) { To->Insert(La); return true; } return false; } bool MailUi::SeekMsg(int delta) { bool Status = false; if (Container) { Mail *Item = GetItem(); auto Index = Container->IndexOf(Item); Mail *Next = Index >= 0 ? (*Container)[Index + delta] : 0; if (Next) { SetDirty(false); // called OnSave() if necessary _Running = false; if (Item->Ui) { // close any existing user interface if (Item->Ui != this) { Item->Ui->Quit(); } else { Item->Ui = 0; } } if (Header) Header->SetEnv(0); Caps.Empty(); SetItem(Next); Item = GetItem(); // select this item in the list if (Item->GetList()) { Item->GetList()->Select(Item); Item->ScrollTo(); } Status = true; OnLoad(); _Running = true; } } return Status; } int MailUi::HandleCmd(int Cmd) { switch (Cmd) { case IDM_READ_RECEIPT: { if (Commands.Toolbar) { int f = GetItem()->GetFlags(); if (Commands.Toolbar->GetCtrlValue(IDM_READ_RECEIPT)) { SetFlag(f, MAIL_READ_RECEIPT); } else { ClearFlag(f, MAIL_READ_RECEIPT); } GetItem()->SetFlags(f); } break; } case IDM_HIGH_PRIORITY: { if (Commands.Toolbar) { GetItem()->SetPriority(Commands.Toolbar->GetCtrlValue(IDM_HIGH_PRIORITY) ? MAIL_PRIORITY_HIGH : MAIL_PRIORITY_NORMAL); SetDirty(true); Commands.Toolbar->SetCtrlValue(IDM_HIGH_PRIORITY, GetItem()->GetPriority() < MAIL_PRIORITY_NORMAL); Commands.Toolbar->SetCtrlValue(IDM_LOW_PRIORITY, GetItem()->GetPriority() > MAIL_PRIORITY_NORMAL); } break; } case IDM_LOW_PRIORITY: { if (Commands.Toolbar) { GetItem()->SetPriority(Commands.Toolbar->GetCtrlValue(IDM_LOW_PRIORITY) ? MAIL_PRIORITY_LOW : MAIL_PRIORITY_NORMAL); SetDirty(true); Commands.Toolbar->SetCtrlValue(IDM_HIGH_PRIORITY, GetItem()->GetPriority() < MAIL_PRIORITY_NORMAL); Commands.Toolbar->SetCtrlValue(IDM_LOW_PRIORITY, GetItem()->GetPriority() > MAIL_PRIORITY_NORMAL); } break; } case IDM_PREV_MSG: { SeekMsg(1); break; } case IDM_NEXT_MSG: { SeekMsg(-1); break; } case IDM_SEND_MSG: { bool Working = IsWorking(); if (Working) { SetCmdAfterResize(Cmd); break; } if (!GetItem() || !App) { LAssert(!"Missing item or window ptr."); break; } // Normal save bool IsInPublicFolder = GetItem()->GetFolder() && GetItem()->GetFolder()->IsPublicFolders(); if (IsInPublicFolder) { auto i = GetItem(); LDateTime n; n.SetNow(); i->SetDateSent(&n); i->Update(); } OnDataEntered(); OnSave(); SetDirty(false, false); GetItem()->Send(true); Quit(); return 0; } case IDM_DELETE_MSG: { LVariant ConfirmDelete, DelDirection; App->GetOptions()->GetValue(OPT_ConfirmDelete, ConfirmDelete); App->GetOptions()->GetValue(OPT_DelDirection, DelDirection); int WinAction = DelDirection.CastInt32() - 1; // -1 == Next, 0 == Close, 1 == Prev if (!ConfirmDelete.CastInt32() || LgiMsg(this, LLoadString(IDS_DELETE_ASK), AppName, MB_YESNO) == IDYES) { Mail *Del = GetItem()->GetObject() ? GetItem() : 0; if (Del) { if (!Del->GetObject()->IsOnDisk()) Del = 0; } if (!WinAction || !SeekMsg(WinAction)) { SetItem(0); PostEvent(M_CLOSE); } if (Del && Del->GetObject()) { Del->OnDelete(); } } break; } case IDM_DELETE_AS_SPAM: { LVariant DelDirection; App->GetOptions()->GetValue(OPT_DelDirection, DelDirection); int WinAction = DelDirection.CastInt32() - 1; // -1 == Next, 0 == Close, 1 == Prev if (GetItem()) { Mail *Del = GetItem()->GetObject() ? GetItem() : 0; if (!WinAction || !SeekMsg(WinAction)) { SetItem(0); PostEvent(M_CLOSE); } if (Del) { Del->DeleteAsSpam(this); } } break; } case IDM_SAVE: { OnDataEntered(); SetDirty(false, false); break; } case IDM_SAVE_CLOSE: { bool Working = IsWorking(); if (Working) { SetCmdAfterResize(Cmd); break; } OnDataEntered(); SetDirty(false, false); // fall thru } case IDM_CLOSE: { Quit(); return 0; } case IDM_REPLY: case IDM_REPLY_ALL: { App->MailReplyTo(GetItem(), Cmd == IDM_REPLY_ALL); SetDirty(false); PostEvent(M_CLOSE); break; } case IDM_FORWARD: { App->MailForward(GetItem()); if (IsDirty()) OnSave(); PostEvent(M_CLOSE); break; } case IDM_BOUNCE: { App->MailBounce(GetItem()); OnSave(); PostEvent(M_CLOSE); break; } case IDM_PRINT: { if (GetItem() && App) { if (IsDirty()) { OnDataEntered(); OnSave(); } App->ThingPrint(NULL, GetItem(), 0, this); } break; } case IDM_ATTACH_FILE: { auto Select = new LFileSelect(this); Select->MultiSelect(true); Select->Type("All files", LGI_ALL_FILES); Select->Open([this](auto dlg, auto status) { if (status) { Mail *m = GetItem(); if (m) { for (size_t i=0; iLength(); i++) { char File[MAX_PATH_LEN]; if (!LResolveShortcut((*dlg)[i], File, sizeof(File))) { strcpy_s(File, sizeof(File), (*dlg)[i]); } Attachment *a = m->AttachFile(this, File); if (a && Attachments) { Attachments->Insert(a); Attachments->ResizeColumnsToContent(); } } } } delete dlg; }); break; } default: { if (Commands.ExecuteCallbacks(App, this, GetItem(), Cmd)) return true; return false; } } return true; } int MailUi::OnCommand(int Cmd, int Event, OsView From) { if (GpgUi) { GpgUi->DoCommand(Cmd, [this, Cmd](auto r) { if (!r) HandleCmd(Cmd); }); } else { HandleCmd(Cmd); } return LWindow::OnCommand(Cmd, Event, From); } LArray Mail::GetCalendarAttachments() { List Attachments; if (!GetAttachments(&Attachments)) return false; LArray Cal; for (auto a: Attachments) { LString Mt = a->GetMimeType(); if (Mt.Equals("text/calendar")) Cal.Add(a); } return Cal; } bool MailUi::AddCalendarEvent(bool AddPopupReminder) { LString Msg; auto Result = GetItem()->AddCalendarEvent(this, AddPopupReminder, &Msg); auto css = CalPanelStatus->GetCss(true); if (Result) css->Color(LCss::ColorInherit); else css->Color(LColour::Red); CalPanelStatus->Name(Msg); return Result; } bool Mail::AddCalendarEvent(LViewI *Parent, bool AddPopupReminder, LString *Msg) { LString Err, s; auto Cal = GetCalendarAttachments(); int NewEvents = 0, DupeEvents = 0, Processed = 0, Cancelled = 0, NotMatched = 0; ScribeFolder *Folder = NULL; if (Cal.Length() == 0) { Err = "There are no attached events to add."; goto OnError; } Folder = App->GetFolder(FOLDER_CALENDAR); if (!Folder) { Err = "There no calendar folder to save to."; goto OnError; } for (auto a: Cal) { auto Event = App->CreateThingOfType(MAGIC_CALENDAR); if (Event) { LString Mt = a->GetMimeType(); LAutoPtr Data(a->GotoObject(_FL)); if (Data) { if (Event->Import(AutoCast(Data), Mt)) { auto c = Event->IsCalendar(); auto obj = c ? c->GetObject() : NULL; if (!obj) continue; if (AddPopupReminder) { LString s; s.Printf("%g,%i,%i,", 10.0, CalMinutes, CalPopup); obj->SetStr(FIELD_CAL_REMINDERS, s); } // Is it a cancellation? auto Status = obj->GetStr(FIELD_CAL_STATUS); auto IsCancel = Stristr(Status, "CANCELLED") != NULL; if (!IsCancel) { // Does the folder already have a copy of this event? bool AlreadyAdded = false; for (auto t: Folder->Items) { auto Obj = t->IsCalendar(); if (Obj && *Obj == *c) { AlreadyAdded = true; break; } } if (AlreadyAdded) { DupeEvents++; } else { // Write the event to the folder auto Status = Folder->WriteThing(Event); if (Status > Store3Error) { NewEvents++; Event = NULL; } } } else { // Cancellation processing auto Uid = obj->GetStr(FIELD_UID); Thing *Match = NULL; for (auto t: Folder->Items) { auto tCal = t->IsCalendar(); if (tCal && tCal->GetObject()) { auto tUid = tCal->GetObject()->GetStr(FIELD_UID); if (!Stricmp(Uid, tUid)) { Match = t; break; } } } if (Match) { if (!Parent || LgiMsg(Parent, "Delete cancelled event?", "Calendar", MB_YESNO) == IDYES) { auto f = Match->GetFolder(); LArray items; items.Add(Match); f->Delete(items, true); Cancelled++; } } else NotMatched++; } } else LgiTrace("%s:%i - vCal event import failed.\n", _FL); } else LgiTrace("%s:%i - GotoObject failed.\n", _FL); if (Event) Event->DecRef(); } else LgiTrace("%s:%i - CreateThingOfType failed.\n", _FL); } Processed = NewEvents + DupeEvents; if (Processed != Cal.Length()) { Err.Printf("There were errors processing %i events, check the console.", (int)Cal.Length() - Processed); goto OnError; } if (NewEvents || DupeEvents) s.Printf("%i new events, %i duplicates.", NewEvents, DupeEvents); else s.Printf("%i events cancelled, %i not matched.", Cancelled, NotMatched); if (Msg) *Msg = s; if (Processed > 0) { for (auto v: CalendarView::CalendarViews) v->OnContentsChanged(); } return true; OnError: if (Msg) *Msg = Err; return false; } int MailUi::OnNotify(LViewI *Col, LNotification n) { if (dynamic_cast(Col)) { Sx = Sy = -1; OnPosChange(); return 0; } if (GpgUi) { if (n.Type == LNotifyItemDelete && Col == (LViewI*)GpgUi) { GpgUi = NULL; } else { int r = GpgUi->OnNotify(Col, n); if (r) return r; } } int CtrlId = Col->GetId(); switch (CtrlId) { case IDC_MAIL_UI_TABS: { Mail *Item = GetItem(); if (!Item) break; // bool Edit = TestFlag(Item->GetFlags(), MAIL_CREATED); if (n.Type == LNotifyValueChanged) { switch (Col->Value()) { case 0: // Text tab { if (!TextLoaded) { Item->CreateView(this, sTextPlain, true, -1); OnPosChange(); } if (CurrentEditCtrl == 1) { if (HtmlView && TextView && TextCtrlDirty) { // Convert HTML to Text here... TextCtrlDirty = false; auto Html = HtmlView->Name(); if (Html) { LString Txt = HtmlToText(Html, HtmlView->GetCharset()); if (Txt) TextView->Name(Txt); } } CurrentEditCtrl = 0; } break; } case 1: // Html tab { if (!HtmlLoaded) { Item->CreateView(this, sTextHtml, true, -1); OnPosChange(); } if (CurrentEditCtrl == 0) { if (HtmlView && TextView && HtmlCtrlDirty) { // Convert Text to HTML here... HtmlCtrlDirty = false; auto Text = TextView->Name(); if (Text) { LString Html = TextToHtml(Text, TextView->GetCharset()); if (Html) HtmlView->Name(Html); } } CurrentEditCtrl = 1; } break; } default: // Do nothing on other tabs.. break; } } else if (n.Type == LNotifyItemClick) { LMouse m; if (!Col->GetMouse(m)) break; int TabIdx = Tab->HitTest(m); if (TabIdx == 0 || TabIdx == 1) { if (Item && m.IsContextMenu()) { LSubMenu s; s.AppendItem(LLoadString(IDS_DELETE), IDM_DELETE); m.ToScreen(); int Cmd = s.Float(this, m.x, m.y, false); if (Cmd == IDM_DELETE) { if (TabIdx == 0) // Txt { Item->SetBody(NULL); Item->SetBodyCharset(NULL); TextView->Name(NULL); if (TabText) { TabText->GetCss(true)->FontBold(false); TabText->OnStyleChange(); } TextCtrlDirty = false; } else // HTML { Item->SetHtml(NULL); Item->SetHtmlCharset(NULL); HtmlView->Name(NULL); if (TabHtml) { TabHtml->GetCss(true)->FontBold(false); TabHtml->OnStyleChange(); } HtmlCtrlDirty = false; } } } } } break; } case IDC_FROM: { if (_Running && FromCbo && n.Type == LNotifyValueChanged) SetDirty(true); break; } case IDC_SHOW_FROM: { LVariant Show = Col->Value();; App->GetOptions()->SetValue(OPT_MailShowFrom, Show); break; } case IDC_LAUNCH_HTML: { char File[MAX_PATH_LEN]; if (GetItem() && GetItem()->WriteAlternateHtml(File)) { LExecute(File); } break; } case IDC_ENTRY: { if (Entry) { if (ValidStr(Entry->Name())) { if (!Browse) { Browse = new AddressBrowse(App, Entry, To, SetTo); } } if (Browse) { Browse->OnNotify(Entry, n); } if (n.Type == LNotifyReturnKey) { OnDataEntered(); } } break; } case IDC_SET_TO: { if (SetTo) { AddMode = (int)SetTo->Value(); } break; } case IDC_SEND: { OnCommand(IDM_SEND_MSG, 0, #if LGI_VIEW_HANDLE Col->Handle() #else (OsView)NULL #endif ); break; } #if SAVE_HEADERS case IDC_INTERNET_HEADER: #endif case IDC_TEXT_VIEW: case IDC_HTML_VIEW: { Mail *Item = GetItem(); if (!Item) break; bool Edit = TestFlag(Item->GetFlags(), MAIL_CREATED); if ( ( n.Type == LNotifyDocChanged || n.Type == LNotifyCharsetChanged || n.Type == LNotifyFixedWidthChanged || (!IgnoreShowImgNotify && n.Type == LNotifyShowImagesChanged) ) && _Running ) { if (GetItem()) GetItem()->OnNotify(Col, n); SetDirty(true); if (Edit) { if (CtrlId == IDC_TEXT_VIEW) { CurrentEditCtrl = 0; HtmlCtrlDirty = true; TabText->GetCss(true)->FontBold(true); TabText->OnStyleChange(); } else if (CtrlId == IDC_HTML_VIEW) { CurrentEditCtrl = 1; TextCtrlDirty = true; TabHtml->GetCss(true)->FontBold(true); TabHtml->OnStyleChange(); } // LgiTrace("%s:%i - OnNotify: TextLoaded=%i, HtmlLoaded=%i\n", _FL, TextLoaded, HtmlLoaded); } } break; } case IDC_ATTACHMENTS: { if (n.Type == LNotifyItemInsert || n.Type == LNotifyItemDelete) { // fall thru } else { break; } } case IDC_TO: { if ( _Running && ( n.Type == LNotifyItemInsert || n.Type == LNotifyItemDelete || n.Type == LNotifyItemChange ) ) { SetDirty(true); } break; } case IDC_COLOUR: { if (_Running && GetItem()) { MetaFieldsDirty = true; OnDirty(true); } break; } case IDC_LABEL: { if (_Running) { MetaFieldsDirty = true; OnDirty(true); } break; } case IDC_SUBJECT: { if (_Running) { SetDirty(true); } break; } case IDCANCEL: { if (CmdAfterResize) { int Cmd = CmdAfterResize; CmdAfterResize = 0; IsWorking(false); OnCommand(Cmd, 0, NULL); } break; } case IDC_ADD_CAL_EVENT: { AddCalendarEvent(false); break; } case IDC_ADD_CAL_EVENT_POPUP: { AddCalendarEvent(true); break; } } return 0; } void MailUi::OnDataEntered() { auto Name = Entry->Name(); if (ValidStr(Name)) { List New; // Decode the entries Mailto mt(App, Name); New = mt.To; mt.To.Empty(); if (mt.Subject && !ValidStr(GetCtrlName(IDC_SUBJECT))) { SetCtrlName(IDC_SUBJECT, mt.Subject); } // Add the new entries List Cache; App->GetContacts(Cache); AddressDescriptor *ad; while ((ad = New[0])) { New.Delete(ad); ListAddr *t = dynamic_cast(ad); if (t) { t->CC = (EmailAddressType)GetCtrlValue(IDC_SET_TO); t->OnFind(&Cache); To->Insert(t, 0); } else { DeleteObj(ad); } } // Clear the entry box for the next one Entry->Name(""); Entry->Select(-1, -1); } } void MailUi::OnPosChange() { LWindow::OnPosChange(); if (Tab && (Sx != X() || Sy != Y())) { Sx = X(); Sy = Y(); LRect r = Tab->GetCurrent()->GetClient(); r.Inset(CONTENT_BORDER, CONTENT_BORDER); if (TextView) TextView->SetPos(r, true); if (HtmlView) HtmlView->SetPos(r, true); if (Attachments) Attachments->SetPos(r, true); if (Header) Header->SetPos(r, true); } } void MailUi::OnPaint(LSurface *pDC) { LCssTools Tools(this); Tools.PaintContent(pDC, GetClient()); } void MailUi::OnPulse() { if (IsDirty() && TextView && GetItem()) { // Ui -> Object OnSave(); // Object -> Disk GetItem()->Save(0); } else { SetPulse(); } } void MailUi::OnDirty(bool Dirty) { SetCtrlEnabled(IDM_SAVE, Dirty); SetCtrlEnabled(IDM_SAVE_CLOSE, Dirty); if (Dirty) { SetPulse(60 * 1000); // every minute } else { SetPulse(); } } bool MailUi::CallMethod(const char *Name, LScriptArguments &Args) { ScribeDomType Method = StrToDom(Name); *Args.GetReturn() = false; switch (Method) { case SdShowRemoteContent: // Type: () if (HtmlView) { bool Always = Args.Length() > 0 ? Args[0]->CastBool() : false; if (Always && GetItem()) { auto From = GetItem()->GetFrom(); if (From) App->RemoteContent_AddSender(From->GetStr(FIELD_EMAIL), true); else LgiTrace("%s:%i - No from address.\n", _FL); } IgnoreShowImgNotify = true; HtmlView->SetLoadImages(true); IgnoreShowImgNotify = false; PostEvent(M_UPDATE); *Args.GetReturn() = true; } break; case SdSetHtml: // Type: (String Html) if (HtmlView) { if (Args.Length() > 0) { HtmlView->Name(Args[0]->Str()); *Args.GetReturn() = true; } } break; default: return false; } return true; } LMessage::Result MailUi::OnEvent(LMessage *Msg) { switch (Msg->Msg()) { case M_SET_HTML: { LAutoPtr s((LString*)Msg->A()); if (s && HtmlView) HtmlView->Name(*s); break; } case M_NEEDS_CAP: { LAutoString c((char*)Msg->A()); NeedsCapability(c); return 0; } case M_RESIZE_IMAGE: { LAutoPtr Job((ImageResizeThread::Job*)Msg->A()); if (Job && GetItem()) { // Find the right attachment... List Attachments; if (GetItem()->GetAttachments(&Attachments)) { Attachment *Match = NULL; for (auto a: Attachments) { LString Nm = a->GetName(); if (Nm.Equals(Job->FileName)) { Match = a; break; } } if (Match) { if (Job->Data) Match->Set(Job->Data); auto Mt = Match->GetMimeType(); if (_stricmp(Mt, "image/jpeg")) { auto Name = Match->GetName(); char *Ext = LGetExtension(Name); if (Ext) *Ext = 0; LString NewName = Name; NewName += "jpg"; Match->SetName(NewName); Match->SetMimeType("image/jpeg"); } Match->SetIsResizing(false); LDataI *AttachPoint = GetItem()->GetFileAttachPoint(); if (AttachPoint) { // Do final save to the mail store... Match->GetObject()->Save(AttachPoint); } else { LAssert(0); Match->DecRef(); Match = NULL; } if (CmdAfterResize) { bool Working = IsWorking(); if (!Working) { // All resizing work is done... OnCommand(CmdAfterResize, 0, NULL); return 0; } } } else LAssert(!"No matching attachment image to store resized image in."); } } break; } #if WINNATIVE case WM_COMMAND: { LAssert((NativeInt)Commands.Toolbar != 0xdddddddd); if (Commands.Toolbar && Commands.Toolbar->Handle() == (HWND)Msg->b) { return LWindow::OnEvent(Msg); } break; } #endif } return LWindow::OnEvent(Msg); } void MailUi::OnReceiveFiles(LArray &Files) { List Att; GetItem()->GetAttachments(&Att); for (unsigned i=0; iGetName(), f) == 0) { int Result = LgiMsg(this, LLoadString(IDS_ATTACH_WARNING_DLG), LLoadString(IDS_ATTACHMENTS), MB_YESNO); if (Result == IDNO) { Add = false; break; } } } if (Add) { Attachment *a = GetItem()->AttachFile(this, Path); if (a) { Attachments->Insert(a); Attachments->ResizeColumnsToContent(); } } } } ////////////////////////////////////////////////////////////////////////////// bool Mail::PreviewLines = false; bool Mail::RunMailPipes = true; List Mail::NewMailLst; bool Mail::AdjustDateTz = true; LHashTbl,Mail*> Mail::MessageIdMap; Mail::Mail(ScribeWnd *app, LDataI *object) : Thing(app, object) { DefaultObject(object); d = new MailPrivate(this); _New(); } Mail::~Mail() { if (GetObject()) { auto Id = GetObject()->GetStr(FIELD_MESSAGE_ID); if (Id) MessageIdMap.Delete(Id); } NewMailLst.Delete(this); _Delete(); DeleteObj(d); } void Mail::_New() { SendAttempts = 0; Container = 0; FlagsCache = -1; PreviewCacheX = 0; Cursor = 0; ParentFile = 0; PreviousMail = 0; NewEmail = NewEmailNone; Ui = 0; TotalSizeCache = -1; } void Mail::_Delete() { if (ParentFile) { ParentFile->SetMsg(0); } UnloadAttachments(); PreviewCache.DeleteObjects(); DeleteObj(Container); if (Ui) Ui->PostEvent(M_CLOSE); if (Ui) Ui->SetItem(0); } bool Mail::AppendItems(LSubMenu *Menu, const char *Param, int Base) { auto Remove = Menu->AppendSub(LLoadString(IDS_REMOVE)); if (Remove) { Remove->AppendItem("'>'", IDM_REMOVE_GRTH, true); Remove->AppendItem("'> '", IDM_REMOVE_GRTH_SP, true); Remove->AppendItem(LLoadString(IDS_HTML_TAGS), IDM_REMOVE_HTML, true); } auto FilterMenu = Menu->AppendSub(LLoadString(IDS_FILTER)); if (FilterMenu) { FilterMenu->SetImageList(App->GetIconImgList(), false); Actions.Empty(); AddActions(FilterMenu, Actions, App->GetThingSources(MAGIC_FILTER)); } auto Convert = Menu->AppendSub(LLoadString(IDS_CONVERT_SELECTION)); if (Convert) { Convert->AppendItem("To Base64", IDM_CONVERT_BIN_TO_B64, true); Convert->AppendItem("To Binary", IDM_CONVERT_B64_TO_BIN, true); } if (!TestFlag(GetFlags(), MAIL_CREATED)) { auto Charset = Menu->AppendSub(LLoadString(L_CHANGE_CHARSET)); if (Charset) { int n=0; for (LCharset *c = LGetCsList(); c->Charset; c++, n++) Charset->AppendItem(c->Charset, IDM_CHARSET_BASE + n, c->IsAvailable()); } } return true; } bool Mail::OnMenu(LDocView *View, int Id, void *Context) { const char *RemoveStr = 0; switch (Id) { case IDM_REMOVE_GRTH: { RemoveStr = ">"; break; } case IDM_REMOVE_GRTH_SP: { RemoveStr = "> "; break; } case IDM_REMOVE_HTML: { auto s = View ? View->Name() : 0; if (s) { auto n = DeHtml(s); if (n) { View->Name(n); DeleteArray(n); } } break; } default: { if (Id >= IDM_CHARSET_BASE) { int n=0; LCharset *c; for (c = LGetCsList(); c->Charset; c++, n++) { if (Id - IDM_CHARSET_BASE == n) { break; } } if (c->Charset) { SetBodyCharset((char*)c->Charset); SetDirty(); if (GetBodyCharset() && Ui) { Ui->OnLoad(); } } } else if (Id >= IDM_FILTER_BASE) { Filter *Action = Actions[Id-IDM_FILTER_BASE]; if (Action) { // Save the message... SetDirty(false); // Hide the mail window... if (Ui) Ui->Visible(false); // Do the action bool Stop; Mail *This = this; Action->DoActions(This, Stop); if (This != this) break; if (Ui) DeleteObj(Ui); return true; } } break; } #ifdef _DEBUG case IDM_CONVERT_BIN_TO_B64: { char *t = View->GetSelection(); if (t) { size_t In = strlen(t); size_t Len = BufferLen_BinTo64(In); char *B64 = new char[Len+1]; if (B64) { ConvertBinaryToBase64(B64, Len, (uchar*)t, In); B64[Len] = 0; char Temp[256]; ConvertBase64ToBinary((uchar*)Temp, sizeof(Temp), B64, Len); char16 *Str = Utf8ToWide(B64); if (Str) { LTextView3 *Tv = dynamic_cast(View); if (Tv) { Tv->DeleteSelection(); Tv->Insert(Tv->GetCaret(), Str, Len); } DeleteArray(Str); } DeleteArray(B64); } } break; } case IDM_CONVERT_B64_TO_BIN: { char *t = View->GetSelection(); if (t) { size_t In = strlen(t); size_t Len = BufferLen_64ToBin(In); char *Bin = new char[Len+1]; if (Bin) { ssize_t Out = ConvertBase64ToBinary((uchar*)Bin, Len, t, In); Bin[Out] = 0; char16 *Str = Utf8ToWide(Bin, Out); if (Str) { LTextView3 *Tv = dynamic_cast(View); if (Tv) { Tv->DeleteSelection(); Tv->Insert(Tv->GetCaret(), Str, Out); } DeleteArray(Str); } DeleteArray(Bin); } } break; } #endif } if (RemoveStr) { size_t TokenLen = strlen(RemoveStr); auto s = View ? View->Name() : 0; if (s) { LMemQueue Temp; auto Start = s; const char *End; while (*Start) { // seek to EOL for (End = Start; *End && *End != '\n'; End++); End++; ssize_t Len = End - Start; if (_strnicmp(Start, RemoveStr, TokenLen) == 0) { Temp.Write((uchar*)Start + TokenLen, Len - TokenLen); } else { Temp.Write((uchar*)Start, Len); } Start = End; } int Size = (int)Temp.GetSize(); char *Buf = new char[Size+1]; if (Buf) { Temp.Read((uchar*) Buf, Size); Buf[Size] = 0; View->Name(Buf); DeleteArray(Buf); } } } return true; } LDocumentEnv::LoadType Mail::GetContent(LoadJob *&j) { if (!j) return LoadError; LUri Uri(j->Uri); if ( Uri.sProtocol && ( !_stricmp(Uri.sProtocol, "http") || !_stricmp(Uri.sProtocol, "https") || !_stricmp(Uri.sProtocol, "ftp") ) ) { // We don't check OPT_HtmlLoadImages here because it's done elsewhere: // - ScribeWnd::CreateTextControl calls LHtml::SetLoadImages with the value from OPT_HtmlLoadImages // - LTag::LoadImage checks LHtml::GetLoadImages // // If there is a remote job here, it's because it's probably whitelisted. if (!Worker) Worker = App->GetImageLoader(); if (!Worker) return LoadError; Worker->AddJob(j); j = 0; return LoadDeferred; } else if (Uri.sProtocol && !_stricmp(Uri.sProtocol, "file")) { if (!_strnicmp(Uri.sPath, "//", 2)) { // This seems to hang the windows CreateFile function... } else { // Is it a local file then? if (j->pDC.Reset(GdcD->Load(Uri.sPath))) { return LoadImmediate; } } } else { List Files; if (GetAttachments(&Files)) { auto Dir = FilePart(j->Uri); Attachment *a = NULL; for (auto It = Files.begin(); It != Files.end(); It++) { a = *It; if (_strnicmp(j->Uri, "cid:", 4) == 0) { char *ContentId = j->Uri + 4; auto AttachmentId = a->GetContentId(); if (AttachmentId) { if (AttachmentId[0] == '<') { auto s = AttachmentId + 1; auto e = strrchr(s, '>'); if (e) { ssize_t len = e - s; if (strlen(ContentId) == len && !strncmp(s, ContentId, len)) break; } } else if (!strcmp(AttachmentId, ContentId)) { break; } } } else { auto Name = a->GetName(); if (Name) { auto NameDir = FilePart(Name); if (_stricmp(NameDir, Dir) == 0) { break; } } } } if (a) { j->MimeType = a->GetMimeType(); j->ContentId = a->GetContentId(); if (j->Pref == LoadJob::FmtStream) { j->Filename = a->GetName(); j->Stream = a->GetObject()->GetStream(_FL); return LoadImmediate; } else { char *Tmp = ScribeTempPath(); if (Tmp) { auto File = a->GetName(); auto Ext = LGetExtension(File); char s[MAX_PATH_LEN] = ""; LString part; do { if (part.Printf("%x.%s", LRand(), Ext) < 0) return LoadError; if (!LMakePath(s, sizeof(s), Tmp, part)) return LoadError; } while (LFileExists(s)); if (a->SaveTo(s, true)) { if (j->Pref == LoadJob::FmtFilename) { j->Filename = s; return LoadImmediate; } else { int Promote = GdcD->SetOption(GDC_PROMOTE_ON_LOAD, 0); j->pDC.Reset(GdcD->Load(s)); j->Filename = a->GetName(); GdcD->SetOption(GDC_PROMOTE_ON_LOAD, Promote); FileDev->Delete(s, false); return LoadImmediate; } } } } } } } return LoadError; } class MailCapabilities : public LLayout { LArray MissingCaps; LDocView *Doc; LButton *Install; public: MailCapabilities(LDocView *d) { Doc = d; Install = 0; } const char *GetClass() { return "MailCapabilities"; } void OnPosChange() { LRect c = GetClient(); if (MissingCaps.Length()) { if (!Install) { if ((Install = new LButton(IDOK, 0, 0, -1, -1, "Install"))) Install->Attach(this); } LRect r = c; r.x1 = r.x2 - Install->X(); r.y2 -= 7; Install->SetPos(r); } } void OnPaint(LSurface *pDC) { LRect cli = GetClient(); if (MissingCaps.Length()) { char Msg[256]; int c = sprintf_s(Msg, sizeof(Msg), "This content requires "); for (unsigned i=0; iTransparent(false); LSysFont->Colour(L_TEXT, L_MED); ds.Draw(pDC, cli.x1, cli.y1, &cli); } else { pDC->Colour(L_MED); pDC->Rectangle(); } } }; LDocView *Mail::CreateView( MailViewOwner *Owner, LString MimeType, bool Sunken, size_t MaxBytes, bool NoEdit) { bool Created = TestFlag(GetFlags(), MAIL_CREATED); bool Edit = NoEdit ? false : Created; bool ReadOnly = !Created; bool DisabledLook = false; LString TextMem; LVariant DefAlt; App->GetOptions()->GetValue(OPT_DefaultAlternative, DefAlt); auto TextBody = GetBody(); auto TextCharset = GetBodyCharset(); auto HtmlBody = GetHtml(); auto HtmlCharset = GetHtmlCharset(); const char *CtrlType = NULL; if (!MimeType) { bool TextValid = TextBody != NULL; bool HtmlValid = HtmlBody != NULL; if (TextValid && HtmlValid) MimeType = DefAlt.CastInt32() ? sTextHtml : sTextPlain; else if (TextValid) MimeType = sTextPlain; else if (HtmlValid) MimeType = sTextHtml; else { auto rootSeg = GetObject()->GetObj(FIELD_MIME_SEG); if (rootSeg) MimeType = rootSeg->GetStr(FIELD_MIME_TYPE); if (!MimeType) return NULL; // This is the 'multipart/encrypted' case here... TextMem.Printf("'%s' content.", MimeType.Get()); TextBody = TextMem; DisabledLook = ReadOnly = true; } } #ifdef WINDOWS if (DefAlt.CastInt32() == 2 && MimeType == sTextHtml) CtrlType = sApplicationInternetExplorer; else #endif CtrlType = MimeType; const char *Content, *Charset; if (MimeType == sTextHtml) { Content = HtmlBody; Charset = HtmlCharset; } else { Content = TextBody; Charset = TextCharset; } // Emoji check LVariant NoEmoji; App->GetOptions()->GetValue(OPT_NoEmoji, NoEmoji); // Check if the control needs changing LDocView *View = Owner->GetDoc(MimeType); if (View) { const char *ViewMimeType = View->GetMimeType(); if (MimeType != ViewMimeType) { Owner->SetDoc(NULL, ViewMimeType); View = NULL; } } if (!View) { View = App->CreateTextControl( MimeType == sTextHtml ? IDC_HTML_VIEW : IDC_TEXT_VIEW, MimeType, Edit, this); } if (!View) return NULL; // Control setup View->Sunken(Sunken); View->SetReadOnly(ReadOnly); View->SetEnv(this); if (DisabledLook) { View->GetCss(true)->BackgroundColor(L_MED); View->GetCss()->Color(L_LOW); } else { View->GetCss(true)->BackgroundColor(LCss::ColorInherit); View->GetCss()->Color(LCss::ColorInherit); } LVariant UseCid = true; View->SetValue(LDomPropToString(HtmlImagesLinkCid), UseCid); LVariant LoadImages; App->GetOptions()->GetValue(OPT_HtmlLoadImages, LoadImages); bool AppLoadImages = LoadImages.CastInt32() != 0; bool MailLoadImages = TestFlag(GetFlags(), MAIL_SHOW_IMAGES); const char *SenderAddr = GetFrom() ? GetFrom()->GetStr(FIELD_EMAIL) : NULL; auto SenderStatus = App->RemoteContent_GetSenderStatus(SenderAddr); View->SetLoadImages ( SenderStatus != RemoteNeverLoad && ( AppLoadImages || MailLoadImages || SenderStatus == RemoteAlwaysLoad ) ); // Attach control Owner->SetDoc(View, MimeType); LCharset *CsInfo = LGetCsInfo(Charset); // Check for render scripts LArray Renderers; LString RenderMsg = "Rendering..."; if (App->GetScriptCallbacks(LRenderMail, Renderers)) { for (auto r: Renderers) { LScriptArguments Args(NULL); Args.New() = new LVariant(App); Args.New() = new LVariant(this); Args.New() = new LVariant((void*)NULL); bool Status = App->ExecuteScriptCallback(*r, Args); if (Status) { auto Ret = Args.GetReturn(); if (Ret->IsString() || Ret->CastInt32()) { if (Ret->IsString()) RenderMsg = Ret->Str(); d->Renderer.Reset(new MailRendererScript(this, r)); break; } } } } if (d->Renderer) { LString Nm; if (MimeType.Equals(sTextHtml)) Nm.Printf("%s", RenderMsg.Get()); else Nm = RenderMsg; View->Name(Nm); } else { // Send the data to the control size_t ContentLen = Content ? strlen(Content) : 0; Html1::LHtml *Html = dynamic_cast(View); if (MimeType.Equals(sTextHtml)) { if (CsInfo) { int OverideDocCharset = *Charset == '>' ? 1 : 0; View->SetCharset(Charset + OverideDocCharset); if (Html) Html->SetOverideDocCharset(OverideDocCharset != 0); } else { View->SetCharset(0); if (Html) Html->SetOverideDocCharset(0); } View->Name(Content); } else { LAutoPtr Utf32((uint32_t*)LNewConvertCp("utf-32", Content, Charset ? Charset : (char*)"utf-8", MaxBytes > 0 ? MIN(ContentLen, MaxBytes) : ContentLen)); if (Utf32) { int Len = 0; while (Utf32[Len]) Len++; #if 0 LFile f; if (f.Open("c:\\temp\\utf32.txt", O_WRITE)) { uchar bom[4] = { 0xff, 0xfe, 0, 0 }; f.Write(bom, 4); f.Write(Utf32, Len * sizeof(uint32)); f.Close(); } #endif } LAutoWString Wide; Wide.Reset((char16*)LNewConvertCp(LGI_WideCharset, Content, Charset ? Charset : (char*)"utf-8", MaxBytes > 0 ? MIN(ContentLen, MaxBytes) : ContentLen)); if (Wide) { View->NameW(Wide); } else { // Fall back... try and show something at least LAutoString t(NewStr(Content, MaxBytes > 0 ? MIN(ContentLen, MaxBytes) : ContentLen)); if (t) { uint8_t *i = (uint8_t*)t.Get(); while (*i) { if (*i & 0x80) *i &= 0x7f; i++; } View->Name(t); } else View->NameW(0); } View->SetFixedWidthFont(TestFlag(GetFlags(), MAIL_FIXED_WIDTH_FONT)); } } return View; } bool Mail::OnNavigate(LDocView *Parent, const char *Uri) { if (Uri) { if ( _strnicmp(Uri, "mailto:", 7) == 0 || ( strchr(Uri, '@') && !strchr(Uri, '/') ) ) { // Mail address return App->CreateMail(0, Uri, 0) != 0; } else { return LDefaultDocumentEnv::OnNavigate(Parent, Uri); } } return false; } void Mail::Update() { TotalSizeCache = -1; LListItem::Update(); } LString::Array ParseIdList(const char *In) { LString::Array result; if (!In) return result; while (*In && strchr(WhiteSpace, *In)) In++; if (*In == '<') { // Standard msg-id list.. for (auto s = In; s && *s; ) { s = strchr(s, '<'); if (!s) break; while (*s == '<') s++; char *e = strchr(s, '>'); if (e) { result.New().Set(s, e-s); s = e + 1; } else break; } } else { // Non compliant msg-id list... const char Delim[] = ", \t\r\n"; for (auto s = In; s && *s; ) { if (strchr(Delim, *s)) s++; else { auto Start = s; while (*s && !strchr(Delim, *s)) s++; result.New().Set(Start, s - Start); } } } return result; } void Base36(char *Out, uint64 In) { while (In) { int p = (int)(In % 36); if (p < 10) { *Out++ = '0' + p; } else { *Out++ = 'A' + p - 10; } In /= 36; } *Out++ = 0; } void Mail::ClearCachedItems() { TotalSizeCache = -1; PreviewCacheX = -1; PreviewCache.DeleteObjects(); Attachment *a; int i = 0; while ((a = Attachments[i])) { if (!a->DecRef()) i++; } } void Mail::NewRecipient(char *Email, char *Name) { LDataPropI *a = GetTo()->Create(GetObject()->GetStore()); if (a) { a->SetStr(FIELD_EMAIL, Email); a->SetStr(FIELD_NAME, Name); GetTo()->Insert(a); } } void Mail::PrepSend() { // Set flags and other data SetDirty(); int OldFlags = GetFlags(); SetFlags((OldFlags | MAIL_READY_TO_SEND) & ~MAIL_SENT); // we want to send now... // Check we're in the Outbox ScribeFolder *OutBox = App->GetFolder(FOLDER_OUTBOX, GetObject()); if (OutBox) { ScribeFolder *f = GetFolder(); if (!f || f != OutBox) { LArray Items; Items.Add(this); OutBox->MoveTo(Items, false); } } } bool Mail::Send(bool Now) { // Check for any "on before send" callbacks: bool AllowSend = true; LArray Callbacks; if (App->GetScriptCallbacks(LMailOnBeforeSend, Callbacks)) { for (unsigned i=0; AllowSend && iExecuteScriptCallback(c, Args)) { if (!Args.GetReturn()->CastInt32()) AllowSend = false; } Args.DeleteObjects(); } } } if (!AllowSend) return false; // Set the ready to send flag.. bool IsInPublicFolder = GetFolder() && GetFolder()->IsPublicFolders(); if (!IsInPublicFolder) { PrepSend(); if (Now) { // Kick off send thread if relevant LVariant Offline; App->GetOptions()->GetValue(OPT_WorkOffline, Offline); if (!Offline.CastInt32() && !IsInPublicFolder) { App->PostEvent(M_COMMAND, IDM_SEND_MAIL, (LMessage::Param)Handle()); } } } return true; } bool Mail::MailMessageIdMap(bool Add) { if (Add) { auto LoadState = GetLoaded(); if (LoadState < Store3Headers) { LAssert(!"Not loaded yet."); LStackTrace("MailMessageIdMap msg not loaded yet: %i\n", LoadState); return false; } // char *ObjId = GetObject()->GetStr(FIELD_MESSAGE_ID); auto Id = GetMessageId(true); if (!Id) { LAssert(!"No message ID? Impossible!"); GetMessageId(true); return false; } #if 0 LgiTrace("MailMessageIdMap(%i) Old=%s Id=%s Mail=%p\n", Add, ObjId, Id, this); #endif return MessageIdMap.Add(Id, this); } else { auto Id = GetMessageId(); #if 0 LgiTrace("MailMessageIdMap(%i) Id=%s Mail=%p\n", Add, Id, this); #endif return MessageIdMap.Delete(Id); } } Mail *Mail::GetMailFromId(const char *Id) { Mail *m = MessageIdMap.Find(Id); // LgiTrace("GetMailFromId(%s)=%p\n", Id, m); return m; } bool Mail::SetMessageId(const char *MsgId) { if (LAppInst->InThread()) { LAssert(GetObject() != NULL); auto OldId = GetObject()->GetStr(FIELD_MESSAGE_ID); if (OldId) MessageIdMap.Delete(OldId); LAutoString m(NewStr(MsgId)); LAutoString s(TrimStr(m, "<>")); if (GetObject()->SetStr(FIELD_MESSAGE_ID, s)) SetDirty(); return s != 0; } else { // No no no NO NON NOT NADA. LAssert(0); } return false; } LAutoString Mail::GetThreadIndex(int TruncateChars) { LAutoString Id; LAutoString Raw(InetGetHeaderField(GetInternetHeader(), "Thread-Index")); if (Raw) { Id = ConvertThreadIndex(Raw, TruncateChars); } return Id; } const char *Mail::GetMessageId(bool Create) { LAssert(GetObject() != NULL); d->MsgIdCache = GetObject()->GetStr(FIELD_MESSAGE_ID); if (!d->MsgIdCache) { bool InThread = GetCurrentThreadId() == LAppInst->GetGuiThreadId(); LAutoString Header(InetGetHeaderField(GetInternetHeader(), "Message-ID")); if (Header) { LAssert(InThread); if (InThread) { auto Ids = ParseIdList(Header); SetMessageId(d->MsgIdCache = Ids[0]); } } if (!d->MsgIdCache && Create) { LAssert(InThread); if (InThread) { auto FromEmail = GetFromStr(FIELD_EMAIL); const char *At = FromEmail ? strchr(FromEmail, '@') : 0; LVariant Email; if (!At) { if (App->GetOptions()->GetValue(OPT_Email, Email) && Email.Str()) { At = strchr(Email.Str(), '@'); } else { At = "@domain.com"; } } if (At) { char m[96], a[32], b[32]; Base36(a, LCurrentTime()); Base36(b, LRand(RAND_MAX)); sprintf_s(m, sizeof(m), "<%s.%i%s%s>", a, LRand(RAND_MAX), b, At); if (GetObject()->SetStr(FIELD_MESSAGE_ID, m)) SetDirty(); d->MsgIdCache = GetObject()->GetStr(FIELD_MESSAGE_ID); } else LgiTrace("%s:%i - Error, no '@' in %s.\n", _FL, FromEmail); } else LgiTrace("%s:%i - Error, not in thread.\n", _FL); } } else { d->MsgIdCache = d->MsgIdCache.Strip("<>").Strip(); } LAssert ( (!d->MsgIdCache && !Create) || (d->MsgIdCache && !strchr(d->MsgIdCache, '\n')) ); return d->MsgIdCache; } bool Mail::GetReferences(LString::Array &Ids) { LAutoString References(InetGetHeaderField(GetInternetHeader(), "References")); if (References) { Ids = ParseIdList(References); } LAutoString InReplyTo(InetGetHeaderField(GetInternetHeader(), "In-Reply-To")); if (InReplyTo) { auto To = ParseIdList(InReplyTo); bool Has = false; auto &r = To[0]; if (r) { for (auto h: Ids) { if (!strcmp(h, r)) Has = true; } if (!Has) { To.Delete(r); Ids.New() = r; } } To.DeleteArrays(); } if (Ids.Length() == 0) { LAutoString Id = GetThreadIndex(5); if (Id) { size_t Len = strlen(Id); size_t Bytes = Len >> 1; if (Bytes >= 22) { Ids.New() = Id.Get(); } } } return Ids.Length() > 0; } LString AddrToHtml(LDataPropI *a) { LString s; if (a) { auto Name = a->GetStr(FIELD_NAME); auto Addr = a->GetStr(FIELD_EMAIL); LXmlTree t; LAutoString eName(t.EncodeEntities(Name)); LAutoString eAddr(t.EncodeEntities(Addr)); if (Name && Addr) s.Printf("%s", eAddr.Get(), eName.Get()); else if (Name) s = eName.Get(); else if (Addr) s.Printf("%s", eAddr.Get(), eAddr.Get()); } return s; } LDataPropI *FindMimeSeg(LDataPropI *s, char *Type) { if (!s) return 0; const char *Mt = s->GetStr(FIELD_MIME_TYPE); if (!Mt) Mt = "text/plain"; if (!_stricmp(Type, Mt)) return s; LDataIt c = s->GetList(FIELD_MIME_SEG); if (!c) return 0; for (LDataPropI *i=c->First(); i; i=c->Next()) { LDataPropI *Child = FindMimeSeg(i, Type); if (Child) return Child; } return 0; } bool Mail::GetAttachmentObjs(LArray &Objs) { if (!GetObject()) return false; CollectAttachments(&Objs, NULL, NULL, NULL, GetObject()->GetObj(FIELD_MIME_SEG)); return Objs.Length() > 0; } void DescribeMime(LStream &p, LDataI *Seg) { auto Mt = Seg->GetStr(FIELD_MIME_TYPE); p.Print("%s", Mt); auto Cs = Seg->GetStr(FIELD_CHARSET); if (Cs) p.Print(" - %s", Cs); p.Print("
\n"); LDataIt c = Seg->GetList(FIELD_MIME_SEG); if (c && c->First()) { p.Print("
\n"); for (LDataPropI *i=c->First(); i; i=c->Next()) { LDataI *a = dynamic_cast(i); if (a) DescribeMime(p, a); } p.Print("
\n"); } } bool Mail::GetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { case SdFrom: // Type: ListAddr { Value = GetFrom(); break; } case SdFromHtml: // Type: String { LString s = AddrToHtml(GetFrom()); if (s.Length() == 0) return false; Value = s; break; } case SdContact: // Type: Contact { if (!GetFrom()) return false; auto Addr = GetFrom()->GetStr(FIELD_EMAIL); if (!Addr) return false; Contact *c = Contact::LookupEmail(Addr); if (!c) return App->GetVariant("NoContact", Value); Value = (LDom*)c; break; } case SdTo: // Type: ListAddr[] { if (Array) { LDataIt To = GetTo(); int Idx = atoi(Array); bool Create = false; if (Idx < 0) { Create = true; Idx = -Idx; } LDataPropI *a = Idx < (int)To->Length() ? (*To)[Idx] : 0; if (!a && Create) { if ((a = To->Create(GetObject()->GetStore()))) { To->Insert(a); } } Value = a; } else if (Value.SetList()) { LDataIt To = GetTo(); for (LDataPropI *a=To->First(); a; a=To->Next()) { LVariant *Recip = new LVariant; if (Recip) { *Recip = a; Value.Value.Lst->Insert(Recip); } } } else return false; break; } case SdToHtml: // Type: String { if (GetTo()) { LStringPipe p; for (LDataPropI *t=GetTo()->First(); t; t=GetTo()->Next()) { if (p.GetSize()) p.Write((char*)", ", 2); LString s = AddrToHtml(t); if (s) p.Write(s, s.Length()); } Value.Type = GV_STRING; Value.Value.String = p.NewStr(); } break; } case SdSubject: // Type: String { Value = GetSubject(); break; } case SdBody: // Type: String { if (Array) { auto Body = GetBody(); LAutoString h; for (auto c = Body; c && *c; ) { while (*c && strchr(WhiteSpace, *c)) c++; auto Start = c; while (*c && *c != ':' && *c != '\n') c++; if (*c == ':') { if (Start[0] == '\"' && c[1] == '\"') c++; LString s(Start, c - Start); s = s.Strip("\'\" \t[]<>{}:"); if (s.Equals(Array)) { // Match... c++; while (*c && strchr(WhiteSpace, *c)) c++; Start = c; while (*c && *c != '\n') c++; while (c > Start && strchr(WhiteSpace, c[-1])) c--; h.Reset(NewStr(Start, c - Start)); break; } else { while (*c && *c != '\n') c++; } } if (*c == '\n') c++; } if (h) { if (GetBodyCharset()) { char *u = (char*)LNewConvertCp("utf-8", h, GetBodyCharset()); if (u) { Value.OwnStr(u); } else { Value.OwnStr(h.Release()); } } else { Value.OwnStr(h.Release()); } } } else { Value = GetBody(); } break; } case SdBodyAsText: // Type: String { size_t MaxSize = Atoi(Array); auto Txt = GetBody(); auto Html = GetHtml(); if (ValidStr(Txt) && ValidStr(Html)) { LVariant Def; App->GetOptions()->GetValue(OPT_DefaultAlternative, Def); if (Def.CastInt32()) { goto DoHtml; } goto DoText; } else if (ValidStr(Txt)) { DoText: LAutoString CharSet = GetCharSet(); if (CharSet) { size_t TxtLen = strlen(Txt); Value.OwnStr((char*)LNewConvertCp("utf-8", Txt, CharSet, MIN(TxtLen, MaxSize))); } else Value = Txt; } else if (ValidStr(Html)) { DoHtml: auto cs = GetHtmlCharset(); auto v = HtmlToText(Html, cs); Value = v.Get(); } else return false; break; } case SdBodyAsHtml: // Type: String { // int MaxSize = ValidStr(Array) ? atoi(Array) : -1; MailPrivate::HtmlBody *b = d->GetBody(); if (!b) return false; LStringPipe p; p.Print("
\n%s\n
\n", ScribeReplyClass, b->Html.Get()); Value.OwnStr(p.NewStr()); LgiTrace("Value=%s\n", Value.Str()); break; } case SdHtmlHeadFields: // Type: String { MailPrivate::HtmlBody *b = d->GetBody(); if (!b) return false; LStringPipe p; if (ValidStr(b->Charset)) p.Print("\t\n", b->Charset.Get()); p.Print("\t"); Value.OwnStr(p.NewStr()); break; } case SdMessageId: // Type: String { Value = GetMessageId(); return true; } case SdInternetHeaders: // Type: String { Value = GetInternetHeader(); break; } case SdInternetHeader: // Type: String[] { if (!Array || !GetInternetHeader()) return false; LAutoString s(InetGetHeaderField(GetInternetHeader(), Array)); if (s) Value = s; else Value.Empty(); break; } case SdPriority: // Type: Int32 { Value = (int)GetPriority(); break; } case SdHtml: // Type: String { Value = GetHtml(); break; } case SdFlags: // Type: Int32 { if (Array) { int Flag = StringToMailFlag(Array); Value = (GetFlags() & Flag) != 0; } else { Value = (int)GetFlags(); } break; } case SdFolder: // Type: ScribeFolder { ScribeFolder *f = GetFolder(); if (!f) return false; Value = (LDom*)f; break; } case SdScribe: // Type: ScribeWnd { Value = (LDom*)App; break; } case SdMail: // Type: Mail { Value = PreviousMail; break; } case SdDateSent: // Type: DateTime { Value = GetDateSent(); break; } case SdDateReceived: // Type: DateTime { Value = GetDateReceived(); break; } case SdSig: // Type: String { bool Type = false; if (Array && stristr(Array, "html")) Type = true; Value = GetSig(Type); break; } case SdSize: // Type: Int64 { Value = TotalSizeof(); break; } case SdLabel: // Type: String { Value = GetLabel(); break; } case SdAttachments: // Type: Int32 { LArray Lst; GetAttachmentObjs(Lst); Value = (int)Lst.Length(); break; } case SdAttachment: // Type: Attachment[] { List Files; if (GetAttachments(&Files) && Array) { int i = atoi(Array); Value = Files[i]; if (Value.Type == GV_DOM) return true; } LAssert(!"Not a valid attachment?"); return false; } case SdMimeTree: // Type: String { LDataI *Root = dynamic_cast(GetObject()->GetObj(FIELD_MIME_SEG)); if (!Root) return false; LStringPipe p(256); DescribeMime(p, Root); Value.OwnStr(p.NewStr()); break; } case SdUi: // Type: LView { Value = Ui; break; } case SdType: // Type: Int32 { Value = GetObject()->Type(); break; } case SdSelected: // Type: Bool { Value = Select(); break; } case SdRead: // Type: Bool { Value = (GetFlags() & MAIL_READ) != 0; break; } case SdShowImages: // Type: Bool { Value = (GetFlags() & MAIL_SHOW_IMAGES) != 0; break; } case SdColour: // Type: Int32 { Value = GetMarkColour(); break; } case SdReceivedDomain: // Type: String { Value = GetFieldText(FIELD_RECEIVED_DOMAIN); break; } default: { return false; } } return true; } #define DomSetStr(Dom, Fld) \ case Dom: \ if (GetObject() && Value.Type == GV_STRING) \ GetObject()->SetStr(Fld, Value.Str()); \ else \ LAssert(!"Missing object or type err"); \ break; #define DomSetInt(Dom, Fld) \ case Dom: \ if (GetObject() && Value.Type == GV_INT32) \ GetObject()->SetInt(Fld, Value.Value.Int); \ else \ LAssert(!"Missing object or type err"); \ break; #define DomSetDate(Dom, Fld) \ case Dom: \ if (GetObject() && Value.Type == GV_DATETIME) \ GetObject()->SetDate(Fld, Value.Value.Date); \ else \ LAssert(!"Missing object or type err"); \ break; bool Mail::SetVariant(const char *Name, LVariant &Value, const char *Array) { ScribeDomType Fld = StrToDom(Name); switch (Fld) { DomSetStr(SdSubject, FIELD_SUBJECT) DomSetStr(SdMessageId, FIELD_MESSAGE_ID) DomSetStr(SdInternetHeaders, FIELD_INTERNET_HEADER) DomSetInt(SdPriority, FIELD_PRIORITY) DomSetDate(SdDateSent, FIELD_DATE_SENT) DomSetDate(SdDateReceived, FIELD_DATE_RECEIVED) case SdBody: { if (!SetBody(Value.Str())) return false; break; } case SdHtml: { if (!SetHtml(Value.Str())) return false; break; } case SdRead: { if (Value.CastInt32()) SetFlags(GetFlags() | MAIL_READ); else SetFlags(GetFlags() & ~MAIL_READ); break; } case SdColour: { auto u32 = Value.IsNull() ? 0 : (uint32_t)Value.CastInt32(); if (!SetMarkColour(u32)) return false; break; } case SdShowImages: { if (Value.CastInt32()) SetFlags(GetFlags() | MAIL_SHOW_IMAGES); else SetFlags(GetFlags() & ~MAIL_SHOW_IMAGES); break; } case SdSelected: { Select(Value.CastInt32() != 0); break; } case SdLabel: { if (!SetLabel(Value.Str())) return false; break; } case SdFlags: { if (Array) { int Flag = StringToMailFlag(Array); if (Value.CastInt32()) // Set SetFlags(GetFlags() | Flag); else SetFlags(GetFlags() & ~Flag); } else { SetFlags(Value.CastInt32()); } break; } default: { return false; } } SetDirty(); Update(); return true; } bool Mail::CallMethod(const char *MethodName, LScriptArguments &Args) { ScribeDomType Fld = StrToDom(MethodName); switch (Fld) { default: break; case SdSend: // Type: ([Bool SendNow = true]) { bool Now = Args.Length() > 0 ? Args[0]->CastInt32() != 0 : true; bool Result = Send(Now); *Args.GetReturn() = Result; return true; } case SdAddCalendarEvent: // Type: ([Bool AddPopupReminder]) { bool AddPopupReminder = Args.Length() > 0 ? Args[0]->CastInt32() != 0 : true; bool Result = AddCalendarEvent(NULL, AddPopupReminder, NULL); *Args.GetReturn() = Result; return true; } case SdGetRead: // Type: () { *Args.GetReturn() = (GetFlags() & MAIL_READ) != 0; return true; } case SdSetRead: // Type: (Bool IsRead = true) { bool Rd = Args.Length() ? Args[0]->CastInt32() != 0 : true; auto Flags = GetFlags(); if (Rd) SetFlags(Flags | MAIL_READ); else SetFlags(Flags & ~MAIL_READ); *Args.GetReturn() = true; return true; } case SdBayesianChange: // Type: (int SpamWordOffset, int HamWordOffset) { if (Args.Length() != 2) { LgiTrace("%s:%i - Invalid arg count, expecting (int SpamWordOffset, int HamWordOffset)\n", _FL); *Args.GetReturn() = false; return true; } auto SpamWordOffset = Args[0]->CastInt32(); auto HamWordOffset = Args[1]->CastInt32(); if (SpamWordOffset > 1) App->OnBayesianMailEvent(this, BayesMailUnknown, BayesMailSpam); else if (SpamWordOffset < 0) App->OnBayesianMailEvent(this, BayesMailSpam, BayesMailUnknown); if (HamWordOffset > 1) App->OnBayesianMailEvent(this, BayesMailUnknown, BayesMailHam); else if (HamWordOffset < 1) App->OnBayesianMailEvent(this, BayesMailHam, BayesMailUnknown); break; } case SdBayesianScore: // Type: () { double Result; if (App->IsSpam(Result, this)) { *Args.GetReturn() = Result; return true; } break; } case SdSearchHtml: // Type: (String SearchExpression) { if (Args.Length() != 2) { LgiTrace("%s:%i - Method needs 1 argument.\n", _FL); *Args.GetReturn() = false; return true; } auto Html = GetHtml(); if (!Html) { LgiTrace("%s:%i - No HTML to parse.\n", _FL); *Args.GetReturn() = false; return true; } // auto Cs = GetHtmlCharset(); SearchHtml(Args.GetReturn(), Html, Args[0]->Str(), Args[1]->Str()); return true; } case SdDeleteAsSpam: // Type: () { DeleteAsSpam(App); return true; } } return Thing::CallMethod(MethodName, Args); } char *Mail::GetDropFileName() { if (!DropFileName) { LString Subj = GetSubject(); Subj = Subj.Strip(" \t\r\n."); DropFileName.Reset(MakeFileName(ValidStr(Subj) ? Subj.Get() : (char*)"Untitled", "eml")); } return DropFileName; } bool Mail::GetDropFiles(LString::Array &Files) { if (!GetDropFileName()) return false; if (!LFileExists(DropFileName)) { LAutoPtr F(new LFile); if (F->Open(DropFileName, O_WRITE)) { F->SetSize(0); if (!Export(AutoCast(F), sMimeMessage)) return false; } else return false; } if (!LFileExists(DropFileName)) return false; Files.Add(DropFileName.Get()); return true; } OsView Mail::Handle() { return #if LGI_VIEW_HANDLE (Ui) ? Ui->Handle() : #endif NULL; } Thing &Mail::operator =(Thing &t) { Mail *m = t.IsMail(); if (m) { #define CopyStr(dst, src) \ { DeleteArray(dst); dst = NewStr(src); } /* for (LDataPropI *a = m->GetTo()->First(); a; a = m->GetTo()->Next()) { LDataPropI *NewA = GetTo()->Create(Object->GetStore()); if (NewA) { *NewA = *a; GetTo()->Insert(NewA); } } */ if (GetObject() && m->GetObject()) { GetObject()->CopyProps(*m->GetObject()); } else LAssert(!"This shouldn't happen right?"); Update(); } return *this; } bool Mail::HasAlternateHtml(Attachment **Attach) { // New system of storing alternate HTML in a Mail object field. if (GetHtml()) return true; // Old way of storing alternate HTML, in an attachment. // pre v1.53 List Files; if (GetAttachments(&Files)) { for (auto a: Files) { if ((_stricmp(a->GetText(0), "attachment.html") == 0 || _stricmp(a->GetText(0), "index.html") == 0) && (a->GetMimeType() ? _stricmp(a->GetMimeType(), "text/html") == 0 : 1)) { if (Attach) { *Attach = a; } return true; } } } // None found return false; } char *Mail::GetAlternateHtml(List *Refs) { char *Status = 0; Attachment *Attach = 0; if (HasAlternateHtml(&Attach)) { if (GetHtml()) { // New system of storing alternate HTML in a Mail object field. Status = NewStr(GetHtml()); } else if (Attach) { // Old way of storing alternate HTML, in an attachment. // pre v1.53 char *Ptr; ssize_t Size; if (Attach->Get(&Ptr, &Size)) { Status = NewStr((char*)Ptr, Size); } } if (Status && Refs) { // Turn all the cid: references into file names... LStringPipe Out; char *n; for (char *s=Status; *s; s=n) { n = stristr(s, "\"cid:"); if (n) { // Extract cid n++; Out.Push(s, n-s); s = n += 4; while (*n && !strchr("\"\' >", *n)) n++; char *Cid = NewStr(s, n-s); if (Cid) { // Find attachment List Files; if (GetAttachments(&Files)) { for (auto a: Files) { if (a->GetContentId() && strcmp(a->GetContentId(), Cid) == 0) { Refs->Insert(a); Out.Push(a->GetName()); } } } } } else { Out.Push(s); break; } } DeleteArray(Status); Status = Out.NewStr(); } } return Status; } bool Mail::WriteAlternateHtml(char *DstFile, int DstFileLen) { bool Status = false; char *Tmp = ScribeTempPath(); List Refs; char *Html; if (Tmp && (Html = GetAlternateHtml(&Refs))) { char FileName[256]; LMakePath(FileName, sizeof(FileName), Tmp, "Alt.html"); if (DstFile) { strcpy_s(DstFile, DstFileLen, FileName); } LFile f; if (f.Open(FileName, O_WRITE)) { size_t Len = strlen(Html); Status = f.Write(Html, Len) == Len; f.Close(); } DeleteArray(Html); for (auto a: Refs) { LMakePath(FileName, sizeof(FileName), Tmp, a->GetName()); FileDev->Delete(FileName, false); a->SaveTo(FileName); } } return Status; } bool Mail::DeleteAttachment(Attachment *File) { bool Status = false; if (File) { if (Attachments.HasItem(File)) { Attachments.Delete(File); if (File->GetObject()) { auto r = File->GetObject()->Delete(); if (r > Store3Error) { auto o = File->GetObject(); if (o->IsOrphan()) o = NULL; // SetObject will delete... File->SetObject(NULL, false, _FL); DeleteObj(o); } } File->DecRef(); File = NULL; if (Attachments.Length() == 0) { // Remove attachments flag SetFlags(GetFlags() & (~MAIL_ATTACHMENTS)); } Update(); if (Ui) { Ui->OnAttachmentsChange(); } } } return Status; } bool Mail::UnloadAttachments() { for (auto it = Attachments.begin(); it != Attachments.end(); ) { Attachment *a = *it; if (!a->DecRef()) it++; } return true; } LArray Mail::GetAttachments() { LArray result; LArray Lst; if (GetAttachmentObjs(Lst)) { LHashTbl, bool> Loaded; for (size_t i=0; iGetObject(), true); } // Load attachments for (unsigned i=0; iSetOwner(this); Attachments.Insert(k); } } } } for (auto a: Attachments) { if (a->GetObject()->UserData != a) LAssert(!"Wut?"); else result.Add(a); } return result; } bool Mail::GetAttachments(List *Files) { if (!Files) return false; auto files = GetAttachments(); for (auto a: files) Files->Add(a); return true; } int64 Mail::TotalSizeof() { if (TotalSizeCache < 0) { int Size = GetObject() ? (int)GetObject()->GetInt(FIELD_SIZE) : -1; if (Size >= 0) { TotalSizeCache = Size; } else { TotalSizeCache = ((GetBody()) ? strlen(GetBody()) : 0) + ((GetHtml()) ? strlen(GetHtml()) : 0); List Attachments; if (GetAttachments(&Attachments)) { for (auto a: Attachments) { TotalSizeCache += a->GetSize(); } } } } return TotalSizeCache; } bool Mail::_GetListItems(List &l, bool All) { LList *ParentList = LListItem::Parent; l.Empty(); if (All) { if (ParentList) { ParentList->GetAll(l); } else { l.Insert(this); } } else { if (ParentList) { ParentList->GetSelection(l); } else if (Select()) { l.Insert(this); } } return l.Length() > 0; } void Mail::GetThread(List &Thread) { MContainer *c; for (c=Container; c->Parent; c=c->Parent); List Stack; Stack.Insert(c); while ((c=Stack[0])) { Stack.Delete(c); for (unsigned i=0; iChildren.Length(); i++) Stack.Insert(c->Children[i]); if (c->Message && !Thread.HasItem(c->Message)) { Thread.Insert(c->Message); } } } void Mail::SetListRead(bool Read) { List Sel; if (!_GetListItems(Sel, false)) return; LArray a; for (auto t: Sel) { auto m = dynamic_cast(t); if (!m) continue; bool isRead = (m->GetFlags() & MAIL_READ) != 0; if (Read ^ isRead) a.Add(m->GetObject()); } if (!a.Length()) return; auto Store = a[0]->GetStore(); if (!Store) { LAssert(0); return; } LVariant read = MAIL_READ; Store->Change(a, FIELD_FLAGS, read, Read ? OpPlusEquals : OpMinusEquals); } void SetFolderCallback(LInput *Dlg, LViewI *EditCtrl, void *Param) { ScribeWnd *App = (ScribeWnd*) Param; auto Str = EditCtrl->Name(); auto Select = new FolderDlg(Dlg, App, MAGIC_MAIL, 0, Str); Select->DoModal([Select, EditCtrl](auto dlg, auto id) { if (id) EditCtrl->Name(Select->Get()); delete dlg; }); } void Mail::DoContextMenu(LMouse &m, LView *p) { #ifdef _DEBUG LAutoPtr Prof(new LProfile("Mail::DoContextMenu")); Prof->HideResultsIfBelow(100); #endif if (!p) p = Parent; // open the right click menu MarkedState MarkState = MS_None; // Pre-processing for marks List Sel; if (_GetListItems(Sel, false)) { int Marked = 0; for (auto t: Sel) { Mail *m = dynamic_cast(t); if (m) { if (m->GetMarkColour()) { Marked++; } } } if (Marked == 1) { MarkState = MS_One; } else if (Marked) { MarkState = MS_Multiple; } } #ifdef _DEBUG Prof->Add("CreateMenu"); #endif // Create menu LScriptUi s(new LSubMenu); if (s.Sub) { /* Keyboard shortcuts: o - Open d - Delete x - Export e - Set read u - Set unread r - Reply a - Reply all f - Forward b - Bounce m - Mark s - Select marked c - Create filter i - Inspect p - Properties */ LMenuItem *i; s.Sub->SetImageList(App->GetIconImgList(), false); s.Sub->AppendItem(LLoadString(IDS_OPEN), IDM_OPEN, true); i = s.Sub->AppendItem(LLoadString(IDS_DELETE), IDM_DELETE, true); i->Icon(ICON_TRASH); s.Sub->AppendItem(LLoadString(IDS_EXPORT), IDM_EXPORT, true); int AttachBaseMsg = 10000; #ifdef _DEBUG Prof->Add("GetAttachments"); #endif List AttachLst; if (GetAttachments(&AttachLst) && AttachLst[0]) { LSubMenu *Attachments = s.Sub->AppendSub(LLoadString(IDS_SAVE_ATTACHMENT_AS)); if (Attachments) { int n=0; for (auto a: AttachLst) { auto Name = a->GetName(); Attachments->AppendItem(Name?Name:(char*)"Attachment.txt", AttachBaseMsg+(n++), true); } } } s.Sub->AppendSeparator(); #ifdef _DEBUG Prof->Add("Read/Unread"); #endif i = s.Sub->AppendItem(AddAmp(LLoadString(IDS_SET_READ), 'e'), IDM_SET_READ, true); i->Icon(ICON_READ_MAIL); i = s.Sub->AppendItem(AddAmp(LLoadString(IDS_SET_UNREAD), 'u'), IDM_SET_UNREAD, true); i->Icon(ICON_UNREAD_MAIL); s.Sub->AppendSeparator(); #ifdef _DEBUG Prof->Add("Reply/Bounce"); #endif i = s.Sub->AppendItem(AddAmp(LLoadString(IDS_REPLY), 'r'), IDM_REPLY, true); i->Icon(ICON_FLAGS_REPLY); s.Sub->AppendItem(AddAmp(LLoadString(IDS_REPLYALL), 'a'), IDM_REPLY_ALL, true); i = s.Sub->AppendItem(AddAmp(LLoadString(IDS_FORWARD), 'f'), IDM_FORWARD, true); i->Icon(ICON_FLAGS_FORWARD); i = s.Sub->AppendItem(AddAmp(LLoadString(IDS_BOUNCE), 'b'), IDM_BOUNCE, true); i->Icon(ICON_FLAGS_BOUNCE); s.Sub->AppendSeparator(); #ifdef _DEBUG Prof->Add("Thread"); #endif if (App->GetCtrlValue(IDM_THREAD)) { auto Thread = s.Sub->AppendSub(LLoadString(IDS_THREAD)); if (Thread) { Thread->AppendItem(LLoadString(IDS_THREAD_SELECT), IDM_SELECT_THREAD); Thread->AppendItem(LLoadString(IDS_THREAD_DELETE), IDM_DELETE_THREAD); Thread->AppendItem(LLoadString(IDS_THREAD_IGNORE), IDM_IGNORE_THREAD); s.Sub->AppendSeparator(); } } #ifdef _DEBUG Prof->Add("Mark"); #endif auto MarkMenu = s.Sub->AppendSub(AddAmp(LLoadString(IDS_MARK), 'm')); if (MarkMenu) { BuildMarkMenu(MarkMenu, MarkState, (uint32_t)GetMarkColour(), true); } MarkMenu = s.Sub->AppendSub(AddAmp(LLoadString(IDS_SELECT_MARKED), 's')); if (MarkMenu) { BuildMarkMenu(MarkMenu, MS_Multiple, 0, true, true, true); } s.Sub->AppendSeparator(); s.Sub->AppendItem(AddAmp(LLoadString(IDS_INSPECT), 'i'), IDM_INSPECT); s.Sub->AppendItem(LLoadString(IDS_REPARSE), IDM_REPARSE); s.Sub->AppendItem(LLoadString(IDS_PROPERTIES), IDM_PROPERTIES); #ifdef _DEBUG Prof->Add("ScriptCallbacks"); #endif LArray Callbacks; if (App->GetScriptCallbacks(LThingContextMenu, Callbacks)) { LScriptArguments Args(NULL); Args[0] = new LVariant(App); Args[1] = new LVariant(this); Args[2] = new LVariant(&s); for (auto c: Callbacks) App->ExecuteScriptCallback(*c, Args); } m.ToScreen(); int Result; #ifdef _DEBUG Prof.Reset(); #endif int Btn = 0; if (m.Left()) Btn = LSubMenu::BtnLeft; else if (m.Right()) Btn = LSubMenu::BtnRight; Result = s.Sub->Float(p, m.x, m.y); switch (Result) { case IDM_OPEN: { DoUI(); break; } case IDM_DELETE: { LVariant ConfirmDelete = false; App->GetOptions()->GetValue(OPT_ConfirmDelete, ConfirmDelete); if (!ConfirmDelete.CastInt32() || LgiMsg(GetList(), LLoadString(IDS_DELETE_ASK), AppName, MB_YESNO) == IDYES) { if (_GetListItems(Sel, false)) { int Index = -1; LList *TheList = LListItem::Parent; LArray Items; for (auto s: Sel) { Mail *m = dynamic_cast(s); if (m) { if (Index < 0) Index = TheList->IndexOf(m); Items.Add(m); } } GetFolder()->Delete(Items, true); if (Index >= 0) { LListItem *i = TheList->ItemAt(Index); if (i) i->Select(true); } } } break; } case IDM_EXPORT: { ExportAll(Parent, sMimeMessage, NULL); break; } case IDM_REPLY: case IDM_REPLY_ALL: { App->MailReplyTo(this, Result == IDM_REPLY_ALL); SetDirty(false); break; } case IDM_FORWARD: { App->MailForward(this); SetDirty(false); break; } case IDM_BOUNCE: { App->MailBounce(this); SetDirty(false); break; } case IDM_SET_READ: { SetListRead(true); break; } case IDM_SET_UNREAD: { SetListRead(false); break; } case IDM_INSPECT: { OnInspect(); break; } case IDM_PROPERTIES: { OnProperties(); break; } case IDM_SELECT_THREAD: { if (_GetListItems(Sel, false)) { List Thread; for (auto s: Sel) { Mail *m = dynamic_cast(s); if (m) m->GetThread(Thread); } for (auto m: Thread) { m->Select(true); } } break; } case IDM_DELETE_THREAD: { if (_GetListItems(Sel, false)) { List Thread; for (auto s: Sel) { Mail *m = dynamic_cast(s); if (m) m->GetThread(Thread); } for (auto m: Thread) { m->OnDelete(); } } break; } case IDM_IGNORE_THREAD: { if (_GetListItems(Sel, false)) { List Thread; for (auto s: Sel) { Mail *m = dynamic_cast(s); if (m) m->GetThread(Thread); } for (auto m: Thread) { m->SetFlags(m->GetFlags() | MAIL_IGNORE | MAIL_READ); } } break; } case IDM_REPARSE: { if (!_GetListItems(Sel, false)) break; for (auto s: Sel) { Mail *m = dynamic_cast(s); if (m) m->Reparse(); } break; } case IDM_UNMARK: case IDM_SELECT_NONE: case IDM_SELECT_ALL: default: { if (Result == IDM_UNMARK || (Result >= IDM_MARK_BASE && Result < IDM_MARK_BASE+CountOf(MarkColours32))) { bool Marked = Result != IDM_UNMARK; if (_GetListItems(Sel, false)) { COLOUR Col32 = Marked ? MarkColours32[Result - IDM_MARK_BASE] : 0; for (auto s: Sel) { Mail *m = dynamic_cast(s); if (m) { if (m->SetMarkColour(Col32)) { if (m->GetObject()->GetInt(FIELD_STORE_TYPE) != Store3Imap) // Imap knows to save itself. m->SetDirty(); } m->Update(); } } } } else if (Result == IDM_SELECT_NONE || Result == IDM_SELECT_ALL || (Result >= IDM_MARK_SELECT_BASE && Result < IDM_MARK_SELECT_BASE+CountOf(MarkColours32))) { bool None = Result == IDM_SELECT_NONE; bool All = Result == IDM_SELECT_ALL; uint32_t c32 = MarkColours32[Result - IDM_MARK_SELECT_BASE]; if (_GetListItems(Sel, true)) { for (auto s: Sel) { Mail *m = dynamic_cast(s); if (!m) break; auto CurCol = m->GetMarkColour(); if (None) { m->Select(CurCol <= 0); } else { if (CurCol > 0) { if (All) { m->Select(true); } else { m->Select(CurCol && CurCol == c32); } } else { m->Select(false); } } } } } else if (Result >= AttachBaseMsg && Result < AttachBaseMsg + (ssize_t)AttachLst.Length()) { // save attachment as... Attachment *a = AttachLst.ItemAt(Result - AttachBaseMsg); if (a) { a->OnSaveAs(Parent); } } else { // Handle any installed callbacks for menu items for (unsigned i=0; iExecuteScriptCallback(Cb, Args); } } } break; } } DeleteObj(s.Sub); } } void Mail::OnMouseClick(LMouse &m) { if (m.Down()) { if (!m.IsContextMenu()) { if (m.Double()) { // open the UI for the Item DoUI(); } } else { DoContextMenu(m); } } } void Mail::OnCreate() { LOptionsFile *Options = App->GetOptions(); LVariant v; if (GetObject()) GetObject()->SetInt(FIELD_FLAGS, MAIL_CREATED); // Get identity and set from info ScribeAccount *Ident = App->GetCurrentAccount(); if (Ident) { v = Ident->Identity.Name(); GetFrom()->SetStr(FIELD_NAME, v.Str()); v = Ident->Identity.Email(); GetFrom()->SetStr(FIELD_EMAIL, v.Str()); v = Ident->Identity.ReplyTo(); if (v.Str()) GetReply()->SetStr(FIELD_REPLY, v.Str()); } else { LAssert(!"No identity selected"); } LVariant EditCtrl; App->GetOptions()->GetValue(OPT_EditControl, EditCtrl); LAutoString Sig = GetSig(EditCtrl.CastInt32() != 0, Ident); if (!Sig && EditCtrl.CastInt32()) { Sig = GetSig(false, Ident); SetBody(Sig); } else if (EditCtrl.CastInt32()) { SetHtmlCharset("utf-8"); SetHtml(Sig); } else { SetBodyCharset("utf-8"); SetBody(Sig); } LVariant ClipRecip; if (Options->GetValue(OPT_RecipientFromClipboard, ClipRecip) && ClipRecip.CastInt32()) { LClipBoard Clip(App); char *Txt = Clip.Text(); if (Txt && strchr(Txt, '@') && strlen(Txt) < 100) { for (char *s=Txt; *s; s++) { if (*s == '\n' || *s == '\r') { *s = 0; } } Mailto mt(App, Txt); for (auto a: mt.To) { LDataPropI *OldA = dynamic_cast(a); if (OldA) { LDataPropI *NewA = GetTo()->Create(GetObject()->GetStore()); if (NewA) { NewA->CopyProps(*OldA); GetTo()->Insert(NewA); } } } mt.To.Empty(); if (mt.Subject && !ValidStr(GetSubject())) { SetSubject(mt.Subject); } /* if (_strnicmp(Txt, MailToStr, 7) == 0) { char *Question = strchr(Txt + 7, '?'); if (Question) { *Question = 0; Question++; int Len = strlen(SubjectStr); if (_strnicmp(Question, SubjectStr, Len) == 0) { Subject = NewStr(Question + Len); } } La->Addr = NewStr(Txt + 7); } else { La->Addr = NewStr(Txt); } To.Insert(La); */ } } Update(); } void Mail::Reparse() { #ifdef _DEBUG _debug = true; #endif // This temporarily removes the attachments that will be // deleted by the call to ParseHeaders after this... ClearCachedItems(); Thing::Reparse(); } void Mail::CreateMailHeaders() { LStringPipe Hdrs(256); MailProtocol Protocol; LVariant HideId; App->GetOptions()->GetValue(OPT_HideId, HideId); Protocol.ProgramName = GetFullAppName(!HideId.CastInt32()); LDataI *Obj = GetObject(); if (Obj && ::CreateMailHeaders(App, Hdrs, Obj, &Protocol)) { LAutoString FullHdrs(Hdrs.NewStr()); Obj->SetStr(FIELD_INTERNET_HEADER, FullHdrs); } else LAssert(!"CreateMailHeaders failed."); } bool Mail::OnBeforeSend(ScribeEnvelope *Out) { if (!Out) { LAssert(!"No output envelope."); return false; } // First check the email from address... if (!ValidStr(GetFrom()->GetStr(FIELD_EMAIL))) { LOptionsFile *Options = App->GetOptions(); if (Options) { ScribeAccount *Ident = App->GetAccounts()->ItemAt(App->GetCurrentIdentity()); if (Ident) { LVariant v = Ident->Identity.Email(); GetFrom()->SetStr(FIELD_EMAIL, v.Str()); v = Ident->Identity.Name(); GetFrom()->SetStr(FIELD_NAME, v.Str()); } } } Out->From = GetFromStr(FIELD_EMAIL); LDataIt To = GetTo(); ContactGroup *Group = NULL; for (LDataPropI *t = To->First(); t; t = To->Next()) { LString Addr = t->GetStr(FIELD_EMAIL); if (LIsValidEmail(Addr)) Out->To.New() = Addr; else if ((Group = LookupContactGroup(App, Addr))) { LString::Array a = Group->GetAddresses(); Out->To.Add(a); } } LDataPropI *Root = GetObject()->GetObj(FIELD_MIME_SEG); if (!Root) { LAssert(!"No root element."); return false; } // Check the headers have been created.. if (!Root->GetStr(FIELD_INTERNET_HEADER)) CreateMailHeaders(); Out->MsgId = GetMessageId(true); // Convert the mime stream LMime Mime(ScribeTempPath()); LTempStream Buf(ScribeTempPath()); Store3ToLMime(&Mime, Root); // Do the encode if (!Mime.Text.Encode.Push(&Buf)) { LAssert(!"Mime encode failed."); return false; } Out->References = GetReferences(); Out->FwdMsgId = GetFwdMsgId(); Out->BounceMsgId = GetBounceMsgId(); // Read the resulting string into the output envelope int Sz = (int)Buf.GetSize(); Out->Rfc822.Length(Sz); Buf.SetPos(0); Buf.Read(Out->Rfc822.Get(), Sz); Out->Rfc822.Get()[Sz] = 0; return true; } void Mail::OnAfterSend() { int f = GetFlags(); f |= MAIL_SENT | MAIL_READ; // set sent flag f &= ~MAIL_READY_TO_SEND; // clear read to send flag // LgiTrace("Setting flags: %x\n", f); SetFlags(f); LDateTime n; n.SetNow(); SetDateSent(&n); // LString s = GetDateSent()->Get(); // LgiTrace("Setting sent date: %s\n", s.Get()); Update(); SetDirty(); if (App) { ScribeFolder *Sent = App->GetFolder(FOLDER_SENT, GetObject()); if (Sent) { LArray Items; Items.Add(this); Sent->MoveTo(Items, false); } } } bool Mail::OnBeforeReceive() { return true; } enum MimeDecodeMode { MODE_FIELDS, MODE_WAIT_MSG, MODE_SEGMENT, MODE_WAIT_TYPE, MODE_WAIT_BOUNDRY }; #define MF_UNKNOWN 1 #define MF_SUBJECT 2 #define MF_TO 3 #define MF_FROM 4 #define MF_REPLY_TO 5 #define MF_CONTENT_TYPE 6 #define MF_CC 7 #define MF_CONTENT_TRANSFER_ENCODING 9 #define MF_DATE_SENT 10 #define MF_PRIORITY 11 #define MF_COLOUR 12 #define MF_DISPOSITIONNOTIFICATIONTO 13 void MungCharset(Mail *Msg, bool &HasRealCs, ScribeAccount *Acc) { if (ValidStr(Msg->GetBodyCharset())) { HasRealCs = true; } if (Acc) { // LCharset *Cs = 0; if (Msg->GetBodyCharset() && stristr(Msg->GetBodyCharset(), "ascii") && Acc->Receive.AssumeAsciiCharset().Str()) { Msg->SetBodyCharset(Acc->Receive.AssumeAsciiCharset().Str()); HasRealCs = false; } else if (!ValidStr(Msg->GetBodyCharset()) || !LGetCsInfo(Msg->GetBodyCharset())) { Msg->SetBodyCharset(Acc->Receive.Assume8BitCharset().Str()); HasRealCs = false; } } } bool Mail::OnAfterReceive(LStreamI *Msg) { if (!Msg) { LAssert(!"No input."); return false; } // Clear the codepage setting here so that we can // check later, down the bottom we must set it to // the default if it's not set anywhere along the // way. SetBodyCharset(0); auto obj = GetObject(); if (!obj) { LAssert(!"No storage object."); return false; } if (!obj->SetRfc822(Msg)) { LgiTrace("%s:%i - SetRfc822 failed with '%s'\n", _FL, obj->GetStr(FIELD_ERROR)); return false; } // Now parse them into the meta fields... obj->ParseHeaders(); ScribeAccount *Acc = GetAccountSentTo(); LAutoString ContentType; // Fill out any Contact's TimeZone if missing LAutoString DateStr(InetGetHeaderField(GetInternetHeader(), "Date")); LDateTime DateObj; if (DateStr && DateObj.Decode(DateStr)) { double SenderTz = DateObj.GetTimeZoneHours(); if (SenderTz != 0.0) { ListAddr *Sender = dynamic_cast(GetFrom()); if (Sender) { auto It = Sender->begin(); if (*It) { Contact *c = (*It)->GetContact(); if (c) { const char *Tz = ""; if (!c->Get(OPT_TimeZone, Tz) || atof(Tz) != SenderTz) { char s[32]; sprintf_s(s, sizeof(s), "%.1f", SenderTz); c->Set(OPT_TimeZone, s); c->SetDirty(true); } } } } } } auto BodyText = GetBody(); if (BodyText) { if (!GetBodyCharset()) { if (Acc && Acc->Receive.Assume8BitCharset().Str()) { SetBodyCharset(Acc->Receive.Assume8BitCharset().Str()); } else { SetBodyCharset("us-ascii"); } } LStringPipe NewBody; LArray Files; if (DecodeUuencodedAttachment(obj->GetStore(), Files, &NewBody, BodyText)) { LDataI *AttachPoint = GetFileAttachPoint(); if (AttachPoint) { for (unsigned i=0; iSetStr(FIELD_MIME_TYPE, sAppOctetStream); Files[i]->Save(AttachPoint); } SetDirty(!TestFlag(GetFlags(), MAIL_ATTACHMENTS)); SetFlags(GetFlags() | MAIL_ATTACHMENTS); LAutoString n(NewBody.NewStr()); SetBody(n); Update(); if (Ui) Ui->OnAttachmentsChange(); } } } LDateTime n; n.SetNow(); SetDateReceived(&n); Update(); SetDirty(); // Call any 'after receive' callbacks LArray Callbacks; if (App->GetScriptCallbacks(LMailOnAfterReceive, Callbacks)) { for (auto c: Callbacks) { if (!c->Func) continue; LVirtualMachine Vm; LScriptArguments Args(&Vm); Args.New() = new LVariant(App); Args.New() = new LVariant(this); App->ExecuteScriptCallback(*c, Args); Args.DeleteObjects(); } } return true; } ThingUi *Mail::GetUI() { return Ui; } LVariant Mail::GetServerUid() { LVariant v; if (GetObject()) { auto Type = (Store3Backend)GetObject()->GetInt(FIELD_STORE_TYPE); if (Type == Store3Imap) v = GetObject()->GetInt(FIELD_SERVER_UID); else v = GetObject()->GetStr(FIELD_SERVER_UID); } else LAssert(!"No object."); return v; } bool Mail::SetServerUid(LVariant &v) { if (!GetObject()) return false; Store3Status s; if (GetObject()->GetInt(FIELD_STORE_TYPE) == Store3Imap) s = GetObject()->SetInt(FIELD_SERVER_UID, v.CastInt32()); else s = GetObject()->SetStr(FIELD_SERVER_UID, v.Str()); return s > Store3Error; } bool Mail::SetObject(LDataI *o, bool IsDestructor, const char *File, int Line) { bool b = LDataUserI::SetObject(o, IsDestructor, File, Line); if (b) { // Clear out stale attachment objects... UnloadAttachments(); } return b; } bool Mail::SetUI(ThingUi *new_ui) { MailUi *NewMailUi = dynamic_cast(new_ui); if (NewMailUi == Ui) return true; if (Ui) { LWindow *w = Ui; Ui->SetItem(0); w->Quit(); LAssert(Ui == NULL); } if (new_ui) { Ui = dynamic_cast(new_ui); if (Ui) Ui->SetItem(this); else LAssert(!"Incorrect object."); } return true; } ThingUi *Mail::DoUI(MailContainer *c) { if (App && !Ui) { if (App->InThread()) { Ui = new MailUi(this, c ? c : GetFolder()); } else { App->PostEvent(M_SCRIBE_OPEN_THING, (LMessage::Param)this, 0); } } if (Ui) { #if WINNATIVE SetActiveWindow(Ui->Handle()); #endif } return Ui; } int Mail::Compare(LListItem *t, ssize_t Field) { Thing *T = (Thing*)t->_UserPtr; Mail *m = T ? T->IsMail() : 0; if (m) { static int Fields[] = {FIELD_FROM, FIELD_SUBJECT, FIELD_SIZE, FIELD_DATE_RECEIVED}; if (Field < 0) { auto i = -Field - 1; if (i >= 0 && i < CountOf(Fields)) { Field = Fields[i]; } } LVariant v1, v2; const char *s1 = "", *s2 = ""; switch (Field) { case 0: { break; } case FIELD_TO: { LDataPropI *a1 = GetTo()->First(); if (a1) { if (a1->GetStr(FIELD_NAME)) s1 = a1->GetStr(FIELD_NAME); else if (a1->GetStr(FIELD_EMAIL)) s1 = a1->GetStr(FIELD_EMAIL); } LDataPropI *a2 = m->GetTo()->First(); if (a2) { if (a2->GetStr(FIELD_NAME)) s2 = a2->GetStr(FIELD_NAME); else if (a2->GetStr(FIELD_EMAIL)) s2 = a2->GetStr(FIELD_EMAIL); } break; } case FIELD_FROM: { LDataPropI *f1 = GetFrom(); LDataPropI *f2 = m->GetFrom(); if (f1->GetStr(FIELD_NAME)) s1 = f1->GetStr(FIELD_NAME); else if (f1->GetStr(FIELD_EMAIL)) s1 = f1->GetStr(FIELD_EMAIL); if (f2->GetStr(FIELD_NAME)) s2 = f2->GetStr(FIELD_NAME); else if (f2->GetStr(FIELD_EMAIL)) s2 = f2->GetStr(FIELD_EMAIL); break; } case FIELD_SUBJECT: { s1 = GetSubject(); s2 = m->GetSubject(); break; } case FIELD_SIZE: { return (int) (TotalSizeof() - m->TotalSizeof()); break; } case FIELD_DATE_SENT: { auto Sent1 = GetDateSent(); auto Sent2 = m->GetDateSent(); if (!Sent1 || !Sent2) break; return Sent1->Compare(Sent2); break; } case FIELD_DATE_RECEIVED: { return GetDateReceived()->Compare(m->GetDateReceived()); break; } case FIELD_PRIORITY: { return GetPriority() - m->GetPriority(); break; } case FIELD_FLAGS: { int Mask = MAIL_FORWARDED | MAIL_REPLIED | MAIL_READ; return (GetFlags() & Mask) - (m->GetFlags() & Mask); break; } case FIELD_LABEL: { if (GetLabel()) s1 = GetLabel(); if (m->GetLabel()) s2 = m->GetLabel(); break; } case FIELD_MESSAGE_ID: { s1 = GetMessageId(); s2 = m->GetMessageId(); break; } case FIELD_FROM_CONTACT_NAME: { s1 = GetFieldText(FIELD_FROM_CONTACT_NAME); s2 = m->GetFieldText(FIELD_FROM_CONTACT_NAME); break; } case FIELD_SERVER_UID: { v1 = GetServerUid(); v2 = m->GetServerUid(); if (v1.Str() && v2.Str()) { s1 = v1.Str(); s2 = v2.Str(); } else { auto diff = v1.CastInt64() - v2.CastInt64(); if (diff < 0) return -1; return diff > 1; } } default: { s1 = GetObject() ? GetObject()->GetStr((int)Field) : 0; s2 = m->GetObject() ? m->GetObject()->GetStr((int)Field) : 0; break; } } const char *Empty = ""; return _stricmp(s1?s1:Empty, s2?s2:Empty); } return -1; } class MailPropDlg : public LDialog { List Lst; LViewI *Table = NULL; struct FlagInfo { int Flag = 0, Ctrl = 0; void Set(int f, int c) { Flag = f; Ctrl = c; } }; LArray Flags; public: MailPropDlg(LView *Parent, List &lst) { SetParent(Parent); Lst = lst; Flags.New().Set(MAIL_SENT, IDC_SENT); Flags.New().Set(MAIL_RECEIVED, IDC_RECEIVED); Flags.New().Set(MAIL_CREATED, IDC_CREATED); Flags.New().Set(MAIL_FORWARDED, IDC_FORWARDED); Flags.New().Set(MAIL_REPLIED, IDC_REPLIED); Flags.New().Set(MAIL_ATTACHMENTS, IDC_HAS_ATTACH); Flags.New().Set(MAIL_READ, IDC_READ); Flags.New().Set(MAIL_READY_TO_SEND, IDC_READY_SEND); if (LoadFromResource(IDD_MAIL_PROPERTIES)) { GetViewById(IDC_TABLE, Table); LAssert(Table != NULL); SetCtrlEnabled(IDC_OPEN_INSPECTOR, Lst.Length() == 1); for (auto &i: Flags) { int Set = 0; for (auto m: Lst) { if (m->GetFlags() & i.Flag) Set++; } if (Set == Lst.Length()) { SetCtrlValue(i.Ctrl, LCheckBox::CheckOn); } else if (Set) { LCheckBox *Cb; if (GetViewById(i.Ctrl, Cb)) Cb->ThreeState(true); SetCtrlValue(i.Ctrl, LCheckBox::CheckPartial); } } SetCtrlEnabled(IDC_HAS_ATTACH, false); char Msg[512] = ""; if (Lst.Length() == 1) { Mail *m = Lst[0]; auto mObj = m->GetObject(); if (mObj) { auto dt = mObj->GetDate(FIELD_DATE_RECEIVED); sprintf_s( Msg, sizeof(Msg), LLoadString(IDS_MAIL_PROPS_DLG), LFormatSize(mObj->GetInt(FIELD_SIZE)).Get(), dt ? dt->Get().Get() : LLoadString(IDS_NONE), mObj->GetStr(FIELD_DEBUG)); SetCtrlName(IDC_MSG_ID, mObj->GetStr(FIELD_MESSAGE_ID)); } } else { sprintf_s(Msg, sizeof(Msg), LLoadString(IDS_MULTIPLE_ITEMS), Lst.Length()); SetCtrlEnabled(IDC_MSG_ID, false); } SetCtrlName(IDC_MAIL_DESCRIPTION, Msg); MoveSameScreen(Parent); } } void OnPosChange() { if (Table) { LRect r = GetClient(); r.Inset(LTableLayout::CellSpacing, LTableLayout::CellSpacing); Table->SetPos(r); } } int OnNotify(LViewI *Ctr, LNotification n) { switch (Ctr->GetId()) { case IDC_OPEN_INSPECTOR: { if (Lst.Length()) Lst[0]->OnInspect(); break; } case IDOK: { for (auto &i: Flags) { auto v = GetCtrlValue(i.Ctrl); LAssert(v >= 0); // the control couldn't be found... check the .lr8 file for (auto m: Lst) { if (v == 1) m->SetFlags(m->GetFlags() | i.Flag); else if (v == 0) m->SetFlags(m->GetFlags() & ~i.Flag); } } // fall thru } case IDCANCEL: { EndModal(Ctr->GetId()); break; } } return 0; } }; void Mail::OnInspect() { new ObjectInspector(App, this); } void Mail::OnProperties(int Tab) { List Sel; if (GetList()->GetSelection(Sel)) { List Lst; for (auto i: Sel) { Lst.Insert(dynamic_cast(i)); } if (Lst[0]) { auto Dlg = new MailPropDlg(GetList(), Lst); Dlg->DoModal([this, Dlg](auto dlg, auto id) { if (id == IDOK) { SetDirty(); Update(); } delete dlg; }); } } } int Mail::GetImage(int SelFlags) { if (TestFlag(GetFlags(), MAIL_READY_TO_SEND) && !TestFlag(GetFlags(), MAIL_SENT)) { return ICON_UNSENT_MAIL; } if (GetFlags() & MAIL_READ) { return (GetFlags() & MAIL_ATTACHMENTS) ? ICON_READ_ATT_MAIL : ICON_READ_MAIL; } else { return (GetFlags() & MAIL_ATTACHMENTS) ? ICON_UNREAD_ATT_MAIL : ICON_UNREAD_MAIL; } return ICON_READ_MAIL; } int *Mail::GetDefaultFields() { return DefaultMailFields; } void Mail::DeleteAsSpam(LView *View) { // Set read.. SetFlags(GetFlags() | MAIL_READ | MAIL_BAYES_SPAM, true); // Remove from NewMail NewMailLst.Delete(this); LVariant DeleteOnServer, DeleteAttachments, SetRead; auto Opts = App->GetOptions(); if (!Opts->GetValue(OPT_BayesDeleteOnServer, DeleteOnServer)) DeleteOnServer = false; if (!Opts->GetValue(OPT_BayesDeleteAttachments, DeleteAttachments)) DeleteAttachments = false; if (!Opts->GetValue(OPT_BayesSetRead, SetRead)) SetRead = false; if (DeleteOnServer.CastBool()) { // Tell the account it's spam... so that any further connects can // delete it off the server. ScribeAccount *a = GetAccountSentTo(); if (a) { auto ServerUid = GetServerUid(); if (ServerUid.Str()) { a->Receive.DeleteAsSpam(ServerUid.Str()); - SetServerUid(ServerUid = NULL); + ServerUid.Empty(); + SetServerUid(ServerUid); } else { LAutoString Uid(InetGetHeaderField(GetInternetHeader(), "X-UIDL")); if (Uid) a->Receive.DeleteAsSpam(Uid); } } else { #if 0 // def _DEBUG if (LgiMsg(Ui?(LView*)Ui:(LView*)App, "Debug: GetAccountSentTo failed. Debug?", AppName, MB_YESNO) == IDYES) { LAssert(0); } #endif } } if (DeleteAttachments.CastBool()) { // Delete all attachments... they're useless and more than likely // just virii anyway. List Files; if (GetAttachments(&Files)) { for (auto a: Files) { DeleteAttachment(a); } Files.Empty(); } } if (SetRead.CastInt32() != 0) { SetFlags(GetFlags() | MAIL_READ); } // Move it to the spam folder if it exists. auto FolderPath = GetFolder()->GetPath(); auto Parts = FolderPath.SplitDelimit("/"); if (Parts.Length() == 0) LgiMsg(View, "Error: No folder path?", AppName); else { LString SpamPath; LString SpamLeaf = "Spam"; SpamPath.Printf("/%s/%s", Parts[0].Get(), SpamLeaf.Get()); ScribeFolder *Spam = App->GetFolder(SpamPath); if (!Spam) { LMailStore *Ms = App->GetMailStoreForPath(FolderPath); if (!Ms) { if (GetFolder()->GetObject()->GetInt(FIELD_STORE_TYPE) == Store3Imap) { Ms = App->GetDefaultMailStore(); if (Ms && Ms->Root) { Spam = Ms->Root->GetSubFolder(SpamLeaf); if (!Spam) Spam = Ms->Root->CreateSubFolder(SpamLeaf, MAGIC_MAIL); } } else LgiMsg(View, "Error: Couldn't get mail store for '%s'.", AppName, MB_OK, FolderPath.Get()); } else Spam = Ms->Root->CreateSubFolder("Spam", MAGIC_MAIL); } if (Spam && Spam != GetFolder()) { LArray Items; Items.Add(this); Spam->MoveTo(Items, false, [View](auto result, auto status) { if (!result) LgiMsg(View, "Error: Couldn't move email to spam folder.", AppName); }); } } } void Mail::SetFlagsCache(int64_t NewFlags, bool IgnoreReceipt, bool UpdateScreen) { if (FlagsCache == NewFlags) return; DepthCheck Depth(d->InSetFlagsCache); bool Read1 = TestFlag(FlagsCache, MAIL_READ); bool Read2 = TestFlag(NewFlags, MAIL_READ); bool ChangeRead = Read1 ^ Read2; // LgiTrace("%p::SetFlagsCache: %s -> %s\n", this, EmailFlagsToStr(FlagsCache).Get(), EmailFlagsToStr(StoreFlags).Get()); FlagsCache = NewFlags; if (ChangeRead && App) { if (Read2) { // Becoming read List Objs; Objs.Insert(this); App->OnNewMail(&Objs, false); PreviewCache.DeleteObjects(); // Read receipt if (!IgnoreReceipt && !TestFlag(NewFlags, MAIL_CREATED) && TestFlag(NewFlags, MAIL_READ_RECEIPT)) { LAutoString Header(InetGetHeaderField(GetInternetHeader(), "Disposition-Notification-To")); if (Header) { LAutoString Name, Addr; DecodeAddrName(Header, Name, Addr, 0); if (Addr) { if (LgiMsg( Ui != 0 ? (LView*)Ui : (LView*)App, LLoadString(IDS_RECEIPT_ASK), AppName, MB_YESNO, GetSubject(), GetFrom()->GetStr(FIELD_NAME), GetFrom()->GetStr(FIELD_EMAIL), (char*)Name, (char*)Addr) == IDYES) { Mail *m = new Mail(App); if (m) { m->App = App; LDataPropI *ToAddr = m->GetTo()->Create(m->GetObject()->GetStore()); if (ToAddr) { ToAddr->SetStr(FIELD_NAME, Name); ToAddr->SetStr(FIELD_EMAIL, Addr); m->GetTo()->Insert(ToAddr); } m->OnReceipt(this); m->Save(); App->Send(); } } } } } LVariant Inc; if (App->GetOptions()->GetValue(OPT_BayesIncremental, Inc) && Inc.CastInt32()) { // Incremental bayesian update App->OnBayesianMailEvent(this, BayesMailUnknown, BayesMailHam); } } if (UpdateScreen) { // Changing read status ScribeFolder *t = GetFolder(); if (t) t->OnUpdateUnRead(0, true); } } if (UpdateScreen || ChangeRead) { Update(); } } uint32_t Mail::GetFlags() { if (GetObject()) { // Make sure the objects are in sync... auto StoreFlags = GetObject()->GetInt(FIELD_FLAGS); if (FlagsCache < 0) FlagsCache = StoreFlags; else if (FlagsCache != StoreFlags) SetFlagsCache(StoreFlags, false, true); } return (uint32_t)FlagsCache; } void Mail::SetFlags(ulong NewFlags, bool IgnoreReceipt, bool UpdateScreen) { if (FlagsCache == NewFlags) return; DepthCheck SetFlagsDepth(d->InSetFlags); if (GetObject()) { Store3Status Result = GetObject()->SetInt(FIELD_FLAGS, NewFlags); if (Result == Store3Success && GetObject()->IsOnDisk()) { // Imap mail shouldn't fall in here. SetDirty(); } } else { LAssert(0); return; } SetFlagsCache(NewFlags, IgnoreReceipt, UpdateScreen); } const char *Mail::GetFieldText(int Field) { static char Buf[512]; switch (Field) { case FIELD_TO: { size_t ch = 0; Buf[0] = 0; LDataIt To = GetTo(); for (LDataPropI *a=To->First(); a; a=To->Next()) { if (ch > 0) ch += sprintf_s(Buf+ch, sizeof(Buf)-ch, ", "); auto Name = a->GetStr(FIELD_NAME); auto Addr = a->GetStr(FIELD_EMAIL); auto n = Name ? Name : Addr; if (!n) continue; // Is the buffer too small? size_t n_len = strlen(n); if (ch + n_len + 8 >= sizeof(Buf)) { // Yes... just truncate it with "..." and bail. ch += sprintf_s(Buf+ch, sizeof(Buf)-ch, "..."); break; } ch += sprintf_s(Buf+ch, sizeof(Buf)-ch, "%s", n); LAssert(ch < sizeof(Buf) - 1); } return Buf; break; } case FIELD_FROM_CONTACT_NAME: { static bool InMethod = false; if (!InMethod) { InMethod = true; LDataPropI *la = GetFrom(); if (la) { auto Email = la->GetStr(FIELD_EMAIL); Contact *c = Contact::LookupEmail(Email); if (c) { const char *f = 0, *l = 0; c->Get(OPT_First, f); c->Get(OPT_Last, l); if (f && l) sprintf_s(Buf, sizeof(Buf), "%s %s", f, l); else if (f) strcpy_s(Buf, sizeof(Buf), f); else if (l) strcpy_s(Buf, sizeof(Buf), l); else Buf[0] = 0; InMethod = false; return Buf; } } InMethod = false; } else { LAssert(0); } // fall through } case FIELD_FROM: { LDataPropI *f = GetFrom(); auto n = f->GetStr(FIELD_NAME); if (n) return n; auto e = f->GetStr(FIELD_EMAIL); if (e) return e; break; } case FIELD_SUBJECT: { auto s = GetSubject(); if (Strchr(s, '\n')) { char *e = Buf + sizeof(Buf) - 1; char *o = Buf; for (auto i = s; o 4 + 3) { int digits = ch - 4; char *s = Buf + ((digits % 3) ? digits % 3 : 3); char *e = Buf + ch - 4; while (s < e) { memmove(s + 1, s, strlen(s)+1); *s = ','; s += 3; } } } else LFormatSize(Buf, sizeof(Buf), TotalSizeCache); return Buf; } case FIELD_DATE_SENT: { auto DateSent = GetDateSent(); if (DateSent->Year()) { LDateTime dt = *DateSent; if (GetObject() && GetObject()->GetInt(FIELD_STORE_TYPE) == Store3Imap) { ValidateImapDate(dt); } if (AdjustDateTz) { if (dt.GetTimeZone() != LDateTime::SystemTimeZone()) dt.SetTimeZone(LDateTime::SystemTimeZone(), true); } if (ShowRelativeDates) { auto rel = RelativeTime(dt); strcpy_s(Buf, sizeof(Buf), rel ? rel : "#error:RelativeTime"); } else { dt.Get(Buf, sizeof(Buf)); } } else { sprintf_s(Buf, sizeof(Buf), "(%s)", LLoadString(IDS_NONE, "None")); } return Buf; } case FIELD_DATE_RECEIVED: { auto DateReceived = GetDateReceived(); if (DateReceived->Year()) { LDateTime dt = *DateReceived; if (AdjustDateTz) { if (dt.GetTimeZone() != LDateTime::SystemTimeZone()) dt.SetTimeZone(LDateTime::SystemTimeZone(), true); } dt.Get(Buf, sizeof(Buf)); if (ShowRelativeDates) { auto rel = RelativeTime(dt); strcpy_s(Buf, sizeof(Buf), rel ? rel : "#error:RelativeTime"); } else { dt.Get(Buf, sizeof(Buf)); } } else { sprintf_s(Buf, sizeof(Buf), "(%s)", LLoadString(IDS_NONE, "None")); } return Buf; } case FIELD_TEXT: return GetBody(); case FIELD_MESSAGE_ID: return GetMessageId(); case FIELD_INTERNET_HEADER: return GetInternetHeader(); case FIELD_ALTERNATE_HTML: return GetHtml(); case FIELD_LABEL: return GetLabel(); case FIELD_PRIORITY: case FIELD_FLAGS: return 0; case FIELD_SERVER_UID: sprintf_s(Buf, sizeof(Buf), "%i", GetObject() ? (int)GetObject()->GetInt(Field) : -1); return Buf; case FIELD_RECEIVED_DOMAIN: { if (!d->DomainCache) { if (!GetObject()) return NULL; auto hdr = GetObject()->GetStr(FIELD_INTERNET_HEADER); if (!hdr) return NULL; for (auto ln: LString(hdr).SplitDelimit("\n")) { if (ln.Find("Received: from") == 0) { auto p = ln.SplitDelimit(); if (p.Length() > 2) { auto t = p[2]; if (t.Find(".") > 0) d->DomainCache = t.Strip("[]"); } } } } return d->DomainCache; } default: return GetObject() ? GetObject()->GetStr(Field) : NULL; } return 0; } int Mail::DefaultMailFields[] = { /* FIELD_FLAGS, FIELD_PRIORITY, */ FIELD_FROM, FIELD_SUBJECT, FIELD_SIZE, FIELD_DATE_SENT }; const char *Mail::GetText(int i) { if (FieldArray.Length()) { if (i >= 0 && i < (int)FieldArray.Length()) return GetFieldText(FieldArray[i]); } else if (i < CountOf(DefaultMailFields)) { return GetFieldText(DefaultMailFields[i]); } return NULL; } LDataI *Mail::GetFileAttachPoint() { if (!GetObject()) { LAssert(!"No object ptr."); return 0; } auto Store = GetObject()->GetStore(); // Check existing root node for "multipart/mixed"? auto r = dynamic_cast(GetObject()->GetObj(FIELD_MIME_SEG)); if (!r) { // Create one... r = Store->Create(MAGIC_ATTACHMENT); if (!r) { LAssert(!"No MIME segment ptr."); return NULL; } r->SetStr(FIELD_MIME_TYPE, sMultipartMixed); if (!GetObject()->SetObj(FIELD_MIME_SEG, r)) { LAssert(!"Can't set MIME segment."); return NULL; } } auto Mt = r->GetStr(FIELD_MIME_TYPE); if (!Stricmp(Mt, sMultipartMixed)) { // Yes is it mixed... return that... return r; } // No, a different type of segment, make the parent segment a "mixed", and attach the old segment to the mixed auto Mixed = Store->Create(MAGIC_ATTACHMENT); if (!Mixed) { LAssert(!"Failed to create MAGIC_ATTACHMENT."); return NULL; } // Set the type... Mixed->SetStr(FIELD_MIME_TYPE, sMultipartMixed); // Re-parent the content to the mixed if (!r->Save(Mixed)) { LAssert(!"Can't reparent the content into the mixed seg."); return NULL; } // Re-parent the mixed to the mail object if (!Mixed->Save(GetObject())) { LAssert(!"Can't reparent the mixed seg into the mail object."); return NULL; } return Mixed; } bool Mail::AttachFile(Attachment *File) { bool Status = false; if (File && !Attachments.HasItem(File)) { LDataI *AttachPoint = GetFileAttachPoint(); if (AttachPoint) { if (File->GetObject()->Save(AttachPoint)) { File->App = App; File->SetOwner(this); Attachments.Insert(File); Status = true; SetDirty(!TestFlag(GetFlags(), MAIL_ATTACHMENTS)); SetFlags(GetFlags() | MAIL_ATTACHMENTS); Update(); if (Ui) { Ui->OnAttachmentsChange(); } } } else { File->DecRef(); } } return Status; } Attachment *Mail::AttachFile(LView *Parent, const char *FileName) { LString MimeType = ScribeGetFileMimeType(FileName); LVariant Resize = false; if (!Strnicmp(MimeType.Get(), "image/", 6)) { // Check if we have to do a image resize App->GetOptions()->GetValue(OPT_ResizeImgAttachments, Resize); } auto AttachPoint = GetFileAttachPoint(); if (!AttachPoint) { LAssert(!"No attachment point in MIME heirarchy for file"); return NULL; } auto File = new Attachment( App, GetObject()->GetStore()->Create(MAGIC_ATTACHMENT), FileName); if (File) { File->App = App; if (Resize.CastInt32() || File->GetSize() > 0) { File->SetOwner(this); Attachments.Insert(File); if (Resize.CastInt32()) d->AddResizeImage(File); else File->GetObject()->Save(AttachPoint); SetFlags(GetFlags() | MAIL_ATTACHMENTS); Update(); if (Ui) Ui->OnAttachmentsChange(); } else { LgiMsg(Parent, LLoadString(IDS_ERROR_FILE_EMPTY), AppName); File->DecRef(); File = NULL; } } return File; } bool Mail::Save(ScribeFolder *Into) { bool Status = false; if (!Into) { if (GetFolder()) Into = GetFolder(); else Into = App->GetFolder(FOLDER_OUTBOX); } if (Into) { ScribeFolder *Old = GetFolder(); if (!GetFolder()) { SetParentFolder(Into); } else if (GetFolder() != Into) { // If this fails, you should really by using ScribeFolder::MoveTo to move // the item from it's existing location to the new folder... LAssert(!"Use MoveTo instead."); return false; } Store3Status Result = Into->WriteThing(this); if (Result != Store3Error) { Status = true; SetDirty(false); if (Into && Into == App->GetFolder(FOLDER_TEMPLATES, NULL, true)) { // saving into the templates folder... update the menu App->BuildDynMenus(); } if (Status && DropFileName) { FileDev->Delete(DropFileName, false); DropFileName.Reset(); } } else { SetParentFolder(Old); } } // This frees the resizer thread... // so they aren't hanging around pointlessly d->OnSave(); return Status; } struct WrapQuoteBlock { int Depth; LArray Text; }; void WrapAndQuote( LStream &Pipe, const char *Quote, int Wrap, const char *Text, const char *Cp, const char *MimeType) { int IsHtml = MimeType && !_stricmp(MimeType, sTextHtml); LAutoString In((char*) LNewConvertCp("utf-8", Text, Cp ? Cp : (char*)"utf-8")); const char *QuoteStr = Quote; if (In) { size_t QuoteChars = Strlen(QuoteStr); char NewLine[8]; int NewLineLen = sprintf_s(NewLine, sizeof(NewLine), "%s", IsHtml ? "
\n" : "\n"); // Two step process, parse all the input into an array of paragraph blocks and then output each block // in wrapped form LArray Para; // 1) Parse all input into paragraphs char *i = In; while (*i) { // Parse out the next line... char *e = i; while (*e && *e != '\n') e++; ssize_t len = e - i; int depth = 0; for (int n=0; n') depth++; else if (i[n] != ' ' || i[n] != '\t') break; } if (Para.Length()) { // Can this be added to the previous paragraph? WrapQuoteBlock &Prev = Para[Para.Length()-1]; if (Prev.Depth == depth) { // Yes?! Prev.Text.Add(NewStr(i, len)); i = *e ? e + 1 : e; continue; } } // Create a new paragraph WrapQuoteBlock &New = Para.New(); New.Depth = depth; New.Text.Add(NewStr(i, len)); // Move current position to next line i = *e ? e + 1 : e; } // 2) Output all paragraphs const char *PrefixCharacters = "> \t"; for (unsigned n=0; n 0); if (WordLen == 0) break; // This won't end well... if (Wrap > 0) { // Check if we can put this word on the current line without making it longer than 'Wrap' if (CharPos + WordLen > Wrap) { // No it won't fit... so emit [NewLine][QuoteStr][Prefix] Pipe.Write(NewLine, NewLineLen); if (QuoteStr) Pipe.Write(QuoteStr, QuoteChars * sizeof(*QuoteStr)); Pipe.Write(Prefix, PrefixChars * sizeof(*Prefix)); // Now we are ready to write more words... CharPos = QuoteChars + PrefixChars; } // else Yes it fits... } // Write out the word... if (IsHtml && Strnchr(Start, '<', WordLen)) { for (auto *c = Start; c < Start + WordLen; c++) if (*c == '<') Pipe.Print("<"); else if (*c == '>') Pipe.Print(">"); else Pipe.Write(c, sizeof(*c)); } else Pipe.Write(Start, WordLen * sizeof(*Start)); CharPos += WordLen; Start = End; } if (HardNewLine) { Pipe.Write(NewLine, NewLineLen); if (QuoteStr) Pipe.Write(QuoteStr, QuoteChars * sizeof(*QuoteStr)); Pipe.Write(Prefix, PrefixChars * sizeof(*Prefix)); CharPos = QuoteChars + PrefixChars; } } // Finish the paragraph Pipe.Write(NewLine, NewLineLen); } } } void Mail::WrapAndQuote(LStringPipe &p, const char *Quote, int WrapAt) { LString Mem; LAutoString Cp; if (!ValidStr(GetBody())) Mem = HtmlToText(GetHtml(), GetHtmlCharset()); else Cp = GetCharSet(); ::WrapAndQuote(p, Quote, WrapAt > 0 ? WrapAt : 76, Mem ? Mem.Get() : GetBody(), Cp); } LAutoString Mail::GetSig(bool HtmlVersion, ScribeAccount *Account) { LAutoString r; LVariant Xml; if (!Account) Account = GetAccountSentTo(); if (Account) { if (HtmlVersion) Xml = Account->Identity.HtmlSig(); else Xml = Account->Identity.TextSig(); } if (Xml.Str()) { r = App->ProcessSig(this, Xml.Str(), HtmlVersion ? sTextHtml : sTextPlain); } return r; } void Mail::ProcessTextForResponse(Mail *From, LOptionsFile *Options, ScribeAccount *Account) { LStringPipe Temp; LVariant QuoteStr, Quote, WrapAt; Options->GetValue(OPT_QuoteReply, Quote); Options->GetValue(OPT_WrapAtColumn, WrapAt); Options->GetValue(OPT_QuoteReplyStr, QuoteStr); From->WrapAndQuote(Temp, Quote.CastInt32() ? QuoteStr.Str() : NULL, WrapAt.CastInt32()); LAutoString s = From->GetSig(false, Account); if (s) Temp.Write(s, strlen(s)); s.Reset(Temp.NewStr()); SetBody(s); SetBodyCharset("utf-8"); } ScribeAccount *Mail::GetAccountSentTo() { ScribeAccount *Account = App->GetAccountById(GetAccountId()); if (!Account) { // Older style lookup based on to address... for (auto a : *App->GetAccounts()) { LVariant e = a->Identity.Email(); if (ValidStr(e.Str())) { for (LDataPropI *t=GetTo()->First(); t; t=GetTo()->Next()) { auto Addr = t->GetStr(FIELD_EMAIL); if (ValidStr(Addr)) { if (_stricmp(Addr, e.Str()) == 0) { Account = a; break; } } } } } } return Account; } void Mail::OnReceipt(Mail *m) { if (m) { SetFlags(MAIL_CREATED | MAIL_READY_TO_SEND); ScribeAccount *MatchedAccount = m->GetAccountSentTo(); if (!MatchedAccount) { MatchedAccount = App->GetAccounts()->ItemAt(App->GetCurrentIdentity()); } if (MatchedAccount) { GetFrom()->SetStr(FIELD_NAME, MatchedAccount->Identity.Name().Str()); GetFrom()->SetStr(FIELD_EMAIL, MatchedAccount->Identity.Email().Str()); } char Sent[64]; m->GetDateReceived()->Get(Sent, sizeof(Sent)); char Read[64]; LDateTime t; t.SetNow(); t.Get(Read, sizeof(Read)); char s[256]; sprintf_s(s, sizeof(s), LLoadString(IDS_RECEIPT_BODY), GetFrom()->GetStr(FIELD_NAME), GetFrom()->GetStr(FIELD_EMAIL), Sent, m->GetSubject(), Read); SetBody(s); sprintf_s(s, sizeof(s), "Read: %s", m->GetSubject() ? m->GetSubject() : (char*)""); SetSubject(s); } } LString Mail::GetMailRef() { LString Ret; if (auto MsgId = GetMessageId(true)) { LAssert(!strchr(MsgId, '\n')); ScribeFolder *Fld = GetFolder(); LString FldPath; if (Fld) FldPath = Fld->GetPath(); if (FldPath) { LUri u; auto a = u.EncodeStr(MsgId, MsgIdEncodeChars); if (a) Ret.Printf("%s/%s", FldPath.Get(), a.Get()); } else { Ret = MsgId; } } return Ret; } void Mail::OnReply(Mail *m, bool All, bool MarkOriginal) { if (!m) return; SetWillDirty(false); LVariant ReplyAllSetting = MAIL_ADDR_TO; if (All) { App->GetOptions()->GetValue(OPT_DefaultReplyAllSetting, ReplyAllSetting); } if (MarkOriginal) { // source email has been replyed to m->SetFlags(m->GetFlags() | MAIL_READ); } // this email is ready to send SetFlags(GetFlags() | MAIL_READ | MAIL_CREATED); LDataPropI *ToAddr = GetTo()->Create(GetObject()->GetStore()); if (ToAddr) { bool CopyStatus; auto From = m->GetFrom(); auto Reply = m->GetReply(); auto ReplyTo = Reply->GetStr(FIELD_EMAIL); if (ReplyTo) CopyStatus = ToAddr->CopyProps(*Reply); else CopyStatus = ToAddr->CopyProps(*From); LAssert(CopyStatus); ToAddr->SetInt(FIELD_CC, ReplyAllSetting.CastInt32()); GetTo()->Insert(ToAddr); } LVariant UserName, EmailAddr, ReplyTo; ScribeAccount *a = App->GetCurrentAccount(); if (a) { UserName = a->Identity.Name(); EmailAddr = a->Identity.Email(); ReplyTo = a->Identity.ReplyTo(); } else LAssert(!"No current account."); if (All) { for (LDataPropI *a = m->GetTo()->First(); a; a = m->GetTo()->Next()) { bool Same = App->IsMyEmail(a->GetStr(FIELD_EMAIL)); bool AlreadyThere = false; for (LDataPropI *r = GetTo()->First(); r; r=GetTo()->Next()) { if (_stricmp(r->GetStr(FIELD_EMAIL), a->GetStr(FIELD_EMAIL)) == 0) { AlreadyThere = true; break; } } if (!Same && !AlreadyThere) { LDataPropI *New = GetTo()->Create(GetObject()->GetStore()); if (New) { New->SetInt(FIELD_CC, ReplyAllSetting.CastInt32()); New->SetStr(FIELD_EMAIL, a->GetStr(FIELD_EMAIL)); New->SetStr(FIELD_NAME, a->GetStr(FIELD_NAME)); GetTo()->Insert(New); } } } } ScribeAccount *MatchedAccount = m->GetAccountSentTo(); if (MatchedAccount && MatchedAccount->Identity.Email().Str()) { GetFrom()->SetStr(FIELD_NAME, MatchedAccount->Identity.Name().Str()); GetFrom()->SetStr(FIELD_EMAIL, MatchedAccount->Identity.Email().Str()); ReplyTo = MatchedAccount->Identity.ReplyTo(); if (ReplyTo.Str()) { GetReply()->SetStr(FIELD_NAME, MatchedAccount->Identity.Name().Str()); GetReply()->SetStr(FIELD_EMAIL, ReplyTo.Str()); } } else { GetFrom()->SetStr(FIELD_NAME, UserName.Str()); GetFrom()->SetStr(FIELD_EMAIL, EmailAddr.Str()); if (ValidStr(ReplyTo.Str())) { GetReply()->SetStr(FIELD_NAME, UserName.Str()); GetReply()->SetStr(FIELD_EMAIL, ReplyTo.Str()); } } char Re[32]; sprintf_s(Re, sizeof(Re), "%s:", LLoadString(IDS_REPLY_PREFIX)); auto SrcSubject = (m->GetSubject()) ? m->GetSubject() : ""; if (_strnicmp(SrcSubject, Re, strlen(Re)) != 0) { size_t Len = strlen(SrcSubject) + strlen(Re) + 2; char *s = new char[Len]; if (s) { sprintf_s(s, Len, "%s %s", Re, SrcSubject); SetSubject(s); DeleteArray(s); } } else { SetSubject(m->GetSubject()); } auto EditMimeType = App->EditCtrlMimeType(); auto Xml = App->GetReplyXml(EditMimeType); if (ValidStr(Xml)) { RemoveReturns(Xml); PreviousMail = m; LString Body = App->ProcessReplyForwardTemplate(m, this, Xml, Cursor, EditMimeType); LVariant EditCtrl; if (App->GetOptions()->GetValue(OPT_EditControl, EditCtrl) && EditCtrl.CastInt32()) { SetHtml(Body); } else { SetBody(Body); SetBodyCharset("utf-8"); } PreviousMail = 0; } else { ProcessTextForResponse(m, App->GetOptions(), MatchedAccount); } // Reference SetReferences(0); auto MailRef = m->GetMailRef(); if (MailRef) SetReferences(MailRef); GetMessageId(true); SetWillDirty(true); ScribeFolder *Folder = m->GetFolder(); if (Folder && Folder->IsPublicFolders()) { SetParentFolder(Folder); } } bool Mail::OnForward(Mail *m, bool MarkOriginal, int WithAttachments) { if (!m) { LAssert(!"No mail object."); LgiTrace("%s:%i - No mail object.\n", _FL); return false; } // ask about attachments? List Attach; if (m->GetAttachments(&Attach) && Attach.Length() > 0) { if (WithAttachments < 0) { LView *p = (m->Ui)?(LView*)m->Ui:(LView*)App; int Result = LgiMsg(p, LLoadString(IDS_ASK_FORWARD_ATTACHMENTS), AppName, MB_YESNOCANCEL); if (Result == IDYES) { WithAttachments = 1; } else if (Result == IDCANCEL) { return false; } } } if (MarkOriginal) { // source email has been forwarded auto MailRef = m->GetMailRef(); if (MailRef) SetFwdMsgId(MailRef); } // this email is ready to send SetFlags(GetFlags() | MAIL_CREATED); SetDirty(); LVariant UserName, EmailAddr, ReplyTo; ScribeAccount *a = App->GetCurrentAccount(); if (a) { UserName = a->Identity.Name(); EmailAddr = a->Identity.Email(); ReplyTo = a->Identity.ReplyTo(); } else LAssert(!"No current account."); ScribeAccount *MatchedAccount = m->GetAccountSentTo(); if (MatchedAccount && MatchedAccount->Identity.Email().Str()) { GetFrom()->SetStr(FIELD_NAME, MatchedAccount->Identity.Name().Str()); GetFrom()->SetStr(FIELD_EMAIL, MatchedAccount->Identity.Email().Str()); ReplyTo = MatchedAccount->Identity.ReplyTo(); if (ValidStr(ReplyTo.Str())) { GetReply()->SetStr(FIELD_NAME, MatchedAccount->Identity.Name().Str()); GetReply()->SetStr(FIELD_EMAIL, ReplyTo.Str()); } } else { GetFrom()->SetStr(FIELD_NAME, UserName.Str()); GetFrom()->SetStr(FIELD_EMAIL, EmailAddr.Str()); if (ValidStr(ReplyTo.Str())) { GetReply()->SetStr(FIELD_NAME, UserName.Str()); GetReply()->SetStr(FIELD_EMAIL, ReplyTo.Str()); } } SetBodyCharset(0); char PostFix[32]; sprintf_s(PostFix, sizeof(PostFix), "%s:", LLoadString(IDS_FORWARD_PREFIX)); auto SrcSubject = (m->GetSubject()) ? m->GetSubject() : ""; if (_strnicmp(SrcSubject, PostFix, strlen(PostFix)) != 0) { size_t Len = strlen(SrcSubject) + strlen(PostFix) + 2; char *s = new char[Len]; if (s) { sprintf_s(s, Len, "%s %s", PostFix, SrcSubject); SetSubject(s); DeleteArray(s); } } else { SetSubject(m->GetSubject()); } // Wrap/Quote/Sig the text... const char *EditMimeType = App->EditCtrlMimeType(); LAutoString Xml = App->GetForwardXml(EditMimeType); if (ValidStr(Xml)) { RemoveReturns(Xml); PreviousMail = m; LString Body = App->ProcessReplyForwardTemplate(m, this, Xml, Cursor, EditMimeType); LVariant EditCtrl; if (App->GetOptions()->GetValue(OPT_EditControl, EditCtrl) && EditCtrl.CastInt32()) { SetHtml(Body); } else { SetBody(Body); SetBodyCharset("utf-8"); } PreviousMail = 0; } else { ProcessTextForResponse(m, App->GetOptions(), MatchedAccount); } // Attachments if (WithAttachments > 0) for (auto From: Attach) AttachFile(new Attachment(App, From)); // Set MsgId GetMessageId(true); // Unless the user edits the message it shouldn't be made dirty. SetDirty(false); return true; } bool Mail::OnBounce(Mail *m, bool MarkOriginal, int WithAttachments) { if (m) { if (MarkOriginal) { // source email has been forwarded auto MsgId = m->GetMessageId(true); if (MsgId) { ScribeFolder *Fld = m->GetFolder(); LString FldPath; if (Fld) FldPath = Fld->GetPath(); if (FldPath) { LUri u; LString a = u.EncodeStr(MsgId, MsgIdEncodeChars); if (a) { LString p; p.Printf("%s/%s", FldPath.Get(), a.Get()); SetBounceMsgId(p); } } else { SetBounceMsgId(MsgId); } } else m->SetFlags(m->GetFlags() | MAIL_BOUNCED); } *this = (Thing&)*m; GetTo()->DeleteObjects(); SetFlags(MAIL_READ | MAIL_BOUNCE); return true; } return false; } void Mail::OnMeasure(LPoint *Info) { LListItem::OnMeasure(Info); if (PreviewLines && !(GetFlags() & MAIL_READ) && (!GetObject() || !GetObject()->GetInt(FIELD_DONT_SHOW_PREVIEW))) { LFont *PreviewFont = App->GetPreviewFont(); Info->y += PreviewFont ? PreviewFont->GetHeight() << 1 : 28; } } void Mail::OnPaintColumn(LItem::ItemPaintCtx &Ctx, int i, LItemColumn *c) { int Field = 0; if (FieldArray.Length()) { Field = i >= 0 && i < (int)FieldArray.Length() ? FieldArray[i] : 0; } else if (i >= 0 && i < CountOf(DefaultMailFields)) { Field = DefaultMailFields[i]; } if (Container && i >= 0 && Field == FIELD_SUBJECT) { LFont *f = Parent->GetFont(); auto Subj = GetSubject(); if (Container) { // Container needs to paint the subject... Container->OnPaint(Ctx.pDC, Ctx, c, Ctx.Fore, Ctx.Back, f?f:LSysFont, Subj); } } else { LListItem::OnPaintColumn(Ctx, i, c); if (i >= 0 && App->GetIconImgList()) { int Icon = -1; switch (Field) { case FIELD_PRIORITY: { switch (GetPriority()) { case MAIL_PRIORITY_HIGH: { Icon = ICON_PRIORITY_HIGH; break; } case MAIL_PRIORITY_LOW: { Icon = ICON_PRIORITY_LOW; break; } default: break; } break; } case FIELD_FLAGS: { if (GetFlags() & MAIL_BOUNCED) { Icon = ICON_FLAGS_BOUNCE; } else if (GetFlags() & MAIL_REPLIED) { Icon = ICON_FLAGS_REPLY; } else if (GetFlags() & MAIL_FORWARDED) { Icon = ICON_FLAGS_FORWARD; } break; } default: break; } if (Icon >= 0) { LRect *b = App->GetIconImgList()->GetBounds(); if (b) { b += Icon; int x = Ctx.x1 + ((Ctx.X()-b->X())/2) - b->x1; int y = Ctx.y1 + ((MIN(Ctx.Y(), 16)-b->Y())/2) - b->y1; LColour Back(Ctx.Back); App->GetIconImgList()->Draw(Ctx.pDC, x, y, Icon, Back); } } } } } void Mail::OnPaint(LItem::ItemPaintCtx &InCtx) { LItem::ItemPaintCtx Ctx = InCtx; int64_t MarkCol = GetMarkColour(); if (MarkCol > 0) { LColour Col((uint32_t)MarkCol, 32); LColour CtxBack(Ctx.Back); LColour Mixed = Col.Mix(CtxBack, MarkColourMix); Ctx.Back = Mixed; } if (Parent) ((ThingList*)Parent)->CurrentMail = this; LListItem::OnPaint(Ctx); if (Parent) ((ThingList*)Parent)->CurrentMail = 0; LFont *PreviewFont = App->GetPreviewFont(); LFont *ColumnFont = Parent && Parent->GetFont() ? Parent->GetFont() : LSysFont; if (Parent && PreviewLines && PreviewFont && ColumnFont && GetObject() && !GetObject()->GetInt(FIELD_DONT_SHOW_PREVIEW) && !(GetFlags() & MAIL_READ)) { int y = ColumnFont->GetHeight() + 2; int Space = (Ctx.Y() - y) >> 1; int Line = MAX(PreviewFont->GetHeight(), Space); PreviewFont->Transparent(true); // Setup if (PreviewCache.Length() == 0 || abs(PreviewCacheX - Ctx.X()) > 20) { PreviewCache.DeleteObjects(); PreviewCacheX = Ctx.X(); LVariant v; if (GetValue("BodyAsText[1024]", v) && v.Str()) { char16 *Base = Utf8ToWide(v.Str()); char16 *Txt = Base; int i; for (i=0; Txt[i]; i++) { if (Txt[i] < ' ') Txt[i] = ' '; } auto TxtLen = StrlenW(Txt); for (i=0; Txt && *Txt && i<2; i++) { LDisplayString *Temp = new LDisplayString(PreviewFont, Txt, MIN(1024, TxtLen)); if (Temp) { int W = Ctx.X()-18; ssize_t Cx = Temp->CharAt(W); if (Cx > 0 && Cx <= TxtLen) { LDisplayString *d = new LDisplayString(PreviewFont, Txt, Cx); if (d) { PreviewCache.Insert(d); } DeleteObj(Temp); Txt += Cx; // LSeekUtf8 TxtLen -= Cx; } else break; } } DeleteArray(Base); } } // Display LColour PreviewCol(App->GetColour(L_MAIL_PREVIEW)); if (Select()) { int GreyPrev = PreviewCol.GetGray(); int GreyBack = Ctx.Back.GetGray(); int d = GreyPrev - GreyBack; if (d < 0) d = -d; if (d < 128) { // too near PreviewFont->Colour(Ctx.Fore, Ctx.Back); } else { // ok PreviewFont->Colour(PreviewCol, Ctx.Back); } } else { PreviewFont->Colour(PreviewCol, Ctx.Back); } for (auto p: PreviewCache) { p->Draw(Ctx.pDC, Ctx.x1+16, Ctx.y1+y, 0); y += Line; } } } bool Mail::GetFormats(bool Export, LString::Array &MimeTypes) { if (Export) { MimeTypes.Add("text/plain"); MimeTypes.Add(sMimeMbox); } MimeTypes.Add(sMimeMessage); return MimeTypes.Length() > 0; } Thing::IoProgress Mail::Import(IoProgressImplArgs) { if (!mimeType) IoProgressError("No mime type."); if (Stricmp(mimeType, sMimeMessage)) IoProgressNotImpl(); // Single email.. OnAfterReceive(stream); int Flags = GetFlags(); Flags &= ~MAIL_CREATED; Flags |= MAIL_READ; SetFlags(Flags); Update(); IoProgressSuccess(); } #define TIMEOUT_OBJECT_LOAD 20000 Thing::IoProgress Mail::Export(IoProgressImplArgs) { if (!mimeType) IoProgressError("No mimetype."); if (!Stricmp(mimeType, "text/plain")) { LStringPipe Buf; char Str[256]; char Eol[] = EOL_SEQUENCE; // Addressee if (GetFlags() & MAIL_CREATED) { Buf.Push("To:"); for (LDataPropI *a=GetTo()->First(); a; a=GetTo()->Next()) { sprintf_s(Str, sizeof(Str), "\t%s <%s>%s", a->GetStr(FIELD_NAME), a->GetStr(FIELD_EMAIL), Eol); Buf.Push(Str); } } else { sprintf_s(Str, sizeof(Str), "From: %s <%s>%s", GetFrom()->GetStr(FIELD_NAME), GetFrom()->GetStr(FIELD_EMAIL), Eol); Buf.Push(Str); } // Subject sprintf_s(Str, sizeof(Str), "Subject: %s%s", GetSubject(), Eol); Buf.Push(Str); // Sent date if (GetDateSent()->Year()) { int ch = sprintf_s(Str, sizeof(Str), "Sent Date: "); GetDateSent()->Get(Str+ch, sizeof(Str)-ch); } else { int ch = sprintf_s(Str, sizeof(Str), "Date Received: "); GetDateReceived()->Get(Str+ch, sizeof(Str)-ch); } strcat(Str, Eol); Buf.Push(Str); // Body Buf.Push(Eol); Buf.Push(GetBody()); // Write the output auto s = Buf.NewLStr(); if (!s) IoProgressError("No data to output."); stream->Write(s.Get(), s.Length()); IoProgressSuccess(); } else if (!Stricmp(mimeType, sMimeMbox)) { char Temp[256]; // generate from header sprintf_s(Temp, sizeof(Temp), "From %s ", GetFrom()->GetStr(FIELD_EMAIL)); struct tm Ft; ZeroObj(Ft); LDateTime Rec = *GetDateReceived(); if (!Rec.Year()) Rec.SetNow(); Ft.tm_sec = Rec.Seconds(); /* seconds after the minute - [0,59] */ Ft.tm_min = Rec.Minutes(); /* minutes after the hour - [0,59] */ Ft.tm_hour = Rec.Hours(); /* hours since midnight - [0,23] */ Ft.tm_mday = Rec.Day(); /* day of the month - [1,31] */ Ft.tm_mon = Rec.Month() - 1; /* months since January - [0,11] */ Ft.tm_year = Rec.Year() - 1900; /* years since 1900 */ Ft.tm_wday = Rec.DayOfWeek(); strftime(Temp+strlen(Temp), MAX_NAME_SIZE, "%a %b %d %H:%M:%S %Y", &Ft); strcat(Temp, "\r\n"); // write mail stream->Write(Temp, strlen(Temp)); auto Status = Export(stream, sMimeMessage, [cb](auto io, auto stream) { if (io->status == Store3Success) stream->Write((char*)"\r\n.\r\n", 2); else if (io->status == Store3Delayed) LAssert(!"We should never get delayed here... it's the callback!"); if (cb) cb(io, stream); }); return Status; } else if (!Stricmp(mimeType, sMimeMessage)) { // This function can't be asynchronous, it must complete with UI or waiting for a callback. // Because it is used by the drag and drop system. Which won't wait. auto state = GetLoaded(); if (state != Store3Loaded) IoProgressError("Mail not loaded."); if (!GetObject()) IoProgressError("No store object."); auto Data = GetObject()->GetStream(_FL); if (!Data) IoProgressError("Mail for export has no data."); Data->SetPos(0); LCopyStreamer Cp(512<<10); if (Cp.Copy(Data, stream) <= 0) IoProgressError("Mail copy stream failed."); IoProgressSuccess(); } IoProgressNotImpl(); } char *Mail::GetNewText(int Max, const char *AsCp) { LAutoString CharSet = GetCharSet(); char *Txt = 0; // This limits the preview of the text to // the first 64kb, which is reasonable. int Len = Max > 0 ? Strnlen(GetBody(), Max) : -1; // Convert or allocate the text. if (CharSet) { Txt = (char*)LNewConvertCp(AsCp, GetBody(), GetBodyCharset(), Len); } else { Txt = NewStr(GetBody(), Len); } return Txt; } LAutoString Mail::GetCharSet() { LAutoString Status; LAutoString ContentType; if (GetBodyCharset()) { // If the charset has a stray colon... char *Colon = strchr(GetBodyCharset(), ';'); // Kill it. if (Colon) *Colon = 0; // Copy the string.. Status.Reset(NewStr(GetBodyCharset())); } if (!GetBodyCharset()) { ContentType.Reset(InetGetHeaderField(GetInternetHeader(), "Content-Type")); if (ContentType) { char *CharSet = stristr(ContentType, "charset="); if (CharSet) { CharSet += 8; if (*CharSet) { if (strchr("\"\'", *CharSet)) { char Delim = *CharSet++; char *e = CharSet; while (*e && *e != Delim) { e++; } Status.Reset(NewStr(CharSet, e - CharSet)); } else { char *e = CharSet; while (*e && (IsDigit(*e) || IsAlpha(*e) || strchr("-_", *e))) { e++; } Status.Reset(NewStr(CharSet, e - CharSet)); } } } } /* // If it's not a valid charset... if (!LGetCsInfo(Status)) { // Then kill it. Status.Reset(); if (GetBodyCharset()) { SetBodyCharset(0); SetDirty(); } } */ } return Status; } /* Old Body Printing Code int Lines = 0; char *n; for (n=Body.Str(); n && *n; n++) { if (*n == '\n') Lines++; } char *c = Body.Str(); if (c) { c = WrapLines(c, c ? strlen(c) : 0, 76); Lines = 0; for (n=c; *n; n++) { if (*n == '\n') Lines++; } while (c && *c) { if (Cy + Line > r.y2) { pDC->EndPage(); if (Window->GetMaxPages() > 0 && ++Page >= Window->GetMaxPages()) { break; } pDC->StartPage(); Cy = 0; } char *Eol = strchr(c, '\n'); int Len = 0; int Ret = 0; if (Eol) { Len = (int) Eol - (int) c; if (Len > 0 && c[Len-1] == '\r') { Len--; Ret = 1; } } else { Len = strlen(c); } MailFont->Text(pDC, r.x1, Cy, c, Len); Cy += Line; c += Len + Ret; if (*c == '\n') c++; } } */ int Mail::OnNotify(LViewI *Ctrl, LNotification n) { switch (Ctrl->GetId()) { case IDC_TEXT: { LDocView *Doc = dynamic_cast(Ctrl); if (Doc) { if (n.Type == LNotifyShowImagesChanged) { if (Doc->GetLoadImages() ^ TestFlag(GetFlags(), MAIL_SHOW_IMAGES)) { if (Doc->GetLoadImages()) { SetFlags(GetFlags() | MAIL_SHOW_IMAGES); } else { SetFlags(GetFlags() & ~MAIL_SHOW_IMAGES); } } } if (n.Type == LNotifyFixedWidthChanged) { bool DocFixed = Doc->GetFixedWidthFont(); bool MailFixed = TestFlag(GetFlags(), MAIL_FIXED_WIDTH_FONT); if (DocFixed ^ MailFixed) { if (Doc->GetFixedWidthFont()) { SetFlags(GetFlags() | MAIL_FIXED_WIDTH_FONT); } else { SetFlags(GetFlags() & ~MAIL_FIXED_WIDTH_FONT); } } } if (n.Type == LNotifyCharsetChanged && dynamic_cast(Doc)) { char s[256]; sprintf_s(s, sizeof(s), ">%s", Doc->GetCharset()); SetHtmlCharset(s); } } break; } } return 0; } void Mail::OnPrintHeaders(ScribePrintContext &Context) { char Str[256]; // print document LDrawListSurface *Page = Context.Pages.Last(); int Line = Context.AppFont->GetHeight(); Page->SetFont(Context.AppFont); LDisplayString *Ds = Context.Text(LLoadString(IDS_MAIL_MESSAGE)); Context.CurrentY += Line; LDataPropI *Ad = GetTo()->First(); if (Ad) { sprintf_s(Str, sizeof(Str), "%s: ", LLoadString(FIELD_TO)); Ds = Page->Text(Context.MarginPx.x1, Context.CurrentY, Str); int x = Ds->X(); for (; Ad; Ad = GetTo()->Next()) { if (Ad->GetStr(FIELD_EMAIL) && Ad->GetStr(FIELD_NAME)) { sprintf_s(Str, sizeof(Str), "%s <%s>", Ad->GetStr(FIELD_NAME), Ad->GetStr(FIELD_EMAIL)); } else if (Ad->GetStr(FIELD_EMAIL)) { strcpy_s(Str, sizeof(Str), Ad->GetStr(FIELD_EMAIL)); } else if (Ad->GetStr(FIELD_EMAIL)) { strcpy_s(Str, sizeof(Str), Ad->GetStr(FIELD_EMAIL)); } else continue; Ds = Page->Text(Context.MarginPx.x1 + x, Context.CurrentY, Str); Context.CurrentY += Ds->Y(); } } if (GetFrom()->GetStr(FIELD_EMAIL) || GetFrom()->GetStr(FIELD_NAME)) { sprintf_s(Str, sizeof(Str), "%s: ", LLoadString(FIELD_FROM)); Ds = Page->Text(Context.MarginPx.x1, Context.CurrentY, Str); int x = Ds->X(); if (GetFrom()->GetStr(FIELD_EMAIL) && GetFrom()->GetStr(FIELD_NAME)) { sprintf_s(Str, sizeof(Str), "%s <%s>", GetFrom()->GetStr(FIELD_NAME), GetFrom()->GetStr(FIELD_EMAIL)); } else if (GetFrom()->GetStr(FIELD_EMAIL)) { strcpy_s(Str, sizeof(Str), GetFrom()->GetStr(FIELD_EMAIL)); } else if (GetFrom()->GetStr(FIELD_NAME)) { strcpy_s(Str, sizeof(Str), GetFrom()->GetStr(FIELD_NAME)); } else Str[0] = 0; Ds = Page->Text(Context.MarginPx.x1 + x, Context.CurrentY, Str); Context.CurrentY += Ds->Y(); } { // Subject LString s; const char *Subj = GetSubject(); s.Printf("%s: %s", LLoadString(FIELD_SUBJECT), Subj?Subj:""); Context.Text(s); } { // Date LString s; GetDateSent()->Get(Str, sizeof(Str)); s.Printf("%s: %s", LLoadString(IDS_DATE), Str); Context.Text(s); } // attachment list... List Attached; if (GetAttachments(&Attached) && Attached.Length() > 0) { sprintf_s(Str, sizeof(Str), "%s: ", LLoadString(IDS_ATTACHMENTS)); Ds = Page->Text(Context.MarginPx.x1, Context.CurrentY, Str); int x = Ds->X(); for (auto a: Attached) { char Size[32]; LFormatSize(Size, sizeof(Size), a->GetSize()); sprintf_s(Str, sizeof(Str), "%s (%s)", a->GetName(), Size); Ds = Page->Text(Context.MarginPx.x1 + x, Context.CurrentY, Str); Context.CurrentY += Ds->Y(); } } // separator line LRect Sep(Context.MarginPx.x1, Context.CurrentY + (Line * 5 / 10), Context.MarginPx.x2, Context.CurrentY + (Line * 6 / 10)); Page->Rectangle(&Sep); Context.CurrentY += Line; } void Mail::OnPrintText(ScribePrintContext &Context, LPrintPageRanges &Pages) { // Text printing LDrawListSurface *Page = Context.Pages.Last(); LVariant Body; if (!GetVariant("BodyAsText", Body) || !Body.Str()) { LgiTrace("%s:%i - No content to print.\n", _FL); return; } LAutoWString w(Utf8ToWide(Body.Str())); if (!w) { LgiTrace("%s:%i - Utf8ToWide failed.\n", _FL); return; } Page->SetFont(Context.MailFont); for (char16 *s = w; s && *s; ) { // Find end of line.. char16 *n = StrchrW(s, '\n'); if (!n) n = s + StrlenW(s); ssize_t LineLen = n - s; // Find how many characters will fit on the page ssize_t Fit = 0; if (n > s) { LDisplayString a(Context.MailFont, s, MIN(LineLen, 1024)); Fit = a.CharAt(Context.MarginPx.X()); } if (Fit < 0) { break; } char16 *e = s + Fit; if (e < n) { // The whole line doesn't fit on the page... // Find the best breaking opportunity before that... #define ExtraBreak(c) ( ( (c) >= 0x3040 && (c) <= 0x30FF ) || \ ( (c) >= 0x3300 && (c) <= 0x9FAF ) \ ) char16 *StartE = e; while (e > s) { if (e[-1] == ' ' || ExtraBreak(e[-1])) { break; } e--; } if (e == s) { e = StartE; } } // Output the segment of text bool HasRet = e > s ? e[-1] == '\r' : false; LString Str(s, (e - s) - (HasRet ? 1 : 0)); Context.Text(Str); // Update the pointers s = e; if (*s == '\n') s++; } } /// \returns the number of pages printed int Mail::OnPrintHtml(ScribePrintContext &Context, LPrintPageRanges &Pages, LSurface *RenderedHtml) { // HTML printing... LDrawListSurface *Page = Context.Pages.Last(); // Work out the scaling from memory bitmap to page.. double MemScale = (double) Context.pDC->X() / (double) RenderedHtml->X(); // Now paint the bitmap onto the existing page int PageIdx = 1, Printed = 0; for (int y = 0; y < RenderedHtml->Y(); PageIdx++) { // Work out how much bitmap we can paint onto the current page... int PageRemaining = Context.MarginPx.Y() - Context.CurrentY; int MemPaint = (int) (PageRemaining / MemScale); // This is how much of the memory context we can fit on the page LRect MemRect(0, y, RenderedHtml->X()-1, y + MemPaint - 1); LRect Bnds = RenderedHtml->Bounds(); MemRect.Bound(&Bnds); // Work out how much page that is take up int PageHeight = (int) (MemRect.Y() * MemScale); // This is the position on the page we are blting to LRect PageRect(Context.MarginPx.x1, Context.CurrentY, Context.MarginPx.x2, Context.CurrentY + PageHeight - 1); if (Pages.InRanges(PageIdx)) { // Do the blt Page->StretchBlt(&PageRect, RenderedHtml, &MemRect); Printed++; // Now move our position down the page.. Context.CurrentY += PageHeight; if ((Context.MarginPx.Y() - Context.CurrentY) * 100 / Context.MarginPx.Y() < 5) { // Ok we hit the end of the page and need to go to the next page Context.CurrentY = Context.MarginPx.y1; Page = Context.NewPage(); } } y += MemRect.Y(); } return Printed; } ////////////////////////////////////////////////////////////////////////////// size_t Mail::Length() { size_t Status = 0; for (auto a: Attachments) { if (a->GetMimeType() && _stricmp(a->GetMimeType(), sMimeMessage) == 0) { Status++; } } return Status; } ssize_t Mail::IndexOf(Mail *m) { ssize_t Status = -1; ssize_t n = 0; for (auto a: Attachments) { if (a->GetMimeType() && _stricmp(a->GetMimeType(), sMimeMessage) == 0) { if (a->GetMsg() == m) { Status = n; break; } n++; } } return Status; } Mail *Mail::operator [](size_t i) { size_t n = 0; for (auto a: Attachments) { if (a->GetMimeType() && _stricmp(a->GetMimeType(), sMimeMessage) == 0) { if (n == i) return a->GetMsg(); n++; } } return NULL; } bool Mail::LoadFromFile(char *File) { if (File) { LAutoPtr f(new LFile); if (f->Open(File, O_READ)) { return Import(AutoCast(f), sMimeMessage); } } return false; } /////////////////////////////////////////////////////////////////////////////////////////////////////// bool CreateMailAddress(LStream &Out, LDataPropI *Addr, MailProtocol *Protocol) { if (!Addr) return false; auto Email = Addr->GetStr(FIELD_EMAIL); if (!Email) return false; auto Name = LEncodeRfc2047(Addr->GetStr(FIELD_NAME), NULL/*charset*/, &Protocol->CharsetPrefs); if (Name) { if (Name.Find("\"") >= 0) Out.Print("'%s' ", Name.Get()); else Out.Print("\"%s\" ", Name.Get()); } Out.Print("<%s>", Email); return true; } bool CreateMailHeaders(ScribeWnd *App, LStream &Out, LDataI *Mail, MailProtocol *Protocol) { bool Status = true; // Setup char Buffer[1025]; // Construct date LDateTime Dt; int TimeZone = Dt.SystemTimeZone(); Dt.SetNow(); sprintf_s(Buffer, sizeof(Buffer), "Date: %s, %i %s %i %i:%2.2i:%2.2i %s%2.2d%2.2d\r\n", LDateTime::WeekdaysShort[Dt.DayOfWeek()], Dt.Day(), LDateTime::MonthsShort[Dt.Month()-1], Dt.Year(), Dt.Hours(), Dt.Minutes(), Dt.Seconds(), (TimeZone >= 0) ? "+" : "", TimeZone / 60, abs(TimeZone) % 60); Status &= Out.Write(Buffer, strlen(Buffer)) > 0; if (Protocol && Protocol->ProgramName) { // X-Mailer: Status &= Out.Print("X-Mailer: %s\r\n", Protocol->ProgramName.Get()) > 0; } if (Protocol && Protocol->ExtraOutgoingHeaders) { for (char *s=Protocol->ExtraOutgoingHeaders; s && *s; ) { char *e = s; while (*e && *e != '\r' && *e != '\n') e++; ssize_t l = e-s; if (l > 0) { Status &= Out.Write(s, l) > 0; Status &= Out.Write((char*)"\r\n", 2) > 0; } while (*e && (*e == '\r' || *e == '\n')) e++; s = e; } } int Priority = (int)Mail->GetInt(FIELD_PRIORITY); if (Priority != MAIL_PRIORITY_NORMAL) { // X-Priority: Status &= Out.Print("X-Priority: %i\r\n", Priority) > 0; } uint32_t MarkColour = (uint32_t)Mail->GetInt(FIELD_COLOUR); if (MarkColour) { // X-Color (HTML Colour Ref for email marking) Status &= Out.Print("X-Color: #%2.2X%2.2X%2.2X\r\n", R24(MarkColour), G24(MarkColour), B24(MarkColour)) > 0; } // Message-ID: auto MessageID = Mail->GetStr(FIELD_MESSAGE_ID); if (MessageID) { for (auto m=MessageID; *m; m++) { if (*m <= ' ') { printf("%s:%i - Bad message ID '%s'\n", _FL, MessageID); return false; } } Status &= Out.Print("Message-ID: %s\r\n", MessageID) > 0; } // References: auto References = Mail->GetStr(FIELD_REFERENCES); if (ValidStr(References)) { auto Dir = strrchr(References, '/'); LString Ref; if (Dir) { LUri u; Ref = u.DecodeStr(Dir + 1); } else Ref = References; if (*Ref == '<') Status &= Out.Print("References: %s\r\n", Ref.Get()) > 0; else Status &= Out.Print("References: <%s>\r\n", Ref.Get()) > 0; } // To: LDataIt Addr = Mail->GetList(FIELD_TO); LArray Objs; LArray To, Cc; ContactGroup *Group; for (unsigned i=0; iLength(); i++) { LDataPropI *a = (*Addr)[i]; EmailAddressType AddrType = (EmailAddressType)a->GetInt(FIELD_CC); LString Addr = a->GetStr(FIELD_EMAIL); if (LIsValidEmail(Addr)) { if (AddrType == MAIL_ADDR_CC) Cc.Add(a); else if (AddrType == MAIL_ADDR_TO) To.Add(a); } else if ((Group = LookupContactGroup(App, Addr))) { Group->UsedTs.SetNow(); LString::Array Addrs = Group->GetAddresses(); for (unsigned n=0; nGetObject()->GetStore(), NULL); if (sa) { sa->Addr = Addrs[n]; Objs.Add(sa); if (AddrType == MAIL_ADDR_CC) Cc.Add(sa); else if (AddrType == MAIL_ADDR_TO) To.Add(sa); } } } } if (To.Length()) { for (unsigned i=0; iGetObj(FIELD_FROM); if (From && From->GetStr(FIELD_EMAIL)) { Out.Print("From: "); if (!CreateMailAddress(Out, From, Protocol)) return false; Out.Print("\r\n"); } else { LgiTrace("%s:%i - No from address.\n", _FL); return false; } // Reply-To: LDataPropI *Reply = Mail->GetObj(FIELD_REPLY); if (Reply && ValidStr(Reply->GetStr(FIELD_EMAIL))) { Out.Print("Reply-To: "); if (!CreateMailAddress(Out, Reply, Protocol)) return false; Out.Print("\r\n"); } // Subject: - char *Subj = EncodeRfc2047(NewStr(Mail->GetStr(FIELD_SUBJECT)), 0, &Protocol->CharsetPrefs, 9); - sprintf_s(Buffer, sizeof(Buffer), "Subject: %s\r\n", (Subj) ? Subj : ""); + auto Subj = LEncodeRfc2047(Mail->GetStr(FIELD_SUBJECT), 0, &Protocol->CharsetPrefs, 9); + sprintf_s(Buffer, sizeof(Buffer), "Subject: %s\r\n", Subj ? Subj.Get() : ""); Status &= Out.Write(Buffer, strlen(Buffer)) > 0; - DeleteArray(Subj); // DispositionNotificationTo uint8_t DispositionNotificationTo = TestFlag(Mail->GetInt(FIELD_FLAGS), MAIL_READ_RECEIPT); if (DispositionNotificationTo && From) { int ch = sprintf_s(Buffer, sizeof(Buffer), "Disposition-Notification-To:"); - char *Nme = EncodeRfc2047(NewStr(From->GetStr(FIELD_NAME)), 0, &Protocol->CharsetPrefs); - if (Nme) - { - ch += sprintf_s(Buffer+ch, sizeof(Buffer)-ch, " \"%s\"", Nme); - DeleteArray(Nme); - } + if (auto Nme = LEncodeRfc2047(From->GetStr(FIELD_NAME), 0, &Protocol->CharsetPrefs)) + ch += sprintf_s(Buffer+ch, sizeof(Buffer)-ch, " \"%s\"", Nme.Get()); ch += sprintf_s(Buffer+ch, sizeof(Buffer)-ch, " <%s>\r\n", From->GetStr(FIELD_EMAIL)); Status &= Out.Write(Buffer, ch) > 0; } // Content-Type LDataPropI *Root = Mail->GetObj(FIELD_MIME_SEG); if (Root) { auto MimeType = Root->GetStr(FIELD_MIME_TYPE); auto Charset = Root->GetStr(FIELD_CHARSET); if (MimeType) { LString s; s.Printf("Content-Type: %s%s%s\r\n", MimeType, Charset?"; charset=":"", Charset?Charset:""); Status &= Out.Write(s, s.Length()) == s.Length(); } } else LAssert(0); Objs.DeleteObjects(); return Status; }