From 05b410c3168ad5847903e176fb5a2617e90a9893 Mon Sep 17 00:00:00 2001 From: Amani Nyumu Date: Wed, 19 Jun 2024 10:05:59 +0100 Subject: [PATCH] [ENH] add functionality to send later scheduled sending --- language/az.php | 1 + language/de.php | 1 + language/en.php | 2 +- language/es.php | 1 + language/et.php | 1 + language/fa.php | 2 +- language/fr.php | 1 + language/hu.php | 1 + language/id.php | 1 + language/it.php | 1 + language/ja.php | 1 + language/nl.php | 1 + language/pt-BR.php | 1 + language/ro.php | 1 + language/ru.php | 1 + language/zh-Hans.php | 1 + modules/core/functions.php | 106 +++++++-- modules/core/message_list_functions.php | 6 +- modules/core/site.css | 2 +- modules/core/site.js | 53 ++++- modules/imap/functions.php | 71 ++++-- modules/imap/handler_modules.php | 9 +- modules/imap/hm-imap.php | 16 +- modules/imap/output_modules.php | 7 +- modules/imap/site.css | 4 +- modules/imap/site.js | 67 ++++++ modules/profiles/functions.php | 2 +- modules/profiles/hm-profiles.php | 10 + modules/smtp/hm-mime-message.php | 14 +- modules/smtp/modules.php | 295 ++++++++++++++++++++++-- modules/smtp/setup.php | 28 ++- modules/smtp/site.js | 276 +++++++++++++++++++++- 32 files changed, 907 insertions(+), 77 deletions(-) diff --git a/language/az.php b/language/az.php index 5e1c54a187..e3f91e1ced 100755 --- a/language/az.php +++ b/language/az.php @@ -621,6 +621,7 @@ 'You must provide a name for your script' => false, 'Empty script' => false, 'Please create a profile for saving sent messages option' => false, + 'You have %d scheduled messages that won\'t be executed if you quit' => false, 'Attachment storage unavailable, please contact your site administrator' => false, 'Your subject is empty!' => false, 'Your body is empty!' => false, diff --git a/language/de.php b/language/de.php index 7150e9c182..8bd66b1a43 100755 --- a/language/de.php +++ b/language/de.php @@ -618,6 +618,7 @@ 'You must provide a name for your script' => false, 'Empty script' => false, 'Please create a profile for saving sent messages option' => false, + 'You have %d scheduled messages that won\'t be executed if you quit' => false, 'Attachment storage unavailable, please contact your site administrator' => false, 'Your subject is empty!' => false, 'Your body is empty!' => false, diff --git a/language/en.php b/language/en.php index f630f6a2d3..7dcf4bcccd 100755 --- a/language/en.php +++ b/language/en.php @@ -636,6 +636,7 @@ 'You must provide a name for your script' => false, 'Empty script' => false, 'Please create a profile for saving sent messages option' => false, + 'You have %d scheduled messages that won\'t be executed if you quit' => false, 'Attachment storage unavailable, please contact your site administrator' => false, 'Your subject is empty!' => false, 'Your body is empty!' => false, @@ -645,7 +646,6 @@ 'Are you sure you want to send this message?' => false, 'IMAP and JMAP Servers' => false, 'Junk' => false, - 'Trash' => false, 'Pasted text has leading or trailing spaces' => false, 'No tags available yet.' => false, 'Server capabilities' => false, diff --git a/language/es.php b/language/es.php index fa551b1ea9..c253b1f363 100755 --- a/language/es.php +++ b/language/es.php @@ -618,6 +618,7 @@ 'You must provide a name for your script' => false, 'Empty script' => false, 'Please create a profile for saving sent messages option' => false, + 'You have %d scheduled messages that won\'t be executed if you quit' => false, 'Attachment storage unavailable, please contact your site administrator' => false, 'Your subject is empty!' => false, 'Your body is empty!' => false, diff --git a/language/et.php b/language/et.php index 61b7b6addb..53e6207a9f 100755 --- a/language/et.php +++ b/language/et.php @@ -626,6 +626,7 @@ 'You must provide a name for your script' => false, 'Empty script' => false, 'Please create a profile for saving sent messages option' => false, + 'You have %d scheduled messages that won\'t be executed if you quit' => false, 'Attachment storage unavailable, please contact your site administrator' => false, 'Your subject is empty!' => false, 'Your body is empty!' => false, diff --git a/language/fa.php b/language/fa.php index b2ad0e2221..7a7b5ee481 100755 --- a/language/fa.php +++ b/language/fa.php @@ -670,6 +670,7 @@ 'You must provide a name for your script' => false, 'Empty script' => false, 'Please create a profile for saving sent messages option' => false, + 'You have %d scheduled messages that won\'t be executed if you quit' => false, 'Attachment storage unavailable, please contact your site administrator' => false, 'Your subject is empty!' => false, 'Your body is empty!' => false, @@ -679,7 +680,6 @@ 'Are you sure you want to send this message?' => false, 'IMAP and JMAP Servers' => false, 'Junk' => false, - 'Trash' => false, 'Pasted text has leading or trailing spaces' => false, 'No tags available yet.' => false, 'Server capabilities' => false, diff --git a/language/fr.php b/language/fr.php index b60c7c3592..8e0550fc0e 100755 --- a/language/fr.php +++ b/language/fr.php @@ -617,6 +617,7 @@ 'You must provide a name for your script' => false, 'Empty script' => false, 'Please create a profile for saving sent messages option' => false, + 'You have %d scheduled messages that won\'t be executed if you quit' => 'Vous avez %d messages programmés qui ne seront pas exécutés si vous quittez', 'Attachment storage unavailable, please contact your site administrator' => false, 'Your subject is empty!' => false, 'Your body is empty!' => false, diff --git a/language/hu.php b/language/hu.php index e91cd6a66d..7e9b587059 100755 --- a/language/hu.php +++ b/language/hu.php @@ -618,6 +618,7 @@ 'You must provide a name for your script' => false, 'Empty script' => false, 'Please create a profile for saving sent messages option' => false, + 'You have %d scheduled messages that won\'t be executed if you quit' => false, 'Attachment storage unavailable, please contact your site administrator' => false, 'Your subject is empty!' => false, 'Your body is empty!' => false, diff --git a/language/id.php b/language/id.php index 07b205ab8b..95c4ac6a5f 100755 --- a/language/id.php +++ b/language/id.php @@ -625,6 +625,7 @@ 'You must provide a name for your script' => false, 'Empty script' => false, 'Please create a profile for saving sent messages option' => false, + 'You have %d scheduled messages that won\'t be executed if you quit' => false, 'Attachment storage unavailable, please contact your site administrator' => false, 'Your subject is empty!' => false, 'Your body is empty!' => false, diff --git a/language/it.php b/language/it.php index 5147b4e8ad..d720e4214a 100755 --- a/language/it.php +++ b/language/it.php @@ -618,6 +618,7 @@ 'You must provide a name for your script' => false, 'Empty script' => false, 'Please create a profile for saving sent messages option' => false, + 'You have %d scheduled messages that won\'t be executed if you quit' => false, 'Attachment storage unavailable, please contact your site administrator' => false, 'Your subject is empty!' => false, 'Your body is empty!' => false, diff --git a/language/ja.php b/language/ja.php index 30596c066d..7c7d601ac5 100755 --- a/language/ja.php +++ b/language/ja.php @@ -618,6 +618,7 @@ 'You must provide a name for your script' => false, 'Empty script' => false, 'Please create a profile for saving sent messages option' => false, + 'You have %d scheduled messages that won\'t be executed if you quit' => false, 'Attachment storage unavailable, please contact your site administrator' => false, 'Your subject is empty!' => false, 'Your body is empty!' => false, diff --git a/language/nl.php b/language/nl.php index 1d1db295cc..82e94158d6 100755 --- a/language/nl.php +++ b/language/nl.php @@ -618,6 +618,7 @@ 'You must provide a name for your script' => false, 'Empty script' => false, 'Please create a profile for saving sent messages option' => false, + 'You have %d scheduled messages that won\'t be executed if you quit' => false, 'Attachment storage unavailable, please contact your site administrator' => false, 'Your subject is empty!' => false, 'Your body is empty!' => false, diff --git a/language/pt-BR.php b/language/pt-BR.php index 1fcf9142ec..7ad23625b8 100755 --- a/language/pt-BR.php +++ b/language/pt-BR.php @@ -617,6 +617,7 @@ 'You must provide a name for your script' => false, 'Empty script' => false, 'Please create a profile for saving sent messages option' => false, + 'You have %d scheduled messages that won\'t be executed if you quit' => false, 'Attachment storage unavailable, please contact your site administrator' => false, 'Your subject is empty!' => false, 'Your body is empty!' => false, diff --git a/language/ro.php b/language/ro.php index 5a52506d25..c019b88263 100755 --- a/language/ro.php +++ b/language/ro.php @@ -617,6 +617,7 @@ 'You must provide a name for your script' => false, 'Empty script' => false, 'Please create a profile for saving sent messages option' => false, + 'You have %d scheduled messages that won\'t be executed if you quit' => false, 'Attachment storage unavailable, please contact your site administrator' => false, 'Your subject is empty!' => false, 'Your body is empty!' => false, diff --git a/language/ru.php b/language/ru.php index 60813edac2..70cd16aa1c 100755 --- a/language/ru.php +++ b/language/ru.php @@ -619,6 +619,7 @@ 'You must provide a name for your script' => false, 'Empty script' => false, 'Please create a profile for saving sent messages option' => false, + 'You have %d scheduled messages that won\'t be executed if you quit' => false, 'Attachment storage unavailable, please contact your site administrator' => false, 'Your subject is empty!' => false, 'Your body is empty!' => false, diff --git a/language/zh-Hans.php b/language/zh-Hans.php index 8baf2310ff..526e932c4a 100644 --- a/language/zh-Hans.php +++ b/language/zh-Hans.php @@ -639,6 +639,7 @@ 'You must provide a name for your script' => '请提供脚本名称', 'Empty script' => '空脚本', 'Please create a profile for saving sent messages option' => '请创建用于保存已发送信息选项的配置文件', + 'You have %d scheduled messages that won\'t be executed if you quit' => false, 'Attachment storage unavailable, please contact your site administrator' => '附件存储不可用,请联系您的网站管理员', 'Your subject is empty!' => '主题为空!', 'Your body is empty!' => '内容为空!', diff --git a/modules/core/functions.php b/modules/core/functions.php index 6b1c5a7bce..e196c99177 100644 --- a/modules/core/functions.php +++ b/modules/core/functions.php @@ -619,20 +619,98 @@ function check_file_upload($request, $key) { return true; }} -function privacy_setting_callback($val, $key, $mod) { - $setting = Hm_Output_privacy_settings::$settings[$key]; - $key .= '_setting'; - $user_setting = $mod->user_config->get($key); - $update = $mod->request->post['update']; +if (!hm_exists('get_nexter_date')) { +function get_nexter_date($format, $only_label = false) { + if ($format == 'later_in_day') { + $date_string = 'today 18:00'; + $label = 'Later in the day'; + } elseif ($format == 'tomorrow') { + $date_string = '+1 day 08:00'; + $label = 'Tomorrow'; + } elseif ($format == 'next_weekend') { + $date_string = 'next Saturday 08:00'; + $label = 'Next weekend'; + } elseif ($format == 'next_week') { + $date_string = 'next week 08:00'; + $label = 'Next week'; + } elseif ($format == 'next_month') { + $date_string = 'next month 08:00'; + $label = 'Next month'; + } else { + $date_string = $format; + $label = 'Certain date'; + } + $time = strtotime($date_string); + if ($only_label) { + return [$label, date('D, H:i', $time)]; + } + return date('D, d M Y H:i', $time); +}} + +/** + * @subpackage imap/functions + */ +if (!hm_exists('nexter_formats')) { +function nexter_formats() { + $values = array( + 'tomorrow', + 'next_weekend', + 'next_week', + 'next_month' + ); + if (date('H') <= 16) { + array_push($values, 'later_in_day'); + } + return $values; +}} - if ($update) { - $val = implode($setting['separator'], array_filter(array_merge(explode($setting['separator'], $user_setting), [$val]))); - $mod->user_config->set($key, $val); +if (!hm_exists('schedule_dropdown')) { +function schedule_dropdown($output, $send_now = false) { + $values = nexter_formats(); - $user_data = $mod->session->get('user_data', array()); - $user_data[$key] = $val; - $mod->session->set('user_data', $user_data); - $mod->session->record_unsaved('Privacy settings updated'); + $txt = ''; + if ($send_now) { + $txt .= ''; + } + + return $txt; +}} + +/** + * @subpackage imap/functions + */ +if (!hm_exists('parse_nexter_header')) { + function parse_nexter_header($header, $name) + { + $header = str_replace("$name: ", '', $header); + $result = []; + foreach (explode(';', $header) as $kv) + { + $kv = trim($kv); + $spacePos = strpos($kv, ' '); + if ($spacePos > 0) { + $result[rtrim(substr($kv, 0, $spacePos), ':')] = trim(substr($kv, $spacePos+1)); + } else { + $result[$kv] = true; + } + } + return $result; + }} diff --git a/modules/core/message_list_functions.php b/modules/core/message_list_functions.php index 793bc398d9..b2b7471e8c 100644 --- a/modules/core/message_list_functions.php +++ b/modules/core/message_list_functions.php @@ -322,11 +322,11 @@ function subject_callback($vals, $style, $output_mod) { */ if (!hm_exists('date_callback')) { function date_callback($vals, $style, $output_mod) { - $snooze_class = isset($vals[2]) && $vals[2]? ' snoozed_date': ''; + $nexter_class = isset($vals[2]) && $vals[2]? ' nexter_date': ''; if ($style == 'news') { - return sprintf('
%s
', $snooze_class, $output_mod->html_safe($vals[0]), $output_mod->html_safe($vals[1])); + return sprintf('
%s
', $nexter_class, $output_mod->html_safe($vals[0]), $output_mod->html_safe($vals[1])); } - return sprintf('%s', $snooze_class, $output_mod->html_safe(date('r', $vals[1])), $output_mod->html_safe($vals[0]), $output_mod->html_safe($vals[1])); + return sprintf('%s', $nexter_class, $output_mod->html_safe(date('r', $vals[1])), $output_mod->html_safe($vals[0]), $output_mod->html_safe($vals[1])); }} /** diff --git a/modules/core/site.css b/modules/core/site.css index 95e98e0f8a..1609ae3636 100644 --- a/modules/core/site.css +++ b/modules/core/site.css @@ -1276,7 +1276,7 @@ div.unseen, .mobile .imap_sort { width: 100%; } -.snoozed_date { +.nexter_date { color: var(--bs-primary) !important; } diff --git a/modules/core/site.js b/modules/core/site.js index cb42a5cacb..9e25b8ee03 100644 --- a/modules/core/site.js +++ b/modules/core/site.js @@ -1784,10 +1784,23 @@ var imap_smtp_edit_action = function(event) { } }; + var hasLeadingOrTrailingSpaces = function(str) { return str !== str.trim(); }; +var sprintf = function(format, ...args) { + let i = 0; + return format.replace(/%([sd])/g, (match, type) => { + let arg = args[i++]; + switch (type) { + case 's': return String(arg); + case 'd': return Number(arg); + default: return match; + } + }); +} + /* create a default message list object */ var Hm_Message_List = new Message_List(); @@ -1821,7 +1834,7 @@ $(function() { /* fire up the job scheduler */ Hm_Timer.fire(); - + /* show any pending notices */ Hm_Utils.show_sys_messages(); @@ -1835,6 +1848,12 @@ $(function() { try { navigator.registerProtocolHandler("mailto", "?page=compose&compose_to=%s", "Cypht"); } catch(e) {} } + if (hm_page_name() == 'home') { + $('.pw_update').on("click", function() { update_password($(this).data('id')); }); + } + if (hm_page_name() == 'servers') { + $('.edit_server_connection').on('click', imap_smtp_edit_action); + } if (hm_mobile()) { swipe_event(document.body, function() { Hm_Folders.open_folder_list(); }, 'right'); swipe_event(document.body, function() { Hm_Folders.hide_folder_list(); }, 'left'); @@ -2038,7 +2057,7 @@ function handleSmtpImapCheckboxChange(checkbox) { if ($('#srv_setup_stepper_is_sender').prop('checked') && $('#srv_setup_stepper_is_receiver').prop('checked')) { $('#srv_setup_stepper_profile_bloc').show(); $('#srv_setup_stepper_profile_checkbox_bloc').show(); - + } else if(! $('#srv_setup_stepper_is_sender').prop('checked') || ! $('#srv_setup_stepper_is_receiver').prop('checked')) { $('#srv_setup_stepper_profile_bloc').hide(); $('#srv_setup_stepper_profile_checkbox_bloc').hide(); @@ -2090,7 +2109,7 @@ function display_config_step(stepNumber) { $(`#${item.key}-error`).text('Required'); isValid = false; } - + } else { $(`#${item.key}-error`).text(''); } @@ -2381,7 +2400,7 @@ const observeMessageTextMutationAndHandleExternalResources = (inline) => { if (mutation.addedNodes.length > 0) { mutation.addedNodes.forEach(function (node) { if (node.classList.contains('msg_text_inner')) { - handleExternalResources(inline); + handleExternalResources(inline); } }); } @@ -2391,3 +2410,29 @@ const observeMessageTextMutationAndHandleExternalResources = (inline) => { }); } }; + +var setup_nexter_date = function(callback) { + $(document).on('click', '.nexter_date_picker', function(e) { + document.querySelector('.nexter_input_date').showPicker(); + }); + $(document).on('click', '.nexter_date_helper', function(e) { + e.preventDefault(); + $('.nexter_input').val($(this).attr('data-value')).trigger('change'); + }); + $(document).on('input', '.nexter_input_date', function(e) { + var now = new Date(); + now.setMinutes(now.getMinutes() + 1); + $(this).attr('min', now.toJSON().slice(0, 16)); + if (new Date($(this).val()).getTime() <= now.getTime()) { + $('.nexter_date_picker').css('border', '1px solid red'); + } else { + $('.nexter_date_picker').css({'border': 'unset', 'border-top': '1px solid #ddd'}); + } + }); + $(document).on('change', '.nexter_input_date', function(e) { + if ($(this).val() && new Date().getTime() < new Date($(this).val()).getTime()) { + $('.nexter_input').val($(this).val()).trigger('change'); + } + }); + $(document).on('change', '.nexter_input', callback); +} diff --git a/modules/imap/functions.php b/modules/imap/functions.php index a0c1dcafbe..e35c119e9f 100644 --- a/modules/imap/functions.php +++ b/modules/imap/functions.php @@ -241,10 +241,14 @@ function format_imap_message_list($msg_list, $output_module, $parent_list=false, $nofrom = ' nofrom'; } $is_snoozed = !empty($msg['x_snoozed']) && hex2bin($msg['folder']) == 'Snoozed'; + $is_scheduled = !empty($msg['x_schedule']) && hex2bin($msg['folder']) == 'Scheduled'; if ($is_snoozed) { - $snooze_header = parse_snooze_header('X-Snoozed: '.$msg['x_snoozed']); + $snooze_header = parse_nexter_header('X-Snoozed: '.$msg['x_snoozed'], 'X-Snoozed'); $date = $snooze_header['until']; $timestamp = strtotime($date); + } elseif ($is_scheduled) { + $date = $msg['x_schedule']; + $timestamp = strtotime($date); } else { if ($list_sort == 'date') { $date_field = 'date'; @@ -318,8 +322,8 @@ function format_imap_message_list($msg_list, $output_module, $parent_list=false, array('checkbox_callback', $id), array('safe_output_callback', 'source', $source, $icon), array('safe_output_callback', 'from'.$nofrom, $from, null, str_replace(array($from, '<', '>'), '', $msg['from'])), - array('subject_callback', $subject, $url, $flags, null, $preview_msg), - array('date_callback', $date, $timestamp, $is_snoozed), + array('subject_callback', $subject, $url, $flags), + array('date_callback', $date, $timestamp, $is_snoozed || $is_scheduled), array('icon_callback', $flags) ), $id, @@ -1339,7 +1343,7 @@ function snooze_message($imap, $msg_id, $folder, $snooze_tag) { preg_match("/^X-Snoozed:.*(\r?\n[ \t]+.*)*\r?\n?/im", $msg, $matches); if (count($matches)) { $msg = str_replace($matches[0], '', $msg); - $old_folder = parse_snooze_header($matches[0])['from']; + $old_folder = parse_nexter_header($matches[0], 'X-Snoozed')['from']; } if ($snooze_tag) { $from = $old_folder ?? $folder; @@ -1368,7 +1372,7 @@ function snooze_message($imap, $msg_id, $folder, $snooze_tag) { } } } else { - $snooze_headers = parse_snooze_header($matches[0]); + $snooze_headers = parse_nexter_header($matches[0], 'X-Snoozed'); $original_folder = $snooze_headers['from']; if ($imap->select_mailbox($original_folder) && $imap->append_start($original_folder, mb_strlen($msg))) { $imap->append_feed($msg."\r\n"); @@ -1460,21 +1464,21 @@ function snooze_formats() { */ if (!hm_exists('snooze_dropdown')) { function snooze_dropdown($output, $unsnooze = false) { - $values = snooze_formats(); + $values = nexter_formats(); $txt = ''; @@ -1608,3 +1612,42 @@ function connect_to_imap_server($address, $name, $port, $user, $pass, $tls, $ima } } } + +if (!hm_exists('save_sent_msg')) { +function save_sent_msg($handler, $imap_id, $imap, $imap_details, $msg, $msg_id, $show_errors = true) { + $specials = get_special_folders($handler, $imap_id); + if (array_key_exists('sent', $specials) && $specials['sent']) { + $sent_folder = $specials['sent']; + } + + if (!$sent_folder) { + $auto_sent = $imap->get_special_use_mailboxes('sent'); + if (!array_key_exists('sent', $auto_sent)) { + return; + } + $sent_folder = $auto_sent['sent']; + } + if (!$sent_folder) { + Hm_Debug::add(sprintf("Unable to save sent message, no sent folder for IMAP %s", $imap_details['server'])); + } + $uid = null; + if ($sent_folder) { + Hm_Debug::add(sprintf("Attempting to save sent message for IMAP server %s in folder %s", $imap_details['server'], $sent_folder)); + if ($imap->append_start($sent_folder, strlen($msg), true)) { + $imap->append_feed($msg."\r\n"); + if (!$imap->append_end() && $show_errors) { + Hm_Msgs::add('ERRAn error occurred saving the sent message'); + } + } + + $mailbox_page = $imap->get_mailbox_page($sent_folder, 'ARRIVAL', true, 'ALL', 0, 10); + foreach ($mailbox_page[1] as $mail) { + $msg_header = $imap->get_message_headers($mail['uid']); + if ($msg_header['Message-Id'] === $msg_id) { + $uid = $mail['uid']; + break; + } + } + } + return $uid; +}} diff --git a/modules/imap/handler_modules.php b/modules/imap/handler_modules.php index 658c1c2902..431f879536 100644 --- a/modules/imap/handler_modules.php +++ b/modules/imap/handler_modules.php @@ -369,7 +369,10 @@ public function process() { break; } } - if ($uid && $this->user_config->get('review_sent_email_setting', true)) { + } + $uid = save_sent_msg($this, $imap_id, $imap, $imap_details, $msg, $mime->get_headers()['Message-Id']); + if ($uid) { + if ($uid && $this->user_config->get('review_sent_email_setting', false)) { $this->out('redirect_url', '?page=message&uid='.$uid.'&list_path=imap_'.$imap_id.'_'.bin2hex($sent_folder)); } } @@ -1151,7 +1154,7 @@ public function process() { $snooze_tag = null; if ($form['imap_snooze_until'] != 'unsnooze') { $at = date('D, d M Y H:i:s O'); - $until = get_snooze_date($form['imap_snooze_until']); + $until = get_nexter_date($form['imap_snooze_until']); $snooze_tag = "X-Snoozed: at $at; until $until"; } $ids = explode(',', $form['imap_snooze_ids']); @@ -1202,7 +1205,7 @@ public function process() { $msg_headers = $imap->get_message_headers($msg['uid']); if (isset($msg_headers['X-Snoozed'])) { try { - $snooze_headers = parse_snooze_header($msg_headers['X-Snoozed']); + $snooze_headers = parse_nexter_header($msg_headers['X-Snoozed'], 'X-Snoozed'); if (new DateTime($snooze_headers['until']) <= new DateTime()) { snooze_message($imap, $msg['uid'], $folder, null); } diff --git a/modules/imap/hm-imap.php b/modules/imap/hm-imap.php index 5546f9ffe4..133f2d2c44 100644 --- a/modules/imap/hm-imap.php +++ b/modules/imap/hm-imap.php @@ -890,11 +890,7 @@ public function get_message_list($uids, $raw=false, $include_content_body = fals if ($this->is_supported( 'X-GM-EXT-1' )) { $command .= 'X-GM-MSGID X-GM-THRID X-GM-LABELS '; } - $command .= "BODY.PEEK[HEADER.FIELDS (SUBJECT X-AUTO-BCC FROM DATE CONTENT-TYPE X-PRIORITY TO LIST-ARCHIVE REFERENCES MESSAGE-ID X-SNOOZED)]"; - if ($include_content_body) { - $command .= " BODY.PEEK[0.1]"; - } - $command .= ")\r\n"; + $command .= "BODY.PEEK[HEADER.FIELDS (SUBJECT X-AUTO-BCC FROM DATE CONTENT-TYPE X-PRIORITY TO LIST-ARCHIVE REFERENCES MESSAGE-ID X-SNOOZED X-SCHEDULE X-PROFILE-ID X-DELIVERY)])\r\n"; $cache_command = $command.(string)$raw; $cache = $this->check_cache($cache_command); if ($cache !== false) { @@ -904,8 +900,8 @@ public function get_message_list($uids, $raw=false, $include_content_body = fals $res = $this->get_response(false, true); $status = $this->check_response($res, true); $tags = array('X-GM-MSGID' => 'google_msg_id', 'X-GM-THRID' => 'google_thread_id', 'X-GM-LABELS' => 'google_labels', 'UID' => 'uid', 'FLAGS' => 'flags', 'RFC822.SIZE' => 'size', 'INTERNALDATE' => 'internal_date'); - $junk = array('X-AUTO-BCC', 'MESSAGE-ID', 'REFERENCES', 'X-SNOOZED', 'LIST-ARCHIVE', 'SUBJECT', 'FROM', 'CONTENT-TYPE', 'TO', '(', ')', ']', 'X-PRIORITY', 'DATE'); - $flds = array('x-auto-bcc' => 'x_auto_bcc', 'message-id' => 'message_id', 'references' => 'references', 'x-snoozed' => 'x_snoozed', 'list-archive' => 'list_archive', 'date' => 'date', 'from' => 'from', 'to' => 'to', 'subject' => 'subject', 'content-type' => 'content_type', 'x-priority' => 'x_priority', 'body' => 'content_body'); + $junk = array('X-AUTO-BCC', 'MESSAGE-ID', 'REFERENCES', 'X-SNOOZED', 'X-SCHEDULE', 'X-PROFILE-ID', 'X-DELIVERY', 'LIST-ARCHIVE', 'SUBJECT', 'FROM', 'CONTENT-TYPE', 'TO', '(', ')', ']', 'X-PRIORITY', 'DATE'); + $flds = array('x-auto-bcc' => 'x_auto_bcc', 'message-id' => 'message_id', 'references' => 'references', 'x-snoozed' => 'x_snoozed', 'x-schedule' => 'x_schedule', 'x-profile-id' => 'x_profile_id', 'x-delivery' => 'x_delivery', 'list-archive' => 'list_archive', 'date' => 'date', 'from' => 'from', 'to' => 'to', 'subject' => 'subject', 'content-type' => 'content_type', 'x-priority' => 'x_priority'); $headers = array(); foreach ($res as $n => $vals) { @@ -928,6 +924,9 @@ public function get_message_list($uids, $raw=false, $include_content_body = fals $google_labels = ''; $x_auto_bcc = ''; $x_snoozed = ''; + $x_schedule = ''; + $x_profile_id = ''; + $x_delivery = ''; $count = count($vals); for ($i=0;$i<$count;$i++) { if ($vals[$i] == 'BODY[HEADER.FIELDS') { @@ -990,8 +989,7 @@ public function get_message_list($uids, $raw=false, $include_content_body = fals 'timestamp' => time(), 'charset' => $cset, 'x-priority' => $x_priority, 'google_msg_id' => $google_msg_id, 'google_thread_id' => $google_thread_id, 'google_labels' => $google_labels, 'list_archive' => $list_archive, 'references' => $references, 'message_id' => $message_id, 'x_auto_bcc' => $x_auto_bcc, - 'x_snoozed' => $x_snoozed); - $headers[$uid]['preview_msg'] = $flds['body'] != "content_body" ? $flds['body'] : ""; + 'x_snoozed' => $x_snoozed, 'x_schedule' => $x_schedule, 'x_profile_id' => $x_profile_id, 'x_delivery' => $x_delivery); if ($raw) { $headers[$uid] = array_map('trim', $headers[$uid]); diff --git a/modules/imap/output_modules.php b/modules/imap/output_modules.php index c095513301..af6ac70c5e 100644 --- a/modules/imap/output_modules.php +++ b/modules/imap/output_modules.php @@ -206,9 +206,9 @@ protected function output() { $txt .= $this->html_safe($value).''; } elseif ($fld == 'x-snoozed') { - $snooze_header = parse_snooze_header($value); + $snooze_header = parse_nexter_header($value, 'X-Snoozed'); $txt .= ''; - $txt .= $this->trans('Snoozed').''.$this->trans('Until').' '.$this->html_safe($snooze_header['until']).' Unsnooze'; + $txt .= $this->trans('Snoozed').''.$this->trans('Until').' '.$this->html_safe($snooze_header['until']).' Unsnooze'; } elseif ($fld == 'date') { try { @@ -390,6 +390,9 @@ protected function output() { if($this->get('tags')){ $txt .= ' | '. tags_dropdown($this, $headers); } + if (isset($headers['X-Schedule'])) { + $txt .= ' | ' . schedule_dropdown($this, true); + } $is_draft = isset($headers['Flags']) && mb_stristr($headers['Flags'], 'draft'); if ($this->get('sieve_filters_enabled') && !$is_draft) { diff --git a/modules/imap/site.css b/modules/imap/site.css index f291345bab..db8576ec53 100644 --- a/modules/imap/site.css +++ b/modules/imap/site.css @@ -126,7 +126,7 @@ .header_links { padding-top: 10px !important; } -.header_links #dropdownMenuSnooze { +.header_links #dropdownMenuNexterDate { padding: 0; border: unset; font-variant: inherit; @@ -134,7 +134,7 @@ font-size: inherit; vertical-align: baseline; } -.header_links #dropdownMenuSnooze:hover { +.header_links #dropdownMenuNexterDate:hover { background-color: inherit; color: inherit; } diff --git a/modules/imap/site.js b/modules/imap/site.js index 4dbfb78178..8e71a0ccb4 100644 --- a/modules/imap/site.js +++ b/modules/imap/site.js @@ -1200,6 +1200,73 @@ $(function() { setTimeout(search_selected_for_imap, 100); }); + if (hm_page_name() === 'message_list' && hm_list_path().substr(0, 4) === 'imap') { + setup_imap_folder_page(); + } + else if (hm_page_name() === 'message_list' && hm_list_path() === 'combined_inbox') { + setup_imap_message_list_content_page(); + } + else if (hm_page_name() === 'message' && hm_list_path().substr(0, 4) === 'imap') { + imap_setup_message_view_page(); + } + else if (hm_page_name() === 'servers') { + imap_setup_server_page(); + } + else if (hm_page_name() === 'info') { + setTimeout(imap_status_update, 100); + } + + if (hm_page_name() === 'message_list' || hm_page_name() === 'message') { + imap_setup_snooze(); + imap_setup_tags(); + setup_nexter_date(function(e) { + $('.nexter_dropdown').hide(); + var ids = []; + if (hm_page_name() == 'message') { + var list_path = hm_list_path().split('_'); + ids.push(list_path[1]+'_'+hm_msg_uid()+'_'+list_path[2]); + } else { + $('input[type=checkbox]').each(function() { + if (this.checked && this.id.search('imap') != -1) { + var parts = this.id.split('_'); + ids.push(parts[1]+'_'+parts[2]+'_'+parts[3]); + } + }); + if (ids.length == 0) { + return; + }; + } + var reload_and_redirect = function() { + Hm_Folders.reload_folders(true); + var path = hm_list_parent()? hm_list_parent(): hm_list_path(); + window.location.replace('?page=message_list&list_path='+path); + } + if ($(this).parent().parent().is('.snooze_dropdown')) { + Hm_Ajax.request( + [{'name': 'hm_ajax_hook', 'value': 'ajax_imap_snooze'}, + {'name': 'imap_snooze_ids', 'value': ids}, + {'name': 'imap_snooze_until', 'value': $(this).val()}], + function(res) { + if (res.snoozed_messages > 0) { + reload_and_redirect(); + } + } + ); + } else { + Hm_Ajax.request( + [{'name': 'hm_ajax_hook', 'value': 'ajax_re_schedule_message_sending'}, + {'name': 'scheduled_msg_ids', 'value': ids}, + {'name': 'schedule_date', 'value': $(this).val()}], + function(res) { + if (res.scheduled_msg_count > 0) { + reload_and_redirect(); + } + } + ); + } + }); + } + if (hm_is_logged()) { imap_unsnooze_messages(); setInterval(imap_unsnooze_messages, 60000); diff --git a/modules/profiles/functions.php b/modules/profiles/functions.php index bb7abe0148..228cc936d0 100644 --- a/modules/profiles/functions.php +++ b/modules/profiles/functions.php @@ -12,7 +12,7 @@ function add_profile($name, $signature, $reply_to, $is_default, $email, $server_ 'replyto' => $reply_to, 'default' => $is_default, 'address' => $email, - 'server' => $server_mail, + 'server' => $imap_server_id, 'user' => $email, 'type' => 'imap' ); diff --git a/modules/profiles/hm-profiles.php b/modules/profiles/hm-profiles.php index 16997e0582..966827baa1 100644 --- a/modules/profiles/hm-profiles.php +++ b/modules/profiles/hm-profiles.php @@ -109,4 +109,14 @@ public static function loadLegacy($hmod) { } } } + + public static function search($fld, $value) { + $res = array(); + foreach (self::getAll() as $profile) { + if (!empty($profile[$fld]) && $profile[$fld] == $value) { + $res[] = $profile; + } + } + return $res; + } } diff --git a/modules/smtp/hm-mime-message.php b/modules/smtp/hm-mime-message.php index 1b775e0941..64cfe7b9dc 100644 --- a/modules/smtp/hm-mime-message.php +++ b/modules/smtp/hm-mime-message.php @@ -21,10 +21,15 @@ class Hm_MIME_Msg { private $final_msg = ''; /* build mime message data */ - function __construct($to, $subject, $body, $from, $html=false, $cc='', $bcc='', $in_reply_to_id='', $from_name='', $reply_to='') { + function __construct($to, $subject, $body, $from, $html=false, $cc='', $bcc='', $in_reply_to_id='', $from_name='', $reply_to='', $delivery_receipt='', $schedule='', $profile_id = '') { if ($cc) { $this->headers['Cc'] = $cc; } + if ($schedule) { + $this->headers['X-Schedule'] = $schedule; + $this->headers['X-Profile-ID'] = $profile_id; + } + if ($in_reply_to_id) { $this->headers['In-Reply-To'] = $in_reply_to_id; } @@ -41,6 +46,9 @@ function __construct($to, $subject, $body, $from, $html=false, $cc='', $bcc='', else { $this->headers['Reply-To'] = $from; } + if ($delivery_receipt) { + $this->headers['X-Delivery'] = $delivery_receipt; + } $this->headers['To'] = $to; $this->headers['Subject'] = html_entity_decode($subject, ENT_QUOTES); $this->headers['Date'] = date('r'); @@ -190,7 +198,7 @@ function prep_fld($val, $name) { return $this->encode_fld($val); } - function find_addresses($str) { + static function find_addresses($str) { $res = array(); foreach (process_address_fld($str) as $vals) { $res[] = $vals['email']; @@ -210,7 +218,7 @@ function get_recipient_addresses() { else { continue; } - $res = array_merge($res, $this->find_addresses($v)); + $res = array_merge($res, self::find_addresses($v)); } return $res; } diff --git a/modules/smtp/modules.php b/modules/smtp/modules.php index 6d2adb9eeb..4dbf2b58ab 100644 --- a/modules/smtp/modules.php +++ b/modules/smtp/modules.php @@ -114,6 +114,8 @@ public function process() { } } + + /** * @subpackage smtp/handler */ @@ -210,6 +212,7 @@ public function process() { } } + /** * @subpackage smtp/handler */ @@ -319,6 +322,8 @@ public function process() { $draft_id = array_key_exists('draft_id', $this->request->post) ? $this->request->post['draft_id'] : false; $draft_notice = array_key_exists('draft_notice', $this->request->post) ? $this->request->post['draft_notice'] : false; $uploaded_files = array_key_exists('uploaded_files', $this->request->post) ? $this->request->post['uploaded_files'] : false; + $delivery_receipt = array_key_exists('compose_delivery_receipt', $this->request->post) ? $this->request->post['compose_delivery_receipt'] : false; + $schedule = array_key_exists('schedule', $this->request->post) ? $this->request->post['schedule'] : ''; if (array_key_exists('delete_uploaded_files', $this->request->post) && $this->request->post['delete_uploaded_files']) { delete_uploaded_files($this->session, $draft_id); @@ -335,7 +340,7 @@ public function process() { if ($this->get('save_draft_to_imap') === false) { $from = isset($profile) ? $profile['replyto'] : ''; $name = isset($profile) ? $profile['name'] : ''; - $mime = prepare_draft_mime($msg_attrs, $uploaded_files, $from, $name); + $mime = prepare_draft_mime($msg_attrs, $uploaded_files, $from, $name, $profile['id']); $this->out('draft_mime', $mime); return; } @@ -345,15 +350,20 @@ public function process() { foreach($uploaded_files as $key => $file) { $uploaded_files[$key] = $this->config->get('attachment_dir').DIRECTORY_SEPARATOR.$userpath.DIRECTORY_SEPARATOR.$file; } - $new_draft_id = save_imap_draft($msg_attrs, $draft_id, $this->session, $this, $this->cache, $uploaded_files, $profile); + $new_draft_id = save_imap_draft(array('draft_smtp' => $smtp, 'draft_to' => $to, 'draft_body' => $body, + 'draft_subject' => $subject, 'draft_cc' => $cc, 'draft_bcc' => $bcc, + 'draft_in_reply_to' => $inreplyto, 'delivery_receipt' => $delivery_receipt, 'schedule' => $schedule), $draft_id, $this->session, + $this, $this->cache, $uploaded_files, $profile); if ($new_draft_id >= 0) { if ($draft_notice) { - Hm_Msgs::add('Draft saved'); + $msg = $schedule ? 'Message scheduled to be sent later' : 'Draft saved'; + Hm_Msgs::add($msg); } $this->out('draft_id', $new_draft_id); } elseif ($draft_notice) { - Hm_Msgs::add('ERRUnable to save draft'); + $msg = $schedule ? 'Something went wrong when scheduling draft' : 'Unable to save draft'; + Hm_Msgs::add('ERR' . $msg); } return; } @@ -660,6 +670,7 @@ public function process() { 'draft_subject' => $form['compose_subject'], 'draft_smtp' => $smtp_id ); + $delivery_receipt = !empty($this->request->post['compose_delivery_receipt']); $from_params = ''; $recipients_params = ''; @@ -711,7 +722,7 @@ public function process() { } /* build message */ - $mime = new Hm_MIME_Msg($to, $subject, $body, $from, $body_type, $cc, $bcc, $in_reply_to, $from_name, $reply_to); + $mime = new Hm_MIME_Msg($to, $subject, $body, $from, $body_type, $cc, $bcc, $in_reply_to, $from_name, $reply_to, $delivery_receipt); /* add attachments */ $mime->add_attachments($uploaded_files); @@ -880,6 +891,132 @@ protected function output() { } } +/** + * Send scheduled messages + * @subpackage smtp/handler + */ +class Hm_Handler_send_scheduled_messages extends Hm_Handler_Module { + /** + * Send delayed messages + * This should use cron + */ + public function process() { + if (!($this->module_is_supported('imap') || $this->module_is_supported('profiles'))) { + return; + } + $servers = Hm_IMAP_List::dump(); + $scheduled_msg_count = 0; + foreach (array_keys($servers) as $server_id) { + $cache = Hm_IMAP_List::get_cache($this->cache, $server_id); + $imap = Hm_IMAP_List::connect($server_id, $cache); + if (imap_authed($imap)) { + $folder = 'Scheduled'; + $ret = $imap->get_mailbox_page($folder, 'DATE', false, 'ALL'); + foreach ($ret[1] as $msg) { + $msg_headers = $imap->get_message_headers($msg['uid']); + try { + if (!empty($msg_headers['X-Schedule'])) { + $scheduled_msg_count++; + } else { + continue; + } + if (new DateTime($msg_headers['X-Schedule']) <= new DateTime()) { + $profile = Hm_Profiles::get($msg_headers['X-Profile-ID']); + if (!$profile) { + $profiles = Hm_Profiles::search('server', $imap_details['server']); + + if (!$profiles) { + Hm_Debug::add(sprintf('ERRCannot find profiles corresponding with IMAP server: %s', $imap_details['server'])); + continue; + } + $profile = $profiles[0]; + } + + $smtp = Hm_SMTP_List::connect($profile['smtp_id'], false); + + if (smtp_authed($smtp)) { + if (isset($msg_headers['X-Delivery'])) { + $from_params = 'RET=HDRS'; + $recipients_params = 'NOTIFY=SUCCESS,FAILURE'; + } else { + $from_params = ''; + $recipients_params = ''; + } + $recipients = []; + foreach (['To', 'Cc', 'Bcc'] as $fld) { + if (array_key_exists($fld, $msg_headers)) { + $recipients = array_merge($recipients, Hm_MIME_Msg::find_addresses($msg_headers[$fld])); + } + } + $msg_content = $imap->get_message_content($msg['uid'], 0); + $from = process_address_fld($msg_headers['From']); + + $err_msg = $smtp->send_message($from[0]['email'], $recipients, $msg_content, $from_params, $recipients_params); + + if (!$err_msg) { + if ($imap->message_action('DELETE', [$msg['uid']])) { + $imap->message_action('EXPUNGE', [$msg['uid']]); + } + save_sent_msg($this, $server_id, $imap, $imap_details, $msg_content, $msg['uid'], false); + $scheduled_msg_count--; + } + } + } + } catch (Exception $e) { + Hm_Debug::add(sprintf('ERRCannot send message: %s', $msg_headers['subject'])); + if (send_scheduled_message($this, $imap, $msg, $server_id)) { + $scheduled_msg_count++; + } + } + } + } + } + $this->out('scheduled_msg_count', $scheduled_msg_count); + } +} + +/** + * Changes the schedule of the message + * @subpackage smtp/handler + */ +class Hm_Handler_re_schedule_message_sending extends Hm_Handler_Module { + public function process() { + if (!($this->module_is_supported('imap') || $this->module_is_supported('profiles'))) { + return; + } + list($success, $form) = $this->process_form(array('schedule_date', 'scheduled_msg_ids')); + if (!$success) { + return; + } + $scheduled_msg_count = 0; + if ($form['schedule_date'] != 'now') { + $new_schedule_date = get_nexter_date($form['schedule_date']); + } + $ids = explode(',', $form['scheduled_msg_ids']); + foreach ($ids as $msg_part) { + list($imap_server_id, $msg_id, $folder) = explode('_', $msg_part); + $cache = Hm_IMAP_List::get_cache($this->cache, $imap_server_id); + $imap = Hm_IMAP_List::connect($imap_server_id, $cache); + if (imap_authed($imap)) { + $folder = hex2bin($folder); + if (reschedule_message_sending($this, $imap, $msg_id, $folder, $new_schedule_date, $imap_server_id)) { + $scheduled_msg_count++; + } + } + } + $this->out('scheduled_msg_count', $scheduled_msg_count); + if ($scheduled_msg_count == count($ids)) { + $msg = 'Operation successful'; + } elseif ($scheduled_msg_count > 0) { + $msg = 'Some messages have been scheduled for sending'; + } else { + $msg = 'ERRFailed to schedule sending for messages'; + } + Hm_Msgs::add($msg); + $this->save_hm_msgs(); + } +} + /** * @subpackage keyboard_shortcuts/output */ @@ -1231,11 +1368,15 @@ protected function output() { } } } - $res .= ''. smtp_server_dropdown($this->module_output(), $this, $recip, $selected_id). - ''; - + '
+ + '. + schedule_dropdown($this). + '
'; if ($this->get('list_path') && ($reply_type == 'reply' || $reply_type == 'reply_all')) { $res .= ''; } @@ -1528,6 +1669,20 @@ protected function output() { } } +/** + * Add scheduled send to the message list controls + * @subpackage imap/output + */ +class Hm_Output_scheduled_send_msg_control extends Hm_Output_Module { + protected function output() { + $parts = explode('_', $this->get('list_path')); + if ($parts[0] == 'imap' && hex2bin($parts[2]) == 'Scheduled') { + $res = schedule_dropdown($this, true); + $this->concat('msg_controls_extra', $res); + } + } +} + /** * @subpackage smtp/functions */ @@ -1857,7 +2012,7 @@ function get_uploaded_files_from_array($uploaded_files) { } } -function prepare_draft_mime($atts, $uploaded_files, $from = false, $name = '') { +function prepare_draft_mime($atts, $uploaded_files, $from = false, $name = '', $profile_id = null) { $uploaded_files = get_uploaded_files_from_array($uploaded_files); $mime = new Hm_MIME_Msg( $atts['draft_to'], @@ -1869,7 +2024,10 @@ function prepare_draft_mime($atts, $uploaded_files, $from = false, $name = '') { $atts['draft_bcc'], '', $name, - $atts['draft_in_reply_to'] + $atts['draft_in_reply_to'], + $atts['delivery_receipt'], + $atts['schedule'], + $profile_id ); $mime->add_attachments($uploaded_files); @@ -1908,15 +2066,25 @@ function save_imap_draft($atts, $id, $session, $mod, $mod_cache, $uploaded_files $specials = get_special_folders($mod, $imap_profile['id']); - if (!array_key_exists('draft', $specials) || !$specials['draft']) { + if ((!array_key_exists('draft', $specials) || !$specials['draft']) && !array_key_exists('schedule', $atts)) { Hm_Msgs::add('ERRThere is no draft directory configured for this account.'); return -1; } $cache = Hm_IMAP_List::get_cache($mod_cache, $imap_profile['id']); $imap = Hm_IMAP_List::connect($imap_profile['id'], $cache); - $draft_folder = $imap->select_mailbox($specials['draft']); - $mime = prepare_draft_mime($atts, $uploaded_files, $from, $name); + if (!empty($atts['schedule'])) { + $folder ='Scheduled'; + if (!count($imap->get_mailbox_status($folder))) { + $imap->create_mailbox($folder); + } + $atts['schedule'] = get_nexter_date($atts['schedule']); + } else { + $folder = $specials['draft']; + } + $imap->select_mailbox($folder); + + $mime = prepare_draft_mime($atts, $uploaded_files, $from, $name, $profile['id']); $res = $mime->process_attachments(); $msg = str_replace("\r\n", "\n", $mime->get_mime_msg()); @@ -1931,7 +2099,7 @@ function save_imap_draft($atts, $id, $session, $mod, $mod_cache, $uploaded_files } } - $mailbox_page = $imap->get_mailbox_page($specials['draft'], 'ARRIVAL', true, 'DRAFT', 0, 10); + $mailbox_page = $imap->get_mailbox_page($folder, 'ARRIVAL', true, 'DRAFT', 0, 10); // Remove old version from the mailbox if ($id) { @@ -2220,3 +2388,102 @@ function recip_count_check($headers, $omod) { Hm_Msgs::add('ERRMessage contains more than the maximum number of recipients, proceed with caution'); } }} + +/** + * @subpackage smtp/functions + */ +if (!hm_exists('send_scheduled_message')) { +function send_scheduled_message($handler, $imap, $msg, $server_id) { + $msg_headers = $imap->get_message_headers($msg['uid']); + $imap_details = Hm_IMAP_List::dump($server_id); + if (empty($imap_details)) { + return; + } + try { + if (empty($msg_headers['X-Schedule'])) { + return; + } + if (new DateTime($msg_headers['X-Schedule']) <= new DateTime()) { + $profile = Hm_Profiles::get($msg_headers['X-Profile-ID']); + if (!$profile) { + $profiles = Hm_Profiles::search('server', $imap_details['server']); + + if (!$profiles) { + Hm_Debug::add(sprintf('ERRCannot find profiles corresponding with IMAP server: %s', $imap_details['server'])); + return; + } + $profile = $profiles[0]; + } + + $smtp = Hm_SMTP_List::connect($profile['smtp_id'], false); + + if (smtp_authed($smtp)) { + if (isset($msg_headers['X-Delivery'])) { + $from_params = 'RET=HDRS'; + $recipients_params = 'NOTIFY=SUCCESS,FAILURE'; + } else { + $from_params = ''; + $recipients_params = ''; + } + $recipients = []; + foreach (['To', 'Cc', 'Bcc'] as $fld) { + if (array_key_exists($fld, $msg_headers)) { + $recipients = array_merge($recipients, Hm_MIME_Msg::find_addresses($msg_headers[$fld])); + } + } + $msg_content = $imap->get_message_content($msg['uid'], 0); + $from = process_address_fld($msg_headers['From']); + + $err_msg = $smtp->send_message($from[0]['email'], $recipients, $msg_content, $from_params, $recipients_params); + + if (!$err_msg) { + if ($imap->message_action('DELETE', [$msg['uid']])) { + $imap->message_action('EXPUNGE', [$msg['uid']]); + } + save_sent_msg($handler, $server_id, $imap, $imap_details, $msg_content, $msg['uid'], false); + return true; + } + } + } + } catch (Exception $e) { + Hm_Debug::add(sprintf('ERRCannot send message: %s', $msg_headers['subject'])); + } + return; +}} + +if (!hm_exists('reschedule_message_sending')) { +function reschedule_message_sending($handler, $imap, $msg_id, $folder, $new_date, $server_id) { + if (!$imap->select_mailbox($folder)) { + return; + } + $msg = $imap->get_message_content($msg_id, 0); + if ($new_date == 'now') { + return send_scheduled_message($handler, $imap, $msg, $server_id); + } + preg_match("/^X-Schedule:.*(\r?\n[ \t]+.*)*\r?\n?/im", $msg, $matches); + if (count($matches)) { + $new_date = get_nexter_date($new_date); + $msg = str_replace($matches[0], "X-Schedule: {$new_date}\n", $msg); + } else { + return; + } + $msg = str_replace("\r\n", "\n", $msg); + $msg = str_replace("\n", "\r\n", $msg); + $msg = rtrim($msg)."\r\n"; + + $schedule_folder = 'Scheduled'; + if (!count($imap->get_mailbox_status($schedule_folder))) { + return; + } + $res = false; + if ($imap->select_mailbox($schedule_folder) && $imap->append_start($schedule_folder, strlen($msg))) { + $imap->append_feed($msg."\r\n"); + if ($imap->append_end()) { + if ($imap->select_mailbox($folder) && $imap->message_action('DELETE', array($msg_id))) { + $imap->message_action('EXPUNGE', array($msg_id)); + $res = true; + } + } + } + return $res; +}} diff --git a/modules/smtp/setup.php b/modules/smtp/setup.php index 9549266349..60fd9218ea 100644 --- a/modules/smtp/setup.php +++ b/modules/smtp/setup.php @@ -21,6 +21,7 @@ add_handler('compose', 'load_smtp_is_imap_forward_as_attachment', true, 'smtp', 'load_user_data', 'after'); add_handler('compose', 'load_smtp_is_imap_forward', true, 'smtp', 'load_smtp_is_imap_forward_as_attachment', 'after'); + add_handler('functional_api', 'default_smtp_server', true, 'smtp'); add_handler('profiles', 'load_smtp_servers_from_config', true, 'smtp', 'load_user_data', 'after'); @@ -102,6 +103,20 @@ add_handler('settings', 'process_enable_compose_delivery_receipt_setting', true, 'core', 'save_user_settings', 'before'); add_output('settings', 'enable_compose_delivery_receipt_setting', true, 'core', 'start_general_settings', 'after'); +/* send delayed emails */ +setup_base_ajax_page('ajax_send_scheduled_messages', 'core'); +add_handler('ajax_send_scheduled_messages', 'load_imap_servers_from_config', true, 'imap', 'load_user_data', 'after'); +add_handler('ajax_send_scheduled_messages', 'load_smtp_servers_from_config', true, 'smtp', 'load_user_data', 'after'); +add_handler('ajax_send_scheduled_messages', 'compose_profile_data', true, 'profiles'); +add_handler('ajax_send_scheduled_messages', 'send_scheduled_messages', true, 'smtp'); + +setup_base_ajax_page('ajax_re_schedule_message_sending', 'core'); +add_handler('ajax_re_schedule_message_sending', 'load_imap_servers_from_config', true, 'imap', 'load_user_data', 'after'); +add_handler('ajax_re_schedule_message_sending', 'load_smtp_servers_from_config', true, 'smtp', 'load_user_data', 'after'); +add_handler('ajax_re_schedule_message_sending', 'compose_profile_data', true, 'profiles'); +add_handler('ajax_re_schedule_message_sending', 're_schedule_message_sending', true, 'smtp'); + +add_output('message_list', 'scheduled_send_msg_control', true, 'smtp', 'imap_custom_controls', 'after'); return array( 'allowed_pages' => array( @@ -112,7 +127,9 @@ 'ajax_profiles_status', 'ajax_attachment_reminder_check', 'ajax_get_test_chunk', - 'ajax_upload_chunk' + 'ajax_upload_chunk', + 'ajax_send_scheduled_messages', + 'ajax_re_schedule_message_sending' ), 'allowed_get' => array( 'imap_draft' => FILTER_VALIDATE_INT, @@ -144,9 +161,13 @@ 'msg_sent_and_archived' => array(FILTER_VALIDATE_BOOLEAN, false), 'sent_msg_id' => array(FILTER_VALIDATE_BOOLEAN, false), 'enable_attachment_reminder' => array(FILTER_VALIDATE_BOOLEAN, false), + 'scheduled_msg_count' => array(FILTER_VALIDATE_INT, false), ), 'allowed_post' => array( 'post_archive' => FILTER_VALIDATE_INT, + 'send_tomorrow_morning' => FILTER_DEFAULT, + 'send_today_afternoon' => FILTER_DEFAULT, + 'schedule_sending' => FILTER_DEFAULT, 'attachment_id' => FILTER_DEFAULT, 'smtp_compose_type' => FILTER_VALIDATE_INT, 'new_smtp_name' => FILTER_DEFAULT, @@ -185,6 +206,9 @@ 'uploaded_files' => FILTER_DEFAULT, 'send_uploaded_files' => FILTER_DEFAULT, 'next_email_post' => FILTER_DEFAULT, - 'enable_attachment_reminder' => FILTER_VALIDATE_INT + 'enable_attachment_reminder' => FILTER_VALIDATE_INT, + 'schedule' => FILTER_DEFAULT, + 'schedule_date' => FILTER_DEFAULT, + 'scheduled_msg_ids' => FILTER_DEFAULT, ) ); diff --git a/modules/smtp/site.js b/modules/smtp/site.js index 44c62b2bb5..def3ef7b06 100644 --- a/modules/smtp/site.js +++ b/modules/smtp/site.js @@ -69,7 +69,7 @@ var send_archive = function() { document.getElementsByClassName("smtp_send_placeholder")[0].click(); } -var save_compose_state = function(no_files, notice) { +var save_compose_state = function(no_files, notice, schedule, callback) { var no_icon = true; if (notice) { no_icon = false; @@ -82,6 +82,7 @@ var save_compose_state = function(no_files, notice) { var cc = $('.compose_cc').val(); var bcc = $('.compose_bcc').val(); var inreplyto = $('.compose_in_reply_to').val(); + var delivery_receipt = $('#compose_delivery_receipt').prop('checked'); var draft_id = $('.compose_draft_id').val(); if (globals.draft_state == body+subject+to+smtp+cc+bcc+uploaded_files) { @@ -105,6 +106,8 @@ var save_compose_state = function(no_files, notice) { {'name': 'draft_in_reply_to', 'value': inreplyto}, {'name': 'delete_uploaded_files', 'value': no_files}, {'name': 'draft_to', 'value': to}, + {'name': 'schedule', 'value': schedule}, + {'name': 'compose_delivery_receipt', 'value': delivery_receipt}, {'name': 'uploaded_files', 'value': uploaded_files}], function(res) { if (res.draft_id) { @@ -113,6 +116,9 @@ var save_compose_state = function(no_files, notice) { if (res.draft_subject) { $('.draft_list .draft_'+draft_id+' a').text(res.draft_subject); } + if (callback) { + callback(res); + } }, [], no_icon @@ -138,7 +144,7 @@ function smtpServersPageHandler() { } } -var reset_smtp_form = function() { +var reset_smtp_form = function(save = true) { $('.compose_body').val(''); $('.compose_subject').val(''); $('.compose_to').val(''); @@ -146,7 +152,10 @@ var reset_smtp_form = function() { $('.compose_bcc').val(''); $('.ke-content', $('iframe').contents()).html(''); $('.uploaded_files').html(''); - save_compose_state(true); + $('#compose_delivery_receipt').prop('checked', false); + if (save) { + save_compose_state(true); + } }; var replace_cursor_positon = function (txtElement) { @@ -381,3 +390,264 @@ function smtpSettingsPageHandler() { ); }); } +$(function () { + if (!hm_is_logged()) { + return; + } + if (hm_page_name() === 'settings') { + $('#clear_chunks_button').on('click', function(e) { + e.preventDefault(); + Hm_Ajax.request( + [{'name': 'hm_ajax_hook', 'value': 'ajax_clear_attachment_chunks'}], + function(res) { + + }, + [] + ); + }); + } + if (hm_page_name() === 'compose') { + init_resumable_upload() + setup_nexter_date(function() { + $('.smtp_send_placeholder').trigger('click'); + }); + + var interval = Hm_Utils.get_from_global('compose_save_interval', 30); + Hm_Timer.add_job(function() { save_compose_state(); }, interval, true); + $('.draft_title').on("click", function() { $('.draft_list').toggle(); }); + $('.toggle_recipients').on("click", function() { return toggle_recip_flds(); }); + $('.smtp_reset').on("click", reset_smtp_form); + $('.delete_draft').on("click", function() { smtp_delete_draft($(this).data('id')); }); + $('.smtp_save').on("click", function() { save_compose_state(false, true); }); + $('.smtp_send_archive').on("click", function() { send_archive(false, true); }); + + const modal = new Hm_Modal({ + modalId: 'emptySubjectBodyModal', + title: 'Warning', + btnSize: 'sm' + }); + + $('.smtp_send_placeholder').on("click", function (e) { + if (window.kindEditor) { + kindEditor.sync(); + } + + if (window.mdEditor) { + mdEditor.codemirror.save(); + } + + const body = $('.compose_body').val().trim(); + const subject = $('.compose_subject').val().trim(); + + let modalContentHeadline = ''; + let dontWanValueInStorage = ''; + let showBtnSendAnywayDontWarnFuture = true; + + // If the subject is empty, we should warn the user + if (!subject) { + dontWanValueInStorage = 'dont_warn_empty_subject'; + modalContentHeadline = "Your subject is empty!"; + } + + // If the body is empty, we should warn the user + if (!body) { + dontWanValueInStorage = 'dont_warn_empty_body'; + modalContentHeadline = "Your body is empty!"; + } + + // if both the subject and the body are empty, we should warn the user + if (!body && !subject) { + dontWanValueInStorage = 'dont_warn_empty_subject_body'; + modalContentHeadline = "Your subject and body are empty!"; + } + + if (hm_module_is_supported('contacts')) { + var checkInList = check_cc_exist_in_contacts_list(); + // if contact_cc not exist in contact list for user + if (checkInList) { + modalContentHeadline = "Adress mail not exist in your contact list"; + showBtnSendAnywayDontWarnFuture = false; + } + + } + + // If the user has disabled the warning, we should send the message + if (Boolean(Hm_Utils.get_from_local_storage(dontWanValueInStorage))) { + handleSendAnyway(); + } + // Otherwise, we should show the modal if we have a headline + else if (modalContentHeadline) { + modalContentHeadline = `

${hm_trans(modalContentHeadline)}

`; + return showModal(modalContentHeadline); + } + // Subject and body are not empty, we can send the message + else { + handleSendAnyway(); + } + + /* + ======================================== + Functions declarations + ======================================== + */ + function showModal() { + if (! modal.modalContent.html()) { + modal.addFooterBtn(hm_trans('Send anyway'), 'btn-warning', handleSendAnyway); + if (showBtnSendAnywayDontWarnFuture) { + modal.addFooterBtn(hm_trans("Send anyway and don't warn in the future"), 'btn-warning', handleSendAnywayAndDontWarnMe); + } + } + modal.setContent(modalContentHeadline + checkInList + `

${hm_trans('Are you sure you want to send this message?')}

`); + modal.open(); + } + + function handleSendAnyway() { + if (handleMissingAttachment()) { + if ($('.nexter_input').val()) { + save_compose_state(false, true, $('.nexter_input').val(), function(res) { + if (!res.router_user_msgs[0].startsWith('ERR')) { + reset_smtp_form(false); + Hm_Folders.reload_folders(true); + Hm_Utils.redirect(); + } + }); + } else { + document.getElementsByClassName("smtp_send")[0].click(); + } + } else { + e.preventDefault(); + } + }; + + function handleSendAnywayAndDontWarnMe() { + Hm_Utils.save_to_local_storage(dontWanValueInStorage, true); + handleSendAnyway(); + }; + + function handleMissingAttachment() { + var uploaded_files = $("input[name='uploaded_files[]']").map(function () { return $(this).val(); }).get(); + const compose_body_value = document.getElementById('compose_body').value; + const force_send = document.getElementById('force_send')?.value; + var reminder_value = $('.compose_form').data('reminder'); + if (reminder_value === 1 && force_send !== '1') { + let all_translated_keywords = []; + for (let lang in window.hm_translations) { + if (window.hm_translations.hasOwnProperty(lang)) { + // Get translated keywords for the current language + const translated_keywords = hm_trans('attachment,file,attach,attached,attaching,enclosed,CV,cover letter', lang).split(','); + // Concatenate translated keywords with the array + all_translated_keywords = all_translated_keywords.concat(translated_keywords); + } + } + const additional_keywords = ['.doc', '.pdf']; + // Split the translated keywords into an array && Add additional keywords or file extensions + const combined_keywords = all_translated_keywords.concat(additional_keywords); + // Build the regex pattern + const pattern = new RegExp('(' + combined_keywords.map(keyword => keyword.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')).join('|') + ')', 'i'); + // Check if the pattern is found in the message + if (pattern.test(compose_body_value) && uploaded_files.length === 0) { + + if (confirm(hm_trans('We couldn\'t find the attachment you referred to. Please confirm if you attached it or provide the details again.'))) { + force_send_message(); + } else { + return false; + } + } + } + return true; + } + }); + $('.compose_form').on('submit', function() { + process_compose_form(); + }); + if ($('.compose_cc').val() || $('.compose_bcc').val()) { + toggle_recip_flds(); + } + if (window.location.href.search('&reply=1') !== -1 || window.location.href.search('&reply_all=1') !== -1) { + replace_cursor_positon ($('textarea[name="compose_body"]')); + } + if (window.location.href.search('&forward=1') !== -1) { + setTimeout(function() { + save_compose_state(); + }, 100); + } + if ($('.sys_messages').text() != 'Message Sent') { + get_smtp_profile($('.compose_server').val()); + } + $('.compose_server').on('change', function() { + get_smtp_profile($('.compose_server').val()); + }); + if($('.compose_attach_button').attr('disabled') == 'disabled'){ + check_attachment_dir_access(); + }; + + $('.compose_container').attr('ondrop', 'move_recipient_to_section(event)').attr('ondragover', 'allow_drop(event)'); + $('.compose_to, .compose_cc, .compose_bcc').on('keypress', function(e) { + if(e.which == 13) { + e.preventDefault(); + text_to_bubbles(this); + } + }); + $('.compose_to, .compose_cc, .compose_bcc').on('blur', function(e) { + e.preventDefault(); + text_to_bubbles(this); + }); + $('.compose_subject, .compose_body, .compose_server, .smtp_send_placeholder, .smtp_send_archive').on('focus', function(e) { + $('.compose_to, .compose_cc, .compose_bcc').each(function() { + bubbles_to_text(this); + }); + }); + $('.compose_to, .compose_cc, .compose_bcc').on('focus', function(e) { + text_to_bubbles(this); + }); + $('.compose_container').on('click', function() { + $(this).find('input').focus(); + }); + $(document).on('click', '.bubble_close', function(e) { + e.stopPropagation(); + $(".bubble_dropdown-content").remove(); + $(this).parent().remove(); + }); + + var selectedOption = $('#compose_smtp_id option[selected]'); + var selectedEmail = selectedOption.data('email'); + var selectedVal = selectedOption.val(); + + var recipientsInput = $('#compose_cc'); + var excludedEmail = null; + + const excludeEmail = function () { + var newRecipients = recipientsInput.val().split(',').filter(function(email) { + if (email.includes(selectedEmail)) { + excludedEmail = email; + return false; + } + return true; + }).join(', '); + recipientsInput.val(newRecipients); + }; + + if (recipientsInput.val().includes(selectedEmail)) { + excludeEmail(); + $(document).on('change', '#compose_smtp_id', function() { + if ($(this).val() !== selectedVal) { + if (!recipientsInput.val().includes(selectedEmail)) { + recipientsInput.val(recipientsInput.val() + ', ' + excludedEmail); + } + } else { + excludeEmail(); + } + }); + } + + } + send_scheduled_messages(); + setInterval(send_scheduled_messages, 60000); + + window.onbeforeunload = (e) => { + if (scheduled_msg_count > 0 && e.currentTarget.location.hostname !== document.location.hostname) { + return sprintf(hm_trans("You have %d scheduled messages that won\'t be executed if you quit"), scheduled_msg_count); + } + return; + }; +});