diff --git a/api/js/jsapi/app_base.js b/api/js/jsapi/app_base.js index 7a600af678..9076c95267 100644 --- a/api/js/jsapi/app_base.js +++ b/api/js/jsapi/app_base.js @@ -210,25 +210,67 @@ var AppJS = (function(){ "use strict"; return Class.extend( }, /** - * Push method receives push notification about updates to entries from the application + * Handle a push notification about entry changes from the websocket * - * It can use the extra _data parameter to determine if the client has read access to - * the entry - if an update of the list is necessary. + * Get's called for data of all apps, but should only handle data of apps it displays, + * which is by default only it's own, but can be for multiple apps eg. for calendar. * - * @param {string} _type either 'update', 'edit', 'delete', 'add' or null + * @param pushData + * @param {string} pushData.app application name + * @param {(string|number)} pushData.id id of entry to refresh or null + * @param {string} pushData.type either 'update', 'edit', 'delete', 'add' or null * - update: request just modified data from given rows. Sorting is not considered, * so if the sort field is changed, the row will not be moved. * - edit: rows changed, but sorting may be affected. Requires full reload. * - delete: just delete the given rows clientside (no server interaction neccessary) * - add: requires full reload for proper sorting - * @param {string} _app application name - * @param {(string|number)} _id id of entry to refresh or null - * @param {mixed} _data eg. owner or responsible to decide if update is necessary - * @returns {undefined} + * @param {object|null} pushData.acl Extra data for determining relevance. eg: owner or responsible to decide if update is necessary + * @param {number} pushData.account_id User that caused the notification */ - push: function(_type, _app, _id, _data) + push: function(pushData) { + // don't care about other apps data, reimplement if your app does care eg. calendar + if (pushData.app !== this.appname) return; + // only handle delete by default, for simple case of uid === "$app::$id" + if (pushData.type === 'delete') + { + egw.dataStoreUID(this.uid(pushData), null); + } + }, + + /** + * Get (possible) app-specific uid + * + * @param {object} pushData see push method for individual attributes + */ + uid(pushData) + { + return pushData.app + '::' + pushData.id; + }, + + /** + * Method called after apps push implementation checked visibility + * + * @param {et2_nextmatch} nm + * @param pushData see push method for individual attributes + * @todo implement better way to update nextmatch widget without disturbing the user / state + * @todo show indicator that an update has happend + * @todo rate-limit update frequency + */ + updateList: function(nm, pushData) + { + switch (pushData.type) + { + case 'add': + case 'unknown': + nm.applyFilters(); + break; + + default: + egw.dataRefreshUID(this.uid(pushData)); + break; + } }, /** @@ -243,7 +285,7 @@ var AppJS = (function(){ "use strict"; return Class.extend( open: function(_action, _senders) { var id_app = _senders[0].id.split('::'); egw.open(id_app[1], this.appname); - }, + }, /** * A generic method to action to server asynchronously diff --git a/api/src/Mail/Imap/Dovecot.php b/api/src/Mail/Imap/Dovecot.php index 1c0aca7f18..302d4a7dbc 100644 --- a/api/src/Mail/Imap/Dovecot.php +++ b/api/src/Mail/Imap/Dovecot.php @@ -14,6 +14,7 @@ namespace EGroupware\Api\Mail\Imap; use EGroupware\Api; use EGroupware\Api\Mail; +use EGroupware\SwoolePush\Tokens; /** * Manages connection to Dovecot IMAP server @@ -24,7 +25,7 @@ use EGroupware\Api\Mail; * --> require by webserver writable user_home to be configured, otherwise deleting get ignored like with defaultimap * - quota can be read, but not set */ -class Dovecot extends Mail\Imap +class Dovecot extends Mail\Imap implements Mail\Imap\PushIface { /** * Label shown in EMailAdmin @@ -281,4 +282,49 @@ class Dovecot extends Mail\Imap // mailbox get's automatic created with full rights for user return true; } + + /** + * Metadata name to enable push notifications + */ + const METADATA_NAME = '/private/vendor/vendor.dovecot/http-notify'; + const METADATA_MAILBOX = ''; + const METADATA_PREFIX = 'user='; + + /** + * Enable push notifictions for current connection and given account_id + * + * @param int $account_id =null 0=everyone on the instance + * @return bool true on success, false on failure + */ + function enablePush($account_id=null) + { + if (!class_exists(Tokens::class)) + { + return false; + } + try { + $this->setMetadata(self::METADATA_MAILBOX, [ + self::METADATA_NAME => self::METADATA_PREFIX.$GLOBALS['egw_info']['user']['account_id'].'::'.$this->acc_id.';'. + $this->getMailBoxUserName($GLOBALS['egw_info']['user']['account_lid']) . ';' . + ((string)$account_id === '0' ? Tokens::instance() : Tokens::user($account_id)) . '@' . + Api\Header\Http::host(), + ]); + } + catch (Horde_Imap_Client_Exception $e) { + _egw_log_exception($e); + return false; + } + return true; + } + + /** + * Check if push is available / konfigured for given server + * + * @return bool + */ + function pushAvailable() + { + return in_array($this->acc_imap_host, ['imap.egroupware.org', 'mail.egroupware.org']) || + $this->acc_imap_host === 'mail' && $this->acc_imap_port == 10143; + } } diff --git a/api/src/Mail/Imap/Iface.php b/api/src/Mail/Imap/Iface.php index ee68b7d49b..10e8571f94 100644 --- a/api/src/Mail/Imap/Iface.php +++ b/api/src/Mail/Imap/Iface.php @@ -5,10 +5,9 @@ * @link http://www.stylite.de * @package api * @subpackage mail - * @author Ralf Becker - * @author Stylite AG + * @author Ralf Becker + * @author EGroupware GmbH * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @version $Id$ */ namespace EGroupware\Api\Mail\Imap; diff --git a/api/src/Mail/Imap/PushIface.php b/api/src/Mail/Imap/PushIface.php new file mode 100644 index 0000000000..88879f38d6 --- /dev/null +++ b/api/src/Mail/Imap/PushIface.php @@ -0,0 +1,38 @@ + + * @author EGroupware GmbH + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + */ + +namespace EGroupware\Api\Mail\Imap; + +/** + * This class holds all information about the imap connection. + * This is the base class for all other imap classes. + * + * Also proxies Sieve calls to Mail\Sieve (eg. it behaves like the former felamimail bosieve), + * to allow IMAP plugins to also manage Sieve connection. + */ +interface PushIface +{ + /** + * Check if push is available / konfigured for given server + * + * @return bool + */ + function pushAvailable(); + + /** + * Enable push notifictions for current connection and given account_id + * + * @param int $account_id =null 0=everyone on the instance + * @return bool true on success, false on failure + */ + function enablePush($account_id=null); +} \ No newline at end of file diff --git a/mail/inc/class.mail_ui.inc.php b/mail/inc/class.mail_ui.inc.php index 9843ba0e99..7a0c2b8fc2 100644 --- a/mail/inc/class.mail_ui.inc.php +++ b/mail/inc/class.mail_ui.inc.php @@ -248,7 +248,16 @@ class mail_ui // save session varchar $oldicServerID =& Api\Cache::getSession('mail','activeProfileID'); - if ($oldicServerID <> self::$icServerID) $this->mail_bo->openConnection(self::$icServerID); + if ($oldicServerID != self::$icServerID) + { + $this->mail_bo->openConnection(self::$icServerID); + // enable push notifications, if supported (and konfigured) by the server + if ($this->mail_bo->icServer instanceof Api\Mail\Imap\PushIface && + $this->mail_bo->icServer->pushAvailable()) + { + $this->mail_bo->icServer->enablePush(); + } + } if (true) $oldicServerID = self::$icServerID; if (!Mail::storeActiveProfileIDToPref($this->mail_bo->icServer, self::$icServerID, true )) { diff --git a/mail/js/app.js b/mail/js/app.js index ecd3317d52..68a7bd7286 100644 --- a/mail/js/app.js +++ b/mail/js/app.js @@ -5,10 +5,9 @@ * * @link http://www.egroupware.org * @author EGroupware GmbH [info@egroupware.org] - * @copyright (c) 2013-2014 by EGroupware GmbH + * @copyright (c) 2013-2020 by EGroupware GmbH * @package mail * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License - * @version $Id$ */ /*egw:uses @@ -367,6 +366,54 @@ app.classes.mail = AppJS.extend( this.preSetToggledOnActions (); }, + /** + * Handle a push notification about entry changes from the websocket + * + * Get's called for data of all apps, but should only handle data of apps it displays, + * which is by default only it's own, but can be for multiple apps eg. for calendar. + * + * @param pushData + * @param {string} pushData.app application name + * @param {(string|number)} pushData.id id of entry to refresh or null + * @param {string} pushData.type either 'update', 'edit', 'delete', 'add' or null + * - update: request just modified data from given rows. Sorting is not considered, + * so if the sort field is changed, the row will not be moved. + * - edit: rows changed, but sorting may be affected. Requires full reload. + * - delete: just delete the given rows clientside (no server interaction neccessary) + * - add: requires full reload for proper sorting + * @param {object|null} pushData.acl Extra data for determining relevance. eg: owner or responsible to decide if update is necessary + * @param {number} pushData.account_id User that caused the notification + */ + push: function(pushData) + { + // don't care about other apps data, reimplement if your app does care eg. calendar + if (pushData.app !== this.appname) return; + + // only handle delete by default, for simple case of uid === "$app::$id" + if (pushData.type === 'delete') + { + return this._super.call(this, pushData); + } + + // notify user a new mail arrived + if (pushData.type === 'add') + { + this.egw.message(this.egw.lang('New mail from %1', pushData.acl.from)+'\n'+pushData.acl.subject+'\n'+pushData.acl.snippet, 'success'); + } + // check if we might not see it because we are on a different mail account or folder + let nm = this.et2 ? this.et2.getWidgetById('nm') : null; + let nm_value = nm ? nm.getValue() : null; + if (nm_value && nm_value.col_filter) + { + this.updateList(nm, pushData); + } + // update unseen counter in folder-tree + if (pushData.type === 'add' && pushData.acl.folder && pushData.acl.unseen) + { + // todo: pushData.id contains acc_id + } + }, + /** * Observer method receives update notifications from all applications *