From 21c0c1ed5d26b82349ed58d0ab755385f53d4a0b Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Fri, 25 Nov 2022 11:00:20 +0100 Subject: [PATCH 01/11] Move some cPanel code to /usr/local/cpanel/base/3rdparty/snappymail/include.php #697 --- build/cpanel.php | 3 +- .../base/3rdparty/snappymail/include.php | 37 +++++++++++++++++++ plugins/login-remote/index.php | 2 +- snappymail/v/0.0.0/cpanel.php | 2 +- snappymail/v/0.0.0/include.php | 8 +--- 5 files changed, 41 insertions(+), 11 deletions(-) create mode 100644 integrations/cpanel/usr/local/cpanel/base/3rdparty/snappymail/include.php diff --git a/build/cpanel.php b/build/cpanel.php index 2718b75aa..d8280d9e4 100755 --- a/build/cpanel.php +++ b/build/cpanel.php @@ -21,9 +21,8 @@ foreach ($files as $file) { } $cpanel_tar->addFile('.htaccess', "{$cpanel_path}.htaccess"); -//$cpanel_tar->addFile('_include.php', 'snappymail/app/_include.php'); $cpanel_tar->addFile('README.md', "{$cpanel_path}README.md"); -$cpanel_tar->addFromString('VERSION', $package->version); +$cpanel_tar->addFromString("{$cpanel_path}VERSION", $package->version); $index = file_get_contents('index.php'); $index = str_replace('0.0.0', $package->version, $index); diff --git a/integrations/cpanel/usr/local/cpanel/base/3rdparty/snappymail/include.php b/integrations/cpanel/usr/local/cpanel/base/3rdparty/snappymail/include.php new file mode 100644 index 000000000..0135d1f8d --- /dev/null +++ b/integrations/cpanel/usr/local/cpanel/base/3rdparty/snappymail/include.php @@ -0,0 +1,37 @@ +Set('plugins', 'enabled_list', \implode(',', \array_unique($aList))); $oConfig->Set('login', 'default_domain', 'cpanel'); $oConfig->Set('logs', 'path', $_ENV['HOME'] . '/logs/snappymail'); - $oConfig->Set('cache', 'path', $_ENV['HOME'] . '/tmp/snappymail'); + $oConfig->Set('cache', 'path', $_ENV['TMPDIR'] . '/snappymail'); $oConfig->Save(); $sFile = APP_PRIVATE_DATA.'domains/cpanel.json'; diff --git a/snappymail/v/0.0.0/include.php b/snappymail/v/0.0.0/include.php index f1577d535..00acfa5c7 100644 --- a/snappymail/v/0.0.0/include.php +++ b/snappymail/v/0.0.0/include.php @@ -70,13 +70,7 @@ define('APP_PRIVATE_DATA_NAME', $sPrivateDataFolderInternalName ?: '_default_'); unset($sPrivateDataFolderInternalName); if (!defined('APP_DATA_FOLDER_PATH')) { - // cPanel https://github.com/the-djmaze/snappymail/issues/697 - if (!empty($_ENV['CPANEL']) && isset($_ENV['HOME'])) { - $sCustomDataPath = $_ENV['HOME'] . '/var/snappymail'; -// $sCustomDataPath = $_ENV['TMPDIR'] . '/snappymail'; - } else { - $sCustomDataPath = function_exists('__get_custom_data_full_path') ? rtrim(trim(__get_custom_data_full_path()), '\\/') : $sCustomDataPath; - } + $sCustomDataPath = rtrim(trim(function_exists('__get_custom_data_full_path') ? __get_custom_data_full_path() : $sCustomDataPath), '\\/'); define('APP_DATA_FOLDER_PATH', strlen($sCustomDataPath) ? $sCustomDataPath.'/' : APP_INDEX_ROOT_PATH.'data/'); } unset($sCustomDataPath); From 33cb705b07fe9e365359d87fc5c6fcb75bc45d76 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Fri, 25 Nov 2022 12:29:00 +0100 Subject: [PATCH 02/11] Update to OpenPGP.js v5.5.0 --- vendors/openpgp-5/dist/openpgp.js | 739 ++++++++++++++++-------------- 1 file changed, 405 insertions(+), 334 deletions(-) diff --git a/vendors/openpgp-5/dist/openpgp.js b/vendors/openpgp-5/dist/openpgp.js index c7858bd32..28b80bc47 100644 --- a/vendors/openpgp-5/dist/openpgp.js +++ b/vendors/openpgp-5/dist/openpgp.js @@ -1,4 +1,4 @@ -/*! OpenPGP.js v5.2.1 - 2022-03-30 - this is LGPL licensed code, see LICENSE/our website https://openpgpjs.org/ for more information. */ +/*! OpenPGP.js v5.5.0 - 2022-11-25 - this is LGPL licensed code, see LICENSE/our website https://openpgpjs.org/ for more information. */ var openpgp = (function (exports) { 'use strict'; @@ -2637,7 +2637,10 @@ var openpgp = (function (exports) { */ allowUnauthenticatedMessages: false, /** - * Allow streaming unauthenticated data before its integrity has been checked. + * Allow streaming unauthenticated data before its integrity has been checked. This would allow the application to + * process large streams while limiting memory usage by releasing the decrypted chunks as soon as possible + * and deferring checking their integrity until the decrypted stream has been read in full. + * * This setting is **insecure** if the partially decrypted message is processed further or displayed to the user. * @memberof module:config * @property {Boolean} allowUnauthenticatedStream @@ -2733,7 +2736,7 @@ var openpgp = (function (exports) { * @memberof module:config * @property {String} versionString A version string to be included in armored messages */ - versionString: 'OpenPGP.js 5.2.1', + versionString: 'OpenPGP.js 5.5.0', /** * @memberof module:config * @property {String} commentString A comment string to be included in armored messages @@ -2784,7 +2787,7 @@ var openpgp = (function (exports) { * @memberof module:config * @property {Set} rejectCurves {@link module:enums.curve} */ - rejectCurves: new Set([enums.curve.brainpoolP256r1, enums.curve.brainpoolP384r1, enums.curve.brainpoolP512r1, enums.curve.secp256k1]) + rejectCurves: new Set([enums.curve.secp256k1]) }; // GPG4Browsers - An OpenPGP implementation in javascript @@ -2950,8 +2953,10 @@ var openpgp = (function (exports) { } /** - * Verify armored headers. RFC4880, section 6.3: "OpenPGP should consider improperly formatted - * Armor Headers to be corruption of the ASCII Armor." + * Verify armored headers. crypto-refresh-06, section 6.2: + * "An OpenPGP implementation may consider improperly formatted Armor + * Headers to be corruption of the ASCII Armor, but SHOULD make an + * effort to recover." * @private * @param {Array} headers - Armor headers */ @@ -3152,7 +3157,7 @@ var openpgp = (function (exports) { result.push('-----END PGP MESSAGE, PART ' + partIndex + '-----\n'); break; case enums.armor.signed: - result.push('\n-----BEGIN PGP SIGNED MESSAGE-----\n'); + result.push('-----BEGIN PGP SIGNED MESSAGE-----\n'); result.push('Hash: ' + hash + '\n\n'); result.push(text.replace(/^-/mg, '- -')); result.push('\n-----BEGIN PGP SIGNATURE-----\n'); @@ -11750,6 +11755,301 @@ var openpgp = (function (exports) { return new elliptic$1.ec(name); } + // GPG4Browsers - An OpenPGP implementation in javascript + + function readSimpleLength(bytes) { + let len = 0; + let offset; + const type = bytes[0]; + + + if (type < 192) { + [len] = bytes; + offset = 1; + } else if (type < 255) { + len = ((bytes[0] - 192) << 8) + (bytes[1]) + 192; + offset = 2; + } else if (type === 255) { + len = util.readNumber(bytes.subarray(1, 1 + 4)); + offset = 5; + } + + return { + len: len, + offset: offset + }; + } + + /** + * Encodes a given integer of length to the openpgp length specifier to a + * string + * + * @param {Integer} length - The length to encode + * @returns {Uint8Array} String with openpgp length representation. + */ + function writeSimpleLength(length) { + if (length < 192) { + return new Uint8Array([length]); + } else if (length > 191 && length < 8384) { + /* + * let a = (total data packet length) - 192 let bc = two octet + * representation of a let d = b + 192 + */ + return new Uint8Array([((length - 192) >> 8) + 192, (length - 192) & 0xFF]); + } + return util.concatUint8Array([new Uint8Array([255]), util.writeNumber(length, 4)]); + } + + function writePartialLength(power) { + if (power < 0 || power > 30) { + throw new Error('Partial Length power must be between 1 and 30'); + } + return new Uint8Array([224 + power]); + } + + function writeTag(tag_type) { + /* we're only generating v4 packet headers here */ + return new Uint8Array([0xC0 | tag_type]); + } + + /** + * Writes a packet header version 4 with the given tag_type and length to a + * string + * + * @param {Integer} tag_type - Tag type + * @param {Integer} length - Length of the payload + * @returns {String} String of the header. + */ + function writeHeader(tag_type, length) { + /* we're only generating v4 packet headers here */ + return util.concatUint8Array([writeTag(tag_type), writeSimpleLength(length)]); + } + + /** + * Whether the packet type supports partial lengths per RFC4880 + * @param {Integer} tag - Tag type + * @returns {Boolean} String of the header. + */ + function supportsStreaming(tag) { + return [ + enums.packet.literalData, + enums.packet.compressedData, + enums.packet.symmetricallyEncryptedData, + enums.packet.symEncryptedIntegrityProtectedData, + enums.packet.aeadEncryptedData + ].includes(tag); + } + + /** + * Generic static Packet Parser function + * + * @param {Uint8Array | ReadableStream} input - Input stream as string + * @param {Function} callback - Function to call with the parsed packet + * @returns {Boolean} Returns false if the stream was empty and parsing is done, and true otherwise. + */ + async function readPackets(input, callback) { + const reader = getReader(input); + let writer; + let callbackReturned; + try { + const peekedBytes = await reader.peekBytes(2); + // some sanity checks + if (!peekedBytes || peekedBytes.length < 2 || (peekedBytes[0] & 0x80) === 0) { + throw new Error('Error during parsing. This message / key probably does not conform to a valid OpenPGP format.'); + } + const headerByte = await reader.readByte(); + let tag = -1; + let format = -1; + let packetLength; + + format = 0; // 0 = old format; 1 = new format + if ((headerByte & 0x40) !== 0) { + format = 1; + } + + let packetLengthType; + if (format) { + // new format header + tag = headerByte & 0x3F; // bit 5-0 + } else { + // old format header + tag = (headerByte & 0x3F) >> 2; // bit 5-2 + packetLengthType = headerByte & 0x03; // bit 1-0 + } + + const packetSupportsStreaming = supportsStreaming(tag); + let packet = null; + if (packetSupportsStreaming) { + if (util.isStream(input) === 'array') { + const arrayStream = new ArrayStream(); + writer = getWriter(arrayStream); + packet = arrayStream; + } else { + const transform = new TransformStream$1(); + writer = getWriter(transform.writable); + packet = transform.readable; + } + callbackReturned = callback({ tag, packet }); + } else { + packet = []; + } + + let wasPartialLength; + do { + if (!format) { + // 4.2.1. Old Format Packet Lengths + switch (packetLengthType) { + case 0: + // The packet has a one-octet length. The header is 2 octets + // long. + packetLength = await reader.readByte(); + break; + case 1: + // The packet has a two-octet length. The header is 3 octets + // long. + packetLength = (await reader.readByte() << 8) | await reader.readByte(); + break; + case 2: + // The packet has a four-octet length. The header is 5 + // octets long. + packetLength = (await reader.readByte() << 24) | (await reader.readByte() << 16) | (await reader.readByte() << + 8) | await reader.readByte(); + break; + default: + // 3 - The packet is of indeterminate length. The header is 1 + // octet long, and the implementation must determine how long + // the packet is. If the packet is in a file, this means that + // the packet extends until the end of the file. In general, + // an implementation SHOULD NOT use indeterminate-length + // packets except where the end of the data will be clear + // from the context, and even then it is better to use a + // definite length, or a new format header. The new format + // headers described below have a mechanism for precisely + // encoding data of indeterminate length. + packetLength = Infinity; + break; + } + } else { // 4.2.2. New Format Packet Lengths + // 4.2.2.1. One-Octet Lengths + const lengthByte = await reader.readByte(); + wasPartialLength = false; + if (lengthByte < 192) { + packetLength = lengthByte; + // 4.2.2.2. Two-Octet Lengths + } else if (lengthByte >= 192 && lengthByte < 224) { + packetLength = ((lengthByte - 192) << 8) + (await reader.readByte()) + 192; + // 4.2.2.4. Partial Body Lengths + } else if (lengthByte > 223 && lengthByte < 255) { + packetLength = 1 << (lengthByte & 0x1F); + wasPartialLength = true; + if (!packetSupportsStreaming) { + throw new TypeError('This packet type does not support partial lengths.'); + } + // 4.2.2.3. Five-Octet Lengths + } else { + packetLength = (await reader.readByte() << 24) | (await reader.readByte() << 16) | (await reader.readByte() << + 8) | await reader.readByte(); + } + } + if (packetLength > 0) { + let bytesRead = 0; + while (true) { + if (writer) await writer.ready; + const { done, value } = await reader.read(); + if (done) { + if (packetLength === Infinity) break; + throw new Error('Unexpected end of packet'); + } + const chunk = packetLength === Infinity ? value : value.subarray(0, packetLength - bytesRead); + if (writer) await writer.write(chunk); + else packet.push(chunk); + bytesRead += value.length; + if (bytesRead >= packetLength) { + reader.unshift(value.subarray(packetLength - bytesRead + value.length)); + break; + } + } + } + } while (wasPartialLength); + + // If this was not a packet that "supports streaming", we peek to check + // whether it is the last packet in the message. We peek 2 bytes instead + // of 1 because the beginning of this function also peeks 2 bytes, and we + // want to cut a `subarray` of the correct length into `web-stream-tools`' + // `externalBuffer` as a tiny optimization here. + // + // If it *was* a streaming packet (i.e. the data packets), we peek at the + // entire remainder of the stream, in order to forward errors in the + // remainder of the stream to the packet data. (Note that this means we + // read/peek at all signature packets before closing the literal data + // packet, for example.) This forwards MDC errors to the literal data + // stream, for example, so that they don't get lost / forgotten on + // decryptedMessage.packets.stream, which we never look at. + // + // An example of what we do when stream-parsing a message containing + // [ one-pass signature packet, literal data packet, signature packet ]: + // 1. Read the one-pass signature packet + // 2. Peek 2 bytes of the literal data packet + // 3. Parse the one-pass signature packet + // + // 4. Read the literal data packet, simultaneously stream-parsing it + // 5. Peek until the end of the message + // 6. Finish parsing the literal data packet + // + // 7. Read the signature packet again (we already peeked at it in step 5) + // 8. Peek at the end of the stream again (`peekBytes` returns undefined) + // 9. Parse the signature packet + // + // Note that this means that if there's an error in the very end of the + // stream, such as an MDC error, we throw in step 5 instead of in step 8 + // (or never), which is the point of this exercise. + const nextPacket = await reader.peekBytes(packetSupportsStreaming ? Infinity : 2); + if (writer) { + await writer.ready; + await writer.close(); + } else { + packet = util.concatUint8Array(packet); + await callback({ tag, packet }); + } + return !nextPacket || !nextPacket.length; + } catch (e) { + if (writer) { + await writer.abort(e); + return true; + } else { + throw e; + } + } finally { + if (writer) { + await callbackReturned; + } + reader.releaseLock(); + } + } + + class UnsupportedError extends Error { + constructor(...params) { + super(...params); + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, UnsupportedError); + } + + this.name = 'UnsupportedError'; + } + } + + class UnparseablePacket { + constructor(tag, rawContent) { + this.tag = tag; + this.rawContent = rawContent; + } + + write() { + return this.rawContent; + } + } + // OpenPGP.js - An OpenPGP implementation in javascript const webCrypto$6 = util.getWebCrypto(); @@ -11846,7 +12146,7 @@ var openpgp = (function (exports) { // by curve name or oid string this.name = enums.write(enums.curve, oidOrName); } catch (err) { - throw new Error('Not valid curve'); + throw new UnsupportedError('Unknown curve'); } params = params || curves[this.name]; @@ -13233,7 +13533,7 @@ var openpgp = (function (exports) { oid, kdfParams, V, C.data, Q, d, fingerprint); } default: - throw new Error('Invalid public key encryption algorithm.'); + throw new Error('Unknown public key encryption algorithm.'); } } @@ -13268,23 +13568,26 @@ var openpgp = (function (exports) { } case enums.publicKey.ecdsa: { const oid = new OID(); read += oid.read(bytes); + checkSupportedCurve(oid); const Q = util.readMPI(bytes.subarray(read)); read += Q.length + 2; return { read: read, publicParams: { oid, Q } }; } case enums.publicKey.eddsa: { const oid = new OID(); read += oid.read(bytes); + checkSupportedCurve(oid); let Q = util.readMPI(bytes.subarray(read)); read += Q.length + 2; Q = util.leftPad(Q, 33); return { read: read, publicParams: { oid, Q } }; } case enums.publicKey.ecdh: { const oid = new OID(); read += oid.read(bytes); + checkSupportedCurve(oid); const Q = util.readMPI(bytes.subarray(read)); read += Q.length + 2; const kdfParams = new KDFParams(); read += kdfParams.read(bytes.subarray(read)); return { read: read, publicParams: { oid, Q, kdfParams } }; } default: - throw new Error('Invalid public key encryption algorithm.'); + throw new UnsupportedError('Unknown public key encryption algorithm.'); } } @@ -13320,12 +13623,13 @@ var openpgp = (function (exports) { return { read, privateParams: { d } }; } case enums.publicKey.eddsa: { + const curve = new Curve(publicParams.oid); let seed = util.readMPI(bytes.subarray(read)); read += seed.length + 2; - seed = util.leftPad(seed, 32); + seed = util.leftPad(seed, curve.payloadSize); return { read, privateParams: { seed } }; } default: - throw new Error('Invalid public key encryption algorithm.'); + throw new UnsupportedError('Unknown public key encryption algorithm.'); } } @@ -13362,7 +13666,7 @@ var openpgp = (function (exports) { return { V, C }; } default: - throw new Error('Invalid public key encryption algorithm.'); + throw new UnsupportedError('Unknown public key encryption algorithm.'); } } @@ -13421,7 +13725,7 @@ var openpgp = (function (exports) { case enums.publicKey.elgamal: throw new Error('Unsupported algorithm for key generation.'); default: - throw new Error('Invalid public key algorithm.'); + throw new Error('Unknown public key algorithm.'); } } @@ -13468,7 +13772,7 @@ var openpgp = (function (exports) { return publicKey.elliptic.eddsa.validateParams(oid, Q, seed); } default: - throw new Error('Invalid public key algorithm.'); + throw new Error('Unknown public key algorithm.'); } } @@ -13520,6 +13824,19 @@ var openpgp = (function (exports) { return cipher[algoName]; } + /** + * Check whether the given curve OID is supported + * @param {module:type/oid} oid - EC object identifier + * @throws {UnsupportedError} if curve is not supported + */ + function checkSupportedCurve(oid) { + try { + oid.getName(); + } catch (e) { + throw new UnsupportedError('Unknown curve OID'); + } + } + var crypto$2 = /*#__PURE__*/Object.freeze({ __proto__: null, publicKeyEncrypt: publicKeyEncrypt, @@ -14628,7 +14945,7 @@ var openpgp = (function (exports) { return { r, s }; } default: - throw new Error('Invalid signature algorithm.'); + throw new UnsupportedError('Unknown signature algorithm.'); } } @@ -14674,7 +14991,7 @@ var openpgp = (function (exports) { return publicKey.elliptic.eddsa.verify(oid, hashAlgo, signature, data, Q, hashed); } default: - throw new Error('Invalid signature algorithm.'); + throw new Error('Unknown signature algorithm.'); } } @@ -14724,7 +15041,7 @@ var openpgp = (function (exports) { return publicKey.elliptic.eddsa.sign(oid, hashAlgo, data, Q, seed, hashed); } default: - throw new Error('Invalid signature algorithm.'); + throw new Error('Unknown signature algorithm.'); } } @@ -21899,290 +22216,6 @@ var openpgp = (function (exports) { // GPG4Browsers - An OpenPGP implementation in javascript - function readSimpleLength(bytes) { - let len = 0; - let offset; - const type = bytes[0]; - - - if (type < 192) { - [len] = bytes; - offset = 1; - } else if (type < 255) { - len = ((bytes[0] - 192) << 8) + (bytes[1]) + 192; - offset = 2; - } else if (type === 255) { - len = util.readNumber(bytes.subarray(1, 1 + 4)); - offset = 5; - } - - return { - len: len, - offset: offset - }; - } - - /** - * Encodes a given integer of length to the openpgp length specifier to a - * string - * - * @param {Integer} length - The length to encode - * @returns {Uint8Array} String with openpgp length representation. - */ - function writeSimpleLength(length) { - if (length < 192) { - return new Uint8Array([length]); - } else if (length > 191 && length < 8384) { - /* - * let a = (total data packet length) - 192 let bc = two octet - * representation of a let d = b + 192 - */ - return new Uint8Array([((length - 192) >> 8) + 192, (length - 192) & 0xFF]); - } - return util.concatUint8Array([new Uint8Array([255]), util.writeNumber(length, 4)]); - } - - function writePartialLength(power) { - if (power < 0 || power > 30) { - throw new Error('Partial Length power must be between 1 and 30'); - } - return new Uint8Array([224 + power]); - } - - function writeTag(tag_type) { - /* we're only generating v4 packet headers here */ - return new Uint8Array([0xC0 | tag_type]); - } - - /** - * Writes a packet header version 4 with the given tag_type and length to a - * string - * - * @param {Integer} tag_type - Tag type - * @param {Integer} length - Length of the payload - * @returns {String} String of the header. - */ - function writeHeader(tag_type, length) { - /* we're only generating v4 packet headers here */ - return util.concatUint8Array([writeTag(tag_type), writeSimpleLength(length)]); - } - - /** - * Whether the packet type supports partial lengths per RFC4880 - * @param {Integer} tag - Tag type - * @returns {Boolean} String of the header. - */ - function supportsStreaming(tag) { - return [ - enums.packet.literalData, - enums.packet.compressedData, - enums.packet.symmetricallyEncryptedData, - enums.packet.symEncryptedIntegrityProtectedData, - enums.packet.aeadEncryptedData - ].includes(tag); - } - - /** - * Generic static Packet Parser function - * - * @param {Uint8Array | ReadableStream} input - Input stream as string - * @param {Function} callback - Function to call with the parsed packet - * @returns {Boolean} Returns false if the stream was empty and parsing is done, and true otherwise. - */ - async function readPackets(input, callback) { - const reader = getReader(input); - let writer; - let callbackReturned; - try { - const peekedBytes = await reader.peekBytes(2); - // some sanity checks - if (!peekedBytes || peekedBytes.length < 2 || (peekedBytes[0] & 0x80) === 0) { - throw new Error('Error during parsing. This message / key probably does not conform to a valid OpenPGP format.'); - } - const headerByte = await reader.readByte(); - let tag = -1; - let format = -1; - let packetLength; - - format = 0; // 0 = old format; 1 = new format - if ((headerByte & 0x40) !== 0) { - format = 1; - } - - let packetLengthType; - if (format) { - // new format header - tag = headerByte & 0x3F; // bit 5-0 - } else { - // old format header - tag = (headerByte & 0x3F) >> 2; // bit 5-2 - packetLengthType = headerByte & 0x03; // bit 1-0 - } - - const packetSupportsStreaming = supportsStreaming(tag); - let packet = null; - if (packetSupportsStreaming) { - if (util.isStream(input) === 'array') { - const arrayStream = new ArrayStream(); - writer = getWriter(arrayStream); - packet = arrayStream; - } else { - const transform = new TransformStream$1(); - writer = getWriter(transform.writable); - packet = transform.readable; - } - callbackReturned = callback({ tag, packet }); - } else { - packet = []; - } - - let wasPartialLength; - do { - if (!format) { - // 4.2.1. Old Format Packet Lengths - switch (packetLengthType) { - case 0: - // The packet has a one-octet length. The header is 2 octets - // long. - packetLength = await reader.readByte(); - break; - case 1: - // The packet has a two-octet length. The header is 3 octets - // long. - packetLength = (await reader.readByte() << 8) | await reader.readByte(); - break; - case 2: - // The packet has a four-octet length. The header is 5 - // octets long. - packetLength = (await reader.readByte() << 24) | (await reader.readByte() << 16) | (await reader.readByte() << - 8) | await reader.readByte(); - break; - default: - // 3 - The packet is of indeterminate length. The header is 1 - // octet long, and the implementation must determine how long - // the packet is. If the packet is in a file, this means that - // the packet extends until the end of the file. In general, - // an implementation SHOULD NOT use indeterminate-length - // packets except where the end of the data will be clear - // from the context, and even then it is better to use a - // definite length, or a new format header. The new format - // headers described below have a mechanism for precisely - // encoding data of indeterminate length. - packetLength = Infinity; - break; - } - } else { // 4.2.2. New Format Packet Lengths - // 4.2.2.1. One-Octet Lengths - const lengthByte = await reader.readByte(); - wasPartialLength = false; - if (lengthByte < 192) { - packetLength = lengthByte; - // 4.2.2.2. Two-Octet Lengths - } else if (lengthByte >= 192 && lengthByte < 224) { - packetLength = ((lengthByte - 192) << 8) + (await reader.readByte()) + 192; - // 4.2.2.4. Partial Body Lengths - } else if (lengthByte > 223 && lengthByte < 255) { - packetLength = 1 << (lengthByte & 0x1F); - wasPartialLength = true; - if (!packetSupportsStreaming) { - throw new TypeError('This packet type does not support partial lengths.'); - } - // 4.2.2.3. Five-Octet Lengths - } else { - packetLength = (await reader.readByte() << 24) | (await reader.readByte() << 16) | (await reader.readByte() << - 8) | await reader.readByte(); - } - } - if (packetLength > 0) { - let bytesRead = 0; - while (true) { - if (writer) await writer.ready; - const { done, value } = await reader.read(); - if (done) { - if (packetLength === Infinity) break; - throw new Error('Unexpected end of packet'); - } - const chunk = packetLength === Infinity ? value : value.subarray(0, packetLength - bytesRead); - if (writer) await writer.write(chunk); - else packet.push(chunk); - bytesRead += value.length; - if (bytesRead >= packetLength) { - reader.unshift(value.subarray(packetLength - bytesRead + value.length)); - break; - } - } - } - } while (wasPartialLength); - - // If this was not a packet that "supports streaming", we peek to check - // whether it is the last packet in the message. We peek 2 bytes instead - // of 1 because the beginning of this function also peeks 2 bytes, and we - // want to cut a `subarray` of the correct length into `web-stream-tools`' - // `externalBuffer` as a tiny optimization here. - // - // If it *was* a streaming packet (i.e. the data packets), we peek at the - // entire remainder of the stream, in order to forward errors in the - // remainder of the stream to the packet data. (Note that this means we - // read/peek at all signature packets before closing the literal data - // packet, for example.) This forwards MDC errors to the literal data - // stream, for example, so that they don't get lost / forgotten on - // decryptedMessage.packets.stream, which we never look at. - // - // An example of what we do when stream-parsing a message containing - // [ one-pass signature packet, literal data packet, signature packet ]: - // 1. Read the one-pass signature packet - // 2. Peek 2 bytes of the literal data packet - // 3. Parse the one-pass signature packet - // - // 4. Read the literal data packet, simultaneously stream-parsing it - // 5. Peek until the end of the message - // 6. Finish parsing the literal data packet - // - // 7. Read the signature packet again (we already peeked at it in step 5) - // 8. Peek at the end of the stream again (`peekBytes` returns undefined) - // 9. Parse the signature packet - // - // Note that this means that if there's an error in the very end of the - // stream, such as an MDC error, we throw in step 5 instead of in step 8 - // (or never), which is the point of this exercise. - const nextPacket = await reader.peekBytes(packetSupportsStreaming ? Infinity : 2); - if (writer) { - await writer.ready; - await writer.close(); - } else { - packet = util.concatUint8Array(packet); - await callback({ tag, packet }); - } - return !nextPacket || !nextPacket.length; - } catch (e) { - if (writer) { - await writer.abort(e); - return true; - } else { - throw e; - } - } finally { - if (writer) { - await callbackReturned; - } - reader.releaseLock(); - } - } - - class UnsupportedError extends Error { - constructor(...params) { - super(...params); - - if (Error.captureStackTrace) { - Error.captureStackTrace(this, UnsupportedError); - } - - this.name = 'UnsupportedError'; - } - } - - // GPG4Browsers - An OpenPGP implementation in javascript - // Symbol to store cryptographic validity of the signature, to avoid recomputing multiple times on verification. const verified = Symbol('verified'); @@ -22348,6 +22381,11 @@ var openpgp = (function (exports) { // Add hashed subpackets arr.push(this.writeHashedSubPackets()); + // Remove unhashed subpackets, in case some allowed unhashed + // subpackets existed, in order not to duplicate them (in both + // the hashed and unhashed subpackets) when re-signing. + this.unhashedSubpackets = []; + this.signatureData = util.concat(arr); const toHash = this.toHash(this.signatureType, data, detached); @@ -22410,6 +22448,11 @@ var openpgp = (function (exports) { bytes = util.concat([bytes, this.revocationKeyFingerprint]); arr.push(writeSubPacket(sub.revocationKey, bytes)); } + if (!this.issuerKeyID.isNull() && this.issuerKeyVersion !== 5) { + // If the version of [the] key is greater than 4, this subpacket + // MUST NOT be included in the signature. + arr.push(writeSubPacket(sub.issuer, this.issuerKeyID.write())); + } this.rawNotations.forEach(([{ name, value, humanReadable }]) => { bytes = [new Uint8Array([humanReadable ? 0x80 : 0, 0, 0, 0])]; // 2 octets of name length @@ -22463,6 +22506,14 @@ var openpgp = (function (exports) { bytes = util.concat(bytes); arr.push(writeSubPacket(sub.signatureTarget, bytes)); } + if (this.embeddedSignature !== null) { + arr.push(writeSubPacket(sub.embeddedSignature, this.embeddedSignature.write())); + } + if (this.issuerFingerprint !== null) { + bytes = [new Uint8Array([this.issuerKeyVersion]), this.issuerFingerprint]; + bytes = util.concat(bytes); + arr.push(writeSubPacket(sub.issuerFingerprint, bytes)); + } if (this.preferredAEADAlgorithms !== null) { bytes = util.stringToUint8Array(util.uint8ArrayToString(this.preferredAEADAlgorithms)); arr.push(writeSubPacket(sub.preferredAEADAlgorithms, bytes)); @@ -22475,26 +22526,11 @@ var openpgp = (function (exports) { } /** - * Creates Uint8Array of bytes of Issuer and Embedded Signature subpackets + * Creates an Uint8Array containing the unhashed subpackets * @returns {Uint8Array} Subpacket data. */ writeUnhashedSubPackets() { - const sub = enums.signatureSubpacket; const arr = []; - let bytes; - if (!this.issuerKeyID.isNull() && this.issuerKeyVersion !== 5) { - // If the version of [the] key is greater than 4, this subpacket - // MUST NOT be included in the signature. - arr.push(writeSubPacket(sub.issuer, this.issuerKeyID.write())); - } - if (this.embeddedSignature !== null) { - arr.push(writeSubPacket(sub.embeddedSignature, this.embeddedSignature.write())); - } - if (this.issuerFingerprint !== null) { - bytes = [new Uint8Array([this.issuerKeyVersion]), this.issuerFingerprint]; - bytes = util.concat(bytes); - arr.push(writeSubPacket(sub.issuerFingerprint, bytes)); - } this.unhashedSubpackets.forEach(data => { arr.push(writeSimpleLength(data.length)); arr.push(data); @@ -22514,9 +22550,11 @@ var openpgp = (function (exports) { const critical = bytes[mypos] & 0x80; const type = bytes[mypos] & 0x7F; - if (!hashed && !allowedUnhashedSubpackets.has(type)) { + if (!hashed) { this.unhashedSubpackets.push(bytes.subarray(mypos, bytes.length)); - return; + if (!allowedUnhashedSubpackets.has(type)) { + return; + } } mypos++; @@ -23127,6 +23165,9 @@ var openpgp = (function (exports) { // Those are also the ones we want to be more strict about and throw on parse errors // (since we likely cannot process the message without these packets anyway). await writer.abort(e); + } else { + const unparsedPacket = new UnparseablePacket(parsed.tag, parsed.packet); + await writer.write(unparsedPacket); } console.error(e); } @@ -23167,12 +23208,13 @@ var openpgp = (function (exports) { const arr = []; for (let i = 0; i < this.length; i++) { + const tag = this[i] instanceof UnparseablePacket ? this[i].tag : this[i].constructor.tag; const packetbytes = this[i].write(); if (util.isStream(packetbytes) && supportsStreaming(this[i].constructor.tag)) { let buffer = []; let bufferLength = 0; const minLength = 512; - arr.push(writeTag(this[i].constructor.tag)); + arr.push(writeTag(tag)); arr.push(transform(packetbytes, value => { buffer.push(value); bufferLength += value.length; @@ -23190,9 +23232,9 @@ var openpgp = (function (exports) { let length = 0; arr.push(transform(clone(packetbytes), value => { length += value.length; - }, () => writeHeader(this[i].constructor.tag, length))); + }, () => writeHeader(tag, length))); } else { - arr.push(writeHeader(this[i].constructor.tag, packetbytes.length)); + arr.push(writeHeader(tag, packetbytes.length)); } arr.push(packetbytes); } @@ -24282,13 +24324,9 @@ var openpgp = (function (exports) { } // - A series of values comprising the key material. - try { - const { read, publicParams } = mod.parsePublicKeyParams(this.algorithm, bytes.subarray(pos)); - this.publicParams = publicParams; - pos += read; - } catch (err) { - throw new Error('Error reading MPIs'); - } + const { read, publicParams } = mod.parsePublicKeyParams(this.algorithm, bytes.subarray(pos)); + this.publicParams = publicParams; + pos += read; // we set the fingerprint and keyID already to make it possible to put together the key packets directly in the Key constructor await this.computeFingerprintAndKeyID(); @@ -24825,6 +24863,8 @@ var openpgp = (function (exports) { const { privateParams } = mod.parsePrivateKeyParams(this.algorithm, cleartext, this.publicParams); this.privateParams = privateParams; } catch (err) { + if (err instanceof UnsupportedError) throw err; + // avoid throwing potentially sensitive errors throw new Error('Error reading MPIs'); } } @@ -26722,7 +26762,7 @@ var openpgp = (function (exports) { try { options.curve = enums.write(enums.curve, options.curve); } catch (e) { - throw new Error('Invalid curve'); + throw new Error('Unknown curve'); } if (options.curve === enums.curve.ed25519 || options.curve === enums.curve.curve25519) { options.curve = options.sign ? enums.curve.ed25519 : enums.curve.curve25519; @@ -27244,6 +27284,11 @@ var openpgp = (function (exports) { // A key revocation certificate can contain the following packets const allowedRevocationPackets = /*#__PURE__*/ util.constructAllowedPackets([SignaturePacket]); + const mainKeyPacketTags = new Set([enums.packet.publicKey, enums.packet.privateKey]); + const keyPacketTags = new Set([ + enums.packet.publicKey, enums.packet.privateKey, + enums.packet.publicSubkey, enums.packet.privateSubkey + ]); /** * Abstract class that represents an OpenPGP key. Must contain a primary key. @@ -27264,8 +27309,29 @@ var openpgp = (function (exports) { let user; let primaryKeyID; let subkey; + let ignoreUntil; + for (const packet of packetlist) { + + if (packet instanceof UnparseablePacket) { + const isUnparseableKeyPacket = keyPacketTags.has(packet.tag); + if (isUnparseableKeyPacket && !ignoreUntil){ + // Since non-key packets apply to the preceding key packet, if a (sub)key is Unparseable we must + // discard all non-key packets that follow, until another (sub)key packet is found. + if (mainKeyPacketTags.has(packet.tag)) { + ignoreUntil = mainKeyPacketTags; + } else { + ignoreUntil = keyPacketTags; + } + } + continue; + } + const tag = packet.constructor.tag; + if (ignoreUntil) { + if (!ignoreUntil.has(tag)) continue; + ignoreUntil = null; + } if (disallowedPackets.has(tag)) { throw new Error(`Unexpected packet type: ${tag}`); } @@ -28669,7 +28735,7 @@ var openpgp = (function (exports) { ); if (symEncryptedPacketlist.length === 0) { - return this; + throw new Error('No encrypted data found'); } const symEncryptedPacket = symEncryptedPacketlist[0]; @@ -29496,7 +29562,7 @@ var openpgp = (function (exports) { * @param {Signature} signature - The detached signature or an empty signature for unsigned messages */ constructor(text, signature) { - // normalize EOL to canonical form + // remove trailing whitespace and normalize EOL to canonical form this.text = util.removeTrailingSpaces(text).replace(/\r?\n/g, '\r\n'); if (signature && !(signature instanceof Signature$2)) { throw new Error('Invalid signature input'); @@ -29896,7 +29962,7 @@ var openpgp = (function (exports) { /** - * Encrypts a message using public keys, passwords or both at once. At least one of `encryptionKeys` or `passwords` + * Encrypts a message using public keys, passwords or both at once. At least one of `encryptionKeys`, `passwords` or `sessionKeys` * must be specified. If signing keys are specified, those will be used to sign the message. * @param {Object} options * @param {Message} options.message - Message to be encrypted as created by {@link createMessage} @@ -30211,6 +30277,10 @@ var openpgp = (function (exports) { if (rest.publicKeys) throw new Error('The `publicKeys` option has been removed from openpgp.encryptSessionKey, pass `encryptionKeys` instead'); const unknownOptions = Object.keys(rest); if (unknownOptions.length > 0) throw new Error(`Unknown option: ${unknownOptions.join(', ')}`); + if ((!encryptionKeys || encryptionKeys.length === 0) && (!passwords || passwords.length === 0)) { + throw new Error('No encryption keys or passwords provided.'); + } + try { const message = await Message.encryptSessionKey(data, algorithm, aeadAlgorithm, encryptionKeys, passwords, wildcard, encryptionKeyIDs, date, encryptionUserIDs, config); return formatObject(message, format, config); @@ -42906,6 +42976,7 @@ var openpgp = (function (exports) { exports.SymEncryptedSessionKeyPacket = SymEncryptedSessionKeyPacket; exports.SymmetricallyEncryptedDataPacket = SymmetricallyEncryptedDataPacket; exports.TrustPacket = TrustPacket; + exports.UnparseablePacket = UnparseablePacket; exports.UserAttributePacket = UserAttributePacket; exports.UserIDPacket = UserIDPacket; exports.armor = armor; From ad4159fd2ceeecb7ee0917d91b2565ef797e9f99 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Fri, 25 Nov 2022 15:52:46 +0100 Subject: [PATCH 03/11] Resolve #707 --- dev/External/User/ko.js | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/dev/External/User/ko.js b/dev/External/User/ko.js index c20c06af9..e09b1bccb 100644 --- a/dev/External/User/ko.js +++ b/dev/External/User/ko.js @@ -29,37 +29,39 @@ const rlContentType = 'snappymail/action', }, dragTimer = { - id: 0, - stop: () => clearTimeout(dragTimer.id), - start: fn => dragTimer.id = setTimeout(fn, 500) + id: 0 }, - fnStop = (e, element) => { + dragStop = (e, element) => { e.preventDefault(); - element.classList.remove('droppableHover'); - dragTimer.stop(); + element?.classList.remove('droppableHover'); + if (dragTimer.node == element) { + dragTimer.node = null; + clearTimeout(dragTimer.id); + } }, - fnHover = (e, element, folder) => { + dragEnter = (e, element, folder) => { let files = false; // if (e.dataTransfer.types.includes('Files')) for (const item of e.dataTransfer.items) { files |= 'file' === item.kind && 'message/rfc822' === item.type; } if (files || dragMessages()) { - fnStop(e, element); - files && e.stopPropagation(); + e.stopPropagation(); + dragStop(e, dragTimer.node); e.dataTransfer.dropEffect = files ? 'copy' : (e.ctrlKey ? 'copy' : 'move'); element.classList.add('droppableHover'); if (folder.collapsed()) { - dragTimer.start(() => { + dragTimer.node = element; + dragTimer.id = setTimeout(() => { folder.collapsed(false); setExpandedFolder(folder.fullName, true); }, 500); } } }, - fnDrop = (e, element, folder, dragData) => { - fnStop(e, element); + dragDrop = (e, element, folder, dragData) => { + dragStop(e, element); if (dragMessages() && 'copyMove' == e.dataTransfer.effectAllowed) { moveMessagesToFolder(FolderUserStore.currentFolderFullName(), dragData.data, folder.fullName, e.ctrlKey); } else if (e.dataTransfer.types.includes('Files')) { @@ -160,10 +162,10 @@ Object.assign(ko.bindingHandlers, { init: (element, fValueAccessor) => { const folder = fValueAccessor(); // ko.dataFor(element) folder && addEventsListeners(element, { - dragenter: e => fnHover(e, element, folder), - dragover: e => fnHover(e, element, folder), - dragleave: e => fnStop(e, element), - drop: e => fnDrop(e, element, folder, dragData) + dragenter: e => dragEnter(e, element, folder), +// dragover: e => dragOver(e, element, folder), + dragleave: e => dragStop(e, element), + drop: e => dragDrop(e, element, folder, dragData) }); } }, From 3799a107a2287f0c2c46df4c78f910c324a9804a Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Fri, 25 Nov 2022 16:24:49 +0100 Subject: [PATCH 04/11] Make resolveLoginCredentials() public for #705 --- snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php index ebb1ffe65..6c7b858a4 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php @@ -21,7 +21,7 @@ trait UserAuth /** * @throws \RainLoop\Exceptions\ClientException */ - protected function resolveLoginCredentials(string &$sEmail, string &$sPassword, string &$sLogin): void + public function resolveLoginCredentials(string &$sEmail, string &$sPassword, string &$sLogin): void { $this->Plugins()->RunHook('login.credentials.step-1', array(&$sEmail)); From a34041c2dd54d677d83566761fab55a8880ec493 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Fri, 25 Nov 2022 16:33:08 +0100 Subject: [PATCH 05/11] v2.22.3 --- .../files/usr/local/include/application.ini | 2 +- CHANGELOG.md | 17 ++++++++++++++++ README.md | 20 +++++++++---------- integrations/cloudron/DESCRIPTION.md | 2 +- integrations/cloudron/Dockerfile | 2 +- .../nextcloud/snappymail/appinfo/info.xml | 2 +- integrations/virtualmin/snappymail.pl | 2 +- package.json | 2 +- plugins/nextcloud/index.php | 4 ++-- release.php | 13 ++++++++++-- 10 files changed, 46 insertions(+), 20 deletions(-) diff --git a/.docker/release/files/usr/local/include/application.ini b/.docker/release/files/usr/local/include/application.ini index 1f5ed1023..36d814643 100644 --- a/.docker/release/files/usr/local/include/application.ini +++ b/.docker/release/files/usr/local/include/application.ini @@ -289,5 +289,5 @@ dev_email = "" dev_password = "" [version] -current = "2.22.2" +current = "2.22.3" saved = "Fri, 30 Sep 2022 12:34:56 +0000" diff --git a/CHANGELOG.md b/CHANGELOG.md index f9c1d1058..d2dd2edac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +## 2.22.3 – 2022-11-25 + +### Added +- application.ini config logs.path and cache.path to improve custom data structure. + +### Changed +- Improved cPanel integration + [#697](https://github.com/the-djmaze/snappymail/pull/697) +- Update to OpenPGP.js v5.5.0 + +### Fixed +- drag & drop folder expansion #707 + [#707](https://github.com/the-djmaze/snappymail/pull/707) +- Save selected messages as .eml in Nextcloud failed + [#704](https://github.com/the-djmaze/snappymail/pull/704) + + ## 2.22.2 – 2022-11-24 ### Added diff --git a/README.md b/README.md index f9bc9a61e..ff6b4eaa0 100644 --- a/README.md +++ b/README.md @@ -142,24 +142,24 @@ RainLoop 1.17 vs SnappyMail |js/* |RainLoop |Snappy | |--------------- |--------: |--------: | |admin.js |2.170.153 | 82.419 | -|app.js |4.207.787 | 407.238 | +|app.js |4.207.787 | 407.335 | |boot.js | 868.735 | 1.989 | |libs.js | 658.812 | 193.075 | |sieve.js | 0 | 86.121 | |polyfills.js | 334.608 | 0 | |serviceworker.js | 0 | 285 | -|TOTAL |8.240.095 | 771.127 | +|TOTAL |8.240.095 | 771.224 | |js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli | |--------------- |--------: |--------: |------: |------: |--------: |--------: | |admin.min.js | 256.831 | 40.933 | 73.606 | 13.575 | 60.877 | 12.185 | -|app.min.js | 515.367 | 188.830 |139.456 | 62.816 |110.485 | 53.944 | +|app.min.js | 515.367 | 188.819 |139.456 | 62.830 |110.485 | 53.925 | |boot.min.js | 84.659 | 1.216 | 26.998 | 761 | 23.643 | 614 | |libs.min.js | 584.772 | 92.084 |180.901 | 34.119 |155.182 | 30.621 | |sieve.min.js | 0 | 41.926 | 0 | 10.484 | 0 | 9.451 | |polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 | -|TOTAL user |1.217.635 | 282.130 |358.761 | 97.696 |299.485 | 85.179 | -|TOTAL user+sieve |1.217.635 | 324.056 |358.761 |108.180 |299.485 | 94.630 | +|TOTAL user |1.217.635 | 282.119 |358.761 | 97.710 |299.485 | 85.160 | +|TOTAL user+sieve |1.217.635 | 324.045 |358.761 |108.194 |299.485 | 94.611 | |TOTAL admin | 959.099 | 134.233 |292.911 | 48.455 |249.877 | 43.420 | For a user its around 70% smaller and faster than traditional RainLoop. @@ -189,12 +189,12 @@ For a user its around 70% smaller and faster than traditional RainLoop. |css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli | |------------ |-------: |------: |------: |------: |--------: | -|app.css | 340.331 | 82.425 | 46.946 | 17.049 | 14.595 | -|app.min.css | 274.947 | 66.502 | 39.647 | 15.049 | 13.221 | +|app.css | 340.331 | 82.471 | 46.946 | 17.107 | 14.645 | +|app.min.css | 274.947 | 66.548 | 39.647 | 15.117 | 13.260 | |boot.css | | 1.326 | | 664 | 545 | |boot.min.css | | 1.071 | | 590 | 474 | -|admin.css | | 29.807 | | 6.744 | 5.851 | -|admin.min.css | | 24.223 | | 6.142 | 5.397 | +|admin.css | | 29.853 | | 6.812 | 5.933 | +|admin.min.css | | 24.269 | | 6.210 | 5.480 | ### PGP RainLoop uses the old OpenPGP.js v2 @@ -208,7 +208,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 | 539.257 |102.388 | 166.998 | 84.241 | 137.186 | +|openpgp.min.js | 330.742 | 539.988 |102.388 | 167.371 | 84.241 | 137.413 | |openpgp.worker | 1.499 | | 824 | | 695 | | diff --git a/integrations/cloudron/DESCRIPTION.md b/integrations/cloudron/DESCRIPTION.md index a182909e6..4ca3ec11e 100644 --- a/integrations/cloudron/DESCRIPTION.md +++ b/integrations/cloudron/DESCRIPTION.md @@ -1,4 +1,4 @@ -This app packages SnappyMail 2.22.2. +This app packages SnappyMail 2.22.3. SnappyMail is a simple, modern, lightweight & fast web-based email client. diff --git a/integrations/cloudron/Dockerfile b/integrations/cloudron/Dockerfile index d1d70f1ad..da746c635 100644 --- a/integrations/cloudron/Dockerfile +++ b/integrations/cloudron/Dockerfile @@ -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.22.2 +VERSION=2.22.3 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 && \ diff --git a/integrations/nextcloud/snappymail/appinfo/info.xml b/integrations/nextcloud/snappymail/appinfo/info.xml index 5f121a00f..dfb335d19 100644 --- a/integrations/nextcloud/snappymail/appinfo/info.xml +++ b/integrations/nextcloud/snappymail/appinfo/info.xml @@ -3,7 +3,7 @@ snappymail SnappyMail SnappyMail Webmail - 2.22.2 + 2.22.3 agpl SnappyMail, RainLoop Team, Nextgen-Networks, Tab Fitts, Nathan Kinkade, Pierre-Alain Bandinelli version}/"; is_dir($destPath) || mkdir($destPath, 0777, true); @@ -263,6 +268,10 @@ if (isset($options['sign'])) { passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign ' .escapeshellarg("{$destPath}snappymail-{$package->version}-nextcloud.tar.gz"), $return_var); } + if (isset($options['cpanel'])) { + passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign ' + .escapeshellarg("{$destPath}snappymail-{$package->version}-cpanel.tar.gz"), $return_var); + } if (isset($options['debian'])) { passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign ' . escapeshellarg(__DIR__ . "/build/dist/releases/webmail/{$package->version}/" . basename(DEB_DEST_DIR.'.deb')), $return_var); From ebaa181fd3a09c90a765f5103b3d959cc0ffb982 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Fri, 25 Nov 2022 22:51:30 +0100 Subject: [PATCH 06/11] Resolve #709 --- snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php index 9e2c6c203..a3593db44 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php @@ -510,10 +510,8 @@ class Actions switch (true) { default: case $bForceFile: - $sCacheDir = \trim($this->oConfig->Get('cache', 'path', '')) ?: APP_PRIVATE_DATA . 'cache'; - \is_dir($sCacheDir) || \mkdir($sCacheDir, 0700, true); $oDriver = new \MailSo\Cache\Drivers\File( - \trim($this->oConfig->Get('cache', 'path', APP_PRIVATE_DATA . 'cache')), + \trim($this->oConfig->Get('cache', 'path', '')) ?: APP_PRIVATE_DATA . 'cache', $sKey ); break; From de7d1a82c3771017f0d0fc984da3c9815ae80b4e Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Sat, 26 Nov 2022 10:41:18 +0100 Subject: [PATCH 07/11] Resolve #710 --- dev/External/User/ko.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/External/User/ko.js b/dev/External/User/ko.js index e09b1bccb..194f1b1af 100644 --- a/dev/External/User/ko.js +++ b/dev/External/User/ko.js @@ -163,7 +163,7 @@ Object.assign(ko.bindingHandlers, { const folder = fValueAccessor(); // ko.dataFor(element) folder && addEventsListeners(element, { dragenter: e => dragEnter(e, element, folder), -// dragover: e => dragOver(e, element, folder), + dragover: e => e.preventDefault(), dragleave: e => dragStop(e, element), drop: e => dragDrop(e, element, folder, dragData) }); From a085bbb51d37ed5ab2377d8a9e67e749a5fbca00 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Sat, 26 Nov 2022 10:42:39 +0100 Subject: [PATCH 08/11] Small change based on comments of #355 --- dev/View/Popup/Compose.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/View/Popup/Compose.js b/dev/View/Popup/Compose.js index d17098331..8862785fe 100644 --- a/dev/View/Popup/Compose.js +++ b/dev/View/Popup/Compose.js @@ -942,7 +942,7 @@ export class ComposePopupView extends AbstractViewPopup { this.editor(editor => { encrypted || editor.setHtml(sText); - if (encrypted || isPlainEditor() || !message.isHtml()) { + if (encrypted || isPlainEditor()) { editor.modePlain(); } encrypted && editor.setPlain(sText); From e5ed52b79ea375e91e12ef04165de8fda87646d1 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Sun, 27 Nov 2022 16:05:44 +0100 Subject: [PATCH 09/11] Changes for #714 --- dev/Knoin/AbstractModel.js | 1 + plugins/avatars/avatars.js | 7 ++- plugins/avatars/index.php | 47 ++++++++++++++----- .../libraries/MailSo/Cache/Drivers/File.php | 26 ++++------ .../0.0.0/app/libraries/RainLoop/Actions.php | 13 ++--- 5 files changed, 55 insertions(+), 39 deletions(-) diff --git a/dev/Knoin/AbstractModel.js b/dev/Knoin/AbstractModel.js index f98deade1..fe5ebab81 100644 --- a/dev/Knoin/AbstractModel.js +++ b/dev/Knoin/AbstractModel.js @@ -105,6 +105,7 @@ export class AbstractModel { // fall through case 'undefined': default: + this[key] = value; // console.log((typeof this[key])+' '+(model.name)+'.'+key+' not revived'); } } catch (e) { diff --git a/plugins/avatars/avatars.js b/plugins/avatars/avatars.js index ba872301e..43a9cec42 100644 --- a/plugins/avatars/avatars.js +++ b/plugins/avatars/avatars.js @@ -1,5 +1,4 @@ (rl => { -// if (rl.settings.get('Nextcloud')) const queue = [], avatars = new Map, @@ -45,6 +44,9 @@ fn = url=>{element.src = url}; if (url) { fn(url); + } else if (msg.avatar) { + let bimi = 'pass' == msg.from[0].dkimStatus ? 1 : 0; + fn(`?Avatar/${bimi}/${msg.avatar}`); } else { queue.push([msg, fn]); runQueue(); @@ -80,6 +82,9 @@ }; if (url) { fn(url); + } else if (msg.avatar) { + let bimi = 'pass' == msg.from[0].dkimStatus ? 1 : 0; + fn(`?Avatar/${bimi}/${msg.avatar}`); } else { // let from = msg.from[0], bimi = 'pass' == from.dkimStatus ? 1 : 0; // view.viewUserPic(`?Avatar/${bimi}/${encodeURIComponent(from.email)}`); diff --git a/plugins/avatars/index.php b/plugins/avatars/index.php index fb3145bf9..8c82541e9 100644 --- a/plugins/avatars/index.php +++ b/plugins/avatars/index.php @@ -3,10 +3,10 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin { const - NAME = 'Avatar', + NAME = 'Avatars', AUTHOR = 'SnappyMail', URL = 'https://snappymail.eu/', - VERSION = '1.0', + VERSION = '1.1', RELEASE = '2022-11-23', REQUIRED = '2.22.0', CATEGORY = 'Contacts', @@ -19,6 +19,29 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin $this->addJs('avatars.js'); $this->addJsonHook('Avatar', 'DoAvatar'); $this->addPartHook('Avatar', 'ServiceAvatar'); + // TODO: https://github.com/the-djmaze/snappymail/issues/714 +// $this->addHook('filter.json-response', 'FilterJsonResponse'); + } + + public function FilterJsonResponse(string $sAction, array &$aResponseItem) + { + if ('MessageList' === $sAction && !empty($aResponseItem['Result']['@Collection'])) { + foreach ($aResponseItem['Result']['@Collection'] as $id => $message) { + $aResponseItem['Result']['@Collection'][$id]['Avatar'] = static::encryptFrom($message['From'][0]); + } + } else if ('Message' === $sAction && !empty($aResponseItem['Result']['From'])) { + $aResponseItem['Result']['Avatar'] = static::encryptFrom($aResponseItem['Result']['From'][0]); + } + } + + private static function encryptFrom($mFrom) + { + if ($mFrom instanceof \MailSo\Mime\Email) { + $mFrom = $mFrom->jsonSerialize(); + } + return \is_array($mFrom) + ? \SnappyMail\Crypt::EncryptUrlSafe($mFrom['Email']) + : null; } /** @@ -28,7 +51,7 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin { $bBimi = !empty($this->jsonParam('bimi')); $sEmail = $this->jsonParam('email'); - $aResult = $this->getAvatar(\urldecode($sEmail), !empty($sEmail)); + $aResult = $this->getAvatar($sEmail, !empty($bBimi)); if ($aResult) { $aResult = [ 'type' => $aResult[0], @@ -39,15 +62,17 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin } /** - * GET /?Avatar/${bimi}/${from.email} - * Not fond of this idea because email address is exposed - * Maybe use btoa(from.email) or Crypto.subtle.encrypt({name:'AES-GCM',iv:''}, token, from.email) + * GET /?Avatar/${bimi}/Encrypted(${from.email}) + * Nextcloud Mail uses insecure unencrypted 'index.php/apps/mail/api/avatars/url/local%40example.com' */ // public function ServiceAvatar(...$aParts) public function ServiceAvatar(string $sServiceName, string $sBimi, string $sEmail) { - $aResult = $this->getAvatar(\urldecode($sEmail), !empty($sEmail)); - if ($aResult) { + $sEmail = \SnappyMail\Crypt::DecryptUrlSafe($sEmail); + $oActions = \RainLoop\Api::Actions(); + $oActions->verifyCacheByKey($sEmail, true); + if ($sEmail && ($aResult = $this->getAvatar($sEmail, !empty($sBimi)))) { + $oActions->Http()->ServerUseCache($oActions->etag($sEmail), \time(), \time() + 86400); \header('Content-Type: '.$aResult[0]); echo $aResult[1]; return true; @@ -74,7 +99,7 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin } $oActions = \RainLoop\Api::Actions(); - $oActions->verifyCacheByKey($sEmail); +// $oActions->verifyCacheByKey($sEmail, true); $aResult = null; @@ -88,7 +113,7 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin \mime_content_type($aFiles[0]), \file_get_contents($aFiles[0]) ]; - $oActions->cacheByKey($sEmail); +// $oActions->Http()->ServerUseCache($oActions->etag($sEmail), \time(), \time() + 86400); return $aResult; } @@ -167,7 +192,7 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin } } - $oActions->cacheByKey($sEmail); +// $oActions->Http()->ServerUseCache($oActions->etag($sEmail), \time(), \time() + 86400); return $aResult; } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/File.php b/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/File.php index 0c32dc0e6..ed54f97f0 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/File.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Cache/Drivers/File.php @@ -18,30 +18,20 @@ namespace MailSo\Cache\Drivers; */ class File implements \MailSo\Cache\DriverInterface { - /** - * @var string - */ - private $sCacheFolder; + private string $sCacheFolder; - /** - * @var string - */ - private $sKeyPrefix; + private string $sKeyPrefix = ''; function __construct(string $sCacheFolder, string $sKeyPrefix = '') { - $this->sCacheFolder = $sCacheFolder; - $this->sCacheFolder = rtrim(trim($this->sCacheFolder), '\\/').'/'; - - $this->sKeyPrefix = $sKeyPrefix; - if (!empty($this->sKeyPrefix)) - { - $this->sKeyPrefix = \str_pad(\preg_replace('/[^a-zA-Z0-9_]/', '_', - rtrim(trim($this->sKeyPrefix), '\\/')), 5, '_'); + $this->sCacheFolder = \rtrim(\trim($sCacheFolder), '\\/').'/'; + if (!empty($sKeyPrefix)) { + $sKeyPrefix = \str_pad(\preg_replace('/[^a-zA-Z0-9_]/', '_', + \rtrim(\trim($sKeyPrefix), '\\/')), 5, '_'); $this->sKeyPrefix = '__/'. - \substr($this->sKeyPrefix, 0, 2).'/'.\substr($this->sKeyPrefix, 2, 2).'/'. - $this->sKeyPrefix.'/'; + \substr($sKeyPrefix, 0, 2).'/'.\substr($sKeyPrefix, 2, 2).'/'. + $sKeyPrefix.'/'; } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php index a3593db44..2b880b930 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php @@ -1067,25 +1067,20 @@ class Actions public function cacheByKey(string $sKey, bool $bForce = false): bool { - $bResult = false; if (!empty($sKey) && ($bForce || ($this->oConfig->Get('cache', 'enable', true) && $this->oConfig->Get('cache', 'http', true)))) { $iExpires = $this->oConfig->Get('cache', 'http_expires', 3600); if (0 < $iExpires) { $this->Http()->ServerUseCache($this->etag($sKey), 1382478804, \time() + $iExpires); - $bResult = true; + return true; } } - - if (!$bResult) { - $this->Http()->ServerNoCache(); - } - - return $bResult; + $this->Http()->ServerNoCache(); + return false; } public function verifyCacheByKey(string $sKey, bool $bForce = false): void { - if (!empty($sKey) && ($bForce || $this->oConfig->Get('cache', 'enable', true) && $this->oConfig->Get('cache', 'http', true))) { + if (!empty($sKey) && ($bForce || ($this->oConfig->Get('cache', 'enable', true) && $this->oConfig->Get('cache', 'http', true)))) { $sIfNoneMatch = $this->Http()->GetHeader('If-None-Match', ''); if ($this->etag($sKey) === $sIfNoneMatch) { \MailSo\Base\Http::StatusHeader(304); From b89d194863e4946988c0fb1d1463a676631678e9 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 28 Nov 2022 10:09:24 +0100 Subject: [PATCH 10/11] Resolve #714 --- plugins/avatars/avatars.js | 4 +- plugins/avatars/index.php | 38 ++--- .../0.0.0/app/libraries/MailSo/Base/Http.php | 140 +++++++++--------- .../0.0.0/app/libraries/RainLoop/Actions.php | 29 ++-- .../RainLoop/Actions/Localization.php | 2 +- .../app/libraries/RainLoop/Actions/Raw.php | 2 +- 6 files changed, 106 insertions(+), 109 deletions(-) diff --git a/plugins/avatars/avatars.js b/plugins/avatars/avatars.js index 43a9cec42..6a1ccd56f 100644 --- a/plugins/avatars/avatars.js +++ b/plugins/avatars/avatars.js @@ -64,7 +64,7 @@ if (messageItemHeader) { messageItemHeader.prepend(Element.fromHTML( - `` + `` )); } @@ -98,7 +98,7 @@ if ('MailMessageList' === e.detail.viewModelTemplateID) { document.getElementById('MailMessageList').content.querySelector('.messageCheckbox') - .append(Element.fromHTML(``)); + .append(Element.fromHTML(``)); } }); diff --git a/plugins/avatars/index.php b/plugins/avatars/index.php index 8c82541e9..fcf762763 100644 --- a/plugins/avatars/index.php +++ b/plugins/avatars/index.php @@ -7,8 +7,8 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin AUTHOR = 'SnappyMail', URL = 'https://snappymail.eu/', VERSION = '1.1', - RELEASE = '2022-11-23', - REQUIRED = '2.22.0', + RELEASE = '2022-11-27', + REQUIRED = '2.22.4', CATEGORY = 'Contacts', LICENSE = 'MIT', DESCRIPTION = 'Show photo of sender in message and messages list (supports BIMI and Gravatar, Contacts is still TODO)'; @@ -19,8 +19,9 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin $this->addJs('avatars.js'); $this->addJsonHook('Avatar', 'DoAvatar'); $this->addPartHook('Avatar', 'ServiceAvatar'); - // TODO: https://github.com/the-djmaze/snappymail/issues/714 -// $this->addHook('filter.json-response', 'FilterJsonResponse'); + // https://github.com/the-djmaze/snappymail/issues/714 + $this->Config()->Get('plugin', 'delay', true) + || $this->addHook('filter.json-response', 'FilterJsonResponse'); } public function FilterJsonResponse(string $sAction, array &$aResponseItem) @@ -69,10 +70,7 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin public function ServiceAvatar(string $sServiceName, string $sBimi, string $sEmail) { $sEmail = \SnappyMail\Crypt::DecryptUrlSafe($sEmail); - $oActions = \RainLoop\Api::Actions(); - $oActions->verifyCacheByKey($sEmail, true); if ($sEmail && ($aResult = $this->getAvatar($sEmail, !empty($sBimi)))) { - $oActions->Http()->ServerUseCache($oActions->etag($sEmail), \time(), \time() + 86400); \header('Content-Type: '.$aResult[0]); echo $aResult[1]; return true; @@ -83,6 +81,9 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin protected function configMapping() : array { return array( + \RainLoop\Plugins\Property::NewInstance('delay')->SetLabel('Delay loading') + ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) + ->SetDefaultValue(true), \RainLoop\Plugins\Property::NewInstance('bimi')->SetLabel('Use BIMI (https://bimigroup.org/)') ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) ->SetDefaultValue(false), @@ -98,28 +99,30 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin return null; } - $oActions = \RainLoop\Api::Actions(); -// $oActions->verifyCacheByKey($sEmail, true); + $sAsciiEmail = \mb_strtolower(\MailSo\Base\Utils::IdnToAscii($sEmail, true)); + $sEmailId = \sha1($sAsciiEmail); + + \MailSo\Base\Http::setETag($sEmailId); + \header('Cache-Control: private'); +// \header('Expires: '.\gmdate('D, j M Y H:i:s', \time() + 86400).' UTC'); $aResult = null; - $sAsciiEmail = \MailSo\Base\Utils::IdnToAscii($sEmail, true); - $sEmailId = \sha1(\strtolower($sAsciiEmail)); - $sFile = \APP_PRIVATE_DATA . 'avatars/' . $sEmailId; $aFiles = \glob("{$sFile}.*"); if ($aFiles) { + \MailSo\Base\Http::setLastModified(\filemtime($aFiles[0])); $aResult = [ \mime_content_type($aFiles[0]), \file_get_contents($aFiles[0]) ]; -// $oActions->Http()->ServerUseCache($oActions->etag($sEmail), \time(), \time() + 86400); return $aResult; } // TODO: lookup contacts vCard and return PHOTO value /* if (!$aResult) { + $oActions = \RainLoop\Api::Actions(); $oAccount = $oActions->getAccountFromToken(); if ($oAccount) { $oAddressBookProvider = $oActions->AddressBookProvider($oAccount); @@ -172,6 +175,7 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin $sFile . \SnappyMail\File\MimeType::toExtension($aResult[0]), $aResult[1] ); + \MailSo\Base\Http::setLastModified(\time()); } if (!$aResult) { @@ -182,18 +186,18 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin 'empty-contact' // DATA_IMAGE_USER_DOT_PIC ]; foreach ($aServices as $service) { - if (\file_exists(__DIR__ . "/images/{$service}.png")) { + $file = __DIR__ . "/images/{$service}.png"; + if (\file_exists($file)) { + \MailSo\Base\Http::setLastModified(\filemtime($file)); $aResult = [ 'image/png', - \file_get_contents(__DIR__ . "/images/{$service}.png") + \file_get_contents($file) ]; break; } } } -// $oActions->Http()->ServerUseCache($oActions->etag($sEmail), \time(), \time() + 86400); - return $aResult; } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Http.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Http.php index 16ee13cfe..5ac3fa106 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Http.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Http.php @@ -36,14 +36,14 @@ class Http * * @return mixed */ - public function GetServer(string $sKey, $mDefault = null) + public static function GetServer(string $sKey, $mDefault = null) { return isset($_SERVER[$sKey]) ? $_SERVER[$sKey] : $mDefault; } public function GetMethod() : string { - return $this->GetServer('REQUEST_METHOD', ''); + return static::GetServer('REQUEST_METHOD', ''); } public function IsPost() : bool @@ -67,7 +67,7 @@ class Http { if (empty($sValueToCheck)) { - $sValueToCheck = $this->GetServer('REMOTE_ADDR', ''); + $sValueToCheck = static::GetServer('REMOTE_ADDR', ''); } return $this->CheckLocalhost($sValueToCheck); @@ -84,21 +84,16 @@ class Http return $sRawBody; } - public function GetHeader(string $sHeader) : string + public static function GetHeader(string $sHeader) : string { $sServerKey = 'HTTP_'.\strtoupper(\str_replace('-', '_', $sHeader)); - $sResultHeader = $this->GetServer($sServerKey, ''); - - if (0 === \strlen($sResultHeader) && - \MailSo\Base\Utils::FunctionCallable('apache_request_headers')) - { + $sResultHeader = static::GetServer($sServerKey, ''); + if (0 === \strlen($sResultHeader) && \MailSo\Base\Utils::FunctionCallable('apache_request_headers')) { $sHeaders = \apache_request_headers(); - if (isset($sHeaders[$sHeader])) - { + if (isset($sHeaders[$sHeader])) { $sResultHeader = $sHeaders[$sHeader]; } } - return $sResultHeader; } @@ -109,15 +104,15 @@ class Http public function IsSecure(bool $bCheckProxy = true) : bool { - $sHttps = \strtolower($this->GetServer('HTTPS', '')); - if ('on' === $sHttps || ('' === $sHttps && '443' === (string) $this->GetServer('SERVER_PORT', ''))) + $sHttps = \strtolower(static::GetServer('HTTPS', '')); + if ('on' === $sHttps || ('' === $sHttps && '443' === (string) static::GetServer('SERVER_PORT', ''))) { return true; } if ($bCheckProxy && ( - ('https' === \strtolower($this->GetServer('HTTP_X_FORWARDED_PROTO', ''))) || - ('on' === \strtolower($this->GetServer('HTTP_X_FORWARDED_SSL', ''))) + ('https' === \strtolower(static::GetServer('HTTP_X_FORWARDED_PROTO', ''))) || + ('on' === \strtolower(static::GetServer('HTTP_X_FORWARDED_SSL', ''))) )) { return true; @@ -128,11 +123,11 @@ class Http public function GetHost(bool $bWithRemoteUserData = false, bool $bWithoutWWW = true, bool $bWithoutPort = false) : string { - $sHost = $this->GetServer('HTTP_HOST', ''); + $sHost = static::GetServer('HTTP_HOST', ''); if (!\strlen($sHost)) { - $sName = $this->GetServer('SERVER_NAME'); - $iPort = (int) $this->GetServer('SERVER_PORT', 80); + $sName = static::GetServer('SERVER_NAME'); + $iPort = (int) static::GetServer('SERVER_PORT', 80); $sHost = (\in_array($iPort, array(80, 433))) ? $sName : $sName.':'.$iPort; } @@ -144,7 +139,7 @@ class Http if ($bWithRemoteUserData) { - $sUser = \trim($this->GetServer('REMOTE_USER', '')); + $sUser = \trim(static::GetServer('REMOTE_USER', '')); $sHost = (\strlen($sUser) ? $sUser.'@' : '').$sHost; } @@ -159,63 +154,68 @@ class Http public function GetClientIp(bool $bCheckProxy = false) : string { $sIp = ''; - if ($bCheckProxy && null !== $this->GetServer('HTTP_CLIENT_IP', null)) + if ($bCheckProxy && null !== static::GetServer('HTTP_CLIENT_IP', null)) { - $sIp = $this->GetServer('HTTP_CLIENT_IP', ''); + $sIp = static::GetServer('HTTP_CLIENT_IP', ''); } - else if ($bCheckProxy && null !== $this->GetServer('HTTP_X_FORWARDED_FOR', null)) + else if ($bCheckProxy && null !== static::GetServer('HTTP_X_FORWARDED_FOR', null)) { - $sIp = $this->GetServer('HTTP_X_FORWARDED_FOR', ''); + $sIp = static::GetServer('HTTP_X_FORWARDED_FOR', ''); } else { - $sIp = $this->GetServer('REMOTE_ADDR', ''); + $sIp = static::GetServer('REMOTE_ADDR', ''); } return $sIp; } - public function ServerNotModifiedCache(int $iExpireTime, bool $bSetCacheHeader = true, string $sEtag = '') : bool + public static function checkETag(string $ETag) : void { - $bResult = false; - if (0 < $iExpireTime) - { - $iUtcTimeStamp = \time(); - $sIfModifiedSince = $this->GetHeader('If-Modified-Since', ''); - if (0 === \strlen($sIfModifiedSince)) - { - if ($bSetCacheHeader) - { - \header('Cache-Control: public'); - \header('Pragma: public'); - \header('Last-Modified: '.\gmdate('D, d M Y H:i:s', $iUtcTimeStamp - $iExpireTime).' UTC'); - \header('Expires: '.\gmdate('D, j M Y H:i:s', $iUtcTimeStamp + $iExpireTime).' UTC'); - - if (\strlen($sEtag)) - { - \header('Etag: '.$sEtag); - } - } - } - else - { - static::StatusHeader(304); - $bResult = true; - } + $sIfNoneMatch = static::GetHeader('If-None-Match'); + if ($sIfNoneMatch && false !== \strpos($sIfNoneMatch, $ETag)) { + static::StatusHeader(304); + exit; + } + $sIfMatch = static::GetHeader('If-Match'); + if ($sIfMatch && false === \strpos($sIfMatch, $ETag)) { + static::StatusHeader(412); + exit; } - - return $bResult; } - /** - * @staticvar boolean $bCache - */ + public static function setETag(string $ETag) : void + { + static::checkETag($ETag); + \header("ETag: \"{$ETag}\""); + } + + public static function checkLastModified(int $mtime) : void + { + $sIfModifiedSince = static::GetHeader('If-Modified-Since'); + if ($sIfModifiedSince && $mtime <= \strtotime($sIfModifiedSince)) { + static::StatusHeader(304); + exit; + } + $sIfUnmodifiedSince = static::GetHeader('If-Unmodified-Since'); + if ($sIfUnmodifiedSince && $mtime > \strtotime($sIfUnmodifiedSince)) { + static::StatusHeader(412); + exit; + } + } + + public static function setLastModified(int $mtime) : void + { + static::checkLastModified($mtime); + \header('Last-Modified: '.\gmdate('D, d M Y H:i:s \G\M\T', $mtime)); # DATE_RFC1123 + } + + private static $bCache = false; + public function ServerNoCache() { - static $bCache = false; - if (false === $bCache) - { - $bCache = true; + if (!static::$bCache) { + static::$bCache = true; \header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); \header('Last-Modified: '.\gmdate('D, d M Y H:i:s').' GMT'); \header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0, post-check=0, pre-check=0'); @@ -223,19 +223,14 @@ class Http } } - /** - * @staticvar boolean $bCache - */ - public function ServerUseCache(string $sEtag, int $iLastModified, int $iExpires) + public static function ServerUseCache(string $sEtag = '', int $iLastModified = 0, int $iExpires = 0) : void { - static $bCache = false; - if (false === $bCache) - { - $bCache = true; + if (!static::$bCache) { + static::$bCache = true; \header('Cache-Control: private'); - \header('ETag: '.$sEtag); - \header('Last-Modified: '.\gmdate('D, d M Y H:i:s', $iLastModified).' UTC'); - \header('Expires: '.\gmdate('D, j M Y H:i:s', $iExpires).' UTC'); + $iExpires && \header('Expires: '.\gmdate('D, j M Y H:i:s', \time() + $iExpires).' UTC'); + $sEtag && static::setETag($sEtag); + $iLastModified && static::setLastModified($iLastModified); } } @@ -253,6 +248,7 @@ class Http 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', + 412 => 'Precondition Failed', 416 => 'Requested range not satisfiable', 500 => 'Internal Server Error' ); @@ -271,7 +267,7 @@ class Http public function GetPath() : string { - $sUrl = \ltrim(\substr($this->GetServer('SCRIPT_NAME', ''), 0, \strrpos($this->GetServer('SCRIPT_NAME', ''), '/')), '/'); + $sUrl = \ltrim(\substr(static::GetServer('SCRIPT_NAME', ''), 0, \strrpos(static::GetServer('SCRIPT_NAME', ''), '/')), '/'); return '' === $sUrl ? '/' : '/'.$sUrl.'/'; } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php index 2b880b930..ee7952367 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php @@ -147,10 +147,10 @@ class Actions '[SM:' . APP_VERSION . '][IP:' . $oHttp->GetClientIp($this->oConfig->Get('labs', 'http_client_ip_check_proxy', false)) . '][PID:' . (\MailSo\Base\Utils::FunctionCallable('getmypid') ? \getmypid() : 'unknown') - . '][' . $oHttp->GetServer('SERVER_SOFTWARE', '~') + . '][' . \MailSo\Base\Http::GetServer('SERVER_SOFTWARE', '~') . '][' . (\MailSo\Base\Utils::FunctionCallable('php_sapi_name') ? \php_sapi_name() : '~') . '][Streams:' . \implode(',', \stream_get_transports()) - . '][' . $oHttp->GetMethod() . ' ' . $oHttp->GetScheme() . '://' . $oHttp->GetHost(false, false) . $oHttp->GetServer('REQUEST_URI', '') . ']' + . '][' . $oHttp->GetMethod() . ' ' . $oHttp->GetScheme() . '://' . $oHttp->GetHost(false, false) . \MailSo\Base\Http::GetServer('REQUEST_URI', '') . ']' ); } @@ -1062,17 +1062,18 @@ class Actions public function etag(string $sKey): string { - return \md5('Etag:' . \md5($sKey . \md5($this->oConfig->Get('cache', 'index', '')))); + return \md5($sKey . $this->oConfig->Get('cache', 'index', '')); } public function cacheByKey(string $sKey, bool $bForce = false): bool { - if (!empty($sKey) && ($bForce || ($this->oConfig->Get('cache', 'enable', true) && $this->oConfig->Get('cache', 'http', true)))) { - $iExpires = $this->oConfig->Get('cache', 'http_expires', 3600); - if (0 < $iExpires) { - $this->Http()->ServerUseCache($this->etag($sKey), 1382478804, \time() + $iExpires); - return true; - } + if ($sKey && ($bForce || ($this->oConfig->Get('cache', 'enable', true) && $this->oConfig->Get('cache', 'http', true)))) { + \MailSo\Base\Http::ServerUseCache( + $this->etag($sKey), + 1382478804, + $this->oConfig->Get('cache', 'http_expires', 3600) + ); + return true; } $this->Http()->ServerNoCache(); return false; @@ -1080,13 +1081,9 @@ class Actions public function verifyCacheByKey(string $sKey, bool $bForce = false): void { - if (!empty($sKey) && ($bForce || ($this->oConfig->Get('cache', 'enable', true) && $this->oConfig->Get('cache', 'http', true)))) { - $sIfNoneMatch = $this->Http()->GetHeader('If-None-Match', ''); - if ($this->etag($sKey) === $sIfNoneMatch) { - \MailSo\Base\Http::StatusHeader(304); - $this->cacheByKey($sKey); - exit(0); - } + if ($sKey && ($bForce || ($this->oConfig->Get('cache', 'enable', true) && $this->oConfig->Get('cache', 'http', true)))) { + \MailSo\Base\Http::checkETag($this->etag($sKey)); +// $this->cacheByKey($sKey, $bForce); } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Localization.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Localization.php index 58a979b97..4d8d15c91 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Localization.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Localization.php @@ -69,7 +69,7 @@ trait Localization private function getUserLanguagesFromHeader(): array { $aResult = $aList = array(); - $sAcceptLang = \strtolower($this->Http()->GetServer('HTTP_ACCEPT_LANGUAGE', 'en')); + $sAcceptLang = \strtolower(\MailSo\Base\Http::GetServer('HTTP_ACCEPT_LANGUAGE', 'en')); if (!empty($sAcceptLang) && \preg_match_all('/([a-z]{1,8}(?:-[a-z]{1,8})?)(?:;q=([0-9.]+))?/', $sAcceptLang, $aList)) { $aResult = \array_combine($aList[1], $aList[2]); foreach ($aResult as $n => $v) { diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Raw.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Raw.php index 0e9c78f42..e0ad8b60a 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Raw.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Raw.php @@ -117,7 +117,7 @@ trait Raw $aValues = $this->getDecodedRawKeyValue($sRawKey); - $sRange = $this->Http()->GetHeader('Range'); + $sRange = \MailSo\Base\Http::GetHeader('Range'); $aMatch = array(); $sRangeStart = $sRangeEnd = ''; From 4085f58b4e8d682d008e4af9ef3ea1dd1d1b6c13 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 28 Nov 2022 10:39:28 +0100 Subject: [PATCH 11/11] Resolve #713 --- dev/boot.js | 13 +++++++++++++ .../v/0.0.0/app/libraries/snappymail/crypt.php | 10 ++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/dev/boot.js b/dev/boot.js index f4d131326..0d52d9ba9 100644 --- a/dev/boot.js +++ b/dev/boot.js @@ -31,6 +31,19 @@ const navigator.cookieEnabled || redirect('NoCookie'); [].flat || redirect('BadBrowser'); +try { + let smctoken = localStorage.getItem('smctoken'); + if (!smctoken) { + let data = new Uint8Array(16); + crypto.getRandomValues(data); + smctoken = btoa(String.fromCharCode(...data)); + localStorage.setItem('smctoken', smctoken); + } + document.cookie = 'smctoken='+encodeURIComponent(smctoken)+"; path=/; samesite=strict"; +} catch (e) { + console.error(e); +} + let RL_APP_DATA = {}; doc.documentElement.classList.toggle('rl-mobile', 'mobile' === layout[2] || (!layout && 1000 > innerWidth)); diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/crypt.php b/snappymail/v/0.0.0/app/libraries/snappymail/crypt.php index d49b51454..6ec79cec0 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/crypt.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/crypt.php @@ -45,10 +45,12 @@ abstract class Crypt */ private static function Passphrase(?string $key) : string { - return \sha1( - ($key ?: \preg_replace('/[^a-z]+/i', '', \explode(')', $_SERVER['HTTP_USER_AGENT'])[0])) . APP_SALT, - true - ); + if (!$key) { + $key = isset($_COOKIE['smctoken']) + ? $_COOKIE['smctoken'] + : \preg_replace('/[^a-z]+/i', '', \explode(')', $_SERVER['HTTP_USER_AGENT'])[0]); + } + return \sha1($key . APP_SALT, true); } public static function Decrypt(array $data, string $key = null) /* : mixed */