Compare commits

..

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

2529 changed files with 129025 additions and 251053 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,11 +27,34 @@ stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0 stderr_logfile_maxbytes=0
; reads snappymail logs
[program:snappymail-auth]
command=tail -f /snappymail/data/_data_/_default_/logs/auth.log
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:snappymail-errors]
command=tail -f /snappymail/data/_data_/_default_/logs/errors.log
# everything is an error
stdout_logfile=/dev/stderr
stdout_logfile_maxbytes=0
redirect_stderr=true
[program:logrotate]
command=/logrotate-loop.sh
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[eventlistener:subprocess-stopped] [eventlistener:subprocess-stopped]
command=php /listener.php command=php /listener.php
process_name=%(program_name)s_%(process_num)02d process_name=%(program_name)s_%(process_num)02d
user=root user=php-cli
numprocs=1 numprocs=1
events=PROCESS_STATE_EXITED,PROCESS_STATE_STOPPED,PROCESS_STATE_FATAL events=PROCESS_STATE_EXITED,PROCESS_STATE_STOPPED,PROCESS_STATE_FATAL
autostart=true autostart=true
autorestart=unexpected autorestart=unexpected

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,296 @@
; SnappyMail configuration file
; Please don't add custom parameters here, those will be overwritten
[webmail]
; Text displayed as page title
title = "SnappyMail Webmail"
; Text displayed on startup
loading_description = "SnappyMail"
favicon_url = ""
; 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 = 2
[interface]
show_attachment_thumbnail = On
new_move_to_folder_button = On
[contacts]
; Enable contacts
enable = Off
allow_sync = Off
sync_interval = 20
type = "sqlite"
pdo_dsn = "host=127.0.0.1;port=3306;dbname=snappymail"
pdo_user = "root"
pdo_password = ""
suggestions_limit = 30
[security]
; Enable CSRF protection (http://en.wikipedia.org/wiki/Cross-site_request_forgery)
csrf_protection = On
custom_server_signature = "SnappyMail"
x_frame_options_header = "DENY"
x_xss_protection_header = "1; mode=block"
openpgp = Off
; Login and password for web admin panel
admin_login = "admin"
admin_password = ""
admin_totp = ""
; Access settings
allow_admin_panel = On
hide_x_mailer_header = On
admin_panel_host = ""
admin_panel_key = "admin"
content_security_policy = ""
[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]
composer = On
contacts = On
settings = On
quota = On
help = On
reload = On
search = On
search_adv = 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
hide_submit_button = On
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_zone = "UTC"
session_filter = ""
; Log filename.
; For security reasons, some characters are removed from filename.
; Allows for pattern-based folder creation (see examples below).
;
; Patterns:
; {date:Y-m-d} - Replaced by pattern-based date
; Detailed info: http://www.php.net/manual/en/function.date.php
; {user:email} - Replaced by user's email address
; If user is not logged in, value is set to "unknown"
; {user:login} - Replaced by user's login (the user part of an email)
; If user is not logged in, value is set to "unknown"
; {user:domain} - Replaced by user's domain name (the domain part of an email)
; If user is not logged in, value is set to "unknown"
; {user:uid} - Replaced by user's UID regardless of account currently used
;
; {user:ip}
; {request:ip} - Replaced by user's IP address
;
; Others:
; {imap:login} {imap:host} {imap:port}
; {smtp:login} {smtp:host} {smtp:port}
;
; Examples:
; filename = "log-{date:Y-m-d}.txt"
; filename = "{date:Y-m-d}/{user:domain}/{user:email}_{user:uid}.log"
; filename = "{user:email}-{date:Y-m-d}.txt"
; filename = "syslog"
filename = "log-{date:Y-m-d}.txt"
; Enable auth logging in a separate file (for fail2ban)
auth_logging = Off
auth_logging_filename = "fail2ban/auth-{date:Y-m-d}.txt"
auth_logging_format = "[{date:Y-m-d H:i:s}] Auth failed: ip={request:ip} user={imap:login} host={imap:host} port={imap:port}"
[debug]
; Special option required for development purposes
enable = Off
[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]
update_channel = "stable"
allow_prefetch = On
allow_smart_html_links = On
cache_system_data = On
date_from_headers = On
autocreate_system_folders = On
allow_message_append = Off
login_fault_delay = 1
log_ajax_response_write_limit = 300
allow_html_editor_biti_buttons = Off
allow_ctrl_enter_on_compose = On
try_to_detect_hidden_images = 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_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
curl_proxy = ""
curl_proxy_auth = ""
in_iframe = Off
force_https = Off
http_client_ip_check_proxy = Off
fast_cache_memcache_host = "127.0.0.1"
fast_cache_memcache_port = 11211
fast_cache_redis_host = "127.0.0.1"
fast_cache_redis_port = 6379
use_local_proxy_for_external_images = On
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
boundary_prefix = ""
dev_email = ""
dev_password = ""
[version]
current = "2.7.2"
saved = "Mon, 23 Aug 2021 07:55:13 +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

@ -3,13 +3,13 @@ module.exports = {
// extends: ['eslint:recommended', 'plugin:prettier/recommended'], // extends: ['eslint:recommended', 'plugin:prettier/recommended'],
extends: ['eslint:recommended'], extends: ['eslint:recommended'],
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
@ -27,25 +27,18 @@ module.exports = {
'SquireUI': "readonly", 'SquireUI': "readonly",
// node_modules/knockout but dev/External/ko.js is used // node_modules/knockout but dev/External/ko.js is used
'ko': "readonly", 'ko': "readonly",
// dev/External/ifvisible.js
'ifvisible': "readonly",
// vendors/routes/ // vendors/routes/
'hasher': "readonly", 'hasher': "readonly",
'Crossroads': "readonly", 'Crossroads': "readonly",
// vendors/jua // vendors/jua
'Jua': "readonly", 'Jua': "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
'no-mixed-spaces-and-tabs': 'off', 'no-mixed-spaces-and-tabs': 'off',
'max-len': [ 'max-len': [
@ -58,7 +51,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

@ -23,16 +23,13 @@ A clear and concise description of what you expected to happen.
**Screenshots** **Screenshots**
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**Please complete the following information:** **Desktop (please complete the following information):**
- Browser: [e.g. chromium 100, firefox 100, safari 14, mobile] - Browser [e.g. chromium 89, firefox 86, safari 14]
- IMAP daemon: [e.g. courier, dovecot] - SnappyMail Version [e.g. 2.3.4]
- PHP version:
- SnappyMail Version:
- Mode: [e.g. standalone, nextcloud, cyberpanel, docker]
**Debug/logging information** **Mobile (please complete the following information):**
[Read here how to log](https://github.com/the-djmaze/snappymail/wiki/FAQ#how-do-i-enable-logging) - Browser [e.g. stock browser, chrome 87, firefox 86, safari 14]
- [ ] I've placed them here (few lines) or as attachments (many lines) - SnappyMail Version [e.g. 2.3.4]
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem 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,15 +1,7 @@
<FilesMatch "index\.php">
AcceptPathInfo On
</FilesMatch>
<IfModule mod_rewrite.c> <IfModule mod_rewrite.c>
RewriteEngine On RewriteEngine On
# Redirect cPanel # Redirect cPanel
RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301] RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.+)$ index.php/$1 [L,QSA]
</IfModule> </IfModule>
<IfModule mod_expires.c> <IfModule mod_expires.c>
@ -31,9 +23,10 @@
# 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-Frame-Options "DENY"
# Header set X-XSS-Protection "1; mode=block" # Header set X-XSS-Protection "1; mode=block"
Header set Service-Worker-Allowed "/" Header set Service-Worker-Allowed "/"

File diff suppressed because it is too large Load diff

View file

@ -29,7 +29,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** **Release**

View file

@ -658,4 +658,4 @@ specific requirements.
You should also get your employer (if you work as a programmer) or school, You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>. <http://www.gnu.org/licenses/>.

View file

@ -61,6 +61,8 @@ rl-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

124
README.md
View file

@ -5,10 +5,6 @@
<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,38 +34,35 @@ 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 PharData and ZipArchive
* Dark mode * PHP yaml extension else use the old Spyc
* Added option to remove background/font colors from messages for real "dark mode" * 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 (re-implemented as plugin)
* 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
@ -80,24 +73,20 @@ This fork of RainLoop has the following changes:
* Added support to load plugins as .phar * Added support to load plugins as .phar
* Replaced old Sabre library * Replaced old Sabre library
* AddressBook Contacts support MySQL/MariaDB utf8mb4 * AddressBook Contacts support MySQL/MariaDB utf8mb4
* Prevent Google FLoC
* Added [Fetch Metadata Request Headers](https://www.w3.org/TR/fetch-metadata/) checks * Added [Fetch Metadata Request Headers](https://www.w3.org/TR/fetch-metadata/) checks
* Reduced excessive DOM size * Reduced excessive DOM size
* Support [Kolab groupware](https://kolab.org/)
* Support many more [IMAP RFC's](https://snappymail.eu/comparison#IMAP)
* Support Sodium and OpenSSL for encryption
* Much better PGP support
### Supported browsers ### Supported browsers
This fork uses downsized/simplified versions of scripts and has no support for Internet Explorer nor Edge Legacy. This fork uses downsized/simplified versions of scripts and has no support for Internet Explorer nor Edge Legacy.
Supported are: Supported are:
* Chrome 80+ * Chrome 69+
* Edge 80+ * Edge 79+
* Firefox 78+ * Firefox 69+
* Opera 67+ * Opera 56+
* Safari 13.1+ * Safari 12+
### Removal of old JavaScript ### Removal of old JavaScript
@ -105,9 +94,12 @@ Supported are:
The result is faster and smaller download code (good for mobile networks). The result is faster and smaller download code (good for mobile networks).
* Added dev/prototype.js for some additional features * 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
@ -117,8 +109,6 @@ The result is faster and smaller download code (good for mobile networks).
* Replaced simplestatemanager with CSS @media * Replaced simplestatemanager with CSS @media
* Replaced inputosaurus with own code * Replaced inputosaurus with own code
* 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,35 +123,36 @@ 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 | 86.143 |
|app.js |4.207.787 | 447.263 | |app.js |4.215.733 | 435.037 |
|boot.js | 868.735 | 4.343 | |boot.js | 672.433 | 2.707 |
|libs.js | 658.812 | 233.728 | |libs.js | 647.679 | 213.208 |
|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 | 737.380 |
|js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli | |js/min/* |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|--------------- |--------: |--------: |------: |------: |--------: |--------: | |--------------- |--------: |--------: |------: |------: |--------: |--------: |
|admin.min.js | 256.831 | 41.719 | 73.606 | 14.022 | 60.877 | 12.567 | |admin.min.js | 255.514 | 43.930 | 73.899 | 13.591 | 60.674 | 12.162 |
|app.min.js | 515.367 | 202.101 |139.456 | 68.505 |110.485 | 58.481 | |app.min.js | 516.000 | 222.420 |140.430 | 66.268 |110.657 | 56.207 |
|boot.min.js | 84.659 | 2.231 | 26.998 | 1.271 | 23.643 | 1.067 | |boot.min.js | 66.456 | 1.621 | 22.553 | 983 | 20.043 | 805 |
|libs.min.js | 584.772 | 110.646 |180.901 | 39.518 |155.182 | 35.207 | |libs.min.js | 574.626 | 102.959 |177.280 | 37.514 |151.855 | 33.617 |
|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 | 370.930 |425.477 |118.356 |353.301 |102.791 |
|TOTAL user |1.217.635 | 314.978 |358.761 |109.294 |299.485 | 94.755 | |TOTAL (no admin) |1.189.690 | 327.000 |351.061 |104.765 |292.627 | 90.629 |
|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 68% smaller and faster than traditional RainLoop.
|OpenPGP |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|--------------- |--------: |--------: |------: |------: |--------: |--------: |
|openpgp.min.js | 330.742 | 293.972 |102.388 | 93.030 | 84.241 | 77.142 |
|openpgp.worker | 1.499 | 1.125 | 824 | 567 | 695 | 467 |
### CSS changes ### CSS changes
@ -169,7 +160,6 @@ For a user it is around 66% smaller and faster than traditional RainLoop.
* Themes work in mobile mode * Themes work in mobile mode
* Bugfix invalid/conflicting css rules * Bugfix invalid/conflicting css rules
* Use flexbox * Use flexbox
* Use border-box
* Split app.css to have separate admin.css * Split app.css to have separate admin.css
* Remove oldschool 'float' * Remove oldschool 'float'
* Remove unused css * Remove unused css
@ -179,7 +169,13 @@ For a user it is around 66% smaller and faster than traditional RainLoop.
* 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
@ -188,27 +184,12 @@ For a user it is around 66% smaller and faster than traditional RainLoop.
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli | |css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|------------ |-------: |------: |------: |------: |--------: | |------------ |-------: |------: |------: |------: |--------: |
|app.css | 340.331 | 85.073 | 46.946 | 17.792 | 15.210 | |app.css | 340.334 | 92.117 | 46.959 | 17.079 | 14.790 |
|app.min.css | 274.947 | 68.272 | 39.647 | 15.615 | 13.636 | |app.min.css | 274.791 | 74.827 | 39.618 | 15.259 | 13.473 |
|boot.css | | 1.326 | | 664 | 545 | |boot.css | | 1.326 | | 664 | 545 |
|boot.min.css | | 1.071 | | 590 | 474 | |boot.min.css | | 1.071 | | 590 | 474 |
|admin.css | | 30.880 | | 7.045 | 6.127 | |admin.css | | 39.492 | | 8.354 | 7.308 |
|admin.min.css | | 24.959 | | 6.368 | 5.615 | |admin.min.css | | 31.114 | | 7.324 | 6.506 |
### 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,6 +198,7 @@ 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 |
|-------- |-------: |-------: |------: |--------: | |-------- |-------: |-------: |------: |--------: |
@ -224,3 +206,5 @@ Still TODO:
|ckeditor | ? | 520.035 | ? | 155.916 | |ckeditor | ? | 520.035 | ? | 155.916 |
CKEditor including the 7 asset requests (css,language,plugins,icons) is 633.46 KB / 180.47 KB (gzip). CKEditor including the 7 asset requests (css,language,plugins,icons) is 633.46 KB / 180.47 KB (gzip).
To use the old CKEditor, you must install the plugin.

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,32 @@
//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.
* Uncomment to disable APCU.
*/
//define('APP_USE_APCU_CACHE', false); //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);

50
assets/.htaccess Normal file
View file

@ -0,0 +1,50 @@
<IfModule mod_headers.c>
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTP:Accept-encoding} br
RewriteCond "%{REQUEST_FILENAME}\.br" -s
RewriteRule "^(.+\.js)$" "$1\.br" [L,T=application/javascript,QSA]
RewriteCond %{HTTP:Accept-encoding} gzip
RewriteCond "%{REQUEST_FILENAME}\.gz" -s
RewriteRule "^(.+\.js)$" "$1\.gz" [L,T=application/javascript,QSA]
RewriteCond %{HTTP:Accept-encoding} br
RewriteCond "%{REQUEST_FILENAME}\.br" -s
RewriteRule "^(.+\.css)$" "$1\.br" [L,T=text/css,QSA]
RewriteCond %{HTTP:Accept-encoding} gzip
RewriteCond "%{REQUEST_FILENAME}\.gz" -s
RewriteRule "^(.+\.css)$" "$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>
<FilesMatch "(\.js\.br|\.js\.gz)$">
Header set Content-Type "application/javascript; charset=utf-8"
ForceType application/javascript
</FilesMatch>
<FilesMatch "(\.css\.br|\.css\.gz)$">
Header set Content-Type "text/css; charset=utf-8"
ForceType text/css
</FilesMatch>
</IfModule>
</IfModule>

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: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

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

View file

@ -3,8 +3,6 @@
"short_name": "SnappyMail", "short_name": "SnappyMail",
"description": "Simple, modern & fast web-based email client", "description": "Simple, modern & fast web-based email client",
"display": "standalone", "display": "standalone",
"background_color": "white",
"theme_color": "black",
"icons": [{ "icons": [{
"src": "apple-touch-icon.png", "src": "apple-touch-icon.png",
"sizes": "64x64", "sizes": "64x64",
@ -13,11 +11,6 @@
"src": "android-icon.png", "src": "android-icon.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png" "type": "image/png"
}, {
"src": "logo-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}], }],
"start_url": "../../../../" "start_url": "../../../../"
} }

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,20 @@
import ko from 'ko'; import ko from 'ko';
import {
elementById,
Settings
} from 'Common/Globals';
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 { 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 {
@ -19,38 +25,57 @@ export class AbstractApp {
this.Remote = Remote; this.Remote = Remote;
} }
logoutReload(url) { logoutReload(close = false) {
arePopupsVisible(false); const url = logoutLink();
url = url || logoutLink();
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 register = (key, ClassObject, templateID) => ko.components.register(key, {
template: { element: ClassObject.name }, template: { element: templateID || (key + 'Component') },
viewModel: { viewModel: {
createViewModel: (params, componentInfo) => { createViewModel: (params, componentInfo) => {
params = params || {}; params = params || {};
i18nToNodes(componentInfo.element);
if (componentInfo && componentInfo.element) {
i18nToNodes(componentInfo.element);
if (params.inline) {
componentInfo.element.style.display = 'inline-block';
}
}
return new ClassObject(params); return new ClassObject(params);
} }
} }
}); });
register('Input', InputComponent);
register('Select', SelectComponent); register('Select', SelectComponent);
register('Checkbox', CheckboxComponent); register('TextArea', TextAreaComponent);
register('Checkbox', CheckboxMaterialDesignComponent, 'CheckboxMaterialDesignComponent');
register('CheckboxSimple', CheckboxComponent, 'CheckboxComponent');
initOnStartOrLangChange(); initOnStartOrLangChange();
LanguageStore.populate(); LanguageStore.populate();
initThemes(); ThemeStore.populate();
this.start();
} }
/**
* @returns {void}
*/
hideLoading() {
elementById('rl-content').hidden = false;
elementById('rl-loading').remove();
}
} }

View file

@ -1,7 +1,6 @@
import 'External/ko'; import 'External/Admin/ko';
import { SettingsGet, SettingsAdmin } from 'Common/Globals'; import { Settings, SettingsGet } from 'Common/Globals';
import { initThemes } from 'Stores/Theme';
import Remote from 'Remote/Admin/Fetch'; import Remote from 'Remote/Admin/Fetch';
@ -11,42 +10,25 @@ 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); this.weakPassword = ko.observable(false);
} }
refresh() { bootstart() {
initThemes(); super.bootstart();
this.start(); if (!Settings.app('allowAdminPanel')) {
}
start() {
// if (!Settings.app('adminAllowed')) {
if (!SettingsAdmin('allowed')) {
rl.route.root(); rl.route.root();
setTimeout(() => location.href = '/', 1); setTimeout(() => location.href = '/', 1);
} else if (SettingsGet('Auth')) { } else if (SettingsGet('Auth')) {
this.weakPassword(SettingsGet('weakPassword')); this.weakPassword(!!SettingsGet('WeakPassword'));
startScreens([SettingsAdminScreen]); startScreens([SettingsAdminScreen]);
} else { } else {
startScreens([LoginAdminScreen]); startScreens([LoginAdminScreen]);
} }
this.hideLoading();
} }
} }
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, SettingsGet } 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) {
@ -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,18 @@ 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)) {
*/ notificator = notificator || createNewObject();
playNotification(force, silent) { if (notificator) {
if (force || this.notifications()) { notificator.src = Links.staticLink('sounds/'
if ('running' == audioCtx.state && (this.supportedMp3 || this.supportedOgg)) { + SettingsGet('NotificationSound')
notificator = notificator || createNewObject(); + (this.supportedMp3 ? '.mp3' : '.ogg'));
if (notificator) { notificator.volume = silent ? 0.01 : 1;
// SettingsGet('NotificationSound').startsWith('custom@') notificator.play();
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);
} }
} else {
console.log('No audio: ' + audioCtx.state);
} }
} }
}; };

View file

@ -1,16 +1,71 @@
let FOLDERS_CACHE = new Map, import { MessageSetAction } from 'Common/EnumsUser';
FOLDERS_HASH_MAP = new Map, import { arrayLength, 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';
const REQUESTED_MESSAGE_CACHE = {};
export const export const
/** /**
* @returns {void} * @returns {void}
*/ */
clearCache = () => { clear = () => {
FOLDERS_CACHE.clear(); FOLDERS_CACHE = {};
FOLDERS_HASH_MAP.clear(); FOLDERS_NAME_CACHE = {};
FOLDERS_HASH_CACHE = {};
FOLDERS_UID_NEXT_CACHE = {};
MESSAGE_FLAGS_CACHE = {};
}, },
/**
* @param {string} folderFullNameRaw
* @param {string} uid
* @returns {string}
*/
getMessageKey = (folderFullNameRaw, uid) => `${folderFullNameRaw}#${uid}`,
/**
* @param {string} folder
* @param {string} uid
*/
addRequestedMessage = (folder, uid) => REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)] = true,
/**
* @param {string} folder
* @param {string} uid
* @returns {boolean}
*/
hasRequestedMessage = (folder, uid) => true === REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)],
/**
* @param {string} folderFullNameRaw
* @param {string} uid
*/
addNewMessageCache = (folderFullNameRaw, uid) => NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)] = true,
/**
* @param {string} folderFullNameRaw
* @param {string} uid
*/
hasNewMessageAndRemoveFromCache = (folderFullNameRaw, uid) => {
if (NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)]) {
NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)] = null;
return true;
}
return false;
},
/**
* @returns {void}
*/
clearNewMessageCache = () => NEW_MESSAGE_CACHE = {},
/** /**
* @returns {string} * @returns {string}
*/ */
@ -22,35 +77,200 @@ export const
setFolderInboxName = name => inboxFolderName = name, setFolderInboxName = name => inboxFolderName = name,
/** /**
* @param {string} fullNameHash * @param {string} folderHash
* @returns {string} * @returns {string}
*/ */
getFolderFromHashMap = fullNameHash => getFolderFromCacheList(FOLDERS_HASH_MAP.get(fullNameHash)), getFolderFullNameRaw = folderHash =>
folderHash && FOLDERS_NAME_CACHE[folderHash] ? FOLDERS_NAME_CACHE[folderHash] : '',
/** /**
* @param {string} folderHash
* @param {string} folderFullNameRaw
* @param {?FolderModel} folder * @param {?FolderModel} folder
*/ */
setFolder = folder => { setFolder = (folderHash, folderFullNameRaw, folder) => {
folder.etag = ''; FOLDERS_CACHE[folderFullNameRaw] = folder;
FOLDERS_CACHE.set(folder.fullName, folder); FOLDERS_NAME_CACHE[folderHash] = folderFullNameRaw;
FOLDERS_HASH_MAP.set(folder.fullNameHash, folder.fullName);
}, },
/** /**
* @param {string} folderFullName * @param {string} folderFullNameRaw
* @param {string} folderETag * @returns {string}
*/ */
setFolderETag = (folderFullName, folderETag) => getFolderHash = folderFullNameRaw =>
FOLDERS_CACHE.has(folderFullName) && (FOLDERS_CACHE.get(folderFullName).etag = folderETag), folderFullNameRaw && FOLDERS_HASH_CACHE[folderFullNameRaw] ? FOLDERS_HASH_CACHE[folderFullNameRaw] : '',
/** /**
* @param {string} folderFullName * @param {string} folderFullNameRaw
* @param {string} folderHash
*/
setFolderHash = (folderFullNameRaw, folderHash) =>
folderFullNameRaw && (FOLDERS_HASH_CACHE[folderFullNameRaw] = folderHash),
/**
* @param {string} folderFullNameRaw
* @returns {string}
*/
getFolderUidNext = folderFullNameRaw =>
folderFullNameRaw && FOLDERS_UID_NEXT_CACHE[folderFullNameRaw]
? FOLDERS_UID_NEXT_CACHE[folderFullNameRaw]
: '',
/**
* @param {string} folderFullNameRaw
* @param {string} uidNext
*/
setFolderUidNext = (folderFullNameRaw, uidNext) =>
FOLDERS_UID_NEXT_CACHE[folderFullNameRaw] = uidNext,
/**
* @param {string} folderFullNameRaw
* @returns {?FolderModel} * @returns {?FolderModel}
*/ */
getFolderFromCacheList = folderFullName => getFolderFromCacheList = folderFullNameRaw =>
FOLDERS_CACHE.get(folderFullName), folderFullNameRaw && FOLDERS_CACHE[folderFullNameRaw] ? FOLDERS_CACHE[folderFullNameRaw] : null,
/**
* @param {string} folderFullNameRaw
*/
removeFolderFromCacheList = folderFullNameRaw => delete FOLDERS_CACHE[folderFullNameRaw];
export class MessageFlagsCache
{
/**
* @param {string} folderFullName
* @param {string} uid
* @returns {?Array}
*/
static getFor(folderFullName, uid) {
return MESSAGE_FLAGS_CACHE[folderFullName] && MESSAGE_FLAGS_CACHE[folderFullName][uid]
? MESSAGE_FLAGS_CACHE[folderFullName][uid]
: null;
}
/**
* @param {string} folderFullName
* @param {string} uid
* @param {Array} flagsCache
*/
static setFor(folderFullName, uid, flagsCache) {
if (!MESSAGE_FLAGS_CACHE[folderFullName]) {
MESSAGE_FLAGS_CACHE[folderFullName] = {};
}
MESSAGE_FLAGS_CACHE[folderFullName][uid] = flagsCache;
}
/** /**
* @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 (arrayLength(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 (arrayLength(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,56 +1,82 @@
/* eslint quote-props: 0 */ /* eslint quote-props: 0 */
export const
/** /**
* @enum {string} * @enum {string}
*/ */
ScopeMessageList = 'MessageList', export const Capa = {
ScopeFolderList = 'FolderList', OpenPGP: 'OPEN_PGP',
ScopeMessageView = 'MessageView', Prefetch: 'PREFETCH',
ScopeSettings = 'Settings', Contacts: 'CONTACTS',
Search: 'SEARCH',
SearchAdv: 'SEARCH_ADV',
MessageActions: 'MESSAGE_ACTIONS',
AttachmentsActions: 'ATTACHMENTS_ACTIONS',
DangerousActions: 'DANGEROUS_ACTIONS',
Themes: 'THEMES',
UserBackground: 'USER_BACKGROUND',
Sieve: 'SIEVE',
AttachmentThumbnails: 'ATTACHMENT_THUMBNAILS',
AutoLogout: 'AUTOLOGOUT',
AdditionalAccounts: 'ADDITIONAL_ACCOUNTS',
Identities: 'IDENTITIES'
};
/**
* @enum {string}
*/
export const Scope = {
All: 'all',
None: 'none',
Contacts: 'Contacts',
MessageList: 'MessageList',
FolderList: 'FolderList',
MessageView: 'MessageView',
Compose: 'Compose',
Settings: 'Settings',
Menu: 'Menu',
ComposeOpenPgp: 'ComposeOpenPgp',
MessageOpenPgp: 'MessageOpenPgp',
ViewOpenPgpKey: 'ViewOpenPgpKey',
KeyboardShortcutsHelp: 'KeyboardShortcutsHelp',
Ask: '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, RequestError: 1,
RequestAborted: 2, 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,
ContactsSyncError: 140, ContactsSyncError: 140,
@ -78,15 +104,20 @@ 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,
@ -96,12 +127,6 @@ Notifications = {
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

@ -4,82 +4,108 @@
* @enum {number} * @enum {number}
*/ */
export const FolderType = { export const FolderType = {
User: 0,
Inbox: 1, Inbox: 1,
Sent: 2, Sent: 2,
Drafts: 3, Drafts: 3,
Junk: 4, // Spam Spam: 4, // JUNK
Trash: 5, Trash: 5,
Archive: 6 Archive: 6,
/* NotSpam: 80
IMPORTANT : 10; };
FLAGGED : 11;
ALL : 13;
// TODO: SnappyMail
TEMPLATES : 19;
// Kolab
CONFIGURATION : 20;
CALENDAR : 21;
CONTACTS : 22;
TASKS : 23;
NOTES : 24;
FILES : 25;
JOURNAL : 26;
*/
},
/** /**
* @enum {string} * @enum {string}
*/ */
FolderMetadataKeys = { export const FolderSortMode = {
// RFC 5464 DateDesc: '', // default 'REVERSE DATE'
Comment: '/private/comment', DateAsc: 'DATE',
CommentShared: '/shared/comment', FromDesc: 'REVERSE FROM',
// RFC 6154 FromAsc: 'FROM',
SpecialUse: '/private/specialuse', SizeDesc: 'REVERSE SIZE',
// Kolab SizeAsc: 'SIZE',
KolabFolderType: '/private/vendor/kolab/folder-type', SubjectDesc: 'REVERSE SUBJECT',
KolabFolderTypeShared: '/shared/vendor/kolab/folder-type' SubjectAsc: 'SUBJECT'
}, // ToDesc: 'REVERSE TO',
// ToAsc: 'TO',
};
/** /**
* @enum {string} * @enum {string}
*/ */
ComposeType = { export const ComposeType = {
Empty: 0, Empty: 'empty',
Reply: 1, Reply: 'reply',
ReplyAll: 2, ReplyAll: 'replyall',
Forward: 3, Forward: 'forward',
ForwardAsAttachment: 4, ForwardAsAttachment: 'forward-as-attachment',
Draft: 5, Draft: 'draft',
EditAsNew: 6 EditAsNew: 'editasnew'
}, };
/** /**
* @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,9 +1,7 @@
/* eslint key-spacing: 0 */ /* eslint key-spacing: 0 */
/* eslint quote-props: 0 */ /* eslint quote-props: 0 */
import { arrayLength, pInt } from 'Common/Utils'; import { arrayLength } from 'Common/Utils';
export const RFC822 = 'message/rfc822';
const const
cache = {}, cache = {},
@ -11,11 +9,10 @@ const
msOffice = app+'vnd.openxmlformats-officedocument.', msOffice = app+'vnd.openxmlformats-officedocument.',
openDoc = app+'vnd.oasis.opendocument.', openDoc = app+'vnd.oasis.opendocument.',
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', vcard: 'text/vcard',
vcf: 'text/vcard', vcf: 'text/vcard',
htm: 'text/html', htm: 'text/html',
@ -24,14 +21,11 @@ const
ics: 'text/calendar', ics: 'text/calendar',
xml: 'text/xml', xml: 'text/xml',
json: app+'json', json: app+'json',
// asc: app+'pgp-signature', asc: app+'pgp-signature',
// asc: app+'pgp-keys',
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',
pfx: app+'x-pkcs12',
torrent: app+'x-bittorrent', torrent: app+'x-bittorrent',
// scripts // scripts
@ -120,8 +114,7 @@ export const FileType = {
Spreadsheet: 'spreadsheet', Spreadsheet: 'spreadsheet',
Presentation: 'presentation', Presentation: 'presentation',
Certificate: 'certificate', Certificate: 'certificate',
Archive: 'archive', Archive: 'archive'
Calendar: 'calendar'
}; };
export const FileInfo = { export const FileInfo = {
@ -130,15 +123,15 @@ export const FileInfo = {
* @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 app + 'ms-tnef';
} }
let ext = fileName.split('.').pop(); let ext = fileName.split('.').pop();
if (/^(txt|text|def|list|in|ini|log|sql|cfg|conf)$/.test(ext)) if (/^(txt|text|def|list|in|ini|log|sql|cfg|conf)$/.test(ext))
@ -165,8 +158,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,7 +168,7 @@ 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 => mimeType.includes(str),
archive = /^(zip|7z|tar|rar|gzip|bzip|bzip2)$/; archive = /^(zip|7z|tar|rar|gzip|bzip|bzip2)$/;
@ -192,12 +185,9 @@ export const FileInfo = {
case ['php', 'js', 'css', 'xml', 'html'].includes(ext) || 'text/html' == mimeType: case ['php', 'js', 'css', 'xml', 'html'].includes(ext) || 'text/html' == mimeType:
result = FileType.Code; result = FileType.Code;
break; break;
case 'eml' == ext || ['message/delivery-status', 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':
result = FileType.Calendar;
break;
case 'text' == mimeTypeParts[0] || 'txt' == ext || 'log' == ext: case 'text' == mimeTypeParts[0] || 'txt' == ext || 'log' == ext:
result = FileType.Text; result = FileType.Text;
break; break;
@ -207,8 +197,9 @@ export const FileInfo = {
case 'pdf' == type || 'pdf' == ext: case 'pdf' == type || 'pdf' == ext:
result = FileType.Pdf; result = FileType.Pdf;
break; break;
case [app+'pgp-signature', app+'pgp-keys', exts.p7m, exts.p7s, exts.p12, exts.pfx].includes(mimeType) case [app+'pgp-signature', app+'pgp-keys'].includes(mimeType)
|| ['asc', 'pem', 'ppk', 'p7s', 'p7m', 'p12', 'pfx'].includes(ext): || ['asc', 'pem', 'ppk'].includes(ext)
|| [app+'pkcs7-signature'].includes(mimeType) || 'p7s' == ext:
result = FileType.Certificate; result = FileType.Certificate;
break; break;
case match(msOffice+'.wordprocessingml') || match(openDoc+'.text') || match('vnd.ms-word') case match(msOffice+'.wordprocessingml') || match(openDoc+'.text') || match('vnd.ms-word')
@ -247,7 +238,6 @@ export const FileInfo = {
case FileType.Certificate: case FileType.Certificate:
case FileType.Spreadsheet: case FileType.Spreadsheet:
case FileType.Presentation: case FileType.Presentation:
case FileType.Calendar:
return result + '-' + fileType; return result + '-' + fileType;
} }
return result; return result;
@ -259,13 +249,13 @@ export const FileInfo = {
* @param {string} sFileType * @param {string} sFileType
* @returns {string} * @returns {string}
*/ */
getAttachmentsIconClass: data => { getCombinedIconClass: data => {
if (arrayLength(data)) { if (arrayLength(data)) {
let icons = data let icons = data
.map(item => item ? FileInfo.getIconClass(FileInfo.getExtension(item.fileName), item.mimeType) : '') .map(item => item ? FileInfo.getIconClass(FileInfo.getExtension(item[0]), item[1]) : '')
.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,8 +264,8 @@ 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,9 +1,9 @@
import ko from 'ko'; import ko from 'ko';
import { Scope } from 'Common/Enums';
let keyScopeFake = 'all'; let keyScopeFake = Scope.All;
export const export const
ScopeMenu = 'Menu',
doc = document, doc = document,
@ -11,18 +11,16 @@ export const
elementById = id => doc.getElementById(id), elementById = id => doc.getElementById(id),
appEl = elementById('rl-app'), exitFullscreen = () => getFullscreenElement() && (doc.exitFullscreen || doc.webkitExitFullscreen)(),
getFullscreenElement = () => doc.fullscreenElement || doc.webkitFullscreenElement,
Settings = rl.settings, Settings = rl.settings,
SettingsGet = Settings.get, SettingsGet = Settings.get,
SettingsAdmin = name => (SettingsGet('Admin') || {})[name],
SettingsCapa = name => name && !!(SettingsGet('Capa') || {})[name],
dropdowns = [],
dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 }), dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 }),
moveAction = ko.observable(false),
leftPanelDisabled = ko.observable(false), leftPanelDisabled = ko.observable(false),
toggleLeftPanel = () => leftPanelDisabled(!leftPanelDisabled()),
createElement = (name, attr) => { createElement = (name, attr) => {
let el = doc.createElement(name); let el = doc.createElement(name);
@ -30,50 +28,34 @@ export const
return el; return el;
}, },
fireEvent = (name, detail, cancelable) => dispatchEvent( // keys
new CustomEvent(name, {detail:detail, cancelable: !!cancelable}) keyScopeReal = ko.observable(Scope.All),
),
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 => { keyScope = value => {
if (!value) { if (value) {
if (Scope.Menu !== value) {
keyScopeFake = value;
if (dropdownVisibility()) {
value = Scope.Menu;
}
}
keyScopeReal(value);
shortcuts.setScope(value);
} else {
return keyScopeFake; return keyScopeFake;
} }
if (ScopeMenu !== value) {
keyScopeFake = value;
if (dropdownVisibility()) {
value = ScopeMenu;
}
}
keyScopeReal(value);
shortcuts.setScope(value);
}; };
dropdownVisibility.subscribe(value => { dropdownVisibility.subscribe(value => {
if (value) { if (value) {
keyScope(ScopeMenu); keyScope(Scope.Menu);
} else if (ScopeMenu === shortcuts.getScope()) { } else if (Scope.Menu === shortcuts.getScope()) {
keyScope(keyScopeFake); keyScope(keyScopeFake);
} }
}); });
leftPanelDisabled.subscribe(value => $htmlCL.toggle('rl-left-panel-disabled', value)); leftPanelDisabled.subscribe(value => {
value && moveAction() && moveAction(false);
$htmlCL.toggle('rl-left-panel-disabled', value);
});
moveAction.subscribe(value => value && leftPanelDisabled() && leftPanelDisabled(false));

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,13 +1,12 @@
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 = './?',
adminPath = () => rl.adminArea() && !SettingsAdmin('host'), VERSION = Settings.app('version'),
VERSION_PREFIX = Settings.app('webVersionPath') || 'snappymail/v/' + VERSION + '/';
prefix = () => BASE + '?' + (adminPath() ? SettingsAdmin('path') : '');
export const export const
SUB_QUERY_PREFIX = '&q[]=', SUB_QUERY_PREFIX = '&q[]=',
@ -16,12 +15,14 @@ export const
* @param {string=} startupUrl * @param {string=} startupUrl
* @returns {string} * @returns {string}
*/ */
root = () => HASH_PREFIX, root = (startupUrl = '') => HASH_PREFIX + pString(startupUrl),
/** /**
* @returns {string} * @returns {string}
*/ */
logoutLink = () => adminPath() ? prefix() : BASE, logoutLink = () => (rl.adminArea() && !Settings.app('adminHostUse'))
? SERVER_PREFIX + (Settings.app('adminPath') || 'admin')
: ROOT,
/** /**
* @param {string} type * @param {string} type
@ -30,33 +31,31 @@ export const
* @returns {string} * @returns {string}
*/ */
serverRequestRaw = (type, hash) => serverRequestRaw = (type, hash) =>
BASE + '?/Raw/' + SUB_QUERY_PREFIX + '/' SERVER_PREFIX + '/Raw/' + SUB_QUERY_PREFIX + '/'
+ '0/' // Settings.get('accountHash') ? + '0/' // AuthAccountHash ?
+ (type + (type
? type + '/' + (hash ? SUB_QUERY_PREFIX + '/' + hash : '') ? type + '/' + (hash ? SUB_QUERY_PREFIX + '/' + hash : '')
: ''), : ''),
/** /**
* @param {string} download * @param {string} download
* @param {string=} customSpecSuffix
* @returns {string} * @returns {string}
*/ */
attachmentDownload = (download) => attachmentDownload = (download, customSpecSuffix) =>
serverRequestRaw('Download', download), serverRequestRaw('Download', download, customSpecSuffix),
proxy = url =>
BASE + '?/ProxyExternal/'
// + btoa(JSON.stringify([token,url]).replace(/ /g, '%20')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
+ btoa(url.replace(/ /g, '%20')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
// + b64EncodeJSONSafe(url.replace(/ /g, '%20')),
/** /**
* @param {string} type * @param {string} type
* @returns {string} * @returns {string}
*/ */
serverRequest = type => prefix() + '/' + type + '/' + SUB_QUERY_PREFIX + '/0/', serverRequest = type => SERVER_PREFIX + '/' + type + '/' + SUB_QUERY_PREFIX + '/0/',
// Is '?/Css/0/Admin' needed? /**
cssLink = theme => BASE + '?/Css/0/User/-/' + encodeURI(theme) + '/-/' + Date.now() + '/Hash/-/Json/', * @param {string} email
* @returns {string}
*/
change = email => serverRequest('Change') + encodeURIComponent(email) + '/',
/** /**
* @param {string} lang * @param {string} lang
@ -64,31 +63,36 @@ export const
* @returns {string} * @returns {string}
*/ */
langLink = (lang, isAdmin) => langLink = (lang, isAdmin) =>
BASE + '?/Lang/0/' + (isAdmin ? 'Admin' : 'App') SERVER_PREFIX + '/Lang/0/' + (isAdmin ? 'Admin' : 'App') + '/' + encodeURI(lang) + '/' + VERSION + '/',
+ '/' + encodeURI(lang)
+ '/' + Settings.app('version') + '/',
/** /**
* @param {string} path * @param {string} path
* @returns {string} * @returns {string}
*/ */
staticLink = path => Settings.app('webVersionPath') + 'static/' + path, staticLink = path => VERSION_PREFIX + 'static/' + path,
/**
* @returns {string}
*/
openPgpJs = () => staticLink('js/min/openpgp.min.js'),
/**
* @returns {string}
*/
openPgpWorkerJs = () => staticLink('js/min/openpgp.worker.min.js'),
/** /**
* @param {string} theme * @param {string} theme
* @returns {string} * @returns {string}
*/ */
themePreviewLink = theme => { themePreviewLink = theme => {
if (theme.endsWith('@nextcloud')) { let prefix = VERSION_PREFIX;
theme = theme.slice(0, theme.length - 10).trim(); if ('@custom' === theme.substr(-7)) {
return parent.OC.webroot + '/themes/' + encodeURI(theme) + '/snappymail/preview.png'; theme = theme.substr(0, theme.length - 7).trim();
prefix = Settings.app('webPath') || '';
} }
let path = 'webVersionPath';
if (theme.endsWith('@custom')) { return prefix + 'themes/' + encodeURI(theme) + '/images/preview.png';
theme = theme.slice(0, theme.length - 7).trim();
path = 'webPath';
}
return Settings.app(path) + 'themes/' + encodeURI(theme) + '/images/preview.png';
}, },
/** /**
@ -120,24 +124,23 @@ export const
* @param {number=} threadUid = 0 * @param {number=} threadUid = 0
* @returns {string} * @returns {string}
*/ */
mailBox = (folder, page, search, threadUid, messageUid) => { mailBox = (folder, page = 1, search = '', threadUid = 0) => {
let result = [HASH_PREFIX + 'mailbox']; page = pInt(page, 1);
search = pString(search);
let result = HASH_PREFIX + 'mailbox/';
if (folder) { if (folder) {
result.push(folder + (threadUid ? '~' + threadUid : '')); result += encodeURI(folder) + (threadUid ? '~' + threadUid : '');
} }
if (messageUid) { if (1 < page) {
result.push('m' + messageUid); result = result.replace(/\/+$/, '') + '/p' + page;
} else {
page = pInt(page, 1);
if (1 < page) {
result.push('p' + page);
}
search && result.push(encodeURI(search));
} }
return result.join('/'); if (search) {
}, result = result.replace(/\/+$/, '') + '/' + encodeURI(search);
}
mailBoxMessage = (folder, messageUid) => mailBox(folder, 1, '', 0, pInt(messageUid)); return result;
};

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

@ -0,0 +1,55 @@
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 {
if (time) {
element.dateTime = (new Date(time * 1000)).format('Y-m-d\\TH:i:s');
} else {
time = Date.parse(element.dateTime) / 1000;
}
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);
}
}

View file

@ -1,5 +1,6 @@
import { settingsAddViewModel } from 'Screen/AbstractSettings'; import { settingsAddViewModel } from 'Screen/AbstractSettings';
import { SettingsGet } from 'Common/Globals'; import { SettingsGet } from 'Common/Globals';
import { showScreenPopup } from 'Knoin/Knoin';
import { AbstractViewPopup } from 'Knoin/AbstractViews'; import { AbstractViewPopup } from 'Knoin/AbstractViews';
const USER_VIEW_MODELS_HOOKS = [], const USER_VIEW_MODELS_HOOKS = [],
@ -12,7 +13,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 +40,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 +50,11 @@ 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 = SettingsGet('Plugins');
plugins = plugins && null != plugins[pluginSection] ? plugins[pluginSection] : null;
return plugins ? (null == plugins[name] ? null : plugins[name]) : null;
};
rl.showPluginPopup = showScreenPopup;
rl.pluginPopupView = AbstractViewPopup; rl.pluginPopupView = AbstractViewPopup;

View file

@ -1,19 +1,15 @@
import ko from 'ko'; import ko from 'ko';
import { addEventsListeners, addShortcut, registerShortcut } from 'Common/Globals';
import { isArray } from 'Common/Utils'; import { isArray } from 'Common/Utils';
import { koComputable } from 'External/ko';
/* /*
oCallbacks: oCallbacks:
ItemSelect ItemSelect
MiddleClick MiddleClick
canSelect AutoSelect
ItemGetUid ItemGetUid
UpOrDown UpOrDown
*/ */
let shiftStart;
export class Selector { export class Selector {
/** /**
* @param {koProperty} koList * @param {koProperty} koList
@ -21,86 +17,89 @@ export class Selector {
* @param {koProperty} koFocusedItem * @param {koProperty} koFocusedItem
* @param {string} sItemSelector * @param {string} sItemSelector
* @param {string} sItemCheckedSelector * @param {string} sItemCheckedSelector
* @param {string} sItemFocusedSelector
*/ */
constructor( constructor(
koList, koList,
koSelectedItem, koSelectedItem,
koFocusedItem, koFocusedItem,
sItemSelector, sItemSelector,
sItemCheckedSelector sItemCheckedSelector,
sItemFocusedSelector
) { ) {
koFocusedItem = (koFocusedItem || ko.observable(null)).extend({ toggleSubscribeProperty: [this, 'focused'] });
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.listChecked = ko.computed(() => this.list.filter(item => item.checked())).extend({ rateLimit: 0 });
this.isListChecked = ko.computed(() => 0 < this.listChecked().length);
this.focusedItem = koFocusedItem; this.focusedItem = koFocusedItem || ko.observable(null);
this.selectedItem = koSelectedItem; this.selectedItem = koSelectedItem || ko.observable(null);
this.selectedItemUseCallback = true;
this.iSelectNextHelper = 0; this.iSelectNextHelper = 0;
this.iFocusedNextHelper = 0; this.iFocusedNextHelper = 0;
// this.oContentScrollable = null; this.oContentScrollable;
this.sItemSelector = sItemSelector; this.sItemSelector = sItemSelector;
this.sItemCheckedSelector = sItemCheckedSelector; this.sItemCheckedSelector = sItemCheckedSelector;
this.sItemFocusedSelector = sItemSelector + '.focused'; this.sItemFocusedSelector = sItemFocusedSelector;
this.sLastUid = ''; this.sLastUid = '';
this.oCallbacks = {}; this.oCallbacks = {};
const const itemSelectedThrottle = (item => this.itemSelected(item)).debounce(300);
itemSelected = item => {
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) => {
this.listChecked.subscribe(items => {
if (items.length) { if (items.length) {
koSelectedItem() ? koSelectedItem(null) : koSelectedItem.valueHasMutated?.(); if (null === this.selectedItem()) {
} else { if (this.selectedItem.valueHasMutated) {
this.autoSelect(); this.selectedItem.valueHasMutated();
}
} else {
this.selectedItem(null);
}
} else if (this.autoSelect() && this.focusedItem()) {
this.selectedItem(this.focusedItem());
} }
}); }, this);
let selectedItemUseCallback = true; this.selectedItem.subscribe((item) => {
koSelectedItem.subscribe(item => {
if (item) { if (item) {
// koList.forEach(subItem => subItem.checked(false)); if (this.isListChecked()) {
selectedItemUseCallback && itemSelectedThrottle(item); this.listChecked().forEach(subItem => subItem.checked(false));
} else { }
selectedItemUseCallback && itemSelected();
if (this.selectedItemUseCallback) {
itemSelectedThrottle(item);
}
} else if (this.selectedItemUseCallback) {
this.itemSelected(null);
} }
}); }, this);
koFocusedItem.subscribe(item => item && (this.sLastUid = this.getItemUid(item))); this.selectedItem = this.selectedItem.extend({ toggleSubscribeProperty: [this, 'selected'] });
this.focusedItem = this.focusedItem.extend({ toggleSubscribeProperty: [null, 'focused'] });
/** this.focusedItem.subscribe(item => item && (this.sLastUid = this.getItemUid(item)), this);
* Below code is used to keep checked/focused/selected states when array is refreshed.
*/
let aCheckedCache = [], let aCache = [],
aCheckedCache = [],
mFocused = null, mFocused = null,
mSelected = null; mSelected = null;
// Before removing old list this.list.subscribe(
koList.subscribe(
items => { items => {
if (isArray(items)) { if (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 +110,120 @@ 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 = [];
this.selectedItemUseCallback = false;
this.focusedItem(null);
this.selectedItem(null);
if (isArray(aItems)) { if (isArray(aItems)) {
let temp, len = aCheckedCache.length;
isChecked,
next = this.iFocusedNextHelper || this.iSelectNextHelper;
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()) {
item || (this.oCallbacks.ItemSelect || (()=>0))(null);
} else if (item) {
(this.oCallbacks.ItemSelect || (()=>0))(item);
}
}
unselect() { unselect() {
this.selectedItem(null); this.selectedItem(null);
this.focusedItem(null); this.focusedItem(null);
@ -183,34 +238,32 @@ export class Selector {
return el ? ko.dataFor(el) : null; return el ? ko.dataFor(el) : null;
}; };
addEventsListeners(contentScrollable, { contentScrollable.addEventListener('click', event => {
click: event => { let el = event.target.closestWithin(this.sItemSelector, contentScrollable);
const el = event.target.closestWithin(this.sItemSelector, contentScrollable); el && this.actionClick(ko.dataFor(el), event);
let item = el && ko.dataFor(el);
el && (this.oCallbacks.click || (()=>1))(event, item) && this.actionClick(item, event);
item = getItem(this.sItemCheckedSelector); const item = getItem(this.sItemCheckedSelector);
if (item) { if (item) {
if (event.shiftKey) { if (event.shiftKey) {
this.actionClick(item, event); this.actionClick(item, event);
} else { } else {
this.focusedItem(item); this.focusedItem(item);
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, () => { contentScrollable.addEventListener('auxclick', event => {
if (1 == event.button) {
const item = getItem(this.sItemSelector);
if (item) {
this.focusedItem(item);
(this.oCallbacks.MiddleClick || (()=>0))(item);
}
}
});
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);
@ -218,13 +271,13 @@ export class Selector {
} }
}); });
addShortcut('arrowup,arrowdown', 'meta', keyScope, () => false); shortcuts.add('arrowup,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 +287,8 @@ export class Selector {
/** /**
* @returns {boolean} * @returns {boolean}
*/ */
autoSelect(bForce) { autoSelect() {
(bForce || (this.oCallbacks.canSelect || (()=>1))()) return !!(this.oCallbacks.AutoSelect || (()=>true))();
&& this.focusedItem()
&& this.selectedItem(this.focusedItem());
} }
/** /**
@ -245,7 +296,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.ItemGetUid || null;
if (getItemUidCallback && item) {
uid = getItemUidCallback(item);
}
return uid.toString();
} }
/** /**
@ -254,36 +312,24 @@ export class Selector {
* @param {boolean=} bForceSelect = false * @param {boolean=} bForceSelect = false
*/ */
newSelectPosition(sEventKey, bShiftKey, bForceSelect) { newSelectPosition(sEventKey, bShiftKey, bForceSelect) {
let result; let isArrow = 'ArrowUp' === sEventKey || 'ArrowDown' === sEventKey,
result = null;
const up = 'ArrowUp' === sEventKey, const pageStep = 10,
isArrow = up || 'ArrowDown' === sEventKey,
pageStep = 10,
list = this.list(), list = this.list(),
listLen = list.length, listLen = list.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); result || (this.oCallbacks.UpOrDown || (()=>true))('ArrowUp' === sEventKey);
result || this.oCallbacks.UpOrDown?.(up);
} else if ('Home' === sEventKey) { } else if ('Home' === sEventKey) {
result = list[0]; result = list[0];
} else if ('End' === sEventKey) { } else if ('End' === sEventKey) {
@ -310,12 +356,26 @@ export class Selector {
) { ) {
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 && ((bShiftKey && isArrow) || ' ' === sEventKey)) {
focused.checked(!focused.checked());
} }
if (' ' !== sEventKey && (this.autoSelect() || bForceSelect) && !this.isListChecked()) {
this.selectedItem(result);
}
this.scrollToFocused();
} else if (focused) {
if ((bShiftKey && isArrow) || ' ' === sEventKey) {
focused.checked(!focused.checked());
}
this.focusedItem(focused);
} }
} }
@ -325,15 +385,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 +408,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,58 +1,56 @@
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, createElement } from 'Common/Globals';
import { getKeyByValue, forEachObjectEntry } from 'Common/Utils';
import { pInt } from 'Common/Utils';
import { LanguageStore } from 'Stores/Language';
let I18N_DATA = {}; let I18N_DATA = {};
const 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);
}
}
},
init = () => { init = () => {
if (rl.I18N) { if (rl.I18N) {
I18N_DATA = rl.I18N; I18N_DATA = rl.I18N;
Date.defineRelativeTimeFormat(rl.relativeTime || {});
rl.I18N = null; rl.I18N = null;
doc.documentElement.dir = I18N_DATA.LANG_DIR;
return 1; return 1;
} }
}, },
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)); getNotificationMessage = code => {
let key = getKeyByValue(Notification, code);
if (key) {
key = i18nKey(key).replace('_NOTIFICATION', '_ERROR');
return I18N_DATA.NOTIFICATIONS[key];
}
};
export const export const
translateTrigger = ko.observable(false), trigger = ko.observable(false),
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat
// see /snappymail/v/0.0.0/app/localization/relativetimeformat/
relativeTime = seconds => {
let unit = 'second',
t = [[60,'minute'],[3600,'hour'],[86400,'day'],[2628000,'month'],[31536000,'year']],
i = 5,
abs = Math.abs(seconds);
while (i--) {
if (t[i][0] <= abs) {
seconds = Math.round(seconds / t[i][0]);
unit = t[i][1];
break;
}
}
if (Intl.RelativeTimeFormat) {
return (new Intl.RelativeTimeFormat(doc.documentElement.lang)).format(seconds, unit);
}
// Safari < 14
abs = Math.abs(seconds);
let rtf = rl.relativeTime.long[unit][0 > seconds ? 'past' : 'future'],
plural = rl.relativeTime.plural(abs);
return (rtf[plural] || rtf).replace('{0}', abs);
},
/** /**
* @param {string} key * @param {string} key
@ -61,14 +59,18 @@ export const
* @returns {string} * @returns {string}
*/ */
i18n = (key, valueList, defaulValue) => { i18n = (key, valueList, defaulValue) => {
let result = defaulValue ?? key; let result = defaulValue || key;
let path = key.split('/'); if (key) {
if (I18N_DATA[path[0]] && path[1]) { let path = key.split('/');
result = I18N_DATA[path[0]][path[1]] || result; if (I18N_DATA[path[0]] && path[1]) {
result = I18N_DATA[path[0]][path[1]] || result;
}
}
if (valueList) {
Object.entries(valueList).forEach(([key, value]) => {
result = result.replace('%' + key + '%', value);
});
} }
valueList && forEachObjectEntry(valueList, (key, value) => {
result = result.replace('%' + key + '%', value);
});
return result; return result;
}, },
@ -78,93 +80,17 @@ export const
*/ */
i18nToNodes = element => i18nToNodes = element =>
setTimeout(() => setTimeout(() =>
element.querySelectorAll('[data-i18n]').forEach(element => { element.querySelectorAll('[data-i18n]').forEach(item => i18nToNode(item))
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), , 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} startCallback
* @param {Function=} langCallback = null * @param {Function=} langCallback = null
*/ */
initOnStartOrLangChange = (startCallback, langCallback) => { initOnStartOrLangChange = (startCallback, langCallback = null) => {
startCallback?.(); startCallback && startCallback();
startCallback && translateTrigger.subscribe(startCallback); startCallback && trigger.subscribe(startCallback);
langCallback && translateTrigger.subscribe(langCallback); langCallback && trigger.subscribe(langCallback);
}, },
/** /**
@ -174,52 +100,61 @@ export const
* @returns {string} * @returns {string}
*/ */
getNotification = (code, message = '', defCode = 0) => { getNotification = (code, message = '', defCode = 0) => {
code = pInt(code); code = parseInt(code, 10) || 0;
if (Notifications.ClientViewError === code && message) { if (Notification.ClientViewError === code && message) {
return message; return message;
} }
return getNotificationMessage(code) return getNotificationMessage(code)
|| getNotificationMessage(pInt(defCode)) || getNotificationMessage(parseInt(defCode, 10))
|| ''; || '';
}, },
getErrorMessage = (code, data) =>
getNotification(code) || data?.messageAdditional || data?.message || data,
/** /**
* @param {*} code * @param {*} code
* @returns {string} * @returns {string}
*/ */
getUploadErrorDescByCode = code => { getUploadErrorDescByCode = code => {
let key = getKeyByValue(UploadErrorCode, parseInt(code, 10)); let result = 'UNKNOWN';
return i18n('UPLOAD/ERROR_' + (key ? i18nKey(key) : '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 {boolean} admin
* @param {string} language * @param {string} language
*/ */
translatorReload = (language, admin) => reload = (admin, language) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
const script = createElement('script'); const script = createElement('script');
script.onload = () => { script.onload = () => {
// reload the data // reload the data
if (init()) { if (init()) {
i18nToNodes(doc); i18nToNodes(doc);
translateTrigger(!translateTrigger()); admin || rl.app.reloadTime();
// admin || reloadTime(); trigger(!trigger());
} }
script.remove(); script.remove();
resolve(); resolve();
}; };
script.onerror = () => reject(Error('Language '+language+' failed')); script.onerror = () => reject(new Error('Language '+language+' failed'));
script.src = langLink(language, admin); script.src = langLink(language, admin);
// script.async = true; // script.async = true;
doc.head.append(script); doc.head.append(script);
}), }),
/** /**
*
* @param {string} language * @param {string} language
* @param {boolean=} isEng = false * @param {boolean=} isEng = false
* @returns {string} * @returns {string}
@ -229,8 +164,6 @@ export const
'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language, 'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language,
null, null,
language language
), );
baseCollator = numeric => new Intl.Collator(doc.documentElement.lang, {numeric: !!numeric, sensitivity: 'base'});
init(); init();

View file

@ -1,31 +1,96 @@
import { SaveSettingsStep } from 'Common/Enums';
import { doc, elementById } from 'Common/Globals';
let __themeTimer = 0,
__themeJson = null;
export const export const
isArray = Array.isArray, isArray = Array.isArray,
arrayLength = array => isArray(array) && array.length, arrayLength = array => isArray(array) && array.length,
isFunction = v => typeof v === 'function', isFunction = v => typeof v === 'function',
pString = value => null != value ? '' + value : '', pString = value => null != value ? '' + value : '',
forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn),
forEachObjectEntry = (obj, fn) => Object.entries(obj).forEach(([key, value]) => fn(key, value)),
pInt = (value, defaultValue = 0) => { pInt = (value, defaultValue = 0) => {
value = parseInt(value, 10); value = parseInt(value, 10);
return isFinite(value) ? value : defaultValue; return isNaN(value) || !isFinite(value) ? defaultValue : value;
}, },
convertThemeName = theme => theme
.replace(/@custom$/, '')
.replace(/([A-Z])/g, ' $1')
.replace(/[^a-zA-Z0-9]+/g, ' ')
.trim(),
defaultOptionsAfterRender = (domItem, item) => defaultOptionsAfterRender = (domItem, item) =>
item && undefined !== item.disabled && domItem?.classList.toggle('disabled', domItem.disabled = item.disabled), domItem && item && undefined !== item.disabled
&& domItem.classList.toggle('disabled', domItem.disabled = item.disabled),
// unescape(encodeURIComponent()) makes the UTF-16 DOMString to an UTF-8 string addObservablesTo = (target, observables) =>
b64Encode = data => btoa(unescape(encodeURIComponent(data))), Object.entries(observables).forEach(([key, value]) =>
/* // Without deprecated 'unescape': target[key] = /*isArray(value) ? ko.observableArray(value) :*/ ko.observable(value) ),
b64Encode = data => btoa(encodeURIComponent(data).replace(
/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode('0x' + p1)
)),
*/
b64EncodeJSON = data => b64Encode(JSON.stringify(data)), addComputablesTo = (target, computables) =>
Object.entries(computables).forEach(([key, fn]) => target[key] = ko.computed(fn)),
b64EncodeJSONSafe = data => b64EncodeJSON(data).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''), addSubscribablesTo = (target, subscribables) =>
Object.entries(subscribables).forEach(([key, fn]) => target[key].subscribe(fn)),
getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v); inFocus = () => {
try {
return doc.activeElement && doc.activeElement.matches(
'input,textarea,[contenteditable]'
);
} catch (e) {
return false;
}
},
settingsSaveHelperSimpleFunction = (koTrigger, context) =>
iError => {
koTrigger.call(context, iError ? SaveSettingsStep.FalseResult : SaveSettingsStep.TrueResult);
setTimeout(() => koTrigger.call(context, SaveSettingsStep.Idle), 1000);
},
changeTheme = (value, themeTrigger = ()=>0) => {
const themeStyle = elementById('app-theme-style'),
clearTimer = () => {
__themeTimer = setTimeout(() => themeTrigger(SaveSettingsStep.Idle), 1000);
__themeJson = null;
};
let url = 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 (2 === arrayLength(data)) {
themeStyle.textContent = data[1];
themeStyle.dataset.href = url;
themeStyle.dataset.theme = data[0];
themeTrigger(SaveSettingsStep.TrueResult);
}
})
.then(clearTimer, clearTimer);
}
};

View file

@ -1,138 +1,285 @@
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 { encodeHtml } from 'Common/Html';
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 { isArray } from 'Common/Utils';
import { createElement } from 'Common/Globals';
import { showScreenPopup } from 'Knoin/Knoin'; import { FolderUserStore } from 'Stores/User/Folder';
import { IdentityPopupView } from 'View/Popup/Identity'; import { SettingsUserStore } from 'Stores/User/Settings';
export const
// 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) {
console.log('download: '+link); return null != value && /^[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} html
let params = { * @returns {string}
target: 'zip', */
filename: name, export function htmlToPlain(html) {
hashes: hashes let pos = 0,
}; limit = 800,
if (!onError) { iP1 = 0,
onError = () => alert('Download failed'); iP2 = 0,
} iP3 = 0,
if (folder) { text = '';
params.folder = folder;
// params.uids = uids; const
} tpl = createElement('template'),
Remote.post('AttachmentsActions', fTrigger || null, params)
.then(result => { convertBlockquote = (blockquoteText) => {
let hash = result?.Result?.fileHash; blockquoteText = '> ' + blockquoteText.trim().replace(/\n/gm, '\n> ');
hash ? download(attachmentDownload(hash), hash+'.zip') : onError(); return blockquoteText.replace(/(^|\n)([> ]+)/gm, (...args) =>
}) args && 2 < args.length ? args[1] + args[2].replace(/[\s]/g, '').trim() + ' ' : ''
.catch(onError); );
},
convertDivs = (...args) => {
let divText = 1 < args.length ? args[1].trim() : '';
if (divText.length) {
divText = '\n' + divText.replace(/<div[^>]*>([\s\S\r\n]*)<\/div>/gim, convertDivs).trim() + '\n';
}
return divText;
},
convertPre = (...args) =>
1 < args.length
? args[1]
.toString()
.replace(/[\n]/gm, '<br/>')
.replace(/[\r]/gm, '')
: '',
fixAttibuteValue = (...args) => (1 < args.length ? args[1] + encodeHtml(args[2]) : ''),
convertLinks = (...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 = tpl.content.textContent;
if (text) {
text = text
.replace(/\n[ \t]+/gm, '\n')
.replace(/[\n]{3,}/gm, '\n\n')
.replace(/&gt;/gi, '>')
.replace(/&lt;/gi, '<')
.replace(/&amp;/gi, '&')
// wordwrap max line length 100
.match(/.{1,100}(\s|$)|\S+?(\s|$)/g).join('\n');
} }
},
while (0 < --limit) {
iP1 = text.indexOf('__bq__start__', pos);
if (0 > iP1) {
break;
}
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;
}
}
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=} aDisabled
* @param {Array=} aHeaderLines
* @param {Function=} fDisableCallback
* @param {Function=} fRenameCallback
* @param {boolean=} bNoSelectSelectable Used in FolderCreatePopupView
* @returns {Array}
*/
export function folderListOptionsBuilder(
aDisabled,
aHeaderLines,
fRenameCallback,
fDisableCallback,
bNoSelectSelectable,
aList = FolderUserStore.folderList()
) {
const
aResult = [],
sDeepPrefix = '\u00A0\u00A0\u00A0',
// FolderSystemPopupView should always be true
showUnsubscribed = fRenameCallback ? !SettingsUserStore.hideUnsubscribed() : true,
foldersWalk = folders => {
folders.forEach(oItem => {
if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
aResult.push({
id: oItem.fullNameRaw,
name:
sDeepPrefix.repeat(oItem.deep) +
fRenameCallback(oItem),
system: false,
disabled: !bNoSelectSelectable && (
!oItem.selectable() ||
aDisabled.includes(oItem.fullNameRaw) ||
fDisableCallback(oItem))
});
}
if (oItem.subFolders.length) {
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(aList);
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;
@ -188,187 +335,71 @@ computedPaginatorHelper = (koCurrentPage, koPageCount) => {
return result; return result;
}; };
}, }
/** /**
* @param {string} mailToUrl * @param {string} mailToUrl
* @returns {boolean} * @returns {boolean}
*/ */
mailToHelper = mailToUrl => { export function mailToHelper(mailToUrl) {
if ('mailto:' === mailToUrl?.slice(0, 7).toLowerCase()) { if (
mailToUrl = mailToUrl.slice(7).split('?'); mailToUrl &&
'mailto:' ===
mailToUrl
.toString()
.substr(0, 7)
.toLowerCase()
) {
mailToUrl = mailToUrl.toString().substr(7);
const let to = [],
email = decodeURIComponent(mailToUrl[0]), params = {};
params = new URLSearchParams(mailToUrl[1]),
to = params.get('to'), const email = mailToUrl.replace(/\?.+$/, ''),
toEmailModel = value => EmailCollectionModel.fromString(value); query = mailToUrl.replace(/^[^?]*\?/, ''),
toEmailModel = value => null != value ? EmailModel.parseEmailLine(decodeURIComponent(value)) : null;
query.split('&').forEach(temp => {
temp = temp.split('=');
params[decodeURIComponent(temp[0])] = decodeURIComponent(temp[1]);
});
if (null != params.to) {
to = Object.values(
toEmailModel(email + ',' + params.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);
}
showMessageComposer([ showMessageComposer([
ComposeType.Empty, ComposeType.Empty,
null, null,
toEmailModel(to ? email + ',' + to : email), to,
toEmailModel(params.get('cc')), toEmailModel(params.cc),
toEmailModel(params.get('bcc')), toEmailModel(params.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 = []) => export function showMessageComposer(params = [])
{ {
rl.app.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));

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

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

View file

@ -0,0 +1,28 @@
import ko from 'ko';
import { AbstractComponent } from 'Component/Abstract';
class AbstractCheckbox extends AbstractComponent {
/**
* @param {Object} params = {}
*/
constructor(params = {}) {
super();
this.value = ko.isObservable(params.value) ? params.value
: ko.observable(!!params.value);
this.enable = ko.isObservable(params.enable) ? params.enable
: ko.observable(undefined === params.enable || !!params.enable);
this.label = params.label || '';
this.inline = !!params.inline;
this.labeled = undefined !== params.label;
}
click() {
this.enable() && this.value(!this.value());
}
}
export { AbstractCheckbox };

View file

@ -0,0 +1,54 @@
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.label = params.label || '';
this.enable = null == params.enable ? true : params.enable;
this.trigger = params.trigger && params.trigger.subscribe ? params.trigger : null;
this.placeholder = params.placeholder || '';
this.labeled = null != params.label;
let size = params.size || 0;
if (this.trigger) {
const
classForTrigger = ko.observable(''),
setTriggerState = value => {
switch (pInt(value)) {
case SaveSettingsStep.TrueResult:
classForTrigger('success');
break;
case SaveSettingsStep.FalseResult:
classForTrigger('error');
break;
default:
classForTrigger('');
break;
}
};
setTriggerState(this.trigger());
this.className = ko.computed(() =>
((0 < size ? 'span' + size : '') + ' settings-saved-trigger-input ' + classForTrigger()).trim()
);
this.disposable.push(this.trigger.subscribe(setTriggerState, this));
} else {
this.className = ko.computed(() => 0 < size ? 'span' + size : '');
}
this.disposable.push(this.className);
}
}
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,30 +1,9 @@
import { doc, createElement, addEventsListeners } from 'Common/Globals'; import { doc, createElement } from 'Common/Globals';
import { isArray } from 'Common/Utils';
import { EmailModel } from 'Model/Email'; import { EmailModel } from 'Model/Email';
import { addressparser } from 'Mime/Address';
const contentType = 'snappymail/emailaddress', const contentType = 'snappymail/emailaddress',
getAddressKey = li => li?.emailaddress?.key, getAddressKey = li => li && li.emailaddress && 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; let dragAddress, datalist;
@ -38,12 +17,10 @@ export class EmailAddressesComponent {
doc.body.append(datalist); doc.body.append(datalist);
} }
const self = this, var 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 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 // In Chrome Mobile dataTransfer.types.includes(contentType) fails, only text/plain is set
validDropzone = () => dragAddress?.li.parentNode !== self.ul, validDropzone = () => dragAddress && dragAddress.li.parentNode !== self.ul,
fnDrag = e => validDropzone() && e.preventDefault(); fnDrag = e => validDropzone() && e.preventDefault();
self.element = element; self.element = element;
@ -65,61 +42,57 @@ export class EmailAddressesComponent {
// Create the elements // Create the elements
self.ul = createElement('ul',{class:"emailaddresses"}); self.ul = createElement('ul',{class:"emailaddresses"});
addEventsListeners(self.ul, { self.ul.addEventListener('click', e => self._focus(e));
click: e => self._focus(e), self.ul.addEventListener('dblclick', e => self._editTag(e));
dblclick: e => self._editTag(e), self.ul.addEventListener("dragenter", fnDrag);
dragenter: fnDrag, self.ul.addEventListener("dragover", fnDrag);
dragover: fnDrag, self.ul.addEventListener("drop", e => {
drop: e => { if (validDropzone() && dragAddress.value) {
if (validDropzone() && dragAddress.value) { e.preventDefault();
e.preventDefault(); dragAddress.source._removeDraggedTag(dragAddress.li);
dragAddress.source._removeDraggedTag(dragAddress.li); self._parseValue(dragAddress.value);
self._parseValue(dragAddress.value);
}
} }
}); });
self.input = input; self.input = createElement('input',{type:"text", list:datalist.id,
autocomplete:"off", autocorrect:"off", autocapitalize:"off", spellcheck:"false"});
addEventsListeners(input, { self.input.addEventListener('focus', () => self._focusTrigger(true));
focus: () => { self.input.addEventListener('blur', () => {
self._focusTrigger(true); // prevent autoComplete menu click from causing a false 'blur'
input.value || self._resetDatalist(); self._parseInput(true);
}, self._focusTrigger(false);
blur: () => { });
// prevent autoComplete menu click from causing a false 'blur' self.input.addEventListener('keydown', e => {
self._parseInput(true); if ('Backspace' === e.key || 'ArrowLeft' === e.key) {
self._focusTrigger(false); // if our input contains no value and backspace has been pressed, select the last tag
}, var lastTag = self.inputCont.previousElementSibling,
keydown: e => { input = self.input;
if ('Backspace' === e.key || 'ArrowLeft' === e.key) { if (lastTag && (!input.value
// if our input contains no value and backspace has been pressed, select the last tag || (('selectionStart' in input) && input.selectionStart === 0 && input.selectionEnd === 0))
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(); e.preventDefault();
self._parseInput(true); lastTag.querySelector('a').focus();
} }
},
input: () => {
self._parseInput();
self._updateDatalist(); self._updateDatalist();
} else if (e.key == 'Enter') {
e.preventDefault();
self._parseInput(true);
} }
}); });
self.input.addEventListener('input', () => {
self._parseInput();
self._updateDatalist();
});
self.input.addEventListener('focus', () => self.input.value || self._resetDatalist());
// define starting placeholder // define starting placeholder
if (element.placeholder) { if (element.placeholder) {
input.placeholder = element.placeholder; self.input.placeholder = element.placeholder;
} }
self.inputCont = createElement('li',{class:"emailaddresses-input"}); self.inputCont = createElement('li',{class:"emailaddresses-input"});
self.inputCont.append(input); self.inputCont.append(self.input);
self.ul.append(self.inputCont); self.ul.append(self.inputCont);
element.replaceWith(self.ul); element.replaceWith(self.ul);
@ -131,23 +104,14 @@ export class EmailAddressesComponent {
self._updateDatalist = self.options.autoCompleteSource self._updateDatalist = self.options.autoCompleteSource
? (() => { ? (() => {
let value = input.value.trim(); let value = self.input.value.trim();
if (datalist.inputValue !== value) { if (datalist.inputValue !== value) {
datalist.inputValue = value; datalist.inputValue = value;
value.length && self.options.autoCompleteSource( value.length && self.options.autoCompleteSource(
value, {term:value},
items => { items => {
self._resetDatalist(); self._resetDatalist();
let chars = value.length; items && items.forEach(item => datalist.append(new Option(item)));
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';
}
} }
) )
} }
@ -166,66 +130,28 @@ export class EmailAddressesComponent {
_parseInput(force) { _parseInput(force) {
let val = this.input.value; let val = this.input.value;
if ((force || val.includes(',') || val.includes(';') if (force || val.includes(',') || val.includes(';')) {
|| (val.charAt(val.length-1)===' ' && this._simpleEmailMatch(val))) this._parseValue(val) && (this.input.value = '');
&& this._parseValue(val)) {
this.input.value = '';
} }
this._resizeInput(); this._resizeInput();
} }
_parseValue(val) { _parseValue(val) {
if (val) { if (val) {
const self = this, var self = this,
v = val.trim(), values = [];
hook = (v && [',', ';', '\n'].includes(v.slice(-1))) ? splitEmailLine(val) : null,
values = (hook || [val]).map(value => parseEmailLine(value)) const v = val.trim(),
.flat(Infinity) hook = (v && [',', ';', '\n'].includes(v.substr(-1)))
.map(item => (item.toLine ? [item.toLine(), item] : [item, null])); ? EmailModel.splitEmailLine(val)
: null;
values = (hook || [val]).map(value => EmailModel.parseEmailLine(value))
.flat(Infinity)
.map(item => (item.toLine ? [item.toLine(false), item] : [item, null]));
if (values.length) { if (values.length) {
values.forEach(a => { self._setChosen(values);
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; return true;
} }
} }
@ -235,7 +161,7 @@ export class EmailAddressesComponent {
_resizeInput() { _resizeInput() {
let input = this.input; let input = this.input;
if (input.clientWidth < input.scrollWidth) { if (input.clientWidth < input.scrollWidth) {
input.style.width = (input.scrollWidth + 20) + 'px'; input.style.width = Math.min(500, Math.max(200, input.scrollWidth)) + 'px';
} }
} }
@ -276,6 +202,56 @@ export class EmailAddressesComponent {
self._resizeInput(ev); self._resizeInput(ev);
} }
_setChosen(valArr) {
var self = this;
if (!isArray(valArr)){
return false;
}
valArr.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;
}
vv.value === v && (exists = true);
});
if (v !== '' && a && 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 (valArr.length === 1 && valArr[0] === '' && self._lastEdit !== '') {
self._lastEdit = '';
self._renderTags();
}
self._setValue(self._buildValue());
}
_buildValue() { _buildValue() {
return this._chosenValues.map(v => v.value).join(','); return this._chosenValues.map(v => v.value).join(',');
} }
@ -287,90 +263,79 @@ export class EmailAddressesComponent {
} }
} }
_simpleEmailMatch(value) {
// A very SIMPLE test to check if the value might be an email
const val = value.trim();
return /^[^@]*<[^\s@]{1,128}@[^\s@]{1,256}\.[\w]{2,32}>$/g.test(val)
|| /^[^\s@]{1,128}@[^\s@]{1,256}\.[\w]{2,32}$/g.test(val);
}
_renderTags() { _renderTags() {
let self = this; let self = this;
[...self.ul.children].forEach(node => node !== self.inputCont && node.remove()); [...self.ul.children].forEach(node => node !== self.inputCont && node.remove());
self._chosenValues.forEach(v => { self._chosenValues.forEach(v => {
if (v.obj) { if (v.obj) {
let li = createElement('li',{title:v.obj.toLine(),draggable:'true'}), let li = createElement('li',{title:v.obj.toLine(false, false, true),draggable:'true'}),
el = createElement('span'); el = createElement('span');
el.append(v.obj.toLine(true)); el.append(v.obj.toLine(true, false, true));
li.append(el); li.append(el);
el = createElement('a',{href:'#', class:'ficon'}); el = createElement('a',{href:'#', class:'ficon'});
el.append('✖'); el.append('✖');
addEventsListeners(el, { el.addEventListener('click', e => self._removeTag(e, li));
click: e => self._removeTag(e, li), el.addEventListener('focus', () => li.className = 'emailaddresses-selected');
focus: () => li.className = 'emailaddresses-selected', el.addEventListener('blur', () => li.className = null);
blur: () => li.className = null, el.addEventListener('keydown', e => {
keydown: e => { switch (e.key) {
switch (e.key) { case 'Delete':
case 'Delete': case 'Backspace':
case 'Backspace': self._removeTag(e, li);
self._removeTag(e, li); break;
break;
// 'e' - edit tag (removes tag and places value into visible input // 'e' - edit tag (removes tag and places value into visible input
case 'e': case 'e':
case 'Enter': case 'Enter':
self._editTag(e); self._editTag(e);
break; break;
case 'ArrowLeft': case 'ArrowLeft':
// select the previous tag or input if no more tags exist // select the previous tag or input if no more tags exist
var previous = el.closest('li').previousElementSibling; var previous = el.closest('li').previousElementSibling;
if (previous.matches('li')) { if (previous.matches('li')) {
previous.querySelector('a').focus(); previous.querySelector('a').focus();
} else { } else {
self.focus(); self.focus();
} }
break; break;
case 'ArrowRight': case 'ArrowRight':
// select the next tag or input if no more tags exist // select the next tag or input if no more tags exist
var next = el.closest('li').nextElementSibling; var next = el.closest('li').nextElementSibling;
if (next !== this.inputCont) { if (next !== this.inputCont) {
next.querySelector('a').focus(); next.querySelector('a').focus();
} else { } else {
this.focus(); this.focus();
} }
break; break;
case 'ArrowDown': case 'ArrowDown':
self._focus(e); self._focus(e);
break; break;
}
} }
}); });
li.append(el); li.append(el);
li.emailaddress = v; li.emailaddress = v;
addEventsListeners(li, { li.addEventListener("dragstart", e => {
dragstart: e => { dragAddress = {
dragAddress = { source: self,
source: self, li: li,
li: li, value: li.emailaddress.obj.toLine()
value: li.emailaddress.obj.toLine() };
}; // e.dataTransfer.setData(contentType, li.emailaddress.obj.toLine());
// e.dataTransfer.setData(contentType, li.emailaddress.obj.toLine()); e.dataTransfer.setData('text/plain', contentType);
e.dataTransfer.setData('text/plain', contentType); // e.dataTransfer.setDragImage(li, 0, 0);
// e.dataTransfer.setDragImage(li, 0, 0); e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.effectAllowed = 'move'; li.style.opacity = 0.25;
li.style.opacity = 0.25; });
}, li.addEventListener("dragend", () => {
dragend: () => { dragAddress = null;
dragAddress = null; li.style.cssText = '';
li.style.cssText = '';
}
}); });
self.inputCont.before(li); self.inputCont.before(li);

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,28 @@
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.disposable.push(
this.value.subscribe(value => this.triggerAnimation(value), this)
);
}
triggerAnimation(box) {
if (box) {
this.animationBox(true);
setTimeout(()=>this.animationCheckmark(true), 200);
} else {
this.animationCheckmark(true);
setTimeout(()=>this.animationBox(true), 200);
}
}
}

View file

@ -1,56 +1,20 @@
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.className = koComputable(() => this.optionsCaption = i18n(params.optionsCaption || null);
(size + ' settings-save-trigger-input ' + classForTrigger()).trim()
);
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
];
}
}

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