Compare commits
No commits in common. "master" and "v2.1.1" have entirely different histories.
4
.cmds
|
|
@ -15,5 +15,5 @@ webpack --color --watch
|
||||||
|
|
||||||
#
|
#
|
||||||
gpg --import x
|
gpg --import x
|
||||||
gpg --detach-sign --armor --openpgp -u 87DA4591 --passphrase ?? x
|
gpg --detach-sign --armor --openpgp -u 87DA4591 x
|
||||||
for ff in `ls ./build/dist/releases/**/**/*.zip | xargs`; do gpg --detach-sign --no-tty --yes --armor --openpgp -u 87DA4591 --passphrase ?? $ff; done
|
for ff in `ls *.zip`; do gpg --detach-sign --armor --openpgp -u 87DA4591 $ff; done
|
||||||
|
|
|
||||||
0
.docker/dev/nginx/ssl.sh
Executable file → Normal file
|
|
@ -15,7 +15,7 @@ RUN pecl install xxtea-1.0.11 && \
|
||||||
|
|
||||||
RUN docker-php-ext-configure intl && \
|
RUN docker-php-ext-configure intl && \
|
||||||
docker-php-ext-configure ldap && \
|
docker-php-ext-configure ldap && \
|
||||||
docker-php-ext-configure gd --with-freetype=/usr/include/ --with-jpeg=/usr/include/ && \
|
docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ && \
|
||||||
docker-php-ext-install opcache pdo_mysql zip intl gd ldap
|
docker-php-ext-install opcache pdo_mysql zip intl gd ldap
|
||||||
|
|
||||||
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
||||||
|
|
|
||||||
|
|
@ -1,192 +1,48 @@
|
||||||
# syntax=docker/dockerfile:1
|
|
||||||
|
|
||||||
FROM alpine:3.24.3 AS builder
|
|
||||||
RUN apk add --no-cache php85 php85-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.5-fpm-alpine AS final
|
FROM php:7.4-fpm-buster
|
||||||
|
|
||||||
LABEL org.label-schema.description="SnappyMail webmail client image using nginx, php-fpm on Alpine"
|
ARG FILES_ZIP
|
||||||
|
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 apk add --no-cache ca-certificates nginx supervisor bash
|
RUN mkdir -p /usr/share/man/man1/ /usr/share/man/man3/ /usr/share/man/man7/ && \
|
||||||
|
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
|
||||||
# apcu
|
RUN php -m && \
|
||||||
RUN set -eux; \
|
docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu/ && \
|
||||||
apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
|
docker-php-ext-configure intl && \
|
||||||
pecl install apcu; \
|
docker-php-ext-configure gd --with-freetype-dir=/usr/include --with-jpeg-dir=/usr/include/ && \
|
||||||
docker-php-ext-enable apcu; \
|
docker-php-ext-install ldap opcache pdo_mysql pdo_pgsql zip intl gd && \
|
||||||
docker-php-source delete; \
|
php -m
|
||||||
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
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# sqlite
|
|
||||||
RUN set -eux; \
|
|
||||||
apk add --no-cache sqlite-libs; \
|
|
||||||
apk add --no-cache --virtual .deps sqlite-dev; \
|
|
||||||
docker-php-ext-install pdo_sqlite; \
|
|
||||||
apk del .deps
|
|
||||||
|
|
||||||
# tidy
|
|
||||||
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
|
||||||
# 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
|
WORKDIR /tmp
|
||||||
COPY --chown=www-data:www-data --from=builder /snappymail /snappymail
|
COPY ${FILES_ZIP} .
|
||||||
# Use a custom snappymail data folder
|
RUN mkdir /snappymail && \
|
||||||
RUN mv -v /snappymail/data /var/lib/snappymail;
|
unzip -q ${FILES_ZIP} -d /snappymail && \
|
||||||
# Setup configs
|
find /snappymail -type d -exec chmod 755 {} \; && \
|
||||||
COPY --chown=root:root .docker/release/files /
|
find /snappymail -type f -exec chmod 644 {} \; && \
|
||||||
RUN set -eux; \
|
rm -rf ${FILES_ZIP}
|
||||||
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;
|
|
||||||
|
|
||||||
USER root
|
# Install other content
|
||||||
WORKDIR /snappymail
|
COPY files /
|
||||||
VOLUME /var/lib/snappymail
|
RUN chmod +x /entrypoint.sh && chmod +x /logrotate-loop.sh
|
||||||
|
VOLUME /snappymail/data
|
||||||
EXPOSE 8888
|
EXPOSE 8888
|
||||||
EXPOSE 9000
|
|
||||||
ENTRYPOINT []
|
|
||||||
CMD ["/entrypoint.sh"]
|
CMD ["/entrypoint.sh"]
|
||||||
|
|
|
||||||
96
.docker/release/files/entrypoint.sh
Executable file → Normal file
|
|
@ -1,26 +1,23 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -eu
|
|
||||||
|
|
||||||
DEBUG=${DEBUG:-}
|
# Create not root user
|
||||||
if [ "$DEBUG" = 'true' ]; then
|
groupadd --gid "$GID" php-cli -f
|
||||||
set -x
|
adduser --uid "$UID" --disabled-password --gid "$GID" --shell /bin/bash --home /home/php-cli php-cli --force --gecos ""
|
||||||
fi
|
|
||||||
UPLOAD_MAX_SIZE=${UPLOAD_MAX_SIZE:-25M}
|
|
||||||
MEMORY_LIMIT=${MEMORY_LIMIT:-128M}
|
|
||||||
SECURE_COOKIES=${SECURE_COOKIES:-true}
|
|
||||||
|
|
||||||
# Disable listening on ipv6 for ipv4-only environments, or nginx will fail to start
|
|
||||||
if ! ip a | grep 'inet6 ' > /dev/null; then
|
|
||||||
echo "[INFO] ipv6 is disabled. Configuring nginx to not listen on ipv6"
|
|
||||||
sed -i '/listen \[::\]:/d' /etc/nginx/nginx.conf
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Set attachment size limit
|
# Set attachment size limit
|
||||||
sed -i "s/<UPLOAD_MAX_SIZE>/$UPLOAD_MAX_SIZE/g" /usr/local/etc/php-fpm.d/php-fpm.conf /etc/nginx/nginx.conf
|
sed -i "s/<UPLOAD_MAX_SIZE>/$UPLOAD_MAX_SIZE/g" /usr/local/etc/php-fpm.d/php-fpm.conf /etc/nginx/nginx.conf
|
||||||
sed -i "s/<MEMORY_LIMIT>/$MEMORY_LIMIT/g" /usr/local/etc/php-fpm.d/php-fpm.conf
|
sed -i "s/<MEMORY_LIMIT>/$MEMORY_LIMIT/g" /usr/local/etc/php-fpm.d/php-fpm.conf
|
||||||
|
|
||||||
|
# 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';
|
||||||
|
|
@ -29,58 +26,43 @@ if [ "${SECURE_COOKIES}" = 'true' ]; then
|
||||||
} > /usr/local/etc/php/conf.d/cookies.ini;
|
} > /usr/local/etc/php/conf.d/cookies.ini;
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "[INFO] Snappymail version: $( ls /snappymail/snappymail/v )"
|
# Copy snappymail default config if absent
|
||||||
|
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: $SNAPPYMAIL_CONFIG_FILE"
|
echo "[INFO] Creating default Snappymail configuration"
|
||||||
# Run snappymail and exit. This populates the snappymail data directory and generates the config file
|
mkdir -p $(dirname $SNAPPYMAIL_CONFIG_FILE)
|
||||||
# On error, print php exception and exit
|
cp /usr/local/include/application.ini $SNAPPYMAIL_CONFIG_FILE
|
||||||
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
|
||||||
sed '/^\; Enable logging/{
|
if [ "${LOG_TO_STDERR}" = true ]; then
|
||||||
N
|
sed -z 's/\; Enable logging\nenable = Off/\; Enable logging\nenable = On/' -i $SNAPPYMAIL_CONFIG_FILE
|
||||||
s/enable = Off/enable = On/
|
sed 's/^filename = .*/filename = "errors.log"/' -i $SNAPPYMAIL_CONFIG_FILE
|
||||||
}' -i $SNAPPYMAIL_CONFIG_FILE
|
sed 's/^write_on_error_only = .*/write_on_error_only = Off/' -i $SNAPPYMAIL_CONFIG_FILE
|
||||||
# Redirect snappymail logs to stderr /stdout
|
sed 's/^write_on_php_error_only = .*/write_on_php_error_only = On/' -i $SNAPPYMAIL_CONFIG_FILE
|
||||||
sed 's/^filename = .*/filename = "stderr"/' -i $SNAPPYMAIL_CONFIG_FILE
|
else
|
||||||
sed 's/^write_on_error_only = .*/write_on_error_only = Off/' -i $SNAPPYMAIL_CONFIG_FILE
|
sed -z 's/\; Enable logging\nenable = On/\; Enable logging\nenable = Off/' -i $SNAPPYMAIL_CONFIG_FILE
|
||||||
sed 's/^write_on_php_error_only = .*/write_on_php_error_only = On/' -i $SNAPPYMAIL_CONFIG_FILE
|
fi
|
||||||
# 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
|
||||||
sed 's/^auth_syslog = .*/auth_syslog = Off/' -i $SNAPPYMAIL_CONFIG_FILE
|
# Redirect snappymail logs to stderr /stdout
|
||||||
|
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
|
||||||
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
|
chown -R $UID:$GID /snappymail/data /var/log /var/lib/nginx
|
||||||
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/stdout
|
||||||
# Create snappymail admin password if absent
|
chmod o+w /dev/stderr
|
||||||
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
|
|
||||||
echo "[INFO] Snappymail ready at http://localhost:8888/"
|
# Touch supervisord PID file in order to fix permissions
|
||||||
) &
|
touch /run/supervisord.pid
|
||||||
|
chown php-cli:php-cli /run/supervisord.pid
|
||||||
|
|
||||||
# RUN !
|
# RUN !
|
||||||
exec /usr/bin/supervisord -c /supervisor.conf --pidfile /run/supervisord.pid
|
exec sudo -u php-cli -g php-cli /usr/bin/supervisord -c '/supervisor.conf' --pidfile '/run/supervisord.pid'
|
||||||
|
|
|
||||||
5
.docker/release/files/etc/logrotate.d/snappymail
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
/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 /dev/stderr error;
|
error_log /tmp/ngx_error.log error;
|
||||||
|
|
||||||
sendfile on;
|
sendfile on;
|
||||||
keepalive_timeout 15;
|
keepalive_timeout 15;
|
||||||
|
|
@ -52,8 +52,6 @@ http {
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 8888;
|
listen 8888;
|
||||||
listen [::]:8888;
|
|
||||||
#listen [::]:8888 ipv6only=off;
|
|
||||||
root /snappymail;
|
root /snappymail;
|
||||||
index index.php;
|
index index.php;
|
||||||
charset utf-8;
|
charset utf-8;
|
||||||
|
|
@ -97,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 127.0.0.1:9000;
|
fastcgi_pass unix:/tmp/php-fpm.sock;
|
||||||
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;
|
||||||
|
|
|
||||||
113
.docker/release/files/logrotate-loop.sh
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
#!/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}
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
<?php
|
|
||||||
define('APP_DATA_FOLDER_PATH', '/var/lib/snappymail/');
|
|
||||||
?>
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
[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=root
|
user=php-cli
|
||||||
numprocs=1
|
numprocs=1
|
||||||
autostart=true
|
autostart=true
|
||||||
autorestart=false
|
autorestart=false
|
||||||
|
|
@ -20,7 +17,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=root
|
user=php-cli
|
||||||
numprocs=1
|
numprocs=1
|
||||||
autostart=true
|
autostart=true
|
||||||
autorestart=false
|
autorestart=false
|
||||||
|
|
@ -30,11 +27,34 @@ 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=root
|
user=php-cli
|
||||||
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,21 +1,15 @@
|
||||||
[global]
|
[global]
|
||||||
daemonize = no
|
daemonize = no
|
||||||
error_log = /dev/stderr
|
|
||||||
log_buffering = no
|
|
||||||
|
|
||||||
[default]
|
[default]
|
||||||
listen = 9000
|
listen = /tmp/php-fpm.sock
|
||||||
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 = /
|
||||||
pm.status_path = /status
|
php_admin_value[error_log] = /tmp/php_error.log
|
||||||
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
|
||||||
|
|
|
||||||
359
.docker/release/files/usr/local/include/application.ini
Normal file
|
|
@ -0,0 +1,359 @@
|
||||||
|
; SnappyMail Webmail 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 = ""
|
||||||
|
|
||||||
|
; 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
|
||||||
|
|
||||||
|
; File size limit (MB) for file upload on compose screen
|
||||||
|
; 0 for unlimited.
|
||||||
|
attachment_size_limit = 25
|
||||||
|
|
||||||
|
[interface]
|
||||||
|
show_attachment_thumbnail = On
|
||||||
|
use_native_scrollbars = Off
|
||||||
|
new_move_to_folder_button = On
|
||||||
|
|
||||||
|
[branding]
|
||||||
|
login_logo = ""
|
||||||
|
login_background = ""
|
||||||
|
login_desc = ""
|
||||||
|
login_css = ""
|
||||||
|
user_css = ""
|
||||||
|
user_logo = ""
|
||||||
|
user_logo_title = ""
|
||||||
|
user_logo_message = ""
|
||||||
|
user_iframe_message = ""
|
||||||
|
welcome_page_url = ""
|
||||||
|
welcome_page_display = "none"
|
||||||
|
|
||||||
|
[contacts]
|
||||||
|
; Enable contacts
|
||||||
|
enable = Off
|
||||||
|
allow_sync = On
|
||||||
|
sync_interval = 20
|
||||||
|
type = "sqlite"
|
||||||
|
pdo_dsn = "mysql:host=127.0.0.1;port=3306;dbname=snappymail"
|
||||||
|
pdo_user = "root"
|
||||||
|
pdo_password = ""
|
||||||
|
suggestions_limit = 30
|
||||||
|
|
||||||
|
[security]
|
||||||
|
; Enable CSRF protection (http://en.wikipedia.org/wiki/Cross-site_request_forgery)
|
||||||
|
csrf_protection = On
|
||||||
|
custom_server_signature = "SnappyMail"
|
||||||
|
x_frame_options_header = ""
|
||||||
|
x_xss_protection_header = "1; mode=block"
|
||||||
|
openpgp = Off
|
||||||
|
|
||||||
|
; Login and password for web admin panel
|
||||||
|
admin_login = "admin"
|
||||||
|
admin_password = "12345"
|
||||||
|
|
||||||
|
; Access settings
|
||||||
|
allow_admin_panel = On
|
||||||
|
allow_two_factor_auth = Off
|
||||||
|
force_two_factor_auth = Off
|
||||||
|
hide_x_mailer_header = Off
|
||||||
|
admin_panel_host = ""
|
||||||
|
admin_panel_key = "admin"
|
||||||
|
content_security_policy = ""
|
||||||
|
core_install_access_domain = ""
|
||||||
|
|
||||||
|
[ssl]
|
||||||
|
; Require verification of SSL certificate used.
|
||||||
|
verify_certificate = Off
|
||||||
|
|
||||||
|
; Allow self-signed certificates. Requires verify_certificate.
|
||||||
|
allow_self_signed = On
|
||||||
|
|
||||||
|
; 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
|
||||||
|
client_cert = ""
|
||||||
|
|
||||||
|
[capa]
|
||||||
|
folders = On
|
||||||
|
composer = On
|
||||||
|
contacts = On
|
||||||
|
settings = On
|
||||||
|
quota = On
|
||||||
|
help = On
|
||||||
|
reload = On
|
||||||
|
search = On
|
||||||
|
search_adv = On
|
||||||
|
filters = On
|
||||||
|
x-templates = Off
|
||||||
|
dangerous_actions = On
|
||||||
|
message_actions = On
|
||||||
|
messagelist_actions = On
|
||||||
|
attachments_actions = On
|
||||||
|
|
||||||
|
[login]
|
||||||
|
default_domain = ""
|
||||||
|
|
||||||
|
; Allow language selection on webmail login screen
|
||||||
|
allow_languages_on_login = On
|
||||||
|
determine_user_language = On
|
||||||
|
determine_user_domain = Off
|
||||||
|
welcome_page = Off
|
||||||
|
hide_submit_button = On
|
||||||
|
forgot_password_link_url = ""
|
||||||
|
registration_link_url = ""
|
||||||
|
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
|
||||||
|
|
||||||
|
; List of enabled plugins
|
||||||
|
enabled_list = ""
|
||||||
|
|
||||||
|
[defaults]
|
||||||
|
; Editor mode used by default (Plain, Html, HtmlForced or PlainForced)
|
||||||
|
view_editor_type = "Html"
|
||||||
|
|
||||||
|
; layout: 0 - no preview, 1 - side preview, 2 - bottom preview
|
||||||
|
view_layout = 1
|
||||||
|
view_use_checkboxes = On
|
||||||
|
autologout = 30
|
||||||
|
show_images = Off
|
||||||
|
contacts_autosave = On
|
||||||
|
mail_use_threads = Off
|
||||||
|
allow_draft_autosave = On
|
||||||
|
mail_reply_same_folder = Off
|
||||||
|
|
||||||
|
[logs]
|
||||||
|
; Enable logging
|
||||||
|
enable = Off
|
||||||
|
|
||||||
|
; Logs entire request only if error occured (php requred)
|
||||||
|
write_on_error_only = Off
|
||||||
|
|
||||||
|
; Logs entire request only if php error occured
|
||||||
|
write_on_php_error_only = Off
|
||||||
|
|
||||||
|
; Logs entire request only if request timeout (in seconds) occured.
|
||||||
|
write_on_timeout_only = 0
|
||||||
|
|
||||||
|
; Required for development purposes only.
|
||||||
|
; Disabling this option is not recommended.
|
||||||
|
hide_passwords = On
|
||||||
|
time_offset = "0"
|
||||||
|
session_filter = ""
|
||||||
|
sentry_dsn = ""
|
||||||
|
|
||||||
|
; 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 = "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}"
|
||||||
|
|
||||||
|
[debug]
|
||||||
|
; Special option required for development purposes
|
||||||
|
enable = Off
|
||||||
|
|
||||||
|
[social]
|
||||||
|
; Google
|
||||||
|
google_enable = Off
|
||||||
|
google_enable_auth = Off
|
||||||
|
google_enable_auth_gmail = Off
|
||||||
|
google_enable_drive = Off
|
||||||
|
google_enable_preview = Off
|
||||||
|
google_client_id = ""
|
||||||
|
google_client_secret = ""
|
||||||
|
google_api_key = ""
|
||||||
|
|
||||||
|
; Facebook
|
||||||
|
fb_enable = Off
|
||||||
|
fb_app_id = ""
|
||||||
|
fb_app_secret = ""
|
||||||
|
|
||||||
|
; Twitter
|
||||||
|
twitter_enable = Off
|
||||||
|
twitter_consumer_key = ""
|
||||||
|
twitter_consumer_secret = ""
|
||||||
|
|
||||||
|
; Dropbox
|
||||||
|
dropbox_enable = Off
|
||||||
|
dropbox_api_key = ""
|
||||||
|
|
||||||
|
[cache]
|
||||||
|
; The section controls caching of the entire application.
|
||||||
|
;
|
||||||
|
; Enables caching in the system
|
||||||
|
enable = On
|
||||||
|
|
||||||
|
; Additional caching key. If changed, cache is purged
|
||||||
|
index = "v1"
|
||||||
|
|
||||||
|
; Can be: files, APC, 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
|
||||||
|
|
||||||
|
[labs]
|
||||||
|
; Experimental settings. Handle with care.
|
||||||
|
;
|
||||||
|
allow_mobile_version = On
|
||||||
|
ignore_folders_subscription = Off
|
||||||
|
check_new_password_strength = On
|
||||||
|
update_channel = "stable"
|
||||||
|
allow_gravatar = On
|
||||||
|
allow_prefetch = On
|
||||||
|
allow_smart_html_links = On
|
||||||
|
cache_system_data = On
|
||||||
|
date_from_headers = On
|
||||||
|
autocreate_system_folders = On
|
||||||
|
allow_message_append = Off
|
||||||
|
disable_iconv_if_mbstring_supported = Off
|
||||||
|
login_fault_delay = 1
|
||||||
|
log_ajax_response_write_limit = 300
|
||||||
|
allow_html_editor_source_button = Off
|
||||||
|
allow_html_editor_biti_buttons = Off
|
||||||
|
allow_ctrl_enter_on_compose = On
|
||||||
|
try_to_detect_hidden_images = Off
|
||||||
|
hide_dangerous_actions = Off
|
||||||
|
use_app_debug_js = Off
|
||||||
|
use_mobile_version_for_tablets = Off
|
||||||
|
use_app_debug_css = Off
|
||||||
|
use_imap_sort = On
|
||||||
|
use_imap_force_selection = Off
|
||||||
|
use_imap_list_subscribe = On
|
||||||
|
use_imap_thread = On
|
||||||
|
use_imap_move = Off
|
||||||
|
use_imap_expunge_all_on_delete = Off
|
||||||
|
imap_forwarded_flag = "$Forwarded"
|
||||||
|
imap_read_receipt_flag = "$ReadReceipt"
|
||||||
|
imap_body_text_limit = 555000
|
||||||
|
imap_message_list_fast_simple_search = On
|
||||||
|
imap_message_list_count_limit_trigger = 0
|
||||||
|
imap_message_list_date_filter = 0
|
||||||
|
imap_message_list_permanent_filter = ""
|
||||||
|
imap_message_all_headers = Off
|
||||||
|
imap_large_thread_limit = 50
|
||||||
|
imap_folder_list_limit = 200
|
||||||
|
imap_show_login_alert = On
|
||||||
|
imap_use_auth_plain = On
|
||||||
|
imap_use_auth_cram_md5 = Off
|
||||||
|
smtp_show_server_errors = Off
|
||||||
|
smtp_use_auth_plain = On
|
||||||
|
smtp_use_auth_cram_md5 = Off
|
||||||
|
sieve_allow_raw_script = Off
|
||||||
|
sieve_utf8_folder_name = On
|
||||||
|
sieve_auth_plain_initial = On
|
||||||
|
sieve_allow_fileinto_inbox = Off
|
||||||
|
imap_timeout = 300
|
||||||
|
smtp_timeout = 60
|
||||||
|
sieve_timeout = 10
|
||||||
|
domain_list_limit = 99
|
||||||
|
mail_func_clear_headers = On
|
||||||
|
mail_func_additional_parameters = Off
|
||||||
|
favicon_status = On
|
||||||
|
folders_spec_limit = 50
|
||||||
|
owncloud_save_folder = "Attachments"
|
||||||
|
owncloud_suggestions = On
|
||||||
|
curl_proxy = ""
|
||||||
|
curl_proxy_auth = ""
|
||||||
|
in_iframe = Off
|
||||||
|
force_https = Off
|
||||||
|
custom_login_link = ""
|
||||||
|
custom_logout_link = ""
|
||||||
|
allow_external_login = Off
|
||||||
|
allow_external_sso = Off
|
||||||
|
external_sso_key = ""
|
||||||
|
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 = Off
|
||||||
|
detect_image_exif_orientation = On
|
||||||
|
cookie_default_path = ""
|
||||||
|
cookie_default_secure = Off
|
||||||
|
check_new_messages = On
|
||||||
|
replace_env_in_configuration = ""
|
||||||
|
startup_url = ""
|
||||||
|
strict_html_parser = Off
|
||||||
|
allow_cmd = Off
|
||||||
|
dev_email = ""
|
||||||
|
dev_password = ""
|
||||||
|
|
||||||
|
[version]
|
||||||
|
current = "1.14.0"
|
||||||
|
saved = "Wed, 08 Apr 2020 16:37:27 +0000"
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
#!/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"
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
# 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
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
#!/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
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
/.git
|
|
||||||
/node_modules
|
|
||||||
23
.eslintrc.js
|
|
@ -2,14 +2,15 @@ module.exports = {
|
||||||
parser: 'babel-eslint',
|
parser: 'babel-eslint',
|
||||||
// extends: ['eslint:recommended', 'plugin:prettier/recommended'],
|
// extends: ['eslint:recommended', 'plugin:prettier/recommended'],
|
||||||
extends: ['eslint:recommended'],
|
extends: ['eslint:recommended'],
|
||||||
|
plugins: ['prettier'],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 11,
|
ecmaVersion: 6,
|
||||||
sourceType: 'module'
|
sourceType: 'module'
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
node: true,
|
node: true,
|
||||||
browser: true,
|
browser: true,
|
||||||
es2020: true
|
es6: true
|
||||||
},
|
},
|
||||||
globals: {
|
globals: {
|
||||||
// SnappyMail
|
// SnappyMail
|
||||||
|
|
@ -29,24 +30,19 @@ module.exports = {
|
||||||
'ko': "readonly",
|
'ko': "readonly",
|
||||||
// vendors/routes/
|
// vendors/routes/
|
||||||
'hasher': "readonly",
|
'hasher': "readonly",
|
||||||
|
'signals': "readonly",
|
||||||
'Crossroads': "readonly",
|
'Crossroads': "readonly",
|
||||||
// vendors/jua
|
// vendors/jua
|
||||||
'Jua': "readonly",
|
'Jua': "readonly",
|
||||||
|
// vendors/qr.js
|
||||||
|
'qr': "readonly",
|
||||||
// vendors/bootstrap/bootstrap.native.js
|
// vendors/bootstrap/bootstrap.native.js
|
||||||
'BSN': "readonly",
|
'BSN': "readonly"
|
||||||
// Mailvelope
|
|
||||||
'mailvelope': "readonly",
|
|
||||||
// Punycode
|
|
||||||
'IDN': "readonly",
|
|
||||||
// Turndown
|
|
||||||
'TurndownService': "readonly",
|
|
||||||
// Marked
|
|
||||||
'marked': "readonly"
|
|
||||||
},
|
},
|
||||||
// http://eslint.org/docs/rules/
|
// http://eslint.org/docs/rules/
|
||||||
rules: {
|
rules: {
|
||||||
'no-cond-assign': 0,
|
|
||||||
// plugins
|
// plugins
|
||||||
|
// 'prettier/prettier': 'error',
|
||||||
'no-mixed-spaces-and-tabs': 'off',
|
'no-mixed-spaces-and-tabs': 'off',
|
||||||
'max-len': [
|
'max-len': [
|
||||||
'error',
|
'error',
|
||||||
|
|
@ -58,7 +54,6 @@ module.exports = {
|
||||||
ignoreTrailingComments: true,
|
ignoreTrailingComments: true,
|
||||||
ignorePattern: '(^\\s*(const|let|var)\\s.+=\\s*require\\s*\\(|^import\\s.+\\sfrom\\s.+;$)'
|
ignorePattern: '(^\\s*(const|let|var)\\s.+=\\s*require\\s*\\(|^import\\s.+\\sfrom\\s.+;$)'
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
'no-constant-condition': ["error", { "checkLoops": false }]
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
1
.github/FUNDING.yml
vendored
|
|
@ -1,2 +1,3 @@
|
||||||
github: the-djmaze
|
github: the-djmaze
|
||||||
|
community_bridge: SnappyMail
|
||||||
custom: ["https://www.paypal.me/thedjmaze", "https://snappymail.eu"]
|
custom: ["https://www.paypal.me/thedjmaze", "https://snappymail.eu"]
|
||||||
|
|
|
||||||
2
.github/ISSUE_TEMPLATE.md
vendored
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
**SnappyMail version, browser, browser version:**
|
**RainLoop version, browser, OS:**
|
||||||
|
|
||||||
**Expected behavior and actual behavior:**
|
**Expected behavior and actual behavior:**
|
||||||
|
|
||||||
|
|
|
||||||
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -1,38 +0,0 @@
|
||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**To Reproduce**
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See error
|
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
**Please complete the following information:**
|
|
||||||
- Browser: [e.g. chromium 100, firefox 100, safari 14, mobile]
|
|
||||||
- IMAP daemon: [e.g. courier, dovecot]
|
|
||||||
- PHP version:
|
|
||||||
- SnappyMail Version:
|
|
||||||
- Mode: [e.g. standalone, nextcloud, cyberpanel, docker]
|
|
||||||
|
|
||||||
**Debug/logging information**
|
|
||||||
[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**
|
|
||||||
Add any other context about the problem here.
|
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
|
@ -1,20 +0,0 @@
|
||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
||||||
93
.github/workflows/docker-pr.yml
vendored
|
|
@ -1,93 +0,0 @@
|
||||||
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
|
|
@ -1,117 +0,0 @@
|
||||||
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
|
|
||||||
17
.gitignore
vendored
|
|
@ -1,5 +1,4 @@
|
||||||
*.bak
|
/*.bak
|
||||||
*.old
|
|
||||||
/api.php
|
/api.php
|
||||||
/*.log
|
/*.log
|
||||||
/snappymail/v/0.0.0/static/*
|
/snappymail/v/0.0.0/static/*
|
||||||
|
|
@ -12,21 +11,9 @@
|
||||||
/.docker/dev/mail/config
|
/.docker/dev/mail/config
|
||||||
/.docker/dev/nginx/ssl/*
|
/.docker/dev/nginx/ssl/*
|
||||||
!/.docker/dev/nginx/ssl/.gitempty
|
!/.docker/dev/nginx/ssl/.gitempty
|
||||||
/.docker/release/snappymail-*.zip
|
|
||||||
/dist
|
/dist
|
||||||
/data
|
/data
|
||||||
/MULTIPLY
|
/MULTIPLY
|
||||||
/include.php
|
/include.php
|
||||||
/issues
|
|
||||||
.idea/
|
.idea/
|
||||||
.env
|
.env
|
||||||
/test
|
|
||||||
/public_html
|
|
||||||
/vendors/knockout/spec
|
|
||||||
/vendors/openpgp-5
|
|
||||||
!/vendors/openpgp-5/dist
|
|
||||||
/vendors/squire2
|
|
||||||
!/vendors/squire2/dist
|
|
||||||
/vendors/vanillaqr.js/
|
|
||||||
/integrations/nextcloud/rainloop
|
|
||||||
/integrations/owncloud/rainloop
|
|
||||||
7
.gitmodules
vendored
|
|
@ -1,7 +0,0 @@
|
||||||
[submodule "vendors/openpgp-5"]
|
|
||||||
path = vendors/openpgp-5
|
|
||||||
url = git@github.com:the-djmaze/openpgpjs.git
|
|
||||||
[submodule "vendors/squire2"]
|
|
||||||
path = vendors/squire2
|
|
||||||
url = git@github.com:the-djmaze/Squire.git
|
|
||||||
branch = snappymail
|
|
||||||
66
.htaccess
|
|
@ -1,16 +1,6 @@
|
||||||
<FilesMatch "index\.php">
|
RewriteEngine On
|
||||||
AcceptPathInfo On
|
# Redirect cPanel
|
||||||
</FilesMatch>
|
RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301]
|
||||||
|
|
||||||
<IfModule mod_rewrite.c>
|
|
||||||
RewriteEngine On
|
|
||||||
# Redirect cPanel
|
|
||||||
RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301]
|
|
||||||
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
|
||||||
RewriteCond %{REQUEST_FILENAME} !-d
|
|
||||||
RewriteRule ^(.+)$ index.php/$1 [L,QSA]
|
|
||||||
</IfModule>
|
|
||||||
|
|
||||||
<IfModule mod_expires.c>
|
<IfModule mod_expires.c>
|
||||||
ExpiresActive On
|
ExpiresActive On
|
||||||
|
|
@ -28,17 +18,51 @@
|
||||||
</IfModule>
|
</IfModule>
|
||||||
|
|
||||||
<IfModule mod_headers.c>
|
<IfModule mod_headers.c>
|
||||||
# Header set Cache-Control "public, max-age=31536000"
|
Header set Cache-Control "public, max-age=31536000"
|
||||||
# Header set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data:; style-src 'self' 'unsafe-inline'"
|
Header set Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data:; style-src 'self' 'unsafe-inline'"
|
||||||
# Header set Referrer-Policy "no-referrer"
|
Header set Referrer-Policy "no-referrer"
|
||||||
# Header set Strict-Transport-Security "max-age=31536000"
|
Header set Strict-Transport-Security "max-age=31536000"
|
||||||
Header set imagetoolbar "no"
|
Header set imagetoolbar "no"
|
||||||
# Header set X-Content-Type-Options "nosniff"
|
Header set X-Content-Type-Options "nosniff"
|
||||||
# Header set X-XSS-Protection "1; mode=block"
|
Header set X-Frame-Options "DENY"
|
||||||
|
Header set X-XSS-Protection "1; mode=block"
|
||||||
Header set Service-Worker-Allowed "/"
|
Header set Service-Worker-Allowed "/"
|
||||||
|
|
||||||
# Google FLoC
|
RewriteCond %{HTTP:Accept-encoding} br
|
||||||
# Header set Permissions-Policy "interest-cohort=()"
|
RewriteCond "%{REQUEST_FILENAME}\.br" -s
|
||||||
|
RewriteRule "^(.+)" "$1\.br" [L,T=text/javascript,QSA]
|
||||||
|
|
||||||
|
RewriteCond %{HTTP:Accept-encoding} gzip
|
||||||
|
RewriteCond "%{REQUEST_FILENAME}\.gz" -s
|
||||||
|
RewriteRule "^(.+)" "$1\.gz" [L,T=text/javascript,QSA]
|
||||||
|
|
||||||
|
RewriteCond %{HTTP:Accept-encoding} br
|
||||||
|
RewriteCond "%{REQUEST_FILENAME}\.br" -s
|
||||||
|
RewriteRule "^(.+)" "$1\.br" [L,T=text/css,QSA]
|
||||||
|
|
||||||
|
RewriteCond %{HTTP:Accept-encoding} gzip
|
||||||
|
RewriteCond "%{REQUEST_FILENAME}\.gz" -s
|
||||||
|
RewriteRule "^(.+)" "$1\.gz" [L,T=text/css,QSA]
|
||||||
|
|
||||||
|
<FilesMatch "(\.js\.br|\.css\.br)$">
|
||||||
|
SetEnv no-gzip 1
|
||||||
|
SetEnv no-brotli 1
|
||||||
|
# Serve correct encoding type.
|
||||||
|
Header append Content-Encoding br
|
||||||
|
# Force proxies to cache brotli &
|
||||||
|
# non-brotli css/js files separately.
|
||||||
|
Header append Vary Accept-Encoding
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
<FilesMatch "(\.js\.gz|\.css\.gz)$">
|
||||||
|
SetEnv no-gzip 1
|
||||||
|
SetEnv no-brotli 1
|
||||||
|
# Serve correct encoding type.
|
||||||
|
Header append Content-Encoding gzip
|
||||||
|
# Force proxies to cache gzipped &
|
||||||
|
# non-gzipped css/js files separately.
|
||||||
|
Header append Vary Accept-Encoding
|
||||||
|
</FilesMatch>
|
||||||
</IfModule>
|
</IfModule>
|
||||||
|
|
||||||
#<IfModule mod_brotli.c>
|
#<IfModule mod_brotli.c>
|
||||||
|
|
|
||||||
15
.tx/config
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
[main]
|
||||||
|
host = https://www.transifex.com
|
||||||
|
minimum_perc = 60
|
||||||
|
type = YAML
|
||||||
|
|
||||||
|
[snappymail-webmail.snappymail-webmail]
|
||||||
|
file_filter = snappymail/v/0.0.0/app/localization/webmail/<lang>.yml
|
||||||
|
source_file = snappymail/v/0.0.0/app/localization/webmail/_source.en.yml
|
||||||
|
source_lang = en
|
||||||
|
|
||||||
|
[snappymail-webmail.snappymail-admin]
|
||||||
|
file_filter = snappymail/v/0.0.0/app/localization/admin/<lang>.yml
|
||||||
|
source_file = snappymail/v/0.0.0/app/localization/admin/_source.en.yml
|
||||||
|
source_lang = en
|
||||||
|
|
||||||
2422
CHANGELOG.md
|
|
@ -7,15 +7,14 @@
|
||||||
|
|
||||||
**Getting started**
|
**Getting started**
|
||||||
|
|
||||||
1. Install PHP
|
1. Install node.js - `https://nodejs.org/download/`
|
||||||
2. Install node.js - `https://nodejs.org/download/`
|
2. Install yarn - `https://yarnpkg.com/en/docs/install`
|
||||||
3. Install yarn - `https://yarnpkg.com/en/docs/install`
|
3. Install gulp - `npm install gulp -g`
|
||||||
4. Install gulp - `npm install gulp -g`
|
4. Fork snappymail - `https://github.com/the-djmaze/snappymail/issues/new#fork-destination-box`
|
||||||
5. Fork snappymail from https://github.com/the-djmaze/snappymail
|
5. Clone snappymail - `git clone git@github.com:USERNAME/snappymail.git snappymail`
|
||||||
6. Clone snappymail - `git clone git@github.com:USERNAME/snappymail.git snappymail`
|
6. `cd snappymail`
|
||||||
7. `cd snappymail`
|
7. Install install all dependencies - `yarn install`
|
||||||
8. Install all dependencies - `yarn install`
|
8. Run gulp - `gulp`
|
||||||
9. Run gulp - `gulp`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -29,18 +28,7 @@
|
||||||
**Editing HTML Template Files**
|
**Editing HTML Template Files**
|
||||||
|
|
||||||
1. Edit data/\_data_/\_default_/configs/application.ini
|
1. Edit data/\_data_/\_default_/configs/application.ini
|
||||||
2. Set `[cache] system_data` to Off
|
2. Set 'cache_system_data' to Off
|
||||||
|
|
||||||
**Release**
|
|
||||||
|
|
||||||
1. Install gzip
|
|
||||||
2. Install brotli
|
|
||||||
3. php release.php
|
|
||||||
|
|
||||||
Options:
|
|
||||||
* `php release.php --aur` = Build Arch Linux package
|
|
||||||
* `php release.php --docker` = Build Docker instance
|
|
||||||
* `php release.php --plugins` = Build plugins
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
2
LICENSE
|
|
@ -658,4 +658,4 @@ specific requirements.
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
<http://www.gnu.org/licenses/>.
|
<http://www.gnu.org/licenses/>.
|
||||||
54
Makefile
|
|
@ -1,73 +1,75 @@
|
||||||
#!make
|
#!make
|
||||||
|
|
||||||
rebuild: _down
|
rebuild: _down
|
||||||
docker compose build --no-cache
|
docker-compose build --no-cache
|
||||||
|
|
||||||
up: _up status
|
up: _up status
|
||||||
_up:
|
_up:
|
||||||
docker compose up -d
|
docker-compose up -d
|
||||||
|
|
||||||
stop: _stop status
|
stop: _stop status
|
||||||
_stop:
|
_stop:
|
||||||
docker compose stop
|
docker-compose stop
|
||||||
|
|
||||||
down: _down status
|
down: _down status
|
||||||
_down:
|
_down:
|
||||||
docker compose down
|
docker-compose down
|
||||||
|
|
||||||
restart: _stop _up status
|
restart: _stop _up status
|
||||||
|
|
||||||
status:
|
status:
|
||||||
@docker compose ps
|
@docker-compose ps
|
||||||
|
|
||||||
tx:
|
tx:
|
||||||
@docker compose run --no-deps --rm tx tx pull -a -s -f -d
|
@docker-compose run --no-deps --rm tx tx pull -a -s -f -d
|
||||||
|
|
||||||
console-node:
|
console-node:
|
||||||
@docker compose run --no-deps --rm node sh
|
@docker-compose run --no-deps --rm node sh
|
||||||
console-tx:
|
console-tx:
|
||||||
@docker compose run --no-deps --rm tx sh
|
@docker-compose run --no-deps --rm tx sh
|
||||||
console-php:
|
console-php:
|
||||||
@docker compose exec php sh
|
@docker-compose exec php sh
|
||||||
console: console-node
|
console: console-node
|
||||||
|
|
||||||
logs:
|
logs:
|
||||||
@docker compose logs --tail=100 -f
|
@docker-compose logs --tail=100 -f
|
||||||
logs-db:
|
logs-db:
|
||||||
@docker compose logs --tail=100 -f db
|
@docker-compose logs --tail=100 -f db
|
||||||
logs-php:
|
logs-php:
|
||||||
@docker compose logs --tail=100 -f php
|
@docker-compose logs --tail=100 -f php
|
||||||
logs-node:
|
logs-node:
|
||||||
@docker compose logs --tail=100 -f node
|
@docker-compose logs --tail=100 -f node
|
||||||
logs-nginx:
|
logs-nginx:
|
||||||
@docker compose logs --tail=100 -f nginx
|
@docker-compose logs --tail=100 -f nginx
|
||||||
logs-mail:
|
logs-mail:
|
||||||
@docker compose logs --tail=100 -f mail
|
@docker-compose logs --tail=100 -f mail
|
||||||
logs-tx:
|
logs-tx:
|
||||||
@docker compose logs --tail=100 -f tx
|
@docker-compose logs --tail=100 -f tx
|
||||||
|
|
||||||
rl-lint:
|
rl-lint:
|
||||||
@docker compose run --no-deps --rm node gulp lint
|
@docker-compose run --no-deps --rm node gulp lint
|
||||||
rl-dev:
|
rl-dev:
|
||||||
@docker compose run --no-deps --rm node npm run watch-js
|
@docker-compose run --no-deps --rm node npm run watch-js
|
||||||
rl-compile:
|
rl-compile:
|
||||||
@docker compose run --no-deps --rm node gulp build
|
@docker-compose run --no-deps --rm node gulp build
|
||||||
rl-compile-with-source:
|
rl-compile-with-source:
|
||||||
@docker compose run --no-deps --rm node gulp build --source
|
@docker-compose run --no-deps --rm node gulp build --source
|
||||||
rl-watch-css:
|
rl-watch-css:
|
||||||
@docker compose run --no-deps --rm node npm run watch-css
|
@docker-compose run --no-deps --rm node npm run watch-css
|
||||||
rl-watch-js:
|
rl-watch-js:
|
||||||
@docker compose run --no-deps --rm node npm run watch-js
|
@docker-compose run --no-deps --rm node npm run watch-js
|
||||||
|
|
||||||
rl-build:
|
rl-build:
|
||||||
@docker compose run --no-deps --rm node gulp all
|
@docker-compose run --no-deps --rm node gulp all
|
||||||
|
rl-build-pro:
|
||||||
|
@docker-compose run --no-deps --rm node gulp all --pro
|
||||||
|
|
||||||
yarn-install:
|
yarn-install:
|
||||||
@docker compose run --no-deps --rm node yarn install
|
@docker-compose run --no-deps --rm node yarn install
|
||||||
yarn-outdated:
|
yarn-outdated:
|
||||||
@docker compose run --no-deps --rm node yarn outdated
|
@docker-compose run --no-deps --rm node yarn outdated
|
||||||
yarn-upgrade:
|
yarn-upgrade:
|
||||||
@docker compose run --no-deps --rm node yarn upgrade-interactive --exact --latest
|
@docker-compose run --no-deps --rm node yarn upgrade-interactive --exact --latest
|
||||||
|
|
||||||
gpg:
|
gpg:
|
||||||
docker run -it --rm -w=/var/www \
|
docker run -it --rm -w=/var/www \
|
||||||
|
|
|
||||||
175
README.md
|
|
@ -1,14 +1,10 @@
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://github.com/the-djmaze/snappymail">
|
<a href="https://github.com/the-djmaze/snappymail">
|
||||||
<img src="https://snappymail.eu/static/img/logo-256x256-white.png">
|
<img width="200" heigth="200" src="https://snappymail.eu/static/img/logo-256x256.png">
|
||||||
</a>
|
</a>
|
||||||
<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>
|
||||||
|
|
@ -30,7 +26,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 whole [Wiki](https://github.com/the-djmaze/snappymail/wiki).
|
And don't forget to read the [RainLoop documentation](https://www.rainloop.net/docs/).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
@ -38,87 +34,62 @@ And don't forget to read the whole [Wiki](https://github.com/the-djmaze/snappyma
|
||||||
**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 - 2024 SnappyMail
|
Copyright (c) 2020 SnappyMail
|
||||||
Copyright (c) 2013 - 2022 RainLoop
|
Copyright (c) 2019 RainLoop
|
||||||
|
|
||||||
## Modifications
|
## Modifications
|
||||||
|
|
||||||
This fork of RainLoop has the following changes:
|
This fork of RainLoop has the following changes:
|
||||||
|
|
||||||
* Privacy/GDPR friendly (no: Social, Gravatar, Facebook, Google, Twitter, DropBox, X-Mailer)
|
* Privacy/GDPR friendly (no: Social, Gravatar, Facebook, Google, Twitter, DropBox, OwnCloud, X-Mailer)
|
||||||
* 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
|
||||||
* ES2020
|
* ES2015
|
||||||
* PHP 7.4+ required
|
* PHP 7.3+ required
|
||||||
* PHP mbstring extension required
|
* PHP mbstring extension required
|
||||||
* PHP replaced pclZip with PharData and ZipArchive
|
* PHP replaced pclZip with ZipArchive
|
||||||
* Dark mode
|
* PHP yaml extension else use the old Spyc
|
||||||
* Added option to remove background/font colors from messages for real "dark mode"
|
|
||||||
* Removed BackwardCapability (class \RainLoop\Account)
|
* Removed BackwardCapability (class \RainLoop\Account)
|
||||||
* Removed ChangePassword (re-implemented as plugin)
|
* Removed ChangePassword (plugins won't work)
|
||||||
|
* Removed OAuth support
|
||||||
* Removed POP3 support
|
* Removed POP3 support
|
||||||
* Removed background video support
|
* Removed background video support
|
||||||
* Removed Sentry (Application Monitoring and Error Tracking Software)
|
* Removed Sentry (Application Monitoring and Error Tracking Software)
|
||||||
* Removed Spyc yaml
|
|
||||||
* Removed OwnCloud
|
|
||||||
* Replaced gulp-uglify with gulp-terser
|
* Replaced gulp-uglify with gulp-terser
|
||||||
* CRLF => LF line endings
|
* CRLF => LF line endings
|
||||||
* Embed boot.js and boot.css into index.html
|
* Embed boot.js and boot.css into index.html
|
||||||
* Removal of old JavaScript code (things are native these days)
|
* Ongoing removal of old JavaScript code (things are native these days)
|
||||||
* Added modified [Squire](https://github.com/the-djmaze/Squire/tree/snappymail) HTML editor as replacement for CKEditor
|
* Added modified [Squire](https://github.com/neilj/Squire) HTML editor as replacement for CKEditor
|
||||||
* Updated [Sabre/VObject](https://github.com/sabre-io/vobject)
|
|
||||||
* Split Admin specific JavaScript code from User code
|
* Split Admin specific JavaScript code from User code
|
||||||
* Split Sieve specific JavaScript code from User code
|
|
||||||
* JSON reviver
|
* JSON reviver
|
||||||
* Better memory garbage collection management
|
* Better memory garbage collection management
|
||||||
* Added serviceworker for Notifications
|
* Added serviceworker for Notifications
|
||||||
* Added advanced Sieve scripts editor
|
|
||||||
* Slimmed down language files
|
|
||||||
* Replaced webpack with rollup
|
|
||||||
* No user-agent detection (use device width)
|
|
||||||
* Added support to load plugins as .phar
|
|
||||||
* Replaced old Sabre library
|
|
||||||
* AddressBook Contacts support MySQL/MariaDB utf8mb4
|
|
||||||
* Added [Fetch Metadata Request Headers](https://www.w3.org/TR/fetch-metadata/) checks
|
|
||||||
* Reduced excessive DOM size
|
|
||||||
* Support [Kolab groupware](https://kolab.org/)
|
|
||||||
* Support many more [IMAP RFC's](https://snappymail.eu/comparison#IMAP)
|
|
||||||
* Support Sodium and OpenSSL for encryption
|
|
||||||
* Much better PGP support
|
|
||||||
|
|
||||||
|
|
||||||
### Supported browsers
|
|
||||||
|
|
||||||
This fork uses downsized/simplified versions of scripts and has no support for Internet Explorer nor Edge Legacy.
|
|
||||||
Supported are:
|
|
||||||
|
|
||||||
* Chrome 80+
|
|
||||||
* Edge 80+
|
|
||||||
* Firefox 78+
|
|
||||||
* Opera 67+
|
|
||||||
* Safari 13.1+
|
|
||||||
|
|
||||||
|
|
||||||
### Removal of old JavaScript
|
### Removal of old JavaScript
|
||||||
|
|
||||||
|
This fork uses downsized/simplified versions of scripts and has no support for Internet Explorer.
|
||||||
|
|
||||||
The result is faster and smaller download code (good for mobile networks).
|
The result is faster and smaller download code (good for mobile networks).
|
||||||
|
|
||||||
* Added dev/prototype.js for some additional features
|
Things might work in Edge 18, Firefox 50-62 and Chrome 54-68 due to one polyfill for array.flat().
|
||||||
|
|
||||||
|
* Added dev/prototype*.js for some additional features
|
||||||
|
* boot.js without webpack overhead
|
||||||
* Modified Jua.js to be without jQuery
|
* Modified Jua.js to be without jQuery
|
||||||
|
* Replaced ProgressJS with simple native dropin
|
||||||
* Replaced Autolinker with simple https/email detection
|
* Replaced Autolinker with simple https/email detection
|
||||||
* Replaced momentToNode with proper HTML5 `<time>`
|
* Replaced ifvisible.js with simple drop-in replacement
|
||||||
|
* Replaced momentToNode with proper HTML5 <time>
|
||||||
* Replaced resize listeners with ResizeObserver
|
* Replaced resize listeners with ResizeObserver
|
||||||
* Replaced bootstrap.js with native drop-in replacement
|
* Replaced bootstrap.js with native drop-in replacement
|
||||||
* Replaced dev/Common/ClientStorageDriver/* with Web Storage Objects polyfill
|
* Replaced dev/Common/ClientStorageDriver/* with Web Storage Objects polyfill
|
||||||
* Replaced *Ajax with *Fetch classes because we use the Fetch API, not jQuery.ajax
|
* Replaced *Ajax with *Fetch classes because we use the Fetch API, not jQuery.ajax
|
||||||
* Replaced [knockoutjs](https://github.com/knockout/knockout) 3.4 with a modified 3.5.1
|
* Replaced knockoutjs 3.4 with a modified 3.5.1
|
||||||
* Replaced knockout-sortable with native HTML5 drag&drop
|
* Replaced knockout-sortable with native HTML5 drag&drop
|
||||||
* Replaced simplestatemanager with CSS @media
|
* Replaced simplestatemanager with @media
|
||||||
* Replaced inputosaurus with own code
|
* Replaced inputosaurus to native
|
||||||
* Replaced keymaster with own shortcuts handler
|
* Replaced keymaster with own shortcuts handler
|
||||||
* Replaced OpenPGP.js v2 with OpenPGP.js v5
|
|
||||||
* Removed ifvisible.js
|
|
||||||
* Removed pikaday
|
* Removed pikaday
|
||||||
* Removed underscore
|
* Removed underscore
|
||||||
* Removed polyfills
|
* Removed polyfills
|
||||||
|
|
@ -133,82 +104,59 @@ The result is faster and smaller download code (good for mobile networks).
|
||||||
* Removed momentjs (use Intl)
|
* Removed momentjs (use Intl)
|
||||||
* Removed opentip (use CSS)
|
* Removed opentip (use CSS)
|
||||||
* Removed non-community (aka Prem/Premium/License) code
|
* Removed non-community (aka Prem/Premium/License) code
|
||||||
* Removed ProgressJS
|
|
||||||
|
|
||||||
|
|
||||||
RainLoop 1.17 vs SnappyMail
|
RainLoop 1.14 vs SnappyMail
|
||||||
|
|
||||||
|js/* |RainLoop |Snappy |
|
|js/* |RainLoop |Snappy |
|
||||||
|--------------- |--------: |--------: |
|
|--------------- |--------: |--------: |
|
||||||
|admin.js |2.170.153 | 84.925 |
|
|admin.js |2.130.942 | 648.785 |
|
||||||
|app.js |4.207.787 | 447.263 |
|
|app.js |4.184.455 |2.311.912 |
|
||||||
|boot.js | 868.735 | 4.343 |
|
|boot.js | 671.522 | 4.842 |
|
||||||
|libs.js | 658.812 | 233.728 |
|
|libs.js | 647.614 | 235.394 |
|
||||||
|sieve.js | 0 | 91.418 |
|
|polyfills.js | 325.834 | 0 |
|
||||||
|polyfills.js | 334.608 | 0 |
|
|
||||||
|serviceworker.js | 0 | 285 |
|
|serviceworker.js | 0 | 285 |
|
||||||
|TOTAL |8.240.095 | 861.962 |
|
|TOTAL |7.960.367 |3.200.933 |
|
||||||
|
|
||||||
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
|js/min/* |RainLoop |Snappy |Rain gzip |gzip |brotli |
|
||||||
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|
|--------------- |--------: |--------: |--------: |--------: |--------: |
|
||||||
|admin.min.js | 256.831 | 41.719 | 73.606 | 14.022 | 60.877 | 12.567 |
|
|admin.min.js | 252.147 | 89.946 | 73.657 | 23.622 | 20.672 |
|
||||||
|app.min.js | 515.367 | 202.101 |139.456 | 68.505 |110.485 | 58.481 |
|
|app.min.js | 511.202 | 310.186 |140.462 | 83.124 | 67.807 |
|
||||||
|boot.min.js | 84.659 | 2.231 | 26.998 | 1.271 | 23.643 | 1.067 |
|
|boot.min.js | 66.007 | 2.630 | 22.567 | 1.375 | 1.189 |
|
||||||
|libs.min.js | 584.772 | 110.646 |180.901 | 39.518 |155.182 | 35.207 |
|
|libs.min.js | 572.545 | 130.850 |176.720 | 47.317 | 42.092 |
|
||||||
|sieve.min.js | 0 | 45.504 | 0 | 11.131 | 0 | 9.917 |
|
|polyfills.min.js | 32.452 | 0 | 11.312 | 0 | 0 |
|
||||||
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 |
|
|TOTAL |1.434.353 | 533.612 |424.718 |155.438 |131.760 |
|
||||||
|TOTAL user |1.217.635 | 314.978 |358.761 |109.294 |299.485 | 94.755 |
|
|TOTAL (no admin) |1.182.206 | 443.666 |351.061 |131.816 |111.088 |
|
||||||
|TOTAL user+sieve |1.217.635 | 360.482 |358.761 |120.425 |299.485 |104.672 |
|
|
||||||
|TOTAL admin | 959.099 | 154.596 |292.911 | 54.811 |249.877 | 48.841 |
|
|
||||||
|
|
||||||
For a user it is around 66% smaller and faster than traditional RainLoop.
|
For a user its around 62% smaller and faster than traditional RainLoop.
|
||||||
|
|
||||||
### CSS changes
|
### CSS changes
|
||||||
|
|
||||||
* Solve jQuery removed "features" with native css code
|
* Solve jQuery removed "features" with native css code.
|
||||||
* Themes work in mobile mode
|
|
||||||
* Bugfix invalid/conflicting css rules
|
|
||||||
* Use flexbox
|
|
||||||
* Use border-box
|
|
||||||
* Split app.css to have separate admin.css
|
|
||||||
* Remove oldschool 'float'
|
|
||||||
* Remove unused css
|
|
||||||
* Removed html.no-css
|
* Removed html.no-css
|
||||||
* Removed dev/Styles/Cmd.less
|
* Removed dev/Styles/Cmd.less
|
||||||
* Removed dev/Styles/Scroll.less
|
* Removed dev/Styles/Scroll.less
|
||||||
* Removed Internet Explorer from normalize.css
|
* Removed Internet Explorer from normalize.css
|
||||||
* Removed node_modules/opentip/css/opentip.css
|
* Removed node_modules/opentip/css/opentip.css
|
||||||
* Removed node_modules/pikaday/css/pikaday.css
|
* Removed node_modules/pikaday/css/pikaday.css
|
||||||
* Removed unused vendors/bootstrap/less/*
|
* Removed vendors/bootstrap/less/breadcrumbs.less
|
||||||
|
* Removed vendors/bootstrap/less/navbar.less
|
||||||
|
* Removed vendors/bootstrap/less/popovers.less
|
||||||
|
* Removed vendors/bootstrap/less/progress-bars.less
|
||||||
|
* Removed vendors/bootstrap/less/scaffolding.less
|
||||||
|
* Removed vendors/bootstrap/less/sprites.less
|
||||||
|
* Removed vendors/bootstrap/less/tooltip.less
|
||||||
* Removed vendors/jquery-nanoscroller/nanoscroller.css
|
* Removed vendors/jquery-nanoscroller/nanoscroller.css
|
||||||
* Removed vendors/jquery-letterfx/jquery-letterfx.min.css
|
* Removed vendors/jquery-letterfx/jquery-letterfx.min.css
|
||||||
* Removed vendors/Progress.js/minified/progressjs.min.css
|
* Removed vendors/Progress.js/minified/progressjs.min.css
|
||||||
* Removed gulp-autoprefixer
|
|
||||||
|
|
||||||
|
|
||||||
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|
|css/* |RainLoop |Snappy |Rain gzip |gzip |brotli |
|
||||||
|------------ |-------: |------: |------: |------: |--------: |
|
|-------------- |-------: |-------: |------: |------: |------: |
|
||||||
|app.css | 340.331 | 85.073 | 46.946 | 17.792 | 15.210 |
|
|app.css | 340.334 | 185.667 | 46,959 | 28.735 | 24.150 |
|
||||||
|app.min.css | 274.947 | 68.272 | 39.647 | 15.615 | 13.636 |
|
|app.min.css | 274.791 | 151.837 | 39.618 | 25.364 | 21.888 |
|
||||||
|boot.css | | 1.326 | | 664 | 545 |
|
|boot.css | | 2.534 | | 837 | 668 |
|
||||||
|boot.min.css | | 1.071 | | 590 | 474 |
|
|boot.min.css | | 2.055 | | 732 | 560 |
|
||||||
|admin.css | | 30.880 | | 7.045 | 6.127 |
|
|
||||||
|admin.min.css | | 24.959 | | 6.368 | 5.615 |
|
|
||||||
|
|
||||||
### PGP
|
|
||||||
RainLoop uses the old OpenPGP.js v2
|
|
||||||
SnappyMail v2.12 uses OpenPGP.js v5, GnuPG and Mailvelope.
|
|
||||||
SnappyMail is able to use and generate ECDSA and EDDSA keys, where RainLoop does not.
|
|
||||||
|
|
||||||
Since SnappyMail tries to achieve the best mobile experience, it forked OpenPGP.js to strip it down.
|
|
||||||
* remove all unused Node.js
|
|
||||||
* remove all old browsers support
|
|
||||||
See https://github.com/the-djmaze/openpgpjs for development
|
|
||||||
|
|
||||||
|OpenPGP |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|
|
||||||
|--------------- |--------: |--------: |------: |-------: |--------: |--------: |
|
|
||||||
|openpgp.min.js | 330.742 | 546.165 |102.388 | 169.207 | 84.241 | 138.688 |
|
|
||||||
|openpgp.worker | 1.499 | | 824 | | 695 | |
|
|
||||||
|
|
||||||
|
|
||||||
### Squire vs CKEditor
|
### Squire vs CKEditor
|
||||||
|
|
@ -217,10 +165,19 @@ The [Squire](https://github.com/neilj/Squire) implementation is not 100% compati
|
||||||
Still TODO:
|
Still TODO:
|
||||||
|
|
||||||
* support for tables (really needed?!?)
|
* support for tables (really needed?!?)
|
||||||
|
* support BIDI (really needed?!?)
|
||||||
|
|
||||||
| | normal | min | gzip | min gzip |
|
| | normal | min | gzip | min gzip |
|
||||||
|-------- |-------: |-------: |------: |--------: |
|
|-------- |-------: |-------: |------: |--------: |
|
||||||
|squire | 122.321 | 41.906 | 31.867 | 14.330 |
|
|squire | 128.826 | 47.074 | 33.671 | 15.596 |
|
||||||
|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 enable it in /data/\_data\_/\_default\_/configs/application.ini
|
||||||
|
in the [labs] section add/edit: `use_ck_html_editor = On`
|
||||||
|
|
||||||
|
### PHP73 branch
|
||||||
|
|
||||||
|
There's a RainLoop 1.14.0 branch with only the PHP 7.3 changes at
|
||||||
|
https://github.com/the-djmaze/snappymail/tree/php73
|
||||||
|
|
|
||||||
40
SECURITY.md
|
|
@ -1,40 +0,0 @@
|
||||||
# Security Policy
|
|
||||||
|
|
||||||
## Supported Versions
|
|
||||||
|
|
||||||
Currently due to the fast development only the latest version receives security updates.
|
|
||||||
|
|
||||||
| Version | Supported |
|
|
||||||
| -------- | --------- |
|
|
||||||
| 2.34.x | ✔ |
|
|
||||||
| < 2.34.0 | ❌ |
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
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-----
|
|
||||||
Comment: Type: 255-bit EdDSA
|
|
||||||
Comment: Fingerprint: 445D265124E6072671E64D0733F868A7E35E8277
|
|
||||||
|
|
||||||
mDMEYiE2QRYJKwYBBAHaRw8BAQdAqMrQUm6DddWcQNo0VEjNIu3Q6CfP3nokVv2Y
|
|
||||||
rNQ1avq0LFNuYXBweU1haWwgU2VjdXJpdHkgPHNlY3VyaXR5QHNuYXBweW1haWwu
|
|
||||||
ZXU+iJQEExYKADwWIQREXSZRJOYHJnHmTQcz+Gin416CdwUCYiE2QQIbAwULCQgH
|
|
||||||
AgMiAgEGFQoJCAsCBBYCAwECHgcCF4AACgkQM/hop+NegnfSGgD9GEHpOrvWpBGY
|
|
||||||
dYfvVd/+Lv5d+dFBcPyki9zu9zHfhwkBAL343EF6ZR0XwMlOQu9wu0hT9KBz4g55
|
|
||||||
6D41i0PrEaoBuDgEYiE2QRIKKwYBBAGXVQEFAQEHQMMr9gcVcJ3aiup/tpl8ZXxy
|
|
||||||
aJiJRGkPyNwGI5vxHMpZAwEIB4h4BBgWCgAgFiEERF0mUSTmByZx5k0HM/hop+Ne
|
|
||||||
gncFAmIhNkECGwwACgkQM/hop+NegndVhgD/SVGSKbF4G2W024VpW2tm3zCT+ue+
|
|
||||||
YMXQVq4SJt7UpWABAORudfJxsBqCRKtPlZMgGTJLjcOkyFJ9C2Fx7DeN0J4I
|
|
||||||
=nSOi
|
|
||||||
-----END PGP PUBLIC KEY BLOCK-----
|
|
||||||
```
|
|
||||||
|
|
||||||
## Publishing and Credits
|
|
||||||
|
|
||||||
I will analyze and fix the reported issue as fast as possible.
|
|
||||||
Together with the reporter, I plan the disclosure of the found and fixed vulnerability.
|
|
||||||
Credits to the reporter are granted and can be included in all public communication if desired.
|
|
||||||
33
_include.php
|
|
@ -4,28 +4,29 @@
|
||||||
|
|
||||||
//header('Strict-Transport-Security: max-age=31536000');
|
//header('Strict-Transport-Security: max-age=31536000');
|
||||||
|
|
||||||
/**
|
// Uncomment to use gzip encoded output
|
||||||
* Uncomment to enable multiple domain installation.
|
//define('USE_GZIP', 1);
|
||||||
*/
|
|
||||||
|
// Uncomment to enable multiple domain installation.
|
||||||
//define('MULTIDOMAIN', 1);
|
//define('MULTIDOMAIN', 1);
|
||||||
|
|
||||||
/**
|
|
||||||
* Uncomment to disable APCU.
|
|
||||||
*/
|
|
||||||
//define('APP_USE_APCU_CACHE', false);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom 'data' folder path
|
* Custom 'data' folder path
|
||||||
|
* @return string
|
||||||
*/
|
*/
|
||||||
//define('APP_DATA_FOLDER_PATH', dirname(__DIR__) . '/snappymail-data/');
|
function __get_custom_data_full_path()
|
||||||
//define('APP_DATA_FOLDER_PATH', '/var/external-snappymail-data-folder/');
|
{
|
||||||
|
return '';
|
||||||
|
return dirname(__DIR__) . '/snappymail-data';
|
||||||
|
return '/var/external-snappymail-data-folder';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Additional configuration file name
|
* Additional configuration file name
|
||||||
|
* @return string
|
||||||
*/
|
*/
|
||||||
//define('APP_CONFIGURATION_NAME', $_SERVER['HTTP_HOST'].'.ini');
|
function __get_additional_configuration_name()
|
||||||
|
{
|
||||||
/**
|
return '';
|
||||||
* Also update extensions on upgrade
|
return defined('APP_SITE') && 0 < strlen(APP_SITE) ? APP_SITE.'.ini' : '';
|
||||||
*/
|
}
|
||||||
//define('SNAPPYMAIL_UPDATE_PLUGINS', 1);
|
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 992 B After Width: | Height: | Size: 992 B |
BIN
assets/css/images/empty-contact.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 992 B After Width: | Height: | Size: 992 B |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 866 B After Width: | Height: | Size: 866 B |
|
Before Width: | Height: | Size: 335 B After Width: | Height: | Size: 335 B |
|
Before Width: | Height: | Size: 992 B After Width: | Height: | Size: 992 B |
16
assets/manifest.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "SnappyMail Webmail",
|
||||||
|
"short_name": "SnappyMail",
|
||||||
|
"description": "Simple, modern & fast web-based email client",
|
||||||
|
"display": "standalone",
|
||||||
|
"icons": [{
|
||||||
|
"src": "apple-touch-icon.png",
|
||||||
|
"sizes": "64x64",
|
||||||
|
"type": "image/png"
|
||||||
|
}, {
|
||||||
|
"src": "android-icon.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
}],
|
||||||
|
"start_url": "../../../../"
|
||||||
|
}
|
||||||
BIN
assets/sounds/new-mail.mp3
Normal file
BIN
assets/sounds/new-mail.ogg
Normal file
|
|
@ -1,9 +0,0 @@
|
||||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
||||||
|
|
||||||
mDMEYg0atBYJKwYBBAHaRw8BAQdA2S2tvGavChACjtBastsKRThD3rsBW1LUZLmN
|
|
||||||
Zbs4uaG0I1NuYXBweU1haWwgPHJlbGVhc2VzQHNuYXBweW1haWwuZXU+iJQEExYK
|
|
||||||
ADwWIQQQFuRweRRVQvi6EzVIIIuhMpDz6wUCYg0atAIbAwULCQgHAgMiAgEGFQoJ
|
|
||||||
CAsCBBYCAwECHgcCF4AACgkQSCCLoTKQ8+u9SAD/Q/IoAwjUkKDJBPq0RGwCFnl6
|
|
||||||
FG/VHB97CvBSpGOxtIsBAMCwMhWlsaBHAEqbzxiN+cdlMYwV23+SWLUJ/XMFgukE
|
|
||||||
=5ZwF
|
|
||||||
-----END PGP PUBLIC KEY BLOCK-----
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
# Maintainer: George Rawlinson <george@rawlinson.net.nz>
|
|
||||||
|
|
||||||
pkgname=snappymail
|
|
||||||
pkgver=2.7.2
|
|
||||||
pkgrel=1
|
|
||||||
pkgdesc="modern PHP webmail client"
|
|
||||||
arch=('any')
|
|
||||||
license=('AGPL3')
|
|
||||||
url="https://github.com/the-djmaze/snappymail"
|
|
||||||
depends=('php-fpm')
|
|
||||||
makedepends=('php', 'nodejs' 'yarn' 'gulp', 'rollup')
|
|
||||||
optdepends=('mariadb: storage backend for contacts'
|
|
||||||
'php-pgsql: storage backend for contacts'
|
|
||||||
'php-sqlite: storage backend for contacts')
|
|
||||||
source=("$pkgname-$pkgver.tar.gz::$url/archive/v$pkgver.tar.gz"
|
|
||||||
"$pkgname.sysusers"
|
|
||||||
"$pkgname.tmpfiles")
|
|
||||||
b2sums=('a877041eaa74ca9824323b0ba80b3515205628825ed76fccff1511b2547c27419bae4733d24f8f36e6860c671cb609c4c5f31fa19529a92c628cb714570ff8ae'
|
|
||||||
'e020b2d4bc694ca056f5c15b148c69553ab610b5e1789f52543aa65e098f8097a41709b5b0fc22a6a01088a9d3f14d623b1b6e9ae2570acd4f380f429301c003'
|
|
||||||
'2536e11622895322cc752c6b651811b2122d3ae60099fe609609d7b45ba1ed00ea729c23f344405078698d161dbf9bcaffabf8eff14b740acdce3c681c513318')
|
|
||||||
|
|
||||||
prepare() {
|
|
||||||
sed -i "s/\$sCustomDataPath = '';/\$sCustomDataPath = '\/var\/lib\/$pkgname';/" "$pkgname-$pkgver/$pkgname/v/0.0.0/include.php"
|
|
||||||
|
|
||||||
# create folder for build output
|
|
||||||
mkdir -p build
|
|
||||||
}
|
|
||||||
|
|
||||||
build() {
|
|
||||||
cd "$pkgname-$pkgver"
|
|
||||||
yarn install
|
|
||||||
|
|
||||||
# build snappymail
|
|
||||||
php release.php --aur
|
|
||||||
bsdtar -x \
|
|
||||||
-C "$srcdir/build" \
|
|
||||||
-f "build/dist/releases/webmail/$pkgver/$pkgname-$pkgver.zip"
|
|
||||||
find . -type d -exec chmod 755 {} \;
|
|
||||||
find . -type f -exec chmod 644 {} \;
|
|
||||||
}
|
|
||||||
|
|
||||||
package() {
|
|
||||||
# directories
|
|
||||||
install -dm755 "$pkgdir/usr/share/snappymail" \
|
|
||||||
"$pkgdir/var/lib/snappymail"
|
|
||||||
|
|
||||||
# application files
|
|
||||||
cp -r "$srcdir/build/snappymail" "$pkgdir/usr/share/snappymail"
|
|
||||||
install -Dm644 -t "$pkgdir/usr/share/snappymail" "$srcdir/build/index.php"
|
|
||||||
|
|
||||||
# data files
|
|
||||||
cp -r "$srcdir/build/data" "$pkgdir/var/lib/snappymail"
|
|
||||||
|
|
||||||
# sysusers
|
|
||||||
install -Dm644 "$srcdir/$pkgname.sysusers" "$pkgdir/usr/lib/sysusers.d/$pkgname.conf"
|
|
||||||
install -Dm644 "$srcdir/$pkgname.tmpfiles" "$pkgdir/usr/lib/tmpfiles.d/$pkgname.conf"
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
u snappymail - "Snappymail User" /var/lib/snappymail
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Z /var/lib/snappymail - snappymail snappymail
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
<?php
|
|
||||||
echo "\x1b[33;1m === cPanel === \x1b[0m\n";
|
|
||||||
|
|
||||||
$cpanel_destination = "{$destPath}snappymail-{$package->version}-cpanel.tar";
|
|
||||||
|
|
||||||
@unlink($cpanel_destination);
|
|
||||||
@unlink("{$cpanel_destination}.gz");
|
|
||||||
|
|
||||||
$cpanel_tar = new PharData($cpanel_destination);
|
|
||||||
|
|
||||||
$cpanel_tar->buildFromDirectory('./integrations/cpanel', "@integrations/cpanel/@");
|
|
||||||
|
|
||||||
$cpanel_path = '/usr/local/cpanel/base/3rdparty/snappymail/';
|
|
||||||
|
|
||||||
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('snappymail/v'), RecursiveIteratorIterator::SELF_FIRST);
|
|
||||||
foreach ($files as $file) {
|
|
||||||
if (is_file($file)) {
|
|
||||||
$newFile = str_replace('\\', '/', $file);
|
|
||||||
$cpanel_tar->addFile($file, "{$cpanel_path}{$newFile}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$cpanel_tar->addFile('.htaccess', "{$cpanel_path}.htaccess");
|
|
||||||
$cpanel_tar->addFile('README.md', "{$cpanel_path}README.md");
|
|
||||||
$cpanel_tar->addFromString("{$cpanel_path}VERSION", $package->version);
|
|
||||||
|
|
||||||
$index = file_get_contents('index.php');
|
|
||||||
$index = str_replace('0.0.0', $package->version, $index);
|
|
||||||
$cpanel_tar->addFromString("{$cpanel_path}index.php", $index);
|
|
||||||
|
|
||||||
$data = file_get_contents('dev/serviceworker.js');
|
|
||||||
$cpanel_tar->addFromString("{$cpanel_path}serviceworker.js", $data);
|
|
||||||
|
|
||||||
$cpanel_tar->compress(Phar::GZ);
|
|
||||||
unlink($cpanel_destination);
|
|
||||||
$cpanel_destination .= '.gz';
|
|
||||||
|
|
||||||
echo "{$cpanel_destination} created\n";
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
<?php
|
|
||||||
echo "\x1b[33;1m === Debian === \x1b[0m\n";
|
|
||||||
|
|
||||||
// Debian Repository
|
|
||||||
define('DEB_SOURCE_DIR', __DIR__ . '/deb');
|
|
||||||
define('DEB_DEST_DIR', DEB_SOURCE_DIR . "/snappymail_{$package->version}-1_all");
|
|
||||||
is_dir(DEB_DEST_DIR) && passthru('rm -dfr '.escapeshellarg(DEB_DEST_DIR));
|
|
||||||
|
|
||||||
$dir = DEB_DEST_DIR . '/DEBIAN';
|
|
||||||
$data = file_get_contents(DEB_SOURCE_DIR . '/DEBIAN/control');
|
|
||||||
$data = str_replace('0.0.0', $package->version, $data);
|
|
||||||
mkdir($dir, 0755, true);
|
|
||||||
file_put_contents("{$dir}/control", $data);
|
|
||||||
copy(DEB_SOURCE_DIR . '/DEBIAN/postinst', $dir . '/postinst');
|
|
||||||
chmod($dir . '/postinst', 0755);
|
|
||||||
|
|
||||||
$dir = DEB_DEST_DIR . '/var/lib/snappymail';
|
|
||||||
mkdir($dir, 0755, true);
|
|
||||||
file_put_contents($dir . '/VERSION', $package->version);
|
|
||||||
copy('data/README.md', "{$dir}/README.md");
|
|
||||||
|
|
||||||
$dir = DEB_DEST_DIR . '/usr/share/doc/snappymail';
|
|
||||||
mkdir($dir, 0755, true);
|
|
||||||
copy('CODE_OF_CONDUCT.md', "{$dir}/CODE_OF_CONDUCT.md");
|
|
||||||
copy('CONTRIBUTING.md', "{$dir}/CONTRIBUTING.md");
|
|
||||||
copy('README.md', "{$dir}/README.md");
|
|
||||||
//copy('CODE_OF_CONDUCT.md', "{$dir}/CODE_OF_CONDUCT.md");
|
|
||||||
//usr/share/doc/snappymail/README.Debian
|
|
||||||
//usr/share/doc/snappymail/changelog.Debian.gz
|
|
||||||
//usr/share/doc/snappymail/copyright
|
|
||||||
|
|
||||||
// Move files into package directory
|
|
||||||
$dir = DEB_DEST_DIR . '/usr/share/snappymail';
|
|
||||||
mkdir($dir, 0755, true);
|
|
||||||
passthru('cp -r "' . dirname(__DIR__) . '/snappymail" "' . $dir . '"');
|
|
||||||
|
|
||||||
rename("{$dir}/snappymail/v/0.0.0", "{$dir}/snappymail/v/{$package->version}");
|
|
||||||
|
|
||||||
$data = file_get_contents('index.php');
|
|
||||||
file_put_contents("{$dir}/index.php", str_replace('0.0.0', $package->version, $data));
|
|
||||||
|
|
||||||
$data = file_get_contents('_include.php');
|
|
||||||
file_put_contents("{$dir}/include.php", preg_replace('@(external-snappymail-data-folder/\'\);)@', "\$1\ndefine('APP_DATA_FOLDER_PATH', '/var/lib/snappymail/');", $data));
|
|
||||||
|
|
||||||
passthru('dpkg --build ' . escapeshellarg(DEB_DEST_DIR));
|
|
||||||
|
|
||||||
$TARGET_DIR = __DIR__ . "/dist/releases/webmail/{$package->version}/";
|
|
||||||
|
|
||||||
passthru('mv '
|
|
||||||
. escapeshellarg(DEB_DEST_DIR.'.deb') . ' '
|
|
||||||
. escapeshellarg($TARGET_DIR . basename(DEB_DEST_DIR.'.deb'))
|
|
||||||
);
|
|
||||||
|
|
||||||
passthru('rm -dfr '.escapeshellarg(DEB_DEST_DIR));
|
|
||||||
|
|
||||||
// https://github.com/the-djmaze/snappymail/issues/185#issuecomment-1059420588
|
|
||||||
$cwd = getcwd();
|
|
||||||
chdir($TARGET_DIR);
|
|
||||||
passthru('dpkg-scanpackages . /dev/null > '.escapeshellarg($TARGET_DIR . 'Packages'));
|
|
||||||
passthru('dpkg-scanpackages . /dev/null | gzip -9c > '.escapeshellarg($TARGET_DIR . 'Packages.gz'));
|
|
||||||
$size = filesize($TARGET_DIR . 'Packages');
|
|
||||||
$gz_size = filesize($TARGET_DIR . 'Packages.gz');
|
|
||||||
$Release = 'Origin: SnappyMail Repository
|
|
||||||
Label: SnappyMail
|
|
||||||
Suite: stable
|
|
||||||
Codename: stable
|
|
||||||
Version: 1.0
|
|
||||||
Architectures: all
|
|
||||||
Components: main
|
|
||||||
Description: SnappyMail repository
|
|
||||||
Date: ' . gmdate('r') . '
|
|
||||||
MD5Sum:
|
|
||||||
' . hash_file('md5', $TARGET_DIR . 'Packages') . ' ' . $size . ' Packages
|
|
||||||
' . hash_file('md5', $TARGET_DIR . 'Packages.gz') . ' ' . $gz_size . ' Packages.gz
|
|
||||||
SHA1:
|
|
||||||
' . hash_file('sha1', $TARGET_DIR . 'Packages') . ' ' . $size . ' Packages
|
|
||||||
' . hash_file('sha1', $TARGET_DIR . 'Packages.gz') . ' ' . $gz_size . ' Packages.gz
|
|
||||||
SHA256:
|
|
||||||
' . hash_file('sha256', $TARGET_DIR . 'Packages') . ' ' . $size . ' Packages
|
|
||||||
' . hash_file('sha256', $TARGET_DIR . 'Packages.gz') . ' ' . $gz_size . ' Packages.gz
|
|
||||||
';
|
|
||||||
file_put_contents($TARGET_DIR . 'Release', $Release);
|
|
||||||
chdir($cwd);
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
Package: snappymail
|
|
||||||
Version: 0.0.0
|
|
||||||
Maintainer: SnappyMail <debian@snappymail.eu>
|
|
||||||
Depends: nginx | apache2 | httpd, php-fpm | libapache2-mod-php, php-json, php-mbstring
|
|
||||||
Recommends: php-intl, php-sodium, php-uuid
|
|
||||||
Suggests: php-sqlite3 | php-mysql | php-pgsql, php-curl, php-exif, php-gnupg, php-gd | php-gmagick | php-imagick, php-openssl, php-zip
|
|
||||||
Architecture: all
|
|
||||||
Homepage: https://snappymail.eu
|
|
||||||
Vcs-Browser: https://github.com/the-djmaze/snappymail
|
|
||||||
Vcs-Git: https://github.com/the-djmaze/snappymail.git
|
|
||||||
Description: SnappyMail is a PHP-based simple, modern, lightweight & fast web-based email client with no database requirements.
|
|
||||||
It supports IMAP, SMTP and Sieve protocols, multiple accounts and identities, an admin panel for configuration.
|
|
||||||
Plugins can be installed to further extend functionality.
|
|
||||||
Emails are not stored locally, but are accessed through IMAP.
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
chown -R www-data:www-data /var/lib/snappymail
|
|
||||||
chown -R root:root /usr/share/snappymail/
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
<?php
|
|
||||||
echo "\x1b[33;1m === Nextcloud === \x1b[0m\n";
|
|
||||||
|
|
||||||
$cert_dir = $_SERVER['HOME'].'/.nextcloud/certificates';
|
|
||||||
|
|
||||||
$nc_destination = "{$destPath}snappymail-{$package->version}-nextcloud.tar";
|
|
||||||
|
|
||||||
@unlink($nc_destination);
|
|
||||||
@unlink("{$nc_destination}.gz");
|
|
||||||
|
|
||||||
$nc_tar = new PharData($nc_destination);
|
|
||||||
$hashes = [];
|
|
||||||
|
|
||||||
file_put_contents(ROOT_DIR . '/integrations/nextcloud/snappymail/VERSION', $package->version);
|
|
||||||
$file = ROOT_DIR . '/integrations/nextcloud/snappymail/appinfo/info.xml';
|
|
||||||
file_put_contents($file, preg_replace('/<version>[^<]*</', "<version>{$package->version}<", file_get_contents($file)));
|
|
||||||
|
|
||||||
$nc_tar->buildFromDirectory('./integrations/nextcloud', "@integrations/nextcloud/snappymail/@");
|
|
||||||
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('integrations/nextcloud/snappymail'));
|
|
||||||
foreach ($files as $file) {
|
|
||||||
if (is_file($file)) {
|
|
||||||
$name = str_replace('\\', '/', $file);
|
|
||||||
$name = str_replace('integrations/nextcloud/snappymail/', '', $name);
|
|
||||||
$hashes[$name] = hash_file('sha512', $file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('snappymail/v'), RecursiveIteratorIterator::SELF_FIRST);
|
|
||||||
foreach ($files as $file) {
|
|
||||||
if (is_file($file)) {
|
|
||||||
$newFile = str_replace('\\', '/', $file);
|
|
||||||
$newName = str_replace('/.htaccess', '/_htaccess', $newFile);
|
|
||||||
$nc_tar->addFile($file, "snappymail/app/{$newName}");
|
|
||||||
$hashes["app/{$newFile}"] = hash_file('sha512', $file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
$nc_tar->addFile('data/.htaccess');
|
|
||||||
$nc_tar->addFromString('data/VERSION', $package->version);
|
|
||||||
$nc_tar->addFile('data/README.md');
|
|
||||||
$nc_tar->addFile('_include.php', 'snappymail/app/_include.php');
|
|
||||||
*/
|
|
||||||
$nc_tar->addFile('.htaccess', 'snappymail/app/_htaccess');
|
|
||||||
$hashes['app/.htaccess'] = hash_file('sha512', '.htaccess');
|
|
||||||
|
|
||||||
$index = file_get_contents('index.php');
|
|
||||||
$index = str_replace('0.0.0', $package->version, $index);
|
|
||||||
//$index = str_replace('snappymail/v/', '', $index);
|
|
||||||
$nc_tar->addFromString('snappymail/app/index.php', $index);
|
|
||||||
$hashes['app/index.php'] = hash('sha512', $index);
|
|
||||||
|
|
||||||
$nc_tar->addFile('README.md', 'snappymail/app/README.md');
|
|
||||||
$hashes['app/README.md'] = hash_file('sha512', 'README.md');
|
|
||||||
|
|
||||||
$nc_tar->addFile('CHANGELOG.md', 'snappymail/CHANGELOG.md');
|
|
||||||
$hashes['CHANGELOG.md'] = hash_file('sha512', 'CHANGELOG.md');
|
|
||||||
|
|
||||||
$data = file_get_contents('dev/serviceworker.js');
|
|
||||||
$nc_tar->addFromString('snappymail/app/serviceworker.js', $data);
|
|
||||||
$hashes['app/serviceworker.js'] = hash('sha512', $data);
|
|
||||||
|
|
||||||
spl_autoload_register(function($name){
|
|
||||||
$file = __DIR__ . '/' . str_replace('\\', '/', $name) . '.php';
|
|
||||||
require $file;
|
|
||||||
});
|
|
||||||
|
|
||||||
ksort($hashes);
|
|
||||||
$cert = file_get_contents($cert_dir.'/snappymail.crt');
|
|
||||||
$rsa = new \phpseclib\Crypt\RSA();
|
|
||||||
$rsa->loadKey(file_get_contents($cert_dir.'/snappymail.key'));
|
|
||||||
$x509 = new \phpseclib\File\X509();
|
|
||||||
$x509->loadX509($cert);
|
|
||||||
$x509->setPrivateKey($rsa);
|
|
||||||
$rsa->setSignatureMode(\phpseclib\Crypt\RSA::SIGNATURE_PSS);
|
|
||||||
$rsa->setMGFHash('sha512');
|
|
||||||
$rsa->setSaltLength(0);
|
|
||||||
$signature = $rsa->sign(json_encode($hashes));
|
|
||||||
$nc_tar->addFromString('snappymail/appinfo/signature.json', json_encode([
|
|
||||||
'hashes' => $hashes,
|
|
||||||
'signature' => base64_encode($signature),
|
|
||||||
'certificate' => $cert
|
|
||||||
], JSON_PRETTY_PRINT));
|
|
||||||
|
|
||||||
$nc_tar->compress(Phar::GZ);
|
|
||||||
unlink($nc_destination);
|
|
||||||
$nc_destination .= '.gz';
|
|
||||||
|
|
||||||
$signature = shell_exec("openssl dgst -sha512 -sign {$cert_dir}/snappymail.key {$nc_destination} | openssl base64");
|
|
||||||
file_put_contents($nc_destination.'.sig', $signature);
|
|
||||||
|
|
||||||
echo "{$nc_destination} created\n";
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
<?php
|
|
||||||
echo "\x1b[33;1m === ownCloud === \x1b[0m\n";
|
|
||||||
|
|
||||||
$cert_dir = false; // $_SERVER['HOME'].'/.owncloud/certificates';
|
|
||||||
|
|
||||||
$nc_destination = "{$destPath}snappymail-{$package->version}-owncloud.tar";
|
|
||||||
|
|
||||||
@unlink($nc_destination);
|
|
||||||
@unlink("{$nc_destination}.gz");
|
|
||||||
|
|
||||||
$nc_tar = new PharData($nc_destination);
|
|
||||||
$hashes = [];
|
|
||||||
|
|
||||||
file_put_contents(ROOT_DIR . '/integrations/owncloud/snappymail/VERSION', $package->version);
|
|
||||||
$file = ROOT_DIR . '/integrations/owncloud/snappymail/appinfo/info.xml';
|
|
||||||
file_put_contents($file, preg_replace('/<version>[^<]*</', "<version>{$package->version}<", file_get_contents($file)));
|
|
||||||
|
|
||||||
$nc_tar->buildFromDirectory('./integrations/owncloud', "@integrations/owncloud/snappymail/@");
|
|
||||||
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('integrations/owncloud/snappymail'));
|
|
||||||
foreach ($files as $file) {
|
|
||||||
if (is_file($file)) {
|
|
||||||
$name = str_replace('\\', '/', $file);
|
|
||||||
$name = str_replace('integrations/owncloud/snappymail/', '', $name);
|
|
||||||
$hashes[$name] = hash_file('sha512', $file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('snappymail/v'), RecursiveIteratorIterator::SELF_FIRST);
|
|
||||||
foreach ($files as $file) {
|
|
||||||
if (is_file($file)) {
|
|
||||||
$newFile = str_replace('\\', '/', $file);
|
|
||||||
// $newFile = str_replace("'snappymail/v/'.", '', $newFile);
|
|
||||||
$nc_tar->addFile($file, "snappymail/app/{$newFile}");
|
|
||||||
$hashes["app/{$newFile}"] = hash_file('sha512', $file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$nc_tar->delete("snappymail/app/snappymail/v/{$package->version}/app/.htaccess");
|
|
||||||
$nc_tar->addFile("snappymail/v/{$package->version}/app/.htaccess", "snappymail/app/snappymail/v/{$package->version}/app/_htaccess");
|
|
||||||
$nc_tar->delete("snappymail/app/snappymail/v/{$package->version}/static/.htaccess");
|
|
||||||
$nc_tar->addFile("snappymail/v/{$package->version}/static/.htaccess", "snappymail/app/snappymail/v/{$package->version}/static/_htaccess");
|
|
||||||
|
|
||||||
/*
|
|
||||||
$nc_tar->addFile('data/.htaccess');
|
|
||||||
$nc_tar->addFromString('data/VERSION', $package->version);
|
|
||||||
$nc_tar->addFile('data/README.md');
|
|
||||||
$nc_tar->addFile('_include.php', 'snappymail/app/_include.php');
|
|
||||||
*/
|
|
||||||
$nc_tar->addFile('.htaccess', 'snappymail/app/_htaccess');
|
|
||||||
$hashes['app/.htaccess'] = hash_file('sha512', '.htaccess');
|
|
||||||
|
|
||||||
$index = file_get_contents('index.php');
|
|
||||||
$index = str_replace('0.0.0', $package->version, $index);
|
|
||||||
//$index = str_replace('snappymail/v/', '', $index);
|
|
||||||
$nc_tar->addFromString('snappymail/app/index.php', $index);
|
|
||||||
$hashes['app/index.php'] = hash('sha512', $index);
|
|
||||||
|
|
||||||
$nc_tar->addFile('README.md', 'snappymail/app/README.md');
|
|
||||||
$hashes['app/README.md'] = hash_file('sha512', 'README.md');
|
|
||||||
|
|
||||||
$nc_tar->addFile('CHANGELOG.md', 'snappymail/CHANGELOG.md');
|
|
||||||
$hashes['CHANGELOG.md'] = hash_file('sha512', 'CHANGELOG.md');
|
|
||||||
|
|
||||||
$data = file_get_contents('dev/serviceworker.js');
|
|
||||||
$nc_tar->addFromString('snappymail/app/serviceworker.js', $data);
|
|
||||||
$hashes['app/serviceworker.js'] = hash('sha512', $data);
|
|
||||||
|
|
||||||
if ($cert_dir) {
|
|
||||||
spl_autoload_register(function($name){
|
|
||||||
$file = __DIR__ . '/' . str_replace('\\', '/', $name) . '.php';
|
|
||||||
require $file;
|
|
||||||
});
|
|
||||||
|
|
||||||
ksort($hashes);
|
|
||||||
$cert = file_get_contents($cert_dir.'/snappymail.crt');
|
|
||||||
$rsa = new \phpseclib\Crypt\RSA();
|
|
||||||
$rsa->loadKey(file_get_contents($cert_dir.'/snappymail.key'));
|
|
||||||
$x509 = new \phpseclib\File\X509();
|
|
||||||
$x509->loadX509($cert);
|
|
||||||
$x509->setPrivateKey($rsa);
|
|
||||||
$rsa->setSignatureMode(\phpseclib\Crypt\RSA::SIGNATURE_PSS);
|
|
||||||
$rsa->setMGFHash('sha512');
|
|
||||||
$rsa->setSaltLength(0);
|
|
||||||
$signature = $rsa->sign(json_encode($hashes));
|
|
||||||
$nc_tar->addFromString('snappymail/appinfo/signature.json', json_encode([
|
|
||||||
'hashes' => $hashes,
|
|
||||||
'signature' => base64_encode($signature),
|
|
||||||
'certificate' => $cert
|
|
||||||
], JSON_PRETTY_PRINT));
|
|
||||||
}
|
|
||||||
|
|
||||||
$nc_tar->compress(Phar::GZ);
|
|
||||||
unlink($nc_destination);
|
|
||||||
$nc_destination .= '.gz';
|
|
||||||
|
|
||||||
if ($cert_dir) {
|
|
||||||
$signature = shell_exec("openssl dgst -sha512 -sign {$cert_dir}/snappymail.key {$nc_destination} | openssl base64");
|
|
||||||
file_put_contents($nc_destination.'.sig', $signature);
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "{$nc_destination} created\n";
|
|
||||||
|
|
@ -1,893 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pure-PHP implementations of keyed-hash message authentication codes (HMACs) and various cryptographic hashing functions.
|
|
||||||
*
|
|
||||||
* Uses hash() or mhash() if available and an internal implementation, otherwise. Currently supports the following:
|
|
||||||
*
|
|
||||||
* md2, md5, md5-96, sha1, sha1-96, sha256, sha256-96, sha384, and sha512, sha512-96
|
|
||||||
*
|
|
||||||
* If {@link self::setKey() setKey()} is called, {@link self::hash() hash()} will return the HMAC as opposed to
|
|
||||||
* the hash. If no valid algorithm is provided, sha1 will be used.
|
|
||||||
*
|
|
||||||
* PHP version 5
|
|
||||||
*
|
|
||||||
* {@internal The variable names are the same as those in
|
|
||||||
* {@link http://tools.ietf.org/html/rfc2104#section-2 RFC2104}.}}
|
|
||||||
*
|
|
||||||
* Here's a short example of how to use this library:
|
|
||||||
* <code>
|
|
||||||
* <?php
|
|
||||||
* include 'vendor/autoload.php';
|
|
||||||
*
|
|
||||||
* $hash = new \phpseclib\Crypt\Hash('sha1');
|
|
||||||
*
|
|
||||||
* $hash->setKey('abcdefg');
|
|
||||||
*
|
|
||||||
* echo base64_encode($hash->hash('abcdefg'));
|
|
||||||
* ?>
|
|
||||||
* </code>
|
|
||||||
*
|
|
||||||
* @category Crypt
|
|
||||||
* @package Hash
|
|
||||||
* @author Jim Wigginton <terrafrost@php.net>
|
|
||||||
* @copyright 2007 Jim Wigginton
|
|
||||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
|
||||||
* @link http://phpseclib.sourceforge.net
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace phpseclib\Crypt;
|
|
||||||
|
|
||||||
use phpseclib\Math\BigInteger;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pure-PHP implementations of keyed-hash message authentication codes (HMACs) and various cryptographic hashing functions.
|
|
||||||
*
|
|
||||||
* @package Hash
|
|
||||||
* @author Jim Wigginton <terrafrost@php.net>
|
|
||||||
* @access public
|
|
||||||
*/
|
|
||||||
class Hash
|
|
||||||
{
|
|
||||||
/**#@+
|
|
||||||
* @access private
|
|
||||||
* @see \phpseclib\Crypt\Hash::__construct()
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* Toggles the internal implementation
|
|
||||||
*/
|
|
||||||
const MODE_INTERNAL = 1;
|
|
||||||
/**
|
|
||||||
* Toggles the mhash() implementation, which has been deprecated on PHP 5.3.0+.
|
|
||||||
*/
|
|
||||||
const MODE_MHASH = 2;
|
|
||||||
/**
|
|
||||||
* Toggles the hash() implementation, which works on PHP 5.1.2+.
|
|
||||||
*/
|
|
||||||
const MODE_HASH = 3;
|
|
||||||
/**#@-*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hash Parameter
|
|
||||||
*
|
|
||||||
* @see self::setHash()
|
|
||||||
* @var int
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
var $hashParam;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Byte-length of compression blocks / key (Internal HMAC)
|
|
||||||
*
|
|
||||||
* @see self::setAlgorithm()
|
|
||||||
* @var int
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
var $b;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Byte-length of hash output (Internal HMAC)
|
|
||||||
*
|
|
||||||
* @see self::setHash()
|
|
||||||
* @var int
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
var $l = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hash Algorithm
|
|
||||||
*
|
|
||||||
* @see self::setHash()
|
|
||||||
* @var string
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
var $hash;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Key
|
|
||||||
*
|
|
||||||
* @see self::setKey()
|
|
||||||
* @var string
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
var $key = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Computed Key
|
|
||||||
*
|
|
||||||
* @see self::_computeKey()
|
|
||||||
* @var string
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
var $computedKey = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Outer XOR (Internal HMAC)
|
|
||||||
*
|
|
||||||
* @see self::setKey()
|
|
||||||
* @var string
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
var $opad;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inner XOR (Internal HMAC)
|
|
||||||
*
|
|
||||||
* @see self::setKey()
|
|
||||||
* @var string
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
var $ipad;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Engine
|
|
||||||
*
|
|
||||||
* @see self::setHash()
|
|
||||||
* @var string
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
var $engine;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default Constructor.
|
|
||||||
*
|
|
||||||
* @param string $hash
|
|
||||||
* @return \phpseclib\Crypt\Hash
|
|
||||||
* @access public
|
|
||||||
*/
|
|
||||||
function __construct($hash = 'sha1')
|
|
||||||
{
|
|
||||||
if (!defined('CRYPT_HASH_MODE')) {
|
|
||||||
switch (true) {
|
|
||||||
case extension_loaded('hash'):
|
|
||||||
define('CRYPT_HASH_MODE', self::MODE_HASH);
|
|
||||||
break;
|
|
||||||
case extension_loaded('mhash'):
|
|
||||||
define('CRYPT_HASH_MODE', self::MODE_MHASH);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
define('CRYPT_HASH_MODE', self::MODE_INTERNAL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->setHash($hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the key for HMACs
|
|
||||||
*
|
|
||||||
* Keys can be of any length.
|
|
||||||
*
|
|
||||||
* @access public
|
|
||||||
* @param string $key
|
|
||||||
*/
|
|
||||||
function setKey($key = false)
|
|
||||||
{
|
|
||||||
$this->key = $key;
|
|
||||||
$this->_computeKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pre-compute the key used by the HMAC
|
|
||||||
*
|
|
||||||
* Quoting http://tools.ietf.org/html/rfc2104#section-2, "Applications that use keys longer than B bytes
|
|
||||||
* will first hash the key using H and then use the resultant L byte string as the actual key to HMAC."
|
|
||||||
*
|
|
||||||
* As documented in https://www.reddit.com/r/PHP/comments/9nct2l/symfonypolyfill_hash_pbkdf2_correct_fix_for/
|
|
||||||
* when doing an HMAC multiple times it's faster to compute the hash once instead of computing it during
|
|
||||||
* every call
|
|
||||||
*
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
function _computeKey()
|
|
||||||
{
|
|
||||||
if ($this->key === false) {
|
|
||||||
$this->computedKey = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (strlen($this->key) <= $this->b) {
|
|
||||||
$this->computedKey = $this->key;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ($this->engine) {
|
|
||||||
case self::MODE_MHASH:
|
|
||||||
$this->computedKey = mhash($this->hash, $this->key);
|
|
||||||
break;
|
|
||||||
case self::MODE_HASH:
|
|
||||||
$this->computedKey = hash($this->hash, $this->key, true);
|
|
||||||
break;
|
|
||||||
case self::MODE_INTERNAL:
|
|
||||||
$this->computedKey = call_user_func($this->hash, $this->key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the hash function.
|
|
||||||
*
|
|
||||||
* As set by the constructor or by the setHash() method.
|
|
||||||
*
|
|
||||||
* @access public
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
function getHash()
|
|
||||||
{
|
|
||||||
return $this->hashParam;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the hash function.
|
|
||||||
*
|
|
||||||
* @access public
|
|
||||||
* @param string $hash
|
|
||||||
*/
|
|
||||||
function setHash($hash)
|
|
||||||
{
|
|
||||||
$this->hashParam = $hash = strtolower($hash);
|
|
||||||
switch ($hash) {
|
|
||||||
case 'md5-96':
|
|
||||||
case 'sha1-96':
|
|
||||||
case 'sha256-96':
|
|
||||||
case 'sha512-96':
|
|
||||||
$hash = substr($hash, 0, -3);
|
|
||||||
$this->l = 12; // 96 / 8 = 12
|
|
||||||
break;
|
|
||||||
case 'md2':
|
|
||||||
case 'md5':
|
|
||||||
$this->l = 16;
|
|
||||||
break;
|
|
||||||
case 'sha1':
|
|
||||||
$this->l = 20;
|
|
||||||
break;
|
|
||||||
case 'sha256':
|
|
||||||
$this->l = 32;
|
|
||||||
break;
|
|
||||||
case 'sha384':
|
|
||||||
$this->l = 48;
|
|
||||||
break;
|
|
||||||
case 'sha512':
|
|
||||||
$this->l = 64;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ($hash) {
|
|
||||||
case 'md2-96':
|
|
||||||
case 'md2':
|
|
||||||
$this->b = 16;
|
|
||||||
case 'md5-96':
|
|
||||||
case 'sha1-96':
|
|
||||||
case 'sha224-96':
|
|
||||||
case 'sha256-96':
|
|
||||||
case 'md2':
|
|
||||||
case 'md5':
|
|
||||||
case 'sha1':
|
|
||||||
case 'sha224':
|
|
||||||
case 'sha256':
|
|
||||||
$this->b = 64;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$this->b = 128;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ($hash) {
|
|
||||||
case 'md2':
|
|
||||||
$this->engine = CRYPT_HASH_MODE == self::MODE_HASH && in_array('md2', hash_algos()) ?
|
|
||||||
self::MODE_HASH : self::MODE_INTERNAL;
|
|
||||||
break;
|
|
||||||
case 'sha384':
|
|
||||||
case 'sha512':
|
|
||||||
$this->engine = CRYPT_HASH_MODE == self::MODE_MHASH ? self::MODE_INTERNAL : CRYPT_HASH_MODE;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$this->engine = CRYPT_HASH_MODE;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ($this->engine) {
|
|
||||||
case self::MODE_MHASH:
|
|
||||||
switch ($hash) {
|
|
||||||
case 'md5':
|
|
||||||
$this->hash = MHASH_MD5;
|
|
||||||
break;
|
|
||||||
case 'sha256':
|
|
||||||
$this->hash = MHASH_SHA256;
|
|
||||||
break;
|
|
||||||
case 'sha1':
|
|
||||||
default:
|
|
||||||
$this->hash = MHASH_SHA1;
|
|
||||||
}
|
|
||||||
$this->_computeKey(self::MODE_MHASH);
|
|
||||||
return;
|
|
||||||
case self::MODE_HASH:
|
|
||||||
switch ($hash) {
|
|
||||||
case 'md5':
|
|
||||||
$this->hash = 'md5';
|
|
||||||
return;
|
|
||||||
case 'md2':
|
|
||||||
case 'sha256':
|
|
||||||
case 'sha384':
|
|
||||||
case 'sha512':
|
|
||||||
$this->hash = $hash;
|
|
||||||
return;
|
|
||||||
case 'sha1':
|
|
||||||
default:
|
|
||||||
$this->hash = 'sha1';
|
|
||||||
}
|
|
||||||
$this->_computeKey(self::MODE_HASH);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ($hash) {
|
|
||||||
case 'md2':
|
|
||||||
$this->hash = array($this, '_md2');
|
|
||||||
break;
|
|
||||||
case 'md5':
|
|
||||||
$this->hash = array($this, '_md5');
|
|
||||||
break;
|
|
||||||
case 'sha256':
|
|
||||||
$this->hash = array($this, '_sha256');
|
|
||||||
break;
|
|
||||||
case 'sha384':
|
|
||||||
case 'sha512':
|
|
||||||
$this->hash = array($this, '_sha512');
|
|
||||||
break;
|
|
||||||
case 'sha1':
|
|
||||||
default:
|
|
||||||
$this->hash = array($this, '_sha1');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->ipad = str_repeat(chr(0x36), $this->b);
|
|
||||||
$this->opad = str_repeat(chr(0x5C), $this->b);
|
|
||||||
|
|
||||||
$this->_computeKey(self::MODE_INTERNAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compute the HMAC.
|
|
||||||
*
|
|
||||||
* @access public
|
|
||||||
* @param string $text
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
function hash($text)
|
|
||||||
{
|
|
||||||
if (!empty($this->key) || is_string($this->key)) {
|
|
||||||
switch ($this->engine) {
|
|
||||||
case self::MODE_MHASH:
|
|
||||||
$output = mhash($this->hash, $text, $this->computedKey);
|
|
||||||
break;
|
|
||||||
case self::MODE_HASH:
|
|
||||||
$output = hash_hmac($this->hash, $text, $this->computedKey, true);
|
|
||||||
break;
|
|
||||||
case self::MODE_INTERNAL:
|
|
||||||
$key = str_pad($this->computedKey, $this->b, chr(0)); // step 1
|
|
||||||
$temp = $this->ipad ^ $key; // step 2
|
|
||||||
$temp .= $text; // step 3
|
|
||||||
$temp = call_user_func($this->hash, $temp); // step 4
|
|
||||||
$output = $this->opad ^ $key; // step 5
|
|
||||||
$output.= $temp; // step 6
|
|
||||||
$output = call_user_func($this->hash, $output); // step 7
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch ($this->engine) {
|
|
||||||
case self::MODE_MHASH:
|
|
||||||
$output = mhash($this->hash, $text);
|
|
||||||
break;
|
|
||||||
case self::MODE_HASH:
|
|
||||||
$output = hash($this->hash, $text, true);
|
|
||||||
break;
|
|
||||||
case self::MODE_INTERNAL:
|
|
||||||
$output = call_user_func($this->hash, $text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return substr($output, 0, $this->l);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the hash length (in bytes)
|
|
||||||
*
|
|
||||||
* @access public
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
function getLength()
|
|
||||||
{
|
|
||||||
return $this->l;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper for MD5
|
|
||||||
*
|
|
||||||
* @access private
|
|
||||||
* @param string $m
|
|
||||||
*/
|
|
||||||
function _md5($m)
|
|
||||||
{
|
|
||||||
return pack('H*', md5($m));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper for SHA1
|
|
||||||
*
|
|
||||||
* @access private
|
|
||||||
* @param string $m
|
|
||||||
*/
|
|
||||||
function _sha1($m)
|
|
||||||
{
|
|
||||||
return pack('H*', sha1($m));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pure-PHP implementation of MD2
|
|
||||||
*
|
|
||||||
* See {@link http://tools.ietf.org/html/rfc1319 RFC1319}.
|
|
||||||
*
|
|
||||||
* @access private
|
|
||||||
* @param string $m
|
|
||||||
*/
|
|
||||||
function _md2($m)
|
|
||||||
{
|
|
||||||
static $s = array(
|
|
||||||
41, 46, 67, 201, 162, 216, 124, 1, 61, 54, 84, 161, 236, 240, 6,
|
|
||||||
19, 98, 167, 5, 243, 192, 199, 115, 140, 152, 147, 43, 217, 188,
|
|
||||||
76, 130, 202, 30, 155, 87, 60, 253, 212, 224, 22, 103, 66, 111, 24,
|
|
||||||
138, 23, 229, 18, 190, 78, 196, 214, 218, 158, 222, 73, 160, 251,
|
|
||||||
245, 142, 187, 47, 238, 122, 169, 104, 121, 145, 21, 178, 7, 63,
|
|
||||||
148, 194, 16, 137, 11, 34, 95, 33, 128, 127, 93, 154, 90, 144, 50,
|
|
||||||
39, 53, 62, 204, 231, 191, 247, 151, 3, 255, 25, 48, 179, 72, 165,
|
|
||||||
181, 209, 215, 94, 146, 42, 172, 86, 170, 198, 79, 184, 56, 210,
|
|
||||||
150, 164, 125, 182, 118, 252, 107, 226, 156, 116, 4, 241, 69, 157,
|
|
||||||
112, 89, 100, 113, 135, 32, 134, 91, 207, 101, 230, 45, 168, 2, 27,
|
|
||||||
96, 37, 173, 174, 176, 185, 246, 28, 70, 97, 105, 52, 64, 126, 15,
|
|
||||||
85, 71, 163, 35, 221, 81, 175, 58, 195, 92, 249, 206, 186, 197,
|
|
||||||
234, 38, 44, 83, 13, 110, 133, 40, 132, 9, 211, 223, 205, 244, 65,
|
|
||||||
129, 77, 82, 106, 220, 55, 200, 108, 193, 171, 250, 36, 225, 123,
|
|
||||||
8, 12, 189, 177, 74, 120, 136, 149, 139, 227, 99, 232, 109, 233,
|
|
||||||
203, 213, 254, 59, 0, 29, 57, 242, 239, 183, 14, 102, 88, 208, 228,
|
|
||||||
166, 119, 114, 248, 235, 117, 75, 10, 49, 68, 80, 180, 143, 237,
|
|
||||||
31, 26, 219, 153, 141, 51, 159, 17, 131, 20
|
|
||||||
);
|
|
||||||
|
|
||||||
// Step 1. Append Padding Bytes
|
|
||||||
$pad = 16 - (strlen($m) & 0xF);
|
|
||||||
$m.= str_repeat(chr($pad), $pad);
|
|
||||||
|
|
||||||
$length = strlen($m);
|
|
||||||
|
|
||||||
// Step 2. Append Checksum
|
|
||||||
$c = str_repeat(chr(0), 16);
|
|
||||||
$l = chr(0);
|
|
||||||
for ($i = 0; $i < $length; $i+= 16) {
|
|
||||||
for ($j = 0; $j < 16; $j++) {
|
|
||||||
// RFC1319 incorrectly states that C[j] should be set to S[c xor L]
|
|
||||||
//$c[$j] = chr($s[ord($m[$i + $j] ^ $l)]);
|
|
||||||
// per <http://www.rfc-editor.org/errata_search.php?rfc=1319>, however, C[j] should be set to S[c xor L] xor C[j]
|
|
||||||
$c[$j] = chr($s[ord($m[$i + $j] ^ $l)] ^ ord($c[$j]));
|
|
||||||
$l = $c[$j];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$m.= $c;
|
|
||||||
|
|
||||||
$length+= 16;
|
|
||||||
|
|
||||||
// Step 3. Initialize MD Buffer
|
|
||||||
$x = str_repeat(chr(0), 48);
|
|
||||||
|
|
||||||
// Step 4. Process Message in 16-Byte Blocks
|
|
||||||
for ($i = 0; $i < $length; $i+= 16) {
|
|
||||||
for ($j = 0; $j < 16; $j++) {
|
|
||||||
$x[$j + 16] = $m[$i + $j];
|
|
||||||
$x[$j + 32] = $x[$j + 16] ^ $x[$j];
|
|
||||||
}
|
|
||||||
$t = chr(0);
|
|
||||||
for ($j = 0; $j < 18; $j++) {
|
|
||||||
for ($k = 0; $k < 48; $k++) {
|
|
||||||
$x[$k] = $t = $x[$k] ^ chr($s[ord($t)]);
|
|
||||||
//$t = $x[$k] = $x[$k] ^ chr($s[ord($t)]);
|
|
||||||
}
|
|
||||||
$t = chr(ord($t) + $j);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 5. Output
|
|
||||||
return substr($x, 0, 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pure-PHP implementation of SHA256
|
|
||||||
*
|
|
||||||
* See {@link http://en.wikipedia.org/wiki/SHA_hash_functions#SHA-256_.28a_SHA-2_variant.29_pseudocode SHA-256 (a SHA-2 variant) pseudocode - Wikipedia}.
|
|
||||||
*
|
|
||||||
* @access private
|
|
||||||
* @param string $m
|
|
||||||
*/
|
|
||||||
function _sha256($m)
|
|
||||||
{
|
|
||||||
if (extension_loaded('suhosin')) {
|
|
||||||
return pack('H*', sha256($m));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize variables
|
|
||||||
$hash = array(
|
|
||||||
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
|
|
||||||
);
|
|
||||||
// Initialize table of round constants
|
|
||||||
// (first 32 bits of the fractional parts of the cube roots of the first 64 primes 2..311)
|
|
||||||
static $k = array(
|
|
||||||
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
||||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
||||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
||||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
||||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
||||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
||||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
||||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
|
|
||||||
);
|
|
||||||
|
|
||||||
// Pre-processing
|
|
||||||
$length = strlen($m);
|
|
||||||
// to round to nearest 56 mod 64, we'll add 64 - (length + (64 - 56)) % 64
|
|
||||||
$m.= str_repeat(chr(0), 64 - (($length + 8) & 0x3F));
|
|
||||||
$m[$length] = chr(0x80);
|
|
||||||
// we don't support hashing strings 512MB long
|
|
||||||
$m.= pack('N2', 0, $length << 3);
|
|
||||||
|
|
||||||
// Process the message in successive 512-bit chunks
|
|
||||||
$chunks = str_split($m, 64);
|
|
||||||
foreach ($chunks as $chunk) {
|
|
||||||
$w = array();
|
|
||||||
for ($i = 0; $i < 16; $i++) {
|
|
||||||
extract(unpack('Ntemp', $this->_string_shift($chunk, 4)));
|
|
||||||
$w[] = $temp;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extend the sixteen 32-bit words into sixty-four 32-bit words
|
|
||||||
for ($i = 16; $i < 64; $i++) {
|
|
||||||
// @codingStandardsIgnoreStart
|
|
||||||
$s0 = $this->_rightRotate($w[$i - 15], 7) ^
|
|
||||||
$this->_rightRotate($w[$i - 15], 18) ^
|
|
||||||
$this->_rightShift( $w[$i - 15], 3);
|
|
||||||
$s1 = $this->_rightRotate($w[$i - 2], 17) ^
|
|
||||||
$this->_rightRotate($w[$i - 2], 19) ^
|
|
||||||
$this->_rightShift( $w[$i - 2], 10);
|
|
||||||
// @codingStandardsIgnoreEnd
|
|
||||||
$w[$i] = $this->_add($w[$i - 16], $s0, $w[$i - 7], $s1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize hash value for this chunk
|
|
||||||
list($a, $b, $c, $d, $e, $f, $g, $h) = $hash;
|
|
||||||
|
|
||||||
// Main loop
|
|
||||||
for ($i = 0; $i < 64; $i++) {
|
|
||||||
$s0 = $this->_rightRotate($a, 2) ^
|
|
||||||
$this->_rightRotate($a, 13) ^
|
|
||||||
$this->_rightRotate($a, 22);
|
|
||||||
$maj = ($a & $b) ^
|
|
||||||
($a & $c) ^
|
|
||||||
($b & $c);
|
|
||||||
$t2 = $this->_add($s0, $maj);
|
|
||||||
|
|
||||||
$s1 = $this->_rightRotate($e, 6) ^
|
|
||||||
$this->_rightRotate($e, 11) ^
|
|
||||||
$this->_rightRotate($e, 25);
|
|
||||||
$ch = ($e & $f) ^
|
|
||||||
($this->_not($e) & $g);
|
|
||||||
$t1 = $this->_add($h, $s1, $ch, $k[$i], $w[$i]);
|
|
||||||
|
|
||||||
$h = $g;
|
|
||||||
$g = $f;
|
|
||||||
$f = $e;
|
|
||||||
$e = $this->_add($d, $t1);
|
|
||||||
$d = $c;
|
|
||||||
$c = $b;
|
|
||||||
$b = $a;
|
|
||||||
$a = $this->_add($t1, $t2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add this chunk's hash to result so far
|
|
||||||
$hash = array(
|
|
||||||
$this->_add($hash[0], $a),
|
|
||||||
$this->_add($hash[1], $b),
|
|
||||||
$this->_add($hash[2], $c),
|
|
||||||
$this->_add($hash[3], $d),
|
|
||||||
$this->_add($hash[4], $e),
|
|
||||||
$this->_add($hash[5], $f),
|
|
||||||
$this->_add($hash[6], $g),
|
|
||||||
$this->_add($hash[7], $h)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Produce the final hash value (big-endian)
|
|
||||||
return pack('N8', $hash[0], $hash[1], $hash[2], $hash[3], $hash[4], $hash[5], $hash[6], $hash[7]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pure-PHP implementation of SHA384 and SHA512
|
|
||||||
*
|
|
||||||
* @access private
|
|
||||||
* @param string $m
|
|
||||||
*/
|
|
||||||
function _sha512($m)
|
|
||||||
{
|
|
||||||
static $init384, $init512, $k;
|
|
||||||
|
|
||||||
if (!isset($k)) {
|
|
||||||
// Initialize variables
|
|
||||||
$init384 = array( // initial values for SHA384
|
|
||||||
'cbbb9d5dc1059ed8', '629a292a367cd507', '9159015a3070dd17', '152fecd8f70e5939',
|
|
||||||
'67332667ffc00b31', '8eb44a8768581511', 'db0c2e0d64f98fa7', '47b5481dbefa4fa4'
|
|
||||||
);
|
|
||||||
$init512 = array( // initial values for SHA512
|
|
||||||
'6a09e667f3bcc908', 'bb67ae8584caa73b', '3c6ef372fe94f82b', 'a54ff53a5f1d36f1',
|
|
||||||
'510e527fade682d1', '9b05688c2b3e6c1f', '1f83d9abfb41bd6b', '5be0cd19137e2179'
|
|
||||||
);
|
|
||||||
|
|
||||||
for ($i = 0; $i < 8; $i++) {
|
|
||||||
$init384[$i] = new BigInteger($init384[$i], 16);
|
|
||||||
$init384[$i]->setPrecision(64);
|
|
||||||
$init512[$i] = new BigInteger($init512[$i], 16);
|
|
||||||
$init512[$i]->setPrecision(64);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize table of round constants
|
|
||||||
// (first 64 bits of the fractional parts of the cube roots of the first 80 primes 2..409)
|
|
||||||
$k = array(
|
|
||||||
'428a2f98d728ae22', '7137449123ef65cd', 'b5c0fbcfec4d3b2f', 'e9b5dba58189dbbc',
|
|
||||||
'3956c25bf348b538', '59f111f1b605d019', '923f82a4af194f9b', 'ab1c5ed5da6d8118',
|
|
||||||
'd807aa98a3030242', '12835b0145706fbe', '243185be4ee4b28c', '550c7dc3d5ffb4e2',
|
|
||||||
'72be5d74f27b896f', '80deb1fe3b1696b1', '9bdc06a725c71235', 'c19bf174cf692694',
|
|
||||||
'e49b69c19ef14ad2', 'efbe4786384f25e3', '0fc19dc68b8cd5b5', '240ca1cc77ac9c65',
|
|
||||||
'2de92c6f592b0275', '4a7484aa6ea6e483', '5cb0a9dcbd41fbd4', '76f988da831153b5',
|
|
||||||
'983e5152ee66dfab', 'a831c66d2db43210', 'b00327c898fb213f', 'bf597fc7beef0ee4',
|
|
||||||
'c6e00bf33da88fc2', 'd5a79147930aa725', '06ca6351e003826f', '142929670a0e6e70',
|
|
||||||
'27b70a8546d22ffc', '2e1b21385c26c926', '4d2c6dfc5ac42aed', '53380d139d95b3df',
|
|
||||||
'650a73548baf63de', '766a0abb3c77b2a8', '81c2c92e47edaee6', '92722c851482353b',
|
|
||||||
'a2bfe8a14cf10364', 'a81a664bbc423001', 'c24b8b70d0f89791', 'c76c51a30654be30',
|
|
||||||
'd192e819d6ef5218', 'd69906245565a910', 'f40e35855771202a', '106aa07032bbd1b8',
|
|
||||||
'19a4c116b8d2d0c8', '1e376c085141ab53', '2748774cdf8eeb99', '34b0bcb5e19b48a8',
|
|
||||||
'391c0cb3c5c95a63', '4ed8aa4ae3418acb', '5b9cca4f7763e373', '682e6ff3d6b2b8a3',
|
|
||||||
'748f82ee5defb2fc', '78a5636f43172f60', '84c87814a1f0ab72', '8cc702081a6439ec',
|
|
||||||
'90befffa23631e28', 'a4506cebde82bde9', 'bef9a3f7b2c67915', 'c67178f2e372532b',
|
|
||||||
'ca273eceea26619c', 'd186b8c721c0c207', 'eada7dd6cde0eb1e', 'f57d4f7fee6ed178',
|
|
||||||
'06f067aa72176fba', '0a637dc5a2c898a6', '113f9804bef90dae', '1b710b35131c471b',
|
|
||||||
'28db77f523047d84', '32caab7b40c72493', '3c9ebe0a15c9bebc', '431d67c49c100d4c',
|
|
||||||
'4cc5d4becb3e42b6', '597f299cfc657e2a', '5fcb6fab3ad6faec', '6c44198c4a475817'
|
|
||||||
);
|
|
||||||
|
|
||||||
for ($i = 0; $i < 80; $i++) {
|
|
||||||
$k[$i] = new BigInteger($k[$i], 16);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$hash = $this->l == 48 ? $init384 : $init512;
|
|
||||||
|
|
||||||
// Pre-processing
|
|
||||||
$length = strlen($m);
|
|
||||||
// to round to nearest 112 mod 128, we'll add 128 - (length + (128 - 112)) % 128
|
|
||||||
$m.= str_repeat(chr(0), 128 - (($length + 16) & 0x7F));
|
|
||||||
$m[$length] = chr(0x80);
|
|
||||||
// we don't support hashing strings 512MB long
|
|
||||||
$m.= pack('N4', 0, 0, 0, $length << 3);
|
|
||||||
|
|
||||||
// Process the message in successive 1024-bit chunks
|
|
||||||
$chunks = str_split($m, 128);
|
|
||||||
foreach ($chunks as $chunk) {
|
|
||||||
$w = array();
|
|
||||||
for ($i = 0; $i < 16; $i++) {
|
|
||||||
$temp = new BigInteger($this->_string_shift($chunk, 8), 256);
|
|
||||||
$temp->setPrecision(64);
|
|
||||||
$w[] = $temp;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extend the sixteen 32-bit words into eighty 32-bit words
|
|
||||||
for ($i = 16; $i < 80; $i++) {
|
|
||||||
$temp = array(
|
|
||||||
$w[$i - 15]->bitwise_rightRotate(1),
|
|
||||||
$w[$i - 15]->bitwise_rightRotate(8),
|
|
||||||
$w[$i - 15]->bitwise_rightShift(7)
|
|
||||||
);
|
|
||||||
$s0 = $temp[0]->bitwise_xor($temp[1]);
|
|
||||||
$s0 = $s0->bitwise_xor($temp[2]);
|
|
||||||
$temp = array(
|
|
||||||
$w[$i - 2]->bitwise_rightRotate(19),
|
|
||||||
$w[$i - 2]->bitwise_rightRotate(61),
|
|
||||||
$w[$i - 2]->bitwise_rightShift(6)
|
|
||||||
);
|
|
||||||
$s1 = $temp[0]->bitwise_xor($temp[1]);
|
|
||||||
$s1 = $s1->bitwise_xor($temp[2]);
|
|
||||||
$w[$i] = $w[$i - 16]->copy();
|
|
||||||
$w[$i] = $w[$i]->add($s0);
|
|
||||||
$w[$i] = $w[$i]->add($w[$i - 7]);
|
|
||||||
$w[$i] = $w[$i]->add($s1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize hash value for this chunk
|
|
||||||
$a = $hash[0]->copy();
|
|
||||||
$b = $hash[1]->copy();
|
|
||||||
$c = $hash[2]->copy();
|
|
||||||
$d = $hash[3]->copy();
|
|
||||||
$e = $hash[4]->copy();
|
|
||||||
$f = $hash[5]->copy();
|
|
||||||
$g = $hash[6]->copy();
|
|
||||||
$h = $hash[7]->copy();
|
|
||||||
|
|
||||||
// Main loop
|
|
||||||
for ($i = 0; $i < 80; $i++) {
|
|
||||||
$temp = array(
|
|
||||||
$a->bitwise_rightRotate(28),
|
|
||||||
$a->bitwise_rightRotate(34),
|
|
||||||
$a->bitwise_rightRotate(39)
|
|
||||||
);
|
|
||||||
$s0 = $temp[0]->bitwise_xor($temp[1]);
|
|
||||||
$s0 = $s0->bitwise_xor($temp[2]);
|
|
||||||
$temp = array(
|
|
||||||
$a->bitwise_and($b),
|
|
||||||
$a->bitwise_and($c),
|
|
||||||
$b->bitwise_and($c)
|
|
||||||
);
|
|
||||||
$maj = $temp[0]->bitwise_xor($temp[1]);
|
|
||||||
$maj = $maj->bitwise_xor($temp[2]);
|
|
||||||
$t2 = $s0->add($maj);
|
|
||||||
|
|
||||||
$temp = array(
|
|
||||||
$e->bitwise_rightRotate(14),
|
|
||||||
$e->bitwise_rightRotate(18),
|
|
||||||
$e->bitwise_rightRotate(41)
|
|
||||||
);
|
|
||||||
$s1 = $temp[0]->bitwise_xor($temp[1]);
|
|
||||||
$s1 = $s1->bitwise_xor($temp[2]);
|
|
||||||
$temp = array(
|
|
||||||
$e->bitwise_and($f),
|
|
||||||
$g->bitwise_and($e->bitwise_not())
|
|
||||||
);
|
|
||||||
$ch = $temp[0]->bitwise_xor($temp[1]);
|
|
||||||
$t1 = $h->add($s1);
|
|
||||||
$t1 = $t1->add($ch);
|
|
||||||
$t1 = $t1->add($k[$i]);
|
|
||||||
$t1 = $t1->add($w[$i]);
|
|
||||||
|
|
||||||
$h = $g->copy();
|
|
||||||
$g = $f->copy();
|
|
||||||
$f = $e->copy();
|
|
||||||
$e = $d->add($t1);
|
|
||||||
$d = $c->copy();
|
|
||||||
$c = $b->copy();
|
|
||||||
$b = $a->copy();
|
|
||||||
$a = $t1->add($t2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add this chunk's hash to result so far
|
|
||||||
$hash = array(
|
|
||||||
$hash[0]->add($a),
|
|
||||||
$hash[1]->add($b),
|
|
||||||
$hash[2]->add($c),
|
|
||||||
$hash[3]->add($d),
|
|
||||||
$hash[4]->add($e),
|
|
||||||
$hash[5]->add($f),
|
|
||||||
$hash[6]->add($g),
|
|
||||||
$hash[7]->add($h)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Produce the final hash value (big-endian)
|
|
||||||
// (\phpseclib\Crypt\Hash::hash() trims the output for hashes but not for HMACs. as such, we trim the output here)
|
|
||||||
$temp = $hash[0]->toBytes() . $hash[1]->toBytes() . $hash[2]->toBytes() . $hash[3]->toBytes() .
|
|
||||||
$hash[4]->toBytes() . $hash[5]->toBytes();
|
|
||||||
if ($this->l != 48) {
|
|
||||||
$temp.= $hash[6]->toBytes() . $hash[7]->toBytes();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $temp;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Right Rotate
|
|
||||||
*
|
|
||||||
* @access private
|
|
||||||
* @param int $int
|
|
||||||
* @param int $amt
|
|
||||||
* @see self::_sha256()
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
function _rightRotate($int, $amt)
|
|
||||||
{
|
|
||||||
$invamt = 32 - $amt;
|
|
||||||
$mask = (1 << $invamt) - 1;
|
|
||||||
return (($int << $invamt) & 0xFFFFFFFF) | (($int >> $amt) & $mask);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Right Shift
|
|
||||||
*
|
|
||||||
* @access private
|
|
||||||
* @param int $int
|
|
||||||
* @param int $amt
|
|
||||||
* @see self::_sha256()
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
function _rightShift($int, $amt)
|
|
||||||
{
|
|
||||||
$mask = (1 << (32 - $amt)) - 1;
|
|
||||||
return ($int >> $amt) & $mask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Not
|
|
||||||
*
|
|
||||||
* @access private
|
|
||||||
* @param int $int
|
|
||||||
* @see self::_sha256()
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
function _not($int)
|
|
||||||
{
|
|
||||||
return ~$int & 0xFFFFFFFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add
|
|
||||||
*
|
|
||||||
* _sha256() adds multiple unsigned 32-bit integers. Since PHP doesn't support unsigned integers and since the
|
|
||||||
* possibility of overflow exists, care has to be taken. BigInteger could be used but this should be faster.
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
* @see self::_sha256()
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
function _add()
|
|
||||||
{
|
|
||||||
static $mod;
|
|
||||||
if (!isset($mod)) {
|
|
||||||
$mod = pow(2, 32);
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = 0;
|
|
||||||
$arguments = func_get_args();
|
|
||||||
foreach ($arguments as $argument) {
|
|
||||||
$result+= $argument < 0 ? ($argument & 0x7FFFFFFF) + 0x80000000 : $argument;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((php_uname('m') & "\xDF\xDF\xDF") != 'ARM') {
|
|
||||||
return fmod($result, $mod);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (fmod($result, 0x80000000) & 0x7FFFFFFF) |
|
|
||||||
((fmod(floor($result / 0x80000000), 2) & 1) << 31);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* String Shift
|
|
||||||
*
|
|
||||||
* Inspired by array_shift
|
|
||||||
*
|
|
||||||
* @param string $string
|
|
||||||
* @param int $index
|
|
||||||
* @return string
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
function _string_shift(&$string, $index = 1)
|
|
||||||
{
|
|
||||||
$substr = substr($string, 0, $index);
|
|
||||||
$string = substr($string, $index);
|
|
||||||
return $substr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,277 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Random Number Generator
|
|
||||||
*
|
|
||||||
* PHP version 5
|
|
||||||
*
|
|
||||||
* Here's a short example of how to use this library:
|
|
||||||
* <code>
|
|
||||||
* <?php
|
|
||||||
* include 'vendor/autoload.php';
|
|
||||||
*
|
|
||||||
* echo bin2hex(\phpseclib\Crypt\Random::string(8));
|
|
||||||
* ?>
|
|
||||||
* </code>
|
|
||||||
*
|
|
||||||
* @category Crypt
|
|
||||||
* @package Random
|
|
||||||
* @author Jim Wigginton <terrafrost@php.net>
|
|
||||||
* @copyright 2007 Jim Wigginton
|
|
||||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
|
||||||
* @link http://phpseclib.sourceforge.net
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace phpseclib\Crypt;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pure-PHP Random Number Generator
|
|
||||||
*
|
|
||||||
* @package Random
|
|
||||||
* @author Jim Wigginton <terrafrost@php.net>
|
|
||||||
* @access public
|
|
||||||
*/
|
|
||||||
class Random
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Generate a random string.
|
|
||||||
*
|
|
||||||
* Although microoptimizations are generally discouraged as they impair readability this function is ripe with
|
|
||||||
* microoptimizations because this function has the potential of being called a huge number of times.
|
|
||||||
* eg. for RSA key generation.
|
|
||||||
*
|
|
||||||
* @param int $length
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
static function string($length)
|
|
||||||
{
|
|
||||||
if (!$length) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (version_compare(PHP_VERSION, '7.0.0', '>=')) {
|
|
||||||
try {
|
|
||||||
return \random_bytes($length);
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
// If a sufficient source of randomness is unavailable, random_bytes() will throw an
|
|
||||||
// object that implements the Throwable interface (Exception, TypeError, Error).
|
|
||||||
// We don't actually need to do anything here. The string() method should just continue
|
|
||||||
// as normal. Note, however, that if we don't have a sufficient source of randomness for
|
|
||||||
// random_bytes(), most of the other calls here will fail too, so we'll end up using
|
|
||||||
// the PHP implementation.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
|
|
||||||
// method 1. prior to PHP 5.3 this would call rand() on windows hence the function_exists('class_alias') call.
|
|
||||||
// ie. class_alias is a function that was introduced in PHP 5.3
|
|
||||||
if (extension_loaded('mcrypt') && function_exists('class_alias')) {
|
|
||||||
return @mcrypt_create_iv($length);
|
|
||||||
}
|
|
||||||
// method 2. openssl_random_pseudo_bytes was introduced in PHP 5.3.0 but prior to PHP 5.3.4 there was,
|
|
||||||
// to quote <http://php.net/ChangeLog-5.php#5.3.4>, "possible blocking behavior". as of 5.3.4
|
|
||||||
// openssl_random_pseudo_bytes and mcrypt_create_iv do the exact same thing on Windows. ie. they both
|
|
||||||
// call php_win32_get_random_bytes():
|
|
||||||
//
|
|
||||||
// https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/ext/openssl/openssl.c#L5008
|
|
||||||
// https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/ext/mcrypt/mcrypt.c#L1392
|
|
||||||
//
|
|
||||||
// php_win32_get_random_bytes() is defined thusly:
|
|
||||||
//
|
|
||||||
// https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/win32/winutil.c#L80
|
|
||||||
//
|
|
||||||
// we're calling it, all the same, in the off chance that the mcrypt extension is not available
|
|
||||||
if (extension_loaded('openssl') && version_compare(PHP_VERSION, '5.3.4', '>=')) {
|
|
||||||
return openssl_random_pseudo_bytes($length);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// method 1. the fastest
|
|
||||||
if (extension_loaded('openssl')) {
|
|
||||||
return openssl_random_pseudo_bytes($length);
|
|
||||||
}
|
|
||||||
// method 2
|
|
||||||
static $fp = true;
|
|
||||||
if ($fp === true) {
|
|
||||||
// warning's will be output unles the error suppression operator is used. errors such as
|
|
||||||
// "open_basedir restriction in effect", "Permission denied", "No such file or directory", etc.
|
|
||||||
$fp = @fopen('/dev/urandom', 'rb');
|
|
||||||
}
|
|
||||||
if ($fp !== true && $fp !== false) { // surprisingly faster than !is_bool() or is_resource()
|
|
||||||
$temp = fread($fp, $length);
|
|
||||||
if (strlen($temp) == $length) {
|
|
||||||
return $temp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// method 3. pretty much does the same thing as method 2 per the following url:
|
|
||||||
// https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/ext/mcrypt/mcrypt.c#L1391
|
|
||||||
// surprisingly slower than method 2. maybe that's because mcrypt_create_iv does a bunch of error checking that we're
|
|
||||||
// not doing. regardless, this'll only be called if this PHP script couldn't open /dev/urandom due to open_basedir
|
|
||||||
// restrictions or some such
|
|
||||||
if (extension_loaded('mcrypt')) {
|
|
||||||
return @mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// at this point we have no choice but to use a pure-PHP CSPRNG
|
|
||||||
|
|
||||||
// cascade entropy across multiple PHP instances by fixing the session and collecting all
|
|
||||||
// environmental variables, including the previous session data and the current session
|
|
||||||
// data.
|
|
||||||
//
|
|
||||||
// mt_rand seeds itself by looking at the PID and the time, both of which are (relatively)
|
|
||||||
// easy to guess at. linux uses mouse clicks, keyboard timings, etc, as entropy sources, but
|
|
||||||
// PHP isn't low level to be able to use those as sources and on a web server there's not likely
|
|
||||||
// going to be a ton of keyboard or mouse action. web servers do have one thing that we can use
|
|
||||||
// however, a ton of people visiting the website. obviously you don't want to base your seeding
|
|
||||||
// soley on parameters a potential attacker sends but (1) not everything in $_SERVER is controlled
|
|
||||||
// by the user and (2) this isn't just looking at the data sent by the current user - it's based
|
|
||||||
// on the data sent by all users. one user requests the page and a hash of their info is saved.
|
|
||||||
// another user visits the page and the serialization of their data is utilized along with the
|
|
||||||
// server envirnment stuff and a hash of the previous http request data (which itself utilizes
|
|
||||||
// a hash of the session data before that). certainly an attacker should be assumed to have
|
|
||||||
// full control over his own http requests. he, however, is not going to have control over
|
|
||||||
// everyone's http requests.
|
|
||||||
static $crypto = false, $v;
|
|
||||||
if ($crypto === false) {
|
|
||||||
// save old session data
|
|
||||||
$old_session_id = session_id();
|
|
||||||
$old_use_cookies = ini_get('session.use_cookies');
|
|
||||||
$old_session_cache_limiter = session_cache_limiter();
|
|
||||||
$_OLD_SESSION = isset($_SESSION) ? $_SESSION : false;
|
|
||||||
if ($old_session_id != '') {
|
|
||||||
session_write_close();
|
|
||||||
}
|
|
||||||
|
|
||||||
session_id(1);
|
|
||||||
ini_set('session.use_cookies', 0);
|
|
||||||
session_cache_limiter('');
|
|
||||||
session_start();
|
|
||||||
|
|
||||||
$v = $seed = $_SESSION['seed'] = pack('H*', sha1(
|
|
||||||
(isset($_SERVER) ? phpseclib_safe_serialize($_SERVER) : '') .
|
|
||||||
(isset($_POST) ? phpseclib_safe_serialize($_POST) : '') .
|
|
||||||
(isset($_GET) ? phpseclib_safe_serialize($_GET) : '') .
|
|
||||||
(isset($_COOKIE) ? phpseclib_safe_serialize($_COOKIE) : '') .
|
|
||||||
phpseclib_safe_serialize($GLOBALS) .
|
|
||||||
phpseclib_safe_serialize($_SESSION) .
|
|
||||||
phpseclib_safe_serialize($_OLD_SESSION)
|
|
||||||
));
|
|
||||||
if (!isset($_SESSION['count'])) {
|
|
||||||
$_SESSION['count'] = 0;
|
|
||||||
}
|
|
||||||
$_SESSION['count']++;
|
|
||||||
|
|
||||||
session_write_close();
|
|
||||||
|
|
||||||
// restore old session data
|
|
||||||
if ($old_session_id != '') {
|
|
||||||
session_id($old_session_id);
|
|
||||||
session_start();
|
|
||||||
ini_set('session.use_cookies', $old_use_cookies);
|
|
||||||
session_cache_limiter($old_session_cache_limiter);
|
|
||||||
} else {
|
|
||||||
if ($_OLD_SESSION !== false) {
|
|
||||||
$_SESSION = $_OLD_SESSION;
|
|
||||||
unset($_OLD_SESSION);
|
|
||||||
} else {
|
|
||||||
unset($_SESSION);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// in SSH2 a shared secret and an exchange hash are generated through the key exchange process.
|
|
||||||
// the IV client to server is the hash of that "nonce" with the letter A and for the encryption key it's the letter C.
|
|
||||||
// if the hash doesn't produce enough a key or an IV that's long enough concat successive hashes of the
|
|
||||||
// original hash and the current hash. we'll be emulating that. for more info see the following URL:
|
|
||||||
//
|
|
||||||
// http://tools.ietf.org/html/rfc4253#section-7.2
|
|
||||||
//
|
|
||||||
// see the is_string($crypto) part for an example of how to expand the keys
|
|
||||||
$key = pack('H*', sha1($seed . 'A'));
|
|
||||||
$iv = pack('H*', sha1($seed . 'C'));
|
|
||||||
|
|
||||||
// ciphers are used as per the nist.gov link below. also, see this link:
|
|
||||||
//
|
|
||||||
// http://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator#Designs_based_on_cryptographic_primitives
|
|
||||||
switch (true) {
|
|
||||||
case class_exists('\phpseclib\Crypt\AES'):
|
|
||||||
$crypto = new AES(Base::MODE_CTR);
|
|
||||||
break;
|
|
||||||
case class_exists('\phpseclib\Crypt\Twofish'):
|
|
||||||
$crypto = new Twofish(Base::MODE_CTR);
|
|
||||||
break;
|
|
||||||
case class_exists('\phpseclib\Crypt\Blowfish'):
|
|
||||||
$crypto = new Blowfish(Base::MODE_CTR);
|
|
||||||
break;
|
|
||||||
case class_exists('\phpseclib\Crypt\TripleDES'):
|
|
||||||
$crypto = new TripleDES(Base::MODE_CTR);
|
|
||||||
break;
|
|
||||||
case class_exists('\phpseclib\Crypt\DES'):
|
|
||||||
$crypto = new DES(Base::MODE_CTR);
|
|
||||||
break;
|
|
||||||
case class_exists('\phpseclib\Crypt\RC4'):
|
|
||||||
$crypto = new RC4();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
user_error(__CLASS__ . ' requires at least one symmetric cipher be loaded');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$crypto->setKey($key);
|
|
||||||
$crypto->setIV($iv);
|
|
||||||
$crypto->enableContinuousBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
//return $crypto->encrypt(str_repeat("\0", $length));
|
|
||||||
|
|
||||||
// the following is based off of ANSI X9.31:
|
|
||||||
//
|
|
||||||
// http://csrc.nist.gov/groups/STM/cavp/documents/rng/931rngext.pdf
|
|
||||||
//
|
|
||||||
// OpenSSL uses that same standard for it's random numbers:
|
|
||||||
//
|
|
||||||
// http://www.opensource.apple.com/source/OpenSSL/OpenSSL-38/openssl/fips-1.0/rand/fips_rand.c
|
|
||||||
// (do a search for "ANS X9.31 A.2.4")
|
|
||||||
$result = '';
|
|
||||||
while (strlen($result) < $length) {
|
|
||||||
$i = $crypto->encrypt(microtime()); // strlen(microtime()) == 21
|
|
||||||
$r = $crypto->encrypt($i ^ $v); // strlen($v) == 20
|
|
||||||
$v = $crypto->encrypt($r ^ $i); // strlen($r) == 20
|
|
||||||
$result.= $r;
|
|
||||||
}
|
|
||||||
return substr($result, 0, $length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!function_exists('phpseclib_safe_serialize')) {
|
|
||||||
/**
|
|
||||||
* Safely serialize variables
|
|
||||||
*
|
|
||||||
* If a class has a private __sleep() method it'll give a fatal error on PHP 5.2 and earlier.
|
|
||||||
* PHP 5.3 will emit a warning.
|
|
||||||
*
|
|
||||||
* @param mixed $arr
|
|
||||||
* @access public
|
|
||||||
*/
|
|
||||||
function phpseclib_safe_serialize(&$arr)
|
|
||||||
{
|
|
||||||
if (is_object($arr)) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
if (!is_array($arr)) {
|
|
||||||
return serialize($arr);
|
|
||||||
}
|
|
||||||
// prevent circular array recursion
|
|
||||||
if (isset($arr['__phpseclib_marker'])) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
$safearr = array();
|
|
||||||
$arr['__phpseclib_marker'] = true;
|
|
||||||
foreach (array_keys($arr) as $key) {
|
|
||||||
// do not recurse on the '__phpseclib_marker' key itself, for smaller memory usage
|
|
||||||
if ($key !== '__phpseclib_marker') {
|
|
||||||
$safearr[$key] = phpseclib_safe_serialize($arr[$key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
unset($arr['__phpseclib_marker']);
|
|
||||||
return serialize($safearr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,577 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pure-PHP ANSI Decoder
|
|
||||||
*
|
|
||||||
* PHP version 5
|
|
||||||
*
|
|
||||||
* If you call read() in \phpseclib\Net\SSH2 you may get {@link http://en.wikipedia.org/wiki/ANSI_escape_code ANSI escape codes} back.
|
|
||||||
* They'd look like chr(0x1B) . '[00m' or whatever (0x1B = ESC). They tell a
|
|
||||||
* {@link http://en.wikipedia.org/wiki/Terminal_emulator terminal emulator} how to format the characters, what
|
|
||||||
* color to display them in, etc. \phpseclib\File\ANSI is a {@link http://en.wikipedia.org/wiki/VT100 VT100} terminal emulator.
|
|
||||||
*
|
|
||||||
* @category File
|
|
||||||
* @package ANSI
|
|
||||||
* @author Jim Wigginton <terrafrost@php.net>
|
|
||||||
* @copyright 2012 Jim Wigginton
|
|
||||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
|
||||||
* @link http://phpseclib.sourceforge.net
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace phpseclib\File;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pure-PHP ANSI Decoder
|
|
||||||
*
|
|
||||||
* @package ANSI
|
|
||||||
* @author Jim Wigginton <terrafrost@php.net>
|
|
||||||
* @access public
|
|
||||||
*/
|
|
||||||
class ANSI
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Max Width
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
var $max_x;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Max Height
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
var $max_y;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Max History
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
var $max_history;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* History
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
var $history;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* History Attributes
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
var $history_attrs;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current Column
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
var $x;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current Row
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
var $y;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Old Column
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
var $old_x;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Old Row
|
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
var $old_y;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An empty attribute cell
|
|
||||||
*
|
|
||||||
* @var object
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
var $base_attr_cell;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current attribute cell
|
|
||||||
*
|
|
||||||
* @var object
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
var $attr_cell;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An empty attribute row
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
var $attr_row;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current screen text
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
var $screen;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current screen attributes
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
var $attrs;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current ANSI code
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
var $ansi;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tokenization
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
var $tokenization;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default Constructor.
|
|
||||||
*
|
|
||||||
* @return \phpseclib\File\ANSI
|
|
||||||
* @access public
|
|
||||||
*/
|
|
||||||
function __construct()
|
|
||||||
{
|
|
||||||
$attr_cell = new \stdClass();
|
|
||||||
$attr_cell->bold = false;
|
|
||||||
$attr_cell->underline = false;
|
|
||||||
$attr_cell->blink = false;
|
|
||||||
$attr_cell->background = 'black';
|
|
||||||
$attr_cell->foreground = 'white';
|
|
||||||
$attr_cell->reverse = false;
|
|
||||||
$this->base_attr_cell = clone $attr_cell;
|
|
||||||
$this->attr_cell = clone $attr_cell;
|
|
||||||
|
|
||||||
$this->setHistory(200);
|
|
||||||
$this->setDimensions(80, 24);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set terminal width and height
|
|
||||||
*
|
|
||||||
* Resets the screen as well
|
|
||||||
*
|
|
||||||
* @param int $x
|
|
||||||
* @param int $y
|
|
||||||
* @access public
|
|
||||||
*/
|
|
||||||
function setDimensions($x, $y)
|
|
||||||
{
|
|
||||||
$this->max_x = $x - 1;
|
|
||||||
$this->max_y = $y - 1;
|
|
||||||
$this->x = $this->y = 0;
|
|
||||||
$this->history = $this->history_attrs = array();
|
|
||||||
$this->attr_row = array_fill(0, $this->max_x + 2, $this->base_attr_cell);
|
|
||||||
$this->screen = array_fill(0, $this->max_y + 1, '');
|
|
||||||
$this->attrs = array_fill(0, $this->max_y + 1, $this->attr_row);
|
|
||||||
$this->ansi = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the number of lines that should be logged past the terminal height
|
|
||||||
*
|
|
||||||
* @param int $history
|
|
||||||
* @access public
|
|
||||||
*/
|
|
||||||
function setHistory($history)
|
|
||||||
{
|
|
||||||
$this->max_history = $history;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load a string
|
|
||||||
*
|
|
||||||
* @param string $source
|
|
||||||
* @access public
|
|
||||||
*/
|
|
||||||
function loadString($source)
|
|
||||||
{
|
|
||||||
$this->setDimensions($this->max_x + 1, $this->max_y + 1);
|
|
||||||
$this->appendString($source);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Appdend a string
|
|
||||||
*
|
|
||||||
* @param string $source
|
|
||||||
* @access public
|
|
||||||
*/
|
|
||||||
function appendString($source)
|
|
||||||
{
|
|
||||||
$this->tokenization = array('');
|
|
||||||
for ($i = 0; $i < strlen($source); $i++) {
|
|
||||||
if (strlen($this->ansi)) {
|
|
||||||
$this->ansi.= $source[$i];
|
|
||||||
$chr = ord($source[$i]);
|
|
||||||
// http://en.wikipedia.org/wiki/ANSI_escape_code#Sequence_elements
|
|
||||||
// single character CSI's not currently supported
|
|
||||||
switch (true) {
|
|
||||||
case $this->ansi == "\x1B=":
|
|
||||||
$this->ansi = '';
|
|
||||||
continue 2;
|
|
||||||
case strlen($this->ansi) == 2 && $chr >= 64 && $chr <= 95 && $chr != ord('['):
|
|
||||||
case strlen($this->ansi) > 2 && $chr >= 64 && $chr <= 126:
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
continue 2;
|
|
||||||
}
|
|
||||||
$this->tokenization[] = $this->ansi;
|
|
||||||
$this->tokenization[] = '';
|
|
||||||
// http://ascii-table.com/ansi-escape-sequences-vt-100.php
|
|
||||||
switch ($this->ansi) {
|
|
||||||
case "\x1B[H": // Move cursor to upper left corner
|
|
||||||
$this->old_x = $this->x;
|
|
||||||
$this->old_y = $this->y;
|
|
||||||
$this->x = $this->y = 0;
|
|
||||||
break;
|
|
||||||
case "\x1B[J": // Clear screen from cursor down
|
|
||||||
$this->history = array_merge($this->history, array_slice(array_splice($this->screen, $this->y + 1), 0, $this->old_y));
|
|
||||||
$this->screen = array_merge($this->screen, array_fill($this->y, $this->max_y, ''));
|
|
||||||
|
|
||||||
$this->history_attrs = array_merge($this->history_attrs, array_slice(array_splice($this->attrs, $this->y + 1), 0, $this->old_y));
|
|
||||||
$this->attrs = array_merge($this->attrs, array_fill($this->y, $this->max_y, $this->attr_row));
|
|
||||||
|
|
||||||
if (count($this->history) == $this->max_history) {
|
|
||||||
array_shift($this->history);
|
|
||||||
array_shift($this->history_attrs);
|
|
||||||
}
|
|
||||||
case "\x1B[K": // Clear screen from cursor right
|
|
||||||
$this->screen[$this->y] = substr($this->screen[$this->y], 0, $this->x);
|
|
||||||
|
|
||||||
array_splice($this->attrs[$this->y], $this->x + 1, $this->max_x - $this->x, array_fill($this->x, $this->max_x - ($this->x - 1), $this->base_attr_cell));
|
|
||||||
break;
|
|
||||||
case "\x1B[2K": // Clear entire line
|
|
||||||
$this->screen[$this->y] = str_repeat(' ', $this->x);
|
|
||||||
$this->attrs[$this->y] = $this->attr_row;
|
|
||||||
break;
|
|
||||||
case "\x1B[?1h": // set cursor key to application
|
|
||||||
case "\x1B[?25h": // show the cursor
|
|
||||||
case "\x1B(B": // set united states g0 character set
|
|
||||||
break;
|
|
||||||
case "\x1BE": // Move to next line
|
|
||||||
$this->_newLine();
|
|
||||||
$this->x = 0;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
switch (true) {
|
|
||||||
case preg_match('#\x1B\[(\d+)B#', $this->ansi, $match): // Move cursor down n lines
|
|
||||||
$this->old_y = $this->y;
|
|
||||||
$this->y+= $match[1];
|
|
||||||
break;
|
|
||||||
case preg_match('#\x1B\[(\d+);(\d+)H#', $this->ansi, $match): // Move cursor to screen location v,h
|
|
||||||
$this->old_x = $this->x;
|
|
||||||
$this->old_y = $this->y;
|
|
||||||
$this->x = $match[2] - 1;
|
|
||||||
$this->y = $match[1] - 1;
|
|
||||||
break;
|
|
||||||
case preg_match('#\x1B\[(\d+)C#', $this->ansi, $match): // Move cursor right n lines
|
|
||||||
$this->old_x = $this->x;
|
|
||||||
$this->x+= $match[1];
|
|
||||||
break;
|
|
||||||
case preg_match('#\x1B\[(\d+)D#', $this->ansi, $match): // Move cursor left n lines
|
|
||||||
$this->old_x = $this->x;
|
|
||||||
$this->x-= $match[1];
|
|
||||||
if ($this->x < 0) {
|
|
||||||
$this->x = 0;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case preg_match('#\x1B\[(\d+);(\d+)r#', $this->ansi, $match): // Set top and bottom lines of a window
|
|
||||||
break;
|
|
||||||
case preg_match('#\x1B\[(\d*(?:;\d*)*)m#', $this->ansi, $match): // character attributes
|
|
||||||
$attr_cell = &$this->attr_cell;
|
|
||||||
$mods = explode(';', $match[1]);
|
|
||||||
foreach ($mods as $mod) {
|
|
||||||
switch ($mod) {
|
|
||||||
case '':
|
|
||||||
case '0': // Turn off character attributes
|
|
||||||
$attr_cell = clone $this->base_attr_cell;
|
|
||||||
break;
|
|
||||||
case '1': // Turn bold mode on
|
|
||||||
$attr_cell->bold = true;
|
|
||||||
break;
|
|
||||||
case '4': // Turn underline mode on
|
|
||||||
$attr_cell->underline = true;
|
|
||||||
break;
|
|
||||||
case '5': // Turn blinking mode on
|
|
||||||
$attr_cell->blink = true;
|
|
||||||
break;
|
|
||||||
case '7': // Turn reverse video on
|
|
||||||
$attr_cell->reverse = !$attr_cell->reverse;
|
|
||||||
$temp = $attr_cell->background;
|
|
||||||
$attr_cell->background = $attr_cell->foreground;
|
|
||||||
$attr_cell->foreground = $temp;
|
|
||||||
break;
|
|
||||||
default: // set colors
|
|
||||||
//$front = $attr_cell->reverse ? &$attr_cell->background : &$attr_cell->foreground;
|
|
||||||
$front = &$attr_cell->{ $attr_cell->reverse ? 'background' : 'foreground' };
|
|
||||||
//$back = $attr_cell->reverse ? &$attr_cell->foreground : &$attr_cell->background;
|
|
||||||
$back = &$attr_cell->{ $attr_cell->reverse ? 'foreground' : 'background' };
|
|
||||||
switch ($mod) {
|
|
||||||
// @codingStandardsIgnoreStart
|
|
||||||
case '30': $front = 'black'; break;
|
|
||||||
case '31': $front = 'red'; break;
|
|
||||||
case '32': $front = 'green'; break;
|
|
||||||
case '33': $front = 'yellow'; break;
|
|
||||||
case '34': $front = 'blue'; break;
|
|
||||||
case '35': $front = 'magenta'; break;
|
|
||||||
case '36': $front = 'cyan'; break;
|
|
||||||
case '37': $front = 'white'; break;
|
|
||||||
|
|
||||||
case '40': $back = 'black'; break;
|
|
||||||
case '41': $back = 'red'; break;
|
|
||||||
case '42': $back = 'green'; break;
|
|
||||||
case '43': $back = 'yellow'; break;
|
|
||||||
case '44': $back = 'blue'; break;
|
|
||||||
case '45': $back = 'magenta'; break;
|
|
||||||
case '46': $back = 'cyan'; break;
|
|
||||||
case '47': $back = 'white'; break;
|
|
||||||
// @codingStandardsIgnoreEnd
|
|
||||||
|
|
||||||
default:
|
|
||||||
//user_error('Unsupported attribute: ' . $mod);
|
|
||||||
$this->ansi = '';
|
|
||||||
break 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
//user_error("{$this->ansi} is unsupported\r\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$this->ansi = '';
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->tokenization[count($this->tokenization) - 1].= $source[$i];
|
|
||||||
switch ($source[$i]) {
|
|
||||||
case "\r":
|
|
||||||
$this->x = 0;
|
|
||||||
break;
|
|
||||||
case "\n":
|
|
||||||
$this->_newLine();
|
|
||||||
break;
|
|
||||||
case "\x08": // backspace
|
|
||||||
if ($this->x) {
|
|
||||||
$this->x--;
|
|
||||||
$this->attrs[$this->y][$this->x] = clone $this->base_attr_cell;
|
|
||||||
$this->screen[$this->y] = substr_replace(
|
|
||||||
$this->screen[$this->y],
|
|
||||||
$source[$i],
|
|
||||||
$this->x,
|
|
||||||
1
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "\x0F": // shift
|
|
||||||
break;
|
|
||||||
case "\x1B": // start ANSI escape code
|
|
||||||
$this->tokenization[count($this->tokenization) - 1] = substr($this->tokenization[count($this->tokenization) - 1], 0, -1);
|
|
||||||
//if (!strlen($this->tokenization[count($this->tokenization) - 1])) {
|
|
||||||
// array_pop($this->tokenization);
|
|
||||||
//}
|
|
||||||
$this->ansi.= "\x1B";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$this->attrs[$this->y][$this->x] = clone $this->attr_cell;
|
|
||||||
if ($this->x > strlen($this->screen[$this->y])) {
|
|
||||||
$this->screen[$this->y] = str_repeat(' ', $this->x);
|
|
||||||
}
|
|
||||||
$this->screen[$this->y] = substr_replace(
|
|
||||||
$this->screen[$this->y],
|
|
||||||
$source[$i],
|
|
||||||
$this->x,
|
|
||||||
1
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($this->x > $this->max_x) {
|
|
||||||
$this->x = 0;
|
|
||||||
$this->_newLine();
|
|
||||||
} else {
|
|
||||||
$this->x++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a new line
|
|
||||||
*
|
|
||||||
* Also update the $this->screen and $this->history buffers
|
|
||||||
*
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
function _newLine()
|
|
||||||
{
|
|
||||||
//if ($this->y < $this->max_y) {
|
|
||||||
// $this->y++;
|
|
||||||
//}
|
|
||||||
|
|
||||||
while ($this->y >= $this->max_y) {
|
|
||||||
$this->history = array_merge($this->history, array(array_shift($this->screen)));
|
|
||||||
$this->screen[] = '';
|
|
||||||
|
|
||||||
$this->history_attrs = array_merge($this->history_attrs, array(array_shift($this->attrs)));
|
|
||||||
$this->attrs[] = $this->attr_row;
|
|
||||||
|
|
||||||
if (count($this->history) >= $this->max_history) {
|
|
||||||
array_shift($this->history);
|
|
||||||
array_shift($this->history_attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->y--;
|
|
||||||
}
|
|
||||||
$this->y++;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the current coordinate without preformating
|
|
||||||
*
|
|
||||||
* @access private
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
function _processCoordinate($last_attr, $cur_attr, $char)
|
|
||||||
{
|
|
||||||
$output = '';
|
|
||||||
|
|
||||||
if ($last_attr != $cur_attr) {
|
|
||||||
$close = $open = '';
|
|
||||||
if ($last_attr->foreground != $cur_attr->foreground) {
|
|
||||||
if ($cur_attr->foreground != 'white') {
|
|
||||||
$open.= '<span style="color: ' . $cur_attr->foreground . '">';
|
|
||||||
}
|
|
||||||
if ($last_attr->foreground != 'white') {
|
|
||||||
$close = '</span>' . $close;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($last_attr->background != $cur_attr->background) {
|
|
||||||
if ($cur_attr->background != 'black') {
|
|
||||||
$open.= '<span style="background: ' . $cur_attr->background . '">';
|
|
||||||
}
|
|
||||||
if ($last_attr->background != 'black') {
|
|
||||||
$close = '</span>' . $close;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($last_attr->bold != $cur_attr->bold) {
|
|
||||||
if ($cur_attr->bold) {
|
|
||||||
$open.= '<b>';
|
|
||||||
} else {
|
|
||||||
$close = '</b>' . $close;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($last_attr->underline != $cur_attr->underline) {
|
|
||||||
if ($cur_attr->underline) {
|
|
||||||
$open.= '<u>';
|
|
||||||
} else {
|
|
||||||
$close = '</u>' . $close;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($last_attr->blink != $cur_attr->blink) {
|
|
||||||
if ($cur_attr->blink) {
|
|
||||||
$open.= '<blink>';
|
|
||||||
} else {
|
|
||||||
$close = '</blink>' . $close;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$output.= $close . $open;
|
|
||||||
}
|
|
||||||
|
|
||||||
$output.= htmlspecialchars($char);
|
|
||||||
|
|
||||||
return $output;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the current screen without preformating
|
|
||||||
*
|
|
||||||
* @access private
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
function _getScreen()
|
|
||||||
{
|
|
||||||
$output = '';
|
|
||||||
$last_attr = $this->base_attr_cell;
|
|
||||||
for ($i = 0; $i <= $this->max_y; $i++) {
|
|
||||||
for ($j = 0; $j <= $this->max_x; $j++) {
|
|
||||||
$cur_attr = $this->attrs[$i][$j];
|
|
||||||
$output.= $this->_processCoordinate($last_attr, $cur_attr, isset($this->screen[$i][$j]) ? $this->screen[$i][$j] : '');
|
|
||||||
$last_attr = $this->attrs[$i][$j];
|
|
||||||
}
|
|
||||||
$output.= "\r\n";
|
|
||||||
}
|
|
||||||
$output = substr($output, 0, -2);
|
|
||||||
// close any remaining open tags
|
|
||||||
$output.= $this->_processCoordinate($last_attr, $this->base_attr_cell, '');
|
|
||||||
return rtrim($output);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the current screen
|
|
||||||
*
|
|
||||||
* @access public
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
function getScreen()
|
|
||||||
{
|
|
||||||
return '<pre width="' . ($this->max_x + 1) . '" style="color: white; background: black">' . $this->_getScreen() . '</pre>';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the current screen and the x previous lines
|
|
||||||
*
|
|
||||||
* @access public
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
function getHistory()
|
|
||||||
{
|
|
||||||
$scrollback = '';
|
|
||||||
$last_attr = $this->base_attr_cell;
|
|
||||||
for ($i = 0; $i < count($this->history); $i++) {
|
|
||||||
for ($j = 0; $j <= $this->max_x + 1; $j++) {
|
|
||||||
$cur_attr = $this->history_attrs[$i][$j];
|
|
||||||
$scrollback.= $this->_processCoordinate($last_attr, $cur_attr, isset($this->history[$i][$j]) ? $this->history[$i][$j] : '');
|
|
||||||
$last_attr = $this->history_attrs[$i][$j];
|
|
||||||
}
|
|
||||||
$scrollback.= "\r\n";
|
|
||||||
}
|
|
||||||
$base_attr_cell = $this->base_attr_cell;
|
|
||||||
$this->base_attr_cell = $last_attr;
|
|
||||||
$scrollback.= $this->_getScreen();
|
|
||||||
$this->base_attr_cell = $base_attr_cell;
|
|
||||||
|
|
||||||
return '<pre width="' . ($this->max_x + 1) . '" style="color: white; background: black">' . $scrollback . '</span></pre>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Pure-PHP ASN.1 Parser
|
|
||||||
*
|
|
||||||
* PHP version 5
|
|
||||||
*
|
|
||||||
* @category File
|
|
||||||
* @package ASN1
|
|
||||||
* @author Jim Wigginton <terrafrost@php.net>
|
|
||||||
* @copyright 2012 Jim Wigginton
|
|
||||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
|
||||||
* @link http://phpseclib.sourceforge.net
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace phpseclib\File\ASN1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ASN.1 Element
|
|
||||||
*
|
|
||||||
* Bypass normal encoding rules in phpseclib\File\ASN1::encodeDER()
|
|
||||||
*
|
|
||||||
* @package ASN1
|
|
||||||
* @author Jim Wigginton <terrafrost@php.net>
|
|
||||||
* @access public
|
|
||||||
*/
|
|
||||||
class Element
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Raw element value
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
var $element;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*
|
|
||||||
* @param string $encoded
|
|
||||||
* @return \phpseclib\File\ASN1\Element
|
|
||||||
* @access public
|
|
||||||
*/
|
|
||||||
function __construct($encoded)
|
|
||||||
{
|
|
||||||
$this->element = $encoded;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
# minimalist openssl.cnf file for use with phpseclib
|
|
||||||
|
|
||||||
HOME = .
|
|
||||||
RANDFILE = $ENV::HOME/.rnd
|
|
||||||
|
|
||||||
[ v3_ca ]
|
|
||||||
|
|
@ -1,144 +0,0 @@
|
||||||
<?php
|
|
||||||
define('PLUGINS_DEST_DIR', __DIR__ . '/dist/releases/plugins');
|
|
||||||
|
|
||||||
is_dir(PLUGINS_DEST_DIR) || mkdir(PLUGINS_DEST_DIR, 0777, true);
|
|
||||||
$files = new RecursiveIteratorIterator(
|
|
||||||
new RecursiveDirectoryIterator(PLUGINS_DEST_DIR, RecursiveDirectoryIterator::SKIP_DOTS),
|
|
||||||
RecursiveIteratorIterator::CHILD_FIRST
|
|
||||||
);
|
|
||||||
foreach ($files as $fileinfo) {
|
|
||||||
$fileinfo->isDir() || unlink($fileinfo->getRealPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
$terser = ROOT_DIR . '/node_modules/terser/bin/terser';
|
|
||||||
|
|
||||||
$manifest = [];
|
|
||||||
require ROOT_DIR . '/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/AbstractPlugin.php';
|
|
||||||
$keys = [
|
|
||||||
'author',
|
|
||||||
'category',
|
|
||||||
'description',
|
|
||||||
'file',
|
|
||||||
'id',
|
|
||||||
'license',
|
|
||||||
'name',
|
|
||||||
'release',
|
|
||||||
'required',
|
|
||||||
'type',
|
|
||||||
'url',
|
|
||||||
'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) {
|
|
||||||
if (is_file("{$dir}/index.php") && !strpos($dir, '.bak')) {
|
|
||||||
require "{$dir}/index.php";
|
|
||||||
$name = basename($dir);
|
|
||||||
// if (!in_array($name, $released)) continue;
|
|
||||||
$class = new ReflectionClass(str_replace('-', '', $name) . 'Plugin');
|
|
||||||
$manifest_item = [];
|
|
||||||
foreach ($class->getConstants() as $key => $value) {
|
|
||||||
$key = \strtolower($key);
|
|
||||||
if (in_array($key, $keys)) {
|
|
||||||
$manifest_item[$key] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$version = $manifest_item['version'];
|
|
||||||
if (0 < floatval($version)) {
|
|
||||||
echo "+ {$name} {$version}\n";
|
|
||||||
|
|
||||||
// Minify JavaScript
|
|
||||||
foreach (glob("{$dir}/*.js") as $file) {
|
|
||||||
if (!strpos($file,'.min')) {
|
|
||||||
$mfile = str_replace('.js', '.min.js', $file);
|
|
||||||
passthru("{$terser} {$file} --output {$mfile} --compress 'drop_console' --ecma 6 --mangle");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach (glob("{$dir}/js/*.js") as $file) {
|
|
||||||
if (!strpos($file,'.min')) {
|
|
||||||
$mfile = str_replace('.js', '.min.js', $file);
|
|
||||||
passthru("{$terser} {$file} --output {$mfile} --compress 'drop_console' --ecma 6 --mangle");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$manifest_item['type'] = 'plugin';
|
|
||||||
$manifest_item['id'] = $name;
|
|
||||||
$manifest_item['file'] = "plugins/{$name}-{$version}.tgz";
|
|
||||||
$tar_destination = PLUGINS_DEST_DIR . "/{$name}-{$version}.tar";
|
|
||||||
$tgz_destination = PLUGINS_DEST_DIR . "/{$name}-{$version}.tgz";
|
|
||||||
@unlink($tgz_destination);
|
|
||||||
@unlink("{$tar_destination}.gz");
|
|
||||||
$tar = new PharData($tar_destination);
|
|
||||||
$tar->buildFromDirectory('./plugins/', '/' . \preg_quote("./plugins/{$name}/", '/') . '((?!\.bak).)*$/');
|
|
||||||
$tar->compress(Phar::GZ);
|
|
||||||
unlink($tar_destination);
|
|
||||||
rename("{$tar_destination}.gz", $tgz_destination);
|
|
||||||
/*
|
|
||||||
if (Phar::canWrite()) {
|
|
||||||
$phar_destination = PLUGINS_DEST_DIR . "/{$name}.phar";
|
|
||||||
@unlink($phar_destination);
|
|
||||||
$tar = new Phar($phar_destination);
|
|
||||||
$tar->buildFromDirectory("./plugins/{$name}/", '/^((?!\.bak).)*$/');
|
|
||||||
$tar->compress(Phar::GZ);
|
|
||||||
unlink($phar_destination);
|
|
||||||
rename("{$phar_destination}.gz", $phar_destination);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
if (isset($options['sign'])) {
|
|
||||||
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '.escapeshellarg($tgz_destination), $return_var);
|
|
||||||
$manifest_item['pgp_sig'] = trim(preg_replace('/-----(BEGIN|END) PGP SIGNATURE-----/', '', file_get_contents($tgz_destination.'.asc')));
|
|
||||||
}
|
|
||||||
ksort($manifest_item);
|
|
||||||
$manifest[$name] = $manifest_item;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
echo "- {$name} {$version}\n";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
echo "- {$name}\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ksort($manifest);
|
|
||||||
$manifest = json_encode(array_values($manifest));
|
|
||||||
$manifest = str_replace('{"', "\n\t{\n\t\t\"", $manifest);
|
|
||||||
$manifest = str_replace('"}', "\"\n\t}", $manifest);
|
|
||||||
$manifest = str_replace('}]', "}\n]", $manifest);
|
|
||||||
$manifest = str_replace('","', "\",\n\t\t\"", $manifest);
|
|
||||||
$manifest = str_replace('\/', '/', $manifest);
|
|
||||||
file_put_contents(dirname(PLUGINS_DEST_DIR) . "/packages.json", $manifest);
|
|
||||||
|
|
||||||
exit;
|
|
||||||
280
cli/release.php
|
|
@ -1,280 +0,0 @@
|
||||||
#!/usr/bin/php
|
|
||||||
<?php
|
|
||||||
define('ROOT_DIR', dirname(__DIR__));
|
|
||||||
chdir(ROOT_DIR);
|
|
||||||
|
|
||||||
$options = getopt('', ['aur','docker','plugins','skip-gulp','debian','nextcloud','owncloud','cpanel','sign']);
|
|
||||||
|
|
||||||
if (isset($options['plugins'])) {
|
|
||||||
require(ROOT_DIR . '/build/plugins.php');
|
|
||||||
}
|
|
||||||
|
|
||||||
$gulp = trim(shell_exec('which gulp'));
|
|
||||||
if (!$gulp) {
|
|
||||||
exit('gulp not installed, run as root: npm install --global gulp-cli');
|
|
||||||
}
|
|
||||||
|
|
||||||
$package = json_decode(file_get_contents('package.json'));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update files that contain version
|
|
||||||
*/
|
|
||||||
// cloudron
|
|
||||||
$file = ROOT_DIR . '/integrations/cloudron/Dockerfile';
|
|
||||||
file_put_contents($file, preg_replace('/VERSION=[0-9.]+/', "VERSION={$package->version}", file_get_contents($file)));
|
|
||||||
$file = ROOT_DIR . '/integrations/cloudron/DESCRIPTION.md';
|
|
||||||
file_put_contents($file, preg_replace('/<upstream>[^<]*</', "<upstream>{$package->version}<", file_get_contents($file)));
|
|
||||||
// virtualmin
|
|
||||||
$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)));
|
|
||||||
|
|
||||||
// Arch User Repository
|
|
||||||
// https://aur.archlinux.org/packages/snappymail/
|
|
||||||
$options['aur'] = isset($options['aur']);
|
|
||||||
|
|
||||||
// Docker build
|
|
||||||
$options['docker'] = isset($options['docker']);
|
|
||||||
if ($options['docker'] && !($docker = trim(shell_exec('which docker')))) {
|
|
||||||
exit('docker not found');
|
|
||||||
}
|
|
||||||
if ($options['docker'] && $options['aur']) {
|
|
||||||
exit('Conflict between docker and aur');
|
|
||||||
}
|
|
||||||
|
|
||||||
$destPath = "build/dist/releases/webmail/{$package->version}/";
|
|
||||||
is_dir($destPath) || mkdir($destPath, 0777, true);
|
|
||||||
|
|
||||||
$zip_destination = "{$destPath}snappymail-{$package->version}.zip";
|
|
||||||
$tar_destination = "{$destPath}snappymail-{$package->version}.tar";
|
|
||||||
|
|
||||||
@unlink($zip_destination);
|
|
||||||
@unlink($tar_destination);
|
|
||||||
@unlink("{$tar_destination}.gz");
|
|
||||||
|
|
||||||
if (!isset($options['skip-gulp'])) {
|
|
||||||
echo "\x1b[33;1m === Gulp === \x1b[0m\n";
|
|
||||||
passthru($gulp, $return_var);
|
|
||||||
if ($return_var) {
|
|
||||||
exit("gulp failed with error code {$return_var}\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
$cmddir = escapeshellcmd(ROOT_DIR) . '/snappymail/v/0.0.0/static';
|
|
||||||
|
|
||||||
if ($gzip = trim(shell_exec('which gzip'))) {
|
|
||||||
echo "\x1b[33;1m === Gzip *.js and *.css === \x1b[0m\n";
|
|
||||||
passthru("{$gzip} -k --best {$cmddir}/js/*.js");
|
|
||||||
passthru("{$gzip} -k --best {$cmddir}/js/min/*.js");
|
|
||||||
passthru("{$gzip} -k --best {$cmddir}/css/admin*.css");
|
|
||||||
passthru("{$gzip} -k --best {$cmddir}/css/app*.css");
|
|
||||||
unlink(ROOT_DIR . '/snappymail/v/0.0.0/static/js/boot.js.gz');
|
|
||||||
unlink(ROOT_DIR . '/snappymail/v/0.0.0/static/js/min/boot.min.js.gz');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($brotli = trim(shell_exec('which brotli'))) {
|
|
||||||
echo "\x1b[33;1m === Brotli *.js and *.css === \x1b[0m\n";
|
|
||||||
passthru("{$brotli} -k --best {$cmddir}/js/*.js");
|
|
||||||
passthru("{$brotli} -k --best {$cmddir}/js/min/*.js");
|
|
||||||
passthru("{$brotli} -k --best {$cmddir}/css/admin*.css");
|
|
||||||
passthru("{$brotli} -k --best {$cmddir}/css/app*.css");
|
|
||||||
unlink(ROOT_DIR . '/snappymail/v/0.0.0/static/js/boot.js.br');
|
|
||||||
unlink(ROOT_DIR . '/snappymail/v/0.0.0/static/js/min/boot.min.js.br');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Temporary rename folder to speed up PharData
|
|
||||||
//if (!rename('snappymail/v/0.0.0', "snappymail/v/{$package->version}")){
|
|
||||||
if (!rename('snappymail/v/0.0.0', "snappymail/v/{$package->version}")) {
|
|
||||||
exit('Failed to temporary rename snappymail/v/0.0.0');
|
|
||||||
}
|
|
||||||
register_shutdown_function(function(){
|
|
||||||
// Rename folder back to original
|
|
||||||
@rename("snappymail/v/{$GLOBALS['package']->version}", 'snappymail/v/0.0.0');
|
|
||||||
});
|
|
||||||
|
|
||||||
echo "\x1b[33;1m === Zip/Tar === \x1b[0m\n";
|
|
||||||
|
|
||||||
$zip = new ZipArchive();
|
|
||||||
if (!$zip->open($zip_destination, ZIPARCHIVE::CREATE)) {
|
|
||||||
exit("Failed to create {$zip_destination}");
|
|
||||||
}
|
|
||||||
|
|
||||||
$tar = new PharData($tar_destination);
|
|
||||||
|
|
||||||
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('snappymail/v'), RecursiveIteratorIterator::SELF_FIRST);
|
|
||||||
foreach ($files as $file) {
|
|
||||||
$file = str_replace('\\', '/', $file);
|
|
||||||
//echo "{$file}\n";
|
|
||||||
// Ignore "." and ".." folders
|
|
||||||
if (!in_array(substr($file, strrpos($file, '/')+1), array('.', '..'))) {
|
|
||||||
if (is_dir($file)) {
|
|
||||||
$zip->addEmptyDir($file);
|
|
||||||
} else if (is_file($file)) {
|
|
||||||
$zip->addFile($file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($options['docker']) {
|
|
||||||
$tar->buildFromDirectory('./snappymail/', "@v/{$package->version}@");
|
|
||||||
} else {
|
|
||||||
$tar->buildFromDirectory('./', "@snappymail/v/{$package->version}@");
|
|
||||||
}
|
|
||||||
|
|
||||||
//$zip->addFile('cli/upgrade.sh');
|
|
||||||
//$tar->addFile('cli/upgrade.sh');
|
|
||||||
|
|
||||||
$zip->addFile('data/.htaccess');
|
|
||||||
$tar->addFile('data/.htaccess');
|
|
||||||
|
|
||||||
$zip->addFromString('data/VERSION', $package->version);
|
|
||||||
$tar->addFromString('data/VERSION', $package->version);
|
|
||||||
|
|
||||||
$zip->addFile('data/README.md');
|
|
||||||
$tar->addFile('data/README.md');
|
|
||||||
|
|
||||||
if ($options['aur']) {
|
|
||||||
$data = '<?php
|
|
||||||
function __get_custom_data_full_path()
|
|
||||||
{
|
|
||||||
return \'/var/lib/snappymail\';
|
|
||||||
}
|
|
||||||
';
|
|
||||||
$zip->addFromString('include.php', $data);
|
|
||||||
$tar->addFromString('include.php', $data);
|
|
||||||
} else {
|
|
||||||
$zip->addFile('_include.php');
|
|
||||||
$tar->addFile('_include.php');
|
|
||||||
}
|
|
||||||
|
|
||||||
$zip->addFile('.htaccess');
|
|
||||||
$tar->addFile('.htaccess');
|
|
||||||
|
|
||||||
$index = file_get_contents('index.php');
|
|
||||||
$index = str_replace('0.0.0', $package->version, $index);
|
|
||||||
$zip->addFromString('index.php', $index);
|
|
||||||
$tar->addFromString('index.php', $index);
|
|
||||||
|
|
||||||
$zip->addFile('README.md');
|
|
||||||
$tar->addFile('README.md');
|
|
||||||
|
|
||||||
$zip->close();
|
|
||||||
|
|
||||||
$tar->compress(Phar::GZ);
|
|
||||||
unlink($tar_destination);
|
|
||||||
$tar_destination .= '.gz';
|
|
||||||
|
|
||||||
echo "{$zip_destination} created\n{$tar_destination} created\n";
|
|
||||||
|
|
||||||
if (isset($options['nextcloud'])) {
|
|
||||||
require(ROOT_DIR . '/build/nextcloud.php');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($options['owncloud'])) {
|
|
||||||
require(ROOT_DIR . '/build/owncloud.php');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($options['cpanel'])) {
|
|
||||||
require(ROOT_DIR . '/build/cpanel.php');
|
|
||||||
}
|
|
||||||
|
|
||||||
rename("snappymail/v/{$package->version}", 'snappymail/v/0.0.0');
|
|
||||||
|
|
||||||
file_put_contents("{$destPath}core.json", '{
|
|
||||||
"version": "'.$package->version.'",
|
|
||||||
"file": "../latest.tar.gz",
|
|
||||||
"warnings": []
|
|
||||||
}');
|
|
||||||
|
|
||||||
// Arch User Repository
|
|
||||||
if ($options['aur']) {
|
|
||||||
// extension_loaded('blake2')
|
|
||||||
if (!function_exists('b2sum') && $b2sum = trim(shell_exec('which b2sum'))) {
|
|
||||||
function b2sum($file) {
|
|
||||||
$file = escapeshellarg($file);
|
|
||||||
exec("b2sum --binary {$file} 2>&1", $output, $exitcode);
|
|
||||||
$output = explode(' ', implode("\n", $output));
|
|
||||||
return $output[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$b2sums = function_exists('b2sum') ? [
|
|
||||||
b2sum($tar_destination),
|
|
||||||
b2sum(ROOT_DIR . '/build/arch/snappymail.sysusers'),
|
|
||||||
b2sum(ROOT_DIR . '/build/arch/snappymail.tmpfiles')
|
|
||||||
] : [];
|
|
||||||
|
|
||||||
file_put_contents('build/arch/.SRCINFO', 'pkgbase = snappymail
|
|
||||||
pkgdesc = modern PHP webmail client
|
|
||||||
pkgver = '.$package->version.'
|
|
||||||
pkgrel = 1
|
|
||||||
url = https://github.com/the-djmaze/snappymail
|
|
||||||
arch = any
|
|
||||||
license = AGPL3
|
|
||||||
makedepends = php
|
|
||||||
makedepends = nodejs
|
|
||||||
makedepends = yarn
|
|
||||||
makedepends = gulp
|
|
||||||
depends = php-fpm
|
|
||||||
optdepends = mariadb: storage backend for contacts
|
|
||||||
optdepends = php-pgsql: storage backend for contacts
|
|
||||||
optdepends = php-sqlite: storage backend for contacts
|
|
||||||
source = snappymail-'.$package->version.'.tar.gz::https://github.com/the-djmaze/snappymail/archive/v'.$package->version.'.tar.gz
|
|
||||||
source = snappymail.sysusers
|
|
||||||
source = snappymail.tmpfiles
|
|
||||||
b2sums = '.implode("\n b2sums = ", $b2sums).'
|
|
||||||
|
|
||||||
pkgname = snappymail
|
|
||||||
');
|
|
||||||
|
|
||||||
$file = ROOT_DIR . '/build/arch/PKGBUILD';
|
|
||||||
if (is_file($file)) {
|
|
||||||
$PKGBUILD = file_get_contents($file);
|
|
||||||
$PKGBUILD = preg_replace('/pkgver=[0-9.]+/', "pkgver={$package->version}", $PKGBUILD);
|
|
||||||
$PKGBUILD = preg_replace('/b2sums=\\([^)]+\\)/s', "b2sums=('".implode("'\n '", $b2sums)."')", $PKGBUILD);
|
|
||||||
file_put_contents($file, $PKGBUILD);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Debian Repository
|
|
||||||
else if (isset($options['debian'])) {
|
|
||||||
require(ROOT_DIR . '/build/deb.php');
|
|
||||||
}
|
|
||||||
// Docker build
|
|
||||||
else if ($options['docker']) {
|
|
||||||
echo "\x1b[33;1m === Docker === \x1b[0m\n";
|
|
||||||
$zip_filename = "snappymail-{$package->version}.zip";
|
|
||||||
copy($zip_destination, "./.docker/release/{$zip_filename}");
|
|
||||||
if ($docker) {
|
|
||||||
passthru("{$docker} build --pull " . ROOT_DIR . "/.docker/release/ --build-arg FILES_ZIP={$zip_filename} -t snappymail:{$package->version}");
|
|
||||||
} else {
|
|
||||||
echo "Docker not installed!\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($options['sign'])) {
|
|
||||||
echo "\x1b[33;1m === PGP Sign === \x1b[0m\n";
|
|
||||||
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '.escapeshellarg($tar_destination), $return_var);
|
|
||||||
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '.escapeshellarg($zip_destination), $return_var);
|
|
||||||
if (isset($options['nextcloud'])) {
|
|
||||||
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '
|
|
||||||
.escapeshellarg("{$destPath}snappymail-{$package->version}-nextcloud.tar.gz"), $return_var);
|
|
||||||
}
|
|
||||||
if (isset($options['owncloud'])) {
|
|
||||||
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '
|
|
||||||
.escapeshellarg("{$destPath}snappymail-{$package->version}-owncloud.tar.gz"), $return_var);
|
|
||||||
}
|
|
||||||
if (isset($options['cpanel'])) {
|
|
||||||
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '
|
|
||||||
.escapeshellarg("{$destPath}snappymail-{$package->version}-cpanel.tar.gz"), $return_var);
|
|
||||||
}
|
|
||||||
if (isset($options['debian'])) {
|
|
||||||
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '
|
|
||||||
. escapeshellarg(ROOT_DIR . "/build/dist/releases/webmail/{$package->version}/" . basename(DEB_DEST_DIR.'.deb')), $return_var);
|
|
||||||
// https://github.com/the-djmaze/snappymail/issues/185#issuecomment-1059420588
|
|
||||||
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --digest-algo SHA512 --clearsign --output '
|
|
||||||
. escapeshellarg(ROOT_DIR . "/build/dist/releases/webmail/{$package->version}/InRelease") . ' '
|
|
||||||
. escapeshellarg(ROOT_DIR . "/build/dist/releases/webmail/{$package->version}/Release"), $return_var);
|
|
||||||
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --digest-algo SHA512 -abs --output '
|
|
||||||
. escapeshellarg(ROOT_DIR . "/build/dist/releases/webmail/{$package->version}/Release.gpg") . ' '
|
|
||||||
. escapeshellarg(ROOT_DIR . "/build/dist/releases/webmail/{$package->version}/Release"), $return_var);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# UPGRADING Snappymail
|
|
||||||
#
|
|
||||||
# I've found this to be working fine when upgrading my Snappymail. The
|
|
||||||
# commands should be used at your own risk. I take no responsibility.
|
|
||||||
#
|
|
||||||
# The script can only be run from the SnappyMail install directory!
|
|
||||||
#
|
|
||||||
# By Uwe Bieling <pychi@gmx.de>
|
|
||||||
# and Jordan S (https://github.com/jas8522)
|
|
||||||
|
|
||||||
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
|
||||||
LATEST_URL="https://snappymail.eu/repository/latest.tar.gz"
|
|
||||||
cd "${SCRIPT_DIR}"
|
|
||||||
OWNERGROUP=`stat -c "%U:%G" snappymail`
|
|
||||||
OLD_VERSION=`grep "define('APP_VERSION" index.php | awk -F\' '{print $4}'`
|
|
||||||
|
|
||||||
# check if we are root
|
|
||||||
if [[ $(id -u) -ne 0 ]] ; then echo -e "\033[1;31mPlease run as root\033[0m" ; exit 1 ; fi
|
|
||||||
|
|
||||||
# check if we are in the correct dir
|
|
||||||
if [ ! -d snappymail/v ] ; then echo -e "\033[1;31mThis script can only be run from the SnappyMail install directory\033[0m" ; exit 1 ; fi
|
|
||||||
|
|
||||||
# Download last release to /tmp
|
|
||||||
echo -e "\033[1;33mDownloading last release\033[0m"
|
|
||||||
wget $LATEST_URL -O /tmp/snappymail_latest.tar.gz >/dev/null 2>&1
|
|
||||||
NEW_VERSION=`tar -tf /tmp/snappymail_latest.tar.gz | grep "snappymail/v/.*/index.php" | awk -F/ '{print $3}'`
|
|
||||||
|
|
||||||
if [ $OLD_VERSION != $NEW_VERSION ] ;
|
|
||||||
then
|
|
||||||
# Safety First ... make a backup
|
|
||||||
DSTAMP=`date +%Y-%m-%d`
|
|
||||||
echo -e "\033[1;33mBacking up snappymail $OLD_VERSION to ../backup_snappymail_${DSTAMP}.tar.gz\033[0m"
|
|
||||||
tar -czf ../backup_snappymail_${DSTAMP}.tar.gz .
|
|
||||||
|
|
||||||
# installing
|
|
||||||
echo -e "\033[1;33mInstalling $NEW_VERSION\033[0m";
|
|
||||||
tar -xzf /tmp/snappymail_latest.tar.gz;
|
|
||||||
|
|
||||||
# set permissions
|
|
||||||
echo -e "\033[1;33mSet permissions\033[0m"
|
|
||||||
find . -type d -exec chmod 755 {} \;
|
|
||||||
find . -type f -exec chmod 644 {} \;
|
|
||||||
chmod u+x "${SCRIPT_DIR}/upgrade.sh"
|
|
||||||
chown -R $OWNERGROUP $SCRIPT_DIR
|
|
||||||
echo -e "\033[1;32mFinished with snappymail upgrade from $OLD_VERSION to $NEW_VERSION... \033[0m"
|
|
||||||
|
|
||||||
else
|
|
||||||
echo -e "\033[1;31msame Version skipping.. \033[0m";
|
|
||||||
fi
|
|
||||||
|
|
@ -1,56 +1,134 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
|
||||||
import { logoutLink } from 'Common/Links';
|
import {
|
||||||
import { i18nToNodes, initOnStartOrLangChange } from 'Common/Translator';
|
$htmlCL,
|
||||||
|
leftPanelDisabled,
|
||||||
|
leftPanelType
|
||||||
|
} from 'Common/Globals';
|
||||||
|
|
||||||
import { arePopupsVisible } from 'Knoin/Knoin';
|
import { KeyState } from 'Common/Enums';
|
||||||
|
import { rootAdmin, rootUser } from 'Common/Links';
|
||||||
|
import { initOnStartOrLangChange, initNotificationLanguage } from 'Common/Translator';
|
||||||
|
|
||||||
import { LanguageStore } from 'Stores/Language';
|
import LanguageStore from 'Stores/Language';
|
||||||
import { initThemes } from 'Stores/Theme';
|
import ThemeStore from 'Stores/Theme';
|
||||||
|
|
||||||
import { SelectComponent } from 'Component/Select';
|
const Settings = rl.settings, doc = document;
|
||||||
import { CheckboxComponent } from 'Component/Checkbox';
|
|
||||||
|
|
||||||
export class AbstractApp {
|
class AbstractApp {
|
||||||
/**
|
/**
|
||||||
* @param {RemoteStorage|AdminRemoteStorage} Remote
|
* @param {RemoteStorage|AdminRemoteStorage} Remote
|
||||||
*/
|
*/
|
||||||
constructor(Remote) {
|
constructor() {
|
||||||
this.Remote = Remote;
|
const refresh = (()=>dispatchEvent(new CustomEvent('rl.auto-logout-refresh'))).debounce(5000),
|
||||||
|
fn = (ev=>{
|
||||||
|
$htmlCL.toggle('rl-ctrl-key-pressed', ev.ctrlKey);
|
||||||
|
refresh();
|
||||||
|
}).debounce(500);
|
||||||
|
|
||||||
|
// doc.addEventListener('touchstart', fn, {passive:true});
|
||||||
|
['mousedown','keydown','keyup'/*,'mousemove'*/].forEach(t => doc.addEventListener(t, fn));
|
||||||
|
|
||||||
|
shortcuts.add('escape,enter', '', KeyState.All, () => rl.Dropdowns.detectVisibility());
|
||||||
}
|
}
|
||||||
|
|
||||||
logoutReload(url) {
|
remote() {
|
||||||
arePopupsVisible(false);
|
return null;
|
||||||
url = url || logoutLink();
|
}
|
||||||
if (location.href !== url) {
|
|
||||||
setTimeout(() => location.href = url, 100);
|
data() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} link
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
download(link) {
|
||||||
|
if (rl.settings.app('mobile')) {
|
||||||
|
open(link, '_self');
|
||||||
|
focus();
|
||||||
|
} else {
|
||||||
|
const oLink = doc.createElement('a');
|
||||||
|
oLink.href = link;
|
||||||
|
doc.body.appendChild(oLink).click();
|
||||||
|
oLink.remove();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} token
|
||||||
|
*/
|
||||||
|
setClientSideToken(token) {
|
||||||
|
rl.hash.set();
|
||||||
|
Settings.set('AuthAccountHash', token);
|
||||||
|
}
|
||||||
|
|
||||||
|
logoutReload(close = false) {
|
||||||
|
const logoutLink = rl.adminArea() ? rootAdmin() : rootUser();
|
||||||
|
|
||||||
|
rl.hash.clear();
|
||||||
|
close && window.close && window.close();
|
||||||
|
|
||||||
|
if (location.href !== logoutLink) {
|
||||||
|
setTimeout(() => (Settings.app('inIframe') ? parent : window).location.href = logoutLink, 100);
|
||||||
} else {
|
} else {
|
||||||
rl.route.reload();
|
rl.route.reload();
|
||||||
}
|
}
|
||||||
// this does not work due to ViewModelClass.__builded = true;
|
|
||||||
// rl.settings.set('Auth', false);
|
|
||||||
// rl.app.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstart() {
|
bootstart() {
|
||||||
const register = (name, ClassObject) => ko.components.register(name, {
|
const mobile = Settings.app('mobile');
|
||||||
template: { element: ClassObject.name },
|
|
||||||
viewModel: {
|
|
||||||
createViewModel: (params, componentInfo) => {
|
|
||||||
params = params || {};
|
|
||||||
i18nToNodes(componentInfo.element);
|
|
||||||
return new ClassObject(params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
register('Select', SelectComponent);
|
|
||||||
register('Checkbox', CheckboxComponent);
|
|
||||||
|
|
||||||
initOnStartOrLangChange();
|
ko.components.register('SaveTrigger', require('Component/SaveTrigger').default);
|
||||||
|
ko.components.register('Input', require('Component/Input').default);
|
||||||
|
ko.components.register('Select', require('Component/Select').default);
|
||||||
|
ko.components.register('TextArea', require('Component/TextArea').default);
|
||||||
|
|
||||||
|
if (Settings.app('materialDesign') && !rl.settings.app('mobile')) {
|
||||||
|
ko.components.register('Checkbox', require('Component/MaterialDesign/Checkbox').default);
|
||||||
|
ko.components.register('CheckboxSimple', require('Component/Checkbox').default);
|
||||||
|
} else {
|
||||||
|
ko.components.register('Checkbox', require('Component/Checkbox').default);
|
||||||
|
ko.components.register('CheckboxSimple', require('Component/Checkbox').default);
|
||||||
|
}
|
||||||
|
|
||||||
|
initOnStartOrLangChange(initNotificationLanguage);
|
||||||
|
|
||||||
|
if (!mobile) {
|
||||||
|
// mobile
|
||||||
|
window.addEventListener('resize', () => leftPanelDisabled(767 >= window.innerWidth));
|
||||||
|
} else {
|
||||||
|
$htmlCL.add('rl-mobile');
|
||||||
|
leftPanelDisabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
leftPanelDisabled.subscribe((bValue) => {
|
||||||
|
$htmlCL.toggle('rl-left-panel-disabled', bValue);
|
||||||
|
$htmlCL.toggle('rl-left-panel-enabled', !bValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
leftPanelType.subscribe((sValue) => {
|
||||||
|
$htmlCL.toggle('rl-left-panel-none', 'none' === sValue);
|
||||||
|
$htmlCL.toggle('rl-left-panel-short', 'short' === sValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
leftPanelDisabled.valueHasMutated();
|
||||||
|
|
||||||
LanguageStore.populate();
|
LanguageStore.populate();
|
||||||
initThemes();
|
ThemeStore.populate();
|
||||||
|
|
||||||
this.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
hideLoading() {
|
||||||
|
const id = id => doc.getElementById(id);
|
||||||
|
id('rl-content').hidden = false;
|
||||||
|
id('rl-loading').remove();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { AbstractApp, AbstractApp as default };
|
||||||
|
|
|
||||||
130
dev/App/Admin.js
|
|
@ -1,8 +1,12 @@
|
||||||
import 'External/ko';
|
import ko from 'External/Admin/ko';
|
||||||
|
|
||||||
import { SettingsGet, SettingsAdmin } from 'Common/Globals';
|
import { StorageResultType } from 'Common/Enums';
|
||||||
import { initThemes } from 'Stores/Theme';
|
|
||||||
|
|
||||||
|
import AppStore from 'Stores/Admin/App';
|
||||||
|
import CapaStore from 'Stores/Admin/Capa';
|
||||||
|
import DomainStore from 'Stores/Admin/Domain';
|
||||||
|
import PluginStore from 'Stores/Admin/Plugin';
|
||||||
|
import PackageStore from 'Stores/Admin/Package';
|
||||||
import Remote from 'Remote/Admin/Fetch';
|
import Remote from 'Remote/Admin/Fetch';
|
||||||
|
|
||||||
import { SettingsAdminScreen } from 'Screen/Admin/Settings';
|
import { SettingsAdminScreen } from 'Screen/Admin/Settings';
|
||||||
|
|
@ -11,42 +15,110 @@ 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';
|
class AdminApp extends AbstractApp {
|
||||||
|
|
||||||
export class AdminApp extends AbstractApp {
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(Remote);
|
super(Remote);
|
||||||
this.weakPassword = ko.observable(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh() {
|
remote() {
|
||||||
initThemes();
|
return Remote;
|
||||||
this.start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
reloadDomainList() {
|
||||||
// if (!Settings.app('adminAllowed')) {
|
DomainStore.domains.loading(true);
|
||||||
if (!SettingsAdmin('allowed')) {
|
Remote.domainList((result, data) => {
|
||||||
|
DomainStore.domains.loading(false);
|
||||||
|
if (StorageResultType.Success === result && data && data.Result) {
|
||||||
|
DomainStore.domains(
|
||||||
|
Object.entries(data.Result).map(([name, [enabled, alias]]) => ({
|
||||||
|
name: name,
|
||||||
|
disabled: ko.observable(!enabled),
|
||||||
|
alias: alias,
|
||||||
|
deleteAccess: ko.observable(false)
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadPluginList() {
|
||||||
|
PluginStore.plugins.loading(true);
|
||||||
|
Remote.pluginList((result, data) => {
|
||||||
|
PluginStore.plugins.loading(false);
|
||||||
|
if (StorageResultType.Success === result && data && data.Result) {
|
||||||
|
PluginStore.plugins(
|
||||||
|
data.Result.map(item => ({
|
||||||
|
name: item.Name,
|
||||||
|
disabled: ko.observable(!item.Enabled),
|
||||||
|
configured: ko.observable(!!item.Configured)
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadPackagesList() {
|
||||||
|
PackageStore.packages.loading(true);
|
||||||
|
PackageStore.packagesReal(true);
|
||||||
|
Remote.packagesList((result, data) => {
|
||||||
|
PackageStore.packages.loading(false);
|
||||||
|
if (StorageResultType.Success === result && data && data.Result) {
|
||||||
|
PackageStore.packagesReal(!!data.Result.Real);
|
||||||
|
PackageStore.packagesMainUpdatable(!!data.Result.MainUpdatable);
|
||||||
|
|
||||||
|
let list = [];
|
||||||
|
const loading = {};
|
||||||
|
|
||||||
|
PackageStore.packages().forEach(item => {
|
||||||
|
if (item && item.loading()) {
|
||||||
|
loading[item.file] = item;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Array.isArray(data.Result.List)) {
|
||||||
|
list = data.Result.List.map(item => {
|
||||||
|
if (item) {
|
||||||
|
item.loading = ko.observable(loading[item.file] !== undefined);
|
||||||
|
return 'core' === item.type && !item.canBeInstalled ? null : item;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}).filter(v => v);
|
||||||
|
}
|
||||||
|
|
||||||
|
PackageStore.packages(list);
|
||||||
|
} else {
|
||||||
|
PackageStore.packagesReal(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bootend() {
|
||||||
|
if (window.progressJs) {
|
||||||
|
progressJs.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstart() {
|
||||||
|
super.bootstart();
|
||||||
|
|
||||||
|
AppStore.populate();
|
||||||
|
CapaStore.populate();
|
||||||
|
|
||||||
|
this.hideLoading();
|
||||||
|
|
||||||
|
if (!rl.settings.app('allowAdminPanel')) {
|
||||||
rl.route.root();
|
rl.route.root();
|
||||||
setTimeout(() => location.href = '/', 1);
|
setTimeout(() => location.href = '/', 1);
|
||||||
} else if (SettingsGet('Auth')) {
|
|
||||||
this.weakPassword(SettingsGet('weakPassword'));
|
|
||||||
startScreens([SettingsAdminScreen]);
|
|
||||||
} else {
|
} else {
|
||||||
startScreens([LoginAdminScreen]);
|
if (rl.settings.get('Auth')) {
|
||||||
|
startScreens([SettingsAdminScreen]);
|
||||||
|
} else {
|
||||||
|
startScreens([LoginAdminScreen]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.bootend();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AskPopupView.credentials = function(sAskDesc, btnText) {
|
export default new AdminApp();
|
||||||
return new Promise(resolve => {
|
|
||||||
this.showModal([
|
|
||||||
sAskDesc,
|
|
||||||
view => resolve({username:view.username(), password:view.passphrase()}),
|
|
||||||
() => resolve(null),
|
|
||||||
true,
|
|
||||||
3,
|
|
||||||
btnText
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
|
||||||
1166
dev/App/User.js
|
|
@ -1,10 +1,8 @@
|
||||||
import * as Links from 'Common/Links';
|
import * as Links from 'Common/Links';
|
||||||
import { doc, SettingsGet, fireEvent, addEventsListener } from 'Common/Globals';
|
|
||||||
import { addObservablesTo } from 'External/ko';
|
|
||||||
|
|
||||||
let notificator = null,
|
let notificator = null,
|
||||||
player = null,
|
player = null,
|
||||||
canPlay = type => !!player?.canPlayType(type).replace('no', ''),
|
canPlay = type => player && !!player.canPlayType(type).replace('no', ''),
|
||||||
|
|
||||||
audioCtx = window.AudioContext || window.webkitAudioContext,
|
audioCtx = window.AudioContext || window.webkitAudioContext,
|
||||||
|
|
||||||
|
|
@ -13,7 +11,7 @@ let notificator = null,
|
||||||
player.src = url;
|
player.src = url;
|
||||||
player.play();
|
player.play();
|
||||||
name = name.trim();
|
name = name.trim();
|
||||||
fireEvent('audio.start', name.replace(/\.([a-z0-9]{3})$/, '') || 'audio');
|
dispatchEvent(new CustomEvent('audio.start', {detail:name.replace(/\.([a-z0-9]{3})$/, '') || 'audio'}));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -46,41 +44,44 @@ let notificator = null,
|
||||||
'keydown','keyup'
|
'keydown','keyup'
|
||||||
],
|
],
|
||||||
unlock = () => {
|
unlock = () => {
|
||||||
unlockEvents.forEach(type => doc.removeEventListener(type, unlock, true));
|
|
||||||
if (audioCtx) {
|
if (audioCtx) {
|
||||||
console.log('AudioContext ' + audioCtx.state);
|
console.log('AudioContext ' + audioCtx.state);
|
||||||
audioCtx.resume();
|
audioCtx.resume();
|
||||||
}
|
}
|
||||||
// setTimeout(()=>SMAudio.playNotification(0,1),1);
|
unlockEvents.forEach(type => document.removeEventListener(type, unlock, true));
|
||||||
|
// setTimeout(()=>Audio.playNotification(1),1);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (audioCtx) {
|
if (audioCtx) {
|
||||||
audioCtx = audioCtx ? new audioCtx : null;
|
audioCtx = audioCtx ? new audioCtx : null;
|
||||||
audioCtx.onstatechange = unlock;
|
audioCtx.onstatechange = unlock;
|
||||||
}
|
}
|
||||||
unlockEvents.forEach(type => doc.addEventListener(type, unlock, true));
|
unlockEvents.forEach(type => document.addEventListener(type, unlock, true));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Browsers can't play without user interaction
|
* Browsers can't play without user interaction
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const SMAudio = new class {
|
const SMAudio = new class {
|
||||||
|
supported = false;
|
||||||
|
supportedMp3 = false;
|
||||||
|
supportedOgg = false;
|
||||||
|
supportedWav = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
player || (player = createNewObject());
|
player || (player = createNewObject());
|
||||||
|
|
||||||
this.supported = !!player;
|
this.supported = !!player;
|
||||||
this.supportedMp3 = canPlay('audio/mpeg;');
|
|
||||||
this.supportedWav = canPlay('audio/wav; codecs="1"');
|
|
||||||
this.supportedOgg = canPlay('audio/ogg; codecs="vorbis"');
|
|
||||||
if (player) {
|
if (player) {
|
||||||
|
this.supportedMp3 = !!canPlay('audio/mpeg;');
|
||||||
|
this.supportedWav = !!canPlay('audio/wav; codecs="1"');
|
||||||
|
this.supportedOgg = !!canPlay('audio/ogg; codecs="vorbis"');
|
||||||
|
|
||||||
const stopFn = () => this.pause();
|
const stopFn = () => this.pause();
|
||||||
addEventsListener(player, ['ended','error'], stopFn);
|
player.addEventListener('ended', stopFn);
|
||||||
|
player.addEventListener('error', stopFn);
|
||||||
addEventListener('audio.api.stop', stopFn);
|
addEventListener('audio.api.stop', stopFn);
|
||||||
}
|
}
|
||||||
|
|
||||||
addObservablesTo(this, {
|
|
||||||
notifications: false
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
paused() {
|
paused() {
|
||||||
|
|
@ -92,8 +93,8 @@ export const SMAudio = new class {
|
||||||
}
|
}
|
||||||
|
|
||||||
pause() {
|
pause() {
|
||||||
player?.pause();
|
player && player.pause();
|
||||||
fireEvent('audio.stop');
|
dispatchEvent(new CustomEvent('audio.stop'));
|
||||||
}
|
}
|
||||||
|
|
||||||
playMp3(url, name) {
|
playMp3(url, name) {
|
||||||
|
|
@ -108,24 +109,20 @@ export const SMAudio = new class {
|
||||||
this.supportedWav && play(url, name);
|
this.supportedWav && play(url, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
playNotification(silent) {
|
||||||
* Used with SoundNotification setting
|
if ('running' == audioCtx.state && (this.supportedMp3 || this.supportedOgg)) {
|
||||||
*/
|
if (!notificator) {
|
||||||
playNotification(force, silent) {
|
notificator = createNewObject();
|
||||||
if (force || this.notifications()) {
|
notificator.src = Links.sound('new-mail.'+ (this.supportedMp3 ? 'mp3' : 'ogg'));
|
||||||
if ('running' == audioCtx.state && (this.supportedMp3 || this.supportedOgg)) {
|
|
||||||
notificator = notificator || createNewObject();
|
|
||||||
if (notificator) {
|
|
||||||
// SettingsGet('NotificationSound').startsWith('custom@')
|
|
||||||
notificator.src = Links.staticLink('sounds/'
|
|
||||||
+ SettingsGet('NotificationSound')
|
|
||||||
+ (this.supportedMp3 ? '.mp3' : '.ogg'));
|
|
||||||
notificator.volume = silent ? 0.01 : 1;
|
|
||||||
notificator.play();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('No audio: ' + audioCtx.state);
|
|
||||||
}
|
}
|
||||||
|
if (notificator) {
|
||||||
|
notificator.volume = silent ? 0.01 : 1;
|
||||||
|
notificator.play();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('No audio: ' + audioCtx.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default SMAudio;
|
||||||
|
|
|
||||||
|
|
@ -1,56 +1,299 @@
|
||||||
let FOLDERS_CACHE = new Map,
|
import { MessageSetAction } from 'Common/Enums';
|
||||||
FOLDERS_HASH_MAP = new Map,
|
import { pInt } from 'Common/Utils';
|
||||||
|
|
||||||
|
let FOLDERS_CACHE = {},
|
||||||
|
FOLDERS_NAME_CACHE = {},
|
||||||
|
FOLDERS_HASH_CACHE = {},
|
||||||
|
FOLDERS_UID_NEXT_CACHE = {},
|
||||||
|
MESSAGE_FLAGS_CACHE = {},
|
||||||
|
NEW_MESSAGE_CACHE = {},
|
||||||
inboxFolderName = 'INBOX';
|
inboxFolderName = 'INBOX';
|
||||||
|
|
||||||
export const
|
const REQUESTED_MESSAGE_CACHE = {};
|
||||||
/**
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
clearCache = () => {
|
|
||||||
FOLDERS_CACHE.clear();
|
|
||||||
FOLDERS_HASH_MAP.clear();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
getFolderInboxName = () => inboxFolderName,
|
export function clear() {
|
||||||
|
FOLDERS_CACHE = {};
|
||||||
|
FOLDERS_NAME_CACHE = {};
|
||||||
|
FOLDERS_HASH_CACHE = {};
|
||||||
|
FOLDERS_UID_NEXT_CACHE = {};
|
||||||
|
MESSAGE_FLAGS_CACHE = {};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string}
|
* @param {string} folderFullNameRaw
|
||||||
*/
|
* @param {string} uid
|
||||||
setFolderInboxName = name => inboxFolderName = name,
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getMessageKey(folderFullNameRaw, uid) {
|
||||||
|
return `${folderFullNameRaw}#${uid}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} fullNameHash
|
* @param {string} folder
|
||||||
* @returns {string}
|
* @param {string} uid
|
||||||
*/
|
*/
|
||||||
getFolderFromHashMap = fullNameHash => getFolderFromCacheList(FOLDERS_HASH_MAP.get(fullNameHash)),
|
export function addRequestedMessage(folder, uid) {
|
||||||
|
REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} folder
|
||||||
|
* @param {string} uid
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function hasRequestedMessage(folder, uid) {
|
||||||
|
return true === REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} folderFullNameRaw
|
||||||
|
* @param {string} uid
|
||||||
|
*/
|
||||||
|
export function addNewMessageCache(folderFullNameRaw, uid) {
|
||||||
|
NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} folderFullNameRaw
|
||||||
|
* @param {string} uid
|
||||||
|
*/
|
||||||
|
export function hasNewMessageAndRemoveFromCache(folderFullNameRaw, uid) {
|
||||||
|
if (NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)]) {
|
||||||
|
NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)] = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function clearNewMessageCache() {
|
||||||
|
NEW_MESSAGE_CACHE = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getFolderInboxName() {
|
||||||
|
return inboxFolderName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function setFolderInboxName(name) {
|
||||||
|
inboxFolderName = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} folderHash
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getFolderFullNameRaw(folderHash) {
|
||||||
|
return folderHash && FOLDERS_NAME_CACHE[folderHash] ? FOLDERS_NAME_CACHE[folderHash] : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} folderHash
|
||||||
|
* @param {string} folderFullNameRaw
|
||||||
|
* @param {?FolderModel} folder
|
||||||
|
*/
|
||||||
|
export function setFolder(folderHash, folderFullNameRaw, folder) {
|
||||||
|
FOLDERS_CACHE[folderFullNameRaw] = folder;
|
||||||
|
FOLDERS_NAME_CACHE[folderHash] = folderFullNameRaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} folderFullNameRaw
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getFolderHash(folderFullNameRaw) {
|
||||||
|
return folderFullNameRaw && FOLDERS_HASH_CACHE[folderFullNameRaw] ? FOLDERS_HASH_CACHE[folderFullNameRaw] : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} folderFullNameRaw
|
||||||
|
* @param {string} folderHash
|
||||||
|
*/
|
||||||
|
export function setFolderHash(folderFullNameRaw, folderHash) {
|
||||||
|
if (folderFullNameRaw) {
|
||||||
|
FOLDERS_HASH_CACHE[folderFullNameRaw] = folderHash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} folderFullNameRaw
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getFolderUidNext(folderFullNameRaw) {
|
||||||
|
return folderFullNameRaw && FOLDERS_UID_NEXT_CACHE[folderFullNameRaw]
|
||||||
|
? FOLDERS_UID_NEXT_CACHE[folderFullNameRaw]
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} folderFullNameRaw
|
||||||
|
* @param {string} uidNext
|
||||||
|
*/
|
||||||
|
export function setFolderUidNext(folderFullNameRaw, uidNext) {
|
||||||
|
FOLDERS_UID_NEXT_CACHE[folderFullNameRaw] = uidNext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} folderFullNameRaw
|
||||||
|
* @returns {?FolderModel}
|
||||||
|
*/
|
||||||
|
export function getFolderFromCacheList(folderFullNameRaw) {
|
||||||
|
return folderFullNameRaw && FOLDERS_CACHE[folderFullNameRaw] ? FOLDERS_CACHE[folderFullNameRaw] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} folderFullNameRaw
|
||||||
|
*/
|
||||||
|
export function removeFolderFromCacheList(folderFullNameRaw) {
|
||||||
|
delete FOLDERS_CACHE[folderFullNameRaw];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MessageFlagsCache
|
||||||
|
{
|
||||||
/**
|
/**
|
||||||
* @param {?FolderModel} folder
|
* @param {string} folderFullName
|
||||||
|
* @param {string} uid
|
||||||
|
* @returns {?Array}
|
||||||
*/
|
*/
|
||||||
setFolder = folder => {
|
static getFor(folderFullName, uid) {
|
||||||
folder.etag = '';
|
return MESSAGE_FLAGS_CACHE[folderFullName] && MESSAGE_FLAGS_CACHE[folderFullName][uid]
|
||||||
FOLDERS_CACHE.set(folder.fullName, folder);
|
? MESSAGE_FLAGS_CACHE[folderFullName][uid]
|
||||||
FOLDERS_HASH_MAP.set(folder.fullNameHash, folder.fullName);
|
: null;
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folderFullName
|
* @param {string} folderFullName
|
||||||
* @param {string} folderETag
|
* @param {string} uid
|
||||||
|
* @param {Array} flagsCache
|
||||||
*/
|
*/
|
||||||
setFolderETag = (folderFullName, folderETag) =>
|
static setFor(folderFullName, uid, flagsCache) {
|
||||||
FOLDERS_CACHE.has(folderFullName) && (FOLDERS_CACHE.get(folderFullName).etag = folderETag),
|
if (!MESSAGE_FLAGS_CACHE[folderFullName]) {
|
||||||
|
MESSAGE_FLAGS_CACHE[folderFullName] = {};
|
||||||
/**
|
}
|
||||||
* @param {string} folderFullName
|
MESSAGE_FLAGS_CACHE[folderFullName][uid] = flagsCache;
|
||||||
* @returns {?FolderModel}
|
}
|
||||||
*/
|
|
||||||
getFolderFromCacheList = folderFullName =>
|
|
||||||
FOLDERS_CACHE.get(folderFullName),
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folderFullName
|
* @param {string} folderFullName
|
||||||
*/
|
*/
|
||||||
removeFolderFromCacheList = folderFullName => FOLDERS_CACHE.delete(folderFullName);
|
static clearFolder(folderFullName) {
|
||||||
|
MESSAGE_FLAGS_CACHE[folderFullName] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {(MessageModel|null)} message
|
||||||
|
*/
|
||||||
|
static initMessage(message) {
|
||||||
|
if (message) {
|
||||||
|
const uid = message.uid,
|
||||||
|
flags = this.getFor(message.folder, uid);
|
||||||
|
|
||||||
|
if (flags && flags.length) {
|
||||||
|
message.isFlagged(!!flags[1]);
|
||||||
|
|
||||||
|
if (!message.isSimpleMessage) {
|
||||||
|
message.isUnseen(!!flags[0]);
|
||||||
|
message.isAnswered(!!flags[2]);
|
||||||
|
message.isForwarded(!!flags[3]);
|
||||||
|
message.isReadReceipt(!!flags[4]);
|
||||||
|
message.isDeleted(!!flags[5]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.threads().length) {
|
||||||
|
const unseenSubUid = message.threads().find(sSubUid => {
|
||||||
|
if (uid !== sSubUid) {
|
||||||
|
const subFlags = this.getFor(message.folder, sSubUid);
|
||||||
|
return subFlags && subFlags.length && !!subFlags[0];
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const flaggedSubUid = message.threads().find(sSubUid => {
|
||||||
|
if (uid !== sSubUid) {
|
||||||
|
const subFlags = this.getFor(message.folder, sSubUid);
|
||||||
|
return subFlags && subFlags.length && !!subFlags[1];
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
message.hasUnseenSubMessage(unseenSubUid && 0 < pInt(unseenSubUid));
|
||||||
|
message.hasFlaggedSubMessage(flaggedSubUid && 0 < pInt(flaggedSubUid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {(MessageModel|null)} message
|
||||||
|
*/
|
||||||
|
static store(message) {
|
||||||
|
if (message) {
|
||||||
|
this.setFor(message.folder, message.uid, [
|
||||||
|
message.isUnseen(),
|
||||||
|
message.isFlagged(),
|
||||||
|
message.isAnswered(),
|
||||||
|
message.isForwarded(),
|
||||||
|
message.isReadReceipt(),
|
||||||
|
message.isDeleted()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} folder
|
||||||
|
* @param {string} uid
|
||||||
|
* @param {Array} flags
|
||||||
|
*/
|
||||||
|
static storeByFolderAndUid(folder, uid, flags) {
|
||||||
|
if (Array.isNotEmpty(flags)) {
|
||||||
|
this.setFor(folder, uid, flags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} folder
|
||||||
|
* @param {string} uid
|
||||||
|
* @param {number} setAction
|
||||||
|
*/
|
||||||
|
static storeBySetAction(folder, uid, setAction) {
|
||||||
|
let unread = 0;
|
||||||
|
const flags = this.getFor(folder, uid);
|
||||||
|
|
||||||
|
if (Array.isNotEmpty(flags)) {
|
||||||
|
if (flags[0]) {
|
||||||
|
unread = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (setAction) {
|
||||||
|
case MessageSetAction.SetSeen:
|
||||||
|
flags[0] = false;
|
||||||
|
break;
|
||||||
|
case MessageSetAction.UnsetSeen:
|
||||||
|
flags[0] = true;
|
||||||
|
break;
|
||||||
|
case MessageSetAction.SetFlag:
|
||||||
|
flags[1] = true;
|
||||||
|
break;
|
||||||
|
case MessageSetAction.UnsetFlag:
|
||||||
|
flags[1] = false;
|
||||||
|
break;
|
||||||
|
// no default
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setFor(folder, uid, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
return unread;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1,3 @@
|
||||||
|
export const MESSAGES_PER_PAGE_VALUES = [10, 20, 30, 50, 100];
|
||||||
|
|
||||||
export const UNUSED_OPTION_VALUE = '__UNUSE__';
|
export const UNUSED_OPTION_VALUE = '__UNUSE__';
|
||||||
|
|
|
||||||
|
|
@ -1,56 +1,372 @@
|
||||||
/* eslint quote-props: 0 */
|
/* eslint quote-props: 0 */
|
||||||
|
|
||||||
export const
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
ScopeMessageList = 'MessageList',
|
export const StorageResultType = {
|
||||||
ScopeFolderList = 'FolderList',
|
Success: 'success',
|
||||||
ScopeMessageView = 'MessageView',
|
Abort: 'abort',
|
||||||
ScopeSettings = 'Settings',
|
Error: 'error',
|
||||||
|
Unload: 'unload'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
export const Focused = {
|
||||||
|
None: 'none',
|
||||||
|
MessageList: 'message-list',
|
||||||
|
MessageView: 'message-view',
|
||||||
|
FolderList: 'folder-list'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
export const Capa = {
|
||||||
|
TwoFactor: 'TWO_FACTOR',
|
||||||
|
TwoFactorForce: 'TWO_FACTOR_FORCE',
|
||||||
|
OpenPGP: 'OPEN_PGP',
|
||||||
|
Prefetch: 'PREFETCH',
|
||||||
|
Folders: 'FOLDERS',
|
||||||
|
Composer: 'COMPOSER',
|
||||||
|
Contacts: 'CONTACTS',
|
||||||
|
Reload: 'RELOAD',
|
||||||
|
Search: 'SEARCH',
|
||||||
|
SearchAdv: 'SEARCH_ADV',
|
||||||
|
MessageActions: 'MESSAGE_ACTIONS',
|
||||||
|
MessageListActions: 'MESSAGELIST_ACTIONS',
|
||||||
|
AttachmentsActions: 'ATTACHMENTS_ACTIONS',
|
||||||
|
DangerousActions: 'DANGEROUS_ACTIONS',
|
||||||
|
Settings: 'SETTINGS',
|
||||||
|
Help: 'HELP',
|
||||||
|
Themes: 'THEMES',
|
||||||
|
UserBackground: 'USER_BACKGROUND',
|
||||||
|
Sieve: 'SIEVE',
|
||||||
|
Filters: 'FILTERS',
|
||||||
|
AttachmentThumbnails: 'ATTACHMENT_THUMBNAILS',
|
||||||
|
Templates: 'TEMPLATES',
|
||||||
|
AutoLogout: 'AUTOLOGOUT',
|
||||||
|
AdditionalAccounts: 'ADDITIONAL_ACCOUNTS',
|
||||||
|
Identities: 'IDENTITIES'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
export const KeyState = {
|
||||||
|
All: 'all',
|
||||||
|
None: 'none',
|
||||||
|
ContactList: 'contact-list',
|
||||||
|
MessageList: 'message-list',
|
||||||
|
FolderList: 'folder-list',
|
||||||
|
MessageView: 'message-view',
|
||||||
|
Compose: 'compose',
|
||||||
|
Settings: 'settings',
|
||||||
|
Menu: 'menu',
|
||||||
|
PopupComposeOpenPGP: 'compose-open-pgp',
|
||||||
|
PopupMessageOpenPGP: 'message-open-pgp',
|
||||||
|
PopupViewOpenPGP: 'view-open-pgp',
|
||||||
|
PopupKeyboardShortcutsHelp: 'popup-keyboard-shortcuts-help',
|
||||||
|
PopupAsk: 'popup-ask'
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enum {number}
|
* @enum {number}
|
||||||
*/
|
*/
|
||||||
UploadErrorCode = {
|
export const FolderType = {
|
||||||
|
Inbox: 10,
|
||||||
|
SentItems: 11,
|
||||||
|
Draft: 12,
|
||||||
|
Trash: 13,
|
||||||
|
Spam: 14,
|
||||||
|
Archive: 15,
|
||||||
|
NotSpam: 80,
|
||||||
|
User: 99
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enum {number}
|
||||||
|
*/
|
||||||
|
export const ServerFolderType = {
|
||||||
|
USER: 0,
|
||||||
|
INBOX: 1,
|
||||||
|
SENT: 2,
|
||||||
|
DRAFTS: 3,
|
||||||
|
JUNK: 4,
|
||||||
|
TRASH: 5,
|
||||||
|
IMPORTANT: 10,
|
||||||
|
FLAGGED: 11,
|
||||||
|
ALL: 12
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
export const LoginSignMeTypeAsString = {
|
||||||
|
DefaultOff: 'defaultoff',
|
||||||
|
DefaultOn: 'defaulton',
|
||||||
|
Unused: 'unused'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enum {number}
|
||||||
|
*/
|
||||||
|
export const LoginSignMeType = {
|
||||||
|
DefaultOff: 0,
|
||||||
|
DefaultOn: 1,
|
||||||
|
Unused: 2
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
export const ComposeType = {
|
||||||
|
Empty: 'empty',
|
||||||
|
Reply: 'reply',
|
||||||
|
ReplyAll: 'replyall',
|
||||||
|
Forward: 'forward',
|
||||||
|
ForwardAsAttachment: 'forward-as-attachment',
|
||||||
|
Draft: 'draft',
|
||||||
|
EditAsNew: 'editasnew'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enum {number}
|
||||||
|
*/
|
||||||
|
export const UploadErrorCode = {
|
||||||
Normal: 0,
|
Normal: 0,
|
||||||
FileIsTooBig: 1,
|
FileIsTooBig: 1,
|
||||||
FilePartiallyUploaded: 3,
|
FilePartiallyUploaded: 2,
|
||||||
NoFileUploaded: 4,
|
FileNoUploaded: 3,
|
||||||
MissingTempFolder: 6,
|
MissingTempFolder: 4,
|
||||||
OnSavingFile: 7,
|
FileOnSaveingError: 5,
|
||||||
FileType: 98,
|
FileType: 98,
|
||||||
Unknown: 99
|
Unknown: 99
|
||||||
},
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enum {number}
|
* @enum {number}
|
||||||
*/
|
*/
|
||||||
SaveSettingStatus = {
|
export const SetSystemFoldersNotification = {
|
||||||
Saving: -2,
|
None: 0,
|
||||||
|
Sent: 1,
|
||||||
|
Draft: 2,
|
||||||
|
Spam: 3,
|
||||||
|
Trash: 4,
|
||||||
|
Archive: 5
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enum {number}
|
||||||
|
*/
|
||||||
|
export const ClientSideKeyName = {
|
||||||
|
FoldersLashHash: 0,
|
||||||
|
MessagesInboxLastHash: 1,
|
||||||
|
MailBoxListSize: 2,
|
||||||
|
ExpandedFolders: 3,
|
||||||
|
FolderListSize: 4,
|
||||||
|
MessageListSize: 5,
|
||||||
|
LastReplyAction: 6,
|
||||||
|
LastSignMe: 7,
|
||||||
|
ComposeLastIdentityID: 8,
|
||||||
|
MessageHeaderFullInfo: 9,
|
||||||
|
MessageAttachmentControls: 10
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enum {number}
|
||||||
|
*/
|
||||||
|
export const MessageSetAction = {
|
||||||
|
SetSeen: 0,
|
||||||
|
UnsetSeen: 1,
|
||||||
|
SetFlag: 2,
|
||||||
|
UnsetFlag: 3
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enum {number}
|
||||||
|
*/
|
||||||
|
export const MessageSelectAction = {
|
||||||
|
All: 0,
|
||||||
|
None: 1,
|
||||||
|
Invert: 2,
|
||||||
|
Unseen: 3,
|
||||||
|
Seen: 4,
|
||||||
|
Flagged: 5,
|
||||||
|
Unflagged: 6
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enum {number}
|
||||||
|
*/
|
||||||
|
export const MessagePriority = {
|
||||||
|
Low: 5,
|
||||||
|
Normal: 3,
|
||||||
|
High: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
export const EditorDefaultType = {
|
||||||
|
Html: 'Html',
|
||||||
|
Plain: 'Plain',
|
||||||
|
HtmlForced: 'HtmlForced',
|
||||||
|
PlainForced: 'PlainForced'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enum {number}
|
||||||
|
*/
|
||||||
|
export const ServerSecure = {
|
||||||
|
None: 0,
|
||||||
|
SSL: 1,
|
||||||
|
TLS: 2
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enum {number}
|
||||||
|
*/
|
||||||
|
export const SearchDateType = {
|
||||||
|
All: -1,
|
||||||
|
Days3: 3,
|
||||||
|
Days7: 7,
|
||||||
|
Month: 30
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enum {number}
|
||||||
|
*/
|
||||||
|
export const SaveSettingsStep = {
|
||||||
|
Animate: -2,
|
||||||
Idle: -1,
|
Idle: -1,
|
||||||
Success: 1,
|
TrueResult: 1,
|
||||||
Failed: 0
|
FalseResult: 0
|
||||||
},
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @enum {number}
|
* @enum {number}
|
||||||
*/
|
*/
|
||||||
Notifications = {
|
export const Layout = {
|
||||||
RequestError: 1,
|
NoPreview: 0,
|
||||||
RequestAborted: 2,
|
SidePreview: 1,
|
||||||
RequestTimeout: 3,
|
BottomPreview: 2
|
||||||
|
};
|
||||||
|
|
||||||
// Global
|
/**
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
export const FilterConditionField = {
|
||||||
|
From: 'From',
|
||||||
|
Recipient: 'Recipient',
|
||||||
|
Subject: 'Subject',
|
||||||
|
Header: 'Header',
|
||||||
|
Body: 'Body',
|
||||||
|
Size: 'Size'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
export const FilterConditionType = {
|
||||||
|
Contains: 'Contains',
|
||||||
|
NotContains: 'NotContains',
|
||||||
|
EqualTo: 'EqualTo',
|
||||||
|
NotEqualTo: 'NotEqualTo',
|
||||||
|
Regex: 'Regex',
|
||||||
|
Over: 'Over',
|
||||||
|
Under: 'Under',
|
||||||
|
Text: 'Text',
|
||||||
|
Raw: 'Raw'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
export const FiltersAction = {
|
||||||
|
None: 'None',
|
||||||
|
MoveTo: 'MoveTo',
|
||||||
|
Discard: 'Discard',
|
||||||
|
Vacation: 'Vacation',
|
||||||
|
Reject: 'Reject',
|
||||||
|
Forward: 'Forward'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
export const FilterRulesType = {
|
||||||
|
All: 'All',
|
||||||
|
Any: 'Any'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enum {number}
|
||||||
|
*/
|
||||||
|
export const SignedVerifyStatus = {
|
||||||
|
UnknownPublicKeys: -4,
|
||||||
|
UnknownPrivateKey: -3,
|
||||||
|
Unverified: -2,
|
||||||
|
Error: -1,
|
||||||
|
None: 0,
|
||||||
|
Success: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enum {number}
|
||||||
|
*/
|
||||||
|
export const ContactPropertyType = {
|
||||||
|
Unknown: 0,
|
||||||
|
|
||||||
|
FullName: 10,
|
||||||
|
|
||||||
|
FirstName: 15,
|
||||||
|
LastName: 16,
|
||||||
|
MiddleName: 17,
|
||||||
|
Nick: 18,
|
||||||
|
|
||||||
|
NamePrefix: 20,
|
||||||
|
NameSuffix: 21,
|
||||||
|
|
||||||
|
Email: 30,
|
||||||
|
Phone: 31,
|
||||||
|
Web: 32,
|
||||||
|
|
||||||
|
Birthday: 40,
|
||||||
|
|
||||||
|
Facebook: 90,
|
||||||
|
Skype: 91,
|
||||||
|
GitHub: 92,
|
||||||
|
|
||||||
|
Note: 110,
|
||||||
|
|
||||||
|
Custom: 250
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enum {number}
|
||||||
|
*/
|
||||||
|
export const Notification = {
|
||||||
InvalidToken: 101,
|
InvalidToken: 101,
|
||||||
AuthError: 102,
|
AuthError: 102,
|
||||||
|
AccessError: 103,
|
||||||
// User
|
|
||||||
ConnectionError: 104,
|
ConnectionError: 104,
|
||||||
|
CaptchaError: 105,
|
||||||
|
SocialFacebookLoginAccessDisable: 106,
|
||||||
|
SocialTwitterLoginAccessDisable: 107,
|
||||||
|
SocialGoogleLoginAccessDisable: 108,
|
||||||
DomainNotAllowed: 109,
|
DomainNotAllowed: 109,
|
||||||
AccountNotAllowed: 110,
|
AccountNotAllowed: 110,
|
||||||
CryptKeyError: 111,
|
|
||||||
|
AccountTwoFactorAuthRequired: 120,
|
||||||
|
AccountTwoFactorAuthError: 121,
|
||||||
|
|
||||||
|
CouldNotSaveNewPassword: 130,
|
||||||
|
CurrentPasswordIncorrect: 131,
|
||||||
|
NewPasswordShort: 132,
|
||||||
|
NewPasswordWeak: 133,
|
||||||
|
NewPasswordForbidden: 134,
|
||||||
|
|
||||||
ContactsSyncError: 140,
|
ContactsSyncError: 140,
|
||||||
|
|
||||||
|
|
@ -66,9 +382,7 @@ Notifications = {
|
||||||
|
|
||||||
CantSaveFilters: 351,
|
CantSaveFilters: 351,
|
||||||
CantGetFilters: 352,
|
CantGetFilters: 352,
|
||||||
CantActivateFiltersScript: 353,
|
FiltersAreNotCorrect: 355,
|
||||||
CantDeleteFiltersScript: 354,
|
|
||||||
// FiltersAreNotCorrect: 355,
|
|
||||||
|
|
||||||
CantCreateFolder: 400,
|
CantCreateFolder: 400,
|
||||||
CantRenameFolder: 401,
|
CantRenameFolder: 401,
|
||||||
|
|
@ -77,31 +391,33 @@ Notifications = {
|
||||||
CantUnsubscribeFolder: 404,
|
CantUnsubscribeFolder: 404,
|
||||||
CantDeleteNonEmptyFolder: 405,
|
CantDeleteNonEmptyFolder: 405,
|
||||||
|
|
||||||
// CantSaveSettings: 501,
|
CantSaveSettings: 501,
|
||||||
|
CantSavePluginSettings: 502,
|
||||||
|
|
||||||
DomainAlreadyExists: 601,
|
DomainAlreadyExists: 601,
|
||||||
|
|
||||||
|
CantInstallPackage: 701,
|
||||||
|
CantDeletePackage: 702,
|
||||||
|
InvalidPluginPackage: 703,
|
||||||
|
UnsupportedPluginPackage: 704,
|
||||||
|
|
||||||
DemoSendMessageError: 750,
|
DemoSendMessageError: 750,
|
||||||
DemoAccountError: 751,
|
DemoAccountError: 751,
|
||||||
|
|
||||||
AccountAlreadyExists: 801,
|
AccountAlreadyExists: 801,
|
||||||
AccountDoesNotExist: 802,
|
AccountDoesNotExist: 802,
|
||||||
AccountSwitchFailed: 803,
|
|
||||||
|
|
||||||
MailServerError: 901,
|
MailServerError: 901,
|
||||||
ClientViewError: 902,
|
ClientViewError: 902,
|
||||||
InvalidInputArgument: 903,
|
InvalidInputArgument: 903,
|
||||||
|
|
||||||
JsonFalse: 950,
|
AjaxFalse: 950,
|
||||||
JsonParse: 952,
|
AjaxAbort: 951,
|
||||||
// JsonTimeout: 953,
|
AjaxParse: 952,
|
||||||
|
AjaxTimeout: 953,
|
||||||
|
|
||||||
UnknownError: 999,
|
UnknownNotification: 999,
|
||||||
|
UnknownError: 999
|
||||||
|
|
||||||
// Admin
|
,getKeyByValue: function(v) { return Object.keys(this).find(key => this[key] === v); }
|
||||||
CantInstallPackage: 701,
|
|
||||||
CantDeletePackage: 702,
|
|
||||||
InvalidPluginPackage: 703,
|
|
||||||
UnsupportedPluginPackage: 704,
|
|
||||||
CantSavePluginSettings: 705
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
/* eslint quote-props: 0 */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @enum {number}
|
|
||||||
*/
|
|
||||||
export const FolderType = {
|
|
||||||
Inbox: 1,
|
|
||||||
Sent: 2,
|
|
||||||
Drafts: 3,
|
|
||||||
Junk: 4, // Spam
|
|
||||||
Trash: 5,
|
|
||||||
Archive: 6
|
|
||||||
/*
|
|
||||||
IMPORTANT : 10;
|
|
||||||
FLAGGED : 11;
|
|
||||||
ALL : 13;
|
|
||||||
// TODO: SnappyMail
|
|
||||||
TEMPLATES : 19;
|
|
||||||
// Kolab
|
|
||||||
CONFIGURATION : 20;
|
|
||||||
CALENDAR : 21;
|
|
||||||
CONTACTS : 22;
|
|
||||||
TASKS : 23;
|
|
||||||
NOTES : 24;
|
|
||||||
FILES : 25;
|
|
||||||
JOURNAL : 26;
|
|
||||||
*/
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @enum {string}
|
|
||||||
*/
|
|
||||||
FolderMetadataKeys = {
|
|
||||||
// RFC 5464
|
|
||||||
Comment: '/private/comment',
|
|
||||||
CommentShared: '/shared/comment',
|
|
||||||
// RFC 6154
|
|
||||||
SpecialUse: '/private/specialuse',
|
|
||||||
// Kolab
|
|
||||||
KolabFolderType: '/private/vendor/kolab/folder-type',
|
|
||||||
KolabFolderTypeShared: '/shared/vendor/kolab/folder-type'
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @enum {string}
|
|
||||||
*/
|
|
||||||
ComposeType = {
|
|
||||||
Empty: 0,
|
|
||||||
Reply: 1,
|
|
||||||
ReplyAll: 2,
|
|
||||||
Forward: 3,
|
|
||||||
ForwardAsAttachment: 4,
|
|
||||||
Draft: 5,
|
|
||||||
EditAsNew: 6
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @enum {number}
|
|
||||||
*/
|
|
||||||
ClientSideKeyNameExpandedFolders = 3,
|
|
||||||
ClientSideKeyNameFolderListSize = 4,
|
|
||||||
ClientSideKeyNameMessageListSize = 5,
|
|
||||||
ClientSideKeyNameLastSignMe = 7,
|
|
||||||
ClientSideKeyNameMessageHeaderFullInfo = 9,
|
|
||||||
ClientSideKeyNameMessageAttachmentControls = 10,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @enum {number}
|
|
||||||
*/
|
|
||||||
MessageSetAction = {
|
|
||||||
SetSeen: 0,
|
|
||||||
UnsetSeen: 1,
|
|
||||||
SetFlag: 2,
|
|
||||||
UnsetFlag: 3,
|
|
||||||
SetDeleted: 4,
|
|
||||||
UnsetDeleted: 5
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @enum {number}
|
|
||||||
*/
|
|
||||||
//LayoutNoView = 0,
|
|
||||||
LayoutSideView = 1,
|
|
||||||
LayoutBottomView = 2
|
|
||||||
;
|
|
||||||
|
|
@ -1,147 +1,147 @@
|
||||||
/* eslint key-spacing: 0 */
|
/* eslint key-spacing: 0 */
|
||||||
/* eslint quote-props: 0 */
|
/* eslint quote-props: 0 */
|
||||||
|
|
||||||
import { arrayLength, pInt } from 'Common/Utils';
|
|
||||||
|
|
||||||
export const RFC822 = 'message/rfc822';
|
|
||||||
|
|
||||||
const
|
const
|
||||||
cache = {},
|
cache = {},
|
||||||
app = 'application/',
|
app = 'application/',
|
||||||
msOffice = app+'vnd.openxmlformats-officedocument.',
|
msOffice = app+'vnd.openxmlformats-officedocument.',
|
||||||
openDoc = app+'vnd.oasis.opendocument.',
|
openDoc = app+'vnd.oasis.opendocument.',
|
||||||
|
font = app+'x-font-',
|
||||||
sizes = ['B', 'KiB', 'MiB', 'GiB', 'TiB'],
|
sizes = ['B', 'KiB', 'MiB', 'GiB', 'TiB'],
|
||||||
lowerCase = text => text.toLowerCase().trim(),
|
|
||||||
|
|
||||||
exts = {
|
exts = {
|
||||||
eml: RFC822,
|
'eml': 'message/rfc822',
|
||||||
mime: RFC822,
|
'mime': 'message/rfc822',
|
||||||
vcard: 'text/vcard',
|
'rtx': 'text/richtext',
|
||||||
vcf: 'text/vcard',
|
'vcard': 'text/vcard',
|
||||||
htm: 'text/html',
|
'vcf': 'text/vcard',
|
||||||
html: 'text/html',
|
'htm': 'text/html',
|
||||||
csv: 'text/csv',
|
'html': 'text/html',
|
||||||
ics: 'text/calendar',
|
'csv': 'text/csv',
|
||||||
xml: 'text/xml',
|
'ics': 'text/calendar',
|
||||||
json: app+'json',
|
'ifb': 'text/calendar',
|
||||||
// asc: app+'pgp-signature',
|
'xml': 'text/xml',
|
||||||
// asc: app+'pgp-keys',
|
'json': app+'json',
|
||||||
p10: app+'pkcs10',
|
'p10': app+'pkcs10',
|
||||||
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',
|
'torrent': app+'x-bittorrent',
|
||||||
pfx: app+'x-pkcs12',
|
|
||||||
torrent: app+'x-bittorrent',
|
|
||||||
|
|
||||||
// scripts
|
// scripts
|
||||||
js: app+'javascript',
|
'js': app+'javascript',
|
||||||
pl: 'text/perl',
|
'pl': 'text/perl',
|
||||||
css: 'text/css',
|
'css': 'text/css',
|
||||||
asp: 'text/asp',
|
'asp': 'text/asp',
|
||||||
php: app+'x-php',
|
'php': app+'x-httpd-php',
|
||||||
|
'phtml': app+'x-httpd-php',
|
||||||
|
|
||||||
// images
|
// images
|
||||||
jpg: 'image/jpeg',
|
'jpg': 'image/jpeg',
|
||||||
ico: 'image/x-icon',
|
'ico': 'image/x-icon',
|
||||||
tif: 'image/tiff',
|
'tif': 'image/tiff',
|
||||||
svg: 'image/svg+xml',
|
'svg': 'image/svg+xml',
|
||||||
svgz: 'image/svg+xml',
|
'svgz': 'image/svg+xml',
|
||||||
|
|
||||||
// archives
|
// archives
|
||||||
zip: app+'zip',
|
'zip': app+'zip',
|
||||||
'7z': app+'x-7z-compressed',
|
'7z': app+'x-7z-compressed',
|
||||||
rar: app+'x-rar-compressed',
|
'rar': app+'x-rar-compressed',
|
||||||
cab: app+'vnd.ms-cab-compressed',
|
'cab': app+'vnd.ms-cab-compressed',
|
||||||
gz: app+'x-gzip',
|
'gz': app+'x-gzip',
|
||||||
tgz: app+'x-gzip',
|
'tgz': app+'x-gzip',
|
||||||
bz: app+'x-bzip',
|
'bz': app+'x-bzip',
|
||||||
bz2: app+'x-bzip2',
|
'bz2': app+'x-bzip2',
|
||||||
deb: app+'x-debian-package',
|
'deb': app+'x-debian-package',
|
||||||
|
|
||||||
// audio
|
// fonts
|
||||||
mp3: 'audio/mpeg',
|
'psf': font+'linux-psf',
|
||||||
wav: 'audio/x-wav',
|
'ttc': font+'ttf',
|
||||||
mp4a: 'audio/mp4',
|
|
||||||
weba: 'audio/webm',
|
|
||||||
m3u: 'audio/x-mpegurl',
|
|
||||||
|
|
||||||
// video
|
// audio
|
||||||
qt: 'video/quicktime',
|
'mp3': 'audio/mpeg',
|
||||||
mov: 'video/quicktime',
|
'wav': 'audio/x-wav',
|
||||||
wmv: 'video/windows-media',
|
'mp4a': 'audio/mp4',
|
||||||
avi: 'video/x-msvideo',
|
'weba': 'audio/webm',
|
||||||
'3gp': 'video/3gpp',
|
'm3u': 'audio/x-mpegurl',
|
||||||
'3g2': 'video/3gpp2',
|
|
||||||
mp4v: 'video/mp4',
|
|
||||||
mpg4: 'video/mp4',
|
|
||||||
ogv: 'video/ogg',
|
|
||||||
m4v: 'video/x-m4v',
|
|
||||||
asf: 'video/x-ms-asf',
|
|
||||||
asx: 'video/x-ms-asf',
|
|
||||||
wm: 'video/x-ms-wm',
|
|
||||||
wmx: 'video/x-ms-wmx',
|
|
||||||
wvx: 'video/x-ms-wvx',
|
|
||||||
movie: 'video/x-sgi-movie',
|
|
||||||
|
|
||||||
// adobe
|
// video
|
||||||
pdf: app+'pdf',
|
'qt': 'video/quicktime',
|
||||||
psd: 'image/vnd.adobe.photoshop',
|
'mov': 'video/quicktime',
|
||||||
ai: app+'postscript',
|
'wmv': 'video/windows-media',
|
||||||
eps: app+'postscript',
|
'avi': 'video/x-msvideo',
|
||||||
ps: app+'postscript',
|
'3gp': 'video/3gpp',
|
||||||
|
'3g2': 'video/3gpp2',
|
||||||
|
'mp4v': 'video/mp4',
|
||||||
|
'mpg4': 'video/mp4',
|
||||||
|
'ogv': 'video/ogg',
|
||||||
|
'm4v': 'video/x-m4v',
|
||||||
|
'asf': 'video/x-ms-asf',
|
||||||
|
'asx': 'video/x-ms-asf',
|
||||||
|
'wm': 'video/x-ms-wm',
|
||||||
|
'wmx': 'video/x-ms-wmx',
|
||||||
|
'wvx': 'video/x-ms-wvx',
|
||||||
|
'movie': 'video/x-sgi-movie',
|
||||||
|
|
||||||
// ms office
|
// adobe
|
||||||
doc: app+'msword',
|
'pdf': app+'pdf',
|
||||||
rtf: app+'rtf',
|
'psd': 'image/vnd.adobe.photoshop',
|
||||||
xls: app+'vnd.ms-excel',
|
'ai': app+'postscript',
|
||||||
ppt: app+'vnd.ms-powerpoint',
|
'eps': app+'postscript',
|
||||||
docx: msOffice+'wordprocessingml.document',
|
'ps': app+'postscript',
|
||||||
xlsx: msOffice+'spreadsheetml.sheet',
|
|
||||||
dotx: msOffice+'wordprocessingml.template',
|
|
||||||
pptx: msOffice+'presentationml.presentation',
|
|
||||||
|
|
||||||
// open office
|
// ms office
|
||||||
odt: openDoc+'text',
|
'doc': app+'msword',
|
||||||
ods: openDoc+'spreadsheet',
|
'rtf': app+'rtf',
|
||||||
odp: openDoc+'presentation'
|
'xls': app+'vnd.ms-excel',
|
||||||
};
|
'ppt': app+'vnd.ms-powerpoint',
|
||||||
|
'docx': msOffice+'wordprocessingml.document',
|
||||||
|
'xlsx': msOffice+'spreadsheetml.sheet',
|
||||||
|
'dotx': msOffice+'wordprocessingml.template',
|
||||||
|
'pptx': msOffice+'presentationml.presentation',
|
||||||
|
|
||||||
|
// open office
|
||||||
|
'odt': openDoc+'text',
|
||||||
|
'ods': openDoc+'spreadsheet',
|
||||||
|
'odp': openDoc+'presentation'
|
||||||
|
};
|
||||||
|
|
||||||
export const FileType = {
|
export const FileType = {
|
||||||
Unknown: 'unknown',
|
Unknown: 'unknown',
|
||||||
Text: 'text',
|
Text: 'text',
|
||||||
|
Html: 'html',
|
||||||
Code: 'code',
|
Code: 'code',
|
||||||
Eml: 'eml',
|
Eml: 'eml',
|
||||||
Word: 'word',
|
WordText: 'word-text',
|
||||||
Pdf: 'pdf',
|
Pdf: 'pdf',
|
||||||
Image: 'image',
|
Image: 'image',
|
||||||
Audio: 'audio',
|
Audio: 'audio',
|
||||||
Video: 'video',
|
Video: 'video',
|
||||||
Spreadsheet: 'spreadsheet',
|
Sheet: 'sheet',
|
||||||
Presentation: 'presentation',
|
Presentation: 'presentation',
|
||||||
Certificate: 'certificate',
|
Certificate: 'certificate',
|
||||||
Archive: 'archive',
|
CertificateBin: 'certificate-bin',
|
||||||
Calendar: 'calendar'
|
Archive: 'archive'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FileInfo = {
|
export const File = {
|
||||||
/**
|
/**
|
||||||
* @param {string} fileName
|
* @param {string} fileName
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getExtension: fileName => {
|
getExtension: fileName => {
|
||||||
fileName = lowerCase(fileName);
|
fileName = fileName.toLowerCase().trim();
|
||||||
const result = fileName.split('.').pop();
|
const result = fileName.split('.').pop();
|
||||||
return result === fileName ? '' : result;
|
return result === fileName ? '' : result;
|
||||||
},
|
},
|
||||||
|
|
||||||
getContentType: fileName => {
|
getContentType: fileName => {
|
||||||
fileName = lowerCase(fileName);
|
fileName = fileName.toLowerCase().trim();
|
||||||
if ('winmail.dat' === fileName) {
|
if ('winmail.dat' === fileName) {
|
||||||
return app + 'vnd.ms-tnef';
|
return 'application/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|asc)$/.test(ext))
|
||||||
return 'text/plain';
|
return 'text/plain';
|
||||||
if (/^(mpe?g|mpe|m1v|m2v)$/.test(ext))
|
if (/^(mpe?g|mpe|m1v|m2v)$/.test(ext))
|
||||||
return 'video/mpeg';
|
return 'video/mpeg';
|
||||||
|
|
@ -151,8 +151,8 @@ export const FileInfo = {
|
||||||
return 'audio/'+ext;
|
return 'audio/'+ext;
|
||||||
if (/^(h26[134]|jpgv|mp4|webm)$/.test(ext))
|
if (/^(h26[134]|jpgv|mp4|webm)$/.test(ext))
|
||||||
return 'video/'+ext;
|
return 'video/'+ext;
|
||||||
if (/^(otf|sfnt|ttf|woff2?)$/.test(ext))
|
if (/^(otf|pcf|snf|ttf)$/.test(ext))
|
||||||
return 'font/'+ext;
|
return font+ext;
|
||||||
if (/^(png|jpeg|gif|tiff|webp)$/.test(ext))
|
if (/^(png|jpeg|gif|tiff|webp)$/.test(ext))
|
||||||
return 'image/'+ext;
|
return 'image/'+ext;
|
||||||
|
|
||||||
|
|
@ -165,8 +165,8 @@ export const FileInfo = {
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getType: (ext, mimeType) => {
|
getType: (ext, mimeType) => {
|
||||||
ext = lowerCase(ext);
|
ext = ext.toLowerCase().trim();
|
||||||
mimeType = lowerCase(mimeType).replace('csv/plain', 'text/csv').replace('x-','');
|
mimeType = mimeType.toLowerCase().trim().replace('csv/plain', 'text/csv');
|
||||||
|
|
||||||
let key = ext + mimeType;
|
let key = ext + mimeType;
|
||||||
if (cache[key]) {
|
if (cache[key]) {
|
||||||
|
|
@ -175,55 +175,74 @@ export const FileInfo = {
|
||||||
|
|
||||||
let result = FileType.Unknown;
|
let result = FileType.Unknown;
|
||||||
const mimeTypeParts = mimeType.split('/'),
|
const mimeTypeParts = mimeType.split('/'),
|
||||||
type = mimeTypeParts[1].replace('-compressed',''),
|
type = mimeTypeParts[1],
|
||||||
match = str => mimeType.includes(str),
|
match = str => type.includes(str);
|
||||||
archive = /^(zip|7z|tar|rar|gzip|bzip|bzip2)$/;
|
|
||||||
|
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case 'image' == mimeTypeParts[0] || ['png', 'jpg', 'jpeg', 'gif', 'webp'].includes(ext):
|
case 'image' === mimeTypeParts[0] || ['png', 'jpg', 'jpeg', 'gif'].includes(ext):
|
||||||
result = FileType.Image;
|
result = FileType.Image;
|
||||||
break;
|
break;
|
||||||
case 'audio' == mimeTypeParts[0] || ['mp3', 'ogg', 'oga', 'wav'].includes(ext):
|
case 'audio' === mimeTypeParts[0] || ['mp3', 'ogg', 'oga', 'wav'].includes(ext):
|
||||||
result = FileType.Audio;
|
result = FileType.Audio;
|
||||||
break;
|
break;
|
||||||
case 'video' == mimeTypeParts[0] || 'mkv' == ext || 'avi' == ext:
|
case 'video' === mimeTypeParts[0] || ['mkv', 'avi'].includes(ext):
|
||||||
result = FileType.Video;
|
result = FileType.Video;
|
||||||
break;
|
break;
|
||||||
case ['php', 'js', 'css', 'xml', 'html'].includes(ext) || 'text/html' == mimeType:
|
case ['php', 'js', 'css'].includes(ext):
|
||||||
result = FileType.Code;
|
result = FileType.Code;
|
||||||
break;
|
break;
|
||||||
case 'eml' == ext || ['message/delivery-status', RFC822].includes(mimeType):
|
case 'eml' === ext || ['message/delivery-status', 'message/rfc822'].includes(mimeType):
|
||||||
result = FileType.Eml;
|
result = FileType.Eml;
|
||||||
break;
|
break;
|
||||||
case 'ics' == ext || mimeType == 'text/calendar':
|
case 'text/html' === mimeType || ['html'].includes(ext):
|
||||||
result = FileType.Calendar;
|
result = FileType.Html;
|
||||||
break;
|
break;
|
||||||
case 'text' == mimeTypeParts[0] || 'txt' == ext || 'log' == ext:
|
case 'text' === mimeTypeParts[0] || ['txt', 'log'].includes(ext):
|
||||||
result = FileType.Text;
|
result = FileType.Text;
|
||||||
break;
|
break;
|
||||||
case archive.test(type) || archive.test(ext):
|
case [
|
||||||
|
'zip',
|
||||||
|
'7z',
|
||||||
|
'tar',
|
||||||
|
'rar',
|
||||||
|
'gzip',
|
||||||
|
'bzip',
|
||||||
|
'bzip2',
|
||||||
|
'x-zip',
|
||||||
|
'x-7z',
|
||||||
|
'x-rar',
|
||||||
|
'x-tar',
|
||||||
|
'x-gzip',
|
||||||
|
'x-bzip',
|
||||||
|
'x-bzip2',
|
||||||
|
'x-zip-compressed',
|
||||||
|
'x-7z-compressed',
|
||||||
|
'x-rar-compressed'
|
||||||
|
].includes(type) || ['zip', '7z', 'tar', 'rar', 'gzip', 'bzip', 'bzip2'].includes(ext):
|
||||||
result = FileType.Archive;
|
result = FileType.Archive;
|
||||||
break;
|
break;
|
||||||
case 'pdf' == type || 'pdf' == ext:
|
case ['pdf', 'x-pdf'].includes(type) || ['pdf'].includes(ext):
|
||||||
result = FileType.Pdf;
|
result = FileType.Pdf;
|
||||||
break;
|
break;
|
||||||
case [app+'pgp-signature', app+'pgp-keys', exts.p7m, exts.p7s, exts.p12, exts.pfx].includes(mimeType)
|
case ['application/pgp-signature', 'application/pgp-keys'].includes(mimeType) ||
|
||||||
|| ['asc', 'pem', 'ppk', 'p7s', 'p7m', 'p12', 'pfx'].includes(ext):
|
['asc', 'pem', 'ppk'].includes(ext):
|
||||||
result = FileType.Certificate;
|
result = FileType.Certificate;
|
||||||
break;
|
break;
|
||||||
|
case ['application/pkcs7-signature'].includes(mimeType) || ['p7s'].includes(ext):
|
||||||
|
result = FileType.CertificateBin;
|
||||||
|
break;
|
||||||
case match(msOffice+'.wordprocessingml') || match(openDoc+'.text') || match('vnd.ms-word')
|
case match(msOffice+'.wordprocessingml') || match(openDoc+'.text') || match('vnd.ms-word')
|
||||||
|| ['rtf', 'msword', 'vnd.msword'].includes(type):
|
|| ['rtf', 'msword', 'vnd.msword'].includes(type):
|
||||||
result = FileType.Word;
|
result = FileType.WordText;
|
||||||
break;
|
break;
|
||||||
case match(msOffice+'.spreadsheetml') || match(openDoc+'.spreadsheet') || match('ms-excel'):
|
case match(msOffice+'.spreadsheetml') || match(openDoc+'.spreadsheet') || match('ms-excel'):
|
||||||
result = FileType.Spreadsheet;
|
result = FileType.Sheet;
|
||||||
break;
|
break;
|
||||||
case match(msOffice+'.presentationml') || match(openDoc+'.presentation') || match('ms-powerpoint'):
|
case match(msOffice+'.presentationml') || match(openDoc+'.presentation') || match('ms-powerpoint'):
|
||||||
result = FileType.Presentation;
|
result = FileType.Presentation;
|
||||||
break;
|
break;
|
||||||
// no default
|
// no default
|
||||||
}
|
}
|
||||||
|
|
||||||
return cache[key] = result;
|
return cache[key] = result;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -232,40 +251,60 @@ export const FileInfo = {
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getTypeIconClass: fileType => {
|
getTypeIconClass: fileType => {
|
||||||
let result = 'icon-file';
|
let result = ['icon-file', ''];
|
||||||
switch (fileType) {
|
switch (fileType) {
|
||||||
case FileType.Text:
|
case FileType.Text:
|
||||||
case FileType.Eml:
|
case FileType.Eml:
|
||||||
case FileType.Pdf:
|
case FileType.WordText:
|
||||||
case FileType.Word:
|
result[0] += '-text';
|
||||||
return result + '-text';
|
break;
|
||||||
|
case FileType.Html:
|
||||||
case FileType.Code:
|
case FileType.Code:
|
||||||
|
result[0] += '-code';
|
||||||
|
break;
|
||||||
case FileType.Image:
|
case FileType.Image:
|
||||||
|
result[0] += '-image';
|
||||||
|
break;
|
||||||
case FileType.Audio:
|
case FileType.Audio:
|
||||||
|
result[0] += '-music';
|
||||||
|
break;
|
||||||
case FileType.Video:
|
case FileType.Video:
|
||||||
|
result[0] += '-movie';
|
||||||
|
break;
|
||||||
case FileType.Archive:
|
case FileType.Archive:
|
||||||
|
result[0] += '-zip';
|
||||||
|
break;
|
||||||
case FileType.Certificate:
|
case FileType.Certificate:
|
||||||
case FileType.Spreadsheet:
|
case FileType.CertificateBin:
|
||||||
|
result[0] += '-certificate';
|
||||||
|
break;
|
||||||
|
case FileType.Sheet:
|
||||||
|
result[0] += '-excel';
|
||||||
|
break;
|
||||||
case FileType.Presentation:
|
case FileType.Presentation:
|
||||||
case FileType.Calendar:
|
result[0] += '-chart-graph';
|
||||||
return result + '-' + fileType;
|
break;
|
||||||
|
case FileType.Pdf:
|
||||||
|
result['icon-none', 'pdf'];
|
||||||
|
break;
|
||||||
|
// no default
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
getIconClass: (ext, mime) => FileInfo.getTypeIconClass(FileInfo.getType(ext, mime)),
|
getIconClass: (ext, mime) => File.getTypeIconClass(File.getType(ext, mime)),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} sFileType
|
* @param {string} sFileType
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getAttachmentsIconClass: data => {
|
getCombinedIconClass: data => {
|
||||||
if (arrayLength(data)) {
|
if (Array.isNotEmpty(data)) {
|
||||||
let icons = data
|
let icons = data
|
||||||
.map(item => item ? FileInfo.getIconClass(FileInfo.getExtension(item.fileName), item.mimeType) : '')
|
.map(item => item ? File.getIconClass(File.getExtension(item[0]), item[1])[0] : '')
|
||||||
.validUnique();
|
.validUnique();
|
||||||
|
|
||||||
return (1 === icons?.length && 'icon-file' !== icons[0])
|
return (icons && 1 === icons.length && 'icon-file' !== icons[0])
|
||||||
? icons[0]
|
? icons[0]
|
||||||
: 'icon-attachment';
|
: 'icon-attachment';
|
||||||
}
|
}
|
||||||
|
|
@ -274,9 +313,9 @@ export const FileInfo = {
|
||||||
},
|
},
|
||||||
|
|
||||||
friendlySize: bytes => {
|
friendlySize: bytes => {
|
||||||
bytes = pInt(bytes);
|
bytes = parseInt(bytes, 10) || 0;
|
||||||
let i = bytes ? Math.floor(Math.log(bytes) / Math.log(1024)) : 0;
|
let i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
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,211 +0,0 @@
|
||||||
import { RFC822 } from 'Common/File';
|
|
||||||
import { getFolderInboxName, getFolderFromCacheList } from 'Common/Cache';
|
|
||||||
import { baseCollator } from 'Common/Translator';
|
|
||||||
import { SettingsGet } from 'Common/Globals';
|
|
||||||
import { isArray, arrayLength, pInt } from 'Common/Utils';
|
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
|
||||||
import { FolderUserStore } from 'Stores/User/Folder';
|
|
||||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
|
||||||
|
|
||||||
import Remote from 'Remote/User/Fetch';
|
|
||||||
|
|
||||||
let refreshInterval,
|
|
||||||
// Default every 15 minutes
|
|
||||||
refreshFoldersInterval = 900000;
|
|
||||||
|
|
||||||
export const
|
|
||||||
|
|
||||||
setRefreshFoldersInterval = minutes => {
|
|
||||||
refreshFoldersInterval = Math.max(1, pInt(SettingsGet('minRefreshInterval')), pInt(minutes)) * 60000;
|
|
||||||
clearInterval(refreshInterval);
|
|
||||||
refreshInterval = setInterval(() => {
|
|
||||||
const cF = FolderUserStore.currentFolderFullName(),
|
|
||||||
iF = getFolderInboxName();
|
|
||||||
folderInformation(iF);
|
|
||||||
iF === cF || folderInformation(cF);
|
|
||||||
folderInformationMultiply();
|
|
||||||
}, refreshFoldersInterval);
|
|
||||||
},
|
|
||||||
|
|
||||||
sortFolders = folders => {
|
|
||||||
try {
|
|
||||||
let collator = baseCollator(true);
|
|
||||||
folders.sort((a, b) =>
|
|
||||||
a.isInbox() ? -1 : (b.isInbox() ? 1 : collator.compare(a.fullName, b.fullName))
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Array=} aDisabled
|
|
||||||
* @param {Array=} aHeaderLines
|
|
||||||
* @param {Function=} fRenameCallback
|
|
||||||
* @param {Function=} fDisableCallback
|
|
||||||
* @param {boolean=} bNoSelectSelectable Used in FolderCreatePopupView
|
|
||||||
* @returns {Array}
|
|
||||||
*/
|
|
||||||
folderListOptionsBuilder = (
|
|
||||||
aDisabled,
|
|
||||||
aHeaderLines,
|
|
||||||
fRenameCallback,
|
|
||||||
fDisableCallback
|
|
||||||
) => {
|
|
||||||
const
|
|
||||||
aResult = [],
|
|
||||||
sDeepPrefix = '\u00A0\u00A0\u00A0',
|
|
||||||
// FolderSystemPopupView should always be true
|
|
||||||
showUnsubscribed = fRenameCallback ? !SettingsUserStore.hideUnsubscribed() : true,
|
|
||||||
isDisabled = fDisableCallback || (item => !item.selectable() || aDisabled.includes(item.fullName)),
|
|
||||||
|
|
||||||
foldersWalk = folders => {
|
|
||||||
folders.forEach(oItem => {
|
|
||||||
if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
|
|
||||||
aResult.push({
|
|
||||||
id: oItem.fullName,
|
|
||||||
name:
|
|
||||||
sDeepPrefix.repeat(oItem.deep) +
|
|
||||||
fRenameCallback(oItem),
|
|
||||||
system: false,
|
|
||||||
disabled: isDisabled(oItem)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
foldersWalk(oItem.subFolders());
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
fDisableCallback = fDisableCallback || (() => false);
|
|
||||||
fRenameCallback = fRenameCallback || (oItem => oItem.name());
|
|
||||||
isArray(aDisabled) || (aDisabled = []);
|
|
||||||
|
|
||||||
isArray(aHeaderLines) && aHeaderLines.forEach(line =>
|
|
||||||
aResult.push({
|
|
||||||
id: line[0],
|
|
||||||
name: line[1],
|
|
||||||
system: false,
|
|
||||||
disabled: false
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
foldersWalk(FolderUserStore.folderList());
|
|
||||||
|
|
||||||
return aResult;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} folder
|
|
||||||
* @param {Array=} list = []
|
|
||||||
*/
|
|
||||||
folderInformation = (folder, list) => {
|
|
||||||
if (folder?.trim()) {
|
|
||||||
let count = 1;
|
|
||||||
const uids = [];
|
|
||||||
|
|
||||||
if (arrayLength(list)) {
|
|
||||||
list.forEach(messageListItem => {
|
|
||||||
uids.push(messageListItem.uid);
|
|
||||||
messageListItem.threads.forEach(uid => uids.push(uid));
|
|
||||||
});
|
|
||||||
count = uids.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count) {
|
|
||||||
Remote.request('FolderInformation', (iError, data) => {
|
|
||||||
if (!iError && data.Result) {
|
|
||||||
const result = data.Result,
|
|
||||||
folderFromCache = getFolderFromCacheList(result.name);
|
|
||||||
if (folderFromCache) {
|
|
||||||
const oldHash = folderFromCache.etag,
|
|
||||||
unreadCountChange = (folderFromCache.unreadEmails() !== result.unreadEmails);
|
|
||||||
|
|
||||||
// folderFromCache.revivePropertiesFromJson(result);
|
|
||||||
folderFromCache.expires = Date.now();
|
|
||||||
folderFromCache.uidNext = result.uidNext;
|
|
||||||
folderFromCache.etag = result.etag;
|
|
||||||
folderFromCache.totalEmails(result.totalEmails);
|
|
||||||
folderFromCache.unreadEmails(result.unreadEmails);
|
|
||||||
|
|
||||||
MessagelistUserStore.notifyNewMessages(folderFromCache.fullName, result.newMessages);
|
|
||||||
|
|
||||||
if (!oldHash || unreadCountChange || result.etag !== oldHash) {
|
|
||||||
if (folderFromCache.fullName === FolderUserStore.currentFolderFullName()) {
|
|
||||||
MessagelistUserStore.reload();
|
|
||||||
/*
|
|
||||||
} else if (getFolderInboxName() === folderFromCache.fullName) {
|
|
||||||
// Remote.messageList(null, {folder: getFolderFromCacheList(getFolderInboxName())}, true);
|
|
||||||
Remote.messageList(null, {folder: getFolderInboxName()}, true);
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
folder: folder,
|
|
||||||
flagsUids: uids,
|
|
||||||
uidNext: getFolderFromCacheList(folder)?.uidNext || 0 // Used to check for new messages
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {boolean=} boot = false
|
|
||||||
*/
|
|
||||||
folderInformationMultiply = (boot = false) => {
|
|
||||||
const folders = FolderUserStore.getNextFolderNames(refreshFoldersInterval);
|
|
||||||
if (arrayLength(folders)) {
|
|
||||||
Remote.request('FolderInformationMultiply', (iError, oData) => {
|
|
||||||
if (!iError && arrayLength(oData.Result)) {
|
|
||||||
const utc = Date.now();
|
|
||||||
oData.Result.forEach(item => {
|
|
||||||
const folder = getFolderFromCacheList(item.name);
|
|
||||||
if (folder) {
|
|
||||||
const oldHash = folder.etag,
|
|
||||||
unreadCountChange = folder.unreadEmails() !== item.unreadEmails;
|
|
||||||
|
|
||||||
// folder.revivePropertiesFromJson(item);
|
|
||||||
folder.expires = utc;
|
|
||||||
folder.etag = item.etag;
|
|
||||||
folder.totalEmails(item.totalEmails);
|
|
||||||
folder.unreadEmails(item.unreadEmails);
|
|
||||||
|
|
||||||
if (!oldHash || item.etag !== oldHash) {
|
|
||||||
if (folder.fullName === FolderUserStore.currentFolderFullName()) {
|
|
||||||
MessagelistUserStore.reload();
|
|
||||||
}
|
|
||||||
} else if (unreadCountChange
|
|
||||||
&& folder.fullName === FolderUserStore.currentFolderFullName()
|
|
||||||
&& MessagelistUserStore.length) {
|
|
||||||
folderInformation(folder.fullName, MessagelistUserStore());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
boot && setTimeout(() => folderInformationMultiply(true), 2000);
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
folders: folders
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
dropFilesInFolder = (sFolderFullName, files) => {
|
|
||||||
let count = files.length;
|
|
||||||
for (const file of files) {
|
|
||||||
if (RFC822 === file.type) {
|
|
||||||
let data = new FormData;
|
|
||||||
data.append('folder', sFolderFullName);
|
|
||||||
data.append('appendFile', file);
|
|
||||||
Remote.request('FolderAppend', (iError, data)=>{
|
|
||||||
iError && console.error(data.message);
|
|
||||||
0 == --count
|
|
||||||
&& FolderUserStore.currentFolderFullName() == sFolderFullName
|
|
||||||
&& MessagelistUserStore.reload(true, true);
|
|
||||||
}, data);
|
|
||||||
} else {
|
|
||||||
--count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
import { doc, appEl, $htmlCL } from 'Common/Globals';
|
|
||||||
|
|
||||||
// Fullscreen must be on app, else other popups fail
|
|
||||||
export const
|
|
||||||
appFullscreen = () => (doc.fullscreenElement || doc.webkitFullscreenElement) === appEl,
|
|
||||||
exitFullscreen = () => appFullscreen() && (doc.exitFullscreen || doc.webkitExitFullscreen).call(doc),
|
|
||||||
isFullscreen = ko.observable(false),
|
|
||||||
toggleFullscreen = () => isFullscreen() ? exitFullscreen() : appEl.requestFullscreen();
|
|
||||||
|
|
||||||
if (appEl) {
|
|
||||||
let event = 'fullscreenchange';
|
|
||||||
if (!appEl.requestFullscreen && appEl.webkitRequestFullscreen) {
|
|
||||||
appEl.requestFullscreen = appEl.webkitRequestFullscreen;
|
|
||||||
event = 'webkit'+event;
|
|
||||||
}
|
|
||||||
if (appEl.requestFullscreen) {
|
|
||||||
doc.addEventListener(event, () => {
|
|
||||||
isFullscreen(appFullscreen());
|
|
||||||
$htmlCL.toggle('rl-fullscreen', appFullscreen());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,79 +1,53 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
|
import { KeyState } from 'Common/Enums';
|
||||||
|
|
||||||
let keyScopeFake = 'all';
|
export const $html = document.documentElement;
|
||||||
|
export const $htmlCL = $html.classList;
|
||||||
|
|
||||||
export const
|
/**
|
||||||
ScopeMenu = 'Menu',
|
* @type {?}
|
||||||
|
*/
|
||||||
|
export const dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 });
|
||||||
|
|
||||||
doc = document,
|
export const VIEW_MODELS = {
|
||||||
|
settings: [],
|
||||||
|
'settings-removed': [],
|
||||||
|
'settings-disabled': []
|
||||||
|
};
|
||||||
|
|
||||||
$htmlCL = doc.documentElement.classList,
|
export const moveAction = ko.observable(false);
|
||||||
|
export const leftPanelDisabled = ko.observable(false);
|
||||||
|
export const leftPanelType = ko.observable('');
|
||||||
|
export const leftPanelWidth = ko.observable(0);
|
||||||
|
|
||||||
elementById = id => doc.getElementById(id),
|
leftPanelDisabled.subscribe(value => value && moveAction() && moveAction(false));
|
||||||
|
|
||||||
appEl = elementById('rl-app'),
|
moveAction.subscribe(value => value && leftPanelDisabled() && leftPanelDisabled(false));
|
||||||
|
|
||||||
Settings = rl.settings,
|
// keys
|
||||||
SettingsGet = Settings.get,
|
export const keyScopeReal = ko.observable(KeyState.All);
|
||||||
SettingsAdmin = name => (SettingsGet('Admin') || {})[name],
|
export const keyScopeFake = ko.observable(KeyState.All);
|
||||||
SettingsCapa = name => name && !!(SettingsGet('Capa') || {})[name],
|
|
||||||
|
|
||||||
dropdowns = [],
|
export const keyScope = ko.computed({
|
||||||
dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 }),
|
read: () => keyScopeFake(),
|
||||||
|
write: value => {
|
||||||
leftPanelDisabled = ko.observable(false),
|
if (KeyState.Menu !== value) {
|
||||||
toggleLeftPanel = () => leftPanelDisabled(!leftPanelDisabled()),
|
keyScopeFake(value);
|
||||||
|
|
||||||
createElement = (name, attr) => {
|
|
||||||
let el = doc.createElement(name);
|
|
||||||
attr && Object.entries(attr).forEach(([k,v]) => el.setAttribute(k,v));
|
|
||||||
return el;
|
|
||||||
},
|
|
||||||
|
|
||||||
fireEvent = (name, detail, cancelable) => dispatchEvent(
|
|
||||||
new CustomEvent(name, {detail:detail, cancelable: !!cancelable})
|
|
||||||
),
|
|
||||||
|
|
||||||
stopEvent = event => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
},
|
|
||||||
|
|
||||||
formFieldFocused = () => doc.activeElement?.matches('input,textarea'),
|
|
||||||
|
|
||||||
addShortcut = (...args) => shortcuts.add(...args),
|
|
||||||
|
|
||||||
registerShortcut = (keys, modifiers, scopes, method) =>
|
|
||||||
addShortcut(keys, modifiers, scopes, event => formFieldFocused() ? true : method(event)),
|
|
||||||
|
|
||||||
addEventsListener = (element, events, fn, options) =>
|
|
||||||
events.forEach(event => element.addEventListener(event, fn, options)),
|
|
||||||
|
|
||||||
addEventsListeners = (element, events) =>
|
|
||||||
Object.entries(events).forEach(([event, fn]) => element.addEventListener(event, fn)),
|
|
||||||
|
|
||||||
// keys / shortcuts
|
|
||||||
keyScopeReal = ko.observable('all'),
|
|
||||||
keyScope = value => {
|
|
||||||
if (!value) {
|
|
||||||
return keyScopeFake;
|
|
||||||
}
|
|
||||||
if (ScopeMenu !== value) {
|
|
||||||
keyScopeFake = value;
|
|
||||||
if (dropdownVisibility()) {
|
if (dropdownVisibility()) {
|
||||||
value = ScopeMenu;
|
value = KeyState.Menu;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
keyScopeReal(value);
|
|
||||||
shortcuts.setScope(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
dropdownVisibility.subscribe(value => {
|
keyScopeReal(value);
|
||||||
if (value) {
|
|
||||||
keyScope(ScopeMenu);
|
|
||||||
} else if (ScopeMenu === shortcuts.getScope()) {
|
|
||||||
keyScope(keyScopeFake);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
leftPanelDisabled.subscribe(value => $htmlCL.toggle('rl-left-panel-disabled', value));
|
keyScopeReal.subscribe(value => shortcuts.setScope(value));
|
||||||
|
|
||||||
|
dropdownVisibility.subscribe(value => {
|
||||||
|
if (value) {
|
||||||
|
keyScope(KeyState.Menu);
|
||||||
|
} else if (KeyState.Menu === shortcuts.getScope()) {
|
||||||
|
keyScope(keyScopeFake());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,846 +0,0 @@
|
||||||
import { createElement } from 'Common/Globals';
|
|
||||||
import { forEachObjectEntry, isArray, pInt } from 'Common/Utils';
|
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
|
||||||
|
|
||||||
const
|
|
||||||
tmpl = createElement('template'),
|
|
||||||
|
|
||||||
turndown = new TurndownService(),
|
|
||||||
|
|
||||||
htmlre = /[&<>"']/g,
|
|
||||||
httpre = /^(https?:)?\/\//i,
|
|
||||||
htmlmap = {
|
|
||||||
'&': '&',
|
|
||||||
'<': '<',
|
|
||||||
'>': '>',
|
|
||||||
'"': '"',
|
|
||||||
"'": '''
|
|
||||||
},
|
|
||||||
|
|
||||||
keepTagContent = 'form,button,data', // font
|
|
||||||
|
|
||||||
allowedTags = [
|
|
||||||
// Structural Elements:
|
|
||||||
'blockquote','br','div','figcaption','figure','h1','h2','h3','h4','h5','h6','hgroup','hr','p','wbr',
|
|
||||||
'article','aside','header','footer','main','section',
|
|
||||||
'details','summary','nav',
|
|
||||||
// List Elements
|
|
||||||
'dd','dl','dt','li','ol','ul',
|
|
||||||
// Text Formatting Elements
|
|
||||||
'a','abbr','address','b','bdi','bdo','cite','code','del','dfn',
|
|
||||||
'em','i','ins','kbd','mark','pre','q','rp','rt','ruby','s','samp','small',
|
|
||||||
'span','strong','sub','sup','time','u','var',
|
|
||||||
// Deprecated by HTML Standard
|
|
||||||
'acronym','big','center','dir','font','marquee',
|
|
||||||
'nobr','plaintext','rb','rtc','strike','tt',
|
|
||||||
// Media Elements
|
|
||||||
'img',//'picture','source',
|
|
||||||
// Table Elements
|
|
||||||
'caption','col','colgroup','table','tbody','td','tfoot','th','thead','tr',
|
|
||||||
// Disallowed but converted later
|
|
||||||
'style','xmp'
|
|
||||||
].join(','),
|
|
||||||
|
|
||||||
nonEmptyTags = [
|
|
||||||
'A','B','EM','I','SPAN','STRONG'
|
|
||||||
],
|
|
||||||
|
|
||||||
blockquoteSwitcher = () => {
|
|
||||||
SettingsUserStore.collapseBlockquotes() &&
|
|
||||||
// tmpl.content.querySelectorAll('blockquote').forEach(node => {
|
|
||||||
[...tmpl.content.querySelectorAll('blockquote')].reverse().forEach(node => {
|
|
||||||
const el = createElement('details', {class:'sm-bq-switcher'});
|
|
||||||
el.innerHTML = '<summary>•••</summary>';
|
|
||||||
node.replaceWith(el);
|
|
||||||
el.append(node);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
replaceWithChildren = node => node.replaceWith(...[...node.childNodes]),
|
|
||||||
|
|
||||||
urlRegExp = /https?:\/\/[^\p{C}\p{Z}]+[^\p{C}\p{Z}.]/gu,
|
|
||||||
// eslint-disable-next-line max-len
|
|
||||||
email = /(^|\r|\n|\p{C}\p{Z})((?:[^"(),.:;<>@[\]\\\p{C}\p{Z}]+(?:\.[^"(),.:;<>@[\]\\\p{C}\p{Z}]+)*|"(?:\\?[^"\\\p{C}\p{Z}])*")@[^@\p{C}\p{Z}]+[^@\p{C}\p{Z}.])/gui,
|
|
||||||
// rfc3966
|
|
||||||
tel = /(tel:(\+[0-9().-]+|[0-9*#().-]+(;phone-context=\+[0-9+().-]+)?))/g,
|
|
||||||
|
|
||||||
// Strip tracking
|
|
||||||
/** TODO: implement other url strippers like from
|
|
||||||
* https://www.bleepingcomputer.com/news/security/new-firefox-privacy-feature-strips-urls-of-tracking-parameters/
|
|
||||||
* https://github.com/newhouse/url-tracking-stripper
|
|
||||||
* https://github.com/svenjacobs/leon
|
|
||||||
* https://maxchadwick.xyz/tracking-query-params-registry/
|
|
||||||
* https://github.com/M66B/FairEmail/blob/master/app/src/main/java/eu/faircode/email/UriHelper.java
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line max-len
|
|
||||||
stripParams = /^(utm_|ec_|fbclid|mc_eid|mkt_tok|_hsenc|vero_id|oly_enc_id|oly_anon_id|__s|Referrer|mailing|elq|bch|trc|ref|correlation_id|pd_|pf_|email_hash)$/i,
|
|
||||||
urlGetParam = (url, name) => new URL(url).searchParams.get(name) || url,
|
|
||||||
base64Url = data => atob(data.replace(/_/g,'/').replace(/-/g,'+')),
|
|
||||||
decode = decodeURIComponent,
|
|
||||||
stripTracking = url => {
|
|
||||||
try {
|
|
||||||
let nurl = url
|
|
||||||
// Copernica
|
|
||||||
.replace(/^.+\/(https%253A[^/?&]+).*$/i, (...m) => decode(decode(m[1])))
|
|
||||||
.replace(/tracking\.(printabout\.nl[^?]+)\?.*/i, (...m) => m[1])
|
|
||||||
.replace(/(zalando\.nl[^?]+)\?.*/i, (...m) => m[1])
|
|
||||||
.replace(/^.+(awstrack\.me|redditmail\.com)\/.+(https:%2F%2F[^/]+).*/i, (...m) => decode(m[2]))
|
|
||||||
.replace(/^.+(www\.google|safelinks\.protection\.outlook\.com|mailchimp\.com).+url=.+$/i,
|
|
||||||
() => urlGetParam(url, 'url'))
|
|
||||||
.replace(/^.+click\.godaddy\.com.+$/i, () => urlGetParam(url, 'redir'))
|
|
||||||
.replace(/^.+delivery-status\.com.+$/i, () => urlGetParam(url, 'fb'))
|
|
||||||
.replace(/^.+go\.dhlparcel\.nl.+\/([A-Za-z0-9_-]+)$/i, (...m) => base64Url(m[1]))
|
|
||||||
.replace(/^(.+mopinion\.com.+)\?.*$/i, (...m) => m[1])
|
|
||||||
.replace(/^.+sellercentral\.amazon\.com\/nms\/redirect.+$/i, () => base64Url(urlGetParam(url, 'u')))
|
|
||||||
.replace(/^.+amazon\.com\/gp\/r\.html.+$/i, () => urlGetParam(url, 'U'))
|
|
||||||
// Mandrill
|
|
||||||
.replace(/^.+\/track\/click\/.+\?p=.+$/i, () => {
|
|
||||||
let d = urlGetParam(url, 'p');
|
|
||||||
try {
|
|
||||||
d = JSON.parse(base64Url(d));
|
|
||||||
if (d?.p) {
|
|
||||||
d = JSON.parse(d.p);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
return d?.url || url;
|
|
||||||
})
|
|
||||||
// Remove invalid URL characters
|
|
||||||
.replace(/[\s<>]+/gi, '');
|
|
||||||
nurl = new URL(nurl);
|
|
||||||
let s = nurl.searchParams;
|
|
||||||
[...s.keys()].forEach(key => stripParams.test(key) && s.delete(key));
|
|
||||||
return nurl.toString();
|
|
||||||
} catch (e) {
|
|
||||||
console.dir({
|
|
||||||
error:e,
|
|
||||||
url:url
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
},
|
|
||||||
|
|
||||||
cleanCSS = source =>
|
|
||||||
source.trim()
|
|
||||||
.replace(/;\s*-[^;]+/g, '')
|
|
||||||
.replace(/^\s*-[^;]+(;|$)/g, '')
|
|
||||||
.replace(/white-space[^;]+/g, '')
|
|
||||||
// Drop Microsoft Office style properties
|
|
||||||
// .replace(/mso-[^:;]+:[^;]+/gi, '')
|
|
||||||
,
|
|
||||||
|
|
||||||
/*
|
|
||||||
Parses given css string, and returns css object
|
|
||||||
keys as selectors and values are css rules
|
|
||||||
eliminates all css comments before parsing
|
|
||||||
|
|
||||||
@param source css string to be parsed
|
|
||||||
|
|
||||||
@return object css
|
|
||||||
*/
|
|
||||||
parseCSS = source => {
|
|
||||||
const css = [];
|
|
||||||
css.toString = () => css.reduce(
|
|
||||||
(ret, tmp) =>
|
|
||||||
ret + tmp.selector + ' {\n'
|
|
||||||
+ (tmp.type === 'media' ? tmp.subStyles.toString() : tmp.rules)
|
|
||||||
+ '}\n'
|
|
||||||
,
|
|
||||||
''
|
|
||||||
);
|
|
||||||
/**
|
|
||||||
* Given css array, parses it and then for every selector,
|
|
||||||
* prepends namespace to prevent css collision issues
|
|
||||||
*/
|
|
||||||
css.applyNamespace = namespace => css.forEach(obj => {
|
|
||||||
if (obj.type === 'media') {
|
|
||||||
obj.subStyles.applyNamespace(namespace);
|
|
||||||
} else {
|
|
||||||
obj.selector = obj.selector.split(',').map(selector =>
|
|
||||||
(namespace + ' .mail-body ' + selector.replace(/\./g, '.msg-'))
|
|
||||||
.replace(/\sbody/gi, '')
|
|
||||||
).join(',');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (source) {
|
|
||||||
source = source
|
|
||||||
// strip comments
|
|
||||||
.replace(/\/\*[\s\S]*?\*\//gi, '')
|
|
||||||
// strip MS Word comments
|
|
||||||
.replace(/<!--[\s\S]*?-->/gi, '')
|
|
||||||
// strip HTML, as < is no CSS combinator anyway
|
|
||||||
.replace(/<[\s\S]*/gi, '');
|
|
||||||
// .replace(/<\/?[a-z][\s\S]*?>/gi, '');
|
|
||||||
|
|
||||||
// unified regex to match css & media queries together
|
|
||||||
let unified = /(?:(\s*?@(?:media)[\s\S]*?){([\s\S]*?)}\s*?})|(?:([\s\S]*?){([\s\S]*?)})/gi,
|
|
||||||
arr;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
arr = unified.exec(source);
|
|
||||||
if (arr === null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let selector = arr[arr[2] === undefined ? 3 : 1].split('\r\n').join('\n').trim()
|
|
||||||
// Never have more than a single line break in a row
|
|
||||||
.replace(/\n+/, "\n")
|
|
||||||
// Remove :root and html
|
|
||||||
.split(/\s+/g).map(item => item.replace(/^(:root|html)$/, '')).join(' ').trim();
|
|
||||||
|
|
||||||
// determine the type
|
|
||||||
if (selector.includes('@media')) {
|
|
||||||
// we have a media query
|
|
||||||
css.push({
|
|
||||||
selector: selector,
|
|
||||||
type: 'media',
|
|
||||||
subStyles: parseCSS(arr[2] + '\n}') //recursively parse media query inner css
|
|
||||||
});
|
|
||||||
} else if (selector && !selector.includes('@')) {
|
|
||||||
// we have standard css
|
|
||||||
// ignores @import, @keyframe, @font-face statements
|
|
||||||
css.push({
|
|
||||||
selector: selector,
|
|
||||||
rules: cleanCSS(arr[4])
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return css;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} text
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
encodeHtml = text => (text?.toString?.() || '' + text).replace(htmlre, m => htmlmap[m]),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears the Message Html for viewing
|
|
||||||
* @param {string} text
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
cleanHtml = (html, oAttachments, msgId) => {
|
|
||||||
let aColor;
|
|
||||||
const
|
|
||||||
debug = false, // Config()->Get('debug', 'enable', false);
|
|
||||||
detectHiddenImages = true, // !!SettingsGet('try_to_detect_hidden_images'),
|
|
||||||
|
|
||||||
bqLevel = parseInt(SettingsUserStore.maxBlockquotesLevel()),
|
|
||||||
minFontSize = parseInt(SettingsUserStore.minFontSize()),
|
|
||||||
|
|
||||||
result = {
|
|
||||||
hasExternals: false,
|
|
||||||
tracking: false,
|
|
||||||
linkedData: []
|
|
||||||
},
|
|
||||||
|
|
||||||
isMsg = !!msgId,
|
|
||||||
|
|
||||||
findAttachmentByCid = cId => oAttachments.findByCid(cId),
|
|
||||||
findLocationByCid = cId => {
|
|
||||||
const attachment = findAttachmentByCid(cId);
|
|
||||||
return attachment?.contentLocation ? attachment : 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
// convert body attributes to CSS
|
|
||||||
tasks = {
|
|
||||||
link: value => aColor = value,
|
|
||||||
text: (value, node) => node.style.color = value,
|
|
||||||
topmargin: (value, node) => node.style.marginTop = pInt(value) + 'px',
|
|
||||||
leftmargin: (value, node) => node.style.marginLeft = pInt(value) + 'px',
|
|
||||||
bottommargin: (value, node) => node.style.marginBottom = pInt(value) + 'px',
|
|
||||||
rightmargin: (value, node) => node.style.marginRight = pInt(value) + 'px'
|
|
||||||
},
|
|
||||||
allowedAttributes = [
|
|
||||||
// SnappyMail
|
|
||||||
// 'data-x-href-broken',
|
|
||||||
// 'data-x-href-tracking',
|
|
||||||
'data-x-src',
|
|
||||||
// 'data-x-src-hidden',
|
|
||||||
// 'data-x-src-tracking',
|
|
||||||
// 'data-x-src-broken',
|
|
||||||
// 'data-x-style-url',
|
|
||||||
// 'data-x-style-broken-urls',
|
|
||||||
// defaults
|
|
||||||
'name',
|
|
||||||
'dir', 'lang', 'style', 'title',
|
|
||||||
'background', 'bgcolor', 'alt', 'height', 'width', 'src', 'href',
|
|
||||||
'border', 'bordercolor', 'charset', 'direction',
|
|
||||||
// a
|
|
||||||
'download', 'hreflang',
|
|
||||||
// body
|
|
||||||
'alink', 'bottommargin', 'leftmargin', 'link', 'rightmargin', 'text', 'topmargin', 'vlink',
|
|
||||||
// col
|
|
||||||
'align', 'valign',
|
|
||||||
// font
|
|
||||||
'color', 'face', 'size',
|
|
||||||
// hr
|
|
||||||
'noshade',
|
|
||||||
// img
|
|
||||||
'hspace', 'sizes', 'srcset', 'vspace',
|
|
||||||
// meter
|
|
||||||
'low', 'high', 'optimum', 'value',
|
|
||||||
// ol
|
|
||||||
'reversed', 'start',
|
|
||||||
// table
|
|
||||||
'cols', 'rows', 'frame', 'rules', 'summary', 'cellpadding', 'cellspacing',
|
|
||||||
// th
|
|
||||||
'abbr', 'scope',
|
|
||||||
// td
|
|
||||||
'colspan', 'rowspan', 'headers'
|
|
||||||
// others
|
|
||||||
//'class', 'id', 'target'
|
|
||||||
];
|
|
||||||
|
|
||||||
if (SettingsUserStore.allowStyles()) {
|
|
||||||
allowedAttributes.push('class');
|
|
||||||
} else {
|
|
||||||
msgId = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpl.innerHTML = html
|
|
||||||
// .replace(/<pre[^>]*>[\s\S]*?<\/pre>/gi, pre => pre.replace(/\n/g, '\n<br>'))
|
|
||||||
// Not supported by <template> element
|
|
||||||
// .replace(/<!doctype[^>]*>/gi, '')
|
|
||||||
// .replace(/<\?xml[^>]*\?>/gi, '')
|
|
||||||
// https://github.com/the-djmaze/snappymail/issues/1860
|
|
||||||
.replace(/<(\/?)x-html(\s[^>]*)?>/gi, '')
|
|
||||||
.replace(/<(\/?)head(\s[^>]*)?>/gi, '')
|
|
||||||
.replace(/<(\/?)body(\s[^>]*)?>/gi, '<$1div class="mail-body"$2>')
|
|
||||||
// .replace(/<\/?(html|head)[^>]*>/gi, '')
|
|
||||||
// Fix Reddit https://github.com/the-djmaze/snappymail/issues/540
|
|
||||||
.replace(/<span class="preview-text"[\s\S]+?<\/span>/, '')
|
|
||||||
// https://github.com/the-djmaze/snappymail/issues/900
|
|
||||||
.replace(/\u2028/g,' ')
|
|
||||||
// https://github.com/the-djmaze/snappymail/issues/1415
|
|
||||||
.replace(/<br>\s*<\/p>/gi,'</p>')
|
|
||||||
.trim();
|
|
||||||
html = '';
|
|
||||||
|
|
||||||
// Strip all comments
|
|
||||||
const nodeIterator = document.createNodeIterator(tmpl.content, NodeFilter.SHOW_COMMENT);
|
|
||||||
while (nodeIterator.nextNode()) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// https://github.com/the-djmaze/snappymail/issues/1125
|
|
||||||
tmpl.content.querySelectorAll(keepTagContent).forEach(oElement => replaceWithChildren(oElement));
|
|
||||||
|
|
||||||
tmpl.content.querySelectorAll(
|
|
||||||
':not('+allowedTags+'),a:empty,span:empty'
|
|
||||||
+ (0 < bqLevel ? ',' + (new Array(1 + bqLevel).fill('blockquote').join(' ')) : '')
|
|
||||||
).forEach(oElement => oElement.remove());
|
|
||||||
/* // Is this slower or faster?
|
|
||||||
).forEach(oElement => {
|
|
||||||
if (!node || !node.contains(oElement)) {
|
|
||||||
oElement.remove();
|
|
||||||
node = 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 => {
|
|
||||||
const name = oElement.tagName,
|
|
||||||
oStyle = oElement.style;
|
|
||||||
|
|
||||||
if ('STYLE' === name) {
|
|
||||||
let css = msgId ? parseCSS(oElement.textContent) : [];
|
|
||||||
if (css.length) {
|
|
||||||
css.applyNamespace(msgId);
|
|
||||||
css = css.toString();
|
|
||||||
if (SettingsUserStore.removeColors()) {
|
|
||||||
css = css.replace(/(background-)?color:[^};]+;?/g, '');
|
|
||||||
}
|
|
||||||
oElement.textContent = css;
|
|
||||||
} else {
|
|
||||||
oElement.remove();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('XMP' === name) {
|
|
||||||
const pre = createElement('pre');
|
|
||||||
pre.innerHTML = encodeHtml(oElement.innerHTML);
|
|
||||||
oElement.replaceWith(pre);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// \MailSo\Base\HtmlUtils::ClearTags()
|
|
||||||
if ('none' == oStyle.display
|
|
||||||
|| 'hidden' == oStyle.visibility
|
|
||||||
// || (oStyle.lineHeight && 1 > parseFloat(oStyle.lineHeight)
|
|
||||||
// || (oStyle.maxHeight && 1 > parseFloat(oStyle.maxHeight)
|
|
||||||
// || (oStyle.maxWidth && 1 > parseFloat(oStyle.maxWidth)
|
|
||||||
// || ('0' === oStyle.opacity
|
|
||||||
) {
|
|
||||||
oElement.remove();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const aAttrsForRemove = [],
|
|
||||||
className = oElement.className,
|
|
||||||
hasAttribute = name => oElement.hasAttribute(name),
|
|
||||||
getAttribute = name => hasAttribute(name) ? oElement.getAttribute(name).trim() : '',
|
|
||||||
setAttribute = (name, value) => oElement.setAttribute(name, value),
|
|
||||||
delAttribute = name => {
|
|
||||||
let value = getAttribute(name);
|
|
||||||
oElement.removeAttribute(name);
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
if ('mail-body' === className) {
|
|
||||||
forEachObjectEntry(tasks, (name, cb) =>
|
|
||||||
hasAttribute(name) && cb(delAttribute(name), oElement)
|
|
||||||
);
|
|
||||||
} else if (msgId && className) {
|
|
||||||
oElement.className = className.replace(/(^|\s+)/g, '$1msg-');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oElement.hasAttributes()) {
|
|
||||||
let i = oElement.attributes.length;
|
|
||||||
while (i--) {
|
|
||||||
let sAttrName = oElement.attributes[i].name.toLowerCase();
|
|
||||||
if (!allowedAttributes.includes(sAttrName) && ('class' !== sAttrName || 'mail-body' !== className)) {
|
|
||||||
delAttribute(sAttrName);
|
|
||||||
aAttrsForRemove.push(sAttrName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let value;
|
|
||||||
|
|
||||||
// if ('TABLE' === name || 'TD' === name || 'TH' === name) {
|
|
||||||
if (!oStyle.backgroundImage) {
|
|
||||||
if ('TD' !== name && 'TH' !== name) {
|
|
||||||
['width','height'].forEach(key => {
|
|
||||||
if (hasAttribute(key)) {
|
|
||||||
value = delAttribute(key);
|
|
||||||
oStyle[key] || (oStyle[key] = value.includes('%') ? value : value + 'px');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Make width responsive
|
|
||||||
value = oStyle.width;
|
|
||||||
if (100 < parseInt(value,10) && !oStyle.maxWidth) {
|
|
||||||
oStyle.maxWidth = value;
|
|
||||||
oStyle.width = '100%';
|
|
||||||
}
|
|
||||||
// Make height responsive
|
|
||||||
value = oStyle.removeProperty('height');
|
|
||||||
if (value && !oStyle.maxHeight) {
|
|
||||||
oStyle.maxHeight = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// } else
|
|
||||||
if ('A' === name) {
|
|
||||||
value = oElement.href;
|
|
||||||
if (!/^([a-z]+):/i.test(value)) {
|
|
||||||
setAttribute('data-x-href-broken', value);
|
|
||||||
delAttribute('href');
|
|
||||||
} else {
|
|
||||||
oElement.href = stripTracking(value);
|
|
||||||
if (oElement.href != value) {
|
|
||||||
result.tracking = true;
|
|
||||||
setAttribute('data-x-href-tracking', value);
|
|
||||||
}
|
|
||||||
setAttribute('target', '_blank');
|
|
||||||
// setAttribute('rel', 'external nofollow noopener noreferrer');
|
|
||||||
}
|
|
||||||
setAttribute('tabindex', '-1');
|
|
||||||
aColor && !oElement.style.color && (oElement.style.color = aColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (['CENTER','FORM'].includes(name)) {
|
|
||||||
if (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim())) {
|
|
||||||
('A' !== name || !oElement.querySelector('IMG')) && replaceWithChildren(oElement);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// SVG xlink:href
|
|
||||||
/*
|
|
||||||
if (hasAttribute('xlink:href')) {
|
|
||||||
delAttribute('xlink:href');
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
let skipStyle = false;
|
|
||||||
if (isMsg) {
|
|
||||||
value = isMsg && delAttribute('src');
|
|
||||||
if (value) {
|
|
||||||
if ('IMG' === name) {
|
|
||||||
oElement.loading = 'lazy';
|
|
||||||
let attachment;
|
|
||||||
if (value.startsWith('cid:'))
|
|
||||||
{
|
|
||||||
setAttribute('data-x-src', value);
|
|
||||||
value = value.slice(4);
|
|
||||||
attachment = findAttachmentByCid(value);
|
|
||||||
if (attachment?.download) {
|
|
||||||
oElement.src = attachment.linkPreview();
|
|
||||||
// oElement.title || (oElement.title = '+attachment.fileName);
|
|
||||||
attachment.isInline(true);
|
|
||||||
attachment.isLinked(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if ((attachment = findLocationByCid(value)))
|
|
||||||
{
|
|
||||||
if (attachment.download) {
|
|
||||||
oElement.src = attachment.linkPreview();
|
|
||||||
attachment.isLinked(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (detectHiddenImages
|
|
||||||
&& ((oStyle.maxHeight && 3 > pInt(oStyle.maxHeight)) // TODO: issue with 'in'
|
|
||||||
|| (oStyle.maxWidth && 3 > pInt(oStyle.maxWidth)) // TODO: issue with 'in'
|
|
||||||
|| (oStyle.width && 2 > pInt(oStyle.width))
|
|
||||||
|| [
|
|
||||||
'email.microsoftemail.com/open',
|
|
||||||
'github.com/notifications/beacon/',
|
|
||||||
'/track/open', // mandrillapp.com list-manage.com
|
|
||||||
'google-analytics.com'
|
|
||||||
].filter(uri => value.toLowerCase().includes(uri)).length
|
|
||||||
)) {
|
|
||||||
skipStyle = true;
|
|
||||||
oStyle.display = 'none';
|
|
||||||
// setAttribute('style', 'display:none');
|
|
||||||
setAttribute('data-x-src-hidden', value);
|
|
||||||
// result.tracking = true;
|
|
||||||
}
|
|
||||||
else if (httpre.test(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;
|
|
||||||
oElement.alt || (oElement.alt = src.replace(/^.+\/([^/?]+).*$/, '$1').slice(-20));
|
|
||||||
}
|
|
||||||
else if (value.startsWith('data:image/'))
|
|
||||||
{
|
|
||||||
oElement.src = value;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
setAttribute('data-x-src-broken', value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
setAttribute('data-x-src-broken', value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasAttribute('background')) {
|
|
||||||
oStyle.backgroundImage = 'url("' + delAttribute('background') + '")';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasAttribute('bgcolor')) {
|
|
||||||
oStyle.backgroundColor = delAttribute('bgcolor');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasAttribute('color')) {
|
|
||||||
oStyle.color = delAttribute('color');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!skipStyle) {
|
|
||||||
/*
|
|
||||||
if ('fixed' === oStyle.position) {
|
|
||||||
oStyle.position = 'absolute';
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
oStyle.removeProperty('behavior');
|
|
||||||
oStyle.removeProperty('cursor');
|
|
||||||
oStyle.removeProperty('min-width');
|
|
||||||
|
|
||||||
const
|
|
||||||
urls_remote = [], // 'data-x-style-url'
|
|
||||||
urls_broken = []; // 'data-x-broken-style-src'
|
|
||||||
['backgroundImage', 'listStyleImage', 'content'].forEach(property => {
|
|
||||||
if (oStyle[property]) {
|
|
||||||
let value = oStyle[property],
|
|
||||||
found = value.match(/url\s*\(([^)]+)\)/i);
|
|
||||||
if (found) {
|
|
||||||
oStyle[property] = null;
|
|
||||||
found = found[1].replace(/^["'\s]+|["'\s]+$/g, '');
|
|
||||||
let lowerUrl = found.toLowerCase();
|
|
||||||
if (lowerUrl.startsWith('cid:')) {
|
|
||||||
const attachment = findAttachmentByCid(found);
|
|
||||||
if (attachment?.linkPreview && name) {
|
|
||||||
oStyle[property] = "url('" + attachment.linkPreview() + "')";
|
|
||||||
attachment.isInline(true);
|
|
||||||
attachment.isLinked(true);
|
|
||||||
}
|
|
||||||
} else if (httpre.test(lowerUrl)) {
|
|
||||||
result.hasExternals = true;
|
|
||||||
urls_remote.push([property, found]);
|
|
||||||
} else if (lowerUrl.startsWith('data:image/')) {
|
|
||||||
oStyle[property] = value;
|
|
||||||
} else {
|
|
||||||
urls_broken.push([property, found]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// oStyle.removeProperty('background-image');
|
|
||||||
// oStyle.removeProperty('list-style-image');
|
|
||||||
|
|
||||||
if (urls_remote.length) {
|
|
||||||
setAttribute('data-x-style-url', JSON.stringify(urls_remote));
|
|
||||||
}
|
|
||||||
if (urls_broken.length) {
|
|
||||||
setAttribute('data-x-style-broken-urls', JSON.stringify(urls_broken));
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/the-djmaze/snappymail/issues/1082
|
|
||||||
if (minFontSize > pInt(oStyle.fontSize)) {
|
|
||||||
oStyle.fontSize = minFontSize + "px";
|
|
||||||
// oStyle.removeProperty('font-size');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removes background and color
|
|
||||||
// Many e-mails incorrectly only define one, not both
|
|
||||||
// And in dark theme mode this kills the readability
|
|
||||||
if (SettingsUserStore.removeColors()) {
|
|
||||||
oStyle.removeProperty('background-color');
|
|
||||||
oStyle.removeProperty('background-image');
|
|
||||||
oStyle.removeProperty('color');
|
|
||||||
}
|
|
||||||
|
|
||||||
oStyle.cssText && (oStyle.cssText = cleanCSS(oStyle.cssText));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (debug && aAttrsForRemove.length) {
|
|
||||||
setAttribute('data-removed-attrs', aAttrsForRemove.join(', '));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
isMsg && blockquoteSwitcher();
|
|
||||||
|
|
||||||
// return tmpl.content.firstChild;
|
|
||||||
result.html = tmpl.innerHTML.trim();
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} html
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
htmlToPlain = html => {
|
|
||||||
if (SettingsUserStore.markdown()) {
|
|
||||||
return htmlToMarkdown(html);
|
|
||||||
}
|
|
||||||
const
|
|
||||||
hr = '⎯'.repeat(64),
|
|
||||||
forEach = (selector, fn) => tmpl.content.querySelectorAll(selector).forEach(fn),
|
|
||||||
blockquotes = node => {
|
|
||||||
let bq;
|
|
||||||
while ((bq = node.querySelector('blockquote'))) {
|
|
||||||
// Convert child blockquote first
|
|
||||||
blockquotes(bq);
|
|
||||||
// Convert blockquote
|
|
||||||
// bq.innerHTML = '\n' + ('\n' + bq.innerHTML.replace(/\n{3,}/gm, '\n\n').trim() + '\n').replace(/^/gm, '> ');
|
|
||||||
// replaceWithChildren(bq);
|
|
||||||
bq.replaceWith(
|
|
||||||
'\n' + ('\n' + bq.textContent.replace(/\n{3,}/g, '\n\n').trim() + '\n').replace(/^/gm, '> ')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
html = html
|
|
||||||
.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gim, (...args) =>
|
|
||||||
1 < args.length ? args[1].toString().replace(/\n/g, '<br>') : '')
|
|
||||||
// Remove line duplication
|
|
||||||
.replace(/<br><\/div>/gi, '</div>')
|
|
||||||
.replace(/\r?\n/g, '')
|
|
||||||
.replace(/\s+/gm, ' ');
|
|
||||||
|
|
||||||
while (/<(div|tr)[\s>]/i.test(html)) {
|
|
||||||
html = html.replace(/\n*<(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n');
|
|
||||||
}
|
|
||||||
while (/<\/(div|tr)[\s>]/i.test(html)) {
|
|
||||||
html = html.replace(/\n*<\/(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpl.innerHTML = html
|
|
||||||
.replace(/<t[dh](\s[\s\S]*?)?>/gi, '\t')
|
|
||||||
.replace(/<\/tr(\s[\s\S]*?)?>/gi, '\n');
|
|
||||||
|
|
||||||
forEach('style', node => node.remove());
|
|
||||||
|
|
||||||
// lines
|
|
||||||
forEach('hr', node => node.replaceWith(`\n\n${hr}\n\n`));
|
|
||||||
|
|
||||||
// headings
|
|
||||||
forEach('h1,h2,h3,h4,h5,h6', h => h.replaceWith(`\n\n${'#'.repeat(h.tagName[1])} ${h.textContent}\n\n`));
|
|
||||||
|
|
||||||
// paragraphs
|
|
||||||
forEach('p', node => {
|
|
||||||
node.prepend('\n\n');
|
|
||||||
if ('' == node.textContent.trim()) {
|
|
||||||
node.remove();
|
|
||||||
} else {
|
|
||||||
node.after('\n\n');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// proper indenting and numbering of (un)ordered lists
|
|
||||||
forEach('ol,ul', node => {
|
|
||||||
let prefix = '',
|
|
||||||
parent = node,
|
|
||||||
ordered = 'OL' == node.tagName,
|
|
||||||
i = 0;
|
|
||||||
while ((parent = parent?.parentNode?.closest?.('ol,ul'))) {
|
|
||||||
prefix = ' ' + prefix;
|
|
||||||
}
|
|
||||||
node.querySelectorAll(':scope > li').forEach(li => {
|
|
||||||
li.prepend('\n' + prefix + (ordered ? `${++i}. ` : ' * '));
|
|
||||||
});
|
|
||||||
node.prepend('\n\n');
|
|
||||||
node.after('\n\n');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Convert anchors
|
|
||||||
forEach('a', a => {
|
|
||||||
let txt = a.textContent, href = a.href;
|
|
||||||
return a.replaceWith(
|
|
||||||
txt.replace(/[\s()-]+/g, '').includes(href.replace(/^[a-z]:/, '').replace(/[\s()-]+/g, ''))
|
|
||||||
? txt
|
|
||||||
: txt + ' ' + href + ' '
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Bold
|
|
||||||
forEach('b,strong', b => b.replaceWith(`**${b.textContent}**`));
|
|
||||||
// Italic
|
|
||||||
forEach('i,em', i => i.replaceWith(`*${i.textContent}*`));
|
|
||||||
|
|
||||||
// Convert line-breaks
|
|
||||||
tmpl.innerHTML = tmpl.innerHTML
|
|
||||||
.replace(/\n{3,}/gm, '\n\n')
|
|
||||||
.replace(/\n<br[^>]*>/g, '\n')
|
|
||||||
.replace(/<br[^>]*>\n/g, '\n');
|
|
||||||
forEach('br', br => br.replaceWith('\n'));
|
|
||||||
|
|
||||||
// Blockquotes must be last
|
|
||||||
blockquotes(tmpl.content);
|
|
||||||
|
|
||||||
return (tmpl.content.textContent || '').trim();
|
|
||||||
},
|
|
||||||
|
|
||||||
htmlToMarkdown = html => {
|
|
||||||
tmpl.innerHTML = html;
|
|
||||||
return turndown.turndown(tmpl.content);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} plain
|
|
||||||
* @param {boolean} findEmailAndLinksInText = false
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
plainToHtml = plain => {
|
|
||||||
plain = plain.toString()
|
|
||||||
.replace(/\r/g, '')
|
|
||||||
.replace(/^>[> ]>+/gm, ([match]) => (match ? match.replace(/[ ]+/g, '') : match))
|
|
||||||
// https://github.com/the-djmaze/snappymail/issues/900
|
|
||||||
.replace(/\u2028/g,' ');
|
|
||||||
|
|
||||||
let bIn = false,
|
|
||||||
bDo = true,
|
|
||||||
bStart = true,
|
|
||||||
aNextText = [],
|
|
||||||
aText = plain.split('\n');
|
|
||||||
|
|
||||||
do {
|
|
||||||
bDo = false;
|
|
||||||
aNextText = [];
|
|
||||||
aText.forEach(sLine => {
|
|
||||||
bStart = '>' === sLine.slice(0, 1);
|
|
||||||
if (bStart && !bIn) {
|
|
||||||
bDo = true;
|
|
||||||
bIn = true;
|
|
||||||
aNextText.push('~~~blockquote~~~');
|
|
||||||
aNextText.push(sLine.slice(1));
|
|
||||||
} else if (!bStart && bIn) {
|
|
||||||
if (sLine) {
|
|
||||||
bIn = false;
|
|
||||||
aNextText.push('~~~/blockquote~~~');
|
|
||||||
aNextText.push(sLine);
|
|
||||||
} else {
|
|
||||||
aNextText.push(sLine);
|
|
||||||
}
|
|
||||||
} else if (bStart && bIn) {
|
|
||||||
aNextText.push(sLine.slice(1));
|
|
||||||
} else {
|
|
||||||
aNextText.push(sLine);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (bIn) {
|
|
||||||
bIn = false;
|
|
||||||
aNextText.push('~~~/blockquote~~~');
|
|
||||||
}
|
|
||||||
|
|
||||||
aText = aNextText;
|
|
||||||
} while (bDo);
|
|
||||||
|
|
||||||
tmpl.innerHTML = aText.join('\n')
|
|
||||||
// .replace(/~~~\/blockquote~~~\n~~~blockquote~~~/g, '\n')
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(urlRegExp, (...m) => {
|
|
||||||
m[0] = stripTracking(m[0]);
|
|
||||||
return `<a href="${m[0]}" target="_blank">${m[0]}</a>`;
|
|
||||||
})
|
|
||||||
.replace(email, '$1<a href="mailto:$2">$2</a>')
|
|
||||||
.replace(tel, '<a href="$1">$1</a>')
|
|
||||||
.replace(/~~~blockquote~~~\s*/g, '<blockquote>')
|
|
||||||
.replace(/\s*~~~\/blockquote~~~/g, '</blockquote>')
|
|
||||||
.replace(/\n/g, '<br>');
|
|
||||||
blockquoteSwitcher();
|
|
||||||
return tmpl.innerHTML.trim();
|
|
||||||
};
|
|
||||||
|
|
||||||
rl.Utils = {
|
|
||||||
cleanHtml: cleanHtml,
|
|
||||||
htmlToPlain: htmlToPlain,
|
|
||||||
plainToHtml: plainToHtml,
|
|
||||||
htmlToMarkdown: htmlToMarkdown
|
|
||||||
// markdownToHtml: md => marked.parse(md)
|
|
||||||
};
|
|
||||||
|
|
@ -1,51 +1,46 @@
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
import { createCKEditor } from 'External/CKEditor.js';
|
||||||
|
|
||||||
export const
|
class HtmlEditor {
|
||||||
WYSIWYGS = ko.observableArray();
|
editor;
|
||||||
|
blurTimer = 0;
|
||||||
|
|
||||||
WYSIWYGS.push({
|
__resizable = false;
|
||||||
name: 'Squire',
|
__inited = false;
|
||||||
construct: (owner, container, onReady) => onReady(new SquireUI(container))
|
|
||||||
});
|
|
||||||
|
|
||||||
rl.registerWYSIWYG = (name, construct) => WYSIWYGS.push({name, construct});
|
onBlur = null;
|
||||||
|
onReady = null;
|
||||||
|
onModeChange = null;
|
||||||
|
|
||||||
|
element;
|
||||||
|
|
||||||
|
resize;
|
||||||
|
|
||||||
export class HtmlEditor {
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} element
|
* @param {Object} element
|
||||||
* @param {Function=} onBlur
|
* @param {Function=} onBlur
|
||||||
* @param {Function=} onReady
|
* @param {Function=} onReady
|
||||||
* @param {Function=} onModeChange
|
* @param {Function=} onModeChange
|
||||||
*/
|
*/
|
||||||
constructor(element, onReady = null, onModeChange = null, onBlur = null) {
|
constructor(element, onBlur = null, onReady = null, onModeChange = null) {
|
||||||
this.blurTimer = 0;
|
|
||||||
|
|
||||||
this.onBlur = onBlur;
|
this.onBlur = onBlur;
|
||||||
|
this.onReady = onReady;
|
||||||
this.onModeChange = onModeChange;
|
this.onModeChange = onModeChange;
|
||||||
|
|
||||||
if (element) {
|
this.element = element;
|
||||||
onReady = onReady ? [onReady] : [];
|
|
||||||
this.onReady = fn => onReady.push(fn);
|
this.resize = (() => {
|
||||||
const which = SettingsUserStore.editorWysiwyg(),
|
try {
|
||||||
wysiwyg = WYSIWYGS.find(item => which == item.name) || WYSIWYGS.find(item => 'Squire' == item.name);
|
this.editor && this.__resizable && this.editor.resize(element.clientWidth, element.clientHeight);
|
||||||
wysiwyg.construct(this, element, editor => setTimeout(()=>{
|
} catch (e) {} // eslint-disable-line no-empty
|
||||||
this.editor = editor;
|
}).throttle(100);
|
||||||
editor.on('blur', () => this.blurTrigger());
|
|
||||||
editor.on('focus', () => clearTimeout(this.blurTimer));
|
this.init();
|
||||||
editor.on('mode', () => {
|
|
||||||
this.blurTrigger();
|
|
||||||
this.onModeChange?.(!this.isPlain());
|
|
||||||
});
|
|
||||||
this.onReady = fn => fn();
|
|
||||||
onReady.forEach(fn => fn());
|
|
||||||
},1));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
blurTrigger() {
|
blurTrigger() {
|
||||||
if (this.onBlur) {
|
if (this.onBlur) {
|
||||||
clearTimeout(this.blurTimer);
|
clearTimeout(this.blurTimer);
|
||||||
this.blurTimer = setTimeout(() => this.onBlur?.(), 200);
|
this.blurTimer = setTimeout(() => this.onBlur && this.onBlur(), 200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,23 +48,16 @@ export class HtmlEditor {
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
isHtml() {
|
isHtml() {
|
||||||
return this.editor ? !this.isPlain() : false;
|
return this.editor ? 'plain' !== this.editor.mode : false;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
isPlain() {
|
|
||||||
return this.editor ? 'plain' === this.editor.mode : false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
clearCachedSignature() {
|
clearCachedSignature() {
|
||||||
this.onReady(() => this.editor.execCommand('insertSignature', {
|
this.editor && this.editor.execCommand('insertSignature', {
|
||||||
clearCache: true
|
clearCache: true
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -79,83 +67,158 @@ export class HtmlEditor {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
setSignature(signature, html, insertBefore = false) {
|
setSignature(signature, html, insertBefore = false) {
|
||||||
this.onReady(() => this.editor.execCommand('insertSignature', {
|
this.editor && this.editor.execCommand('insertSignature', {
|
||||||
isHtml: html,
|
isHtml: html,
|
||||||
insertBefore: insertBefore,
|
insertBefore: insertBefore,
|
||||||
signature: signature
|
signature: signature
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean=} wrapIsHtml = false
|
* @param {boolean=} wrapIsHtml = false
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getData() {
|
getData(wrapIsHtml = false) {
|
||||||
let result = '';
|
let result = '';
|
||||||
if (this.editor) {
|
if (this.editor) {
|
||||||
try {
|
try {
|
||||||
if (this.isPlain()) {
|
if ('plain' === this.editor.mode && this.editor.plugins.plain && this.editor.__plain) {
|
||||||
result = this.editor.getPlainData();
|
result = this.editor.__plain.getRawData();
|
||||||
} else {
|
} else {
|
||||||
result = this.editor.getData();
|
result = wrapIsHtml
|
||||||
|
? '<div data-html-editor-font-wrapper="true" style="font-family: arial, sans-serif; font-size: 13px;">' +
|
||||||
|
this.editor.getData() +
|
||||||
|
'</div>'
|
||||||
|
: this.editor.getData();
|
||||||
}
|
}
|
||||||
} catch (e) {} // eslint-disable-line no-empty
|
} catch (e) {} // eslint-disable-line no-empty
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param {boolean=} wrapIsHtml = false
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getDataWithHtmlMark() {
|
getDataWithHtmlMark(wrapIsHtml = false) {
|
||||||
return (this.isHtml() ? ':HTML:' : '') + this.getData();
|
return (this.isHtml() ? ':HTML:' : '') + this.getData(wrapIsHtml);
|
||||||
}
|
}
|
||||||
|
|
||||||
modeWysiwyg() {
|
modeToggle(plain) {
|
||||||
this.onReady(() => this.editor.setMode('wysiwyg'));
|
if (this.editor) {
|
||||||
}
|
|
||||||
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 {
|
try {
|
||||||
editor.setMode(mode);
|
if (plain) {
|
||||||
if (this.isPlain()) {
|
if ('plain' === this.editor.mode) {
|
||||||
editor.setPlainData(data);
|
this.editor.setMode('wysiwyg');
|
||||||
} else {
|
}
|
||||||
editor.setData(data);
|
} else if ('wysiwyg' === this.editor.mode) {
|
||||||
|
this.editor.setMode('plain');
|
||||||
}
|
}
|
||||||
} catch (e) { console.error(e); }
|
} catch (e) {} // eslint-disable-line no-empty
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setHtml(html) {
|
setHtmlOrPlain(text, focus) {
|
||||||
this.setData('wysiwyg', html/*.replace(/<p[^>]*><\/p>/gi, '')*/);
|
if (':HTML:' === text.substr(0, 6)) {
|
||||||
|
this.setHtml(text.substr(6), focus);
|
||||||
|
} else {
|
||||||
|
this.setPlain(text, focus);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setPlain(txt) {
|
setHtml(html, focus) {
|
||||||
this.setData('plain', txt);
|
if (this.editor && this.__inited) {
|
||||||
|
this.clearCachedSignature();
|
||||||
|
|
||||||
|
this.modeToggle(true);
|
||||||
|
|
||||||
|
html = html.replace(/<p[^>]*><\/p>/gi, '');
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.editor.setData(html);
|
||||||
|
} catch (e) {} // eslint-disable-line no-empty
|
||||||
|
|
||||||
|
focus && this.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPlain(plain, focus) {
|
||||||
|
if (this.editor && this.__inited) {
|
||||||
|
this.clearCachedSignature();
|
||||||
|
|
||||||
|
this.modeToggle(false);
|
||||||
|
if ('plain' === this.editor.mode && this.editor.plugins.plain && this.editor.__plain) {
|
||||||
|
this.editor.__plain.setRawData(plain);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
this.editor.setData(plain);
|
||||||
|
} catch (e) {} // eslint-disable-line no-empty
|
||||||
|
}
|
||||||
|
|
||||||
|
focus && this.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if (this.element && !this.editor) {
|
||||||
|
const onReady = () => {
|
||||||
|
if (this.editor.removeMenuItem) {
|
||||||
|
this.editor.removeMenuItem('cut');
|
||||||
|
this.editor.removeMenuItem('copy');
|
||||||
|
this.editor.removeMenuItem('paste');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.__resizable = true;
|
||||||
|
this.__inited = true;
|
||||||
|
|
||||||
|
this.resize();
|
||||||
|
|
||||||
|
this.onReady && this.onReady();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (window.CKEDITOR) {
|
||||||
|
this.editor = createCKEditor(this.element);
|
||||||
|
this.editor.on('instanceReady', onReady);
|
||||||
|
} else {
|
||||||
|
this.editor = new SquireUI(this.element, this.editor);
|
||||||
|
setTimeout(onReady,1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.on('blur', () => this.blurTrigger());
|
||||||
|
this.editor.on('focus', () => this.blurTimer && clearTimeout(this.blurTimer));
|
||||||
|
this.editor.on('mode', () => {
|
||||||
|
this.blurTrigger();
|
||||||
|
this.onModeChange && this.onModeChange('plain' !== this.editor.mode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
this.onReady(() => this.editor.focus());
|
try {
|
||||||
|
this.editor && this.editor.focus();
|
||||||
|
} catch (e) {} // eslint-disable-line no-empty
|
||||||
|
}
|
||||||
|
|
||||||
|
hasFocus() {
|
||||||
|
try {
|
||||||
|
return this.editor && !!this.editor.focusManager.hasFocus;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
blur() {
|
blur() {
|
||||||
this.onReady(() => this.editor.blur());
|
try {
|
||||||
|
this.editor && this.editor.focusManager.blur(true);
|
||||||
|
} catch (e) {} // eslint-disable-line no-empty
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear(focus) {
|
||||||
this.onReady(() => this.isPlain() ? this.setPlain('') : this.setHtml(''));
|
this.setHtml('', focus);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { HtmlEditor, HtmlEditor as default };
|
||||||
|
|
|
||||||
|
|
@ -1,143 +1,361 @@
|
||||||
import { pInt } from 'Common/Utils';
|
import { pString, pInt } from 'Common/Utils';
|
||||||
import { doc, Settings, SettingsAdmin } from 'Common/Globals';
|
|
||||||
|
|
||||||
const
|
const
|
||||||
BASE = doc.location.pathname.replace(/\/+$/,'') + '/',
|
Settings = rl.settings,
|
||||||
|
ROOT = './',
|
||||||
HASH_PREFIX = '#/',
|
HASH_PREFIX = '#/',
|
||||||
|
SERVER_PREFIX = './?',
|
||||||
adminPath = () => rl.adminArea() && !SettingsAdmin('host'),
|
|
||||||
|
|
||||||
prefix = () => BASE + '?' + (adminPath() ? SettingsAdmin('path') : '');
|
|
||||||
|
|
||||||
export const
|
|
||||||
SUB_QUERY_PREFIX = '&q[]=',
|
SUB_QUERY_PREFIX = '&q[]=',
|
||||||
|
VERSION = Settings.app('version'),
|
||||||
|
VERSION_PREFIX = Settings.app('webVersionPath') || 'snappymail/v/' + VERSION + '/',
|
||||||
|
|
||||||
/**
|
getHash = () => Settings.get('AuthAccountHash') || '0';
|
||||||
* @param {string=} startupUrl
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
root = () => HASH_PREFIX,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
logoutLink = () => adminPath() ? prefix() : BASE,
|
export function subQueryPrefix() {
|
||||||
|
return SUB_QUERY_PREFIX;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} type
|
* @param {string=} startupUrl
|
||||||
* @param {string} hash
|
* @returns {string}
|
||||||
* @param {string=} customSpecSuffix
|
*/
|
||||||
* @returns {string}
|
export function root(startupUrl = '') {
|
||||||
*/
|
return HASH_PREFIX + pString(startupUrl);
|
||||||
serverRequestRaw = (type, hash) =>
|
}
|
||||||
BASE + '?/Raw/' + SUB_QUERY_PREFIX + '/'
|
|
||||||
+ '0/' // Settings.get('accountHash') ?
|
|
||||||
+ (type
|
|
||||||
? type + '/' + (hash ? SUB_QUERY_PREFIX + '/' + hash : '')
|
|
||||||
: ''),
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} download
|
* @returns {string}
|
||||||
* @returns {string}
|
*/
|
||||||
*/
|
export function rootAdmin() {
|
||||||
attachmentDownload = (download) =>
|
return Settings.app('adminHostUse') ? ROOT : SERVER_PREFIX + (Settings.app('adminPath') || 'admin');
|
||||||
serverRequestRaw('Download', download),
|
}
|
||||||
|
|
||||||
proxy = url =>
|
/**
|
||||||
BASE + '?/ProxyExternal/'
|
* @returns {string}
|
||||||
// + btoa(JSON.stringify([token,url]).replace(/ /g, '%20')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
*/
|
||||||
+ btoa(url.replace(/ /g, '%20')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
export function rootUser() {
|
||||||
// + b64EncodeJSONSafe(url.replace(/ /g, '%20')),
|
return ROOT;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} type
|
* @param {string} type
|
||||||
* @returns {string}
|
* @param {string} download
|
||||||
*/
|
* @param {string=} customSpecSuffix
|
||||||
serverRequest = type => prefix() + '/' + type + '/' + SUB_QUERY_PREFIX + '/0/',
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function attachmentRaw(type, download, customSpecSuffix) {
|
||||||
|
customSpecSuffix = undefined === customSpecSuffix ? getHash() : customSpecSuffix;
|
||||||
|
return (
|
||||||
|
SERVER_PREFIX +
|
||||||
|
'/Raw/' +
|
||||||
|
SUB_QUERY_PREFIX +
|
||||||
|
'/' +
|
||||||
|
customSpecSuffix +
|
||||||
|
'/' +
|
||||||
|
type +
|
||||||
|
'/' +
|
||||||
|
SUB_QUERY_PREFIX +
|
||||||
|
'/' +
|
||||||
|
download
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Is '?/Css/0/Admin' needed?
|
/**
|
||||||
cssLink = theme => BASE + '?/Css/0/User/-/' + encodeURI(theme) + '/-/' + Date.now() + '/Hash/-/Json/',
|
* @param {string} download
|
||||||
|
* @param {string=} customSpecSuffix
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function attachmentDownload(download, customSpecSuffix) {
|
||||||
|
return attachmentRaw('Download', download, customSpecSuffix);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} lang
|
* @param {string} download
|
||||||
* @param {boolean} isAdmin
|
* @param {string=} customSpecSuffix
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
langLink = (lang, isAdmin) =>
|
export function attachmentPreview(download, customSpecSuffix) {
|
||||||
BASE + '?/Lang/0/' + (isAdmin ? 'Admin' : 'App')
|
return attachmentRaw('View', download, customSpecSuffix);
|
||||||
+ '/' + encodeURI(lang)
|
}
|
||||||
+ '/' + Settings.app('version') + '/',
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} path
|
* @param {string} download
|
||||||
* @returns {string}
|
* @param {string=} customSpecSuffix
|
||||||
*/
|
* @returns {string}
|
||||||
staticLink = path => Settings.app('webVersionPath') + 'static/' + path,
|
*/
|
||||||
|
export function attachmentThumbnailPreview(download, customSpecSuffix) {
|
||||||
|
return attachmentRaw('ViewThumbnail', download, customSpecSuffix);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} theme
|
* @param {string} download
|
||||||
* @returns {string}
|
* @param {string=} customSpecSuffix
|
||||||
*/
|
* @returns {string}
|
||||||
themePreviewLink = theme => {
|
*/
|
||||||
if (theme.endsWith('@nextcloud')) {
|
export function attachmentPreviewAsPlain(download, customSpecSuffix) {
|
||||||
theme = theme.slice(0, theme.length - 10).trim();
|
return attachmentRaw('ViewAsPlain', download, customSpecSuffix);
|
||||||
return parent.OC.webroot + '/themes/' + encodeURI(theme) + '/snappymail/preview.png';
|
}
|
||||||
}
|
|
||||||
let path = 'webVersionPath';
|
|
||||||
if (theme.endsWith('@custom')) {
|
|
||||||
theme = theme.slice(0, theme.length - 7).trim();
|
|
||||||
path = 'webPath';
|
|
||||||
}
|
|
||||||
return Settings.app(path) + 'themes/' + encodeURI(theme) + '/images/preview.png';
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} inboxFolderName = 'INBOX'
|
* @param {string} download
|
||||||
* @returns {string}
|
* @param {string=} customSpecSuffix
|
||||||
*/
|
* @returns {string}
|
||||||
mailbox = (inboxFolderName = 'INBOX') => HASH_PREFIX + 'mailbox/' + inboxFolderName,
|
*/
|
||||||
|
export function attachmentFramed(download, customSpecSuffix) {
|
||||||
|
return attachmentRaw('FramedView', download, customSpecSuffix);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string=} screenName = ''
|
* @param {string} type
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
settings = (screenName = '') => HASH_PREFIX + 'settings' + (screenName ? '/' + screenName : ''),
|
export function serverRequest(type) {
|
||||||
|
return SERVER_PREFIX + '/' + type + '/' + SUB_QUERY_PREFIX + '/' + getHash() + '/';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string=} screenName
|
* @returns {string}
|
||||||
* @returns {string}
|
*/
|
||||||
*/
|
export function upload() {
|
||||||
admin = screenName => HASH_PREFIX + (
|
return serverRequest('Upload');
|
||||||
'AdminDomains' == screenName ? 'domains'
|
}
|
||||||
: 'AdminSecurity' == screenName ? 'security'
|
|
||||||
: ''
|
|
||||||
),
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} folder
|
* @returns {string}
|
||||||
* @param {number=} page = 1
|
*/
|
||||||
* @param {string=} search = ''
|
export function uploadContacts() {
|
||||||
* @param {number=} threadUid = 0
|
return serverRequest('UploadContacts');
|
||||||
* @returns {string}
|
}
|
||||||
*/
|
|
||||||
mailBox = (folder, page, search, threadUid, messageUid) => {
|
|
||||||
let result = [HASH_PREFIX + 'mailbox'];
|
|
||||||
|
|
||||||
if (folder) {
|
/**
|
||||||
result.push(folder + (threadUid ? '~' + threadUid : ''));
|
* @returns {string}
|
||||||
}
|
*/
|
||||||
|
export function uploadBackground() {
|
||||||
|
return serverRequest('UploadBackground');
|
||||||
|
}
|
||||||
|
|
||||||
if (messageUid) {
|
/**
|
||||||
result.push('m' + messageUid);
|
* @returns {string}
|
||||||
} else {
|
*/
|
||||||
page = pInt(page, 1);
|
export function append() {
|
||||||
if (1 < page) {
|
return serverRequest('Append');
|
||||||
result.push('p' + page);
|
}
|
||||||
}
|
|
||||||
search && result.push(encodeURI(search));
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.join('/');
|
/**
|
||||||
},
|
* @param {string} email
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function change(email) {
|
||||||
|
return serverRequest('Change') + encodeURIComponent(email) + '/';
|
||||||
|
}
|
||||||
|
|
||||||
mailBoxMessage = (folder, messageUid) => mailBox(folder, 1, '', 0, pInt(messageUid));
|
/**
|
||||||
|
* @param {string} requestHash
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function messageViewLink(requestHash) {
|
||||||
|
return (
|
||||||
|
SERVER_PREFIX +
|
||||||
|
'/Raw/' +
|
||||||
|
SUB_QUERY_PREFIX +
|
||||||
|
'/' +
|
||||||
|
getHash() +
|
||||||
|
'/ViewAsPlain/' +
|
||||||
|
SUB_QUERY_PREFIX +
|
||||||
|
'/' +
|
||||||
|
requestHash
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} requestHash
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function messageDownloadLink(requestHash) {
|
||||||
|
return (
|
||||||
|
SERVER_PREFIX + '/Raw/' + SUB_QUERY_PREFIX + '/' + getHash() + '/Download/' + SUB_QUERY_PREFIX + '/' + requestHash
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} email
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function avatarLink(email) {
|
||||||
|
return SERVER_PREFIX + '/Raw/0/Avatar/' + encodeURIComponent(email) + '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} hash
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function publicLink(hash) {
|
||||||
|
return SERVER_PREFIX + '/Raw/0/Public/' + hash + '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} hash
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function userBackground(hash) {
|
||||||
|
return (
|
||||||
|
SERVER_PREFIX + '/Raw/' + SUB_QUERY_PREFIX + '/' + getHash() + '/UserBackground/' + SUB_QUERY_PREFIX + '/' + hash
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} lang
|
||||||
|
* @param {boolean} isAdmin
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function langLink(lang, isAdmin) {
|
||||||
|
return SERVER_PREFIX + '/Lang/0/' + (isAdmin ? 'Admin' : 'App') + '/' + encodeURI(lang) + '/' + VERSION + '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function exportContactsVcf() {
|
||||||
|
return SERVER_PREFIX + '/Raw/' + SUB_QUERY_PREFIX + '/' + getHash() + '/ContactsVcf/';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function exportContactsCsv() {
|
||||||
|
return SERVER_PREFIX + '/Raw/' + SUB_QUERY_PREFIX + '/' + getHash() + '/ContactsCsv/';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} path
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function staticPrefix(path) {
|
||||||
|
return VERSION_PREFIX + 'static/' + path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} fileName
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function sound(fileName) {
|
||||||
|
return staticPrefix('sounds/' + fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function notificationMailIcon() {
|
||||||
|
return staticPrefix('css/images/icon-message-notification.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function openPgpJs() {
|
||||||
|
return staticPrefix('js/min/openpgp.min.js');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function openPgpWorkerJs() {
|
||||||
|
return staticPrefix('js/min/openpgp.worker.min.js');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function openPgpWorkerPath() {
|
||||||
|
return staticPrefix('js/min/');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} theme
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function themePreviewLink(theme) {
|
||||||
|
let prefix = VERSION_PREFIX;
|
||||||
|
if ('@custom' === theme.substr(-7)) {
|
||||||
|
theme = theme.substr(0, theme.length - 7).trim();
|
||||||
|
prefix = Settings.app('webPath') || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return prefix + 'themes/' + encodeURI(theme) + '/images/preview.png';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} inboxFolderName = 'INBOX'
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function inbox(inboxFolderName = 'INBOX') {
|
||||||
|
return HASH_PREFIX + 'mailbox/' + inboxFolderName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string=} screenName = ''
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function settings(screenName = '') {
|
||||||
|
return HASH_PREFIX + 'settings' + (screenName ? '/' + screenName : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function about() {
|
||||||
|
return HASH_PREFIX + 'about';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} screenName
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function admin(screenName) {
|
||||||
|
let result = HASH_PREFIX;
|
||||||
|
switch (screenName) {
|
||||||
|
case 'AdminDomains':
|
||||||
|
result += 'domains';
|
||||||
|
break;
|
||||||
|
case 'AdminSecurity':
|
||||||
|
result += 'security';
|
||||||
|
break;
|
||||||
|
// no default
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} folder
|
||||||
|
* @param {number=} page = 1
|
||||||
|
* @param {string=} search = ''
|
||||||
|
* @param {string=} threadUid = ''
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function mailBox(folder, page = 1, search = '', threadUid = '') {
|
||||||
|
page = pInt(page, 1);
|
||||||
|
search = pString(search);
|
||||||
|
|
||||||
|
let result = HASH_PREFIX + 'mailbox/';
|
||||||
|
|
||||||
|
if (folder) {
|
||||||
|
const resultThreadUid = pInt(threadUid);
|
||||||
|
result += encodeURI(folder) + (0 < resultThreadUid ? '~' + resultThreadUid : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (1 < page) {
|
||||||
|
result = result.replace(/[/]+$/, '');
|
||||||
|
result += '/p' + page;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
result = result.replace(/[/]+$/, '');
|
||||||
|
result += '/' + encodeURI(search);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
|
||||||
61
dev/Common/Momentor.js
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { i18n } from 'Common/Translator';
|
||||||
|
|
||||||
|
export function format(timeStampInUTC, formatStr) {
|
||||||
|
const now = Date.now(),
|
||||||
|
time = 0 < timeStampInUTC ? Math.min(now, timeStampInUTC * 1000) : (0 === timeStampInUTC ? now : 0);
|
||||||
|
|
||||||
|
if (31536000000 < time) {
|
||||||
|
const m = new Date(time);
|
||||||
|
switch (formatStr) {
|
||||||
|
case 'FROMNOW':
|
||||||
|
return m.fromNow();
|
||||||
|
case 'SHORT': {
|
||||||
|
if (4 >= (now - time) / 3600000)
|
||||||
|
return m.fromNow();
|
||||||
|
const mt = m.getTime(), date = new Date,
|
||||||
|
dt = date.setHours(0,0,0,0);
|
||||||
|
if (mt > dt)
|
||||||
|
return i18n('MESSAGE_LIST/TODAY_AT', {TIME: m.format('LT')});
|
||||||
|
if (mt > dt - 86400000)
|
||||||
|
return i18n('MESSAGE_LIST/YESTERDAY_AT', {TIME: m.format('LT')});
|
||||||
|
if (date.getFullYear() === m.getFullYear())
|
||||||
|
return m.format('d M');
|
||||||
|
return m.format('LL');
|
||||||
|
}
|
||||||
|
case 'FULL':
|
||||||
|
return m.format('LLL');
|
||||||
|
default:
|
||||||
|
return m.format(formatStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function timeToNode(element, time) {
|
||||||
|
try {
|
||||||
|
time = time || (Date.parse(element.dateTime) / 1000);
|
||||||
|
if (time) {
|
||||||
|
let key, m = new Date(time * 1000);
|
||||||
|
element.dateTime = m.format('Y-m-d\\TH:i:s');
|
||||||
|
|
||||||
|
key = element.dataset.momentFormat;
|
||||||
|
if (key) {
|
||||||
|
element.textContent = format(time, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
key = element.dataset.momentFormatTitle;
|
||||||
|
if (key) {
|
||||||
|
element.title = format(time, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// prevent knockout crashes
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener('reload-time', () => setTimeout(() =>
|
||||||
|
document.querySelectorAll('[data-bind*="moment:"]').forEach(element => timeToNode(element))
|
||||||
|
, 1)
|
||||||
|
);
|
||||||
|
|
@ -1,19 +1,25 @@
|
||||||
import { settingsAddViewModel } from 'Screen/AbstractSettings';
|
|
||||||
import { SettingsGet } from 'Common/Globals';
|
|
||||||
import { AbstractViewPopup } from 'Knoin/AbstractViews';
|
|
||||||
|
|
||||||
const USER_VIEW_MODELS_HOOKS = [],
|
const USER_VIEW_MODELS_HOOKS = [],
|
||||||
ADMIN_VIEW_MODELS_HOOKS = [];
|
ADMIN_VIEW_MODELS_HOOKS = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
* @returns {?}
|
||||||
|
*/
|
||||||
|
export function mainSettingsGet(name) {
|
||||||
|
return rl.settings.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Function} callback
|
* @param {Function} callback
|
||||||
* @param {string} action
|
* @param {string} action
|
||||||
* @param {Object=} parameters
|
* @param {Object=} parameters
|
||||||
* @param {?number=} timeout
|
* @param {?number=} timeout
|
||||||
*/
|
*/
|
||||||
rl.pluginRemoteRequest = (callback, action, parameters, timeout) => {
|
export function remoteRequest(callback, action, parameters, timeout) {
|
||||||
rl.app.Remote.request('Plugin' + action, callback, parameters, timeout);
|
if (rl.app) {
|
||||||
};
|
rl.app.remote().defaultRequest(callback, 'Plugin' + action, parameters, timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Function} SettingsViewModelClass
|
* @param {Function} SettingsViewModelClass
|
||||||
|
|
@ -21,9 +27,9 @@ rl.pluginRemoteRequest = (callback, action, parameters, timeout) => {
|
||||||
* @param {string} template
|
* @param {string} template
|
||||||
* @param {string} route
|
* @param {string} route
|
||||||
*/
|
*/
|
||||||
rl.addSettingsViewModel = (SettingsViewModelClass, template, labelName, route) => {
|
export function addSettingsViewModel(SettingsViewModelClass, template, labelName, route) {
|
||||||
USER_VIEW_MODELS_HOOKS.push([SettingsViewModelClass, template, labelName, route]);
|
USER_VIEW_MODELS_HOOKS.push([SettingsViewModelClass, template, labelName, route]);
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Function} SettingsViewModelClass
|
* @param {Function} SettingsViewModelClass
|
||||||
|
|
@ -31,17 +37,18 @@ rl.addSettingsViewModel = (SettingsViewModelClass, template, labelName, route) =
|
||||||
* @param {string} template
|
* @param {string} template
|
||||||
* @param {string} route
|
* @param {string} route
|
||||||
*/
|
*/
|
||||||
rl.addSettingsViewModelForAdmin = (SettingsViewModelClass, template, labelName, route) => {
|
export function addSettingsViewModelForAdmin(SettingsViewModelClass, template, labelName, route) {
|
||||||
ADMIN_VIEW_MODELS_HOOKS.push([SettingsViewModelClass, template, labelName, route]);
|
ADMIN_VIEW_MODELS_HOOKS.push([SettingsViewModelClass, template, labelName, route]);
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean} admin
|
* @param {boolean} admin
|
||||||
*/
|
*/
|
||||||
export function runSettingsViewModelHooks(admin) {
|
export function runSettingsViewModelHooks(admin) {
|
||||||
(admin ? ADMIN_VIEW_MODELS_HOOKS : USER_VIEW_MODELS_HOOKS).forEach(view =>
|
const Knoin = require('Knoin/Knoin');
|
||||||
settingsAddViewModel(...view)
|
(admin ? ADMIN_VIEW_MODELS_HOOKS : USER_VIEW_MODELS_HOOKS).forEach(view => {
|
||||||
);
|
Knoin.addSettingsViewModel(view[0], view[1], view[2], view[3]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -49,7 +56,8 @@ export function runSettingsViewModelHooks(admin) {
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @returns {?}
|
* @returns {?}
|
||||||
*/
|
*/
|
||||||
rl.pluginSettingsGet = (pluginSection, name) =>
|
export function settingsGet(pluginSection, name) {
|
||||||
SettingsGet('Plugins')?.[pluginSection]?.[name];
|
let plugins = rl.settings.get('Plugins');
|
||||||
|
plugins = plugins && null != plugins[pluginSection] ? plugins[pluginSection] : null;
|
||||||
rl.pluginPopupView = AbstractViewPopup;
|
return plugins ? (null == plugins[name] ? null : plugins[name]) : null;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,106 +1,111 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
import { addEventsListeners, addShortcut, registerShortcut } from 'Common/Globals';
|
|
||||||
import { isArray } from 'Common/Utils';
|
|
||||||
import { koComputable } from 'External/ko';
|
|
||||||
|
|
||||||
/*
|
class Selector {
|
||||||
oCallbacks:
|
list;
|
||||||
ItemSelect
|
listChecked;
|
||||||
MiddleClick
|
isListChecked;
|
||||||
canSelect
|
|
||||||
ItemGetUid
|
|
||||||
UpOrDown
|
|
||||||
*/
|
|
||||||
|
|
||||||
let shiftStart;
|
focusedItem;
|
||||||
|
selectedItem;
|
||||||
|
|
||||||
|
selectedItemUseCallback = true;
|
||||||
|
|
||||||
|
iSelectNextHelper = 0;
|
||||||
|
iFocusedNextHelper = 0;
|
||||||
|
oContentScrollable;
|
||||||
|
|
||||||
|
sItemSelector;
|
||||||
|
sItemSelectedSelector;
|
||||||
|
sItemCheckedSelector;
|
||||||
|
sItemFocusedSelector;
|
||||||
|
|
||||||
|
sLastUid = '';
|
||||||
|
oCallbacks = {};
|
||||||
|
|
||||||
export class Selector {
|
|
||||||
/**
|
/**
|
||||||
* @param {koProperty} koList
|
* @param {koProperty} koList
|
||||||
* @param {koProperty} koSelectedItem
|
* @param {koProperty} koSelectedItem
|
||||||
* @param {koProperty} koFocusedItem
|
* @param {koProperty} koFocusedItem
|
||||||
* @param {string} sItemSelector
|
* @param {string} sItemSelector
|
||||||
|
* @param {string} sItemSelectedSelector
|
||||||
* @param {string} sItemCheckedSelector
|
* @param {string} sItemCheckedSelector
|
||||||
|
* @param {string} sItemFocusedSelector
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
koList,
|
koList,
|
||||||
koSelectedItem,
|
koSelectedItem,
|
||||||
koFocusedItem,
|
koFocusedItem,
|
||||||
sItemSelector,
|
sItemSelector,
|
||||||
sItemCheckedSelector
|
sItemSelectedSelector,
|
||||||
|
sItemCheckedSelector,
|
||||||
|
sItemFocusedSelector
|
||||||
) {
|
) {
|
||||||
koFocusedItem = (koFocusedItem || ko.observable(null)).extend({ toggleSubscribeProperty: [this, 'focused'] });
|
|
||||||
koSelectedItem = (koSelectedItem || ko.observable(null)).extend({ toggleSubscribeProperty: [null, 'selected'] });
|
|
||||||
|
|
||||||
this.list = koList;
|
this.list = koList;
|
||||||
this.listChecked = koComputable(() => koList.filter(item => item.checked())).extend({ rateLimit: 0 });
|
|
||||||
|
|
||||||
this.focusedItem = koFocusedItem;
|
this.listChecked = ko.computed(() => this.list().filter(item => item.checked())).extend({ rateLimit: 0 });
|
||||||
this.selectedItem = koSelectedItem;
|
this.isListChecked = ko.computed(() => 0 < this.listChecked().length);
|
||||||
|
|
||||||
this.iSelectNextHelper = 0;
|
this.focusedItem = koFocusedItem || ko.observable(null);
|
||||||
this.iFocusedNextHelper = 0;
|
this.selectedItem = koSelectedItem || ko.observable(null);
|
||||||
// this.oContentScrollable = null;
|
|
||||||
|
const itemSelectedThrottle = (item => this.itemSelected(item)).debounce(300);
|
||||||
|
|
||||||
|
this.listChecked.subscribe((items) => {
|
||||||
|
if (items.length) {
|
||||||
|
if (null === this.selectedItem()) {
|
||||||
|
if (this.selectedItem.valueHasMutated) {
|
||||||
|
this.selectedItem.valueHasMutated();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.selectedItem(null);
|
||||||
|
}
|
||||||
|
} else if (this.autoSelect() && this.focusedItem()) {
|
||||||
|
this.selectedItem(this.focusedItem());
|
||||||
|
}
|
||||||
|
}, this);
|
||||||
|
|
||||||
|
this.selectedItem.subscribe((item) => {
|
||||||
|
if (item) {
|
||||||
|
if (this.isListChecked()) {
|
||||||
|
this.listChecked().forEach(subItem => subItem.checked(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.selectedItemUseCallback) {
|
||||||
|
itemSelectedThrottle(item);
|
||||||
|
}
|
||||||
|
} else if (this.selectedItemUseCallback) {
|
||||||
|
this.itemSelected(null);
|
||||||
|
}
|
||||||
|
}, this);
|
||||||
|
|
||||||
|
this.selectedItem = this.selectedItem.extend({ toggleSubscribeProperty: [this, 'selected'] });
|
||||||
|
this.focusedItem = this.focusedItem.extend({ toggleSubscribeProperty: [null, 'focused'] });
|
||||||
|
|
||||||
this.sItemSelector = sItemSelector;
|
this.sItemSelector = sItemSelector;
|
||||||
|
this.sItemSelectedSelector = sItemSelectedSelector;
|
||||||
this.sItemCheckedSelector = sItemCheckedSelector;
|
this.sItemCheckedSelector = sItemCheckedSelector;
|
||||||
this.sItemFocusedSelector = sItemSelector + '.focused';
|
this.sItemFocusedSelector = sItemFocusedSelector;
|
||||||
|
|
||||||
this.sLastUid = '';
|
this.focusedItem.subscribe(item => item && (this.sLastUid = this.getItemUid(item)), this);
|
||||||
this.oCallbacks = {};
|
|
||||||
|
|
||||||
const
|
let aCache = [],
|
||||||
itemSelected = item => {
|
aCheckedCache = [],
|
||||||
if (koList.hasChecked()) {
|
|
||||||
item || this.oCallbacks.ItemSelect?.(null);
|
|
||||||
} else if (item) {
|
|
||||||
this.oCallbacks.ItemSelect?.(item);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
itemSelectedThrottle = (item => itemSelected(item)).debounce(300);
|
|
||||||
|
|
||||||
this.listChecked.subscribe(items => {
|
|
||||||
if (items.length) {
|
|
||||||
koSelectedItem() ? koSelectedItem(null) : koSelectedItem.valueHasMutated?.();
|
|
||||||
} else {
|
|
||||||
this.autoSelect();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let selectedItemUseCallback = true;
|
|
||||||
|
|
||||||
koSelectedItem.subscribe(item => {
|
|
||||||
if (item) {
|
|
||||||
// koList.forEach(subItem => subItem.checked(false));
|
|
||||||
selectedItemUseCallback && itemSelectedThrottle(item);
|
|
||||||
} else {
|
|
||||||
selectedItemUseCallback && itemSelected();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
koFocusedItem.subscribe(item => item && (this.sLastUid = this.getItemUid(item)));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Below code is used to keep checked/focused/selected states when array is refreshed.
|
|
||||||
*/
|
|
||||||
|
|
||||||
let aCheckedCache = [],
|
|
||||||
mFocused = null,
|
mFocused = null,
|
||||||
mSelected = null;
|
mSelected = null;
|
||||||
|
|
||||||
// Before removing old list
|
this.list.subscribe(
|
||||||
koList.subscribe(
|
|
||||||
items => {
|
items => {
|
||||||
if (isArray(items)) {
|
if (Array.isArray(items)) {
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
const uid = this.getItemUid(item);
|
if (item) {
|
||||||
if (uid) {
|
const uid = this.getItemUid(item);
|
||||||
|
|
||||||
|
aCache.push(uid);
|
||||||
item.checked() && aCheckedCache.push(uid);
|
item.checked() && aCheckedCache.push(uid);
|
||||||
if (!mFocused && item.focused()) {
|
if (null === mFocused && item.focused()) {
|
||||||
mFocused = uid;
|
mFocused = uid;
|
||||||
}
|
}
|
||||||
if (!mSelected && item.selected()) {
|
if (null === mSelected && item.selected()) {
|
||||||
mSelected = uid;
|
mSelected = uid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -111,64 +116,136 @@ export class Selector {
|
||||||
'beforeChange'
|
'beforeChange'
|
||||||
);
|
);
|
||||||
|
|
||||||
koList.subscribe(aItems => {
|
this.list.subscribe((aItems) => {
|
||||||
selectedItemUseCallback = false;
|
let temp = null,
|
||||||
|
getNext = false,
|
||||||
|
isNextFocused = mFocused,
|
||||||
|
isChecked = false,
|
||||||
|
isSelected = false,
|
||||||
|
len = 0;
|
||||||
|
|
||||||
this.unselect();
|
const uids = [];
|
||||||
|
|
||||||
if (isArray(aItems)) {
|
this.selectedItemUseCallback = false;
|
||||||
let temp,
|
|
||||||
isChecked,
|
this.focusedItem(null);
|
||||||
next = this.iFocusedNextHelper || this.iSelectNextHelper;
|
this.selectedItem(null);
|
||||||
|
|
||||||
|
if (Array.isArray(aItems)) {
|
||||||
|
len = aCheckedCache.length;
|
||||||
|
|
||||||
aItems.forEach(item => {
|
aItems.forEach(item => {
|
||||||
const uid = this.getItemUid(item);
|
const uid = this.getItemUid(item);
|
||||||
if (uid) {
|
uids.push(uid);
|
||||||
|
|
||||||
if (mFocused === uid) {
|
if (null !== mFocused && mFocused === uid) {
|
||||||
koFocusedItem(item);
|
this.focusedItem(item);
|
||||||
mFocused = null;
|
mFocused = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (aCheckedCache.includes(uid)) {
|
if (0 < len && aCheckedCache.includes(uid)) {
|
||||||
item.checked(true);
|
isChecked = true;
|
||||||
isChecked = true;
|
item.checked(true);
|
||||||
}
|
--len;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isChecked && mSelected === uid) {
|
if (!isChecked && null !== mSelected && mSelected === uid) {
|
||||||
koSelectedItem(item);
|
isSelected = true;
|
||||||
mSelected = null;
|
this.selectedItem(item);
|
||||||
}
|
mSelected = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
selectedItemUseCallback = true;
|
this.selectedItemUseCallback = true;
|
||||||
|
|
||||||
|
if (!isChecked && !isSelected && this.autoSelect()) {
|
||||||
|
if (this.focusedItem()) {
|
||||||
|
this.selectedItem(this.focusedItem());
|
||||||
|
} else if (aItems.length) {
|
||||||
|
if (null !== isNextFocused) {
|
||||||
|
getNext = false;
|
||||||
|
isNextFocused = aCache.find(sUid => {
|
||||||
|
if (getNext && uids.includes(sUid)) {
|
||||||
|
return sUid;
|
||||||
|
}
|
||||||
|
if (isNextFocused === sUid) {
|
||||||
|
getNext = true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isNextFocused) {
|
||||||
|
temp = aItems.find(oItem => isNextFocused === this.getItemUid(oItem));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedItem(temp || null);
|
||||||
|
this.focusedItem(this.selectedItem());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(0 !== this.iSelectNextHelper || 0 !== this.iFocusedNextHelper) &&
|
||||||
|
aItems.length &&
|
||||||
|
!this.focusedItem()
|
||||||
|
) {
|
||||||
|
temp = null;
|
||||||
|
if (0 !== this.iFocusedNextHelper) {
|
||||||
|
temp = aItems[-1 === this.iFocusedNextHelper ? aItems.length - 1 : 0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!temp && 0 !== this.iSelectNextHelper) {
|
||||||
|
temp = aItems[-1 === this.iSelectNextHelper ? aItems.length - 1 : 0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
if (next && aItems.length && !koFocusedItem()) {
|
|
||||||
temp = aItems[-1 === next ? aItems.length - 1 : 0];
|
|
||||||
if (temp) {
|
if (temp) {
|
||||||
this.iSelectNextHelper && koSelectedItem(temp);
|
if (0 !== this.iSelectNextHelper) {
|
||||||
|
this.selectedItem(temp || null);
|
||||||
|
}
|
||||||
|
|
||||||
koFocusedItem(temp);
|
this.focusedItem(temp || null);
|
||||||
|
|
||||||
this.scrollToFocused();
|
this.scrollToFocused();
|
||||||
|
|
||||||
setTimeout(this.scrollToFocused, 100);
|
setTimeout(this.scrollToFocused, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.iSelectNextHelper = 0;
|
this.iSelectNextHelper = 0;
|
||||||
this.iFocusedNextHelper = 0;
|
this.iFocusedNextHelper = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
!isChecked && !koSelectedItem() && this.autoSelect();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
aCache = [];
|
||||||
aCheckedCache = [];
|
aCheckedCache = [];
|
||||||
mFocused = null;
|
mFocused = null;
|
||||||
mSelected = null;
|
mSelected = null;
|
||||||
selectedItemUseCallback = true;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
itemSelected(item) {
|
||||||
|
if (this.isListChecked()) {
|
||||||
|
if (!item) {
|
||||||
|
(this.oCallbacks.onItemSelect || (()=>{}))(item || null);
|
||||||
|
}
|
||||||
|
} else if (item) {
|
||||||
|
(this.oCallbacks.onItemSelect || (()=>{}))(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {boolean} forceSelect
|
||||||
|
*/
|
||||||
|
goDown(forceSelect) {
|
||||||
|
this.newSelectPosition('ArrowDown', false, forceSelect);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {boolean} forceSelect
|
||||||
|
*/
|
||||||
|
goUp(forceSelect) {
|
||||||
|
this.newSelectPosition('ArrowUp', false, forceSelect);
|
||||||
|
}
|
||||||
|
|
||||||
unselect() {
|
unselect() {
|
||||||
this.selectedItem(null);
|
this.selectedItem(null);
|
||||||
this.focusedItem(null);
|
this.focusedItem(null);
|
||||||
|
|
@ -178,18 +255,13 @@ export class Selector {
|
||||||
this.oContentScrollable = contentScrollable;
|
this.oContentScrollable = contentScrollable;
|
||||||
|
|
||||||
if (contentScrollable) {
|
if (contentScrollable) {
|
||||||
let getItem = selector => {
|
contentScrollable.addEventListener('click', event => {
|
||||||
let el = event.target.closestWithin(selector, contentScrollable);
|
let el = event.target.closestWithin(this.sItemSelector, contentScrollable);
|
||||||
return el ? ko.dataFor(el) : null;
|
el && this.actionClick(ko.dataFor(el), event);
|
||||||
};
|
|
||||||
|
|
||||||
addEventsListeners(contentScrollable, {
|
el = event.target.closestWithin(this.sItemCheckedSelector, contentScrollable);
|
||||||
click: event => {
|
if (el) {
|
||||||
const el = event.target.closestWithin(this.sItemSelector, contentScrollable);
|
const item = ko.dataFor(el);
|
||||||
let item = el && ko.dataFor(el);
|
|
||||||
el && (this.oCallbacks.click || (()=>1))(event, item) && this.actionClick(item, event);
|
|
||||||
|
|
||||||
item = getItem(this.sItemCheckedSelector);
|
|
||||||
if (item) {
|
if (item) {
|
||||||
if (event.shiftKey) {
|
if (event.shiftKey) {
|
||||||
this.actionClick(item, event);
|
this.actionClick(item, event);
|
||||||
|
|
@ -198,33 +270,27 @@ export class Selector {
|
||||||
item.checked(!item.checked());
|
item.checked(!item.checked());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
auxclick: event => {
|
|
||||||
if (1 == event.button) {
|
|
||||||
const item = getItem(this.sItemSelector);
|
|
||||||
if (item) {
|
|
||||||
this.focusedItem(item);
|
|
||||||
this.oCallbacks.MiddleClick?.(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
registerShortcut('enter,open', '', keyScope, () => {
|
shortcuts.add('enter,open', '', keyScope, () => {
|
||||||
const focused = this.focusedItem();
|
const focused = this.focusedItem();
|
||||||
if (focused && !focused.selected()) {
|
if (focused && !focused.selected()) {
|
||||||
this.actionClick(focused);
|
this.actionClick(focused);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
addShortcut('arrowup,arrowdown', 'meta', keyScope, () => false);
|
shortcuts.add('arrowup', 'meta', keyScope, () => false);
|
||||||
|
shortcuts.add('arrowdown', 'meta', keyScope, () => false);
|
||||||
|
|
||||||
addShortcut('arrowup,arrowdown', 'shift', keyScope, event => {
|
shortcuts.add('arrowup,arrowdown', 'shift', keyScope, event => {
|
||||||
this.newSelectPosition(event.key, true);
|
this.newSelectPosition(event.key, true);
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
registerShortcut('arrowup,arrowdown,home,end,pageup,pagedown,space', '', keyScope, event => {
|
shortcuts.add('arrowup,arrowdown,home,end,pageup,pagedown,space', '', keyScope, event => {
|
||||||
this.newSelectPosition(event.key, false);
|
this.newSelectPosition(event.key, false);
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
@ -234,10 +300,8 @@ export class Selector {
|
||||||
/**
|
/**
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
autoSelect(bForce) {
|
autoSelect() {
|
||||||
(bForce || (this.oCallbacks.canSelect || (()=>1))())
|
return !!(this.oCallbacks.onAutoSelect || (()=>true))();
|
||||||
&& this.focusedItem()
|
|
||||||
&& this.selectedItem(this.focusedItem());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -245,7 +309,14 @@ export class Selector {
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
getItemUid(item) {
|
getItemUid(item) {
|
||||||
return (item && this.oCallbacks.ItemGetUid?.(item)?.toString()) || '';
|
let uid = '';
|
||||||
|
|
||||||
|
const getItemUidCallback = this.oCallbacks.onItemGetUid || null;
|
||||||
|
if (getItemUidCallback && item) {
|
||||||
|
uid = getItemUidCallback(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return uid.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -254,36 +325,26 @@ export class Selector {
|
||||||
* @param {boolean=} bForceSelect = false
|
* @param {boolean=} bForceSelect = false
|
||||||
*/
|
*/
|
||||||
newSelectPosition(sEventKey, bShiftKey, bForceSelect) {
|
newSelectPosition(sEventKey, bShiftKey, bForceSelect) {
|
||||||
let result;
|
let isArrow = 'ArrowUp' === sEventKey || 'ArrowDown' === sEventKey,
|
||||||
|
result = null;
|
||||||
|
|
||||||
const up = 'ArrowUp' === sEventKey,
|
const pageStep = 10,
|
||||||
isArrow = up || 'ArrowDown' === sEventKey,
|
|
||||||
pageStep = 10,
|
|
||||||
list = this.list(),
|
list = this.list(),
|
||||||
listLen = list.length,
|
listLen = list ? list.length : 0,
|
||||||
focused = this.focusedItem();
|
focused = this.focusedItem();
|
||||||
|
|
||||||
bShiftKey || (shiftStart = -1);
|
if (0 < listLen) {
|
||||||
|
|
||||||
if (' ' === sEventKey) {
|
|
||||||
focused?.checked(!focused.checked());
|
|
||||||
} else if (listLen) {
|
|
||||||
if (focused) {
|
if (focused) {
|
||||||
if (isArrow) {
|
if (isArrow) {
|
||||||
let i = list.indexOf(focused);
|
let i = list.indexOf(focused);
|
||||||
if (bShiftKey) {
|
if ('ArrowUp' == sEventKey) {
|
||||||
shiftStart = -1 < shiftStart ? shiftStart : i;
|
i > 0 && (result = list[i-1]);
|
||||||
shiftStart == i
|
|
||||||
? focused.checked(true)
|
|
||||||
: ((up ? shiftStart < i : shiftStart > i) && focused.checked(false));
|
|
||||||
}
|
|
||||||
if (up) {
|
|
||||||
i > 0 && (result = list[--i]);
|
|
||||||
} else if (++i < listLen) {
|
} else if (++i < listLen) {
|
||||||
result = list[i];
|
result = list[i];
|
||||||
}
|
}
|
||||||
bShiftKey && result?.checked(true);
|
if (!result && ' ' !== sEventKey) {
|
||||||
result || this.oCallbacks.UpOrDown?.(up);
|
(this.oCallbacks.onUpUpOrDownDown || (()=>true))('ArrowUp' === sEventKey);
|
||||||
|
}
|
||||||
} else if ('Home' === sEventKey) {
|
} else if ('Home' === sEventKey) {
|
||||||
result = list[0];
|
result = list[0];
|
||||||
} else if ('End' === sEventKey) {
|
} else if ('End' === sEventKey) {
|
||||||
|
|
@ -300,22 +361,44 @@ export class Selector {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
|
'ArrowUp' == sEventKey ||
|
||||||
'Home' == sEventKey ||
|
'Home' == sEventKey ||
|
||||||
'PageUp' == sEventKey
|
'PageUp' == sEventKey
|
||||||
) {
|
) {
|
||||||
result = list[0];
|
result = list[0];
|
||||||
} else if (
|
} else if (
|
||||||
|
'ArrowDown' === sEventKey ||
|
||||||
'End' === sEventKey ||
|
'End' === sEventKey ||
|
||||||
'PageDown' === sEventKey
|
'PageDown' === sEventKey
|
||||||
) {
|
) {
|
||||||
result = list[list.length - 1];
|
result = list[list.length - 1];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
this.focusedItem(result);
|
this.focusedItem(result);
|
||||||
!this.list.hasChecked() && this.autoSelect(bForceSelect);
|
|
||||||
this.scrollToFocused();
|
if (focused) {
|
||||||
|
if (bShiftKey && isArrow) {
|
||||||
|
focused.checked(!focused.checked());
|
||||||
|
} else if (' ' === sEventKey) {
|
||||||
|
focused.checked(!focused.checked());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((this.autoSelect() || !!bForceSelect) && !this.isListChecked() && ' ' !== sEventKey) {
|
||||||
|
this.selectedItem(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scrollToFocused();
|
||||||
|
} else if (focused) {
|
||||||
|
if (bShiftKey && isArrow) {
|
||||||
|
focused.checked(!focused.checked());
|
||||||
|
} else if (' ' === sEventKey) {
|
||||||
|
focused.checked(!focused.checked());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.focusedItem(focused);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -325,15 +408,16 @@ export class Selector {
|
||||||
scrollToFocused() {
|
scrollToFocused() {
|
||||||
const scrollable = this.oContentScrollable;
|
const scrollable = this.oContentScrollable;
|
||||||
if (scrollable) {
|
if (scrollable) {
|
||||||
let focused = scrollable.querySelector(this.sItemFocusedSelector);
|
let block, focused = scrollable.querySelector(this.sItemFocusedSelector);
|
||||||
if (focused) {
|
if (focused) {
|
||||||
const fRect = focused.getBoundingClientRect(),
|
const fRect = focused.getBoundingClientRect(),
|
||||||
sRect = scrollable.getBoundingClientRect();
|
sRect = scrollable.getBoundingClientRect();
|
||||||
if (fRect.top < sRect.top) {
|
if (fRect.top < sRect.top) {
|
||||||
focused.scrollIntoView(true);
|
block = 'start';
|
||||||
} else if (fRect.bottom > sRect.bottom) {
|
} else if (fRect.bottom > sRect.bottom) {
|
||||||
focused.scrollIntoView(false);
|
block = 'end';
|
||||||
}
|
}
|
||||||
|
block && focused.scrollIntoView(block === 'start');
|
||||||
} else {
|
} else {
|
||||||
scrollable.scrollTop = 0;
|
scrollable.scrollTop = 0;
|
||||||
}
|
}
|
||||||
|
|
@ -347,47 +431,78 @@ export class Selector {
|
||||||
this.oContentScrollable && (this.oContentScrollable.scrollTop = 0);
|
this.oContentScrollable && (this.oContentScrollable.scrollTop = 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventClickFunction(item, event) {
|
||||||
|
let index = 0,
|
||||||
|
length = 0,
|
||||||
|
changeRange = false,
|
||||||
|
isInRange = false,
|
||||||
|
list = [],
|
||||||
|
checked = false,
|
||||||
|
listItem = null,
|
||||||
|
lineUid = '';
|
||||||
|
|
||||||
|
const uid = this.getItemUid(item);
|
||||||
|
if (event && event.shiftKey) {
|
||||||
|
if (uid && this.sLastUid && uid !== this.sLastUid) {
|
||||||
|
list = this.list();
|
||||||
|
checked = item.checked();
|
||||||
|
|
||||||
|
for (index = 0, length = list.length; index < length; index++) {
|
||||||
|
listItem = list[index];
|
||||||
|
lineUid = this.getItemUid(listItem);
|
||||||
|
|
||||||
|
changeRange = false;
|
||||||
|
if (lineUid === this.sLastUid || lineUid === uid) {
|
||||||
|
changeRange = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changeRange) {
|
||||||
|
isInRange = !isInRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInRange || changeRange) {
|
||||||
|
listItem.checked(checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sLastUid = uid || '';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} item
|
* @param {Object} item
|
||||||
* @param {Object=} event
|
* @param {Object=} event
|
||||||
*/
|
*/
|
||||||
actionClick(item, event) {
|
actionClick(item, event = null) {
|
||||||
if (item) {
|
if (item) {
|
||||||
let select = true;
|
let click = true;
|
||||||
if (event && !event.altKey) {
|
if (event) {
|
||||||
if (event.shiftKey && !event.ctrlKey && !event.metaKey) {
|
if (event.shiftKey && !(event.ctrlKey || event.metaKey) && !event.altKey) {
|
||||||
const uid = this.getItemUid(item);
|
click = false;
|
||||||
if (uid && this.sLastUid && uid !== this.sLastUid) {
|
if (!this.sLastUid) {
|
||||||
let changeRange = false,
|
this.sLastUid = this.getItemUid(item);
|
||||||
isInRange = false,
|
|
||||||
checked = !item.checked(),
|
|
||||||
lineUid = '';
|
|
||||||
this.list().forEach(listItem => {
|
|
||||||
lineUid = this.getItemUid(listItem);
|
|
||||||
changeRange = (lineUid === this.sLastUid || lineUid === uid);
|
|
||||||
if (isInRange || changeRange) {
|
|
||||||
if (changeRange) {
|
|
||||||
isInRange = !isInRange;
|
|
||||||
}
|
|
||||||
listItem.checked(checked);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
this.sLastUid = uid;
|
|
||||||
|
item.checked(!item.checked());
|
||||||
|
this.eventClickFunction(item, event);
|
||||||
|
|
||||||
this.focusedItem(item);
|
this.focusedItem(item);
|
||||||
return;
|
} else if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
|
||||||
}
|
click = false;
|
||||||
if (!event.shiftKey && (event.ctrlKey || event.metaKey)) {
|
|
||||||
select = false;
|
|
||||||
this.focusedItem(item);
|
this.focusedItem(item);
|
||||||
const selected = this.selectedItem();
|
|
||||||
if (selected && item !== selected) {
|
if (this.selectedItem() && item !== this.selectedItem()) {
|
||||||
selected.checked(true);
|
this.selectedItem().checked(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item.checked(!item.checked());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
select ? this.selectMessageItem(item) : item.checked(!item.checked());
|
if (click) {
|
||||||
|
this.selectMessageItem(item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -401,3 +516,5 @@ export class Selector {
|
||||||
this.scrollToFocused();
|
this.scrollToFocused();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { Selector, Selector as default };
|
||||||
|
|
|
||||||
|
|
@ -1,236 +1,240 @@
|
||||||
import ko from 'ko';
|
import ko from 'ko';
|
||||||
import { Notifications, UploadErrorCode } from 'Common/Enums';
|
import { Notification, UploadErrorCode } from 'Common/Enums';
|
||||||
import { langLink } from 'Common/Links';
|
import { langLink } from 'Common/Links';
|
||||||
import { doc, createElement } from 'Common/Globals';
|
|
||||||
import { getKeyByValue, forEachObjectEntry } from 'Common/Utils';
|
|
||||||
import { pInt } from 'Common/Utils';
|
|
||||||
import { LanguageStore } from 'Stores/Language';
|
|
||||||
|
|
||||||
let I18N_DATA = {};
|
let I18N_DATA = window.rainloopI18N || {};
|
||||||
|
|
||||||
const
|
const doc = document,
|
||||||
init = () => {
|
I18N_NOTIFICATION_DATA = {},
|
||||||
if (rl.I18N) {
|
I18N_NOTIFICATION_MAP = [
|
||||||
I18N_DATA = rl.I18N;
|
[Notification.InvalidToken, 'NOTIFICATIONS/INVALID_TOKEN'],
|
||||||
rl.I18N = null;
|
[Notification.InvalidToken, 'NOTIFICATIONS/INVALID_TOKEN'],
|
||||||
doc.documentElement.dir = I18N_DATA.LANG_DIR;
|
[Notification.AuthError, 'NOTIFICATIONS/AUTH_ERROR'],
|
||||||
return 1;
|
[Notification.AccessError, 'NOTIFICATIONS/ACCESS_ERROR'],
|
||||||
}
|
[Notification.ConnectionError, 'NOTIFICATIONS/CONNECTION_ERROR'],
|
||||||
},
|
[Notification.CaptchaError, 'NOTIFICATIONS/CAPTCHA_ERROR'],
|
||||||
|
[Notification.SocialFacebookLoginAccessDisable, 'NOTIFICATIONS/SOCIAL_FACEBOOK_LOGIN_ACCESS_DISABLE'],
|
||||||
|
[Notification.SocialTwitterLoginAccessDisable, 'NOTIFICATIONS/SOCIAL_TWITTER_LOGIN_ACCESS_DISABLE'],
|
||||||
|
[Notification.SocialGoogleLoginAccessDisable, 'NOTIFICATIONS/SOCIAL_GOOGLE_LOGIN_ACCESS_DISABLE'],
|
||||||
|
[Notification.DomainNotAllowed, 'NOTIFICATIONS/DOMAIN_NOT_ALLOWED'],
|
||||||
|
[Notification.AccountNotAllowed, 'NOTIFICATIONS/ACCOUNT_NOT_ALLOWED'],
|
||||||
|
|
||||||
i18nKey = key => key.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase(),
|
[Notification.AccountTwoFactorAuthRequired, 'NOTIFICATIONS/ACCOUNT_TWO_FACTOR_AUTH_REQUIRED'],
|
||||||
|
[Notification.AccountTwoFactorAuthError, 'NOTIFICATIONS/ACCOUNT_TWO_FACTOR_AUTH_ERROR'],
|
||||||
|
|
||||||
getNotificationMessage = code => {
|
[Notification.CouldNotSaveNewPassword, 'NOTIFICATIONS/COULD_NOT_SAVE_NEW_PASSWORD'],
|
||||||
let key = getKeyByValue(Notifications, code);
|
[Notification.CurrentPasswordIncorrect, 'NOTIFICATIONS/CURRENT_PASSWORD_INCORRECT'],
|
||||||
return key ? I18N_DATA.NOTIFICATIONS[key] : '';
|
[Notification.NewPasswordShort, 'NOTIFICATIONS/NEW_PASSWORD_SHORT'],
|
||||||
},
|
[Notification.NewPasswordWeak, 'NOTIFICATIONS/NEW_PASSWORD_WEAK'],
|
||||||
|
[Notification.NewPasswordForbidden, 'NOTIFICATIONS/NEW_PASSWORD_FORBIDDENT'],
|
||||||
|
|
||||||
fromNow = date => relativeTime(Math.round((date.getTime() - Date.now()) / 1000));
|
[Notification.ContactsSyncError, 'NOTIFICATIONS/CONTACTS_SYNC_ERROR'],
|
||||||
|
|
||||||
export const
|
[Notification.CantGetMessageList, 'NOTIFICATIONS/CANT_GET_MESSAGE_LIST'],
|
||||||
translateTrigger = ko.observable(false),
|
[Notification.CantGetMessage, 'NOTIFICATIONS/CANT_GET_MESSAGE'],
|
||||||
|
[Notification.CantDeleteMessage, 'NOTIFICATIONS/CANT_DELETE_MESSAGE'],
|
||||||
|
[Notification.CantMoveMessage, 'NOTIFICATIONS/CANT_MOVE_MESSAGE'],
|
||||||
|
[Notification.CantCopyMessage, 'NOTIFICATIONS/CANT_MOVE_MESSAGE'],
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat
|
[Notification.CantSaveMessage, 'NOTIFICATIONS/CANT_SAVE_MESSAGE'],
|
||||||
// see /snappymail/v/0.0.0/app/localization/relativetimeformat/
|
[Notification.CantSendMessage, 'NOTIFICATIONS/CANT_SEND_MESSAGE'],
|
||||||
relativeTime = seconds => {
|
[Notification.InvalidRecipients, 'NOTIFICATIONS/INVALID_RECIPIENTS'],
|
||||||
let unit = 'second',
|
|
||||||
t = [[60,'minute'],[3600,'hour'],[86400,'day'],[2628000,'month'],[31536000,'year']],
|
[Notification.CantSaveFilters, 'NOTIFICATIONS/CANT_SAVE_FILTERS'],
|
||||||
i = 5,
|
[Notification.CantGetFilters, 'NOTIFICATIONS/CANT_GET_FILTERS'],
|
||||||
abs = Math.abs(seconds);
|
[Notification.FiltersAreNotCorrect, 'NOTIFICATIONS/FILTERS_ARE_NOT_CORRECT'],
|
||||||
while (i--) {
|
|
||||||
if (t[i][0] <= abs) {
|
[Notification.CantCreateFolder, 'NOTIFICATIONS/CANT_CREATE_FOLDER'],
|
||||||
seconds = Math.round(seconds / t[i][0]);
|
[Notification.CantRenameFolder, 'NOTIFICATIONS/CANT_RENAME_FOLDER'],
|
||||||
unit = t[i][1];
|
[Notification.CantDeleteFolder, 'NOTIFICATIONS/CANT_DELETE_FOLDER'],
|
||||||
break;
|
[Notification.CantDeleteNonEmptyFolder, 'NOTIFICATIONS/CANT_DELETE_NON_EMPTY_FOLDER'],
|
||||||
|
[Notification.CantSubscribeFolder, 'NOTIFICATIONS/CANT_SUBSCRIBE_FOLDER'],
|
||||||
|
[Notification.CantUnsubscribeFolder, 'NOTIFICATIONS/CANT_UNSUBSCRIBE_FOLDER'],
|
||||||
|
|
||||||
|
[Notification.CantSaveSettings, 'NOTIFICATIONS/CANT_SAVE_SETTINGS'],
|
||||||
|
[Notification.CantSavePluginSettings, 'NOTIFICATIONS/CANT_SAVE_PLUGIN_SETTINGS'],
|
||||||
|
|
||||||
|
[Notification.DomainAlreadyExists, 'NOTIFICATIONS/DOMAIN_ALREADY_EXISTS'],
|
||||||
|
|
||||||
|
[Notification.CantInstallPackage, 'NOTIFICATIONS/CANT_INSTALL_PACKAGE'],
|
||||||
|
[Notification.CantDeletePackage, 'NOTIFICATIONS/CANT_DELETE_PACKAGE'],
|
||||||
|
[Notification.InvalidPluginPackage, 'NOTIFICATIONS/INVALID_PLUGIN_PACKAGE'],
|
||||||
|
[Notification.UnsupportedPluginPackage, 'NOTIFICATIONS/UNSUPPORTED_PLUGIN_PACKAGE'],
|
||||||
|
|
||||||
|
[Notification.DemoSendMessageError, 'NOTIFICATIONS/DEMO_SEND_MESSAGE_ERROR'],
|
||||||
|
[Notification.DemoAccountError, 'NOTIFICATIONS/DEMO_ACCOUNT_ERROR'],
|
||||||
|
|
||||||
|
[Notification.AccountAlreadyExists, 'NOTIFICATIONS/ACCOUNT_ALREADY_EXISTS'],
|
||||||
|
[Notification.AccountDoesNotExist, 'NOTIFICATIONS/ACCOUNT_DOES_NOT_EXIST'],
|
||||||
|
|
||||||
|
[Notification.MailServerError, 'NOTIFICATIONS/MAIL_SERVER_ERROR'],
|
||||||
|
[Notification.InvalidInputArgument, 'NOTIFICATIONS/INVALID_INPUT_ARGUMENT'],
|
||||||
|
[Notification.UnknownNotification, 'NOTIFICATIONS/UNKNOWN_ERROR'],
|
||||||
|
[Notification.UnknownError, 'NOTIFICATIONS/UNKNOWN_ERROR']
|
||||||
|
];
|
||||||
|
|
||||||
|
export const trigger = ko.observable(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} key
|
||||||
|
* @param {Object=} valueList
|
||||||
|
* @param {string=} defaulValue
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function i18n(key, valueList, defaulValue) {
|
||||||
|
let valueName = '',
|
||||||
|
result = I18N_DATA[key];
|
||||||
|
|
||||||
|
if (undefined === result) {
|
||||||
|
result = undefined === defaulValue ? key : defaulValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null != valueList) {
|
||||||
|
for (valueName in valueList) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(valueList, valueName)) {
|
||||||
|
result = result.replace('%' + valueName + '%', valueList[valueName]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (Intl.RelativeTimeFormat) {
|
}
|
||||||
return (new Intl.RelativeTimeFormat(doc.documentElement.lang)).format(seconds, unit);
|
|
||||||
}
|
|
||||||
// Safari < 14
|
|
||||||
abs = Math.abs(seconds);
|
|
||||||
let rtf = rl.relativeTime.long[unit][0 > seconds ? 'past' : 'future'],
|
|
||||||
plural = rl.relativeTime.plural(abs);
|
|
||||||
return (rtf[plural] || rtf).replace('{0}', abs);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
return result;
|
||||||
* @param {string} key
|
}
|
||||||
* @param {Object=} valueList
|
|
||||||
* @param {string=} defaulValue
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
i18n = (key, valueList, defaulValue) => {
|
|
||||||
let result = defaulValue ?? key;
|
|
||||||
let path = key.split('/');
|
|
||||||
if (I18N_DATA[path[0]] && path[1]) {
|
|
||||||
result = I18N_DATA[path[0]][path[1]] || result;
|
|
||||||
}
|
|
||||||
valueList && forEachObjectEntry(valueList, (key, value) => {
|
|
||||||
result = result.replace('%' + key + '%', value);
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
const i18nToNode = element => {
|
||||||
* @param {Object} elements
|
const key = element.dataset.i18n;
|
||||||
* @param {boolean=} animate = false
|
if (key) {
|
||||||
*/
|
if ('[' === key.substr(0, 1)) {
|
||||||
i18nToNodes = element =>
|
switch (key.substr(0, 6)) {
|
||||||
setTimeout(() =>
|
case '[html]':
|
||||||
element.querySelectorAll('[data-i18n]').forEach(element => {
|
element.innerHTML = i18n(key.substr(6));
|
||||||
const key = element.dataset.i18n;
|
break;
|
||||||
if ('[' === key[0]) {
|
case '[place':
|
||||||
switch (key.slice(1, 6)) {
|
element.placeholder = i18n(key.substr(13));
|
||||||
case 'html]':
|
break;
|
||||||
element.innerHTML = i18n(key.slice(6));
|
case '[title':
|
||||||
break;
|
element.title = i18n(key.substr(7));
|
||||||
case 'place':
|
break;
|
||||||
element.placeholder = i18n(key.slice(13));
|
// no default
|
||||||
break;
|
|
||||||
case 'title':
|
|
||||||
element.title = i18n(key.slice(7));
|
|
||||||
break;
|
|
||||||
// no default
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
element.textContent = i18n(key);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
, 1),
|
|
||||||
|
|
||||||
timestampToString = (timeStampInUTC, formatStr) => {
|
|
||||||
const now = Date.now(),
|
|
||||||
time = 0 < timeStampInUTC ? timeStampInUTC * 1000 : (0 === timeStampInUTC ? now : 0);
|
|
||||||
if (31536000000 < time) {
|
|
||||||
const m = new Date(time), h = LanguageStore.hourCycle();
|
|
||||||
switch (formatStr) {
|
|
||||||
case 'FROMNOW':
|
|
||||||
return fromNow(m);
|
|
||||||
case 'AUTO': {
|
|
||||||
// 4 hours
|
|
||||||
if (14400000 >= now - time)
|
|
||||||
return fromNow(m);
|
|
||||||
const date = new Date,
|
|
||||||
dt = date.setHours(0,0,0,0);
|
|
||||||
return (time > dt - 86400000)
|
|
||||||
? i18n(
|
|
||||||
time > dt ? 'MESSAGE_LIST/TODAY_AT' : 'MESSAGE_LIST/YESTERDAY_AT',
|
|
||||||
{TIME: m.format('LT',0,h)}
|
|
||||||
)
|
|
||||||
: m.format(
|
|
||||||
date.getFullYear() === m.getFullYear()
|
|
||||||
? {day: '2-digit', month: 'short', hour: 'numeric', minute: 'numeric'}
|
|
||||||
: {dateStyle: 'medium', timeStyle: 'short'}
|
|
||||||
, 0, h);
|
|
||||||
}
|
|
||||||
case 'FULL':
|
|
||||||
return m.format('LLL',0,h);
|
|
||||||
default:
|
|
||||||
return m.format(formatStr,0,h);
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
element.textContent = i18n(key);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return '';
|
/**
|
||||||
},
|
* @param {Object} elements
|
||||||
|
* @param {boolean=} animate = false
|
||||||
|
*/
|
||||||
|
export function i18nToNodes(element) {
|
||||||
|
setTimeout(() =>
|
||||||
|
element.querySelectorAll('[data-i18n]').forEach(item => i18nToNode(item))
|
||||||
|
, 1);
|
||||||
|
}
|
||||||
|
|
||||||
timeToNode = (element, time) => {
|
/**
|
||||||
try {
|
* @returns {void}
|
||||||
if (time) {
|
*/
|
||||||
element.dateTime = new Date(time * 1000).toISOString();
|
export function initNotificationLanguage() {
|
||||||
} else {
|
I18N_NOTIFICATION_MAP.forEach(item => I18N_NOTIFICATION_DATA[item[0]] = i18n(item[1]));
|
||||||
time = Date.parse(element.dateTime) / 1000;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Function} startCallback
|
||||||
|
* @param {Function=} langCallback = null
|
||||||
|
*/
|
||||||
|
export function initOnStartOrLangChange(startCallback, langCallback = null) {
|
||||||
|
startCallback && startCallback();
|
||||||
|
startCallback && trigger.subscribe(startCallback);
|
||||||
|
langCallback && trigger.subscribe(langCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} code
|
||||||
|
* @param {*=} message = ''
|
||||||
|
* @param {*=} defCode = null
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getNotification(code, message = '', defCode = null) {
|
||||||
|
code = parseInt(code, 10) || 0;
|
||||||
|
if (Notification.ClientViewError === code && message) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
defCode = defCode ? parseInt(defCode, 10) || 0 : 0;
|
||||||
|
return undefined === I18N_NOTIFICATION_DATA[code]
|
||||||
|
? defCode && undefined === I18N_NOTIFICATION_DATA[defCode]
|
||||||
|
? I18N_NOTIFICATION_DATA[defCode]
|
||||||
|
: ''
|
||||||
|
: I18N_NOTIFICATION_DATA[code];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} response
|
||||||
|
* @param {number} defCode = Notification.UnknownNotification
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getNotificationFromResponse(response, defCode = Notification.UnknownNotification) {
|
||||||
|
return response && response.ErrorCode
|
||||||
|
? getNotification(parseInt(response.ErrorCode, 10) || defCode, response.ErrorMessage || '')
|
||||||
|
: getNotification(defCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {*} code
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getUploadErrorDescByCode(code) {
|
||||||
|
let result = '';
|
||||||
|
switch (parseInt(code, 10) || 0) {
|
||||||
|
case UploadErrorCode.FileIsTooBig:
|
||||||
|
result = i18n('UPLOAD/ERROR_FILE_IS_TOO_BIG');
|
||||||
|
break;
|
||||||
|
case UploadErrorCode.FilePartiallyUploaded:
|
||||||
|
result = i18n('UPLOAD/ERROR_FILE_PARTIALLY_UPLOADED');
|
||||||
|
break;
|
||||||
|
case UploadErrorCode.FileNoUploaded:
|
||||||
|
result = i18n('UPLOAD/ERROR_NO_FILE_UPLOADED');
|
||||||
|
break;
|
||||||
|
case UploadErrorCode.MissingTempFolder:
|
||||||
|
result = i18n('UPLOAD/ERROR_MISSING_TEMP_FOLDER');
|
||||||
|
break;
|
||||||
|
case UploadErrorCode.FileOnSaveingError:
|
||||||
|
result = i18n('UPLOAD/ERROR_ON_SAVING_FILE');
|
||||||
|
break;
|
||||||
|
case UploadErrorCode.FileType:
|
||||||
|
result = i18n('UPLOAD/ERROR_FILE_TYPE');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
result = i18n('UPLOAD/ERROR_UNKNOWN');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {boolean} admin
|
||||||
|
* @param {string} language
|
||||||
|
*/
|
||||||
|
export function reload(admin, language) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const script = doc.createElement('script');
|
||||||
|
script.onload = () => {
|
||||||
|
// reload the data
|
||||||
|
if (window.rainloopI18N) {
|
||||||
|
I18N_DATA = window.rainloopI18N || {};
|
||||||
|
i18nToNodes(doc);
|
||||||
|
dispatchEvent(new CustomEvent('reload-time'));
|
||||||
|
trigger(!trigger());
|
||||||
}
|
}
|
||||||
|
window.rainloopI18N = null;
|
||||||
let key = element.dataset.timeFormat;
|
script.remove();
|
||||||
if (key) {
|
resolve();
|
||||||
element.textContent = timestampToString(time, key);
|
};
|
||||||
if ('FULL' !== key && 'FROMNOW' !== key) {
|
script.onerror = () => reject(new Error('Language '+language+' failed'));
|
||||||
element.title = timestampToString(time, 'FULL');
|
script.src = langLink(language, admin);
|
||||||
}
|
// script.async = true;
|
||||||
}
|
doc.head.append(script);
|
||||||
} catch (e) {
|
});
|
||||||
// prevent knockout crashes
|
}
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
reloadTime = () => doc.querySelectorAll('time').forEach(element => timeToNode(element)),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Function} startCallback
|
|
||||||
* @param {Function=} langCallback = null
|
|
||||||
*/
|
|
||||||
initOnStartOrLangChange = (startCallback, langCallback) => {
|
|
||||||
startCallback?.();
|
|
||||||
startCallback && translateTrigger.subscribe(startCallback);
|
|
||||||
langCallback && translateTrigger.subscribe(langCallback);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {number} code
|
|
||||||
* @param {*=} message = ''
|
|
||||||
* @param {*=} defCode = null
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
getNotification = (code, message = '', defCode = 0) => {
|
|
||||||
code = pInt(code);
|
|
||||||
if (Notifications.ClientViewError === code && message) {
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return getNotificationMessage(code)
|
|
||||||
|| getNotificationMessage(pInt(defCode))
|
|
||||||
|| '';
|
|
||||||
},
|
|
||||||
|
|
||||||
getErrorMessage = (code, data) =>
|
|
||||||
getNotification(code) || data?.messageAdditional || data?.message || data,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {*} code
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
getUploadErrorDescByCode = code => {
|
|
||||||
let key = getKeyByValue(UploadErrorCode, parseInt(code, 10));
|
|
||||||
return i18n('UPLOAD/ERROR_' + (key ? i18nKey(key) : 'UNKNOWN'));
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {boolean} admin
|
|
||||||
* @param {string} language
|
|
||||||
*/
|
|
||||||
translatorReload = (language, admin) =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
const script = createElement('script');
|
|
||||||
script.onload = () => {
|
|
||||||
// reload the data
|
|
||||||
if (init()) {
|
|
||||||
i18nToNodes(doc);
|
|
||||||
translateTrigger(!translateTrigger());
|
|
||||||
// admin || reloadTime();
|
|
||||||
}
|
|
||||||
script.remove();
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
script.onerror = () => reject(Error('Language '+language+' failed'));
|
|
||||||
script.src = langLink(language, admin);
|
|
||||||
// script.async = true;
|
|
||||||
doc.head.append(script);
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} language
|
|
||||||
* @param {boolean=} isEng = false
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
convertLangName = (language, isEng = false) =>
|
|
||||||
i18n(
|
|
||||||
'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language,
|
|
||||||
null,
|
|
||||||
language
|
|
||||||
),
|
|
||||||
|
|
||||||
baseCollator = numeric => new Intl.Collator(doc.documentElement.lang, {numeric: !!numeric, sensitivity: 'base'});
|
|
||||||
|
|
||||||
init();
|
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,150 @@
|
||||||
export const
|
import { SaveSettingsStep } from 'Common/Enums';
|
||||||
isArray = Array.isArray,
|
|
||||||
arrayLength = array => isArray(array) && array.length,
|
|
||||||
isFunction = v => typeof v === 'function',
|
|
||||||
pString = value => null != value ? '' + value : '',
|
|
||||||
|
|
||||||
forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn),
|
const
|
||||||
|
doc = document;
|
||||||
|
|
||||||
forEachObjectEntry = (obj, fn) => Object.entries(obj).forEach(([key, value]) => fn(key, value)),
|
/**
|
||||||
|
* @param {*} value
|
||||||
|
* @param {number=} defaultValue = 0
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export function pInt(value, defaultValue = 0) {
|
||||||
|
value = parseInt(value, 10);
|
||||||
|
return isNaN(value) || !isFinite(value) ? defaultValue : value;
|
||||||
|
}
|
||||||
|
|
||||||
pInt = (value, defaultValue = 0) => {
|
/**
|
||||||
value = parseInt(value, 10);
|
* @param {*} value
|
||||||
return isFinite(value) ? value : defaultValue;
|
* @returns {string}
|
||||||
},
|
*/
|
||||||
|
export function pString(value) {
|
||||||
|
return null != value ? '' + value : '';
|
||||||
|
}
|
||||||
|
|
||||||
defaultOptionsAfterRender = (domItem, item) =>
|
/**
|
||||||
item && undefined !== item.disabled && domItem?.classList.toggle('disabled', domItem.disabled = item.disabled),
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function inFocus() {
|
||||||
|
try {
|
||||||
|
return doc.activeElement && doc.activeElement.matches(
|
||||||
|
'input,textarea,iframe,.cke_editable'
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// unescape(encodeURIComponent()) makes the UTF-16 DOMString to an UTF-8 string
|
/**
|
||||||
b64Encode = data => btoa(unescape(encodeURIComponent(data))),
|
* @param {string} theme
|
||||||
/* // Without deprecated 'unescape':
|
* @returns {string}
|
||||||
b64Encode = data => btoa(encodeURIComponent(data).replace(
|
*/
|
||||||
/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode('0x' + p1)
|
export const convertThemeName = theme => {
|
||||||
)),
|
if ('@custom' === theme.substr(-7)) {
|
||||||
*/
|
theme = theme.substr(0, theme.length - 7).trim();
|
||||||
|
}
|
||||||
|
|
||||||
b64EncodeJSON = data => b64Encode(JSON.stringify(data)),
|
return theme
|
||||||
|
.replace(/([A-Z])/g, ' $1')
|
||||||
|
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
};
|
||||||
|
|
||||||
b64EncodeJSONSafe = data => b64EncodeJSON(data).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} language
|
||||||
|
* @param {boolean=} isEng = false
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function convertLangName(language, isEng = false) {
|
||||||
|
return require('Common/Translator').i18n(
|
||||||
|
'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/LANG_' + language.toUpperCase().replace(/[^a-zA-Z0-9]+/g, '_'),
|
||||||
|
null,
|
||||||
|
language
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v);
|
/**
|
||||||
|
* @param {object} domOption
|
||||||
|
* @param {object} item
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function defaultOptionsAfterRender(domItem, item) {
|
||||||
|
if (item && undefined !== item.disabled && domItem) {
|
||||||
|
domItem.classList.toggle('disabled', domItem.disabled = item.disabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} koTrigger
|
||||||
|
* @param {mixed} context
|
||||||
|
* @returns {mixed}
|
||||||
|
*/
|
||||||
|
export function settingsSaveHelperSimpleFunction(koTrigger, context) {
|
||||||
|
return (type, data) => {
|
||||||
|
koTrigger.call(context, data && data.Result ? SaveSettingsStep.TrueResult : SaveSettingsStep.FalseResult);
|
||||||
|
setTimeout(() => koTrigger.call(context, SaveSettingsStep.Idle), 1000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let __themeTimer = 0,
|
||||||
|
__themeAjax = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} value
|
||||||
|
* @param {function=} themeTrigger = noop
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function changeTheme(value, themeTrigger = ()=>{}) {
|
||||||
|
const themeLink = doc.getElementById('app-theme-link'),
|
||||||
|
clearTimer = () => {
|
||||||
|
__themeTimer = setTimeout(() => themeTrigger(SaveSettingsStep.Idle), 1000);
|
||||||
|
__themeAjax = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
let themeStyle = doc.getElementById('app-theme-style'),
|
||||||
|
url = (themeLink && themeLink.href) || (themeStyle && themeStyle.dataset.href);
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
url = url.toString()
|
||||||
|
.replace(/\/-\/[^/]+\/-\//, '/-/' + value + '/-/')
|
||||||
|
.replace(/\/Css\/[^/]+\/User\//, '/Css/0/User/')
|
||||||
|
.replace(/\/Hash\/[^/]+\//, '/Hash/-/');
|
||||||
|
|
||||||
|
if ('Json/' !== url.substr(-5)) {
|
||||||
|
url += 'Json/';
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(__themeTimer);
|
||||||
|
|
||||||
|
themeTrigger(SaveSettingsStep.Animate);
|
||||||
|
|
||||||
|
if (__themeAjax) {
|
||||||
|
__themeAjax.abort();
|
||||||
|
}
|
||||||
|
let init = {};
|
||||||
|
if (window.AbortController) {
|
||||||
|
__themeAjax = new AbortController();
|
||||||
|
init.signal = __themeAjax.signal;
|
||||||
|
}
|
||||||
|
rl.fetchJSON(url, init)
|
||||||
|
.then(data => {
|
||||||
|
if (data && Array.isArray(data) && 2 === data.length) {
|
||||||
|
if (themeLink && !themeStyle) {
|
||||||
|
themeStyle = doc.createElement('style');
|
||||||
|
themeStyle.id = 'app-theme-style';
|
||||||
|
themeLink.after(themeStyle);
|
||||||
|
themeLink.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (themeStyle) {
|
||||||
|
themeStyle.textContent = data[1];
|
||||||
|
themeStyle.dataset.href = url;
|
||||||
|
themeStyle.dataset.theme = data[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
themeTrigger(SaveSettingsStep.TrueResult);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(clearTimer, clearTimer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,138 +1,392 @@
|
||||||
import { Notifications } from 'Common/Enums';
|
import { ComposeType, FolderType } from 'Common/Enums';
|
||||||
import { MessageSetAction, ComposeType/*, FolderType*/ } from 'Common/EnumsUser';
|
|
||||||
import { doc, createElement, elementById, dropdowns, dropdownVisibility, SettingsGet, leftPanelDisabled } from 'Common/Globals';
|
|
||||||
import { plainToHtml } from 'Common/Html';
|
|
||||||
import { getNotification } from 'Common/Translator';
|
|
||||||
import { EmailCollectionModel } from 'Model/EmailCollection';
|
|
||||||
import { MessageUserStore } from 'Stores/User/Message';
|
|
||||||
import { MessagelistUserStore } from 'Stores/User/Messagelist';
|
|
||||||
import { SettingsUserStore } from 'Stores/User/Settings';
|
|
||||||
import * as Local from 'Storage/Client';
|
|
||||||
import { ThemeStore } from 'Stores/Theme';
|
|
||||||
import Remote from 'Remote/User/Fetch';
|
|
||||||
import { attachmentDownload } from 'Common/Links';
|
|
||||||
|
|
||||||
import { AccountModel } from 'Model/Account';
|
const
|
||||||
import { IdentityModel } from 'Model/Identity';
|
tpl = document.createElement('template'),
|
||||||
import { AccountUserStore } from 'Stores/User/Account';
|
isArray = Array.isArray,
|
||||||
import { IdentityUserStore } from 'Stores/User/Identity';
|
htmlmap = {
|
||||||
import { isArray } from 'Common/Utils';
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
import { showScreenPopup } from 'Knoin/Knoin';
|
'>': '>',
|
||||||
import { IdentityPopupView } from 'View/Popup/Identity';
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
export const
|
},
|
||||||
|
htmlspecialchars = str => (''+str).replace(/[&<>"']/g, m => htmlmap[m]);
|
||||||
// 1 = move, 2 = copy
|
|
||||||
moveAction = ko.observable(0),
|
|
||||||
|
|
||||||
dropdownsDetectVisibility = (() =>
|
|
||||||
dropdownVisibility(!!dropdowns.find(item => item.classList.contains('show')))
|
|
||||||
).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|number)} value
|
||||||
|
* @param {boolean=} includeZero = true
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
download = (link, name = "") => {
|
export function isPosNumeric(value, includeZero = true) {
|
||||||
console.log('download: '+link);
|
return null != value && (includeZero ? /^[0-9]*$/ : /^[1-9]+[0-9]*$/).test(value.toString());
|
||||||
// Firefox 98 issue https://github.com/the-djmaze/snappymail/issues/301
|
}
|
||||||
if (ThemeStore.isMobile() || /firefox/i.test(navigator.userAgent)) {
|
|
||||||
open(link, '_blank');
|
|
||||||
focus();
|
|
||||||
} else {
|
|
||||||
const oLink = createElement('a', {
|
|
||||||
href: link,
|
|
||||||
target: '_blank',
|
|
||||||
download: name
|
|
||||||
});
|
|
||||||
doc.body.appendChild(oLink).click();
|
|
||||||
oLink.remove();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
downloadZip = (name, hashes, onError, fTrigger, folder) => {
|
/**
|
||||||
if (hashes.length) {
|
* @param {string} text
|
||||||
let params = {
|
* @returns {string}
|
||||||
target: 'zip',
|
*/
|
||||||
filename: name,
|
export function encodeHtml(text) {
|
||||||
hashes: hashes
|
return null != text ? htmlspecialchars(text.toString()) : '';
|
||||||
};
|
}
|
||||||
if (!onError) {
|
|
||||||
onError = () => alert('Download failed');
|
/**
|
||||||
|
* @param {string} text
|
||||||
|
* @param {number=} len = 100
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function splitPlainText(text, len = 100) {
|
||||||
|
let prefix = '',
|
||||||
|
subText = '',
|
||||||
|
result = text,
|
||||||
|
spacePos = 0,
|
||||||
|
newLinePos = 0;
|
||||||
|
|
||||||
|
while (result.length > len) {
|
||||||
|
subText = result.substr(0, len);
|
||||||
|
spacePos = subText.lastIndexOf(' ');
|
||||||
|
newLinePos = subText.lastIndexOf('\n');
|
||||||
|
|
||||||
|
if (-1 !== newLinePos) {
|
||||||
|
spacePos = newLinePos;
|
||||||
}
|
}
|
||||||
if (folder) {
|
|
||||||
params.folder = folder;
|
if (-1 === spacePos) {
|
||||||
// params.uids = uids;
|
spacePos = len;
|
||||||
}
|
}
|
||||||
Remote.post('AttachmentsActions', fTrigger || null, params)
|
|
||||||
.then(result => {
|
prefix += subText.substr(0, spacePos) + '\n';
|
||||||
let hash = result?.Result?.fileHash;
|
result = result.substr(spacePos + 1);
|
||||||
hash ? download(attachmentDownload(hash), hash+'.zip') : onError();
|
|
||||||
})
|
|
||||||
.catch(onError);
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
return prefix + result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} html
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function htmlToPlain(html) {
|
||||||
|
let pos = 0,
|
||||||
|
limit = 0,
|
||||||
|
iP1 = 0,
|
||||||
|
iP2 = 0,
|
||||||
|
iP3 = 0,
|
||||||
|
text = '';
|
||||||
|
|
||||||
|
const convertBlockquote = (blockquoteText) => {
|
||||||
|
blockquoteText = '> ' + blockquoteText.trim().replace(/\n/gm, '\n> ');
|
||||||
|
return blockquoteText.replace(/(^|\n)([> ]+)/gm, (...args) =>
|
||||||
|
args && 2 < args.length ? args[1] + args[2].replace(/[\s]/g, '').trim() + ' ' : ''
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertDivs = (...args) => {
|
||||||
|
if (args && 1 < args.length) {
|
||||||
|
let divText = args[1].trim();
|
||||||
|
if (divText.length) {
|
||||||
|
divText = divText.replace(/<div[^>]*>([\s\S\r\n]*)<\/div>/gim, convertDivs);
|
||||||
|
divText = '\n' + divText.trim() + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
return divText;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertPre = (...args) =>
|
||||||
|
args && 1 < args.length
|
||||||
|
? args[1]
|
||||||
|
.toString()
|
||||||
|
.replace(/[\n]/gm, '<br />')
|
||||||
|
.replace(/[\r]/gm, '')
|
||||||
|
: '',
|
||||||
|
fixAttibuteValue = (...args) => (args && 1 < args.length ? '' + args[1] + htmlspecialchars(args[2]) : ''),
|
||||||
|
convertLinks = (...args) => (args && 1 < args.length ? args[1].trim() : '');
|
||||||
|
|
||||||
|
tpl.innerHTML = html
|
||||||
|
.replace(/<p[^>]*><\/p>/gi, '')
|
||||||
|
.replace(/<pre[^>]*>([\s\S\r\n\t]*)<\/pre>/gim, convertPre)
|
||||||
|
.replace(/[\s]+/gm, ' ')
|
||||||
|
.replace(/((?:href|data)\s?=\s?)("[^"]+?"|'[^']+?')/gim, fixAttibuteValue)
|
||||||
|
.replace(/<br[^>]*>/gim, '\n')
|
||||||
|
.replace(/<\/h[\d]>/gi, '\n')
|
||||||
|
.replace(/<\/p>/gi, '\n\n')
|
||||||
|
.replace(/<ul[^>]*>/gim, '\n')
|
||||||
|
.replace(/<\/ul>/gi, '\n')
|
||||||
|
.replace(/<li[^>]*>/gim, ' * ')
|
||||||
|
.replace(/<\/li>/gi, '\n')
|
||||||
|
.replace(/<\/td>/gi, '\n')
|
||||||
|
.replace(/<\/tr>/gi, '\n')
|
||||||
|
.replace(/<hr[^>]*>/gim, '\n_______________________________\n\n')
|
||||||
|
.replace(/<div[^>]*>([\s\S\r\n]*)<\/div>/gim, convertDivs)
|
||||||
|
.replace(/<blockquote[^>]*>/gim, '\n__bq__start__\n')
|
||||||
|
.replace(/<\/blockquote>/gim, '\n__bq__end__\n')
|
||||||
|
.replace(/<a [^>]*>([\s\S\r\n]*?)<\/a>/gim, convertLinks)
|
||||||
|
.replace(/<\/div>/gi, '\n')
|
||||||
|
.replace(/ /gi, ' ')
|
||||||
|
.replace(/"/gi, '"')
|
||||||
|
.replace(/<[^>]*>/gm, '');
|
||||||
|
|
||||||
|
text = splitPlainText(tpl.content.textContent
|
||||||
|
.replace(/\n[ \t]+/gm, '\n')
|
||||||
|
.replace(/[\n]{3,}/gm, '\n\n')
|
||||||
|
.replace(/>/gi, '>')
|
||||||
|
.replace(/</gi, '<')
|
||||||
|
.replace(/&/gi, '&')
|
||||||
|
);
|
||||||
|
|
||||||
|
pos = 0;
|
||||||
|
limit = 800;
|
||||||
|
|
||||||
|
while (0 < limit) {
|
||||||
|
--limit;
|
||||||
|
iP1 = text.indexOf('__bq__start__', pos);
|
||||||
|
if (-1 < iP1) {
|
||||||
|
iP2 = text.indexOf('__bq__start__', iP1 + 5);
|
||||||
|
iP3 = text.indexOf('__bq__end__', iP1 + 5);
|
||||||
|
|
||||||
|
if ((-1 === iP2 || iP3 < iP2) && iP1 < iP3) {
|
||||||
|
text = text.substr(0, iP1) + convertBlockquote(text.substring(iP1 + 13, iP3)) + text.substr(iP3 + 11);
|
||||||
|
pos = 0;
|
||||||
|
} else if (-1 < iP2 && iP2 < iP3) {
|
||||||
|
pos = iP2 - 1;
|
||||||
|
} else {
|
||||||
|
pos = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text.replace(/__bq__start__|__bq__end__/gm, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} plain
|
||||||
|
* @param {boolean} findEmailAndLinksInText = false
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function plainToHtml(plain) {
|
||||||
|
plain = plain.toString().replace(/\r/g, '');
|
||||||
|
plain = plain.replace(/^>[> ]>+/gm, ([match]) => (match ? match.replace(/[ ]+/g, '') : match));
|
||||||
|
|
||||||
|
let bIn = false,
|
||||||
|
bDo = true,
|
||||||
|
bStart = true,
|
||||||
|
aNextText = [],
|
||||||
|
aText = plain.split('\n');
|
||||||
|
|
||||||
|
do {
|
||||||
|
bDo = false;
|
||||||
|
aNextText = [];
|
||||||
|
aText.forEach(sLine => {
|
||||||
|
bStart = '>' === sLine.substr(0, 1);
|
||||||
|
if (bStart && !bIn) {
|
||||||
|
bDo = true;
|
||||||
|
bIn = true;
|
||||||
|
aNextText.push('~~~blockquote~~~');
|
||||||
|
aNextText.push(sLine.substr(1));
|
||||||
|
} else if (!bStart && bIn) {
|
||||||
|
if (sLine) {
|
||||||
|
bIn = false;
|
||||||
|
aNextText.push('~~~/blockquote~~~');
|
||||||
|
aNextText.push(sLine);
|
||||||
|
} else {
|
||||||
|
aNextText.push(sLine);
|
||||||
|
}
|
||||||
|
} else if (bStart && bIn) {
|
||||||
|
aNextText.push(sLine.substr(1));
|
||||||
|
} else {
|
||||||
|
aNextText.push(sLine);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (bIn) {
|
||||||
|
bIn = false;
|
||||||
|
aNextText.push('~~~/blockquote~~~');
|
||||||
|
}
|
||||||
|
|
||||||
|
aText = aNextText;
|
||||||
|
} while (bDo);
|
||||||
|
|
||||||
|
return aText.join('\n')
|
||||||
|
// .replace(/~~~\/blockquote~~~\n~~~blockquote~~~/g, '\n')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/~~~blockquote~~~[\s]*/g, '<blockquote>')
|
||||||
|
.replace(/[\s]*~~~\/blockquote~~~/g, '</blockquote>')
|
||||||
|
.replace(/\n/g, '<br />');
|
||||||
|
}
|
||||||
|
|
||||||
|
rl.Utils = {
|
||||||
|
htmlToPlain: htmlToPlain,
|
||||||
|
plainToHtml: plainToHtml
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Array} aSystem
|
||||||
|
* @param {Array} aList
|
||||||
|
* @param {Array=} aDisabled
|
||||||
|
* @param {Array=} aHeaderLines
|
||||||
|
* @param {?number=} iUnDeep
|
||||||
|
* @param {Function=} fDisableCallback
|
||||||
|
* @param {Function=} fVisibleCallback
|
||||||
|
* @param {Function=} fRenameCallback
|
||||||
|
* @param {boolean=} bSystem
|
||||||
|
* @param {boolean=} bBuildUnvisible
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
export function folderListOptionsBuilder(
|
||||||
|
aSystem,
|
||||||
|
aList,
|
||||||
|
aDisabled,
|
||||||
|
aHeaderLines,
|
||||||
|
iUnDeep,
|
||||||
|
fDisableCallback,
|
||||||
|
fVisibleCallback,
|
||||||
|
fRenameCallback,
|
||||||
|
bSystem,
|
||||||
|
bBuildUnvisible
|
||||||
|
) {
|
||||||
|
let /**
|
||||||
|
* @type {?FolderModel}
|
||||||
|
*/
|
||||||
|
bSep = false,
|
||||||
|
aResult = [];
|
||||||
|
|
||||||
|
const sDeepPrefix = '\u00A0\u00A0\u00A0';
|
||||||
|
|
||||||
|
bBuildUnvisible = undefined === bBuildUnvisible ? false : !!bBuildUnvisible;
|
||||||
|
bSystem = null == bSystem ? 0 < aSystem.length : bSystem;
|
||||||
|
iUnDeep = null == iUnDeep ? 0 : iUnDeep;
|
||||||
|
fDisableCallback = null != fDisableCallback ? fDisableCallback : null;
|
||||||
|
fVisibleCallback = null != fVisibleCallback ? fVisibleCallback : null;
|
||||||
|
fRenameCallback = null != fRenameCallback ? fRenameCallback : null;
|
||||||
|
|
||||||
|
if (!isArray(aDisabled)) {
|
||||||
|
aDisabled = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isArray(aHeaderLines)) {
|
||||||
|
aHeaderLines = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
aHeaderLines.forEach(line => {
|
||||||
|
aResult.push({
|
||||||
|
id: line[0],
|
||||||
|
name: line[1],
|
||||||
|
system: false,
|
||||||
|
dividerbar: false,
|
||||||
|
disabled: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
bSep = true;
|
||||||
|
aSystem.forEach(oItem => {
|
||||||
|
if (fVisibleCallback ? fVisibleCallback(oItem) : true) {
|
||||||
|
aResult.push({
|
||||||
|
id: oItem.fullNameRaw,
|
||||||
|
name: fRenameCallback ? fRenameCallback(oItem) : oItem.name(),
|
||||||
|
system: true,
|
||||||
|
dividerbar: bSep,
|
||||||
|
disabled:
|
||||||
|
!oItem.selectable ||
|
||||||
|
aDisabled.includes(oItem.fullNameRaw) ||
|
||||||
|
(fDisableCallback ? fDisableCallback(oItem) : false)
|
||||||
|
});
|
||||||
|
bSep = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bSep = true;
|
||||||
|
aList.forEach(oItem => {
|
||||||
|
// if (oItem.subscribed() || !oItem.exists || bBuildUnvisible)
|
||||||
|
if (
|
||||||
|
(oItem.subscribed() || !oItem.exists || bBuildUnvisible) &&
|
||||||
|
(oItem.selectable || oItem.hasSubscribedSubfolders())
|
||||||
|
) {
|
||||||
|
if (fVisibleCallback ? fVisibleCallback(oItem) : true) {
|
||||||
|
if (FolderType.User === oItem.type() || !bSystem || oItem.hasSubscribedSubfolders()) {
|
||||||
|
aResult.push({
|
||||||
|
id: oItem.fullNameRaw,
|
||||||
|
name:
|
||||||
|
new Array(oItem.deep + 1 - iUnDeep).join(sDeepPrefix) +
|
||||||
|
(fRenameCallback ? fRenameCallback(oItem) : oItem.name()),
|
||||||
|
system: false,
|
||||||
|
dividerbar: bSep,
|
||||||
|
disabled:
|
||||||
|
!oItem.selectable ||
|
||||||
|
aDisabled.includes(oItem.fullNameRaw) ||
|
||||||
|
(fDisableCallback ? fDisableCallback(oItem) : false)
|
||||||
|
});
|
||||||
|
bSep = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oItem.subscribed() && oItem.subFolders().length) {
|
||||||
|
aResult = aResult.concat(
|
||||||
|
folderListOptionsBuilder(
|
||||||
|
[],
|
||||||
|
oItem.subFolders(),
|
||||||
|
aDisabled,
|
||||||
|
[],
|
||||||
|
iUnDeep,
|
||||||
|
fDisableCallback,
|
||||||
|
fVisibleCallback,
|
||||||
|
fRenameCallback,
|
||||||
|
bSystem,
|
||||||
|
bBuildUnvisible
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return aResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call the Model/CollectionModel onDestroy() to clear knockout functions/objects
|
||||||
|
* @param {Object|Array} objectOrObjects
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function delegateRunOnDestroy(objectOrObjects) {
|
||||||
|
objectOrObjects && (isArray(objectOrObjects) ? objectOrObjects : [objectOrObjects]).forEach(
|
||||||
|
obj => obj.onDestroy && obj.onDestroy()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {function}
|
* @returns {function}
|
||||||
*/
|
*/
|
||||||
computedPaginatorHelper = (koCurrentPage, koPageCount) => {
|
export function computedPaginatorHelper(koCurrentPage, koPageCount) {
|
||||||
return () => {
|
return () => {
|
||||||
const currentPage = koCurrentPage(),
|
const currentPage = koCurrentPage(),
|
||||||
pageCount = koPageCount(),
|
pageCount = koPageCount(),
|
||||||
result = [],
|
result = [],
|
||||||
lang = doc.documentElement.lang,
|
|
||||||
fAdd = (index, push = true, customName = '') => {
|
fAdd = (index, push = true, customName = '') => {
|
||||||
const name = index.toLocaleString(lang),
|
const data = {
|
||||||
data = {
|
current: index === currentPage,
|
||||||
current: index === currentPage,
|
name: customName ? customName.toString() : index.toString(),
|
||||||
name: customName || name,
|
custom: !!customName,
|
||||||
title: customName ? name : '',
|
title: customName ? index.toString() : '',
|
||||||
value: index
|
value: index.toString()
|
||||||
};
|
};
|
||||||
|
|
||||||
push ? result.push(data) : result.unshift(data);
|
if (push) {
|
||||||
|
result.push(data);
|
||||||
|
} else {
|
||||||
|
result.unshift(data);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let prev = 0,
|
let prev = 0,
|
||||||
next = 0,
|
next = 0,
|
||||||
limit = 2;
|
limit = 2;
|
||||||
|
|
||||||
if (1 < pageCount) {
|
if (1 < pageCount || (0 < pageCount && pageCount < currentPage)) {
|
||||||
if (pageCount < currentPage) {
|
if (pageCount < currentPage) {
|
||||||
fAdd(pageCount);
|
fAdd(pageCount);
|
||||||
prev = pageCount;
|
prev = pageCount;
|
||||||
|
|
@ -167,13 +421,13 @@ computedPaginatorHelper = (koCurrentPage, koPageCount) => {
|
||||||
if (3 === prev) {
|
if (3 === prev) {
|
||||||
fAdd(2, false);
|
fAdd(2, false);
|
||||||
} else if (3 < prev) {
|
} else if (3 < prev) {
|
||||||
fAdd(Math.round((prev - 1) / 2), false, '…');
|
fAdd(Math.round((prev - 1) / 2), false, '...');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pageCount - 2 === next) {
|
if (pageCount - 2 === next) {
|
||||||
fAdd(pageCount - 1, true);
|
fAdd(pageCount - 1, true);
|
||||||
} else if (pageCount - 2 > next) {
|
} else if (pageCount - 2 > next) {
|
||||||
fAdd(Math.round((pageCount + next) / 2), true, '…');
|
fAdd(Math.round((pageCount + next) / 2), true, '...');
|
||||||
}
|
}
|
||||||
|
|
||||||
// first and last
|
// first and last
|
||||||
|
|
@ -188,187 +442,82 @@ computedPaginatorHelper = (koCurrentPage, koPageCount) => {
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} mailToUrl
|
* @param {string} mailToUrl
|
||||||
|
* @param {Function} PopupComposeViewModel
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
mailToHelper = mailToUrl => {
|
export function mailToHelper(mailToUrl, PopupComposeViewModel) {
|
||||||
if ('mailto:' === mailToUrl?.slice(0, 7).toLowerCase()) {
|
if (
|
||||||
mailToUrl = mailToUrl.slice(7).split('?');
|
mailToUrl &&
|
||||||
|
'mailto:' ===
|
||||||
|
mailToUrl
|
||||||
|
.toString()
|
||||||
|
.substr(0, 7)
|
||||||
|
.toLowerCase()
|
||||||
|
) {
|
||||||
|
if (!PopupComposeViewModel) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const
|
mailToUrl = mailToUrl.toString().substr(7);
|
||||||
email = decodeURIComponent(mailToUrl[0]),
|
|
||||||
params = new URLSearchParams(mailToUrl[1]),
|
|
||||||
to = params.get('to'),
|
|
||||||
toEmailModel = value => EmailCollectionModel.fromString(value);
|
|
||||||
|
|
||||||
showMessageComposer([
|
let to = [],
|
||||||
|
cc = null,
|
||||||
|
bcc = null,
|
||||||
|
params = {};
|
||||||
|
|
||||||
|
const email = mailToUrl.replace(/\?.+$/, ''),
|
||||||
|
query = mailToUrl.replace(/^[^?]*\?/, ''),
|
||||||
|
EmailModel = require('Model/Email').default;
|
||||||
|
|
||||||
|
query.split('&').forEach(temp => {
|
||||||
|
temp = temp.split('=');
|
||||||
|
params[decodeURIComponent(temp[0])] = decodeURIComponent(temp[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (undefined !== params.to) {
|
||||||
|
to = EmailModel.parseEmailLine(decodeURIComponent(email + ',' + params.to));
|
||||||
|
to = Object.values(
|
||||||
|
to.reduce((result, value) => {
|
||||||
|
if (value) {
|
||||||
|
if (result[value.email]) {
|
||||||
|
if (!result[value.email].name) {
|
||||||
|
result[value.email] = value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result[value.email] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, {})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
to = EmailModel.parseEmailLine(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (undefined !== params.cc) {
|
||||||
|
cc = EmailModel.parseEmailLine(decodeURIComponent(params.cc));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (undefined !== params.bcc) {
|
||||||
|
bcc = EmailModel.parseEmailLine(decodeURIComponent(params.bcc));
|
||||||
|
}
|
||||||
|
|
||||||
|
require('Knoin/Knoin').showScreenPopup(PopupComposeViewModel, [
|
||||||
ComposeType.Empty,
|
ComposeType.Empty,
|
||||||
null,
|
null,
|
||||||
toEmailModel(to ? email + ',' + to : email),
|
to,
|
||||||
toEmailModel(params.get('cc')),
|
cc,
|
||||||
toEmailModel(params.get('bcc')),
|
bcc,
|
||||||
params.get('subject'),
|
null == params.subject ? null : decodeURIComponent(params.subject),
|
||||||
plainToHtml(params.get('body') || '')
|
null == params.body ? null : plainToHtml(decodeURIComponent(params.body))
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
}
|
||||||
|
|
||||||
showMessageComposer = (params = []) =>
|
|
||||||
{
|
|
||||||
rl.app.showMessageComposer(params);
|
|
||||||
},
|
|
||||||
|
|
||||||
setLayoutResizer = (source, sClientSideKeyName, mode) =>
|
|
||||||
{
|
|
||||||
if (source.layoutResizer && source.layoutResizer.mode != mode) {
|
|
||||||
source.removeAttribute('style');
|
|
||||||
}
|
|
||||||
source.observer?.disconnect();
|
|
||||||
// source.classList.toggle('resizable', mode);
|
|
||||||
if (mode) {
|
|
||||||
const length = Local.get(sClientSideKeyName + mode) || SettingsGet('Resizer' + sClientSideKeyName + mode);
|
|
||||||
if (length) {
|
|
||||||
source.style[mode.toLowerCase()] = length + 'px';
|
|
||||||
}
|
|
||||||
if (!source.layoutResizer) {
|
|
||||||
const resizer = createElement('div', {'class':'resizer'}),
|
|
||||||
save = (data => Remote.saveSettings(0, data)).debounce(500),
|
|
||||||
size = {},
|
|
||||||
store = () => {
|
|
||||||
const value = ('Width' == resizer.mode) ? source.offsetWidth : source.offsetHeight,
|
|
||||||
prop = resizer.key + resizer.mode;
|
|
||||||
(value == Local.get(prop)) || Local.set(prop, value);
|
|
||||||
(value == SettingsGet('Resizer' + prop)) || save({['Resizer' + prop]: value});
|
|
||||||
},
|
|
||||||
cssint = s => {
|
|
||||||
let value = getComputedStyle(source, null)[s].replace('px', '');
|
|
||||||
if (value.includes('%')) {
|
|
||||||
value = source.parentElement['offset'+resizer.mode]
|
|
||||||
* value.replace('%', '') / 100;
|
|
||||||
}
|
|
||||||
return parseFloat(value);
|
|
||||||
};
|
|
||||||
source.layoutResizer = resizer;
|
|
||||||
source.append(resizer);
|
|
||||||
resizer.addEventListener('mousedown', {
|
|
||||||
handleEvent: function(e) {
|
|
||||||
if ('mousedown' == e.type) {
|
|
||||||
const lmode = resizer.mode.toLowerCase();
|
|
||||||
e.preventDefault();
|
|
||||||
size.pos = ('width' == lmode) ? e.pageX : e.pageY;
|
|
||||||
size.min = cssint('min-'+lmode);
|
|
||||||
size.max = cssint('max-'+lmode);
|
|
||||||
size.org = cssint(lmode);
|
|
||||||
addEventListener('mousemove', this);
|
|
||||||
addEventListener('mouseup', this);
|
|
||||||
} else if ('mousemove' == e.type) {
|
|
||||||
const lmode = resizer.mode.toLowerCase(),
|
|
||||||
length = size.org + (('width' == lmode ? e.pageX : e.pageY) - size.pos);
|
|
||||||
if (length >= size.min && length <= size.max ) {
|
|
||||||
source.style[lmode] = length + 'px';
|
|
||||||
source.observer || store();
|
|
||||||
}
|
|
||||||
} else if ('mouseup' == e.type) {
|
|
||||||
removeEventListener('mousemove', this);
|
|
||||||
removeEventListener('mouseup', this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
source.observer = window.ResizeObserver ? new ResizeObserver(store) : null;
|
|
||||||
}
|
|
||||||
source.layoutResizer.mode = mode;
|
|
||||||
source.layoutResizer.key = sClientSideKeyName;
|
|
||||||
source.observer?.observe(source, { box: 'border-box' });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
viewMessage = (oMessage, popup) => {
|
|
||||||
if (popup) {
|
|
||||||
oMessage.popupMessage();
|
|
||||||
} else {
|
|
||||||
MessageUserStore.error('');
|
|
||||||
let id = 'rl-msg-' + oMessage.hash,
|
|
||||||
body = oMessage.body || elementById(id);
|
|
||||||
if (!body) {
|
|
||||||
body = createElement('div',{
|
|
||||||
id:id,
|
|
||||||
hidden:'',
|
|
||||||
class:'b-text-part'
|
|
||||||
+ (oMessage.pgpSigned() ? ' openpgp-signed' : '')
|
|
||||||
+ (oMessage.pgpEncrypted() ? ' openpgp-encrypted' : '')
|
|
||||||
+ (oMessage.smimeSigned() ? ' smime-signed' : '')
|
|
||||||
+ (oMessage.smimeEncrypted() ? ' smime-encrypted' : '')
|
|
||||||
});
|
|
||||||
MessageUserStore.purgeCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
body.message = oMessage;
|
|
||||||
oMessage.body = body;
|
|
||||||
|
|
||||||
if (!SettingsUserStore.viewHTML() || !oMessage.viewHtml()) {
|
|
||||||
oMessage.viewPlain();
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageUserStore.bodiesDom().append(body);
|
|
||||||
|
|
||||||
MessageUserStore.loading(false);
|
|
||||||
oMessage.body.hidden = false;
|
|
||||||
|
|
||||||
if (oMessage.isUnseen() && SettingsUserStore.messageReadAuto()) {
|
|
||||||
MessageUserStore.MessageSeenTimer = setTimeout(
|
|
||||||
() => MessagelistUserStore.setAction(oMessage.folder, MessageSetAction.SetSeen, [oMessage]),
|
|
||||||
SettingsUserStore.messageReadDelay() * 1000 // seconds
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
populateMessageBody = (oMessage, popup) => {
|
|
||||||
if (oMessage) {
|
|
||||||
popup || MessageUserStore.message(oMessage);
|
|
||||||
if (oMessage.body) {
|
|
||||||
viewMessage(oMessage, popup);
|
|
||||||
} else {
|
|
||||||
popup || MessageUserStore.loading(true);
|
|
||||||
Remote.message((iError, oData/*, bCached*/) => {
|
|
||||||
if (iError) {
|
|
||||||
if (Notifications.RequestAborted !== iError && !popup) {
|
|
||||||
MessageUserStore.message(null);
|
|
||||||
MessageUserStore.error(getNotification(iError));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let json = oData?.Result;
|
|
||||||
if (json
|
|
||||||
&& ((
|
|
||||||
oMessage.hash && oMessage.hash === json.hash
|
|
||||||
) || (
|
|
||||||
!oMessage.hash
|
|
||||||
&& oMessage.folder === json.folder
|
|
||||||
&& oMessage.uid == json.uid)
|
|
||||||
)
|
|
||||||
&& oMessage.revivePropertiesFromJson(json)
|
|
||||||
) {
|
|
||||||
/*
|
|
||||||
if (bCached) {
|
|
||||||
delete json.flags;
|
|
||||||
}
|
|
||||||
oMessage.body.remove();
|
|
||||||
*/
|
|
||||||
viewMessage(oMessage, popup);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
popup || MessageUserStore.loading(false);
|
|
||||||
}, oMessage.folder, oMessage.uid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
leftPanelDisabled.subscribe(value => value && moveAction(0));
|
|
||||||
moveAction.subscribe(value => value && leftPanelDisabled(false));
|
|
||||||
|
|
|
||||||
45
dev/Component/Abstract.js
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import ko from 'ko';
|
||||||
|
|
||||||
|
import { i18nToNodes } from 'Common/Translator';
|
||||||
|
|
||||||
|
class AbstractComponent {
|
||||||
|
disposable = [];
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.disposable.forEach((funcToDispose) => {
|
||||||
|
if (funcToDispose && funcToDispose.dispose) {
|
||||||
|
funcToDispose.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {*} ClassObject
|
||||||
|
* @param {string} templateID = ''
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
const componentExportHelper = (ClassObject, templateID = '') => ({
|
||||||
|
template: templateID ? { element: templateID } : '<b></b>',
|
||||||
|
viewModel: {
|
||||||
|
createViewModel: (params, componentInfo) => {
|
||||||
|
params = params || {};
|
||||||
|
params.element = null;
|
||||||
|
|
||||||
|
if (componentInfo && componentInfo.element) {
|
||||||
|
params.component = componentInfo;
|
||||||
|
params.element = componentInfo.element;
|
||||||
|
|
||||||
|
i18nToNodes(componentInfo.element);
|
||||||
|
|
||||||
|
if (undefined !== params.inline && ko.unwrap(params.inline)) {
|
||||||
|
params.element.style.display = 'inline-block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ClassObject(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export { AbstractComponent, componentExportHelper };
|
||||||
40
dev/Component/AbstractCheckbox.js
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import ko from 'ko';
|
||||||
|
import { AbstractComponent } from 'Component/Abstract';
|
||||||
|
|
||||||
|
class AbstractCheckbox extends AbstractComponent {
|
||||||
|
/**
|
||||||
|
* @param {Object} params = {}
|
||||||
|
*/
|
||||||
|
constructor(params = {}) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.value = params.value;
|
||||||
|
if (undefined === this.value || !this.value.subscribe) {
|
||||||
|
this.value = ko.observable(!!this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.enable = params.enable;
|
||||||
|
if (undefined === this.enable || !this.enable.subscribe) {
|
||||||
|
this.enable = ko.observable(undefined === this.enable || !!this.enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.disable = params.disable;
|
||||||
|
if (undefined === this.disable || !this.disable.subscribe) {
|
||||||
|
this.disable = ko.observable(!!this.disable);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.label = params.label || '';
|
||||||
|
this.inline = !!params.inline;
|
||||||
|
|
||||||
|
this.labeled = undefined !== params.label;
|
||||||
|
this.labelAnimated = !!params.labelAnimated;
|
||||||
|
}
|
||||||
|
|
||||||
|
click() {
|
||||||
|
if (this.enable() && !this.disable()) {
|
||||||
|
this.value(!this.value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { AbstractCheckbox, AbstractCheckbox as default };
|
||||||
61
dev/Component/AbstractInput.js
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import ko from 'ko';
|
||||||
|
import { pInt } from 'Common/Utils';
|
||||||
|
import { SaveSettingsStep } from 'Common/Enums';
|
||||||
|
import { AbstractComponent } from 'Component/Abstract';
|
||||||
|
|
||||||
|
class AbstractInput extends AbstractComponent {
|
||||||
|
/**
|
||||||
|
* @param {Object} params
|
||||||
|
*/
|
||||||
|
constructor(params) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.value = params.value || '';
|
||||||
|
this.size = params.size || 0;
|
||||||
|
this.label = params.label || '';
|
||||||
|
this.preLabel = params.preLabel || '';
|
||||||
|
this.enable = undefined === params.enable ? true : params.enable;
|
||||||
|
this.trigger = params.trigger && params.trigger.subscribe ? params.trigger : null;
|
||||||
|
this.placeholder = params.placeholder || '';
|
||||||
|
|
||||||
|
this.labeled = undefined !== params.label;
|
||||||
|
this.preLabeled = undefined !== params.preLabel;
|
||||||
|
this.triggered = undefined !== params.trigger && !!this.trigger;
|
||||||
|
|
||||||
|
this.classForTrigger = ko.observable('');
|
||||||
|
|
||||||
|
this.className = ko.computed(() => {
|
||||||
|
const size = ko.unwrap(this.size),
|
||||||
|
suffixValue = this.trigger ? ' ' + ('settings-saved-trigger-input ' + this.classForTrigger()).trim() : '';
|
||||||
|
return (0 < size ? 'span' + size : '') + suffixValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (undefined !== params.width && params.element) {
|
||||||
|
params.element.querySelectorAll('input,select,textarea').forEach(node => node.style.width = params.width);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.disposable.push(this.className);
|
||||||
|
|
||||||
|
if (this.trigger) {
|
||||||
|
this.setTriggerState(this.trigger());
|
||||||
|
|
||||||
|
this.disposable.push(this.trigger.subscribe(this.setTriggerState, this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTriggerState(value) {
|
||||||
|
switch (pInt(value)) {
|
||||||
|
case SaveSettingsStep.TrueResult:
|
||||||
|
this.classForTrigger('success');
|
||||||
|
break;
|
||||||
|
case SaveSettingsStep.FalseResult:
|
||||||
|
this.classForTrigger('error');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.classForTrigger('');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { AbstractInput, AbstractInput as default };
|
||||||
|
|
@ -1,17 +1,6 @@
|
||||||
export class CheckboxComponent {
|
import { componentExportHelper } from 'Component/Abstract';
|
||||||
constructor(params = {}) {
|
import { AbstractCheckbox } from 'Component/AbstractCheckbox';
|
||||||
this.name = params.name;
|
|
||||||
|
|
||||||
this.value = ko.isObservable(params.value) ? params.value
|
class CheckboxComponent extends AbstractCheckbox {}
|
||||||
: ko.observable(!!params.value);
|
|
||||||
|
|
||||||
this.enable = ko.isObservable(params.enable) ? params.enable
|
export default componentExportHelper(CheckboxComponent, 'CheckboxComponent');
|
||||||
: ko.observable(params.enable ?? 1);
|
|
||||||
|
|
||||||
this.label = params.label;
|
|
||||||
}
|
|
||||||
|
|
||||||
click() {
|
|
||||||
this.enable() && this.value(!this.value());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,437 +0,0 @@
|
||||||
import { doc, createElement, addEventsListeners } from 'Common/Globals';
|
|
||||||
import { EmailModel } from 'Model/Email';
|
|
||||||
import { addressparser } from 'Mime/Address';
|
|
||||||
|
|
||||||
const contentType = 'snappymail/emailaddress',
|
|
||||||
getAddressKey = li => li?.emailaddress?.key,
|
|
||||||
|
|
||||||
parseEmailLine = line => addressparser(line).map(item =>
|
|
||||||
(item.name || item.email)
|
|
||||||
? new EmailModel(item.email, item.name) : null
|
|
||||||
).filter(v => v),
|
|
||||||
splitEmailLine = line => {
|
|
||||||
const result = [];
|
|
||||||
let exists = false;
|
|
||||||
addressparser(line).forEach(item => {
|
|
||||||
const address = (item.name || item.email)
|
|
||||||
? new EmailModel(item.email, item.name)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (address?.email) {
|
|
||||||
exists = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push(address ? address.toLine() : item.name);
|
|
||||||
});
|
|
||||||
return exists ? result : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
let dragAddress, datalist;
|
|
||||||
|
|
||||||
// mailbox-list
|
|
||||||
export class EmailAddressesComponent {
|
|
||||||
|
|
||||||
constructor(element, options) {
|
|
||||||
|
|
||||||
if (!datalist) {
|
|
||||||
datalist = createElement('datalist',{id:"emailaddresses-datalist"});
|
|
||||||
doc.body.append(datalist);
|
|
||||||
}
|
|
||||||
|
|
||||||
const self = this,
|
|
||||||
input = createElement('input',{type:"text", list:datalist.id,
|
|
||||||
autocomplete:"off", autocorrect:"off", autocapitalize:"off"}),
|
|
||||||
// In Chrome we have no access to dataTransfer.getData unless it's the 'drop' event
|
|
||||||
// In Chrome Mobile dataTransfer.types.includes(contentType) fails, only text/plain is set
|
|
||||||
validDropzone = () => dragAddress?.li.parentNode !== self.ul,
|
|
||||||
fnDrag = e => validDropzone() && e.preventDefault();
|
|
||||||
|
|
||||||
self.element = element;
|
|
||||||
|
|
||||||
self.options = Object.assign({
|
|
||||||
|
|
||||||
focusCallback : null,
|
|
||||||
|
|
||||||
// simply passing an autoComplete source (array, string or function) will instantiate autocomplete functionality
|
|
||||||
autoCompleteSource : '',
|
|
||||||
|
|
||||||
onChange : null
|
|
||||||
}, options);
|
|
||||||
|
|
||||||
self._chosenValues = [];
|
|
||||||
|
|
||||||
self._lastEdit = '';
|
|
||||||
|
|
||||||
// Create the elements
|
|
||||||
self.ul = createElement('ul',{class:"emailaddresses"});
|
|
||||||
|
|
||||||
addEventsListeners(self.ul, {
|
|
||||||
click: e => self._focus(e),
|
|
||||||
dblclick: e => self._editTag(e),
|
|
||||||
dragenter: fnDrag,
|
|
||||||
dragover: fnDrag,
|
|
||||||
drop: e => {
|
|
||||||
if (validDropzone() && dragAddress.value) {
|
|
||||||
e.preventDefault();
|
|
||||||
dragAddress.source._removeDraggedTag(dragAddress.li);
|
|
||||||
self._parseValue(dragAddress.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.input = input;
|
|
||||||
|
|
||||||
addEventsListeners(input, {
|
|
||||||
focus: () => {
|
|
||||||
self._focusTrigger(true);
|
|
||||||
input.value || self._resetDatalist();
|
|
||||||
},
|
|
||||||
blur: () => {
|
|
||||||
// prevent autoComplete menu click from causing a false 'blur'
|
|
||||||
self._parseInput(true);
|
|
||||||
self._focusTrigger(false);
|
|
||||||
},
|
|
||||||
keydown: e => {
|
|
||||||
if ('Backspace' === e.key || 'ArrowLeft' === e.key) {
|
|
||||||
// if our input contains no value and backspace has been pressed, select the last tag
|
|
||||||
var lastTag = self.inputCont.previousElementSibling;
|
|
||||||
if (lastTag && (!input.value
|
|
||||||
|| (('selectionStart' in input) && input.selectionStart === 0 && input.selectionEnd === 0))
|
|
||||||
) {
|
|
||||||
e.preventDefault();
|
|
||||||
lastTag.querySelector('a').focus();
|
|
||||||
}
|
|
||||||
self._updateDatalist();
|
|
||||||
} else if (e.key == 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
self._parseInput(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
input: () => {
|
|
||||||
self._parseInput();
|
|
||||||
self._updateDatalist();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// define starting placeholder
|
|
||||||
if (element.placeholder) {
|
|
||||||
input.placeholder = element.placeholder;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.inputCont = createElement('li',{class:"emailaddresses-input"});
|
|
||||||
self.inputCont.append(input);
|
|
||||||
self.ul.append(self.inputCont);
|
|
||||||
|
|
||||||
element.replaceWith(self.ul);
|
|
||||||
|
|
||||||
// if instantiated input already contains a value, parse that junk
|
|
||||||
if (element.value.trim()) {
|
|
||||||
self._parseValue(element.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
self._updateDatalist = self.options.autoCompleteSource
|
|
||||||
? (() => {
|
|
||||||
let value = input.value.trim();
|
|
||||||
if (datalist.inputValue !== value) {
|
|
||||||
datalist.inputValue = value;
|
|
||||||
value.length && self.options.autoCompleteSource(
|
|
||||||
value,
|
|
||||||
items => {
|
|
||||||
self._resetDatalist();
|
|
||||||
let chars = value.length;
|
|
||||||
items?.forEach(item => {
|
|
||||||
datalist.append(new Option(item));
|
|
||||||
chars = Math.max(chars, item.length);
|
|
||||||
});
|
|
||||||
// https://github.com/the-djmaze/snappymail/issues/368 and #513
|
|
||||||
chars *= 8;
|
|
||||||
if (input.clientWidth < chars) {
|
|
||||||
input.style.width = chars + 'px';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}).throttle(500)
|
|
||||||
: () => 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
_focusTrigger(bValue) {
|
|
||||||
this.ul.classList.toggle('emailaddresses-focused', bValue);
|
|
||||||
this.options.focusCallback(bValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
_resetDatalist() {
|
|
||||||
datalist.textContent = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
_parseInput(force) {
|
|
||||||
let val = this.input.value;
|
|
||||||
if ((force || val.includes(',') || val.includes(';')
|
|
||||||
|| (val.charAt(val.length-1)===' ' && this._simpleEmailMatch(val)))
|
|
||||||
&& this._parseValue(val)) {
|
|
||||||
this.input.value = '';
|
|
||||||
}
|
|
||||||
this._resizeInput();
|
|
||||||
}
|
|
||||||
|
|
||||||
_parseValue(val) {
|
|
||||||
if (val) {
|
|
||||||
const self = this,
|
|
||||||
v = val.trim(),
|
|
||||||
hook = (v && [',', ';', '\n'].includes(v.slice(-1))) ? splitEmailLine(val) : null,
|
|
||||||
values = (hook || [val]).map(value => parseEmailLine(value))
|
|
||||||
.flat(Infinity)
|
|
||||||
.map(item => (item.toLine ? [item.toLine(), item] : [item, null]));
|
|
||||||
|
|
||||||
if (values.length) {
|
|
||||||
values.forEach(a => {
|
|
||||||
var v = a[0].trim(),
|
|
||||||
exists = false,
|
|
||||||
lastIndex = -1,
|
|
||||||
obj = {
|
|
||||||
key : '',
|
|
||||||
obj : null,
|
|
||||||
value : ''
|
|
||||||
};
|
|
||||||
|
|
||||||
self._chosenValues.forEach((vv, kk) => {
|
|
||||||
if (vv.value === self._lastEdit) {
|
|
||||||
lastIndex = kk;
|
|
||||||
}
|
|
||||||
|
|
||||||
exists |= vv.value === v;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (v !== '' && a[1] && !exists) {
|
|
||||||
|
|
||||||
obj.key = 'mi_' + Math.random().toString( 16 ).slice( 2, 10 );
|
|
||||||
obj.value = v;
|
|
||||||
obj.obj = a[1];
|
|
||||||
|
|
||||||
if (-1 < lastIndex) {
|
|
||||||
self._chosenValues.splice(lastIndex, 0, obj);
|
|
||||||
} else {
|
|
||||||
self._chosenValues.push(obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
self._lastEdit = '';
|
|
||||||
self._renderTags();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (1 === values.length && '' === values[0] && '' !== self._lastEdit) {
|
|
||||||
self._lastEdit = '';
|
|
||||||
self._renderTags();
|
|
||||||
}
|
|
||||||
|
|
||||||
self._setValue(self._buildValue());
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// the input dynamically resizes based on the length of its value
|
|
||||||
_resizeInput() {
|
|
||||||
let input = this.input;
|
|
||||||
if (input.clientWidth < input.scrollWidth) {
|
|
||||||
input.style.width = (input.scrollWidth + 20) + 'px';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_editTag(ev) {
|
|
||||||
var li = ev.target.closest('li'),
|
|
||||||
tagKey = getAddressKey(li);
|
|
||||||
|
|
||||||
if (!tagKey) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var self = this,
|
|
||||||
tagName = '',
|
|
||||||
oPrev = null,
|
|
||||||
next = false
|
|
||||||
;
|
|
||||||
|
|
||||||
self._chosenValues.forEach(v => {
|
|
||||||
if (v.key === tagKey) {
|
|
||||||
tagName = v.value;
|
|
||||||
next = true;
|
|
||||||
} else if (next && !oPrev) {
|
|
||||||
oPrev = v;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (oPrev)
|
|
||||||
{
|
|
||||||
self._lastEdit = oPrev.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
li.after(self.inputCont);
|
|
||||||
|
|
||||||
self.input.value = tagName;
|
|
||||||
setTimeout(() => self.input.select(), 100);
|
|
||||||
|
|
||||||
self._removeTag(ev, li);
|
|
||||||
self._resizeInput(ev);
|
|
||||||
}
|
|
||||||
|
|
||||||
_buildValue() {
|
|
||||||
return this._chosenValues.map(v => v.value).join(',');
|
|
||||||
}
|
|
||||||
|
|
||||||
_setValue(value) {
|
|
||||||
if (this.element.value !== value) {
|
|
||||||
this.element.value = value;
|
|
||||||
this.options.onChange(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_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() {
|
|
||||||
let self = this;
|
|
||||||
[...self.ul.children].forEach(node => node !== self.inputCont && node.remove());
|
|
||||||
|
|
||||||
self._chosenValues.forEach(v => {
|
|
||||||
if (v.obj) {
|
|
||||||
let li = createElement('li',{title:v.obj.toLine(),draggable:'true'}),
|
|
||||||
el = createElement('span');
|
|
||||||
el.append(v.obj.toLine(true));
|
|
||||||
li.append(el);
|
|
||||||
|
|
||||||
el = createElement('a',{href:'#', class:'ficon'});
|
|
||||||
el.append('✖');
|
|
||||||
addEventsListeners(el, {
|
|
||||||
click: e => self._removeTag(e, li),
|
|
||||||
focus: () => li.className = 'emailaddresses-selected',
|
|
||||||
blur: () => li.className = null,
|
|
||||||
keydown: e => {
|
|
||||||
switch (e.key) {
|
|
||||||
case 'Delete':
|
|
||||||
case 'Backspace':
|
|
||||||
self._removeTag(e, li);
|
|
||||||
break;
|
|
||||||
|
|
||||||
// 'e' - edit tag (removes tag and places value into visible input
|
|
||||||
case 'e':
|
|
||||||
case 'Enter':
|
|
||||||
self._editTag(e);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'ArrowLeft':
|
|
||||||
// select the previous tag or input if no more tags exist
|
|
||||||
var previous = el.closest('li').previousElementSibling;
|
|
||||||
if (previous.matches('li')) {
|
|
||||||
previous.querySelector('a').focus();
|
|
||||||
} else {
|
|
||||||
self.focus();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'ArrowRight':
|
|
||||||
// select the next tag or input if no more tags exist
|
|
||||||
var next = el.closest('li').nextElementSibling;
|
|
||||||
if (next !== this.inputCont) {
|
|
||||||
next.querySelector('a').focus();
|
|
||||||
} else {
|
|
||||||
this.focus();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'ArrowDown':
|
|
||||||
self._focus(e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
li.append(el);
|
|
||||||
|
|
||||||
li.emailaddress = v;
|
|
||||||
|
|
||||||
addEventsListeners(li, {
|
|
||||||
dragstart: e => {
|
|
||||||
dragAddress = {
|
|
||||||
source: self,
|
|
||||||
li: li,
|
|
||||||
value: li.emailaddress.obj.toLine()
|
|
||||||
};
|
|
||||||
// e.dataTransfer.setData(contentType, li.emailaddress.obj.toLine());
|
|
||||||
e.dataTransfer.setData('text/plain', contentType);
|
|
||||||
// e.dataTransfer.setDragImage(li, 0, 0);
|
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
|
||||||
li.style.opacity = 0.25;
|
|
||||||
},
|
|
||||||
dragend: () => {
|
|
||||||
dragAddress = null;
|
|
||||||
li.style.cssText = '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.inputCont.before(li);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_removeTag(ev, li) {
|
|
||||||
ev.preventDefault();
|
|
||||||
|
|
||||||
var key = getAddressKey(li),
|
|
||||||
self = this,
|
|
||||||
indexFound = self._chosenValues.findIndex(v => key === v.key);
|
|
||||||
|
|
||||||
indexFound > -1 && self._chosenValues.splice(indexFound, 1);
|
|
||||||
|
|
||||||
self._setValue(self._buildValue());
|
|
||||||
|
|
||||||
li.remove();
|
|
||||||
setTimeout(() => self.input.focus(), 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
_removeDraggedTag(li) {
|
|
||||||
var
|
|
||||||
key = getAddressKey(li),
|
|
||||||
self = this,
|
|
||||||
indexFound = self._chosenValues.findIndex(v => key === v.key)
|
|
||||||
;
|
|
||||||
if (-1 < indexFound) {
|
|
||||||
self._chosenValues.splice(indexFound, 1);
|
|
||||||
self._setValue(self._buildValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
li.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
focus () {
|
|
||||||
this.input.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
blur() {
|
|
||||||
this.input.blur();
|
|
||||||
}
|
|
||||||
|
|
||||||
_focus(ev) {
|
|
||||||
var li = ev.target.closest('li');
|
|
||||||
if (getAddressKey(li)) {
|
|
||||||
li.querySelector('a').focus();
|
|
||||||
} else {
|
|
||||||
this.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set value(value) {
|
|
||||||
var self = this;
|
|
||||||
if (self.element.value !== value) {
|
|
||||||
// self.input.value = '';
|
|
||||||
// self._resizeInput();
|
|
||||||
self._chosenValues = [];
|
|
||||||
self._renderTags();
|
|
||||||
self._parseValue(self.element.value = value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
6
dev/Component/Input.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { componentExportHelper } from 'Component/Abstract';
|
||||||
|
import { AbstractInput } from 'Component/AbstractInput';
|
||||||
|
|
||||||
|
class InputComponent extends AbstractInput {}
|
||||||
|
|
||||||
|
export default componentExportHelper(InputComponent, 'InputComponent');
|
||||||
44
dev/Component/MaterialDesign/Checkbox.js
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import ko from 'ko';
|
||||||
|
import { componentExportHelper } from 'Component/Abstract';
|
||||||
|
import { AbstractCheckbox } from 'Component/AbstractCheckbox';
|
||||||
|
|
||||||
|
class CheckboxMaterialDesignComponent extends AbstractCheckbox {
|
||||||
|
/**
|
||||||
|
* @param {Object} params
|
||||||
|
*/
|
||||||
|
constructor(params) {
|
||||||
|
super(params);
|
||||||
|
|
||||||
|
this.animationBox = ko.observable(false).extend({ falseTimeout: 200 });
|
||||||
|
this.animationCheckmark = ko.observable(false).extend({ falseTimeout: 200 });
|
||||||
|
|
||||||
|
this.animationBoxSetTrue = this.animationBoxSetTrue.bind(this);
|
||||||
|
this.animationCheckmarkSetTrue = this.animationCheckmarkSetTrue.bind(this);
|
||||||
|
|
||||||
|
this.disposable.push(
|
||||||
|
this.value.subscribe((value) => {
|
||||||
|
this.triggerAnimation(value);
|
||||||
|
}, this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
animationBoxSetTrue() {
|
||||||
|
this.animationBox(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
animationCheckmarkSetTrue() {
|
||||||
|
this.animationCheckmark(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerAnimation(box) {
|
||||||
|
if (box) {
|
||||||
|
this.animationBoxSetTrue();
|
||||||
|
setTimeout(this.animationCheckmarkSetTrue, 200);
|
||||||
|
} else {
|
||||||
|
this.animationCheckmarkSetTrue();
|
||||||
|
setTimeout(this.animationBoxSetTrue, 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default componentExportHelper(CheckboxMaterialDesignComponent, 'CheckboxMaterialDesignComponent');
|
||||||
40
dev/Component/SaveTrigger.js
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { SaveSettingsStep } from 'Common/Enums';
|
||||||
|
import { AbstractComponent, componentExportHelper } from 'Component/Abstract';
|
||||||
|
|
||||||
|
class SaveTriggerComponent extends AbstractComponent {
|
||||||
|
/**
|
||||||
|
* @param {Object} params
|
||||||
|
*/
|
||||||
|
constructor(params) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
const el = params.element;
|
||||||
|
this.element = el || null;
|
||||||
|
this.value = params.value && params.value.subscribe ? params.value : null;
|
||||||
|
|
||||||
|
if (el) {
|
||||||
|
if (this.value) {
|
||||||
|
el.style.display = 'inline-block';
|
||||||
|
|
||||||
|
if (params.verticalAlign) {
|
||||||
|
el.style.verticalAlign = params.verticalAlign;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState(this.value());
|
||||||
|
|
||||||
|
this.disposable.push(this.value.subscribe(this.setState, this));
|
||||||
|
} else {
|
||||||
|
el.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(value) {
|
||||||
|
value = parseInt(value,10);
|
||||||
|
this.element.querySelector('.animated').hidden = value !== SaveSettingsStep.Animate;
|
||||||
|
this.element.querySelector('.success').hidden = value !== SaveSettingsStep.TrueResult;
|
||||||
|
this.element.querySelector('.error').hidden = value !== SaveSettingsStep.FalseResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default componentExportHelper(SaveTriggerComponent, 'SaveTriggerComponent');
|
||||||
31
dev/Component/Script.js
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { AbstractComponent, componentExportHelper } from 'Component/Abstract';
|
||||||
|
|
||||||
|
class ScriptComponent extends AbstractComponent {
|
||||||
|
/**
|
||||||
|
* @param {Object} params
|
||||||
|
*/
|
||||||
|
constructor(params) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
if (
|
||||||
|
params.component &&
|
||||||
|
params.component.templateNodes &&
|
||||||
|
params.element
|
||||||
|
) {
|
||||||
|
let el = params.element, script = el.outerHTML;
|
||||||
|
script = script ? script.replace(/<x-script/i, '<script').replace(/<b><\/b><\/x-script>/i, '</script>') : '';
|
||||||
|
|
||||||
|
if (script) {
|
||||||
|
const koNodes = params.component.templateNodes[0];
|
||||||
|
el.textContent = '';
|
||||||
|
el.replaceWith(
|
||||||
|
Element.fromHTML(script).textContent = koNodes && koNodes.nodeValue ? koNodes.nodeValue : ''
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default componentExportHelper(ScriptComponent, 'ScriptComponent');
|
||||||
|
|
@ -1,56 +1,27 @@
|
||||||
import { SaveSettingStatus } from 'Common/Enums';
|
import { i18n } from 'Common/Translator';
|
||||||
import { dispose, koComputable } from 'External/ko';
|
|
||||||
import { defaultOptionsAfterRender } from 'Common/Utils';
|
import { defaultOptionsAfterRender } from 'Common/Utils';
|
||||||
|
import { componentExportHelper } from 'Component/Abstract';
|
||||||
|
import { AbstractInput } from 'Component/AbstractInput';
|
||||||
|
|
||||||
export class SelectComponent {
|
class SelectComponent extends AbstractInput {
|
||||||
/**
|
/**
|
||||||
* @param {Object} params
|
* @param {Object} params
|
||||||
*/
|
*/
|
||||||
constructor(params) {
|
constructor(params) {
|
||||||
this.value = params.value;
|
super(params);
|
||||||
this.label = params.label;
|
|
||||||
this.trigger = params.trigger?.subscribe ? params.trigger : null;
|
|
||||||
this.placeholder = params.placeholder;
|
|
||||||
this.options = params.options;
|
|
||||||
this.optionsText = params.optionsText;
|
|
||||||
this.optionsValue = params.optionsValue;
|
|
||||||
|
|
||||||
let size = 0 < params.size ? 'span' + params.size : '';
|
this.options = params.options || '';
|
||||||
if (this.trigger) {
|
|
||||||
const
|
|
||||||
classForTrigger = ko.observable(''),
|
|
||||||
setTriggerState = value => {
|
|
||||||
switch (value) {
|
|
||||||
case SaveSettingStatus.Success:
|
|
||||||
classForTrigger('success');
|
|
||||||
break;
|
|
||||||
case SaveSettingStatus.Failed:
|
|
||||||
classForTrigger('error');
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
classForTrigger('');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
setTriggerState(this.trigger());
|
this.optionsText = params.optionsText || null;
|
||||||
|
this.optionsValue = params.optionsValue || null;
|
||||||
|
this.optionsCaption = params.optionsCaption || null;
|
||||||
|
|
||||||
this.className = koComputable(() =>
|
if (this.optionsCaption) {
|
||||||
(size + ' settings-save-trigger-input ' + classForTrigger()).trim()
|
this.optionsCaption = i18n(this.optionsCaption);
|
||||||
);
|
|
||||||
|
|
||||||
this.disposables = [
|
|
||||||
this.trigger.subscribe(setTriggerState, this),
|
|
||||||
this.className
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
this.className = size;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
|
this.defaultOptionsAfterRender = defaultOptionsAfterRender;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose() {
|
|
||||||
this.disposables?.forEach(dispose);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default componentExportHelper(SelectComponent, 'SelectComponent');
|
||||||
|
|
|
||||||
18
dev/Component/TextArea.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { componentExportHelper } from 'Component/Abstract';
|
||||||
|
import { AbstractInput } from 'Component/AbstractInput';
|
||||||
|
|
||||||
|
const DEFAULT_ROWS = 5;
|
||||||
|
|
||||||
|
class TextAreaComponent extends AbstractInput {
|
||||||
|
/**
|
||||||
|
* @param {Object} params
|
||||||
|
*/
|
||||||
|
constructor(params) {
|
||||||
|
super(params);
|
||||||
|
|
||||||
|
this.rows = params.rows || DEFAULT_ROWS;
|
||||||
|
this.spellcheck = !!params.spellcheck;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default componentExportHelper(TextAreaComponent, 'TextAreaComponent');
|
||||||