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+ }
0 commit comments