From dd9f277ccf1b10d6a23169012bb5b18dce9ae45e Mon Sep 17 00:00:00 2001 From: djmaze Date: Wed, 20 Jan 2021 14:52:20 +0100 Subject: [PATCH] Split collectionToFileString() from SieveStorage to Sieve Converted SieveStorage fileStringToCollection() to JavaScript sieveScriptToFilters() Drop the old filtersSave() --- dev/Model/SieveScript.js | 102 +++-- dev/Remote/User/Fetch.js | 14 - dev/Settings/User/Filters.js | 57 +-- .../libraries/RainLoop/Actions/Filters.php | 41 +- .../Providers/Filters/Classes/Filter.php | 28 +- .../RainLoop/Providers/Filters/Sieve.php | 393 ++++++++++++++++ .../Providers/Filters/SieveStorage.php | 428 +----------------- 7 files changed, 509 insertions(+), 554 deletions(-) create mode 100644 snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Filters/Sieve.php diff --git a/dev/Model/SieveScript.js b/dev/Model/SieveScript.js index a9e1455cd..87740db91 100644 --- a/dev/Model/SieveScript.js +++ b/dev/Model/SieveScript.js @@ -4,7 +4,7 @@ import { AbstractModel } from 'Knoin/AbstractModel'; import { FilterModel } from 'Model/Filter'; // collectionToFileString -function filtersToSieveScript(aFilters) +function filtersToSieveScript(filters) { let eol = '\r\n', split = /.{0,74}/g, @@ -16,8 +16,8 @@ function filtersToSieveScript(aFilters) '' ]; - const quote = sValue => '"' + sValue.trim().replace(/(\\|")/, '\\\\$1') + '"'; - const StripSpaces = sValue => sValue.replace(/\s+/, ' ').trim(); + const quote = string => '"' + string.trim().replace(/(\\|")/, '\\\\$1') + '"'; + const StripSpaces = string => string.replace(/\s+/, ' ').trim(); // conditionToSieveScript const conditionToString = (condition, require) => @@ -83,16 +83,11 @@ function filtersToSieveScript(aFilters) return '/* @Error: unknown field value ' + field + ' */ false'; } - if (('From' === field || 'Recipient' === field) && value.includes(',')) - { + if (('From' === field || 'Recipient' === field) && value.includes(',')) { result += ' [' + value.split(',').map(value => quote(value)).join(', ').trim() + ']'; - } - else if ('Size' === field) - { + } else if ('Size' === field) { result += ' ' + value; - } - else - { + } else { result += ' ' + quote(value); } @@ -107,12 +102,12 @@ function filtersToSieveScript(aFilters) { let sTab = ' ', block = true, - result = []; + result = [], + conditions = filter.conditions(); const errorAction = type => result.push(sTab + '# @Error (' + type + '): empty action value'); // Conditions - let conditions = filter.conditions(); if (1 < conditions.length) { result.push('Any' === filter.conditionsType() ? 'if anyof(' @@ -127,28 +122,24 @@ function filtersToSieveScript(aFilters) } // actions - if (block) { - result.push('{'); - } else { - sTab = ''; - } + block ? result.push('{') : (sTab = ''); if (filter.actionMarkAsRead() && ['None','MoveTo','Forward'].includes(filter.actionType())) { require.imap4flags = 1; result.push(sTab + 'addflag "\\\\Seen";'); } + let value = filter.actionValue().trim(); + value = value.length ? quote(value) : 0; switch (filter.actionType()) { - // case FiltersAction.None: case 'None': break; case 'Discard': result.push(sTab + 'discard;'); break; - case 'Vacation': { - let value = filter.actionValue().trim(); - if (value.length) { + case 'Vacation': + if (value) { require.vacation = 1; let days = 1, @@ -175,41 +166,38 @@ function filtersToSieveScript(aFilters) } } - result.push(sTab + 'vacation :days ' + days + ' ' + addresses + subject + quote(value) + ';'); + result.push(sTab + 'vacation :days ' + days + ' ' + addresses + subject + value + ';'); } else { errorAction('vacation'); } - break; } + break; case 'Reject': { - let value = filter.actionValue().trim(); - if (value.length) { + if (value) { require.reject = 1; - result.push(sTab + 'reject ' + quote(value) + ';'); + result.push(sTab + 'reject ' + value + ';'); } else { errorAction('reject'); } break; } - case 'Forward': { - let value = filter.actionValue(); - if (value.length) { + case 'Forward': + if (value) { if (filter.actionKeep()) { require.fileinto = 1; result.push(sTab + 'fileinto "INBOX";'); } - result.push(sTab + 'redirect ' + quote(value) + ';'); + result.push(sTab + 'redirect ' + value + ';'); } else { errorAction('redirect'); } - break; } - case 'MoveTo': { - let value = filter.actionValue(); - if (value.length) { + break; + case 'MoveTo': + if (value) { require.fileinto = 1; - result.push(sTab + 'fileinto ' + quote(value) + ';'); + result.push(sTab + 'fileinto ' + value + ';'); } else { errorAction('fileinto'); } - break; } + break; } filter.actionNoStop() || result.push(sTab + 'stop;'); @@ -219,7 +207,7 @@ function filtersToSieveScript(aFilters) return result.join(eol); }; - aFilters.forEach(filter => { + filters.forEach(filter => { parts.push([ '/*', 'BEGIN:FILTER:' + filter.id, @@ -238,6 +226,27 @@ function filtersToSieveScript(aFilters) return (require.length ? 'require ' + JSON.stringify(require) + ';' + eol : '') + eol + parts.join(eol); } +// fileStringToCollection +function sieveScriptToFilters(script) +{ + let regex = /BEGIN:HEADER([\s\S]+?)END:HEADER/gm, + filters = [], + json, + filter; + if (script.length && script.includes('RAINLOOP:SIEVE')) { + while ((json = regex.exec(script))) { + json = decodeURIComponent(escape(atob(json[1].replace(/\s+/g, '')))); + if (json && json.length && (json = JSON.parse(json))) { + json['@Object'] = 'Object/Filter'; + json.Conditions.forEach(condition => condition['@Object'] = 'Object/FilterCondition'); + filter = FilterModel.reviveFromJson(json); + filter && filters.push(filter); + } + } + } + return filters; +} + class SieveScriptModel extends AbstractModel { constructor() { @@ -266,13 +275,14 @@ class SieveScriptModel extends AbstractModel }); } - setFilters() { - /*let tree = */window.Sieve.parseScript(this.body); -// this.filters = ko.observableArray(tree); - } - filtersToRaw() { return filtersToSieveScript(this.filters); +// this.body(filtersToSieveScript(this.filters)); + } + + rawToFilters() { + return sieveScriptToFilters(this.body()); +// this.filters(sieveScriptToFilters(this.body())); } verify() { @@ -305,9 +315,11 @@ class SieveScriptModel extends AbstractModel static reviveFromJson(json) { const script = super.reviveFromJson(json); if (script) { - if (script.allowFilters() && Array.isNotEmpty(json.filters)) { + if (script.allowFilters()) { script.filters( - json.filters.map(aData => FilterModel.reviveFromJson(aData)).filter(v => v) + Array.isNotEmpty(json.filters) + ? json.filters.map(aData => FilterModel.reviveFromJson(aData)).filter(v => v) + : sieveScriptToFilters(script.body()) ); } else { script.filters([]); diff --git a/dev/Remote/User/Fetch.js b/dev/Remote/User/Fetch.js index a5cd64d6d..5f05d1d71 100644 --- a/dev/Remote/User/Fetch.js +++ b/dev/Remote/User/Fetch.js @@ -222,20 +222,6 @@ class RemoteUserFetch extends AbstractFetchRemote { this.defaultRequest(fCallback, 'AccountsAndIdentities'); } - /** - * @param {?Function} fCallback - * @param {Array} filters - * @param {string} raw - * @param {boolean} isRawIsActive - */ - filtersSave(fCallback, filters, raw, isRawIsActive) { - this.defaultRequest(fCallback, 'FiltersSave', { - Raw: raw, - RawIsActive: isRawIsActive ? 1 : 0, - Filters: filters.map(item => item.toJson()) - }); - } - /** * @param {?Function} fCallback * @param {SieveScriptModel} script diff --git a/dev/Settings/User/Filters.js b/dev/Settings/User/Filters.js index c87425fea..69017f0a8 100644 --- a/dev/Settings/User/Filters.js +++ b/dev/Settings/User/Filters.js @@ -13,8 +13,6 @@ import { showScreenPopup } from 'Knoin/Knoin'; class FiltersUserSettings { constructor() { - this.sieve = SieveStore; - this.scripts = SieveStore.scripts; this.loading = ko.observable(false).extend({ throttle: 200 }); @@ -23,27 +21,24 @@ class FiltersUserSettings { serverErrorDesc: '' }); - this.serverError.subscribe((value) => { - if (!value) { - this.serverErrorDesc(''); - } - }, this); - this.scriptForDeletion = ko.observable(null).deleteAccessHelper(); } + setError(text) { + this.serverError(true); + this.serverErrorDesc(text); + } + updateList() { if (!this.loading()) { this.loading(true); + this.serverError(false); Remote.filtersGet((result, data) => { this.loading(false); - this.serverError(false); this.scripts([]); if (StorageResultType.Success === result && data && data.Result) { - this.serverError(false); - SieveStore.capa(data.Result.Capa); /* this.scripts( @@ -55,10 +50,8 @@ class FiltersUserSettings { value && this.scripts.push(value) }); } else { - this.scripts([]); SieveStore.capa([]); - this.serverError(true); - this.serverErrorDesc( + this.setError( data && data.ErrorCode ? getNotification(data.ErrorCode) : getNotification(Notification.CantGetFilters) ); } @@ -75,34 +68,32 @@ class FiltersUserSettings { } deleteScript(script) { - if (!script.active()) { - Remote.filtersScriptDelete( - (result, data) => { - if (StorageResultType.Success === result && data && data.Result) { - this.scripts.remove(script); - delegateRunOnDestroy(script); - } else { - this.serverError(true); - this.serverErrorDesc((data && data.ErrorCode) - ? (data.ErrorMessageAdditional || getNotification(data.ErrorCode)) - : getNotification(Notification.CantActivateFiltersScript) - ); - } - }, - script.name() - ); - } + this.serverError(false); + Remote.filtersScriptDelete( + (result, data) => { + if (StorageResultType.Success === result && data && data.Result) { + this.scripts.remove(script); + delegateRunOnDestroy(script); + } else { + this.setError((data && data.ErrorCode) + ? (data.ErrorMessageAdditional || getNotification(data.ErrorCode)) + : getNotification(Notification.CantActivateFiltersScript) + ); + } + }, + script.name() + ); } toggleScript(script) { let name = script.active() ? '' : script.name(); + this.serverError(false); Remote.filtersScriptActivate( (result, data) => { if (StorageResultType.Success === result && data && data.Result) { this.scripts().forEach(script => script.active(script.name() === name)); } else { - this.serverError(true); - this.serverErrorDesc((data && data.ErrorCode) + this.setError((data && data.ErrorCode) ? (data.ErrorMessageAdditional || getNotification(data.ErrorCode)) : getNotification(Notification.CantActivateFiltersScript) ); diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Filters.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Filters.php index f8d21ebf0..373ff3de8 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Filters.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Filters.php @@ -41,41 +41,6 @@ trait Filters /** * @throws \MailSo\Base\Exceptions\Exception */ - public function DoFiltersSave() : array - { - $oAccount = $this->getAccountFromToken(); - - if (!$this->GetCapa(false, false, Capa::FILTERS, $oAccount)) - { - return $this->FalseResponse(__FUNCTION__); - } - - $aIncFilters = $this->GetActionParam('Filters', array()); - - $sRaw = $this->GetActionParam('Raw', ''); - $bRawIsActive = '1' === (string) $this->GetActionParam('RawIsActive', '0'); - - $aFilters = array(); - foreach ($aIncFilters as $aFilter) - { - if (\is_array($aFilter)) - { - $oFilter = new \RainLoop\Providers\Filters\Classes\Filter(); - if ($oFilter->FromJSON($aFilter)) - { - $aFilters[] = $oFilter; - } - } - } - - $this->Plugins() - ->RunHook('filter.filters-save', array($oAccount, &$aFilters, &$sRaw, &$bRawIsActive)) - ; - - return $this->DefaultResponse(__FUNCTION__, $this->FiltersProvider()->Save($oAccount, - $aFilters, $sRaw, $bRawIsActive)); - } - public function DoFiltersScriptSave() : array { $oAccount = $this->getAccountFromToken(); @@ -108,6 +73,9 @@ trait Filters )); } + /** + * @throws \MailSo\Base\Exceptions\Exception + */ public function DoFiltersScriptActivate() : array { $oAccount = $this->getAccountFromToken(); @@ -121,6 +89,9 @@ trait Filters )); } + /** + * @throws \MailSo\Base\Exceptions\Exception + */ public function DoFiltersScriptDelete() : array { $oAccount = $this->getAccountFromToken(); diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Filters/Classes/Filter.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Filters/Classes/Filter.php index e2004a6dc..41ba1e561 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Filters/Classes/Filter.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Filters/Classes/Filter.php @@ -179,25 +179,25 @@ class Filter implements \JsonSerializable public function FromJSON(array $aFilter) : bool { - $this->sID = isset($aFilter['ID']) ? $aFilter['ID'] : ''; - $this->sName = isset($aFilter['Name']) ? $aFilter['Name'] : ''; + $this->sID = $aFilter['ID'] ?? ''; + $this->sName = $aFilter['Name'] ?? ''; - $this->bEnabled = isset($aFilter['Enabled']) ? '1' === (string) $aFilter['Enabled'] : true; + $this->bEnabled = !isset($aFilter['Enabled']) || !empty($aFilter['Enabled']); - $this->sConditionsType = isset($aFilter['ConditionsType']) ? $aFilter['ConditionsType'] : - \RainLoop\Providers\Filters\Enumerations\ConditionsType::ANY; + $this->sConditionsType = $aFilter['ConditionsType'] + ?? \RainLoop\Providers\Filters\Enumerations\ConditionsType::ANY; - $this->sActionType = isset($aFilter['ActionType']) ? $aFilter['ActionType'] : - \RainLoop\Providers\Filters\Enumerations\ActionType::MOVE_TO; + $this->sActionType = $aFilter['ActionType'] + ?? \RainLoop\Providers\Filters\Enumerations\ActionType::MOVE_TO; - $this->sActionValue = isset($aFilter['ActionValue']) ? $aFilter['ActionValue'] : ''; - $this->sActionValueSecond = isset($aFilter['ActionValueSecond']) ? $aFilter['ActionValueSecond'] : ''; - $this->sActionValueThird = isset($aFilter['ActionValueThird']) ? $aFilter['ActionValueThird'] : ''; - $this->sActionValueFourth = isset($aFilter['ActionValueFourth']) ? $aFilter['ActionValueFourth'] : ''; + $this->sActionValue = $aFilter['ActionValue'] ?? ''; + $this->sActionValueSecond = $aFilter['ActionValueSecond'] ?? ''; + $this->sActionValueThird = $aFilter['ActionValueThird'] ?? ''; + $this->sActionValueFourth = $aFilter['ActionValueFourth'] ?? ''; - $this->bKeep = isset($aFilter['Keep']) ? '1' === (string) $aFilter['Keep'] : true; - $this->bStop = isset($aFilter['Stop']) ? '1' === (string) $aFilter['Stop'] : true; - $this->bMarkAsRead = isset($aFilter['MarkAsRead']) ? '1' === (string) $aFilter['MarkAsRead'] : false; + $this->bKeep = !isset($aFilter['Keep']) || !empty($aFilter['Keep']); + $this->bStop = !isset($aFilter['Stop']) || !empty($aFilter['Stop']); + $this->bMarkAsRead = !isset($aFilter['MarkAsRead']) || !empty($aFilter['MarkAsRead']); $this->aConditions = empty($aFilter['Conditions']) ? array() : FilterCondition::CollectionFromJSON($aFilter['Conditions']); diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Filters/Sieve.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Filters/Sieve.php new file mode 100644 index 000000000..29c70c3ca --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Filters/Sieve.php @@ -0,0 +1,393 @@ +ID(), + 'BEGIN:HEADER', + \chunk_split(\base64_encode(\json_encode($oItem)), 74, $sNL).'END:HEADER', + '*/', + $oItem->Enabled() ? '' : '/* @Filter is disabled ', + static::filterToSieveScript($oItem, $aCapa), + $oItem->Enabled() ? '' : '*/', + '/* END:FILTER */', + '' + ]); + } + + $aCapa = \array_keys($aCapa); + $sCapa = \count($aCapa) + ? $sNL . 'require ' . \str_replace('","', '", "', \json_encode($aCapa)).';' . $sNL + : ''; + + return $sCapa . $sNL . \implode($sNL, $aParts); + } + + public static function fileStringToCollection(string $sFileString) : array + { + $aResult = array(); + if (!empty($sFileString) && false !== \strpos($sFileString, 'RAINLOOP:SIEVE')) + { + $aMatch = array(); + if (\preg_match_all('/BEGIN:FILTER(.+?)BEGIN:HEADER(.+?)END:HEADER/s', $sFileString, $aMatch) && + isset($aMatch[2]) && \is_array($aMatch[2])) + { + foreach ($aMatch[2] as $sEncodedLine) + { + if (!empty($sEncodedLine)) + { + $sDecodedLine = \base64_decode(\preg_replace('/\\s+/s', '', $sEncodedLine)); + if (!empty($sDecodedLine)) + { + $oItem = new Classes\Filter(); + if ($oItem && $oItem->unserializeFromJson($sDecodedLine)) + { + $aResult[] = $oItem; + } + } + } + } + } + } + + return $aResult; + } + + private static function conditionToSieveScript(Classes\FilterCondition $oCondition, array &$aCapa) : string + { + $sResult = ''; + $sTypeWord = ''; + $bTrue = true; + + $sValue = \trim($oCondition->Value()); + $sValueSecond = \trim($oCondition->ValueSecond()); + + if (0 < \strlen($sValue) || + (0 < \strlen($sValue) && 0 < \strlen($sValueSecond) && + Enumerations\ConditionField::HEADER === $oCondition->Field())) + { + switch ($oCondition->Type()) + { + case Enumerations\ConditionType::TEXT: + case Enumerations\ConditionType::RAW: + case Enumerations\ConditionType::OVER: + case Enumerations\ConditionType::UNDER: + $sTypeWord = ':' . \strtolower($oCondition->Type()); + break; + case Enumerations\ConditionType::NOT_EQUAL_TO: + $sResult .= 'not '; + case Enumerations\ConditionType::EQUAL_TO: + $sTypeWord = ':is'; + break; + case Enumerations\ConditionType::NOT_CONTAINS: + $sResult .= 'not '; + case Enumerations\ConditionType::CONTAINS: + $sTypeWord = ':contains'; + break; + case Enumerations\ConditionType::REGEX: + $sTypeWord = ':regex'; + $aCapa['regex'] = true; + break; + default: + $bTrue = false; + $sResult = '/* @Error: unknown type value */ false'; + break; + } + + switch ($oCondition->Field()) + { + case Enumerations\ConditionField::FROM: + $sResult .= 'header '.$sTypeWord.' ["From"]'; + break; + case Enumerations\ConditionField::RECIPIENT: + $sResult .= 'header '.$sTypeWord.' ["To", "CC"]'; + break; + case Enumerations\ConditionField::SUBJECT: + $sResult .= 'header '.$sTypeWord.' ["Subject"]'; + break; + case Enumerations\ConditionField::HEADER: + $sResult .= 'header '.$sTypeWord.' ["'.static::quote($sValueSecond).'"]'; + break; + case Enumerations\ConditionField::BODY: + // :text :raw :content + $sResult .= 'body '.$sTypeWord.' :contains'; + $aCapa['body'] = true; + break; + case Enumerations\ConditionField::SIZE: + $sResult .= 'size '.$sTypeWord; + break; + default: + $bTrue = false; + $sResult = '/* @Error: unknown field value */ false'; + break; + } + + if ($bTrue) + { + if (\in_array($oCondition->Field(), array( + Enumerations\ConditionField::FROM, + Enumerations\ConditionField::RECIPIENT + )) && false !== \strpos($sValue, ',')) + { + $aValue = \array_map(function ($sValue) use ($self) { + return '"'.static::quote(\trim($sValue)).'"'; + }, \explode(',', $sValue)); + + $sResult .= ' ['.\trim(\implode(', ', $aValue)).']'; + } + else if (Enumerations\ConditionField::SIZE === $oCondition->Field()) + { + $sResult .= ' '.static::quote($sValue); + } + else + { + $sResult .= ' "'.static::quote($sValue).'"'; + } + + $sResult = \MailSo\Base\Utils::StripSpaces($sResult); + } + } + else + { + $sResult = '/* @Error: empty condition value */ false'; + } + + return $sResult; + } + + private static function filterToSieveScript(Classes\Filter $oFilter, array &$aCapa) : string + { + $sNL = static::NEW_LINE; + $sTab = ' '; + + $bBlock = true; + $aResult = array(); + + // Conditions + $aConditions = $oFilter->Conditions(); + if (1 < \count($aConditions)) + { + if (Enumerations\ConditionsType::ANY === + $oFilter->ConditionsType()) + { + $aResult[] = 'if anyof('; + + $bTrim = false; + foreach ($aConditions as $oCond) + { + $bTrim = true; + $sCons = static::conditionToSieveScript($oCond, $aCapa); + if (!empty($sCons)) + { + $aResult[] = $sTab.$sCons.','; + } + } + if ($bTrim) + { + $aResult[\count($aResult) - 1] = \rtrim($aResult[\count($aResult) - 1], ','); + } + + $aResult[] = ')'; + } + else + { + $aResult[] = 'if allof('; + foreach ($aConditions as $oCond) + { + $aResult[] = $sTab . static::conditionToSieveScript($oCond, $aCapa) . ','; + } + + $aResult[\count($aResult) - 1] = \rtrim($aResult[\count($aResult) - 1], ','); + $aResult[] = ')'; + } + } + else if (1 === \count($aConditions)) + { + $aResult[] = 'if ' . static::conditionToSieveScript($aConditions[0], $aCapa); + } + else + { + $bBlock = false; + } + + // actions + if ($bBlock) + { + $aResult[] = '{'; + } + else + { + $sTab = ''; + } + + if ($oFilter->MarkAsRead() && \in_array($oFilter->ActionType(), array( + Enumerations\ActionType::NONE, + Enumerations\ActionType::MOVE_TO, + Enumerations\ActionType::FORWARD + ))) + { + $aCapa['imap4flags'] = true; + $aResult[] = $sTab.'addflag "\\\\Seen";'; + } + + switch ($oFilter->ActionType()) + { + case Enumerations\ActionType::NONE: + if ($oFilter->Stop()) + { + $aResult[] = $sTab.'stop;'; + } + break; + case Enumerations\ActionType::DISCARD: + $aResult[] = $sTab.'discard;'; + if ($oFilter->Stop()) + { + $aResult[] = $sTab.'stop;'; + } + break; + case Enumerations\ActionType::VACATION: + $sValue = \trim($oFilter->ActionValue()); + $sValueSecond = \trim($oFilter->ActionValueSecond()); + $sValueThird = \trim($oFilter->ActionValueThird()); + $sValueFourth = \trim($oFilter->ActionValueFourth()); + if (0 < \strlen($sValue)) + { + $aCapa['vacation'] = true; + + $iDays = 1; + $sSubject = ''; + if (0 < \strlen($sValueSecond)) + { + $sSubject = ':subject "'. + static::quote(\MailSo\Base\Utils::StripSpaces($sValueSecond)).'" '; + } + + if (0 < \strlen($sValueThird) && \is_numeric($sValueThird) && 1 < (int) $sValueThird) + { + $iDays = (int) $sValueThird; + } + + $sAddresses = ''; + if (0 < \strlen($sValueFourth)) + { + $aAddresses = \explode(',', $sValueFourth); + $aAddresses = \array_filter(\array_map(function ($sEmail) use ($self) { + $sEmail = \trim($sEmail); + return 0 < \strlen($sEmail) ? '"'.static::quote($sEmail).'"' : ''; + }, $aAddresses), 'strlen'); + + if (0 < \count($aAddresses)) + { + $sAddresses = ':addresses ['.\implode(', ', $aAddresses).'] '; + } + } + + $aResult[] = $sTab.'vacation :days '.$iDays.' '.$sAddresses.$sSubject.'"'.static::quote($sValue).'";'; + if ($oFilter->Stop()) + { + $aResult[] = $sTab.'stop;'; + } + } + else + { + $aResult[] = $sTab.'# @Error (vacation): empty action value'; + } + break; + case Enumerations\ActionType::REJECT: + $sValue = \trim($oFilter->ActionValue()); + if (0 < \strlen($sValue)) + { + $aCapa['reject'] = true; + + $aResult[] = $sTab.'reject "'.static::quote($sValue).'";'; + if ($oFilter->Stop()) + { + $aResult[] = $sTab.'stop;'; + } + } + else + { + $aResult[] = $sTab.'# @Error (reject): empty action value'; + } + break; + case Enumerations\ActionType::FORWARD: + $sValue = $oFilter->ActionValue(); + if (0 < \strlen($sValue)) + { + if ($oFilter->Keep()) + { + $aCapa['fileinto'] = true; + $aResult[] = $sTab.'fileinto "INBOX";'; + } + + $aResult[] = $sTab.'redirect "'.static::quote($sValue).'";'; + if ($oFilter->Stop()) + { + $aResult[] = $sTab.'stop;'; + } + } + else + { + $aResult[] = $sTab.'# @Error (redirect): empty action value'; + } + break; + case Enumerations\ActionType::MOVE_TO: + $sValue = $oFilter->ActionValue(); + if (0 < \strlen($sValue)) + { + $sFolderName = $sValue; // utf7-imap + if (static::$bUtf8FolderName) // to utf-8 + { + $sFolderName = \MailSo\Base\Utils::ConvertEncoding($sFolderName, + \MailSo\Base\Enumerations\Charset::UTF_7_IMAP, + \MailSo\Base\Enumerations\Charset::UTF_8); + } + + $aCapa['fileinto'] = true; + $aResult[] = $sTab.'fileinto "'.static::quote($sFolderName).'";'; + if ($oFilter->Stop()) + { + $aResult[] = $sTab.'stop;'; + } + } + else + { + $aResult[] = $sTab.'# @Error (fileinto): empty action value'; + } + break; + } + + if ($bBlock) + { + $aResult[] = '}'; + } + + return \implode($sNL, $aResult); + } + + private static function quote(string $sValue) : string + { + return \str_replace(array('\\', '"'), array('\\\\', '\\"'), \trim($sValue)); + } +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Filters/SieveStorage.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Filters/SieveStorage.php index 6333e7b6c..ba01efb5d 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Filters/SieveStorage.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Filters/SieveStorage.php @@ -4,8 +4,6 @@ namespace RainLoop\Providers\Filters; class SieveStorage implements FiltersInterface { - const NEW_LINE = "\r\n"; - const SIEVE_FILE_NAME = 'rainloop.user'; const SIEVE_FILE_NAME_RAW = 'rainloop.user.raw'; @@ -24,19 +22,12 @@ class SieveStorage implements FiltersInterface */ private $oConfig; - /** - * @var bool - */ - private $bUtf8FolderName; - public function __construct($oPlugins, $oConfig) { $this->oLogger = null; $this->oPlugins = $oPlugins; $this->oConfig = $oConfig; - - $this->bUtf8FolderName = !!$this->oConfig->Get('labs', 'sieve_utf8_folder_name', true); } protected function getConnection(\RainLoop\Model\Account $oAccount) : ?\MailSo\Sieve\ManageSieveClient @@ -62,26 +53,12 @@ class SieveStorage implements FiltersInterface $aList = $oSieveClient->ListScripts(); foreach ($aList as $name => $active) { - if ($name != self::SIEVE_FILE_NAME) { - if ($bAllowRaw) { - $aScripts[$name] = array( - '@Object' => 'Object/SieveScript', - 'name' => $name, - 'active' => $active, - 'body' => $oSieveClient->GetScript($name) // \trim() ? - ); - } - } else { - $sS = $oSieveClient->GetScript(self::SIEVE_FILE_NAME); - if ($sS) { - $aFilters = $this->fileStringToCollection($sS); - } + if ($bAllowRaw || $name == self::SIEVE_FILE_NAME) { $aScripts[$name] = array( '@Object' => 'Object/SieveScript', 'name' => $name, 'active' => $active, - 'body' => $oSieveClient->GetScript($name), // \trim() ? - 'filters' => $aFilters + 'body' => $oSieveClient->GetScript($name) ); } } @@ -119,7 +96,8 @@ class SieveStorage implements FiltersInterface public function Save(\RainLoop\Model\Account $oAccount, string $sScriptName, array $aFilters, string $sRaw = '') : bool { if (self::SIEVE_FILE_NAME === $sScriptName) { - $sRaw = $this->collectionToFileString($aFilters); + Sieve::$bUtf8FolderName = !!$this->oConfig->Get('labs', 'sieve_utf8_folder_name', true); + $sRaw = Sieve::collectionToFileString($aFilters); } $oSieveClient = $this->getConnection($oAccount); if ($oSieveClient) { @@ -149,7 +127,17 @@ class SieveStorage implements FiltersInterface } return false; } - +/* + public function Check(\RainLoop\Model\Account $oAccount, string $sScript) : bool + { + $oSieveClient = $this->getConnection($oAccount); + if ($oSieveClient) { + $oSieveClient->CheckScript($sScript); + return true; + } + return false; + } +*/ public function Delete(\RainLoop\Model\Account $oAccount, string $sScriptName) : bool { $oSieveClient = $this->getConnection($oAccount); @@ -160,392 +148,6 @@ class SieveStorage implements FiltersInterface return false; } - private function conditionToSieveScript(Classes\FilterCondition $oCondition, array &$aCapa) : string - { - $sResult = ''; - $sTypeWord = ''; - $bTrue = true; - - $sValue = \trim($oCondition->Value()); - $sValueSecond = \trim($oCondition->ValueSecond()); - - if (0 < \strlen($sValue) || - (0 < \strlen($sValue) && 0 < \strlen($sValueSecond) && - Enumerations\ConditionField::HEADER === $oCondition->Field())) - { - switch ($oCondition->Type()) - { - case Enumerations\ConditionType::TEXT: - case Enumerations\ConditionType::RAW: - case Enumerations\ConditionType::OVER: - case Enumerations\ConditionType::UNDER: - $sTypeWord = ':' . \strtolower($oCondition->Type()); - break; - case Enumerations\ConditionType::NOT_EQUAL_TO: - $sResult .= 'not '; - case Enumerations\ConditionType::EQUAL_TO: - $sTypeWord = ':is'; - break; - case Enumerations\ConditionType::NOT_CONTAINS: - $sResult .= 'not '; - case Enumerations\ConditionType::CONTAINS: - $sTypeWord = ':contains'; - break; - case Enumerations\ConditionType::REGEX: - $sTypeWord = ':regex'; - $aCapa['regex'] = true; - break; - default: - $bTrue = false; - $sResult = '/* @Error: unknown type value */ false'; - break; - } - - switch ($oCondition->Field()) - { - case Enumerations\ConditionField::FROM: - $sResult .= 'header '.$sTypeWord.' ["From"]'; - break; - case Enumerations\ConditionField::RECIPIENT: - $sResult .= 'header '.$sTypeWord.' ["To", "CC"]'; - break; - case Enumerations\ConditionField::SUBJECT: - $sResult .= 'header '.$sTypeWord.' ["Subject"]'; - break; - case Enumerations\ConditionField::HEADER: - $sResult .= 'header '.$sTypeWord.' ["'.$this->quote($sValueSecond).'"]'; - break; - case Enumerations\ConditionField::BODY: - // :text :raw :content - $sResult .= 'body '.$sTypeWord.' :contains'; - $aCapa['body'] = true; - break; - case Enumerations\ConditionField::SIZE: - $sResult .= 'size '.$sTypeWord; - break; - default: - $bTrue = false; - $sResult = '/* @Error: unknown field value */ false'; - break; - } - - if ($bTrue) - { - if (\in_array($oCondition->Field(), array( - Enumerations\ConditionField::FROM, - Enumerations\ConditionField::RECIPIENT - )) && false !== \strpos($sValue, ',')) - { - $self = $this; - $aValue = \array_map(function ($sValue) use ($self) { - return '"'.$self->quote(\trim($sValue)).'"'; - }, \explode(',', $sValue)); - - $sResult .= ' ['.\trim(\implode(', ', $aValue)).']'; - } - else if (Enumerations\ConditionField::SIZE === $oCondition->Field()) - { - $sResult .= ' '.$this->quote($sValue); - } - else - { - $sResult .= ' "'.$this->quote($sValue).'"'; - } - - $sResult = \MailSo\Base\Utils::StripSpaces($sResult); - } - } - else - { - $sResult = '/* @Error: empty condition value */ false'; - } - - return $sResult; - } - - private function filterToSieveScript(Classes\Filter $oFilter, array &$aCapa) : string - { - $sNL = static::NEW_LINE; - $sTab = ' '; - - $bAll = false; - $aResult = array(); - - // Conditions - $aConditions = $oFilter->Conditions(); - if (1 < \count($aConditions)) - { - if (Enumerations\ConditionsType::ANY === - $oFilter->ConditionsType()) - { - $aResult[] = 'if anyof('; - - $bTrim = false; - foreach ($aConditions as $oCond) - { - $bTrim = true; - $sCons = $this->conditionToSieveScript($oCond, $aCapa); - if (!empty($sCons)) - { - $aResult[] = $sTab.$sCons.','; - } - } - if ($bTrim) - { - $aResult[\count($aResult) - 1] = \rtrim($aResult[\count($aResult) - 1], ','); - } - - $aResult[] = ')'; - } - else - { - $aResult[] = 'if allof('; - foreach ($aConditions as $oCond) - { - $aResult[] = $sTab.$this->conditionToSieveScript($oCond, $aCapa).','; - } - - $aResult[\count($aResult) - 1] = \rtrim($aResult[\count($aResult) - 1], ','); - $aResult[] = ')'; - } - } - else if (1 === \count($aConditions)) - { - $aResult[] = 'if '.$this->conditionToSieveScript($aConditions[0], $aCapa).''; - } - else - { - $bAll = true; - } - - // actions - if (!$bAll) - { - $aResult[] = '{'; - } - else - { - $sTab = ''; - } - - if ($oFilter->MarkAsRead() && \in_array($oFilter->ActionType(), array( - Enumerations\ActionType::NONE, - Enumerations\ActionType::MOVE_TO, - Enumerations\ActionType::FORWARD - ))) - { - $aCapa['imap4flags'] = true; - $aResult[] = $sTab.'addflag "\\\\Seen";'; - } - - switch ($oFilter->ActionType()) - { - case Enumerations\ActionType::NONE: - if ($oFilter->Stop()) - { - $aResult[] = $sTab.'stop;'; - } - break; - case Enumerations\ActionType::DISCARD: - $aResult[] = $sTab.'discard;'; - if ($oFilter->Stop()) - { - $aResult[] = $sTab.'stop;'; - } - break; - case Enumerations\ActionType::VACATION: - $sValue = \trim($oFilter->ActionValue()); - $sValueSecond = \trim($oFilter->ActionValueSecond()); - $sValueThird = \trim($oFilter->ActionValueThird()); - $sValueFourth = \trim($oFilter->ActionValueFourth()); - if (0 < \strlen($sValue)) - { - $aCapa['vacation'] = true; - - $iDays = 1; - $sSubject = ''; - if (0 < \strlen($sValueSecond)) - { - $sSubject = ':subject "'. - $this->quote(\MailSo\Base\Utils::StripSpaces($sValueSecond)).'" '; - } - - if (0 < \strlen($sValueThird) && \is_numeric($sValueThird) && 1 < (int) $sValueThird) - { - $iDays = (int) $sValueThird; - } - - $sAddresses = ''; - if (0 < \strlen($sValueFourth)) - { - $self = $this; - - $aAddresses = \explode(',', $sValueFourth); - $aAddresses = \array_filter(\array_map(function ($sEmail) use ($self) { - $sEmail = \trim($sEmail); - return 0 < \strlen($sEmail) ? '"'.$self->quote($sEmail).'"' : ''; - }, $aAddresses), 'strlen'); - - if (0 < \count($aAddresses)) - { - $sAddresses = ':addresses ['.\implode(', ', $aAddresses).'] '; - } - } - - $aResult[] = $sTab.'vacation :days '.$iDays.' '.$sAddresses.$sSubject.'"'.$this->quote($sValue).'";'; - if ($oFilter->Stop()) - { - $aResult[] = $sTab.'stop;'; - } - } - else - { - $aResult[] = $sTab.'# @Error (vacation): empty action value'; - } - break; - case Enumerations\ActionType::REJECT: - $sValue = \trim($oFilter->ActionValue()); - if (0 < \strlen($sValue)) - { - $aCapa['reject'] = true; - - $aResult[] = $sTab.'reject "'.$this->quote($sValue).'";'; - if ($oFilter->Stop()) - { - $aResult[] = $sTab.'stop;'; - } - } - else - { - $aResult[] = $sTab.'# @Error (reject): empty action value'; - } - break; - case Enumerations\ActionType::FORWARD: - $sValue = $oFilter->ActionValue(); - if (0 < \strlen($sValue)) - { - if ($oFilter->Keep()) - { - $aCapa['fileinto'] = true; - $aResult[] = $sTab.'fileinto "INBOX";'; - } - - $aResult[] = $sTab.'redirect "'.$this->quote($sValue).'";'; - if ($oFilter->Stop()) - { - $aResult[] = $sTab.'stop;'; - } - } - else - { - $aResult[] = $sTab.'# @Error (redirect): empty action value'; - } - break; - case Enumerations\ActionType::MOVE_TO: - $sValue = $oFilter->ActionValue(); - if (0 < \strlen($sValue)) - { - $sFolderName = $sValue; // utf7-imap - if ($this->bUtf8FolderName) // to utf-8 - { - $sFolderName = \MailSo\Base\Utils::ConvertEncoding($sFolderName, - \MailSo\Base\Enumerations\Charset::UTF_7_IMAP, - \MailSo\Base\Enumerations\Charset::UTF_8); - } - - $aCapa['fileinto'] = true; - $aResult[] = $sTab.'fileinto "'.$this->quote($sFolderName).'";'; - if ($oFilter->Stop()) - { - $aResult[] = $sTab.'stop;'; - } - } - else - { - $aResult[] = $sTab.'# @Error (fileinto): empty action value'; - } - break; - } - - if (!$bAll) - { - $aResult[] = '}'; - } - - return \implode($sNL, $aResult); - } - - private function collectionToFileString(array $aFilters) : string - { - $sNL = static::NEW_LINE; - - $aCapa = array(); - $aParts = array(); - - $aParts[] = '# This is SnappyMail sieve script.'; - $aParts[] = '# Please don\'t change anything here.'; - $aParts[] = '# RAINLOOP:SIEVE'; - $aParts[] = ''; - - foreach ($aFilters as /* @var $oItem \RainLoop\Providers\Filters\Classes\Filter */ $oItem) - { - $aData = array(); - $aData[] = '/*'; - $aData[] = 'BEGIN:FILTER:'.$oItem->ID(); - $aData[] = 'BEGIN:HEADER'; - $aData[] = \chunk_split(\base64_encode(\json_encode($oItem)), 74, $sNL).'END:HEADER'; - $aData[] = '*/'; - $aData[] = $oItem->Enabled() ? '' : '/* @Filter is disabled '; - $aData[] = $this->filterToSieveScript($oItem, $aCapa); - $aData[] = $oItem->Enabled() ? '' : '*/'; - $aData[] = '/* END:FILTER */'; - $aData[] = ''; - - $aParts[] = \implode($sNL, $aData); - } - - $aCapa = \array_keys($aCapa); - $sCapa = 0 < \count($aCapa) ? $sNL.'require '. - \str_replace('","', '", "', \json_encode($aCapa)).';'.$sNL : ''; - - return $sCapa.$sNL.\implode($sNL, $aParts).$sNL; - } - - private function fileStringToCollection(string $sFileString) : array - { - $aResult = array(); - if (!empty($sFileString) && false !== \strpos($sFileString, 'RAINLOOP:SIEVE')) - { - $aMatch = array(); - if (\preg_match_all('/BEGIN:FILTER(.+?)BEGIN:HEADER(.+?)END:HEADER/s', $sFileString, $aMatch) && - isset($aMatch[2]) && \is_array($aMatch[2])) - { - foreach ($aMatch[2] as $sEncodedLine) - { - if (!empty($sEncodedLine)) - { - $sDecodedLine = \base64_decode(\preg_replace('/\\s+/s', '', $sEncodedLine)); - if (!empty($sDecodedLine)) - { - $oItem = new Classes\Filter(); - if ($oItem && $oItem->unserializeFromJson($sDecodedLine)) - { - $aResult[] = $oItem; - } - } - } - } - } - } - - return $aResult; - } - - public function quote(string $sValue) : string - { - return \str_replace(array('\\', '"'), array('\\\\', '\\"'), \trim($sValue)); - } - public function SetLogger(?\MailSo\Log\Logger $oLogger) { $this->oLogger = $oLogger;