diff --git a/plugins/search-filters/index.php b/plugins/search-filters/index.php new file mode 100644 index 000000000..ba4ba2571 --- /dev/null +++ b/plugins/search-filters/index.php @@ -0,0 +1,211 @@ +UseLangs(true); + + $this->addHook('imap.after-login', 'ApplyFilters'); + + $this->addTemplate('templates/STabSearchFilters.html'); + + $this->addTemplate('templates/PopupsSearchFilters.html'); + $this->addTemplate('templates/PopupsSTabAdvancedSearch.html'); + $this->addJs('js/SearchFilters.js'); + + $this->addJsonHook('SGetFilters', 'GetFilters'); + $this->addJsonHook('SAddEditFilter', 'AddEditFilter'); + $this->addJsonHook('SUpdateSearchQ', 'UpdateSearchQ'); + $this->addJsonHook('SDeleteFilter', 'DeleteFilter'); + } + + public function ApplyFilters( + \RainLoop\Model\Account $oAccount, + \MailSo\Imap\ImapClient $oImapClient, + bool $bSuccess, + \MailSo\Imap\Settings $oSettings + ) { + if (!$bSuccess) { + return; + } + + $aSettings = $this->getUserSettings(); + if (empty($aSettings['SFilters'])) { + $aSettings['SFilters'] = []; + $this->saveUserSettings($aSettings); + return; + } + + $Filters = $aSettings['SFilters']; + + foreach ($Filters as $filter) { + $this->Manager()->logWrite(json_encode([ + 'filter' => $filter, + ]), LOG_WARNING); + + $folder = 'INBOX'; + $searchQ = $filter['searchQ']; + $uids = $this->searchMessages($oImapClient, $searchQ, "INBOX"); + + //Mark as read/seen + if ($filter['fSeen']) { + foreach ($uids as $uid) { + $oRange = new MailSo\Imap\SequenceSet([$uid]); + $this->Manager()->Actions()->MailClient()->MessageSetFlag( + $folder, + $oRange, + MailSo\Imap\Enumerations\MessageFlag::SEEN + ); + } + } + + //Flag/Star message + if ($filter['fFlag']) { + foreach ($uids as $uid) { + $oRange = new MailSo\Imap\SequenceSet([$uid]); + $this->Manager()->Actions()->MailClient()->MessageSetFlag( + $folder, + $oRange, + MailSo\Imap\Enumerations\MessageFlag::FLAGGED + ); + } + } + + // Move to folder + if ($filter['fFolder']) { + $folder = $filter['fFolder']; + foreach ($uids as $uid) { + $oRange = new MailSo\Imap\SequenceSet([$uid]); + $oImapClient->MessageMove("INBOX", $folder, $oRange); + } + } + } + } + + private function searchMessages( + \MailSo\Imap\ImapClient $imapClient, + string $search, + string $folder = "INBOX" + ): array { + $oParams = new \MailSo\Mail\MessageListParams(); + $oParams->sSearch = $search; + $oParams->sFolderName = $folder; + + $bUseCache = false; + $oSearchCriterias = \MailSo\Imap\SearchCriterias::fromString( + $imapClient, + $folder, + $search, + true, + $bUseCache + ); + + $imapClient->FolderSelect($folder); + return $imapClient->MessageSearch($oSearchCriterias, true); + } + + public function GetFilters() + { + $aSettings = $this->getUserSettings(); + $Filters = $aSettings['SFilters'] ?? []; + + $Search = $this->jsonParam('SSearchQ'); + if (!$Search) { + return $this->jsonResponse(__FUNCTION__, ['SFilters' => $Filters]); + } + + $Filter = null; + foreach ($aSettings['SFilters'] as $filter) { + if ($filter['searchQ'] == $Search) { + $Filter = $filter; + } + } + + return $this->jsonResponse(__FUNCTION__, ['SFilter' => $Filter]); + } + + public function AddEditFilter() + { + $SFilter = $this->jsonParam('SFilter'); + $newFilter = [ + 'searchQ' => $SFilter['searchQ'], + 'priority' => $SFilter['priority'] ?? 1, + 'fFolder' => $SFilter['fFolder'], + 'fSeen' => $SFilter['fSeen'], + 'fFlag' => $SFilter['fFlag'], + ]; + + $aSettings = $this->getUserSettings(); + $aSettings['SFilters'] = $aSettings['SFilters'] ?? []; + + $foundIndex = null; + foreach ($aSettings['SFilters'] as $index => $filter) { + if ($filter['searchQ'] == $SFilter['searchQ']) { + if ($filter['priority'] != $SFilter['priority']) { + array_splice($aSettings['SFilters'], $index, 1); + } else { + $foundIndex = $index; + } + } + } + + if ($foundIndex === null) { + $insertIndex = 0; + foreach ($aSettings['SFilters'] as $index => $filter) + if ($filter['priority'] >= $newFilter['priority']) + $insertIndex = $index + 1; + else + break; + + array_splice($aSettings['SFilters'], $insertIndex, 0, [$newFilter]); + } else { + $aSettings['SFilters'][$foundIndex] = $newFilter; + } + + return $this->jsonResponse(__FUNCTION__, $this->saveUserSettings($aSettings)); + } + + public function UpdateSearchQ() + { + $SFilter = $this->jsonParam('SFilter'); + + $aSettings = $this->getUserSettings(); + $aSettings['SFilters'] = $aSettings['SFilters'] ?? []; + + foreach ($aSettings['SFilters'] as $index => $filter) { + if ($filter['searchQ'] == $SFilter['oldSearchQ']) { + $filter['searchQ'] = $SFilter['searchQ']; + $aSettings['SFilters'][$index] = $filter; + break; + } + } + + return $this->jsonResponse(__FUNCTION__, $this->saveUserSettings($aSettings)); + } + + public function DeleteFilter() + { + $Search = $this->jsonParam('SSearchQ'); + + $aSettings = $this->getUserSettings(); + $aSettings['SFilters'] = $aSettings['SFilters'] ?? []; + + foreach ($aSettings['SFilters'] as $index => $filter) { + if ($filter['searchQ'] == $Search) { + array_splice($aSettings['SFilters'], $index, 1); + } + } + + return $this->jsonResponse(__FUNCTION__, $this->saveUserSettings($aSettings)); + } +} diff --git a/plugins/search-filters/js/SearchFilters.js b/plugins/search-filters/js/SearchFilters.js new file mode 100644 index 000000000..39c998548 --- /dev/null +++ b/plugins/search-filters/js/SearchFilters.js @@ -0,0 +1,317 @@ +((rl) => { + const EmptyOption = { id: -1, name: '' }; + const Folders = ko.computed(() => { + return [EmptyOption, ...rl.app.folderList().map((f) => ({ name: f.name }))]; + }); + const Priorities = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + const searchQ = ko.observable(''), + priority = ko.observable(1), + oldSearchQ = ko.observable(''), + fFolder = ko.observable(''), + fSeen = ko.observable(false), + fFlag = ko.observable(false), + ioFilters = ko.observableArray([]); + + const i18n = (val) => rl.i18n(`SFILTERS/${val}`); + + const ServerActions = { + GetFilters(loading) { + ioFilters([]); + rl.pluginRemoteRequest((iError, oData) => { + if (iError) return console.error(iError); + + oData.Result.SFilters.forEach((f) => ioFilters.push(ko.observable(f))); + + if (loading) loading(false); + }, 'SGetFilters'); + }, + GetFilter() { + rl.pluginRemoteRequest( + (iError, oData) => { + if (iError) return console.error(iError); + + const filter = oData.Result.SFilter || {}; + priority(filter.priority || 1); + fFolder(filter.fFolder); + fSeen(filter.fSeen); + fFlag(filter.fFlag); + }, + 'SGetFilters', + { + SSearchQ: searchQ() + } + ); + }, + AddOrEditFilter() { + rl.pluginRemoteRequest( + (iError, oData) => { + if (!iError) this.GetFilters(); + }, + 'SAddEditFilter', + { + SFilter: { + searchQ: oldSearchQ() || searchQ(), + priority: priority(), + fFolder: fFolder(), + fSeen: fSeen(), + fFlag: fFlag() + } + } + ); + }, + UpdateSearchQ(newSearchQ) { + rl.pluginRemoteRequest( + (iError, oData) => { + if (!iError) this.GetFilters(); + }, + 'SUpdateSearchQ', + { + SFilter: { + oldSearchQ: oldSearchQ(), + searchQ: newSearchQ + } + } + ); + }, + RemoveFilter(searchQToRemove) { + rl.pluginRemoteRequest( + (iError, oData) => { + if (iError) return console.error(iError, oData); + const index = ioFilters().findIndex((f) => f.searchQ() === searchQToRemove); + ioFilters.splice(index, 1); + }, + 'SDeleteFilter', + { + SSearchQ: searchQToRemove + } + ); + } + }; + + addEventListener('rl-view-model', (event) => { + const advS = event.detail; + if (advS.viewModelTemplateID == 'PopupsAdvancedSearch') { + const button = document.createElement('button'); + button.setAttribute('data-i18n', 'SFILTERS/CREATE_FILTER'); + button.addEventListener('click', function () { + searchQ(advS.buildSearchString()); + if (searchQ()) SearchFiltersPopupView.showModal(); + }); + + const footer = advS.querySelector('footer'); + footer.style.display = 'flex'; + footer.style.justifyContent = 'space-between'; + + footer.prepend(button); + } + }); + + class SearchFiltersSettingsTab { + constructor() { + this.folders = Folders; + this.Priorities = Priorities; + this.ioFilters = ioFilters; + + this.loading = ko.observable(false); + this.saving = ko.observable(false); + + this.i18n = i18n; + + this.savingOrLoading = ko.computed(() => { + return this.loading() || this.saving(); + }); + } + + edit(filter) { + oldSearchQ(filter.searchQ); + searchQ(filter.searchQ); + priority(filter.priority || 1); + fFolder(filter.fFolder); + fSeen(filter.fSeen); + fFlag(filter.fFlag); + AdvancedSearchPopupView.showModal(); + } + + remove(filter) { + ServerActions.RemoveFilter(filter.searchQ); + } + + onShow() { + this.clear(); + this.loading(true); + ServerActions.GetFilters(this.loading); + } + + clear() { + this.ioFilters([]); + this.loading(false); + this.saving(false); + } + } + + rl.addSettingsViewModel(SearchFiltersSettingsTab, 'STabSearchFilters', 'Filters', 'filters'); + + class SearchFiltersPopupView extends rl.pluginPopupView { + constructor() { + super('SearchFilters'); + + this.folders = Folders; + this.Priorities = Priorities; + + this.priority = priority; + this.fFolder = fFolder; + this.fSeen = fSeen; + this.fFlag = fFlag; + + this.usefFolder = ko.observable(false); + this.fFolder.subscribe((v) => this.usefFolder(!!v)); + } + + submitForm() { + if (!this.usefFolder()) fFolder(''); + ServerActions.AddOrEditFilter(); + this.close(); + } + + beforeShow() { + if (!oldSearchQ()) { + ServerActions.GetFilter(); + } else { + this.usefFolder(!!fFolder()); + } + } + } + + class AdvancedSearchPopupView extends rl.pluginPopupView { + constructor() { + super('STabAdvancedSearch'); + + this.addObservables({ + from: '', + to: '', + subject: '', + text: '', + repliedValue: -1, + selectedDateValue: -1, + selectedTreeValue: '', + + hasAttachment: false, + starred: false, + unseen: false + }); + + this.addComputables({ + repliedOptions: () => { + return [ + { id: -1, name: '' }, + { id: 1, name: rl.i18n('GLOBAL/YES') }, + { id: 0, name: rl.i18n('GLOBAL/NO') } + ]; + }, + + selectedDates: () => { + let prefix = 'SEARCH/DATE_'; + return [ + { id: -1, name: rl.i18n(prefix + 'ALL') }, + { id: 3, name: rl.i18n(prefix + '3_DAYS') }, + { id: 7, name: rl.i18n(prefix + '7_DAYS') }, + { id: 30, name: rl.i18n(prefix + 'MONTH') }, + { id: 90, name: rl.i18n(prefix + '3_MONTHS') }, + { id: 180, name: rl.i18n(prefix + '6_MONTHS') }, + { id: 365, name: rl.i18n(prefix + 'YEAR') } + ]; + }, + + selectedTree: () => { + let prefix = 'SEARCH/SUBFOLDERS_'; + return [ + { id: '', name: rl.i18n(prefix + 'NONE') }, + { id: 'subtree-one', name: rl.i18n(prefix + 'SUBTREE_ONE') }, + { id: 'subtree', name: rl.i18n(prefix + 'SUBTREE') } + ]; + } + }); + } + + submitForm() { + const newSearchQ = this.buildSearchString(); + if (newSearchQ != oldSearchQ()) ServerActions.UpdateSearchQ(newSearchQ); + this.close(); + } + + editFilters() { + SearchFiltersPopupView.showModal(); + } + + buildSearchString() { + const self = this, + data = new FormData(), + append = (key, value) => value.length && data.append(key, value); + + append('from', self.from().trim()); + append('to', self.to().trim()); + append('subject', self.subject().trim()); + append('text', self.text().trim()); + append('in', self.selectedTreeValue()); + if (-1 < self.selectedDateValue()) { + let d = new Date(); + d.setDate(d.getDate() - self.selectedDateValue()); + append('since', d.toISOString().split('T')[0]); + } + + let result = decodeURIComponent(new URLSearchParams(data).toString()); + + if (self.hasAttachment()) { + result += '&attachment'; + } + if (self.unseen()) { + result += '&unseen'; + } + if (self.starred()) { + result += '&flagged'; + } + if (1 == self.repliedValue()) { + result += '&answered'; + } + if (0 == self.repliedValue()) { + result += '&unanswered'; + } + + return result.replace(/^&+/, ''); + } + + onShow() { + const pString = (value) => (null != value ? '' + value : ''); + + const self = this, + params = new URLSearchParams('?' + searchQ()); + self.from(pString(params.get('from'))); + self.to(pString(params.get('to'))); + self.subject(pString(params.get('subject'))); + self.text(pString(params.get('text'))); + self.selectedTreeValue(pString(params.get('in'))); + self.selectedDateValue(-1); + self.hasAttachment(params.has('attachment')); + self.starred(params.has('flagged')); + self.unseen(params.has('unseen')); + if (params.has('answered')) { + self.repliedValue(1); + } else if (params.has('unanswered')) { + self.repliedValue(0); + } + } + + clear() { + oldSearchQ(''); + searchQ(''); + fFolder(''); + priority(1); + fSeen(false); + fFlag(false); + } + + onHide() { + this.clear(); + } + } +})(window.rl); diff --git a/plugins/search-filters/langs/en.json b/plugins/search-filters/langs/en.json new file mode 100644 index 000000000..6fb25bbf3 --- /dev/null +++ b/plugins/search-filters/langs/en.json @@ -0,0 +1,21 @@ +{ + "SFILTERS": { + "WHEN_MESSAGE_MATCH_SEARCH_CRITERIA": "When a message is an exact match for your search criteria:", + "FILTERS": "Filters", + "FILTER": "Filter", + "MOVE_TO_FOLDER": "Move to folder: ", + "CREATE_FILTER": "Create filter", + "EDIT_FILTERS": "Edit filters", + "MATCHES": "Matches: ", + "DO_THIS": "Do this: ", + "SAVE": "Save", + "EDIT": "Edit", + "REMOVE": "Remove", + "NO_FILTERS": "You haven't added any filters yet.", + "THE_FOLLOWING_FILTERS_ARE_APPLIED": "The following filters are applied to all incoming mail: ", + "MARK_AS_READ": "Mark as read", + "STAR_IT": "Star it", + "PRIORITY_LABLE": "Priority points, Higher means it'll apply first", + "PRIORITY_POINTS": "Priority points" + } +} \ No newline at end of file diff --git a/plugins/search-filters/templates/PopupsSTabAdvancedSearch.html b/plugins/search-filters/templates/PopupsSTabAdvancedSearch.html new file mode 100644 index 000000000..d1a260551 --- /dev/null +++ b/plugins/search-filters/templates/PopupsSTabAdvancedSearch.html @@ -0,0 +1,82 @@ +
+ × +

+
+ + \ No newline at end of file diff --git a/plugins/search-filters/templates/PopupsSearchFilters.html b/plugins/search-filters/templates/PopupsSearchFilters.html new file mode 100644 index 000000000..1203272ab --- /dev/null +++ b/plugins/search-filters/templates/PopupsSearchFilters.html @@ -0,0 +1,54 @@ +
+ × +

+
+ + \ No newline at end of file diff --git a/plugins/search-filters/templates/STabSearchFilters.html b/plugins/search-filters/templates/STabSearchFilters.html new file mode 100644 index 000000000..d006da345 --- /dev/null +++ b/plugins/search-filters/templates/STabSearchFilters.html @@ -0,0 +1,39 @@ +
+
+
+ +     + +
+ +
+ +
+ + + +
+ + +
+
+ + +
+
\ No newline at end of file