Add in software files and templates

This commit is contained in:
2023-06-21 14:19:40 +01:00
parent f42fdb947c
commit 5228fc5e9f
143 changed files with 23175 additions and 2 deletions

View File

@@ -0,0 +1,110 @@
<?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\ReportFile\ReportFile;
use Liuch\DmarcSrg\Exception\SoftException;
class MailAttachment
{
private $conn;
private $filename;
private $bytes;
private $number;
private $mnumber;
private $encoding;
private $stream;
private $mime_type;
public function __construct($conn, $params)
{
$this->conn = $conn;
$this->filename = $params['filename'];
$this->bytes = $params['bytes'];
$this->number = $params['number'];
$this->mnumber = $params['mnumber'];
$this->encoding = $params['encoding'];
$this->stream = null;
$this->mime_type = null;
}
public function __destruct()
{
if (!is_null($this->stream) && get_resource_type($this->stream) == 'stream') {
fclose($this->stream);
}
}
public function mimeType()
{
if (is_null($this->mime_type)) {
$this->mime_type = ReportFile::getMimeType($this->filename, $this->datastream());
}
return $this->mime_type;
}
public function size()
{
return $this->bytes;
}
public function filename()
{
return $this->filename;
}
public function extension()
{
return pathinfo($this->filename, PATHINFO_EXTENSION);
}
public function datastream()
{
if (is_null($this->stream)) {
$this->stream = fopen('php://temp', 'r+');
fwrite($this->stream, $this->toString());
}
rewind($this->stream);
return $this->stream;
}
private function fetchBody()
{
return imap_fetchbody($this->conn, $this->mnumber, strval($this->number), FT_PEEK);
}
private function toString()
{
switch ($this->encoding) {
case ENC7BIT:
case ENC8BIT:
case ENCBINARY:
return $this->fetchBody();
case ENCBASE64:
return base64_decode($this->fetchBody());
case ENCQUOTEDPRINTABLE:
return imap_qprint($this->fetchBody());
}
throw new SoftException('Encoding failed: Unknown encoding');
}
}

View File

@@ -0,0 +1,142 @@
<?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;
/**
* The class is designed to easily create multipart/alternative message bodies.
*/
class MailBody
{
private $text = null;
private $html = null;
private $boundary = null;
/**
* Sets text content as a part of the message body
*
* @param array $text Text part of the message as an array of strings
*
* @return void
*/
public function setText(array &$text): void
{
$this->text = $text;
}
/**
* Sets html content as a part of the message body
*
* @param array $html Html part of the message as and array of strings
*
* @return void
*/
public function setHtml(array $html): void
{
$this->html = $html;
}
/**
* Return Content-Type header value for the whole message
*
* @return string
*/
public function contentType(): string
{
if ($this->boundary()) {
$ctype = 'multipart/alternative; boundary="' . $this->boundary() . '"';
} else {
if (!is_null($this->html)) {
$ctype = 'text/html';
} else {
$ctype = 'text/plain';
}
$ctype .= '; charset=utf-8';
}
return $ctype;
}
/**
* Returns all the message parts with required headers as an array of strings
*
* @return array
*/
public function content(): array
{
$content = [];
if ($this->text) {
$this->addBodyPart('text', $this->text, $content);
}
if ($this->html) {
$this->addBodyPart('html', $this->html, $content);
}
return $content;
}
/**
* Generates a boundary string of the message. If the body has only one part of the content
* it returns null
*
* @return string|null
*/
private function boundary()
{
if (!$this->boundary) {
if ($this->text && $this->html) {
$this->boundary = '==========' . sha1(uniqid()) . '=====';
}
}
return $this->boundary;
}
/**
* Adds the specified part of the content to the array passed as the third parameter
* with the required headers.
*
* @param string $type Type of the content to add
* @param array $part Part of the content to add
* @param array $content Where the data with headers should be added
*
* @return void
*/
private function addBodyPart(string $type, array &$part, array &$content): void
{
if ($this->boundary()) {
$content[] = '--' . $this->boundary();
switch ($type) {
case 'text':
$ctype = 'text/plain';
break;
case 'html':
$ctype = 'text/html';
break;
}
$content[] = 'Content-Type: ' . $ctype . '; charset=utf-8';
$content[] = 'Content-Transfer-Encoding: 7bit';
$content[] = '';
}
foreach ($part as $row) {
$content[] = $row;
}
unset($part);
}
}

View File

@@ -0,0 +1,468 @@
<?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');
}
}

View File

@@ -0,0 +1,137 @@
<?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\LogicException;
use Liuch\DmarcSrg\Exception\RuntimeException;
use Liuch\DmarcSrg\Exception\MailboxException;
class MailBoxes implements \Iterator
{
private $box_list;
private $index = 0;
public function __construct()
{
$mailboxes = Core::instance()->config('mailboxes');
$this->box_list = [];
if (is_array($mailboxes)) {
$cnt = count($mailboxes);
if ($cnt > 0) {
if (isset($mailboxes[0])) {
for ($i = 0; $i < $cnt; ++$i) {
$this->box_list[] = new MailBox($mailboxes[$i]);
}
} else {
$this->box_list[] = new MailBox($mailboxes);
}
}
}
}
public function count()
{
return count($this->box_list);
}
public function list()
{
$id = 0;
$res = [];
foreach ($this->box_list as &$mbox) {
$id += 1;
$res[] = [
'id' => $id,
'name' => $mbox->name(),
'host' => $mbox->host(),
'mailbox' => $mbox->mailbox()
];
}
unset($mbox);
return $res;
}
public function mailbox($id)
{
if (!is_int($id) || $id <= 0 || $id > count($this->box_list)) {
throw new LogicException("Incorrect mailbox Id: {$i}");
}
return $this->box_list[$id - 1];
}
public function check($id)
{
if ($id !== 0) {
return $this->mailbox($id)->check();
}
$results = [];
$err_cnt = 0;
$box_cnt = count($this->box_list);
for ($i = 0; $i < $box_cnt; ++$i) {
$r = $this->box_list[$i]->check();
if ($r['error_code'] !== 0) {
++$err_cnt;
}
$results[] = $r;
}
$res = [];
if ($err_cnt == 0) {
$res['error_code'] = 0;
$res['message'] = 'Success';
} else {
$res['error_code'] = -1;
$res['message'] = sprintf('%d of the %d mailboxes failed the check', $err_cnt, $box_cnt);
}
$res['results'] = $results;
return $res;
}
public function current(): object
{
return $this->box_list[$this->index];
}
public function key(): int
{
return $this->index;
}
public function next(): void
{
++$this->index;
}
public function rewind(): void
{
$this->index = 0;
}
public function valid(): bool
{
return isset($this->box_list[$this->index]);
}
}

View File

@@ -0,0 +1,154 @@
<?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\Exception\SoftException;
use Liuch\DmarcSrg\Exception\MailboxException;
class MailMessage
{
private $conn;
private $number;
private $attachment;
private $attachments_cnt;
public function __construct($conn, $number)
{
$this->conn = $conn;
$this->number = $number;
$this->attachment = null;
$this->attachments_cnt = -1;
}
public function overview()
{
$res = @imap_fetch_overview($this->conn, strval($this->number));
if (!isset($res[0])) {
if ($error_message = imap_last_error()) {
Core::instance()->logger()->error("imap_fetch_overview failed: {$error_message}");
}
MailBox::resetErrorStack();
return false;
}
return $res[0];
}
public function setSeen()
{
if (!@imap_setflag_full($this->conn, strval($this->number), '\\Seen')) {
if ($error_message = imap_last_error()) {
$error_message = '?';
}
MailBox::resetErrorStack();
Core::instance()->logger()->error("imap_setflag_full failed: {$error_message}");
throw new MailboxException("Failed to make a message seen: {$error_message}");
}
}
public function validate()
{
$this->ensureAttachment();
if ($this->attachments_cnt !== 1) {
throw new SoftException("Attachment count is not valid ({$this->attachments_cnt})");
}
$bytes = $this->attachment->size();
if ($bytes === -1) {
throw new SoftException("Failed to get attached file size. Wrong message format?");
}
if ($bytes < 50 || $bytes > 1 * 1024 * 1024) {
throw new SoftException("Attachment file size is not valid ({$bytes} bytes)");
}
$mime_type = $this->attachment->mimeType();
if (!in_array($mime_type, [ 'application/zip', 'application/gzip', 'application/x-gzip', 'text/xml' ])) {
throw new SoftException("Attachment file type is not valid ({$mime_type})");
}
}
public function attachment()
{
return $this->attachment;
}
private function ensureAttachment()
{
if ($this->attachments_cnt === -1) {
$structure = imap_fetchstructure($this->conn, $this->number);
if ($structure === false) {
throw new MailboxException('FetchStructure failed: ' . imap_last_error());
}
$this->attachments_cnt = 0;
$parts = isset($structure->parts) ? $structure->parts : [ $structure ];
foreach ($parts as $index => &$part) {
$att_part = $this->scanAttachmentPart($part, $index + 1);
if ($att_part) {
++$this->attachments_cnt;
if (!$this->attachment) {
$this->attachment = new MailAttachment($this->conn, $att_part);
}
}
}
unset($part);
}
}
private function scanAttachmentPart(&$part, $number)
{
$filename = null;
if ($part->ifdparameters) {
$filename = $this->getAttribute($part->dparameters, 'filename');
}
if (empty($filename) && $part->ifparameters) {
$filename = $this->getAttribute($part->parameters, 'name');
}
if (empty($filename)) {
return null;
}
return [
'filename' => imap_utf8($filename),
'bytes' => isset($part->bytes) ? $part->bytes : -1,
'number' => $number,
'mnumber' => $this->number,
'encoding' => $part->encoding
];
}
private function getAttribute(&$params, $name)
{
// need to check all objects as imap_fetchstructure
// returns multiple objects with the same attribute name,
// but first entry contains a truncated value
$value = null;
foreach ($params as &$obj) {
if (strcasecmp($obj->attribute, $name) === 0) {
$value = $obj->value;
}
}
return $value;
}
}