draft mail handler

This commit is contained in:
Dennis Eichhorn 2020-12-12 18:23:03 +01:00
parent 0cfe3a45bd
commit 77b26ad788
28 changed files with 4597 additions and 2354 deletions

View File

@ -0,0 +1,38 @@
<?php
/**
* Orange Management
*
* PHP Version 8.0
*
* @package phpOMS\Message\Mail
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link https://orange-management.org
*/
declare(strict_types=1);
namespace phpOMS\Message\Mail;
use phpOMS\Stdlib\Base\Enum;
/**
* Dsn notification types enum.
*
* @package phpOMS\Message\Mail
* @license OMS License 1.0
* @link https://orange-management.org
* @since 1.0.0
*/
abstract class DsnNotificationType extends Enum
{
public const NONE = '';
public const NEVER = 'NEVER';
public const SUCCESS = 'SUCCESS';
public const FAILURE = 'FAILURE';
public const DELAY = 'DELAY';
}

2401
Message/Mail/Email.php Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,669 +0,0 @@
<?php
/**
* Orange Management
*
* PHP Version 8.0
*
* @package phpOMS\Message\Mail
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link https://orange-management.org
*
* Extended based on:
* GLGPL 2.1 License
* © 2012 - 2015 Marcus Bointon, 2010 - 2012 Jim Jagielski, 2004 - 2009 Andy Prevost
* © PHPMailer
*/
declare(strict_types=1);
namespace phpOMS\Message\Mail;
/**
* Mail class.
*
* @package phpOMS\Message\Mail
* @license OMS License 1.0
* @link https://orange-management.org
* @since 1.0.0
*
* @todo Orange-Management/phpOMS#34
* Implement!!!
*/
abstract class EmailAbstract
{
/**
* Host.
*
* @var string
* @since 1.0.0
*/
protected string $host = '';
/**
* Port.
*
* @var int
* @since 1.0.0
*/
protected int $port = 25;
/**
* Use ssl.
*
* @var bool
* @since 1.0.0
*/
protected bool $ssl = false;
/**
* Mailbox base.
*
* @var string
* @since 1.0.0
*/
protected string $mailbox = '';
/**
* Timeout.
*
* @var int
* @since 1.0.0
*/
protected int $timeout = 30;
/**
* Connection.
*
* @var mixed
* @since 1.0.0
*/
protected $con = null;
/**
* Submit type/software
*
* @var string
* @since 1.0.0
*/
protected string $submitType = SubmitType::SMTP;
/**
* Sendmail path if submit type is not smtp or mail
*
* @var string
* @since 1.0.0
*/
protected string $sendmailPath = '';
/**
* End of line
*
* @var string
* @since 1.0.0
*/
protected string $endOfLine = "\r\n";
/**
* OAuth
*
* @var OAuth
* @since 1.0.0
*/
protected $oauth = null;
/**
* Construct
*
* @param string $host Host
* @param int $port Host port
* @param int $timeout Timeout
* @param bool $ssl Use ssl
*
* @since 1.0.0
*/
public function __construct(string $host = 'localhost', int $port = 25, int $timeout = 30, bool $ssl = false)
{
$this->host = $host;
$this->port = $port;
$this->timeout = $timeout;
$this->ssl = $ssl;
\imap_timeout(\IMAP_OPENTIMEOUT, $timeout);
\imap_timeout(\IMAP_READTIMEOUT, $timeout);
\imap_timeout(\IMAP_WRITETIMEOUT, $timeout);
\imap_timeout(\IMAP_CLOSETIMEOUT, $timeout);
}
/**
* Decode
*
* @param string $content Content to decode
* @param int $encoding Encoding type
*
* @return string
*
* @since 1.0.0
*/
public static function decode(string $content, string $encoding)
{
if ($encoding === EncodingType::E_BASE64) {
return \imap_base64($content);
} elseif ($encoding === EncodingType::E_8BIT) {
return \imap_8bit($content);
}
return \imap_qprint($content);
}
/**
* Descrutor
*
* @since 1.0.0
*/
public function __destruct()
{
$this->disconnect();
}
/**
* Disconnect server
*
* @return void
*
* @since 1.0.0
*/
public function disconnect() : void
{
if ($this->con !== null) {
\imap_close($this->con);
$this->con = null;
}
}
/**
* Connect to server
*
* @param string $user Username
* @param string $pass Password
*
* @return bool
*
* @since 1.0.0
*/
public function connect(string $user = '', string $pass = '') : bool
{
$this->mailbox = \substr($this->mailbox, 0, -1) . ($this->ssl ? '/ssl/validate-cert' : '/novalidate-cert') . '}';
try {
$this->con = \imap_open($this->mailbox . 'INBOX', $user, $pass);
return true;
} catch (\Throwable $t) {
$this->con = null;
return false;
}
}
/**
* Test connection
*
* @return bool
*
* @since 1.0.0
*/
public function isConnected() : bool
{
return $this->con === null ? false : \imap_ping($this->con);
}
/**
* Set submit type/software
*
* @param string $submitType Submit type/software
*
* @return void
*
* @since 1.0.0
*/
public function setSubmitType(string $submitType) : void
{
$this->submitType = $submitType;
if ($this->submitType === SubmitType::SMTP
|| $this->submitType === SubmitType::MAIL
) {
$this->endOfLine = $this->submitType === SubmitType::SMTP || !\stripos(\PHP_OS, 'WIN') === 0 ? \PHP_EOL : "\r\n";
} elseif ($this->submitType === SubmitType::SENDMAIL) {
$this->endOfLine = \PHP_EOL;
$path = \ini_get('sendmail_path');
$this->sendmailPath = \stripos($path, 'sendmail') === false ? '/usr/sbin/sendmail' : $path;
} elseif ($this->submitType === SubmitType::QMAIL) {
$this->endOfLine = \PHP_EOL;
$path = \ini_get('sendmail_path');
$this->sendmailPath = \stripos($path, 'qmail') === false ? '/var/qmail/bin/qmail-inject' : $path;
}
}
/**
* Send a Mail
*
* @param Mail $mail Mail to send
*
* @return bool
*
* @since 1.0.0
*/
public function send(Mail $mail) : bool
{
if (empty($mail->getTo())) {
return false;
}
$this->preSend($mail);
$this->postSend($mail);
return true;
}
private function preSend(Mail $mail) : bool
{
$this->endOfLine = $this->submitType === SubmitType::SMTP
|| ($this->submitType === SubmitType::MAIL && \stripos(\PHP_OS, 'WIN') === 0)
? "\r\n" : \PHP_EOL;
$this->setMessageType();
if ($this->submitType === SubmitType::MAIL) {
$this->header .= 'to: ' . $this->addrAppend('to', $this->to);
$this->header .= $this->headerLine('Subject', $this->encodeHeader($this->secureHeader($this->subject)));
}
return true;
}
private function postSend(Mail $mail) : bool
{
return true;
}
/**
* Get boxes.
*
* @param string $pattern Pattern for boxes
*
* @return array
*
* @since 1.0.0
*/
public function getBoxes(string $pattern = '*') : array
{
$list = \imap_list($this->con, $this->host, $pattern);
return $list === false ? [] : $list;
}
/**
* Get inbox quota.
*
* @return array
*
* @since 1.0.0
*/
public function getQuota() : array
{
$quota = [];
try {
$quota = \imap_get_quotaroot($this->con, "INBOX");
} finally {
return $quota === false ? [] : $quota;
}
}
/**
* Get email.
*
* @param string $id mail id
*
* @return Mail
*
* @since 1.0.0
*/
public function getEmail(string $id) : Mail
{
$mail = new Mail($id);
if ((int) $id > $this->countMessages()) {
return $mail;
}
$mail->setOverview(\imap_fetch_overview($this->con, $id));
$mail->setBody(\imap_fetchbody($this->con, (int) $id, '1.1'));
//$mail->setEncoding(\imap_fetchstructure($this->con, (int) $id));
return $mail;
}
/**
* Get all inbox messages.
*
* @return array
*
* @since 1.0.0
*/
public function getInboxAll() : array
{
return $this->getInboxOverview('ALL');
}
/**
* Get inbox overview.
*
* @param string $option Inbox option (imap_search creterias)
*
* @return array
*
* @since 1.0.0
*/
public function getInboxOverview(string $option = 'ALL') : array
{
$ids = \imap_search($this->con, $option, \SE_FREE, 'UTF-8');
return \is_array($ids) ? \imap_fetch_overview($this->con, \implode(',', $ids)) : [];
}
/**
* Get all new inbox messages.
*
* @return array
*
* @since 1.0.0
*/
public function getInboxNew() : array
{
return $this->getInboxOverview('NEW');
}
/**
* Get all inbox messages from a person.
*
* @param string $from Messages from
*
* @return array
*
* @since 1.0.0
*/
public function getInboxFrom(string $from) : array
{
return $this->getInboxOverview('FROM "' . $from . '"');
}
/**
* Get all inbox messages to a person.
*
* @param string $to Messages to
*
* @return array
*
* @since 1.0.0
*/
public function getInboxTo(string $to) : array
{
return $this->getInboxOverview('TO "' . $to . '"');
}
/**
* Get all inbox messages cc a person.
*
* @param string $cc Messages cc
*
* @return array
*
* @since 1.0.0
*/
public function getInboxCc(string $cc) : array
{
return $this->getInboxOverview('CC "' . $cc . '"');
}
/**
* Get all inbox messages bcc a person.
*
* @param string $bcc Messages bcc
*
* @return array
*
* @since 1.0.0
*/
public function getInboxBcc(string $bcc) : array
{
return $this->getInboxOverview('BCC "' . $bcc . '"');
}
/**
* Get all answered inbox messages.
*
* @return array
*
* @since 1.0.0
*/
public function getInboxAnswered() : array
{
return $this->getInboxOverview('ANSWERED');
}
/**
* Get all inbox messages with a certain subject.
*
* @param string $subject Subject
*
* @return array
*
* @since 1.0.0
*/
public function getInboxSubject(string $subject) : array
{
return $this->getInboxOverview('SUBJECT "' . $subject . '"');
}
/**
* Get all inbox messages from a certain date onwards.
*
* @param \DateTime $since Messages since
*
* @return array
*
* @since 1.0.0
*/
public function getInboxSince(\DateTime $since) : array
{
return $this->getInboxOverview('SINCE "' . $since->format('d-M-Y') . '"');
}
/**
* Get all unseen inbox messages.
*
* @return array
*
* @since 1.0.0
*/
public function getInboxUnseen() : array
{
return $this->getInboxOverview('UNSEEN');
}
/**
* Get all seen inbox messages.
*
* @return array
*
* @since 1.0.0
*/
public function getInboxSeen() : array
{
return $this->getInboxOverview('SEEN');
}
/**
* Get all deleted inbox messages.
*
* @return array
*
* @since 1.0.0
*/
public function getInboxDeleted() : array
{
return $this->getInboxOverview('DELETED');
}
/**
* Get all inbox messages with text.
*
* @param string $text Text in message body
*
* @return array
*
* @since 1.0.0
*/
public function getInboxText(string $text) : array
{
return $this->getInboxOverview('TEXT "' . $text . '"');
}
/**
* Create mailbox
*
* @param string $mailbox Mailbox to create
*
* @return bool
*
* @since 1.0.0
*/
public function createMailbox(string $mailbox) : bool
{
return \imap_createmailbox($this->con, $mailbox);
}
/**
* Rename mailbox
*
* @param string $old Old mailbox name
* @param string $new New mailbox name
*
* @return bool
*
* @since 1.0.0
*/
public function renameMailbox(string $old, string $new) : bool
{
return \imap_renamemailbox($this->con, $old, $new);
}
/**
* Delete mailbox
*
* @param string $mailbox Mailbox to delete
*
* @return bool
*
* @since 1.0.0
*/
public function deleteMailbox(string $mailbox) : bool
{
return \imap_deletemailbox($this->con, $mailbox);
}
/**
* Check message to delete
*
* @param int $id Message id
*
* @return bool
*
* @since 1.0.0
*/
public function deleteMessage(int $id) : bool
{
return \imap_delete($this->con, $id);
}
/**
* Delete all marked messages
*
* @return bool
*
* @since 1.0.0
*/
public function deleteMarkedMessages() : bool
{
return \imap_expunge($this->con);
}
/**
* Check message to delete
*
* @param int $length Amount of message overview
* @param int $start Start index of the overview for pagination
*
* @return array
*
* @since 1.0.0
*/
public function getMessageOverview(int $length = 0, int $start = 1) : array
{
if ($length === 0) {
$info = \imap_check($this->con);
$length = $info->Nmsgs;
}
return \imap_fetch_overview($this->con, $start . ':' . ($length - 1 + $start), 0);
}
/**
* Count messages
*
* @return int
*
* @since 1.0.0
*/
public function countMessages() : int
{
return \imap_num_msg($this->con);
}
/**
* Get message header
*
* @param int $id Message id
*
* @return string
*
* @since 1.0.0
*/
public function getMessageHeader(int $id) : string
{
if ($id > $this->countMessages()) {
return '';
}
return \imap_fetchheader($this->con, $id);
}
/**
* Set the OAuth connection
*
* @param OAuth $oauth OAuth
*
* @return void
*
* @since 1.0.0
*/
public function setOAuth($oauth) : void
{
$this->oauth = $oauth;
}
}

View File

@ -19,7 +19,7 @@ use phpOMS\Stdlib\Base\Enum;
/**
* Encoding enum.
*
* @package phpOMS\Message\Mail
* @package phpOMS\Message\Mail
* @license OMS License 1.0
* @link https://orange-management.org
* @since 1.0.0

View File

@ -19,14 +19,16 @@ use phpOMS\Stdlib\Base\Enum;
/**
* Encryption enum.
*
* @package phpOMS\Message\Mail
* @package phpOMS\Message\Mail
* @license OMS License 1.0
* @link https://orange-management.org
* @since 1.0.0
*/
abstract class EncryptionType extends Enum
{
public const NONE = '';
public const TLS = 'tls';
public const SSL = 'ssl';
public const SMTPS = 'ssl';
}

View File

@ -19,7 +19,7 @@ use phpOMS\Stdlib\Base\Enum;
/**
* Submit enum.
*
* @package phpOMS\Message\Mail
* @package phpOMS\Message\Mail
* @license OMS License 1.0
* @link https://orange-management.org
* @since 1.0.0

View File

@ -19,7 +19,7 @@ use phpOMS\Stdlib\Base\Enum;
/**
* Calendar message types enum.
*
* @package phpOMS\Message\Mail
* @package phpOMS\Message\Mail
* @license OMS License 1.0
* @link https://orange-management.org
* @since 1.0.0

View File

@ -22,22 +22,142 @@ namespace phpOMS\Message\Mail;
* @link https://orange-management.org
* @since 1.0.0
*/
class Imap extends EmailAbstract
class Imap extends MailHandler implements MailBoxInterface
{
/**
* Connect to server
* Destructor.
*
* @param string $user Username
* @param string $pass Password
* @since 1.0.0
*/
public function __destruct()
{
$this->inboxClose();
parent::__destruct();
}
/**
* Connect to server
*
* @return bool
*
* @since 1.0.0
*/
public function connect(string $user = '', string $pass = '') : bool
public function connectInbox() : bool
{
$this->mailbox = '{' . $this->host . ':' . $this->port . '/imap}';
$this->mailbox = ($tmp = \imap_open(
'{'
. $this->host . ':' . $this->port . '/imap'
. ($this->encryption !== EncryptionType::NONE ? '/ssl' : '')
. '}',
$this->username, $this->password
) === false) ? null : $tmp;
}
return parent::connect($user, $pass);
public function getBoxes() : array
{
$list = \imap_list($this->mailbox, '{' . $this->host . ':' . $this->port . '}');
if (!\is_array($list)) {
return [];
}
foreach ($list as $key => $value) {
$list[$key] = \imap_utf7_decode($value);
}
return $list;
}
public function renameBox(string $old, string $new) : bool
{
return \imap_renamemailbox($this->mailbox, $old, \imap_utf7_encode($new));
}
public function deleteBox(string $box) : bool
{
return \imap_deletemailbox($this->mailbox, $box);
}
public function createBox(string $box) : bool
{
return \imap_createmailbox($this->mailbox, \imap_utf7_encode($box));
}
public function countMail(string $box) : int
{
if ($this->box !== $box) {
\imap_reopen($this->box, $box);
$this->box = $box;
}
return \imap_num_msg($this->mailbox);
}
public function getMailboxInfo(string $box) : object
{
if ($this->box !== $box) {
\imap_reopen($this->box, $box);
$this->box = $box;
}
return \imap_status($this->mailbox);
}
public function getRecentCount(string $box) : int
{
if ($this->box !== $box) {
\imap_reopen($this->box, $box);
$this->box = $box;
}
return \imap_num_recent($this->mailbox);
}
public function copyMail(string|array $messages, string $box) : bool
{
return \imap_mail_copy($this->mailbox, !\is_string($messages) ? \implode(',', $messages) : $messages, $box);
}
public function moveMail(string|array $messages, string $box) : bool
{
return \imap_mail_copy($this->mailbox, !\is_string($messages) ? \implode(',', $messages) : $messages, $box);
}
public function deleteMail(int $msg) : bool
{
return \imap_delete($this->mailbox, $msg);
}
public function getHeaders(string $box) : array
{
if ($this->box !== $box) {
\imap_reopen($this->box, $box);
$this->box = $box;
}
return \imap_headers($this->mailbox);
}
public function getHeaderInfo(int $msg) : object
{
return \imap_headerinfo($this->mailbox, $msg);
}
public function getMail(int $msg) : Email
{
return new Email();
}
/**
* Close mailbox
*
* @return void
*
* @since 1.0.0
*/
public function inboxClose() : void
{
if ($this->mailbox !== null) {
\imap_close($this->mailbox);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,29 @@
<?php
/**
* Orange Management
*
* PHP Version 8.0
*
* @package phpOMS\Uri
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link https://orange-management.org
*/
declare(strict_types=1);
namespace phpOMS\Message\Mail;
/**
* Message interface.
*
* @property string $subject Subject
*
* @package phpOMS\Uri
* @license OMS License 1.0
* @link https://orange-management.org
* @since 1.0.0
*/
interface MailBoxInterface
{
}

View File

@ -0,0 +1,615 @@
<?php
/**
* Orange Management
*
* PHP Version 8.0
*
* @package phpOMS\Message\Mail
* @license GLGPL 2.1 License
* @version 1.0.0
* @link https://orange-management.org
*
* Extended based on:
* GLGPL 2.1 License
* © 2012 - 2015 Marcus Bointon, 2010 - 2012 Jim Jagielski, 2004 - 2009 Andy Prevost
* © PHPMailer
*/
declare(strict_types=1);
namespace phpOMS\Message\Mail;
use phpOMS\Validation\Network\Email as EmailValidator;
use phpOMS\Utils\StringUtils;
use phpOMS\Validation\Network\Hostname;
use phpOMS\System\SystemUtils;
/**
* Mail class.
*
* @package phpOMS\Message\Mail
* @license GLGPL 2.1 License
* @link https://orange-management.org
* @since 1.0.0
*/
class MailHandler
{
/**
* The maximum line length allowed by RFC 2822 section 2.1.1.
*
* @var int
* @since 1.0.0
*/
const MAX_LINE_LENGTH = 998;
/**
* Mailer for sending message
*
* @var string
* @since 1.0.0
*/
public string $mailer = SubmitType::MAIL;
/**
* The path to the sendmail program.
*
* @var string
* @since 1.0.0
*/
public string $mailerTool = '';
/**
* Use sendmail MTA
*
* @var bool
* @since 1.0.0
*/
public bool $useMailOptions = true;
/**
* Hostname for Message-ID and HELO string.
*
* If empty this is automatically generated.
*
* @var string
* @since 1.0.0
*/
public string $hostname = '';
/**
* SMTP hosts.
* (e.g. "smtp1.example.com:25;smtp2.example.com").
*
* @var string
* @since 1.0.0
*/
public string $host = 'localhost';
/**
* The default port.
*
* @var int
* @since 1.0.0
*/
public int $port = 25;
/**
* The SMTP HELO/EHLO name
*
* @var string
* @since 1.0.0
*/
public string $helo = '';
/**
* SMTP encryption
*
* @var string
* @since 1.0.0
*/
public string $encryption = EncryptionType::NONE;
/**
* Use TLS automatically if the server supports it.
*
* @var bool
* @since 1.0.0
*/
public bool $useAutoTLS = true;
/**
* Use smtp auth
*
* @var bool
* @since 1.0.0
*/
public bool $useSMTPAuth = false;
/**
* Options passed when connecting via SMTP.
*
* @var array
* @since 1.0.0
*/
public array $smtpOptions = [];
/**
* SMTP username.
*
* @var string
* @since 1.0.0
*/
public string $username = '';
/**
* SMTP password.
*
* @var string
* @since 1.0.0
*/
public string $password = '';
/**
* SMTP auth type.
*
* @var string
* @since 1.0.0
*/
public string $authType = SMTPAuthType::NONE;
/**
* OAuth class.
*
* @var OAuth
* @since 1.0.0
*/
public mixed $oauth;
/**
* Server timeout
*
* @var int
* @since 1.0.0
*/
public int $timeout = 300;
/**
* Comma separated list of DSN notifications
*
* @var string
* @since 1.0.0
*/
public string $dsn = DsnNotificationType::NONE;
/**
* Keep connection alive.
*
* This requires a close call.
*
* @var bool
* @since 1.0.0
*/
public bool $keepAlive = false;
/**
* Use VERP
*
* @var bool
* @since 1.0.0
*/
public bool $useVerp = false;
/**
* An instance of the SMTP sender class.
*
* @var null|Smtp
* @since 1.0.0
*/
public ?Smtp $smtp = null;
/**
* An instance of the SMTP sender class.
*
* @var resource
* @since 1.0.0
*/
public mixed $mailbox = null;
/**
* SMTP RFC standard line ending
*
* @var string
* @since 1.0.0
*/
protected static string $LE = "\r\n";
/**
* Constructor.
*
* @param string $user Username
* @param string $pass Password
* @param int $port Port
* @param string $encryption Encryption type
*
* @since 1.0.0
*/
public function __construct(string $user = '', string $pass = '', int $port = 25, string $encryption = EncryptionType::NONE)
{
$this->username = $user;
$this->password = $pass;
$this->port = $port;
$this->encryption = $encryption;
}
/**
* Destructor.
*
* @since 1.0.0
*/
public function __destruct()
{
$this->smtpClose();
}
/**
* Set the mailer and the mailer tool
*
* @param string $mailer Mailer
*
* @return void
*
* @since 1.0.0
*/
public function setMailer(string $mailer) : void
{
$this->mailer = $mailer;
switch ($mailer) {
case SubmitType::MAIL:
case SubmitType::SMTP:
return;
case SubmitType::SENDMAIL:
$this->mailerTool = \stripos($sendmailPath = \ini_get('sendmail_path'), 'sendmail') === false
? '/usr/sbin/sendmail'
: $sendmailPath;
return;
case SubmitType::QMAIL:
$this->mailerTool = \stripos($sendmailPath = \ini_get('sendmail_path'), 'qmail') === false
? '/var/qmail/bin/qmail-inject'
: $sendmailPath;
return;
default:
return;
}
}
/**
* Send mail
*
* @param $mail Mail
*
* @return bool
*
* @since 1.0.0
*/
public function send(Email $mail) : bool
{
if (!$mail->preSend($this->mailer)) {
return false;
}
return $this->postSend($mail);
}
/**
* Send the mail
*
* @param Email $mail Mail
*
* @return bool
*
* @since 1.0.0
*/
private function postSend(Email $mail) : bool
{
switch ($this->mailer) {
case SubmitType::SENDMAIL:
case SubmitType::QMAIL:
return $this->sendmailSend($mail);
case SubmitType::SMTP:
return $this->smtpSend($mail);
case SubmitType::MAIL:
return $this->mailSend($mail);
default:
return false;
}
return false;
}
/**
* Send mail
*
* @param Email $mail Mail
*
* @return bool
*
* @since 1.0.0
*/
protected function sendmailSend(Email $mail) : bool
{
$header = \rtrim($mail->headerMime, " \r\n\t") . self::$LE . self::$LE;
// CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
if (!empty($mail->sender) && StringUtils::isShellSafe($mail->sender)) {
$mailerToolFmt = $this->mailer === SubmitType::QMAIL
? '%s -f%s'
: '%s -oi -f%s -t';
} elseif ($this->mailer === SubmitType::QMAIL) {
$mailerToolFmt = '%s';
} else {
$mailerToolFmt = '%s -oi -t';
}
$mailerTool = \sprintf($mailerToolFmt, \escapeshellcmd($this->sendmail), $mail->sender);
$con = \popen($mailerTool, 'w');
if ($con === false) {
return false;
}
\fwrite($con, $header);
\fwrite($con, $mail->bodyMime);
$result = \pclose($con);
if ($result !== 0) {
return false;
}
return true;
}
/**
* Send mail
*
* @param Email $mail Mail
*
* @return bool
*
* @since 1.0.0
*/
protected function mailSend(Email $mail) : bool
{
$header = \rtrim($mail->headerMime, " \r\n\t") . self::$LE . self::$LE;
$toArr = [];
foreach ($mail->to as $toaddr) {
$toArr[] = $mail->addrFormat($toaddr);
}
$to = \implode(', ', $toArr);
//This sets the SMTP envelope sender which gets turned into a return-path header by the receiver
// CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
$params = null;
if (!empty($mail->sender) && EmailValidator::isValid($mail->sender) && StringUtils::isShellSafe($mail->sender)) {
$params = \sprintf('-f%s', $mail->sender);
}
if (!empty($mail->sender) && EmailValidator::isValid($mail->sender)) {
$oldFrom = \ini_get('sendmail_from');
\ini_set('sendmail_from', $mail->sender);
}
$result = false;
$result = $this->mailPassthru($to, $mail, $header, $params);
if (isset($oldFrom)) {
\ini_set('sendmail_from', $oldFrom);
}
return $result;
}
/**
* Call mail() in a safe_mode-aware fashion.
*
* @param string $to To
* @param Email $mail Mail
* @param string $body Message Body
* @param string $header Additional Header(s)
* @param null|string $params Params
*
* @return bool
*
* @since 1.0.0
*/
private function mailPassthru(string $to, Email $mail, string $header, string $params = null) : bool
{
$subject = $mail->encodeHeader(\trim(\str_replace(["\r", "\n"], '', $mail->subject)));
return !$this->useMailOptions || $params === null
? \mail($to, $subject, $mail->body, $header)
: \mail($to, $subject, $mail->body, $header, $params);
}
/**
* Send mail
*
* @param Email $mail Mail
*
* @return bool
*
* @since 1.0.0
*/
protected function smtpSend(Email $mail) : bool
{
$header = \rtrim($mail->headerMime, " \r\n\t") . self::$LE . self::$LE;
if (!$this->smtpConnect($this->smtpOptions)) {
return false;
}
$smtpFrom = $mail->sender === '' ? $mail->from : $mail->sender;
if (!$this->smtp->mail($smtpFrom)) {
return false;
}
$badRcpt = [];
foreach ([$mail->to, $mail->cc, $mail->bcc] as $togroup) {
foreach ($togroup as $to) {
$badRcpt = $this->smtp->recipient($to[0], $this->dsn) ? $badRcpt + 1 : $badRcpt;
}
}
// Only send the DATA command if we have viable recipients
if ((\count($this->to) + \count($this->cc) + \count($this->bcc) > $badRcpt)
&& !$this->smtp->data($header . $mail->body, self::MAX_LINE_LENGTH)
) {
return false;
}
//$transactinoId = $this->smtp->getLastTransactionId();
if ($this->keepAlive) {
$this->smtp->reset();
} else {
$this->smtp->quit();
$this->smtp->close();
}
if (!empty($badRcpt)) {
return false;
}
return true;
}
/**
* Initiate a connection to an SMTP server.
*
* @param array $options An array of options compatible with stream_context_create()
*
* @return bool
*
* @since 1.0.0
*/
public function smtpConnect(array $options = null) : bool
{
if ($this->smtp === null) {
$this->smtp = new Smtp();
}
if ($this->smtp->isConnected()) {
return true;
}
if ($options === null) {
$options = $this->smtpOptions;
}
$this->smtp->timeout = $this->timeout;
$this->smtp->doVerp = $this->useVerp;
$hosts = \explode(';', $this->host);
foreach ($hosts as $hostentry) {
$hostinfo = [];
if (!\preg_match(
'/^(?:(ssl|tls):\/\/)?(.+?)(?::(\d+))?$/',
\trim($hostentry),
$hostinfo
)
) {
// Not a valid host entry
continue;
}
// $hostinfo[1]: optional ssl or tls prefix
// $hostinfo[2]: the hostname
// $hostinfo[3]: optional port number
//Check the host name is a valid name or IP address
if (!Hostname::isValid($hostinfo[2])) {
continue;
}
$prefix = '';
$secure = $this->encryption;
$tls = ($this->encryption === EncryptionType::TLS);
if ($hostinfo[1] === 'ssl' || ($hostinfo[1] === '' && $this->encryption === EncryptionType::SMTPS)) {
$prefix = 'ssl://';
$tls = false;
$secure = EncryptionType::SMTPS;
} elseif ('tls' === $hostinfo[1]) {
$tls = true;
$secure = EncryptionType::TLS;
}
//Do we need the OpenSSL extension?
$sslExt = defined('OPENSSL_ALGO_SHA256');
if (($secure === EncryptionType::TLS || $secure === EncryptionType::SMTPS)
&& !$sslExt
) {
return false;
}
$host = $hostinfo[2];
$port = $this->port;
if (isset($hostinfo[3])
&& \is_numeric($hostinfo[3])
&& $hostinfo[3] > 0 && $hostinfo[3] < 65536
) {
$port = (int) $hostinfo[3];
}
if ($this->smtp->connect($prefix . $host, $port, $this->timeout, $options)) {
$hello = !empty($this->helo) ? $this->helo : SystemUtils::getHostname();
$this->smtp->hello($hello);
$tls = $this->useAutoTLS && $sslExt && 'ssl' !== $secure && $this->smtp->getServerExt('STARTTLS')
? true : $tls;
$this->smtp->hello($hello);
//Automatically enable TLS encryption
$tls = $this->useAutoTLS && $sslExt && $secure !== EncryptionType::SMTPS && $this->smtp->getServerExt('STARTTLS')
? true : $tls;
if ($tls) {
if (!$this->smtp->startTLS()) {
return false;
}
// Resend EHLO
$this->smtp->hello($hello);
}
return !($this->useSMTPAuth
&& !$this->smtp->authenticate($this->username, $this->password, $this->authType, $this->oauth )
);
}
}
// If we get here, all connection attempts have failed
$this->smtp->close();
return false;
}
/**
* Close SMTP
*
* @return void
*
* @since 1.0.0
*/
public function smtpClose() : void
{
if ($this->smtp !== null && $this->smtp->isConnected()) {
$this->smtp->quit();
$this->smtp->close();
}
}
}

View File

@ -0,0 +1,40 @@
<?php
/**
* Orange Management
*
* PHP Version 8.0
*
* @package phpOMS\Uri
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link https://orange-management.org
*/
declare(strict_types=1);
namespace phpOMS\Message\Mail;
/**
* Message interface.
*
* @property string $subject Subject
*
* @package phpOMS\Uri
* @license OMS License 1.0
* @link https://orange-management.org
* @since 1.0.0
*/
interface MessageInterface
{
public function preSend(string $mailer) : bool;
public function addTo(string $address, string $name = '') : bool;
public function addCC(string $address, string $name = '') : bool;
public function addBCC(string $address, string $name = '') : bool;
public function addReplyTo(string $address, string $name = '') : bool;
public function setFrom(string $address, string $name = '') : bool;
}

View File

@ -22,29 +22,150 @@ namespace phpOMS\Message\Mail;
* @link https://orange-management.org
* @since 1.0.0
*/
class Pop3 extends EmailAbstract
class Pop3 extends MailHandler implements MailBoxInterface
{
/**
* Connect to server
* Current mailbox
*
* @param string $user Username
* @param string $pass Password
* @var string
* @sicne 1.0.0
*/
private string $box = '';
/**
* Destructor.
*
* @since 1.0.0
*/
public function __destruct()
{
$this->inboxClose();
parent::__destruct();
}
/**
* Connect to server
*
* @return bool
*
* @since 1.0.0
*/
public function connect(string $user = '', string $pass = '') : bool
public function connectInbox() : bool
{
$this->mailbox = '{' . $this->host . ':' . $this->port . '/pop3}';
$this->mailbox = ($tmp = \imap_open(
'{'
. $this->host . ':' . $this->port . '/pop3'
. ($this->encryption !== EncryptionType::NONE ? '/ssl' : '')
. '}',
$this->username, $this->password
) === false) ? null : $tmp;
}
return parent::connect();
public function getBoxes() : array
{
$list = \imap_list($this->mailbox, '{' . $this->host . ':' . $this->port . '}');
if (!\is_array($list)) {
return [];
}
foreach ($list as $key => $value) {
$list[$key] = \imap_utf7_decode($value);
}
return $list;
}
public function renameBox(string $old, string $new) : bool
{
return \imap_renamemailbox($this->mailbox, $old, \imap_utf7_encode($new));
}
public function deleteBox(string $box) : bool
{
return \imap_deletemailbox($this->mailbox, $box);
}
public function createBox(string $box) : bool
{
return \imap_createmailbox($this->mailbox, \imap_utf7_encode($box));
}
public function countMail(string $box) : int
{
if ($this->box !== $box) {
\imap_reopen($this->box, $box);
$this->box = $box;
}
return \imap_num_msg($this->mailbox);
}
public function getMailboxInfo(string $box) : object
{
if ($this->box !== $box) {
\imap_reopen($this->box, $box);
$this->box = $box;
}
return \imap_status($this->mailbox);
}
public function getRecentCount(string $box) : int
{
if ($this->box !== $box) {
\imap_reopen($this->box, $box);
$this->box = $box;
}
return \imap_num_recent($this->mailbox);
}
public function copyMail(string|array $messages, string $box) : bool
{
return \imap_mail_copy($this->mailbox, !\is_string($messages) ? \implode(',', $messages) : $messages, $box);
}
public function moveMail(string|array $messages, string $box) : bool
{
return \imap_mail_copy($this->mailbox, !\is_string($messages) ? \implode(',', $messages) : $messages, $box);
}
public function deleteMail(int $msg) : bool
{
return \imap_delete($this->mailbox, $msg);
}
public function getHeaders(string $box) : array
{
if ($this->box !== $box) {
\imap_reopen($this->box, $box);
$this->box = $box;
}
return \imap_headers($this->mailbox);
}
public function getHeaderInfo(int $msg) : object
{
return \imap_headerinfo($this->mailbox, $msg);
}
public function getMail(int $msg) : Email
{
return new Email();
}
/**
* {@inheritdoc}
* Close mailbox
*
* @return void
*
* @since 1.0.0
*/
public function send() : bool
public function inboxClose() : void
{
if ($this->mailbox !== null) {
\imap_close($this->mailbox);
}
}
}

View File

@ -0,0 +1,38 @@
<?php
/**
* Orange Management
*
* PHP Version 8.0
*
* @package phpOMS\Message\Mail
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link https://orange-management.org
*/
declare(strict_types=1);
namespace phpOMS\Message\Mail;
use phpOMS\Stdlib\Base\Enum;
/**
* SMTP auth types enum.
*
* @package phpOMS\Message\Mail
* @license OMS License 1.0
* @link https://orange-management.org
* @since 1.0.0
*/
abstract class SMTPAuthType extends Enum
{
public const NONE = '';
public const CRAM = 'CRAM-MD5';
public const LOGIN = 'LOGIN';
public const PLAIN = 'PLAIN';
public const XOAUTH2 = 'XOAUTH2';
}

905
Message/Mail/Smtp.php Normal file
View File

@ -0,0 +1,905 @@
<?php
/**
* Orange Management
*
* PHP Version 8.0
*
* @package phpOMS\Message\Mail
* @license GLGPL 2.1 License
* @version 1.0.0
* @link https://orange-management.org
*
* Extended based on:
* GLGPL 2.1 License
* © 2012 - 2015 Marcus Bointon, 2010 - 2012 Jim Jagielski, 2004 - 2009 Andy Prevost
* © PHPMailer
*/
declare(strict_types=1);
namespace phpOMS\Message\Mail;
/**
* Smtp mail class.
*
* @package phpOMS\Message\Mail
* @license GLGPL 2.1 License
* @link https://orange-management.org
* @since 1.0.0
*/
class Smtp
{
/**
* The maximum line length allowed
*
* @var int
* @since 1.0.0
*/
const MAX_REPLY_LENGTH = 512;
/**
* SMTP RFC standard line ending
*
* @var string
* @since 1.0.0
*/
protected static string $LE = "\r\n";
/**
* Whether to use VERP.
*
* @var bool
* @since 1.0.0
*/
public bool $doVerp = false;
/**
* The timeout value for connection, in seconds.
*
* @var int
* @since 1.0.0
*/
public int $timeout = 300;
/**
* How long to wait for commands to complete, in seconds.
*
* @var int
* @since 1.0.0
*/
public int $timeLimit = 300;
/**
* The last transaction ID issued in response to a DATA command,
* if one was detected.
*
* @var string
* @since 1.0.0
*/
protected string $lastSmtpTransactionId = '';
/**
* The socket for the server connection.
*
* @var ?resource
* @since 1.0.0
*/
protected $con;
/**
* The reply the server sent to us for HELO.
* If empty no HELO string has yet been received.
*
* @var string
* @since 1.0.0
*/
protected string $heloRply = '';
/**
* The set of SMTP extensions sent in reply to EHLO command.
*
* @var array
* @since 1.0.0
*/
protected array $serverCaps = [];
/**
* The most recent reply received from the server.
*
* @var string
* @since 1.0.0
*/
protected string $lastReply = '';
/**
* Connect to an SMTP server.
*
* @param string $host SMTP server IP or host name
* @param int $port The port number to connect to
* @param int $timeout How long to wait for the connection to open
* @param array $options An array of options for stream_context_create()
*
* @return bool
*
* @since 1.0.0
*/
public function connect(string $host, int $port = 25, int $timeout = 30, array $options = []) : bool
{
if ($this->isConnected()) {
return false;
}
$this->con = $this->getSMTPConnection($host, $port, $timeout, $options);
if ($this->con === null) {
return false;
}
$this->lastReply = $this->getLines();
$responseCode = (int) \substr($this->lastReply, 0, 3);
if ($responseCode === 220) {
return true;
}
if ($responseCode === 554) {
$this->quit();
}
$this->close();
return false;
}
/**
* Create connection to the SMTP server.
*
* @param string $host SMTP server IP or host name
* @param int $port The port number to connect to
* @param int $timeout How long to wait for the connection to open
* @param array $options An array of options for stream_context_create()
*
* @return null|resource
*
* @since 1.0.0
*/
protected function getSMTPConnection(string $host, int $port = 25, int $timeout = 30, array $options = []) : mixed
{
static $streamok;
if ($streamok === null) {
$streamok = \function_exists('stream_socket_client');
}
$errno = 0;
$errstr = '';
if ($streamok) {
$socketContext = \stream_context_create($options);
$connection = \stream_socket_client($host . ':' . $port, $errno, $errstr, $timeout, \STREAM_CLIENT_CONNECT, $socketContext);
} else {
//Fall back to fsockopen which should work in more places, but is missing some features
$connection = \fsockopen($host, $port, $errno, $errstr, $timeout);
}
if (!\is_resource($connection)) {
return null;
}
// SMTP server can take longer to respond, give longer timeout for first read
// Windows does not have support for this timeout function
if (\strpos(\PHP_OS, 'WIN') !== 0) {
$max = (int) \ini_get('max_execution_time');
if ($max !== 0 && $timeout > $max && \strpos(\ini_get('disable_functions'), 'set_time_limit') === false) {
\set_time_limit($timeout);
}
\stream_set_timeout($connection, $timeout, 0);
}
return $connection === false ? null : $connection;
}
/**
* Initiate a TLS (encrypted) session.
*
* @return bool
*
* @since 1.0.0
*/
public function startTLS() : bool
{
if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) {
return false;
}
$crypto_method = \STREAM_CRYPTO_METHOD_TLS_CLIENT;
if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
$crypto_method |= \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
$crypto_method |= \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
}
return (bool) \stream_socket_enable_crypto($this->con, true, $crypto_method);
}
/**
* Perform SMTP authentication.
* Must be run after hello().
*
* @param string $username The user name
* @param string $password The password
* @param string $authtype The auth type (CRAM-MD5, PLAIN, LOGIN, XOAUTH2)
* @param OAuth $oauth An optional OAuth instance for XOAUTH2 authentication
*
* @return bool
*
* @since 1.0.0
*/
public function authenticate(
string $username,
string $password,
string $authtype = '',
mixed $oauth = null
) : bool {
if (empty($this->serverCaps)) {
return false;
}
if (isset($this->serverCaps['EHLO'])) {
// SMTP extensions are available; try to find a proper authentication method
if (!isset($this->serverCaps['AUTH'])) {
// 'at this stage' means that auth may be allowed after the stage changes
// e.g. after STARTTLS
return false;
}
//If we have requested a specific auth type, check the server supports it before trying others
if ($authtype !== '' && !\in_array($authtype, $this->serverCaps['AUTH'], true)) {
$authtype = '';
}
if ($authtype !== '') {
//If no auth mechanism is specified, attempt to use these, in this order
//Try CRAM-MD5 first as it's more secure than the others
foreach (['CRAM-MD5', 'LOGIN', 'PLAIN', 'XOAUTH2'] as $method) {
if (\in_array($method, $this->serverCaps['AUTH'], true)) {
$authtype = $method;
break;
}
}
if ($authtype === '') {
return false;
}
}
if (!\in_array($authtype, $this->serverCaps['AUTH'], true)) {
return false;
}
} elseif ($authtype === '') {
$authtype = 'LOGIN';
}
switch ($authtype) {
case 'PLAIN':
// Start authentication
if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)
|| !$this->sendCommand('User & Password',
\base64_encode("\0" . $username . "\0" . $password),
235
)
) {
return false;
}
break;
case 'LOGIN':
// Start authentication
if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)
|| !$this->sendCommand('Username', \base64_encode($username), 334)
|| !$this->sendCommand('Password', \base64_encode($password), 235)
) {
return false;
}
break;
case 'CRAM-MD5':
// Start authentication
if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) {
return false;
}
$challenge = \base64_decode(\substr($this->lastReply, 4));
$response = $username . ' ' . $this->hmac($challenge, $password);
// send encoded credentials
return $this->sendCommand('Username', \base64_encode($response), 235);
case 'XOAUTH2':
//The OAuth instance must be set up prior to requesting auth.
if ($OAuth === null) {
return false;
}
$oauth = $OAuth->getOauth64();
if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) {
return false;
}
break;
default:
return false;
}
return true;
}
/**
* Calculate an MD5 HMAC hash.
*
* @param string $data The data to hash
* @param string $key The key to hash with
*
* @return string
*
* @since 1.0.0
*/
protected function hmac(string $data, string $key) : string
{
// RFC 2104 HMAC implementation for php.
// Creates an md5 HMAC.
// by Lance Rushing
$byteLen = 64;
if (strlen($key) > $byteLen) {
$key = \pack('H*', \md5($key));
}
$key = \str_pad($key, $byteLen, \chr(0x00));
$ipad = \str_pad('', $byteLen, \chr(0x36));
$opad = \str_pad('', $byteLen, \chr(0x5c));
$k_ipad = $key ^ $ipad;
$k_opad = $key ^ $opad;
return \md5($k_opad . \pack('H*', \md5($k_ipad . $data)));
}
/**
* Check connection state.
*
* @return bool
*
* @since 1.0.0
*/
public function isConnected() : bool
{
if (!\is_resource($this->con)) {
return false;
}
$status = \stream_get_meta_data($this->con);
if ($status['eof']) {
$this->close();
return false;
}
return true;
}
/**
* Close the socket and clean up the state of the class.
* Try to QUIT first!
*
* @return void
*
* @since 1.0.0
*/
public function close() : void
{
$this->serverCaps = [];
$this->heloRply = '';
if (\is_resource($this->con)) {
fclose($this->con);
$this->con = null;
}
}
/**
* Send an SMTP DATA command.
*
* @param string $msg_data Message data to send
*
* @return bool
*
* @since 1.0.0
*/
public function data($msg_data, int $maxLineLength = 998) : bool
{
if (!$this->sendCommand('DATA', 'DATA', 354)) {
return false;
}
/* The server is ready to accept data!
* According to rfc821 we should not send more than 1000 characters on a single line (including the LE)
*/
$lines = \explode("\n", \str_replace(["\r\n", "\r"], "\n", $msg_data));
/* To distinguish between a complete RFC822 message and a plain message body, we check if the first field
* of the first line (':' separated) does not contain a space then it _should_ be a header and we will
* process all lines before a blank line as headers.
*/
$field = \substr($lines[0], 0, \strpos($lines[0], ':'));
$inHeaders = (!empty($field) && \strpos($field, ' ') === false);
foreach ($lines as $line) {
$linesOut = [];
if ($inHeaders && $line === '') {
$inHeaders = false;
}
while (isset($line[$maxLineLength])) {
$pos = \strrpos(\substr($line, 0, $maxLineLength), ' ');
if (!$pos) {
$pos = $maxLineLength - 1;
$linesOut[] = \substr($line, 0, $pos);
$line = \substr($line, $pos);
} else {
$linesOut[] = \substr($line, 0, $pos);
$line = \substr($line, $pos + 1);
}
if ($inHeaders) {
$line = "\t" . $line;
}
}
$linesOut[] = $line;
foreach ($linesOut as $lineOut) {
if (!empty($lineOut) && $lineOut[0] === '.') {
$lineOut = '.' . $lineOut;
}
$this->clientSend($lineOut . self::$LE, 'DATA');
}
}
$tmpTimeLimit = $this->timeLimit;
$this->timeLimit *= 2;
$result = $this->sendCommand('DATA END', '.', 250);
$this->recordLastTransactionId();
$this->timeLimit = $tmpTimeLimit;
return $result;
}
/**
* Send an SMTP HELO or EHLO command.
*
* @param string $host The host name or IP to connect to
*
* @return bool
*
* @since 1.0.0
*/
public function hello(string $host = '') : bool
{
if ($this->sendHello('EHLO', $host)) {
return true;
}
if (\substr($this->heloRply, 0, 3) == '421') {
return false;
}
return $this->sendHello('HELO', $host);
}
/**
* Send an SMTP HELO or EHLO command.
*
* @param string $hello The HELO string
* @param string $host The hostname to say we are
*
* @return bool
*
* @since 1.0.0
*/
protected function sendHello(string $hello, string $host) : bool
{
$status = $this->sendCommand($hello, $hello . ' ' . $host, 250);
$this->heloRply = $this->lastReply;
if ($status) {
$this->parseHelloFields($hello);
} else {
$this->serverCaps = [];
}
return $status;
}
/**
* Parse a reply to HELO/EHLO command to discover server extensions.
*
* @param string $type `HELO` or `EHLO`
*
* @return void
*
* @since 1.0.0
*/
protected function parseHelloFields(string $type) : void
{
$this->serverCaps = [];
$lines = \explode("\n", $this->heloRply);
foreach ($lines as $n => $s) {
//First 4 chars contain response code followed by - or space
$s = \trim(\substr($s, 4));
if (empty($s)) {
continue;
}
$fields = \explode(' ', $s);
if (!empty($fields)) {
if (!$n) {
$name = $type;
$fields = $fields[0];
} else {
$name = \array_shift($fields);
switch ($name) {
case 'SIZE':
$fields = ($fields ? $fields[0] : 0);
break;
case 'AUTH':
if (!\is_array($fields)) {
$fields = [];
}
break;
default:
$fields = true;
}
}
$this->serverCaps[$name] = $fields;
}
}
}
/**
* Send an SMTP MAIL command.
*
* @param string $from Source address of this message
*
* @return bool
*
* @since 1.0.0
*/
public function mail(string $from) : bool
{
$useVerp = ($this->doVerp ? ' XVERP' : '');
return $this->sendCommand('MAIL FROM', 'MAIL FROM:<' . $from . '>' . $useVerp, 250);
}
/**
* Send an SMTP QUIT command.
*
* @return bool
*
* @since 1.0.0
*/
public function quit() : bool
{
$status = $this->sendCommand('QUIT', 'QUIT', 221);
if ($status) {
$this->close();
}
return $status;
}
/**
* Send an SMTP RCPT command.
*
* @param string $address The address the message is being sent to
* @param string $dsn Comma separated list of DSN notifications
*
* @return bool
*
* @since 1.0.0
*/
public function recipient(string $address, string $dsn = DsnNotificationType::NONE) : bool
{
if ($dsn === '') {
$rcpt = 'RCPT TO:<' . $address . '>';
} else {
$notify = [];
if (\strpos($dsn, 'NEVER') !== false) {
$notify[] = 'NEVER';
} else {
foreach (['SUCCESS', 'FAILURE', 'DELAY'] as $value) {
if (\strpos($dsn, $value) !== false) {
$notify[] = $value;
}
}
}
$rcpt = 'RCPT TO:<' . $address . '> NOTIFY=' . \implode(',', $notify);
}
return $this->sendCommand('RCPT TO', $rcpt, [250, 251]);
}
/**
* Send an SMTP RSET command.
*
* @return bool s
*
* @since 1.0.0
*/
public function reset() : bool
{
return $this->sendCommand('RSET', 'RSET', 250);
}
/**
* Send a command to an SMTP server and check its return code.
*
* @param string $command The command name - not sent to the server
* @param string $commandstring The actual command to send
* @param int|array $expect One or more expected integer success codes
*
* @return bool
*
* @since 1.0.0
*/
protected function sendCommand(string $command, string $commandstring, int|array $expect) : bool
{
if (!$this->isConnected()) {
return false;
}
if ((\strpos($commandstring, "\n") !== false)
|| (\strpos($commandstring, "\r") !== false)
) {
return false;
}
$this->clientSend($commandstring . self::$LE, $command);
$this->lastReply = $this->getLines();
$matches = [];
if (\preg_match('/^([\d]{3})[ -](?:([\d]\\.[\d]\\.[\d]{1,2}) )?/', $this->lastReply, $matches)) {
$code = (int) $matches[1];
$codeEx = \count($matches) > 2 ? $matches[2] : null;
// Cut off error code from each response line
$detail = \preg_replace(
"/{$code}[ -]" .
($codeEx ? \str_replace('.', '\\.', $codeEx) . ' ' : '') . '/m',
'',
$this->lastReply
);
} else {
// Fall back to simple parsing if regex fails
$code = (int) \substr($this->lastReply, 0, 3);
$detail = \substr($this->lastReply, 4);
}
if (!\in_array($code, (array) $expect, true)) {
return false;
}
return true;
}
/**
* Send an SMTP SAML command.
* Starts a mail transaction from the email address specified in $from.
*
* @param string $from The address the message is from
*
* @return bool
*
* @since 1.0.0
*/
public function sendAndMail(string $from) : bool
{
return $this->sendCommand('SAML', "SAML FROM:$from", 250);
}
/**
* Send an SMTP VRFY command.
*
* @param string $name The name to verify
*
* @return bool
*
* @since 1.0.0
*/
public function verify(string $name) : bool
{
return $this->sendCommand('VRFY', "VRFY $name", [250, 251]);
}
/**
* Send an SMTP NOOP command.
* Used to keep keep-alives alive.
*
* @return bool
*
* @since 1.0.0
*/
public function noop() : bool
{
return $this->sendCommand('NOOP', 'NOOP', 250);
}
/**
* Send raw data to the server.
*
* @param string $data The data to send
* @param string $command Optionally, the command this is part of, used only for controlling debug output
*
* @return int
*
* @since 1.0.0
*/
public function clientSend(string $data, string $command = '') : int
{
$result = \fwrite($this->con, $data);
return $result === false ? -1 : $result;
}
/**
* Get SMTP extensions available on the server.
*
* @return array
*
* @since 1.0.0
*/
public function getServerExtList() : array
{
return $this->serverCaps;
}
/**
* Get metadata about the SMTP server from its HELO/EHLO response.
*
* @param string $name Name of SMTP extension
*
* @return bool
*
* @since 1.0.0
*/
public function getServerExt(string $name) : bool
{
if (empty($this->serverCaps)) {
// HELO/EHLO has not been sent
return false;
}
if (!isset($this->serverCaps[$name])) {
if ($name === 'HELO') {
// Server name
//return $this->serverCaps['EHLO'];
}
return false;
}
return isset($this->serverCaps[$name]);
}
/**
* Get the last reply from the server.
*
* @return string
*
* @since 1.0.0
*/
public function getLastReply() : string
{
return $this->lastReply;
}
/**
* Read the SMTP server's response.
*
* @return string
*
* @since 1.0.0
*/
protected function getLines() : string
{
if (!\is_resource($this->con)) {
return '';
}
$data = '';
$endTime = 0;
\stream_set_timeout($this->con, $this->timeout);
if ($this->timeLimit > 0) {
$endTime = \time() + $this->timeLimit;
}
$selR = [$this->con];
$selW = null;
$tries = 0;
while (\is_resource($this->con) && !\feof($this->con)) {
$n = \stream_select($selR, $selW, $selW, $this->timeLimit);
if ($n === false) {
if ($tries < 3) {
++$tries;
continue;
} else {
break;
}
}
$str = \fgets($this->con, self::MAX_REPLY_LENGTH);
$data .= $str;
// If response is only 3 chars (not valid, but RFC5321 S4.2 says it must be handled),
// or 4th character is a space or a line break char, we are done reading, break the loop.
// String array access is a significant micro-optimisation over strlen
if (!isset($str[3]) || $str[3] === ' ' || $str[3] === "\r" || $str[3] === "\n") {
break;
}
$info = \stream_get_meta_data($this->con);
if ($info['timed_out']) {
break;
}
// Now check if reads took too long
if ($endTime && \time() > $endTime) {
break;
}
}
return $data;
}
/**
* Extract and return the ID of the last SMTP transaction
*
* @return string
*
* @since 1.0.0
*/
protected function recordLastTransactionId() : string
{
$reply = $this->getLastReply();
if ($reply === '') {
$this->lastSmtpTransactionId = '';
} else {
$this->lastSmtpTransactionId = '';
$patterns = SmtpTransactionPattern::getConstants();
foreach ($patterns as $pattern) {
$matches = [];
if (\preg_match($pattern, $reply, $matches)) {
$this->lastSmtpTransactionId = \trim($matches[1]);
break;
}
}
}
return $this->lastSmtpTransactionId;
}
/**
* Get the queue/transaction ID of the last SMTP transaction
*
* @return string
*
* @since 1.0.0
*/
public function getLastTransactionId() : string
{
return $this->lastSmtpTransactionId;
}
}

View File

@ -0,0 +1,42 @@
<?php
/**
* Orange Management
*
* PHP Version 8.0
*
* @package phpOMS\Message\Mail
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link https://orange-management.org
*/
declare(strict_types=1);
namespace phpOMS\Message\Mail;
use phpOMS\Stdlib\Base\Enum;
/**
* Transaction types enum.
*
* @package phpOMS\Message\Mail
* @license OMS License 1.0
* @link https://orange-management.org
* @since 1.0.0
*/
abstract class SmtpTransactionPattern extends Enum
{
public const EXIM = '/[\d]{3} OK id=(.*)/';
public const SENDMAIL = '/[\d]{3} 2.0.0 (.*) Message/';
public const POSTFIX = '/[\d]{3} 2.0.0 Ok: queued as (.*)/';
public const MICROSOFT_ESMTP = '/[0-9]{3} 2.[\d].0 (.*)@(?:.*) Queued mail for delivery/';
public const AMAZON_SES = '/[\d]{3} Ok (.*)/';
public const SENDGRID = '/[\d]{3} Ok: queued as (.*)/';
public const CAMPAIGNMONITOR = '/[\d]{3} 2.0.0 OK:([a-zA-Z\d]{48})/';
}

View File

@ -19,7 +19,7 @@ use phpOMS\Stdlib\Base\Enum;
/**
* Submit enum.
*
* @package phpOMS\Message\Mail
* @package phpOMS\Message\Mail
* @license OMS License 1.0
* @link https://orange-management.org
* @since 1.0.0

View File

@ -25,11 +25,11 @@ namespace phpOMS\Message;
interface MessageInterface
{
/**
* Gets the body of the message.
*
* @return string
*
* @since 1.0.0
*/
* Gets the body of the message.
*
* @return string
*
* @since 1.0.0
*/
public function getBody() : string;
}

View File

@ -191,4 +191,85 @@ final class FileUtils
return \intval($perm, 8);
}
/**
* Multi-byte-safe pathinfo.
*
* @param string $path Path
* @param null|int|string $options PATHINFO_* or specifier for the component
*
* @return string|array
*
* @since 1.0.0
*/
public static function mb_pathinfo(string $path, int|string $options = null) : string|array
{
$ret = ['dirname' => '', 'basename' => '', 'extension' => '', 'filename' => ''];
$pathinfo = [];
if (\preg_match('#^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^.\\\\/]+?)|))[\\\\/.]*$#m', $path, $pathinfo)) {
if (isset($pathinfo[1])) {
$ret['dirname'] = $pathinfo[1];
}
if (isset($pathinfo[2])) {
$ret['basename'] = $pathinfo[2];
}
if (isset($pathinfo[5])) {
$ret['extension'] = $pathinfo[5];
}
if (isset($pathinfo[3])) {
$ret['filename'] = $pathinfo[3];
}
}
switch ($options) {
case \PATHINFO_DIRNAME:
case 'dirname':
return $ret['dirname'];
case \PATHINFO_BASENAME:
case 'basename':
return $ret['basename'];
case \PATHINFO_EXTENSION:
case 'extension':
return $ret['extension'];
case \PATHINFO_FILENAME:
case 'filename':
return $ret['filename'];
default:
return $ret;
}
}
/**
* Check whether a file path is safe, accessible, and readable.
*
* @param string $path A relative or absolute path
*
* @return bool
*
* @since 1.0.0
*/
public static function isAccessible(string $path) : bool
{
$readable = \is_file($path);
if (\strpos($path, '\\\\') !== 0) {
$readable = $readable && \is_readable($path);
}
return self::isPermittedPath($path) && $readable;
}
/**
* Check whether a file path is of a permitted type.
*
* @param string $path Path
*
* @return bool
*
* @since 1.0.0
*/
public static function isPermittedPath(string $path) : bool
{
return !\preg_match('#^[a-z]+://#i', $path);
}
}

View File

@ -2009,4 +2009,22 @@ abstract class MimeType extends Enum
public const M_ZMM = 'application/vnd.handheld-entertainment+xml';
public const M_123 = 'application/vnd.lotus-1-2-3';
/**
* Get mime from file extension
*
* @param string $extension Extension
*
* @return string
*
* @since 1.0.0
*/
public static function extensionToMime(string $extension) : string
{
try {
return self::getByName('M_' . \strtoupper($extension)) ?? 'application/octet-stream';
} catch (\Throwable $t) {
return 'application/octet-stream';
}
}
}

View File

@ -126,4 +126,24 @@ final class SystemUtils
return (int) $cpuUsage;
}
/**
* Get the server hostname.
*
* @return string
*
* @since 1.0.0
*/
public static function getHostname() : string
{
if (isset($_SERVER['SERVER_NAME'])) {
return $_SERVER['SERVER_NAME'];
} elseif (($result = gethostname()) !== false) {
return $result;
} elseif (\php_uname('n') !== false) {
return \php_uname('n');
}
return 'localhost.localdomain';
}
}

View File

@ -399,4 +399,34 @@ final class StringUtils
return (int) $res;
}
/**
* Fix CVE-2016-10033 and CVE-2016-10045 by disallowing potentially unsafe shell characters.
*
* @param string $string String to check
*
* @return bool
*
* @since 1.0.0
*/
public static function isShellSafe(string $string)
{
if (\escapeshellcmd($string) !== $string
|| !\in_array(\escapeshellarg($string), ["'$string'", "\"$string\""])
) {
return false;
}
$length = \strlen($string);
for ($i = 0; $i < $length; ++$i) {
$c = $string[$i];
if (!\ctype_alnum($c) && \strpos('@_-.', $c) === false) {
return false;
}
}
return true;
}
}

View File

@ -41,6 +41,21 @@ abstract class Hostname extends ValidatorAbstract
*/
public static function isValid(mixed $value, array $constraints = null) : bool
{
return \filter_var(\gethostbyname($value), \FILTER_VALIDATE_IP) !== false;
//return \filter_var(\gethostbyname($value), \FILTER_VALIDATE_IP) !== false;
if (empty($value)
|| \strlen($value) > 256
|| !\preg_match('/^([a-zA-Z\d.-]*|\[[a-fA-F\d:]+])$/', $value)
) {
return false;
} elseif (\strlen($value) > 2 && \substr($value, 0, 1) === '[' && \substr($value, -1, 1) === ']') {
return \filter_var(\substr($value, 1, -1), \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6) !== false;
} elseif (\is_numeric(str_replace('.', '', $value))) {
return \filter_var($value, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4) !== false;
} elseif (\filter_var('http://' . $value, FILTER_VALIDATE_URL) !== false) {
return true;
}
return false;
}
}

View File

@ -1,67 +0,0 @@
<?php
/**
* Orange Management
*
* PHP Version 8.0
*
* @package tests
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link https://orange-management.org
*/
declare(strict_types=1);
namespace phpOMS\tests\Message\Mail;
use phpOMS\Message\Mail\Imap;
/**
* @internal
*/
class ImapTest extends \PHPUnit\Framework\TestCase
{
/**
* @covers phpOMS\Message\Mail\Imap
* @group framework
*/
public function testDefault() : void
{
self::markTestIncomplete();
return;
$email = new Imap(
$GLOBALS['CONFIG']['mail']['imap']['host'],
$GLOBALS['CONFIG']['mail']['imap']['port'],
30,
$GLOBALS['CONFIG']['mail']['imap']['ssl']
);
self::assertFalse($email->isConnected());
self::assertTrue($email->connect(
$GLOBALS['CONFIG']['mail']['imap']['user'],
$GLOBALS['CONFIG']['mail']['imap']['password']
));
self::assertTrue($email->isConnected());
self::assertEquals([], $email->getBoxes());
self::assertEquals([], $email->getQuota());
self::assertInstanceOf('\phpOMS\Message\Mail\Mail', $email->getEmail('1'));
self::assertEquals([], $email->getInboxAll());
self::assertEquals([], $email->getInboxOverview());
self::assertEquals([], $email->getInboxNew());
self::assertEquals([], $email->getInboxFrom(''));
self::assertEquals([], $email->getInboxTo(''));
self::assertEquals([], $email->getInboxCc(''));
self::assertEquals([], $email->getInboxBcc(''));
self::assertEquals([], $email->getInboxAnswered());
self::assertEquals([], $email->getInboxSubject(''));
self::assertEquals([], $email->getInboxSince(new \DateTime('now')));
self::assertEquals([], $email->getInboxUnseen());
self::assertEquals([], $email->getInboxSeen());
self::assertEquals([], $email->getInboxDeleted());
self::assertEquals([], $email->getInboxText(''));
self::assertEquals([], $email->getMessageOverview(1, 1));
self::assertEquals(0, $email->countMessages());
self::assertEquals('', $email->getMessageHeader(1));
}
}

View File

@ -0,0 +1,53 @@
<?php
/**
* Orange Management
*
* PHP Version 8.0
*
* @package tests
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link https://orange-management.org
*/
declare(strict_types=1);
namespace phpOMS\tests\Message;
require_once __DIR__ . '/../../Autoloader.php';
use phpOMS\Message\Mail\Mail;
use phpOMS\Message\Mail\MailHandler;
use phpOMS\Message\Mail\SubmitType;
use phpOMS\Message\Mail\Email;
use phpOMS\Message\Mail\Imap;
/**
* @testdox phpOMS\tests\Message\MailHandlerTest: Abstract mail handler
*
* @internal
*/
class MailHandlerTest extends \PHPUnit\Framework\TestCase
{
public function testSendTextWithMail() : void
{
$mailer = new MailHandler();
$mailer->setMailer(SubmitType::MAIL);
$mail = new Email();
$mail->setFrom('d.eichhorn@orange-management.org', 'Dennis Eichhorn');
$mail->addTo('coyle.maguire@googlemail.com', 'Donald Duck');
$mail->subject = 'Test email';
$mail->body = 'This is some content';
self::assertTrue($mailer->send($mail));
}
public function testReceiveMailWithImap() : void
{
$mailer = new Imap();
$mailer->connectInbox();
var_dump($mailer->getBoxes());
}
}

View File

@ -1,26 +0,0 @@
<?php
/**
* Orange Management
*
* PHP Version 8.0
*
* @package tests
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link https://orange-management.org
*/
declare(strict_types=1);
namespace phpOMS\tests\Message\Mail;
/**
* @internal
*/
class MailTest extends \PHPUnit\Framework\TestCase
{
public function testPlaceholder() : void
{
self::markTestIncomplete();
}
}

View File

@ -1,26 +0,0 @@
<?php
/**
* Orange Management
*
* PHP Version 8.0
*
* @package tests
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link https://orange-management.org
*/
declare(strict_types=1);
namespace phpOMS\tests\Message\Mail;
/**
* @internal
*/
class NntpTest extends \PHPUnit\Framework\TestCase
{
public function testPlaceholder() : void
{
self::markTestIncomplete();
}
}

View File

@ -1,26 +0,0 @@
<?php
/**
* Orange Management
*
* PHP Version 8.0
*
* @package tests
* @copyright Dennis Eichhorn
* @license OMS License 1.0
* @version 1.0.0
* @link https://orange-management.org
*/
declare(strict_types=1);
namespace phpOMS\tests\Message\Mail;
/**
* @internal
*/
class Pop3Test extends \PHPUnit\Framework\TestCase
{
public function testPlaceholder() : void
{
self::markTestIncomplete();
}
}