Total Complexity | 192 |
Total Lines | 1220 |
Duplicated Lines | 0 % |
Changes | 21 | ||
Bugs | 4 | Features | 5 |
Complex classes like ImapProtocol often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use ImapProtocol, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
31 | class ImapProtocol extends Protocol { |
||
32 | |||
33 | /** |
||
34 | * Request noun |
||
35 | * @var int |
||
36 | */ |
||
37 | protected $noun = 0; |
||
38 | |||
39 | /** |
||
40 | * Imap constructor. |
||
41 | * @param bool $cert_validation set to false to skip SSL certificate validation |
||
42 | * @param mixed $encryption Connection encryption method |
||
43 | */ |
||
44 | public function __construct(bool $cert_validation = true, $encryption = false) { |
||
45 | $this->setCertValidation($cert_validation); |
||
46 | $this->encryption = $encryption; |
||
47 | } |
||
48 | |||
49 | /** |
||
50 | * @throws ImapBadRequestException |
||
51 | * @throws ImapServerErrorException |
||
52 | * @throws RuntimeException |
||
53 | */ |
||
54 | public function __destruct() { |
||
55 | $this->logout(); |
||
56 | } |
||
57 | |||
58 | /** |
||
59 | * Open connection to IMAP server |
||
60 | * @param string $host hostname or IP address of IMAP server |
||
61 | * @param int|null $port of IMAP server, default is 143 and 993 for ssl |
||
62 | * |
||
63 | * @throws ConnectionFailedException |
||
64 | */ |
||
65 | public function connect(string $host, $port = null) { |
||
87 | } |
||
88 | } |
||
89 | |||
90 | /** |
||
91 | * Enable tls on the current connection |
||
92 | * |
||
93 | * @throws ConnectionFailedException |
||
94 | * @throws ImapBadRequestException |
||
95 | * @throws ImapServerErrorException |
||
96 | * @throws RuntimeException |
||
97 | */ |
||
98 | protected function enableStartTls(){ |
||
103 | } |
||
104 | } |
||
105 | |||
106 | /** |
||
107 | * Get the next line from stream |
||
108 | * |
||
109 | * @return string next line |
||
110 | * @throws RuntimeException |
||
111 | */ |
||
112 | public function nextLine(): string { |
||
113 | $line = ""; |
||
114 | while (($next_char = fread($this->stream, 1)) !== false && $next_char !== "\n") { |
||
115 | $line .= $next_char; |
||
116 | } |
||
117 | if ($line === "" && $next_char === false) { |
||
118 | throw new RuntimeException('empty response'); |
||
119 | } |
||
120 | if ($this->debug) echo "<< ".$line."\n"; |
||
121 | return $line . "\n"; |
||
122 | } |
||
123 | |||
124 | /** |
||
125 | * Get the next line and check if it starts with a given string |
||
126 | * @param string $start |
||
127 | * |
||
128 | * @return bool |
||
129 | * @throws RuntimeException |
||
130 | */ |
||
131 | protected function assumedNextLine(string $start): bool { |
||
132 | return str_starts_with($this->nextLine(), $start); |
||
133 | } |
||
134 | |||
135 | /** |
||
136 | * Get the next line and split the tag |
||
137 | * @param string|null $tag reference tag |
||
138 | * |
||
139 | * @return string next line |
||
140 | * @throws RuntimeException |
||
141 | */ |
||
142 | protected function nextTaggedLine(&$tag): string { |
||
147 | } |
||
148 | |||
149 | /** |
||
150 | * Get the next line and check if it contains a given string and split the tag |
||
151 | * @param string $start |
||
152 | * @param $tag |
||
153 | * |
||
154 | * @return bool |
||
155 | * @throws RuntimeException |
||
156 | */ |
||
157 | protected function assumedNextTaggedLine(string $start, &$tag): bool { |
||
160 | } |
||
161 | |||
162 | /** |
||
163 | * Split a given line in values. A value is literal of any form or a list |
||
164 | * @param string $line |
||
165 | * |
||
166 | * @return array |
||
167 | * @throws RuntimeException |
||
168 | */ |
||
169 | protected function decodeLine(string $line): array { |
||
170 | $tokens = []; |
||
171 | $stack = []; |
||
172 | |||
173 | // replace any trailing <NL> including spaces with a single space |
||
174 | $line = rtrim($line) . ' '; |
||
175 | while (($pos = strpos($line, ' ')) !== false) { |
||
176 | $token = substr($line, 0, $pos); |
||
177 | if (!strlen($token)) { |
||
178 | $line = substr($line, $pos + 1); |
||
179 | continue; |
||
180 | } |
||
181 | while ($token[0] == '(') { |
||
182 | $stack[] = $tokens; |
||
183 | $tokens = []; |
||
184 | $token = substr($token, 1); |
||
185 | } |
||
186 | if ($token[0] == '"') { |
||
187 | if (preg_match('%^\(*"((.|\\\\|\\")*?)" *%', $line, $matches)) { |
||
188 | $tokens[] = $matches[1]; |
||
189 | $line = substr($line, strlen($matches[0])); |
||
190 | continue; |
||
191 | } |
||
192 | } |
||
193 | if ($token[0] == '{') { |
||
194 | $endPos = strpos($token, '}'); |
||
195 | $chars = substr($token, 1, $endPos - 1); |
||
196 | if (is_numeric($chars)) { |
||
197 | $token = ''; |
||
198 | while (strlen($token) < $chars) { |
||
199 | $token .= $this->nextLine(); |
||
200 | } |
||
201 | $line = ''; |
||
202 | if (strlen($token) > $chars) { |
||
203 | $line = substr($token, $chars); |
||
204 | $token = substr($token, 0, $chars); |
||
205 | } else { |
||
206 | $line .= $this->nextLine(); |
||
207 | } |
||
208 | $tokens[] = $token; |
||
209 | $line = trim($line) . ' '; |
||
210 | continue; |
||
211 | } |
||
212 | } |
||
213 | if ($stack && $token[strlen($token) - 1] == ')') { |
||
214 | // closing braces are not separated by spaces, so we need to count them |
||
215 | $braces = strlen($token); |
||
216 | $token = rtrim($token, ')'); |
||
217 | // only count braces if more than one |
||
218 | $braces -= strlen($token) + 1; |
||
219 | // only add if token had more than just closing braces |
||
220 | if (rtrim($token) != '') { |
||
221 | $tokens[] = rtrim($token); |
||
222 | } |
||
223 | $token = $tokens; |
||
224 | $tokens = array_pop($stack); |
||
225 | // special handline if more than one closing brace |
||
226 | while ($braces-- > 0) { |
||
227 | $tokens[] = $token; |
||
228 | $token = $tokens; |
||
229 | $tokens = array_pop($stack); |
||
230 | } |
||
231 | } |
||
232 | $tokens[] = $token; |
||
233 | $line = substr($line, $pos + 1); |
||
234 | } |
||
235 | |||
236 | // maybe the server forgot to send some closing braces |
||
237 | while ($stack) { |
||
238 | $child = $tokens; |
||
239 | $tokens = array_pop($stack); |
||
240 | $tokens[] = $child; |
||
241 | } |
||
242 | |||
243 | return $tokens; |
||
244 | } |
||
245 | |||
246 | /** |
||
247 | * Read abd decode a response "line" |
||
248 | * @param array|string $tokens to decode |
||
249 | * @param string $wantedTag targeted tag |
||
250 | * @param bool $dontParse if true only the unparsed line is returned in $tokens |
||
251 | * |
||
252 | * @return bool |
||
253 | * @throws RuntimeException |
||
254 | */ |
||
255 | public function readLine(&$tokens = [], string $wantedTag = '*', bool $dontParse = false): bool { |
||
256 | $line = $this->nextTaggedLine($tag); // get next tag |
||
257 | if (!$dontParse) { |
||
258 | $tokens = $this->decodeLine($line); |
||
259 | } else { |
||
260 | $tokens = $line; |
||
261 | } |
||
262 | |||
263 | // if tag is wanted tag we might be at the end of a multiline response |
||
264 | return $tag == $wantedTag; |
||
265 | } |
||
266 | |||
267 | /** |
||
268 | * Read all lines of response until given tag is found |
||
269 | * |
||
270 | * @param string $tag request tag |
||
271 | * @param bool $dontParse if true every line is returned unparsed instead of the decoded tokens |
||
272 | * |
||
273 | * @return array |
||
274 | * |
||
275 | * @throws ImapBadRequestException |
||
276 | * @throws ImapServerErrorException |
||
277 | * @throws RuntimeException |
||
278 | */ |
||
279 | public function readResponse(string $tag, bool $dontParse = false): array { |
||
280 | $lines = []; |
||
281 | $tokens = null; // define $tokens variable before first use |
||
282 | do { |
||
283 | $readAll = $this->readLine($tokens, $tag, $dontParse); |
||
284 | $lines[] = $tokens; |
||
285 | } while (!$readAll); |
||
286 | |||
287 | if ($dontParse) { |
||
288 | // First two chars are still needed for the response code |
||
289 | $tokens = [substr($tokens, 0, 2)]; |
||
290 | } |
||
291 | |||
292 | // last line has response code |
||
293 | if ($tokens[0] == 'OK') { |
||
294 | return $lines ?: [true]; |
||
295 | } elseif ($tokens[0] == 'NO') { |
||
296 | throw new ImapServerErrorException(); |
||
297 | } |
||
298 | |||
299 | throw new ImapBadRequestException(); |
||
300 | } |
||
301 | |||
302 | /** |
||
303 | * Send a new request |
||
304 | * @param string $command |
||
305 | * @param array $tokens additional parameters to command, use escapeString() to prepare |
||
306 | * @param string|null $tag provide a tag otherwise an autogenerated is returned |
||
307 | * |
||
308 | * @throws RuntimeException |
||
309 | */ |
||
310 | public function sendRequest(string $command, array $tokens = [], string &$tag = null) { |
||
311 | if (!$tag) { |
||
312 | $this->noun++; |
||
313 | $tag = 'TAG' . $this->noun; |
||
314 | } |
||
315 | |||
316 | $line = $tag . ' ' . $command; |
||
317 | |||
318 | foreach ($tokens as $token) { |
||
319 | if (is_array($token)) { |
||
320 | $this->write($line . ' ' . $token[0]); |
||
321 | if (!$this->assumedNextLine('+ ')) { |
||
322 | throw new RuntimeException('failed to send literal string'); |
||
323 | } |
||
324 | $line = $token[1]; |
||
325 | } else { |
||
326 | $line .= ' ' . $token; |
||
327 | } |
||
328 | } |
||
329 | $this->write($line); |
||
330 | } |
||
331 | |||
332 | /** |
||
333 | * Write data to the current stream |
||
334 | * @param string $data |
||
335 | * @return void |
||
336 | * @throws RuntimeException |
||
337 | */ |
||
338 | public function write(string $data) { |
||
339 | if ($this->debug) echo ">> ".$data ."\n"; |
||
340 | |||
341 | if (fwrite($this->stream, $data . "\r\n") === false) { |
||
342 | throw new RuntimeException('failed to write - connection closed?'); |
||
343 | } |
||
344 | } |
||
345 | |||
346 | /** |
||
347 | * Send a request and get response at once |
||
348 | * |
||
349 | * @param string $command |
||
350 | * @param array $tokens parameters as in sendRequest() |
||
351 | * @param bool $dontParse if true unparsed lines are returned instead of tokens |
||
352 | * |
||
353 | * @return array response as in readResponse() |
||
354 | * |
||
355 | * @throws ImapBadRequestException |
||
356 | * @throws ImapServerErrorException |
||
357 | * @throws RuntimeException |
||
358 | */ |
||
359 | public function requestAndResponse(string $command, array $tokens = [], bool $dontParse = false): array { |
||
360 | $this->sendRequest($command, $tokens, $tag); |
||
361 | |||
362 | return $this->readResponse($tag, $dontParse); |
||
363 | } |
||
364 | |||
365 | /** |
||
366 | * Escape one or more literals i.e. for sendRequest |
||
367 | * @param string|array $string the literal/-s |
||
368 | * |
||
369 | * @return string|array escape literals, literals with newline ar returned |
||
370 | * as array('{size}', 'string'); |
||
371 | */ |
||
372 | public function escapeString($string) { |
||
373 | if (func_num_args() < 2) { |
||
374 | if (str_contains($string, "\n")) { |
||
375 | return ['{' . strlen($string) . '}', $string]; |
||
376 | } else { |
||
377 | return '"' . str_replace(['\\', '"'], ['\\\\', '\\"'], $string) . '"'; |
||
378 | } |
||
379 | } |
||
380 | $result = []; |
||
381 | foreach (func_get_args() as $string) { |
||
382 | $result[] = $this->escapeString($string); |
||
383 | } |
||
384 | return $result; |
||
385 | } |
||
386 | |||
387 | /** |
||
388 | * Escape a list with literals or lists |
||
389 | * @param array $list list with literals or lists as PHP array |
||
390 | * |
||
391 | * @return string escaped list for imap |
||
392 | */ |
||
393 | public function escapeList(array $list): string { |
||
394 | $result = []; |
||
395 | foreach ($list as $v) { |
||
396 | if (!is_array($v)) { |
||
397 | $result[] = $v; |
||
398 | continue; |
||
399 | } |
||
400 | $result[] = $this->escapeList($v); |
||
401 | } |
||
402 | return '(' . implode(' ', $result) . ')'; |
||
403 | } |
||
404 | |||
405 | /** |
||
406 | * Login to a new session. |
||
407 | * |
||
408 | * @param string $user username |
||
409 | * @param string $password password |
||
410 | * |
||
411 | * @return array |
||
412 | * @throws AuthFailedException |
||
413 | * @throws ImapBadRequestException |
||
414 | * @throws ImapServerErrorException |
||
415 | */ |
||
416 | public function login(string $user, string $password): array { |
||
417 | try { |
||
418 | $command = 'LOGIN'; |
||
419 | $params = $this->escapeString($user, $password); |
||
420 | |||
421 | return $this->requestAndResponse($command, $params, true); |
||
422 | } catch (RuntimeException $e) { |
||
423 | throw new AuthFailedException("failed to authenticate", 0, $e); |
||
424 | } |
||
425 | } |
||
426 | |||
427 | /** |
||
428 | * Authenticate your current IMAP session. |
||
429 | * @param string $user username |
||
430 | * @param string $token access token |
||
431 | * |
||
432 | * @return bool |
||
433 | * @throws AuthFailedException |
||
434 | */ |
||
435 | public function authenticate(string $user, string $token): bool { |
||
436 | try { |
||
437 | $authenticateParams = ['XOAUTH2', base64_encode("user=$user\1auth=Bearer $token\1\1")]; |
||
438 | $this->sendRequest('AUTHENTICATE', $authenticateParams); |
||
439 | |||
440 | while (true) { |
||
441 | $response = ""; |
||
442 | $is_plus = $this->readLine($response, '+', true); |
||
443 | if ($is_plus) { |
||
444 | // try to log the challenge somewhere where it can be found |
||
445 | error_log("got an extra server challenge: $response"); |
||
446 | // respond with an empty response. |
||
447 | $this->sendRequest(''); |
||
448 | } else { |
||
449 | if (preg_match('/^NO /i', $response) || |
||
450 | preg_match('/^BAD /i', $response)) { |
||
451 | error_log("got failure response: $response"); |
||
452 | return false; |
||
453 | } else if (preg_match("/^OK /i", $response)) { |
||
454 | return true; |
||
455 | } |
||
456 | } |
||
457 | } |
||
458 | } catch (RuntimeException $e) { |
||
459 | throw new AuthFailedException("failed to authenticate", 0, $e); |
||
460 | } |
||
461 | } |
||
462 | |||
463 | /** |
||
464 | * Logout of imap server |
||
465 | * |
||
466 | * @return array success |
||
467 | * |
||
468 | * @throws ImapBadRequestException |
||
469 | * @throws ImapServerErrorException |
||
470 | * @throws RuntimeException |
||
471 | */ |
||
472 | public function logout(): array { |
||
473 | if (!$this->stream) { |
||
474 | throw new RuntimeException('not connected'); |
||
475 | } |
||
476 | |||
477 | $result = $this->requestAndResponse('LOGOUT', [], true); |
||
478 | |||
479 | fclose($this->stream); |
||
480 | $this->stream = null; |
||
481 | $this->uid_cache = null; |
||
482 | |||
483 | return $result; |
||
484 | } |
||
485 | |||
486 | /** |
||
487 | * Check if the current session is connected |
||
488 | * |
||
489 | * @return bool |
||
490 | */ |
||
491 | public function connected(): bool { |
||
492 | return (boolean) $this->stream; |
||
493 | } |
||
494 | |||
495 | /** |
||
496 | * Get an array of available capabilities |
||
497 | * |
||
498 | * @return array list of capabilities |
||
499 | * |
||
500 | * @throws ImapBadRequestException |
||
501 | * @throws ImapServerErrorException |
||
502 | * @throws RuntimeException |
||
503 | */ |
||
504 | public function getCapabilities(): array { |
||
505 | $response = $this->requestAndResponse('CAPABILITY'); |
||
506 | |||
507 | if (!$response) return []; |
||
508 | |||
509 | $capabilities = []; |
||
510 | foreach ($response as $line) { |
||
511 | $capabilities = array_merge($capabilities, $line); |
||
512 | } |
||
513 | return $capabilities; |
||
514 | } |
||
515 | |||
516 | /** |
||
517 | * Examine and select have the same response. |
||
518 | * @param string $command can be 'EXAMINE' or 'SELECT' |
||
519 | * @param string $folder target folder |
||
520 | * |
||
521 | * @return bool|array |
||
522 | * @throws RuntimeException |
||
523 | */ |
||
524 | public function examineOrSelect(string $command = 'EXAMINE', string $folder = 'INBOX') { |
||
525 | $this->sendRequest($command, [$this->escapeString($folder)], $tag); |
||
526 | |||
527 | $result = []; |
||
528 | $tokens = null; // define $tokens variable before first use |
||
529 | while (!$this->readLine($tokens, $tag)) { |
||
530 | if ($tokens[0] == 'FLAGS') { |
||
531 | array_shift($tokens); |
||
532 | $result['flags'] = $tokens; |
||
533 | continue; |
||
534 | } |
||
535 | switch ($tokens[1]) { |
||
536 | case 'EXISTS': |
||
537 | case 'RECENT': |
||
538 | $result[strtolower($tokens[1])] = (int)$tokens[0]; |
||
539 | break; |
||
540 | case '[UIDVALIDITY': |
||
541 | $result['uidvalidity'] = (int)$tokens[2]; |
||
542 | break; |
||
543 | case '[UIDNEXT': |
||
544 | $result['uidnext'] = (int)$tokens[2]; |
||
545 | break; |
||
546 | case '[UNSEEN': |
||
547 | $result['unseen'] = (int)$tokens[2]; |
||
548 | break; |
||
549 | case '[NONEXISTENT]': |
||
550 | throw new RuntimeException("folder doesn't exist"); |
||
551 | default: |
||
552 | // ignore |
||
553 | break; |
||
554 | } |
||
555 | } |
||
556 | |||
557 | if ($tokens[0] != 'OK') { |
||
558 | return false; |
||
559 | } |
||
560 | return $result; |
||
561 | } |
||
562 | |||
563 | /** |
||
564 | * Change the current folder |
||
565 | * @param string $folder change to this folder |
||
566 | * |
||
567 | * @return bool|array see examineOrSelect() |
||
568 | * @throws RuntimeException |
||
569 | */ |
||
570 | public function selectFolder(string $folder = 'INBOX') { |
||
571 | $this->uid_cache = null; |
||
572 | |||
573 | return $this->examineOrSelect('SELECT', $folder); |
||
574 | } |
||
575 | |||
576 | /** |
||
577 | * Examine a given folder |
||
578 | * @param string $folder examine this folder |
||
579 | * |
||
580 | * @return bool|array see examineOrSelect() |
||
581 | * @throws RuntimeException |
||
582 | */ |
||
583 | public function examineFolder(string $folder = 'INBOX') { |
||
584 | return $this->examineOrSelect('EXAMINE', $folder); |
||
585 | } |
||
586 | |||
587 | /** |
||
588 | * Fetch one or more items of one or more messages |
||
589 | * @param string|array $items items to fetch [RFC822.HEADER, FLAGS, RFC822.TEXT, etc] |
||
590 | * @param int|array $from message for items or start message if $to !== null |
||
591 | * @param int|null $to if null only one message ($from) is fetched, else it's the |
||
592 | * last message, INF means last message available |
||
593 | * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use |
||
594 | * message numbers instead. |
||
595 | * |
||
596 | * @return string|array if only one item of one message is fetched it's returned as string |
||
597 | * if items of one message are fetched it's returned as (name => value) |
||
598 | * if one item of messages are fetched it's returned as (msgno => value) |
||
599 | * if items of messages are fetched it's returned as (msgno => (name => value)) |
||
600 | * @throws RuntimeException |
||
601 | */ |
||
602 | public function fetch($items, $from, $to = null, $uid = IMAP::ST_UID) { |
||
603 | if (is_array($from)) { |
||
604 | $set = implode(',', $from); |
||
605 | } elseif ($to === null) { |
||
606 | $set = (int)$from; |
||
607 | } elseif ($to === INF) { |
||
608 | $set = (int)$from . ':*'; |
||
609 | } else { |
||
610 | $set = (int)$from . ':' . (int)$to; |
||
611 | } |
||
612 | |||
613 | $items = (array)$items; |
||
614 | $itemList = $this->escapeList($items); |
||
615 | |||
616 | $this->sendRequest($this->buildUIDCommand("FETCH", $uid), [$set, $itemList], $tag); |
||
617 | $result = []; |
||
618 | $tokens = null; // define $tokens variable before first use |
||
619 | while (!$this->readLine($tokens, $tag)) { |
||
620 | // ignore other responses |
||
621 | if ($tokens[1] != 'FETCH') { |
||
622 | continue; |
||
623 | } |
||
624 | |||
625 | // find array key of UID value; try the last elements, or search for it |
||
626 | if ($uid) { |
||
627 | $count = count($tokens[2]); |
||
628 | if ($tokens[2][$count - 2] == 'UID') { |
||
629 | $uidKey = $count - 1; |
||
630 | } else if ($tokens[2][0] == 'UID') { |
||
631 | $uidKey = 1; |
||
632 | } else { |
||
633 | $found = array_search('UID', $tokens[2]); |
||
634 | if ($found === false || $found === -1) { |
||
635 | continue; |
||
636 | } |
||
637 | |||
638 | $uidKey = $found + 1; |
||
639 | } |
||
640 | } |
||
641 | |||
642 | // ignore other messages |
||
643 | if ($to === null && !is_array($from) && ($uid ? $tokens[2][$uidKey] != $from : $tokens[0] != $from)) { |
||
644 | continue; |
||
645 | } |
||
646 | |||
647 | // if we only want one item we return that one directly |
||
648 | if (count($items) == 1) { |
||
649 | if ($tokens[2][0] == $items[0]) { |
||
650 | $data = $tokens[2][1]; |
||
651 | } elseif ($uid && $tokens[2][2] == $items[0]) { |
||
652 | $data = $tokens[2][3]; |
||
653 | } else { |
||
654 | $expectedResponse = 0; |
||
655 | // maybe the server send an other field we didn't wanted |
||
656 | $count = count($tokens[2]); |
||
657 | // we start with 2, because 0 was already checked |
||
658 | for ($i = 2; $i < $count; $i += 2) { |
||
659 | if ($tokens[2][$i] != $items[0]) { |
||
660 | continue; |
||
661 | } |
||
662 | $data = $tokens[2][$i + 1]; |
||
663 | $expectedResponse = 1; |
||
664 | break; |
||
665 | } |
||
666 | if (!$expectedResponse) { |
||
667 | continue; |
||
668 | } |
||
669 | } |
||
670 | } else { |
||
671 | $data = []; |
||
672 | while (key($tokens[2]) !== null) { |
||
673 | $data[current($tokens[2])] = next($tokens[2]); |
||
674 | next($tokens[2]); |
||
675 | } |
||
676 | } |
||
677 | |||
678 | // if we want only one message we can ignore everything else and just return |
||
679 | if ($to === null && !is_array($from) && ($uid ? $tokens[2][$uidKey] == $from : $tokens[0] == $from)) { |
||
680 | // we still need to read all lines |
||
681 | while (!$this->readLine($tokens, $tag)) |
||
682 | |||
683 | return $data; |
||
684 | } |
||
685 | if ($uid) { |
||
686 | $result[$tokens[2][$uidKey]] = $data; |
||
687 | }else{ |
||
688 | $result[$tokens[0]] = $data; |
||
689 | } |
||
690 | } |
||
691 | |||
692 | if ($to === null && !is_array($from)) { |
||
693 | throw new RuntimeException('the single id was not found in response'); |
||
694 | } |
||
695 | |||
696 | return $result; |
||
697 | } |
||
698 | |||
699 | /** |
||
700 | * Fetch message headers |
||
701 | * @param array|int $uids |
||
702 | * @param string $rfc |
||
703 | * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use |
||
704 | * message numbers instead. |
||
705 | * |
||
706 | * @return array |
||
707 | * @throws RuntimeException |
||
708 | */ |
||
709 | public function content($uids, string $rfc = "RFC822", $uid = IMAP::ST_UID): array { |
||
710 | $result = $this->fetch(["$rfc.TEXT"], $uids, null, $uid); |
||
711 | return is_array($result) ? $result : []; |
||
712 | } |
||
713 | |||
714 | /** |
||
715 | * Fetch message headers |
||
716 | * @param array|int $uids |
||
717 | * @param string $rfc |
||
718 | * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use |
||
719 | * message numbers instead. |
||
720 | * |
||
721 | * @return array |
||
722 | * @throws RuntimeException |
||
723 | */ |
||
724 | public function headers($uids, string $rfc = "RFC822", $uid = IMAP::ST_UID): array{ |
||
725 | $result = $this->fetch(["$rfc.HEADER"], $uids, null, $uid); |
||
726 | return $result === "" ? [] : $result; |
||
727 | } |
||
728 | |||
729 | /** |
||
730 | * Fetch message flags |
||
731 | * @param array|int $uids |
||
732 | * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use |
||
733 | * message numbers instead. |
||
734 | * |
||
735 | * @return array |
||
736 | * @throws RuntimeException |
||
737 | */ |
||
738 | public function flags($uids, $uid = IMAP::ST_UID): array { |
||
739 | $result = $this->fetch(["FLAGS"], $uids, null, $uid); |
||
740 | return is_array($result) ? $result : []; |
||
741 | } |
||
742 | |||
743 | /** |
||
744 | * Get uid for a given id |
||
745 | * @param int|null $id message number |
||
746 | * |
||
747 | * @return array|string message number for given message or all messages as array |
||
748 | * @throws MessageNotFoundException |
||
749 | */ |
||
750 | public function getUid($id = null) { |
||
751 | if (!$this->enable_uid_cache || $this->uid_cache === null || ($this->uid_cache && count($this->uid_cache) <= 0)) { |
||
752 | try { |
||
753 | $this->setUidCache($this->fetch('UID', 1, INF)); // set cache for this folder |
||
754 | } catch (RuntimeException $e) {} |
||
755 | } |
||
756 | $uids = $this->uid_cache; |
||
757 | |||
758 | if ($id == null) { |
||
759 | return $uids; |
||
760 | } |
||
761 | |||
762 | foreach ($uids as $k => $v) { |
||
763 | if ($k == $id) { |
||
764 | return $v; |
||
765 | } |
||
766 | } |
||
767 | |||
768 | // clear uid cache and run method again |
||
769 | if ($this->enable_uid_cache && $this->uid_cache) { |
||
770 | $this->setUidCache(null); |
||
771 | return $this->getUid($id); |
||
772 | } |
||
773 | |||
774 | throw new MessageNotFoundException('unique id not found'); |
||
775 | } |
||
776 | |||
777 | /** |
||
778 | * Get a message number for a uid |
||
779 | * @param string $id uid |
||
780 | * |
||
781 | * @return int message number |
||
782 | * @throws MessageNotFoundException |
||
783 | */ |
||
784 | public function getMessageNumber(string $id): int { |
||
785 | $ids = $this->getUid(); |
||
786 | foreach ($ids as $k => $v) { |
||
787 | if ($v == $id) { |
||
788 | return (int)$k; |
||
789 | } |
||
790 | } |
||
791 | |||
792 | throw new MessageNotFoundException('message number not found'); |
||
793 | } |
||
794 | |||
795 | /** |
||
796 | * Get a list of available folders |
||
797 | * |
||
798 | * @param string $reference mailbox reference for list |
||
799 | * @param string $folder mailbox name match with wildcards |
||
800 | * |
||
801 | * @return array folders that matched $folder as array(name => array('delimiter' => .., 'flags' => ..)) |
||
802 | * |
||
803 | * @throws ImapBadRequestException |
||
804 | * @throws ImapServerErrorException |
||
805 | * @throws RuntimeException |
||
806 | */ |
||
807 | public function folders(string $reference = '', string $folder = '*'): array { |
||
808 | $result = []; |
||
809 | $list = $this->requestAndResponse('LIST', $this->escapeString($reference, $folder)); |
||
810 | |||
811 | if ($list[0] === true) { |
||
812 | return $result; |
||
813 | } |
||
814 | |||
815 | foreach ($list as $item) { |
||
816 | if (count($item) != 4 || $item[0] != 'LIST') { |
||
817 | continue; |
||
818 | } |
||
819 | $result[$item[3]] = ['delimiter' => $item[2], 'flags' => $item[1]]; |
||
820 | } |
||
821 | |||
822 | return $result; |
||
823 | } |
||
824 | |||
825 | /** |
||
826 | * Manage flags |
||
827 | * |
||
828 | * @param array $flags flags to set, add or remove - see $mode |
||
829 | * @param int $from message for items or start message if $to !== null |
||
830 | * @param null $to if null only one message ($from) is fetched, else it's the |
||
831 | * last message, INF means last message available |
||
832 | * @param null $mode '+' to add flags, '-' to remove flags, everything else sets the flags as given |
||
833 | * @param bool $silent if false the return values are the new flags for the wanted messages |
||
834 | * @param int $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use |
||
835 | * message numbers instead. |
||
836 | * @param null $item command used to store a flag |
||
837 | * |
||
838 | * @return array new flags if $silent is false, else true or false depending on success |
||
839 | * @throws ImapBadRequestException |
||
840 | * @throws ImapServerErrorException |
||
841 | * @throws RuntimeException |
||
842 | */ |
||
843 | public function store( |
||
844 | array $flags, int $from, $to = null, $mode = null, bool $silent = true, $uid = IMAP::ST_UID, $item = null |
||
845 | ): array { |
||
846 | $flags = $this->escapeList($flags); |
||
847 | $set = $this->buildSet($from, $to); |
||
848 | |||
849 | $command = $this->buildUIDCommand("STORE", $uid); |
||
850 | $item = ($mode == '-' ? "-" : "+").($item === null ? "FLAGS" : $item).($silent ? '.SILENT' : ""); |
||
851 | |||
852 | $response = $this->requestAndResponse($command, [$set, $item, $flags], $silent); |
||
853 | |||
854 | if ($silent) { |
||
855 | return $response; |
||
856 | } |
||
857 | |||
858 | $result = []; |
||
859 | foreach ($response as $token) { |
||
860 | if ($token[1] != 'FETCH' || $token[2][0] != 'FLAGS') { |
||
861 | continue; |
||
862 | } |
||
863 | $result[$token[0]] = $token[2][1]; |
||
864 | } |
||
865 | |||
866 | return $result; |
||
867 | } |
||
868 | |||
869 | /** |
||
870 | * Append a new message to given folder |
||
871 | * |
||
872 | * @param string $folder name of target folder |
||
873 | * @param string $message full message content |
||
874 | * @param null $flags flags for new message |
||
875 | * @param null $date date for new message |
||
876 | * |
||
877 | * @return array success |
||
878 | * |
||
879 | * @throws ImapBadRequestException |
||
880 | * @throws ImapServerErrorException |
||
881 | * @throws RuntimeException |
||
882 | */ |
||
883 | public function appendMessage(string $folder, string $message, $flags = null, $date = null): array { |
||
884 | $tokens = []; |
||
885 | $tokens[] = $this->escapeString($folder); |
||
886 | if ($flags !== null) { |
||
887 | $tokens[] = $this->escapeList($flags); |
||
888 | } |
||
889 | if ($date !== null) { |
||
890 | $tokens[] = $this->escapeString($date); |
||
891 | } |
||
892 | $tokens[] = $this->escapeString($message); |
||
893 | |||
894 | return $this->requestAndResponse('APPEND', $tokens, true); |
||
895 | } |
||
896 | |||
897 | /** |
||
898 | * Copy a message set from current folder to another folder |
||
899 | * |
||
900 | * @param string $folder destination folder |
||
901 | * @param $from |
||
902 | * @param null $to if null only one message ($from) is fetched, else it's the |
||
903 | * last message, INF means last message available |
||
904 | * @param int $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use |
||
905 | * message numbers instead. |
||
906 | * |
||
907 | * @return array success |
||
908 | * |
||
909 | * @throws ImapBadRequestException |
||
910 | * @throws ImapServerErrorException |
||
911 | * @throws RuntimeException |
||
912 | */ |
||
913 | public function copyMessage(string $folder, $from, $to = null, $uid = IMAP::ST_UID): array { |
||
914 | $set = $this->buildSet($from, $to); |
||
915 | $command = $this->buildUIDCommand("COPY", $uid); |
||
916 | |||
917 | return $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true); |
||
918 | } |
||
919 | |||
920 | /** |
||
921 | * Copy multiple messages to the target folder |
||
922 | * |
||
923 | * @param array $messages List of message identifiers |
||
924 | * @param string $folder Destination folder |
||
925 | * @param int $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use |
||
926 | * message numbers instead. |
||
927 | * |
||
928 | * @return array Tokens if operation successful, false if an error occurred |
||
929 | * |
||
930 | * @throws ImapBadRequestException |
||
931 | * @throws ImapServerErrorException |
||
932 | * @throws RuntimeException |
||
933 | */ |
||
934 | public function copyManyMessages(array $messages, string $folder, $uid = IMAP::ST_UID): array { |
||
935 | $command = $this->buildUIDCommand("COPY", $uid); |
||
936 | |||
937 | $set = implode(',', $messages); |
||
938 | $tokens = [$set, $this->escapeString($folder)]; |
||
939 | |||
940 | return $this->requestAndResponse($command, $tokens, true); |
||
941 | } |
||
942 | |||
943 | /** |
||
944 | * Move a message set from current folder to another folder |
||
945 | * |
||
946 | * @param string $folder destination folder |
||
947 | * @param $from |
||
948 | * @param null $to if null only one message ($from) is fetched, else it's the |
||
949 | * last message, INF means last message available |
||
950 | * @param int $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use |
||
951 | * message numbers instead. |
||
952 | * |
||
953 | * @return array success |
||
954 | * |
||
955 | * @throws ImapBadRequestException |
||
956 | * @throws ImapServerErrorException |
||
957 | * @throws RuntimeException |
||
958 | */ |
||
959 | public function moveMessage(string $folder, $from, $to = null, $uid = IMAP::ST_UID): array { |
||
960 | $set = $this->buildSet($from, $to); |
||
961 | $command = $this->buildUIDCommand("MOVE", $uid); |
||
962 | |||
963 | return $this->requestAndResponse($command, [$set, $this->escapeString($folder)], true); |
||
964 | } |
||
965 | |||
966 | /** |
||
967 | * Move multiple messages to the target folder |
||
968 | * |
||
969 | * @param array $messages List of message identifiers |
||
970 | * @param string $folder Destination folder |
||
971 | * @param int $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use |
||
972 | * message numbers instead. |
||
973 | * |
||
974 | * @return array success |
||
975 | * |
||
976 | * @throws ImapBadRequestException |
||
977 | * @throws ImapServerErrorException |
||
978 | * @throws RuntimeException |
||
979 | */ |
||
980 | public function moveManyMessages(array $messages, string $folder, $uid = IMAP::ST_UID): array { |
||
981 | $command = $this->buildUIDCommand("MOVE", $uid); |
||
982 | |||
983 | $set = implode(',', $messages); |
||
984 | $tokens = [$set, $this->escapeString($folder)]; |
||
985 | |||
986 | return $this->requestAndResponse($command, $tokens, true); |
||
987 | } |
||
988 | |||
989 | /** |
||
990 | * Exchange identification information |
||
991 | * Ref.: https://datatracker.ietf.org/doc/html/rfc2971 |
||
992 | * |
||
993 | * @param array|null $ids |
||
994 | * @return array |
||
995 | * |
||
996 | * @throws ImapBadRequestException |
||
997 | * @throws ImapServerErrorException |
||
998 | * @throws RuntimeException |
||
999 | */ |
||
1000 | public function ID($ids = null): array { |
||
1001 | $token = "NIL"; |
||
1002 | if (is_array($ids) && !empty($ids)) { |
||
1003 | $token = "("; |
||
1004 | foreach ($ids as $id) { |
||
1005 | $token .= '"'.$id.'" '; |
||
1006 | } |
||
1007 | $token = rtrim($token).")"; |
||
1008 | } |
||
1009 | |||
1010 | return $this->requestAndResponse("ID", [$token], true); |
||
1011 | } |
||
1012 | |||
1013 | /** |
||
1014 | * Create a new folder (and parent folders if needed) |
||
1015 | * |
||
1016 | * @param string $folder folder name |
||
1017 | * @return array success |
||
1018 | * |
||
1019 | * @throws ImapBadRequestException |
||
1020 | * @throws ImapServerErrorException |
||
1021 | * @throws RuntimeException |
||
1022 | */ |
||
1023 | public function createFolder(string $folder): array { |
||
1024 | return $this->requestAndResponse('CREATE', [$this->escapeString($folder)], true); |
||
1025 | } |
||
1026 | |||
1027 | /** |
||
1028 | * Rename an existing folder |
||
1029 | * |
||
1030 | * @param string $old old name |
||
1031 | * @param string $new new name |
||
1032 | * |
||
1033 | * @return array success |
||
1034 | * |
||
1035 | * @throws ImapBadRequestException |
||
1036 | * @throws ImapServerErrorException |
||
1037 | * @throws RuntimeException |
||
1038 | */ |
||
1039 | public function renameFolder(string $old, string $new): array { |
||
1040 | return $this->requestAndResponse('RENAME', $this->escapeString($old, $new), true); |
||
1041 | } |
||
1042 | |||
1043 | /** |
||
1044 | * Delete a folder |
||
1045 | * |
||
1046 | * @param string $folder folder name |
||
1047 | * @return array success |
||
1048 | * |
||
1049 | * @throws ImapBadRequestException |
||
1050 | * @throws ImapServerErrorException |
||
1051 | * @throws RuntimeException |
||
1052 | */ |
||
1053 | public function deleteFolder(string $folder): array { |
||
1054 | return $this->requestAndResponse('DELETE', [$this->escapeString($folder)], true); |
||
1055 | } |
||
1056 | |||
1057 | /** |
||
1058 | * Subscribe to a folder |
||
1059 | * |
||
1060 | * @param string $folder folder name |
||
1061 | * @return array success |
||
1062 | * |
||
1063 | * @throws ImapBadRequestException |
||
1064 | * @throws ImapServerErrorException |
||
1065 | * @throws RuntimeException |
||
1066 | */ |
||
1067 | public function subscribeFolder(string $folder): array { |
||
1068 | return $this->requestAndResponse('SUBSCRIBE', [$this->escapeString($folder)], true); |
||
1069 | } |
||
1070 | |||
1071 | /** |
||
1072 | * Unsubscribe from a folder |
||
1073 | * |
||
1074 | * @param string $folder folder name |
||
1075 | * @return array success |
||
1076 | * |
||
1077 | * @throws ImapBadRequestException |
||
1078 | * @throws ImapServerErrorException |
||
1079 | * @throws RuntimeException |
||
1080 | */ |
||
1081 | public function unsubscribeFolder(string $folder): array { |
||
1082 | return $this->requestAndResponse('UNSUBSCRIBE', [$this->escapeString($folder)], true); |
||
1083 | } |
||
1084 | |||
1085 | /** |
||
1086 | * Apply session saved changes to the server |
||
1087 | * |
||
1088 | * @return array success |
||
1089 | * @throws ImapBadRequestException |
||
1090 | * @throws ImapServerErrorException |
||
1091 | * @throws RuntimeException |
||
1092 | */ |
||
1093 | public function expunge(): array { |
||
1094 | return $this->requestAndResponse('EXPUNGE'); |
||
1095 | } |
||
1096 | |||
1097 | /** |
||
1098 | * Send noop command |
||
1099 | * |
||
1100 | * @return array success |
||
1101 | * @throws ImapBadRequestException |
||
1102 | * @throws ImapServerErrorException |
||
1103 | * @throws RuntimeException |
||
1104 | */ |
||
1105 | public function noop(): array { |
||
1107 | } |
||
1108 | |||
1109 | /** |
||
1110 | * Retrieve the quota level settings, and usage statics per mailbox |
||
1111 | * |
||
1112 | * @param $username |
||
1113 | * @return array |
||
1114 | * |
||
1115 | * @throws ImapBadRequestException |
||
1116 | * @throws ImapServerErrorException |
||
1117 | * @throws RuntimeException |
||
1118 | */ |
||
1119 | public function getQuota($username): array { |
||
1120 | $command = "GETQUOTA"; |
||
1121 | $params = ['"#user/' . $username . '"']; |
||
1122 | |||
1123 | return $this->requestAndResponse($command, $params); |
||
1124 | } |
||
1125 | |||
1126 | /** |
||
1127 | * Retrieve the quota settings per user |
||
1128 | * |
||
1129 | * @param string $quota_root |
||
1130 | * @return array |
||
1131 | * |
||
1132 | * @throws ImapBadRequestException |
||
1133 | * @throws ImapServerErrorException |
||
1134 | * @throws RuntimeException |
||
1135 | */ |
||
1136 | public function getQuotaRoot(string $quota_root = 'INBOX'): array { |
||
1137 | $command = "QUOTA"; |
||
1138 | $params = [$quota_root]; |
||
1139 | |||
1140 | return $this->requestAndResponse($command, $params); |
||
1141 | } |
||
1142 | |||
1143 | /** |
||
1144 | * Send idle command |
||
1145 | * |
||
1146 | * @throws RuntimeException |
||
1147 | */ |
||
1148 | public function idle() { |
||
1149 | $this->sendRequest("IDLE"); |
||
1150 | if (!$this->assumedNextLine('+ ')) { |
||
1151 | throw new RuntimeException('idle failed'); |
||
1152 | } |
||
1153 | } |
||
1154 | |||
1155 | /** |
||
1156 | * Send done command |
||
1157 | * @throws RuntimeException |
||
1158 | */ |
||
1159 | public function done(): bool { |
||
1165 | } |
||
1166 | |||
1167 | /** |
||
1168 | * Search for matching messages |
||
1169 | * |
||
1170 | * @param array $params |
||
1171 | * @param int $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use |
||
1172 | * message numbers instead. |
||
1173 | * |
||
1174 | * @return array message ids |
||
1175 | * @throws ImapBadRequestException |
||
1176 | * @throws ImapServerErrorException |
||
1177 | * @throws RuntimeException |
||
1178 | */ |
||
1179 | public function search(array $params, $uid = IMAP::ST_UID): array { |
||
1192 | } |
||
1193 | |||
1194 | /** |
||
1195 | * Get a message overview |
||
1196 | * @param string $sequence |
||
1197 | * @param int|string $uid set to IMAP::ST_UID or any string representing the UID - set to IMAP::ST_MSGN to use |
||
1198 | * message numbers instead. |
||
1199 | * |
||
1200 | * @return array |
||
1201 | * @throws RuntimeException |
||
1202 | * @throws MessageNotFoundException |
||
1203 | * @throws InvalidMessageDateException |
||
1204 | */ |
||
1205 | public function overview(string $sequence, $uid = IMAP::ST_UID): array { |
||
1206 | $result = []; |
||
1207 | list($from, $to) = explode(":", $sequence); |
||
1208 | |||
1209 | $uids = $this->getUid(); |
||
1210 | $ids = []; |
||
1211 | foreach ($uids as $msgn => $v) { |
||
1212 | $id = $uid ? $v : $msgn; |
||
1213 | if ( ($to >= $id && $from <= $id) || ($to === "*" && $from <= $id) ){ |
||
1214 | $ids[] = $id; |
||
1215 | } |
||
1216 | } |
||
1217 | $headers = $this->headers($ids, "RFC822", $uid); |
||
1218 | foreach ($headers as $id => $raw_header) { |
||
1219 | $result[$id] = (new Header($raw_header, false))->getAttributes(); |
||
1220 | } |
||
1221 | return $result; |
||
1222 | } |
||
1223 | |||
1224 | /** |
||
1225 | * Enable the debug mode |
||
1226 | */ |
||
1227 | public function enableDebug(){ |
||
1228 | $this->debug = true; |
||
1229 | } |
||
1230 | |||
1231 | /** |
||
1232 | * Disable the debug mode |
||
1233 | */ |
||
1234 | public function disableDebug(){ |
||
1235 | $this->debug = false; |
||
1236 | } |
||
1237 | |||
1238 | /** |
||
1239 | * Build a valid UID number set |
||
1240 | * @param $from |
||
1241 | * @param null $to |
||
1242 | * |
||
1243 | * @return int|string |
||
1244 | */ |
||
1245 | public function buildSet($from, $to = null) { |
||
1251 | } |
||
1252 | } |
||
1253 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.