Merge branch 'master' into php81

# Conflicts:
#	snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php
#	snappymail/v/0.0.0/app/libraries/RainLoop/Config/Application.php
#	snappymail/v/0.0.0/app/libraries/RainLoop/Enumerations/SignMeType.php
This commit is contained in:
the-djmaze 2024-03-18 13:49:38 +01:00
commit 0661076905
319 changed files with 19562 additions and 7439 deletions

View file

@ -35,7 +35,9 @@ module.exports = {
// vendors/bootstrap/bootstrap.native.js
'BSN': "readonly",
// Mailvelope
'mailvelope': "readonly"
'mailvelope': "readonly",
// Punycode
'IDN': "readonly"
},
// http://eslint.org/docs/rules/
rules: {

View file

@ -1,4 +1,78 @@
## 2.35.2 2024-02-26
## 2.35.4 2024-03-16
### Added
- \SnappyMail\IDN::toAscii()
### Changed
- OpenPGP.js to v5.11.1
- punycode.js lowercase domain names
- Nextcloud changed stored password handling
- application.ini `login_lowercase` removed and now configurable per domain JSON `lowerLogin`
- Update Portuguese by @ner00
### Fixed
- Raise JS TypeEroor "toLowerCase" after update
[#1491](https://github.com/the-djmaze/snappymail/issues/1491)
- Call to undefined function shell_exec
[#1496](https://github.com/the-djmaze/snappymail/issues/1496)
- Download attachments as ZIP doesn't work for PGP encrypted mail
[#1499](https://github.com/the-djmaze/snappymail/issues/1499)
- Importing or downloading a PGP public key attachment from a PGP encrypted message doesn't work
[#1500](https://github.com/the-djmaze/snappymail/issues/1500)
- VCard PHP Notice: Undefined index: ENCODING
## 2.35.3 2024-03-12
### Added
- GnuPG can be disabled
- Missing strings for localization inside identity popup (Cryptography > S/MIME)
[#1458](https://github.com/the-djmaze/snappymail/issues/1458)
- Automatically verify PGP and S/MIME signed messages
- TNEFDecoder for
[#1012](https://github.com/the-djmaze/snappymail/discussions/1012)
- RTF to HTML converter for
[#1012](https://github.com/the-djmaze/snappymail/discussions/1012)
- Polyfill for PHP ctype
[#1250](https://github.com/the-djmaze/snappymail/issues/1250)
### Changed
- `new Error()` to `Error()`
- Reduce KnockoutJS footprint by removing unused code
- CSS reposition rainloopErrorTip location
- Improved error handling on PGP and S/MIME decrypt
- Improved OpenPGP.js import keys
- Use Identity S/MIME key and certificate from server instead of POST
- application.ini `[webmail]language_admin` to `[admin_panel]language`
- application.ini `[security]admin_panel_host` to `[admin_panel]host`
- application.ini `[security]admin_panel_key` to `[admin_panel]key`
- Drop deprecated Domain::SetConfig()
- Internationalized domain names are now handled as punycode
- Cacher->Get() can now return NULL
- Update French by @hguilbert
- Update Polish by @tinola
- Update Portuguese by @ner00
### Fixed
- Handling of Internationalized Domain Names in several areas
- Decrypt error message
- Stalwart ManageSieve Error 352 when getting Filters
[#1455](https://github.com/the-djmaze/snappymail/issues/1455)
- Nextcloud V25+ theme slightly broken
[#1463](https://github.com/the-djmaze/snappymail/issues/1463)
- PGP decryption fails with "Not armored text"
[#1462](https://github.com/the-djmaze/snappymail/issues/1462)
- AUTH_BASIC falling through as AUTH_BEARER; change AUTH_BEARER to a different value
[#1461](https://github.com/the-djmaze/snappymail/issues/1461)
- SetPassword expects \SnappyMail\SensitiveString
- Crash on importing corrupt OpenPGP keys
- Crash on old browsers instead of showing error
- Ignore popups on logoutReload()
- Custom SASLMechanisms fail in IMAP when the connection is secure
[#1484](https://github.com/the-djmaze/snappymail/pull/1484)
## 2.35.2 2024-02-27
### Added
- GnuPG error handling

View file

@ -140,26 +140,26 @@ RainLoop 1.17 vs SnappyMail
|js/* |RainLoop |Snappy |
|--------------- |--------: |--------: |
|admin.js |2.170.153 | 82.445 |
|app.js |4.207.787 | 429.565 |
|boot.js | 868.735 | 4.142 |
|libs.js | 658.812 | 192.786 |
|sieve.js | 0 | 84.707 |
|admin.js |2.170.153 | 82.845 |
|app.js |4.207.787 | 430.201 |
|boot.js | 868.735 | 4.147 |
|libs.js | 658.812 | 201.911 |
|sieve.js | 0 | 84.703 |
|polyfills.js | 334.608 | 0 |
|serviceworker.js | 0 | 285 |
|TOTAL |8.240.095 | 793.930 |
|TOTAL |8.240.095 | 804.092 |
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|admin.min.js | 256.831 | 40.573 | 73.606 | 13.585 | 60.877 | 12.188 |
|app.min.js | 515.367 | 195.013 |139.456 | 66.135 |110.485 | 56.623 |
|boot.min.js | 84.659 | 2.084 | 26.998 | 1.202 | 23.643 | 1.003 |
|libs.min.js | 584.772 | 92.746 |180.901 | 34.452 |155.182 | 30.890 |
|admin.min.js | 256.831 | 40.733 | 73.606 | 13.649 | 60.877 | 12.228 |
|app.min.js | 515.367 | 195.260 |139.456 | 66.275 |110.485 | 56.682 |
|boot.min.js | 84.659 | 2.087 | 26.998 | 1.204 | 23.643 | 1.002 |
|libs.min.js | 584.772 | 92.482 |180.901 | 34.729 |155.182 | 31.017 |
|sieve.min.js | 0 | 41.164 | 0 | 10.365 | 0 | 9.359 |
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 |
|TOTAL user |1.217.635 | 289.843 |358.761 |101.789 |299.485 | 88.516 |
|TOTAL user+sieve |1.217.635 | 331.007 |358.761 |112.154 |299.485 | 97.875 |
|TOTAL admin | 959.099 | 135.403 |292.911 | 49.239 |249.877 | 44.081 |
|TOTAL user |1.217.635 | 289.829 |358.761 |102.208 |299.485 | 88.701 |
|TOTAL user+sieve |1.217.635 | 330.993 |358.761 |112.573 |299.485 | 98.060 |
|TOTAL admin | 959.099 | 135.302 |292.911 | 49.582 |249.877 | 44.247 |
For a user it is around 69% smaller and faster than traditional RainLoop.
@ -188,12 +188,12 @@ For a user it is around 69% smaller and faster than traditional RainLoop.
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|------------ |-------: |------: |------: |------: |--------: |
|app.css | 340.331 | 84.472 | 46.946 | 17.622 | 15.112 |
|app.min.css | 274.947 | 67.857 | 39.647 | 15.537 | 13.549 |
|app.css | 340.331 | 84.531 | 46.946 | 17.627 | 15.111 |
|app.min.css | 274.947 | 67.906 | 39.647 | 15.544 | 13.567 |
|boot.css | | 1.326 | | 664 | 545 |
|boot.min.css | | 1.071 | | 590 | 474 |
|admin.css | | 30.576 | | 7.013 | 6.096 |
|admin.min.css | | 24.692 | | 6.336 | 5.579 |
|admin.css | | 30.573 | | 7.011 | 6.101 |
|admin.min.css | | 24.689 | | 6.334 | 5.575 |
### PGP
RainLoop uses the old OpenPGP.js v2
@ -207,7 +207,7 @@ See https://github.com/the-djmaze/openpgpjs for development
|OpenPGP |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|--------------- |--------: |--------: |------: |-------: |--------: |--------: |
|openpgp.min.js | 330.742 | 540.792 |102.388 | 167.971 | 84.241 | 138.010 |
|openpgp.min.js | 330.742 | 546.309 |102.388 | 169.249 | 84.241 | 138.751 |
|openpgp.worker | 1.499 | | 824 | | 695 | |

View file

@ -3,6 +3,8 @@ import ko from 'ko';
import { logoutLink } from 'Common/Links';
import { i18nToNodes, initOnStartOrLangChange } from 'Common/Translator';
import { arePopupsVisible } from 'Knoin/Knoin';
import { LanguageStore } from 'Stores/Language';
import { initThemes } from 'Stores/Theme';
@ -18,6 +20,7 @@ export class AbstractApp {
}
logoutReload(url) {
arePopupsVisible(false);
url = url || logoutLink();
if (location.href !== url) {
setTimeout(() => location.href = url, 100);

View file

@ -1,6 +1,6 @@
import 'External/ko';
import { Settings, SettingsGet } from 'Common/Globals';
import { SettingsGet, SettingsAdmin } from 'Common/Globals';
import { initThemes } from 'Stores/Theme';
import Remote from 'Remote/Admin/Fetch';
@ -11,6 +11,8 @@ import { LoginAdminScreen } from 'Screen/Admin/Login';
import { startScreens } from 'Knoin/Knoin';
import { AbstractApp } from 'App/Abstract';
import { AskPopupView } from 'View/Popup/Ask';
export class AdminApp extends AbstractApp {
constructor() {
super(Remote);
@ -23,7 +25,8 @@ export class AdminApp extends AbstractApp {
}
start() {
if (!Settings.app('adminAllowed')) {
// if (!Settings.app('adminAllowed')) {
if (!SettingsAdmin('allowed')) {
rl.route.root();
setTimeout(() => location.href = '/', 1);
} else if (SettingsGet('Auth')) {
@ -34,3 +37,16 @@ export class AdminApp extends AbstractApp {
}
}
}
AskPopupView.credentials = function(sAskDesc, btnText) {
return new Promise(resolve => {
this.showModal([
sAskDesc,
view => resolve({username:view.username(), password:view.passphrase()}),
() => resolve(null),
true,
3,
btnText
]);
});
};

View file

@ -86,6 +86,8 @@ export class AppUser extends AbstractApp {
this.folderList = FolderUserStore.folderList;
this.messageList = MessagelistUserStore;
this.ask = AskPopupView;
}
/**
@ -248,3 +250,46 @@ export class AppUser extends AbstractApp {
showScreenPopup(ComposePopupView, params);
}
}
AskPopupView.password = function(sAskDesc, btnText) {
return new Promise(resolve => {
this.showModal([
sAskDesc,
view => resolve({password:view.passphrase(), remember:view.remember()}),
() => resolve(null),
true,
5,
btnText
]);
});
};
AskPopupView.cryptkey = () => new Promise(resolve => {
const fn = () => AskPopupView.showModal([
i18n('CRYPTO/ASK_CRYPTKEY_PASS'),
view => {
let pass = view.passphrase();
if (pass) {
Remote.post('ResealCryptKey', null, {
passphrase: pass
}).then(response => {
resolve(response?.Result);
}).catch(e => {
if (111 === e.code) {
fn();
} else {
console.error(e);
resolve(null);
}
});
} else {
resolve(null);
}
},
() => resolve(null),
true,
1,
i18n('CRYPTO/DECRYPT')
]);
fn();
});

View file

@ -50,6 +50,7 @@ Notifications = {
ConnectionError: 104,
DomainNotAllowed: 109,
AccountNotAllowed: 110,
CryptKeyError: 111,
ContactsSyncError: 140,
@ -95,7 +96,6 @@ Notifications = {
JsonParse: 952,
// JsonTimeout: 953,
UnknownNotification: 998,
UnknownError: 999,
// Admin

View file

@ -138,7 +138,7 @@ export const FileInfo = {
getContentType: fileName => {
fileName = lowerCase(fileName);
if ('winmail.dat' === fileName) {
return app + 'ms-tnef';
return app + 'vnd.ms-tnef';
}
let ext = fileName.split('.').pop();
if (/^(txt|text|def|list|in|ini|log|sql|cfg|conf)$/.test(ext))

View file

@ -15,6 +15,7 @@ export const
Settings = rl.settings,
SettingsGet = Settings.get,
SettingsAdmin = name => (SettingsGet('Admin') || {})[name],
SettingsCapa = name => name && !!(SettingsGet('Capa') || {})[name],
dropdowns = [],

View file

@ -26,10 +26,8 @@ export class HtmlEditor {
if (element) {
onReady = onReady ? [onReady] : [];
this.onReady = fn => onReady.push(fn);
// TODO: make 'which' user configurable
const which = SettingsUserStore.editorWysiwyg(),
wysiwyg = WYSIWYGS.find(item => which == item.name) || WYSIWYGS.find(item => 'Squire' == item.name);
// const wysiwyg = WYSIWYGS.find(item => 'Squire' == item.name);
wysiwyg.construct(this, element, editor => setTimeout(()=>{
this.editor = editor;
editor.on('blur', () => this.blurTrigger());

View file

@ -1,13 +1,13 @@
import { pInt } from 'Common/Utils';
import { doc, Settings } from 'Common/Globals';
import { doc, Settings, SettingsAdmin } from 'Common/Globals';
const
BASE = doc.location.pathname.replace(/\/+$/,'') + '/',
HASH_PREFIX = '#/',
adminPath = () => rl.adminArea() && !Settings.app('adminHost'),
adminPath = () => rl.adminArea() && !SettingsAdmin('host'),
prefix = () => BASE + '?' + (adminPath() ? Settings.app('adminPath') : '');
prefix = () => BASE + '?' + (adminPath() ? SettingsAdmin('path') : '');
export const
SUB_QUERY_PREFIX = '&q[]=',

View file

@ -22,7 +22,7 @@ const
getNotificationMessage = code => {
let key = getKeyByValue(Notifications, code);
return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : '';
return key ? I18N_DATA.NOTIFICATIONS[key] : '';
},
fromNow = date => relativeTime(Math.round((date.getTime() - Date.now()) / 1000));
@ -211,7 +211,7 @@ export const
script.remove();
resolve();
};
script.onerror = () => reject(new Error('Language '+language+' failed'));
script.onerror = () => reject(Error('Language '+language+' failed'));
script.src = langLink(language, admin);
// script.async = true;
doc.head.append(script);

View file

@ -15,7 +15,7 @@ export class JCard {
if (input) {
// read from jCard
if (typeof input !== 'object') {
throw new Error('error reading vcard')
throw Error('error reading vcard')
}
this.parseFromJCard(input)
}
@ -87,7 +87,7 @@ export class JCard {
arg = new VCardProperty(String(arg), value, params, type);
}
if (!(arg instanceof VCardProperty)) {
throw new Error('invalid argument of VCard.set(), expects string arguments or a VCardProperty');
throw Error('invalid argument of VCard.set(), expects string arguments or a VCardProperty');
}
let field = arg.getField();
this.props.set(field, [arg]);
@ -101,7 +101,7 @@ export class JCard {
arg = new VCardProperty(String(arg), value, params, type);
}
if (!(arg instanceof VCardProperty)) {
throw new Error('invalid argument of VCard.add(), expects string arguments or a VCardProperty');
throw Error('invalid argument of VCard.add(), expects string arguments or a VCardProperty');
}
// VCardProperty arguments
let field = arg.getField();
@ -125,14 +125,14 @@ export class JCard {
else if (arg instanceof VCardProperty) {
let propArray = this.props.get(arg.getField());
if (!(propArray === null || propArray === void 0 ? void 0 : propArray.includes(arg)))
throw new Error("Attempted to remove VCardProperty VCard does not have: ".concat(arg));
throw Error("Attempted to remove VCardProperty VCard does not have: ".concat(arg));
propArray.splice(propArray.indexOf(arg), 1);
if (propArray.length === 0)
this.props.delete(arg.getField());
}
// incorrect arguments
else
throw new Error('invalid argument of VCard.remove(), expects ' +
throw Error('invalid argument of VCard.remove(), expects ' +
'string and optional param filter or a VCardProperty');
}
@ -202,7 +202,7 @@ export class JCard {
parseFullName(options) {
let n = this.getOne('n');
if (n === undefined) {
throw new Error('\'fn\' VCardProperty not present in card, cannot parse full name');
throw Error('\'fn\' VCardProperty not present in card, cannot parse full name');
}
let fnString = '';
// Position in n -> position in fn

View file

@ -55,7 +55,7 @@ export class VCardProperty {
}
// invalid property
else {
throw new Error('invalid Property constructor');
throw Error('invalid Property constructor');
}
}

5
dev/External/ko.js vendored
View file

@ -66,8 +66,7 @@ Object.assign(ko.bindingHandlers, {
},
update: (element, fValueAccessor) => {
let value = ko.unwrap(fValueAccessor());
value = isFunction(value) ? value() : value;
errorTip(element, value);
errorTip(element, isFunction(value) ? value() : value);
}
},
@ -107,7 +106,7 @@ Object.assign(ko.bindingHandlers, {
const command = fValueAccessor();
if (!command || !command.canExecute) {
throw new Error('Value should be a command');
throw Error('Value should be a command');
}
ko.bindingHandlers['FORM'==element.nodeName ? 'submit' : 'click'].init(

View file

@ -27,7 +27,7 @@ export class AbstractModel {
constructor() {
/*
if (new.target === AbstractModel) {
throw new Error("Can't instantiate AbstractModel!");
throw Error("Can't instantiate AbstractModel!");
}
*/
Object.defineProperty(this, 'disposables', {value: []});

View file

@ -80,6 +80,12 @@ export function MimeToMessage(data, message)
detached: true
});
}
} else if ('application/pkcs7-mime' === type.value /*&& 'signed-data' === type.params['smime-type']=*/) {
message.smimeSigned({
micAlg: type.micalg,
bodyPart: part,
detached: false
});
}
});

View file

@ -5,7 +5,7 @@ export class AbstractCollectionModel extends Array
constructor() {
/*
if (new.target === AbstractCollectionModel) {
throw new Error("Can't instantiate AbstractCollectionModel!");
throw Error("Can't instantiate AbstractCollectionModel!");
}
*/
super();

View file

@ -28,6 +28,10 @@ export class AccountModel extends AbstractModel {
&& setTimeout(()=>this.fetchUnread(), (Math.ceil(Math.random() * 10)) * 3000);
}
label() {
return this.name || IDN.toUnicode(this.email);
}
/**
* Get INBOX unread messages
*/

View file

@ -126,7 +126,10 @@ export class AttachmentModel extends AbstractModel {
}
get download() {
return b64EncodeJSONSafe({
return b64EncodeJSONSafe(this.url ? {
fileName: this.fileName,
data: this.url.replace(/^.+,/, '')
} : {
folder: this.folder,
uid: this.uid,
mimeIndex: this.mimeIndex,

View file

@ -3,9 +3,8 @@ import { AbstractCollectionModel } from 'Model/AbstractCollection';
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
import { isArray, getKeyByValue, forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
import { ClientSideKeyNameExpandedFolders, FolderType, FolderMetadataKeys } from 'Common/EnumsUser';
import { clearCache, getFolderFromCacheList, setFolder, setFolderInboxName, removeFolderFromCacheList } from 'Common/Cache';
import { clearCache, getFolderFromCacheList, setFolder, setFolderInboxName } from 'Common/Cache';
import { Settings, SettingsGet, fireEvent } from 'Common/Globals';
import { Notifications } from 'Common/Enums';
import * as Local from 'Storage/Client';
@ -15,7 +14,7 @@ import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { SettingsUserStore } from 'Stores/User/Settings';
import { sortFolders } from 'Common/Folders';
import { i18n, translateTrigger, getNotification } from 'Common/Translator';
import { i18n, translateTrigger } from 'Common/Translator';
import { AbstractModel } from 'Knoin/AbstractModel';
@ -485,41 +484,6 @@ export class FolderModel extends AbstractModel {
showScreenPopup(FolderPopupView, [this]);
}
rename(nameToEdit, parentName) {
nameToEdit = nameToEdit.trim();
const folder = this,
parentFolder = getFolderFromCacheList(parentName),
oldFullname = folder.fullName,
newFullname = (parentFolder ? (parentName + parentFolder.delimiter) : '') + nameToEdit;
if (nameToEdit && newFullname != oldFullname) {
Remote.abort('Folders').post('FolderRename', FolderUserStore.foldersRenaming, {
oldName: oldFullname,
newName: newFullname,
subscribe: folder.isSubscribed() ? 1 : 0
})
.then(() => {
folder.fullName = newFullname;
folder.name(nameToEdit);
if (folder.subFolders.length || folder.parentName != parentName) {
Remote.setTrigger(FolderUserStore.foldersLoading, true);
// clearTimeout(Remote.foldersTimeout);
// Remote.foldersTimeout = setTimeout(loadFolders, 500);
setTimeout(loadFolders, 500);
// TODO: rename all subfolders with folder.delimiter to prevent reload?
} else {
removeFolderFromCacheList(folder.fullName);
setFolder(folder);
sortFolders(parentFolder ? parentFolder.subFolders : FolderUserStore.folderList);
}
})
.catch(error => {
FolderUserStore.error(
getNotification(error.code, '', Notifications.CantRenameFolder)
+ '.\n' + error.message);
});
}
}
/**
* For url safe '/#/mailbox/...' path
*/

View file

@ -32,9 +32,9 @@ export class IdentityModel extends AbstractModel {
});
addComputablesTo(this, {
smimeKeyEncrypted: () => this.smimeKey().includes('-----BEGIN ENCRYPTED PRIVATE KEY-----')
// smimeKeyValid: () => this.smimeKeyEncrypted() | this.smimeKey().includes('-----BEGIN PRIVATE KEY-----')
// smimeCertificateValid: () => this.smimeKey().includes('-----BEGIN CERTIFICATE-----')
smimeKeyEncrypted: () => this.smimeKey().includes('-----BEGIN ENCRYPTED PRIVATE KEY-----'),
smimeKeyValid: () => /^-----BEGIN (ENCRYPTED )?PRIVATE KEY-----/.test(this.smimeKey()),
smimeCertificateValid: () => /^-----BEGIN CERTIFICATE-----/.test(this.smimeCertificate())
});
}

View file

@ -120,12 +120,10 @@ export class MessageModel extends AbstractModel {
encrypted: false,
pgpSigned: null,
pgpVerified: null,
pgpEncrypted: null,
pgpDecrypted: false,
smimeSigned: null,
smimeVerified: null,
smimeEncrypted: null,
smimeDecrypted: false,
@ -199,10 +197,9 @@ export class MessageModel extends AbstractModel {
}
});
this.smimeSigned.subscribe(value => {
value?.body && MimeToMessage(value.body, this);
value?.body && this.smimeVerified(value.verified);
});
this.smimeSigned.subscribe(value =>
value?.body && MimeToMessage(value.body, this)
);
}
get requestHash() {
@ -423,6 +420,11 @@ export class MessageModel extends AbstractModel {
return this.viewBody(false);
}
swapColors() {
const cl = this.body?.classList;
cl && cl.toggle('swapColors');
}
/**
* @param {boolean=} print = false
*/

View file

@ -19,7 +19,6 @@ checkResponseError = data => {
Notifications.DomainNotAllowed,
Notifications.AccountNotAllowed,
Notifications.MailServerError,
Notifications.UnknownNotification,
Notifications.UnknownError
].includes(err)
) {
@ -133,7 +132,7 @@ export class AbstractFetchRemote
fetchJSON(sAction, getURL(sGetAdd),
sGetAdd ? null : (params || {}),
undefined === iTimeout ? 30000 : pInt(iTimeout),
data => {
async data => {
let iError = 0;
if (data) {
/*
@ -149,6 +148,10 @@ export class AbstractFetchRemote
}
}
if (111 === iError && rl.app.ask && await rl.app.ask.cryptkey()) {
return this.request(sAction, fCallback, params, iTimeout, sGetAdd);
}
fCallback && fCallback(
iError,
data,
@ -186,12 +189,16 @@ export class AbstractFetchRemote
post(action, fTrigger, params, timeOut) {
this.setTrigger(fTrigger, true);
return fetchJSON(action, getURL(), params || {}, pInt(timeOut, 30000),
data => {
async data => {
abort(action, 0, 1);
if (!data) {
return Promise.reject(new FetchError(Notifications.JsonParse));
}
if (111 === data?.ErrorCode && rl.app.ask && await rl.app.ask.cryptkey()) {
return this.post(action, fTrigger, params, timeOut);
}
/*
let isCached = false, type = '';
if (data?.epoch) {

View file

@ -2,6 +2,9 @@ import ko from 'ko';
import Remote from 'Remote/Admin/Fetch';
import { forEachObjectEntry } from 'Common/Utils';
import { SettingsAdmin } from 'Common/Globals';
import { LanguageStore } from 'Stores/Language';
import { ThemeStore } from 'Stores/Theme';
export class AdminSettingsConfig /*extends AbstractViewSettings*/ {
@ -53,6 +56,11 @@ export class AdminSettingsConfig /*extends AbstractViewSettings*/ {
items: []
};
forEachObjectEntry(items, (skey, item) => {
if ('language' === skey) {
item[2] = ('webmail' === key) ? LanguageStore.languages : SettingsAdmin('languages');
} else if ('theme' === skey) {
item[2] = ThemeStore.themes;
}
'admin_password' === skey ||
section.items.push({
key: `config[${key}][${skey}]`,

View file

@ -1,13 +1,9 @@
import ko from 'ko';
import {
isArray
} from 'Common/Utils';
import { addObservablesTo, addSubscribablesTo, addComputablesTo } from 'External/ko';
import { SaveSettingStatus } from 'Common/Enums';
import { Settings, SettingsGet, SettingsCapa } from 'Common/Globals';
import { SettingsAdmin, SettingsGet, SettingsCapa } from 'Common/Globals';
import { translatorReload, convertLangName } from 'Common/Translator';
import { AbstractViewSettings } from 'Knoin/AbstractViews';
@ -24,11 +20,7 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
super();
this.language = LanguageStore.language;
this.languages = LanguageStore.languages;
const aLanguagesAdmin = Settings.app('languagesAdmin');
this.languagesAdmin = ko.observableArray(isArray(aLanguagesAdmin) ? aLanguagesAdmin : []);
this.languageAdmin = ko.observable(SettingsGet('languageAdmin'));
this.languageAdmin = ko.observable(SettingsAdmin('language'));
this.theme = ThemeStore.theme;
this.themes = ThemeStore.themes;
@ -107,14 +99,18 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
}
selectLanguage() {
showScreenPopup(LanguagesPopupView, [this.language, this.languages(), LanguageStore.userLanguage()]);
showScreenPopup(LanguagesPopupView, [
this.language,
LanguageStore.languages,
LanguageStore.userLanguage()
]);
}
selectLanguageAdmin() {
showScreenPopup(LanguagesPopupView, [
this.languageAdmin,
this.languagesAdmin(),
SettingsGet('languageUsers')
SettingsAdmin('languages'),
SettingsAdmin('clientLanguage')
]);
}
}

View file

@ -10,7 +10,7 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
constructor() {
super();
this.addSettings(['useLocalProxyForExternalImages']);
this.addSettings(['useLocalProxyForExternalImages', 'autoVerifySignatures']);
this.weakPassword = rl.app.weakPassword;
@ -28,6 +28,7 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
viewQRCode: '',
capaGnuPG: SettingsCapa('GnuPG'),
capaOpenPGP: SettingsCapa('OpenPGP')
});
@ -65,7 +66,8 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
adminPasswordNew2: reset,
capaOpenPGP: value => Remote.saveSetting('CapaOpenPGP', value)
capaGnuPG: value => Remote.saveSetting('capaGnuPG', value),
capaOpenPGP: value => Remote.saveSetting('capaOpenPGP', value)
});
this.adminTOTP(SettingsGet('adminTOTP'));
@ -75,6 +77,16 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
});
}
generateTOTP() {
let CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567',
length = 16,
secret = '';
while (0 < length--) {
secret += CHARS[Math.floor(Math.random() * 32)];
}
this.adminTOTP(secret);
}
saveAdminUserCommand() {
if (!this.adminLogin().trim()) {
this.adminLoginError(true);

View file

@ -63,11 +63,7 @@ export class UserSettingsSecurity extends AbstractViewSettings {
importToOpenPGP() {
OpenPGPUserStore.isSupported() && Remote.request('GetPGPKeys',
(iError, oData) => {
if (!iError && oData.Result) {
oData.Result.forEach(key => OpenPGPUserStore.importKey(key));
}
}
(iError, oData) => !iError && oData.Result && OpenPGPUserStore.importKeys(oData.Result)
);
}

View file

@ -22,7 +22,7 @@ export class AbstractModel {
constructor() {
/*
if (new.target === AbstractModel) {
throw new Error("Can't instantiate AbstractModel!");
throw Error("Can't instantiate AbstractModel!");
}
*/
Object.defineProperty(this, 'disposables', {value: []});

View file

@ -13,6 +13,7 @@ DomainAdminStore.fetch = () => {
if (!iError) {
DomainAdminStore(
data.Result.map(item => {
item.name = IDN.toUnicode(item.name);
item.disabled = ko.observable(item.disabled);
item.askDelete = ko.observable(false);
return item;

View file

@ -12,7 +12,7 @@ export const LanguageStore = {
const aLanguages = Settings.app('languages');
this.languages(isArray(aLanguages) ? aLanguages : []);
this.language(SettingsGet('language'));
this.userLanguage(SettingsGet('userLanguage'));
this.userLanguage(SettingsGet('clientLanguage'));
this.hourCycle(SettingsGet('hourCycle'));
}
}

View file

@ -17,7 +17,7 @@ const
keys.find(key =>
// key[sign ? 'can_sign' : 'can_decrypt']
(key.can_sign || key.can_decrypt)
&& (key.emails.includes(query) || key.subkeys.find(key => query == key.keyid || query == key.fingerprint))
&& (key.for(query) || key.subkeys.find(key => query == key.keyid || query == key.fingerprint))
);
export const GnuPGUserStore = new class {
@ -46,6 +46,7 @@ export const GnuPGUserStore = new class {
key.fingerprint = key.subkeys[0].fingerprint;
key.uids.forEach(uid => uid.email && aEmails.push(uid.email));
key.emails = aEmails;
key.for = email => aEmails.includes(IDN.toASCII(email));
key.askDelete = ko.observable(false);
key.openForDeletion = ko.observable(null).askDeleteHelper();
key.remove = () => {
@ -69,7 +70,7 @@ export const GnuPGUserStore = new class {
}
};
if (isPrivate) {
key.password = async (btnTxt = 'CRYPTO/SIGN') => {
key.password = async btnTxt => {
const pass = await Passphrases.ask(key,
'GnuPG key<br>' + key.id + ' ' + key.emails[0],
btnTxt
@ -97,6 +98,7 @@ export const GnuPGUserStore = new class {
}
} catch (e) {
Passphrases.delete(key);
alert(e.message);
}
}
return key.armor;
@ -148,7 +150,7 @@ export const GnuPGUserStore = new class {
const count = recipients.length,
length = count ? recipients.filter(email =>
// (key.can_verify || key.can_encrypt) &&
this.publicKeys.find(key => key.emails.includes(email))
this.publicKeys.find(key => key.for(email))
).length : 0;
return length && length === count;
}
@ -156,7 +158,7 @@ export const GnuPGUserStore = new class {
getPublicKeyFingerprints(recipients) {
const fingerprints = [];
recipients.forEach(email => {
fingerprints.push(this.publicKeys.find(key => key.emails.includes(email)).fingerprint);
fingerprints.push(this.publicKeys.find(key => key.for(email)).fingerprint);
});
return fingerprints;
}
@ -204,7 +206,7 @@ export const GnuPGUserStore = new class {
}
async verify(message) {
let data = message.pgpSigned(); // { bodyPartId: "1", sigPartId: "2", micAlg: "pgp-sha256" }
let data = message.pgpSigned(); // { partId: "1", sigPartId: "2", micAlg: "pgp-sha256" }
if (data) {
data = { ...data }; // clone
// const sender = message.from[0].email;
@ -227,7 +229,7 @@ export const GnuPGUserStore = new class {
}
async sign(privateKey) {
return await privateKey.password();
return await privateKey.password('CRYPTO/SIGN');
}
};

View file

@ -16,7 +16,7 @@ import { Passphrases } from 'Storage/Passphrases';
const
findOpenPGPKey = (keys, query/*, sign*/) =>
keys.find(key =>
key.emails.includes(query) || query == key.id || query == key.fingerprint
key.for(query) || query == key.id || query == key.fingerprint
),
decryptKey = async (privateKey, btnTxt = 'SIGN') => {
@ -59,9 +59,11 @@ const
let keys = [], key,
armoredKeys = JSON.parse(storage.getItem(itemname)),
i = arrayLength(armoredKeys);
while (i--) {
while (i--) try {
key = await openpgp.readKey({armoredKey:armoredKeys[i]});
key.err || keys.push(new OpenPgpKeyModel(armoredKeys[i], key));
} catch (e) {
console.error(e);
}
return keys;
},
@ -77,15 +79,11 @@ const
class OpenPgpKeyModel {
constructor(armor, key) {
this.key = key;
const aEmails = [];
if (key.users) {
key.users.forEach(user => user.userID.email && aEmails.push(user.userID.email));
}
this.id = key.getKeyID().toHex().toUpperCase();
this.fingerprint = key.getFingerprint();
this.can_encrypt = !!key.getEncryptionKey();
this.can_sign = !!key.getSigningKey();
this.emails = aEmails;
this.emails = key.users.map(user => IDN.toASCII(user.userID.email)).filter(email => email);
this.armor = armor;
this.askDelete = ko.observable(false);
this.openForDeletion = ko.observable(null).askDeleteHelper();
@ -93,6 +91,18 @@ class OpenPgpKeyModel {
// key.getPrimaryUser()
}
/*
get id() { return this.key.getKeyID().toHex().toUpperCase(); }
get fingerprint() { return this.key.getFingerprint(); }
get can_encrypt() { return !!this.key.getEncryptionKey(); }
get can_sign() { return !!this.key.getSigningKey(); }
get emails() { return this.key.users.map(user => IDN.toASCII(user.userID.email)).filter(email => email); }
get armor() { return this.key.armor(); }
*/
for(email) {
return this.emails.includes(IDN.toASCII(email));
}
view() {
showScreenPopup(OpenPgpKeyPopupView, [this]);
}
@ -142,18 +152,29 @@ export const OpenPGPUserStore = new class {
}
importKey(armoredKey) {
window.openpgp && openpgp.readKey({armoredKey:armoredKey}).then(key => {
if (!key.err) {
key = new OpenPgpKeyModel(armoredKey, key);
const isPrivate = key.key.isPrivate(),
keys = isPrivate ? this.privateKeys : this.publicKeys;
if (!keys.find(entry => entry.fingerprint == key.fingerprint)) {
keys.push(key);
keys(sort(keys));
storeOpenPgpKeys(keys, isPrivate ? privateKeysItem : publicKeysItem);
this.importKeys([armoredKey]);
}
async importKeys(keys) {
if (window.openpgp) {
const privateKeys = this.privateKeys(),
publicKeys = this.publicKeys();
for (const armoredKey of keys) try {
let key = await openpgp.readKey({armoredKey:armoredKey});
if (!key.err) {
key = new OpenPgpKeyModel(armoredKey, key);
const keys = key.key.isPrivate() ? privateKeys : publicKeys;
keys.find(entry => entry.fingerprint == key.fingerprint)
|| keys.push(key);
}
} catch (e) {
console.error(e, armoredKey);
}
});
this.privateKeys(sort(privateKeys));
this.publicKeys(sort(publicKeys));
storeOpenPgpKeys(privateKeys, privateKeysItem);
storeOpenPgpKeys(publicKeys, publicKeysItem);
}
}
/**
@ -180,7 +201,7 @@ export const OpenPGPUserStore = new class {
hasPublicKeyForEmails(recipients) {
const count = recipients.length,
length = count ? recipients.filter(email =>
this.publicKeys().find(key => key.emails.includes(email))
this.publicKeys().find(key => key.for(email))
).length : 0;
return length && length === count;
}
@ -229,8 +250,8 @@ export const OpenPGPUserStore = new class {
* https://docs.openpgpjs.org/#sign-and-verify-cleartext-messages
*/
async verify(message) {
const data = message.pgpSigned(), // { bodyPartId: "1", sigPartId: "2", micAlg: "pgp-sha256" }
publicKey = this.publicKeys().find(key => key.emails.includes(message.from[0].email));
const data = message.pgpSigned(), // { partId: "1", sigPartId: "2", micAlg: "pgp-sha256" }
publicKey = this.publicKeys().find(key => key.for(message.from[0].email));
if (data && publicKey) {
data.folder = message.folder;
data.uid = message.uid;
@ -289,7 +310,7 @@ export const OpenPGPUserStore = new class {
*/
async encrypt(text, recipients, signPrivateKey) {
const count = recipients.length;
recipients = recipients.map(email => this.publicKeys().find(key => key.emails.includes(email))).filter(key => key);
recipients = recipients.map(email => this.publicKeys().find(key => key.for(email))).filter(key => key);
if (count === recipients.length) {
if (signPrivateKey) {
signPrivateKey = await decryptKey(signPrivateKey);

View file

@ -108,19 +108,14 @@ export const
}
async verify(message) {
const signed = message.pgpSigned();
const signed = message.pgpSigned(),
sender = message.from[0].email;
if (signed) {
const sender = message.from[0].email,
gnupg = GnuPGUserStore.hasPublicKeyForEmails([sender]),
openpgp = OpenPGPUserStore.hasPublicKeyForEmails([sender]);
// Detached signature use GnuPG first, else we must download whole message
if (gnupg && signed.sigPartId) {
return GnuPGUserStore.verify(message);
}
if (openpgp) {
// OpenPGP only when inline, else we must download the whole message
if (!signed.sigPartId && OpenPGPUserStore.hasPublicKeyForEmails([sender])) {
return OpenPGPUserStore.verify(message);
}
if (gnupg) {
if (GnuPGUserStore.hasPublicKeyForEmails([sender])) {
return GnuPGUserStore.verify(message);
}
// Mailvelope can't
@ -133,12 +128,12 @@ export const
let result = {};
recipients.forEach(email => {
OpenPGPUserStore.publicKeys().forEach(key => {
if (key.emails.includes(email)) {
if (key.for(email)) {
result[email] = key.armor;
}
});
GnuPGUserStore.publicKeys.map(async key => {
if (!result[email] && key.emails.includes(email)) {
if (!result[email] && key.for(email)) {
result[email] = await key.fetch();
}
});

View file

@ -331,6 +331,11 @@
padding: 10px;
position: relative;
&.swapColors {
background-color: var(--main-color, currentcolor);
color: var(--main-bg-color);
}
* {
box-sizing: unset;
/* unicode-bidi: plaintext;*/

View file

@ -9,7 +9,7 @@
color: red;
content: attr(data-rainloopErrorTip);
font-size: 90%;
left: 100%;
left: 0;
opacity: 1;
padding: 0.25em;
position : absolute;

View file

@ -123,4 +123,8 @@ html[dir="rtl"] {
border-right: 6px solid #eee;
}
}
[data-rainloopErrorTip]::before {
left: 100%;
}
}

View file

@ -85,29 +85,3 @@ export class AskPopupView extends AbstractViewPopup {
});
}
}
AskPopupView.password = function(sAskDesc, btnText) {
return new Promise(resolve => {
this.showModal([
sAskDesc,
view => resolve({password:view.passphrase(), remember:view.remember()}),
() => resolve(null),
true,
5,
btnText
]);
});
}
AskPopupView.credentials = function(sAskDesc, btnText) {
return new Promise(resolve => {
this.showModal([
sAskDesc,
view => resolve({username:view.username(), password:view.passphrase(), remember:view.remember()}),
() => resolve(null),
true,
3,
btnText
]);
});
}

View file

@ -1377,7 +1377,7 @@ export class ComposePopupView extends AbstractViewPopup {
key && options.push(['OpenPGP', key]);
key = GnuPGUserStore.getPrivateKeyFor(email, 1);
key && options.push(['GnuPG', key]);
identity.smimeKey() && identity.smimeCertificate() && identity.email() === email
identity.smimeKeyValid() && identity.smimeCertificateValid() && identity.email() === email
&& options.push(['S/MIME']);
console.dir({signOptions: options});
this.signOptions(options);
@ -1561,12 +1561,13 @@ export class ComposePopupView extends AbstractViewPopup {
}
if ('S/MIME' == signOptions[i][0]) {
// TODO: sign in PHP fails
params.signCertificate = identity.smimeCertificate();
params.signPrivateKey = identity.smimeKey();
params.sign = 'S/MIME';
// params.signCertificate = identity.smimeCertificate();
// params.signPrivateKey = identity.smimeKey();
if (identity.smimeKeyEncrypted()) {
const pass = await Passphrases.ask(identity,
i18n('SMIME/PRIVATE_KEY_OF', {EMAIL: identity.email()}),
'CRYPTO/DECRYPT'
'CRYPTO/SIGN'
);
if (null != pass) {
params.signPassphrase = pass.password;

View file

@ -26,6 +26,7 @@ const
imapType: 0,
imapTimeout: 300,
imapShortLogin: false,
imapLowerLogin: true,
// SSL
imapSslVerify_peer: false,
imapSslAllow_self_signed: false,
@ -50,6 +51,7 @@ const
smtpType: 0,
smtpTimeout: 60,
smtpShortLogin: false,
smtpLowerLogin: true,
smtpUseAuth: true,
smtpSetSender: false,
smtpAuthPlainLine: false,
@ -69,6 +71,7 @@ const
secure: pInt(oDomain.imapType()),
timeout: oDomain.imapTimeout,
shortLogin: !!oDomain.imapShortLogin(),
lowerLogin: !!oDomain.imapLowerLogin(),
ssl: {
verify_peer: !!oDomain.imapSslVerify_peer(),
verify_peer_name: !!oDomain.imapSslVerify_peer(),
@ -92,6 +95,7 @@ const
secure: pInt(oDomain.smtpType()),
timeout: oDomain.smtpTimeout,
shortLogin: !!oDomain.smtpShortLogin(),
lowerLogin: !!oDomain.smtpLowerLogin(),
ssl: {
verify_peer: !!oDomain.smtpSslVerify_peer(),
verify_peer_name: !!oDomain.smtpSslVerify_peer(),
@ -109,6 +113,7 @@ const
secure: pInt(oDomain.sieveType()),
timeout: oDomain.sieveTimeout,
shortLogin: !!oDomain.imapShortLogin(),
lowerLogin: !!oDomain.imapLowerLogin(),
ssl: {
verify_peer: !!oDomain.imapSslVerify_peer(),
verify_peer_name: !!oDomain.imapSslVerify_peer(),
@ -365,6 +370,8 @@ export class DomainPopupView extends AbstractViewPopup {
this[key]?.(value);
}
});
this.name(IDN.toUnicode(this.name()));
this.aliasName(IDN.toUnicode(this.aliasName()));
this.imapCapabilities(this.imapCapabilities.concat(this.imapDisabled_capabilities()).unique());
this.enableSmartPorts(true);
}

View file

@ -3,8 +3,12 @@ import { addObservablesTo, koComputable } from 'External/ko';
import Remote from 'Remote/User/Fetch';
import { FolderUserStore } from 'Stores/User/Folder';
import { getFolderFromCacheList, setFolder, removeFolderFromCacheList } from 'Common/Cache';
import { Notifications } from 'Common/Enums';
import { FolderMetadataKeys } from 'Common/EnumsUser';
import { folderListOptionsBuilder, sortFolders } from 'Common/Folders';
import { initOnStartOrLangChange, i18n, getNotification } from 'Common/Translator';
import { defaultOptionsAfterRender } from 'Common/Utils';
import { folderListOptionsBuilder } from 'Common/Folders';
export class FolderPopupView extends AbstractViewPopup {
constructor() {
@ -28,6 +32,24 @@ export class FolderPopupView extends AbstractViewPopup {
)
);
this.displaySpecSetting = FolderUserStore.displaySpecSetting;
this.showKolab = FolderUserStore.allowKolab();
this.kolabTypeOptions = ko.observableArray();
let i18nFilter = key => i18n('SETTINGS_FOLDERS/TYPE_' + key);
initOnStartOrLangChange(()=>{
this.kolabTypeOptions([
{ id: '', name: '' },
{ id: 'event', name: i18nFilter('CALENDAR') },
{ id: 'contact', name: i18nFilter('CONTACTS') },
{ id: 'task', name: i18nFilter('TASKS') },
{ id: 'note', name: i18nFilter('NOTES') },
{ id: 'file', name: i18nFilter('FILES') },
{ id: 'journal', name: i18nFilter('JOURNAL') },
{ id: 'configuration', name: i18nFilter('CONFIGURATION') }
]);
});
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
}
@ -35,9 +57,81 @@ export class FolderPopupView extends AbstractViewPopup {
this.editing(false);
}
submitForm(form) {
this.folder().rename(this.name(), this.parentFolder());
console.dir({form});
submitForm(/*form*/) {
const
folder = this.folder(),
nameToEdit = this.name().trim(),
newParentName = this.parentFolder(),
oldParent = getFolderFromCacheList(folder.parentName),
newParent = getFolderFromCacheList(newParentName),
folderList = FolderUserStore.folderList,
newFolderList = newParent ? newParent.subFolders : folderList,
delimiter = (newParent || folder).delimiter,
oldFullname = folder.fullName,
newFullname = (newParent ? newParentName + delimiter : '') + nameToEdit;
if (nameToEdit && newFullname != oldFullname) {
Remote.abort('Folders').post('FolderRename', FolderUserStore.foldersRenaming, {
oldName: oldFullname,
newName: newFullname,
// toggleFolderSubscription / FolderSubscribe
subscribe: folder.isSubscribed() ? 1 : 0,
// toggleFolderCheckable / FolderCheckable
checkable: folder.checkable() ? 1 : 0,
// toggleFolderKolabType / FolderSetMetadata
kolab: {
// TODO: append '.default' ?
type: FolderMetadataKeys.KolabFolderType,
value: folder.kolabType()
}
})
.then(() => {
const
renameFolder = (folder, parent) => {
removeFolderFromCacheList(folder.fullName);
folder.parentName = (parent ? parent.fullName : '');
folder.fullName = (parent ? parent.fullName + delimiter : '') + folder.name();
folder.delimiter = delimiter;
folder.deep = (parent ? parent.deep : -1) + 1;
setFolder(folder);
},
renameChildren = folder => {
folder.subFolders.forEach(child => {
renameFolder(child, folder);
renameChildren(child);
})
};
folder.name(nameToEdit);
renameFolder(folder, newParent);
if (folder.subFolders.length || newParent != oldParent) {
// Rename all subfolders to prevent reload
renameChildren(folder);
}
(oldParent ? oldParent.subFolders : folderList).remove(folder);
newFolderList.push(folder);
sortFolders(newFolderList);
})
.catch(error => {
console.error(error);
FolderUserStore.error(
getNotification(error.code, '', Notifications.CantRenameFolder) + '.\n' + error.message
);
});
} else {
Remote.request('FolderSettings', null, {
folder: folder.fullName,
// toggleFolderSubscription / FolderSubscribe
subscribe: folder.isSubscribed() ? 1 : 0,
// toggleFolderCheckable / FolderCheckable
checkable: folder.checkable() ? 1 : 0,
// toggleFolderKolabType / FolderSetMetadata
kolab: {
// TODO: append '.default' ?
type: FolderMetadataKeys.KolabFolderType,
value: folder.kolabType()
}
});
}
this.close();
}

View file

@ -43,7 +43,7 @@ export class OpenPgpGeneratePopupView extends AbstractViewPopup {
const type = this.keyType().toLowerCase(),
userId = {
name: this.name(),
email: this.email()
email: IDN.toASCII(this.email())
},
cfg = {
type: type,

View file

@ -120,7 +120,7 @@ export class LoginUserView extends AbstractViewLogin {
iError = Notifications.AuthError;
}
this.submitError(getNotification(iError, oData?.ErrorMessage,
Notifications.UnknownNotification));
Notifications.UnknownError));
this.submitErrorAdditional(oData?.ErrorMessageAdditional);
} else {
rl.setData(oData.Result);
@ -138,25 +138,20 @@ export class LoginUserView extends AbstractViewLogin {
onBuild(dom) {
super.onBuild(dom);
const signMe = (SettingsGet('signMe') || '').toLowerCase();
let signMe = SettingsGet('signMe');
switch (signMe) {
case 'defaultoff':
case 'defaulton':
this.signMeType(
'defaulton' === signMe ? SignMeOn : SignMeOff
);
case SignMeOff:
case SignMeOn:
switch (Local.get(ClientSideKeyNameLastSignMe)) {
case '-1-':
this.signMeType(SignMeOn);
signMe = SignMeOn;
break;
case '-0-':
this.signMeType(SignMeOff);
signMe = SignMeOff;
break;
// no default
}
this.signMeType(signMe);
break;
default:
this.signMeType(SignMeUnused);

View file

@ -146,7 +146,7 @@ export class MailMessageView extends AbstractViewRight {
downloadAsZipAllowed: () => this.attachmentsActions.includes('zip')
&& (currentMessage()?.attachments || [])
.filter(item => item?.download /*&& !item?.isLinked()*/ && item?.checked())
.filter(item => item?.checked() && item?.download /*&& !item?.isLinked()*/)
.length,
tagsAllowed: () => FolderUserStore.currentFolder()?.tagsAllowed(),
@ -570,29 +570,31 @@ export class MailMessageView extends AbstractViewRight {
}
pgpDecrypt() {
const oMessage = currentMessage();
const oMessage = currentMessage(),
data = oMessage.pgpEncrypted();
delete data.error;
PgpUserStore.decrypt(oMessage).then(result => {
if (result) {
oMessage.pgpDecrypted(true);
if (result.data) {
MimeToMessage(result.data, oMessage);
oMessage.html() ? oMessage.viewHtml() : oMessage.viewPlain();
if (result.signatures?.length) {
oMessage.pgpSigned(true);
oMessage.pgpVerified({
signatures: result.signatures,
success: !!result.signatures.length
});
}
}
} else {
if (!result) {
// TODO: translate
throw 'Decryption failed, canceled or not possible';
throw Error('Decryption failed, canceled or not possible');
}
oMessage.pgpDecrypted(true);
if (result.data) {
MimeToMessage(result.data, oMessage);
oMessage.html() ? oMessage.viewHtml() : oMessage.viewPlain();
if (result.signatures?.length) {
oMessage.pgpSigned({
signatures: result.signatures,
success: !!result.signatures.length
});
}
}
})
.catch(e => {
console.error(e)
alert(e.message);
data.error = e.message;
})
.finally(() => {
oMessage.pgpEncrypted(data);
});
}
@ -600,7 +602,7 @@ export class MailMessageView extends AbstractViewRight {
const oMessage = currentMessage()/*, ctrl = event.target.closest('.openpgp-control')*/;
PgpUserStore.verify(oMessage).then(result => {
if (result) {
oMessage.pgpVerified(result);
oMessage.pgpSigned(result);
} else {
alert('Verification failed or no valid public key found');
}
@ -627,16 +629,17 @@ export class MailMessageView extends AbstractViewRight {
async smimeDecrypt() {
const message = currentMessage();
let pass, data = message.smimeEncrypted(); // { partId: "1" }
const addresses = message.from.concat(message.to, message.cc, message.bcc).map(item => item.email),
identity = IdentityUserStore.find(item => addresses.includes(item.email()));
identity = IdentityUserStore.find(item => addresses.includes(item.email())),
data = message.smimeEncrypted(); // { partId: "1" }
if (data && identity) {
data = { ...data }; // clone
data.folder = message.folder;
data.uid = message.uid;
// data.bodyPart = data.bodyPart?.raw;
data.certificate = identity.smimeCertificate();
data.privateKey = identity.smimeKey();
delete data.error;
let pass, params = { ...data }; // clone
params.folder = message.folder;
params.uid = message.uid;
// params.bodyPart = params.bodyPart?.raw;
params.certificate = identity.smimeCertificate();
params.privateKey = identity.smimeKey();
if (identity.smimeKeyEncrypted()) {
pass = await Passphrases.ask(identity,
i18n('SMIME/PRIVATE_KEY_OF', {EMAIL: identity.email()}),
@ -645,36 +648,44 @@ export class MailMessageView extends AbstractViewRight {
if (!pass) {
return;
}
data.passphrase = pass?.password;
params.passphrase = pass?.password;
}
Remote.post('SMimeDecryptMessage', null, data).then(response => {
if (response?.Result) {
Remote.post('SMimeDecryptMessage', null, params).then(response => {
if (response?.Result?.data) {
message.smimeDecrypted(true);
MimeToMessage(response.Result, message);
MimeToMessage(response.Result.data, message);
message.html() ? message.viewHtml() : message.viewPlain();
pass && pass.remember && Passphrases.set(identity, pass.password);
if ('signed' in response.Result) {
message.smimeSigned(response.Result.signed);
}
}
}).catch(e => {
data.error = e.message
})
.finally(() => {
message.smimeEncrypted(data);
});
}
}
smimeVerify(/*self, event*/) {
const message = currentMessage();
let data = message.smimeSigned(); // { partId: "1", micAlg: "pgp-sha256" }
const message = currentMessage(),
data = message.smimeSigned(); // { partId: "1", micAlg: "pgp-sha256" }
if (data) {
data = { ...data }; // clone
data.folder = message.folder;
data.uid = message.uid;
data.bodyPart = data.bodyPart?.raw;
data.sigPart = data.sigPart?.bodyRaw;
Remote.post('SMimeVerifyMessage', null, data).then(response => {
const params = { ...data }; // clone
params.folder = message.folder;
params.uid = message.uid;
params.bodyPart = data.bodyPart?.raw;
params.sigPart = data.sigPart?.bodyRaw;
Remote.post('SMimeVerifyMessage', null, params).then(response => {
if (response?.Result) {
if (response.Result.body) {
MimeToMessage(response.Result.body, message);
message.html() ? message.viewHtml() : message.viewPlain();
response.Result.body = null;
}
message.smimeVerified(response.Result.success);
data.success = response.Result.success;
message.smimeSigned(data);
}
});
}

View file

@ -87,9 +87,8 @@ export class SystemDropDownUserView extends AbstractViewRight {
}
accountName() {
let email = AccountUserStore.email(),
account = AccountUserStore.find(account => account.email == email);
return account?.name || email;
const email = AccountUserStore.email();
return AccountUserStore.find(account => account.email == email)?.label() || IDN.toUnicode(email);
}
settingsClick() {

View file

@ -3,7 +3,7 @@
const
qUri = path => doc.location.pathname.replace(/\/+$/,'') + '/?/' + path,
eId = id => doc.getElementById('rl-'+id),
admin = '1' == eId('app')?.dataset?.admin,
admin = '1' == eId('app').dataset.admin,
toggle = div => {
eId('loading').hidden = true;
@ -69,7 +69,7 @@ window.rl = {
headers: {}
}, init);
let asJSON = 1,
XToken = RL_APP_DATA.System?.token,
XToken = (RL_APP_DATA.System || {}).token,
object = {};
if (postData) {
init.method = 'POST';

View file

@ -0,0 +1,106 @@
-----BEGIN PGP PRIVATE KEY BLOCK-----
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
YXYPCv6njzAv1+fTgemAZYyomQ1qhVg=
=a9os
-----END PGP PRIVATE KEY BLOCK-----

View file

@ -0,0 +1,50 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=0ljX
-----END PGP PUBLIC KEY BLOCK-----

View file

@ -0,0 +1,16 @@
-----BEGIN PGP PRIVATE KEY BLOCK-----
lIYEYfQDqBYJKwYBBAHaRw8BAQdAy+llquGb/U4M0kD2xyJoQ2pwlDN02C7XD906
7I8zdB3+BwMC7y2sGFhuzTHldCm0gfdOH3H2PkmhDfM/OSTYGVEu1mKInC3xmp2F
HmC2wUd+yM+hzMrQMgHn61iT9CHDeunUhQqG8PoXfAUAL7syGiXQ0rQZZGVtbyA8
ZGVtb0BzbmFwcHltYWlsLmV1PoiMBBAWCgAdBQJh9AOoBAsJBwgDFQgKBBYAAgEC
GQECGwMCHgEAIQkQXzpc3AmtiuMWIQQsIj8g6irbTLaPgdlfOlzcCa2K4/nPAP4u
UrWr39wv+YKsNcLwHwOpljyu59iHOXA3halUbVCeJwD/dY6JXCwMDgG+BmurPcJh
S/S8Q6fjlN9hUi/za3acYASciwRh9AOoEgorBgEEAZdVAQUBAQdAvXl+RCkqtUqN
VQ3Fj3bFTZjZOeNlI3ibK2eN6EjlnwcDAQgH/gcDAjAzqrbZEDg05XxnZqG/vHCg
1bJ/OofU+0oYZy6DHhAVDa5IhNLkC7kDeA8gXtQnNVuOd5kSIWvLcVRMZZval8iI
Xfrt037sAAciOIft1uSIeAQYFggACQUCYfQDqAIbDAAhCRBfOlzcCa2K4xYhBCwi
PyDqKttMto+B2V86XNwJrYrjolQBAMqdz8VkgMYjM7tinwUUTe4JjZoCsuhHPN6S
pQLd/UzKAQDgRlbAdrl042/nJcdrBrQz3+wVzkaF0ehvihBf4/tfDw==
=5A1c
-----END PGP PRIVATE KEY BLOCK-----

View file

@ -0,0 +1,13 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEYfQDqBYJKwYBBAHaRw8BAQdAy+llquGb/U4M0kD2xyJoQ2pwlDN02C7XD906
7I8zdB20GWRlbW8gPGRlbW9Ac25hcHB5bWFpbC5ldT6IjAQQFgoAHQUCYfQDqAQL
CQcIAxUICgQWAAIBAhkBAhsDAh4BACEJEF86XNwJrYrjFiEELCI/IOoq20y2j4HZ
Xzpc3AmtiuP5zwD+LlK1q9/cL/mCrDXC8B8DqZY8rufYhzlwN4WpVG1QnicA/3WO
iVwsDA4BvgZrqz3CYUv0vEOn45TfYVIv82t2nGAEuDgEYfQDqBIKKwYBBAGXVQEF
AQEHQL15fkQpKrVKjVUNxY92xU2Y2TnjZSN4mytnjehI5Z8HAwEIB4h4BBgWCAAJ
BQJh9AOoAhsMACEJEF86XNwJrYrjFiEELCI/IOoq20y2j4HZXzpc3AmtiuOiVAEA
yp3PxWSAxiMzu2KfBRRN7gmNmgKy6Ec83pKlAt39TMoBAOBGVsB2uXTjb+clx2sG
tDPf7BXORoXR6G+KEF/j+18P
=fymh
-----END PGP PUBLIC KEY BLOCK-----

View file

@ -0,0 +1,18 @@
-----BEGIN PGP PRIVATE KEY BLOCK-----
xYYEZfCT4RYJKwYBBAHaRw8BAQdABPuO2YQhhYx3HV4l8NGfC7TAUICCXYPB
B2YAgTIDR1j+CQMIGrTjdLMkuHvgN0hXM6160YQj51KfsJrY2uWtwTtjiJa7
CgyvXj8krDEGrtdTWL0aGGZN+LpMbieyVPstLjycdxEMx0VwT5VOaMZX68As
vs0vU25hcHB5TWFpbCBEZW1vMyA8ZGVtbzNAeG4tLWR1OGguc25hcHB5bWFp
bC5ldT7CjAQQFgoAPgUCZfCT4QQLCQcICRCgtCjhJALYdQMVCAoEFgACAQIZ
AQIbAwIeARYhBA2rP+d/WzbDEuhm0aC0KOEkAth1AAA7fwEA2spNcx/tdZz8
yBxsTbaOzWXrPa2RnU24QWI3nKiH65IA/j2UV2O6QwoB9PVoLLhqdsP0w7HH
msAcOY47Kt/rkqIHx4sEZfCT4RIKKwYBBAGXVQEFAQEHQB7lSl1px2GgNclu
DDFneXLe+ZjQAY6OvK23ti0zaCsFAwEIB/4JAwiaLdYHtWW/YOCOgzSJiwKU
4zmfRHIkkbCVyJO19Fpc4DwLx8dMKyPBQbLT0X7JI6SnR9qYPm35pJj2Cbrl
GNk3bImoPkcLVrwkrbUo+33gwngEGBYIACoFAmXwk+EJEKC0KOEkAth1AhsM
FiEEDas/539bNsMS6GbRoLQo4SQC2HUAAD+5APwO89z/dHpWj2GWRoLIbGsM
0zJcy7rldxp7FD+joiMOTQD/TnMrX8vwJCG/RMKzEVY02EnTScACrCzWIoLl
SxgrqQo=
=SBKt
-----END PGP PRIVATE KEY BLOCK-----

View file

@ -0,0 +1,14 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
xjMEZfCT4RYJKwYBBAHaRw8BAQdABPuO2YQhhYx3HV4l8NGfC7TAUICCXYPB
B2YAgTIDR1jNL1NuYXBweU1haWwgRGVtbzMgPGRlbW8zQHhuLS1kdThoLnNu
YXBweW1haWwuZXU+wowEEBYKAD4FAmXwk+EECwkHCAkQoLQo4SQC2HUDFQgK
BBYAAgECGQECGwMCHgEWIQQNqz/nf1s2wxLoZtGgtCjhJALYdQAAO38BANrK
TXMf7XWc/MgcbE22js1l6z2tkZ1NuEFiN5yoh+uSAP49lFdjukMKAfT1aCy4
anbD9MOxx5rAHDmOOyrf65KiB844BGXwk+ESCisGAQQBl1UBBQEBB0Ae5Upd
acdhoDXJbgwxZ3ly3vmY0AGOjrytt7YtM2grBQMBCAfCeAQYFggAKgUCZfCT
4QkQoLQo4SQC2HUCGwwWIQQNqz/nf1s2wxLoZtGgtCjhJALYdQAAP7kA/A7z
3P90elaPYZZGgshsawzTMlzLuuV3GnsUP6OiIw5NAP9Ocytfy/AkIb9EwrMR
VjTYSdNJwAKsLNYiguVLGCupCg==
=FuIG
-----END PGP PUBLIC KEY BLOCK-----

34
examples/crypto/smime.crt Normal file
View file

@ -0,0 +1,34 @@
-----BEGIN CERTIFICATE-----
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
r7HREQ/4hof+B0bTZCma/l0n
-----END CERTIFICATE-----

54
examples/crypto/smime.key Normal file
View file

@ -0,0 +1,54 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
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
/ZYVVu8GG0wUcrvQizogAg==
-----END ENCRYPTED PRIVATE KEY-----

1745
examples/eml/tnef.eml Normal file

File diff suppressed because it is too large Load diff

101
examples/eml/tnef2.eml Normal file
View file

@ -0,0 +1,101 @@
From: Alice <alice@example.net>
To: Bob <bob@example.net>
Subject: TNEF test 2
Date: Thu, 10 Feb 2022 12:19:57 +0100
Message-ID: <001d01d81e70$23211250$696336f0$@example.net>
MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary="----=_NextPart_000_001E_01D81E78.84E7EB50"
X-Mailer: Microsoft Outlook 16.0
Content-Language: cs
X-MS-TNEF-Correlator: 0000000045EA84AB25A1494B8CD618FC27E0940E04CA7500
Authentication-Results: example.net;
dkim=pass header.d=example.net header.s=mail header.b=UH8A3Z8P;
dmarc=pass (policy=reject) header.from=example.net;
spf=pass () smtp.mailfrom=example@example.net
This is a multipart message in MIME format.
------=_NextPart_000_001E_01D81E78.84E7EB50
Content-Type: text/plain;
charset="iso-8859-2"
Content-Transfer-Encoding: quoted-printable
Dobr=FD den,
=20
pos=EDl=E1m zku=B9ebn=ED e-mail:
=20
P=F8=EDli=B9 =BElu=BBou=E8k=FD k=F9=F2 =FAp=ECl =EF=E1belsk=E9 =F3dy
=20
S pozdravem
=20
Mgr. Milo=B9 Bart=E1k
------=_NextPart_000_001E_01D81E78.84E7EB50
Content-Type: application/ms-tnef;
name="winmail.dat"
Content-Transfer-Encoding: base64
Content-Disposition: attachment;
filename="winmail.dat"
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
------=_NextPart_000_001E_01D81E78.84E7EB50--

View file

@ -1,4 +1,4 @@
This app packages SnappyMail <upstream>2.35.2</upstream>.
This app packages SnappyMail <upstream>2.35.4</upstream>.
SnappyMail is a simple, modern, lightweight & fast web-based email client.

View file

@ -4,7 +4,7 @@ RUN mkdir -p /app/code
WORKDIR /app/code
# If you change the extraction below, be sure to test on scaleway
VERSION=2.35.2
VERSION=2.35.4
RUN wget https://github.com/the-djmaze/snappymail/releases/download/v${VERSION}/snappymail-${VERSION}.zip -O /tmp/snappymail.zip && \
unzip /tmp/snappymail.zip -d /app/code && \
rm /tmp/snappymail.zip && \

View file

@ -3,10 +3,10 @@
<id>snappymail</id>
<name>SnappyMail</name>
<summary>SnappyMail Webmail</summary>
<version>2.35.2</version>
<version>2.35.4</version>
<licence>agpl</licence>
<author>SnappyMail, RainLoop Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli</author>
<description><![CDATA[**Simple, modern, lightweight & fast web-based email client.**
<description><![CDATA[**Lightweight & fast email client.**
- **Dark mode**
- **Responsive design**
@ -16,6 +16,7 @@
- **Integration with other Nextcloud apps** (Contacts, Files and Calendar)
- **Multiple mail accounts and identities**
- **Send & receive OpenPGP encrypted/signed emails** (With full HTML support, ECC keys, or plain text with Mailvelope)
- **Send & receive S/MIME encrypted/signed emails**
- **Many security features** (Sodium encrypted passwords, Sec-Fetch, TOTP 2FA, DKIM, prevent tracking, etc.)
- **Kolab integrations**
@ -63,7 +64,7 @@ There, click on the link to go to the SnappyMail admin panel.
<lib>xxtea</lib>
<lib>zip</lib>
-->
<nextcloud min-version="20" max-version="28" />
<nextcloud min-version="20" max-version="29" />
</dependencies>
<settings>
<admin>OCA\SnappyMail\Settings\AdminSettings</admin>

View file

@ -75,20 +75,24 @@ class Application extends App implements IBootstrap
$dispatcher = $context->getAppContainer()->query('OCP\EventDispatcher\IEventDispatcher');
$dispatcher->addListener(PostLoginEvent::class, function (PostLoginEvent $Event) {
/*
$config = \OC::$server->getConfig();
// Only store the user's password in the current session if they have
// enabled auto-login using Nextcloud username or email address.
if ($config->getAppValue('snappymail', 'snappymail-autologin', false)
|| $config->getAppValue('snappymail', 'snappymail-autologin-with-email', false)) {
*/
$sUID = $Event->getUser()->getUID();
\OC::$server->getSession()['snappymail-nc-uid'] = $sUID;
\OC::$server->getSession()['snappymail-password'] = SnappyMailHelper::encodePassword($Event->getPassword(), $sUID);
\OC::$server->getSession()['snappymail-passphrase'] = SnappyMailHelper::encodePassword($Event->getPassword(), $sUID);
/*
}
*/
});
$dispatcher->addListener(BeforeUserLoggedOutEvent::class, function (BeforeUserLoggedOutEvent $Event) {
// https://github.com/nextcloud/server/issues/36083#issuecomment-1387370634
// \OC::$server->getSession()['snappymail-password'] = '';
// \OC::$server->getSession()['snappymail-passphrase'] = '';
SnappyMailHelper::loadApp();
// \RainLoop\Api::Actions()->Logout(true);
\RainLoop\Api::Actions()->DoLogout();
@ -99,12 +103,12 @@ class Application extends App implements IBootstrap
$class = 'OCA\Impersonate\Events\BeginImpersonateEvent';
if (\class_exists($class)) {
$dispatcher->addListener($class, function ($Event) {
\OC::$server->getSession()['snappymail-password'] = '';
\OC::$server->getSession()['snappymail-passphrase'] = '';
SnappyMailHelper::loadApp();
\RainLoop\Api::Actions()->Logout(true);
});
$dispatcher->addListener('OCA\Impersonate\Events\EndImpersonateEvent', function ($Event) {
\OC::$server->getSession()['snappymail-password'] = '';
\OC::$server->getSession()['snappymail-passphrase'] = '';
SnappyMailHelper::loadApp();
\RainLoop\Api::Actions()->Logout(true);
});

View file

@ -103,7 +103,7 @@ class FetchController extends Controller {
$sPass = $_POST['snappymail-password'];
if ('******' !== $sPass) {
$this->config->setUserValue($sUser, 'snappymail', 'snappymail-password',
$this->config->setUserValue($sUser, 'snappymail', 'passphrase',
$sPass ? SnappyMailHelper::encodePassword($sPass, \md5($sEmail)) : '');
}
} else {

View file

@ -85,11 +85,15 @@ class InstallStep implements IRepairStep
// $oDomain = \RainLoop\Model\Domain::fromIniArray('nextcloud', []);
$oDomain = new \RainLoop\Model\Domain('nextcloud');
$iSecurityType = \MailSo\Net\Enumerations\ConnectionSecurityType::NONE;
$oDomain->SetConfig(
'localhost', 143, $iSecurityType, true,
true, 'localhost', 4190, $iSecurityType,
'localhost', 25, $iSecurityType, true, true, false, false,
'');
$oDomain->ImapSettings()->host = 'localhost';
$oDomain->ImapSettings()->type = $iSecurityType;
$oDomain->ImapSettings()->shortLogin = true;
$oDomain->SieveSettings()->enabled = true;
$oDomain->SieveSettings()->host = 'localhost';
$oDomain->SieveSettings()->type = $iSecurityType;
$oDomain->SmtpSettings()->host = 'localhost';
$oDomain->SmtpSettings()->type = $iSecurityType;
$oDomain->SmtpSettings()->shortLogin = true;
$oProvider->Save($oDomain);
if (!$oConfig->Get('login', 'default_domain', '')) {
$oConfig->Set('login', 'default_domain', 'nextcloud');

View file

@ -24,15 +24,19 @@ class PersonalSettings implements ISettings
$sEmail = $aRainLoop[0];
$this->config->setUserValue($uid, 'snappymail', 'snappymail-email', $sEmail);
if ($aRainLoop[1]) {
$this->config->setUserValue($uid, 'snappymail', 'snappymail-password',
$this->config->setUserValue($uid, 'snappymail', 'passphrase',
\OCA\SnappyMail\Util\SnappyMailHelper::encodePassword($aRainLoop[1], \md5($sEmail))
);
}
}
}
if ($sPass = $this->config->getUserValue($uid, 'snappymail', 'snappymail-password')) {
$this->config->deleteUserValue($uid, 'snappymail', 'snappymail-password');
$this->config->setUserValue($uid, 'snappymail', 'passphrase', $sPass);
}
$parameters = [
'snappymail-email' => $sEmail,
'snappymail-password' => $this->config->getUserValue($uid, 'snappymail', 'snappymail-password') ? '******' : ''
'snappymail-password' => $this->config->getUserValue($uid, 'snappymail', 'passphrase') ? '******' : ''
];
\OCP\Util::addScript('snappymail', 'snappymail');
return new TemplateResponse('snappymail', 'personal_settings', $parameters, '');

View file

@ -4,15 +4,15 @@ namespace OCA\SnappyMail\Util;
class SnappyMailResponse extends \OCP\AppFramework\Http\Response
{
public function render(): string
{
public function render(): string
{
$data = '';
$i = \ob_get_level();
while ($i--) {
$data .= \ob_get_clean();
}
return $data;
}
}
}
class SnappyMailHelper
@ -98,15 +98,16 @@ class SnappyMailHelper
$oActions->Plugins()->RunHook('login.success', array($oAccount));
$oActions->SetAuthToken($oAccount);
if ($oConfig->Get('login', 'sign_me_auto', \RainLoop\Enumerations\SignMeType::DEFAULT_OFF) === \RainLoop\Enumerations\SignMeType::DEFAULT_ON) {
if ($oConfig->Get('login', 'sign_me_auto', \RainLoop\Enumerations\SignMeType::DefaultOff) === \RainLoop\Enumerations\SignMeType::DefaultOn) {
$oActions->SetSignMeToken($oAccount);
}
}
} catch (\Throwable $e) {
// Login failure, reset password to prevent more attempts
$sUID = \OC::$server->getUserSession()->getUser()->getUID();
\OC::$server->getSession()['snappymail-password'] = '';
\OC::$server->getConfig()->setUserValue($sUID, 'snappymail', 'snappymail-password', '');
\OC::$server->getSession()['snappymail-passphrase'] = '';
\OC::$server->getConfig()->setUserValue($sUID, 'snappymail', 'passphrase', '');
\SnappyMail\Log::error('Nextcloud', $e->getMessage());
}
}
}
@ -132,11 +133,14 @@ class SnappyMailHelper
// If the user has set credentials for SnappyMail in their personal settings,
// this has the first priority.
$sEmail = $config->getUserValue($sUID, 'snappymail', 'snappymail-email');
$sPassword = $config->getUserValue($sUID, 'snappymail', 'snappymail-password');
$sPassword = $config->getUserValue($sUID, 'snappymail', 'passphrase')
?: $config->getUserValue($sUID, 'snappymail', 'snappymail-password');
if ($sEmail && $sPassword) {
$sPassword = static::decodePassword($sPassword, \md5($sEmail));
if ($sPassword) {
return [$sUID, $sEmail, $sPassword];
} else {
\SnappyMail\Log::debug('Nextcloud', 'decodePassword failed for getUserValue');
}
}
@ -165,16 +169,18 @@ class SnappyMailHelper
$sPassword = '';
if ($config->getAppValue('snappymail', 'snappymail-autologin', false)) {
$sEmail = $sUID;
$sPassword = $ocSession['snappymail-password'];
$sPassword = $ocSession['snappymail-passphrase'];
} else if ($config->getAppValue('snappymail', 'snappymail-autologin-with-email', false)) {
$sEmail = $config->getUserValue($sUID, 'settings', 'email');
$sPassword = $ocSession['snappymail-password'];
$sPassword = $ocSession['snappymail-passphrase'];
} else {
\SnappyMail\Log::debug('Nextcloud', 'snappymail-autologin is off');
}
if ($sPassword) {
return [$sUID, $sEmail, static::decodePassword($sPassword, $sUID)];
}
} else {
\SnappyMail\Log::debug('Nextcloud', "snappymail-nc-uid mismatch '{$ocSession['snappymail-nc-uid']}' != '{$sUID}'");
}
return [$sUID, '', ''];
@ -205,6 +211,6 @@ class SnappyMailHelper
{
static::loadApp();
$result = \SnappyMail\Crypt::DecryptUrlSafe($sPassword, $sSalt);
return $result ? new \SnappyMail\SensitiveString($result) : $result;
return $result ? new \SnappyMail\SensitiveString($result) : null;
}
}

View file

@ -40,9 +40,9 @@
<br />
<!-- DISABLED https://github.com/the-djmaze/snappymail/issues/1420#issuecomment-1933045917
<p>
<input id="snappymail-autologin-oidc" name="snappymail-autologin-oidc" type="checkbox" class="checkbox" <?php if ($_['snappymail-autologin-oidc']) echo 'checked="checked"'; ?>>
<input id="snappymail-autologin-oidc" name="snappymail-autologin-oidc" type="checkbox" class="checkbox" <php if ($_['snappymail-autologin-oidc']) echo 'checked="checked"'; ?>>
<label for="snappymail-autologin-oidc">
<?php echo($l->t('Attempt to automatically login with OIDC when active')); ?>
<php echo($l->t('Attempt to automatically login with OIDC when active')); ?>
</label>
</p>
<br />

View file

@ -78,11 +78,15 @@ class SnappyMailHelper
// $oDomain = \RainLoop\Model\Domain::fromIniArray('owncloud', []);
$oDomain = new \RainLoop\Model\Domain('owncloud');
$iSecurityType = \MailSo\Net\Enumerations\ConnectionSecurityType::NONE;
$oDomain->SetConfig(
'localhost', 143, $iSecurityType, true,
true, 'localhost', 4190, $iSecurityType,
'localhost', 25, $iSecurityType, true, true, false, false,
'');
$oDomain->ImapSettings()->host = 'localhost';
$oDomain->ImapSettings()->type = $iSecurityType;
$oDomain->ImapSettings()->shortLogin = true;
$oDomain->SieveSettings()->enabled = true;
$oDomain->SieveSettings()->host = 'localhost';
$oDomain->SieveSettings()->type = $iSecurityType;
$oDomain->SmtpSettings()->host = 'localhost';
$oDomain->SmtpSettings()->type = $iSecurityType;
$oDomain->SmtpSettings()->shortLogin = true;
$oProvider->Save($oDomain);
if (!$oConfig->Get('login', 'default_domain', '')) {
$oConfig->Set('login', 'default_domain', 'owncloud');

View file

@ -20,7 +20,7 @@ return "SnappyMail Webmail is a browser-based multilingual IMAP client with an a
# script_snappymail_versions()
sub script_snappymail_versions
{
return ( "2.35.2" );
return ( "2.35.4" );
}
sub script_snappymail_version_desc

View file

@ -3,7 +3,7 @@
"title": "SnappyMail",
"description": "Simple, modern & fast web-based email client",
"private": true,
"version": "2.35.2",
"version": "2.35.4",
"homepage": "https://snappymail.eu",
"author": {
"name": "DJ Maze",

View file

@ -0,0 +1,13 @@
(() => {
const dom = document.getElementById('MailMessageView').content;
dom.querySelector('.attachmentsControls').dataset.bind = '';
let ds = dom.querySelector('.attachmentsPlace').dataset;
ds.bind = ds.bind.replace('showAttachmentControls', 'true');
ds = dom.querySelector('.controls-handle').dataset;
ds.bind = ds.bind.replace('allowAttachmentControls', 'false');
})();

View file

@ -0,0 +1,20 @@
<?php
class AttachmentsForceOpenPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Attachments force open',
AUTHOR = 'SnappyMail',
URL = 'https://github.com/the-djmaze/snappymail/pull/1489',
VERSION = '0.1',
RELEASE = '2024-03-15',
REQUIRED = '2.14.0',
CATEGORY = 'General',
LICENSE = 'MIT',
DESCRIPTION = '';
public function Init() : void
{
$this->addJs('extension.js'); // add js file
}
}

View file

@ -10,8 +10,8 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
NAME = 'Avatars',
AUTHOR = 'SnappyMail',
URL = 'https://snappymail.eu/',
VERSION = '1.15',
RELEASE = '2024-01-22',
VERSION = '1.16',
RELEASE = '2024-03-12',
REQUIRED = '2.25.0',
CATEGORY = 'Contacts',
LICENSE = 'MIT',
@ -208,7 +208,7 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
return null;
}
$sAsciiEmail = \mb_strtolower(\MailSo\Base\Utils::IdnToAscii($sEmail, true));
$sAsciiEmail = \mb_strtolower(\SnappyMail\IDN::emailToAscii($sEmail));
$sEmailId = \sha1($sAsciiEmail);
\MailSo\Base\Http::setETag($sEmailId);
@ -310,7 +310,7 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
private static function cacheImage(string $sEmail, array $aResult) : void
{
$sEmailId = \sha1(\mb_strtolower(\MailSo\Base\Utils::IdnToAscii($sEmail, true)));
$sEmailId = \sha1(\mb_strtolower(\SnappyMail\IDN::emailToAscii($sEmail)));
if (!\is_dir(\APP_PRIVATE_DATA . 'avatars')) {
\mkdir(\APP_PRIVATE_DATA . 'avatars', 0700);
}
@ -323,7 +323,7 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
private static function getCachedImage(string $sEmail) : ?array
{
$sEmail = \mb_strtolower(\MailSo\Base\Utils::IdnToAscii($sEmail, true));
$sEmail = \mb_strtolower(\SnappyMail\IDN::emailToAscii($sEmail));
$aFiles = \glob(\APP_PRIVATE_DATA . "avatars/{$sEmail}.*");
if ($aFiles) {
\MailSo\Base\Http::setLastModified(\filemtime($aFiles[0]));

View file

@ -4,35 +4,31 @@ class BlackListPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Blacklist',
VERSION = '2.1',
RELEASE = '2021-04-21',
VERSION = '2.2',
RELEASE = '2024-03-04',
REQUIRED = '2.5.0',
CATEGORY = 'Login',
DESCRIPTION = 'Simple blacklist extension (with wildcard and exceptions functionality).';
public function Init() : void
{
$this->addHook('login.credentials', 'FilterLoginCredentials');
$this->addHook('login.credentials.step-1', 'FilterLoginCredentials');
}
/**
* @param string $sEmail
* @param string $sLogin
* @param string $sPassword
*
* @throws \RainLoop\Exceptions\ClientException
*/
public function FilterLoginCredentials(&$sEmail, &$sLogin, &$sPassword)
public function FilterLoginCredentials(string &$sEmail)
{
$sBlackList = \trim($this->Config()->Get('plugin', 'black_list', ''));
if (0 < \strlen($sBlackList) && \RainLoop\Plugins\Helper::ValidateWildcardValues($sEmail, $sBlackList))
{
if (\strlen($sBlackList) && \RainLoop\Plugins\Helper::ValidateWildcardValues($sEmail, $sBlackList)) {
$sExceptions = \trim($this->Config()->Get('plugin', 'exceptions', ''));
if (0 === \strlen($sExceptions) || !\RainLoop\Plugins\Helper::ValidateWildcardValues($sEmail, $sExceptions))
{
if (!\strlen($sExceptions) || !\RainLoop\Plugins\Helper::ValidateWildcardValues($sEmail, $sExceptions)) {
throw new \RainLoop\Exceptions\ClientException(
$this->Config()->Get('plugin', 'auth_error', true) ?
\RainLoop\Notifications::AuthError : \RainLoop\Notifications::AccountNotAllowed);
$this->Config()->Get('plugin', 'auth_error', false)
? \RainLoop\Notifications::AuthError
: \RainLoop\Notifications::AccountNotAllowed
);
}
}
}
@ -46,7 +42,7 @@ class BlackListPlugin extends \RainLoop\Plugins\AbstractPlugin
\RainLoop\Plugins\Property::NewInstance('auth_error')->SetLabel('Auth Error')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDescription('Throw an authentication error instead of an access error.')
->SetDefaultValue(true),
->SetDefaultValue(false),
\RainLoop\Plugins\Property::NewInstance('black_list')->SetLabel('Black List')
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT)
->SetDescription('Emails black list, space as delimiter, wildcard supported.')

View file

@ -1,5 +1,7 @@
<?php
use SnappyMail\SensitiveString;
class ChangePasswordFroxlorDriver
{
const
@ -44,7 +46,7 @@ class ChangePasswordFroxlorDriver
);
}
public function ChangePassword(\RainLoop\Model\Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool
public function ChangePassword(\RainLoop\Model\Account $oAccount, SensitiveString $oPrevPassword, SensitiveString $oNewPassword) : bool
{
if (!\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->oConfig->Get('plugin', 'froxlor_allowed_emails', ''))) {
return false;
@ -72,12 +74,12 @@ class ChangePasswordFroxlorDriver
if (!empty($aFetchResult['id'])) {
$sDbPassword = $aFetchResult['password_enc'];
$sDbSalt = \substr($sDbPassword, 0, \strrpos($sDbPassword, '$'));
if (\crypt($sPrevPassword, $sDbSalt) === $sDbPassword) {
if (\crypt($oPrevPassword, $sDbSalt) === $sDbPassword) {
$oStmt = $oPdo->prepare('UPDATE mail_users SET password_enc = ? WHERE id = ?');
return !!$oStmt->execute(array(
$this->cryptPassword($sNewPassword),
$this->cryptPassword($oNewPassword),
$aFetchResult['id']
));
}
@ -93,7 +95,7 @@ class ChangePasswordFroxlorDriver
return false;
}
private function cryptPassword(string $sPassword) : string
private function cryptPassword(SensitiveString $oPassword) : string
{
if (\defined('CRYPT_SHA512') && CRYPT_SHA512) {
$sSalt = '$6$rounds=5000$' . \bin2hex(\random_bytes(8)) . '$';
@ -102,6 +104,6 @@ class ChangePasswordFroxlorDriver
} else {
$sSalt = '$1$' . \bin2hex(\random_bytes(6)) . '$';
}
return \crypt($sPassword, $sSalt);
return \crypt($oPassword, $sSalt);
}
}

View file

@ -1,15 +1,15 @@
<?php
use \RainLoop\Exceptions\ClientException;
use RainLoop\Exceptions\ClientException;
class ChangePasswordFroxlorPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Change Password Froxlor',
AUTHOR = 'Euphonique',
VERSION = '1.0',
RELEASE = '2022-05-30',
REQUIRED = '2.15.3',
VERSION = '2.36',
RELEASE = '2024-03-17',
REQUIRED = '2.36.0',
CATEGORY = 'Security',
DESCRIPTION = 'Extension to allow users to change their passwords through Froxlor';

View file

@ -1,5 +1,7 @@
<?php
use SnappyMail\SensitiveString;
class ChangePasswordHestiaDriver
{
const
@ -39,7 +41,7 @@ class ChangePasswordHestiaDriver
);
}
public function ChangePassword(\RainLoop\Model\Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool
public function ChangePassword(\RainLoop\Model\Account $oAccount, SensitiveString $oPrevPassword, SensitiveString $oNewPassword) : bool
{
if (!\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->oConfig->Get('plugin', 'hestia_allowed_emails', ''))) {
return false;
@ -53,8 +55,8 @@ class ChangePasswordHestiaDriver
$HTTP = \SnappyMail\HTTP\Request::factory();
$postvars = array(
'email' => $oAccount->Email(),
'password' => $sPrevPassword,
'new' => $sNewPassword,
'password' => (string) $oPrevPassword,
'new' => (string) $oNewPassword,
);
$response = $HTTP->doRequest('POST', 'https://'.$sHost.':'.$sPort.'/reset/mail/', \http_build_query($postvars));
if (!$response) {

View file

@ -1,15 +1,15 @@
<?php
use \RainLoop\Exceptions\ClientException;
use RainLoop\Exceptions\ClientException;
class ChangePasswordHestiaPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Change Password Hestia',
AUTHOR = 'Jaap Marcus',
VERSION = '1.2',
RELEASE = '2023-05-16',
REQUIRED = '2.16.3',
VERSION = '2.36',
RELEASE = '2024-03-17',
REQUIRED = '2.36.0',
CATEGORY = 'Security',
DESCRIPTION = 'Extension to allow users to change their passwords through HestiaCP';

View file

@ -1,6 +1,7 @@
<?php
use MailSo\Net\ConnectSettings;
use SnappyMail\SensitiveString;
class ChangePasswordHMailServerDriver
{
@ -37,7 +38,7 @@ class ChangePasswordHMailServerDriver
);
}
public function ChangePassword(\RainLoop\Model\Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool
public function ChangePassword(\RainLoop\Model\Account $oAccount, SensitiveString $oPrevPassword, SensitiveString $oNewPassword) : bool
{
if (!\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->oConfig->Get('plugin', 'hmailserver_emails', ''))) {
return false;
@ -57,12 +58,12 @@ class ChangePasswordHMailServerDriver
$this->oConfig->Get('plugin', 'hmailserver_password', '')
)) {
$sEmail = $oAccount->Email();
$sDomain = \MailSo\Base\Utils::GetDomainFromEmail($sEmail);
$sDomain = \MailSo\Base\Utils::getEmailAddressDomain($sEmail);
$oHmailDomain = $oHmailApp->Domains->ItemByName($sDomain);
if ($oHmailDomain) {
$oHmailAccount = $oHmailDomain->Accounts->ItemByAddress($sEmail);
if ($oHmailAccount) {
$oHmailAccount->Password = $sNewPassword;
$oHmailAccount->Password = (string) $oNewPassword;
$oHmailAccount->Save();
$bResult = true;
} else {

View file

@ -1,14 +1,14 @@
<?php
use \RainLoop\Exceptions\ClientException;
use RainLoop\Exceptions\ClientException;
class ChangePasswordHMailServerPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Change Password hMailServer',
VERSION = '2.0',
RELEASE = '2022-10-14',
REQUIRED = '2.15.3',
VERSION = '2.36',
RELEASE = '2024-03-17',
REQUIRED = '2.36.0',
CATEGORY = 'Security',
DESCRIPTION = 'Extension to allow users to change their passwords through hMailServer';

View file

@ -1,5 +1,7 @@
<?php
use SnappyMail\SensitiveString;
class ChangePasswordISPConfigDriver
{
const
@ -44,7 +46,7 @@ class ChangePasswordISPConfigDriver
);
}
public function ChangePassword(\RainLoop\Model\Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool
public function ChangePassword(\RainLoop\Model\Account $oAccount, SensitiveString $oPrevPassword, SensitiveString $oNewPassword) : bool
{
if (!\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->oConfig->Get('plugin', 'ispconfig_allowed_emails', ''))) {
return false;
@ -71,10 +73,10 @@ class ChangePasswordISPConfigDriver
if (!empty($aFetchResult['mailuser_id'])) {
$sDbPassword = $aFetchResult['password'];
$sDbSalt = \substr($sDbPassword, 0, \strrpos($sDbPassword, '$'));
if (\crypt($sPrevPassword, $sDbSalt) === $sDbPassword) {
if (\crypt($oPrevPassword, $sDbSalt) === $sDbPassword) {
$oStmt = $oPdo->prepare('UPDATE mail_user SET password = ? WHERE mailuser_id = ?');
return !!$oStmt->execute(array(
$this->cryptPassword($sNewPassword),
$this->cryptPassword($oNewPassword),
$aFetchResult['mailuser_id']
));
}
@ -90,7 +92,7 @@ class ChangePasswordISPConfigDriver
return false;
}
private function cryptPassword(string $sPassword) : string
private function cryptPassword(SensitiveString $oPassword) : string
{
if (\defined('CRYPT_SHA512') && CRYPT_SHA512) {
$sSalt = '$6$rounds=5000$' . \bin2hex(\random_bytes(8)) . '$';
@ -99,6 +101,6 @@ class ChangePasswordISPConfigDriver
} else {
$sSalt = '$1$' . \bin2hex(\random_bytes(6)) . '$';
}
return \crypt($sPassword, $sSalt);
return \crypt($oPassword, $sSalt);
}
}

View file

@ -1,14 +1,14 @@
<?php
use \RainLoop\Exceptions\ClientException;
use RainLoop\Exceptions\ClientException;
class ChangePasswordISPConfigPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Change Password ISPConfig',
VERSION = '2.19',
RELEASE = '2023-04-11',
REQUIRED = '2.23.0',
VERSION = '2.36',
RELEASE = '2024-03-17',
REQUIRED = '2.36.0',
CATEGORY = 'Security',
DESCRIPTION = 'Extension to allow users to change their passwords through ISPConfig';

View file

@ -1,6 +1,7 @@
<?php
use MailSo\Net\ConnectSettings;
use SnappyMail\SensitiveString;
class ChangePasswordPoppassdDriver extends \MailSo\Net\NetClient
{
@ -33,7 +34,7 @@ class ChangePasswordPoppassdDriver extends \MailSo\Net\NetClient
);
}
public function ChangePassword(\RainLoop\Model\Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool
public function ChangePassword(\RainLoop\Model\Account $oAccount, SensitiveString $oPrevPassword, SensitiveString $oNewPassword) : bool
{
if (!\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->oConfig->Get('plugin', 'poppassd_allowed_emails', ''))) {
return false;
@ -55,7 +56,7 @@ class ChangePasswordPoppassdDriver extends \MailSo\Net\NetClient
try
{
$this->sendRequestWithCheck('user', $oAccount->IncLogin(), true);
$this->sendRequestWithCheck('pass', $sPrevPassword, true);
$this->sendRequestWithCheck('pass', $oPrevPassword, true);
}
catch (\Throwable $oException)
{
@ -65,7 +66,7 @@ class ChangePasswordPoppassdDriver extends \MailSo\Net\NetClient
$this->bIsLoggined = true;
if ($this->bIsLoggined) {
$this->sendRequestWithCheck('newpass', $sNewPassword);
$this->sendRequestWithCheck('newpass', $oNewPassword);
} else {
$this->writeLogException(
new \RuntimeException('Required login'),

View file

@ -1,14 +1,14 @@
<?php
use \RainLoop\Exceptions\ClientException;
use RainLoop\Exceptions\ClientException;
class ChangePasswordPoppassdPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Change Password Poppassd',
VERSION = '2.20',
RELEASE = '2023-12-08',
REQUIRED = '2.28.0',
VERSION = '2.36',
RELEASE = '2024-03-17',
REQUIRED = '2.36.0',
CATEGORY = 'Security',
DESCRIPTION = 'Extension to allow users to change their passwords through Poppassd';

View file

@ -1,5 +1,7 @@
<?php
use SnappyMail\SensitiveString;
class ChangePasswordDriverLDAP
{
const
@ -55,19 +57,19 @@ class ChangePasswordDriverLDAP
);
}
public function ChangePassword(\RainLoop\Model\Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool
public function ChangePassword(\RainLoop\Model\Account $oAccount, SensitiveString $oPrevPassword, SensitiveString $oNewPassword) : bool
{
$sDomain = \MailSo\Base\Utils::GetDomainFromEmail($oAccount->Email());
$sDomain = \MailSo\Base\Utils::getEmailAddressDomain($oAccount->Email());
$sUserDn = \strtr($this->sUserDnFormat, array(
'{domain}' => $sDomain,
'{domain:dc}' => 'dc='.\strtr($sDomain, array('.' => ',dc=')),
'{email}' => $oAccount->Email(),
'{email:user}' => \MailSo\Base\Utils::GetAccountNameFromEmail($oAccount->Email()),
'{email:user}' => \MailSo\Base\Utils::getEmailAddressLocalPart($oAccount->Email()),
'{email:domain}' => $sDomain,
'{login}' => $oAccount->IncLogin(),
'{imap:login}' => $oAccount->IncLogin(),
'{imap:host}' => $oAccount->Domain()->IncHost(),
'{imap:port}' => $oAccount->Domain()->IncPort(),
'{imap:host}' => $oAccount->Domain()->ImapSettings()->host,
'{imap:port}' => $oAccount->Domain()->ImapSettings()->port,
'{gecos}' => \function_exists('posix_getpwnam') ? \posix_getpwnam($oAccount->IncLogin()) : ''
));
@ -89,25 +91,25 @@ class ChangePasswordDriverLDAP
throw new \Exception('ldap_start_tls error '.\ldap_errno($oCon).': '.\ldap_error($oCon));
}
if (!\ldap_bind($oCon, $sUserDn, $sPrevPassword)) {
if (!\ldap_bind($oCon, $sUserDn, $oPrevPassword)) {
throw new \Exception('ldap_bind error '.\ldap_errno($oCon).': '.\ldap_error($oCon));
}
$sSshaSalt = '';
$sPrefix = '{'.\strtoupper($this->sPasswordEncType).'}';
$sEncodedNewPassword = $sNewPassword;
$sEncodedNewPassword = $oNewPassword;
switch ($sPrefix)
{
case '{SSHA}':
$sSshaSalt = $this->getSalt(4);
case '{SHA}':
$sEncodedNewPassword = $sPrefix.\base64_encode(\hash('sha1', $sNewPassword.$sSshaSalt, true).$sSshaSalt);
$sEncodedNewPassword = $sPrefix.\base64_encode(\hash('sha1', $oNewPassword.$sSshaSalt, true).$sSshaSalt);
break;
case '{MD5}':
$sEncodedNewPassword = $sPrefix.\base64_encode(\md5($sNewPassword, true));
$sEncodedNewPassword = $sPrefix.\base64_encode(\md5($oNewPassword, true));
break;
case '{CRYPT}':
$sEncodedNewPassword = $sPrefix.\crypt($sNewPassword, $this->getSalt(2));
$sEncodedNewPassword = $sPrefix.\crypt($oNewPassword, $this->getSalt(2));
break;
}

View file

@ -1,5 +1,7 @@
<?php
use SnappyMail\SensitiveString;
class ChangePasswordDriverPDO
{
const
@ -58,7 +60,7 @@ class ChangePasswordDriverPDO
);
}
public function ChangePassword(\RainLoop\Model\Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool
public function ChangePassword(\RainLoop\Model\Account $oAccount, SensitiveString $oPrevPassword, SensitiveString $oNewPassword) : bool
{
try
{
@ -85,10 +87,10 @@ class ChangePasswordDriverPDO
$placeholders = array(
':email' => $sEmail,
':oldpass' => $encrypt_prefix . \ChangePasswordPlugin::encrypt($encrypt, $sPrevPassword),
':newpass' => $encrypt_prefix . \ChangePasswordPlugin::encrypt($encrypt, $sNewPassword),
':domain' => \MailSo\Base\Utils::GetDomainFromEmail($sEmail),
':username' => \MailSo\Base\Utils::GetAccountNameFromEmail($sEmail),
':oldpass' => $encrypt_prefix . \ChangePasswordPlugin::encrypt($encrypt, $oPrevPassword),
':newpass' => $encrypt_prefix . \ChangePasswordPlugin::encrypt($encrypt, $oNewPassword),
':domain' => \MailSo\Base\Utils::getEmailAddressDomain($sEmail),
':username' => \MailSo\Base\Utils::getEmailAddressLocalPart($sEmail),
':login_name' => $oAccount->IncLogin()
);

View file

@ -1,14 +1,15 @@
<?php
use \RainLoop\Exceptions\ClientException;
use RainLoop\Exceptions\ClientException;
use SnappyMail\SensitiveString;
class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Change Password',
VERSION = '2.19',
RELEASE = '2023-04-11',
REQUIRED = '2.23.0',
VERSION = '2.36',
RELEASE = '2024-03-17',
REQUIRED = '2.36.0',
CATEGORY = 'Security',
DESCRIPTION = 'Extension to allow users to change their passwords';
@ -149,15 +150,16 @@ class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin
if ($sPrevPassword !== $oAccount->IncPassword()) {
throw new ClientException(static::CurrentPasswordIncorrect, null, $oActions->StaticI18N('NOTIFICATIONS/CURRENT_PASSWORD_INCORRECT'));
}
$oPrevPassword = new \SnappyMail\SensitiveString($sPrevPassword);
$sNewPassword = $this->jsonParam('NewPassword');
if ($this->Config()->Get('plugin', 'pass_min_length', 10) > \strlen($sNewPassword)) {
throw new ClientException(static::NewPasswordShort, null, $oActions->StaticI18N('NOTIFICATIONS/NEW_PASSWORD_SHORT'));
}
if ($this->Config()->Get('plugin', 'pass_min_strength', 70) > static::PasswordStrength($sNewPassword)) {
throw new ClientException(static::NewPasswordWeak, null, $oActions->StaticI18N('NOTIFICATIONS/NEW_PASSWORD_WEAK'));
}
$oNewPassword = new \SnappyMail\SensitiveString($sNewPassword);
$bResult = false;
$oConfig = $this->Config();
@ -171,7 +173,7 @@ class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin
$oConfig,
$oLogger
);
if (!$oDriver->ChangePassword($oAccount, $sPrevPassword, $sNewPassword)) {
if (!$oDriver->ChangePassword($oAccount, $oPrevPassword, $oNewPassword)) {
throw new ClientException(static::CouldNotSaveNewPassword);
}
$bResult = true;
@ -196,15 +198,16 @@ class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin
throw new ClientException(static::CouldNotSaveNewPassword);
}
$oAccount->SetPassword($sNewPassword);
$oAccount->SetPassword($oNewPassword);
if ($oAccount instanceof \RainLoop\Model\MainAccount) {
$oActions->SetAuthToken($oAccount);
$oAccount->resealCryptKey($oPrevPassword);
}
return $this->jsonResponse(__FUNCTION__, $oActions->AppData(false));
}
public static function encrypt(string $algo, string $password)
public static function encrypt(string $algo, SensitiveString $password)
{
switch (\strtolower($algo))
{
@ -233,7 +236,7 @@ class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin
private static function PasswordStrength(string $sPassword) : int
{
$i = \strlen($sPassword);
$max = min(100, $i * 8);
$max = \min(100, $i * 8);
$s = 0;
while (--$i) {
$s += ($sPassword[$i] != $sPassword[$i-1] ? 1 : -0.5);

View file

@ -0,0 +1,88 @@
.squire-toolbar {
padding-top: 4px;
padding-bottom: 0;
overflow: visible;
z-index: 200;
white-space: normal;
min-height: auto;
}
.squire-toolbar > .btn-group {
margin-bottom: 4px;
}
.squire-toolbar > .btn-group > a.btn,
.squire-toolbar button.btn,
.squire-toolbar select.btn {
line-height: 20px;
padding-top: 4px;
padding-bottom: 4px;
min-height: 24px;
}
.squire-toolbar-menu-item {
display: flex;
align-items: center;
gap: .25em;
margin: .1em !important;
cursor: pointer;
padding: .25em;
}
.squire-toolbar-menu-item.active {
background-color: rgba(128, 128, 128, .1);
}
.squire-toolbar-menu-item:hover {
background-color: rgba(128, 128, 128, .2);
}
.squire-toolbar-svg-icon {
display: block;
fill: var(--dialog-clr, #333);
}
.squire2-mode-wysiwyg .squire-plain,
.squire2-mode-source .squire-wysiwyg,
.squire2-mode-plain .squire-wysiwyg {
display: none;
}
.squire2-mode-source .squire-plain,
.squire2-mode-plain .squire-plain {
display: block;
}
.squire-toolbar > .squire-toolbar-menu-wrap:last-child {
float: right;
}
.squire-toolbar.mode-plain .squire-html-mode-item {
display: none;
}
#V-PopupsCompose .attachmentAreaParent.compact {
height: auto;
min-height: auto;
padding: 0;
overflow: auto;
flex: 1 0 auto;
max-height: 12em;
margin: 0;
}
#V-PopupsCompose .compact > .b-attachment-place {
position: static;
display: none;
margin: .375em;
line-height: 4em;
}
#V-PopupsCompose .compact > .b-attachment-place.dragAndDropOver {
display: block;
}
#V-PopupsCompose .compact .attachmentList {
margin: 0;
padding: 0;
}

View file

@ -0,0 +1,23 @@
<?php
class CompactComposerPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Compact Composer',
AUTHOR = 'Sergey Mosin',
URL = 'https://github.com/the-djmaze/snappymail/pull/1466',
VERSION = '1.0.2',
RELEASE = '2024-02-23',
REQUIRED = '2.34.0',
LICENSE = 'AGPL v3',
DESCRIPTION = 'WYSIWYG editor with a compact toolbar';
public function Init(): void
{
$this->addTemplate('templates/PopupsCompactCompose.html');
$this->addCss('css/composer.css');
$this->addJs('js/squire-raw.js');
$this->addJs('js/parsel.js');
$this->addJs('js/CompactComposer.js');
}
}

View file

@ -0,0 +1,991 @@
/* eslint max-len: 0 */
(win => {
const rl = win.rl;
if (!rl) {
return;
}
rl.registerWYSIWYG('CompactComposer', (owner, container, onReady) => {
const editor = new CompactComposer(container);
onReady(editor);
});
const doc = win.document;
// If a user (or admin) selected the CompactComposer we need to
// replace PopupsCompose template with PopupsCompactCompose template.
// --
// This might break some plugins if they query/change PopupsCompose template
// before this code is called. They should instead listen for
// 'rl-view-model.create' to work properly.
if (rl.settings.get('editorWysiwyg') === 'CompactComposer') {
const compactTemplate = doc.getElementById('PopupsCompactCompose');
if (!compactTemplate) {
console.error('CompactComposer: PopupsCompactCompose template not found');
return;
}
const originalTemplate = doc.getElementById('PopupsCompose');
if (originalTemplate) {
originalTemplate.id = 'PopupsCompose_replaced';
} else {
console.warn('CompactComposer: PopupsCompose template not found');
}
compactTemplate.id = 'PopupsCompose';
addEventListener('rl-view-model.create', e => {
if (e.detail.viewModelTemplateID === 'PopupsCompose') {
// There is a better way to do this probably,
// but we need this for drag and drop to work
e.detail.attachmentsArea = e.detail.bodyArea;
}
});
}
const
removeElements = 'HEAD,LINK,META,NOSCRIPT,SCRIPT,TEMPLATE,TITLE',
allowedElements = 'A,B,BLOCKQUOTE,BR,DIV,EM,FONT,H1,H2,H3,H4,H5,H6,HR,I,IMG,LI,OL,P,SPAN,STRONG,TABLE,TD,TH,TR,U,UL',
allowedAttributes = 'abbr,align,background,bgcolor,border,cellpadding,cellspacing,class,color,colspan,dir,face,frame,height,href,hspace,id,lang,rowspan,rules,scope,size,src,style,target,type,usemap,valign,vspace,width'.split(','),
// TODO: labels translations
i18n = (str, def) => rl.i18n(str) || def,
ctrlKey = shortcuts.getMetaKey() + ' + ',
createElement = name => doc.createElement(name),
tpl = createElement('template'),
trimLines = html => html.trim().replace(/^(<div>\s*<br\s*\/?>\s*<\/div>)+/, '').trim(),
htmlToPlain = html => rl.Utils.htmlToPlain(html).trim(),
plainToHtml = text => rl.Utils.plainToHtml(text),
getFragmentOfChildren = parent => {
let frag = doc.createDocumentFragment();
frag.append(...parent.childNodes);
return frag;
},
/**
* @param {Array} data
* @param {String} prop
*/
getByProp = (data, prop) => {
for (let i = 0; i < data.length; i++) {
const outer = data[i];
if (outer.hasOwnProperty(prop)) {
return outer;
}
if (outer.items && Array.isArray(outer.items)) {
const item = outer.items.find(item => item.prop === prop);
if (item) {
return item;
}
}
}
throw new Error('item with prop ' + prop + ' not found');
},
SquireDefaultConfig = {
/*
addLinks: true // allow_smart_html_links
*/
sanitizeToDOMFragment: (html) => {
tpl.innerHTML = (html || '')
.replace(/<\/?(BODY|HTML)[^>]*>/gi, '')
.replace(/<!--[^>]+-->/g, '')
.replace(/<span[^>]*>\s*<\/span>/gi, '')
.trim();
tpl.querySelectorAll('a:empty,span:empty').forEach(el => el.remove());
return tpl.content;
}
},
pasteSanitizer = (event) => {
const frag = event.detail.fragment;
frag.querySelectorAll('a:empty,span:empty').forEach(el => el.remove());
frag.querySelectorAll(removeElements).forEach(el => el.remove());
frag.querySelectorAll('*').forEach(el => {
if (!el.matches(allowedElements)) {
el.replaceWith(getFragmentOfChildren(el));
} else if (el.hasAttributes()) {
[...el.attributes].forEach(attr => {
let name = attr.name.toLowerCase();
if (!allowedAttributes.includes(name)) {
el.removeAttribute(name);
}
});
}
});
},
pasteImageHandler = (e, squire) => {
const items = [...e.detail.clipboardData.items];
const imageItems = items.filter((item) => /image/.test(item.type));
if (!imageItems.length) {
return false;
}
let reader = new FileReader();
reader.onload = (loadEvent) => {
squire.insertImage(loadEvent.target.result);
};
reader.readAsDataURL(imageItems[0].getAsFile());
};
class CompactComposer {
constructor(container) {
const
plain = createElement('textarea'),
wysiwyg = createElement('div'),
toolbar = createElement('div'),
squire = new win.Squire2(wysiwyg, SquireDefaultConfig);
this.container = container;
plain.className = 'squire-plain';
wysiwyg.className = 'squire-wysiwyg';
wysiwyg.dir = 'auto';
this.mode = ''; // 'plain' | 'wysiwyg'
this.squire = squire;
this.plain = plain;
this.wysiwyg = wysiwyg;
this.toolbar = toolbar;
toolbar.className = 'squire-toolbar btn-toolbar';
const actions = this.#makeActions(squire, toolbar);
this.squire.addEventListener('willPaste', pasteSanitizer);
this.squire.addEventListener('pasteImage', (e) => {
pasteImageHandler(e, squire);
});
// squire.addEventListener('focus', () => shortcuts.off());
// squire.addEventListener('blur', () => shortcuts.on());
container.append(toolbar, wysiwyg, plain);
const fontFamilySelect = getByProp(actions, 'fontFamily').element;
const fontSizeAction = getByProp(actions, 'fontSize');
/**
* @param {string} fontName
* @return {string}
*/
const normalizeFontName = (fontName) => fontName.trim().replace(/(^["']*|["']*$)/g, '').trim().toLowerCase();
/** @type {string[]} - lower cased array of available font families*/
const fontFamiliesLowerCase = Object.values(fontFamilySelect.options).map(option => option.value.toLowerCase());
/**
* A theme might have CSS like div.squire-wysiwyg[contenteditable="true"] {
* font-family: 'Times New Roman', Times, serif; }
* so let's find the best match squire.getRoot()'s font
* it will also help to properly handle generic font names like 'sans-serif'
* @type {number}
*/
let defaultFontFamilyIndex = 0;
const squireRootFonts = getComputedStyle(squire.getRoot()).fontFamily.split(',').map(normalizeFontName);
fontFamiliesLowerCase.some((family, index) => {
const matchFound = family.split(',').some(availableFontName => {
const normalizedFontName = normalizeFontName(availableFontName);
return squireRootFonts.some(squireFontName => squireFontName === normalizedFontName);
});
if (matchFound) {
defaultFontFamilyIndex = index;
}
return matchFound;
});
/**
* Instead of comparing whole 'font-family' strings,
* we are going to look for individual font names, because we might be
* editing a Draft started in another email client for example
*
* @type {Object.<string,number>}
*/
const fontNamesMap = {};
/**
* @param {string} fontFamily
* @param {number} index
*/
const processFontFamilyString = (fontFamily, index) => {
fontFamily.split(',').forEach(fontName => {
const key = normalizeFontName(fontName);
if (fontNamesMap[key] === undefined) {
fontNamesMap[key] = index;
}
});
};
// first deal with the default font family
processFontFamilyString(fontFamiliesLowerCase[defaultFontFamilyIndex], defaultFontFamilyIndex);
// and now with the rest of the font families
fontFamiliesLowerCase.forEach((fontFamily, index) => {
if (index !== defaultFontFamilyIndex) {
processFontFamilyString(fontFamily, index);
}
});
// -----
let ignoreNextSelectEvent = false;
squire.addEventListener('pathChange', e => {
const tokensMap = this.buildTokensMap(e.detail);
if (tokensMap.has('__selection__')) {
ignoreNextSelectEvent = false;
return;
}
this.indicators.forEach((indicator) => {
indicator.element.classList.toggle('active', indicator.selectors.some(selector => tokensMap.has(selector)));
});
let familySelectedIndex = defaultFontFamilyIndex;
const fontFamily = tokensMap.get('__font_family__');
if (fontFamily) {
familySelectedIndex = -1; // show empty select if we don't know the font
const fontNames = fontFamily.split(',');
for (let i = 0; i < fontNames.length; i++) {
const index = fontNamesMap[normalizeFontName(fontNames[i])];
if (index !== undefined) {
familySelectedIndex = index;
break;
}
}
}
fontFamilySelect.selectedIndex = familySelectedIndex;
let sizeSelectedIndex = fontSizeAction.defaultValueIndex;
const fontSize = tokensMap.get('__font_size__');
if (fontSize) {
// -1 is ok because it will just show a blank <select>
sizeSelectedIndex = fontSizeAction.items.indexOf(fontSize);
}
fontSizeAction.element.selectedIndex = sizeSelectedIndex;
ignoreNextSelectEvent = true;
});
squire.addEventListener('select', e => {
if (ignoreNextSelectEvent) {
ignoreNextSelectEvent = false;
return;
}
if (e.detail.range.collapsed) {
return;
}
this.indicators.forEach((indicator) => {
indicator.element.classList.toggle('active', indicator.selectors.some(selector => squire.hasFormat(selector)));
});
});
/*
squire.addEventListener('cursor', e => {
console.dir({cursor:e.range});
});
squire.addEventListener('select', e => {
console.dir({select:e.range});
});
*/
}
/**
* @param {Squire} squire
* @param {HTMLDivElement} toolbar
* @returns {Array}
*/
#makeActions(squire, toolbar) {
const clr = this.#makeClr();
const doClr = name => input => {
// https://github.com/the-djmaze/snappymail/issues/826
clr.style.left = (input.offsetLeft + input.parentNode.offsetLeft) + 'px';
clr.style.width = input.offsetWidth + 'px';
clr.value = '';
clr.onchange = () => {
switch (name) {
case 'color':
squire.setTextColor(clr.value);
break;
case 'backgroundColor':
squire.setHighlightColor(clr.value);
break;
default:
console.error('invalid name:', name);
}
};
// Chrome 110+ https://github.com/the-djmaze/snappymail/issues/1199
// clr.oninput = () => squire.setStyle({[name]:clr.value});
setTimeout(() => clr.click(), 1);
};
toolbar.append(clr);
const browseImage = createElement('input');
browseImage.type = 'file';
browseImage.accept = 'image/*';
browseImage.style.display = 'none';
browseImage.onchange = () => {
if (browseImage.files.length) {
let reader = new FileReader();
reader.readAsDataURL(browseImage.files[0]);
reader.onloadend = () => reader.result && squire.insertImage(reader.result);
}
};
const actions = [
{
type: 'group',
items: [
{
type: 'select',
label: 'Font',
cmd: s => squire.setFontFace(s.value),
prop: 'fontFamily',
items: {
'sans-serif': {
Arial: '\'Nimbus Sans L\', \'Liberation sans\', \'Arial Unicode MS\', Arial, Helvetica, Garuda, Utkal, FreeSans, sans-serif',
Tahoma: '\'Luxi Sans\', Tahoma, Loma, Geneva, Meera, sans-serif',
Trebuchet: '\'DejaVu Sans Condensed\', Trebuchet, \'Trebuchet MS\', sans-serif',
Lucida: '\'Lucida Sans Unicode\', \'Lucida Sans\', \'DejaVu Sans\', \'Bitstream Vera Sans\', \'DejaVu LGC Sans\', sans-serif',
Verdana: '\'DejaVu Sans\', Verdana, Geneva, \'Bitstream Vera Sans\', \'DejaVu LGC Sans\', sans-serif'
},
monospace: {
Courier: '\'Liberation Mono\', \'Courier New\', FreeMono, Courier, monospace',
Lucida: '\'DejaVu Sans Mono\', \'DejaVu LGC Sans Mono\', \'Bitstream Vera Sans Mono\', \'Lucida Console\', Monaco, monospace'
},
sans: {
Times: '\'Nimbus Roman No9 L\', \'Times New Roman\', Times, FreeSerif, serif',
Palatino: '\'Bitstream Charter\', \'Palatino Linotype\', Palatino, Palladio, \'URW Palladio L\', \'Book Antiqua\', Times, serif',
Georgia: '\'URW Palladio L\', Georgia, Times, serif'
}
}
},
{
type: 'select',
label: 'Font size',
cmd: s => squire.setFontSize(s.value),
prop: 'fontSize',
items: ['11px', '13px', '16px', '20px', '24px', '30px'],
defaultValueIndex: 2
}
]
},
{
type: 'menu',
label: 'Colors',
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m8 7.75c0.713 0 1.25-0.559 1.25-1.25 0-0.691-0.54-1.25-1.25-1.25-0.71 0-1.25 0.556-1.25 1.25 0 0.694 0.537 1.25 1.25 1.25zm6.5 3c0.713 0 1.25-0.559 1.25-1.25 0-0.691-0.54-1.25-1.25-1.25s-1.25 0.556-1.25 1.25c0 0.694 0.537 1.25 1.25 1.25zm-9 0c0.713 0 1.25-0.559 1.25-1.25 0-0.691-0.54-1.25-1.25-1.25s-1.25 0.556-1.25 1.25c0 0.694 0.537 1.25 1.25 1.25zm4.5 7.25c-4.47 0-8-3.63-8-8 0-4.81 3.97-8 8.17-8 4.12 0 7.83 3.02 7.83 7.21 0 2.83-2.2 4.79-4.79 4.79h-1.42c-0.277 0-0.417 0.2-0.417 0.375 0 0.208 0.104 0.382 0.312 0.521 0.208 0.139 0.312 0.507 0.312 1.1 0 1.09-0.858 2-2 2zm2-10.2c0.713 0 1.25-0.559 1.25-1.25s-0.54-1.25-1.25-1.25-1.25 0.556-1.25 1.25 0.537 1.25 1.25 1.25zm-2 8.75c0.477 0 0.737-0.739 0.188-1.08-0.226-0.142-0.312-0.514-0.312-1.04 0-1.18 0.934-1.88 1.9-1.88h1.44c2.09-0.032 3.27-1.65 3.29-3.29 0-3.19-2.79-5.71-6.33-5.71-3.74 0-6.67 2.89-6.67 6.5 0 3.49 2.68 6.45 6.5 6.5z"/></svg>',
items: [
{
type: 'menu_item',
label: 'Text Color',
cmd: doClr('color'),
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m3 18v-3h14v3zm2.35-5 3.75-10h1.79l3.75 10h-1.73l-0.896-2.56h-4.02l-0.917 2.56zm3.15-4h3l-1.46-4.04h-0.0833z"/></svg>'
},
{
type: 'menu_item',
label: 'Background Color',
cmd: doClr('backgroundColor'),
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m7.22 16.6-4.87-4.85q-0.166-0.166-0.26-0.364-0.0936-0.198-0.0936-0.427t0.0936-0.447q0.0936-0.218 0.26-0.385l4.6-4.6-2.52-2.52 1.06-1.06 8.18 8.18q0.166 0.166 0.25 0.375 0.0832 0.208 0.0832 0.437 0 0.229-0.0832 0.437-0.0832 0.208-0.25 0.375l-4.85 4.85q-0.166 0.166-0.375 0.26-0.208 0.0936-0.437 0.0936-0.229-0.0208-0.427-0.104-0.198-0.0832-0.364-0.25zm0.791-10-4.35 4.35v-0.0208 0.0208h8.7v-0.0208 0.0208zm8.18 10.3q-0.77 0-1.3-0.52-0.531-0.52-0.531-1.29 0-0.499 0.229-0.967 0.229-0.468 0.541-0.884l1.06-1.33 1.04 1.33q0.291 0.416 0.531 0.884 0.239 0.468 0.239 0.967 0 0.77-0.531 1.29-0.531 0.52-1.28 0.52z"/></svg>'
}
]
},
{
type: 'group',
items: [
{
type: 'button',
label: 'Bold',
cmd: () => this.doAction('bold', 'B'),
key: 'B',
matches: 'B,STRONT',
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m5.53 16v-12h4.75q1.4 0 2.57 0.861 1.18 0.861 1.18 2.39 0 1.06-0.469 1.66t-0.885 0.865q0.542 0.249 1.17 0.895 0.625 0.646 0.625 1.9 0 1.9-1.4 2.67-1.4 0.771-2.62 0.771zm2.65-2.46h2.18q1.01 0 1.21-0.51t0.208-0.74q0-0.229-0.219-0.74-0.219-0.51-1.28-0.51h-2.1zm0-4.83h1.94q0.688 0 1.01-0.365 0.323-0.365 0.323-0.781 0-0.5-0.356-0.812-0.356-0.312-0.923-0.312h-1.99z"/></svg>'
},
{
type: 'button',
label: 'Italic',
cmd: () => this.doAction('italic', 'I'),
key: 'I',
matches: 'I,EM',
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m4.5 16v-2h3.33l2.58-8h-3.42v-2h8.5v2h-3.08l-2.58 8h3.17v2z"/></svg>'
},
{
type: 'button',
label: 'Underline',
cmd: () => this.doAction('underline', 'U'),
key: 'U',
matches: 'U',
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m5 17v-1.5h10v1.5zm5-3q-2 0-3.09-1.24t-1.09-3.28v-6.48h2.03v6.61q0 1.1 0.551 1.79 0.551 0.688 1.61 0.688 1.06 0 1.61-0.688 0.55-0.688 0.55-1.79v-6.61h2.02v6.48q0 2.04-1.09 3.28t-3.09 1.24z"/></svg>'
}
]
},
{
type: 'group',
items: [
{
type: 'button',
label: 'Ordered List',
cmd: () => this.doList('OL'),
key: 'Shift + 8',
matches: 'OL',
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m3 17v-1h2v-0.5h-1v-1h1v-0.5h-2v-1h2.5q0.212 0 0.356 0.144t0.144 0.356v1q0 0.212-0.144 0.356t-0.356 0.144q0.212 0 0.356 0.144t0.144 0.356v1q0 0.212-0.144 0.356t-0.356 0.144zm0-5v-2q0-0.212 0.144-0.356t0.356-0.144h1.5v-0.5h-2v-1h2.5q0.212 0 0.356 0.144t0.144 0.356v1.5q0 0.212-0.144 0.356t-0.356 0.144h-1.5v0.5h2v1zm1-5v-3h-1v-1h2v4zm3.5 8v-1.5h9.5v1.5zm0-4.25v-1.5h9.5v1.5zm0-4.25v-1.5h9.5v1.5z"/></svg>'
},
{
type: 'button',
label: 'List',
cmd: () => this.doList('UL'),
key: 'Shift + 9',
matches: 'UL',
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m7.5 15v-1.5h9.5v1.5zm0-4.25v-1.5h9.5v1.5zm0-4.25v-1.5h9.5v1.5zm-3 9.25q-0.621 0-1.06-0.442-0.438-0.442-0.438-1.06 0-0.621 0.442-1.06 0.442-0.438 1.06-0.438 0.621 0 1.06 0.442 0.438 0.442 0.438 1.06 0 0.621-0.442 1.06-0.442 0.438-1.06 0.438zm0-4.25q-0.621 0-1.06-0.442-0.438-0.442-0.438-1.06 0-0.621 0.442-1.06 0.442-0.438 1.06-0.438 0.621 0 1.06 0.442 0.438 0.442 0.438 1.06 0 0.621-0.442 1.06-0.442 0.438-1.06 0.438zm0-4.25q-0.621 0-1.06-0.442-0.438-0.442-0.438-1.06 0-0.621 0.442-1.06 0.442-0.438 1.06-0.438 0.621 0 1.06 0.442 0.438 0.442 0.438 1.06 0 0.621-0.442 1.06-0.442 0.438-1.06 0.438z"/></svg>'
},
{
type: 'button',
label: 'Decrease Indent',
cmd: () => this.changeLevel('decrease'),
key: ']',
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m3 17v-1.5h14v1.5zm6-3.12v-1.5h8v1.5zm0-3.12v-1.5h8v1.5zm0-3.12v-1.5h8v1.5zm-6-3.12v-1.5h14v1.5zm4 8.5v-6l-4 3z"/></svg>'
},
{
type: 'button',
label: 'Increase Indent',
cmd: () => this.changeLevel('increase'),
key: '[',
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m3 17v-1.5h14v1.5zm6-3.12v-1.5h8v1.5zm0-3.12v-1.5h8v1.5zm0-3.12v-1.5h8v1.5zm-6-3.12v-1.5h14v1.5zm0 8.5v-6l4 3z"/></svg>'
}
]
},
{
type: 'menu',
rightEdge: true,
label: 'Insert Image',
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m15 1v2h-2v2h2v2h2v-2h2v-2h-2v-2zm-12 2c-1.09 0-2 0.909-2 2v10c0 1.09 0.909 2 2 2h14c1.09 0 2-0.909 2-2v-5h-1.75v5.25h-14.5v-10.5h7.25v-1.75zm9 6-3 4-2-3-3 4h12z"/></svg>',
items: [
{
type: 'menu_item',
label: 'Image File',
cmd: () => browseImage.click(),
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m7 11h8l-2.62-3.5-1.88 2.5-1.37-1.83zm-4.5 6q-0.604 0-1.05-0.448t-0.448-1.05v-10h1.5v10h13.5v1.5zm3-3q-0.604 0-1.05-0.448-0.448-0.448-0.448-1.05v-9q0-0.619 0.448-1.06t1.05-0.441h3.52l2 2h5.48q0.619 0 1.06 0.441 0.441 0.441 0.441 1.06v7q0 0.604-0.441 1.05-0.441 0.448-1.06 0.448zm0-1.5h11v-7h-6.08l-2-2h-2.92zm0 0v-9z"/></svg>'
},
{
type: 'menu_item',
label: 'From URL',
cmd: () => {
//TODO: check is if an IMG node is in range already
const src = prompt('Image', 'https://');
if (src) {
this.squire.insertImage(src);
}
},
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m11.9 8h-1.88c-0.692 0-1.28-0.244-1.77-0.732-0.488-0.488-0.731-1.08-0.731-1.77s0.244-1.28 0.731-1.77 1.08-0.729 1.77-0.729h1.88v0.938h-1.88c-0.434 0-0.803 0.152-1.11 0.456s-0.456 0.673-0.456 1.11 0.152 0.803 0.456 1.11 0.607 0.456 1.11 0.456h1.88zm-1.25-2.03v-0.938h3.75v0.938zm2.5 2.03v-0.938h1.88c0.434 0 0.803-0.152 1.11-0.456s0.456-0.673 0.456-1.11-0.152-0.803-0.456-1.11-0.673-0.456-1.11-0.456h-1.88v-0.938h1.88c0.692 0 1.28 0.244 1.77 0.732 0.488 0.488 0.731 1.08 0.731 1.77 0 0.692-0.244 1.28-0.731 1.77-0.488 0.486-1.08 0.729-1.77 0.729zm3.38 2v5.5c0 0.403-0.147 0.753-0.441 1.05s-0.647 0.448-1.06 0.448h-11c-0.412 0-0.766-0.149-1.06-0.448s-0.441-0.649-0.441-1.05v-9.25c0-0.403 0.147-0.753 0.441-1.05s0.649-0.407 1.06-0.448h1.5v1.5h-1.5v9.25h11v-5.5zm-11.5 4h9l-3-4-2.25 3-1.5-2z"/></svg>'
}
]
},
{
// this is a special case: we move the "attach" button group to the toolbar
// TODO: there is probably a better way of doing this in the template
// TODO: move Encrypt/Sign button group ?
type: 'move_parent',
label: 'Attach File',
id: 'composeUploadButton',
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m12.5 9.5v-4.5h1.5v4.5zm-3.5 5.44c-1.02-0.272-1.5-1.15-1.5-2.02v-7.92h1.5zm0.5 3.06c-2.59 0-4.5-2.11-4.5-4.65v-8.1c0-1.95 1.55-3.25 3.25-3.25 1.85 0 3.25 1.51 3.25 3.4v6.35h-1.5v-6.5c0-1.09-0.883-1.75-1.75-1.75-1.06 0-1.75 0.906-1.75 1.79v8.21c0 2.63 3.33 4.15 5.25 1.96v1.94c-0.706 0.428-1.56 0.604-2.25 0.604zm3.75-1v-2.25h-2.25v-1.5h2.25v-2.25h1.5v2.25h2.25v1.5h-2.25v2.25z"/></svg>'
},
{
type: 'menu_more',
label: 'More',
rightEdge: true,
showInPlainMode: true,
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m2 4v2h16v-2zm0 5v2h16v-2zm0 5v2h16v-2z"/></svg>',
items: [
{
type: 'menu_item',
label: 'Undo',
cmd: () => squire.undo(),
key: 'Z',
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m4.82 9.3c1.45-1.26 3.32-2.03 5.4-2.03 3.64 0 6.71 2.37 7.79 5.65l-1.85 0.61c-0.821-2.49-3.17-4.3-5.94-4.3-1.52 0-2.92 0.563-4 1.47l2.83 2.83h-7.04v-7.04z"/></svg>'
},
{
type: 'menu_item',
label: 'Redo',
cmd: () => squire.redo(),
key: 'Y',
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m15.2 9.3c-1.45-1.26-3.32-2.03-5.4-2.03-3.64 0-6.71 2.37-7.79 5.65l1.85 0.61c0.821-2.49 3.17-4.3 5.94-4.3 1.52 0 2.92 0.563 4 1.47l-2.83 2.83h7.04v-7.04z"/></svg>'
},
{
type: 'menu_item',
label: 'Blockquote',
cmd: () => {
if (!['UL', 'OL', 'BLOCKQUOTE'].some(listTag => this.squire.hasFormat(listTag))) {
this.changeLevel('increase');
}
},
matches: 'BLOCKQUOTE',
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m4 3c-0.554 0-1 0.446-1 1s0.446 1 1 1h12c0.554 0 1-0.446 1-1s-0.446-1-1-1h-12zm0 6c-0.554 0-1 0.446-1 1v6c0 0.554 0.446 1 1 1s1-0.446 1-1v-6c0-0.554-0.446-1-1-1zm5 0c-0.554 0-1 0.446-1 1s0.446 1 1 1h7c0.554 0 1-0.446 1-1s-0.446-1-1-1h-7zm0 6c-0.554 0-1 0.446-1 1s0.446 1 1 1h7c0.554 0 1-0.446 1-1s-0.446-1-1-1h-7z"/></svg>'
},
{
type: 'menu_item',
label: 'Link',
cmd: () => {
/** @type {Range} range */
const range = this.squire.getSelection();
let linkNode;
if (range.collapsed || range.startContainer.parentNode === range.endContainer.parentNode) {
const root = this.squire.getRoot();
for (let node = range.startContainer; node !== root; node = node.parentNode) {
if (node.tagName === 'A') {
range.selectNode(node);
linkNode = node;
break;
}
}
}
const url = prompt('Link', linkNode?.href || 'https://');
if (url != null) {
if (url.length) {
if (range.collapsed === false) {
// squire breaks the wrapping node, so if we have a <b>
// inside the selection it will create something like this:
// <a>t</a><b><a>ex</a></b><a>t</a> and we don't want that
// TODO: this could be more elegant
this.squire.removeAllFormatting(range);
}
this.squire.makeLink(url);
} else if (linkNode) {
this.squire.removeLink();
}
}
},
matches: 'A',
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m15.4 16.7c-0.509 0.521-1.13 0.781-1.87 0.781-0.735 0-1.36-0.256-1.87-0.771l-1.91-1.91c-0.515-0.515-0.771-1.14-0.771-1.87 0-0.743 0.267-1.38 0.801-1.9l-0.809-0.809c-0.525 0.533-1.16 0.801-1.91 0.801-0.735 0-1.36-0.255-1.87-0.763l-1.9-1.89c-0.521-0.509-0.781-1.13-0.781-1.87-6e-7 -0.735 0.255-1.36 0.763-1.87l1.34-1.35c0.509-0.521 1.13-0.781 1.87-0.781 0.735 1.6e-4 1.36 0.256 1.87 0.771l1.91 1.91c0.515 0.515 0.772 1.14 0.772 1.87 0 0.739-0.265 1.37-0.792 1.9l0.809 0.809c0.524-0.527 1.16-0.792 1.9-0.792 0.735 0 1.36 0.255 1.87 0.763l1.9 1.89c0.521 0.509 0.781 1.13 0.781 1.87 0 0.735-0.255 1.36-0.763 1.87zm-1.25-1.24 1.34-1.35c0.165-0.178 0.248-0.385 0.248-0.624 0-0.245-0.0862-0.455-0.258-0.626l-1.9-1.89c-0.172-0.172-0.38-0.257-0.625-0.257-0.249-3.1e-5 -0.466 0.0966-0.653 0.286l0.577 0.577c0.353 0.353 0.353 0.922 0 1.28-0.353 0.353-0.922 0.353-1.28 0l-0.577-0.577c-0.184 0.182-0.278 0.401-0.278 0.654 0 0.251 0.0824 0.459 0.248 0.624l1.91 1.91c0.172 0.171 0.38 0.257 0.625 0.257 0.239 0 0.444-0.0849 0.615-0.257zm-6.43-6.5-0.577-0.577c-0.353-0.353-0.353-0.922 0-1.28 0.353-0.353 0.922-0.353 1.28 0l0.575 0.575c0.184-0.18 0.279-0.394 0.279-0.643-6e-7 -0.245-0.0849-0.454-0.257-0.625l-1.91-1.91c-0.172-0.172-0.38-0.257-0.625-0.257-0.239 0-0.444 0.0852-0.615 0.257l-1.34 1.35c-0.159 0.172-0.238 0.379-0.238 0.624 2e-7 0.251 0.0811 0.46 0.247 0.625l1.9 1.89c0.172 0.172 0.38 0.257 0.625 0.257 0.253-9e-7 0.473-0.0991 0.661-0.295z"/></svg>'
},
{
type: 'menu_item',
label: 'Strikethrough',
cmd: () => this.doAction('strikethrough', 'S'),
key: 'Shift + 7',
matches: 'S',
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m10.1 15.9q-1.5 0-2.62-0.875t-1.5-2.31l1.69-0.688q0.333 1.06 0.969 1.6 0.635 0.542 1.51 0.542 0.937 0 1.49-0.448 0.552-0.448 0.552-1.2 0-0.333-0.125-0.615t-0.375-0.51h2.15q0.104 0.229 0.135 0.49t0.0312 0.615q0 1.5-1.08 2.45-1.08 0.948-2.81 0.948zm-8.12-6v-1.5h16v1.5zm8-6q1.33 0 2.21 0.562t1.42 1.79l-1.62 0.708q-0.229-0.625-0.76-1.01-0.531-0.385-1.2-0.385-0.771 0-1.28 0.375-0.51 0.375-0.552 0.958h-1.81q0.0417-1.31 1.06-2.16 1.02-0.844 2.54-0.844z"/></svg>'
},
{
type: 'menu_item',
label: 'Superscript',
cmd: () => this.doAction('superscript', 'SUP'),
key: 'Shift + 6',
matches: 'SUP',
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m13.4 8v-1.99q0-0.424 0.288-0.715 0.288-0.291 0.712-0.292h1v-1h-2v-1h2q0.425 0 0.712 0.287 0.288 0.286 0.288 0.71v0.997q0 0.424-0.288 0.715-0.288 0.292-0.712 0.292h-1v1h2v1zm-9.38 8 3.31-5.21-3.08-4.79h1.89l2.21 3.56h0.0833l2.21-3.56h1.9l-3.1 4.79 3.33 5.21h-1.9l-2.44-3.88h-0.0833l-2.44 3.88z"/></svg>'
},
{
type: 'menu_item',
label: 'Subscript',
cmd: () => this.doAction('subscript', 'SUB'),
key: 'Shift + 5',
matches: 'SUB',
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m13.4 18v-1.99q0-0.424 0.288-0.715 0.288-0.292 0.712-0.292h1v-1h-2v-1h2q0.425 0 0.712 0.287 0.288 0.286 0.288 0.71v0.997q0 0.424-0.288 0.715-0.288 0.292-0.712 0.292h-1v1h2v1zm-9.38-3 3.31-5.21-3.08-4.79h1.89l2.21 3.56h0.0833l2.21-3.56h1.9l-3.1 4.79 3.33 5.21h-1.9l-2.44-3.88h-0.0833l-2.44 3.88z"/></svg>'
},
{
type: 'menu_item',
label: 'Left to Right',
cmd: () => squire.setTextDirection('ltr'),
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m8 12v-4q-1.25 0-2.12-0.875-0.875-0.875-0.875-2.12t0.875-2.12q0.875-0.875 2.11-0.875h6.01v1.5h-1.5v8.5h-1.5v-8.5h-1.5v8.5zm6 6-1.06-1.06 1.19-1.19h-11.1v-1.5h11.1l-1.19-1.19 1.06-1.06 3 3zm-6-11.5v-3q-0.625 0-1.06 0.442-0.438 0.442-0.438 1.06 0 0.621 0.441 1.06 0.441 0.437 1.06 0.437z"/></svg>'
},
{
type: 'menu_item',
label: 'Right to Left',
cmd: () => squire.setTextDirection('rtl'),
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m6 18-3-3 3-3 1.06 1.06-1.19 1.19h11.1v1.5h-11.1l1.19 1.19zm2-6v-4q-1.25 0-2.12-0.875-0.875-0.875-0.875-2.12t0.875-2.12q0.875-0.875 2.11-0.875h6.01v1.5h-1.5v8.5h-1.5v-8.5h-1.5v8.5zm0-5.5v-3q-0.625 0-1.06 0.442-0.438 0.442-0.438 1.06 0 0.621 0.441 1.06 0.441 0.437 1.06 0.437z"/></svg>'
},
{
type: 'menu_item',
label: 'HTML Mode',
id: 'menu-item-mode-wysiwyg',
cmd: () => this.setMode('wysiwyg'),
showInPlainMode: true,
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m2 2v3h1v-2h2v-1zm13 0v1h2v2h1v-3zm-9 3v10h2v-4h4v4h2v-10h-2v4h-4v-4zm-4 10v3h3v-1h-2v-2zm15 0v2h-2v1h3v-3z"/></svg>'
},
{
type: 'menu_item',
label: 'Edit Source',
id: 'menu-item-mode-source',
cmd: () => this.setMode('source'),
showInPlainMode: true,
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m12 2.83c-0.478-0.138-0.976 0.141-1.11 0.619l-3.6 12.6c-0.138 0.478 0.141 0.976 0.619 1.11 0.478 0.138 0.976-0.141 1.11-0.619l3.6-12.6c0.138-0.478-0.141-0.976-0.619-1.11zm2.27 4.65 2.51 2.51-2.51 2.51c-0.352 0.352-0.352 0.923 0 1.27 0.352 0.352 0.923 0.352 1.27 0l3.15-3.15c0.352-0.352 0.352-0.923 0-1.27l-3.15-3.15c-0.352-0.352-0.923-0.352-1.27-0.00141-0.35 0.35-0.35 0.921 0.00141 1.27zm-8.63-1.27c-0.352-0.352-0.923-0.352-1.27 0l-3.15 3.15c-0.352 0.352-0.352 0.923 0 1.27l3.15 3.15c0.352 0.352 0.923 0.352 1.27 0 0.352-0.352 0.352-0.923 0-1.27l-2.51-2.51 2.51-2.51c0.352-0.352 0.352-0.923 0-1.27z"/></svg>'
},
{
type: 'menu_item',
label: 'Plain Text Mode',
id: 'menu-item-mode-plain',
cmd: () => this.setMode('plain'),
showInPlainMode: true,
icon: '<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="m2 2v3h1v-2h2v-1zm13 0v1h2v2h1v-3zm-9 3v2h3v8h2v-8h3v-2zm-4 10v3h3v-1h-2v-2zm15 0v2h-2v1h3v-3z"/></svg>'
}
]
}
];
// // clear: {
// // removeStyle: {
// // html: '⎚',
// // cmd: () => squire.setStyle()
// // }
// // }
dispatchEvent(new CustomEvent('squire2-toolbar', {
detail: {
squire: this,
actions: actions
}
}));
this.indicators = this.#addActionsToParent(actions, toolbar);
return actions;
}
#makeClr() {
/**@type {HTMLInputElement} clr*/
const clr = createElement('input');
clr.type = 'color';
// Chrome https://github.com/the-djmaze/snappymail/issues/1199
let clrid = 'squire-colors',
colorlist = doc.getElementById(clrid),
add = hex => colorlist.append(new Option(hex));
if (!colorlist) {
colorlist = createElement('datalist');
colorlist.id = clrid;
// Color blind safe Tableau 10 by Maureen Stone
add('#4E79A7');
add('#F28E2B');
add('#E15759');
add('#76B7B2');
add('#59A14F');
add('#EDC948');
add('#B07AA1');
add('#FF9DA7');
add('#9C755F');
add('#BAB0AC');
doc.body.append(colorlist);
}
clr.setAttribute('list', clrid);
return clr;
}
/**
* @param {Array} items
* @param {HTMLElement} parent
*/
#addActionsToParent(items, parent) {
const indicators = [];
items.forEach(item => {
let element, event;
switch (item.type) {
case 'group':
const group = createElement('div');
group.className = 'btn-group';
if (!item.showInPlainMode) {
group.className += ' squire-html-mode-item';
}
if (item.items) {
indicators.push(...this.#addActionsToParent(item.items, group));
}
parent.append(group);
return indicators;
case 'menu':
case 'menu_more':
const menuWrap = createElement('div');
menuWrap.className = 'btn-group dropdown squire-toolbar-menu-wrap';
menuWrap.title = item.label;
if (!item.showInPlainMode) {
menuWrap.className += ' squire-html-mode-item';
}
const menuBtn = createElement('button');
menuBtn.type = 'button';
menuBtn.className = 'btn dropdown-toggle';
if (item.icon !== '') {
menuBtn.innerHTML = item.icon;
menuBtn.firstElementChild.setAttribute('class', 'squire-toolbar-svg-icon');
} else {
menuBtn.className += ' fontastic';
menuBtn.textContent = '☰';
}
menuWrap.appendChild(menuBtn);
const menu = createElement('ul');
menu.className = 'dropdown-menu squire-toolbar-menu';
if (item.rightEdge) {
menu.className += ' right-edge';
}
menu.setAttribute('role', 'menu');
if (item.items) {
indicators.push(...this.#addActionsToParent(item.items, menu));
}
menuWrap.appendChild(menu);
parent.append(menuWrap);
ko.applyBindingAccessorsToNode(menuWrap, { registerBootstrapDropdown: true });
item.element = menuWrap;
return indicators;
case 'move_parent':
// we only move into main composer not the signature composer
if (this.container.className.indexOf('e-signature-place') === -1) {
element = doc.getElementById(item.id);
if (element) {
element.className = 'btn';
if (item.icon) {
element.innerHTML = item.icon;
element.firstElementChild.setAttribute('class', 'squire-toolbar-svg-icon');
}
if (item.label) {
element.title = item.label;
}
element.parentElement.className += ' ' + item.id + '-parent';
parent.append(element.parentElement);
}
}
return [];
case 'button':
element = createElement('button');
element.type = 'button';
element.className = 'btn';
element.innerHTML = item.icon;
element.firstElementChild.setAttribute('class', 'squire-toolbar-svg-icon');
event = 'click';
break;
case 'select':
element = createElement('select');
element.className = 'btn';
element.innerHTML = item.icon;
event = 'input';
if (Array.isArray(item.items)) {
item.items.forEach(value => {
value = Array.isArray(value) ? value : [value, value];
const option = new Option(value[0], value[1]);
option.style[item.prop] = value[1];
element.append(option);
});
} else {
Object.entries(item.items).forEach(([label, options]) => {
const optgroup = createElement('optgroup');
optgroup.label = label;
Object.entries(options).forEach(([text, value]) => {
const option = new Option(text, value);
option.style[item.prop] = value;
optgroup.append(option);
});
element.append(optgroup);
});
}
if (item.defaultValueIndex) {
element.selectedIndex = item.defaultValueIndex;
}
item.element = element;
break;
case 'menu_item':
element = createElement('li');
element.className = 'squire-toolbar-menu-item';
if (!item.showInPlainMode) {
element.className += ' squire-html-mode-item';
}
element.innerHTML = item.icon + '<span>' + item.label + '</span>';
element.firstElementChild.setAttribute('class', 'squire-toolbar-svg-icon squire-toolbar-menu-item-icon');
event = 'click';
break;
}
element.title = item.label + (item.key ? ' (' + ctrlKey + item.key + ')' : '');
element.tabIndex = -1;
element.addEventListener(event, () => item.cmd(element));
if (item.id) {
element.id = item.id;
}
if (item.matches) {
indicators.push({
element: element,
selectors: item.matches.split(',')
});
}
parent.append(element);
});
return indicators;
}
/**
* Plugins might add their own pathChange listeners therefore they should
* use this utility function. @see example below
* @param {Object} eventDetail detail of pathChange event
* @returns {Map<any, any>}
*/
buildTokensMap(eventDetail) {
if (!eventDetail.tokensMap) {
const tokensMap = new Map();
if (eventDetail.path !== '(selection)') {
window.parsel.tokenize(eventDetail.path).forEach(token => {
if (token.type === 'type') {
// token.name is a tag like B, I, UL, etc...
tokensMap.set(token.name, '1');
} else if (token.name === 'fontFamily') {
// token.value can be a string like '"LucidaSansUnicode","DejaVuSans","BitstreamVeraSans",sans-serif'
tokensMap.set('__font_family__', token.value);
} else if (token.name === 'fontSize') {
// token.value can be a string like '24px' or 'Large'
tokensMap.set('__font_size__', token.value);
}
});
} else {
tokensMap.set('__selection__', '1');
}
eventDetail.tokensMap = tokensMap;
}
return eventDetail.tokensMap;
}
doAction(name, tag) {
if (tag && this.squire.hasFormat(tag)) {
// ex: bold -> removeBold
name = 'remove' + name.charAt(0).toUpperCase() + name.slice(1);
}
this.squire[name]();
}
doList(type) {
if (this.squire.hasFormat(type)) {
this.squire.removeList();
return;
}
if (type === 'UL') {
this.squire.makeUnorderedList();
} else if (type === 'OL') {
this.squire.makeOrderedList();
}
}
changeLevel(incDec) {
const type = ['UL', 'OL'].some(listTag => this.squire.hasFormat(listTag))
? 'List'
: 'Quote';
this.squire[incDec + type + 'Level']();
}
/*
testPresenceinSelection(format, validation) {
return validation.test(this.squire.getPath()) || this.squire.hasFormat(format);
}
*/
setMode(mode) {
if (this.mode !== mode) {
let cl = this.container.classList,
source = 'source' === this.mode;
cl.remove('squire2-mode-' + this.mode);
if ('plain' === mode) {
this.plain.value = htmlToPlain(source ? this.plain.value : this.squire.getHTML(), true);
this.toolbar.classList.add('mode-plain');
} else if ('source' === mode) {
this.plain.value = this.squire.getHTML();
this.toolbar.classList.add('mode-plain');
} else {
this.setData(source ? this.plain.value : plainToHtml(this.plain.value, true));
mode = 'wysiwyg';
this.toolbar.classList.remove('mode-plain');
}
doc.getElementById('menu-item-mode-' + this.mode)?.classList.remove('active');
doc.getElementById('menu-item-mode-' + mode).classList.add('active');
this.mode = mode;
cl.add('squire2-mode-' + mode);
this.onModeChange?.();
setTimeout(() => this.focus(), 1);
}
}
on(type, fn) {
if ('mode' === type) {
this.onModeChange = fn;
} else {
this.squire.addEventListener(type, fn);
this.plain.addEventListener(type, fn);
}
}
execCommand(cmd, cfg) {
if ('insertSignature' === cmd) {
cfg = Object.assign({
clearCache: false,
isHtml: false,
insertBefore: false,
signature: ''
}, cfg);
if (cfg.clearCache) {
this._prev_txt_sig = null;
} else try {
const signature = cfg.isHtml ? htmlToPlain(cfg.signature) : cfg.signature;
if ('plain' === this.mode) {
let
text = this.plain.value,
prevSignature = this._prev_txt_sig;
if (prevSignature) {
text = text.replace(prevSignature, '').trim();
}
this.plain.value = cfg.insertBefore ? '\n\n' + signature + '\n\n' + text : text + '\n\n' + signature;
} else {
const squire = this.squire,
root = squire.getRoot(),
div = createElement('div');
div.className = 'rl-signature';
div.innerHTML = cfg.isHtml ? cfg.signature : plainToHtml(cfg.signature);
root.querySelectorAll('div.rl-signature').forEach(node => node.remove());
cfg.insertBefore ? root.prepend(div) : root.append(div);
// Move cursor above signature
for (let i = 0; i < 2; i++) {
const divbr = createElement('div');
divbr.append(createElement('br'));
div.before(divbr);
}
}
this._prev_txt_sig = signature;
} catch (e) {
console.error(e);
}
}
}
getData() {
return 'source' === this.mode ? this.plain.value : trimLines(this.squire.getHTML());
}
setData(html) {
// this.plain.value = html;
const squire = this.squire;
squire.setHTML(trimLines(html));
const node = squire.getRoot(),
range = squire.getSelection();
range.setStart(node, 0);
range.setEnd(node, 0);
squire.setSelection(range);
}
getPlainData() {
return this.plain.value;
}
setPlainData(text) {
this.plain.value = text;
}
blur() {
this.squire.blur();
}
focus() {
if ('wysiwyg' === this.mode) {
this.squire.focus();
} else {
this.plain.focus();
this.plain.setSelectionRange(0, 0);
}
}
}
})(window);

View file

@ -0,0 +1,413 @@
var parsel = (function (exports) {
'use strict';
const TOKENS = {
attribute: /\[\s*(?:(?<namespace>\*|[-\w\P{ASCII}]*)\|)?(?<name>[-\w\P{ASCII}]+)\s*(?:(?<operator>\W?=)\s*(?<value>.+?)\s*(\s(?<caseSensitive>[iIsS]))?\s*)?\]/gu,
id: /#(?<name>[-\w\P{ASCII}]+)/gu,
class: /\.(?<name>[-\w\P{ASCII}]+)/gu,
comma: /\s*,\s*/g,
combinator: /\s*[\s>+~]\s*/g,
'pseudo-element': /::(?<name>[-\w\P{ASCII}]+)(?:\((?<argument>¶*)\))?/gu,
'pseudo-class': /:(?<name>[-\w\P{ASCII}]+)(?:\((?<argument>¶*)\))?/gu,
universal: /(?:(?<namespace>\*|[-\w\P{ASCII}]*)\|)?\*/gu,
type: /(?:(?<namespace>\*|[-\w\P{ASCII}]*)\|)?(?<name>[-\w\P{ASCII}]+)/gu, // this must be last
};
const TRIM_TOKENS = new Set(['combinator', 'comma']);
const RECURSIVE_PSEUDO_CLASSES = new Set([
'not',
'is',
'where',
'has',
'matches',
'-moz-any',
'-webkit-any',
'nth-child',
'nth-last-child',
]);
const nthChildRegExp = /(?<index>[\dn+-]+)\s+of\s+(?<subtree>.+)/;
const RECURSIVE_PSEUDO_CLASSES_ARGS = {
'nth-child': nthChildRegExp,
'nth-last-child': nthChildRegExp,
};
const getArgumentPatternByType = (type) => {
switch (type) {
case 'pseudo-element':
case 'pseudo-class':
return new RegExp(TOKENS[type].source.replace('(?<argument>¶*)', '(?<argument>.*)'), 'gu');
default:
return TOKENS[type];
}
};
function gobbleParens(text, offset) {
let nesting = 0;
let result = '';
for (; offset < text.length; offset++) {
const char = text[offset];
switch (char) {
case '(':
++nesting;
break;
case ')':
--nesting;
break;
}
result += char;
if (nesting === 0) {
return result;
}
}
return result;
}
function tokenizeBy(text, grammar = TOKENS) {
if (!text) {
return [];
}
const tokens = [text];
for (const [type, pattern] of Object.entries(grammar)) {
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (typeof token !== 'string') {
continue;
}
pattern.lastIndex = 0;
const match = pattern.exec(token);
if (!match) {
continue;
}
const from = match.index - 1;
const args = [];
const content = match[0];
const before = token.slice(0, from + 1);
if (before) {
args.push(before);
}
args.push({
...match.groups,
type,
content,
});
const after = token.slice(from + content.length + 1);
if (after) {
args.push(after);
}
tokens.splice(i, 1, ...args);
}
}
let offset = 0;
for (const token of tokens) {
switch (typeof token) {
case 'string':
throw new Error(`Unexpected sequence ${token} found at index ${offset}`);
case 'object':
offset += token.content.length;
token.pos = [offset - token.content.length, offset];
if (TRIM_TOKENS.has(token.type)) {
token.content = token.content.trim() || ' ';
}
break;
}
}
return tokens;
}
const STRING_PATTERN = /(['"])([^\\\n]+?)\1/g;
const ESCAPE_PATTERN = /\\./g;
function tokenize(selector, grammar = TOKENS) {
// Prevent leading/trailing whitespaces from being interpreted as combinators
selector = selector.trim();
if (selector === '') {
return [];
}
const replacements = [];
// Replace escapes with placeholders.
selector = selector.replace(ESCAPE_PATTERN, (value, offset) => {
replacements.push({ value, offset });
return '\uE000'.repeat(value.length);
});
// Replace strings with placeholders.
selector = selector.replace(STRING_PATTERN, (value, quote, content, offset) => {
replacements.push({ value, offset });
return `${quote}${'\uE001'.repeat(content.length)}${quote}`;
});
// Replace parentheses with placeholders.
{
let pos = 0;
let offset;
while ((offset = selector.indexOf('(', pos)) > -1) {
const value = gobbleParens(selector, offset);
replacements.push({ value, offset });
selector = `${selector.substring(0, offset)}(${'¶'.repeat(value.length - 2)})${selector.substring(offset + value.length)}`;
pos = offset + value.length;
}
}
// Now we have no nested structures and we can parse with regexes
const tokens = tokenizeBy(selector, grammar);
// Replace placeholders in reverse order.
const changedTokens = new Set();
for (const replacement of replacements.reverse()) {
for (const token of tokens) {
const { offset, value } = replacement;
if (!(token.pos[0] <= offset &&
offset + value.length <= token.pos[1])) {
continue;
}
const { content } = token;
const tokenOffset = offset - token.pos[0];
token.content =
content.slice(0, tokenOffset) +
value +
content.slice(tokenOffset + value.length);
if (token.content !== content) {
changedTokens.add(token);
}
}
}
// Update changed tokens.
for (const token of changedTokens) {
const pattern = getArgumentPatternByType(token.type);
if (!pattern) {
throw new Error(`Unknown token type: ${token.type}`);
}
pattern.lastIndex = 0;
const match = pattern.exec(token.content);
if (!match) {
throw new Error(`Unable to parse content for ${token.type}: ${token.content}`);
}
Object.assign(token, match.groups);
}
return tokens;
}
/**
* Convert a flat list of tokens into a tree of complex & compound selectors
*/
function nestTokens(tokens, { list = true } = {}) {
if (list && tokens.find((t) => t.type === 'comma')) {
const selectors = [];
const temp = [];
for (let i = 0; i < tokens.length; i++) {
if (tokens[i].type === 'comma') {
if (temp.length === 0) {
throw new Error('Incorrect comma at ' + i);
}
selectors.push(nestTokens(temp, { list: false }));
temp.length = 0;
}
else {
temp.push(tokens[i]);
}
}
if (temp.length === 0) {
throw new Error('Trailing comma');
}
else {
selectors.push(nestTokens(temp, { list: false }));
}
return { type: 'list', list: selectors };
}
for (let i = tokens.length - 1; i >= 0; i--) {
let token = tokens[i];
if (token.type === 'combinator') {
let left = tokens.slice(0, i);
let right = tokens.slice(i + 1);
return {
type: 'complex',
combinator: token.content,
left: nestTokens(left),
right: nestTokens(right),
};
}
}
switch (tokens.length) {
case 0:
throw new Error('Could not build AST.');
case 1:
// If we're here, there are no combinators, so it's just a list.
return tokens[0];
default:
return {
type: 'compound',
list: [...tokens], // clone to avoid pointers messing up the AST
};
}
}
/**
* Traverse an AST in depth-first order
*/
function* flatten(node,
/**
* @internal
*/
parent) {
switch (node.type) {
case 'list':
for (let child of node.list) {
yield* flatten(child, node);
}
break;
case 'complex':
yield* flatten(node.left, node);
yield* flatten(node.right, node);
break;
case 'compound':
yield* node.list.map((token) => [token, node]);
break;
default:
yield [node, parent];
}
}
/**
* Traverse an AST (or part thereof), in depth-first order
*/
function walk(node, visit,
/**
* @internal
*/
parent) {
if (!node) {
return;
}
for (const [token, ast] of flatten(node, parent)) {
visit(token, ast);
}
}
/**
* Parse a CSS selector
*
* @param selector - The selector to parse
* @param options.recursive - Whether to parse the arguments of pseudo-classes like :is(), :has() etc. Defaults to true.
* @param options.list - Whether this can be a selector list (A, B, C etc). Defaults to true.
*/
function parse(selector, { recursive = true, list = true } = {}) {
const tokens = tokenize(selector);
if (!tokens) {
return;
}
const ast = nestTokens(tokens, { list });
if (!recursive) {
return ast;
}
for (const [token] of flatten(ast)) {
if (token.type !== 'pseudo-class' || !token.argument) {
continue;
}
if (!RECURSIVE_PSEUDO_CLASSES.has(token.name)) {
continue;
}
let argument = token.argument;
const childArg = RECURSIVE_PSEUDO_CLASSES_ARGS[token.name];
if (childArg) {
const match = childArg.exec(argument);
if (!match) {
continue;
}
Object.assign(token, match.groups);
argument = match.groups['subtree'];
}
if (!argument) {
continue;
}
Object.assign(token, {
subtree: parse(argument, {
recursive: true,
list: true,
}),
});
}
return ast;
}
/**
* Converts the given list or (sub)tree to a string.
*/
function stringify(listOrNode) {
let tokens;
if (Array.isArray(listOrNode)) {
tokens = listOrNode;
}
else {
tokens = [...flatten(listOrNode)].map(([token]) => token);
}
return tokens.map(token => token.content).join('');
}
/**
* To convert the specificity array to a number
*/
function specificityToNumber(specificity, base) {
base = base || Math.max(...specificity) + 1;
return (specificity[0] * (base << 1) + specificity[1] * base + specificity[2]);
}
/**
* Calculate specificity of a selector.
*
* If the selector is a list, the max specificity is returned.
*/
function specificity(selector) {
let ast = selector;
if (typeof ast === 'string') {
ast = parse(ast, { recursive: true });
}
if (!ast) {
return [];
}
if (ast.type === 'list' && 'list' in ast) {
let base = 10;
const specificities = ast.list.map((ast) => {
const sp = specificity(ast);
base = Math.max(base, ...specificity(ast));
return sp;
});
const numbers = specificities.map((ast) => specificityToNumber(ast, base));
return specificities[numbers.indexOf(Math.max(...numbers))];
}
const ret = [0, 0, 0];
for (const [token] of flatten(ast)) {
switch (token.type) {
case 'id':
ret[0]++;
break;
case 'class':
case 'attribute':
ret[1]++;
break;
case 'pseudo-element':
case 'type':
ret[2]++;
break;
case 'pseudo-class':
if (token.name === 'where') {
break;
}
if (!RECURSIVE_PSEUDO_CLASSES.has(token.name) ||
!token.subtree) {
ret[1]++;
break;
}
const sub = specificity(token.subtree);
sub.forEach((s, i) => (ret[i] += s));
// :nth-child() & :nth-last-child() add (0, 1, 0) to the specificity of their most complex selector
if (token.name === 'nth-child' ||
token.name === 'nth-last-child') {
ret[1]++;
}
}
}
return ret;
}
exports.RECURSIVE_PSEUDO_CLASSES = RECURSIVE_PSEUDO_CLASSES;
exports.RECURSIVE_PSEUDO_CLASSES_ARGS = RECURSIVE_PSEUDO_CLASSES_ARGS;
exports.TOKENS = TOKENS;
exports.TRIM_TOKENS = TRIM_TOKENS;
exports.flatten = flatten;
exports.gobbleParens = gobbleParens;
exports.parse = parse;
exports.specificity = specificity;
exports.specificityToNumber = specificityToNumber;
exports.stringify = stringify;
exports.tokenize = tokenize;
exports.tokenizeBy = tokenizeBy;
exports.walk = walk;
Object.defineProperty(exports, '__esModule', { value: true });
return exports;
}({}));

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View file

@ -0,0 +1,201 @@
<header class="g-ui-user-select-none" data-bind="css: {loading: saving() || sending()}">
<a class="btn" data-bind="command: sendCommand, tooltipErrorTip: sendErrorDesc, css: {'btn-success': sendButtonSuccess, 'btn-danger': sendError, 'btn-warning': sendSuccessButSaveError }">
<i data-bind="css: {'icon-paper-plane': !sending(), 'icon-spinner': sending()}"></i>
<span class="hide-mobile" data-i18n="COMPOSE/BUTTON_SEND"></span>
</a>
<a class="btn button-save" data-bind="command: saveCommand, tooltipErrorTip: savedErrorDesc, css: {'btn-danger': savedError }">
<i class="fontastic" data-bind="css: {'icon-spinner': saving()}">💾</i>
<span class="hide-mobile" data-i18n="GLOBAL/SAVE"></span>
</a>
<a class="btn btn-danger button-delete fontastic" data-bind="command: deleteCommand">🗑</a>
<span class="saved-text hide-mobile" data-bind="text: savedTimeText"></span>
<div class="pull-right">
<a class="btn hide-mobile" data-i18n="GLOBAL/BCC" data-bind="visible: !showBcc(), toggle: showBcc"></a>
<a class="btn hide-mobile" data-i18n="GLOBAL/CC" data-bind="visible: !showCc(), toggle: showCc"></a>
<a class="btn fontastic" data-bind="visible: allowContacts, command: contactsCommand" data-i18n="[title]GLOBAL/CONTACTS">📇</a>
<div class="btn-group dropdown" data-bind="registerBootstrapDropdown: true" style="display:inline-block;vertical-align:top">
<a class="btn dropdown-toggle fontastic"></a>
<menu class="dropdown-menu right-edge" role="menu">
<li data-bind="toggle: showBcc">
<a>
<i class="fontastic" data-bind="text: showBcc() ? '☑' : '☐'"></i>
<span data-i18n="GLOBAL/BCC"></span>
</a>
</li>
<li data-bind="toggle: showCc">
<a>
<i class="fontastic" data-bind="text: showCc() ? '☑' : '☐'"></i>
<span data-i18n="GLOBAL/CC"></span>
</a>
</li>
<li data-bind="toggle: showReplyTo">
<a>
<i class="fontastic" data-bind="text: showReplyTo() ? '☑' : '☐'"></i>
<span data-i18n="GLOBAL/REPLY_TO"></span>
</a>
</li>
<li data-bind="toggle: requestReadReceipt">
<a>
<i class="fontastic" data-bind="text: requestReadReceipt() ? '☑' : '☐'"></i>
<span data-i18n="COMPOSE/BUTTON_REQUEST_READ_RECEIPT"></span>
</a>
</li>
<li data-bind="toggle: requestDsn">
<a>
<i class="fontastic" data-bind="text: requestDsn() ? '☑' : '☐'"></i>
<span data-i18n="COMPOSE/BUTTON_REQUEST_DSN"></span>
</a>
</li>
<li data-bind="toggle: requireTLS">
<a>
<i class="fontastic" data-bind="text: requireTLS() ? '☑' : '☐'"></i>
<span data-i18n="COMPOSE/BUTTON_REQUIRE_TLS"></span>
</a>
</li>
<li data-bind="toggle: markAsImportant">
<a>
<i class="fontastic" data-bind="text: markAsImportant() ? '☑' : '☐'"></i>
<span data-i18n="COMPOSE/BUTTON_MARK_AS_IMPORTANT"></span>
</a>
</li>
<li data-bind="toggle: doSign, visible: canSign">
<a>
<i class="fontastic" data-bind="text: doSign() ? '☑' : '☐'"></i>
<span data-icon="✍" data-i18n="CRYPTO/SIGN"></span>
</a>
</li>
<li data-bind="toggle: doEncrypt, visible: canEncrypt">
<a>
<i class="fontastic" data-bind="text: doEncrypt() || 'mailvelope' == viewArea() ? '☑' : '☐'"></i>
<span data-icon="🔒" data-i18n="CRYPTO/ENCRYPT"></span>
</a>
</li>
</menu>
</div>
<a class="minimize-custom" data-bind="click: skipCommand" data-i18n="[title]COMPOSE/BUTTON_MINIMIZE"></a>
<a class="close" data-bind="click: tryToClose" data-i18n="[title]GLOBAL/CANCEL">×</a>
</div>
</header>
<div class="modal-body">
<div class="b-header g-ui-user-select-none">
<table>
<tr>
<td data-i18n="GLOBAL/FROM"></td>
<td>
<!-- ko if: allowIdentities -->
<input type="text" data-bind="textInput: from" style="width:calc(100% - 20px)">
<!-- /ko -->
<span class="e-identity" data-bind="hidden: allowIdentities, text: from"></span>
<!-- ko if: 1 < identitiesOptions().length -->
<div class="dropdown" style="display:inline-block" data-bind="registerBootstrapDropdown: true, initDom: identitiesMenu">
<a class="dropdown-toggle" href="#" tabindex="-1" id="identity-toggle" role="button"></a>
<menu class="dropdown-menu right-edge" role="menu" aria-labelledby="identity-toggle" data-bind="foreach: identitiesOptions">
<li role="presentation">
<a tabindex="-1" href="#" data-bind="click: function (oIdentity) { $root.selectIdentity(oIdentity); return true; }, text: optText"></a>
</li>
</menu>
</div>
<!-- /ko -->
</td>
</tr>
<tr>
<td>
<label data-bind="css: {'error-to': emptyToError}, tooltipErrorTip: emptyToErrorTooltip"
data-i18n="GLOBAL/TO"></label>
</td>
<td>
<input type="text" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" data-bind="emailsTags: to, autoCompleteSource: emailsSource">
</td>
</tr>
<tr class="cc-row" data-bind="visible: showCc">
<td data-i18n="GLOBAL/CC"></td>
<td>
<input type="text" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" data-bind="emailsTags: cc, autoCompleteSource: emailsSource">
</td>
</tr>
<tr class="bcc-row" data-bind="visible: showBcc">
<td data-i18n="GLOBAL/BCC"></td>
<td>
<input type="text" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" data-bind="emailsTags: bcc, autoCompleteSource: emailsSource">
</td>
</tr>
<tr class="reply-to-row" data-bind="visible: showReplyTo">
<td data-i18n="GLOBAL/REPLY_TO"></td>
<td>
<input type="text" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" data-bind="emailsTags: replyTo">
</td>
</tr>
<tr>
<td data-i18n="GLOBAL/SUBJECT"></td>
<td>
<input type="text" name="subject" autocomplete="off" data-bind="textInput: subject, attr:{spellcheck:allowSpellcheck()?'true':'false'}">
</td>
</tr>
</table>
<div style="display:flex">
<div class="btn-group" style="flex-grow:1"></div>
<div class="btn-group">
<a class="btn fontastic" data-bind="toggle: doSign, visible: canSign, css: {'btn-success': doSign()}" data-i18n="[title]CRYPTO/SIGN">
</a>
<a class="btn fontastic" data-bind="toggle: doEncrypt, visible: canEncrypt, css: {'btn-success': doEncrypt() || 'mailvelope' == viewArea()}" data-i18n="[title]CRYPTO/ENCRYPT">
🔒
</a>
</div>
<div class="btn-group">
<button class="btn fontastic" id="composeUploadButton"
data-bind="visible: addAttachmentEnabled()" data-i18n="[title]COMPOSE/ATTACH_FILES">
⁺📎
</button>
</div>
</div>
</div>
<div class="tabs">
<input type="radio" name="tabs" value="body" id="tab-body" data-bind="checked: viewArea">
<label for="tab-body"
role="tab"
aria-selected="true"
aria-controls="panel1"
data-bind="visible: canMailvelope"
tabindex="0">
<i class="icon-file-text"></i>
<span data-i18n="GLOBAL/TEXT"></span>
</label>
<div class="tab-content" role="tabpanel" aria-hidden="false" style="grid-column-end:3">
<div class="textAreaParent" data-bind="initDom: editorArea, attr:{spellcheck:allowSpellcheck()?'true':'false'}"></div>
</div>
<input type="radio" name="tabs" value="mailvelope" id="tab-mailvelope" data-bind="checked: viewArea">
<label for="tab-mailvelope"
role="tab"
aria-selected="false"
aria-controls="panel3"
tabindex="0"
data-bind="visible: canMailvelope">
<i class="mailvelope-icon"></i>
<span>Mailvelope</span>
</label>
<div class="tab-content textAreaParent" id="mailvelope-editor" role="tabpanel" aria-hidden="true" style="grid-column-end:3" data-bind="visible: canMailvelope"></div>
</div>
</div>
<div class="attachmentAreaParent compact">
<div class="b-attachment-place" data-bind="visible: addAttachmentEnabled(), css: {dragAndDropOver: dragAndDropVisible}"
data-i18n="COMPOSE/ATTACH_DROP_FILES_DESC"></div>
<ul class="attachmentList" data-bind="foreach: attachments">
<li class="attachmentItem" data-bind="attr: { title: title }, css: { waiting: waiting, error: '' !== error() }">
<div class="attachmentIcon">
<i class="iconMain" data-bind="css: iconClass, visible: !uploading() || 0 === progress()"></i>
<div class="iconProgress" data-bind="attr: { style: progressStyle }, visible: uploading"></div>
<div class="iconBG" data-bind="text: progressText, visible: uploading"></div>
</div>
<div class="attachmentNameParent">
<a href="#" class="close pull-right" style="margin-top:-4px;" data-bind="click: cancel">×</a>
<div class="attachmentName" data-bind="text: fileName"></div>
<span class="attachmentSize" data-bind="text: friendlySize"></span>
</div>
</li>
</ul>
</div>

View file

@ -1,4 +1,4 @@
<?php
getEmailAddressDomain<?php
class LdapContactsSuggestions implements \RainLoop\Providers\Suggestions\ISuggestions
{
@ -93,17 +93,17 @@ class LdapContactsSuggestions implements \RainLoop\Providers\Suggestions\ISugges
return $aResult;
}
$sDomain = \MailSo\Base\Utils::GetDomainFromEmail($oAccount->Email());
$sDomain = \MailSo\Base\Utils::getEmailAddressDomain($oAccount->Email());
$sBaseDn = \strtr($this->sBaseDn, array(
'{domain}' => $sDomain,
'{domain:dc}' => 'dc='.\strtr($sDomain, array('.' => ',dc=')),
'{email}' => $oAccount->Email(),
'{email:user}' => \MailSo\Base\Utils::GetAccountNameFromEmail($oAccount->Email()),
'{email:user}' => \MailSo\Base\Utils::getEmailAddressLocalPart($oAccount->Email()),
'{email:domain}' => $sDomain,
'{login}' => $oAccount->IncLogin(),
'{imap:login}' => $oAccount->IncLogin(),
'{imap:host}' => $oAccount->Domain()->IncHost(),
'{imap:port}' => $oAccount->Domain()->IncPort()
'{imap:host}' => $oAccount->Domain()->ImapSettings()->host,
'{imap:port}' => $oAccount->Domain()->ImapSettings()->port
));
$aObjectClasses = empty($this->sObjectClasses) ? array() : \explode(',', $this->sObjectClasses);

View file

@ -4,9 +4,9 @@ class LdapContactsSuggestionsPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Contacts suggestions (LDAP)',
VERSION = '2.13',
RELEASE = '2023-10-01',
REQUIRED = '2.23.0',
VERSION = '2.14',
RELEASE = '2024-03-12',
REQUIRED = '2.35.3',
CATEGORY = 'Contacts',
DESCRIPTION = 'Get contacts suggestions from LDAP.';

View file

@ -8,10 +8,10 @@ class LDAPLoginMappingPlugin extends AbstractPlugin
{
const
NAME = 'LDAP login mapping',
VERSION = '2.1',
VERSION = '2.2',
AUTHOR = 'RainLoop Team, Ludovic Pouzenc<ludovic@pouzenc.fr>, ZephOne<zephone@protonmail.com>',
RELEASE = '2023-01-19',
REQUIRED = '2.19.2',
RELEASE = '2024-03-12',
REQUIRED = '2.35.3',
CATEGORY = 'Login',
DESCRIPTION = 'Enable custom mapping using ldap field';
/**
@ -175,7 +175,7 @@ class LDAPLoginMappingPlugin extends AbstractPlugin
'LDAP');
return FALSE;
}
$sLogin = \MailSo\Base\Utils::GetAccountNameFromEmail($sEmail);
$sLogin = \MailSo\Base\Utils::getEmailAddressLocalPart($sEmail);
$this->oLogger->Write('ldap_connect: trying...', \LOG_INFO, 'LDAP');

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