mirror of
https://github.com/the-djmaze/snappymail.git
synced 2024-12-26 00:51:24 +08:00
Resolve #571 by allowing to give an account a name/label
This commit is contained in:
parent
2ee9d973e2
commit
decbbd8817
13 changed files with 168 additions and 122 deletions
|
@ -152,41 +152,29 @@ export class AppUser extends AbstractApp {
|
|||
IdentityUserStore.loading(false);
|
||||
|
||||
if (!iError) {
|
||||
const
|
||||
// counts = {},
|
||||
accounts = oData.Result.Accounts,
|
||||
mainEmail = SettingsGet('MainEmail');
|
||||
let items = oData.Result.Accounts;
|
||||
AccountUserStore(isArray(items)
|
||||
? items.map(oValue => new AccountModel(oValue.email, oValue.name))
|
||||
: []
|
||||
);
|
||||
AccountUserStore.unshift(new AccountModel(SettingsGet('MainEmail'), '', false));
|
||||
|
||||
if (isArray(accounts)) {
|
||||
// AccountUserStore.accounts.forEach(oAccount => counts[oAccount.email] = oAccount.count());
|
||||
|
||||
AccountUserStore.accounts(
|
||||
accounts.map(
|
||||
sValue => new AccountModel(sValue/*, counts[sValue]*/)
|
||||
)
|
||||
);
|
||||
// accounts.length &&
|
||||
AccountUserStore.accounts.unshift(new AccountModel(mainEmail/*, counts[mainEmail]*/, false));
|
||||
}
|
||||
|
||||
if (isArray(oData.Result.Identities)) {
|
||||
IdentityUserStore(
|
||||
oData.Result.Identities.map(identityData => {
|
||||
const identity = new IdentityModel(
|
||||
pString(identityData.Id),
|
||||
pString(identityData.Email)
|
||||
);
|
||||
|
||||
identity.name(pString(identityData.Name));
|
||||
identity.replyTo(pString(identityData.ReplyTo));
|
||||
identity.bcc(pString(identityData.Bcc));
|
||||
identity.signature(pString(identityData.Signature));
|
||||
identity.signatureInsertBefore(!!identityData.SignatureInsertBefore);
|
||||
|
||||
return identity;
|
||||
})
|
||||
);
|
||||
}
|
||||
items = oData.Result.Identities;
|
||||
IdentityUserStore(isArray(items)
|
||||
? items.map(identityData => {
|
||||
const identity = new IdentityModel(
|
||||
pString(identityData.Id),
|
||||
pString(identityData.Email)
|
||||
);
|
||||
identity.name(pString(identityData.Name));
|
||||
identity.replyTo(pString(identityData.ReplyTo));
|
||||
identity.bcc(pString(identityData.Bcc));
|
||||
identity.signature(pString(identityData.Signature));
|
||||
identity.signatureInsertBefore(!!identityData.SignatureInsertBefore);
|
||||
return identity;
|
||||
})
|
||||
: []
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -7,11 +7,14 @@ export class AccountModel extends AbstractModel {
|
|||
* @param {boolean=} canBeDelete = true
|
||||
* @param {number=} count = 0
|
||||
*/
|
||||
constructor(email/*, count = 0*/, isAdditional = true) {
|
||||
constructor(email, name/*, count = 0*/, isAdditional = true) {
|
||||
super();
|
||||
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
|
||||
this.displayName = name ? name + ' <' + email + '>' : email;
|
||||
|
||||
addObservablesTo(this, {
|
||||
// count: count || 0,
|
||||
askDelete: false,
|
||||
|
|
|
@ -101,7 +101,7 @@ export class MailBoxUserScreen extends AbstractScreen {
|
|||
FolderUserStore.foldersInboxUnreadCount(e.detail);
|
||||
/* // Disabled in SystemDropDown.html
|
||||
const email = AccountUserStore.email();
|
||||
AccountUserStore.accounts.forEach(item =>
|
||||
AccountUserStore.forEach(item =>
|
||||
email === item?.email && item?.count(e.detail)
|
||||
);
|
||||
*/
|
||||
|
|
|
@ -16,7 +16,7 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
|
|||
this.allowAdditionalAccount = SettingsCapa('AdditionalAccounts');
|
||||
this.allowIdentities = SettingsCapa('Identities');
|
||||
|
||||
this.accounts = AccountUserStore.accounts;
|
||||
this.accounts = AccountUserStore;
|
||||
this.loading = AccountUserStore.loading;
|
||||
this.identities = IdentityUserStore;
|
||||
this.mainEmail = SettingsGet('MainEmail');
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { addObservablesTo, koArrayWithDestroy } from 'External/ko';
|
||||
|
||||
export const AccountUserStore = {
|
||||
accounts: koArrayWithDestroy(),
|
||||
loading: ko.observable(false).extend({ debounce: 100 }),
|
||||
export const AccountUserStore = koArrayWithDestroy();
|
||||
|
||||
getEmailAddresses: () => AccountUserStore.accounts.map(item => item.email)
|
||||
};
|
||||
AccountUserStore.loading = ko.observable(false).extend({ debounce: 100 });
|
||||
|
||||
AccountUserStore.getEmailAddresses = () => AccountUserStore.map(item => item.email);
|
||||
|
||||
addObservablesTo(AccountUserStore, {
|
||||
email: '',
|
||||
|
|
|
@ -12,6 +12,7 @@ export class AccountPopupView extends AbstractViewPopup {
|
|||
addObservablesTo(this, {
|
||||
isNew: true,
|
||||
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
|
||||
|
@ -40,18 +41,17 @@ export class AccountPopupView extends AbstractViewPopup {
|
|||
}
|
||||
}
|
||||
|
||||
onShow(account) {
|
||||
if (account?.isAdditional()) {
|
||||
this.isNew(false);
|
||||
this.email(account.email);
|
||||
} else {
|
||||
this.isNew(true);
|
||||
this.email('');
|
||||
}
|
||||
onHide() {
|
||||
this.password('');
|
||||
|
||||
this.submitRequest(false);
|
||||
this.submitError('');
|
||||
this.submitErrorAdditional('');
|
||||
}
|
||||
|
||||
onShow(account) {
|
||||
let edit = account?.isAdditional();
|
||||
this.isNew(!edit);
|
||||
this.name(edit ? account.name : '');
|
||||
this.email(edit ? account.email : '');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,11 +30,11 @@ export class SystemDropDownUserView extends AbstractViewRight {
|
|||
|
||||
this.accountEmail = AccountUserStore.email;
|
||||
|
||||
this.accounts = AccountUserStore.accounts;
|
||||
this.accounts = AccountUserStore;
|
||||
this.accountsLoading = AccountUserStore.loading;
|
||||
/*
|
||||
this.accountsUnreadCount = : koComputable(() => 0);
|
||||
this.accountsUnreadCount = : koComputable(() => AccountUserStore.accounts().reduce((result, item) => result + item.count(), 0));
|
||||
this.accountsUnreadCount = : koComputable(() => AccountUserStore().reduce((result, item) => result + item.count(), 0));
|
||||
*/
|
||||
|
||||
addObservablesTo(this, {
|
||||
|
@ -91,8 +91,10 @@ export class SystemDropDownUserView extends AbstractViewRight {
|
|||
return true;
|
||||
}
|
||||
|
||||
emailTitle() {
|
||||
return AccountUserStore.email();
|
||||
accountName() {
|
||||
let email = AccountUserStore.email(),
|
||||
account = AccountUserStore.find(account => account.email == email);
|
||||
return account?.name || email;
|
||||
}
|
||||
|
||||
settingsClick() {
|
||||
|
|
|
@ -41,7 +41,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;
|
||||
}
|
||||
|
@ -84,6 +85,7 @@ trait Accounts
|
|||
|
||||
$sEmail = \trim($this->GetActionParam('Email', ''));
|
||||
$sPassword = $this->GetActionParam('Password', '');
|
||||
$sName = $this->GetActionParam('Name', '');
|
||||
$bNew = '1' === (string)$this->GetActionParam('New', '1');
|
||||
|
||||
$sEmail = \MailSo\Base\Utils::IdnToAscii($sEmail, true);
|
||||
|
@ -93,10 +95,17 @@ 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__);
|
||||
}
|
||||
|
@ -241,11 +250,16 @@ trait Accounts
|
|||
*/
|
||||
public function DoAccountsAndIdentities(): array
|
||||
{
|
||||
// https://github.com/the-djmaze/snappymail/issues/571
|
||||
return $this->DefaultResponse(__FUNCTION__, array(
|
||||
'Accounts' => \array_map(
|
||||
'MailSo\\Base\\Utils::IdnToUtf8',
|
||||
\array_keys($this->GetAccounts($this->getMainAccountFromToken()))
|
||||
),
|
||||
'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())
|
||||
));
|
||||
}
|
||||
|
|
|
@ -8,41 +8,30 @@ use RainLoop\Exceptions\ClientException;
|
|||
|
||||
abstract class Account implements \JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sEmail;
|
||||
private string $sName = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sLogin;
|
||||
private string $sEmail = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sPassword;
|
||||
private string $sLogin = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sProxyAuthUser = '';
|
||||
private string $sPassword = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $sProxyAuthPassword = '';
|
||||
private string $sProxyAuthUser = '';
|
||||
|
||||
/**
|
||||
* @var \RainLoop\Model\Domain
|
||||
*/
|
||||
private $oDomain;
|
||||
private string $sProxyAuthPassword = '';
|
||||
|
||||
private Domain $oDomain;
|
||||
|
||||
public function Email() : string
|
||||
{
|
||||
return $this->sEmail;
|
||||
}
|
||||
|
||||
public function Name() : string
|
||||
{
|
||||
return $this->sName;
|
||||
}
|
||||
|
||||
public function ProxyAuthUser() : string
|
||||
{
|
||||
return $this->sProxyAuthUser;
|
||||
|
@ -123,15 +112,21 @@ abstract class Account implements \JsonSerializable
|
|||
#[\ReturnTypeWillChange]
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return array(
|
||||
'account', // 0
|
||||
$this->sEmail, // 1
|
||||
$this->sLogin, // 2
|
||||
$this->sPassword, // 3
|
||||
'', // 4 sClientCert
|
||||
$this->sProxyAuthUser, // 5
|
||||
$this->sProxyAuthPassword // 6
|
||||
);
|
||||
$result = [
|
||||
// 'account', // 0
|
||||
'email' => $this->sEmail, // 1
|
||||
'login' => $this->sLogin, // 2
|
||||
'pass' => $this->sPassword, // 3
|
||||
// '', // 4 sClientCert
|
||||
'name' => $this->sName
|
||||
];
|
||||
if ($this->sProxyAuthUser && $this->sProxyAuthPassword) {
|
||||
$result['proxy'] = [
|
||||
'user' => $this->sProxyAuthUser, // 5
|
||||
'pass' => $this->sProxyAuthPassword // 6
|
||||
];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function NewInstanceFromCredentials(\RainLoop\Actions $oActions,
|
||||
|
@ -165,34 +160,60 @@ abstract class Account implements \JsonSerializable
|
|||
return $oAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts old numeric array to new associative array
|
||||
*/
|
||||
public static function convertArray(array $aAccount) : array
|
||||
{
|
||||
if (isset($aAccount['email'])) {
|
||||
return $aAccount;
|
||||
}
|
||||
if (empty($aAccount[0]) || 'account' != $aAccount[0] || 7 > \count($aAccount)) {
|
||||
return [];
|
||||
}
|
||||
$aResult = [
|
||||
'email' => $aAccount[1] ?: '',
|
||||
'login' => $aAccount[2] ?: '',
|
||||
'pass' => $aAccount[3] ?: ''
|
||||
];
|
||||
if ($aAccount[5] && $aAccount[6]) {
|
||||
$aResult['proxy'] = [
|
||||
'user' => $aAccount[5],
|
||||
'pass' => $aAccount[6]
|
||||
];
|
||||
}
|
||||
return $aResult;
|
||||
}
|
||||
|
||||
public static function NewInstanceFromTokenArray(
|
||||
\RainLoop\Actions $oActions,
|
||||
array $aAccountHash,
|
||||
bool $bThrowExceptionOnFalse = false): ?self
|
||||
{
|
||||
if (!empty($aAccountHash[0]) && 'account' === $aAccountHash[0] && 7 <= \count($aAccountHash)) {
|
||||
$oAccount = null;
|
||||
$aAccountHash = static::convertArray($aAccountHash);
|
||||
if (!empty($aAccountHash['email']) && 3 <= \count($aAccountHash)) {
|
||||
$oAccount = static::NewInstanceFromCredentials(
|
||||
$oActions,
|
||||
$aAccountHash[1] ?: '',
|
||||
$aAccountHash[2] ?: '',
|
||||
$aAccountHash[3] ?: '',
|
||||
$aAccountHash['email'],
|
||||
$aAccountHash['login'],
|
||||
$aAccountHash['pass'],
|
||||
$bThrowExceptionOnFalse
|
||||
);
|
||||
|
||||
if ($oAccount) {
|
||||
// init proxy user/password
|
||||
if (!empty($aAccountHash[5]) && !empty($aAccountHash[6])) {
|
||||
$oAccount->SetProxyAuthUser($aAccountHash[5]);
|
||||
$oAccount->SetProxyAuthPassword($aAccountHash[6]);
|
||||
if (isset($aAccountHash['name'])) {
|
||||
$oAccount->sName = $aAccountHash['name'];
|
||||
}
|
||||
// init proxy user/password
|
||||
if (isset($aAccountHash['proxy'])) {
|
||||
$oAccount->sProxyAuthUser = $aAccountHash['proxy']['user'];
|
||||
$oAccount->sProxyAuthPassword = $aAccountHash['proxy']['pass'];
|
||||
}
|
||||
|
||||
$oActions->Logger()->AddSecret($oAccount->Password());
|
||||
$oActions->Logger()->AddSecret($oAccount->ProxyAuthPassword());
|
||||
|
||||
return $oAccount;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return $oAccount;
|
||||
}
|
||||
|
||||
public function ImapConnectAndLoginHelper(\RainLoop\Plugins\Manager $oPlugins, \MailSo\Mail\MailClient $oMailClient, \RainLoop\Config\Application $oConfig) : bool
|
||||
|
|
|
@ -17,13 +17,25 @@ class AdditionalAccount extends Account
|
|||
return \md5(parent::Hash() . $this->ParentEmail());
|
||||
}
|
||||
|
||||
public static function convertArray(array $aAccount) : array
|
||||
{
|
||||
$aResult = parent::convertArray($aAccount);
|
||||
$iCount = \count($aAccount);
|
||||
if ($aResult && 7 < $iCount && 9 >= $iCount) {
|
||||
$aResult['hmac'] = \array_pop($aAccount);
|
||||
}
|
||||
return $aResult;
|
||||
}
|
||||
|
||||
public function asTokenArray(MainAccount $oMainAccount) : array
|
||||
{
|
||||
$sHash = $oMainAccount->CryptKey();
|
||||
$aData = $this->jsonSerialize();
|
||||
$aData[3] = \SnappyMail\Crypt::EncryptUrlSafe($aData[3], $sHash); // sPassword
|
||||
$aData[6] = \SnappyMail\Crypt::EncryptUrlSafe($aData[6], $sHash); // sProxyAuthPassword
|
||||
$aData[] = \hash_hmac('sha1', $aData[3], $sHash);
|
||||
$aData['pass'] = \SnappyMail\Crypt::EncryptUrlSafe($aData['pass'], $sHash); // sPassword
|
||||
if (isset($aAccountHash['proxy'])) {
|
||||
$aData['proxy']['pass'] = \SnappyMail\Crypt::EncryptUrlSafe($aData['proxy']['pass'], $sHash); // sProxyAuthPassword
|
||||
}
|
||||
$aData['hmac'] = \hash_hmac('sha1', $aData['pass'], $sHash);
|
||||
return $aData;
|
||||
}
|
||||
|
||||
|
@ -32,13 +44,16 @@ class AdditionalAccount extends Account
|
|||
array $aAccountHash,
|
||||
bool $bThrowExceptionOnFalse = false) : ?Account /* PHP7.4: ?self*/
|
||||
{
|
||||
$iCount = \count($aAccountHash);
|
||||
if (!empty($aAccountHash[0]) && 'account' === $aAccountHash[0] && 7 <= $iCount && 9 >= $iCount) {
|
||||
$aAccountHash = static::convertArray($aAccountHash);
|
||||
if (!empty($aAccountHash['email'])) {
|
||||
$sHash = $oActions->getMainAccountFromToken()->CryptKey();
|
||||
$sPasswordHMAC = (7 < $iCount) ? \array_pop($aAccountHash) : null;
|
||||
if ($sPasswordHMAC && $sPasswordHMAC === \hash_hmac('sha1', $aAccountHash[3], $sHash)) {
|
||||
$aAccountHash[3] = \SnappyMail\Crypt::DecryptUrlSafe($aAccountHash[3], $sHash);
|
||||
$aAccountHash[6] = \SnappyMail\Crypt::DecryptUrlSafe($aAccountHash[6], $sHash);
|
||||
// hmac only set when asTokenArray() was used
|
||||
$sPasswordHMAC = $aAccountHash['hmac'] ?? null;
|
||||
if ($sPasswordHMAC && $sPasswordHMAC === \hash_hmac('sha1', $aAccountHash['pass'], $sHash)) {
|
||||
$aAccountHash['pass'] = \SnappyMail\Crypt::DecryptUrlSafe($aAccountHash['pass'], $sHash);
|
||||
if (isset($aAccountHash['proxy'])) {
|
||||
$aAccountHash['proxy']['pass'] = \SnappyMail\Crypt::DecryptUrlSafe($aAccountHash['proxy']['pass'], $sHash);
|
||||
}
|
||||
}
|
||||
return parent::NewInstanceFromTokenArray($oActions, $aAccountHash, $bThrowExceptionOnFalse);
|
||||
}
|
||||
|
|
|
@ -19,7 +19,11 @@
|
|||
<div class="control-group">
|
||||
<label data-i18n="GLOBAL/PASSWORD"></label>
|
||||
<input name="Password" type="password" class="input-xlarge" autocomplete="new-password" autocorrect="off" autocapitalize="off"
|
||||
required="" data-bind="value: password">
|
||||
data-bind="value: password, attr: {required:isNew}">
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label data-i18n="GLOBAL/NAME"></label>
|
||||
<input name="Name" type="text" class="input-xlarge" data-bind="value: name">
|
||||
</div>
|
||||
</form>
|
||||
<footer>
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
<i class="fontastic drag-handle">⬍</i>
|
||||
</td>
|
||||
<td class="e-action">
|
||||
<span class="account-name" data-bind="text: email"></span>
|
||||
<span class="account-name" data-bind="text: displayName"></span>
|
||||
</td>
|
||||
<td>
|
||||
<a class="btn btn-small btn-danger button-confirm-delete" data-bind="css: {'delete-access': askDelete}, click: function(oAccount) { $root.deleteAccount(oAccount); }"
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="playIcon"><div></div></div>
|
||||
<i class="stopIcon fontastic">⏸</i>
|
||||
</div>
|
||||
<div class="accountPlace hide-mobile" data-bind="text: emailTitle()"></div>
|
||||
<div class="accountPlace hide-mobile" data-bind="text: accountName(), title: accountEmail"></div>
|
||||
<div class="btn-group dropdown" data-bind="registerBootstrapDropdown: true, openDropdownTrigger: accountMenuDropdownTrigger">
|
||||
<a id="top-system-dropdown-id" href="#" tabindex="-1" class="btn single btn-block dropdown-toggle">
|
||||
<i style="vertical-align:middle" class="fontastic" data-bind="css: {'icon-spinner': accountsLoading()}">👤</i>
|
||||
|
@ -24,7 +24,7 @@
|
|||
<span data-bind="visible: 99 < count()">99+</span>
|
||||
</b>-->
|
||||
<i class="fontastic" data-bind="text: $root.accountEmail() === email ? '✔' : '👤'"></i>
|
||||
<span class="email-title" data-bind="text: email, attr: {title: email}"></span>
|
||||
<span class="email-title" data-bind="text: name || email, attr: {title: email}"></span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- /ko -->
|
||||
|
|
Loading…
Reference in a new issue