Merge branch 'master' into plugin-2fa

This commit is contained in:
djmaze 2021-07-14 12:51:54 +02:00
commit 7985eaa23a
303 changed files with 51374 additions and 11952 deletions

View file

@ -34,8 +34,6 @@ module.exports = {
'Crossroads': "readonly",
// vendors/jua
'Jua': "readonly",
// vendors/qr.js
'qr': "readonly",
// vendors/bootstrap/bootstrap.native.js
'BSN': "readonly"
},

View file

@ -28,6 +28,9 @@ RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301]
Header set X-XSS-Protection "1; mode=block"
Header set Service-Worker-Allowed "/"
# Google FLoC
Header set Permissions-Policy "interest-cohort=()"
RewriteCond %{HTTP:Accept-encoding} br
RewriteCond "%{REQUEST_FILENAME}\.br" -s
RewriteRule "^(.+)" "$1\.br" [L,T=text/javascript,QSA]

View file

@ -1,75 +1,75 @@
#!make
rebuild: _down
docker-compose build --no-cache
docker compose build --no-cache
up: _up status
_up:
docker-compose up -d
docker compose up -d
stop: _stop status
_stop:
docker-compose stop
docker compose stop
down: _down status
_down:
docker-compose down
docker compose down
restart: _stop _up status
status:
@docker-compose ps
@docker compose ps
tx:
@docker-compose run --no-deps --rm tx tx pull -a -s -f -d
@docker compose run --no-deps --rm tx tx pull -a -s -f -d
console-node:
@docker-compose run --no-deps --rm node sh
@docker compose run --no-deps --rm node sh
console-tx:
@docker-compose run --no-deps --rm tx sh
@docker compose run --no-deps --rm tx sh
console-php:
@docker-compose exec php sh
@docker compose exec php sh
console: console-node
logs:
@docker-compose logs --tail=100 -f
@docker compose logs --tail=100 -f
logs-db:
@docker-compose logs --tail=100 -f db
@docker compose logs --tail=100 -f db
logs-php:
@docker-compose logs --tail=100 -f php
@docker compose logs --tail=100 -f php
logs-node:
@docker-compose logs --tail=100 -f node
@docker compose logs --tail=100 -f node
logs-nginx:
@docker-compose logs --tail=100 -f nginx
@docker compose logs --tail=100 -f nginx
logs-mail:
@docker-compose logs --tail=100 -f mail
@docker compose logs --tail=100 -f mail
logs-tx:
@docker-compose logs --tail=100 -f tx
@docker compose logs --tail=100 -f tx
rl-lint:
@docker-compose run --no-deps --rm node gulp lint
@docker compose run --no-deps --rm node gulp lint
rl-dev:
@docker-compose run --no-deps --rm node npm run watch-js
@docker compose run --no-deps --rm node npm run watch-js
rl-compile:
@docker-compose run --no-deps --rm node gulp build
@docker compose run --no-deps --rm node gulp build
rl-compile-with-source:
@docker-compose run --no-deps --rm node gulp build --source
@docker compose run --no-deps --rm node gulp build --source
rl-watch-css:
@docker-compose run --no-deps --rm node npm run watch-css
@docker compose run --no-deps --rm node npm run watch-css
rl-watch-js:
@docker-compose run --no-deps --rm node npm run watch-js
@docker compose run --no-deps --rm node npm run watch-js
rl-build:
@docker-compose run --no-deps --rm node gulp all
@docker compose run --no-deps --rm node gulp all
rl-build-pro:
@docker-compose run --no-deps --rm node gulp all --pro
@docker compose run --no-deps --rm node gulp all --pro
yarn-install:
@docker-compose run --no-deps --rm node yarn install
@docker compose run --no-deps --rm node yarn install
yarn-outdated:
@docker-compose run --no-deps --rm node yarn outdated
@docker compose run --no-deps --rm node yarn outdated
yarn-upgrade:
@docker-compose run --no-deps --rm node yarn upgrade-interactive --exact --latest
@docker compose run --no-deps --rm node yarn upgrade-interactive --exact --latest
gpg:
docker run -it --rm -w=/var/www \

View file

@ -1,6 +1,6 @@
<div align="center">
<a href="https://github.com/the-djmaze/snappymail">
<img width="200" heigth="200" src="https://snappymail.eu/static/img/logo-256x256.png">
<img src="https://snappymail.eu/static/img/logo-256x256.png">
</a>
<br>
<h1>SnappyMail</h1>
@ -71,6 +71,9 @@ This fork of RainLoop has the following changes:
* Replaced webpack with rollup
* No user-agent detection (use device width)
* Added support to load plugins as .phar
* Replaced old Sabre library
* AddressBook Contacts support MySQL/MariaDB utf8mb4
* Prevent Google FLoC
### Removal of old JavaScript
@ -116,25 +119,30 @@ RainLoop 1.15 vs SnappyMail
|js/* |RainLoop |Snappy |
|--------------- |--------: |--------: |
|admin.js |2.158.025 | 94.597 |
|app.js |4.215.733 | 460.014 |
|boot.js | 672.433 | 4.554 |
|libs.js | 647.679 | 218.511 |
|admin.js |2.158.025 | 91.664 |
|app.js |4.215.733 | 453.928 |
|boot.js | 672.433 | 3.147 |
|libs.js | 647.679 | 218.466 |
|polyfills.js | 325.908 | 0 |
|serviceworker.js | 0 | 285 |
|TOTAL |8.019.778 | 777.961 |
|TOTAL |8.019.778 | 767.490 |
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|admin.min.js | 255.514 | 49.908 | 73.899 | 14.781 | 60.674 | 13.233 |
|app.min.js | 516.000 | 236.129 |140.430 | 69.396 |110.657 | 58.601 |
|boot.min.js | 66.456 | 2.442 | 22.553 | 1.371 | 20.043 | 1.178 |
|libs.min.js | 574.626 | 106.574 |177.280 | 38.600 |151.855 | 34.532 |
|polyfills.min.js | 32.608 | 0 | 11.315 | 0 | 10.072 | 0 |
|TOTAL |1.445.204 | 395.053 |425.477 |124.148 |353.301 |107.544 |
|TOTAL (no admin) |1.189.690 | 345.145 |351.061 |109.367 |292.627 | 94.311 |
|admin.min.js | 255.514 | 47.908 | 73.899 | 14.313 | 60.674 | 12.787 |
|app.min.js | 516.000 | 233.532 |140.430 | 68.577 |110.657 | 57.878 |
|boot.min.js | 66.456 | 1.751 | 22.553 | 1.025 | 20.043 | 858 |
|libs.min.js | 574.626 | 106.529 |177.280 | 38.607 |151.855 | 34.567 |
|polyfills.min.js | 32.608 | 0 | 11.315 | 0 | 10.072 | 0 |
|TOTAL |1.445.204 | 389.720 |425.477 |122.522 |353.301 |106.090 |
|TOTAL (no admin) |1.189.690 | 341.812 |351.061 |108.209 |292.627 | 93.303 |
For a user its around 66% smaller and faster than traditional RainLoop.
For a user its around 68% smaller and faster than traditional RainLoop.
|OpenPGP |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|openpgp.min.js | 330.742 | 293.972 |102.388 | 93.030 | 84.241 | 77.142 |
|openpgp.worker | 1.499 | 1.125 | 824 | 567 | 695 | 467 |
### CSS changes
@ -166,12 +174,12 @@ For a user its around 66% smaller and faster than traditional RainLoop.
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|------------ |-------: |-------: |------: |------: |--------: |
|app.css | 340.334 | 107.026 | 46,959 | 18.779 | 16.133 |
|app.min.css | 274.791 | 88.207 | 39.618 | 16.817 | 14.816 |
|boot.css | | 2.066 | | 913 | 742 |
|boot.min.css | | 1.696 | | 818 | 664 |
|admin.css | | 46.326 | | 9.219 | 8.067 |
|admin.min.css | | 37.240 | | 8.178 | 7.268 |
|app.css | 340.334 | 100.825 | 46,959 | 18.002 | 15.540 |
|app.min.css | 274.791 | 82.684 | 39.618 | 16.138 | 14.234 |
|boot.css | | 1.326 | | 664 | 545 |
|boot.min.css | | 1.071 | | 590 | 474 |
|admin.css | | 42.217 | | 8.714 | 7.602 |
|admin.min.css | | 33.576 | | 7.680 | 6.810 |
### Squire vs CKEditor

View file

@ -10,6 +10,9 @@
// Uncomment to enable multiple domain installation.
//define('MULTIDOMAIN', 1);
// Uncomment to disable APCU.
//define('APP_USE_APCU_CACHE', false);
/**
* Custom 'data' folder path
* @return string

View file

@ -28,7 +28,6 @@ export class AbstractApp {
logoutReload(close = false) {
const url = logoutLink();
rl.hash.clear();
close && window.close && window.close();
if (location.href !== url) {

View file

@ -18,9 +18,6 @@ class AdminApp extends AbstractApp {
bootstart() {
super.bootstart();
this.hideLoading();
if (!Settings.app('allowAdminPanel')) {
rl.route.root();
setTimeout(() => location.href = '/', 1);
@ -30,8 +27,7 @@ class AdminApp extends AbstractApp {
} else {
startScreens([LoginAdminScreen]);
}
progressJs.end();
this.hideLoading();
}
}

View file

@ -5,6 +5,7 @@ import { isPosNumeric, delegateRunOnDestroy, mailToHelper } from 'Common/UtilsUs
import {
Capa,
Notification,
Scope
} from 'Common/Enums';
@ -100,9 +101,6 @@ class AppUser extends AbstractApp {
setInterval(() => {
const currentTime = Date.now();
if (currentTime > (lastTime + interval + 1000)) {
if (rl.hash.check()) {
this.reload();
}
Remote.jsVersion(iError => {
if (100 < iError) {
this.reload();
@ -112,10 +110,6 @@ class AppUser extends AbstractApp {
lastTime = currentTime;
}, interval);
if (rl.hash.check()) {
this.reload();
}
if (SettingsGet('UserBackgroundHash')) {
setTimeout(() => {
const img = userBackground(SettingsGet('UserBackgroundHash'));
@ -886,12 +880,7 @@ class AppUser extends AbstractApp {
}
logout() {
Remote.logout(() => this.logoutReload((SettingsGet('ParentEmail')||{length:0}).length));
}
bootend() {
progressJs.end();
this.hideLoading();
Remote.logout(() => rl.logoutReload((SettingsGet('ParentEmail')||{length:0}).length));
}
bootstart() {
@ -907,16 +896,12 @@ class AppUser extends AbstractApp {
const startupUrl = pString(SettingsGet('StartupUrl'));
progressJs.set(90);
rl.setWindowTitle();
if (SettingsGet('Auth')) {
rl.setWindowTitle(i18n('GLOBAL/LOADING'));
this.foldersReload(value => {
try {
this.bootend();
if (value) {
if (startupUrl) {
rl.route.setHash(root(startupUrl), true);
@ -960,6 +945,7 @@ class AppUser extends AbstractApp {
Settings.capa(Capa.Settings) ? SettingsUserScreen : null
// false ? AboutUserScreen : null
]);
this.hideLoading();
setInterval(() => {
const cF = FolderUserStore.currentFolderFullNameRaw(),
@ -1030,8 +1016,8 @@ class AppUser extends AbstractApp {
});
} else {
this.bootend();
startScreens([LoginUserScreen]);
this.hideLoading();
}
setInterval(() => dispatchEvent(new CustomEvent('reload-time')), 60000);

View file

@ -1,14 +1,12 @@
import { pString, pInt } from 'Common/Utils';
import { Settings, SettingsGet } from 'Common/Globals';
import { Settings } from 'Common/Globals';
const
ROOT = './',
HASH_PREFIX = '#/',
SERVER_PREFIX = './?',
VERSION = Settings.app('version'),
VERSION_PREFIX = Settings.app('webVersionPath') || 'snappymail/v/' + VERSION + '/',
getHash = () => SettingsGet('AuthAccountHash') || '0';
VERSION_PREFIX = Settings.app('webVersionPath') || 'snappymail/v/' + VERSION + '/';
/**
* @returns {string}
@ -40,7 +38,7 @@ export function logoutLink() {
*/
export function serverRequestRaw(type, hash, customSpecSuffix) {
return SERVER_PREFIX + '/Raw/' + SUB_QUERY_PREFIX + '/'
+ (null == customSpecSuffix ? getHash() : customSpecSuffix) + '/'
+ (null == customSpecSuffix ? '0' : customSpecSuffix) + '/'
+ (type
? type + '/' + (hash ? SUB_QUERY_PREFIX + '/' + hash : '')
: '')
@ -61,7 +59,7 @@ export function attachmentDownload(download, customSpecSuffix) {
* @returns {string}
*/
export function serverRequest(type) {
return SERVER_PREFIX + '/' + type + '/' + SUB_QUERY_PREFIX + '/' + getHash() + '/';
return SERVER_PREFIX + '/' + type + '/' + SUB_QUERY_PREFIX + '/0/';
}
/**

View file

@ -3,7 +3,7 @@ import { Notification, UploadErrorCode } from 'Common/Enums';
import { langLink } from 'Common/Links';
import { doc, createElement } from 'Common/Globals';
let I18N_DATA = window.snappymailI18N || {};
let I18N_DATA = {};
export const trigger = ko.observable(false);
@ -49,10 +49,21 @@ const i18nToNode = element => {
}
},
init = () => {
if (rl.I18N) {
I18N_DATA = rl.I18N;
Date.defineRelativeTimeFormat(rl.relativeTime || {});
rl.I18N = null;
return 1;
}
},
i18nKey = key => key.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase(),
getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v);
init();
/**
* @param {Object} elements
* @param {boolean=} animate = false
@ -127,13 +138,11 @@ export function reload(admin, language) {
const script = createElement('script');
script.onload = () => {
// reload the data
if (window.snappymailI18N) {
I18N_DATA = window.snappymailI18N;
if (init()) {
i18nToNodes(doc);
dispatchEvent(new CustomEvent('reload-time'));
trigger(!trigger());
}
window.snappymailI18N = null;
script.remove();
resolve();
};

View file

@ -1,5 +1,5 @@
import { SaveSettingsStep } from 'Common/Enums';
import { doc, createElement, elementById } from 'Common/Globals';
import { doc, elementById } from 'Common/Globals';
export const
isArray = Array.isArray,
@ -84,14 +84,13 @@ let __themeTimer = 0,
* @returns {void}
*/
export function changeTheme(value, themeTrigger = ()=>{}) {
const themeLink = elementById('app-theme-link'),
const themeStyle = elementById('app-theme-style'),
clearTimer = () => {
__themeTimer = setTimeout(() => themeTrigger(SaveSettingsStep.Idle), 1000);
__themeJson = null;
};
let themeStyle = elementById('app-theme-style'),
url = (themeLink && themeLink.href) || (themeStyle && themeStyle.dataset.href);
let url = themeStyle.dataset.href;
if (url) {
url = url.toString()
@ -118,19 +117,9 @@ export function changeTheme(value, themeTrigger = ()=>{}) {
rl.fetchJSON(url, init)
.then(data => {
if (data && isArray(data) && 2 === data.length) {
if (themeLink && !themeStyle) {
themeStyle = createElement('style');
themeStyle.id = 'app-theme-style';
themeLink.after(themeStyle);
themeLink.remove();
}
if (themeStyle) {
themeStyle.textContent = data[1];
themeStyle.dataset.href = url;
themeStyle.dataset.theme = data[0];
}
themeStyle.textContent = data[1];
themeStyle.dataset.href = url;
themeStyle.dataset.theme = data[0];
themeTrigger(SaveSettingsStep.TrueResult);
}
})

View file

@ -11,31 +11,18 @@ export class CheckboxMaterialDesignComponent extends AbstractCheckbox {
this.animationBox = ko.observable(false).extend({ falseTimeout: 200 });
this.animationCheckmark = ko.observable(false).extend({ falseTimeout: 200 });
this.animationBoxSetTrue = this.animationBoxSetTrue.bind(this);
this.animationCheckmarkSetTrue = this.animationCheckmarkSetTrue.bind(this);
this.disposable.push(
this.value.subscribe((value) => {
this.triggerAnimation(value);
}, this)
this.value.subscribe(value => this.triggerAnimation(value), this)
);
}
animationBoxSetTrue() {
this.animationBox(true);
}
animationCheckmarkSetTrue() {
this.animationCheckmark(true);
}
triggerAnimation(box) {
if (box) {
this.animationBoxSetTrue();
setTimeout(this.animationCheckmarkSetTrue, 200);
this.animationBox(true);
setTimeout(()=>this.animationCheckmark(true), 200);
} else {
this.animationCheckmarkSetTrue();
setTimeout(this.animationBoxSetTrue, 200);
this.animationCheckmark(true);
setTimeout(()=>this.animationBox(true), 200);
}
}
}

View file

@ -412,6 +412,7 @@ class SquireUI
input.title = ctrlKey + cfg.key;
}
input.dataset.action = action;
input.tabIndex = -1;
cfg.input = input;
toolgroup.append(input);
}

View file

@ -92,7 +92,7 @@ export class AbstractViewCenter extends AbstractView
{
constructor(name, templateID)
{
super(name, templateID, ViewType.Center);
super(name, templateID, ViewType.Content);
}
}

View file

@ -18,7 +18,7 @@ export const ViewType = {
Popup: 'Popups',
Left: 'Left',
Right: 'Right',
Center: 'Center'
Content: 'Content'
};
/**
@ -76,7 +76,7 @@ function buildViewModel(ViewModelClass, vmScreen) {
let vmDom = null;
const vm = new ViewModelClass(vmScreen),
position = vm.viewModelPosition || '',
vmPlace = position ? doc.querySelector('#rl-content #rl-' + position.toLowerCase()) : null;
vmPlace = position ? doc.getElementById('rl-' + position.toLowerCase()) : null;
ViewModelClass.__builded = true;
ViewModelClass.__vm = vm;
@ -145,6 +145,8 @@ function buildViewModel(ViewModelClass, vmScreen) {
if (vm && ViewType.Popup === position) {
vm.registerPopupKeyDown();
}
dispatchEvent(new CustomEvent('rl-view-model', {detail:vm}));
} else {
console.log('Cannot find view model position: ' + position);
}
@ -153,7 +155,7 @@ function buildViewModel(ViewModelClass, vmScreen) {
return ViewModelClass && ViewModelClass.__vm;
}
function getScreenPopupViewModel(ViewModelClassToShow) {
export function getScreenPopupViewModel(ViewModelClassToShow) {
return (buildViewModel(ViewModelClassToShow) && ViewModelClassToShow.__dom) && ViewModelClassToShow.__vm;
}
@ -175,15 +177,6 @@ export function showScreenPopup(ViewModelClassToShow, params = []) {
}
}
/**
* @param {Function} ViewModelClassToShow
* @returns {void}
*/
export function warmUpScreenPopup(ViewModelClassToShow) {
const vm = getScreenPopupViewModel(ViewModelClassToShow);
vm && vm.onWarmUp && vm.onWarmUp();
}
/**
* @param {Function} ViewModelClassToShow
* @returns {boolean}

View file

@ -100,10 +100,8 @@ export class FolderCollectionModel extends AbstractCollectionModel
storeIt() {
const cnt = pInt(this.CountRec);
let limit = pInt(Settings.app('folderSpecLimit'));
limit = 100 < limit ? 100 : 10 > limit ? 10 : limit;
FolderUserStore.displaySpecSetting(0 >= cnt || limit < cnt);
FolderUserStore.displaySpecSetting(0 >= cnt
|| Math.max(10, Math.min(100, pInt(Settings.app('folderSpecLimit')))) < cnt);
FolderUserStore.folderList(this);

View file

@ -1,5 +1,4 @@
import { Notification } from 'Common/Enums';
import { Settings } from 'Common/Globals';
import { isArray, pInt, pString } from 'Common/Utils';
import { serverRequest } from 'Common/Links';
@ -8,13 +7,6 @@ let iJsonErrorCount = 0,
const getURL = (add = '') => serverRequest('Json') + add,
updateToken = data => {
if (data.UpdateToken) {
rl.hash.set();
Settings.set('AuthAccountHash', data.UpdateToken);
}
},
checkResponseError = data => {
const err = data ? data.ErrorCode : null;
if (Notification.InvalidToken === err && 10 < ++iTokenErrorCount) {
@ -32,9 +24,8 @@ checkResponseError = data => {
) {
++iJsonErrorCount;
}
if (data.ClearAuth || data.Logout || 7 < iJsonErrorCount) {
rl.hash.clear();
data.ClearAuth || rl.logoutReload();
if (data.Logout || 7 < iJsonErrorCount) {
rl.logoutReload();
}
}
},
@ -97,12 +88,8 @@ export class AbstractFetchRemote
undefined === iTimeout ? 30000 : pInt(iTimeout),
data => {
let cached = false;
if (data) {
if (data.Time) {
cached = pInt(data.Time) > Date.now() - start;
}
updateToken(data);
if (data && data.Time) {
cached = pInt(data.Time) > Date.now() - start;
}
let iError = 0;
@ -188,8 +175,6 @@ export class AbstractFetchRemote
if (!data) {
return Promise.reject(Notification.JsonParse);
}
updateToken(data);
/*
let isCached = false, type = '';
if (data && data.Time) {

View file

@ -126,8 +126,7 @@ export class AbstractSettingsScreen extends AbstractScreen {
this.menu.push({
route: SettingsViewModel.__rlSettingsData.Route,
label: SettingsViewModel.__rlSettingsData.Label,
selected: ko.observable(false),
disabled: false
selected: ko.observable(false)
});
}
});

View file

@ -15,7 +15,7 @@ import { FolderListMailBoxUserView } from 'View/User/MailBox/FolderList';
import { MessageListMailBoxUserView } from 'View/User/MailBox/MessageList';
import { MessageViewMailBoxUserView } from 'View/User/MailBox/MessageView';
import { warmUpScreenPopup } from 'Knoin/Knoin';
import { getScreenPopupViewModel } from 'Knoin/Knoin';
import { AbstractScreen } from 'Knoin/AbstractScreen';
@ -88,7 +88,7 @@ export class MailBoxUserScreen extends AbstractScreen {
onStart() {
if (!this.__started) {
super.onStart();
setTimeout(() => warmUpScreenPopup(ComposePopupView), 500);
setTimeout(() => getScreenPopupViewModel(ComposePopupView), 500);
addEventListener('mailbox.inbox-unread-count', e => {
FolderUserStore.foldersInboxUnreadCount(e.detail);

View file

@ -105,8 +105,6 @@ export class ContactsAdminSettings {
})
})
this.onTestContactsResponse = this.onTestContactsResponse.bind(this);
decorateKoCommands(this, {
testContactsCommand: self => self.pdoDsn() && self.pdoUser()
});
@ -118,7 +116,24 @@ export class ContactsAdminSettings {
this.testContactsErrorMessage('');
this.testing(true);
Remote.testContacts(this.onTestContactsResponse, {
Remote.testContacts((iError, data) => {
this.testContactsSuccess(false);
this.testContactsError(false);
this.testContactsErrorMessage('');
if (!iError && data.Result.Result) {
this.testContactsSuccess(true);
} else {
this.testContactsError(true);
if (data && data.Result) {
this.testContactsErrorMessage(data.Result.Message || '');
} else {
this.testContactsErrorMessage('');
}
}
this.testing(false);
}, {
ContactsPdoType: this.contactsType(),
ContactsPdoDsn: this.pdoDsn(),
ContactsPdoUser: this.pdoUser(),
@ -126,25 +141,6 @@ export class ContactsAdminSettings {
});
}
onTestContactsResponse(iError, data) {
this.testContactsSuccess(false);
this.testContactsError(false);
this.testContactsErrorMessage('');
if (!iError && data.Result.Result) {
this.testContactsSuccess(true);
} else {
this.testContactsError(true);
if (data && data.Result) {
this.testContactsErrorMessage(data.Result.Message || '');
} else {
this.testContactsErrorMessage('');
}
}
this.testing(false);
}
onShow() {
this.testContactsSuccess(false);
this.testContactsError(false);

View file

@ -13,9 +13,6 @@ export class DomainsAdminSettings {
this.domains = DomainAdminStore;
this.domainForDeletion = ko.observable(null).deleteAccessHelper();
this.onDomainListChangeRequest = this.onDomainListChangeRequest.bind(this);
this.onDomainLoadRequest = this.onDomainLoadRequest.bind(this);
}
createDomain() {
@ -28,30 +25,22 @@ export class DomainsAdminSettings {
deleteDomain(domain) {
DomainAdminStore.remove(domain);
Remote.domainDelete(this.onDomainListChangeRequest, domain.name);
Remote.domainDelete(DomainAdminStore.fetch, domain.name);
}
disableDomain(domain) {
domain.disabled(!domain.disabled());
Remote.domainDisable(this.onDomainListChangeRequest, domain.name, domain.disabled());
Remote.domainDisable(DomainAdminStore.fetch, domain.name, domain.disabled());
}
onBuild(oDom) {
oDom.addEventListener('click', event => {
let el = event.target.closestWithin('.b-admin-domains-list-table .e-item .e-action', oDom);
el && ko.dataFor(el) && Remote.domain(this.onDomainLoadRequest, ko.dataFor(el).name);
let el = event.target.closestWithin('.b-admin-domains-list-table .e-action', oDom);
el && ko.dataFor(el) && Remote.domain(
(iError, oData) => iError || showScreenPopup(DomainPopupView, [oData.Result]), ko.dataFor(el).name
);
});
DomainAdminStore.fetch();
}
onDomainLoadRequest(iError, oData) {
if (!iError) {
showScreenPopup(DomainPopupView, [oData.Result]);
}
}
onDomainListChangeRequest() {
DomainAdminStore.fetch();
}
}

View file

@ -12,16 +12,14 @@ import Remote from 'Remote/Admin/Fetch';
import { PluginPopupView } from 'View/Popup/Plugin';
export class PluginsAdminSettings {
export class PluginsAdminSettings
{
constructor() {
this.enabledPlugins = ko.observable(!!SettingsGet('EnabledPlugins'));
this.plugins = PluginAdminStore;
this.pluginsError = PluginAdminStore.error;
this.onPluginLoadRequest = this.onPluginLoadRequest.bind(this);
this.onPluginDisableRequest = this.onPluginDisableRequest.bind(this);
this.enabledPlugins.subscribe(value =>
Remote.saveAdminConfig(null, {
EnabledPlugins: value ? 1 : 0
@ -30,20 +28,30 @@ export class PluginsAdminSettings {
}
disablePlugin(plugin) {
plugin.disabled(!plugin.disabled());
Remote.pluginDisable(this.onPluginDisableRequest, plugin.name, plugin.disabled());
let b = !plugin.disabled();
plugin.disabled(b);
Remote.pluginDisable((iError, data) => {
if (iError) {
PluginAdminStore.error(
(Notification.UnsupportedPluginPackage === iError && data && data.ErrorMessage)
? data.ErrorMessage
: getNotification(iError)
);
}
PluginAdminStore.fetch();
}, plugin.name, b);
}
configurePlugin(plugin) {
Remote.plugin(this.onPluginLoadRequest, plugin.name);
Remote.plugin((iError, data) => iError || showScreenPopup(PluginPopupView, [data.Result]), plugin.name);
}
onBuild(oDom) {
oDom.addEventListener('click', event => {
let el = event.target.closestWithin('.e-item .configure-plugin-action', oDom);
let el = event.target.closestWithin('.configure-plugin-action', oDom);
el && ko.dataFor(el) && this.configurePlugin(ko.dataFor(el));
el = event.target.closestWithin('.e-item .disabled-plugin', oDom);
el = event.target.closestWithin('.disabled-plugin', oDom);
el && ko.dataFor(el) && this.disablePlugin(ko.dataFor(el));
});
}
@ -52,22 +60,4 @@ export class PluginsAdminSettings {
PluginAdminStore.error('');
PluginAdminStore.fetch();
}
onPluginLoadRequest(iError, data) {
if (!iError) {
showScreenPopup(PluginPopupView, [data.Result]);
}
}
onPluginDisableRequest(iError, data) {
if (iError) {
if (Notification.UnsupportedPluginPackage === iError && data && data.ErrorMessage) {
PluginAdminStore.error(data.ErrorMessage);
} else {
PluginAdminStore.error(getNotification(iError));
}
}
PluginAdminStore.fetch();
}
}

View file

@ -72,8 +72,6 @@ export class SecurityAdminSettings {
})
});
this.onNewAdminPasswordResponse = this.onNewAdminPasswordResponse.bind(this);
decorateKoCommands(this, {
saveNewAdminPasswordCommand: self => self.adminLogin().trim() && self.adminPassword()
});
@ -93,7 +91,19 @@ export class SecurityAdminSettings {
this.adminPasswordUpdateError(false);
this.adminPasswordUpdateSuccess(false);
Remote.saveNewAdminPassword(this.onNewAdminPasswordResponse, {
Remote.saveNewAdminPassword((iError, data) => {
if (iError) {
this.adminPasswordUpdateError(true);
} else {
this.adminPassword('');
this.adminPasswordNew('');
this.adminPasswordNew2('');
this.adminPasswordUpdateSuccess(true);
this.weakPassword(!!data.Result.Weak);
}
}, {
'Login': this.adminLogin(),
'Password': this.adminPassword(),
'NewPassword': this.adminPasswordNew()
@ -102,20 +112,6 @@ export class SecurityAdminSettings {
return true;
}
onNewAdminPasswordResponse(iError, data) {
if (iError) {
this.adminPasswordUpdateError(true);
} else {
this.adminPassword('');
this.adminPasswordNew('');
this.adminPasswordNew2('');
this.adminPasswordUpdateSuccess(true);
this.weakPassword(!!data.Result.Weak);
}
}
onHide() {
this.adminPassword('');
this.adminPasswordNew('');

View file

@ -1,9 +1,9 @@
const storage = localStorage,
const
CLIENT_SIDE_STORAGE_INDEX_NAME = 'rlcsc',
getStorage = () => {
try {
const value = storage.getItem(CLIENT_SIDE_STORAGE_INDEX_NAME) || null;
return null == value ? null : JSON.parse(value);
const value = localStorage.getItem(CLIENT_SIDE_STORAGE_INDEX_NAME);
return value ? JSON.parse(value) : null;
} catch (e) {
return null;
}
@ -19,7 +19,7 @@ export function set(key, data) {
storageResult['p' + key] = data;
try {
storage.setItem(CLIENT_SIDE_STORAGE_INDEX_NAME, JSON.stringify(storageResult));
localStorage.setItem(CLIENT_SIDE_STORAGE_INDEX_NAME, JSON.stringify(storageResult));
return true;
} catch (e) {
return false;
@ -32,10 +32,7 @@ export function set(key, data) {
*/
export function get(key) {
try {
key = 'p' + key;
const storageResult = getStorage();
return storageResult && null != storageResult[key] ? storageResult[key] : null;
return (getStorage() || {})['p' + key];
} catch (e) {
return null;
}

View file

@ -220,8 +220,6 @@ export const MessageUserStore = new class {
}
});
this.onMessageResponse = this.onMessageResponse.bind(this);
this.purgeMessageBodyCacheThrottle = this.purgeMessageBodyCache.throttle(30000);
}
@ -437,7 +435,6 @@ export const MessageUserStore = new class {
id = '',
plain = '',
resultHtml = '',
pgpSigned = false,
messagesDom = this.messagesBodiesDom(),
selectedMessage = this.selectorMessageSelected(),
message = this.message();
@ -497,27 +494,17 @@ export const MessageUserStore = new class {
if ((message.isPgpSigned() || message.isPgpEncrypted()) && PgpUserStore.capaOpenPGP()) {
plain = pString(json.Plain);
const isPgpEncrypted = /---BEGIN PGP MESSAGE---/.test(plain);
if (!isPgpEncrypted) {
pgpSigned =
/-----BEGIN PGP SIGNED MESSAGE-----/.test(plain) && /-----BEGIN PGP SIGNATURE-----/.test(plain);
}
const pre = createElement('pre');
if (pgpSigned && message.isPgpSigned()) {
if (message.isPgpSigned()) {
pre.className = 'b-plain-openpgp signed';
pre.textContent = plain;
} else if (isPgpEncrypted && message.isPgpEncrypted()) {
} else if (message.isPgpEncrypted()) {
pre.className = 'b-plain-openpgp encrypted';
pre.textContent = plain;
} else {
pre.innerHTML = resultHtml;
}
resultHtml = pre.outerHTML;
message.isPgpSigned(pgpSigned);
message.isPgpEncrypted(isPgpEncrypted);
} else {
resultHtml = '<pre>' + resultHtml + '</pre>';
}
@ -630,27 +617,20 @@ export const MessageUserStore = new class {
if (oMessage) {
this.hideMessageBodies();
this.messageLoading(true);
Remote.message(this.onMessageResponse, oMessage.folder, oMessage.uid);
Remote.message((iError, oData, bCached) => {
if (iError) {
if (Notification.RequestAborted !== iError) {
this.message(null);
this.messageError(getNotification(iError));
}
} else {
this.setMessage(oData, bCached);
}
this.messageLoading(false);
}, oMessage.folder, oMessage.uid);
}
}
/**
* @param {string} sResult
* @param {FetchJsonDefaultResponse} oData
* @param {boolean} bCached
*/
onMessageResponse(iError, oData, bCached) {
if (iError) {
if (Notification.RequestAborted !== iError) {
this.message(null);
this.messageError(getNotification(iError));
}
} else {
this.setMessage(oData, bCached);
}
this.messageLoading(false);
}
/**
* @param {Array} list
* @returns {string}

View file

@ -7,7 +7,7 @@ import * as Links from 'Common/Links';
/**
* Might not work due to the new ServiceWorkerRegistration.showNotification
*/
const HTML5Notification = window.Notification ? Notification : null,
const HTML5Notification = window.Notification,
HTML5NotificationStatus = () => (HTML5Notification && HTML5Notification.permission) || 'denied',
NotificationsDenied = () => 'denied' === HTML5NotificationStatus(),
NotificationsGranted = () => 'granted' === HTML5NotificationStatus(),

View file

@ -340,11 +340,11 @@ export const PgpUserStore = new class {
*/
initMessageBodyControls(dom, rainLoopMessage) {
const cl = dom && dom.classList;
if (!cl.has('inited')) {
if (!cl.contains('inited')) {
cl.add('inited');
const encrypted = cl.has('encrypted'),
signed = cl.has('signed'),
const encrypted = cl.contains('encrypted'),
signed = cl.contains('signed'),
recipients = rainLoopMessage ? rainLoopMessage.getEmails(['from', 'to', 'cc']) : [];
let verControl = null;
@ -355,10 +355,10 @@ export const PgpUserStore = new class {
verControl = Element.fromHTML('<div class="b-openpgp-control"><i class="fontastic">🔒</i></div>');
if (encrypted) {
verControl.title = i18n('MESSAGE/PGP_ENCRYPTED_MESSAGE_DESC');
verControl.addEventHandler('click', domControlEncryptedClickHelper(this, dom, domText, recipients));
verControl.addEventListener('click', domControlEncryptedClickHelper(this, dom, domText, recipients));
} else {
verControl.title = i18n('MESSAGE/PGP_SIGNED_MESSAGE_DESC');
verControl.addEventHandler('click', domControlSignedClickHelper(this, dom, domText));
verControl.addEventListener('click', domControlSignedClickHelper(this, dom, domText));
}
dom.before(verControl, createElement('div'));

View file

@ -33,7 +33,7 @@ body {
text-align: center;
}
.e-spinner .e-bounce {
.e-spinner div {
animation: bouncedelay 1.4s infinite ease-in-out;
background-color: var(--loading-color, #000);
border-radius: 100%;
@ -46,12 +46,12 @@ body {
animation-fill-mode: both;
}
.e-spinner .bounce1 {
animation-delay: -0.32s;
.e-spinner div + div {
animation-delay: 0.16s;
}
.e-spinner .bounce2 {
animation-delay: -0.16s;
.e-spinner div + div + div {
animation-delay: 0.32s;
}
@keyframes bouncedelay {

View file

@ -1,67 +1,25 @@
.b-admin-left {
#rl-left {
overflow: auto;
padding-top: 50px + @rlLowMargin + 10px;
.b-toolbar {
position: absolute;
top: 0;
right: 0;
left: 0;
height: 34px;
padding: 8px 0 0 @rlLowMargin;
}
.b-content {
position: absolute;
top: 50px + @rlLowMargin + 10px;
bottom: @rlLowMargin;
left: 0;
right: 0;
overflow: hidden;
.content {
-webkit-overflow-scrolling: touch;
nav {
a {
color: var(--settings-menu-color, #333);
cursor: pointer;
display: block;
font-size: 18px;
line-height: 30px;
padding: 4px 10px;
text-decoration: none;
}
}
}
.b-admin-menu {
.e-item {
overflow: hidden;
text-decoration: none;
outline: 0;
}
.e-link {
position: relative;
display: block;
height: 30px;
line-height: 29px;
cursor: pointer;
font-size: 18px;
z-index: 1;
cursor: default;
background-color: transparent;
color: var(--settings-menu-disabled-color, #666);
padding: 4px 10px;
outline: 0;
text-decoration: none;
}
.e-item.selectable .e-link {
cursor: pointer;
color: var(--settings-menu-color, #333);
}
.e-item.selectable {
&:hover .e-link{
a:focus, a:hover {
background-color: var(--settings-menu-hover-bg-color, #333);
color: var(--settings-menu-hover-color, #eee);
}
&.selected .e-link {
a.selected {
background-color: var(--settings-menu-selected-bg-color, #333);
color: var(--settings-menu-selected-color, #eee);
}
@ -97,14 +55,10 @@
z-index: 2;
background-color: #fff;
border: @rlMainBorderSize solid @rlMainDarkColor;
border: 1px solid @rlMainDarkColor;
box-shadow: @rlMainShadow;
border-radius: @rlMainBorderRadius;
.content {
-webkit-overflow-scrolling: touch;
}
}
}

View file

@ -3,54 +3,49 @@
width: 600px;
.e-item {
.domain-name {
display: inline-block;
word-break: break-all;
box-sizing: border-box;
}
.domain-name {
display: inline-block;
word-break: break-all;
box-sizing: border-box;
.domain-alias {
display: inline-block;
box-sizing: border-box;
color: #bbb;
padding-left: 5px;
}
.button-delete {
position: relative;
right: 0;
visibility: hidden;
opacity: 0;
white-space: nowrap;
&.delete-access {
visibility: visible;
right: -15px;
opacity: 1;
}
}
.domain-alias {
display: inline-block;
box-sizing: border-box;
.delete-domain, .disabled-domain {
cursor: pointer;
opacity: 0.5;
&:hover {
opacity: 1;
}
}
&.disabled {
.domain-name, .domain-alias {
color: #bbb;
padding-left: 5px;
}
&.disabled {
.domain-name, .domain-alias {
color: #bbb;
}
}
.button-delete {
margin-right: 15px;
visibility: hidden;
opacity: 0;
}
.delete-access {
&.button-delete {
visibility: visible;
margin-right: 0;
opacity: 1;
}
}
.delete-domain, .disabled-domain {
cursor: pointer;
.disabled-domain {
opacity: 0.5;
}
&.disabled .disabled-domain {
opacity: 0.5;
}
.delete-domain, .disabled-domain {
&:hover {
opacity: 1;
}
}
}
}

View file

@ -11,26 +11,23 @@
width: 700px;
.e-item {
.package-img {
font-size: 12px;
margin-right: 2px;
}
.package-img {
font-size: 12px;
margin-right: 2px;
}
.package-name.core {
font-weight: bold;
}
.package-desc {
color: #999;
font-size: 12px;
}
.package-name.core {
font-weight: bold;
}
.package-desc {
color: #999;
font-size: 12px;
}
.package-release, .package-actions {
text-align: center;
}
.package-actions {
vertical-align: middle;
}
.package-release, .package-actions {
text-align: center;
}
.package-actions {
vertical-align: middle;
}
}

View file

@ -1,25 +1,13 @@
.b-admin-plugins-list-table {
max-width: 620px; /* span8 */
&.disabled {
opacity: 0.5;
background-color: #eee;
}
.e-item {
.plugin-img {
font-size: 12px;
margin-right: 2px;
}
&.disabled {
.plugin-img, .plugin-name {
color: #bbb;
}
.disabled-plugin {
opacity: 0.5;
}
}
.disabled {
opacity: 0.5;
}
}

View file

@ -25,7 +25,7 @@ html.rl-started-delay {
@media screen and (min-width: 1000px) {
html.rl-started-trigger .b-login-content .loginFormWrapper {
html.rl-started-trigger .b-login-content {
/*transform: scale(1.1);*/
transform: translateY(-20px);
opacity: 0.5;
@ -40,11 +40,7 @@ html.rl-started-delay {
font-weight: bold;
}
.b-login-content .afterLoginHide {
opacity: 0;
}
.b-login-content .loginFormWrapper {
.b-login-content {
transition: all 0.3s ease-out;
}

View file

@ -39,7 +39,7 @@
transition: margin 0.3s linear;
}
.b-folders .b-content.opacity-on-panel-disabled {
#rl-left .b-content {
transition: opacity 0.3s linear;
}

View file

@ -41,7 +41,7 @@
}
&.error {
.attachmentIcon, .attachmentSize, .attachmentName {
.iconMain, .iconPreview, .attachmentSize, .attachmentName {
color: red;
}
}
@ -91,11 +91,10 @@
border-left: 1px solid #ddd;
}
.attachmentIcon {
margin: 6px 0 0;
.iconMain, .iconPreview {
box-sizing: border-box;
padding: 6px 0 0;
font-size: 36px;
width: 36px;
height: 36px;
color: #aaa;
&.icon-none {
@ -125,7 +124,7 @@
}
.attachmentIconParent.hasPreview .showPreview,
.attachmentIconParent.hasPreplay .showPreview {
.attachmentIconParent.hasPreplay .showPreplay {
display: inline;
cursor: pointer;
}

View file

@ -57,7 +57,6 @@
&.disabled {
cursor: not-allowed;
opacity: 0.5;
outline: 0;
}
}

View file

@ -20,10 +20,10 @@
.attachmentAreaParent {
border-top: 1px solid #ccc;
overflow-y: auto;
padding: 10px 10px 6px 10px;
padding: 10px;
.no-attachments-desc {
padding: 50px 10px;
padding-top: 50px;
text-align: center;
font-size: 24px;
color: #666;
@ -38,7 +38,6 @@
.b-header-toolbar {
color: #fff;
background-color: rgba(0,0,0,0.8);
.close, .minimize-custom {
@ -72,23 +71,17 @@
.b-header {
padding: 10px;
background-color: #eee;
color: #333;
background-color: rgba(128,128,128,0.2);;
.e-identity {
color: #333;
color: var(--main-color);
text-decoration: none;
font-weight: bold;
&:hover {
color: #333;
text-decoration: none;
}
&.multiply {
cursor: pointer;
border-bottom: 1px dashed #555;
border-bottom: 1px dashed var(--main-color);
}
}

View file

@ -8,16 +8,11 @@
padding-top: 0;
width: 50px;
}
.controls {
margin-left: 70px;
}
}
&.modal {
position: absolute;
right: 0;
top: 0;
bottom: 0;
left: 0;
width: auto;
@ -28,9 +23,9 @@
}
.modal-body {
overflow: auto;
height: calc(100% - 49px);
padding: 0;
position: relative;
}
.b-list-toolbar {
@ -71,10 +66,6 @@
overflow-y: auto;
scroll-behavior: smooth;
.content {
-webkit-overflow-scrolling: touch;
}
.listClear {
text-align: center;
padding: 10px;
@ -184,27 +175,13 @@
overflow: auto;
border-left: 1px solid rgba(128,128,128,0.25);
.content {
-webkit-overflow-scrolling: touch;
}
.contactValueStatic, .contactValueLargeStatic, .contactValueTextAreaStatic {
span {
height: 20px;
line-height: 20px;
font-size: 18px;
display: inline-block;
padding: 5px 7px;
display: none;
}
&.read-only {
.contactValueStatic, .contactValueLargeStatic, .contactValueTextAreaStatic {
display: inline-block;
}
.contactValueInput, .contactValueInputLarge, .contactValueTextArea {
display: none;
}
padding: 5px 7px;
font-size: 18px;
display: none;
}
.b-contact-view-desc {
@ -235,16 +212,12 @@
color: #aaa;
}
.contactValueStatic, .contactValueLargeStatic, .contactValueTextAreaStatic {
font-size: 18px;
display: none;
}
.contactValueInput, .contactValueInputLarge, .contactValueTextArea {
input, textarea {
box-shadow: none;
border-color: #fff;
font-size: 18px;
width: 300px;
width: 70vw;
max-width: 400px;
&:hover {
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
@ -257,16 +230,8 @@
}
}
.contactValueTextArea {
width: 300px;
}
.contactValueInputLarge {
width: 400px;
}
.hasError {
.contactValueInput {
input {
color: #ee5f5b;
border-color: #ee5f5b;
}
@ -293,11 +258,11 @@
}
&.read-only {
.e-read-only-sign {
span, .e-read-only-sign {
display: inline-block;
}
.e-share-sign {
input, textarea, .e-share-sign {
display: none;
}
}

View file

@ -14,7 +14,7 @@
.emailaddresses.emailaddresses-focused {
background-color: #fff;
border: @rlInputBorderSize solid darken(@inputBorder, 20%);
border: 1px solid darken(@inputBorder, 20%);
box-shadow: none;
}
@ -49,7 +49,6 @@
.emailaddresses li a {
font-size: 12px;
color: #999;
outline: 0;
padding: 1px;
position: absolute;
right: 2px;
@ -73,7 +72,6 @@
height: auto;
line-height: inherit;
margin: 0;
outline: 0;
padding: 0;
vertical-align: baseline;
}

View file

@ -20,7 +20,7 @@
}
.b-folders-user {
.e-link.print-count.system .badge {
a.print-count.system .badge {
display: none !important;
}
}
@ -58,10 +58,6 @@
overflow: hidden;
overflow-y: auto;
min-width: 100px;
.content {
-webkit-overflow-scrolling: touch;
}
}
hr {
@ -75,7 +71,7 @@
overflow: hidden;
white-space: nowrap;
.e-link {
a {
display: block;
position: relative;
z-index: 1;
@ -108,11 +104,8 @@
display: inline;
}
padding: 0;
padding-left: @folderItemPadding;
padding-right: @folderItemPadding;
padding: 0 @folderItemPadding;
outline: 0;
text-decoration: none;
&.selectable {
@ -204,21 +197,21 @@
font-weight: bold;
}
.b-sub-folders .e-item .e-link {
.b-sub-folders .e-item a {
padding-left: @subPadding * 1 + @folderItemPadding;
}
.b-sub-folders .b-sub-folders .e-item .e-link {
.b-sub-folders .b-sub-folders .e-item a {
padding-left: @subPadding * 2 + @folderItemPadding;
}
.b-sub-folders .b-sub-folders .b-sub-folders .e-item .e-link {
.b-sub-folders .b-sub-folders .b-sub-folders .e-item a {
padding-left: @subPadding * 3 + @folderItemPadding;
}
.b-sub-folders .b-sub-folders .b-sub-folders .b-sub-folders .e-item .e-link {
.b-sub-folders .b-sub-folders .b-sub-folders .b-sub-folders .e-item a {
padding-left: @subPadding * 4 + @folderItemPadding;
}
/**/
&.single-root-inbox .i-am-inbox.e-link {
&.single-root-inbox a.i-am-inbox {
display: none !important;
}
@ -229,16 +222,16 @@
}
&.single-root-inbox .i-am-inbox-wrapper {
.b-sub-folders .e-item .e-link {
.b-sub-folders .e-item a {
padding-left: @folderItemPadding;
}
.b-sub-folders .b-sub-folders .e-item .e-link {
.b-sub-folders .b-sub-folders .e-item a {
padding-left: @subPadding * 1 + @folderItemPadding;
}
.b-sub-folders .b-sub-folders .b-sub-folders .e-item .e-link {
.b-sub-folders .b-sub-folders .b-sub-folders .e-item a {
padding-left: @subPadding * 2 + @folderItemPadding;
}
.b-sub-folders .b-sub-folders .b-sub-folders .b-sub-folders .e-item .e-link {
.b-sub-folders .b-sub-folders .b-sub-folders .b-sub-folders .e-item a {
padding-left: @subPadding * 3 + @folderItemPadding;
}
}

View file

@ -5,7 +5,7 @@
background-position: center;
}
#rl-center, #rl-top, #rl-left, #rl-right {
#rl-left, #rl-right {
position: absolute;
top: 0;
right: 0;
@ -17,23 +17,8 @@
z-index: 0;
}
#rl-content {
height: 100%;
width: 100%;
}
#rl-center {
min-width: 600px;
min-height: 400px;
}
html.rl-mobile {
#rl-center {
min-width: 250px;
min-height: 250px;
}
&:not(.rl-left-panel-disabled) #rl-right {
right: -150px;
}
@ -43,11 +28,6 @@ html.rl-mobile {
}
}
#rl-top {
bottom: auto;
z-index: 2;
}
#rl-left {
width: @rlLeftWidth;
}
@ -223,11 +203,10 @@ html:not(.rl-left-panel-disabled) #rl-left.resizable > .resizer,
z-index: 1100;
overflow: auto;
background-color: rgba(0,0,0,0.3);
// -webkit-overflow-scrolling: touch;
}
.b-settings-content {
padding:20px 20px 20px 30px;
#rl-settings-subscreen {
padding:20px;
}
.dropdown-menu * + .dividerbar {
@ -264,7 +243,7 @@ html:not(.rl-left-panel-disabled) #rl-left.resizable > .resizer,
}
.btn-group > .btn.single {
border-radius: @btnBorderRadius !important;
border-radius: 3px !important;
}
/* desktop */
@ -300,8 +279,8 @@ html:not(.rl-left-panel-disabled) #rl-left.resizable > .resizer,
left: 155px;
}
.b-settings-content {
padding: 10px 10px 10px 20px;
#rl-settings-subscreen {
padding: 10px;
}
.dropdown-menu a {
@ -324,7 +303,7 @@ html.rl-left-panel-disabled {
display: block;
}
.opacity-on-panel-disabled {
.b-content {
opacity: 0.3;
}

View file

@ -5,7 +5,7 @@
background-position: center;
}
#rl-center, #rl-top, #rl-left, #rl-right {
#rl-left, #rl-right {
position: absolute;
top: 0;
right: 0;
@ -17,21 +17,6 @@
z-index: 0;
}
#rl-content {
height: 100%;
width: 100%;
}
#rl-center {
min-width: 600px;
min-height: 400px;
}
#rl-top {
bottom: auto;
z-index: 2;
}
#rl-left {
width: @rlLeftWidth;
}
@ -50,11 +35,10 @@
z-index: 1100;
overflow: auto;
background-color: rgba(0,0,0,0.3);
// -webkit-overflow-scrolling: touch;
}
.b-settings-content {
padding:20px 20px 20px 30px;
#rl-settings-subscreen {
padding: 20px;
}
/* desktop */
@ -77,13 +61,8 @@
/* mobile and tablet */
@media screen and (max-width: 999px) {
.b-settings-content {
padding: 10px 10px 10px 20px;
}
#rl-center {
min-width: 250px;
min-height: 250px;
#rl-settings-subscreen {
padding: 10px;
}
html:not(.rl-left-panel-disabled) #rl-right {
@ -92,11 +71,7 @@
html.rl-left-panel-disabled {
#rl-left {
width: 0px !important;
.opacity-on-panel-disabled {
opacity: 0.3;
}
width: 0 !important;
}
#rl-right {
left: 5px !important;

View file

@ -1,139 +1,145 @@
.rl-view-model {
&.RL-Login, &.RL-AdminLogin {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 100%;
max-width: 450px;
z-index: 5;
}
.RL-Login, .RL-AdminLogin {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 100%;
min-width: 250px;
max-width: 600px;
z-index: 5;
}
@glass-color: #fff !important;
@glass-error-color: #f76260 !important;
@glass-m-color: rgba(255, 255, 255, .8) !important;
@glass-color: #fff;
@glass-error-color: #f76260;
@glass-m-color: rgba(255, 255, 255, .8);
.b-login-content {
.loginFormWrapper {
position: relative;
.descWrapper {
color: var(--loading-color, #000);
font-size: 30px;
margin-bottom: 10px;
text-align: center;
text-shadow: var(--loading-text-shadow);
}
.controls, .control-group {
margin-bottom: 25px;
}
form {
background-color: var(--login-bg-color, rgba(0, 0, 0, .5));
border: var(--login-border, 1px solid rgba(255, 255, 255, .2));
border-radius: var(--login-border-radius, 7px);
box-shadow: var(--login-box-shadow);
color: var(--login-color, @glass-m-color);
margin: 0;
padding: 40px 40px 20px 40px;
position: relative;
.descWrapper {
margin-bottom: 10px;
text-align: center;
.desc {
font-size: 30px;
color: var(--loading-color, #000);
text-shadow: var(--loading-text-shadow);
}
}
.loginForm {
background-color: var(--login-bg-color, rgba(0, 0, 0, .5));
border: var(--login-border, 1px solid rgba(255, 255, 255, .2));
border-radius: var(--login-border-radius, 7px);
box-shadow: var(--login-box-shadow);
margin: 0;
}
.input-append .add-on *,
.loginForm, .legend, .e-checkbox-icon, .g-ui-link, .language-button {
* {
color: var(--login-color, @glass-m-color);
text-shadow: none !important;
}
.controls, .control-group {
margin-bottom: 25px;
}
.wrapper {
padding: 40px 40px 20px 40px;
}
.controls {
.inputLoginForm, .inputEmail, .inputLogin, .inputPassword {
font-size: 18px;
height: 40px;
line-height: 20px;
padding-left: 12px;
padding-right: 12px;
}
.inputEmail, .inputLogin, .inputPassword {
padding-right: 35px;
}
input, .btn {
border: 1px solid none !important;
background: none !important;
outline: none !important;
text-shadow: none !important;
box-shadow: none !important;
color: @glass-color;
border-color: @glass-m-color;
.controls.error {
.input-append .add-on,
.input-append .add-on * {
color: @glass-error-color;
}
input {
&:placeholder {
color: @glass-color;
text-shadow: none !important;
}
&:focus, &:hover {
border-color: @glass-color;
}
}
.btn {
text-transform: uppercase;
font-size: 13px;
&:hover, &:active {
border-color: @glass-color;
}
}
&.error {
.input-append .add-on * {
color: @glass-error-color;
}
input {
color: @glass-error-color;
border-color: @glass-error-color;
}
color: @glass-error-color;
border-color: @glass-error-color;
}
}
.signMeLabel .e-checkbox {
margin-top: 5px;
&.submitting > * {
opacity: 0.3;
}
.input-append .add-on {
position: relative;
height: 30px;
background: none;
margin-left: -35px;
z-index: 1000;
border: 0;
&.submitting::after{
content: '';
position: absolute;
width: 60px;
height: 60px;
top: 50%;
left: 50%;
margin-top: -30px;
margin-left: -30px;
border: 8px solid transparent;
border-top-color: var(--spinner-color, #fff);
animation: loginRotation 1s infinite ease-in-out;
border-radius: 50%;
z-index: 1052;
}
}
.controls {
.inputLoginForm, .inputLogin, .inputPassword {
font-size: 18px;
height: 40px;
line-height: 20px;
padding-left: 12px;
padding-right: 12px;
}
.inputLogin, .inputPassword {
padding-right: 35px;
}
.input-append .add-on * {
font-size: 17px;
line-height: 29px;
outline: none !important;
box-shadow: none !important;
input, .btn {
border: 1px solid none;
background: none !important;
text-shadow: none;
box-shadow: none;
color: @glass-color;
border-color: @glass-m-color;
}
.controls.error .add-on *,
.control-group.error .add-on * {
color: #b94a48;
input {
&:placeholder {
color: @glass-color;
text-shadow: none;
}
&:focus, &:hover {
border-color: @glass-color;
}
}
.btn {
text-transform: uppercase;
font-size: 13px;
&:hover, &:active {
border-color: @glass-color;
}
}
}
.signMeLabel .e-checkbox {
margin-top: 5px;
}
.input-append .add-on {
margin-left: -1.5em;
position: absolute;
font-size: 17px;
line-height: 29px;
right: 6px;
top: 6px;
z-index: 6;
}
.input-append .add-on * {
font-size: 17px;
line-height: 29px;
}
.buttonLogin {
@ -163,14 +169,13 @@
.language-button {
padding: 5px;
outline: none;
text-decoration: none;
}
}
@media screen and (max-width: 480px) {
.b-login-content .loginFormWrapper {
.wrapper {
.b-login-content {
form {
padding: 30px 4vw 10px;
}
}
@ -180,36 +185,11 @@
to {transform: rotate(1turn);}
}
.submitting-pane {
position: relative;
&.submitting > * {
opacity: 0.3;
}
html &.submitting::after{
content: '';
position: absolute;
width: 60px;
height: 60px;
top: 50%;
left: 50%;
margin-top: -30px;
margin-left: -30px;
border: 8px solid transparent;
border-top-color: var(--spinner-color, #fff);
animation: loginRotation 1s infinite ease-in-out;
border-radius: 50%;
z-index: 1052;
}
}
.btn-submit-icon-wrp {
border: none;
background: none;
display: inline-block;
margin: 0;
padding: 0;
outline: none;
cursor: pointer;
}

View file

@ -19,7 +19,7 @@ html.rl-no-preview-pane {
right: 0;
left: 0;
height: 30px;
padding: 10px @rlMainBorderSize;
padding: 10px 1px;
z-index: 102;
white-space: nowrap;
}
@ -64,7 +64,7 @@ html.rl-no-preview-pane {
left: 0;
bottom: @rlBottomMargin;
border: @rlMainBorderSize solid @rlMainDarkColor;
border: 1px solid @rlMainDarkColor;
box-shadow: @rlMainShadow;
border-radius: @rlMainBorderRadius;
@ -90,7 +90,7 @@ html.rl-no-preview-pane {
.checkboxCheckAll {
cursor: pointer;
line-height: 30px;
vertical-align: middle
}
.input-append {
@ -134,10 +134,6 @@ html.rl-no-preview-pane {
background-color: #fff;
.content {
-webkit-overflow-scrolling: touch;
}
.listClear {
text-align: center;
padding: 10px;

View file

@ -1,4 +1,3 @@
.g-ui-min-height-300 {
min-height: 300px;
}
@ -24,9 +23,9 @@
right: @rlLowMargin;
left: -2px;
/*overflow: hidden;*/
border: @rlLowBorderSize solid @rlMainDarkColor;
border: 1px solid @rlMainDarkColor;
border-radius: @rlLowBorderRadius;
border-radius: @rlMainBorderRadius;
background-color: #fff;
@ -57,10 +56,6 @@
color: #DA4F49;
}
.content {
-webkit-overflow-scrolling: touch;
}
.message-fixed-button-toolbar {
z-index: 100;
position: absolute;
@ -96,11 +91,8 @@
padding: 10px;
background-color: #f8f8f8;
border-top: 0;
border-bottom: 1px solid #ddd;
border-top-right-radius: 5px;
border-top-left-radius: 5px;
z-index: 1;
border-radius: @rlMainBorderRadius @rlMainBorderRadius 0 0;
.subjectParent {
font-size: 18px;
@ -109,9 +101,6 @@
overflow: hidden;
white-space: nowrap;
margin-bottom: 8px;
line-height: 100%;
height: 22px;
vertical-align: middle;
}
.messageButtons {
@ -190,8 +179,6 @@
left: 0;
right: 0;
overflow: auto;
-webkit-overflow-scrolling: touch;
border-radius: @rlLowBorderRadius;
scroll-behavior: smooth;
.buttonFull {
@ -294,13 +281,10 @@
}
.rlBlockquoteSwitcher {
background-color: #eee;
border: 1px solid #999;
display: block;
width: 30px;
height: 14px;
line-height: 14px;
width: 3em;
line-height: 1em;
text-align: center;
cursor: pointer;
margin: 2em 0 10px;
@ -311,10 +295,6 @@
}
}
.bodySubHeader {
z-index: 2;
}
.b-text-part {
height: 100%;
@ -458,19 +438,21 @@
display: inline-block;
cursor: pointer;
color: #777;
opacity: 0.5;
&:hover {
color: #111;
opacity: 1;
}
&.success {
color: green;
cursor: help;
opacity: 1;
}
&.error {
color: red;
opacity: 1;
}
}
}
@ -479,7 +461,6 @@
&.message-focused .b-content {
z-index: 101;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
border-radius: @rlLowBorderRadius;
border-color: darken(@rlMainDarkColor, 5%);
}
@ -528,7 +509,7 @@ html.rl-no-preview-pane .messageView {
}
.toolbar {
padding-left: @rlMainBorderSize;
padding-left: 1px;
}
.b-content {
@ -536,9 +517,8 @@ html.rl-no-preview-pane .messageView {
left: 0;
bottom: @rlBottomMargin;
right: @rlBottomMargin;
border: @rlMainBorderSize solid @rlMainDarkColor;
border: 1px solid @rlMainDarkColor;
box-shadow: @rlMainShadow;
border-radius: @rlMainBorderRadius;
}
}
@ -577,8 +557,7 @@ html.rl-message-fullscreen {
right: 0 !important;
bottom: 0 !important;
z-index: 10000 !important;
border: @rlLowBorderSize solid @rlMainDarkColor !important;
border-radius: @rlLowBorderRadius !important;
border: 1px solid @rlMainDarkColor !important;
}
}

View file

@ -1,21 +1,11 @@
.b-settins-left {
.b-toolbar {
position: absolute;
top: 0;
right: 0;
left: 0;
height: 34px;
padding: 8px 0 0 @rlLowMargin;
}
.b-footer {
position: absolute;
bottom: 20px;
bottom: 10px;
right: 0;
left: 0;
height: 20px;
padding: 0 10px 0 5px;
z-index: 101;
}
@ -27,50 +17,29 @@
left: 0;
right: 0;
overflow: hidden;
}
.content {
-webkit-overflow-scrolling: touch;
nav {
a {
background-color: transparent;
color: var(--settings-menu-color, #333);
cursor: pointer;
display: block;
font-size: 18px;
height: 30px;
line-height: 29px;
overflow: hidden;
padding: 4px 10px;
text-decoration: none;
}
}
}
.b-settings-menu {
.e-item {
overflow: hidden;
text-decoration: none;
outline: 0;
}
.e-link {
position: relative;
display: block;
height: 30px;
line-height: 29px;
font-size: 18px;
z-index: 1;
cursor: default;
background-color: transparent;
color: var(--settings-menu-disabled-color, #666);
padding: 4px 10px;
outline: 0;
text-decoration: none;
}
.e-item.selectable .e-link {
cursor: pointer;
color: var(--settings-menu-color, #333);
}
.e-item.selectable {
&:hover .e-link{
a:focus, a:hover {
background-color: var(--settings-menu-hover-bg-color, #333);
color: var(--settings-menu-hover-color, #eee);
}
&.selected .e-link {
a.selected {
background-color: var(--settings-menu-selected-bg-color, #333);
color: var(--settings-menu-selected-color, #eee);
}
@ -85,7 +54,7 @@
right: 0;
left: 0;
height: 34px;
padding: 8px 5px;
padding: 8px 0;
color: #fff;
}
@ -100,13 +69,9 @@
scroll-behavior: smooth;
background-color: #fff;
border: @rlMainBorderSize solid @rlMainDarkColor;
border: 1px solid @rlMainDarkColor;
box-shadow: @rlMainShadow;
border-radius: @rlMainBorderRadius;
.content {
-webkit-overflow-scrolling: touch;
}
}
td {

View file

@ -2,7 +2,6 @@
.squire-toolbar {
padding: 2px;
border-bottom: 1px solid #b6b6b6;
background: #EEE;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
@ -143,7 +142,6 @@ Secondly, we can't rely on MUA's what to do with :empty
border-radius: 0;
display: none;
font-family: var(--fontMono);
outline: none;
margin: 0;
resize: none;
white-space: pre-wrap;
@ -162,6 +160,7 @@ Secondly, we can't rely on MUA's what to do with :empty
display: none;
}
.rl-mobile .squire-toolbar {
background: #EEE;
padding: 1px;
position: fixed;
bottom: 0;

View file

@ -23,7 +23,7 @@
.email-title {
display: inline-block;
max-width: 200px;
max-width: 50vw;
text-align: left;
text-overflow: ellipsis;
overflow: hidden;
@ -61,41 +61,27 @@
}
.accountPlace {
background-color: #000;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 4px;
color: #fff;
text-shadow: 0 1px 0 #000;
display: inline-block;
height: 29px;
max-width: 250px;
border-radius: 4px;
font-size: 16px;
line-height: 30px;
padding: 1px 8px;
overflow: hidden;
text-overflow: ellipsis;
border-radius: 4px;
font-weight: bold;
font-size: 16px;
height: 29px;
line-height: 30px;
max-width: 25vw;
overflow: hidden;
padding: 1px 8px;
text-overflow: ellipsis;
text-shadow: 0 1px 0 #000;
white-space: nowrap;
}
.account-item {
.icon-ok {
&:not(.current) i:first-child,
&.current i + i {
display: none;
}
&.current {
.icon-ok {
display: inline-block;
}
.icon-user {
display: none;
}
}
}
.counter {
@ -107,13 +93,6 @@
}
}
/* mobile */
@media screen and (max-width: 767px) {
.accountPlace {
max-width: 150px !important;
}
}
@keyframes firstBar {
0% { height: 30%; }
50% { height: 100%; }

View file

@ -17,7 +17,7 @@
top: 100%;
transition: opacity 0.2s ease-in-out;
white-space: nowrap;
z-index: 2001 !important;
z-index: 2001;
}
@media only screen and (max-width: 600px) {
[data-rainloopErrorTip]::before {

View file

@ -1,11 +1,7 @@
@rlLeftWidth: 200px;
@rlInputBorderSize: 1px;
@rlMainBorderSize: 1px;
@rlLowBorderSize: 1px;
@rlMainBorderRadius: 5px;
@rlLowBorderRadius: 3px;
@rlMainShadow: 0 2px 8px rgba(0, 0, 0, 0.2);
@ -13,6 +9,3 @@
@rlBottomMargin: 5px;
@rlMainDarkColor: #aaa;
// bootstart
@btnBorderRadius: 3px;

View file

@ -1,33 +0,0 @@
#progressjs {
left: 0;
position: fixed;
top: 0;
width: 100%;
z-index: 2000;
}
.progressjs-inner {
background-color: #939595;
height: 3px;
overflow: hidden;
position: relative;
transition: width .5s;
width: 0;
z-index: 2000;
}
.progressjs-percent {
position: absolute;
top: 0;
left: 0;
right: -32px;
bottom: 0;
background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.3) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.3) 50%, rgba(255, 255, 255, 0.3) 75%, transparent 75%, transparent);
background-size: 32px 32px;
animation: simple-pace-stripe-animation 500ms linear infinite;
}
@keyframes simple-pace-stripe-animation {
0% { transform: none; }
100% { transform: translate(-32px, 0); }
}

View file

@ -21,8 +21,6 @@ class LoginAdminView extends AbstractViewCenter {
loginError: false,
passwordError: false,
formHidden: false,
submitRequest: false,
submitError: ''
});

View file

@ -1,6 +1,3 @@
import { Scope } from 'Common/Enums';
import { settingsMenuKeysHandler } from 'Knoin/Knoin';
import { AbstractViewLeft } from 'Knoin/AbstractViews';
class MenuSettingsAdminView extends AbstractViewLeft {
@ -16,11 +13,6 @@ class MenuSettingsAdminView extends AbstractViewLeft {
link(route) {
return '#/' + route;
}
onBuild(dom) {
shortcuts.add('arrowup,arrowdown', '', Scope.Settings,
settingsMenuKeysHandler(dom.querySelectorAll('.b-admin-menu .e-item')));
}
}
export { MenuSettingsAdminView };

View file

@ -419,7 +419,24 @@ class ComposePopupView extends AbstractViewPopup {
setFolderHash(sSentFolder, '');
Remote.sendMessage(
this.sendMessageResponse.bind(this),
(iError, data) => {
this.sending(false);
if (this.modalVisibility()) {
if (iError) {
if (Notification.CantSaveMessage === iError) {
this.sendSuccessButSaveError(true);
this.savedErrorDesc(i18n('COMPOSE/SAVED_ERROR_ON_SEND').trim());
} else {
this.sendError(true);
this.sendErrorDesc(getNotification(iError, data && data.ErrorMessage)
|| getNotification(Notification.CantSendMessage));
}
} else {
this.closeCommand && this.closeCommand();
}
}
this.reloadDraftFolder();
},
this.getMessageRequestParams(sSentFolder)
);
}
@ -438,7 +455,40 @@ class ComposePopupView extends AbstractViewPopup {
setFolderHash(FolderUserStore.draftFolder(), '');
Remote.saveMessage(
this.saveMessageResponse.bind(this),
(iError, oData) => {
let result = false;
this.saving(false);
if (!iError) {
if (oData.Result.NewFolder && oData.Result.NewUid) {
result = true;
if (this.bFromDraft) {
const message = MessageUserStore.message();
if (message && this.draftFolder() === message.folder && this.draftUid() === message.uid) {
MessageUserStore.message(null);
}
}
this.draftFolder(oData.Result.NewFolder);
this.draftUid(oData.Result.NewUid);
this.savedTime(new Date);
if (this.bFromDraft) {
setFolderHash(this.draftFolder(), '');
}
}
}
if (!result) {
this.savedError(true);
this.savedErrorDesc(getNotification(Notification.CantSaveMessage));
}
this.reloadDraftFolder();
},
this.getMessageRequestParams(FolderUserStore.draftFolder())
);
}
@ -578,63 +628,6 @@ class ComposePopupView extends AbstractViewPopup {
}
}
sendMessageResponse(iError, data) {
this.sending(false);
if (this.modalVisibility()) {
if (iError) {
if (Notification.CantSaveMessage === iError) {
this.sendSuccessButSaveError(true);
this.savedErrorDesc(i18n('COMPOSE/SAVED_ERROR_ON_SEND').trim());
} else {
this.sendError(true);
this.sendErrorDesc(getNotification(iError, data && data.ErrorMessage)
|| getNotification(Notification.CantSendMessage));
}
} else {
this.closeCommand && this.closeCommand();
}
}
this.reloadDraftFolder();
}
saveMessageResponse(iError, oData) {
let result = false;
this.saving(false);
if (!iError) {
if (oData.Result.NewFolder && oData.Result.NewUid) {
result = true;
if (this.bFromDraft) {
const message = MessageUserStore.message();
if (message && this.draftFolder() === message.folder && this.draftUid() === message.uid) {
MessageUserStore.message(null);
}
}
this.draftFolder(oData.Result.NewFolder);
this.draftUid(oData.Result.NewUid);
this.savedTime(new Date);
if (this.bFromDraft) {
setFolderHash(this.draftFolder(), '');
}
}
}
if (!result) {
this.savedError(true);
this.savedErrorDesc(getNotification(Notification.CantSaveMessage));
}
this.reloadDraftFolder();
}
onHide() {
// Stop autosave
clearTimeout(this.iTimer);
@ -748,12 +741,6 @@ class ComposePopupView extends AbstractViewPopup {
}
}
onWarmUp() {
if (this.modalVisibility && !this.modalVisibility()) {
this.editor(editor => editor.modeWysiwyg());
}
}
/**
* @param {Function} fKoValue
* @param {Array} emails
@ -1146,6 +1133,8 @@ class ComposePopupView extends AbstractViewPopup {
ro.header = dom.querySelector('.b-header');
ro.toolbar = dom.querySelector('.b-header-toolbar');
ro.els = [dom.querySelector('.textAreaParent'), dom.querySelector('.attachmentAreaParent')];
this.editor(editor => editor.modeWysiwyg());
}
/**

View file

@ -90,7 +90,7 @@ class ContactsPopupView extends AbstractViewPopup {
this.bDropPageAfterDelete = false;
// this.saveCommandDebounce = _.debounce(this.saveCommand.bind(this), 1000);
// this.saveCommandDebounce = this.saveCommand.bind(this).debounce(1000);
const
// propertyFocused = property => !property.isValid() && !property.focused(),
@ -382,26 +382,18 @@ class ContactsPopupView extends AbstractViewPopup {
deleteSelectedContacts() {
if (this.contactsCheckedOrSelected().length) {
Remote.contactsDelete(this.deleteResponse.bind(this), this.contactsCheckedOrSelectedUids());
Remote.contactsDelete((iError, oData) => {
if (500 < (!iError && oData && oData.Time ? pInt(oData.Time) : 0)) {
this.reloadContactList(this.bDropPageAfterDelete);
} else {
setTimeout(() => this.reloadContactList(this.bDropPageAfterDelete), 500);
}
}, this.contactsCheckedOrSelectedUids());
this.removeCheckedOrSelectedContactsFromList();
}
}
/**
* @param {int} iError
* @param {FetchJsonDefaultResponse} oData
*/
deleteResponse(iError, oData) {
if (500 < (!iError && oData && oData.Time ? pInt(oData.Time) : 0)) {
this.reloadContactList(this.bDropPageAfterDelete);
} else {
setTimeout(() => {
this.reloadContactList(this.bDropPageAfterDelete);
}, 500);
}
}
removeProperty(oProp) {
this.viewProperties.remove(oProp);
delegateRunOnDestroy(oProp);

View file

@ -187,7 +187,48 @@ class DomainPopupView extends AbstractViewPopup {
this.testing(true);
Remote.testConnectionForDomain(
this.onTestConnectionResponse.bind(this),
(iError, oData) => {
this.testing(false);
if (iError) {
this.testingImapError(true);
this.testingSieveError(true);
this.testingSmtpError(true);
this.sieveSettings(false);
} else {
let bImap = false,
bSieve = false;
this.testingDone(true);
this.testingImapError(true !== oData.Result.Imap);
this.testingSieveError(true !== oData.Result.Sieve);
this.testingSmtpError(true !== oData.Result.Smtp);
if (this.testingImapError() && oData.Result.Imap) {
bImap = true;
this.testingImapErrorDesc('');
this.testingImapErrorDesc(oData.Result.Imap);
}
if (this.testingSieveError() && oData.Result.Sieve) {
bSieve = true;
this.testingSieveErrorDesc('');
this.testingSieveErrorDesc(oData.Result.Sieve);
}
if (this.testingSmtpError() && oData.Result.Smtp) {
this.testingSmtpErrorDesc('');
this.testingSmtpErrorDesc(oData.Result.Smtp);
}
if (this.sieveSettings()) {
if (!bSieve && bImap) {
this.sieveSettings(false);
}
} else if (bSieve && !bImap) {
this.sieveSettings(true);
}
}
},
this
);
}
@ -205,49 +246,6 @@ class DomainPopupView extends AbstractViewPopup {
this.clearTesting();
}
onTestConnectionResponse(iError, oData) {
this.testing(false);
if (iError) {
this.testingImapError(true);
this.testingSieveError(true);
this.testingSmtpError(true);
this.sieveSettings(false);
} else {
let bImap = false,
bSieve = false;
this.testingDone(true);
this.testingImapError(true !== oData.Result.Imap);
this.testingSieveError(true !== oData.Result.Sieve);
this.testingSmtpError(true !== oData.Result.Smtp);
if (this.testingImapError() && oData.Result.Imap) {
bImap = true;
this.testingImapErrorDesc('');
this.testingImapErrorDesc(oData.Result.Imap);
}
if (this.testingSieveError() && oData.Result.Sieve) {
bSieve = true;
this.testingSieveErrorDesc('');
this.testingSieveErrorDesc(oData.Result.Sieve);
}
if (this.testingSmtpError() && oData.Result.Smtp) {
this.testingSmtpErrorDesc('');
this.testingSmtpErrorDesc(oData.Result.Smtp);
}
if (this.sieveSettings()) {
if (!bSieve && bImap) {
this.sieveSettings(false);
}
} else if (bSieve && !bImap) {
this.sieveSettings(true);
}
}
}
onDomainCreateOrSaveResponse(iError) {
this.saving(false);
if (iError) {

View file

@ -27,8 +27,7 @@ class LanguagesPopupView extends AbstractViewPopup {
}
languageTooltipName(language) {
const result = convertLangName(language, true);
return convertLangName(language, false) === result ? '' : result;
return convertLangName(language, true);
}
setLanguageSelection() {

View file

@ -14,8 +14,6 @@ class PluginPopupView extends AbstractViewPopup {
constructor() {
super('Plugin');
this.onPluginSettingsUpdateResponse = this.onPluginSettingsUpdateResponse.bind(this);
this.addObservables({
saveError: '',
name: '',
@ -50,15 +48,11 @@ class PluginPopupView extends AbstractViewPopup {
});
this.saveError('');
Remote.pluginSettingsUpdate(this.onPluginSettingsUpdateResponse, list);
}
onPluginSettingsUpdateResponse(iError) {
if (iError) {
this.saveError(getNotification(iError));
} else {
this.cancelCommand();
}
Remote.pluginSettingsUpdate(iError =>
iError
? this.saveError(getNotification(iError))
: this.cancelCommand()
, list);
}
onShow(oPlugin) {

View file

@ -46,8 +46,6 @@ class LoginUserView extends AbstractViewCenter {
emailError: false,
passwordError: false,
formHidden: false,
submitRequest: false,
submitError: '',
submitErrorAddidional: '',
@ -57,9 +55,6 @@ class LoginUserView extends AbstractViewCenter {
signMeType: LoginSignMeType.Unused
});
this.forgotPasswordLinkUrl = Settings.app('forgotPasswordLinkUrl');
this.registrationLinkUrl = Settings.app('registrationLinkUrl');
this.formError = ko.observable(false).extend({ falseTimeout: 500 });
this.allowLanguagesOnLogin = !!SettingsGet('AllowLanguagesOnLogin');
@ -127,7 +122,8 @@ class LoginUserView extends AbstractViewCenter {
if (Notification.InvalidInputArgument == iError) {
iError = Notification.AuthError;
}
this.submitError(getNotification(iError, oData.ErrorMessage, Notification.UnknownNotification));
this.submitError(getNotification(iError, (oData ? oData.ErrorMessage : ''),
Notification.UnknownNotification));
this.submitErrorAddidional((oData && oData.ErrorMessageAdditional) || '');
} else {
rl.route.reload();

View file

@ -108,16 +108,16 @@ export class FolderListMailBoxUserView extends AbstractViewLeft {
}
}
el = eqs(event, '.b-folders .e-item .e-link.selectable .inbox-star-icon');
el = eqs(event, '.b-folders .e-item a.selectable .inbox-star-icon');
el && fSelectFolder(el, event, !this.isInboxStarred());
el = eqs(event, '.b-folders .e-item .e-link.selectable');
el = eqs(event, '.b-folders .e-item a.selectable');
el && fSelectFolder(el, event, false);
});
shortcuts.add('arrowup,arrowdown', '', Scope.FolderList, event => {
let items = [], index = 0;
dom.querySelectorAll('.b-folders .e-item .e-link:not(.hidden)').forEach(node => {
dom.querySelectorAll('.b-folders .e-item a:not(.hidden)').forEach(node => {
if (node.offsetHeight || node.getClientRects().length) {
items.push(node);
if (node.matches('.focused')) {
@ -140,7 +140,7 @@ export class FolderListMailBoxUserView extends AbstractViewLeft {
});
shortcuts.add('enter,open', '', Scope.FolderList, () => {
const item = qs('.b-folders .e-item .e-link:not(.hidden).focused');
const item = qs('.b-folders .e-item a:not(.hidden).focused');
if (item) {
AppUserStore.focusedState(Scope.MessageList);
item.click();
@ -150,7 +150,7 @@ export class FolderListMailBoxUserView extends AbstractViewLeft {
});
shortcuts.add('space', '', Scope.FolderList, () => {
const item = qs('.b-folders .e-item .e-link:not(.hidden).focused'),
const item = qs('.b-folders .e-item a:not(.hidden).focused'),
folder = item && ko.dataFor(item);
if (folder) {
const collapsed = folder.collapsed();
@ -169,11 +169,11 @@ export class FolderListMailBoxUserView extends AbstractViewLeft {
});
AppUserStore.focusedState.subscribe(value => {
let el = qs('.b-folders .e-item .e-link.focused');
el && qs('.b-folders .e-item .e-link.focused').classList.remove('focused');
let el = qs('.b-folders .e-item a.focused');
el && qs('.b-folders .e-item a.focused').classList.remove('focused');
if (Scope.FolderList === value) {
el = qs('.b-folders .e-item .e-link.selected');
el && qs('.b-folders .e-item .e-link.selected').classList.add('focused');
el = qs('.b-folders .e-item a.selected');
el && qs('.b-folders .e-item a.selected').classList.add('focused');
}
});
}
@ -181,7 +181,7 @@ export class FolderListMailBoxUserView extends AbstractViewLeft {
scrollToFocused() {
const scrollable = this.oContentScrollable;
if (scrollable) {
let block, focused = scrollable.querySelector('.e-item .e-link.focused');
let block, focused = scrollable.querySelector('.e-item a.focused');
if (focused) {
const fRect = focused.getBoundingClientRect(),
sRect = scrollable.getBoundingClientRect();

View file

@ -263,8 +263,6 @@ class MessageViewMailBoxUserView extends AbstractViewRight {
addEventListener('mailbox.message-view.toggle-full-screen', () => this.toggleFullScreen());
this.attachmentPreview = this.attachmentPreview.bind(this);
decorateKoCommands(this, {
closeMessageCommand: 1,
messageEditCommand: self => self.messageVisibility(),
@ -313,57 +311,6 @@ class MessageViewMailBoxUserView extends AbstractViewRight {
this.oHeaderDom && this.viewBodyTopValue(this.message() ? this.oHeaderDom.offsetHeight : 0);
}
// displayMailToPopup(sMailToUrl) {
// sMailToUrl = sMailToUrl.replace(/\?.+$/, '');
//
// var
// sResult = '',
// aTo = [],
// EmailModel = require('Model/Email').default,
// fParseEmailLine = function(sLine) {
// return sLine ? [decodeURIComponent(sLine)].map(sItem => {
// var oEmailModel = new EmailModel();
// oEmailModel.parse(sItem);
// return oEmailModel.email ? oEmailModel : null;
// }).filter(v => v) : null;
// }
// ;
//
// aTo = fParseEmailLine(sMailToUrl);
// sResult = aTo && aTo[0] ? aTo[0].email : '';
//
// return sResult;
// }
/**
* @param {Object} oAttachment
* @returns {boolean}
*/
attachmentPreview(/*attachment*/) {
/*
if (attachment && attachment.isImage() && !attachment.isLinked && this.message() && this.message().attachments()) {
const items = this.message().attachments.map(item => {
if (item && !item.isLinked && item.isImage()) {
if (item === attachment) {
index = listIndex;
}
++listIndex;
return {
src: item.linkPreview(),
msrc: item.linkThumbnail(),
title: item.fileName
};
}
return null;
}).filter(v => v);
if (items.length) {
}
}
*/
return true;
}
onBuild(dom) {
this.fullScreenMode.subscribe(value => value && this.message() && AppUserStore.focusedState(Scope.MessageView));
@ -476,33 +423,28 @@ class MessageViewMailBoxUserView extends AbstractViewRight {
this.initShortcuts();
}
/**
* @returns {boolean}
*/
escShortcuts() {
if (this.viewModelVisible && this.message()) {
const preview = SettingsUserStore.usePreviewPane();
if (this.fullScreenMode()) {
this.fullScreenMode(false);
if (preview) {
AppUserStore.focusedState(Scope.MessageList);
}
} else if (!preview) {
this.message(null);
} else {
AppUserStore.focusedState(Scope.MessageList);
}
return false;
}
return true;
}
initShortcuts() {
// exit fullscreen, back
shortcuts.add('escape,backspace', '', Scope.MessageView, this.escShortcuts.bind(this));
shortcuts.add('escape,backspace', '', Scope.MessageView, () => {
if (this.viewModelVisible && this.message()) {
const preview = SettingsUserStore.usePreviewPane();
if (this.fullScreenMode()) {
this.fullScreenMode(false);
if (preview) {
AppUserStore.focusedState(Scope.MessageList);
}
} else if (!preview) {
this.message(null);
} else {
AppUserStore.focusedState(Scope.MessageList);
}
return false;
}
return true;
});
// fullscreen
shortcuts.add('enter,open', '', Scope.MessageView, () => {

View file

@ -1,9 +1,7 @@
import { Scope } from 'Common/Enums';
import { leftPanelDisabled } from 'Common/Globals';
import { settings, mailbox } from 'Common/Links';
import { getFolderInboxName } from 'Common/Cache';
import { settingsMenuKeysHandler } from 'Knoin/Knoin';
import { AbstractViewLeft } from 'Knoin/AbstractViews';
import { ThemeStore } from 'Stores/Theme';
@ -23,12 +21,9 @@ export class MenuSettingsUserView extends AbstractViewLeft {
onBuild(dom) {
dom.addEventListener('click', event =>
ThemeStore.isMobile()
&& event.target.closestWithin('.b-settings-menu .e-item.selectable', dom)
&& event.target.closestWithin('.b-settins-left nav a', dom)
&& leftPanelDisabled(true)
);
shortcuts.add('arrowup,arrowdown', '', Scope.Settings,
settingsMenuKeysHandler(dom.querySelectorAll('.b-settings-menu .e-item')));
}
link(route) {

View file

@ -38,17 +38,10 @@ const
}
return win[name];
},
STORAGE_KEY = '__rlA',
TIME_KEY = '__rlT',
AUTH_KEY = 'AuthAccountHash',
storage = Storage('session'),
timestamp = () => Math.round(Date.now() / 1000),
setTimestamp = () => storage.setItem(TIME_KEY, timestamp()),
showError = () => {
eId('rl-loading').hidden = true;
eId('rl-loading-error').hidden = false;
p.end();
},
loadScript = src => {
@ -57,29 +50,12 @@ const
}
return new Promise((resolve, reject) => {
const script = doc.createElement('script');
script.onload = () => {
p.set(pStep += step);
resolve();
};
script.onload = () => resolve();
script.onerror = () => reject(new Error(src));
script.src = src;
// script.async = true;
doc.head.append(script);
});
},
step = 100 / 7,
p = win.progressJs = {
set: percent => progress.style.width = Math.min(percent, 100) + '%',
end: () => {
if (container) {
p.set(100);
setTimeout(() => {
container.remove();
container = progress = null;
}, 600);
}
}
};
if (!navigator || !navigator.cookieEnabled) {
@ -89,37 +65,15 @@ if (!navigator || !navigator.cookieEnabled) {
const layout = getCookie('rllayout');
doc.documentElement.classList.toggle('rl-mobile', 'mobile' === layout || (!layout && 1000 > innerWidth));
let pStep = 0,
container = eId('progressjs'),
progress = container.querySelector('.progressjs-inner'),
let progress = eId('progressjs'),
RL_APP_DATA = {};
if (progress) {
progress.remove();
progress = null;
}
win.rl = {
hash: {
// getHash
get: () => storage.getItem(STORAGE_KEY) || null,
// setHash
set: () => {
storage.setItem(STORAGE_KEY, RL_APP_DATA && RL_APP_DATA[AUTH_KEY]
? RL_APP_DATA[AUTH_KEY] : '');
setTimestamp();
},
// clearHash
clear: () => {
storage.setItem(STORAGE_KEY, '');
setTimestamp();
},
// checkTimestamp
check: () => {
if (timestamp() > (parseInt(storage.getItem(TIME_KEY) || 0, 10) || 0) + 3600000) {
// 60m
rl.hash.clear();
return true;
}
return false;
}
},
data: () => RL_APP_DATA,
adminArea: () => admin,
settings: {
@ -142,15 +96,8 @@ win.rl = {
initData: appData => {
RL_APP_DATA = appData;
rl.hash.set();
if (appData) {
if (appData.NewThemeLink) {
eId('app-theme-link').href = appData.NewThemeLink;
}
loadScript(appData.StaticLibJsLink)
.then(() => Promise.all([loadScript(appData.TemplatesLink), loadScript(appData.LangLink)]))
.then(() => loadScript(appData.StaticAppJsLink))
.then(() => appData.PluginsLink ? loadScript(appData.PluginsLink) : Promise.resolve())
.then(() => win.__APP_BOOT ? win.__APP_BOOT(showError) : showError())
@ -164,16 +111,11 @@ win.rl = {
}
};
p.set(1);
Storage('local');
// init section
setInterval(setTimestamp, 60000); // 1m
eId('app-css').href = eId('app-css').dataset.href;
[eId('app-css'),eId('app-theme-link')].forEach(css => css.href = css.dataset.href);
loadScript(`./?/${admin ? 'Admin' : ''}AppData/${rl.hash.get() || '0'}/${Math.random().toString().substr(2)}/`)
loadScript(`./?/${admin ? 'Admin' : ''}AppData/0/${Math.random().toString().substr(2)}/`)
.then(() => {});
})(this);

20
dev/bootstrap.js vendored
View file

@ -1,9 +1,9 @@
import { doc, elementById, dropdownVisibility, Settings } from 'Common/Globals';
import { doc, dropdownVisibility, Settings } from 'Common/Globals';
import { i18n } from 'Common/Translator';
import { root } from 'Common/Links';
export default (App) => {
export default App => {
addEventListener('keydown', event => {
event = event || window.event;
@ -135,17 +135,11 @@ export default (App) => {
});
};
window.__APP_BOOT = fErrorCallback => {
const cb = () => setTimeout(() => {
if (rl.TEMPLATES) {
elementById('rl-templates').innerHTML = rl.TEMPLATES;
setTimeout(() => App.bootstart(), 10);
} else {
fErrorCallback();
}
window.__APP_BOOT = null;
}, 10);
window.__APP_BOOT = () => {
const cb = () => {
window.__APP_BOOT = null;
App.bootstart();
};
('loading' !== doc.readyState) ? cb() : doc.addEventListener('DOMContentLoaded', cb);
};
};

View file

@ -3,7 +3,7 @@
"title": "SnappyMail",
"description": "Simple, modern & fast web-based email client",
"private": true,
"version": "2.5.0-rc.3",
"version": "2.5.2",
"homepage": "https://snappymail.eu",
"author": {
"name": "DJ Maze",
@ -64,7 +64,6 @@
"gulp-terser": "^1.4.0",
"gulp-util": "3.0.8",
"node-fs": "0.1.7",
"openpgp": "2.6.2",
"rimraf": "3.0.2"
},
"dependencies": {

View file

@ -14,6 +14,8 @@ class AutoDomainGrabPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = '',
VERSION = '2.1',
REQUIRED = '2.5.0',
CATEGORY = 'General',
DESCRIPTION = '';

View file

@ -4,7 +4,9 @@ class BlackListPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Black list',
VERSION = '2.0',
VERSION = '2.1',
RELEASE = '2021-04-21',
REQUIRED = '2.5.0',
CATEGORY = 'Login',
DESCRIPTION = 'Simple black list plugin (with wildcard and exceptions functionality).';

View file

@ -5,7 +5,7 @@ class CKEditorPlugin extends \RainLoop\Plugins\AbstractPlugin
const
NAME = 'CKEditor',
VERSION = '2.1',
RELEASE = '2021-04-01',
RELEASE = '2021-04-19',
REQUIRED = '2.5.0',
DESCRIPTION = 'Use CKEditor instead of Squire as WYSIWYG';

View file

@ -4,6 +4,8 @@ class CustomAuthExamplePlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = '',
VERSION = '2.1',
REQUIRED = '2.5.0',
CATEGORY = 'Login',
DESCRIPTION = '';

View file

@ -4,7 +4,9 @@ class CustomLoginMappingPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Custom Login Mapping',
VERSION = '2.0',
VERSION = '2.1',
RELEASE = '2021-04-21',
REQUIRED = '2.5.0',
CATEGORY = 'Login',
DESCRIPTION = 'Plugin which allows you to set up custom username by email address.';

View file

@ -0,0 +1,33 @@
(rl => {
if (rl) {
const
forgotUrl = rl.settings.get('forgotPasswordLinkUrl'),
registerUrl = rl.settings.get('registrationLinkUrl');
if (forgotUrl || registerUrl) {
addEventListener('rl-view-model', e => {
if (e.detail && 'Login' === e.detail.viewModelTemplateID) {
const container = e.detail.viewModelDom.querySelector('#plugin-Login-BottomControlGroup'),
forgot = 'LOGIN/LABEL_FORGOT_PASSWORD',
register = 'LOGIN/LABEL_REGISTRATION';
if (container) {
let html = '';
if (forgotUrl) {
html = html + '<p class="forgot-link">'
+ '<a href="'+forgotUrl+'" target="_blank" class="g-ui-link" data-i18n="'+forgot+'">'+rl.i18n(forgot)+'</a>'
+ '</p>';
}
if (registerUrl) {
html = html + '<p class="registration-link">'
+ '<a href="'+registerUrl+'" target="_blank" class="g-ui-link" data-i18n="'+register+'">'+rl.i18n(register)+'</a>'
+ '</p>';
}
container.append(Element.fromHTML('<div class="controls clearfix">' + html + '</div>'));
}
}
});
}
}
})(window.rl);

View file

@ -0,0 +1,42 @@
<?php
class LoginRegisterPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Register and Forgot',
VERSION = '2.0',
RELEASE = '2021-06-01',
REQUIRED = '2.5.2',
CATEGORY = 'Login',
DESCRIPTION = 'Links on login screen for registration and forgot password';
public function Init() : void
{
$this->UseLangs(true);
$this->addJs('LoginRegister.js');
$this->addHook('filter.app-data', 'FilterAppData');
}
public function configMapping() : array
{
return [
\RainLoop\Plugins\Property::NewInstance("forgot_password_link_url")
// ->SetLabel('TAB_LOGIN/LABEL_FORGOT_PASSWORD_LINK_URL')
->SetLabel('Forgot password url')
->SetType(\RainLoop\Enumerations\PluginPropertyType::URL),
\RainLoop\Plugins\Property::NewInstance("registration_link_url")
// ->SetLabel('TAB_LOGIN/LABEL_REGISTRATION_LINK_URL')
->SetLabel('Register url')
->SetType(\RainLoop\Enumerations\PluginPropertyType::URL),
];
}
public function FilterAppData($bAdmin, &$aResult)
{
if (!$bAdmin && \is_array($aResult) && empty($aResult['Auth'])) {
$aResult['forgotPasswordLinkUrl'] = \trim($this->Config()->Get('plugin', 'forgot_password_link_url', ''));
$aResult['registrationLinkUrl'] = \trim($this->Config()->Get('plugin', 'registration_link_url', ''));
}
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Passwort vergessen",
"LABEL_REGISTRATION": "Registrieren"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Forgot password",
"LABEL_REGISTRATION": "Register"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Olvidé mi contraseña",
"LABEL_REGISTRATION": "Registrarse"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Mot de passe oublié",
"LABEL_REGISTRATION": "S'enregistrer"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Elfelejtett jelszó",
"LABEL_REGISTRATION": "Regisztráció"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Wachtwoord vergeten",
"LABEL_REGISTRATION": "Registreren"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Glömt lösenord",
"LABEL_REGISTRATION": "Registrera"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "忘记密码",
"LABEL_REGISTRATION": "注册"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -4,7 +4,9 @@ class OverrideSmtpCredentialsPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Override Smtp Credentials',
VERSION = '2.0',
VERSION = '2.1',
RELEASE = '2021-04-21',
REQUIRED = '2.5.0',
CATEGORY = 'Filters',
DESCRIPTION = 'Plugin which allows you to override smtp credentials specified users.';

View file

@ -4,6 +4,8 @@ class ProxyauthLoginExamplePlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = '',
VERSION = '2.1',
REQUIRED = '2.5.0',
CATEGORY = 'General',
DESCRIPTION = '';

View file

@ -4,7 +4,9 @@ class WhiteListPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'White list',
VERSION = '2.0',
VERSION = '2.1',
RELEASE = '2021-04-21',
REQUIRED = '2.5.0',
CATEGORY = 'Login',
DESCRIPTION = 'Simple white list plugin (with wildcard and exceptions functionality).';

View file

@ -145,6 +145,10 @@ if ($brotli = trim(`which brotli`)) {
unlink(__DIR__ . '/snappymail/v/0.0.0/static/js/min/boot.min.js.br');
}
unlink(__DIR__ . '/snappymail/v/0.0.0/static/js/openpgp.js');
unlink(__DIR__ . '/snappymail/v/0.0.0/static/js/openpgp.js.br');
unlink(__DIR__ . '/snappymail/v/0.0.0/static/js/openpgp.js.gz');
// Temporary rename folder to speed up PharData
//if (!rename('snappymail/v/0.0.0', "snappymail/v/{$package->version}")){
if (!rename('snappymail/v/0.0.0', "snappymail/v/{$package->version}")) {

View file

@ -28,6 +28,11 @@ if (!\defined('RAINLOOP_APP_LIBRARIES_PATH'))
if (\class_exists('RainLoop\Api'))
{
if (!\SnappyMail\HTTP\SecFetch::isEntering() && !\SnappyMail\HTTP\SecFetch::isSameOrigin()) {
\MailSo\Base\Http::StatusHeader(403);
exit('Disallowed Sec-Fetch-Site: ' . ($_SERVER['HTTP_SEC_FETCH_SITE'] ?? ''));
}
\MailSo\Base\Loader::Init();
if (!empty($_ENV['RAINLOOP_INCLUDE_AS_API']))

View file

@ -43,7 +43,7 @@ abstract class DateTimeHelper
}
$sDateTime = \trim(\preg_replace('/ \([a-zA-Z0-9]+\)$/', '', $sDateTime));
$oDateTime = \DateTime::createFromFormat('D, d M Y H:i:s O', $sDateTime, \MailSo\Base\DateTimeHelper::GetUtcTimeZoneObject());
$oDateTime = \DateTime::createFromFormat(\DateTime::RFC2822, $sDateTime, static::GetUtcTimeZoneObject());
return $oDateTime ? $oDateTime->getTimestamp() : 0;
}
@ -61,10 +61,10 @@ abstract class DateTimeHelper
if (\preg_match('/^[a-z]{2,4}, /i', $sDateTime)) // RFC2822 ~ "Thu, 10 Jun 2010 08:58:33 -0700 (PDT)"
{
return \MailSo\Base\DateTimeHelper::ParseRFC2822DateString($sDateTime);
return static::ParseRFC2822DateString($sDateTime);
}
$oDateTime = \DateTime::createFromFormat('d-M-Y H:i:s O', $sDateTime, \MailSo\Base\DateTimeHelper::GetUtcTimeZoneObject());
$oDateTime = \DateTime::createFromFormat('d-M-Y H:i:s O', $sDateTime, static::GetUtcTimeZoneObject());
return $oDateTime ? $oDateTime->getTimestamp() : 0;
}
@ -79,7 +79,7 @@ abstract class DateTimeHelper
return 0;
}
$oDateTime = \DateTime::createFromFormat('Y-m-d H:i:s O', $sDateTime, \MailSo\Base\DateTimeHelper::GetUtcTimeZoneObject());
$oDateTime = \DateTime::createFromFormat('Y-m-d H:i:s O', $sDateTime, static::GetUtcTimeZoneObject());
return $oDateTime ? $oDateTime->getTimestamp() : 0;
}
@ -88,11 +88,16 @@ abstract class DateTimeHelper
*/
public static function TryToParseSpecEtagFormat(string $sDateTime) : int
{
$sDateTime = \trim(\preg_replace('/ \([a-zA-Z0-9]+\)$/', '', \trim($sDateTime)));
$sDateTime = \trim(\preg_replace('/(:[\d]{2})\.[\d]{3}/', '$1', \trim($sDateTime)));
$sDateTime = \trim(\preg_replace('/(-[\d]{2})T([\d]{2}:)/', '$1 $2', \trim($sDateTime)));
$sDateTime = \trim(\preg_replace('/([\-+][\d]{2}):([\d]{2})$/', ' $1$2', \trim($sDateTime)));
$oDateTime = \DateTime::createFromFormat(\DateTime::RFC3339_EXTENDED, $sDateTime, static::GetUtcTimeZoneObject());
if ($oDateTime) {
return $oDateTime->getTimestamp();
}
return \MailSo\Base\DateTimeHelper::ParseDateStringType1($sDateTime);
$sDateTime = \preg_replace('/ \([a-zA-Z0-9]+\)$/', '', \trim($sDateTime));
$sDateTime = \preg_replace('/(:[\d]{2})\.[\d]{3}/', '$1', \trim($sDateTime));
$sDateTime = \preg_replace('/(-[\d]{2})T([\d]{2}:)/', '$1 $2', \trim($sDateTime));
$sDateTime = \preg_replace('/([\-+][\d]{2}):([\d]{2})$/', ' $1$2', \trim($sDateTime));
return static::ParseDateStringType1($sDateTime);
}
}

View file

@ -31,66 +31,6 @@ class Http
return $oInstance;
}
public function HasQuery(string $sKey) : bool
{
return isset($_GET[$sKey]);
}
/**
* @param mixed $mDefault = null
*
* @return mixed
*/
public function GetQuery(string $sKey, $mDefault = null)
{
return isset($_GET[$sKey]) ? $_GET[$sKey] : $mDefault;
}
public function GetQueryAsArray() : ?array
{
return isset($_GET) && \is_array($_GET) ? $_GET : null;
}
public function HasPost(string $sKey) : bool
{
return isset($_POST[$sKey]);
}
/**
* @param mixed $mDefault = null
*
* @return mixed
*/
public function GetPost(string $sKey, $mDefault = null)
{
return isset($_POST[$sKey]) ? $_POST[$sKey] : $mDefault;
}
public function GetPostAsArray() : ?array
{
return isset($_POST) && \is_array($_POST) ? $_POST : null;
}
public function HasRequest(string $sKey) : bool
{
return isset($_REQUEST[$sKey]);
}
/**
* @param mixed $mDefault = null
*
* @return mixed
*/
public function GetRequest(string $sKey, $mDefault = null)
{
return isset($_REQUEST[$sKey]) ? $_REQUEST[$sKey] : $mDefault;
}
public function HasServer(string $sKey) : bool
{
return isset($_SERVER[$sKey]);
}
/**
* @param mixed $mDefault = null
*
@ -101,26 +41,6 @@ class Http
return isset($_SERVER[$sKey]) ? $_SERVER[$sKey] : $mDefault;
}
public function HasEnv(string $sKey) : bool
{
return isset($_ENV[$sKey]);
}
/**
* @param mixed $mDefault = null
*
* @return mixed
*/
public function GetEnv(string $sKey, $mDefault = null)
{
return isset($_ENV[$sKey]) ? $_ENV[$sKey] : $mDefault;
}
public function ServerProtocol() : string
{
return $this->GetServer('SERVER_PROTOCOL', 'HTTP/1.0');
}
public function GetMethod() : string
{
return $this->GetServer('REQUEST_METHOD', '');
@ -136,11 +56,6 @@ class Http
return ('GET' === $this->GetMethod());
}
public function GetQueryString() : string
{
return $this->GetServer('QUERY_STRING', '');
}
public function CheckLocalhost(string $sServer) : bool
{
return \in_array(\strtolower(\trim($sServer)), array(
@ -229,7 +144,7 @@ class Http
if ($bWithRemoteUserData)
{
$sUser = \trim($this->HasServer('REMOTE_USER') ? $this->GetServer('REMOTE_USER', '') : '');
$sUser = \trim($this->GetServer('REMOTE_USER', ''));
$sHost = (0 < \strlen($sUser) ? $sUser.'@' : '').$sHost;
}
@ -529,7 +444,7 @@ class Http
}
else
{
$this->StatusHeader(304);
static::StatusHeader(304);
$bResult = true;
}
}
@ -570,9 +485,8 @@ class Http
}
}
public function StatusHeader(int $iStatus, string $sCustomStatusText = '') : void
public static function StatusHeader(int $iStatus, string $sCustomStatusText = '') : void
{
$iStatus = (int) $iStatus;
if (99 < $iStatus)
{
$aStatus = array(
@ -588,11 +502,15 @@ class Http
416 => 'Requested range not satisfiable'
);
$sCustomStatusText = \trim($sCustomStatusText);
$sHeaderHead = \ini_get('cgi.rfc2616_headers') && false !== \strpos(\strtolower(\php_sapi_name()), 'cgi') ? 'Status:' : $this->ServerProtocol();
$sHeaderText = (0 === \strlen($sCustomStatusText) && isset($aStatus[$iStatus]) ? $aStatus[$iStatus] : $sCustomStatusText);
\header(\trim($sHeaderHead.' '.$iStatus.' '.$sHeaderText), true, $iStatus);
\http_response_code($iStatus);
if (isset($_SERVER['SERVER_PROTOCOL'])) {
\header("{$_SERVER['SERVER_PROTOCOL']} {$iStatus} {$sHeaderText}", true, $iStatus);
}
if (\ini_get('cgi.rfc2616_headers') && false !== \strpos(\strtolower(\php_sapi_name()), 'cgi')) {
\header("Status: {$iStatus} {$sHeaderText}");
}
}
}
@ -602,9 +520,9 @@ class Http
return '' === $sUrl ? '/' : '/'.$sUrl.'/';
}
public function GetUrl()
public function GetUrl() : string
{
return $this->GetServer('REQUEST_URI', '');
return $_SERVER['REQUEST_URI'] ?? '';
}
public function GetFullUrl() : string

View file

@ -65,7 +65,7 @@ class SubStreams
$sHashName = \MailSo\Base\Utils::Md5Rand();
self::$aStreams[$sHashName] = array_map(function($mItem) {
self::$aStreams[$sHashName] = \array_map(function ($mItem) {
return \is_resource($mItem) ? $mItem :
\MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString($mItem);
}, $aSubStreams);

View file

@ -132,7 +132,7 @@ class Xxtea
return $aV;
}
private static function int32(int $iN) : int
private static function int32($iN) : int
{
return $iN & 0xffffffff;
}

View file

@ -16,7 +16,7 @@ namespace MailSo\Cache\Drivers;
* @package Cache
* @subpackage Drivers
*/
class APC implements \MailSo\Cache\DriverInterface
class APCU implements \MailSo\Cache\DriverInterface
{
/**
* @var string
@ -35,25 +35,25 @@ class APC implements \MailSo\Cache\DriverInterface
public function Set(string $sKey, string $sValue) : bool
{
return \apc_store($this->generateCachedKey($sKey), (string) $sValue);
return \apcu_store($this->generateCachedKey($sKey), (string) $sValue);
}
public function Get(string $sKey) : string
{
$sValue = \apc_fetch($this->generateCachedKey($sKey));
$sValue = \apcu_fetch($this->generateCachedKey($sKey));
return \is_string($sValue) ? $sValue : '';
}
public function Delete(string $sKey) : void
{
\apc_delete($this->generateCachedKey($sKey));
\apcu_delete($this->generateCachedKey($sKey));
}
public function GC(int $iTimeToClearInHours = 24) : bool
{
if (0 === $iTimeToClearInHours)
{
return \apc_clear_cache('user');
return \apcu_clear_cache('user');
}
return false;

View file

@ -104,31 +104,24 @@ class FetchResponse
*/
public function GetFetchValue(string $sFetchItemName)
{
$mReturn = null;
$bNextIsValue = false;
if (Enumerations\FetchType::INDEX === $sFetchItemName)
{
$mReturn = $this->oImapResponse->ResponseList[1];
if (Enumerations\FetchType::INDEX === $sFetchItemName) {
return $this->oImapResponse->ResponseList[1];
}
else if (isset($this->oImapResponse->ResponseList[3]) && \is_array($this->oImapResponse->ResponseList[3]))
{
foreach ($this->oImapResponse->ResponseList[3] as $mItem)
{
if ($bNextIsValue)
{
$mReturn = $mItem;
break;
if (isset($this->oImapResponse->ResponseList[3]) && \is_array($this->oImapResponse->ResponseList[3])) {
$bNextIsValue = false;
foreach ($this->oImapResponse->ResponseList[3] as $mItem) {
if ($bNextIsValue) {
return $mItem;
}
if ($sFetchItemName === $mItem)
{
if ($sFetchItemName === $mItem) {
$bNextIsValue = true;
}
}
}
return $mReturn;
return null;
}
public function GetHeaderFieldsValue(string $sRfc822SubMimeIndex = '') : string

View file

@ -184,7 +184,7 @@ class ImapClient extends \MailSo\Net\NetClient
if (0 === \strpos($type, 'SCRAM-SHA-'))
{
$sAuthzid = $this->getResponseValue($this->SendRequestGetResponse('AUTHENTICATE', array($type)), Enumerations\ResponseType::CONTINUATION);
$this->sendRaw($SASL->authenticate($sLogin, $sPassword/*, $sAuthzid*/));
$this->sendRaw($SASL->authenticate($sLogin, $sPassword/*, $sAuthzid*/), true);
$sChallenge = $SASL->challenge($this->getResponseValue($this->getResponse(), Enumerations\ResponseType::CONTINUATION));
if ($this->oLogger) {
$this->oLogger->AddSecret($sChallenge);
@ -211,20 +211,31 @@ class ImapClient extends \MailSo\Net\NetClient
$this->oLogger->AddSecret($sAuth);
}
if ($this->IsSupported('SASL-IR')) {
$this->SendRequestGetResponse('AUTHENTICATE', array('PLAIN', $sAuth));
$this->SendRequestGetResponse('AUTHENTICATE', array($type, $sAuth));
} else {
$this->SendRequestGetResponse('AUTHENTICATE', array('PLAIN'));
$this->SendRequestGetResponse('AUTHENTICATE', array($type));
$this->sendRaw($sAuth, true, '*******');
$this->getResponse();
}
}
else if ($this->IsSupported('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*/);
if ($this->oLogger) {
$this->oLogger->AddSecret($sPass);
}
$this->sendRaw($sPass, true, '*******');
$this->getResponse();
}
else
{
if ($this->oLogger)
{
$this->oLogger->AddSecret($this->EscapeString($sPassword));
}
$this->SendRequestGetResponse('LOGIN',
array(
$this->EscapeString($sLogin),
@ -1082,6 +1093,17 @@ class ImapClient extends \MailSo\Net\NetClient
return $oResult;
}
private function skipBracketParse(?Response $oImapResponse) : bool
{
return $oImapResponse &&
$oImapResponse->ResponseType === \MailSo\Imap\Enumerations\ResponseType::UNTAGGED &&
(
($oImapResponse->StatusOrIndex === 'STATUS' && 2 === \count($oImapResponse->ResponseList)) ||
($oImapResponse->StatusOrIndex === 'LIST' && 4 === \count($oImapResponse->ResponseList)) ||
($oImapResponse->StatusOrIndex === 'LSUB' && 4 === \count($oImapResponse->ResponseList))
);
}
/**
* @return array|string
* @throws \MailSo\Net\Exceptions\Exception
@ -1265,8 +1287,16 @@ class ImapClient extends \MailSo\Net\NetClient
{
case ']' === $sChar:
case ')' === $sChar:
++$iPos;
$sPreviousAtomUpperCase = null;
if ($this->skipBracketParse($oImapResponse))
{
$bIsGotoDefault = true;
$bIsGotoNotAtomBracket = false;
}
else
{
++$iPos;
$sPreviousAtomUpperCase = null;
}
break 2;
case ' ' === $sChar:
if ($bTreatAsAtom)
@ -1283,12 +1313,20 @@ class ImapClient extends \MailSo\Net\NetClient
{
$sAtomBuilder .= $sChar;
$bIsGotoAtomBracket = true;
$this->iResponseBufParsedPos = ++$iPos;
}
else if ($this->skipBracketParse($oImapResponse))
{
$sOpenBracket = '';
$sClosingBracket = '';
$bIsGotoDefault = true;
$bIsGotoNotAtomBracket = false;
}
else
{
$bIsGotoNotAtomBracket = true;
$this->iResponseBufParsedPos = ++$iPos;
}
$this->iResponseBufParsedPos = ++$iPos;
break;
case '{' === $sChar:
$bIsLiteralParsed = false;
@ -1416,6 +1454,10 @@ class ImapClient extends \MailSo\Net\NetClient
$sCharDef = $this->sResponseBuffer[$iPos];
switch (true)
{
case ('[' === $sCharDef || ']' === $sCharDef || '(' === $sCharDef || ')' === $sCharDef) &&
$this->skipBracketParse($oImapResponse):
++$iPos;
break;
case '[' === $sCharDef:
if (null === $sAtomBuilder)
{

View file

@ -91,6 +91,34 @@ class ResponseCollection extends \MailSo\Base\Collection
return $aReturn;
}
/**
* @param array|string $mName
*/
private function getArrayNameToStringName($mName) : string
{
if (\is_string($mName))
{
return $mName;
}
if (\is_array($mName))
{
if (0 === \count($mName))
{
return '[]';
}
foreach ($mName as &$mSubName)
{
$mSubName = "[{$this->getArrayNameToStringName($mSubName)}]";
}
return \implode('', $mName);
}
return '';
}
public function getFoldersResult(string $sStatus, bool $bUseListStatus = false) : array
{
$aReturn = array();
@ -110,15 +138,12 @@ class ResponseCollection extends \MailSo\Base\Collection
* https://github.com/the-djmaze/snappymail/issues/1
* https://github.com/the-djmaze/snappymail/issues/70
*/
$sFullNameRaw = \array_slice($oResponse->ResponseList, 4);
foreach ($sFullNameRaw as &$name) {
if (\is_array($name)) {
$name = "[{$name[0]}]";
} else if (!\is_string($name)) {
$name = '';
}
$aFullNameRawList = \array_slice($oResponse->ResponseList, 4);
foreach ($aFullNameRawList as &$sName) {
$sName = $this->getArrayNameToStringName($sName);
}
$sFullNameRaw = \implode('', $sFullNameRaw);
$sFullNameRaw = \implode('', $aFullNameRawList);
$oFolder = new Folder($sFullNameRaw,
$oResponse->ResponseList[3], $oResponse->ResponseList[2]);

View file

@ -20,44 +20,32 @@ class FolderCollection extends \MailSo\Base\Collection
/**
* @var string
*/
public $Namespace;
public $Namespace = '';
/**
* @var string
*/
public $FoldersHash;
public $FoldersHash = '';
/**
* @var bool
*/
public $IsThreadsSupported;
public $IsThreadsSupported = false;
/**
* @var bool
*/
public $IsSortSupported;
public $IsSortSupported = false;
/**
* @var bool
*/
public $Optimized;
public $Optimized = false;
/**
* @var array
*/
public $SystemFolders;
function __construct($input = array())
{
parent::__construct($input);
$this->Namespace = '';
$this->FoldersHash = '';
$this->SystemFolders = array();
$this->IsThreadsSupported = false;
$this->IsSortSupported = false;
$this->Optimized = false;
}
public $SystemFolders = array();
public function append($oFolder, bool $bToTop = false) : void
{
@ -118,12 +106,7 @@ class FolderCollection extends \MailSo\Base\Collection
$oFolder = $this[0];
}
if ($oFolder)
{
$sDelimiter = $oFolder->Delimiter();
}
return $sDelimiter;
return $oFolder ? $oFolder->Delimiter() : '/';
}
public function SetNamespace(string $sNamespace) : self
@ -133,92 +116,18 @@ class FolderCollection extends \MailSo\Base\Collection
return $this;
}
public function InitByUnsortedMailFolderArray(array $aUnsortedMailFolders) : void
public function AddWithPositionSearch(Folder $oMailFolder) : void
{
$this->Clear();
$aSortedByLenImapFolders = array();
foreach ($aUnsortedMailFolders as /* @var $oMailFolder Folder */ $oMailFolder)
{
$aSortedByLenImapFolders[$oMailFolder->FullNameRaw()] =& $oMailFolder;
unset($oMailFolder);
}
unset($aUnsortedMailFolders);
$aAddedFolders = array();
foreach ($aSortedByLenImapFolders as /* @var $oMailFolder Folder */ $oMailFolder)
{
$sDelimiter = $oMailFolder->Delimiter();
$aFolderExplode = \explode($sDelimiter, $oMailFolder->FullNameRaw());
if (1 < \count($aFolderExplode))
{
\array_pop($aFolderExplode);
$sNonExistenFolderFullNameRaw = '';
foreach ($aFolderExplode as $sFolderExplodeItem)
{
$sNonExistenFolderFullNameRaw .= (0 < \strlen($sNonExistenFolderFullNameRaw))
? $sDelimiter.$sFolderExplodeItem : $sFolderExplodeItem;
if (!isset($aSortedByLenImapFolders[$sNonExistenFolderFullNameRaw]))
{
try
{
$aAddedFolders[$sNonExistenFolderFullNameRaw] =
Folder::NewNonExistenInstance($sNonExistenFolderFullNameRaw, $sDelimiter);
}
catch (\Throwable $oExc)
{
unset($oExc);
}
}
}
}
}
$aSortedByLenImapFolders = \array_merge($aSortedByLenImapFolders, $aAddedFolders);
unset($aAddedFolders);
\uasort($aSortedByLenImapFolders, function ($oFolderA, $oFolderB) {
return \strnatcmp($oFolderA->FullNameRaw(), $oFolderB->FullNameRaw());
});
foreach ($aSortedByLenImapFolders as $oMailFolder)
{
$this->AddWithPositionSearch($oMailFolder);
unset($oMailFolder);
}
unset($aSortedByLenImapFolders);
}
public function AddWithPositionSearch(Folder $oMailFolder) : bool
{
$oItemFolder = null;
$bIsAdded = false;
foreach ($this as $oItemFolder)
{
if ($oMailFolder instanceof Folder &&
0 === \strpos($oMailFolder->FullNameRaw(), $oItemFolder->FullNameRaw().$oItemFolder->Delimiter()))
if (0 === \strpos($oMailFolder->FullNameRaw(), $oItemFolder->FullNameRaw().$oItemFolder->Delimiter()))
{
if ($oItemFolder->SubFolders(true)->AddWithPositionSearch($oMailFolder))
{
$bIsAdded = true;
}
break;
$oItemFolder->SubFolders(true)->AddWithPositionSearch($oMailFolder);
return;
}
}
if (!$bIsAdded && $oMailFolder instanceof Folder)
{
$bIsAdded = true;
$this->append($oMailFolder);
}
return $bIsAdded;
$this->append($oMailFolder);
}
public function SortByCallback(callable $fCallback) : void
@ -241,7 +150,7 @@ class FolderCollection extends \MailSo\Base\Collection
public function jsonSerialize()
{
return array_merge(parent::jsonSerialize(), array(
return \array_merge(parent::jsonSerialize(), array(
'Namespace' => $this->GetNamespace(),
'FoldersHash' => $this->FoldersHash ?: '',
'IsThreadsSupported' => $this->IsThreadsSupported,

View file

@ -281,31 +281,6 @@ class MailClient
$oBodyStructure = null;
$oMessage = null;
$aBodyPeekMimeIndexes = array();
$aSignatureMimeIndexes = array();
$aFetchResponse = $this->oImapClient->Fetch(array(\MailSo\Imap\Enumerations\FetchType::BODYSTRUCTURE), $iIndex, $bIndexIsUid);
if (0 < \count($aFetchResponse) && isset($aFetchResponse[0]))
{
$oBodyStructure = $aFetchResponse[0]->GetFetchBodyStructure();
if ($oBodyStructure)
{
foreach ($oBodyStructure->SearchHtmlOrPlainParts() as $oPart)
{
$aBodyPeekMimeIndexes[] = array($oPart->PartID(), $oPart->Size());
}
$aSignatureParts = $oBodyStructure->SearchByContentType('application/pgp-signature');
if (is_array($aSignatureParts) && 0 < \count($aSignatureParts))
{
foreach ($aSignatureParts as $oPart)
{
$aSignatureMimeIndexes[] = $oPart->PartID();
}
}
}
}
$aFetchItems = array(
\MailSo\Imap\Enumerations\FetchType::INDEX,
\MailSo\Imap\Enumerations\FetchType::UID,
@ -315,25 +290,31 @@ class MailClient
$this->getEnvelopeOrHeadersRequestString()
);
if (0 < \count($aBodyPeekMimeIndexes))
$aFetchResponse = $this->oImapClient->Fetch(array(\MailSo\Imap\Enumerations\FetchType::BODYSTRUCTURE), $iIndex, $bIndexIsUid);
if (0 < \count($aFetchResponse) && isset($aFetchResponse[0]))
{
foreach ($aBodyPeekMimeIndexes as $aTextMimeData)
$oBodyStructure = $aFetchResponse[0]->GetFetchBodyStructure();
if ($oBodyStructure)
{
$sLine = \MailSo\Imap\Enumerations\FetchType::BODY_PEEK.'['.$aTextMimeData[0].']';
if (0 < $iBodyTextLimit && $iBodyTextLimit < $aTextMimeData[1])
foreach ($oBodyStructure->SearchHtmlOrPlainParts() as $oPart)
{
$sLine .= "<0.{$iBodyTextLimit}>";
$sLine = \MailSo\Imap\Enumerations\FetchType::BODY_PEEK.'['.$oPart->PartID().']';
if (0 < $iBodyTextLimit && $iBodyTextLimit < $oPart->Size())
{
$sLine .= "<0.{$iBodyTextLimit}>";
}
$aFetchItems[] = $sLine;
}
$aFetchItems[] = $sLine;
}
}
if (0 < \count($aSignatureMimeIndexes))
{
foreach ($aSignatureMimeIndexes as $sTextMimeIndex)
{
$aFetchItems[] = \MailSo\Imap\Enumerations\FetchType::BODY_PEEK.'['.$sTextMimeIndex.']';
$aSignatureParts = $oBodyStructure->SearchByContentType('application/pgp-signature');
if (is_array($aSignatureParts) && 0 < \count($aSignatureParts))
{
foreach ($aSignatureParts as $oPart)
{
$aFetchItems[] = \MailSo\Imap\Enumerations\FetchType::BODY_PEEK.'['.$oPart->PartID().']';
}
}
}
}
@ -1439,14 +1420,12 @@ class MailClient
if (0 < \count($aFetchResponse))
{
$aFetchIndexArray = array();
$oFetchResponseItem = null;
foreach ($aFetchResponse as /* @var $oFetchResponseItem \MailSo\Imap\FetchResponse */ $oFetchResponseItem)
{
$aFetchIndexArray[($bIndexAsUid)
$aFetchIndexArray[$bIndexAsUid
? $oFetchResponseItem->GetFetchValue(\MailSo\Imap\Enumerations\FetchType::UID)
: $oFetchResponseItem->GetFetchValue(\MailSo\Imap\Enumerations\FetchType::INDEX)] =& $oFetchResponseItem;
unset($oFetchResponseItem);
: $oFetchResponseItem->GetFetchValue(\MailSo\Imap\Enumerations\FetchType::INDEX)
] = $oFetchResponseItem;
}
foreach ($aRequestIndexOrUids as $iFUid)
@ -1979,8 +1958,6 @@ class MailClient
public function Folders(string $sParent = '', string $sListPattern = '*', bool $bUseListSubscribeStatus = true, int $iOptimizationLimit = 0) : ?FolderCollection
{
$oFolderCollection = null;
$aImapSubscribedFoldersHelper = null;
if ($bUseListSubscribeStatus)
{
@ -2000,56 +1977,102 @@ class MailClient
}
$aFolders = $this->oImapClient->FolderList($sParent, $sListPattern);
if (!$aFolders) {
return null;
}
if ($aFolders)
$aMailFoldersHelper = array();
foreach ($aFolders as /* @var $oImapFolder \MailSo\Imap\Folder */ $oImapFolder)
{
$aMailFoldersHelper = array();
$aMailFoldersHelper[] = new Folder($oImapFolder,
(null === $aImapSubscribedFoldersHelper || \in_array($oImapFolder->FullNameRaw(), $aImapSubscribedFoldersHelper)) ||
$oImapFolder->IsInbox()
);
}
foreach ($aFolders as /* @var $oImapFolder \MailSo\Imap\Folder */ $oImapFolder)
$iCount = \count($aMailFoldersHelper);
$aMailFoldersHelper = $this->folderListOptimization($aMailFoldersHelper, $iOptimizationLimit);
if (!$aMailFoldersHelper) {
return null;
}
$oFolderCollection = new FolderCollection;
$oFolderCollection->IsThreadsSupported = $this->IsThreadsSupported();
$oFolderCollection->IsSortSupported = $this->oImapClient->IsSupported('SORT');
$oFolderCollection->Optimized = $iCount !== \count($aMailFoldersHelper);
$aSortedByLenImapFolders = array();
foreach ($aMailFoldersHelper as /* @var $oMailFolder Folder */ $oMailFolder)
{
$aSortedByLenImapFolders[$oMailFolder->FullNameRaw()] = $oMailFolder;
}
unset($aMailFoldersHelper);
$aAddedFolders = array();
foreach ($aSortedByLenImapFolders as /* @var $oMailFolder Folder */ $oMailFolder)
{
$sDelimiter = $oMailFolder->Delimiter();
$aFolderExplode = \explode($sDelimiter, $oMailFolder->FullNameRaw());
if (1 < \count($aFolderExplode))
{
$aMailFoldersHelper[] = new Folder($oImapFolder,
(null === $aImapSubscribedFoldersHelper || \in_array($oImapFolder->FullNameRaw(), $aImapSubscribedFoldersHelper)) ||
$oImapFolder->IsInbox()
);
}
\array_pop($aFolderExplode);
$iCount = \count($aMailFoldersHelper);
$aMailFoldersHelper = $this->folderListOptimization($aMailFoldersHelper, $iOptimizationLimit);
$sNonExistenFolderFullNameRaw = '';
foreach ($aFolderExplode as $sFolderExplodeItem)
{
$sNonExistenFolderFullNameRaw .= (0 < \strlen($sNonExistenFolderFullNameRaw))
? $sDelimiter.$sFolderExplodeItem : $sFolderExplodeItem;
if ($aMailFoldersHelper)
{
$oFolderCollection = new FolderCollection;
$oFolderCollection->InitByUnsortedMailFolderArray($aMailFoldersHelper);
$oFolderCollection->Optimized = $iCount !== \count($aMailFoldersHelper);
if (!isset($aSortedByLenImapFolders[$sNonExistenFolderFullNameRaw]))
{
try
{
$aAddedFolders[$sNonExistenFolderFullNameRaw] =
Folder::NewNonExistenInstance($sNonExistenFolderFullNameRaw, $sDelimiter);
}
catch (\Throwable $oExc)
{
unset($oExc);
}
}
}
}
}
if ($oFolderCollection)
$aSortedByLenImapFolders = \array_merge($aSortedByLenImapFolders, $aAddedFolders);
unset($aAddedFolders);
\uasort($aSortedByLenImapFolders, function ($oFolderA, $oFolderB) {
return \strnatcmp($oFolderA->FullNameRaw(), $oFolderB->FullNameRaw());
});
foreach ($aSortedByLenImapFolders as $oMailFolder)
{
$oFolderCollection->SortByCallback(function ($oFolderA, $oFolderB) {
$sA = \strtoupper($oFolderA->FullNameRaw());
$sB = \strtoupper($oFolderB->FullNameRaw());
switch (true)
{
case 'INBOX' === $sA:
return -1;
case 'INBOX' === $sB:
return 1;
}
$oFolderCollection->AddWithPositionSearch($oMailFolder);
unset($oMailFolder);
}
return \strnatcasecmp($oFolderA->FullName(), $oFolderB->FullName());
});
unset($aSortedByLenImapFolders);
$oNamespace = $this->oImapClient->GetNamespace();
if ($oNamespace)
$oFolderCollection->SortByCallback(function ($oFolderA, $oFolderB) {
$sA = \strtoupper($oFolderA->FullNameRaw());
$sB = \strtoupper($oFolderB->FullNameRaw());
switch (true)
{
$oFolderCollection->SetNamespace($oNamespace->GetPersonalNamespace());
case 'INBOX' === $sA:
return -1;
case 'INBOX' === $sB:
return 1;
}
$oFolderCollection->IsThreadsSupported = $this->IsThreadsSupported();
return \strnatcasecmp($oFolderA->FullName(), $oFolderB->FullName());
});
$oFolderCollection->IsSortSupported = $this->oImapClient->IsSupported('SORT');
$oNamespace = $this->oImapClient->GetNamespace();
if ($oNamespace)
{
$oFolderCollection->SetNamespace($oNamespace->GetPersonalNamespace());
}
return $oFolderCollection;

View file

@ -31,157 +31,61 @@ class Message implements \JsonSerializable
$iHeaderTimeStampInUTC = 0,
$sHeaderDate = '',
$aFlags = [],
$aFlagsLowerCase = [];
$aFlagsLowerCase = [],
/**
* @var \MailSo\Mime\EmailCollection
*/
private $oFrom;
/**
* @var \MailSo\Mime\EmailCollection
*/
$oFrom = null,
$oSender = null,
$oReplyTo = null,
$oDeliveredTo = null,
$oTo = null,
$oCc = null,
$oBcc = null,
/**
* @var \MailSo\Mime\EmailCollection
*/
private $oSender;
$sInReplyTo = '',
/**
* @var \MailSo\Mime\EmailCollection
*/
private $oReplyTo;
$sPlain = '',
$sHtml = '',
/**
* @var \MailSo\Mime\EmailCollection
*/
private $oDeliveredTo;
/**
* @var AttachmentCollection
*/
$oAttachments = null,
/**
* @var \MailSo\Mime\EmailCollection
*/
private $oTo;
/**
* @var array
*/
$aDraftInfo = null,
/**
* @var \MailSo\Mime\EmailCollection
*/
private $oCc;
$sReferences = '',
/**
* @var \MailSo\Mime\EmailCollection
*/
private $oBcc;
/**
* @var int
*/
$iSensitivity,
$iPriority,
/**
* @var string
*/
private $sInReplyTo;
$sDeliveryReceipt = '',
/**
* @var string
*/
private $sPlain;
$sReadReceipt = '',
/**
* @var string
*/
private $sHtml;
$aUnsubsribeLinks = array(),
/**
* @var AttachmentCollection
*/
private $oAttachments;
$aThreads = array(),
/**
* @var array
*/
private $aDraftInfo;
$bTextPartIsTrimmed = false,
/**
* @var string
*/
private $sReferences;
/**
* @var int
*/
private $iSensitivity;
/**
* @var int
*/
private $iPriority;
/**
* @var string
*/
private $sDeliveryReceipt;
/**
* @var string
*/
private $sReadReceipt;
/**
* @var array
*/
private $aUnsubsribeLinks;
/**
* @var array
*/
private $aThreads;
/**
* @var bool
*/
private $bTextPartIsTrimmed;
/**
* @var string
*/
private $sPgpSignature;
/**
* @var bool
*/
private $bPgpSigned;
/**
* @var bool
*/
private $bPgpEncrypted;
$sPgpSignature = '',
$sPgpSignatureMicAlg = '',
$bPgpSigned = false,
$bPgpEncrypted = false;
function __construct()
{
$this->oFrom = null;
$this->oSender = null;
$this->oReplyTo = null;
$this->oDeliveredTo = null;
$this->oTo = null;
$this->oCc = null;
$this->oBcc = null;
$this->sPlain = '';
$this->sHtml = '';
$this->oAttachments = null;
$this->aDraftInfo = null;
$this->sInReplyTo = '';
$this->sReferences = '';
$this->aUnsubsribeLinks = array();
$this->iSensitivity = \MailSo\Mime\Enumerations\Sensitivity::NOTHING;
$this->iPriority = \MailSo\Mime\Enumerations\MessagePriority::NORMAL;
$this->sDeliveryReceipt = '';
$this->sReadReceipt = '';
$this->aThreads = array();
$this->bTextPartIsTrimmed = false;
$this->sPgpSignature = '';
$this->bPgpSigned = false;
$this->bPgpEncrypted = false;
return $this;
}
public function Plain() : string
@ -199,6 +103,11 @@ class Message implements \JsonSerializable
return $this->sPgpSignature;
}
public function PgpSignatureMicAlg() : string
{
return $this->sPgpSignatureMicAlg;
}
public function isPgpSigned() : bool
{
return $this->bPgpSigned;
@ -611,6 +520,24 @@ class Message implements \JsonSerializable
$this->sInReplyTo = $oFetchResponse->GetFetchEnvelopeValue(8, '');
}
// Content-Type: multipart/signed; micalg="pgp-sha256"; protocol="application/pgp-signature"
if ('multipart/signed' === \strtolower($this->sContentType)
&& 'application/pgp-signature' === \strtolower($oHeaders->ParameterValue(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, \MailSo\Mime\Enumerations\Parameter::PROTOCOL)))
{
$aPgpSignatureParts = $oBodyStructure ? $oBodyStructure->SearchByContentType('application/pgp-signature') : null;
if ($this->bPgpSigned = !empty($aPgpSignatureParts)) {
$sPgpSignatureText = $oFetchResponse->GetFetchValue(\MailSo\Imap\Enumerations\FetchType::BODY.'['.$aPgpSignatureParts[0]->PartID().']');
if (\is_string($sPgpSignatureText) && 0 < \strlen($sPgpSignatureText) && 0 < \strpos($sPgpSignatureText, 'BEGIN PGP SIGNATURE')) {
$this->sPgpSignature = \trim($sPgpSignatureText);
$this->sPgpSignatureMicAlg = (string) $oHeaders->ParameterValue(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'micalg');
}
}
}
// Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"
$this->bPgpEncrypted = ('multipart/encrypted' === \strtolower($this->sContentType)
&& 'application/pgp-encrypted' === \strtolower($oHeaders->ParameterValue(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, \MailSo\Mime\Enumerations\Parameter::PROTOCOL)));
$aTextParts = $oBodyStructure ? $oBodyStructure->SearchHtmlOrPlainParts() : null;
if ($aTextParts)
{
@ -674,39 +601,17 @@ class Message implements \JsonSerializable
}
$aMatch = array();
if (\preg_match('/-----BEGIN PGP SIGNATURE-----(.+)-----END PGP SIGNATURE-----/ism', $this->sPlain, $aMatch) && !empty($aMatch[0]))
if (!$this->bPgpSigned && \preg_match('/-----BEGIN PGP SIGNATURE-----(.+)-----END PGP SIGNATURE-----/ism', $this->sPlain, $aMatch) && !empty($aMatch[0]))
{
$this->sPgpSignature = \trim($aMatch[0]);
$this->bPgpSigned = true;
}
$aMatch = array();
if (\preg_match('/-----BEGIN PGP MESSAGE-----/ism', $this->sPlain, $aMatch) && !empty($aMatch[0]))
{
$this->bPgpEncrypted = true;
}
$this->bPgpEncrypted = !$this->bPgpEncrypted && false !== \stripos($this->sPlain, '-----BEGIN PGP MESSAGE-----');
unset($aHtmlParts, $aPlainParts, $aMatch);
}
// if (empty($this->sPgpSignature) && 'multipart/signed' === \strtolower($this->sContentType) &&
// 'application/pgp-signature' === \strtolower($oHeaders->ParameterValue(
// \MailSo\Mime\Enumerations\Header::CONTENT_TYPE,
// \MailSo\Mime\Enumerations\Parameter::PROTOCOL
// )))
// {
// $aPgpSignatureParts = $oBodyStructure ? $oBodyStructure->SearchByContentType('application/pgp-signature') : null;
// if (\is_array($aPgpSignatureParts) && 0 < \count($aPgpSignatureParts) && isset($aPgpSignatureParts[0]))
// {
// $sPgpSignatureText = $oFetchResponse->GetFetchValue(\MailSo\Imap\Enumerations\FetchType::BODY.'['.$aPgpSignatureParts[0]->PartID().']');
// if (\is_string($sPgpSignatureText) && 0 < \strlen($sPgpSignatureText) && 0 < \strpos($sPgpSignatureText, 'BEGIN PGP SIGNATURE'))
// {
// $this->sPgpSignature = \trim($sPgpSignatureText);
// $this->bPgpSigned = true;
// }
// }
// }
if ($oBodyStructure)
{
$aAttachmentsParts = $oBodyStructure->SearchAttachmentsParts();

View file

@ -301,9 +301,9 @@ class Message
return $this;
}
public function SetSender(EmailCollection $oEmails) : self
public function SetSender(Email $oEmail) : self
{
$this->aHeadersValue[Enumerations\Header::SENDER] = $oEmails;
$this->aHeadersValue[Enumerations\Header::SENDER] = $oEmail;
return $this;
}

View file

@ -12,8 +12,9 @@ class Actions
use Actions\User;
use Actions\Raw;
use Actions\Response;
use Actions\Localization;
use Actions\Themes;
const AUTH_TFA_SIGN_ME_TOKEN_KEY = 'rltfasmauth';
const AUTH_SIGN_ME_TOKEN_KEY = 'rlsmauth';
const AUTH_MAILTO_TOKEN_KEY = 'rlmailtoauth';
const AUTH_SPEC_TOKEN_KEY = 'rlspecauth';
@ -110,11 +111,6 @@ class Actions
*/
private $sSpecAuthToken;
/**
* @var string
*/
private $sUpdateAuthToken;
/**
* @access private
*/
@ -139,7 +135,6 @@ class Actions
$this->oSuggestionsProvider = null;
$this->sSpecAuthToken = '';
$this->sUpdateAuthToken = '';
$this->bIsJson = false;
$oConfig = $this->Config();
@ -155,13 +150,6 @@ class Actions
return $this;
}
public function SetUpdateAuthToken(string $sUpdateAuthToken): self
{
$this->sUpdateAuthToken = $sUpdateAuthToken;
return $this;
}
public function SetIsJson(bool $bIsJson): self
{
$this->bIsJson = $bIsJson;
@ -174,11 +162,6 @@ class Actions
return $this->sSpecAuthToken;
}
public function GetUpdateAuthToken(): string
{
return $this->sUpdateAuthToken;
}
public function GetIsJson(): bool
{
return $this->bIsJson;
@ -313,9 +296,9 @@ class Actions
}
}
public function ParseQueryAuthString(): string
public function ParseQueryString(): string
{
$sQuery = \trim($this->Http()->GetQueryString());
$sQuery = \trim($_SERVER['QUERY_STRING'] ?? '');
$iPos = \strpos($sQuery, '&');
if (0 < $iPos) {
@ -324,7 +307,7 @@ class Actions
$sQuery = \trim(\trim($sQuery), ' /');
$aSubQuery = $this->Http()->GetQuery('q');
$aSubQuery = $_GET['q'] ?? null;
if (\is_array($aSubQuery)) {
$aSubQuery = \array_map(function ($sS) {
return \trim(\trim($sS), ' /');
@ -335,16 +318,33 @@ class Actions
}
}
if ('' === $this->GetSpecAuthToken()) {
$aPaths = \explode('/', $sQuery);
if (!empty($aPaths[0]) && !empty($aPaths[1]) && '_' === substr($aPaths[1], 0, 1)) {
$this->SetSpecAuthToken($aPaths[1]);
}
}
return $sQuery;
}
// rlspecauth / AuthAccountHash
public function getAuthAccountHash() : string
{
if ('' === $this->sSpecAuthToken && !\strlen($this->GetSpecAuthLogoutTokenWithDeletion())) {
$sAuthAccountHash = $this->GetSpecAuthTokenCookie() ?: $this->GetSpecAuthToken();
if (empty($sAuthAccountHash)) {
$oAccount = $this->GetAccountFromSignMeToken();
if ($oAccount) try
{
$this->CheckMailConnection($oAccount);
$this->AuthToken($oAccount);
$sAuthAccountHash = $this->GetSpecAuthToken();
}
catch (\Throwable $oException)
{
$oException = null;
$this->ClearSignMeData($oAccount);
}
}
$this->SetSpecAuthToken($sAuthAccountHash);
}
return $this->GetSpecAuthToken();
}
private function compileLogParams(string $sLine, ?Model\Account $oAccount = null, bool $bUrlEncode = false, array $aAdditionalParams = array()): string
{
$aClear = array();
@ -360,7 +360,7 @@ class Actions
if (false !== \strpos($sLine, '{imap:') || false !== \strpos($sLine, '{smtp:')) {
if (!$oAccount) {
$this->ParseQueryAuthString();
$this->getAuthAccountHash();
$oAccount = $this->getAccountFromToken(false);
}
@ -414,7 +414,7 @@ class Actions
if (\preg_match('/\{user:(email|login|domain)\}/i', $sLine)) {
if (!$oAccount) {
$this->ParseQueryAuthString();
$this->getAuthAccountHash();
$oAccount = $this->getAccountFromToken(false);
}
@ -505,14 +505,9 @@ class Actions
}
}
public function GetSpecAuthTokenWithDeletion(): string
public function GetSpecAuthTokenCookie(): string
{
$sResult = Utils::GetCookie(self::AUTH_SPEC_TOKEN_KEY, '');
if (0 < strlen($sResult)) {
Utils::ClearCookie(self::AUTH_SPEC_TOKEN_KEY);
}
return $sResult;
return Utils::GetCookie(self::AUTH_SPEC_TOKEN_KEY, '');
}
public function GetSpecAuthLogoutTokenWithDeletion(): string
@ -691,9 +686,9 @@ class Actions
$oDriver = new \MailSo\Cache\Drivers\File(APP_PRIVATE_DATA . 'cache', $sKey);
break;
case ('APC' === $sDriver || 'APCU' === $sDriver) &&
case ('APCU' === $sDriver) &&
\MailSo\Base\Utils::FunctionExistsAndEnabled(array(
'apc_store', 'apc_fetch', 'apc_delete', 'apc_clear_cache')):
'apcu_store', 'apcu_fetch', 'apcu_delete', 'apcu_clear_cache')):
$oDriver = new \MailSo\Cache\Drivers\APC($sKey);
break;
@ -800,7 +795,7 @@ class Actions
);
$this->oLogger->Write(
'[APC:' . (\MailSo\Base\Utils::FunctionExistsAndEnabled('apc_fetch') ? 'on' : 'off') .
'[APCU:' . (\MailSo\Base\Utils::FunctionExistsAndEnabled('apcu_fetch') ? 'on' : 'off') .
'][MB:' . (\MailSo\Base\Utils::FunctionExistsAndEnabled('mb_convert_encoding') ? 'on' : 'off') .
'][PDO:' . (\class_exists('PDO') ? (\implode(',', \Pdo::getAvailableDrivers()) ?: '~') : 'off') .
'][Streams:' . \implode(',', \stream_get_transports()) .
@ -995,8 +990,6 @@ class Actions
'allowHtmlEditorSourceButton' => (bool)$oConfig->Get('labs', 'allow_html_editor_source_button', false),
'allowHtmlEditorBitiButtons' => (bool)$oConfig->Get('labs', 'allow_html_editor_biti_buttons', false),
'allowCtrlEnterOnCompose' => (bool)$oConfig->Get('labs', 'allow_ctrl_enter_on_compose', false),
'forgotPasswordLinkUrl' => \trim($oConfig->Get('login', 'forgot_password_link_url', '')),
'registrationLinkUrl' => \trim($oConfig->Get('login', 'registration_link_url', '')),
'hideSubmitButton' => (bool)$oConfig->Get('login', 'hide_submit_button', true),
'useImapThread' => (bool)$oConfig->Get('labs', 'use_imap_thread', false),
'useImapSubscribe' => (bool)$oConfig->Get('labs', 'use_imap_list_subscribe', true),
@ -1015,18 +1008,14 @@ class Actions
) : array());
}
public function AppData(bool $bAdmin, string $sAuthAccountHash = ''): array
public function AppData(bool $bAdmin): array
{
if (0 < \strlen($sAuthAccountHash) && \preg_match('/[^_\-\.a-zA-Z0-9]/', $sAuthAccountHash)) {
$sAuthAccountHash = '';
}
$oAccount = null;
$oConfig = $this->Config();
/*
required by Index.html and rl.js:
NewThemeLink TemplatesLink LangLink PluginsLink AuthAccountHash
PluginsLink
*/
$value = \ini_get('upload_max_filesize');
@ -1041,7 +1030,6 @@ class Actions
'Auth' => false,
'AccountHash' => '',
'AccountSignMe' => false,
'AuthAccountHash' => '',
'MailToEmail' => '',
'Email' => '',
'DevEmail' => '',
@ -1088,10 +1076,6 @@ class Actions
'UserBackgroundHash' => ''
);
if (0 < \strlen($sAuthAccountHash)) {
$aResult['AuthAccountHash'] = $sAuthAccountHash;
}
$oSettings = null;
$passfile = APP_PRIVATE_DATA.'admin_password.txt';
@ -1104,6 +1088,9 @@ class Actions
$oConfig->Save();
}
$sLanguage = $oConfig->Get('webmail', 'language', 'en');
$UserLanguageRaw = $this->detectUserLanguage($bAdmin);
if (!$bAdmin) {
$oAccount = $this->getAccountFromToken(false);
if ($oAccount) {
@ -1152,7 +1139,60 @@ class Actions
if (!empty($aResult['StartupUrl'])) {
$aResult['StartupUrl'] = $this->compileLogParams($aResult['StartupUrl'], $oAccount, true);
}
} else {
$aResult['ParentEmail'] = $oAccount->ParentEmail();
$oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount);
if ($oSettingsLocal instanceof Settings) {
$aResult['SentFolder'] = (string)$oSettingsLocal->GetConf('SentFolder', '');
$aResult['DraftFolder'] = (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']);
}
if ($this->GetCapa(false, Enumerations\Capa::SETTINGS, $oAccount)) {
if ($oSettings instanceof Settings) {
if ($oConfig->Get('webmail', 'allow_languages_on_settings', true)) {
$sLanguage = (string)$oSettings->GetConf('Language', $sLanguage);
}
$aResult['EditorDefaultType'] = (string)$oSettings->GetConf('EditorDefaultType', $aResult['EditorDefaultType']);
$aResult['ShowImages'] = (bool)$oSettings->GetConf('ShowImages', $aResult['ShowImages']);
$aResult['RemoveColors'] = (bool)$oSettings->GetConf('RemoveColors', $aResult['RemoveColors']);
$aResult['ContactsAutosave'] = (bool)$oSettings->GetConf('ContactsAutosave', $aResult['ContactsAutosave']);
$aResult['MPP'] = (int)$oSettings->GetConf('MPP', $aResult['MPP']);
$aResult['SoundNotification'] = (bool)$oSettings->GetConf('SoundNotification', $aResult['SoundNotification']);
$aResult['DesktopNotifications'] = (bool)$oSettings->GetConf('DesktopNotifications', $aResult['DesktopNotifications']);
$aResult['UseCheckboxesInList'] = (bool)$oSettings->GetConf('UseCheckboxesInList', $aResult['UseCheckboxesInList']);
$aResult['AllowDraftAutosave'] = (bool)$oSettings->GetConf('AllowDraftAutosave', $aResult['AllowDraftAutosave']);
$aResult['AutoLogout'] = (int)$oSettings->GetConf('AutoLogout', $aResult['AutoLogout']);
$aResult['Layout'] = (int)$oSettings->GetConf('Layout', $aResult['Layout']);
if (!$this->GetCapa(false, Enumerations\Capa::AUTOLOGOUT, $oAccount)) {
$aResult['AutoLogout'] = 0;
}
if ($this->GetCapa(false, Enumerations\Capa::USER_BACKGROUND, $oAccount)) {
$aResult['UserBackgroundName'] = (string)$oSettings->GetConf('UserBackgroundName', $aResult['UserBackgroundName']);
$aResult['UserBackgroundHash'] = (string)$oSettings->GetConf('UserBackgroundHash', $aResult['UserBackgroundHash']);
}
}
if ($oSettingsLocal instanceof Settings) {
$aResult['UseThreads'] = (bool)$oSettingsLocal->GetConf('UseThreads', $aResult['UseThreads']);
$aResult['ReplySameFolder'] = (bool)$oSettingsLocal->GetConf('ReplySameFolder', $aResult['ReplySameFolder']);
}
}
}
else {
if ($oConfig->Get('login', 'allow_languages_on_login', true)
&& $oConfig->Get('login', 'determine_user_language', true)) {
$sLanguage = $this->ValidateLanguage($UserLanguageRaw, $sLanguage, false);
}
$aResult['DevEmail'] = $oConfig->Get('labs', 'dev_email', '');
$aResult['DevPassword'] = $oConfig->Get('labs', 'dev_password', '');
@ -1198,98 +1238,22 @@ class Actions
$aResult['ProjectHash'] = \md5($aResult['AccountHash'] . APP_VERSION . $this->Plugins()->Hash());
$sLanguage = $oConfig->Get('webmail', 'language', 'en');
$sLanguageAdmin = $oConfig->Get('webmail', 'language_admin', 'en');
$sTheme = $oConfig->Get('webmail', 'theme', 'Default');
if (!$bAdmin && $oAccount) {
$aResult['ParentEmail'] = $oAccount->ParentEmail();
$oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount);
if ($oSettingsLocal instanceof Settings) {
$aResult['SentFolder'] = (string)$oSettingsLocal->GetConf('SentFolder', '');
$aResult['DraftFolder'] = (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']);
}
if ($this->GetCapa(false, Enumerations\Capa::SETTINGS, $oAccount)) {
if ($oSettings instanceof Settings) {
if ($oConfig->Get('webmail', 'allow_languages_on_settings', true)) {
$sLanguage = (string)$oSettings->GetConf('Language', $sLanguage);
}
$aResult['EditorDefaultType'] = (string)$oSettings->GetConf('EditorDefaultType', $aResult['EditorDefaultType']);
$aResult['ShowImages'] = (bool)$oSettings->GetConf('ShowImages', $aResult['ShowImages']);
$aResult['RemoveColors'] = (bool)$oSettings->GetConf('RemoveColors', $aResult['RemoveColors']);
$aResult['ContactsAutosave'] = (bool)$oSettings->GetConf('ContactsAutosave', $aResult['ContactsAutosave']);
$aResult['MPP'] = (int)$oSettings->GetConf('MPP', $aResult['MPP']);
$aResult['SoundNotification'] = (bool)$oSettings->GetConf('SoundNotification', $aResult['SoundNotification']);
$aResult['DesktopNotifications'] = (bool)$oSettings->GetConf('DesktopNotifications', $aResult['DesktopNotifications']);
$aResult['UseCheckboxesInList'] = (bool)$oSettings->GetConf('UseCheckboxesInList', $aResult['UseCheckboxesInList']);
$aResult['AllowDraftAutosave'] = (bool)$oSettings->GetConf('AllowDraftAutosave', $aResult['AllowDraftAutosave']);
$aResult['AutoLogout'] = (int)$oSettings->GetConf('AutoLogout', $aResult['AutoLogout']);
$aResult['Layout'] = (int)$oSettings->GetConf('Layout', $aResult['Layout']);
if (!$this->GetCapa(false, Enumerations\Capa::AUTOLOGOUT, $oAccount)) {
$aResult['AutoLogout'] = 0;
}
if ($this->GetCapa(false, Enumerations\Capa::USER_BACKGROUND, $oAccount)) {
$aResult['UserBackgroundName'] = (string)$oSettings->GetConf('UserBackgroundName', $aResult['UserBackgroundName']);
$aResult['UserBackgroundHash'] = (string)$oSettings->GetConf('UserBackgroundHash', $aResult['UserBackgroundHash']);
}
}
if ($oSettingsLocal instanceof Settings) {
$aResult['UseThreads'] = (bool)$oSettingsLocal->GetConf('UseThreads', $aResult['UseThreads']);
$aResult['ReplySameFolder'] = (bool)$oSettingsLocal->GetConf('ReplySameFolder', $aResult['ReplySameFolder']);
if ($this->GetCapa(false, Enumerations\Capa::THEMES, $oAccount)) {
$sTheme = (string)$oSettingsLocal->GetConf('Theme', $sTheme);
}
}
}
}
if (!$aResult['Auth']) {
if (!$bAdmin) {
if ($oConfig->Get('login', 'allow_languages_on_login', true) &&
$oConfig->Get('login', 'determine_user_language', true)) {
$sLanguage = $this->ValidateLanguage(
$this->detectUserLanguage($bAdmin), $sLanguage, false);
}
}
}
$sTheme = $this->ValidateTheme($sTheme);
$sStaticCache = $this->StaticCache();
$aResult['Theme'] = $sTheme;
$aResult['NewThemeLink'] = $this->ThemeLink($sTheme, $bAdmin);
$aResult['Theme'] = $this->GetTheme($bAdmin);
$aResult['Language'] = $this->ValidateLanguage($sLanguage, '', false);
$aResult['LanguageAdmin'] = $this->ValidateLanguage($sLanguageAdmin, '', true);
$aResult['UserLanguageRaw'] = $this->detectUserLanguage($bAdmin);
$aResult['UserLanguage'] = $this->ValidateLanguage($aResult['UserLanguageRaw'], '', false, true);
$aResult['UserLanguageAdmin'] = $this->ValidateLanguage($aResult['UserLanguageRaw'], '', true, true);
$aResult['UserLanguage'] = $this->ValidateLanguage($UserLanguageRaw, '', false, true);
if ($bAdmin) {
$aResult['LanguageAdmin'] = $this->ValidateLanguage($oConfig->Get('webmail', 'language_admin', 'en'), '', true);
$aResult['UserLanguageAdmin'] = $this->ValidateLanguage($UserLanguageRaw, '', true, true);
}
$aResult['PluginsLink'] = '';
if (0 < $this->Plugins()->Count() && $this->Plugins()->HaveJs($bAdmin)) {
$aResult['PluginsLink'] = './?/Plugins/0/' . ($bAdmin ? 'Admin' : 'User') . '/' . $sStaticCache . '/';
}
$aResult['LangLink'] = './?/Lang/0/' . ($bAdmin ? 'Admin' : 'App') . '/' .
($bAdmin ? $aResult['LanguageAdmin'] : $aResult['Language']) . '/' . $sStaticCache . '/';
// $aResult['TemplatesLink'] = './?/Templates/0/'.($bAdmin ? 'Admin' : 'App').'/'.$sStaticCache.'/';
$aResult['TemplatesLink'] = './?/Templates/0/' . ($bAdmin ? 'Admin' : 'App') . '/' . $sStaticCache . '/';
$bAppJsDebug = !!$this->Config()->Get('labs', 'use_app_debug_js', false);
$aResult['StaticLibJsLink'] = $this->StaticPath('js/' . ($bAppJsDebug ? '' : 'min/') .
@ -1311,52 +1275,19 @@ class Actions
return $aResult;
}
private function getUserLanguagesFromHeader(): array
protected function requestSleep(int $iDelay = 1): void
{
$aResult = $aList = array();
$sAcceptLang = \strtolower($this->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) {
$aResult[$n] = $v ? $v : 1;
}
\arsort($aResult, SORT_NUMERIC);
}
return $aResult;
}
public function detectUserLanguage(bool $bAdmin = false): string
{
$sResult = '';
$aLangs = $this->getUserLanguagesFromHeader();
foreach (\array_keys($aLangs) as $sLang) {
$sLang = $this->ValidateLanguage($sLang, '', $bAdmin, true);
if (!empty($sLang)) {
$sResult = $sLang;
break;
}
}
return $sResult;
}
private function requestSleep(int $iWait = 1, int $iDelay = 1): void
{
if (0 < $iDelay && 0 < $iWait) {
if ($iWait > \time() - $_SERVER['REQUEST_TIME_FLOAT']) {
\sleep($iDelay);
}
$time = \microtime(true) - $_SERVER['REQUEST_TIME_FLOAT'];
if ($iDelay > $time) {
\usleep(($iDelay - $time) * 1000000);
}
}
private function loginErrorDelay(): void
protected function loginErrorDelay(): void
{
$iDelay = (int)$this->Config()->Get('labs', 'login_fault_delay', 0);
if (0 < $iDelay) {
$this->requestSleep(1, $iDelay);
$this->requestSleep($iDelay);
}
}
@ -2073,13 +2004,13 @@ class Actions
if (!empty($sKey) && ($bForce || ($this->Config()->Get('cache', 'enable', true) && $this->Config()->Get('cache', 'http', true)))) {
$iExpires = $this->Config()->Get('cache', 'http_expires', 3600);
if (0 < $iExpires) {
$this->oHttp->ServerUseCache($this->etag($sKey), 1382478804, \time() + $iExpires);
$this->Http()->ServerUseCache($this->etag($sKey), 1382478804, \time() + $iExpires);
$bResult = true;
}
}
if (!$bResult) {
$this->oHttp->ServerNoCache();
$this->Http()->ServerNoCache();
}
return $bResult;
@ -2090,7 +2021,7 @@ class Actions
if (!empty($sKey) && ($bForce || $this->Config()->Get('cache', 'enable', true) && $this->Config()->Get('cache', 'http', true))) {
$sIfNoneMatch = $this->Http()->GetHeader('If-None-Match', '');
if ($this->etag($sKey) === $sIfNoneMatch) {
$this->Http()->StatusHeader(304);
\MailSo\Base\Http::StatusHeader(304);
$this->cacheByKey($sKey);
exit(0);
}
@ -2132,123 +2063,11 @@ class Actions
return $sCache;
}
public function ThemeLink(string $sTheme, bool $bAdmin): string
{
return './?/Css/0/' . ($bAdmin ? 'Admin' : 'User') . '/-/' . $sTheme . '/-/' . $this->StaticCache() . '/Hash/-/';
}
public function ValidateTheme(string $sTheme): string
{
return \in_array($sTheme, $this->GetThemes()) ?
$sTheme : $this->Config()->Get('themes', 'default', 'Default');
}
public function ValidateLanguage(string $sLanguage, string $sDefault = '', bool $bAdmin = false, bool $bAllowEmptyResult = false): string
{
$aLang = \SnappyMail\L10n::getLanguages($bAdmin);
$aHelper = array(
'ar' => 'ar-SA',
'cs' => 'cs-CZ',
'no' => 'nb-NO',
'ua' => 'uk-UA',
'cn' => 'zh-CN',
'zh' => 'zh-CN',
'tw' => 'zh-TW',
'fa' => 'fa-IR'
);
$sLanguage = isset($aHelper[$sLanguage]) ? $aHelper[$sLanguage] : \strtr($sLanguage, '_', '-');
$sDefault = isset($aHelper[$sDefault]) ? $aHelper[$sDefault] : \strtr($sDefault, '_', '-');
if (\in_array($sLanguage, $aLang)) {
return $sLanguage;
}
$sLangCountry = \preg_replace_callback('/-([a-zA-Z]{2})$/', function ($aData) {
return \strtoupper($aData[0]);
}, $sLanguage);
if (\in_array($sLangCountry, $aLang)) {
return $sLangCountry;
}
if (\in_array($sDefault, $aLang)) {
return $sDefault;
}
if ($bAllowEmptyResult) {
return '';
}
$sResult = $this->Config()->Get('webmail', $bAdmin ? 'language_admin' : 'language', 'en');
return \in_array($sResult, $aLang) ? $sResult : 'en';
}
public function ValidateContactPdoType(string $sType): string
{
return \in_array($sType, \RainLoop\Common\PdoAbstract::getAvailableDrivers()) ? $sType : 'sqlite';
}
/**
* @staticvar array $aCache
*/
public function GetThemes(): array
{
static $aCache = array();
if ($aCache) {
return $aCache;
}
$bClear = false;
$bDefault = false;
$aCache = array();
$sDir = APP_VERSION_ROOT_PATH . 'themes';
if (\is_dir($sDir)) {
$rDirH = \opendir($sDir);
if ($rDirH) {
while (($sFile = \readdir($rDirH)) !== false) {
if ('.' !== $sFile[0] && \is_dir($sDir . '/' . $sFile) && \file_exists($sDir . '/' . $sFile . '/styles.less')) {
if ('Default' === $sFile) {
$bDefault = true;
} else if ('Clear' === $sFile) {
$bClear = true;
} else {
$aCache[] = $sFile;
}
}
}
closedir($rDirH);
}
}
$sDir = APP_INDEX_ROOT_PATH . 'themes'; // custom user themes
if (\is_dir($sDir)) {
$rDirH = \opendir($sDir);
if ($rDirH) {
while (($sFile = \readdir($rDirH)) !== false) {
if ('.' !== $sFile[0] && \is_dir($sDir . '/' . $sFile) && \file_exists($sDir . '/' . $sFile . '/styles.less')) {
$aCache[] = $sFile . '@custom';
}
}
\closedir($rDirH);
}
}
$aCache = \array_unique($aCache);
\sort($aCache);
if ($bDefault) {
\array_unshift($aCache, 'Default');
}
if ($bClear) {
\array_push($aCache, 'Clear');
}
return $aCache;
}
public function ProcessTemplate(string $sName, string $sHtml): string
{
$sHtml = \preg_replace('/<script/i', '<x-script', $sHtml);
@ -2296,55 +2115,6 @@ class Actions
\header('Location: ' . $sUrl);
}
public function GetLanguageAndTheme(bool $bAdmin = false): array
{
$sTheme = $this->Config()->Get('webmail', 'theme', 'Default');
if ($bAdmin) {
$sLanguage = $this->Config()->Get('webmail', 'language_admin', 'en');
} else {
$oAccount = $this->GetAccount();
$sLanguage = $this->Config()->Get('webmail', 'language', 'en');
if ($oAccount) {
$oSettings = $this->SettingsProvider()->Load($oAccount);
if ($oSettings instanceof Settings) {
$sLanguage = $oSettings->GetConf('Language', $sLanguage);
}
$oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount);
if ($oSettingsLocal instanceof Settings) {
$sTheme = $oSettingsLocal->GetConf('Theme', $sTheme);
}
}
}
$sLanguage = $this->ValidateLanguage($sLanguage, '', $bAdmin);
$sTheme = $this->ValidateTheme($sTheme);
return array($sLanguage, $sTheme);
}
public function StaticI18N(string $sKey): string
{
static $sLang = null;
static $aLang = null;
if (null === $sLang) {
$aList = $this->GetLanguageAndTheme();
$sLang = $aList[0];
}
if (null === $aLang) {
$sLang = $this->ValidateLanguage($sLang, 'en');
$aLang = \SnappyMail\L10n::load($sLang, 'static');
$this->Plugins()->ReadLang($sLang, $aLang);
}
return $aLang[$aKey] ?? $sKey;
}
public function StaticPath(string $sPath): string
{
return Utils::WebStaticPath() . $sPath;

View file

@ -30,14 +30,6 @@ trait Folders
{
$this->Plugins()->RunHook('filter.folders-post', array($oAccount, $oFolderCollection));
$aFolders = $oFolderCollection->getArrayCopy();
foreach ($aFolders as $i => $oFolder) {
if (!$oFolder->IsSelectable()) {
unset($aFolders[$i]);
}
}
$oFolderCollection->exchangeArray(\array_values($aFolders));
$oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount);
$aSystemFolders = array();
@ -351,7 +343,7 @@ trait Folders
$aFlagsFilteredUids = array();
if (0 < strlen($sFlagsUids))
{
$aFlagsUids = explode(',', $sFlagsUids);
$aFlagsUids = \explode(',', $sFlagsUids);
$aFlagsFilteredUids = \array_filter(\array_map('intval', $aFlagsUids));
}

View file

@ -0,0 +1,150 @@
<?php
namespace RainLoop\Actions;
trait Localization
{
public function GetLanguage(bool $bAdmin = false): string
{
$oConfig = $this->Config();
if ($bAdmin) {
$sLanguage = $oConfig->Get('webmail', 'language_admin', 'en');
} else {
$sLanguage = $oConfig->Get('webmail', 'language', 'en');
if ($oAccount = $this->getAccountFromToken(false)) {
if ($oConfig->Get('webmail', 'allow_languages_on_settings', true)
&& $this->GetCapa(false, \RainLoop\Enumerations\Capa::SETTINGS, $oAccount)
&& ($oSettings = $this->SettingsProvider()->Load($oAccount))) {
$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);
}
}
return $this->ValidateLanguage($sLanguage, '', $bAdmin) ?: 'en';
}
public function ValidateLanguage(string $sLanguage, string $sDefault = '', bool $bAdmin = false, bool $bAllowEmptyResult = false): string
{
$aLang = \SnappyMail\L10n::getLanguages($bAdmin);
$aHelper = array(
'ar' => 'ar-SA',
'cs' => 'cs-CZ',
'no' => 'nb-NO',
'ua' => 'uk-UA',
'cn' => 'zh-CN',
'zh' => 'zh-CN',
'tw' => 'zh-TW',
'fa' => 'fa-IR'
);
$sLanguage = isset($aHelper[$sLanguage]) ? $aHelper[$sLanguage] : \strtr($sLanguage, '_', '-');
$sDefault = isset($aHelper[$sDefault]) ? $aHelper[$sDefault] : \strtr($sDefault, '_', '-');
if (\in_array($sLanguage, $aLang)) {
return $sLanguage;
}
$sLanguage = \preg_replace('/^([a-zA-Z]{2})$/', '\1-\1', $sLanguage);
$sLangCountry = \preg_replace_callback('/-([a-zA-Z]{2})$/', function ($aData) {
return \strtoupper($aData[0]);
}, $sLanguage);
if (\in_array($sLangCountry, $aLang)) {
return $sLangCountry;
}
if (\in_array($sDefault, $aLang)) {
return $sDefault;
}
if ($bAllowEmptyResult) {
return '';
}
$sResult = $this->Config()->Get('webmail', $bAdmin ? 'language_admin' : 'language', 'en');
return \in_array($sResult, $aLang) ? $sResult : 'en';
}
private function getUserLanguagesFromHeader(): array
{
$aResult = $aList = array();
$sAcceptLang = \strtolower($this->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) {
$aResult[$n] = $v ? $v : 1;
}
\arsort($aResult, SORT_NUMERIC);
}
return $aResult;
}
public function detectUserLanguage(bool $bAdmin): string
{
$sResult = '';
$aLangs = $this->getUserLanguagesFromHeader();
foreach (\array_keys($aLangs) as $sLang) {
$sLang = $this->ValidateLanguage($sLang, '', $bAdmin, true);
if (!empty($sLang)) {
$sResult = $sLang;
break;
}
}
return $sResult;
}
public function StaticI18N(string $sKey): string
{
static $sLang = null;
static $aLang = null;
if (null === $sLang) {
$sLang = $this->GetLanguage();
}
if (null === $aLang) {
$sLang = $this->ValidateLanguage($sLang, 'en');
$aLang = \SnappyMail\L10n::load($sLang, 'static');
$this->Plugins()->ReadLang($sLang, $aLang);
}
return $aLang[$aKey] ?? $sKey;
}
public function compileLanguage(string $sLanguage, bool $bAdmin = false) : string
{
$sLanguage = \strtr($sLanguage, '_', '-');
$aResultLang = \json_decode(\file_get_contents(APP_VERSION_ROOT_PATH.'app/localization/langs.json'), true);
$langs = \array_flip(\SnappyMail\L10n::getLanguages($bAdmin));
$aResultLang['LANGS_NAMES'] = \array_intersect_key($aResultLang['LANGS_NAMES'], $langs);
$aResultLang['LANGS_NAMES_EN'] = \array_intersect_key($aResultLang['LANGS_NAMES_EN'], $langs);
$aResultLang = \array_replace_recursive(
$aResultLang,
\SnappyMail\L10n::load($sLanguage, ($bAdmin ? 'admin' : 'user'))
);
$this->Plugins()->ReadLang($sLanguage, $aResultLang);
$sResult = \json_encode($aResultLang, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK);
$sTimeFormat = '';
$options = [$sLanguage, \substr($sLanguage, 0, 2), 'en'];
foreach ($options as $lang) {
$sFileName = APP_VERSION_ROOT_PATH.'app/localization/'.$lang.'/relativetimeformat.js';
if (\is_file($sFileName)) {
$sTimeFormat = \preg_replace('/^\\s+/', '', \file_get_contents($sFileName));
break;
}
}
return "document.documentElement.lang = '{$sLanguage}';\nrl.I18N={$sResult};\nrl.relativeTime = {$sTimeFormat};";
}
}

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