mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-09-05 15:37:03 +03:00
Resolve #233
This commit is contained in:
parent
76ae57b481
commit
cc383f6b0e
5 changed files with 142 additions and 76 deletions
20
plugins/login-override/LICENSE
Normal file
20
plugins/login-override/LICENSE
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 RainLoop Team
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
63
plugins/login-override/index.php
Normal file
63
plugins/login-override/index.php
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
class LoginOverridePlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||||
|
{
|
||||||
|
const
|
||||||
|
NAME = 'Login Override',
|
||||||
|
VERSION = '2.0',
|
||||||
|
RELEASE = '2023-02-01',
|
||||||
|
REQUIRED = '2.25.2',
|
||||||
|
CATEGORY = 'Filters',
|
||||||
|
DESCRIPTION = 'Override IMAP/SMTP login credentials for specific users.';
|
||||||
|
|
||||||
|
public function Init() : void
|
||||||
|
{
|
||||||
|
$this->addHook('imap.before-login', 'MapImapCredentials');
|
||||||
|
$this->addHook('smtp.before-login', 'MapSmtpCredentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function MapImapCredentials(\RainLoop\Model\Account $oAccount, \MailSo\Imap\ImapClient $oSmtpClient, \MailSo\Imap\Settings $oSettings)
|
||||||
|
{
|
||||||
|
static::MapCredentials($oAccount, $oSettings, $this->Config()->getDecrypted('plugin', 'imap_mapping', ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function MapSmtpCredentials(\RainLoop\Model\Account $oAccount, \MailSo\Smtp\SmtpClient $oSmtpClient, \MailSo\Smtp\Settings $oSettings)
|
||||||
|
{
|
||||||
|
static::MapCredentials($oAccount, $oSettings, $this->Config()->getDecrypted('plugin', 'smtp_mapping', ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function MapCredentials(\RainLoop\Model\Account $oAccount, \MailSo\Net\ConnectSettings $oSettings, string $mapping)
|
||||||
|
{
|
||||||
|
$sEmail = $oAccount->Email();
|
||||||
|
$aList = \preg_split('/\\R/', $mapping);
|
||||||
|
foreach ($aList as $line) {
|
||||||
|
$line = \explode(':', $line, 3);
|
||||||
|
if (!empty($line[0]) && $line[0] === $sEmail) {
|
||||||
|
if (!empty($line[1])) {
|
||||||
|
$oSettings->Login = $line[1];
|
||||||
|
}
|
||||||
|
if (!empty($line[2])) {
|
||||||
|
$oSettings->Password = $line[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configMapping() : array
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
\RainLoop\Plugins\Property::NewInstance('imap_mapping')
|
||||||
|
->SetLabel('IMAP mapping')
|
||||||
|
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT)
|
||||||
|
->SetDescription('Each line as email:loginname:password, loginname or password may be empty to use default')
|
||||||
|
->SetDefaultValue('user@example.com:user1:password1')
|
||||||
|
->SetEncrypted(),
|
||||||
|
\RainLoop\Plugins\Property::NewInstance('smtp_mapping')
|
||||||
|
->SetLabel('SMTP mapping')
|
||||||
|
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT)
|
||||||
|
->SetDescription('Each line as email:loginname:password, loginname or password may be empty to use default')
|
||||||
|
->SetDefaultValue('user@example.com:user1:password1')
|
||||||
|
->SetEncrypted()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -122,12 +122,16 @@ trait AdminExtensions
|
||||||
$aSettings = (array) $this->GetActionParam('settings', []);
|
$aSettings = (array) $this->GetActionParam('settings', []);
|
||||||
foreach ($aMap as $oItem) {
|
foreach ($aMap as $oItem) {
|
||||||
$sKey = $oItem->Name();
|
$sKey = $oItem->Name();
|
||||||
$sValue = $aSettings[$sKey] ?? $oConfig->Get('plugin', $sKey);
|
$mValue = $aSettings[$sKey] ?? $oConfig->Get('plugin', $sKey);
|
||||||
if (PluginPropertyType::PASSWORD !== $oItem->Type() || static::APP_DUMMY !== $sValue) {
|
if (PluginPropertyType::PASSWORD !== $oItem->Type() || static::APP_DUMMY !== $mValue) {
|
||||||
$oItem->SetValue($sValue);
|
$oItem->SetValue($mValue);
|
||||||
$mResultValue = $oItem->Value();
|
$mValue = $oItem->Value();
|
||||||
if (null !== $mResultValue) {
|
if (null !== $mValue) {
|
||||||
$oConfig->Set('plugin', $sKey, $mResultValue);
|
if ($oItem->encrypted) {
|
||||||
|
$oConfig->setEncrypted('plugin', $sKey, $mValue);
|
||||||
|
} else {
|
||||||
|
$oConfig->Set('plugin', $sKey, $mValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,30 +4,15 @@ namespace RainLoop\Config;
|
||||||
|
|
||||||
abstract class AbstractConfig implements \JsonSerializable
|
abstract class AbstractConfig implements \JsonSerializable
|
||||||
{
|
{
|
||||||
/**
|
private string $sFile;
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
private $sFile;
|
|
||||||
|
|
||||||
/**
|
private string $sAdditionalFile = '';
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
private $sAdditionalFile = '';
|
|
||||||
|
|
||||||
/**
|
private array $aData;
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
private $aData;
|
|
||||||
|
|
||||||
/**
|
private bool $bUseApcCache;
|
||||||
* @var bool
|
|
||||||
*/
|
|
||||||
private $bUseApcCache;
|
|
||||||
|
|
||||||
/**
|
private string $sFileHeader;
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
private $sFileHeader;
|
|
||||||
|
|
||||||
public function __construct(string $sFileName, string $sFileHeader = '', string $sAdditionalFileName = '')
|
public function __construct(string $sFileName, string $sFileHeader = '', string $sAdditionalFileName = '')
|
||||||
{
|
{
|
||||||
|
|
@ -78,8 +63,7 @@ abstract class AbstractConfig implements \JsonSerializable
|
||||||
*/
|
*/
|
||||||
public function Set(string $sSectionKey, string $sParamKey, $mParamValue) : void
|
public function Set(string $sSectionKey, string $sParamKey, $mParamValue) : void
|
||||||
{
|
{
|
||||||
if (isset($this->aData[$sSectionKey][$sParamKey][0]))
|
if (isset($this->aData[$sSectionKey][$sParamKey][0])) {
|
||||||
{
|
|
||||||
if (!\is_scalar($mParamValue)) {
|
if (!\is_scalar($mParamValue)) {
|
||||||
$mParamValue = null;
|
$mParamValue = null;
|
||||||
}
|
}
|
||||||
|
|
@ -99,13 +83,26 @@ abstract class AbstractConfig implements \JsonSerializable
|
||||||
$this->aData[$sSectionKey][$sParamKey][0] = (string) $mParamValue;
|
$this->aData[$sSectionKey][$sParamKey][0] = (string) $mParamValue;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
} else if ('custom' === $sSectionKey) {
|
||||||
else if ('custom' === $sSectionKey)
|
|
||||||
{
|
|
||||||
$this->aData[$sSectionKey][$sParamKey] = array((string) $mParamValue);
|
$this->aData[$sSectionKey][$sParamKey] = array((string) $mParamValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getDecrypted(string $sSection, string $sName, $mDefault = null)
|
||||||
|
{
|
||||||
|
// $salt = \basename($this->sFile) not possible due to RainLoop\Plugins\Property
|
||||||
|
return isset($this->aData[$sSection][$sName][0])
|
||||||
|
? \SnappyMail\Crypt::DecryptFromJSON($this->aData[$sSection][$sName][0], \APP_SALT)
|
||||||
|
: $mDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEncrypted(string $sSectionKey, string $sParamKey, $mParamValue) : void
|
||||||
|
{
|
||||||
|
// $salt = \basename($this->sFile) not possible due to RainLoop\Plugins\Property
|
||||||
|
$mParamValue = \SnappyMail\Crypt::EncryptToJSON($mParamValue, \APP_SALT);
|
||||||
|
$this->Set($sSectionKey, $sParamKey, $mParamValue);
|
||||||
|
}
|
||||||
|
|
||||||
private function cacheKey() : string
|
private function cacheKey() : string
|
||||||
{
|
{
|
||||||
return 'config:'.\sha1($this->sFile).':'.\sha1($this->sAdditionalFile).':';
|
return 'config:'.\sha1($this->sFile).':'.\sha1($this->sAdditionalFile).':';
|
||||||
|
|
@ -113,24 +110,20 @@ abstract class AbstractConfig implements \JsonSerializable
|
||||||
|
|
||||||
private function loadDataFromCache() : bool
|
private function loadDataFromCache() : bool
|
||||||
{
|
{
|
||||||
if ($this->bUseApcCache)
|
if ($this->bUseApcCache) {
|
||||||
{
|
|
||||||
$iMTime = \filemtime($this->sFile);
|
$iMTime = \filemtime($this->sFile);
|
||||||
$iMTime = \is_int($iMTime) && 0 < $iMTime ? $iMTime : 0;
|
$iMTime = \is_int($iMTime) && 0 < $iMTime ? $iMTime : 0;
|
||||||
|
|
||||||
$iATime = $this->sAdditionalFile ? \filemtime($this->sAdditionalFile) : 0;
|
$iATime = $this->sAdditionalFile ? \filemtime($this->sAdditionalFile) : 0;
|
||||||
$iATime = \is_int($iATime) && 0 < $iATime ? $iATime : 0;
|
$iATime = \is_int($iATime) && 0 < $iATime ? $iATime : 0;
|
||||||
|
|
||||||
if (0 < $iMTime)
|
if (0 < $iMTime) {
|
||||||
{
|
|
||||||
$sKey = $this->cacheKey();
|
$sKey = $this->cacheKey();
|
||||||
|
|
||||||
$sTimeHash = \apcu_fetch($sKey.'time');
|
$sTimeHash = \apcu_fetch($sKey.'time');
|
||||||
if ($sTimeHash && $sTimeHash === \md5($iMTime.'/'.$iATime))
|
if ($sTimeHash && $sTimeHash === \md5($iMTime.'/'.$iATime)) {
|
||||||
{
|
|
||||||
$aFetchData = \apcu_fetch($sKey.'data');
|
$aFetchData = \apcu_fetch($sKey.'data');
|
||||||
if (\is_array($aFetchData))
|
if (\is_array($aFetchData)) {
|
||||||
{
|
|
||||||
$this->aData = $aFetchData;
|
$this->aData = $aFetchData;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -143,16 +136,14 @@ abstract class AbstractConfig implements \JsonSerializable
|
||||||
|
|
||||||
private function storeDataToCache() : bool
|
private function storeDataToCache() : bool
|
||||||
{
|
{
|
||||||
if ($this->bUseApcCache)
|
if ($this->bUseApcCache) {
|
||||||
{
|
|
||||||
$iMTime = \filemtime($this->sFile);
|
$iMTime = \filemtime($this->sFile);
|
||||||
$iMTime = \is_int($iMTime) && 0 < $iMTime ? $iMTime : 0;
|
$iMTime = \is_int($iMTime) && 0 < $iMTime ? $iMTime : 0;
|
||||||
|
|
||||||
$iATime = $this->sAdditionalFile ? \filemtime($this->sAdditionalFile) : 0;
|
$iATime = $this->sAdditionalFile ? \filemtime($this->sAdditionalFile) : 0;
|
||||||
$iATime = \is_int($iATime) && 0 < $iATime ? $iATime : 0;
|
$iATime = \is_int($iATime) && 0 < $iATime ? $iATime : 0;
|
||||||
|
|
||||||
if (0 < $iMTime)
|
if (0 < $iMTime) {
|
||||||
{
|
|
||||||
$sKey = $this->cacheKey();
|
$sKey = $this->cacheKey();
|
||||||
|
|
||||||
\apcu_store($sKey.'time', \md5($iMTime.'/'.$iATime));
|
\apcu_store($sKey.'time', \md5($iMTime.'/'.$iATime));
|
||||||
|
|
@ -167,8 +158,7 @@ abstract class AbstractConfig implements \JsonSerializable
|
||||||
|
|
||||||
private function clearCache() : bool
|
private function clearCache() : bool
|
||||||
{
|
{
|
||||||
if ($this->bUseApcCache)
|
if ($this->bUseApcCache) {
|
||||||
{
|
|
||||||
$sKey = $this->cacheKey();
|
$sKey = $this->cacheKey();
|
||||||
|
|
||||||
\apcu_delete($sKey.'time');
|
\apcu_delete($sKey.'time');
|
||||||
|
|
@ -186,10 +176,8 @@ abstract class AbstractConfig implements \JsonSerializable
|
||||||
if (!\file_exists($sFile) && \str_ends_with($sFile, '.json')) {
|
if (!\file_exists($sFile) && \str_ends_with($sFile, '.json')) {
|
||||||
$sFile = \str_replace('.json', '.ini', $sFile);
|
$sFile = \str_replace('.json', '.ini', $sFile);
|
||||||
}
|
}
|
||||||
if (\file_exists($sFile) && \is_readable($sFile))
|
if (\file_exists($sFile) && \is_readable($sFile)) {
|
||||||
{
|
if ($this->loadDataFromCache()) {
|
||||||
if ($this->loadDataFromCache())
|
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -198,14 +186,10 @@ abstract class AbstractConfig implements \JsonSerializable
|
||||||
} else {
|
} else {
|
||||||
$aData = \parse_ini_file($sFile, true);
|
$aData = \parse_ini_file($sFile, true);
|
||||||
}
|
}
|
||||||
if ($aData && \count($aData))
|
if ($aData && \count($aData)) {
|
||||||
{
|
foreach ($aData as $sSectionKey => $aSectionValue) {
|
||||||
foreach ($aData as $sSectionKey => $aSectionValue)
|
if (\is_array($aSectionValue)) {
|
||||||
{
|
foreach ($aSectionValue as $sParamKey => $mParamValue) {
|
||||||
if (\is_array($aSectionValue))
|
|
||||||
{
|
|
||||||
foreach ($aSectionValue as $sParamKey => $mParamValue)
|
|
||||||
{
|
|
||||||
$this->Set($sSectionKey, $sParamKey, $mParamValue);
|
$this->Set($sSectionKey, $sParamKey, $mParamValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -213,21 +197,16 @@ abstract class AbstractConfig implements \JsonSerializable
|
||||||
|
|
||||||
unset($aData);
|
unset($aData);
|
||||||
|
|
||||||
if (\file_exists($this->sAdditionalFile) && \is_readable($this->sAdditionalFile))
|
if (\file_exists($this->sAdditionalFile) && \is_readable($this->sAdditionalFile)) {
|
||||||
{
|
|
||||||
if (\str_ends_with($this->sAdditionalFile, '.json')) {
|
if (\str_ends_with($this->sAdditionalFile, '.json')) {
|
||||||
$aData = \json_decode(\file_get_contents($this->sAdditionalFile), true);
|
$aData = \json_decode(\file_get_contents($this->sAdditionalFile), true);
|
||||||
} else {
|
} else {
|
||||||
$aData = \parse_ini_file($this->sAdditionalFile, true);
|
$aData = \parse_ini_file($this->sAdditionalFile, true);
|
||||||
}
|
}
|
||||||
if ($aData && \count($aData))
|
if ($aData && \count($aData)) {
|
||||||
{
|
foreach ($aData as $sSectionKey => $aSectionValue) {
|
||||||
foreach ($aData as $sSectionKey => $aSectionValue)
|
if (\is_array($aSectionValue)) {
|
||||||
{
|
foreach ($aSectionValue as $sParamKey => $mParamValue) {
|
||||||
if (\is_array($aSectionValue))
|
|
||||||
{
|
|
||||||
foreach ($aSectionValue as $sParamKey => $mParamValue)
|
|
||||||
{
|
|
||||||
$this->Set($sSectionKey, $sParamKey, $mParamValue);
|
$this->Set($sSectionKey, $sParamKey, $mParamValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -248,8 +227,7 @@ abstract class AbstractConfig implements \JsonSerializable
|
||||||
|
|
||||||
public function Save() : bool
|
public function Save() : bool
|
||||||
{
|
{
|
||||||
if (\file_exists($this->sFile) && !\is_writable($this->sFile))
|
if (\file_exists($this->sFile) && !\is_writable($this->sFile)) {
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ class Property implements \JsonSerializable
|
||||||
|
|
||||||
private string $sPlaceholder = '';
|
private string $sPlaceholder = '';
|
||||||
|
|
||||||
|
public bool $encrypted = false;
|
||||||
|
|
||||||
function __construct(string $sName)
|
function __construct(string $sName)
|
||||||
{
|
{
|
||||||
$this->sName = $sName;
|
$this->sName = $sName;
|
||||||
|
|
@ -43,7 +45,6 @@ class Property implements \JsonSerializable
|
||||||
public function SetType(int $iType) : self
|
public function SetType(int $iType) : self
|
||||||
{
|
{
|
||||||
$this->iType = (int) $iType;
|
$this->iType = (int) $iType;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,42 +94,42 @@ class Property implements \JsonSerializable
|
||||||
} else {
|
} else {
|
||||||
$this->mDefaultValue = $mDefaultValue;
|
$this->mDefaultValue = $mDefaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function SetOptions(array $aOptions) : self
|
public function SetOptions(array $aOptions) : self
|
||||||
{
|
{
|
||||||
$this->aOptions = $aOptions;
|
$this->aOptions = $aOptions;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function SetPlaceholder(string $sPlaceholder) : self
|
public function SetPlaceholder(string $sPlaceholder) : self
|
||||||
{
|
{
|
||||||
$this->sPlaceholder = $sPlaceholder;
|
$this->sPlaceholder = $sPlaceholder;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function SetLabel(string $sLabel) : self
|
public function SetLabel(string $sLabel) : self
|
||||||
{
|
{
|
||||||
$this->sLabel = $sLabel;
|
$this->sLabel = $sLabel;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function SetDescription(string $sDesc) : self
|
public function SetDescription(string $sDesc) : self
|
||||||
{
|
{
|
||||||
$this->sDesc = $sDesc;
|
$this->sDesc = $sDesc;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function SetAllowedInJs(bool $bValue = true) : self
|
public function SetAllowedInJs(bool $bValue = true) : self
|
||||||
{
|
{
|
||||||
$this->bAllowedInJs = $bValue;
|
$this->bAllowedInJs = $bValue;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function SetEncrypted(bool $bValue = true) : self
|
||||||
|
{
|
||||||
|
$this->encrypted = $bValue;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -188,7 +189,7 @@ class Property implements \JsonSerializable
|
||||||
{
|
{
|
||||||
return array(
|
return array(
|
||||||
'@Object' => 'Object/PluginProperty',
|
'@Object' => 'Object/PluginProperty',
|
||||||
'value' => $this->mValue,
|
'value' => $this->encrypted ? \SnappyMail\Crypt::DecryptFromJSON($this->mValue, \APP_SALT) : $this->mValue,
|
||||||
'placeholder' => $this->sPlaceholder,
|
'placeholder' => $this->sPlaceholder,
|
||||||
'name' => $this->sName,
|
'name' => $this->sName,
|
||||||
'type' => $this->iType,
|
'type' => $this->iType,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue