Compare commits

..

1 commit

Author SHA1 Message Date
djmaze
208f1c7298 v2.38.2.1 2026-10-23 11:08:37 +02:00
306 changed files with 7052 additions and 9905 deletions

View file

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
FROM alpine:3.24.3 AS builder FROM alpine:3.18.5 AS builder
RUN apk add --no-cache php85 php85-json php-phar php-zip RUN apk add --no-cache php82 php82-json php-phar php-zip
RUN apk add --no-cache npm RUN apk add --no-cache npm
RUN npm install -g gulp yarn RUN npm install -g gulp yarn
WORKDIR /source WORKDIR /source
@ -23,7 +23,7 @@ RUN set -eux; \
rm -v /snappymail/README.md /snappymail/_include.php rm -v /snappymail/README.md /snappymail/_include.php
# Inspired by the original Rainloop dockerfile from youtous on GitLab # Inspired by the original Rainloop dockerfile from youtous on GitLab
FROM php:8.5-fpm-alpine AS final FROM php:8.2-fpm-alpine AS final
LABEL org.label-schema.description="SnappyMail webmail client image using nginx, php-fpm on Alpine" LABEL org.label-schema.description="SnappyMail webmail client image using nginx, php-fpm on Alpine"
@ -99,6 +99,9 @@ RUN set -eux; \
# mysql # mysql
RUN docker-php-ext-install pdo_mysql RUN docker-php-ext-install pdo_mysql
# opcache
RUN docker-php-ext-install opcache
# postgres # postgres
RUN set -eux; \ RUN set -eux; \
apk add --no-cache postgresql-libs; \ apk add --no-cache postgresql-libs; \
@ -119,13 +122,6 @@ RUN set -eux; \
apk del .build-dependencies; \ apk del .build-dependencies; \
apk del .deps apk del .deps
# sqlite
RUN set -eux; \
apk add --no-cache sqlite-libs; \
apk add --no-cache --virtual .deps sqlite-dev; \
docker-php-ext-install pdo_sqlite; \
apk del .deps
# tidy # tidy
RUN set -eux; \ RUN set -eux; \
apk add --no-cache tidyhtml; \ apk add --no-cache tidyhtml; \

View file

@ -9,12 +9,6 @@ UPLOAD_MAX_SIZE=${UPLOAD_MAX_SIZE:-25M}
MEMORY_LIMIT=${MEMORY_LIMIT:-128M} MEMORY_LIMIT=${MEMORY_LIMIT:-128M}
SECURE_COOKIES=${SECURE_COOKIES:-true} SECURE_COOKIES=${SECURE_COOKIES:-true}
# Disable listening on ipv6 for ipv4-only environments, or nginx will fail to start
if ! ip a | grep 'inet6 ' > /dev/null; then
echo "[INFO] ipv6 is disabled. Configuring nginx to not listen on ipv6"
sed -i '/listen \[::\]:/d' /etc/nginx/nginx.conf
fi
# Set attachment size limit # Set attachment size limit
sed -i "s/<UPLOAD_MAX_SIZE>/$UPLOAD_MAX_SIZE/g" /usr/local/etc/php-fpm.d/php-fpm.conf /etc/nginx/nginx.conf sed -i "s/<UPLOAD_MAX_SIZE>/$UPLOAD_MAX_SIZE/g" /usr/local/etc/php-fpm.d/php-fpm.conf /etc/nginx/nginx.conf
sed -i "s/<MEMORY_LIMIT>/$MEMORY_LIMIT/g" /usr/local/etc/php-fpm.d/php-fpm.conf sed -i "s/<MEMORY_LIMIT>/$MEMORY_LIMIT/g" /usr/local/etc/php-fpm.d/php-fpm.conf

View file

@ -36,5 +36,5 @@ YMXQVq4SJt7UpWABAORudfJxsBqCRKtPlZMgGTJLjcOkyFJ9C2Fx7DeN0J4I
## Publishing and Credits ## Publishing and Credits
I will analyze and fix the reported issue as fast as possible. I will analyze and fix the reported issue as fast as possible.
Together with the reporter, I plan the disclosure of the found and fixed vulnerability. Together with the reporter I plan the disclosure of the found and fixed vulnerability.
Credits to the reporter are granted and can be included in all public communication if desired. Credits to the reporter are granted and can be included in all public communication if desired.

View file

@ -1,247 +0,0 @@
#!/usr/bin/php
<?php
define('ROOT_DIR', dirname(__DIR__));
chdir(ROOT_DIR);
$options = getopt('', ['readme','source','knockout','openpgp','squire']);
$return_var = '';
function execCmd($cmd)
{
passthru($cmd, $return_var);
if ($return_var) {
exit("`{$cmd}` failed with error code {$return_var}\n");
}
}
if (isset($options['knockout'])) {
chdir(ROOT_DIR . '/vendors/knockout');
if ($grunt = trim(`which grunt`)) {
execCmd($grunt, $return_var);
} else {
exit('grunt not installed');
}
$file = ROOT_DIR . '/vendors/knockout/build/output/knockout-latest.js';
$data = file_get_contents($file);
/*
Strip start: (()=>{'use strict';var aa="function"==typeof Object.defineProperties?Object.defineProperty:function(C,F,E){if(C==Array.prototype||C==Object.prototype)return C;C[F]=E.value;return C};function ba(C){C=["object"==typeof globalThis&&globalThis,C,"object"==typeof window&&window,"object"==typeof self&&self,"object"==typeof global&&global];for(var F=0;F<C.length;++F){var E=C[F];if(E&&E.Math==Math)return E}throw Error("Cannot find global object");}var ca=ba(this);
function Z(C,F){if(F)a:{var E=ca;C=C.split(".");for(var J=0;J<C.length-1;J++){var S=C[J];if(!(S in E))break a;E=E[S]}C=C[C.length-1];J=E[C];F=F(J);F!=J&&null!=F&&aa(E,C,{configurable:!0,writable:!0,value:F})}}Z("Object.entries",function(C){return C?C:function(F){var E=[],J;for(J in F)Object.prototype.hasOwnProperty.call(F,J)&&E.push([J,F[J]]);return E}});
Z("Array.prototype.includes",function(C){return C?C:function(F,E){var J=this;J instanceof String&&(J=String(J));var S=J.length;E=E||0;for(0>E&&(E=Math.max(E+S,0));E<S;E++){var V=J[E];if(V===F||Object.is(V,F))return!0}return!1}});
Strip end: })();
*/
$p1 = strpos($data, '(()=>');
// ECMASCRIPT_202
$data = substr($data, 0, $p1) . substr($data, $p1 + 6, -6) . "\n";
// ECMASCRIPT_2018
// $data = substr($data, 0, $p1) . substr($data, $p1 + 19, -6) . "\n";
// ECMASCRIPT6
// $data = substr($data, 0, $p1) . substr($data, strpos($data, 'return!0}return!1}});') + 22, -6) . "\n";
file_put_contents($file, $data);
exit;
}
if (isset($options['openpgp'])) {
chdir(ROOT_DIR . '/vendors/openpgp-5');
execCmd('git pull', $return_var);
execCmd('./build.sh', $return_var);
exit;
}
if (isset($options['squire'])) {
chdir(ROOT_DIR . '/vendors/squire2');
// execCmd('git pull', $return_var);
// execCmd('git checkout snappymail', $return_var);
execCmd('./build.sh', $return_var);
exit;
}
if (isset($options['source'])) {
#cd ROOT_DIR . '/vendors/openpgp-2.6.2/';
#rollup -c
#npm cache clean --force
if ($gulp = trim(`which gulp`)) {
execCmd($gulp, $return_var);
} else {
exit('gulp not installed, run as root: npm install --global gulp-cli');
}
#rollup -c
$cmddir = escapeshellcmd(ROOT_DIR) . '/snappymail/v/0.0.0/static';
if ($gzip = trim(`which gzip`)) {
echo "\x1b[33;1m === Gzip *.js and *.css === \x1b[0m\n";
// execCmd("{$gzip} -k --best {$cmddir}/js/*.js");
execCmd("{$gzip} -k --best {$cmddir}/js/min/*.js");
execCmd("{$gzip} -k --best {$cmddir}/css/admin*.css");
execCmd("{$gzip} -k --best {$cmddir}/css/app*.css");
// unlink($cmddir . '/js/boot.js.gz');
// unlink($cmddir . '/js/min/boot.min.js.gz');
}
if ($brotli = trim(`which brotli`)) {
echo "\x1b[33;1m === Brotli *.js and *.css === \x1b[0m\n";
// execCmd("{$brotli} -k --best {$cmddir}/js/*.js");
execCmd("{$brotli} -k --best {$cmddir}/js/min/*.js");
execCmd("{$brotli} -k --best {$cmddir}/css/admin*.css");
execCmd("{$brotli} -k --best {$cmddir}/css/app*.css");
// unlink($cmddir . '/js/boot.js.br');
// unlink($cmddir . '/js/min/boot.min.js.br');
}
}
if (isset($options['readme'])) {
$basedir = escapeshellcmd(ROOT_DIR) . '/snappymail/v/0.0.0';
$readme = file_get_contents(ROOT_DIR . '/README.md');
$total = filesize("{$basedir}/static/js/serviceworker.js");
foreach (['admin','app','boot','libs','sieve'] as $name) {
$size = filesize("{$basedir}/static/js/{$name}.js");
$total += $size;
$readme = preg_replace("/(\\|{$name}\\.js\\s+\\|[\\s0-9.]+\\|)[\\s0-9.]+\\|/Ds",
'$1' . str_pad(number_format($size, 0, '', '.'), 9, ' ', STR_PAD_LEFT) . ' |',
$readme
);
}
$readme = preg_replace("/(\\|TOTAL\\s+\\|8[\\s0-9.]+\\|)[\\s0-9.]+\\|/Ds",
'$1' . str_pad(number_format($total, 0, '', '.'), 9, ' ', STR_PAD_LEFT) . ' |',
$readme
);
$total = [];
$total_br = [];
$total_gz = [];
foreach (['admin','app','boot','libs','sieve'] as $name) {
$size = filesize("{$basedir}/static/js/min/{$name}.min.js");
$size_br = filesize("{$basedir}/static/js/min/{$name}.min.js.br");
$size_gz = filesize("{$basedir}/static/js/min/{$name}.min.js.gz");
$total[$name] = $size;
$total_br[$name] = $size_br;
$total_gz[$name] = $size_gz;
$readme = preg_replace(
"/(\\|{$name}\\.min\\.js\\s+\\|[\\s0-9.]+)\\|[\\s0-9.]+(\\|[\\s0-9.]+)\\|[\\s0-9.]+(\\|[\\s0-9.]+)\\|[\\s0-9.]+\\|/Ds",
'$1|'
. str_pad(number_format($size, 0, '', '.'), 9, ' ', STR_PAD_LEFT) . ' '
. '$2|'
. str_pad(number_format($size_gz, 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
. '$3|'
. str_pad(number_format($size_br, 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
. '|'
,
$readme
);
}
$readme = preg_replace(
"/(TOTAL user\+sieve\\s+\\|[\\s0-9.]+)\\|[\\s0-9.]+(\\|[\\s0-9.]+)\\|[\\s0-9.]+(\\|[\\s0-9.]+)\\|[\\s0-9.]+\\|/Ds",
'$1|'
. str_pad(number_format(array_sum($total) - $total['admin'], 0, '', '.'), 9, ' ', STR_PAD_LEFT) . ' '
. '$2|'
. str_pad(number_format(array_sum($total_gz) - $total_gz['admin'], 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
. '$3|'
. str_pad(number_format(array_sum($total_br) - $total_br['admin'], 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
. '|'
,
$readme
);
//$smaller = 1217635 / (array_sum($total) - $total['admin']);
$smaller = 358761 / (array_sum($total_gz) - $total_gz['admin']);
//$smaller = 299485 / (array_sum($total_br) - $total_br['admin']);
$readme = preg_replace(
'/(For a user it is around) [0-9]+/',
'$1 ' . round(100 - 100 / $smaller),
$readme
);
unset($total['sieve']);
unset($total_br['sieve']);
unset($total_gz['sieve']);
$readme = preg_replace(
"/(TOTAL user\\s+\\|[\\s0-9.]+)\\|[\\s0-9.]+(\\|[\\s0-9.]+)\\|[\\s0-9.]+(\\|[\\s0-9.]+)\\|[\\s0-9.]+\\|/Ds",
'$1|'
. str_pad(number_format(array_sum($total) - $total['admin'], 0, '', '.'), 9, ' ', STR_PAD_LEFT) . ' '
. '$2|'
. str_pad(number_format(array_sum($total_gz) - $total_gz['admin'], 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
. '$3|'
. str_pad(number_format(array_sum($total_br) - $total_br['admin'], 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
. '|'
,
$readme
);
$readme = preg_replace(
"/(TOTAL admin\\s+\\|[\\s0-9.]+)\\|[\\s0-9.]+(\\|[\\s0-9.]+)\\|[\\s0-9.]+(\\|[\\s0-9.]+)\\|[\\s0-9.]+\\|/Ds",
'$1|'
. str_pad(number_format(array_sum($total) - $total['app'], 0, '', '.'), 9, ' ', STR_PAD_LEFT) . ' '
. '$2|'
. str_pad(number_format(array_sum($total_gz) - $total_gz['app'], 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
. '$3|'
. str_pad(number_format(array_sum($total_br) - $total_br['app'], 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
. '|'
,
$readme
);
foreach (['admin','app'] as $name) {
$readme = preg_replace(
"/(\\|{$name}\\.css\\s+\\|[\\s0-9.]+)\\|[\\s0-9.]+(\\|[\\s0-9.]+)\\|[\\s0-9.]+\\|[\\s0-9.]+\\|/Ds",
'$1|'
. str_pad(number_format(filesize("{$basedir}/static/css/{$name}.css"), 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
. '$2|'
. str_pad(number_format(filesize("{$basedir}/static/css/{$name}.css.gz"), 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' |'
. str_pad(number_format(filesize("{$basedir}/static/css/{$name}.css.br"), 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
. '|'
,
$readme
);
$readme = preg_replace(
"/(\\|{$name}\\.min\\.css\\s+\\|[\\s0-9.]+)\\|[\\s0-9.]+(\\|[\\s0-9.]+)\\|[\\s0-9.]+\\|[\\s0-9.]+\\|/Ds",
'$1|'
. str_pad(number_format(filesize("{$basedir}/static/css/{$name}.min.css"), 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
. '$2|'
. str_pad(number_format(filesize("{$basedir}/static/css/{$name}.min.css.gz"), 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' |'
. str_pad(number_format(filesize("{$basedir}/static/css/{$name}.min.css.br"), 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
. '|'
,
$readme
);
}
$sizes = [
filesize("{$basedir}/static/js/min/openpgp.min.js"),
filesize("{$basedir}/static/js/min/openpgp.min.js.gz"),
filesize("{$basedir}/static/js/min/openpgp.min.js.br")
];
//|openpgp.min.js | 330.742 | 540.542 |102.388 | 167.397 | 84.241 | 137.502 |
$readme = preg_replace(
"/(openpgp\.min\.js\\s+\\|[\\s0-9.]+)\\|[\\s0-9.]+(\\|[\\s0-9.]+)\\|[\\s0-9.]+(\\|[\\s0-9.]+)\\|[\\s0-9.]+\\|/Ds",
'$1|'
. str_pad(number_format($sizes[0], 0, '', '.'), 9, ' ', STR_PAD_LEFT) . ' '
. '$2| '
. str_pad(number_format($sizes[1], 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
. '$3| '
. str_pad(number_format($sizes[2], 0, '', '.'), 7, ' ', STR_PAD_LEFT) . ' '
. '|'
,
$readme
);
//file://
file_put_contents(ROOT_DIR . '/README.md', $readme);
$data = file_put_contents(
"{$basedir}/app/templates/Views/Admin/AdminSettingsAbout.html",
preg_replace(
'/20[0-9][0-9] ©/',
date('Y').' ©',
file_get_contents("{$basedir}/app/templates/Views/Admin/AdminSettingsAbout.html")
)
);
}

View file

@ -9,7 +9,7 @@ if (isset($options['plugins'])) {
require(ROOT_DIR . '/build/plugins.php'); require(ROOT_DIR . '/build/plugins.php');
} }
$gulp = trim(shell_exec('which gulp')); $gulp = trim(`which gulp`);
if (!$gulp) { if (!$gulp) {
exit('gulp not installed, run as root: npm install --global gulp-cli'); exit('gulp not installed, run as root: npm install --global gulp-cli');
} }
@ -34,7 +34,7 @@ $options['aur'] = isset($options['aur']);
// Docker build // Docker build
$options['docker'] = isset($options['docker']); $options['docker'] = isset($options['docker']);
if ($options['docker'] && !($docker = trim(shell_exec('which docker')))) { if ($options['docker'] && !($docker = trim(`which docker`))) {
exit('docker not found'); exit('docker not found');
} }
if ($options['docker'] && $options['aur']) { if ($options['docker'] && $options['aur']) {
@ -60,7 +60,7 @@ if (!isset($options['skip-gulp'])) {
$cmddir = escapeshellcmd(ROOT_DIR) . '/snappymail/v/0.0.0/static'; $cmddir = escapeshellcmd(ROOT_DIR) . '/snappymail/v/0.0.0/static';
if ($gzip = trim(shell_exec('which gzip'))) { if ($gzip = trim(`which gzip`)) {
echo "\x1b[33;1m === Gzip *.js and *.css === \x1b[0m\n"; echo "\x1b[33;1m === Gzip *.js and *.css === \x1b[0m\n";
passthru("{$gzip} -k --best {$cmddir}/js/*.js"); passthru("{$gzip} -k --best {$cmddir}/js/*.js");
passthru("{$gzip} -k --best {$cmddir}/js/min/*.js"); passthru("{$gzip} -k --best {$cmddir}/js/min/*.js");
@ -70,7 +70,7 @@ if (!isset($options['skip-gulp'])) {
unlink(ROOT_DIR . '/snappymail/v/0.0.0/static/js/min/boot.min.js.gz'); unlink(ROOT_DIR . '/snappymail/v/0.0.0/static/js/min/boot.min.js.gz');
} }
if ($brotli = trim(shell_exec('which brotli'))) { if ($brotli = trim(`which brotli`)) {
echo "\x1b[33;1m === Brotli *.js and *.css === \x1b[0m\n"; echo "\x1b[33;1m === Brotli *.js and *.css === \x1b[0m\n";
passthru("{$brotli} -k --best {$cmddir}/js/*.js"); passthru("{$brotli} -k --best {$cmddir}/js/*.js");
passthru("{$brotli} -k --best {$cmddir}/js/min/*.js"); passthru("{$brotli} -k --best {$cmddir}/js/min/*.js");
@ -188,7 +188,7 @@ file_put_contents("{$destPath}core.json", '{
// Arch User Repository // Arch User Repository
if ($options['aur']) { if ($options['aur']) {
// extension_loaded('blake2') // extension_loaded('blake2')
if (!function_exists('b2sum') && $b2sum = trim(shell_exec('which b2sum'))) { if (!function_exists('b2sum') && $b2sum = trim(`which b2sum`)) {
function b2sum($file) { function b2sum($file) {
$file = escapeshellarg($file); $file = escapeshellarg($file);
exec("b2sum --binary {$file} 2>&1", $output, $exitcode); exec("b2sum --binary {$file} 2>&1", $output, $exitcode);

View file

@ -1,7 +1,6 @@
import 'External/User/ko'; import 'External/User/ko';
import { SMAudio } from 'Common/Audio'; import { SMAudio } from 'Common/Audio';
import { arrayLength } from 'Common/Utils';
import { mailToHelper, setLayoutResizer, dropdownsDetectVisibility, loadAccountsAndIdentities } from 'Common/UtilsUser'; import { mailToHelper, setLayoutResizer, dropdownsDetectVisibility, loadAccountsAndIdentities } from 'Common/UtilsUser';
import { import {
@ -241,16 +240,8 @@ export class AppUser extends AbstractApp {
showMessageComposer(params = []) showMessageComposer(params = [])
{ {
let msg = params[1];
if (1 == arrayLength(msg)) {
msg = msg[0];
}
if (msg) {
msg.decrypt().then((/*success*/)=>showScreenPopup(ComposePopupView, params));
} else {
showScreenPopup(ComposePopupView, params); showScreenPopup(ComposePopupView, params);
} }
}
} }
AskPopupView.password = function(sAskDesc, btnText, ask) { AskPopupView.password = function(sAskDesc, btnText, ask) {
@ -260,8 +251,7 @@ AskPopupView.password = function(sAskDesc, btnText, ask) {
view => resolve({ view => resolve({
password:view.passphrase(), password:view.passphrase(),
username:/*ask & 2 ? */view.username(), username:/*ask & 2 ? */view.username(),
remember:/*ask & 4 ? */view.remember(), remember:/*ask & 4 ? */view.remember()
rememberPermanent: view.rememberPermanent(),
}), }),
() => resolve(null), () => resolve(null),
true, true,

View file

@ -232,7 +232,6 @@ export const
detectHiddenImages = true, // !!SettingsGet('try_to_detect_hidden_images'), detectHiddenImages = true, // !!SettingsGet('try_to_detect_hidden_images'),
bqLevel = parseInt(SettingsUserStore.maxBlockquotesLevel()), bqLevel = parseInt(SettingsUserStore.maxBlockquotesLevel()),
minFontSize = parseInt(SettingsUserStore.minFontSize()),
result = { result = {
hasExternals: false, hasExternals: false,
@ -258,15 +257,6 @@ export const
rightmargin: (value, node) => node.style.marginRight = pInt(value) + 'px' rightmargin: (value, node) => node.style.marginRight = pInt(value) + 'px'
}, },
allowedAttributes = [ allowedAttributes = [
// SnappyMail
// 'data-x-href-broken',
// 'data-x-href-tracking',
'data-x-src',
// 'data-x-src-hidden',
// 'data-x-src-tracking',
// 'data-x-src-broken',
// 'data-x-style-url',
// 'data-x-style-broken-urls',
// defaults // defaults
'name', 'name',
'dir', 'lang', 'style', 'title', 'dir', 'lang', 'style', 'title',
@ -309,11 +299,8 @@ export const
// Not supported by <template> element // Not supported by <template> element
// .replace(/<!doctype[^>]*>/gi, '') // .replace(/<!doctype[^>]*>/gi, '')
// .replace(/<\?xml[^>]*\?>/gi, '') // .replace(/<\?xml[^>]*\?>/gi, '')
// https://github.com/the-djmaze/snappymail/issues/1860
.replace(/<(\/?)x-html(\s[^>]*)?>/gi, '')
.replace(/<(\/?)head(\s[^>]*)?>/gi, '') .replace(/<(\/?)head(\s[^>]*)?>/gi, '')
.replace(/<body(\s[^>]*)?>/i, '<div class="mail-body"$1>') .replace(/<(\/?)body(\s[^>]*)?>/gi, '<$1div class="mail-body"$2>')
.replace(/<(\/?)body(\s[^>]*)?>/gi, '<$1div$2>')
// .replace(/<\/?(html|head)[^>]*>/gi, '') // .replace(/<\/?(html|head)[^>]*>/gi, '')
// Fix Reddit https://github.com/the-djmaze/snappymail/issues/540 // Fix Reddit https://github.com/the-djmaze/snappymail/issues/540
.replace(/<span class="preview-text"[\s\S]+?<\/span>/, '') .replace(/<span class="preview-text"[\s\S]+?<\/span>/, '')
@ -506,12 +493,12 @@ export const
let attachment; let attachment;
if (value.startsWith('cid:')) if (value.startsWith('cid:'))
{ {
setAttribute('data-x-src', value);
value = value.slice(4); value = value.slice(4);
setAttribute('data-x-src-cid', value);
attachment = findAttachmentByCid(value); attachment = findAttachmentByCid(value);
if (attachment?.download) { if (attachment?.download) {
oElement.src = attachment.linkPreview(); oElement.src = attachment.linkPreview();
// oElement.title || (oElement.title = '+attachment.fileName); oElement.title += ' ('+attachment.fileName+')';
attachment.isInline(true); attachment.isInline(true);
attachment.isLinked(true); attachment.isLinked(true);
} }
@ -627,14 +614,12 @@ export const
if (urls_broken.length) { if (urls_broken.length) {
setAttribute('data-x-style-broken-urls', JSON.stringify(urls_broken)); setAttribute('data-x-style-broken-urls', JSON.stringify(urls_broken));
} }
/*
// https://github.com/the-djmaze/snappymail/issues/1082 // https://github.com/the-djmaze/snappymail/issues/1082
// Use getComputedStyle? if (11 > pInt(oStyle.fontSize)) {
if (oStyle.fontSize && minFontSize > pInt(oStyle.fontSize)) { oStyle.removeProperty('font-size');
oStyle.fontSize = minFontSize + "px";
// oStyle.setProperty('font-size', minFontSize + "px");
} }
*/
// Removes background and color // Removes background and color
// Many e-mails incorrectly only define one, not both // Many e-mails incorrectly only define one, not both
// And in dark theme mode this kills the readability // And in dark theme mode this kills the readability

View file

@ -97,7 +97,7 @@ Object.assign(ko.bindingHandlers, {
fValue.subscribe(fUpdateEditorValue); fValue.subscribe(fUpdateEditorValue);
// ko.addDisposeCallback(element, () => { // ko.utils.domNodeDisposal.addDisposeCallback(element, () => {
// }); // });
} }
} }

7
dev/External/ko.js vendored
View file

@ -15,7 +15,6 @@ export const
* based solely on the values of other observables in the application * based solely on the values of other observables in the application
*/ */
koComputable = fn => ko.computed(fn, {'pure':true}), koComputable = fn => ko.computed(fn, {'pure':true}),
// koObservable = value => ko.observable(value),
addObservablesTo = (target, observables) => addObservablesTo = (target, observables) =>
forEachObjectEntry(observables, (key, value) => forEachObjectEntry(observables, (key, value) =>
@ -31,7 +30,7 @@ export const
onEvent = (element, event, fn) => { onEvent = (element, event, fn) => {
element.addEventListener(event, fn); element.addEventListener(event, fn);
ko.addDisposeCallback(element, () => element.removeEventListener(event, fn)); ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener(event, fn));
}, },
onKey = (key, element, fValueAccessor, fAllBindings, model) => { onKey = (key, element, fValueAccessor, fAllBindings, model) => {
@ -176,6 +175,6 @@ ko.extenders.falseTimeout = (target, option) => {
// functions // functions
ko.observable.fn.askDeleteHelper = function(prop='askDelete') { ko.observable.fn.askDeleteHelper = function() {
return this.extend({ falseTimeout: 3000, toggleSubscribeProperty: [this, prop] }); return this.extend({ falseTimeout: 3000, toggleSubscribeProperty: [this, 'askDelete'] });
}; };

View file

@ -73,9 +73,4 @@ export class EmailModel extends AbstractModel {
} }
return result || name; return result || name;
} }
get domain() {
return this.email.replace(/^.+@([^@]+)$/, '$1');
// return this.email.slice(this.email.lastIndexOf('@'))
}
} }

View file

@ -1,16 +1,19 @@
//import { AbstractModel } from 'Knoin/AbstractModel'; import { AbstractModel } from 'Knoin/AbstractModel';
import { EmailModel } from 'Model/Email';
import { addObservablesTo, addComputablesTo } from 'External/ko'; import { addObservablesTo, addComputablesTo } from 'External/ko';
export class IdentityModel extends EmailModel /*AbstractModel*/ { export class IdentityModel extends AbstractModel {
/**
* @param {string} id
* @param {string} email
*/
constructor() { constructor() {
super(); super();
addObservablesTo(this, { addObservablesTo(this, {
id: '', id: '',
label: '', label: '',
// email: '', email: '',
// name: '', name: '',
replyTo: '', replyTo: '',
bcc: '', bcc: '',
@ -24,7 +27,6 @@ export class IdentityModel extends EmailModel /*AbstractModel*/ {
smimeKey: '', smimeKey: '',
smimeCertificate: '', smimeCertificate: '',
smimeCertificateChain: '',
askDelete: false, askDelete: false,
@ -34,18 +36,16 @@ export class IdentityModel extends EmailModel /*AbstractModel*/ {
addComputablesTo(this, { addComputablesTo(this, {
smimeKeyEncrypted: () => this.smimeKey().includes('-----BEGIN ENCRYPTED PRIVATE KEY-----'), smimeKeyEncrypted: () => this.smimeKey().includes('-----BEGIN ENCRYPTED PRIVATE KEY-----'),
smimeKeyValid: () => /^-----BEGIN (ENCRYPTED |RSA )?PRIVATE KEY-----/.test(this.smimeKey()), smimeKeyValid: () => /^-----BEGIN (ENCRYPTED |RSA )?PRIVATE KEY-----/.test(this.smimeKey()),
smimeCertificateValid: () => /^-----BEGIN CERTIFICATE-----/.test(this.smimeCertificate()), smimeCertificateValid: () => /^-----BEGIN CERTIFICATE-----/.test(this.smimeCertificate())
smimeCertificateChainValid: () => !this.smimeCertificateChain()
|| /^-----BEGIN CERTIFICATE-----/.test(this.smimeCertificateChain())
}); });
} }
/** /**
* @returns {string} * @returns {string}
*/ */
toString() { formattedName() {
const name = this.name, const name = this.name(),
email = this.email, email = this.email(),
label = this.label(); label = this.label();
return (name ? `${name} ` : '') + `<${email}>` + (label ? ` (${label})` : ''); return (name ? `${name} ` : '') + `<${email}>` + (label ? ` (${label})` : '');
} }

View file

@ -24,10 +24,6 @@ import Remote from 'Remote/User/Fetch';
import { MimeToMessage } from 'Mime/Utils'; import { MimeToMessage } from 'Mime/Utils';
import { PgpUserStore } from 'Stores/User/Pgp';
import { IdentityUserStore } from 'Stores/User/Identity';
import { Passphrases } from 'Storage/Passphrases';
const const
PreviewHTML = `<html> PreviewHTML = `<html>
<head> <head>
@ -602,140 +598,4 @@ export class MessageModel extends AbstractModel {
return result.html || plainToHtml(this.plain()); return result.html || plainToHtml(this.plain());
} }
async decrypt() {
const msg = this;
if (msg.pgpEncrypted() && !msg.pgpDecrypted()) {
return await msg.pgpDecrypt().then(()=>msg.pgpDecrypted());
}
if (msg.smimeEncrypted() && !msg.smimeDecrypted()) {
return await msg.smimeDecrypt().then(()=>msg.smimeDecrypted());
}
return true;
}
/*
async decrypted() {
return msg.pgpDecrypted() || msg.smimeDecrypted());
}
*/
async pgpDecrypt() {
const oMessage = this,
data = oMessage.pgpEncrypted();
delete data.error;
await PgpUserStore.decrypt(oMessage).then(result => {
if (!result) {
// TODO: translate
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 => {
data.error = e.message;
})
.finally(() => {
oMessage.pgpEncrypted(data);
});
}
pgpVerify(/*self, event*/) {
const oMessage = this;
PgpUserStore.verify(oMessage).then(result => {
if (result) {
oMessage.pgpSigned(result);
} else {
alert('Verification failed or no valid public key found');
}
/*
if (result?.success) {
i18n('CRYPTO/GOOD_SIGNATURE', {
USER: validKey.user + ' (' + validKey.id + ')'
});
message.getText()
} else {
const keyIds = arrayLength(signingKeyIds) ? signingKeyIds : null,
additional = keyIds
? keyIds.map(item => item?.toHex?.()).filter(v => v).join(', ')
: '';
i18n('CRYPTO/ERROR', {
TYPE: 'OpenPGP',
ERROR: 'message'
}) + (additional ? ' (' + additional + ')' : '');
}
*/
});
}
async smimeDecrypt() {
const message = this;
const addresses = message.from.concat(message.to, message.cc, message.bcc).map(item => item.email),
identity = IdentityUserStore.find(item => addresses.includes(item.email)),
data = message.smimeEncrypted(); // { partId: "1" }
if (data && identity) {
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}),
'CRYPTO/DECRYPT'
);
if (!pass) {
return;
}
params.passphrase = pass?.password;
}
await Remote.post('SMimeDecryptMessage', null, params).then(response => {
if (response?.Result?.data) {
message.smimeDecrypted(true);
MimeToMessage(response.Result.data, message);
message.html() ? message.viewHtml() : message.viewPlain();
pass && pass.remember && Passphrases.handle(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 = this,
data = message.smimeSigned(); // { partId: "1", micAlg: "pgp-sha256" }
if (data) {
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();
}
data.success = response.Result.success;
message.smimeSigned(data);
}
});
}
}
} }

View file

@ -18,8 +18,8 @@ export class AdminSettingsDomains /*extends AbstractViewSettings*/ {
testUsername() { testUsername() {
Remote.request('AdminDomainMatch', Remote.request('AdminDomainMatch',
(iError, oData) => { (iError, oData) => {
if (oData?.Result?.name) { if (oData?.Result?.domain) {
alert(`${oData.Result.email} matched domain: ${oData.Result.name}`); alert(`${oData.Result.email} matched domain: ${oData.Result.domain.name}`);
} else { } else {
alert('No domain match'); alert('No domain match');
} }

View file

@ -2,7 +2,7 @@ import ko from 'ko';
import { koComputable } from 'External/ko'; import { koComputable } from 'External/ko';
import { SettingsGet } from 'Common/Globals'; import { SettingsGet } from 'Common/Globals';
import { i18n, translateTrigger, getErrorMessage } from 'Common/Translator'; import { i18n, translateTrigger } from 'Common/Translator';
import { ContactUserStore } from 'Stores/User/Contact'; import { ContactUserStore } from 'Stores/User/Contact';
import Remote from 'Remote/User/Fetch'; import Remote from 'Remote/User/Fetch';
@ -15,7 +15,6 @@ export class UserSettingsContacts /*extends AbstractViewSettings*/ {
this.syncUrl = ContactUserStore.syncUrl; this.syncUrl = ContactUserStore.syncUrl;
this.syncUser = ContactUserStore.syncUser; this.syncUser = ContactUserStore.syncUser;
this.syncPass = ContactUserStore.syncPass; this.syncPass = ContactUserStore.syncPass;
this.syncError = ko.observable('');
this.syncModeOptions = koComputable(() => { this.syncModeOptions = koComputable(() => {
translateTrigger(); translateTrigger();
@ -49,15 +48,4 @@ export class UserSettingsContacts /*extends AbstractViewSettings*/ {
}) })
); );
} }
test() {
this.syncError('');
Remote.request('TestContactsSyncData', (iError, data) => {
iError && this.syncError(data.messageAdditional || data.message || getErrorMessage(iError, data));
}, {
Url: ContactUserStore.syncUrl(),
User: ContactUserStore.syncUser(),
Password: ContactUserStore.syncPass()
})
}
} }

View file

@ -44,51 +44,6 @@ export class UserSettingsGeneral extends AbstractViewSettings {
this.desktopNotifications = NotificationUserStore.enabled; this.desktopNotifications = NotificationUserStore.enabled;
this.isDesktopNotificationAllowed = NotificationUserStore.allowed; this.isDesktopNotificationAllowed = NotificationUserStore.allowed;
const i18nSort = s => i18n('MESSAGE_LIST/SORT_'+s);
this.sortSupported = FolderUserStore.hasCapability('SORT');
this.sortOptions = [
{
value: '',
text: i18nSort('DATE_DESC')
},
{
value: 'DATE',
text: i18nSort('DATE_ASC')
},
{
value: 'FROM',
text: i18nSort('FROM_ASC')
},
{
value: 'REVERSE FROM',
text: i18nSort('FROM_DESC')
},
{
value: 'REVERSE SIZE',
text: i18nSort('SIZE_DESC')
},
{
value: 'SIZE',
text: i18nSort('SIZE_ASC')
},
{
value: 'SUBJECT',
text: i18nSort('SUBJECT_ASC')
},
{
value: 'REVERSE SUBJECT',
text: i18nSort('SUBJECT_DESC')
},
{
value: 'REVERSE ARRIVAL',
text: i18nSort('ARRIVAL_DESC')
},
{
value: 'ARRIVAL',
text: i18nSort('ARRIVAL_ASC')
}
];
this.threadsAllowed = AppUserStore.threadsAllowed; this.threadsAllowed = AppUserStore.threadsAllowed;
// 'THREAD=REFS', 'THREAD=REFERENCES', 'THREAD=ORDEREDSUBJECT' // 'THREAD=REFS', 'THREAD=REFERENCES', 'THREAD=ORDEREDSUBJECT'
this.threadAlgorithms = ko.observableArray(); this.threadAlgorithms = ko.observableArray();
@ -96,11 +51,12 @@ export class UserSettingsGeneral extends AbstractViewSettings {
capa.startsWith('THREAD=') && this.threadAlgorithms.push(capa.slice(7)) capa.startsWith('THREAD=') && this.threadAlgorithms.push(capa.slice(7))
); );
this.threadAlgorithms.sort((a, b) => a.length - b.length); this.threadAlgorithms.sort((a, b) => a.length - b.length);
this.threadAlgorithm = SettingsUserStore.threadAlgorithm;
['defaultSort', 'useThreads', 'threadAlgorithm', ['useThreads', 'threadAlgorithm',
// These use addSetting() // These use addSetting()
'layout', 'messageReadDelay', 'messagesPerPage', 'checkMailInterval', 'layout', 'messageReadDelay', 'messagesPerPage', 'checkMailInterval',
'editorDefaultType', 'editorWysiwyg', 'msgDefaultAction', 'maxBlockquotesLevel', 'minFontSize', 'editorDefaultType', 'editorWysiwyg', 'msgDefaultAction', 'maxBlockquotesLevel',
// These are in addSettings() // These are in addSettings()
'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt', 'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt',
'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles', 'allowDraftAutosave', 'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles', 'allowDraftAutosave',
@ -122,7 +78,7 @@ export class UserSettingsGeneral extends AbstractViewSettings {
identityMainDesc: () => { identityMainDesc: () => {
const identity = IdentityUserStore.main(); const identity = IdentityUserStore.main();
return identity ? identity.toString() : '---'; return identity ? identity.formattedName() : '---';
}, },
editorDefaultTypes: () => { editorDefaultTypes: () => {
@ -161,8 +117,6 @@ export class UserSettingsGeneral extends AbstractViewSettings {
this.addSetting('CheckMailInterval'); this.addSetting('CheckMailInterval');
this.addSetting('Layout'); this.addSetting('Layout');
this.addSetting('MaxBlockquotesLevel'); this.addSetting('MaxBlockquotesLevel');
this.addSetting('minFontSize');
this.addSetting('defaultSort');
this.addSettings([ this.addSettings([
'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt', 'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt',

View file

@ -1,155 +1,21 @@
import { AskPopupView } from 'View/Popup/Ask'; import { AskPopupView } from 'View/Popup/Ask';
import { SettingsUserStore } from 'Stores/User/Settings'; import { SettingsUserStore } from 'Stores/User/Settings';
import { SettingsGet } from '../Common/Globals';
import { isArray } from '../Common/Utils';
export const Passphrases = new WeakMap(); export const Passphrases = new WeakMap();
Passphrases.ask = async (key, sAskDesc, btnText) => { Passphrases.ask = async (key, sAskDesc, btnText) =>
if (Passphrases.has(key)) { Passphrases.has(key)
return { password: Passphrases.handle(key)/*, remember:false*/ }; ? {password:Passphrases.handle(key)/*, remember:false*/}
} else if (Passphrases.hasInLocalStorage(key)) { : await AskPopupView.password(sAskDesc, btnText, 5);
return { password: await getFromLocalStorage(key) };
} else {
const pass = await AskPopupView.password(sAskDesc, btnText,
window.crypto.subtle && canUseLocalStorage(key) ? 0b1101 : 0b0101);
pass.rememberPermanent && await saveToLocalStorage(key, pass.password);
return pass;
}
};
Passphrases._deleteFromSession = Passphrases.delete;
const timeouts = {}; const timeouts = {};
// get/set accessor to control deletion after N minutes of inactivity // get/set accessor to control deletion after N minutes of inactivity
Passphrases.handle = (key, pass) => { Passphrases.handle = (key, pass) => {
const timeout = SettingsUserStore.keyPassForget(); const timeout = SettingsUserStore.keyPassForget();
if (timeout && !timeouts[key]) { if (timeout && !timeouts[key]) {
timeouts[key] = (() => Passphrases._deleteFromSession(key)).debounce(timeout * 60 * 1000); timeouts[key] = (()=>Passphrases.delete(key)).debounce(timeout * 1000);
} }
pass && Passphrases.set(key, pass); pass && Passphrases.set(key, pass);
timeout && timeouts[key](); timeout && timeouts[key]();
return Passphrases.get(key); return Passphrases.get(key);
}; };
const deleteFromLocalStorage = (key) => {
const keyId = getKeyId(key);
if (keyId) {
localStorage.removeItem(keyId);
}
};
Passphrases.delete = (key) => {
deleteFromLocalStorage(key);
return Passphrases._deleteFromSession(key);
};
Passphrases.hasInLocalStorage = (key) => {
const keyId = getKeyId(key);
return keyId && localStorage.getItem(keyId) !== null;
};
const saveToLocalStorage = async (key, pass) => {
const keyId = getKeyId(key);
if (!keyId) {
return;
}
if (!pass) {
localStorage.removeItem(keyId);
return;
}
try {
const salt = window.crypto.getRandomValues(new Uint8Array(16));
const derivedKey = await deriveKeyFromHash(SettingsGet('accountHash'), salt);
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const encrypted = await window.crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: iv },
derivedKey,
new TextEncoder().encode(pass)
);
localStorage.setItem(keyId, JSON.stringify([
btoa(String.fromCharCode.apply(null, salt)),
btoa(String.fromCharCode.apply(null, iv)),
btoa(String.fromCharCode.apply(null, new Uint8Array(encrypted)))
]));
} catch (e) {
console.error('Passphrases.saveToLocalStorage failed', e);
}
};
const getFromLocalStorage = async (key) => {
const keyId = getKeyId(key);
if (!keyId) {
return undefined;
}
const jsonData = localStorage.getItem(keyId);
if (!jsonData) {
console.error('Passphrases.getFromLocalStorage failed: no data found');
return undefined;
}
try {
const saltIvData = JSON.parse(jsonData);
if (!saltIvData || !isArray(saltIvData) || saltIvData.length !== 3) {
// noinspection ExceptionCaughtLocallyJS
throw new Error('invalid passphrase data');
}
const toUint8 = (str) => new Uint8Array(atob(str).split('').map(c => c.charCodeAt(0)));
const derivedKey = await deriveKeyFromHash(SettingsGet('accountHash'), toUint8(saltIvData[0]));
const decrypted = await window.crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: toUint8(saltIvData[1])
},
derivedKey,
toUint8(saltIvData[2])
);
return String.fromCharCode.apply(null, new Uint8Array(decrypted));
} catch (e) {
localStorage.removeItem(keyId);
console.error('Passphrases.getFromLocalStorage failed', e);
return undefined;
}
};
const canUseLocalStorage = (key) => getKeyId(key) !== undefined;
const getKeyId = (key) => {
if (key && typeof key.id === 'string' && key.id.length > 4 && typeof key.forgetPass === 'function') {
// only deal with keys that we can forget (OpenPGB, GnuPG)
return key.id + '_local_key';
} else {
console.info('Passphrases.getKeyId: unsupported key type');
return undefined;
}
};
const deriveKeyFromHash = async (hash, salt) => {
if (!hash) {
throw new Error('empty accountHash');
}
return window.crypto.subtle.importKey(
'raw',
new TextEncoder().encode(hash),
{ 'name': 'PBKDF2' },
false,
['deriveKey']
).then(keyMaterial => {
return window.crypto.subtle.deriveKey(
{
'name': 'PBKDF2',
'salt': salt,
'iterations': 512,
'hash': 'SHA-256'
},
keyMaterial,
{ 'name': 'AES-GCM', 'length': 256 },
false,
['encrypt', 'decrypt']
);
});
};

View file

@ -51,13 +51,6 @@ export const GnuPGUserStore = new class {
key.for = email => aEmails.includes(IDN.toASCII(email)); key.for = email => aEmails.includes(IDN.toASCII(email));
key.askDelete = ko.observable(false); key.askDelete = ko.observable(false);
key.openForDeletion = ko.observable(null).askDeleteHelper(); key.openForDeletion = ko.observable(null).askDeleteHelper();
key.askForgetPass = ko.observable(false);
key.openForPassForget = ko.observable(null).askDeleteHelper('askForgetPass');
key.forgetPass = () => {
Passphrases.delete(key);
key.hasStoredPass(false);
};
key.hasStoredPass = ko.observable(Passphrases.hasInLocalStorage(key));
key.remove = () => { key.remove = () => {
if (key.askDelete()) { if (key.askDelete()) {
Remote.request('GnupgDeleteKey', Remote.request('GnupgDeleteKey',
@ -76,7 +69,6 @@ export const GnuPGUserStore = new class {
isPrivate: isPrivate isPrivate: isPrivate
} }
); );
isPrivate && key.forgetPass();
} }
}; };
if (isPrivate) { if (isPrivate) {

View file

@ -13,7 +13,7 @@ import { OpenPgpKeyPopupView } from 'View/Popup/OpenPgpKey';
import { Passphrases } from 'Storage/Passphrases'; import { Passphrases } from 'Storage/Passphrases';
import { baseCollator, getNotification } from 'Common/Translator'; import { baseCollator } from 'Common/Translator';
const const
loaded = () => !!window.openpgp, loaded = () => !!window.openpgp,
@ -58,9 +58,10 @@ const
*/ */
publicKeysItem = 'openpgp-public-keys', publicKeysItem = 'openpgp-public-keys',
privateKeysItem = 'openpgp-private-keys', privateKeysItem = 'openpgp-private-keys',
storage = window.localStorage,
loadOpenPgpKeys = async itemname => { loadOpenPgpKeys = async itemname => {
let keys = [], key, let keys = [], key,
armoredKeys = JSON.parse(localStorage.getItem(itemname)), armoredKeys = JSON.parse(storage.getItem(itemname)),
i = arrayLength(armoredKeys); i = arrayLength(armoredKeys);
while (i--) try { while (i--) try {
key = await openpgp.readKey({armoredKey:armoredKeys[i]}); key = await openpgp.readKey({armoredKey:armoredKeys[i]});
@ -73,9 +74,9 @@ const
storeOpenPgpKeys = (keys, section) => { storeOpenPgpKeys = (keys, section) => {
let armoredKeys = keys.map(item => item.armor); let armoredKeys = keys.map(item => item.armor);
if (armoredKeys.length) { if (armoredKeys.length) {
localStorage.setItem(section, JSON.stringify(armoredKeys)); storage.setItem(section, JSON.stringify(armoredKeys));
} else { } else {
localStorage.removeItem(section); storage.removeItem(section);
} }
}; };
@ -90,9 +91,6 @@ class OpenPgpKeyModel {
this.armor = armor; this.armor = armor;
this.askDelete = ko.observable(false); this.askDelete = ko.observable(false);
this.openForDeletion = ko.observable(null).askDeleteHelper(); this.openForDeletion = ko.observable(null).askDeleteHelper();
this.hasStoredPass = ko.observable(Passphrases.hasInLocalStorage(this));
this.askForgetPass = ko.observable(false);
this.openForPassForget = ko.observable(null).askDeleteHelper('askForgetPass');
// key.getUserIDs() // key.getUserIDs()
// key.getPrimaryUser() // key.getPrimaryUser()
} }
@ -118,29 +116,12 @@ class OpenPgpKeyModel {
if (this.key.isPrivate()) { if (this.key.isPrivate()) {
OpenPGPUserStore.privateKeys.remove(this); OpenPGPUserStore.privateKeys.remove(this);
storeOpenPgpKeys(OpenPGPUserStore.privateKeys, privateKeysItem); storeOpenPgpKeys(OpenPGPUserStore.privateKeys, privateKeysItem);
this.forgetPass()
} else { } else {
OpenPGPUserStore.publicKeys.remove(this); OpenPGPUserStore.publicKeys.remove(this);
storeOpenPgpKeys(OpenPGPUserStore.publicKeys, publicKeysItem); storeOpenPgpKeys(OpenPGPUserStore.publicKeys, publicKeysItem);
} }
Remote.request('DeletePGPKey',
(iError, oData) => {
if (oData) {
if (iError || oData.Result === false) {
alert(oData.message || getNotification(iError));
} }
} }
}, {
key: this.armor
}
);
}
}
forgetPass() {
Passphrases.delete(this);
this.hasStoredPass(false);
}
/* /*
toJSON() { toJSON() {
return this.armor; return this.armor;

View file

@ -5,7 +5,6 @@ import { LayoutSideView, LayoutBottomView } from 'Common/EnumsUser';
import { pInt } from 'Common/Utils'; import { pInt } from 'Common/Utils';
import { $htmlCL, SettingsGet, fireEvent } from 'Common/Globals'; import { $htmlCL, SettingsGet, fireEvent } from 'Common/Globals';
import { ThemeStore } from 'Stores/Theme'; import { ThemeStore } from 'Stores/Theme';
import { FolderUserStore } from 'Stores/User/Folder';
export const SettingsUserStore = new class { export const SettingsUserStore = new class {
constructor() { constructor() {
@ -23,7 +22,6 @@ export const SettingsUserStore = new class {
allowStyles: 0, allowStyles: 0,
collapseBlockquotes: 1, collapseBlockquotes: 1,
maxBlockquotesLevel: 0, maxBlockquotesLevel: 0,
minFontSize: 0,
listInlineAttachments: 0, listInlineAttachments: 0,
simpleAttachmentsList: 0, simpleAttachmentsList: 0,
useCheckboxesInList: 1, useCheckboxesInList: 1,
@ -31,7 +29,6 @@ export const SettingsUserStore = new class {
showNextMessage: 0, showNextMessage: 0,
allowDraftAutosave: 1, allowDraftAutosave: 1,
useThreads: 0, useThreads: 0,
defaultSort: '',
threadAlgorithm: '', threadAlgorithm: '',
replySameFolder: 0, replySameFolder: 0,
hideUnsubscribed: 0, hideUnsubscribed: 0,
@ -99,7 +96,6 @@ export const SettingsUserStore = new class {
'AllowStyles', 'AllowStyles',
'CollapseBlockquotes', 'CollapseBlockquotes',
'MaxBlockquotesLevel', 'MaxBlockquotesLevel',
'minFontSize',
'ListInlineAttachments', 'ListInlineAttachments',
'simpleAttachmentsList', 'simpleAttachmentsList',
'UseCheckboxesInList', 'UseCheckboxesInList',
@ -107,7 +103,6 @@ export const SettingsUserStore = new class {
'showNextMessage', 'showNextMessage',
'AllowDraftAutosave', 'AllowDraftAutosave',
'useThreads', 'useThreads',
'defaultSort',
'threadAlgorithm', 'threadAlgorithm',
'ReplySameFolder', 'ReplySameFolder',
'HideUnsubscribed', 'HideUnsubscribed',
@ -166,7 +161,5 @@ export const SettingsUserStore = new class {
self.messageReadDelay(pInt(SettingsGet('MessageReadDelay'))); self.messageReadDelay(pInt(SettingsGet('MessageReadDelay')));
self.autoLogout(pInt(SettingsGet('AutoLogout'))); self.autoLogout(pInt(SettingsGet('AutoLogout')));
self.keyPassForget(pInt(SettingsGet('keyPassForget'))); self.keyPassForget(pInt(SettingsGet('keyPassForget')));
FolderUserStore.sortMode(self.defaultSort());
} }
}; };

View file

@ -1,6 +1,6 @@
#V-PopupsLanguages { #V-PopupsLanguages {
max-width: 720px; max-width: 710px;
label { label {
display: inline-block; display: inline-block;

View file

@ -125,9 +125,8 @@
/* Right */ /* Right */
.right { .right {
border-left: 1px solid var(--border-color, #ddd);
max-width: 600px;
width: 100%; width: 100%;
border-left: 1px solid var(--border-color, #ddd);
} }
.b-view-content-toolbar { .b-view-content-toolbar {

View file

@ -82,7 +82,7 @@
border-left: 3px solid transparent; border-left: 3px solid transparent;
color: var(--folders-color, #333); color: var(--folders-color, #333);
display: block; display: block;
line-height: 2.2em; line-height: 34px;
padding: 0 2em 0 @folderItemPadding; padding: 0 2em 0 @folderItemPadding;
position: relative; position: relative;
text-decoration: none; text-decoration: none;
@ -151,7 +151,7 @@
color: var(--unread-count-color, #fff); color: var(--unread-count-color, #fff);
font-size: 80%; font-size: 80%;
line-height: 1.5em; line-height: 1.5em;
margin-top: 0.5em; margin-top: 7px;
min-width: 1.7em; min-width: 1.7em;
padding: 1px 4px; padding: 1px 4px;
text-align: center; text-align: center;

View file

@ -45,8 +45,4 @@
content: ')'; content: ')';
} }
.identities-list tr + tr .identity-default {
display: none;
}
} }

View file

@ -15,14 +15,10 @@ export class AskPopupView extends AbstractViewPopup {
askUsername: false, askUsername: false,
passphrase: '', passphrase: '',
askPass: false, askPass: false,
remember: true, // remember for session remember: true,
askRemeber: false, askRemeber: false
rememberPermanent: false,
askRememberPermanent: false,
}); });
this.rememberPermanent.subscribe(value => value && this.remember(true));
this.fYesAction = null; this.fYesAction = null;
this.fNoAction = null; this.fNoAction = null;
@ -52,12 +48,10 @@ export class AskPopupView extends AbstractViewPopup {
this.askDesc(sAskDesc || ''); this.askDesc(sAskDesc || '');
this.askUsername(ask & 2); this.askUsername(ask & 2);
this.askPass(ask & 1); this.askPass(ask & 1);
this.askRemeber(ask & 4); // 0b0100 this.askRemeber(ask & 4);
this.askRememberPermanent(ask & 0b1000)
this.username(''); this.username('');
this.passphrase(''); this.passphrase('');
this.remember(true); this.remember(true);
this.rememberPermanent(false);
this.yesButton(i18n(btnText || 'GLOBAL/YES')); this.yesButton(i18n(btnText || 'GLOBAL/YES'));
this.noButton(i18n(ask ? 'GLOBAL/CANCEL' : 'GLOBAL/NO')); this.noButton(i18n(ask ? 'GLOBAL/CANCEL' : 'GLOBAL/NO'));
this.fYesAction = fYesFunc; this.fYesAction = fYesFunc;

View file

@ -44,7 +44,6 @@ import Remote from 'Remote/User/Fetch';
import { ComposeAttachmentModel } from 'Model/ComposeAttachment'; import { ComposeAttachmentModel } from 'Model/ComposeAttachment';
import { EmailModel } from 'Model/Email'; import { EmailModel } from 'Model/Email';
import { IdentityModel } from 'Model/Identity';
import { MimeHeaderAutocryptModel } from 'Model/MimeHeaderAutocrypt'; import { MimeHeaderAutocryptModel } from 'Model/MimeHeaderAutocrypt';
import { addressparser } from 'Mime/Address'; import { addressparser } from 'Mime/Address';
@ -93,7 +92,7 @@ const
findIdentity = addresses => { findIdentity = addresses => {
addresses = addresses.map(item => item.email); addresses = addresses.map(item => item.email);
return IdentityUserStore.find(item => addresses.includes(item.email)); return IdentityUserStore.find(item => addresses.includes(item.email()));
}, },
/** /**
@ -344,7 +343,7 @@ export class ComposePopupView extends AbstractViewPopup {
IdentityUserStore.map(item => ({ IdentityUserStore.map(item => ({
item: item, item: item,
optValue: item.id(), optValue: item.id(),
optText: item optText: item.formattedName()
})), })),
canBeSentOrSaved: () => !this.sending() && !this.saving() canBeSentOrSaved: () => !this.sending() && !this.saving()
@ -359,7 +358,7 @@ export class ComposePopupView extends AbstractViewPopup {
currentIdentity: value => { currentIdentity: value => {
if (value) { if (value) {
this.from(value.toString()); this.from(value.formattedName());
this.doEncrypt(value.pgpEncrypt() || SettingsUserStore.pgpEncrypt()); this.doEncrypt(value.pgpEncrypt() || SettingsUserStore.pgpEncrypt());
this.doSign(value.pgpSign() || SettingsUserStore.pgpSign()); this.doSign(value.pgpSign() || SettingsUserStore.pgpSign());
} }
@ -455,7 +454,7 @@ export class ComposePopupView extends AbstractViewPopup {
contactsCommand: self => self.allowContacts contactsCommand: self => self.allowContacts
}); });
this.from(IdentityUserStore()[0].toString()); this.from(IdentityUserStore()[0].formattedName());
} }
sentFolder() sentFolder()
@ -863,7 +862,6 @@ export class ComposePopupView extends AbstractViewPopup {
case ComposeType.Forward: case ComposeType.Forward:
case ComposeType.ForwardAsAttachment: case ComposeType.ForwardAsAttachment:
identity = findIdentity(oLastMessage.to.concat(oLastMessage.cc, oLastMessage.bcc)) identity = findIdentity(oLastMessage.to.concat(oLastMessage.cc, oLastMessage.bcc))
|| findIdentity(oLastMessage.from)
/* || findIdentity(oLastMessage.deliveredTo)*/; /* || findIdentity(oLastMessage.deliveredTo)*/;
break; break;
case ComposeType.Draft: case ComposeType.Draft:
@ -873,20 +871,10 @@ export class ComposePopupView extends AbstractViewPopup {
// case ComposeType.Empty: // case ComposeType.Empty:
} }
} }
// Set from custom email
if (!identity
&& oLastMessage && (ComposeType.Reply === options.mode || ComposeType.ReplyAll === options.mode)
&& 1 === oLastMessage.to.length
// && mEmail.includes(oLastMessage.to[0].domain)
) {
identity = new IdentityModel;
identity.name = oLastMessage.to[0].name;
identity.email = oLastMessage.to[0].email;
}
identity = identity || IdentityUserStore()[0]; identity = identity || IdentityUserStore()[0];
if (identity) { if (identity) {
// excludeEmail.add(identity.email); // excludeEmail.add(identity.email());
excludeEmail[identity.email] = true; excludeEmail[identity.email()] = true;
} }
if (arrayLength(options.to)) { if (arrayLength(options.to)) {
@ -971,7 +959,7 @@ export class ComposePopupView extends AbstractViewPopup {
// https://github.com/the-djmaze/snappymail/issues/491 // https://github.com/the-djmaze/snappymail/issues/491
tpl.innerHTML = oLastMessage.bodyAsHTML(); tpl.innerHTML = oLastMessage.bodyAsHTML();
tpl.content.querySelectorAll('img').forEach(img => { tpl.content.querySelectorAll('img').forEach(img => {
img.src || img.dataset.xSrc || img.replaceWith(img.alt || img.title) img.src || img.dataset.xSrcCid || img.dataset.xSrc || img.replaceWith(img.alt || img.title)
}); });
sText = tpl.innerHTML.trim(); sText = tpl.innerHTML.trim();
@ -1393,7 +1381,7 @@ export class ComposePopupView extends AbstractViewPopup {
allRecipients() { allRecipients() {
return [ return [
// From/sender is also recipient (Sent mailbox) // From/sender is also recipient (Sent mailbox)
// this.currentIdentity().email, // this.currentIdentity().email(),
this.from(), this.from(),
this.to(), this.to(),
this.cc(), this.cc(),
@ -1413,8 +1401,7 @@ export class ComposePopupView extends AbstractViewPopup {
key && options.push(['OpenPGP', key]); key && options.push(['OpenPGP', key]);
key = GnuPGUserStore.getPrivateKeyFor(email, 1); key = GnuPGUserStore.getPrivateKeyFor(email, 1);
key && options.push(['GnuPG', key]); key && options.push(['GnuPG', key]);
identity.smimeKeyValid() && identity.smimeCertificateValid() identity.smimeKeyValid() && identity.smimeCertificateValid() && identity.email() === email
&& identity.smimeCertificateChainValid() && identity.email === email
&& options.push(['S/MIME']); && options.push(['S/MIME']);
console.dir({signOptions: options}); console.dir({signOptions: options});
this.signOptions(options); this.signOptions(options);
@ -1433,7 +1420,7 @@ export class ComposePopupView extends AbstractViewPopup {
const count = recipients.length, const count = recipients.length,
identity = this.currentIdentity(), identity = this.currentIdentity(),
from = (identity.smimeKey() && identity.smimeCertificate()) ? identity.email : null; from = (identity.smimeKey() && identity.smimeCertificate()) ? identity.email() : null;
count count
&& count === recipients.filter(email => && count === recipients.filter(email =>
email == from email == from
@ -1510,21 +1497,12 @@ export class ComposePopupView extends AbstractViewPopup {
**/ **/
linkedData: [] linkedData: []
}, },
recipients = draft ? [identity.email] : this.allRecipients(), recipients = draft ? [identity.email()] : this.allRecipients(),
signOptions = !draft && this.doSign() && this.signOptions(), signOptions = !draft && this.doSign() && this.signOptions(),
encryptOptions = this.doEncrypt() && this.encryptOptions(), encryptOptions = this.doEncrypt() && this.encryptOptions(),
isHtml = this.oEditor.isHtml(); isHtml = this.oEditor.isHtml();
if (isHtml) { if (isHtml) {
tpl.innerHTML = Text;
tpl.content.querySelectorAll('img').forEach(img => {
if (img.dataset.xSrc) {
img.src = img.dataset.xSrc;
img.removeAttribute('data-x-src')
}
});
Text = tpl.innerHTML.trim();
do { do {
l = Text.length; l = Text.length;
Text = Text Text = Text
@ -1594,7 +1572,6 @@ export class ComposePopupView extends AbstractViewPopup {
*/ */
break; break;
} catch (e) { } catch (e) {
Passphrases.delete(signOptions[i][1])
console.error(e); console.error(e);
} }
} else if ('GnuPG' == signOptions[i][0]) { } else if ('GnuPG' == signOptions[i][0]) {
@ -1612,12 +1589,11 @@ export class ComposePopupView extends AbstractViewPopup {
// TODO: sign in PHP fails // TODO: sign in PHP fails
params.sign = 'S/MIME'; params.sign = 'S/MIME';
// params.signCertificate = identity.smimeCertificate(); // params.signCertificate = identity.smimeCertificate();
// params.signCertificateChain = identity.smimeCertificateChain();
// params.signPrivateKey = identity.smimeKey(); // params.signPrivateKey = identity.smimeKey();
// params.attachCertificate = false; // params.attachCertificate = false;
if (identity.smimeKeyEncrypted()) { if (identity.smimeKeyEncrypted()) {
const pass = await Passphrases.ask(identity, const pass = await Passphrases.ask(identity,
i18n('SMIME/PRIVATE_KEY_OF', {EMAIL: identity.email}), i18n('SMIME/PRIVATE_KEY_OF', {EMAIL: identity.email()}),
'CRYPTO/SIGN' 'CRYPTO/SIGN'
); );
if (null != pass) { if (null != pass) {
@ -1637,9 +1613,7 @@ export class ComposePopupView extends AbstractViewPopup {
Object.entries(PgpUserStore.getPublicKeyOfEmails(recipients) || {}).forEach(([k,v]) => Object.entries(PgpUserStore.getPublicKeyOfEmails(recipients) || {}).forEach(([k,v]) =>
params.autocrypt.push({ params.autocrypt.push({
addr: k, addr: k,
keydata: v keydata: v.replace(/-----(BEGIN|END) PGP PUBLIC KEY BLOCK-----/g, '').trim()
.replace(/-----(BEGIN|END) PGP PUBLIC KEY BLOCK-----/g, '')
.replace(/\s+/g, '') // Single base64 string, let the server do compliance formatting
}) })
); );
for (let i = 0; i < encryptOptions.length; ++i) { for (let i = 0; i < encryptOptions.length; ++i) {
@ -1659,7 +1633,7 @@ export class ComposePopupView extends AbstractViewPopup {
if ('S/MIME' == encryptOptions[i]) { if ('S/MIME' == encryptOptions[i]) {
params.encryptCertificates = [identity.smimeCertificate()]; params.encryptCertificates = [identity.smimeCertificate()];
SMimeUserStore.forEach(certificate => { SMimeUserStore.forEach(certificate => {
certificate.emailAddress != identity.email certificate.emailAddress != identity.email()
&& recipients.includes(certificate.emailAddress) && recipients.includes(certificate.emailAddress)
&& params.encryptCertificates.push(certificate.id) && params.encryptCertificates.push(certificate.id)
}); });

View file

@ -57,8 +57,8 @@ export class IdentityPopupView extends AbstractViewPopup {
this.submitError(oData.message); this.submitError(oData.message);
} }
}, { }, {
name: identity.name, name: identity.name(),
email: identity.email, email: identity.email(),
privateKey: identity.smimeKey(), privateKey: identity.smimeKey(),
passphrase: result.password passphrase: result.password
}); });

View file

@ -105,9 +105,9 @@ export class OpenPgpGeneratePopupView extends AbstractViewPopup {
} }
onShow() { onShow() {
this.name(''/*IdentityUserStore()[0].name*/); this.name(''/*IdentityUserStore()[0].name()*/);
this.password(''); this.password('');
this.email(''/*IdentityUserStore()[0].email*/); this.email(''/*IdentityUserStore()[0].email()*/);
this.emailError(false); this.emailError(false);
this.submitError(''); this.submitError('');
} }

View file

@ -213,19 +213,15 @@ export class MailMessageList extends AbstractViewRight {
sortText: () => { sortText: () => {
let mode = FolderUserStore.sortMode(), let mode = FolderUserStore.sortMode(),
has = w => mode.includes(w), desc = '' === mode || mode.includes('REVERSE');
desc = '' === mode || has('REVERSE');
mode = mode.split(/\s+/); mode = mode.split(/\s+/);
if (has('FROM')) { if (mode.includes('FROM')) {
return '@' + (desc ? '⬆' : '⬇'); return '@' + (desc ? '⬆' : '⬇');
} }
if (has('SUBJECT')) { if (mode.includes('SUBJECT')) {
return '𝐒' + (desc ? '⬆' : '⬇'); return '𝐒' + (desc ? '⬆' : '⬇');
} }
if (has('SIZE')) { return (mode.includes('SIZE') ? '✉' : '📅') + (desc ? '⬇' : '⬆');
return '✉' + (desc ? '⬇' : '⬆');
}
return (has('ARRIVAL') ? '📨' : '📅') + (desc ? '⬇' : '⬆');
}, },
downloadAsZipAllowed: () => this.attachmentsActions.includes('zip') downloadAsZipAllowed: () => this.attachmentsActions.includes('zip')
@ -303,14 +299,14 @@ export class MailMessageList extends AbstractViewRight {
item => sFolder === item?.folder && iUid == item?.uid item => sFolder === item?.folder && iUid == item?.uid
); );
if ('INBOX' === sFolder && 'INBOX' !== FolderUserStore.currentFolderFullName()) { if ('INBOX' === sFolder) {
hasher.setHash(mailBox(sFolder)); hasher.setHash(mailBox(sFolder));
} }
if (message) { if (message) {
this.selector.selectMessageItem(message); this.selector.selectMessageItem(message);
} else { } else {
if ('INBOX' !== sFolder && sFolder !== FolderUserStore.currentFolderFullName()) { if ('INBOX' !== sFolder) {
hasher.setHash(mailBox(sFolder)); hasher.setHash(mailBox(sFolder));
} }
if (sFolder && iUid) { if (sFolder && iUid) {

View file

@ -56,6 +56,9 @@ import { showScreenPopup } from 'Knoin/Knoin';
import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport'; import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
import { GnuPGUserStore } from 'Stores/User/GnuPG'; import { GnuPGUserStore } from 'Stores/User/GnuPG';
import { OpenPGPUserStore } from 'Stores/User/OpenPGP'; import { OpenPGPUserStore } from 'Stores/User/OpenPGP';
import { IdentityUserStore } from 'Stores/User/Identity';
import { Passphrases } from 'Storage/Passphrases';
const const
oMessageScrollerDom = () => elementById('messageItem') || {}, oMessageScrollerDom = () => elementById('messageItem') || {},
@ -130,8 +133,6 @@ export class MailMessageView extends AbstractViewRight {
// viewer // viewer
viewFromShort: '', viewFromShort: '',
dkimData: ['none', '', ''], dkimData: ['none', '', ''],
spfData: ['none', '', ''],
dmarcData: ['none', '', ''],
nowTracking: false nowTracking: false
}); });
@ -196,7 +197,7 @@ export class MailMessageView extends AbstractViewRight {
dkimIcon: () => { dkimIcon: () => {
switch (this.dkimData()[0]) { switch (this.dkimData()[0]) {
case 'none': case 'none':
return '🚫︎'; return '';
case 'pass': case 'pass':
return '✔'; return '✔';
default: default:
@ -204,43 +205,12 @@ export class MailMessageView extends AbstractViewRight {
} }
}, },
dkimIconClass: () => 'pass' === this.dkimData()[0] ? 'iconcolor-green' : 'iconcolor-red', dkimIconClass: () => 'pass' === this.dkimData()[0] ? 'iconcolor-green' : 'iconcolor-red',
dkimTitle:() => { dkimTitle:() => {
const dkim = this.dkimData(); const dkim = this.dkimData();
return dkim[0] ? dkim[2] || 'DKIM: ' + dkim[0] : ''; return dkim[0] ? dkim[2] || 'DKIM: ' + dkim[0] : '';
}, },
spfIcon: () => {
switch (this.spfData()[0]) {
case 'none':
return '🚫︎';
case 'pass':
return '✔';
default:
return '✖';
}
},
spfIconClass: () => 'pass' === this.spfData()[0] ? 'iconcolor-green' : 'iconcolor-red',
spfTitle:() => {
const spf = this.spfData();
return spf[0] ? spf[2] || 'SPF: ' + spf[0] : '';
},
dmarcIcon: () => {
switch (this.dmarcData()[0]) {
case 'none':
return '🚫︎';
case 'pass':
return '✔';
default:
return '✖';
}
},
dmarcIconClass: () => 'pass' === this.dmarcData()[0] ? 'iconcolor-green' : 'iconcolor-red',
dmarcTitle:() => {
const dmarc = this.dmarcData();
return dmarc[0] ? dmarc[2] || 'DMARC: ' + dmarc[0] : '';
},
showWhitelistOptions: () => 'match' === SettingsUserStore.viewImages(), showWhitelistOptions: () => 'match' === SettingsUserStore.viewImages(),
firstUnsubsribeLink: () => currentMessage()?.unsubsribeLinks()[0] || '', firstUnsubsribeLink: () => currentMessage()?.unsubsribeLinks()[0] || '',
@ -263,8 +233,6 @@ export class MailMessageView extends AbstractViewRight {
// TODO: make first param a user setting #683 // TODO: make first param a user setting #683
this.viewFromShort(message.from.toString(false, true)); this.viewFromShort(message.from.toString(false, true));
this.dkimData(message.dkim[0] || ['none', '', '']); this.dkimData(message.dkim[0] || ['none', '', '']);
this.spfData(message.spf[0] || ['none', '', '']);
this.dmarcData(message.dmarc[0] || ['none', '', '']);
this.nowTracking(false); this.nowTracking(false);
} else { } else {
MessagelistUserStore.selectedMessage(null); MessagelistUserStore.selectedMessage(null);
@ -642,19 +610,125 @@ export class MailMessageView extends AbstractViewRight {
} }
pgpDecrypt() { pgpDecrypt() {
currentMessage().decrypt(); const oMessage = currentMessage(),
data = oMessage.pgpEncrypted();
delete data.error;
PgpUserStore.decrypt(oMessage).then(result => {
if (!result) {
// TODO: translate
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 => {
data.error = e.message;
})
.finally(() => {
oMessage.pgpEncrypted(data);
});
} }
pgpVerify(/*self, event*/) { pgpVerify(/*self, event*/) {
currentMessage().pgpVerify(); const oMessage = currentMessage()/*, ctrl = event.target.closest('.openpgp-control')*/;
PgpUserStore.verify(oMessage).then(result => {
if (result) {
oMessage.pgpSigned(result);
} else {
alert('Verification failed or no valid public key found');
}
/*
if (result?.success) {
i18n('CRYPTO/GOOD_SIGNATURE', {
USER: validKey.user + ' (' + validKey.id + ')'
});
message.getText()
} else {
const keyIds = arrayLength(signingKeyIds) ? signingKeyIds : null,
additional = keyIds
? keyIds.map(item => item?.toHex?.()).filter(v => v).join(', ')
: '';
i18n('CRYPTO/ERROR', {
TYPE: 'OpenPGP',
ERROR: 'message'
}) + (additional ? ' (' + additional + ')' : '');
}
*/
});
} }
async smimeDecrypt() { async smimeDecrypt() {
currentMessage().decrypt(); const message = currentMessage();
const addresses = message.from.concat(message.to, message.cc, message.bcc).map(item => item.email),
identity = IdentityUserStore.find(item => addresses.includes(item.email())),
data = message.smimeEncrypted(); // { partId: "1" }
if (data && identity) {
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()}),
'CRYPTO/DECRYPT'
);
if (!pass) {
return;
}
params.passphrase = pass?.password;
}
Remote.post('SMimeDecryptMessage', null, params).then(response => {
if (response?.Result?.data) {
message.smimeDecrypted(true);
MimeToMessage(response.Result.data, message);
message.html() ? message.viewHtml() : message.viewPlain();
pass && pass.remember && Passphrases.handle(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*/) { smimeVerify(/*self, event*/) {
currentMessage().smimeVerify(); const message = currentMessage(),
data = message.smimeSigned(); // { partId: "1", micAlg: "pgp-sha256" }
if (data) {
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();
}
data.success = response.Result.success;
message.smimeSigned(data);
}
});
}
} }
} }

View file

@ -77,7 +77,7 @@ Now, login to [http://localhost:8888/?admin](http://localhost:8888/?admin) with
To use PostgreSQL as the DB, in Admin Panel, click `Contacts`, check `Enable contacts` and , and under `Storage (PDO)` choose the following: To use PostgreSQL as the DB, in Admin Panel, click `Contacts`, check `Enable contacts` and , and under `Storage (PDO)` choose the following:
- Type: `PostgresSQL` - Type: `PostgresSQL`
- Data Source Name (DSN): `host=db;port=5432;dbname=snappymail` - Data Source Name (DSN): `host=postgres;port=5432;dbname=snappymail`
- User `snappymail` - User `snappymail`
- Password `snappymail` - Password `snappymail`

View file

@ -1,4 +1,4 @@
This app packages SnappyMail <upstream>2.38.2</upstream>. This app packages SnappyMail <upstream>2.38.2.1</upstream>.
SnappyMail is a simple, modern, lightweight & fast web-based email client. 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 WORKDIR /app/code
# If you change the extraction below, be sure to test on scaleway # If you change the extraction below, be sure to test on scaleway
VERSION=2.38.2 VERSION=2.38.2.1
RUN wget https://github.com/the-djmaze/snappymail/releases/download/v${VERSION}/snappymail-${VERSION}.zip -O /tmp/snappymail.zip && \ 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 && \ unzip /tmp/snappymail.zip -d /app/code && \
rm /tmp/snappymail.zip && \ rm /tmp/snappymail.zip && \

View file

@ -28,12 +28,7 @@
* Custom 'data' folder path * Custom 'data' folder path
*/ */
if (class_exists('OC')) { if (class_exists('OC')) {
define('APP_DATA_FOLDER_PATH', \rtrim(\trim( define('APP_DATA_FOLDER_PATH', \rtrim(\trim(\OC::$server->getSystemConfig()->getValue('datadirectory', '')), '\\/').'/appdata_snappymail/');
\method_exists(\OC::$server, 'getSystemConfig')
? \OC::$server->getSystemConfig()->getValue('datadirectory', '')
// Nextcloud 34+
: \OC::$server->get(\OCP\IConfig::class)->getSystemValue('datadirectory', '')
), '\\/') . '/appdata_snappymail/');
} else { } else {
http_response_code(400); http_response_code(400);
header($_SERVER['SERVER_PROTOCOL'].' 400 Bad Request', true, 400); header($_SERVER['SERVER_PROTOCOL'].' 400 Bad Request', true, 400);

View file

@ -63,9 +63,8 @@ There, click on the link to go to the SnappyMail admin panel.
<lib>uuid</lib> <lib>uuid</lib>
<lib>xxtea</lib> <lib>xxtea</lib>
<lib>zip</lib> <lib>zip</lib>
<nextcloud min-version="20" max-version="34" />
--> -->
<nextcloud min-version="20" /> <nextcloud min-version="20" max-version="30" />
</dependencies> </dependencies>
<settings> <settings>
<admin>OCA\SnappyMail\Settings\AdminSettings</admin> <admin>OCA\SnappyMail\Settings\AdminSettings</admin>

View file

@ -37,11 +37,7 @@ class ContentSecurityPolicy extends \OCP\AppFramework\Http\ContentSecurityPolicy
public function getSnappyMailNonce() { public function getSnappyMailNonce() {
static $sNonce; static $sNonce;
if (!$sNonce) { if (!$sNonce) {
$cspManager = (\method_exists(\OC::$server, 'getContentSecurityPolicyNonceManager') $cspManager = \OC::$server->getContentSecurityPolicyNonceManager();
? \OC::$server->getContentSecurityPolicyNonceManager()
// Nextcloud 34+
: \OC::$server->get(\OC\Security\CSP\ContentSecurityPolicyNonceManager::class)
);
$sNonce = $cspManager->getNonce() ?: \SnappyMail\UUID::generate(); $sNonce = $cspManager->getNonce() ?: \SnappyMail\UUID::generate();
if (\method_exists($cspManager, 'browserSupportsCspV3') && !$cspManager->browserSupportsCspV3()) { if (\method_exists($cspManager, 'browserSupportsCspV3') && !$cspManager->browserSupportsCspV3()) {
$this->addAllowedScriptDomain("'nonce-{$sNonce}'"); $this->addAllowedScriptDomain("'nonce-{$sNonce}'");

View file

@ -176,8 +176,7 @@ class SnappyMailHelper
// If OpenID Connect (OIDC) is enabled and used for login, use this. // If OpenID Connect (OIDC) is enabled and used for login, use this.
if (static::isOIDCLogin()) { if (static::isOIDCLogin()) {
$sEmail = $config->getUserValue($sUID, 'settings', 'email'); $sEmail = $config->getUserValue($sUID, 'settings', 'email');
$pwd = new \SnappyMail\SensitiveString("oidc_login|{$sUID}"); return [$sUID, $sEmail, "oidc_login|{$sUID}"];
return [$sUID, $sEmail, $pwd];
} }
// Only use the user's password in the current session if they have // Only use the user's password in the current session if they have

View file

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

View file

@ -3,7 +3,7 @@
"title": "SnappyMail", "title": "SnappyMail",
"description": "Simple, modern & fast web-based email client", "description": "Simple, modern & fast web-based email client",
"private": true, "private": true,
"version": "2.38.2", "version": "2.38.2.1",
"homepage": "https://snappymail.eu", "homepage": "https://snappymail.eu",
"author": { "author": {
"name": "DJ Maze", "name": "DJ Maze",
@ -43,11 +43,10 @@
"dependencies": { "dependencies": {
"@rollup/plugin-node-resolve": "^13.1.3", "@rollup/plugin-node-resolve": "^13.1.3",
"@rollup/plugin-replace": "^3.0.1", "@rollup/plugin-replace": "^3.0.1",
"@rollup/plugin-terser": "^1.0.2",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"del": "^6.0.0", "del": "^6.0.0",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"gulp": "^5.0.1", "gulp": "^5.0.0",
"gulp-append-prepend": "^1.0.9", "gulp-append-prepend": "^1.0.9",
"gulp-cached": "^1.1.1", "gulp-cached": "^1.1.1",
"gulp-clean-css": "^4.3.0", "gulp-clean-css": "^4.3.0",
@ -65,6 +64,7 @@
"gulp-terser": "^2.1.0", "gulp-terser": "^2.1.0",
"rollup": "^2.56.3", "rollup": "^2.56.3",
"rollup-plugin-external-globals": "^0.6.1", "rollup-plugin-external-globals": "^0.6.1",
"rollup-plugin-includepaths": "^0.2.4" "rollup-plugin-includepaths": "^0.2.4",
"rollup-plugin-terser": "^7.0.2"
} }
} }

View file

@ -38,10 +38,7 @@
queue = [], queue = [],
avatars = new Map, avatars = new Map,
ncAvatars = new Map, ncAvatars = new Map,
identicons = new Map,
templateId = 'MailMessageView', templateId = 'MailMessageView',
b64 = data => btoa(unescape(encodeURIComponent(data))),
b64url = data => b64(data).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
getBimiSelector = msg => { getBimiSelector = msg => {
// Get 's' value out of 'v=BIMI1; s=foo;' // Get 's' value out of 'v=BIMI1; s=foo;'
let bimiSelector = msg.headers().valueByName('BIMI-Selector'); let bimiSelector = msg.headers().valueByName('BIMI-Selector');
@ -49,21 +46,9 @@
return bimiSelector || ''; return bimiSelector || '';
}, },
getBimiId = msg => ('pass' == msg.from[0].dkimStatus ? 1 : 0) + '-' + getBimiSelector(msg), getBimiId = msg => ('pass' == msg.from[0].dkimStatus ? 1 : 0) + '-' + getBimiSelector(msg),
email = msg => msg.from[0].email.toLowerCase(), getAvatarUrl = msg => `?Avatar/${getBimiId(msg)}/${msg.avatar}`,
getAvatarUid = msg => `${getBimiId(msg)}/${email(msg)}`, getAvatarUid = msg => `${getBimiId(msg)}/${msg.from[0].email.toLowerCase()}`,
getAvatar = msg => ncAvatars.get(email(msg)) || avatars.get(getAvatarUid(msg)), getAvatar = msg => ncAvatars.get(msg.from[0].email.toLowerCase()) || avatars.get(getAvatarUid(msg)),
getAvatarUri = msg => {
if ('remote' === msg.avatar) {
msg.avatar = `?Avatar/${getBimiId(msg)}/${b64url(email(msg))}`;
}
/*
else if (!msg.avatar.startsWith('data:')) {
msg.avatar = null;
}
*/
return msg.avatar;
},
hash = async txt => { hash = async txt => {
if (/^[0-9a-f]{15,}$/i.test(txt)) { if (/^[0-9a-f]{15,}$/i.test(txt)) {
return txt; return txt;
@ -79,56 +64,58 @@
(from.name?.split(/[^\p{L}]+/gu) || []).reduce((a, s) => a + (s[0] || ''), '') (from.name?.split(/[^\p{L}]+/gu) || []).reduce((a, s) => a + (s[0] || ''), '')
.slice(0,2) .slice(0,2)
.toUpperCase(), .toUpperCase(),
setIdenticon = (msg, fn, cb) => { setIdenticon = (from, fn) => hash(from.email).then(hash =>
const from = email(msg); fn('data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(window.identiconSvg(
if (identicons.get(from)) {
fn(identicons.get(from));
} else {
hash(from).then(hash => {
const uri = 'data:image/svg+xml;base64,' + b64(window.identiconSvg(
hash, hash,
fromChars(msg.from[0]), fromChars(from),
window.getComputedStyle(getEl('rl-app'), null).getPropertyValue('font-family') window.getComputedStyle(getEl('rl-app'), null).getPropertyValue('font-family')
)); )))))
fn(uri); ),
identicons.set(email(msg), uri);
cb?.(uri);
});
}
},
addQueue = (msg, fn) => { addQueue = (msg, fn) => {
if (msg.from?.[0]) { msg.from?.[0] && setIdenticon(msg.from[0], fn);
if (getAvatarUri(msg)) {
if (rl.pluginSettingsGet('avatars', 'delay')) { if (rl.pluginSettingsGet('avatars', 'delay')) {
setIdenticon(msg, fn, ()=>{
queue.push([msg, fn]); queue.push([msg, fn]);
runQueue(); runQueue();
}); } else if (msg.avatar) {
} else { fn(getAvatarUrl(msg));
fn(msg.avatar);
}
} else {
setIdenticon(msg, fn);
}
} }
}, },
runQueue = (() => { runQueue = (() => {
let item = queue.shift(); let item = queue.shift();
while (item) { while (item) {
let url = getAvatar(item[0]); if (item[0].from) {
let url = getAvatar(item[0]),
uid = getAvatarUid(item[0]);
if (url) { if (url) {
item[1](url); item[1](url);
item = queue.shift(); item = queue.shift();
continue; continue;
} else if (item[0].avatar) { } else if (item[0].avatar) {
item[1](item[0].avatar); item[1](getAvatarUrl(item[0]));
} else if (!avatars.has(uid)) {
let from = item[0].from[0];
rl.pluginRemoteRequest((iError, data) => {
if (!iError && data?.Result.type) {
url = `data:${data.Result.type};base64,${data.Result.data}`;
avatars.set(uid, url);
item[1](url);
} else {
avatars.set(uid, '');
}
runQueue();
}, 'Avatar', {
bimi: 'pass' == from.dkimStatus ? 1 : 0,
bimiSelector: getBimiSelector(item[0]),
email: from.email
});
}
} }
runQueue(); runQueue();
break; break;
} }
}).debounce(1000); }).debounce(1000);
// addEventListener('DOMContentLoaded', () => {
/** /**
* Modify templates * Modify templates
*/ */
@ -201,19 +188,13 @@
fn = url=>{element.src = url}; fn = url=>{element.src = url};
element.onerror = ()=>{ element.onerror = ()=>{
element.onerror = null; element.onerror = null;
setIdenticon(msg, fn, uri=>avatars.set(getAvatarUid(msg), uri)); setIdenticon(msg.from[0], fn);
}; };
if (url) { if (url) {
fn(url); fn(url);
} else if (msg.avatar?.startsWith('data:')) { } else if (msg.avatar?.startsWith('data:')) {
fn(msg.avatar); fn(msg.avatar);
} else { } else {
element.onload = ()=>{
if (!element.src.startsWith('data:')) {
element.onload = null;
avatars.set(getAvatarUid(msg), element.src);
}
};
addQueue(msg, fn); addQueue(msg, fn);
} }
} }
@ -240,8 +221,11 @@
if (url) { if (url) {
fn(url); fn(url);
} else if (msg.avatar) { } else if (msg.avatar) {
fn(getAvatarUri(msg)); fn(msg.avatar.startsWith('data:') ? msg.avatar : getAvatarUrl(msg));
} else { } else {
// let from = msg.from[0];
// view.viewUserPic(`?Avatar/${'pass' == from.dkimStatus ? 1 : 0}/${encodeURIComponent(from.email)}`);
// view.viewUserPicVisible(true);
addQueue(msg, fn); addQueue(msg, fn);
} }
} }

View file

@ -10,12 +10,12 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
NAME = 'Avatars', NAME = 'Avatars',
AUTHOR = 'SnappyMail', AUTHOR = 'SnappyMail',
URL = 'https://snappymail.eu/', URL = 'https://snappymail.eu/',
VERSION = '1.22', VERSION = '1.20',
RELEASE = '2025-03-10', RELEASE = '2024-08-26',
REQUIRED = '2.33.0', REQUIRED = '2.33.0',
CATEGORY = 'Contacts', CATEGORY = 'Contacts',
LICENSE = 'MIT', LICENSE = 'MIT',
DESCRIPTION = 'Show graphic of sender in message and messages list (supports Contacts, BIMI, Gravatar, favicon and identicon)'; DESCRIPTION = 'Show graphic of sender in message and messages list (supports BIMI, Gravatar, favicon and identicon, Contacts is still TODO)';
public function Init() : void public function Init() : void
{ {
@ -79,8 +79,11 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
|| ($this->Config()->Get('plugin', 'bimi', false) && 'pass' == $mFrom['dkimStatus']) || ($this->Config()->Get('plugin', 'bimi', false) && 'pass' == $mFrom['dkimStatus'])
|| ($this->Config()->Get('plugin', 'favicon', false) && 'pass' == $mFrom['dkimStatus']) || ($this->Config()->Get('plugin', 'favicon', false) && 'pass' == $mFrom['dkimStatus'])
) )
) { ) try {
return 'remote'; // Base64Url
return \SnappyMail\Crypt::EncryptUrlSafe($mFrom['email']);
} catch (\Throwable $e) {
\SnappyMail\Log::error('Crypt', $e->getMessage());
} }
if ('pass' == $mFrom['dkimStatus'] && $this->Config()->Get('plugin', 'service', true)) { if ('pass' == $mFrom['dkimStatus'] && $this->Config()->Get('plugin', 'service', true)) {
// 'data:image/png;base64,[a-zA-Z0-9+/=]' // 'data:image/png;base64,[a-zA-Z0-9+/=]'
@ -109,17 +112,17 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
} }
/** /**
* GET /?Avatar/${bimi}/Encoded(${from.email}) * GET /?Avatar/${bimi}/Encrypted(${from.email})
* Nextcloud Mail uses insecure unencoded 'index.php/apps/mail/api/avatars/url/local%40example.com' * Nextcloud Mail uses insecure unencrypted 'index.php/apps/mail/api/avatars/url/local%40example.com'
*/ */
// public function ServiceAvatar(...$aParts) // public function ServiceAvatar(...$aParts)
public function ServiceAvatar(string $sServiceName, string $sBimi, string $sEncodedEmail) public function ServiceAvatar(string $sServiceName, string $sBimi, string $sEncryptedEmail)
{ {
$maxAge = 86400; $maxAge = 86400;
$sEmail = \MailSo\Base\Utils::UrlSafeBase64Decode($sEncodedEmail); $sEmail = \SnappyMail\Crypt::DecryptUrlSafe($sEncryptedEmail);
$aBimi = \explode('-', $sBimi, 2); $aBimi = \explode('-', $sBimi, 2);
$sBimiSelector = isset($aBimi[1]) ? $aBimi[1] : 'default'; $sBimiSelector = isset($aBimi[1]) ? $aBimi[1] : 'default';
// $sEmail && \MailSo\Base\Http::setETag("{$sBimiSelector}-{$sEncodedEmail}"); // $sEmail && \MailSo\Base\Http::setETag("{$sBimiSelector}-{$sEncryptedEmail}");
if ($sEmail && ($aResult = $this->getAvatar($sEmail, !empty($aBimi[0]), $sBimiSelector))) { if ($sEmail && ($aResult = $this->getAvatar($sEmail, !empty($aBimi[0]), $sBimiSelector))) {
\header("Cache-Control: max-age={$maxAge}, private"); \header("Cache-Control: max-age={$maxAge}, private");
\header('Expires: '.\gmdate('D, j M Y H:i:s', $maxAge + \time()).' UTC'); \header('Expires: '.\gmdate('D, j M Y H:i:s', $maxAge + \time()).' UTC');
@ -139,10 +142,6 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetAllowedInJs(true) ->SetAllowedInJs(true)
->SetDefaultValue(true), ->SetDefaultValue(true),
\RainLoop\Plugins\Property::NewInstance('contacts')->SetLabel('Contacts')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDescription('Use the PHOTO stored on a matching address book contact')
->SetDefaultValue(true),
\RainLoop\Plugins\Property::NewInstance('bimi')->SetLabel('BIMI') \RainLoop\Plugins\Property::NewInstance('bimi')->SetLabel('BIMI')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL) ->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDefaultValue(false) ->SetDefaultValue(false)
@ -189,8 +188,11 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
return $aResult; return $aResult;
} }
private static function getServicePng(string $sDomain) : ?string // Only allow service icon when DKIM is valid. $bBimi is true when DKIM is valid.
private static function getServiceIcon(string $sEmail) : ?string
{ {
$sDomain = \explode('@', $sEmail);
$sDomain = \array_pop($sDomain);
$aServices = [ $aServices = [
"services/{$sDomain}", "services/{$sDomain}",
'services/' . static::serviceDomain($sDomain) 'services/' . static::serviceDomain($sDomain)
@ -198,20 +200,9 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
foreach ($aServices as $service) { foreach ($aServices as $service) {
$file = __DIR__ . "/images/{$service}.png"; $file = __DIR__ . "/images/{$service}.png";
if (\file_exists($file)) { if (\file_exists($file)) {
return $file;
}
}
return null;
}
// Only allow service icon when DKIM is valid. $bBimi is true when DKIM is valid.
private static function getServiceIcon(string $sEmail) : ?string
{
$aParts = \explode('@', $sEmail);
$file = static::getServicePng(\array_pop($aParts));
if ($file) {
return 'data:image/png;base64,' . \base64_encode(\file_get_contents($file)); return 'data:image/png;base64,' . \base64_encode(\file_get_contents($file));
} }
}
$aResult = static::getCachedImage($sEmail); $aResult = static::getCachedImage($sEmail);
if ($aResult) { if ($aResult) {
@ -234,23 +225,30 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
\header('Cache-Control: private'); \header('Cache-Control: private');
// \header('Expires: '.\gmdate('D, j M Y H:i:s', \time() + 86400).' UTC'); // \header('Expires: '.\gmdate('D, j M Y H:i:s', \time() + 86400).' UTC');
// A photo stored on the contact is authoritative, so it is consulted
// before the cache and before any network lookup: it beats a guessed
// avatar and replaces up to five remote round-trips. Deliberately not
// written to the avatars cache, which has no expiry - a cached copy
// would shadow later edits to the contact.
if ($this->Config()->Get('plugin', 'contacts', true)) {
$aResult = static::getContactPhoto($sAsciiEmail);
if ($aResult) {
return $aResult;
}
}
$aResult = static::getCachedImage($sEmail); $aResult = static::getCachedImage($sEmail);
if ($aResult) { if ($aResult) {
return $aResult; 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);
if ($oAddressBookProvider) {
$oContact = $oAddressBookProvider->GetContactByEmail($sEmail);
if ($oContact && $oContact->vCard && $oContact->vCard['PHOTO']) {
$aResult = [
'text/vcard',
$oContact->vCard
];
}
}
}
}
*/
if (!$aResult) { if (!$aResult) {
$sDomain = \explode('@', $sEmail); $sDomain = \explode('@', $sEmail);
@ -270,7 +268,7 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
} }
if ($this->Config()->Get('plugin', 'gravatar', false)) { if ($this->Config()->Get('plugin', 'gravatar', false)) {
$aUrls[] = 'https://gravatar.com/avatar/'.\hash('sha256', \strtolower($sAsciiEmail)).'?s=80&d=404'; $aUrls[] = 'https://gravatar.com/avatar/'.\md5(\strtolower($sAsciiEmail)).'?s=80&d=404';
} }
foreach ($aUrls as $sUrl) { foreach ($aUrls as $sUrl) {
@ -286,13 +284,20 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
// Only allow service icon when DKIM is valid. $bBimi is true when DKIM is valid. // Only allow service icon when DKIM is valid. $bBimi is true when DKIM is valid.
if ($bBimi && !$aResult) { if ($bBimi && !$aResult) {
$file = static::getServicePng($sDomain); $aServices = [
if ($file) { "services/{$sDomain}",
'services/' . static::serviceDomain($sDomain)
];
foreach ($aServices as $service) {
$file = __DIR__ . "/images/{$service}.png";
if (\file_exists($file)) {
\MailSo\Base\Http::setLastModified(\filemtime($file)); \MailSo\Base\Http::setLastModified(\filemtime($file));
$aResult = [ $aResult = [
'image/png', 'image/png',
\file_get_contents($file) \file_get_contents($file)
]; ];
break;
}
} }
if (!$aResult && $this->Config()->Get('plugin', 'favicon', false)) { if (!$aResult && $this->Config()->Get('plugin', 'favicon', false)) {
@ -313,77 +318,6 @@ class AvatarsPlugin extends \RainLoop\Plugins\AbstractPlugin
return $sDomain; return $sDomain;
} }
/**
* Return the PHOTO of the address book contact owning this address.
*
* vCard 4.0 stores it as a data: URI, vCard 3.0 as ENCODING=b with a TYPE
* parameter, and either version may point at an external URL instead.
*/
private static function getContactPhoto(string $sEmail) : ?array
{
try {
$oActions = \RainLoop\Api::Actions();
$oAccount = $oActions->getAccountFromToken(false);
if (!$oAccount) {
return null;
}
$oProvider = $oActions->AddressBookProvider($oAccount);
if (!$oProvider || !$oProvider->IsActive()) {
return null;
}
// Several contacts can share an address - a synced card, a manual
// entry, an auto-collected one - and only one of them may carry the
// photo. GetContactByEmail() returns whichever matches first, so
// fall back to scanning the matches for one that actually has it.
$oPhoto = null;
$oContact = $oProvider->GetContactByEmail($sEmail);
if ($oContact && $oContact->vCard && isset($oContact->vCard->PHOTO)) {
$oPhoto = $oContact->vCard->PHOTO;
} else {
$iCount = 0;
foreach ($oProvider->GetContacts(0, 20, $sEmail, $iCount) as $oCandidate) {
if ($oCandidate->vCard && isset($oCandidate->vCard->PHOTO)) {
$oPhoto = $oCandidate->vCard->PHOTO;
break;
}
}
}
if (!$oPhoto) {
return null;
}
$sValue = \trim((string) $oPhoto);
if (!\strlen($sValue)) {
return null;
}
// vCard 4.0: data:image/jpeg;base64,...
if (\preg_match('#^data:(image/[a-z0-9.+-]+);base64,(.+)$#si', $sValue, $aMatch)) {
$sBinary = \base64_decode(\preg_replace('#\s+#', '', $aMatch[2]), true);
return $sBinary ? [\strtolower($aMatch[1]), $sBinary] : null;
}
// vCard 3.0: PHOTO;ENCODING=b;TYPE=JPEG:<base64>
$sEncoding = \strtolower((string) ($oPhoto['ENCODING'] ?? ''));
if ('b' === $sEncoding || 'base64' === $sEncoding) {
$sBinary = \base64_decode(\preg_replace('#\s+#', '', $sValue), true);
if ($sBinary) {
$sType = \strtolower((string) ($oPhoto['TYPE'] ?? '')) ?: 'jpeg';
return [\str_starts_with($sType, 'image/') ? $sType : "image/{$sType}", $sBinary];
}
return null;
}
// Either version may reference the image instead of embedding it.
if (\preg_match('#^https?://#i', $sValue)) {
return static::getUrl($sValue);
}
} catch (\Throwable $oException) {
\SnappyMail\Log::notice('Avatar', 'contact photo lookup failed: ' . $oException->getMessage());
}
return null;
}
private static function cacheImage(string $sEmail, array $aResult) : void private static function cacheImage(string $sEmail, array $aResult) : void
{ {
if (!\is_dir(\APP_PRIVATE_DATA . 'avatars')) { if (!\is_dir(\APP_PRIVATE_DATA . 'avatars')) {

View file

@ -1,13 +0,0 @@
[SETTINGS_CHANGE_PASSWORD]
LEGEND_CHANGE_PASSWORD = "Змінити пароль"
LABEL_CURRENT_PASSWORD = "Поточний пароль"
LABEL_NEW_PASSWORD = "Новий пароль"
LABEL_REPEAT_PASSWORD = "Підтвердьте новий пароль"
BUTTON_UPDATE_PASSWORD = "Змінити"
ERROR_PASSWORD_MISMATCH = "Паролі не збігаються, спробуйте ще раз"
[NOTIFICATIONS]
COULD_NOT_SAVE_NEW_PASSWORD = "Не вдалося зберегти новий пароль"
CURRENT_PASSWORD_INCORRECT = "Поточний пароль не вірний"
NEW_PASSWORD_SHORT = "Пароль закороткий"
NEW_PASSWORD_WEAK = "Пароль надто легкий"
NEW_PASSWORD_HIBP = "Пароль знайдено в сервісі Have I Been Pwned"

View file

@ -6,7 +6,7 @@ class CompactComposerPlugin extends \RainLoop\Plugins\AbstractPlugin
NAME = 'Compact Composer', NAME = 'Compact Composer',
AUTHOR = 'Sergey Mosin', AUTHOR = 'Sergey Mosin',
URL = 'https://github.com/the-djmaze/snappymail/pull/1466', URL = 'https://github.com/the-djmaze/snappymail/pull/1466',
VERSION = '1.0.6', VERSION = '1.0.5',
RELEASE = '2024-08-08', RELEASE = '2024-08-08',
REQUIRED = '2.34.0', REQUIRED = '2.34.0',
LICENSE = 'AGPL v3', LICENSE = 'AGPL v3',

View file

@ -100,6 +100,10 @@
} }
}, },
pasteSanitizer = (event) => {
return rl.Utils.cleanHtml(event.detail.html).html;
},
pasteImageHandler = (e, squire) => { pasteImageHandler = (e, squire) => {
const items = [...e.detail.clipboardData.items]; const items = [...e.detail.clipboardData.items];
@ -138,12 +142,7 @@
toolbar.className = 'squire-toolbar btn-toolbar'; toolbar.className = 'squire-toolbar btn-toolbar';
const actions = this.makeActions(squire, toolbar); const actions = this.makeActions(squire, toolbar);
this.squire.addEventListener('willPaste', (event) => { this.squire.addEventListener('willPaste', pasteSanitizer);
// https://github.com/fastmail/Squire?tab=readme-ov-file#addeventlistener
// The content that will be inserted is available as either the fragment property, or the text property for plain text, on the detail property of the event. You can modify this text/fragment in your event handler to change what will be pasted
tpl.innerHTML = rl.Utils.cleanHtml(event.detail.html).html;
event.detail.fragment = tpl.content;
});
this.squire.addEventListener('pasteImage', (e) => { this.squire.addEventListener('pasteImage', (e) => {
pasteImageHandler(e, squire); pasteImageHandler(e, squire);
}); });

File diff suppressed because it is too large Load diff

View file

@ -5,8 +5,8 @@ class ICSViewerPlugin extends \RainLoop\Plugins\AbstractPlugin
const const
NAME = 'ICS Viewer', NAME = 'ICS Viewer',
AUTHOR = 'PhieF', AUTHOR = 'PhieF',
VERSION = '1.1', VERSION = '1.0',
RELEASE = '2026-10-22', RELEASE = '2024-09-17',
CATEGORY = 'Messages', CATEGORY = 'Messages',
DESCRIPTION = 'Display ICS attachment using ical lib, or JSON-LD details, based on viewICS', DESCRIPTION = 'Display ICS attachment using ical lib, or JSON-LD details, based on viewICS',
REQUIRED = '2.34.0'; REQUIRED = '2.34.0';

View file

@ -86,17 +86,10 @@
}); });
} }
}); });
// ICS attachment. // ICS attachment
// attachments is a ko.observableArray, so it must be // let ics = msg.attachments.find(attachment => 'application/ics' == attachment.mimeType);
// unwrapped before array methods are used on it.
// A scheduling mail does not always carry a text/calendar let ics = msg.attachments.find(attachment => 'text/calendar' == attachment.mimeType);
// attachment either: the part is often inline inside
// multipart/alternative and repeated as application/ics.
let ics = (msg.attachments() || []).find(attachment =>
'text/calendar' == attachment.mimeType
|| 'application/ics' == attachment.mimeType
|| 'text/x-vcalendar' == attachment.mimeType
|| /\.ics$/i.test(attachment.fileName || ''));
if (ics && ics.download) { if (ics && ics.download) {
// fetch it and parse the VEVENT // fetch it and parse the VEVENT

View file

@ -139,18 +139,8 @@ class LdapContactsSuggestions implements \RainLoop\Providers\Suggestions\ISugges
$sFilter .= (1 < count($aItems) ? '(|' : '').$sSubFilter.(1 < count($aItems) ? ')' : ''); $sFilter .= (1 < count($aItems) ? '(|' : '').$sSubFilter.(1 < count($aItems) ? ')' : '');
$sFilter .= ')'; $sFilter .= ')';
// A directory rarely keeps everything worth suggesting in one branch: $this->logWrite('ldap_search: start: '.$sBaseDn.' / '.$sFilter, \LOG_INFO, 'LDAP');
// meeting rooms and other bookable resources commonly live outside $oS = @\ldap_search($oCon, $sBaseDn, $sFilter, $aItems, 0, 30, 30);
// the people branch. Searching a single subtree either misses them,
// or - if the base is widened to the domain root - drags every
// service account into the suggestion list. Base DNs are therefore
// separated by '|', which cannot appear unescaped in a DN, so an
// existing single-branch configuration keeps working unchanged.
$aBaseDns = \array_filter(\array_map('trim', \explode('|', $sBaseDn)), 'strlen');
foreach ($aBaseDns as $sOneBaseDn) {
$this->logWrite('ldap_search: start: '.$sOneBaseDn.' / '.$sFilter, \LOG_INFO, 'LDAP');
$oS = @\ldap_search($oCon, $sOneBaseDn, $sFilter, $aItems, 0, 30, 30);
if ($oS) { if ($oS) {
$aEntries = @\ldap_get_entries($oCon, $oS); $aEntries = @\ldap_get_entries($oCon, $oS);
if (is_array($aEntries)) { if (is_array($aEntries)) {
@ -171,9 +161,7 @@ class LdapContactsSuggestions implements \RainLoop\Providers\Suggestions\ISugges
$this->logLdapError($oCon, 'ldap_get_entries'); $this->logLdapError($oCon, 'ldap_get_entries');
} }
} else { } else {
// One unreachable branch must not silence the others. $this->logLdapError($oCon, 'ldap_search');
$this->logLdapError($oCon, 'ldap_search ('.$sOneBaseDn.')');
}
} }
} }

View file

@ -4,8 +4,8 @@ class LdapContactsSuggestionsPlugin extends \RainLoop\Plugins\AbstractPlugin
{ {
const const
NAME = 'Contacts suggestions (LDAP)', NAME = 'Contacts suggestions (LDAP)',
VERSION = '2.15', VERSION = '2.14',
RELEASE = '2026-08-19', RELEASE = '2024-03-12',
REQUIRED = '2.35.3', REQUIRED = '2.35.3',
CATEGORY = 'Contacts', CATEGORY = 'Contacts',
DESCRIPTION = 'Get contacts suggestions from LDAP.'; DESCRIPTION = 'Get contacts suggestions from LDAP.';
@ -79,7 +79,7 @@ class LdapContactsSuggestionsPlugin extends \RainLoop\Plugins\AbstractPlugin
->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD) ->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD)
->SetDefaultValue(''), ->SetDefaultValue(''),
\RainLoop\Plugins\Property::NewInstance('base_dn')->SetLabel('Search base DN') \RainLoop\Plugins\Property::NewInstance('base_dn')->SetLabel('Search base DN')
->SetDescription('DN to use as the search base. Supported tokens: {domain}, {domain:dc}, {email}, {email:user}, {email:domain}, {login}, {imap:login}, {imap:host}, {imap:port} Several branches may be given, separated by | - useful when meeting rooms or other resources live outside the people branch.') ->SetDescription('DN to use as the search base. Supported tokens: {domain}, {domain:dc}, {email}, {email:user}, {email:domain}, {login}, {imap:login}, {imap:host}, {imap:port}')
->SetDefaultValue('ou=People,dc=example,dc=com'), ->SetDefaultValue('ou=People,dc=example,dc=com'),
\RainLoop\Plugins\Property::NewInstance('object_classes')->SetLabel('objectClasses') \RainLoop\Plugins\Property::NewInstance('object_classes')->SetLabel('objectClasses')
->SetDescription('LDAP objectClasses to search for, comma separated list') ->SetDescription('LDAP objectClasses to search for, comma separated list')

View file

@ -1 +1 @@
This plugins allows to authenticate using LDAP mail field, yet login field would still be used for IMAP and SMTP authentication. Plugin which allows you to set up LDAP username by email address (when IMAP/SMTP requires it)

View file

@ -8,12 +8,12 @@ class LDAPLoginMappingPlugin extends AbstractPlugin
{ {
const const
NAME = 'LDAP login mapping', NAME = 'LDAP login mapping',
VERSION = '2.4', VERSION = '2.3',
AUTHOR = 'RainLoop Team, Ludovic Pouzenc<ludovic@pouzenc.fr>, ZephOne<zephone@protonmail.com>', AUTHOR = 'RainLoop Team, Ludovic Pouzenc<ludovic@pouzenc.fr>, ZephOne<zephone@protonmail.com>',
RELEASE = '2025-01-09', RELEASE = '2024-09-20',
REQUIRED = '2.36.1', REQUIRED = '2.36.1',
CATEGORY = 'Login', CATEGORY = 'Login',
DESCRIPTION = 'This plugins allows to authenticate using LDAP mail field, yet login field would still be used for IMAP and SMTP authentication'; DESCRIPTION = 'Enable custom mapping using ldap field';
/** /**
* @var array * @var array
*/ */
@ -104,10 +104,9 @@ class LDAPLoginMappingPlugin extends AbstractPlugin
$sResult = $this->ldapSearch($sEmail); $sResult = $this->ldapSearch($sEmail);
if ( is_array($sResult) ) { if ( is_array($sResult) ) {
$sImapUser = $sResult['login']; $sImapUser = $sResult['login'];
$sSmtpUser = $sResult['login'];
$sEmail = $sResult['email']; $sEmail = $sResult['email'];
} }
syslog(LOG_WARNING, "plugins/ldap-login-mapping/index.php:FilterLoginCredentials() auth try: $sIP/$sEmail, resolved as $sImapUser/$sEmail"); syslog(LOG_WARNING, "plugins/ldap-login-mapping/index.php:FilterLoginСredentials() auth try: $sIP/$sEmail, resolved as $sImapUser/$sEmail");
} }
} }

View file

@ -1,20 +0,0 @@
The MIT License (MIT)
Copyright (c) 2016 RainLoop Team
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1,39 +0,0 @@
<?php
class LoginHttpPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Login HTTP',
AUTHOR = 'Alterative Editions',
URL = 'https://alterative.fr/',
VERSION = '1.0',
RELEASE = '2026-02-09',
REQUIRED = '2.21.0',
CATEGORY = 'Login',
LICENSE = 'MIT',
DESCRIPTION = 'Automatic HTTP Login : uses PHP_AUTH_USER & PHP_AUTH_PW for IMAP connection';
public function Init() : void
{
$this->addPartHook('ExternalHTTP', 'ServiceExternalHTTP');
}
public function ServiceExternalHTTP() : string
{
$oActions = \RainLoop\Api::Actions();
$oActions->Http()->ServerNoCache();
$sUser=$_SERVER['PHP_AUTH_USER'] ?? '';
$sPassword=$_SERVER['PHP_AUTH_PW'] ?? '';
try {
$oAccount = $oActions->LoginProcess($sUser, new \SnappyMail\SensitiveString($sPassword));
if ($oAccount) {
\MailSo\Base\Http::Location('./');
return true;
}
} catch (\Throwable $e) {
// Log l'erreur si besoin
}
\MailSo\Base\Http::Location('./?ExternalHTTP');
return true;
}
}

View file

@ -1,91 +1,54 @@
((rl) => { (rl => {
const client_id = rl.pluginSettingsGet("login-o365", "client_id"), const client_id = rl.pluginSettingsGet('login-o365', 'client_id'),
allowAnyDomain = !!rl.pluginSettingsGet("login-o365", "allow_any_domain"), // https://learn.microsoft.com/en-us/entra/identity-platform/reply-url#query-parameter-support-in-redirect-uris
isSupportedEmail = (email) => { query = rl.pluginSettingsGet('login-o365', 'personal') ? '' : '?',
email = (email || "").toLowerCase(); tenant = rl.pluginSettingsGet('login-o365', 'tenant'),
if (!email.includes("@")) return false; login = () => {
if (allowAnyDomain) return true; document.location = 'https://login.microsoftonline.com/'+tenant+'/oauth2/v2.0/authorize?' + (new URLSearchParams({
return /@(outlook\.com|hotmail\.com|live\.com)$/.test(email); response_type: 'code',
}, client_id: client_id,
startOAuth = (op, opts = {}) => { redirect_uri: document.location.href.replace(/\/$/, '') + '/' + query + 'LoginO365',
const email = (opts.email || "").toLowerCase(); scope: [
const name = opts.name || ""; // Associate personal info
const returnHash = opts.return || ""; 'openid',
'offline_access',
// Server mints a signed state + correct redirect_uri and returns full authUrl. 'email',
rl.pluginRemoteRequest((iError, data) => { 'profile',
const url = data?.Result?.authUrl; // Access IMAP and SMTP through OAUTH
if (!iError && url) { 'https://graph.microsoft.com/IMAP.AccessAsUser.All',
document.location = url; // 'https://graph.microsoft.com/Mail.ReadWrite'
} 'https://graph.microsoft.com/Mail.Send'
}, "LoginO365AuthUrl", { /* // Legacy:
op: op, 'https://outlook.office.com/SMTP.Send',
email: email, 'https://outlook.office.com/IMAP.AccessAsUser.All'
name: name, */
return: returnHash, ].join(' '),
}); state: 'o365', // + rl.settings.app('token') + localStorage.getItem('smctoken')
// Force authorize screen, so we always get a refresh_token
access_type: 'offline',
prompt: 'consent'
}));
}; };
if (client_id) { if (client_id) {
addEventListener("sm-user-login", (e) => { addEventListener('sm-user-login', e => {
const email = (e.detail.get("Email") || "").toLowerCase(); if (event.detail.get('Email').includes('@hotmail.com')) {
if (isSupportedEmail(email)) {
e.preventDefault(); e.preventDefault();
startOAuth("login", { email }); login();
} }
}); });
addEventListener("rl-view-model", (e) => { addEventListener('rl-view-model', e => {
if ("Login" === e.detail.viewModelTemplateID) { if ('Login' === e.detail.viewModelTemplateID) {
const const
container = e.detail.viewModelDom.querySelector("#plugin-Login-BottomControlGroup"), container = e.detail.viewModelDom.querySelector('#plugin-Login-BottomControlGroup'),
btn = Element.fromHTML('<button type="button">Outlook</button>'), btn = Element.fromHTML('<button type="button">Outlook</button>'),
div = Element.fromHTML('<div class="controls"></div>'); div = Element.fromHTML('<div class="controls"></div>');
btn.onclick = () => { btn.onclick = login;
// Best-effort: try to read the email field if present.
const input = e.detail.viewModelDom.querySelector('input[type="email"], input[name="Email"], input[name="email"], input');
const email = (input?.value || "").toLowerCase();
if (!email || isSupportedEmail(email)) {
startOAuth("login", { email });
}
};
div.append(btn); div.append(btn);
container && container.append(div); container && container.append(div);
} }
// "Add account" popup (Settings → Accounts → Add account)
if ("PopupsAccount" === e.detail.viewModelTemplateID) {
// Only for the "Add account" mode, not "Edit account".
if (typeof e.detail.isNew === "function" && !e.detail.isNew()) {
return;
}
const root = e.detail.viewModelDom;
if (!root) return;
const footer = root.querySelector("footer");
const form = root.querySelector("#accountform");
const addButton = root.querySelector("button.buttonAddAccount");
if (!footer || !form || !addButton) return;
// Avoid inserting duplicates when view model is re-rendered.
if (root.querySelector(".plugin-o365-add-account")) return;
const btn = Element.fromHTML(
'<button type="button" class="btn plugin-o365-add-account" style="margin-left: 6px;">Outlook</button>'
);
btn.onclick = () => {
const email = (form.querySelector('input[name="email"]')?.value || "").trim().toLowerCase();
const name = (form.querySelector('input[name="name"]')?.value || "").trim();
if (!email || !isSupportedEmail(email)) {
return;
}
startOAuth("add", { email, name, return: document.location.hash || "#/settings/accounts" });
};
// Put the button next to the default Add Account submit button.
footer.insertBefore(btn, addButton.nextSibling);
}
}); });
} }
})(window.rl); })(window.rl);

View file

@ -1,20 +1,15 @@
<?php <?php
/** /**
* SnappyMail login-o365 plugin * Microsoft requires an Azure account that has an active subscription
* You need to register an app in Azure portal and add * I'm not going to pay, so feel free to fix this code yourself.
* a secret, redirect URIs and the following API permissions: * https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth
* https://outlook.office.com/IMAP.AccessAsUser.All * https://answers.microsoft.com/en-us/msoffice/forum/all/configuration-for-imap-pop-and-smtp-with-oauth-in/3db47d43-25ac-4e0b-b957-22585e6caf15
* https://outlook.office.com/SMTP.Send
* openid offline_access email profile
* https://learn.microsoft.com/en-us/entra/identity-platform/reply-url#query-parameter-support-in-redirect-uris
* Query: redirect_uri=https://{DOMAIN}/?LoginO365
* Path: redirect_uri=https://{DOMAIN}/LoginO365
* *
* If running behind nginx reverse proxy you might * https://portal.azure.com/#view/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/~/RegisteredApps
* need to add the following to your nginx config: *
* location = /LoginO365 { * https://learn.microsoft.com/en-us/entra/identity-platform/reply-url#query-parameter-support-in-redirect-uris
* return 302 /?LoginO365&$args; * Azure: redirect_uri=https://{DOMAIN}/?LoginO365
* } * Personal: redirect_uri=https://{DOMAIN}/LoginO365
*/ */
use RainLoop\Model\MainAccount; use RainLoop\Model\MainAccount;
@ -24,28 +19,18 @@ class LoginO365Plugin extends \RainLoop\Plugins\AbstractPlugin
{ {
const const
NAME = 'Office365/Outlook OAuth2', NAME = 'Office365/Outlook OAuth2',
VERSION = '0.4', VERSION = '0.3',
RELEASE = '2025-12-22', RELEASE = '2024-09-29',
REQUIRED = '2.36.1', REQUIRED = '2.36.1',
CATEGORY = 'Login', CATEGORY = 'Login',
DESCRIPTION = 'Office365/Outlook IMAP, Sieve & SMTP login using RFC 7628 OAuth2'; DESCRIPTION = 'Office365/Outlook IMAP, Sieve & SMTP login using RFC 7628 OAuth2';
// v2 endpoints // https://login.microsoftonline.com/{{tenant}}/v2.0/.well-known/openid-configuration
const const
AUTH_URI = 'https://login.microsoftonline.com/{{tenant}}/oauth2/v2.0/authorize', LOGIN_URI = 'https://login.microsoftonline.com/{{tenant}}/oauth2/v2.0/auth',
TOKEN_URI = 'https://login.microsoftonline.com/{{tenant}}/oauth2/v2.0/token'; TOKEN_URI = 'https://login.microsoftonline.com/{{tenant}}/oauth2/v2.0/token';
/** private static ?array $auth = null;
* In-request cache of decrypted token bundles, keyed by lowercase email.
* This avoids re-decrypting the same blob multiple times during a single request.
*
* Shape:
* [
* 'user@outlook.com' => ['access_token'=>..., 'refresh_token'=>..., 'expires'=>..., 'expires_in'=>...],
* ...
* ]
*/
private static array $auth = [];
public function Init() : void public function Init() : void
{ {
@ -56,18 +41,14 @@ class LoginO365Plugin extends \RainLoop\Plugins\AbstractPlugin
$this->addHook('sieve.before-login', 'clientLogin'); $this->addHook('sieve.before-login', 'clientLogin');
$this->addPartHook('LoginO365', 'ServiceLoginO365'); $this->addPartHook('LoginO365', 'ServiceLoginO365');
// Used by JS to obtain an auth URL with signed state (for both login + add-account flows).
$this->addJsonHook('LoginO365AuthUrl', 'DoLoginO365AuthUrl');
// Prevent Disallowed Sec-Fetch Dest: document Mode: navigate Site: cross-site User: true // Prevent Disallowed Sec-Fetch Dest: document Mode: navigate Site: cross-site User: true
$this->addHook('filter.http-paths', 'httpPaths'); $this->addHook('filter.http-paths', 'httpPaths');
// Cleanup: when an additional account is removed, also remove its encrypted refresh token bundle.
$this->addHook('json.after-AccountDelete', 'afterAccountDelete');
} }
public function httpPaths(array &$aPaths) : void public function httpPaths(array &$aPaths) : void
{ {
// Personal accounts workaround
if (!empty($_SERVER['PATH_INFO']) && \str_ends_with($_SERVER['PATH_INFO'], 'LoginO365')) { if (!empty($_SERVER['PATH_INFO']) && \str_ends_with($_SERVER['PATH_INFO'], 'LoginO365')) {
$aPaths = ['LoginO365']; $aPaths = ['LoginO365'];
} }
@ -89,167 +70,85 @@ class LoginO365Plugin extends \RainLoop\Plugins\AbstractPlugin
try try
{ {
if (isset($_GET['error'])) { if (isset($_GET['error'])) {
$desc = $_GET['error_description'] ?? ''; throw new \RuntimeException("{$_GET['error']}: {$_GET['error_description']}");
throw new \RuntimeException("{$_GET['error']}: {$desc}");
} }
if (!isset($_GET['code']) || empty($_GET['state']) || 'o365' !== $_GET['state']) {
// Must have code + state
if (!isset($_GET['code']) || empty($_GET['state'])) {
$oActions->Location(\RainLoop\Utils::WebPath()); $oActions->Location(\RainLoop\Utils::WebPath());
exit; exit;
} }
$oO365 = $this->o365Connector(); $oO365 = $this->o365Connector();
if (!$oO365) { if (!$oO365) {
$oActions->Location(\RainLoop\Utils::WebPath()); $oActions->Location(\RainLoop\Utils::WebPath());
exit; exit;
} }
$iNow = \time(); $iExpires = \time();
$aResponse = $oO365->getAccessToken(
$redirectUri = $this->redirectUri(); \str_replace('{{tenant}}', $this->Config()->Get('plugin', 'tenant', 'common'), static::TOKEN_URI),
$tenant = $this->Config()->Get('plugin', 'tenant', 'common');
$state = (string) $_GET['state'];
$statePayload = $this->verifyAndConsumeState($state);
if (!$statePayload) {
$oActions->Location(\RainLoop\Utils::WebPath());
exit;
}
$aTokenWrap = $oO365->getAccessToken(
\str_replace('{{tenant}}', $tenant, static::TOKEN_URI),
'authorization_code', 'authorization_code',
[ array(
'code' => $_GET['code'], 'code' => $_GET['code'],
'redirect_uri' => $redirectUri 'redirect_uri' => $oHttp->GetFullUrl().'?LoginO365'
] )
);
if (200 != $aResponse['code']) {
if (isset($aResponse['result']['error'])) {
throw new \RuntimeException(
$aResponse['code']
. ': '
. $aResponse['result']['error']
. ' / '
. $aResponse['result']['error_description']
); );
if (!\is_array($aTokenWrap) || !isset($aTokenWrap['code'])) {
throw new \RuntimeException('Token request failed: ' . \json_encode($aTokenWrap));
} }
if (200 !== (int)$aTokenWrap['code']) { throw new \RuntimeException("HTTP: {$aResponse['code']}");
$err = $aTokenWrap['result']['error'] ?? '';
$desc = $aTokenWrap['result']['error_description'] ?? '';
throw new \RuntimeException("Token HTTP {$aTokenWrap['code']}: {$err} / {$desc}");
} }
$aResponse = $aResponse['result'];
$aToken = $aTokenWrap['result'] ?? []; if (empty($aResponse['access_token'])) {
$accessToken = $aToken['access_token'] ?? '';
$refreshToken = $aToken['refresh_token'] ?? '';
$expiresIn = (int)($aToken['expires_in'] ?? 0);
$idToken = $aToken['id_token'] ?? '';
if ($accessToken === '') {
throw new \RuntimeException('access_token missing'); throw new \RuntimeException('access_token missing');
} }
if (empty($aResponse['refresh_token'])) {
if ($refreshToken === '') {
throw new \RuntimeException('refresh_token missing'); throw new \RuntimeException('refresh_token missing');
} }
if ($idToken === '') {
// We rely on id_token to get email/sub without Graph. $sAccessToken = $aResponse['access_token'];
throw new \RuntimeException('id_token missing (add openid email profile scopes)'); $iExpires += $aResponse['expires_in'];
$oO365->setAccessToken($sAccessToken);
$aUserInfo = $oO365->fetch('https://graph.microsoft.com/oidc/userinfo');
if (200 != $aUserInfo['code']) {
throw new \RuntimeException("HTTP: {$aResponse['code']}");
}
$aUserInfo = $aUserInfo['result'];
if (empty($aUserInfo['id'])) {
throw new \RuntimeException('unknown id');
}
if (empty($aUserInfo['email'])) {
throw new \RuntimeException('unknown email address');
} }
// Parse id_token (JWT) to get identity (sub + email) static::$auth = [
$claims = $this->decodeJwtPayload($idToken); 'access_token' => $sAccessToken,
if (!\is_array($claims)) { 'refresh_token' => $aResponse['refresh_token'],
throw new \RuntimeException('Cannot decode id_token payload'); 'expires_in' => $aResponse['expires_in'],
} 'expires' => $iExpires
$email = $claims['email'] ?? ($claims['preferred_username'] ?? ($claims['upn'] ?? ''));
$sub = $claims['sub'] ?? '';
if ($sub === '') {
throw new \RuntimeException('unknown id from id_token');
}
if ($email === '') {
throw new \RuntimeException('unknown email address from id_token');
}
if (!$this->isSupportedEmail(\strtolower($email))) {
throw new \RuntimeException('Unsupported email domain for this plugin');
}
$tokenBundle = [
'access_token' => $accessToken,
'refresh_token' => $refreshToken,
'expires_in' => $expiresIn,
'expires' => $iNow + $expiresIn
]; ];
$op = $statePayload['op'] ?? 'login'; $oPassword = new \SnappyMail\SensitiveString($aUserInfo['id']);
if ('add' === $op) { $oAccount = $oActions->LoginProcess($aUserInfo['email'], $oPassword);
$oMainAccount = $oActions->getMainAccountFromToken(false); // $oAccount = MainAccount::NewInstanceFromCredentials($oActions, $aUserInfo['email'], $aUserInfo['email'], $oPassword, true);
if (!$oMainAccount) {
throw new \RuntimeException('Add-account flow requires logged in main account');
}
if (!empty($statePayload['main']) && $statePayload['main'] !== $oMainAccount->Email()) {
throw new \RuntimeException('Add-account state does not match current main account');
}
// Store token bundle encrypted with MAIN account crypt key (never store refresh_token unencrypted).
// This is later used by imap/smtp/sieve.before-login for *additional* accounts.
$this->storeAccountTokens($oMainAccount, $email, $tokenBundle);
// Create/validate an AdditionalAccount entry exactly like SnappyMail expects in "additionalaccounts".
// We set the "password" to the OAuth subject (sub) as an opaque secret; the plugin will inject XOAUTH2.
$oPassword = new \SnappyMail\SensitiveString($sub);
$oAdditional = $oActions->LoginProcess($email, $oPassword, false);
if (!$oAdditional instanceof \RainLoop\Model\AdditionalAccount) {
throw new \RuntimeException('Failed to create additional account');
}
$asciiEmail = \SnappyMail\IDN::emailToAscii($oAdditional->Email());
if ($asciiEmail === $oMainAccount->Email()) {
throw new \RuntimeException('Cannot add main account as additional');
}
$aAccounts = $oActions->GetAccounts($oMainAccount);
$aEntry = $oAdditional->asTokenArray($oMainAccount);
if (!empty($statePayload['name']) && \is_string($statePayload['name'])) {
$aEntry['name'] = \trim($statePayload['name']);
} else if (isset($aAccounts[$asciiEmail]['name'])) {
// Preserve previous custom label if re-adding/updating.
$aEntry['name'] = (string) $aAccounts[$asciiEmail]['name'];
}
$aAccounts[$asciiEmail] = $aEntry;
$oActions->SetAccounts($oMainAccount, $aAccounts);
// Cache for this request (used during LoginProcess() above and any subsequent logins).
static::$auth[\strtolower($asciiEmail)] = $tokenBundle;
$returnHash = '';
if (!empty($statePayload['return']) && \is_string($statePayload['return']) && \str_starts_with($statePayload['return'], '#')) {
$returnHash = $statePayload['return'];
}
$oActions->Location(\RainLoop\Utils::WebPath() . $returnHash);
exit;
}
// Default: "login" flow (preserve existing behavior)
static::$auth[\strtolower($email)] = $tokenBundle;
// SnappyMail uses password as opaque string; plugin injects XOAUTH2 later.
$oPassword = new \SnappyMail\SensitiveString($sub);
$oAccount = $oActions->LoginProcess($email, $oPassword);
if ($oAccount) { if ($oAccount) {
$oActions->StorageProvider()->Put( // $oActions->SetMainAuthAccount($oAccount);
$oAccount, // $oActions->SetAuthToken($oAccount);
StorageType::SESSION, $oActions->StorageProvider()->Put($oAccount, StorageType::SESSION, \RainLoop\Utils::GetSessionToken(),
\RainLoop\Utils::GetSessionToken(), \SnappyMail\Crypt::EncryptToJSON(static::$auth, $oAccount->CryptKey())
\SnappyMail\Crypt::EncryptToJSON($tokenBundle, $oAccount->CryptKey())
); );
} }
} }
catch (\Throwable $e) { catch (\Exception $oException)
$oActions->Logger()->WriteException($e, \LOG_ERR); {
$oActions->Logger()->WriteException($oException, \LOG_ERR);
} }
$oActions->Location(\RainLoop\Utils::WebPath()); $oActions->Location(\RainLoop\Utils::WebPath());
exit; exit;
} }
@ -257,147 +156,66 @@ class LoginO365Plugin extends \RainLoop\Plugins\AbstractPlugin
public function configMapping() : array public function configMapping() : array
{ {
return [ return [
\RainLoop\Plugins\Property::NewInstance('personal')
->SetLabel('Use with personal accounts')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDefaultValue(true)
->SetAllowedInJs()
->SetDescription('Sign in users with personal Microsoft accounts such as Outlook.com (Hotmail)'),
\RainLoop\Plugins\Property::NewInstance('client_id') \RainLoop\Plugins\Property::NewInstance('client_id')
->SetLabel('Client ID') ->SetLabel('Client ID')
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING) ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING)
->SetAllowedInJs(), ->SetAllowedInJs()
->SetDescription('https://github.com/the-djmaze/snappymail/wiki/FAQ#o365'),
\RainLoop\Plugins\Property::NewInstance('client_secret') \RainLoop\Plugins\Property::NewInstance('client_secret')
->SetLabel('Client Secret') ->SetLabel('Client Secret')
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING) ->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING)
->SetEncrypted(), ->SetEncrypted(),
\RainLoop\Plugins\Property::NewInstance('tenant') \RainLoop\Plugins\Property::NewInstance('tenant_id')
->SetLabel('Tenant') ->SetLabel('Tenant ID')
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING),
\RainLoop\Plugins\Property::NewInstance('tenant')->SetLabel('Tenant')
->SetType(\RainLoop\Enumerations\PluginPropertyType::SELECTION) ->SetType(\RainLoop\Enumerations\PluginPropertyType::SELECTION)
->SetDefaultValue(['common','consumers','organizations']) ->SetDefaultValue(['common','consumers','organizations'])
->SetAllowedInJs(),
\RainLoop\Plugins\Property::NewInstance('personal')
// When true: redirect URI uses query parameter form "/?LoginO365" (Azure supports it).
// When false: redirect URI uses path form "/LoginO365" (useful behind reverse proxies).
->SetLabel('Use "/?LoginO365" redirect URI')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDefaultValue(false)
->SetAllowedInJs(),
\RainLoop\Plugins\Property::NewInstance('allow_any_domain')
->SetLabel('Allow any domain (not only outlook.com/hotmail.com/live.com)')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDefaultValue(false)
->SetAllowedInJs() ->SetAllowedInJs()
]; ];
} }
public function clientLogin(\RainLoop\Model\Account $oAccount, \MailSo\Net\NetClient $oClient, \MailSo\Net\ConnectSettings $oSettings) : void public function clientLogin(\RainLoop\Model\Account $oAccount, \MailSo\Net\NetClient $oClient, \MailSo\Net\ConnectSettings $oSettings) : void
{ {
$email = \strtolower($oAccount->Email()); if ($oAccount instanceof MainAccount && \str_ends_with($oAccount->Email(), '@hotmail.com')) {
if (!$this->isSupportedEmail($email)) {
return;
}
$oActions = \RainLoop\Api::Actions(); $oActions = \RainLoop\Api::Actions();
$aData = static::$auth[$email] ?? null;
if (!$aData) {
try { try {
if ($oAccount instanceof MainAccount) { $aData = static::$auth ?: \SnappyMail\Crypt::DecryptFromJSON(
$blob = $oActions->StorageProvider()->Get( $oActions->StorageProvider()->Get($oAccount, StorageType::SESSION, \RainLoop\Utils::GetSessionToken()),
$oAccount, $oAccount->CryptKey()
StorageType::SESSION,
\RainLoop\Utils::GetSessionToken()
); );
$aData = \SnappyMail\Crypt::DecryptFromJSON($blob, $oAccount->CryptKey()); } catch (\Throwable $oException) {
} else if ($oAccount instanceof \RainLoop\Model\AdditionalAccount) { // $oActions->Logger()->WriteException($oException, \LOG_ERR);
$oMain = $oActions->getMainAccountFromToken(false);
if (!$oMain) {
return; return;
} }
$blob = $oActions->StorageProvider()->Get( if (!empty($aData['expires']) && !empty($aData['access_token']) && !empty($aData['refresh_token'])) {
$oMain, if (\time() >= $aData['expires']) {
StorageType::CONFIG, $iExpires = \time();
$this->tokenStorageKey($email)
);
$aData = \SnappyMail\Crypt::DecryptFromJSON($blob, $oMain->CryptKey());
}
} catch (\Throwable $e) {
return;
}
}
if (empty($aData['access_token']) || empty($aData['refresh_token']) || empty($aData['expires'])) {
return;
}
// Refresh if expired (or close to expiry)
if (\time() >= ((int)$aData['expires'] - 30)) {
$oO365 = $this->o365Connector(); $oO365 = $this->o365Connector();
if ($oO365) { if ($oO365) {
$tenant = $this->Config()->Get('plugin', 'tenant', 'common'); $aRefreshTokenResponse = $oO365->getAccessToken(
$aRefreshWrap = $oO365->getAccessToken( \str_replace('{{tenant}}', $this->Config()->Get('plugin', 'tenant', 'common'), static::TOKEN_URI),
\str_replace('{{tenant}}', $tenant, static::TOKEN_URI),
'refresh_token', 'refresh_token',
['refresh_token' => $aData['refresh_token']] array('refresh_token' => $aData['refresh_token'])
); );
if (!empty($aRefreshTokenResponse['result']['access_token'])) {
if (\is_array($aRefreshWrap) && isset($aRefreshWrap['code']) && 200 === (int)$aRefreshWrap['code']) { $aData['access_token'] = $aRefreshTokenResponse['result']['access_token'];
$r = $aRefreshWrap['result'] ?? []; $aResponse['expires'] = $iExpires + $aResponse['expires_in'];
if (!empty($r['access_token'])) { $oActions->StorageProvider()->Put($oAccount, StorageType::SESSION, \RainLoop\Utils::GetSessionToken(),
$aData['access_token'] = $r['access_token'];
}
if (!empty($r['refresh_token'])) {
$aData['refresh_token'] = $r['refresh_token'];
}
$expiresIn = (int)($r['expires_in'] ?? 0);
if ($expiresIn > 0) {
$aData['expires'] = \time() + $expiresIn;
$aData['expires_in'] = $expiresIn;
}
// Persist updated bundle (encrypted).
if ($oAccount instanceof MainAccount) {
$oActions->StorageProvider()->Put(
$oAccount,
StorageType::SESSION,
\RainLoop\Utils::GetSessionToken(),
\SnappyMail\Crypt::EncryptToJSON($aData, $oAccount->CryptKey()) \SnappyMail\Crypt::EncryptToJSON($aData, $oAccount->CryptKey())
); );
} else if ($oAccount instanceof \RainLoop\Model\AdditionalAccount) {
$oMain = $oActions->getMainAccountFromToken(false);
if ($oMain) {
$oActions->StorageProvider()->Put(
$oMain,
StorageType::CONFIG,
$this->tokenStorageKey($email),
\SnappyMail\Crypt::EncryptToJSON($aData, $oMain->CryptKey())
);
} }
} }
} }
}
}
static::$auth[$email] = $aData;
// Inject XOAUTH2/OAUTHBEARER
$oSettings->passphrase = $aData['access_token']; $oSettings->passphrase = $aData['access_token'];
\array_unshift($oSettings->SASLMechanisms, 'OAUTHBEARER', 'XOAUTH2'); \array_unshift($oSettings->SASLMechanisms, 'OAUTHBEARER', 'XOAUTH2');
} }
/**
* Server-side cleanup hook: after a successful AccountDelete, remove stored token bundle for that email.
* This prevents leaving encrypted refresh tokens behind when an additional account is removed.
*/
public function afterAccountDelete(array &$aResponse) : void
{
if (empty($aResponse['Result'])) {
return;
}
$oActions = \RainLoop\Api::Actions();
$oMain = $oActions->getMainAccountFromToken(false);
if (!$oMain) {
return;
}
$email = \strtolower(\SnappyMail\IDN::emailToAscii(\trim((string) $oActions->GetActionParam('emailToDelete', ''))));
if ($email && $this->isSupportedEmail($email)) {
$oActions->StorageProvider()->Clear($oMain, StorageType::CONFIG, $this->tokenStorageKey($email));
} }
} }
@ -405,11 +223,10 @@ class LoginO365Plugin extends \RainLoop\Plugins\AbstractPlugin
{ {
$client_id = \trim($this->Config()->Get('plugin', 'client_id', '')); $client_id = \trim($this->Config()->Get('plugin', 'client_id', ''));
$client_secret = \trim($this->Config()->getDecrypted('plugin', 'client_secret', '')); $client_secret = \trim($this->Config()->getDecrypted('plugin', 'client_secret', ''));
if ($client_id && $client_secret) { if ($client_id && $client_secret) {
try { try
{
$oO365 = new \OAuth2\Client($client_id, $client_secret); $oO365 = new \OAuth2\Client($client_id, $client_secret);
$oActions = \RainLoop\Api::Actions(); $oActions = \RainLoop\Api::Actions();
$sProxy = $oActions->Config()->Get('labs', 'curl_proxy', ''); $sProxy = $oActions->Config()->Get('labs', 'curl_proxy', '');
if (\strlen($sProxy)) { if (\strlen($sProxy)) {
@ -419,277 +236,13 @@ class LoginO365Plugin extends \RainLoop\Plugins\AbstractPlugin
$oO365->setCurlOption(CURLOPT_PROXYUSERPWD, $sProxyAuth); $oO365->setCurlOption(CURLOPT_PROXYUSERPWD, $sProxyAuth);
} }
} }
return $oO365; return $oO365;
} catch (\Throwable $e) { }
\RainLoop\Api::Actions()->Logger()->WriteException($e, \LOG_ERR); catch (\Exception $oException)
{
$oActions->Logger()->WriteException($oException, \LOG_ERR);
} }
} }
return null; return null;
} }
private function decodeJwtPayload(string $jwt) : ?array
{
$parts = \explode('.', $jwt);
if (\count($parts) < 2) {
return null;
}
$payload = $parts[1];
$payload .= \str_repeat('=', (4 - (\strlen($payload) % 4)) % 4);
$json = \base64_decode(\strtr($payload, '-_', '+/'));
if ($json === false) {
return null;
}
$data = \json_decode($json, true);
return \is_array($data) ? $data : null;
}
/**
* JSON action called by JS to obtain an MS authorize URL with signed state.
* This avoids exposing any signing secret to JS and keeps redirect_uri consistent with server logic.
*/
public function DoLoginO365AuthUrl() : array
{
$oActions = \RainLoop\Api::Actions();
$op = (string) $this->jsonParam('op', 'login');
if (!\in_array($op, ['login', 'add'], true)) {
return $this->jsonResponse(__FUNCTION__, false);
}
$email = \strtolower(\trim((string) $this->jsonParam('email', '')));
$name = \trim((string) $this->jsonParam('name', ''));
$returnHash = (string) $this->jsonParam('return', '');
if ($returnHash && !\str_starts_with($returnHash, '#')) {
$returnHash = '';
}
// For add-account flow, require a logged-in main account (we must write to its additionalaccounts storage).
$oMainAccount = null;
if ('add' === $op) {
$oMainAccount = $oActions->getMainAccountFromToken(false);
if (!$oMainAccount) {
return $this->jsonResponse(__FUNCTION__, false);
}
}
// Optional server-side guard: only permit supported consumer domains unless configured otherwise.
if ($email && !$this->isSupportedEmail($email)) {
return $this->jsonResponse(__FUNCTION__, false);
}
$oConfig = $this->Config();
$client_id = \trim($oConfig->Get('plugin', 'client_id', ''));
if (!$client_id) {
return $this->jsonResponse(__FUNCTION__, false);
}
$nonce = $this->b64url(\random_bytes(16));
// Store nonce server-side to prevent replay; consumed on callback.
$oActions->StorageProvider()->Put(
null,
StorageType::NOBODY,
$this->stateNonceKey($nonce),
(string) \time()
);
$payload = [
'v' => 1,
'op' => $op,
'csrf' => \RainLoop\Utils::GetCsrfToken(),
'nonce' => $nonce,
'ts' => \time()
];
if ('add' === $op && $oMainAccount) {
$payload['main'] = $oMainAccount->Email();
if ($name) {
$payload['name'] = \substr($name, 0, 100);
}
if ($returnHash) {
$payload['return'] = \substr($returnHash, 0, 200);
}
}
$state = $this->signState($payload);
$tenant = $oConfig->Get('plugin', 'tenant', 'common');
$redirectUri = $this->redirectUri();
$params = [
'response_type' => 'code',
'client_id' => $client_id,
'redirect_uri' => $redirectUri,
'scope' => \implode(' ', [
'openid',
'offline_access',
'email',
'profile',
'https://outlook.office.com/IMAP.AccessAsUser.All',
'https://outlook.office.com/SMTP.Send',
]),
'state' => $state,
// Helps MS UI prefill, but does not change server-side validation.
];
if ($email) {
$params['login_hint'] = $email;
}
$authUrl = \str_replace('{{tenant}}', $tenant, static::AUTH_URI)
. '?'
. \http_build_query($params, '', '&', PHP_QUERY_RFC3986);
return $this->jsonResponse(__FUNCTION__, [
'authUrl' => $authUrl
]);
}
private function isSupportedEmail(string $email) : bool
{
if ((bool)$this->Config()->Get('plugin', 'allow_any_domain', false)) {
return \str_contains($email, '@');
}
return \str_ends_with($email, '@hotmail.com')
|| \str_ends_with($email, '@outlook.com')
|| \str_ends_with($email, '@live.com');
}
/**
* Build absolute base URL (works behind nginx reverse proxy).
*/
private function baseUrl() : string
{
$scheme = (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']))
? $_SERVER['HTTP_X_FORWARDED_PROTO']
: ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http');
$host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? '';
if (!$host) {
throw new \RuntimeException('Cannot determine HTTP_HOST');
}
return $scheme . '://' . $host;
}
/**
* Redirect URI used for the Azure app registration.
* When plugin.personal=true -> "/?LoginO365"
* When plugin.personal=false -> "/LoginO365"
*/
private function redirectUri() : string
{
$base = \rtrim($this->baseUrl(), '/');
$useQuery = (bool)$this->Config()->Get('plugin', 'personal', false);
return $useQuery ? ($base . '/?LoginO365') : ($base . '/LoginO365');
}
private function tokenStorageKey(string $emailLower) : string
{
// Stored under MAIN account StorageType::CONFIG (encrypted with main CryptKey).
// Email is hashed to avoid path/encoding issues across storage backends.
return 'login-o365.tokens.' . \sha1($emailLower);
}
private function storeAccountTokens(MainAccount $oMainAccount, string $email, array $tokenBundle) : void
{
$emailLower = \strtolower(\SnappyMail\IDN::emailToAscii($email));
\RainLoop\Api::Actions()->StorageProvider()->Put(
$oMainAccount,
StorageType::CONFIG,
$this->tokenStorageKey($emailLower),
\SnappyMail\Crypt::EncryptToJSON($tokenBundle, $oMainAccount->CryptKey())
);
}
private function stateNonceKey(string $nonce) : string
{
return 'login-o365.state.' . $nonce;
}
private function b64url(string $bin) : string
{
return \rtrim(\strtr(\base64_encode($bin), '+/', '-_'), '=');
}
private function b64urlDecode(string $b64url) /*: string|false*/
{
$pad = (4 - (\strlen($b64url) % 4)) % 4;
return \base64_decode(\strtr($b64url . \str_repeat('=', $pad), '-_', '+/'), true);
}
private function stateHmacKey() : string
{
// Uses the plugin client_secret (server-side only) as HMAC key.
// This prevents any user-controlled tampering of the state payload.
$key = \trim($this->Config()->getDecrypted('plugin', 'client_secret', ''));
if (!$key) {
// Fallback for misconfiguration; keeps behavior deterministic.
$key = 'login-o365';
}
return $key;
}
private function signState(array $payload) : string
{
$json = \json_encode($payload);
if (!$json) {
$json = '{}';
}
$payloadB64 = $this->b64url($json);
$sig = \hash_hmac('sha256', $payloadB64, $this->stateHmacKey(), true);
return $payloadB64 . '.' . $this->b64url($sig);
}
/**
* Verify signature + CSRF + nonce, then consumes nonce to prevent replay.
* Returns decoded payload on success, null on failure.
*/
private function verifyAndConsumeState(string $state) : ?array
{
$parts = \explode('.', $state, 2);
if (2 !== \count($parts)) {
return null;
}
[$payloadB64, $sigB64] = $parts;
$sig = $this->b64urlDecode($sigB64);
if ($sig === false) {
return null;
}
$expected = \hash_hmac('sha256', $payloadB64, $this->stateHmacKey(), true);
if (!\hash_equals($expected, $sig)) {
return null;
}
$payloadJson = $this->b64urlDecode($payloadB64);
if ($payloadJson === false) {
return null;
}
$payload = \json_decode($payloadJson, true);
if (!\is_array($payload) || empty($payload['csrf']) || empty($payload['nonce']) || empty($payload['op'])) {
return null;
}
// Must match the current browser session.
if ($payload['csrf'] !== \RainLoop\Utils::GetCsrfToken()) {
return null;
}
// Replay protection: nonce must exist server-side and is consumed once.
$oActions = \RainLoop\Api::Actions();
$key = $this->stateNonceKey((string) $payload['nonce']);
$seen = $oActions->StorageProvider()->Get(null, StorageType::NOBODY, $key);
if (!$seen) {
return null;
}
$ts = (int) ($payload['ts'] ?? 0);
if ($ts && \abs(\time() - $ts) > 900) { // 15 minutes
// Expired: clear nonce to avoid accumulating stale entries.
$oActions->StorageProvider()->Clear(null, StorageType::NOBODY, $key);
return null;
}
$oActions->StorageProvider()->Clear(null, StorageType::NOBODY, $key);
return $payload;
}
} }

View file

@ -6,8 +6,8 @@ class LoginRemotePlugin extends \RainLoop\Plugins\AbstractPlugin
NAME = 'Login Remote', NAME = 'Login Remote',
AUTHOR = 'SnappyMail', AUTHOR = 'SnappyMail',
URL = 'https://snappymail.eu/', URL = 'https://snappymail.eu/',
VERSION = '1.6', VERSION = '1.5',
RELEASE = '2026-10-22', RELEASE = '2024-09-20',
REQUIRED = '2.36.1', REQUIRED = '2.36.1',
CATEGORY = 'Login', CATEGORY = 'Login',
LICENSE = 'MIT', LICENSE = 'MIT',
@ -43,7 +43,6 @@ class LoginRemotePlugin extends \RainLoop\Plugins\AbstractPlugin
try try
{ {
static::$login = true; static::$login = true;
$sPassword = new \SnappyMail\SensitiveString($sPassword);
$oAccount = $oActions->LoginProcess($sEmail, $sPassword); $oAccount = $oActions->LoginProcess($sEmail, $sPassword);
} }
catch (\Throwable $oException) catch (\Throwable $oException)

View file

@ -19,34 +19,33 @@ The following steps are require in SnappyMail:
- Master User is dependent on Dovecot config (see below) - Master User is dependent on Dovecot config (see below)
- Master User Password is dependent on Dovecot config (see below) - Master User Password is dependent on Dovecot config (see below)
- Header Name is dependent on authentication solution. This is the header containing the name of currently logged in user. In case of Authelia, this is "Remote-User". - Header Name is dependent on authentication solution. This is the header containing the name of currently logged in user. In case of Authelia, this is "Remote-User".
- Check Proxy: Since this plugin partially bypasses authentication, it is important to only allow this access from well-defined hosts. It is highly recommended to activate this option!
- When checking for reverse proxy, it is required to set the IP filter to either an IP address or a subnet.
- Automatic Login: Automatically logs in the user of user header is present (see below) - Automatic Login: Automatically logs in the user of user header is present (see below)
> **Security note**
>
> This plugin trusts the configured request header as proof of identity. Anyone who can reach the SnappyMail container directly and set that header can log in as any user. You **must** ensure that SnappyMail is only reachable through your reverse proxy / SSO chain (e.g. via the docker network, a firewall, or by binding SnappyMail to a non-public interface) and that the upstream proxy strips any client-supplied value of the header before forwarding. The plugin itself does no source-IP validation — earlier versions had a `check_proxy` option, but it inspected the forwarded client IP (the end user's IP) and therefore did not actually verify that the request came from the proxy. It has been removed; gate access at the network layer instead.
This concludes the setup of SnappyMail. This concludes the setup of SnappyMail.
### Dovecot ### Dovecot
In Dovecot, you need to enable Master User. In Dovecot, you need to enable Master User.
Enable ```!include auth-master.conf.ext``` in /etc/dovecot/conf.d/10-auth.conf. Enable ```!include auth-master.conf.ext``` in /etc/dovecot/conf.d/10-auth.conf.
In Dovecot 2.3, the file /etc/dovecot/conf.d/auth-master.conf.ext should contain: The file /etc/dovecot/conf.d/auth-master.conf.ext should contain:
``` ```
# Authentication for master users. Included from auth.conf.
# By adding master=yes setting inside a passdb you make the passdb a list
# of "master users", who can log in as anyone else.
# <doc/wiki/Authentication.MasterUsers.txt>
# Example master user passdb using passwd-file. You can use any passdb though.
passdb { passdb {
driver = passwd-file driver = passwd-file
master = yes master = yes
args = /etc/dovecot/master-users args = /etc/dovecot/master-users
pass = yes
}
```
In Dovecot 2.4, the file /etc/dovecot/conf.d/auth-master.conf.ext should contain: # Unless you're using PAM, you probably still want the destination user to
``` # be looked up from passdb that it really exists. pass=yes does that.
passdb passwd-file { pass = yes
master = yes
passwd_file_path = /etc/dovecot/master-users
result_success = continue
} }
``` ```
@ -81,11 +80,3 @@ The user is always considered logged in, as authentication is handled through re
Auto login can be disabled in the plugin settings. Auto login can be disabled in the plugin settings.
You can also change the logout link in admin panel -> Config -> custom_logout_link to the one of your authentication system, e.g., ```https://auth.yourdomain.com/logout```. You can also change the logout link in admin panel -> Config -> custom_logout_link to the one of your authentication system, e.g., ```https://auth.yourdomain.com/logout```.
In this case, you can log out from your overall system via SnappyMail. In this case, you can log out from your overall system via SnappyMail.
## Troubleshooting
### IMAP `AUTHENTICATIONFAILED` after a container rebuild / upgrade
The master user/password fields are encrypted at rest using SnappyMail's `APP_SALT`. If that salt is regenerated (e.g., the data volume was reset, the container was rebuilt without persisting `_data_`, or the salt file was rotated), the values in `plugin-proxy-auth.json` can no longer be decrypted. `getDecrypted()` then silently returns `null`, an empty password is passed to IMAP, and Dovecot rejects the login with `AUTHENTICATIONFAILED`.
Fix: open admin panel -> Extensions -> Proxy Auth, re-enter the Master User and Master Password (and any other previously-set encrypted fields), and save. The values will be re-encrypted under the current salt.

View file

@ -6,8 +6,8 @@ class ProxyAuthPlugin extends \RainLoop\Plugins\AbstractPlugin
NAME = 'Proxy Auth', NAME = 'Proxy Auth',
AUTHOR = 'Philipp', AUTHOR = 'Philipp',
URL = 'https://www.mundhenk.org/', URL = 'https://www.mundhenk.org/',
VERSION = '0.7', VERSION = '0.5',
RELEASE = '2026-05-17', RELEASE = '2024-09-20',
REQUIRED = '2.36.1', REQUIRED = '2.36.1',
CATEGORY = 'Login', CATEGORY = 'Login',
LICENSE = 'MIT', LICENSE = 'MIT',
@ -21,6 +21,26 @@ class ProxyAuthPlugin extends \RainLoop\Plugins\AbstractPlugin
$this->addHook('login.credentials', 'MapEmailAddress'); $this->addHook('login.credentials', 'MapEmailAddress');
} }
/* by https://gist.github.com/tott/7684443 */
/**
* Check if a given ip is in a network
* @param string $ip IP to check in IPV4 format eg. 127.0.0.1
* @param string $range IP/CIDR netmask eg. 127.0.0.0/24, also 127.0.0.1 is accepted and /32 assumed
* @return boolean true if the ip is in this range / false if not.
*/
private function ip_in_range( $ip, $range ) {
if ( strpos( $range, '/' ) == false ) {
$range .= '/32';
}
// $range is in IP/CIDR format eg 127.0.0.1/24
list( $range, $netmask ) = explode( '/', $range, 2 );
$range_decimal = ip2long( $range );
$ip_decimal = ip2long( $ip );
$wildcard_decimal = pow( 2, ( 32 - $netmask ) ) - 1;
$netmask_decimal = ~ $wildcard_decimal;
return ( ( $ip_decimal & $netmask_decimal ) == ( $range_decimal & $netmask_decimal ) );
}
public function MapEmailAddress(string &$sEmail, string &$sImapUser, string &$sPassword, string &$sSmtpUser) public function MapEmailAddress(string &$sEmail, string &$sImapUser, string &$sPassword, string &$sSmtpUser)
{ {
$oActions = \RainLoop\Api::Actions(); $oActions = \RainLoop\Api::Actions();
@ -60,6 +80,42 @@ class ProxyAuthPlugin extends \RainLoop\Plugins\AbstractPlugin
$sMsg = "Remote User: " . $sRemoteUser; $sMsg = "Remote User: " . $sRemoteUser;
$oLogger->Write($sMsg, $sLevel, $sPrefix); $oLogger->Write($sMsg, $sLevel, $sPrefix);
$sProxyIP = $this->Config()->getDecrypted('plugin', 'proxy_ip', '');
$sMsg = "ProxyIP: " . $sProxyIP;
$oLogger->Write($sMsg, $sLevel, $sPrefix);
$sProxyCheck = $this->Config()->getDecrypted('plugin', 'proxy_check', '');
$sClientIPs = $this->Manager()->Actions()->Http()->GetClientIP(true);
/* make sure that remote user is only set by authorized proxy to avoid security risks */
if ($sProxyCheck) {
$sProxyRequest = false;
$sMsg = "checking client IPs: " . $sClientIPs;
$oLogger->Write($sMsg, $sLevel, $sPrefix);
$sClientIPs = explode(", ", $sClientIPs);
if (is_array($sClientIPs)) {
foreach ($sClientIPs as &$sIP) {
$sMsg = "checking client IP: " . $sIP;
$oLogger->Write($sMsg, $sLevel, $sPrefix);
if ($this->ip_in_range($sIP, $sProxyIP)) {
$sProxyRequest = true;
}
}
} else {
$sMsg = "checking client IP: " . $sClientIPs;
$oLogger->Write($sMsg, $sLevel, $sPrefix);
if ($this->ip_in_range($sClientIPs, $sProxyIP)) {
$sProxyRequest = true;
}
}
} else {
$sProxyRequest = true;
}
if ($sProxyRequest) {
/* create master user login from remote user header and settings */ /* create master user login from remote user header and settings */
$sEmail = $sRemoteUser . $sMasterSeparator . $sMasterUser; $sEmail = $sRemoteUser . $sMasterSeparator . $sMasterUser;
$sPassword = new \SnappyMail\SensitiveString(\trim($this->Config()->getDecrypted('plugin', 'master_password', ''))); $sPassword = new \SnappyMail\SensitiveString(\trim($this->Config()->getDecrypted('plugin', 'master_password', '')));
@ -79,6 +135,10 @@ class ProxyAuthPlugin extends \RainLoop\Plugins\AbstractPlugin
return true; return true;
} }
\MailSo\Base\Http::Location('./');
return true;
}
public function ServiceUserHeaderSet() : bool public function ServiceUserHeaderSet() : bool
{ {
$oActions = \RainLoop\Api::Actions(); $oActions = \RainLoop\Api::Actions();
@ -128,6 +188,18 @@ class ProxyAuthPlugin extends \RainLoop\Plugins\AbstractPlugin
->SetDescription('Name of header containing username') ->SetDescription('Name of header containing username')
->SetDefaultValue('Remote-User') ->SetDefaultValue('Remote-User')
->SetEncrypted(), ->SetEncrypted(),
\RainLoop\Plugins\Property::NewInstance('check_proxy')
->SetLabel('Check Proxy')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDescription('Activates check if proxy is connecting')
->SetDefaultValue(true)
->SetEncrypted(),
\RainLoop\Plugins\Property::NewInstance('proxy_ip')
->SetLabel('Proxy IPNet')
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT)
->SetDescription('IP or Subnet of proxy, auth header will only be accepted from this address')
->SetDefaultValue('10.1.0.0/24')
->SetEncrypted(),
\RainLoop\Plugins\Property::NewInstance('auto_login') \RainLoop\Plugins\Property::NewInstance('auto_login')
->SetAllowedInJs(true) ->SetAllowedInJs(true)
->SetLabel('Activate automatic login') ->SetLabel('Activate automatic login')

View file

@ -6,8 +6,8 @@ class SearchFiltersPlugin extends \RainLoop\Plugins\AbstractPlugin
NAME = 'Search Filters', NAME = 'Search Filters',
AUTHOR = 'AbdoBnHesham', AUTHOR = 'AbdoBnHesham',
URL = 'https://github.com/the-djmaze/snappymail/pull/1673', URL = 'https://github.com/the-djmaze/snappymail/pull/1673',
VERSION = '0.3', VERSION = '0.2',
RELEASE = '2026-10-22', RELEASE = '2024-06-28',
REQUIRED = '2.36.3', REQUIRED = '2.36.3',
CATEGORY = 'General', CATEGORY = 'General',
LICENSE = 'MIT', LICENSE = 'MIT',
@ -51,33 +51,45 @@ class SearchFiltersPlugin extends \RainLoop\Plugins\AbstractPlugin
$Filters = $aSettings['SFilters']; $Filters = $aSettings['SFilters'];
foreach ($Filters as $filter) { foreach ($Filters as $filter) {
$this->Manager()->logWrite(json_encode(['filter' => $filter]), LOG_WARNING); $this->Manager()->logWrite(json_encode([
'filter' => $filter,
]), LOG_WARNING);
$folder = 'INBOX';
$searchQ = $filter['searchQ']; $searchQ = $filter['searchQ'];
$uids = $this->searchMessages($oImapClient, $searchQ, "INBOX");
// obsługa keyword (multi-folder) //Mark as read/seen
if (stripos($searchQ, 'keyword=') !== false) { if ($filter['fSeen']) {
$oMailClient = $this->Manager()->Actions()->MailClient(); foreach ($uids as $uid) {
$oRange = new MailSo\Imap\SequenceSet([$uid]);
// Pobieramy wszystkie foldery top-level $this->Manager()->Actions()->MailClient()->MessageSetFlag(
$folders = $oMailClient->Folders('', false, ''); $folder,
foreach ($folders as $f) { $oRange,
$folder = $f->FullName ?? null; MailSo\Imap\Enumerations\MessageFlag::SEEN
if (!$folder) continue; );
$uids = $this->searchMessages($oImapClient, $searchQ, $folder);
if (!empty($uids)) {
$this->applyActions($oImapClient, $filter, $uids, $folder);
} }
} }
continue; // przechodzimy do następnego filtra //Flag/Star message
if ($filter['fFlag']) {
foreach ($uids as $uid) {
$oRange = new MailSo\Imap\SequenceSet([$uid]);
$this->Manager()->Actions()->MailClient()->MessageSetFlag(
$folder,
$oRange,
MailSo\Imap\Enumerations\MessageFlag::FLAGGED
);
}
} }
// normalny search w INBOX // Move to folder
$uids = $this->searchMessages($oImapClient, $searchQ, 'INBOX'); if ($filter['fFolder']) {
if (!empty($uids)) { $folder = $filter['fFolder'];
$this->applyActions($oImapClient, $filter, $uids, 'INBOX'); foreach ($uids as $uid) {
$oRange = new MailSo\Imap\SequenceSet([$uid]);
$oImapClient->MessageMove("INBOX", $folder, $oRange);
}
} }
} }
} }
@ -87,7 +99,10 @@ class SearchFiltersPlugin extends \RainLoop\Plugins\AbstractPlugin
string $search, string $search,
string $folder = "INBOX" string $folder = "INBOX"
): array { ): array {
try { $oParams = new \MailSo\Mail\MessageListParams();
$oParams->sSearch = $search;
$oParams->sFolderName = $folder;
$bUseCache = false; $bUseCache = false;
$oSearchCriterias = \MailSo\Imap\SearchCriterias::fromString( $oSearchCriterias = \MailSo\Imap\SearchCriterias::fromString(
$imapClient, $imapClient,
@ -99,54 +114,6 @@ class SearchFiltersPlugin extends \RainLoop\Plugins\AbstractPlugin
$imapClient->FolderSelect($folder); $imapClient->FolderSelect($folder);
return $imapClient->MessageSearch($oSearchCriterias, true); return $imapClient->MessageSearch($oSearchCriterias, true);
} catch (\Throwable $e) {
$this->Manager()->logWrite(
'SearchFilters IMAP error in folder "' . $folder .
'" for search "' . $search . '": ' . $e->getMessage(),
LOG_ERR
);
return [];
}
}
private function applyActions(
\MailSo\Imap\ImapClient $imapClient,
array $filter,
array $uids,
string $folder
) {
// Mark as read/seen
if (!empty($filter['fSeen'])) {
foreach ($uids as $uid) {
$oRange = new \MailSo\Imap\SequenceSet([$uid]);
$this->Manager()->Actions()->MailClient()->MessageSetFlag(
$folder,
$oRange,
\MailSo\Imap\Enumerations\MessageFlag::SEEN
);
}
}
// Flag/Star message
if (!empty($filter['fFlag'])) {
foreach ($uids as $uid) {
$oRange = new \MailSo\Imap\SequenceSet([$uid]);
$this->Manager()->Actions()->MailClient()->MessageSetFlag(
$folder,
$oRange,
\MailSo\Imap\Enumerations\MessageFlag::FLAGGED
);
}
}
// Move to folder
if (!empty($filter['fFolder']) && $filter['fFolder'] !== -1) {
foreach ($uids as $uid) {
$oRange = new \MailSo\Imap\SequenceSet([$uid]);
$imapClient->MessageMove('INBOX', $filter['fFolder'], $oRange);
}
}
} }
public function GetFilters() public function GetFilters()
@ -206,6 +173,7 @@ class SearchFiltersPlugin extends \RainLoop\Plugins\AbstractPlugin
} else { } else {
$aSettings['SFilters'][$foundIndex] = $newFilter; $aSettings['SFilters'][$foundIndex] = $newFilter;
} }
return $this->jsonResponse(__FUNCTION__, $this->saveUserSettings($aSettings)); return $this->jsonResponse(__FUNCTION__, $this->saveUserSettings($aSettings));
} }

View file

@ -131,7 +131,7 @@ class TwoFactorAuthPlugin extends \RainLoop\Plugins\AbstractPlugin
private static function getQRCode(MainAccount $oAccount, string $secret) : string private static function getQRCode(MainAccount $oAccount, string $secret) : string
{ {
$email = \rawurlencode($oAccount->Email()); $email = \rawurlencode($oAccount->Email());
// $issuer = \rawurlencode(\RainLoop\Api::Config()->Get('webmail', 'title', 'SnappyMail')); // $issuer = \rawurlencode(\RainLoop\API::Config()->Get('webmail', 'title', 'SnappyMail'));
$QR = \SnappyMail\QRCode::getMinimumQRCode( $QR = \SnappyMail\QRCode::getMinimumQRCode(
// "otpauth://totp/{$issuer}:{$email}?secret={$secret}&issuer={$issuer}", // "otpauth://totp/{$issuer}:{$email}?secret={$secret}&issuer={$issuer}",
"otpauth://totp/{$email}?secret={$secret}", "otpauth://totp/{$email}?secret={$secret}",

View file

@ -12,8 +12,8 @@ BUTTON_SHOW_SECRET = "Show Secret"
BUTTON_HIDE_SECRET = "Hide Secret" BUTTON_HIDE_SECRET = "Hide Secret"
TWO_FACTOR_SECRET_CONFIGURED_DESC = "Configured" TWO_FACTOR_SECRET_CONFIGURED_DESC = "Configured"
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Not configured" TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Not configured"
TWO_FACTOR_SECRET_DESC = "Import this info into your Google Authenticator client (or other TOTP client) using the provided QR code below or by entering the code manually." TWO_FACTOR_SECRET_DESC = "Import this info into your Google Authenticator client (or other TOTP client) using the provided QR code below or by entering the code manually.\n"
TWO_FACTOR_BACKUP_CODES_DESC = "If you can't receive codes via Google Authenticator (or other TOTP client), you can use backup codes to sign in. After you’ve used a backup code to sign in, it will become inactive." TWO_FACTOR_BACKUP_CODES_DESC = "If you can't receive codes via Google Authenticator (or other TOTP client), you can use backup codes to sign in. After you’ve used a backup code to sign in, it will become inactive.\n"
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "You can't change this setting before test." TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "You can't change this setting before test."
TITLE_TEST_CODE = "2-Step verification test" TITLE_TEST_CODE = "2-Step verification test"
LABEL_CODE = "Code" LABEL_CODE = "Code"

View file

@ -12,8 +12,8 @@ BUTTON_SHOW_SECRET = "Show Secret"
BUTTON_HIDE_SECRET = "Hide Secret" BUTTON_HIDE_SECRET = "Hide Secret"
TWO_FACTOR_SECRET_CONFIGURED_DESC = "Configured" TWO_FACTOR_SECRET_CONFIGURED_DESC = "Configured"
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Not configured" TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Not configured"
TWO_FACTOR_SECRET_DESC = "Import this info into your Google Authenticator client (or other TOTP client) using the provided QR code below or by entering the code manually." TWO_FACTOR_SECRET_DESC = "Import this info into your Google Authenticator client (or other TOTP client) using the provided QR code below or by entering the code manually.\n"
TWO_FACTOR_BACKUP_CODES_DESC = "If you can't receive codes via Google Authenticator (or other TOTP client), you can use backup codes to sign in. After you’ve used a backup code to sign in, it will become inactive." TWO_FACTOR_BACKUP_CODES_DESC = "If you can't receive codes via Google Authenticator (or other TOTP client), you can use backup codes to sign in. After you’ve used a backup code to sign in, it will become inactive.\n"
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "You can't change this setting before test." TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "You can't change this setting before test."
TITLE_TEST_CODE = "2-Step verification test" TITLE_TEST_CODE = "2-Step verification test"
LABEL_CODE = "Code" LABEL_CODE = "Code"

View file

@ -12,8 +12,8 @@ BUTTON_SHOW_SECRET = "Titok megjelenítése"
BUTTON_HIDE_SECRET = "Titok elrejtése" BUTTON_HIDE_SECRET = "Titok elrejtése"
TWO_FACTOR_SECRET_CONFIGURED_DESC = "Beállítva" TWO_FACTOR_SECRET_CONFIGURED_DESC = "Beállítva"
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Nincs beállítva" TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Nincs beállítva"
TWO_FACTOR_SECRET_DESC = "Importáld ezt az infót a Google Authenticator kliensedbe (vagy más TOTP kliensbe) az alábbi QR kód használatával vagy a kód manuális megadatásával." TWO_FACTOR_SECRET_DESC = "Importáld ezt az infót a Google Authenticator kliensedbe (vagy más TOTP kliensbe) az alábbi QR kód használatával vagy a kód manuális megadatásával.\n"
TWO_FACTOR_BACKUP_CODES_DESC = "Ha nem kapod meg a kódokat a Google Authenticator kliensből (vagy más TOTP kliensből), akkor a bejelentkezéshez használhatod a biztonsági kódot. A biztonsági kód használata után inaktívvá válik." TWO_FACTOR_BACKUP_CODES_DESC = "Ha nem kapod meg a kódokat a Google Authenticator kliensből (vagy más TOTP kliensből), akkor a bejelentkezéshez használhatod a biztonsági kódot. A biztonsági kód használata után inaktívvá válik.\n"
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Tesztelés nélkül nem lehet megváltoztatni ezt a beállítást." TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Tesztelés nélkül nem lehet megváltoztatni ezt a beállítást."
TITLE_TEST_CODE = "2-lépéses hitelesítés teszt" TITLE_TEST_CODE = "2-lépéses hitelesítés teszt"
LABEL_CODE = "Kód" LABEL_CODE = "Kód"

View file

@ -12,8 +12,8 @@ BUTTON_SHOW_SECRET = "Mostra la chiave segreta"
BUTTON_HIDE_SECRET = "Nascondi la chiave segreta" BUTTON_HIDE_SECRET = "Nascondi la chiave segreta"
TWO_FACTOR_SECRET_CONFIGURED_DESC = "Configurato" TWO_FACTOR_SECRET_CONFIGURED_DESC = "Configurato"
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Non configurato" TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Non configurato"
TWO_FACTOR_SECRET_DESC = "Importa queste informazioni nella tua app Google Authenticator (o un altro programma per l'autenticazione a due fattori) utilizzando il codice QR fornito qui sotto o inserendo il codice manualmente." TWO_FACTOR_SECRET_DESC = "Importa queste informazioni nella tua app Google Authenticator (o un altro programma per l'autenticazione a due fattori) utilizzando il codice QR fornito qui sotto o inserendo il codice manualmente.\n"
TWO_FACTOR_BACKUP_CODES_DESC = "Se non riesci a ricevere i codici tramite Google Authenticator (o un altro programma per l'autenticazione a due fattori), puoi utilizzare i codici di backup per accedere. Dopo che hai utilizzato un codice di backup per accedere, diventerà inattivo." TWO_FACTOR_BACKUP_CODES_DESC = "Se non riesci a ricevere i codici tramite Google Authenticator (o un altro programma per l'autenticazione a due fattori), puoi utilizzare i codici di backup per accedere. Dopo che hai utilizzato un codice di backup per accedere, diventerà inattivo.\n"
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Non puoi modificare questa impostazione prima del test." TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Non puoi modificare questa impostazione prima del test."
TITLE_TEST_CODE = "Test di verifica a 2 fattori" TITLE_TEST_CODE = "Test di verifica a 2 fattori"
LABEL_CODE = "Codice" LABEL_CODE = "Codice"

View file

@ -1,20 +0,0 @@
[PLUGIN_2FA]
LEGEND_TWO_FACTOR_AUTH = "2-етапна перевірка (TOTP)"
LABEL_ENABLE_TWO_FACTOR = "Увімкнути 2-етапну перевірку"
LABEL_TWO_FACTOR_USER = "Ім'я користувача"
LABEL_TWO_FACTOR_STATUS = "Статус"
LABEL_TWO_FACTOR_SECRET = "Ключ"
LABEL_TWO_FACTOR_BACKUP_CODES = "Резервні коди"
BUTTON_CREATE = "Create a secret"
BUTTON_ACTIVATE = "Activate"
LINK_TEST = "Тестувати"
BUTTON_SHOW_SECRET = "Показати ключ"
BUTTON_HIDE_SECRET = "Приховати ключ"
TWO_FACTOR_SECRET_CONFIGURED_DESC = "Налаштовано"
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Не налаштовано"
TWO_FACTOR_SECRET_DESC = "Імпортуйте цю інформацію у свій клієнт Google Authenticator (або інший клієнт TOTP), використовуючи наданий нижче QR-код або ввівши код вручну"
TWO_FACTOR_BACKUP_CODES_DESC = "Якщо ви не можете отримати код через Google Authenticator (або інший клієнт TOTP), ви можете скористатися резервними кодами для входу. Після того, як ви використаєте резервний код для входу, він стане неактивним. Закреслюйте його"
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Ви не можете змінити це налаштування перед тестуванням."
TITLE_TEST_CODE = "Тест 2-етапної перевірки"
LABEL_CODE = "Код"
LABEL_TWO_FACTOR_CODE = "Код підтвердження"

View file

@ -12,8 +12,8 @@ BUTTON_SHOW_SECRET = "显示密钥"
BUTTON_HIDE_SECRET = "隐藏密钥" BUTTON_HIDE_SECRET = "隐藏密钥"
TWO_FACTOR_SECRET_CONFIGURED_DESC = "已设置" TWO_FACTOR_SECRET_CONFIGURED_DESC = "已设置"
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "未设置" TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "未设置"
TWO_FACTOR_SECRET_DESC = "将下面的信息导入 Google Authenticator (或其他 TOTP 客户端)。 扫描下面的二维码或手动输入密钥。" TWO_FACTOR_SECRET_DESC = "将下面的信息导入 Google Authenticator (或其他 TOTP 客户端)。 扫描下面的二维码或手动输入密钥。\n"
TWO_FACTOR_BACKUP_CODES_DESC = "如果你无法从验证器获取验证码,你可以使用备用代码登录。 当你使用过某一个备用代码后,它将会失效。" TWO_FACTOR_BACKUP_CODES_DESC = "如果你无法从验证器获取验证码,你可以使用备用代码登录。 当你使用过某一个备用代码后,它将会失效。\n"
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "完成测试前你无法更改此设置。" TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "完成测试前你无法更改此设置。"
TITLE_TEST_CODE = "两步验证测试" TITLE_TEST_CODE = "两步验证测试"
LABEL_CODE = "代码" LABEL_CODE = "代码"

View file

@ -25,7 +25,10 @@ abstract class Collection extends \ArrayObject implements \JsonSerializable
} }
} }
public function append(mixed $mItem, bool $bToTop = false): void /**
* @param mixed $mItem
*/
public function append($mItem, bool $bToTop = false) : void
{ {
if ($bToTop) { if ($bToTop) {
$array = $this->getArrayCopy(); $array = $this->getArrayCopy();
@ -36,18 +39,18 @@ abstract class Collection extends \ArrayObject implements \JsonSerializable
} }
} }
public function keys(): array public function keys() : array
{ {
return \array_keys($this->getArrayCopy()); return \array_keys($this->getArrayCopy());
} }
public function Add($mItem, bool $bToTop = false): self public function Add($mItem, bool $bToTop = false) : self
{ {
$this->append($mItem, $bToTop); $this->append($mItem, $bToTop);
return $this; return $this;
} }
public function Clear(): void public function Clear() : void
{ {
$this->exchangeArray([]); $this->exchangeArray([]);
} }
@ -61,14 +64,14 @@ abstract class Collection extends \ArrayObject implements \JsonSerializable
return $callable($this->getArrayCopy(), ...$arguments); return $callable($this->getArrayCopy(), ...$arguments);
} }
*/ */
public function Slice(int $offset, ?int $length = null, bool $preserve_keys = false)/*: static*/ public function Slice(int $offset, ?int $length = null, bool $preserve_keys = false)
{ {
return new static( return new static(
\array_slice($this->getArrayCopy(), $offset, $length, $preserve_keys) \array_slice($this->getArrayCopy(), $offset, $length, $preserve_keys)
); );
} }
public function Crop(?int $length = null, int $offset = 0, bool $preserve_keys = false): self public function Crop(?int $length = null, int $offset = 0, bool $preserve_keys = false)
{ {
$this->exchangeArray( $this->exchangeArray(
\array_slice($this->getArrayCopy(), $offset, $length, $preserve_keys) \array_slice($this->getArrayCopy(), $offset, $length, $preserve_keys)
@ -76,8 +79,8 @@ abstract class Collection extends \ArrayObject implements \JsonSerializable
return $this; return $this;
} }
// public function jsonSerialize(): array #[\ReturnTypeWillChange]
public function jsonSerialize(): mixed public function jsonSerialize()
{ {
$aNames = \explode('\\', \get_class($this)); $aNames = \explode('\\', \get_class($this));
return array( return array(

View file

@ -54,22 +54,11 @@ abstract class DateTimeHelper
\DateTime::createFromFormat('d M Y H:i:s O', $sDateTime, static::GetUtcTimeZoneObject()) \DateTime::createFromFormat('d M Y H:i:s O', $sDateTime, static::GetUtcTimeZoneObject())
// Using T (obsolete timezone abbreviation) // Using T (obsolete timezone abbreviation)
?: \DateTime::createFromFormat('d M Y H:i:s T', $sDateTime, static::GetUtcTimeZoneObject()); ?: \DateTime::createFromFormat('d M Y H:i:s T', $sDateTime, static::GetUtcTimeZoneObject());
// 398045302 is 1982-08-13 00:08:22 the date RFC 822 was created
try { if (!$oDateTime || 398045302 > $oDateTime->getTimestamp()) {
$timestamp = $oDateTime->getTimestamp();
// 398045302 is 1982-08-13 00:08:22, the date RFC 822 was created
if (398045302 > $timestamp) {
\SnappyMail\Log::notice('', "Failed to parse RFC 2822 date '{$sDateTime}'"); \SnappyMail\Log::notice('', "Failed to parse RFC 2822 date '{$sDateTime}'");
return 0;
}
return $timestamp;
} catch (\Throwable $error) {
// Catch integer overflow or other fatal errors
\SnappyMail\Log::notice('', "Failed to parse RFC 2822 date '{$sDateTime}'. {$error->getMessage()}");
return 0;
} }
return $oDateTime ? $oDateTime->getTimestamp() : 0;
} }
/** /**

View file

@ -160,20 +160,13 @@ abstract class HtmlUtils
// Remove all remaining data-* attributes // Remove all remaining data-* attributes
foreach ($xpath->query('//*[@*[starts-with(name(), "data-")]]') as $oElement) { foreach ($xpath->query('//*[@*[starts-with(name(), "data-")]]') as $oElement) {
$sTagNameLower = \strtolower($oElement->nodeName);
if ($oElement->hasAttributes()) { if ($oElement->hasAttributes()) {
// DOMNamedNodeMap is live: removing the attribute the foreach is
// currently on ends the iteration, so exactly one data-* was
// removed per element however many it carried. Collect the
// names first, then remove.
$aNames = [];
foreach ($oElement->attributes as $oAttr) { foreach ($oElement->attributes as $oAttr) {
if ('data-' === \substr(\strtolower($oAttr->nodeName), 0, 5)) { if ('data-' === \substr(\strtolower($oAttr->nodeName), 0, 5)) {
$aNames[] = $oAttr->nodeName; $oElement->removeAttribute($oAttr->nodeName);
} }
} }
foreach ($aNames as $sName) {
$oElement->removeAttribute($sName);
}
} }
} }

View file

@ -30,7 +30,12 @@ class Http
return $oInstance; return $oInstance;
} }
public static function GetServer(string $sKey, mixed $mDefault = null): mixed /**
* @param mixed $mDefault = null
*
* @return mixed
*/
public static function GetServer(string $sKey, $mDefault = null)
{ {
return isset($_SERVER[$sKey]) ? $_SERVER[$sKey] : $mDefault; return isset($_SERVER[$sKey]) ? $_SERVER[$sKey] : $mDefault;
} }

View file

@ -59,7 +59,7 @@ abstract class Utils
return 'iso-8859-8'; return 'iso-8859-8';
} }
return \preg_replace('/^(cp|windows)-?(12[\d]*)/i', 'windows-$2', $sEncoding); return \preg_replace('/^(cp-?|windows?)(12[\d])/', 'windows-$1', $sEncoding);
} }
public static function NormalizeCharsetByValue(string $sCharset, string $sValue) : string public static function NormalizeCharsetByValue(string $sCharset, string $sValue) : string

View file

@ -40,27 +40,27 @@ class BodyStructure implements \JsonSerializable
*/ */
private array $aSubParts; private array $aSubParts;
public function Charset(): string public function Charset() : string
{ {
return $this->sCharset; return $this->sCharset;
} }
public function ContentTransferEncoding(): string public function ContentTransferEncoding() : string
{ {
return $this->sContentTransferEncoding; return $this->sContentTransferEncoding;
} }
public function ContentType(): string public function ContentType() : string
{ {
return $this->sContentType; return $this->sContentType;
} }
public function PartID(): string public function PartID() : string
{ {
return $this->sPartID; return $this->sPartID;
} }
public function FileName(bool $bCalculateOnEmpty = false): string public function FileName(bool $bCalculateOnEmpty = false) : string
{ {
$sFileName = \trim($this->sFileName); $sFileName = \trim($this->sFileName);
if (\strlen($sFileName) || !$bCalculateOnEmpty) { if (\strlen($sFileName) || !$bCalculateOnEmpty) {
@ -90,7 +90,7 @@ class BodyStructure implements \JsonSerializable
return ($this->isInline() ? 'inline' : 'part' ) . $sIdx; return ($this->isInline() ? 'inline' : 'part' ) . $sIdx;
} }
public function EstimatedSize(): int public function EstimatedSize() : int
{ {
$fCoefficient = 1; $fCoefficient = 1;
switch ($this->sContentTransferEncoding) switch ($this->sContentTransferEncoding)
@ -106,17 +106,17 @@ class BodyStructure implements \JsonSerializable
return (int) ($this->iSize * $fCoefficient); return (int) ($this->iSize * $fCoefficient);
} }
public function SubParts(): array public function SubParts() : array
{ {
return $this->aSubParts; return $this->aSubParts;
} }
public function isInline(): bool public function isInline() : bool
{ {
return 'inline' === $this->sDisposition || \strlen($this->sContentID); return 'inline' === $this->sDisposition || \strlen($this->sContentID);
} }
public function isText(): bool public function isText() : bool
{ {
return 'text/html' === $this->sContentType return 'text/html' === $this->sContentType
|| 'text/plain' === $this->sContentType || 'text/plain' === $this->sContentType
@ -125,7 +125,7 @@ class BodyStructure implements \JsonSerializable
} }
// https://datatracker.ietf.org/doc/html/rfc3156#section-4 // https://datatracker.ietf.org/doc/html/rfc3156#section-4
public function isPgpEncrypted(): bool public function isPgpEncrypted() : bool
{ {
return 'multipart/encrypted' === $this->sContentType return 'multipart/encrypted' === $this->sContentType
&& !empty($this->aContentTypeParams['protocol']) && !empty($this->aContentTypeParams['protocol'])
@ -138,7 +138,7 @@ class BodyStructure implements \JsonSerializable
} }
// https://datatracker.ietf.org/doc/html/rfc3156#section-5 // https://datatracker.ietf.org/doc/html/rfc3156#section-5
public function isPgpSigned(): bool public function isPgpSigned() : bool
{ {
return 'multipart/signed' === $this->sContentType return 'multipart/signed' === $this->sContentType
&& !empty($this->aContentTypeParams['protocol']) && !empty($this->aContentTypeParams['protocol'])
@ -149,7 +149,7 @@ class BodyStructure implements \JsonSerializable
} }
// https://datatracker.ietf.org/doc/html/rfc2633#section-3.3 // https://datatracker.ietf.org/doc/html/rfc2633#section-3.3
public function isSMimeEncrypted(): bool public function isSMimeEncrypted() : bool
{ {
$type = \strtolower(\trim($this->aContentTypeParams['smime-type'] ?? '')); $type = \strtolower(\trim($this->aContentTypeParams['smime-type'] ?? ''));
return ContentType::isPkcs7Mime($this->sContentType) return ContentType::isPkcs7Mime($this->sContentType)
@ -158,7 +158,7 @@ class BodyStructure implements \JsonSerializable
} }
// https://www.rfc-editor.org/rfc/rfc8551.html#section-3.5 // https://www.rfc-editor.org/rfc/rfc8551.html#section-3.5
public function isSMimeSigned(): bool public function isSMimeSigned() : bool
{ {
return ('multipart/signed' === $this->sContentType return ('multipart/signed' === $this->sContentType
&& !empty($this->aContentTypeParams['protocol']) && !empty($this->aContentTypeParams['protocol'])
@ -172,7 +172,7 @@ class BodyStructure implements \JsonSerializable
); );
} }
protected function IsAttachment(/*BodyStructure*/ $oParent): bool protected function IsAttachment(/*BodyStructure*/ $oParent) : bool
{ {
// return 'application/pgp-encrypted' !== $this->sContentType // return 'application/pgp-encrypted' !== $this->sContentType
return (!$oParent || !$oParent->isPgpEncrypted()) return (!$oParent || !$oParent->isPgpEncrypted())
@ -184,14 +184,14 @@ class BodyStructure implements \JsonSerializable
); );
} }
public function IsFlowedFormat(): bool public function IsFlowedFormat() : bool
{ {
return !empty($this->aContentTypeParams['format']) return !empty($this->aContentTypeParams['format'])
&& 'flowed' === \strtolower(\trim($this->aContentTypeParams['format'])) && 'flowed' === \strtolower(\trim($this->aContentTypeParams['format']))
&& !\in_array($this->sContentTransferEncoding, array('base64', 'quoted-printable')); && !\in_array($this->sContentTransferEncoding, array('base64', 'quoted-printable'));
} }
public function GetHtmlAndPlainParts(): array public function GetHtmlAndPlainParts() : array
{ {
$aParts = []; $aParts = [];
@ -231,7 +231,7 @@ class BodyStructure implements \JsonSerializable
return $aParts; return $aParts;
} }
public function SearchCharset(): string public function SearchCharset() : string
{ {
$gParts = $this->SearchByCallback(function ($oPart, $oParent) { $gParts = $this->SearchByCallback(function ($oPart, $oParent) {
return $oPart->Charset() && $oPart->isText() && !$oPart->IsAttachment($oParent); return $oPart->Charset() && $oPart->isText() && !$oPart->IsAttachment($oParent);
@ -246,7 +246,7 @@ class BodyStructure implements \JsonSerializable
return $gParts->valid() ? $gParts->current()->Charset() : ''; return $gParts->valid() ? $gParts->current()->Charset() : '';
} }
public function SearchByCallback(callable $fCallback, /*?BodyStructure*/ $parent = null): iterable public function SearchByCallback(callable $fCallback, /*BodyStructure*/ $parent = null) : iterable
{ {
if ($fCallback($this, $parent)) { if ($fCallback($this, $parent)) {
yield $this; yield $this;
@ -256,7 +256,7 @@ class BodyStructure implements \JsonSerializable
} }
} }
public function SearchAttachmentsParts(): iterable public function SearchAttachmentsParts() : iterable
{ {
return $this->SearchByCallback(function ($oItem, $oParent) { return $this->SearchByCallback(function ($oItem, $oParent) {
// return $oItem->IsAttachment(); // return $oItem->IsAttachment();
@ -264,7 +264,7 @@ class BodyStructure implements \JsonSerializable
}); });
} }
public function SearchByContentType(string $sContentType): iterable public function SearchByContentType(string $sContentType) : iterable
{ {
$sContentType = \strtolower($sContentType); $sContentType = \strtolower($sContentType);
return $this->SearchByCallback(function ($oItem) use ($sContentType) { return $this->SearchByCallback(function ($oItem) use ($sContentType) {
@ -272,14 +272,14 @@ class BodyStructure implements \JsonSerializable
}); });
} }
public function SearchByContentTypes(array $aContentTypes): iterable public function SearchByContentTypes(array $aContentTypes) : iterable
{ {
return $this->SearchByCallback(function ($oItem) use ($aContentTypes) { return $this->SearchByCallback(function ($oItem) use ($aContentTypes) {
return \in_array($oItem->sContentType, $aContentTypes); return \in_array($oItem->sContentType, $aContentTypes);
}); });
} }
public function GetPartByMimeIndex(string $sMimeIndex): self public function GetPartByMimeIndex(string $sMimeIndex) : self
{ {
$oPart = null; $oPart = null;
if (\strlen($sMimeIndex)) { if (\strlen($sMimeIndex)) {
@ -300,7 +300,7 @@ class BodyStructure implements \JsonSerializable
return $oPart; return $oPart;
} }
private static function decodeAttrParameter(array $aParams, string $sParamName, string $sCharset): string private static function decodeAttrParameter(array $aParams, string $sParamName, string $sCharset) : string
{ {
$sResult = ''; $sResult = '';
if (isset($aParams[$sParamName])) { if (isset($aParams[$sParamName])) {
@ -349,7 +349,7 @@ class BodyStructure implements \JsonSerializable
return $sResult; return $sResult;
} }
public static function NewInstance(array $aBodyStructure, string $sPartID = ''): ?self public static function NewInstance(array $aBodyStructure, string $sPartID = '') : ?self
{ {
if (2 > \count($aBodyStructure)) { if (2 > \count($aBodyStructure)) {
return null; return null;
@ -542,7 +542,7 @@ class BodyStructure implements \JsonSerializable
* Returns dict with key="charset" and value="US-ASCII" for array ("CHARSET" "US-ASCII"). * Returns dict with key="charset" and value="US-ASCII" for array ("CHARSET" "US-ASCII").
* Keys are lowercased (StringDictionary itself does this), values are not altered. * Keys are lowercased (StringDictionary itself does this), values are not altered.
*/ */
private static function getKeyValueListFromArrayList(array $aList): array private static function getKeyValueListFromArrayList(array $aList) : array
{ {
$aDict = array(); $aDict = array();
$iLen = \count($aList); $iLen = \count($aList);
@ -557,8 +557,8 @@ class BodyStructure implements \JsonSerializable
return $aDict; return $aDict;
} }
// public function jsonSerialize(): array #[\ReturnTypeWillChange]
public function jsonSerialize(): mixed public function jsonSerialize()
{ {
return array( return array(
'mimeIndex' => $this->sPartID, 'mimeIndex' => $this->sPartID,

View file

@ -26,7 +26,7 @@ class FetchResponse
$this->oImapResponse = $oImapResponse; $this->oImapResponse = $oImapResponse;
} }
public function GetEnvelope(bool $bForce = false): ?array public function GetEnvelope(bool $bForce = false) : ?array
{ {
if (null === $this->aEnvelopeCache || $bForce) { if (null === $this->aEnvelopeCache || $bForce) {
$this->aEnvelopeCache = $this->GetFetchValue(Enumerations\FetchType::ENVELOPE); $this->aEnvelopeCache = $this->GetFetchValue(Enumerations\FetchType::ENVELOPE);
@ -34,12 +34,15 @@ class FetchResponse
return $this->aEnvelopeCache; return $this->aEnvelopeCache;
} }
public function GetFetchEnvelopeValue(int $iIndex, ?string $mNullResult = null): mixed /**
* @return mixed
*/
public function GetFetchEnvelopeValue(int $iIndex, ?string $mNullResult = null)
{ {
return self::findEnvelopeIndex($this->GetEnvelope(), $iIndex, $mNullResult); return self::findEnvelopeIndex($this->GetEnvelope(), $iIndex, $mNullResult);
} }
public function GetFetchEnvelopeEmailCollection(int $iIndex, string $sParentCharset = \MailSo\Base\Enumerations\Charset::ISO_8859_1): ?\MailSo\Mime\EmailCollection public function GetFetchEnvelopeEmailCollection(int $iIndex, string $sParentCharset = \MailSo\Base\Enumerations\Charset::ISO_8859_1) : ?\MailSo\Mime\EmailCollection
{ {
$oResult = null; $oResult = null;
$aEmails = $this->GetFetchEnvelopeValue($iIndex); $aEmails = $this->GetFetchEnvelopeValue($iIndex);
@ -68,7 +71,7 @@ class FetchResponse
return $oResult; return $oResult;
} }
public function GetFetchBodyStructure(): ?BodyStructure public function GetFetchBodyStructure() : ?BodyStructure
{ {
$aBodyStructureArray = $this->GetFetchValue(Enumerations\FetchType::BODYSTRUCTURE); $aBodyStructureArray = $this->GetFetchValue(Enumerations\FetchType::BODYSTRUCTURE);
@ -79,8 +82,9 @@ class FetchResponse
/** /**
* Like: UID, RFC822.SIZE, MODSEQ, INTERNALDATE, FLAGS, BODYSTRUCTURE * Like: UID, RFC822.SIZE, MODSEQ, INTERNALDATE, FLAGS, BODYSTRUCTURE
* @return mixed
*/ */
public function GetFetchValue(string $sFetchItemName): mixed public function GetFetchValue(string $sFetchItemName)
{ {
if (isset($this->oImapResponse->ResponseList[3]) && \is_array($this->oImapResponse->ResponseList[3])) { if (isset($this->oImapResponse->ResponseList[3]) && \is_array($this->oImapResponse->ResponseList[3])) {
$bNextIsValue = false; $bNextIsValue = false;
@ -100,8 +104,9 @@ class FetchResponse
/** /**
* Like: BODY[HEADER.FIELDS (RETURN-PATH RECEIVED MIME-VERSION MESSAGE-ID CONTENT-TYPE FROM TO CC BCC SENDER REPLY-TO DELIVERED-TO IN-REPLY-TO REFERENCES DATE SUBJECT SENSITIVITY X-MSMAIL-PRIORITY IMPORTANCE X-PRIORITY X-DRAFT-INFO RETURN-RECEIPT-TO DISPOSITION-NOTIFICATION-TO X-CONFIRM-READING-TO AUTHENTICATION-RESULTS X-DKIM-AUTHENTICATION-RESULTS LIST-UNSUBSCRIBE X-SPAM-STATUS X-SPAMD-RESULT X-BOGOSITY X-VIRUS X-VIRUS-SCANNED X-VIRUS-STATUS)] * Like: BODY[HEADER.FIELDS (RETURN-PATH RECEIVED MIME-VERSION MESSAGE-ID CONTENT-TYPE FROM TO CC BCC SENDER REPLY-TO DELIVERED-TO IN-REPLY-TO REFERENCES DATE SUBJECT SENSITIVITY X-MSMAIL-PRIORITY IMPORTANCE X-PRIORITY X-DRAFT-INFO RETURN-RECEIPT-TO DISPOSITION-NOTIFICATION-TO X-CONFIRM-READING-TO AUTHENTICATION-RESULTS X-DKIM-AUTHENTICATION-RESULTS LIST-UNSUBSCRIBE X-SPAM-STATUS X-SPAMD-RESULT X-BOGOSITY X-VIRUS X-VIRUS-SCANNED X-VIRUS-STATUS)]
* @return mixed
*/ */
public function GetHeaderFieldsValue(): string public function GetHeaderFieldsValue() : string
{ {
$bNextIsValue = false; $bNextIsValue = false;
@ -124,7 +129,7 @@ class FetchResponse
return ''; return '';
} }
public static function isValidImapResponse(Response $oImapResponse): bool public static function isValidImapResponse(Response $oImapResponse) : bool
{ {
return return
!$oImapResponse->IsStatusResponse !$oImapResponse->IsStatusResponse
@ -133,13 +138,16 @@ class FetchResponse
&& \is_array($oImapResponse->ResponseList[3]); && \is_array($oImapResponse->ResponseList[3]);
} }
public static function hasUidAndSize(Response $oImapResponse): bool public static function hasUidAndSize(Response $oImapResponse) : bool
{ {
return \in_array(Enumerations\FetchType::UID, $oImapResponse->ResponseList[3]) return \in_array(Enumerations\FetchType::UID, $oImapResponse->ResponseList[3])
&& \in_array(Enumerations\FetchType::RFC822_SIZE, $oImapResponse->ResponseList[3]); && \in_array(Enumerations\FetchType::RFC822_SIZE, $oImapResponse->ResponseList[3]);
} }
private static function findEnvelopeIndex(array $aEnvelope, int $iIndex, ?string $mNullResult): mixed /**
* @return mixed
*/
private static function findEnvelopeIndex(array $aEnvelope, int $iIndex, ?string $mNullResult)
{ {
return (isset($aEnvelope[$iIndex]) && '' !== $aEnvelope[$iIndex]) return (isset($aEnvelope[$iIndex]) && '' !== $aEnvelope[$iIndex])
? $aEnvelope[$iIndex] : $mNullResult; ? $aEnvelope[$iIndex] : $mNullResult;

View file

@ -53,22 +53,22 @@ class Folder implements \JsonSerializable
*/ */
} }
public function setAttributes(array $aAttributes): void public function setAttributes(array $aAttributes) : void
{ {
$this->aAttributes = \array_map('mb_strtolower', $aAttributes); $this->aAttributes = \array_map('mb_strtolower', $aAttributes);
} }
public function setSubscribed(): void public function setSubscribed() : void
{ {
$this->aAttributes = \array_unique(\array_merge($this->aAttributes, ['\\subscribed'])); $this->aAttributes = \array_unique(\array_merge($this->aAttributes, ['\\subscribed']));
} }
public function setDelimiter(?string $sDelimiter): void public function setDelimiter(?string $sDelimiter) : void
{ {
$this->sDelimiter = $sDelimiter; $this->sDelimiter = $sDelimiter;
} }
public function Name(): string public function Name() : string
{ {
$sNameRaw = $this->FullName; $sNameRaw = $this->FullName;
if ($this->sDelimiter) { if ($this->sDelimiter) {
@ -78,49 +78,49 @@ class Folder implements \JsonSerializable
return $sNameRaw; return $sNameRaw;
} }
public function Delimiter(): ?string public function Delimiter() : ?string
{ {
return $this->sDelimiter; return $this->sDelimiter;
} }
public function Selectable(): bool public function Selectable() : bool
{ {
return !\in_array('\\noselect', $this->aAttributes) return !\in_array('\\noselect', $this->aAttributes)
&& !\in_array('\\nonexistent', $this->aAttributes); && !\in_array('\\nonexistent', $this->aAttributes);
} }
public function IsSubscribed(): bool public function IsSubscribed() : bool
{ {
return \in_array('\\subscribed', $this->aAttributes); return \in_array('\\subscribed', $this->aAttributes);
} }
public function IsInbox(): bool public function IsInbox() : bool
{ {
return 'INBOX' === \strtoupper($this->FullName) || \in_array('\\inbox', $this->aAttributes); return 'INBOX' === \strtoupper($this->FullName) || \in_array('\\inbox', $this->aAttributes);
} }
public function SetMetadata(string $sName, string $sData): void public function SetMetadata(string $sName, string $sData) : void
{ {
$this->aMetadata[$sName] = $sData; $this->aMetadata[$sName] = $sData;
} }
public function SetAllMetadata(array $aMetadata): void public function SetAllMetadata(array $aMetadata) : void
{ {
$this->aMetadata = $aMetadata; $this->aMetadata = $aMetadata;
} }
public function GetMetadata(string $sName): ?string public function GetMetadata(string $sName) : ?string
{ {
return isset($this->aMetadata[$sName]) ? $this->aMetadata[$sName] : null; return isset($this->aMetadata[$sName]) ? $this->aMetadata[$sName] : null;
} }
public function Metadata(): array public function Metadata() : array
{ {
return $this->aMetadata; return $this->aMetadata;
} }
// JMAP RFC 8621 // JMAP RFC 8621
public function Role(): ?string public function Role() : ?string
{ {
$role = \strtolower($this->GetMetadata(MetadataKeys::SPECIALUSE) ?: ''); $role = \strtolower($this->GetMetadata(MetadataKeys::SPECIALUSE) ?: '');
if (!$role) { if (!$role) {
@ -145,8 +145,8 @@ class Folder implements \JsonSerializable
return $role ? \ltrim($role, '\\') : null; return $role ? \ltrim($role, '\\') : null;
} }
// public function jsonSerialize(): array #[\ReturnTypeWillChange]
public function jsonSerialize(): mixed public function jsonSerialize()
{ {
$selectable = $this->Selectable(); $selectable = $this->Selectable();
$result = array( $result = array(

View file

@ -19,14 +19,14 @@ class FolderCollection extends \MailSo\Base\Collection
{ {
// public bool $Optimized = false; // public bool $Optimized = false;
public function append($oFolder, bool $bToTop = false): void public function append($oFolder, bool $bToTop = false) : void
{ {
assert($oFolder instanceof Folder); assert($oFolder instanceof Folder);
parent::append($oFolder, $bToTop); parent::append($oFolder, $bToTop);
} }
// public function jsonSerialize(): array #[\ReturnTypeWillChange]
public function jsonSerialize(): mixed public function jsonSerialize()
{ {
return array( return array(
'@Object' => 'Collection/FolderCollection', '@Object' => 'Collection/FolderCollection',

View file

@ -40,15 +40,15 @@ class FolderInformation implements \JsonSerializable
$this->IsWritable = $bIsWritable; $this->IsWritable = $bIsWritable;
} }
public function IsFlagSupported(string $sFlag): bool public function IsFlagSupported(string $sFlag) : bool
{ {
return \in_array('\\*', $this->PermanentFlags) return \in_array('\\*', $this->PermanentFlags)
|| \in_array($sFlag, $this->PermanentFlags); || \in_array($sFlag, $this->PermanentFlags);
// || \in_array($sFlag, $this->Flags); // || \in_array($sFlag, $this->Flags);
} }
// public function jsonSerialize(): array #[\ReturnTypeWillChange]
public function jsonSerialize(): mixed public function jsonSerialize()
{ {
$result = array( $result = array(
'id' => $this->MAILBOXID, 'id' => $this->MAILBOXID,

View file

@ -49,7 +49,7 @@ class ImapClient extends \MailSo\Net\NetClient
*/ */
private bool $UTF8 = false; private bool $UTF8 = false;
public function Hash(): string public function Hash() : string
{ {
return \md5('ImapClientHash/'. return \md5('ImapClientHash/'.
$this->Settings->username . '@' . $this->Settings->username . '@' .
@ -64,8 +64,8 @@ class ImapClient extends \MailSo\Net\NetClient
* @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\* * @throws \MailSo\Imap\Exceptions\*
*/ */
// public function Connect(Settings $oSettings): void // public function Connect(Settings $oSettings) : void
public function Connect(\MailSo\Net\ConnectSettings $oSettings): void public function Connect(\MailSo\Net\ConnectSettings $oSettings) : void
{ {
$this->aTagTimeouts['*'] = \microtime(true); $this->aTagTimeouts['*'] = \microtime(true);
@ -79,7 +79,7 @@ class ImapClient extends \MailSo\Net\NetClient
} }
} }
private function StartTLS(): void private function StartTLS() : void
{ {
if ($this->hasCapability('STARTTLS')) { if ($this->hasCapability('STARTTLS')) {
$this->SendRequestGetResponse('STARTTLS'); $this->SendRequestGetResponse('STARTTLS');
@ -93,7 +93,7 @@ class ImapClient extends \MailSo\Net\NetClient
} }
} }
public function supportsAuthType(string $sasl_type): bool public function supportsAuthType(string $sasl_type) : bool
{ {
return $this->hasCapability("AUTH={$sasl_type}"); return $this->hasCapability("AUTH={$sasl_type}");
} }
@ -104,7 +104,7 @@ class ImapClient extends \MailSo\Net\NetClient
* @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\* * @throws \MailSo\Imap\Exceptions\*
*/ */
public function Login(Settings $oSettings): self public function Login(Settings $oSettings) : self
{ {
if ($this->bIsLoggined) { if ($this->bIsLoggined) {
return $this; return $this;
@ -242,7 +242,7 @@ class ImapClient extends \MailSo\Net\NetClient
* @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\* * @throws \MailSo\Imap\Exceptions\*
*/ */
public function Logout(): void public function Logout() : void
{ {
if ($this->bIsLoggined) { if ($this->bIsLoggined) {
$this->bIsLoggined = false; $this->bIsLoggined = false;
@ -250,12 +250,12 @@ class ImapClient extends \MailSo\Net\NetClient
} }
} }
public function IsLoggined(): bool public function IsLoggined() : bool
{ {
return $this->IsConnected() && $this->bIsLoggined; return $this->IsConnected() && $this->bIsLoggined;
} }
public function IsSelected(): bool public function IsSelected() : bool
{ {
return $this->IsLoggined() && $this->oCurrentFolderInfo; return $this->IsLoggined() && $this->oCurrentFolderInfo;
} }
@ -265,7 +265,7 @@ class ImapClient extends \MailSo\Net\NetClient
* @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\* * @throws \MailSo\Imap\Exceptions\*
*/ */
public function Capability(): ?array public function Capability() : ?array
{ {
if (!$this->aCapaRaw) { if (!$this->aCapaRaw) {
$this->setCapabilities($this->SendRequestGetResponse('CAPABILITY')); $this->setCapabilities($this->SendRequestGetResponse('CAPABILITY'));
@ -276,7 +276,7 @@ class ImapClient extends \MailSo\Net\NetClient
return $this->aCapa; return $this->aCapa;
} }
public function Capabilities(): ?array public function Capabilities() : ?array
{ {
if (!$this->aCapaRaw) { if (!$this->aCapaRaw) {
$this->Capability(); $this->Capability();
@ -284,7 +284,7 @@ class ImapClient extends \MailSo\Net\NetClient
return $this->aCapaRaw; return $this->aCapaRaw;
} }
public function CapabilityValue(string $sExtentionName): ?string public function CapabilityValue(string $sExtentionName) : ?string
{ {
$sExtentionName = \trim($sExtentionName) . '='; $sExtentionName = \trim($sExtentionName) . '=';
$aCapabilities = $this->Capability() ?: []; $aCapabilities = $this->Capability() ?: [];
@ -296,7 +296,7 @@ class ImapClient extends \MailSo\Net\NetClient
return null; return null;
} }
private function setCapabilities(ResponseCollection $oResponseCollection): void private function setCapabilities(ResponseCollection $oResponseCollection) : void
{ {
$aList = $oResponseCollection->getCapabilityResult(); $aList = $oResponseCollection->getCapabilityResult();
$this->aCapaRaw = $aList; $this->aCapaRaw = $aList;
@ -330,7 +330,7 @@ class ImapClient extends \MailSo\Net\NetClient
* @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\* * @throws \MailSo\Imap\Exceptions\*
*/ */
public function hasCapability(string $sExtentionName): bool public function hasCapability(string $sExtentionName) : bool
{ {
$sExtentionName = \trim($sExtentionName); $sExtentionName = \trim($sExtentionName);
return $sExtentionName && \in_array(\strtoupper($sExtentionName), $this->Capability() ?: []); return $sExtentionName && \in_array(\strtoupper($sExtentionName), $this->Capability() ?: []);
@ -339,7 +339,7 @@ class ImapClient extends \MailSo\Net\NetClient
/** /**
* RFC 5161 * RFC 5161
*/ */
public function Enable(string|array $mCapabilityNames): void public function Enable(/*string|array*/ $mCapabilityNames) : void
{ {
if (\is_string($mCapabilityNames)) { if (\is_string($mCapabilityNames)) {
$mCapabilityNames = [$mCapabilityNames]; $mCapabilityNames = [$mCapabilityNames];
@ -354,7 +354,7 @@ class ImapClient extends \MailSo\Net\NetClient
* @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\* * @throws \MailSo\Imap\Exceptions\*
*/ */
public function GetNamespaces(): ?NamespaceResult public function GetNamespaces() : ?NamespaceResult
{ {
if (!$this->hasCapability('NAMESPACE')) { if (!$this->hasCapability('NAMESPACE')) {
return null; return null;
@ -382,7 +382,7 @@ class ImapClient extends \MailSo\Net\NetClient
* and that the client will need to discover upload limits for each mailbox, * and that the client will need to discover upload limits for each mailbox,
* as they might differ from mailbox to mailbox. * as they might differ from mailbox to mailbox.
*/ */
public function AppendLimit(): ?int public function AppendLimit() : ?int
{ {
$string = $this->CapabilityValue('APPENDLIMIT'); $string = $this->CapabilityValue('APPENDLIMIT');
return \is_null($string) ? null : (int) $string; return \is_null($string) ? null : (int) $string;
@ -393,7 +393,7 @@ class ImapClient extends \MailSo\Net\NetClient
* @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\* * @throws \MailSo\Imap\Exceptions\*
*/ */
public function Noop(): self public function Noop() : self
{ {
$this->SendRequestGetResponse('NOOP'); $this->SendRequestGetResponse('NOOP');
return $this; return $this;
@ -405,7 +405,7 @@ class ImapClient extends \MailSo\Net\NetClient
* @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\* * @throws \MailSo\Imap\Exceptions\*
*/ */
public function SendRequest(string $sCommand, array $aParams = array(), bool $bBreakOnLiteral = false): string public function SendRequest(string $sCommand, array $aParams = array(), bool $bBreakOnLiteral = false) : string
{ {
$sCommand = \trim($sCommand); $sCommand = \trim($sCommand);
if (!\strlen($sCommand)) { if (!\strlen($sCommand)) {
@ -438,13 +438,13 @@ class ImapClient extends \MailSo\Net\NetClient
* @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\* * @throws \MailSo\Imap\Exceptions\*
*/ */
public function SendRequestGetResponse(string $sCommand, array $aParams = array()): ResponseCollection public function SendRequestGetResponse(string $sCommand, array $aParams = array()) : ResponseCollection
{ {
$this->SendRequest($sCommand, $aParams); $this->SendRequest($sCommand, $aParams);
return $this->getResponse(); return $this->getResponse();
} }
protected function getResponseValue(ResponseCollection $oResponseCollection, int $type = 0): string protected function getResponseValue(ResponseCollection $oResponseCollection, int $type = 0) : string
{ {
$oResponse = $oResponseCollection->getLast(); $oResponse = $oResponseCollection->getLast();
if ($oResponse && (!$type || $type === $oResponse->ResponseType)) { if ($oResponse && (!$type || $type === $oResponse->ResponseType)) {
@ -466,7 +466,7 @@ class ImapClient extends \MailSo\Net\NetClient
* @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\* * @throws \MailSo\Imap\Exceptions\*
*/ */
protected function streamResponse(?string $sEndTag = null): void protected function streamResponse(?string $sEndTag = null) : void
{ {
try { try {
if (\is_resource($this->ConnectionResource())) { if (\is_resource($this->ConnectionResource())) {
@ -488,7 +488,7 @@ class ImapClient extends \MailSo\Net\NetClient
} }
} }
protected function getResponse(?string $sEndTag = null): ResponseCollection protected function getResponse(?string $sEndTag = null) : ResponseCollection
{ {
try { try {
$oResult = new ResponseCollection; $oResult = new ResponseCollection;
@ -535,8 +535,8 @@ class ImapClient extends \MailSo\Net\NetClient
return $oResult; return $oResult;
} }
// public function yieldUntaggedResponses(): \Generator // public function yieldUntaggedResponses() : \Generator
public function yieldUntaggedResponses(): iterable public function yieldUntaggedResponses() : iterable
{ {
try { try {
$oResult = new ResponseCollection; $oResult = new ResponseCollection;
@ -577,7 +577,7 @@ class ImapClient extends \MailSo\Net\NetClient
} }
} }
protected function prepareParamLine(array $aParams = array()): string protected function prepareParamLine(array $aParams = array()) : string
{ {
$sReturn = ''; $sReturn = '';
foreach ($aParams as $mParamItem) { foreach ($aParams as $mParamItem) {
@ -590,18 +590,18 @@ class ImapClient extends \MailSo\Net\NetClient
return $sReturn; return $sReturn;
} }
protected function getNewTag(): string protected function getNewTag() : string
{ {
++$this->iTagCount; ++$this->iTagCount;
return $this->getCurrentTag(); return $this->getCurrentTag();
} }
protected function getCurrentTag(): string protected function getCurrentTag() : string
{ {
return $this->TAG_PREFIX.$this->iTagCount; return $this->TAG_PREFIX.$this->iTagCount;
} }
public function EscapeString(?string $sStringForEscape): string public function EscapeString(?string $sStringForEscape) : string
{ {
if (null === $sStringForEscape) { if (null === $sStringForEscape) {
return 'NIL'; return 'NIL';
@ -617,7 +617,7 @@ class ImapClient extends \MailSo\Net\NetClient
return '"' . \addcslashes($sStringForEscape, '\\"') . '"'; return '"' . \addcslashes($sStringForEscape, '\\"') . '"';
} }
public function getLogName(): string public function getLogName() : string
{ {
return 'IMAP'; return 'IMAP';
} }
@ -626,7 +626,7 @@ class ImapClient extends \MailSo\Net\NetClient
* RFC 2971 * RFC 2971
* Don't have to be logged in to call this command * Don't have to be logged in to call this command
*/ */
public function ServerID(): string public function ServerID() : string
{ {
if ($this->hasCapability('ID')) { if ($this->hasCapability('ID')) {
foreach ($this->SendRequestGetResponse('ID', [null]) as $oResponse) { foreach ($this->SendRequestGetResponse('ID', [null]) as $oResponse) {
@ -647,7 +647,7 @@ class ImapClient extends \MailSo\Net\NetClient
* RFC 4978 * RFC 4978
* It is RECOMMENDED that the client uses TLS compression. * It is RECOMMENDED that the client uses TLS compression.
*//* *//*
public function Compress(): bool public function Compress() : bool
{ {
try { try {
if ($this->hasCapability('COMPRESS=DEFLATE')) { if ($this->hasCapability('COMPRESS=DEFLATE')) {
@ -663,12 +663,12 @@ class ImapClient extends \MailSo\Net\NetClient
return false; return false;
}*/ }*/
public function EscapeFolderName(string $sFolderName): string public function EscapeFolderName(string $sFolderName) : string
{ {
return $this->EscapeString($this->UTF8 ? $sFolderName : \MailSo\Base\Utils::Utf8ToUtf7Modified($sFolderName)); return $this->EscapeString($this->UTF8 ? $sFolderName : \MailSo\Base\Utils::Utf8ToUtf7Modified($sFolderName));
} }
public function toUTF8(string $sText): string public function toUTF8(string $sText) : string
{ {
return $this->UTF8 ? $sText : \MailSo\Base\Utils::Utf7ModifiedToUtf8($sText); return $this->UTF8 ? $sText : \MailSo\Base\Utils::Utf7ModifiedToUtf8($sText);
} }

View file

@ -59,7 +59,7 @@ class NamespaceResult implements \JsonSerializable
} }
} }
public function GetPersonalPrefix(): string public function GetPersonalPrefix() : string
{ {
$sPrefix = ''; $sPrefix = '';
if (isset($this->aPersonal[0])) { if (isset($this->aPersonal[0])) {
@ -72,8 +72,8 @@ class NamespaceResult implements \JsonSerializable
return $sPrefix; return $sPrefix;
} }
// public function jsonSerialize(): array #[\ReturnTypeWillChange]
public function jsonSerialize(): mixed public function jsonSerialize()
{ {
return array( return array(
'@Object' => 'Object/Namespaces', '@Object' => 'Object/Namespaces',

View file

@ -42,7 +42,7 @@ class THREAD extends Request
parent::__construct($oImapClient); parent::__construct($oImapClient);
} }
public function setAlgorithm(string $sAlgorithm): void public function setAlgorithm(string $sAlgorithm) : void
{ {
$sAlgorithm = \strtoupper($sAlgorithm); $sAlgorithm = \strtoupper($sAlgorithm);
if (!$this->oImapClient->hasCapability("THREAD={$sAlgorithm}")) { if (!$this->oImapClient->hasCapability("THREAD={$sAlgorithm}")) {
@ -51,7 +51,7 @@ class THREAD extends Request
$this->sAlgorithm = $sAlgorithm; $this->sAlgorithm = $sAlgorithm;
} }
public function SendRequestIterateResponse(): iterable public function SendRequestIterateResponse() : iterable
{ {
$this->oImapClient->SendRequest( $this->oImapClient->SendRequest(
($this->bUid ? 'UID THREAD' : 'THREAD'), ($this->bUid ? 'UID THREAD' : 'THREAD'),
@ -79,7 +79,12 @@ class THREAD extends Request
} }
} }
private function validateThreadItem(mixed $mValue): int|array|false /**
* @param mixed $mValue
*
* @return int | array | false
*/
private function validateThreadItem($mValue)
{ {
if (\is_numeric($mValue)) { if (\is_numeric($mValue)) {
$mValue = (int) $mValue; $mValue = (int) $mValue;

View file

@ -28,19 +28,19 @@ class ACL implements \JsonSerializable
$this->rights = $rights; $this->rights = $rights;
} }
public function identifier(): string public function identifier() : string
{ {
return $this->identifier; return $this->identifier;
} }
/** PHP 8.1 /** PHP 8.1
public function hasRight(string|\MailSo\Imap\Enumerations\FolderACL $right): bool public function hasRight(string|\MailSo\Imap\Enumerations\FolderACL $right)
{ {
if ($right instanceof \BackedEnum) { if ($right instanceof \BackedEnum) {
return \str_contains($this->rights, $right->value); return \str_contains($this->rights, $right->value);
} }
*/ */
public function hasRight(string $right): bool public function hasRight(string $right) : bool
{ {
$const = '\\MailSo\\Imap\\Enumerations\\FolderACL::' . \strtoupper($right); $const = '\\MailSo\\Imap\\Enumerations\\FolderACL::' . \strtoupper($right);
if (\defined($const)) { if (\defined($const)) {
@ -49,8 +49,8 @@ class ACL implements \JsonSerializable
return \str_contains($this->rights, $right); return \str_contains($this->rights, $right);
} }
// public function jsonSerialize(): array #[\ReturnTypeWillChange]
public function jsonSerialize(): mixed public function jsonSerialize()
{ {
return [ return [
'@Object' => 'Object/FolderACLRights', '@Object' => 'Object/FolderACLRights',

View file

@ -53,7 +53,7 @@ class SequenceSet /*extends \SplFixedArray*/ implements \Countable
return \in_array($value, $this->data); return \in_array($value, $this->data);
} }
public function indexOf($value): int|false public function indexOf($value)/*: int|false*/
{ {
return \array_search($value, $this->data); return \array_search($value, $this->data);
} }

View file

@ -40,7 +40,7 @@ class Settings extends \MailSo\Net\ConnectSettings
public array public array
$disabled_capabilities = []; $disabled_capabilities = [];
public static function fromArray(array $aSettings): self public static function fromArray(array $aSettings) : self
{ {
$object = parent::fromArray($aSettings); $object = parent::fromArray($aSettings);
@ -119,8 +119,8 @@ class Settings extends \MailSo\Net\ConnectSettings
return $object; return $object;
} }
// public function jsonSerialize(): array #[\ReturnTypeWillChange]
public function jsonSerialize(): mixed public function jsonSerialize()
{ {
if (\in_array('SORT', $this->disabled_capabilities)) { if (\in_array('SORT', $this->disabled_capabilities)) {
$this->disabled_capabilities[] = 'ESORT'; $this->disabled_capabilities[] = 'ESORT';

View file

@ -48,7 +48,7 @@ abstract class Driver
$this->oTimeZone = new \DateTimeZone('UTC'); $this->oTimeZone = new \DateTimeZone('UTC');
} }
public function SetTimeZone(\DateTimeZone|string $mTimeZone): self public function SetTimeZone(/*\DateTimeZone | string*/ $mTimeZone) : self
{ {
$this->oTimeZone = $mTimeZone instanceof \DateTimeZone $this->oTimeZone = $mTimeZone instanceof \DateTimeZone
? $mTimeZone ? $mTimeZone
@ -56,43 +56,43 @@ abstract class Driver
return $this; return $this;
} }
public function DisableGuidPrefix(): self public function DisableGuidPrefix() : self
{ {
$this->bGuidPrefix = false; $this->bGuidPrefix = false;
return $this; return $this;
} }
public function DisableTimePrefix(): self public function DisableTimePrefix() : self
{ {
$this->bTimePrefix = false; $this->bTimePrefix = false;
return $this; return $this;
} }
public function DisableTypedPrefix(): self public function DisableTypedPrefix() : self
{ {
$this->bTypedPrefix = false; $this->bTypedPrefix = false;
return $this; return $this;
} }
abstract protected function writeImplementation($mDesc): bool; abstract protected function writeImplementation($mDesc) : bool;
protected function clearImplementation(): bool protected function clearImplementation() : bool
{ {
return true; return true;
} }
protected function getTimeWithMicroSec(): string protected function getTimeWithMicroSec() : string
{ {
return \substr((new \DateTime('now', $this->oTimeZone))->format('Y-m-d H:i:s.u'), 0, -3); return \substr((new \DateTime('now', $this->oTimeZone))->format('Y-m-d H:i:s.u'), 0, -3);
} }
protected function getTypedPrefix(int $iType, string $sName = ''): string protected function getTypedPrefix(int $iType, string $sName = '') : string
{ {
$sName = \strlen($sName) ? $sName : $this->sName; $sName = \strlen($sName) ? $sName : $this->sName;
return isset(self::PREFIXES[$iType]) ? $sName . self::PREFIXES[$iType].': ' : ''; return isset(self::PREFIXES[$iType]) ? $sName . self::PREFIXES[$iType].': ' : '';
} }
final public function Write(string $sDesc, int $iType = \LOG_INFO, string $sName = '', bool $bDiplayCrLf = false): bool final public function Write(string $sDesc, int $iType = \LOG_INFO, string $sName = '', bool $bDiplayCrLf = false)
{ {
$sDesc = \ltrim( $sDesc = \ltrim(
($this->bTimePrefix ? '['.$this->getTimeWithMicroSec().']' : ''). ($this->bTimePrefix ? '['.$this->getTimeWithMicroSec().']' : '').
@ -105,7 +105,7 @@ abstract class Driver
return $this->writeImplementation($sDesc); return $this->writeImplementation($sDesc);
} }
final public function Clear(): bool final public function Clear() : bool
{ {
return $this->clearImplementation(); return $this->clearImplementation();
} }

View file

@ -84,7 +84,7 @@ class Logger extends \SplFixedArray
/** /**
* @staticvar string $sCache; * @staticvar string $sCache;
*/ */
public static function Guid(): string public static function Guid() : string
{ {
static $sCache = null; static $sCache = null;
if (null === $sCache) { if (null === $sCache) {
@ -94,7 +94,7 @@ class Logger extends \SplFixedArray
return $sCache; return $sCache;
} }
public function append($oDriver): void public function append($oDriver) : void
{ {
if ($oDriver) { if ($oDriver) {
$this->setSize(1); $this->setSize(1);
@ -102,7 +102,7 @@ class Logger extends \SplFixedArray
} }
} }
public function IsEnabled(): bool public function IsEnabled() : bool
{ {
return 0 < $this->count(); return 0 < $this->count();
} }
@ -110,7 +110,7 @@ class Logger extends \SplFixedArray
public function AddSecret( public function AddSecret(
#[\SensitiveParameter] #[\SensitiveParameter]
string $sWord string $sWord
): void ) : void
{ {
// $this->bShowSecrets && $this->Write("AddSecret '{$sWord}'", \LOG_INFO); // $this->bShowSecrets && $this->Write("AddSecret '{$sWord}'", \LOG_INFO);
$sWord = \trim($sWord); $sWord = \trim($sWord);
@ -121,18 +121,18 @@ class Logger extends \SplFixedArray
} }
} }
public function SetShowSecrets(bool $bShow): self public function SetShowSecrets(bool $bShow) : self
{ {
$this->bShowSecrets = $bShow; $this->bShowSecrets = $bShow;
return $this; return $this;
} }
public function ShowSecrets(): bool public function ShowSecrets() : bool
{ {
return $this->bShowSecrets; return $this->bShowSecrets;
} }
public function SetLevel(int $iLevel): self public function SetLevel(int $iLevel) : self
{ {
$this->iLevel = $iLevel; $this->iLevel = $iLevel;
return $this; return $this;
@ -150,7 +150,7 @@ class Logger extends \SplFixedArray
\E_USER_ERROR => \LOG_ERR, \E_USER_ERROR => \LOG_ERR,
\E_USER_WARNING => \LOG_WARNING, \E_USER_WARNING => \LOG_WARNING,
\E_USER_NOTICE => \LOG_NOTICE, \E_USER_NOTICE => \LOG_NOTICE,
// \E_STRICT => \LOG_CRIT, \E_STRICT => \LOG_CRIT,
\E_RECOVERABLE_ERROR => \LOG_ERR, \E_RECOVERABLE_ERROR => \LOG_ERR,
\E_DEPRECATED => \LOG_INFO, \E_DEPRECATED => \LOG_INFO,
\E_USER_DEPRECATED => \LOG_INFO \E_USER_DEPRECATED => \LOG_INFO
@ -168,13 +168,13 @@ class Logger extends \SplFixedArray
\E_USER_ERROR => '-USER', \E_USER_ERROR => '-USER',
\E_USER_WARNING => '-USER', \E_USER_WARNING => '-USER',
\E_USER_NOTICE => '-USER', \E_USER_NOTICE => '-USER',
// \E_STRICT => '-STRICT', \E_STRICT => '-STRICT',
\E_RECOVERABLE_ERROR => '-RECOVERABLE', \E_RECOVERABLE_ERROR => '-RECOVERABLE',
\E_DEPRECATED => '-DEPRECATED', \E_DEPRECATED => '-DEPRECATED',
\E_USER_DEPRECATED => '-USER_DEPRECATED' \E_USER_DEPRECATED => '-USER_DEPRECATED'
); );
public function __phpErrorHandler(int $iErrNo, string $sErrStr, string $sErrFile, int $iErrLine): bool public function __phpErrorHandler(int $iErrNo, string $sErrStr, string $sErrFile, int $iErrLine) : bool
{ {
if (\error_reporting() & $iErrNo) { if (\error_reporting() & $iErrNo) {
$this->Write( $this->Write(
@ -196,7 +196,7 @@ class Logger extends \SplFixedArray
\error_log('Uncaught exception: ' . $oException); \error_log('Uncaught exception: ' . $oException);
} }
public function __loggerShutDown(): void public function __loggerShutDown() : void
{ {
if ($this->bUsed) { if ($this->bUsed) {
$error = \error_get_last(); $error = \error_get_last();
@ -206,7 +206,7 @@ class Logger extends \SplFixedArray
} }
} }
public function signalHandler(int $signo, mixed $siginfo = null): void public function signalHandler($signo, /*?array*/$siginfo = null)
{ {
if (\SIGTERM == $signo) { if (\SIGTERM == $signo) {
exit; exit;
@ -219,7 +219,7 @@ class Logger extends \SplFixedArray
} }
} }
public function Write(string $sDesc, int $iType = \LOG_INFO, string $sName = '', bool $bDiplayCrLf = false): bool public function Write(string $sDesc, int $iType = \LOG_INFO, string $sName = '', bool $bDiplayCrLf = false) : bool
{ {
if ($this->iLevel < $iType) { if ($this->iLevel < $iType) {
return true; return true;
@ -241,14 +241,17 @@ class Logger extends \SplFixedArray
return (bool) $iResult; return (bool) $iResult;
} }
public function WriteDump(mixed $mValue, int $iType = \LOG_INFO, string $sName = ''): bool /**
* @param mixed $mValue
*/
public function WriteDump($mValue, int $iType = \LOG_INFO, string $sName = '') : bool
{ {
return $this->Write(\print_r($mValue, true), $iType, $sName); return $this->Write(\print_r($mValue, true), $iType, $sName);
} }
private $aExceptions = []; private $aExceptions = [];
public function WriteException(\Throwable $oException, int $iType = \LOG_NOTICE, string $sName = ''): void public function WriteException(\Throwable $oException, int $iType = \LOG_NOTICE, string $sName = '') : void
{ {
if (!\in_array($oException, $this->aExceptions)) { if (!\in_array($oException, $this->aExceptions)) {
$this->Write((string) $oException, $iType, $sName); $this->Write((string) $oException, $iType, $sName);
@ -256,7 +259,7 @@ class Logger extends \SplFixedArray
} }
} }
public function WriteExceptionShort(\Throwable $oException, int $iType = \LOG_NOTICE, string $sName = ''): void public function WriteExceptionShort(\Throwable $oException, int $iType = \LOG_NOTICE, string $sName = '') : void
{ {
if (!\in_array($oException, $this->aExceptions)) { if (!\in_array($oException, $this->aExceptions)) {
$this->Write($oException->getMessage(), $iType, $sName); $this->Write($oException->getMessage(), $iType, $sName);

View file

@ -32,7 +32,7 @@ class Attachment implements \JsonSerializable
$this->oBodyStructure = $oBodyStructure; $this->oBodyStructure = $oBodyStructure;
} }
public function Clear(): self public function Clear() : self
{ {
$this->sFolder = ''; $this->sFolder = '';
$this->iUid = 0; $this->iUid = 0;
@ -41,23 +41,23 @@ class Attachment implements \JsonSerializable
return $this; return $this;
} }
public function Folder(): string public function Folder() : string
{ {
return $this->sFolder; return $this->sFolder;
} }
public function Uid(): int public function Uid() : int
{ {
return $this->iUid; return $this->iUid;
} }
public function __call(string $name, array $arguments): mixed public function __call(string $name, array $arguments) //: mixed
{ {
return $this->oBodyStructure->{$name}(...$arguments); return $this->oBodyStructure->{$name}(...$arguments);
} }
// public function jsonSerialize(): array #[\ReturnTypeWillChange]
public function jsonSerialize(): mixed public function jsonSerialize()
{ {
return \array_merge([ return \array_merge([
'@Object' => 'Object/Attachment', '@Object' => 'Object/Attachment',

View file

@ -37,12 +37,12 @@ class MailClient
$this->oImapClient = new \MailSo\Imap\ImapClient; $this->oImapClient = new \MailSo\Imap\ImapClient;
} }
public function ImapClient(): \MailSo\Imap\ImapClient public function ImapClient() : \MailSo\Imap\ImapClient
{ {
return $this->oImapClient; return $this->oImapClient;
} }
private function getEnvelopeOrHeadersRequestString(): string private function getEnvelopeOrHeadersRequestString() : string
{ {
if ($this->oImapClient->Settings->message_all_headers) { if ($this->oImapClient->Settings->message_all_headers) {
return FetchType::BODY_HEADER_PEEK; return FetchType::BODY_HEADER_PEEK;
@ -115,7 +115,7 @@ class MailClient
* @throws \MailSo\Imap\Exceptions\* * @throws \MailSo\Imap\Exceptions\*
* @throws \MailSo\Mail\Exceptions\* * @throws \MailSo\Mail\Exceptions\*
*/ */
public function MessageSetFlag(string $sFolderName, SequenceSet $oRange, string $sMessageFlag, bool $bSetAction = true, bool $bSkipUnsupportedFlag = false): void public function MessageSetFlag(string $sFolderName, SequenceSet $oRange, string $sMessageFlag, bool $bSetAction = true, bool $bSkipUnsupportedFlag = false) : void
{ {
if (\count($oRange)) { if (\count($oRange)) {
if ($this->oImapClient->FolderSelect($sFolderName)->IsFlagSupported($sMessageFlag)) { if ($this->oImapClient->FolderSelect($sFolderName)->IsFlagSupported($sMessageFlag)) {
@ -133,7 +133,7 @@ class MailClient
* @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\* * @throws \MailSo\Imap\Exceptions\*
*/ */
public function Message(string $sFolderName, int $iIndex, bool $bIndexIsUid = true, ?\MailSo\Cache\CacheClient $oCacher = null): ?Message public function Message(string $sFolderName, int $iIndex, bool $bIndexIsUid = true, ?\MailSo\Cache\CacheClient $oCacher = null) : ?Message
{ {
if (1 > $iIndex) { if (1 > $iIndex) {
throw new \ValueError; throw new \ValueError;
@ -181,12 +181,14 @@ class MailClient
/** /**
* Streams mime part to $mCallback * Streams mime part to $mCallback
* *
* @param mixed $mCallback
*
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
* @throws \MailSo\RuntimeException * @throws \MailSo\RuntimeException
* @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\* * @throws \MailSo\Imap\Exceptions\*
*/ */
public function MessageMimeStream(callable $mCallback, string $sFolderName, int $iIndex, string $sMimeIndex): bool public function MessageMimeStream($mCallback, string $sFolderName, int $iIndex, string $sMimeIndex) : bool
{ {
if (!\is_callable($mCallback)) { if (!\is_callable($mCallback)) {
throw new \ValueError; throw new \ValueError;
@ -277,7 +279,7 @@ class MailClient
return ($aFetchResponse && 1 === \count($aFetchResponse)); return ($aFetchResponse && 1 === \count($aFetchResponse));
} }
public function MessageAppendFile(string $sMessageFileName, string $sFolderToSave, ?array $aAppendFlags = null): int public function MessageAppendFile(string $sMessageFileName, string $sFolderToSave, ?array $aAppendFlags = null) : int
{ {
if (!\is_file($sMessageFileName) || !\is_readable($sMessageFileName)) { if (!\is_file($sMessageFileName) || !\is_readable($sMessageFileName)) {
throw new \ValueError; throw new \ValueError;
@ -297,7 +299,7 @@ class MailClient
* Returns list of new messages since $iPrevUidNext * Returns list of new messages since $iPrevUidNext
* Currently only for INBOX * Currently only for INBOX
*/ */
private function getFolderNextMessageInformation(string $sFolderName, int $iPrevUidNext, int $iCurrentUidNext): array private function getFolderNextMessageInformation(string $sFolderName, int $iPrevUidNext, int $iCurrentUidNext) : array
{ {
$aNewMessages = array(); $aNewMessages = array();
@ -347,7 +349,7 @@ class MailClient
* @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\* * @throws \MailSo\Imap\Exceptions\*
*/ */
public function FolderInformation(string $sFolderName, int $iPrevUidNext = 0, ?SequenceSet $oRange = null): array public function FolderInformation(string $sFolderName, int $iPrevUidNext = 0, ?SequenceSet $oRange = null) : array
{ {
if ($oRange) { if ($oRange) {
// $aInfo = $this->oImapClient->FolderExamine($sFolderName)->jsonSerialize(); // $aInfo = $this->oImapClient->FolderExamine($sFolderName)->jsonSerialize();
@ -389,7 +391,7 @@ class MailClient
* @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\* * @throws \MailSo\Imap\Exceptions\*
*/ */
public function FolderHash(string $sFolderName): string public function FolderHash(string $sFolderName) : string
{ {
try try
{ {
@ -403,7 +405,7 @@ class MailClient
return ''; return '';
} }
public function MessageThread(string $sFolderName, string $sMessageID): MessageCollection public function MessageThread(string $sFolderName, string $sMessageID) : MessageCollection
{ {
$this->oImapClient->FolderExamine($sFolderName); $this->oImapClient->FolderExamine($sFolderName);
@ -432,7 +434,7 @@ class MailClient
* @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\* * @throws \MailSo\Imap\Exceptions\*
*/ */
protected function ThreadsMap(string $sAlgorithm, MessageCollection $oMessageCollection, ?\MailSo\Cache\CacheClient $oCacher, bool $bBackground = false): array protected function ThreadsMap(string $sAlgorithm, MessageCollection $oMessageCollection, ?\MailSo\Cache\CacheClient $oCacher, bool $bBackground = false) : array
{ {
$oFolderInfo = $oMessageCollection->FolderInfo; $oFolderInfo = $oMessageCollection->FolderInfo;
$sFolderName = $oFolderInfo->FullName; $sFolderName = $oFolderInfo->FullName;
@ -510,7 +512,7 @@ class MailClient
} }
// All threads UID's except the most recent UID of each thread // All threads UID's except the most recent UID of each thread
protected function ThreadsOldUids(array $aAllThreads, MessageCollection $oMessageCollection, ?\MailSo\Cache\CacheClient $oCacher, bool $bBackground = false): array protected function ThreadsOldUids(array $aAllThreads, MessageCollection $oMessageCollection, ?\MailSo\Cache\CacheClient $oCacher, bool $bBackground = false) : array
{ {
$oFolderInfo = $oMessageCollection->FolderInfo; $oFolderInfo = $oMessageCollection->FolderInfo;
@ -583,7 +585,7 @@ class MailClient
* @throws \MailSo\Imap\Exceptions\* * @throws \MailSo\Imap\Exceptions\*
*/ */
protected function MessageListByRequestIndexOrUids(MessageCollection $oMessageCollection, SequenceSet $oRange, protected function MessageListByRequestIndexOrUids(MessageCollection $oMessageCollection, SequenceSet $oRange,
array &$aAllThreads = [], array &$aUnseenUIDs = []): void array &$aAllThreads = [], array &$aUnseenUIDs = []) : void
{ {
if (\count($oRange)) { if (\count($oRange)) {
$aFetchItems = array( $aFetchItems = array(
@ -631,7 +633,7 @@ class MailClient
* @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\* * @throws \MailSo\Imap\Exceptions\*
*/ */
private function GetUids(MessageListParams $oParams, FolderInformation $oInfo, bool $onlyCache = false): ?array private function GetUids(MessageListParams $oParams, FolderInformation $oInfo, bool $onlyCache = false) : ?array
{ {
$oCacher = $oParams->oCacher; $oCacher = $oParams->oCacher;
$sFolderName = $oParams->sFolderName; $sFolderName = $oParams->sFolderName;
@ -723,7 +725,7 @@ class MailClient
return $aResultUids; return $aResultUids;
} }
public function MessageListUnseen(MessageListParams $oParams, FolderInformation $oInfo): array public function MessageListUnseen(MessageListParams $oParams, FolderInformation $oInfo) : array
{ {
$oUnseenParams = new MessageListParams; $oUnseenParams = new MessageListParams;
$oUnseenParams->sFolderName = $oParams->sFolderName; $oUnseenParams->sFolderName = $oParams->sFolderName;
@ -747,7 +749,7 @@ class MailClient
* @throws \MailSo\Net\Exceptions\* * @throws \MailSo\Net\Exceptions\*
* @throws \MailSo\Imap\Exceptions\* * @throws \MailSo\Imap\Exceptions\*
*/ */
public function MessageList(MessageListParams $oParams): MessageCollection public function MessageList(MessageListParams $oParams) : MessageCollection
{ {
if (0 > $oParams->iOffset || 0 > $oParams->iLimit) { if (0 > $oParams->iOffset || 0 > $oParams->iLimit) {
throw new \ValueError; throw new \ValueError;
@ -909,7 +911,7 @@ class MailClient
return $oMessageCollection; return $oMessageCollection;
} }
public function FindMessageUidByMessageId(string $sFolderName, string $sMessageId): ?int public function FindMessageUidByMessageId(string $sFolderName, string $sMessageId) : ?int
{ {
if (!\strlen($sMessageId)) { if (!\strlen($sMessageId)) {
throw new \ValueError; throw new \ValueError;
@ -922,7 +924,7 @@ class MailClient
return 1 === \count($aUids) && \is_numeric($aUids[0]) ? (int) $aUids[0] : null; return 1 === \count($aUids) && \is_numeric($aUids[0]) ? (int) $aUids[0] : null;
} }
public function Folders(string $sParent, string $sListPattern, bool $bUseListSubscribeStatus): ?FolderCollection public function Folders(string $sParent, string $sListPattern, bool $bUseListSubscribeStatus) : ?FolderCollection
{ {
$oFolderCollection = $this->oImapClient->FolderStatusList($sParent, $sListPattern); $oFolderCollection = $this->oImapClient->FolderStatusList($sParent, $sListPattern);
if (!$oFolderCollection->count()) { if (!$oFolderCollection->count()) {
@ -955,7 +957,7 @@ class MailClient
/** /**
* @throws \ValueError * @throws \ValueError
*/ */
public function FolderCreate(string $sFolderNameInUtf8, string $sFolderParentFullName = '', bool $bSubscribeOnCreation = true, string $sDelimiter = ''): ?\MailSo\Imap\Folder public function FolderCreate(string $sFolderNameInUtf8, string $sFolderParentFullName = '', bool $bSubscribeOnCreation = true, string $sDelimiter = '') : ?\MailSo\Imap\Folder
{ {
$sFolderNameInUtf8 = \trim($sFolderNameInUtf8); $sFolderNameInUtf8 = \trim($sFolderNameInUtf8);
$sFolderParentFullName = \trim($sFolderParentFullName); $sFolderParentFullName = \trim($sFolderParentFullName);
@ -1002,7 +1004,7 @@ class MailClient
/** /**
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
*/ */
public function FolderRename(string $sPrevFolderFullName, string $sNewFolderFullName): self public function FolderRename(string $sPrevFolderFullName, string $sNewFolderFullName) : self
{ {
if (!\strlen($sPrevFolderFullName) || !\strlen($sNewFolderFullName)) { if (!\strlen($sPrevFolderFullName) || !\strlen($sNewFolderFullName)) {
throw new \ValueError; throw new \ValueError;
@ -1045,7 +1047,7 @@ class MailClient
/** /**
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
*/ */
public function SetLogger(?\MailSo\Log\Logger $oLogger): void public function SetLogger(?\MailSo\Log\Logger $oLogger) : void
{ {
$this->oLogger = $oLogger; $this->oLogger = $oLogger;
$this->oImapClient->SetLogger($oLogger); $this->oImapClient->SetLogger($oLogger);

View file

@ -92,52 +92,52 @@ class Message implements \JsonSerializable
return \property_exists($this, $k) ? $this->$k : null; return \property_exists($this, $k) ? $this->$k : null;
} }
public function Subject(): string public function Subject() : string
{ {
return $this->sSubject; return $this->sSubject;
} }
public function From(): ?\MailSo\Mime\EmailCollection public function From() : ?\MailSo\Mime\EmailCollection
{ {
return $this->oFrom; return $this->oFrom;
} }
public function Uid(): int public function Uid() : int
{ {
return $this->Uid; return $this->Uid;
} }
public function Attachments(): ?AttachmentCollection public function Attachments() : ?AttachmentCollection
{ {
return $this->Attachments; return $this->Attachments;
} }
public function setPlain(string $value): void public function setPlain(string $value) : void
{ {
$this->sPlain = $value; $this->sPlain = $value;
} }
public function setHtml(string $value): void public function setHtml(string $value) : void
{ {
$this->sHtml = $value; $this->sHtml = $value;
} }
private function setSpamScore($value): void private function setSpamScore($value) : void
{ {
$this->SpamScore = \intval(\max(0, \min(100, $value))); $this->SpamScore = \intval(\max(0, \min(100, $value)));
} }
public function SetThreads(array $aThreadUIDs): void public function SetThreads(array $aThreadUIDs)
{ {
$this->aThreadUIDs = $aThreadUIDs; $this->aThreadUIDs = $aThreadUIDs;
} }
public function SetThreadUnseen(array $aUnseenUIDs): void public function SetThreadUnseen(array $aUnseenUIDs)
{ {
$this->aThreadUnseenUIDs = $aUnseenUIDs; $this->aThreadUnseenUIDs = $aUnseenUIDs;
} }
public static function fromFetchResponse(string $sFolder, \MailSo\Imap\FetchResponse $oFetchResponse, ?\MailSo\Imap\BodyStructure $oBodyStructure = null): self public static function fromFetchResponse(string $sFolder, \MailSo\Imap\FetchResponse $oFetchResponse, ?\MailSo\Imap\BodyStructure $oBodyStructure = null) : self
{ {
$oMessage = new self; $oMessage = new self;
@ -463,7 +463,7 @@ class Message implements \JsonSerializable
return $oMessage; return $oMessage;
} }
public function ETag(string $sClientHash): string public function ETag(string $sClientHash) : string
{ {
return \md5('MessageHash/' . \implode('/', [ return \md5('MessageHash/' . \implode('/', [
$this->sFolder, $this->sFolder,
@ -477,7 +477,7 @@ class Message implements \JsonSerializable
// https://datatracker.ietf.org/doc/html/rfc5788#section-3.4.1 // https://datatracker.ietf.org/doc/html/rfc5788#section-3.4.1
// Thunderbird $label1 is same as $Important? // Thunderbird $label1 is same as $Important?
// Thunderbird $label4 is same as $todo? // Thunderbird $label4 is same as $todo?
protected function getFlags(): array protected function getFlags() : array
{ {
return \array_unique(\str_replace( return \array_unique(\str_replace(
['$readreceipt', '$replied', /* 'junk', 'nonjunk', '$queued', '$sent', 'sent'*/], ['$readreceipt', '$replied', /* 'junk', 'nonjunk', '$queued', '$sent', 'sent'*/],
@ -486,8 +486,8 @@ class Message implements \JsonSerializable
)); ));
} }
// public function jsonSerialize(): array #[\ReturnTypeWillChange]
public function jsonSerialize(): mixed public function jsonSerialize()
{ {
/* /*
// JMAP-only RFC8621 keywords (RFC5788) // JMAP-only RFC8621 keywords (RFC5788)

View file

@ -36,7 +36,8 @@ class MessageCollection extends \MailSo\Base\Collection
public int $ThreadUid = 0; public int $ThreadUid = 0;
public ?\MailSo\Imap\FolderInformation $FolderInfo = null; // MailSo\Imap\FolderInformation
public $FolderInfo = null;
public array $NewMessages = array(); public array $NewMessages = array();
@ -44,19 +45,19 @@ class MessageCollection extends \MailSo\Base\Collection
public bool $Limited = false; public bool $Limited = false;
public function append($oMessage, bool $bToTop = false): void public function append($oMessage, bool $bToTop = false) : void
{ {
assert($oMessage instanceof Message); assert($oMessage instanceof Message);
parent::append($oMessage, $bToTop); parent::append($oMessage, $bToTop);
} }
public function Clear(): void public function Clear() : void
{ {
throw new \BadMethodCallException('disallowed'); throw new \BadMethodCallException('disallowed');
} }
// public function jsonSerialize(): array #[\ReturnTypeWillChange]
public function jsonSerialize(): mixed public function jsonSerialize()
{ {
return \array_merge(parent::jsonSerialize(), array( return \array_merge(parent::jsonSerialize(), array(
'totalEmails' => $this->totalEmails, 'totalEmails' => $this->totalEmails,

View file

@ -200,8 +200,8 @@ class Email implements \JsonSerializable
return $this->ToString(); return $this->ToString();
} }
// public function jsonSerialize(): array #[\ReturnTypeWillChange]
public function jsonSerialize(): mixed public function jsonSerialize()
{ {
return array( return array(
'@Object' => 'Object/Email', '@Object' => 'Object/Email',

View file

@ -27,13 +27,13 @@ class EmailCollection extends \MailSo\Base\Collection
} }
} }
public function append($oEmail, bool $bToTop = false): void public function append($oEmail, bool $bToTop = false) : void
{ {
assert($oEmail instanceof Email); assert($oEmail instanceof Email);
parent::append($oEmail, $bToTop); parent::append($oEmail, $bToTop);
} }
public function Unique(): self public function Unique() : self
{ {
$aReturn = array(); $aReturn = array();
@ -47,7 +47,7 @@ class EmailCollection extends \MailSo\Base\Collection
return new static($aReturn); return new static($aReturn);
} }
public function ToString(bool $bConvertSpecialsName = false, bool $bIdn = false): string public function ToString(bool $bConvertSpecialsName = false, bool $bIdn = false) : string
{ {
$aReturn = array(); $aReturn = array();
foreach ($this as $oEmail) { foreach ($this as $oEmail) {
@ -57,12 +57,12 @@ class EmailCollection extends \MailSo\Base\Collection
return \implode(', ', $aReturn); return \implode(', ', $aReturn);
} }
public function __toString(): string public function __toString() : string
{ {
return $this->ToString(); return $this->ToString();
} }
private function parseEmailAddresses(string $sRawEmails): void private function parseEmailAddresses(string $sRawEmails) : void
{ {
// $sRawEmails = \MailSo\Base\Utils::Trim($sRawEmails); // $sRawEmails = \MailSo\Base\Utils::Trim($sRawEmails);
$sRawEmails = \trim($sRawEmails); $sRawEmails = \trim($sRawEmails);
@ -156,8 +156,8 @@ class EmailCollection extends \MailSo\Base\Collection
} }
} }
// public function jsonSerialize(): array #[\ReturnTypeWillChange]
public function jsonSerialize(): mixed public function jsonSerialize()
{ {
return \array_slice($this->getArrayCopy(), 0, 100); return \array_slice($this->getArrayCopy(), 0, 100);
} }

View file

@ -35,7 +35,7 @@ class Header implements \JsonSerializable
$this->initInputData($sName, $sValue, $sEncodedValueForReparse); $this->initInputData($sName, $sValue, $sEncodedValueForReparse);
} }
private function initInputData(string $sName, string $sValue, string $sEncodedValueForReparse): void private function initInputData(string $sName, string $sValue, string $sEncodedValueForReparse) : void
{ {
$this->sName = \trim($sName); $this->sName = \trim($sName);
$this->sFullValue = \trim($sValue); $this->sFullValue = \trim($sValue);
@ -62,7 +62,7 @@ class Header implements \JsonSerializable
} }
} }
public static function NewInstanceFromEncodedString(string $sEncodedLines, string $sIncomingCharset = \MailSo\Base\Enumerations\Charset::ISO_8859_1): Header public static function NewInstanceFromEncodedString(string $sEncodedLines, string $sIncomingCharset = \MailSo\Base\Enumerations\Charset::ISO_8859_1) : Header
{ {
if (empty($sIncomingCharset)) { if (empty($sIncomingCharset)) {
$sIncomingCharset = \MailSo\Base\Enumerations\Charset::ISO_8859_1; $sIncomingCharset = \MailSo\Base\Enumerations\Charset::ISO_8859_1;
@ -81,27 +81,27 @@ class Header implements \JsonSerializable
return false; return false;
} }
public function Name(): string public function Name() : string
{ {
return $this->sName; return $this->sName;
} }
public function Value(): string public function Value() : string
{ {
return $this->sValue; return $this->sValue;
} }
public function FullValue(): string public function FullValue() : string
{ {
return $this->sFullValue; return $this->sFullValue;
} }
public function EncodedValue(): string public function EncodedValue() : string
{ {
return $this->sEncodedValue ?: $this->sFullValue; return $this->sEncodedValue ?: $this->sFullValue;
} }
public function SetParentCharset(string $sParentCharset): Header public function SetParentCharset(string $sParentCharset) : Header
{ {
if ($this->sParentCharset !== $sParentCharset && \strlen($this->sEncodedValue)) { if ($this->sParentCharset !== $sParentCharset && \strlen($this->sEncodedValue)) {
$this->initInputData( $this->initInputData(
@ -116,17 +116,17 @@ class Header implements \JsonSerializable
return $this; return $this;
} }
public function Parameters(): ?ParameterCollection public function Parameters() : ?ParameterCollection
{ {
return $this->oParameters; return $this->oParameters;
} }
public function setParameter(string $sName, string $sValue): void public function setParameter(string $sName, string $sValue) : void
{ {
$this->oParameters->setParameter($sName, $sValue); $this->oParameters->setParameter($sName, $sValue);
} }
public function __toString(): string public function __toString() : string
{ {
$sResult = $this->sFullValue; $sResult = $this->sFullValue;
@ -159,12 +159,12 @@ class Header implements \JsonSerializable
return $sName . \wordwrap($sResult, 78 - \strlen($sName) - 1, "\r\n "); return $sName . \wordwrap($sResult, 78 - \strlen($sName) - 1, "\r\n ");
} }
private function IsSubject(): bool private function IsSubject() : bool
{ {
return \strtolower(Enumerations\Header::SUBJECT) === \strtolower($this->Name()); return \strtolower(Enumerations\Header::SUBJECT) === \strtolower($this->Name());
} }
private function IsParameterized(): bool private function IsParameterized() : bool
{ {
return \in_array(\strtolower($this->sName), array( return \in_array(\strtolower($this->sName), array(
\strtolower(Enumerations\Header::CONTENT_TYPE), \strtolower(Enumerations\Header::CONTENT_TYPE),
@ -173,7 +173,7 @@ class Header implements \JsonSerializable
)); ));
} }
private function IsEmail(): bool private function IsEmail() : bool
{ {
return \in_array(\strtolower($this->sName), array( return \in_array(\strtolower($this->sName), array(
\strtolower(Enumerations\Header::FROM_), \strtolower(Enumerations\Header::FROM_),
@ -186,7 +186,7 @@ class Header implements \JsonSerializable
)); ));
} }
public function ValueWithCharsetAutoDetect(): string public function ValueWithCharsetAutoDetect() : string
{ {
if (!\MailSo\Base\Utils::IsAscii($this->Value()) if (!\MailSo\Base\Utils::IsAscii($this->Value())
&& \strlen($this->sEncodedValue) && \strlen($this->sEncodedValue)
@ -198,8 +198,8 @@ class Header implements \JsonSerializable
return $this->Value(); return $this->Value();
} }
// public function jsonSerialize(): array #[\ReturnTypeWillChange]
public function jsonSerialize(): mixed public function jsonSerialize()
{ {
$aResult = array( $aResult = array(
'@Object' => 'Object/MimeHeader', '@Object' => 'Object/MimeHeader',

View file

@ -27,30 +27,30 @@ class HeaderCollection extends \MailSo\Base\Collection
} }
} }
public function append($oHeader, bool $bToTop = false): void public function append($oHeader, bool $bToTop = false) : void
{ {
assert($oHeader instanceof Header); assert($oHeader instanceof Header);
parent::append($oHeader, $bToTop); parent::append($oHeader, $bToTop);
} }
public function AddByName(string $sName, string $sValue, bool $bToTop = false): self public function AddByName(string $sName, string $sValue, bool $bToTop = false) : self
{ {
$this->append(new Header($sName, $sValue), $bToTop); $this->append(new Header($sName, $sValue), $bToTop);
return $this; return $this;
} }
public function SetByName(string $sName, string $sValue, bool $bToTop = false): self public function SetByName(string $sName, string $sValue, bool $bToTop = false) : self
{ {
return $this->RemoveByName($sName)->Add(new Header($sName, $sValue), $bToTop); return $this->RemoveByName($sName)->Add(new Header($sName, $sValue), $bToTop);
} }
public function ValueByName(string $sHeaderName, bool $bCharsetAutoDetect = false): string public function ValueByName(string $sHeaderName, bool $bCharsetAutoDetect = false) : string
{ {
$oHeader = $this->GetByName($sHeaderName); $oHeader = $this->GetByName($sHeaderName);
return $oHeader ? ($bCharsetAutoDetect ? $oHeader->ValueWithCharsetAutoDetect() : $oHeader->Value()) : ''; return $oHeader ? ($bCharsetAutoDetect ? $oHeader->ValueWithCharsetAutoDetect() : $oHeader->Value()) : '';
} }
public function ValuesByName(string $sHeaderName): array public function ValuesByName(string $sHeaderName) : array
{ {
$aResult = array(); $aResult = array();
$sHeaderNameLower = \strtolower($sHeaderName); $sHeaderNameLower = \strtolower($sHeaderName);
@ -62,7 +62,7 @@ class HeaderCollection extends \MailSo\Base\Collection
return $aResult; return $aResult;
} }
public function RemoveByName(string $sHeaderName): self public function RemoveByName(string $sHeaderName) : self
{ {
$sHeaderName = \strtolower($sHeaderName); $sHeaderName = \strtolower($sHeaderName);
$this->exchangeArray(array_filter($this->getArrayCopy(), function ($oHeader) use ($sHeaderName) { $this->exchangeArray(array_filter($this->getArrayCopy(), function ($oHeader) use ($sHeaderName) {
@ -71,7 +71,7 @@ class HeaderCollection extends \MailSo\Base\Collection
return $this; return $this;
} }
public function GetAsEmailCollection(string $sHeaderName): ?EmailCollection public function GetAsEmailCollection(string $sHeaderName) : ?EmailCollection
{ {
if ($oHeader = $this->GetByName($sHeaderName)) { if ($oHeader = $this->GetByName($sHeaderName)) {
return new EmailCollection($oHeader->EncodedValue()); return new EmailCollection($oHeader->EncodedValue());
@ -79,14 +79,14 @@ class HeaderCollection extends \MailSo\Base\Collection
return new EmailCollection(); return new EmailCollection();
} }
public function ParameterValue(string $sHeaderName, string $sParamName): string public function ParameterValue(string $sHeaderName, string $sParamName) : string
{ {
$oHeader = $this->GetByName($sHeaderName); $oHeader = $this->GetByName($sHeaderName);
$oParameters = $oHeader ? $oHeader->Parameters() : null; $oParameters = $oHeader ? $oHeader->Parameters() : null;
return (null !== $oParameters) ? $oParameters->ParameterValueByName($sParamName) : ''; return (null !== $oParameters) ? $oParameters->ParameterValueByName($sParamName) : '';
} }
public function GetByName(string $sHeaderName): ?Header public function GetByName(string $sHeaderName) : ?Header
{ {
$sHeaderNameLower = \strtolower($sHeaderName); $sHeaderNameLower = \strtolower($sHeaderName);
foreach ($this as $oHeader) { foreach ($this as $oHeader) {
@ -97,7 +97,7 @@ class HeaderCollection extends \MailSo\Base\Collection
return null; return null;
} }
public function SetParentCharset(string $sParentCharset): self public function SetParentCharset(string $sParentCharset) : self
{ {
if (\strlen($sParentCharset) && $this->sParentCharset !== $sParentCharset) { if (\strlen($sParentCharset) && $this->sParentCharset !== $sParentCharset) {
foreach ($this as $oHeader) { foreach ($this as $oHeader) {
@ -108,7 +108,7 @@ class HeaderCollection extends \MailSo\Base\Collection
return $this; return $this;
} }
public function Parse(string $sRawHeaders, string $sParentCharset = ''): self public function Parse(string $sRawHeaders, string $sParentCharset = '') : self
{ {
$this->Clear(); $this->Clear();
@ -136,12 +136,14 @@ class HeaderCollection extends \MailSo\Base\Collection
$sHeadersValue = \rtrim($sHeadersValue); $sHeadersValue = \rtrim($sHeadersValue);
} }
// RFC 5322 unfolding removes the line break and keeps the folding if ('=?' === \substr(\ltrim($sHeadersValue), 0, 2)) {
// whitespace; RFC 2047 6.2 drops it only between two encoded words. $sHeadersValue = \ltrim($sHeadersValue);
if ('=?' === \substr(\ltrim($sHeadersValue), 0, 2) && '?=' === \substr($sValue, -2)) { }
$sValue .= \ltrim($sHeadersValue);
if ('=?' === \substr($sHeadersValue, 0, 2)) {
$sValue .= $sHeadersValue;
} else { } else {
$sValue .= ' '.\ltrim($sHeadersValue); $sValue .= "\n".$sHeadersValue;
} }
} else { } else {
if (null !== $sName) { if (null !== $sName) {
@ -180,7 +182,7 @@ class HeaderCollection extends \MailSo\Base\Collection
* spf=pass (ORIGINATING: domain of "snappymail@domain.tld" designates 0.0.0.0 as permitted sender) smtp.mailfrom="snappymail@domain.tld"; * spf=pass (ORIGINATING: domain of "snappymail@domain.tld" designates 0.0.0.0 as permitted sender) smtp.mailfrom="snappymail@domain.tld";
* dmarc=fail reason="SPF not aligned (relaxed), DKIM not aligned (relaxed)" header.from=domain.tld (policy=none) * dmarc=fail reason="SPF not aligned (relaxed), DKIM not aligned (relaxed)" header.from=domain.tld (policy=none)
*/ */
public function AuthStatuses(): array public function AuthStatuses() : array
{ {
$aResult = [ $aResult = [
'dkim' => [], 'dkim' => [],
@ -222,13 +224,13 @@ class HeaderCollection extends \MailSo\Base\Collection
return $aResult; return $aResult;
} }
public function __toString(): string public function __toString() : string
{ {
return \implode("\r\n", $this->getArrayCopy()); return \implode("\r\n", $this->getArrayCopy());
} }
// public function jsonSerialize(): array #[\ReturnTypeWillChange]
public function jsonSerialize(): mixed public function jsonSerialize()
{ {
return array( return array(
'@Object' => 'Collection/MimeHeaderCollection', '@Object' => 'Collection/MimeHeaderCollection',

View file

@ -27,7 +27,7 @@ class Parameter implements \JsonSerializable
$this->sValue = $sValue; $this->sValue = $sValue;
} }
public static function FromString(string $sRawParam): self public static function FromString(string $sRawParam) : self
{ {
$oParameter = new self('', ''); $oParameter = new self('', '');
@ -40,22 +40,22 @@ class Parameter implements \JsonSerializable
return $oParameter; return $oParameter;
} }
public function Name(): string public function Name() : string
{ {
return $this->sName; return $this->sName;
} }
public function Value(): string public function Value() : string
{ {
return $this->sValue; return $this->sValue;
} }
public function setValue(string $sValue): void public function setValue(string $sValue) : void
{ {
$this->sValue = $sValue; $this->sValue = $sValue;
} }
public function ToString(bool $bConvertSpecialsName = false): string public function ToString(bool $bConvertSpecialsName = false) : string
{ {
if (!\strlen($this->sName)) { if (!\strlen($this->sName)) {
return ''; return '';
@ -72,13 +72,13 @@ class Parameter implements \JsonSerializable
return $this->sName . '="' . $this->sValue . '"'; return $this->sName . '="' . $this->sValue . '"';
} }
public function __toString(): string public function __toString() : string
{ {
return $this->ToString(); return $this->ToString();
} }
// public function jsonSerialize(): array #[\ReturnTypeWillChange]
public function jsonSerialize(): mixed public function jsonSerialize()
{ {
return array( return array(
'name' => $this->sName, 'name' => $this->sName,

View file

@ -24,25 +24,25 @@ class ParameterCollection extends \MailSo\Base\Collection
\strlen($sRawParams) && $this->Parse($sRawParams); \strlen($sRawParams) && $this->Parse($sRawParams);
} }
public function append($oParameter, bool $bToTop = false): void public function append($oParameter, bool $bToTop = false) : void
{ {
assert($oParameter instanceof Parameter); assert($oParameter instanceof Parameter);
parent::offsetSet(\strtolower($oParameter->Name()), $oParameter); parent::offsetSet(\strtolower($oParameter->Name()), $oParameter);
} }
public function ParameterValueByName(string $sName): string public function ParameterValueByName(string $sName) : string
{ {
$oParam = $this->getParameter($sName); $oParam = $this->getParameter($sName);
return $oParam ? $oParam->Value() : ''; return $oParam ? $oParam->Value() : '';
} }
public function getParameter(string $sName): ?Parameter public function getParameter(string $sName) : ?Parameter
{ {
$sName = \strtolower(\trim($sName)); $sName = \strtolower(\trim($sName));
return parent::offsetExists($sName) ? parent::offsetGet($sName) : null; return parent::offsetExists($sName) ? parent::offsetGet($sName) : null;
} }
public function setParameter(string $sName, string $sValue): void public function setParameter(string $sName, string $sValue) : void
{ {
$oParam = $this->getParameter($sName); $oParam = $this->getParameter($sName);
if ($oParam) { if ($oParam) {
@ -52,7 +52,7 @@ class ParameterCollection extends \MailSo\Base\Collection
} }
} }
public function Parse(string $sRawParams): self public function Parse(string $sRawParams) : self
{ {
$this->Clear(); $this->Clear();
@ -67,7 +67,7 @@ class ParameterCollection extends \MailSo\Base\Collection
return $this; return $this;
} }
public function ToString(bool $bConvertSpecialsName = false): string public function ToString(bool $bConvertSpecialsName = false) : string
{ {
$aResult = array(); $aResult = array();
foreach ($this as $oParam) { foreach ($this as $oParam) {
@ -79,14 +79,14 @@ class ParameterCollection extends \MailSo\Base\Collection
return \count($aResult) ? \implode('; ', $aResult) : ''; return \count($aResult) ? \implode('; ', $aResult) : '';
} }
public function __toString(): string public function __toString() : string
{ {
return $this->ToString(); return $this->ToString();
} }
/* /*
// public function jsonSerialize(): array #[\ReturnTypeWillChange]
public function jsonSerialize(): mixed public function jsonSerialize()
{ {
$aResult = array(); $aResult = array();
foreach ($this as $oParam) { foreach ($this as $oParam) {
@ -99,7 +99,7 @@ class ParameterCollection extends \MailSo\Base\Collection
} }
*/ */
private function reParseParameters(): void private function reParseParameters() : void
{ {
$aDataToReParse = $this->getArrayCopy(); $aDataToReParse = $this->getArrayCopy();
$sCharset = \MailSo\Base\Enumerations\Charset::UTF_8; $sCharset = \MailSo\Base\Enumerations\Charset::UTF_8;

View file

@ -83,7 +83,7 @@ class ConnectSettings implements \JsonSerializable
} }
} }
public function fixUsername(string $value, bool $allowShorten = true): string public function fixUsername(string $value, bool $allowShorten = true) : string
{ {
$value = \SnappyMail\IDN::emailToAscii($value); $value = \SnappyMail\IDN::emailToAscii($value);
// $value = \SnappyMail\IDN::emailToAscii(\MailSo\Base\Utils::Trim($value)); // $value = \SnappyMail\IDN::emailToAscii(\MailSo\Base\Utils::Trim($value));
@ -104,7 +104,7 @@ class ConnectSettings implements \JsonSerializable
return $value; return $value;
} }
public static function fromArray(array $aSettings): self public static function fromArray(array $aSettings) : self
{ {
$object = new static; $object = new static;
$object->host = $aSettings['host']; $object->host = $aSettings['host'];
@ -128,8 +128,8 @@ class ConnectSettings implements \JsonSerializable
return $object; return $object;
} }
// public function jsonSerialize(): array #[\ReturnTypeWillChange]
public function jsonSerialize(): mixed public function jsonSerialize()
{ {
return array( return array(
// '@Object' => 'Object/ConnectSettings', // '@Object' => 'Object/ConnectSettings',

View file

@ -39,7 +39,7 @@ class SSLContext implements \JsonSerializable
public function __construct() public function __construct()
{ {
$oConfig = \RainLoop\Api::Config(); $oConfig = \RainLoop\API::Config();
$this->verify_peer = !!$oConfig->Get('ssl', 'verify_certificate', true); $this->verify_peer = !!$oConfig->Get('ssl', 'verify_certificate', true);
$this->verify_peer_name = !!$oConfig->Get('ssl', 'verify_certificate', true); $this->verify_peer_name = !!$oConfig->Get('ssl', 'verify_certificate', true);
$this->allow_self_signed = !!$oConfig->Get('ssl', 'allow_self_signed', false); $this->allow_self_signed = !!$oConfig->Get('ssl', 'allow_self_signed', false);
@ -50,7 +50,7 @@ class SSLContext implements \JsonSerializable
$this->local_cert = \trim($oConfig->Get('ssl', 'local_cert', '')); $this->local_cert = \trim($oConfig->Get('ssl', 'local_cert', ''));
} }
public static function fromArray(array $settings): self public static function fromArray(array $settings) : self
{ {
$object = new static; $object = new static;
foreach ($settings as $key => $value) { foreach ($settings as $key => $value) {
@ -59,8 +59,8 @@ class SSLContext implements \JsonSerializable
return $object; return $object;
} }
// public function jsonSerialize(): array #[\ReturnTypeWillChange]
public function jsonSerialize(): mixed public function jsonSerialize()
{ {
$aResult = \get_object_vars($this); $aResult = \get_object_vars($this);
// $aResult['@Object'] = 'Object/SSLContext'; // $aResult['@Object'] = 'Object/SSLContext';

View file

@ -23,7 +23,7 @@ class Settings extends \MailSo\Net\ConnectSettings
public bool $authLiteral = true; public bool $authLiteral = true;
public static function fromArray(array $aSettings): self public static function fromArray(array $aSettings) : self
{ {
$object = parent::fromArray($aSettings); $object = parent::fromArray($aSettings);
$object->enabled = !empty($aSettings['enabled']); $object->enabled = !empty($aSettings['enabled']);
@ -31,8 +31,8 @@ class Settings extends \MailSo\Net\ConnectSettings
return $object; return $object;
} }
// public function jsonSerialize(): array #[\ReturnTypeWillChange]
public function jsonSerialize(): mixed public function jsonSerialize()
{ {
return \array_merge( return \array_merge(
parent::jsonSerialize(), parent::jsonSerialize(),

View file

@ -33,11 +33,11 @@ class Settings extends \MailSo\Net\ConnectSettings
public function __construct() public function __construct()
{ {
parent::__construct(); parent::__construct();
$oConfig = \RainLoop\Api::Config(); $oConfig = \RainLoop\API::Config();
$this->viewErrors = !!$oConfig->Get('labs', 'smtp_show_server_errors', false); $this->viewErrors = !!$oConfig->Get('labs', 'smtp_show_server_errors', false);
} }
public static function fromArray(array $aSettings): self public static function fromArray(array $aSettings) : self
{ {
$object = parent::fromArray($aSettings); $object = parent::fromArray($aSettings);
$object->useAuth = !empty($aSettings['useAuth']); $object->useAuth = !empty($aSettings['useAuth']);
@ -48,8 +48,8 @@ class Settings extends \MailSo\Net\ConnectSettings
return $object; return $object;
} }
// public function jsonSerialize(): array #[\ReturnTypeWillChange]
public function jsonSerialize(): mixed public function jsonSerialize()
{ {
return \array_merge( return \array_merge(
parent::jsonSerialize(), parent::jsonSerialize(),

View file

@ -203,8 +203,7 @@ class SmtpClient extends \MailSo\Net\NetClient
$SASL->verify($sResult); $SASL->verify($sResult);
// Now end the authentication // Now end the authentication
$sRequest = ''; $sRequest = '';
$this->sendRaw($sRequest, true, ''); $this->sendRequestWithCheck($sRequest, 235);
$this->validateResponse(235, '');
} else switch ($type) { } else switch ($type) {
// RFC 4616 // RFC 4616
case 'PLAIN': case 'PLAIN':

View file

@ -167,7 +167,10 @@ class Actions
return $this->oConfig; return $this->oConfig;
} }
protected function fabrica(string $sName, ?Model\Account $oAccount = null): mixed /**
* @return mixed
*/
protected function fabrica(string $sName, ?Model\Account $oAccount = null)
{ {
$mResult = null; $mResult = null;
$this->oPlugins->RunHook('main.fabrica', array($sName, &$mResult), false); $this->oPlugins->RunHook('main.fabrica', array($sName, &$mResult), false);
@ -502,7 +505,7 @@ class Actions
return $this->oPlugins; return $this->oPlugins;
} }
public function LoggerAuthHelper(?Model\Account $oAccount, string $sLogin = '', bool $admin = false): void protected function LoggerAuthHelper(?Model\Account $oAccount, string $sLogin = '', bool $admin = false): void
{ {
if ($sLogin) { if ($sLogin) {
$sHost = $admin ? $this->Http()->GetHost(true, true) : \MailSo\Base\Utils::getEmailAddressDomain($sLogin); $sHost = $admin ? $this->Http()->GetHost(true, true) : \MailSo\Base\Utils::getEmailAddressDomain($sLogin);
@ -588,7 +591,6 @@ class Actions
'ListInlineAttachments' => false, 'ListInlineAttachments' => false,
'CollapseBlockquotes' => $oConfig->Get('defaults', 'collapse_blockquotes', true), 'CollapseBlockquotes' => $oConfig->Get('defaults', 'collapse_blockquotes', true),
'MaxBlockquotesLevel' => 0, 'MaxBlockquotesLevel' => 0,
'minFontSize' => 5,
'simpleAttachmentsList' => false, 'simpleAttachmentsList' => false,
'listGrouped' => $oConfig->Get('defaults', 'mail_list_grouped', false), 'listGrouped' => $oConfig->Get('defaults', 'mail_list_grouped', false),
'MessagesPerPage' => \max(10, \intval($oConfig->Get('webmail', 'messages_per_page', 25)) ?: 25), 'MessagesPerPage' => \max(10, \intval($oConfig->Get('webmail', 'messages_per_page', 25)) ?: 25),
@ -685,7 +687,6 @@ class Actions
$aResult['ListInlineAttachments'] = (bool)$oSettings->GetConf('ListInlineAttachments', $aResult['ListInlineAttachments']); $aResult['ListInlineAttachments'] = (bool)$oSettings->GetConf('ListInlineAttachments', $aResult['ListInlineAttachments']);
$aResult['CollapseBlockquotes'] = (bool)$oSettings->GetConf('CollapseBlockquotes', $aResult['CollapseBlockquotes']); $aResult['CollapseBlockquotes'] = (bool)$oSettings->GetConf('CollapseBlockquotes', $aResult['CollapseBlockquotes']);
$aResult['MaxBlockquotesLevel'] = (int)$oSettings->GetConf('MaxBlockquotesLevel', $aResult['MaxBlockquotesLevel']); $aResult['MaxBlockquotesLevel'] = (int)$oSettings->GetConf('MaxBlockquotesLevel', $aResult['MaxBlockquotesLevel']);
$aResult['minFontSize'] = (int)$oSettings->GetConf('minFontSize', $aResult['minFontSize']);
$aResult['simpleAttachmentsList'] = (bool)$oSettings->GetConf('simpleAttachmentsList', $aResult['simpleAttachmentsList']); $aResult['simpleAttachmentsList'] = (bool)$oSettings->GetConf('simpleAttachmentsList', $aResult['simpleAttachmentsList']);
$aResult['listGrouped'] = (bool)$oSettings->GetConf('listGrouped', $aResult['listGrouped']); $aResult['listGrouped'] = (bool)$oSettings->GetConf('listGrouped', $aResult['listGrouped']);
$aResult['ContactsAutosave'] = (bool)$oSettings->GetConf('ContactsAutosave', $aResult['ContactsAutosave']); $aResult['ContactsAutosave'] = (bool)$oSettings->GetConf('ContactsAutosave', $aResult['ContactsAutosave']);
@ -955,7 +956,12 @@ class Actions
return $this; return $this;
} }
public function GetActionParam(string $sKey, mixed $mDefault = null): mixed /**
* @param mixed $mDefault = null
*
* @return mixed
*/
public function GetActionParam(string $sKey, $mDefault = null)
{ {
return isset($this->aCurrentActionParams[$sKey]) ? return isset($this->aCurrentActionParams[$sKey]) ?
$this->aCurrentActionParams[$sKey] : $mDefault; $this->aCurrentActionParams[$sKey] : $mDefault;

View file

@ -210,7 +210,6 @@ trait Accounts
'mainEmail' => \RainLoop\Api::Actions()->getMainAccountFromToken()->Email(), 'mainEmail' => \RainLoop\Api::Actions()->getMainAccountFromToken()->Email(),
'contactsAllowed' => $this->AddressBookProvider($oAccount)->IsActive(), 'contactsAllowed' => $this->AddressBookProvider($oAccount)->IsActive(),
'HideUnsubscribed' => false, 'HideUnsubscribed' => false,
'defaultSort' => '',
'useThreads' => (bool) $oConfig->Get('defaults', 'mail_use_threads', false), 'useThreads' => (bool) $oConfig->Get('defaults', 'mail_use_threads', false),
'threadAlgorithm' => '', 'threadAlgorithm' => '',
'ReplySameFolder' => (bool) $oConfig->Get('defaults', 'mail_reply_same_folder', false), 'ReplySameFolder' => (bool) $oConfig->Get('defaults', 'mail_reply_same_folder', false),
@ -227,7 +226,6 @@ trait Accounts
$aResult['TrashFolder'] = (string) $oSettingsLocal->GetConf('TrashFolder', ''); $aResult['TrashFolder'] = (string) $oSettingsLocal->GetConf('TrashFolder', '');
$aResult['ArchiveFolder'] = (string) $oSettingsLocal->GetConf('ArchiveFolder', ''); $aResult['ArchiveFolder'] = (string) $oSettingsLocal->GetConf('ArchiveFolder', '');
$aResult['HideUnsubscribed'] = (bool) $oSettingsLocal->GetConf('HideUnsubscribed', $aResult['HideUnsubscribed']); $aResult['HideUnsubscribed'] = (bool) $oSettingsLocal->GetConf('HideUnsubscribed', $aResult['HideUnsubscribed']);
$aResult['defaultSort'] = (string) $oSettingsLocal->GetConf('defaultSort', $aResult['defaultSort']);
$aResult['useThreads'] = (bool) $oSettingsLocal->GetConf('UseThreads', $aResult['useThreads']); $aResult['useThreads'] = (bool) $oSettingsLocal->GetConf('UseThreads', $aResult['useThreads']);
$aResult['threadAlgorithm'] = (string) $oSettingsLocal->GetConf('threadAlgorithm', $aResult['threadAlgorithm']); $aResult['threadAlgorithm'] = (string) $oSettingsLocal->GetConf('threadAlgorithm', $aResult['threadAlgorithm']);
$aResult['ReplySameFolder'] = (bool) $oSettingsLocal->GetConf('ReplySameFolder', $aResult['ReplySameFolder']); $aResult['ReplySameFolder'] = (bool) $oSettingsLocal->GetConf('ReplySameFolder', $aResult['ReplySameFolder']);

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