mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-10-29 16:43:33 +03:00
Compare commits
1 commit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
208f1c7298 |
306 changed files with 7052 additions and 9905 deletions
|
|
@ -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; \
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
247
cli/compile.php
247
cli/compile.php
|
|
@ -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")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
2
dev/External/User/ko.js
vendored
2
dev/External/User/ko.js
vendored
|
|
@ -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
7
dev/External/ko.js
vendored
|
|
@ -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'] });
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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('@'))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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})` : '');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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']
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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());
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#V-PopupsLanguages {
|
#V-PopupsLanguages {
|
||||||
|
|
||||||
max-width: 720px;
|
max-width: 710px;
|
||||||
|
|
||||||
label {
|
label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,4 @@
|
||||||
content: ')';
|
content: ')';
|
||||||
}
|
}
|
||||||
|
|
||||||
.identities-list tr + tr .identity-default {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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('');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 && \
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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}'");
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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')) {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.')');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
|
||||||
|
|
@ -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')
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 = "Код підтвердження"
|
|
||||||
|
|
@ -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 = "代码"
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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':
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue