mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-09-02 22:17:03 +03:00
Merge branch 'upstream/master' into dev/sync-nextcloud-addressbook
This commit is contained in:
commit
957654c746
1528 changed files with 85567 additions and 32765 deletions
|
|
@ -1,51 +1,188 @@
|
||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
FROM alpine:3.18.5 AS builder
|
||||||
|
RUN apk add --no-cache php82 php82-json php-phar php-zip
|
||||||
|
RUN apk add --no-cache npm
|
||||||
|
RUN npm install -g gulp yarn
|
||||||
|
WORKDIR /source
|
||||||
|
COPY package.json yarn.lock ./
|
||||||
|
RUN yarn install
|
||||||
|
COPY . .
|
||||||
|
# Patch release.php with hotfix from: https://github.com/xgbstar1/snappymail-docker/blob/main/Dockerfile, so that release.php doesn't fail with error
|
||||||
|
RUN sed -i 's_^if.*rename.*snappymail.v.0.0.0.*$_if (!!system("mv snappymail/v/0.0.0 snappymail/v/{$package->version}")) {_' cli/release.php || true
|
||||||
|
RUN php release.php
|
||||||
|
RUN set -eux; \
|
||||||
|
VERSION=$( ls build/dist/releases/webmail ); \
|
||||||
|
ls -al build/dist/releases/webmail/$VERSION/snappymail-$VERSION.tar.gz; \
|
||||||
|
mkdir -p /snappymail; \
|
||||||
|
tar -zxvf build/dist/releases/webmail/$VERSION/snappymail-$VERSION.tar.gz -C /snappymail; \
|
||||||
|
find /snappymail -type d -exec chmod 550 {} \; ; \
|
||||||
|
find /snappymail -type f -exec chmod 440 {} \; ; \
|
||||||
|
find /snappymail/data -type d -exec chmod 750 {} \; ; \
|
||||||
|
# Remove unneeded files
|
||||||
|
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.1-fpm-bullseye
|
FROM php:8.2-fpm-alpine AS final
|
||||||
|
|
||||||
ARG FILES_ZIP
|
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 based on Debian Buster"
|
|
||||||
|
|
||||||
ENV UID=991 GID=991 UPLOAD_MAX_SIZE=25M LOG_TO_STDERR=true MEMORY_LIMIT=128M SECURE_COOKIES=true
|
|
||||||
ENV fpm.pool.clear_env=false
|
|
||||||
|
|
||||||
# Install dependencies such as nginx
|
# Install dependencies such as nginx
|
||||||
RUN mkdir -p /usr/share/man/man1/ /usr/share/man/man3/ /usr/share/man/man7/ && \
|
RUN apk add --no-cache ca-certificates nginx supervisor bash
|
||||||
apt-get update -q --fix-missing && \
|
|
||||||
apt-get -y upgrade && \
|
|
||||||
apt-get install --no-install-recommends -y \
|
|
||||||
apt-transport-https gnupg openssl wget curl ca-certificates nginx supervisor sudo \
|
|
||||||
unzip libzip-dev libxml2-dev libldb-dev libldap2-dev \
|
|
||||||
sqlite3 libsqlite3-dev libsqlite3-0 libpq-dev postgresql-client mariadb-client logrotate \
|
|
||||||
zip mlocate libpcre3-dev libicu-dev \
|
|
||||||
build-essential chrpath libssl-dev \
|
|
||||||
libxft-dev libfreetype6 libfreetype6-dev \
|
|
||||||
libpng-dev libjpeg62-turbo-dev \
|
|
||||||
libfontconfig1 libfontconfig1-dev \
|
|
||||||
&& \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install PHP extensions
|
# Install PHP extensions
|
||||||
RUN php -m && \
|
# apcu
|
||||||
docker-php-ext-configure ldap --with-libdir=lib/$(uname -m)-linux-gnu/ && \
|
RUN set -eux; \
|
||||||
docker-php-ext-configure intl && \
|
apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
|
||||||
docker-php-ext-configure gd --with-freetype --with-jpeg && \
|
pecl install apcu; \
|
||||||
docker-php-ext-install ldap opcache pdo_mysql pdo_pgsql zip intl gd && \
|
docker-php-ext-enable apcu; \
|
||||||
php -m
|
docker-php-source delete; \
|
||||||
|
apk del .build-dependencies;
|
||||||
|
|
||||||
|
# gd
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache freetype libjpeg-turbo libpng; \
|
||||||
|
apk add --no-cache --virtual .deps freetype-dev libjpeg-turbo-dev libpng-dev; \
|
||||||
|
docker-php-ext-configure gd --with-freetype --with-jpeg; \
|
||||||
|
docker-php-ext-install gd; \
|
||||||
|
apk del .deps
|
||||||
|
|
||||||
|
# gmagick
|
||||||
|
# RUN set -eux; \
|
||||||
|
# apk add --no-cache graphicsmagick libgomp; \
|
||||||
|
# apk add --no-cache --virtual .deps graphicsmagick-dev libtool; \
|
||||||
|
# apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
|
||||||
|
# pecl install gmagick-2.0.6RC1; \
|
||||||
|
# docker-php-ext-enable gmagick; \
|
||||||
|
# docker-php-source delete; \
|
||||||
|
# apk del .build-dependencies; \
|
||||||
|
# apk del .deps
|
||||||
|
|
||||||
|
# gnupg
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache gnupg gpgme; \
|
||||||
|
apk add --no-cache --virtual .deps gpgme-dev; \
|
||||||
|
apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
|
||||||
|
pecl install gnupg; \
|
||||||
|
docker-php-ext-enable gnupg; \
|
||||||
|
docker-php-source delete; \
|
||||||
|
apk del .build-dependencies; \
|
||||||
|
apk del .deps
|
||||||
|
|
||||||
|
# imagick
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache imagemagick libgomp; \
|
||||||
|
apk add --no-cache --virtual .deps imagemagick-dev; \
|
||||||
|
apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
|
||||||
|
echo | pecl install imagick; \
|
||||||
|
docker-php-ext-enable imagick; \
|
||||||
|
docker-php-source delete; \
|
||||||
|
apk del .build-dependencies; \
|
||||||
|
apk del .deps
|
||||||
|
|
||||||
|
# intl
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache icu-libs; \
|
||||||
|
apk add --no-cache --virtual .deps icu-dev; \
|
||||||
|
docker-php-ext-configure intl; \
|
||||||
|
docker-php-ext-install intl; \
|
||||||
|
apk del .deps
|
||||||
|
|
||||||
|
# ldap
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache libldap; \
|
||||||
|
apk add --no-cache --virtual .deps openldap-dev; \
|
||||||
|
docker-php-ext-configure ldap; \
|
||||||
|
docker-php-ext-install ldap; \
|
||||||
|
apk del .deps
|
||||||
|
|
||||||
|
# mysql
|
||||||
|
RUN docker-php-ext-install pdo_mysql
|
||||||
|
|
||||||
|
# opcache
|
||||||
|
RUN docker-php-ext-install opcache
|
||||||
|
|
||||||
|
# postgres
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache postgresql-libs; \
|
||||||
|
apk add --no-cache --virtual .deps postgresql-dev; \
|
||||||
|
docker-php-ext-install pdo_pgsql; \
|
||||||
|
apk del .deps
|
||||||
|
|
||||||
|
# redis
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache liblzf zstd-libs; \
|
||||||
|
apk add --no-cache --virtual .deps zstd-dev; \
|
||||||
|
apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
|
||||||
|
pecl install igbinary; \
|
||||||
|
docker-php-ext-enable igbinary; \
|
||||||
|
pecl install --configureoptions 'enable-redis-igbinary="yes" enable-redis-lzf="yes" enable-redis-zstd="yes"' redis; \
|
||||||
|
docker-php-ext-enable redis; \
|
||||||
|
docker-php-source delete; \
|
||||||
|
apk del .build-dependencies; \
|
||||||
|
apk del .deps
|
||||||
|
|
||||||
|
# tidy
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache tidyhtml; \
|
||||||
|
apk add --no-cache --virtual .deps tidyhtml-dev; \
|
||||||
|
docker-php-ext-install tidy; \
|
||||||
|
apk del .deps
|
||||||
|
|
||||||
|
# uuid
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache libuuid; \
|
||||||
|
apk add --no-cache --virtual .deps util-linux-dev; \
|
||||||
|
apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
|
||||||
|
pecl install uuid; \
|
||||||
|
docker-php-ext-enable uuid; \
|
||||||
|
docker-php-source delete; \
|
||||||
|
apk del .build-dependencies; \
|
||||||
|
apk del .deps
|
||||||
|
|
||||||
|
# xxtea - Manually install php8 compatible version from https://github.com/xxtea/xxtea-pecl master branch
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
|
||||||
|
wget -q https://github.com/xxtea/xxtea-pecl/tarball/3f5888a29045e12301254151737c5dab4523a1c1 -O xxtea.tar; \
|
||||||
|
echo '9cbfd9c27255767deb26ddedf69e738d401d88ac9762d82c8510f9768842ca18 xxtea.tar' | sha256sum -c -; \
|
||||||
|
tar -C /usr/src -xvf xxtea.tar; \
|
||||||
|
cd /usr/src/xxtea-xxtea-pecl-3f5888a; \
|
||||||
|
phpize; \
|
||||||
|
./configure --with-php-config=/usr/local/bin/php-config --enable-xxtea=yes; \
|
||||||
|
make install; \
|
||||||
|
docker-php-ext-enable xxtea; \
|
||||||
|
cd -; \
|
||||||
|
rm -fv xxtea.tar; \
|
||||||
|
rm -rfv /usr/src/xxtea*; \
|
||||||
|
apk del .build-dependencies;
|
||||||
|
|
||||||
|
# zip
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache libzip; \
|
||||||
|
apk add --no-cache --virtual .deps libzip-dev; \
|
||||||
|
docker-php-ext-install zip; \
|
||||||
|
apk del .deps
|
||||||
|
|
||||||
# Install snappymail
|
# Install snappymail
|
||||||
WORKDIR /tmp
|
# The 'www-data' user/group in alpine is 82:82. The 'nginx' user/group in alpine is 101:101, and is part of www-data group
|
||||||
COPY ${FILES_ZIP} .
|
COPY --chown=www-data:www-data --from=builder /snappymail /snappymail
|
||||||
RUN mkdir /snappymail && \
|
# Use a custom snappymail data folder
|
||||||
unzip -q ${FILES_ZIP} -d /snappymail && \
|
RUN mv -v /snappymail/data /var/lib/snappymail;
|
||||||
find /snappymail -type d -exec chmod 755 {} \; && \
|
# Setup configs
|
||||||
find /snappymail -type f -exec chmod 644 {} \; && \
|
COPY --chown=root:root .docker/release/files /
|
||||||
rm -rf ${FILES_ZIP}
|
RUN set -eux; \
|
||||||
|
chown www-data:www-data /snappymail/include.php; \
|
||||||
|
chmod 440 /snappymail/include.php; \
|
||||||
|
chmod +x /entrypoint.sh; \
|
||||||
|
# Disable the built-in php-fpm configs, since we're using our own config
|
||||||
|
mv -v /usr/local/etc/php-fpm.d/docker.conf /usr/local/etc/php-fpm.d/docker.conf.disabled; \
|
||||||
|
mv -v /usr/local/etc/php-fpm.d/www.conf /usr/local/etc/php-fpm.d/www.conf.disabled; \
|
||||||
|
mv -v /usr/local/etc/php-fpm.d/zz-docker.conf /usr/local/etc/php-fpm.d/zz-docker.conf.disabled;
|
||||||
|
|
||||||
# Install other content
|
USER root
|
||||||
COPY files /
|
WORKDIR /snappymail
|
||||||
RUN chmod +x /entrypoint.sh && chmod +x /logrotate-loop.sh
|
VOLUME /var/lib/snappymail
|
||||||
VOLUME /snappymail/data
|
|
||||||
EXPOSE 8888
|
EXPOSE 8888
|
||||||
|
EXPOSE 9000
|
||||||
|
ENTRYPOINT []
|
||||||
CMD ["/entrypoint.sh"]
|
CMD ["/entrypoint.sh"]
|
||||||
|
|
|
||||||
92
.docker/release/files/entrypoint.sh
Normal file → Executable file
92
.docker/release/files/entrypoint.sh
Normal file → Executable file
|
|
@ -1,23 +1,20 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
# Create not root user
|
DEBUG=${DEBUG:-}
|
||||||
groupadd --gid "$GID" php-cli -f
|
if [ "$DEBUG" = 'true' ]; then
|
||||||
adduser --uid "$UID" --disabled-password --gid "$GID" --shell /bin/bash --home /home/php-cli php-cli --force --gecos ""
|
set -x
|
||||||
|
fi
|
||||||
|
UPLOAD_MAX_SIZE=${UPLOAD_MAX_SIZE:-25M}
|
||||||
|
MEMORY_LIMIT=${MEMORY_LIMIT:-128M}
|
||||||
|
SECURE_COOKIES=${SECURE_COOKIES:-true}
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
# Set log output to STDERR if wanted (LOG_TO_STDERR=true)
|
|
||||||
if [ "$LOG_TO_STDERR" = true ]; then
|
|
||||||
echo "[INFO] Logging to stderr activated"
|
|
||||||
sed -i "s/.*error_log.*$/error_log \/dev\/stderr warn;/" /etc/nginx/nginx.conf
|
|
||||||
sed -i "s/.*error_log.*$/php_admin_value[error_log] = \/dev\/stderr/" /usr/local/etc/php-fpm.d/php-fpm.conf
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Secure cookies
|
# Secure cookies
|
||||||
if [ "${SECURE_COOKIES}" = true ]; then
|
if [ "${SECURE_COOKIES}" = 'true' ]; then
|
||||||
echo "[INFO] Secure cookies activated"
|
echo "[INFO] Secure cookies activated"
|
||||||
{
|
{
|
||||||
echo 'session.cookie_httponly = On';
|
echo 'session.cookie_httponly = On';
|
||||||
|
|
@ -26,43 +23,58 @@ if [ "${SECURE_COOKIES}" = true ]; then
|
||||||
} > /usr/local/etc/php/conf.d/cookies.ini;
|
} > /usr/local/etc/php/conf.d/cookies.ini;
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Copy snappymail default config if absent
|
echo "[INFO] Snappymail version: $( ls /snappymail/snappymail/v )"
|
||||||
SNAPPYMAIL_CONFIG_FILE=/snappymail/data/_data_/_default_/configs/application.ini
|
|
||||||
|
# Set permissions on snappymail data
|
||||||
|
echo "[INFO] Setting permissions on /var/lib/snappymail"
|
||||||
|
chown -R www-data:www-data /var/lib/snappymail/
|
||||||
|
chmod 550 /var/lib/snappymail/
|
||||||
|
find /var/lib/snappymail/ -type d -exec chmod 750 {} \;
|
||||||
|
|
||||||
|
# Create snappymail default config if absent
|
||||||
|
SNAPPYMAIL_CONFIG_FILE=/var/lib/snappymail/_data_/_default_/configs/application.ini
|
||||||
if [ ! -f "$SNAPPYMAIL_CONFIG_FILE" ]; then
|
if [ ! -f "$SNAPPYMAIL_CONFIG_FILE" ]; then
|
||||||
echo "[INFO] Creating default Snappymail configuration"
|
echo "[INFO] Creating default Snappymail configuration: $SNAPPYMAIL_CONFIG_FILE"
|
||||||
mkdir -p $(dirname $SNAPPYMAIL_CONFIG_FILE)
|
# Run snappymail and exit. This populates the snappymail data directory and generates the config file
|
||||||
cp /usr/local/include/application.ini $SNAPPYMAIL_CONFIG_FILE
|
# On error, print php exception and exit
|
||||||
|
EXITCODE=
|
||||||
|
su - www-data -s /bin/sh -c 'php /snappymail/index.php' > /tmp/out || EXITCODE=$?
|
||||||
|
if [ -n "$EXITCODE" ]; then
|
||||||
|
cat /tmp/out
|
||||||
|
exit "$EXITCODE"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "[INFO] Overriding values in snappymail configuration: $SNAPPYMAIL_CONFIG_FILE"
|
||||||
# Enable output of snappymail logs
|
# Enable output of snappymail logs
|
||||||
if [ "${LOG_TO_STDERR}" = true ]; then
|
sed '/^\; Enable logging/{
|
||||||
sed -z 's/\; Enable logging\nenable = Off/\; Enable logging\nenable = On/' -i $SNAPPYMAIL_CONFIG_FILE
|
N
|
||||||
sed 's/^filename = .*/filename = "errors.log"/' -i $SNAPPYMAIL_CONFIG_FILE
|
s/enable = Off/enable = On/
|
||||||
sed 's/^write_on_error_only = .*/write_on_error_only = Off/' -i $SNAPPYMAIL_CONFIG_FILE
|
}' -i $SNAPPYMAIL_CONFIG_FILE
|
||||||
sed 's/^write_on_php_error_only = .*/write_on_php_error_only = On/' -i $SNAPPYMAIL_CONFIG_FILE
|
# Redirect snappymail logs to stderr /stdout
|
||||||
else
|
sed 's/^filename = .*/filename = "stderr"/' -i $SNAPPYMAIL_CONFIG_FILE
|
||||||
sed -z 's/\; Enable logging\nenable = On/\; Enable logging\nenable = Off/' -i $SNAPPYMAIL_CONFIG_FILE
|
sed 's/^write_on_error_only = .*/write_on_error_only = Off/' -i $SNAPPYMAIL_CONFIG_FILE
|
||||||
fi
|
sed 's/^write_on_php_error_only = .*/write_on_php_error_only = On/' -i $SNAPPYMAIL_CONFIG_FILE
|
||||||
# Always enable snappymail Auth logging
|
# Always enable snappymail Auth logging
|
||||||
sed 's/^auth_logging = .*/auth_logging = On/' -i $SNAPPYMAIL_CONFIG_FILE
|
sed 's/^auth_logging = .*/auth_logging = On/' -i $SNAPPYMAIL_CONFIG_FILE
|
||||||
sed 's/^auth_logging_filename = .*/auth_logging_filename = "auth.log"/' -i $SNAPPYMAIL_CONFIG_FILE
|
sed 's/^auth_logging_filename = .*/auth_logging_filename = "auth.log"/' -i $SNAPPYMAIL_CONFIG_FILE
|
||||||
sed 's/^auth_logging_format = .*/auth_logging_format = "[{date:Y-m-d H:i:s}] Auth failed: ip={request:ip} user={imap:login} host={imap:host} port={imap:port}"/' -i $SNAPPYMAIL_CONFIG_FILE
|
sed 's/^auth_logging_format = .*/auth_logging_format = "[{date:Y-m-d H:i:s}] Auth failed: ip={request:ip} user={imap:login} host={imap:host} port={imap:port}"/' -i $SNAPPYMAIL_CONFIG_FILE
|
||||||
# Redirect snappymail logs to stderr /stdout
|
sed 's/^auth_syslog = .*/auth_syslog = Off/' -i $SNAPPYMAIL_CONFIG_FILE
|
||||||
mkdir -p /snappymail/data/_data_/_default_/logs/
|
|
||||||
# empty logs
|
|
||||||
cp /dev/null /snappymail/data/_data_/_default_/logs/errors.log
|
|
||||||
cp /dev/null /snappymail/data/_data_/_default_/logs/auth.log
|
|
||||||
chown -R php-cli:php-cli /snappymail/data/
|
|
||||||
|
|
||||||
# Fix permissions
|
(
|
||||||
chown -R $UID:$GID /snappymail/data /var/log /var/lib/nginx
|
while ! nc -vz -w 1 127.0.0.1 8888 > /dev/null 2>&1; do echo "[INFO] Checking whether nginx is alive"; sleep 1; done
|
||||||
chmod o+w /dev/stdout
|
while ! nc -vz -w 1 127.0.0.1 9000 > /dev/null 2>&1; do echo "[INFO] Checking whether php-fpm is alive"; sleep 1; done
|
||||||
chmod o+w /dev/stderr
|
# Create snappymail admin password if absent
|
||||||
|
SNAPPYMAIL_ADMIN_PASSWORD_FILE=/var/lib/snappymail/_data_/_default_/admin_password.txt
|
||||||
|
if [ ! -f "$SNAPPYMAIL_ADMIN_PASSWORD_FILE" ]; then
|
||||||
|
echo "[INFO] Creating Snappymail admin password file: $SNAPPYMAIL_ADMIN_PASSWORD_FILE"
|
||||||
|
wget -T 1 -qO- 'http://127.0.0.1:8888/?/AdminAppData/0/12345/' > /dev/null
|
||||||
|
echo "[INFO] Snappymail Admin Panel ready at http://localhost:8888/?admin. Login using password in $SNAPPYMAIL_ADMIN_PASSWORD_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
wget -T 1 -qO- 'http://127.0.0.1:8888/' > /dev/null
|
||||||
# Touch supervisord PID file in order to fix permissions
|
echo "[INFO] Snappymail ready at http://localhost:8888/"
|
||||||
touch /run/supervisord.pid
|
) &
|
||||||
chown php-cli:php-cli /run/supervisord.pid
|
|
||||||
|
|
||||||
# RUN !
|
# RUN !
|
||||||
exec sudo -u php-cli -g php-cli /usr/bin/supervisord -c '/supervisor.conf' --pidfile '/run/supervisord.pid'
|
exec /usr/bin/supervisord -c /supervisor.conf --pidfile /run/supervisord.pid
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
/snappymail/data/_data_/_default_/logs/* {
|
|
||||||
size 10M
|
|
||||||
rotate 0
|
|
||||||
missingok
|
|
||||||
}
|
|
||||||
|
|
@ -11,7 +11,7 @@ http {
|
||||||
default_type application/octet-stream;
|
default_type application/octet-stream;
|
||||||
|
|
||||||
access_log off;
|
access_log off;
|
||||||
error_log /tmp/ngx_error.log error;
|
error_log /dev/stderr error;
|
||||||
|
|
||||||
sendfile on;
|
sendfile on;
|
||||||
keepalive_timeout 15;
|
keepalive_timeout 15;
|
||||||
|
|
@ -95,7 +95,7 @@ http {
|
||||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||||
fastcgi_param HTTP_PROXY "";
|
fastcgi_param HTTP_PROXY "";
|
||||||
fastcgi_index index.php;
|
fastcgi_index index.php;
|
||||||
fastcgi_pass unix:/tmp/php-fpm.sock;
|
fastcgi_pass 127.0.0.1:9000;
|
||||||
fastcgi_intercept_errors on;
|
fastcgi_intercept_errors on;
|
||||||
fastcgi_request_buffering off;
|
fastcgi_request_buffering off;
|
||||||
fastcgi_param REMOTE_ADDR $http_x_real_ip;
|
fastcgi_param REMOTE_ADDR $http_x_real_ip;
|
||||||
|
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# Simple script to invoke logrotate at regular intervals
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
# from https://github.com/misho-kr/docker-appliances/blob/master/nginx-nodejs/logrotate-loop.sh
|
|
||||||
|
|
||||||
LOGROTATE_BIN="logrotate"
|
|
||||||
|
|
||||||
STATE="$HOME/logrotate.state"
|
|
||||||
CONF="/etc/logrotate.d/snappymail"
|
|
||||||
|
|
||||||
export LOGROTATE_BIN STATE CONF
|
|
||||||
|
|
||||||
RUN_INTERVAL="3600" # every hour
|
|
||||||
|
|
||||||
# helper functions for logging
|
|
||||||
export FMT="%a %b %d %Y %H:%M:%S GMT%z (%Z)"
|
|
||||||
|
|
||||||
function log_date() {
|
|
||||||
echo "$(date +"$FMT"): $*"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# Main loop of the logrotate service:
|
|
||||||
#
|
|
||||||
# while True:
|
|
||||||
# sleep N seconds
|
|
||||||
# run logrotate
|
|
||||||
#
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
function logrotate_loop() {
|
|
||||||
|
|
||||||
trap on_terminate TERM INT
|
|
||||||
|
|
||||||
local interval="${1}"
|
|
||||||
|
|
||||||
log_date "===================================================="
|
|
||||||
log_date
|
|
||||||
log_date "logrotate service starting (pid=$$)"
|
|
||||||
log_date "logrotate process will run every ${interval} seconds"
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
|
|
||||||
current_time=$(date "+%s")
|
|
||||||
next_run_time=$(( current_time + interval ))
|
|
||||||
|
|
||||||
while (( current_time < next_run_time ))
|
|
||||||
do
|
|
||||||
logrotate_sleep $(( next_run_time - current_time ))
|
|
||||||
current_time=$(date "+%s")
|
|
||||||
done
|
|
||||||
|
|
||||||
logrotate_run
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
# helper function to execute logrotate and pass it the right parameters
|
|
||||||
function logrotate_run() {
|
|
||||||
|
|
||||||
log_date "logrotate will run now"
|
|
||||||
${LOGROTATE_BIN} -s ${STATE} ${CONF}
|
|
||||||
}
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# Procedure to idle the execution for a number of seconds
|
|
||||||
#
|
|
||||||
# There are two requirements:
|
|
||||||
#
|
|
||||||
# - export the PID of the sleep command so that it can be terminated
|
|
||||||
# in case the logrotate service is being shutdown
|
|
||||||
# - keep this (bash) process responsive to SIGTERM while in sleep
|
|
||||||
# mode (normally the signal will be masked and will not be delivered
|
|
||||||
# until the subprocess completes)
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
proc_sleep_pid=""
|
|
||||||
|
|
||||||
function logrotate_sleep() {
|
|
||||||
|
|
||||||
local sleep_interval=${1}
|
|
||||||
|
|
||||||
log_date "logrotate will sleep for ${sleep_interval} seconds"
|
|
||||||
|
|
||||||
( exec -a "logrotate: sleep" sleep ${sleep_interval} )&
|
|
||||||
|
|
||||||
proc_sleep_pid=$!
|
|
||||||
wait ${proc_sleep_pid}
|
|
||||||
}
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# Signal handler for logrotate service to make sure the process exits:
|
|
||||||
#
|
|
||||||
# - properly by terminating the sleep process that is used to idle
|
|
||||||
# the service
|
|
||||||
# - gracefully by writing a message in the log
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
function on_terminate() {
|
|
||||||
|
|
||||||
log_date "logrotate will terminate"
|
|
||||||
log_date
|
|
||||||
|
|
||||||
kill -TERM ${proc_sleep_pid}
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# main
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
logrotate_loop ${1:-RUN_INTERVAL}
|
|
||||||
3
.docker/release/files/snappymail/include.php
Normal file
3
.docker/release/files/snappymail/include.php
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<?php
|
||||||
|
define('APP_DATA_FOLDER_PATH', '/var/lib/snappymail/');
|
||||||
|
?>
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
[supervisord]
|
[supervisord]
|
||||||
nodaemon=true
|
nodaemon=true
|
||||||
|
user=root
|
||||||
|
logfile=/dev/null
|
||||||
|
logfile_maxbytes=0
|
||||||
|
|
||||||
[program:nginx]
|
[program:nginx]
|
||||||
command=nginx -c /etc/nginx/nginx.conf -g 'daemon off;'
|
command=nginx -c /etc/nginx/nginx.conf -g 'daemon off;'
|
||||||
process_name=%(program_name)s_%(process_num)02d
|
process_name=%(program_name)s_%(process_num)02d
|
||||||
user=php-cli
|
user=root
|
||||||
numprocs=1
|
numprocs=1
|
||||||
autostart=true
|
autostart=true
|
||||||
autorestart=false
|
autorestart=false
|
||||||
|
|
@ -17,7 +20,7 @@ stderr_logfile_maxbytes=0
|
||||||
[program:php-fpm]
|
[program:php-fpm]
|
||||||
command=php-fpm -F
|
command=php-fpm -F
|
||||||
process_name=%(program_name)s_%(process_num)02d
|
process_name=%(program_name)s_%(process_num)02d
|
||||||
user=php-cli
|
user=root
|
||||||
numprocs=1
|
numprocs=1
|
||||||
autostart=true
|
autostart=true
|
||||||
autorestart=false
|
autorestart=false
|
||||||
|
|
@ -27,34 +30,11 @@ stdout_logfile_maxbytes=0
|
||||||
stderr_logfile=/dev/stderr
|
stderr_logfile=/dev/stderr
|
||||||
stderr_logfile_maxbytes=0
|
stderr_logfile_maxbytes=0
|
||||||
|
|
||||||
; reads snappymail logs
|
|
||||||
[program:snappymail-auth]
|
|
||||||
command=tail -f /snappymail/data/_data_/_default_/logs/auth.log
|
|
||||||
stdout_logfile=/dev/stdout
|
|
||||||
stdout_logfile_maxbytes=0
|
|
||||||
stderr_logfile=/dev/stderr
|
|
||||||
stderr_logfile_maxbytes=0
|
|
||||||
|
|
||||||
[program:snappymail-errors]
|
|
||||||
command=tail -f /snappymail/data/_data_/_default_/logs/errors.log
|
|
||||||
# everything is an error
|
|
||||||
stdout_logfile=/dev/stderr
|
|
||||||
stdout_logfile_maxbytes=0
|
|
||||||
redirect_stderr=true
|
|
||||||
|
|
||||||
[program:logrotate]
|
|
||||||
command=/logrotate-loop.sh
|
|
||||||
autorestart=true
|
|
||||||
stdout_logfile=/dev/stdout
|
|
||||||
stdout_logfile_maxbytes=0
|
|
||||||
stderr_logfile=/dev/stderr
|
|
||||||
stderr_logfile_maxbytes=0
|
|
||||||
|
|
||||||
[eventlistener:subprocess-stopped]
|
[eventlistener:subprocess-stopped]
|
||||||
command=php /listener.php
|
command=php /listener.php
|
||||||
process_name=%(program_name)s_%(process_num)02d
|
process_name=%(program_name)s_%(process_num)02d
|
||||||
user=php-cli
|
user=root
|
||||||
numprocs=1
|
numprocs=1
|
||||||
events=PROCESS_STATE_EXITED,PROCESS_STATE_STOPPED,PROCESS_STATE_FATAL
|
events=PROCESS_STATE_EXITED,PROCESS_STATE_STOPPED,PROCESS_STATE_FATAL
|
||||||
autostart=true
|
autostart=true
|
||||||
autorestart=unexpected
|
autorestart=unexpected
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,21 @@
|
||||||
[global]
|
[global]
|
||||||
daemonize = no
|
daemonize = no
|
||||||
|
error_log = /dev/stderr
|
||||||
|
log_buffering = no
|
||||||
|
|
||||||
[default]
|
[default]
|
||||||
listen = /tmp/php-fpm.sock
|
listen = 9000
|
||||||
|
user = www-data
|
||||||
|
listen.owner = www-data
|
||||||
|
listen.group = www-data
|
||||||
pm = ondemand
|
pm = ondemand
|
||||||
pm.max_children = 30
|
pm.max_children = 30
|
||||||
pm.process_idle_timeout = 10s
|
pm.process_idle_timeout = 10s
|
||||||
pm.max_requests = 500
|
pm.max_requests = 500
|
||||||
catch_workers_output = yes
|
catch_workers_output = yes
|
||||||
|
decorate_workers_output = no
|
||||||
chdir = /
|
chdir = /
|
||||||
php_admin_value[error_log] = /tmp/php_error.log
|
pm.status_path = /status
|
||||||
php_admin_value[log_errors] = On
|
php_admin_value[log_errors] = On
|
||||||
php_admin_value[expose_php] = Off
|
php_admin_value[expose_php] = Off
|
||||||
php_admin_value[display_errors] = Off
|
php_admin_value[display_errors] = Off
|
||||||
|
|
|
||||||
|
|
@ -1,320 +0,0 @@
|
||||||
; SnappyMail configuration file
|
|
||||||
; Please don't add custom parameters here, those will be overwritten
|
|
||||||
|
|
||||||
[webmail]
|
|
||||||
; Text displayed as page title
|
|
||||||
title = "SnappyMail Webmail"
|
|
||||||
|
|
||||||
; Text displayed on startup
|
|
||||||
loading_description = "SnappyMail"
|
|
||||||
favicon_url = ""
|
|
||||||
app_path = ""
|
|
||||||
|
|
||||||
; Theme used by default
|
|
||||||
theme = "Default"
|
|
||||||
|
|
||||||
; Allow theme selection on settings screen
|
|
||||||
allow_themes = On
|
|
||||||
allow_user_background = Off
|
|
||||||
|
|
||||||
; Language used by default
|
|
||||||
language = "en"
|
|
||||||
|
|
||||||
; Admin Panel interface language
|
|
||||||
language_admin = "en"
|
|
||||||
|
|
||||||
; Allow language selection on settings screen
|
|
||||||
allow_languages_on_settings = On
|
|
||||||
allow_additional_accounts = On
|
|
||||||
allow_additional_identities = On
|
|
||||||
|
|
||||||
; Number of messages displayed on page by default
|
|
||||||
messages_per_page = 20
|
|
||||||
|
|
||||||
; Mark message read after N seconds
|
|
||||||
message_read_delay = 5
|
|
||||||
|
|
||||||
; File size limit (MB) for file upload on compose screen
|
|
||||||
; 0 for unlimited.
|
|
||||||
attachment_size_limit = 2
|
|
||||||
|
|
||||||
[interface]
|
|
||||||
show_attachment_thumbnail = On
|
|
||||||
|
|
||||||
[contacts]
|
|
||||||
; Enable contacts
|
|
||||||
enable = Off
|
|
||||||
allow_sync = Off
|
|
||||||
sync_interval = 20
|
|
||||||
type = "sqlite"
|
|
||||||
pdo_dsn = "host=127.0.0.1;port=3306;dbname=snappymail"
|
|
||||||
pdo_user = "root"
|
|
||||||
pdo_password = ""
|
|
||||||
suggestions_limit = 30
|
|
||||||
|
|
||||||
[security]
|
|
||||||
custom_server_signature = "SnappyMail"
|
|
||||||
x_xss_protection_header = "1; mode=block"
|
|
||||||
openpgp = Off
|
|
||||||
|
|
||||||
; Access settings
|
|
||||||
allow_admin_panel = On
|
|
||||||
|
|
||||||
; Login and password for web admin panel
|
|
||||||
admin_login = "admin"
|
|
||||||
admin_password = ""
|
|
||||||
admin_totp = ""
|
|
||||||
admin_panel_host = ""
|
|
||||||
admin_panel_key = "admin"
|
|
||||||
force_https = Off
|
|
||||||
hide_x_mailer_header = On
|
|
||||||
|
|
||||||
; For example to allow all images use "img-src https:". More info at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#directives
|
|
||||||
content_security_policy = ""
|
|
||||||
|
|
||||||
; Report CSP errors to PHP and/or SnappyMail Log
|
|
||||||
csp_report = Off
|
|
||||||
|
|
||||||
; A valid cipher method from https://php.net/openssl_get_cipher_methods
|
|
||||||
encrypt_cipher = "aes-256-cbc-hmac-sha1"
|
|
||||||
|
|
||||||
; Strict, Lax or None
|
|
||||||
cookie_samesite = "Strict"
|
|
||||||
|
|
||||||
; Additional allowed Sec-Fetch combinations separated by ";".
|
|
||||||
; For example:
|
|
||||||
; * Allow iframe on same domain in any mode: dest=iframe,site=same-origin
|
|
||||||
; * Allow navigate to iframe on same domain: mode=navigate,dest=iframe,site=same-origin
|
|
||||||
; * Allow navigate to iframe on (sub)domain: mode=navigate,dest=iframe,site=same-site
|
|
||||||
; * Allow navigate to iframe from any domain: mode=navigate,dest=iframe,site=cross-site
|
|
||||||
;
|
|
||||||
; Default is "site=same-origin;site=none"
|
|
||||||
secfetch_allow = ""
|
|
||||||
|
|
||||||
[admin_panel]
|
|
||||||
allow_update = Off
|
|
||||||
|
|
||||||
[ssl]
|
|
||||||
; Require verification of SSL certificate used.
|
|
||||||
verify_certificate = Off
|
|
||||||
|
|
||||||
; Allow self-signed certificates. Requires verify_certificate.
|
|
||||||
allow_self_signed = On
|
|
||||||
|
|
||||||
; https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_security_level.html
|
|
||||||
security_level = 1
|
|
||||||
|
|
||||||
; Location of Certificate Authority file on local filesystem (/etc/ssl/certs/ca-certificates.crt)
|
|
||||||
cafile = ""
|
|
||||||
|
|
||||||
; capath must be a correctly hashed certificate directory. (/etc/ssl/certs/)
|
|
||||||
capath = ""
|
|
||||||
|
|
||||||
; Location of client certificate file (pem format with private key) on local filesystem
|
|
||||||
local_cert = ""
|
|
||||||
|
|
||||||
; This can help mitigate the CRIME attack vector.
|
|
||||||
disable_compression = On
|
|
||||||
|
|
||||||
[capa]
|
|
||||||
quota = On
|
|
||||||
|
|
||||||
; Allow clear folder and delete messages without moving to trash
|
|
||||||
dangerous_actions = On
|
|
||||||
|
|
||||||
; Allow download attachments as Zip (and optionally others)
|
|
||||||
attachments_actions = On
|
|
||||||
|
|
||||||
[login]
|
|
||||||
; If someone logs in without "@domain.tld", this value will be used
|
|
||||||
; When this value is HTTP_HOST, the $_SERVER["HTTP_HOST"] value is used.
|
|
||||||
; When this value is SERVER_NAME, the $_SERVER["SERVER_NAME"] value is used.
|
|
||||||
; When this value is gethostname, the gethostname() value is used.
|
|
||||||
;
|
|
||||||
default_domain = ""
|
|
||||||
|
|
||||||
; Allow language selection on webmail login screen
|
|
||||||
allow_languages_on_login = On
|
|
||||||
|
|
||||||
; Detect language from browser header `Accept-Language`
|
|
||||||
determine_user_language = On
|
|
||||||
|
|
||||||
; Like default_domain but then HTTP_HOST/SERVER_NAME without www.
|
|
||||||
determine_user_domain = Off
|
|
||||||
login_lowercase = On
|
|
||||||
|
|
||||||
; This option allows webmail to remember the logged in user
|
|
||||||
; once they closed the browser window.
|
|
||||||
;
|
|
||||||
; Values:
|
|
||||||
; "DefaultOff" - can be used, disabled by default;
|
|
||||||
; "DefaultOn" - can be used, enabled by default;
|
|
||||||
; "Unused" - cannot be used
|
|
||||||
sign_me_auto = "DefaultOff"
|
|
||||||
|
|
||||||
[plugins]
|
|
||||||
; Enable plugin support
|
|
||||||
enable = Off
|
|
||||||
|
|
||||||
; Comma-separated list of enabled plugins
|
|
||||||
enabled_list = ""
|
|
||||||
|
|
||||||
[defaults]
|
|
||||||
; Editor mode used by default (Plain, Html)
|
|
||||||
view_editor_type = "Html"
|
|
||||||
|
|
||||||
; layout: 0 - no preview, 1 - side preview, 2 - bottom preview
|
|
||||||
view_layout = 1
|
|
||||||
view_use_checkboxes = On
|
|
||||||
autologout = 30
|
|
||||||
view_html = On
|
|
||||||
show_images = Off
|
|
||||||
contacts_autosave = On
|
|
||||||
mail_use_threads = Off
|
|
||||||
allow_draft_autosave = On
|
|
||||||
mail_reply_same_folder = Off
|
|
||||||
|
|
||||||
[logs]
|
|
||||||
; Enable logging
|
|
||||||
enable = Off
|
|
||||||
|
|
||||||
; Path where log files will be stored
|
|
||||||
path = ""
|
|
||||||
|
|
||||||
; Log messages of set RFC 5424 section 6.2.1 Severity level and higher (0 = highest, 7 = lowest).
|
|
||||||
; 0 = Emergency
|
|
||||||
; 1 = Alert
|
|
||||||
; 2 = Critical
|
|
||||||
; 3 = Error
|
|
||||||
; 4 = Warning
|
|
||||||
; 5 = Notice
|
|
||||||
; 6 = Informational
|
|
||||||
; 7 = Debug
|
|
||||||
level = 4
|
|
||||||
|
|
||||||
; Required for development purposes only.
|
|
||||||
; Disabling this option is not recommended.
|
|
||||||
hide_passwords = On
|
|
||||||
time_zone = "UTC"
|
|
||||||
|
|
||||||
; Log filename.
|
|
||||||
; For security reasons, some characters are removed from filename.
|
|
||||||
; Allows for pattern-based folder creation (see examples below).
|
|
||||||
;
|
|
||||||
; Patterns:
|
|
||||||
; {date:Y-m-d} - Replaced by pattern-based date
|
|
||||||
; Detailed info: http://www.php.net/manual/en/function.date.php
|
|
||||||
; {user:email} - Replaced by user's email address
|
|
||||||
; If user is not logged in, value is set to "unknown"
|
|
||||||
; {user:login} - Replaced by user's login (the user part of an email)
|
|
||||||
; If user is not logged in, value is set to "unknown"
|
|
||||||
; {user:domain} - Replaced by user's domain name (the domain part of an email)
|
|
||||||
; If user is not logged in, value is set to "unknown"
|
|
||||||
; {user:uid} - Replaced by user's UID regardless of account currently used
|
|
||||||
;
|
|
||||||
; {user:ip}
|
|
||||||
; {request:ip} - Replaced by user's IP address
|
|
||||||
;
|
|
||||||
; Others:
|
|
||||||
; {imap:login} {imap:host} {imap:port}
|
|
||||||
; {smtp:login} {smtp:host} {smtp:port}
|
|
||||||
;
|
|
||||||
; Examples:
|
|
||||||
; filename = "log-{date:Y-m-d}.txt"
|
|
||||||
; filename = "{date:Y-m-d}/{user:domain}/{user:email}_{user:uid}.log"
|
|
||||||
; filename = "{user:email}-{date:Y-m-d}.txt"
|
|
||||||
; filename = "syslog"
|
|
||||||
filename = "log-{date:Y-m-d}.txt"
|
|
||||||
|
|
||||||
; Enable auth logging in a separate file (for fail2ban)
|
|
||||||
auth_logging = Off
|
|
||||||
auth_logging_filename = "fail2ban/auth-{date:Y-m-d}.txt"
|
|
||||||
auth_logging_format = "[{date:Y-m-d H:i:s}] Auth failed: ip={request:ip} user={imap:login} host={imap:host} port={imap:port}"
|
|
||||||
|
|
||||||
; Enable auth logging to syslog for fail2ban
|
|
||||||
auth_syslog = On
|
|
||||||
|
|
||||||
[debug]
|
|
||||||
; Special option required for development purposes
|
|
||||||
enable = Off
|
|
||||||
javascript = Off
|
|
||||||
css = Off
|
|
||||||
|
|
||||||
[cache]
|
|
||||||
; The section controls caching of the entire application.
|
|
||||||
;
|
|
||||||
; Enables caching in the system
|
|
||||||
enable = On
|
|
||||||
|
|
||||||
; Path where cache files will be stored
|
|
||||||
path = ""
|
|
||||||
|
|
||||||
; Additional caching key. If changed, cache is purged
|
|
||||||
index = "v1"
|
|
||||||
|
|
||||||
; Can be: files, APCU, memcache, redis (beta)
|
|
||||||
fast_cache_driver = "files"
|
|
||||||
|
|
||||||
; Additional caching key. If changed, fast cache is purged
|
|
||||||
fast_cache_index = "v1"
|
|
||||||
|
|
||||||
; Browser-level cache. If enabled, caching is maintainted without using files
|
|
||||||
http = On
|
|
||||||
|
|
||||||
; Browser-level cache time (seconds, Expires header)
|
|
||||||
http_expires = 3600
|
|
||||||
|
|
||||||
; Caching message UIDs when searching and sorting (threading)
|
|
||||||
server_uids = On
|
|
||||||
system_data = On
|
|
||||||
|
|
||||||
[imap]
|
|
||||||
use_force_selection = Off
|
|
||||||
use_expunge_all_on_delete = Off
|
|
||||||
message_list_fast_simple_search = On
|
|
||||||
message_list_permanent_filter = ""
|
|
||||||
message_all_headers = Off
|
|
||||||
show_login_alert = On
|
|
||||||
fetch_new_messages = On
|
|
||||||
|
|
||||||
[labs]
|
|
||||||
; Display message RFC 2822 date and time header, instead of the arrival internal date.
|
|
||||||
date_from_headers = On
|
|
||||||
allow_message_append = Off
|
|
||||||
|
|
||||||
; When login fails, wait N seconds before responding
|
|
||||||
login_fault_delay = 5
|
|
||||||
log_ajax_response_write_limit = 300
|
|
||||||
allow_html_editor_biti_buttons = Off
|
|
||||||
allow_ctrl_enter_on_compose = On
|
|
||||||
smtp_show_server_errors = Off
|
|
||||||
sieve_auth_plain_initial = On
|
|
||||||
sieve_allow_fileinto_inbox = Off
|
|
||||||
|
|
||||||
; PHP mail() remove To and Subject headers
|
|
||||||
mail_func_clear_headers = On
|
|
||||||
|
|
||||||
; PHP mail() set -f emailaddress
|
|
||||||
mail_func_additional_parameters = Off
|
|
||||||
folders_spec_limit = 50
|
|
||||||
curl_proxy = ""
|
|
||||||
curl_proxy_auth = ""
|
|
||||||
custom_login_link = ""
|
|
||||||
custom_logout_link = ""
|
|
||||||
http_client_ip_check_proxy = Off
|
|
||||||
fast_cache_memcache_host = "127.0.0.1"
|
|
||||||
fast_cache_memcache_port = 11211
|
|
||||||
fast_cache_redis_host = "127.0.0.1"
|
|
||||||
fast_cache_redis_port = 6379
|
|
||||||
use_local_proxy_for_external_images = On
|
|
||||||
image_exif_auto_rotate = Off
|
|
||||||
cookie_default_path = ""
|
|
||||||
cookie_default_secure = Off
|
|
||||||
replace_env_in_configuration = ""
|
|
||||||
boundary_prefix = ""
|
|
||||||
dev_email = ""
|
|
||||||
dev_password = ""
|
|
||||||
|
|
||||||
[version]
|
|
||||||
current = "2.28.4"
|
|
||||||
saved = "Sun, 18 Dec 2022 22:10:48 +0000"
|
|
||||||
7
.docker/release/test/build_and_test.sh
Executable file
7
.docker/release/test/build_and_test.sh
Executable file
|
|
@ -0,0 +1,7 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# This script uses docker buildx to build a docker image, and loads it into the docker daemon. Then it executes ./test.sh to test the docker image
|
||||||
|
# It is useful for testing release builds in development
|
||||||
|
set -eu
|
||||||
|
IMAGE=snappymail/snappymail:test
|
||||||
|
DOCKER_BUILDX=1 docker build -t "$IMAGE" -f .docker/release/Dockerfile .
|
||||||
|
.docker/release/test/test.sh "$IMAGE"
|
||||||
29
.docker/release/test/config.yaml
Normal file
29
.docker/release/test/config.yaml
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# See: https://github.com/GoogleContainerTools/container-structure-test
|
||||||
|
# See: https://github.com/GoogleContainerTools/container-structure-test/issues/78
|
||||||
|
schemaVersion: 2.0.0
|
||||||
|
commandTests:
|
||||||
|
- name: Integration test
|
||||||
|
command: /bin/sh
|
||||||
|
args:
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
set -eux
|
||||||
|
DEBUG=true /entrypoint.sh > /tmp/test 2>&1 &
|
||||||
|
sleep 5
|
||||||
|
cat /tmp/test
|
||||||
|
pidof supervisord
|
||||||
|
pidof nginx
|
||||||
|
pidof php-fpm
|
||||||
|
ls -al /var/lib/snappymail/_data_/_default_/configs/application.ini
|
||||||
|
ls -al /var/lib/snappymail/_data_/_default_/admin_password.txt
|
||||||
|
nc -vz 127.0.0.1 8888
|
||||||
|
nc -vz 127.0.0.1 9000
|
||||||
|
wget -S -T 3 -O /dev/null http://127.0.0.1:8888
|
||||||
|
kill `pidof supervisord`
|
||||||
|
metadataTest:
|
||||||
|
exposedPorts: ["8888", "9000"]
|
||||||
|
volumes: ["/var/lib/snappymail"]
|
||||||
|
entrypoint: []
|
||||||
|
cmd: ["/entrypoint.sh"]
|
||||||
|
workdir: /snappymail
|
||||||
|
user: root
|
||||||
10
.docker/release/test/test.sh
Executable file
10
.docker/release/test/test.sh
Executable file
|
|
@ -0,0 +1,10 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# This script tests a given docker image using https://github.com/GoogleContainerTools/container-structure-test
|
||||||
|
set -eu
|
||||||
|
SCRIPT_DIR=$( cd "$(dirname "$0")" && pwd )
|
||||||
|
IMAGE=${1:-}
|
||||||
|
echo "Testing image: $IMAGE"
|
||||||
|
docker run --rm -i \
|
||||||
|
-v /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||||
|
-v "$SCRIPT_DIR/config.yaml:/config.yaml:ro" \
|
||||||
|
gcr.io/gcp-runtimes/container-structure-test:latest test --image "$IMAGE" --config config.yaml
|
||||||
2
.dockerignore
Normal file
2
.dockerignore
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
/.git
|
||||||
|
/node_modules
|
||||||
|
|
@ -3,7 +3,7 @@ module.exports = {
|
||||||
// extends: ['eslint:recommended', 'plugin:prettier/recommended'],
|
// extends: ['eslint:recommended', 'plugin:prettier/recommended'],
|
||||||
extends: ['eslint:recommended'],
|
extends: ['eslint:recommended'],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 6,
|
ecmaVersion: 11,
|
||||||
sourceType: 'module'
|
sourceType: 'module'
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
|
|
@ -35,10 +35,13 @@ module.exports = {
|
||||||
// vendors/bootstrap/bootstrap.native.js
|
// vendors/bootstrap/bootstrap.native.js
|
||||||
'BSN': "readonly",
|
'BSN': "readonly",
|
||||||
// Mailvelope
|
// Mailvelope
|
||||||
'mailvelope': "readonly"
|
'mailvelope': "readonly",
|
||||||
|
// Punycode
|
||||||
|
'IDN': "readonly"
|
||||||
},
|
},
|
||||||
// http://eslint.org/docs/rules/
|
// http://eslint.org/docs/rules/
|
||||||
rules: {
|
rules: {
|
||||||
|
'no-cond-assign': 0,
|
||||||
// plugins
|
// plugins
|
||||||
'no-mixed-spaces-and-tabs': 'off',
|
'no-mixed-spaces-and-tabs': 'off',
|
||||||
'max-len': [
|
'max-len': [
|
||||||
|
|
|
||||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
|
|
@ -1,2 +1,2 @@
|
||||||
community_bridge: SnappyMail
|
github: the-djmaze
|
||||||
custom: ["https://www.paypal.me/thedjmaze", "https://snappymail.eu"]
|
custom: ["https://www.paypal.me/thedjmaze", "https://snappymail.eu"]
|
||||||
|
|
|
||||||
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -30,8 +30,9 @@ If applicable, add screenshots to help explain your problem.
|
||||||
- SnappyMail Version:
|
- SnappyMail Version:
|
||||||
- Mode: [e.g. standalone, nextcloud, cyberpanel, docker]
|
- Mode: [e.g. standalone, nextcloud, cyberpanel, docker]
|
||||||
|
|
||||||
**[Debug/logging information](https://github.com/the-djmaze/snappymail/wiki/FAQ#how-do-i-enable-logging)**
|
**Debug/logging information**
|
||||||
Place them here (few lines) or as attachments (many lines)
|
[Read here how to log](https://github.com/the-djmaze/snappymail/wiki/FAQ#how-do-i-enable-logging)
|
||||||
|
- [ ] I've placed them here (few lines) or as attachments (many lines)
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
||||||
|
|
|
||||||
93
.github/workflows/docker-pr.yml
vendored
Normal file
93
.github/workflows/docker-pr.yml
vendored
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
name: docker-pr
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
# This step generates the docker tags
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
env:
|
||||||
|
# This env var ensures {{sha}} is a real commit SHA for type=ref,event=pr
|
||||||
|
DOCKER_METADATA_PR_HEAD_SHA: 'true'
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
djmaze/snappymail
|
||||||
|
ghcr.io/${{ github.repository }}
|
||||||
|
# type=ref,event=pr generates tag(s) on PRs only. E.g. 'pr-123', 'pr-123-abc0123'
|
||||||
|
tags: |
|
||||||
|
type=ref,event=pr
|
||||||
|
type=ref,suffix=-{{sha}},event=pr
|
||||||
|
# The rest of the org.opencontainers.image.xxx labels are dynamically generated
|
||||||
|
labels: |
|
||||||
|
org.opencontainers.image.description=SnappyMail
|
||||||
|
org.opencontainers.image.licenses=AGPLv3
|
||||||
|
|
||||||
|
# See: https://github.com/docker/build-push-action/blob/v2.6.1/docs/advanced/cache.md#github-cache
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Cache Docker layers
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: /tmp/.buildx-cache
|
||||||
|
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-buildx-
|
||||||
|
|
||||||
|
# See: https://github.com/docker/buildx/issues/59
|
||||||
|
- name: Build
|
||||||
|
id: build
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: '.'
|
||||||
|
file: ./.docker/release/Dockerfile
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
|
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
||||||
|
|
||||||
|
- name: Docker images
|
||||||
|
run: |
|
||||||
|
docker images
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: |
|
||||||
|
TAG=$( echo "${{ steps.meta.outputs.tags }}" | head -n1 )
|
||||||
|
.docker/release/test/test.sh "$TAG"
|
||||||
|
|
||||||
|
- name: Build all archs
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: '.'
|
||||||
|
file: ./.docker/release/Dockerfile
|
||||||
|
platforms: linux/386,linux/amd64,linux/arm64
|
||||||
|
push: false
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
|
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
||||||
|
|
||||||
|
# Temp fix
|
||||||
|
# https://github.com/docker/build-push-action/issues/252
|
||||||
|
# https://github.com/moby/buildkit/issues/1896
|
||||||
|
- name: Move cache
|
||||||
|
run: |
|
||||||
|
rm -rf /tmp/.buildx-cache
|
||||||
|
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
||||||
117
.github/workflows/docker.yml
vendored
Normal file
117
.github/workflows/docker.yml
vendored
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
name: docker
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v2.*'
|
||||||
|
|
||||||
|
# This is needed to push to GitHub Container Registry. See https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
# This step generates the docker tags
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
env:
|
||||||
|
# This env var ensures {{sha}} is a real commit SHA for type=ref,event=pr
|
||||||
|
DOCKER_METADATA_PR_HEAD_SHA: 'true'
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
djmaze/snappymail
|
||||||
|
ghcr.io/${{ github.repository }}
|
||||||
|
# type=ref,event=branch generates tag(s) on branch only. E.g. 'master', 'master-abc0123'
|
||||||
|
# type=ref,event=tag generates tag(s) on tags only. E.g. 'v0.0.0', 'v0.0.0-abc0123', and 'latest'
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=tag
|
||||||
|
# The rest of the org.opencontainers.image.xxx labels are dynamically generated
|
||||||
|
labels: |
|
||||||
|
org.opencontainers.image.description=SnappyMail
|
||||||
|
org.opencontainers.image.licenses=AGPLv3
|
||||||
|
|
||||||
|
# See: https://github.com/docker/build-push-action/blob/v2.6.1/docs/advanced/cache.md#github-cache
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Cache Docker layers
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: /tmp/.buildx-cache
|
||||||
|
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-buildx-
|
||||||
|
|
||||||
|
- name: Login to Docker Hub registry
|
||||||
|
if: startsWith(github.ref, 'refs/tags/') # Login only on tags
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
if: startsWith(github.ref, 'refs/tags/') # Login only on tags
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# See: https://github.com/docker/buildx/issues/59
|
||||||
|
- name: Build
|
||||||
|
id: build
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: '.'
|
||||||
|
file: ./.docker/release/Dockerfile
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
|
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
||||||
|
|
||||||
|
- name: Docker images
|
||||||
|
run: |
|
||||||
|
docker images
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: |
|
||||||
|
TAG=$( echo "${{ steps.meta.outputs.tags }}" | head -n1 )
|
||||||
|
.docker/release/test/test.sh "$TAG"
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
id: build-and-push
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: '.'
|
||||||
|
file: ./.docker/release/Dockerfile
|
||||||
|
# TODO: Add more arches?
|
||||||
|
# platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x
|
||||||
|
platforms: linux/386,linux/amd64,linux/arm64
|
||||||
|
push: ${{ startsWith(github.ref, 'refs/tags/') }} # Push only on tags
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
|
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
||||||
|
|
||||||
|
# Temp fix
|
||||||
|
# https://github.com/docker/build-push-action/issues/252
|
||||||
|
# https://github.com/moby/buildkit/issues/1896
|
||||||
|
- name: Move cache
|
||||||
|
run: |
|
||||||
|
rm -rf /tmp/.buildx-cache
|
||||||
|
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
||||||
|
|
@ -1,7 +1,15 @@
|
||||||
|
<FilesMatch "index\.php">
|
||||||
|
AcceptPathInfo On
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
<IfModule mod_rewrite.c>
|
<IfModule mod_rewrite.c>
|
||||||
RewriteEngine On
|
RewriteEngine On
|
||||||
# Redirect cPanel
|
# Redirect cPanel
|
||||||
RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301]
|
RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301]
|
||||||
|
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule ^(.+)$ index.php/$1 [L,QSA]
|
||||||
</IfModule>
|
</IfModule>
|
||||||
|
|
||||||
<IfModule mod_expires.c>
|
<IfModule mod_expires.c>
|
||||||
|
|
|
||||||
1592
CHANGELOG.md
1592
CHANGELOG.md
File diff suppressed because it is too large
Load diff
59
README.md
59
README.md
|
|
@ -5,6 +5,10 @@
|
||||||
<br>
|
<br>
|
||||||
<h1>SnappyMail</h1>
|
<h1>SnappyMail</h1>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
[](https://github.com/the-djmaze/snappymail/actions/workflows/docker.yml)
|
||||||
|
[](https://hub.docker.com/r/djmaze/snappymail/tags)
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Simple, modern, lightweight & fast web-based email client.
|
Simple, modern, lightweight & fast web-based email client.
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -26,7 +30,7 @@ For more information about the product, check [snappymail.eu](https://snappymail
|
||||||
|
|
||||||
Information about installing the product, check the [wiki page](https://github.com/the-djmaze/snappymail/wiki/Installation-instructions).
|
Information about installing the product, check the [wiki page](https://github.com/the-djmaze/snappymail/wiki/Installation-instructions).
|
||||||
|
|
||||||
And don't forget to read the [RainLoop documentation](https://www.rainloop.net/docs/).
|
And don't forget to read the whole [Wiki](https://github.com/the-djmaze/snappymail/wiki).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
@ -34,7 +38,7 @@ And don't forget to read the [RainLoop documentation](https://www.rainloop.net/d
|
||||||
**GNU AFFERO GENERAL PUBLIC LICENSE Version 3 (AGPL)**.
|
**GNU AFFERO GENERAL PUBLIC LICENSE Version 3 (AGPL)**.
|
||||||
http://www.gnu.org/licenses/agpl-3.0.html
|
http://www.gnu.org/licenses/agpl-3.0.html
|
||||||
|
|
||||||
Copyright (c) 2020 - 2023 SnappyMail
|
Copyright (c) 2020 - 2024 SnappyMail
|
||||||
Copyright (c) 2013 - 2022 RainLoop
|
Copyright (c) 2013 - 2022 RainLoop
|
||||||
|
|
||||||
## Modifications
|
## Modifications
|
||||||
|
|
@ -45,7 +49,7 @@ This fork of RainLoop has the following changes:
|
||||||
* Admin uses password_hash/password_verify
|
* Admin uses password_hash/password_verify
|
||||||
* Auth failed attempts written to syslog
|
* Auth failed attempts written to syslog
|
||||||
* Added Fail2ban instructions
|
* Added Fail2ban instructions
|
||||||
* ES2018
|
* ES2020
|
||||||
* PHP 7.4+ required
|
* PHP 7.4+ required
|
||||||
* PHP mbstring extension required
|
* PHP mbstring extension required
|
||||||
* PHP replaced pclZip with PharData and ZipArchive
|
* PHP replaced pclZip with PharData and ZipArchive
|
||||||
|
|
@ -79,12 +83,7 @@ This fork of RainLoop has the following changes:
|
||||||
* Added [Fetch Metadata Request Headers](https://www.w3.org/TR/fetch-metadata/) checks
|
* Added [Fetch Metadata Request Headers](https://www.w3.org/TR/fetch-metadata/) checks
|
||||||
* Reduced excessive DOM size
|
* Reduced excessive DOM size
|
||||||
* Support [Kolab groupware](https://kolab.org/)
|
* Support [Kolab groupware](https://kolab.org/)
|
||||||
* Support IMAP RFC 2971 ID extension
|
* Support many more [IMAP RFC's](https://snappymail.eu/comparison#IMAP)
|
||||||
* Support IMAP RFC 5258 LIST-EXTENDED
|
|
||||||
* Support IMAP RFC 5464 METADATA
|
|
||||||
* Support IMAP RFC 5819 LIST-STATUS
|
|
||||||
* Support IMAP RFC 7628 SASL OAUTHBEARER aka XOAUTH2
|
|
||||||
* Support IMAP4rev2 RFC 9051
|
|
||||||
* Support Sodium and OpenSSL for encryption
|
* Support Sodium and OpenSSL for encryption
|
||||||
* Much better PGP support
|
* Much better PGP support
|
||||||
|
|
||||||
|
|
@ -141,28 +140,28 @@ RainLoop 1.17 vs SnappyMail
|
||||||
|
|
||||||
|js/* |RainLoop |Snappy |
|
|js/* |RainLoop |Snappy |
|
||||||
|--------------- |--------: |--------: |
|
|--------------- |--------: |--------: |
|
||||||
|admin.js |2.170.153 | 80.102 |
|
|admin.js |2.170.153 | 84.054 |
|
||||||
|app.js |4.207.787 | 407.874 |
|
|app.js |4.207.787 | 441.754 |
|
||||||
|boot.js | 868.735 | 4.142 |
|
|boot.js | 868.735 | 4.147 |
|
||||||
|libs.js | 658.812 | 187.076 |
|
|libs.js | 658.812 | 193.716 |
|
||||||
|sieve.js | 0 | 85.141 |
|
|sieve.js | 0 | 84.598 |
|
||||||
|polyfills.js | 334.608 | 0 |
|
|polyfills.js | 334.608 | 0 |
|
||||||
|serviceworker.js | 0 | 285 |
|
|serviceworker.js | 0 | 285 |
|
||||||
|TOTAL |8.240.095 | 764.620 |
|
|TOTAL |8.240.095 | 808.554 |
|
||||||
|
|
||||||
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
||||||
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|
||||||
|admin.min.js | 256.831 | 39.350 | 73.606 | 13.163 | 60.877 | 11.805 |
|
|admin.min.js | 256.831 | 41.162 | 73.606 | 13.885 | 60.877 | 12.434 |
|
||||||
|app.min.js | 515.367 | 186.311 |139.456 | 62.929 |110.485 | 54.076 |
|
|app.min.js | 515.367 | 199.730 |139.456 | 67.669 |110.485 | 57.672 |
|
||||||
|boot.min.js | 84.659 | 2.084 | 26.998 | 1.202 | 23.643 | 1.003 |
|
|boot.min.js | 84.659 | 2.087 | 26.998 | 1.204 | 23.643 | 1.002 |
|
||||||
|libs.min.js | 584.772 | 90.808 |180.901 | 33.754 |155.182 | 30.224 |
|
|libs.min.js | 584.772 | 92.365 |180.901 | 34.487 |155.182 | 30.830 |
|
||||||
|sieve.min.js | 0 | 41.399 | 0 | 10.394 | 0 | 9.356 |
|
|sieve.min.js | 0 | 41.093 | 0 | 10.325 | 0 | 9.327 |
|
||||||
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 |
|
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 |
|
||||||
|TOTAL user |1.217.635 | 279.203 |358.761 | 97.885 |299.485 | 85.303 |
|
|TOTAL user |1.217.635 | 294.182 |358.761 |103.360 |299.485 | 89.504 |
|
||||||
|TOTAL user+sieve |1.217.635 | 320.602 |358.761 |108.279 |299.485 | 94.659 |
|
|TOTAL user+sieve |1.217.635 | 335.275 |358.761 |113.685 |299.485 | 98.831 |
|
||||||
|TOTAL admin | 959.099 | 132.242 |292.911 | 48.119 |249.877 | 43.032 |
|
|TOTAL admin | 959.099 | 135.614 |292.911 | 49.576 |249.877 | 44.266 |
|
||||||
|
|
||||||
For a user it is around 72% smaller and faster than traditional RainLoop.
|
For a user it is around 68% smaller and faster than traditional RainLoop.
|
||||||
|
|
||||||
### CSS changes
|
### CSS changes
|
||||||
|
|
||||||
|
|
@ -189,12 +188,12 @@ For a user it is around 72% smaller and faster than traditional RainLoop.
|
||||||
|
|
||||||
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|
||||||
|------------ |-------: |------: |------: |------: |--------: |
|
|------------ |-------: |------: |------: |------: |--------: |
|
||||||
|app.css | 340.331 | 84.390 | 46.946 | 17.605 | 15.084 |
|
|app.css | 340.331 | 84.691 | 46.946 | 17.693 | 15.157 |
|
||||||
|app.min.css | 274.947 | 67.774 | 39.647 | 15.487 | 13.527 |
|
|app.min.css | 274.947 | 68.052 | 39.647 | 15.589 | 13.610 |
|
||||||
|boot.css | | 1.326 | | 664 | 545 |
|
|boot.css | | 1.326 | | 664 | 545 |
|
||||||
|boot.min.css | | 1.071 | | 590 | 474 |
|
|boot.min.css | | 1.071 | | 590 | 474 |
|
||||||
|admin.css | | 30.482 | | 6.988 | 6.092 |
|
|admin.css | | 30.602 | | 7.023 | 6.112 |
|
||||||
|admin.min.css | | 24.607 | | 6.315 | 5.579 |
|
|admin.min.css | | 24.717 | | 6.346 | 5.586 |
|
||||||
|
|
||||||
### PGP
|
### PGP
|
||||||
RainLoop uses the old OpenPGP.js v2
|
RainLoop uses the old OpenPGP.js v2
|
||||||
|
|
@ -208,7 +207,7 @@ See https://github.com/the-djmaze/openpgpjs for development
|
||||||
|
|
||||||
|OpenPGP |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
|OpenPGP |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
||||||
|--------------- |--------: |--------: |------: |-------: |--------: |--------: |
|
|--------------- |--------: |--------: |------: |-------: |--------: |--------: |
|
||||||
|openpgp.min.js | 330.742 | 541.176 |102.388 | 168.266 | 84.241 | 138.278 |
|
|openpgp.min.js | 330.742 | 546.165 |102.388 | 169.207 | 84.241 | 138.688 |
|
||||||
|openpgp.worker | 1.499 | | 824 | | 695 | |
|
|openpgp.worker | 1.499 | | 824 | | 695 | |
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -225,5 +224,3 @@ Still TODO:
|
||||||
|ckeditor | ? | 520.035 | ? | 155.916 |
|
|ckeditor | ? | 520.035 | ? | 155.916 |
|
||||||
|
|
||||||
CKEditor including the 7 asset requests (css,language,plugins,icons) is 633.46 KB / 180.47 KB (gzip).
|
CKEditor including the 7 asset requests (css,language,plugins,icons) is 633.46 KB / 180.47 KB (gzip).
|
||||||
|
|
||||||
To use the old CKEditor, you must install the plugin.
|
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,15 @@ Currently due to the fast development only the latest version receives security
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| -------- | --------- |
|
| -------- | --------- |
|
||||||
| 2.13.x | ✔ |
|
| 2.30.x | ✔ |
|
||||||
| < 2.13.0 | ❌ |
|
| < 2.30.0 | ❌ |
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
Please report security issues or vulnerabilities as an encrypted email to [security@snappymail.eu](mailto:security@snappymail.eu).
|
Please report security issues or vulnerabilities as an encrypted email to [security@snappymail.eu](mailto:security@snappymail.eu).
|
||||||
Your report should be detailed enough with clear steps to reproduce and classify the found vulnerability.
|
Your report should be detailed enough with clear steps to reproduce and classify the found vulnerability.
|
||||||
|
|
||||||
You can find the PGP public key below and on the major public keyservers like [pgp.key-server.io](https://pgp.key-server.io).
|
You can find the PGP public key below and on the major public keyservers like [keys.openpgp.org](https://keys.openpgp.org).
|
||||||
```
|
```
|
||||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
Comment: Type: 255-bit EdDSA
|
Comment: Type: 255-bit EdDSA
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,46 @@ $keys = [
|
||||||
'url',
|
'url',
|
||||||
'version'
|
'version'
|
||||||
];
|
];
|
||||||
|
/*
|
||||||
|
$released = [
|
||||||
|
'add-x-originating-ip-header',
|
||||||
|
'avatars',
|
||||||
|
'backup',
|
||||||
|
'black-list',
|
||||||
|
'change-password',
|
||||||
|
'change-password-froxlor',
|
||||||
|
'change-password-hestia',
|
||||||
|
'change-password-hmailserver',
|
||||||
|
'change-password-ispconfig',
|
||||||
|
'change-password-poppassd',
|
||||||
|
'custom-login-mapping',
|
||||||
|
'imap-contacts-suggestions',
|
||||||
|
'kolab',
|
||||||
|
'ldap-contacts-suggestions',
|
||||||
|
'ldap-identities',
|
||||||
|
'ldap-login-mapping',
|
||||||
|
'ldap-mail-accounts',
|
||||||
|
'login-external',
|
||||||
|
'login-external-sso',
|
||||||
|
'login-override',
|
||||||
|
'login-register',
|
||||||
|
'login-remote',
|
||||||
|
'mailbox-detect',
|
||||||
|
'nextcloud',
|
||||||
|
'override-smtp-credentials',
|
||||||
|
'set-remote-addr',
|
||||||
|
'smtp-use-from-adr-account',
|
||||||
|
'snowfall-on-login-screen',
|
||||||
|
'two-factor-auth',
|
||||||
|
'view-ics',
|
||||||
|
'white-list'
|
||||||
|
];
|
||||||
|
*/
|
||||||
foreach (glob(ROOT_DIR . '/plugins/*', GLOB_NOSORT | GLOB_ONLYDIR) as $dir) {
|
foreach (glob(ROOT_DIR . '/plugins/*', GLOB_NOSORT | GLOB_ONLYDIR) as $dir) {
|
||||||
if (is_file("{$dir}/index.php") && !strpos($dir, '.bak')) {
|
if (is_file("{$dir}/index.php") && !strpos($dir, '.bak')) {
|
||||||
require "{$dir}/index.php";
|
require "{$dir}/index.php";
|
||||||
$name = basename($dir);
|
$name = basename($dir);
|
||||||
|
// if (!in_array($name, $released)) continue;
|
||||||
$class = new ReflectionClass(str_replace('-', '', $name) . 'Plugin');
|
$class = new ReflectionClass(str_replace('-', '', $name) . 'Plugin');
|
||||||
$manifest_item = [];
|
$manifest_item = [];
|
||||||
foreach ($class->getConstants() as $key => $value) {
|
foreach ($class->getConstants() as $key => $value) {
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,6 @@ $file = ROOT_DIR . '/integrations/cloudron/Dockerfile';
|
||||||
file_put_contents($file, preg_replace('/VERSION=[0-9.]+/', "VERSION={$package->version}", file_get_contents($file)));
|
file_put_contents($file, preg_replace('/VERSION=[0-9.]+/', "VERSION={$package->version}", file_get_contents($file)));
|
||||||
$file = ROOT_DIR . '/integrations/cloudron/DESCRIPTION.md';
|
$file = ROOT_DIR . '/integrations/cloudron/DESCRIPTION.md';
|
||||||
file_put_contents($file, preg_replace('/<upstream>[^<]*</', "<upstream>{$package->version}<", file_get_contents($file)));
|
file_put_contents($file, preg_replace('/<upstream>[^<]*</', "<upstream>{$package->version}<", file_get_contents($file)));
|
||||||
// docker
|
|
||||||
$file = ROOT_DIR . '/.docker/release/files/usr/local/include/application.ini';
|
|
||||||
file_put_contents($file, preg_replace('/current = "[0-9.]+"/', "current = \"{$package->version}\"", file_get_contents($file)));
|
|
||||||
// virtualmin
|
// virtualmin
|
||||||
$file = ROOT_DIR . '/integrations/virtualmin/snappymail.pl';
|
$file = ROOT_DIR . '/integrations/virtualmin/snappymail.pl';
|
||||||
file_put_contents($file, preg_replace('/return \\( "[0-9]+\\.[0-9]+\\.[0-9]+" \\)/', "return ( \"{$package->version}\" )", file_get_contents($file)));
|
file_put_contents($file, preg_replace('/return \\( "[0-9]+\\.[0-9]+\\.[0-9]+" \\)/', "return ( \"{$package->version}\" )", file_get_contents($file)));
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import ko from 'ko';
|
||||||
import { logoutLink } from 'Common/Links';
|
import { logoutLink } from 'Common/Links';
|
||||||
import { i18nToNodes, initOnStartOrLangChange } from 'Common/Translator';
|
import { i18nToNodes, initOnStartOrLangChange } from 'Common/Translator';
|
||||||
|
|
||||||
|
import { arePopupsVisible } from 'Knoin/Knoin';
|
||||||
|
|
||||||
import { LanguageStore } from 'Stores/Language';
|
import { LanguageStore } from 'Stores/Language';
|
||||||
import { initThemes } from 'Stores/Theme';
|
import { initThemes } from 'Stores/Theme';
|
||||||
|
|
||||||
|
|
@ -18,6 +20,7 @@ export class AbstractApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
logoutReload(url) {
|
logoutReload(url) {
|
||||||
|
arePopupsVisible(false);
|
||||||
url = url || logoutLink();
|
url = url || logoutLink();
|
||||||
if (location.href !== url) {
|
if (location.href !== url) {
|
||||||
setTimeout(() => location.href = url, 100);
|
setTimeout(() => location.href = url, 100);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import 'External/ko';
|
import 'External/ko';
|
||||||
|
|
||||||
import { Settings, SettingsGet } from 'Common/Globals';
|
import { SettingsGet, SettingsAdmin } from 'Common/Globals';
|
||||||
import { initThemes } from 'Stores/Theme';
|
import { initThemes } from 'Stores/Theme';
|
||||||
|
|
||||||
import Remote from 'Remote/Admin/Fetch';
|
import Remote from 'Remote/Admin/Fetch';
|
||||||
|
|
@ -11,6 +11,8 @@ import { LoginAdminScreen } from 'Screen/Admin/Login';
|
||||||
import { startScreens } from 'Knoin/Knoin';
|
import { startScreens } from 'Knoin/Knoin';
|
||||||
import { AbstractApp } from 'App/Abstract';
|
import { AbstractApp } from 'App/Abstract';
|
||||||
|
|
||||||
|
import { AskPopupView } from 'View/Popup/Ask';
|
||||||
|
|
||||||
export class AdminApp extends AbstractApp {
|
export class AdminApp extends AbstractApp {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(Remote);
|
super(Remote);
|
||||||
|
|
@ -23,7 +25,8 @@ export class AdminApp extends AbstractApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
if (!Settings.app('adminAllowed')) {
|
// if (!Settings.app('adminAllowed')) {
|
||||||
|
if (!SettingsAdmin('allowed')) {
|
||||||
rl.route.root();
|
rl.route.root();
|
||||||
setTimeout(() => location.href = '/', 1);
|
setTimeout(() => location.href = '/', 1);
|
||||||
} else if (SettingsGet('Auth')) {
|
} else if (SettingsGet('Auth')) {
|
||||||
|
|
@ -34,3 +37,16 @@ export class AdminApp extends AbstractApp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AskPopupView.credentials = function(sAskDesc, btnText) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.showModal([
|
||||||
|
sAskDesc,
|
||||||
|
view => resolve({username:view.username(), password:view.passphrase()}),
|
||||||
|
() => resolve(null),
|
||||||
|
true,
|
||||||
|
3,
|
||||||
|
btnText
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
||||||
124
dev/App/User.js
124
dev/App/User.js
|
|
@ -1,8 +1,7 @@
|
||||||
import 'External/User/ko';
|
import 'External/User/ko';
|
||||||
|
|
||||||
import { SMAudio } from 'Common/Audio';
|
import { SMAudio } from 'Common/Audio';
|
||||||
import { isArray, pInt } from 'Common/Utils';
|
import { mailToHelper, setLayoutResizer, dropdownsDetectVisibility, loadAccountsAndIdentities } from 'Common/UtilsUser';
|
||||||
import { mailToHelper, setLayoutResizer, dropdownsDetectVisibility } from 'Common/UtilsUser';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FolderType,
|
FolderType,
|
||||||
|
|
@ -27,15 +26,15 @@ import {
|
||||||
getFolderFromCacheList
|
getFolderFromCacheList
|
||||||
} from 'Common/Cache';
|
} from 'Common/Cache';
|
||||||
|
|
||||||
import { i18n, reloadTime } from 'Common/Translator';
|
import { i18n, reloadTime, getErrorMessage } from 'Common/Translator';
|
||||||
|
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
import { NotificationUserStore } from 'Stores/User/Notification';
|
import { NotificationUserStore } from 'Stores/User/Notification';
|
||||||
import { AccountUserStore } from 'Stores/User/Account';
|
import { AccountUserStore } from 'Stores/User/Account';
|
||||||
import { ContactUserStore } from 'Stores/User/Contact';
|
import { ContactUserStore } from 'Stores/User/Contact';
|
||||||
import { IdentityUserStore } from 'Stores/User/Identity';
|
|
||||||
import { FolderUserStore } from 'Stores/User/Folder';
|
import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
import { PgpUserStore } from 'Stores/User/Pgp';
|
import { PgpUserStore } from 'Stores/User/Pgp';
|
||||||
|
import { SMimeUserStore } from 'Stores/User/SMime';
|
||||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
import { ThemeStore, initThemes } from 'Stores/Theme';
|
import { ThemeStore, initThemes } from 'Stores/Theme';
|
||||||
import { LanguageStore } from 'Stores/Language';
|
import { LanguageStore } from 'Stores/Language';
|
||||||
|
|
@ -43,9 +42,6 @@ import { MessageUserStore } from 'Stores/User/Message';
|
||||||
|
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
import { AccountModel } from 'Model/Account';
|
|
||||||
import { IdentityModel } from 'Model/Identity';
|
|
||||||
|
|
||||||
import { LoginUserScreen } from 'Screen/User/Login';
|
import { LoginUserScreen } from 'Screen/User/Login';
|
||||||
import { MailBoxUserScreen } from 'Screen/User/MailBox';
|
import { MailBoxUserScreen } from 'Screen/User/MailBox';
|
||||||
import { SettingsUserScreen } from 'Screen/User/Settings';
|
import { SettingsUserScreen } from 'Screen/User/Settings';
|
||||||
|
|
@ -89,6 +85,10 @@ export class AppUser extends AbstractApp {
|
||||||
|
|
||||||
this.folderList = FolderUserStore.folderList;
|
this.folderList = FolderUserStore.folderList;
|
||||||
this.messageList = MessagelistUserStore;
|
this.messageList = MessagelistUserStore;
|
||||||
|
|
||||||
|
this.ask = AskPopupView;
|
||||||
|
|
||||||
|
this.loadAccountsAndIdentities = loadAccountsAndIdentities;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -113,7 +113,7 @@ export class AppUser extends AbstractApp {
|
||||||
case FolderType.Trash:
|
case FolderType.Trash:
|
||||||
oMoveFolder = getFolderFromCacheList(FolderUserStore.trashFolder());
|
oMoveFolder = getFolderFromCacheList(FolderUserStore.trashFolder());
|
||||||
nSetSystemFoldersNotification = iFolderType;
|
nSetSystemFoldersNotification = iFolderType;
|
||||||
bDelete = bDelete || UNUSED_OPTION_VALUE === FolderUserStore.trashFolder()
|
bDelete = bDelete/* || UNUSED_OPTION_VALUE === FolderUserStore.trashFolder()*/
|
||||||
|| sFromFolderFullName === FolderUserStore.spamFolder()
|
|| sFromFolderFullName === FolderUserStore.spamFolder()
|
||||||
|| sFromFolderFullName === FolderUserStore.trashFolder();
|
|| sFromFolderFullName === FolderUserStore.trashFolder();
|
||||||
break;
|
break;
|
||||||
|
|
@ -125,9 +125,7 @@ export class AppUser extends AbstractApp {
|
||||||
// no default
|
// no default
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!oMoveFolder && !bDelete) {
|
if (bDelete) {
|
||||||
showScreenPopup(FolderSystemPopupView, [nSetSystemFoldersNotification]);
|
|
||||||
} else if (bDelete) {
|
|
||||||
showScreenPopup(AskPopupView, [
|
showScreenPopup(AskPopupView, [
|
||||||
i18n('POPUPS_ASK/DESC_WANT_DELETE_MESSAGES'),
|
i18n('POPUPS_ASK/DESC_WANT_DELETE_MESSAGES'),
|
||||||
() => {
|
() => {
|
||||||
|
|
@ -136,34 +134,11 @@ export class AppUser extends AbstractApp {
|
||||||
]);
|
]);
|
||||||
} else if (oMoveFolder) {
|
} else if (oMoveFolder) {
|
||||||
MessagelistUserStore.moveMessages(sFromFolderFullName, oUids, oMoveFolder.fullName);
|
MessagelistUserStore.moveMessages(sFromFolderFullName, oUids, oMoveFolder.fullName);
|
||||||
|
} else {
|
||||||
|
showScreenPopup(FolderSystemPopupView, [nSetSystemFoldersNotification]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
accountsAndIdentities() {
|
|
||||||
AccountUserStore.loading(true);
|
|
||||||
IdentityUserStore.loading(true);
|
|
||||||
|
|
||||||
Remote.request('AccountsAndIdentities', (iError, oData) => {
|
|
||||||
AccountUserStore.loading(false);
|
|
||||||
IdentityUserStore.loading(false);
|
|
||||||
|
|
||||||
if (!iError) {
|
|
||||||
let items = oData.Result.Accounts;
|
|
||||||
AccountUserStore(isArray(items)
|
|
||||||
? items.map(oValue => new AccountModel(oValue.email, oValue.name))
|
|
||||||
: []
|
|
||||||
);
|
|
||||||
AccountUserStore.unshift(new AccountModel(SettingsGet('mainEmail'), '', false));
|
|
||||||
|
|
||||||
items = oData.Result.Identities;
|
|
||||||
IdentityUserStore(isArray(items)
|
|
||||||
? items.map(identityData => IdentityModel.reviveFromJson(identityData))
|
|
||||||
: []
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folder
|
* @param {string} folder
|
||||||
* @param {Array=} list = []
|
* @param {Array=} list = []
|
||||||
|
|
@ -173,8 +148,10 @@ export class AppUser extends AbstractApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
localStorage.removeItem('register_protocol_offered');
|
Remote.request('Logout', (iError, data) =>
|
||||||
Remote.request('Logout', () => rl.logoutReload(Settings.app('customLogoutLink')));
|
iError ? alert('Logout error: ' + getErrorMessage(iError, data))
|
||||||
|
: rl.logoutReload(Settings.app('customLogoutLink'))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstart() {
|
bootstart() {
|
||||||
|
|
@ -206,19 +183,17 @@ export class AppUser extends AbstractApp {
|
||||||
SettingsUserStore.init();
|
SettingsUserStore.init();
|
||||||
ContactUserStore.init();
|
ContactUserStore.init();
|
||||||
|
|
||||||
loadFolders(value => {
|
loadFolders((success, error) => {
|
||||||
try {
|
try {
|
||||||
if (value) {
|
if (success) {
|
||||||
startScreens([
|
startScreens([
|
||||||
MailBoxUserScreen,
|
MailBoxUserScreen,
|
||||||
SettingsUserScreen
|
SettingsUserScreen
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setRefreshFoldersInterval(pInt(SettingsGet('CheckMailInterval')));
|
setRefreshFoldersInterval(SettingsGet('CheckMailInterval'));
|
||||||
|
|
||||||
ContactUserStore.init();
|
loadAccountsAndIdentities();
|
||||||
|
|
||||||
this.accountsAndIdentities();
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const cF = FolderUserStore.currentFolderFullName();
|
const cF = FolderUserStore.currentFolderFullName();
|
||||||
|
|
@ -247,27 +222,17 @@ export class AppUser extends AbstractApp {
|
||||||
setInterval(reloadTime, 60000);
|
setInterval(reloadTime, 60000);
|
||||||
|
|
||||||
PgpUserStore.init();
|
PgpUserStore.init();
|
||||||
|
SMimeUserStore.loadCertificates();
|
||||||
|
|
||||||
setTimeout(() => mailToHelper(SettingsGet('mailToEmail')), 500);
|
setTimeout(() => mailToHelper(SettingsGet('mailToEmail')), 500);
|
||||||
|
|
||||||
if (!localStorage.getItem('register_protocol_offered')) {
|
|
||||||
// When auto-login is active
|
|
||||||
navigator.registerProtocolHandler?.(
|
|
||||||
'mailto',
|
|
||||||
location.protocol + '//' + location.host + location.pathname + '?mailto&to=%s',
|
|
||||||
(SettingsGet('title') || 'SnappyMail')
|
|
||||||
);
|
|
||||||
localStorage.setItem('register_protocol_offered', '1');
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.logout();
|
this.logout();
|
||||||
|
alert('Folders error: ' + getErrorMessage(0, error))
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
startScreens([LoginUserScreen]);
|
startScreens([LoginUserScreen]);
|
||||||
}
|
}
|
||||||
|
|
@ -278,3 +243,50 @@ export class AppUser extends AbstractApp {
|
||||||
showScreenPopup(ComposePopupView, params);
|
showScreenPopup(ComposePopupView, params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AskPopupView.password = function(sAskDesc, btnText, ask) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this.showModal([
|
||||||
|
sAskDesc,
|
||||||
|
view => resolve({
|
||||||
|
password:view.passphrase(),
|
||||||
|
username:/*ask & 2 ? */view.username(),
|
||||||
|
remember:/*ask & 4 ? */view.remember()
|
||||||
|
}),
|
||||||
|
() => resolve(null),
|
||||||
|
true,
|
||||||
|
ask || 1,
|
||||||
|
btnText
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
AskPopupView.cryptkey = () => new Promise(resolve => {
|
||||||
|
const fn = () => AskPopupView.showModal([
|
||||||
|
i18n('CRYPTO/ASK_CRYPTKEY_PASS'),
|
||||||
|
view => {
|
||||||
|
let pass = view.passphrase();
|
||||||
|
if (pass) {
|
||||||
|
Remote.post('ResealCryptKey', null, {
|
||||||
|
passphrase: pass
|
||||||
|
}).then(response => {
|
||||||
|
resolve(response?.Result);
|
||||||
|
}).catch(e => {
|
||||||
|
if (111 === e.code) {
|
||||||
|
fn();
|
||||||
|
} else {
|
||||||
|
console.error(e);
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => resolve(null),
|
||||||
|
true,
|
||||||
|
1,
|
||||||
|
i18n('CRYPTO/DECRYPT')
|
||||||
|
]);
|
||||||
|
fn();
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,7 @@ export const SMAudio = new class {
|
||||||
if ('running' == audioCtx.state && (this.supportedMp3 || this.supportedOgg)) {
|
if ('running' == audioCtx.state && (this.supportedMp3 || this.supportedOgg)) {
|
||||||
notificator = notificator || createNewObject();
|
notificator = notificator || createNewObject();
|
||||||
if (notificator) {
|
if (notificator) {
|
||||||
|
// SettingsGet('NotificationSound').startsWith('custom@')
|
||||||
notificator.src = Links.staticLink('sounds/'
|
notificator.src = Links.staticLink('sounds/'
|
||||||
+ SettingsGet('NotificationSound')
|
+ SettingsGet('NotificationSound')
|
||||||
+ (this.supportedMp3 ? '.mp3' : '.ogg'));
|
+ (this.supportedMp3 ? '.mp3' : '.ogg'));
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ Notifications = {
|
||||||
ConnectionError: 104,
|
ConnectionError: 104,
|
||||||
DomainNotAllowed: 109,
|
DomainNotAllowed: 109,
|
||||||
AccountNotAllowed: 110,
|
AccountNotAllowed: 110,
|
||||||
|
CryptKeyError: 111,
|
||||||
|
|
||||||
ContactsSyncError: 140,
|
ContactsSyncError: 140,
|
||||||
|
|
||||||
|
|
@ -95,7 +96,6 @@ Notifications = {
|
||||||
JsonParse: 952,
|
JsonParse: 952,
|
||||||
// JsonTimeout: 953,
|
// JsonTimeout: 953,
|
||||||
|
|
||||||
UnknownNotification: 998,
|
|
||||||
UnknownError: 999,
|
UnknownError: 999,
|
||||||
|
|
||||||
// Admin
|
// Admin
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,9 @@ MessageSetAction = {
|
||||||
SetSeen: 0,
|
SetSeen: 0,
|
||||||
UnsetSeen: 1,
|
UnsetSeen: 1,
|
||||||
SetFlag: 2,
|
SetFlag: 2,
|
||||||
UnsetFlag: 3
|
UnsetFlag: 3,
|
||||||
|
SetDeleted: 4,
|
||||||
|
UnsetDeleted: 5
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
/* eslint key-spacing: 0 */
|
/* eslint key-spacing: 0 */
|
||||||
/* eslint quote-props: 0 */
|
/* eslint quote-props: 0 */
|
||||||
|
|
||||||
import { arrayLength } from 'Common/Utils';
|
import { arrayLength, pInt } from 'Common/Utils';
|
||||||
|
|
||||||
|
export const RFC822 = 'message/rfc822';
|
||||||
|
|
||||||
const
|
const
|
||||||
cache = {},
|
cache = {},
|
||||||
|
|
@ -12,8 +14,8 @@ const
|
||||||
lowerCase = text => text.toLowerCase().trim(),
|
lowerCase = text => text.toLowerCase().trim(),
|
||||||
|
|
||||||
exts = {
|
exts = {
|
||||||
eml: 'message/rfc822',
|
eml: RFC822,
|
||||||
mime: 'message/rfc822',
|
mime: RFC822,
|
||||||
vcard: 'text/vcard',
|
vcard: 'text/vcard',
|
||||||
vcf: 'text/vcard',
|
vcf: 'text/vcard',
|
||||||
htm: 'text/html',
|
htm: 'text/html',
|
||||||
|
|
@ -28,6 +30,8 @@ const
|
||||||
p7c: app+'pkcs7-mime',
|
p7c: app+'pkcs7-mime',
|
||||||
p7m: app+'pkcs7-mime',
|
p7m: app+'pkcs7-mime',
|
||||||
p7s: app+'pkcs7-signature',
|
p7s: app+'pkcs7-signature',
|
||||||
|
p12: app+'pkcs12',
|
||||||
|
pfx: app+'x-pkcs12',
|
||||||
torrent: app+'x-bittorrent',
|
torrent: app+'x-bittorrent',
|
||||||
|
|
||||||
// scripts
|
// scripts
|
||||||
|
|
@ -116,7 +120,8 @@ export const FileType = {
|
||||||
Spreadsheet: 'spreadsheet',
|
Spreadsheet: 'spreadsheet',
|
||||||
Presentation: 'presentation',
|
Presentation: 'presentation',
|
||||||
Certificate: 'certificate',
|
Certificate: 'certificate',
|
||||||
Archive: 'archive'
|
Archive: 'archive',
|
||||||
|
Calendar: 'calendar'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FileInfo = {
|
export const FileInfo = {
|
||||||
|
|
@ -133,7 +138,7 @@ export const FileInfo = {
|
||||||
getContentType: fileName => {
|
getContentType: fileName => {
|
||||||
fileName = lowerCase(fileName);
|
fileName = lowerCase(fileName);
|
||||||
if ('winmail.dat' === fileName) {
|
if ('winmail.dat' === fileName) {
|
||||||
return app + 'ms-tnef';
|
return app + 'vnd.ms-tnef';
|
||||||
}
|
}
|
||||||
let ext = fileName.split('.').pop();
|
let ext = fileName.split('.').pop();
|
||||||
if (/^(txt|text|def|list|in|ini|log|sql|cfg|conf)$/.test(ext))
|
if (/^(txt|text|def|list|in|ini|log|sql|cfg|conf)$/.test(ext))
|
||||||
|
|
@ -161,7 +166,7 @@ export const FileInfo = {
|
||||||
*/
|
*/
|
||||||
getType: (ext, mimeType) => {
|
getType: (ext, mimeType) => {
|
||||||
ext = lowerCase(ext);
|
ext = lowerCase(ext);
|
||||||
mimeType = lowerCase(mimeType).replace('csv/plain', 'text/csv');
|
mimeType = lowerCase(mimeType).replace('csv/plain', 'text/csv').replace('x-','');
|
||||||
|
|
||||||
let key = ext + mimeType;
|
let key = ext + mimeType;
|
||||||
if (cache[key]) {
|
if (cache[key]) {
|
||||||
|
|
@ -170,7 +175,7 @@ export const FileInfo = {
|
||||||
|
|
||||||
let result = FileType.Unknown;
|
let result = FileType.Unknown;
|
||||||
const mimeTypeParts = mimeType.split('/'),
|
const mimeTypeParts = mimeType.split('/'),
|
||||||
type = mimeTypeParts[1].replace('x-','').replace('-compressed',''),
|
type = mimeTypeParts[1].replace('-compressed',''),
|
||||||
match = str => mimeType.includes(str),
|
match = str => mimeType.includes(str),
|
||||||
archive = /^(zip|7z|tar|rar|gzip|bzip|bzip2)$/;
|
archive = /^(zip|7z|tar|rar|gzip|bzip|bzip2)$/;
|
||||||
|
|
||||||
|
|
@ -187,9 +192,12 @@ export const FileInfo = {
|
||||||
case ['php', 'js', 'css', 'xml', 'html'].includes(ext) || 'text/html' == mimeType:
|
case ['php', 'js', 'css', 'xml', 'html'].includes(ext) || 'text/html' == mimeType:
|
||||||
result = FileType.Code;
|
result = FileType.Code;
|
||||||
break;
|
break;
|
||||||
case 'eml' == ext || ['message/delivery-status', 'message/rfc822'].includes(mimeType):
|
case 'eml' == ext || ['message/delivery-status', RFC822].includes(mimeType):
|
||||||
result = FileType.Eml;
|
result = FileType.Eml;
|
||||||
break;
|
break;
|
||||||
|
case 'ics' == ext || mimeType == 'text/calendar':
|
||||||
|
result = FileType.Calendar;
|
||||||
|
break;
|
||||||
case 'text' == mimeTypeParts[0] || 'txt' == ext || 'log' == ext:
|
case 'text' == mimeTypeParts[0] || 'txt' == ext || 'log' == ext:
|
||||||
result = FileType.Text;
|
result = FileType.Text;
|
||||||
break;
|
break;
|
||||||
|
|
@ -199,9 +207,8 @@ export const FileInfo = {
|
||||||
case 'pdf' == type || 'pdf' == ext:
|
case 'pdf' == type || 'pdf' == ext:
|
||||||
result = FileType.Pdf;
|
result = FileType.Pdf;
|
||||||
break;
|
break;
|
||||||
case [app+'pgp-signature', app+'pgp-keys'].includes(mimeType)
|
case [app+'pgp-signature', app+'pgp-keys', exts.p7m, exts.p7s, exts.p12, exts.pfx].includes(mimeType)
|
||||||
|| ['asc', 'pem', 'ppk'].includes(ext)
|
|| ['asc', 'pem', 'ppk', 'p7s', 'p7m', 'p12', 'pfx'].includes(ext):
|
||||||
|| [app+'pkcs7-signature'].includes(mimeType) || 'p7s' == ext:
|
|
||||||
result = FileType.Certificate;
|
result = FileType.Certificate;
|
||||||
break;
|
break;
|
||||||
case match(msOffice+'.wordprocessingml') || match(openDoc+'.text') || match('vnd.ms-word')
|
case match(msOffice+'.wordprocessingml') || match(openDoc+'.text') || match('vnd.ms-word')
|
||||||
|
|
@ -240,6 +247,7 @@ export const FileInfo = {
|
||||||
case FileType.Certificate:
|
case FileType.Certificate:
|
||||||
case FileType.Spreadsheet:
|
case FileType.Spreadsheet:
|
||||||
case FileType.Presentation:
|
case FileType.Presentation:
|
||||||
|
case FileType.Calendar:
|
||||||
return result + '-' + fileType;
|
return result + '-' + fileType;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -266,8 +274,8 @@ export const FileInfo = {
|
||||||
},
|
},
|
||||||
|
|
||||||
friendlySize: bytes => {
|
friendlySize: bytes => {
|
||||||
bytes = parseInt(bytes, 10) || 0;
|
bytes = pInt(bytes);
|
||||||
let i = Math.floor(Math.log(bytes) / Math.log(1024));
|
let i = bytes ? Math.floor(Math.log(bytes) / Math.log(1024)) : 0;
|
||||||
return (bytes / Math.pow(1024, i)).toFixed(2>i ? 0 : 1) + ' ' + sizes[i];
|
return (bytes / Math.pow(1024, i)).toFixed(2>i ? 0 : 1) + ' ' + sizes[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { isArray, arrayLength } from 'Common/Utils';
|
import { RFC822 } from 'Common/File';
|
||||||
import {
|
import { getFolderInboxName, getFolderFromCacheList } from 'Common/Cache';
|
||||||
getFolderInboxName,
|
import { baseCollator } from 'Common/Translator';
|
||||||
getFolderFromCacheList
|
import { SettingsGet } from 'Common/Globals';
|
||||||
} from 'Common/Cache';
|
import { isArray, arrayLength, pInt } from 'Common/Utils';
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
import { FolderUserStore } from 'Stores/User/Folder';
|
import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
|
|
@ -10,13 +10,13 @@ import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
let refreshInterval,
|
let refreshInterval,
|
||||||
// Default every 5 minutes
|
// Default every 15 minutes
|
||||||
refreshFoldersInterval = 300000;
|
refreshFoldersInterval = 900000;
|
||||||
|
|
||||||
export const
|
export const
|
||||||
|
|
||||||
setRefreshFoldersInterval = minutes => {
|
setRefreshFoldersInterval = minutes => {
|
||||||
refreshFoldersInterval = Math.max(5, minutes) * 60000;
|
refreshFoldersInterval = Math.max(1, pInt(SettingsGet('minRefreshInterval')), pInt(minutes)) * 60000;
|
||||||
clearInterval(refreshInterval);
|
clearInterval(refreshInterval);
|
||||||
refreshInterval = setInterval(() => {
|
refreshInterval = setInterval(() => {
|
||||||
const cF = FolderUserStore.currentFolderFullName(),
|
const cF = FolderUserStore.currentFolderFullName(),
|
||||||
|
|
@ -29,7 +29,7 @@ setRefreshFoldersInterval = minutes => {
|
||||||
|
|
||||||
sortFolders = folders => {
|
sortFolders = folders => {
|
||||||
try {
|
try {
|
||||||
let collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
|
let collator = baseCollator(true);
|
||||||
folders.sort((a, b) =>
|
folders.sort((a, b) =>
|
||||||
a.isInbox() ? -1 : (b.isInbox() ? 1 : collator.compare(a.fullName, b.fullName))
|
a.isInbox() ? -1 : (b.isInbox() ? 1 : collator.compare(a.fullName, b.fullName))
|
||||||
);
|
);
|
||||||
|
|
@ -50,15 +50,14 @@ folderListOptionsBuilder = (
|
||||||
aDisabled,
|
aDisabled,
|
||||||
aHeaderLines,
|
aHeaderLines,
|
||||||
fRenameCallback,
|
fRenameCallback,
|
||||||
fDisableCallback,
|
fDisableCallback
|
||||||
bNoSelectSelectable,
|
|
||||||
aList = FolderUserStore.folderList()
|
|
||||||
) => {
|
) => {
|
||||||
const
|
const
|
||||||
aResult = [],
|
aResult = [],
|
||||||
sDeepPrefix = '\u00A0\u00A0\u00A0',
|
sDeepPrefix = '\u00A0\u00A0\u00A0',
|
||||||
// FolderSystemPopupView should always be true
|
// FolderSystemPopupView should always be true
|
||||||
showUnsubscribed = fRenameCallback ? !SettingsUserStore.hideUnsubscribed() : true,
|
showUnsubscribed = fRenameCallback ? !SettingsUserStore.hideUnsubscribed() : true,
|
||||||
|
isDisabled = fDisableCallback || (item => !item.selectable() || aDisabled.includes(item.fullName)),
|
||||||
|
|
||||||
foldersWalk = folders => {
|
foldersWalk = folders => {
|
||||||
folders.forEach(oItem => {
|
folders.forEach(oItem => {
|
||||||
|
|
@ -69,10 +68,7 @@ folderListOptionsBuilder = (
|
||||||
sDeepPrefix.repeat(oItem.deep) +
|
sDeepPrefix.repeat(oItem.deep) +
|
||||||
fRenameCallback(oItem),
|
fRenameCallback(oItem),
|
||||||
system: false,
|
system: false,
|
||||||
disabled: !bNoSelectSelectable && (
|
disabled: isDisabled(oItem)
|
||||||
!oItem.selectable() ||
|
|
||||||
aDisabled.includes(oItem.fullName) ||
|
|
||||||
fDisableCallback(oItem))
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
foldersWalk(oItem.subFolders());
|
foldersWalk(oItem.subFolders());
|
||||||
|
|
@ -93,7 +89,7 @@ folderListOptionsBuilder = (
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
foldersWalk(aList);
|
foldersWalk(FolderUserStore.folderList());
|
||||||
|
|
||||||
return aResult;
|
return aResult;
|
||||||
},
|
},
|
||||||
|
|
@ -198,12 +194,12 @@ folderInformationMultiply = (boot = false) => {
|
||||||
dropFilesInFolder = (sFolderFullName, files) => {
|
dropFilesInFolder = (sFolderFullName, files) => {
|
||||||
let count = files.length;
|
let count = files.length;
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if ('message/rfc822' === file.type) {
|
if (RFC822 === file.type) {
|
||||||
let data = new FormData;
|
let data = new FormData;
|
||||||
data.append('folder', sFolderFullName);
|
data.append('folder', sFolderFullName);
|
||||||
data.append('appendFile', file);
|
data.append('appendFile', file);
|
||||||
Remote.request('FolderAppend', (iError, data)=>{
|
Remote.request('FolderAppend', (iError, data)=>{
|
||||||
iError && console.error(data.ErrorMessage);
|
iError && console.error(data.message);
|
||||||
0 == --count
|
0 == --count
|
||||||
&& FolderUserStore.currentFolderFullName() == sFolderFullName
|
&& FolderUserStore.currentFolderFullName() == sFolderFullName
|
||||||
&& MessagelistUserStore.reload(true, true);
|
&& MessagelistUserStore.reload(true, true);
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ export const
|
||||||
|
|
||||||
Settings = rl.settings,
|
Settings = rl.settings,
|
||||||
SettingsGet = Settings.get,
|
SettingsGet = Settings.get,
|
||||||
|
SettingsAdmin = name => (SettingsGet('Admin') || {})[name],
|
||||||
SettingsCapa = name => name && !!(SettingsGet('Capa') || {})[name],
|
SettingsCapa = name => name && !!(SettingsGet('Capa') || {})[name],
|
||||||
|
|
||||||
dropdowns = [],
|
dropdowns = [],
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { createElement } from 'Common/Globals';
|
import { createElement } from 'Common/Globals';
|
||||||
import { forEachObjectEntry, pInt } from 'Common/Utils';
|
import { forEachObjectEntry, isArray, pInt } from 'Common/Utils';
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
|
|
||||||
const
|
const
|
||||||
|
|
@ -207,7 +207,9 @@ export const
|
||||||
bqLevel = parseInt(SettingsUserStore.maxBlockquotesLevel()),
|
bqLevel = parseInt(SettingsUserStore.maxBlockquotesLevel()),
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
hasExternals: false
|
hasExternals: false,
|
||||||
|
tracking: false,
|
||||||
|
linkedData: []
|
||||||
},
|
},
|
||||||
|
|
||||||
findAttachmentByCid = cId => oAttachments.findByCid(cId),
|
findAttachmentByCid = cId => oAttachments.findByCid(cId),
|
||||||
|
|
@ -269,12 +271,15 @@ export const
|
||||||
// Not supported by <template> element
|
// Not supported by <template> element
|
||||||
// .replace(/<!doctype[^>]*>/gi, '')
|
// .replace(/<!doctype[^>]*>/gi, '')
|
||||||
// .replace(/<\?xml[^>]*\?>/gi, '')
|
// .replace(/<\?xml[^>]*\?>/gi, '')
|
||||||
|
.replace(/<(\/?)head(\s[^>]*)?>/gi, '')
|
||||||
.replace(/<(\/?)body(\s[^>]*)?>/gi, '<$1div class="mail-body"$2>')
|
.replace(/<(\/?)body(\s[^>]*)?>/gi, '<$1div class="mail-body"$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>/, '')
|
||||||
// https://github.com/the-djmaze/snappymail/issues/900
|
// https://github.com/the-djmaze/snappymail/issues/900
|
||||||
.replace(/\u2028/g,' ')
|
.replace(/\u2028/g,' ')
|
||||||
|
// https://github.com/the-djmaze/snappymail/issues/1415
|
||||||
|
.replace(/<br>\s*<\/p>/gi,'</p>')
|
||||||
.trim();
|
.trim();
|
||||||
html = '';
|
html = '';
|
||||||
|
|
||||||
|
|
@ -284,6 +289,21 @@ export const
|
||||||
nodeIterator.referenceNode.remove();
|
nodeIterator.referenceNode.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic support for Linked Data (Structured Email)
|
||||||
|
* https://json-ld.org/
|
||||||
|
* https://structured.email/
|
||||||
|
**/
|
||||||
|
tmpl.content.querySelectorAll('script[type="application/ld+json"]').forEach(oElement => {
|
||||||
|
// Could be array of objects or single object
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(oElement.textContent);
|
||||||
|
(isArray(data) ? data : [data]).forEach(entry => result.linkedData.push(entry));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e, oElement.textContent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
tmpl.content.querySelectorAll(
|
tmpl.content.querySelectorAll(
|
||||||
disallowedTags
|
disallowedTags
|
||||||
+ (0 < bqLevel ? ',' + (new Array(1 + bqLevel).fill('blockquote').join(' ')) : '')
|
+ (0 < bqLevel ? ',' + (new Array(1 + bqLevel).fill('blockquote').join(' ')) : '')
|
||||||
|
|
@ -292,6 +312,18 @@ export const
|
||||||
// https://github.com/the-djmaze/snappymail/issues/1125
|
// https://github.com/the-djmaze/snappymail/issues/1125
|
||||||
tmpl.content.querySelectorAll('form,button').forEach(oElement => replaceWithChildren(oElement));
|
tmpl.content.querySelectorAll('form,button').forEach(oElement => replaceWithChildren(oElement));
|
||||||
|
|
||||||
|
// https://github.com/the-djmaze/snappymail/issues/1641
|
||||||
|
let body = tmpl.content.querySelector('.mail-body');
|
||||||
|
[...tmpl.content.querySelectorAll('.mail-body + .mail-body')]
|
||||||
|
.forEach(oElement => body.append(...oElement.childNodes));
|
||||||
|
/*
|
||||||
|
.forEach(oElement => {
|
||||||
|
let bq = createElement('blockquote');
|
||||||
|
bq.append(...oElement.childNodes);
|
||||||
|
body.replaceWith(bq);
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
[...tmpl.content.querySelectorAll('*')].forEach(oElement => {
|
[...tmpl.content.querySelectorAll('*')].forEach(oElement => {
|
||||||
const name = oElement.tagName,
|
const name = oElement.tagName,
|
||||||
oStyle = oElement.style;
|
oStyle = oElement.style;
|
||||||
|
|
@ -381,10 +413,14 @@ export const
|
||||||
if ('A' === name) {
|
if ('A' === name) {
|
||||||
value = oElement.href;
|
value = oElement.href;
|
||||||
if (!/^([a-z]+):/i.test(value)) {
|
if (!/^([a-z]+):/i.test(value)) {
|
||||||
setAttribute('data-x-broken-href', value);
|
setAttribute('data-x-href-broken', value);
|
||||||
delAttribute('href');
|
delAttribute('href');
|
||||||
} else {
|
} else {
|
||||||
oElement.href = stripTracking(value);
|
oElement.href = stripTracking(value);
|
||||||
|
if (oElement.href != value) {
|
||||||
|
result.tracking = true;
|
||||||
|
setAttribute('data-x-href-tracking', value);
|
||||||
|
}
|
||||||
setAttribute('target', '_blank');
|
setAttribute('target', '_blank');
|
||||||
// setAttribute('rel', 'external nofollow noopener noreferrer');
|
// setAttribute('rel', 'external nofollow noopener noreferrer');
|
||||||
}
|
}
|
||||||
|
|
@ -406,9 +442,8 @@ export const
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let skipStyle = false;
|
let skipStyle = false;
|
||||||
if (hasAttribute('src')) {
|
value = delAttribute('src');
|
||||||
value = stripTracking(delAttribute('src'));
|
if (value) {
|
||||||
|
|
||||||
if ('IMG' === name) {
|
if ('IMG' === name) {
|
||||||
oElement.loading = 'lazy';
|
oElement.loading = 'lazy';
|
||||||
let attachment;
|
let attachment;
|
||||||
|
|
@ -445,12 +480,18 @@ export const
|
||||||
oStyle.display = 'none';
|
oStyle.display = 'none';
|
||||||
// setAttribute('style', 'display:none');
|
// setAttribute('style', 'display:none');
|
||||||
setAttribute('data-x-src-hidden', value);
|
setAttribute('data-x-src-hidden', value);
|
||||||
|
// result.tracking = true;
|
||||||
}
|
}
|
||||||
else if (httpre.test(value))
|
else if (httpre.test(value))
|
||||||
{
|
{
|
||||||
setAttribute('data-x-src', value);
|
let src = stripTracking(value);
|
||||||
|
if (src != value) {
|
||||||
|
result.tracking = true;
|
||||||
|
setAttribute('data-x-src-tracking', value);
|
||||||
|
}
|
||||||
|
setAttribute('data-x-src', src);
|
||||||
result.hasExternals = true;
|
result.hasExternals = true;
|
||||||
oElement.alt || (oElement.alt = value.replace(/^.+\/([^/?]+).*$/, '$1').slice(-20));
|
oElement.alt || (oElement.alt = src.replace(/^.+\/([^/?]+).*$/, '$1').slice(-20));
|
||||||
}
|
}
|
||||||
else if (value.startsWith('data:image/'))
|
else if (value.startsWith('data:image/'))
|
||||||
{
|
{
|
||||||
|
|
@ -582,6 +623,8 @@ export const
|
||||||
html = html
|
html = html
|
||||||
.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gim, (...args) =>
|
.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gim, (...args) =>
|
||||||
1 < args.length ? args[1].toString().replace(/\n/g, '<br>') : '')
|
1 < args.length ? args[1].toString().replace(/\n/g, '<br>') : '')
|
||||||
|
// Remove line duplication
|
||||||
|
.replace(/<br><\/div>/gi, '</div>')
|
||||||
.replace(/\r?\n/g, '')
|
.replace(/\r?\n/g, '')
|
||||||
.replace(/\s+/gm, ' ');
|
.replace(/\s+/gm, ' ');
|
||||||
|
|
||||||
|
|
@ -721,184 +764,7 @@ export const
|
||||||
.replace(/\n/g, '<br>');
|
.replace(/\n/g, '<br>');
|
||||||
blockquoteSwitcher();
|
blockquoteSwitcher();
|
||||||
return tmpl.innerHTML.trim();
|
return tmpl.innerHTML.trim();
|
||||||
},
|
};
|
||||||
|
|
||||||
WYSIWYGS = ko.observableArray();
|
|
||||||
|
|
||||||
WYSIWYGS.push(['Squire', (owner, container, onReady)=>{
|
|
||||||
let squire = new SquireUI(container);
|
|
||||||
setTimeout(()=>onReady(squire), 1);
|
|
||||||
/*
|
|
||||||
squire.on('blur', () => owner.blurTrigger());
|
|
||||||
squire.on('focus', () => clearTimeout(owner.blurTimer));
|
|
||||||
squire.on('mode', () => {
|
|
||||||
owner.blurTrigger();
|
|
||||||
owner.onModeChange?.(!owner.isPlain());
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
}]);
|
|
||||||
|
|
||||||
rl.registerWYSIWYG = (name, construct) => WYSIWYGS.push([name, construct]);
|
|
||||||
|
|
||||||
export class HtmlEditor {
|
|
||||||
/**
|
|
||||||
* @param {Object} element
|
|
||||||
* @param {Function=} onBlur
|
|
||||||
* @param {Function=} onReady
|
|
||||||
* @param {Function=} onModeChange
|
|
||||||
*/
|
|
||||||
constructor(element, onBlur = null, onReady = null, onModeChange = null) {
|
|
||||||
this.blurTimer = 0;
|
|
||||||
|
|
||||||
this.onBlur = onBlur;
|
|
||||||
this.onModeChange = onModeChange;
|
|
||||||
|
|
||||||
if (element) {
|
|
||||||
onReady = onReady ? [onReady] : [];
|
|
||||||
this.onReady = fn => onReady.push(fn);
|
|
||||||
// TODO: make 'which' user configurable
|
|
||||||
// const which = 'CKEditor4',
|
|
||||||
// wysiwyg = WYSIWYGS.find(item => which == item[0]) || WYSIWYGS.find(item => 'Squire' == item[0]);
|
|
||||||
const wysiwyg = WYSIWYGS.find(item => 'Squire' == item[0]);
|
|
||||||
wysiwyg[1](this, element, editor => {
|
|
||||||
this.editor = editor;
|
|
||||||
editor.on('blur', () => this.blurTrigger());
|
|
||||||
editor.on('focus', () => clearTimeout(this.blurTimer));
|
|
||||||
editor.on('mode', () => {
|
|
||||||
this.blurTrigger();
|
|
||||||
this.onModeChange?.(!this.isPlain());
|
|
||||||
});
|
|
||||||
this.onReady = fn => fn();
|
|
||||||
onReady.forEach(fn => fn());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
blurTrigger() {
|
|
||||||
if (this.onBlur) {
|
|
||||||
clearTimeout(this.blurTimer);
|
|
||||||
this.blurTimer = setTimeout(() => this.onBlur?.(), 200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
isHtml() {
|
|
||||||
return this.editor ? !this.isPlain() : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
isPlain() {
|
|
||||||
return this.editor ? 'plain' === this.editor.mode : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
clearCachedSignature() {
|
|
||||||
this.onReady(() => this.editor.execCommand('insertSignature', {
|
|
||||||
clearCache: true
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} signature
|
|
||||||
* @param {bool} html
|
|
||||||
* @param {bool} insertBefore
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
setSignature(signature, html, insertBefore = false) {
|
|
||||||
this.onReady(() => this.editor.execCommand('insertSignature', {
|
|
||||||
isHtml: html,
|
|
||||||
insertBefore: insertBefore,
|
|
||||||
signature: signature
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {boolean=} wrapIsHtml = false
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
getData() {
|
|
||||||
let result = '';
|
|
||||||
if (this.editor) {
|
|
||||||
try {
|
|
||||||
if (this.isPlain() && this.editor.plugins.plain && this.editor.__plain) {
|
|
||||||
result = this.editor.__plain.getRawData();
|
|
||||||
} else {
|
|
||||||
result = this.editor.getData();
|
|
||||||
}
|
|
||||||
} catch (e) {} // eslint-disable-line no-empty
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
getDataWithHtmlMark() {
|
|
||||||
return (this.isHtml() ? ':HTML:' : '') + this.getData();
|
|
||||||
}
|
|
||||||
|
|
||||||
modeWysiwyg() {
|
|
||||||
this.onReady(() => this.editor.setMode('wysiwyg'));
|
|
||||||
}
|
|
||||||
modePlain() {
|
|
||||||
this.onReady(() => this.editor.setMode('plain'));
|
|
||||||
}
|
|
||||||
|
|
||||||
setHtmlOrPlain(text) {
|
|
||||||
text.startsWith(':HTML:')
|
|
||||||
? this.setHtml(text.slice(6))
|
|
||||||
: this.setPlain(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
setData(mode, data) {
|
|
||||||
this.onReady(() => {
|
|
||||||
const editor = this.editor;
|
|
||||||
this.clearCachedSignature();
|
|
||||||
try {
|
|
||||||
editor.setMode(mode);
|
|
||||||
if (this.isPlain() && editor.plugins.plain && editor.__plain) {
|
|
||||||
editor.__plain.setRawData(data);
|
|
||||||
} else {
|
|
||||||
editor.setData(data);
|
|
||||||
}
|
|
||||||
} catch (e) { console.error(e); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setHtml(html) {
|
|
||||||
this.setData('wysiwyg', html/*.replace(/<p[^>]*><\/p>/gi, '')*/);
|
|
||||||
}
|
|
||||||
|
|
||||||
setPlain(txt) {
|
|
||||||
this.setData('plain', txt);
|
|
||||||
}
|
|
||||||
|
|
||||||
focus() {
|
|
||||||
this.onReady(() => this.editor.focus());
|
|
||||||
}
|
|
||||||
|
|
||||||
hasFocus() {
|
|
||||||
try {
|
|
||||||
return !!this.editor?.focusManager.hasFocus;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
blur() {
|
|
||||||
this.onReady(() => this.editor.focusManager.blur(true));
|
|
||||||
}
|
|
||||||
|
|
||||||
clear() {
|
|
||||||
this.onReady(() => this.isPlain() ? this.setPlain('') : this.setHtml(''));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rl.Utils = {
|
rl.Utils = {
|
||||||
htmlToPlain: htmlToPlain,
|
htmlToPlain: htmlToPlain,
|
||||||
|
|
|
||||||
161
dev/Common/HtmlEditor.js
Normal file
161
dev/Common/HtmlEditor.js
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
|
|
||||||
|
export const
|
||||||
|
WYSIWYGS = ko.observableArray();
|
||||||
|
|
||||||
|
WYSIWYGS.push({
|
||||||
|
name: 'Squire',
|
||||||
|
construct: (owner, container, onReady) => onReady(new SquireUI(container))
|
||||||
|
});
|
||||||
|
|
||||||
|
rl.registerWYSIWYG = (name, construct) => WYSIWYGS.push({name, construct});
|
||||||
|
|
||||||
|
export class HtmlEditor {
|
||||||
|
/**
|
||||||
|
* @param {Object} element
|
||||||
|
* @param {Function=} onBlur
|
||||||
|
* @param {Function=} onReady
|
||||||
|
* @param {Function=} onModeChange
|
||||||
|
*/
|
||||||
|
constructor(element, onReady = null, onModeChange = null, onBlur = null) {
|
||||||
|
this.blurTimer = 0;
|
||||||
|
|
||||||
|
this.onBlur = onBlur;
|
||||||
|
this.onModeChange = onModeChange;
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
onReady = onReady ? [onReady] : [];
|
||||||
|
this.onReady = fn => onReady.push(fn);
|
||||||
|
const which = SettingsUserStore.editorWysiwyg(),
|
||||||
|
wysiwyg = WYSIWYGS.find(item => which == item.name) || WYSIWYGS.find(item => 'Squire' == item.name);
|
||||||
|
wysiwyg.construct(this, element, editor => setTimeout(()=>{
|
||||||
|
this.editor = editor;
|
||||||
|
editor.on('blur', () => this.blurTrigger());
|
||||||
|
editor.on('focus', () => clearTimeout(this.blurTimer));
|
||||||
|
editor.on('mode', () => {
|
||||||
|
this.blurTrigger();
|
||||||
|
this.onModeChange?.(!this.isPlain());
|
||||||
|
});
|
||||||
|
this.onReady = fn => fn();
|
||||||
|
onReady.forEach(fn => fn());
|
||||||
|
},1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blurTrigger() {
|
||||||
|
if (this.onBlur) {
|
||||||
|
clearTimeout(this.blurTimer);
|
||||||
|
this.blurTimer = setTimeout(() => this.onBlur?.(), 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isHtml() {
|
||||||
|
return this.editor ? !this.isPlain() : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isPlain() {
|
||||||
|
return this.editor ? 'plain' === this.editor.mode : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
clearCachedSignature() {
|
||||||
|
this.onReady(() => this.editor.execCommand('insertSignature', {
|
||||||
|
clearCache: true
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} signature
|
||||||
|
* @param {bool} html
|
||||||
|
* @param {bool} insertBefore
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
setSignature(signature, html, insertBefore = false) {
|
||||||
|
this.onReady(() => this.editor.execCommand('insertSignature', {
|
||||||
|
isHtml: html,
|
||||||
|
insertBefore: insertBefore,
|
||||||
|
signature: signature
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {boolean=} wrapIsHtml = false
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getData() {
|
||||||
|
let result = '';
|
||||||
|
if (this.editor) {
|
||||||
|
try {
|
||||||
|
if (this.isPlain()) {
|
||||||
|
result = this.editor.getPlainData();
|
||||||
|
} else {
|
||||||
|
result = this.editor.getData();
|
||||||
|
}
|
||||||
|
} catch (e) {} // eslint-disable-line no-empty
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getDataWithHtmlMark() {
|
||||||
|
return (this.isHtml() ? ':HTML:' : '') + this.getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
modeWysiwyg() {
|
||||||
|
this.onReady(() => this.editor.setMode('wysiwyg'));
|
||||||
|
}
|
||||||
|
modePlain() {
|
||||||
|
this.onReady(() => this.editor.setMode('plain'));
|
||||||
|
}
|
||||||
|
|
||||||
|
setHtmlOrPlain(text) {
|
||||||
|
text.startsWith(':HTML:')
|
||||||
|
? this.setHtml(text.slice(6))
|
||||||
|
: this.setPlain(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(mode, data) {
|
||||||
|
this.onReady(() => {
|
||||||
|
const editor = this.editor;
|
||||||
|
this.clearCachedSignature();
|
||||||
|
try {
|
||||||
|
editor.setMode(mode);
|
||||||
|
if (this.isPlain()) {
|
||||||
|
editor.setPlainData(data);
|
||||||
|
} else {
|
||||||
|
editor.setData(data);
|
||||||
|
}
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setHtml(html) {
|
||||||
|
this.setData('wysiwyg', html/*.replace(/<p[^>]*><\/p>/gi, '')*/);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPlain(txt) {
|
||||||
|
this.setData('plain', txt);
|
||||||
|
}
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
this.onReady(() => this.editor.focus());
|
||||||
|
}
|
||||||
|
|
||||||
|
blur() {
|
||||||
|
this.onReady(() => this.editor.blur());
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.onReady(() => this.isPlain() ? this.setPlain('') : this.setHtml(''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { pInt } from 'Common/Utils';
|
import { pInt } from 'Common/Utils';
|
||||||
import { doc, Settings } from 'Common/Globals';
|
import { doc, Settings, SettingsAdmin } from 'Common/Globals';
|
||||||
|
|
||||||
const
|
const
|
||||||
BASE = doc.location.pathname.replace(/\/+$/,'') + '/',
|
BASE = doc.location.pathname.replace(/\/+$/,'') + '/',
|
||||||
HASH_PREFIX = '#/',
|
HASH_PREFIX = '#/',
|
||||||
|
|
||||||
adminPath = () => rl.adminArea() && !Settings.app('adminHost'),
|
adminPath = () => rl.adminArea() && !SettingsAdmin('host'),
|
||||||
|
|
||||||
prefix = () => BASE + '?' + (adminPath() ? Settings.app('adminPath') : '');
|
prefix = () => BASE + '?' + (adminPath() ? SettingsAdmin('path') : '');
|
||||||
|
|
||||||
export const
|
export const
|
||||||
SUB_QUERY_PREFIX = '&q[]=',
|
SUB_QUERY_PREFIX = '&q[]=',
|
||||||
|
|
@ -38,11 +38,10 @@ export const
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} download
|
* @param {string} download
|
||||||
* @param {string=} customSpecSuffix
|
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
attachmentDownload = (download, customSpecSuffix) =>
|
attachmentDownload = (download) =>
|
||||||
serverRequestRaw('Download', download, customSpecSuffix),
|
serverRequestRaw('Download', download),
|
||||||
|
|
||||||
proxy = url =>
|
proxy = url =>
|
||||||
BASE + '?/ProxyExternal/'
|
BASE + '?/ProxyExternal/'
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { koComputable } from 'External/ko';
|
||||||
oCallbacks:
|
oCallbacks:
|
||||||
ItemSelect
|
ItemSelect
|
||||||
MiddleClick
|
MiddleClick
|
||||||
AutoSelect
|
canSelect
|
||||||
ItemGetUid
|
ItemGetUid
|
||||||
UpOrDown
|
UpOrDown
|
||||||
*/
|
*/
|
||||||
|
|
@ -21,15 +21,13 @@ export class Selector {
|
||||||
* @param {koProperty} koFocusedItem
|
* @param {koProperty} koFocusedItem
|
||||||
* @param {string} sItemSelector
|
* @param {string} sItemSelector
|
||||||
* @param {string} sItemCheckedSelector
|
* @param {string} sItemCheckedSelector
|
||||||
* @param {string} sItemFocusedSelector
|
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
koList,
|
koList,
|
||||||
koSelectedItem,
|
koSelectedItem,
|
||||||
koFocusedItem,
|
koFocusedItem,
|
||||||
sItemSelector,
|
sItemSelector,
|
||||||
sItemCheckedSelector,
|
sItemCheckedSelector
|
||||||
sItemFocusedSelector
|
|
||||||
) {
|
) {
|
||||||
koFocusedItem = (koFocusedItem || ko.observable(null)).extend({ toggleSubscribeProperty: [this, 'focused'] });
|
koFocusedItem = (koFocusedItem || ko.observable(null)).extend({ toggleSubscribeProperty: [this, 'focused'] });
|
||||||
koSelectedItem = (koSelectedItem || ko.observable(null)).extend({ toggleSubscribeProperty: [null, 'selected'] });
|
koSelectedItem = (koSelectedItem || ko.observable(null)).extend({ toggleSubscribeProperty: [null, 'selected'] });
|
||||||
|
|
@ -46,7 +44,7 @@ export class Selector {
|
||||||
|
|
||||||
this.sItemSelector = sItemSelector;
|
this.sItemSelector = sItemSelector;
|
||||||
this.sItemCheckedSelector = sItemCheckedSelector;
|
this.sItemCheckedSelector = sItemCheckedSelector;
|
||||||
this.sItemFocusedSelector = sItemFocusedSelector;
|
this.sItemFocusedSelector = sItemSelector + '.focused';
|
||||||
|
|
||||||
this.sLastUid = '';
|
this.sLastUid = '';
|
||||||
this.oCallbacks = {};
|
this.oCallbacks = {};
|
||||||
|
|
@ -74,7 +72,7 @@ export class Selector {
|
||||||
|
|
||||||
koSelectedItem.subscribe(item => {
|
koSelectedItem.subscribe(item => {
|
||||||
if (item) {
|
if (item) {
|
||||||
koList.forEach(subItem => subItem.checked(false));
|
// koList.forEach(subItem => subItem.checked(false));
|
||||||
selectedItemUseCallback && itemSelectedThrottle(item);
|
selectedItemUseCallback && itemSelectedThrottle(item);
|
||||||
} else {
|
} else {
|
||||||
selectedItemUseCallback && itemSelected();
|
selectedItemUseCallback && itemSelected();
|
||||||
|
|
@ -120,7 +118,8 @@ export class Selector {
|
||||||
|
|
||||||
if (isArray(aItems)) {
|
if (isArray(aItems)) {
|
||||||
let temp,
|
let temp,
|
||||||
isChecked;
|
isChecked,
|
||||||
|
next = this.iFocusedNextHelper || this.iSelectNextHelper;
|
||||||
|
|
||||||
aItems.forEach(item => {
|
aItems.forEach(item => {
|
||||||
const uid = this.getItemUid(item);
|
const uid = this.getItemUid(item);
|
||||||
|
|
@ -145,24 +144,10 @@ export class Selector {
|
||||||
|
|
||||||
selectedItemUseCallback = true;
|
selectedItemUseCallback = true;
|
||||||
|
|
||||||
if (
|
if (next && aItems.length && !koFocusedItem()) {
|
||||||
(this.iSelectNextHelper || this.iFocusedNextHelper) &&
|
temp = aItems[-1 === next ? aItems.length - 1 : 0];
|
||||||
aItems.length &&
|
|
||||||
!koFocusedItem()
|
|
||||||
) {
|
|
||||||
temp = null;
|
|
||||||
if (this.iFocusedNextHelper) {
|
|
||||||
temp = aItems[-1 === this.iFocusedNextHelper ? aItems.length - 1 : 0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!temp && this.iSelectNextHelper) {
|
|
||||||
temp = aItems[-1 === this.iSelectNextHelper ? aItems.length - 1 : 0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (temp) {
|
if (temp) {
|
||||||
if (this.iSelectNextHelper) {
|
this.iSelectNextHelper && koSelectedItem(temp);
|
||||||
koSelectedItem(temp);
|
|
||||||
}
|
|
||||||
|
|
||||||
koFocusedItem(temp);
|
koFocusedItem(temp);
|
||||||
|
|
||||||
|
|
@ -200,10 +185,11 @@ export class Selector {
|
||||||
|
|
||||||
addEventsListeners(contentScrollable, {
|
addEventsListeners(contentScrollable, {
|
||||||
click: event => {
|
click: event => {
|
||||||
let el = event.target.closestWithin(this.sItemSelector, contentScrollable);
|
const el = event.target.closestWithin(this.sItemSelector, contentScrollable);
|
||||||
el && this.actionClick(ko.dataFor(el), event);
|
let item = el && ko.dataFor(el);
|
||||||
|
el && (this.oCallbacks.click || (()=>1))(event, item) && this.actionClick(item, event);
|
||||||
|
|
||||||
const item = getItem(this.sItemCheckedSelector);
|
item = getItem(this.sItemCheckedSelector);
|
||||||
if (item) {
|
if (item) {
|
||||||
if (event.shiftKey) {
|
if (event.shiftKey) {
|
||||||
this.actionClick(item, event);
|
this.actionClick(item, event);
|
||||||
|
|
@ -249,7 +235,7 @@ export class Selector {
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
autoSelect(bForce) {
|
autoSelect(bForce) {
|
||||||
(bForce || (this.oCallbacks.AutoSelect || (()=>1))())
|
(bForce || (this.oCallbacks.canSelect || (()=>1))())
|
||||||
&& this.focusedItem()
|
&& this.focusedItem()
|
||||||
&& this.selectedItem(this.focusedItem());
|
&& this.selectedItem(this.focusedItem());
|
||||||
}
|
}
|
||||||
|
|
@ -268,10 +254,11 @@ export class Selector {
|
||||||
* @param {boolean=} bForceSelect = false
|
* @param {boolean=} bForceSelect = false
|
||||||
*/
|
*/
|
||||||
newSelectPosition(sEventKey, bShiftKey, bForceSelect) {
|
newSelectPosition(sEventKey, bShiftKey, bForceSelect) {
|
||||||
let isArrow = 'ArrowUp' === sEventKey || 'ArrowDown' === sEventKey,
|
let result;
|
||||||
result;
|
|
||||||
|
|
||||||
const pageStep = 10,
|
const up = 'ArrowUp' === sEventKey,
|
||||||
|
isArrow = up || 'ArrowDown' === sEventKey,
|
||||||
|
pageStep = 10,
|
||||||
list = this.list(),
|
list = this.list(),
|
||||||
listLen = list.length,
|
listLen = list.length,
|
||||||
focused = this.focusedItem();
|
focused = this.focusedItem();
|
||||||
|
|
@ -283,8 +270,7 @@ export class Selector {
|
||||||
} else if (listLen) {
|
} else if (listLen) {
|
||||||
if (focused) {
|
if (focused) {
|
||||||
if (isArrow) {
|
if (isArrow) {
|
||||||
let i = list.indexOf(focused),
|
let i = list.indexOf(focused);
|
||||||
up = 'ArrowUp' == sEventKey;
|
|
||||||
if (bShiftKey) {
|
if (bShiftKey) {
|
||||||
shiftStart = -1 < shiftStart ? shiftStart : i;
|
shiftStart = -1 < shiftStart ? shiftStart : i;
|
||||||
shiftStart == i
|
shiftStart == i
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ const
|
||||||
|
|
||||||
getNotificationMessage = code => {
|
getNotificationMessage = code => {
|
||||||
let key = getKeyByValue(Notifications, code);
|
let key = getKeyByValue(Notifications, code);
|
||||||
return key ? I18N_DATA.NOTIFICATIONS[i18nKey(key).replace('_NOTIFICATION', '_ERROR')] : '';
|
return key ? I18N_DATA.NOTIFICATIONS[key] : '';
|
||||||
},
|
},
|
||||||
|
|
||||||
fromNow = date => relativeTime(Math.round((date.getTime() - Date.now()) / 1000));
|
fromNow = date => relativeTime(Math.round((date.getTime() - Date.now()) / 1000));
|
||||||
|
|
@ -45,8 +45,7 @@ export const
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Intl.RelativeTimeFormat) {
|
if (Intl.RelativeTimeFormat) {
|
||||||
let rtf = new Intl.RelativeTimeFormat(doc.documentElement.lang);
|
return (new Intl.RelativeTimeFormat(doc.documentElement.lang)).format(seconds, unit);
|
||||||
return rtf.format(seconds, unit);
|
|
||||||
}
|
}
|
||||||
// Safari < 14
|
// Safari < 14
|
||||||
abs = Math.abs(seconds);
|
abs = Math.abs(seconds);
|
||||||
|
|
@ -62,7 +61,7 @@ export const
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
i18n = (key, valueList, defaulValue) => {
|
i18n = (key, valueList, defaulValue) => {
|
||||||
let result = null == defaulValue ? key : defaulValue;
|
let result = defaulValue ?? key;
|
||||||
let path = key.split('/');
|
let path = key.split('/');
|
||||||
if (I18N_DATA[path[0]] && path[1]) {
|
if (I18N_DATA[path[0]] && path[1]) {
|
||||||
result = I18N_DATA[path[0]][path[1]] || result;
|
result = I18N_DATA[path[0]][path[1]] || result;
|
||||||
|
|
@ -102,8 +101,7 @@ export const
|
||||||
|
|
||||||
timestampToString = (timeStampInUTC, formatStr) => {
|
timestampToString = (timeStampInUTC, formatStr) => {
|
||||||
const now = Date.now(),
|
const now = Date.now(),
|
||||||
time = 0 < timeStampInUTC ? Math.min(now, timeStampInUTC * 1000) : (0 === timeStampInUTC ? now : 0);
|
time = 0 < timeStampInUTC ? timeStampInUTC * 1000 : (0 === timeStampInUTC ? now : 0);
|
||||||
|
|
||||||
if (31536000000 < time) {
|
if (31536000000 < time) {
|
||||||
const m = new Date(time), h = LanguageStore.hourCycle();
|
const m = new Date(time), h = LanguageStore.hourCycle();
|
||||||
switch (formatStr) {
|
switch (formatStr) {
|
||||||
|
|
@ -144,7 +142,7 @@ export const
|
||||||
time = Date.parse(element.dateTime) / 1000;
|
time = Date.parse(element.dateTime) / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
let key = element.dataset.momentFormat;
|
let key = element.dataset.timeFormat;
|
||||||
if (key) {
|
if (key) {
|
||||||
element.textContent = timestampToString(time, key);
|
element.textContent = timestampToString(time, key);
|
||||||
if ('FULL' !== key && 'FROMNOW' !== key) {
|
if ('FULL' !== key && 'FROMNOW' !== key) {
|
||||||
|
|
@ -186,6 +184,9 @@ export const
|
||||||
|| '';
|
|| '';
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getErrorMessage = (code, data) =>
|
||||||
|
getNotification(code) || data?.messageAdditional || data?.message || data,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {*} code
|
* @param {*} code
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
|
|
@ -212,14 +213,13 @@ export const
|
||||||
script.remove();
|
script.remove();
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
script.onerror = () => reject(new Error('Language '+language+' failed'));
|
script.onerror = () => reject(Error('Language '+language+' failed'));
|
||||||
script.src = langLink(language, admin);
|
script.src = langLink(language, admin);
|
||||||
// script.async = true;
|
// script.async = true;
|
||||||
doc.head.append(script);
|
doc.head.append(script);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @param {string} language
|
* @param {string} language
|
||||||
* @param {boolean=} isEng = false
|
* @param {boolean=} isEng = false
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
|
|
@ -229,6 +229,8 @@ export const
|
||||||
'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language,
|
'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language,
|
||||||
null,
|
null,
|
||||||
language
|
language
|
||||||
);
|
),
|
||||||
|
|
||||||
|
baseCollator = numeric => new Intl.Collator(doc.documentElement.lang, {numeric: !!numeric, sensitivity: 'base'});
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export const
|
||||||
|
|
||||||
pInt = (value, defaultValue = 0) => {
|
pInt = (value, defaultValue = 0) => {
|
||||||
value = parseInt(value, 10);
|
value = parseInt(value, 10);
|
||||||
return isNaN(value) || !isFinite(value) ? defaultValue : value;
|
return isFinite(value) ? value : defaultValue;
|
||||||
},
|
},
|
||||||
|
|
||||||
defaultOptionsAfterRender = (domItem, item) =>
|
defaultOptionsAfterRender = (domItem, item) =>
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,57 @@ import { ThemeStore } from 'Stores/Theme';
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
import { attachmentDownload } from 'Common/Links';
|
import { attachmentDownload } from 'Common/Links';
|
||||||
|
|
||||||
|
import { AccountModel } from 'Model/Account';
|
||||||
|
import { IdentityModel } from 'Model/Identity';
|
||||||
|
import { AccountUserStore } from 'Stores/User/Account';
|
||||||
|
import { IdentityUserStore } from 'Stores/User/Identity';
|
||||||
|
import { isArray } from 'Common/Utils';
|
||||||
|
|
||||||
|
import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
|
import { IdentityPopupView } from 'View/Popup/Identity';
|
||||||
|
|
||||||
export const
|
export const
|
||||||
|
|
||||||
moveAction = ko.observable(false),
|
// 1 = move, 2 = copy
|
||||||
|
moveAction = ko.observable(0),
|
||||||
|
|
||||||
dropdownsDetectVisibility = (() =>
|
dropdownsDetectVisibility = (() =>
|
||||||
dropdownVisibility(!!dropdowns.find(item => item.classList.contains('show')))
|
dropdownVisibility(!!dropdowns.find(item => item.classList.contains('show')))
|
||||||
).debounce(50),
|
).debounce(50),
|
||||||
|
|
||||||
|
|
||||||
|
editIdentity = Identity => showScreenPopup(IdentityPopupView, [Identity]),
|
||||||
|
|
||||||
|
loadAccountsAndIdentities = () => {
|
||||||
|
AccountUserStore.loading(true);
|
||||||
|
IdentityUserStore.loading(true);
|
||||||
|
|
||||||
|
Remote.request('AccountsAndIdentities', (iError, oData) => {
|
||||||
|
AccountUserStore.loading(false);
|
||||||
|
IdentityUserStore.loading(false);
|
||||||
|
|
||||||
|
if (!iError) {
|
||||||
|
let items = oData.Result.Accounts;
|
||||||
|
AccountUserStore(isArray(items)
|
||||||
|
? items.map(oValue => new AccountModel(oValue.email, oValue.name))
|
||||||
|
: []
|
||||||
|
);
|
||||||
|
AccountUserStore.unshift(new AccountModel(SettingsGet('mainEmail'), '', false));
|
||||||
|
|
||||||
|
items = oData.Result.Identities;
|
||||||
|
IdentityUserStore(isArray(items)
|
||||||
|
? items.map(identityData => IdentityModel.reviveFromJson(identityData))
|
||||||
|
: []
|
||||||
|
);
|
||||||
|
|
||||||
|
// Invoke "Update Identity" pop up right after login
|
||||||
|
// https://github.com/the-djmaze/snappymail/issues/1689
|
||||||
|
const main = IdentityUserStore.main();
|
||||||
|
main && !main.exists() && setTimeout(()=>editIdentity(main), 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} link
|
* @param {string} link
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
|
|
@ -89,7 +132,7 @@ computedPaginatorHelper = (koCurrentPage, koPageCount) => {
|
||||||
next = 0,
|
next = 0,
|
||||||
limit = 2;
|
limit = 2;
|
||||||
|
|
||||||
if (1 < pageCount || (0 < pageCount && pageCount < currentPage)) {
|
if (1 < pageCount) {
|
||||||
if (pageCount < currentPage) {
|
if (pageCount < currentPage) {
|
||||||
fAdd(pageCount);
|
fAdd(pageCount);
|
||||||
prev = pageCount;
|
prev = pageCount;
|
||||||
|
|
@ -248,7 +291,7 @@ setLayoutResizer = (source, sClientSideKeyName, mode) =>
|
||||||
|
|
||||||
viewMessage = (oMessage, popup) => {
|
viewMessage = (oMessage, popup) => {
|
||||||
if (popup) {
|
if (popup) {
|
||||||
oMessage.viewPopupMessage();
|
oMessage.popupMessage();
|
||||||
} else {
|
} else {
|
||||||
MessageUserStore.error('');
|
MessageUserStore.error('');
|
||||||
let id = 'rl-msg-' + oMessage.hash,
|
let id = 'rl-msg-' + oMessage.hash,
|
||||||
|
|
@ -260,6 +303,8 @@ viewMessage = (oMessage, popup) => {
|
||||||
class:'b-text-part'
|
class:'b-text-part'
|
||||||
+ (oMessage.pgpSigned() ? ' openpgp-signed' : '')
|
+ (oMessage.pgpSigned() ? ' openpgp-signed' : '')
|
||||||
+ (oMessage.pgpEncrypted() ? ' openpgp-encrypted' : '')
|
+ (oMessage.pgpEncrypted() ? ' openpgp-encrypted' : '')
|
||||||
|
+ (oMessage.smimeSigned() ? ' smime-signed' : '')
|
||||||
|
+ (oMessage.smimeEncrypted() ? ' smime-encrypted' : '')
|
||||||
});
|
});
|
||||||
MessageUserStore.purgeCache();
|
MessageUserStore.purgeCache();
|
||||||
}
|
}
|
||||||
|
|
@ -276,7 +321,7 @@ viewMessage = (oMessage, popup) => {
|
||||||
MessageUserStore.loading(false);
|
MessageUserStore.loading(false);
|
||||||
oMessage.body.hidden = false;
|
oMessage.body.hidden = false;
|
||||||
|
|
||||||
if (oMessage.isUnseen()) {
|
if (oMessage.isUnseen() && SettingsUserStore.messageReadAuto()) {
|
||||||
MessageUserStore.MessageSeenTimer = setTimeout(
|
MessageUserStore.MessageSeenTimer = setTimeout(
|
||||||
() => MessagelistUserStore.setAction(oMessage.folder, MessageSetAction.SetSeen, [oMessage]),
|
() => MessagelistUserStore.setAction(oMessage.folder, MessageSetAction.SetSeen, [oMessage]),
|
||||||
SettingsUserStore.messageReadDelay() * 1000 // seconds
|
SettingsUserStore.messageReadDelay() * 1000 // seconds
|
||||||
|
|
@ -301,10 +346,14 @@ populateMessageBody = (oMessage, popup) => {
|
||||||
} else {
|
} else {
|
||||||
let json = oData?.Result;
|
let json = oData?.Result;
|
||||||
if (json
|
if (json
|
||||||
&& oMessage.hash === json.hash
|
&& ((
|
||||||
// && oMessage.folder === json.folder
|
oMessage.hash && oMessage.hash === json.hash
|
||||||
// && oMessage.uid == json.uid
|
) || (
|
||||||
&& oMessage.revivePropertiesFromJson(json)
|
!oMessage.hash
|
||||||
|
&& oMessage.folder === json.folder
|
||||||
|
&& oMessage.uid == json.uid)
|
||||||
|
)
|
||||||
|
&& oMessage.revivePropertiesFromJson(json)
|
||||||
) {
|
) {
|
||||||
/*
|
/*
|
||||||
if (bCached) {
|
if (bCached) {
|
||||||
|
|
@ -321,5 +370,5 @@ populateMessageBody = (oMessage, popup) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
leftPanelDisabled.subscribe(value => value && moveAction(false));
|
leftPanelDisabled.subscribe(value => value && moveAction(0));
|
||||||
moveAction.subscribe(value => value && leftPanelDisabled(false));
|
moveAction.subscribe(value => value && leftPanelDisabled(false));
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ export class CheckboxComponent {
|
||||||
: ko.observable(!!params.value);
|
: ko.observable(!!params.value);
|
||||||
|
|
||||||
this.enable = ko.isObservable(params.enable) ? params.enable
|
this.enable = ko.isObservable(params.enable) ? params.enable
|
||||||
: ko.observable(undefined === params.enable || !!params.enable);
|
: ko.observable(params.enable ?? 1);
|
||||||
|
|
||||||
this.label = params.label;
|
this.label = params.label;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { doc, createElement, addEventsListeners } from 'Common/Globals';
|
import { doc, createElement, addEventsListeners } from 'Common/Globals';
|
||||||
import { EmailModel, addressparser } from 'Model/Email';
|
import { EmailModel } from 'Model/Email';
|
||||||
|
import { addressparser } from 'Mime/Address';
|
||||||
|
|
||||||
const contentType = 'snappymail/emailaddress',
|
const contentType = 'snappymail/emailaddress',
|
||||||
getAddressKey = li => li?.emailaddress?.key,
|
getAddressKey = li => li?.emailaddress?.key,
|
||||||
|
|
@ -165,7 +166,9 @@ export class EmailAddressesComponent {
|
||||||
|
|
||||||
_parseInput(force) {
|
_parseInput(force) {
|
||||||
let val = this.input.value;
|
let val = this.input.value;
|
||||||
if ((force || val.includes(',') || val.includes(';')) && this._parseValue(val)) {
|
if ((force || val.includes(',') || val.includes(';')
|
||||||
|
|| (val.charAt(val.length-1)===' ' && this._simpleEmailMatch(val)))
|
||||||
|
&& this._parseValue(val)) {
|
||||||
this.input.value = '';
|
this.input.value = '';
|
||||||
}
|
}
|
||||||
this._resizeInput();
|
this._resizeInput();
|
||||||
|
|
@ -284,6 +287,13 @@ export class EmailAddressesComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_simpleEmailMatch(value) {
|
||||||
|
// A very SIMPLE test to check if the value might be an email
|
||||||
|
const val = value.trim();
|
||||||
|
return /^[^@]*<[^\s@]{1,128}@[^\s@]{1,256}\.[\w]{2,32}>$/g.test(val)
|
||||||
|
|| /^[^\s@]{1,128}@[^\s@]{1,256}\.[\w]{2,32}$/g.test(val);
|
||||||
|
}
|
||||||
|
|
||||||
_renderTags() {
|
_renderTags() {
|
||||||
let self = this;
|
let self = this;
|
||||||
[...self.ul.children].forEach(node => node !== self.inputCont && node.remove());
|
[...self.ul.children].forEach(node => node !== self.inputCont && node.remove());
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export class JCard {
|
||||||
if (input) {
|
if (input) {
|
||||||
// read from jCard
|
// read from jCard
|
||||||
if (typeof input !== 'object') {
|
if (typeof input !== 'object') {
|
||||||
throw new Error('error reading vcard')
|
throw Error('error reading vcard')
|
||||||
}
|
}
|
||||||
this.parseFromJCard(input)
|
this.parseFromJCard(input)
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +87,7 @@ export class JCard {
|
||||||
arg = new VCardProperty(String(arg), value, params, type);
|
arg = new VCardProperty(String(arg), value, params, type);
|
||||||
}
|
}
|
||||||
if (!(arg instanceof VCardProperty)) {
|
if (!(arg instanceof VCardProperty)) {
|
||||||
throw new Error('invalid argument of VCard.set(), expects string arguments or a VCardProperty');
|
throw Error('invalid argument of VCard.set(), expects string arguments or a VCardProperty');
|
||||||
}
|
}
|
||||||
let field = arg.getField();
|
let field = arg.getField();
|
||||||
this.props.set(field, [arg]);
|
this.props.set(field, [arg]);
|
||||||
|
|
@ -101,7 +101,7 @@ export class JCard {
|
||||||
arg = new VCardProperty(String(arg), value, params, type);
|
arg = new VCardProperty(String(arg), value, params, type);
|
||||||
}
|
}
|
||||||
if (!(arg instanceof VCardProperty)) {
|
if (!(arg instanceof VCardProperty)) {
|
||||||
throw new Error('invalid argument of VCard.add(), expects string arguments or a VCardProperty');
|
throw Error('invalid argument of VCard.add(), expects string arguments or a VCardProperty');
|
||||||
}
|
}
|
||||||
// VCardProperty arguments
|
// VCardProperty arguments
|
||||||
let field = arg.getField();
|
let field = arg.getField();
|
||||||
|
|
@ -124,15 +124,15 @@ export class JCard {
|
||||||
// VCardProperty argument
|
// VCardProperty argument
|
||||||
else if (arg instanceof VCardProperty) {
|
else if (arg instanceof VCardProperty) {
|
||||||
let propArray = this.props.get(arg.getField());
|
let propArray = this.props.get(arg.getField());
|
||||||
if (!(propArray === null || propArray === void 0 ? void 0 : propArray.includes(arg)))
|
if (!propArray?.includes(arg))
|
||||||
throw new Error("Attempted to remove VCardProperty VCard does not have: ".concat(arg));
|
throw Error("Attempted to remove VCardProperty VCard does not have: ".concat(arg));
|
||||||
propArray.splice(propArray.indexOf(arg), 1);
|
propArray.splice(propArray.indexOf(arg), 1);
|
||||||
if (propArray.length === 0)
|
if (propArray.length === 0)
|
||||||
this.props.delete(arg.getField());
|
this.props.delete(arg.getField());
|
||||||
}
|
}
|
||||||
// incorrect arguments
|
// incorrect arguments
|
||||||
else
|
else
|
||||||
throw new Error('invalid argument of VCard.remove(), expects ' +
|
throw Error('invalid argument of VCard.remove(), expects ' +
|
||||||
'string and optional param filter or a VCardProperty');
|
'string and optional param filter or a VCardProperty');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -202,7 +202,7 @@ export class JCard {
|
||||||
parseFullName(options) {
|
parseFullName(options) {
|
||||||
let n = this.getOne('n');
|
let n = this.getOne('n');
|
||||||
if (n === undefined) {
|
if (n === undefined) {
|
||||||
throw new Error('\'fn\' VCardProperty not present in card, cannot parse full name');
|
throw Error('\'fn\' VCardProperty not present in card, cannot parse full name');
|
||||||
}
|
}
|
||||||
let fnString = '';
|
let fnString = '';
|
||||||
// Position in n -> position in fn
|
// Position in n -> position in fn
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ export class VCardProperty {
|
||||||
}
|
}
|
||||||
// invalid property
|
// invalid property
|
||||||
else {
|
else {
|
||||||
throw new Error('invalid Property constructor');
|
throw Error('invalid Property constructor');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
59
dev/External/SquireUI.js
vendored
59
dev/External/SquireUI.js
vendored
|
|
@ -133,11 +133,11 @@ class SquireUI
|
||||||
dir: {
|
dir: {
|
||||||
dir_ltr: {
|
dir_ltr: {
|
||||||
html: '⁋',
|
html: '⁋',
|
||||||
cmd: () => squire.bidi('ltr')
|
cmd: () => squire.setTextDirection('ltr')
|
||||||
},
|
},
|
||||||
dir_rtl: {
|
dir_rtl: {
|
||||||
html: '¶',
|
html: '¶',
|
||||||
cmd: () => squire.bidi('rtl')
|
cmd: () => squire.setTextDirection('rtl')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
|
|
@ -237,7 +237,7 @@ class SquireUI
|
||||||
cmd: () => {
|
cmd: () => {
|
||||||
let node = squire.getSelectionClosest('IMG'),
|
let node = squire.getSelectionClosest('IMG'),
|
||||||
src = prompt("Image", node?.src || "https://");
|
src = prompt("Image", node?.src || "https://");
|
||||||
src?.length ? squire.insertImage(src) : (node && squire.detach(node));
|
src?.length ? squire.insertImage(src) : node?.remove();
|
||||||
},
|
},
|
||||||
matches: 'IMG'
|
matches: 'IMG'
|
||||||
},
|
},
|
||||||
|
|
@ -270,6 +270,13 @@ class SquireUI
|
||||||
btn.classList.toggle('active', 'source' == this.mode);
|
btn.classList.toggle('active', 'source' == this.mode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clear: {
|
||||||
|
removeStyle: {
|
||||||
|
html: '⎚',
|
||||||
|
cmd: () => squire.setStyle()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -318,11 +325,6 @@ class SquireUI
|
||||||
wysiwyg.className = 'squire-wysiwyg';
|
wysiwyg.className = 'squire-wysiwyg';
|
||||||
wysiwyg.dir = 'auto';
|
wysiwyg.dir = 'auto';
|
||||||
this.mode = ''; // 'plain' | 'wysiwyg'
|
this.mode = ''; // 'plain' | 'wysiwyg'
|
||||||
this.__plain = {
|
|
||||||
getRawData: () => this.plain.value,
|
|
||||||
setRawData: plain => this.plain.value = plain
|
|
||||||
};
|
|
||||||
|
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.squire = squire;
|
this.squire = squire;
|
||||||
this.plain = plain;
|
this.plain = plain;
|
||||||
|
|
@ -403,9 +405,9 @@ class SquireUI
|
||||||
|
|
||||||
let changes = actions.changes;
|
let changes = actions.changes;
|
||||||
changes.undo.input.disabled = changes.redo.input.disabled = true;
|
changes.undo.input.disabled = changes.redo.input.disabled = true;
|
||||||
squire.addEventListener('undoStateChange', state => {
|
squire.addEventListener('undoStateChange', e => {
|
||||||
changes.undo.input.disabled = !state.canUndo;
|
changes.undo.input.disabled = !e.detail.canUndo;
|
||||||
changes.redo.input.disabled = !state.canRedo;
|
changes.redo.input.disabled = !e.detail.canRedo;
|
||||||
});
|
});
|
||||||
|
|
||||||
actions.font.fontSize.input.selectedIndex = actions.font.fontSize.defaultValueIndex;
|
actions.font.fontSize.input.selectedIndex = actions.font.fontSize.defaultValueIndex;
|
||||||
|
|
@ -478,21 +480,21 @@ class SquireUI
|
||||||
squire.addEventListener('pathChange', e => {
|
squire.addEventListener('pathChange', e => {
|
||||||
|
|
||||||
const squireRoot = squire.getRoot();
|
const squireRoot = squire.getRoot();
|
||||||
|
let elm = e.detail.element;
|
||||||
|
|
||||||
forEachObjectValue(actions, entries => {
|
forEachObjectValue(actions, entries => {
|
||||||
forEachObjectValue(entries, cfg => {
|
forEachObjectValue(entries, cfg => {
|
||||||
// cfg.matches && cfg.input.classList.toggle('active', e.element && e.element.matches(cfg.matches));
|
// cfg.matches && cfg.input.classList.toggle('active', elm && elm.matches(cfg.matches));
|
||||||
cfg.matches && cfg.input.classList.toggle('active', e.element && e.element.closestWithin(cfg.matches, squireRoot));
|
cfg.matches && cfg.input.classList.toggle('active', elm && elm.closestWithin(cfg.matches, squireRoot));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (e.element) {
|
if (elm) {
|
||||||
// try to find font-family and/or font-size and set "select" elements' values
|
// try to find font-family and/or font-size and set "select" elements' values
|
||||||
|
|
||||||
let sizeSelectedIndex = actions.font.fontSize.defaultValueIndex;
|
let sizeSelectedIndex = actions.font.fontSize.defaultValueIndex;
|
||||||
let familySelectedIndex = defaultFontFamilyIndex;
|
let familySelectedIndex = defaultFontFamilyIndex;
|
||||||
|
|
||||||
let elm = e.element;
|
|
||||||
let familyFound = false;
|
let familyFound = false;
|
||||||
let sizeFound = false;
|
let sizeFound = false;
|
||||||
do {
|
do {
|
||||||
|
|
@ -524,21 +526,12 @@ class SquireUI
|
||||||
});
|
});
|
||||||
/*
|
/*
|
||||||
squire.addEventListener('cursor', e => {
|
squire.addEventListener('cursor', e => {
|
||||||
console.dir({cursor:e.range});
|
console.dir({cursor:e.detail.range});
|
||||||
});
|
});
|
||||||
squire.addEventListener('select', e => {
|
squire.addEventListener('select', e => {
|
||||||
console.dir({select:e.range});
|
console.dir({select:e.detail.range});
|
||||||
});
|
});
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// CKEditor gimmicks used by HtmlEditor
|
|
||||||
this.plugins = {
|
|
||||||
plain: true
|
|
||||||
};
|
|
||||||
this.focusManager = {
|
|
||||||
hasFocus: () => squire._isFocused,
|
|
||||||
blur: () => squire.blur()
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
doAction(name) {
|
doAction(name) {
|
||||||
|
|
@ -576,7 +569,6 @@ class SquireUI
|
||||||
this.modeSelect.selectedIndex = 'plain' == this.mode ? 1 : 0;
|
this.modeSelect.selectedIndex = 'plain' == this.mode ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// CKeditor gimmicks used by HtmlEditor
|
|
||||||
on(type, fn) {
|
on(type, fn) {
|
||||||
if ('mode' == type) {
|
if ('mode' == type) {
|
||||||
this.onModeChange = fn;
|
this.onModeChange = fn;
|
||||||
|
|
@ -619,6 +611,7 @@ class SquireUI
|
||||||
// Move cursor above signature
|
// Move cursor above signature
|
||||||
div.before(br);
|
div.before(br);
|
||||||
div.before(br.cloneNode());
|
div.before(br.cloneNode());
|
||||||
|
// squire._docWasChanged();
|
||||||
}
|
}
|
||||||
this._prev_txt_sig = signature;
|
this._prev_txt_sig = signature;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -642,6 +635,18 @@ class SquireUI
|
||||||
squire.setSelection( range );
|
squire.setSelection( range );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPlainData() {
|
||||||
|
return this.plain.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPlainData(text) {
|
||||||
|
this.plain.value = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
blur() {
|
||||||
|
this.squire.blur();
|
||||||
|
}
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
if ('plain' == this.mode) {
|
if ('plain' == this.mode) {
|
||||||
this.plain.focus();
|
this.plain.focus();
|
||||||
|
|
|
||||||
27
dev/External/User/ko.js
vendored
27
dev/External/User/ko.js
vendored
|
|
@ -1,9 +1,9 @@
|
||||||
import 'External/ko';
|
import 'External/ko';
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
import { HtmlEditor } from 'Common/Html';
|
import { RFC822 } from 'Common/File';
|
||||||
|
import { HtmlEditor } from 'Common/HtmlEditor';
|
||||||
import { timeToNode } from 'Common/Translator';
|
import { timeToNode } from 'Common/Translator';
|
||||||
import { doc, elementById, addEventsListeners, dropdowns, leftPanelDisabled } from 'Common/Globals';
|
import { doc, elementById, addEventsListeners, dropdowns, leftPanelDisabled } from 'Common/Globals';
|
||||||
import { dropdownsDetectVisibility } from 'Common/UtilsUser';
|
|
||||||
import { EmailAddressesComponent } from 'Component/EmailAddresses';
|
import { EmailAddressesComponent } from 'Component/EmailAddresses';
|
||||||
import { ThemeStore } from 'Stores/Theme';
|
import { ThemeStore } from 'Stores/Theme';
|
||||||
import { dropFilesInFolder } from 'Common/Folders';
|
import { dropFilesInFolder } from 'Common/Folders';
|
||||||
|
|
@ -44,7 +44,7 @@ const rlContentType = 'snappymail/action',
|
||||||
let files = false;
|
let files = false;
|
||||||
// if (e.dataTransfer.types.includes('Files'))
|
// if (e.dataTransfer.types.includes('Files'))
|
||||||
for (const item of e.dataTransfer.items) {
|
for (const item of e.dataTransfer.items) {
|
||||||
files |= 'file' === item.kind && 'message/rfc822' === item.type;
|
files |= 'file' === item.kind && RFC822 === item.type;
|
||||||
}
|
}
|
||||||
if (files || dragMessages()) {
|
if (files || dragMessages()) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -91,7 +91,7 @@ Object.assign(ko.bindingHandlers, {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (ko.isObservable(fValue)) {
|
if (ko.isObservable(fValue)) {
|
||||||
editor = new HtmlEditor(element, fUpdateKoValue, fOnReady, fUpdateKoValue);
|
editor = new HtmlEditor(element, fOnReady, fUpdateKoValue, fUpdateKoValue);
|
||||||
|
|
||||||
fValue.__fetchEditorValue = fUpdateKoValue;
|
fValue.__fetchEditorValue = fUpdateKoValue;
|
||||||
|
|
||||||
|
|
@ -103,7 +103,7 @@ Object.assign(ko.bindingHandlers, {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
moment: {
|
time: {
|
||||||
init: ttn,
|
init: ttn,
|
||||||
update: ttn
|
update: ttn
|
||||||
},
|
},
|
||||||
|
|
@ -194,10 +194,6 @@ Object.assign(ko.bindingHandlers, {
|
||||||
};
|
};
|
||||||
addEventsListeners(element, {
|
addEventsListeners(element, {
|
||||||
dragstart: e => {
|
dragstart: e => {
|
||||||
dragData = {
|
|
||||||
action: 'sortable',
|
|
||||||
element: element
|
|
||||||
};
|
|
||||||
setDragAction(e, 'sortable', 'move', element, element);
|
setDragAction(e, 'sortable', 'move', element, element);
|
||||||
element.style.opacity = 0.25;
|
element.style.opacity = 0.25;
|
||||||
},
|
},
|
||||||
|
|
@ -247,18 +243,5 @@ Object.assign(ko.bindingHandlers, {
|
||||||
dropdowns.push(element);
|
dropdowns.push(element);
|
||||||
element.ddBtn = new BSN.Dropdown(element.querySelector('.dropdown-toggle'));
|
element.ddBtn = new BSN.Dropdown(element.querySelector('.dropdown-toggle'));
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
openDropdownTrigger: {
|
|
||||||
update: (element, fValueAccessor) => {
|
|
||||||
if (ko.unwrap(fValueAccessor())) {
|
|
||||||
const el = element.ddBtn;
|
|
||||||
el.open || el.toggle();
|
|
||||||
// el.focus();
|
|
||||||
|
|
||||||
dropdownsDetectVisibility();
|
|
||||||
fValueAccessor()(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
30
dev/External/ko.js
vendored
30
dev/External/ko.js
vendored
|
|
@ -28,6 +28,11 @@ export const
|
||||||
|
|
||||||
dispose = disposable => isFunction(disposable?.dispose) && disposable.dispose(),
|
dispose = disposable => isFunction(disposable?.dispose) && disposable.dispose(),
|
||||||
|
|
||||||
|
onEvent = (element, event, fn) => {
|
||||||
|
element.addEventListener(event, fn);
|
||||||
|
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener(event, fn));
|
||||||
|
},
|
||||||
|
|
||||||
onKey = (key, element, fValueAccessor, fAllBindings, model) => {
|
onKey = (key, element, fValueAccessor, fAllBindings, model) => {
|
||||||
let fn = event => {
|
let fn = event => {
|
||||||
if (key == event.key) {
|
if (key == event.key) {
|
||||||
|
|
@ -36,8 +41,7 @@ export const
|
||||||
fValueAccessor().call(model);
|
fValueAccessor().call(model);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
element.addEventListener('keydown', fn);
|
onEvent(element, 'keydown', fn);
|
||||||
ko.utils.domNodeDisposal.addDisposeCallback(element, () => element.removeEventListener('keydown', fn));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// With this we don't need delegateRunOnDestroy
|
// With this we don't need delegateRunOnDestroy
|
||||||
|
|
@ -62,8 +66,7 @@ Object.assign(ko.bindingHandlers, {
|
||||||
},
|
},
|
||||||
update: (element, fValueAccessor) => {
|
update: (element, fValueAccessor) => {
|
||||||
let value = ko.unwrap(fValueAccessor());
|
let value = ko.unwrap(fValueAccessor());
|
||||||
value = isFunction(value) ? value() : value;
|
errorTip(element, isFunction(value) ? value() : value);
|
||||||
errorTip(element, value);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -82,6 +85,15 @@ Object.assign(ko.bindingHandlers, {
|
||||||
onKey(' ', element, fValueAccessor, fAllBindings, model)
|
onKey(' ', element, fValueAccessor, fAllBindings, model)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggle: {
|
||||||
|
init: (element, fValueAccessor) => {
|
||||||
|
let observable = fValueAccessor(),
|
||||||
|
fn = () => observable(!observable());
|
||||||
|
onEvent(element, 'click', fn);
|
||||||
|
onEvent(element, 'keydown', event => ' ' == event.key && fn());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
i18nUpdate: {
|
i18nUpdate: {
|
||||||
update: (element, fValueAccessor) => {
|
update: (element, fValueAccessor) => {
|
||||||
ko.unwrap(fValueAccessor());
|
ko.unwrap(fValueAccessor());
|
||||||
|
|
@ -89,16 +101,12 @@ Object.assign(ko.bindingHandlers, {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
title: {
|
|
||||||
update: (element, fValueAccessor) => element.title = ko.unwrap(fValueAccessor())
|
|
||||||
},
|
|
||||||
|
|
||||||
command: {
|
command: {
|
||||||
init: (element, fValueAccessor, fAllBindings, viewModel, bindingContext) => {
|
init: (element, fValueAccessor, fAllBindings, viewModel, bindingContext) => {
|
||||||
const command = fValueAccessor();
|
const command = fValueAccessor();
|
||||||
|
|
||||||
if (!command || !command.canExecute) {
|
if (!command || !command.canExecute) {
|
||||||
throw new Error('Value should be a command');
|
throw Error('Value should be a command');
|
||||||
}
|
}
|
||||||
|
|
||||||
ko.bindingHandlers['FORM'==element.nodeName ? 'submit' : 'click'].init(
|
ko.bindingHandlers['FORM'==element.nodeName ? 'submit' : 'click'].init(
|
||||||
|
|
@ -110,10 +118,8 @@ Object.assign(ko.bindingHandlers, {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
update: (element, fValueAccessor) => {
|
update: (element, fValueAccessor) => {
|
||||||
const cl = element.classList;
|
|
||||||
|
|
||||||
let disabled = !fValueAccessor().canExecute();
|
let disabled = !fValueAccessor().canExecute();
|
||||||
cl.toggle('disabled', disabled);
|
element.classList.toggle('disabled', disabled);
|
||||||
|
|
||||||
if (element.matches('INPUT,TEXTAREA,BUTTON')) {
|
if (element.matches('INPUT,TEXTAREA,BUTTON')) {
|
||||||
element.disabled = disabled;
|
element.disabled = disabled;
|
||||||
|
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8"/>
|
|
||||||
<title></title>
|
|
||||||
<style>
|
|
||||||
html, body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
background: rgba(125,128,128,0.3);
|
|
||||||
border-bottom: 1px solid #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
header h1 {
|
|
||||||
font-size: 120%;
|
|
||||||
}
|
|
||||||
|
|
||||||
header * {
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
header time {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
|
||||||
border-left: 2px solid rgba(125,128,128,0.5);
|
|
||||||
margin: 0;
|
|
||||||
padding: 0 0 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
word-break: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
body > * {
|
|
||||||
padding: 0.5em 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#attachments > * {
|
|
||||||
border: 1px solid rgba(125,128,128,0.5);
|
|
||||||
padding: 0.25em;
|
|
||||||
margin-right: 1em;
|
|
||||||
}
|
|
||||||
#attachments > *::before {
|
|
||||||
content: '📎 ';
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body></body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -5,9 +5,13 @@ function typeCast(curValue, newValue) {
|
||||||
if (null != curValue) {
|
if (null != curValue) {
|
||||||
switch (typeof curValue)
|
switch (typeof curValue)
|
||||||
{
|
{
|
||||||
case 'boolean': return 0 != newValue && !!newValue;
|
case 'boolean':
|
||||||
case 'number': return isFinite(newValue) ? parseFloat(newValue) : 0;
|
return 0 != newValue && !!newValue;
|
||||||
case 'string': return null != newValue ? '' + newValue : '';
|
case 'number':
|
||||||
|
newValue = parseFloat(newValue);
|
||||||
|
return isFinite(newValue) ? newValue : 0;
|
||||||
|
case 'string':
|
||||||
|
return null != newValue ? '' + newValue : '';
|
||||||
case 'object':
|
case 'object':
|
||||||
if (curValue.constructor.reviveFromJson) {
|
if (curValue.constructor.reviveFromJson) {
|
||||||
return curValue.constructor.reviveFromJson(newValue);
|
return curValue.constructor.reviveFromJson(newValue);
|
||||||
|
|
@ -23,10 +27,10 @@ export class AbstractModel {
|
||||||
constructor() {
|
constructor() {
|
||||||
/*
|
/*
|
||||||
if (new.target === AbstractModel) {
|
if (new.target === AbstractModel) {
|
||||||
throw new Error("Can't instantiate AbstractModel!");
|
throw Error("Can't instantiate AbstractModel!");
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
this.disposables = [];
|
Object.defineProperty(this, 'disposables', {value: []});
|
||||||
}
|
}
|
||||||
|
|
||||||
addObservables(observables) {
|
addObservables(observables) {
|
||||||
|
|
|
||||||
|
|
@ -60,10 +60,9 @@ export class AbstractViewPopup extends AbstractView
|
||||||
this.keyScope.scope = name;
|
this.keyScope.scope = name;
|
||||||
this.modalVisible = ko.observable(false).extend({ rateLimit: 0 });
|
this.modalVisible = ko.observable(false).extend({ rateLimit: 0 });
|
||||||
this.close = () => this.modalVisible(false);
|
this.close = () => this.modalVisible(false);
|
||||||
|
this.tryToClose = () => (false === this.onClose()) || this.close();
|
||||||
addShortcut('escape,close', '', name, () => {
|
addShortcut('escape,close', '', name, () => {
|
||||||
if (this.modalVisible() && false !== this.onClose()) {
|
this.modalVisible() && this.tryToClose();
|
||||||
this.close();
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
// return true; Issue with supported modal close
|
// return true; Issue with supported modal close
|
||||||
});
|
});
|
||||||
|
|
@ -116,6 +115,11 @@ export class AbstractViewSettings
|
||||||
onHide() {}
|
onHide() {}
|
||||||
viewModelDom
|
viewModelDom
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* When this[name] does not exists, create as observable with value of SettingsGet(name)
|
||||||
|
* When this[name+'Trigger'] does not exists, create as observable
|
||||||
|
* Subscribe to this[name], and handle saving the setting
|
||||||
|
*/
|
||||||
addSetting(name, valueCb)
|
addSetting(name, valueCb)
|
||||||
{
|
{
|
||||||
let prop = name[0].toLowerCase() + name.slice(1),
|
let prop = name[0].toLowerCase() + name.slice(1),
|
||||||
|
|
@ -131,6 +135,7 @@ export class AbstractViewSettings
|
||||||
rl.app.Remote.saveSetting(name, value,
|
rl.app.Remote.saveSetting(name, value,
|
||||||
iError => {
|
iError => {
|
||||||
this[trigger](iError ? SaveSettingStatus.Failed : SaveSettingStatus.Success);
|
this[trigger](iError ? SaveSettingStatus.Failed : SaveSettingStatus.Success);
|
||||||
|
// iError || Settings.set(name, value);
|
||||||
setTimeout(() => this[trigger](SaveSettingStatus.Idle), 1000);
|
setTimeout(() => this[trigger](SaveSettingStatus.Idle), 1000);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
@ -138,6 +143,10 @@ export class AbstractViewSettings
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Foreach name if this[name] does not exists, create as observable with value of SettingsGet(name)
|
||||||
|
* Subscribe to this[name], for saving the setting
|
||||||
|
*/
|
||||||
addSettings(names)
|
addSettings(names)
|
||||||
{
|
{
|
||||||
names.forEach(name => {
|
names.forEach(name => {
|
||||||
|
|
|
||||||
|
|
@ -25,13 +25,13 @@ const
|
||||||
screen = screenName => (screenName && SCREENS.get(screenName)) || null,
|
screen = screenName => (screenName && SCREENS.get(screenName)) || null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Creates the extended AbstractView model
|
||||||
* @param {Function} ViewModelClass
|
* @param {Function} ViewModelClass
|
||||||
* @param {Object=} vmScreen
|
* @param {Object=} vmScreen
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
buildViewModel = (ViewModelClass, vmScreen) => {
|
buildViewModel = (ViewModelClass, vmScreen) => {
|
||||||
if (ViewModelClass && !ViewModelClass.__builded) {
|
if (ViewModelClass && !ViewModelClass.__vm) {
|
||||||
let vmDom = null;
|
|
||||||
const
|
const
|
||||||
vm = new ViewModelClass(vmScreen),
|
vm = new ViewModelClass(vmScreen),
|
||||||
id = vm.viewModelTemplateID,
|
id = vm.viewModelTemplateID,
|
||||||
|
|
@ -39,16 +39,15 @@ const
|
||||||
dialog = ViewTypePopup === vm.viewType,
|
dialog = ViewTypePopup === vm.viewType,
|
||||||
vmPlace = doc.getElementById(position);
|
vmPlace = doc.getElementById(position);
|
||||||
|
|
||||||
ViewModelClass.__builded = true;
|
|
||||||
ViewModelClass.__vm = vm;
|
|
||||||
|
|
||||||
if (vmPlace) {
|
if (vmPlace) {
|
||||||
vmDom = dialog
|
ViewModelClass.__vm = vm;
|
||||||
|
|
||||||
|
let vmDom = dialog
|
||||||
? createElement('dialog',{id:'V-'+id})
|
? createElement('dialog',{id:'V-'+id})
|
||||||
: createElement('div',{id:'V-'+id,hidden:''})
|
: createElement('div',{id:'V-'+id,hidden:''})
|
||||||
vmPlace.append(vmDom);
|
vmPlace.append(vmDom);
|
||||||
|
|
||||||
vm.viewModelDom = ViewModelClass.__dom = vmDom;
|
vm.viewModelDom = vmDom;
|
||||||
|
|
||||||
if (dialog) {
|
if (dialog) {
|
||||||
// Firefox < 98 / Safari < 15.4 HTMLDialogElement not defined
|
// Firefox < 98 / Safari < 15.4 HTMLDialogElement not defined
|
||||||
|
|
@ -59,13 +58,11 @@ const
|
||||||
vmDom.before(vmDom.backdrop = createElement('div',{class:'dialog-backdrop'}));
|
vmDom.before(vmDom.backdrop = createElement('div',{class:'dialog-backdrop'}));
|
||||||
vmDom.setAttribute('open','');
|
vmDom.setAttribute('open','');
|
||||||
vmDom.open = true;
|
vmDom.open = true;
|
||||||
vmDom.returnValue = null;
|
|
||||||
vmDom.backdrop.hidden = false;
|
vmDom.backdrop.hidden = false;
|
||||||
};
|
};
|
||||||
vmDom.close = v => {
|
vmDom.close = () => {
|
||||||
// if (vmDom.dispatchEvent(new CustomEvent('cancel', {cancelable:true}))) {
|
// if (vmDom.dispatchEvent(new CustomEvent('cancel', {cancelable:true}))) {
|
||||||
vmDom.backdrop.hidden = true;
|
vmDom.backdrop.hidden = true;
|
||||||
vmDom.returnValue = v;
|
|
||||||
vmDom.removeAttribute('open', null);
|
vmDom.removeAttribute('open', null);
|
||||||
vmDom.open = false;
|
vmDom.open = false;
|
||||||
// vmDom.dispatchEvent(new CustomEvent('close'));
|
// vmDom.dispatchEvent(new CustomEvent('close'));
|
||||||
|
|
@ -77,13 +74,17 @@ const
|
||||||
// vmDom.addEventListener('close', () => vm.modalVisible(false));
|
// vmDom.addEventListener('close', () => vm.modalVisible(false));
|
||||||
|
|
||||||
// show/hide popup/modal
|
// show/hide popup/modal
|
||||||
|
// transitionend is called for each property, so we only listen to `opacity`
|
||||||
|
// as defined in CSS by `dialog:not(.animate)`
|
||||||
const endShowHide = e => {
|
const endShowHide = e => {
|
||||||
if (e.target === vmDom) {
|
if (e.target === vmDom && 'opacity' === e.propertyName) {
|
||||||
if (vmDom.classList.contains('animate')) {
|
if (vmDom.classList.contains('animate')) {
|
||||||
vm.afterShow?.();
|
vm.afterShow?.();
|
||||||
|
fireEvent('rl-vm-visible', vm);
|
||||||
} else {
|
} else {
|
||||||
vmDom.close();
|
vmDom.close();
|
||||||
vm.afterHide?.();
|
vm.afterHide?.();
|
||||||
|
// fireEvent('rl-vm-hidden', vm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -139,10 +140,9 @@ const
|
||||||
screen.viewModels.forEach(ViewModelClass => {
|
screen.viewModels.forEach(ViewModelClass => {
|
||||||
if (
|
if (
|
||||||
ViewModelClass.__vm &&
|
ViewModelClass.__vm &&
|
||||||
ViewModelClass.__dom &&
|
|
||||||
ViewTypePopup !== ViewModelClass.__vm.viewType
|
ViewTypePopup !== ViewModelClass.__vm.viewType
|
||||||
) {
|
) {
|
||||||
fn(ViewModelClass.__vm, ViewModelClass.__dom);
|
fn(ViewModelClass.__vm, ViewModelClass.__vm.viewModelDom);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
@ -152,7 +152,7 @@ const
|
||||||
forEachViewModel(screenToHide, (vm, dom) => {
|
forEachViewModel(screenToHide, (vm, dom) => {
|
||||||
dom.hidden = true;
|
dom.hidden = true;
|
||||||
vm.onHide?.();
|
vm.onHide?.();
|
||||||
destroy && vm.viewModelDom.remove();
|
destroy && dom.remove();
|
||||||
});
|
});
|
||||||
ThemeStore.isMobile() && leftPanelDisabled(true);
|
ThemeStore.isMobile() && leftPanelDisabled(true);
|
||||||
},
|
},
|
||||||
|
|
@ -164,10 +164,10 @@ const
|
||||||
*/
|
*/
|
||||||
screenOnRoute = (screenName, subPart) => {
|
screenOnRoute = (screenName, subPart) => {
|
||||||
screenName = screenName || defaultScreenName;
|
screenName = screenName || defaultScreenName;
|
||||||
if (screenName && fireEvent('sm-show-screen', screenName, 1)) {
|
if (screenName && fireEvent('sm-show-screen', screenName + (subPart ? '/' + subPart : ''), 1)) {
|
||||||
// Close all popups
|
// Close all popups
|
||||||
for (let vm of visiblePopups) {
|
for (let vm of visiblePopups) {
|
||||||
(false === vm.onClose()) || vm.close();
|
vm.tryToClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
let vmScreen = screen(screenName);
|
let vmScreen = screen(screenName);
|
||||||
|
|
@ -229,15 +229,11 @@ export const
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
showScreenPopup = (ViewModelClassToShow, params = []) => {
|
showScreenPopup = (ViewModelClassToShow, params = []) => {
|
||||||
const vm = buildViewModel(ViewModelClassToShow) && ViewModelClassToShow.__dom && ViewModelClassToShow.__vm;
|
const vm = buildViewModel(ViewModelClassToShow);
|
||||||
|
|
||||||
if (vm) {
|
if (vm) {
|
||||||
params = params || [];
|
params = params || [];
|
||||||
|
|
||||||
vm.beforeShow?.(...params);
|
vm.beforeShow?.(...params);
|
||||||
|
|
||||||
vm.modalVisible(true);
|
vm.modalVisible(true);
|
||||||
|
|
||||||
vm.onShow?.(...params);
|
vm.onShow?.(...params);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
197
dev/Mime/Address.js
Normal file
197
dev/Mime/Address.js
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
import { decodeEncodedWords } from 'Mime/Encoding';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses structured e-mail addresses from an address/mailbox(-list) field
|
||||||
|
* https://datatracker.ietf.org/doc/html/rfc2822#section-3.4
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* "Name <address@domain>"
|
||||||
|
*
|
||||||
|
* will be converted to
|
||||||
|
*
|
||||||
|
* [{name: "Name", email: "address@domain"}]
|
||||||
|
*
|
||||||
|
* @param {String} str Address field
|
||||||
|
* @return {Array} An array of address objects
|
||||||
|
*/
|
||||||
|
export function addressparser(str) {
|
||||||
|
str = (str || '').toString();
|
||||||
|
|
||||||
|
let
|
||||||
|
endOperator = '',
|
||||||
|
node = {
|
||||||
|
type: 'text',
|
||||||
|
value: ''
|
||||||
|
},
|
||||||
|
escaped = false,
|
||||||
|
address = [],
|
||||||
|
addresses = [];
|
||||||
|
|
||||||
|
const
|
||||||
|
/*
|
||||||
|
* Operator tokens and which tokens are expected to end the sequence
|
||||||
|
*/
|
||||||
|
OPERATORS = {
|
||||||
|
'"': '"',
|
||||||
|
'(': ')',
|
||||||
|
'<': '>',
|
||||||
|
',': '',
|
||||||
|
// Groups are ended by semicolons
|
||||||
|
':': ';',
|
||||||
|
// Semicolons are not a legal delimiter per the RFC2822 grammar other
|
||||||
|
// than for terminating a group, but they are also not valid for any
|
||||||
|
// other use in this context. Given that some mail clients have
|
||||||
|
// historically allowed the semicolon as a delimiter equivalent to the
|
||||||
|
// comma in their UI, it makes sense to treat them the same as a comma
|
||||||
|
// when used outside of a group.
|
||||||
|
';': ''
|
||||||
|
},
|
||||||
|
pushToken = token => {
|
||||||
|
token.value = (token.value || '').toString().trim();
|
||||||
|
token.value.length && address.push(token);
|
||||||
|
node = {
|
||||||
|
type: 'text',
|
||||||
|
value: ''
|
||||||
|
},
|
||||||
|
escaped = false;
|
||||||
|
},
|
||||||
|
pushAddress = () => {
|
||||||
|
if (address.length) {
|
||||||
|
address = _handleAddress(address);
|
||||||
|
if (address.length) {
|
||||||
|
addresses = addresses.concat(address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
address = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
[...str].forEach(chr => {
|
||||||
|
if (!escaped && (chr === endOperator || (!endOperator && chr in OPERATORS))) {
|
||||||
|
pushToken(node);
|
||||||
|
if (',' === chr || ';' === chr) {
|
||||||
|
pushAddress();
|
||||||
|
} else {
|
||||||
|
endOperator = endOperator ? '' : OPERATORS[chr];
|
||||||
|
if ('<' === chr) {
|
||||||
|
node.type = 'email';
|
||||||
|
} else if ('(' === chr) {
|
||||||
|
node.type = 'comment';
|
||||||
|
} else if (':' === chr) {
|
||||||
|
node.type = 'group';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
node.value += chr;
|
||||||
|
escaped = !escaped && '\\' === chr;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pushToken(node);
|
||||||
|
|
||||||
|
pushAddress();
|
||||||
|
|
||||||
|
return addresses;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts tokens for a single address into an address object
|
||||||
|
*
|
||||||
|
* @param {Array} tokens Tokens object
|
||||||
|
* @return {Object} Address object
|
||||||
|
*/
|
||||||
|
function _handleAddress(tokens) {
|
||||||
|
let
|
||||||
|
isGroup = false,
|
||||||
|
address = {},
|
||||||
|
addresses = [],
|
||||||
|
data = {
|
||||||
|
email: [],
|
||||||
|
comment: [],
|
||||||
|
group: [],
|
||||||
|
text: []
|
||||||
|
};
|
||||||
|
|
||||||
|
tokens.forEach(token => {
|
||||||
|
isGroup = isGroup || 'group' === token.type;
|
||||||
|
data[token.type].push(token.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// If there is no text but a comment, replace the two
|
||||||
|
if (!data.text.length && data.comment.length) {
|
||||||
|
data.text = data.comment;
|
||||||
|
data.comment = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGroup) {
|
||||||
|
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3
|
||||||
|
/*
|
||||||
|
addresses.push({
|
||||||
|
email: '',
|
||||||
|
name: data.text.join(' ').trim(),
|
||||||
|
group: addressparser(data.group.join(','))
|
||||||
|
// ,comment: data.comment.join(' ').trim()
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
addresses = addresses.concat(addressparser(data.group.join(',')));
|
||||||
|
} else {
|
||||||
|
// If no address was found, try to detect one from regular text
|
||||||
|
if (!data.email.length && data.text.length) {
|
||||||
|
var i = data.text.length;
|
||||||
|
while (i--) {
|
||||||
|
if (data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
|
||||||
|
data.email = data.text.splice(i, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// still no address
|
||||||
|
if (!data.email.length) {
|
||||||
|
i = data.text.length;
|
||||||
|
while (i--) {
|
||||||
|
data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^@\s]+\b\s*/, address => {
|
||||||
|
if (!data.email.length) {
|
||||||
|
data.email = [address.trim()];
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return address.trim();
|
||||||
|
});
|
||||||
|
if (data.email.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's still no text but a comment exists, replace the two
|
||||||
|
if (!data.text.length && data.comment.length) {
|
||||||
|
data.text = data.comment;
|
||||||
|
data.comment = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep only the first address occurence, push others to regular text
|
||||||
|
if (data.email.length > 1) {
|
||||||
|
data.text = data.text.concat(data.email.splice(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
address = {
|
||||||
|
// Join values with spaces
|
||||||
|
email: decodeEncodedWords(data.email.join(' ').trim()),
|
||||||
|
name: decodeEncodedWords(data.text.join(' ').trim())
|
||||||
|
// ,comment: data.comment.join(' ').trim()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (address.email === address.name) {
|
||||||
|
if (address.email.includes('@')) {
|
||||||
|
address.name = '';
|
||||||
|
} else {
|
||||||
|
address.email = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// address.email = address.email.replace(/^[<]+(.*)[>]+$/g, '$1');
|
||||||
|
|
||||||
|
addresses.push(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
return addresses;
|
||||||
|
}
|
||||||
35
dev/Mime/Encoding.js
Normal file
35
dev/Mime/Encoding.js
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
const
|
||||||
|
QPDecodeParams = [/=([0-9A-F]{2})/g, (...args) => String.fromCharCode(parseInt(args[1], 16))];
|
||||||
|
|
||||||
|
export const
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2045#section-6.8
|
||||||
|
BDecode = atob,
|
||||||
|
|
||||||
|
// unescape(encodeURIComponent()) makes the UTF-16 DOMString to an UTF-8 string
|
||||||
|
BEncode = data => btoa(unescape(encodeURIComponent(data))),
|
||||||
|
/* // Without deprecated 'unescape':
|
||||||
|
BEncode = data => btoa(encodeURIComponent(data).replace(
|
||||||
|
/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode('0x' + p1)
|
||||||
|
)),
|
||||||
|
*/
|
||||||
|
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2045#section-6.7
|
||||||
|
QPDecode = data => data.replace(/=\r?\n/g, '').replace(...QPDecodeParams),
|
||||||
|
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2047#section-4.1
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2047#section-4.2
|
||||||
|
// encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
|
||||||
|
decodeEncodedWords = data =>
|
||||||
|
data.replace(/=\?([^?]+)\?(B|Q)\?(.+?)\?=/g, (m, charset, encoding, text) =>
|
||||||
|
decodeText(charset, 'B' == encoding ? BDecode(text) : QPDecode(text))
|
||||||
|
)
|
||||||
|
,
|
||||||
|
|
||||||
|
decodeText = (charset, data) => {
|
||||||
|
try {
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Encoding_API/Encodings
|
||||||
|
return new TextDecoder(charset).decode(Uint8Array.from(data, c => c.charCodeAt(0)));
|
||||||
|
} catch (e) {
|
||||||
|
console.error({charset:charset,error:e});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,17 +1,5 @@
|
||||||
//import { b64Encode } from 'Common/Utils';
|
import { decodeEncodedWords, BDecode, BEncode, QPDecode, decodeText } from 'Mime/Encoding';
|
||||||
|
import { addressparser } from 'Mime/Address';
|
||||||
const
|
|
||||||
// RFC2045
|
|
||||||
QPDecodeParams = [/=([0-9A-F]{2})/g, (...args) => String.fromCharCode(parseInt(args[1], 16))],
|
|
||||||
QPDecode = data => data.replace(/=\r?\n/g, '').replace(...QPDecodeParams),
|
|
||||||
decodeText = (charset, data) => {
|
|
||||||
try {
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Encoding_API/Encodings
|
|
||||||
return new TextDecoder(charset).decode(Uint8Array.from(data, c => c.charCodeAt(0)));
|
|
||||||
} catch (e) {
|
|
||||||
console.error({charset:charset,error:e});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ParseMime(text)
|
export function ParseMime(text)
|
||||||
{
|
{
|
||||||
|
|
@ -27,7 +15,52 @@ export function ParseMime(text)
|
||||||
this.bodyEnd = 0;
|
this.bodyEnd = 0;
|
||||||
this.boundary = '';
|
this.boundary = '';
|
||||||
this.bodyText = '';
|
this.bodyText = '';
|
||||||
this.headers = {};
|
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.6
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc4021
|
||||||
|
this.headers = {
|
||||||
|
// Required
|
||||||
|
date = null,
|
||||||
|
from = [], // mailbox-list
|
||||||
|
// Optional
|
||||||
|
sender = [], // mailbox MUST occur with multi-address
|
||||||
|
'reply-to' = [], // address-list
|
||||||
|
to = [], // address-list
|
||||||
|
cc = [], // address-list
|
||||||
|
bcc = [], // address-list
|
||||||
|
'message-id' = '', // msg-id SHOULD be present
|
||||||
|
'in-reply-to' = '', // 1*msg-id SHOULD occur in some replies
|
||||||
|
references = '', // 1*msg-id SHOULD occur in some replies
|
||||||
|
subject = '', // unstructured
|
||||||
|
// Optional unlimited
|
||||||
|
comments = [], // unstructured
|
||||||
|
keywords = [], // phrase *("," phrase)
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.6.6
|
||||||
|
'resent-date' = [],
|
||||||
|
'resent-from' = [],
|
||||||
|
'resent-sender' = [],
|
||||||
|
'resent-to' = [],
|
||||||
|
'resent-cc' = [],
|
||||||
|
'resent-bcc' = [],
|
||||||
|
'resent-msg-id' = [],
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.6.7
|
||||||
|
trace = [],
|
||||||
|
'return-path' = '', // angle-addr
|
||||||
|
received = [],
|
||||||
|
// optional others outside RFC2822
|
||||||
|
'mime-version' = '', // RFC2045
|
||||||
|
'content-transfer-encoding' = '',
|
||||||
|
'content-type' = '',
|
||||||
|
'delivered-to' = [], // RFC9228 addr-spec
|
||||||
|
'authentication-results' = '', // dkim, spf, dmarc
|
||||||
|
'dkim-signature' = '',
|
||||||
|
'x-rspamd-queue-id' = '',
|
||||||
|
'x-rspamd-action' = '',
|
||||||
|
'x-spamd-bar' = '',
|
||||||
|
'x-rspamd-server' = '',
|
||||||
|
'x-spamd-result' = '',
|
||||||
|
'x-remote-address' = '',
|
||||||
|
// etc.
|
||||||
|
};
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -50,26 +83,25 @@ export function ParseMime(text)
|
||||||
get body() {
|
get body() {
|
||||||
let body = this.bodyRaw,
|
let body = this.bodyRaw,
|
||||||
charset = this.header('content-type')?.params.charset,
|
charset = this.header('content-type')?.params.charset,
|
||||||
encoding = this.headerValue('content-transfer-encoding');
|
encoding = this.headerValue('content-transfer-encoding')?.toLowerCase();
|
||||||
if ('quoted-printable' == encoding) {
|
if ('quoted-printable' == encoding) {
|
||||||
body = QPDecode(body);
|
body = QPDecode(body);
|
||||||
} else if ('base64' == encoding) {
|
} else if ('base64' == encoding) {
|
||||||
body = atob(body.replace(/\r?\n/g, ''));
|
body = BDecode(body.replace(/\r?\n/g, ''));
|
||||||
}
|
}
|
||||||
return decodeText(charset, body);
|
return decodeText(charset, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
get dataUrl() {
|
get dataUrl() {
|
||||||
let body = this.bodyRaw,
|
let body = this.bodyRaw,
|
||||||
encoding = this.headerValue('content-transfer-encoding');
|
encoding = this.headerValue('content-transfer-encoding')?.toLowerCase();
|
||||||
if ('base64' == encoding) {
|
if ('base64' == encoding) {
|
||||||
body = body.replace(/\r?\n/g, '');
|
body = body.replace(/\r?\n/g, '');
|
||||||
} else {
|
} else {
|
||||||
if ('quoted-printable' == encoding) {
|
if ('quoted-printable' == encoding) {
|
||||||
body = QPDecode(body);
|
body = QPDecode(body);
|
||||||
}
|
}
|
||||||
body = btoa(body);
|
body = BEncode(body);
|
||||||
// body = b64Encode(body);
|
|
||||||
}
|
}
|
||||||
return 'data:' + this.headerValue('content-type') + ';base64,' + body;
|
return 'data:' + this.headerValue('content-type') + ';base64,' + body;
|
||||||
}
|
}
|
||||||
|
|
@ -80,7 +112,7 @@ export function ParseMime(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
getByContentType(type) {
|
getByContentType(type) {
|
||||||
if (type == this.headerValue('content-type')) {
|
if (type == this.headerValue('content-type')?.toLowerCase()) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
let i = 0, p = this.parts, part;
|
let i = 0, p = this.parts, part;
|
||||||
|
|
@ -92,6 +124,9 @@ export function ParseMime(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mailbox-list or address-list
|
||||||
|
const lists = ['from','reply-to','to','cc','bcc'];
|
||||||
|
|
||||||
const ParsePart = (mimePart, start_pos = 0, id = '') =>
|
const ParsePart = (mimePart, start_pos = 0, id = '') =>
|
||||||
{
|
{
|
||||||
let part = new MimePart,
|
let part = new MimePart,
|
||||||
|
|
@ -113,11 +148,19 @@ export function ParseMime(text)
|
||||||
[...header.matchAll(/;\s*([^;=]+)=\s*"?([^;"]+)"?/g)].forEach(param =>
|
[...header.matchAll(/;\s*([^;=]+)=\s*"?([^;"]+)"?/g)].forEach(param =>
|
||||||
params[param[1].trim().toLowerCase()] = param[2].trim()
|
params[param[1].trim().toLowerCase()] = param[2].trim()
|
||||||
);
|
);
|
||||||
// encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
|
let field = match[1].trim().toLowerCase();
|
||||||
match[2] = match[2].trim().replace(/=\?([^?]+)\?(B|Q)\?(.+?)\?=/g, (m, charset, encoding, text) =>
|
if (lists.includes(field)) {
|
||||||
decodeText(charset, 'B' == encoding ? atob(text) : QPDecode(text))
|
match[2] = addressparser(match[2]);
|
||||||
);
|
} else if ('keywords' === field) {
|
||||||
headers[match[1].trim().toLowerCase()] = {
|
match[2] = match[2].split(',').forEach(entry => decodeEncodedWords(entry.trim()));
|
||||||
|
match[2] = (headers[field]?.value || []).concat(match[2]);
|
||||||
|
} else {
|
||||||
|
match[2] = decodeEncodedWords(match[2].trim());
|
||||||
|
if ('comments' === field) {
|
||||||
|
match[2] = (headers[field]?.value || []).push(match[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
headers[field] = {
|
||||||
value: match[2],
|
value: match[2],
|
||||||
params: params
|
params: params
|
||||||
};
|
};
|
||||||
|
|
@ -132,7 +175,7 @@ export function ParseMime(text)
|
||||||
let boundary = headers['content-type']?.params.boundary;
|
let boundary = headers['content-type']?.params.boundary;
|
||||||
if (boundary) {
|
if (boundary) {
|
||||||
part.boundary = boundary;
|
part.boundary = boundary;
|
||||||
let regex = new RegExp('(?:^|\r?\n)--' + boundary + '(?:--)?(?:\r?\n|$)', 'g'),
|
let regex = new RegExp('(?:^|\r?\n)--' + RegExp.escape(boundary) + '(?:--)?(?:\r?\n|$)', 'g'),
|
||||||
body = mimePart.slice(head.length),
|
body = mimePart.slice(head.length),
|
||||||
bodies = body.split(regex),
|
bodies = body.split(regex),
|
||||||
pos = part.bodyStart;
|
pos = part.bodyStart;
|
||||||
|
|
|
||||||
|
|
@ -4,23 +4,34 @@ import { AttachmentModel } from 'Model/Attachment';
|
||||||
import { FileInfo } from 'Common/File';
|
import { FileInfo } from 'Common/File';
|
||||||
import { BEGIN_PGP_MESSAGE } from 'Stores/User/Pgp';
|
import { BEGIN_PGP_MESSAGE } from 'Stores/User/Pgp';
|
||||||
|
|
||||||
|
import { EmailModel } from 'Model/Email';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string data
|
* @param string data
|
||||||
* @param MessageModel message
|
* @param MessageModel message
|
||||||
*/
|
*/
|
||||||
export function MimeToMessage(data, message)
|
export function MimeToMessage(data, message)
|
||||||
{
|
{
|
||||||
let signed;
|
|
||||||
const struct = ParseMime(data);
|
const struct = ParseMime(data);
|
||||||
if (struct.headers) {
|
if (struct.headers) {
|
||||||
let html = struct.getByContentType('text/html'),
|
let html = struct.getByContentType('text/html'),
|
||||||
subject = struct.headerValue('subject');
|
subject = struct.headerValue('subject');
|
||||||
html = html ? html.body : '';
|
html = html ? html.body : '';
|
||||||
|
|
||||||
|
// Content-Type: ...; protected-headers="v1"
|
||||||
subject && message.subject(subject);
|
subject && message.subject(subject);
|
||||||
|
|
||||||
// EmailCollectionModel
|
// EmailCollectionModel
|
||||||
['from','to'].forEach(name => message[name].fromString(struct.headerValue(name)));
|
['from','to'].forEach(name => {
|
||||||
|
const items = message[name];
|
||||||
|
struct.headerValue(name)?.forEach(item => {
|
||||||
|
item = new EmailModel(item.email, item.name);
|
||||||
|
// Make them unique
|
||||||
|
if (item.email && item.name || !items.find(address => address.email == item.email)) {
|
||||||
|
items.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
struct.forEach(part => {
|
struct.forEach(part => {
|
||||||
let cd = part.header('content-disposition'),
|
let cd = part.header('content-disposition'),
|
||||||
|
|
@ -54,12 +65,28 @@ export function MimeToMessage(data, message)
|
||||||
} else {
|
} else {
|
||||||
message.attachments.push(attachment);
|
message.attachments.push(attachment);
|
||||||
}
|
}
|
||||||
} else if ('multipart/signed' === type.value && 'application/pgp-signature' === type.params.protocol) {
|
} else if ('multipart/signed' === type.value) {
|
||||||
signed = {
|
let protocol = type.params.protocol;
|
||||||
|
if ('application/pgp-signature' === protocol) {
|
||||||
|
message.pgpSigned({
|
||||||
|
micAlg: type.micalg,
|
||||||
|
bodyPart: part.parts[0],
|
||||||
|
sigPart: part.parts[1]
|
||||||
|
});
|
||||||
|
} else if ('application/pkcs7-signature' === protocol.replace('x-')) {
|
||||||
|
message.smimeSigned({
|
||||||
|
micAlg: type.micalg,
|
||||||
|
bodyPart: part,
|
||||||
|
sigPart: part.parts[1], // For importing
|
||||||
|
detached: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if ('application/pkcs7-mime' === type.value /*&& 'signed-data' === type.params['smime-type']=*/) {
|
||||||
|
message.smimeSigned({
|
||||||
micAlg: type.micalg,
|
micAlg: type.micalg,
|
||||||
bodyPart: part.parts[0],
|
bodyPart: part,
|
||||||
sigPart: part.parts[1]
|
detached: false
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -70,10 +97,7 @@ export function MimeToMessage(data, message)
|
||||||
message.plain(data);
|
message.plain(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!signed && message.plain().includes(BEGIN_PGP_MESSAGE)) {
|
if (message.plain().includes(BEGIN_PGP_MESSAGE)) {
|
||||||
signed = true;
|
message.pgpSigned(true);
|
||||||
}
|
}
|
||||||
message.pgpSigned(signed);
|
|
||||||
|
|
||||||
// TODO: Verify instantly?
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ export class AbstractCollectionModel extends Array
|
||||||
constructor() {
|
constructor() {
|
||||||
/*
|
/*
|
||||||
if (new.target === AbstractCollectionModel) {
|
if (new.target === AbstractCollectionModel) {
|
||||||
throw new Error("Can't instantiate AbstractCollectionModel!");
|
throw Error("Can't instantiate AbstractCollectionModel!");
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
super();
|
super();
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,10 @@ export class AccountModel extends AbstractModel {
|
||||||
&& setTimeout(()=>this.fetchUnread(), (Math.ceil(Math.random() * 10)) * 3000);
|
&& setTimeout(()=>this.fetchUnread(), (Math.ceil(Math.random() * 10)) * 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label() {
|
||||||
|
return this.name || IDN.toUnicode(this.email);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get INBOX unread messages
|
* Get INBOX unread messages
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,10 @@ export class AttachmentModel extends AbstractModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
get download() {
|
get download() {
|
||||||
return b64EncodeJSONSafe({
|
return b64EncodeJSONSafe(this.url ? {
|
||||||
|
fileName: this.fileName,
|
||||||
|
data: this.url.replace(/^.+,/, '')
|
||||||
|
} : {
|
||||||
folder: this.folder,
|
folder: this.folder,
|
||||||
uid: this.uid,
|
uid: this.uid,
|
||||||
mimeIndex: this.mimeIndex,
|
mimeIndex: this.mimeIndex,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { baseCollator } from 'Common/Translator';
|
||||||
import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
||||||
import { AttachmentModel } from 'Model/Attachment';
|
import { AttachmentModel } from 'Model/Attachment';
|
||||||
|
|
||||||
|
|
@ -10,14 +11,24 @@ export class AttachmentCollectionModel extends AbstractCollectionModel
|
||||||
* @returns {AttachmentCollectionModel}
|
* @returns {AttachmentCollectionModel}
|
||||||
*/
|
*/
|
||||||
static reviveFromJson(items) {
|
static reviveFromJson(items) {
|
||||||
return super.reviveFromJson(items, attachment => AttachmentModel.reviveFromJson(attachment));
|
|
||||||
/*
|
|
||||||
const attachments = super.reviveFromJson(items, attachment => AttachmentModel.reviveFromJson(attachment));
|
const attachments = super.reviveFromJson(items, attachment => AttachmentModel.reviveFromJson(attachment));
|
||||||
|
let collator = baseCollator(true);
|
||||||
|
attachments.sort((a, b) => {
|
||||||
|
if (a.isInline()) {
|
||||||
|
if (!b.isInline()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
} else if (!b.isInline()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return collator.compare(a.fileName, b.fileName);
|
||||||
|
});
|
||||||
|
/*
|
||||||
if (attachments) {
|
if (attachments) {
|
||||||
attachments.InlineCount = attachments.reduce((accumulator, a) => accumulator + (a.isInline ? 1 : 0), 0);
|
attachments.InlineCount = attachments.reduce((accumulator, a) => accumulator + (a.isInline ? 1 : 0), 0);
|
||||||
}
|
}
|
||||||
return attachments;
|
|
||||||
*/
|
*/
|
||||||
|
return attachments;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ export class ContactModel extends AbstractModel {
|
||||||
focused: false,
|
focused: false,
|
||||||
selected: false,
|
selected: false,
|
||||||
checked: false,
|
checked: false,
|
||||||
|
sendToAll: true,
|
||||||
|
|
||||||
deleted: false,
|
deleted: false,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
|
|
@ -134,22 +135,6 @@ export class ContactModel extends AbstractModel {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {Array|null}
|
|
||||||
*/
|
|
||||||
getNameAndEmailHelper() {
|
|
||||||
let name = (this.givenName() + ' ' + this.surName()).trim(),
|
|
||||||
email = this.email()[0]?.value();
|
|
||||||
/*
|
|
||||||
// this.jCard.getOne('fn')?.notEmpty() ||
|
|
||||||
this.jCard.parseFullName({set:true});
|
|
||||||
// let name = this.jCard.getOne('nickname'),
|
|
||||||
let name = this.jCard.getOne('fn'),
|
|
||||||
email = this.jCard.getOne('email');
|
|
||||||
*/
|
|
||||||
return email ? [email, name] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @static
|
* @static
|
||||||
* @param {jCard} json
|
* @param {jCard} json
|
||||||
|
|
@ -209,19 +194,15 @@ export class ContactModel extends AbstractModel {
|
||||||
return contact;
|
return contact;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
generateUid() {
|
|
||||||
return '' + this.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
addEmail() {
|
addEmail() {
|
||||||
// home, work
|
// home, work
|
||||||
this.email.push({
|
this.email.push({
|
||||||
value: ko.observable('')
|
value: ko.observable('')
|
||||||
// type: prop.params.type
|
// type: prop.params.type
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.sendToAllDisplayStatus())
|
||||||
|
document.getElementById('send-to-all').style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
addTel() {
|
addTel() {
|
||||||
|
|
@ -318,4 +299,9 @@ export class ContactModel extends AbstractModel {
|
||||||
+ (this.checked() ? ' checked' : '')
|
+ (this.checked() ? ' checked' : '')
|
||||||
+ (this.focused() ? ' focused' : '');
|
+ (this.focused() ? ' focused' : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sendToAllDisplayStatus() {
|
||||||
|
return this.email.length > 1
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,202 +4,6 @@ import { AbstractModel } from 'Knoin/AbstractModel';
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses structured e-mail addresses from an address field
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
*
|
|
||||||
* "Name <address@domain>"
|
|
||||||
*
|
|
||||||
* will be converted to
|
|
||||||
*
|
|
||||||
* [{name: "Name", address: "address@domain"}]
|
|
||||||
*
|
|
||||||
* @param {String} str Address field
|
|
||||||
* @return {Array} An array of address objects
|
|
||||||
*/
|
|
||||||
export function addressparser(str) {
|
|
||||||
str = (str || '').toString();
|
|
||||||
|
|
||||||
let
|
|
||||||
endOperator = '',
|
|
||||||
node = {
|
|
||||||
type: 'text',
|
|
||||||
value: ''
|
|
||||||
},
|
|
||||||
escaped = false,
|
|
||||||
address = [],
|
|
||||||
addresses = [];
|
|
||||||
|
|
||||||
const
|
|
||||||
/*
|
|
||||||
* Operator tokens and which tokens are expected to end the sequence
|
|
||||||
*/
|
|
||||||
OPERATORS = {
|
|
||||||
'"': '"',
|
|
||||||
'(': ')',
|
|
||||||
'<': '>',
|
|
||||||
',': '',
|
|
||||||
// Groups are ended by semicolons
|
|
||||||
':': ';',
|
|
||||||
// Semicolons are not a legal delimiter per the RFC2822 grammar other
|
|
||||||
// than for terminating a group, but they are also not valid for any
|
|
||||||
// other use in this context. Given that some mail clients have
|
|
||||||
// historically allowed the semicolon as a delimiter equivalent to the
|
|
||||||
// comma in their UI, it makes sense to treat them the same as a comma
|
|
||||||
// when used outside of a group.
|
|
||||||
';': ''
|
|
||||||
},
|
|
||||||
pushToken = token => {
|
|
||||||
token.value = (token.value || '').toString().trim();
|
|
||||||
token.value.length && address.push(token);
|
|
||||||
node = {
|
|
||||||
type: 'text',
|
|
||||||
value: ''
|
|
||||||
},
|
|
||||||
escaped = false;
|
|
||||||
},
|
|
||||||
pushAddress = () => {
|
|
||||||
if (address.length) {
|
|
||||||
address = _handleAddress(address);
|
|
||||||
if (address.length) {
|
|
||||||
addresses = addresses.concat(address);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
address = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
[...str].forEach(chr => {
|
|
||||||
if (!escaped && (chr === endOperator || (!endOperator && chr in OPERATORS))) {
|
|
||||||
pushToken(node);
|
|
||||||
if (',' === chr || ';' === chr) {
|
|
||||||
pushAddress();
|
|
||||||
} else {
|
|
||||||
endOperator = endOperator ? '' : OPERATORS[chr];
|
|
||||||
if ('<' === chr) {
|
|
||||||
node.type = 'email';
|
|
||||||
} else if ('(' === chr) {
|
|
||||||
node.type = 'comment';
|
|
||||||
} else if (':' === chr) {
|
|
||||||
node.type = 'group';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
node.value += chr;
|
|
||||||
escaped = !escaped && '\\' === chr;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
pushToken(node);
|
|
||||||
|
|
||||||
pushAddress();
|
|
||||||
|
|
||||||
return addresses;
|
|
||||||
// return addresses.map(item => (item.name || item.email) ? new EmailModel(item.email, item.name) : null).filter(v => v);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts tokens for a single address into an address object
|
|
||||||
*
|
|
||||||
* @param {Array} tokens Tokens object
|
|
||||||
* @return {Object} Address object
|
|
||||||
*/
|
|
||||||
function _handleAddress(tokens) {
|
|
||||||
let
|
|
||||||
isGroup = false,
|
|
||||||
address = {},
|
|
||||||
addresses = [],
|
|
||||||
data = {
|
|
||||||
email: [],
|
|
||||||
comment: [],
|
|
||||||
group: [],
|
|
||||||
text: []
|
|
||||||
};
|
|
||||||
|
|
||||||
tokens.forEach(token => {
|
|
||||||
isGroup = isGroup || 'group' === token.type;
|
|
||||||
data[token.type].push(token.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
// If there is no text but a comment, replace the two
|
|
||||||
if (!data.text.length && data.comment.length) {
|
|
||||||
data.text = data.comment;
|
|
||||||
data.comment = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isGroup) {
|
|
||||||
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3
|
|
||||||
/*
|
|
||||||
addresses.push({
|
|
||||||
email: '',
|
|
||||||
name: data.text.join(' ').trim(),
|
|
||||||
group: addressparser(data.group.join(','))
|
|
||||||
// ,comment: data.comment.join(' ').trim()
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
addresses = addresses.concat(addressparser(data.group.join(',')));
|
|
||||||
} else {
|
|
||||||
// If no address was found, try to detect one from regular text
|
|
||||||
if (!data.email.length && data.text.length) {
|
|
||||||
var i = data.text.length;
|
|
||||||
while (i--) {
|
|
||||||
if (data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
|
|
||||||
data.email = data.text.splice(i, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// still no address
|
|
||||||
if (!data.email.length) {
|
|
||||||
i = data.text.length;
|
|
||||||
while (i--) {
|
|
||||||
data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^@\s]+\b\s*/, address => {
|
|
||||||
if (!data.email.length) {
|
|
||||||
data.email = [address.trim()];
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return address.trim();
|
|
||||||
});
|
|
||||||
if (data.email.length) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's still no text but a comment exists, replace the two
|
|
||||||
if (!data.text.length && data.comment.length) {
|
|
||||||
data.text = data.comment;
|
|
||||||
data.comment = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep only the first address occurence, push others to regular text
|
|
||||||
if (data.email.length > 1) {
|
|
||||||
data.text = data.text.concat(data.email.splice(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
address = {
|
|
||||||
// Join values with spaces
|
|
||||||
email: data.email.join(' ').trim(),
|
|
||||||
name: data.text.join(' ').trim()
|
|
||||||
// ,comment: data.comment.join(' ').trim()
|
|
||||||
};
|
|
||||||
|
|
||||||
if (address.email === address.name) {
|
|
||||||
if (address.email.includes('@')) {
|
|
||||||
address.name = '';
|
|
||||||
} else {
|
|
||||||
address.email = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// address.email = address.email.replace(/^[<]+(.*)[>]+$/g, '$1');
|
|
||||||
|
|
||||||
addresses.push(address);
|
|
||||||
}
|
|
||||||
|
|
||||||
return addresses;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class EmailModel extends AbstractModel {
|
export class EmailModel extends AbstractModel {
|
||||||
/**
|
/**
|
||||||
* @param {string=} email = ''
|
* @param {string=} email = ''
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
||||||
import { EmailModel, addressparser } from 'Model/Email';
|
import { EmailModel } from 'Model/Email';
|
||||||
import { forEachObjectValue } from 'Common/Utils';
|
import { forEachObjectValue } from 'Common/Utils';
|
||||||
|
import { addressparser } from 'Mime/Address';
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
|
@ -51,4 +52,24 @@ export class EmailCollectionModel extends AbstractCollectionModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {array} [{name: "Name", email: "address@domain"}]
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
static fromArray(addresses) {
|
||||||
|
let list = new this();
|
||||||
|
list.fromArray(addresses);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
fromArray(addresses) {
|
||||||
|
addresses.forEach(item => {
|
||||||
|
item = new EmailModel(item.email, item.name);
|
||||||
|
// Make them unique
|
||||||
|
if (item.email && item.name || !this.find(address => address.email == item.email)) {
|
||||||
|
this.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,8 @@ import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
||||||
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
|
import { UNUSED_OPTION_VALUE } from 'Common/Consts';
|
||||||
import { isArray, getKeyByValue, forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
|
import { isArray, getKeyByValue, forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
|
||||||
import { ClientSideKeyNameExpandedFolders, FolderType, FolderMetadataKeys } from 'Common/EnumsUser';
|
import { ClientSideKeyNameExpandedFolders, FolderType, FolderMetadataKeys } from 'Common/EnumsUser';
|
||||||
import { clearCache, getFolderFromCacheList, setFolder, setFolderInboxName, removeFolderFromCacheList } from 'Common/Cache';
|
import { clearCache, getFolderFromCacheList, setFolder, setFolderInboxName } from 'Common/Cache';
|
||||||
import { Settings, SettingsGet, fireEvent } from 'Common/Globals';
|
import { Settings, SettingsGet, fireEvent } from 'Common/Globals';
|
||||||
import { Notifications } from 'Common/Enums';
|
|
||||||
|
|
||||||
import * as Local from 'Storage/Client';
|
import * as Local from 'Storage/Client';
|
||||||
|
|
||||||
|
|
@ -15,16 +14,23 @@ import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
|
|
||||||
import { sortFolders } from 'Common/Folders';
|
import { sortFolders } from 'Common/Folders';
|
||||||
import { i18n, translateTrigger, getNotification } from 'Common/Translator';
|
import { i18n, translateTrigger } from 'Common/Translator';
|
||||||
|
|
||||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||||
|
|
||||||
import { /*koComputable,*/ addObservablesTo } from 'External/ko';
|
import { /*koComputable,*/ addObservablesTo } from 'External/ko';
|
||||||
|
|
||||||
//import { mailBox } from 'Common/Links';
|
import { mailBox } from 'Common/Links';
|
||||||
|
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
|
import { FileInfo } from 'Common/File';
|
||||||
|
|
||||||
|
import { FolderPopupView } from 'View/Popup/Folder';
|
||||||
|
import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
|
|
||||||
|
import { isAllowedKeyword } from 'Stores/User/Folder';
|
||||||
|
|
||||||
const
|
const
|
||||||
// isPosNumeric = value => null != value && /^[0-9]*$/.test(value.toString()),
|
// isPosNumeric = value => null != value && /^[0-9]*$/.test(value.toString()),
|
||||||
|
|
||||||
|
|
@ -98,7 +104,7 @@ export const
|
||||||
// Repeat every 15 minutes?
|
// Repeat every 15 minutes?
|
||||||
// this.foldersTimeout = setTimeout(loadFolders, 900000);
|
// this.foldersTimeout = setTimeout(loadFolders, 900000);
|
||||||
})
|
})
|
||||||
.catch(() => fCallback && setTimeout(fCallback, 1, false));
|
.catch(e => fCallback && setTimeout(fCallback, 1, false, e));
|
||||||
};
|
};
|
||||||
|
|
||||||
export class FolderCollectionModel extends AbstractCollectionModel
|
export class FolderCollectionModel extends AbstractCollectionModel
|
||||||
|
|
@ -111,6 +117,8 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
||||||
this.namespace;
|
this.namespace;
|
||||||
this.optimized
|
this.optimized
|
||||||
this.capabilities
|
this.capabilities
|
||||||
|
this.allow; // allow adding
|
||||||
|
// this.exist;
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -252,6 +260,10 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
visible() {
|
||||||
|
return this.filter(folder => folder.visible());
|
||||||
|
}
|
||||||
|
|
||||||
storeIt() {
|
storeIt() {
|
||||||
FolderUserStore.displaySpecSetting(Settings.app('folderSpecLimit') < this.CountRec);
|
FolderUserStore.displaySpecSetting(Settings.app('folderSpecLimit') < this.CountRec);
|
||||||
|
|
||||||
|
|
@ -273,7 +285,7 @@ export class FolderCollectionModel extends AbstractCollectionModel
|
||||||
// 'THREAD=REFS', 'THREAD=REFERENCES', 'THREAD=ORDEREDSUBJECT'
|
// 'THREAD=REFS', 'THREAD=REFERENCES', 'THREAD=ORDEREDSUBJECT'
|
||||||
AppUserStore.threadsAllowed(!!this.capabilities.some(capa => capa.startsWith('THREAD=')));
|
AppUserStore.threadsAllowed(!!this.capabilities.some(capa => capa.startsWith('THREAD=')));
|
||||||
|
|
||||||
// FolderUserStore.folderListOptimized(!!this.optimized);
|
// FolderUserStore.optimized(!!this.optimized);
|
||||||
FolderUserStore.quotaUsage(this.quotaUsage);
|
FolderUserStore.quotaUsage(this.quotaUsage);
|
||||||
FolderUserStore.quotaLimit(this.quotaLimit);
|
FolderUserStore.quotaLimit(this.quotaLimit);
|
||||||
FolderUserStore.capabilities(this.capabilities);
|
FolderUserStore.capabilities(this.capabilities);
|
||||||
|
|
@ -294,6 +306,7 @@ export class FolderModel extends AbstractModel {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.fullName = '';
|
this.fullName = '';
|
||||||
|
this.parentName = '';
|
||||||
this.delimiter = '';
|
this.delimiter = '';
|
||||||
this.deep = 0;
|
this.deep = 0;
|
||||||
this.expires = 0;
|
this.expires = 0;
|
||||||
|
|
@ -304,6 +317,7 @@ export class FolderModel extends AbstractModel {
|
||||||
this.etag = '';
|
this.etag = '';
|
||||||
this.id = 0;
|
this.id = 0;
|
||||||
this.uidNext = 0;
|
this.uidNext = 0;
|
||||||
|
this.size = 0;
|
||||||
|
|
||||||
addObservablesTo(this, {
|
addObservablesTo(this, {
|
||||||
name: '',
|
name: '',
|
||||||
|
|
@ -313,12 +327,10 @@ export class FolderModel extends AbstractModel {
|
||||||
|
|
||||||
focused: false,
|
focused: false,
|
||||||
selected: false,
|
selected: false,
|
||||||
editing: false,
|
|
||||||
isSubscribed: true,
|
isSubscribed: true,
|
||||||
checkable: false, // Check for new messages
|
checkable: false, // Check for new messages
|
||||||
askDelete: false,
|
askDelete: false,
|
||||||
|
|
||||||
nameForEdit: '',
|
|
||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
|
|
||||||
totalEmails: 0,
|
totalEmails: 0,
|
||||||
|
|
@ -338,7 +350,6 @@ export class FolderModel extends AbstractModel {
|
||||||
this.addSubscribables({
|
this.addSubscribables({
|
||||||
kolabType: sValue => this.metadata[FolderMetadataKeys.KolabFolderType] = sValue,
|
kolabType: sValue => this.metadata[FolderMetadataKeys.KolabFolderType] = sValue,
|
||||||
permanentFlags: aValue => this.tagsAllowed(aValue.includes('\\*')),
|
permanentFlags: aValue => this.tagsAllowed(aValue.includes('\\*')),
|
||||||
editing: value => value && this.nameForEdit(this.name()),
|
|
||||||
unreadEmails: unread => FolderType.Inbox === this.type() && fireEvent('mailbox.inbox-unread-count', unread)
|
unreadEmails: unread => FolderType.Inbox === this.type() && fireEvent('mailbox.inbox-unread-count', unread)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -360,20 +371,19 @@ export class FolderModel extends AbstractModel {
|
||||||
.extend({ notify: 'always' });
|
.extend({ notify: 'always' });
|
||||||
*/
|
*/
|
||||||
/*
|
/*
|
||||||
https://www.rfc-editor.org/rfc/rfc8621.html#section-2
|
// https://www.rfc-editor.org/rfc/rfc8621.html#section-2
|
||||||
"myRights": {
|
this.myRights = {
|
||||||
"mayAddItems": true,
|
'mayAddItems': true,
|
||||||
"mayRename": false,
|
'mayCreateChild': true,
|
||||||
"maySubmit": true,
|
'mayDelete': true,
|
||||||
"mayDelete": false,
|
'mayReadItems': true,
|
||||||
"maySetKeywords": true,
|
'mayRemoveItems': true,
|
||||||
"mayRemoveItems": true,
|
'mayRename': true,
|
||||||
"mayCreateChild": true,
|
'maySetKeywords': true,
|
||||||
"maySetSeen": true,
|
'maySetSeen': true,
|
||||||
"mayReadItems": true
|
'maySubmit': true
|
||||||
},
|
};
|
||||||
*/
|
*/
|
||||||
|
|
||||||
this.addComputables({
|
this.addComputables({
|
||||||
|
|
||||||
isInbox: () => FolderType.Inbox === this.type(),
|
isInbox: () => FolderType.Inbox === this.type(),
|
||||||
|
|
@ -384,6 +394,7 @@ export class FolderModel extends AbstractModel {
|
||||||
// isSubscribed: () => this.attributes().includes('\\subscribed'),
|
// isSubscribed: () => this.attributes().includes('\\subscribed'),
|
||||||
|
|
||||||
hasVisibleSubfolders: () => !!this.subFolders().find(folder => folder.visible()),
|
hasVisibleSubfolders: () => !!this.subFolders().find(folder => folder.visible()),
|
||||||
|
visibleSubfolders: () => this.subFolders().visible(),
|
||||||
|
|
||||||
hasSubscriptions: () => this.isSubscribed() | !!this.subFolders().find(
|
hasSubscriptions: () => this.isSubscribed() | !!this.subFolders().find(
|
||||||
oFolder => {
|
oFolder => {
|
||||||
|
|
@ -392,8 +403,6 @@ export class FolderModel extends AbstractModel {
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|
||||||
canBeEdited: () => !this.type() && this.exists/* && this.selectable()*/,
|
|
||||||
|
|
||||||
isSystemFolder: () => this.type()
|
isSystemFolder: () => this.type()
|
||||||
| (FolderUserStore.allowKolab() && !!this.kolabType() & !SettingsUserStore.unhideKolabFolders()),
|
| (FolderUserStore.allowKolab() && !!this.kolabType() & !SettingsUserStore.unhideKolabFolders()),
|
||||||
|
|
||||||
|
|
@ -404,6 +413,8 @@ export class FolderModel extends AbstractModel {
|
||||||
canBeSubscribed: () => this.selectable()
|
canBeSubscribed: () => this.selectable()
|
||||||
&& !(this.isSystemFolder() | !SettingsUserStore.hideUnsubscribed()),
|
&& !(this.isSystemFolder() | !SettingsUserStore.hideUnsubscribed()),
|
||||||
|
|
||||||
|
optionalTags: () => this.permanentFlags.filter(isAllowedKeyword),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Folder is visible when:
|
* Folder is visible when:
|
||||||
* - hasVisibleSubfolders()
|
* - hasVisibleSubfolders()
|
||||||
|
|
@ -456,62 +467,34 @@ export class FolderModel extends AbstractModel {
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
|
|
||||||
|
friendlySize: () => FileInfo.friendlySize(this.size),
|
||||||
|
|
||||||
detailedName: () => this.name() + ' ' + this.nameInfo(),
|
detailedName: () => this.name() + ' ' + this.nameInfo(),
|
||||||
|
|
||||||
hasSubscribedUnreadMessagesSubfolders: () =>
|
icon: () => {
|
||||||
!!this.subFolders().find(
|
switch (this.type())
|
||||||
folder => folder.unreadCount() | folder.hasSubscribedUnreadMessagesSubfolders()
|
{
|
||||||
)
|
case 1: return '📥'; // FolderType.Inbox
|
||||||
/*
|
case 2: return '📧'; // FolderType.Sent icon-paper-plane
|
||||||
!!this.subFolders().filter(
|
case 3: return '🗎'; // FolderType.Drafts
|
||||||
folder => folder.unreadCount() | folder.hasSubscribedUnreadMessagesSubfolders()
|
case 4: return '⚠'; // FolderType.Junk
|
||||||
).length
|
case 5: return '🗑'; // FolderType.Trash
|
||||||
*/
|
case 6: return '🗄'; // FolderType.Archive
|
||||||
// ,href: () => this.canBeSelected() && mailBox(this.fullNameHash)
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
hasUnreadInSub: () =>
|
||||||
|
this.subFolders().some(
|
||||||
|
folder => folder.unreadEmails() | folder.hasUnreadInSub()
|
||||||
|
),
|
||||||
|
|
||||||
|
href: () => this.canBeSelected() && mailBox(this.fullNameHash)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
edit() {
|
edit() {
|
||||||
this.canBeEdited() && this.editing(true);
|
showScreenPopup(FolderPopupView, [this]);
|
||||||
}
|
|
||||||
|
|
||||||
unedit() {
|
|
||||||
this.editing(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
rename() {
|
|
||||||
const folder = this,
|
|
||||||
nameToEdit = folder.nameForEdit().trim();
|
|
||||||
if (nameToEdit && folder.name() !== nameToEdit) {
|
|
||||||
Remote.abort('Folders').post('FolderRename', FolderUserStore.foldersRenaming, {
|
|
||||||
folder: folder.fullName,
|
|
||||||
newFolderName: nameToEdit,
|
|
||||||
subscribe: folder.isSubscribed() ? 1 : 0
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
folder.name(nameToEdit/*data.name*/);
|
|
||||||
if (folder.subFolders.length) {
|
|
||||||
Remote.setTrigger(FolderUserStore.foldersLoading, true);
|
|
||||||
// clearTimeout(Remote.foldersTimeout);
|
|
||||||
// Remote.foldersTimeout = setTimeout(loadFolders, 500);
|
|
||||||
setTimeout(loadFolders, 500);
|
|
||||||
// TODO: rename all subfolders with folder.delimiter to prevent reload?
|
|
||||||
} else {
|
|
||||||
removeFolderFromCacheList(folder.fullName);
|
|
||||||
folder.fullName = data.Result.fullName;
|
|
||||||
setFolder(folder);
|
|
||||||
const parent = getFolderFromCacheList(folder.parentName);
|
|
||||||
sortFolders(parent ? parent.subFolders : FolderUserStore.folderList);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
FolderUserStore.folderListError(
|
|
||||||
getNotification(error.code, '', Notifications.CantRenameFolder)
|
|
||||||
+ '.\n' + error.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
folder.editing(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -543,6 +526,8 @@ export class FolderModel extends AbstractModel {
|
||||||
|
|
||||||
folder.isSubscribed(attr('\\subscribed'));
|
folder.isSubscribed(attr('\\subscribed'));
|
||||||
folder.exists = !attr('\\nonexistent');
|
folder.exists = !attr('\\nonexistent');
|
||||||
|
folder.subFolders.allow = !attr('\\noinferiors');
|
||||||
|
// folder.subFolders.exist = attr('\\haschildren') || !attr('\\hasnochildren');
|
||||||
folder.selectable(folder.exists && !attr('\\noselect'));
|
folder.selectable(folder.exists && !attr('\\noselect'));
|
||||||
|
|
||||||
type && 'mail' != type && folder.kolabType(type);
|
type && 'mail' != type && folder.kolabType(type);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||||
import { addObservablesTo } from 'External/ko';
|
import { addObservablesTo, addComputablesTo } from 'External/ko';
|
||||||
|
|
||||||
export class IdentityModel extends AbstractModel {
|
export class IdentityModel extends AbstractModel {
|
||||||
/**
|
/**
|
||||||
|
|
@ -11,16 +11,32 @@ export class IdentityModel extends AbstractModel {
|
||||||
|
|
||||||
addObservablesTo(this, {
|
addObservablesTo(this, {
|
||||||
id: '',
|
id: '',
|
||||||
|
label: '',
|
||||||
email: '',
|
email: '',
|
||||||
name: '',
|
name: '',
|
||||||
|
|
||||||
replyTo: '',
|
replyTo: '',
|
||||||
bcc: '',
|
bcc: '',
|
||||||
|
sentFolder: '',
|
||||||
|
|
||||||
signature: '',
|
signature: '',
|
||||||
signatureInsertBefore: false,
|
signatureInsertBefore: false,
|
||||||
|
|
||||||
askDelete: false
|
pgpSign: false,
|
||||||
|
pgpEncrypt: false,
|
||||||
|
|
||||||
|
smimeKey: '',
|
||||||
|
smimeCertificate: '',
|
||||||
|
|
||||||
|
askDelete: false,
|
||||||
|
|
||||||
|
exists: false
|
||||||
|
});
|
||||||
|
|
||||||
|
addComputablesTo(this, {
|
||||||
|
smimeKeyEncrypted: () => this.smimeKey().includes('-----BEGIN ENCRYPTED PRIVATE KEY-----'),
|
||||||
|
smimeKeyValid: () => /^-----BEGIN (ENCRYPTED |RSA )?PRIVATE KEY-----/.test(this.smimeKey()),
|
||||||
|
smimeCertificateValid: () => /^-----BEGIN CERTIFICATE-----/.test(this.smimeCertificate())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,8 +45,8 @@ export class IdentityModel extends AbstractModel {
|
||||||
*/
|
*/
|
||||||
formattedName() {
|
formattedName() {
|
||||||
const name = this.name(),
|
const name = this.name(),
|
||||||
email = this.email();
|
email = this.email(),
|
||||||
|
label = this.label();
|
||||||
return name ? name + ' <' + email + '>' : email;
|
return (name ? `${name} ` : '') + `<${email}>` + (label ? ` (${label})` : '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,27 +2,85 @@ import ko from 'ko';
|
||||||
|
|
||||||
import { i18n } from 'Common/Translator';
|
import { i18n } from 'Common/Translator';
|
||||||
|
|
||||||
import { doc, SettingsGet } from 'Common/Globals';
|
import { doc, elementById, SettingsGet } from 'Common/Globals';
|
||||||
import { encodeHtml, plainToHtml, htmlToPlain, cleanHtml } from 'Common/Html';
|
import { encodeHtml, plainToHtml, htmlToPlain, cleanHtml } from 'Common/Html';
|
||||||
import { forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
|
import { forEachObjectEntry, b64EncodeJSONSafe } from 'Common/Utils';
|
||||||
import { serverRequestRaw, proxy } from 'Common/Links';
|
import { serverRequestRaw, proxy } from 'Common/Links';
|
||||||
import { addObservablesTo, addComputablesTo } from 'External/ko';
|
import { addObservablesTo, addComputablesTo } from 'External/ko';
|
||||||
|
|
||||||
import { FolderUserStore, isAllowedKeyword } from 'Stores/User/Folder';
|
import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
|
|
||||||
import { FileInfo } from 'Common/File';
|
import { FileInfo, RFC822 } from 'Common/File';
|
||||||
import { AttachmentCollectionModel } from 'Model/AttachmentCollection';
|
import { AttachmentCollectionModel } from 'Model/AttachmentCollection';
|
||||||
import { EmailCollectionModel } from 'Model/EmailCollection';
|
import { EmailCollectionModel } from 'Model/EmailCollection';
|
||||||
|
import { MimeHeaderCollectionModel } from 'Model/MimeHeaderCollection';
|
||||||
|
//import { MimeHeaderAutocryptModel } from 'Model/MimeHeaderAutocrypt';
|
||||||
import { AbstractModel } from 'Knoin/AbstractModel';
|
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||||
|
|
||||||
import PreviewHTML from 'Html/PreviewMessage.html';
|
|
||||||
|
|
||||||
import { LanguageStore } from 'Stores/Language';
|
import { LanguageStore } from 'Stores/Language';
|
||||||
|
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
|
import { MimeToMessage } from 'Mime/Utils';
|
||||||
|
|
||||||
const
|
const
|
||||||
|
PreviewHTML = `<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title></title>
|
||||||
|
<style>
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
background: rgba(125,128,128,0.3);
|
||||||
|
border-bottom: 1px solid #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
font-size: 120%;
|
||||||
|
}
|
||||||
|
|
||||||
|
header * {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
header time {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
border-left: 2px solid rgba(125,128,128,0.5);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
body > * {
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#attachments > * {
|
||||||
|
border: 1px solid rgba(125,128,128,0.5);
|
||||||
|
padding: 0.25em;
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
#attachments > *::before {
|
||||||
|
content: '📎 ';
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body></body>
|
||||||
|
</html>`,
|
||||||
|
|
||||||
msgHtml = msg => cleanHtml(msg.html(), msg.attachments(), '#rl-msg-' + msg.hash),
|
msgHtml = msg => cleanHtml(msg.html(), msg.attachments(), '#rl-msg-' + msg.hash),
|
||||||
|
|
||||||
toggleTag = (message, keyword) => {
|
toggleTag = (message, keyword) => {
|
||||||
|
|
@ -55,43 +113,51 @@ export class MessageModel extends AbstractModel {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.folder = '';
|
Object.assign(this, {
|
||||||
this.uid = 0;
|
folder: '',
|
||||||
this.hash = '';
|
uid: 0,
|
||||||
this.from = new EmailCollectionModel;
|
hash: '',
|
||||||
this.to = new EmailCollectionModel;
|
from: new EmailCollectionModel,
|
||||||
this.cc = new EmailCollectionModel;
|
to: new EmailCollectionModel,
|
||||||
this.bcc = new EmailCollectionModel;
|
cc: new EmailCollectionModel,
|
||||||
this.sender = new EmailCollectionModel;
|
bcc: new EmailCollectionModel,
|
||||||
this.replyTo = new EmailCollectionModel;
|
sender: new EmailCollectionModel,
|
||||||
this.deliveredTo = new EmailCollectionModel;
|
replyTo: new EmailCollectionModel,
|
||||||
this.body = null;
|
deliveredTo: new EmailCollectionModel,
|
||||||
this.draftInfo = [];
|
body: null,
|
||||||
this.dkim = [];
|
draftInfo: [],
|
||||||
this.spf = [];
|
dkim: [],
|
||||||
this.dmarc = [];
|
spf: [],
|
||||||
this.messageId = '';
|
dmarc: [],
|
||||||
this.inReplyTo = '';
|
messageId: '',
|
||||||
this.references = '';
|
inReplyTo: '',
|
||||||
this.autocrypt = {};
|
references: '',
|
||||||
|
// autocrypt: ko.observableArray(),
|
||||||
|
hasVirus: null, // or boolean when scanned
|
||||||
|
priority: 3, // Normal
|
||||||
|
senderEmailsString: '',
|
||||||
|
senderClearEmailsString: '',
|
||||||
|
isSpam: false,
|
||||||
|
spamScore: 0,
|
||||||
|
spamResult: '',
|
||||||
|
size: 0,
|
||||||
|
readReceipt: '',
|
||||||
|
preview: null,
|
||||||
|
|
||||||
|
attachments: ko.observableArray(new AttachmentCollectionModel),
|
||||||
|
threads: ko.observableArray(),
|
||||||
|
threadUnseen: ko.observableArray(),
|
||||||
|
unsubsribeLinks: ko.observableArray(),
|
||||||
|
flags: ko.observableArray(),
|
||||||
|
headers: ko.observableArray(new MimeHeaderCollectionModel)
|
||||||
|
});
|
||||||
|
|
||||||
addObservablesTo(this, {
|
addObservablesTo(this, {
|
||||||
subject: '',
|
subject: '',
|
||||||
plain: '',
|
plain: '',
|
||||||
html: '',
|
html: '',
|
||||||
size: 0,
|
|
||||||
spamScore: 0,
|
|
||||||
spamResult: '',
|
|
||||||
isSpam: false,
|
|
||||||
hasVirus: null, // or boolean when scanned
|
|
||||||
dateTimestamp: 0,
|
dateTimestamp: 0,
|
||||||
internalTimestamp: 0,
|
dateTimestampSource: 0,
|
||||||
priority: 3, // Normal
|
|
||||||
|
|
||||||
senderEmailsString: '',
|
|
||||||
senderClearEmailsString: '',
|
|
||||||
|
|
||||||
deleted: false,
|
|
||||||
|
|
||||||
// Also used by Selector
|
// Also used by Selector
|
||||||
focused: false,
|
focused: false,
|
||||||
|
|
@ -101,26 +167,29 @@ export class MessageModel extends AbstractModel {
|
||||||
isHtml: false,
|
isHtml: false,
|
||||||
hasImages: false,
|
hasImages: false,
|
||||||
hasExternals: false,
|
hasExternals: false,
|
||||||
|
hasTracking: false,
|
||||||
pgpSigned: null,
|
|
||||||
pgpVerified: null,
|
|
||||||
|
|
||||||
encrypted: false,
|
encrypted: false,
|
||||||
|
|
||||||
|
pgpSigned: null,
|
||||||
pgpEncrypted: null,
|
pgpEncrypted: null,
|
||||||
pgpDecrypted: false,
|
pgpDecrypted: false,
|
||||||
|
|
||||||
readReceipt: '',
|
smimeSigned: null,
|
||||||
|
smimeEncrypted: null,
|
||||||
|
smimeDecrypted: false,
|
||||||
|
|
||||||
// rfc8621
|
// rfc8621
|
||||||
id: '',
|
id: '',
|
||||||
// threadId: ''
|
// threadId: ''
|
||||||
});
|
|
||||||
|
|
||||||
this.attachments = ko.observableArray(new AttachmentCollectionModel);
|
/**
|
||||||
this.threads = ko.observableArray();
|
* Basic support for Linked Data (Structured Email)
|
||||||
this.threadUnseen = ko.observableArray();
|
* https://json-ld.org/
|
||||||
this.unsubsribeLinks = ko.observableArray();
|
* https://structured.email/
|
||||||
this.flags = ko.observableArray();
|
**/
|
||||||
|
linkedData: []
|
||||||
|
});
|
||||||
|
|
||||||
addComputablesTo(this, {
|
addComputablesTo(this, {
|
||||||
attachmentIconClass: () =>
|
attachmentIconClass: () =>
|
||||||
|
|
@ -128,24 +197,28 @@ export class MessageModel extends AbstractModel {
|
||||||
threadsLen: () => rl.app.messageList.threadUid() ? 0 : this.threads().length,
|
threadsLen: () => rl.app.messageList.threadUid() ? 0 : this.threads().length,
|
||||||
threadUnseenLen: () => rl.app.messageList.threadUid() ? 0 : this.threadUnseen().length,
|
threadUnseenLen: () => rl.app.messageList.threadUid() ? 0 : this.threadUnseen().length,
|
||||||
|
|
||||||
|
threadsLenText: () => {
|
||||||
|
const unseenLen = this.threadUnseenLen();
|
||||||
|
return this.threadsLen() + (unseenLen > 0 ? '/' + unseenLen : '');
|
||||||
|
},
|
||||||
|
|
||||||
isUnseen: () => !this.flags().includes('\\seen'),
|
isUnseen: () => !this.flags().includes('\\seen'),
|
||||||
isFlagged: () => this.flags().includes('\\flagged'),
|
isFlagged: () => this.flags().includes('\\flagged'),
|
||||||
|
isDeleted: () => this.flags().includes('\\deleted'),
|
||||||
// isJunk: () => this.flags().includes('$junk') && !this.flags().includes('$nonjunk'),
|
// isJunk: () => this.flags().includes('$junk') && !this.flags().includes('$nonjunk'),
|
||||||
// isPhishing: () => this.flags().includes('$phishing'),
|
// isPhishing: () => this.flags().includes('$phishing'),
|
||||||
|
|
||||||
tagOptions: () => {
|
tagOptions: () => {
|
||||||
const tagOptions = [];
|
const tagOptions = [];
|
||||||
FolderUserStore.currentFolder().permanentFlags.forEach(value => {
|
FolderUserStore.currentFolder().optionalTags().forEach(value => {
|
||||||
if (isAllowedKeyword(value)) {
|
let lower = value.toLowerCase();
|
||||||
let lower = value.toLowerCase();
|
tagOptions.push({
|
||||||
tagOptions.push({
|
css: 'msgflag-' + lower,
|
||||||
css: 'msgflag-' + lower,
|
value: value,
|
||||||
value: value,
|
checked: this.flags().includes(lower),
|
||||||
checked: this.flags().includes(lower),
|
label: i18n('MESSAGE_TAGS/'+lower, 0, value),
|
||||||
label: i18n('MESSAGE_TAGS/'+lower, 0, value),
|
toggle: (/*obj*/) => toggleTag(this, value)
|
||||||
toggle: (/*obj*/) => toggleTag(this, value)
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return tagOptions
|
return tagOptions
|
||||||
},
|
},
|
||||||
|
|
@ -174,14 +247,18 @@ export class MessageModel extends AbstractModel {
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.smimeSigned.subscribe(value =>
|
||||||
|
value?.body && MimeToMessage(value.body, this)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get requestHash() {
|
get requestHash() {
|
||||||
return b64EncodeJSONSafe({
|
return b64EncodeJSONSafe({
|
||||||
folder: this.folder,
|
folder: this.folder,
|
||||||
uid: this.uid,
|
uid: this.uid,
|
||||||
mimeType: 'message/rfc822',
|
mimeType: RFC822,
|
||||||
fileName: (this.subject() || 'message-' + this.hash) + '.eml',
|
fileName: (this.subject() || 'message') + '-' + this.hash + '.eml',
|
||||||
accountHash: SettingsGet('accountHash')
|
accountHash: SettingsGet('accountHash')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -191,23 +268,23 @@ export class MessageModel extends AbstractModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
spamStatus() {
|
spamStatus() {
|
||||||
let spam = this.spamResult();
|
let spam = this.spamResult;
|
||||||
return spam ? i18n(this.isSpam() ? 'GLOBAL/SPAM' : 'GLOBAL/NOT_SPAM') + ': ' + spam : '';
|
return spam ? i18n(this.isSpam ? 'GLOBAL/SPAM' : 'GLOBAL/NOT_SPAM') + ': ' + spam : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
friendlySize() {
|
friendlySize() {
|
||||||
return FileInfo.friendlySize(this.size());
|
return FileInfo.friendlySize(this.size);
|
||||||
}
|
}
|
||||||
|
|
||||||
computeSenderEmail() {
|
computeSenderEmail() {
|
||||||
const list = this[
|
const list = this[
|
||||||
[FolderUserStore.sentFolder(), FolderUserStore.draftsFolder()].includes(this.folder) ? 'to' : 'from'
|
[FolderUserStore.sentFolder(), FolderUserStore.draftsFolder()].includes(this.folder) ? 'to' : 'from'
|
||||||
];
|
];
|
||||||
this.senderEmailsString(list.toString(true));
|
this.senderEmailsString = list.toString(true);
|
||||||
this.senderClearEmailsString(list.map(email => email?.email).filter(email => email).join(', '));
|
this.senderClearEmailsString = list.map(email => email?.email).filter(email => email).join(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -218,8 +295,63 @@ export class MessageModel extends AbstractModel {
|
||||||
if (super.revivePropertiesFromJson(json)) {
|
if (super.revivePropertiesFromJson(json)) {
|
||||||
// this.foundCIDs = isArray(json.FoundCIDs) ? json.FoundCIDs : [];
|
// this.foundCIDs = isArray(json.FoundCIDs) ? json.FoundCIDs : [];
|
||||||
// this.attachments(AttachmentCollectionModel.reviveFromJson(json.attachments, this.foundCIDs));
|
// this.attachments(AttachmentCollectionModel.reviveFromJson(json.attachments, this.foundCIDs));
|
||||||
|
// this.headers(MimeHeaderCollectionModel.reviveFromJson(json.headers));
|
||||||
|
|
||||||
this.computeSenderEmail();
|
this.computeSenderEmail();
|
||||||
|
|
||||||
|
let value, headers = this.headers();
|
||||||
|
/* // These could be by Envelope or MIME
|
||||||
|
this.messageId = headers.valueByName('Message-Id');
|
||||||
|
this.subject(headers.valueByName('Subject'));
|
||||||
|
this.sender = EmailCollectionModel.fromString(headers.valueByName('Sender'));
|
||||||
|
this.from = EmailCollectionModel.fromArray(headers.valueByName('From'));
|
||||||
|
this.replyTo = EmailCollectionModel.fromArray(headers.valueByName('Reply-To'));
|
||||||
|
this.to = EmailCollectionModel.fromArray(headers.valueByName('To'));
|
||||||
|
this.cc = EmailCollectionModel.fromArray(headers.valueByName('Cc'));
|
||||||
|
this.bcc = EmailCollectionModel.fromArray(headers.valueByName('Bcc'));
|
||||||
|
this.inReplyTo = headers.valueByName('In-Reply-To');
|
||||||
|
|
||||||
|
this.deliveredTo = EmailCollectionModel.fromString(headers.valueByName('Delivered-To'));
|
||||||
|
*/
|
||||||
|
// Priority
|
||||||
|
value = headers.valueByName('X-MSMail-Priority')
|
||||||
|
|| headers.valueByName('Importance')
|
||||||
|
|| headers.valueByName('X-Priority');
|
||||||
|
if (value) {
|
||||||
|
if (/[h12]/.test(value[0])) {
|
||||||
|
this.priority = 1;
|
||||||
|
} else if (/[l45]/.test(value[0])) {
|
||||||
|
this.priority = 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe links
|
||||||
|
if (value = headers.valueByName('List-Unsubscribe')) {
|
||||||
|
this.unsubsribeLinks(value.split(',').map(
|
||||||
|
link => link.replace(/^[ <>]+|[ <>]+$/g, '')
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headers.valueByName('X-Virus')) {
|
||||||
|
this.hasVirus = true;
|
||||||
|
}
|
||||||
|
if (value = headers.valueByName('X-Virus-Status')) {
|
||||||
|
if (value.includes('infected')) {
|
||||||
|
this.hasVirus = true;
|
||||||
|
} else if (value.includes('clean')) {
|
||||||
|
this.hasVirus = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
if (value = headers.valueByName('X-Virus-Scanned')) {
|
||||||
|
this.virusScanned(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://autocrypt.org/level1.html#the-autocrypt-header
|
||||||
|
headers.valuesByName('Autocrypt').forEach(value => {
|
||||||
|
this.autocrypt.push(new MimeHeaderAutocryptModel(value));
|
||||||
|
});
|
||||||
|
*/
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -230,12 +362,11 @@ export class MessageModel extends AbstractModel {
|
||||||
lineAsCss(flags=1) {
|
lineAsCss(flags=1) {
|
||||||
let classes = [];
|
let classes = [];
|
||||||
forEachObjectEntry({
|
forEachObjectEntry({
|
||||||
deleted: this.deleted(),
|
|
||||||
selected: this.selected(),
|
selected: this.selected(),
|
||||||
checked: this.checked(),
|
checked: this.checked(),
|
||||||
unseen: this.isUnseen(),
|
unseen: this.isUnseen(),
|
||||||
focused: this.focused(),
|
focused: this.focused(),
|
||||||
priorityHigh: this.priority() === 1,
|
priorityHigh: this.priority === 1,
|
||||||
withAttachments: !!this.attachments().length,
|
withAttachments: !!this.attachments().length,
|
||||||
// hasChildrenMessage: 1 < this.threadsLen()
|
// hasChildrenMessage: 1 < this.threadsLen()
|
||||||
}, (key, value) => value && classes.push(key));
|
}, (key, value) => value && classes.push(key));
|
||||||
|
|
@ -301,8 +432,10 @@ export class MessageModel extends AbstractModel {
|
||||||
let result = msgHtml(this);
|
let result = msgHtml(this);
|
||||||
this.hasExternals(result.hasExternals);
|
this.hasExternals(result.hasExternals);
|
||||||
this.hasImages(!!result.hasExternals);
|
this.hasImages(!!result.hasExternals);
|
||||||
|
this.hasTracking(!!result.tracking);
|
||||||
|
this.linkedData(result.linkedData);
|
||||||
body.innerHTML = result.html;
|
body.innerHTML = result.html;
|
||||||
if (!this.isSpam() && FolderUserStore.spamFolder() != this.folder) {
|
if (!this.isSpam && FolderUserStore.spamFolder() != this.folder) {
|
||||||
if ('always' === SettingsUserStore.viewImages()) {
|
if ('always' === SettingsUserStore.viewImages()) {
|
||||||
this.showExternalImages();
|
this.showExternalImages();
|
||||||
}
|
}
|
||||||
|
|
@ -316,7 +449,7 @@ export class MessageModel extends AbstractModel {
|
||||||
? this.plain()
|
? this.plain()
|
||||||
.replace(/-----BEGIN PGP (SIGNED MESSAGE-----(\r?\n[^\r\n]+)+|SIGNATURE-----[\s\S]*)/sg, '')
|
.replace(/-----BEGIN PGP (SIGNED MESSAGE-----(\r?\n[^\r\n]+)+|SIGNATURE-----[\s\S]*)/sg, '')
|
||||||
.trim()
|
.trim()
|
||||||
: htmlToPlain(body.innerHTML)
|
: htmlToPlain(body.innerHTML || msgHtml(this).html)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
this.hasImages(false);
|
this.hasImages(false);
|
||||||
|
|
@ -326,6 +459,7 @@ export class MessageModel extends AbstractModel {
|
||||||
this.isHtml(html);
|
this.isHtml(html);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
viewHtml() {
|
viewHtml() {
|
||||||
|
|
@ -336,14 +470,22 @@ export class MessageModel extends AbstractModel {
|
||||||
return this.viewBody(false);
|
return this.viewBody(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
viewPopupMessage(print) {
|
swapColors() {
|
||||||
|
const cl = this.body?.classList;
|
||||||
|
cl && cl.toggle('swapColors');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {boolean=} print = false
|
||||||
|
*/
|
||||||
|
popupMessage(print) {
|
||||||
const
|
const
|
||||||
timeStampInUTC = this.dateTimestamp() || 0,
|
timeStampInUTC = this.dateTimestamp() || 0,
|
||||||
ccLine = this.cc.toString(),
|
ccLine = this.cc.toString(),
|
||||||
bccLine = this.bcc.toString(),
|
bccLine = this.bcc.toString(),
|
||||||
m = 0 < timeStampInUTC ? new Date(timeStampInUTC * 1000) : null,
|
m = 0 < timeStampInUTC ? new Date(timeStampInUTC * 1000) : null,
|
||||||
win = open('', 'sm-msg-'+this.requestHash
|
win = open('', 'sm-msg-'+this.requestHash
|
||||||
/*,newWindow ? 'innerWidth=' + elementById('V-MailMessageView').clientWidth : ''*/
|
,SettingsUserStore.messageNewWindow() ? 'innerWidth=' + elementById('V-MailMessageView').clientWidth : ''
|
||||||
),
|
),
|
||||||
sdoc = win.document,
|
sdoc = win.document,
|
||||||
subject = encodeHtml(this.subject()),
|
subject = encodeHtml(this.subject()),
|
||||||
|
|
@ -364,25 +506,11 @@ export class MessageModel extends AbstractModel {
|
||||||
.replace('</body>', `<div id="attachments">${attachments}</div></body>`)
|
.replace('</body>', `<div id="attachments">${attachments}</div></body>`)
|
||||||
);
|
);
|
||||||
sdoc.close();
|
sdoc.close();
|
||||||
print && setTimeout(() => win.print(), 100);
|
(true === print) && setTimeout(() => win.print(), 100);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {boolean=} print = false
|
|
||||||
*/
|
|
||||||
popupMessage() {
|
|
||||||
this.viewPopupMessage();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
printMessage() {
|
printMessage() {
|
||||||
this.viewPopupMessage(true);
|
this.popupMessage(true);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
generateUid() {
|
|
||||||
return this.folder + '/' + this.uid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -435,7 +563,7 @@ export class MessageModel extends AbstractModel {
|
||||||
hasImages = true;
|
hasImages = true;
|
||||||
},
|
},
|
||||||
attr = 'data-x-src',
|
attr = 'data-x-src',
|
||||||
src, useProxy = !!SettingsGet('useLocalProxyForExternalImages');
|
src, useProxy = !!SettingsGet('proxyExternalImages');
|
||||||
body.querySelectorAll('img[' + attr + ']').forEach(node => {
|
body.querySelectorAll('img[' + attr + ']').forEach(node => {
|
||||||
src = node.getAttribute(attr);
|
src = node.getAttribute(attr);
|
||||||
if (isValid(src)) {
|
if (isValid(src)) {
|
||||||
|
|
|
||||||
|
|
@ -30,12 +30,13 @@ export class MessageCollectionModel extends AbstractCollectionModel
|
||||||
let msg = MessageUserStore.message();
|
let msg = MessageUserStore.message();
|
||||||
return super.reviveFromJson(object, message => {
|
return super.reviveFromJson(object, message => {
|
||||||
// If message is currently viewed, use that.
|
// If message is currently viewed, use that.
|
||||||
// Maybe then use msg.revivePropertiesFromJson(message) ?
|
if (msg && msg.hash === message.hash) {
|
||||||
message = (msg && msg.hash === message.hash) ? msg : MessageModel.reviveFromJson(message);
|
msg.revivePropertiesFromJson(message);
|
||||||
if (message) {
|
message = msg;
|
||||||
message.deleted(false);
|
} else {
|
||||||
return message;
|
message = MessageModel.reviveFromJson(message);
|
||||||
}
|
}
|
||||||
|
return message;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
dev/Model/MimeHeader.js
Normal file
13
dev/Model/MimeHeader.js
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import ko from 'ko';
|
||||||
|
|
||||||
|
import { AbstractModel } from 'Knoin/AbstractModel';
|
||||||
|
|
||||||
|
export class MimeHeaderModel extends AbstractModel
|
||||||
|
{
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.name = '';
|
||||||
|
this.value = '';
|
||||||
|
this.parameters = ko.observableArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
34
dev/Model/MimeHeaderAutocrypt.js
Normal file
34
dev/Model/MimeHeaderAutocrypt.js
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
//import { AbstractModel } from 'Knoin/AbstractModel';
|
||||||
|
|
||||||
|
export class MimeHeaderAutocryptModel/* extends AbstractModel*/
|
||||||
|
{
|
||||||
|
constructor(value) {
|
||||||
|
// super();
|
||||||
|
this.addr = '';
|
||||||
|
this.prefer_encrypt = 'nopreference', // nopreference or mutual
|
||||||
|
this.keydata = '';
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
value.split(';').forEach(entry => {
|
||||||
|
entry = entry.match(/^([^=]+)=(.*)$/);
|
||||||
|
const trim = str => (str || '').trim().replace(/^["']|["']+$/g, '');
|
||||||
|
this[trim(entry[1]).replace('-', '_')] = trim(entry[2]);
|
||||||
|
});
|
||||||
|
this.keydata = this.keydata.replace(/\s+/g, '\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
let result = `addr=${this.addr}; `;
|
||||||
|
if ('mutual' === this.prefer_encrypt) {
|
||||||
|
result += 'prefer-encrypt=mutual; ';
|
||||||
|
}
|
||||||
|
return result + 'keydata=' + this.keydata.replace(/\n/g, '\n ');
|
||||||
|
}
|
||||||
|
|
||||||
|
pem() {
|
||||||
|
return '-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n'
|
||||||
|
+ this.keydata
|
||||||
|
+ '\n-----END PGP PUBLIC KEY BLOCK-----';
|
||||||
|
}
|
||||||
|
}
|
||||||
38
dev/Model/MimeHeaderCollection.js
Normal file
38
dev/Model/MimeHeaderCollection.js
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { AbstractCollectionModel } from 'Model/AbstractCollection';
|
||||||
|
import { MimeHeaderModel } from 'Model/MimeHeader';
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
export class MimeHeaderCollectionModel extends AbstractCollectionModel
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param {?Array} json
|
||||||
|
* @returns {MimeHeaderCollectionModel}
|
||||||
|
*/
|
||||||
|
static reviveFromJson(items) {
|
||||||
|
return super.reviveFromJson(items, header => MimeHeaderModel.reviveFromJson(header));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
* @returns {?MimeHeader}
|
||||||
|
*/
|
||||||
|
getByName(name)
|
||||||
|
{
|
||||||
|
name = name.toLowerCase();
|
||||||
|
return this.find(header => header.name.toLowerCase() === name);
|
||||||
|
}
|
||||||
|
|
||||||
|
valueByName(name)
|
||||||
|
{
|
||||||
|
const header = this.getByName(name);
|
||||||
|
return header ? header.value : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
valuesByName(name)
|
||||||
|
{
|
||||||
|
name = name.toLowerCase();
|
||||||
|
return this.filter(header => header.name.toLowerCase() === name).map(header => header.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -8,18 +8,17 @@ let iJsonErrorCount = 0;
|
||||||
const getURL = (add = '') => serverRequest('Json') + pString(add),
|
const getURL = (add = '') => serverRequest('Json') + pString(add),
|
||||||
|
|
||||||
checkResponseError = data => {
|
checkResponseError = data => {
|
||||||
const err = data ? data.ErrorCode : null;
|
const err = data ? data.code : null;
|
||||||
if (Notifications.InvalidToken === err) {
|
if (Notifications.InvalidToken === err) {
|
||||||
console.error(getNotification(err));
|
console.error(getNotification(err) + ` (${data.messageAdditional})`);
|
||||||
// alert(getNotification(err));
|
// alert(getNotification(err));
|
||||||
rl.logoutReload();
|
setTimeout(rl.logoutReload, 5000);
|
||||||
} else if ([
|
} else if ([
|
||||||
Notifications.AuthError,
|
Notifications.AuthError,
|
||||||
Notifications.ConnectionError,
|
Notifications.ConnectionError,
|
||||||
Notifications.DomainNotAllowed,
|
Notifications.DomainNotAllowed,
|
||||||
Notifications.AccountNotAllowed,
|
Notifications.AccountNotAllowed,
|
||||||
Notifications.MailServerError,
|
Notifications.MailServerError,
|
||||||
Notifications.UnknownNotification,
|
|
||||||
Notifications.UnknownError
|
Notifications.UnknownError
|
||||||
].includes(err)
|
].includes(err)
|
||||||
) {
|
) {
|
||||||
|
|
@ -132,8 +131,8 @@ export class AbstractFetchRemote
|
||||||
|
|
||||||
fetchJSON(sAction, getURL(sGetAdd),
|
fetchJSON(sAction, getURL(sGetAdd),
|
||||||
sGetAdd ? null : (params || {}),
|
sGetAdd ? null : (params || {}),
|
||||||
undefined === iTimeout ? 30000 : pInt(iTimeout),
|
pInt(iTimeout ?? 30000),
|
||||||
data => {
|
async data => {
|
||||||
let iError = 0;
|
let iError = 0;
|
||||||
if (data) {
|
if (data) {
|
||||||
/*
|
/*
|
||||||
|
|
@ -145,10 +144,14 @@ export class AbstractFetchRemote
|
||||||
iJsonErrorCount = 0;
|
iJsonErrorCount = 0;
|
||||||
} else {
|
} else {
|
||||||
checkResponseError(data);
|
checkResponseError(data);
|
||||||
iError = data.ErrorCode || Notifications.UnknownError
|
iError = data.code || Notifications.UnknownError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (111 === iError && rl.app.ask && await rl.app.ask.cryptkey()) {
|
||||||
|
return this.request(sAction, fCallback, params, iTimeout, sGetAdd);
|
||||||
|
}
|
||||||
|
|
||||||
fCallback && fCallback(
|
fCallback && fCallback(
|
||||||
iError,
|
iError,
|
||||||
data,
|
data,
|
||||||
|
|
@ -170,13 +173,6 @@ export class AbstractFetchRemote
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {?Function} fCallback
|
|
||||||
*/
|
|
||||||
getPublicKey(fCallback) {
|
|
||||||
this.request('GetPublicKey', fCallback);
|
|
||||||
}
|
|
||||||
|
|
||||||
setTrigger(trigger, value) {
|
setTrigger(trigger, value) {
|
||||||
if (trigger) {
|
if (trigger) {
|
||||||
value = !!value;
|
value = !!value;
|
||||||
|
|
@ -193,12 +189,16 @@ export class AbstractFetchRemote
|
||||||
post(action, fTrigger, params, timeOut) {
|
post(action, fTrigger, params, timeOut) {
|
||||||
this.setTrigger(fTrigger, true);
|
this.setTrigger(fTrigger, true);
|
||||||
return fetchJSON(action, getURL(), params || {}, pInt(timeOut, 30000),
|
return fetchJSON(action, getURL(), params || {}, pInt(timeOut, 30000),
|
||||||
data => {
|
async data => {
|
||||||
abort(action, 0, 1);
|
abort(action, 0, 1);
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return Promise.reject(new FetchError(Notifications.JsonParse));
|
return Promise.reject(new FetchError(Notifications.JsonParse));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (111 === data?.code && rl.app.ask && await rl.app.ask.cryptkey()) {
|
||||||
|
return this.post(action, fTrigger, params, timeOut);
|
||||||
|
}
|
||||||
/*
|
/*
|
||||||
let isCached = false, type = '';
|
let isCached = false, type = '';
|
||||||
if (data?.epoch) {
|
if (data?.epoch) {
|
||||||
|
|
@ -222,8 +222,8 @@ export class AbstractFetchRemote
|
||||||
if (!data.Result || action !== data.Action) {
|
if (!data.Result || action !== data.Action) {
|
||||||
checkResponseError(data);
|
checkResponseError(data);
|
||||||
return Promise.reject(new FetchError(
|
return Promise.reject(new FetchError(
|
||||||
data ? data.ErrorCode : 0,
|
data ? data.code : 0,
|
||||||
data ? (data.ErrorMessageAdditional || data.ErrorMessage) : ''
|
data ? (data.messageAdditional || data.message) : ''
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -64,16 +64,6 @@ class RemoteUserFetch extends AbstractFetchRemote {
|
||||||
[key]: value
|
[key]: value
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
folderMove(sPrevFolderFullName, sNewFolderFullName, bSubscribe) {
|
|
||||||
return this.post('FolderMove', FolderUserStore.foldersRenaming, {
|
|
||||||
folder: sPrevFolderFullName,
|
|
||||||
newFolder: sNewFolderFullName,
|
|
||||||
subscribe: bSubscribe ? 1 : 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new RemoteUserFetch();
|
export default new RemoteUserFetch();
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export class AbstractSettingsScreen extends AbstractScreen {
|
||||||
|
|
||||||
if (RoutedSettingsViewModel) {
|
if (RoutedSettingsViewModel) {
|
||||||
// const vmPlace = elementById('V-SettingsPane') || elementById('V-AdminPane);
|
// const vmPlace = elementById('V-SettingsPane') || elementById('V-AdminPane);
|
||||||
const vmPlace = this.viewModels[1].__dom,
|
const vmPlace = this.viewModels[1].__vm.viewModelDom,
|
||||||
SettingsViewModelClass = RoutedSettingsViewModel.vmc;
|
SettingsViewModelClass = RoutedSettingsViewModel.vmc;
|
||||||
if (SettingsViewModelClass.__vm) {
|
if (SettingsViewModelClass.__vm) {
|
||||||
settingsScreen = SettingsViewModelClass.__vm;
|
settingsScreen = SettingsViewModelClass.__vm;
|
||||||
|
|
@ -46,7 +46,6 @@ export class AbstractSettingsScreen extends AbstractScreen {
|
||||||
settingsScreen.viewModelDom = viewModelDom;
|
settingsScreen.viewModelDom = viewModelDom;
|
||||||
settingsScreen.viewModelTemplateID = RoutedSettingsViewModel.template;
|
settingsScreen.viewModelTemplateID = RoutedSettingsViewModel.template;
|
||||||
|
|
||||||
SettingsViewModelClass.__dom = viewModelDom;
|
|
||||||
SettingsViewModelClass.__vm = settingsScreen;
|
SettingsViewModelClass.__vm = settingsScreen;
|
||||||
|
|
||||||
fireEvent('rl-view-model.create', settingsScreen);
|
fireEvent('rl-view-model.create', settingsScreen);
|
||||||
|
|
@ -118,7 +117,7 @@ export class AbstractSettingsScreen extends AbstractScreen {
|
||||||
rules = {
|
rules = {
|
||||||
subname: /^(.*)$/,
|
subname: /^(.*)$/,
|
||||||
normalize_: (rquest, vals) => {
|
normalize_: (rquest, vals) => {
|
||||||
vals.subname = null == vals.subname ? defaultRoute : pString(vals.subname);
|
vals.subname = pString(vals.subname ?? defaultRoute);
|
||||||
return [vals.subname];
|
return [vals.subname];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ export class MailBoxUserScreen extends AbstractScreen {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
onRoute(folderHash, page, search, messageUid) {
|
onRoute(folderHash, page, search, messageUid) {
|
||||||
|
// Only works when FolderUserStore.folderList() is loaded
|
||||||
const folder = getFolderFromHashMap(folderHash.replace(/~([\d]+)$/, ''));
|
const folder = getFolderFromHashMap(folderHash.replace(/~([\d]+)$/, ''));
|
||||||
if (folder) {
|
if (folder) {
|
||||||
FolderUserStore.currentFolder(folder);
|
FolderUserStore.currentFolder(folder);
|
||||||
|
|
@ -108,7 +109,7 @@ export class MailBoxUserScreen extends AbstractScreen {
|
||||||
*/
|
*/
|
||||||
onBuild() {
|
onBuild() {
|
||||||
doc.addEventListener('click', event =>
|
doc.addEventListener('click', event =>
|
||||||
event.target.closest('#rl-right') && moveAction(false)
|
event.target.closest('#rl-right') && moveAction(0)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,10 @@ export class AdminSettingsAbout /*extends AbstractViewSettings*/ {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearCache() {
|
||||||
|
Remote.request('AdminClearCache');
|
||||||
|
}
|
||||||
|
|
||||||
updateCoreData() {
|
updateCoreData() {
|
||||||
if (!this.coreUpdating()) {
|
if (!this.coreUpdating()) {
|
||||||
this.coreUpdating(true);
|
this.coreUpdating(true);
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,40 @@ import ko from 'ko';
|
||||||
|
|
||||||
import Remote from 'Remote/Admin/Fetch';
|
import Remote from 'Remote/Admin/Fetch';
|
||||||
import { forEachObjectEntry } from 'Common/Utils';
|
import { forEachObjectEntry } from 'Common/Utils';
|
||||||
|
import { SettingsAdmin } from 'Common/Globals';
|
||||||
|
import { LanguageStore } from 'Stores/Language';
|
||||||
|
import { ThemeStore } from 'Stores/Theme';
|
||||||
|
|
||||||
export class AdminSettingsConfig /*extends AbstractViewSettings*/ {
|
export class AdminSettingsConfig /*extends AbstractViewSettings*/ {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.config = ko.observableArray();
|
this.config = ko.observableArray();
|
||||||
|
this.search = ko.observableArray();
|
||||||
this.saved = ko.observable(false).extend({ falseTimeout: 5000 });
|
this.saved = ko.observable(false).extend({ falseTimeout: 5000 });
|
||||||
|
|
||||||
|
this.search.subscribe(value => {
|
||||||
|
const v = value.toLowerCase(),
|
||||||
|
qsa = (node, selector, fn) => node.querySelectorAll(selector).forEach(fn),
|
||||||
|
match = node => node.textContent.toLowerCase().includes(v);
|
||||||
|
if (v.length) {
|
||||||
|
qsa(this.viewModelDom, 'tbody', tbody => {
|
||||||
|
let show = match(tbody.querySelector('th'));
|
||||||
|
if (show) {
|
||||||
|
qsa(tbody, '[hidden]', n => n.hidden = false);
|
||||||
|
} else {
|
||||||
|
qsa(tbody, 'tbody td:first-child', td => {
|
||||||
|
let hide = !match(td);
|
||||||
|
show = show || !hide;
|
||||||
|
// td.closest('tr').hidden = hide;
|
||||||
|
td.parentNode.hidden = hide;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
tbody.hidden = !show;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
qsa(this.viewModelDom, 'table [hidden]', n => n.hidden = false);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeShow() {
|
beforeShow() {
|
||||||
|
|
@ -28,13 +56,19 @@ export class AdminSettingsConfig /*extends AbstractViewSettings*/ {
|
||||||
items: []
|
items: []
|
||||||
};
|
};
|
||||||
forEachObjectEntry(items, (skey, item) => {
|
forEachObjectEntry(items, (skey, item) => {
|
||||||
|
if ('language' === skey) {
|
||||||
|
item[2] = ('webmail' === key) ? LanguageStore.languages : SettingsAdmin('languages');
|
||||||
|
} else if ('theme' === skey) {
|
||||||
|
item[2] = ThemeStore.themes;
|
||||||
|
}
|
||||||
'admin_password' === skey ||
|
'admin_password' === skey ||
|
||||||
section.items.push({
|
section.items.push({
|
||||||
key: `config[${key}][${skey}]`,
|
key: `config[${key}][${skey}]`,
|
||||||
name: skey,
|
name: skey,
|
||||||
value: item[0],
|
value: item[0],
|
||||||
type: getInputType(item[0], skey.includes('password')),
|
type: getInputType(item[0], skey.includes('password')),
|
||||||
comment: item[1]
|
comment: item[1],
|
||||||
|
options: item[2]
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
cfg.push(section);
|
cfg.push(section);
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ export class AdminSettingsContacts extends AbstractViewSettings {
|
||||||
this.addSetting('contactsMySQLSSLVerify');
|
this.addSetting('contactsMySQLSSLVerify');
|
||||||
this.addSetting('contactsMySQLSSLCiphers');
|
this.addSetting('contactsMySQLSSLCiphers');
|
||||||
|
|
||||||
|
this.addSetting('contactsSQLiteGlobal');
|
||||||
|
|
||||||
addObservablesTo(this, {
|
addObservablesTo(this, {
|
||||||
testing: false,
|
testing: false,
|
||||||
testContactsSuccess: false,
|
testContactsSuccess: false,
|
||||||
|
|
@ -102,7 +104,8 @@ export class AdminSettingsContacts extends AbstractViewSettings {
|
||||||
PdoPassword: this.contactsPdoPassword(),
|
PdoPassword: this.contactsPdoPassword(),
|
||||||
MySQLSSLCA: this.contactsMySQLSSLCA(),
|
MySQLSSLCA: this.contactsMySQLSSLCA(),
|
||||||
MySQLSSLVerify: this.contactsMySQLSSLVerify(),
|
MySQLSSLVerify: this.contactsMySQLSSLVerify(),
|
||||||
MySQLSSLCiphers: this.contactsMySQLSSLCiphers()
|
MySQLSSLCiphers: this.contactsMySQLSSLCiphers(),
|
||||||
|
SQLiteGlobal: this.contactsSQLiteGlobal()
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,9 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
|
||||||
import {
|
|
||||||
isArray
|
|
||||||
} from 'Common/Utils';
|
|
||||||
|
|
||||||
import { addObservablesTo, addSubscribablesTo, addComputablesTo } from 'External/ko';
|
import { addObservablesTo, addSubscribablesTo, addComputablesTo } from 'External/ko';
|
||||||
|
|
||||||
import { SaveSettingStatus } from 'Common/Enums';
|
import { SaveSettingStatus } from 'Common/Enums';
|
||||||
import { Settings, SettingsGet, SettingsCapa } from 'Common/Globals';
|
import { SettingsAdmin, SettingsGet, SettingsCapa } from 'Common/Globals';
|
||||||
import { translatorReload, convertLangName } from 'Common/Translator';
|
import { translatorReload, convertLangName } from 'Common/Translator';
|
||||||
|
|
||||||
import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
||||||
|
|
@ -24,11 +20,7 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.language = LanguageStore.language;
|
this.language = LanguageStore.language;
|
||||||
this.languages = LanguageStore.languages;
|
this.languageAdmin = ko.observable(SettingsAdmin('language'));
|
||||||
|
|
||||||
const aLanguagesAdmin = Settings.app('languagesAdmin');
|
|
||||||
this.languagesAdmin = ko.observableArray(isArray(aLanguagesAdmin) ? aLanguagesAdmin : []);
|
|
||||||
this.languageAdmin = ko.observable(SettingsGet('languageAdmin'));
|
|
||||||
|
|
||||||
this.theme = ThemeStore.theme;
|
this.theme = ThemeStore.theme;
|
||||||
this.themes = ThemeStore.themes;
|
this.themes = ThemeStore.themes;
|
||||||
|
|
@ -107,14 +99,18 @@ export class AdminSettingsGeneral extends AbstractViewSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
selectLanguage() {
|
selectLanguage() {
|
||||||
showScreenPopup(LanguagesPopupView, [this.language, this.languages(), LanguageStore.userLanguage()]);
|
showScreenPopup(LanguagesPopupView, [
|
||||||
|
this.language,
|
||||||
|
LanguageStore.languages,
|
||||||
|
LanguageStore.userLanguage()
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectLanguageAdmin() {
|
selectLanguageAdmin() {
|
||||||
showScreenPopup(LanguagesPopupView, [
|
showScreenPopup(LanguagesPopupView, [
|
||||||
this.languageAdmin,
|
this.languageAdmin,
|
||||||
this.languagesAdmin(),
|
SettingsAdmin('languages'),
|
||||||
SettingsGet('languageUsers')
|
SettingsAdmin('clientLanguage')
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ export class AdminSettingsPackages extends AbstractViewSettings {
|
||||||
if (iError) {
|
if (iError) {
|
||||||
this.packagesError(
|
this.packagesError(
|
||||||
getNotification(install ? Notifications.CantInstallPackage : Notifications.CantDeletePackage)
|
getNotification(install ? Notifications.CantInstallPackage : Notifications.CantDeletePackage)
|
||||||
+ (data.ErrorMessage ? ':\n' + data.ErrorMessage : '')
|
+ (data.message ? ':\n' + data.message : '')
|
||||||
);
|
);
|
||||||
} else if (data.Result.Reload) {
|
} else if (data.Result.Reload) {
|
||||||
location.reload();
|
location.reload();
|
||||||
|
|
@ -113,8 +113,8 @@ export class AdminSettingsPackages extends AbstractViewSettings {
|
||||||
if (iError) {
|
if (iError) {
|
||||||
plugin.enabled(disable);
|
plugin.enabled(disable);
|
||||||
this.packagesError(
|
this.packagesError(
|
||||||
(Notifications.UnsupportedPluginPackage === iError && data?.ErrorMessage)
|
(Notifications.UnsupportedPluginPackage === iError && data?.message)
|
||||||
? data.ErrorMessage
|
? data.message
|
||||||
: getNotification(iError)
|
: getNotification(iError)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.addSettings(['useLocalProxyForExternalImages']);
|
this.addSettings(['proxyExternalImages', 'autoVerifySignatures']);
|
||||||
|
|
||||||
this.weakPassword = rl.app.weakPassword;
|
this.weakPassword = rl.app.weakPassword;
|
||||||
|
|
||||||
|
|
@ -28,6 +28,7 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
|
||||||
|
|
||||||
viewQRCode: '',
|
viewQRCode: '',
|
||||||
|
|
||||||
|
capaGnuPG: SettingsCapa('GnuPG'),
|
||||||
capaOpenPGP: SettingsCapa('OpenPGP')
|
capaOpenPGP: SettingsCapa('OpenPGP')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -65,7 +66,8 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
|
||||||
|
|
||||||
adminPasswordNew2: reset,
|
adminPasswordNew2: reset,
|
||||||
|
|
||||||
capaOpenPGP: value => Remote.saveSetting('CapaOpenPGP', value)
|
capaGnuPG: value => Remote.saveSetting('capaGnuPG', value),
|
||||||
|
capaOpenPGP: value => Remote.saveSetting('capaOpenPGP', value)
|
||||||
});
|
});
|
||||||
|
|
||||||
this.adminTOTP(SettingsGet('adminTOTP'));
|
this.adminTOTP(SettingsGet('adminTOTP'));
|
||||||
|
|
@ -75,6 +77,16 @@ export class AdminSettingsSecurity extends AbstractViewSettings {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateTOTP() {
|
||||||
|
let CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567',
|
||||||
|
length = 16,
|
||||||
|
secret = '';
|
||||||
|
while (0 < length--) {
|
||||||
|
secret += CHARS[Math.floor(Math.random() * 32)];
|
||||||
|
}
|
||||||
|
this.adminTOTP(secret);
|
||||||
|
}
|
||||||
|
|
||||||
saveAdminUserCommand() {
|
saveAdminUserCommand() {
|
||||||
if (!this.adminLogin().trim()) {
|
if (!this.adminLogin().trim()) {
|
||||||
this.adminLoginError(true);
|
this.adminLoginError(true);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import ko from 'ko';
|
||||||
|
|
||||||
//import { koComputable } from 'External/ko';
|
//import { koComputable } from 'External/ko';
|
||||||
import { SettingsCapa, SettingsGet } from 'Common/Globals';
|
import { SettingsCapa, SettingsGet } from 'Common/Globals';
|
||||||
|
import { loadAccountsAndIdentities, editIdentity } from 'Common/UtilsUser';
|
||||||
|
|
||||||
import { AccountUserStore } from 'Stores/User/Account';
|
import { AccountUserStore } from 'Stores/User/Account';
|
||||||
import { IdentityUserStore } from 'Stores/User/Identity';
|
import { IdentityUserStore } from 'Stores/User/Identity';
|
||||||
|
|
@ -11,7 +12,6 @@ import Remote from 'Remote/User/Fetch';
|
||||||
import { showScreenPopup } from 'Knoin/Knoin';
|
import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
|
|
||||||
import { AccountPopupView } from 'View/Popup/Account';
|
import { AccountPopupView } from 'View/Popup/Account';
|
||||||
import { IdentityPopupView } from 'View/Popup/Identity';
|
|
||||||
|
|
||||||
export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
|
export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -43,11 +43,11 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
|
||||||
}
|
}
|
||||||
|
|
||||||
addNewIdentity() {
|
addNewIdentity() {
|
||||||
showScreenPopup(IdentityPopupView);
|
editIdentity();
|
||||||
}
|
}
|
||||||
|
|
||||||
editIdentity(identity) {
|
editIdentity(identity) {
|
||||||
showScreenPopup(IdentityPopupView, [identity]);
|
editIdentity(identity);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -64,7 +64,7 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
|
||||||
rl.route.root();
|
rl.route.root();
|
||||||
setTimeout(() => location.reload(), 1);
|
setTimeout(() => location.reload(), 1);
|
||||||
} else {
|
} else {
|
||||||
rl.app.accountsAndIdentities();
|
loadAccountsAndIdentities();
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
emailToDelete: accountToRemove.email
|
emailToDelete: accountToRemove.email
|
||||||
|
|
@ -88,7 +88,7 @@ export class UserSettingsAccounts /*extends AbstractViewSettings*/ {
|
||||||
|
|
||||||
accountsAndIdentitiesAfterMove() {
|
accountsAndIdentitiesAfterMove() {
|
||||||
Remote.request('AccountsAndIdentitiesSortOrder', null, {
|
Remote.request('AccountsAndIdentitiesSortOrder', null, {
|
||||||
Accounts: AccountUserStore.getEmailAddresses().filter(v => v != SettingsGet('mainEmail')),
|
Accounts: AccountUserStore.filter(item => item.isAdditional()).map(item => item.email),
|
||||||
Identities: IdentityUserStore.map(item => (item ? item.id() : ""))
|
Identities: IdentityUserStore.map(item => (item ? item.id() : ""))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
import { showScreenPopup } from 'Knoin/Knoin';
|
import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
|
|
||||||
|
//import { FolderPopupView } from 'View/Popup/Folder';
|
||||||
import { FolderCreatePopupView } from 'View/Popup/FolderCreate';
|
import { FolderCreatePopupView } from 'View/Popup/FolderCreate';
|
||||||
import { FolderSystemPopupView } from 'View/Popup/FolderSystem';
|
import { FolderSystemPopupView } from 'View/Popup/FolderSystem';
|
||||||
|
|
||||||
|
|
@ -41,8 +42,8 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
|
||||||
|
|
||||||
this.displaySpecSetting = FolderUserStore.displaySpecSetting;
|
this.displaySpecSetting = FolderUserStore.displaySpecSetting;
|
||||||
this.folderList = FolderUserStore.folderList;
|
this.folderList = FolderUserStore.folderList;
|
||||||
this.folderListOptimized = FolderUserStore.folderListOptimized;
|
this.folderListOptimized = FolderUserStore.optimized;
|
||||||
this.folderListError = FolderUserStore.folderListError;
|
this.folderListError = FolderUserStore.error;
|
||||||
this.hideUnsubscribed = SettingsUserStore.hideUnsubscribed;
|
this.hideUnsubscribed = SettingsUserStore.hideUnsubscribed;
|
||||||
this.unhideKolabFolders = SettingsUserStore.unhideKolabFolders;
|
this.unhideKolabFolders = SettingsUserStore.unhideKolabFolders;
|
||||||
|
|
||||||
|
|
@ -55,7 +56,7 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
|
||||||
}
|
}
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
FolderUserStore.folderListError('');
|
FolderUserStore.error('');
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
onBuild(oDom) {
|
onBuild(oDom) {
|
||||||
|
|
@ -75,7 +76,7 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
|
||||||
&& folderToRemove.askDelete()
|
&& folderToRemove.askDelete()
|
||||||
) {
|
) {
|
||||||
if (0 < folderToRemove.totalEmails()) {
|
if (0 < folderToRemove.totalEmails()) {
|
||||||
// FolderUserStore.folderListError(getNotification(Notifications.CantDeleteNonEmptyFolder));
|
// FolderUserStore.error(getNotification(Notifications.CantDeleteNonEmptyFolder));
|
||||||
folderToRemove.errorMsg(getNotification(Notifications.CantDeleteNonEmptyFolder));
|
folderToRemove.errorMsg(getNotification(Notifications.CantDeleteNonEmptyFolder));
|
||||||
} else {
|
} else {
|
||||||
folderForDeletion(null);
|
folderForDeletion(null);
|
||||||
|
|
@ -96,7 +97,7 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
FolderUserStore.folderListError(
|
FolderUserStore.error(
|
||||||
getNotification(error.code, '', Notifications.CantDeleteFolder)
|
getNotification(error.code, '', Notifications.CantDeleteFolder)
|
||||||
+ '.\n' + error.message
|
+ '.\n' + error.message
|
||||||
);
|
);
|
||||||
|
|
@ -108,7 +109,7 @@ export class UserSettingsFolders /*extends AbstractViewSettings*/ {
|
||||||
}
|
}
|
||||||
|
|
||||||
hideError() {
|
hideError() {
|
||||||
this.folderListError('');
|
FolderUserStore.error('');
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleFolderKolabType(folder, event) {
|
toggleFolderKolabType(folder, event) {
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,17 @@ import { SaveSettingStatus } from 'Common/Enums';
|
||||||
import { LayoutSideView, LayoutBottomView } from 'Common/EnumsUser';
|
import { LayoutSideView, LayoutBottomView } from 'Common/EnumsUser';
|
||||||
import { setRefreshFoldersInterval } from 'Common/Folders';
|
import { setRefreshFoldersInterval } from 'Common/Folders';
|
||||||
import { Settings, SettingsGet } from 'Common/Globals';
|
import { Settings, SettingsGet } from 'Common/Globals';
|
||||||
import { isArray } from 'Common/Utils';
|
import { WYSIWYGS } from 'Common/HtmlEditor';
|
||||||
import { addSubscribablesTo, addComputablesTo } from 'External/ko';
|
import { addSubscribablesTo, addComputablesTo } from 'External/ko';
|
||||||
import { i18n, translateTrigger, translatorReload, convertLangName } from 'Common/Translator';
|
import { i18n, translateTrigger, translatorReload, convertLangName } from 'Common/Translator';
|
||||||
|
import { editIdentity } from 'Common/UtilsUser';
|
||||||
|
|
||||||
import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
import { AbstractViewSettings } from 'Knoin/AbstractViews';
|
||||||
import { showScreenPopup } from 'Knoin/Knoin';
|
import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
|
|
||||||
import { AppUserStore } from 'Stores/User/App';
|
import { AppUserStore } from 'Stores/User/App';
|
||||||
import { LanguageStore } from 'Stores/Language';
|
import { LanguageStore } from 'Stores/Language';
|
||||||
|
import { FolderUserStore } from 'Stores/User/Folder';
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
import { IdentityUserStore } from 'Stores/User/Identity';
|
import { IdentityUserStore } from 'Stores/User/Identity';
|
||||||
import { NotificationUserStore } from 'Stores/User/Notification';
|
import { NotificationUserStore } from 'Stores/User/Notification';
|
||||||
|
|
@ -21,13 +23,14 @@ import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
||||||
|
|
||||||
import Remote from 'Remote/User/Fetch';
|
import Remote from 'Remote/User/Fetch';
|
||||||
|
|
||||||
import { IdentityPopupView } from 'View/Popup/Identity';
|
|
||||||
import { LanguagesPopupView } from 'View/Popup/Languages';
|
import { LanguagesPopupView } from 'View/Popup/Languages';
|
||||||
|
|
||||||
export class UserSettingsGeneral extends AbstractViewSettings {
|
export class UserSettingsGeneral extends AbstractViewSettings {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
this.mailto = ko.observable(!!navigator.registerProtocolHandler);
|
||||||
|
|
||||||
this.language = LanguageStore.language;
|
this.language = LanguageStore.language;
|
||||||
this.languages = LanguageStore.languages;
|
this.languages = LanguageStore.languages;
|
||||||
this.hourCycle = LanguageStore.hourCycle;
|
this.hourCycle = LanguageStore.hourCycle;
|
||||||
|
|
@ -36,17 +39,30 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
||||||
this.notificationSound = ko.observable(SettingsGet('NotificationSound'));
|
this.notificationSound = ko.observable(SettingsGet('NotificationSound'));
|
||||||
this.notificationSounds = ko.observableArray(SettingsGet('newMailSounds'));
|
this.notificationSounds = ko.observableArray(SettingsGet('newMailSounds'));
|
||||||
|
|
||||||
|
this.minRefreshInterval = SettingsGet('minRefreshInterval');
|
||||||
|
|
||||||
this.desktopNotifications = NotificationUserStore.enabled;
|
this.desktopNotifications = NotificationUserStore.enabled;
|
||||||
this.isDesktopNotificationAllowed = NotificationUserStore.allowed;
|
this.isDesktopNotificationAllowed = NotificationUserStore.allowed;
|
||||||
|
|
||||||
this.threadsAllowed = AppUserStore.threadsAllowed;
|
this.threadsAllowed = AppUserStore.threadsAllowed;
|
||||||
|
// 'THREAD=REFS', 'THREAD=REFERENCES', 'THREAD=ORDEREDSUBJECT'
|
||||||
|
this.threadAlgorithms = ko.observableArray();
|
||||||
|
FolderUserStore.capabilities.forEach(capa =>
|
||||||
|
capa.startsWith('THREAD=') && this.threadAlgorithms.push(capa.slice(7))
|
||||||
|
);
|
||||||
|
this.threadAlgorithms.sort((a, b) => a.length - b.length);
|
||||||
|
this.threadAlgorithm = SettingsUserStore.threadAlgorithm;
|
||||||
|
|
||||||
['layout', 'messageReadDelay', 'messagesPerPage', 'checkMailInterval',
|
['useThreads', 'threadAlgorithm',
|
||||||
'editorDefaultType', 'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt',
|
// These use addSetting()
|
||||||
|
'layout', 'messageReadDelay', 'messagesPerPage', 'checkMailInterval',
|
||||||
|
'editorDefaultType', 'editorWysiwyg', 'msgDefaultAction', 'maxBlockquotesLevel',
|
||||||
|
// These are in addSettings()
|
||||||
|
'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt',
|
||||||
'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles', 'allowDraftAutosave',
|
'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles', 'allowDraftAutosave',
|
||||||
'hideDeleted', 'listInlineAttachments', 'simpleAttachmentsList', 'collapseBlockquotes', 'maxBlockquotesLevel',
|
'hideDeleted', 'listInlineAttachments', 'simpleAttachmentsList', 'collapseBlockquotes',
|
||||||
'useCheckboxesInList', 'listGrouped', 'useThreads', 'replySameFolder', 'msgDefaultAction', 'allowSpellcheck',
|
'useCheckboxesInList', 'listGrouped', 'replySameFolder', 'allowSpellcheck',
|
||||||
'showNextMessage'
|
'messageReadAuto', 'showNextMessage', 'messageNewWindow'
|
||||||
].forEach(name => this[name] = SettingsUserStore[name]);
|
].forEach(name => this[name] = SettingsUserStore[name]);
|
||||||
|
|
||||||
this.allowLanguagesOnSettings = !!SettingsGet('allowLanguagesOnSettings');
|
this.allowLanguagesOnSettings = !!SettingsGet('allowLanguagesOnSettings');
|
||||||
|
|
@ -55,16 +71,13 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
||||||
|
|
||||||
this.identities = IdentityUserStore;
|
this.identities = IdentityUserStore;
|
||||||
|
|
||||||
|
this.wysiwygs = WYSIWYGS;
|
||||||
|
|
||||||
addComputablesTo(this, {
|
addComputablesTo(this, {
|
||||||
languageFullName: () => convertLangName(this.language()),
|
languageFullName: () => convertLangName(this.language()),
|
||||||
|
|
||||||
identityMain: () => {
|
|
||||||
const list = this.identities();
|
|
||||||
return isArray(list) ? list.find(item => item && !item.id()) : null;
|
|
||||||
},
|
|
||||||
|
|
||||||
identityMainDesc: () => {
|
identityMainDesc: () => {
|
||||||
const identity = this.identityMain();
|
const identity = IdentityUserStore.main();
|
||||||
return identity ? identity.formattedName() : '---';
|
return identity ? identity.formattedName() : '---';
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -76,6 +89,8 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
hasWysiwygs: () => 1 < WYSIWYGS().length,
|
||||||
|
|
||||||
msgDefaultActions: () => {
|
msgDefaultActions: () => {
|
||||||
translateTrigger();
|
translateTrigger();
|
||||||
return [
|
return [
|
||||||
|
|
@ -95,6 +110,7 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.addSetting('EditorDefaultType');
|
this.addSetting('EditorDefaultType');
|
||||||
|
this.addSetting('editorWysiwyg');
|
||||||
this.addSetting('MsgDefaultAction');
|
this.addSetting('MsgDefaultAction');
|
||||||
this.addSetting('MessageReadDelay');
|
this.addSetting('MessageReadDelay');
|
||||||
this.addSetting('MessagesPerPage');
|
this.addSetting('MessagesPerPage');
|
||||||
|
|
@ -102,10 +118,13 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
||||||
this.addSetting('Layout');
|
this.addSetting('Layout');
|
||||||
this.addSetting('MaxBlockquotesLevel');
|
this.addSetting('MaxBlockquotesLevel');
|
||||||
|
|
||||||
this.addSettings(['ViewHTML', 'ViewImages', 'ViewImagesWhitelist', 'HideDeleted', 'RemoveColors', 'AllowStyles',
|
this.addSettings([
|
||||||
'ListInlineAttachments', 'simpleAttachmentsList', 'UseCheckboxesInList', 'listGrouped', 'ReplySameFolder',
|
'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt',
|
||||||
'requestReadReceipt', 'requestDsn', 'requireTLS', 'pgpSign', 'pgpEncrypt', 'allowSpellcheck',
|
'ViewHTML', 'ViewImages', 'ViewImagesWhitelist', 'RemoveColors', 'AllowStyles', 'AllowDraftAutosave',
|
||||||
'DesktopNotifications', 'SoundNotification', 'CollapseBlockquotes', 'AllowDraftAutosave', 'showNextMessage']);
|
'HideDeleted', 'ListInlineAttachments', 'simpleAttachmentsList', 'CollapseBlockquotes',
|
||||||
|
'UseCheckboxesInList', 'listGrouped', 'ReplySameFolder', 'allowSpellcheck',
|
||||||
|
'messageReadAuto', 'showNextMessage', 'messageNewWindow',
|
||||||
|
'DesktopNotifications', 'SoundNotification']);
|
||||||
|
|
||||||
const fReloadLanguageHelper = (saveSettingsStep) => () => {
|
const fReloadLanguageHelper = (saveSettingsStep) => () => {
|
||||||
this.languageTrigger(saveSettingsStep);
|
this.languageTrigger(saveSettingsStep);
|
||||||
|
|
@ -133,6 +152,11 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
||||||
Remote.saveSetting('UseThreads', value);
|
Remote.saveSetting('UseThreads', value);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
threadAlgorithm: value => {
|
||||||
|
MessagelistUserStore([]);
|
||||||
|
Remote.saveSetting('threadAlgorithm', value);
|
||||||
|
},
|
||||||
|
|
||||||
checkMailInterval: () => {
|
checkMailInterval: () => {
|
||||||
setRefreshFoldersInterval(SettingsUserStore.checkMailInterval());
|
setRefreshFoldersInterval(SettingsUserStore.checkMailInterval());
|
||||||
}
|
}
|
||||||
|
|
@ -140,8 +164,7 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
editMainIdentity() {
|
editMainIdentity() {
|
||||||
const identity = this.identityMain();
|
editIdentity(IdentityUserStore.main());
|
||||||
identity && showScreenPopup(IdentityPopupView, [identity]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
testSoundNotification() {
|
testSoundNotification() {
|
||||||
|
|
@ -155,4 +178,15 @@ export class UserSettingsGeneral extends AbstractViewSettings {
|
||||||
selectLanguage() {
|
selectLanguage() {
|
||||||
showScreenPopup(LanguagesPopupView, [this.language, this.languages(), LanguageStore.userLanguage()]);
|
showScreenPopup(LanguagesPopupView, [this.language, this.languages(), LanguageStore.userLanguage()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerMailto() {
|
||||||
|
console.log(`mailto = ${location.protocol}//${location.host}${location.pathname}?mailto`);
|
||||||
|
navigator.registerProtocolHandler(
|
||||||
|
'mailto',
|
||||||
|
`${location.protocol}//${location.host}${location.pathname}?mailto&to=%s`,
|
||||||
|
(SettingsGet('title') || 'SnappyMail')
|
||||||
|
);
|
||||||
|
alert(i18n('GLOBAL/DONE'));
|
||||||
|
this.mailto(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ import { showScreenPopup } from 'Knoin/Knoin';
|
||||||
import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
|
import { OpenPgpImportPopupView } from 'View/Popup/OpenPgpImport';
|
||||||
import { OpenPgpGeneratePopupView } from 'View/Popup/OpenPgpGenerate';
|
import { OpenPgpGeneratePopupView } from 'View/Popup/OpenPgpGenerate';
|
||||||
|
|
||||||
//import Remote from 'Remote/User/Fetch';
|
import { SMimeUserStore } from 'Stores/User/SMime';
|
||||||
|
import { SMimeImportPopupView } from 'View/Popup/SMimeImport';
|
||||||
|
|
||||||
export class UserSettingsSecurity extends AbstractViewSettings {
|
export class UserSettingsSecurity extends AbstractViewSettings {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -25,9 +26,9 @@ export class UserSettingsSecurity extends AbstractViewSettings {
|
||||||
this.autoLogoutOptions = koComputable(() => {
|
this.autoLogoutOptions = koComputable(() => {
|
||||||
translateTrigger();
|
translateTrigger();
|
||||||
return [
|
return [
|
||||||
{ id: 0, name: i18n('SETTINGS_SECURITY/AUTOLOGIN_NEVER_OPTION_NAME') },
|
{ id: 0, name: i18n('SETTINGS_SECURITY/NEVER') },
|
||||||
{ id: 5, name: relativeTime(300) },
|
{ id: 5, name: relativeTime(300) },
|
||||||
{ id: 10, name: relativeTime(600) },
|
{ id: 15, name: relativeTime(900) },
|
||||||
{ id: 30, name: relativeTime(1800) },
|
{ id: 30, name: relativeTime(1800) },
|
||||||
{ id: 60, name: relativeTime(3600) },
|
{ id: 60, name: relativeTime(3600) },
|
||||||
{ id: 120, name: relativeTime(7200) },
|
{ id: 120, name: relativeTime(7200) },
|
||||||
|
|
@ -37,12 +38,17 @@ export class UserSettingsSecurity extends AbstractViewSettings {
|
||||||
});
|
});
|
||||||
this.addSetting('AutoLogout');
|
this.addSetting('AutoLogout');
|
||||||
|
|
||||||
|
this.keyPassForget = SettingsUserStore.keyPassForget;
|
||||||
|
this.addSetting('keyPassForget');
|
||||||
|
|
||||||
this.gnupgPublicKeys = GnuPGUserStore.publicKeys;
|
this.gnupgPublicKeys = GnuPGUserStore.publicKeys;
|
||||||
this.gnupgPrivateKeys = GnuPGUserStore.privateKeys;
|
this.gnupgPrivateKeys = GnuPGUserStore.privateKeys;
|
||||||
|
|
||||||
this.openpgpkeysPublic = OpenPGPUserStore.publicKeys;
|
this.openpgpkeysPublic = OpenPGPUserStore.publicKeys;
|
||||||
this.openpgpkeysPrivate = OpenPGPUserStore.privateKeys;
|
this.openpgpkeysPrivate = OpenPGPUserStore.privateKeys;
|
||||||
|
|
||||||
|
this.smimeCertificates = SMimeUserStore;
|
||||||
|
|
||||||
this.canOpenPGP = SettingsCapa('OpenPGP');
|
this.canOpenPGP = SettingsCapa('OpenPGP');
|
||||||
this.canGnuPG = GnuPGUserStore.isSupported();
|
this.canGnuPG = GnuPGUserStore.isSupported();
|
||||||
this.canMailvelope = !!window.mailvelope;
|
this.canMailvelope = !!window.mailvelope;
|
||||||
|
|
@ -56,6 +62,14 @@ export class UserSettingsSecurity extends AbstractViewSettings {
|
||||||
showScreenPopup(OpenPgpGeneratePopupView);
|
showScreenPopup(OpenPgpGeneratePopupView);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
importToOpenPGP() {
|
||||||
|
OpenPGPUserStore.loadBackupKeys();
|
||||||
|
}
|
||||||
|
|
||||||
|
importToSMime() {
|
||||||
|
showScreenPopup(SMimeImportPopupView);
|
||||||
|
}
|
||||||
|
|
||||||
onBuild() {
|
onBuild() {
|
||||||
/**
|
/**
|
||||||
* Create an iframe to display the Mailvelope keyring settings.
|
* Create an iframe to display the Mailvelope keyring settings.
|
||||||
|
|
|
||||||
|
|
@ -99,8 +99,8 @@ export class UserSettingsThemes /*extends AbstractViewSettings*/ {
|
||||||
themeBackground.hash(data?.Result?.hash || '');
|
themeBackground.hash(data?.Result?.hash || '');
|
||||||
if (!themeBackground.name() || !themeBackground.hash()) {
|
if (!themeBackground.name() || !themeBackground.hash()) {
|
||||||
let errorMsg = '';
|
let errorMsg = '';
|
||||||
if (data.ErrorCode) {
|
if (data.code) {
|
||||||
switch (data.ErrorCode) {
|
switch (data.code) {
|
||||||
case UploadErrorCode.FileIsTooBig:
|
case UploadErrorCode.FileIsTooBig:
|
||||||
errorMsg = i18n('SETTINGS_THEMES/ERROR_FILE_IS_TOO_BIG');
|
errorMsg = i18n('SETTINGS_THEMES/ERROR_FILE_IS_TOO_BIG');
|
||||||
break;
|
break;
|
||||||
|
|
@ -111,7 +111,7 @@ export class UserSettingsThemes /*extends AbstractViewSettings*/ {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
themeBackground.error(errorMsg || data.ErrorMessage || i18n('SETTINGS_THEMES/ERROR_UNKNOWN'));
|
themeBackground.error(errorMsg || data.message || i18n('SETTINGS_THEMES/ERROR_UNKNOWN'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,9 @@ import {
|
||||||
*/
|
*/
|
||||||
export class ConditionalCommand extends ControlCommand
|
export class ConditionalCommand extends ControlCommand
|
||||||
{
|
{
|
||||||
constructor()
|
constructor(identifier)
|
||||||
{
|
{
|
||||||
super();
|
super(identifier);
|
||||||
this.test = null;
|
this.test = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,8 @@ export class AddressTest extends TestCommand
|
||||||
this.header_list = new GrammarStringList;
|
this.header_list = new GrammarStringList;
|
||||||
this.key_list = new GrammarStringList;
|
this.key_list = new GrammarStringList;
|
||||||
// rfc5260#section-6
|
// rfc5260#section-6
|
||||||
// this.index = new GrammarNumber;
|
this.index = new GrammarNumber;
|
||||||
// this.last = false;
|
this.last = false;
|
||||||
// rfc5703#section-6
|
// rfc5703#section-6
|
||||||
// this.mime
|
// this.mime
|
||||||
// this.anychild
|
// this.anychild
|
||||||
|
|
@ -67,7 +67,7 @@ export class AddressTest extends TestCommand
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
// + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
|
+ (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
|
||||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||||
+ ' ' + this.address_part
|
+ ' ' + this.address_part
|
||||||
+ ' ' + this.match_type
|
+ ' ' + this.match_type
|
||||||
|
|
@ -234,8 +234,8 @@ export class HeaderTest extends TestCommand
|
||||||
this.header_names = new GrammarStringList;
|
this.header_names = new GrammarStringList;
|
||||||
this.key_list = new GrammarStringList;
|
this.key_list = new GrammarStringList;
|
||||||
// rfc5260#section-6
|
// rfc5260#section-6
|
||||||
// this.index = new GrammarNumber;
|
this.index = new GrammarNumber;
|
||||||
// this.last = false;
|
this.last = false;
|
||||||
// rfc5703#section-6
|
// rfc5703#section-6
|
||||||
this.mime = false;
|
this.mime = false;
|
||||||
this.anychild = false;
|
this.anychild = false;
|
||||||
|
|
@ -278,7 +278,7 @@ export class HeaderTest extends TestCommand
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
// + (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
|
+ (this.last ? ' :last' : (this.index.value ? ' :index ' + this.index : ''))
|
||||||
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
+ (this.comparator ? ' :comparator ' + this.comparator : '')
|
||||||
+ ' ' + this.match_type
|
+ ' ' + this.match_type
|
||||||
+ ' ' + this.header_names
|
+ ' ' + this.header_names
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@ class FlagCommand extends ActionCommand
|
||||||
|
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
return this.identifier + ' ' + this._variablename + ' ' + this.list_of_flags + ';';
|
let name = this._variablename;
|
||||||
|
return this.identifier + (name.length ? ' ' + this.variablename : '') + ' ' + this.list_of_flags + ';';
|
||||||
}
|
}
|
||||||
|
|
||||||
get variablename()
|
get variablename()
|
||||||
|
|
|
||||||
|
|
@ -8,22 +8,17 @@ import {
|
||||||
GrammarString
|
GrammarString
|
||||||
} from 'Sieve/Grammar';
|
} from 'Sieve/Grammar';
|
||||||
|
|
||||||
/**
|
class rfc5429Command extends ActionCommand
|
||||||
* https://tools.ietf.org/html/rfc5429#section-2.1
|
|
||||||
*/
|
|
||||||
export class ErejectCommand extends ActionCommand
|
|
||||||
{
|
{
|
||||||
constructor()
|
constructor(identifier)
|
||||||
{
|
{
|
||||||
super();
|
super(identifier);
|
||||||
this._reason = new GrammarQuotedString;
|
this._reason = new GrammarQuotedString;
|
||||||
}
|
}
|
||||||
|
|
||||||
get require() { return 'ereject'; }
|
|
||||||
|
|
||||||
toString()
|
toString()
|
||||||
{
|
{
|
||||||
return 'ereject ' + this._reason + ';';
|
return this.require + ' ' + this._reason + ';';
|
||||||
}
|
}
|
||||||
|
|
||||||
get reason()
|
get reason()
|
||||||
|
|
@ -44,38 +39,20 @@ export class ErejectCommand extends ActionCommand
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://tools.ietf.org/html/rfc5429#section-2.1
|
||||||
|
*/
|
||||||
|
export class ErejectCommand extends rfc5429Command
|
||||||
|
{
|
||||||
|
constructor() { super('ereject'); }
|
||||||
|
get require() { return 'ereject'; }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://tools.ietf.org/html/rfc5429#section-2.2
|
* https://tools.ietf.org/html/rfc5429#section-2.2
|
||||||
*/
|
*/
|
||||||
export class RejectCommand extends ActionCommand
|
export class RejectCommand extends rfc5429Command
|
||||||
{
|
{
|
||||||
constructor()
|
constructor() { super('reject'); }
|
||||||
{
|
|
||||||
super();
|
|
||||||
this._reason = new GrammarQuotedString;
|
|
||||||
}
|
|
||||||
|
|
||||||
get require() { return 'reject'; }
|
get require() { return 'reject'; }
|
||||||
|
|
||||||
toString()
|
|
||||||
{
|
|
||||||
return 'reject ' + this._reason + ';';
|
|
||||||
}
|
|
||||||
|
|
||||||
get reason()
|
|
||||||
{
|
|
||||||
return this._reason.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
set reason(value)
|
|
||||||
{
|
|
||||||
this._reason.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
pushArguments(args)
|
|
||||||
{
|
|
||||||
if (args[0] instanceof GrammarString) {
|
|
||||||
this._reason = args[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -196,7 +196,7 @@ export class GrammarTestList extends Array
|
||||||
// return '(\r\n\t' + arrayToString(this, ',\r\n\t') + '\r\n)';
|
// return '(\r\n\t' + arrayToString(this, ',\r\n\t') + '\r\n)';
|
||||||
return '(' + this.join(', ') + ')';
|
return '(' + this.join(', ') + ')';
|
||||||
}
|
}
|
||||||
return this.length ? this[0] : '';
|
return this.length ? this[0].toString() : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
push(value)
|
push(value)
|
||||||
|
|
@ -254,7 +254,7 @@ export class GrammarStringList extends Array
|
||||||
if (1 < this.length) {
|
if (1 < this.length) {
|
||||||
return '[' + this.join(',') + ']';
|
return '[' + this.join(',') + ']';
|
||||||
}
|
}
|
||||||
return this.length ? this[0] : '';
|
return this.length ? this[0].toString() : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
push(value)
|
push(value)
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,10 @@ export class AbstractModel {
|
||||||
constructor() {
|
constructor() {
|
||||||
/*
|
/*
|
||||||
if (new.target === AbstractModel) {
|
if (new.target === AbstractModel) {
|
||||||
throw new Error("Can't instantiate AbstractModel!");
|
throw Error("Can't instantiate AbstractModel!");
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
this.disposables = [];
|
Object.defineProperty(this, 'disposables', {value: []});
|
||||||
}
|
}
|
||||||
|
|
||||||
addObservables(observables) {
|
addObservables(observables) {
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ export class FilterModel extends AbstractModel {
|
||||||
this.addObservables({
|
this.addObservables({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
askDelete: false,
|
askDelete: false,
|
||||||
canBeDeleted: true,
|
|
||||||
|
|
||||||
name: '',
|
name: '',
|
||||||
nameError: false,
|
nameError: false,
|
||||||
|
|
@ -181,27 +180,6 @@ export class FilterModel extends AbstractModel {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
|
||||||
return {
|
|
||||||
// '@Object': 'Object/Filter',
|
|
||||||
ID: this.id,
|
|
||||||
Enabled: this.enabled() ? 1 : 0,
|
|
||||||
Name: this.name,
|
|
||||||
Conditions: this.conditions,
|
|
||||||
ConditionsType: this.conditionsType,
|
|
||||||
|
|
||||||
ActionType: this.actionType(),
|
|
||||||
ActionValue: this.actionValue,
|
|
||||||
ActionValueSecond: this.actionValueSecond,
|
|
||||||
ActionValueThird: this.actionValueThird,
|
|
||||||
ActionValueFourth: this.actionValueFourth,
|
|
||||||
|
|
||||||
Keep: this.keep() ? 1 : 0,
|
|
||||||
Stop: this.stop() ? 1 : 0,
|
|
||||||
MarkAsRead: this.markAsRead() ? 1 : 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
addCondition() {
|
addCondition() {
|
||||||
this.conditions.push(new FilterConditionModel());
|
this.conditions.push(new FilterConditionModel());
|
||||||
}
|
}
|
||||||
|
|
@ -210,6 +188,24 @@ export class FilterModel extends AbstractModel {
|
||||||
this.conditions.remove(oConditionToDelete);
|
this.conditions.remove(oConditionToDelete);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
ID: this.id,
|
||||||
|
Enabled: this.enabled(),
|
||||||
|
Name: this.name(),
|
||||||
|
Conditions: this.conditions(),
|
||||||
|
ConditionsType: this.conditionsType(),
|
||||||
|
ActionType: this.actionType(),
|
||||||
|
ActionValue: this.actionValue(),
|
||||||
|
ActionValueSecond: this.actionValueSecond(),
|
||||||
|
ActionValueThird: this.actionValueThird(),
|
||||||
|
ActionValueFourth: this.actionValueFourth(),
|
||||||
|
Keep: this.keep(),
|
||||||
|
Stop: this.stop(),
|
||||||
|
MarkAsRead: this.markAsRead()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @static
|
* @static
|
||||||
* @param {FetchJsonFilter} json
|
* @param {FetchJsonFilter} json
|
||||||
|
|
@ -222,7 +218,10 @@ export class FilterModel extends AbstractModel {
|
||||||
if (filter) {
|
if (filter) {
|
||||||
filter.id = '' + (filter.id || '');
|
filter.id = '' + (filter.id || '');
|
||||||
filter.conditions(
|
filter.conditions(
|
||||||
(json.Conditions || []).map(aData => FilterConditionModel.reviveFromJson(aData)).filter(v => v)
|
(json.Conditions || json.conditions || []).map(condition => {
|
||||||
|
condition['@Object'] = 'Object/FilterCondition';
|
||||||
|
return FilterConditionModel.reviveFromJson(condition)
|
||||||
|
}).filter(v => v)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return filter;
|
return filter;
|
||||||
|
|
|
||||||
|
|
@ -79,18 +79,17 @@ export class FilterConditionModel extends AbstractModel {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// static reviveFromJson(json) {}
|
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
// '@Object': 'Object/FilterCondition',
|
Field: this.field(),
|
||||||
Field: this.field,
|
Type: this.type(),
|
||||||
Type: this.type,
|
Value: this.value(),
|
||||||
Value: this.value,
|
ValueSecond: this.valueSecond()
|
||||||
ValueSecond: this.valueSecond
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// static reviveFromJson(json) {}
|
||||||
|
|
||||||
cloneSelf() {
|
cloneSelf() {
|
||||||
const filterCond = new FilterConditionModel();
|
const filterCond = new FilterConditionModel();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ function filtersToSieveScript(filters)
|
||||||
''
|
''
|
||||||
];
|
];
|
||||||
|
|
||||||
const quote = string => '"' + string.trim().replace(/(\\|")/g, '\\$1') + '"';
|
const quote = string => '"' + string.replace(/(\\|")/g, '\\$1') + '"';
|
||||||
const StripSpaces = string => string.replace(/\s+/, ' ').trim();
|
const StripSpaces = string => string.replace(/\s+/, ' ');
|
||||||
|
|
||||||
// conditionToSieveScript
|
// conditionToSieveScript
|
||||||
const conditionToString = (condition, require) =>
|
const conditionToString = (condition, require) =>
|
||||||
|
|
@ -26,8 +26,8 @@ function filtersToSieveScript(filters)
|
||||||
let result = '',
|
let result = '',
|
||||||
type = condition.type(),
|
type = condition.type(),
|
||||||
field = condition.field(),
|
field = condition.field(),
|
||||||
value = condition.value().trim(),
|
value = condition.value(),
|
||||||
valueSecond = condition.valueSecond().trim();
|
valueSecond = condition.valueSecond();
|
||||||
|
|
||||||
if (value.length && ('Header' !== field || valueSecond.length)) {
|
if (value.length && ('Header' !== field || valueSecond.length)) {
|
||||||
switch (type)
|
switch (type)
|
||||||
|
|
@ -85,7 +85,7 @@ function filtersToSieveScript(filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (('From' === field || 'Recipient' === field) && value.includes(',')) {
|
if (('From' === field || 'Recipient' === field) && value.includes(',')) {
|
||||||
result += ' [' + value.split(',').map(value => quote(value)).join(', ').trim() + ']';
|
result += ' [' + value.split(',').map(value => quote(value)).join(', ') + ']';
|
||||||
} else if ('Size' === field) {
|
} else if ('Size' === field) {
|
||||||
result += ' ' + value;
|
result += ' ' + value;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -130,7 +130,7 @@ function filtersToSieveScript(filters)
|
||||||
result.push(sTab + 'addflag "\\\\Seen";');
|
result.push(sTab + 'addflag "\\\\Seen";');
|
||||||
}
|
}
|
||||||
|
|
||||||
let value = filter.actionValue().trim();
|
let value = filter.actionValue();
|
||||||
value = value.length ? quote(value) : 0;
|
value = value.length ? quote(value) : 0;
|
||||||
switch (filter.actionType())
|
switch (filter.actionType())
|
||||||
{
|
{
|
||||||
|
|
@ -146,21 +146,21 @@ function filtersToSieveScript(filters)
|
||||||
let days = 1,
|
let days = 1,
|
||||||
subject = '',
|
subject = '',
|
||||||
addresses = '',
|
addresses = '',
|
||||||
paramValue = filter.actionValueSecond().trim();
|
paramValue = filter.actionValueSecond();
|
||||||
|
|
||||||
if (paramValue.length) {
|
if (paramValue.length) {
|
||||||
subject = ':subject ' + quote(StripSpaces(paramValue)) + ' ';
|
subject = ':subject ' + quote(StripSpaces(paramValue)) + ' ';
|
||||||
}
|
}
|
||||||
|
|
||||||
paramValue = ('' + (filter.actionValueThird() || '')).trim();
|
paramValue = ('' + (filter.actionValueThird() || ''));
|
||||||
if (paramValue.length) {
|
if (paramValue.length) {
|
||||||
days = Math.max(1, parseInt(paramValue, 10));
|
days = Math.max(1, parseInt(paramValue, 10));
|
||||||
}
|
}
|
||||||
|
|
||||||
paramValue = ('' + (filter.actionValueFourth() || '')).trim()
|
paramValue = ('' + (filter.actionValueFourth() || ''))
|
||||||
if (paramValue.length) {
|
if (paramValue.length) {
|
||||||
paramValue = paramValue.split(',').map(email =>
|
paramValue = paramValue.split(',').map(email =>
|
||||||
email.trim().length ? quote(email) : ''
|
email.length ? quote(email) : ''
|
||||||
).filter(email => email.length);
|
).filter(email => email.length);
|
||||||
if (paramValue.length) {
|
if (paramValue.length) {
|
||||||
addresses = ':addresses [' + paramValue.join(', ') + '] ';
|
addresses = ':addresses [' + paramValue.join(', ') + '] ';
|
||||||
|
|
@ -228,7 +228,7 @@ function filtersToSieveScript(filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fileStringToCollection
|
// fileStringToCollection
|
||||||
function sieveScriptToFilters(script)
|
function rainloopScriptToFilters(script)
|
||||||
{
|
{
|
||||||
let regex = /BEGIN:HEADER([\s\S]+?)END:HEADER/gm,
|
let regex = /BEGIN:HEADER([\s\S]+?)END:HEADER/gm,
|
||||||
filters = [],
|
filters = [],
|
||||||
|
|
@ -239,7 +239,6 @@ function sieveScriptToFilters(script)
|
||||||
json = decodeURIComponent(escape(atob(json[1].replace(/\s+/g, ''))));
|
json = decodeURIComponent(escape(atob(json[1].replace(/\s+/g, ''))));
|
||||||
if (json && json.length && (json = JSON.parse(json))) {
|
if (json && json.length && (json = JSON.parse(json))) {
|
||||||
json['@Object'] = 'Object/Filter';
|
json['@Object'] = 'Object/Filter';
|
||||||
json.Conditions.forEach(condition => condition['@Object'] = 'Object/FilterCondition');
|
|
||||||
filter = FilterModel.reviveFromJson(json);
|
filter = FilterModel.reviveFromJson(json);
|
||||||
filter && filters.push(filter);
|
filter && filters.push(filter);
|
||||||
}
|
}
|
||||||
|
|
@ -261,7 +260,6 @@ export class SieveScriptModel extends AbstractModel
|
||||||
exists: false,
|
exists: false,
|
||||||
nameError: false,
|
nameError: false,
|
||||||
askDelete: false,
|
askDelete: false,
|
||||||
canBeDeleted: true,
|
|
||||||
hasChanges: false
|
hasChanges: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -280,13 +278,8 @@ export class SieveScriptModel extends AbstractModel
|
||||||
// this.body(filtersToSieveScript(this.filters));
|
// this.body(filtersToSieveScript(this.filters));
|
||||||
}
|
}
|
||||||
|
|
||||||
rawToFilters() {
|
|
||||||
return sieveScriptToFilters(this.body());
|
|
||||||
// this.filters(sieveScriptToFilters(this.body()));
|
|
||||||
}
|
|
||||||
|
|
||||||
verify() {
|
verify() {
|
||||||
this.nameError(!this.name().trim());
|
this.nameError(!this.name());
|
||||||
return !this.nameError();
|
return !this.nameError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -315,9 +308,8 @@ export class SieveScriptModel extends AbstractModel
|
||||||
const script = super.reviveFromJson(json);
|
const script = super.reviveFromJson(json);
|
||||||
if (script) {
|
if (script) {
|
||||||
if (script.allowFilters()) {
|
if (script.allowFilters()) {
|
||||||
script.filters(sieveScriptToFilters(script.body()));
|
script.filters(rainloopScriptToFilters(script.body()));
|
||||||
}
|
}
|
||||||
script.canBeDeleted(SIEVE_FILE_NAME !== json.name);
|
|
||||||
script.exists(true);
|
script.exists(true);
|
||||||
script.hasChanges(false);
|
script.hasChanges(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,6 @@ const
|
||||||
sDeepPrefix = '\u00A0\u00A0\u00A0',
|
sDeepPrefix = '\u00A0\u00A0\u00A0',
|
||||||
showUnsubscribed = true/*!SettingsUserStore.hideUnsubscribed()*/,
|
showUnsubscribed = true/*!SettingsUserStore.hideUnsubscribed()*/,
|
||||||
|
|
||||||
disabled = rl.settings.get('sieveAllowFileintoInbox') ? '' : 'INBOX',
|
|
||||||
|
|
||||||
foldersWalk = folders => {
|
foldersWalk = folders => {
|
||||||
folders.forEach(oItem => {
|
folders.forEach(oItem => {
|
||||||
if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
|
if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
|
||||||
|
|
@ -39,7 +37,7 @@ const
|
||||||
id: oItem.fullName,
|
id: oItem.fullName,
|
||||||
name: sDeepPrefix.repeat(oItem.deep) + oItem.detailedName(),
|
name: sDeepPrefix.repeat(oItem.deep) + oItem.detailedName(),
|
||||||
system: false,
|
system: false,
|
||||||
disabled: !oItem.selectable() || disabled == oItem.fullName
|
disabled: !oItem.selectable()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,12 +118,14 @@ export class FilterPopupView extends rl.pluginPopupView {
|
||||||
id: FilterAction.MoveTo,
|
id: FilterAction.MoveTo,
|
||||||
name: i18nFilter('ACTION_MOVE_TO')
|
name: i18nFilter('ACTION_MOVE_TO')
|
||||||
});
|
});
|
||||||
this.actionTypeOptions.push({
|
|
||||||
id: FilterAction.Forward,
|
|
||||||
name: i18nFilter('ACTION_FORWARD_TO')
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// redirect command
|
||||||
|
this.actionTypeOptions.push({
|
||||||
|
id: FilterAction.Forward,
|
||||||
|
name: i18nFilter('ACTION_FORWARD_TO')
|
||||||
|
});
|
||||||
|
|
||||||
if (capa.includes('reject')) {
|
if (capa.includes('reject')) {
|
||||||
this.actionTypeOptions.push({ id: FilterAction.Reject, name: i18nFilter('ACTION_REJECT') });
|
this.actionTypeOptions.push({ id: FilterAction.Reject, name: i18nFilter('ACTION_REJECT') });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ export class SieveScriptPopupView extends rl.pluginPopupView {
|
||||||
|
|
||||||
if (iError) {
|
if (iError) {
|
||||||
self.saveError(true);
|
self.saveError(true);
|
||||||
self.errorText(data?.ErrorMessageAdditional || getNotification(iError));
|
self.errorText(data?.messageAdditional || getNotification(iError));
|
||||||
} else {
|
} else {
|
||||||
script.exists() || scripts.push(script);
|
script.exists() || scripts.push(script);
|
||||||
script.exists(true);
|
script.exists(true);
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ try {
|
||||||
data = data ? decodeURIComponent(data[2]) : null;
|
data = data ? decodeURIComponent(data[2]) : null;
|
||||||
data = data ? JSON.parse(data) : {};
|
data = data ? JSON.parse(data) : {};
|
||||||
win[sName] = {
|
win[sName] = {
|
||||||
getItem: key => data[key] == null ? null : data[key],
|
getItem: key => data[key] ?? null,
|
||||||
setItem: (key, value) => {
|
setItem: (key, value) => {
|
||||||
data[key] = ''+value; // forces the value to a string
|
data[key] = ''+value; // forces the value to a string
|
||||||
document.cookie = sName+'='+encodeURIComponent(JSON.stringify(data))
|
document.cookie = sName+'='+encodeURIComponent(JSON.stringify(data))
|
||||||
|
|
|
||||||
|
|
@ -1 +1,21 @@
|
||||||
export const Passphrases = new Map();
|
import { AskPopupView } from 'View/Popup/Ask';
|
||||||
|
import { SettingsUserStore } from 'Stores/User/Settings';
|
||||||
|
|
||||||
|
export const Passphrases = new WeakMap();
|
||||||
|
|
||||||
|
Passphrases.ask = async (key, sAskDesc, btnText) =>
|
||||||
|
Passphrases.has(key)
|
||||||
|
? {password:Passphrases.handle(key)/*, remember:false*/}
|
||||||
|
: await AskPopupView.password(sAskDesc, btnText, 5);
|
||||||
|
|
||||||
|
const timeouts = {};
|
||||||
|
// get/set accessor to control deletion after N minutes of inactivity
|
||||||
|
Passphrases.handle = (key, pass) => {
|
||||||
|
const timeout = SettingsUserStore.keyPassForget();
|
||||||
|
if (timeout && !timeouts[key]) {
|
||||||
|
timeouts[key] = (()=>Passphrases.delete(key)).debounce(timeout * 1000);
|
||||||
|
}
|
||||||
|
pass && Passphrases.set(key, pass);
|
||||||
|
timeout && timeouts[key]();
|
||||||
|
return Passphrases.get(key);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ DomainAdminStore.fetch = () => {
|
||||||
if (!iError) {
|
if (!iError) {
|
||||||
DomainAdminStore(
|
DomainAdminStore(
|
||||||
data.Result.map(item => {
|
data.Result.map(item => {
|
||||||
|
item.name = IDN.toUnicode(item.name);
|
||||||
item.disabled = ko.observable(item.disabled);
|
item.disabled = ko.observable(item.disabled);
|
||||||
item.askDelete = ko.observable(false);
|
item.askDelete = ko.observable(false);
|
||||||
return item;
|
return item;
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export const LanguageStore = {
|
||||||
const aLanguages = Settings.app('languages');
|
const aLanguages = Settings.app('languages');
|
||||||
this.languages(isArray(aLanguages) ? aLanguages : []);
|
this.languages(isArray(aLanguages) ? aLanguages : []);
|
||||||
this.language(SettingsGet('language'));
|
this.language(SettingsGet('language'));
|
||||||
this.userLanguage(SettingsGet('userLanguage'));
|
this.userLanguage(SettingsGet('clientLanguage'));
|
||||||
this.hourCycle(SettingsGet('hourCycle'));
|
this.hourCycle(SettingsGet('hourCycle'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,7 @@ import { addObservablesTo, koArrayWithDestroy } from 'External/ko';
|
||||||
|
|
||||||
export const AccountUserStore = koArrayWithDestroy();
|
export const AccountUserStore = koArrayWithDestroy();
|
||||||
|
|
||||||
AccountUserStore.loading = ko.observable(false).extend({ debounce: 100 });
|
|
||||||
|
|
||||||
AccountUserStore.getEmailAddresses = () => AccountUserStore.map(item => item.email);
|
|
||||||
|
|
||||||
addObservablesTo(AccountUserStore, {
|
addObservablesTo(AccountUserStore, {
|
||||||
email: '',
|
email: '',
|
||||||
signature: ''
|
loading: false
|
||||||
});
|
});
|
||||||
|
|
|
||||||
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