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 = '
';
+
+ 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();