├── .gitignore ├── LICENSE.txt ├── README.md ├── composer.json └── src ├── Exceptions ├── FilterActionParamException.php ├── LiteralException.php ├── ResponseException.php ├── SieveException.php └── SocketException.php ├── Filters ├── Actions │ ├── AddFlagFilterAction.php │ ├── AddHeaderFilterAction.php │ ├── BaseFilterAction.php │ ├── BaseFlagFilterAction.php │ ├── BaseRejectFilterAction.php │ ├── ConvertFilterAction.php │ ├── DeleteHeaderFilterAction.php │ ├── DiscardFilterAction.php │ ├── EncloseFilterAction.php │ ├── ErejectFilterAction.php │ ├── ExtractTextFilterAction.php │ ├── FileIntoFilterAction.php │ ├── FilterAction.php │ ├── FlagFilterAction.php │ ├── KeepFilterAction.php │ ├── NotifyFilterAction.php │ ├── RedirectFilterAction.php │ ├── RejectFilterAction.php │ ├── RemoveFlagFilterAction.php │ ├── ReplaceFilterAction.php │ ├── SetFilterAction.php │ ├── StopFilterAction.php │ └── VacationFilterAction.php ├── Condition.php ├── FilterCriteria.php ├── FilterFactory.php └── Parser │ └── FilterParser.php ├── ManageSieve ├── Auth │ ├── BaseAuthMechanism.php │ ├── DigestMd5AuthMechanism.php │ ├── ExternalAuthMechanism.php │ ├── Interfaces │ │ └── AuthMechanism.php │ ├── LoginAuthMechanism.php │ ├── OauthbearerAuthMechanism.php │ ├── PlainAuthMechanism.php │ ├── Utils │ │ └── DigestMD5.php │ └── Xoauth2AuthMechanism.php ├── Client.php ├── Interfaces │ └── SieveClient.php └── SieveCommand.php └── Utils └── StringUtils.php /.gitignore: -------------------------------------------------------------------------------- 1 | test.php 2 | vendor/ 3 | .idea/ 4 | test.sieve 5 | composer.lock -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022- Marc Laporte, Henrique Borba, Josaphat Imani and Victor Emanouilov. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # php-sieve-manager 2 | 3 | A modern (started in 2022) PHP library for the ManageSieve protocol (RFC5804) to create/edit Sieve scripts (RFC5228). Used by [Cypht Webmail](https://github.com/cypht-org/cypht) and available to all PHP projects via [https://packagist.org/packages/henrique-borba/php-sieve-manager](https://packagist.org/packages/henrique-borba/php-sieve-manager) 4 | 5 | [Tiki Wiki CMS Groupware bundles Cypht webmail](https://doc.tiki.org/Cypht) and [extends filters beyond what is possible via the Sieve protocol](https://doc.tiki.org/Email-filters). 6 | 7 | [Compare php-sieve-manager to other options](https://github.com/cypht-org/php-sieve-manager/wiki/Comparison-of-Sieve-libs-in-PHP) 8 | 9 | # How to use 10 | 11 | ### Connect to ManageSieve 12 | 13 | ```php 14 | require_once "vendor/autoload.php"; 15 | 16 | $client = new \PhpSieveManager\ManageSieve\Client("localhost", 4190); 17 | $client->connect("test@localhost", "mypass", false, "", "PLAIN"); 18 | 19 | 20 | $client->listScripts(); 21 | ``` 22 | 23 | ### Generate Sieve script 24 | 25 | ```php 26 | $filter = \PhpSieveManager\Filters\FilterFactory::create('MaxFileSize'); 27 | 28 | 29 | $criteria = \PhpSieveManager\Filters\FilterCriteria::if('body')->contains('"test"'); 30 | 31 | // Messages bigger than 2MB will be rejected with an error message 32 | $size_condition = new \PhpSieveManager\Filters\Condition( 33 | "Messages bigger than 2MB will be rejected with an error message", $criteria 34 | ); 35 | 36 | $size_condition->addCriteria($criteria); 37 | $size_condition->addAction( 38 | new \PhpSieveManager\Filters\Actions\DiscardFilterAction() 39 | ); 40 | 41 | 42 | // Add the condition to the Filter 43 | $filter->setCondition($size_condition); 44 | $filter->toScript(); 45 | ``` 46 | 47 | ## Actions 48 | 49 | [\[RFC5293\]](https://www.rfc-editor.org/rfc/rfc5293.html) 50 | 51 | ``` 52 | addheader [":last"] 53 | 54 | 55 | ``` 56 | 57 | [\[RFC5293\]](https://www.rfc-editor.org/rfc/rfc5293.html) 58 | 59 | ``` 60 | deleteheader [":index" [":last"]] 61 | [COMPARATOR] [MATCH-TYPE] 62 | 63 | [] 64 | ``` 65 | 66 | [\[RFC8580\]](https://www.rfc-editor.org/rfc/rfc8580.html) [\[RFC5435\]](https://www.rfc-editor.org/rfc/rfc5434.html) 67 | 68 | ``` 69 | notify [":from" string] 70 | [":importance" <"1" / "2" / "3">] 71 | [":options" string-list] 72 | [":message" string] 73 | [:fcc "INBOX.Sent"] 74 | 75 | ``` 76 | 77 | [\[RFC5230\]](https://www.rfc-editor.org/rfc/rfc5230.html) [\[RFC6131\]](https://www.rfc-editor.org/rfc/rfc6131.html) [\[RFC8580\]](https://www.rfc-editor.org/rfc/rfc8580.html) 78 | 79 | ``` 80 | vacation [[":days" number] | [":seconds"]] 81 | [":subject" string] 82 | [":from" string] 83 | [":addresses" string-list] 84 | [":mime"] 85 | [":handle" string] 86 | 87 | ``` 88 | 89 | [\[RFC5232\]](https://www.rfc-editor.org/rfc/rfc5232.html) 90 | 91 | ``` 92 | setflag [] 93 | 94 | ``` 95 | 96 | [\[RFC5232\]](https://www.rfc-editor.org/rfc/rfc5232.html) 97 | 98 | ``` 99 | addflag [] 100 | 101 | ``` 102 | 103 | [\[RFC5232\]](https://www.rfc-editor.org/rfc/rfc5232.html) 104 | 105 | ``` 106 | removeflag [] 107 | 108 | ``` 109 | 110 | [\[RFC5703\]](https://www.rfc-editor.org/rfc/rfc5703.html) 111 | 112 | ``` 113 | replace [":mime"] 114 | [":subject" string] 115 | [":from" string] 116 | 117 | ``` 118 | 119 | [\[RFC5703\]](https://www.rfc-editor.org/rfc/rfc5703.html) 120 | 121 | ``` 122 | enclose <:subject string> 123 | <:headers string-list> 124 | string 125 | ``` 126 | 127 | [\[RFC5229\]](https://www.rfc-editor.org/rfc/rfc5229.html) 128 | 129 | ``` 130 | extracttext [MODIFIER] 131 | [":first" number] 132 | 133 | ``` 134 | 135 | [\[RFC6558\]](https://www.rfc-editor.org/rfc/rfc6558.html) 136 | 137 | ``` 138 | convert 139 | 140 | 141 | ``` 142 | 143 | [\[RFC5229\]](https://www.rfc-editor.org/rfc/rfc5232.html) 144 | 145 | ``` 146 | set [MODIFIER] 147 | 148 | 149 | Modifiers: ":lower" / ":upper" / ":lowerfirst" / ":upperfirst" / 150 | ":quotewildcard" / ":length" 151 | ``` 152 | 153 | [\[RFC5232\]](https://www.rfc-editor.org/rfc/rfc5232.html) [\[RFC3894\]](https://www.rfc-editor.org/rfc/rfc3894.html) [\[RFC5228\]](https://www.rfc-editor.org/rfc/rfc5228.html) [\[RFC5490\]](https://www.rfc-editor.org/rfc/rfc5490.html) [\[RFC9042\]](https://www.rfc-editor.org/rfc/rfc9042.html) [\[RFC8579\]](https://www.rfc-editor.org/rfc/rfc8579.html) 154 | 155 | ``` 156 | fileinto [:mailboxid ] [:specialuse ] [:create] [":copy"] [":flags" ] 157 | ``` 158 | 159 | [\[RFC5228\]](https://www.rfc-editor.org/rfc/rfc5232.html) [\[RFC3894\]](https://www.rfc-editor.org/rfc/rfc3894.html) [\[RFC6009\]](https://www.rfc-editor.org/rfc/rfc6009.html) 160 | 161 | ``` 162 | redirect [":copy"] [:notify "value"] [:ret "FULL"|"HDRS"] [":copy"] 163 | ``` 164 | 165 | [\[RFC5228\]](https://www.rfc-editor.org/rfc/rfc5232.html) [\[RFC5232\]](https://www.rfc-editor.org/rfc/rfc5232.html) 166 | 167 | ``` 168 | keep [":flags" ] 169 | ``` 170 | 171 | [\[RFC5228\]](https://www.rfc-editor.org/rfc/rfc5232.html) 172 | 173 | ``` 174 | discard 175 | ``` 176 | 177 | [\[RFC5429\]](https://www.rfc-editor.org/rfc/rfc5429.html) 178 | 179 | ``` 180 | reject 181 | ``` 182 | 183 | [\[RFC5429\]](https://www.rfc-editor.org/rfc/rfc5429.html) 184 | 185 | ``` 186 | ereject 187 | ``` 188 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypht-org/php-sieve-manager", 3 | "description": "A modern (started in 2022) PHP library for the ManageSieve protocol (RFC5804) to create/edit Sieve scripts (RFC5228). Used by Cypht Webmail.", 4 | "type": "library", 5 | "keywords": [ 6 | "mail", 7 | "php", 8 | "sieve", 9 | "managesieve", 10 | "filters", 11 | "imap", 12 | "email", 13 | "cypht", 14 | "RFC5804", 15 | "RFC5228" 16 | ], 17 | "homepage": "https://github.com/cypht-org/php-sieve-manager", 18 | "license": "MIT", 19 | "authors": [ 20 | { 21 | "name": "Henrique Borba", 22 | "email": "henrique.borba.dev@gmail.com", 23 | "role": "Initial developer, no longer active" 24 | }, 25 | { 26 | "name": "Victor Emanouilov", 27 | "role": "Maintainer" 28 | }, 29 | { 30 | "name": "Josaphat Imani", 31 | "role": "Maintainer" 32 | }, 33 | { 34 | "name": "Marc Laporte", 35 | "role": "Maintainer", 36 | "homepage": "https://MarcLaporte.com" 37 | } 38 | ], 39 | "support": { 40 | "issues": "https://github.com/cypht-org/php-sieve-manager/issues", 41 | "wiki": "https://github.com/cypht-org/php-sieve-manager/wiki", 42 | "source": "https://github.com/cypht-org/php-sieve-manager", 43 | "chat": "https://gitter.im/cypht-org/community" 44 | }, 45 | "require": { 46 | "php": ">=5.4" 47 | }, 48 | "autoload": { 49 | "psr-4": { 50 | "PhpSieveManager\\": "src/" 51 | } 52 | }, 53 | "minimum-stability": "stable" 54 | } 55 | -------------------------------------------------------------------------------- /src/Exceptions/FilterActionParamException.php: -------------------------------------------------------------------------------- 1 | data = $data; 14 | $this->code = $code; 15 | parent::__construct($data, 0); 16 | } 17 | } -------------------------------------------------------------------------------- /src/Exceptions/SieveException.php: -------------------------------------------------------------------------------- 1 | 'bool', 20 | 'field-name' => 'string', 21 | 'value' => 'string' 22 | ]; 23 | } 24 | 25 | /** 26 | * @return string 27 | */ 28 | public function parse() { 29 | $script = "addheader"; 30 | if (!empty($this->params['last'])) { 31 | $script .= " :last"; 32 | } 33 | $script .= " \"{$this->params['field-name']}\" \"{$this->params['value']}\";\n"; 34 | return $script; 35 | } 36 | } -------------------------------------------------------------------------------- /src/Filters/Actions/BaseFilterAction.php: -------------------------------------------------------------------------------- 1 | params = $params; 12 | $this->validateParams(); 13 | } 14 | 15 | protected function validateParams() { 16 | foreach ($this->getRequiredParams() as $param) { 17 | if (!isset($this->params[$param])) { 18 | throw new FilterActionParamException("Missing required parameter: $param"); 19 | } 20 | } 21 | $this->validateTypes(); 22 | } 23 | 24 | protected function validateTypes() { 25 | $paramTypes = $this->getParamTypes(); 26 | foreach ($this->params as $key => $value) { 27 | if (isset($paramTypes[$key]) && !$this->isValidType($value, $paramTypes[$key])) { 28 | throw new FilterActionParamException("Invalid type for parameter: $key. Expected " . $value); 29 | } 30 | } 31 | } 32 | 33 | /** 34 | * @param mixed $value 35 | * @param string $type 36 | * 37 | * @return bool 38 | */ 39 | protected function isValidType($value, $type) { 40 | switch ($type) { 41 | case 'string': 42 | return is_string($value); 43 | case 'int': 44 | return is_int($value); 45 | case 'float': 46 | return is_float($value); 47 | case 'bool': 48 | return is_bool($value); 49 | case 'array': 50 | return is_array($value); 51 | case 'string-list': 52 | return is_array($value) && array_reduce($value, function($carry, $item) { return $carry && is_string($item); }, true); 53 | default: 54 | return false; 55 | } 56 | } 57 | 58 | /** 59 | * @return array 60 | */ 61 | abstract protected function getRequiredParams(); 62 | 63 | /** 64 | * @return array 65 | */ 66 | abstract protected function getParamTypes(); 67 | } -------------------------------------------------------------------------------- /src/Filters/Actions/BaseFlagFilterAction.php: -------------------------------------------------------------------------------- 1 | 'string', 17 | 'flags' => 'string-list' 18 | ]; 19 | } 20 | 21 | /** 22 | * @return string 23 | */ 24 | public function parse() { 25 | $script = $this->getScriptName(); 26 | if (!empty($this->params['variablename'])) { 27 | $script .= "\"{$this->params['variablename']}\""; 28 | } 29 | $script .= " [" . implode(', ', array_map(function($flag) { return "\"$flag\""; }, $this->params['flags'])) . "];\n"; 30 | 31 | return $script; 32 | } 33 | 34 | abstract public function getScriptName(); 35 | } -------------------------------------------------------------------------------- /src/Filters/Actions/BaseRejectFilterAction.php: -------------------------------------------------------------------------------- 1 | 'string']; 16 | } 17 | 18 | /** 19 | * @return string 20 | */ 21 | public function parse() { 22 | $type = $this->getType(); 23 | $this->require[] = $type; 24 | return "{$type} \"{$type}\";\n"; 25 | } 26 | 27 | abstract protected function getType(); 28 | } -------------------------------------------------------------------------------- /src/Filters/Actions/ConvertFilterAction.php: -------------------------------------------------------------------------------- 1 | getParamTypes()); 15 | } 16 | 17 | protected function getParamTypes() { 18 | return [ 19 | 'quoted-from-media-type' => 'string', 20 | 'quoted-to-media-type' => 'string', 21 | 'transcoding-params' => 'string-list' 22 | ]; 23 | } 24 | 25 | /** 26 | * @return string 27 | */ 28 | public function parse() { 29 | return "convert \"{$this->params['quoted-from-media-type']}\" \"{$this->params['quoted-to-media-type']}\" [" . implode(', ', array_map(function($param) { return "\"$param\""; }, $this->params['transcoding-params'])) . "];\n"; 30 | } 31 | } -------------------------------------------------------------------------------- /src/Filters/Actions/DeleteHeaderFilterAction.php: -------------------------------------------------------------------------------- 1 | 'bool', 20 | 'last' => 'bool', 21 | 'comparator' => 'string', 22 | 'match-type' => 'string', 23 | 'field-name' => 'string', 24 | 'value-patterns' => 'string-list' 25 | ]; 26 | } 27 | 28 | /** 29 | * @return string 30 | */ 31 | public function parse() { 32 | $script = "deleteheader"; 33 | if (!empty($this->params['index'])) { 34 | $script .= " :index {$this->params['index']}"; 35 | if (!empty($this->params['last'])) { 36 | $script .= " :last"; 37 | } 38 | } 39 | if (!empty($this->params['comparator'])) { 40 | $script .= " {$this->params['comparator']}"; 41 | } 42 | if (!empty($this->params['match-type'])) { 43 | $script .= " {$this->params['match-type']}"; 44 | } 45 | $script .= " \"{$this->params['field-name']}\""; 46 | if (!empty($this->params['value-patterns'])) { 47 | $script .= " [\"" . implode('", "', $this->params['value-patterns']) . "\"]"; 48 | } 49 | $script .= ";\n"; 50 | return $script; 51 | } 52 | } -------------------------------------------------------------------------------- /src/Filters/Actions/DiscardFilterAction.php: -------------------------------------------------------------------------------- 1 | getParamTypes()); 15 | } 16 | 17 | protected function getParamTypes() { 18 | return [ 19 | 'subject' => 'string', 20 | 'headers' => 'string-list', 21 | 'content' => 'string' 22 | ]; 23 | } 24 | 25 | /** 26 | * @return string 27 | */ 28 | public function parse() { 29 | return "enclose :subject \"{$this->params['subject']}\" :headers [\"" . implode('", "', $this->params['headers']) . "\"] \"{$this->params['content']}\";\n"; 30 | } 31 | } -------------------------------------------------------------------------------- /src/Filters/Actions/ErejectFilterAction.php: -------------------------------------------------------------------------------- 1 | 'string', 20 | 'first' => 'int', 21 | 'varname' => 'string' 22 | ]; 23 | } 24 | 25 | /** 26 | * @return string 27 | */ 28 | public function parse() { 29 | $script = "extracttext"; 30 | if (!empty($this->params['modifier'])) { 31 | $script .= " {$this->params['modifier']}"; 32 | } 33 | if (!empty($this->params['first'])) { 34 | $script .= " :first {$this->params['first']}"; 35 | } 36 | $script .= " \"{$this->params['varname']}\";\n"; 37 | return $script; 38 | } 39 | } -------------------------------------------------------------------------------- /src/Filters/Actions/FileIntoFilterAction.php: -------------------------------------------------------------------------------- 1 | 'string', 17 | 'flags' => 'string-list', 18 | 'copy' => 'bool', 19 | 'mailboxid' => 'string', 20 | 'create' => 'bool', 21 | 'specialuse' => 'string', 22 | ]; 23 | } 24 | 25 | /** 26 | * @return string 27 | */ 28 | public function parse() { 29 | $script = "fileinto"; 30 | if (!empty($this->params['special-use-attr'])) { 31 | $this->require[] = 'special-use'; 32 | $script .= " :specialuse \"{$this->params['specialuse']}\""; 33 | } 34 | if (!empty($this->params['create'])) { 35 | $this->require[] = 'mailbox'; 36 | $script .= " :create"; 37 | } 38 | if (!empty($this->params['mailboxid'])) { 39 | $this->require[] = 'mailboxid'; 40 | $script .= " :mailboxid \"{$this->params['mailboxid']}\""; 41 | } 42 | if (!empty($this->params['copy'])) { 43 | $this->require[] = 'copy'; 44 | $script .= " :copy"; 45 | } 46 | if (!empty($this->params['flags'])) { 47 | $this->require[] = 'imap4flags'; 48 | $script .= " :flags \"" . implode('", "', $this->params['flags']) . "\""; 49 | } 50 | $script .= " \"{$this->params['mailbox']}\";\n"; 51 | return $script; 52 | } 53 | } -------------------------------------------------------------------------------- /src/Filters/Actions/FilterAction.php: -------------------------------------------------------------------------------- 1 | 'string-list']; 17 | } 18 | 19 | /** 20 | * @return string 21 | */ 22 | public function parse() { 23 | $flags = ''; 24 | if (!empty($this->params['flags'])) { 25 | $this->require[] = 'imap4flags'; 26 | $flags = " :flags \"" . implode('", "', $this->params['flags']) . "\""; 27 | } 28 | return "keep{$flags};\n"; 29 | } 30 | } -------------------------------------------------------------------------------- /src/Filters/Actions/NotifyFilterAction.php: -------------------------------------------------------------------------------- 1 | 'string', 20 | 'importance' => 'int', 21 | 'options' => 'string-list', 22 | 'message' => 'string', 23 | 'fcc' => 'string', 24 | 'method' => 'string', 25 | ]; 26 | } 27 | 28 | /** 29 | * @return string 30 | */ 31 | public function parse() { 32 | $script = "notify"; 33 | if (!empty($this->params['from'])) { 34 | $script .= " :from \"{$this->params['from']}\""; 35 | } 36 | if (!empty($this->params['importance'])) { 37 | $script .= " :importance \"{$this->params['importance']}\""; 38 | } 39 | if (!empty($this->params['options'])) { 40 | $script .= " :options [\"" . implode('", "', $this->params['options']) . "\"]"; 41 | } 42 | if (!empty($this->params['fcc'])) { 43 | $this->require[] = 'fcc'; 44 | $script .= " :fcc \"{$this->params['fcc']}\""; 45 | } 46 | if (!empty($this->params['message'])) { 47 | $script .= " :message \"{$this->params['message']}\""; 48 | } 49 | $script .= " \"{$this->params['method']}\";\n"; 50 | return $script; 51 | } 52 | } -------------------------------------------------------------------------------- /src/Filters/Actions/RedirectFilterAction.php: -------------------------------------------------------------------------------- 1 | 'string', 17 | 'copy' => 'bool', 18 | 'notify' => 'string', 19 | 'ret' => 'string', 20 | ]; 21 | } 22 | 23 | /** 24 | * @return string 25 | */ 26 | public function parse() { 27 | $script = "redirect"; 28 | if (!empty($this->params['copy'])) { 29 | $this->require[] = 'copy'; 30 | $script .= " :copy"; 31 | } 32 | if (!empty($this->params['notify'])) { 33 | $this->require[] = 'redirect-dsn'; 34 | $script .= " :notify \"{$this->params['notify']}\""; 35 | } 36 | if (!empty($this->params['ret'])) { 37 | $script .= " :ret \"{$this->params['ret']}\""; 38 | } 39 | $script .= " \"{$this->params['address']}\";\n"; 40 | return $script; 41 | } 42 | } -------------------------------------------------------------------------------- /src/Filters/Actions/RejectFilterAction.php: -------------------------------------------------------------------------------- 1 | 'string', 17 | 'subject' => 'int', 18 | 'from' => 'string-list', 19 | 'replacement' => 'string', 20 | ]; 21 | } 22 | 23 | public function parse() { 24 | $script = "replace"; 25 | if (!empty($this->params['mime'])) { 26 | $script .= " :mime {$this->params['mime']}"; 27 | } 28 | if (!empty($this->params['subject'])) { 29 | $script .= " :subject \"{$this->params['subject']}\""; 30 | } 31 | if (!empty($this->params['from'])) { 32 | $script .= " :from \"{$this->params['from']}\""; 33 | } 34 | $script .= " \"{$this->params['replacement']}\";\n"; 35 | return $script; 36 | } 37 | } -------------------------------------------------------------------------------- /src/Filters/Actions/SetFilterAction.php: -------------------------------------------------------------------------------- 1 | 'string', 20 | 'value' => 'string', 21 | 'modifier' => 'string', 22 | ]; 23 | } 24 | 25 | /** 26 | * @return string 27 | */ 28 | public function parse() { 29 | $script = "set"; 30 | if (!empty($this->params['modifier'])) { 31 | $script .= " {$this->params['modifier']}"; 32 | } 33 | $script .= " \"{$this->params['name']}\" \"{$this->params['value']}\";\n"; 34 | return $script; 35 | } 36 | } -------------------------------------------------------------------------------- /src/Filters/Actions/StopFilterAction.php: -------------------------------------------------------------------------------- 1 | 'int', 12 | 'seconds' => 'int', 13 | 'subject' => 'string', 14 | 'from' => 'string', 15 | 'addresses' => 'string-list', 16 | 'mime' => 'bool', 17 | 'handle' => 'string', 18 | 'reason' => 'string' 19 | ]; 20 | } 21 | 22 | protected function getRequiredParams() { 23 | return ['reason']; 24 | } 25 | 26 | /** 27 | * @return string 28 | */ 29 | public function parse() { 30 | $script = "vacation"; 31 | if (!empty($this->params['seconds'])) { 32 | $script .= " :seconds {$this->params['seconds']}"; 33 | $this->require[] = 'vacation-seconds'; 34 | } elseif (!empty($this->params['days'])) { 35 | $script .= " :days {$this->params['days']}"; 36 | } 37 | if (!empty($this->params['subject'])) { 38 | $script .= " :subject \"{$this->params['subject']}\""; 39 | } 40 | if (!empty($this->params['from'])) { 41 | $script .= " :from \"{$this->params['from']}\""; 42 | } 43 | if (!empty($this->params['addresses'])) { 44 | $script .= " :addresses [\"" . implode('", "', $this->params['addresses']) . "\"]"; 45 | } 46 | if (!empty($this->params['mime'])) { 47 | $script .= " :mime {$this->params['mime']}"; 48 | } 49 | if (!empty($this->params['handle'])) { 50 | $script .= " :handle \"{$this->params['handle']}\""; 51 | } 52 | $script .= " \"{$this->params['reason']}\";\n"; 53 | return $script; 54 | } 55 | } -------------------------------------------------------------------------------- /src/Filters/Condition.php: -------------------------------------------------------------------------------- 1 | description = $description; 23 | $this->test_list = $test_list; 24 | } 25 | 26 | /** 27 | * @return string 28 | */ 29 | public function getDescription(): string 30 | { 31 | return $this->description; 32 | } 33 | 34 | /** 35 | * @param $action 36 | * @return Condition 37 | */ 38 | public function addAction($action) 39 | { 40 | if (isset($action->require)) { 41 | $reqs = array_merge($this->requirements, $action->require); 42 | $this->requirements = array_unique($reqs); 43 | } 44 | $this->actions[] = $action; 45 | return $this; 46 | } 47 | 48 | /** 49 | * @return array 50 | */ 51 | public function getRequirements() { 52 | return $this->requirements; 53 | } 54 | 55 | /** 56 | * @param FilterCriteria $criteria 57 | * @return $this; 58 | */ 59 | public function addCriteria(FilterCriteria $criteria) 60 | { 61 | $this->criterias[] = $criteria; 62 | return $this; 63 | } 64 | 65 | /** 66 | * @return string 67 | */ 68 | public function parse($first = true) 69 | { 70 | $parsed_str = "\n"; 71 | if ($this->description != "") { 72 | $parsed_str .= "# ".$this->description. "\n"; 73 | } 74 | 75 | $parsed_str .= $first ? 'if ' : 'elsif '; 76 | if (count($this->criterias) > 1) { 77 | $parsed_str .= $this->test_list.'('; 78 | } 79 | 80 | foreach ($this->criterias as $idx => $criteria) { 81 | if ($idx != 0) { 82 | $parsed_str .= ' ,'.$this->comparator_type.' '; 83 | } 84 | $parsed_str .= $criteria->parse($idx); 85 | } 86 | if (count($this->criterias) > 1) { 87 | $parsed_str .= ')'; 88 | } 89 | $parsed_str .= ' {'."\n"; 90 | foreach ($this->actions as $action) { 91 | $parsed_str .= "\t".$action->parse(); 92 | } 93 | $parsed_str .= "\n".'}'; 94 | return $parsed_str; 95 | } 96 | } -------------------------------------------------------------------------------- /src/Filters/FilterCriteria.php: -------------------------------------------------------------------------------- 1 | target_field = $target_field; 37 | } 38 | 39 | /** 40 | * @param string $target_field 41 | * @return FilterCriteria 42 | */ 43 | public static function if(string $target_field = ""): FilterCriteria 44 | { 45 | return new FilterCriteria($target_field); 46 | } 47 | 48 | /** 49 | * @param string $value 50 | * @return FilterCriteria 51 | */ 52 | public function over(string $value) { 53 | $this->comparator = ':over'; 54 | $this->value = $value; 55 | return $this; 56 | } 57 | 58 | /** 59 | * @param string $value 60 | * @return $this 61 | */ 62 | public function is($value, ... $params) { 63 | $this->comparator = ':is'; 64 | 65 | if (is_array($value)) { 66 | $this->value = '['; 67 | foreach ($value as $idx => $v) { 68 | $this->value .= '"'.$v.'"'; 69 | if ($idx-1 != count($value)) { 70 | $this->value .= ','; 71 | } 72 | } 73 | $this->value .= "]"; 74 | $this->value .= " ".implode(' ', $params); 75 | return $this; 76 | } 77 | $this->value = $value; 78 | return $this; 79 | } 80 | 81 | /** 82 | * @param string $value 83 | * @return $this 84 | */ 85 | public function matches(string $value) { 86 | $this->comparator = ':is'; 87 | $this->value = $value; 88 | return $this; 89 | } 90 | 91 | /** 92 | * @param string $value 93 | * @return $this 94 | */ 95 | public function contains(string $value) { 96 | $this->comparator = ':contains'; 97 | $this->value = $value; 98 | return $this; 99 | } 100 | 101 | /** 102 | * @param string $value 103 | * @return $this 104 | */ 105 | public function regex(string $value) { 106 | $this->comparator = ':regex'; 107 | $this->value = $value; 108 | return $this; 109 | } 110 | 111 | /** 112 | * @param string $value 113 | * @return $this 114 | */ 115 | public function under(string $value) { 116 | $this->comparator = ':under'; 117 | $this->value = $value; 118 | return $this; 119 | } 120 | 121 | /** 122 | * @param string $value 123 | * @return $this 124 | */ 125 | public function count(string $value) { 126 | $this->comparator = ':count'; 127 | $this->value = $value; 128 | return $this; 129 | } 130 | 131 | /** 132 | * @param FilterCriteria $criteria 133 | * @return $this 134 | */ 135 | public function addCriteria(FilterCriteria $criteria) 136 | { 137 | $this->subcriterias[] = $criteria; 138 | return $this; 139 | } 140 | 141 | /** 142 | * @return string 143 | */ 144 | public function parse($index = 0) 145 | { 146 | $parsed_str = $this->target_field." ".$this->comparator." ".$this->value; 147 | return $parsed_str; 148 | } 149 | } -------------------------------------------------------------------------------- /src/Filters/FilterFactory.php: -------------------------------------------------------------------------------- 1 | name = $name; 25 | } 26 | 27 | /** 28 | * @param $name string 29 | * @return FilterFactory 30 | */ 31 | public static function create(string $name): FilterFactory 32 | { 33 | return new FilterFactory($name); 34 | } 35 | 36 | /** 37 | * @return string 38 | */ 39 | public function getName(): string 40 | { 41 | return $this->name; 42 | } 43 | 44 | /** 45 | * @param $requirement string 46 | * @return $this 47 | */ 48 | public function addRequirement(string $requirement): FilterFactory 49 | { 50 | if (! in_array($requirement, $this->require)) { 51 | $this->require[] = $requirement; 52 | } 53 | return $this; 54 | } 55 | 56 | 57 | /** 58 | * @return $this 59 | */ 60 | public function setCondition(Condition $condition): FilterFactory 61 | { 62 | if (count($condition->getRequirements()) > 0) { 63 | foreach ($condition->getRequirements() as $req) { 64 | $this->addRequirement($req); 65 | } 66 | } 67 | $this->conditions[] = $condition; 68 | return $this; 69 | } 70 | 71 | /** 72 | * @return string 73 | */ 74 | public function toScript() 75 | { 76 | return FilterParser::parseFromConditions($this->require, $this->conditions); 77 | } 78 | } -------------------------------------------------------------------------------- /src/Filters/Parser/FilterParser.php: -------------------------------------------------------------------------------- 1 | requirements = $requirements; 22 | $this->conditions = $conditions; 23 | } 24 | 25 | /** 26 | * @param array $requirements 27 | * @param Condition $conditions 28 | * @return string 29 | */ 30 | public static function parseFromConditions(array $requirements, array $conditions) 31 | { 32 | $parser = new self($requirements, $conditions); 33 | return $parser->parse(); 34 | } 35 | 36 | /** 37 | * @return string 38 | */ 39 | private function parseRequirements() { 40 | if (!count($this->requirements)) { 41 | return ""; 42 | } 43 | 44 | $parsed = "# Requirements\n"; 45 | $reqs = []; 46 | foreach ($this->requirements as $req) { 47 | $reqs[] = '"'.$req.'"'; 48 | } 49 | $parsed .= 'require ['.implode(',', $reqs).'];'."\n"; 50 | return $parsed; 51 | } 52 | 53 | /** 54 | * @return string 55 | */ 56 | private function parseConditions() { 57 | $ret = ''; 58 | foreach ($this->conditions as $key => $condition) { 59 | $ret .= $condition->parse($key == 0); 60 | } 61 | return $ret; 62 | } 63 | 64 | /** 65 | * @return string 66 | */ 67 | public function parse() { 68 | $parsed = $this->parseRequirements(); 69 | $parsed .= $this->parseConditions(); 70 | return $parsed; 71 | } 72 | } -------------------------------------------------------------------------------- /src/ManageSieve/Auth/BaseAuthMechanism.php: -------------------------------------------------------------------------------- 1 | username = $username; 26 | $this->password = $password; 27 | $this->authz_id = $authz_id; 28 | $this->client = $client; 29 | } 30 | } -------------------------------------------------------------------------------- /src/ManageSieve/Auth/DigestMd5AuthMechanism.php: -------------------------------------------------------------------------------- 1 | client->sendCommand( 18 | 'AUTHENTICATE', 19 | ['"DIGEST-MD5"'], 20 | true, 21 | null, 22 | 1 23 | ); 24 | 25 | $dmd5 = new DigestMD5( 26 | $return['response'], 27 | "sieve/".$this->client->getServerAddr() 28 | ); 29 | } 30 | } -------------------------------------------------------------------------------- /src/ManageSieve/Auth/ExternalAuthMechanism.php: -------------------------------------------------------------------------------- 1 | username); 17 | return new SieveCommand( 18 | "AUTHENTICATE", 19 | ['"EXTERNAL"', '"'.$args.'"'] 20 | ); 21 | } 22 | } -------------------------------------------------------------------------------- /src/ManageSieve/Auth/Interfaces/AuthMechanism.php: -------------------------------------------------------------------------------- 1 | username), 18 | base64_encode($this->password) 19 | ]; 20 | return new SieveCommand( 21 | "AUTHENTICATE", 22 | ['"LOGIN"', false, $extraLines] 23 | ); 24 | } 25 | } -------------------------------------------------------------------------------- /src/ManageSieve/Auth/OauthbearerAuthMechanism.php: -------------------------------------------------------------------------------- 1 | username}\001auth=$this->password\001\001"); 17 | return new SieveCommand( 18 | "AUTHENTICATE", 19 | ['"OAUTHBEARER"', '"'.$args.'"'] 20 | ); 21 | } 22 | } -------------------------------------------------------------------------------- /src/ManageSieve/Auth/PlainAuthMechanism.php: -------------------------------------------------------------------------------- 1 | username."\x00".$this->password); 16 | return new SieveCommand( 17 | "AUTHENTICATE", 18 | ['"PLAIN"', '"'.$args.'"'] 19 | ); 20 | } 21 | } -------------------------------------------------------------------------------- /src/ManageSieve/Auth/Utils/DigestMD5.php: -------------------------------------------------------------------------------- 1 | challenge = $challenge; 44 | $this->digestUri = $digestUri; 45 | 46 | $this->params = []; 47 | $this->realm = ""; 48 | $this->cnonce = null; 49 | 50 | $pexpression = '%(\w+)="(.+)"%'; 51 | foreach (explode(',', base64_decode($challenge)) as $elt) { 52 | preg_match($pexpression, $elt, $matches); 53 | if (count($matches)) { 54 | continue; 55 | } 56 | $this->params[$matches[1]] = $matches[2]; 57 | } 58 | } 59 | 60 | /** 61 | * @return string 62 | */ 63 | private function makeCnonce() { 64 | $return = ""; 65 | for ($i = 0; $i < 12; $i++) { 66 | $return .= rand(0, 0xff); 67 | } 68 | return base64_encode($return); 69 | } 70 | 71 | /** 72 | * @param $value string 73 | * @return string 74 | */ 75 | private function digest($value) { 76 | return md5($value); 77 | } 78 | 79 | /** 80 | * @param $value string 81 | * @return string 82 | */ 83 | private function hexdigest($value) { 84 | return bin2hex($this->digest($value)); 85 | } 86 | 87 | /** 88 | * @param $username 89 | * @param $password 90 | * @param $check 91 | * @return string 92 | */ 93 | private function makeResponse($username, $password, $check=false) { 94 | $a1 = $this->digest($username.':'.$this->realm.':'.$password).':'. 95 | $this->params["nonce"].':'. 96 | $this->cnonce; 97 | $a2 = "AUTHENTICATE:".$this->digestUri; 98 | 99 | if ($check) { 100 | $a2 = ':'.$this->digestUri; 101 | } 102 | 103 | $resp = $this->hexdigest($a1).":".$this->params["nonce"].":00000001:".$this->cnonce.":auth:".$this->hexdigest($a2); 104 | return $this->hexdigest($resp); 105 | } 106 | 107 | /** 108 | * @param $username 109 | * @param $password 110 | * @param $authz_id 111 | * @return string 112 | */ 113 | public function response($username, $password, $authz_id=null) { 114 | if (array_key_exists('realm', $this->params)) { 115 | $this->realm = $this->params['realm']; 116 | } 117 | $this->cnonce = $this->makeCnonce(); 118 | $responseValue = $this->makeResponse($username, $password); 119 | 120 | $dgres = 'username="'.$username.'",nonce="'.$this->params['nonce'].'",cnonce="'.$this->cnonce.'",nc=00000001,qop=auth,'. 121 | 'digest-uri="'.$this->digestUri.'",response='.$responseValue; 122 | 123 | if ($authz_id) { 124 | $dgres .= ',authzid="'.$authz_id.'"'; 125 | } 126 | 127 | return base64_encode($dgres); 128 | } 129 | } -------------------------------------------------------------------------------- /src/ManageSieve/Auth/Xoauth2AuthMechanism.php: -------------------------------------------------------------------------------- 1 | username}\001auth={$this->password}\001\001"); 17 | return new SieveCommand( 18 | "AUTHENTICATE", 19 | ['"XOAUTH2"', '"'.$args.'"'] 20 | ); 21 | } 22 | } -------------------------------------------------------------------------------- /src/ManageSieve/Client.php: -------------------------------------------------------------------------------- 1 | addr = $addr; 49 | $this->port = $port; 50 | $this->debug = $debug; 51 | $this->initExpressions(); 52 | } 53 | 54 | /** 55 | * Init regex expressions variables 56 | * 57 | * @return void 58 | */ 59 | private function initExpressions() { 60 | $this->respCodeExpression = "#(OK|NO|BYE)\s*(.+)?#"; 61 | $this->errorCodeExpression = '#(\([\w/-]+\))?\s*(".+")#'; 62 | $this->sizeExpression = "#\{(\d+)\+?\}#"; 63 | $this->activeExpression = "#ACTIVE#"; 64 | } 65 | 66 | private function getSingleLine() { 67 | $pos = strpos($this->readBuffer, "\r\n"); 68 | $return = substr($this->readBuffer, 0, $pos); 69 | return [$return, $pos]; 70 | } 71 | 72 | /** 73 | * Read line from the server 74 | * 75 | * @return false|string 76 | * @throws SocketException 77 | * @throws LiteralException 78 | * @throws ResponseException 79 | */ 80 | private function readLine() { 81 | $return = ""; 82 | while (true) { 83 | try { 84 | if ($this->readBuffer != null) { 85 | list($return, $pos) = $this->getSingleLine(); 86 | $this->readBuffer = substr($this->readBuffer, $pos + strlen("\r\n")); 87 | break; 88 | } 89 | } catch (\Exception $e) { 90 | $this->debugPrint($e->getMessage()); 91 | } 92 | 93 | try { 94 | $nval = \fread($this->sock, $this->readSize); 95 | if ($nval === false || $nval === "") { 96 | break; 97 | } 98 | $this->readBuffer .= $nval; 99 | } catch (\Exception $e) { 100 | throw new SocketException("Failed to read data from the server."); 101 | } 102 | } 103 | 104 | if (strlen($return)) { 105 | preg_match($this->respCodeExpression, $return, $matches); 106 | if (count($matches)) { 107 | if (strpos($matches[0], "NOTIFY") !== false) { 108 | return $return; 109 | } 110 | switch ($matches[1]) { 111 | case "BYE": 112 | throw new SocketException("Connection closed by the server"); 113 | case "NO": 114 | $this->parseError($matches[2]); 115 | } 116 | throw new ResponseException($matches[1], $matches[2]); 117 | } 118 | 119 | preg_match($this->sizeExpression, $return, $matches); 120 | if (count($matches)) { 121 | throw new LiteralException($matches[1]); 122 | } 123 | } 124 | 125 | return $return; 126 | } 127 | 128 | /** 129 | * Read a block of $size bytes from the server. 130 | * 131 | * @param $size 132 | * @return false|string 133 | * @throws SocketException 134 | */ 135 | private function readBlock($size) { 136 | $buffer = ""; 137 | if (strlen($this->readBuffer)) { 138 | $limit = strlen($this->readBuffer); 139 | if ($size <= strlen($this->readBuffer)) { 140 | $limit = $size; 141 | } 142 | 143 | $buffer = substr($this->readBuffer, 0, $limit); 144 | $this->readBuffer = substr($this->readBuffer, $limit); 145 | $size -= $limit; 146 | } 147 | 148 | if (! isset($size) || empty($size)) { 149 | return $buffer; 150 | } 151 | 152 | try { 153 | $buffer .= \fread($this->sock, $size); 154 | } catch (\Exception $e) { 155 | throw new SocketException("Failed to read from the server"); 156 | } 157 | 158 | return $buffer; 159 | } 160 | 161 | /** 162 | * Get last error message retrieved from 163 | * the server. 164 | * 165 | * @return mixed 166 | */ 167 | public function getErrorMessage() { 168 | return $this->errorMessage; 169 | } 170 | 171 | /** 172 | * Parse errors received from the server 173 | * 174 | * @return void 175 | * @throws SocketException 176 | */ 177 | private function parseError($text) { 178 | preg_match($this->sizeExpression, $text, $matches); 179 | if ($matches) { 180 | $this->errorCode = ""; 181 | $errorMessage = $matches[1] + 2; 182 | list($nextLine, $_) = $this->getSingleLine(); 183 | if (preg_match('/^\d+$/', trim($errorMessage)) && preg_match('/error:/i', $nextLine)) { 184 | $this->errorMessage = $nextLine; 185 | } else { 186 | $this->errorMessage = $this->readBlock($errorMessage); 187 | } 188 | return; 189 | } 190 | 191 | preg_match($this->errorCodeExpression, $text, $matches); 192 | if ($matches == false || count($matches) == 0) { 193 | throw new SocketException("Bad error message"); 194 | } 195 | 196 | if (array_key_exists(1, $matches)) { 197 | $this->errorCode = trim($matches[1], '()'); 198 | } else { 199 | $this->errorCode = ""; 200 | } 201 | $this->errorMessage = trim($matches[2], '"'); 202 | } 203 | 204 | /** 205 | * LOGOUT 206 | * 207 | * @return void 208 | * @throws LiteralException 209 | * @throws ResponseException 210 | * @throws SocketException 211 | */ 212 | public function logout() { 213 | $this->sendCommand("LOGOUT"); 214 | } 215 | 216 | /** 217 | * CAPABILITY 218 | * 219 | * @return string|null 220 | * @throws LiteralException 221 | * @throws ResponseException 222 | * @throws SocketException 223 | */ 224 | public function capability() { 225 | $return_payload = $this->sendCommand("CAPABILITY", null, true); 226 | if ($return_payload['code'] == 'OK') { 227 | return $return_payload['response']; 228 | } 229 | return null; 230 | } 231 | 232 | /** 233 | * Read server response line per line 234 | * 235 | * @param int $num_lines 236 | * @return array 237 | * @throws LiteralException 238 | * @throws ResponseException 239 | * @throws SocketException 240 | */ 241 | private function readResponse($num_lines = -1): array 242 | { 243 | $empty_return_count = 0; 244 | $response = ""; 245 | $code = null; 246 | $data = null; 247 | $cpt = 0; 248 | 249 | while (true) { 250 | try { 251 | $line = $this->readLine(); 252 | } catch (ResponseException $e) { 253 | $code = $e->code; 254 | $data = $e->data; 255 | break; 256 | } catch (LiteralException $e) { 257 | $response .= $this->readBlock($e->getMessage()); 258 | if (StringUtils::endsWith($response, "\r\n")) { 259 | $response .= $this->readLine() . "\r\n"; 260 | } 261 | continue; 262 | } 263 | 264 | if (!strlen($line)) { 265 | $empty_return_count = $empty_return_count + 1; 266 | if ($empty_return_count < 5) { 267 | continue; 268 | } 269 | throw new SocketException("readResponse time out"); 270 | } 271 | 272 | $response .= $line . "\r\n"; 273 | $cpt += 1; 274 | if ($num_lines != -1 && $cpt == $num_lines) { 275 | break; 276 | } 277 | } 278 | 279 | return [ 280 | "code" => $code, 281 | "data" => $data, 282 | "response" => $response 283 | ]; 284 | } 285 | 286 | /** 287 | * Debug messages if debug is enabled 288 | * 289 | * @param $message 290 | * @return void 291 | */ 292 | private function debugPrint($message) { 293 | if ($this->debug) { 294 | echo ("[DEBUG][".date("Y-m-d H:i:s")."] " . $message. "\n"); 295 | } 296 | } 297 | 298 | /** 299 | * Send a command to the server. 300 | * 301 | * @param $name 302 | * @param $args 303 | * @param bool $withResponse 304 | * @param $extralines 305 | * @param int $numLines 306 | * @return string[] 307 | * @throws LiteralException 308 | * @throws ResponseException 309 | * @throws SocketException 310 | */ 311 | public function sendCommand($name, $args=null, $withResponse=false, $extralines=null, $numLines=-1): array 312 | { 313 | $command = $name; 314 | if ($args != null) { 315 | $command .= ' '; 316 | $command .= implode(' ', $args); 317 | } 318 | 319 | $command = $command."\r\n"; 320 | 321 | $this->debugPrint($command); 322 | \fwrite($this->sock, $command, strlen($command)); 323 | 324 | if ($extralines) { 325 | foreach ($extralines as $line) { 326 | \fwrite($this->sock, $line."\r\n", strlen($line."\r\n")); 327 | } 328 | } 329 | 330 | $response_payload = $this->readResponse($numLines); 331 | 332 | if ($withResponse) { 333 | return [ 334 | "code" => $response_payload["code"], 335 | "data" => $response_payload["data"], 336 | "response" => $response_payload["response"] 337 | ]; 338 | } 339 | 340 | return [ 341 | "code" => $response_payload["code"], 342 | "data" => $response_payload["data"] 343 | ]; 344 | } 345 | 346 | /** 347 | * Return the supported authentication mechanism 348 | * from the server. 349 | * 350 | * @return false|string[] 351 | */ 352 | public function getSASLMechanisms() { 353 | $available = $this->capabilities["SASL"]; 354 | if (empty($available)) { 355 | return []; 356 | } else { 357 | return explode(" ", $available); 358 | } 359 | } 360 | 361 | /** 362 | * Return true if server support the mechanism 363 | * 364 | * @return bool 365 | */ 366 | public function hasSASLMechanism($mech) 367 | { 368 | return in_array($mech, $this->getSASLMechanisms()); 369 | } 370 | 371 | /** 372 | * Authenticate to server 373 | * 374 | * @param $username 375 | * @param $password 376 | * @param string $authz_id 377 | * @param $auth_mechanism 378 | * @return bool 379 | * @throws LiteralException 380 | * @throws ResponseException 381 | * @throws SieveException 382 | * @throws SocketException 383 | */ 384 | private function authenticate($username, $password, bool $tls = false, string $authz_id = "", $auth_mechanism = null): bool 385 | { 386 | if(array_key_exists("SASL", $this->capabilities) && $server_mechanisms = $this->getSASLMechanisms()) { 387 | // will filter by server_mechanisms 388 | } elseif (array_key_exists("STARTTLS", $this->capabilities) && $tls) { 389 | // start the TLS connection here 390 | $response = $this->sendCommand('STARTTLS'); 391 | if ($response['code'] != 'OK') { 392 | throw new SieveException('Error starting TLS connection to ManageSieve server: ' . $response['data']); 393 | } 394 | if (function_exists('get_tls_stream_type')) { 395 | $type = get_tls_stream_type(); 396 | } else { 397 | $type = STREAM_CRYPTO_METHOD_TLS_CLIENT; 398 | if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) { 399 | $type |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; 400 | $type |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; 401 | } 402 | } 403 | stream_socket_enable_crypto($this->sock, true, $type); 404 | $this->getCapabilitiesFromServer(); 405 | $server_mechanisms = $this->getSASLMechanisms(); 406 | } else { 407 | // continue without tls or sasl 408 | $server_mechanisms = []; 409 | } 410 | 411 | $mech_list = $this::SUPPORTED_AUTH_MECHS; 412 | if ($auth_mechanism != null && in_array($auth_mechanism, $this::SUPPORTED_AUTH_MECHS)) { 413 | $mech_list = [$auth_mechanism]; 414 | } 415 | 416 | foreach ($mech_list as $mech) { 417 | if ($server_mechanisms && ! in_array($mech, $server_mechanisms)) { 418 | continue; 419 | } 420 | $mech = str_replace("-", "", strtolower($mech)); 421 | $authentication_method = "PhpSieveManager\ManageSieve\Auth\\".ucfirst($mech)."AuthMechanism"; 422 | $auth_mechanism_obj = new $authentication_method($username, $password, $this, $authz_id); 423 | $generated_command = $auth_mechanism_obj->parse(); 424 | $return_payload = $this->sendCommand( 425 | $generated_command->name, 426 | $generated_command->args, 427 | $generated_command->withResponse, 428 | $generated_command->extralines, 429 | $generated_command->numLines 430 | ); 431 | 432 | if ($return_payload['code'] == "OK") { 433 | $this->authenticated = true; 434 | return true; 435 | } 436 | return false; 437 | } 438 | 439 | $this->errorMessage = "No suitable mechanism found"; 440 | return false; 441 | } 442 | 443 | /** 444 | * Format script before send to server 445 | * 446 | * @param $content 447 | * @return string 448 | */ 449 | private function prepareContent($content): string 450 | { 451 | return "{".strlen($content)."+}"."\r\n".$content; 452 | } 453 | 454 | /** 455 | * Upload script 456 | * 457 | * @param $name 458 | * @param $content 459 | * @return bool 460 | * @throws LiteralException 461 | * @throws ResponseException 462 | * @throws SocketException 463 | */ 464 | public function putScript($name, $content): bool 465 | { 466 | $content = $this->prepareContent($content); 467 | $return_payload = $this->sendCommand("PUTSCRIPT", ['"'.$name.'"', $content]); 468 | if ($return_payload["code"] == "OK") { 469 | return true; 470 | } 471 | return false; 472 | } 473 | 474 | /** 475 | * Delete script 476 | * 477 | * @param string $name 478 | * @return bool 479 | * @throws LiteralException 480 | * @throws ResponseException 481 | * @throws SocketException 482 | */ 483 | public function getScript(string $name) 484 | { 485 | $return_payload = $this->sendCommand("GETSCRIPT", ['"'.$name.'"'], true); 486 | if ($return_payload["code"] == "OK") { 487 | return $return_payload['response']; 488 | } 489 | return false; 490 | } 491 | 492 | /** 493 | * Activate script 494 | * 495 | * @param string $name 496 | * @return bool 497 | * @throws LiteralException 498 | * @throws ResponseException 499 | * @throws SocketException 500 | */ 501 | public function activateScript(string $name): bool 502 | { 503 | $return_payload = $this->sendCommand("SETACTIVE", ['"'.$name.'"']); 504 | if ($return_payload["code"] == "OK") { 505 | return true; 506 | } 507 | return false; 508 | } 509 | 510 | /** 511 | * Delete script 512 | * 513 | * @param string $name 514 | * @return bool 515 | * @throws LiteralException 516 | * @throws ResponseException 517 | * @throws SocketException 518 | */ 519 | public function removeScripts(string $name): bool 520 | { 521 | $return_payload = $this->sendCommand("DELETESCRIPT", ['"'.$name.'"']); 522 | if ($return_payload["code"] == "OK") { 523 | return true; 524 | } 525 | return false; 526 | } 527 | 528 | /** 529 | * Retrieve script 530 | * 531 | * @return bool|array 532 | * @throws LiteralException 533 | * @throws ResponseException 534 | * @throws SocketException 535 | */ 536 | public function listScripts() { 537 | $return_payload = $this->sendCommand("LISTSCRIPTS", NULL, true); 538 | if ($return_payload["code"] == "OK") { 539 | $scripts = []; 540 | foreach (explode("\n", $return_payload['response']) as $script_name) { 541 | if (trim($script_name) != '') { 542 | $scripts[] = str_replace('" ACTIV', '', substr($script_name, 1, -2)); 543 | } 544 | } 545 | return $scripts; 546 | } 547 | return false; 548 | } 549 | 550 | /** 551 | * Get server capabilities 552 | * 553 | * @return bool 554 | * @throws LiteralException 555 | * @throws ResponseException 556 | * @throws SocketException 557 | */ 558 | private function getCapabilitiesFromServer(): bool 559 | { 560 | $payload = $this->readResponse(); 561 | if ($payload["code"] == "NO") { 562 | return false; 563 | } 564 | foreach (explode("\r\n", $payload["response"]) as $l) { 565 | $parts = explode(" ", $l, 2); 566 | $cname = trim($parts[0], '"'); 567 | if (!in_array($cname, $this::KNOWN_CAPABILITIES)) { 568 | continue; 569 | } 570 | $this->capabilities[$cname] = null; 571 | if (count($parts) > 1) { 572 | $this->capabilities[$cname] = trim($parts[1], '"'); 573 | } 574 | } 575 | $this->capabilities['extensions'] = preg_split('/\s+/', $this->capabilities['SIEVE']); 576 | return true; 577 | } 578 | 579 | /** 580 | * Get capabilities array 581 | * 582 | * @return mixed 583 | */ 584 | public function getCapabilities() { 585 | return $this->capabilities; 586 | } 587 | 588 | /** 589 | * Connect to ManageSieve Protocol 590 | * 591 | * @param string $username 592 | * @param string $password 593 | * @param bool $tls 594 | * @return bool 595 | * @throws LiteralException 596 | * @throws ResponseException 597 | * @throws SocketException|SieveException 598 | */ 599 | public function connect($username, $password, $tls=false, $authz_id="", $auth_mechanism=null) { 600 | $connectionKey = $this->getConnectionKey($username, $tls); 601 | if (isset(self::$connectionPool[$connectionKey])) { 602 | $connection = self::$connectionPool[$connectionKey]; 603 | if ($this->isConnectionValid($connection)) { 604 | $this->sock = $connection; 605 | return true; 606 | } else { 607 | unset(self::$connectionPool[$connectionKey]); 608 | } 609 | } 610 | 611 | $ctx = stream_context_create(); 612 | stream_context_set_option($ctx, 'ssl', 'verify_peer_name', false); 613 | stream_context_set_option($ctx, 'ssl', 'verify_peer', false); 614 | $this->sock = stream_socket_client($this->addr.':'.$this->port, $errorno, $errorstr, $this->readTimeout, STREAM_CLIENT_CONNECT, $ctx); 615 | 616 | if ($this->sock === false) { 617 | throw new SocketException("Socket creation failed: " . $errorstr); 618 | } 619 | 620 | $this->connected = true; 621 | self::$connectionPool[$connectionKey] = $this->sock; 622 | if (!$this->getCapabilitiesFromServer()) { 623 | throw new SocketException("Failed to read capabilities from the server"); 624 | } 625 | if ($this->authenticate($username, $password, $tls, $authz_id, $auth_mechanism)) { 626 | return true; 627 | } 628 | throw new SieveException("Error while trying to connect to ManageSieve"); 629 | } 630 | 631 | private function getConnectionKey($username, $tls) { 632 | return md5($username . '|' . ($tls ? 'TLS' : 'PLAIN') . '|' . $this->addr . ':' . $this->port); 633 | } 634 | 635 | private function isConnectionValid($socket) { 636 | return is_resource($socket) && !feof($socket); 637 | } 638 | 639 | 640 | /** 641 | * @return void 642 | */ 643 | public function close() { 644 | if ($this->connected) { 645 | try { 646 | $this->connected = false; 647 | \fclose($this->sock); 648 | } catch (\Exception $e) {} 649 | } 650 | } 651 | 652 | /** 653 | * Make sure the socket is closed when 654 | * the object is freed 655 | */ 656 | public function __destruct() { 657 | if ($this->connected) { 658 | $this->close(); 659 | } 660 | } 661 | 662 | /** 663 | * @return string 664 | */ 665 | public function getServerAddr(): string 666 | { 667 | return $this->addr; 668 | } 669 | 670 | /** 671 | * Gets list of extensions that server supports. 672 | * 673 | * @return array 674 | */ 675 | public function getExtensions() 676 | { 677 | return $this->capabilities['extensions'] ?? []; 678 | } 679 | 680 | /** 681 | * Checks if server has extension 682 | * 683 | * @param string $extension 684 | * @return boolean 685 | */ 686 | public function hasExtension($extension) 687 | { 688 | $extension = trim(strtolower($extension)); 689 | if (is_array($this->capabilities['extensions'])) { 690 | foreach ($this->capabilities['extensions'] as $ext) { 691 | if ($ext == $extension) { 692 | return true; 693 | } 694 | } 695 | } 696 | 697 | return false; 698 | } 699 | 700 | /** 701 | * Checks if server can store script 702 | * 703 | * @param string $name The name of the script. 704 | * @param string $size The size of the script. 705 | * 706 | * @return boolean 707 | */ 708 | public function hasSpace($name, $size) 709 | { 710 | $return_payload = $this->sendCommand("HAVESPACE", ['"'.$name.'"', $size]); 711 | if ($return_payload["code"] == "OK") { 712 | return true; 713 | } 714 | return false; 715 | } 716 | 717 | /** 718 | * Verifies script validity without storing the script on the server 719 | * 720 | * @param string $name The name of the script. 721 | * @param string $size The size of the script. 722 | * 723 | * @return boolean 724 | */ 725 | public function checkScript($content) 726 | { 727 | $content = $this->prepareContent($content); 728 | $return_payload = $this->sendCommand("CHECKSCRIPT", [$content]); 729 | if ($return_payload["code"] == "OK") { 730 | return true; 731 | } 732 | return false; 733 | } 734 | 735 | /** 736 | * Used for re-synchronization or to reset any inactivity auto-logout timer on the server. 737 | * 738 | * @param string $name The name of the script. 739 | * @param string $size The size of the script. 740 | * 741 | * @return boolean 742 | */ 743 | public function noop() 744 | { 745 | $return_payload = $this->sendCommand("NOOP"); 746 | if ($return_payload["code"] == "OK") { 747 | return true; 748 | } 749 | return false; 750 | } 751 | 752 | /** 753 | * Rename a user's Sieve script 754 | * 755 | * @param string $oldName The old Script name 756 | * @param string $newName The new Script name 757 | * 758 | * @return boolean 759 | */ 760 | public function renameScript($oldName, $newName) 761 | { 762 | $return_payload = $this->sendCommand("RENAMESCRIPT", ['"'.$oldName.'"', '"'.$newName.'"',]); 763 | if ($return_payload["code"] == "OK") { 764 | return true; 765 | } 766 | return false; 767 | } 768 | 769 | /** 770 | * Creates a copy of another script 771 | * 772 | * @param string $oldName The old Script name 773 | * @param string $newName The new Script name 774 | * 775 | * @return boolean 776 | */ 777 | public function copyScript($name, $copy) 778 | { 779 | if ($content = $this->getScript($copy)) { 780 | return $this->putScript($name, $content); 781 | } 782 | return false; 783 | } 784 | } -------------------------------------------------------------------------------- /src/ManageSieve/Interfaces/SieveClient.php: -------------------------------------------------------------------------------- 1 | name = $name; 23 | $this->args = $args; 24 | $this->withResponse = $withResponse; 25 | $this->extralines = $extralines; 26 | $this->numLines = $numLines; 27 | } 28 | } -------------------------------------------------------------------------------- /src/Utils/StringUtils.php: -------------------------------------------------------------------------------- 1 |