1<?php 2 3namespace RainLoop\Providers\Filters; 4 5class SieveStorage implements \RainLoop\Providers\Filters\FiltersInterface 6{ 7 const NEW_LINE = "\r\n"; 8 9 const SIEVE_FILE_NAME = 'rainloop.user'; 10 const SIEVE_FILE_NAME_RAW = 'rainloop.user.raw'; 11 12 /** 13 * @var \MailSo\Log\Logger 14 */ 15 private $oLogger; 16 17 /** 18 * @var \RainLoop\Plugins\Manager 19 */ 20 private $oPlugins; 21 22 /** 23 * @var \RainLoop\Application 24 */ 25 private $oConfig; 26 27 /** 28 * @var bool 29 */ 30 private $bUtf8FolderName; 31 32 /** 33 * @return void 34 */ 35 public function __construct($oPlugins, $oConfig) 36 { 37 $this->oLogger = null; 38 39 $this->oPlugins = $oPlugins; 40 $this->oConfig = $oConfig; 41 42 $this->bUtf8FolderName = !!$this->oConfig->Get('labs', 'sieve_utf8_folder_name', true); 43 } 44 45 /** 46 * @param \RainLoop\Model\Account $oAccount 47 * @param bool $bAllowRaw = false 48 * 49 * @return array 50 */ 51 public function Load($oAccount, $bAllowRaw = false) 52 { 53 $sRaw = ''; 54 55 $bBasicIsActive = false; 56 $bRawIsActive = false; 57 58 $aModules = array(); 59 $aFilters = array(); 60 61 $oSieveClient = \MailSo\Sieve\ManageSieveClient::NewInstance()->SetLogger($this->oLogger); 62 $oSieveClient->SetTimeOuts(10, (int) $this->oConfig->Get('labs', 'sieve_timeout', 10)); 63 64 if ($oAccount->SieveConnectAndLoginHelper($this->oPlugins, $oSieveClient, $this->oConfig)) 65 { 66 $aModules = $oSieveClient->Modules(); 67 $aList = $oSieveClient->ListScripts(); 68 69 if (\is_array($aList) && 0 < \count($aList)) 70 { 71 if (isset($aList[self::SIEVE_FILE_NAME])) 72 { 73 $bBasicIsActive = !!$aList[self::SIEVE_FILE_NAME]; 74 $sS = $oSieveClient->GetScript(self::SIEVE_FILE_NAME); 75 if ($sS) 76 { 77 $aFilters = $this->fileStringToCollection($sS); 78 } 79 } 80 81 if ($bAllowRaw && isset($aList[self::SIEVE_FILE_NAME_RAW])) 82 { 83 $bRawIsActive = !!$aList[self::SIEVE_FILE_NAME_RAW]; 84 $sRaw = \trim($oSieveClient->GetScript(self::SIEVE_FILE_NAME_RAW)); 85 } 86 } 87 88 $oSieveClient->LogoutAndDisconnect(); 89 } 90 91 return array( 92 'RawIsAllow' => $bAllowRaw, 93 'RawIsActive' => $bRawIsActive, 94 'Raw' => $bAllowRaw ? $sRaw : '', 95 'Filters' => !$bBasicIsActive && !$bRawIsActive ? array() : $aFilters, 96 'Capa' => $bAllowRaw ? $aModules : array(), 97 'Modules' => array( 98 'redirect' => \in_array('fileinto', $aModules), 99 'regex' => \in_array('regex', $aModules), 100 'relational' => \in_array('relational', $aModules), 101 'date' => \in_array('date', $aModules), 102 'moveto' => \in_array('fileinto', $aModules), 103 'reject' => \in_array('reject', $aModules), 104 'vacation' => \in_array('vacation', $aModules), 105 'markasread' => \in_array('imap4flags', $aModules) 106 ) 107 ); 108 } 109 110 /** 111 * @param \RainLoop\Model\Account $oAccount 112 * @param array $aFilters 113 * @param string $sRaw = '' 114 * @param bool $bRawIsActive = false 115 * 116 * @return bool 117 */ 118 public function Save($oAccount, $aFilters, $sRaw = '', $bRawIsActive = false) 119 { 120 $oSieveClient = \MailSo\Sieve\ManageSieveClient::NewInstance()->SetLogger($this->oLogger); 121 $oSieveClient->SetTimeOuts(10, (int) \RainLoop\Api::Config()->Get('labs', 'sieve_timeout', 10)); 122 123 if ($oAccount->SieveConnectAndLoginHelper($this->oPlugins, $oSieveClient, $this->oConfig)) 124 { 125 $aList = $oSieveClient->ListScripts(); 126 127 if ($bRawIsActive) 128 { 129 if (!empty($sRaw)) 130 { 131 $oSieveClient->PutScript(self::SIEVE_FILE_NAME_RAW, $sRaw); 132 $oSieveClient->SetActiveScript(self::SIEVE_FILE_NAME_RAW); 133 } 134 else if (isset($aList[self::SIEVE_FILE_NAME_RAW])) 135 { 136 $oSieveClient->DeleteScript(self::SIEVE_FILE_NAME_RAW); 137 } 138 } 139 else 140 { 141 $sUserFilter = $this->collectionToFileString($aFilters); 142 143 if (!empty($sUserFilter)) 144 { 145 $oSieveClient->PutScript(self::SIEVE_FILE_NAME, $sUserFilter); 146 $oSieveClient->SetActiveScript(self::SIEVE_FILE_NAME); 147 } 148 else if (isset($aList[self::SIEVE_FILE_NAME])) 149 { 150 $oSieveClient->DeleteScript(self::SIEVE_FILE_NAME); 151 } 152 } 153 154 $oSieveClient->LogoutAndDisconnect(); 155 156 return true; 157 } 158 159 return false; 160 } 161 162 /** 163 * @param \RainLoop\Providers\Filters\Classes\FilterCondition $oCondition 164 * 165 * @return string 166 */ 167 private function conditionToSieveScript($oCondition, &$aCapa) 168 { 169 $sResult = ''; 170 $sTypeWord = ''; 171 $bTrue = true; 172 173 $sValue = \trim($oCondition->Value()); 174 $sValueSecond = \trim($oCondition->ValueSecond()); 175 176 if (0 < \strlen($sValue) || 177 (0 < \strlen($sValue) && 0 < \strlen($sValueSecond) && 178 \RainLoop\Providers\Filters\Enumerations\ConditionField::HEADER === $oCondition->Field())) 179 { 180 switch ($oCondition->Type()) 181 { 182 case \RainLoop\Providers\Filters\Enumerations\ConditionType::OVER: 183 $sTypeWord = ':over'; 184 break; 185 case \RainLoop\Providers\Filters\Enumerations\ConditionType::UNDER: 186 $sTypeWord = ':under'; 187 break; 188 case \RainLoop\Providers\Filters\Enumerations\ConditionType::NOT_EQUAL_TO: 189 $sResult .= 'not '; 190 case \RainLoop\Providers\Filters\Enumerations\ConditionType::EQUAL_TO: 191 $sTypeWord = ':is'; 192 break; 193 case \RainLoop\Providers\Filters\Enumerations\ConditionType::NOT_CONTAINS: 194 $sResult .= 'not '; 195 case \RainLoop\Providers\Filters\Enumerations\ConditionType::CONTAINS: 196 $sTypeWord = ':contains'; 197 break; 198 case \RainLoop\Providers\Filters\Enumerations\ConditionType::REGEX: 199 $sTypeWord = ':regex'; 200 $aCapa['regex'] = true; 201 break; 202 default: 203 $bTrue = false; 204 $sResult = '/* @Error: unknown type value */ false'; 205 break; 206 } 207 208 switch ($oCondition->Field()) 209 { 210 case \RainLoop\Providers\Filters\Enumerations\ConditionField::FROM: 211 $sResult .= 'header '.$sTypeWord.' ["From"]'; 212 break; 213 case \RainLoop\Providers\Filters\Enumerations\ConditionField::RECIPIENT: 214 $sResult .= 'header '.$sTypeWord.' ["To", "CC"]'; 215 break; 216 case \RainLoop\Providers\Filters\Enumerations\ConditionField::SUBJECT: 217 $sResult .= 'header '.$sTypeWord.' ["Subject"]'; 218 break; 219 case \RainLoop\Providers\Filters\Enumerations\ConditionField::HEADER: 220 $sResult .= 'header '.$sTypeWord.' ["'.$this->quote($sValueSecond).'"]'; 221 break; 222 case \RainLoop\Providers\Filters\Enumerations\ConditionField::SIZE: 223 $sResult .= 'size '.$sTypeWord; 224 break; 225 default: 226 $bTrue = false; 227 $sResult = '/* @Error: unknown field value */ false'; 228 break; 229 } 230 231 if ($bTrue) 232 { 233 if (\in_array($oCondition->Field(), array( 234 \RainLoop\Providers\Filters\Enumerations\ConditionField::FROM, 235 \RainLoop\Providers\Filters\Enumerations\ConditionField::RECIPIENT 236 )) && false !== \strpos($sValue, ',')) 237 { 238 $self = $this; 239 $aValue = \array_map(function ($sValue) use ($self) { 240 return '"'.$self->quote(\trim($sValue)).'"'; 241 }, \explode(',', $sValue)); 242 243 $sResult .= ' ['.\trim(\implode(', ', $aValue)).']'; 244 } 245 else if (\RainLoop\Providers\Filters\Enumerations\ConditionField::SIZE === $oCondition->Field()) 246 { 247 $sResult .= ' '.$this->quote($sValue); 248 } 249 else 250 { 251 $sResult .= ' "'.$this->quote($sValue).'"'; 252 } 253 254 $sResult = \MailSo\Base\Utils::StripSpaces($sResult); 255 } 256 } 257 else 258 { 259 $sResult = '/* @Error: empty condition value */ false'; 260 } 261 262 return $sResult; 263 } 264 265 /** 266 * @param \RainLoop\Providers\Filters\Classes\Filter $oFilter 267 * @param array $aCapa 268 * 269 * @return string 270 */ 271 private function filterToSieveScript($oFilter, &$aCapa) 272 { 273 $sNL = \RainLoop\Providers\Filters\SieveStorage::NEW_LINE; 274 $sTab = ' '; 275 276 $bAll = false; 277 $aResult = array(); 278 279 // Conditions 280 $aConditions = $oFilter->Conditions(); 281 if (\is_array($aConditions)) 282 { 283 if (1 < \count($aConditions)) 284 { 285 if (\RainLoop\Providers\Filters\Enumerations\ConditionsType::ANY === 286 $oFilter->ConditionsType()) 287 { 288 $aResult[] = 'if anyof('; 289 290 $bTrim = false; 291 foreach ($aConditions as $oCond) 292 { 293 $bTrim = true; 294 $sCons = $this->conditionToSieveScript($oCond, $aCapa); 295 if (!empty($sCons)) 296 { 297 $aResult[] = $sTab.$sCons.','; 298 } 299 } 300 if ($bTrim) 301 { 302 $aResult[\count($aResult) - 1] = \rtrim($aResult[\count($aResult) - 1], ','); 303 } 304 305 $aResult[] = ')'; 306 } 307 else 308 { 309 $aResult[] = 'if allof('; 310 foreach ($aConditions as $oCond) 311 { 312 $aResult[] = $sTab.$this->conditionToSieveScript($oCond, $aCapa).','; 313 } 314 315 $aResult[\count($aResult) - 1] = \rtrim($aResult[\count($aResult) - 1], ','); 316 $aResult[] = ')'; 317 } 318 } 319 else if (1 === \count($aConditions)) 320 { 321 $aResult[] = 'if '.$this->conditionToSieveScript($aConditions[0], $aCapa).''; 322 } 323 else 324 { 325 $bAll = true; 326 } 327 } 328 329 // actions 330 if (!$bAll) 331 { 332 $aResult[] = '{'; 333 } 334 else 335 { 336 $sTab = ''; 337 } 338 339 if ($oFilter->MarkAsRead() && \in_array($oFilter->ActionType(), array( 340 \RainLoop\Providers\Filters\Enumerations\ActionType::NONE, 341 \RainLoop\Providers\Filters\Enumerations\ActionType::MOVE_TO, 342 \RainLoop\Providers\Filters\Enumerations\ActionType::FORWARD 343 ))) 344 { 345 $aCapa['imap4flags'] = true; 346 $aResult[] = $sTab.'addflag "\\\\Seen";'; 347 } 348 349 switch ($oFilter->ActionType()) 350 { 351 case \RainLoop\Providers\Filters\Enumerations\ActionType::NONE: 352 $aResult[] = $sTab.'stop;'; 353 break; 354 case \RainLoop\Providers\Filters\Enumerations\ActionType::DISCARD: 355 $aResult[] = $sTab.'discard;'; 356 $aResult[] = $sTab.'stop;'; 357 break; 358 case \RainLoop\Providers\Filters\Enumerations\ActionType::VACATION: 359 $sValue = \trim($oFilter->ActionValue()); 360 $sValueSecond = \trim($oFilter->ActionValueSecond()); 361 $sValueThird = \trim($oFilter->ActionValueThird()); 362 $sValueFourth = \trim($oFilter->ActionValueFourth()); 363 if (0 < \strlen($sValue)) 364 { 365 $aCapa['vacation'] = true; 366 367 $iDays = 1; 368 $sSubject = ''; 369 if (0 < \strlen($sValueSecond)) 370 { 371 $sSubject = ':subject "'. 372 $this->quote(\MailSo\Base\Utils::StripSpaces($sValueSecond)).'" '; 373 } 374 375 if (0 < \strlen($sValueThird) && \is_numeric($sValueThird) && 1 < (int) $sValueThird) 376 { 377 $iDays = (int) $sValueThird; 378 } 379 380 $sAddresses = ''; 381 if (0 < \strlen($sValueFourth)) 382 { 383 $self = $this; 384 385 $aAddresses = \explode(',', $sValueFourth); 386 $aAddresses = \array_filter(\array_map(function ($sEmail) use ($self) { 387 $sEmail = \trim($sEmail); 388 return 0 < \strlen($sEmail) ? '"'.$self->quote($sEmail).'"' : ''; 389 }, $aAddresses), 'strlen'); 390 391 if (0 < \count($aAddresses)) 392 { 393 $sAddresses = ':addresses ['.\implode(', ', $aAddresses).'] '; 394 } 395 } 396 397 $aResult[] = $sTab.'vacation :days '.$iDays.' '.$sAddresses.$sSubject.'"'.$this->quote($sValue).'";'; 398 if ($oFilter->Stop()) 399 { 400 $aResult[] = $sTab.'stop;'; 401 } 402 } 403 else 404 { 405 $aResult[] = $sTab.'# @Error (vacation): empty action value'; 406 } 407 break; 408 case \RainLoop\Providers\Filters\Enumerations\ActionType::REJECT: 409 $sValue = \trim($oFilter->ActionValue()); 410 if (0 < \strlen($sValue)) 411 { 412 $aCapa['reject'] = true; 413 414 $aResult[] = $sTab.'reject "'.$this->quote($sValue).'";'; 415 $aResult[] = $sTab.'stop;'; 416 } 417 else 418 { 419 $aResult[] = $sTab.'# @Error (reject): empty action value'; 420 } 421 break; 422 case \RainLoop\Providers\Filters\Enumerations\ActionType::FORWARD: 423 $sValue = $oFilter->ActionValue(); 424 if (0 < \strlen($sValue)) 425 { 426 if ($oFilter->Keep()) 427 { 428 $aCapa['fileinto'] = true; 429 $aResult[] = $sTab.'fileinto "INBOX";'; 430 } 431 432 $aResult[] = $sTab.'redirect "'.$this->quote($sValue).'";'; 433 $aResult[] = $sTab.'stop;'; 434 } 435 else 436 { 437 $aResult[] = $sTab.'# @Error (redirect): empty action value'; 438 } 439 break; 440 case \RainLoop\Providers\Filters\Enumerations\ActionType::MOVE_TO: 441 $sValue = $oFilter->ActionValue(); 442 if (0 < \strlen($sValue)) 443 { 444 $sFolderName = $sValue; // utf7-imap 445 if ($this->bUtf8FolderName) // to utf-8 446 { 447 $sFolderName = \MailSo\Base\Utils::ConvertEncoding($sFolderName, 448 \MailSo\Base\Enumerations\Charset::UTF_7_IMAP, 449 \MailSo\Base\Enumerations\Charset::UTF_8); 450 } 451 452 $aCapa['fileinto'] = true; 453 $aResult[] = $sTab.'fileinto "'.$this->quote($sFolderName).'";'; 454 $aResult[] = $sTab.'stop;'; 455 } 456 else 457 { 458 $aResult[] = $sTab.'# @Error (fileinto): empty action value'; 459 } 460 break; 461 } 462 463 if (!$bAll) 464 { 465 $aResult[] = '}'; 466 } 467 468 return \implode($sNL, $aResult); 469 } 470 471 /** 472 * @param array $aFilters 473 * 474 * @return string 475 */ 476 private function collectionToFileString($aFilters) 477 { 478 $sNL = \RainLoop\Providers\Filters\SieveStorage::NEW_LINE; 479 480 $aCapa = array(); 481 $aParts = array(); 482 483 $aParts[] = '# This is RainLoop Webmail sieve script.'; 484 $aParts[] = '# Please don\'t change anything here.'; 485 $aParts[] = '# RAINLOOP:SIEVE'; 486 $aParts[] = ''; 487 488 foreach ($aFilters as /* @var $oItem \RainLoop\Providers\Filters\Classes\Filter */ $oItem) 489 { 490 $aData = array(); 491 $aData[] = '/*'; 492 $aData[] = 'BEGIN:FILTER:'.$oItem->ID(); 493 $aData[] = 'BEGIN:HEADER'; 494 $aData[] = \chunk_split(\base64_encode($oItem->serializeToJson()), 74, $sNL).'END:HEADER'; 495 $aData[] = '*/'; 496 $aData[] = $oItem->Enabled() ? '' : '/* @Filter is disabled '; 497 $aData[] = $this->filterToSieveScript($oItem, $aCapa); 498 $aData[] = $oItem->Enabled() ? '' : '*/'; 499 $aData[] = '/* END:FILTER */'; 500 $aData[] = ''; 501 502 $aParts[] = \implode($sNL, $aData); 503 } 504 505 $aCapa = \array_keys($aCapa); 506 $sCapa = 0 < \count($aCapa) ? $sNL.'require '. 507 \str_replace('","', '", "', \json_encode($aCapa)).';'.$sNL : ''; 508 509 return $sCapa.$sNL.\implode($sNL, $aParts).$sNL; 510 } 511 512 /** 513 * @param string $sFileString 514 * 515 * @return array 516 */ 517 private function fileStringToCollection($sFileString) 518 { 519 $aResult = array(); 520 if (!empty($sFileString) && false !== \strpos($sFileString, 'RAINLOOP:SIEVE')) 521 { 522 $aMatch = array(); 523 if (\preg_match_all('/BEGIN:FILTER(.+?)BEGIN:HEADER(.+?)END:HEADER/s', $sFileString, $aMatch) && 524 isset($aMatch[2]) && \is_array($aMatch[2])) 525 { 526 foreach ($aMatch[2] as $sEncodedLine) 527 { 528 if (!empty($sEncodedLine)) 529 { 530 $sDecodedLine = \base64_decode(\preg_replace('/[\s]+/', '', $sEncodedLine)); 531 if (!empty($sDecodedLine)) 532 { 533 $oItem = new \RainLoop\Providers\Filters\Classes\Filter(); 534 if ($oItem && $oItem->unserializeFromJson($sDecodedLine)) 535 { 536 $aResult[] = $oItem; 537 } 538 } 539 } 540 } 541 } 542 } 543 544 return $aResult; 545 } 546 547 /** 548 * @param string $sValue 549 * 550 * @return string 551 */ 552 public function quote($sValue) 553 { 554 return \str_replace(array('\\', '"'), array('\\\\', '\\"'), \trim($sValue)); 555 } 556 557 /** 558 * @param \MailSo\Log\Logger $oLogger 559 */ 560 public function SetLogger($oLogger) 561 { 562 $this->oLogger = $oLogger instanceof \MailSo\Log\Logger ? $oLogger : null; 563 } 564} 565