. */ namespace Liuch\DmarcSrg\Mail; use Liuch\DmarcSrg\Core; use Liuch\DmarcSrg\ErrorHandler; use Liuch\DmarcSrg\Exception\SoftException; use Liuch\DmarcSrg\Exception\LogicException; use Liuch\DmarcSrg\Exception\MailboxException; class MailBox { private $conn; private $server; private $host; private $mbox; private $name; private $uname; private $passw; private $delim; private $attrs; private $expunge; private $options; public function __construct($params) { if (!is_array($params)) { throw new LogicException('Incorrect mailbox params'); } $this->conn = null; $this->uname = $params['username']; $this->passw = $params['password']; if (isset($params['name']) && is_string($params['name']) && strlen($params['name']) > 0) { $this->name = $params['name']; } else { $name = $this->uname; $pos = strpos($name, '@'); if ($pos !== false && $pos !== 0) { $name = substr($name, 0, $pos); } $this->name = $name; } $this->mbox = $params['mailbox']; $this->host = $params['host']; $flags = $params['encryption'] ?? ''; switch ($flags) { case 'ssl': default: $flags = '/ssl'; break; case 'none': $flags = '/notls'; break; case 'starttls': $flags = '/tls'; break; } if (isset($params['novalidate-cert']) && $params['novalidate-cert'] === true) { $flags .= '/novalidate-cert'; } $this->server = sprintf('{%s/imap%s}', $this->host, $flags); $this->expunge = false; $this->options = []; if (isset($params['auth_exclude'])) { $auth_exclude = $params['auth_exclude']; switch (gettype($auth_exclude)) { case 'string': $auth_exclude = [ $auth_exclude ]; break; case 'array': break; default: $auth_exclude = null; break; } if ($auth_exclude) { $this->options['DISABLE_AUTHENTICATOR'] = $auth_exclude; } } } public function __destruct() { if (extension_loaded('imap')) { $this->cleanup(); } } public function childMailbox(string $mailbox_name) { $this->ensureConnection(); try { $mb_list = imap_list( $this->conn, self::utf8ToMutf7($this->server), self::utf8ToMutf7($this->mbox) . $this->delim . self::utf8ToMutf7($mailbox_name) ); } catch (\ErrorException $e) { $mb_list = false; } $this->ensureErrorLog('imap_list'); if (!$mb_list) { return null; } $child = clone $this; $child->mbox .= $this->delim . $mailbox_name; $child->conn = null; $child->expunge = false; return $child; } public function name() { return $this->name; } public function host() { return $this->host; } public function mailbox() { return $this->mbox; } public function check() { try { $this->ensureConnection(); try { $res = imap_status( $this->conn, self::utf8ToMutf7($this->server . $this->mbox), SA_MESSAGES | SA_UNSEEN ); } catch (\ErrorException $e) { $res = false; } $error_message = $this->ensureErrorLog(); if (!$res) { throw new MailboxException($error_message ?? 'Failed to get the mail box status'); } if ($this->attrs & \LATT_NOSELECT) { throw new MailboxException('The resource is not a mailbox'); } $this->checkRights(); } catch (MailboxException $e) { return ErrorHandler::exceptionResult($e); } return [ 'error_code' => 0, 'message' => 'Successfully', 'status' => [ 'messages' => $res->messages, 'unseen' => $res->unseen ] ]; } public function search($criteria) { $this->ensureConnection(); try { $res = imap_search($this->conn, $criteria); } catch (\ErrorException $e) { $res = false; } $error_message = $this->ensureErrorLog('imap_search'); if ($res === false) { if (!$error_message) { return []; } throw new MailboxException( 'Failed to search email messages', -1, new \ErrorException($error_message) ); } return $res; } public function sort($criteria, $search_criteria, $reverse) { $this->ensureConnection(); try { $res = imap_sort($this->conn, $criteria, $reverse ? 1 : 0, SE_NOPREFETCH, $search_criteria); } catch (\ErrorException $e) { $res = false; } $error_message = $this->ensureErrorLog('imap_sort'); if ($res === false) { if (!$error_message) { return []; } throw new MailboxException( 'Failed to sort email messages', -1, new \ErrorException($error_message) ); } return $res; } public function message($number) { return new MailMessage($this->conn, $number); } public function ensureMailbox($mailbox_name) { $mbn = self::utf8ToMutf7($mailbox_name); $srv = self::utf8ToMutf7($this->server); $mbo = self::utf8ToMutf7($this->mbox); $this->ensureConnection(); try { $mb_list = imap_list($this->conn, $srv, $mbo . $this->delim . $mbn); } catch (\ErrorException $e) { $mb_list = false; } $error_message = $this->ensureErrorLog('imap_list'); if (empty($mb_list)) { if ($error_message) { throw new MailboxException( 'Failed to get the list of mailboxes', -1, new \ErrorException($error_message) ); } $new_mailbox = "{$srv}{$mbo}{$this->delim}{$mbn}"; try { $res = imap_createmailbox($this->conn, $new_mailbox); } catch (\ErrorException $e) { $res = false; } $error_message = $this->ensureErrorLog('imap_createmailbox'); if (!$res) { throw new MailboxException( 'Failed to create a new mailbox', -1, new \ErrorException($error_message ?? 'Unknown') ); } try { imap_subscribe($this->conn, $new_mailbox); } catch (\ErrorException $e) { } $this->ensureErrorLog('imap_subscribe'); } } public function moveMessage($number, $mailbox_name) { $this->ensureConnection(); $target = self::utf8ToMutf7($this->mbox) . $this->delim . self::utf8ToMutf7($mailbox_name); try { $res = imap_mail_move($this->conn, strval($number), $target); } catch (\ErrorException $e) { $res = false; } $error_message = $this->ensureErrorLog('imap_mail_move'); if (!$res) { throw new MailboxException( 'Failed to move a message', -1, new \ErrorException($error_message ?? 'Unknown') ); } $this->expunge = true; } public function deleteMessage($number) { $this->ensureConnection(); try { imap_delete($this->conn, strval($number)); } catch (\ErrorException $e) { } $this->ensureErrorLog('imap_delete'); $this->expunge = true; } public static function resetErrorStack() { imap_errors(); imap_alerts(); } private function ensureConnection() { if (is_null($this->conn)) { $error_message = null; $srv = self::utf8ToMutf7($this->server); try { $this->conn = imap_open( $srv, $this->uname, $this->passw, OP_HALFOPEN, 0, $this->options ); } catch (\ErrorException $e) { $this->conn = null; } if ($this->conn) { $mbx = self::utf8ToMutf7($this->mbox); try { $mb_list = imap_getmailboxes($this->conn, $srv, $mbx); } catch (\ErrorException $e) { $mb_list = null; } if ($mb_list && count($mb_list) === 1) { $this->delim = $mb_list[0]->delimiter ?? '/'; $this->attrs = $mb_list[0]->attributes ?? 0; try { if (imap_reopen($this->conn, $srv . $mbx)) { return; } } catch (\ErrorException $e) { } } else { $error_message = "Mailbox `{$this->mbox}` not found"; } } if (!$error_message) { $error_message = imap_last_error(); if (!$error_message) { $error_message = 'Cannot connect to the mail server'; } } Core::instance()->logger()->error("IMAP error: {$error_message}"); self::resetErrorStack(); if ($this->conn) { try { imap_close($this->conn); } catch (\ErrorException $e) { } $this->ensureErrorLog('imap_close'); } $this->conn = null; throw new MailboxException($error_message); } } private function ensureErrorLog(string $prefix = 'IMAP error') { if ($error_message = imap_last_error()) { self::resetErrorStack(); $error_message = "{$prefix}: {$error_message}"; Core::instance()->logger()->error($error_message); return $error_message; } return null; } private function checkRights(): void { if ($this->attrs & \LATT_NOINFERIORS) { throw new SoftException('The mailbox may not have any children mailboxes'); } if (!function_exists('imap_getacl')) { return; } $mbox = self::utf8ToMutf7($this->mbox); try { $acls = imap_getacl($this->conn, $mbox); } catch (\ErrorException $e) { // It's not possible to get the ACLs information $acls = false; } $this->ensureErrorLog('imap_getacl'); if ($acls !== false) { $needed_rights_map = [ 'l' => 'LOOKUP', 'r' => 'READ', 's' => 'WRITE-SEEN', 't' => 'WRITE-DELETE', 'k' => 'CREATE' ]; $result = []; $needed_rights = array_keys($needed_rights_map); foreach ([ "#{$this->uname}", '#authenticated', '#anyone' ] as $identifier) { if (isset($acls[$identifier])) { $rights = $acls[$identifier]; foreach ($needed_rights as $r) { if (!str_contains($rights, $r)) { $result[] = $needed_rights_map[$r]; } } break; } } if (count($result) > 0) { throw new SoftException( 'Not enough rights. Additionally, these rights are required: ' . implode(', ', $result) ); } } } /** * Deletes messages marked for deletion, if any, and closes the connection * * @return void */ private function cleanup(): void { self::resetErrorStack(); if (!is_null($this->conn)) { try { if ($this->expunge) { imap_expunge($this->conn); } } catch (\ErrorException $e) { } $this->ensureErrorLog('imap_expunge'); try { imap_close($this->conn); } catch (\ErrorException $e) { } $this->ensureErrorLog('imap_close'); } } /** * It's a replacement for the standard function imap_utf8_to_mutf7 * * @param string $s A UTF-8 encoded string * * @return string|false */ private static function utf8ToMutf7(string $s) { return mb_convert_encoding($s, 'UTF7-IMAP', 'UTF-8'); } }