Compare commits

..

No commits in common. "master" and "v2.2.1" have entirely different histories.

3190 changed files with 173417 additions and 296035 deletions

0
.docker/dev/nginx/ssl.sh Executable file → Normal file
View file

View 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

View file

@ -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
View 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'

View file

@ -0,0 +1,5 @@
/snappymail/data/_data_/_default_/logs/* {
size 10M
rotate 0
missingok
}

View file

@ -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;

View 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}

View file

@ -1,3 +0,0 @@
<?php
define('APP_DATA_FOLDER_PATH', '/var/lib/snappymail/');
?>

View file

@ -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,10 +27,33 @@ 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

View file

@ -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

View 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"

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -1,2 +0,0 @@
/.git
/node_modules

View file

@ -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
View file

@ -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"]

View file

@ -1,5 +1,5 @@
**SnappyMail version, browser, browser version:** **RainLoop version, browser, OS:**
**Expected behavior and actual behavior:** **Expected behavior and actual behavior:**

View file

@ -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.

View file

@ -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.

View file

@ -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

View file

@ -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

12
.gitignore vendored
View file

@ -12,21 +12,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
View file

@ -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

View file

@ -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
View 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

File diff suppressed because it is too large Load diff

View file

@ -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
--- ---

View file

@ -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 \

169
README.md
View file

@ -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>
[![github-actions](https://github.com/the-djmaze/snappymail/actions/workflows/docker.yml/badge.svg)](https://github.com/the-djmaze/snappymail/actions/workflows/docker.yml)
[![docker-image-size](https://img.shields.io/docker/image-size/djmaze/snappymail/latest)](https://hub.docker.com/r/djmaze/snappymail/tags)
<p> <p>
Simple, modern, lightweight &amp; fast web-based email client. Simple, modern, lightweight &amp; 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,65 @@ 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 - 2021 SnappyMail
Copyright (c) 2013 - 2022 RainLoop Copyright (c) 2013 - 2021 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 * Added advanced Sieve scripts editor
* Slimmed down language files * Slimmed down language files
* Replaced webpack with rollup * 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).
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 * 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 +107,60 @@ 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.15 vs SnappyMail
|js/* |RainLoop |Snappy | |js/* |RainLoop |Snappy |
|--------------- |--------: |--------: | |--------------- |--------: |--------: |
|admin.js |2.170.153 | 84.925 | |admin.js |2.158.025 | 119.197 |
|app.js |4.207.787 | 447.263 | |app.js |4.215.733 | 527.415 |
|boot.js | 868.735 | 4.343 | |boot.js | 672.433 | 4.842 |
|libs.js | 658.812 | 233.728 | |libs.js | 647.679 | 235.475 |
|sieve.js | 0 | 91.418 | |polyfills.js | 325.908 | 0 |
|polyfills.js | 334.608 | 0 |
|serviceworker.js | 0 | 285 | |serviceworker.js | 0 | 285 |
|TOTAL |8.240.095 | 861.962 | |TOTAL |8.019.778 | 887.214 |
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli | |js/min/* |RainLoop |Snappy |Rain gzip |S gzip |R brotli |S brotli |
|--------------- |--------: |--------: |------: |------: |--------: |--------: | |--------------- |--------: |--------: |--------: |------: |-------: |-------: |
|admin.min.js | 256.831 | 41.719 | 73.606 | 14.022 | 60.877 | 12.567 | |admin.min.js | 255.514 | 61.165 | 73.899 | 17.419 | 60.674 | 15.447 |
|app.min.js | 515.367 | 202.101 |139.456 | 68.505 |110.485 | 58.481 | |app.min.js | 516.000 | 256.632 |140.430 | 73.688 |110.657 | 61.812 |
|boot.min.js | 84.659 | 2.231 | 26.998 | 1.271 | 23.643 | 1.067 | |boot.min.js | 66.456 | 2.630 | 22.553 | 1.375 | 20.043 | 1.189 |
|libs.min.js | 584.772 | 110.646 |180.901 | 39.518 |155.182 | 35.207 | |libs.min.js | 574.626 | 130.930 |177.280 | 47.397 |151.855 | 42.116 |
|sieve.min.js | 0 | 45.504 | 0 | 11.131 | 0 | 9.917 | |polyfills.min.js | 32.608 | 0 | 11.315 | 0 | 10.072 | 0 |
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 | |TOTAL |1.445.204 | 451.357 |425.477 |139.879 |353.301 |120.564 |
|TOTAL user |1.217.635 | 314.978 |358.761 |109.294 |299.485 | 94.755 | |TOTAL (no admin) |1.189.690 | 390.192 |351.061 |122.460 |292.627 |105.117 |
|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 65% 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 * Cleanup unused css
* 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 | 183.662 | 46,959 | 28.665 | 24.135 |
|app.min.css | 274.947 | 68.272 | 39.647 | 15.615 | 13.636 | |app.min.css | 274.791 | 149.991 | 39.618 | 25.303 | 21.868 |
|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 +169,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

View file

@ -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.

View file

@ -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);

View file

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 992 B

After

Width:  |  Height:  |  Size: 992 B

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

Before

Width:  |  Height:  |  Size: 992 B

After

Width:  |  Height:  |  Size: 992 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 866 B

After

Width:  |  Height:  |  Size: 866 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 335 B

After

Width:  |  Height:  |  Size: 335 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 992 B

After

Width:  |  Height:  |  Size: 992 B

Before After
Before After

16
assets/manifest.json Normal file
View 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

Binary file not shown.

BIN
assets/sounds/new-mail.ogg Normal file

Binary file not shown.

View 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-----

View file

@ -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"
}

View file

@ -1 +0,0 @@
u snappymail - "Snappymail User" /var/lib/snappymail

View file

@ -1 +0,0 @@
Z /var/lib/snappymail - snappymail snappymail

View file

@ -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";

View file

@ -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);

View file

@ -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.

View file

@ -1,3 +0,0 @@
#!/bin/sh
chown -R www-data:www-data /var/lib/snappymail
chown -R root:root /usr/share/snappymail/

View file

@ -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";

View file

@ -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";

View file

@ -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;
}
}

File diff suppressed because it is too large Load diff

View file

@ -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);
}
}

View file

@ -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>';
}
}

File diff suppressed because it is too large Load diff

View file

@ -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;
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,6 +0,0 @@
# minimalist openssl.cnf file for use with phpseclib
HOME = .
RANDFILE = $ENV::HOME/.rnd
[ v3_ca ]

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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

View file

@ -1,14 +1,24 @@
import ko from 'ko'; import ko from 'ko';
import {
doc,
$htmlCL,
leftPanelDisabled,
Settings
} from 'Common/Globals';
import { KeyState } from 'Common/Enums';
import { logoutLink } from 'Common/Links'; import { logoutLink } from 'Common/Links';
import { i18nToNodes, initOnStartOrLangChange } from 'Common/Translator'; import { i18nToNodes, initOnStartOrLangChange } from 'Common/Translator';
import { arePopupsVisible } from 'Knoin/Knoin';
import { LanguageStore } from 'Stores/Language'; import { LanguageStore } from 'Stores/Language';
import { initThemes } from 'Stores/Theme'; import { ThemeStore } from 'Stores/Theme';
import { SaveTriggerComponent } from 'Component/SaveTrigger';
import { InputComponent } from 'Component/Input';
import { SelectComponent } from 'Component/Select'; import { SelectComponent } from 'Component/Select';
import { TextAreaComponent } from 'Component/TextArea';
import { CheckboxMaterialDesignComponent } from 'Component/MaterialDesign/Checkbox';
import { CheckboxComponent } from 'Component/Checkbox'; import { CheckboxComponent } from 'Component/Checkbox';
export class AbstractApp { export class AbstractApp {
@ -17,40 +27,119 @@ export class AbstractApp {
*/ */
constructor(Remote) { constructor(Remote) {
this.Remote = Remote; 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(); }
/**
* @param {string} link
* @returns {boolean}
*/
download(link) {
if (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 url = logoutLink();
rl.hash.clear();
close && window.close && window.close();
if (location.href !== url) { if (location.href !== url) {
setTimeout(() => location.href = url, 100); setTimeout(() => (Settings.app('inIframe') ? parent : window).location.href = url, 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 }, register = (key, ClassObject, templateID) => ko.components.register(key, {
template: { element: templateID || (key + 'Component') },
viewModel: { viewModel: {
createViewModel: (params, componentInfo) => { createViewModel: (params, componentInfo) => {
params = params || {}; params = params || {};
i18nToNodes(componentInfo.element); 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); return new ClassObject(params);
} }
} }
}); });
register('SaveTrigger', SaveTriggerComponent);
register('Input', InputComponent);
register('Select', SelectComponent); register('Select', SelectComponent);
register('Checkbox', CheckboxComponent); register('TextArea', TextAreaComponent);
register('CheckboxSimple', CheckboxComponent, 'CheckboxComponent');
if (mobile || !Settings.app('materialDesign')) {
register('Checkbox', CheckboxComponent);
} else {
register('Checkbox', CheckboxMaterialDesignComponent, 'CheckboxMaterialDesignComponent');
}
initOnStartOrLangChange(); initOnStartOrLangChange();
LanguageStore.populate(); if (mobile) {
initThemes(); $htmlCL.add('rl-mobile');
leftPanelDisabled(true);
} else {
window.addEventListener('resize', () => leftPanelDisabled(767 >= window.innerWidth));
}
this.start(); leftPanelDisabled.valueHasMutated();
LanguageStore.populate();
ThemeStore.populate();
} }
/**
* @returns {void}
*/
hideLoading() {
const id = id => doc.getElementById(id);
id('rl-content').hidden = false;
id('rl-loading').remove();
}
} }

View file

@ -1,8 +1,13 @@
import 'External/ko'; import 'External/Admin/ko';
import ko from '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 +16,108 @@ 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() {
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
]);
});
};

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,9 @@
import * as Links from 'Common/Links'; import * as Links from 'Common/Links';
import { doc, SettingsGet, fireEvent, addEventsListener } from 'Common/Globals'; import { doc } 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 +12,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,12 +45,12 @@ 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 => doc.removeEventListener(type, unlock, true));
// setTimeout(()=>Audio.playNotification(1),1);
}; };
if (audioCtx) { if (audioCtx) {
@ -64,7 +63,7 @@ unlockEvents.forEach(type => doc.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 {
constructor() { constructor() {
player || (player = createNewObject()); player || (player = createNewObject());
@ -74,13 +73,10 @@ export const SMAudio = new class {
this.supportedOgg = canPlay('audio/ogg; codecs="vorbis"'); this.supportedOgg = canPlay('audio/ogg; codecs="vorbis"');
if (player) { if (player) {
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 +88,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 +104,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;

View file

@ -1,56 +1,299 @@
let FOLDERS_CACHE = new Map, import { MessageSetAction } from 'Common/EnumsUser';
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;
}
}

View file

@ -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__';

View file

@ -1,56 +1,107 @@
/* 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 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 UploadErrorCode = {
Normal: 0, Normal: 0,
FileIsTooBig: 1, FileIsTooBig: 1,
FilePartiallyUploaded: 3, FilePartiallyUploaded: 2,
NoFileUploaded: 4, NoFileUploaded: 3,
MissingTempFolder: 6, MissingTempFolder: 4,
OnSavingFile: 7, OnSavingFile: 5,
FileType: 98, FileType: 98,
Unknown: 99 Unknown: 99
}, };
/** /**
* @enum {number} * @enum {number}
*/ */
SaveSettingStatus = { export const SaveSettingsStep = {
Saving: -2, Animate: -2,
Idle: -1, Idle: -1,
Success: 1, TrueResult: 1,
Failed: 0 FalseResult: 0
}, };
/** /**
* @enum {number} * @enum {number}
*/ */
Notifications = { export const Notification = {
RequestError: 1,
RequestAborted: 2,
RequestTimeout: 3,
// Global
InvalidToken: 101, InvalidToken: 101,
AuthError: 102, AuthError: 102,
// User
ConnectionError: 104, ConnectionError: 104,
DomainNotAllowed: 109, DomainNotAllowed: 109,
AccountNotAllowed: 110, AccountNotAllowed: 110,
CryptKeyError: 111,
AccountTwoFactorAuthRequired: 120,
AccountTwoFactorAuthError: 121,
// CurrentPasswordIncorrect: 131,
// NewPasswordShort: 132,
// NewPasswordWeak: 133,
// NewPasswordForbidden: 134,
ContactsSyncError: 140, ContactsSyncError: 140,
@ -78,30 +129,30 @@ Notifications = {
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, JsonFalse: 950,
JsonAbort: 951,
JsonParse: 952, JsonParse: 952,
// JsonTimeout: 953, // JsonTimeout: 953,
UnknownError: 999, UnknownNotification: 998,
UnknownError: 999
// Admin
CantInstallPackage: 701,
CantDeletePackage: 702,
InvalidPluginPackage: 703,
UnsupportedPluginPackage: 704,
CantSavePluginSettings: 705
}; };

View file

@ -1,85 +1,105 @@
/* eslint quote-props: 0 */ /* eslint quote-props: 0 */
/**
* @enum {string}
*/
export const Focused = {
None: 'none',
MessageList: 'message-list',
MessageView: 'message-view',
FolderList: 'folder-list'
};
/** /**
* @enum {number} * @enum {number}
*/ */
export const FolderType = { export const FolderType = {
Inbox: 1, Inbox: 10,
Sent: 2, SentItems: 11,
Drafts: 3, Draft: 12,
Junk: 4, // Spam Trash: 13,
Trash: 5, Spam: 14,
Archive: 6 Archive: 15,
/* NotSpam: 80,
IMPORTANT : 10; User: 99
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} * @enum {string}
*/ */
FolderMetadataKeys = { export const ComposeType = {
// RFC 5464 Empty: 'empty',
Comment: '/private/comment', Reply: 'reply',
CommentShared: '/shared/comment', ReplyAll: 'replyall',
// RFC 6154 Forward: 'forward',
SpecialUse: '/private/specialuse', ForwardAsAttachment: 'forward-as-attachment',
// Kolab Draft: 'draft',
KolabFolderType: '/private/vendor/kolab/folder-type', EditAsNew: 'editasnew'
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} * @enum {number}
*/ */
ClientSideKeyNameExpandedFolders = 3, export const SetSystemFoldersNotification = {
ClientSideKeyNameFolderListSize = 4, None: 0,
ClientSideKeyNameMessageListSize = 5, Sent: 1,
ClientSideKeyNameLastSignMe = 7, Draft: 2,
ClientSideKeyNameMessageHeaderFullInfo = 9, Spam: 3,
ClientSideKeyNameMessageAttachmentControls = 10, Trash: 4,
Archive: 5
};
/** /**
* @enum {number} * @enum {number}
*/ */
MessageSetAction = { 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, SetSeen: 0,
UnsetSeen: 1, UnsetSeen: 1,
SetFlag: 2, SetFlag: 2,
UnsetFlag: 3, UnsetFlag: 3
SetDeleted: 4, };
UnsetDeleted: 5
},
/** /**
* @enum {number} * @enum {number}
*/ */
//LayoutNoView = 0, export const MessagePriority = {
LayoutSideView = 1, Low: 5,
LayoutBottomView = 2 Normal: 3,
; High: 1
};
/**
* @enum {string}
*/
export const EditorDefaultType = {
Html: 'Html',
Plain: 'Plain',
HtmlForced: 'HtmlForced',
PlainForced: 'PlainForced'
};
/**
* @enum {number}
*/
export const Layout = {
NoPreview: 0,
SidePreview: 1,
BottomPreview: 2
};

View file

@ -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,64 @@ 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].replace('x-','').replace('-compressed',''),
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', 'webp'].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'
].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' == type || 'pdf' == 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' == 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 +241,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 +303,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];
} }
}; };

View file

@ -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;
}
}
};

View file

@ -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());
});
}
}

View file

@ -1,79 +1,55 @@
import ko from 'ko'; import ko from 'ko';
import { KeyState } from 'Common/Enums';
let keyScopeFake = 'all'; export const doc = document;
export const export const $htmlCL = doc.documentElement.classList;
ScopeMenu = 'Menu',
doc = document, export const Settings = rl.settings;
$htmlCL = doc.documentElement.classList, export const dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 });
elementById = id => doc.getElementById(id), export const moveAction = ko.observable(false);
export const leftPanelDisabled = ko.observable(false);
export const leftPanelType = ko.observable('');
appEl = elementById('rl-app'), leftPanelDisabled.subscribe(value => {
value && moveAction() && moveAction(false);
Settings = rl.settings, $htmlCL.toggle('rl-left-panel-disabled', value);
SettingsGet = Settings.get,
SettingsAdmin = name => (SettingsGet('Admin') || {})[name],
SettingsCapa = name => name && !!(SettingsGet('Capa') || {})[name],
dropdowns = [],
dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 }),
leftPanelDisabled = ko.observable(false),
toggleLeftPanel = () => leftPanelDisabled(!leftPanelDisabled()),
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()) {
value = ScopeMenu;
}
}
keyScopeReal(value);
shortcuts.setScope(value);
};
dropdownVisibility.subscribe(value => {
if (value) {
keyScope(ScopeMenu);
} else if (ScopeMenu === shortcuts.getScope()) {
keyScope(keyScopeFake);
}
}); });
leftPanelDisabled.subscribe(value => $htmlCL.toggle('rl-left-panel-disabled', value)); leftPanelType.subscribe(sValue => {
$htmlCL.toggle('rl-left-panel-none', 'none' === sValue);
$htmlCL.toggle('rl-left-panel-short', 'short' === sValue);
});
moveAction.subscribe(value => value && leftPanelDisabled() && leftPanelDisabled(false));
// keys
export const keyScopeReal = ko.observable(KeyState.All);
export const keyScope = (()=>{
let keyScopeFake = KeyState.All;
dropdownVisibility.subscribe(value => {
if (value) {
keyScope(KeyState.Menu);
} else if (KeyState.Menu === shortcuts.getScope()) {
keyScope(keyScopeFake);
}
});
return ko.computed({
read: () => keyScopeFake,
write: value => {
if (KeyState.Menu !== value) {
keyScopeFake = value;
if (dropdownVisibility()) {
value = KeyState.Menu;
}
}
keyScopeReal(value);
}
});
})();
keyScopeReal.subscribe(value => shortcuts.setScope(value));

File diff suppressed because it is too large Load diff

View file

@ -1,161 +0,0 @@
import { SettingsUserStore } from 'Stores/User/Settings';
export const
WYSIWYGS = ko.observableArray();
WYSIWYGS.push({
name: 'Squire',
construct: (owner, container, onReady) => onReady(new SquireUI(container))
});
rl.registerWYSIWYG = (name, construct) => WYSIWYGS.push({name, construct});
export class HtmlEditor {
/**
* @param {Object} element
* @param {Function=} onBlur
* @param {Function=} onReady
* @param {Function=} onModeChange
*/
constructor(element, onReady = null, onModeChange = null, onBlur = null) {
this.blurTimer = 0;
this.onBlur = onBlur;
this.onModeChange = onModeChange;
if (element) {
onReady = onReady ? [onReady] : [];
this.onReady = fn => onReady.push(fn);
const which = SettingsUserStore.editorWysiwyg(),
wysiwyg = WYSIWYGS.find(item => which == item.name) || WYSIWYGS.find(item => 'Squire' == item.name);
wysiwyg.construct(this, element, editor => setTimeout(()=>{
this.editor = editor;
editor.on('blur', () => this.blurTrigger());
editor.on('focus', () => clearTimeout(this.blurTimer));
editor.on('mode', () => {
this.blurTrigger();
this.onModeChange?.(!this.isPlain());
});
this.onReady = fn => fn();
onReady.forEach(fn => fn());
},1));
}
}
blurTrigger() {
if (this.onBlur) {
clearTimeout(this.blurTimer);
this.blurTimer = setTimeout(() => this.onBlur?.(), 200);
}
}
/**
* @returns {boolean}
*/
isHtml() {
return this.editor ? !this.isPlain() : false;
}
/**
* @returns {boolean}
*/
isPlain() {
return this.editor ? 'plain' === this.editor.mode : false;
}
/**
* @returns {void}
*/
clearCachedSignature() {
this.onReady(() => this.editor.execCommand('insertSignature', {
clearCache: true
}));
}
/**
* @param {string} signature
* @param {bool} html
* @param {bool} insertBefore
* @returns {void}
*/
setSignature(signature, html, insertBefore = false) {
this.onReady(() => this.editor.execCommand('insertSignature', {
isHtml: html,
insertBefore: insertBefore,
signature: signature
}));
}
/**
* @param {boolean=} wrapIsHtml = false
* @returns {string}
*/
getData() {
let result = '';
if (this.editor) {
try {
if (this.isPlain()) {
result = this.editor.getPlainData();
} else {
result = this.editor.getData();
}
} catch (e) {} // eslint-disable-line no-empty
}
return result;
}
/**
* @returns {string}
*/
getDataWithHtmlMark() {
return (this.isHtml() ? ':HTML:' : '') + this.getData();
}
modeWysiwyg() {
this.onReady(() => this.editor.setMode('wysiwyg'));
}
modePlain() {
this.onReady(() => this.editor.setMode('plain'));
}
setHtmlOrPlain(text) {
text.startsWith(':HTML:')
? this.setHtml(text.slice(6))
: this.setPlain(text);
}
setData(mode, data) {
this.onReady(() => {
const editor = this.editor;
this.clearCachedSignature();
try {
editor.setMode(mode);
if (this.isPlain()) {
editor.setPlainData(data);
} else {
editor.setData(data);
}
} catch (e) { console.error(e); }
});
}
setHtml(html) {
this.setData('wysiwyg', html/*.replace(/<p[^>]*><\/p>/gi, '')*/);
}
setPlain(txt) {
this.setData('plain', txt);
}
focus() {
this.onReady(() => this.editor.focus());
}
blur() {
this.onReady(() => this.editor.blur());
}
clear() {
this.onReady(() => this.isPlain() ? this.setPlain('') : this.setHtml(''));
}
}

View file

@ -1,143 +1,314 @@
import { pInt } from 'Common/Utils'; import { pString, pInt } from 'Common/Utils';
import { doc, Settings, SettingsAdmin } from 'Common/Globals'; import { Settings } from 'Common/Globals';
const const
BASE = doc.location.pathname.replace(/\/+$/,'') + '/', ROOT = './',
HASH_PREFIX = '#/', HASH_PREFIX = '#/',
SERVER_PREFIX = './?',
VERSION = Settings.app('version'),
VERSION_PREFIX = Settings.app('webVersionPath') || 'snappymail/v/' + VERSION + '/',
adminPath = () => rl.adminArea() && !SettingsAdmin('host'), getHash = () => Settings.get('AuthAccountHash') || '0';
prefix = () => BASE + '?' + (adminPath() ? SettingsAdmin('path') : ''); /**
* @returns {string}
*/
export const SUB_QUERY_PREFIX = '&q[]=';
export const /**
SUB_QUERY_PREFIX = '&q[]=', * @param {string=} startupUrl
* @returns {string}
*/
export function root(startupUrl = '') {
return HASH_PREFIX + pString(startupUrl);
}
/** /**
* @param {string=} startupUrl * @returns {string}
* @returns {string} */
*/ export function rootAdmin() {
root = () => HASH_PREFIX, return Settings.app('adminHostUse') ? ROOT : SERVER_PREFIX + (Settings.app('adminPath') || 'admin');
}
/** /**
* @returns {string} * @returns {string}
*/ */
logoutLink = () => adminPath() ? prefix() : BASE, export function logoutLink() {
return rl.adminArea() ? rootAdmin() : ROOT;
}
/** /**
* @param {string} type * @param {string} type
* @param {string} hash * @param {string} hash
* @param {string=} customSpecSuffix * @param {string=} customSpecSuffix
* @returns {string} * @returns {string}
*/ */
serverRequestRaw = (type, hash) => export function serverRequestRaw(type, hash, customSpecSuffix) {
BASE + '?/Raw/' + SUB_QUERY_PREFIX + '/' return SERVER_PREFIX + '/Raw/' + SUB_QUERY_PREFIX + '/'
+ '0/' // Settings.get('accountHash') ? + (null == customSpecSuffix ? getHash() : customSpecSuffix) + '/'
+ (type + (type
? type + '/' + (hash ? SUB_QUERY_PREFIX + '/' + hash : '') ? type + '/' + (hash ? SUB_QUERY_PREFIX + '/' + hash : '')
: ''), : '')
;
}
/** /**
* @param {string} download * @param {string} download
* @returns {string} * @param {string=} customSpecSuffix
*/ * @returns {string}
attachmentDownload = (download) => */
serverRequestRaw('Download', download), export function attachmentDownload(download, customSpecSuffix) {
return serverRequestRaw('Download', download, customSpecSuffix);
}
proxy = url => /**
BASE + '?/ProxyExternal/' * @param {string} download
// + btoa(JSON.stringify([token,url]).replace(/ /g, '%20')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''), * @param {string=} customSpecSuffix
+ btoa(url.replace(/ /g, '%20')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''), * @returns {string}
// + b64EncodeJSONSafe(url.replace(/ /g, '%20')), */
export function attachmentPreview(download, customSpecSuffix) {
return serverRequestRaw('View', download, customSpecSuffix);
}
/** /**
* @param {string} type * @param {string} download
* @returns {string} * @param {string=} customSpecSuffix
*/ * @returns {string}
serverRequest = type => prefix() + '/' + type + '/' + SUB_QUERY_PREFIX + '/0/', */
export function attachmentThumbnailPreview(download, customSpecSuffix) {
return serverRequestRaw('ViewThumbnail', download, customSpecSuffix);
}
// 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 attachmentPreviewAsPlain(download, customSpecSuffix) {
return serverRequestRaw('ViewAsPlain', download, customSpecSuffix);
}
/** /**
* @param {string} lang * @param {string} download
* @param {boolean} isAdmin * @param {string=} customSpecSuffix
* @returns {string} * @returns {string}
*/ */
langLink = (lang, isAdmin) => export function attachmentFramed(download, customSpecSuffix) {
BASE + '?/Lang/0/' + (isAdmin ? 'Admin' : 'App') return serverRequestRaw('FramedView', download, customSpecSuffix);
+ '/' + encodeURI(lang) }
+ '/' + Settings.app('version') + '/',
/** /**
* @param {string} path * @param {string} type
* @returns {string} * @returns {string}
*/ */
staticLink = path => Settings.app('webVersionPath') + 'static/' + path, export function serverRequest(type) {
return SERVER_PREFIX + '/' + type + '/' + SUB_QUERY_PREFIX + '/' + getHash() + '/';
}
/** /**
* @param {string} theme * @returns {string}
* @returns {string} */
*/ export function upload() {
themePreviewLink = theme => { return serverRequest('Upload');
if (theme.endsWith('@nextcloud')) { }
theme = theme.slice(0, theme.length - 10).trim();
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' * @returns {string}
* @returns {string} */
*/ export function uploadContacts() {
mailbox = (inboxFolderName = 'INBOX') => HASH_PREFIX + 'mailbox/' + inboxFolderName, return serverRequest('UploadContacts');
}
/** /**
* @param {string=} screenName = '' * @returns {string}
* @returns {string} */
*/ export function uploadBackground() {
settings = (screenName = '') => HASH_PREFIX + 'settings' + (screenName ? '/' + screenName : ''), return serverRequest('UploadBackground');
}
/** /**
* @param {string=} screenName * @returns {string}
* @returns {string} */
*/ export function append() {
admin = screenName => HASH_PREFIX + ( return serverRequest('Append');
'AdminDomains' == screenName ? 'domains' }
: 'AdminSecurity' == screenName ? 'security'
: ''
),
/** /**
* @param {string} folder * @param {string} email
* @param {number=} page = 1 * @returns {string}
* @param {string=} search = '' */
* @param {number=} threadUid = 0 export function change(email) {
* @returns {string} return serverRequest('Change') + encodeURIComponent(email) + '/';
*/ }
mailBox = (folder, page, search, threadUid, messageUid) => {
let result = [HASH_PREFIX + 'mailbox'];
if (folder) { /**
result.push(folder + (threadUid ? '~' + threadUid : '')); * @param {string} requestHash
} * @returns {string}
*/
export function messageViewLink(requestHash) {
return serverRequestRaw('ViewAsPlain', requestHash);
}
if (messageUid) { /**
result.push('m' + messageUid); * @param {string} requestHash
} else { * @returns {string}
page = pInt(page, 1); */
if (1 < page) { export function messageDownloadLink(requestHash) {
result.push('p' + page); return serverRequestRaw('Download', requestHash);
} }
search && result.push(encodeURI(search));
}
return result.join('/'); /**
}, * @param {string} hash
* @returns {string}
*/
export function publicLink(hash) {
return SERVER_PREFIX + '/Raw/0/Public/' + hash + '/';
}
mailBoxMessage = (folder, messageUid) => mailBox(folder, 1, '', 0, pInt(messageUid)); /**
* @param {string} hash
* @returns {string}
*/
export function userBackground(hash) {
return serverRequestRaw('UserBackground', 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 serverRequestRaw('ContactsVcf');
}
/**
* @returns {string}
*/
export function exportContactsCsv() {
return serverRequestRaw('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');
}
/**
* @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 : '');
}
/**
* @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;
}

60
dev/Common/Momentor.js Normal file
View file

@ -0,0 +1,60 @@
import { doc } from 'Common/Globals';
import { i18n } from 'Common/Translator';
export function timestampToString(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) {
element.dateTime = (new Date(time * 1000)).format('Y-m-d\\TH:i:s');
let key = element.dataset.momentFormat;
if (key) {
element.textContent = timestampToString(time, key);
}
if ((key = element.dataset.momentFormatTitle)) {
element.title = timestampToString(time, key);
}
}
} catch (e) {
// prevent knockout crashes
console.error(e);
}
}
addEventListener('reload-time', () => setTimeout(() =>
doc.querySelectorAll('[data-bind*="moment:"]').forEach(element => timeToNode(element))
, 1)
);

View file

@ -1,6 +1,4 @@
import { settingsAddViewModel } from 'Screen/AbstractSettings'; 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 = [];
@ -12,7 +10,7 @@ const USER_VIEW_MODELS_HOOKS = [],
* @param {?number=} timeout * @param {?number=} timeout
*/ */
rl.pluginRemoteRequest = (callback, action, parameters, timeout) => { rl.pluginRemoteRequest = (callback, action, parameters, timeout) => {
rl.app.Remote.request('Plugin' + action, callback, parameters, timeout); rl.app && rl.app.remote().defaultRequest(callback, 'Plugin' + action, parameters, timeout);
}; };
/** /**
@ -39,9 +37,9 @@ rl.addSettingsViewModelForAdmin = (SettingsViewModelClass, template, labelName,
* @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 => (admin ? ADMIN_VIEW_MODELS_HOOKS : USER_VIEW_MODELS_HOOKS).forEach(view => {
settingsAddViewModel(...view) settingsAddViewModel(view[0], view[1], view[2], view[3]);
); });
} }
/** /**
@ -49,7 +47,8 @@ export function runSettingsViewModelHooks(admin) {
* @param {string} name * @param {string} name
* @returns {?} * @returns {?}
*/ */
rl.pluginSettingsGet = (pluginSection, name) => rl.pluginSettingsGet = (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;
};

View file

@ -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';
/*
oCallbacks:
ItemSelect
MiddleClick
canSelect
ItemGetUid
UpOrDown
*/
let shiftStart;
export class Selector { export class Selector {
list;
listChecked;
isListChecked;
focusedItem;
selectedItem;
selectedItemUseCallback = true;
iSelectNextHelper = 0;
iFocusedNextHelper = 0;
oContentScrollable;
sItemSelector;
sItemSelectedSelector;
sItemCheckedSelector;
sItemFocusedSelector;
sLastUid = '';
oCallbacks = {};
/** /**
* @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, list = this.list,
pageStep = 10,
list = this.list(),
listLen = list.length, listLen = list.length,
focused = this.focusedItem(); focused = this.focusedItem();
bShiftKey || (shiftStart = -1); if (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);
}
} }
} }

View file

@ -1,236 +1,176 @@
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 { doc } 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 export const trigger = ko.observable(false);
init = () => {
if (rl.I18N) { /**
I18N_DATA = rl.I18N; * @param {string} key
rl.I18N = null; * @param {Object=} valueList
doc.documentElement.dir = I18N_DATA.LANG_DIR; * @param {string=} defaulValue
return 1; * @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]);
}
} }
}, }
return result;
}
const i18nToNode = element => {
const key = element.dataset.i18n;
if (key) {
if ('[' === key.substr(0, 1)) {
switch (key.substr(0, 6)) {
case '[html]':
element.innerHTML = i18n(key.substr(6));
break;
case '[place':
element.placeholder = i18n(key.substr(13));
break;
case '[title':
element.title = i18n(key.substr(7));
break;
// no default
}
} else {
element.textContent = i18n(key);
}
}
},
i18nKey = key => key.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase(), i18nKey = key => key.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase(),
getNotificationMessage = code => { getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v);
let key = getKeyByValue(Notifications, code);
return key ? I18N_DATA.NOTIFICATIONS[key] : '';
},
fromNow = date => relativeTime(Math.round((date.getTime() - Date.now()) / 1000)); /**
* @param {Object} elements
* @param {boolean=} animate = false
*/
export function i18nToNodes(element) {
setTimeout(() =>
element.querySelectorAll('[data-i18n]').forEach(item => i18nToNode(item))
, 1);
}
export const /**
translateTrigger = ko.observable(false), * @param {Function} startCallback
* @param {Function=} langCallback = null
*/
export function initOnStartOrLangChange(startCallback, langCallback = null) {
startCallback && startCallback();
startCallback && trigger.subscribe(startCallback);
langCallback && trigger.subscribe(langCallback);
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat function getNotificationMessage(code) {
// see /snappymail/v/0.0.0/app/localization/relativetimeformat/ let key = getKeyByValue(Notification, code);
relativeTime = seconds => { if (key) {
let unit = 'second', key = i18nKey(key).replace('_NOTIFICATION', '_ERROR');
t = [[60,'minute'],[3600,'hour'],[86400,'day'],[2628000,'month'],[31536000,'year']], return I18N_DATA['NOTIFICATIONS/' + key];
i = 5, }
abs = Math.abs(seconds); }
while (i--) {
if (t[i][0] <= abs) { /**
seconds = Math.round(seconds / t[i][0]); * @param {number} code
unit = t[i][1]; * @param {*=} message = ''
break; * @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 getNotificationMessage(code)
|| getNotificationMessage(defCode)
|| '';
}
/**
* @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 = 'UNKNOWN';
code = parseInt(code, 10) || 0;
switch (code) {
case UploadErrorCode.FileIsTooBig:
case UploadErrorCode.FilePartiallyUploaded:
case UploadErrorCode.NoFileUploaded:
case UploadErrorCode.MissingTempFolder:
case UploadErrorCode.OnSavingFile:
case UploadErrorCode.FileType:
result = i18nKey(getKeyByValue(UploadErrorCode, code));
break;
}
return i18n('UPLOAD/ERROR_' + 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;
if (Intl.RelativeTimeFormat) { script.remove();
return (new Intl.RelativeTimeFormat(doc.documentElement.lang)).format(seconds, unit); resolve();
} };
// Safari < 14 script.onerror = () => reject(new Error('Language '+language+' failed'));
abs = Math.abs(seconds); script.src = langLink(language, admin);
let rtf = rl.relativeTime.long[unit][0 > seconds ? 'past' : 'future'], // script.async = true;
plural = rl.relativeTime.plural(abs); doc.head.append(script);
return (rtf[plural] || rtf).replace('{0}', abs); });
}, }
/** /**
* @param {string} key *
* @param {Object=} valueList * @param {string} language
* @param {string=} defaulValue * @param {boolean=} isEng = false
* @returns {string} * @returns {string}
*/ */
i18n = (key, valueList, defaulValue) => { export function convertLangName(language, isEng = false) {
let result = defaulValue ?? key; return i18n(
let path = key.split('/'); 'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/LANG_' + language.toUpperCase().replace(/[^a-zA-Z0-9]+/g, '_'),
if (I18N_DATA[path[0]] && path[1]) { null,
result = I18N_DATA[path[0]][path[1]] || result; language
} );
valueList && forEachObjectEntry(valueList, (key, value) => { }
result = result.replace('%' + key + '%', value);
});
return result;
},
/**
* @param {Object} elements
* @param {boolean=} animate = false
*/
i18nToNodes = element =>
setTimeout(() =>
element.querySelectorAll('[data-i18n]').forEach(element => {
const key = element.dataset.i18n;
if ('[' === key[0]) {
switch (key.slice(1, 6)) {
case 'html]':
element.innerHTML = i18n(key.slice(6));
break;
case 'place':
element.placeholder = i18n(key.slice(13));
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);
}
}
return '';
},
timeToNode = (element, time) => {
try {
if (time) {
element.dateTime = new Date(time * 1000).toISOString();
} else {
time = Date.parse(element.dateTime) / 1000;
}
let key = element.dataset.timeFormat;
if (key) {
element.textContent = timestampToString(time, key);
if ('FULL' !== key && 'FROMNOW' !== key) {
element.title = timestampToString(time, 'FULL');
}
}
} 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();

View file

@ -1,31 +1,138 @@
import { SaveSettingsStep } from 'Common/Enums';
import { doc } from 'Common/Globals';
export const export const
isArray = Array.isArray, isArray = Array.isArray,
arrayLength = array => isArray(array) && array.length, isNonEmptyArray = Array.isNotEmpty;
isFunction = v => typeof v === 'function',
pString = value => null != value ? '' + value : '',
forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn), /**
* @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;
}
forEachObjectEntry = (obj, fn) => Object.entries(obj).forEach(([key, value]) => fn(key, value)), /**
* @param {*} value
* @returns {string}
*/
export function pString(value) {
return null != value ? '' + value : '';
}
pInt = (value, defaultValue = 0) => { /**
value = parseInt(value, 10); * @returns {boolean}
return isFinite(value) ? value : defaultValue; */
}, export function inFocus() {
try {
return doc.activeElement && doc.activeElement.matches(
'input,textarea,iframe,.cke_editable'
);
} catch (e) {
return false;
}
}
defaultOptionsAfterRender = (domItem, item) => /**
item && undefined !== item.disabled && domItem?.classList.toggle('disabled', domItem.disabled = item.disabled), * @param {string} theme
* @returns {string}
*/
export const convertThemeName = theme => {
if ('@custom' === theme.substr(-7)) {
theme = theme.substr(0, theme.length - 7).trim();
}
// unescape(encodeURIComponent()) makes the UTF-16 DOMString to an UTF-8 string return theme
b64Encode = data => btoa(unescape(encodeURIComponent(data))), .replace(/([A-Z])/g, ' $1')
/* // Without deprecated 'unescape': .replace(/[^a-zA-Z0-9]+/g, ' ')
b64Encode = data => btoa(encodeURIComponent(data).replace( .trim();
/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode('0x' + p1) };
)),
*/
b64EncodeJSON = data => b64Encode(JSON.stringify(data)), /**
* @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);
}
}
b64EncodeJSONSafe = data => b64EncodeJSON(data).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''), /**
* @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);
};
}
getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v); let __themeTimer = 0,
__themeJson = 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);
__themeJson = 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 (__themeJson) {
__themeJson.abort();
}
let init = {};
if (window.AbortController) {
__themeJson = new AbortController();
init.signal = __themeJson.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);
}
}

View file

@ -1,138 +1,379 @@
import { Notifications } from 'Common/Enums'; import { ComposeType, FolderType } from 'Common/EnumsUser';
import { MessageSetAction, ComposeType/*, FolderType*/ } from 'Common/EnumsUser'; import { EmailModel } from 'Model/Email';
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';
import { IdentityModel } from 'Model/Identity';
import { AccountUserStore } from 'Stores/User/Account';
import { IdentityUserStore } from 'Stores/User/Identity';
import { isArray } from 'Common/Utils';
import { showScreenPopup } from 'Knoin/Knoin'; import { showScreenPopup } from 'Knoin/Knoin';
import { IdentityPopupView } from 'View/Popup/Identity'; import { encodeHtml } from 'Common/Html';
import { isArray } from 'Common/Utils';
export const import { doc } from 'Common/Globals';
// 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 = { * @param {number=} len = 100
target: 'zip', * @returns {string}
filename: name, */
hashes: hashes function splitPlainText(text, len = 100) {
}; let prefix = '',
if (!onError) { subText = '',
onError = () => alert('Download failed'); 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
tpl = doc.createElement('template'),
convertPre = (...args) =>
args && 1 < args.length
? args[1]
.toString()
.replace(/[\n]/gm, '<br />')
.replace(/[\r]/gm, '')
: '',
fixAttibuteValue = (...args) => (args && 1 < args.length ? '' + args[1] + encodeHtml(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(/&nbsp;/gi, ' ')
.replace(/&quot;/gi, '"')
.replace(/<[^>]*>/gm, '');
text = splitPlainText(tpl.content.textContent
.replace(/\n[ \t]+/gm, '\n')
.replace(/[\n]{3,}/gm, '\n\n')
.replace(/&gt;/gi, '>')
.replace(/&lt;/gi, '<')
.replace(/&amp;/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, '&amp;')
.replace(/>/g, '&gt;')
.replace(/</g, '&lt;')
.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 +408,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 +429,81 @@ 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(/^[^?]*\?/, '');
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));
}
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));

12
dev/Component/Abstract.js Normal file
View file

@ -0,0 +1,12 @@
export class AbstractComponent {
disposable = [];
dispose() {
this.disposable.forEach((funcToDispose) => {
if (funcToDispose && funcToDispose.dispose) {
funcToDispose.dispose();
}
});
}
}

View 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 };

View 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 };

View file

@ -1,17 +1,3 @@
export class CheckboxComponent { import { AbstractCheckbox } from 'Component/AbstractCheckbox';
constructor(params = {}) {
this.name = params.name;
this.value = ko.isObservable(params.value) ? params.value export class CheckboxComponent extends AbstractCheckbox {}
: ko.observable(!!params.value);
this.enable = ko.isObservable(params.enable) ? params.enable
: ko.observable(params.enable ?? 1);
this.label = params.label;
}
click() {
this.enable() && this.value(!this.value());
}
}

View file

@ -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);
}
}
}

3
dev/Component/Input.js Normal file
View file

@ -0,0 +1,3 @@
import { AbstractInput } from 'Component/AbstractInput';
export class InputComponent extends AbstractInput {}

View file

@ -0,0 +1,41 @@
import ko from 'ko';
import { AbstractCheckbox } from 'Component/AbstractCheckbox';
export 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);
}
}
}

View file

@ -0,0 +1,38 @@
import { SaveSettingsStep } from 'Common/Enums';
import { AbstractComponent } from 'Component/Abstract';
export 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;
}
}

View file

@ -1,56 +1,24 @@
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 { AbstractInput } from 'Component/AbstractInput';
export class SelectComponent { export 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);
}
} }

13
dev/Component/TextArea.js Normal file
View file

@ -0,0 +1,13 @@
import { AbstractInput } from 'Component/AbstractInput';
export class TextAreaComponent extends AbstractInput {
/**
* @param {Object} params
*/
constructor(params) {
super(params);
this.rows = params.rows || 5;
this.spellcheck = !!params.spellcheck;
}
}

View file

@ -1,227 +0,0 @@
/**
* https://datatracker.ietf.org/doc/html/rfc7095
*
* Inspired by https://github.com/mcpar-land/vcfer
*/
import { VCardProperty } from './VCardProperty'
export class JCard {
constructor(input)
{
this.props = new Map()
this.version = '4.0'
if (input) {
// read from jCard
if (typeof input !== 'object') {
throw Error('error reading vcard')
}
this.parseFromJCard(input)
}
}
parseFromJCard(json)
{
json = JSON.parse(JSON.stringify(json));
if (!/vcard/i.test(json[0])) {
throw new SyntaxError('Incorrect jCard format');
}
json[1].forEach(jprop => this.add(new VCardProperty(jprop)));
}
/**
* Retrieve an array of {@link VCardProperty} objects under the specified field.
* Returns [] if there are no VCardProperty objects found.
* Properites are always stored in an array.
* @param field to get.
* @param type If provided, only return {@link VCardProperty}s with the specified
* type as a param.
*/
get(field, type)
{
if (type) {
let props = this.props.get(field);
return props
? props.filter(prop => {
let types = prop.type;
return (Array.isArray(types) ? types : [types]).includes(type);
})
: [];
}
return this.props.get(field) || [];
// TODO with type filter-er
}
/**
* Retrieve a _single_ VCardProperty of the specified field. Attempts to pick based
* on the following priorities, in order:
* - `TYPE={type}` of the value specified in the `type` argument. Ignored
* if the argument isn't supplied.
* - `TYPE=pref` is present.
* - is the VCardProperty at index 0 from get(field)
* @param field
* @param type
*/
getOne(field, type)
{
return this.get(field, type || 'pref')[0] || this.get(field)[0];
}
/**
* Set the contents of a field to contain a single {@link VCardProperty}.
*
* Accepts either 2-4 arguments to construct a VCardProperty,
* or 1 argument of a preexisting VCardProperty object.
*
* This will always overwrite all existing properties of the given
* field. For just adding a new VCardProperty, see {@link VCard#add}
* @param arg the field, or a VCardProperty object
* @param value the value for the VCardProperty object
* @param params the parameters for the VCardProperty object
* @param type the type for the VCardProperty object
*/
set(arg, value, params, type)
{
if (typeof arg === 'string') {
arg = new VCardProperty(String(arg), value, params, type);
}
if (!(arg instanceof VCardProperty)) {
throw Error('invalid argument of VCard.set(), expects string arguments or a VCardProperty');
}
let field = arg.getField();
this.props.set(field, [arg]);
return arg;
}
add(arg, value, params, type)
{
// string arguments
if (typeof arg === 'string') {
arg = new VCardProperty(String(arg), value, params, type);
}
if (!(arg instanceof VCardProperty)) {
throw Error('invalid argument of VCard.add(), expects string arguments or a VCardProperty');
}
// VCardProperty arguments
let field = arg.getField();
if (this.props.get(field)) this.props.get(field)?.push(arg)
else this.props.set(field, [arg])
return arg;
}
/**
* Removes a {@link VCardProperty}, or all properties of the supplied field.
* @param arg the field, or a {@link VCardProperty} object
* @param paramFilter (incomplete)
*/
remove(arg) {
// string arguments
if (typeof arg === 'string') {
// TODO filter by param
this.props.delete(arg);
}
// VCardProperty argument
else if (arg instanceof VCardProperty) {
let propArray = this.props.get(arg.getField());
if (!propArray?.includes(arg))
throw Error("Attempted to remove VCardProperty VCard does not have: ".concat(arg));
propArray.splice(propArray.indexOf(arg), 1);
if (propArray.length === 0)
this.props.delete(arg.getField());
}
// incorrect arguments
else
throw Error('invalid argument of VCard.remove(), expects ' +
'string and optional param filter or a VCardProperty');
}
/**
* Returns true if the vCard has at least one @{link VCardProperty}
* of the given field.
* @param field The field to query
*/
has(field)
{
return (!!this.props.get(field) && this.props.get(field).length > 0);
}
/**
* Returns stringified JSON
*/
toString()
{
return JSON.stringify(this);
}
/**
* Returns a {@link JCard} object as a JSON array.
*/
toJSON()
{
let data = [['version', {}, 'text', '4.0']];
/*
this.props.forEach((props, field) =>
(field === 'version') || props.forEach(prop => prop.isEmpty() || data.push(prop.toJSON()))
);
*/
for (const [field, props] of this.props.entries()) {
if ('version' !== field) {
for (const prop of props) {
prop.isEmpty() || data.push(prop.toJSON());
}
}
}
return ['vcard', data];
}
/**
* Automatically generate the 'fn' VCardProperty from the preferred 'n' VCardProperty.
*
* #### `set` (`boolean`)
*
* - `false`: (default) return the generated full name string without
* modifying the VCard.
*
* - `true`: modify the VCard's `fn` VCardProperty directly, as specified
* by `append`
*
* #### `append` (`boolean`)
*
* (ignored if `set` is `false`)
*
* - `false`: (default) replace the existing 'fn' VCardProperty/properties with
* a new one.
*
* - `true`: append a new `fn` VCardProperty to the array.
*
* see: [RFC 6350 section 6.2.1](https://tools.ietf.org/html/rfc6350#section-6.2.1)
*/
parseFullName(options) {
let n = this.getOne('n');
if (n === undefined) {
throw Error('\'fn\' VCardProperty not present in card, cannot parse full name');
}
let fnString = '';
// Position in n -> position in fn
[3, 1, 2, 0, 4].forEach(pos => {
let splitStr = n.value[pos];
if (splitStr) {
// comma separated values separated by spaces
fnString += ' ' + splitStr.replace(',', ' ');
}
});
fnString = fnString.trim();
let fn = new VCardProperty('fn', fnString);
if (options?.set) {
if (options.append) {
this.add(fn);
} else {
this.set(fn);
}
}
return fn;
}
}

View file

@ -1,146 +0,0 @@
/**
* Inspired by https://github.com/mcpar-land/vcfer
*/
export class VCardProperty {
/**
* A class describing a single vCard property.
* Will almost always be a member of a
* {@link VCard}'s [props]{@link VCard.props} map.
*
* Accepts either 2-4 arguments, or 1 argument in jCard property format.
* @param arg the field, or a jCard property
* @param value
* @param params
* @param type
*/
constructor(arg, value, params, type = 'text')
{
this.field = '';
/**
* the value of the property.
* @example '(123) 456 7890'
*/
this.value = '';
/**
* the type of the property value.
* @example 'text'
*/
this.type = '';
/**
* https://www.rfc-editor.org/rfc/rfc6350.html#section-5
* An jCard parameters object.
* @example
* {
* type: ['work', 'voice', 'pref'],
* value: 'uri'
* }
*/
this.params = {};
// Construct from arguments
if (value !== undefined && typeof arg === 'string') {
this.field = arg;
this.value = value;
this.params = params || {};
this.type = type;
}
// construct from jcard
else if (value === undefined && params === undefined && typeof arg === 'object') {
this.parseFromJCardProperty(arg);
}
// invalid property
else {
throw Error('invalid Property constructor');
}
}
parseFromJCardProperty(jCardProp)
{
jCardProp = JSON.parse(JSON.stringify(jCardProp));
this.field = jCardProp[0].toLowerCase();
this.params = jCardProp[1];
this.type = jCardProp[2];
this.value = jCardProp[3];
}
addParam(key, value)
{
if (Array.isArray(this.params[key])) {
this.params[key].push(value);
}
else if (this.params[key] != null) {
this.params[key] = [this.params[key], value];
}
else {
this.params[key] = value;
}
}
/** Returns a copy of the Property's string value. */
getValue()
{
return '' + this.value;
}
/**
* https://www.rfc-editor.org/rfc/rfc6350.html#section-5.3
*/
pref()
{
return this.params.pref || 100;
}
/**
* https://www.rfc-editor.org/rfc/rfc6350.html#section-5.6
*/
tags()
{
return this.params.type || [];
}
/** Returns `true` if all the following are true:
* - the property's value contains charactes other than `;`
* - the property has no parameters
*/
isEmpty()
{
return ((null == this.value || !/[^;]+/.test(this.value)) && !Object.keys(this.params).length);
}
notEmpty()
{
return !this.isEmpty();
}
/** Returns a readonly string copy of the property's field. */
getField()
{
return '' + this.field;
}
/**
* Returns stringified JSON
*/
toString()
{
return JSON.stringify(this);
}
/**
* Returns a JSON array in a [jCard property]{@link JCardProperty} format
*/
toJSON()
{
return [
this.field,
this.params,
this.type || 'text',
this.value
];
}
}

66
dev/External/Admin/ko.js vendored Normal file
View file

@ -0,0 +1,66 @@
import 'External/ko';
import ko from 'ko';
import { SaveSettingsStep } from 'Common/Enums';
import { pInt } from 'Common/Utils';
ko.bindingHandlers.saveTrigger = {
init: (element) => {
element.saveTriggerType = element.matches('input[type=text],input[type=email],input[type=password],select,textarea')
? 'input' : 'custom';
if ('custom' === element.saveTriggerType) {
element.append(
'  ',
Element.fromHTML('<i class="icon-spinner animated"></i>'),
Element.fromHTML('<i class="icon-remove error"></i>'),
Element.fromHTML('<i class="icon-ok success"></i>')
);
element.classList.add('settings-saved-trigger');
} else {
element.classList.add('settings-saved-trigger-input');
}
},
update: (element, fValueAccessor) => {
const value = parseInt(ko.unwrap(fValueAccessor()),10);
if ('custom' === element.saveTriggerType) {
element.querySelector('.animated').hidden = value !== SaveSettingsStep.Animate;
element.querySelector('.success').hidden = value !== SaveSettingsStep.TrueResult;
element.querySelector('.error').hidden = value !== SaveSettingsStep.FalseResult;
} else if (value !== SaveSettingsStep.Animate) {
element.classList.toggle('success', value === SaveSettingsStep.TrueResult);
element.classList.toggle('error', value === SaveSettingsStep.FalseResult);
}
}
};
ko.extenders.idleTrigger = (target) => {
target.trigger = ko.observable(SaveSettingsStep.Idle);
return target;
};
ko.extenders.posInterer = (target, defaultVal) => {
const result = ko.computed({
read: target,
write: newValue => {
let val = pInt(newValue.toString(), defaultVal);
if (0 >= val) {
val = defaultVal;
}
if (val === target() && '' + val !== '' + newValue) {
target(val + 1);
}
target(val);
}
});
result(target());
return result;
};
// functions
ko.observable.fn.idleTrigger = function() {
return this.extend({ 'idleTrigger': true });
};

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