Resolve #571 by allowing to give an account a name/label

This commit is contained in:
the-djmaze 2022-11-08 17:40:12 +01:00
parent 2ee9d973e2
commit decbbd8817
13 changed files with 168 additions and 122 deletions

View file

@ -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;
})
: []
);
}
});
}

View file

@ -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,

View file

@ -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)
);
*/

View file

@ -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');

View file

@ -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: '',

View file

@ -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 : '');
}
}

View file

@ -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() {

View file

@ -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())
));
}

View file

@ -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

View file

@ -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);
}

View file

@ -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>

View file

@ -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); }"

View file

@ -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 -->