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 @@
+