Skip to content

Commit 4c35500

Browse files
Partial ManageSieve protocol implementation and exceptions
0 parents  commit 4c35500

File tree

8 files changed

+334
-0
lines changed

8 files changed

+334
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
test.php
2+
vendor/
3+
.idea/

README.md

Whitespace-only changes.

composer.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "henrique-borba/php-sieve-manager",
3+
"description": "PHP library to create and manage Sieve filters through the ManageSieve protocol",
4+
"type": "library",
5+
"license": "MIT",
6+
"autoload": {
7+
"psr-4": {
8+
"PhpSieveManager\\": "src/"
9+
}
10+
},
11+
"authors": [
12+
{
13+
"name": "Henrique Borba",
14+
"email": "henrique.borba.dev@gmail.com"
15+
}
16+
],
17+
"minimum-stability": "dev",
18+
"require": {}
19+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace PhpSieveManager\Exceptions;
4+
5+
class LiteralException extends \Exception
6+
{
7+
8+
}

src/Exceptions/SocketException.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace PhpSieveManager\Exceptions;
4+
5+
class SocketException extends \Exception
6+
{
7+
8+
}

src/ManageSieve/Client.php

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
<?php
2+
3+
namespace PhpSieveManager\ManageSieve;
4+
5+
use PhpSieveManager\Exceptions\LiteralException;
6+
use PhpSieveManager\Exceptions\SocketException;
7+
use PhpSieveManager\ManageSieve\Interfaces\SieveClient;
8+
use PhpSieveManager\Utils\StringUtils;
9+
10+
/**
11+
* ManageSieve Client
12+
*/
13+
class Client extends SieveClient
14+
{
15+
const KNOWN_CAPABILITIES = ["IMPLEMENTATION", "SASL", "SIEVE", "STARTTLS", "NOTIFY", "LANGUAGE", "VERSION"];
16+
17+
private $readSize = 4096;
18+
private $readTimeout = 5;
19+
20+
private $errorMessage;
21+
private $readBuffer;
22+
private $capabilities;
23+
private $addr;
24+
private $port;
25+
private $debug;
26+
private $sock;
27+
private $authenticated;
28+
private $errorCode;
29+
private $connected = false;
30+
31+
private $respCodeExpression;
32+
private $errorCodeExpression;
33+
private $sizeExpression;
34+
private $activeExpression;
35+
36+
/**
37+
* @param $addr
38+
* @param $port
39+
* @param $debug
40+
*/
41+
public function __construct($addr, $port=4190, $debug=false) {
42+
$this->addr = $addr;
43+
$this->port = $port;
44+
$this->debug = $debug;
45+
$this->initExpressions();
46+
}
47+
48+
/**
49+
* @return void
50+
*/
51+
private function initExpressions() {
52+
$this->respCodeExpression = "(OK|NO|BYE)\s*(.+)?";
53+
$this->errorCodeExpression = '(\([\w/-]+\))?\s*(".+")';
54+
$this->sizeExpression = "\{(\d+)\+?\}";
55+
$this->activeExpression = "ACTIVE";
56+
}
57+
58+
/**
59+
* Read line from the server
60+
* @return false|string
61+
* @throws SocketException
62+
* @throws LiteralException
63+
*/
64+
private function readLine() {
65+
$return = "";
66+
while (true) {
67+
try {
68+
$pos = strpos($this->readBuffer, "\r\n");
69+
$return = substr($this->readBuffer, 0, $pos);
70+
$this->readBuffer = $this->readBuffer[$pos + strlen("\r\n")];
71+
break;
72+
} catch (\Exception $e) { }
73+
74+
try {
75+
$nval = socket_read($this->sock, $this->readSize);
76+
if ($nval === false) {
77+
break;
78+
}
79+
$this->readBuffer .= $nval;
80+
} catch (\Exception $e) {
81+
throw new SocketException("Failed to read data from the server.");
82+
}
83+
}
84+
85+
if (strlen($return)) {
86+
preg_match($this->sizeExpression, $return, $matches);
87+
if ($matches) {
88+
throw new LiteralException($matches[1]);
89+
}
90+
91+
preg_match($this->respCodeExpression, $return, $matches);
92+
if ($matches) {
93+
switch ($matches[1]) {
94+
case "BYE":
95+
throw new SocketException("Connection closed by the server");
96+
case "NO":
97+
$this->parseError($matches[2]);
98+
}
99+
throw new SocketException($matches[1] . ' ' . $matches[2]);
100+
}
101+
}
102+
103+
return $return;
104+
}
105+
106+
/**
107+
* Read a block of $size bytes from the server.
108+
*
109+
* @param $size
110+
* @return false|string
111+
* @throws SocketException
112+
*/
113+
private function readBlock($size) {
114+
$buffer = "";
115+
if (count($this->readBuffer)) {
116+
$limit = count($this->readBuffer);
117+
if ($size <= count($this->readBuffer)) {
118+
$limit = $size;
119+
}
120+
121+
$buffer = substr($this->readBuffer, 0, $limit);
122+
$this->readBuffer = substr($this->readBuffer, $limit);
123+
$size -= $limit;
124+
}
125+
126+
if (!isset($size)) {
127+
return $buffer;
128+
}
129+
130+
try {
131+
$buffer .= socket_read($this->sock, $size);
132+
} catch (\Exception $e) {
133+
throw new SocketException("Failed to read from the server");
134+
}
135+
136+
return $buffer;
137+
}
138+
139+
/**
140+
* Parse errors received from the server
141+
*
142+
* @return void
143+
* @throws SocketException
144+
*/
145+
private function parseError($text) {
146+
preg_match($this->sizeExpression, $text, $matches);
147+
if ($matches) {
148+
$this->errorCode = "";
149+
$this->errorMessage = $this->readBlock($matches[1] + 2);
150+
return;
151+
}
152+
153+
preg_match($this->errorCodeExpression, $text, $matches);
154+
if ($matches == false || count($matches) == 0) {
155+
throw new SocketException("Bad error message");
156+
}
157+
158+
if (array_key_exists(1, $matches)) {
159+
$this->errorCode = trim($matches[1], ['(', ')']);
160+
} else {
161+
$this->errorCode = "";
162+
}
163+
$this->errorMessage = trim($matches[2], ['"']);
164+
}
165+
166+
/**
167+
* @param $num_lines
168+
* @return array
169+
*/
170+
private function readResponse($num_lines = -1) {
171+
$response = "";
172+
$code = null;
173+
$data = null;
174+
$cpt = 0;
175+
176+
while (true) {
177+
try {
178+
$line = $this->readLine();
179+
} catch (SocketException $e) {
180+
$code = $e->getCode();
181+
$data = $e->getMessage();
182+
break;
183+
} catch (LiteralException $e) {
184+
$response .= $this->readBlock($e->getMessage());
185+
if (StringUtils::endsWith($response, "\r\n")) {
186+
$response .= $this->readLine() . "\r\n";
187+
}
188+
continue;
189+
}
190+
191+
if (!strlen($line)) {
192+
continue;
193+
}
194+
195+
$response .= $line . "\r\n";
196+
$cpt += 1;
197+
if ($num_lines != -1 && $cpt == $num_lines) {
198+
break;
199+
}
200+
}
201+
202+
return [
203+
"code" => $code,
204+
"data" => $data,
205+
"response" => $response
206+
];
207+
}
208+
209+
/**
210+
* @return bool
211+
*/
212+
private function getCapabilities() {
213+
$payload = $this->readResponse();
214+
if ($payload["code"] == "NO") {
215+
return false;
216+
}
217+
218+
foreach (explode("\n", $payload["response"]) as $l) {
219+
$parts = explode(" ", $l, 1);
220+
$cname = trim($parts[0], ['"']);
221+
if (!in_array($cname, $this::KNOWN_CAPABILITIES)) {
222+
continue;
223+
}
224+
225+
$this->capabilities[$cname] = null;
226+
if (count($parts) > 1) {
227+
$this->capabilities[$cname] = trim($parts[1], ['"']);
228+
}
229+
}
230+
return true;
231+
}
232+
233+
/**
234+
* @param $username
235+
* @param $password
236+
* @param bool $tls
237+
* @return void
238+
* @throws SocketException
239+
*/
240+
public function connect($username, $password, $tls=false) {
241+
if (($this->sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) === false) {
242+
throw new SocketException("Socket creation failed: " . socket_strerror(socket_last_error()));
243+
}
244+
245+
if(($result = socket_connect($this->sock, $this->addr, $this->port)) === false) {
246+
throw new SocketException("Socket connect failed: (".$result.") " . socket_strerror(socket_last_error($this->sock)));
247+
}
248+
$this->connected = true;
249+
250+
if (!$this->getCapabilities()) {
251+
throw new SocketException("Failed to read capabilities from the server");
252+
}
253+
}
254+
255+
/**
256+
* @return void
257+
*/
258+
public function close() {
259+
socket_close($this->sock);
260+
}
261+
262+
public function __destruct()
263+
{
264+
if ($this->connected) {
265+
$this->close();
266+
}
267+
}
268+
269+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace PhpSieveManager\ManageSieve\Interfaces;
4+
5+
class SieveClient
6+
{
7+
8+
}

src/Utils/StringUtils.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace PhpSieveManager\Utils;
4+
5+
class StringUtils
6+
{
7+
public static function startsWith( $haystack, $needle ) {
8+
$length = strlen( $needle );
9+
return substr( $haystack, 0, $length ) === $needle;
10+
}
11+
12+
public static function endsWith( $haystack, $needle ) {
13+
$length = strlen( $needle );
14+
if( !$length ) {
15+
return true;
16+
}
17+
return substr( $haystack, -$length ) === $needle;
18+
}
19+
}

0 commit comments

Comments
 (0)