mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-08-29 12:09:20 +03:00
Merge branch 'master' into plugin-2fa
This commit is contained in:
commit
0aab26383c
156 changed files with 3155 additions and 1686 deletions
|
|
@ -1,6 +1,8 @@
|
|||
RewriteEngine On
|
||||
# Redirect cPanel
|
||||
RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301]
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
# Redirect cPanel
|
||||
RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301]
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_expires.c>
|
||||
ExpiresActive On
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
1. Install node.js - `https://nodejs.org/download/`
|
||||
2. Install yarn - `https://yarnpkg.com/en/docs/install`
|
||||
3. Install gulp - `npm install gulp -g`
|
||||
4. Fork snappymail - `https://github.com/the-djmaze/snappymail/issues/new#fork-destination-box`
|
||||
4. Fork snappymail from https://github.com/the-djmaze/snappymail
|
||||
5. Clone snappymail - `git clone git@github.com:USERNAME/snappymail.git snappymail`
|
||||
6. `cd snappymail`
|
||||
7. Install install all dependencies - `yarn install`
|
||||
|
|
|
|||
35
README.md
35
README.md
|
|
@ -74,6 +74,7 @@ This fork of RainLoop has the following changes:
|
|||
* Replaced old Sabre library
|
||||
* AddressBook Contacts support MySQL/MariaDB utf8mb4
|
||||
* Prevent Google FLoC
|
||||
* Added [Fetch Metadata Request Headers](https://www.w3.org/TR/fetch-metadata/) checks
|
||||
|
||||
### Removal of old JavaScript
|
||||
|
||||
|
|
@ -119,25 +120,25 @@ RainLoop 1.15 vs SnappyMail
|
|||
|
||||
|js/* |RainLoop |Snappy |
|
||||
|--------------- |--------: |--------: |
|
||||
|admin.js |2.158.025 | 91.664 |
|
||||
|app.js |4.215.733 | 453.928 |
|
||||
|admin.js |2.158.025 | 91.364 |
|
||||
|app.js |4.215.733 | 454.661 |
|
||||
|boot.js | 672.433 | 3.147 |
|
||||
|libs.js | 647.679 | 218.466 |
|
||||
|libs.js | 647.679 | 218.491 |
|
||||
|polyfills.js | 325.908 | 0 |
|
||||
|serviceworker.js | 0 | 285 |
|
||||
|TOTAL |8.019.778 | 767.490 |
|
||||
|TOTAL |8.019.778 | 767.663 |
|
||||
|
||||
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
||||
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|
||||
|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 |
|
||||
|admin.min.js | 255.514 | 47.527 | 73.899 | 14.263 | 60.674 | 12.750 |
|
||||
|app.min.js | 516.000 | 233.755 |140.430 | 68.663 |110.657 | 58.026 |
|
||||
|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 |
|
||||
|libs.min.js | 574.626 | 106.543 |177.280 | 38.613 |151.855 | 34.543 |
|
||||
|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 |
|
||||
|TOTAL |1.445.204 | 389.576 |425.477 |122.564 |353.301 |106.177 |
|
||||
|TOTAL (no admin) |1.189.690 | 342.049 |351.061 |108.301 |292.627 | 93.427 |
|
||||
|
||||
For a user its around 68% smaller and faster than traditional RainLoop.
|
||||
For a user its around 69% smaller and faster than traditional RainLoop.
|
||||
|
||||
|OpenPGP |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
||||
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|
||||
|
|
@ -173,13 +174,13 @@ For a user its around 68% smaller and faster than traditional RainLoop.
|
|||
|
||||
|
||||
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|
||||
|------------ |-------: |-------: |------: |------: |--------: |
|
||||
|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 |
|
||||
|------------ |-------: |------: |------: |------: |--------: |
|
||||
|app.css | 340.334 | 98.499 | 46,959 | 17.842 | 15.378 |
|
||||
|app.min.css | 274.791 | 81.603 | 39.618 | 15.982 | 14.056 |
|
||||
|boot.css | | 1.326 | | 664 | 545 |
|
||||
|boot.min.css | | 1.071 | | 590 | 474 |
|
||||
|admin.css | | 40.516 | | 8.481 | 7.401 |
|
||||
|admin.min.css | | 32.041 | | 7.447 | 6.610 |
|
||||
|
||||
|
||||
### Squire vs CKEditor
|
||||
|
|
|
|||
BIN
assets/sounds/alert.mp3
Normal file
BIN
assets/sounds/alert.mp3
Normal file
Binary file not shown.
BIN
assets/sounds/alert.ogg
Normal file
BIN
assets/sounds/alert.ogg
Normal file
Binary file not shown.
BIN
assets/sounds/ping.mp3
Normal file
BIN
assets/sounds/ping.mp3
Normal file
Binary file not shown.
BIN
assets/sounds/ping.ogg
Normal file
BIN
assets/sounds/ping.ogg
Normal file
Binary file not shown.
|
|
@ -38,7 +38,6 @@ import {
|
|||
} from 'Common/Cache';
|
||||
|
||||
import {
|
||||
userBackground,
|
||||
mailBox,
|
||||
root,
|
||||
openPgpWorkerJs,
|
||||
|
|
@ -81,6 +80,8 @@ import { ComposePopupView } from 'View/Popup/Compose';
|
|||
import { FolderSystemPopupView } from 'View/Popup/FolderSystem';
|
||||
import { AskPopupView } from 'View/Popup/Ask';
|
||||
|
||||
import { timeToNode } from 'Common/Momentor';
|
||||
|
||||
// Every 5 minutes
|
||||
const refreshFolders = 300000;
|
||||
|
||||
|
|
@ -110,16 +111,6 @@ class AppUser extends AbstractApp {
|
|||
lastTime = currentTime;
|
||||
}, interval);
|
||||
|
||||
if (SettingsGet('UserBackgroundHash')) {
|
||||
setTimeout(() => {
|
||||
const img = userBackground(SettingsGet('UserBackgroundHash'));
|
||||
if (img) {
|
||||
$htmlCL.add('UserBackground');
|
||||
doc.body.style.backgroundImage = "url("+img+")";
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
const fn = (ev=>$htmlCL.toggle('rl-ctrl-key-pressed', ev.ctrlKey)).debounce(500);
|
||||
['keydown','keyup'].forEach(t => doc.addEventListener(t, fn));
|
||||
|
||||
|
|
@ -1020,7 +1011,14 @@ class AppUser extends AbstractApp {
|
|||
this.hideLoading();
|
||||
}
|
||||
|
||||
setInterval(() => dispatchEvent(new CustomEvent('reload-time')), 60000);
|
||||
setInterval(this.reloadTime(), 60000);
|
||||
}
|
||||
|
||||
reloadTime()
|
||||
{
|
||||
setTimeout(() =>
|
||||
doc.querySelectorAll('[data-bind*="moment:"]').forEach(element => timeToNode(element))
|
||||
, 1)
|
||||
}
|
||||
|
||||
showMessageComposer(params = [])
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import * as Links from 'Common/Links';
|
||||
import { doc } from 'Common/Globals';
|
||||
import { doc, SettingsGet } from 'Common/Globals';
|
||||
|
||||
let notificator = null,
|
||||
player = null,
|
||||
|
|
@ -106,11 +106,11 @@ export const SMAudio = new class {
|
|||
|
||||
playNotification(silent) {
|
||||
if ('running' == audioCtx.state && (this.supportedMp3 || this.supportedOgg)) {
|
||||
if (!notificator) {
|
||||
notificator = createNewObject();
|
||||
notificator.src = Links.staticLink('sounds/new-mail.'+ (this.supportedMp3 ? 'mp3' : 'ogg'));
|
||||
}
|
||||
notificator = notificator || createNewObject();
|
||||
if (notificator) {
|
||||
notificator.src = Links.staticLink('sounds/'
|
||||
+ SettingsGet('NotificationSound')
|
||||
+ (this.supportedMp3 ? '.mp3' : '.ogg'));
|
||||
notificator.volume = silent ? 0.01 : 1;
|
||||
notificator.play();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,14 +70,6 @@ export function change(email) {
|
|||
return serverRequest('Change') + encodeURIComponent(email) + '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} hash
|
||||
* @returns {string}
|
||||
*/
|
||||
export function userBackground(hash) {
|
||||
return serverRequestRaw('UserBackground', hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} lang
|
||||
* @param {boolean} isAdmin
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { doc } from 'Common/Globals';
|
||||
import { i18n } from 'Common/Translator';
|
||||
|
||||
export function timestampToString(timeStampInUTC, formatStr) {
|
||||
|
|
@ -53,8 +52,3 @@ export function timeToNode(element, time) {
|
|||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
addEventListener('reload-time', () => setTimeout(() =>
|
||||
doc.querySelectorAll('[data-bind*="moment:"]').forEach(element => timeToNode(element))
|
||||
, 1)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
import ko from 'ko';
|
||||
import { isArray } from 'Common/Utils';
|
||||
|
||||
/*
|
||||
oCallbacks:
|
||||
ItemSelect
|
||||
MiddleClick
|
||||
AutoSelect
|
||||
ItemGetUid
|
||||
UpOrDown
|
||||
*/
|
||||
|
||||
export class Selector {
|
||||
/**
|
||||
* @param {koProperty} koList
|
||||
|
|
@ -209,11 +218,9 @@ export class Selector {
|
|||
|
||||
itemSelected(item) {
|
||||
if (this.isListChecked()) {
|
||||
if (!item) {
|
||||
(this.oCallbacks.onItemSelect || (()=>{}))(item || null);
|
||||
}
|
||||
item || (this.oCallbacks.ItemSelect || (()=>{}))(null);
|
||||
} else if (item) {
|
||||
(this.oCallbacks.onItemSelect || (()=>{}))(item);
|
||||
(this.oCallbacks.ItemSelect || (()=>{}))(item);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -226,20 +233,32 @@ export class Selector {
|
|||
this.oContentScrollable = contentScrollable;
|
||||
|
||||
if (contentScrollable) {
|
||||
let getItem = selector => {
|
||||
let el = event.target.closestWithin(selector, contentScrollable);
|
||||
return el ? ko.dataFor(el) : null;
|
||||
};
|
||||
|
||||
contentScrollable.addEventListener('click', event => {
|
||||
let el = event.target.closestWithin(this.sItemSelector, contentScrollable);
|
||||
el && this.actionClick(ko.dataFor(el), event);
|
||||
|
||||
el = event.target.closestWithin(this.sItemCheckedSelector, contentScrollable);
|
||||
if (el) {
|
||||
const item = ko.dataFor(el);
|
||||
const item = getItem(this.sItemCheckedSelector);
|
||||
if (item) {
|
||||
if (event.shiftKey) {
|
||||
this.actionClick(item, event);
|
||||
} else {
|
||||
this.focusedItem(item);
|
||||
item.checked(!item.checked());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
contentScrollable.addEventListener('auxclick', event => {
|
||||
if (1 == event.button) {
|
||||
const item = getItem(this.sItemSelector);
|
||||
if (item) {
|
||||
if (event.shiftKey) {
|
||||
this.actionClick(item, event);
|
||||
} else {
|
||||
this.focusedItem(item);
|
||||
item.checked(!item.checked());
|
||||
}
|
||||
this.focusedItem(item);
|
||||
(this.oCallbacks.MiddleClick || (()=>{}))(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -271,7 +290,7 @@ export class Selector {
|
|||
* @returns {boolean}
|
||||
*/
|
||||
autoSelect() {
|
||||
return !!(this.oCallbacks.onAutoSelect || (()=>true))();
|
||||
return !!(this.oCallbacks.AutoSelect || (()=>true))();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -281,7 +300,7 @@ export class Selector {
|
|||
getItemUid(item) {
|
||||
let uid = '';
|
||||
|
||||
const getItemUidCallback = this.oCallbacks.onItemGetUid || null;
|
||||
const getItemUidCallback = this.oCallbacks.ItemGetUid || null;
|
||||
if (getItemUidCallback && item) {
|
||||
uid = getItemUidCallback(item);
|
||||
}
|
||||
|
|
@ -312,7 +331,7 @@ export class Selector {
|
|||
} else if (++i < listLen) {
|
||||
result = list[i];
|
||||
}
|
||||
result || (this.oCallbacks.onUpUpOrDownDown || (()=>true))('ArrowUp' === sEventKey);
|
||||
result || (this.oCallbacks.UpOrDown || (()=>true))('ArrowUp' === sEventKey);
|
||||
} else if ('Home' === sEventKey) {
|
||||
result = list[0];
|
||||
} else if ('End' === sEventKey) {
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ export function reload(admin, language) {
|
|||
// reload the data
|
||||
if (init()) {
|
||||
i18nToNodes(doc);
|
||||
dispatchEvent(new CustomEvent('reload-time'));
|
||||
admin || rl.app.reloadTime();
|
||||
trigger(!trigger());
|
||||
}
|
||||
script.remove();
|
||||
|
|
|
|||
|
|
@ -38,9 +38,6 @@ class AbstractView {
|
|||
keyScope(this.sCurrentScope);
|
||||
}
|
||||
|
||||
cancelCommand() {}
|
||||
closeCommand() {}
|
||||
|
||||
querySelector(selectors) {
|
||||
return this.viewModelDom.querySelector(selectors);
|
||||
}
|
||||
|
|
@ -68,7 +65,10 @@ export class AbstractViewPopup extends AbstractView
|
|||
this.sDefaultScope = Scope[name];
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
cancelCommand() {}
|
||||
closeCommand() {}
|
||||
*/
|
||||
/**
|
||||
* @returns {void}
|
||||
*/
|
||||
|
|
@ -76,7 +76,7 @@ export class AbstractViewPopup extends AbstractView
|
|||
addEventListener('keydown', event => {
|
||||
if (event && this.modalVisibility && this.modalVisibility()) {
|
||||
if (!this.bDisabeCloseOnEsc && 'Escape' == event.key) {
|
||||
this.cancelCommand && this.cancelCommand();
|
||||
this.cancelCommand();
|
||||
return false;
|
||||
} else if ('Backspace' == event.key && !inFocus()) {
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -524,13 +524,6 @@ export class MessageModel extends AbstractModel {
|
|||
}
|
||||
}
|
||||
|
||||
storeDataInDom() {
|
||||
if (this.body) {
|
||||
this.body.rlIsHtml = !!this.isHtml();
|
||||
this.body.rlHasImages = !!this.hasImages();
|
||||
}
|
||||
}
|
||||
|
||||
fetchDataFromDom() {
|
||||
if (this.body) {
|
||||
this.isHtml(!!this.body.rlIsHtml);
|
||||
|
|
|
|||
|
|
@ -38,13 +38,6 @@ class RemoteAdminFetch extends AbstractFetchRemote {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?Function} fCallback
|
||||
*/
|
||||
pluginList(fCallback) {
|
||||
this.defaultRequest(fCallback, 'AdminPluginList');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {?Function} fCallback
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { DomainsAdminSettings } from 'Settings/Admin/Domains';
|
|||
import { LoginAdminSettings } from 'Settings/Admin/Login';
|
||||
import { ContactsAdminSettings } from 'Settings/Admin/Contacts';
|
||||
import { SecurityAdminSettings } from 'Settings/Admin/Security';
|
||||
import { PluginsAdminSettings } from 'Settings/Admin/Plugins';
|
||||
import { PackagesAdminSettings } from 'Settings/Admin/Packages';
|
||||
import { AboutAdminSettings } from 'Settings/Admin/About';
|
||||
import { BrandingAdminSettings } from 'Settings/Admin/Branding';
|
||||
|
|
@ -38,7 +37,6 @@ export class SettingsAdminScreen extends AbstractSettingsScreen {
|
|||
[BrandingAdminSettings, 'Branding'],
|
||||
[ContactsAdminSettings, 'Contacts'],
|
||||
[SecurityAdminSettings, 'Security'],
|
||||
[PluginsAdminSettings, 'Plugins'],
|
||||
[PackagesAdminSettings, 'Packages'],
|
||||
[AboutAdminSettings, 'About'],
|
||||
].forEach(item =>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ import { getNotification } from 'Common/Translator';
|
|||
import { PackageAdminStore } from 'Stores/Admin/Package';
|
||||
import Remote from 'Remote/Admin/Fetch';
|
||||
|
||||
import { showScreenPopup } from 'Knoin/Knoin';
|
||||
import { PluginPopupView } from 'View/Popup/Plugin';
|
||||
import { SettingsGet } from 'Common/Globals';
|
||||
|
||||
export class PackagesAdminSettings {
|
||||
constructor() {
|
||||
this.packagesError = ko.observable('');
|
||||
|
|
@ -13,24 +17,42 @@ export class PackagesAdminSettings {
|
|||
this.packages = PackageAdminStore;
|
||||
|
||||
this.packagesCurrent = ko.computed(() =>
|
||||
PackageAdminStore.filter(item => item && item.installed && !item.compare)
|
||||
PackageAdminStore.filter(item => item && item.installed && !item.canBeUpdated)
|
||||
);
|
||||
this.packagesAvailableForUpdate = ko.computed(() =>
|
||||
PackageAdminStore.filter(item => item && item.installed && !!item.compare)
|
||||
PackageAdminStore.filter(item => item && item.installed && !!item.canBeUpdated)
|
||||
);
|
||||
this.packagesAvailableForInstallation = ko.computed(() =>
|
||||
PackageAdminStore.filter(item => item && !item.installed)
|
||||
);
|
||||
|
||||
this.visibility = ko.computed(() => (PackageAdminStore.loading() ? 'visible' : 'hidden'));
|
||||
|
||||
this.enabledPlugins = ko.observable(!!SettingsGet('EnabledPlugins'));
|
||||
this.enabledPlugins.subscribe(value =>
|
||||
Remote.saveAdminConfig(null, {
|
||||
EnabledPlugins: value ? 1 : 0
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
onShow() {
|
||||
this.packagesError('');
|
||||
}
|
||||
|
||||
onBuild() {
|
||||
onBuild(oDom) {
|
||||
PackageAdminStore.fetch();
|
||||
|
||||
oDom.addEventListener('click', event => {
|
||||
// configurePlugin
|
||||
let el = event.target.closestWithin('.package-configure', oDom),
|
||||
data = el ? ko.dataFor(el) : 0;
|
||||
data && Remote.plugin((iError, data) => iError || showScreenPopup(PluginPopupView, [data.Result]), data.name)
|
||||
// disablePlugin
|
||||
el = event.target.closestWithin('.package-active', oDom);
|
||||
data = el ? ko.dataFor(el) : 0;
|
||||
data && this.disablePlugin(data);
|
||||
});
|
||||
}
|
||||
|
||||
requestHelper(packageToRequest, install) {
|
||||
|
|
@ -70,4 +92,20 @@ export class PackagesAdminSettings {
|
|||
Remote.packageInstall(this.requestHelper(packageToInstall, true), packageToInstall);
|
||||
}
|
||||
}
|
||||
|
||||
disablePlugin(plugin) {
|
||||
let b = !plugin.enabled();
|
||||
plugin.enabled(b);
|
||||
Remote.pluginDisable((iError, data) => {
|
||||
if (iError) {
|
||||
this.packagesError(
|
||||
(Notification.UnsupportedPluginPackage === iError && data && data.ErrorMessage)
|
||||
? data.ErrorMessage
|
||||
: getNotification(iError)
|
||||
);
|
||||
}
|
||||
// PackageAdminStore.fetch();
|
||||
}, plugin.name, !b);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,63 +0,0 @@
|
|||
import ko from 'ko';
|
||||
|
||||
import { Notification } from 'Common/Enums';
|
||||
import { SettingsGet } from 'Common/Globals';
|
||||
import { getNotification } from 'Common/Translator';
|
||||
|
||||
import { showScreenPopup } from 'Knoin/Knoin';
|
||||
|
||||
import { PluginAdminStore } from 'Stores/Admin/Plugin';
|
||||
|
||||
import Remote from 'Remote/Admin/Fetch';
|
||||
|
||||
import { PluginPopupView } from 'View/Popup/Plugin';
|
||||
|
||||
export class PluginsAdminSettings
|
||||
{
|
||||
constructor() {
|
||||
this.enabledPlugins = ko.observable(!!SettingsGet('EnabledPlugins'));
|
||||
|
||||
this.plugins = PluginAdminStore;
|
||||
this.pluginsError = PluginAdminStore.error;
|
||||
|
||||
this.enabledPlugins.subscribe(value =>
|
||||
Remote.saveAdminConfig(null, {
|
||||
EnabledPlugins: value ? 1 : 0
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
disablePlugin(plugin) {
|
||||
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((iError, data) => iError || showScreenPopup(PluginPopupView, [data.Result]), plugin.name);
|
||||
}
|
||||
|
||||
onBuild(oDom) {
|
||||
oDom.addEventListener('click', event => {
|
||||
let el = event.target.closestWithin('.configure-plugin-action', oDom);
|
||||
el && ko.dataFor(el) && this.configurePlugin(ko.dataFor(el));
|
||||
|
||||
el = event.target.closestWithin('.disabled-plugin', oDom);
|
||||
el && ko.dataFor(el) && this.disablePlugin(ko.dataFor(el));
|
||||
});
|
||||
}
|
||||
|
||||
onShow() {
|
||||
PluginAdminStore.error('');
|
||||
PluginAdminStore.fetch();
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,8 @@ export class GeneralUserSettings {
|
|||
this.layout = SettingsUserStore.layout;
|
||||
|
||||
this.enableSoundNotification = NotificationUserStore.enableSoundNotification;
|
||||
this.notificationSound = ko.observable(SettingsGet('NotificationSound'));
|
||||
this.notificationSounds = ko.observableArray(SettingsGet('NewMailSounds'));
|
||||
|
||||
this.enableDesktopNotification = NotificationUserStore.enableDesktopNotification;
|
||||
this.isDesktopNotificationDenied = NotificationUserStore.isDesktopNotificationDenied;
|
||||
|
|
@ -115,6 +117,10 @@ export class GeneralUserSettings {
|
|||
enableDesktopNotification: value => Remote.saveSetting('DesktopNotifications', value ? 1 : 0),
|
||||
|
||||
enableSoundNotification: value => Remote.saveSetting('SoundNotification', value ? 1 : 0),
|
||||
notificationSound: value => {
|
||||
Remote.saveSetting('NotificationSound', value);
|
||||
rl.settings.set('NotificationSound', value);
|
||||
},
|
||||
|
||||
replySameFolder: value => Remote.saveSetting('ReplySameFolder', value ? 1 : 0),
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import ko from 'ko';
|
|||
|
||||
import { SaveSettingsStep, UploadErrorCode, Capa } from 'Common/Enums';
|
||||
import { changeTheme, convertThemeName } from 'Common/Utils';
|
||||
import { userBackground, themePreviewLink, serverRequest } from 'Common/Links';
|
||||
import { themePreviewLink, serverRequest } from 'Common/Links';
|
||||
import { i18n } from 'Common/Translator';
|
||||
import { doc, $htmlCL, Settings } from 'Common/Globals';
|
||||
import { Settings } from 'Common/Globals';
|
||||
|
||||
import { ThemeStore } from 'Stores/Theme';
|
||||
|
||||
|
|
@ -38,16 +38,6 @@ export class ThemesUserSettings {
|
|||
Theme: value
|
||||
});
|
||||
});
|
||||
|
||||
this.background.hash.subscribe((value) => {
|
||||
if (!value) {
|
||||
$htmlCL.remove('UserBackground');
|
||||
doc.body.removeAttribute('style');
|
||||
} else {
|
||||
$htmlCL.add('UserBackground');
|
||||
doc.body.style.backgroundImage = "url("+userBackground(value)+")";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onBuild() {
|
||||
|
|
|
|||
|
|
@ -8,35 +8,34 @@ PackageAdminStore.real = ko.observable(true);
|
|||
|
||||
PackageAdminStore.loading = ko.observable(false);
|
||||
|
||||
//PackageAdminStore.error = ko.observable('');
|
||||
|
||||
PackageAdminStore.fetch = () => {
|
||||
PackageAdminStore.loading(true);
|
||||
Remote.packagesList((iError, data) => {
|
||||
PackageAdminStore.loading(false);
|
||||
if (!iError) {
|
||||
if (iError) {
|
||||
PackageAdminStore.real(false);
|
||||
} else {
|
||||
PackageAdminStore.real(!!data.Result.Real);
|
||||
|
||||
let list = [];
|
||||
const loading = {};
|
||||
|
||||
PackageAdminStore.forEach(item => {
|
||||
if (item && item.loading()) {
|
||||
loading[item.file] = item;
|
||||
}
|
||||
});
|
||||
|
||||
let list = [];
|
||||
if (isArray(data.Result.List)) {
|
||||
list = data.Result.List.map(item => {
|
||||
if (item) {
|
||||
item.loading = ko.observable(loading[item.file] !== undefined);
|
||||
return 'core' === item.type && !item.canBeInstalled ? null : item;
|
||||
}
|
||||
return null;
|
||||
}).filter(v => v);
|
||||
list = data.Result.List.filter(v => v).map(item => {
|
||||
item.loading = ko.observable(loading[item.file] !== undefined);
|
||||
item.enabled = ko.observable(item.enabled);
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
PackageAdminStore(list);
|
||||
} else {
|
||||
PackageAdminStore.real(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
import ko from 'ko';
|
||||
import Remote from 'Remote/Admin/Fetch';
|
||||
|
||||
export const PluginAdminStore = ko.observableArray();
|
||||
|
||||
PluginAdminStore.loading = ko.observable(false);
|
||||
|
||||
PluginAdminStore.error = ko.observable('');
|
||||
|
||||
PluginAdminStore.fetch = () => {
|
||||
PluginAdminStore.loading(true);
|
||||
Remote.pluginList((iError, data) => {
|
||||
PluginAdminStore.loading(false);
|
||||
if (!iError) {
|
||||
PluginAdminStore(
|
||||
data.Result.map(item => ({
|
||||
name: item.Name,
|
||||
disabled: ko.observable(!item.Enabled),
|
||||
configured: ko.observable(!!item.Configured)
|
||||
}))
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import ko from 'ko';
|
||||
import { $htmlCL, leftPanelDisabled, Settings, SettingsGet } from 'Common/Globals';
|
||||
import { doc, $htmlCL, leftPanelDisabled, Settings, SettingsGet } from 'Common/Globals';
|
||||
import { isArray } from 'Common/Utils';
|
||||
import { serverRequestRaw } from 'Common/Links';
|
||||
|
||||
export const ThemeStore = {
|
||||
themes: ko.observableArray(),
|
||||
|
|
@ -25,3 +26,13 @@ export const ThemeStore = {
|
|||
ThemeStore.theme = ko.observable('').extend({ limitedList: ThemeStore.themes });
|
||||
|
||||
ThemeStore.isMobile.subscribe(value => $htmlCL.toggle('rl-mobile', value));
|
||||
|
||||
ThemeStore.userBackgroundHash.subscribe(value => {
|
||||
if (value) {
|
||||
$htmlCL.add('UserBackground');
|
||||
doc.body.style.backgroundImage = "url("+serverRequestRaw('UserBackground', value)+")";
|
||||
} else {
|
||||
$htmlCL.remove('UserBackground');
|
||||
doc.body.removeAttribute('style');
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import ko from 'ko';
|
|||
|
||||
import { Scope, Notification } from 'Common/Enums';
|
||||
import { MessageSetAction } from 'Common/EnumsUser';
|
||||
import { doc, createElement, elementById } from 'Common/Globals';
|
||||
import { doc, $htmlCL, createElement, elementById } from 'Common/Globals';
|
||||
import { isNonEmptyArray, pInt, pString, addObservablesTo, addSubscribablesTo } from 'Common/Utils';
|
||||
import { plainToHtml } from 'Common/UtilsUser';
|
||||
|
||||
|
|
@ -186,6 +186,8 @@ export const MessageUserStore = new class {
|
|||
}
|
||||
},
|
||||
|
||||
listLoadingAnimation: value => $htmlCL.toggle('list-loading', value),
|
||||
|
||||
listLoading: value =>
|
||||
this.listCompleteLoading(value || this.listIsNotCompleted()),
|
||||
|
||||
|
|
@ -220,7 +222,7 @@ export const MessageUserStore = new class {
|
|||
}
|
||||
});
|
||||
|
||||
this.purgeMessageBodyCacheThrottle = this.purgeMessageBodyCache.throttle(30000);
|
||||
this.purgeMessageBodyCache = this.purgeMessageBodyCache.throttle(30000);
|
||||
}
|
||||
|
||||
purgeMessageBodyCache() {
|
||||
|
|
@ -428,16 +430,12 @@ export const MessageUserStore = new class {
|
|||
);
|
||||
}
|
||||
|
||||
setMessage(data, cached) {
|
||||
setMessage(data, cached, oMessage) {
|
||||
let isNew = false,
|
||||
body = null,
|
||||
json = data && data.Result,
|
||||
id = '',
|
||||
plain = '',
|
||||
resultHtml = '',
|
||||
messagesDom = this.messagesBodiesDom(),
|
||||
selectedMessage = this.selectorMessageSelected(),
|
||||
message = this.message();
|
||||
message = oMessage || this.message();
|
||||
|
||||
if (
|
||||
json &&
|
||||
|
|
@ -447,7 +445,7 @@ export const MessageUserStore = new class {
|
|||
) {
|
||||
const threads = message.threads();
|
||||
if (message.uid !== json.Uid && 1 < threads.length && threads.includes(json.Uid)) {
|
||||
message = MessageModel.reviveFromJson(json);
|
||||
message = oMessage ? null : MessageModel.reviveFromJson(json);
|
||||
if (message) {
|
||||
message.threads(threads);
|
||||
MessageFlagsCache.initMessage(message);
|
||||
|
|
@ -460,7 +458,7 @@ export const MessageUserStore = new class {
|
|||
}
|
||||
|
||||
if (message && message.uid === json.Uid) {
|
||||
this.messageError('');
|
||||
oMessage || this.messageError('');
|
||||
|
||||
if (cached) {
|
||||
delete json.IsSeen;
|
||||
|
|
@ -475,7 +473,8 @@ export const MessageUserStore = new class {
|
|||
addRequestedMessage(message.folder, message.uid);
|
||||
|
||||
if (messagesDom) {
|
||||
id = 'rl-mgs-' + message.hash.replace(/[^a-zA-Z0-9]/g, '');
|
||||
let body = null,
|
||||
id = 'rl-mgs-' + message.hash.replace(/[^a-zA-Z0-9]/g, '');
|
||||
|
||||
const textBody = elementById(id);
|
||||
if (textBody) {
|
||||
|
|
@ -483,7 +482,9 @@ export const MessageUserStore = new class {
|
|||
message.fetchDataFromDom();
|
||||
messagesDom.append(textBody);
|
||||
} else {
|
||||
let isHtml = !!json.Html;
|
||||
let isHtml = !!json.Html,
|
||||
plain = '',
|
||||
resultHtml = '<pre></pre>';
|
||||
if (isHtml) {
|
||||
resultHtml = json.Html.toString();
|
||||
if (SettingsUserStore.removeColors()) {
|
||||
|
|
@ -508,8 +509,6 @@ export const MessageUserStore = new class {
|
|||
} else {
|
||||
resultHtml = '<pre>' + resultHtml + '</pre>';
|
||||
}
|
||||
} else {
|
||||
resultHtml = '<pre>' + resultHtml + '</pre>';
|
||||
}
|
||||
|
||||
body = Element.fromHTML('<div id="' + id + '" hidden="" class="b-text-part '
|
||||
|
|
@ -517,22 +516,25 @@ export const MessageUserStore = new class {
|
|||
+ findEmailAndLinks(resultHtml)
|
||||
+ '</div>');
|
||||
|
||||
// Drop Microsoft Office style properties
|
||||
const rgbRE = /rgb\((\d+),\s*(\d+),\s*(\d+)\)/g,
|
||||
hex = n => ('0' + parseInt(n).toString(16)).slice(-2);
|
||||
body.querySelectorAll('[style*=mso]').forEach(el =>
|
||||
el.setAttribute('style', el.style.cssText.replace(rgbRE, (m,r,g,b) => '#' + hex(r) + hex(g) + hex(b)))
|
||||
);
|
||||
if (isHtml) {
|
||||
// Drop Microsoft Office style properties
|
||||
const rgbRE = /rgb\((\d+),\s*(\d+),\s*(\d+)\)/g,
|
||||
hex = n => ('0' + parseInt(n).toString(16)).slice(-2);
|
||||
body.querySelectorAll('[style*=mso]').forEach(el =>
|
||||
el.setAttribute('style', el.style.cssText.replace(rgbRE, (m,r,g,b) => '#' + hex(r) + hex(g) + hex(b)))
|
||||
);
|
||||
}
|
||||
|
||||
body.rlIsHtml = isHtml;
|
||||
body.rlHasImages = !!json.HasExternals;
|
||||
|
||||
message.body = body;
|
||||
|
||||
message.isHtml(isHtml);
|
||||
message.hasImages(!!json.HasExternals);
|
||||
message.hasImages(body.rlHasImages);
|
||||
|
||||
messagesDom.append(body);
|
||||
|
||||
message.storeDataInDom();
|
||||
|
||||
if (json.HasInternals) {
|
||||
message.showInternalImages();
|
||||
}
|
||||
|
|
@ -541,12 +543,12 @@ export const MessageUserStore = new class {
|
|||
message.showExternalImages();
|
||||
}
|
||||
|
||||
this.purgeMessageBodyCacheThrottle();
|
||||
this.purgeMessageBodyCache();
|
||||
}
|
||||
|
||||
this.messageActiveDom(message.body);
|
||||
oMessage || this.messageActiveDom(message.body);
|
||||
|
||||
this.hideMessageBodies();
|
||||
oMessage || this.hideMessageBodies();
|
||||
|
||||
if (body) {
|
||||
this.initOpenPgpControls(body, message);
|
||||
|
|
@ -554,7 +556,8 @@ export const MessageUserStore = new class {
|
|||
this.initBlockquoteSwitcher(body);
|
||||
}
|
||||
|
||||
message.body.hidden = false;
|
||||
oMessage || (message.body.hidden = false);
|
||||
oMessage && message.viewPopupMessage();
|
||||
}
|
||||
|
||||
MessageFlagsCache.initMessage(message);
|
||||
|
|
@ -563,8 +566,6 @@ export const MessageUserStore = new class {
|
|||
}
|
||||
|
||||
if (isNew) {
|
||||
message = this.message();
|
||||
|
||||
if (
|
||||
selectedMessage &&
|
||||
message &&
|
||||
|
|
@ -613,20 +614,20 @@ export const MessageUserStore = new class {
|
|||
}
|
||||
}
|
||||
|
||||
populateMessageBody(oMessage) {
|
||||
populateMessageBody(oMessage, preload) {
|
||||
if (oMessage) {
|
||||
this.hideMessageBodies();
|
||||
this.messageLoading(true);
|
||||
preload || this.hideMessageBodies();
|
||||
preload || this.messageLoading(true);
|
||||
Remote.message((iError, oData, bCached) => {
|
||||
if (iError) {
|
||||
if (Notification.RequestAborted !== iError) {
|
||||
if (Notification.RequestAborted !== iError && !preload) {
|
||||
this.message(null);
|
||||
this.messageError(getNotification(iError));
|
||||
}
|
||||
} else {
|
||||
this.setMessage(oData, bCached);
|
||||
this.setMessage(oData, bCached, preload ? oMessage : null);
|
||||
}
|
||||
this.messageLoading(false);
|
||||
preload || this.messageLoading(false);
|
||||
}, oMessage.folder, oMessage.uid);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,21 +23,19 @@
|
|||
|
||||
@import "Ui.less";
|
||||
@import "Main.less";
|
||||
@import "LayoutAdmin.less";
|
||||
@import "Components.less";
|
||||
@import "Login.less";
|
||||
@import "Ask.less";
|
||||
@import "Languages.less";
|
||||
|
||||
@import "Admin.less";
|
||||
@import "AdminGeneral.less";
|
||||
@import "AdminBranding.less";
|
||||
@import "AdminDomains.less";
|
||||
@import "AdminDomain.less";
|
||||
@import "AdminPackages.less";
|
||||
@import "AdminPlugins.less";
|
||||
@import "AdminPlugin.less";
|
||||
@import "AdminAbout.less";
|
||||
@import "Admin/Layout.less";
|
||||
@import "Admin/General.less";
|
||||
@import "Admin/Branding.less";
|
||||
@import "Admin/Domains.less";
|
||||
@import "Admin/Domain.less";
|
||||
@import "Admin/Packages.less";
|
||||
@import "Admin/Plugin.less";
|
||||
@import "Admin/About.less";
|
||||
|
||||
@import "Animations.less";
|
||||
|
||||
|
|
|
|||
|
|
@ -24,45 +24,39 @@
|
|||
|
||||
@import "Ui.less";
|
||||
@import "Main.less";
|
||||
@import "Layout.less";
|
||||
@import "Components.less";
|
||||
@import "Login.less";
|
||||
@import "Ask.less";
|
||||
@import "Languages.less";
|
||||
|
||||
@import "SystemDropDown.less";
|
||||
@import "Shortcuts.less";
|
||||
@import "FolderList.less";
|
||||
/*
|
||||
@import "FolderClear.less";
|
||||
@import "FolderCreate.less";
|
||||
@import "FolderSystem.less";
|
||||
@import "Account.less";
|
||||
*/
|
||||
@import "Filter.less";
|
||||
@import "Template.less";
|
||||
@import "OpenPgpKey.less";
|
||||
@import "Identity.less";
|
||||
@import "AdvancedSearch.less";
|
||||
@import "Attachmnets.less";
|
||||
@import "MessageList.less";
|
||||
@import "MessageView.less";
|
||||
@import "Contacts.less";
|
||||
@import "Compose.less";
|
||||
@import "EmailAddresses.less";
|
||||
@import "User/Layout.less";
|
||||
@import "User/SystemDropDown.less";
|
||||
@import "User/Shortcuts.less";
|
||||
@import "User/FolderList.less";
|
||||
@import "User/Filter.less";
|
||||
@import "User/Template.less";
|
||||
@import "User/OpenPgpKey.less";
|
||||
@import "User/Identity.less";
|
||||
@import "User/AdvancedSearch.less";
|
||||
@import "User/Attachmnets.less";
|
||||
@import "User/MessageList.less";
|
||||
@import "User/MessageView.less";
|
||||
@import "User/Contacts.less";
|
||||
@import "User/Compose.less";
|
||||
@import "User/EmailAddresses.less";
|
||||
|
||||
@import "Settings.less";
|
||||
@import "SettingsGeneral.less";
|
||||
@import "SettingsAccounts.less";
|
||||
@import "SettingsTemplates.less";
|
||||
@import "SettingsOpenPGP.less";
|
||||
@import "SettingsFolders.less";
|
||||
@import "SettingsThemes.less";
|
||||
@import "SettingsFilters.less";
|
||||
@import "User/Settings.less";
|
||||
@import "User/SettingsGeneral.less";
|
||||
@import "User/SettingsAccounts.less";
|
||||
@import "User/SettingsTemplates.less";
|
||||
@import "User/SettingsOpenPGP.less";
|
||||
@import "User/SettingsFolders.less";
|
||||
@import "User/SettingsThemes.less";
|
||||
@import "User/SettingsFilters.less";
|
||||
|
||||
@import "SquireUI.less";
|
||||
@import "User/SquireUI.less";
|
||||
|
||||
@import "AnimationsUser.less";
|
||||
@import "User/Animations.less";
|
||||
@import "Animations.less";
|
||||
|
||||
@import "_End.less";
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
.b-account-add-content {
|
||||
.modal-header {
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
.b-domain-alias-content {
|
||||
&.modal {
|
||||
width: 330px;
|
||||
max-width: 330px;
|
||||
}
|
||||
|
||||
.error-desc {
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
.b-domain-content {
|
||||
|
||||
&.modal {
|
||||
width: 810px;
|
||||
max-width: 810px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
|
|
@ -1,7 +1,25 @@
|
|||
.UserBackground body {
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
#rl-left, #rl-right {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
#rl-left {
|
||||
overflow: auto;
|
||||
padding-top: 50px + @rlLowMargin + 10px;
|
||||
width: @rlLeftWidth;
|
||||
|
||||
nav {
|
||||
a {
|
||||
|
|
@ -26,6 +44,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
#rl-right {
|
||||
z-index: 1;
|
||||
left: @rlLeftWidth;
|
||||
}
|
||||
|
||||
.b-admin-right {
|
||||
|
||||
.b-toolbar {
|
||||
|
|
@ -67,3 +90,56 @@
|
|||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
#rl-popups > .rl-view-model {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1100;
|
||||
overflow: auto;
|
||||
background-color: rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
#rl-settings-subscreen {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* desktop */
|
||||
@media screen and (min-width: 1000px) {
|
||||
.toggleLeft {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* desktop-large */
|
||||
@media screen and (min-width: 1401px) {
|
||||
#rl-left {
|
||||
width: @rlLeftWidth + 20;
|
||||
}
|
||||
|
||||
#rl-right {
|
||||
left: @rlLeftWidth + 20;
|
||||
}
|
||||
}
|
||||
|
||||
/* mobile and tablet */
|
||||
@media screen and (max-width: 999px) {
|
||||
#rl-settings-subscreen {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
html:not(.rl-left-panel-disabled) #rl-right {
|
||||
right: 5-@rlLeftWidth;
|
||||
}
|
||||
|
||||
html.rl-left-panel-disabled {
|
||||
#rl-left {
|
||||
width: 0 !important;
|
||||
}
|
||||
#rl-right {
|
||||
left: 5px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,11 @@
|
|||
width: 650px;
|
||||
}
|
||||
|
||||
.disabled table,
|
||||
table .disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.b-admin-packages-list-table {
|
||||
|
|
@ -30,4 +35,9 @@
|
|||
.package-actions {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.package-configure, .package-active {
|
||||
cursor: pointer;
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
.b-plugin-content {
|
||||
|
||||
&.modal {
|
||||
width: 660px;
|
||||
max-width: 660px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
|
||||
.b-admin-plugins-list-table {
|
||||
max-width: 620px; /* span8 */
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.b-admin-plugin-property {
|
||||
.help-block {
|
||||
display: block; // account for any element using help-block
|
||||
font-style: italic;
|
||||
opacity: 0.5;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.controls {
|
||||
width: 470px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
.b-folder-clear-content {
|
||||
.modal-header {
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
.b-folder-create-content {
|
||||
.modal-header {
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
.b-folder-system-content {
|
||||
.modal-header {
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,15 @@
|
|||
.b-languages-content {
|
||||
|
||||
&.modal {
|
||||
width: 700px;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.lang-item {
|
||||
label {
|
||||
display: inline-block;
|
||||
padding: 5px 15px;
|
||||
margin: 2px 5px;
|
||||
width: 180px;
|
||||
background-color: rgba(128, 128, 128, 0.1);
|
||||
text-align: left;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
position: relative;
|
||||
|
|
@ -26,7 +25,6 @@
|
|||
}
|
||||
&.selected::after {
|
||||
content: "✔";
|
||||
color: #080;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
font-family: snappymail;
|
||||
|
|
@ -39,7 +37,7 @@
|
|||
}
|
||||
|
||||
@media screen and (max-width: 999px) {
|
||||
.b-languages-content .lang-item {
|
||||
width: calc(~'100% - 40px');
|
||||
.b-languages-content label {
|
||||
width: calc(100% - 40px);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,80 +0,0 @@
|
|||
|
||||
.UserBackground body {
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
#rl-left, #rl-right {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
#rl-left {
|
||||
width: @rlLeftWidth;
|
||||
}
|
||||
|
||||
#rl-right {
|
||||
z-index: 1;
|
||||
left: @rlLeftWidth;
|
||||
}
|
||||
|
||||
#rl-popups > .rl-view-model {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1100;
|
||||
overflow: auto;
|
||||
background-color: rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
#rl-settings-subscreen {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* desktop */
|
||||
@media screen and (min-width: 1000px) {
|
||||
.toggleLeft {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* desktop-large */
|
||||
@media screen and (min-width: 1401px) {
|
||||
#rl-left {
|
||||
width: @rlLeftWidth + 20;
|
||||
}
|
||||
|
||||
#rl-right {
|
||||
left: @rlLeftWidth + 20;
|
||||
}
|
||||
}
|
||||
|
||||
/* mobile and tablet */
|
||||
@media screen and (max-width: 999px) {
|
||||
#rl-settings-subscreen {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
html:not(.rl-left-panel-disabled) #rl-right {
|
||||
right: 5-@rlLeftWidth;
|
||||
}
|
||||
|
||||
html.rl-left-panel-disabled {
|
||||
#rl-left {
|
||||
width: 0 !important;
|
||||
}
|
||||
#rl-right {
|
||||
left: 5px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,11 @@ body {
|
|||
right: 0;
|
||||
}
|
||||
|
||||
html.list-loading body {
|
||||
/* cursor: wait;*/
|
||||
cursor: progress;
|
||||
}
|
||||
|
||||
html:not(.rl-mobile) {
|
||||
min-width: 700px;
|
||||
}
|
||||
|
|
@ -32,10 +37,6 @@ html:not(.rl-mobile) {
|
|||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
option:disabled {
|
||||
color: #aaa;
|
||||
cursor: not-allowed;
|
||||
|
|
|
|||
|
|
@ -16,32 +16,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.e-paginator {
|
||||
|
||||
.e-page {
|
||||
|
||||
display: inline-block;
|
||||
opacity: 0.5;
|
||||
text-decoration: none;
|
||||
font-size: 22px;
|
||||
padding: 3px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&.current {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.current .e-page-number {
|
||||
font-size: 115%;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-save-trigger {
|
||||
|
||||
display: inline-block;
|
||||
|
|
@ -93,25 +67,6 @@ textarea + .settings-save-trigger {
|
|||
}
|
||||
}
|
||||
|
||||
.e-languages {
|
||||
|
||||
margin-top: 8px;
|
||||
color: #333;
|
||||
|
||||
.flag-name {
|
||||
|
||||
color: #333;
|
||||
border-bottom: 1px dashed #333;
|
||||
cursor: pointer;
|
||||
padding: 2px 0;
|
||||
|
||||
&:focus {
|
||||
outline: 1px;
|
||||
outline-style: dotted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.e-action {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
.b-advanced-search-content {
|
||||
&.modal {
|
||||
width: 780px;
|
||||
max-width: 780px;
|
||||
}
|
||||
.control-label {
|
||||
width: 110px;
|
||||
|
|
@ -43,15 +43,6 @@
|
|||
transition: opacity 0.3s linear;
|
||||
}
|
||||
|
||||
.messageList {
|
||||
.messageListItem {
|
||||
transition: max-height 400ms ease;
|
||||
}
|
||||
.listDragOver {
|
||||
transition: all 400ms ease;
|
||||
}
|
||||
}
|
||||
|
||||
.b-list-content {
|
||||
.e-contact-item {
|
||||
transition: max-height 400ms ease;
|
||||
|
|
@ -144,6 +144,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 999px) {
|
||||
.b-compose.modal {
|
||||
border: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.minimize-custom {
|
||||
border: 0 solid #333;
|
||||
border-bottom-width: 3px;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
@contacts-popup-left-width: 220px;
|
||||
@contacts-popup-left-width: 25%;
|
||||
|
||||
.b-contacts-content {
|
||||
.RL-PopupsContacts {
|
||||
|
||||
.control-group {
|
||||
.control-label {
|
||||
|
|
@ -10,11 +10,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.modal {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
.modal {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: auto;
|
||||
min-width: 550px;
|
||||
max-width: 900px;
|
||||
|
|
@ -36,8 +33,8 @@
|
|||
border-bottom: 1px solid rgba(128,128,128,0.4);
|
||||
|
||||
.e-search {
|
||||
margin-top: 7px;
|
||||
width: @contacts-popup-left-width - 20;
|
||||
margin: 7px 0 0;
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -268,18 +265,3 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 700px) {
|
||||
.b-contacts-content {
|
||||
.b-list-content, .b-list-toolbar, .b-list-footer-toolbar {
|
||||
width: 170px;
|
||||
}
|
||||
.b-list-toolbar .e-search {
|
||||
width: 150px;
|
||||
}
|
||||
.b-view-content-toolbar, .b-view-content {
|
||||
left: 170px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -240,7 +240,7 @@
|
|||
|
||||
.btn {
|
||||
&.buttonCompose {
|
||||
width: calc(~'100% - 85px');
|
||||
width: calc(100% - 85px);
|
||||
max-width: -moz-fit-content;
|
||||
max-width: -webkit-fit-content;
|
||||
max-width: fit-content;
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
.b-identity-content {
|
||||
|
||||
&.modal {
|
||||
width: 770px;
|
||||
max-width: 770px;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
|
|
@ -371,3 +371,24 @@ html:not(.rl-mobile) .show-mobile {
|
|||
html.rl-mobile .width100-on-mobile {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.e-paginator {
|
||||
|
||||
.e-page {
|
||||
opacity: 0.5;
|
||||
text-decoration: none;
|
||||
font-size: 22px;
|
||||
padding: 3px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&.current {
|
||||
opacity: 1;
|
||||
font-size: 25px;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -198,215 +198,6 @@ html.rl-no-preview-pane {
|
|||
}
|
||||
}
|
||||
|
||||
.messageListItem {
|
||||
|
||||
position: relative;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
border: 0;
|
||||
border-bottom: 1px solid rgba(153, 153, 153, 0.2);
|
||||
border-left: 6px solid #eee;
|
||||
|
||||
z-index: 100;
|
||||
|
||||
&.focused {
|
||||
background-color: rgba(128, 128, 128, 0.1);
|
||||
border-left-color: #ccc;
|
||||
}
|
||||
|
||||
.importantMark {
|
||||
display: none;
|
||||
|
||||
color:red;
|
||||
margin-right:5px
|
||||
}
|
||||
|
||||
&.deleted-mark {
|
||||
opacity: .7;
|
||||
.subjectParent {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
|
||||
&.important .importantMark {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
&.new {
|
||||
max-height: 0;
|
||||
}
|
||||
|
||||
&.deleted {
|
||||
max-height: 0;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
.checkboxMessage {
|
||||
line-height: 12px;
|
||||
padding: 0 8px 0 6px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.flagParent {
|
||||
padding: 0 10px 0 5px;
|
||||
}
|
||||
|
||||
time, .sizeParent {
|
||||
margin: 0 5px;
|
||||
opacity: 0.6;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.threadsParent {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.attachmentParent {
|
||||
position: relative;
|
||||
margin: 2px 10px 0 5px;
|
||||
color: #666;
|
||||
text-shadow: 0 1px 0 #eee;
|
||||
}
|
||||
|
||||
.senderParent, .subjectParent {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.threadsCountParent {
|
||||
display: inline;
|
||||
overflow: hidden;
|
||||
background-color: #eee;
|
||||
padding: 1px 5px;
|
||||
margin-right: 5px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.threadsCountParent.lastSelected {
|
||||
background-color: #999;
|
||||
border-color: #999;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.threadsCountParent:hover {
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
.subjectParent .emptySubjectText {
|
||||
display: none;
|
||||
font-style: italic;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.emptySubject .subjectParent {
|
||||
.subject {
|
||||
display: none;
|
||||
}
|
||||
.emptySubjectText {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.sender, .subject {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.threads-len {
|
||||
.threads-len-data {
|
||||
|
||||
border-radius: 6px;
|
||||
padding: 2px 0 1px 4px;
|
||||
border: 1px solid #ccc;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(127,127,127,0.3);
|
||||
border-color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.flagOff {
|
||||
opacity: 0.5;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.flagOn, .flagOnHalf {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
.replyFlag, .forwardFlag {
|
||||
display: none;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
&.answered .replyFlag {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&.forwarded .forwardFlag {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&:not(.withAttachments) .attachmentParent {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.hasUnseenSubMessage {
|
||||
background-color: rgba(255, 255, 64, 0.15);
|
||||
border-left-color: lighten(orange, 30%);
|
||||
&.focused {
|
||||
border-left-color: darken(orange, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&.unseen {
|
||||
background-color: rgba(255, 255, 64, 0.15);
|
||||
border-left-color: orange;
|
||||
.sender, .subjectParent {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.focused {
|
||||
border-left-color: darken(orange, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&.checked {
|
||||
border-left-color: lighten(#398CF2, 10%);
|
||||
|
||||
&.focused {
|
||||
border-left-color: darken(#398CF2, 5%);
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: rgba(140, 200, 255, 0.3);
|
||||
border-bottom-color: rgba(57, 140, 242, 0.2);
|
||||
border-left-color: #398CF2;
|
||||
z-index: 101;
|
||||
|
||||
+ .messageListItem {
|
||||
border-bottom-color: rgba(57, 140, 242, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.flagged) .flagOn,
|
||||
&:not(.hasFlaggedSubMessage) .flagOnHalf,
|
||||
&.flagged .flagOff, &.hasFlaggedSubMessage .flagOff {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.message-focused {
|
||||
.b-message-list-wrapper {
|
||||
background-color: #000;
|
||||
|
|
@ -415,17 +206,250 @@ html.rl-no-preview-pane {
|
|||
opacity: 0.97;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.hideMessageListCheckbox {
|
||||
.checkboxMessage, .checkboxCheckAll {
|
||||
visibility: hidden;
|
||||
.hideMessageListCheckbox {
|
||||
.checkboxCheckAll {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.checkboxMessage {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.messageListItem {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
position: relative;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
|
||||
margin: 0;
|
||||
padding: 5px 0;
|
||||
border: 0;
|
||||
border-bottom: 1px solid rgba(153, 153, 153, 0.2);
|
||||
border-left: 6px solid #eee;
|
||||
|
||||
z-index: 100;
|
||||
|
||||
> * {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
order: 0;
|
||||
}
|
||||
|
||||
&.focused {
|
||||
background-color: rgba(128, 128, 128, 0.1);
|
||||
border-left-color: #ccc;
|
||||
}
|
||||
|
||||
.importantMark {
|
||||
display: none;
|
||||
|
||||
color:red;
|
||||
margin-right:5px
|
||||
}
|
||||
|
||||
&.deleted-mark {
|
||||
opacity: .7;
|
||||
.subjectParent {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
|
||||
&.important .importantMark {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
&.new {
|
||||
max-height: 0;
|
||||
}
|
||||
|
||||
&.deleted {
|
||||
max-height: 0;
|
||||
border-color: transparent !important;
|
||||
}
|
||||
|
||||
.checkboxMessage {
|
||||
line-height: 12px;
|
||||
padding: 0 8px 0 6px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.flagParent {
|
||||
padding: 0 10px 0 5px;
|
||||
}
|
||||
|
||||
time, .sizeParent {
|
||||
margin: 0 5px;
|
||||
opacity: 0.6;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.threadsParent {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.attachmentParent {
|
||||
position: relative;
|
||||
margin: 2px 10px 0 5px;
|
||||
color: #666;
|
||||
text-shadow: 0 1px 0 #eee;
|
||||
}
|
||||
|
||||
.senderParent, .subjectParent {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.threadsCountParent {
|
||||
display: inline;
|
||||
overflow: hidden;
|
||||
background-color: #eee;
|
||||
padding: 1px 5px;
|
||||
margin-right: 5px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.threadsCountParent.lastSelected {
|
||||
background-color: #999;
|
||||
border-color: #999;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.threadsCountParent:hover {
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
.subjectParent .emptySubjectText {
|
||||
display: none;
|
||||
font-style: italic;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.emptySubject .subjectParent {
|
||||
.subject {
|
||||
display: none;
|
||||
}
|
||||
.emptySubjectText {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.sender, .subject {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.threads-len {
|
||||
.threads-len-data {
|
||||
|
||||
border-radius: 6px;
|
||||
padding: 2px 0 1px 4px;
|
||||
border: 1px solid #ccc;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(127,127,127,0.3);
|
||||
border-color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.flagOff {
|
||||
opacity: 0.5;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.flagOn, .flagOnHalf {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
.replyFlag, .forwardFlag {
|
||||
display: none;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
&.answered .replyFlag {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&.forwarded .forwardFlag {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&:not(.withAttachments) .attachmentParent {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.hasUnseenSubMessage {
|
||||
background-color: rgba(255, 255, 64, 0.15);
|
||||
border-left-color: lighten(orange, 30%);
|
||||
&.focused {
|
||||
border-left-color: darken(orange, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&.unseen {
|
||||
background-color: rgba(255, 255, 64, 0.15);
|
||||
border-left-color: orange;
|
||||
.sender, .subjectParent {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.focused {
|
||||
border-left-color: darken(orange, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&.checked {
|
||||
border-left-color: lighten(#398CF2, 10%);
|
||||
|
||||
&.focused {
|
||||
border-left-color: darken(#398CF2, 5%);
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: rgba(140, 200, 255, 0.3);
|
||||
border-bottom-color: rgba(57, 140, 242, 0.2);
|
||||
border-left-color: #398CF2;
|
||||
z-index: 101;
|
||||
|
||||
+ .messageListItem {
|
||||
border-bottom-color: rgba(57, 140, 242, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.flagged) .flagOn,
|
||||
&:not(.hasFlaggedSubMessage) .flagOnHalf,
|
||||
&.flagged .flagOff, &.hasFlaggedSubMessage .flagOff {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
html.rl-ctrl-key-pressed .messageListItem {
|
||||
cursor: copy;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1000px) {
|
||||
.messageList {
|
||||
.listDragOver {
|
||||
transition: all 400ms ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* desktop-large */
|
||||
@media screen and (min-width: 1401px) {
|
||||
.messageList .messageListItem {
|
||||
.messageListItem {
|
||||
font-size: 13px;
|
||||
time {
|
||||
font-size: 13px;
|
||||
|
|
@ -448,18 +472,6 @@ html .messageList .line-loading {
|
|||
top: 0;
|
||||
}
|
||||
|
||||
.messageListItem {
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
> * {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
order: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.rl-side-preview-pane, .rl-mobile {
|
||||
|
||||
.messageListItem {
|
||||
|
|
@ -471,7 +483,6 @@ html .messageList .line-loading {
|
|||
.subjectParent {
|
||||
flex: 1 0 auto;
|
||||
line-height: 16px;
|
||||
margin-left: 30px;
|
||||
order: 1;
|
||||
width: calc(100% - 120px);
|
||||
}
|
||||
|
|
@ -492,9 +503,13 @@ html .messageList .line-loading {
|
|||
order: 3;
|
||||
}
|
||||
}
|
||||
|
||||
.b-message-list-wrapper:not(.hideMessageListCheckbox) .subjectParent {
|
||||
margin-left: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
html:not(.rl-mobile):not(.rl-side-preview-pane) .messageList {
|
||||
html:not(.rl-mobile):not(.rl-side-preview-pane) {
|
||||
|
||||
.messageListItem {
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
.b-open-pgp-key-view-content, .b-open-pgp-key-generate-content {
|
||||
&.modal {
|
||||
width: 570px;
|
||||
max-width: 570px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
.b-compose-open-pgp-content {
|
||||
&.modal {
|
||||
width: 800px;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.key-list {
|
||||
|
|
@ -97,7 +97,7 @@
|
|||
|
||||
.b-message-open-pgp-content {
|
||||
&.modal {
|
||||
width: 700px;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.key-list {
|
||||
|
|
@ -134,7 +134,7 @@
|
|||
|
||||
.b-open-pgp-key-add-content {
|
||||
&.modal {
|
||||
width: 645px;
|
||||
max-width: 645px;
|
||||
}
|
||||
.inputKey {
|
||||
font-family: var(--fontMono);
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
.rl-view-model {
|
||||
.b-shortcuts-content {
|
||||
&.modal {
|
||||
width: 700px;
|
||||
max-width: 700px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
.b-template-add-content {
|
||||
|
||||
&.modal {
|
||||
width: 750px;
|
||||
max-width: 750px;
|
||||
}
|
||||
|
||||
.e-template-place {
|
||||
|
|
@ -12,12 +12,7 @@ label.inline, span.inline {
|
|||
line-height: @baseLineHeight * 2;
|
||||
color: @grayDark;
|
||||
border: 0;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
|
||||
small {
|
||||
font-size: @baseLineHeight * .75;
|
||||
color: @grayLight;
|
||||
}
|
||||
border-bottom: 1px solid @dropdownDividerTop;
|
||||
}
|
||||
|
||||
.legend + .control-group {
|
||||
|
|
@ -40,30 +35,19 @@ label.inline, span.inline {
|
|||
|
||||
.alert a {
|
||||
text-decoration: underline;
|
||||
color: #c09853;
|
||||
color: @warningText;
|
||||
}
|
||||
|
||||
.alert.alert-info a {
|
||||
color: #3a87ad;
|
||||
color: @infoText;
|
||||
}
|
||||
|
||||
.alert.alert-error a {
|
||||
color: #b94a48;
|
||||
}
|
||||
|
||||
.form-horizontal.long-label .control-group {
|
||||
.control-label {
|
||||
width: 160px;
|
||||
}
|
||||
color: @errorText;
|
||||
}
|
||||
|
||||
/* mobile and tablet */
|
||||
@media screen and (max-width: 999px) {
|
||||
|
||||
.modal {
|
||||
width: calc(~'100% - 20px') !important;
|
||||
}
|
||||
|
||||
.form-horizontal {
|
||||
.control-group {
|
||||
.control-label {
|
||||
|
|
|
|||
|
|
@ -63,7 +63,8 @@
|
|||
margin: -1px -2px;
|
||||
}
|
||||
|
||||
.icon-spinner:not(.not-animated) {
|
||||
.icon-spinner:not(.not-animated),
|
||||
.list-loading .icon-spinner.not-animated {
|
||||
border-color: transparent;
|
||||
border-top-color: #999;
|
||||
animation: rotation .8s infinite ease-in-out;
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ class AddOpenPgpKeyPopupView extends AbstractViewPopup {
|
|||
return false;
|
||||
}
|
||||
|
||||
this.cancelCommand && this.cancelCommand();
|
||||
this.cancelCommand();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { serverRequest } from 'Common/Links';
|
|||
import { i18n, getNotification, getUploadErrorDescByCode } from 'Common/Translator';
|
||||
import { timestampToString } from 'Common/Momentor';
|
||||
import { MessageFlagsCache, setFolderHash } from 'Common/Cache';
|
||||
import { Settings, SettingsGet } from 'Common/Globals';
|
||||
import { doc, Settings, SettingsGet } from 'Common/Globals';
|
||||
|
||||
import { AppUserStore } from 'Stores/User/App';
|
||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||
|
|
@ -43,6 +43,8 @@ import { AskPopupView } from 'View/Popup/Ask';
|
|||
import { ContactsPopupView } from 'View/Popup/Contacts';
|
||||
import { ComposeOpenPgpPopupView } from 'View/Popup/ComposeOpenPgp';
|
||||
|
||||
import { ThemeStore } from 'Stores/Theme';
|
||||
|
||||
const
|
||||
/**
|
||||
* @param {string} prefix
|
||||
|
|
@ -432,7 +434,7 @@ class ComposePopupView extends AbstractViewPopup {
|
|||
|| getNotification(Notification.CantSendMessage));
|
||||
}
|
||||
} else {
|
||||
this.closeCommand && this.closeCommand();
|
||||
this.closeCommand();
|
||||
}
|
||||
}
|
||||
this.reloadDraftFolder();
|
||||
|
|
@ -643,6 +645,8 @@ class ComposePopupView extends AbstractViewPopup {
|
|||
rl.route.on();
|
||||
|
||||
this.resizeObserver.disconnect();
|
||||
|
||||
(doc.fullscreenElement || doc.webkitFullscreenElement) === this.oContent && doc.exitFullscreen();
|
||||
}
|
||||
|
||||
editor(fOnInit) {
|
||||
|
|
@ -739,6 +743,9 @@ class ComposePopupView extends AbstractViewPopup {
|
|||
} else {
|
||||
this.initOnShow(type, oMessageOrArray, aToEmails, aCcEmails, aBccEmails, sCustomSubject, sCustomPlainText);
|
||||
}
|
||||
|
||||
// (navigator.standalone || matchMedia('(display-mode: standalone)').matches || matchMedia('(display-mode: fullscreen)').matches) &&
|
||||
ThemeStore.isMobile() && this.oContent.requestFullscreen && this.oContent.requestFullscreen();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1062,14 +1069,12 @@ class ComposePopupView extends AbstractViewPopup {
|
|||
tryToClosePopup() {
|
||||
if (!isPopupVisible(AskPopupView) && this.modalVisibility()) {
|
||||
if (this.bSkipNextHide || (this.isEmptyForm() && !this.draftUid())) {
|
||||
this.closeCommand && this.closeCommand();
|
||||
this.closeCommand();
|
||||
} else {
|
||||
showScreenPopup(AskPopupView, [
|
||||
i18n('POPUPS_ASK/DESC_WANT_CLOSE_THIS_WINDOW'),
|
||||
() => {
|
||||
if (this.modalVisibility()) {
|
||||
this.closeCommand && this.closeCommand();
|
||||
}
|
||||
this.modalVisibility() && this.closeCommand();
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
|
@ -1135,6 +1140,26 @@ class ComposePopupView extends AbstractViewPopup {
|
|||
ro.els = [dom.querySelector('.textAreaParent'), dom.querySelector('.attachmentAreaParent')];
|
||||
|
||||
this.editor(editor => editor.modeWysiwyg());
|
||||
|
||||
// Fullscreen must be on app, else other popups fail
|
||||
const el = doc.getElementById('rl-app');
|
||||
let event = 'fullscreenchange';
|
||||
if (!el.requestFullscreen && el.webkitRequestFullscreen) {
|
||||
el.requestFullscreen = el.webkitRequestFullscreen;
|
||||
event = 'webkit' + event;
|
||||
}
|
||||
if (el.requestFullscreen) {
|
||||
if (!doc.exitFullscreen && doc.webkitExitFullscreen) {
|
||||
doc.exitFullscreen = doc.webkitExitFullscreen;
|
||||
}
|
||||
this.oContent = el;
|
||||
el.addEventListener(event, () =>
|
||||
ThemeStore.isMobile()
|
||||
&& this.modalVisibility()
|
||||
&& (doc.fullscreenElement || doc.webkitFullscreenElement) !== el
|
||||
&& this.skipCommand()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -79,14 +79,14 @@ class ContactsPopupView extends AbstractViewPopup {
|
|||
'.e-contact-item.focused'
|
||||
);
|
||||
|
||||
this.selector.on('onItemSelect', contact => {
|
||||
this.populateViewContact(contact || null);
|
||||
this.selector.on('ItemSelect', contact => {
|
||||
this.populateViewContact(contact);
|
||||
if (!contact) {
|
||||
this.emptySelection(true);
|
||||
}
|
||||
});
|
||||
|
||||
this.selector.on('onItemGetUid', contact => contact ? contact.generateUid() : '');
|
||||
this.selector.on('ItemGetUid', contact => contact ? contact.generateUid() : '');
|
||||
|
||||
this.bDropPageAfterDelete = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ class FilterPopupView extends AbstractViewPopup {
|
|||
|
||||
this.fTrueCallback && this.fTrueCallback(this.filter());
|
||||
|
||||
this.modalVisibility() && this.closeCommand && this.closeCommand();
|
||||
this.modalVisibility() && this.closeCommand();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ class NewOpenPgpKeyPopupView extends AbstractViewPopup {
|
|||
openpgpKeyring.store();
|
||||
|
||||
rl.app.reloadOpenPgpKeys();
|
||||
this.cancelCommand && this.cancelCommand();
|
||||
this.cancelCommand();
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ class PluginPopupView extends AbstractViewPopup {
|
|||
if (!isPopupVisible(AskPopupView)) {
|
||||
showScreenPopup(AskPopupView, [
|
||||
i18n('POPUPS_ASK/DESC_WANT_CLOSE_THIS_WINDOW'),
|
||||
() => this.modalVisibility() && this.cancelCommand && this.cancelCommand()
|
||||
() => this.modalVisibility() && this.cancelCommand()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export class MessageListMailBoxUserView extends AbstractViewRight {
|
|||
this.bPrefetch = false;
|
||||
this.emptySubjectValue = '';
|
||||
|
||||
this.iGoToUpUpOrDownDownTimeout = 0;
|
||||
this.iGoToUpOrDownTimeout = 0;
|
||||
|
||||
this.newMoveToFolder = !!SettingsGet('NewMoveToFolder');
|
||||
|
||||
|
|
@ -86,7 +86,6 @@ export class MessageListMailBoxUserView extends AbstractViewRight {
|
|||
|
||||
this.messageListCheckedOrSelected = MessageUserStore.listCheckedOrSelected;
|
||||
this.messageListCompleteLoadingThrottle = MessageUserStore.listCompleteLoading;
|
||||
this.messageListCompleteLoadingThrottleForAnimation = MessageUserStore.listLoadingAnimation;
|
||||
|
||||
initOnStartOrLangChange(() => this.emptySubjectValue = i18n('MESSAGE_LIST/EMPTY_SUBJECT_TEXT'));
|
||||
|
||||
|
|
@ -191,13 +190,15 @@ export class MessageListMailBoxUserView extends AbstractViewRight {
|
|||
'.messageListItem.focused'
|
||||
);
|
||||
|
||||
this.selector.on('onItemSelect', message => MessageUserStore.selectMessage(message));
|
||||
this.selector.on('ItemSelect', message => MessageUserStore.selectMessage(message));
|
||||
|
||||
this.selector.on('onItemGetUid', message => (message ? message.generateUid() : ''));
|
||||
this.selector.on('MiddleClick', message => MessageUserStore.populateMessageBody(message, true));
|
||||
|
||||
this.selector.on('onAutoSelect', () => this.useAutoSelect());
|
||||
this.selector.on('ItemGetUid', message => (message ? message.generateUid() : ''));
|
||||
|
||||
this.selector.on('onUpUpOrDownDown', v => this.goToUpUpOrDownDown(v));
|
||||
this.selector.on('AutoSelect', () => this.useAutoSelect());
|
||||
|
||||
this.selector.on('UpOrDown', v => this.goToUpOrDown(v));
|
||||
|
||||
addEventListener('mailbox.message-list.selector.go-down',
|
||||
e => this.selector.newSelectPosition('ArrowDown', false, e.detail)
|
||||
|
|
@ -351,13 +352,13 @@ export class MessageListMailBoxUserView extends AbstractViewRight {
|
|||
showMessageComposer();
|
||||
}
|
||||
|
||||
goToUpUpOrDownDown(up) {
|
||||
goToUpOrDown(up) {
|
||||
if (MessageUserStore.listChecked().length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
clearTimeout(this.iGoToUpUpOrDownDownTimeout);
|
||||
this.iGoToUpUpOrDownDownTimeout = setTimeout(() => {
|
||||
clearTimeout(this.iGoToUpOrDownTimeout);
|
||||
this.iGoToUpOrDownTimeout = setTimeout(() => {
|
||||
let prev, next, temp, current;
|
||||
|
||||
this.messageListPaginator().find(item => {
|
||||
|
|
@ -668,31 +669,31 @@ export class MessageListMailBoxUserView extends AbstractViewRight {
|
|||
}
|
||||
|
||||
onBuild(dom) {
|
||||
const eqs = (ev, s) => ev.target.closestWithin(s, dom);
|
||||
const eqs = (ev, s) => ev.target.closestWithin('.messageList '+s, dom);
|
||||
|
||||
this.selector.init(dom.querySelector('.b-content'), Scope.MessageList);
|
||||
|
||||
dom.addEventListener('click', event => {
|
||||
ThemeStore.isMobile() && leftPanelDisabled(true);
|
||||
|
||||
if (eqs(event, '.messageList .b-message-list-wrapper') && Scope.MessageView === AppUserStore.focusedState()) {
|
||||
if (eqs(event, '.b-message-list-wrapper') && Scope.MessageView === AppUserStore.focusedState()) {
|
||||
AppUserStore.focusedState(Scope.MessageList);
|
||||
}
|
||||
|
||||
let el = eqs(event, '.e-paginator .e-page');
|
||||
el && this.gotoPage(ko.dataFor(el));
|
||||
|
||||
eqs(event, '.messageList .checkboxCheckAll') && this.checkAll(!this.checkAll());
|
||||
eqs(event, '.checkboxCheckAll') && this.checkAll(!this.checkAll());
|
||||
|
||||
el = eqs(event, '.messageList .messageListItem .flagParent');
|
||||
el = eqs(event, '.messageListItem .flagParent');
|
||||
el && this.flagMessages(ko.dataFor(el));
|
||||
|
||||
el = eqs(event, '.messageList .messageListItem .threads-len');
|
||||
el = eqs(event, '.messageListItem .threads-len');
|
||||
el && this.gotoThread(ko.dataFor(el));
|
||||
});
|
||||
|
||||
dom.addEventListener('dblclick', event => {
|
||||
let el = eqs(event, '.messageList .messageListItem .actionHandle');
|
||||
let el = eqs(event, '.messageListItem .actionHandle');
|
||||
el && this.gotoThread(ko.dataFor(el));
|
||||
});
|
||||
|
||||
|
|
|
|||
17
integrations/nextcloud/snappymail/INSTALL
Executable file
17
integrations/nextcloud/snappymail/INSTALL
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
************************************************************************
|
||||
*
|
||||
* Nextcloud - SnappyMail Webmail package
|
||||
*
|
||||
* @author SnappyMail Team, Nextgen-Networks (@nextgen-networks), Tab Fitts (@tabp0le), Pierre-Alain Bandinelli (@pierre-alain-b)
|
||||
*
|
||||
************************************************************************
|
||||
|
||||
REQUIREMENTS:
|
||||
- Nextcloud version 14 and above
|
||||
|
||||
INSTALL & CONFIGURATION:
|
||||
Start within Nextcloud, and click on the "+ Apps" button in the upper-left corner dropdown menu:
|
||||
Then, enable the SnappyMail plugin that is in the "Social & communication" section.
|
||||
After a quick wait, SnappyMail is installed. Even if it is really attractive, it is too soon to click on the newly appeared "Email" icon in the apps list. You should configure SnappyMail before using it (which makes some sense, doesn'it): go to Nextcloud admin panel (upper-right corner dropdown menu) and go to "Additionnal settings". There click on the "Go to SnappyMail Webmail admin panel".
|
||||
In the SnappyMail admin prompt, the default login is "admin" and the default password is "12345". No need to advise you to change it once in the admin panel!
|
||||
This is it, you are now free to configure SnappyMail as you wish. One important point is the Domains section when you will set up the IMAP/SMTP parameters that shall be associated with the email adresses of your users.
|
||||
76
integrations/nextcloud/snappymail/README.md
Normal file
76
integrations/nextcloud/snappymail/README.md
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# snappymail-nextcloud
|
||||
|
||||
snappymail-nextcloud is a plugin for Nextcloud to use the excellent SnappyMail webmail (https://snappymail.eu/).
|
||||
|
||||
## Which branch for which version of Nextcloud?
|
||||
|
||||
- The master branch corresponds to the latest stable version of the plugin that is suitable for Nextcloud 19 and upwards.
|
||||
- The 'up-to-nc18' corresponds to the working version of the plugin for Nextcloud 14 to 18.
|
||||
- The 'nc14' branch corresponds to a beta version of the plugin for Nextcloud 14. All the changes of 'nc14' branch were merged into master when it was deemed suitable for production.
|
||||
- The 'nc13' branch corresponds to the working version of the plugin for Nextcloud 10 to Nextcloud 13.
|
||||
|
||||
|
||||
Thank you to all contributors to SnappyMail for nextcloud:
|
||||
- SnappyMail Team, who initiated it
|
||||
- Tab Fitts (@tabp0le)
|
||||
- Nextgen Networks (@nextgen-networks)
|
||||
|
||||
## How to Install
|
||||
|
||||
Start within Nextcloud, and click on the "+ Apps" button in the upper-left corner dropdown menu:
|
||||
|
||||

|
||||
|
||||
Then, enable the SnappyMail plugin that you will find in the "Social & communication" section:
|
||||
|
||||

|
||||
|
||||
After a quick wait, SnappyMail is installed. Now you should configure it before use: open the Nextcloud admin panel (upper-right corner dropdown menu) and go to "Additionnal settings". There, click on the "Go to SnappyMail Webmail admin panel" link.
|
||||
|
||||

|
||||
|
||||
To enter SnappyMail admin area, the default login is "admin" and the default password is "12345". Don't forget to change it once in the admin panel!
|
||||
|
||||
From that point, all instance-wide SnappyMail settings can be tweaked as you wish. One important point is the "Domains" section where you should set up the IMAP/SMTP parameters that will be associated with the email adresses of your users. Basically, if a user of the Nextcloud instance starts SnappyMail and puts "firstname@domain.tld" as an email address, then SnappyMail should know how to connect to the IMAP & SMTP of domain.tld. You can fill in this information in the "Domains" section of the SnappyMail admin settings.
|
||||
|
||||

|
||||
|
||||
## SnappyMail Settings, Where Are They?
|
||||
|
||||
SnappyMail for Nextcloud is highly configurable. But settings are available in multiple places and this can be misleading for first-time users.
|
||||
|
||||
### SnappyMail admin settings
|
||||
SnappyMail admin settings can be reached only by the Nextcloud administrator. Open the Nextcloud admin panel ("Admin" in the upper-right corner dropdown menu) and go to "Additionnal settings". There, click on the "Go to SnappyMail Webmail admin panel" link. Alternatively, you may use the following link: https://path.to.nextcloud/index.php/apps/snappymail/app/?admin.
|
||||
|
||||
SnappyMail admin settings include all settings that will apply to all SnappyMail users (default login rules, branding, management of plugins, security rules and domains).
|
||||
|
||||
### SnappyMail user settings
|
||||
Each user of SnappyMail can also change user-specific behaviors in the SnappyMail user settings. SnappyMail user settings are found within SnappyMail by clicking on the user button (in the upper-right corner of SnappyMail) and then choosing "Settings" in the dropdown menu.
|
||||
|
||||
SnappyMail user settings include management of contacts, of email accounts, of folders, appearance and OpenPGP.
|
||||
|
||||
### The specificity of SnappyMail user accounts
|
||||
The plugin passes the login information of the user to the SnappyMail app which then creates and manages the user accounts. Accounts in SnappyMail are based soley on the authenticated email accounts, and do not take into account the nextcloud user which created them in the first place. If two or more Nextcloud users have the same email account in additional settings, they will in fact share the same 'email account' in SnappyMail including any additional email accounts that they may have added subsequently to their main account.
|
||||
This is to be kept in mind for the use case where multiple users shall have the same email account but may be also tempted to add additionnal acounts to their SnappyMail.
|
||||
|
||||
## How to auto-connect to SnappyMail?
|
||||
|
||||
### Auto-connection for all Nextcloud users
|
||||
If your Nextcloud users base is synchronized with an email system, then it is possible that Nextcloud credentials could be used right away to access the centralized email system. In the SnappyMail admin settings, the Nextcloud administrator can then tick the "Automatically login with Nextcloud/Nextcloud user credentials" checkbox.
|
||||
|
||||
Beware, if you tick this box, all Nextcloud users will *not* be able to use the override it with the setting below.
|
||||
|
||||
### Auto-connection for one user at a time
|
||||
Except if the above setting is activated, any Nextcloud user can have Nextcloud and SnappyMail keep in mind the default email/password to connect to SnappyMail. There, logging in Nextcloud is sufficient to then access SnappyMail within Nextcloud.
|
||||
|
||||
To fill in the default email address and password to use, each Nextcloud user should go in the personal settings: choose "Personal" in the upper-right corner dropdown menu. Then select the "Rainlopp webmail" section. You can also use this direct link: https://path.to.nextcloud/index.php/settings/personal#snappymail-webmail.
|
||||
|
||||
|
||||
## How to Activate SnappyMail Logging and then Find Logs
|
||||
|
||||
You can activate SnappyMail logging here: `/path/to/nextcloud/data/snappymail-storage/_data_/_default_/configs/application.ini`
|
||||
```
|
||||
[logs]
|
||||
enable = On
|
||||
```
|
||||
Logs are then available in `/path/to/nextcloud/data/snappymail-storage/_data_/_default_/logs/`
|
||||
1
integrations/nextcloud/snappymail/VERSION
Executable file
1
integrations/nextcloud/snappymail/VERSION
Executable file
|
|
@ -0,0 +1 @@
|
|||
2.5.3
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
class OwnCloudSuggestions implements \RainLoop\Providers\Suggestions\ISuggestions
|
||||
{
|
||||
/**
|
||||
* @var \MailSo\Log\Logger
|
||||
*/
|
||||
protected $oLogger;
|
||||
|
||||
public function Process(\RainLoop\Model\Account $oAccount, string $sQuery, int $iLimit = 20): array
|
||||
{
|
||||
$iInputLimit = $iLimit;
|
||||
$aResult = array();
|
||||
$sQuery = \trim($sQuery);
|
||||
|
||||
try
|
||||
{
|
||||
if ('' === $sQuery || !$oAccount || !OwnCloudPlugin::IsOwnCloudLoggedIn())
|
||||
{
|
||||
return $aResult;
|
||||
}
|
||||
|
||||
$aParams = array('FN', 'NICKNAME', 'TITLE', 'EMAIL');
|
||||
if (\class_exists('OC') && isset(\OC::$server) && \method_exists(\OC::$server, 'getContactsManager'))
|
||||
{
|
||||
$cm = \OC::$server->getContactsManager();
|
||||
if (!$cm && !$cm->isEnabled())
|
||||
{
|
||||
return $aResult;
|
||||
}
|
||||
|
||||
$aSearchResult = $cm->search($sQuery, $aParams);
|
||||
}
|
||||
else if (\class_exists('OCP\Contacts') && \OCP\Contacts::isEnabled())
|
||||
{
|
||||
$aSearchResult = \OCP\Contacts::search($sQuery, $aParams);
|
||||
}
|
||||
else
|
||||
{
|
||||
return $aResult;
|
||||
}
|
||||
|
||||
//$this->oLogger->WriteDump($aSearchResult);
|
||||
|
||||
$aHashes = array();
|
||||
if (\is_array($aSearchResult) && 0 < \count($aSearchResult))
|
||||
{
|
||||
foreach ($aSearchResult as $aContact)
|
||||
{
|
||||
if (0 >= $iLimit)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
$sUid = empty($aContact['UID']) ? '' : $aContact['UID'];
|
||||
if (!empty($sUid))
|
||||
{
|
||||
$mEmails = isset($aContact['EMAIL']) ? $aContact['EMAIL'] : '';
|
||||
|
||||
$sFullName = isset($aContact['FN']) ? \trim($aContact['FN']) : '';
|
||||
if (empty($sFullName))
|
||||
{
|
||||
$sFullName = isset($aContact['NICKNAME']) ? \trim($aContact['NICKNAME']) : '';
|
||||
}
|
||||
|
||||
if (!\is_array($mEmails))
|
||||
{
|
||||
$mEmails = array($mEmails);
|
||||
}
|
||||
|
||||
foreach ($mEmails as $sEmail)
|
||||
{
|
||||
$sHash = '"'.$sFullName.'" <'.$sEmail.'>';
|
||||
if (!isset($aHashes[$sHash]))
|
||||
{
|
||||
$aHashes[$sHash] = true;
|
||||
$aResult[] = array($sEmail, $sFullName);
|
||||
$iLimit--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$aResult = \array_slice($aResult, 0, $iInputLimit);
|
||||
}
|
||||
|
||||
unset($aSearchResult, $aHashes);
|
||||
}
|
||||
catch (\Throwable $oException)
|
||||
{
|
||||
if ($this->oLogger)
|
||||
{
|
||||
$this->oLogger->WriteException($oException);
|
||||
}
|
||||
}
|
||||
|
||||
return $aResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \MailSo\Log\Logger $oLogger
|
||||
*/
|
||||
public function SetLogger($oLogger)
|
||||
{
|
||||
$this->oLogger = $oLogger instanceof \MailSo\Log\Logger ? $oLogger : null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
<?php
|
||||
|
||||
class NextCloudPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||
{
|
||||
const
|
||||
NAME = 'NextCloud',
|
||||
VERSION = '2.0',
|
||||
CATEGORY = 'Integrations',
|
||||
DESCRIPTION = 'Plugin that adds functionality to integrate with NextCloud.';
|
||||
|
||||
private static function IsNextCloud() : bool
|
||||
{
|
||||
return !empty($_ENV['SNAPPYMAIL_NEXTCLOUD']) && \class_exists('OC');
|
||||
}
|
||||
|
||||
private static function IsNextCloudLoggedIn() : bool
|
||||
{
|
||||
return static::IsNextCloud() && \class_exists('OCP\User') && \OCP\User::isLoggedIn();
|
||||
}
|
||||
|
||||
public function Init() : void
|
||||
{
|
||||
if (static::IsNextCloud()) {
|
||||
$this->addHook('main.fabrica', 'MainFabrica');
|
||||
$this->addHook('filter.app-data', 'FilterAppData');
|
||||
$this->addHook('json.attachments', 'DoAttachmentsActions');
|
||||
|
||||
$sAppPath = '';
|
||||
if (\class_exists('OC_App')) {
|
||||
$sAppPath = \rtrim(\trim(\OC_App::getAppWebPath('snappymail')), '\\/').'/app/';
|
||||
}
|
||||
if (!$sAppPath) {
|
||||
$sUrl = \MailSo\Base\Http::SingletonInstance()->GetUrl();
|
||||
if ($sUrl && \preg_match('/\/index\.php\/apps\/snappymail/', $sUrl)) {
|
||||
$sAppPath = \preg_replace('/\/index\.php\/apps\/snappymail.+$/',
|
||||
'/apps/snappymail/app/', $sUrl);
|
||||
}
|
||||
}
|
||||
$_SERVER['SCRIPT_NAME'] = $sAppPath;
|
||||
}
|
||||
}
|
||||
|
||||
public function Supported() : string
|
||||
{
|
||||
if (!static::IsNextCloud()) {
|
||||
return 'NextCloud not found to use this plugin';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// DoAttachmentsActions
|
||||
public function DoAttachmentsActions(\SnappyMail\AttachmentsAction $data)
|
||||
{
|
||||
if ('nextcloud' === $data->action) {
|
||||
if (static::IsNextCloudLoggedIn() && \class_exists('OCP\Files')) {
|
||||
$oFiles = \OCP\Files::getStorage('files');
|
||||
if ($oFiles && $data->filesProvider->IsActive() && \method_exists($oFiles, 'file_put_contents')) {
|
||||
$sSaveFolder = $this->Config()->Get('plugin', 'save_folder', '') ?: 'Attachments';
|
||||
$oFiles->is_dir($sSaveFolder) || $oFiles->mkdir($sSaveFolder);
|
||||
$data->result = true;
|
||||
foreach ($data->items as $aItem) {
|
||||
$sSavedFileName = isset($aItem['FileName']) ? $aItem['FileName'] : 'file.dat';
|
||||
$sSavedFileHash = !empty($aItem['FileHash']) ? $aItem['FileHash'] : '';
|
||||
if (!empty($sSavedFileHash)) {
|
||||
$fFile = $data->filesProvider->GetFile($data->account, $sSavedFileHash, 'rb');
|
||||
if (\is_resource($fFile)) {
|
||||
$sSavedFileNameFull = \MailSo\Base\Utils::SmartFileExists($sSaveFolder.'/'.$sSavedFileName, function ($sPath) use ($oFiles) {
|
||||
return $oFiles->file_exists($sPath);
|
||||
});
|
||||
|
||||
if (!$oFiles->file_put_contents($sSavedFileNameFull, $fFile)) {
|
||||
$data->result = false;
|
||||
}
|
||||
|
||||
if (\is_resource($fFile)) {
|
||||
\fclose($fFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($data->items as $aItem) {
|
||||
$sFileHash = (string) (isset($aItem['FileHash']) ? $aItem['FileHash'] : '');
|
||||
if (!empty($sFileHash)) {
|
||||
$data->filesProvider->Clear($data->account, $sFileHash);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: create pre-login auth hook
|
||||
*/
|
||||
public function ServiceNextCloudAuth()
|
||||
{
|
||||
/*
|
||||
$this->oHttp->ServerNoCache();
|
||||
|
||||
if (!static::IsNextCloud() ||
|
||||
!isset($_ENV['___snappymail_nextcloud_email']) ||
|
||||
!isset($_ENV['___snappymail_nextcloud_password']) ||
|
||||
empty($_ENV['___snappymail_nextcloud_email'])
|
||||
)
|
||||
{
|
||||
$this->oActions->SetAuthLogoutToken();
|
||||
$this->oActions->Location('./');
|
||||
return '';
|
||||
}
|
||||
|
||||
$bLogout = true;
|
||||
|
||||
$sEmail = $_ENV['___snappymail_nextcloud_email'];
|
||||
$sPassword = $_ENV['___snappymail_nextcloud_password'];
|
||||
|
||||
try
|
||||
{
|
||||
$oAccount = $this->oActions->LoginProcess($sEmail, $sPassword);
|
||||
$this->oActions->AuthToken($oAccount);
|
||||
|
||||
$bLogout = !($oAccount instanceof \snappymail\Model\Account);
|
||||
}
|
||||
catch (\Exception $oException)
|
||||
{
|
||||
$this->oActions->Logger()->WriteException($oException);
|
||||
}
|
||||
|
||||
if ($bLogout)
|
||||
{
|
||||
$this->oActions->SetAuthLogoutToken();
|
||||
}
|
||||
|
||||
$this->oActions->Location('./');
|
||||
return '';
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function FilterAppData($bAdmin, &$aResult)
|
||||
{
|
||||
if (!$bAdmin && \is_array($aResult) && static::IsNextCloud()) {
|
||||
$key = \array_search(\RainLoop\Enumerations\Capa::AUTOLOGOUT, $aResult['Capa']);
|
||||
if (false !== $key) {
|
||||
unset($aResult['Capa'][$key]);
|
||||
}
|
||||
if (static::IsNextCloudLoggedIn() && \class_exists('OCP\Files')) {
|
||||
$aResult['System']['attachmentsActions'][] = 'nextcloud';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sName
|
||||
* @param mixed $mResult
|
||||
*/
|
||||
public function MainFabrica($sName, &$mResult)
|
||||
{
|
||||
if ('suggestions' === $sName && static::IsNextCloud() && $this->Config()->Get('plugin', 'suggestions', true)) {
|
||||
include_once __DIR__.'/NextCloudSuggestions.php';
|
||||
if (!\is_array($mResult)) {
|
||||
$mResult = array();
|
||||
}
|
||||
$mResult[] = new NextCloudSuggestions();
|
||||
}
|
||||
}
|
||||
|
||||
protected function configMapping() : array
|
||||
{
|
||||
return array(
|
||||
\RainLoop\Plugins\Property::NewInstance('save_folder')->SetLabel('Save Folder')
|
||||
->SetDefaultValue('Attachments'),
|
||||
\RainLoop\Plugins\Property::NewInstance('suggestions')->SetLabel('Suggestions')
|
||||
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
|
||||
->SetDefaultValue(true)
|
||||
);
|
||||
}
|
||||
}
|
||||
15
integrations/nextcloud/snappymail/appinfo/app.php
Normal file
15
integrations/nextcloud/snappymail/appinfo/app.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Nextcloud - SnappyMail mail plugin
|
||||
*
|
||||
* @author SnappyMail Team, Nextgen-Networks (@nextgen-networks), Tab Fitts (@tabp0le), Pierre-Alain Bandinelli (@pierre-alain-b)
|
||||
*
|
||||
* Based initially on https://github.com/SnappyMail/snappymail-webmail/tree/master/build/owncloud
|
||||
*/
|
||||
|
||||
$app = new OCA\SnappyMail\AppInfo\Application();
|
||||
$app->registerNavigation();
|
||||
$app->registerPersonalSettings();
|
||||
$app->getContainer()->query('SnappyMailHelper')->registerHooks();
|
||||
|
||||
29
integrations/nextcloud/snappymail/appinfo/info.xml
Normal file
29
integrations/nextcloud/snappymail/appinfo/info.xml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0"?>
|
||||
<info xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
|
||||
<id>snappymail</id>
|
||||
<name>SnappyMail</name>
|
||||
<summary>SnappyMail Webmail</summary>
|
||||
<description>Simple, modern and fast web-based email client. After enabling in Nextcloud, go to Nextcloud admin panel, "Additionnal settings" and you will see a "SnappyMail webmail" section. There, click on the link to go to the SnappyMail admin panel. The default user/password is admin/12345. This version is based on SnappyMail 2.5.3 (2021-07).</description>
|
||||
<version>2.5.3</version>
|
||||
<licence>agpl</licence>
|
||||
<author>SnappyMail Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli</author>
|
||||
<namespace>SnappyMail</namespace>
|
||||
<documentation>
|
||||
<user>https://github.com/pierre-alain-b/snappymail-nextcloud/blob/master/README.md</user>
|
||||
<website>https://snappymail.eu/</website>
|
||||
</documentation>
|
||||
<category>social</category>
|
||||
<category>tools</category>
|
||||
<category>integration</category>
|
||||
<screenshot>https://raw.githubusercontent.com/pierre-alain-b/snappymail-nextcloud/master/screenshots/2016.10.20-screenshot.jpg</screenshot>
|
||||
<website>https://github.com/pierre-alain-b/snappymail-nextcloud</website>
|
||||
<bugs>https://github.com/pierre-alain-b/snappymail-nextcloud/issues</bugs>
|
||||
<dependencies>
|
||||
<php min-version="7.4" />
|
||||
<owncloud min-version="6" max-version="9.2" />
|
||||
<nextcloud min-version="20" max-version="22" />
|
||||
</dependencies>
|
||||
<settings>
|
||||
<admin>OCA\SnappyMail\Settings\AdminSettings</admin>
|
||||
</settings>
|
||||
</info>
|
||||
31
integrations/nextcloud/snappymail/appinfo/routes.php
Executable file
31
integrations/nextcloud/snappymail/appinfo/routes.php
Executable file
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'routes' => [
|
||||
[
|
||||
'name' => 'page#index',
|
||||
'url' => '/',
|
||||
'verb' => 'GET'
|
||||
],
|
||||
[
|
||||
'name' => 'page#appGet',
|
||||
'url' => '/app/',
|
||||
'verb' => 'GET'
|
||||
],
|
||||
[
|
||||
'name' => 'page#appPost',
|
||||
'url' => '/app/',
|
||||
'verb' => 'POST'
|
||||
],
|
||||
[
|
||||
'name' => 'ajax#setPersonal',
|
||||
'url' => '/ajax/personal.php',
|
||||
'verb' => 'POST'
|
||||
],
|
||||
[
|
||||
'name' => 'ajax#setAdmin',
|
||||
'url' => '/ajax/admin.php',
|
||||
'verb' => 'POST'
|
||||
]
|
||||
]
|
||||
];
|
||||
1
integrations/nextcloud/snappymail/appinfo/version
Executable file
1
integrations/nextcloud/snappymail/appinfo/version
Executable file
|
|
@ -0,0 +1 @@
|
|||
7.1.2
|
||||
6
integrations/nextcloud/snappymail/css/style.css
Executable file
6
integrations/nextcloud/snappymail/css/style.css
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
/*
|
||||
Empty style sheet!
|
||||
Only needed to give you the opportunity to theme the owncloud part
|
||||
of the snappymail app with the theming system integrated in ownCoud
|
||||
if the snappymail app is activated.
|
||||
*/
|
||||
BIN
integrations/nextcloud/snappymail/img/favicon.png
Normal file
BIN
integrations/nextcloud/snappymail/img/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 992 B |
BIN
integrations/nextcloud/snappymail/img/logo-64x64.png
Normal file
BIN
integrations/nextcloud/snappymail/img/logo-64x64.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 992 B |
12
integrations/nextcloud/snappymail/js/admin.js
Executable file
12
integrations/nextcloud/snappymail/js/admin.js
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
|
||||
/**
|
||||
* Nextcloud - SnappyMail mail plugin
|
||||
*
|
||||
* @author SnappyMail Team, Nextgen-Networks (@nextgen-networks), Tab Fitts (@tabp0le), Pierre-Alain Bandinelli (@pierre-alain-b)
|
||||
*
|
||||
* Based initially on https://github.com/SnappyMail/snappymail-webmail/tree/master/build/owncloud
|
||||
*/
|
||||
|
||||
$(function() {
|
||||
SnappyMailFormHelper('#mail-snappymail-admin-form', 'admin.php');
|
||||
});
|
||||
12
integrations/nextcloud/snappymail/js/personal.js
Executable file
12
integrations/nextcloud/snappymail/js/personal.js
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
|
||||
/**
|
||||
* Nextcloud - SnappyMail mail plugin
|
||||
*
|
||||
* @author SnappyMail Team, Nextgen-Networks (@nextgen-networks), Tab Fitts (@tabp0le), Pierre-Alain Bandinelli (@pierre-alain-b)
|
||||
*
|
||||
* Based initially on https://github.com/SnappyMail/snappymail-webmail/tree/master/build/owncloud
|
||||
*/
|
||||
|
||||
$(function() {
|
||||
SnappyMailFormHelper('#mail-snappymail-personal-form', 'personal.php');
|
||||
});
|
||||
114
integrations/nextcloud/snappymail/js/rainloop.js
Executable file
114
integrations/nextcloud/snappymail/js/rainloop.js
Executable file
|
|
@ -0,0 +1,114 @@
|
|||
// Do the following things once the document is fully loaded.
|
||||
document.onreadystatechange = function () {
|
||||
if (document.readyState === 'complete') {
|
||||
watchIFrameTitle();
|
||||
}
|
||||
}
|
||||
|
||||
// The SnappyMail application is already configured to modify the <title> element
|
||||
// of its root document with the number of unread messages in the inbox.
|
||||
// However, its document is the SnappyMail iframe. This function sets up a
|
||||
// Mutation Observer to watch the <title> element of the iframe for changes in
|
||||
// the unread message count and propagates that to the parent <title> element,
|
||||
// allowing the unread message count to be displayed in the NC tab's text when
|
||||
// the SnappyMail app is selected.
|
||||
function watchIFrameTitle() {
|
||||
iframe = document.getElementById('rliframe');
|
||||
if (!iframe) {
|
||||
return;
|
||||
}
|
||||
target = iframe.contentDocument.getElementsByTagName('title')[0];
|
||||
config = {
|
||||
characterData: true,
|
||||
childList: true,
|
||||
subtree: true
|
||||
}
|
||||
observer = new MutationObserver(function(mutations) {
|
||||
title = mutations[0].target.innerText;
|
||||
if (title) {
|
||||
matches = title.match(/\(([0-9]+)\)/);
|
||||
if (matches) {
|
||||
document.title = '('+ matches[1] + ') ' + t('snappymail', 'Email') + ' - Nextcloud';
|
||||
} else {
|
||||
document.title = t('snappymail', 'Email') + ' - Nextcloud';
|
||||
}
|
||||
}
|
||||
});
|
||||
observer.observe(target, config);
|
||||
}
|
||||
|
||||
function SnappyMailFormHelper(sID, sAjaxFile, fCallback)
|
||||
{
|
||||
try
|
||||
{
|
||||
var
|
||||
oForm = $(sID),
|
||||
oSubmit = $('#snappymail-save-button', oForm),
|
||||
sSubmitValue = oSubmit.val(),
|
||||
oDesc = oForm.find('.snappymail-result-desc')
|
||||
;
|
||||
|
||||
oSubmit.click(function (oEvent) {
|
||||
|
||||
var oDefAjax = null;
|
||||
|
||||
oEvent.preventDefault();
|
||||
|
||||
oForm
|
||||
.addClass('snappymail-ajax')
|
||||
.removeClass('snappymail-error')
|
||||
.removeClass('snappymail-success')
|
||||
;
|
||||
|
||||
oDesc.text('');
|
||||
oSubmit.val('...');
|
||||
|
||||
oDefAjax = $.ajax({
|
||||
'type': 'POST',
|
||||
'async': true,
|
||||
'url': OC.filePath('snappymail', 'ajax', sAjaxFile),
|
||||
'data': oForm.serialize(),
|
||||
'dataType': 'json',
|
||||
'global': true
|
||||
});
|
||||
|
||||
oDefAjax.always(function (oData) {
|
||||
|
||||
var bResult = false;
|
||||
|
||||
oForm.removeClass('snappymail-ajax');
|
||||
oSubmit.val(sSubmitValue);
|
||||
|
||||
if (oData)
|
||||
{
|
||||
bResult = 'success' === oData['status'];
|
||||
if (oData['Message'])
|
||||
{
|
||||
oDesc.text(t('snappymail', oData['Message']));
|
||||
}
|
||||
}
|
||||
|
||||
if (bResult)
|
||||
{
|
||||
oForm.addClass('snappymail-success');
|
||||
}
|
||||
else
|
||||
{
|
||||
oForm.addClass('snappymail-error');
|
||||
if ('' === oDesc.text())
|
||||
{
|
||||
oDesc.text(t('snappymail', 'Error'));
|
||||
}
|
||||
}
|
||||
|
||||
if (fCallback)
|
||||
{
|
||||
fCallback(bResult, oData);
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
catch(e) {}
|
||||
}
|
||||
26
integrations/nextcloud/snappymail/js/resize.js
Executable file
26
integrations/nextcloud/snappymail/js/resize.js
Executable file
|
|
@ -0,0 +1,26 @@
|
|||
$(function (window, document) {
|
||||
|
||||
var
|
||||
buffer = 0, //was 5 but this was creating a white space of 5px at the bottom of the page
|
||||
ifr = document.getElementById('rliframe')
|
||||
;
|
||||
|
||||
|
||||
function pageY(elem) {
|
||||
return elem.offsetParent ? (elem.offsetTop + pageY(elem.offsetParent)) : elem.offsetTop;
|
||||
}
|
||||
|
||||
function resizeIframe() {
|
||||
var height = document.documentElement.clientHeight;
|
||||
height -= pageY(ifr) + buffer;
|
||||
height = (height < 0) ? 0 : height;
|
||||
ifr.style.height = height + 'px';
|
||||
}
|
||||
|
||||
if (ifr)
|
||||
{
|
||||
ifr.onload = resizeIframe;
|
||||
window.onresize = resizeIframe;
|
||||
}
|
||||
|
||||
}(window, document));
|
||||
9
integrations/nextcloud/snappymail/l10n/en_GB.js
Normal file
9
integrations/nextcloud/snappymail/l10n/en_GB.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
OC.L10N.register(
|
||||
"snappymail",
|
||||
{
|
||||
"Email" : "Email",
|
||||
"Error" : "Error",
|
||||
"Save" : "Save",
|
||||
"Password" : "Password"
|
||||
},
|
||||
"nplurals=2; plural=(n != 1);");
|
||||
7
integrations/nextcloud/snappymail/l10n/en_GB.json
Normal file
7
integrations/nextcloud/snappymail/l10n/en_GB.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{ "translations": {
|
||||
"Email" : "Email",
|
||||
"Error" : "Error",
|
||||
"Save" : "Save",
|
||||
"Password" : "Password"
|
||||
},"pluralForm" :"nplurals=2; plural=(n != 1);"
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\SnappyMail\AppInfo;
|
||||
|
||||
use OCA\SnappyMail\Util\SnappyMailHelper;
|
||||
use OCA\SnappyMail\Controller\AjaxController;
|
||||
use OCA\SnappyMail\Controller\PageController;
|
||||
|
||||
use OCP\AppFramework\App;
|
||||
use OCP\IL10N;
|
||||
use OCP\IUser;
|
||||
|
||||
class Application extends App {
|
||||
|
||||
public function __construct(array $urlParams = []) {
|
||||
parent::__construct('snappymail', $urlParams);
|
||||
|
||||
$container = $this->getContainer();
|
||||
$server = $container->getServer();
|
||||
$config = $server->getConfig();
|
||||
|
||||
/**
|
||||
* Controllers
|
||||
*/
|
||||
$container->registerService(
|
||||
'PageController', function($c) {
|
||||
return new PageController(
|
||||
$c->query('AppName'),
|
||||
$c->query('Request'),
|
||||
$c->getServer()->getAppManager(),
|
||||
$c->query('ServerContainer')->getConfig(),
|
||||
$c->getServer()->getSession()
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
$container->registerService(
|
||||
'AjaxController', function($c) {
|
||||
return new AjaxController(
|
||||
$c->query('AppName'),
|
||||
$c->query('Request'),
|
||||
$c->getServer()->getAppManager(),
|
||||
$c->query('ServerContainer')->getConfig(),
|
||||
$c->query(IL10N::class)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Utils
|
||||
*/
|
||||
$container->registerService(
|
||||
'SnappyMailHelper', function($c) {
|
||||
return new SnappyMailHelper(
|
||||
$c->getServer()->getConfig(),
|
||||
$c->getServer()->getUserSession(),
|
||||
$c->getServer()->getAppManager(),
|
||||
$c->getServer()->getSession()
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Add script js/snappymail.js
|
||||
\OCP\Util::addScript('snappymail', 'snappymail');
|
||||
}
|
||||
|
||||
public function registerNavigation() {
|
||||
$container = $this->getContainer();
|
||||
|
||||
$container->query('OCP\INavigationManager')->add(function () use ($container) {
|
||||
$urlGenerator = $container->query('OCP\IURLGenerator');
|
||||
return [
|
||||
'id' => 'snappymail',
|
||||
'order' => 10,
|
||||
'href' => $urlGenerator->linkToRoute('snappymail.page.index'),
|
||||
'icon' => $urlGenerator->imagePath('snappymail', 'logo-64x64.png'),
|
||||
'name' => \OCP\Util::getL10N('snappymail')->t('Email')
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
public function registerPersonalSettings() {
|
||||
\OCP\App::registerPersonal('snappymail', 'templates/personal');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\SnappyMail\Controller;
|
||||
|
||||
use OCA\SnappyMail\Util\SnappyMailHelper;
|
||||
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
use OCP\IRequest;
|
||||
|
||||
class AjaxController extends Controller {
|
||||
private $config;
|
||||
private $appManager;
|
||||
|
||||
public function __construct(string $appName, IRequest $request, IAppManager $appManager, IConfig $config, IL10N $l) {
|
||||
parent::__construct($appName, $request);
|
||||
$this->config = $config;
|
||||
$this->appManager = $appManager;
|
||||
$this->l = $l;
|
||||
}
|
||||
|
||||
public function setAdmin(): JSONResponse {
|
||||
try {
|
||||
$sUrl = '';
|
||||
$sPath = '';
|
||||
$bAutologin = false;
|
||||
|
||||
if (isset($_POST['appname']) && 'snappymail' === $_POST['appname']) {
|
||||
$this->config->setAppValue('snappymail', 'snappymail-autologin',
|
||||
isset($_POST['snappymail-autologin']) ? '1' === $_POST['snappymail-autologin'] : false);
|
||||
$this->config->setAppValue('snappymail', 'snappymail-autologin-with-email',
|
||||
isset($_POST['snappymail-autologin']) ? '2' === $_POST['snappymail-autologin'] : false);
|
||||
$bAutologin = $this->config->getAppValue('snappymail', 'snappymail-autologin', false);
|
||||
} else {
|
||||
return new JSONResponse([
|
||||
'status' => 'error',
|
||||
'Message' => $this->l->t('Invalid argument(s)')
|
||||
]);
|
||||
}
|
||||
|
||||
return new JSONResponse([
|
||||
'status' => 'success',
|
||||
'Message' => $this->l->t('Saved successfully')
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
return new JSONResponse([
|
||||
'status' => 'error',
|
||||
'Message' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*/
|
||||
public function setPersonal(): JSONResponse {
|
||||
try {
|
||||
|
||||
if (isset($_POST['appname'], $_POST['snappymail-password'], $_POST['snappymail-email']) && 'snappymail' === $_POST['appname']) {
|
||||
$sUser = \OC::$server->getUserSession()->getUser()->getUID();
|
||||
|
||||
$sPostEmail = $_POST['snappymail-email'];
|
||||
$this->config->setUserValue($sUser, 'snappymail', 'snappymail-email', $sPostEmail);
|
||||
|
||||
$sPass = $_POST['snappymail-password'];
|
||||
if ('******' !== $sPass && '' !== $sPass) {
|
||||
include_once $this->appManager->getAppPath('snappymail').'/lib/Util/SnappyMailHelper.php';
|
||||
|
||||
$this->config->setUserValue($sUser, 'snappymail', 'snappymail-password',
|
||||
SnappyMailHelper::encodePassword($sPass, md5($sPostEmail)));
|
||||
}
|
||||
|
||||
$sEmail = $this->config->getUserValue($sUser, 'snappymail', 'snappymail-email', '');
|
||||
} else {
|
||||
return new JSONResponse([
|
||||
'status' => 'error',
|
||||
'Message' => $this->l->t('Invalid argument(s)'),
|
||||
'Email' => $sEmail
|
||||
]);
|
||||
}
|
||||
|
||||
return new JSONResponse([
|
||||
'status' => 'success',
|
||||
'Message' => $this->l->t('Saved successfully'),
|
||||
'Email' => $sEmail
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
return new JSONResponse([
|
||||
'status' => 'error',
|
||||
'Message' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\SnappyMail\Controller;
|
||||
|
||||
use OCA\SnappyMail\Util\SnappyMailHelper;
|
||||
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http\ContentSecurityPolicy;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\IConfig;
|
||||
use OCP\IRequest;
|
||||
use OCP\ISession;
|
||||
|
||||
class PageController extends Controller {
|
||||
private $userId;
|
||||
private $config;
|
||||
private $appManager;
|
||||
private $appPath;
|
||||
private $session;
|
||||
|
||||
public function __construct($AppName, IRequest $request, IAppManager $appManager, IConfig $config, ISession $session) {
|
||||
parent::__construct($AppName, $request);
|
||||
$this->appPath = $appManager->getAppPath('snappymail');
|
||||
$this->config = $config;
|
||||
$this->appManager = $appManager;
|
||||
$this->session = $session;
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
* @NoCSRFRequired
|
||||
*/
|
||||
public function index() {
|
||||
\OC::$server->getNavigationManager()->setActiveEntry('snappymail');
|
||||
|
||||
\OCP\Util::addStyle('snappymail', 'style');
|
||||
|
||||
$sUrl = SnappyMailHelper::normalizeUrl(SnappyMailHelper::getAppUrl());
|
||||
|
||||
$params = [
|
||||
'snappymail-iframe-url' => SnappyMailHelper::normalizeUrl($sUrl).'?OwnCloudAuth'
|
||||
];
|
||||
|
||||
$response = new TemplateResponse('snappymail', 'index', $params);
|
||||
|
||||
$csp = new ContentSecurityPolicy();
|
||||
$csp->addAllowedFrameDomain("'self'");
|
||||
$response->setContentSecurityPolicy($csp);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
* @NoCSRFRequired
|
||||
*/
|
||||
public function appGet() {
|
||||
$this->app();
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
* @NoCSRFRequired
|
||||
*/
|
||||
public function appPost() {
|
||||
$this->app();
|
||||
}
|
||||
|
||||
public function app() {
|
||||
|
||||
SnappyMailHelper::regSnappyMailDataFunction();
|
||||
|
||||
if (isset($_GET['OwnCloudAuth'])) {
|
||||
$sEmail = '';
|
||||
$sEncodedPassword = '';
|
||||
|
||||
$sUser = \OC::$server->getUserSession()->getUser()->getUID();
|
||||
|
||||
if ($this->config->getAppValue('snappymail', 'snappymail-autologin', false)) {
|
||||
$sEmail = $sUser;
|
||||
$sPasswordSalt = $sUser;
|
||||
$sEncodedPassword = $this->session['snappymail-autologin-password'];
|
||||
} else if ($this->config->getAppValue('snappymail', 'snappymail-autologin-with-email', false)) {
|
||||
$sEmail = $this->config->getUserValue($sUser, 'settings', 'email', '');
|
||||
$sPasswordSalt = $sUser;
|
||||
$sEncodedPassword = $this->session['snappymail-autologin-password'];
|
||||
}
|
||||
|
||||
// If the user has set credentials for SnappyMail in their personal
|
||||
// settings, override everything before and use those instead.
|
||||
$sIndividualEmail = $this->config->getUserValue($sUser, 'snappymail', 'snappymail-email', '');
|
||||
if ($sIndividualEmail) {
|
||||
$sEmail = $sIndividualEmail;
|
||||
$sPasswordSalt = $sEmail;
|
||||
$sEncodedPassword = $this->config->getUserValue($sUser, 'snappymail', 'snappymail-password', '');
|
||||
}
|
||||
|
||||
$sDecodedPassword = SnappyMailHelper::decodePassword($sEncodedPassword, md5($sPasswordSalt));
|
||||
|
||||
$_ENV['___snappymail_owncloud_email'] = $sEmail;
|
||||
$_ENV['___snappymail_owncloud_password'] = $sDecodedPassword;
|
||||
}
|
||||
|
||||
include $this->appPath . '/app/index.php';
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
namespace OCA\SnappyMail\Settings;
|
||||
|
||||
use OCA\SnappyMail\Util\SnappyMailHelper;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\IConfig;
|
||||
use OCP\Settings\ISettings;
|
||||
|
||||
class AdminSettings implements ISettings {
|
||||
|
||||
private $config;
|
||||
|
||||
public function __construct(IConfig $config) {
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function getForm() {
|
||||
$keys = [
|
||||
'snappymail-autologin',
|
||||
'snappymail-autologin-with-email'
|
||||
];
|
||||
|
||||
$parameters = [];
|
||||
foreach ($keys as $k) {
|
||||
$v = $this->config->getAppValue('snappymail', $k);
|
||||
$parameters[$k] = $v;
|
||||
}
|
||||
|
||||
$uid = \OC::$server->getUserSession()->getUser()->getUID();
|
||||
if (\OC_User::isAdminUser($uid)) {
|
||||
$parameters['snappymail-admin-panel-link'] = SnappyMailHelper::getAppUrl().'?admin';
|
||||
}
|
||||
|
||||
return new TemplateResponse('snappymail', 'admin-local', $parameters);
|
||||
}
|
||||
|
||||
public function getSection() {
|
||||
return 'additional';
|
||||
}
|
||||
|
||||
public function getPriority() {
|
||||
return 50;
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue