469 lines
14 KiB
PHP
469 lines
14 KiB
PHP
|
<?php
|
||
|
|
||
|
/**
|
||
|
* dmarc-srg - A php parser, viewer and summary report generator for incoming DMARC reports.
|
||
|
* Copyright (C) 2020 Aleksey Andreev (liuch)
|
||
|
*
|
||
|
* Available at:
|
||
|
* https://github.com/liuch/dmarc-srg
|
||
|
*
|
||
|
* This program is free software: you can redistribute it and/or modify it
|
||
|
* under the terms of the GNU General Public License as published by the Free
|
||
|
* Software Foundation, either version 3 of the License.
|
||
|
*
|
||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||
|
* more details.
|
||
|
*
|
||
|
* You should have received a copy of the GNU General Public License along with
|
||
|
* this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
*/
|
||
|
|
||
|
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');
|
||
|
}
|
||
|
}
|