Merge branch 'the-djmaze:master' into nc-theme

This commit is contained in:
Rene Hampölz 2023-07-27 11:22:16 +02:00 committed by GitHub
commit 47ed699525
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
881 changed files with 48156 additions and 36724 deletions

View file

@ -1,18 +1,89 @@
{
"imapHost": "localhost",
"imapPort": 143,
"imapSecure": 0,
"imapShortLogin": false,
"useSieve": false,
"sieveHost": "localhost",
"sievePort": 4190,
"sieveSecure": 0,
"smtpHost": "localhost",
"smtpPort": 25,
"smtpSecure": 0,
"smtpShortLogin": false,
"smtpAuth": true,
"smtpSetSender": false,
"smtpPhpMail": false,
"name": "*",
"IMAP": {
"host": "localhost",
"port": 143,
"type": 0,
"timeout": 300,
"shortLogin": false,
"sasl": [
"SCRAM-SHA3-512",
"SCRAM-SHA-512",
"SCRAM-SHA-256",
"SCRAM-SHA-1",
"PLAIN",
"LOGIN"
],
"ssl": {
"verify_peer": false,
"verify_peer_name": false,
"allow_self_signed": false,
"SNI_enabled": true,
"disable_compression": true,
"security_level": 1
},
"disable_list_status": false,
"disable_metadata": false,
"disable_move": false,
"disable_sort": false,
"disable_thread": false,
"disable_binary": false,
"use_expunge_all_on_delete": false,
"fast_simple_search": true,
"force_select": false,
"message_all_headers": false,
"message_list_limit": 0,
"search_filter": ""
},
"SMTP": {
"host": "localhost",
"port": 25,
"type": 0,
"timeout": 60,
"shortLogin": false,
"sasl": [
"SCRAM-SHA3-512",
"SCRAM-SHA-512",
"SCRAM-SHA-256",
"SCRAM-SHA-1",
"PLAIN",
"LOGIN"
],
"ssl": {
"verify_peer": false,
"verify_peer_name": false,
"allow_self_signed": false,
"SNI_enabled": true,
"disable_compression": true,
"security_level": 1
},
"useAuth": false,
"setSender": false,
"usePhpMail": false
},
"Sieve": {
"host": "localhost",
"port": 4190,
"type": 0,
"timeout": 10,
"shortLogin": false,
"sasl": [
"SCRAM-SHA3-512",
"SCRAM-SHA-512",
"SCRAM-SHA-256",
"SCRAM-SHA-1",
"PLAIN",
"LOGIN"
],
"ssl": {
"verify_peer": false,
"verify_peer_name": false,
"allow_self_signed": false,
"SNI_enabled": true,
"disable_compression": true,
"security_level": 1
},
"enabled": false
},
"whiteList": ""
}

View file

@ -1 +1 @@
outlook.com,qq.com,yahoo.com
outlook.com,qq.com,yahoo.com,gmail.com,hotmail.com

View file

@ -39,6 +39,11 @@ abstract class Collection extends \ArrayObject implements \JsonSerializable
}
}
public function keys() : array
{
return \array_keys($this->getArrayCopy());
}
public function Add($mItem, bool $bToTop = false) : self
{
$this->append($mItem, $bToTop);

View file

@ -23,8 +23,7 @@ abstract class DateTimeHelper
public static function GetUtcTimeZoneObject() : \DateTimeZone
{
static $oDateTimeZone = null;
if (null === $oDateTimeZone)
{
if (null === $oDateTimeZone) {
$oDateTimeZone = new \DateTimeZone('UTC');
}
return $oDateTimeZone;
@ -37,8 +36,7 @@ abstract class DateTimeHelper
public static function ParseRFC2822DateString(string $sDateTime) : int
{
$sDateTime = \trim($sDateTime);
if (empty($sDateTime))
{
if (empty($sDateTime)) {
return 0;
}
@ -54,13 +52,12 @@ abstract class DateTimeHelper
public static function ParseInternalDateString(string $sDateTime) : int
{
$sDateTime = \trim($sDateTime);
if (empty($sDateTime))
{
if (empty($sDateTime)) {
return 0;
}
if (\preg_match('/^[a-z]{2,4}, /i', $sDateTime)) // RFC2822 ~ "Thu, 10 Jun 2010 08:58:33 -0700 (PDT)"
{
// RFC2822 ~ "Thu, 10 Jun 2010 08:58:33 -0700 (PDT)"
if (\preg_match('/^[a-z]{2,4}, /i', $sDateTime)) {
return static::ParseRFC2822DateString($sDateTime);
}
@ -74,8 +71,7 @@ abstract class DateTimeHelper
public static function ParseDateStringType1(string $sDateTime) : int
{
$sDateTime = \trim($sDateTime);
if (empty($sDateTime))
{
if (empty($sDateTime)) {
return 0;
}

View file

@ -23,8 +23,7 @@ abstract class HtmlUtils
public static function BuildHtml(string $sHtml, array &$aFoundCids, array &$aFoundDataURL, array &$aFoundContentLocationUrls) : string
{
$bState = true;
if (\MailSo\Base\Utils::FunctionCallable('libxml_use_internal_errors'))
{
if (\MailSo\Base\Utils::FunctionCallable('libxml_use_internal_errors')) {
$bState = \libxml_use_internal_errors(true);
}
@ -86,13 +85,11 @@ abstract class HtmlUtils
$oDoc->normalizeDocument();
if (\MailSo\Base\Utils::FunctionCallable('libxml_clear_errors'))
{
if (\MailSo\Base\Utils::FunctionCallable('libxml_clear_errors')) {
\libxml_clear_errors();
}
if (\MailSo\Base\Utils::FunctionCallable('libxml_use_internal_errors'))
{
if (\MailSo\Base\Utils::FunctionCallable('libxml_use_internal_errors')) {
\libxml_use_internal_errors($bState);
}
@ -100,63 +97,62 @@ abstract class HtmlUtils
$oBody = $oDoc->getElementsByTagName('body')->item(0);
$aRemoveTags = array(
'svg', 'link', 'base', 'meta', 'title', 'x-script', 'script', 'bgsound', 'keygen', 'source',
'svg', 'link', 'base', 'meta', 'title', 'script', 'bgsound', 'keygen', 'source',
'object', 'embed', 'applet', 'mocha', 'iframe', 'frame', 'frameset', 'video', 'audio', 'area', 'map',
'head', 'style'
);
$aRemove = array();
$sIdRight = \md5(\microtime());
$aNodes = $oBody->getElementsByTagName('*');
foreach ($aNodes as /* @var $oElement \DOMElement */ $oElement)
{
$sTagNameLower = \strtolower($oElement->nodeName);
if (\in_array($sTagNameLower, $aRemoveTags))
{
$aRemove[] = $oElement;
continue;
}
// images
if ($oElement->hasAttribute('data-x-src-broken') || $oElement->hasAttribute('data-x-src-hidden')) {
$aRemove[] = $oElement;
continue;
}
if ($oElement->hasAttribute('data-x-src-cid')) {
$sCid = $oElement->getAttribute('data-x-src-cid');
$oElement->removeAttribute('data-x-src-cid');
if (!empty($sCid)) {
$aFoundCids[] = $sCid;
$oElement->setAttribute('src', 'cid:'.$sCid);
foreach ($aRemoveTags as $name) {
$aNodes = $oBody->getElementsByTagName($name);
foreach ($aNodes as /* @var $oElement \DOMElement */ $oElement) {
if (isset($oElement->parentNode)) {
@$oElement->parentNode->removeChild($oElement);
}
}
if ($oElement->hasAttribute('data-x-src')) {
$oElement->setAttribute('src', $oElement->getAttribute('data-x-src'));
$oElement->removeAttribute('data-x-src');
}
}
// style attribute images
$aCid = array();
if ($oElement->hasAttribute('data-x-style-url')) {
$aCid = \array_merge($aCid, \json_decode($oElement->getAttribute('data-x-style-url'), true));
$oElement->removeAttribute('data-x-style-url');
$xpath = new \DomXpath($oDoc);
foreach ($xpath->query('//*[@data-x-src-broken]') as $oElement) {
if (isset($oElement->parentNode)) {
@$oElement->parentNode->removeChild($oElement);
}
}
foreach ($xpath->query('//*[@data-x-src-hidden]') as $oElement) {
if (isset($oElement->parentNode)) {
@$oElement->parentNode->removeChild($oElement);
}
}
foreach ($xpath->query('//*[@data-x-src-cid]') as $oElement) {
$sCid = $oElement->getAttribute('data-x-src-cid');
$oElement->removeAttribute('data-x-src-cid');
if (!empty($sCid)) {
$aFoundCids[] = $sCid;
$oElement->setAttribute('src', 'cid:'.$sCid);
}
}
foreach ($xpath->query('//*[@data-x-src]') as $oElement) {
$oElement->setAttribute('src', $oElement->getAttribute('data-x-src'));
$oElement->removeAttribute('data-x-src');
}
// style attribute images
foreach ($xpath->query('//*[@data-x-style-url]') as $oElement) {
$aCid = \json_decode($oElement->getAttribute('data-x-style-url'), true);
$oElement->removeAttribute('data-x-style-url');
if ($aCid) {
foreach ($aCid as $sCidName => $sCid) {
$sCidName = \strtolower(\preg_replace('/([A-Z])/', '-\1', $sCidName));
if (\in_array($sCidName, array('background-image', 'list-style-image', 'content')))
{
if (\in_array($sCidName, array('background-image', 'list-style-image', 'content'))) {
$sStyles = $oElement->hasAttribute('style')
? \trim(\trim($oElement->getAttribute('style')), ';')
: '';
$sBack = $sCidName.':url(cid:'.$sCid.')';
$sStyles = \preg_replace('/'.\preg_quote($sCidName).'\\s*:\\s*[^;]+/i', $sBack, $sStyles);
if (false === \strpos($sStyles, $sBack))
{
if (false === \strpos($sStyles, $sBack)) {
$sStyles .= ";{$sBack}";
}
@ -165,35 +161,28 @@ abstract class HtmlUtils
}
}
}
}
// Remove all remaining data-* attributes
// Remove all remaining data-* attributes
foreach ($xpath->query('//*[@*[starts-with(name(), "data-")]]') as $oElement) {
$sTagNameLower = \strtolower($oElement->nodeName);
if ($oElement->hasAttributes()) {
foreach ($oElement->attributes as $oAttr) {
if ('data-' === \substr(\strtolower($oAttr->nodeName), 0, 5))
{
if ('data-' === \substr(\strtolower($oAttr->nodeName), 0, 5)) {
$oElement->removeAttribute($oAttr->nodeName);
}
}
}
if ('img' === $sTagNameLower)
{
$sSrc = $oElement->getAttribute('src');
if ('data:image/' === \strtolower(\substr($sSrc, 0, 11)))
{
$sHash = \md5($sSrc) . '@' . $sIdRight;
$aFoundDataURL[$sHash] = $sSrc;
$oElement->setAttribute('src', 'cid:'.$sHash);
}
}
}
foreach ($aRemove as /* @var $oElement \DOMElement */ $oElement)
{
if (isset($oElement->parentNode))
{
@$oElement->parentNode->removeChild($oElement);
$sIdRight = \md5(\microtime());
$aNodes = $oBody->getElementsByTagName('img');
foreach ($aNodes as /* @var $oElement \DOMElement */ $oElement) {
$sSrc = $oElement->getAttribute('src');
if ('data:image/' === \strtolower(\substr($sSrc, 0, 11))) {
$sHash = \md5($sSrc) . '@' . $sIdRight;
$aFoundDataURL[$sHash] = $sSrc;
$oElement->setAttribute('src', 'cid:'.$sHash);
}
}
@ -201,87 +190,6 @@ abstract class HtmlUtils
. $oDoc->saveHTML($oBody) . '</html>';
}
public static function ConvertPlainToHtml(string $sText, bool $bLinksWithTargetBlank = true) : string
{
$sText = \trim($sText);
if (!\strlen($sText))
{
return '';
}
$sText = (new \MailSo\Base\LinkFinder)
->Text($sText)
->UseDefaultWrappers($bLinksWithTargetBlank)
->CompileText()
;
$sText = \str_replace("\r", '', $sText);
$aText = \explode("\n", $sText);
unset($sText);
$bIn = false;
$bDo = true;
do
{
$bDo = false;
$aNextText = array();
foreach ($aText as $sTextLine)
{
$bStart = 0 === \strpos(\ltrim($sTextLine), '&gt;');
if ($bStart && !$bIn)
{
$bDo = true;
$bIn = true;
$aNextText[] = '<blockquote>';
$aNextText[] = \substr(\ltrim($sTextLine), 4);
}
else if (!$bStart && $bIn)
{
$bIn = false;
$aNextText[] = '</blockquote>';
$aNextText[] = $sTextLine;
}
else if ($bStart && $bIn)
{
$aNextText[] = \substr(\ltrim($sTextLine), 4);
}
else
{
$aNextText[] = $sTextLine;
}
}
if ($bIn)
{
$bIn = false;
$aNextText[] = '</blockquote>';
}
$aText = $aNextText;
}
while ($bDo);
$sText = \join("\n", $aText);
unset($aText);
$sText = \preg_replace('/[\n][ ]+/', "\n", $sText);
// $sText = \preg_replace('/[\s]+([\s])/', '\\1', $sText);
$sText = \preg_replace('/<blockquote>[\s]+/i', '<blockquote>', $sText);
$sText = \preg_replace('/[\s]+<\/blockquote>/i', '</blockquote>', $sText);
$sText = \preg_replace('/<\/blockquote>([\n]{0,2})<blockquote>/i', '\\1', $sText);
$sText = \preg_replace('/[\n]{3,}/', "\n\n", $sText);
$sText = \strtr($sText, array(
"\t" => "\xC2\xA0\xC2\xA0\xC2\xA0\xC2\xA0",
' ' => "\xC2\xA0\xC2\xA0"
));
return \nl2br($sText);
}
public static function ConvertHtmlToPlain(string $sText) : string
{
$sText = \MailSo\Base\Utils::StripSpaces($sText);

View file

@ -23,8 +23,7 @@ class Http
public static function SingletonInstance() : self
{
static $oInstance = null;
if (null === $oInstance)
{
if (null === $oInstance) {
$oInstance = new self;
}
@ -36,14 +35,14 @@ class Http
*
* @return mixed
*/
public function GetServer(string $sKey, $mDefault = null)
public static function GetServer(string $sKey, $mDefault = null)
{
return isset($_SERVER[$sKey]) ? $_SERVER[$sKey] : $mDefault;
}
public function GetMethod() : string
{
return $this->GetServer('REQUEST_METHOD', '');
return static::GetServer('REQUEST_METHOD', '');
}
public function IsPost() : bool
@ -65,9 +64,8 @@ class Http
public function IsLocalhost(string $sValueToCheck = '') : bool
{
if (empty($sValueToCheck))
{
$sValueToCheck = $this->GetServer('REMOTE_ADDR', '');
if (empty($sValueToCheck)) {
$sValueToCheck = static::GetServer('REMOTE_ADDR', '');
}
return $this->CheckLocalhost($sValueToCheck);
@ -76,29 +74,23 @@ class Http
public function GetRawBody() : string
{
static $sRawBody = null;
if (null === $sRawBody)
{
if (null === $sRawBody) {
$sBody = \file_get_contents('php://input');
$sRawBody = (false !== $sBody) ? $sBody : '';
}
return $sRawBody;
}
public function GetHeader(string $sHeader) : string
public static function GetHeader(string $sHeader) : string
{
$sServerKey = 'HTTP_'.\strtoupper(\str_replace('-', '_', $sHeader));
$sResultHeader = $this->GetServer($sServerKey, '');
if (0 === \strlen($sResultHeader) &&
\MailSo\Base\Utils::FunctionCallable('apache_request_headers'))
{
$sResultHeader = static::GetServer($sServerKey, '');
if (!\strlen($sResultHeader) && \MailSo\Base\Utils::FunctionCallable('apache_request_headers')) {
$sHeaders = \apache_request_headers();
if (isset($sHeaders[$sHeader]))
{
if (isset($sHeaders[$sHeader])) {
$sResultHeader = $sHeaders[$sHeader];
}
}
return $sResultHeader;
}
@ -109,155 +101,128 @@ class Http
public function IsSecure(bool $bCheckProxy = true) : bool
{
$sHttps = \strtolower($this->GetServer('HTTPS', ''));
if ('on' === $sHttps || ('' === $sHttps && '443' === (string) $this->GetServer('SERVER_PORT', '')))
{
return true;
}
if ($bCheckProxy && (
('https' === \strtolower($this->GetServer('HTTP_X_FORWARDED_PROTO', ''))) ||
('on' === \strtolower($this->GetServer('HTTP_X_FORWARDED_SSL', '')))
))
{
return true;
}
return false;
$sHttps = \strtolower(static::GetServer('HTTPS', ''));
return ('on' === $sHttps || ('' === $sHttps && '443' === (string) static::GetServer('SERVER_PORT', '')))
|| ($bCheckProxy && (
('https' === \strtolower(static::GetServer('HTTP_X_FORWARDED_PROTO', ''))) ||
('on' === \strtolower(static::GetServer('HTTP_X_FORWARDED_SSL', '')))
));
}
public function GetHost(bool $bWithRemoteUserData = false, bool $bWithoutWWW = true, bool $bWithoutPort = false) : string
public function GetHost(bool $bWithoutWWW = true, bool $bWithoutPort = false) : string
{
$sHost = $this->GetServer('HTTP_HOST', '');
if (!\strlen($sHost))
{
$sName = $this->GetServer('SERVER_NAME');
$iPort = (int) $this->GetServer('SERVER_PORT', 80);
$sHost = static::GetServer('HTTP_HOST', '');
if (!\strlen($sHost)) {
$sName = static::GetServer('SERVER_NAME', '');
$iPort = (int) static::GetServer('SERVER_PORT', 80);
$sHost = (\in_array($iPort, array(80, 433))) ? $sName : $sName.':'.$iPort;
}
if ($bWithoutWWW)
{
$sHost = 'www.' === \substr(\strtolower($sHost), 0, 4) ? \substr($sHost, 4) : $sHost;
if ($bWithoutWWW && 'www.' === \substr(\strtolower($sHost), 0, 4)) {
$sHost = \substr($sHost, 4);
}
if ($bWithRemoteUserData)
{
$sUser = \trim($this->GetServer('REMOTE_USER', ''));
$sHost = (\strlen($sUser) ? $sUser.'@' : '').$sHost;
}
if ($bWithoutPort)
{
$sHost = \preg_replace('/:\d+$/', '', $sHost);
}
return $sHost;
return $bWithoutPort ? \preg_replace('/:\d+$/', '', $sHost) : $sHost;
}
public function GetClientIp(bool $bCheckProxy = false) : string
{
$sIp = '';
if ($bCheckProxy && null !== $this->GetServer('HTTP_CLIENT_IP', null))
{
$sIp = $this->GetServer('HTTP_CLIENT_IP', '');
if ($bCheckProxy && null !== static::GetServer('HTTP_CLIENT_IP', null)) {
return static::GetServer('HTTP_CLIENT_IP', '');
}
else if ($bCheckProxy && null !== $this->GetServer('HTTP_X_FORWARDED_FOR', null))
{
$sIp = $this->GetServer('HTTP_X_FORWARDED_FOR', '');
if ($bCheckProxy && null !== static::GetServer('HTTP_X_FORWARDED_FOR', null)) {
return static::GetServer('HTTP_X_FORWARDED_FOR', '');
}
else
{
$sIp = $this->GetServer('REMOTE_ADDR', '');
}
return $sIp;
return static::GetServer('REMOTE_ADDR', '');
}
public function ServerNotModifiedCache(int $iExpireTime, bool $bSetCacheHeader = true, string $sEtag = '') : bool
public static function checkETag(string $ETag) : void
{
$bResult = false;
if (0 < $iExpireTime)
{
$iUtcTimeStamp = \time();
$sIfModifiedSince = $this->GetHeader('If-Modified-Since', '');
if (0 === \strlen($sIfModifiedSince))
{
if ($bSetCacheHeader)
{
\header('Cache-Control: public');
\header('Pragma: public');
\header('Last-Modified: '.\gmdate('D, d M Y H:i:s', $iUtcTimeStamp - $iExpireTime).' UTC');
\header('Expires: '.\gmdate('D, j M Y H:i:s', $iUtcTimeStamp + $iExpireTime).' UTC');
if (\strlen($sEtag))
{
\header('Etag: '.$sEtag);
}
}
}
else
{
static::StatusHeader(304);
$bResult = true;
}
// $ETag . APP_VERSION
$sIfNoneMatch = static::GetHeader('If-None-Match');
if ($sIfNoneMatch && false !== \strpos($sIfNoneMatch, $ETag)) {
static::StatusHeader(304);
exit;
}
$sIfMatch = static::GetHeader('If-Match');
if ($sIfMatch && false === \strpos($sIfMatch, $ETag)) {
static::StatusHeader(412);
exit;
}
return $bResult;
}
/**
* @staticvar boolean $bCache
*/
public static function setETag(string $ETag) : void
{
// $ETag . APP_VERSION
static::checkETag($ETag);
\header("ETag: \"{$ETag}\"");
}
public static function checkLastModified(int $mtime) : void
{
$sIfModifiedSince = static::GetHeader('If-Modified-Since');
if ($sIfModifiedSince && $mtime <= \strtotime($sIfModifiedSince)) {
static::StatusHeader(304);
exit;
}
$sIfUnmodifiedSince = static::GetHeader('If-Unmodified-Since');
if ($sIfUnmodifiedSince && $mtime > \strtotime($sIfUnmodifiedSince)) {
static::StatusHeader(412);
exit;
}
}
public static function setLastModified(int $mtime) : void
{
static::checkLastModified($mtime);
\header('Last-Modified: '.\gmdate('D, d M Y H:i:s \G\M\T', $mtime)); # DATE_RFC1123
}
private static $bCache = false;
public function ServerNoCache()
{
static $bCache = false;
if (false === $bCache)
{
$bCache = true;
if (!static::$bCache) {
static::$bCache = true;
\header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
\header('Last-Modified: '.\gmdate('D, d M Y H:i:s').' GMT');
\header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0, post-check=0, pre-check=0');
\header('Cache-Control: no-store');
\header('Pragma: no-cache');
}
}
/**
* @staticvar boolean $bCache
*/
public function ServerUseCache(string $sEtag, int $iLastModified, int $iExpires)
public static function ServerUseCache(string $sEtag = '', int $iLastModified = 0, int $iExpires = 0) : void
{
static $bCache = false;
if (false === $bCache)
{
$bCache = true;
if (!static::$bCache) {
static::$bCache = true;
\header('Cache-Control: private');
\header('ETag: '.$sEtag);
\header('Last-Modified: '.\gmdate('D, d M Y H:i:s', $iLastModified).' UTC');
\header('Expires: '.\gmdate('D, j M Y H:i:s', $iExpires).' UTC');
$iExpires && \header('Expires: '.\gmdate('D, j M Y H:i:s', \time() + $iExpires).' UTC');
$sEtag && static::setETag($sEtag);
$iLastModified && static::setLastModified($iLastModified);
}
}
public static function StatusHeader(int $iStatus, string $sCustomStatusText = '') : void
{
if (99 < $iStatus)
{
if (99 < $iStatus) {
$aStatus = array(
200 => 'OK',
206 => 'Partial Content',
301 => 'Moved Permanently',
302 => 'Found',
304 => 'Not Modified',
400 => 'Bad Request',
401 => 'Unauthorized',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
412 => 'Precondition Failed',
416 => 'Requested range not satisfiable',
500 => 'Internal Server Error'
);
$sHeaderText = (0 === \strlen($sCustomStatusText) && isset($aStatus[$iStatus]) ? $aStatus[$iStatus] : $sCustomStatusText);
$sHeaderText = (!\strlen($sCustomStatusText) && isset($aStatus[$iStatus]) ? $aStatus[$iStatus] : $sCustomStatusText);
\http_response_code($iStatus);
if (isset($_SERVER['SERVER_PROTOCOL'])) {
@ -269,9 +234,15 @@ class Http
}
}
public static function Location(string $sUrl, int $iStatus = 302): void
{
static::StatusHeader($iStatus);
\header('Location: ' . $sUrl);
}
public function GetPath() : string
{
$sUrl = \ltrim(\substr($this->GetServer('SCRIPT_NAME', ''), 0, \strrpos($this->GetServer('SCRIPT_NAME', ''), '/')), '/');
$sUrl = \ltrim(\substr(static::GetServer('SCRIPT_NAME', ''), 0, \strrpos(static::GetServer('SCRIPT_NAME', ''), '/')), '/');
return '' === $sUrl ? '/' : '/'.$sUrl.'/';
}
@ -282,6 +253,6 @@ class Http
public function GetFullUrl() : string
{
return $this->GetScheme().'://'.$this->GetHost(true, false).$this->GetPath();
return $this->GetScheme().'://'.$this->GetHost(false).$this->GetPath();
}
}

View file

@ -1,259 +0,0 @@
<?php
/*
* This file is part of MailSo.
*
* (c) 2014 Usenko Timur
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace MailSo\Base;
/**
* @category MailSo
* @package Base
*/
class LinkFinder
{
/**
* @const
*/
const OPEN_LINK = '@#@link{';
const CLOSE_LINK = '}link@#@';
/**
* @var array
*/
private $aPrepearPlainStringUrls;
/**
* @var string
*/
private $sText;
/**
* @var mixed
*/
private $fLinkWrapper;
/**
* @var int
*/
private $iHtmlSpecialCharsFlags;
/**
* @var mixed
*/
private $fMailWrapper;
/**
* @var int
*/
private $iOptimizationLimit;
function __construct()
{
$this->iHtmlSpecialCharsFlags = (\defined('ENT_QUOTES') && \defined('ENT_SUBSTITUTE') && \defined('ENT_HTML401'))
? ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 : ENT_QUOTES;
if (\defined('ENT_IGNORE'))
{
$this->iHtmlSpecialCharsFlags |= ENT_IGNORE;
}
$this->iOptimizationLimit = 300000;
$this->Clear();
}
public function Clear() : self
{
$this->aPrepearPlainStringUrls = array();
$this->fLinkWrapper = null;
$this->fMailWrapper = null;
$this->sText = '';
return $this;
}
public function Text(string $sText) : self
{
$this->sText = $sText;
return $this;
}
/**
* @param mixed $fLinkWrapper
*/
public function LinkWrapper($fLinkWrapper) : self
{
$this->fLinkWrapper = $fLinkWrapper;
return $this;
}
/**
* @param mixed $fMailWrapper
*/
public function MailWrapper($fMailWrapper) : self
{
$this->fMailWrapper = $fMailWrapper;
return $this;
}
public function UseDefaultWrappers(bool $bAddTargetBlank = false) : self
{
$this->fLinkWrapper = function ($sLink) use ($bAddTargetBlank) {
$sNameLink = $sLink;
if (!\preg_match('/^[a-z]{3,5}\:\/\//i', \ltrim($sLink)))
{
$sLink = 'https://'.\ltrim($sLink);
}
return '<a '.($bAddTargetBlank ? 'target="_blank" ': '').'href="'.$sLink.'">'.$sNameLink.'</a>';
};
$this->fMailWrapper = function ($sEmail) use ($bAddTargetBlank) {
return '<a '.($bAddTargetBlank ? 'target="_blank" ': '').'href="mailto:'.$sEmail.'">'.$sEmail.'</a>';
};
return $this;
}
public function CompileText(bool $bUseHtmlSpecialChars = true) : string
{
$sText = \substr($this->sText, 0, $this->iOptimizationLimit);
$sSubText = \substr($this->sText, $this->iOptimizationLimit);
$this->aPrepearPlainStringUrls = array();
if (null !== $this->fLinkWrapper && \is_callable($this->fLinkWrapper))
{
$sText = $this->findLinks($sText, $this->fLinkWrapper);
}
if (null !== $this->fMailWrapper && \is_callable($this->fMailWrapper))
{
$sText = $this->findMails($sText, $this->fMailWrapper);
}
$sResult = '';
if ($bUseHtmlSpecialChars)
{
$sResult = \htmlentities($sText.$sSubText, $this->iHtmlSpecialCharsFlags, 'UTF-8');
}
else
{
$sResult = $sText.$sSubText;
}
unset($sText, $sSubText);
if (\count($this->aPrepearPlainStringUrls))
{
$aPrepearPlainStringUrls = $this->aPrepearPlainStringUrls;
$sResult = \preg_replace_callback('/'.\preg_quote(static::OPEN_LINK, '/').
'([\d]+)'.\preg_quote(static::CLOSE_LINK, '/').'/',
function ($aMatches) use ($aPrepearPlainStringUrls) {
$iIndex = (int) $aMatches[1];
return isset($aPrepearPlainStringUrls[$iIndex]) ? $aPrepearPlainStringUrls[$iIndex] : '';
}, $sResult);
$this->aPrepearPlainStringUrls = array();
}
return $sResult;
}
/**
* @param mixed $fWrapper
*/
private function findLinks(string $sText, $fWrapper) : string
{
$sPattern = '/([\W]|^)((?:https?:\/\/)|(?:svn:\/\/)|(?:git:\/\/)|(?:s?ftps?:\/\/)|(?:www\.))'.
'((\S+?)(\\/)?)((?:&gt;)?|[^\w\=\\/;\(\)\[\]]*?)(?=<|\s|$)/imu';
$aPrepearPlainStringUrls = $this->aPrepearPlainStringUrls;
$sText = \preg_replace_callback($sPattern, function ($aMatch) use ($fWrapper, &$aPrepearPlainStringUrls) {
if (\is_array($aMatch) && 6 < \count($aMatch))
{
while (\in_array($sChar = \substr($aMatch[3], -1), array(']', ')')))
{
if (\substr_count($aMatch[3], ']' === $sChar ? '[': '(') - \substr_count($aMatch[3], $sChar) < 0)
{
$aMatch[3] = \substr($aMatch[3], 0, -1);
$aMatch[6] = (']' === $sChar ? ']': ')').$aMatch[6];
}
else
{
break;
}
}
$sLinkWithWrap = $fWrapper($aMatch[2].$aMatch[3]);
if (\is_string($sLinkWithWrap) && \strlen($sLinkWithWrap))
{
$aPrepearPlainStringUrls[] = \stripslashes($sLinkWithWrap);
return $aMatch[1].
static::OPEN_LINK.
(\count($aPrepearPlainStringUrls) - 1).
static::CLOSE_LINK.
$aMatch[6];
}
return $aMatch[0];
}
return '';
}, $sText);
if (\count($aPrepearPlainStringUrls))
{
$this->aPrepearPlainStringUrls = $aPrepearPlainStringUrls;
}
return $sText;
}
/**
* @param mixed $fWrapper
*/
private function findMails(string $sText, $fWrapper) : string
{
$sPattern = '/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/';
$aPrepearPlainStringUrls = $this->aPrepearPlainStringUrls;
$sText = \preg_replace_callback($sPattern, function ($aMatch) use ($fWrapper, &$aPrepearPlainStringUrls) {
if (\is_array($aMatch) && isset($aMatch[1]))
{
$sMailWithWrap = $fWrapper($aMatch[1]);
if (\is_string($sMailWithWrap) && \strlen($sMailWithWrap))
{
$aPrepearPlainStringUrls[] = \stripslashes($sMailWithWrap);
return static::OPEN_LINK.
(\count($aPrepearPlainStringUrls) - 1).
static::CLOSE_LINK;
}
return $aMatch[1];
}
return '';
}, $sText);
if (\count($aPrepearPlainStringUrls))
{
$this->aPrepearPlainStringUrls = $aPrepearPlainStringUrls;
}
return $sText;
}
}

View file

@ -17,10 +17,7 @@ namespace MailSo\Base;
*/
abstract class ResourceRegistry
{
/**
* @var array
*/
public static $Resources = array();
public static array $Resources = array();
/**
* @staticvar bool $bInited
@ -28,16 +25,12 @@ abstract class ResourceRegistry
private static function regResourcesShutdownFunc() : void
{
static $bInited = false;
if (!$bInited)
{
if (!$bInited) {
$bInited = true;
\register_shutdown_function(function () {
if (\is_array(static::$Resources))
{
foreach (\array_keys(static::$Resources) as $sKey)
{
if (\is_resource(static::$Resources[$sKey]))
{
if (\is_array(static::$Resources)) {
foreach (\array_keys(static::$Resources) as $sKey) {
if (\is_resource(static::$Resources[$sKey])) {
\fclose(static::$Resources[$sKey]);
}
static::$Resources[$sKey] = null;
@ -57,8 +50,7 @@ abstract class ResourceRegistry
self::regResourcesShutdownFunc();
$oResult = \fopen('php://temp/maxmemory:'.($iMemoryMaxInMb * 1024 * 1024), 'r+b');
if (\is_resource($oResult))
{
if (\is_resource($oResult)) {
static::$Resources[(string) $oResult] = $oResult;
return $oResult;
}
@ -72,8 +64,7 @@ abstract class ResourceRegistry
public static function CreateMemoryResourceFromString(string $sString)
{
$oResult = self::CreateMemoryResource();
if (\is_resource($oResult))
{
if (\is_resource($oResult)) {
\fwrite($oResult, $sString);
\rewind($oResult);
}
@ -86,18 +77,15 @@ abstract class ResourceRegistry
*/
public static function CloseMemoryResource(&$rResource) : void
{
if (\is_resource($rResource))
{
if (\is_resource($rResource)) {
$sKey = (string) $rResource;
if (isset(static::$Resources[$sKey]))
{
if (isset(static::$Resources[$sKey])) {
\fclose(static::$Resources[$sKey]);
static::$Resources[$sKey] = null;
unset(static::$Resources[$sKey]);
}
if (\is_resource($rResource))
{
if (\is_resource($rResource)) {
\fclose($rResource);
}

View file

@ -19,55 +19,34 @@ namespace MailSo\Base\StreamWrappers;
*/
class Binary
{
/** @var resource|null */
public $context;
/**
* @var string
*/
const STREAM_NAME = 'mailsobinary';
/**
* @var array
*/
private static $aStreams = array();
private static array $aStreams = array();
/**
* @var array
*/
private static $aRememberStreams = array();
private static array $aRememberStreams = array();
/**
* @var resource
*/
private $rStream;
/**
* @var string
*/
private $sFromEncoding;
private string $sFromEncoding;
/**
* @var string
*/
private $sToEncoding;
private string $sToEncoding;
/**
* @var string
*/
private $sFunctionName;
private string $sFunctionName;
/**
* @var int
*/
private $iPos;
private int $iPos;
/**
* @var string
*/
private $sBuffer;
private string $sBuffer;
/**
* @var string
*/
private $sReadEndBuffer;
private string $sReadEndBuffer;
public static function GetInlineDecodeOrEncodeFunctionName(string $sContentTransferEncoding, bool $bDecode = true) : string
{
@ -93,8 +72,7 @@ class Binary
$sEndBuffer = '';
$iQuotedPrintableLen = \strlen($sEncodedString);
$iLastSpace = \strrpos($sEncodedString, ' ');
if (false !== $iLastSpace && $iLastSpace + 1 < $iQuotedPrintableLen)
{
if (false !== $iLastSpace && $iLastSpace + 1 < $iQuotedPrintableLen) {
$sEndBuffer = \substr($sEncodedString, $iLastSpace + 1);
$sEncodedString = \substr($sEncodedString, 0, $iLastSpace + 1);
}
@ -106,10 +84,8 @@ class Binary
*/
public static function IsStreamRemembed($rStream) : bool
{
foreach (self::$aRememberStreams as $rRem)
{
if ($rStream === $rRem)
{
foreach (self::$aRememberStreams as $rRem) {
if ($rStream === $rRem) {
return true;
}
}
@ -122,8 +98,7 @@ class Binary
*/
public static function RememberStream($rStream)
{
if (!self::IsStreamRemembed($rStream))
{
if (!self::IsStreamRemembed($rStream)) {
self::$aRememberStreams[] = $rStream;
}
}
@ -136,15 +111,13 @@ class Binary
public static function CreateStream($rStream,
string $sUtilsDecodeOrEncodeFunctionName = null, string $sFromEncoding = null, string $sToEncoding = null)
{
if (null === $sUtilsDecodeOrEncodeFunctionName || !\strlen($sUtilsDecodeOrEncodeFunctionName))
{
if (null === $sUtilsDecodeOrEncodeFunctionName || !\strlen($sUtilsDecodeOrEncodeFunctionName)) {
$sUtilsDecodeOrEncodeFunctionName = 'InlineNullDecode';
}
$sHashName = \md5(\microtime(true).\rand(1000, 9999));
if (null !== $sFromEncoding && null !== $sToEncoding && $sFromEncoding !== $sToEncoding)
{
if (null !== $sFromEncoding && null !== $sToEncoding && $sFromEncoding !== $sToEncoding) {
$rStream = self::CreateStream($rStream, $sUtilsDecodeOrEncodeFunctionName);
$sUtilsDecodeOrEncodeFunctionName = 'InlineConvertDecode';
}
@ -213,27 +186,19 @@ class Binary
$sReturn = '';
$sFunctionName = $this->sFunctionName;
if ($iCount > 0)
{
if ($iCount < \strlen($this->sBuffer))
{
if ($iCount > 0) {
if ($iCount < \strlen($this->sBuffer)) {
$sReturn = \substr($this->sBuffer, 0, $iCount);
$this->sBuffer = \substr($this->sBuffer, $iCount);
}
else
{
} else {
$sReturn = $this->sBuffer;
while ($iCount > 0)
{
if (\feof($this->rStream))
{
if (!\strlen($this->sBuffer.$sReturn))
{
while ($iCount > 0) {
if (\feof($this->rStream)) {
if (!\strlen($this->sBuffer.$sReturn)) {
return false;
}
if (\strlen($this->sReadEndBuffer))
{
if (\strlen($this->sReadEndBuffer)) {
$sReturn .= self::$sFunctionName($this->sReadEndBuffer,
$this->sReadEndBuffer, $this->sFromEncoding, $this->sToEncoding);
@ -242,12 +207,9 @@ class Binary
$iCount = 0;
$this->sBuffer = '';
}
else
{
} else {
$sReadResult = \fread($this->rStream, 8192);
if (false === $sReadResult)
{
if (false === $sReadResult) {
return false;
}
@ -255,14 +217,11 @@ class Binary
$this->sReadEndBuffer, $this->sFromEncoding, $this->sToEncoding);
$iDecodeLen = \strlen($sReadResult);
if ($iCount < $iDecodeLen)
{
if ($iCount < $iDecodeLen) {
$this->sBuffer = \substr($sReturn, $iCount);
$sReturn = \substr($sReturn, 0, $iCount);
$iCount = 0;
}
else
{
} else {
$iCount -= $iDecodeLen;
}
}

View file

@ -18,30 +18,24 @@ namespace MailSo\Base\StreamWrappers;
*/
class Literal
{
/** @var resource|null */
public $context;
/**
* @var string
*/
const STREAM_NAME = 'mailsoliteral';
/**
* @var array
*/
private static $aStreams = array();
private static array $aStreams = array();
/**
* @var resource
*/
private $rStream;
/**
* @var int
*/
private $iSize;
private int $iSize;
/**
* @var int
*/
private $iPos;
private int $iPos;
/**
* @param resource $rStream
@ -92,20 +86,16 @@ class Literal
public function stream_read(int $iCount) : string
{
$sResult = false;
if ($this->iSize < $this->iPos + $iCount)
{
if ($this->iSize < $this->iPos + $iCount) {
$iCount = $this->iSize - $this->iPos;
}
if ($iCount > 0)
{
if ($iCount > 0) {
$sReadResult = '';
$iRead = $iCount;
while (0 < $iRead)
{
while (0 < $iRead) {
$sAddRead = \fread($this->rStream, $iRead);
if (false === $sAddRead)
{
if (false === $sAddRead) {
$sReadResult = false;
break;
}
@ -115,8 +105,7 @@ class Literal
$this->iPos += \strlen($sAddRead);
}
if (false !== $sReadResult)
{
if (false !== $sReadResult) {
$sResult = $sReadResult;
}
}
@ -160,6 +149,8 @@ class Literal
public function stream_seek() : bool
{
// $this->iPos = $offset;
// \fseek($this->rStream, $offset, $whence);
return false;
}
}

View file

@ -19,40 +19,25 @@ namespace MailSo\Base\StreamWrappers;
*/
class SubStreams
{
/** @var resource|null */
public $context;
/**
* @var string
*/
const STREAM_NAME = 'mailsosubstreams';
/**
* @var array
*/
private static $aStreams = array();
private static array $aStreams = array();
/**
* @var array
*/
private $aSubStreams;
private array $aSubStreams;
/**
* @var int
*/
private $iIndex;
private int $iIndex;
/**
* @var string
*/
private $sBuffer;
private string $sBuffer;
/**
* @var bool
*/
private $bIsEnd;
private bool $bIsEnd;
/**
* @var int
*/
private $iPos;
private int $iPos;
/**
* @return resource|bool
@ -69,20 +54,6 @@ class SubStreams
return \fopen(self::STREAM_NAME.'://'.$sHashName, 'rb');
}
/**
* @return resource|null
*/
protected function &getPart()
{
$nNull = null;
if (isset($this->aSubStreams[$this->iIndex]))
{
return $this->aSubStreams[$this->iIndex];
}
return $nNull;
}
public function stream_open(string $sPath) : bool
{
$this->aSubStreams = array();
@ -116,53 +87,41 @@ class SubStreams
{
$sReturn = '';
$mCurrentPart = null;
if ($iCount > 0)
{
if ($iCount < \strlen($this->sBuffer))
{
if ($iCount > 0) {
if ($iCount < \strlen($this->sBuffer)) {
$sReturn = \substr($this->sBuffer, 0, $iCount);
$this->sBuffer = \substr($this->sBuffer, $iCount);
}
else
{
} else {
$sReturn = $this->sBuffer;
while ($iCount > 0)
{
$mCurrentPart =& $this->getPart();
if (null === $mCurrentPart)
{
while ($iCount > 0) {
$mCurrentPart = isset($this->aSubStreams[$this->iIndex])
? $this->aSubStreams[$this->iIndex]
: null;
if (null === $mCurrentPart) {
$this->bIsEnd = true;
$this->sBuffer = '';
$iCount = 0;
break;
}
if (\is_resource($mCurrentPart))
{
if (!\feof($mCurrentPart))
{
if (\is_resource($mCurrentPart)) {
if (!\feof($mCurrentPart)) {
$sReadResult = \fread($mCurrentPart, 8192);
if (false === $sReadResult)
{
if (false === $sReadResult) {
return false;
}
$sReturn .= $sReadResult;
}
else
{
$this->iIndex++;
} else {
++$this->iIndex;
}
}
$iLen = \strlen($sReturn);
if ($iCount < $iLen)
{
if ($iCount < $iLen) {
$this->sBuffer = \substr($sReturn, $iCount);
$sReturn = \substr($sReturn, 0, $iCount);
$iCount = 0;
}
else
{
} else {
$iCount -= $iLen;
}
}
@ -211,6 +170,8 @@ class SubStreams
public function stream_seek() : bool
{
// $this->iPos = $offset;
// foreach ($this->aSubStreams as $rStream) \fseek($rStream, $offset, $whence);
return false;
}
}

View file

@ -18,6 +18,9 @@ namespace MailSo\Base\StreamWrappers;
*/
class TempFile
{
/** @var resource|null */
public $context;
/**
* @var string
*/
@ -56,15 +59,11 @@ class TempFile
self::STREAM_NAME === $aPath['scheme'])
{
$sHashName = $aPath['host'];
if (isset(self::$aStreams[$sHashName]) &&
\is_resource(self::$aStreams[$sHashName]))
{
if (isset(self::$aStreams[$sHashName]) && \is_resource(self::$aStreams[$sHashName])) {
$this->rStream = self::$aStreams[$sHashName];
\fseek($this->rStream, 0);
$bResult = true;
}
else
{
} else {
$this->rStream = \fopen('php://temp', 'r+b');
self::$aStreams[$sHashName] = $this->rStream;

View file

@ -190,8 +190,7 @@ abstract class Utils
public static function FormatFileSize(int $iSize, int $iRound = 0) : string
{
$aSizes = array('B', 'KB', 'MB');
for ($iIndex = 0; $iSize > 1024 && isset($aSizes[$iIndex + 1]); $iIndex++)
{
for ($iIndex = 0; $iSize > 1024 && isset($aSizes[$iIndex + 1]); ++$iIndex) {
$iSize /= 1024;
}
return \round($iSize, $iRound).$aSizes[$iIndex];
@ -223,12 +222,10 @@ abstract class Utils
public static function DecodeHeaderValue(string $sEncodedValue, string $sIncomingCharset = '', string $sForcedIncomingCharset = '') : string
{
$sValue = $sEncodedValue;
if (\strlen($sIncomingCharset))
{
if (\strlen($sIncomingCharset)) {
$sIncomingCharset = static::NormalizeCharsetByValue($sIncomingCharset, $sValue);
$sValue = static::ConvertEncoding($sValue, $sIncomingCharset,
Enumerations\Charset::UTF_8);
$sValue = static::ConvertEncoding($sValue, $sIncomingCharset, Enumerations\Charset::UTF_8);
}
$sValue = \preg_replace('/\?=[\n\r\t\s]{1,5}=\?/m', '?==?', $sValue);
@ -239,15 +236,11 @@ abstract class Utils
// \preg_match_all('/=\?[^\?]+\?[q|b|Q|B]\?[^\?]*?\?=/', $sValue, $aMatch);
\preg_match_all('/=\?[^\?]+\?[q|b|Q|B]\?.*?\?=/', $sValue, $aMatch);
if (isset($aMatch[0]) && \is_array($aMatch[0]))
{
for ($iIndex = 0, $iLen = \count($aMatch[0]); $iIndex < $iLen; $iIndex++)
{
if (isset($aMatch[0][$iIndex]))
{
if (isset($aMatch[0]) && \is_array($aMatch[0])) {
for ($iIndex = 0, $iLen = \count($aMatch[0]); $iIndex < $iLen; ++$iIndex) {
if (isset($aMatch[0][$iIndex])) {
$iPos = \strpos($aMatch[0][$iIndex], '*');
if (false !== $iPos)
{
if (false !== $iPos) {
$aMatch[0][$iIndex][0] = \substr($aMatch[0][$iIndex][0], 0, $iPos);
}
}
@ -261,11 +254,9 @@ abstract class Utils
$sMainCharset = '';
$bOneCharset = true;
for ($iIndex = 0, $iLen = \count($aEncodeArray); $iIndex < $iLen; $iIndex++)
{
for ($iIndex = 0, $iLen = \count($aEncodeArray); $iIndex < $iLen; ++$iIndex) {
$aTempArr = array('', $aEncodeArray[$iIndex]);
if ('=?' === \substr(\trim($aTempArr[1]), 0, 2))
{
if ('=?' === \substr(\trim($aTempArr[1]), 0, 2)) {
$iPos = \strpos($aTempArr[1], '?', 2);
$aTempArr[0] = \substr($aTempArr[1], 2, $iPos - 2);
$sEncType = \strtoupper($aTempArr[1][$iPos + 1]);
@ -284,17 +275,13 @@ abstract class Utils
}
}
if (\strlen($aTempArr[0]))
{
if (\strlen($aTempArr[0])) {
$sCharset = \strlen($sForcedIncomingCharset) ? $sForcedIncomingCharset : $aTempArr[0];
$sCharset = static::NormalizeCharset($sCharset, true);
if ('' === $sMainCharset)
{
if ('' === $sMainCharset) {
$sMainCharset = $sCharset;
}
else if ($sMainCharset !== $sCharset)
{
} else if ($sMainCharset !== $sCharset) {
$bOneCharset = false;
}
}
@ -308,14 +295,10 @@ abstract class Utils
unset($aTempArr);
}
for ($iIndex = 0, $iLen = \count($aParts); $iIndex < $iLen; $iIndex++)
{
if ($bOneCharset)
{
for ($iIndex = 0, $iLen = \count($aParts); $iIndex < $iLen; ++$iIndex) {
if ($bOneCharset) {
$sValue = \str_replace($aParts[$iIndex][0], $aParts[$iIndex][1], $sValue);
}
else
{
} else {
$aParts[$iIndex][2] = static::NormalizeCharsetByValue($aParts[$iIndex][2], $aParts[$iIndex][1]);
$sValue = \str_replace($aParts[$iIndex][0],
@ -324,8 +307,7 @@ abstract class Utils
}
}
if ($bOneCharset && \strlen($sMainCharset))
{
if ($bOneCharset && \strlen($sMainCharset)) {
$sMainCharset = static::NormalizeCharsetByValue($sMainCharset, $sValue);
$sValue = static::ConvertEncoding($sValue, $sMainCharset, Enumerations\Charset::UTF_8);
}
@ -337,8 +319,7 @@ abstract class Utils
{
$sResultHeaders = $sIncHeaders;
if ($aHeadersToRemove)
{
if ($aHeadersToRemove) {
$aHeadersToRemove = \array_map('strtolower', $aHeadersToRemove);
$sIncHeaders = \preg_replace('/[\r\n]+/', "\n", $sIncHeaders);
@ -347,31 +328,21 @@ abstract class Utils
$bSkip = false;
$aResult = array();
foreach ($aHeaders as $sLine)
{
if (\strlen($sLine))
{
foreach ($aHeaders as $sLine) {
if (\strlen($sLine)) {
$sFirst = \substr($sLine,0,1);
if (' ' === $sFirst || "\t" === $sFirst)
{
if (!$bSkip)
{
if (' ' === $sFirst || "\t" === $sFirst) {
if (!$bSkip) {
$aResult[] = $sLine;
}
}
else
{
} else {
$bSkip = false;
$aParts = \explode(':', $sLine, 2);
if (!empty($aParts) && !empty($aParts[0]))
{
if (\in_array(\strtolower(\trim($aParts[0])), $aHeadersToRemove))
{
if (!empty($aParts) && !empty($aParts[0])) {
if (\in_array(\strtolower(\trim($aParts[0])), $aHeadersToRemove)) {
$bSkip = true;
}
else
{
} else {
$aResult[] = $sLine;
}
}
@ -388,20 +359,17 @@ abstract class Utils
public static function EncodeUnencodedValue(string $sEncodeType, string $sValue) : string
{
$sValue = \trim($sValue);
if (\strlen($sValue) && !static::IsAscii($sValue))
{
if (\strlen($sValue) && !static::IsAscii($sValue)) {
switch (\strtoupper($sEncodeType))
{
case 'B':
$sValue = '=?'.\strtolower(Enumerations\Charset::UTF_8).
return '=?'.\strtolower(Enumerations\Charset::UTF_8).
'?B?'.\base64_encode($sValue).'?=';
break;
case 'Q':
$sValue = '=?'.\strtolower(Enumerations\Charset::UTF_8).
return '=?'.\strtolower(Enumerations\Charset::UTF_8).
'?Q?'.\str_replace(array('?', ' ', '_'), array('=3F', '_', '=5F'),
\quoted_printable_encode($sValue)).'?=';
break;
}
}
@ -418,28 +386,22 @@ abstract class Utils
$iNlen = \strlen($sAttrName);
$iVlen = \strlen($sValue);
if (\strlen($sAttrName) + $iVlen > $iLen - 3)
{
if (\strlen($sAttrName) + $iVlen > $iLen - 3) {
$sections = array();
$section = 0;
for ($i = 0, $j = 0; $i < $iVlen; $i += $j)
{
for ($i = 0, $j = 0; $i < $iVlen; $i += $j) {
$j = $iLen - $iNlen - \strlen($section) - 4;
$sections[$section++] = \substr($sValue, $i, $j);
}
for ($i = 0, $n = $section; $i < $n; $i++)
{
for ($i = 0, $n = $section; $i < $n; ++$i) {
$sections[$i] = ' '.$sAttrName.'*'.$i.'*='.$sections[$i];
}
return \implode(";\r\n", $sections);
}
else
{
return $sAttrName.'*='.$sValue;
}
return $sAttrName.'*='.$sValue;
}
public static function EncodeHeaderUtf8AttributeValue(string $sAttrName, string $sValue) : string
@ -447,12 +409,9 @@ abstract class Utils
$sAttrName = \trim($sAttrName);
$sValue = \trim($sValue);
if (\strlen($sValue) && !static::IsAscii($sValue))
{
if (\strlen($sValue) && !static::IsAscii($sValue)) {
$sValue = static::AttributeRfc2231Encode($sAttrName, $sValue);
}
else
{
} else {
$sValue = $sAttrName.'="'.\str_replace('"', '\\"', $sValue).'"';
}
@ -462,8 +421,7 @@ abstract class Utils
public static function GetAccountNameFromEmail(string $sEmail) : string
{
$sResult = '';
if (\strlen($sEmail))
{
if (\strlen($sEmail)) {
$iPos = \strrpos($sEmail, '@');
$sResult = (false === $iPos) ? $sEmail : \substr($sEmail, 0, $iPos);
}
@ -474,11 +432,9 @@ abstract class Utils
public static function GetDomainFromEmail(string $sEmail) : string
{
$sResult = '';
if (\strlen($sEmail))
{
if (\strlen($sEmail)) {
$iPos = \strrpos($sEmail, '@');
if (false !== $iPos && 0 < $iPos)
{
if (false !== $iPos && 0 < $iPos) {
$sResult = \substr($sEmail, $iPos + 1);
}
}
@ -501,243 +457,13 @@ abstract class Utils
return false === $iLast ? '' : \strtolower(\substr($sFileName, $iLast + 1));
}
public static function MimeContentType(string $sFileName) : string
{
$sResult = 'application/octet-stream';
$sFileName = \trim(\strtolower($sFileName));
if ('winmail.dat' === $sFileName)
{
return 'application/ms-tnef';
}
$aMimeTypes = array(
'eml' => 'message/rfc822',
'mime' => 'message/rfc822',
'txt' => 'text/plain',
'text' => 'text/plain',
'def' => 'text/plain',
'list' => 'text/plain',
'in' => 'text/plain',
'ini' => 'text/plain',
'log' => 'text/plain',
'sql' => 'text/plain',
'cfg' => 'text/plain',
'conf' => 'text/plain',
'rtx' => 'text/richtext',
'vcard' => 'text/vcard',
'vcf' => 'text/vcard',
'htm' => 'text/html',
'html' => 'text/html',
'csv' => 'text/csv',
'ics' => 'text/calendar',
'ifb' => 'text/calendar',
'xml' => 'text/xml',
'json' => 'application/json',
'swf' => 'application/x-shockwave-flash',
'hlp' => 'application/winhlp',
'wgt' => 'application/widget',
'chm' => 'application/vnd.ms-htmlhelp',
'asc' => 'application/pgp-signature',
'p10' => 'application/pkcs10',
'p7c' => 'application/pkcs7-mime',
'p7m' => 'application/pkcs7-mime',
'p7s' => 'application/pkcs7-signature',
'torrent' => 'application/x-bittorrent',
// scripts
'js' => 'application/javascript',
'pl' => 'text/perl',
'css' => 'text/css',
'asp' => 'text/asp',
'php' => 'application/x-php',
// images
'png' => 'image/png',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'jpe' => 'image/jpeg',
'jfif' => 'image/jpeg',
'gif' => 'image/gif',
'bmp' => 'image/bmp',
'cgm' => 'image/cgm',
'ief' => 'image/ief',
'ico' => 'image/x-icon',
'tif' => 'image/tiff',
'tiff' => 'image/tiff',
'svg' => 'image/svg+xml',
'svgz' => 'image/svg+xml',
'djv' => 'image/vnd.djvu',
'djvu' => 'image/vnd.djvu',
'webp' => 'image/webp',
// archives
'zip' => 'application/zip',
'7z' => 'application/x-7z-compressed',
'rar' => 'application/x-rar-compressed',
'exe' => 'application/x-msdownload',
'dll' => 'application/x-msdownload',
'scr' => 'application/x-msdownload',
'com' => 'application/x-msdownload',
'bat' => 'application/x-msdownload',
'msi' => 'application/x-msdownload',
'cab' => 'application/vnd.ms-cab-compressed',
'gz' => 'application/x-gzip',
'tgz' => 'application/x-gzip',
'bz' => 'application/x-bzip',
'bz2' => 'application/x-bzip2',
'deb' => 'application/x-debian-package',
'tar' => 'application/x-tar',
// fonts
'psf' => 'application/x-font-linux-psf',
'otf' => 'application/x-font-otf',
'pcf' => 'application/x-font-pcf',
'snf' => 'application/x-font-snf',
'ttf' => 'application/x-font-ttf',
'ttc' => 'application/x-font-ttf',
// audio
'aac' => 'audio/aac',
'flac' => 'audio/flac',
'mp3' => 'audio/mpeg',
'aif' => 'audio/aiff',
'aifc' => 'audio/aiff',
'aiff' => 'audio/aiff',
'wav' => 'audio/x-wav',
'midi' => 'audio/midi',
'mp4a' => 'audio/mp4',
'ogg' => 'audio/ogg',
'weba' => 'audio/webm',
'm3u' => 'audio/x-mpegurl',
// video
'qt' => 'video/quicktime',
'mov' => 'video/quicktime',
'avi' => 'video/x-msvideo',
'mpg' => 'video/mpeg',
'mpeg' => 'video/mpeg',
'mpe' => 'video/mpeg',
'm1v' => 'video/mpeg',
'm2v' => 'video/mpeg',
'3gp' => 'video/3gpp',
'3g2' => 'video/3gpp2',
'h261' => 'video/h261',
'h263' => 'video/h263',
'h264' => 'video/h264',
'jpgv' => 'video/jpgv',
'mp4' => 'video/mp4',
'mp4v' => 'video/mp4',
'mpg4' => 'video/mp4',
'ogv' => 'video/ogg',
'webm' => 'video/webm',
'm4v' => 'video/x-m4v',
'asf' => 'video/x-ms-asf',
'asx' => 'video/x-ms-asf',
'wm' => 'video/x-ms-wm',
'wmv' => 'video/x-ms-wmv',
'wmx' => 'video/x-ms-wmx',
'wvx' => 'video/x-ms-wvx',
'movie' => 'video/x-sgi-movie',
// adobe
'pdf' => 'application/pdf',
'psd' => 'image/vnd.adobe.photoshop',
'ai' => 'application/postscript',
'eps' => 'application/postscript',
'ps' => 'application/postscript',
// ms office
'doc' => 'application/msword',
'dot' => 'application/msword',
'rtf' => 'application/rtf',
'xls' => 'application/vnd.ms-excel',
'ppt' => 'application/vnd.ms-powerpoint',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
// open office
'odt' => 'application/vnd.oasis.opendocument.text',
'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
'odp' => 'application/vnd.oasis.opendocument.presentation'
);
$sExt = static::GetFileExtension($sFileName);
if (\strlen($sExt) && isset($aMimeTypes[$sExt]))
{
$sResult = $aMimeTypes[$sExt];
}
return $sResult;
}
public static function ContentTypeType(string $sContentType, string $sFileName) : string
{
$sContentType = \strtolower($sContentType);
if (\str_starts_with($sContentType, 'image/')) {
return 'image';
}
switch ($sContentType)
{
case 'application/zip':
case 'application/x-7z-compressed':
case 'application/x-rar-compressed':
case 'application/x-msdownload':
case 'application/vnd.ms-cab-compressed':
case 'application/gzip':
case 'application/x-gzip':
case 'application/x-bzip':
case 'application/x-bzip2':
case 'application/x-debian-package':
case 'application/x-tar':
return 'archive';
case 'application/msword':
case 'application/rtf':
case 'application/vnd.ms-excel':
case 'application/vnd.ms-powerpoint':
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.template':
case 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
case 'application/vnd.oasis.opendocument.text':
case 'application/vnd.oasis.opendocument.spreadsheet':
return 'doc';
case 'application/pdf':
case 'application/x-pdf':
return 'pdf';
}
switch (\strtolower(static::GetFileExtension($sFileName)))
{
case 'zip':
case '7z':
case 'rar':
case 'tar':
case 'tgz':
return 'archive';
case 'pdf':
return 'pdf';
}
return '';
}
/**
* @staticvar bool $bValidateAction
*/
public static function ResetTimeLimit(int $iTimeToReset = 15, int $iTimeToAdd = 120) : bool
{
$iTime = \time();
if ($iTime < $_SERVER['REQUEST_TIME_FLOAT'] + 5)
{
if ($iTime < $_SERVER['REQUEST_TIME_FLOAT'] + 5) {
// do nothing first 5s
return true;
}
@ -745,18 +471,15 @@ abstract class Utils
static $bValidateAction = null;
static $iResetTimer = null;
if (null === $bValidateAction)
{
if (null === $bValidateAction) {
$iResetTimer = 0;
$bValidateAction = static::FunctionCallable('set_time_limit');
}
if ($bValidateAction && $iTimeToReset < $iTime - $iResetTimer)
{
if ($bValidateAction && $iTimeToReset < $iTime - $iResetTimer) {
$iResetTimer = $iTime;
if (!\set_time_limit($iTimeToAdd))
{
if (!\set_time_limit($iTimeToAdd)) {
$bValidateAction = false;
return false;
}
@ -767,11 +490,13 @@ abstract class Utils
return false;
}
# Replace ampersand, spaces and reserved characters (based on Win95 VFAT)
# en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
/**
* Replace control characters, ampersand and reserved characters (based on Win95 VFAT)
* en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
*/
public static function SecureFileName(string $sFileName) : string
{
return \preg_replace('#[|\\\\?*<":>+\\[\\]/&\\s\\pC]#su', '-', $sFileName);
return \preg_replace('#[|\\\\?*<":>+\\[\\]/&\\pC]#su', '-', $sFileName);
}
public static function Trim(string $sValue) : string
@ -781,24 +506,7 @@ abstract class Utils
public static function RecRmDir(string $sDir) : bool
{
\clearstatcache();
if (\is_dir($sDir)) {
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($sDir, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST);
foreach ($iterator as $path) {
if ($path->isDir()) {
\rmdir($path);
} else {
\unlink($path);
}
}
\clearstatcache();
// \realpath_cache_size() && \clearstatcache(true);
return \rmdir($sDir);
}
return false;
return static::RecTimeDirRemove($sDir, 0);
}
public static function RecTimeDirRemove(string $sDir, int $iTime2Kill) : bool
@ -810,9 +518,10 @@ abstract class Utils
new \RecursiveDirectoryIterator($sDir, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST);
foreach ($iterator as $path) {
if ($path->isFile() && $path->getMTime() < $iTime) {
if ($path->isFile() && (!$iTime2Kill || $path->getMTime() < $iTime)) {
\is_callable('opcache_invalidate') && \opcache_invalidate($path, true);
\unlink($path);
} else if ($path->isDir() && !(new \FilesystemIterator($path))->valid()) {
} else if ($path->isDir() && (!$iTime2Kill || !(new \FilesystemIterator($path))->valid())) {
\rmdir($path);
}
}
@ -848,24 +557,19 @@ abstract class Utils
public static function Base64Decode(string $sString) : string
{
$sResultString = \base64_decode($sString, true);
if (false === $sResultString)
{
if (false === $sResultString) {
$sString = \str_replace(array(' ', "\r", "\n", "\t"), '', $sString);
$sString = \preg_replace('/[^a-zA-Z0-9=+\/](.*)$/', '', $sString);
if (false !== \strpos(\trim(\trim($sString), '='), '='))
{
if (false !== \strpos(\trim(\trim($sString), '='), '=')) {
$sString = \preg_replace('/=([^=])/', '= $1', $sString);
$aStrings = \explode(' ', $sString);
foreach ($aStrings as $iIndex => $sParts)
{
foreach ($aStrings as $iIndex => $sParts) {
$aStrings[$iIndex] = \base64_decode($sParts);
}
$sResultString = \implode('', $aStrings);
}
else
{
} else {
$sResultString = \base64_decode($sString);
}
}
@ -880,7 +584,7 @@ abstract class Utils
public static function UrlSafeBase64Decode(string $sValue) : string
{
return \base64_decode(\strtr($sValue, '-_', '+/'), '=');
return \base64_decode(\strtr($sValue, '-_', '+/'), true);
}
/**
@ -888,23 +592,16 @@ abstract class Utils
*/
public static function FpassthruWithTimeLimitReset($fResource, int $iBufferLen = 8192) : bool
{
$bResult = false;
if (\is_resource($fResource))
{
while (!\feof($fResource))
{
$bResult = \is_resource($fResource);
if ($bResult) {
while (!\feof($fResource)) {
$sBuffer = \fread($fResource, $iBufferLen);
if (false !== $sBuffer)
{
echo $sBuffer;
static::ResetTimeLimit();
continue;
if (false === $sBuffer) {
break;
}
break;
echo $sBuffer;
static::ResetTimeLimit();
}
$bResult = true;
}
return $bResult;
@ -912,63 +609,44 @@ abstract class Utils
/**
* @param resource $rRead
* @param resource $rWrite
*/
public static function MultipleStreamWriter($rRead, array $aWrite, int $iBufferLen = 8192, bool $bResetTimeLimit = true, bool $bFixCrLf = false, bool $bRewindOnComplete = false) : int
public static function WriteStream($rRead, $rWrite, int $iBufferLen = 8192, bool $bFixCrLf = false, bool $bRewindOnComplete = false) : int
{
$mResult = false;
if (\is_resource($rRead) && \count($aWrite))
{
$mResult = 0;
while (!\feof($rRead))
{
$sBuffer = \fread($rRead, $iBufferLen);
if (false === $sBuffer)
{
$mResult = false;
break;
}
if ('' === $sBuffer)
{
break;
}
if ($bFixCrLf)
{
$sBuffer = \str_replace("\n", "\r\n", \str_replace("\r", '', $sBuffer));
}
$mResult += \strlen($sBuffer);
foreach ($aWrite as $rWriteStream)
{
$mWriteResult = \fwrite($rWriteStream, $sBuffer);
if (false === $mWriteResult)
{
$mResult = false;
break 2;
}
}
if ($bResetTimeLimit)
{
static::ResetTimeLimit();
}
}
if (!\is_resource($rRead) || !\is_resource($rWrite)) {
return -1;
}
if ($mResult && $bRewindOnComplete)
{
foreach ($aWrite as $rWriteStream)
{
if (\is_resource($rWriteStream))
{
\rewind($rWriteStream);
}
$iResult = 0;
while (!\feof($rRead)) {
$sBuffer = \fread($rRead, $iBufferLen);
if (false === $sBuffer) {
return -1;
}
if ('' === $sBuffer) {
break;
}
if ($bFixCrLf) {
$sBuffer = \str_replace("\n", "\r\n", \str_replace("\r", '', $sBuffer));
}
$iResult += \strlen($sBuffer);
if (false === \fwrite($rWrite, $sBuffer)) {
return -1;
}
static::ResetTimeLimit();
}
return $mResult;
if ($bRewindOnComplete) {
\rewind($rWrite);
}
return $iResult;
}
public static function Utf7ModifiedToUtf8(string $sStr) : string
@ -1016,8 +694,7 @@ abstract class Utils
public static function ValidateDomain(string $sDomain, bool $bSimple = false) : bool
{
$aMatch = array();
if ($bSimple)
{
if ($bSimple) {
return \preg_match('/.+(\.[a-zA-Z]+)$/', $sDomain, $aMatch) && !empty($aMatch[1]);
}
@ -1034,8 +711,7 @@ abstract class Utils
public static function IdnToUtf8(string $sStr, bool $bLowerIfAscii = false) : string
{
if (\strlen($sStr) && \preg_match('/(^|\.|@)xn--/i', $sStr))
{
if (\strlen($sStr) && \preg_match('/(^|\.|@)xn--/i', $sStr)) {
try
{
$sStr = \SnappyMail\IDN::anyToUtf8($sStr);
@ -1052,14 +728,12 @@ abstract class Utils
$sUser = '';
$sDomain = $sStr;
if (false !== \strpos($sStr, '@'))
{
if (false !== \strpos($sStr, '@')) {
$sUser = static::GetAccountNameFromEmail($sStr);
$sDomain = static::GetDomainFromEmail($sStr);
}
if (\strlen($sDomain) && \preg_match('/[^\x20-\x7E]/', $sDomain))
{
if (\strlen($sDomain) && \preg_match('/[^\x20-\x7E]/', $sDomain)) {
try
{
$sDomain = \SnappyMail\IDN::anyToAscii($sDomain);

View file

@ -1,29 +0,0 @@
<?php
/*
* This file is part of MailSo.
*
* (c) 2014 Usenko Timur
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace MailSo\Base;
/**
* @category MailSo
* @package Base
*/
class Validator
{
public static function RangeInt(int $iNumber, int $iMin = null, int $iMax = null) : bool
{
return (null === $iMin || $iNumber >= $iMin) && (null === $iMax || $iNumber <= $iMax);
}
public static function PortInt(int $iPort) : bool
{
return static::RangeInt($iPort, 0, 65535);
}
}

View file

@ -22,10 +22,8 @@ class Xxtea
{
$aV = self::str2long($sString, true);
$aK = self::str2long($sKey, false);
if (\count($aK) < 4)
{
for ($iIndex = \count($aK); $iIndex < 4; $iIndex++)
{
if (\count($aK) < 4) {
for ($iIndex = \count($aK); $iIndex < 4; $iIndex++) {
$aK[$iIndex] = 0;
}
}
@ -36,12 +34,10 @@ class Xxtea
$iDelta = 0x9E3779B9;
$iQ = \floor(6 + 52 / ($iN + 1));
$iSum = 0;
while (0 < $iQ--)
{
while (0 < $iQ--) {
$iSum = self::int32($iSum + $iDelta);
$iE = $iSum >> 2 & 3;
for ($iPIndex = 0; $iPIndex < $iN; $iPIndex++)
{
for ($iPIndex = 0; $iPIndex < $iN; ++$iPIndex) {
$iY = $aV[$iPIndex + 1];
$iMx = self::int32((($iZ >> 5 & 0x07ffffff) ^ $iY << 2) +
(($iY >> 3 & 0x1fffffff) ^ $iZ << 4)) ^ self::int32(($iSum ^ $iY) + ($aK[$iPIndex & 3 ^ $iE] ^ $iZ));
@ -61,10 +57,8 @@ class Xxtea
$aV = self::str2long($sEncryptedString, false);
$aK = self::str2long($sKey, false);
if (\count($aK) < 4)
{
for ($iIndex = \count($aK); $iIndex < 4; $iIndex++)
{
if (\count($aK) < 4) {
for ($iIndex = \count($aK); $iIndex < 4; ++$iIndex) {
$aK[$iIndex] = 0;
}
}
@ -76,11 +70,9 @@ class Xxtea
$iDelta = 0x9E3779B9;
$iQ = \floor(6 + 52 / ($iN + 1));
$iSum = self::int32($iQ * $iDelta);
while ($iSum != 0)
{
while ($iSum != 0) {
$iE = $iSum >> 2 & 3;
for ($iPIndex = $iN; $iPIndex > 0; $iPIndex--)
{
for ($iPIndex = $iN; $iPIndex > 0; --$iPIndex) {
$iZ = $aV[$iPIndex - 1];
$iMx = self::int32((($iZ >> 5 & 0x07ffffff) ^ $iY << 2) +
(($iY >> 3 & 0x1fffffff) ^ $iZ << 4)) ^ self::int32(($iSum ^ $iY) + ($aK[$iPIndex & 3 ^ $iE] ^ $iZ));
@ -100,33 +92,25 @@ class Xxtea
{
$iLen = \count($aV);
$iN = ($iLen - 1) << 2;
if ($aW)
{
if ($aW) {
$iM = $aV[$iLen - 1];
if (($iM < $iN - 3) || ($iM > $iN))
{
if (($iM < $iN - 3) || ($iM > $iN)) {
return false;
}
$iN = $iM;
}
$aS = array();
for ($iIndex = 0; $iIndex < $iLen; $iIndex++)
{
for ($iIndex = 0; $iIndex < $iLen; ++$iIndex) {
$aS[$iIndex] = \pack('V', $aV[$iIndex]);
}
if ($aW)
{
return \substr(\join('', $aS), 0, $iN);
}
return \join('', $aS);
return $aW ? \substr(\join('', $aS), 0, $iN) : \join('', $aS);
}
private static function str2long(string $sS, string $sW) : array
{
$aV = \unpack('V*', $sS . \str_repeat("\0", (4 - \strlen($sS) % 4) & 3));
$aV = \array_values($aV);
if ($sW)
{
if ($sW) {
$aV[\count($aV)] = \strlen($sS);
}
return $aV;

View file

@ -22,10 +22,7 @@ class CacheClient
*/
private $oDriver;
/**
* @var string
*/
private $sCacheIndex = '';
private string $sCacheIndex = '';
public function Set(string $sKey, string $sValue) : bool
{
@ -56,13 +53,11 @@ class CacheClient
{
$sValue = '';
if ($this->oDriver)
{
if ($this->oDriver) {
$sValue = $this->oDriver->Get($sKey.$this->sCacheIndex);
}
if ($bClearAfterGet)
{
if ($bClearAfterGet) {
$this->Delete($sKey);
}
@ -73,8 +68,7 @@ class CacheClient
{
$iTimer = 0;
$sValue = $this->Get($sKey.'/TIMER');
if (\strlen($sValue) && is_numeric($sValue))
{
if (\strlen($sValue) && \is_numeric($sValue)) {
$iTimer = (int) $sValue;
}
@ -83,8 +77,7 @@ class CacheClient
public function Delete(string $sKey) : self
{
if ($this->oDriver)
{
if ($this->oDriver) {
$this->oDriver->Delete($sKey.$this->sCacheIndex);
}
@ -117,11 +110,9 @@ class CacheClient
public function Verify(bool $bCache = false) : bool
{
if ($this->oDriver)
{
if ($this->oDriver) {
$sCacheData = \gmdate('Y-m-d-H');
if ($bCache && $sCacheData === $this->Get('__verify_key__'))
{
if ($bCache && $sCacheData === $this->Get('__verify_key__')) {
return true;
}

View file

@ -18,19 +18,13 @@ namespace MailSo\Cache\Drivers;
*/
class APCU implements \MailSo\Cache\DriverInterface
{
/**
* @var string
*/
private $sKeyPrefix;
private string $sKeyPrefix;
function __construct(string $sKeyPrefix = '')
{
$this->sKeyPrefix = $sKeyPrefix;
if (!empty($this->sKeyPrefix))
{
$this->sKeyPrefix =
\preg_replace('/[^a-zA-Z0-9_]/', '_', rtrim(trim($this->sKeyPrefix), '\\/')).'/';
}
$this->sKeyPrefix = empty($sKeyPrefix)
? $sKeyPrefix
: \preg_replace('/[^a-zA-Z0-9_]/', '_', \rtrim(\trim($sKeyPrefix), '\\/')).'/';
}
public function Set(string $sKey, string $sValue) : bool
@ -51,12 +45,7 @@ class APCU implements \MailSo\Cache\DriverInterface
public function GC(int $iTimeToClearInHours = 24) : bool
{
if (0 === $iTimeToClearInHours)
{
return \apcu_clear_cache('user');
}
return false;
return (0 === $iTimeToClearInHours) ? \apcu_clear_cache('user') : false;
}
private function generateCachedKey(string $sKey) : string

View file

@ -18,30 +18,20 @@ namespace MailSo\Cache\Drivers;
*/
class File implements \MailSo\Cache\DriverInterface
{
/**
* @var string
*/
private $sCacheFolder;
private string $sCacheFolder;
/**
* @var string
*/
private $sKeyPrefix;
private string $sKeyPrefix = '';
function __construct(string $sCacheFolder, string $sKeyPrefix = '')
{
$this->sCacheFolder = $sCacheFolder;
$this->sCacheFolder = rtrim(trim($this->sCacheFolder), '\\/').'/';
$this->sKeyPrefix = $sKeyPrefix;
if (!empty($this->sKeyPrefix))
{
$this->sKeyPrefix = \str_pad(\preg_replace('/[^a-zA-Z0-9_]/', '_',
rtrim(trim($this->sKeyPrefix), '\\/')), 5, '_');
$this->sCacheFolder = \rtrim(\trim($sCacheFolder), '\\/').'/';
if (!empty($sKeyPrefix)) {
$sKeyPrefix = \str_pad(\preg_replace('/[^a-zA-Z0-9_]/', '_',
\rtrim(\trim($sKeyPrefix), '\\/')), 5, '_');
$this->sKeyPrefix = '__/'.
\substr($this->sKeyPrefix, 0, 2).'/'.\substr($this->sKeyPrefix, 2, 2).'/'.
$this->sKeyPrefix.'/';
\substr($sKeyPrefix, 0, 2).'/'.\substr($sKeyPrefix, 2, 2).'/'.
$sKeyPrefix.'/';
}
}
@ -55,49 +45,41 @@ class File implements \MailSo\Cache\DriverInterface
{
$sValue = '';
$sPath = $this->generateCachedFileName($sKey);
if ('' !== $sPath && \file_exists($sPath))
{
if ('' !== $sPath && \file_exists($sPath)) {
$sValue = \file_get_contents($sPath);
}
return \is_string($sValue) ? $sValue : '';
}
public function Delete(string $sKey) : void
{
$sPath = $this->generateCachedFileName($sKey);
if ('' !== $sPath && \file_exists($sPath))
{
if ('' !== $sPath && \file_exists($sPath)) {
\unlink($sPath);
}
}
public function GC(int $iTimeToClearInHours = 24) : bool
{
if (0 === $iTimeToClearInHours) {
\MailSo\Base\Utils::RecRmDir($this->sCacheFolder);
} else {
\MailSo\Base\Utils::RecTimeDirRemove($this->sCacheFolder, 3600 * $iTimeToClearInHours);
}
\MailSo\Base\Utils::RecTimeDirRemove($this->sCacheFolder, 3600 * $iTimeToClearInHours);
return true;
}
private function generateCachedFileName(string $sKey, bool $bMkDir = false) : string
{
$sFilePath = '';
if (3 < \strlen($sKey))
{
if (3 < \strlen($sKey)) {
$sKeyPath = \sha1($sKey);
$sKeyPath = \substr($sKeyPath, 0, 2).'/'.\substr($sKeyPath, 2, 2).'/'.$sKeyPath;
$sFilePath = $this->sCacheFolder.$this->sKeyPrefix.$sKeyPath;
$dir = \dirname($sFilePath);
if ($bMkDir && !\is_dir($dir) && !\mkdir($dir, 0700, true))
{
$sFilePath = '';
$sFilePath = $this->sCacheFolder . $this->sKeyPrefix
. \substr($sKeyPath, 0, 2) . '/' . \substr($sKeyPath, 2, 2) . '/' . $sKeyPath;
if ($bMkDir) {
$dir = \dirname($sFilePath);
if (!\is_dir($dir) && !\mkdir($dir, 0700, true)) {
\error_log("mkdir({$dir}) failed");
$sFilePath = '';
}
}
}
return $sFilePath;
}
}

View file

@ -18,37 +18,27 @@ namespace MailSo\Cache\Drivers;
*/
class Memcache implements \MailSo\Cache\DriverInterface
{
/**
* @var int
*/
private $iExpire;
private int $iExpire;
/**
* @var \Memcache|null
* @var \Memcache|\Memcached|null
*/
private $oMem;
/**
* @var string
*/
private $sKeyPrefix;
private string $sKeyPrefix;
function __construct(string $sHost = '127.0.0.1', int $iPort = 11211, int $iExpire = 43200, string $sKeyPrefix = '')
{
$this->iExpire = 0 < $iExpire ? $iExpire : 43200;
$this->oMem = \class_exists('Memcache',false) ? new \Memcache : new \Memcached;
if (!$this->oMem->addServer($sHost, \strpos($sHost, ':/') ? 0 : $this->iPort))
{
if (!$this->oMem->addServer($sHost, \strpos($sHost, ':/') ? 0 : $iPort)) {
$this->oMem = null;
}
$this->sKeyPrefix = $sKeyPrefix;
if (!empty($this->sKeyPrefix))
{
$this->sKeyPrefix =
\preg_replace('/[^a-zA-Z0-9_]/', '_', rtrim(trim($this->sKeyPrefix), '\\/')).'/';
}
$this->sKeyPrefix = empty($sKeyPrefix)
? $sKeyPrefix
: \preg_replace('/[^a-zA-Z0-9_]/', '_', \rtrim(\trim($this->sKeyPrefix), '\\/')) . '/';
}
public function Set(string $sKey, string $sValue) : bool
@ -64,19 +54,14 @@ class Memcache implements \MailSo\Cache\DriverInterface
public function Delete(string $sKey) : void
{
if ($this->oMem)
{
$this->oMem->delete($this->generateCachedKey($sKey));
}
$this->oMem && $this->oMem->delete($this->generateCachedKey($sKey));
}
public function GC(int $iTimeToClearInHours = 24) : bool
{
if (0 === $iTimeToClearInHours && $this->oMem)
{
if (0 === $iTimeToClearInHours && $this->oMem) {
return $this->oMem->flush();
}
return false;
}

View file

@ -18,21 +18,14 @@ namespace MailSo\Cache\Drivers;
*/
class Redis implements \MailSo\Cache\DriverInterface
{
/**
* @var int
*/
private $iExpire;
private int $iExpire;
/**
* @var \Memcache|null
* @var \Predis\Client|null
*/
private $oRedis;
/**
* @var string
*/
private $sKeyPrefix;
private string $sKeyPrefix;
function __construct(string $sHost = '127.0.0.1', int $iPort = 6379, int $iExpire = 43200, string $sKeyPrefix = '')
{
@ -49,8 +42,7 @@ class Redis implements \MailSo\Cache\DriverInterface
$this->oRedis->connect();
if (!$this->oRedis->isConnected())
{
if (!$this->oRedis->isConnected()) {
$this->oRedis = null;
}
}
@ -60,18 +52,14 @@ class Redis implements \MailSo\Cache\DriverInterface
unset($oExc);
}
$this->sKeyPrefix = $sKeyPrefix;
if (!empty($this->sKeyPrefix))
{
$this->sKeyPrefix =
\preg_replace('/[^a-zA-Z0-9_]/', '_', rtrim(trim($this->sKeyPrefix), '\\/')).'/';
}
$this->sKeyPrefix = empty($sKeyPrefix)
? $sKeyPrefix
: \preg_replace('/[^a-zA-Z0-9_]/', '_', rtrim(trim($sKeyPrefix), '\\/')) . '/';
}
public function Set(string $sKey, string $sValue) : bool
{
if (!$this->oRedis)
{
if (!$this->oRedis) {
return false;
}
@ -88,16 +76,14 @@ class Redis implements \MailSo\Cache\DriverInterface
public function Delete(string $sKey) : void
{
if ($this->oRedis)
{
if ($this->oRedis) {
$this->oRedis->del($this->generateCachedKey($sKey));
}
}
public function GC(int $iTimeToClearInHours = 24) : bool
{
if (0 === $iTimeToClearInHours && $this->oRedis)
{
if (0 === $iTimeToClearInHours && $this->oRedis) {
return $this->oRedis->flushdb();
}

View file

@ -16,53 +16,5 @@ namespace MailSo;
*/
class Config
{
/**
* @var int
*/
public static $MessageListFastSimpleSearch = true;
/**
* @var int
*/
public static $MessageListCountLimitTrigger = 0;
/**
* @var int
*/
public static $MessageListDateFilter = 0;
/**
* @var string
*/
public static $MessageListPermanentFilter = '';
/**
* @var int
*/
public static $LargeThreadLimit = 50;
/**
* @var bool
*/
public static $MessageAllHeaders = false;
/**
* @var bool
*/
public static $LogSimpleLiterals = false;
/**
* @var bool
*/
public static $CheckNewMessages = true;
/**
* @var string
*/
public static $BoundaryPrefix = '_Part_';
/**
* @var int
*/
public static $ImapTimeout = 300;
public static string $BoundaryPrefix = '_Part_';
}

View file

@ -1,50 +0,0 @@
<?php
/*
* This file is part of MailSo.
*
* (c) 2014 Usenko Timur
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace MailSo;
/**
* @category MailSo
*/
class Hooks
{
/**
* @var array
*/
static $aCallbacks = array();
static public function Run(string $sName, array $aArg = array()) : void
{
if (isset(static::$aCallbacks[$sName]))
{
foreach (static::$aCallbacks[$sName] as $mCallback)
{
$mCallback(...$aArg);
}
}
}
/**
* @param mixed $mCallback
*/
static public function Add(string $sName, $mCallback) : void
{
if (\is_callable($mCallback))
{
if (!isset(static::$aCallbacks[$sName]))
{
static::$aCallbacks[$sName] = array();
}
static::$aCallbacks[$sName][] = $mCallback;
}
}
}

View file

@ -11,76 +11,42 @@
namespace MailSo\Imap;
use MailSo\Mime\ParameterCollection;
/**
* @category MailSo
* @package Imap
*/
class BodyStructure
class BodyStructure implements \JsonSerializable
{
/**
* @var string
*/
private $sContentType;
private string $sContentType;
private array $aContentTypeParams;
private string $sCharset;
private string $sContentID;
private string $sDescription;
private string $sMailEncodingName;
private string $sDisposition;
private string $sFileName;
private string $sLanguage = '';
private string $sLocation = '';
private int $iSize;
private string $sPartID;
/**
* @var string
* \MailSo\Imap\BodyStructure[]
*/
private $sCharset;
/**
* @var array
*/
private $aBodyParams;
/**
* @var string
*/
private $sContentID;
/**
* @var string
*/
private $sDescription;
/**
* @var string
*/
private $sMailEncodingName;
/**
* @var string
*/
private $sDisposition;
/**
* @var string
*/
private $sFileName;
/**
* @var string
*/
private $sLanguage = '';
/**
* @var string
*/
private $sLocation = '';
/**
* @var int
*/
private $iSize;
/**
* @var string
*/
private $sPartID;
/**
* @var array
*/
private $aSubParts;
private array $aSubParts;
public function MailEncodingName() : string
{
@ -92,9 +58,34 @@ class BodyStructure
return $this->sPartID;
}
public function FileName() : string
public function FileName(bool $bCalculateOnEmpty = false) : string
{
return $this->sFileName;
$sFileName = \trim($this->sFileName);
if (\strlen($sFileName) || !$bCalculateOnEmpty) {
return $sFileName;
}
$sIdx = '-' . $this->PartID();
$sMimeType = $this->sContentType;
if ('message/rfc822' === $sMimeType) {
return "message{$sIdx}.eml";
}
if ('text/calendar' === $sMimeType) {
return "calendar{$sIdx}.ics";
}
if ('text/plain' === $sMimeType) {
return "part{$sIdx}.txt";
}
if (\preg_match('@text/(vcard|html|csv|xml|css|asp)@', $sMimeType, $aMatch)
|| \preg_match('@image/(png|jpeg|gif|bmp|cgm|ief|tiff|webp)@', $sMimeType, $aMatch)) {
return "part{$sIdx}.{$aMatch[1]}";
}
if (\strlen($sMimeType)) {
return \str_replace('/', $sIdx.'.', $sMimeType);
}
return ($this->isInline() ? 'inline' : 'part' ) . $sIdx;
}
public function ContentType() : string
@ -102,11 +93,6 @@ class BodyStructure
return $this->sContentType;
}
public function Size() : int
{
return $this->iSize;
}
public function EstimatedSize() : int
{
$fCoefficient = 1;
@ -128,52 +114,27 @@ class BodyStructure
return $this->sCharset;
}
public function ContentID() : string
{
return $this->sContentID;
}
public function ContentLocation() : string
{
return $this->sLocation;
}
public function SubParts() : array
{
return $this->aSubParts;
}
public function IsInline() : bool
public function isInline() : bool
{
return 'inline' === $this->sDisposition || \strlen($this->sContentID);
}
public function IsImage() : bool
public function isText() : bool
{
return 'image' === \MailSo\Base\Utils::ContentTypeType($this->sContentType, $this->sFileName);
}
public function IsArchive() : bool
{
return 'archive' === \MailSo\Base\Utils::ContentTypeType($this->sContentType, $this->sFileName);
}
public function IsPdf() : bool
{
return 'pdf' === \MailSo\Base\Utils::ContentTypeType($this->sContentType, $this->sFileName);
}
public function IsDoc() : bool
{
return 'doc' === \MailSo\Base\Utils::ContentTypeType($this->sContentType, $this->sFileName);
return 'text/html' === $this->sContentType || 'text/plain' === $this->sContentType;
}
public function IsPgpEncrypted() : bool
{
// https://datatracker.ietf.org/doc/html/rfc3156#section-4
return 'multipart/encrypted' === $this->sContentType
&& !empty($this->aBodyParams['protocol'])
&& 'application/pgp-encrypted' === \strtolower(\trim($this->aBodyParams['protocol']))
&& !empty($this->aContentTypeParams['protocol'])
&& 'application/pgp-encrypted' === \strtolower(\trim($this->aContentTypeParams['protocol']))
// The multipart/encrypted body MUST consist of exactly two parts.
&& 2 === \count($this->aSubParts)
&& 'application/pgp-encrypted' === $this->aSubParts[0]->ContentType()
@ -185,8 +146,8 @@ class BodyStructure
{
// https://datatracker.ietf.org/doc/html/rfc3156#section-5
return 'multipart/signed' === $this->sContentType
&& !empty($this->aBodyParams['protocol'])
&& 'application/pgp-signature' === \strtolower(\trim($this->aBodyParams['protocol']))
&& !empty($this->aContentTypeParams['protocol'])
&& 'application/pgp-signature' === \strtolower(\trim($this->aContentTypeParams['protocol']))
// The multipart/signed body MUST consist of exactly two parts.
&& 2 === \count($this->aSubParts)
&& $this->aSubParts[1]->IsPgpSignature();
@ -197,22 +158,21 @@ class BodyStructure
return \in_array($this->sContentType, ['application/pgp-signature', 'application/pkcs7-signature']);
}
public function IsAttachBodyPart() : bool
public function IsAttachment() : bool
{
return 'application/pgp-encrypted' !== $this->sContentType
&& (
'attachment' === $this->sDisposition || (
!\str_starts_with($this->sContentType, 'multipart/')
&& 'text/html' !== $this->sContentType
&& 'text/plain' !== $this->sContentType
&& !$this->isText()
)
);
}
public function IsFlowedFormat() : bool
{
return !empty($this->aBodyParams['format'])
&& 'flowed' === \strtolower(\trim($this->aBodyParams['format']))
return !empty($this->aContentTypeParams['format'])
&& 'flowed' === \strtolower(\trim($this->aContentTypeParams['format']))
&& !\in_array($this->sMailEncodingName, array('base64', 'quoted-printable'));
}
@ -221,8 +181,7 @@ class BodyStructure
$aParts = [];
$gParts = $this->SearchByCallback(function ($oItem) {
return ('text/html' === $oItem->sContentType || 'text/plain' === $oItem->sContentType)
&& !$oItem->IsAttachBodyPart();
return $oItem->isText() && !$oItem->IsAttachment();
});
foreach ($gParts as $oPart) {
$aParts[] = $oPart;
@ -235,7 +194,7 @@ class BodyStructure
if (!$aParts) {
$gEncryptedParts = $this->SearchByContentType('multipart/encrypted');
foreach ($gEncryptedParts as $oPart) {
if ($oPart->IsPgpEncrypted() && $oPart->SubParts()[1]->IsInline()) {
if ($oPart->IsPgpEncrypted() && $oPart->SubParts()[1]->isInline()) {
return array($oPart->SubParts()[1]);
}
}
@ -247,25 +206,19 @@ class BodyStructure
public function SearchCharset() : string
{
$gParts = $this->SearchByCallback(function ($oPart) {
return $oPart->Charset()
&& ('text/html' === $oPart->sContentType || 'text/plain' === $oPart->sContentType)
&& !$oPart->IsAttachBodyPart();
return $oPart->Charset() && $oPart->isText() && !$oPart->IsAttachment();
});
if (!$gParts->valid()) {
$gParts = $this->SearchByCallback(function ($oPart) {
return $oPart->Charset() && $oPart->IsAttachBodyPart();
return $oPart->Charset() && $oPart->IsAttachment();
});
}
return $gParts->valid() ? $gParts->current()->Charset() : '';
}
/**
* @param mixed $fCallback
*/
// public function SearchByCallback($fCallback) : \Generator
public function SearchByCallback($fCallback, $parent = null) : iterable
public function SearchByCallback(callable $fCallback, /*BodyStructure*/ $parent = null) : iterable
{
if ($fCallback($this, $parent)) {
yield $this;
@ -278,8 +231,8 @@ class BodyStructure
public function SearchAttachmentsParts() : iterable
{
return $this->SearchByCallback(function ($oItem, $oParent) {
// return $oItem->IsAttachBodyPart();
return $oItem->IsAttachBodyPart() && (!$oParent || !$oParent->IsPgpEncrypted());
// return $oItem->IsAttachment();
return $oItem->IsAttachment() && (!$oParent || !$oParent->IsPgpEncrypted());
});
}
@ -294,20 +247,15 @@ class BodyStructure
public function GetPartByMimeIndex(string $sMimeIndex) : self
{
$oPart = null;
if (\strlen($sMimeIndex))
{
if ($sMimeIndex === $this->sPartID)
{
if (\strlen($sMimeIndex)) {
if ($sMimeIndex === $this->sPartID) {
$oPart = $this;
}
if (null === $oPart)
{
foreach ($this->aSubParts as /* @var $oSubPart \MailSo\Imap\BodyStructure */ $oSubPart)
{
if (null === $oPart) {
foreach ($this->aSubParts as /* @var $oSubPart \MailSo\Imap\BodyStructure */ $oSubPart) {
$oPart = $oSubPart->GetPartByMimeIndex($sMimeIndex);
if (null !== $oPart)
{
if (null !== $oPart) {
break;
}
}
@ -320,60 +268,43 @@ class BodyStructure
private static function decodeAttrParameter(array $aParams, string $sParamName, string $sCharset) : string
{
$sResult = '';
if (isset($aParams[$sParamName]))
{
if (isset($aParams[$sParamName])) {
$sResult = \MailSo\Base\Utils::DecodeHeaderValue($aParams[$sParamName], $sCharset);
}
else if (isset($aParams[$sParamName.'*']))
{
} else if (isset($aParams[$sParamName.'*'])) {
$aValueParts = \explode("''", $aParams[$sParamName.'*'], 2);
if (2 === \count($aValueParts))
{
if (2 === \count($aValueParts)) {
$sCharset = isset($aValueParts[0]) ? $aValueParts[0] : \MailSo\Base\Enumerations\Charset::UTF_8;
$sResult = \MailSo\Base\Utils::ConvertEncoding(
\urldecode($aValueParts[1]), $sCharset, \MailSo\Base\Enumerations\Charset::UTF_8);
}
else
{
} else {
$sResult = \urldecode($aParams[$sParamName.'*']);
}
}
else
{
} else {
$sCharset = '';
$sCharsetIndex = -1;
$aFileNames = array();
foreach ($aParams as $sName => $sValue)
{
foreach ($aParams as $sName => $sValue) {
$aMatches = array();
if (\preg_match('/^'.\preg_quote($sParamName, '/').'\*([0-9]+)\*$/i', $sName, $aMatches))
{
if (\preg_match('/^'.\preg_quote($sParamName, '/').'\*([0-9]+)\*$/i', $sName, $aMatches)) {
$iIndex = (int) $aMatches[1];
if ($sCharsetIndex < $iIndex && false !== \strpos($sValue, "''"))
{
if ($sCharsetIndex < $iIndex && false !== \strpos($sValue, "''")) {
$aValueParts = \explode("''", $sValue, 2);
if (2 === \count($aValueParts) && \strlen($aValueParts[0]))
{
if (2 === \count($aValueParts) && \strlen($aValueParts[0])) {
$sCharsetIndex = $iIndex;
$sCharset = $aValueParts[0];
$sValue = $aValueParts[1];
}
}
$aFileNames[$iIndex] = $sValue;
}
}
if (\count($aFileNames))
{
if (\count($aFileNames)) {
\ksort($aFileNames, SORT_NUMERIC);
$sResult = \implode(\array_values($aFileNames));
$sResult = \urldecode($sResult);
if (\strlen($sCharset))
{
if (\strlen($sCharset)) {
$sResult = \MailSo\Base\Utils::ConvertEncoding($sResult,
$sCharset, \MailSo\Base\Enumerations\Charset::UTF_8);
}
@ -385,16 +316,15 @@ class BodyStructure
public static function NewInstance(array $aBodyStructure, string $sPartID = '') : ?self
{
if (2 > \count($aBodyStructure))
{
if (2 > \count($aBodyStructure)) {
return null;
}
$sContentTypeMain = '';
$sContentTypeSub = '';
$aSubParts = array();
$aBodyParams = array();
$sName = '';
$aContentTypeParams = array();
$sFileName = '';
$sCharset = ''; // \MailSo\Base\Enumerations\Charset::UTF_8 ?
$sContentID = '';
$sDescription = '';
@ -402,20 +332,16 @@ class BodyStructure
$iSize = 0;
$iExtraItemPos = 0; // list index of items which have no well-established position (such as 0, 1, 5, etc).
if (\is_array($aBodyStructure[0]))
{
if (\is_array($aBodyStructure[0])) {
// Process multipart body structure
$sContentTypeMain = 'multipart';
$sContentTypeSub = 'mixed'; // primary default
$sSubPartIDPrefix = '';
if (!\strlen($sPartID) || '.' === $sPartID[\strlen($sPartID) - 1])
{
if (!\strlen($sPartID) || '.' === $sPartID[\strlen($sPartID) - 1]) {
// This multi-part is root part of message.
$sSubPartIDPrefix = $sPartID;
$sPartID .= 'TEXT';
}
else if (\strlen($sPartID))
{
} else if (\strlen($sPartID)) {
// This multi-part is a part of another multi-part.
$sSubPartIDPrefix = $sPartID.'.';
}
@ -427,18 +353,15 @@ class BodyStructure
("text" "plain" ("charset" "utf-8") )
("text" "html" )
*/
while ($iExtraItemPos < \count($aBodyStructure) && \is_array($aBodyStructure[$iExtraItemPos]))
{
while ($iExtraItemPos < \count($aBodyStructure) && \is_array($aBodyStructure[$iExtraItemPos])) {
$oPart = self::NewInstance($aBodyStructure[$iExtraItemPos], $sSubPartIDPrefix.$iIndex);
if (!$oPart)
{
if (!$oPart) {
return null;
}
// For multipart, we have no charset info in the part itself. Thus,
// obtain charset from nested parts.
if (!$sCharset)
{
if (!$sCharset) {
$sCharset = $oPart->Charset();
}
@ -451,26 +374,20 @@ class BodyStructure
* Now process the subparts containter like:
"alternative" ("boundary" "--boundary_id")
*/
if ($iExtraItemPos < \count($aBodyStructure))
{
if (!\is_string($aBodyStructure[$iExtraItemPos]))
{
if ($iExtraItemPos < \count($aBodyStructure)) {
if (!\is_string($aBodyStructure[$iExtraItemPos])) {
return null;
}
$sContentTypeSub = \strtolower($aBodyStructure[$iExtraItemPos]);
++$iExtraItemPos;
if ($iExtraItemPos < \count($aBodyStructure) && \is_array($aBodyStructure[$iExtraItemPos]))
{
$aBodyParams = self::getKeyValueListFromArrayList($aBodyStructure[$iExtraItemPos]);
if ($iExtraItemPos < \count($aBodyStructure) && \is_array($aBodyStructure[$iExtraItemPos])) {
$aContentTypeParams = self::getKeyValueListFromArrayList($aBodyStructure[$iExtraItemPos]);
}
}
}
else if (\is_string($aBodyStructure[0]))
{
} else if (\is_string($aBodyStructure[0])) {
// Process simple (singlepart) body structure
if (7 > \count($aBodyStructure) || !\is_string($aBodyStructure[1]))
{
if (7 > \count($aBodyStructure) || !\is_string($aBodyStructure[1])) {
return null;
}
@ -478,39 +395,33 @@ class BodyStructure
$sContentTypeSub = \strtolower($aBodyStructure[1]);
$aBodyParamList = $aBodyStructure[2];
if (\is_array($aBodyParamList))
{
$aBodyParams = self::getKeyValueListFromArrayList($aBodyParamList);
if (isset($aBodyParams['charset']))
{
$sCharset = $aBodyParams['charset'];
if (\is_array($aBodyParamList)) {
$aContentTypeParams = self::getKeyValueListFromArrayList($aBodyParamList);
if (isset($aContentTypeParams['charset'])) {
$sCharset = $aContentTypeParams['charset'];
}
$sFileName = self::decodeAttrParameter($aContentTypeParams, 'name', $sCharset);
if ($sFileName) {
$aContentTypeParams['name'] = $sFileName;
}
$sName = self::decodeAttrParameter($aBodyParams, 'name', $sCharset);
}
if (null !== $aBodyStructure[3])
{
if (!\is_string($aBodyStructure[3]))
{
if (null !== $aBodyStructure[3]) {
if (!\is_string($aBodyStructure[3])) {
return null;
}
$sContentID = $aBodyStructure[3];
}
if (null !== $aBodyStructure[4])
{
if (!\is_string($aBodyStructure[4]))
{
if (null !== $aBodyStructure[4]) {
if (!\is_string($aBodyStructure[4])) {
return null;
}
$sDescription = $aBodyStructure[4];
}
if (null !== $aBodyStructure[5])
{
if (!\is_string($aBodyStructure[5]))
{
if (null !== $aBodyStructure[5]) {
if (!\is_string($aBodyStructure[5])) {
return null;
}
$sMailEncodingName = $aBodyStructure[5];
@ -518,24 +429,20 @@ class BodyStructure
$iSize = \is_numeric($aBodyStructure[6]) ? (int) $aBodyStructure[6] : -1;
if (!\strlen($sPartID) || '.' === $sPartID[\strlen($sPartID) - 1])
{
if (!\strlen($sPartID) || '.' === $sPartID[\strlen($sPartID) - 1]) {
// This is the only sub-part of the message (otherwise, it would be
// one of sub-parts of a multi-part, and partID would already be fully set up).
$sPartID .= '1';
}
$iExtraItemPos = 7;
if ('text' === $sContentTypeMain)
{
if ('text' === $sContentTypeMain) {
/**
* A body type of type TEXT contains, immediately after the basic
* fields, the size of the body in text lines.
*/
++$iExtraItemPos;
}
else if ('message' === $sContentTypeMain && 'rfc822' === $sContentTypeSub)
{
} else if ('message' === $sContentTypeMain && 'rfc822' === $sContentTypeSub) {
/**
* A body type of type MESSAGE and subtype RFC822 contains,
* immediately after the basic fields, the envelope structure,
@ -543,9 +450,7 @@ class BodyStructure
*/
$iExtraItemPos += 3;
}
}
else
{
} else {
return null;
}
@ -553,20 +458,15 @@ class BodyStructure
++$iExtraItemPos;
$sDisposition = '';
$sFileName = '';
if ($iExtraItemPos < \count($aBodyStructure))
{
if ($iExtraItemPos < \count($aBodyStructure)) {
$aDispList = $aBodyStructure[$iExtraItemPos];
if (\is_array($aDispList) && 1 < \count($aDispList))
{
if (!\is_string($aDispList[0]))
{
if (\is_array($aDispList) && 1 < \count($aDispList)) {
if (!\is_string($aDispList[0])) {
return null;
}
$sDisposition = $aDispList[0];
if (\is_array($aDispList[1]))
{
if (\is_array($aDispList[1])) {
$aDispositionParams = self::getKeyValueListFromArrayList($aDispList[1]);
$sFileName = self::decodeAttrParameter($aDispositionParams, 'filename', $sCharset);
}
@ -575,32 +475,27 @@ class BodyStructure
}
$oStructure = new self;
$oStructure->sContentType = \strtolower($sContentTypeMain.'/'.$sContentTypeSub);
$oStructure->sContentType = \strtolower(\trim($sContentTypeMain.'/'.$sContentTypeSub));
$oStructure->aContentTypeParams = $aContentTypeParams;
$oStructure->sCharset = $sCharset;
$oStructure->aBodyParams = $aBodyParams;
$oStructure->sContentID = $sContentID;
$oStructure->sContentID = \trim($sContentID);
$oStructure->sDescription = $sDescription;
$oStructure->sMailEncodingName = \strtolower($sMailEncodingName);
$oStructure->sDisposition = \strtolower($sDisposition);
$oStructure->sFileName = \MailSo\Base\Utils::Utf8Clear($sFileName ?: $sName);
$oStructure->sFileName = \MailSo\Base\Utils::Utf8Clear($sFileName);
$oStructure->iSize = $iSize;
$oStructure->sPartID = $sPartID;
$oStructure->aSubParts = $aSubParts;
if ($iExtraItemPos < \count($aBodyStructure))
{
if (\is_array($aBodyStructure[$iExtraItemPos]))
{
if ($iExtraItemPos < \count($aBodyStructure)) {
if (\is_array($aBodyStructure[$iExtraItemPos])) {
$oStructure->sLanguage = \implode(',', $aBodyStructure[$iExtraItemPos]);
}
else if (\is_string($aBodyStructure[$iExtraItemPos]))
{
} else if (\is_string($aBodyStructure[$iExtraItemPos])) {
$oStructure->sLanguage = $aBodyStructure[$iExtraItemPos];
}
++$iExtraItemPos;
if ($iExtraItemPos < \count($aBodyStructure) && \is_string($aBodyStructure[$iExtraItemPos]))
{
if ($iExtraItemPos < \count($aBodyStructure) && \is_string($aBodyStructure[$iExtraItemPos])) {
$oStructure->sLocation = $aBodyStructure[$iExtraItemPos];
}
}
@ -616,12 +511,9 @@ class BodyStructure
{
$aDict = array();
$iLen = \count($aList);
if (0 === ($iLen % 2))
{
for ($iIndex = 0; $iIndex < $iLen; $iIndex += 2)
{
if (\is_string($aList[$iIndex]) && \is_string($aList[$iIndex + 1]))
{
if (0 === ($iLen % 2)) {
for ($iIndex = 0; $iIndex < $iLen; $iIndex += 2) {
if (\is_string($aList[$iIndex]) && \is_string($aList[$iIndex + 1])) {
$aDict[\strtolower($aList[$iIndex])] = $aList[$iIndex + 1];
}
}
@ -629,4 +521,19 @@ class BodyStructure
return $aDict;
}
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return array(
'mimeIndex' => $this->sPartID,
'mimeType' => $this->sContentType,
// 'mimeTypeParams' => $this->aContentTypeParams,
'fileName' => \MailSo\Base\Utils::SecureFileName($this->FileName(true)),
'estimatedSize' => $this->EstimatedSize(),
'cId' => $this->sContentID,
'contentLocation' => $this->sLocation,
'isInline' => $this->isInline()
);
}
}

View file

@ -24,7 +24,7 @@ trait ACL
*/
public function ACLAllow(string $sFolderName, string $command) : bool
{
if ($this->IsSupported('ACL') || $this->IsSupported('RIGHTS=texk')) {
if ($this->hasCapability('ACL') || $this->CapabilityValue('RIGHTS')) {
if ('MYRIGHTS' === $command) {
return true;
}
@ -128,7 +128,8 @@ trait ACL
public function FolderMyRights(string $sFolderName) : ?ACLResponse
{
// if ($this->IsSupported('ACL')) {
// if ($this->ACLAllow($sFolderName, 'MYRIGHTS')) {
// if ($this->hasCapability('ACL')) {
$oResponses = $this->SendRequestGetResponse('MYRIGHTS', array($this->EscapeString($sFolderName)));
foreach ($oResponses as $oResponse) {
if (\MailSo\Imap\Enumerations\ResponseType::UNTAGGED === $oResponse->ResponseType

View file

@ -13,10 +13,12 @@
namespace MailSo\Imap\Commands;
use MailSo\Imap\Folder;
use MailSo\Imap\FolderCollection;
use MailSo\Imap\FolderInformation;
use MailSo\Imap\SequenceSet;
use MailSo\Imap\Enumerations\FolderStatus;
use MailSo\Imap\Enumerations\FolderResponseStatus;
use MailSo\Imap\Enumerations\MessageFlag;
use MailSo\Imap\Enumerations\StoreAction;
/**
* @category MailSo
@ -24,19 +26,33 @@ use MailSo\Imap\Enumerations\FolderResponseStatus;
*/
trait Folders
{
/**
* @throws \InvalidArgumentException
*/
public function FolderClear(string $sFolderFullName) : void
{
if (0 < $this->FolderSelect($sFolderFullName)->MESSAGES) {
$this->MessageStoreFlag(new SequenceSet('1:*', false),
array(MessageFlag::DELETED),
StoreAction::ADD_FLAGS_SILENT
);
$this->FolderExpunge();
}
}
/**
* @throws \InvalidArgumentException
* @throws \MailSo\RuntimeException
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
public function FolderCreate(string $sFolderName) : self
public function FolderCreate(string $sFolderName, bool $bSubscribe = false) : void
{
$this->SendRequestGetResponse('CREATE', array(
$this->EscapeFolderName($sFolderName)
// , ['(USE (\Drafts \Sent))'] RFC 6154
));
return $this;
$bSubscribe && $this->FolderSubscribe($sFolderName);
}
/**
@ -45,23 +61,34 @@ trait Folders
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
public function FolderDelete(string $sFolderName) : self
public function FolderDelete(string $sFolderName) : void
{
if (!$sFolderName || 'INBOX' === $sFolderName) {
throw new \InvalidArgumentException;
}
$oInfo = $this->hasCapability('IMAP4rev2')
? $this->FolderExamine($sFolderName)
: $this->FolderStatus($sFolderName);
if ($oInfo->MESSAGES) {
throw new \MailSo\Mail\Exceptions\NonEmptyFolder;
}
$this->FolderUnsubscribe($sFolderName);
$this->FolderUnselect();
// Uncomment will work issue #124 ?
// $this->selectOrExamineFolder($sFolderName, true);
$this->SendRequestGetResponse('DELETE',
array($this->EscapeFolderName($sFolderName)));
$this->SendRequestGetResponse('DELETE', [$this->EscapeFolderName($sFolderName)]);
// $this->FolderCheck();
// $this->FolderUnselect();
// Will this workaround solve Dovecot issue #124 ?
try {
$this->FolderRename($sFolderName, "{$sFolderName}-dummy");
$this->FolderRename("{$sFolderName}-dummy", $sFolderName);
} catch (\Throwable $e) {
} catch (\Throwable $oException) {
$this->writeLogException($oException, \LOG_WARNING, false);
}
return $this;
}
/**
@ -70,11 +97,9 @@ trait Folders
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
public function FolderSubscribe(string $sFolderName) : self
public function FolderSubscribe(string $sFolderName) : void
{
$this->SendRequestGetResponse('SUBSCRIBE',
array($this->EscapeFolderName($sFolderName)));
return $this;
$this->SendRequestGetResponse('SUBSCRIBE', [$this->EscapeFolderName($sFolderName)]);
}
/**
@ -83,11 +108,9 @@ trait Folders
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
public function FolderUnsubscribe(string $sFolderName) : self
public function FolderUnsubscribe(string $sFolderName) : void
{
$this->SendRequestGetResponse('UNSUBSCRIBE',
array($this->EscapeFolderName($sFolderName)));
return $this;
$this->SendRequestGetResponse('UNSUBSCRIBE', [$this->EscapeFolderName($sFolderName)]);
}
/**
@ -96,12 +119,44 @@ trait Folders
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
public function FolderRename(string $sOldFolderName, string $sNewFolderName) : self
public function FolderRename(string $sOldFolderName, string $sNewFolderName) : void
{
$this->SendRequestGetResponse('RENAME', array(
$this->SendRequestGetResponse('RENAME', [
$this->EscapeFolderName($sOldFolderName),
$this->EscapeFolderName($sNewFolderName)));
return $this;
$this->EscapeFolderName($sNewFolderName)
]);
}
private function FolderStatusItems() : array
{
$aStatusItems = array(
FolderStatus::MESSAGES,
FolderStatus::UNSEEN,
FolderStatus::UIDNEXT,
FolderStatus::UIDVALIDITY
);
// RFC 4551
if ($this->hasCapability('CONDSTORE')) {
$aStatusItems[] = FolderStatus::HIGHESTMODSEQ;
}
// RFC 7889
if ($this->hasCapability('APPENDLIMIT')) {
$aStatusItems[] = FolderStatus::APPENDLIMIT;
}
// RFC 8474
if ($this->hasCapability('OBJECTID')) {
$aStatusItems[] = FolderStatus::MAILBOXID;
/*
} else if ($this->hasCapability('X-DOVECOT')) {
$aStatusItems[] = 'X-GUID';
*/
}
/* // STATUS SIZE can take a significant amount of time, therefore not active
if ($this->hasCapability('IMAP4rev2')) {
$aStatusItems[] = FolderStatus::SIZE;
}
*/
return $aStatusItems;
}
/**
@ -112,48 +167,26 @@ trait Folders
*
* https://datatracker.ietf.org/doc/html/rfc9051#section-6.3.11
*/
public function FolderStatus(string $sFolderName) : FolderInformation
public function FolderStatus(string $sFolderName, bool $bSelect = false) : FolderInformation
{
$aStatusItems = array(
FolderResponseStatus::MESSAGES,
FolderResponseStatus::UNSEEN,
FolderResponseStatus::UIDNEXT,
FolderResponseStatus::UIDVALIDITY
);
if ($this->IsSupported('CONDSTORE')) {
$aStatusItems[] = FolderResponseStatus::HIGHESTMODSEQ;
}
if ($this->IsSupported('APPENDLIMIT')) {
$aStatusItems[] = FolderResponseStatus::APPENDLIMIT;
}
if ($this->IsSupported('OBJECTID')) {
$aStatusItems[] = FolderResponseStatus::MAILBOXID;
/*
} else if ($this->IsSupported('X-DOVECOT')) {
$aStatusItems[] = 'X-GUID';
*/
}
/* // STATUS SIZE can take a significant amount of time, therefore not active
if ($this->IsSupported('IMAP4rev2')) {
$aStatusItems[] = FolderResponseStatus::SIZE;
}
*/
$oFolderInfo = $this->oCurrentFolderInfo;
$bReselect = false;
$bWritable = false;
if ($oFolderInfo && $sFolderName === $oFolderInfo->FolderName) {
if ($oFolderInfo && $sFolderName === $oFolderInfo->FullName) {
if ($oFolderInfo->hasStatus) {
return $oFolderInfo;
}
/**
* There's a long standing IMAP CLIENTBUG where STATUS command is executed
* after SELECT/EXAMINE on same folder (it should not).
* So we must unselect the folder to be able to get the APPENDLIMIT and UNSEEN.
*/
/*
if ($this->IsSupported('ESEARCH')) {
$aResult = $oFolderInfo->getStatusItems();
// SELECT or EXAMINE command then UNSEEN is the message sequence number of the first unseen message
$aResult['UNSEEN'] = $this->MessageSimpleESearch('UNSEEN', ['COUNT'])['COUNT'];
return $aResult;
if ($this->hasCapability('ESEARCH') && !isset($oFolderInfo->UNSEEN)) {
$oFolderInfo->UNSEEN = $this->MessageSimpleESearch('UNSEEN', ['COUNT'])['COUNT'];
}
return $oFolderInfo;
*/
$bWritable = $oFolderInfo->IsWritable;
$bReselect = true;
@ -161,30 +194,49 @@ trait Folders
}
$oInfo = new FolderInformation($sFolderName, false);
$this->SendRequest('STATUS', array($this->EscapeFolderName($sFolderName), $aStatusItems));
$this->SendRequest('STATUS', array($this->EscapeFolderName($sFolderName), $this->FolderStatusItems()));
foreach ($this->yieldUntaggedResponses() as $oResponse) {
$oInfo->setStatusFromResponse($oResponse);
}
if ($bReselect) {
$this->selectOrExamineFolder($sFolderName, $bWritable, false);
// $this->oCurrentFolderInfo->UNSEEN = $oInfo->UNSEEN;
if ($bReselect || $bSelect) {
// Don't use FolderExamine, else PERMANENTFLAGS is empty in Dovecot
$oFolderInfo = $this->selectOrExamineFolder($sFolderName, $bSelect || $bWritable, false);
$oFolderInfo->MESSAGES = \max(0, $oFolderInfo->MESSAGES, $oInfo->MESSAGES);
// SELECT or EXAMINE command then UNSEEN is the message sequence number of the first unseen message.
// And deprecated in IMAP4rev2, so we set it to the amount of unseen messages
$oFolderInfo->UNSEEN = \max(0, $oInfo->UNSEEN);
$oFolderInfo->UIDNEXT = \max(0, $oFolderInfo->UIDNEXT, $oInfo->UIDNEXT);
$oFolderInfo->UIDVALIDITY = \max(0, $oFolderInfo->UIDVALIDITY, $oInfo->UIDVALIDITY);
$oFolderInfo->HIGHESTMODSEQ = \max(0, $oInfo->HIGHESTMODSEQ);
$oFolderInfo->APPENDLIMIT = \max(0, $oFolderInfo->APPENDLIMIT, $oInfo->APPENDLIMIT);
$oFolderInfo->MAILBOXID = $oFolderInfo->MAILBOXID ?: $oInfo->MAILBOXID;
// $oFolderInfo->SIZE = \max($oFolderInfo->SIZE, $oInfo->SIZE);
// $oFolderInfo->RECENT = \max(0, $oFolderInfo->RECENT, $oInfo->RECENT);
$oFolderInfo->hasStatus = $oInfo->hasStatus;
$oFolderInfo->generateETag($this);
return $oFolderInfo;
}
$oInfo->generateETag($this);
return $oInfo;
}
public function FolderStatusAndSelect(string $sFolderName) : FolderInformation
{
return $this->FolderStatus($sFolderName, true);
}
/**
* @throws \MailSo\RuntimeException
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
public function FolderCheck() : self
public function FolderCheck() : void
{
if ($this->IsSelected()) {
$this->SendRequestGetResponse('CHECK');
}
return $this;
}
/**
@ -209,25 +261,21 @@ trait Folders
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
public function FolderUnselect() : self
public function FolderUnselect() : void
{
if ($this->IsSelected()) {
if ($this->IsSupported('UNSELECT')) {
if ($this->hasCapability('UNSELECT')) {
$this->SendRequestGetResponse('UNSELECT');
$this->oCurrentFolderInfo = null;
} else {
try {
$this->SendRequestGetResponse('SELECT', ['""']);
// * OK [CLOSED] Previous mailbox closed.
// 3 NO [CANNOT] Invalid mailbox name: Name is empty
} catch (\MailSo\Imap\Exceptions\NegativeResponseException $e) {
if ('NO' === $e->GetResponseStatus()) {
$this->oCurrentFolderInfo = null;
}
} catch (\MailSo\Imap\Exceptions\NegativeResponseException $oException) {
}
}
}
return $this;
$this->oCurrentFolderInfo = null;
}
/**
@ -243,7 +291,7 @@ trait Folders
$sCmd = 'EXPUNGE';
$aArguments = array();
if ($oUidRange && \count($oUidRange) && $oUidRange->UID && $this->IsSupported('UIDPLUS')) {
if ($oUidRange && \count($oUidRange) && $oUidRange->UID && $this->hasCapability('UIDPLUS')) {
$sCmd = 'UID '.$sCmd;
$aArguments = array((string) $oUidRange);
}
@ -272,9 +320,9 @@ trait Folders
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
public function FolderSelect(string $sFolderName, bool $bReSelectSameFolders = false) : FolderInformation
public function FolderSelect(string $sFolderName, bool $bForceReselect = false) : FolderInformation
{
return $this->selectOrExamineFolder($sFolderName, true, $bReSelectSameFolders);
return $this->selectOrExamineFolder($sFolderName, true, $bForceReselect);
}
/**
@ -288,9 +336,9 @@ trait Folders
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
public function FolderExamine(string $sFolderName, bool $bReSelectSameFolders = false) : FolderInformation
public function FolderExamine(string $sFolderName, bool $bForceReselect = false) : FolderInformation
{
return $this->selectOrExamineFolder($sFolderName, $this->__FORCE_SELECT_ON_EXAMINE__, $bReSelectSameFolders);
return $this->selectOrExamineFolder($sFolderName, $this->Settings->force_select, $bForceReselect);
}
/**
@ -302,18 +350,17 @@ trait Folders
* REQUIRED IMAP4rev2 untagged responses: FLAGS, EXISTS, LIST
* REQUIRED IMAP4rev2 OK untagged responses: PERMANENTFLAGS, UIDNEXT, UIDVALIDITY
*/
protected function selectOrExamineFolder(string $sFolderName, bool $bIsWritable, bool $bReSelectSameFolders) : FolderInformation
protected function selectOrExamineFolder(string $sFolderName, bool $bIsWritable, bool $bForceReselect) : FolderInformation
{
if (!$bReSelectSameFolders
if (!$bForceReselect
&& $this->oCurrentFolderInfo
&& $sFolderName === $this->oCurrentFolderInfo->FolderName
&& $bIsWritable === $this->oCurrentFolderInfo->IsWritable
&& $sFolderName === $this->oCurrentFolderInfo->FullName
&& ($bIsWritable === $this->oCurrentFolderInfo->IsWritable || $this->oCurrentFolderInfo->IsWritable)
) {
return $this->oCurrentFolderInfo;
}
if (!\strlen(\trim($sFolderName)))
{
if (!\strlen(\trim($sFolderName))) {
throw new \InvalidArgumentException;
}
@ -321,7 +368,7 @@ trait Folders
/*
// RFC 5162
if ($this->IsSupported('QRESYNC')) {
if ($this->hasCapability('QRESYNC')) {
$this->Enable(['QRESYNC', 'CONDSTORE']);
- the last known UIDVALIDITY,
- the last known modification sequence,
@ -332,7 +379,7 @@ trait Folders
}
// RFC 4551
if ($this->IsSupported('CONDSTORE')) {
if ($this->hasCapability('CONDSTORE')) {
$aSelectParams[] = 'CONDSTORE';
}
@ -365,9 +412,9 @@ trait Folders
$oResult->PermanentFlags = \array_map('\\MailSo\\Base\\Utils::Utf7ModifiedToUtf8', $oResponse->OptionalResponse[1]);
}
} else if ('READ-ONLY' === $key) {
// $oResult->IsWritable = false;
$oResult->IsWritable = false;
} else if ('READ-WRITE' === $key) {
// $oResult->IsWritable = true;
$oResult->IsWritable = true;
} else if ('NOMODSEQ' === $key) {
// https://datatracker.ietf.org/doc/html/rfc4551#section-3.1.2
}
@ -377,11 +424,20 @@ trait Folders
else if (\count($oResponse->ResponseList) > 2
&& 'FLAGS' === $oResponse->ResponseList[1]
&& \is_array($oResponse->ResponseList[2])) {
$oResult->Flags = \array_map('\\MailSo\\Base\\Utils::Utf7ModifiedToUtf8', $oResponse->ResponseList[2]);
// These could be not permanent, so we don't use them
// $oResult->Flags = \array_map('\\MailSo\\Base\\Utils::Utf7ModifiedToUtf8', $oResponse->ResponseList[2]);
}
}
}
// SELECT or EXAMINE command then UNSEEN is the message sequence number of the first unseen message.
// IMAP4rev2 deprecated
$oResult->UNSEEN = null;
/*
if ($this->hasCapability('ESEARCH')) {
$oResult->UNSEEN = $this->MessageSimpleESearch('UNSEEN', ['COUNT'])['COUNT'];
}
*/
$this->oCurrentFolderInfo = $oResult;
return $oResult;
@ -392,7 +448,7 @@ trait Folders
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
public function FolderList(string $sParentFolderName, string $sListPattern, bool $bIsSubscribeList = false, bool $bUseListStatus = false) : array
public function FolderList(string $sParentFolderName, string $sListPattern, bool $bIsSubscribeList = false, bool $bUseListStatus = false) : FolderCollection
{
$sCmd = 'LIST';
@ -402,7 +458,7 @@ trait Folders
if ($bIsSubscribeList) {
// IMAP4rev2 deprecated
$sCmd = 'LSUB';
} else if ($this->IsSupported('LIST-EXTENDED')) {
} else if ($this->hasCapability('LIST-EXTENDED')) {
// RFC 5258
$aReturnParams[] = 'SUBSCRIBED';
// $aReturnParams[] = 'CHILDREN';
@ -412,7 +468,7 @@ trait Folders
// $aParameters[0] = '()';
}
// RFC 6154
if ($this->IsSupported('SPECIAL-USE')) {
if ($this->hasCapability('SPECIAL-USE')) {
$aReturnParams[] = 'SPECIAL-USE';
}
}
@ -422,32 +478,9 @@ trait Folders
// $aParameters[] = $this->EscapeString(\strlen(\trim($sListPattern)) ? $sListPattern : '*');
// RFC 5819
if ($bUseListStatus && !$bIsSubscribeList && $this->IsSupported('LIST-STATUS'))
{
$aL = array(
FolderStatus::MESSAGES,
FolderStatus::UNSEEN,
FolderStatus::UIDNEXT
);
// RFC 4551
if ($this->IsSupported('CONDSTORE')) {
$aL[] = FolderStatus::HIGHESTMODSEQ;
}
// RFC 7889
if ($this->IsSupported('APPENDLIMIT')) {
$aL[] = FolderStatus::APPENDLIMIT;
}
// RFC 8474
if ($this->IsSupported('OBJECTID')) {
$aL[] = FolderStatus::MAILBOXID;
/*
} else if ($this->IsSupported('X-DOVECOT')) {
$aL[] = 'X-GUID';
*/
}
if ($bUseListStatus && !$bIsSubscribeList && $this->hasCapability('LIST-STATUS')) {
$aReturnParams[] = 'STATUS';
$aReturnParams[] = $aL;
$aReturnParams[] = $this->FolderStatusItems();
}
/*
// RFC 5738
@ -460,98 +493,94 @@ trait Folders
$aParameters[] = $aReturnParams;
}
/*
$bPassthru = false;
$aReturn = array();
if ($bPassthru) {
$this->SendRequest($sCmd, $aParameters);
$this->streamResponse();
return [];
}
*/
// RFC 5464
$bMetadata = !$bIsSubscribeList && $this->IsSupported('METADATA');
$aMetadata = null;
if ($bMetadata) {
// Dovecot supports fetching all METADATA at once
$aMetadata = $this->getAllMetadata();
}
$bMetadata = !$bIsSubscribeList && $this->hasCapability('METADATA');
// Dovecot supports fetching all METADATA at once
$aMetadata = $bMetadata ? $this->getAllMetadata() : null;
$this->SendRequest($sCmd, $aParameters);
if ($bPassthru) {
$this->streamResponse();
} else {
$sDelimiter = '';
$bInbox = false;
foreach ($this->yieldUntaggedResponses() as $oResponse) {
if ('STATUS' === $oResponse->StatusOrIndex && isset($oResponse->ResponseList[2])) {
$sFullName = $this->toUTF8($oResponse->ResponseList[2]);
if (!isset($aReturn[$sFullName])) {
$aReturn[$sFullName] = new Folder($sFullName);
}
$aReturn[$sFullName]->setStatusFromResponse($oResponse);
}
else if ($sCmd === $oResponse->StatusOrIndex && 5 === \count($oResponse->ResponseList)) {
try
{
$sFullName = $this->toUTF8($oResponse->ResponseList[4]);
/**
* $oResponse->ResponseList[0] = *
* $oResponse->ResponseList[1] = LIST (all) | LSUB (subscribed)
* $oResponse->ResponseList[2] = Flags
* $oResponse->ResponseList[3] = Delimiter
* $oResponse->ResponseList[4] = FullName
*/
if (!isset($aReturn[$sFullName])) {
$oFolder = new Folder($sFullName,
$oResponse->ResponseList[3], $oResponse->ResponseList[2]);
$aReturn[$sFullName] = $oFolder;
} else {
$oFolder = $aReturn[$sFullName];
$oFolder->setDelimiter($oResponse->ResponseList[3]);
$oFolder->setFlags($oResponse->ResponseList[2]);
}
if ($oFolder->IsInbox()) {
$bInbox = true;
}
if (!$sDelimiter) {
$sDelimiter = $oFolder->Delimiter();
}
if (isset($aMetadata[$oResponse->ResponseList[4]])) {
$oFolder->SetAllMetadata($aMetadata[$oResponse->ResponseList[4]]);
}
$aReturn[$sFullName] = $oFolder;
}
catch (\InvalidArgumentException $oException)
{
$this->writeLogException($oException, \LOG_WARNING, false);
}
catch (\Throwable $oException)
{
$this->writeLogException($oException, \LOG_WARNING, false);
}
$aFolders = array();
$oFolderCollection = new FolderCollection;
$sDelimiter = '';
$bInbox = false;
foreach ($this->yieldUntaggedResponses() as $oResponse) {
if ('STATUS' === $oResponse->StatusOrIndex && isset($oResponse->ResponseList[2])) {
$sFullName = $this->toUTF8($oResponse->ResponseList[2]);
if (!isset($oFolderCollection[$sFullName])) {
$oFolderCollection[$sFullName] = new Folder($sFullName);
}
$oFolderCollection[$sFullName]->setStatusFromResponse($oResponse);
$oFolderCollection[$sFullName]->generateETag($this);
}
else if ($sCmd === $oResponse->StatusOrIndex && 5 === \count($oResponse->ResponseList)) {
try
{
$sFullName = $this->toUTF8($oResponse->ResponseList[4]);
if (!$bInbox && !$sParentFolderName && !isset($aReturn['INBOX'])) {
$aReturn['INBOX'] = new Folder('INBOX', $sDelimiter);
/**
* $oResponse->ResponseList[0] = *
* $oResponse->ResponseList[1] = LIST (all) | LSUB (subscribed)
* $oResponse->ResponseList[2] = Attribute flags
* $oResponse->ResponseList[3] = Delimiter
* $oResponse->ResponseList[4] = FullName
*/
if (isset($oFolderCollection[$sFullName])) {
$oFolder = $oFolderCollection[$sFullName];
$oFolder->setDelimiter($oResponse->ResponseList[3]);
$oFolder->setAttributes($oResponse->ResponseList[2]);
} else {
$oFolder = new Folder($sFullName, $oResponse->ResponseList[3], $oResponse->ResponseList[2]);
$oFolderCollection[$sFullName] = $oFolder;
}
$bInbox = $bInbox || $oFolder->IsInbox();
if (!$sDelimiter) {
$sDelimiter = $oFolder->Delimiter();
}
if (isset($aMetadata[$oResponse->ResponseList[4]])) {
$oFolder->SetAllMetadata($aMetadata[$oResponse->ResponseList[4]]);
}
}
catch (\Throwable $oException)
{
$this->writeLogException($oException, \LOG_WARNING, false);
}
}
}
// $iOptimizationLimit = $this->Settings->folder_list_limit;
// $oFolderCollection->Optimized = 10 < $iOptimizationLimit && $oFolderCollection->count() > $iOptimizationLimit;
// RFC 5464
if ($bMetadata && !$aMetadata /*&& 50 < \count($aReturn)*/) {
foreach ($aReturn as $oFolder) {
// if (2 > \substr_count($oFolder->FullName(), $oFolder->Delimiter()))
if ($bMetadata && !$aMetadata /*&& 50 < $oFolderCollection->count()*/) {
foreach ($oFolderCollection as $oFolder) {
// if (2 > \substr_count($oFolder->FullName, $oFolder->Delimiter()))
try {
$oFolder->SetAllMetadata(
$this->getMetadata($oFolder->FullName(), ['/shared', '/private'], ['DEPTH'=>'infinity'])
$this->getMetadata($oFolder->FullName, ['/shared', '/private'], ['DEPTH'=>'infinity'])
);
} catch (\Throwable $e) {
} catch (\Throwable $oException) {
// Ignore error
}
}
}
return $aReturn;
if (!$bInbox && !$sParentFolderName && !isset($oFolderCollection['INBOX'])) {
$oFolderCollection['INBOX'] = new Folder('INBOX', $sDelimiter);
}
return $oFolderCollection;
}
/**
@ -559,7 +588,7 @@ trait Folders
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
public function FolderSubscribeList(string $sParentFolderName, string $sListPattern) : array
public function FolderSubscribeList(string $sParentFolderName, string $sListPattern) : FolderCollection
{
return $this->FolderList($sParentFolderName, $sListPattern, true);
}
@ -569,7 +598,7 @@ trait Folders
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
public function FolderStatusList(string $sParentFolderName, string $sListPattern) : array
public function FolderStatusList(string $sParentFolderName, string $sListPattern) : FolderCollection
{
return $this->FolderList($sParentFolderName, $sListPattern, false, true);
}

View file

@ -16,7 +16,9 @@ use MailSo\Imap\FetchResponse;
use MailSo\Imap\Enumerations\FetchType;
use MailSo\Imap\ResponseCollection;
use MailSo\Imap\SequenceSet;
use MailSo\Imap\Enumerations\MessageFlag;
use MailSo\Imap\Enumerations\ResponseType;
use MailSo\Imap\Enumerations\StoreAction;
/**
* @category MailSo
@ -30,11 +32,10 @@ trait Messages
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
public function Fetch(array $aInputFetchItems, string $sIndexRange, bool $bIndexIsUid) : array
public function FetchIterate(array $aInputFetchItems, string $sIndexRange, bool $bIndexIsUid) : iterable
{
if (!\strlen(\trim($sIndexRange)))
{
$this->writeLogException(new \InvalidArgumentException, \LOG_ERR, true);
if (!\strlen(\trim($sIndexRange))) {
$this->writeLogException(new \InvalidArgumentException, \LOG_ERR);
}
$aReturn = array();
@ -44,8 +45,7 @@ trait Messages
FetchType::UID,
FetchType::RFC822_SIZE
);
foreach ($aInputFetchItems as $mFetchKey)
{
foreach ($aInputFetchItems as $mFetchKey) {
switch ($mFetchKey)
{
case FetchType::UID:
@ -77,20 +77,20 @@ trait Messages
break;
}
}
if ($this->IsSupported('OBJECTID')) {
if ($this->hasCapability('OBJECTID')) {
$aFetchItems[] = FetchType::EMAILID;
$aFetchItems[] = FetchType::THREADID;
} else if ($this->IsSupported('X-GM-EXT-1')) {
} else if ($this->hasCapability('X-GM-EXT-1')) {
// https://developers.google.com/gmail/imap/imap-extensions
$aFetchItems[] = 'X-GM-MSGID';
$aFetchItems[] = 'X-GM-THRID';
/*
} else if ($this->IsSupported('X-DOVECOT')) {
} else if ($this->hasCapability('X-DOVECOT')) {
$aFetchItems[] = 'X-GUID';
*/
}
/*
if ($this->IsSupported('X-GM-EXT-1') && \in_array(FetchType::FLAGS, $aFetchItems)) {
if ($this->hasCapability('X-GM-EXT-1') && \in_array(FetchType::FLAGS, $aFetchItems)) {
$aFetchItems[] = 'X-GM-LABELS';
}
*/
@ -112,31 +112,42 @@ trait Messages
foreach ($this->yieldUntaggedResponses() as $oResponse) {
if (FetchResponse::isValidImapResponse($oResponse)) {
if (FetchResponse::hasUidAndSize($oResponse)) {
$aReturn[] = new FetchResponse($oResponse);
} else if ($this->oLogger) {
$this->oLogger->Write('Skipped Imap Response! ['.$oResponse.']', \LOG_NOTICE);
yield new FetchResponse($oResponse);
} else {
$this->logWrite('Skipped Imap Response! ['.$oResponse.']', \LOG_NOTICE);
}
}
}
} finally {
$this->aFetchCallbacks = array();
}
}
public function Fetch(array $aInputFetchItems, string $sIndexRange, bool $bIndexIsUid) : array
{
$aReturn = array();
foreach ($this->FetchIterate($aInputFetchItems, $sIndexRange, $bIndexIsUid) as $oFetchResponse) {
$aReturn[] = $oFetchResponse;
}
return $aReturn;
}
/**
* Appends message to specified folder
*
* @param resource $rMessageAppendStream
* @param resource $rMessageStream
*
* @throws \InvalidArgumentException
* @throws \MailSo\RuntimeException
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
public function MessageAppendStream(string $sFolderName, $rMessageAppendStream, int $iStreamSize, array $aFlagsList = null, int $iDateTime = 0) : ?int
public function MessageAppendStream(string $sFolderName, $rMessageStream, int $iStreamSize, array $aFlagsList = null, int $iDateTime = 0) : ?int
{
if (!\is_resource($rMessageStream) || !\strlen($sFolderName) || 1 > $iStreamSize) {
throw new \InvalidArgumentException;
}
$aParams = array(
$this->EscapeFolderName($sFolderName),
$aFlagsList
@ -147,7 +158,7 @@ trait Messages
/*
// RFC 3516 || RFC 6855 section-4
if ($this->IsSupported('BINARY') || $this->IsSupported('UTF8=ACCEPT')) {
if ($this->hasCapability('BINARY') || $this->hasCapability('UTF8=ACCEPT')) {
$aParams[] = '~{'.$iStreamSize.'}';
}
*/
@ -155,14 +166,14 @@ trait Messages
$this->SendRequestGetResponse('APPEND', $aParams);
return $this->writeMessageStream($rMessageAppendStream);
return $this->writeMessageStream($rMessageStream);
}
private function writeMessageStream($rMessageStream) : ?int
{
$this->writeLog('Write to connection stream', \LOG_INFO);
\MailSo\Base\Utils::MultipleStreamWriter($rMessageStream, array($this->ConnectionResource()));
\MailSo\Base\Utils::WriteStream($rMessageStream, $this->ConnectionResource());
$this->sendRaw('');
$oResponses = $this->getResponse();
@ -196,12 +207,14 @@ trait Messages
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
public function MessageCopy(string $sToFolder, SequenceSet $oRange) : ResponseCollection
public function MessageCopy(string $sFromFolder, string $sToFolder, SequenceSet $oRange) : ResponseCollection
{
if (!\count($oRange)) {
$this->writeLogException(new \InvalidArgumentException, \LOG_ERR, true);
if (!$sFromFolder || !$sToFolder || !\count($oRange)) {
$this->writeLogException(new \InvalidArgumentException, \LOG_ERR);
}
$this->FolderSelect($sFromFolder);
return $this->SendRequestGetResponse(
$oRange->UID ? 'UID COPY' : 'COPY',
array((string) $oRange, $this->EscapeFolderName($sToFolder))
@ -214,22 +227,48 @@ trait Messages
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
public function MessageMove(string $sToFolder, SequenceSet $oRange) : ResponseCollection
public function MessageMove(string $sFromFolder, string $sToFolder, SequenceSet $oRange) : void
{
if (!\count($oRange)) {
$this->writeLogException(new \InvalidArgumentException, \LOG_ERR, true);
if (!$sFromFolder || !$sToFolder || !\count($oRange)) {
$this->writeLogException(new \InvalidArgumentException, \LOG_ERR);
}
if (!$this->IsSupported('MOVE')) {
$this->writeLogException(
new \MailSo\RuntimeException('Move is not supported'),
\LOG_ERR, true);
if ($this->hasCapability('MOVE')) {
$this->FolderSelect($sFromFolder);
$this->SendRequestGetResponse(
$oRange->UID ? 'UID MOVE' : 'MOVE',
array((string) $oRange, $this->EscapeFolderName($sToFolder))
);
} else {
$this->MessageCopy($sFromFolder, $sToFolder, $oRange);
$this->MessageDelete($sFromFolder, $oRange, true);
}
}
/**
* @throws \InvalidArgumentException
* @throws \MailSo\RuntimeException
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
public function MessageDelete(string $sFolder, SequenceSet $oRange, bool $bExpungeAll = false) : void
{
if (!$sFolder || !\count($oRange)) {
$this->writeLogException(new \InvalidArgumentException, \LOG_ERR);
}
return $this->SendRequestGetResponse(
$oRange->UID ? 'UID MOVE' : 'MOVE',
array((string) $oRange, $this->EscapeFolderName($sToFolder))
$this->FolderSelect($sFolder);
$this->MessageStoreFlag($oRange,
array(MessageFlag::DELETED),
StoreAction::ADD_FLAGS_SILENT
);
if ($bExpungeAll && $this->Settings->expunge_all_on_delete) {
$this->FolderExpunge();
} else {
$this->FolderExpunge($oRange);
}
}
/**
@ -246,14 +285,14 @@ trait Messages
*/
public function MessageReplaceStream(string $sFolderName, int $iUid, $rMessageStream, int $iStreamSize, array $aFlagsList = null, int $iDateTime = 0) : ?int
{
if (1 > $iUid || !$this->IsSupported('REPLACE')) {
if (1 > $iUid || !$this->hasCapability('REPLACE')) {
$this->FolderSelect($sFolderName);
$iNewUid = $this->MessageAppendStream($sFolderName, $rMessageStream, $iStreamSize, $aFlagsList, $iDateTime);
if ($iUid) {
$oRange = new SequenceSet([$iUid]);
$this->MessageStoreFlag($oRange,
array(\MailSo\Imap\Enumerations\MessageFlag::DELETED),
\MailSo\Imap\Enumerations\StoreAction::ADD_FLAGS_SILENT
array(MessageFlag::DELETED),
StoreAction::ADD_FLAGS_SILENT
);
$this->FolderExpunge($oRange);
}
@ -271,7 +310,7 @@ trait Messages
/*
// RFC 3516 || RFC 6855 section-4
if ($this->IsSupported('BINARY') || $this->IsSupported('UTF8=ACCEPT')) {
if ($this->hasCapability('BINARY') || $this->hasCapability('UTF8=ACCEPT')) {
$aParams[] = '~{'.$iStreamSize.'}';
}
*/
@ -287,6 +326,7 @@ trait Messages
* @throws \MailSo\RuntimeException
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
* $sStoreAction = \MailSo\Imap\Enumerations\StoreAction::ADD_FLAGS_SILENT
*/
public function MessageStoreFlag(SequenceSet $oRange, array $aInputStoreItems, string $sStoreAction) : ?ResponseCollection
{

View file

@ -96,23 +96,23 @@ trait Metadata
public function ServerGetMetadata(array $aEntries, array $aOptions = []) : array
{
return $this->IsSupported('METADATA-SERVER')
return $this->hasCapability('METADATA-SERVER')
? $this->getMetadata('', $aEntries, $aOptions)
: [];
}
public function FolderGetMetadata(string $sFolderName, array $aEntries, array $aOptions = []) : array
{
return $this->IsSupported('METADATA')
return $this->hasCapability('METADATA')
? $this->getMetadata($sFolderName, $aEntries, $aOptions)
: [];
}
public function FolderSetMetadata(string $sFolderName, array $aEntries) : void
{
if ($this->IsSupported('METADATA')) {
if ($this->hasCapability('METADATA')) {
if (!$aEntries) {
throw new \MailSo\Base\Exceptions\InvalidArgumentException("Wrong argument for SETMETADATA command");
throw new \InvalidArgumentException('Wrong argument for SETMETADATA command');
}
$arguments = [$this->EscapeFolderName($sFolderName)];
@ -126,4 +126,8 @@ trait Metadata
}
}
public function FolderRemoveMetadata($sFolderName, array $aEntries) : void
{
$this->FolderSetMetadata($sFolderName, \array_fill_keys(\array_keys($aEntries), null));
}
}

View file

@ -47,7 +47,7 @@ trait Quota
// * QUOTA "User quota" (STORAGE 1284645 2097152)\r\n
private function getQuota(bool $root, string $sFolderName) : ?array
{
if (!$this->IsSupported('QUOTA')) {
if (!$this->hasCapability('QUOTA')) {
return null;
}
$oResponseCollection = $this->SendRequest(($root?'GETQUOTAROOT':'GETQUOTA') . " {$this->EscapeFolderName($sFolderName)}");

View file

@ -1,35 +0,0 @@
<?php
/*
* This file is part of MailSo.
*
* (c) 2014 Usenko Timur
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace MailSo\Imap\Enumerations;
/**
* @category MailSo
* @package Imap
* @subpackage Enumerations
*/
abstract class FolderResponseStatus
{
// RFC 3501
const MESSAGES = 'MESSAGES';
// const RECENT = 'RECENT'; // IMAP4rev2 deprecated
const UIDNEXT = 'UIDNEXT';
const UIDVALIDITY = 'UIDVALIDITY';
const UNSEEN = 'UNSEEN';
// RFC 4551
const HIGHESTMODSEQ = 'HIGHESTMODSEQ';
// RFC 7889
const APPENDLIMIT = 'APPENDLIMIT';
// RFC 8474
const MAILBOXID = 'MAILBOXID';
// RFC 9051 IMAP4rev2
const SIZE = 'SIZE';
}

View file

@ -30,4 +30,6 @@ abstract class FolderStatus
const APPENDLIMIT = 'APPENDLIMIT';
// RFC 8474
const MAILBOXID = 'MAILBOXID';
// RFC 9051 IMAP4rev2
const SIZE = 'SIZE';
}

View file

@ -18,9 +18,6 @@ namespace MailSo\Imap\Exceptions;
*/
class ResponseException extends \MailSo\RuntimeException
{
/**
* @var array
*/
private $oResponses;
public function __construct(?\MailSo\Imap\ResponseCollection $oResponses = null, string $sMessage = '', int $iCode = 0, ?\Throwable $oPrevious = null)

View file

@ -17,26 +17,18 @@ namespace MailSo\Imap;
*/
class FetchResponse
{
/**
* @var \MailSo\Imap\Response
*/
private $oImapResponse;
public Response $oImapResponse;
/**
* @var array|null
*/
private $aEnvelopeCache;
private ?array $aEnvelopeCache = null;
function __construct(Response $oImapResponse)
{
$this->oImapResponse = $oImapResponse;
$this->aEnvelopeCache = null;
}
public function GetEnvelope(bool $bForce = false) : ?array
{
if (null === $this->aEnvelopeCache || $bForce)
{
if (null === $this->aEnvelopeCache || $bForce) {
$this->aEnvelopeCache = $this->GetFetchValue(Enumerations\FetchType::ENVELOPE);
}
return $this->aEnvelopeCache;
@ -54,13 +46,10 @@ class FetchResponse
{
$oResult = null;
$aEmails = $this->GetFetchEnvelopeValue($iIndex);
if (\is_array($aEmails) && \count($aEmails))
{
if (\is_array($aEmails) && \count($aEmails)) {
$oResult = new \MailSo\Mime\EmailCollection;
foreach ($aEmails as $aEmailItem)
{
if (\is_array($aEmailItem) && 4 === \count($aEmailItem))
{
foreach ($aEmails as $aEmailItem) {
if (\is_array($aEmailItem) && 4 === \count($aEmailItem)) {
$sDisplayName = \MailSo\Base\Utils::DecodeHeaderValue(
self::findEnvelopeIndex($aEmailItem, 0, ''), $sParentCharset);
@ -70,8 +59,7 @@ class FetchResponse
$sLocalPart = self::findEnvelopeIndex($aEmailItem, 2, '');
$sDomainPart = self::findEnvelopeIndex($aEmailItem, 3, '');
if (\strlen($sLocalPart) && \strlen($sDomainPart))
{
if (\strlen($sLocalPart) && \strlen($sDomainPart)) {
$oResult->append(
new \MailSo\Mime\Email($sLocalPart.'@'.$sDomainPart, $sDisplayName)
);
@ -122,18 +110,15 @@ class FetchResponse
{
$bNextIsValue = false;
if (isset($this->oImapResponse->ResponseList[3]) && \is_array($this->oImapResponse->ResponseList[3]))
{
foreach ($this->oImapResponse->ResponseList[3] as $mItem)
{
if ($bNextIsValue)
{
if (isset($this->oImapResponse->ResponseList[3]) && \is_array($this->oImapResponse->ResponseList[3])) {
foreach ($this->oImapResponse->ResponseList[3] as $mItem) {
if ($bNextIsValue) {
return (string) $mItem;
}
if (\is_string($mItem) && (
$mItem === 'BODY[HEADER]' ||
0 === \strpos($mItem, 'BODY[HEADER.FIELDS') ||
\str_starts_with($mItem, 'BODY[HEADER.FIELDS') ||
$mItem === 'BODY[MIME]'))
{
$bNextIsValue = true;

View file

@ -11,58 +11,54 @@
namespace MailSo\Imap;
use MailSo\Imap\Enumerations\MetadataKeys;
/**
* @category MailSo
* @package Imap
*/
class Folder
class Folder implements \JsonSerializable
{
// RFC5258 Response data STATUS items when using LIST-EXTENDED
use Traits\Status;
/**
* @var string
*/
private $sFullName;
private ?string $sDelimiter;
/**
* @var string
*/
private $sDelimiter;
/**
* @var array
*/
private $aFlagsLowerCase;
private array $aAttributes;
/**
* RFC 5464
*/
private $aMetadata = array();
private array $aMetadata = array();
/**
* @throws \InvalidArgumentException
*/
function __construct(string $sFullName, string $sDelimiter = null, array $aFlags = array())
function __construct(string $sFullName, string $sDelimiter = null, array $aAttributes = array())
{
if (!\strlen($sFullName)) {
throw new \InvalidArgumentException;
}
$this->sFullName = $sFullName;
$this->FullName = $sFullName;
$this->setDelimiter($sDelimiter);
$this->setFlags($aFlags);
$this->setAttributes($aAttributes);
/*
// RFC 5738
if (\in_array('\\noutf8', $this->aFlagsLowerCase)) {
if (\in_array('\\noutf8', $this->aAttributes)) {
}
if (\in_array('\\utf8only', $this->aFlagsLowerCase)) {
if (\in_array('\\utf8only', $this->aAttributes)) {
}
*/
}
public function setFlags(array $aFlags) : void
public function setAttributes(array $aAttributes) : void
{
$this->aFlagsLowerCase = \array_map('strtolower', $aFlags);
$this->aAttributes = \array_map('mb_strtolower', $aAttributes);
}
public function setSubscribed() : void
{
$this->aAttributes = \array_unique(\array_merge($this->aAttributes, ['\\subscribed']));
}
public function setDelimiter(?string $sDelimiter) : void
@ -72,7 +68,7 @@ class Folder
public function Name() : string
{
$sNameRaw = $this->sFullName;
$sNameRaw = $this->FullName;
if ($this->sDelimiter) {
$aNames = \explode($this->sDelimiter, $sNameRaw);
return \end($aNames);
@ -80,29 +76,25 @@ class Folder
return $sNameRaw;
}
public function FullName() : string
{
return $this->sFullName;
}
public function Delimiter() : ?string
{
return $this->sDelimiter;
}
public function FlagsLowerCase() : array
public function Selectable() : bool
{
return $this->aFlagsLowerCase;
return !\in_array('\\noselect', $this->aAttributes)
&& !\in_array('\\nonexistent', $this->aAttributes);
}
public function IsSelectable() : bool
public function IsSubscribed() : bool
{
return !\in_array('\\noselect', $this->aFlagsLowerCase) && !\in_array('\\nonexistent', $this->aFlagsLowerCase);
return \in_array('\\subscribed', $this->aAttributes);
}
public function IsInbox() : bool
{
return 'INBOX' === \strtoupper($this->sFullName) || \in_array('\\inbox', $this->aFlagsLowerCase);
return 'INBOX' === \strtoupper($this->FullName) || \in_array('\\inbox', $this->aAttributes);
}
public function SetMetadata(string $sName, string $sData) : void
@ -124,4 +116,72 @@ class Folder
{
return $this->aMetadata;
}
// JMAP RFC 8621
public function Role() : ?string
{
$role = \strtolower($this->GetMetadata(MetadataKeys::SPECIALUSE) ?: '');
if (!$role) {
$match = \array_intersect([
'\\inbox',
'\\all', // '\\allmail'
'\\archive',
'\\drafts',
'\\flagged', // '\\starred'
'\\important',
'\\junk', // '\\spam'
'\\sent', // '\\sentmail'
'\\trash', // '\\bin'
], $this->aAttributes);
if ($match) {
$role = \array_shift($match);
}
if (!$role && 'INBOX' === \strtoupper($this->FullName)) {
return 'inbox';
}
}
return $role ? \ltrim($role, '\\') : null;
}
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
/*
if ($this->ImapClient->hasCapability('ACL') || $this->ImapClient->CapabilityValue('RIGHTS')) {
// MailSo\Imap\Responses\ACL
$rights = $this->ImapClient->FolderMyRights($this->FullName);
}
*/
$result = array(
'@Object' => 'Object/Folder',
'name' => $this->Name(),
'fullName' => $this->FullName,
'delimiter' => (string) $this->sDelimiter,
'attributes' => $this->aAttributes,
'metadata' => $this->aMetadata,
'uidNext' => $this->UIDNEXT,
// https://datatracker.ietf.org/doc/html/rfc8621#section-2
'totalEmails' => $this->MESSAGES,
'unreadEmails' => $this->UNSEEN,
'id' => $this->MAILBOXID,
'role' => $this->Role()
/*
'myRights' => [
'mayReadItems' => !$rights || ($rights->hasRight('l') && $rights->hasRight('r')),
'mayAddItems' => !$rights || $rights->hasRight('i'),
'mayRemoveItems' => !$rights || ($rights->hasRight('t') && $rights->hasRight('e')),
'maySetSeen' => !$rights || $rights->hasRight('s'),
'maySetKeywords' => !$rights || $rights->hasRight('w'),
'mayCreateChild' => !$rights || $rights->hasRight('k'),
'mayRename' => !$rights || $rights->hasRight('x'),
'mayDelete' => !$rights || $rights->hasRight('x'),
'maySubmit' => !$rights || $rights->hasRight('p')
]
*/
);
if ($this->etag) {
$result['etag'] = $this->etag;
}
return $result;
}
}

View file

@ -9,7 +9,7 @@
* file that was distributed with this source code.
*/
namespace MailSo\Mail;
namespace MailSo\Imap;
/**
* @category MailSo
@ -17,12 +17,7 @@ namespace MailSo\Mail;
*/
class FolderCollection extends \MailSo\Base\Collection
{
/**
* @var bool
*/
public $Optimized = false;
public $TotalCount = 0;
// public bool $Optimized = false;
public function append($oFolder, bool $bToTop = false) : void
{
@ -30,9 +25,13 @@ class FolderCollection extends \MailSo\Base\Collection
parent::append($oFolder, $bToTop);
}
public function FindDelimiter() : string
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
$oFolder = $this['INBOX'] ?? $this[0] ?? null;
return $oFolder ? $oFolder->Delimiter() : '/';
return array(
'@Object' => 'Collection/FolderCollection',
'@Collection' => \array_values($this->getArrayCopy()),
// 'optimized' => $this->Optimized
);
}
}

View file

@ -19,45 +19,32 @@ class FolderInformation implements \JsonSerializable
{
use Traits\Status;
/**
* @var string
*/
public $FolderName;
public bool $IsWritable;
/**
* @var bool
*/
public $IsWritable;
/**
* @var array
* Message flags
* https://www.rfc-editor.org/rfc/rfc3501#section-7.2.6
* These could be not permanent so we don't use them
*/
public $Flags = array();
public array $Flags = array();
/**
* @var array
* NOTE: Empty when FolderExamine is used
* https://www.rfc-editor.org/rfc/rfc3501#page-64
*/
public $PermanentFlags = array();
public array $PermanentFlags = array();
/**
* https://datatracker.ietf.org/doc/html/rfc3501#section-7.3.1
* @var int
*/
public $Exists = null;
function __construct(string $sFolderName, bool $bIsWritable)
function __construct(string $sFullName, bool $bIsWritable)
{
$this->FolderName = $sFolderName;
$this->FullName = $sFullName;
$this->IsWritable = $bIsWritable;
}
public function IsFlagSupported(string $sFlag) : bool
{
return \in_array('\\*', $this->PermanentFlags) ||
\in_array($sFlag, $this->PermanentFlags) ||
\in_array($sFlag, $this->Flags);
return \in_array('\\*', $this->PermanentFlags)
|| \in_array($sFlag, $this->PermanentFlags);
// || \in_array($sFlag, $this->Flags);
}
#[\ReturnTypeWillChange]
@ -65,24 +52,33 @@ class FolderInformation implements \JsonSerializable
{
$result = array(
'id' => $this->MAILBOXID,
'Name' => $this->FolderName,
'Flags' => $this->Flags,
'PermanentFlags' => $this->PermanentFlags,
'UidNext' => $this->UIDNEXT,
'UidValidity' => $this->UIDVALIDITY
'name' => $this->FullName,
'uidNext' => $this->UIDNEXT,
'uidValidity' => $this->UIDVALIDITY
);
if (isset($this->MESSAGES)) {
$result['totalEmails'] = $this->MESSAGES;
$result['unreadEmails'] = $this->UNSEEN;
}
if (isset($this->HIGHESTMODSEQ)) {
$result['Highestmodseq'] = $this->HIGHESTMODSEQ;
$result['highestModSeq'] = $this->HIGHESTMODSEQ;
}
if (isset($this->APPENDLIMIT)) {
$result['Appendlimit'] = $this->APPENDLIMIT;
$result['appendLimit'] = $this->APPENDLIMIT;
}
if (isset($this->SIZE)) {
$result['Size'] = $this->SIZE;
$result['size'] = $this->SIZE;
}
if ($this->etag) {
$result['etag'] = $this->etag;
}
/*
if ($this->Flags) {
$result['flags'] = $this->Flags;
}
*/
if ($this->PermanentFlags) {
$result['permanentFlags'] = $this->PermanentFlags;
}
return $result;
}

View file

@ -26,13 +26,9 @@ class ImapClient extends \MailSo\Net\NetClient
use Commands\Metadata;
use Commands\Quota;
const
TAG_PREFIX = 'TAG';
public string $TAG_PREFIX = 'TAG';
/**
* @var int
*/
private $iTagCount = 0;
private int $iTagCount = 0;
/**
* @var array
@ -46,39 +42,22 @@ class ImapClient extends \MailSo\Net\NetClient
/**
* Used by \MailSo\Mail\MailClient::MessageMimeStream
* @var array
*/
private $aFetchCallbacks;
private array $aFetchCallbacks = array();
/**
* @var array
*/
private $aTagTimeouts = array();
private array $aTagTimeouts = array();
/**
* @var bool
*/
private $bIsLoggined = false;
private bool $bIsLoggined = false;
/**
* @var string
*/
private $sLogginedUser = '';
private bool $UTF8 = false;
/**
* @var bool
*/
public $__FORCE_SELECT_ON_EXAMINE__ = false;
public $__DISABLE_METADATA = false;
/**
* @var bool
*/
private $UTF8 = false;
public function GetLogginedUser() : string
public function Hash() : string
{
return $this->sLogginedUser;
return \md5('ImapClientHash/'.
$this->Settings->Login . '@' .
$this->Settings->host . ':' .
$this->Settings->port
);
}
/**
@ -87,6 +66,7 @@ class ImapClient extends \MailSo\Net\NetClient
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
// public function Connect(Settings $oSettings) : void
public function Connect(\MailSo\Net\ConnectSettings $oSettings) : void
{
$this->aTagTimeouts['*'] = \microtime(true);
@ -95,63 +75,73 @@ class ImapClient extends \MailSo\Net\NetClient
$this->setCapabilities($this->getResponse('*'));
if (ConnectionSecurityType::STARTTLS === $this->iSecurityType
|| (ConnectionSecurityType::AUTO_DETECT === $this->iSecurityType && $this->IsSupported('STARTTLS'))) {
if (ConnectionSecurityType::STARTTLS === $this->Settings->type
|| (ConnectionSecurityType::AUTO_DETECT === $this->Settings->type && $this->hasCapability('STARTTLS'))) {
$this->StartTLS();
}
}
private function StartTLS() : void
{
if ($this->IsSupported('STARTTLS')) {
if ($this->hasCapability('STARTTLS')) {
$this->SendRequestGetResponse('STARTTLS');
$this->EnableCrypto();
$this->aCapabilityItems = null;
} else {
$this->writeLogException(
new \MailSo\Net\Exceptions\SocketUnsuppoterdSecureConnectionException('STARTTLS is not supported'),
\LOG_ERR, true);
\LOG_ERR);
}
}
public function supportsAuthType(string $sasl_type) : bool
{
return $this->hasCapability("AUTH={$sasl_type}");
}
/**
* @throws \InvalidArgumentException
* @throws \MailSo\RuntimeException
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
public function Login(array $aCredentials) : self
public function Login(Settings $oSettings) : self
{
if (!empty($aCredentials['ProxyAuthUser']) && !empty($aCredentials['ProxyAuthPassword'])) {
$sLogin = \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($aCredentials['ProxyAuthUser']));
$sPassword = $aCredentials['ProxyAuthPassword'];
$sProxyAuthUser = $aCredentials['Login'];
if ($this->bIsLoggined) {
return $this;
}
if (!empty($oSettings->ProxyAuthUser) && !empty($oSettings->ProxyAuthPassword)) {
$sLogin = $oSettings->ProxyAuthUser;
$sPassword = $oSettings->ProxyAuthPassword;
$sProxyAuthUser = $oSettings->Login;
} else {
$sLogin = \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($aCredentials['Login']));
$sPassword = $aCredentials['Password'];
$sLogin = $oSettings->Login;
$sPassword = $oSettings->Password;
$sProxyAuthUser = '';
}
if (!\strlen($sLogin) || !\strlen($sPassword))
{
$this->writeLogException(
new \InvalidArgumentException,
\LOG_ERR, true);
$sLogin = \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($sLogin));
if (!\strlen($sLogin) || !\strlen($sPassword)) {
$this->writeLogException(new \InvalidArgumentException, \LOG_ERR);
}
$this->sLogginedUser = $sLogin;
$type = $this->IsSupported('LOGINDISABLED') ? '' : 'LOGIN'; // RFC3501 6.2.3
foreach ($aCredentials['SASLMechanisms'] as $sasl_type) {
if ($this->IsSupported("AUTH={$sasl_type}") && \SnappyMail\SASL::isSupported($sasl_type)) {
$type = '';
foreach ($oSettings->SASLMechanisms as $sasl_type) {
if ($this->hasCapability("AUTH={$sasl_type}") && \SnappyMail\SASL::isSupported($sasl_type)) {
$type = $sasl_type;
break;
}
}
// RFC3501 6.2.3
if (!$type && \in_array('LOGIN', $oSettings->SASLMechanisms) && !$this->hasCapability('LOGINDISABLED')) {
$type = 'LOGIN';
}
if (!$type) {
if (!$this->Encrypted() && $this->IsSupported('STARTTLS')) {
if (!$this->Encrypted() && $this->hasCapability('STARTTLS')) {
$this->StartTLS();
return $this->Login($aCredentials);
return $this->Login($oSettings);
}
throw new \MailSo\RuntimeException('No supported SASL mechanism found, remote server wants: '
. \implode(', ', \array_filter($this->Capability() ?: [], function($var){
@ -161,16 +151,15 @@ class ImapClient extends \MailSo\Net\NetClient
}
$SASL = \SnappyMail\SASL::factory($type);
$SASL->base64 = true;
try
{
if (0 === \strpos($type, 'SCRAM-'))
if (\str_starts_with($type, 'SCRAM-'))
{
$sAuthzid = $this->getResponseValue($this->SendRequestGetResponse('AUTHENTICATE', array($type)), Enumerations\ResponseType::CONTINUATION);
$this->sendRaw($SASL->authenticate($sLogin, $sPassword/*, $sAuthzid*/), true);
$sChallenge = $SASL->challenge($this->getResponseValue($this->getResponse(), Enumerations\ResponseType::CONTINUATION));
$this->oLogger && $this->oLogger->AddSecret($sChallenge);
$this->logMask($sChallenge);
$this->sendRaw($sChallenge);
$oResponse = $this->getResponse();
$SASL->verify($this->getResponseValue($oResponse));
@ -178,17 +167,17 @@ class ImapClient extends \MailSo\Net\NetClient
else if ('CRAM-MD5' === $type)
{
$sChallenge = $this->getResponseValue($this->SendRequestGetResponse('AUTHENTICATE', array($type)), Enumerations\ResponseType::CONTINUATION);
$this->oLogger->Write('challenge: '.\base64_decode($sChallenge));
$this->logWrite('challenge: '.\base64_decode($sChallenge));
$sAuth = $SASL->authenticate($sLogin, $sPassword, $sChallenge);
$this->oLogger && $this->oLogger->AddSecret($sAuth);
$this->logMask($sAuth);
$this->sendRaw($sAuth);
$oResponse = $this->getResponse();
}
else if ('PLAIN' === $type || 'OAUTHBEARER' === $type)
else if ('PLAIN' === $type || 'OAUTHBEARER' === $type /*|| 'PLAIN-CLIENTTOKEN' === $type*/)
{
$sAuth = $SASL->authenticate($sLogin, $sPassword);
$this->oLogger && $this->oLogger->AddSecret($sAuth);
if ($this->IsSupported('SASL-IR')) {
$this->logMask($sAuth);
if ($this->hasCapability('SASL-IR')) {
$oResponse = $this->SendRequestGetResponse('AUTHENTICATE', array($type, $sAuth));
} else {
$this->SendRequestGetResponse('AUTHENTICATE', array($type));
@ -203,27 +192,26 @@ class ImapClient extends \MailSo\Net\NetClient
$oR = $oResponse->getLast();
if ($oR && Enumerations\ResponseType::CONTINUATION === $oR->ResponseType) {
if (!empty($oR->ResponseList[1]) && preg_match('/^[a-zA-Z0-9=+\/]+$/', $oR->ResponseList[1])) {
$this->Logger()->Write(\base64_decode($oR->ResponseList[1]),
\LOG_WARNING);
$this->logWrite(\base64_decode($oR->ResponseList[1]), \LOG_WARNING);
}
$this->sendRaw('');
$oResponse = $this->getResponse();
}
}
else if ($this->IsSupported('LOGINDISABLED'))
else if ($this->hasCapability('LOGINDISABLED'))
{
$sB64 = $this->getResponseValue($this->SendRequestGetResponse('AUTHENTICATE', array($type)), Enumerations\ResponseType::CONTINUATION);
$this->sendRaw($SASL->authenticate($sLogin, $sPassword, $sB64), true);
$this->getResponse();
$sPass = $SASL->challenge(''/*UGFzc3dvcmQ6*/);
$this->oLogger && $this->oLogger->AddSecret($sPass);
$this->logMask($sPass);
$this->sendRaw($sPass);
$oResponse = $this->getResponse();
}
else
{
$sPassword = $this->EscapeString(\mb_convert_encoding($sPassword, 'ISO-8859-1', 'UTF-8'));
$this->oLogger && $this->oLogger->AddSecret($sPassword);
$this->logMask($sPassword);
$oResponse = $this->SendRequestGetResponse('LOGIN',
array(
$this->EscapeString($sLogin),
@ -233,28 +221,24 @@ class ImapClient extends \MailSo\Net\NetClient
$this->setCapabilities($oResponse);
if (\strlen($sProxyAuthUser))
{
if (\strlen($sProxyAuthUser)) {
$this->SendRequestGetResponse('PROXYAUTH', array($this->EscapeString($sProxyAuthUser)));
}
/*
// TODO: RFC 9051
if ($this->IsSupported('IMAP4rev2')) {
if ($this->hasCapability('IMAP4rev2')) {
$this->Enable('IMAP4rev1');
}
*/
// RFC 6855 || RFC 5738
$this->UTF8 = $this->IsSupported('UTF8=ONLY') || $this->IsSupported('UTF8=ACCEPT');
$this->UTF8 = $this->hasCapability('UTF8=ONLY') || $this->hasCapability('UTF8=ACCEPT');
if ($this->UTF8) {
$this->Enable('UTF8=ACCEPT');
}
}
catch (Exceptions\NegativeResponseException $oException)
{
$this->writeLogException(
new Exceptions\LoginBadCredentialsException(
$oException->GetResponses(), '', 0, $oException),
\LOG_NOTICE, true);
$this->writeLogException(new Exceptions\LoginBadCredentialsException($oException->GetResponses(), '', 0, $oException));
}
$this->bIsLoggined = true;
@ -269,8 +253,7 @@ class ImapClient extends \MailSo\Net\NetClient
*/
public function Logout() : void
{
if ($this->bIsLoggined)
{
if ($this->bIsLoggined) {
$this->bIsLoggined = false;
$this->SendRequestGetResponse('LOGOUT');
}
@ -302,12 +285,44 @@ class ImapClient extends \MailSo\Net\NetClient
return $this->aCapabilityItems;
}
public function CapabilityValue(string $sExtentionName) : ?string
{
$sExtentionName = \trim($sExtentionName) . '=';
$aCapabilities = $this->Capability() ?: [];
foreach ($aCapabilities as $string) {
if (\str_starts_with($string, $sExtentionName)) {
return \substr($string, \strlen($sExtentionName));
}
}
return null;
}
private function setCapabilities(ResponseCollection $oResponseCollection) : void
{
$aList = $oResponseCollection->getCapabilityResult();
if ($aList && $this->__DISABLE_METADATA) {
// Issue #365: Many folders on Cyrus IMAP breaks login
$aList = \array_diff($aList, ['METADATA']);
if ($aList) {
if ($this->Settings->disable_metadata) {
// Issue #365: Many folders on Cyrus IMAP breaks login
$aList = \array_diff($aList, ['METADATA']);
}
if ($this->Settings->disable_move) {
$aList = \array_diff($aList, ['MOVE']);
}
if ($this->Settings->disable_sort) {
$aList = \array_diff($aList, ['SORT','ESORT']);
}
if ($this->Settings->disable_thread) {
$aList = \array_filter($aList, function ($item) { \str_starts_with($item, 'THREAD='); });
}
if ($this->Settings->disable_list_status) {
$aList = \array_diff($aList, ['LIST-STATUS']);
}
if ($this->Settings->disable_binary) {
$aList = \array_diff($aList, ['BINARY']);
}
if (8 > PHP_INT_SIZE) {
$aList = \array_diff($aList, ['CONDSTORE']);
}
}
$this->aCapabilityItems = $aList;
}
@ -326,11 +341,15 @@ class ImapClient extends \MailSo\Net\NetClient
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\*
*/
public function IsSupported(string $sExtentionName) : bool
public function hasCapability(string $sExtentionName) : bool
{
$sExtentionName = \trim($sExtentionName);
return $sExtentionName && \in_array(\strtoupper($sExtentionName), $this->Capability() ?: []);
}
public function IsSupported(string $sExtentionName) : bool
{
return $this->hasCapability($sExtentionName);
}
/**
* RFC 5161
@ -352,7 +371,7 @@ class ImapClient extends \MailSo\Net\NetClient
*/
public function GetNamespace() : ?NamespaceResult
{
if (!$this->IsSupported('NAMESPACE')) {
if (!$this->hasCapability('NAMESPACE')) {
return null;
}
@ -362,18 +381,21 @@ class ImapClient extends \MailSo\Net\NetClient
if (Enumerations\ResponseType::UNTAGGED === $oResponse->ResponseType
&& 'NAMESPACE' === $oResponse->StatusOrIndex)
{
$oReturn = new NamespaceResult;
$oReturn->InitByImapResponse($oResponse);
return $oReturn;
return new NamespaceResult($oResponse);
}
}
throw new Exceptions\ResponseException;
} catch (\Throwable $e) {
$this->writeLogException($e, \LOG_ERR);
throw $e;
}
}
public function GetPersonalNamespace() : string
{
$oNamespace = $this->GetNamespace();
return $oNamespace ? $oNamespace->GetPersonalNamespace() : '';
}
/**
* RFC 7889
* APPENDLIMIT=<number> indicates that the IMAP server has the same upload limit for all mailboxes.
@ -383,14 +405,8 @@ class ImapClient extends \MailSo\Net\NetClient
*/
public function AppendLimit() : ?int
{
if ($this->Capability()) {
foreach ($this->aCapabilityItems as $string) {
if ('APPENDLIMIT=' === \substr($string, 0, 12)) {
return (int) \substr($string, 12);
}
}
}
return null;
$string = $this->CapabilityValue('APPENDLIMIT');
return \is_null($string) ? null : (int) $string;
}
/**
@ -414,7 +430,7 @@ class ImapClient extends \MailSo\Net\NetClient
{
$sCommand = \trim($sCommand);
if (!\strlen($sCommand)) {
$this->writeLogException(new \InvalidArgumentException, \LOG_ERR, true);
$this->writeLogException(new \InvalidArgumentException, \LOG_ERR);
}
$this->IsConnected(true);
@ -457,13 +473,9 @@ class ImapClient extends \MailSo\Net\NetClient
if ($sResult) {
return $sResult;
}
$this->writeLogException(
new Exceptions\LoginException,
\LOG_NOTICE, true);
$this->writeLogException(new Exceptions\LoginException);
}
$this->writeLogException(
new Exceptions\LoginException,
\LOG_NOTICE, true);
$this->writeLogException(new Exceptions\LoginException);
}
/**
@ -494,7 +506,6 @@ class ImapClient extends \MailSo\Net\NetClient
}
} catch (\Throwable $e) {
$this->writeLogException($e, \LOG_WARNING);
throw $e;
}
}
@ -532,7 +543,6 @@ class ImapClient extends \MailSo\Net\NetClient
} catch (\Throwable $e) {
$this->writeLogException($e, \LOG_WARNING);
throw $e;
}
return $oResult;
@ -577,21 +587,16 @@ class ImapClient extends \MailSo\Net\NetClient
} catch (\Throwable $e) {
$this->writeLogException($e, \LOG_WARNING);
throw $e;
}
}
protected function prepareParamLine(array $aParams = array()) : string
{
$sReturn = '';
foreach ($aParams as $mParamItem)
{
if (\is_array($mParamItem) && \count($mParamItem))
{
foreach ($aParams as $mParamItem) {
if (\is_array($mParamItem) && \count($mParamItem)) {
$sReturn .= ' ('.\trim($this->prepareParamLine($mParamItem)).')';
}
else if (\is_string($mParamItem))
{
} else if (\is_string($mParamItem)) {
$sReturn .= ' '.$mParamItem;
}
}
@ -606,7 +611,7 @@ class ImapClient extends \MailSo\Net\NetClient
protected function getCurrentTag() : string
{
return self::TAG_PREFIX.$this->iTagCount;
return $this->TAG_PREFIX.$this->iTagCount;
}
public function EscapeString(?string $sStringForEscape) : string
@ -616,6 +621,7 @@ class ImapClient extends \MailSo\Net\NetClient
}
/*
// literal-string
$this->hasCapability('LITERAL+')
if (\preg_match('/[\r\n\x00\x80-\xFF]/', $sStringForEscape)) {
return \sprintf("{%d}\r\n%s", \strlen($sStringForEscape), $sStringForEscape);
}
@ -635,7 +641,7 @@ class ImapClient extends \MailSo\Net\NetClient
*/
public function ServerID() : string
{
if ($this->IsSupported('ID')) {
if ($this->hasCapability('ID')) {
foreach ($this->SendRequestGetResponse('ID', [null]) as $oResponse) {
if ('ID' === $oResponse->ResponseList[1] && \is_array($oResponse->ResponseList[2])) {
$c = \count($oResponse->ResponseList[2]);
@ -657,7 +663,7 @@ class ImapClient extends \MailSo\Net\NetClient
public function Compress() : bool
{
try {
if ($this->IsSupported('COMPRESS=DEFLATE')) {
if ($this->hasCapability('COMPRESS=DEFLATE')) {
$this->SendRequestGetResponse('COMPRESS', ['DEFLATE']);
\stream_filter_append($this->ConnectionResource(), 'zlib.inflate');
\stream_filter_append($this->ConnectionResource(), 'zlib.deflate', STREAM_FILTER_WRITE, array(

View file

@ -17,75 +17,38 @@ namespace MailSo\Imap;
*/
class NamespaceResult
{
/**
* @var string
*/
private $sPersonal = '';
private string $sPersonal = '';
/**
* @var string
*/
private $sPersonalDelimiter = '';
private string $sPersonalDelimiter = '';
/*
private string $sOtherUser = '';
/**
* @var string
*/
private $sOtherUser = '';
private string $sOtherUserDelimiter = '';
/**
* @var string
*/
private $sOtherUserDelimiter = '';
private string $sShared = '';
/**
* @var string
*/
private $sShared = '';
/**
* @var string
*/
private $sSharedDelimiter = '';
public function InitByImapResponse(\MailSo\Imap\Response $oImapResponse) : self
private string $sSharedDelimiter = '';
*/
function __construct(Response $oImapResponse)
{
if ($oImapResponse)
{
if (isset($oImapResponse->ResponseList[2][0]) &&
\is_array($oImapResponse->ResponseList[2][0]) &&
2 <= \count($oImapResponse->ResponseList[2][0]))
{
$this->sPersonal = $oImapResponse->ResponseList[2][0][0];
$this->sPersonalDelimiter = $oImapResponse->ResponseList[2][0][1];
$this->sPersonal = 'INBOX'.$this->sPersonalDelimiter === \substr(\strtoupper($this->sPersonal), 0, 6) ?
'INBOX'.$this->sPersonalDelimiter.\substr($this->sPersonal, 6) : $this->sPersonal;
}
if (isset($oImapResponse->ResponseList[3][0]) &&
\is_array($oImapResponse->ResponseList[3][0]) &&
2 <= \count($oImapResponse->ResponseList[3][0]))
{
$this->sOtherUser = $oImapResponse->ResponseList[3][0][0];
$this->sOtherUserDelimiter = $oImapResponse->ResponseList[3][0][1];
$this->sOtherUser = 'INBOX'.$this->sOtherUserDelimiter === \substr(\strtoupper($this->sOtherUser), 0, 6) ?
'INBOX'.$this->sOtherUserDelimiter.\substr($this->sOtherUser, 6) : $this->sOtherUser;
}
if (isset($oImapResponse->ResponseList[4][0]) &&
\is_array($oImapResponse->ResponseList[4][0]) &&
2 <= \count($oImapResponse->ResponseList[4][0]))
{
$this->sShared = $oImapResponse->ResponseList[4][0][0];
$this->sSharedDelimiter = $oImapResponse->ResponseList[4][0][1];
$this->sShared = 'INBOX'.$this->sSharedDelimiter === \substr(\strtoupper($this->sShared), 0, 6) ?
'INBOX'.$this->sSharedDelimiter.\substr($this->sShared, 6) : $this->sShared;
}
$space = static::getNamespace($oImapResponse, 2);
if ($space) {
$this->sPersonal = $space[0];
$this->sPersonalDelimiter = $space[1];
}
/*
$space = static::getNamespace($oImapResponse, 3);
if ($space) {
$this->sOtherUser = $space[0];
$this->sOtherUserDelimiter = $space[1];
}
return $this;
$space = static::getNamespace($oImapResponse, 4);
if ($space) {
$this->sShared = $space[0];
$this->sSharedDelimiter = $space[1];
}
*/
}
public function GetPersonalNamespace() : string
@ -93,4 +56,20 @@ class NamespaceResult
return $this->sPersonal;
}
private static function getNamespace(Response $oImapResponse, int $section) : ?array
{
if (isset($oImapResponse->ResponseList[$section][0])
&& \is_array($oImapResponse->ResponseList[$section][0])
&& 2 <= \count($oImapResponse->ResponseList[$section][0]))
{
$sName = $oImapResponse->ResponseList[$section][0][0];
$sDelimiter = $oImapResponse->ResponseList[$section][0][1];
$sName = 'INBOX'.$sDelimiter === \substr(\strtoupper($sName), 0, 6)
? 'INBOX'.$sDelimiter.\substr($sName, 6)
: $sName;
return [$sName, $sDelimiter];
}
return null;
}
}

View file

@ -18,9 +18,9 @@ namespace MailSo\Imap\Requests;
*/
class ESEARCH extends Request
{
public
$sCriterias = 'ALL',
$aReturn = [
public string $sCriterias = 'ALL';
public array $aReturn = [
/**
ALL
Return all message numbers/UIDs which match the search criteria,
@ -41,21 +41,23 @@ class ESEARCH extends Request
Return all message numbers/UIDs which match the search criteria,
in the requested sort order, using a sequence-set.
*/
],
$bUid = true,
$sLimit = '',
$sCharset = '',
// https://datatracker.ietf.org/doc/html/rfc7377
$aMailboxes = [],
$aSubtrees = [],
$aSubtreesOne = [];
];
public bool $bUid = true;
public string $sLimit = '';
public string $sCharset = '';
// https://datatracker.ietf.org/doc/html/rfc7377
public array $aMailboxes = [];
public array $aSubtrees = [];
public array $aSubtreesOne = [];
function __construct(\MailSo\Imap\ImapClient $oImapClient)
{
if (!$oImapClient->IsSupported('ESEARCH')) {
$oImapClient->writeLogException(
new \MailSo\RuntimeException('ESEARCH is not supported'),
\LOG_ERR, true);
if (!$oImapClient->hasCapability('ESEARCH')) {
$oImapClient->writeLogException(new \MailSo\RuntimeException('ESEARCH is not supported'), \LOG_ERR);
}
parent::__construct($oImapClient);
}
@ -66,10 +68,8 @@ class ESEARCH extends Request
$aRequest = array();
/* // RFC 6203
if (false !== \stripos($this->sCriterias, 'FUZZY') && !$this->oImapClient->IsSupported('SEARCH=FUZZY')) {
$this->oImapClient->writeLogException(
new \MailSo\RuntimeException('SEARCH=FUZZY is not supported'),
\LOG_ERR, true);
if (false !== \stripos($this->sCriterias, 'FUZZY') && !$this->oImapClient->hasCapability('SEARCH=FUZZY')) {
$this->oImapClient->writeLogException(new \MailSo\RuntimeException('SEARCH=FUZZY is not supported'), \LOG_ERR);
}
*/
@ -87,10 +87,8 @@ class ESEARCH extends Request
$aFolders[] = $this->aSubtreesOne;
}
if ($aFolders) {
if (!$this->oImapClient->IsSupported('MULTISEARCH')) {
$this->oImapClient->writeLogException(
new \MailSo\RuntimeException('MULTISEARCH is not supported'),
\LOG_ERR, true);
if (!$this->oImapClient->hasCapability('MULTISEARCH')) {
$this->oImapClient->writeLogException(new \MailSo\RuntimeException('MULTISEARCH is not supported'), \LOG_ERR);
}
$sCmd = 'ESEARCH';
$aReques[] = 'IN';
@ -105,12 +103,10 @@ class ESEARCH extends Request
$aRequest[] = 'RETURN';
if ($this->aReturn) {
// RFC 5267 checks
if (!$this->oImapClient->IsSupported('CONTEXT=SEARCH')) {
if (!$this->oImapClient->hasCapability('CONTEXT=SEARCH')) {
foreach ($this->aReturn as $sReturn) {
if (\preg_match('/PARTIAL|UPDATE|CONTEXT/i', $sReturn)) {
$this->oImapClient->writeLogException(
new \MailSo\RuntimeException('CONTEXT=SEARCH is not supported'),
\LOG_ERR, true);
$this->oImapClient->writeLogException(new \MailSo\RuntimeException('CONTEXT=SEARCH is not supported'), \LOG_ERR);
}
}
}

View file

@ -13,8 +13,7 @@ namespace MailSo\Imap\Requests;
abstract class Request
{
protected
$oImapClient;
protected \MailSo\Imap\ImapClient $oImapClient;
function __construct(\MailSo\Imap\ImapClient $oImapClient)
{

View file

@ -21,14 +21,18 @@ namespace MailSo\Imap\Requests;
*/
class SORT extends Request
{
public
$sCriterias = 'ALL',
$sCharset = '',
$bUid = true,
$aSortTypes = [],
$sLimit = '',
// RFC 5267
$aReturn = [
public string $sCriterias = 'ALL';
public string $sCharset = '';
public bool $bUid = true;
public array $aSortTypes = [];
public string $sLimit = '';
// RFC 5267
public array $aReturn = [
/**
ALL
Return all message numbers/UIDs which match the search criteria,
@ -53,10 +57,8 @@ class SORT extends Request
function __construct(\MailSo\Imap\ImapClient $oImapClient)
{
if (!$oImapClient->IsSupported('SORT')) {
$oImapClient->writeLogException(
new \MailSo\RuntimeException('SORT is not supported'),
\LOG_ERR, true);
if (!$oImapClient->hasCapability('SORT')) {
$oImapClient->writeLogException(new \MailSo\RuntimeException('SORT is not supported'), \LOG_ERR);
}
parent::__construct($oImapClient);
}
@ -64,26 +66,20 @@ class SORT extends Request
public function SendRequest() : string
{
if (!$this->aSortTypes) {
$this->oImapClient->writeLogException(
new \MailSo\RuntimeException('SortTypes are missing'),
\LOG_ERR, true);
$this->oImapClient->writeLogException(new \MailSo\RuntimeException('SortTypes are missing'), \LOG_ERR);
}
$aRequest = array();
if ($this->aReturn) {
// RFC 5267 checks
if (!$this->oImapClient->IsSupported('ESORT')) {
$this->oImapClient->writeLogException(
new \MailSo\RuntimeException('ESORT is not supported'),
\LOG_ERR, true);
if (!$this->oImapClient->hasCapability('ESORT')) {
$this->oImapClient->writeLogException(new \MailSo\RuntimeException('ESORT is not supported'), \LOG_ERR);
}
if (!$this->oImapClient->IsSupported('CONTEXT=SORT')) {
if (!$this->oImapClient->hasCapability('CONTEXT=SORT')) {
foreach ($this->aReturn as $sReturn) {
if (\preg_match('/PARTIAL|UPDATE|CONTEXT/i', $sReturn)) {
$this->oImapClient->writeLogException(
new \MailSo\RuntimeException('CONTEXT=SORT is not supported'),
\LOG_ERR, true);
$this->oImapClient->writeLogException(new \MailSo\RuntimeException('CONTEXT=SORT is not supported'), \LOG_ERR);
}
}
}

View file

@ -20,35 +20,38 @@ namespace MailSo\Imap\Requests;
*/
class THREAD extends Request
{
public
$sAlgorithm = '', // ORDEREDSUBJECT or REFERENCES or REFS
$sCriterias = 'ALL',
$bUid = true;
// ORDEREDSUBJECT or REFERENCES or REFS
private string $sAlgorithm = '';
public string $sCriterias = 'ALL';
public bool $bUid = true;
function __construct(\MailSo\Imap\ImapClient $oImapClient)
{
if ($oImapClient->IsSupported('THREAD=REFS')) {
if ($oImapClient->hasCapability('THREAD=REFS')) {
$this->sAlgorithm = 'REFS';
} else if ($oImapClient->IsSupported('THREAD=REFERENCES')) {
} else if ($oImapClient->hasCapability('THREAD=REFERENCES')) {
$this->sAlgorithm = 'REFERENCES';
} else if ($oImapClient->IsSupported('THREAD=ORDEREDSUBJECT')) {
} else if ($oImapClient->hasCapability('THREAD=ORDEREDSUBJECT')) {
$this->sAlgorithm = 'ORDEREDSUBJECT';
} else {
$oImapClient->writeLogException(
new \MailSo\RuntimeException('THREAD is not supported'),
\LOG_ERR, true);
$oImapClient->writeLogException(new \MailSo\RuntimeException('THREAD is not supported'), \LOG_ERR);
}
parent::__construct($oImapClient);
}
public function setAlgorithm(string $sAlgorithm) : void
{
$sAlgorithm = \strtoupper($sAlgorithm);
if (!$this->oImapClient->hasCapability("THREAD={$sAlgorithm}")) {
$this->oImapClient->writeLogException(new \MailSo\RuntimeException("THREAD={$sAlgorithm} is not supported"), \LOG_ERR);
}
$this->sAlgorithm = $sAlgorithm;
}
public function SendRequestIterateResponse() : iterable
{
if (!$this->oImapClient->IsSupported(\strtoupper("THREAD={$this->sAlgorithm}"))) {
$this->oImapClient->writeLogException(
new \MailSo\RuntimeException("THREAD={$this->sAlgorithm} is not supported"),
\LOG_ERR, true);
}
$this->oImapClient->SendRequest(
($this->bUid ? 'UID THREAD' : 'THREAD'),
array(

View file

@ -19,46 +19,24 @@ use MailSo\Imap\Enumerations\ResponseType;
*/
class Response
{
/**
* @var array
*/
public $ResponseList = array();
public array $ResponseList = array();
/**
* @var array | null
*/
public $OptionalResponse = null;
public ?array $OptionalResponse = null;
/**
* @var string
*/
public $StatusOrIndex;
public ?string $StatusOrIndex = null;
/**
* @var string
*/
public $HumanReadable = '';
public string $HumanReadable = '';
/**
* @var bool
*/
public $IsStatusResponse = false;
public bool $IsStatusResponse = false;
/**
* @var int
*/
public $ResponseType = 0;
public int $ResponseType = 0;
/**
* @var string
*/
public $Tag;
public ?string $Tag = null;
private function recToLine(array $aList) : string
{
$aResult = array();
foreach ($aList as $mItem)
{
foreach ($aList as $mItem) {
$aResult[] = \is_array($mItem) ? '('.$this->recToLine($mItem).')' : (string) $mItem;
}
return \implode(' ', $aResult);

View file

@ -67,9 +67,7 @@ class ResponseCollection extends \MailSo\Base\Collection
}
if (\is_array($aList) && \count($aList)) {
$aList = \array_map('strtoupper', $aList);
\sort($aList);
return $aList;
return \array_map('strtoupper', $aList);
}
}
return null;

View file

@ -17,7 +17,7 @@ namespace MailSo\Imap\Responses;
*/
class ACL implements \JsonSerializable
{
private $rights;
private array $rights;
function __construct(array $rights)
{

View file

@ -19,8 +19,7 @@ namespace MailSo\Imap;
abstract class SearchCriterias
{
const
RegEx = 'in|e?mail|from|to|subject|has|is|date|since|before|text|body|size|larger|bigger|smaller|maxsize|minsize|keyword|older_than|newer_than';
RegEx = 'in|e?mail|from|to|subject|has|is|date|since|before|text|body|size|larger|bigger|smaller|maxsize|minsize|keyword|older_than|newer_than|on|senton|sentsince|sentbefore';
/**
https://datatracker.ietf.org/doc/html/rfc3501#section-6.4.4
@ -66,22 +65,22 @@ abstract class SearchCriterias
NOT <search-key>
Messages that do not match the specified search key.
ON <date>
ON <date>
Messages whose internal date (disregarding time and timezone)
is within the specified date.
OR <search-key1> <search-key2>
Messages that match either search key.
SENTBEFORE <date>
SENTBEFORE <date>
Messages whose [RFC-2822] Date: header (disregarding time and
timezone) is earlier than the specified date.
SENTON <date>
SENTON <date>
Messages whose [RFC-2822] Date: header (disregarding time and
timezone) is within the specified date.
SENTSINCE <date>
SENTSINCE <date>
Messages whose [RFC-2822] Date: header (disregarding time and
timezone) is within or later than the specified date.
@ -132,11 +131,6 @@ abstract class SearchCriterias
$iTimeFilter = 0;
$aCriteriasResult = array();
if (0 < \MailSo\Config::$MessageListDateFilter) {
$iD = \time() - 3600 * 24 * 30 * \MailSo\Config::$MessageListDateFilter;
$iTimeFilter = \gmmktime(1, 1, 1, \gmdate('n', $iD), 1, \gmdate('Y', $iD));
}
$sSearch = \trim($sSearch);
if (\strlen($sSearch)) {
$aLines = \preg_match('/^('.static::RegEx.'):/i', $sSearch)
@ -146,7 +140,7 @@ abstract class SearchCriterias
if (!$aLines) {
$sValue = static::escapeSearchString($oImapClient, $sSearch);
if (\MailSo\Config::$MessageListFastSimpleSearch) {
if ($oImapClient->Settings->fast_simple_search) {
$aCriteriasResult[] = 'OR OR OR';
$aCriteriasResult[] = 'FROM';
$aCriteriasResult[] = $sValue;
@ -161,7 +155,7 @@ abstract class SearchCriterias
$aCriteriasResult[] = $sValue;
}
} else {
if (isset($aLines['IN']) && $oImapClient->IsSupported('MULTISEARCH') && \in_array($aLines['IN'], ['subtree','subtree-one','mailboxes'])) {
if (isset($aLines['IN']) && $oImapClient->hasCapability('MULTISEARCH') && \in_array($aLines['IN'], ['subtree','subtree-one','mailboxes'])) {
$aCriteriasResult[] = "IN ({$aLines['IN']} \"{$sFolderName}\")";
}
@ -242,10 +236,14 @@ abstract class SearchCriterias
$iTimeFilter = \max($iTimeFilter, $sValue);
}
break;
case 'ON':
case 'SENTON':
case 'SENTSINCE':
case 'SENTBEFORE':
case 'BEFORE':
$sValue = static::parseSearchDate($sRawValue);
if ($sValue) {
$aCriteriasResult[] = 'BEFORE';
$aCriteriasResult[] = $sName;
$aCriteriasResult[] = \gmdate('j-M-Y', $sValue);
}
break;
@ -307,8 +305,8 @@ abstract class SearchCriterias
$aCriteriasResult[] = 'UNDELETED';
}
if (\MailSo\Config::$MessageListPermanentFilter) {
$aCriteriasResult[] = \MailSo\Config::$MessageListPermanentFilter;
if ($oImapClient->Settings->search_filter) {
$aCriteriasResult[] = $oImapClient->Settings->search_filter;
}
$sCriteriasResult = \trim(\implode(' ', $aCriteriasResult));
@ -318,8 +316,12 @@ abstract class SearchCriterias
public static function escapeSearchString(\MailSo\Imap\ImapClient $oImapClient, string $sSearch) : string
{
return !\MailSo\Base\Utils::IsAscii($sSearch)
? '{'.\strlen($sSearch).'}'."\r\n".$sSearch : $oImapClient->EscapeString($sSearch);
// https://github.com/the-djmaze/snappymail/issues/836
// return $oImapClient->EscapeString($sSearch);
// return \MailSo\Base\Utils::IsAscii($sSearch) || $oImapClient->hasCapability('QQMail'))
return (\MailSo\Base\Utils::IsAscii($sSearch) || !$oImapClient->hasCapability('LITERAL+'))
? $oImapClient->EscapeString($sSearch)
: '{'.\strlen($sSearch).'}'."\r\n{$sSearch}";
}
private static function parseSearchDate(string $sDate) : int
@ -379,6 +381,10 @@ abstract class SearchCriterias
case 'SMALLER':
case 'LARGER':
case 'SINCE':
case 'ON':
case 'SENTON':
case 'SENTSINCE':
case 'SENTBEFORE':
case 'BEFORE':
if (\strlen($mValue)) {
$aResult[$sName] = $mValue;

View file

@ -24,9 +24,9 @@ class SequenceSet /*extends \SplFixedArray*/ implements \Countable
/**
* By default the numbers are unique identifiers as this is more reliable.
*/
public $UID = true;
public bool $UID = true;
private $data = [];
private array $data = [];
public function __construct($mItems, bool $uid = true)
{

View file

@ -0,0 +1,106 @@
<?php
/*
* This file is part of MailSo.
*
* (c) 2022 DJMaze
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace MailSo\Imap;
/**
* @category MailSo
* @package Net
*/
class Settings extends \MailSo\Net\ConnectSettings
{
public int
$port = 143,
$timeout = 300;
public bool
$disable_list_status = false,
$disable_metadata = false,
$disable_move = false,
$disable_sort = false,
$disable_thread = false,
$disable_binary = false,
$expunge_all_on_delete = false,
$fast_simple_search = true,
$fetch_new_messages = true,
$force_select = false,
$message_all_headers = false;
public int
$body_text_limit = 0,
// $folder_list_limit = 200,
$message_list_limit = 0,
$thread_limit = 50;
public string
$search_filter = '';
public static function fromArray(array $aSettings) : self
{
$object = parent::fromArray($aSettings);
$options = [
'disable_list_status',
'disable_metadata',
'disable_move',
'disable_sort',
'disable_thread',
'disable_binary',
'expunge_all_on_delete',
'fast_simple_search',
'fetch_new_messages',
'force_select',
'message_all_headers'
];
foreach ($options as $option) {
if (isset($aSettings[$option])) {
$object->$option = !empty($aSettings[$option]);
}
}
$options = [
// 'body_text_limit',
// 'folder_list_limit',
'message_list_limit',
// 'thread_limit',
];
foreach ($options as $option) {
if (isset($aSettings[$option])) {
$object->$option = \intval($aSettings[$option]);
}
}
return $object;
}
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return \array_merge(
parent::jsonSerialize(),
[
// '@Object' => 'Object/ImapSettings',
'disable_list_status' => $this->disable_list_status,
'disable_metadata' => $this->disable_metadata,
'disable_move' => $this->disable_move,
'disable_sort' => $this->disable_sort,
'disable_thread' => $this->disable_thread,
'disable_binary' => $this->disable_binary,
'use_expunge_all_on_delete' => $this->expunge_all_on_delete,
// 'body_text_limit' => $this->body_text_limit,
'fast_simple_search' => $this->fast_simple_search,
// 'folder_list_limit' => $this->folder_list_limit,
'force_select' => $this->force_select,
'message_all_headers' => $this->message_all_headers,
'message_list_limit' => $this->message_list_limit,
'search_filter' => $this->search_filter,
// 'thread_limit' => $this->thread_limit
]
);
}
}

View file

@ -21,15 +21,9 @@ use MailSo\Imap\Exceptions\ResponseNotFoundException;
*/
trait ResponseParser
{
/**
* @var int
*/
private $iResponseBufParsedPos;
private int $iResponseBufParsedPos;
/**
* @var bool
*/
private $bNeedNext = true;
private bool $bNeedNext = true;
protected function partialParseResponse() : Response
{
@ -66,6 +60,8 @@ trait ResponseParser
return !$oImapResponse->IsStatusResponse || 2 < \count($oImapResponse->ResponseList);
}
private $sResponseBuffer = '';
/**
* @return array|string
* @throws \MailSo\RuntimeException
@ -87,35 +83,25 @@ trait ResponseParser
$sAtomBuilder = $bTreatAsAtom ? '' : null;
$aList = array();
if ($bRoot)
{
if ($bRoot) {
$aList =& $oImapResponse->ResponseList;
}
while (true)
{
if ($this->bNeedNext)
{
/**
* $this->sResponseBuffer is a single fgets() that ends with \r\n
*/
while (true) {
if ($this->bNeedNext) {
$iPos = 0;
$this->getNextBuffer();
$this->sResponseBuffer = $this->getNextBuffer();
$this->iResponseBufParsedPos = $iPos;
$this->bNeedNext = false;
}
$sChar = null;
if ($bIsGotoDefault)
{
if ($bIsGotoDefault) {
$bIsGotoDefault = false;
}
else
{
} else {
$iBufferEndIndex = \strlen($this->sResponseBuffer) - 3;
if ($iPos > $iBufferEndIndex)
{
if ($iPos > $iBufferEndIndex) {
break;
}
@ -185,9 +171,7 @@ trait ResponseParser
if (null !== $sLiteral) {
if (!$bTreatAsAtom) {
$aList[] = $sLiteral;
if (\MailSo\Config::$LogSimpleLiterals) {
$this->writeLog('{'.$iLiteralLen.'} '.$sLiteral, \LOG_INFO);
}
// $this->writeLog('{'.$iLiteralLen.'} '.$sLiteral, \LOG_INFO);
} else {
\SnappyMail\Log::notice('IMAP', 'Literal treated as atom and skipped');
}
@ -255,16 +239,14 @@ trait ResponseParser
default:
$iCharBlockStartPos = $iPos;
if ($bRoot && $oImapResponse->IsStatusResponse)
{
if ($bRoot && $oImapResponse->IsStatusResponse) {
$iPos = $iBufferEndIndex;
if ($iPos > $iCharBlockStartPos) {
$iCharBlockStartPos += \strspn($this->sResponseBuffer, ' ', $iCharBlockStartPos, $iPos - $iCharBlockStartPos);
}
}
while ($iPos <= $iBufferEndIndex)
{
while ($iPos <= $iBufferEndIndex) {
$sCharDef = $this->sResponseBuffer[$iPos];
switch (true)
{
@ -272,8 +254,7 @@ trait ResponseParser
++$iPos;
break;
case '[' === $sCharDef:
if (null === $sAtomBuilder)
{
if (null === $sAtomBuilder) {
$sAtomBuilder = '';
}
@ -285,8 +266,7 @@ trait ResponseParser
$oImapResponse, true, $sPreviousAtomUpperCase, '['
);
if (null !== $sListBlock)
{
if (null !== $sListBlock) {
$sAtomBuilder .= $sListBlock.']';
}
@ -303,33 +283,25 @@ trait ResponseParser
}
}
if ($iPos > $iCharBlockStartPos || null !== $sAtomBuilder)
{
if ($iPos > $iCharBlockStartPos || null !== $sAtomBuilder) {
$sLastCharBlock = \substr($this->sResponseBuffer, $iCharBlockStartPos, $iPos - $iCharBlockStartPos);
if (null === $sAtomBuilder)
{
if (null === $sAtomBuilder) {
$aList[] = 'NIL' === $sLastCharBlock ? null : $sLastCharBlock;
$sPreviousAtomUpperCase = \strtoupper($sLastCharBlock);
}
else
{
} else {
$sAtomBuilder .= $sLastCharBlock;
if (!$bTreatAsAtom)
{
if (!$bTreatAsAtom) {
$aList[] = $sAtomBuilder;
$sPreviousAtomUpperCase = \strtoupper($sAtomBuilder);
$sAtomBuilder = null;
}
}
if ($bRoot)
{
if (!isset($oImapResponse->Tag) && 1 === \count($aList))
{
if ($bRoot) {
if (!isset($oImapResponse->Tag) && 1 === \count($aList)) {
$oImapResponse->setTag($aList[0]);
if ($this->getCurrentTag() === $oImapResponse->Tag)
{
if ($this->getCurrentTag() === $oImapResponse->Tag) {
$oImapResponse->ResponseType = ResponseType::TAGGED;
}
}
@ -374,8 +346,7 @@ trait ResponseParser
$iLiteralSize = \strlen($sLiteral);
if ($iLiteralLen !== $iLiteralSize) {
$this->writeLog('Literal stream read warning "read '.$iLiteralSize.' of '.
$iLiteralLen.'" bytes', \LOG_WARNING);
$this->writeLog('Literal stream read warning "read '.$iLiteralSize.' of '.$iLiteralLen.'" bytes', \LOG_WARNING);
}
return $sLiteral;
}
@ -387,9 +358,9 @@ trait ResponseParser
}
$sLiteralAtomUpperCasePeek = '';
if (0 === \strpos($sLiteralAtomUpperCase, 'BODY')) {
if (\str_starts_with($sLiteralAtomUpperCase, 'BODY')) {
$sLiteralAtomUpperCasePeek = \str_replace('BODY', 'BODY.PEEK', $sLiteralAtomUpperCase);
} else if (0 === \strpos($sLiteralAtomUpperCase, 'BINARY')) {
} else if (\str_starts_with($sLiteralAtomUpperCase, 'BINARY')) {
$sLiteralAtomUpperCasePeek = \str_replace('BINARY', 'BINARY.PEEK', $sLiteralAtomUpperCase);
}
@ -401,8 +372,7 @@ trait ResponseParser
return false;
}
$rImapLiteralStream =
\MailSo\Base\StreamWrappers\Literal::CreateStream($this->ConnectionResource(), $iLiteralLen);
$rImapLiteralStream = \MailSo\Base\StreamWrappers\Literal::CreateStream($this->ConnectionResource(), $iLiteralLen);
$this->writeLog('Start Callback for '.$sParent.' / '.$sLiteralAtomUpperCase.
' - try to read '.$iLiteralLen.' bytes.', \LOG_INFO);
@ -411,14 +381,14 @@ trait ResponseParser
try
{
$this->aFetchCallbacks[$sFetchKey]($sParent, $sLiteralAtomUpperCase, $rImapLiteralStream);
$this->aFetchCallbacks[$sFetchKey]($sParent, $sLiteralAtomUpperCase, $rImapLiteralStream, $iLiteralLen);
}
catch (\Throwable $oException)
{
$this->writeLogException($oException);
$this->writeLogException($oException, \LOG_NOTICE, false);
}
if ($rImapLiteralStream) {
if (\is_resource($rImapLiteralStream)) {
$iNotReadLiteralLen = 0;
$bFeof = \feof($rImapLiteralStream);
@ -445,12 +415,10 @@ trait ResponseParser
\fclose($rImapLiteralStream);
if (0 < $iNotReadLiteralLen) {
$this->writeLog('Not read literal size is '.$iNotReadLiteralLen.' bytes.',
\LOG_WARNING);
$this->writeLog('Not read literal size is '.$iNotReadLiteralLen.' bytes.', \LOG_WARNING);
}
} else {
$this->writeLog('Literal stream is not resource after callback.',
\LOG_WARNING);
$this->writeLog('Literal stream is not resource after callback.', \LOG_WARNING);
}
$this->bRunningCallback = false;

View file

@ -22,84 +22,94 @@ namespace MailSo\Imap\Traits;
*/
trait Status
{
public
/**
* The number of messages in the mailbox.
* This response is named EXISTS as a result of a SELECT or EXAMINE command.
* @var int
*/
$MESSAGES,
public string $FullName;
/**
* The number of messages with the \Recent flag set.
* This response also occurs as a result of a SELECT or EXAMINE command.
* IMAP4rev2 deprecated.
* @var int
*/
$RECENT,
public bool $hasStatus = false;
/**
* The next unique identifier value of the mailbox.
* A 32-bit value
* This response also occurs as a result of a SELECT or EXAMINE command.
* @var int
*/
$UIDNEXT,
/**
* The number of messages in the mailbox.
* This response is named EXISTS as a result of a SELECT or EXAMINE command.
*/
public ?int $MESSAGES = null;
/**
* The unique identifier validity value of the mailbox.
* This response also occurs as a result of a SELECT or EXAMINE command.
* @var int
*/
$UIDVALIDITY,
/**
* The number of messages with the \Recent flag set.
* This response also occurs as a result of a SELECT or EXAMINE command.
* IMAP4rev2 deprecated.
*/
public ?int $RECENT = null;
/**
* The number of messages which do not have the \Seen flag set.
* This response also occurs as a result of a IMAP4rev1 SELECT or EXAMINE command,
* but then it is the message sequence number of the first unseen message.
* IMAP4rev2 deprecated on SELECT/EXAMINE.
* @var int
*/
$UNSEEN,
/**
* The next unique identifier value of the mailbox.
* A 32-bit value
* This response also occurs as a result of a SELECT or EXAMINE command.
*/
public ?int $UIDNEXT = null;
/**
* RFC 4551
* The highest mod-sequence value of all messages in the mailbox.
* This response also occurs as a result of a SELECT or EXAMINE command.
* @var int 1*DIGIT Positive unsigned 64-bit integer
*/
$HIGHESTMODSEQ,
/**
* The unique identifier validity value of the mailbox.
* This response also occurs as a result of a SELECT or EXAMINE command.
*/
public ?int $UIDVALIDITY = null;
/**
* RFC 7889
* Message upload size limit.
* @var int
*/
$APPENDLIMIT,
/**
* The number of messages which do not have the \Seen flag set.
* This response also occurs as a result of a IMAP4rev1 SELECT or EXAMINE command,
* but then it is the message sequence number of the first unseen message.
* IMAP4rev2 deprecated on SELECT/EXAMINE.
*/
public ?int $UNSEEN = null;
/**
* RFC 8474
* A server-allocated unique identifier for the mailbox.
* This response also occurs as a result of a CREATE, SELECT or EXAMINE command.
* @var string
*/
$MAILBOXID,
/**
* RFC 4551
* The highest mod-sequence value of all messages in the mailbox.
* This response also occurs as a result of a SELECT or EXAMINE command.
* 1*DIGIT Positive unsigned 64-bit integer
*/
public ?int $HIGHESTMODSEQ = null;
/**
* RFC 9051
* The total size of the mailbox in octets.
* @var int
*/
$SIZE;
/**
* RFC 7889
* Message upload size limit.
*/
public ?int $APPENDLIMIT = null;
public function getStatusItems() : array
/**
* RFC 8474
* A server-allocated unique identifier for the mailbox.
* This response also occurs as a result of a CREATE, SELECT or EXAMINE command.
*/
public ?string $MAILBOXID = null;
/**
* RFC 9051
* The total size of the mailbox in octets.
*/
public ?int $SIZE = null;
public ?string $etag = null;
public function generateETag(\MailSo\Imap\ImapClient $oImapClient) : void
{
return \array_filter(\get_object_vars($this), function($v, $k){
return \property_exists(__TRAIT__, $k);
}, ARRAY_FILTER_USE_BOTH);
if (!$this->hasStatus) {
// UNSEEN undefined when only SELECT/EXAMINE is used
\error_log("{$this->FullName} STATUS missing " . \print_r(\debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS),true));
return;
}
if (!isset($this->MESSAGES, $this->UIDNEXT)) {
return;
}
$this->etag = \md5('FolderHash/'. \implode('-', [
$this->FullName,
$this->MESSAGES,
$this->UIDNEXT,
$this->UIDVALIDITY,
$this->UNSEEN,
$this->HIGHESTMODSEQ,
$oImapClient->Hash()
]));
}
public function setStatus(string $name, $value) : bool
private function setStatusItem(string $name, $value) : bool
{
if ('EXISTS' === $name) {
$name = 'MESSAGES';
@ -107,10 +117,11 @@ trait Status
$name = 'MAILBOXID';
}
if (\property_exists(__TRAIT__, $name)) {
if ('MAILBOXID' !== $name) {
$value = (int) $value;
if ('MAILBOXID' === $name) {
$this->MAILBOXID = \base64_encode(\is_array($value) ? $value[0] : $value);
} else {
$this->$name = (int) $value;
}
$this->$name = $value;
return true;
}
return false;
@ -137,7 +148,7 @@ trait Status
// OK untagged responses
if (\is_array($oResponse->OptionalResponse)) {
if (\count($oResponse->OptionalResponse) > 1) {
$bResult = $this->setStatus($oResponse->OptionalResponse[0], $oResponse->OptionalResponse[1]);
$bResult = $this->setStatusItem($oResponse->OptionalResponse[0], $oResponse->OptionalResponse[1]);
}
}
@ -149,16 +160,18 @@ trait Status
&& \is_array($oResponse->ResponseList[3])) {
$c = \count($oResponse->ResponseList[3]);
for ($i = 0; $i < $c; $i += 2) {
$bResult |= $this->setStatus(
$bResult |= $this->setStatusItem(
$oResponse->ResponseList[3][$i],
$oResponse->ResponseList[3][$i+1]
);
}
$this->hasStatus = $bResult;
}
// SELECT or EXAMINE command
else if (\is_numeric($oResponse->ResponseList[1]) && \is_string($oResponse->ResponseList[2])) {
// UNSEEN deprecated in IMAP4rev2
if ('UNSEEN' !== $oResponse->ResponseList[2]) {
$bResult |= $this->setStatus($oResponse->ResponseList[2], $oResponse->ResponseList[1]);
$bResult |= $this->setStatusItem($oResponse->ResponseList[2], $oResponse->ResponseList[1]);
}
}
}

View file

@ -17,15 +17,9 @@ namespace MailSo\Log;
*/
abstract class Driver
{
/**
* @var string
*/
protected $sDatePattern = 'H:i:s';
/**
* @var string
*/
protected $sName = '';
protected string
$sDatePattern = 'H:i:s',
$sName = '';
/**
* @var array
@ -41,25 +35,13 @@ abstract class Driver
\LOG_DEBUG => '[DEBUG]'
];
/**
* @var bool
*/
protected $bGuidPrefix = true;
protected bool $bGuidPrefix = true;
/**
* @var DateTimeZone
*/
protected $oTimeZone;
protected \DateTimeZone $oTimeZone;
/**
* @var bool
*/
protected $bTimePrefix = true;
protected bool $bTimePrefix = true;
/**
* @var bool
*/
protected $bTypedPrefix = true;
protected bool $bTypedPrefix = true;
function __construct()
{

View file

@ -18,19 +18,12 @@ namespace MailSo\Log\Drivers;
*/
class File extends \MailSo\Log\Driver
{
/**
* @var string
*/
private $sLoggerFileName;
private string $sLoggerFileName;
function __construct(string $sLoggerFileName)
{
parent::__construct();
$this->SetLoggerFileName($sLoggerFileName);
}
public function SetLoggerFileName(string $sLoggerFileName)
{
$sLogFileDir = \dirname($sLoggerFileName);
if (!\is_dir($sLogFileDir)) {
\mkdir($sLogFileDir, 0755, true);
@ -40,19 +33,14 @@ class File extends \MailSo\Log\Driver
protected function writeImplementation($mDesc) : bool
{
return $this->writeToLogFile($mDesc);
if (\is_array($mDesc)) {
$mDesc = \implode("\n\t", $mDesc);
}
return \error_log($mDesc . "\n", 3, $this->sLoggerFileName);
}
protected function clearImplementation() : bool
{
return \unlink($this->sLoggerFileName);
}
private function writeToLogFile($mDesc) : bool
{
if (\is_array($mDesc)) {
$mDesc = \implode("\n\t", $mDesc);
}
return \error_log($mDesc . "\n", 3, $this->sLoggerFileName);
}
}

View file

@ -0,0 +1,33 @@
<?php
/*
* This file is part of MailSo.
*
* (c) 2023 DJMaze
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace MailSo\Log\Drivers;
/**
* @category MailSo
* @package Log
* @subpackage Drivers
*/
class StderrStream extends \MailSo\Log\Driver
{
protected function writeImplementation($mDesc) : bool
{
if (!\defined('STDERR')) {
\define('STDERR', \fopen('php://stderr', 'wb'));
}
return 0 < \fwrite(STDERR, $mDesc . "\n");
}
protected function clearImplementation() : bool
{
return true;
}
}

View file

@ -18,7 +18,7 @@ namespace MailSo\Log\Drivers;
*/
class Syslog extends \MailSo\Log\Driver
{
private $iLogLevel;
private int $iLogLevel = 0;
function __construct()
{

View file

@ -0,0 +1,52 @@
<?php
/*
* This file is part of MailSo.
*
* (c) 2014 Usenko Timur
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace MailSo\Log;
/**
* @category MailSo
* @package Log
*/
trait Inherit
{
protected ?Logger $oLogger = null;
public function Logger(): ?Logger
{
return $this->oLogger;
}
public function SetLogger(?Logger $oLogger): void
{
$this->oLogger = $oLogger;
}
public function logWrite(string $sDesc, int $iType = \LOG_INFO,
string $sName = '', bool $bSearchSecretWords = true, bool $bDiplayCrLf = false): bool
{
return $this->oLogger && $this->oLogger->Write($sDesc, $iType, $sName, $bSearchSecretWords, $bDiplayCrLf);
}
public function logException(\Throwable $oException, int $iType = \LOG_NOTICE, string $sName = ''): void
{
$this->oLogger && $this->oLogger->WriteException($oException, $iType, $sName);
}
public function logMask(string $sWord): void
{
$this->oLogger && $this->oLogger->AddSecret($sWord);
}
/*
public function logWriteDump($mValue, int $iType = \LOG_INFO, string $sName = '') : bool
public function logWriteExceptionShort(\Throwable $oException, int $iType = \LOG_NOTICE, string $sName = '') : void
*/
}

View file

@ -17,23 +17,67 @@ namespace MailSo\Log;
*/
class Logger extends \SplFixedArray
{
private $bUsed = false;
private bool $bUsed = false;
private $iLevel = \LOG_WARNING;
private int $iLevel = \LOG_WARNING;
private $aSecretWords = [];
private array $aSecretWords = [];
private $bShowSecrets = false;
private bool $bShowSecrets = false;
private static $SIGNALS = [
'SIGABRT',
'SIGALRM',
'SIGBABY',
'SIGBUS',
'SIGCHLD',
'SIGCLD',
// 'SIGCONT',
'SIGFPE',
'SIGHUP',
'SIGILL',
// 'SIGINT',
'SIGIO',
'SIGIOT',
// 'SIGKILL',
'SIGPIPE',
'SIGPOLL',
'SIGPROF',
'SIGPWR',
'SIGQUIT',
'SIGSEGV',
'SIGSTKFLT',
// 'SIGSTOP',
'SIGSYS',
'SIGTERM',
'SIGTRAP',
'SIGTSTP',
'SIGTTIN',
'SIGTTOU',
'SIGURG',
'SIGUSR1',
'SIGUSR2',
'SIGVTALRM',
'SIGWINCH',
'SIGXCPU',
'SIGXFSZ'
];
function __construct(bool $bMainLogger = false)
{
parent::__construct();
if ($bMainLogger)
{
\set_error_handler(array($this, '__phpErrorHandler'));
\set_exception_handler(array($this, '__phpExceptionHandler'));
\register_shutdown_function(array($this, '__loggerShutDown'));
if ($bMainLogger) {
\set_error_handler([$this, '__phpErrorHandler']);
\set_exception_handler([$this, '__phpExceptionHandler']);
\register_shutdown_function([$this, '__loggerShutDown']);
if (\is_callable('pcntl_signal')) {
\pcntl_async_signals(true);
foreach (static::$SIGNALS as $SIGNAL) {
\defined($SIGNAL) && \pcntl_signal(\constant($SIGNAL), [$this, 'signalHandler']);
}
}
}
}
@ -43,8 +87,7 @@ class Logger extends \SplFixedArray
public static function Guid() : string
{
static $sCache = null;
if (null === $sCache)
{
if (null === $sCache) {
$sCache = \substr(\MailSo\Base\Utils::Sha1Rand(), -8);
}
@ -152,11 +195,28 @@ class Logger extends \SplFixedArray
public function __loggerShutDown() : void
{
if ($this->bUsed) {
$error = \error_get_last();
$error && $this->Write('Last error: '.\json_encode($error));
$this->Write('Memory peak usage: '.\MailSo\Base\Utils::FormatFileSize(\memory_get_peak_usage(true), 2));
$this->Write('Time delta: '.(\microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']));
}
}
public function signalHandler($signo)
{
if (\SIGTERM == $signo) {
exit;
}
if ($this->bUsed) {
foreach (static::$SIGNALS as $SIGNAL) {
if (\defined($SIGNAL) && \constant($SIGNAL) == $signo) {
$this->Write("Caught {$SIGNAL}");
break;
}
}
}
}
public function Write(string $sDesc, int $iType = \LOG_INFO,
string $sName = '', bool $bSearchSecretWords = true, bool $bDiplayCrLf = false) : bool
{
@ -166,8 +226,7 @@ class Logger extends \SplFixedArray
$this->bUsed = true;
if ($bSearchSecretWords && !$this->bShowSecrets && $this->aSecretWords)
{
if ($bSearchSecretWords && !$this->bShowSecrets && $this->aSecretWords) {
$sDesc = \str_replace($this->aSecretWords, '*******', $sDesc);
}
@ -189,19 +248,21 @@ class Logger extends \SplFixedArray
return $this->Write(\print_r($mValue, true), $iType, $sName);
}
private $aExceptions = [];
public function WriteException(\Throwable $oException, int $iType = \LOG_NOTICE, string $sName = '') : void
{
if (empty($oException->__WRITTEN__)) {
$oException->__WRITTEN__ = true;
if (!\in_array($oException, $this->aExceptions)) {
$this->Write((string) $oException, $iType, $sName);
$this->aExceptions[] = $oException;
}
}
public function WriteExceptionShort(\Throwable $oException, int $iType = \LOG_NOTICE, string $sName = '') : void
{
if (empty($oException->__WRITTEN__)) {
$oException->__WRITTEN__ = true;
if (!\in_array($oException, $this->aExceptions)) {
$this->Write($oException->getMessage(), $iType, $sName);
$this->aExceptions[] = $oException;
}
}
}

View file

@ -11,30 +11,25 @@
namespace MailSo\Mail;
use MailSo\Imap\BodyStructure;
/**
* @category MailSo
* @package Mail
*/
class Attachment implements \JsonSerializable
{
/**
* @var string
*/
private $sFolder;
private string $sFolder;
/**
* @var int
*/
private $iUid;
private int $iUid;
/**
* @var \MailSo\Imap\BodyStructure
*/
private $oBodyStructure;
private ?BodyStructure $oBodyStructure;
function __construct()
function __construct(string $sFolder, int $iUid, BodyStructure $oBodyStructure)
{
$this->Clear();
$this->sFolder = $sFolder;
$this->iUid = $iUid;
$this->oBodyStructure = $oBodyStructure;
}
public function Clear() : self
@ -56,127 +51,18 @@ class Attachment implements \JsonSerializable
return $this->iUid;
}
public function MimeIndex() : string
public function __call(string $name, array $arguments) //: mixed
{
return $this->oBodyStructure ? $this->oBodyStructure->PartID() : '';
}
public function FileName(bool $bCalculateOnEmpty = false) : string
{
if (!$this->oBodyStructure) {
return '';
}
$sFileName = \trim($this->oBodyStructure->FileName());
if (\strlen($sFileName) || !$bCalculateOnEmpty) {
return $sFileName;
}
$sIdx = '-' . $this->MimeIndex();
$sMimeType = \strtolower(\trim($this->MimeType()));
if ('message/rfc822' === $sMimeType) {
return "message{$sIdx}.eml";
}
if ('text/calendar' === $sMimeType) {
return "calendar{$sIdx}.ics";
}
if ('text/plain' === $sMimeType) {
return "part{$sIdx}.txt";
}
if (\preg_match('@text/(vcard|html|csv|xml|css|asp)@', $sMimeType, $aMatch)
|| \preg_match('@image/(png|jpeg|gif|bmp|cgm|ief|tiff|webp)@', $sMimeType, $aMatch)) {
return "part{$sIdx}.{$aMatch[1]}";
}
if (\strlen($sMimeType)) {
return \str_replace('/', $sIdx.'.', $sMimeType);
}
return ($this->oBodyStructure->IsInline() ? 'inline' : 'part' ) . $sIdx;
}
public function MimeType() : string
{
return $this->oBodyStructure ? $this->oBodyStructure->ContentType() : '';
}
public function EncodedSize() : int
{
return $this->oBodyStructure ? $this->oBodyStructure->Size() : 0;
}
public function EstimatedSize() : int
{
return $this->oBodyStructure ? $this->oBodyStructure->EstimatedSize() : 0;
}
public function Cid() : string
{
return $this->oBodyStructure ? $this->oBodyStructure->ContentID() : '';
}
public function ContentLocation() : string
{
return $this->oBodyStructure ? $this->oBodyStructure->ContentLocation() : '';
}
public function IsInline() : bool
{
return $this->oBodyStructure ? $this->oBodyStructure->IsInline() : false;
}
public function IsImage() : bool
{
return $this->oBodyStructure ? $this->oBodyStructure->IsImage() : false;
}
public function IsArchive() : bool
{
return $this->oBodyStructure ? $this->oBodyStructure->IsArchive() : false;
}
public function IsPdf() : bool
{
return $this->oBodyStructure ? $this->oBodyStructure->IsPdf() : false;
}
public function IsDoc() : bool
{
return $this->oBodyStructure ? $this->oBodyStructure->IsDoc() : false;
}
public function IsPgpSignature() : bool
{
return $this->oBodyStructure ? $this->oBodyStructure->IsPgpSignature() : false;
}
public static function NewBodyStructureInstance(string $sFolder, int $iUid, \MailSo\Imap\BodyStructure $oBodyStructure) : self
{
return (new self)->InitByBodyStructure($sFolder, $iUid, $oBodyStructure);
}
public function InitByBodyStructure(string $sFolder, int $iUid, \MailSo\Imap\BodyStructure $oBodyStructure) : self
{
$this->sFolder = $sFolder;
$this->iUid = $iUid;
$this->oBodyStructure = $oBodyStructure;
return $this;
return $this->oBodyStructure->{$name}(...$arguments);
}
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return array(
return \array_merge([
'@Object' => 'Object/Attachment',
'Folder' => $this->Folder(),
'Uid' => (string) $this->Uid(),
'MimeIndex' => (string) $this->MimeIndex(),
'MimeType' => $this->MimeType(),
'FileName' => \MailSo\Base\Utils::SecureFileName($this->FileName(true)),
'EstimatedSize' => $this->EstimatedSize(),
'Cid' => $this->Cid(),
'ContentLocation' => $this->ContentLocation(),
'IsInline' => $this->IsInline()
);
'folder' => $this->sFolder,
'uid' => $this->iUid
], $this->oBodyStructure->jsonSerialize());
}
}

View file

@ -1,240 +0,0 @@
<?php
/*
* This file is part of MailSo.
*
* (c) 2014 Usenko Timur
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace MailSo\Mail;
use MailSo\Imap\Enumerations\FolderType;
use MailSo\Imap\Enumerations\MetadataKeys;
/**
* @category MailSo
* @package Mail
*/
class Folder implements \JsonSerializable
{
/**
* @var bool
*/
private $bExists;
/**
* @var bool
*/
private $bSubscribed;
/**
* @var \MailSo\Imap\Folder
*/
private $oImapFolder;
/**
* @throws \InvalidArgumentException
*/
function __construct(\MailSo\Imap\Folder $oImapFolder, bool $bSubscribed = true, bool $bExists = true)
{
$this->oImapFolder = $oImapFolder;
$this->bSubscribed = $bSubscribed || \in_array('\\subscribed', $oImapFolder->FlagsLowerCase());
$this->bExists = $bExists;
}
/**
* @throws \InvalidArgumentException
* @throws \InvalidArgumentException
*/
public static function NewNonExistentInstance(string $sFullName, string $sDelimiter) : self
{
return new self(
new \MailSo\Imap\Folder($sFullName, $sDelimiter, array('\\Noselect')), false, false);
}
public function Name() : string
{
return $this->oImapFolder->Name();
}
public function FullName() : string
{
return $this->oImapFolder->FullName();
}
public function NameRaw() : string
{
return $this->oImapFolder->NameRaw();
}
public function Delimiter() : string
{
return $this->oImapFolder->Delimiter();
}
public function FlagsLowerCase() : array
{
return $this->oImapFolder->FlagsLowerCase();
}
public function IsSubscribed() : bool
{
return $this->bSubscribed;
}
public function IsExists() : bool
{
return $this->bExists;
}
public function IsSelectable() : bool
{
return $this->bExists && $this->oImapFolder->IsSelectable();
}
public function Status() : array
{
return $this->oImapFolder->getStatusItems();
}
public function IsInbox() : bool
{
return $this->oImapFolder->IsInbox();
}
public function GetFolderListType() : int
{
$aFlags = $this->oImapFolder->FlagsLowerCase();
// RFC 6154
// $aFlags[] = \strtolower($this->oImapFolder->GetMetadata(MetadataKeys::SPECIALUSE));
switch (true)
{
case \in_array('\\inbox', $aFlags) || 'INBOX' === \strtoupper($this->FullName()):
return FolderType::INBOX;
case \in_array('\\sent', $aFlags):
case \in_array('\\sentmail', $aFlags):
return FolderType::SENT;
case \in_array('\\drafts', $aFlags):
return FolderType::DRAFTS;
case \in_array('\\junk', $aFlags):
case \in_array('\\spam', $aFlags):
return FolderType::JUNK;
case \in_array('\\trash', $aFlags):
case \in_array('\\bin', $aFlags):
return FolderType::TRASH;
case \in_array('\\important', $aFlags):
return FolderType::IMPORTANT;
case \in_array('\\flagged', $aFlags):
case \in_array('\\starred', $aFlags):
return FolderType::FLAGGED;
case \in_array('\\archive', $aFlags):
return FolderType::ARCHIVE;
case \in_array('\\all', $aFlags):
case \in_array('\\allmail', $aFlags):
return FolderType::ALL;
// TODO
// case 'Templates' === $this->FullName():
// return FolderType::TEMPLATES;
}
// Kolab
$type = $this->GetMetadata(MetadataKeys::KOLAB_CTYPE) ?: $this->GetMetadata(MetadataKeys::KOLAB_CTYPE_SHARED);
switch ($type)
{
/*
// TODO: Kolab
case 'event':
case 'event.default':
return FolderType::CALENDAR;
case 'contact':
case 'contact.default':
return FolderType::CONTACTS;
case 'task':
case 'task.default':
return FolderType::TASKS;
case 'note':
case 'note.default':
return FolderType::NOTES;
case 'file':
case 'file.default':
return FolderType::FILES;
case 'configuration':
return FolderType::CONFIGURATION;
case 'journal':
case 'journal.default':
return FolderType::JOURNAL;
*/
case 'mail.inbox':
return FolderType::INBOX;
// case 'mail.outbox':
case 'mail.sentitems':
return FolderType::SENT;
case 'mail.drafts':
return FolderType::DRAFTS;
case 'mail.junkemail':
return FolderType::JUNK;
case 'mail.wastebasket':
return FolderType::TRASH;
}
return FolderType::USER;
}
/**
* @return mixed
*/
public function GetMetadata(string $sName) : ?string
{
return $this->oImapFolder->GetMetadata($sName);
}
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
/*
$aExtended = null;
$aStatus = $this->oImapFolder->getStatusItems();
if ($aStatus && isset($aStatus['MESSAGES'], $aStatus['UNSEEN'], $aStatus['UIDNEXT'])) {
$aExtended = array(
'totalEmails' => (int) $aStatus['MESSAGES'],
'unreadEmails' => (int) $aStatus['UNSEEN'],
'UidNext' => (int) $aStatus['UIDNEXT'],
// 'Hash' => $this->MailClient()->GenerateFolderHash(
// $this->FullName(), $aStatus['MESSAGES'], $aStatus['UIDNEXT'],
// empty($aStatus['HIGHESTMODSEQ']) ? 0 : $aStatus['HIGHESTMODSEQ'])
);
}
*/
return array(
'@Object' => 'Object/Folder',
'Name' => $this->Name(),
'FullName' => $this->FullName(),
'Delimiter' => (string) $this->Delimiter(),
'isSubscribed' => $this->bSubscribed,
'Exists' => $this->bExists,
'Selectable' => $this->IsSelectable(),
'Flags' => $this->FlagsLowerCase(),
// 'Extended' => $aExtended,
// 'PermanentFlags' => $this->oImapFolder->PermanentFlags,
'Metadata' => $this->oImapFolder->Metadata(),
'UidNext' => $this->oImapFolder->UIDNEXT,
// RFC 8621
'totalEmails' => $this->oImapFolder->MESSAGES,
'unreadEmails' => $this->oImapFolder->UNSEEN,
'id' => $this->oImapFolder->MAILBOXID
);
}
}

File diff suppressed because it is too large Load diff

View file

@ -11,8 +11,8 @@
namespace MailSo\Mail;
use \MailSo\Base\Utils;
use \MailSo\Imap\Enumerations\FetchType;
use MailSo\Base\Utils;
use MailSo\Imap\Enumerations\FetchType;
/**
* @category MailSo
@ -20,114 +20,77 @@ use \MailSo\Imap\Enumerations\FetchType;
*/
class Message implements \JsonSerializable
{
private
private string
$sFolder = '',
$iUid = 0,
$sSubject = '',
$sMessageId = '',
$sContentType = '',
$iSize = 0,
$iSpamScore = 0,
$sSpamResult = '',
$bIsSpam = false,
$bHasVirus = null,
$iInternalTimeStampInUTC = 0,
$iHeaderTimeStampInUTC = 0,
$sHeaderDate = '',
// $aFlags = [],
$aFlagsLowerCase = [],
$sVirusScanned = '',
$InReplyTo = '',
$sPlain = '',
$sHtml = '',
$References = '',
$sDeliveryReceipt = '',
$ReadReceipt = '';
private ?string
/**
* https://www.rfc-editor.org/rfc/rfc8474#section-5
*/
$sEmailId = '',
$sThreadId = '',
$sEmailId = null,
$sThreadId = null,
// https://autocrypt.org/level1.html#the-autocrypt-header
$sAutocrypt = '';
private int
$Uid = 0,
$iSize = 0,
$SpamScore = 0,
$iInternalTimeStampInUTC = 0,
$iHeaderTimeStampInUTC = 0,
$iPriority = \MailSo\Mime\Enumerations\MessagePriority::NORMAL;
private bool
$bIsSpam = false;
private ?bool
/**
* @var \MailSo\Mime\EmailCollection
* null = not scanned
* true = scanned and infected
* false = scanned and no infection found
*/
$bHasVirus = null;
private array
$SPF = [],
$DKIM = [],
$DMARC = [],
// $aFlags = [],
$aFlagsLowerCase = [],
$UnsubsribeLinks = [],
$aThreadUIDs = [],
$aThreadUnseenUIDs = [];
private ?array $DraftInfo = null;
private ?array $pgpSigned = null;
private ?array $pgpEncrypted = null;
private ?\MailSo\Mime\EmailCollection
$oFrom = null,
$oSender = null,
$oReplyTo = null,
$oDeliveredTo = null,
$oTo = null,
$oCc = null,
$oBcc = null,
$oBcc = null;
$sInReplyTo = '',
private ?AttachmentCollection
$Attachments = null;
$sPlain = '',
$sHtml = '',
/**
* @var AttachmentCollection
*/
$oAttachments = null,
/**
* @var array
*/
$aDraftInfo = null,
$sReferences = '',
/**
* @var int
*/
$iPriority,
$sDeliveryReceipt = '',
$sReadReceipt = '',
// https://autocrypt.org/level1.html#the-autocrypt-header
$sAutocrypt = '',
$aUnsubsribeLinks = array(),
$aThreads = array(),
$aPgpSigned = null,
$aPgpEncrypted = null;
function __construct()
function __get($k)
{
$this->iPriority = \MailSo\Mime\Enumerations\MessagePriority::NORMAL;
}
public function Plain() : string
{
return $this->sPlain;
}
public function Html() : string
{
return $this->sHtml;
}
public function PgpSigned() : ?array
{
return $this->aPgpSigned;
}
public function PgpEncrypted() : ?array
{
return $this->aPgpEncrypted;
}
public function Folder() : string
{
return $this->sFolder;
}
public function Uid() : int
{
return $this->iUid;
}
public function MessageId() : string
{
return $this->sMessageId;
return \property_exists($this, $k) ? $this->$k : null;
}
public function Subject() : string
@ -135,164 +98,48 @@ class Message implements \JsonSerializable
return $this->sSubject;
}
public function ContentType() : string
{
return $this->sContentType;
}
public function Size() : int
{
return $this->iSize;
}
public function SpamScore() : int
{
return $this->iSpamScore;
}
private function setSpamScore($value) : void
{
$this->iSpamScore = \intval(\max(0, \min(100, $value)));
}
public function SpamResult() : string
{
return $this->sSpamResult;
}
public function IsSpam() : bool
{
return $this->bIsSpam;
}
/**
* null = not scanned
* true = scanned and infected
* false = scanned and no infection found
*/
public function HasVirus() : ?bool
{
return $this->bHasVirus;
}
public function InternalTimeStampInUTC() : int
{
return $this->iInternalTimeStampInUTC;
}
public function HeaderTimeStampInUTC() : int
{
return $this->iHeaderTimeStampInUTC;
}
public function HeaderDate() : string
{
return $this->sHeaderDate;
}
public function From() : ?\MailSo\Mime\EmailCollection
{
return $this->oFrom;
}
public function Priority() : int
public function Uid() : int
{
return $this->iPriority;
}
public function Sender() : ?\MailSo\Mime\EmailCollection
{
return $this->oSender;
}
public function ReplyTo() : ?\MailSo\Mime\EmailCollection
{
return $this->oReplyTo;
}
public function DeliveredTo() : ?\MailSo\Mime\EmailCollection
{
return $this->oDeliveredTo;
}
public function To() : ?\MailSo\Mime\EmailCollection
{
return $this->oTo;
}
public function Cc() : ?\MailSo\Mime\EmailCollection
{
return $this->oCc;
}
public function Bcc() : ?\MailSo\Mime\EmailCollection
{
return $this->oBcc;
return $this->Uid;
}
public function Attachments() : ?AttachmentCollection
{
return $this->oAttachments;
return $this->Attachments;
}
public function InReplyTo() : string
private function setSpamScore($value) : void
{
return $this->sInReplyTo;
$this->SpamScore = \intval(\max(0, \min(100, $value)));
}
public function References() : string
public function SetThreads(array $aThreadUIDs)
{
return $this->sReferences;
$this->aThreadUIDs = $aThreadUIDs;
}
public function DeliveryReceipt() : string
public function SetThreadUnseen(array $aUnseenUIDs)
{
return $this->sDeliveryReceipt;
$this->aThreadUnseenUIDs = $aUnseenUIDs;
}
public function ReadReceipt() : string
{
return $this->sReadReceipt;
}
public function UnsubsribeLinks() : array
{
return $this->aUnsubsribeLinks;
}
public function ReadingConfirmation() : string
{
return $this->ReadReceipt();
}
public function DraftInfo() : ?array
{
return $this->aDraftInfo;
}
public function Threads() : array
{
return $this->aThreads;
}
public function SetThreads(array $aThreads)
{
$this->aThreads = $aThreads;
}
public static function NewFetchResponseInstance(string $sFolder, \MailSo\Imap\FetchResponse $oFetchResponse, ?\MailSo\Imap\BodyStructure $oBodyStructure = null) : self
public static function fromFetchResponse(string $sFolder, \MailSo\Imap\FetchResponse $oFetchResponse, ?\MailSo\Imap\BodyStructure $oBodyStructure = null) : self
{
$oMessage = new self;
if (!$oBodyStructure)
{
if (!$oBodyStructure) {
$oBodyStructure = $oFetchResponse->GetFetchBodyStructure();
}
$aFlags = $oFetchResponse->GetFetchValue(FetchType::FLAGS) ?: [];
$oMessage->sFolder = $sFolder;
$oMessage->iUid = (int) $oFetchResponse->GetFetchValue(FetchType::UID);
$oMessage->Uid = (int) $oFetchResponse->GetFetchValue(FetchType::UID);
$oMessage->iSize = (int) $oFetchResponse->GetFetchValue(FetchType::RFC822_SIZE);
// $oMessage->aFlags = $aFlags;
$oMessage->aFlagsLowerCase = \array_map('mb_strtolower', \array_map('\\MailSo\\Base\\Utils::Utf7ModifiedToUtf8', $aFlags));
@ -300,31 +147,28 @@ class Message implements \JsonSerializable
$oFetchResponse->GetFetchValue(FetchType::INTERNALDATE)
);
$oMessage->sEmailId = $oFetchResponse->GetFetchValue(FetchType::EMAILID)
// ?: $oFetchResponse->GetFetchValue('X-GUID')
?: $oFetchResponse->GetFetchValue('X-GM-MSGID');
$oMessage->sThreadId = $oFetchResponse->GetFetchValue(FetchType::THREADID)
?: $oFetchResponse->GetFetchValue('X-GM-THRID');
// https://www.rfc-editor.org/rfc/rfc8474
$aEmailId = $oFetchResponse->GetFetchValue(FetchType::EMAILID);
$oMessage->sEmailId = $aEmailId ? $aEmailId[0] : $oFetchResponse->GetFetchValue('X-GM-MSGID');
// $oMessage->sEmailId = $oMessage->sEmailId ?: $oFetchResponse->GetFetchValue('X-GUID');
$aThreadId = $oFetchResponse->GetFetchValue(FetchType::THREADID);
$oMessage->sThreadId = $aThreadId ? $aThreadId[0] : $oFetchResponse->GetFetchValue('X-GM-THRID');
$sCharset = $oBodyStructure ? Utils::NormalizeCharset($oBodyStructure->SearchCharset()) : '';
$sHeaders = $oFetchResponse->GetHeaderFieldsValue();
if (\strlen($sHeaders))
{
$oHeaders = new \MailSo\Mime\HeaderCollection($sHeaders, false, $sCharset);
$oHeaders = \strlen($sHeaders) ? new \MailSo\Mime\HeaderCollection($sHeaders, false, $sCharset) : null;
if ($oHeaders) {
$sContentTypeCharset = $oHeaders->ParameterValue(
\MailSo\Mime\Enumerations\Header::CONTENT_TYPE,
\MailSo\Mime\Enumerations\Parameter::CHARSET
);
if (\strlen($sContentTypeCharset))
{
if (\strlen($sContentTypeCharset)) {
$sCharset = Utils::NormalizeCharset($sContentTypeCharset);
}
if (\strlen($sCharset))
{
if (\strlen($sCharset)) {
$oHeaders->SetParentCharset($sCharset);
}
@ -339,48 +183,32 @@ class Message implements \JsonSerializable
$oMessage->oCc = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::CC, $bCharsetAutoDetect);
$oMessage->oBcc = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::BCC, $bCharsetAutoDetect);
if ($oMessage->oFrom) {
$oHeaders->PopulateEmailColectionByDkim($oMessage->oFrom);
}
$oMessage->oSender = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::SENDER, $bCharsetAutoDetect);
$oMessage->oReplyTo = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::REPLY_TO, $bCharsetAutoDetect);
$oMessage->oDeliveredTo = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::DELIVERED_TO, $bCharsetAutoDetect);
$oMessage->sInReplyTo = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::IN_REPLY_TO);
$oMessage->sReferences = Utils::StripSpaces(
$oMessage->InReplyTo = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::IN_REPLY_TO);
$oMessage->References = Utils::StripSpaces(
$oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::REFERENCES));
$sHeaderDate = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::DATE);
$oMessage->sHeaderDate = $sHeaderDate;
$oMessage->iHeaderTimeStampInUTC = \MailSo\Base\DateTimeHelper::ParseRFC2822DateString($sHeaderDate);
$oMessage->iHeaderTimeStampInUTC = \MailSo\Base\DateTimeHelper::ParseRFC2822DateString(
$oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::DATE)
);
// Priority
$oMessage->iPriority = \MailSo\Mime\Enumerations\MessagePriority::NORMAL;
$sPriority = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_MSMAIL_PRIORITY);
if (!\strlen($sPriority))
{
$sPriority = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::IMPORTANCE);
}
if (!\strlen($sPriority))
{
$sPriority = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_PRIORITY);
}
if (\strlen($sPriority))
{
switch (\str_replace(' ', '', \strtolower($sPriority)))
$sPriority = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_MSMAIL_PRIORITY)
?: $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::IMPORTANCE)
?: $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_PRIORITY);
if (\strlen($sPriority)) {
switch (\substr(\trim($sPriority), 0, 1))
{
case 'high':
case '1(highest)':
case '2(high)':
case 'h':
case '1':
case '2':
$oMessage->iPriority = \MailSo\Mime\Enumerations\MessagePriority::HIGH;
break;
case 'low':
case '4(low)':
case '5(lowest)':
case 'l':
case '4':
case '5':
$oMessage->iPriority = \MailSo\Mime\Enumerations\MessagePriority::LOW;
@ -392,26 +220,19 @@ class Message implements \JsonSerializable
$oMessage->sDeliveryReceipt = \trim($oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::RETURN_RECEIPT_TO));
// Read Receipt
$oMessage->sReadReceipt = \trim($oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::DISPOSITION_NOTIFICATION_TO));
if (empty($oMessage->sReadReceipt))
{
$oMessage->sReadReceipt = \trim($oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_CONFIRM_READING_TO));
$oMessage->ReadReceipt = \trim($oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::DISPOSITION_NOTIFICATION_TO));
if (empty($oMessage->ReadReceipt)) {
$oMessage->ReadReceipt = \trim($oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::X_CONFIRM_READING_TO));
}
// Unsubscribe links
$oMessage->aUnsubsribeLinks = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::LIST_UNSUBSCRIBE);
if (empty($oMessage->aUnsubsribeLinks))
{
$oMessage->aUnsubsribeLinks = array();
}
else
{
$oMessage->aUnsubsribeLinks = explode(',', $oMessage->aUnsubsribeLinks);
$oMessage->aUnsubsribeLinks = array_map(
$UnsubsribeLinks = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::LIST_UNSUBSCRIBE);
if ($UnsubsribeLinks) {
$oMessage->UnsubsribeLinks = \array_map(
function ($link) {
return trim($link, ' <>');
},
$oMessage->aUnsubsribeLinks
\explode(',', $UnsubsribeLinks)
);
}
@ -480,7 +301,22 @@ class Message implements \JsonSerializable
}
if (\strlen($sType) && \strlen($sFolder) && $iUid) {
$oMessage->aDraftInfo = array($sType, $iUid, $sFolder);
$oMessage->DraftInfo = array($sType, $iUid, $sFolder);
}
}
$aAuth = $oHeaders->AuthStatuses();
$oMessage->SPF = $aAuth['spf'];
$oMessage->DKIM = $aAuth['dkim'];
$oMessage->DMARC = $aAuth['dmarc'];
if ($aAuth['dkim'] && $oMessage->oFrom) {
foreach ($oMessage->oFrom as $oEmail) {
$sEmail = $oEmail->GetEmail();
foreach ($aAuth['dkim'] as $aDkimData) {
if (\strpos($sEmail, $aDkimData[1])) {
$oEmail->SetDkimStatus($aDkimData[0]);
}
}
}
}
@ -500,16 +336,15 @@ class Message implements \JsonSerializable
$oMessage->oTo = $oFetchResponse->GetFetchEnvelopeEmailCollection(5, $sCharset);
$oMessage->oCc = $oFetchResponse->GetFetchEnvelopeEmailCollection(6, $sCharset);
$oMessage->oBcc = $oFetchResponse->GetFetchEnvelopeEmailCollection(7, $sCharset);
$oMessage->sInReplyTo = $oFetchResponse->GetFetchEnvelopeValue(8, '');
$oMessage->InReplyTo = $oFetchResponse->GetFetchEnvelopeValue(8, '');
}
if ($oBodyStructure)
{
if ($oBodyStructure) {
$gEncryptedParts = $oBodyStructure->SearchByContentType('multipart/encrypted');
foreach ($gEncryptedParts as $oPart) {
if ($oPart->IsPgpEncrypted()) {
$oMessage->aPgpEncrypted = [
'PartId' => $oPart->SubParts()[1]->PartID()
$oMessage->pgpEncrypted = [
'partId' => $oPart->SubParts()[1]->PartID()
];
}
}
@ -520,51 +355,47 @@ class Message implements \JsonSerializable
continue;
}
$oPgpSignaturePart = $oPart->SubParts()[1];
$oMessage->aPgpSigned = [
$oMessage->pgpSigned = [
// /?/Raw/&q[]=/0/Download/&q[]=/...
// /?/Raw/&q[]=/0/View/&q[]=/...
'BodyPartId' => $oPart->SubParts()[0]->PartID(),
'SigPartId' => $oPgpSignaturePart->PartID(),
'MicAlg' => (string) $oHeaders->ParameterValue(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'micalg')
'bodyPartId' => $oPart->SubParts()[0]->PartID(),
'sigPartId' => $oPgpSignaturePart->PartID(),
'micAlg' => $oHeaders ? (string) $oHeaders->ParameterValue(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'micalg') : ''
];
/*
// An empty section specification refers to the entire message, including the header.
// But Dovecot does not return it with BODY.PEEK[1], so we also use BODY.PEEK[1.MIME].
$sPgpText = \trim(
\trim($oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oMessage->aPgpSigned['BodyPartId'].'.MIME]'))
\trim($oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oMessage->pgpSigned['bodyPartId'].'.MIME]'))
. "\r\n\r\n"
. \trim($oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oMessage->aPgpSigned['BodyPartId'].']'))
. \trim($oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oMessage->pgpSigned['bodyPartId'].']'))
);
if ($sPgpText) {
$oMessage->aPgpSigned['Body'] = $sPgpText;
$oMessage->pgpSigned['body'] = $sPgpText;
}
$sPgpSignatureText = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oMessage->aPgpSigned['SigPartId'].']');
$sPgpSignatureText = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oMessage->pgpSigned['sigPartId'].']');
if ($sPgpSignatureText && 0 < \strpos($sPgpSignatureText, 'BEGIN PGP SIGNATURE')) {
$oMessage->aPgpSigned['Signature'] = $oPart->SubParts()[0]->PartID();
$oMessage->pgpSigned['signature'] = $oPart->SubParts()[0]->PartID();
}
*/
break;
}
$aTextParts = $oBodyStructure->GetHtmlAndPlainParts();
if ($aTextParts)
{
if ($aTextParts) {
$sCharset = $sCharset ?: \MailSo\Base\Enumerations\Charset::UTF_8;
$aHtmlParts = array();
$aPlainParts = array();
foreach ($aTextParts as $oPart)
{
foreach ($aTextParts as $oPart) {
$sText = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oPart->PartID().']');
if (null === $sText)
{
if (null === $sText) {
// TextPartIsTrimmed ?
$sText = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oPart->PartID().']<0>');
}
if (\is_string($sText) && \strlen($sText))
{
if (\is_string($sText) && \strlen($sText)) {
$sText = Utils::DecodeEncodingValue($sText, $oPart->MailEncodingName());
$sText = Utils::ConvertEncoding($sText,
Utils::NormalizeCharset($oPart->Charset() ?: $sCharset, true),
@ -574,34 +405,28 @@ class Message implements \JsonSerializable
// https://datatracker.ietf.org/doc/html/rfc4880#section-7
// Cleartext Signature
if (!$oMessage->aPgpSigned && \str_contains($sText, '-----BEGIN PGP SIGNED MESSAGE-----'))
{
$oMessage->aPgpSigned = [
'BodyPartId' => $oPart->PartID()
if (!$oMessage->pgpSigned && \str_contains($sText, '-----BEGIN PGP SIGNED MESSAGE-----')) {
$oMessage->pgpSigned = [
'bodyPartId' => $oPart->PartID()
];
}
if (\str_contains($sText, '-----BEGIN PGP MESSAGE-----'))
{
if (\str_contains($sText, '-----BEGIN PGP MESSAGE-----')) {
$keyIds = [];
if (\SnappyMail\PGP\GPG::isSupported()) {
$GPG = new \SnappyMail\PGP\GPG('');
$keyIds = $GPG->getEncryptedMessageKeys($sText);
}
$oMessage->aPgpEncrypted = [
'PartId' => $oPart->PartID(),
'KeyIds' => $keyIds
$oMessage->pgpEncrypted = [
'partId' => $oPart->PartID(),
'keyIds' => $keyIds
];
}
if ('text/html' === $oPart->ContentType())
{
if ('text/html' === $oPart->ContentType()) {
$aHtmlParts[] = $sText;
}
else
{
if ($oPart->IsFlowedFormat())
{
} else {
if ($oPart->IsFlowedFormat()) {
$sText = Utils::DecodeFlowedFormat($sText);
}
@ -617,14 +442,12 @@ class Message implements \JsonSerializable
}
$gAttachmentsParts = $oBodyStructure->SearchAttachmentsParts();
if ($gAttachmentsParts->valid())
{
$oMessage->oAttachments = new AttachmentCollection;
foreach ($gAttachmentsParts as /* @var $oAttachmentItem \MailSo\Imap\BodyStructure */ $oAttachmentItem)
{
if ($gAttachmentsParts->valid()) {
$oMessage->Attachments = new AttachmentCollection;
foreach ($gAttachmentsParts as /* @var $oAttachmentItem \MailSo\Imap\BodyStructure */ $oAttachmentItem) {
// if ('application/pgp-keys' === $oAttachmentItem->ContentType()) import ???
$oMessage->oAttachments->append(
Attachment::NewBodyStructureInstance($oMessage->sFolder, $oMessage->iUid, $oAttachmentItem)
$oMessage->Attachments->append(
new Attachment($oMessage->sFolder, $oMessage->Uid, $oAttachmentItem)
);
}
}
@ -633,6 +456,29 @@ class Message implements \JsonSerializable
return $oMessage;
}
public function ETag(string $sClientHash) : string
{
return \md5('MessageHash/' . \implode('/', [
$this->sFolder,
$this->Uid,
\implode(',', $this->getFlags()),
// \implode(',', $this->aThreadUIDs),
$sClientHash
]));
}
// https://datatracker.ietf.org/doc/html/rfc5788#section-3.4.1
// Thunderbird $label1 is same as $Important?
// Thunderbird $label4 is same as $todo?
protected function getFlags() : array
{
return \array_unique(\str_replace(
['$readreceipt', '$replied', /* 'junk', 'nonjunk', '$queued', '$sent', 'sent'*/],
['$mdnsent', '\\answered',/* '$junk', '$notjunk', '$submitpending', '$submitted', '$submitted'*/],
$this->aFlagsLowerCase
));
}
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
@ -644,55 +490,105 @@ class Message implements \JsonSerializable
$this->aFlagsLowerCase
), true);
*/
// https://datatracker.ietf.org/doc/html/rfc5788#section-3.4.1
// Thunderbird $label1 is same as $Important?
// Thunderbird $label4 is same as $todo?
$aFlags = \array_unique(\str_replace(
['$readreceipt', '$replied', /* 'junk', 'nonjunk', '$queued', '$sent', 'sent'*/],
['$mdnsent', '\\answered',/* '$junk', '$notjunk', '$submitpending', '$submitted', '$submitted'*/],
$this->aFlagsLowerCase
));
$aAutocrypt = [];
if ($this->sAutocrypt) {
foreach (\explode(';', $this->sAutocrypt) as $entry) {
$entry = \explode('=', \trim($entry), 2);
$aAutocrypt[$entry[0]] = $entry[1];
}
}
return array(
$aFlags = $this->getFlags();
$sReadReceipt = $this->ReadReceipt;
if (\strlen($sReadReceipt) && !\in_array('$forwarded', $aFlags)) {
try
{
if (!\MailSo\Mime\Email::Parse($sReadReceipt)) {
$sReadReceipt = '';
}
}
catch (\Throwable $oException)
{
$sReadReceipt = '';
}
}
$result = array(
'@Object' => 'Object/Message',
'Folder' => $this->sFolder,
'Uid' => $this->iUid,
'folder' => $this->sFolder,
'uid' => $this->Uid,
'hash' => \md5($this->sFolder . $this->Uid),
'subject' => \trim(Utils::Utf8Clear($this->sSubject)),
'encrypted' => 'multipart/encrypted' == $this->sContentType || $this->PgpEncrypted(),
'MessageId' => $this->sMessageId,
'SpamScore' => $this->bIsSpam ? 100 : $this->iSpamScore,
'SpamResult' => $this->sSpamResult,
'IsSpam' => $this->bIsSpam,
'HasVirus' => $this->bHasVirus,
'DateTimeStampInUTC' => $this->iInternalTimeStampInUTC,
'encrypted' => 'multipart/encrypted' == $this->sContentType || $this->pgpEncrypted,
'messageId' => $this->sMessageId,
'spamScore' => $this->bIsSpam ? 100 : $this->SpamScore,
'spamResult' => $this->sSpamResult,
'isSpam' => $this->bIsSpam,
'hasVirus' => $this->bHasVirus,
// 'virusScanned' => $this->sVirusScanned,
'dateTimestamp' => $this->iHeaderTimeStampInUTC ?: $this->iInternalTimeStampInUTC,
'internalTimestamp' => $this->iInternalTimeStampInUTC,
// \MailSo\Mime\EmailCollection
'From' => $this->oFrom,
'ReplyTo' => $this->oReplyTo,
'To' => $this->oTo,
'Cc' => $this->oCc,
'Bcc' => $this->oBcc,
'Sender' => $this->oSender,
'DeliveredTo' => $this->oDeliveredTo,
'from' => $this->oFrom,
'replyTo' => $this->oReplyTo,
'to' => $this->oTo,
'cc' => $this->oCc,
'bcc' => $this->oBcc,
'sender' => $this->oSender,
'deliveredTo' => $this->oDeliveredTo,
'Priority' => $this->iPriority,
'Threads' => $this->aThreads,
'UnsubsribeLinks' => $this->aUnsubsribeLinks,
'ReadReceipt' => '',
'Autocrypt' => $this->sAutocrypt,
'priority' => $this->iPriority,
'readReceipt' => $sReadReceipt,
'autocrypt' => $aAutocrypt ?: null,
'Attachments' => $this->oAttachments,
'attachments' => $this->Attachments,
'Flags' => $aFlags,
'spf' => $this->SPF,
'dkim' => $this->DKIM,
'dmarc' => $this->DMARC,
'flags' => $aFlags,
'inReplyTo' => $this->InReplyTo,
// https://datatracker.ietf.org/doc/html/rfc8621#section-4.1.1
'id' => $this->sEmailId,
// 'blobId' => $this->sEmailIdBlob,
'threadId' => $this->sThreadId,
// 'threadId' => $this->sThreadId,
// 'mailboxIds' => ['mailboxid'=>true],
// 'keywords' => $keywords,
'size' => $this->iSize,
'receivedAt' => \gmdate('Y-m-d\\TH:i:s\\Z', $this->iInternalTimeStampInUTC)
'size' => $this->iSize
);
if ($this->DraftInfo) {
$result['draftInfo'] = $this->DraftInfo;
}
if ($this->UnsubsribeLinks) {
$result['unsubsribeLinks'] = $this->UnsubsribeLinks;
}
if ($this->References) {
$result['references'] = $this->References;
}
if ($this->sHtml || $this->sPlain) {
$result['html'] = $this->sHtml;
$result['plain'] = $this->sPlain;
}
// $this->GetCapa(Capa::OPEN_PGP) || $this->GetCapa(Capa::GNUPG)
if ($this->pgpSigned) {
$result['pgpSigned'] = $this->pgpSigned;
}
if ($this->pgpEncrypted) {
$result['pgpEncrypted'] = $this->pgpEncrypted;
}
if ($this->aThreadUIDs) {
$result['threads'] = $this->aThreadUIDs;
}
if ($this->aThreadUnseenUIDs) {
$result['threadUnseen'] = $this->aThreadUnseenUIDs;
}
return $result;
}
}

View file

@ -18,52 +18,30 @@ namespace MailSo\Mail;
class MessageCollection extends \MailSo\Base\Collection
{
/**
* @var string
* Amount of UIDs in this list (could be less then total messages when using threads)
*/
public $FolderHash = '';
public int $totalEmails = 0;
/**
* @var int
*/
public $MessageResultCount = 0;
public ?int $totalThreads = null;
/**
* @var string
*/
public $FolderName = '';
public string $FolderName = '';
/**
* @var int
*/
public $Offset = 0;
public int $Offset = 0;
/**
* @var int
*/
public $Limit = 0;
public int $Limit = 0;
/**
* @var string
*/
public $Search = '';
public string $Search = '';
/**
* @var int
*/
public $ThreadUid = 0;
public int $ThreadUid = 0;
// MailSo\Imap\FolderInformation
public $FolderInfo = null;
/**
* @var array
*/
public $NewMessages = array();
public array $NewMessages = array();
/**
* @var bool
*/
public $Filtered = false;
// public bool $Filtered = false;
public bool $Limited = false;
public function append($oMessage, bool $bToTop = false) : void
{
@ -80,16 +58,16 @@ class MessageCollection extends \MailSo\Base\Collection
public function jsonSerialize()
{
return array_merge(parent::jsonSerialize(), array(
'MessageResultCount' => $this->MessageResultCount,
'Folder' => $this->FolderName,
'FolderHash' => $this->FolderHash,
'FolderInfo' => $this->FolderInfo,
'ThreadUid' => $this->ThreadUid,
'NewMessages' => $this->NewMessages,
'Filtered' => $this->Filtered,
'Offset' => $this->Offset,
'Limit' => $this->Limit,
'Search' => $this->Search
'totalEmails' => $this->totalEmails,
'totalThreads' => $this->totalThreads,
'threadUid' => $this->ThreadUid,
'newMessages' => $this->NewMessages,
// 'filtered' => $this->Filtered,
'offset' => $this->Offset,
'limit' => $this->Limit,
'search' => $this->Search,
'limited' => $this->Limited,
'folder' => $this->FolderInfo
));
}
}

View file

@ -13,21 +13,30 @@ namespace MailSo\Mail;
class MessageListParams
{
public
$sFolderName, // string
$sSearch = '', // string
$oCacher = null, // ?\MailSo\Cache\CacheClient
$bUseSortIfSupported = false, // bool
$bUseThreads = false, // bool
$bHideDeleted = true, // bool
$sSort = ''; // string
public string
$sFolderName,
$sSearch = '',
$sSort = '';
protected
public ?\MailSo\Cache\CacheClient
$oCacher = null;
public bool
$bUseSort = true,
$bUseThreads = false,
$bHideDeleted = true;
protected int
$iOffset = 0,
$iLimit = 10,
$iLimit = 0,
$iPrevUidNext = 0, // used to check for new messages
$iThreadUid = 0;
/**
* Messages with message sequence numbers corresponding to the specified message sequence number set.
*/
public ?\MailSo\Imap\SequenceSet $oSequenceSet = null;
public function __get($k)
{
return \property_exists($this, $k) ? $this->$k : null;
@ -38,7 +47,22 @@ class MessageListParams
if ('i' === $k[0]) {
$this->$k = \max(0, (int) $v);
}
// \MailSo\Base\Validator::RangeInt($oParams->iOffset, 0)
// \MailSo\Base\Validator::RangeInt($oParams->iLimit, 0, 999)
// 0 > $oParams->iOffset
// 0 > $oParams->iLimit
// 999 < $oParams->iLimit
}
public function hash() : string
{
return \md5(\implode('-', [
$this->sFolderName,
$this->iOffset,
$this->iLimit,
$this->bHideDeleted ? '1' : '0',
$this->sSearch,
$this->bUseSort ? $this->sSort : '',
$this->bUseThreads ? $this->iThreadUid : '',
$this->iPrevUidNext
]));
}
}

View file

@ -22,55 +22,41 @@ class Attachment
*/
private $rResource;
/**
* @var string
*/
private $sFileName;
private string $sFileName;
/**
* @var int
*/
private $iFileSize;
// private int $iFileSize;
/**
* @var string
*/
private $sCID;
private string $sContentID;
/**
* @var bool
*/
private $bIsInline;
private bool $bIsInline;
/**
* @var bool
*/
private $bIsLinked;
private bool $bIsLinked;
/**
* @var array
*/
private $aCustomContentTypeParams;
private array $aCustomContentTypeParams;
/**
* @var string
*/
private $sContentLocation;
private string $sContentLocation;
private string $sContentType;
/**
* @param resource $rResource
*/
function __construct($rResource, string $sFileName, int $iFileSize, bool $bIsInline,
bool $bIsLinked, string $sCID, array $aCustomContentTypeParams = [], string $sContentLocation = '')
bool $bIsLinked, string $sContentID, array $aCustomContentTypeParams = [],
string $sContentLocation = '', string $sContentType = '')
{
$this->rResource = $rResource;
$this->sFileName = $sFileName;
$this->iFileSize = $iFileSize;
// $this->iFileSize = $iFileSize;
$this->bIsInline = $bIsInline;
$this->bIsLinked = $bIsLinked;
$this->sCID = $sCID;
$this->sContentID = $sContentID;
$this->aCustomContentTypeParams = $aCustomContentTypeParams;
$this->sContentLocation = $sContentLocation;
$this->sContentType = $sContentType
?: \SnappyMail\File\MimeType::fromStream($rResource, $sFileName)
?: \SnappyMail\File\MimeType::fromFilename($sFileName)
?: 'application/octet-stream';
}
/**
@ -83,7 +69,7 @@ class Attachment
public function ContentType() : string
{
return \MailSo\Base\Utils::MimeContentType($this->sFileName);
return $this->sContentType;
}
public function CustomContentTypeParams() : array
@ -91,69 +77,33 @@ class Attachment
return $this->aCustomContentTypeParams;
}
public function CID() : string
{
return $this->sCID;
}
public function ContentLocation() : string
{
return $this->sContentLocation;
}
public function FileName() : string
{
return $this->sFileName;
}
public function FileSize() : int
{
return $this->iFileSize;
}
public function IsInline() : bool
public function isInline() : bool
{
return $this->bIsInline;
}
public function IsImage() : bool
public function isLinked() : bool
{
return 'image' === \MailSo\Base\Utils::ContentTypeType($this->ContentType(), $this->FileName());
}
public function IsArchive() : bool
{
return 'archive' === \MailSo\Base\Utils::ContentTypeType($this->ContentType(), $this->FileName());
}
public function IsPdf() : bool
{
return 'pdf' === \MailSo\Base\Utils::ContentTypeType($this->ContentType(), $this->FileName());
}
public function IsDoc() : bool
{
return 'doc' === \MailSo\Base\Utils::ContentTypeType($this->ContentType(), $this->FileName());
}
public function IsLinked() : bool
{
return $this->bIsLinked && \strlen($this->sCID);
return $this->bIsLinked && \strlen($this->sContentID);
}
public function ToPart() : Part
{
$oAttachmentPart = new Part;
$sFileName = $this->FileName();
$sCID = $this->CID();
$sContentLocation = $this->ContentLocation();
$sFileName = \trim($this->sFileName);
$sContentID = $this->sContentID;
$sContentLocation = $this->sContentLocation;
$oContentTypeParameters = null;
$oContentDispositionParameters = null;
if (\strlen(\trim($sFileName)))
{
if (\strlen($sFileName)) {
$oContentTypeParameters =
(new ParameterCollection)->Add(new Parameter(
Enumerations\Parameter::NAME, $sFileName));
@ -165,27 +115,25 @@ class Attachment
$oAttachmentPart->Headers->append(
new Header(Enumerations\Header::CONTENT_TYPE,
$this->ContentType().';'.
(($oContentTypeParameters) ? ' '.$oContentTypeParameters->ToString() : '')
$this->ContentType().
($oContentTypeParameters ? '; '.$oContentTypeParameters : '')
)
);
$oAttachmentPart->Headers->append(
new Header(Enumerations\Header::CONTENT_DISPOSITION,
($this->IsInline() ? 'inline' : 'attachment').';'.
(($oContentDispositionParameters) ? ' '.$oContentDispositionParameters->ToString() : '')
($this->isInline() ? 'inline' : 'attachment').
($oContentDispositionParameters ? '; '.$oContentDispositionParameters : '')
)
);
if (\strlen($sCID))
{
if (\strlen($sContentID)) {
$oAttachmentPart->Headers->append(
new Header(Enumerations\Header::CONTENT_ID, $sCID)
new Header(Enumerations\Header::CONTENT_ID, $sContentID)
);
}
if (\strlen($sContentLocation))
{
if (\strlen($sContentLocation)) {
$oAttachmentPart->Headers->append(
new Header(Enumerations\Header::CONTENT_LOCATION, $sContentLocation)
);
@ -193,8 +141,7 @@ class Attachment
$oAttachmentPart->Body = $this->Resource();
if ('message/rfc822' !== \strtolower($this->ContentType()))
{
if ('message/rfc822' !== \strtolower($this->ContentType())) {
$oAttachmentPart->Headers->append(
new Header(
Enumerations\Header::CONTENT_TRANSFER_ENCODING,
@ -202,17 +149,13 @@ class Attachment
)
);
if (\is_resource($oAttachmentPart->Body))
{
if (!\MailSo\Base\StreamWrappers\Binary::IsStreamRemembed($oAttachmentPart->Body))
{
$oAttachmentPart->Body =
\MailSo\Base\StreamWrappers\Binary::CreateStream($oAttachmentPart->Body,
\MailSo\Base\StreamWrappers\Binary::GetInlineDecodeOrEncodeFunctionName(
\MailSo\Base\Enumerations\Encoding::BASE64, false));
if (\is_resource($oAttachmentPart->Body) && !\MailSo\Base\StreamWrappers\Binary::IsStreamRemembed($oAttachmentPart->Body)) {
$oAttachmentPart->Body =
\MailSo\Base\StreamWrappers\Binary::CreateStream($oAttachmentPart->Body,
\MailSo\Base\StreamWrappers\Binary::GetInlineDecodeOrEncodeFunctionName(
\MailSo\Base\Enumerations\Encoding::BASE64, false));
\MailSo\Base\StreamWrappers\Binary::RememberStream($oAttachmentPart->Body);
}
\MailSo\Base\StreamWrappers\Binary::RememberStream($oAttachmentPart->Body);
}
}

View file

@ -17,33 +17,18 @@ namespace MailSo\Mime;
*/
class Email implements \JsonSerializable
{
/**
* @var string
*/
private $sDisplayName;
private string $sDisplayName;
/**
* @var string
*/
private $sEmail;
private string $sEmail;
/**
* @var string
*/
private $sDkimStatus;
/**
* @var string
*/
private $sDkimValue;
private string $sDkimStatus = Enumerations\DkimStatus::NONE;
/**
* @throws \InvalidArgumentException
*/
function __construct(string $sEmail, string $sDisplayName = '')
{
if (!\strlen(\trim($sEmail)))
{
if (!\strlen(\trim($sEmail)) && !\strlen(\trim($sDisplayName))) {
throw new \InvalidArgumentException;
}
@ -51,9 +36,6 @@ class Email implements \JsonSerializable
\MailSo\Base\Utils::Trim($sEmail), true);
$this->sDisplayName = \MailSo\Base\Utils::Trim($sDisplayName);
$this->sDkimStatus = Enumerations\DkimStatus::NONE;
$this->sDkimValue = '';
}
/**
@ -62,8 +44,7 @@ class Email implements \JsonSerializable
public static function Parse(string $sEmailAddress) : self
{
$sEmailAddress = \MailSo\Base\Utils::Trim($sEmailAddress);
if (!\strlen(\trim($sEmailAddress)))
{
if (!\strlen(\trim($sEmailAddress))) {
throw new \InvalidArgumentException;
}
@ -79,20 +60,16 @@ class Email implements \JsonSerializable
$iEndIndex = 0;
$iCurrentIndex = 0;
while ($iCurrentIndex < \strlen($sEmailAddress))
{
while ($iCurrentIndex < \strlen($sEmailAddress)) {
switch ($sEmailAddress[$iCurrentIndex])
{
// case '\'':
case '"':
// $sQuoteChar = $sEmailAddress[$iCurrentIndex];
if ((!$bInName) && (!$bInAddress) && (!$bInComment))
{
if (!$bInName && !$bInAddress && !$bInComment) {
$bInName = true;
$iStartIndex = $iCurrentIndex;
}
else if ((!$bInAddress) && (!$bInComment))
{
} else if (!$bInAddress && !$bInComment) {
$iEndIndex = $iCurrentIndex;
$sName = \substr($sEmailAddress, $iStartIndex + 1, $iEndIndex - $iStartIndex - 1);
$sEmailAddress = \substr_replace($sEmailAddress, '', $iStartIndex, $iEndIndex - $iStartIndex + 1);
@ -103,10 +80,8 @@ class Email implements \JsonSerializable
}
break;
case '<':
if ((!$bInName) && (!$bInAddress) && (!$bInComment))
{
if ($iCurrentIndex > 0 && \strlen($sName) === 0)
{
if (!$bInName && !$bInAddress && !$bInComment) {
if ($iCurrentIndex > 0 && !\strlen($sName)) {
$sName = \substr($sEmailAddress, 0, $iCurrentIndex);
}
@ -115,8 +90,7 @@ class Email implements \JsonSerializable
}
break;
case '>':
if ($bInAddress)
{
if ($bInAddress) {
$iEndIndex = $iCurrentIndex;
$sEmail = \substr($sEmailAddress, $iStartIndex + 1, $iEndIndex - $iStartIndex - 1);
$sEmailAddress = \substr_replace($sEmailAddress, '', $iStartIndex, $iEndIndex - $iStartIndex + 1);
@ -127,15 +101,13 @@ class Email implements \JsonSerializable
}
break;
case '(':
if ((!$bInName) && (!$bInAddress) && (!$bInComment))
{
if (!$bInName && !$bInAddress && !$bInComment) {
$bInComment = true;
$iStartIndex = $iCurrentIndex;
}
break;
case ')':
if ($bInComment)
{
if ($bInComment) {
$iEndIndex = $iCurrentIndex;
$sComment = \substr($sEmailAddress, $iStartIndex + 1, $iEndIndex - $iStartIndex - 1);
$sEmailAddress = \substr_replace($sEmailAddress, '', $iStartIndex, $iEndIndex - $iStartIndex + 1);
@ -146,28 +118,23 @@ class Email implements \JsonSerializable
}
break;
case '\\':
$iCurrentIndex++;
++$iCurrentIndex;
break;
}
$iCurrentIndex++;
++$iCurrentIndex;
}
if (\strlen($sEmail) === 0)
{
if (!\strlen($sEmail)) {
$aRegs = array('');
if (\preg_match('/[^@\s]+@\S+/i', $sEmailAddress, $aRegs) && isset($aRegs[0]))
{
if (\preg_match('/[^@\s]+@\S+/i', $sEmailAddress, $aRegs) && isset($aRegs[0])) {
$sEmail = $aRegs[0];
}
else
{
} else {
$sName = $sEmailAddress;
}
}
if ((\strlen($sEmail) > 0) && (\strlen($sName) == 0) && (\strlen($sComment) == 0))
{
if (\strlen($sEmail) && !\strlen($sName) && !\strlen($sComment)) {
$sName = \str_replace($sEmail, '', $sEmailAddress);
}
@ -196,16 +163,6 @@ class Email implements \JsonSerializable
return $this->sDisplayName;
}
public function GetDkimStatus() : string
{
return $this->sDkimStatus;
}
public function GetDkimValue() : string
{
return $this->sDkimValue;
}
public function GetAccountName() : string
{
return \MailSo\Base\Utils::GetAccountNameFromEmail($this->GetEmail(false));
@ -216,17 +173,9 @@ class Email implements \JsonSerializable
return \MailSo\Base\Utils::GetDomainFromEmail($this->GetEmail($bIdn));
}
public function SetDkimStatusAndValue(string $sDkimStatus, string $sDkimValue = '')
public function SetDkimStatus(string $sDkimStatus)
{
$this->sDkimStatus = Enumerations\DkimStatus::normalizeValue($sDkimStatus);
$this->sDkimValue = $sDkimValue;
}
public function ToArray(bool $bIdn = false, bool $bDkim = true) : array
{
return $bDkim ?
array($this->sDisplayName, $this->GetEmail($bIdn), $this->sDkimStatus, $this->sDkimValue) :
array($this->sDisplayName, $this->GetEmail($bIdn));
}
public function ToString(bool $bConvertSpecialsName = false, bool $bIdn = false) : string
@ -234,19 +183,16 @@ class Email implements \JsonSerializable
$sReturn = '';
$sDisplayName = \str_replace('"', '\"', $this->sDisplayName);
if ($bConvertSpecialsName)
{
$sDisplayName = 0 === \strlen($sDisplayName) ? '' : \MailSo\Base\Utils::EncodeUnencodedValue(
\MailSo\Base\Enumerations\Encoding::BASE64_SHORT,
$sDisplayName);
if ($bConvertSpecialsName) {
$sDisplayName = \strlen($sDisplayName)
? \MailSo\Base\Utils::EncodeUnencodedValue(\MailSo\Base\Enumerations\Encoding::BASE64_SHORT, $sDisplayName)
: '';
}
$sDisplayName = 0 === \strlen($sDisplayName) ? '' : '"'.$sDisplayName.'"';
if (\strlen($this->sEmail))
{
$sDisplayName = \strlen($sDisplayName) ? '"'.$sDisplayName.'"' : '';
if (\strlen($this->sEmail)) {
$sReturn = $this->GetEmail($bIdn);
if (\strlen($sDisplayName))
{
if (\strlen($sDisplayName)) {
$sReturn = $sDisplayName.' <'.$sReturn.'>';
}
}
@ -254,22 +200,19 @@ class Email implements \JsonSerializable
return \trim($sReturn);
}
public function __toString() : string
{
return $this->ToString();
}
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
/*
$BIMI = '';
if (Enumerations\DkimStatus::PASS == $this->GetDkimStatus()) {
$BIMI = \SnappyMail\DNS\BIMI($this->GetDomain());
}
*/
return array(
'@Object' => 'Object/Email',
'Name' => \MailSo\Base\Utils::Utf8Clear($this->GetDisplayName()),
'Email' => \MailSo\Base\Utils::Utf8Clear($this->GetEmail(true)),
'DkimStatus' => $this->GetDkimStatus(),
'DkimValue' => $this->GetDkimValue()
// 'BIMI' => $BIMI
'name' => \MailSo\Base\Utils::Utf8Clear($this->GetDisplayName()),
'email' => \MailSo\Base\Utils::Utf8Clear($this->GetEmail(true)),
'dkimStatus' => $this->sDkimStatus
);
}
}

View file

@ -33,21 +33,9 @@ class EmailCollection extends \MailSo\Base\Collection
parent::append($oEmail, $bToTop);
}
public function ToArray() : array
{
$aReturn = array();
foreach ($this as $oEmail)
{
$aReturn[] = $oEmail->ToArray();
}
return $aReturn;
}
public function MergeWithOtherCollection(EmailCollection $oEmails) : self
{
foreach ($oEmails as $oEmail)
{
foreach ($oEmails as $oEmail) {
$this->append($oEmail);
}
@ -58,11 +46,9 @@ class EmailCollection extends \MailSo\Base\Collection
{
$aReturn = array();
foreach ($this as $oEmail)
{
foreach ($this as $oEmail) {
$sEmail = $oEmail->GetEmail();
if (!isset($aReturn[$sEmail]))
{
if (!isset($aReturn[$sEmail])) {
$aReturn[$sEmail] = $oEmail;
}
}
@ -73,22 +59,25 @@ class EmailCollection extends \MailSo\Base\Collection
public function ToString(bool $bConvertSpecialsName = false, bool $bIdn = false) : string
{
$aReturn = array();
foreach ($this as $oEmail)
{
foreach ($this as $oEmail) {
$aReturn[] = $oEmail->ToString($bConvertSpecialsName, $bIdn);
}
return \implode(', ', $aReturn);
}
public function __toString() : string
{
return $this->ToString();
}
private function parseEmailAddresses(string $sRawEmails) : void
{
// $sRawEmails = \MailSo\Base\Utils::Trim($sRawEmails);
$sRawEmails = \trim($sRawEmails);
$sWorkingRecipientsLen = \strlen($sRawEmails);
if (!$sWorkingRecipientsLen)
{
if (!$sWorkingRecipientsLen) {
return;
}
@ -102,59 +91,49 @@ class EmailCollection extends \MailSo\Base\Collection
$iCurrentPos = 0;
while ($iCurrentPos < $sWorkingRecipientsLen)
{
while ($iCurrentPos < $sWorkingRecipientsLen) {
switch ($sRawEmails[$iCurrentPos])
{
case '\'':
case '"':
if (!$bIsInQuotes)
{
if (!$bIsInQuotes) {
$sChQuote = $sRawEmails[$iCurrentPos];
$bIsInQuotes = true;
}
else if ($sChQuote == $sRawEmails[$iCurrentPos])
{
} else if ($sChQuote == $sRawEmails[$iCurrentPos]) {
$bIsInQuotes = false;
}
break;
case '<':
if (!$bIsInAngleBrackets)
{
if (!$bIsInAngleBrackets) {
$bIsInAngleBrackets = true;
if ($bIsInQuotes)
{
if ($bIsInQuotes) {
$bIsInQuotes = false;
}
}
break;
case '>':
if ($bIsInAngleBrackets)
{
if ($bIsInAngleBrackets) {
$bIsInAngleBrackets = false;
}
break;
case '(':
if (!$bIsInBrackets)
{
if (!$bIsInBrackets) {
$bIsInBrackets = true;
}
break;
case ')':
if ($bIsInBrackets)
{
if ($bIsInBrackets) {
$bIsInBrackets = false;
}
break;
case ',':
case ';':
if (!$bIsInAngleBrackets && !$bIsInBrackets && !$bIsInQuotes)
{
if (!$bIsInAngleBrackets && !$bIsInBrackets && !$bIsInQuotes) {
$iEmailEndPos = $iCurrentPos;
try
@ -175,8 +154,7 @@ class EmailCollection extends \MailSo\Base\Collection
++$iCurrentPos;
}
if ($iEmailStartPos < $iCurrentPos)
{
if ($iEmailStartPos < $iCurrentPos) {
try
{
$this->append(
@ -186,4 +164,11 @@ class EmailCollection extends \MailSo\Base\Collection
catch (\InvalidArgumentException $oException) {}
}
}
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return \array_slice($this->getArrayCopy(), 0, 100);
}
}

View file

@ -44,6 +44,7 @@ abstract class Header
CONTENT_DISPOSITION = 'Content-Disposition',
CONTENT_DESCRIPTION = 'Content-Description',
CONTENT_ID = 'Content-ID',
// CONTENT_BASE = 'Content-Base', // rfc2110
CONTENT_LOCATION = 'Content-Location',
RECEIVED_SPF = 'Received-SPF',

View file

@ -1,41 +0,0 @@
<?php
/*
* This file is part of MailSo.
*
* (c) 2014 Usenko Timur
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace MailSo\Mime\Enumerations;
/**
* @category MailSo
* @package Mime
* @subpackage Enumerations
*/
abstract class MimeType
{
const TEXT_PLAIN = 'text/plain';
const MULTIPART_ALTERNATIVE = 'multipart/alternative';
const MULTIPART_RELATED = 'multipart/related';
const MULTIPART_MIXED = 'multipart/mixed';
const MULTIPART_SIGNED = 'multipart/signed';
const APPLICATION_PGP_SIGNATURE = 'application/pgp-signature';
const MULTIPART_ENCRYPTED = 'multipart/encrypted';
const APPLICATION_PGP_ENCRYPTED = 'application/pgp-encrypted';
const APPLICATION_OCTET_STREAM = 'application/octet-stream';
const MESSAGE_RFC822 = 'message/rfc822';
const MESSAGE_PARTIAL = 'message/partial';
const MESSAGE_REPORT = 'message/report';
const APPLICATION_MS_TNEF = 'application/ms-tnef';
const APPLICATION_PKCS7_MIME = 'application/pkcs7-mime';
const APPLICATION_PKCS7_SIGNATURE = 'application/pkcs7-signature';
}

View file

@ -17,35 +17,17 @@ namespace MailSo\Mime;
*/
class Header
{
/**
* @var string
*/
private $sName;
private string $sName;
/**
* @var string
*/
private $sValue;
private string $sValue;
/**
* @var string
*/
private $sFullValue;
private string $sFullValue;
/**
* @var string
*/
private $sEncodedValueForReparse;
private string $sEncodedValueForReparse;
/**
* @var ParameterCollection
*/
private $oParameters;
private ?ParameterCollection $oParameters = null;
/**
* @var strign
*/
private $sParentCharset;
private string $sParentCharset;
function __construct(string $sName, string $sValue = '', string $sEncodedValueForReparse = '', string $sParentCharset = '')
{
@ -59,26 +41,19 @@ class Header
$this->sFullValue = \trim($sValue);
$this->sEncodedValueForReparse = '';
if (\strlen($sEncodedValueForReparse) && $this->IsReparsed())
{
if (\strlen($sEncodedValueForReparse) && $this->IsReparsed()) {
$this->sEncodedValueForReparse = \trim($sEncodedValueForReparse);
}
if (\strlen($this->sFullValue) && $this->IsParameterized())
{
if (\strlen($this->sFullValue) && $this->IsParameterized()) {
$aRawExplode = \explode(';', $this->sFullValue, 2);
if (2 === \count($aRawExplode))
{
if (2 === \count($aRawExplode)) {
$this->sValue = $aRawExplode[0];
$this->oParameters = new ParameterCollection($aRawExplode[1]);
}
else
{
} else {
$this->sValue = $this->sFullValue;
}
}
else
{
} else {
$this->sValue = $this->sFullValue;
}
@ -89,14 +64,12 @@ class Header
public static function NewInstanceFromEncodedString(string $sEncodedLines, string $sIncomingCharset = \MailSo\Base\Enumerations\Charset::ISO_8859_1) : Header
{
if (empty($sIncomingCharset))
{
if (empty($sIncomingCharset)) {
$sIncomingCharset = \MailSo\Base\Enumerations\Charset::ISO_8859_1;
}
$aParts = \explode(':', \str_replace("\r", '', $sEncodedLines), 2);
if (isset($aParts[0]) && isset($aParts[1]) && \strlen($aParts[0]) && \strlen($aParts[1]))
{
if (isset($aParts[0]) && isset($aParts[1]) && \strlen($aParts[0]) && \strlen($aParts[1])) {
return new self(
\trim($aParts[0]),
\trim(\MailSo\Base\Utils::DecodeHeaderValue(\trim($aParts[1]), $sIncomingCharset)),
@ -130,8 +103,7 @@ class Header
public function SetParentCharset(string $sParentCharset) : Header
{
if ($this->sParentCharset !== $sParentCharset && $this->IsReparsed() && \strlen($this->sEncodedValueForReparse))
{
if ($this->sParentCharset !== $sParentCharset && $this->IsReparsed() && \strlen($this->sEncodedValueForReparse)) {
$this->initInputData(
$this->sName,
\trim(\MailSo\Base\Utils::DecodeHeaderValue($this->sEncodedValueForReparse, $sParentCharset)),
@ -154,22 +126,12 @@ class Header
$this->oParameters->setParameter($sName, $sValue);
}
private function wordWrapHelper(string $sValue, string $sGlue = "\r\n ") : string
{
return \trim(\substr(\wordwrap($this->NameWithDelimitrom().$sValue,
74, $sGlue
), \strlen($this->NameWithDelimitrom())));
}
public function __toString() : string
{
$sResult = $this->sFullValue;
if ($this->IsSubject())
{
if (!\MailSo\Base\Utils::IsAscii($sResult) &&
\function_exists('iconv_mime_encode'))
{
if ($this->IsSubject()) {
if (!\MailSo\Base\Utils::IsAscii($sResult) && \function_exists('iconv_mime_encode')) {
$aPreferences = array(
// 'scheme' => \MailSo\Base\Enumerations\Encoding::QUOTED_PRINTABLE_SHORT,
'scheme' => \MailSo\Base\Enumerations\Encoding::BASE64_SHORT,
@ -182,20 +144,20 @@ class Header
return \iconv_mime_encode($this->Name(), $sResult, $aPreferences);
}
}
else if ($this->IsParameterized() && 0 < $this->oParameters->Count())
else if ($this->IsParameterized() && 0 < $this->oParameters->count())
{
$sResult = $this->sValue.'; '.$this->oParameters->ToString(true);
}
else if ($this->IsEmail())
{
$oEmailCollection = new EmailCollection($this->sFullValue);
if ($oEmailCollection && 0 < $oEmailCollection->Count())
{
$sResult = $oEmailCollection->ToString(true, false);
if ($oEmailCollection && $oEmailCollection->count()) {
$sResult = $oEmailCollection->ToString(true);
}
}
return $this->NameWithDelimitrom().$this->wordWrapHelper($sResult);
// https://www.rfc-editor.org/rfc/rfc2822#section-2.1.1
return \wordwrap($this->NameWithDelimitrom() . $sResult, 78, "\r\n ");
}
public function IsSubject() : bool

View file

@ -18,12 +18,9 @@ namespace MailSo\Mime;
class HeaderCollection extends \MailSo\Base\Collection
{
protected $sRawHeaders = '';
protected string $sRawHeaders = '';
/**
* @var string
*/
protected $sParentCharset = '';
protected string $sParentCharset = '';
function __construct(string $sRawHeaders = '', bool $bStoreRawHeaders = true, string $sParentCharset = '')
{
@ -79,12 +76,9 @@ class HeaderCollection extends \MailSo\Base\Collection
public function GetAsEmailCollection(string $sHeaderName, bool $bCharsetAutoDetect = false) : ?EmailCollection
{
$oResult = null;
$sValue = $this->ValueByName($sHeaderName, $bCharsetAutoDetect);
if (\strlen($sValue)) {
$oResult = new EmailCollection($sValue);
}
return $oResult && 0 < $oResult->Count() ? $oResult : null;
return new EmailCollection(
$this->ValueByName($sHeaderName, $bCharsetAutoDetect)
);
}
public function ParametersByName(string $sHeaderName) : ?ParameterCollection
@ -149,40 +143,29 @@ class HeaderCollection extends \MailSo\Base\Collection
}
$sFirstChar = \substr($sHeadersValue, 0, 1);
if ($sFirstChar !== ' ' && $sFirstChar !== "\t" && false === \strpos($sHeadersValue, ':'))
{
if ($sFirstChar !== ' ' && $sFirstChar !== "\t" && false === \strpos($sHeadersValue, ':')) {
continue;
}
else if (null !== $sName && ($sFirstChar === ' ' || $sFirstChar === "\t"))
{
if (null !== $sName && ($sFirstChar === ' ' || $sFirstChar === "\t")) {
$sValue = \is_null($sValue) ? '' : $sValue;
if ('?=' === \substr(\rtrim($sHeadersValue), -2))
{
if ('?=' === \substr(\rtrim($sHeadersValue), -2)) {
$sHeadersValue = \rtrim($sHeadersValue);
}
if ('=?' === \substr(\ltrim($sHeadersValue), 0, 2))
{
if ('=?' === \substr(\ltrim($sHeadersValue), 0, 2)) {
$sHeadersValue = \ltrim($sHeadersValue);
}
if ('=?' === \substr($sHeadersValue, 0, 2))
{
if ('=?' === \substr($sHeadersValue, 0, 2)) {
$sValue .= $sHeadersValue;
}
else
{
} else {
$sValue .= "\n".$sHeadersValue;
}
}
else
{
if (null !== $sName)
{
} else {
if (null !== $sName) {
$oHeader = Header::NewInstanceFromEncodedString($sName.': '.$sValue, $this->sParentCharset);
if ($oHeader)
{
if ($oHeader) {
$this->append($oHeader);
}
@ -194,18 +177,15 @@ class HeaderCollection extends \MailSo\Base\Collection
$sName = $aHeaderParts[0];
$sValue = isset($aHeaderParts[1]) ? $aHeaderParts[1] : '';
if ('?=' === \substr(\rtrim($sValue), -2))
{
if ('?=' === \substr(\rtrim($sValue), -2)) {
$sValue = \rtrim($sValue);
}
}
}
if (null !== $sName)
{
if (null !== $sName) {
$oHeader = Header::NewInstanceFromEncodedString($sName.': '.$sValue, $this->sParentCharset);
if ($oHeader)
{
if ($oHeader) {
$this->append($oHeader);
}
}
@ -213,72 +193,46 @@ class HeaderCollection extends \MailSo\Base\Collection
return $this;
}
public function DkimStatuses() : array
/**
* https://www.rfc-editor.org/rfc/rfc8601
* dkim=pass header.d=domain.tld header.s=s1 header.b=F2SfoZWw;
* spf=pass (ORIGINATING: domain of "snappymail@domain.tld" designates 0.0.0.0 as permitted sender) smtp.mailfrom="snappymail@domain.tld";
* dmarc=fail reason="SPF not aligned (relaxed), DKIM not aligned (relaxed)" header.from=domain.tld (policy=none)
*/
public function AuthStatuses() : array
{
$aResult = array();
$aResult = [
'dkim' => [],
'dmarc' => [],
'spf' => []
];
$aHeaders = $this->ValuesByName(Enumerations\Header::AUTHENTICATION_RESULTS);
if (\count($aHeaders))
{
foreach ($aHeaders as $sHeaderValue)
{
$sStatus = '';
$sHeader = '';
$sDkimLine = '';
$aMatch = array();
$sHeaderValue = \preg_replace('/[\r\n\t\s]+/', ' ', $sHeaderValue);
if (\preg_match('/dkim=.+/i', $sHeaderValue, $aMatch) && !empty($aMatch[0]))
{
$sDkimLine = $aMatch[0];
$aMatch = array();
if (\preg_match('/dkim=([a-zA-Z0-9]+)/i', $sDkimLine, $aMatch) && !empty($aMatch[1]))
{
$sStatus = $aMatch[1];
}
$aMatch = array();
if (\preg_match('/header\.(d|i|from)=([^\s;]+)/i', $sDkimLine, $aMatch) && !empty($aMatch[2]))
{
$sHeader = \trim($aMatch[2]);
}
if (!empty($sStatus) && !empty($sHeader))
{
$aResult[] = array($sStatus, $sHeader, $sDkimLine);
}
if (\count($aHeaders)) {
$aHeaders = \implode(';', $aHeaders);
$aHeaders = \preg_replace('/[\\r\\n\\t\\s]+/', ' ', $aHeaders);
$aHeaders = \explode(';', $aHeaders);
foreach ($aHeaders as $sLine) {
$aStatus = array();
$aHeader = array();
if (\preg_match("/(dkim|dmarc|spf)=([a-z0-9]+).*?(;|$)/Di", $sLine, $aStatus)
&& \preg_match('/(?:header\\.(?:d|i|from)|smtp.mailfrom)="?([^\\s;"]+)/i', $sLine, $aHeader)
) {
$sType = \strtolower($aStatus[1]);
$aResult[$sType][] = array(\strtolower($aStatus[2]), $aHeader[1], \trim($sLine));
}
}
}
else
{
if (!\count($aResult['dkim'])) {
// X-DKIM-Authentication-Results: signer="hostinger.com" status="pass"
$aHeaders = $this->ValuesByName(Enumerations\Header::X_DKIM_AUTHENTICATION_RESULTS);
foreach ($aHeaders as $sHeaderValue)
{
$sStatus = '';
$sHeader = '';
$aMatch = array();
$sHeaderValue = \preg_replace('/[\r\n\t\s]+/', ' ', $sHeaderValue);
if (\preg_match('/status[\s]?=[\s]?"([a-zA-Z0-9]+)"/i', $sHeaderValue, $aMatch) && !empty($aMatch[1]))
{
$sStatus = $aMatch[1];
}
if (\preg_match('/signer[\s]?=[\s]?"([^";]+)"/i', $sHeaderValue, $aMatch) && !empty($aMatch[1]))
{
$sHeader = \trim($aMatch[1]);
}
if (!empty($sStatus) && !empty($sHeader))
{
$aResult[] = array($sStatus, $sHeader, $sHeaderValue);
foreach ($aHeaders as $sHeaderValue) {
$aStatus = array();
$aHeader = array();
$sHeaderValue = \preg_replace('/[\\r\\n\\t\\s]+/', ' ', $sHeaderValue);
if (\preg_match('/status[\\s]?=[\\s]?"([a-zA-Z0-9]+)"/i', $sHeaderValue, $aStatus) && !empty($aStatus[1])
&& \preg_match('/signer[\\s]?=[\\s]?"([^";]+)"/i', $sHeaderValue, $aHeader) && !empty($aHeader[1])
) {
$aResult['dkim'][] = array($aStatus[1], \trim($aHeader[1]), $sHeaderValue);
}
}
}
@ -286,21 +240,6 @@ class HeaderCollection extends \MailSo\Base\Collection
return $aResult;
}
public function PopulateEmailColectionByDkim(EmailCollection $oEmails) : void
{
$aDkimStatuses = $this->DkimStatuses();
foreach ($oEmails as $oEmail) {
$sEmail = $oEmail->GetEmail();
foreach ($aDkimStatuses as $aDkimData) {
if (isset($aDkimData[0], $aDkimData[1]) &&
$aDkimData[1] === \strstr($sEmail, $aDkimData[1]))
{
$oEmail->SetDkimStatusAndValue($aDkimData[0], empty($aDkimData[2]) ? '' : $aDkimData[2]);
}
}
}
}
public function __toString() : string
{
return \implode("\r\n", $this->getArrayCopy());

View file

@ -17,10 +17,7 @@ namespace MailSo\Mime;
*/
class Message extends Part
{
/**
* @var array
*/
private $aHeadersValue = array(
private array $aHeadersValue = array(
/*
Enumerations\Header::BCC => '',
Enumerations\Header::CC => '',
@ -42,22 +39,11 @@ class Message extends Part
*/
);
/**
* @var AttachmentCollection
*/
private $oAttachmentCollection;
private AttachmentCollection $oAttachmentCollection;
/**
* @var bool
*/
private $bAddEmptyTextPart = true;
private bool $bAddEmptyTextPart = true;
/**
* @var bool
*/
private $bAddDefaultXMailer = true;
public $messageIdRequired = true;
private bool $bAddDefaultXMailer = true;
function __construct()
{
@ -177,8 +163,7 @@ class Message extends Part
public function SetCustomHeader(string $sHeaderName, string $sValue) : self
{
$sHeaderName = \trim($sHeaderName);
if (\strlen($sHeaderName))
{
if (\strlen($sHeaderName)) {
$this->aHeadersValue[$sHeaderName] = $sValue;
}
@ -194,16 +179,19 @@ class Message extends Part
public function SetInReplyTo(string $sInReplyTo) : self
{
$this->aHeadersValue[Enumerations\Header::IN_REPLY_TO] = $sInReplyTo;
$sInReplyTo = \trim($sInReplyTo);
if (\strlen($sInReplyTo)) {
$this->aHeadersValue[Enumerations\Header::IN_REPLY_TO] = $sInReplyTo;
}
return $this;
}
public function SetReferences(string $sReferences) : self
{
$this->aHeadersValue[Enumerations\Header::REFERENCES] =
\MailSo\Base\Utils::StripSpaces($sReferences);
$sReferences = \MailSo\Base\Utils::StripSpaces($sReferences);
if (\strlen($sReferences)) {
$this->aHeadersValue[Enumerations\Header::REFERENCES] = $sReferences;
}
return $this;
}
@ -215,11 +203,6 @@ class Message extends Part
return $this;
}
public function SetReadConfirmation(string $sEmail) : self
{
return $this->SetReadReceipt($sEmail);
}
public function SetPriority(int $iValue) : self
{
$sResult = '';
@ -236,8 +219,7 @@ class Message extends Part
break;
}
if (\strlen($sResult))
{
if (\strlen($sResult)) {
$this->aHeadersValue[Enumerations\Header::X_PRIORITY] = $sResult;
}
@ -260,8 +242,9 @@ class Message extends Part
public function SetTo(EmailCollection $oEmails) : self
{
$this->aHeadersValue[Enumerations\Header::TO_] = $oEmails;
if ($oEmails->count()) {
$this->aHeadersValue[Enumerations\Header::TO_] = $oEmails;
}
return $this;
}
@ -274,22 +257,25 @@ class Message extends Part
public function SetReplyTo(EmailCollection $oEmails) : self
{
$this->aHeadersValue[Enumerations\Header::REPLY_TO] = $oEmails;
if ($oEmails->count()) {
$this->aHeadersValue[Enumerations\Header::REPLY_TO] = $oEmails;
}
return $this;
}
public function SetCc(EmailCollection $oEmails) : self
{
$this->aHeadersValue[Enumerations\Header::CC] = $oEmails;
if ($oEmails->count()) {
$this->aHeadersValue[Enumerations\Header::CC] = $oEmails;
}
return $this;
}
public function SetBcc(EmailCollection $oEmails) : self
{
$this->aHeadersValue[Enumerations\Header::BCC] = $oEmails;
if ($oEmails->count()) {
$this->aHeadersValue[Enumerations\Header::BCC] = $oEmails;
}
return $this;
}
@ -313,18 +299,15 @@ class Message extends Part
private function generateNewMessageId(string $sHostName = '') : string
{
if (0 === \strlen($sHostName))
{
if (!\strlen($sHostName)) {
$sHostName = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : '';
}
if (empty($sHostName) && \MailSo\Base\Utils::FunctionCallable('php_uname'))
{
if (empty($sHostName) && \MailSo\Base\Utils::FunctionCallable('php_uname')) {
$sHostName = \php_uname('n');
}
if (empty($sHostName))
{
if (empty($sHostName)) {
$sHostName = 'localhost';
}
@ -361,7 +344,7 @@ class Message extends Part
}
$oPart->Headers->append(
new Header(Enumerations\Header::CONTENT_TYPE,
$aAttachments[0]->ContentType().'; '.$oParameters->ToString())
$aAttachments[0]->ContentType().'; '.$oParameters)
);
if ($resource = $aAttachments[0]->Resource()) {
@ -384,13 +367,10 @@ class Message extends Part
if (1 == \count($this->SubParts)) {
$oRootPart = $this->SubParts[0];
foreach ($this->oAttachmentCollection as $oAttachment) {
if ($oAttachment->IsLinked()) {
if ($oAttachment->isLinked()) {
$oRelatedPart = new Part;
$oRelatedPart->Headers->append(
new Header(
Enumerations\Header::CONTENT_TYPE,
Enumerations\MimeType::MULTIPART_RELATED
)
new Header(Enumerations\Header::CONTENT_TYPE, 'multipart/related')
);
$oRelatedPart->SubParts->append($oRootPart);
$oRootPart = $oRelatedPart;
@ -399,24 +379,18 @@ class Message extends Part
}
} else {
$oRootPart = new Part;
$oRootPart->Headers->AddByName(
Enumerations\Header::CONTENT_TYPE,
Enumerations\MimeType::MULTIPART_MIXED
);
$oRootPart->Headers->AddByName(Enumerations\Header::CONTENT_TYPE, 'multipart/mixed');
$oRootPart->SubParts = $this->SubParts;
}
$oMixedPart = null;
foreach ($this->oAttachmentCollection as $oAttachment) {
if ($oRelatedPart && $oAttachment->IsLinked()) {
if ($oRelatedPart && $oAttachment->isLinked()) {
$oRelatedPart->SubParts->append($oAttachment->ToPart());
} else {
if (!$oMixedPart) {
$oMixedPart = new Part;
$oMixedPart->Headers->AddByName(
Enumerations\Header::CONTENT_TYPE,
Enumerations\MimeType::MULTIPART_MIXED
);
$oMixedPart->Headers->AddByName(Enumerations\Header::CONTENT_TYPE, 'multipart/mixed');
$oMixedPart->SubParts->append($oRootPart);
$oRootPart = $oMixedPart;
}
@ -441,7 +415,7 @@ class Message extends Part
$oRootPart->Headers->SetByName(Enumerations\Header::DATE, \gmdate('r'), true);
}
if ($this->messageIdRequired && !isset($this->aHeadersValue[Enumerations\Header::MESSAGE_ID])) {
if (!isset($this->aHeadersValue[Enumerations\Header::MESSAGE_ID])) {
$oRootPart->Headers->SetByName(Enumerations\Header::MESSAGE_ID, $this->generateNewMessageId(), true);
}
@ -455,11 +429,6 @@ class Message extends Part
foreach ($this->aHeadersValue as $sName => $mValue) {
if (!($bWithoutBcc && \strtolower(Enumerations\Header::BCC) === \strtolower($sName))) {
if (\is_object($mValue)) {
if ($mValue instanceof EmailCollection || $mValue instanceof Email || $mValue instanceof ParameterCollection) {
$mValue = $mValue->ToString();
}
}
$oRootPart->Headers->SetByName($sName, (string) $mValue);
}
}

View file

@ -15,17 +15,11 @@ namespace MailSo\Mime;
* @category MailSo
* @package Mime
*/
class Parameter
class Parameter implements \JsonSerializable
{
/**
* @var string
*/
private $sName;
private string $sName;
/**
* @var string
*/
private $sValue;
private string $sValue;
function __construct(string $sName, string $sValue)
{
@ -66,12 +60,11 @@ class Parameter
{
$this->Reset();
$aParts = explode($sSeparator, $sRawParam, 2);
$aParts = \explode($sSeparator, $sRawParam, 2);
$this->sName = trim(trim($aParts[0]), '"\'');
if (2 === count($aParts))
{
$this->sValue = trim(trim($aParts[1]), '"\'');
$this->sName = \trim(\trim($aParts[0]), '"\'');
if (2 === \count($aParts)) {
$this->sValue = \trim(\trim($aParts[1]), '"\'');
}
return $this;
@ -79,25 +72,34 @@ class Parameter
public function ToString(bool $bConvertSpecialsName = false) : string
{
$sResult = '';
if (\strlen($this->sName))
{
$sResult = $this->sName.'=';
if ($bConvertSpecialsName && in_array(strtolower($this->sName), array(
strtolower(Enumerations\Parameter::NAME),
strtolower(Enumerations\Parameter::FILENAME)
)))
{
$sResult .= '"'.\MailSo\Base\Utils::EncodeUnencodedValue(
\MailSo\Base\Enumerations\Encoding::BASE64_SHORT,
$this->sValue).'"';
}
else
{
$sResult .= '"'.$this->sValue.'"';
}
if (!\strlen($this->sName)) {
return '';
}
return $sResult;
if ($bConvertSpecialsName && \in_array(\strtolower($this->sName), array(
\strtolower(Enumerations\Parameter::NAME),
\strtolower(Enumerations\Parameter::FILENAME)
)))
{
return $this->sName . '="' . \MailSo\Base\Utils::EncodeUnencodedValue(
\MailSo\Base\Enumerations\Encoding::BASE64_SHORT,
$this->sValue) . '"';
}
return $this->sName . '="' . $this->sValue . '"';
}
public function __toString() : string
{
return $this->ToString();
}
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return array(
'name' => $this->sName,
'value' => $this->sValue
);
}
}

View file

@ -21,10 +21,7 @@ class ParameterCollection extends \MailSo\Base\Collection
{
parent::__construct();
if (\strlen($sRawParams))
{
$this->Parse($sRawParams);
}
\strlen($sRawParams) && $this->Parse($sRawParams);
}
public function append($oParameter, bool $bToTop = false) : void
@ -66,8 +63,7 @@ class ParameterCollection extends \MailSo\Base\Collection
$aDataToParse = \explode(';', $sRawParams);
foreach ($aDataToParse as $sParam)
{
foreach ($aDataToParse as $sParam) {
$this->append(Parameter::CreateFromParameterLine($sParam));
}
@ -79,18 +75,35 @@ class ParameterCollection extends \MailSo\Base\Collection
public function ToString(bool $bConvertSpecialsName = false) : string
{
$aResult = array();
foreach ($this as $oParam)
{
foreach ($this as $oParam) {
$sLine = $oParam->ToString($bConvertSpecialsName);
if (\strlen($sLine))
{
if (\strlen($sLine)) {
$aResult[] = $sLine;
}
}
return \count($aResult) ? \implode('; ', $aResult) : '';
}
public function __toString() : string
{
return $this->ToString();
}
/*
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
$aResult = array();
foreach ($this as $oParam) {
$aResult[$oParam->Name()] = $oParam->Value();
}
return array(
'@Object' => 'Collection/ParameterCollection',
'@Collection' => $aResult
);
}
*/
private function reParseParameters() : void
{
$aDataToReParse = $this->getArrayCopy();
@ -99,26 +112,22 @@ class ParameterCollection extends \MailSo\Base\Collection
$this->Clear();
$aPreParams = array();
foreach ($aDataToReParse as $oParam)
{
foreach ($aDataToReParse as $oParam) {
$aMatch = array();
$sParamName = $oParam->Name();
if (\preg_match('/([^\*]+)\*([\d]{1,2})\*/', $sParamName, $aMatch) && isset($aMatch[1], $aMatch[2])
&& \strlen($aMatch[1]) && \is_numeric($aMatch[2]))
{
if (!isset($aPreParams[$aMatch[1]]))
{
if (!isset($aPreParams[$aMatch[1]])) {
$aPreParams[$aMatch[1]] = array();
}
$sValue = $oParam->Value();
if (false !== \strpos($sValue, "''"))
{
if (false !== \strpos($sValue, "''")) {
$aValueParts = \explode("''", $sValue, 2);
if (\is_array($aValueParts) && 2 === \count($aValueParts) && \strlen($aValueParts[1]))
{
if (\is_array($aValueParts) && 2 === \count($aValueParts) && \strlen($aValueParts[1])) {
$sCharset = $aValueParts[0];
$sValue = $aValueParts[1];
}
@ -128,17 +137,14 @@ class ParameterCollection extends \MailSo\Base\Collection
}
else if (\preg_match('/([^\*]+)\*/', $sParamName, $aMatch) && isset($aMatch[1]))
{
if (!isset($aPreParams[$aMatch[1]]))
{
if (!isset($aPreParams[$aMatch[1]])) {
$aPreParams[$aMatch[1]] = array();
}
$sValue = $oParam->Value();
if (false !== \strpos($sValue, "''"))
{
if (false !== \strpos($sValue, "''")) {
$aValueParts = \explode("''", $sValue, 2);
if (\is_array($aValueParts) && 2 === \count($aValueParts) && \strlen($aValueParts[1]))
{
if (\is_array($aValueParts) && 2 === \count($aValueParts) && \strlen($aValueParts[1])) {
$sCharset = $aValueParts[0];
$sValue = $aValueParts[1];
}
@ -152,14 +158,12 @@ class ParameterCollection extends \MailSo\Base\Collection
}
}
foreach ($aPreParams as $sName => $aValues)
{
ksort($aValues);
foreach ($aPreParams as $sName => $aValues) {
\ksort($aValues);
$sResult = \implode(\array_values($aValues));
$sResult = \urldecode($sResult);
if (\strlen($sCharset))
{
if (\strlen($sCharset)) {
$sResult = \MailSo\Base\Utils::ConvertEncoding($sResult,
$sCharset, \MailSo\Base\Enumerations\Charset::UTF_8);
}

View file

@ -23,7 +23,7 @@ abstract class Parser
POS_SUBPARTS = 3,
POS_CLOSE_BOUNDARY = 4;
private static $LineParts = [];
private static array $LineParts = [];
protected static function writeBody(Part $oPart, string $sBuffer) : void
{
@ -59,21 +59,18 @@ abstract class Parser
$oMimePart = null;
$sFirstNotNullCharset = null;
foreach (static::$LineParts as /* @var $oMimePart Part */ $oMimePart)
{
foreach (static::$LineParts as /* @var $oMimePart Part */ $oMimePart) {
$sCharset = $oMimePart->HeaderCharset();
if (\strlen($sCharset))
{
if (\strlen($sCharset)) {
$sFirstNotNullCharset = $sCharset;
break;
}
}
$sFirstNotNullCharset = (null !== $sFirstNotNullCharset)
? $sFirstNotNullCharset : Part::$DefaultCharset;
? $sFirstNotNullCharset : \MailSo\Base\Enumerations\Charset::ISO_8859_1;
foreach (static::$LineParts as /* @var $oMimePart Part */ $oMimePart)
{
foreach (static::$LineParts as /* @var $oMimePart Part */ $oMimePart) {
$sHeaderCharset = $oMimePart->HeaderCharset();
$oMimePart->Headers->SetParentCharset($sHeaderCharset);
}
@ -93,53 +90,38 @@ abstract class Parser
$sCurrentBoundary = '';
$bIsBoundaryCheck = false;
$aHeadersLines = array();
while (true)
{
if (!$bNotFirstRead)
{
while (true) {
if (!$bNotFirstRead) {
$sPrevBuffer = $sBuffer;
$sBuffer = '';
}
if (!$bIsOef && !\feof($rStreamHandle))
{
if (!$bNotFirstRead)
{
if (!$bIsOef && !\feof($rStreamHandle)) {
if (!$bNotFirstRead) {
$sBuffer = \fread($rStreamHandle, 8192);
if (false === $sBuffer)
{
if (false === $sBuffer) {
break;
}
}
else
{
} else {
$bNotFirstRead = false;
}
}
else if ($bIsOef && !\strlen($sBuffer))
{
} else if ($bIsOef && !\strlen($sBuffer)) {
break;
}
else
{
} else {
$bIsOef = true;
}
while (true)
{
while (true) {
$sCurrentLine = $sPrevBuffer.$sBuffer;
if (self::POS_HEADERS === $iParsePosition)
{
if (self::POS_HEADERS === $iParsePosition) {
$iEndLen = 4;
$iPos = \strpos($sCurrentLine, "\r\n\r\n", $iOffset);
if (false === $iPos)
{
if (false === $iPos) {
$iEndLen = 2;
$iPos = \strpos($sCurrentLine, "\n\n", $iOffset);
}
if (false !== $iPos)
{
if (false !== $iPos) {
$aHeadersLines[] = \substr($sCurrentLine, $iOffset, $iPos + $iEndLen - $iOffset);
$oPart->Headers->Parse(\implode($aHeadersLines))->SetParentCharset($oPart->HeaderCharset());
@ -156,42 +138,31 @@ abstract class Parser
$iOffset = $iPos + $iEndLen;
$iParsePosition = self::POS_BODY;
continue;
}
else
{
} else {
$iBufferLen = \strlen($sPrevBuffer);
if ($iBufferLen > $iOffset)
{
if ($iBufferLen > $iOffset) {
$aHeadersLines[] = \substr($sPrevBuffer, $iOffset);
$iOffset = 0;
}
else
{
} else {
$iOffset -= $iBufferLen;
}
break;
}
}
else if (self::POS_BODY === $iParsePosition)
{
} else if (self::POS_BODY === $iParsePosition) {
$iPos = false;
$sBoundaryLen = 0;
$bIsBoundaryEnd = false;
$bCurrentPartBody = false;
$bIsBoundaryCheck = \count($aBoundaryStack);
foreach ($aBoundaryStack as $sKey => $sBoundary)
{
if (false !== ($iPos = \strpos($sCurrentLine, $sBoundary, $iOffset)))
{
if ($sCurrentBoundary === $sBoundary)
{
foreach ($aBoundaryStack as $sKey => $sBoundary) {
if (false !== ($iPos = \strpos($sCurrentLine, $sBoundary, $iOffset))) {
if ($sCurrentBoundary === $sBoundary) {
$bCurrentPartBody = true;
}
$sBoundaryLen = \strlen($sBoundary);
if ('--' === \substr($sCurrentLine, $iPos + $sBoundaryLen, 2))
{
if ('--' === \substr($sCurrentLine, $iPos + $sBoundaryLen, 2)) {
$sBoundaryLen += 2;
$bIsBoundaryEnd = true;
unset($aBoundaryStack[$sKey]);
@ -203,54 +174,42 @@ abstract class Parser
}
}
if (false !== $iPos)
{
if (false !== $iPos) {
static::writeBody($oPart, \substr($sCurrentLine, $iOffset, $iPos - $iOffset));
$iOffset = $iPos;
if ($bCurrentPartBody)
{
if ($bCurrentPartBody) {
$iParsePosition = self::POS_SUBPARTS;
continue;
}
return;
}
else
{
} else {
$iBufferLen = \strlen($sPrevBuffer);
if ($iBufferLen > $iOffset)
{
if ($iBufferLen > $iOffset) {
static::writeBody($oPart, \substr($sPrevBuffer, $iOffset));
$iOffset = 0;
}
else
{
} else {
$iOffset -= $iBufferLen;
}
break;
}
}
else if (self::POS_SUBPARTS === $iParsePosition)
{
} else if (self::POS_SUBPARTS === $iParsePosition) {
$iPos = false;
$iBoundaryLen = 0;
$bIsBoundaryEnd = false;
$bCurrentPartBody = false;
$bIsBoundaryCheck = \count($aBoundaryStack);
foreach ($aBoundaryStack as $sKey => $sBoundary)
{
if (false !== ($iPos = \strpos($sCurrentLine, $sBoundary, $iOffset)))
{
if ($sCurrentBoundary === $sBoundary)
{
foreach ($aBoundaryStack as $sKey => $sBoundary) {
$iPos = \strpos($sCurrentLine, $sBoundary, $iOffset);
if (false !== $iPos) {
if ($sCurrentBoundary === $sBoundary) {
$bCurrentPartBody = true;
}
$iBoundaryLen = \strlen($sBoundary);
if ('--' === \substr($sCurrentLine, $iPos + $iBoundaryLen, 2))
{
if ('--' === \substr($sCurrentLine, $iPos + $iBoundaryLen, 2)) {
$iBoundaryLen += 2;
$bIsBoundaryEnd = true;
unset($aBoundaryStack[$sKey]);
@ -261,8 +220,7 @@ abstract class Parser
}
}
if (false !== $iPos && $bCurrentPartBody)
{
if (false !== $iPos && $bCurrentPartBody) {
$iOffset = $iPos + $iBoundaryLen;
$oSubPart = new Part;
@ -274,39 +232,27 @@ abstract class Parser
static::$LineParts[] = $oSubPart;
//$iParsePosition = self::POS_HEADERS;
unset($oSubPart);
}
else
{
} else {
return;
}
}
}
}
if (\strlen($sPrevBuffer))
{
if (self::POS_HEADERS === $iParsePosition)
{
if (\strlen($sPrevBuffer)) {
if (self::POS_HEADERS === $iParsePosition) {
$aHeadersLines[] = ($iOffset < \strlen($sPrevBuffer))
? \substr($sPrevBuffer, $iOffset)
: $sPrevBuffer;
$oPart->Headers->Parse(\implode($aHeadersLines))->SetParentCharset($oPart->HeaderCharset());
$aHeadersLines = array();
} else if (!$bIsBoundaryCheck && self::POS_BODY === $iParsePosition) {
static::writeBody($oPart, ($iOffset < \strlen($sPrevBuffer))
? \substr($sPrevBuffer, $iOffset) : $sPrevBuffer);
}
else if (self::POS_BODY === $iParsePosition)
{
if (!$bIsBoundaryCheck)
{
static::writeBody($oPart, ($iOffset < \strlen($sPrevBuffer))
? \substr($sPrevBuffer, $iOffset) : $sPrevBuffer);
}
}
}
else
{
if (self::POS_HEADERS === $iParsePosition && \count($aHeadersLines))
{
} else {
if (self::POS_HEADERS === $iParsePosition && \count($aHeadersLines)) {
$oPart->Headers->Parse(\implode($aHeadersLines))->SetParentCharset($oPart->HeaderCharset());
$aHeadersLines = array();
}

View file

@ -17,15 +17,7 @@ namespace MailSo\Mime;
*/
class Part
{
/**
* @var string
*/
public static $DefaultCharset = \MailSo\Base\Enumerations\Charset::ISO_8859_1;
/**
* @var HeaderCollection
*/
public $Headers;
public HeaderCollection $Headers;
/**
* @var resource
@ -37,10 +29,7 @@ class Part
*/
public $Raw = null;
/**
* @var PartCollection
*/
public $SubParts;
public PartCollection $SubParts;
function __construct()
{
@ -63,29 +52,14 @@ class Part
return \trim(\strtolower($this->Headers->ValueByName(Enumerations\Header::CONTENT_TYPE)));
}
public function ContentID() : string
{
return \trim($this->Headers->ValueByName(Enumerations\Header::CONTENT_ID));
}
public function ContentLocation() : string
{
return \trim($this->Headers->ValueByName(Enumerations\Header::CONTENT_LOCATION));
}
public function IsFlowedFormat() : bool
{
$bResult = false;
if ($this->Headers)
{
$bResult = 'flowed' === \trim(\strtolower($this->Headers->ParameterValue(
Enumerations\Header::CONTENT_TYPE,
Enumerations\Parameter::FORMAT)));
$bResult = 'flowed' === \trim(\strtolower($this->Headers->ParameterValue(
Enumerations\Header::CONTENT_TYPE,
Enumerations\Parameter::FORMAT)));
if ($bResult && \in_array($this->MailEncodingName(), array('base64', 'quoted-printable')))
{
$bResult = false;
}
if ($bResult && \in_array($this->MailEncodingName(), array('base64', 'quoted-printable'))) {
$bResult = false;
}
return $bResult;
@ -93,19 +67,14 @@ class Part
public function FileName() : string
{
$sResult = '';
if ($this->Headers)
{
$sResult = \trim($this->Headers->ParameterValue(
Enumerations\Header::CONTENT_DISPOSITION,
Enumerations\Parameter::FILENAME));
$sResult = \trim($this->Headers->ParameterValue(
Enumerations\Header::CONTENT_DISPOSITION,
Enumerations\Parameter::FILENAME));
if (!\strlen($sResult))
{
$sResult = \trim($this->Headers->ParameterValue(
Enumerations\Header::CONTENT_TYPE,
Enumerations\Parameter::NAME));
}
if (!\strlen($sResult)) {
$sResult = \trim($this->Headers->ParameterValue(
Enumerations\Header::CONTENT_TYPE,
Enumerations\Parameter::NAME));
}
return $sResult;

View file

@ -17,7 +17,7 @@ namespace MailSo\Mime;
*/
class PartCollection extends \MailSo\Base\Collection
{
protected $sBoundary;
protected string $sBoundary = '';
public function append($oPart, bool $bToTop = false) : void
{

View file

@ -15,66 +15,84 @@ namespace MailSo\Net;
* @category MailSo
* @package Net
*/
class ConnectSettings
class ConnectSettings implements \JsonSerializable
{
public
$host,
public string $host;
$port,
public int $port;
// none, TLS, STARTTLS
$type = \MailSo\Net\Enumerations\ConnectionSecurityType::AUTO_DETECT,
/**
* stream timeout in seconds
*/
public int $timeout = 10;
// https://www.php.net/context.ssl
$ssl = [
// 'peer_name' => '',
// 'peer_fingerprint' => '', // string | array
'verify_peer' => true,
'verify_peer_name' => true,
'allow_self_signed' => false,
// 'cafile' => '',
// 'capath' => '',
// none, TLS, STARTTLS
public int $type = Enumerations\ConnectionSecurityType::AUTO_DETECT;
// public int $type = Enumerations\ConnectionSecurityType::NONE;
// 'ciphers' => 'HIGH:!SSLv2:!SSLv3',
'SNI_enabled' => true,
'disable_compression' => true,
'security_level' => 1,
public SSLContext $ssl;
// public bool $tls_weak = false;
// 'local_cert' => '',
// 'local_pk' => '',
// 'passphrase' => '',
// Authentication settings use by all child classes
public bool $useAuth = true;
public bool $shortLogin = false;
public array $SASLMechanisms = [
// https://github.com/the-djmaze/snappymail/issues/182
'SCRAM-SHA3-512',
'SCRAM-SHA-512',
'SCRAM-SHA-256',
'SCRAM-SHA-1',
// 'CRAM-MD5',
'PLAIN',
'LOGIN'
];
public string $Login = '';
public string $Password = '';
public string $ProxyAuthUser = '';
public string $ProxyAuthPassword = '';
// 'verify_depth' => 0,
// 'capture_peer_cert' => false,
// 'capture_peer_cert_chain' => false,
];
function __construct()
public function __construct()
{
// TODO: This should be moved to \RainLoop\Model\Domain
$oConfig = \RainLoop\API::Config();
$this->ssl['verify_peer'] = !!$oConfig->Get('ssl', 'verify_certificate', false);
$this->ssl['verify_peer_name'] = !!$oConfig->Get('ssl', 'verify_certificate', false);
$this->ssl['allow_self_signed'] = !!$oConfig->Get('ssl', 'allow_self_signed', true);
$this->ssl['security_level'] = (int) $oConfig->Get('ssl', 'security_level', 1);
// $this->ssl['local_cert'] = (string) $oConfig->Get('ssl', 'client_cert', '');
// $this->ssl['cafile'] = (string) $oConfig->Get('ssl', 'cafile', '');
// $this->ssl['capath'] = (string) $oConfig->Get('ssl', 'capath', '');
$this->ssl = new SSLContext;
}
public static function Host() : string
{
return \SnappyMail\IDN::toAscii($this->host);
}
public static function fromArray(array $aSettings) : self
{
$object = new self;
$object->host = $aSettings['Host'];
$object->port = $aSettings['Port'];
$object->type = $aSettings['Secure'];
$object->ssl['verify_peer'] = !!$aSettings['VerifySsl'];
$object->ssl['verify_peer_name'] = !!$aSettings['VerifySsl'];
$object->ssl['allow_self_signed'] = !!$aSettings['AllowSelfSigned'];
if (!empty($aSettings['ClientCert'])) {
$object->ssl['local_cert'] = $aSettings['ClientCert'];
$object = new static;
$object->host = $aSettings['host'];
$object->port = $aSettings['port'];
$object->type = isset($aSettings['type']) ? $aSettings['type'] : $aSettings['secure'];
if (isset($aSettings['timeout'])) {
$object->timeout = $aSettings['timeout'];
}
$object->shortLogin = !empty($aSettings['shortLogin']);
$object->ssl = SSLContext::fromArray($aSettings['ssl'] ?? []);
if (isset($aSettings['sasl'])) {
$object->SASLMechanisms = $aSettings['sasl'];
}
// $object->tls_weak = !empty($aSettings['tls_weak']);
return $object;
}
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return array(
// '@Object' => 'Object/ConnectSettings',
'host' => $this->host,
'port' => $this->port,
'type' => $this->type,
'timeout' => $this->timeout,
'shortLogin' => $this->shortLogin,
'sasl' => $this->SASLMechanisms,
'ssl' => $this->ssl
// 'tls_weak' => $this->tls_weak
);
}
}

View file

@ -18,15 +18,9 @@ namespace MailSo\Net\Exceptions;
*/
class SocketCanNotConnectToHostException extends ConnectionException
{
/**
* @var string
*/
private $sSocketMessage;
private string $sSocketMessage;
/**
* @var int
*/
private $iSocketCode;
private int $iSocketCode;
public function __construct(string $sSocketMessage = '', int $iSocketCode = 0, string $sMessage = '', int $iCode = 0, ?\Throwable $oPrevious = null)
{

View file

@ -17,65 +17,26 @@ namespace MailSo\Net;
*/
abstract class NetClient
{
use \MailSo\Log\Inherit;
/**
* @var resource
*/
private $rConnect = null;
/**
* @var bool
*/
private $bUnreadBuffer = false;
private bool $bUnreadBuffer = false;
/**
* @var bool
*/
protected $bRunningCallback = false;
protected bool $bRunningCallback = false;
/**
* @var string
*/
protected $sResponseBuffer = '';
private string $sConnectedHost = '';
/**
* @var int
*/
protected $iSecurityType = \MailSo\Net\Enumerations\ConnectionSecurityType::NONE;
private bool $ssl = false;
/**
* @var string
*/
private $sConnectedHost = '';
private int $iConnectTimeOut = 10;
/**
* @var int
*/
private $iConnectedPort = 0;
private float $iStartConnectTime = 0;
/**
* @var bool
*/
private $bSecure = false;
/**
* @var int
*/
private $iConnectTimeOut = 10;
/**
* @var int
*/
private $iSocketTimeOut = 10;
/**
* @var int
*/
private $iStartConnectTime = 0;
/**
* @var \MailSo\Log\Logger
*/
protected $oLogger = null;
public ConnectSettings $Settings;
public function __destruct()
{
@ -91,15 +52,9 @@ abstract class NetClient
return $this->sConnectedHost;
}
public function GetConnectedPort() : int
public function SetTimeOuts(int $iConnectTimeOut = 10) : void
{
return $this->iConnectedPort;
}
public function SetTimeOuts(int $iConnectTimeOut = 10, int $iSocketTimeOut = 10) : void
{
$this->iConnectTimeOut = max(5, $iConnectTimeOut);
$this->iSocketTimeOut = max(5, $iSocketTimeOut);
$this->iConnectTimeOut = \max(5, $iConnectTimeOut);
}
/**
@ -116,65 +71,56 @@ abstract class NetClient
}
/**
* @throws \InvalidArgumentException
* @throws \MailSo\RuntimeException
* @throws \MailSo\Base\Exceptions\InvalidArgumentException
* @throws \MailSo\Net\Exceptions\SocketAlreadyConnectedException
* @throws \MailSo\Net\Exceptions\SocketCanNotConnectToHostException
*/
public function Connect(ConnectSettings $oSettings) : void
{
$oSettings->host = \trim($oSettings->host);
if (!\strlen($oSettings->host) || !\MailSo\Base\Validator::PortInt($oSettings->port)) {
$this->writeLogException(
new \MailSo\Base\Exceptions\InvalidArgumentException,
\LOG_ERR, true);
if (!\strlen($oSettings->host) || 0 > $oSettings->port || 65535 < $oSettings->port) {
$this->writeLogException(new \InvalidArgumentException, \LOG_ERR);
}
if ($this->IsConnected())
{
$this->writeLogException(
new Exceptions\SocketAlreadyConnectedException,
\LOG_ERR, true);
if ($this->IsConnected()) {
$this->writeLogException(new Exceptions\SocketAlreadyConnectedException, \LOG_ERR, false);
// $this->Disconnect();
return;
}
$this->Settings = $oSettings;
$sErrorStr = '';
$iErrorNo = 0;
$this->sConnectedHost = $oSettings->host;
$this->iConnectedPort = $oSettings->port;
$this->iSecurityType = $oSettings->type;
$this->bSecure = \MailSo\Net\Enumerations\ConnectionSecurityType::UseSSL(
$this->iConnectedPort, $this->iSecurityType);
if (!\preg_match('/^[a-z0-9._]{2,8}:\/\//i', $this->sConnectedHost))
{
$this->sConnectedHost = ($this->bSecure ? 'ssl://' : 'tcp://').$this->sConnectedHost;
// $this->sConnectedHost = ($this->bSecure ? 'ssl://' : '').$this->sConnectedHost;
$this->ssl = \MailSo\Net\Enumerations\ConnectionSecurityType::UseSSL($this->Settings->port, $this->Settings->type);
if (!\preg_match('/^[a-z0-9._]{2,8}:\/\//i', $this->sConnectedHost)) {
$this->sConnectedHost = ($this->ssl ? 'ssl://' : 'tcp://') . $this->sConnectedHost;
// $this->sConnectedHost = ($this->ssl ? 'ssl://' : '') . $this->sConnectedHost;
}
if (!$this->bSecure && \MailSo\Net\Enumerations\ConnectionSecurityType::SSL === $this->iSecurityType)
{
if (!$this->ssl && \MailSo\Net\Enumerations\ConnectionSecurityType::SSL === $this->Settings->type) {
$this->writeLogException(
new \MailSo\Net\Exceptions\SocketUnsuppoterdSecureConnectionException('SSL isn\'t supported: ('.\implode(', ', \stream_get_transports()).')'),
\LOG_ERR, true);
\LOG_ERR);
}
$this->iStartConnectTime = \microtime(true);
$this->writeLog('Start connection to "'.$this->sConnectedHost.':'.$this->iConnectedPort.'"');
$this->writeLog('Start connection to "'.$this->sConnectedHost.':'.$this->Settings->port.'"');
$aStreamContextSettings = array(
'ssl' => $oSettings->ssl
);
\MailSo\Hooks::Run('Net.NetClient.StreamContextSettings/Filter', array(&$aStreamContextSettings));
$rStreamContext = \stream_context_create($aStreamContextSettings);
$rStreamContext = \stream_context_create(array(
'ssl' => $oSettings->ssl->jsonSerialize()
));
\set_error_handler(array($this, 'capturePhpErrorWithException'));
try
{
$this->rConnect = \stream_socket_client($this->sConnectedHost.':'.$this->iConnectedPort,
$this->rConnect = \stream_socket_client($this->sConnectedHost.':'.$this->Settings->port,
$iErrorNo, $sErrorStr, $this->iConnectTimeOut, STREAM_CLIENT_CONNECT, $rStreamContext);
}
catch (\Throwable $oExc)
@ -187,23 +133,19 @@ abstract class NetClient
$this->writeLog('Connect ('.($this->rConnect ? 'success' : 'failed').')');
if (!$this->rConnect)
{
if (!$this->rConnect) {
$this->writeLogException(
new Exceptions\SocketCanNotConnectToHostException(
\MailSo\Base\Locale::ConvertSystemString($sErrorStr), (int) $iErrorNo,
'Can\'t connect to host "'.$this->sConnectedHost.':'.$this->iConnectedPort.'"'
), \LOG_NOTICE, true);
'Can\'t connect to host "'.$this->sConnectedHost.':'.$this->Settings->port.'"'
)
);
}
$this->writeLog((\microtime(true) - $this->iStartConnectTime).' (raw connection)', \LOG_DEBUG);
if ($this->rConnect)
{
if (\MailSo\Base\Utils::FunctionCallable('stream_set_timeout'))
{
\stream_set_timeout($this->rConnect, $this->iSocketTimeOut);
}
if ($this->rConnect && \MailSo\Base\Utils::FunctionCallable('stream_set_timeout')) {
\stream_set_timeout($this->rConnect, \max(5, $oSettings->timeout));
}
}
@ -212,27 +154,21 @@ abstract class NetClient
return $this->rConnect && !empty(\stream_get_meta_data($this->rConnect)['crypto']);
}
public function EnableCrypto(bool $insecure = false)
public function EnableCrypto() : void
{
$bError = true;
$bSuccess = false;
if ($this->rConnect && \MailSo\Base\Utils::FunctionCallable('stream_socket_enable_crypto')) {
$crypto_method = STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
if (\defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && OPENSSL_VERSION_NUMBER >= 0x10101000) {
$crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT;
}
if ($insecure) {
$crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
}
if (\stream_socket_enable_crypto($this->rConnect, true, $crypto_method)) {
$bError = false;
}
// if ($this->Settings->tls_weak) {
// $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
// }
$bSuccess = \stream_socket_enable_crypto($this->rConnect, true, $crypto_method);
}
if ($bError) {
$this->writeLogException(
new \MailSo\RuntimeException('Cannot enable STARTTLS.'),
\LOG_ERR, true);
}
$bSuccess || $this->writeLogException(new \MailSo\RuntimeException('Cannot enable STARTTLS.'), \LOG_ERR);
}
/**
@ -241,29 +177,26 @@ abstract class NetClient
*/
public function Disconnect() : void
{
if ($this->rConnect)
{
if (!$this->bUnreadBuffer && !$this->bRunningCallback)
{
if ($this->rConnect) {
if (!$this->bUnreadBuffer && !$this->bRunningCallback) {
$this->Logout();
}
$bResult = \fclose($this->rConnect);
$this->writeLog('Disconnected from "'.$this->sConnectedHost.':'.$this->iConnectedPort.'" ('.
$this->writeLog('Disconnected from "'.$this->sConnectedHost.':'.$this->Settings->port.'" ('.
(($bResult) ? 'success' : 'unsuccess').')');
if ($this->iStartConnectTime)
{
if ($this->iStartConnectTime) {
$this->writeLog((\microtime(true) - $this->iStartConnectTime).' (net session)', \LOG_DEBUG);
$this->iStartConnectTime = 0;
}
$this->rConnect = null;
}
}
abstract public function supportsAuthType(string $sasl_type) : bool;
// abstract public function Login(ConnectSettings $oSettings) : self;
abstract public function Logout() : void;
public function IsConnected(bool $bThrowExceptionOnFalse = false) : bool
@ -272,21 +205,11 @@ abstract class NetClient
return true;
}
if ($bThrowExceptionOnFalse) {
$this->writeLogException(
new Exceptions\SocketConnectionDoesNotAvailableException,
\LOG_ERR, true);
$this->writeLogException(new Exceptions\SocketConnectionDoesNotAvailableException, \LOG_ERR);
}
return false;
}
/**
* @throws \MailSo\Net\Exceptions\SocketConnectionDoesNotAvailableException
*/
public function IsConnectedWithException() : void
{
$this->IsConnected(true);
}
public function StreamContextParams() : array
{
return $this->rConnect && \MailSo\Base\Utils::FunctionCallable('stream_context_get_options')
@ -300,7 +223,7 @@ abstract class NetClient
protected function sendRaw(string $sRaw, bool $bWriteToLog = true, string $sFakeRaw = '') : void
{
if ($this->bUnreadBuffer) {
$this->writeLogException(new Exceptions\SocketUnreadBufferException, \LOG_ERR, true);
$this->writeLogException(new Exceptions\SocketUnreadBufferException, \LOG_ERR);
}
$sRaw .= "\r\n";
@ -311,13 +234,10 @@ abstract class NetClient
}
$mResult = \fwrite($this->rConnect, $sRaw);
if (false === $mResult)
{
if (false === $mResult) {
$this->IsConnected(true);
$this->writeLogException(new Exceptions\SocketWriteException, \LOG_ERR, true);
}
else if ($bWriteToLog)
{
$this->writeLogException(new Exceptions\SocketWriteException, \LOG_ERR);
} else if ($bWriteToLog) {
$this->writeLogWithCrlf('> '.($bFake ? $sFakeRaw : $sRaw));
}
}
@ -326,112 +246,69 @@ abstract class NetClient
* @throws \MailSo\Net\Exceptions\SocketConnectionDoesNotAvailableException
* @throws \MailSo\Net\Exceptions\SocketReadException
*/
protected function getNextBuffer(?int $iReadLen = null, bool $bForceLogin = false) : void
protected function getNextBuffer(?int $iReadLen = null) : ?string
{
if (null === $iReadLen)
{
$this->sResponseBuffer = \fgets($this->rConnect);
}
else
{
$this->sResponseBuffer = '';
if (null === $iReadLen) {
$sResponseBuffer = \fgets($this->rConnect);
} else {
$sResponseBuffer = '';
$iRead = $iReadLen;
while (0 < $iRead)
{
while (0 < $iRead) {
$sAddRead = \fread($this->rConnect, $iRead);
if (false === $sAddRead)
{
$this->sResponseBuffer = false;
if (false === $sAddRead) {
$sResponseBuffer = false;
break;
}
$this->sResponseBuffer .= $sAddRead;
$sResponseBuffer .= $sAddRead;
$iRead -= \strlen($sAddRead);
}
}
if (false === $this->sResponseBuffer)
{
if (false === $sResponseBuffer) {
$this->IsConnected(true);
$this->bUnreadBuffer = true;
$aSocketStatus = \stream_get_meta_data($this->rConnect);
if (isset($aSocketStatus['timed_out']) && $aSocketStatus['timed_out'])
{
$this->writeLogException(
new Exceptions\SocketReadTimeoutException,
\LOG_ERR, true);
}
else
{
if (isset($aSocketStatus['timed_out']) && $aSocketStatus['timed_out']) {
$this->writeLogException(new Exceptions\SocketReadTimeoutException, \LOG_ERR);
} else {
$this->writeLog('Stream Meta: '.\print_r($aSocketStatus, true), \LOG_ERR);
$this->writeLogException(new Exceptions\SocketReadException, \LOG_ERR);
}
return null;
}
$this->writeLogException(
new Exceptions\SocketReadException,
\LOG_ERR, true);
}
}
else
{
$iReadedLen = \strlen($this->sResponseBuffer);
if (null === $iReadLen || $bForceLogin)
{
$iLimit = 5000; // 5KB
if ($iLimit < $iReadedLen)
{
$this->writeLogWithCrlf('[cutted:'.$iReadedLen.'] < '.\substr($this->sResponseBuffer, 0, $iLimit).'...');
}
else
{
// $this->writeLogWithCrlf('< '.$this->sResponseBuffer, //.' ['.$iReadedLen.']',
$this->writeLogWithCrlf('< '.$this->sResponseBuffer);
}
}
else
{
$this->writeLog('Received '.$iReadedLen.'/'.$iReadLen.' bytes.');
}
$iReadBytes = \strlen($sResponseBuffer);
// $iReadLen && $this->writeLog('Received '.$iReadBytes.'/'.$iReadLen.' bytes.');
$iLimit = 5000; // 5KB
if ($iLimit < $iReadBytes) {
$this->writeLogWithCrlf('[cutted:'.$iReadBytes.'] < '.\substr($sResponseBuffer, 0, $iLimit).'...');
} else {
$this->writeLogWithCrlf('< '.$sResponseBuffer);
}
return $sResponseBuffer;
}
abstract function getLogName() : string;
protected function writeLog(string $sDesc, int $iDescType = \LOG_INFO) : void
{
$this->oLogger && $this->oLogger->Write($sDesc, $iDescType, $this->getLogName());
$this->logWrite($sDesc, $iDescType, $this->getLogName());
}
protected function writeLogWithCrlf(string $sDesc) : void
{
$this->oLogger && $this->oLogger->Write($sDesc, \LOG_INFO, $this->getLogName(), true, true);
$this->logWrite($sDesc, \LOG_INFO, $this->getLogName(), true, true);
}
protected function writeLogException(\Throwable $oException,
int $iDescType = \LOG_NOTICE, bool $bThrowException = false) : void
protected function writeLogException(\Throwable $oException, int $iDescType = \LOG_NOTICE, bool $bThrowException = true) : void
{
if ($this->oLogger)
{
if ($oException instanceof Exceptions\SocketCanNotConnectToHostException)
{
$this->oLogger->Write('Socket: ['.$oException->getSocketCode().'] '.$oException->getSocketMessage(), $iDescType, $this->getLogName());
}
$this->oLogger->WriteException($oException, $iDescType, $this->getLogName());
if ($oException instanceof Exceptions\SocketCanNotConnectToHostException) {
$this->logWrite('Socket: ['.$oException->getSocketCode().'] '.$oException->getSocketMessage(), $iDescType, $this->getLogName());
}
if ($bThrowException)
{
$this->logException($oException, $iDescType, $this->getLogName());
if ($bThrowException) {
throw $oException;
}
}
public function SetLogger(\MailSo\Log\Logger $oLogger) : void
{
$this->oLogger = $oLogger;
}
public function Logger() : ?\MailSo\Log\Logger
{
return $this->oLogger;
}
}

View file

@ -0,0 +1,73 @@
<?php
/*
* This file is part of MailSo.
*
* (c) 2022 DJMaze
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* https://www.php.net/context.ssl
*/
namespace MailSo\Net;
/**
* @category MailSo
* @package Net
*/
class SSLContext implements \JsonSerializable
{
// public string $peer_name = '';
// public string $peer_fingerprint = '';
public bool $verify_peer = true;
public bool $verify_peer_name = true;
public bool $allow_self_signed = false;
public string $cafile = '';
public string $capath = '';
// public string $ciphers = 'HIGH:!SSLv2:!SSLv3';
public bool $SNI_enabled = true;
public bool $disable_compression = true;
public int $security_level = 1;
public string $local_cert = '';
// public string $local_pk = '';
// public string $passphrase = '';
// public int $verify_depth = 0;
// public bool $capture_peer_cert = false;
// public bool $capture_peer_cert_chain = false;
public function __construct()
{
$oConfig = \RainLoop\API::Config();
$this->verify_peer = !!$oConfig->Get('ssl', 'verify_certificate', true);
$this->verify_peer_name = !!$oConfig->Get('ssl', 'verify_certificate', true);
$this->allow_self_signed = !!$oConfig->Get('ssl', 'allow_self_signed', false);
$this->cafile = \trim($oConfig->Get('ssl', 'cafile', ''));
$this->capath = \trim($oConfig->Get('ssl', 'capath', ''));
$this->disable_compression = !!$oConfig->Get('ssl', 'disable_compression', true);
$this->security_level = (int) $oConfig->Get('ssl', 'security_level', 1);
$this->local_cert = \trim($oConfig->Get('ssl', 'local_cert', ''));
}
public static function fromArray(array $settings) : self
{
$object = new static;
foreach ($settings as $key => $value) {
$object->$key = $value;
}
return $object;
}
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
$aResult = \get_object_vars($this);
// $aResult['@Object'] = 'Object/SSLContext';
return \array_filter(
$aResult,
function($var){return !\is_string($var) || \strlen($var);}
);
}
}

View file

@ -18,10 +18,7 @@ namespace MailSo\Sieve\Exceptions;
*/
class ResponseException extends \MailSo\RuntimeException
{
/**
* @var array
*/
private $aResponses;
private array $aResponses;
public function __construct(array $aResponses = array(), string $sMessage = '', int $iCode = 0, ?\Throwable $oPrevious = null)
{
@ -37,7 +34,7 @@ class ResponseException extends \MailSo\RuntimeException
public function GetLastResponse() : ?\MailSo\Sieve\Response
{
$iCnt = count($this->aResponses);
$iCnt = \count($this->aResponses);
return $iCnt ? $this->aResponses[$iCnt - 1] : null;
}
}

View file

@ -0,0 +1,54 @@
<?php
/*
* This file is part of MailSo.
*
* (c) 2022 DJMaze
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace MailSo\Sieve;
/**
* @category MailSo
* @package Net
*/
class Settings extends \MailSo\Net\ConnectSettings
{
public int $port = 4190;
public bool $enabled = false;
public bool $authPlainLiteral = false;
public function __construct()
{
parent::__construct();
$oConfig = \RainLoop\API::Config();
$this->authPlainLiteral = !$oConfig->Get('labs', 'sieve_auth_plain_initial', true);
}
public static function fromArray(array $aSettings) : self
{
$object = parent::fromArray($aSettings);
$object->enabled = !empty($aSettings['enabled']);
// $object->authPlainLiteral = !empty($aSettings['authPlainLiteral']);
return $object;
}
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return \array_merge(
parent::jsonSerialize(),
[
// '@Object' => 'Object/SmtpSettings',
'enabled' => $this->enabled,
// 'authPlainLiteral' => $this->authPlainLiteral
]
);
}
}

View file

@ -17,48 +17,26 @@ use MailSo\Net\Enumerations\ConnectionSecurityType;
* @category MailSo
* @package Sieve
*/
class ManageSieveClient extends \MailSo\Net\NetClient
class SieveClient extends \MailSo\Net\NetClient
{
/**
* @var bool
*/
private $bIsLoggined = false;
private bool $bIsLoggined = false;
/**
* @var array
*/
private $aCapa = array();
private array $aCapa = array();
/**
* @var array
*/
private $aAuth = array();
private array $aAuth = array();
/**
* @var array
*/
private $aModules = array();
private array $aModules = array();
public function IsSupported(string $sCapa) : bool
public function hasCapability(string $sCapa) : bool
{
return isset($this->aCapa[\strtoupper($sCapa)]);
}
public function IsModuleSupported(string $sModule) : bool
{
return $this->IsSupported('SIEVE') && \in_array(\strtolower(\trim($sModule)), $this->aModules);
}
public function Modules() : array
{
return $this->aModules;
}
public function IsAuthSupported(string $sAuth) : bool
{
return $this->IsSupported('SASL') && \in_array(\strtoupper($sAuth), $this->aAuth);
}
/**
* @throws \InvalidArgumentException
* @throws \MailSo\RuntimeException
@ -70,80 +48,81 @@ class ManageSieveClient extends \MailSo\Net\NetClient
parent::Connect($oSettings);
$aResponse = $this->parseResponse();
$this->validateResponse($aResponse);
$this->parseStartupResponse($aResponse);
if (ConnectionSecurityType::STARTTLS === $this->iSecurityType
|| (ConnectionSecurityType::AUTO_DETECT === $this->iSecurityType && $this->IsSupported('STARTTLS'))) {
if (ConnectionSecurityType::STARTTLS === $this->Settings->type
|| (ConnectionSecurityType::AUTO_DETECT === $this->Settings->type && $this->hasCapability('STARTTLS'))) {
$this->StartTLS();
}
}
private function StartTLS() : void
{
if ($this->IsSupported('STARTTLS')) {
if ($this->hasCapability('STARTTLS')) {
$this->sendRequestWithCheck('STARTTLS');
$this->EnableCrypto();
$aResponse = $this->parseResponse();
$this->validateResponse($aResponse);
$this->parseStartupResponse($aResponse);
} else {
$this->writeLogException(
new \MailSo\Net\Exceptions\SocketUnsuppoterdSecureConnectionException('STARTTLS is not supported'),
\LOG_ERR, true);
\LOG_ERR);
}
}
public function supportsAuthType(string $sasl_type) : bool
{
return \in_array(\strtoupper($sasl_type), $this->aAuth);
}
/**
* @throws \InvalidArgumentException
* @throws \MailSo\RuntimeException
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Sieve\Exceptions\*
*/
public function Login(array $aCredentials) : self
public function Login(Settings $oSettings) : self
{
$sLogin = $aCredentials['Login'];
$sPassword = $aCredentials['Password'];
if ($this->bIsLoggined) {
return $this;
}
$sLogin = $oSettings->Login;
$sPassword = $oSettings->Password;
$sLoginAuthKey = '';
if (!\strlen($sLogin) || !\strlen($sPassword))
{
$this->writeLogException(
new \InvalidArgumentException,
\LOG_ERR, true);
if (!\strlen($sLogin) || !\strlen($sPassword)) {
$this->writeLogException(new \InvalidArgumentException, \LOG_ERR);
}
$type = '';
\array_push($aCredentials['SASLMechanisms'], 'PLAIN', 'LOGIN');
foreach ($aCredentials['SASLMechanisms'] as $sasl_type) {
if ($this->IsAuthSupported($sasl_type) && \SnappyMail\SASL::isSupported($sasl_type)) {
$type = $sasl_type;
break;
if ($this->hasCapability('SASL')) {
foreach ($oSettings->SASLMechanisms as $sasl_type) {
if (\in_array(\strtoupper($sasl_type), $this->aAuth) && \SnappyMail\SASL::isSupported($sasl_type)) {
$type = $sasl_type;
break;
}
}
}
if (!$type) {
if (!$this->Encrypted() && $this->IsSupported('STARTTLS')) {
if (!$this->Encrypted() && $this->hasCapability('STARTTLS')) {
$this->StartTLS();
return $this->Login($aCredentials);
return $this->Login($oSettings);
}
$this->writeLogException(
new \MailSo\Sieve\Exceptions\LoginException,
\LOG_ERR, true);
$this->writeLogException(new \MailSo\Sieve\Exceptions\LoginException, \LOG_ERR);
}
$SASL = \SnappyMail\SASL::factory($type);
$SASL->base64 = true;
$bAuth = false;
try
{
if (0 === \strpos($type, 'SCRAM-'))
if (\str_starts_with($type, 'SCRAM-'))
{
/*
$sAuthzid = $this->getResponseValue($this->SendRequestGetResponse('AUTHENTICATE', array($type)), \MailSo\Imap\Enumerations\ResponseType::CONTINUATION);
$this->sendRaw($SASL->authenticate($sLogin, $sPassword/*, $sAuthzid* /), true);
$sChallenge = $SASL->challenge($this->getResponseValue($this->getResponse(), \MailSo\Imap\Enumerations\ResponseType::CONTINUATION));
$this->oLogger && $this->oLogger->AddSecret($sChallenge);
$this->logMask($sChallenge);
$this->sendRaw($sChallenge);
$oResponse = $this->getResponse();
$SASL->verify($this->getResponseValue($oResponse));
@ -152,17 +131,16 @@ class ManageSieveClient extends \MailSo\Net\NetClient
else if ('PLAIN' === $type || 'OAUTHBEARER' === $type || 'XOAUTH2' === $type)
{
$sAuth = $SASL->authenticate($sLogin, $sPassword, $sLoginAuthKey);
$this->oLogger && $this->oLogger->AddSecret($sAuth);
$this->logMask($sAuth);
if ($aCredentials['InitialAuthPlain']) {
$this->sendRaw("AUTHENTICATE \"{$type}\" \"{$sAuth}\"");
} else {
if ($oSettings->authPlainLiteral) {
$this->sendRaw("AUTHENTICATE \"{$type}\" {".\strlen($sAuth).'+}');
$this->sendRaw($sAuth);
} else {
$this->sendRaw("AUTHENTICATE \"{$type}\" \"{$sAuth}\"");
}
$aResponse = $this->parseResponse();
$this->validateResponse($aResponse);
$this->parseStartupResponse($aResponse);
$bAuth = true;
}
@ -170,7 +148,7 @@ class ManageSieveClient extends \MailSo\Net\NetClient
{
$sLogin = $SASL->authenticate($sLogin, $sPassword);
$sPassword = $SASL->challenge('');
$this->oLogger && $this->oLogger->AddSecret($sPassword);
$this->logMask($sPassword);
$this->sendRaw('AUTHENTICATE "LOGIN"');
$this->sendRaw('{'.\strlen($sLogin).'+}');
@ -179,7 +157,6 @@ class ManageSieveClient extends \MailSo\Net\NetClient
$this->sendRaw($sPassword);
$aResponse = $this->parseResponse();
$this->validateResponse($aResponse);
$this->parseStartupResponse($aResponse);
$bAuth = true;
}
@ -187,17 +164,11 @@ class ManageSieveClient extends \MailSo\Net\NetClient
catch (\MailSo\Sieve\Exceptions\NegativeResponseException $oException)
{
$this->writeLogException(
new \MailSo\Sieve\Exceptions\LoginBadCredentialsException(
$oException->GetResponses(), '', 0, $oException),
\LOG_ERR, true);
new \MailSo\Sieve\Exceptions\LoginBadCredentialsException($oException->GetResponses(), '', 0, $oException),
\LOG_ERR);
}
if (!$bAuth)
{
$this->writeLogException(
new \MailSo\Sieve\Exceptions\LoginBadMethodException,
\LOG_ERR, true);
}
$bAuth || $this->writeLogException(new \MailSo\Sieve\Exceptions\LoginBadMethodException, \LOG_ERR);
$this->bIsLoggined = true;
@ -211,8 +182,7 @@ class ManageSieveClient extends \MailSo\Net\NetClient
*/
public function Logout() : void
{
if ($this->bIsLoggined)
{
if ($this->bIsLoggined) {
$this->sendRequestWithCheck('LOGOUT');
$this->bIsLoggined = false;
}
@ -227,18 +197,12 @@ class ManageSieveClient extends \MailSo\Net\NetClient
{
$this->sendRequest('LISTSCRIPTS');
$aResponse = $this->parseResponse();
$this->validateResponse($aResponse);
$aResult = array();
foreach ($aResponse as $sLine)
{
foreach ($aResponse as $sLine) {
$aTokens = $this->parseLine($sLine);
if (!$aTokens)
{
continue;
if ($aTokens) {
$aResult[$aTokens[0]] = 'ACTIVE' === \substr($sLine, -6);
}
$aResult[$aTokens[0]] = 'ACTIVE' === substr($sLine, -6);
}
return $aResult;
@ -253,7 +217,6 @@ class ManageSieveClient extends \MailSo\Net\NetClient
{
$this->sendRequest('CAPABILITY');
$aResponse = $this->parseResponse();
$this->validateResponse($aResponse);
$this->parseStartupResponse($aResponse);
return $this->aCapa;
@ -278,20 +241,17 @@ class ManageSieveClient extends \MailSo\Net\NetClient
*/
public function GetScript(string $sScriptName) : string
{
$sScriptName = \addcslashes($sScriptName, '"\\');
$this->sendRequest('GETSCRIPT "'.$sScriptName.'"');
$aResponse = $this->parseResponse();
$this->validateResponse($aResponse);
$sScript = '';
if (\count($aResponse))
{
if ('{' === $aResponse[0][0])
{
if (\count($aResponse)) {
if ('{' === $aResponse[0][0]) {
\array_shift($aResponse);
}
if (\in_array(\substr($aResponse[\count($aResponse) - 1], 0, 2), array('OK', 'NO')))
{
if (\in_array(\substr($aResponse[\count($aResponse) - 1], 0, 2), array('OK', 'NO'))) {
\array_pop($aResponse);
}
@ -308,6 +268,7 @@ class ManageSieveClient extends \MailSo\Net\NetClient
*/
public function PutScript(string $sScriptName, string $sScriptSource) : self
{
$sScriptName = \addcslashes($sScriptName, '"\\');
$sScriptSource = \preg_replace('/\r?\n/', "\r\n", $sScriptSource);
$this->sendRequest('PUTSCRIPT "'.$sScriptName.'" {'.\strlen($sScriptSource).'+}');
$this->sendRequestWithCheck($sScriptSource);
@ -336,6 +297,7 @@ class ManageSieveClient extends \MailSo\Net\NetClient
*/
public function SetActiveScript(string $sScriptName) : self
{
$sScriptName = \addcslashes($sScriptName, '"\\');
$this->sendRequestWithCheck('SETACTIVE "'.$sScriptName.'"');
return $this;
@ -348,6 +310,7 @@ class ManageSieveClient extends \MailSo\Net\NetClient
*/
public function DeleteScript(string $sScriptName) : self
{
$sScriptName = \addcslashes($sScriptName, '"\\');
$this->sendRequestWithCheck('DELETESCRIPT "'.$sScriptName.'"');
return $this;
@ -360,68 +323,22 @@ class ManageSieveClient extends \MailSo\Net\NetClient
*/
public function RenameScript(string $sOldName, string $sNewName) : self
{
$sOldName = \addcslashes($sOldName, '"\\');
$sNewName = \addcslashes($sNewName, '"\\');
$this->sendRequestWithCheck('RENAMESCRIPT "'.$sOldName.'" "'.$sNewName.'"');
return $this;
}
/**
* @throws \MailSo\RuntimeException
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Sieve\Exceptions\*
*/
public function GetActiveScriptName() : string
{
$aList = $this->ListScripts();
foreach ($aList as $sName => $bIsActive)
{
if ($bIsActive)
{
return $sName;
}
}
return '';
}
/**
* @throws \MailSo\RuntimeException
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Sieve\Exceptions\*
*/
public function IsActiveScript(string $sScriptName) : bool
{
return $sScriptName === $this->GetActiveScriptName();
}
private function parseLine(string $sLine) : ?array
{
if (false === $sLine || null === $sLine || \in_array(\substr($sLine, 0, 2), array('OK', 'NO')))
{
return null;
}
$iStart = -1;
$iIndex = 0;
$aResult = array();
for ($iPos = 0; $iPos < \strlen($sLine); $iPos++)
{
if ('"' === $sLine[$iPos] && '\\' !== $sLine[$iPos])
{
if (-1 === $iStart)
{
$iStart = $iPos;
}
else
{
$aResult[$iIndex++] = \substr($sLine, $iStart + 1, $iPos - $iStart - 1);
$iStart = -1;
}
if (!\in_array(\substr($sLine, 0, 2), array('OK', 'NO'))) {
$aResult = array();
if (\preg_match_all('/(?:(?:"((?:\\\\"|[^"])*)"))/', $sLine, $aResult)) {
return \array_map('stripcslashes', $aResult[1]);
}
}
return isset($aResult[0]) ? $aResult : null;
return null;
}
/**
@ -431,21 +348,16 @@ class ManageSieveClient extends \MailSo\Net\NetClient
*/
private function parseStartupResponse(array $aResponse) : void
{
foreach ($aResponse as $sLine)
{
foreach ($aResponse as $sLine) {
$aTokens = $this->parseLine($sLine);
if (false === $aTokens || !isset($aTokens[0]) ||
\in_array(\substr($sLine, 0, 2), array('OK', 'NO')))
{
if (empty($aTokens[0]) || \in_array(\substr($sLine, 0, 2), array('OK', 'NO'))) {
continue;
}
$sToken = \strtoupper($aTokens[0]);
$this->aCapa[$sToken] = isset($aTokens[1]) ? $aTokens[1] : '';
if (isset($aTokens[1]))
{
if (isset($aTokens[1])) {
switch ($sToken) {
case 'SASL':
$this->aAuth = \explode(' ', \strtoupper($aTokens[1]));
@ -465,11 +377,8 @@ class ManageSieveClient extends \MailSo\Net\NetClient
*/
private function sendRequest(string $sRequest) : void
{
if (!\strlen(\trim($sRequest)))
{
$this->writeLogException(
new \InvalidArgumentException,
\LOG_ERR, true);
if (!\strlen(\trim($sRequest))) {
$this->writeLogException(new \InvalidArgumentException, \LOG_ERR);
}
$this->IsConnected(true);
@ -485,76 +394,45 @@ class ManageSieveClient extends \MailSo\Net\NetClient
private function sendRequestWithCheck(string $sRequest) : void
{
$this->sendRequest($sRequest);
$this->validateResponse($this->parseResponse());
}
private function convertEndOfLine(string $sLine) : string
{
$sLine = \trim($sLine);
if ('}' === \substr($sLine, -1))
{
$iPos = \strrpos($sLine, '{');
if (false !== $iPos)
{
$sSunLine = \substr($sLine, $iPos + 1, -1);
if (\is_numeric($sSunLine) && 0 < (int) $sSunLine)
{
$iLen = (int) $sSunLine;
$this->getNextBuffer($iLen, true);
if (\strlen($this->sResponseBuffer) === $iLen)
{
$sLine = \trim(\substr_replace($sLine, $this->sResponseBuffer, $iPos));
}
}
}
}
return $sLine;
$this->parseResponse();
}
private function parseResponse() : array
{
$aResult = array();
do
{
$this->getNextBuffer();
$sLine = $this->sResponseBuffer;
if (false === $sLine)
{
while (true) {
$sResponseBuffer = $this->getNextBuffer();
if (null === $sResponseBuffer) {
break;
}
else if (\in_array(\substr($sLine, 0, 2), array('OK', 'NO')))
{
$aResult[] = $this->convertEndOfLine($sLine);
break;
$bEnd = \in_array(\substr($sResponseBuffer, 0, 2), array('OK', 'NO'));
// convertEndOfLine
$sLine = \trim($sResponseBuffer);
if ('}' === \substr($sLine, -1)) {
$iPos = \strrpos($sLine, '{');
if (false !== $iPos) {
$iLen = \intval(\substr($sLine, $iPos + 1, -1));
if (0 < $iLen) {
$sResponseBuffer = $this->getNextBuffer($iLen);
if (\strlen($sResponseBuffer) === $iLen) {
$sLine = \trim(\substr_replace($sLine, $sResponseBuffer, $iPos));
}
}
}
}
else
{
$aResult[] = $this->convertEndOfLine($sLine);
$aResult[] = $sLine;
if ($bEnd) {
break;
}
}
while (true);
if (!$aResult || 'OK' !== \substr($aResult[\array_key_last($aResult)], 0, 2)) {
$this->writeLogException(new \MailSo\Sieve\Exceptions\NegativeResponseException($aResult), \LOG_WARNING);
}
return $aResult;
}
/**
* @throws \MailSo\Sieve\Exceptions\NegativeResponseException
*/
private function validateResponse(array $aResponse)
{
if (!$aResponse ||
'OK' !== \substr($aResponse[\count($aResponse) - 1], 0, 2))
{
$this->writeLogException(
new \MailSo\Sieve\Exceptions\NegativeResponseException($aResponse),
\LOG_WARNING, true);
}
}
public function getLogName() : string
{
return 'SIEVE';

View file

@ -18,10 +18,7 @@ namespace MailSo\Smtp\Exceptions;
*/
class ResponseException extends \MailSo\RuntimeException
{
/**
* @var array
*/
private $aResponses;
private array $aResponses;
public function __construct(array $aResponses = array(), string $sMessage = '', int $iCode = 0, ?\Throwable $oPrevious = null)
{
@ -37,7 +34,7 @@ class ResponseException extends \MailSo\RuntimeException
public function GetLastResponse() : ?\MailSo\Smtp\Response
{
$iCnt = count($this->aResponses);
$iCnt = \count($this->aResponses);
return $iCnt ? $this->aResponses[$iCnt - 1] : null;
}
}

View file

@ -0,0 +1,67 @@
<?php
/*
* This file is part of MailSo.
*
* (c) 2022 DJMaze
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace MailSo\Smtp;
/**
* @category MailSo
* @package Net
*/
class Settings extends \MailSo\Net\ConnectSettings
{
public int
$port = 25,
$timeout = 60;
public bool
$setSender = false,
$usePhpMail = false,
// https://github.com/the-djmaze/snappymail/issues/1038
$authPlainLine = false,
$viewErrors = false;
public string $Ehlo;
public function __construct()
{
parent::__construct();
$oConfig = \RainLoop\API::Config();
$this->viewErrors = !!$oConfig->Get('labs', 'smtp_show_server_errors', false);
}
public static function fromArray(array $aSettings) : self
{
$object = parent::fromArray($aSettings);
$object->useAuth = !empty($aSettings['useAuth']);
$object->setSender = !empty($aSettings['setSender']);
$object->usePhpMail = !empty($aSettings['usePhpMail']);
$object->authPlainLine = !empty($aSettings['authPlainLine']);
// $object->viewErrors = !empty($aSettings['viewErrors']);
return $object;
}
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return \array_merge(
parent::jsonSerialize(),
[
// '@Object' => 'Object/SmtpSettings',
'useAuth' => $this->useAuth,
'setSender' => $this->setSender,
'usePhpMail' => $this->usePhpMail,
'authPlainLine' => $this->authPlainLine
// 'viewErrors' => $this->viewErrors
]
);
}
}

View file

@ -19,78 +19,49 @@ use MailSo\Net\Enumerations\ConnectionSecurityType;
*/
class SmtpClient extends \MailSo\Net\NetClient
{
/**
* @var string
*/
private $sEhlo = '';
private bool $bIsLoggined = false;
private string $sEhlo = '';
private bool $bRcpt = false;
private bool $bMail = false;
private array $aAuthTypes = array();
private array $aCapa = array();
/**
* @var bool
* RFC 1870
*/
private $bRcpt = false;
private int $iSizeCapaValue = 0;
/**
* @var bool
*/
private $bMail = false;
private array $aResults = array();
/**
* @var bool
*/
private $bData = false;
/**
* @var array
*/
private $aAuthTypes = array();
/**
* @var array
*/
private $aCapa = array();
/**
* @var int
*/
private $iSizeCapaValue = 0;
/**
* @var array
*/
private $aResults = array();
public function IsSupported(string $sCapa) : bool
public function hasCapability(string $sCapa) : bool
{
return \in_array(\strtoupper($sCapa), $this->aCapa);
}
public function IsAuthSupported(string $sAuth) : bool
public function maxSize() : int
{
return \in_array(\strtoupper($sAuth), $this->aAuthTypes);
}
public function HasSupportedAuth() : bool
{
return $this->IsAuthSupported('PLAIN') || $this->IsAuthSupported('LOGIN');
return $this->iSizeCapaValue;
}
public static function EhloHelper() : string
{
$sEhloHost = empty($_SERVER['SERVER_NAME']) ? '' : \trim($_SERVER['SERVER_NAME']);
if (empty($sEhloHost))
{
if (empty($sEhloHost)) {
$sEhloHost = empty($_SERVER['HTTP_HOST']) ? '' : \trim($_SERVER['HTTP_HOST']);
}
if (empty($sEhloHost))
{
if (empty($sEhloHost)) {
$sEhloHost = \function_exists('gethostname') ? \gethostname() : 'localhost';
}
$sEhloHost = \trim(\preg_replace('/:\d+$/', '', \trim($sEhloHost)));
if (\preg_match('/^\d+\.\d+\.\d+\.\d+$/', $sEhloHost))
{
if (\preg_match('/^\d+\.\d+\.\d+\.\d+$/', $sEhloHost)) {
$sEhloHost = '['.$sEhloHost.']';
}
@ -103,127 +74,140 @@ class SmtpClient extends \MailSo\Net\NetClient
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Smtp\Exceptions\*
*/
public function Connect(\MailSo\Net\ConnectSettings $oSettings, string $sEhloHost = '[127.0.0.1]') : void
public function Connect(\MailSo\Net\ConnectSettings $oSettings) : void
{
parent::Connect($oSettings);
$this->validateResponse(220);
$this->ehloOrHelo($sEhloHost);
$this->sEhlo = $sEhloHost;
$this->ehloOrHelo($oSettings->Ehlo);
$this->sEhlo = $oSettings->Ehlo;
if (ConnectionSecurityType::STARTTLS === $this->iSecurityType
|| (ConnectionSecurityType::AUTO_DETECT === $this->iSecurityType && $this->IsSupported('STARTTLS'))) {
if (ConnectionSecurityType::STARTTLS === $this->Settings->type
|| (ConnectionSecurityType::AUTO_DETECT === $this->Settings->type && $this->hasCapability('STARTTLS'))) {
$this->StartTLS();
}
}
private function StartTLS() : void
{
if ($this->IsSupported('STARTTLS')) {
if ($this->hasCapability('STARTTLS')) {
$this->sendRequestWithCheck('STARTTLS', 220);
$this->EnableCrypto();
$this->ehloOrHelo($this->sEhlo);
} else {
$this->writeLogException(
new \MailSo\Net\Exceptions\SocketUnsuppoterdSecureConnectionException('STARTTLS is not supported'),
\LOG_ERR, true);
\LOG_ERR);
}
}
public function supportsAuthType(string $sasl_type) : bool
{
return \in_array(\strtoupper($sasl_type), $this->aAuthTypes);
}
/**
* @throws \InvalidArgumentException
* @throws \MailSo\RuntimeException
* @throws \MailSo\Net\*
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Smtp\Exceptions\*
*/
public function Login(array $aCredentials) : self
public function Login(Settings $oSettings) : self
{
$sLogin = \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($aCredentials['Login']));
$sPassword = $aCredentials['Password'];
if ($this->bIsLoggined) {
return $this;
}
$sLogin = \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($oSettings->Login));
$sPassword = $oSettings->Password;
$type = '';
// https://github.com/the-djmaze/snappymail/pull/423
// $aCredentials['SASLMechanisms'][] = 'LOGIN';
\array_unshift($aCredentials['SASLMechanisms'], 'LOGIN');
foreach ($aCredentials['SASLMechanisms'] as $sasl_type) {
if ($this->IsAuthSupported($sasl_type) && \SnappyMail\SASL::isSupported($sasl_type)) {
foreach ($oSettings->SASLMechanisms as $sasl_type) {
if (\in_array(\strtoupper($sasl_type), $this->aAuthTypes) && \SnappyMail\SASL::isSupported($sasl_type)) {
$type = $sasl_type;
break;
}
}
if (!$type) {
if (!$this->Encrypted() && $this->IsSupported('STARTTLS')) {
if (!$this->Encrypted() && $this->hasCapability('STARTTLS')) {
$this->StartTLS();
return $this->Login($aCredentials);
return $this->Login($oSettings);
}
\trigger_error("SMTP {$this->GetConnectedHost()} no supported AUTH options. Disable login");
$this->writeLogException(
new \MailSo\Smtp\Exceptions\LoginBadMethodException,
\LOG_NOTICE, true);
$this->writeLogException(new \MailSo\Smtp\Exceptions\LoginBadMethodException);
}
$SASL = \SnappyMail\SASL::factory($type);
$SASL->base64 = true;
// Start authentication
try
{
$sResult = $this->sendRequestWithCheck('AUTH', 334, $type);
}
catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException)
{
$this->writeLogException(
new \MailSo\Smtp\Exceptions\LoginBadMethodException(
$oException->GetResponses(), $oException->getMessage(), 0, $oException),
\LOG_NOTICE, true);
}
if ($this->Settings->authPlainLine && $SASL instanceof \SnappyMail\SASL\Plain) {
// https://github.com/the-djmaze/snappymail/issues/1038
try
{
$sResult = $this->sendRequestWithCheck('AUTH', 235, 'PLAIN ' . $SASL->authenticate($sLogin, $sPassword));
}
catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException)
{
$this->writeLogException(
new \MailSo\Smtp\Exceptions\LoginBadCredentialsException($oException->GetResponses(), $oException->getMessage(), 0, $oException)
);
}
} else {
// Start authentication
try
{
$sResult = $this->sendRequestWithCheck('AUTH', 334, $type);
}
catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException)
{
$this->writeLogException(
new \MailSo\Smtp\Exceptions\LoginBadMethodException($oException->GetResponses(), $oException->getMessage(), 0, $oException)
);
}
try
{
if (0 === \strpos($type, 'SCRAM-')) {
// RFC 5802
$sResult = $this->sendRequestWithCheck($SASL->authenticate($sLogin, $sPassword, $sResult), 234, '');
$sChallenge = $SASL->challenge($sResult);
$this->oLogger && $this->oLogger->AddSecret($sChallenge);
$SASL->verify($this->sendRequestWithCheck($sChallenge, 235, '', true));
} else switch ($type) {
// RFC 4616
case 'PLAIN':
case 'XOAUTH2':
case 'OAUTHBEARER':
$sAuth = $SASL->authenticate($sLogin, $sPassword);
$this->oLogger && $this->oLogger->AddSecret($sAuth);
$this->sendRequestWithCheck($sAuth, 235, '', true);
break;
try
{
if (\str_starts_with($type, 'SCRAM-')) {
// RFC 5802
$sResult = $this->sendRequestWithCheck($SASL->authenticate($sLogin, $sPassword, $sResult), 234, '');
$sChallenge = $SASL->challenge($sResult);
$this->logMask($sChallenge);
$SASL->verify($this->sendRequestWithCheck($sChallenge, 235, '', true));
} else switch ($type) {
// RFC 4616
case 'PLAIN':
case 'XOAUTH2':
case 'OAUTHBEARER':
$sAuth = $SASL->authenticate($sLogin, $sPassword);
$this->logMask($sAuth);
$this->sendRequestWithCheck($sAuth, 235, '', true);
break;
case 'LOGIN':
$sResult = $this->sendRequestWithCheck($SASL->authenticate($sLogin, $sPassword, $sResult), 334, '');
$sPassword = $SASL->challenge($sResult);
$this->oLogger && $this->oLogger->AddSecret($sPassword);
$this->sendRequestWithCheck($sPassword, 235, '', true);
break;
case 'LOGIN':
$sResult = $this->sendRequestWithCheck($SASL->authenticate($sLogin, $sPassword, $sResult), 334, '');
$sPassword = $SASL->challenge($sResult);
$this->logMask($sPassword);
$this->sendRequestWithCheck($sPassword, 235, '', true);
break;
// RFC 2195
case 'CRAM-MD5':
if (empty($sResult)) {
$this->writeLogException(
new \MailSo\Smtp\Exceptions\NegativeResponseException,
\LOG_NOTICE, true
);
// RFC 2195
case 'CRAM-MD5':
if (empty($sResult)) {
$this->writeLogException(new \MailSo\Smtp\Exceptions\NegativeResponseException);
}
$this->sendRequestWithCheck($SASL->authenticate($sLogin, $sPassword, $sResult), 235, '', true);
break;
}
$this->sendRequestWithCheck($SASL->authenticate($sLogin, $sPassword, $sResult), 235, '', true);
break;
}
catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException)
{
$this->writeLogException(
new \MailSo\Smtp\Exceptions\LoginBadCredentialsException($oException->GetResponses(), $oException->getMessage(), 0, $oException)
);
}
}
catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException)
{
$this->writeLogException(
new \MailSo\Smtp\Exceptions\LoginBadCredentialsException(
$oException->GetResponses(), $oException->getMessage(), 0, $oException),
\LOG_NOTICE, true);
}
$this->bIsLoggined = true;
return $this;
}
@ -233,28 +217,35 @@ class SmtpClient extends \MailSo\Net\NetClient
* @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Smtp\Exceptions\*
*/
public function MailFrom(string $sFrom, string $sSizeIfSupported = '', bool $bDsn = false) : self
public function MailFrom(string $sFrom, int $iSizeIfSupported = 0, bool $bDsn = false, bool $bRequireTLS = false) : self
{
$sFrom = \MailSo\Base\Utils::IdnToAscii(
\MailSo\Base\Utils::Trim($sFrom), true);
$sFrom = \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($sFrom), true);
$sCmd = 'FROM:<'.$sFrom.'>';
$sCmd = "FROM:<{$sFrom}>";
if (\strlen($sSizeIfSupported) && \is_numeric($sSizeIfSupported) && $this->IsSupported('SIZE'))
{
$sCmd .= ' SIZE='.$sSizeIfSupported;
if (0 < $iSizeIfSupported && $this->hasCapability('SIZE')) {
$sCmd .= ' SIZE='.$iSizeIfSupported;
}
if ($bDsn && $this->IsSupported('DSN'))
{
// RFC 3461
if ($bDsn && $this->hasCapability('DSN')) {
$sCmd .= ' RET=HDRS';
}
// RFC 6152
if ($this->hasCapability('8BITMIME')) {
// $sCmd .= ' BODY=8BITMIME';
}
// RFC 8689
if ($bRequireTLS && $this->hasCapability('REQUIRETLS')) {
$sCmd .= ' REQUIRETLS';
}
$this->sendRequestWithCheck('MAIL', 250, $sCmd);
$this->bMail = true;
$this->bRcpt = false;
$this->bData = false;
return $this;
}
@ -266,20 +257,15 @@ class SmtpClient extends \MailSo\Net\NetClient
*/
public function Rcpt(string $sTo, bool $bDsn = false) : self
{
if (!$this->bMail)
{
$this->writeLogException(
new \MailSo\RuntimeException('No sender reverse path has been supplied'),
\LOG_ERR, true);
if (!$this->bMail) {
$this->writeLogException(new \MailSo\RuntimeException('No sender reverse path has been supplied'), \LOG_ERR);
}
$sTo = \MailSo\Base\Utils::IdnToAscii(
\MailSo\Base\Utils::Trim($sTo), true);
$sTo = \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($sTo), true);
$sCmd = 'TO:<'.$sTo.'>';
if ($bDsn && $this->IsSupported('DSN'))
{
if ($bDsn && $this->hasCapability('DSN')) {
$sCmd .= ' NOTIFY=SUCCESS,FAILURE';
}
@ -310,8 +296,7 @@ class SmtpClient extends \MailSo\Net\NetClient
*/
public function Data(string $sData) : self
{
if (!\strlen(\trim($sData)))
{
if (!\strlen(\trim($sData))) {
throw new \InvalidArgumentException;
}
@ -333,16 +318,12 @@ class SmtpClient extends \MailSo\Net\NetClient
*/
public function DataWithStream($rDataStream) : self
{
if (!\is_resource($rDataStream))
{
if (!\is_resource($rDataStream)) {
throw new \InvalidArgumentException;
}
if (!$this->bRcpt)
{
$this->writeLogException(
new \MailSo\RuntimeException('No recipient forward path has been supplied'),
\LOG_ERR, true);
if (!$this->bRcpt) {
$this->writeLogException(new \MailSo\RuntimeException('No recipient forward path has been supplied'), \LOG_ERR);
}
$this->sendRequestWithCheck('DATA', 354);
@ -351,37 +332,25 @@ class SmtpClient extends \MailSo\Net\NetClient
$this->bRunningCallback = true;
while (!\feof($rDataStream))
{
while (!\feof($rDataStream)) {
$sBuffer = \fgets($rDataStream);
if (false !== $sBuffer)
{
if (0 === \strpos($sBuffer, '.'))
{
$sBuffer = '.'.$sBuffer;
if (false === $sBuffer) {
if (!\feof($rDataStream)) {
$this->writeLogException(new \MailSo\RuntimeException('Cannot read input resource'), \LOG_ERR);
}
$this->sendRaw(\rtrim($sBuffer, "\r\n"), false);
\MailSo\Base\Utils::ResetTimeLimit();
continue;
break;
}
else if (!\feof($rDataStream))
{
$this->writeLogException(
new \MailSo\RuntimeException('Cannot read input resource'),
\LOG_ERR, true);
if (\str_starts_with($sBuffer, '.')) {
$sBuffer = '.' . $sBuffer;
}
break;
$this->sendRaw(\rtrim($sBuffer, "\r\n"), false);
\MailSo\Base\Utils::ResetTimeLimit();
}
$this->sendRequestWithCheck('.', 250);
$this->bRunningCallback = false;
$this->bData = true;
return $this;
}
@ -396,7 +365,6 @@ class SmtpClient extends \MailSo\Net\NetClient
$this->bMail = false;
$this->bRcpt = false;
$this->bData = false;
return $this;
}
@ -408,11 +376,8 @@ class SmtpClient extends \MailSo\Net\NetClient
*/
public function Vrfy(string $sUser) : self
{
$sUser = \MailSo\Base\Utils::IdnToAscii(
\MailSo\Base\Utils::Trim($sUser));
$this->sendRequestWithCheck('VRFY', array(250, 251, 252), $sUser);
$this->sendRequestWithCheck('VRFY', array(250, 251, 252),
\MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($sUser)));
return $this;
}
@ -435,14 +400,11 @@ class SmtpClient extends \MailSo\Net\NetClient
*/
public function Logout() : void
{
if ($this->IsConnected())
{
if ($this->IsConnected()) {
$this->sendRequestWithCheck('QUIT', 221);
}
$this->bMail = false;
$this->bRcpt = false;
$this->bData = false;
}
/**
@ -452,19 +414,16 @@ class SmtpClient extends \MailSo\Net\NetClient
*/
private function sendRequest(string $sCommand, string $sAddToCommand = '', bool $bSecureLog = false) : void
{
if (!\strlen(\trim($sCommand)))
{
$this->writeLogException(
new \InvalidArgumentException,
\LOG_ERR, true);
$sCommand = \trim($sCommand);
if (!\strlen($sCommand)) {
$this->writeLogException(new \InvalidArgumentException, \LOG_ERR);
}
$this->IsConnected(true);
$sCommand = \trim($sCommand);
$sRealCommand = $sCommand.(0 === \strlen($sAddToCommand) ? '' : ' '.$sAddToCommand);
$sRealCommand = $sCommand . (\strlen($sAddToCommand) ? ' '.$sAddToCommand : '');
$sFakeCommand = ($bSecureLog) ? '********' : '';
$sFakeCommand = $bSecureLog ? '********' : '';
$this->sendRaw($sRealCommand, true, $sFakeCommand);
}
@ -509,33 +468,32 @@ class SmtpClient extends \MailSo\Net\NetClient
private function ehlo(string $sHost) : void
{
$this->sendRequestWithCheck('EHLO', 250, $sHost);
foreach ($this->aResults as $sLine)
{
/*
250-PIPELINING\r\n
250-SIZE 256000000\r\n
250-ETRN\r\n
250-STARTTLS\r\n
250-ENHANCEDSTATUSCODES\r\n
250-8BITMIME\r\n
250-DSN\r\n
250 SMTPUTF8\r\n
*/
foreach ($this->aResults as $sLine) {
$aMatch = array();
if (\preg_match('/[\d]+[ \-](.+)$/', $sLine, $aMatch) && isset($aMatch[1]) && \strlen($aMatch[1]))
{
$sLine = \trim($aMatch[1]);
$aLine = \preg_split('/[ =]/', $sLine, 2);
if (!empty($aLine[0]))
{
if (\preg_match('/[\d]+[ \-](.+)$/', $sLine, $aMatch) && isset($aMatch[1]) && \strlen($aMatch[1])) {
$aLine = \preg_split('/[ =]/', \trim($aMatch[1]), 2);
if (!empty($aLine[0])) {
$sCapa = \strtoupper($aLine[0]);
if (('AUTH' === $sCapa || 'SIZE' === $sCapa) && !empty($aLine[1]))
{
if (!empty($aLine[1]) && ('AUTH' === $sCapa || 'SIZE' === $sCapa)) {
$sSubLine = \trim(\strtoupper($aLine[1]));
if (\strlen($sSubLine))
{
if ('AUTH' === $sCapa)
{
if (\strlen($sSubLine)) {
if ('AUTH' === $sCapa) {
$this->aAuthTypes = \explode(' ', $sSubLine);
}
else if ('SIZE' === $sCapa && \is_numeric($sSubLine))
{
} else if ('SIZE' === $sCapa && \is_numeric($sSubLine)) {
$this->iSizeCapaValue = (int) $sSubLine;
}
}
}
$this->aCapa[] = $sCapa;
}
}
@ -560,43 +518,34 @@ class SmtpClient extends \MailSo\Net\NetClient
*/
private function validateResponse($mExpectCode, string $sErrorPrefix = '') : void
{
if (!\is_array($mExpectCode))
{
$mExpectCode = array((int) $mExpectCode);
}
else
{
$mExpectCode = \array_map('intval', $mExpectCode);
}
$mExpectCode = \is_array($mExpectCode)
? \array_map('intval', $mExpectCode)
: array((int) $mExpectCode);
$aParts = array('', '', '');
$this->aResults = array();
do
{
$this->getNextBuffer();
$aParts = \preg_split('/([\s\-]+)/', $this->sResponseBuffer, 2, PREG_SPLIT_DELIM_CAPTURE);
$sResponse = $this->getNextBuffer();
$aParts = \preg_split('/([\s\-]+)/', $sResponse, 2, PREG_SPLIT_DELIM_CAPTURE);
if (3 === \count($aParts) && \is_numeric($aParts[0]))
{
if ('-' !== \substr($aParts[1], 0, 1) && !\in_array((int) $aParts[0], $mExpectCode))
{
if (3 === \count($aParts) && \is_numeric($aParts[0])) {
if ('-' !== \substr($aParts[1], 0, 1) && !\in_array((int) $aParts[0], $mExpectCode)) {
$this->writeLogException(
new Exceptions\NegativeResponseException($this->aResults,
('' === $sErrorPrefix ? '' : $sErrorPrefix.': ').\trim(
(\count($this->aResults) ? \implode("\r\n", $this->aResults)."\r\n" : '').
$this->sResponseBuffer)), \LOG_ERR, true);
$sResponse)), \LOG_ERR);
}
}
else
{
} else {
$this->writeLogException(
new Exceptions\ResponseException($this->aResults,
('' === $sErrorPrefix ? '' : $sErrorPrefix.': ').\trim(
(\count($this->aResults) ? \implode("\r\n", $this->aResults)."\r\n" : '').
$this->sResponseBuffer)), \LOG_ERR, true);
$sResponse)), \LOG_ERR);
}
$this->aResults[] = $this->sResponseBuffer;
$this->aResults[] = $sResponse;
}
while ('-' === \substr($aParts[1], 0, 1));
}

View file

@ -2,8 +2,6 @@
namespace RainLoop;
use RainLoop\Enumerations\UploadError;
class Actions
{
use Actions\Admin;
@ -14,6 +12,8 @@ class Actions
use Actions\Localization;
use Actions\Themes;
use \MailSo\Log\Inherit;
const AUTH_MAILTO_TOKEN_KEY = 'smmailtoauth';
/**
@ -26,16 +26,17 @@ class Actions
/**
* This session cookie contains a \RainLoop\Model\Account
* Value is Base64 EncryptToJSON
*/
const AUTH_SPEC_TOKEN_KEY = 'smaccount';
/**
* This session cookie optionally contains a \RainLoop\Model\AdditionalAccount
* Value is Base64 EncryptToJSON
*/
const AUTH_ADDITIONAL_TOKEN_KEY = 'smadditional';
const AUTH_SPEC_LOGOUT_TOKEN_KEY = 'smspeclogout';
const AUTH_SPEC_LOGOUT_CUSTOM_MSG_KEY = 'smspeclogoutcmk';
const APP_DUMMY = '********';
/**
* @var \MailSo\Base\Http
@ -57,11 +58,6 @@ class Actions
*/
protected $oPlugins = null;
/**
* @var \MailSo\Log\Logger
*/
protected $oLogger = null;
/**
* @var \MailSo\Log\Logger
*/
@ -128,9 +124,12 @@ class Actions
$sLogFileName = $this->oConfig->Get('logs', 'filename', '');
if ('syslog' === $sLogFileName) {
$oDriver = new \MailSo\Log\Drivers\Syslog();
} else if ('stderr' === $sLogFileName) {
$oDriver = new \MailSo\Log\Drivers\StderrStream();
} else {
$sLogFileFullPath = \APP_PRIVATE_DATA . 'logs/' . $this->compileLogFileName($sLogFileName);
$oDriver = new \MailSo\Log\Drivers\File($sLogFileFullPath);
$sLogFileFullPath = \trim($this->oConfig->Get('logs', 'path', '')) ?: \APP_PRIVATE_DATA . 'logs';
\is_dir($sLogFileFullPath) || \mkdir($sLogFileFullPath, 0700, true);
$oDriver = new \MailSo\Log\Drivers\File($sLogFileFullPath . '/' . $this->compileLogFileName($sLogFileName));
}
$this->oLogger->append($oDriver
->SetTimeZone($this->oConfig->Get('logs', 'time_zone', 'UTC'))
@ -138,14 +137,14 @@ class Actions
$oHttp = $this->Http();
$this->oLogger->Write(
$this->logWrite(
'[SM:' . APP_VERSION . '][IP:'
. $oHttp->GetClientIp($this->oConfig->Get('labs', 'http_client_ip_check_proxy', false))
. '][PID:' . (\MailSo\Base\Utils::FunctionCallable('getmypid') ? \getmypid() : 'unknown')
. '][' . $oHttp->GetServer('SERVER_SOFTWARE', '~')
. '][' . \MailSo\Base\Http::GetServer('SERVER_SOFTWARE', '~')
. '][' . (\MailSo\Base\Utils::FunctionCallable('php_sapi_name') ? \php_sapi_name() : '~')
. '][Streams:' . \implode(',', \stream_get_transports())
. '][' . $oHttp->GetMethod() . ' ' . $oHttp->GetScheme() . '://' . $oHttp->GetHost(false, false) . $oHttp->GetServer('REQUEST_URI', '') . ']'
. '][' . $oHttp->GetMethod() . ' ' . $oHttp->GetScheme() . '://' . $oHttp->GetHost(false) . \MailSo\Base\Http::GetServer('REQUEST_URI', '') . ']'
);
}
@ -243,22 +242,22 @@ class Actions
public function BootEnd(): void
{
try {
if ($this->MailClient()->IsLoggined()) {
$this->MailClient()->Disconnect();
if ($this->ImapClient()->IsLoggined()) {
$this->ImapClient()->Disconnect();
}
} catch (\Throwable $oException) {
unset($oException);
}
}
protected function compileLogParams(string $sLine, ?Model\Account $oAccount = null, bool $bUrlEncode = false, array $aAdditionalParams = array()): string
protected function compileLogParams(string $sLine, ?Model\Account $oAccount = null, array $aAdditionalParams = array()): string
{
$aClear = array();
if (false !== \strpos($sLine, '{date:')) {
$oConfig = $this->oConfig;
$sLine = \preg_replace_callback('/\{date:([^}]+)\}/', function ($aMatch) use ($oConfig, $bUrlEncode) {
return Utils::UrlEncode((new \DateTime('now', new \DateTimeZone($oConfig->Get('logs', 'time_zone', 'UTC'))))->format($aMatch[1]), $bUrlEncode);
$sLine = \preg_replace_callback('/\{date:([^}]+)\}/', function ($aMatch) use ($oConfig) {
return (new \DateTime('now', new \DateTimeZone($oConfig->Get('logs', 'time_zone', 'UTC'))))->format($aMatch[1]);
}, $sLine);
$aClear['/\{date:([^}]*)\}/'] = 'date';
@ -270,13 +269,13 @@ class Actions
}
if ($oAccount) {
$sLine = \str_replace('{imap:login}', Utils::UrlEncode($oAccount->IncLogin(), $bUrlEncode), $sLine);
$sLine = \str_replace('{imap:host}', Utils::UrlEncode($oAccount->Domain()->IncHost(), $bUrlEncode), $sLine);
$sLine = \str_replace('{imap:port}', Utils::UrlEncode($oAccount->Domain()->IncPort(), $bUrlEncode), $sLine);
$sLine = \str_replace('{imap:login}', $oAccount->IncLogin(), $sLine);
$sLine = \str_replace('{imap:host}', $oAccount->Domain()->IncHost(), $sLine);
$sLine = \str_replace('{imap:port}', $oAccount->Domain()->IncPort(), $sLine);
$sLine = \str_replace('{smtp:login}', Utils::UrlEncode($oAccount->OutLogin(), $bUrlEncode), $sLine);
$sLine = \str_replace('{smtp:host}', Utils::UrlEncode($oAccount->Domain()->OutHost(), $bUrlEncode), $sLine);
$sLine = \str_replace('{smtp:port}', Utils::UrlEncode($oAccount->Domain()->OutPort(), $bUrlEncode), $sLine);
$sLine = \str_replace('{smtp:login}', $oAccount->OutLogin(), $sLine);
$sLine = \str_replace('{smtp:host}', $oAccount->Domain()->OutHost(), $sLine);
$sLine = \str_replace('{smtp:port}', $oAccount->Domain()->OutPort(), $sLine);
}
$aClear['/\{imap:([^}]*)\}/i'] = 'imap';
@ -285,19 +284,19 @@ class Actions
if (false !== \strpos($sLine, '{request:')) {
if (false !== \strpos($sLine, '{request:ip}')) {
$sLine = \str_replace('{request:ip}', Utils::UrlEncode($this->Http()->GetClientIp(
$this->oConfig->Get('labs', 'http_client_ip_check_proxy', false)), $bUrlEncode), $sLine);
$sLine = \str_replace('{request:ip}',
$this->Http()->GetClientIp($this->oConfig->Get('labs', 'http_client_ip_check_proxy', false)),
$sLine);
}
if (false !== \strpos($sLine, '{request:domain}')) {
$sLine = \str_replace('{request:domain}',
Utils::UrlEncode($this->Http()->GetHost(false, true, true), $bUrlEncode), $sLine);
$sLine = \str_replace('{request:domain}', $this->Http()->GetHost(true, true), $sLine);
}
if (false !== \strpos($sLine, '{request:domain-clear}')) {
$sLine = \str_replace('{request:domain-clear}',
Utils::UrlEncode(
\MailSo\Base\Utils::GetClearDomainName($this->Http()->GetHost(false, true, true)), $bUrlEncode), $sLine);
\MailSo\Base\Utils::GetClearDomainName($this->Http()->GetHost(true, true)),
$sLine);
}
$aClear['/\{request:([^}]*)\}/i'] = 'request';
@ -306,15 +305,15 @@ class Actions
if (false !== \strpos($sLine, '{user:')) {
if (false !== \strpos($sLine, '{user:uid}')) {
$sLine = \str_replace('{user:uid}',
Utils::UrlEncode(\base_convert(\sprintf('%u',
\crc32(Utils::GetConnectionToken())), 10, 32), $bUrlEncode),
\base_convert(\sprintf('%u', \crc32(Utils::GetConnectionToken())), 10, 32),
$sLine
);
}
if (false !== \strpos($sLine, '{user:ip}')) {
$sLine = \str_replace('{user:ip}', Utils::UrlEncode($this->Http()->GetClientIp(
$this->oConfig->Get('labs', 'http_client_ip_check_proxy', false)), $bUrlEncode), $sLine);
$sLine = \str_replace('{user:ip}',
$this->Http()->GetClientIp($this->oConfig->Get('labs', 'http_client_ip_check_proxy', false)),
$sLine);
}
if (\preg_match('/\{user:(email|login|domain)\}/i', $sLine)) {
@ -325,14 +324,12 @@ class Actions
if ($oAccount) {
$sEmail = $oAccount->Email();
$sLine = \str_replace('{user:email}', Utils::UrlEncode($sEmail, $bUrlEncode), $sLine);
$sLine = \str_replace('{user:login}', Utils::UrlEncode(
\MailSo\Base\Utils::GetAccountNameFromEmail($sEmail), $bUrlEncode), $sLine);
$sLine = \str_replace('{user:domain}', Utils::UrlEncode(
\MailSo\Base\Utils::GetDomainFromEmail($sEmail), $bUrlEncode), $sLine);
$sLine = \str_replace('{user:domain-clear}', Utils::UrlEncode(
\MailSo\Base\Utils::GetClearDomainName(
\MailSo\Base\Utils::GetDomainFromEmail($sEmail)), $bUrlEncode), $sLine);
$sLine = \str_replace('{user:email}', $sEmail, $sLine);
$sLine = \str_replace('{user:login}', \MailSo\Base\Utils::GetAccountNameFromEmail($sEmail), $sLine);
$sLine = \str_replace('{user:domain}', \MailSo\Base\Utils::GetDomainFromEmail($sEmail), $sLine);
$sLine = \str_replace('{user:domain-clear}',
\MailSo\Base\Utils::GetClearDomainName(\MailSo\Base\Utils::GetDomainFromEmail($sEmail)),
$sLine);
}
}
@ -403,6 +400,12 @@ class Actions
return $this->oMailClient;
}
public function ImapClient(): \MailSo\Imap\ImapClient
{
// $this->initMailClientConnection();
return $this->MailClient()->ImapClient();
}
// Stores data in AdditionalAccount else MainAccount
public function LocalStorageProvider(): Providers\Storage
{
@ -460,13 +463,13 @@ class Actions
return $this->oDomainProvider;
}
public function AddressBookProvider(?Model\Account $oAccount = null, bool $bForceEnable = false): Providers\AddressBook
public function AddressBookProvider(?Model\Account $oAccount = null): Providers\AddressBook
{
if (null === $this->oAddressBookProvider) {
$oDriver = null;
try {
// if ($bForceEnable || $this->oConfig->Get('contacts', 'enable', false)) {
if ($bForceEnable || $this->GetCapa(Enumerations\Capa::CONTACTS)) {
// if ($this->oConfig->Get('contacts', 'enable', false)) {
if ($this->GetCapa(Enumerations\Capa::CONTACTS)) {
$oDriver = $this->fabrica('address-book', $oAccount);
}
if ($oAccount && $oDriver) {
@ -474,8 +477,9 @@ class Actions
$oDriver->setDAVClientConfig($this->getContactsSyncData($oAccount));
}
} catch (\Throwable $e) {
\SnappyMail\LOG::error('AddressBook', $e->getMessage()."\n".$e->getTraceAsString());
$oDriver = null;
\SnappyMail\LOG::error('AddressBook', $e->getMessage());
// $oDriver = new Providers\AddressBook\PdoAddressBook();
}
$this->oAddressBookProvider = new Providers\AddressBook($oDriver);
$this->oAddressBookProvider->SetLogger($this->oLogger);
@ -505,7 +509,10 @@ class Actions
switch (true) {
default:
case $bForceFile:
$oDriver = new \MailSo\Cache\Drivers\File(APP_PRIVATE_DATA . 'cache', $sKey);
$oDriver = new \MailSo\Cache\Drivers\File(
\trim($this->oConfig->Get('cache', 'path', '')) ?: APP_PRIVATE_DATA . 'cache',
$sKey
);
break;
case ('APCU' === $sDriver) &&
@ -550,11 +557,6 @@ class Actions
return $this->oPlugins;
}
public function Logger(): \MailSo\Log\Logger
{
return $this->oLogger;
}
public function LoggerAuth(): \MailSo\Log\Logger
{
if (!$this->oLoggerAuth) {
@ -562,13 +564,10 @@ class Actions
if ($this->oConfig->Get('logs', 'auth_logging', false)) {
// $this->oLoggerAuth->SetLevel(\LOG_WARNING);
$sAuthLogFileFullPath = \APP_PRIVATE_DATA . 'logs/' . $this->compileLogFileName(
$this->oConfig->Get('logs', 'auth_logging_filename', ''));
$sAuthLogFileFullPath = (\trim($this->oConfig->Get('logs', 'path', '') ?: \APP_PRIVATE_DATA . 'logs'))
. '/' . $this->compileLogFileName($this->oConfig->Get('logs', 'auth_logging_filename', ''));
$sLogFileDir = \dirname($sAuthLogFileFullPath);
if (!\is_dir($sLogFileDir)) {
\mkdir($sLogFileDir, 0755, true);
}
\is_dir($sLogFileDir) || \mkdir($sLogFileDir, 0755, true);
$this->oLoggerAuth->append(
(new \MailSo\Log\Drivers\File($sAuthLogFileFullPath))
->DisableTimePrefix()
@ -584,13 +583,13 @@ class Actions
{
$sLine = $this->oConfig->Get('logs', 'auth_logging_format', '');
if (!empty($sLine)) {
$this->LoggerAuth()->Write($this->compileLogParams($sLine, $oAccount, false, $aAdditionalParams));
$this->LoggerAuth()->Write($this->compileLogParams($sLine, $oAccount, $aAdditionalParams), \LOG_WARNING);
}
if (($this->oConfig->Get('logs', 'auth_logging', false) || $this->oConfig->Get('logs', 'auth_syslog', false))
&& \openlog('snappymail', 0, \LOG_AUTHPRIV)) {
\syslog(\LOG_ERR, $this->compileLogParams(
$admin ? 'Admin Auth failed: ip={request:ip} user={user:login}' : 'Auth failed: ip={request:ip} user={imap:login}',
$oAccount, false, $aAdditionalParams
$oAccount, $aAdditionalParams
));
\closelog();
}
@ -603,60 +602,61 @@ class Actions
$aResult = array(
'Auth' => false,
'Title' => $oConfig->Get('webmail', 'title', 'SnappyMail Webmail'),
'LoadingDescription' => $oConfig->Get('webmail', 'loading_description', 'SnappyMail'),
'title' => $oConfig->Get('webmail', 'title', 'SnappyMail Webmail'),
'loadingDescription' => $oConfig->Get('webmail', 'loading_description', 'SnappyMail'),
'Plugins' => array(),
'System' => \array_merge(
array(
'version' => APP_VERSION,
'token' => $oConfig->Get('security', 'csrf_protection', false) ? Utils::GetCsrfToken() : '',
'token' => Utils::GetCsrfToken(),
'languages' => \SnappyMail\L10n::getLanguages(false),
'webPath' => \RainLoop\Utils::WebPath(),
'webVersionPath' => \RainLoop\Utils::WebVersionPath()
), $bAdmin ? array(
'adminHostUse' => '' !== $oConfig->Get('security', 'admin_panel_host', ''),
'adminHost' => '' !== $oConfig->Get('security', 'admin_panel_host', ''),
'adminPath' => $oConfig->Get('security', 'admin_panel_key', '') ?: 'admin',
'adminAllowed' => (bool)$oConfig->Get('security', 'allow_admin_panel', true)
) : array()
),
'AllowLanguagesOnLogin' => (bool) $oConfig->Get('login', 'allow_languages_on_login', true)
'allowLanguagesOnLogin' => (bool) $oConfig->Get('login', 'allow_languages_on_login', true)
);
$sLanguage = $oConfig->Get('webmail', 'language', 'en');
$UserLanguageRaw = $this->detectUserLanguage($bAdmin);
if ($bAdmin) {
// $this->AdminAppData($aResult);
$aResult['Auth'] = $this->IsAdminLoggined(false);
if ($aResult['Auth']) {
$aResult['AdminLogin'] = (string)$oConfig->Get('security', 'admin_login', '');
$aResult['AdminTOTP'] = (string)$oConfig->Get('security', 'admin_totp', '');
$aResult['UseTokenProtection'] = (bool)$oConfig->Get('security', 'csrf_protection', true);
$aResult['EnabledPlugins'] = (bool)$oConfig->Get('plugins', 'enable', false);
$aResult['adminLogin'] = (string)$oConfig->Get('security', 'admin_login', '');
$aResult['adminTOTP'] = (string)$oConfig->Get('security', 'admin_totp', '');
$aResult['pluginsEnable'] = (bool)$oConfig->Get('plugins', 'enable', false);
$aResult['LoginDefaultDomain'] = $oConfig->Get('login', 'default_domain', '');
$aResult['DetermineUserLanguage'] = (bool)$oConfig->Get('login', 'determine_user_language', true);
$aResult['DetermineUserDomain'] = (bool)$oConfig->Get('login', 'determine_user_domain', false);
$aResult['loginDefaultDomain'] = $oConfig->Get('login', 'default_domain', '');
$aResult['determineUserLanguage'] = (bool)$oConfig->Get('login', 'determine_user_language', true);
$aResult['determineUserDomain'] = (bool)$oConfig->Get('login', 'determine_user_domain', false);
$aResult['VerifySslCertificate'] = (bool)$oConfig->Get('ssl', 'verify_certificate', false);
$aResult['AllowSelfSigned'] = (bool)$oConfig->Get('ssl', 'allow_self_signed', true);
$aResult['supportedPdoDrivers'] = \RainLoop\Pdo\Base::getAvailableDrivers();
$aResult['supportedPdoDrivers'] = \RainLoop\Common\PdoAbstract::getAvailableDrivers();
$aResult['contactsEnable'] = (bool)$oConfig->Get('contacts', 'enable', false);
$aResult['contactsSync'] = (bool)$oConfig->Get('contacts', 'allow_sync', false);
$aResult['contactsPdoType'] = Providers\AddressBook\PdoAddressBook::validPdoType($oConfig->Get('contacts', 'type', 'sqlite'));
$aResult['contactsPdoDsn'] = (string)$oConfig->Get('contacts', 'pdo_dsn', '');
$aResult['contactsPdoType'] = (string)$oConfig->Get('contacts', 'type', '');
$aResult['contactsPdoUser'] = (string)$oConfig->Get('contacts', 'pdo_user', '');
$aResult['contactsPdoPassword'] = static::APP_DUMMY;
$aResult['contactsMySQLSSLCA'] = (string) $oConfig->Get('contacts', 'mysql_ssl_ca', '');
$aResult['contactsMySQLSSLVerify'] = !!$oConfig->Get('contacts', 'mysql_ssl_verify', true);
$aResult['contactsMySQLSSLCiphers'] = (string) $oConfig->Get('contacts', 'mysql_ssl_ciphers', '');
$aResult['contactsSuggestionsLimit'] = (int)$oConfig->Get('contacts', 'suggestions_limit', 20);
$aResult['ContactsEnable'] = (bool)$oConfig->Get('contacts', 'enable', false);
$aResult['ContactsSync'] = (bool)$oConfig->Get('contacts', 'allow_sync', false);
$aResult['ContactsPdoType'] = Providers\AddressBook\PdoAddressBook::validPdoType($this->oConfig->Get('contacts', 'type', 'sqlite'));
$aResult['ContactsPdoDsn'] = (string)$oConfig->Get('contacts', 'pdo_dsn', '');
$aResult['ContactsPdoType'] = (string)$oConfig->Get('contacts', 'type', '');
$aResult['ContactsPdoUser'] = (string)$oConfig->Get('contacts', 'pdo_user', '');
$aResult['ContactsPdoPassword'] = (string)APP_DUMMY;
$aResult['faviconUrl'] = $oConfig->Get('webmail', 'favicon_url', '');
$aResult['FaviconUrl'] = $oConfig->Get('webmail', 'favicon_url', '');
$aResult['WeakPassword'] = \is_file(APP_PRIVATE_DATA.'admin_password.txt');
$aResult['weakPassword'] = \is_file(APP_PRIVATE_DATA.'admin_password.txt');
$aResult['System']['languagesAdmin'] = \SnappyMail\L10n::getLanguages(true);
$aResult['LanguageAdmin'] = $this->ValidateLanguage($oConfig->Get('webmail', 'language_admin', 'en'), '', true);
$aResult['UserLanguageAdmin'] = $this->ValidateLanguage($UserLanguageRaw, '', true, true);
$aResult['languageAdmin'] = $this->ValidateLanguage($oConfig->Get('webmail', 'language_admin', 'en'), '', true);
$aResult['languageUsers'] = $this->ValidateLanguage($UserLanguageRaw, '', true, true);
} else {
$passfile = APP_PRIVATE_DATA.'admin_password.txt';
$sPassword = $oConfig->Get('security', 'admin_password', '');
@ -674,45 +674,35 @@ class Actions
$aResult = \array_merge($aResult, [
'Auth' => true,
'Email' => \MailSo\Base\Utils::IdnToUtf8($oAccount->Email()),
'IncLogin' => $oAccount->IncLogin(),
'OutLogin' => $oAccount->OutLogin(),
'AccountHash' => $oAccount->Hash(),
'AccountSignMe' => isset($_COOKIE[self::AUTH_SIGN_ME_TOKEN_KEY]),
'MainEmail' => \MailSo\Base\Utils::IdnToUtf8($this->getMainAccountFromToken()->Email()),
'MailToEmail' => '',
'accountHash' => $oAccount->Hash(),
'accountSignMe' => isset($_COOKIE[self::AUTH_SIGN_ME_TOKEN_KEY]),
'ContactsIsAllowed' => $this->AddressBookProvider($oAccount)->IsActive(),
'ContactsSyncIsAllowed' => (bool)$oConfig->Get('contacts', 'allow_sync', false),
'ContactsSyncInterval' => (int)$oConfig->Get('contacts', 'sync_interval', 20),
'ContactsSyncMode' => 0,
'ContactsSyncUrl' => '',
'ContactsSyncUser' => '',
'ContactsSyncPassword' => '',
'contactsAllowed' => $this->AddressBookProvider($oAccount)->IsActive(),
'ViewHTML' => (bool) $oConfig->Get('defaults', 'view_html', true),
'ShowImages' => (bool) $oConfig->Get('defaults', 'show_images', false),
'ViewImages' => $oConfig->Get('defaults', 'view_images', 'ask'),
'ViewImagesWhitelist' => '',
'RemoveColors' => (bool) $oConfig->Get('defaults', 'remove_colors', false),
'AllowStyles' => false,
'ListInlineAttachments' => false,
'CollapseBlockquotes' => true,
'MaxBlockquotesLevel' => 0,
'simpleAttachmentsList' => false,
'listGrouped' => false,
'MessagesPerPage' => (int) $oConfig->Get('webmail', 'messages_per_page', 25),
'MessageReadDelay' => (int) $oConfig->Get('webmail', 'message_read_delay', 5),
'MsgDefaultAction' => 1,
'MsgDefaultAction' => (int) $oConfig->Get('defaults', 'msg_default_action', 1),
'SoundNotification' => true,
'NotificationSound' => 'new-mail',
'DesktopNotifications' => true,
'Layout' => (int) $oConfig->Get('defaults', 'view_layout', Enumerations\Layout::SIDE_PREVIEW),
'EditorDefaultType' => \str_replace('Forced', '', $oConfig->Get('defaults', 'view_editor_type', '')),
'UseCheckboxesInList' => (bool) $oConfig->Get('defaults', 'view_use_checkboxes', true),
'showNextMessage' => (bool) $oConfig->Get('defaults', 'view_show_next_message', false),
'AutoLogout' => (int) $oConfig->Get('defaults', 'autologout', 30),
'UseThreads' => (bool) $oConfig->Get('defaults', 'mail_use_threads', false),
'AllowDraftAutosave' => (bool) $oConfig->Get('defaults', 'allow_draft_autosave', true),
'ReplySameFolder' => (bool) $oConfig->Get('defaults', 'mail_reply_same_folder', false),
'ContactsAutosave' => (bool) $oConfig->Get('defaults', 'contacts_autosave', true),
'HideUnsubscribed' => false,
'HideDeleted' => true,
'UnhideKolabFolders' => false,
'UserBackgroundName' => '',
'UserBackgroundHash' => '',
'SieveAllowFileintoInbox' => (bool)$oConfig->Get('labs', 'sieve_allow_fileinto_inbox', false)
'sieveAllowFileintoInbox' => (bool)$oConfig->Get('labs', 'sieve_allow_fileinto_inbox', false)
]);
$aAttachmentsActions = array();
@ -723,9 +713,6 @@ class Actions
}
$aResult['System'] = \array_merge(
$aResult['System'], array(
'allowHtmlEditorBitiButtons' => (bool)$oConfig->Get('labs', 'allow_html_editor_biti_buttons', false),
'allowCtrlEnterOnCompose' => (bool)$oConfig->Get('labs', 'allow_ctrl_enter_on_compose', false),
'useImapThread' => (bool)$oConfig->Get('imap', 'use_thread', false),
'allowAppendMessage' => (bool)$oConfig->Get('labs', 'allow_message_append', false),
'folderSpecLimit' => (int)$oConfig->Get('labs', 'folders_spec_limit', 50),
'listPermanentFiltered' => '' !== \trim($oConfig->Get('imap', 'message_list_permanent_filter', '')),
@ -734,49 +721,48 @@ class Actions
)
);
if ($aResult['ContactsIsAllowed'] && $aResult['ContactsSyncIsAllowed']) {
$mData = $this->getContactsSyncData($oAccount);
if (\is_array($mData)) {
$aResult['ContactsSyncMode'] = isset($mData['Mode']) ? $mData['Mode'] : 0;
$aResult['ContactsSyncUrl'] = isset($mData['Url']) ? \trim($mData['Url']) : '';
$aResult['ContactsSyncUser'] = isset($mData['User']) ? \trim($mData['User']) : '';
$aResult['ContactsSyncPassword'] = APP_DUMMY;
}
if ($aResult['contactsAllowed'] && $oConfig->Get('contacts', 'allow_sync', false)) {
$aData = $this->getContactsSyncData($oAccount) ?: [
'Mode' => 0,
'Url' => '',
'User' => ''
];
$aData['Password'] = empty($aData['Password']) ? '' : static::APP_DUMMY;
$aData['Interval'] = \max(20, \min(320, (int) $oConfig->Get('contacts', 'sync_interval', 20)));
unset($aData['PasswordHMAC']);
$aResult['ContactsSync'] = $aData;
}
$sToken = Utils::GetCookie(self::AUTH_MAILTO_TOKEN_KEY, null);
$sToken = \SnappyMail\Cookies::get(self::AUTH_MAILTO_TOKEN_KEY);
if (null !== $sToken) {
Utils::ClearCookie(self::AUTH_MAILTO_TOKEN_KEY);
\SnappyMail\Cookies::clear(self::AUTH_MAILTO_TOKEN_KEY);
$mMailToData = Utils::DecodeKeyValuesQ($sToken);
if (!empty($mMailToData['MailTo']) && 'MailTo' === $mMailToData['MailTo'] && !empty($mMailToData['To'])) {
$aResult['MailToEmail'] = \MailSo\Base\Utils::IdnToUtf8($mMailToData['To']);
$aResult['mailToEmail'] = \MailSo\Base\Utils::IdnToUtf8($mMailToData['To']);
}
}
$oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount);
if ($oSettingsLocal instanceof Settings) {
$aResult['SentFolder'] = (string)$oSettingsLocal->GetConf('SentFolder', '');
$aResult['DraftsFolder'] = (string)$oSettingsLocal->GetConf('DraftFolder', '');
$aResult['SpamFolder'] = (string)$oSettingsLocal->GetConf('SpamFolder', '');
$aResult['TrashFolder'] = (string)$oSettingsLocal->GetConf('TrashFolder', '');
$aResult['ArchiveFolder'] = (string)$oSettingsLocal->GetConf('ArchiveFolder', '');
$aResult['HideUnsubscribed'] = (bool)$oSettingsLocal->GetConf('HideUnsubscribed', $aResult['HideUnsubscribed']);
$aResult['UseThreads'] = (bool)$oSettingsLocal->GetConf('UseThreads', $aResult['UseThreads']);
$aResult['ReplySameFolder'] = (bool)$oSettingsLocal->GetConf('ReplySameFolder', $aResult['ReplySameFolder']);
$aResult['HideDeleted'] = (bool)$oSettingsLocal->GetConf('HideDeleted', $aResult['HideDeleted']);
$aResult['UnhideKolabFolders'] = (bool)$oSettingsLocal->GetConf('UnhideKolabFolders', $aResult['UnhideKolabFolders']);
}
if ($oConfig->Get('login', 'determine_user_language', true)) {
$sLanguage = $this->ValidateLanguage($UserLanguageRaw, $sLanguage, false);
}
// MainAccount or AdditionalAccount
$aResult = \array_merge($aResult, $this->getAccountData($oAccount));
// MainAccount
$oSettings = $this->SettingsProvider()->Load($oAccount);
if ($oSettings instanceof Settings) {
if ($oConfig->Get('webmail', 'allow_languages_on_settings', true)) {
$sLanguage = (string) $oSettings->GetConf('Language', $sLanguage);
/*
foreach ($oSettings->toArray() as $key => $value) {
$aResult[\lcfirst($key)] = $value;
}
*/
if ($oConfig->Get('webmail', 'allow_languages_on_settings', true)) {
$sLanguage = (string) $oSettings->GetConf('language', $sLanguage);
}
$aResult['hourCycle'] = $oSettings->GetConf('hourCycle', '');
if (!$oSettings->GetConf('MessagesPerPage')) {
$oSettings->SetConf('MessagesPerPage', $oSettings->GetConf('MPP', $aResult['MessagesPerPage']));
}
@ -784,13 +770,23 @@ class Actions
$aResult['EditorDefaultType'] = \str_replace('Forced', '', $oSettings->GetConf('EditorDefaultType', $aResult['EditorDefaultType']));
$aResult['requestReadReceipt'] = (bool) $oSettings->GetConf('requestReadReceipt', false);
$aResult['requestDsn'] = (bool) $oSettings->GetConf('requestDsn', false);
$aResult['requireTLS'] = (bool) $oSettings->GetConf('requireTLS', false);
$aResult['pgpSign'] = (bool) $oSettings->GetConf('pgpSign', false);
$aResult['pgpEncrypt'] = (bool) $oSettings->GetConf('pgpEncrypt', false);
$aResult['allowSpellcheck'] = (bool) $oSettings->GetConf('allowSpellcheck', false);
// $aResult['allowCtrlEnterOnCompose'] = (bool) $oSettings->GetConf('allowCtrlEnterOnCompose', true);
$aResult['ViewHTML'] = (bool)$oSettings->GetConf('ViewHTML', $aResult['ViewHTML']);
$aResult['ShowImages'] = (bool)$oSettings->GetConf('ShowImages', $aResult['ShowImages']);
$show_images = (bool) $oSettings->GetConf('ShowImages', false);
$aResult['ViewImages'] = $oSettings->GetConf('ViewImages', $show_images ? 'always' : $aResult['ViewImages']);
$aResult['ViewImagesWhitelist'] = $oSettings->GetConf('ViewImagesWhitelist', '');
$aResult['RemoveColors'] = (bool)$oSettings->GetConf('RemoveColors', $aResult['RemoveColors']);
$aResult['AllowStyles'] = (bool)$oSettings->GetConf('AllowStyles', $aResult['AllowStyles']);
$aResult['ListInlineAttachments'] = (bool)$oSettings->GetConf('ListInlineAttachments', $aResult['ListInlineAttachments']);
$aResult['CollapseBlockquotes'] = (bool)$oSettings->GetConf('CollapseBlockquotes', $aResult['CollapseBlockquotes']);
$aResult['MaxBlockquotesLevel'] = (int)$oSettings->GetConf('MaxBlockquotesLevel', $aResult['MaxBlockquotesLevel']);
$aResult['simpleAttachmentsList'] = (bool)$oSettings->GetConf('simpleAttachmentsList', $aResult['simpleAttachmentsList']);
$aResult['listGrouped'] = (bool)$oSettings->GetConf('listGrouped', $aResult['listGrouped']);
$aResult['ContactsAutosave'] = (bool)$oSettings->GetConf('ContactsAutosave', $aResult['ContactsAutosave']);
$aResult['MessagesPerPage'] = (int)$oSettings->GetConf('MessagesPerPage', $aResult['MessagesPerPage']);
$aResult['MessageReadDelay'] = (int)$oSettings->GetConf('MessageReadDelay', $aResult['MessageReadDelay']);
@ -799,6 +795,7 @@ class Actions
$aResult['NotificationSound'] = (string)$oSettings->GetConf('NotificationSound', $aResult['NotificationSound']);
$aResult['DesktopNotifications'] = (bool)$oSettings->GetConf('DesktopNotifications', $aResult['DesktopNotifications']);
$aResult['UseCheckboxesInList'] = (bool)$oSettings->GetConf('UseCheckboxesInList', $aResult['UseCheckboxesInList']);
$aResult['showNextMessage'] = (bool)$oSettings->GetConf('showNextMessage', $aResult['showNextMessage']);
$aResult['AllowDraftAutosave'] = (bool)$oSettings->GetConf('AllowDraftAutosave', $aResult['AllowDraftAutosave']);
$aResult['AutoLogout'] = (int)$oSettings->GetConf('AutoLogout', $aResult['AutoLogout']);
$aResult['Layout'] = (int)$oSettings->GetConf('Layout', $aResult['Layout']);
@ -811,14 +808,14 @@ class Actions
$aResult['fontMono'] = $oSettings->GetConf('fontMono', '');
if ($this->GetCapa(Enumerations\Capa::USER_BACKGROUND)) {
$aResult['UserBackgroundName'] = (string)$oSettings->GetConf('UserBackgroundName', $aResult['UserBackgroundName']);
$aResult['UserBackgroundHash'] = (string)$oSettings->GetConf('UserBackgroundHash', $aResult['UserBackgroundHash']);
$aResult['userBackgroundName'] = (string)$oSettings->GetConf('UserBackgroundName', '');
$aResult['userBackgroundHash'] = (string)$oSettings->GetConf('UserBackgroundHash', '');
}
}
$aResult['NewMailSounds'] = [];
$aResult['newMailSounds'] = [];
foreach (\glob(APP_VERSION_ROOT_PATH.'static/sounds/*.mp3') as $file) {
$aResult['NewMailSounds'][] = \basename($file, '.mp3');
$aResult['newMailSounds'][] = \basename($file, '.mp3');
}
}
else {
@ -826,7 +823,7 @@ class Actions
$sLanguage = $this->ValidateLanguage($UserLanguageRaw, $sLanguage, false);
}
if ('0.0.0' === APP_VERSION) {
if (SNAPPYMAIL_DEV) {
$aResult['DevEmail'] = $oConfig->Get('labs', 'dev_email', '');
$aResult['DevPassword'] = $oConfig->Get('labs', 'dev_password', '');
} else {
@ -834,15 +831,13 @@ class Actions
$aResult['DevPassword'] = '';
}
$aResult['SignMe'] = (string) $oConfig->Get('login', 'sign_me_auto', Enumerations\SignMeType::DEFAULT_OFF);
$aResult['AdditionalLoginError'] = $this->GetSpecLogoutCustomMgsWithDeletion();
$aResult['signMe'] = (string) $oConfig->Get('login', 'sign_me_auto', Enumerations\SignMeType::DEFAULT_OFF);
}
}
if ($aResult['Auth']) {
$aResult['UseLocalProxyForExternalImages'] = (bool)$oConfig->Get('labs', 'use_local_proxy_for_external_images', false);
$aResult['AllowLanguagesOnSettings'] = (bool) $oConfig->Get('webmail', 'allow_languages_on_settings', true);
$aResult['useLocalProxyForExternalImages'] = (bool)$oConfig->Get('labs', 'use_local_proxy_for_external_images', false);
$aResult['allowLanguagesOnSettings'] = (bool) $oConfig->Get('webmail', 'allow_languages_on_settings', true);
$aResult['Capa'] = $this->Capa($bAdmin, $oAccount);
$value = \ini_get('upload_max_filesize');
$upload_max_filesize = \intval($value);
@ -851,26 +846,25 @@ class Actions
case 'M': $upload_max_filesize *= 1024;
case 'K': $upload_max_filesize *= 1024;
}
$aResult['AttachmentLimit'] = \min($upload_max_filesize, ((int) $oConfig->Get('webmail', 'attachment_size_limit', 10)) * 1024 * 1024);
$aResult['PhpUploadSizes'] = array(
$aResult['attachmentLimit'] = \min($upload_max_filesize, ((int) $oConfig->Get('webmail', 'attachment_size_limit', 10)) * 1024 * 1024);
$aResult['phpUploadSizes'] = array(
'upload_max_filesize' => $value,
'post_max_size' => \ini_get('post_max_size')
);
$aResult['System']['themes'] = $this->GetThemes();
}
$sStaticCache = $this->StaticCache();
$aResult['Theme'] = $this->GetTheme($bAdmin);
$aResult['Language'] = $this->ValidateLanguage($sLanguage, '', false);
$aResult['UserLanguage'] = $this->ValidateLanguage($UserLanguageRaw, '', false, true);
$aResult['language'] = $this->ValidateLanguage($sLanguage, '', false);
$aResult['userLanguage'] = $this->ValidateLanguage($UserLanguageRaw, '', false, true);
$aResult['PluginsLink'] = $this->oPlugins->HaveJs($bAdmin)
? './?/Plugins/0/' . ($bAdmin ? 'Admin' : 'User') . '/' . $sStaticCache . '/'
? 'Plugins/0/' . ($bAdmin ? 'Admin' : 'User') . '/' . $this->etag($this->oPlugins->Hash()) . '/'
: '';
$bAppJsDebug = $this->oConfig->Get('labs', 'use_app_debug_js', false);
$bAppJsDebug = $this->oConfig->Get('debug', 'javascript', false)
|| $this->oConfig->Get('debug', 'enable', false);
$aResult['StaticLibsJs'] = Utils::WebStaticPath('js/' . ($bAppJsDebug ? '' : 'min/') .
'libs' . ($bAppJsDebug ? '' : '.min') . '.js');
@ -880,25 +874,20 @@ class Actions
return $aResult;
}
protected function requestSleep(int $iDelay = 1): void
{
$time = \microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'];
if ($iDelay > $time) {
\usleep(($iDelay - $time) * 1000000);
}
}
protected function loginErrorDelay(): void
{
$iDelay = (int)$this->oConfig->Get('labs', 'login_fault_delay', 0);
$iDelay = (int) $this->oConfig->Get('labs', 'login_fault_delay', 0);
if (0 < $iDelay) {
$this->requestSleep($iDelay);
$seconds = $iDelay - (\microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']);
if (0 < $seconds) {
\usleep(\intval($seconds * 1000000));
}
}
}
protected function getAdditionalLogParamsByUserLogin(string $sLogin, bool $bAdmin = false): array
{
$sHost = $bAdmin ? $this->Http()->GetHost(false, true, true) : \MailSo\Base\Utils::GetDomainFromEmail($sLogin);
$sHost = $bAdmin ? $this->Http()->GetHost(true, true) : \MailSo\Base\Utils::GetDomainFromEmail($sLogin);
return array(
'{imap:login}' => $sLogin,
'{imap:host}' => $sHost,
@ -912,13 +901,12 @@ class Actions
public function DoPing(): array
{
return $this->DefaultResponse(__FUNCTION__, 'Pong');
return $this->DefaultResponse('Pong');
}
public function DoVersion(): array
{
return $this->DefaultResponse(__FUNCTION__,
APP_VERSION === (string)$this->GetActionParam('Version', ''));
return $this->DefaultResponse(APP_VERSION === (string)$this->GetActionParam('version', ''));
}
public function MainClearFileName(string $sFileName, string $sContentType, string $sMimeIndex, int $iMaxLength = 250): string
@ -934,82 +922,38 @@ class Actions
return \MailSo\Base\Utils::SecureFileName(\MailSo\Base\Utils::Utf8Clear($sClearedFileName));
}
protected function getUploadErrorMessageByCode(int $iError, int &$iClientError): string
{
$sError = '';
$iClientError = UploadError::NORMAL;
switch ($iError) {
case UPLOAD_ERR_OK:
break;
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
case UploadError::CONFIG_SIZE:
case UploadError::EMPTY_FILES_DATA:
$sError = 'File is too big';
$iClientError = UploadError::FILE_IS_TOO_BIG;
break;
case UPLOAD_ERR_PARTIAL:
$sError = 'File partially uploaded';
$iClientError = UploadError::FILE_PARTIALLY_UPLOADED;
break;
case UPLOAD_ERR_NO_FILE:
$sError = 'No file uploaded';
$iClientError = UploadError::FILE_NO_UPLOADED;
break;
case UPLOAD_ERR_NO_TMP_DIR:
case UPLOAD_ERR_CANT_WRITE:
case UPLOAD_ERR_EXTENSION:
$sError = 'Missing temp folder';
$iClientError = UploadError::MISSING_TEMP_FOLDER;
break;
case UploadError::ON_SAVING:
$sError = 'Error on saving file';
$iClientError = UploadError::FILE_ON_SAVING_ERROR;
break;
case UploadError::FILE_TYPE:
$sError = 'Invalid file type';
$iClientError = UploadError::FILE_TYPE;
break;
case UploadError::UNKNOWN:
default:
$sError = 'Unknown error';
$iClientError = UploadError::UNKNOWN;
break;
}
return $sError;
}
public function Upload(): array
public function Upload(?array $aFile, int $iError): array
{
$oAccount = $this->getAccountFromToken();
$aResponse = array();
$aFile = $this->GetActionParam('File', null);
$iError = $this->GetActionParam('Error', Enumerations\UploadError::UNKNOWN);
if ($oAccount && UPLOAD_ERR_OK === $iError && \is_array($aFile)) {
$sSavedName = 'upload-post-' . \md5($aFile['name'] . $aFile['tmp_name']);
// Detect content-type
$type = \SnappyMail\File\MimeType::fromFile($aFile['tmp_name'], $aFile['name'])
?: \SnappyMail\File\MimeType::fromFilename($aFile['name']);
if ($type) {
$aFile['type'] = $type;
$sSavedName .= \SnappyMail\File\MimeType::toExtension($type);
}
if (!$this->FilesProvider()->MoveUploadedFile($oAccount, $sSavedName, $aFile['tmp_name'])) {
$iError = Enumerations\UploadError::ON_SAVING;
} else {
$sUploadName = $aFile['name'];
$iSize = $aFile['size'];
$sMimeType = $aFile['type'];
$aResponse['Attachment'] = array(
'Name' => $sUploadName,
'TempName' => $sSavedName,
'MimeType' => $sMimeType,
'Size' => (int)$iSize
'name' => $aFile['name'],
'tempName' => $sSavedName,
'mimeType' => $aFile['type'],
'size' => (int) $aFile['size']
);
}
}
if (UPLOAD_ERR_OK !== $iError) {
$iClientError = Enumerations\UploadError::NORMAL;
$sError = $this->getUploadErrorMessageByCode($iError, $iClientError);
$iClientError = 0;
$sError = Enumerations\UploadError::getUserMessage($iError, $iClientError);
if (!empty($sError)) {
$aResponse['ErrorCode'] = $iClientError;
@ -1017,88 +961,7 @@ class Actions
}
}
return $this->DefaultResponse(__FUNCTION__, $aResponse);
}
public function UploadBackground(): array
{
$oAccount = $this->getAccountFromToken();
if (!$this->GetCapa(Enumerations\Capa::USER_BACKGROUND)) {
return $this->FalseResponse(__FUNCTION__);
}
$sName = '';
$sHash = '';
$aFile = $this->GetActionParam('File', null);
$iError = $this->GetActionParam('Error', Enumerations\UploadError::UNKNOWN);
if ($oAccount && UPLOAD_ERR_OK === $iError && \is_array($aFile)) {
$sMimeType = \strtolower(\MailSo\Base\Utils::MimeContentType($aFile['name']));
if (\in_array($sMimeType, array('image/png', 'image/jpg', 'image/jpeg'))) {
$sSavedName = 'upload-post-' . \md5($aFile['name'] . $aFile['tmp_name']);
if (!$this->FilesProvider()->MoveUploadedFile($oAccount, $sSavedName, $aFile['tmp_name'])) {
$iError = Enumerations\UploadError::ON_SAVING;
} else {
$rData = $this->FilesProvider()->GetFile($oAccount, $sSavedName);
if (\is_resource($rData)) {
$sData = \stream_get_contents($rData);
if (!empty($sData) && \strlen($sData)) {
$sName = $aFile['name'];
if (empty($sName)) {
$sName = '_';
}
if ($this->StorageProvider()->Put($oAccount,
Providers\Storage\Enumerations\StorageType::CONFIG,
'background',
Utils::jsonEncode(array(
'Name' => $aFile['name'],
'ContentType' => $sMimeType,
'Raw' => \base64_encode($sData)
))
)) {
$oSettings = $this->SettingsProvider()->Load($oAccount);
if ($oSettings) {
$sHash = \MailSo\Base\Utils::Sha1Rand($sName . APP_VERSION . APP_SALT);
$oSettings->SetConf('UserBackgroundName', $sName);
$oSettings->SetConf('UserBackgroundHash', $sHash);
$this->SettingsProvider()->Save($oAccount, $oSettings);
}
}
}
unset($sData);
}
if (\is_resource($rData)) {
\fclose($rData);
}
unset($rData);
}
$this->FilesProvider()->Clear($oAccount, $sSavedName);
} else {
$iError = Enumerations\UploadError::FILE_TYPE;
}
}
if (UPLOAD_ERR_OK !== $iError) {
$iClientError = Enumerations\UploadError::NORMAL;
$sError = $this->getUploadErrorMessageByCode($iError, $iClientError);
if (!empty($sError)) {
return $this->FalseResponse(__FUNCTION__, $iClientError, $sError);
}
}
return $this->DefaultResponse(__FUNCTION__, !empty($sName) && !empty($sHash) ? array(
'Name' => $sName,
'Hash' => $sHash
) : false);
return $this->DefaultResponse($aResponse);
}
public function Capa(bool $bAdmin, ?Model\Account $oAccount = null): array
@ -1108,14 +971,18 @@ class Actions
$oConfig = $this->oConfig;
$aResult = array(
'AdditionalAccounts' => (bool) $oConfig->Get('webmail', 'allow_additional_accounts', false),
'AttachmentThumbnails' => (bool) $oConfig->Get('interface', 'show_attachment_thumbnail', true),
'AttachmentThumbnails' => (bool) $oConfig->Get('interface', 'show_attachment_thumbnail', true)
&& ($bAdmin
|| \extension_loaded('gd')
|| \extension_loaded('gmagick')
|| \extension_loaded('imagick')
),
'AttachmentsActions' => (bool) $oConfig->Get('capa', 'attachments_actions', false),
'Contacts' => (bool) $oConfig->Get('contacts', 'enable', false),
'DangerousActions' => (bool) $oConfig->Get('capa', 'dangerous_actions', true),
'GnuPG' => (bool) $oConfig->Get('security', 'openpgp', false) && \SnappyMail\PGP\GnuPG::isSupported(),
'Identities' => (bool) $oConfig->Get('webmail', 'allow_additional_identities', false),
'Kolab' => false, // See Kolab plugin
'MessageActions' => (bool) $oConfig->Get('capa', 'message_actions', true),
'OpenPGP' => (bool) $oConfig->Get('security', 'openpgp', false),
'Quota' => (bool) $oConfig->Get('capa', 'quota', true),
'Sieve' => false,
@ -1134,36 +1001,29 @@ class Actions
public function etag(string $sKey): string
{
return \md5('Etag:' . \md5($sKey . \md5($this->oConfig->Get('cache', 'index', ''))));
// if ($sKey && $this->oConfig->Get('cache', 'enable', true) && $this->oConfig->Get('cache', 'http', true)) {
return \md5($sKey . $this->oConfig->Get('cache', 'index', '') . APP_VERSION);
}
public function cacheByKey(string $sKey, bool $bForce = false): bool
public function cacheByKey(string $sKey): bool
{
$bResult = false;
if (!empty($sKey) && ($bForce || ($this->oConfig->Get('cache', 'enable', true) && $this->oConfig->Get('cache', 'http', true)))) {
$iExpires = $this->oConfig->Get('cache', 'http_expires', 3600);
if (0 < $iExpires) {
$this->Http()->ServerUseCache($this->etag($sKey), 1382478804, \time() + $iExpires);
$bResult = true;
}
if ($sKey && $this->oConfig->Get('cache', 'enable', true) && $this->oConfig->Get('cache', 'http', true)) {
\MailSo\Base\Http::ServerUseCache(
$this->etag($sKey),
0, // issue with messages
$this->oConfig->Get('cache', 'http_expires', 3600)
);
return true;
}
if (!$bResult) {
$this->Http()->ServerNoCache();
}
return $bResult;
$this->Http()->ServerNoCache();
return false;
}
public function verifyCacheByKey(string $sKey, bool $bForce = false): void
public function verifyCacheByKey(string $sKey): void
{
if (!empty($sKey) && ($bForce || $this->oConfig->Get('cache', 'enable', true) && $this->oConfig->Get('cache', 'http', true))) {
$sIfNoneMatch = $this->Http()->GetHeader('If-None-Match', '');
if ($this->etag($sKey) === $sIfNoneMatch) {
\MailSo\Base\Http::StatusHeader(304);
$this->cacheByKey($sKey);
exit(0);
}
if ($sKey && $this->oConfig->Get('cache', 'enable', true) && $this->oConfig->Get('cache', 'http', true)) {
\MailSo\Base\Http::checkETag($this->etag($sKey));
// \MailSo\Base\Http::checkLastModified(0);
}
}
@ -1174,9 +1034,9 @@ class Actions
{
$oAccount = $this->getAccountFromToken();
if (!$this->MailClient()->IsLoggined()) {
if ($oAccount && !$this->ImapClient()->IsLoggined()) {
try {
$oAccount->ImapConnectAndLoginHelper($this->oPlugins, $this->MailClient(), $this->oConfig);
$oAccount->ImapConnectAndLogin($this->oPlugins, $this->ImapClient(), $this->oConfig);
} catch (\MailSo\Net\Exceptions\ConnectionException $oException) {
throw new Exceptions\ClientException(Notifications::ConnectionError, $oException);
} catch (\Throwable $oException) {
@ -1187,18 +1047,21 @@ class Actions
return $oAccount;
}
protected function getDecodedRawKeyValue(string $sRawKey): array
public function encodeRawKey(array $aValues): string
{
return empty($sRawKey) ? array() : Utils::DecodeKeyValuesQ($sRawKey);
$aValues['accountHash'] = $this->getAccountFromToken()->Hash();
return \MailSo\Base\Utils::UrlSafeBase64Encode(\json_encode($aValues));
}
public function StaticCache(): string
public function decodeRawKey(string $sRawKey): array
{
static $sCache = null;
if (!$sCache) {
$sCache = \md5(APP_VERSION . $this->oPlugins->Hash());
return empty($sRawKey) ? []
: (\json_decode(\MailSo\Base\Utils::UrlSafeBase64Decode($sRawKey), true) ?: []);
/*
if (empty($aValues['accountHash']) || $aValues['accountHash'] !== $oAccount->Hash()) {
return [];
}
return $sCache;
*/
}
public function SetActionParams(array $aCurrentActionParams, string $sMethodName = ''): self
@ -1231,10 +1094,10 @@ class Actions
return isset($this->aCurrentActionParams[$sKey]);
}
public function Location(string $sUrl): void
public function Location(string $sUrl, int $iStatus = 302): void
{
$this->oLogger->Write('Location: ' . $sUrl);
\header('Location: ' . $sUrl);
$this->logWrite("{$iStatus} Location: {$sUrl}");
\MailSo\Base\Http::Location($sUrl, $iStatus);
}
}

View file

@ -15,10 +15,7 @@ use RainLoop\Utils;
trait Accounts
{
/**
* @var RainLoop\Providers\Identities
*/
private $oIdentitiesProvider;
private ?\RainLoop\Providers\Identities $oIdentitiesProvider = null;
protected function GetMainEmail(Account $oAccount)
{
@ -41,7 +38,8 @@ trait Accounts
StorageType::CONFIG,
'additionalaccounts'
);
$aAccounts = $sAccounts ? \json_decode($sAccounts, true) : \SnappyMail\Upgrade::ConvertInsecureAccounts($this, $oAccount);
$aAccounts = $sAccounts ? \json_decode($sAccounts, true)
: \SnappyMail\Upgrade::ConvertInsecureAccounts($this, $oAccount);
if ($aAccounts && \is_array($aAccounts)) {
return $aAccounts;
}
@ -77,14 +75,15 @@ trait Accounts
$oMainAccount = $this->getMainAccountFromToken();
if (!$this->GetCapa(Capa::ADDITIONAL_ACCOUNTS)) {
return $this->FalseResponse(__FUNCTION__);
return $this->FalseResponse();
}
$aAccounts = $this->GetAccounts($oMainAccount);
$sEmail = \trim($this->GetActionParam('Email', ''));
$sPassword = $this->GetActionParam('Password', '');
$bNew = '1' === (string)$this->GetActionParam('New', '1');
$sEmail = \trim($this->GetActionParam('email', ''));
$sPassword = $this->GetActionParam('password', '');
$sName = \trim($this->GetActionParam('name', ''));
$bNew = !empty($this->GetActionParam('new', 1));
$sEmail = \MailSo\Base\Utils::IdnToAscii($sEmail, true);
if ($bNew && ($oMainAccount->Email() === $sEmail || isset($aAccounts[$sEmail]))) {
@ -93,12 +92,73 @@ trait Accounts
throw new ClientException(Notifications::AccountDoesNotExist);
}
$oNewAccount = $this->LoginProcess($sEmail, $sPassword, false, false);
if ($bNew || $sPassword) {
$oNewAccount = $this->LoginProcess($sEmail, $sPassword, false, false);
$aAccounts[$sEmail] = $oNewAccount->asTokenArray($oMainAccount);
} else {
$aAccounts[$sEmail] = \RainLoop\Model\AdditionalAccount::convertArray($aAccounts[$sEmail]);
}
$aAccounts[$oNewAccount->Email()] = $oNewAccount->asTokenArray($oMainAccount);
$this->SetAccounts($oMainAccount, $aAccounts);
if ($aAccounts[$sEmail]) {
$aAccounts[$sEmail]['name'] = $sName;
$this->SetAccounts($oMainAccount, $aAccounts);
}
return $this->TrueResponse(__FUNCTION__);
return $this->TrueResponse();
}
protected function loadAdditionalAccountImapClient(string $sEmail): \MailSo\Imap\ImapClient
{
$sEmail = \MailSo\Base\Utils::IdnToAscii(\trim($sEmail), true);
if (!\strlen($sEmail)) {
throw new ClientException(Notifications::AccountDoesNotExist);
}
$oMainAccount = $this->getMainAccountFromToken();
$aAccounts = $this->GetAccounts($oMainAccount);
if (!isset($aAccounts[$sEmail])) {
throw new ClientException(Notifications::AccountDoesNotExist);
}
$oAccount = AdditionalAccount::NewInstanceFromTokenArray($this, $aAccounts[$sEmail]);
if (!$oAccount) {
throw new ClientException(Notifications::AccountDoesNotExist);
}
$oImapClient = new \MailSo\Imap\ImapClient;
$oImapClient->SetLogger($this->Logger());
$this->imapConnect($oAccount, false, $oImapClient);
return $oImapClient;
}
public function DoAccountUnread(): array
{
$oImapClient = $this->loadAdditionalAccountImapClient($this->GetActionParam('email', ''));
$oInfo = $oImapClient->FolderStatus('INBOX');
return $this->DefaultResponse([
'unreadEmails' => \max(0, $oInfo->UNSEEN)
]);
}
/**
* Imports all mail from AdditionalAccount into MainAccount
*/
public function DoAccountImport(): array
{
$sEmail = $this->GetActionParam('email', '');
$oImapSource = $this->loadAdditionalAccountImapClient($sEmail);
$oMainAccount = $this->getMainAccountFromToken();
$oImapTarget = new \MailSo\Imap\ImapClient;
$oImapTarget->SetLogger($this->Logger());
$this->imapConnect($oMainAccount, false, $oImapTarget);
$oSync = new \SnappyMail\Imap\Sync;
$oSync->oImapSource = $oImapSource;
$oSync->oImapTarget = $oImapTarget;
$rootfolder = $this->GetActionParam('rootfolder', '') ?: $sEmail;
$oSync->import($rootfolder);
exit;
}
/**
@ -109,10 +169,10 @@ trait Accounts
$oMainAccount = $this->getMainAccountFromToken();
if (!$this->GetCapa(Capa::ADDITIONAL_ACCOUNTS)) {
return $this->FalseResponse(__FUNCTION__);
return $this->FalseResponse();
}
$sEmailToDelete = \trim($this->GetActionParam('EmailToDelete', ''));
$sEmailToDelete = \trim($this->GetActionParam('emailToDelete', ''));
$sEmailToDelete = \MailSo\Base\Utils::IdnToAscii($sEmailToDelete, true);
$aAccounts = $this->GetAccounts($oMainAccount);
@ -121,17 +181,59 @@ trait Accounts
$bReload = false;
$oAccount = $this->getAccountFromToken();
if ($oAccount instanceof AdditionalAccount && $oAccount->Email() === $sEmailToDelete) {
Utils::ClearCookie(self::AUTH_ADDITIONAL_TOKEN_KEY);
\SnappyMail\Cookies::clear(self::AUTH_ADDITIONAL_TOKEN_KEY);
$bReload = true;
}
unset($aAccounts[$sEmailToDelete]);
$this->SetAccounts($oMainAccount, $aAccounts);
return $this->TrueResponse(__FUNCTION__, array('Reload' => $bReload));
return $this->TrueResponse(array('Reload' => $bReload));
}
return $this->FalseResponse(__FUNCTION__);
return $this->FalseResponse();
}
public function getAccountData(Account $oAccount): array
{
$oConfig = $this->Config();
$aResult = [
'Email' => $oAccount->Email(),
'accountHash' => $oAccount->Hash(),
'mainEmail' => \RainLoop\Api::Actions()->getMainAccountFromToken()->Email(),
'contactsAllowed' => $this->AddressBookProvider($oAccount)->IsActive(),
'HideUnsubscribed' => false,
'UseThreads' => (bool) $oConfig->Get('defaults', 'mail_use_threads', false),
'ReplySameFolder' => (bool) $oConfig->Get('defaults', 'mail_reply_same_folder', false),
'HideDeleted' => true,
'ShowUnreadCount' => false,
'UnhideKolabFolders' => false,
'CheckMailInterval' => 15
];
$oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount);
if ($oSettingsLocal instanceof \RainLoop\Settings) {
$aResult['SentFolder'] = (string) $oSettingsLocal->GetConf('SentFolder', '');
$aResult['DraftsFolder'] = (string) $oSettingsLocal->GetConf('DraftsFolder', '');
$aResult['JunkFolder'] = (string) $oSettingsLocal->GetConf('JunkFolder', '');
$aResult['TrashFolder'] = (string) $oSettingsLocal->GetConf('TrashFolder', '');
$aResult['ArchiveFolder'] = (string) $oSettingsLocal->GetConf('ArchiveFolder', '');
$aResult['HideUnsubscribed'] = (bool) $oSettingsLocal->GetConf('HideUnsubscribed', $aResult['HideUnsubscribed']);
$aResult['UseThreads'] = (bool) $oSettingsLocal->GetConf('UseThreads', $aResult['UseThreads']);
$aResult['ReplySameFolder'] = (bool) $oSettingsLocal->GetConf('ReplySameFolder', $aResult['ReplySameFolder']);
$aResult['HideDeleted'] = (bool)$oSettingsLocal->GetConf('HideDeleted', $aResult['HideDeleted']);
$aResult['ShowUnreadCount'] = (bool)$oSettingsLocal->GetConf('ShowUnreadCount', $aResult['ShowUnreadCount']);
$aResult['UnhideKolabFolders'] = (bool)$oSettingsLocal->GetConf('UnhideKolabFolders', $aResult['UnhideKolabFolders']);
$aResult['CheckMailInterval'] = (int)$oSettingsLocal->GetConf('CheckMailInterval', $aResult['CheckMailInterval']);
/*
foreach ($oSettingsLocal->toArray() as $key => $value) {
$aResult[\lcfirst($key)] = $value;
}
$aResult['junkFolder'] = $aResult['spamFolder'];
unset($aResult['checkableFolder']);
unset($aResult['theme']);
*/
}
return $aResult;
}
/**
@ -141,32 +243,11 @@ trait Accounts
{
if ($this->switchAccount(\trim($this->GetActionParam('Email', '')))) {
$oAccount = $this->getAccountFromToken();
$aResult['Email'] = $oAccount->Email();
$aResult['IncLogin'] = $oAccount->IncLogin();
$aResult['OutLogin'] = $oAccount->OutLogin();
$aResult['AccountHash'] = $oAccount->Hash();
$aResult['MainEmail'] = ($oAccount instanceof AdditionalAccount)
? $oAccount->ParentEmail() : '';
$aResult['ContactsIsAllowed'] = $this->AddressBookProvider($oAccount)->IsActive();
$oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount);
if ($oSettingsLocal instanceof \RainLoop\Settings) {
$oConfig = $this->Config();
$aResult['SentFolder'] = (string) $oSettingsLocal->GetConf('SentFolder', '');
$aResult['DraftsFolder'] = (string) $oSettingsLocal->GetConf('DraftFolder', '');
$aResult['SpamFolder'] = (string) $oSettingsLocal->GetConf('SpamFolder', '');
$aResult['TrashFolder'] = (string) $oSettingsLocal->GetConf('TrashFolder', '');
$aResult['ArchiveFolder'] = (string) $oSettingsLocal->GetConf('ArchiveFolder', '');
$aResult['HideUnsubscribed'] = (bool) $oSettingsLocal->GetConf('HideUnsubscribed', false);
$aResult['UseThreads'] = (bool) $oSettingsLocal->GetConf('UseThreads', $oConfig->Get('defaults', 'mail_use_threads', false));
$aResult['ReplySameFolder'] = (bool) $oSettingsLocal->GetConf('ReplySameFolder', $oConfig->Get('defaults', 'mail_reply_same_folder', false));
$aResult['HideDeleted'] = (bool) $oSettingsLocal->GetConf('HideDeleted', true);
$aResult['UnhideKolabFolders'] = (bool) $oSettingsLocal->GetConf('UnhideKolabFolders', false);
}
$aResult = $this->getAccountData($oAccount);
// $this->Plugins()->InitAppData($bAdmin, $aResult, $oAccount);
return $this->DefaultResponse(__FUNCTION__, $aResult);
return $this->DefaultResponse($aResult);
}
return $this->FalseResponse(__FUNCTION__);
return $this->FalseResponse();
}
/**
@ -182,7 +263,7 @@ trait Accounts
}
$this->IdentitiesProvider()->UpdateIdentity($oAccount, $oIdentity);
return $this->DefaultResponse(__FUNCTION__, true);
return $this->TrueResponse();
}
/**
@ -193,16 +274,16 @@ trait Accounts
$oAccount = $this->getAccountFromToken();
if (!$this->GetCapa(Capa::IDENTITIES)) {
return $this->FalseResponse(__FUNCTION__);
return $this->FalseResponse();
}
$sId = \trim($this->GetActionParam('IdToDelete', ''));
$sId = \trim($this->GetActionParam('idToDelete', ''));
if (empty($sId)) {
throw new ClientException(Notifications::UnknownError);
}
$this->IdentitiesProvider()->DeleteIdentity($oAccount, $sId);
return $this->DefaultResponse(__FUNCTION__, true);
return $this->TrueResponse();
}
/**
@ -214,7 +295,7 @@ trait Accounts
$aIdentities = $this->GetActionParam('Identities', null);
if (!\is_array($aAccounts) && !\is_array($aIdentities)) {
return $this->FalseResponse(__FUNCTION__);
return $this->FalseResponse();
}
if (\is_array($aAccounts) && 1 < \count($aAccounts)) {
@ -226,7 +307,7 @@ trait Accounts
$this->SetAccounts($oAccount, $aAccounts);
}
return $this->DefaultResponse(__FUNCTION__, $this->LocalStorageProvider()->Put(
return $this->DefaultResponse($this->LocalStorageProvider()->Put(
$this->getAccountFromToken(),
StorageType::CONFIG,
'identities_order',
@ -241,11 +322,16 @@ trait Accounts
*/
public function DoAccountsAndIdentities(): array
{
return $this->DefaultResponse(__FUNCTION__, array(
'Accounts' => \array_map(
'MailSo\\Base\\Utils::IdnToUtf8',
\array_keys($this->GetAccounts($this->getMainAccountFromToken()))
),
// https://github.com/the-djmaze/snappymail/issues/571
return $this->DefaultResponse(array(
'Accounts' => \array_values(\array_map(function($value){
return [
'email' => \MailSo\Base\Utils::IdnToUtf8($value['email'] ?? $value[1]),
'name' => $value['name'] ?? ''
];
},
$this->GetAccounts($this->getMainAccountFromToken())
)),
'Identities' => $this->GetIdentities($this->getAccountFromToken())
));
}

View file

@ -7,11 +7,9 @@ use RainLoop\KeyPathHelper;
use RainLoop\Notifications;
use RainLoop\Utils;
//define('APP_DEV_VERSION', '0.0.0');
trait Admin
{
protected static $AUTH_ADMIN_TOKEN_KEY = 'smadmin';
protected static string $AUTH_ADMIN_TOKEN_KEY = 'smadmin';
public function IsAdminLoggined(bool $bThrowExceptionOnFalse = true) : bool
{
@ -22,8 +20,7 @@ trait Admin
}
}
if ($bThrowExceptionOnFalse)
{
if ($bThrowExceptionOnFalse) {
throw new ClientException(Notifications::AuthError);
}
@ -32,13 +29,13 @@ trait Admin
protected function getAdminAuthKey() : string
{
$cookie = Utils::GetCookie(static::$AUTH_ADMIN_TOKEN_KEY, '');
$cookie = \SnappyMail\Cookies::get(static::$AUTH_ADMIN_TOKEN_KEY);
if ($cookie) {
$aAdminHash = Utils::DecodeKeyValuesQ($cookie);
if (!empty($aAdminHash[1]) && 'token' === $aAdminHash[0]) {
return $aAdminHash[1];
}
Utils::ClearCookie(static::$AUTH_ADMIN_TOKEN_KEY);
\SnappyMail\Cookies::clear(static::$AUTH_ADMIN_TOKEN_KEY);
}
return '';
}

View file

@ -0,0 +1,205 @@
<?php
namespace RainLoop\Actions;
use RainLoop\Enumerations\PluginPropertyType;
use RainLoop\Exceptions\ClientException;
trait AdminDomains
{
public function DoAdminDomainLoad() : array
{
$this->IsAdminLoggined();
return $this->DefaultResponse($this->DomainProvider()->Load($this->GetActionParam('name', ''), false, false));
}
public function DoAdminDomainList() : array
{
$this->IsAdminLoggined();
$bIncludeAliases = !empty($this->GetActionParam('includeAliases', '1'));
return $this->DefaultResponse($this->DomainProvider()->GetList($bIncludeAliases));
}
public function DoAdminDomainDelete() : array
{
$this->IsAdminLoggined();
return $this->DefaultResponse($this->DomainProvider()->Delete((string) $this->GetActionParam('name', '')));
}
public function DoAdminDomainDisable() : array
{
$this->IsAdminLoggined();
return $this->DefaultResponse($this->DomainProvider()->Disable(
(string) $this->GetActionParam('name', ''),
!empty($this->GetActionParam('disabled', '0'))
));
}
public function DoAdminDomainSave() : array
{
$this->IsAdminLoggined();
$oDomain = $this->DomainProvider()->LoadOrCreateNewFromAction($this);
return $this->DefaultResponse($oDomain ? $this->DomainProvider()->Save($oDomain) : false);
}
public function DoAdminDomainAliasSave() : array
{
$this->IsAdminLoggined();
return $this->DefaultResponse($this->DomainProvider()->SaveAlias(
(string) $this->GetActionParam('name', ''),
(string) $this->GetActionParam('alias', '')
));
}
public function DoAdminDomainMatch() : array
{
$sEmail = $this->GetActionParam('username');
$sPassword = '********';
$sLogin = '';
$this->resolveLoginCredentials($sEmail, $sPassword, $sLogin);
$oDomain = \str_contains($sEmail, '@')
? $this->DomainProvider()->Load(\MailSo\Base\Utils::GetDomainFromEmail($sEmail), true)
: null;
return $this->DefaultResponse(array(
'email' => $sEmail,
'login' => $sLogin,
'domain' => $oDomain,
'whitelist' => $oDomain ? $oDomain->ValidateWhiteList($sEmail, $sLogin) : null
));
}
public function DoAdminDomainTest() : array
{
$this->IsAdminLoggined();
$bImapResult = false;
$sImapErrorDesc = '';
$bSmtpResult = false;
$sSmtpErrorDesc = '';
$bSieveResult = false;
$sSieveErrorDesc = '';
$oDomain = $this->DomainProvider()->LoadOrCreateNewFromAction($this, 'test.example.com');
if ($oDomain) {
$aAuth = $this->GetActionParam('auth');
try
{
$oImapClient = new \MailSo\Imap\ImapClient();
$oImapClient->SetLogger($this->Logger());
$oSettings = $oDomain->ImapSettings();
$oImapClient->Connect($oSettings);
if (!empty($aAuth['user'])) {
$oSettings->Login = $aAuth['user'];
$oSettings->Password = $aAuth['pass'];
$oImapClient->Login($oSettings);
}
$oImapClient->Disconnect();
$bImapResult = true;
}
catch (\MailSo\Net\Exceptions\SocketCanNotConnectToHostException $oException)
{
$this->logException($oException, \LOG_ERR);
$sImapErrorDesc = $oException->getSocketMessage();
if (empty($sImapErrorDesc)) {
$sImapErrorDesc = $oException->getMessage();
}
}
catch (\Throwable $oException)
{
$this->logException($oException, \LOG_ERR);
$sImapErrorDesc = $oException->getMessage();
}
if ($oDomain->OutUsePhpMail()) {
$bSmtpResult = \MailSo\Base\Utils::FunctionCallable('mail');
if (!$bSmtpResult) {
$sSmtpErrorDesc = 'PHP: mail() function is undefined';
}
} else {
try
{
$oSmtpClient = new \MailSo\Smtp\SmtpClient();
$oSmtpClient->SetLogger($this->Logger());
$oSettings = $oDomain->SmtpSettings();
$oSettings->Ehlo = \MailSo\Smtp\SmtpClient::EhloHelper();
$oSmtpClient->Connect($oSettings);
if (!empty($aAuth['user'])) {
$oSettings->Login = $aAuth['user'];
$oSettings->Password = $aAuth['pass'];
$oSmtpClient->Login($oSettings);
}
$oSmtpClient->Disconnect();
$bSmtpResult = true;
}
catch (\MailSo\Net\Exceptions\SocketCanNotConnectToHostException $oException)
{
$this->logException($oException, \LOG_ERR);
$sSmtpErrorDesc = $oException->getSocketMessage();
if (empty($sSmtpErrorDesc)) {
$sSmtpErrorDesc = $oException->getMessage();
}
}
catch (\Throwable $oException)
{
$this->logException($oException, \LOG_ERR);
$sSmtpErrorDesc = $oException->getMessage();
}
}
if ($oDomain->UseSieve()) {
try
{
$oSieveClient = new \MailSo\Sieve\SieveClient();
$oSieveClient->SetLogger($this->Logger());
$oSettings = $oDomain->SieveSettings();
$oSieveClient->Connect($oSettings);
if (!empty($aAuth['user'])) {
$oSettings->Login = $aAuth['user'];
$oSettings->Password = $aAuth['pass'];
$oSieveClient->Login($oSettings);
}
$oSieveClient->Disconnect();
$bSieveResult = true;
}
catch (\MailSo\Net\Exceptions\SocketCanNotConnectToHostException $oException)
{
$this->logException($oException, \LOG_ERR);
$sSieveErrorDesc = $oException->getSocketMessage();
if (empty($sSieveErrorDesc)) {
$sSieveErrorDesc = $oException->getMessage();
}
}
catch (\Throwable $oException)
{
$this->logException($oException, \LOG_ERR);
$sSieveErrorDesc = $oException->getMessage();
}
} else {
$bSieveResult = true;
}
}
return $this->DefaultResponse(array(
'Imap' => $bImapResult ? true : $sImapErrorDesc,
'Smtp' => $bSmtpResult ? true : $sSmtpErrorDesc,
'Sieve' => $bSieveResult ? true : $sSieveErrorDesc
));
}
}

View file

@ -0,0 +1,145 @@
<?php
namespace RainLoop\Actions;
use RainLoop\Enumerations\PluginPropertyType;
use RainLoop\Exceptions\ClientException;
use RainLoop\Notifications;
use SnappyMail\Repository;
trait AdminExtensions
{
public function DoAdminPackagesList() : array
{
return $this->DefaultResponse(Repository::getPackagesList());
}
public function DoAdminPackageDelete() : array
{
return $this->DefaultResponse(Repository::deletePackage($this->GetActionParam('id', '')));
}
public function DoAdminPackageInstall() : array
{
$sType = $this->GetActionParam('type', '');
$bResult = Repository::installPackage(
$sType,
$this->GetActionParam('id', ''),
$this->GetActionParam('file', '')
);
return $this->DefaultResponse($bResult ?
('plugin' !== $sType ? array('Reload' => true) : true) : false);
}
public function DoAdminPluginDisable() : array
{
$this->IsAdminLoggined();
$sId = (string) $this->GetActionParam('id', '');
$bDisable = '1' === (string) $this->GetActionParam('disabled', '1');
if (!$bDisable) {
$oPlugin = $this->Plugins()->CreatePluginByName($sId);
if ($oPlugin) {
$sValue = $oPlugin->Supported();
if (\strlen($sValue)) {
return $this->FalseResponse(Notifications::UnsupportedPluginPackage, $sValue);
}
} else {
return $this->FalseResponse(Notifications::InvalidPluginPackage);
}
}
return $this->DefaultResponse(Repository::enablePackage($sId, !$bDisable));
}
public function DoAdminPluginLoad() : array
{
$this->IsAdminLoggined();
$mResult = false;
$sId = (string) $this->GetActionParam('id', '');
if (!empty($sId)) {
$oPlugin = $this->Plugins()->CreatePluginByName($sId);
if ($oPlugin) {
$mResult = array(
'@Object' => 'Object/Plugin',
'id' => $sId,
'name' => $oPlugin->Name(),
'readme' => $oPlugin->Description(),
'config' => array()
);
$aMap = $oPlugin->ConfigMap();
if (\is_array($aMap)) {
$oConfig = $oPlugin->Config();
foreach ($aMap as $oItem) {
if ($oItem) {
if ($oItem instanceof \RainLoop\Plugins\Property) {
if (PluginPropertyType::PASSWORD === $oItem->Type()) {
$oItem->SetValue(static::APP_DUMMY);
} else {
$oItem->SetValue($oConfig->Get('plugin', $oItem->Name(), ''));
}
$mResult['config'][] = $oItem;
} else if ($oItem instanceof \RainLoop\Plugins\PropertyCollection) {
foreach ($oItem as $oSubItem) {
if ($oSubItem && $oSubItem instanceof \RainLoop\Plugins\Property) {
if (PluginPropertyType::PASSWORD === $oSubItem->Type()) {
$oSubItem->SetValue(static::APP_DUMMY);
} else {
$oSubItem->SetValue($oConfig->Get('plugin', $oSubItem->Name(), ''));
}
}
}
$mResult['config'][] = $oItem;
}
}
}
}
}
}
return $this->DefaultResponse($mResult);
}
public function DoAdminPluginSettingsUpdate() : array
{
$this->IsAdminLoggined();
$sId = (string) $this->GetActionParam('id', '');
if (!empty($sId)) {
$oPlugin = $this->Plugins()->CreatePluginByName($sId);
if ($oPlugin) {
$oConfig = $oPlugin->Config();
$aMap = $oPlugin->ConfigMap(true);
if (\is_array($aMap)) {
$aSettings = (array) $this->GetActionParam('settings', []);
foreach ($aMap as $oItem) {
$sKey = $oItem->Name();
$mValue = $aSettings[$sKey] ?? $oConfig->Get('plugin', $sKey);
if (PluginPropertyType::PASSWORD !== $oItem->Type() || static::APP_DUMMY !== $mValue) {
$oItem->SetValue($mValue);
$mValue = $oItem->Value();
if (null !== $mValue) {
if ($oItem->encrypted) {
$oConfig->setEncrypted('plugin', $sKey, $mValue);
} else {
$oConfig->Set('plugin', $sKey, $mValue);
}
}
}
}
}
if ($oConfig->Save()) {
return $this->TrueResponse();
}
}
}
throw new ClientException(Notifications::CantSavePluginSettings);
}
}

View file

@ -0,0 +1,175 @@
<?php
namespace RainLoop\Actions;
use RainLoop\Enumerations\Capa;
use RainLoop\Utils;
trait Attachments
{
/**
* @throws \MailSo\RuntimeException
*/
public function DoAttachmentsActions() : array
{
$sAction = $this->GetActionParam('target', '');
$sFolder = $this->GetActionParam('folder', '');
$sFilename = \MailSo\Base\Utils::SecureFileName($this->GetActionParam('filename', ''));
$aHashes = $this->GetActionParam('hashes', null);
$oFilesProvider = $this->FilesProvider();
if (empty($sAction) || !$this->GetCapa(Capa::ATTACHMENTS_ACTIONS) || !$oFilesProvider || !$oFilesProvider->IsActive()) {
return $this->FalseResponse();
}
$oAccount = $this->initMailClientConnection();
$bError = false;
$aData = [];
$mUIDs = [];
if (\is_array($aHashes) && \count($aHashes)) {
foreach ($aHashes as $sZipHash) {
$aResult = $this->getMimeFileByHash($oAccount, $sZipHash);
if (empty($aResult['fileHash'])) {
$bError = true;
break;
}
$aData[] = $aResult;
if (!empty($aResult['mimeIndex'])) {
$mUIDs[$aResult['uid']] = $aResult['uid'];
}
}
}
$mUIDs = 1 < \count($mUIDs);
if ($bError || !\count($aData)) {
return $this->FalseResponse();
}
$mResult = false;
switch (\strtolower($sAction))
{
case 'zip':
$sZipHash = \MailSo\Base\Utils::Sha1Rand();
$sZipFileName = $oFilesProvider->GenerateLocalFullFileName($oAccount, $sZipHash);
if (!empty($sZipFileName)) {
if (\class_exists('ZipArchive')) {
$oZip = new \ZipArchive();
$oZip->open($sZipFileName, \ZIPARCHIVE::CREATE | \ZIPARCHIVE::OVERWRITE);
$oZip->setArchiveComment('SnappyMail/'.APP_VERSION);
foreach ($aData as $aItem) {
$sFullFileNameHash = $oFilesProvider->GetFileName($oAccount, $aItem['fileHash']);
$sFileName = ($mUIDs ? "{$aItem['uid']}/" : ($sFolder ? "{$aItem['uid']}-" : '')) . $aItem['fileName'];
if (!$oZip->addFile($sFullFileNameHash, $sFileName)) {
$bError = true;
}
}
if ($bError) {
$oZip->close();
} else {
$bError = !$oZip->close();
}
/*
} else {
@\unlink($sZipFileName);
$oZip = new \SnappyMail\Stream\ZIP($sZipFileName);
// $oZip->setArchiveComment('SnappyMail/'.APP_VERSION);
foreach ($aData as $aItem) {
if ($aItem['fileHash']) {
$sFullFileNameHash = $oFilesProvider->GetFileName($oAccount, $aItem['fileHash']);
if (!$oZip->addFile($sFullFileNameHash, $aItem['fileName'])) {
$bError = true;
}
}
}
$oZip->close();
*/
} else {
@\unlink($sZipFileName);
$oZip = new \PharData($sZipFileName . '.zip', 0, null, \Phar::ZIP);
$oZip->compressFiles(\Phar::GZ);
foreach ($aData as $aItem) {
$oZip->addFile(
$oFilesProvider->GetFileName($oAccount, $aItem['fileHash']),
($mUIDs ? "{$aItem['uid']}/" : ($sFolder ? "{$aItem['uid']}-" : '')) . $aItem['fileName']
);
}
$oZip->compressFiles(\Phar::GZ);
unset($oZip);
\rename($sZipFileName . '.zip', $sZipFileName);
}
foreach ($aData as $aItem) {
$oFilesProvider->Clear($oAccount, $aItem['fileHash']);
}
if (!$bError) {
$mResult = array(
'fileHash' => $this->encodeRawKey(array(
'fileName' => ($sFilename ?: ($sFolder ? 'messages' : 'attachments')) . \date('-YmdHis') . '.zip',
'mimeType' => 'application/zip',
'fileHash' => $sZipHash
))
);
}
}
break;
default:
$data = new \SnappyMail\AttachmentsAction;
$data->action = $sAction;
$data->items = $aData;
$data->filesProvider = $oFilesProvider;
$data->account = $oAccount;
$this->Plugins()->RunHook('json.attachments', array($data));
$mResult = $data->result;
break;
}
return $this->DefaultResponse($bError ? false : $mResult);
}
private function getMimeFileByHash(\RainLoop\Model\Account $oAccount, string $sHash) : array
{
$aValues = $this->decodeRawKey($sHash);
$sFolder = isset($aValues['folder']) ? (string) $aValues['folder'] : '';
$iUid = isset($aValues['uid']) ? (int) $aValues['uid'] : 0;
$sMimeIndex = isset($aValues['mimeIndex']) ? (string) $aValues['mimeIndex'] : '';
$sContentTypeIn = isset($aValues['mimeType']) ? (string) $aValues['mimeType'] : '';
$sFileNameIn = isset($aValues['fileName']) ? (string) $aValues['fileName'] : 'file.dat';
$oFileProvider = $this->FilesProvider();
$sResultHash = '';
$mResult = $this->MailClient()->MessageMimeStream(function ($rResource, $sContentType, $sFileName, $sMimeIndex = '')
use ($oAccount, $oFileProvider, $sFileNameIn, $sContentTypeIn, &$sResultHash) {
unset($sContentType, $sFileName, $sMimeIndex);
if (\is_resource($rResource)) {
$sHash = \MailSo\Base\Utils::Sha1Rand($sFileNameIn.'~'.$sContentTypeIn);
$rTempResource = $oFileProvider->GetFile($oAccount, $sHash, 'wb+');
if (\is_resource($rTempResource)) {
if (-1 < \MailSo\Base\Utils::WriteStream($rResource, $rTempResource)) {
$sResultHash = $sHash;
}
\fclose($rTempResource);
}
}
}, $sFolder, $iUid, $sMimeIndex);
$aValues['fileName'] = $sFileNameIn;
$aValues['fileHash'] = $mResult ? $sResultHash : '';
return $aValues;
}
}

View file

@ -10,9 +10,8 @@ trait Contacts
$oAccount = $this->getAccountFromToken();
$oAddressBookProvider = $this->AddressBookProvider($oAccount);
if (!$oAddressBookProvider || !$oAddressBookProvider->IsActive())
{
return $this->FalseResponse(__FUNCTION__);
if (!$oAddressBookProvider || !$oAddressBookProvider->IsActive()) {
return $this->FalseResponse();
}
$sPassword = $this->GetActionParam('Password', '');
@ -22,12 +21,13 @@ trait Contacts
$bResult = $this->setContactsSyncData($oAccount, array(
'Mode' => \intval($this->GetActionParam('Mode', '0')),
'User' => $this->GetActionParam('User', ''),
'Password' => APP_DUMMY === $sPassword && isset($mData['Password'])
? $mData['Password'] : (APP_DUMMY === $sPassword ? '' : $sPassword),
'Password' => static::APP_DUMMY === $sPassword
? (isset($mData['Password']) ? $mData['Password'] : '')
: $sPassword,
'Url' => $this->GetActionParam('Url', '')
));
return $this->DefaultResponse(__FUNCTION__, $bResult);
return $this->DefaultResponse($bResult);
}
public function DoContactsSync() : array
@ -35,15 +35,15 @@ trait Contacts
$oAccount = $this->getAccountFromToken();
$oAddressBookProvider = $this->AddressBookProvider($oAccount);
if (!$oAddressBookProvider) {
throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::ContactsSyncError);
throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::ContactsSyncError, null, 'No AddressBookProvider');
}
\ignore_user_abort(true);
\SnappyMail\HTTP\Stream::start(/*$binary = false*/);
\SnappyMail\HTTP\Stream::JSON(['messsage'=>'start']);
if (!$oAddressBookProvider->Sync()) {
throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::ContactsSyncError);
throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::ContactsSyncError, null, 'AddressBookProvider->Sync() failed');
}
return $this->TrueResponse(__FUNCTION__);
return $this->TrueResponse();
}
public function DoContacts() : array
@ -60,13 +60,12 @@ trait Contacts
$mResult = array();
$oAbp = $this->AddressBookProvider($oAccount);
if ($oAbp->IsActive())
{
if ($oAbp->IsActive()) {
$iResultCount = 0;
$mResult = $oAbp->GetContacts($iOffset, $iLimit, $sSearch, $iResultCount);
}
return $this->DefaultResponse(__FUNCTION__, array(
return $this->DefaultResponse(array(
'Offset' => $iOffset,
'Limit' => $iLimit,
'Count' => $iResultCount,
@ -78,17 +77,16 @@ trait Contacts
public function DoContactsDelete() : array
{
$oAccount = $this->getAccountFromToken();
$aUids = \explode(',', (string) $this->GetActionParam('Uids', ''));
$aUids = \explode(',', (string) $this->GetActionParam('uids', ''));
$aFilteredUids = \array_filter(\array_map('intval', $aUids));
$bResult = false;
if (\count($aFilteredUids) && $this->AddressBookProvider($oAccount)->IsActive())
{
if (\count($aFilteredUids) && $this->AddressBookProvider($oAccount)->IsActive()) {
$bResult = $this->AddressBookProvider($oAccount)->DeleteContacts($aFilteredUids);
}
return $this->DefaultResponse(__FUNCTION__, $bResult);
return $this->DefaultResponse($bResult);
}
public function DoContactSave() : array
@ -97,14 +95,14 @@ trait Contacts
$bResult = false;
if ($this->HasActionParam('Uid') && $this->HasActionParam('jCard')) {
if ($this->HasActionParam('uid') && $this->HasActionParam('jCard')) {
$oAddressBookProvider = $this->AddressBookProvider($oAccount);
if ($oAddressBookProvider && $oAddressBookProvider->IsActive()) {
$vCard = \Sabre\VObject\Reader::readJson($this->GetActionParam('jCard'));
if ($vCard && $vCard instanceof \Sabre\VObject\Component\VCard) {
$vCard->REV = \gmdate('Ymd\\THis\\Z');
$vCard->PRODID = 'SnappyMail-'.APP_VERSION;
$sUid = \trim($this->GetActionParam('Uid'));
$sUid = \trim($this->GetActionParam('uid'));
$oContact = $sUid ? $oAddressBookProvider->GetContactByID($sUid) : null;
if (!$oContact) {
$oContact = new \RainLoop\Providers\AddressBook\Classes\Contact();
@ -115,75 +113,58 @@ trait Contacts
}
}
return $this->DefaultResponse(__FUNCTION__, array(
return $this->DefaultResponse(array(
'ResultID' => $bResult ? $oContact->id : '',
'Result' => $bResult
));
}
public function UploadContacts() : array
public function UploadContacts(?array $aFile, int $iError) : array
{
$oAccount = $this->getAccountFromToken();
$mResponse = false;
$aFile = $this->GetActionParam('File', null);
$iError = $this->GetActionParam('Error', \RainLoop\Enumerations\UploadError::UNKNOWN);
if ($oAccount && UPLOAD_ERR_OK === $iError && \is_array($aFile))
{
if ($oAccount && UPLOAD_ERR_OK === $iError && \is_array($aFile)) {
$sSavedName = 'upload-post-'.\md5($aFile['name'].$aFile['tmp_name']);
if (!$this->FilesProvider()->MoveUploadedFile($oAccount, $sSavedName, $aFile['tmp_name']))
{
if (!$this->FilesProvider()->MoveUploadedFile($oAccount, $sSavedName, $aFile['tmp_name'])) {
$iError = \RainLoop\Enumerations\UploadError::ON_SAVING;
}
else
{
\ini_set('auto_detect_line_endings', true);
} else {
\ini_set('auto_detect_line_endings', '1');
$mData = $this->FilesProvider()->GetFile($oAccount, $sSavedName);
if ($mData)
{
$sFileStart = \fread($mData, 20);
if ($mData) {
$sFileStart = \fread($mData, 128);
\rewind($mData);
if (false !== $sFileStart)
{
if (false !== $sFileStart) {
$sFileStart = \trim($sFileStart);
if (false !== \strpos($sFileStart, 'BEGIN:VCARD'))
{
if (false !== \strpos($sFileStart, 'BEGIN:VCARD')) {
$mResponse = $this->importContactsFromVcfFile($oAccount, $mData);
}
else if (false !== \strpos($sFileStart, ',') || false !== \strpos($sFileStart, ';'))
{
} else if (false !== \strpos($sFileStart, ',') || false !== \strpos($sFileStart, ';')) {
$mResponse = $this->importContactsFromCsvFile($oAccount, $mData, $sFileStart);
}
}
}
if (\is_resource($mData))
{
if (\is_resource($mData)) {
\fclose($mData);
}
unset($mData);
$this->FilesProvider()->Clear($oAccount, $sSavedName);
\ini_set('auto_detect_line_endings', false);
\ini_set('auto_detect_line_endings', '0');
}
}
if (UPLOAD_ERR_OK !== $iError)
{
$iClientError = \RainLoop\Enumerations\UploadError::NORMAL;
$sError = $this->getUploadErrorMessageByCode($iError, $iClientError);
if (!empty($sError))
{
return $this->FalseResponse(__FUNCTION__, $iClientError, $sError);
if (UPLOAD_ERR_OK !== $iError) {
$iClientError = 0;
$sError = \RainLoop\Enumerations\UploadError::getUserMessage($iError, $iClientError);
if (!empty($sError)) {
return $this->FalseResponse($iClientError, $sError);
}
}
return $this->DefaultResponse(__FUNCTION__, $mResponse);
return $this->DefaultResponse($mResponse);
}
public function setContactsSyncData(\RainLoop\Model\Account $oAccount, array $aData) : bool
@ -273,64 +254,45 @@ trait Contacts
private function importContactsFromVcfFile(\RainLoop\Model\Account $oAccount, /*resource*/ $rFile): int
{
$iCount = 0;
if ($oAccount && \is_resource($rFile)) {
$oAddressBookProvider = $this->AddressBookProvider($oAccount);
if ($oAddressBookProvider && $oAddressBookProvider->IsActive()) {
$sFile = \stream_get_contents($rFile);
if (\is_resource($rFile)) {
\fclose($rFile);
}
if (is_string($sFile) && 5 < \strlen($sFile)) {
$this->Logger()->Write('Import contacts from vcf');
$iCount = $oAddressBookProvider->ImportVcfFile($sFile);
$oAddressBookProvider = $this->AddressBookProvider($oAccount);
if (\is_resource($rFile) && $oAddressBookProvider && $oAddressBookProvider->IsActive()) {
try
{
$this->logWrite('Import contacts from vcf');
foreach (\RainLoop\Providers\AddressBook\Utils::VcfStreamToContacts($rFile) as $oContact) {
if ($oAddressBookProvider->ContactSave($oContact)) {
++$iCount;
}
}
}
catch (\Throwable $oExc)
{
$this->logException($oExc);
}
}
return $iCount;
}
private function importContactsFromCsvFile(\RainLoop\Model\Account $oAccount, /*resource*/ $rFile, string $sFileStart): int
{
$iCount = 0;
$aHeaders = null;
$aData = array();
if ($oAccount && \is_resource($rFile)) {
$oAddressBookProvider = $this->AddressBookProvider($oAccount);
if ($oAddressBookProvider && $oAddressBookProvider->IsActive()) {
$oAddressBookProvider = $this->AddressBookProvider($oAccount);
if (\is_resource($rFile) && $oAddressBookProvider && $oAddressBookProvider->IsActive()) {
try
{
$this->logWrite('Import contacts from csv');
$sDelimiter = ((int)\strpos($sFileStart, ',') > (int)\strpos($sFileStart, ';')) ? ',' : ';';
\setlocale(LC_CTYPE, 'en_US.UTF-8');
while (false !== ($mRow = \fgetcsv($rFile, 5000, $sDelimiter, '"'))) {
if (null === $aHeaders) {
if (3 >= \count($mRow)) {
return 0;
}
$aHeaders = $mRow;
foreach ($aHeaders as $iIndex => $sHeaderValue) {
$aHeaders[$iIndex] = \MailSo\Base\Utils::Utf8Clear($sHeaderValue);
}
} else {
$aNewItem = array();
foreach ($aHeaders as $iIndex => $sHeaderValue) {
$aNewItem[$sHeaderValue] = isset($mRow[$iIndex]) ? $mRow[$iIndex] : '';
}
$aData[] = $aNewItem;
foreach (\RainLoop\Providers\AddressBook\Utils::CsvStreamToContacts($rFile, $sDelimiter) as $oContact) {
if ($oAddressBookProvider->ContactSave($oContact)) {
++$iCount;
}
}
if (\count($aData)) {
$this->oLogger->Write('Import contacts from csv');
$iCount = $oAddressBookProvider->ImportCsvArray($aData);
}
}
catch (\Throwable $oExc)
{
$this->logException($oExc);
}
}
return $iCount;
}

View file

@ -6,10 +6,7 @@ use RainLoop\Enumerations\Capa;
trait Filters
{
/**
* @var \RainLoop\Providers\Filters
*/
private $oFiltersProvider;
private ?\RainLoop\Providers\Filters $oFiltersProvider = null;
/**
* @throws \MailSo\RuntimeException
@ -18,12 +15,11 @@ trait Filters
{
$oAccount = $this->getAccountFromToken();
if (!$this->GetCapa(Capa::SIEVE, $oAccount))
{
return $this->FalseResponse(__FUNCTION__);
if (!$this->GetCapa(Capa::SIEVE, $oAccount)) {
return $this->FalseResponse();
}
return $this->DefaultResponse(__FUNCTION__, $this->FiltersProvider()->Load($oAccount));
return $this->DefaultResponse($this->FiltersProvider()->Load($oAccount));
}
/**
@ -34,7 +30,7 @@ trait Filters
$oAccount = $this->getAccountFromToken();
if (!$this->GetCapa(Capa::SIEVE, $oAccount)) {
return $this->FalseResponse(__FUNCTION__);
return $this->FalseResponse();
}
$sName = $this->GetActionParam('name', '');
@ -43,7 +39,7 @@ trait Filters
// $this->FiltersProvider()->ActivateScript($oAccount, $sName);
}
return $this->DefaultResponse(__FUNCTION__, $this->FiltersProvider()->Save(
return $this->DefaultResponse($this->FiltersProvider()->Save(
$oAccount, $sName, $this->GetActionParam('body', '')
));
}
@ -56,10 +52,10 @@ trait Filters
$oAccount = $this->getAccountFromToken();
if (!$this->GetCapa(Capa::SIEVE, $oAccount)) {
return $this->FalseResponse(__FUNCTION__);
return $this->FalseResponse();
}
return $this->DefaultResponse(__FUNCTION__, $this->FiltersProvider()->ActivateScript(
return $this->DefaultResponse($this->FiltersProvider()->ActivateScript(
$oAccount, $this->GetActionParam('name', '')
));
}
@ -72,10 +68,10 @@ trait Filters
$oAccount = $this->getAccountFromToken();
if (!$this->GetCapa(Capa::SIEVE, $oAccount)) {
return $this->FalseResponse(__FUNCTION__);
return $this->FalseResponse();
}
return $this->DefaultResponse(__FUNCTION__, $this->FiltersProvider()->DeleteScript(
return $this->DefaultResponse($this->FiltersProvider()->DeleteScript(
$oAccount, $this->GetActionParam('name', '')
));
}

View file

@ -10,44 +10,39 @@ use MailSo\Imap\Enumerations\FolderType;
trait Folders
{
private function getFolderCollection(bool $HideUnsubscribed) : ?\MailSo\Mail\FolderCollection
{
return $this->MailClient()->Folders('', '*',
$HideUnsubscribed,
(int) $this->Config()->Get('imap', 'folder_list_limit', 200),
(bool) $this->Config()->Get('imap', 'use_list_status', true)
);
}
/**
* Appends uploaded rfc822 message to mailbox
* @throws \MailSo\RuntimeException
*/
public function Append(): bool
public function DoFolderAppend(): array
{
$oAccount = $this->initMailClientConnection();
$sFolderFullName = $this->GetActionParam('Folder', '');
$sFolderFullName = $this->GetActionParam('folder', '');
if ($oAccount
&& !empty($sFolderFullName)
&& !empty($_FILES['AppendFile'])
&& \is_uploaded_file($_FILES['AppendFile']['tmp_name'])
&& \UPLOAD_ERR_OK == $_FILES['AppendFile']['error']
&& $this->oConfig->Get('labs', 'allow_message_append', false)
) {
$sSavedName = 'append-post-' . \md5($sFolderFullName . $_FILES['AppendFile']['name'] . $_FILES['AppendFile']['tmp_name']);
if ($this->FilesProvider()->MoveUploadedFile($oAccount, $sSavedName, $_FILES['AppendFile']['tmp_name'])) {
$iMessageStreamSize = $this->FilesProvider()->FileSize($oAccount, $sSavedName);
$rMessageStream = $this->FilesProvider()->GetFile($oAccount, $sSavedName);
$this->MailClient()->MessageAppendStream($rMessageStream, $iMessageStreamSize, $sFolderFullName);
$this->FilesProvider()->Clear($oAccount, $sSavedName);
}
if (!$this->oConfig->Get('labs', 'allow_message_append', false)) {
return $this->FalseResponse(999, 'Permission denied');
}
return $this->DefaultResponse(__FUNCTION__, true);
if (empty($_FILES['appendFile'])) {
return $this->FalseResponse(999, 'No file');
}
if (\UPLOAD_ERR_OK != $_FILES['appendFile']['error']) {
return $this->FalseResponse($iErrorCode, \RainLoop\Enumerations\UploadError::getMessage($iErrorCode));
}
if ($oAccount && !empty($sFolderFullName) && \is_uploaded_file($_FILES['appendFile']['tmp_name'])) {
$sSavedName = 'append-post-' . \md5($sFolderFullName . $_FILES['appendFile']['name'] . $_FILES['appendFile']['tmp_name']);
if ($this->FilesProvider()->MoveUploadedFile($oAccount, $sSavedName, $_FILES['appendFile']['tmp_name'])) {
$iMessageStreamSize = $this->FilesProvider()->FileSize($oAccount, $sSavedName);
$rMessageStream = $this->FilesProvider()->GetFile($oAccount, $sSavedName);
$this->ImapClient()->MessageAppendStream($sFolderFullName, $rMessageStream, $iMessageStreamSize);
$this->FilesProvider()->Clear($oAccount, $sSavedName);
return $this->TrueResponse();
}
}
return $this->FalseResponse(999);
}
public function DoFolders() : array
@ -60,159 +55,35 @@ trait Folders
$HideUnsubscribed = (bool) $oSettingsLocal->GetConf('HideUnsubscribed', $HideUnsubscribed);
}
$oFolderCollection = $this->getFolderCollection($HideUnsubscribed);
$oFolderCollection = $this->MailClient()->Folders('', '*', $HideUnsubscribed);
if ($oFolderCollection)
{
$sNamespace = $this->MailClient()->GetPersonalNamespace();
$this->Plugins()->RunHook('filter.folders-post', array($oAccount, $oFolderCollection));
$aSystemFolders = array();
$this->recFoldersTypes($oAccount, $oFolderCollection, $aSystemFolders);
if (!$this->Config()->Get('imap', 'use_sort', true)) {
$oFolderCollection->capabilities = \array_filter($oFolderCollection->capabilities, function($item){
return !\preg_match('/^E?SORT/', $item);
});
}
if ($this->Config()->Get('labs', 'autocreate_system_folders', false))
{
$bDoItAgain = false;
$sParent = \substr($sNamespace, 0, -1);
$sDelimiter = $oFolderCollection->FindDelimiter();
$aList = array();
$aMap = $this->systemFoldersNames($oAccount);
if ('' === $oSettingsLocal->GetConf('SentFolder', ''))
{
$aList[] = FolderType::SENT;
}
if ('' === $oSettingsLocal->GetConf('DraftFolder', ''))
{
$aList[] = FolderType::DRAFTS;
}
if ('' === $oSettingsLocal->GetConf('SpamFolder', ''))
{
$aList[] = FolderType::JUNK;
}
if ('' === $oSettingsLocal->GetConf('TrashFolder', ''))
{
$aList[] = FolderType::TRASH;
}
if ('' === $oSettingsLocal->GetConf('ArchiveFolder', ''))
{
$aList[] = FolderType::ARCHIVE;
}
$this->Plugins()->RunHook('filter.folders-system-types', array($oAccount, &$aList));
foreach ($aList as $iType)
{
if (!isset($aSystemFolders[$iType]))
{
$mFolderNameToCreate = \array_search($iType, $aMap);
if (!empty($mFolderNameToCreate))
{
$iPos = \strrpos($mFolderNameToCreate, $sDelimiter);
if (false !== $iPos)
{
$mNewParent = \substr($mFolderNameToCreate, 0, $iPos);
$mNewFolderNameToCreate = \substr($mFolderNameToCreate, $iPos + 1);
if (\strlen($mNewFolderNameToCreate))
{
$mFolderNameToCreate = $mNewFolderNameToCreate;
}
if (\strlen($mNewParent))
{
$sParent = \strlen($sParent) ? $sParent.$sDelimiter.$mNewParent : $mNewParent;
}
}
$sFullNameToCheck = $mFolderNameToCreate;
if (\strlen($sParent))
{
$sFullNameToCheck = $sParent.$sDelimiter.$sFullNameToCheck;
}
if (!isset($oFolderCollection[$sFullNameToCheck]))
{
try
{
$this->MailClient()->FolderCreate($mFolderNameToCreate, $sParent, true, $sDelimiter);
$bDoItAgain = true;
}
catch (\Throwable $oException)
{
$this->Logger()->WriteException($oException);
}
}
}
}
}
if ($bDoItAgain)
{
$oFolderCollection = $this->getFolderCollection($HideUnsubscribed);
if ($oFolderCollection)
{
$aSystemFolders = array();
$this->recFoldersTypes($oAccount, $oFolderCollection, $aSystemFolders);
}
if ($oFolderCollection) {
$aQuota = null;
if ($this->GetCapa(Capa::QUOTA)) {
try {
// $aQuota = $this->ImapClient()->Quota();
$aQuota = $this->ImapClient()->QuotaRoot();
} catch (\Throwable $oException) {
// ignore
}
}
if ($oFolderCollection)
{
$this->Plugins()->RunHook('filter.folders-complete', array($oAccount, $oFolderCollection));
$aCapabilities = \array_values(\array_filter($this->ImapClient()->Capability(), function ($item) {
return !\preg_match('/^(IMAP|AUTH|LOGIN|SASL)/', $item);
}));
$aQuota = null;
if ($this->GetCapa(Capa::QUOTA)) {
try {
// $aQuota = $this->MailClient()->Quota();
$aQuota = $this->MailClient()->QuotaRoot();
} catch (\Throwable $oException) {
// ignore
}
}
$aCapabilities = \array_filter($this->MailClient()->Capability(), function($item){
return !\preg_match('/^(IMAP|AUTH|LOGIN|SASL)/', $item);
});
if (!$this->Config()->Get('imap', 'use_list_status', true)) {
$key = \array_search('LIST-STATUS', $aCapabilities);
if (false !== $key) {
unset($aCapabilities[$key]);
}
}
$oFolderCollection = \array_merge(
$oFolderCollection->jsonSerialize(),
array(
'quotaUsage' => $aQuota ? $aQuota[0] * 1024 : null,
'quotaLimit' => $aQuota ? $aQuota[1] * 1024 : null,
'Namespace' => $sNamespace,
'IsThreadsSupported' => $this->MailClient()->IsThreadsSupported(),
'Optimized' => $oFolderCollection->Optimized,
'CountRec' => $oFolderCollection->TotalCount,
'SystemFolders' => empty($aSystemFolders) ? null : $aSystemFolders,
'Capabilities' => \array_values($aCapabilities)
)
);
}
$oFolderCollection = \array_merge(
$oFolderCollection->jsonSerialize(),
array(
'quotaUsage' => $aQuota ? $aQuota[0] * 1024 : null,
'quotaLimit' => $aQuota ? $aQuota[1] * 1024 : null,
'namespace' => $this->ImapClient()->GetPersonalNamespace(),
'capabilities' => $aCapabilities
)
);
}
return $this->DefaultResponse(__FUNCTION__, $oFolderCollection);
return $this->DefaultResponse($oFolderCollection);
}
public function DoFolderCreate() : array
@ -222,13 +93,13 @@ trait Folders
try
{
$oFolder = $this->MailClient()->FolderCreate(
$this->GetActionParam('Folder', ''),
$this->GetActionParam('Parent', ''),
!!$this->GetActionParam('Subscribe', 1)
$this->GetActionParam('folder', ''),
$this->GetActionParam('parent', ''),
!empty($this->GetActionParam('subscribe', 0))
);
// FolderInformation(string $sFolderName, int $iPrevUidNext = 0, array $aUids = array())
return $this->DefaultResponse(__FUNCTION__, $oFolder);
return $this->DefaultResponse($oFolder);
}
catch (\Throwable $oException)
{
@ -239,82 +110,60 @@ trait Folders
public function DoFolderSetMetadata() : array
{
$this->initMailClientConnection();
$sFolderFullName = $this->GetActionParam('Folder');
$sMetadataKey = $this->GetActionParam('Key');
$sFolderFullName = $this->GetActionParam('folder');
$sMetadataKey = $this->GetActionParam('key');
if ($sFolderFullName && $sMetadataKey) {
$this->MailClient()->FolderSetMetadata($sFolderFullName, [
$sMetadataKey => $this->GetActionParam('Value') ?: null
$this->ImapClient()->FolderSetMetadata($sFolderFullName, [
$sMetadataKey => $this->GetActionParam('value') ?: null
]);
}
return $this->TrueResponse(__FUNCTION__);
return $this->TrueResponse();
}
public function DoFolderSubscribe() : array
{
$this->initMailClientConnection();
$sFolderFullName = $this->GetActionParam('Folder', '');
$bSubscribe = '1' === (string) $this->GetActionParam('Subscribe', '0');
$sFolderFullName = $this->GetActionParam('folder', '');
$bSubscribe = !empty($this->GetActionParam('subscribe', 0));
try
{
$this->MailClient()->FolderSubscribe($sFolderFullName, $bSubscribe);
$this->ImapClient()->{$bSubscribe ? 'FolderSubscribe' : 'FolderUnsubscribe'}($sFolderFullName);
}
catch (\Throwable $oException)
{
if ($bSubscribe)
{
throw new ClientException(Notifications::CantSubscribeFolder, $oException);
}
else
{
throw new ClientException(Notifications::CantUnsubscribeFolder, $oException);
}
throw new ClientException(
$bSubscribe ? Notifications::CantSubscribeFolder : Notifications::CantUnsubscribeFolder,
$oException
);
}
return $this->TrueResponse(__FUNCTION__);
return $this->TrueResponse();
}
public function DoFolderCheckable() : array
{
$oAccount = $this->getAccountFromToken();
$sFolderFullName = $this->GetActionParam('Folder', '');
$bCheckable = '1' === (string) $this->GetActionParam('Checkable', '0');
$sFolderFullName = $this->GetActionParam('folder', '');
$oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount);
$sCheckableFolder = $oSettingsLocal->GetConf('CheckableFolder', '[]');
$aCheckableFolder = \json_decode($sCheckableFolder);
if (!\is_array($aCheckableFolder))
{
$aCheckableFolder = \json_decode($oSettingsLocal->GetConf('CheckableFolder', '[]'));
if (!\is_array($aCheckableFolder)) {
$aCheckableFolder = array();
}
if ($bCheckable)
{
if (!empty($this->GetActionParam('checkable', '0'))) {
$aCheckableFolder[] = $sFolderFullName;
}
else
{
$aCheckableFolderNew = array();
foreach ($aCheckableFolder as $sFolder)
{
if ($sFolder !== $sFolderFullName)
{
$aCheckableFolderNew[] = $sFolder;
}
}
$aCheckableFolder = $aCheckableFolderNew;
} else if (($key = \array_search($sFolderFullName, $aCheckableFolder)) !== false) {
\array_splice($aCheckableFolder, $key, 1);
}
$aCheckableFolder = \array_unique($aCheckableFolder);
$oSettingsLocal->SetConf('CheckableFolder', \json_encode(\array_unique($aCheckableFolder)));
$oSettingsLocal->SetConf('CheckableFolder', \json_encode($aCheckableFolder));
return $this->DefaultResponse(__FUNCTION__,
$this->SettingsProvider(true)->Save($oAccount, $oSettingsLocal));
return $this->DefaultResponse($this->SettingsProvider(true)->Save($oAccount, $oSettingsLocal));
}
/**
@ -327,9 +176,9 @@ trait Folders
try
{
$this->MailClient()->FolderMove(
$this->GetActionParam('Folder', ''),
$this->GetActionParam('NewFolder', ''),
!!$this->GetActionParam('Subscribe', 1)
$this->GetActionParam('folder', ''),
$this->GetActionParam('newFolder', ''),
!empty($this->GetActionParam('subscribe', 1))
);
}
catch (\Throwable $oException)
@ -337,7 +186,7 @@ trait Folders
throw new ClientException(Notifications::CantRenameFolder, $oException);
}
return $this->TrueResponse(__FUNCTION__);
return $this->TrueResponse();
}
/**
@ -347,13 +196,13 @@ trait Folders
{
$this->initMailClientConnection();
$sName = $this->GetActionParam('NewFolderName', '');
$sName = $this->GetActionParam('newFolderName', '');
try
{
$sFullName = $this->MailClient()->FolderRename(
$this->GetActionParam('Folder', ''),
$this->GetActionParam('folder', ''),
$sName,
!!$this->GetActionParam('Subscribe', 1)
!empty($this->GetActionParam('subscribe', 1))
);
}
catch (\Throwable $oException)
@ -362,9 +211,9 @@ trait Folders
}
// FolderInformation(string $sFolderName, int $iPrevUidNext = 0, array $aUids = array())
return $this->DefaultResponse(__FUNCTION__, array(
'Name' => $sName,
'FullName' => $sFullName,
return $this->DefaultResponse(array(
'name' => $sName,
'fullName' => $sFullName,
));
}
@ -377,7 +226,7 @@ trait Folders
try
{
$this->MailClient()->FolderDelete($this->GetActionParam('Folder', ''));
$this->ImapClient()->FolderDelete($this->GetActionParam('folder', ''));
}
catch (\MailSo\Mail\Exceptions\NonEmptyFolder $oException)
{
@ -388,7 +237,7 @@ trait Folders
throw new ClientException(Notifications::CantDeleteFolder, $oException);
}
return $this->TrueResponse(__FUNCTION__);
return $this->TrueResponse();
}
/**
@ -400,14 +249,14 @@ trait Folders
try
{
$this->MailClient()->FolderClear($this->GetActionParam('Folder', ''));
$this->ImapClient()->FolderClear($this->GetActionParam('folder', ''));
}
catch (\Throwable $oException)
{
throw new ClientException(Notifications::MailServerError, $oException);
}
return $this->TrueResponse(__FUNCTION__);
return $this->TrueResponse();
}
/**
@ -419,12 +268,11 @@ trait Folders
try
{
$aInboxInformation = $this->MailClient()->FolderInformation(
$this->GetActionParam('Folder', ''),
(int) $this->GetActionParam('UidNext', 0),
new \MailSo\Imap\SequenceSet($this->GetActionParam('FlagsUids', []))
);
return $this->DefaultResponse(__FUNCTION__, $aInboxInformation);
return $this->DefaultResponse($this->MailClient()->FolderInformation(
$this->GetActionParam('folder', ''),
(int) $this->GetActionParam('uidNext', 0),
new \MailSo\Imap\SequenceSet($this->GetActionParam('flagsUids', []))
));
}
catch (\Throwable $oException)
{
@ -439,38 +287,24 @@ trait Folders
{
$aResult = array();
$aFolders = $this->GetActionParam('Folders', null);
if (\is_array($aFolders))
{
$aFolders = $this->GetActionParam('folders', null);
if (\is_array($aFolders)) {
$this->initMailClientConnection();
$aFolders = \array_unique($aFolders);
foreach ($aFolders as $sFolder)
{
if (\strlen($sFolder) && 'INBOX' !== \strtoupper($sFolder))
foreach ($aFolders as $sFolder) {
try
{
try
{
$aInboxInformation = $this->MailClient()->FolderInformation($sFolder);
if (isset($aInboxInformation['Folder']))
{
$aResult[] = [
'Folder' => $aInboxInformation['Folder'],
'Hash' => $aInboxInformation['Hash'],
'totalEmails' => $aInboxInformation['totalEmails'],
'unreadEmails' => $aInboxInformation['unreadEmails'],
];
}
}
catch (\Throwable $oException)
{
$this->Logger()->WriteException($oException);
}
$aResult[] = $this->MailClient()->FolderInformation($sFolder);
}
catch (\Throwable $oException)
{
$this->logException($oException);
}
}
}
return $this->DefaultResponse(__FUNCTION__, $aResult);
return $this->DefaultResponse($aResult);
}
public function DoSystemFoldersUpdate() : array
@ -479,132 +313,12 @@ trait Folders
$oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount);
$oSettingsLocal->SetConf('SentFolder', $this->GetActionParam('Sent', ''));
$oSettingsLocal->SetConf('DraftFolder', $this->GetActionParam('Drafts', ''));
$oSettingsLocal->SetConf('SpamFolder', $this->GetActionParam('Spam', ''));
$oSettingsLocal->SetConf('TrashFolder', $this->GetActionParam('Trash', ''));
$oSettingsLocal->SetConf('ArchiveFolder', $this->GetActionParam('Archive', ''));
$oSettingsLocal->SetConf('SentFolder', $this->GetActionParam('sent', ''));
$oSettingsLocal->SetConf('DraftsFolder', $this->GetActionParam('drafts', ''));
$oSettingsLocal->SetConf('JunkFolder', $this->GetActionParam('junk', ''));
$oSettingsLocal->SetConf('TrashFolder', $this->GetActionParam('trash', ''));
$oSettingsLocal->SetConf('ArchiveFolder', $this->GetActionParam('archive', ''));
return $this->DefaultResponse(__FUNCTION__,
$this->SettingsProvider(true)->Save($oAccount, $oSettingsLocal));
}
private function recFoldersTypes(\RainLoop\Model\Account $oAccount, \MailSo\Mail\FolderCollection $oFolders, array &$aResult, bool $bListFolderTypes = true) : void
{
if ($oFolders->Count())
{
if ($bListFolderTypes)
{
foreach ($oFolders as $oFolder)
{
$iFolderType = $oFolder->GetFolderListType();
if (!isset($aResult[$iFolderType]) && \in_array($iFolderType, array(
FolderType::INBOX,
FolderType::SENT,
FolderType::DRAFTS,
FolderType::JUNK,
FolderType::TRASH,
FolderType::ARCHIVE
)))
{
$aResult[$iFolderType] = $oFolder->FullName();
}
}
}
$aMap = $this->systemFoldersNames($oAccount);
foreach ($oFolders as $oFolder)
{
$sName = $oFolder->Name();
$sFullName = $oFolder->FullName();
if (isset($aMap[$sName]) || isset($aMap[$sFullName]))
{
$iFolderType = isset($aMap[$sName]) ? $aMap[$sName] : $aMap[$sFullName];
if ((!isset($aResult[$iFolderType]) || $sName === $sFullName || "INBOX{$oFolder->Delimiter()}{$sName}" === $sFullName) && \in_array($iFolderType, array(
FolderType::INBOX,
FolderType::SENT,
FolderType::DRAFTS,
FolderType::JUNK,
FolderType::TRASH,
FolderType::ARCHIVE
)))
{
$aResult[$iFolderType] = $oFolder->FullName();
}
}
}
}
}
/**
* @staticvar array $aCache
*/
private function systemFoldersNames(\RainLoop\Model\Account $oAccount) : array
{
static $aCache = null;
if (null === $aCache)
{
$aCache = array(
'Sent' => FolderType::SENT,
'Send' => FolderType::SENT,
'Outbox' => FolderType::SENT,
'Out box' => FolderType::SENT,
'Sent Item' => FolderType::SENT,
'Sent Items' => FolderType::SENT,
'Send Item' => FolderType::SENT,
'Send Items' => FolderType::SENT,
'Sent Mail' => FolderType::SENT,
'Sent Mails' => FolderType::SENT,
'Send Mail' => FolderType::SENT,
'Send Mails' => FolderType::SENT,
'Drafts' => FolderType::DRAFTS,
'Draft' => FolderType::DRAFTS,
'Draft Mail' => FolderType::DRAFTS,
'Draft Mails' => FolderType::DRAFTS,
'Drafts Mail' => FolderType::DRAFTS,
'Drafts Mails' => FolderType::DRAFTS,
'Junk E-mail' => FolderType::JUNK,
'Spam' => FolderType::JUNK,
'Spams' => FolderType::JUNK,
'Junk' => FolderType::JUNK,
'Bulk Mail' => FolderType::JUNK,
'Bulk Mails' => FolderType::JUNK,
'Deleted Items' => FolderType::TRASH,
'Trash' => FolderType::TRASH,
'Deleted' => FolderType::TRASH,
'Bin' => FolderType::TRASH,
'Archive' => FolderType::ARCHIVE,
'Archives' => FolderType::ARCHIVE,
'All' => FolderType::ALL,
'All Mail' => FolderType::ALL,
'All Mails' => FolderType::ALL,
);
$aNewCache = array();
foreach ($aCache as $sKey => $iType)
{
$aNewCache[$sKey] = $iType;
$aNewCache[\str_replace(' ', '', $sKey)] = $iType;
}
$aCache = $aNewCache;
$this->Plugins()->RunHook('filter.system-folders-names', array($oAccount, &$aCache));
}
return $aCache;
return $this->DefaultResponse($this->SettingsProvider(true)->Save($oAccount, $oSettingsLocal));
}
}

View file

@ -14,7 +14,7 @@ trait Localization
if ($oAccount = $this->getAccountFromToken(false)) {
if ($oConfig->Get('webmail', 'allow_languages_on_settings', true)
&& ($oSettings = $this->SettingsProvider()->Load($oAccount))) {
$sLanguage = $oSettings->GetConf('Language', $sLanguage);
$sLanguage = $oSettings->GetConf('language', $sLanguage);
}
} else if ($oConfig->Get('login', 'allow_languages_on_login', true) && $oConfig->Get('login', 'determine_user_language', true)) {
$sLanguage = $this->ValidateLanguage($this->detectUserLanguage($bAdmin), $sLanguage, false);
@ -69,7 +69,7 @@ trait Localization
private function getUserLanguagesFromHeader(): array
{
$aResult = $aList = array();
$sAcceptLang = \strtolower($this->Http()->GetServer('HTTP_ACCEPT_LANGUAGE', 'en'));
$sAcceptLang = \strtolower(\MailSo\Base\Http::GetServer('HTTP_ACCEPT_LANGUAGE', 'en'));
if (!empty($sAcceptLang) && \preg_match_all('/([a-z]{1,8}(?:-[a-z]{1,8})?)(?:;q=([0-9.]+))?/', $sAcceptLang, $aList)) {
$aResult = \array_combine($aList[1], $aList[2]);
foreach ($aResult as $n => $v) {

View file

@ -2,6 +2,8 @@
namespace RainLoop\Actions;
use SnappyMail\PGP\Backup;
trait Pgp
{
/**
@ -74,15 +76,15 @@ trait Pgp
{
$GPG = $this->GnuPG();
if (!$GPG) {
return $this->FalseResponse(__FUNCTION__);
return $this->FalseResponse();
}
$GPG->addDecryptKey(
$this->GetActionParam('KeyId', ''),
$this->GetActionParam('Passphrase', '')
$this->GetActionParam('keyId', ''),
$this->GetActionParam('passphrase', '')
);
$sData = $this->GetActionParam('Data', '');
$sData = $this->GetActionParam('data', '');
$oPart = null;
$result = [
'data' => '',
@ -102,9 +104,9 @@ trait Pgp
// $oPart = \MailSo\Mime\Part::FromStream($rStreamHandle);
}
},
$this->GetActionParam('Folder', ''),
(int) $this->GetActionParam('Uid', ''),
$this->GetActionParam('PartId', '')
$this->GetActionParam('folder', ''),
(int) $this->GetActionParam('uid', ''),
$this->GetActionParam('partId', '')
);
}
@ -113,21 +115,21 @@ trait Pgp
// $result['signatures'] = $oPart->SubParts[0];
}
return $this->DefaultResponse(__FUNCTION__, $result);
return $this->DefaultResponse($result);
}
public function DoGnupgGetKeys() : array
{
$GPG = $this->GnuPG();
return $this->DefaultResponse(__FUNCTION__, $GPG ? $GPG->keyInfo('') : false);
return $this->DefaultResponse($GPG ? $GPG->keyInfo('') : false);
}
public function DoGnupgExportKey() : array
{
$GPG = $this->GnuPG();
return $this->DefaultResponse(__FUNCTION__, $GPG ? $GPG->export(
$this->GetActionParam('KeyId', ''),
$this->GetActionParam('Passphrase', '')
return $this->DefaultResponse($GPG ? $GPG->export(
$this->GetActionParam('keyId', ''),
$this->GetActionParam('passphrase', '')
) : false);
}
@ -136,29 +138,29 @@ trait Pgp
$fingerprint = false;
$GPG = $this->GnuPG();
if ($GPG) {
$sName = $this->GetActionParam('Name', '');
$sEmail = $this->GetActionParam('Email', '');
$sName = $this->GetActionParam('name', '');
$sEmail = $this->GetActionParam('email', '');
$fingerprint = $GPG->generateKey(
$sName ? "{$sName} <{$sEmail}>" : $sEmail,
$this->GetActionParam('Passphrase', '')
$this->GetActionParam('passphrase', '')
);
}
return $this->DefaultResponse(__FUNCTION__, $fingerprint);
return $this->DefaultResponse($fingerprint);
}
public function DoGnupgDeleteKey() : array
{
$GPG = $this->GnuPG();
$sKeyId = $this->GetActionParam('KeyId', '');
$sKeyId = $this->GetActionParam('keyId', '');
$bPrivate = !!$this->GetActionParam('isPrivate', 0);
return $this->DefaultResponse(__FUNCTION__, $GPG ? $GPG->deleteKey($sKeyId, $bPrivate) : false);
return $this->DefaultResponse($GPG ? $GPG->deleteKey($sKeyId, $bPrivate) : false);
}
public function DoGnupgImportKey() : array
{
$sKey = $this->GetActionParam('Key', '');
$sKeyId = $this->GetActionParam('KeyId', '');
$sEmail = $this->GetActionParam('Email', '');
$sKey = $this->GetActionParam('key', '');
$sKeyId = $this->GetActionParam('keyId', '');
$sEmail = $this->GetActionParam('email', '');
if (!$sKey) {
try {
@ -182,7 +184,7 @@ trait Pgp
}
$GPG = $sKey ? $this->GnuPG() : null;
return $this->DefaultResponse(__FUNCTION__, $GPG ? $GPG->import($sKey) : false);
return $this->DefaultResponse($GPG ? $GPG->import($sKey) : false);
}
/**
@ -191,35 +193,7 @@ trait Pgp
*/
public function DoGetStoredPGPKeys() : array
{
$oAccount = $this->getMainAccountFromToken();
if (!$oAccount) {
return null;
}
$dir = $this->StorageProvider()->GenerateFilePath(
$oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::PGP
);
$keys = [];
foreach (\glob("{$dir}/*") as $file) {
if (\is_file($file)) {
if ('.asc' === \substr($file, -4)) {
$keys[] = \file_get_contents($file);
} else if ('.key' === \substr($file, -4)) {
$key = \json_decode(\file_get_contents($file), true);
$mac = \array_pop($key);
$hash = $oAccount->CryptKey();
if ($mac === \hash_hmac('sha1', $key[2], $hash)) {
$key[1] = \base64_decode($key[1]);
$key[2] = \base64_decode($key[2]);
$keys[] = \SnappyMail\Crypt::Decrypt($key, $hash);
}
}
}
}
return $this->DefaultResponse(__FUNCTION__, $keys);
return $this->DefaultResponse(\SnappyMail\PGP\Backup::getKeys());
}
/**
@ -232,16 +206,16 @@ trait Pgp
$privateKey = $this->GetActionParam('privateKey', '');
$result = [
'onServer' => [false, false, false],
'inGnuPG' => [false, false, false]
'onServer' => [false, false],
'inGnuPG' => [false, false]
];
$onServer = (int) $this->GetActionParam('onServer', 0);
if ($publicKey && $onServer & 1) {
$result['onServer'][0] = $this->StorePGPKey($publicKey);
$result['onServer'][0] = Backup::PGPKey($publicKey);
}
if ($privateKey && $onServer & 2) {
$result['onServer'][1] = $this->StorePGPKey($privateKey);
$result['onServer'][1] = Backup::PGPKey($privateKey);
}
$inGnuPG = (int) $this->GetActionParam('inGnuPG', 0);
@ -256,7 +230,7 @@ trait Pgp
}
// $revocationCertificate = $this->GetActionParam('revocationCertificate', '');
return $this->DefaultResponse(__FUNCTION__, $result);
return $this->DefaultResponse($result);
}
/**
@ -265,34 +239,8 @@ trait Pgp
*/
public function DoStorePGPKey() : array
{
$key = $this->GetActionParam('Key', '');
$keyId = $this->GetActionParam('KeyId', '');
return $this->DefaultResponse(__FUNCTION__, ($key && $keyId && $this->StorePGPKey($key, $keyId)));
$key = $this->GetActionParam('key', '');
$keyId = $this->GetActionParam('keyId', '');
return $this->DefaultResponse(($key && $keyId && Backup::PGPKey($key, $keyId)));
}
private function StorePGPKey(string $key, string $keyId = '') : bool
{
$oAccount = $this->getMainAccountFromToken();
if ($oAccount) {
$keyId = $keyId ? "0x{$keyId}" : \sha1($key);
$dir = $this->StorageProvider()->GenerateFilePath(
$oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::PGP,
true
);
if (\str_contains($key, 'PGP PRIVATE KEY')) {
$hash = $oAccount->CryptKey();
$key = \SnappyMail\Crypt::Encrypt($key, $hash);
$key[1] = \base64_encode($key[1]);
$key[2] = \base64_encode($key[2]);
$key[] = \hash_hmac('sha1', $key[2], $hash);
return !!\file_put_contents("{$dir}{$keyId}.key", \json_encode($key));
}
if (\str_contains($key, 'PGP PUBLIC KEY')) {
return !!\file_put_contents("{$dir}{$keyId}_public.asc", $key);
}
}
return false;
}
}

View file

@ -9,22 +9,28 @@ trait Raw
*/
public function RawViewAsPlain() : bool
{
$this->initMailClientConnection();
$sRawKey = (string) $this->GetActionParam('RawKey', '');
$aValues = $this->getDecodedRawKeyValue($sRawKey);
$sFolder = isset($aValues['Folder']) ? (string) $aValues['Folder'] : '';
$iUid = isset($aValues['Uid']) ? (int) $aValues['Uid'] : 0;
$sMimeIndex = isset($aValues['MimeIndex']) ? (string) $aValues['MimeIndex'] : '';
\header('Content-Type: text/plain');
return $this->MailClient()->MessageMimeStream(function ($rResource) {
if (\is_resource($rResource)) {
\MailSo\Base\Utils::FpassthruWithTimeLimitReset($rResource);
}
}, $sFolder, $iUid, $sMimeIndex);
$oAccount = $this->getAccountFromToken();
$sRawKey = $this->GetActionParam('RawKey', '');
$aValues = $this->decodeRawKey($sRawKey);
if (!empty($aValues['folder']) && !empty($aValues['uid'])
&& !empty($aValues['accountHash']) && $aValues['accountHash'] === $oAccount->Hash()
) {
$this->verifyCacheByKey($sRawKey);
$this->initMailClientConnection();
\header('Content-Type: text/plain');
return $this->MailClient()->MessageMimeStream(
function ($rResource) use ($sRawKey) {
if (\is_resource($rResource)) {
$this->cacheByKey($sRawKey);
\MailSo\Base\Utils::FpassthruWithTimeLimitReset($rResource);
}
},
(string) $aValues['folder'],
(int) $aValues['uid'],
isset($aValues['mimeIndex']) ? (string) $aValues['mimeIndex'] : ''
);
}
return false;
}
public function RawDownload() : bool
@ -49,60 +55,19 @@ trait Raw
$oAccount = $this->getAccountFromToken();
$sData = $this->StorageProvider()->Get($oAccount,
$mData = $this->StorageProvider()->Get($oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG,
'background'
);
if (!empty($sData))
{
$aData = \json_decode($sData, true);
unset($sData);
if (!empty($aData['ContentType']) && !empty($aData['Raw']) &&
\in_array($aData['ContentType'], array('image/png', 'image/jpg', 'image/jpeg')))
{
if (!empty($mData)) {
$mData = \json_decode($mData, true);
if (!empty($mData['ContentType']) && !empty($mData['Raw'])) {
$this->cacheByKey($sRawKey);
\header('Content-Type: '.$aData['ContentType']);
echo \base64_decode($aData['Raw']);
unset($aData);
return true;
}
}
return false;
}
public function RawPublic() : bool
{
$sRawKey = (string) $this->GetActionParam('RawKey', '');
$this->verifyCacheByKey($sRawKey);
$sHash = $sRawKey;
$sData = '';
if (!empty($sHash))
{
$sData = $this->StorageProvider()->Get(null,
\RainLoop\Providers\Storage\Enumerations\StorageType::NOBODY,
\RainLoop\KeyPathHelper::PublicFile($sHash)
);
}
$aMatch = array();
if (!empty($sData) && 0 === \strpos($sData, 'data:') &&
\preg_match('/^data:([^:]+):/', $sData, $aMatch) && !empty($aMatch[1]))
{
$sContentType = \trim($aMatch[1]);
if (\in_array($sContentType, array('image/png', 'image/jpg', 'image/jpeg')))
{
$this->cacheByKey($sRawKey);
\header('Content-Type: '.$sContentType);
echo \preg_replace('/^data:[^:]+:/', '', $sData);
unset($sData);
\header('Content-Type: '.$mData['ContentType']);
echo \base64_decode($mData['Raw']);
unset($mData);
return true;
}
@ -111,52 +76,51 @@ trait Raw
return false;
}
/**
* Message, Message Attachment or Zip
*/
private function rawSmart(bool $bDownload, bool $bThumbnail = false) : bool
{
$sRawKey = (string) $this->GetActionParam('RawKey', '');
$aValues = $this->getDecodedRawKeyValue($sRawKey);
$sRange = $this->Http()->GetHeader('Range');
$oAccount = $this->getAccountFromToken();
$aValues = $this->decodeRawKey($sRawKey);
if (empty($aValues['accountHash']) || $aValues['accountHash'] !== $oAccount->Hash()) {
return false;
}
$sRange = \MailSo\Base\Http::GetHeader('Range');
$aMatch = array();
$sRangeStart = $sRangeEnd = '';
$bIsRangeRequest = false;
if (!empty($sRange) && 'bytes=0-' !== \strtolower($sRange) && \preg_match('/^bytes=([0-9]+)-([0-9]*)/i', \trim($sRange), $aMatch))
if (!empty($sRange) && 'bytes=0-' !== \strtolower($sRange)
&& \preg_match('/^bytes=([0-9]+)-([0-9]*)/i', \trim($sRange), $aMatch))
{
$sRangeStart = $aMatch[1];
$sRangeEnd = $aMatch[2];
$bIsRangeRequest = true;
}
$sFolder = isset($aValues['Folder']) ? (string) $aValues['Folder'] : '';
$iUid = isset($aValues['Uid']) ? (int) $aValues['Uid'] : 0;
$sMimeIndex = isset($aValues['MimeIndex']) ? (string) $aValues['MimeIndex'] : '';
$sMimeIndex = isset($aValues['mimeIndex']) ? (string) $aValues['mimeIndex'] : '';
$sContentTypeIn = isset($aValues['mimeType']) ? (string) $aValues['mimeType'] : '';
$sFileNameIn = isset($aValues['fileName']) ? (string) $aValues['fileName'] : '';
$sContentTypeIn = isset($aValues['MimeType']) ? (string) $aValues['MimeType'] : '';
$sFileNameIn = isset($aValues['FileName']) ? (string) $aValues['FileName'] : '';
$sFileHashIn = isset($aValues['FileHash']) ? (string) $aValues['FileHash'] : '';
if (!empty($sFileHashIn))
{
if (!empty($aValues['fileHash'])) {
$this->verifyCacheByKey($sRawKey);
$oAccount = $this->getAccountFromToken();
// https://github.com/the-djmaze/snappymail/issues/144
if ('.pdf' === \substr($sFileNameIn,-4)) {
$sContentTypeOut = 'application/pdf'; // application/octet-stream
} else {
$sContentTypeOut = $sContentTypeIn ?: \MailSo\Base\Utils::MimeContentType($sFileNameIn);
$sContentTypeOut = $sContentTypeIn
?: \SnappyMail\File\MimeType::fromFilename($sFileNameIn)
?: 'application/octet-stream';
}
$sFileNameOut = $this->MainClearFileName($sFileNameIn, $sContentTypeIn, $sMimeIndex);
$rResource = $this->FilesProvider()->GetFile($oAccount, $sFileHashIn);
if (\is_resource($rResource))
{
$rResource = $this->FilesProvider()->GetFile($oAccount, (string) $aValues['fileHash']);
if (\is_resource($rResource)) {
\header('Content-Type: '.$sContentTypeOut);
\header('Content-Disposition: attachment; '.
\trim(\MailSo\Base\Utils::EncodeHeaderUtf8AttributeValue('filename', $sFileNameOut)));
@ -170,25 +134,35 @@ trait Raw
return false;
}
else if (!empty($sFolder) && 0 < $iUid)
{
$this->verifyCacheByKey($sRawKey);
$sFolder = isset($aValues['folder']) ? (string) $aValues['folder'] : '';
$iUid = isset($aValues['uid']) ? (int) $aValues['uid'] : 0;
if (empty($sFolder) || 1 > $iUid) {
return false;
}
$oAccount = $this->initMailClientConnection();
$this->verifyCacheByKey($sRawKey);
$this->initMailClientConnection();
$self = $this;
return $this->MailClient()->MessageMimeStream(
function($rResource, $sContentType, $sFileName, $sMimeIndex = '') use (
$self, $oAccount, $sRawKey, $sContentTypeIn, $sFileNameIn, $bDownload, $bThumbnail,
$self, $sRawKey, $sContentTypeIn, $sFileNameIn, $bDownload, $bThumbnail,
$bIsRangeRequest, $sRangeStart, $sRangeEnd
) {
if ($oAccount && \is_resource($rResource))
{
if (\is_resource($rResource)) {
\MailSo\Base\Utils::ResetTimeLimit();
$self->cacheByKey($sRawKey);
$self->logWrite(\print_r([
$sFileName,
$sContentType,
$sFileNameIn,
$sContentTypeIn
],true), \LOG_DEBUG, 'RAW');
if ($sFileNameIn) {
$sFileName = $sFileNameIn;
}
@ -197,30 +171,28 @@ trait Raw
if ('.pdf' === \substr($sFileName, -4)) {
// https://github.com/the-djmaze/snappymail/issues/144
$sContentType = 'application/pdf';
} else if ($sContentTypeIn) {
$sContentType = $sContentTypeIn;
} else if (!$sContentType) {
$sContentType = $sFileName ? \MailSo\Base\Utils::MimeContentType($sFileName) : 'text/plain';
} else {
$sContentType = $sContentTypeIn
?: $sContentType
// ?: \SnappyMail\File\MimeType::fromStream($rResource, $sFileName)
?: \SnappyMail\File\MimeType::fromFilename($sFileName)
?: 'application/octet-stream';
}
if (!$bDownload)
{
if (!$bDownload) {
$bDetectImageOrientation = $self->Config()->Get('labs', 'image_exif_auto_rotate', false)
// Mostly only JPEG has EXIF metadata
&& 'image/jpeg' == $sContentType;
try
{
if ($bThumbnail)
{
if ($bThumbnail) {
$oImage = static::loadImage($rResource, $bDetectImageOrientation, 48);
\header('Content-Disposition: inline; '.
\trim(\MailSo\Base\Utils::EncodeHeaderUtf8AttributeValue('filename', $sFileName.'_thumb60x60.png')));
$oImage->show('png');
// $oImage->show('webp'); // Little Britain: "Safari says NO"
exit;
}
else if ($bDetectImageOrientation)
{
} else if ($bDetectImageOrientation) {
$oImage = static::loadImage($rResource, $bDetectImageOrientation);
\header('Content-Disposition: inline; '.
\trim(\MailSo\Base\Utils::EncodeHeaderUtf8AttributeValue('filename', $sFileName)));
@ -259,24 +231,20 @@ trait Raw
\MailSo\Base\Http::StatusHeader(206);
$iRangeStart = (int) $sRangeStart;
$iRangeEnd = (int) $sRangeEnd;
$iRangeStart = \max(0, \intval($sRangeStart));
$iRangeEnd = \max(0, \intval($sRangeEnd));
if ('' === $sRangeEnd) {
$sLoadedData = 0 < $iRangeStart ? \substr($sLoadedData, $iRangeStart) : $sLoadedData;
} else {
if ($iRangeStart < $iRangeEnd) {
$sLoadedData = \substr($sLoadedData, $iRangeStart, $iRangeEnd - $iRangeStart);
} else {
$sLoadedData = 0 < $iRangeStart ? \substr($sLoadedData, $iRangeStart) : $sLoadedData;
}
if ($iRangeEnd && $iRangeStart < $iRangeEnd) {
$sLoadedData = \substr($sLoadedData, $iRangeStart, $iRangeEnd - $iRangeStart);
} else if ($iRangeStart) {
$sLoadedData = \substr($sLoadedData, $iRangeStart);
}
$iContentLength = \strlen($sLoadedData);
if (0 < $iContentLength) {
\header('Content-Length: '.$iContentLength);
\header('Content-Range: bytes '.$sRangeStart.'-'.(0 < $iRangeEnd ? $iRangeEnd : $iFullContentLength - 1).'/'.$iFullContentLength);
\header('Content-Range: bytes '.$sRangeStart.'-'.($iRangeEnd ?: $iFullContentLength - 1).'/'.$iFullContentLength);
}
} else {
\header('Content-Length: '.\strlen($sLoadedData));

View file

@ -9,128 +9,64 @@ use RainLoop\Utils;
trait Response
{
/**
* @param mixed $mResult = false
* @param mixed $mResult
*/
public function DefaultResponse(string $sActionName, $mResult = false, array $aAdditionalParams = array()) : array
public function DefaultResponse($mResult, array $aAdditionalParams = array(), string $sActionName = '') : array
{
$this->Plugins()->RunHook('main.default-response-data', array($sActionName, &$mResult));
$aResponseItem = $this->mainDefaultResponse($sActionName, $mResult, $aAdditionalParams);
$this->Plugins()->RunHook('main.default-response', array($sActionName, &$aResponseItem));
return $aResponseItem;
}
public function TrueResponse(string $sActionName, array $aAdditionalParams = array()) : array
{
$mResult = true;
$this->Plugins()->RunHook('main.default-response-data', array($sActionName, &$mResult));
$aResponseItem = $this->mainDefaultResponse($sActionName, $mResult, $aAdditionalParams);
$this->Plugins()->RunHook('main.default-response', array($sActionName, &$aResponseItem));
return $aResponseItem;
}
public function FalseResponse(string $sActionName, ?int $iErrorCode = null, ?string $sErrorMessage = null, ?string $sAdditionalErrorMessage = null) : array
{
$mResult = false;
$this->Plugins()
->RunHook('main.default-response-data', array($sActionName, &$mResult))
->RunHook('main.default-response-error-data', array($sActionName, &$iErrorCode, &$sErrorMessage))
;
$aAdditionalParams = array();
if (null !== $iErrorCode)
{
$aAdditionalParams['ErrorCode'] = (int) $iErrorCode;
$aAdditionalParams['ErrorMessage'] = null === $sErrorMessage ? '' : (string) $sErrorMessage;
$aAdditionalParams['ErrorMessageAdditional'] = null === $sAdditionalErrorMessage ? '' : (string) $sAdditionalErrorMessage;
if (false === $mResult) {
if (!isset($aAdditionalParams['ErrorCode'])) {
$aAdditionalParams['ErrorCode'] = 0;
}
if (!isset($aAdditionalParams['ErrorMessage'])) {
$aAdditionalParams['ErrorMessage'] = '';
}
}
$aResponseItem = $this->mainDefaultResponse($sActionName, $mResult, $aAdditionalParams);
$this->Plugins()->RunHook('main.default-response', array($sActionName, &$aResponseItem));
return $aResponseItem;
return \array_merge(array(
// 'version' => APP_VERSION,
'Action' => $sActionName,
'Result' => $this->responseObject($mResult)
), $aAdditionalParams);
}
public function ExceptionResponse(string $sActionName, \Throwable $oException) : array
public function TrueResponse(array $aAdditionalParams = array()) : array
{
$iErrorCode = null;
$sErrorMessage = null;
$sErrorMessageAdditional = null;
return $this->DefaultResponse(true, $aAdditionalParams);
}
if ($oException instanceof \RainLoop\Exceptions\ClientException)
{
public function FalseResponse(int $iErrorCode = 0, string $sErrorMessage = '', string $sAdditionalErrorMessage = '') : array
{
return $this->DefaultResponse(false, [
'ErrorCode' => $iErrorCode,
'ErrorMessage' => $sErrorMessage,
'ErrorMessageAdditional' => $sAdditionalErrorMessage
]);
}
public function ExceptionResponse(\Throwable $oException) : array
{
$iErrorCode = 0;
$sErrorMessage = '';
$sErrorMessageAdditional = '';
if ($oException instanceof \RainLoop\Exceptions\ClientException) {
$iErrorCode = $oException->getCode();
$sErrorMessage = null;
if ($iErrorCode === Notifications::ClientViewError)
{
if ($iErrorCode === Notifications::ClientViewError) {
$sErrorMessage = $oException->getMessage();
}
$sErrorMessageAdditional = $oException->getAdditionalMessage();
if (empty($sErrorMessageAdditional))
{
$sErrorMessageAdditional = null;
}
}
else
{
} else {
$iErrorCode = Notifications::UnknownError;
$sErrorMessage = $oException->getCode().' - '.$oException->getMessage();
}
$oPrevious = $oException->getPrevious();
if ($oPrevious)
{
$this->Logger()->WriteException($oPrevious);
}
else
{
$this->Logger()->WriteException($oException);
}
$this->logException($oException->getPrevious() ?: $oException);
return $this->FalseResponse($sActionName, $iErrorCode, $sErrorMessage, $sErrorMessageAdditional);
}
/**
* @param mixed $mResult = false
*/
private function mainDefaultResponse(string $sActionName, $mResult = false, array $aAdditionalParams = array()) : array
{
$sActionName = 'Do' === \substr($sActionName, 0, 2) ? \substr($sActionName, 2) : $sActionName;
$sActionName = \preg_replace('/[^a-zA-Z0-9_]+/', '', $sActionName);
$aResult = array(
// 'Version' => APP_VERSION,
'Action' => $sActionName,
'Result' => $this->responseObject($mResult, $sActionName)
);
foreach ($aAdditionalParams as $sKey => $mValue)
{
$aResult[$sKey] = $mValue;
}
return $aResult;
}
private function isFileHasThumbnail(string $sFileName) : bool
{
static $aCache = array();
$sExt = \MailSo\Base\Utils::GetFileExtension($sFileName);
if (isset($aCache[$sExt])) {
return $aCache[$sExt];
}
$bResult = (
\extension_loaded('gd')
|| \extension_loaded('gmagick')
|| \extension_loaded('imagick')
) && \in_array($sExt, ['png', 'gif', 'jpg', 'jpeg']);
$aCache[$sExt] = $bResult;
return $bResult;
return $this->DefaultResponse(false, [
'ErrorCode' => $iErrorCode,
'ErrorMessage' => $sErrorMessage,
'ErrorMessageAdditional' => $sErrorMessageAdditional
]);
}
/**
@ -141,124 +77,36 @@ trait Response
private $aCheckableFolder = null;
private function responseObject($mResponse, string $sParent = '')
{
if (!($mResponse instanceof \JsonSerializable))
{
if (\is_object($mResponse))
{
if (!($mResponse instanceof \JsonSerializable)) {
if (\is_object($mResponse)) {
return '["'.\get_class($mResponse).'"]';
}
if (\is_array($mResponse))
{
foreach ($mResponse as $iKey => $oItem)
{
$mResponse[$iKey] = $this->responseObject($oItem, $sParent);
if (\is_array($mResponse)) {
foreach ($mResponse as $iKey => $oItem) {
$mResponse[$iKey] = $this->responseObject($oItem, 'Array');
}
}
return $mResponse;
}
if ($mResponse instanceof \MailSo\Mail\Message)
{
$mResult = $mResponse->jsonSerialize();
$oAccount = $this->getAccountFromToken();
if (!$mResult['DateTimeStampInUTC'] || $this->Config()->Get('labs', 'date_from_headers', false)) {
$iDateTimeStampInUTC = $mResponse->HeaderTimeStampInUTC();
if ($iDateTimeStampInUTC) {
$mResult['DateTimeStampInUTC'] = $iDateTimeStampInUTC;
}
}
// \MailSo\Mime\EmailCollection
foreach (['ReplyTo','From','To','Cc','Bcc','Sender','DeliveredTo'] as $prop) {
$mResult[$prop] = $this->responseObject($mResult[$prop], $sParent);
}
$sSubject = $mResult['subject'];
$mResult['Hash'] = \md5($mResult['Folder'].$mResult['Uid']);
$mResult['RequestHash'] = Utils::EncodeKeyValuesQ(array(
'Account' => $oAccount->Hash(),
'Folder' => $mResult['Folder'],
'Uid' => $mResult['Uid'],
'MimeType' => 'message/rfc822',
'FileName' => (\strlen($sSubject) ? \MailSo\Base\Utils::SecureFileName($sSubject) : 'message-'.$mResult['Uid']) . '.eml'
));
$mResult['Attachments'] = $this->responseObject($mResponse->Attachments(), $sParent);
if ('Message' === $sParent)
{
$mResult['DraftInfo'] = $mResponse->DraftInfo();
$mResult['InReplyTo'] = $mResponse->InReplyTo();
$mResult['UnsubsribeLinks'] = $mResponse->UnsubsribeLinks();
$mResult['References'] = $mResponse->References();
$mResult['Html'] = $mResponse->Html();
$mResult['Plain'] = $mResponse->Plain();
// $this->GetCapa(Capa::OPEN_PGP) || $this->GetCapa(Capa::GNUPG)
$mResult['PgpSigned'] = $mResponse->PgpSigned();
$mResult['PgpEncrypted'] = $mResponse->PgpEncrypted();
$mResult['ReadReceipt'] = $mResponse->ReadReceipt();
if (\strlen($mResult['ReadReceipt']) && !\in_array('$forwarded', $mResult['Flags']))
{
// \in_array('$mdnsent', $mResult['Flags'])
if (\strlen($mResult['ReadReceipt']))
{
try
{
$oReadReceipt = \MailSo\Mime\Email::Parse($mResult['ReadReceipt']);
if (!$oReadReceipt)
{
$mResult['ReadReceipt'] = '';
}
}
catch (\Throwable $oException) { unset($oException); }
}
if (\strlen($mResult['ReadReceipt']) && '1' === $this->Cacher($oAccount)->Get(
\RainLoop\KeyPathHelper::ReadReceiptCache($oAccount->Email(), $mResult['Folder'], $mResult['Uid']), '0'))
{
$mResult['ReadReceipt'] = '';
}
}
}
return $mResult;
}
if ($mResponse instanceof \MailSo\Mail\Attachment)
{
$mResult = $mResponse->jsonSerialize();
$mResult['IsThumbnail'] = $this->GetCapa(Capa::ATTACHMENT_THUMBNAILS) && $this->isFileHasThumbnail($mResult['FileName']);
$mResult['Download'] = Utils::EncodeKeyValuesQ(array(
'Account' => $this->getAccountFromToken()->Hash(),
'Folder' => $mResult['Folder'],
'Uid' => $mResult['Uid'],
'MimeIndex' => $mResult['MimeIndex'],
'MimeType' => $mResult['MimeType'],
'FileName' => $mResult['FileName']
));
return $mResult;
}
if ($mResponse instanceof \MailSo\Mail\Folder)
{
if ($mResponse instanceof \MailSo\Mail\Message) {
$aResult = $mResponse->jsonSerialize();
$aStatus = $mResponse->Status();
if ($aStatus && isset($aStatus['MESSAGES'], $aStatus['UIDNEXT'])) {
$aResult['Hash'] = $this->MailClient()->GenerateFolderHash(
$mResponse->FullName(),
$aStatus['MESSAGES'],
$aStatus['UIDNEXT'],
empty($aStatus['HIGHESTMODSEQ']) ? 0 : $aStatus['HIGHESTMODSEQ']
);
if (!$this->Config()->Get('labs', 'date_from_headers', true) && $aResult['internalTimestamp']) {
$aResult['dateTimestamp'] = $aResult['internalTimestamp'];
}
if (!$sParent && \strlen($aResult['readReceipt']) && !\in_array('$forwarded', $aResult['flags'])) {
$oAccount = $this->getAccountFromToken();
if ('1' === $this->Cacher($oAccount)->Get(\RainLoop\KeyPathHelper::ReadReceiptCache($oAccount->Email(), $aResult['folder'], $aResult['uid']), '0')) {
$aResult['readReceipt'] = '';
}
}
return $aResult;
}
if ($mResponse instanceof \MailSo\Imap\Folder) {
$aResult = $mResponse->jsonSerialize();
if (null === $this->aCheckableFolder) {
$aCheckable = \json_decode(
@ -268,27 +116,11 @@ trait Response
);
$this->aCheckableFolder = \is_array($aCheckable) ? $aCheckable : array();
}
$aResult['Checkable'] = \in_array($mResponse->FullName(), $this->aCheckableFolder);
$aResult['checkable'] = \in_array($mResponse->FullName, $this->aCheckableFolder);
return $aResult;
}
if ($mResponse instanceof \MailSo\Base\Collection)
{
$mResult = $mResponse->jsonSerialize();
$mResult['@Collection'] = $this->responseObject($mResult['@Collection'], $sParent);
if ($mResponse instanceof \MailSo\Mail\EmailCollection) {
return \array_slice($mResult['@Collection'], 0, 100);
}
if ($mResponse instanceof \MailSo\Mail\AttachmentCollection
|| $mResponse instanceof \MailSo\Mail\FolderCollection
|| $mResponse instanceof \MailSo\Mail\MessageCollection
) {
return $mResult;
}
return $mResult['@Collection'];
}
return $mResponse;
}
}

View file

@ -15,7 +15,7 @@ trait Themes
&& ($oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount))) {
$sTheme = (string) $oSettingsLocal->GetConf('Theme', $sTheme);
}
$sTheme = $this->ValidateTheme($sTheme) ?: 'Default';
$sTheme = $this->ValidateTheme($sTheme);
}
return $sTheme;
}
@ -54,16 +54,24 @@ trait Themes
}
$sDir = APP_INDEX_ROOT_PATH . 'themes'; // custom user themes
if (\is_dir($sDir)) {
$rDirH = \opendir($sDir);
if ($rDirH) {
if (\is_dir($sDir) && ($rDirH = \opendir($sDir))) {
while (($sFile = \readdir($rDirH)) !== false) {
if ('.' !== $sFile[0] && \is_dir($sDir . '/' . $sFile)
&& (\file_exists("{$sDir}/{$sFile}/styles.css") || \file_exists("{$sDir}/{$sFile}/styles.less"))) {
$aCache[] = $sFile . '@custom';
}
}
\closedir($rDirH);
}
if (\class_exists('OC', false)) {
$sDir = \OC::$SERVERROOT . '/themes'; // custom user themes
if (\is_dir($sDir) && ($rDirH = \opendir($sDir))) {
while (($sFile = \readdir($rDirH)) !== false) {
if ('.' !== $sFile[0] && \is_dir($sDir . '/' . $sFile)
&& (\file_exists("{$sDir}/{$sFile}/styles.css") || \file_exists("{$sDir}/{$sFile}/styles.less"))) {
$aCache[] = $sFile . '@custom';
if ('.' !== $sFile[0] && \is_dir("{$sDir}/{$sFile}") && \file_exists("{$sDir}/{$sFile}/snappymail/style.css")) {
$aCache[] = $sFile . '@nextcloud';
}
}
\closedir($rDirH);
}
}
@ -82,43 +90,49 @@ trait Themes
return $aCache;
}
public function ThemeLink(bool $bAdmin): string
{
return './?/Css/0/' . ($bAdmin ? 'Admin' : 'User') . '/-/' . $this->GetTheme($bAdmin) . '/-/' . $this->StaticCache() . '/Hash/-/';
}
public function ValidateTheme(string $sTheme): string
{
return \in_array($sTheme, $this->GetThemes()) ? $sTheme : $this->Config()->Get('themes', 'default', 'Default');
}
public function compileCss(string $sTheme, bool $bAdmin) : string
{
$bCustomTheme = '@custom' === \substr($sTheme, -7);
if ($bCustomTheme) {
$sTheme = \substr($sTheme, 0, -7);
}
$mResult = array();
$sBase = ($bCustomTheme ? \RainLoop\Utils::WebPath() : \RainLoop\Utils::WebVersionPath())
. "themes/{$sTheme}/";
$bLess = false;
$sThemeCSSFile = ($bCustomTheme ? APP_INDEX_ROOT_PATH : APP_VERSION_ROOT_PATH).'themes/'.$sTheme.'/styles.css';
if (\is_file($sThemeCSSFile)) {
$mResult[] = \file_get_contents($sThemeCSSFile);
} else {
$sThemeCSSFile = \str_replace('styles.css', 'styles.less', $sThemeCSSFile);
if (\is_file($sThemeCSSFile)) {
$bLess = true;
$mResult[] = "@base: \"{$sBase}\";";
$mResult[] = \file_get_contents($sThemeCSSFile);
if (!\in_array($sTheme, $this->GetThemes())) {
$sTheme = $this->Config()->Get('webmail', 'theme', 'Default');
if (!\in_array($sTheme, $this->GetThemes())) {
$sTheme = 'Default';
}
}
return $sTheme;
}
$mResult[] = $this->Plugins()->CompileCss($bAdmin, $bLess);
public function compileCss(string $sTheme, bool $bAdmin, bool $bMinified = false) : string
{
$mResult = array();
$bLess = false;
if ('@nextcloud' === \substr($sTheme, -10)) {
$sBase = \OC::$WEBROOT . '/';
$sThemeCSSFile = \OC::$SERVERROOT . '/themes/' . \str_replace('@nextcloud', '/snappymail/style.css', $sTheme);
} else {
$bCustomTheme = '@custom' === \substr($sTheme, -7);
if ($bCustomTheme) {
$sTheme = \substr($sTheme, 0, -7);
$sBase = \RainLoop\Utils::WebPath();
} else {
$sBase = \RainLoop\Utils::WebVersionPath();
}
$sBase .= "themes/{$sTheme}/";
$sThemeCSSFile = ($bCustomTheme ? APP_INDEX_ROOT_PATH : APP_VERSION_ROOT_PATH).'themes/'.$sTheme.'/styles.css';
if (!\is_file($sThemeCSSFile)) {
$sThemeCSSFile = \str_replace('styles.css', 'styles.less', $sThemeCSSFile);
if (\is_file($sThemeCSSFile)) {
$bLess = true;
$mResult[] = "@base: \"{$sBase}\";";
$mResult[] = \file_get_contents($sThemeCSSFile);
}
}
}
if (\is_file($sThemeCSSFile)) {
$mResult[] = \file_get_contents($sThemeCSSFile);
}
$mResult[] = $this->Plugins()->CompileCss($bAdmin, $bLess, $bMinified);
$mResult = \preg_replace('@(url\(["\']?)(\\./)?([a-z]+[^:a-z])@',
"\$1{$sBase}\$3",
@ -127,4 +141,84 @@ trait Themes
return $bLess ? (new \LessPHP\lessc())->compile($mResult) : $mResult;
// : \str_replace(';}', '}', \preg_replace('/\\s*([:;{},])\\s*/', '\1', \preg_replace('/\\s+/', ' ', \preg_replace('#/\\*.*?\\*/#s', '', $mResult))));
}
public function UploadBackground(?array $aFile, int $iError): array
{
$oAccount = $this->getAccountFromToken();
if (!$this->GetCapa(\RainLoop\Enumerations\Capa::USER_BACKGROUND)) {
return $this->FalseResponse();
}
$sName = '';
$sHash = '';
if ($oAccount && UPLOAD_ERR_OK === $iError && \is_array($aFile)) {
$sMimeType = \SnappyMail\File\MimeType::fromFile($aFile['tmp_name'], $aFile['name'])
?: \SnappyMail\File\MimeType::fromFilename($aFile['name'])
?: $aFile['type'];
if (\in_array($sMimeType, array('image/png', 'image/jpg', 'image/jpeg', 'image/webp'))) {
$sSavedName = 'upload-post-' . \md5($aFile['name'] . $aFile['tmp_name'])
. \SnappyMail\File\MimeType::toExtension($sMimeType);
if (!$this->FilesProvider()->MoveUploadedFile($oAccount, $sSavedName, $aFile['tmp_name'])) {
$iError = \RainLoop\Enumerations\UploadError::ON_SAVING;
} else {
$rData = $this->FilesProvider()->GetFile($oAccount, $sSavedName);
if (\is_resource($rData)) {
$sData = \stream_get_contents($rData);
if (!empty($sData) && \strlen($sData)) {
$sName = $aFile['name'];
if (empty($sName)) {
$sName = '_';
}
if ($this->StorageProvider()->Put($oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG,
'background',
// Used by RawUserBackground()
\RainLoop\Utils::jsonEncode(array(
'ContentType' => $sMimeType,
'Raw' => \base64_encode($sData)
))
)) {
$oSettings = $this->SettingsProvider()->Load($oAccount);
if ($oSettings) {
$sHash = \MailSo\Base\Utils::Sha1Rand($sName . APP_VERSION . APP_SALT);
$oSettings->SetConf('UserBackgroundName', $sName);
$oSettings->SetConf('UserBackgroundHash', $sHash);
$this->SettingsProvider()->Save($oAccount, $oSettings);
}
}
}
unset($sData);
}
if (\is_resource($rData)) {
\fclose($rData);
}
unset($rData);
}
$this->FilesProvider()->Clear($oAccount, $sSavedName);
} else {
$iError = \RainLoop\Enumerations\UploadError::FILE_TYPE;
}
}
if (UPLOAD_ERR_OK !== $iError) {
$iClientError = 0;
$sError = \RainLoop\Enumerations\UploadError::getUserMessage($iError, $iClientError);
if (!empty($sError)) {
return $this->FalseResponse($iClientError, $sError);
}
}
return $this->DefaultResponse(!empty($sName) && !empty($sHash) ? array(
'name' => $sName,
'hash' => $sHash
) : false);
}
}

View file

@ -15,18 +15,15 @@ trait User
use Filters;
use Folders;
use Messages;
use Attachments;
use Pgp;
/**
* @var \RainLoop\Providers\Suggestions
*/
private $oSuggestionsProvider = null;
private ?Suggestions $oSuggestionsProvider = null;
public function SuggestionsProvider(): Suggestions
{
if (null === $this->oSuggestionsProvider) {
$this->oSuggestionsProvider = new Suggestions(
$this->fabrica('suggestions'));
$this->oSuggestionsProvider = new Suggestions($this->fabrica('suggestions'));
}
return $this->oSuggestionsProvider;
@ -39,177 +36,36 @@ trait User
{
$sEmail = \MailSo\Base\Utils::Trim($this->GetActionParam('Email', ''));
$sPassword = $this->GetActionParam('Password', '');
$bSignMe = !empty($this->GetActionParam('SignMe', 0));
$bSignMe = !empty($this->GetActionParam('signMe', 0));
$this->Logger()->AddSecret($sPassword);
$this->logMask($sPassword);
$oAccount = $this->LoginProcess($sEmail, $sPassword, $bSignMe);
$this->Plugins()->RunHook('login.success', array($oAccount));
try {
$oAccount = $this->LoginProcess($sEmail, $sPassword, $bSignMe);
} catch (\Throwable $oException) {
$this->loginErrorDelay();
throw $oException;
}
$this->SetAuthToken($oAccount);
$sLanguage = $this->GetActionParam('Language', '');
if ($oAccount && $sLanguage)
{
$oSettings = $this->SettingsProvider()->Load($oAccount);
if ($oSettings)
{
$sLanguage = $this->ValidateLanguage($sLanguage);
$sCurrentLanguage = $oSettings->GetConf('Language', '');
$this->Plugins()->RunHook('login.success', array($oAccount));
if ($sCurrentLanguage !== $sLanguage)
{
$oSettings->SetConf('Language', $sLanguage);
$sLanguage = $this->GetActionParam('language', '');
if ($oAccount && $sLanguage) {
$oSettings = $this->SettingsProvider()->Load($oAccount);
if ($oSettings) {
$sLanguage = $this->ValidateLanguage($sLanguage);
$sCurrentLanguage = $oSettings->GetConf('language', '');
if ($sCurrentLanguage !== $sLanguage) {
$oSettings->SetConf('language', $sLanguage);
$this->SettingsProvider()->Save($oAccount, $oSettings);
}
}
}
return $this->DefaultResponse(__FUNCTION__, $this->AppData(false));
}
/**
* @throws \MailSo\RuntimeException
*/
public function DoAttachmentsActions() : array
{
if (!$this->GetCapa(Capa::ATTACHMENTS_ACTIONS))
{
return $this->FalseResponse(__FUNCTION__);
}
$oAccount = $this->initMailClientConnection();
$sAction = $this->GetActionParam('Do', '');
$aHashes = $this->GetActionParam('Hashes', null);
$mResult = false;
$bError = false;
$aData = false;
if (\is_array($aHashes) && \count($aHashes))
{
$aData = array();
foreach ($aHashes as $sZipHash)
{
$aResult = $this->getMimeFileByHash($oAccount, $sZipHash);
if (!empty($aResult['FileHash']))
{
$aData[] = $aResult;
}
else
{
$bError = true;
break;
}
}
}
$oFilesProvider = $this->FilesProvider();
if (!empty($sAction) && !$bError && \is_array($aData) && \count($aData) &&
$oFilesProvider && $oFilesProvider->IsActive())
{
$bError = false;
switch (\strtolower($sAction))
{
case 'zip':
$sZipHash = \MailSo\Base\Utils::Sha1Rand();
$sZipFileName = $oFilesProvider->GenerateLocalFullFileName($oAccount, $sZipHash);
if (!empty($sZipFileName)) {
if (\class_exists('ZipArchive')) {
$oZip = new \ZipArchive();
$oZip->open($sZipFileName, \ZIPARCHIVE::CREATE | \ZIPARCHIVE::OVERWRITE);
$oZip->setArchiveComment('SnappyMail/'.APP_VERSION);
foreach ($aData as $aItem) {
$sFileName = (string) (isset($aItem['FileName']) ? $aItem['FileName'] : 'file.dat');
$sFileHash = (string) (isset($aItem['FileHash']) ? $aItem['FileHash'] : '');
if (!empty($sFileHash)) {
$sFullFileNameHash = $oFilesProvider->GetFileName($oAccount, $sFileHash);
if (!$oZip->addFile($sFullFileNameHash, $sFileName)) {
$bError = true;
}
}
}
if (!$bError) {
$bError = !$oZip->close();
} else {
$oZip->close();
}
/*
} else {
@\unlink($sZipFileName);
$oZip = new \SnappyMail\Stream\ZIP($sZipFileName);
// $oZip->setArchiveComment('SnappyMail/'.APP_VERSION);
foreach ($aData as $aItem) {
$sFileName = (string) (isset($aItem['FileName']) ? $aItem['FileName'] : 'file.dat');
$sFileHash = (string) (isset($aItem['FileHash']) ? $aItem['FileHash'] : '');
if (!empty($sFileHash)) {
$sFullFileNameHash = $oFilesProvider->GetFileName($oAccount, $sFileHash);
if (!$oZip->addFile($sFullFileNameHash, $sFileName)) {
$bError = true;
}
}
}
$oZip->close();
*/
} else {
@\unlink($sZipFileName);
$oZip = new \PharData($sZipFileName . '.zip', 0, null, \Phar::ZIP);
$oZip->compressFiles(\Phar::GZ);
foreach ($aData as $aItem) {
$sFileName = (isset($aItem['FileName']) ? (string) $aItem['FileName'] : 'file.dat');
$sFileHash = (isset($aItem['FileHash']) ? (string) $aItem['FileHash'] : '');
if ($sFileHash) {
$sFullFileNameHash = $oFilesProvider->GetFileName($oAccount, $sFileHash);
$oZip->addFile($sFullFileNameHash, $sFileName);
}
}
$oZip->compressFiles(\Phar::GZ);
unset($oZip);
\rename($sZipFileName . '.zip', $sZipFileName);
}
foreach ($aData as $aItem) {
$sFileHash = (isset($aItem['FileHash']) ? (string) $aItem['FileHash'] : '');
if ($sFileHash) {
$oFilesProvider->Clear($oAccount, $sFileHash);
}
}
if (!$bError) {
$mResult = array(
'FileHash' => Utils::EncodeKeyValuesQ(array(
'Account' => $oAccount ? $oAccount->Hash() : '',
'FileName' => 'attachments.zip',
'MimeType' => 'application/zip',
'FileHash' => $sZipHash
))
);
}
}
break;
default:
$data = new \SnappyMail\AttachmentsAction;
$data->action = $sAction;
$data->items = $aData;
$data->filesProvider = $oFilesProvider;
$data->account = $oAccount;
$this->Plugins()->RunHook('json.attachments', array($data));
$mResult = $data->result;
break;
}
}
else
{
$bError = true;
}
$this->requestSleep();
return $this->DefaultResponse(__FUNCTION__, $bError ? false : $mResult);
return $this->DefaultResponse($this->AppData(false));
}
public function DoLogout() : array
@ -217,7 +73,7 @@ trait User
$bMain = true; // empty($_COOKIE[self::AUTH_ADDITIONAL_TOKEN_KEY]);
$this->Logout($bMain);
$bMain && $this->ClearSignMeData();
return $this->TrueResponse(__FUNCTION__);
return $this->TrueResponse();
}
public function DoAppDelayStart() : array
@ -240,20 +96,17 @@ trait User
$iMainCacheTime = !empty($aTimers[0]) && \is_numeric($aTimers[0]) ? (int) $aTimers[0] : 0;
$iFilesCacheTime = !empty($aTimers[1]) && \is_numeric($aTimers[1]) ? (int) $aTimers[1] : 0;
if (0 === $iMainCacheTime || $iMainCacheTime + $iOneDay1 < \time())
{
if (0 === $iMainCacheTime || $iMainCacheTime + $iOneDay1 < \time()) {
$bMainCache = true;
$iMainCacheTime = \time();
}
if (0 === $iFilesCacheTime || $iFilesCacheTime + $iOneDay2 < \time())
{
if (0 === $iFilesCacheTime || $iFilesCacheTime + $iOneDay2 < \time()) {
$bFilesCache = true;
$iFilesCacheTime = \time();
}
if ($bMainCache || $bFilesCache)
{
if ($bMainCache || $bFilesCache) {
if (!$this->StorageProvider()->Put(null,
\RainLoop\Providers\Storage\Enumerations\StorageType::NOBODY, 'Cache/Timers',
\implode(',', array($iMainCacheTime, $iFilesCacheTime))))
@ -262,26 +115,23 @@ trait User
}
}
if ($bMainCache)
{
$this->Logger()->Write('Cacher GC: Begin');
if ($bMainCache) {
$this->logWrite('Cacher GC: Begin');
$this->Cacher()->GC(48);
$this->Logger()->Write('Cacher GC: End');
$this->logWrite('Cacher GC: End');
$this->Logger()->Write('Storage GC: Begin');
$this->logWrite('Storage GC: Begin');
$this->StorageProvider()->GC();
$this->Logger()->Write('Storage GC: End');
}
else if ($bFilesCache)
{
$this->Logger()->Write('Files GC: Begin');
$this->logWrite('Storage GC: End');
} else if ($bFilesCache) {
$this->logWrite('Files GC: Begin');
$this->FilesProvider()->GC(48);
$this->Logger()->Write('Files GC: End');
$this->logWrite('Files GC: End');
}
$this->Plugins()->RunHook('service.app-delay-start-end');
return $this->TrueResponse(__FUNCTION__);
return $this->TrueResponse();
}
public function DoSettingsUpdate() : array
@ -294,28 +144,23 @@ trait User
$oSettings = $this->SettingsProvider()->Load($oAccount);
$oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount);
if ($oConfig->Get('webmail', 'allow_languages_on_settings', true))
{
$this->setSettingsFromParams($oSettings, 'Language', 'string', function ($sLanguage) use ($self) {
if ($oConfig->Get('webmail', 'allow_languages_on_settings', true)) {
$this->setSettingsFromParams($oSettings, 'language', 'string', function ($sLanguage) use ($self) {
return $self->ValidateLanguage($sLanguage);
});
} else {
// $oSettings->SetConf('language', $this->ValidateLanguage($oConfig->Get('webmail', 'language', 'en')));
}
else
{
// $oSettings->SetConf('Language', $this->ValidateLanguage($oConfig->Get('webmail', 'language', 'en')));
}
$this->setSettingsFromParams($oSettings, 'hourCycle', 'string');
if ($this->GetCapa(Capa::THEMES))
{
if ($this->GetCapa(Capa::THEMES)) {
$this->setSettingsFromParams($oSettingsLocal, 'Theme', 'string', function ($sTheme) use ($self) {
return $self->ValidateTheme($sTheme);
});
$this->setSettingsFromParams($oSettings, 'fontSansSerif', 'string');
$this->setSettingsFromParams($oSettings, 'fontSerif', 'string');
$this->setSettingsFromParams($oSettings, 'fontMono', 'string');
}
else
{
} else {
// $oSettingsLocal->SetConf('Theme', $this->ValidateTheme($oConfig->Get('webmail', 'theme', 'Default')));
}
@ -332,13 +177,21 @@ trait User
$this->setSettingsFromParams($oSettings, 'EditorDefaultType', 'string');
$this->setSettingsFromParams($oSettings, 'requestReadReceipt', 'bool');
$this->setSettingsFromParams($oSettings, 'requestDsn', 'bool');
$this->setSettingsFromParams($oSettings, 'requireTLS', 'bool');
$this->setSettingsFromParams($oSettings, 'pgpSign', 'bool');
$this->setSettingsFromParams($oSettings, 'pgpEncrypt', 'bool');
$this->setSettingsFromParams($oSettings, 'allowSpellcheck', 'bool');
$this->setSettingsFromParams($oSettings, 'ViewHTML', 'bool');
$this->setSettingsFromParams($oSettings, 'ShowImages', 'bool');
$this->setSettingsFromParams($oSettings, 'ViewImages', 'string');
$this->setSettingsFromParams($oSettings, 'ViewImagesWhitelist', 'string');
$this->setSettingsFromParams($oSettings, 'RemoveColors', 'bool');
$this->setSettingsFromParams($oSettings, 'AllowStyles', 'bool');
$this->setSettingsFromParams($oSettings, 'ListInlineAttachments', 'bool');
$this->setSettingsFromParams($oSettings, 'CollapseBlockquotes', 'bool');
$this->setSettingsFromParams($oSettings, 'MaxBlockquotesLevel', 'int');
$this->setSettingsFromParams($oSettings, 'simpleAttachmentsList', 'bool');
$this->setSettingsFromParams($oSettings, 'listGrouped', 'bool');
$this->setSettingsFromParams($oSettings, 'ContactsAutosave', 'bool');
$this->setSettingsFromParams($oSettings, 'DesktopNotifications', 'bool');
$this->setSettingsFromParams($oSettings, 'SoundNotification', 'bool');
@ -348,6 +201,7 @@ trait User
$this->setSettingsFromParams($oSettings, 'AutoLogout', 'int');
$this->setSettingsFromParams($oSettings, 'MessageReadDelay', 'int');
$this->setSettingsFromParams($oSettings, 'MsgDefaultAction', 'int');
$this->setSettingsFromParams($oSettings, 'showNextMessage', 'bool');
$this->setSettingsFromParams($oSettings, 'Resizer4Width', 'int');
$this->setSettingsFromParams($oSettings, 'Resizer5Width', 'int');
@ -358,9 +212,10 @@ trait User
$this->setSettingsFromParams($oSettingsLocal, 'HideUnsubscribed', 'bool');
$this->setSettingsFromParams($oSettingsLocal, 'HideDeleted', 'bool');
$this->setSettingsFromParams($oSettingsLocal, 'UnhideKolabFolders', 'bool');
$this->setSettingsFromParams($oSettingsLocal, 'ShowUnreadCount', 'bool');
$this->setSettingsFromParams($oSettingsLocal, 'CheckMailInterval', 'int');
return $this->DefaultResponse(__FUNCTION__,
$this->SettingsProvider()->Save($oAccount, $oSettings) &&
return $this->DefaultResponse($this->SettingsProvider()->Save($oAccount, $oSettings) &&
$this->SettingsProvider(true)->Save($oAccount, $oSettingsLocal));
}
@ -368,21 +223,20 @@ trait User
{
$oAccount = $this->initMailClientConnection();
if (!$this->GetCapa(Capa::QUOTA))
{
return $this->DefaultResponse(__FUNCTION__, array(0, 0, 0, 0));
if (!$this->GetCapa(Capa::QUOTA)) {
return $this->DefaultResponse(array(0, 0, 0, 0));
}
try
{
$aQuota = $this->MailClient()->QuotaRoot();
$aQuota = $this->ImapClient()->QuotaRoot();
}
catch (\Throwable $oException)
{
throw new ClientException(Notifications::MailServerError, $oException);
}
return $this->DefaultResponse(__FUNCTION__, $aQuota);
return $this->DefaultResponse($aQuota);
}
public function DoSuggestions() : array
@ -392,86 +246,27 @@ trait User
$sQuery = \trim($this->GetActionParam('Query', ''));
$iLimit = (int) $this->Config()->Get('contacts', 'suggestions_limit', 20);
$aResult = array();
$this->Plugins()->RunHook('json.suggestions-input-parameters', array(&$sQuery, &$iLimit, $oAccount));
$iLimit = (int) $iLimit;
if (5 > $iLimit)
{
$iLimit = 5;
$aResult = array();
if ($oSuggestionsProvider = $this->SuggestionsProvider()) {
$aResult = $oSuggestionsProvider->Process($oAccount, $sQuery, $iLimit);
}
$this->Plugins()->RunHook('json.suggestions-pre', array(&$aResult, $sQuery, $oAccount, $iLimit));
if ($iLimit > \count($aResult) && \strlen($sQuery))
{
try
{
// Address Book
$oAddressBookProvider = $this->AddressBookProvider($oAccount);
if ($oAddressBookProvider && $oAddressBookProvider->IsActive())
{
$aSuggestions = $oAddressBookProvider->GetSuggestions($sQuery, $iLimit);
if (!\count($aResult))
{
$aResult = $aSuggestions;
}
else
{
$aResult = \array_merge($aResult, $aSuggestions);
}
}
}
catch (\Throwable $oException)
{
$this->Logger()->WriteException($oException);
}
}
if ($iLimit > \count($aResult) && \strlen($sQuery))
{
$oSuggestionsProvider = $this->SuggestionsProvider();
if ($oSuggestionsProvider && $oSuggestionsProvider->IsActive())
{
$aSuggestionsProviderResult = $oSuggestionsProvider->Process($oAccount, $sQuery, $iLimit);
if (\is_array($aSuggestionsProviderResult) && \count($aSuggestionsProviderResult))
{
$aResult = \array_merge($aResult, $aSuggestionsProviderResult);
}
}
}
$aResult = Utils::RemoveSuggestionDuplicates($aResult);
if ($iLimit < \count($aResult))
{
$aResult = \array_slice($aResult, 0, $iLimit);
}
$this->Plugins()->RunHook('json.suggestions-post', array(&$aResult, $sQuery, $oAccount, $iLimit));
$aResult = Utils::RemoveSuggestionDuplicates($aResult);
if ($iLimit < \count($aResult))
{
$aResult = \array_slice($aResult, 0, $iLimit);
}
return $this->DefaultResponse(__FUNCTION__, $aResult);
return $this->DefaultResponse($aResult);
}
public function DoClearUserBackground() : array
{
$oAccount = $this->getAccountFromToken();
if (!$this->GetCapa(Capa::USER_BACKGROUND))
{
return $this->FalseResponse(__FUNCTION__);
if (!$this->GetCapa(Capa::USER_BACKGROUND)) {
return $this->FalseResponse();
}
$oSettings = $this->SettingsProvider()->Load($oAccount);
if ($oAccount && $oSettings)
{
if ($oAccount && $oSettings) {
$this->StorageProvider()->Clear($oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG,
'background'
@ -481,61 +276,13 @@ trait User
$oSettings->SetConf('UserBackgroundHash', '');
}
return $this->DefaultResponse(__FUNCTION__, $oAccount && $oSettings ?
return $this->DefaultResponse($oAccount && $oSettings ?
$this->SettingsProvider()->Save($oAccount, $oSettings) : false);
}
private function getMimeFileByHash(\RainLoop\Model\Account $oAccount, string $sHash) : array
{
$aValues = $this->getDecodedRawKeyValue($sHash);
$sFolder = isset($aValues['Folder']) ? (string) $aValues['Folder'] : '';
$iUid = isset($aValues['Uid']) ? (int) $aValues['Uid'] : 0;
$sMimeIndex = isset($aValues['MimeIndex']) ? (string) $aValues['MimeIndex'] : '';
$sContentTypeIn = isset($aValues['MimeType']) ? (string) $aValues['MimeType'] : '';
$sFileNameIn = isset($aValues['FileName']) ? (string) $aValues['FileName'] : '';
$oFileProvider = $this->FilesProvider();
$sResultHash = '';
$mResult = $this->MailClient()->MessageMimeStream(function ($rResource, $sContentType, $sFileName, $sMimeIndex = '')
use ($oAccount, $oFileProvider, $sFileNameIn, $sContentTypeIn, &$sResultHash) {
unset($sContentType, $sFileName, $sMimeIndex);
if ($oAccount && \is_resource($rResource))
{
$sHash = \MailSo\Base\Utils::Sha1Rand($sFileNameIn.'~'.$sContentTypeIn);
$rTempResource = $oFileProvider->GetFile($oAccount, $sHash, 'wb+');
if (\is_resource($rTempResource))
{
if (false !== \MailSo\Base\Utils::MultipleStreamWriter($rResource, array($rTempResource)))
{
$sResultHash = $sHash;
}
\fclose($rTempResource);
}
}
}, $sFolder, $iUid, $sMimeIndex);
$aValues['FileHash'] = '';
if ($mResult)
{
$aValues['FileHash'] = $sResultHash;
}
return $aValues;
}
private function setSettingsFromParams(\RainLoop\Settings $oSettings, string $sConfigName, string $sType = 'string', ?callable $cCallback = null) : void
{
if ($this->HasActionParam($sConfigName))
{
if ($this->HasActionParam($sConfigName)) {
$sValue = $this->GetActionParam($sConfigName, '');
switch ($sType)
{

Some files were not shown because too many files have changed in this diff Show more