From 8cc9ce5bdd83d55c62c20685a5f897211dc85232 Mon Sep 17 00:00:00 2001 From: Josaphat Imani Date: Mon, 5 Dec 2022 23:00:11 +0100 Subject: [PATCH] Added snooze functionnality to messages --- modules/imap/functions.php | 147 +++++++++++++++++++++++++++++++ modules/imap/handler_modules.php | 80 +++++++++++++++++ modules/imap/output_modules.php | 18 +++- modules/imap/setup.php | 26 +++++- modules/imap/site.css | 10 ++- modules/imap/site.js | 74 ++++++++++++++++ 6 files changed, 350 insertions(+), 5 deletions(-) diff --git a/modules/imap/functions.php b/modules/imap/functions.php index db0bfef27..51b657181 100644 --- a/modules/imap/functions.php +++ b/modules/imap/functions.php @@ -1144,3 +1144,150 @@ function get_personal_ns($imap) { ); }} +/** + * @subpackage imap/functions + */ +if (!hm_exists('snooze_message')) { +function snooze_message($imap, $msg_id, $folder, $snooze_tag) { + if (!$imap->select_mailbox($folder)) { + return false; + } + if (!$snooze_tag) { + $imap->message_action('UNREAD', array($msg_id)); + } + $msg = $imap->get_message_content($msg_id, 0); + 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']; + } + if ($snooze_tag) { + $from = $old_folder ?? $folder; + $msg = "$snooze_tag;\n \tfrom $from\n".$msg; + } + $msg = str_replace("\r\n", "\n", $msg); + $msg = str_replace("\n", "\r\n", $msg); + $msg = rtrim($msg)."\r\n"; + + $res = false; + $snooze_folder = 'Snoozed'; + if ($snooze_tag) { + if (!count($imap->get_mailbox_status($snooze_folder))) { + $imap->create_mailbox($snooze_folder); + } + if ($imap->select_mailbox($snooze_folder) && $imap->append_start($snooze_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; + } + } + } + } else { + $snooze_headers = parse_snooze_header($matches[0]); + $original_folder = $snooze_headers['from']; + if ($imap->select_mailbox($original_folder) && $imap->append_start($original_folder, strlen($msg))) { + $imap->append_feed($msg."\r\n"); + if ($imap->append_end()) { + if ($imap->select_mailbox($snooze_folder) && $imap->message_action('DELETE', array($msg_id))) { + $imap->message_action('EXPUNGE', array($msg_id)); + $res = true; + } + } + } + } + return $res; +}} + +/** + * @subpackage imap/functions + */ +if (!hm_exists('parse_snooze_header')) { +function parse_snooze_header($snooze_header) +{ + $snooze_header = str_replace('X-Snoozed: ', '', $snooze_header); + $result = []; + foreach (explode(';', str_replace("\r\n", " ", $snooze_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; +}} + +/** + * @subpackage imap/functions + */ +if (!hm_exists('get_snooze_date')) { +function get_snooze_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('snooze_formats')) { +function snooze_formats() { + return array( + 'tomorrow', + 'next_weekend', + 'next_week', + 'next_month' + ); +}} + +/** + * @subpackage imap/functions + */ +if (!hm_exists('snooze_dropdown')) { +function snooze_dropdown($output, $unsnooze = false) { + if (date('H') <= 16) { + $values = array_merge(['later_in_day'], snooze_formats()); + } + $txt = '
'; + $txt .= ''.$output->trans('Snooze').''; + $txt .= '
'; + + return $txt; +}} + diff --git a/modules/imap/handler_modules.php b/modules/imap/handler_modules.php index a4d612e9f..e4872e94d 100644 --- a/modules/imap/handler_modules.php +++ b/modules/imap/handler_modules.php @@ -907,6 +907,86 @@ public function process() { } } +/** + * Snooze message + * @subpackage imap/handler + */ +class Hm_Handler_imap_snooze_message extends Hm_Handler_Module { + /** + * Use IMAP to snooze the selected message uid + */ + public function process() { + list($success, $form) = $this->process_form(array('imap_snooze_ids', 'imap_snooze_until')); + if (!$success) { + return; + } + $snoozed_messages = 0; + $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']); + $snooze_tag = "X-Snoozed: at $at;\n \tuntil $until"; + } + $ids = explode(',', $form['imap_snooze_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 (snooze_message($imap, $msg_id, $folder, $snooze_tag)) { + $snoozed_messages++; + } + } + } + $this->out('snoozed_messages', $snoozed_messages); + if ($snoozed_messages == count($ids)) { + $msg = 'Messages snoozed'; + } elseif ($snoozed_messages > 0) { + $msg = 'Some messages have been snoozed'; + } else { + $msg = 'ERRFailed to snooze selected messages'; + } + Hm_Msgs::add($msg); + $msgs = Hm_Msgs::get(); + Hm_Msgs::flush(); + $this->session->secure_cookie($this->request, 'hm_msgs', base64_encode(json_encode($msgs))); + } +} + +/** + * Unsnooze messages + * @subpackage imap/handler + */ +class Hm_Handler_imap_unsnooze_message extends Hm_Handler_Module { + /** + * Use IMAP unsnooze messages in snoozed directory + * This should use cron + */ + public function process() { + $servers = Hm_IMAP_List::dump(); + 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 = 'Snoozed'; + $ret = $imap->get_mailbox_page($folder, 'DATE', false, 'ALL'); + foreach ($ret[1] as $msg) { + $msg_headers = $imap->get_message_headers($msg['uid']); + try { + $snooze_headers = parse_snooze_header($msg_headers['X-Snoozed']); + if (new DateTime($snooze_headers['until']) <= new DateTime()) { + snooze_message($imap, $msg['uid'], $folder, null); + } + } catch (Exception $e) { + Hm_Debug::add(sprintf('ERRCannot unsnooze message: %s', $msg_headers['subject'])); + } + } + } + } + } +} + /** * Perform an IMAP message action * @subpackage imap/handler diff --git a/modules/imap/output_modules.php b/modules/imap/output_modules.php index 633c0d9ee..4293bbca2 100644 --- a/modules/imap/output_modules.php +++ b/modules/imap/output_modules.php @@ -281,7 +281,8 @@ protected function output() { $txt .= ' | '.$this->trans('Delete').''; $txt .= ' | '.$this->trans('Copy').''; $txt .= ' | '.$this->trans('Move').''; - $txt .= ' | '.$this->trans('Archive').''; + $txt .= ' | '.$this->trans('Archive').''; + $txt .= ' | ' . snooze_dropdown($this, isset($headers['X-Snoozed'])); if ($this->get('sieve_filters_enabled')) { $imap_server = Hm_IMAP_List::get($this->get('msg_server_id'), false); @@ -1041,4 +1042,17 @@ protected function output() { $this->trans('Archive to the original folder').''. ''.$reset.''; } -} \ No newline at end of file +} + +/** + * Add snooze dialog to the message list controls + * @subpackage imap/output + */ +class Hm_Output_snooze_msg_control extends Hm_Output_Module { + protected function output() { + $parts = explode('_', $this->get('list_path')); + $unsnooze = $parts[0] == 'imap' && hex2bin($parts[2]) == 'Snoozed'; + $res = snooze_dropdown($this, $unsnooze); + $this->concat('msg_controls_extra', $res); + } +} diff --git a/modules/imap/setup.php b/modules/imap/setup.php index 6c9d44610..189db8746 100644 --- a/modules/imap/setup.php +++ b/modules/imap/setup.php @@ -65,6 +65,7 @@ add_handler('message_list', 'imap_message_list_type', true, 'imap', 'message_list_type', 'after'); add_output('message_list', 'imap_custom_controls', true, 'imap', 'message_list_heading', 'before'); add_output('message_list', 'move_copy_controls', true, 'imap', 'message_list_heading', 'before'); +add_output('message_list', 'snooze_msg_control', true, 'imap', 'imap_custom_controls', 'after'); /* message view page */ add_handler('message', 'imap_download_message', true, 'imap', 'message_list_type', 'after'); @@ -272,6 +273,22 @@ add_handler('ajax_update_server_pw', 'load_imap_servers_from_config', true, 'imap', 'load_user_data', 'after'); add_handler('ajax_update_server_pw', 'save_imap_servers', true, 'imap', 'save_user_data', 'before'); +/* snooze email */ +setup_base_ajax_page('ajax_imap_snooze', 'core'); +add_handler('ajax_imap_snooze', 'load_imap_servers_from_config', true); +add_handler('ajax_imap_snooze', 'imap_oauth2_token_check', true); +add_handler('ajax_imap_snooze', 'close_session_early', true, 'core'); +add_handler('ajax_imap_snooze', 'save_imap_cache', true); +add_handler('ajax_imap_snooze', 'imap_snooze_message', true, 'core'); + +/* unsnooze emails in snoozed folders */ +setup_base_ajax_page('ajax_imap_unsnooze', 'core'); +add_handler('ajax_imap_unsnooze', 'load_imap_servers_from_config', true); +add_handler('ajax_imap_unsnooze', 'imap_oauth2_token_check', true); +add_handler('ajax_imap_unsnooze', 'close_session_early', true, 'core'); +add_handler('ajax_imap_unsnooze', 'save_imap_cache', true); +add_handler('ajax_imap_unsnooze', 'imap_unsnooze_message', true, 'core'); + /* allowed input */ return array( 'allowed_pages' => array( @@ -295,6 +312,8 @@ 'ajax_imap_mark_as_read', 'ajax_imap_move_copy_action', 'ajax_imap_folder_status', + 'ajax_imap_snooze', + 'ajax_imap_unsnooze', ), 'allowed_output' => array( @@ -312,7 +331,8 @@ 'combined_inbox_server_ids' => array(FILTER_SANITIZE_FULL_SPECIAL_CHARS, false), 'imap_delete_error' => array(FILTER_VALIDATE_BOOLEAN, false), 'move_count' => array(FILTER_SANITIZE_FULL_SPECIAL_CHARS, FILTER_REQUIRE_ARRAY), - 'show_pagination_links' => array(FILTER_VALIDATE_BOOLEAN, false) + 'show_pagination_links' => array(FILTER_VALIDATE_BOOLEAN, false), + 'snoozed_messages' => array(FILTER_VALIDATE_INT, false), ), 'allowed_get' => array( @@ -370,7 +390,9 @@ 'imap_move_page' => FILTER_SANITIZE_FULL_SPECIAL_CHARS, 'compose_unflag_send' => FILTER_VALIDATE_BOOLEAN, 'imap_per_page' => FILTER_VALIDATE_INT, - 'original_folder' => FILTER_VALIDATE_BOOLEAN + 'original_folder' => FILTER_VALIDATE_BOOLEAN, + 'imap_snooze_ids' => FILTER_SANITIZE_FULL_SPECIAL_CHARS, + 'imap_snooze_until' => FILTER_SANITIZE_FULL_SPECIAL_CHARS ) ); diff --git a/modules/imap/site.css b/modules/imap/site.css index 8f01cb5cc..dcd9c7f24 100644 --- a/modules/imap/site.css +++ b/modules/imap/site.css @@ -87,4 +87,12 @@ #archive_val { padding-left: 20px; } .attached_image { margin-right: 20px; margin-bottom: 20px; height: 200px; } -.attached_image_box { display: flex; flex-wrap: wrap; border-top: solid 1px #ddd; padding-top: 20px; padding-left: 20px; width: 100%; padding-bottom: 40px; } \ No newline at end of file +.attached_image_box { display: flex; flex-wrap: wrap; border-top: solid 1px #ddd; padding-top: 20px; padding-left: 20px; width: 100%; padding-bottom: 40px; } + +.snooze_date_picker, .snooze_helper { color: #333; text-transform: capitalize; } +.snooze_date_picker { border-top: 1px solid #ddd; } +.snooze_dropdown { position: absolute; margin-top: 15px; background-color: #fff; border: 1px solid #ddd; box-shadow: 3px 3px 3px #ddd; font-variant: none; min-width: 250px; } +.snooze_date_picker:hover, .snooze_helper:hover { background-color: #eee; } +.header_links a.snooze_date_picker, .header_links a.snooze_helper, .msg_controls a.snooze_date_picker, .msg_controls a.snooze_helper { padding: 8px; padding-left: 15px !important; padding-right: 15px !important; white-space: nowrap; font-size: 1rem; display: block !important; margin-right: 0; border: 0; } +.snooze_helper span { float: right; } +.snooze_date_picker { display: block; padding: 8px 15px; color: #333; font-size: 1rem; cursor: pointer; } diff --git a/modules/imap/site.js b/modules/imap/site.js index 27f3fc313..cbd6694d5 100644 --- a/modules/imap/site.js +++ b/modules/imap/site.js @@ -998,6 +998,73 @@ var imap_folder_status = function() { } }; +var imap_setup_snooze = function() { + $(document).on('click', '#snooze_message', function(e) { + e.preventDefault(); + $('.snooze_dropdown').toggle(); + $('.snooze_input').hide(); + }); + $(document).on('click', '.snooze_date_picker', function(e) { + document.querySelector('.snooze_input_date').showPicker(); + }); + $(document).on('click', '.snooze_helper', function(e) { + e.preventDefault(); + $('.snooze_input').val($(this).attr('data-value')).trigger('change'); + }); + $(document).on('input', '.snooze_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()) { + $('.snooze_date_picker').css('border', '1px solid red'); + } else { + $('.snooze_date_picker').css({'border': 'unset', 'border-top': '1px solid #ddd'}); + } + }); + $(document).on('change', '.snooze_input_date', function(e) { + if ($(this).val() && new Date().getTime() < new Date($(this).val()).getTime()) { + $('.snooze_input').val($(this).val()).trigger('change'); + } + }); + $(document).on('change', '.snooze_input', function(e) { + $('.snooze_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; + }; + } + 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) { + Hm_Folders.reload_folders(true); + var path = hm_list_parent()? hm_list_parent(): hm_list_path(); + window.location.href = '?page=message_list&list_path='+path; + } + } + ); + }); +} + +var imap_unsnooze_messages = function() { + Hm_Ajax.request( + [{'name': 'hm_ajax_hook', 'value': 'ajax_imap_unsnooze'}], + function() {}, + ); +} + if (hm_list_path() == 'sent') { Hm_Message_List.page_caches.sent = 'formatted_sent_data'; } @@ -1038,6 +1105,13 @@ $(function() { 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_unsnooze_messages(); + setInterval(imap_unsnooze_messages, 60000); if ($('.imap_move').length > 0) { check_select_for_imap();