Revamp NetClient (SMTP, IMAP, Sieve) login for rfc7628 OAuth plugin support

This commit is contained in:
djmaze 2021-10-22 12:45:14 +02:00
parent dff2415762
commit 617d700a5d
11 changed files with 194 additions and 178 deletions

View file

@ -29,48 +29,38 @@ $Plugin->addHook('hook.name', 'functionName');
## IMAP ## IMAP
### imap.credentials
params:
\RainLoop\Model\Account $oAccount
array &$aCredentials
### imap.before-connect ### imap.before-connect
params: params:
\RainLoop\Model\Account $oAccount \RainLoop\Model\Account $oAccount
\MailSo\Mail\MailClient $oMailClient \MailSo\Imap\ImapClient $oImapClient
array $aCredentials array &$aCredentials
### imap.after-connect ### imap.after-connect
params: params:
\RainLoop\Model\Account $oAccount \RainLoop\Model\Account $oAccount
\MailSo\Mail\MailClient $oMailClient \MailSo\Imap\ImapClient $oImapClient
array $aCredentials array $aCredentials
### imap.before-login ### imap.before-login
params: params:
\RainLoop\Model\Account $oAccount \RainLoop\Model\Account $oAccount
\MailSo\Mail\MailClient $oMailClient \MailSo\Imap\ImapClient $oImapClient
array $aCredentials array &$aCredentials
### imap.after-login ### imap.after-login
params: params:
\RainLoop\Model\Account $oAccount \RainLoop\Model\Account $oAccount
\MailSo\Mail\MailClient $oMailClient \MailSo\Imap\ImapClient $oImapClient
bool $bSuccess bool $bSuccess
array $aCredentials array $aCredentials
## Sieve ## Sieve
### sieve.credentials
params:
\RainLoop\Model\Account $oAccount
array &$aCredentials
### sieve.before-connect ### sieve.before-connect
params: params:
\RainLoop\Model\Account $oAccount \RainLoop\Model\Account $oAccount
\MailSo\Sieve\ManageSieveClient $oSieveClient \MailSo\Sieve\ManageSieveClient $oSieveClient
array $aCredentials array &$aCredentials
### sieve.after-connect ### sieve.after-connect
params: params:
@ -82,27 +72,22 @@ $Plugin->addHook('hook.name', 'functionName');
params: params:
\RainLoop\Model\Account $oAccount \RainLoop\Model\Account $oAccount
\MailSo\Sieve\ManageSieveClient $oSieveClient \MailSo\Sieve\ManageSieveClient $oSieveClient
bool $bSuccess array &$aCredentials
array $aCredentials
### sieve.after-login ### sieve.after-login
params: params:
\RainLoop\Model\Account $oAccount \RainLoop\Model\Account $oAccount
\MailSo\Sieve\ManageSieveClient $oSieveClient \MailSo\Sieve\ManageSieveClient $oSieveClient
bool $bSuccess
array $aCredentials array $aCredentials
## SMTP ## SMTP
### smtp.credentials
params:
\RainLoop\Model\Account $oAccount
array &$aCredentials
### smtp.before-connect ### smtp.before-connect
params: params:
\RainLoop\Model\Account $oAccount \RainLoop\Model\Account $oAccount
\MailSo\Smtp\SmtpClient $oSmtpClient \MailSo\Smtp\SmtpClient $oSmtpClient
array $aCredentials array &$aCredentials
### smtp.after-connect ### smtp.after-connect
params: params:
@ -114,7 +99,7 @@ $Plugin->addHook('hook.name', 'functionName');
params: params:
\RainLoop\Model\Account $oAccount \RainLoop\Model\Account $oAccount
\MailSo\Smtp\SmtpClient $oSmtpClient \MailSo\Smtp\SmtpClient $oSmtpClient
array $aCredentials array &$aCredentials
### smtp.after-login ### smtp.after-login
params: params:

View file

@ -130,18 +130,24 @@ class ImapClient extends \MailSo\Net\NetClient
* @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Net\Exceptions\Exception
* @throws \MailSo\Imap\Exceptions\Exception * @throws \MailSo\Imap\Exceptions\Exception
*/ */
public function Login(string $sLogin, string $sPassword, string $sProxyAuthUser = '', public function Login(array $aCredentials) : self
bool $bUseAuthPlainIfSupported = true, bool $bUseAuthCramMd5IfSupported = true) : self
{ {
if (!\strlen(\trim($sLogin)) || !\strlen(\trim($sPassword))) if (!empty($aCredentials['ProxyAuthUser']) && !empty($aCredentials['ProxyAuthPassword'])) {
$sLogin = \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($aCredentials['ProxyAuthUser']));
$sPassword = $aCredentials['ProxyAuthPassword'];
$sProxyAuthUser = $aCredentials['Login'];
} else {
$sLogin = \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($aCredentials['Login']));
$sPassword = $aCredentials['Password'];
}
if (!\strlen($sLogin) || !\strlen($sPassword))
{ {
$this->writeLogException( $this->writeLogException(
new \MailSo\Base\Exceptions\InvalidArgumentException, new \MailSo\Base\Exceptions\InvalidArgumentException,
\MailSo\Log\Enumerations\Type::ERROR, true); \MailSo\Log\Enumerations\Type::ERROR, true);
} }
$sLogin = \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($sLogin));
$this->sLogginedUser = $sLogin; $this->sLogginedUser = $sLogin;
// $encrypted = !empty(\stream_get_meta_data($this->ConnectionResource())['crypto']); // $encrypted = !empty(\stream_get_meta_data($this->ConnectionResource())['crypto']);
@ -149,9 +155,11 @@ class ImapClient extends \MailSo\Net\NetClient
$types = [ $types = [
// 'SCRAM-SHA-256' => 1, // !$encrypted // 'SCRAM-SHA-256' => 1, // !$encrypted
// 'SCRAM-SHA-1' => 1, // !$encrypted // 'SCRAM-SHA-1' => 1, // !$encrypted
'CRAM-MD5' => $bUseAuthCramMd5IfSupported, 'CRAM-MD5' => $aCredentials['UseAuthCramMd5IfSupported'],
'PLAIN' => $bUseAuthPlainIfSupported, 'PLAIN' => $aCredentials['UseAuthPlainIfSupported'],
'LOGIN' => 1, 'OAUTHBEARER' => $aCredentials['UseAuthOAuth2IfSupported'],
'XOAUTH2' => $aCredentials['UseAuthOAuth2IfSupported'],
'LOGIN' => 1
]; ];
foreach ($types as $sasl_type => $active) { foreach ($types as $sasl_type => $active) {
if ($active && $this->IsSupported("AUTH={$sasl_type}") && \SnappyMail\SASL::isSupported($sasl_type)) { if ($active && $this->IsSupported("AUTH={$sasl_type}") && \SnappyMail\SASL::isSupported($sasl_type)) {
@ -188,7 +196,7 @@ class ImapClient extends \MailSo\Net\NetClient
$this->sendRaw($sAuth, true, '*******'); $this->sendRaw($sAuth, true, '*******');
$this->getResponse(); $this->getResponse();
} }
else if ('PLAIN' === $type) else if ('PLAIN' === $type || 'OAUTHBEARER' === $type)
{ {
$sAuth = $SASL->authenticate($sLogin, $sPassword); $sAuth = $SASL->authenticate($sLogin, $sPassword);
if ($this->oLogger) { if ($this->oLogger) {
@ -202,6 +210,23 @@ class ImapClient extends \MailSo\Net\NetClient
$this->getResponse(); $this->getResponse();
} }
} }
else if ('XOAUTH2' === $type)
{
$sAuth = $SASL->authenticate($sLogin, $sPassword);
$this->SendRequest('AUTHENTICATE', array($type, $sAuth));
$aR = $this->parseResponseWithValidation();
if (\is_array($aR) && \count($aR) && isset($aR[\count($aR) - 1])) {
$oR = $aR[\count($aR) - 1];
if (\MailSo\Imap\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]),
\MailSo\Log\Enumerations\Type::WARNING);
}
$this->sendRaw('');
$this->parseResponseWithValidation();
}
}
}
else if ($this->IsSupported('LOGINDISABLED')) else if ($this->IsSupported('LOGINDISABLED'))
{ {
$sB64 = $this->getResponseValue($this->SendRequestGetResponse('AUTHENTICATE', array($type)), Enumerations\ResponseType::CONTINUATION); $sB64 = $this->getResponseValue($this->SendRequestGetResponse('AUTHENTICATE', array($type)), Enumerations\ResponseType::CONTINUATION);
@ -227,7 +252,7 @@ class ImapClient extends \MailSo\Net\NetClient
)); ));
} }
if (0 < \strlen($sProxyAuthUser)) if (\strlen($sProxyAuthUser))
{ {
$this->SendRequestGetResponse('PROXYAUTH', array($this->EscapeString($sProxyAuthUser))); $this->SendRequestGetResponse('PROXYAUTH', array($this->EscapeString($sProxyAuthUser)));
} }
@ -578,7 +603,7 @@ class ImapClient extends \MailSo\Net\NetClient
$aFetchItems = Enumerations\FetchType::ChangeFetchItemsBefourRequest($aInputFetchItems); $aFetchItems = Enumerations\FetchType::ChangeFetchItemsBefourRequest($aInputFetchItems);
foreach ($aFetchItems as $sName => $mItem) foreach ($aFetchItems as $sName => $mItem)
{ {
if (0 < \strlen($sName) && '' !== $mItem) if (\strlen($sName) && '' !== $mItem)
{ {
$this->aFetchCallbacks[$sName] = $mItem; $this->aFetchCallbacks[$sName] = $mItem;
} }
@ -704,7 +729,7 @@ class ImapClient extends \MailSo\Net\NetClient
} }
else else
{ {
if (0 < \strlen($sCharset)) if (\strlen($sCharset))
{ {
$aRequest[] = 'CHARSET'; $aRequest[] = 'CHARSET';
$aRequest[] = \strtoupper($sCharset); $aRequest[] = \strtoupper($sCharset);
@ -716,7 +741,7 @@ class ImapClient extends \MailSo\Net\NetClient
$aRequest[] = $sSearchCriterias; $aRequest[] = $sSearchCriterias;
if (0 < \strlen($sLimit)) if (\strlen($sLimit))
{ {
$aRequest[] = $sLimit; $aRequest[] = $sLimit;
} }
@ -755,7 +780,7 @@ class ImapClient extends \MailSo\Net\NetClient
$sCommandPrefix = ($bReturnUid) ? 'UID ' : ''; $sCommandPrefix = ($bReturnUid) ? 'UID ' : '';
$aRequest = array(); $aRequest = array();
if (0 < \strlen($sCharset)) if (\strlen($sCharset))
{ {
$aRequest[] = 'CHARSET'; $aRequest[] = 'CHARSET';
$aRequest[] = \strtoupper($sCharset); $aRequest[] = \strtoupper($sCharset);
@ -890,7 +915,7 @@ class ImapClient extends \MailSo\Net\NetClient
$sCmd = 'EXPUNGE'; $sCmd = 'EXPUNGE';
$aArguments = array(); $aArguments = array();
if (!$bExpungeAll && $bForceUidExpunge && 0 < \strlen($sUidRangeIfSupported) && $this->IsSupported('UIDPLUS')) if (!$bExpungeAll && $bForceUidExpunge && \strlen($sUidRangeIfSupported) && $this->IsSupported('UIDPLUS'))
{ {
$sCmd = 'UID '.$sCmd; $sCmd = 'UID '.$sCmd;
$aArguments = array($sUidRangeIfSupported); $aArguments = array($sUidRangeIfSupported);
@ -950,8 +975,8 @@ class ImapClient extends \MailSo\Net\NetClient
$oLast = $this->GetLastResponse()->getLast(); $oLast = $this->GetLastResponse()->getLast();
if ($oLast && Enumerations\ResponseType::TAGGED === $oLast->ResponseType && \is_array($oLast->OptionalResponse)) if ($oLast && Enumerations\ResponseType::TAGGED === $oLast->ResponseType && \is_array($oLast->OptionalResponse))
{ {
if (0 < \strlen($oLast->OptionalResponse[0]) && if (\strlen($oLast->OptionalResponse[0]) &&
0 < \strlen($oLast->OptionalResponse[2]) && \strlen($oLast->OptionalResponse[2]) &&
'APPENDUID' === strtoupper($oLast->OptionalResponse[0]) && 'APPENDUID' === strtoupper($oLast->OptionalResponse[0]) &&
\is_numeric($oLast->OptionalResponse[2]) \is_numeric($oLast->OptionalResponse[2])
) )
@ -1603,11 +1628,11 @@ class ImapClient extends \MailSo\Net\NetClient
} }
$sFetchKey = ''; $sFetchKey = '';
if (0 < \strlen($sLiteralAtomUpperCasePeek) && isset($this->aFetchCallbacks[$sLiteralAtomUpperCasePeek])) if (\strlen($sLiteralAtomUpperCasePeek) && isset($this->aFetchCallbacks[$sLiteralAtomUpperCasePeek]))
{ {
$sFetchKey = $sLiteralAtomUpperCasePeek; $sFetchKey = $sLiteralAtomUpperCasePeek;
} }
else if (0 < \strlen($sLiteralAtomUpperCase) && isset($this->aFetchCallbacks[$sLiteralAtomUpperCase])) else if (\strlen($sLiteralAtomUpperCase) && isset($this->aFetchCallbacks[$sLiteralAtomUpperCase]))
{ {
$sFetchKey = $sLiteralAtomUpperCase; $sFetchKey = $sLiteralAtomUpperCase;
} }
@ -1726,7 +1751,7 @@ class ImapClient extends \MailSo\Net\NetClient
return '"' . \addcslashes($sStringForEscape, '\\"') . '"'; return '"' . \addcslashes($sStringForEscape, '\\"') . '"';
} }
protected function getLogName() : string public function getLogName() : string
{ {
return 'IMAP'; return 'IMAP';
} }

View file

@ -40,31 +40,6 @@ class MailClient
return $this->oImapClient; return $this->oImapClient;
} }
/**
* @throws \MailSo\Base\Exceptions\InvalidArgumentException
* @throws \MailSo\Net\Exceptions\Exception
* @throws \MailSo\Imap\Exceptions\Exception
*/
public function Connect(string $sServerName, int $iPort = 143,
int $iSecurityType = \MailSo\Net\Enumerations\ConnectionSecurityType::AUTO_DETECT, bool $bVerifySsl = false,
bool $bAllowSelfSigned = false, string $sClientCert = '') : self
{
$this->oImapClient->Connect($sServerName, $iPort, $iSecurityType, $bVerifySsl, $bAllowSelfSigned, $sClientCert);
return $this;
}
/**
* @throws \MailSo\Base\Exceptions\InvalidArgumentException
* @throws \MailSo\Net\Exceptions\Exception
* @throws \MailSo\Imap\Exceptions\LoginException
*/
public function Login(string $sLogin, string $sPassword, string $sProxyAuthUser = '',
bool $bUseAuthPlainIfSupported = true, bool $bUseAuthCramMd5IfSupported = true) : self
{
$this->oImapClient->Login($sLogin, $sPassword, $sProxyAuthUser, $bUseAuthPlainIfSupported, $bUseAuthCramMd5IfSupported);
return $this;
}
/** /**
* @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Net\Exceptions\Exception
*/ */

View file

@ -433,10 +433,7 @@ abstract class NetClient
} }
} }
protected function getLogName() : string abstract function getLogName() : string;
{
return 'NET';
}
protected function writeLog(string $sDesc, int $iDescType = \MailSo\Log\Enumerations\Type::INFO, bool $bDiplayCrLf = false) : void protected function writeLog(string $sDesc, int $iDescType = \MailSo\Log\Enumerations\Type::INFO, bool $bDiplayCrLf = false) : void
{ {

View file

@ -37,11 +37,6 @@ class ManageSieveClient extends \MailSo\Net\NetClient
*/ */
private $aModules = array(); private $aModules = array();
/**
* @var bool
*/
public $__USE_INITIAL_AUTH_PLAIN_COMMAND = true;
public function IsSupported(string $sCapa) : bool public function IsSupported(string $sCapa) : bool
{ {
return isset($this->aCapa[\strtoupper($sCapa)]); return isset($this->aCapa[\strtoupper($sCapa)]);
@ -100,9 +95,12 @@ class ManageSieveClient extends \MailSo\Net\NetClient
* @throws \MailSo\Base\Exceptions\InvalidArgumentException * @throws \MailSo\Base\Exceptions\InvalidArgumentException
* @throws \MailSo\Sieve\Exceptions\LoginException * @throws \MailSo\Sieve\Exceptions\LoginException
*/ */
public function Login(string $sLogin, string $sPassword, string $sLoginAuthKey = '') : self public function Login(array $aCredentials) : self
{ {
if (!\strlen(\trim($sLogin)) || !\strlen(\trim($sPassword))) $sLogin = $aCredentials['Login'];
$sPassword = $aCredentials['Password'];
$sLoginAuthKey = '';
if (!\strlen($sLogin) || !\strlen($sPassword))
{ {
$this->writeLogException( $this->writeLogException(
new \MailSo\Base\Exceptions\InvalidArgumentException, new \MailSo\Base\Exceptions\InvalidArgumentException,
@ -137,7 +135,7 @@ class ManageSieveClient extends \MailSo\Net\NetClient
{ {
$sAuth = $SASL->authenticate($sLogin, $sPassword, $sLoginAuthKey); $sAuth = $SASL->authenticate($sLogin, $sPassword, $sLoginAuthKey);
if ($this->__USE_INITIAL_AUTH_PLAIN_COMMAND) if ($aCredentials['InitialAuthPlain'])
{ {
$this->sendRequest('AUTHENTICATE "PLAIN" "'.$sAuth.'"'); $this->sendRequest('AUTHENTICATE "PLAIN" "'.$sAuth.'"');
} }
@ -530,7 +528,7 @@ class ManageSieveClient extends \MailSo\Net\NetClient
} }
} }
protected function getLogName() : string public function getLogName() : string
{ {
return 'SIEVE'; return 'SIEVE';
} }

View file

@ -117,18 +117,21 @@ class SmtpClient extends \MailSo\Net\NetClient
* @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Net\Exceptions\Exception
* @throws \MailSo\Smtp\Exceptions\Exception * @throws \MailSo\Smtp\Exceptions\Exception
*/ */
public function Login(string $sLogin, string $sPassword, bool $bUseAuthPlainIfSupported = true, bool $bUseAuthCramMd5IfSupported = true) : self public function Login(array $aCredentials) : self
{ {
$sLogin = \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($sLogin)); $sLogin = \MailSo\Base\Utils::IdnToAscii(\MailSo\Base\Utils::Trim($aCredentials['Login']));
$sPassword = $aCredentials['Password'];
// $encrypted = !empty(\stream_get_meta_data($this->ConnectionResource())['crypto']); // $encrypted = !empty(\stream_get_meta_data($this->ConnectionResource())['crypto']);
$type = ''; $type = '';
$types = [ $types = [
'SCRAM-SHA-256' => 1, // !$encrypted 'SCRAM-SHA-256' => 1, // !$encrypted
'SCRAM-SHA-1' => 1, // !$encrypted 'SCRAM-SHA-1' => 1, // !$encrypted
'CRAM-MD5' => $bUseAuthCramMd5IfSupported, 'CRAM-MD5' => $aCredentials['UseAuthCramMd5IfSupported'],
'PLAIN' => $bUseAuthPlainIfSupported, 'PLAIN' => $aCredentials['UseAuthPlainIfSupported'],
'LOGIN' => 1 // $encrypted 'OAUTHBEARER' => $aCredentials['UseAuthOAuth2IfSupported'],
'XOAUTH2' => $aCredentials['UseAuthOAuth2IfSupported'],
'LOGIN' => 1, // $encrypted
]; ];
foreach ($types as $sasl_type => $active) { foreach ($types as $sasl_type => $active) {
if ($active && $this->IsAuthSupported($sasl_type) && \SnappyMail\SASL::isSupported($sasl_type)) { if ($active && $this->IsAuthSupported($sasl_type) && \SnappyMail\SASL::isSupported($sasl_type)) {
@ -166,6 +169,8 @@ class SmtpClient extends \MailSo\Net\NetClient
{ {
// RFC 4616 // RFC 4616
case 'PLAIN': case 'PLAIN':
case 'XOAUTH2':
case 'OAUTHBEARER':
$this->sendRequestWithCheck($SASL->authenticate($sLogin, $sPassword), 235, '', true); $this->sendRequestWithCheck($SASL->authenticate($sLogin, $sPassword), 235, '', true);
break; break;
@ -192,14 +197,6 @@ class SmtpClient extends \MailSo\Net\NetClient
$sResult = $this->sendRequestWithCheck($SASL->challenge($sResult), 235, '', true); $sResult = $this->sendRequestWithCheck($SASL->challenge($sResult), 235, '', true);
$SASL->verify($sResult); $SASL->verify($sResult);
break; break;
/*
// https://developers.google.com/gmail/imap/xoauth2-protocol
case 'XOAUTH2':
throw new \Exception('Please use app passphrases: https://support.google.com/mail/answer/185833');
break;
*/
} }
} }
catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException) catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException)
@ -603,7 +600,7 @@ class SmtpClient extends \MailSo\Net\NetClient
while ('-' === \substr($aParts[1], 0, 1)); while ('-' === \substr($aParts[1], 0, 1));
} }
protected function getLogName() : string public function getLogName() : string
{ {
return 'SMTP'; return 'SMTP';
} }

View file

@ -430,7 +430,6 @@ trait Admin
$oSieveClient = new \MailSo\Sieve\ManageSieveClient(); $oSieveClient = new \MailSo\Sieve\ManageSieveClient();
$oSieveClient->SetLogger($this->Logger()); $oSieveClient->SetLogger($this->Logger());
$oSieveClient->SetTimeOuts($iConnectionTimeout); $oSieveClient->SetTimeOuts($iConnectionTimeout);
$oSieveClient->__USE_INITIAL_AUTH_PLAIN_COMMAND = !!$this->Config()->Get('labs', 'sieve_auth_plain_initial', true);
$iTime = \microtime(true); $iTime = \microtime(true);
$oSieveClient->Connect($oDomain->SieveHost(), $oDomain->SievePort(), $oDomain->SieveSecure(), $oSieveClient->Connect($oDomain->SieveHost(), $oDomain->SievePort(), $oDomain->SieveSecure(),

View file

@ -716,7 +716,7 @@ trait Messages
$oSmtpClient->SetTimeOuts(10, (int) \RainLoop\Api::Config()->Get('labs', 'smtp_timeout', 60)); $oSmtpClient->SetTimeOuts(10, (int) \RainLoop\Api::Config()->Get('labs', 'smtp_timeout', 60));
$oAccount->OutConnectAndLoginHelper( $oAccount->OutConnectAndLoginHelper(
$this->Plugins(), $oSmtpClient, $this->Config(), null, $bUsePhpMail $this->Plugins(), $oSmtpClient, $this->Config(), $bUsePhpMail
); );
if ($bUsePhpMail) if ($bUsePhpMail)

View file

@ -85,12 +85,12 @@ class Account
public function ParentEmailHelper() : string public function ParentEmailHelper() : string
{ {
return 0 < \strlen($this->sParentEmail) ? $this->sParentEmail : $this->sEmail; return \strlen($this->sParentEmail) ? $this->sParentEmail : $this->sEmail;
} }
public function IsAdditionalAccount() : string public function IsAdditionalAccount() : string
{ {
return 0 < \strlen($this->sParentEmail); return \strlen($this->sParentEmail);
} }
public function IncLogin() : string public function IncLogin() : string
@ -137,7 +137,7 @@ class Account
public function SignMe() : bool public function SignMe() : bool
{ {
return 0 < \strlen($this->sSignMeToken); return \strlen($this->sSignMeToken);
} }
public function SignMeToken() : string public function SignMeToken() : string
@ -262,9 +262,9 @@ class Account
)); ));
} }
public function IncConnectAndLoginHelper(\RainLoop\Plugins\Manager $oPlugins, \MailSo\Mail\MailClient $oMailClient, \RainLoop\Config\Application $oConfig, ?callable $refreshTokenCallback = null) : bool public function IncConnectAndLoginHelper(\RainLoop\Plugins\Manager $oPlugins, \MailSo\Mail\MailClient $oMailClient, \RainLoop\Config\Application $oConfig) : bool
{ {
$bLogin = false; $oImapClient = $oMailClient->ImapClient();
$aImapCredentials = array( $aImapCredentials = array(
'UseConnect' => true, 'UseConnect' => true,
@ -273,54 +273,25 @@ class Account
'Port' => $this->DomainIncPort(), 'Port' => $this->DomainIncPort(),
'Secure' => $this->DomainIncSecure(), 'Secure' => $this->DomainIncSecure(),
'Login' => $this->IncLogin(), 'Login' => $this->IncLogin(),
'Password' => $this->Password(),
'ProxyAuthUser' => $this->ProxyAuthUser(),
'ProxyAuthPassword' => $this->ProxyAuthPassword(),
'VerifySsl' => !!$oConfig->Get('ssl', 'verify_certificate', false), 'VerifySsl' => !!$oConfig->Get('ssl', 'verify_certificate', false),
'ClientCert' => $this->ClientCert(), 'ClientCert' => $this->ClientCert(),
'AllowSelfSigned' => !!$oConfig->Get('ssl', 'allow_self_signed', true), 'AllowSelfSigned' => !!$oConfig->Get('ssl', 'allow_self_signed', true)
'UseAuthPlainIfSupported' => !!$oConfig->Get('labs', 'imap_use_auth_plain', true),
'UseAuthCramMd5IfSupported' => !!$oConfig->Get('labs', 'imap_use_auth_cram_md5', true)
); );
$oPlugins->RunHook('filter.imap-credentials', array($this, &$aImapCredentials)); $oPlugins->RunHook('imap.before-connect', array($this, $oImapClient, &$aImapCredentials));
$oPlugins->RunHook('imap.before-connect', array($this, $oMailClient, $aImapCredentials));
if ($aImapCredentials['UseConnect']) { if ($aImapCredentials['UseConnect']) {
$oMailClient $oImapClient->Connect($aImapCredentials['Host'], $aImapCredentials['Port'],
->Connect($aImapCredentials['Host'], $aImapCredentials['Port'],
$aImapCredentials['Secure'], $aImapCredentials['VerifySsl'], $aImapCredentials['Secure'], $aImapCredentials['VerifySsl'],
$aImapCredentials['AllowSelfSigned'], $aImapCredentials['ClientCert']); $aImapCredentials['AllowSelfSigned'], $aImapCredentials['ClientCert']);
} }
$oPlugins->RunHook('imap.after-connect', array($this, $oMailClient, $aImapCredentials)); $oPlugins->RunHook('imap.after-connect', array($this, $oImapClient, $aImapCredentials));
$oPlugins->RunHook('imap.before-login', array($this, $oMailClient, $aImapCredentials)); return $this->netClientLogin($oImapClient, $oConfig, $oPlugins, $aImapCredentials);
if ($aImapCredentials['UseAuth']) { }
if (0 < \strlen($aImapCredentials['ProxyAuthUser']) &&
0 < \strlen($aImapCredentials['ProxyAuthPassword'])) public function OutConnectAndLoginHelper(\RainLoop\Plugins\Manager $oPlugins, \MailSo\Smtp\SmtpClient $oSmtpClient, \RainLoop\Config\Application $oConfig, bool &$bUsePhpMail = false) : bool
{ {
$oMailClient
->Login($aImapCredentials['ProxyAuthUser'], $aImapCredentials['ProxyAuthPassword'],
$aImapCredentials['Login'], $aImapCredentials['UseAuthPlainIfSupported'], $aImapCredentials['UseAuthCramMd5IfSupported']);
}
else
{
$oMailClient->Login($aImapCredentials['Login'], $aImapCredentials['Password'], '',
$aImapCredentials['UseAuthPlainIfSupported'], $aImapCredentials['UseAuthCramMd5IfSupported']);
}
$bLogin = true;
}
$oPlugins->RunHook('imap.after-login', array($this, $oMailClient, $bLogin, $aImapCredentials));
return $bLogin;
}
public function OutConnectAndLoginHelper(\RainLoop\Plugins\Manager $oPlugins, \MailSo\Smtp\SmtpClient $oSmtpClient, \RainLoop\Config\Application $oConfig, ?callable $refreshTokenCallback = null, bool &$bUsePhpMail = false) : bool
{
$bLogin = false;
$aSmtpCredentials = array( $aSmtpCredentials = array(
'UseConnect' => !$bUsePhpMail, 'UseConnect' => !$bUsePhpMail,
'UseAuth' => $this->DomainOutAuth(), 'UseAuth' => $this->DomainOutAuth(),
@ -330,21 +301,14 @@ class Account
'Port' => $this->DomainOutPort(), 'Port' => $this->DomainOutPort(),
'Secure' => $this->DomainOutSecure(), 'Secure' => $this->DomainOutSecure(),
'Login' => $this->OutLogin(), 'Login' => $this->OutLogin(),
'Password' => $this->Password(),
'ProxyAuthUser' => $this->ProxyAuthUser(),
'ProxyAuthPassword' => $this->ProxyAuthPassword(),
'VerifySsl' => !!$oConfig->Get('ssl', 'verify_certificate', false), 'VerifySsl' => !!$oConfig->Get('ssl', 'verify_certificate', false),
'AllowSelfSigned' => !!$oConfig->Get('ssl', 'allow_self_signed', true), 'AllowSelfSigned' => !!$oConfig->Get('ssl', 'allow_self_signed', true)
'UseAuthPlainIfSupported' => !!$oConfig->Get('labs', 'smtp_use_auth_plain', true),
'UseAuthCramMd5IfSupported' => !!$oConfig->Get('labs', 'smtp_use_auth_cram_md5', true)
); );
$oPlugins->RunHook('smtp.credentials', array($this, &$aSmtpCredentials)); $oPlugins->RunHook('smtp.before-connect', array($this, $oSmtpClient, &$aSmtpCredentials));
$bUsePhpMail = $aSmtpCredentials['UsePhpMail']; $bUsePhpMail = $aSmtpCredentials['UsePhpMail'];
$aSmtpCredentials['UseAuth'] = $aSmtpCredentials['UseAuth'] && !$aSmtpCredentials['UsePhpMail'];
$oPlugins->RunHook('smtp.before-connect', array($this, $oSmtpClient, $aSmtpCredentials)); if ($aSmtpCredentials['UseConnect'] && !$aSmtpCredentials['UsePhpMail']) {
if ($aSmtpCredentials['UseConnect']) {
$oSmtpClient->Connect($aSmtpCredentials['Host'], $aSmtpCredentials['Port'], $oSmtpClient->Connect($aSmtpCredentials['Host'], $aSmtpCredentials['Port'],
$aSmtpCredentials['Secure'], $aSmtpCredentials['VerifySsl'], $aSmtpCredentials['AllowSelfSigned'], $aSmtpCredentials['Secure'], $aSmtpCredentials['VerifySsl'], $aSmtpCredentials['AllowSelfSigned'],
'', $aSmtpCredentials['Ehlo'] '', $aSmtpCredentials['Ehlo']
@ -352,23 +316,11 @@ class Account
} }
$oPlugins->RunHook('smtp.after-connect', array($this, $oSmtpClient, $aSmtpCredentials)); $oPlugins->RunHook('smtp.after-connect', array($this, $oSmtpClient, $aSmtpCredentials));
$oPlugins->RunHook('smtp.before-login', array($this, $oSmtpClient, $aSmtpCredentials)); return $this->netClientLogin($oSmtpClient, $oConfig, $oPlugins, $aSmtpCredentials);
if ($aSmtpCredentials['UseAuth'] && !$aSmtpCredentials['UsePhpMail'] && $oSmtpClient)
{
$oSmtpClient->Login($aSmtpCredentials['Login'], $aSmtpCredentials['Password'],
$aSmtpCredentials['UseAuthPlainIfSupported'], $aSmtpCredentials['UseAuthCramMd5IfSupported']);
$bLogin = true;
}
$oPlugins->RunHook('smtp.after-login', array($this, $oSmtpClient, $bLogin, $aSmtpCredentials));
return $bLogin;
} }
public function SieveConnectAndLoginHelper(\RainLoop\Plugins\Manager $oPlugins, \MailSo\Sieve\ManageSieveClient $oSieveClient, \RainLoop\Config\Application $oConfig) public function SieveConnectAndLoginHelper(\RainLoop\Plugins\Manager $oPlugins, \MailSo\Sieve\ManageSieveClient $oSieveClient, \RainLoop\Config\Application $oConfig)
{ {
$bLogin = false;
$aSieveCredentials = array( $aSieveCredentials = array(
'UseConnect' => true, 'UseConnect' => true,
'UseAuth' => true, 'UseAuth' => true,
@ -376,17 +328,12 @@ class Account
'Port' => $this->DomainSievePort(), 'Port' => $this->DomainSievePort(),
'Secure' => $this->DomainSieveSecure(), 'Secure' => $this->DomainSieveSecure(),
'Login' => $this->IncLogin(), 'Login' => $this->IncLogin(),
'Password' => $this->Password(),
'VerifySsl' => !!$oConfig->Get('ssl', 'verify_certificate', false), 'VerifySsl' => !!$oConfig->Get('ssl', 'verify_certificate', false),
'AllowSelfSigned' => !!$oConfig->Get('ssl', 'allow_self_signed', true), 'AllowSelfSigned' => !!$oConfig->Get('ssl', 'allow_self_signed', true),
'InitialAuthPlain' => !!$oConfig->Get('ssl', 'sieve_auth_plain_initial', true) 'InitialAuthPlain' => !!$oConfig->Get('ssl', 'sieve_auth_plain_initial', true)
); );
$oPlugins->RunHook('sieve.credentials', array($this, &$aSieveCredentials)); $oPlugins->RunHook('sieve.before-connect', array($this, $oSieveClient, &$aSieveCredentials));
$oSieveClient->__USE_INITIAL_AUTH_PLAIN_COMMAND = $aSieveCredentials['InitialAuthPlain'];
$oPlugins->RunHook('sieve.before-connect', array($this, $oSieveClient, $aSieveCredentials));
if ($aSieveCredentials['UseConnect']) { if ($aSieveCredentials['UseConnect']) {
$oSieveClient->Connect($aSieveCredentials['Host'], $aSieveCredentials['Port'], $oSieveClient->Connect($aSieveCredentials['Host'], $aSieveCredentials['Port'],
$aSieveCredentials['Secure'], $aSieveCredentials['VerifySsl'], $aSieveCredentials['AllowSelfSigned'] $aSieveCredentials['Secure'], $aSieveCredentials['VerifySsl'], $aSieveCredentials['AllowSelfSigned']
@ -394,13 +341,82 @@ class Account
} }
$oPlugins->RunHook('sieve.after-connect', array($this, $oSieveClient, $aSieveCredentials)); $oPlugins->RunHook('sieve.after-connect', array($this, $oSieveClient, $aSieveCredentials));
$oPlugins->RunHook('event.sieve-pre-login', array($this, $oSieveClient, $aSieveCredentials)); return $this->netClientLogin($oSieveClient, $oConfig, $oPlugins, $aSieveCredentials);
if ($aSieveCredentials['UseAuth']) {
$oSieveClient->Login($aSieveCredentials['Login'], $aSieveCredentials['Password']);
$bLogin = true;
} }
$oPlugins->RunHook('sieve.after-login', array($this, $oSieveClient, $bLogin, $aSieveCredentials));
return $bLogin; private function netClientLogin(\MailSo\Net\NetClient $oClient, \RainLoop\Config\Application $oConfig, \RainLoop\Plugins\Manager $oPlugins, array $aCredentials) : bool
{
$aCredentials = \array_merge(
$aCredentials,
array(
'Password' => $this->Password(),
'ProxyAuthUser' => $this->ProxyAuthUser(),
'ProxyAuthPassword' => $this->ProxyAuthPassword(),
'UseAuthPlainIfSupported' => !!$oConfig->Get('labs', 'imap_use_auth_plain', true),
'UseAuthCramMd5IfSupported' => !!$oConfig->Get('labs', 'imap_use_auth_cram_md5', true),
'UseAuthOAuth2IfSupported' => false
)
);
$client_name = \strtolower($oClient->getLogName());
$oPlugins->RunHook("{$client_name}.before-login", array($this, $oClient, &$aCredentials));
$bResult = $aCredentials['UseAuth'] && $oClient->Login($aCredentials);
$oPlugins->RunHook("{$client_name}.after-login", array($this, $oClient, $bResult, $aCredentials));
return $bResult;
/*
// $encrypted = !empty(\stream_get_meta_data($oClient->ConnectionResource())['crypto']);
$type = $oClient->IsSupported('LOGINDISABLED') ? '' : 'LOGIN'; // RFC3501 6.2.3
$types = [
// 'SCRAM-SHA-256' => 1, // !$encrypted
// 'SCRAM-SHA-1' => 1, // !$encrypted
'CRAM-MD5' => $aSmtpCredentials['UseAuthCramMd5IfSupported'],
'PLAIN' => $aSmtpCredentials['UseAuthPlainIfSupported'],
'OAUTHBEARER' => $aSmtpCredentials['UseAuthOAuth2IfSupported'],
'XOAUTH2' => $aSmtpCredentials['UseAuthOAuth2IfSupported'],
'LOGIN' => 1,
];
foreach ($types as $sasl_type => $active) {
if ($active && $oClient->IsSupported("AUTH={$sasl_type}") && \SnappyMail\SASL::isSupported($sasl_type)) {
$type = $sasl_type;
break;
} }
}
$SASL = \SnappyMail\SASL::factory($type);
$SASL->base64 = true;
// $oClient->Login($SASL);
*/
/*
// $encrypted = !empty(\stream_get_meta_data($oClient->ConnectionResource())['crypto']);
$type = '';
$types = [
'SCRAM-SHA-256' => 1, // !$encrypted
'SCRAM-SHA-1' => 1, // !$encrypted
'CRAM-MD5' => $bUseAuthCramMd5IfSupported,
'PLAIN' => $bUseAuthPlainIfSupported,
'LOGIN' => 1, // $encrypted
'XOAUTH2' => 0
];
foreach ($types as $sasl_type => $active) {
if ($active && $oClient->IsAuthSupported($sasl_type) && \SnappyMail\SASL::isSupported($sasl_type)) {
$type = $sasl_type;
break;
}
}
if (!$type) {
\trigger_error("SMTP {$oClient->GetConnectedHost()} no supported AUTH options. Disable login" . ($oClient->IsSupported('STARTTLS') ? ' or try with STARTTLS' : ''));
$oClient->writeLogException(
new \MailSo\Smtp\Exceptions\LoginBadMethodException,
\MailSo\Log\Enumerations\Type::NOTICE, true);
}
$SASL = \SnappyMail\SASL::factory($type);
$SASL->base64 = true;
*/
}
} }

View file

@ -33,6 +33,9 @@ abstract class SASL
public static function isSupported(string $type) : bool public static function isSupported(string $type) : bool
{ {
if (\preg_match('/^([A-Z2]+)(?:-(.+))?$/Di', $type, $m)) { if (\preg_match('/^([A-Z2]+)(?:-(.+))?$/Di', $type, $m)) {
if ('XOAUTH2' === $m[1] || 'OAUTHBEARER' === $m[1]) {
$m[1] = 'OAUTH';
}
$class = __CLASS__ . "\\{$m[1]}"; $class = __CLASS__ . "\\{$m[1]}";
return \class_exists($class) && $class::isSupported($m[2] ?? ''); return \class_exists($class) && $class::isSupported($m[2] ?? '');
} }

View file

@ -0,0 +1,21 @@
<?php
/**
* https://datatracker.ietf.org/doc/html/rfc7628
* https://developers.google.com/gmail/imap/xoauth2-protocol
*/
namespace SnappyMail\SASL;
class OAuth extends \SnappyMail\SASL
{
public function authenticate(string $username, string $passphrase, ?string $authzid = null) : string
{
return $this->encode("user={$username}\x01auth=Bearer {$passphrase}\x01\x01");
}
public static function isSupported(string $param) : bool
{
return true;
}
}