Compare commits

..

No commits in common. "master" and "v2.0.0-rc1" have entirely different histories.

3460 changed files with 194456 additions and 312964 deletions

4
.cmds
View file

@ -15,5 +15,5 @@ webpack --color --watch
#
gpg --import x
gpg --detach-sign --armor --openpgp -u 87DA4591 --passphrase ?? x
for ff in `ls ./build/dist/releases/**/**/*.zip | xargs`; do gpg --detach-sign --no-tty --yes --armor --openpgp -u 87DA4591 --passphrase ?? $ff; done
gpg --detach-sign --armor --openpgp -u 87DA4591 x
for ff in `ls *.zip`; do gpg --detach-sign --armor --openpgp -u 87DA4591 $ff; done

View file

@ -1,31 +0,0 @@
FROM php:7.4-fpm
RUN apt-get update
RUN apt-get install -y \
git unzip wget zip curl mlocate \
libicu-dev libpcre3-dev libicu-dev \
build-essential chrpath libssl-dev \
libxft-dev libfreetype6 libfreetype6-dev \
libpng-dev libjpeg62-turbo-dev \
libfontconfig1 libfontconfig1-dev libzip-dev libldap2-dev
RUN pecl install xxtea-1.0.11 && \
docker-php-ext-enable xxtea
RUN docker-php-ext-configure intl && \
docker-php-ext-configure ldap && \
docker-php-ext-configure gd --with-freetype=/usr/include/ --with-jpeg=/usr/include/ && \
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 --location --output /usr/local/bin/phpunit https://phar.phpunit.de/phpunit.phar && chmod +x /usr/local/bin/phpunit
RUN apt-get -y autoremove && apt-get clean
RUN sed -i '/^;catch_workers_output/ccatch_workers_output = yes' '/usr/local/etc/php-fpm.d/www.conf'
EXPOSE 9000
CMD ["php-fpm"]

View file

@ -1,8 +0,0 @@
date.timezone = UTC
upload_max_filesize = 24M
post_max_size = 25M
# log_errors = On
# display_errors = On
# error_reporting = E_ALL
# error_log = /dev/stderr

0
.docker/dev/mail/setup.sh → .docker/mail/setup.sh Normal file → Executable file
View file

View file

@ -1,3 +1,4 @@
server {
listen 80 default;
listen 443 ssl;

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

6
.docker/node/Dockerfile Normal file
View file

@ -0,0 +1,6 @@
FROM node:10.16.0-alpine
RUN apk add --no-cache git
RUN yarn global add gulp
CMD ["node", "--version"]

30
.docker/php/Dockerfile Normal file
View file

@ -0,0 +1,30 @@
FROM php:7.3-fpm
RUN apt-get update
RUN apt-get install -y \
git unzip wget zip curl mlocate \
libmcrypt-dev libicu-dev libpcre3-dev libicu-dev \
build-essential chrpath libssl-dev \
libxft-dev libfreetype6 libfreetype6-dev \
libpng-dev libjpeg62-turbo-dev \
libfontconfig1 libfontconfig1-dev libzip-dev
RUN pecl install mcrypt-1.0.2 && \
docker-php-ext-enable mcrypt
RUN docker-php-ext-configure intl && \
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
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
RUN curl --location --output /usr/local/bin/phpunit https://phar.phpunit.de/phpunit.phar && chmod +x /usr/local/bin/phpunit
RUN apt-get -y autoremove && apt-get clean
RUN sed -i '/^;catch_workers_output/ccatch_workers_output = yes' '/usr/local/etc/php-fpm.d/www.conf'
EXPOSE 9000
CMD ["php-fpm"]

8
.docker/php/rainloop.ini Normal file
View file

@ -0,0 +1,8 @@
date.timezone = UTC
upload_max_filesize = 1G
post_max_size = 1G
# log_errors = On
# display_errors = On
# error_reporting = E_ALL
# error_log = /dev/stderr

View file

@ -1,192 +0,0 @@
# 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
FROM php:8.5-fpm-alpine AS final
LABEL org.label-schema.description="SnappyMail webmail client image using nginx, php-fpm on Alpine"
# Install dependencies such as nginx
RUN apk add --no-cache ca-certificates nginx supervisor bash
# Install PHP extensions
# apcu
RUN set -eux; \
apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
pecl install apcu; \
docker-php-ext-enable apcu; \
docker-php-source delete; \
apk del .build-dependencies;
# gd
RUN set -eux; \
apk add --no-cache freetype libjpeg-turbo libpng; \
apk add --no-cache --virtual .deps freetype-dev libjpeg-turbo-dev libpng-dev; \
docker-php-ext-configure gd --with-freetype --with-jpeg; \
docker-php-ext-install gd; \
apk del .deps
# gmagick
# RUN set -eux; \
# apk add --no-cache graphicsmagick libgomp; \
# apk add --no-cache --virtual .deps graphicsmagick-dev libtool; \
# apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
# pecl install gmagick-2.0.6RC1; \
# docker-php-ext-enable gmagick; \
# docker-php-source delete; \
# apk del .build-dependencies; \
# apk del .deps
# gnupg
RUN set -eux; \
apk add --no-cache gnupg gpgme; \
apk add --no-cache --virtual .deps gpgme-dev; \
apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
pecl install gnupg; \
docker-php-ext-enable gnupg; \
docker-php-source delete; \
apk del .build-dependencies; \
apk del .deps
# imagick
RUN set -eux; \
apk add --no-cache imagemagick libgomp; \
apk add --no-cache --virtual .deps imagemagick-dev; \
apk add --no-cache --virtual .build-dependencies $PHPIZE_DEPS; \
echo | pecl install imagick; \
docker-php-ext-enable imagick; \
docker-php-source delete; \
apk del .build-dependencies; \
apk del .deps
# intl
RUN set -eux; \
apk add --no-cache icu-libs; \
apk add --no-cache --virtual .deps icu-dev; \
docker-php-ext-configure intl; \
docker-php-ext-install intl; \
apk del .deps
# ldap
RUN set -eux; \
apk add --no-cache libldap; \
apk add --no-cache --virtual .deps openldap-dev; \
docker-php-ext-configure ldap; \
docker-php-ext-install ldap; \
apk del .deps
# mysql
RUN docker-php-ext-install pdo_mysql
# 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
# The 'www-data' user/group in alpine is 82:82. The 'nginx' user/group in alpine is 101:101, and is part of www-data group
COPY --chown=www-data:www-data --from=builder /snappymail /snappymail
# Use a custom snappymail data folder
RUN mv -v /snappymail/data /var/lib/snappymail;
# Setup configs
COPY --chown=root:root .docker/release/files /
RUN set -eux; \
chown www-data:www-data /snappymail/include.php; \
chmod 440 /snappymail/include.php; \
chmod +x /entrypoint.sh; \
# Disable the built-in php-fpm configs, since we're using our own config
mv -v /usr/local/etc/php-fpm.d/docker.conf /usr/local/etc/php-fpm.d/docker.conf.disabled; \
mv -v /usr/local/etc/php-fpm.d/www.conf /usr/local/etc/php-fpm.d/www.conf.disabled; \
mv -v /usr/local/etc/php-fpm.d/zz-docker.conf /usr/local/etc/php-fpm.d/zz-docker.conf.disabled;
USER root
WORKDIR /snappymail
VOLUME /var/lib/snappymail
EXPOSE 8888
EXPOSE 9000
ENTRYPOINT []
CMD ["/entrypoint.sh"]

View file

@ -1,86 +0,0 @@
#!/bin/sh
set -eu
DEBUG=${DEBUG:-}
if [ "$DEBUG" = 'true' ]; then
set -x
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
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
# Secure cookies
if [ "${SECURE_COOKIES}" = 'true' ]; then
echo "[INFO] Secure cookies activated"
{
echo 'session.cookie_httponly = On';
echo 'session.cookie_secure = On';
echo 'session.use_only_cookies = On';
} > /usr/local/etc/php/conf.d/cookies.ini;
fi
echo "[INFO] Snappymail version: $( ls /snappymail/snappymail/v )"
# 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
echo "[INFO] Creating default Snappymail configuration: $SNAPPYMAIL_CONFIG_FILE"
# Run snappymail and exit. This populates the snappymail data directory and generates the config file
# On error, print php exception and exit
EXITCODE=
su - www-data -s /bin/sh -c 'php /snappymail/index.php' > /tmp/out || EXITCODE=$?
if [ -n "$EXITCODE" ]; then
cat /tmp/out
exit "$EXITCODE"
fi
fi
echo "[INFO] Overriding values in snappymail configuration: $SNAPPYMAIL_CONFIG_FILE"
# Enable output of snappymail logs
sed '/^\; Enable logging/{
N
s/enable = Off/enable = On/
}' -i $SNAPPYMAIL_CONFIG_FILE
# Redirect snappymail logs to stderr /stdout
sed 's/^filename = .*/filename = "stderr"/' -i $SNAPPYMAIL_CONFIG_FILE
sed 's/^write_on_error_only = .*/write_on_error_only = Off/' -i $SNAPPYMAIL_CONFIG_FILE
sed 's/^write_on_php_error_only = .*/write_on_php_error_only = On/' -i $SNAPPYMAIL_CONFIG_FILE
# Always enable snappymail Auth logging
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_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
(
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
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
# Create snappymail admin password if absent
SNAPPYMAIL_ADMIN_PASSWORD_FILE=/var/lib/snappymail/_data_/_default_/admin_password.txt
if [ ! -f "$SNAPPYMAIL_ADMIN_PASSWORD_FILE" ]; then
echo "[INFO] Creating Snappymail admin password file: $SNAPPYMAIL_ADMIN_PASSWORD_FILE"
wget -T 1 -qO- 'http://127.0.0.1:8888/?/AdminAppData/0/12345/' > /dev/null
echo "[INFO] Snappymail Admin Panel ready at http://localhost:8888/?admin. Login using password in $SNAPPYMAIL_ADMIN_PASSWORD_FILE"
fi
wget -T 1 -qO- 'http://127.0.0.1:8888/' > /dev/null
echo "[INFO] Snappymail ready at http://localhost:8888/"
) &
# RUN !
exec /usr/bin/supervisord -c /supervisor.conf --pidfile /run/supervisord.pid

View file

@ -1,108 +0,0 @@
worker_processes auto;
pid /tmp/nginx.pid;
events {
worker_connections 1024;
use epoll;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log off;
error_log /dev/stderr error;
sendfile on;
keepalive_timeout 15;
keepalive_disable msie6;
keepalive_requests 100;
tcp_nopush on;
tcp_nodelay on;
server_tokens off;
fastcgi_temp_path /tmp/fastcgi 1 2;
client_body_temp_path /tmp/client_body 1 2;
proxy_temp_path /tmp/proxy 1 2;
uwsgi_temp_path /tmp/uwsgi 1 2;
scgi_temp_path /tmp/scgi 1 2;
gzip on;
gzip_comp_level 5;
gzip_min_length 512;
gzip_buffers 4 8k;
gzip_proxied any;
gzip_vary on;
gzip_disable "msie6";
gzip_types
text/css
text/javascript
text/xml
text/plain
text/x-component
application/javascript
application/x-javascript
application/json
application/xml
application/rss+xml
application/vnd.ms-fontobject
font/truetype
font/opentype
image/svg+xml;
server {
listen 8888;
listen [::]:8888;
#listen [::]:8888 ipv6only=off;
root /snappymail;
index index.php;
charset utf-8;
client_max_body_size <UPLOAD_MAX_SIZE>;
location ^~ /data {
deny all;
}
location / {
try_files $uri $uri/ index.php;
}
# Assets cache control
# --------------------------------------
location ~* \.(?:html|xml|json)$ {
expires -1;
}
location ~* \.(?:css|js)$ {
expires 7d;
add_header Pragma public;
add_header Cache-Control "public";
}
location ~* \.(?:gif|jpe?g|png|ico|otf|eot|svg|ttf|woff|woff2)$ {
expires 30d;
log_not_found off;
add_header Pragma public;
add_header Cache-Control "public";
}
# PHP Backend
# --------------------------------------
location ~* \.php$ {
try_files $uri =404;
include fastcgi_params;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param HTTP_PROXY "";
fastcgi_index index.php;
fastcgi_pass 127.0.0.1:9000;
fastcgi_intercept_errors on;
fastcgi_request_buffering off;
fastcgi_param REMOTE_ADDR $http_x_real_ip;
}
}
}

View file

@ -1,23 +0,0 @@
<?php
$stdIn = STDIN;
$stdOut = STDOUT;
fwrite($stdOut, "READY\n");
while(true) {
if (false == $line = trim(fgets($stdIn))) {
continue;
}
$match = null;
if (preg_match('/eventname:(.*?) /', $line, $match)) {
if (in_array($match[1], ['PROCESS_STATE_EXITED', 'PROCESS_STATE_STOPPED', 'PROCESS_STATE_FATAL'])) {
exec('kill -15 '.file_get_contents('/run/supervisord.pid'));
}
}
fwrite($stdOut, "RESULT 2\nOK");
sleep(1);
fwrite($stdOut, "READY\n");
}

View file

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

View file

@ -1,40 +0,0 @@
[supervisord]
nodaemon=true
user=root
logfile=/dev/null
logfile_maxbytes=0
[program:nginx]
command=nginx -c /etc/nginx/nginx.conf -g 'daemon off;'
process_name=%(program_name)s_%(process_num)02d
user=root
numprocs=1
autostart=true
autorestart=false
startsecs=0
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:php-fpm]
command=php-fpm -F
process_name=%(program_name)s_%(process_num)02d
user=root
numprocs=1
autostart=true
autorestart=false
startsecs=0
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[eventlistener:subprocess-stopped]
command=php /listener.php
process_name=%(program_name)s_%(process_num)02d
user=root
numprocs=1
events=PROCESS_STATE_EXITED,PROCESS_STATE_STOPPED,PROCESS_STATE_FATAL
autostart=true
autorestart=unexpected

View file

@ -1,25 +0,0 @@
[global]
daemonize = no
error_log = /dev/stderr
log_buffering = no
[default]
listen = 9000
user = www-data
listen.owner = www-data
listen.group = www-data
pm = ondemand
pm.max_children = 30
pm.process_idle_timeout = 10s
pm.max_requests = 500
catch_workers_output = yes
decorate_workers_output = no
chdir = /
pm.status_path = /status
php_admin_value[log_errors] = On
php_admin_value[expose_php] = Off
php_admin_value[display_errors] = Off
php_admin_value[date.timezone] = UTC
php_admin_value[post_max_size] = <UPLOAD_MAX_SIZE>
php_admin_value[upload_max_filesize] = <UPLOAD_MAX_SIZE>
php_admin_value[memory_limit] = <MEMORY_LIMIT>

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

5
.docker/tx/Dockerfile Normal file
View file

@ -0,0 +1,5 @@
FROM python:3.6-alpine
RUN pip install transifex-client
CMD ["tx", "--version"]

View file

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

View file

@ -1,2 +0,0 @@
[*.php]
indent_style = tab

View file

@ -2,14 +2,15 @@ module.exports = {
parser: 'babel-eslint',
// extends: ['eslint:recommended', 'plugin:prettier/recommended'],
extends: ['eslint:recommended'],
plugins: ['prettier'],
parserOptions: {
ecmaVersion: 11,
ecmaVersion: 6,
sourceType: 'module'
},
env: {
node: true,
browser: true,
es2020: true
es6: true
},
globals: {
// SnappyMail
@ -26,27 +27,22 @@ module.exports = {
'Squire': "readonly",
'SquireUI': "readonly",
// node_modules/knockout but dev/External/ko.js is used
'ko': "readonly",
// 'ko': "readonly",
// vendors/routes/
'hasher': "readonly",
'signals': "readonly",
'Crossroads': "readonly",
// vendors/jua
'Jua': "readonly",
// vendors/qr.js
'qr': "readonly",
// vendors/bootstrap/bootstrap.native.js
'BSN': "readonly",
// Mailvelope
'mailvelope': "readonly",
// Punycode
'IDN': "readonly",
// Turndown
'TurndownService': "readonly",
// Marked
'marked': "readonly"
'BSN': "readonly"
},
// http://eslint.org/docs/rules/
rules: {
'no-cond-assign': 0,
// plugins
// 'prettier/prettier': 'error',
'no-mixed-spaces-and-tabs': 'off',
'max-len': [
'error',
@ -58,7 +54,6 @@ module.exports = {
ignoreTrailingComments: true,
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
community_bridge: SnappyMail
custom: ["https://www.paypal.me/thedjmaze", "https://snappymail.eu"]

View file

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

View file

@ -1,38 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Please complete the following information:**
- Browser: [e.g. chromium 100, firefox 100, safari 14, mobile]
- IMAP daemon: [e.g. courier, dovecot]
- PHP version:
- SnappyMail Version:
- Mode: [e.g. standalone, nextcloud, cyberpanel, docker]
**Debug/logging information**
[Read here how to log](https://github.com/the-djmaze/snappymail/wiki/FAQ#how-do-i-enable-logging)
- [ ] I've placed them here (few lines) or as attachments (many lines)
**Additional context**
Add any other context about the problem here.

View file

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View file

@ -1,93 +0,0 @@
name: docker-pr
on:
pull_request:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
# This step generates the docker tags
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
env:
# This env var ensures {{sha}} is a real commit SHA for type=ref,event=pr
DOCKER_METADATA_PR_HEAD_SHA: 'true'
with:
images: |
djmaze/snappymail
ghcr.io/${{ github.repository }}
# type=ref,event=pr generates tag(s) on PRs only. E.g. 'pr-123', 'pr-123-abc0123'
tags: |
type=ref,event=pr
type=ref,suffix=-{{sha}},event=pr
# The rest of the org.opencontainers.image.xxx labels are dynamically generated
labels: |
org.opencontainers.image.description=SnappyMail
org.opencontainers.image.licenses=AGPLv3
# See: https://github.com/docker/build-push-action/blob/v2.6.1/docs/advanced/cache.md#github-cache
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
- name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
# See: https://github.com/docker/buildx/issues/59
- name: Build
id: build
uses: docker/build-push-action@v3
with:
context: '.'
file: ./.docker/release/Dockerfile
platforms: linux/amd64
push: false
load: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
- name: Docker images
run: |
docker images
- name: Test
run: |
TAG=$( echo "${{ steps.meta.outputs.tags }}" | head -n1 )
.docker/release/test/test.sh "$TAG"
- name: Build all archs
uses: docker/build-push-action@v3
with:
context: '.'
file: ./.docker/release/Dockerfile
platforms: linux/386,linux/amd64,linux/arm64
push: false
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
# Temp fix
# https://github.com/docker/build-push-action/issues/252
# https://github.com/moby/buildkit/issues/1896
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache

View file

@ -1,117 +0,0 @@
name: docker
on:
push:
tags:
- 'v2.*'
# This is needed to push to GitHub Container Registry. See https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry
permissions:
contents: read
packages: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
# This step generates the docker tags
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
env:
# This env var ensures {{sha}} is a real commit SHA for type=ref,event=pr
DOCKER_METADATA_PR_HEAD_SHA: 'true'
with:
images: |
djmaze/snappymail
ghcr.io/${{ github.repository }}
# type=ref,event=branch generates tag(s) on branch only. E.g. 'master', 'master-abc0123'
# type=ref,event=tag generates tag(s) on tags only. E.g. 'v0.0.0', 'v0.0.0-abc0123', and 'latest'
tags: |
type=ref,event=branch
type=ref,event=tag
# The rest of the org.opencontainers.image.xxx labels are dynamically generated
labels: |
org.opencontainers.image.description=SnappyMail
org.opencontainers.image.licenses=AGPLv3
# See: https://github.com/docker/build-push-action/blob/v2.6.1/docs/advanced/cache.md#github-cache
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
- name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Login to Docker Hub registry
if: startsWith(github.ref, 'refs/tags/') # Login only on tags
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if: startsWith(github.ref, 'refs/tags/') # Login only on tags
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# See: https://github.com/docker/buildx/issues/59
- name: Build
id: build
uses: docker/build-push-action@v3
with:
context: '.'
file: ./.docker/release/Dockerfile
platforms: linux/amd64
push: false
load: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
- name: Docker images
run: |
docker images
- name: Test
run: |
TAG=$( echo "${{ steps.meta.outputs.tags }}" | head -n1 )
.docker/release/test/test.sh "$TAG"
- name: Build and push
id: build-and-push
uses: docker/build-push-action@v3
with:
context: '.'
file: ./.docker/release/Dockerfile
# TODO: Add more arches?
# platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x
platforms: linux/386,linux/amd64,linux/arm64
push: ${{ startsWith(github.ref, 'refs/tags/') }} # Push only on tags
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
# Temp fix
# https://github.com/docker/build-push-action/issues/252
# https://github.com/moby/buildkit/issues/1896
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache

39
.gitignore vendored
View file

@ -1,32 +1,27 @@
*.bak
*.old
/.idea
/.vscode
/api.php
/*.log
/snappymail/v/0.0.0/static/*
/error.log
/nbproject
/npm-debug.log
/yarn-error.log
/rainloop.sublime-project
/rainloop.sublime-workspace
/rainloop/v/0.0.0/static/*
/rainloop/v/0.0.0/app/localization/moment/*
!/rainloop/v/0.0.0/app/localization/moment/.gitempty
/vendors/.*
/node_modules
/build/local
/build/dist
/build/tmp
/build/docker
/.docker/dev/.cache
/.docker/dev/mail/config
/.docker/dev/nginx/ssl/*
!/.docker/dev/nginx/ssl/.gitempty
/.docker/release/snappymail-*.zip
/.docker/.cache
/.docker/mail/config
/.docker/nginx/ssl/*
!/.docker/nginx/ssl/.gitempty
/dist
/data
.DS_Store
/MULTIPLY
/include.php
/issues
.idea/
.env
/test
/public_html
/vendors/knockout/spec
/vendors/openpgp-5
!/vendors/openpgp-5/dist
/vendors/squire2
!/vendors/squire2/dist
/vendors/vanillaqr.js/
/integrations/nextcloud/rainloop
/integrations/owncloud/rainloop

7
.gitmodules vendored
View file

@ -1,7 +0,0 @@
[submodule "vendors/openpgp-5"]
path = vendors/openpgp-5
url = git@github.com:the-djmaze/openpgpjs.git
[submodule "vendors/squire2"]
path = vendors/squire2
url = git@github.com:the-djmaze/Squire.git
branch = snappymail

View file

@ -1,16 +1,6 @@
<FilesMatch "index\.php">
AcceptPathInfo On
</FilesMatch>
<IfModule mod_rewrite.c>
RewriteEngine On
# Redirect cPanel
RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.+)$ index.php/$1 [L,QSA]
</IfModule>
RewriteEngine On
# Redirect cPanel
RewriteRule cpsess.* https://%{HTTP_HOST}/ [L,R=301]
<IfModule mod_expires.c>
ExpiresActive On
@ -24,21 +14,55 @@
ExpiresByType image/webp A15768000
ExpiresByType image/vnd.microsoft.icon A15768000
ExpiresByType font/woff A15768000
ExpiresByType font/woff2 A15768000
</IfModule>
<IfModule mod_headers.c>
# 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 Referrer-Policy "no-referrer"
# Header set Strict-Transport-Security "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 Referrer-Policy "no-referrer"
Header set Strict-Transport-Security "max-age=31536000"
Header set imagetoolbar "no"
# Header set X-Content-Type-Options "nosniff"
# Header set X-XSS-Protection "1; mode=block"
Header set Service-Worker-Allowed "/"
Header set X-Content-Type-Options "nosniff"
Header set X-Frame-Options "DENY"
Header set X-XSS-Protection "1; mode=block"
</IfModule>
# Google FLoC
# Header set Permissions-Policy "interest-cohort=()"
<IfModule mod_headers.c>
RewriteCond %{HTTP:Accept-encoding} br
RewriteCond "%{REQUEST_FILENAME}\.br" -s
RewriteRule "^(.+)" "$1\.br" [L,T=text/javascript,QSA]
RewriteCond %{HTTP:Accept-encoding} gzip
RewriteCond "%{REQUEST_FILENAME}\.gz" -s
RewriteRule "^(.+)" "$1\.gz" [L,T=text/javascript,QSA]
RewriteCond %{HTTP:Accept-encoding} br
RewriteCond "%{REQUEST_FILENAME}\.br" -s
RewriteRule "^(.+)" "$1\.br" [L,T=text/css,QSA]
RewriteCond %{HTTP:Accept-encoding} gzip
RewriteCond "%{REQUEST_FILENAME}\.gz" -s
RewriteRule "^(.+)" "$1\.gz" [L,T=text/css,QSA]
<FilesMatch "(\.js\.br|\.css\.br)$">
SetEnv no-gzip 1
SetEnv no-brotli 1
# Serve correct encoding type.
Header append Content-Encoding br
# Force proxies to cache brotli &
# non-brotli css/js files separately.
Header append Vary Accept-Encoding
</FilesMatch>
<FilesMatch "(\.js\.gz|\.css\.gz)$">
SetEnv no-gzip 1
SetEnv no-brotli 1
# Serve correct encoding type.
Header append Content-Encoding gzip
# Force proxies to cache gzipped &
# non-gzipped css/js files separately.
Header append Vary Accept-Encoding
</FilesMatch>
</IfModule>
#<IfModule mod_brotli.c>

15
.tx/config Normal file
View file

@ -0,0 +1,15 @@
[main]
host = https://www.transifex.com
minimum_perc = 60
type = YAML
[rainloop-webmail.rainloop-webmail]
file_filter = rainloop/v/0.0.0/app/localization/webmail/<lang>.yml
source_file = rainloop/v/0.0.0/app/localization/webmail/_source.en.yml
source_lang = en
[rainloop-webmail.rainloop-admin]
file_filter = rainloop/v/0.0.0/app/localization/admin/<lang>.yml
source_file = rainloop/v/0.0.0/app/localization/admin/_source.en.yml
source_lang = en

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
**Thanks for contributing to SnappyMail Webmail!**
**Thanks for contributing to RainLoop Webmail!**
1. Fork the repo, do work in a feature branch.
2. Issue a pull request.
@ -7,15 +7,14 @@
**Getting started**
1. Install PHP
2. Install node.js - `https://nodejs.org/download/`
3. Install yarn - `https://yarnpkg.com/en/docs/install`
4. Install gulp - `npm install gulp -g`
5. Fork snappymail from https://github.com/the-djmaze/snappymail
6. Clone snappymail - `git clone git@github.com:USERNAME/snappymail.git snappymail`
7. `cd snappymail`
8. Install all dependencies - `yarn install`
9. Run gulp - `gulp`
1. Install node.js - `https://nodejs.org/download/`
2. Install yarn - `https://yarnpkg.com/en/docs/install`
3. Install gulp - `npm install gulp -g`
4. Fork rainloop - `https://github.com/RainLoop/rainloop-webmail/issues/new#fork-destination-box`
5. Clone rainloop - `git clone git@github.com:USERNAME/rainloop-webmail.git rainloop`
6. `cd rainloop`
7. Install install all dependencies - `yarn install`
8. Run gulp - `gulp`
---
@ -29,19 +28,8 @@
**Editing HTML Template Files**
1. Edit data/\_data_/\_default_/configs/application.ini
2. Set `[cache] system_data` to Off
**Release**
1. Install gzip
2. Install brotli
3. php release.php
Options:
* `php release.php --aur` = Build Arch Linux package
* `php release.php --docker` = Build Docker instance
* `php release.php --plugins` = Build plugins
2. Set 'cache_system_data' to Off
---
If you have any questions, open an issue or email support@snappymail.eu.
If you have any questions, open an issue or email support@rainloop.net.

View file

@ -658,4 +658,4 @@ specific requirements.
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.
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

@ -1,73 +1,75 @@
#!make
rebuild: _down
docker compose build --no-cache
docker-compose build --no-cache
up: _up status
_up:
docker compose up -d
docker-compose up -d
stop: _stop status
_stop:
docker compose stop
docker-compose stop
down: _down status
_down:
docker compose down
docker-compose down
restart: _stop _up status
status:
@docker compose ps
@docker-compose ps
tx:
@docker compose run --no-deps --rm tx tx pull -a -s -f -d
@docker-compose run --no-deps --rm tx tx pull -a -s -f -d
console-node:
@docker compose run --no-deps --rm node sh
@docker-compose run --no-deps --rm node sh
console-tx:
@docker compose run --no-deps --rm tx sh
@docker-compose run --no-deps --rm tx sh
console-php:
@docker compose exec php sh
@docker-compose exec php sh
console: console-node
logs:
@docker compose logs --tail=100 -f
@docker-compose logs --tail=100 -f
logs-db:
@docker compose logs --tail=100 -f db
@docker-compose logs --tail=100 -f db
logs-php:
@docker compose logs --tail=100 -f php
@docker-compose logs --tail=100 -f php
logs-node:
@docker compose logs --tail=100 -f node
@docker-compose logs --tail=100 -f node
logs-nginx:
@docker compose logs --tail=100 -f nginx
@docker-compose logs --tail=100 -f nginx
logs-mail:
@docker compose logs --tail=100 -f mail
@docker-compose logs --tail=100 -f mail
logs-tx:
@docker compose logs --tail=100 -f tx
@docker-compose logs --tail=100 -f tx
rl-lint:
@docker compose run --no-deps --rm node gulp lint
@docker-compose run --no-deps --rm node gulp lint
rl-dev:
@docker compose run --no-deps --rm node npm run watch-js
@docker-compose run --no-deps --rm node npm run watch-js
rl-compile:
@docker compose run --no-deps --rm node gulp build
@docker-compose run --no-deps --rm node gulp build
rl-compile-with-source:
@docker compose run --no-deps --rm node gulp build --source
@docker-compose run --no-deps --rm node gulp build --source
rl-watch-css:
@docker compose run --no-deps --rm node npm run watch-css
@docker-compose run --no-deps --rm node npm run watch-css
rl-watch-js:
@docker compose run --no-deps --rm node npm run watch-js
@docker-compose run --no-deps --rm node npm run watch-js
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:
@docker compose run --no-deps --rm node yarn install
@docker-compose run --no-deps --rm node yarn install
yarn-outdated:
@docker compose run --no-deps --rm node yarn outdated
@docker-compose run --no-deps --rm node yarn outdated
yarn-upgrade:
@docker compose run --no-deps --rm node yarn upgrade-interactive --exact --latest
@docker-compose run --no-deps --rm node yarn upgrade-interactive --exact --latest
gpg:
docker run -it --rm -w=/var/www \

187
README.md
View file

@ -1,14 +1,10 @@
<div align="center">
<a href="https://github.com/the-djmaze/snappymail">
<img src="https://snappymail.eu/static/img/logo-256x256-white.png">
<img width="200" heigth="200" src="https://snappymail.eu/static/img/logo-256x256.png">
</a>
<br>
<h1>SnappyMail</h1>
<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>
Simple, modern, lightweight &amp; fast web-based email client.
</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).
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
@ -38,87 +34,58 @@ And don't forget to read the whole [Wiki](https://github.com/the-djmaze/snappyma
**GNU AFFERO GENERAL PUBLIC LICENSE Version 3 (AGPL)**.
http://www.gnu.org/licenses/agpl-3.0.html
Copyright (c) 2020 - 2024 SnappyMail
Copyright (c) 2013 - 2022 RainLoop
Copyright (c) 2020 SnappyMail
## Modifications
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
* Auth failed attempts written to syslog
* Added Fail2ban instructions
* ES2020
* PHP 7.4+ required
* ES2015
* PHP 7.3+ required
* PHP mbstring extension required
* PHP replaced pclZip with PharData and ZipArchive
* Dark mode
* Added option to remove background/font colors from messages for real "dark mode"
* PHP replaced pclZip with ZipArchive
* PHP yaml extension else use the old Spyc
* Removed BackwardCapability (class \RainLoop\Account)
* Removed ChangePassword (re-implemented as plugin)
* Removed ChangePassword (plugins won't work)
* Removed OAuth support
* Removed POP3 support
* Removed background video support
* Removed Sentry (Application Monitoring and Error Tracking Software)
* Removed Spyc yaml
* Removed OwnCloud
* Replaced gulp-uglify with gulp-terser
* CRLF => LF line endings
* Embed boot.js and boot.css into index.html
* 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
* Updated [Sabre/VObject](https://github.com/sabre-io/vobject)
* Ongoing removal of old JavaScript code (things are native these days)
* Added modified [Squire](https://github.com/neilj/Squire) HTML editor as replacement for CKEditor
* Split Admin specific JavaScript code from User code
* Split Sieve specific JavaScript code from User code
* JSON reviver
* Better memory garbage collection management
* Added serviceworker for Notifications
* Added advanced Sieve scripts editor
* Slimmed down language files
* Replaced webpack with rollup
* No user-agent detection (use device width)
* Added support to load plugins as .phar
* Replaced old Sabre library
* AddressBook Contacts support MySQL/MariaDB utf8mb4
* Added [Fetch Metadata Request Headers](https://www.w3.org/TR/fetch-metadata/) checks
* Reduced excessive DOM size
* Support [Kolab groupware](https://kolab.org/)
* Support many more [IMAP RFC's](https://snappymail.eu/comparison#IMAP)
* Support Sodium and OpenSSL for encryption
* Much better PGP support
### Supported browsers
This fork uses downsized/simplified versions of scripts and has no support for Internet Explorer nor Edge Legacy.
Supported are:
* Chrome 80+
* Edge 80+
* Firefox 78+
* Opera 67+
* Safari 13.1+
### Removal of old JavaScript
This fork uses downsized/simplified versions of scripts and has no support for Internet Explorer.
The result is faster and smaller download code (good for mobile networks).
* Added dev/prototype.js for some additional features
Things might work in Edge 18, Firefox 50-62 and Chrome 54-68 due to one polyfill for array.flat().
* Added dev/prototype-*.js for some additional features
* boot.js without webpack overhead
* Modified Jua.js to be without jQuery
* Replaced ProgressJS with simple native dropin
* 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 bootstrap.js with native drop-in replacement
* Replaced dev/Common/ClientStorageDriver/* with Web Storage Objects polyfill
* Replaced *Ajax with *Fetch classes because we use the Fetch API, not jQuery.ajax
* Replaced [knockoutjs](https://github.com/knockout/knockout) 3.4 with a modified 3.5.1
* Replaced knockoutjs 3.4 with a modified 3.5.1
* Replaced knockout-sortable with native HTML5 drag&drop
* Replaced simplestatemanager with CSS @media
* Replaced inputosaurus with own code
* Replaced simplestatemanager with @media
* Replaced inputosaurus to native
* Replaced keymaster with own shortcuts handler
* Replaced OpenPGP.js v2 with OpenPGP.js v5
* Removed ifvisible.js
* Removed pikaday
* Removed underscore
* Removed polyfills
@ -130,97 +97,79 @@ The result is faster and smaller download code (good for mobile networks).
* Removed jquery-scrollstop
* Removed jquery-mousewheel
* Removed matchmedia-polyfill
* Removed momentjs (use Intl)
* Removed momentjs (localization still used)
* Removed opentip (use CSS)
* Removed non-community (aka Prem/Premium/License) code
* Removed ProgressJS
|js/* |1.14.0 |native |
|----------- |--------: |--------: |
|admin.js |2.130.942 | 844.753 |
|app.js |4.184.455 |2.487.401 |
|boot.js | 671.522 | 5.285 |
|libs.js | 647.614 | 246.183 |
|polyfills.js | 325.834 | 0 |
|TOTAL |7.960.367 |3.583.524 |
RainLoop 1.17 vs SnappyMail
|js/min/* |1.14.0 |native |gzip 1.14 |gzip |brotli |
|--------------- |--------: |--------: |--------: |--------: |--------: |
|admin.min.js | 252.147 | 116.068 | 73.657 | 33.279 | 28.711 |
|app.min.js | 511.202 | 339.263 |140.462 | 89.051 | 72.194 |
|boot.min.js | 66.007 | 2.935 | 22.567 | 1.510 | 1.285 |
|libs.min.js | 572.545 | 147.113 |176.720 | 51.608 | 45.901 |
|polyfills.min.js | 32.452 | 0 | 11.312 | 0 | 0 |
|TOTAL |1.434.353 | 605.379 |424.718 |175.448 |148.091 |
|TOTAL (no admin) |1.182.206 | 489.311 |351.061 |142.169 |119.380 |
|js/* |RainLoop |Snappy |
|--------------- |--------: |--------: |
|admin.js |2.170.153 | 84.925 |
|app.js |4.207.787 | 447.263 |
|boot.js | 868.735 | 4.343 |
|libs.js | 658.812 | 233.728 |
|sieve.js | 0 | 91.418 |
|polyfills.js | 334.608 | 0 |
|serviceworker.js | 0 | 285 |
|TOTAL |8.240.095 | 861.962 |
|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 |
|app.min.js | 515.367 | 202.101 |139.456 | 68.505 |110.485 | 58.481 |
|boot.min.js | 84.659 | 2.231 | 26.998 | 1.271 | 23.643 | 1.067 |
|libs.min.js | 584.772 | 110.646 |180.901 | 39.518 |155.182 | 35.207 |
|sieve.min.js | 0 | 45.504 | 0 | 11.131 | 0 | 9.917 |
|polyfills.min.js | 32.837 | 0 | 11.406 | 0 | 10.175 | 0 |
|TOTAL user |1.217.635 | 314.978 |358.761 |109.294 |299.485 | 94.755 |
|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 58% smaller and faster than traditional RainLoop.
### CSS changes
* Solve jQuery removed "features" with native css code
* Themes work in mobile mode
* Bugfix invalid/conflicting css rules
* Use flexbox
* Use border-box
* Split app.css to have separate admin.css
* Remove oldschool 'float'
* Remove unused css
* Solve jQuery removed "features" with native css code.
* Removed html.no-css
* Removed dev/Styles/Cmd.less
* Removed dev/Styles/Scroll.less
* Removed Internet Explorer from normalize.css
* Removed node_modules/opentip/css/opentip.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-letterfx/jquery-letterfx.min.css
* Removed vendors/Progress.js/minified/progressjs.min.css
* Removed gulp-autoprefixer
|css/* |RainLoop |Snappy |RL gzip |SM gzip |SM brotli |
|------------ |-------: |------: |------: |------: |--------: |
|app.css | 340.331 | 85.073 | 46.946 | 17.792 | 15.210 |
|app.min.css | 274.947 | 68.272 | 39.647 | 15.615 | 13.636 |
|boot.css | | 1.326 | | 664 | 545 |
|boot.min.css | | 1.071 | | 590 | 474 |
|admin.css | | 30.880 | | 7.045 | 6.127 |
|admin.min.css | | 24.959 | | 6.368 | 5.615 |
### PGP
RainLoop uses the old OpenPGP.js v2
SnappyMail v2.12 uses OpenPGP.js v5, GnuPG and Mailvelope.
SnappyMail is able to use and generate ECDSA and EDDSA keys, where RainLoop does not.
Since SnappyMail tries to achieve the best mobile experience, it forked OpenPGP.js to strip it down.
* remove all unused Node.js
* remove all old browsers support
See https://github.com/the-djmaze/openpgpjs for development
|OpenPGP |RainLoop |Snappy |RL gzip |SM gzip |RL brotli |SM brotli |
|--------------- |--------: |--------: |------: |-------: |--------: |--------: |
|openpgp.min.js | 330.742 | 546.165 |102.388 | 169.207 | 84.241 | 138.688 |
|openpgp.worker | 1.499 | | 824 | | 695 | |
|css/* |1.14.0 |native |gzip 1.14 |gzip |brotli |
|-------------- |-------: |-------: |------: |------: |------: |
|app.css | 340.334 | 190.086 | 46,959 | 29.415 | 24.717 |
|app.min.css | 274.791 | 156.076 | 39.618 | 26.034 | 22.418 |
|boot.css | | 2.534 | | 837 | 668 |
|boot.min.css | | 2.055 | | 732 | 560 |
### Squire vs CKEditor
The [Squire](https://github.com/neilj/Squire) implementation is not 100% compatible yet, but it shows the massive overhead of CKEditor.
The [Squire](https://github.com/neilj/Squire) implementation is not 100% compatible yet, but is shows the massive overhead of CKEditor.
Still TODO:
* support for tables (really needed?!?)
* support BIDI (really needed?!?)
| | normal | min | gzip | min gzip |
|-------- |-------: |-------: |------: |--------: |
|squire | 122.321 | 41.906 | 31.867 | 14.330 |
|squire | 128.826 | 47.074 | 33.671 | 15.596 |
|ckeditor | ? | 520.035 | ? | 155.916 |
CKEditor including the 7 asset requests (css,language,plugins,icons) is 633.46 KB / 180.47 KB (gzip).
To use the old CKEditor, you must enable it in /data/\_data\_/\_default\_/configs/application.ini
in the [labs] section add/edit: `use_ck_html_editor = On`
### PHP73 branch
There's a RainLoop 1.14.0 branch with only the PHP 7.3 changes at
https://github.com/the-djmaze/snappymail/tree/php73

View file

@ -1,40 +0,0 @@
# Security Policy
## Supported Versions
Currently due to the fast development only the latest version receives security updates.
| Version | Supported |
| -------- | --------- |
| 2.34.x | ✔ |
| < 2.34.0 | ❌ |
## Reporting a Vulnerability
Please report security issues or vulnerabilities as an encrypted email to [security@snappymail.eu](mailto:security@snappymail.eu).
Your report should be detailed enough with clear steps to reproduce and classify the found vulnerability.
You can find the PGP public key below and on the major public keyservers like [keys.openpgp.org](https://keys.openpgp.org).
```
-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: Type: 255-bit EdDSA
Comment: Fingerprint: 445D265124E6072671E64D0733F868A7E35E8277
mDMEYiE2QRYJKwYBBAHaRw8BAQdAqMrQUm6DddWcQNo0VEjNIu3Q6CfP3nokVv2Y
rNQ1avq0LFNuYXBweU1haWwgU2VjdXJpdHkgPHNlY3VyaXR5QHNuYXBweW1haWwu
ZXU+iJQEExYKADwWIQREXSZRJOYHJnHmTQcz+Gin416CdwUCYiE2QQIbAwULCQgH
AgMiAgEGFQoJCAsCBBYCAwECHgcCF4AACgkQM/hop+NegnfSGgD9GEHpOrvWpBGY
dYfvVd/+Lv5d+dFBcPyki9zu9zHfhwkBAL343EF6ZR0XwMlOQu9wu0hT9KBz4g55
6D41i0PrEaoBuDgEYiE2QRIKKwYBBAGXVQEFAQEHQMMr9gcVcJ3aiup/tpl8ZXxy
aJiJRGkPyNwGI5vxHMpZAwEIB4h4BBgWCgAgFiEERF0mUSTmByZx5k0HM/hop+Ne
gncFAmIhNkECGwwACgkQM/hop+NegndVhgD/SVGSKbF4G2W024VpW2tm3zCT+ue+
YMXQVq4SJt7UpWABAORudfJxsBqCRKtPlZMgGTJLjcOkyFJ9C2Fx7DeN0J4I
=nSOi
-----END PGP PUBLIC KEY BLOCK-----
```
## Publishing and Credits
I will analyze and fix the reported issue as fast as possible.
Together with the reporter, I plan the disclosure of the found and fixed vulnerability.
Credits to the reporter are granted and can be included in all public communication if desired.

1
_MULTIPLY Normal file
View file

@ -0,0 +1 @@
Name this file as "MULTIPLY" to enable multiple domain installation.

View file

@ -4,28 +4,24 @@
//header('Strict-Transport-Security: max-age=31536000');
/**
* Uncomment to enable multiple domain installation.
*/
//define('MULTIDOMAIN', 1);
//define('USE_GZIP', true); // to use gzip encoded output
/**
* Uncomment to disable APCU.
* @return string
*/
//define('APP_USE_APCU_CACHE', false);
function __get_custom_data_full_path()
{
return '';
return dirname(__DIR__) . '/rainloop-data';
return '/var/external-rainloop-data-folder'; // custom data folder path
}
/**
* Custom 'data' folder path
* @return string
*/
//define('APP_DATA_FOLDER_PATH', dirname(__DIR__) . '/snappymail-data/');
//define('APP_DATA_FOLDER_PATH', '/var/external-snappymail-data-folder/');
function __get_additional_configuration_name()
{
return '';
return defined('APP_SITE') && 0 < strlen(APP_SITE) ? APP_SITE.'.ini' : ''; // additional configuration file name
}
/**
* Additional configuration file name
*/
//define('APP_CONFIGURATION_NAME', $_SERVER['HTTP_HOST'].'.ini');
/**
* Also update extensions on upgrade
*/
//define('SNAPPYMAIL_UPDATE_PLUGINS', 1);

View file

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 992 B

After

Width:  |  Height:  |  Size: 992 B

Before After
Before After

BIN
assets/css/flags.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

Before

Width:  |  Height:  |  Size: 992 B

After

Width:  |  Height:  |  Size: 992 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 866 B

After

Width:  |  Height:  |  Size: 866 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 335 B

After

Width:  |  Height:  |  Size: 335 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 992 B

After

Width:  |  Height:  |  Size: 992 B

Before After
Before After

16
assets/manifest.json Normal file
View file

@ -0,0 +1,16 @@
{
"name": "RainLoop Webmail",
"short_name": "RainLoop",
"description": "Simple, modern & fast web-based email client",
"display": "standalone",
"icons": [{
"src": "apple-touch-icon.png",
"sizes": "64x64",
"type": "image/png"
}, {
"src": "android-icon.png",
"sizes": "192x192",
"type": "image/png"
}],
"start_url": "../../../../"
}

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

Binary file not shown.

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

Binary file not shown.

View file

@ -1,9 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEYg0atBYJKwYBBAHaRw8BAQdA2S2tvGavChACjtBastsKRThD3rsBW1LUZLmN
Zbs4uaG0I1NuYXBweU1haWwgPHJlbGVhc2VzQHNuYXBweW1haWwuZXU+iJQEExYK
ADwWIQQQFuRweRRVQvi6EzVIIIuhMpDz6wUCYg0atAIbAwULCQgHAgMiAgEGFQoJ
CAsCBBYCAwECHgcCF4AACgkQSCCLoTKQ8+u9SAD/Q/IoAwjUkKDJBPq0RGwCFnl6
FG/VHB97CvBSpGOxtIsBAMCwMhWlsaBHAEqbzxiN+cdlMYwV23+SWLUJ/XMFgukE
=5ZwF
-----END PGP PUBLIC KEY BLOCK-----

View file

@ -1,57 +0,0 @@
# Maintainer: George Rawlinson <george@rawlinson.net.nz>
pkgname=snappymail
pkgver=2.7.2
pkgrel=1
pkgdesc="modern PHP webmail client"
arch=('any')
license=('AGPL3')
url="https://github.com/the-djmaze/snappymail"
depends=('php-fpm')
makedepends=('php', 'nodejs' 'yarn' 'gulp', 'rollup')
optdepends=('mariadb: storage backend for contacts'
'php-pgsql: storage backend for contacts'
'php-sqlite: storage backend for contacts')
source=("$pkgname-$pkgver.tar.gz::$url/archive/v$pkgver.tar.gz"
"$pkgname.sysusers"
"$pkgname.tmpfiles")
b2sums=('a877041eaa74ca9824323b0ba80b3515205628825ed76fccff1511b2547c27419bae4733d24f8f36e6860c671cb609c4c5f31fa19529a92c628cb714570ff8ae'
'e020b2d4bc694ca056f5c15b148c69553ab610b5e1789f52543aa65e098f8097a41709b5b0fc22a6a01088a9d3f14d623b1b6e9ae2570acd4f380f429301c003'
'2536e11622895322cc752c6b651811b2122d3ae60099fe609609d7b45ba1ed00ea729c23f344405078698d161dbf9bcaffabf8eff14b740acdce3c681c513318')
prepare() {
sed -i "s/\$sCustomDataPath = '';/\$sCustomDataPath = '\/var\/lib\/$pkgname';/" "$pkgname-$pkgver/$pkgname/v/0.0.0/include.php"
# create folder for build output
mkdir -p build
}
build() {
cd "$pkgname-$pkgver"
yarn install
# build snappymail
php release.php --aur
bsdtar -x \
-C "$srcdir/build" \
-f "build/dist/releases/webmail/$pkgver/$pkgname-$pkgver.zip"
find . -type d -exec chmod 755 {} \;
find . -type f -exec chmod 644 {} \;
}
package() {
# directories
install -dm755 "$pkgdir/usr/share/snappymail" \
"$pkgdir/var/lib/snappymail"
# application files
cp -r "$srcdir/build/snappymail" "$pkgdir/usr/share/snappymail"
install -Dm644 -t "$pkgdir/usr/share/snappymail" "$srcdir/build/index.php"
# data files
cp -r "$srcdir/build/data" "$pkgdir/var/lib/snappymail"
# sysusers
install -Dm644 "$srcdir/$pkgname.sysusers" "$pkgdir/usr/lib/sysusers.d/$pkgname.conf"
install -Dm644 "$srcdir/$pkgname.tmpfiles" "$pkgdir/usr/lib/tmpfiles.d/$pkgname.conf"
}

View file

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

View file

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

View file

@ -1,38 +0,0 @@
<?php
echo "\x1b[33;1m === cPanel === \x1b[0m\n";
$cpanel_destination = "{$destPath}snappymail-{$package->version}-cpanel.tar";
@unlink($cpanel_destination);
@unlink("{$cpanel_destination}.gz");
$cpanel_tar = new PharData($cpanel_destination);
$cpanel_tar->buildFromDirectory('./integrations/cpanel', "@integrations/cpanel/@");
$cpanel_path = '/usr/local/cpanel/base/3rdparty/snappymail/';
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('snappymail/v'), RecursiveIteratorIterator::SELF_FIRST);
foreach ($files as $file) {
if (is_file($file)) {
$newFile = str_replace('\\', '/', $file);
$cpanel_tar->addFile($file, "{$cpanel_path}{$newFile}");
}
}
$cpanel_tar->addFile('.htaccess', "{$cpanel_path}.htaccess");
$cpanel_tar->addFile('README.md', "{$cpanel_path}README.md");
$cpanel_tar->addFromString("{$cpanel_path}VERSION", $package->version);
$index = file_get_contents('index.php');
$index = str_replace('0.0.0', $package->version, $index);
$cpanel_tar->addFromString("{$cpanel_path}index.php", $index);
$data = file_get_contents('dev/serviceworker.js');
$cpanel_tar->addFromString("{$cpanel_path}serviceworker.js", $data);
$cpanel_tar->compress(Phar::GZ);
unlink($cpanel_destination);
$cpanel_destination .= '.gz';
echo "{$cpanel_destination} created\n";

View file

@ -1,83 +0,0 @@
<?php
echo "\x1b[33;1m === Debian === \x1b[0m\n";
// Debian Repository
define('DEB_SOURCE_DIR', __DIR__ . '/deb');
define('DEB_DEST_DIR', DEB_SOURCE_DIR . "/snappymail_{$package->version}-1_all");
is_dir(DEB_DEST_DIR) && passthru('rm -dfr '.escapeshellarg(DEB_DEST_DIR));
$dir = DEB_DEST_DIR . '/DEBIAN';
$data = file_get_contents(DEB_SOURCE_DIR . '/DEBIAN/control');
$data = str_replace('0.0.0', $package->version, $data);
mkdir($dir, 0755, true);
file_put_contents("{$dir}/control", $data);
copy(DEB_SOURCE_DIR . '/DEBIAN/postinst', $dir . '/postinst');
chmod($dir . '/postinst', 0755);
$dir = DEB_DEST_DIR . '/var/lib/snappymail';
mkdir($dir, 0755, true);
file_put_contents($dir . '/VERSION', $package->version);
copy('data/README.md', "{$dir}/README.md");
$dir = DEB_DEST_DIR . '/usr/share/doc/snappymail';
mkdir($dir, 0755, true);
copy('CODE_OF_CONDUCT.md', "{$dir}/CODE_OF_CONDUCT.md");
copy('CONTRIBUTING.md', "{$dir}/CONTRIBUTING.md");
copy('README.md', "{$dir}/README.md");
//copy('CODE_OF_CONDUCT.md', "{$dir}/CODE_OF_CONDUCT.md");
//usr/share/doc/snappymail/README.Debian
//usr/share/doc/snappymail/changelog.Debian.gz
//usr/share/doc/snappymail/copyright
// Move files into package directory
$dir = DEB_DEST_DIR . '/usr/share/snappymail';
mkdir($dir, 0755, true);
passthru('cp -r "' . dirname(__DIR__) . '/snappymail" "' . $dir . '"');
rename("{$dir}/snappymail/v/0.0.0", "{$dir}/snappymail/v/{$package->version}");
$data = file_get_contents('index.php');
file_put_contents("{$dir}/index.php", str_replace('0.0.0', $package->version, $data));
$data = file_get_contents('_include.php');
file_put_contents("{$dir}/include.php", preg_replace('@(external-snappymail-data-folder/\'\);)@', "\$1\ndefine('APP_DATA_FOLDER_PATH', '/var/lib/snappymail/');", $data));
passthru('dpkg --build ' . escapeshellarg(DEB_DEST_DIR));
$TARGET_DIR = __DIR__ . "/dist/releases/webmail/{$package->version}/";
passthru('mv '
. escapeshellarg(DEB_DEST_DIR.'.deb') . ' '
. escapeshellarg($TARGET_DIR . basename(DEB_DEST_DIR.'.deb'))
);
passthru('rm -dfr '.escapeshellarg(DEB_DEST_DIR));
// https://github.com/the-djmaze/snappymail/issues/185#issuecomment-1059420588
$cwd = getcwd();
chdir($TARGET_DIR);
passthru('dpkg-scanpackages . /dev/null > '.escapeshellarg($TARGET_DIR . 'Packages'));
passthru('dpkg-scanpackages . /dev/null | gzip -9c > '.escapeshellarg($TARGET_DIR . 'Packages.gz'));
$size = filesize($TARGET_DIR . 'Packages');
$gz_size = filesize($TARGET_DIR . 'Packages.gz');
$Release = 'Origin: SnappyMail Repository
Label: SnappyMail
Suite: stable
Codename: stable
Version: 1.0
Architectures: all
Components: main
Description: SnappyMail repository
Date: ' . gmdate('r') . '
MD5Sum:
' . hash_file('md5', $TARGET_DIR . 'Packages') . ' ' . $size . ' Packages
' . hash_file('md5', $TARGET_DIR . 'Packages.gz') . ' ' . $gz_size . ' Packages.gz
SHA1:
' . hash_file('sha1', $TARGET_DIR . 'Packages') . ' ' . $size . ' Packages
' . hash_file('sha1', $TARGET_DIR . 'Packages.gz') . ' ' . $gz_size . ' Packages.gz
SHA256:
' . hash_file('sha256', $TARGET_DIR . 'Packages') . ' ' . $size . ' Packages
' . hash_file('sha256', $TARGET_DIR . 'Packages.gz') . ' ' . $gz_size . ' Packages.gz
';
file_put_contents($TARGET_DIR . 'Release', $Release);
chdir($cwd);

View file

@ -1,14 +0,0 @@
Package: snappymail
Version: 0.0.0
Maintainer: SnappyMail <debian@snappymail.eu>
Depends: nginx | apache2 | httpd, php-fpm | libapache2-mod-php, php-json, php-mbstring
Recommends: php-intl, php-sodium, php-uuid
Suggests: php-sqlite3 | php-mysql | php-pgsql, php-curl, php-exif, php-gnupg, php-gd | php-gmagick | php-imagick, php-openssl, php-zip
Architecture: all
Homepage: https://snappymail.eu
Vcs-Browser: https://github.com/the-djmaze/snappymail
Vcs-Git: https://github.com/the-djmaze/snappymail.git
Description: SnappyMail is a PHP-based simple, modern, lightweight & fast web-based email client with no database requirements.
It supports IMAP, SMTP and Sieve protocols, multiple accounts and identities, an admin panel for configuration.
Plugins can be installed to further extend functionality.
Emails are not stored locally, but are accessed through IMAP.

View file

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

View file

@ -1,92 +0,0 @@
<?php
echo "\x1b[33;1m === Nextcloud === \x1b[0m\n";
$cert_dir = $_SERVER['HOME'].'/.nextcloud/certificates';
$nc_destination = "{$destPath}snappymail-{$package->version}-nextcloud.tar";
@unlink($nc_destination);
@unlink("{$nc_destination}.gz");
$nc_tar = new PharData($nc_destination);
$hashes = [];
file_put_contents(ROOT_DIR . '/integrations/nextcloud/snappymail/VERSION', $package->version);
$file = ROOT_DIR . '/integrations/nextcloud/snappymail/appinfo/info.xml';
file_put_contents($file, preg_replace('/<version>[^<]*</', "<version>{$package->version}<", file_get_contents($file)));
$nc_tar->buildFromDirectory('./integrations/nextcloud', "@integrations/nextcloud/snappymail/@");
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('integrations/nextcloud/snappymail'));
foreach ($files as $file) {
if (is_file($file)) {
$name = str_replace('\\', '/', $file);
$name = str_replace('integrations/nextcloud/snappymail/', '', $name);
$hashes[$name] = hash_file('sha512', $file);
}
}
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('snappymail/v'), RecursiveIteratorIterator::SELF_FIRST);
foreach ($files as $file) {
if (is_file($file)) {
$newFile = str_replace('\\', '/', $file);
$newName = str_replace('/.htaccess', '/_htaccess', $newFile);
$nc_tar->addFile($file, "snappymail/app/{$newName}");
$hashes["app/{$newFile}"] = hash_file('sha512', $file);
}
}
/*
$nc_tar->addFile('data/.htaccess');
$nc_tar->addFromString('data/VERSION', $package->version);
$nc_tar->addFile('data/README.md');
$nc_tar->addFile('_include.php', 'snappymail/app/_include.php');
*/
$nc_tar->addFile('.htaccess', 'snappymail/app/_htaccess');
$hashes['app/.htaccess'] = hash_file('sha512', '.htaccess');
$index = file_get_contents('index.php');
$index = str_replace('0.0.0', $package->version, $index);
//$index = str_replace('snappymail/v/', '', $index);
$nc_tar->addFromString('snappymail/app/index.php', $index);
$hashes['app/index.php'] = hash('sha512', $index);
$nc_tar->addFile('README.md', 'snappymail/app/README.md');
$hashes['app/README.md'] = hash_file('sha512', 'README.md');
$nc_tar->addFile('CHANGELOG.md', 'snappymail/CHANGELOG.md');
$hashes['CHANGELOG.md'] = hash_file('sha512', 'CHANGELOG.md');
$data = file_get_contents('dev/serviceworker.js');
$nc_tar->addFromString('snappymail/app/serviceworker.js', $data);
$hashes['app/serviceworker.js'] = hash('sha512', $data);
spl_autoload_register(function($name){
$file = __DIR__ . '/' . str_replace('\\', '/', $name) . '.php';
require $file;
});
ksort($hashes);
$cert = file_get_contents($cert_dir.'/snappymail.crt');
$rsa = new \phpseclib\Crypt\RSA();
$rsa->loadKey(file_get_contents($cert_dir.'/snappymail.key'));
$x509 = new \phpseclib\File\X509();
$x509->loadX509($cert);
$x509->setPrivateKey($rsa);
$rsa->setSignatureMode(\phpseclib\Crypt\RSA::SIGNATURE_PSS);
$rsa->setMGFHash('sha512');
$rsa->setSaltLength(0);
$signature = $rsa->sign(json_encode($hashes));
$nc_tar->addFromString('snappymail/appinfo/signature.json', json_encode([
'hashes' => $hashes,
'signature' => base64_encode($signature),
'certificate' => $cert
], JSON_PRETTY_PRINT));
$nc_tar->compress(Phar::GZ);
unlink($nc_destination);
$nc_destination .= '.gz';
$signature = shell_exec("openssl dgst -sha512 -sign {$cert_dir}/snappymail.key {$nc_destination} | openssl base64");
file_put_contents($nc_destination.'.sig', $signature);
echo "{$nc_destination} created\n";

View file

@ -1,100 +0,0 @@
<?php
echo "\x1b[33;1m === ownCloud === \x1b[0m\n";
$cert_dir = false; // $_SERVER['HOME'].'/.owncloud/certificates';
$nc_destination = "{$destPath}snappymail-{$package->version}-owncloud.tar";
@unlink($nc_destination);
@unlink("{$nc_destination}.gz");
$nc_tar = new PharData($nc_destination);
$hashes = [];
file_put_contents(ROOT_DIR . '/integrations/owncloud/snappymail/VERSION', $package->version);
$file = ROOT_DIR . '/integrations/owncloud/snappymail/appinfo/info.xml';
file_put_contents($file, preg_replace('/<version>[^<]*</', "<version>{$package->version}<", file_get_contents($file)));
$nc_tar->buildFromDirectory('./integrations/owncloud', "@integrations/owncloud/snappymail/@");
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('integrations/owncloud/snappymail'));
foreach ($files as $file) {
if (is_file($file)) {
$name = str_replace('\\', '/', $file);
$name = str_replace('integrations/owncloud/snappymail/', '', $name);
$hashes[$name] = hash_file('sha512', $file);
}
}
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('snappymail/v'), RecursiveIteratorIterator::SELF_FIRST);
foreach ($files as $file) {
if (is_file($file)) {
$newFile = str_replace('\\', '/', $file);
// $newFile = str_replace("'snappymail/v/'.", '', $newFile);
$nc_tar->addFile($file, "snappymail/app/{$newFile}");
$hashes["app/{$newFile}"] = hash_file('sha512', $file);
}
}
$nc_tar->delete("snappymail/app/snappymail/v/{$package->version}/app/.htaccess");
$nc_tar->addFile("snappymail/v/{$package->version}/app/.htaccess", "snappymail/app/snappymail/v/{$package->version}/app/_htaccess");
$nc_tar->delete("snappymail/app/snappymail/v/{$package->version}/static/.htaccess");
$nc_tar->addFile("snappymail/v/{$package->version}/static/.htaccess", "snappymail/app/snappymail/v/{$package->version}/static/_htaccess");
/*
$nc_tar->addFile('data/.htaccess');
$nc_tar->addFromString('data/VERSION', $package->version);
$nc_tar->addFile('data/README.md');
$nc_tar->addFile('_include.php', 'snappymail/app/_include.php');
*/
$nc_tar->addFile('.htaccess', 'snappymail/app/_htaccess');
$hashes['app/.htaccess'] = hash_file('sha512', '.htaccess');
$index = file_get_contents('index.php');
$index = str_replace('0.0.0', $package->version, $index);
//$index = str_replace('snappymail/v/', '', $index);
$nc_tar->addFromString('snappymail/app/index.php', $index);
$hashes['app/index.php'] = hash('sha512', $index);
$nc_tar->addFile('README.md', 'snappymail/app/README.md');
$hashes['app/README.md'] = hash_file('sha512', 'README.md');
$nc_tar->addFile('CHANGELOG.md', 'snappymail/CHANGELOG.md');
$hashes['CHANGELOG.md'] = hash_file('sha512', 'CHANGELOG.md');
$data = file_get_contents('dev/serviceworker.js');
$nc_tar->addFromString('snappymail/app/serviceworker.js', $data);
$hashes['app/serviceworker.js'] = hash('sha512', $data);
if ($cert_dir) {
spl_autoload_register(function($name){
$file = __DIR__ . '/' . str_replace('\\', '/', $name) . '.php';
require $file;
});
ksort($hashes);
$cert = file_get_contents($cert_dir.'/snappymail.crt');
$rsa = new \phpseclib\Crypt\RSA();
$rsa->loadKey(file_get_contents($cert_dir.'/snappymail.key'));
$x509 = new \phpseclib\File\X509();
$x509->loadX509($cert);
$x509->setPrivateKey($rsa);
$rsa->setSignatureMode(\phpseclib\Crypt\RSA::SIGNATURE_PSS);
$rsa->setMGFHash('sha512');
$rsa->setSaltLength(0);
$signature = $rsa->sign(json_encode($hashes));
$nc_tar->addFromString('snappymail/appinfo/signature.json', json_encode([
'hashes' => $hashes,
'signature' => base64_encode($signature),
'certificate' => $cert
], JSON_PRETTY_PRINT));
}
$nc_tar->compress(Phar::GZ);
unlink($nc_destination);
$nc_destination .= '.gz';
if ($cert_dir) {
$signature = shell_exec("openssl dgst -sha512 -sign {$cert_dir}/snappymail.key {$nc_destination} | openssl base64");
file_put_contents($nc_destination.'.sig', $signature);
}
echo "{$nc_destination} created\n";

View file

@ -1,893 +0,0 @@
<?php
/**
* Pure-PHP implementations of keyed-hash message authentication codes (HMACs) and various cryptographic hashing functions.
*
* Uses hash() or mhash() if available and an internal implementation, otherwise. Currently supports the following:
*
* md2, md5, md5-96, sha1, sha1-96, sha256, sha256-96, sha384, and sha512, sha512-96
*
* If {@link self::setKey() setKey()} is called, {@link self::hash() hash()} will return the HMAC as opposed to
* the hash. If no valid algorithm is provided, sha1 will be used.
*
* PHP version 5
*
* {@internal The variable names are the same as those in
* {@link http://tools.ietf.org/html/rfc2104#section-2 RFC2104}.}}
*
* Here's a short example of how to use this library:
* <code>
* <?php
* include 'vendor/autoload.php';
*
* $hash = new \phpseclib\Crypt\Hash('sha1');
*
* $hash->setKey('abcdefg');
*
* echo base64_encode($hash->hash('abcdefg'));
* ?>
* </code>
*
* @category Crypt
* @package Hash
* @author Jim Wigginton <terrafrost@php.net>
* @copyright 2007 Jim Wigginton
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @link http://phpseclib.sourceforge.net
*/
namespace phpseclib\Crypt;
use phpseclib\Math\BigInteger;
/**
* Pure-PHP implementations of keyed-hash message authentication codes (HMACs) and various cryptographic hashing functions.
*
* @package Hash
* @author Jim Wigginton <terrafrost@php.net>
* @access public
*/
class Hash
{
/**#@+
* @access private
* @see \phpseclib\Crypt\Hash::__construct()
*/
/**
* Toggles the internal implementation
*/
const MODE_INTERNAL = 1;
/**
* Toggles the mhash() implementation, which has been deprecated on PHP 5.3.0+.
*/
const MODE_MHASH = 2;
/**
* Toggles the hash() implementation, which works on PHP 5.1.2+.
*/
const MODE_HASH = 3;
/**#@-*/
/**
* Hash Parameter
*
* @see self::setHash()
* @var int
* @access private
*/
var $hashParam;
/**
* Byte-length of compression blocks / key (Internal HMAC)
*
* @see self::setAlgorithm()
* @var int
* @access private
*/
var $b;
/**
* Byte-length of hash output (Internal HMAC)
*
* @see self::setHash()
* @var int
* @access private
*/
var $l = false;
/**
* Hash Algorithm
*
* @see self::setHash()
* @var string
* @access private
*/
var $hash;
/**
* Key
*
* @see self::setKey()
* @var string
* @access private
*/
var $key = false;
/**
* Computed Key
*
* @see self::_computeKey()
* @var string
* @access private
*/
var $computedKey = false;
/**
* Outer XOR (Internal HMAC)
*
* @see self::setKey()
* @var string
* @access private
*/
var $opad;
/**
* Inner XOR (Internal HMAC)
*
* @see self::setKey()
* @var string
* @access private
*/
var $ipad;
/**
* Engine
*
* @see self::setHash()
* @var string
* @access private
*/
var $engine;
/**
* Default Constructor.
*
* @param string $hash
* @return \phpseclib\Crypt\Hash
* @access public
*/
function __construct($hash = 'sha1')
{
if (!defined('CRYPT_HASH_MODE')) {
switch (true) {
case extension_loaded('hash'):
define('CRYPT_HASH_MODE', self::MODE_HASH);
break;
case extension_loaded('mhash'):
define('CRYPT_HASH_MODE', self::MODE_MHASH);
break;
default:
define('CRYPT_HASH_MODE', self::MODE_INTERNAL);
}
}
$this->setHash($hash);
}
/**
* Sets the key for HMACs
*
* Keys can be of any length.
*
* @access public
* @param string $key
*/
function setKey($key = false)
{
$this->key = $key;
$this->_computeKey();
}
/**
* Pre-compute the key used by the HMAC
*
* Quoting http://tools.ietf.org/html/rfc2104#section-2, "Applications that use keys longer than B bytes
* will first hash the key using H and then use the resultant L byte string as the actual key to HMAC."
*
* As documented in https://www.reddit.com/r/PHP/comments/9nct2l/symfonypolyfill_hash_pbkdf2_correct_fix_for/
* when doing an HMAC multiple times it's faster to compute the hash once instead of computing it during
* every call
*
* @access private
*/
function _computeKey()
{
if ($this->key === false) {
$this->computedKey = false;
return;
}
if (strlen($this->key) <= $this->b) {
$this->computedKey = $this->key;
return;
}
switch ($this->engine) {
case self::MODE_MHASH:
$this->computedKey = mhash($this->hash, $this->key);
break;
case self::MODE_HASH:
$this->computedKey = hash($this->hash, $this->key, true);
break;
case self::MODE_INTERNAL:
$this->computedKey = call_user_func($this->hash, $this->key);
}
}
/**
* Gets the hash function.
*
* As set by the constructor or by the setHash() method.
*
* @access public
* @return string
*/
function getHash()
{
return $this->hashParam;
}
/**
* Sets the hash function.
*
* @access public
* @param string $hash
*/
function setHash($hash)
{
$this->hashParam = $hash = strtolower($hash);
switch ($hash) {
case 'md5-96':
case 'sha1-96':
case 'sha256-96':
case 'sha512-96':
$hash = substr($hash, 0, -3);
$this->l = 12; // 96 / 8 = 12
break;
case 'md2':
case 'md5':
$this->l = 16;
break;
case 'sha1':
$this->l = 20;
break;
case 'sha256':
$this->l = 32;
break;
case 'sha384':
$this->l = 48;
break;
case 'sha512':
$this->l = 64;
}
switch ($hash) {
case 'md2-96':
case 'md2':
$this->b = 16;
case 'md5-96':
case 'sha1-96':
case 'sha224-96':
case 'sha256-96':
case 'md2':
case 'md5':
case 'sha1':
case 'sha224':
case 'sha256':
$this->b = 64;
break;
default:
$this->b = 128;
}
switch ($hash) {
case 'md2':
$this->engine = CRYPT_HASH_MODE == self::MODE_HASH && in_array('md2', hash_algos()) ?
self::MODE_HASH : self::MODE_INTERNAL;
break;
case 'sha384':
case 'sha512':
$this->engine = CRYPT_HASH_MODE == self::MODE_MHASH ? self::MODE_INTERNAL : CRYPT_HASH_MODE;
break;
default:
$this->engine = CRYPT_HASH_MODE;
}
switch ($this->engine) {
case self::MODE_MHASH:
switch ($hash) {
case 'md5':
$this->hash = MHASH_MD5;
break;
case 'sha256':
$this->hash = MHASH_SHA256;
break;
case 'sha1':
default:
$this->hash = MHASH_SHA1;
}
$this->_computeKey(self::MODE_MHASH);
return;
case self::MODE_HASH:
switch ($hash) {
case 'md5':
$this->hash = 'md5';
return;
case 'md2':
case 'sha256':
case 'sha384':
case 'sha512':
$this->hash = $hash;
return;
case 'sha1':
default:
$this->hash = 'sha1';
}
$this->_computeKey(self::MODE_HASH);
return;
}
switch ($hash) {
case 'md2':
$this->hash = array($this, '_md2');
break;
case 'md5':
$this->hash = array($this, '_md5');
break;
case 'sha256':
$this->hash = array($this, '_sha256');
break;
case 'sha384':
case 'sha512':
$this->hash = array($this, '_sha512');
break;
case 'sha1':
default:
$this->hash = array($this, '_sha1');
}
$this->ipad = str_repeat(chr(0x36), $this->b);
$this->opad = str_repeat(chr(0x5C), $this->b);
$this->_computeKey(self::MODE_INTERNAL);
}
/**
* Compute the HMAC.
*
* @access public
* @param string $text
* @return string
*/
function hash($text)
{
if (!empty($this->key) || is_string($this->key)) {
switch ($this->engine) {
case self::MODE_MHASH:
$output = mhash($this->hash, $text, $this->computedKey);
break;
case self::MODE_HASH:
$output = hash_hmac($this->hash, $text, $this->computedKey, true);
break;
case self::MODE_INTERNAL:
$key = str_pad($this->computedKey, $this->b, chr(0)); // step 1
$temp = $this->ipad ^ $key; // step 2
$temp .= $text; // step 3
$temp = call_user_func($this->hash, $temp); // step 4
$output = $this->opad ^ $key; // step 5
$output.= $temp; // step 6
$output = call_user_func($this->hash, $output); // step 7
}
} else {
switch ($this->engine) {
case self::MODE_MHASH:
$output = mhash($this->hash, $text);
break;
case self::MODE_HASH:
$output = hash($this->hash, $text, true);
break;
case self::MODE_INTERNAL:
$output = call_user_func($this->hash, $text);
}
}
return substr($output, 0, $this->l);
}
/**
* Returns the hash length (in bytes)
*
* @access public
* @return int
*/
function getLength()
{
return $this->l;
}
/**
* Wrapper for MD5
*
* @access private
* @param string $m
*/
function _md5($m)
{
return pack('H*', md5($m));
}
/**
* Wrapper for SHA1
*
* @access private
* @param string $m
*/
function _sha1($m)
{
return pack('H*', sha1($m));
}
/**
* Pure-PHP implementation of MD2
*
* See {@link http://tools.ietf.org/html/rfc1319 RFC1319}.
*
* @access private
* @param string $m
*/
function _md2($m)
{
static $s = array(
41, 46, 67, 201, 162, 216, 124, 1, 61, 54, 84, 161, 236, 240, 6,
19, 98, 167, 5, 243, 192, 199, 115, 140, 152, 147, 43, 217, 188,
76, 130, 202, 30, 155, 87, 60, 253, 212, 224, 22, 103, 66, 111, 24,
138, 23, 229, 18, 190, 78, 196, 214, 218, 158, 222, 73, 160, 251,
245, 142, 187, 47, 238, 122, 169, 104, 121, 145, 21, 178, 7, 63,
148, 194, 16, 137, 11, 34, 95, 33, 128, 127, 93, 154, 90, 144, 50,
39, 53, 62, 204, 231, 191, 247, 151, 3, 255, 25, 48, 179, 72, 165,
181, 209, 215, 94, 146, 42, 172, 86, 170, 198, 79, 184, 56, 210,
150, 164, 125, 182, 118, 252, 107, 226, 156, 116, 4, 241, 69, 157,
112, 89, 100, 113, 135, 32, 134, 91, 207, 101, 230, 45, 168, 2, 27,
96, 37, 173, 174, 176, 185, 246, 28, 70, 97, 105, 52, 64, 126, 15,
85, 71, 163, 35, 221, 81, 175, 58, 195, 92, 249, 206, 186, 197,
234, 38, 44, 83, 13, 110, 133, 40, 132, 9, 211, 223, 205, 244, 65,
129, 77, 82, 106, 220, 55, 200, 108, 193, 171, 250, 36, 225, 123,
8, 12, 189, 177, 74, 120, 136, 149, 139, 227, 99, 232, 109, 233,
203, 213, 254, 59, 0, 29, 57, 242, 239, 183, 14, 102, 88, 208, 228,
166, 119, 114, 248, 235, 117, 75, 10, 49, 68, 80, 180, 143, 237,
31, 26, 219, 153, 141, 51, 159, 17, 131, 20
);
// Step 1. Append Padding Bytes
$pad = 16 - (strlen($m) & 0xF);
$m.= str_repeat(chr($pad), $pad);
$length = strlen($m);
// Step 2. Append Checksum
$c = str_repeat(chr(0), 16);
$l = chr(0);
for ($i = 0; $i < $length; $i+= 16) {
for ($j = 0; $j < 16; $j++) {
// RFC1319 incorrectly states that C[j] should be set to S[c xor L]
//$c[$j] = chr($s[ord($m[$i + $j] ^ $l)]);
// per <http://www.rfc-editor.org/errata_search.php?rfc=1319>, however, C[j] should be set to S[c xor L] xor C[j]
$c[$j] = chr($s[ord($m[$i + $j] ^ $l)] ^ ord($c[$j]));
$l = $c[$j];
}
}
$m.= $c;
$length+= 16;
// Step 3. Initialize MD Buffer
$x = str_repeat(chr(0), 48);
// Step 4. Process Message in 16-Byte Blocks
for ($i = 0; $i < $length; $i+= 16) {
for ($j = 0; $j < 16; $j++) {
$x[$j + 16] = $m[$i + $j];
$x[$j + 32] = $x[$j + 16] ^ $x[$j];
}
$t = chr(0);
for ($j = 0; $j < 18; $j++) {
for ($k = 0; $k < 48; $k++) {
$x[$k] = $t = $x[$k] ^ chr($s[ord($t)]);
//$t = $x[$k] = $x[$k] ^ chr($s[ord($t)]);
}
$t = chr(ord($t) + $j);
}
}
// Step 5. Output
return substr($x, 0, 16);
}
/**
* Pure-PHP implementation of SHA256
*
* See {@link http://en.wikipedia.org/wiki/SHA_hash_functions#SHA-256_.28a_SHA-2_variant.29_pseudocode SHA-256 (a SHA-2 variant) pseudocode - Wikipedia}.
*
* @access private
* @param string $m
*/
function _sha256($m)
{
if (extension_loaded('suhosin')) {
return pack('H*', sha256($m));
}
// Initialize variables
$hash = array(
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
);
// Initialize table of round constants
// (first 32 bits of the fractional parts of the cube roots of the first 64 primes 2..311)
static $k = array(
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
);
// Pre-processing
$length = strlen($m);
// to round to nearest 56 mod 64, we'll add 64 - (length + (64 - 56)) % 64
$m.= str_repeat(chr(0), 64 - (($length + 8) & 0x3F));
$m[$length] = chr(0x80);
// we don't support hashing strings 512MB long
$m.= pack('N2', 0, $length << 3);
// Process the message in successive 512-bit chunks
$chunks = str_split($m, 64);
foreach ($chunks as $chunk) {
$w = array();
for ($i = 0; $i < 16; $i++) {
extract(unpack('Ntemp', $this->_string_shift($chunk, 4)));
$w[] = $temp;
}
// Extend the sixteen 32-bit words into sixty-four 32-bit words
for ($i = 16; $i < 64; $i++) {
// @codingStandardsIgnoreStart
$s0 = $this->_rightRotate($w[$i - 15], 7) ^
$this->_rightRotate($w[$i - 15], 18) ^
$this->_rightShift( $w[$i - 15], 3);
$s1 = $this->_rightRotate($w[$i - 2], 17) ^
$this->_rightRotate($w[$i - 2], 19) ^
$this->_rightShift( $w[$i - 2], 10);
// @codingStandardsIgnoreEnd
$w[$i] = $this->_add($w[$i - 16], $s0, $w[$i - 7], $s1);
}
// Initialize hash value for this chunk
list($a, $b, $c, $d, $e, $f, $g, $h) = $hash;
// Main loop
for ($i = 0; $i < 64; $i++) {
$s0 = $this->_rightRotate($a, 2) ^
$this->_rightRotate($a, 13) ^
$this->_rightRotate($a, 22);
$maj = ($a & $b) ^
($a & $c) ^
($b & $c);
$t2 = $this->_add($s0, $maj);
$s1 = $this->_rightRotate($e, 6) ^
$this->_rightRotate($e, 11) ^
$this->_rightRotate($e, 25);
$ch = ($e & $f) ^
($this->_not($e) & $g);
$t1 = $this->_add($h, $s1, $ch, $k[$i], $w[$i]);
$h = $g;
$g = $f;
$f = $e;
$e = $this->_add($d, $t1);
$d = $c;
$c = $b;
$b = $a;
$a = $this->_add($t1, $t2);
}
// Add this chunk's hash to result so far
$hash = array(
$this->_add($hash[0], $a),
$this->_add($hash[1], $b),
$this->_add($hash[2], $c),
$this->_add($hash[3], $d),
$this->_add($hash[4], $e),
$this->_add($hash[5], $f),
$this->_add($hash[6], $g),
$this->_add($hash[7], $h)
);
}
// Produce the final hash value (big-endian)
return pack('N8', $hash[0], $hash[1], $hash[2], $hash[3], $hash[4], $hash[5], $hash[6], $hash[7]);
}
/**
* Pure-PHP implementation of SHA384 and SHA512
*
* @access private
* @param string $m
*/
function _sha512($m)
{
static $init384, $init512, $k;
if (!isset($k)) {
// Initialize variables
$init384 = array( // initial values for SHA384
'cbbb9d5dc1059ed8', '629a292a367cd507', '9159015a3070dd17', '152fecd8f70e5939',
'67332667ffc00b31', '8eb44a8768581511', 'db0c2e0d64f98fa7', '47b5481dbefa4fa4'
);
$init512 = array( // initial values for SHA512
'6a09e667f3bcc908', 'bb67ae8584caa73b', '3c6ef372fe94f82b', 'a54ff53a5f1d36f1',
'510e527fade682d1', '9b05688c2b3e6c1f', '1f83d9abfb41bd6b', '5be0cd19137e2179'
);
for ($i = 0; $i < 8; $i++) {
$init384[$i] = new BigInteger($init384[$i], 16);
$init384[$i]->setPrecision(64);
$init512[$i] = new BigInteger($init512[$i], 16);
$init512[$i]->setPrecision(64);
}
// Initialize table of round constants
// (first 64 bits of the fractional parts of the cube roots of the first 80 primes 2..409)
$k = array(
'428a2f98d728ae22', '7137449123ef65cd', 'b5c0fbcfec4d3b2f', 'e9b5dba58189dbbc',
'3956c25bf348b538', '59f111f1b605d019', '923f82a4af194f9b', 'ab1c5ed5da6d8118',
'd807aa98a3030242', '12835b0145706fbe', '243185be4ee4b28c', '550c7dc3d5ffb4e2',
'72be5d74f27b896f', '80deb1fe3b1696b1', '9bdc06a725c71235', 'c19bf174cf692694',
'e49b69c19ef14ad2', 'efbe4786384f25e3', '0fc19dc68b8cd5b5', '240ca1cc77ac9c65',
'2de92c6f592b0275', '4a7484aa6ea6e483', '5cb0a9dcbd41fbd4', '76f988da831153b5',
'983e5152ee66dfab', 'a831c66d2db43210', 'b00327c898fb213f', 'bf597fc7beef0ee4',
'c6e00bf33da88fc2', 'd5a79147930aa725', '06ca6351e003826f', '142929670a0e6e70',
'27b70a8546d22ffc', '2e1b21385c26c926', '4d2c6dfc5ac42aed', '53380d139d95b3df',
'650a73548baf63de', '766a0abb3c77b2a8', '81c2c92e47edaee6', '92722c851482353b',
'a2bfe8a14cf10364', 'a81a664bbc423001', 'c24b8b70d0f89791', 'c76c51a30654be30',
'd192e819d6ef5218', 'd69906245565a910', 'f40e35855771202a', '106aa07032bbd1b8',
'19a4c116b8d2d0c8', '1e376c085141ab53', '2748774cdf8eeb99', '34b0bcb5e19b48a8',
'391c0cb3c5c95a63', '4ed8aa4ae3418acb', '5b9cca4f7763e373', '682e6ff3d6b2b8a3',
'748f82ee5defb2fc', '78a5636f43172f60', '84c87814a1f0ab72', '8cc702081a6439ec',
'90befffa23631e28', 'a4506cebde82bde9', 'bef9a3f7b2c67915', 'c67178f2e372532b',
'ca273eceea26619c', 'd186b8c721c0c207', 'eada7dd6cde0eb1e', 'f57d4f7fee6ed178',
'06f067aa72176fba', '0a637dc5a2c898a6', '113f9804bef90dae', '1b710b35131c471b',
'28db77f523047d84', '32caab7b40c72493', '3c9ebe0a15c9bebc', '431d67c49c100d4c',
'4cc5d4becb3e42b6', '597f299cfc657e2a', '5fcb6fab3ad6faec', '6c44198c4a475817'
);
for ($i = 0; $i < 80; $i++) {
$k[$i] = new BigInteger($k[$i], 16);
}
}
$hash = $this->l == 48 ? $init384 : $init512;
// Pre-processing
$length = strlen($m);
// to round to nearest 112 mod 128, we'll add 128 - (length + (128 - 112)) % 128
$m.= str_repeat(chr(0), 128 - (($length + 16) & 0x7F));
$m[$length] = chr(0x80);
// we don't support hashing strings 512MB long
$m.= pack('N4', 0, 0, 0, $length << 3);
// Process the message in successive 1024-bit chunks
$chunks = str_split($m, 128);
foreach ($chunks as $chunk) {
$w = array();
for ($i = 0; $i < 16; $i++) {
$temp = new BigInteger($this->_string_shift($chunk, 8), 256);
$temp->setPrecision(64);
$w[] = $temp;
}
// Extend the sixteen 32-bit words into eighty 32-bit words
for ($i = 16; $i < 80; $i++) {
$temp = array(
$w[$i - 15]->bitwise_rightRotate(1),
$w[$i - 15]->bitwise_rightRotate(8),
$w[$i - 15]->bitwise_rightShift(7)
);
$s0 = $temp[0]->bitwise_xor($temp[1]);
$s0 = $s0->bitwise_xor($temp[2]);
$temp = array(
$w[$i - 2]->bitwise_rightRotate(19),
$w[$i - 2]->bitwise_rightRotate(61),
$w[$i - 2]->bitwise_rightShift(6)
);
$s1 = $temp[0]->bitwise_xor($temp[1]);
$s1 = $s1->bitwise_xor($temp[2]);
$w[$i] = $w[$i - 16]->copy();
$w[$i] = $w[$i]->add($s0);
$w[$i] = $w[$i]->add($w[$i - 7]);
$w[$i] = $w[$i]->add($s1);
}
// Initialize hash value for this chunk
$a = $hash[0]->copy();
$b = $hash[1]->copy();
$c = $hash[2]->copy();
$d = $hash[3]->copy();
$e = $hash[4]->copy();
$f = $hash[5]->copy();
$g = $hash[6]->copy();
$h = $hash[7]->copy();
// Main loop
for ($i = 0; $i < 80; $i++) {
$temp = array(
$a->bitwise_rightRotate(28),
$a->bitwise_rightRotate(34),
$a->bitwise_rightRotate(39)
);
$s0 = $temp[0]->bitwise_xor($temp[1]);
$s0 = $s0->bitwise_xor($temp[2]);
$temp = array(
$a->bitwise_and($b),
$a->bitwise_and($c),
$b->bitwise_and($c)
);
$maj = $temp[0]->bitwise_xor($temp[1]);
$maj = $maj->bitwise_xor($temp[2]);
$t2 = $s0->add($maj);
$temp = array(
$e->bitwise_rightRotate(14),
$e->bitwise_rightRotate(18),
$e->bitwise_rightRotate(41)
);
$s1 = $temp[0]->bitwise_xor($temp[1]);
$s1 = $s1->bitwise_xor($temp[2]);
$temp = array(
$e->bitwise_and($f),
$g->bitwise_and($e->bitwise_not())
);
$ch = $temp[0]->bitwise_xor($temp[1]);
$t1 = $h->add($s1);
$t1 = $t1->add($ch);
$t1 = $t1->add($k[$i]);
$t1 = $t1->add($w[$i]);
$h = $g->copy();
$g = $f->copy();
$f = $e->copy();
$e = $d->add($t1);
$d = $c->copy();
$c = $b->copy();
$b = $a->copy();
$a = $t1->add($t2);
}
// Add this chunk's hash to result so far
$hash = array(
$hash[0]->add($a),
$hash[1]->add($b),
$hash[2]->add($c),
$hash[3]->add($d),
$hash[4]->add($e),
$hash[5]->add($f),
$hash[6]->add($g),
$hash[7]->add($h)
);
}
// Produce the final hash value (big-endian)
// (\phpseclib\Crypt\Hash::hash() trims the output for hashes but not for HMACs. as such, we trim the output here)
$temp = $hash[0]->toBytes() . $hash[1]->toBytes() . $hash[2]->toBytes() . $hash[3]->toBytes() .
$hash[4]->toBytes() . $hash[5]->toBytes();
if ($this->l != 48) {
$temp.= $hash[6]->toBytes() . $hash[7]->toBytes();
}
return $temp;
}
/**
* Right Rotate
*
* @access private
* @param int $int
* @param int $amt
* @see self::_sha256()
* @return int
*/
function _rightRotate($int, $amt)
{
$invamt = 32 - $amt;
$mask = (1 << $invamt) - 1;
return (($int << $invamt) & 0xFFFFFFFF) | (($int >> $amt) & $mask);
}
/**
* Right Shift
*
* @access private
* @param int $int
* @param int $amt
* @see self::_sha256()
* @return int
*/
function _rightShift($int, $amt)
{
$mask = (1 << (32 - $amt)) - 1;
return ($int >> $amt) & $mask;
}
/**
* Not
*
* @access private
* @param int $int
* @see self::_sha256()
* @return int
*/
function _not($int)
{
return ~$int & 0xFFFFFFFF;
}
/**
* Add
*
* _sha256() adds multiple unsigned 32-bit integers. Since PHP doesn't support unsigned integers and since the
* possibility of overflow exists, care has to be taken. BigInteger could be used but this should be faster.
*
* @return int
* @see self::_sha256()
* @access private
*/
function _add()
{
static $mod;
if (!isset($mod)) {
$mod = pow(2, 32);
}
$result = 0;
$arguments = func_get_args();
foreach ($arguments as $argument) {
$result+= $argument < 0 ? ($argument & 0x7FFFFFFF) + 0x80000000 : $argument;
}
if ((php_uname('m') & "\xDF\xDF\xDF") != 'ARM') {
return fmod($result, $mod);
}
return (fmod($result, 0x80000000) & 0x7FFFFFFF) |
((fmod(floor($result / 0x80000000), 2) & 1) << 31);
}
/**
* String Shift
*
* Inspired by array_shift
*
* @param string $string
* @param int $index
* @return string
* @access private
*/
function _string_shift(&$string, $index = 1)
{
$substr = substr($string, 0, $index);
$string = substr($string, $index);
return $substr;
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,277 +0,0 @@
<?php
/**
* Random Number Generator
*
* PHP version 5
*
* Here's a short example of how to use this library:
* <code>
* <?php
* include 'vendor/autoload.php';
*
* echo bin2hex(\phpseclib\Crypt\Random::string(8));
* ?>
* </code>
*
* @category Crypt
* @package Random
* @author Jim Wigginton <terrafrost@php.net>
* @copyright 2007 Jim Wigginton
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @link http://phpseclib.sourceforge.net
*/
namespace phpseclib\Crypt;
/**
* Pure-PHP Random Number Generator
*
* @package Random
* @author Jim Wigginton <terrafrost@php.net>
* @access public
*/
class Random
{
/**
* Generate a random string.
*
* Although microoptimizations are generally discouraged as they impair readability this function is ripe with
* microoptimizations because this function has the potential of being called a huge number of times.
* eg. for RSA key generation.
*
* @param int $length
* @return string
*/
static function string($length)
{
if (!$length) {
return '';
}
if (version_compare(PHP_VERSION, '7.0.0', '>=')) {
try {
return \random_bytes($length);
} catch (\Throwable $e) {
// If a sufficient source of randomness is unavailable, random_bytes() will throw an
// object that implements the Throwable interface (Exception, TypeError, Error).
// We don't actually need to do anything here. The string() method should just continue
// as normal. Note, however, that if we don't have a sufficient source of randomness for
// random_bytes(), most of the other calls here will fail too, so we'll end up using
// the PHP implementation.
}
}
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
// method 1. prior to PHP 5.3 this would call rand() on windows hence the function_exists('class_alias') call.
// ie. class_alias is a function that was introduced in PHP 5.3
if (extension_loaded('mcrypt') && function_exists('class_alias')) {
return @mcrypt_create_iv($length);
}
// method 2. openssl_random_pseudo_bytes was introduced in PHP 5.3.0 but prior to PHP 5.3.4 there was,
// to quote <http://php.net/ChangeLog-5.php#5.3.4>, "possible blocking behavior". as of 5.3.4
// openssl_random_pseudo_bytes and mcrypt_create_iv do the exact same thing on Windows. ie. they both
// call php_win32_get_random_bytes():
//
// https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/ext/openssl/openssl.c#L5008
// https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/ext/mcrypt/mcrypt.c#L1392
//
// php_win32_get_random_bytes() is defined thusly:
//
// https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/win32/winutil.c#L80
//
// we're calling it, all the same, in the off chance that the mcrypt extension is not available
if (extension_loaded('openssl') && version_compare(PHP_VERSION, '5.3.4', '>=')) {
return openssl_random_pseudo_bytes($length);
}
} else {
// method 1. the fastest
if (extension_loaded('openssl')) {
return openssl_random_pseudo_bytes($length);
}
// method 2
static $fp = true;
if ($fp === true) {
// warning's will be output unles the error suppression operator is used. errors such as
// "open_basedir restriction in effect", "Permission denied", "No such file or directory", etc.
$fp = @fopen('/dev/urandom', 'rb');
}
if ($fp !== true && $fp !== false) { // surprisingly faster than !is_bool() or is_resource()
$temp = fread($fp, $length);
if (strlen($temp) == $length) {
return $temp;
}
}
// method 3. pretty much does the same thing as method 2 per the following url:
// https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/ext/mcrypt/mcrypt.c#L1391
// surprisingly slower than method 2. maybe that's because mcrypt_create_iv does a bunch of error checking that we're
// not doing. regardless, this'll only be called if this PHP script couldn't open /dev/urandom due to open_basedir
// restrictions or some such
if (extension_loaded('mcrypt')) {
return @mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
}
}
// at this point we have no choice but to use a pure-PHP CSPRNG
// cascade entropy across multiple PHP instances by fixing the session and collecting all
// environmental variables, including the previous session data and the current session
// data.
//
// mt_rand seeds itself by looking at the PID and the time, both of which are (relatively)
// easy to guess at. linux uses mouse clicks, keyboard timings, etc, as entropy sources, but
// PHP isn't low level to be able to use those as sources and on a web server there's not likely
// going to be a ton of keyboard or mouse action. web servers do have one thing that we can use
// however, a ton of people visiting the website. obviously you don't want to base your seeding
// soley on parameters a potential attacker sends but (1) not everything in $_SERVER is controlled
// by the user and (2) this isn't just looking at the data sent by the current user - it's based
// on the data sent by all users. one user requests the page and a hash of their info is saved.
// another user visits the page and the serialization of their data is utilized along with the
// server envirnment stuff and a hash of the previous http request data (which itself utilizes
// a hash of the session data before that). certainly an attacker should be assumed to have
// full control over his own http requests. he, however, is not going to have control over
// everyone's http requests.
static $crypto = false, $v;
if ($crypto === false) {
// save old session data
$old_session_id = session_id();
$old_use_cookies = ini_get('session.use_cookies');
$old_session_cache_limiter = session_cache_limiter();
$_OLD_SESSION = isset($_SESSION) ? $_SESSION : false;
if ($old_session_id != '') {
session_write_close();
}
session_id(1);
ini_set('session.use_cookies', 0);
session_cache_limiter('');
session_start();
$v = $seed = $_SESSION['seed'] = pack('H*', sha1(
(isset($_SERVER) ? phpseclib_safe_serialize($_SERVER) : '') .
(isset($_POST) ? phpseclib_safe_serialize($_POST) : '') .
(isset($_GET) ? phpseclib_safe_serialize($_GET) : '') .
(isset($_COOKIE) ? phpseclib_safe_serialize($_COOKIE) : '') .
phpseclib_safe_serialize($GLOBALS) .
phpseclib_safe_serialize($_SESSION) .
phpseclib_safe_serialize($_OLD_SESSION)
));
if (!isset($_SESSION['count'])) {
$_SESSION['count'] = 0;
}
$_SESSION['count']++;
session_write_close();
// restore old session data
if ($old_session_id != '') {
session_id($old_session_id);
session_start();
ini_set('session.use_cookies', $old_use_cookies);
session_cache_limiter($old_session_cache_limiter);
} else {
if ($_OLD_SESSION !== false) {
$_SESSION = $_OLD_SESSION;
unset($_OLD_SESSION);
} else {
unset($_SESSION);
}
}
// in SSH2 a shared secret and an exchange hash are generated through the key exchange process.
// the IV client to server is the hash of that "nonce" with the letter A and for the encryption key it's the letter C.
// if the hash doesn't produce enough a key or an IV that's long enough concat successive hashes of the
// original hash and the current hash. we'll be emulating that. for more info see the following URL:
//
// http://tools.ietf.org/html/rfc4253#section-7.2
//
// see the is_string($crypto) part for an example of how to expand the keys
$key = pack('H*', sha1($seed . 'A'));
$iv = pack('H*', sha1($seed . 'C'));
// ciphers are used as per the nist.gov link below. also, see this link:
//
// http://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator#Designs_based_on_cryptographic_primitives
switch (true) {
case class_exists('\phpseclib\Crypt\AES'):
$crypto = new AES(Base::MODE_CTR);
break;
case class_exists('\phpseclib\Crypt\Twofish'):
$crypto = new Twofish(Base::MODE_CTR);
break;
case class_exists('\phpseclib\Crypt\Blowfish'):
$crypto = new Blowfish(Base::MODE_CTR);
break;
case class_exists('\phpseclib\Crypt\TripleDES'):
$crypto = new TripleDES(Base::MODE_CTR);
break;
case class_exists('\phpseclib\Crypt\DES'):
$crypto = new DES(Base::MODE_CTR);
break;
case class_exists('\phpseclib\Crypt\RC4'):
$crypto = new RC4();
break;
default:
user_error(__CLASS__ . ' requires at least one symmetric cipher be loaded');
return false;
}
$crypto->setKey($key);
$crypto->setIV($iv);
$crypto->enableContinuousBuffer();
}
//return $crypto->encrypt(str_repeat("\0", $length));
// the following is based off of ANSI X9.31:
//
// http://csrc.nist.gov/groups/STM/cavp/documents/rng/931rngext.pdf
//
// OpenSSL uses that same standard for it's random numbers:
//
// http://www.opensource.apple.com/source/OpenSSL/OpenSSL-38/openssl/fips-1.0/rand/fips_rand.c
// (do a search for "ANS X9.31 A.2.4")
$result = '';
while (strlen($result) < $length) {
$i = $crypto->encrypt(microtime()); // strlen(microtime()) == 21
$r = $crypto->encrypt($i ^ $v); // strlen($v) == 20
$v = $crypto->encrypt($r ^ $i); // strlen($r) == 20
$result.= $r;
}
return substr($result, 0, $length);
}
}
if (!function_exists('phpseclib_safe_serialize')) {
/**
* Safely serialize variables
*
* If a class has a private __sleep() method it'll give a fatal error on PHP 5.2 and earlier.
* PHP 5.3 will emit a warning.
*
* @param mixed $arr
* @access public
*/
function phpseclib_safe_serialize(&$arr)
{
if (is_object($arr)) {
return '';
}
if (!is_array($arr)) {
return serialize($arr);
}
// prevent circular array recursion
if (isset($arr['__phpseclib_marker'])) {
return '';
}
$safearr = array();
$arr['__phpseclib_marker'] = true;
foreach (array_keys($arr) as $key) {
// do not recurse on the '__phpseclib_marker' key itself, for smaller memory usage
if ($key !== '__phpseclib_marker') {
$safearr[$key] = phpseclib_safe_serialize($arr[$key]);
}
}
unset($arr['__phpseclib_marker']);
return serialize($safearr);
}
}

View file

@ -1,577 +0,0 @@
<?php
/**
* Pure-PHP ANSI Decoder
*
* PHP version 5
*
* If you call read() in \phpseclib\Net\SSH2 you may get {@link http://en.wikipedia.org/wiki/ANSI_escape_code ANSI escape codes} back.
* They'd look like chr(0x1B) . '[00m' or whatever (0x1B = ESC). They tell a
* {@link http://en.wikipedia.org/wiki/Terminal_emulator terminal emulator} how to format the characters, what
* color to display them in, etc. \phpseclib\File\ANSI is a {@link http://en.wikipedia.org/wiki/VT100 VT100} terminal emulator.
*
* @category File
* @package ANSI
* @author Jim Wigginton <terrafrost@php.net>
* @copyright 2012 Jim Wigginton
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @link http://phpseclib.sourceforge.net
*/
namespace phpseclib\File;
/**
* Pure-PHP ANSI Decoder
*
* @package ANSI
* @author Jim Wigginton <terrafrost@php.net>
* @access public
*/
class ANSI
{
/**
* Max Width
*
* @var int
* @access private
*/
var $max_x;
/**
* Max Height
*
* @var int
* @access private
*/
var $max_y;
/**
* Max History
*
* @var int
* @access private
*/
var $max_history;
/**
* History
*
* @var array
* @access private
*/
var $history;
/**
* History Attributes
*
* @var array
* @access private
*/
var $history_attrs;
/**
* Current Column
*
* @var int
* @access private
*/
var $x;
/**
* Current Row
*
* @var int
* @access private
*/
var $y;
/**
* Old Column
*
* @var int
* @access private
*/
var $old_x;
/**
* Old Row
*
* @var int
* @access private
*/
var $old_y;
/**
* An empty attribute cell
*
* @var object
* @access private
*/
var $base_attr_cell;
/**
* The current attribute cell
*
* @var object
* @access private
*/
var $attr_cell;
/**
* An empty attribute row
*
* @var array
* @access private
*/
var $attr_row;
/**
* The current screen text
*
* @var array
* @access private
*/
var $screen;
/**
* The current screen attributes
*
* @var array
* @access private
*/
var $attrs;
/**
* Current ANSI code
*
* @var string
* @access private
*/
var $ansi;
/**
* Tokenization
*
* @var array
* @access private
*/
var $tokenization;
/**
* Default Constructor.
*
* @return \phpseclib\File\ANSI
* @access public
*/
function __construct()
{
$attr_cell = new \stdClass();
$attr_cell->bold = false;
$attr_cell->underline = false;
$attr_cell->blink = false;
$attr_cell->background = 'black';
$attr_cell->foreground = 'white';
$attr_cell->reverse = false;
$this->base_attr_cell = clone $attr_cell;
$this->attr_cell = clone $attr_cell;
$this->setHistory(200);
$this->setDimensions(80, 24);
}
/**
* Set terminal width and height
*
* Resets the screen as well
*
* @param int $x
* @param int $y
* @access public
*/
function setDimensions($x, $y)
{
$this->max_x = $x - 1;
$this->max_y = $y - 1;
$this->x = $this->y = 0;
$this->history = $this->history_attrs = array();
$this->attr_row = array_fill(0, $this->max_x + 2, $this->base_attr_cell);
$this->screen = array_fill(0, $this->max_y + 1, '');
$this->attrs = array_fill(0, $this->max_y + 1, $this->attr_row);
$this->ansi = '';
}
/**
* Set the number of lines that should be logged past the terminal height
*
* @param int $history
* @access public
*/
function setHistory($history)
{
$this->max_history = $history;
}
/**
* Load a string
*
* @param string $source
* @access public
*/
function loadString($source)
{
$this->setDimensions($this->max_x + 1, $this->max_y + 1);
$this->appendString($source);
}
/**
* Appdend a string
*
* @param string $source
* @access public
*/
function appendString($source)
{
$this->tokenization = array('');
for ($i = 0; $i < strlen($source); $i++) {
if (strlen($this->ansi)) {
$this->ansi.= $source[$i];
$chr = ord($source[$i]);
// http://en.wikipedia.org/wiki/ANSI_escape_code#Sequence_elements
// single character CSI's not currently supported
switch (true) {
case $this->ansi == "\x1B=":
$this->ansi = '';
continue 2;
case strlen($this->ansi) == 2 && $chr >= 64 && $chr <= 95 && $chr != ord('['):
case strlen($this->ansi) > 2 && $chr >= 64 && $chr <= 126:
break;
default:
continue 2;
}
$this->tokenization[] = $this->ansi;
$this->tokenization[] = '';
// http://ascii-table.com/ansi-escape-sequences-vt-100.php
switch ($this->ansi) {
case "\x1B[H": // Move cursor to upper left corner
$this->old_x = $this->x;
$this->old_y = $this->y;
$this->x = $this->y = 0;
break;
case "\x1B[J": // Clear screen from cursor down
$this->history = array_merge($this->history, array_slice(array_splice($this->screen, $this->y + 1), 0, $this->old_y));
$this->screen = array_merge($this->screen, array_fill($this->y, $this->max_y, ''));
$this->history_attrs = array_merge($this->history_attrs, array_slice(array_splice($this->attrs, $this->y + 1), 0, $this->old_y));
$this->attrs = array_merge($this->attrs, array_fill($this->y, $this->max_y, $this->attr_row));
if (count($this->history) == $this->max_history) {
array_shift($this->history);
array_shift($this->history_attrs);
}
case "\x1B[K": // Clear screen from cursor right
$this->screen[$this->y] = substr($this->screen[$this->y], 0, $this->x);
array_splice($this->attrs[$this->y], $this->x + 1, $this->max_x - $this->x, array_fill($this->x, $this->max_x - ($this->x - 1), $this->base_attr_cell));
break;
case "\x1B[2K": // Clear entire line
$this->screen[$this->y] = str_repeat(' ', $this->x);
$this->attrs[$this->y] = $this->attr_row;
break;
case "\x1B[?1h": // set cursor key to application
case "\x1B[?25h": // show the cursor
case "\x1B(B": // set united states g0 character set
break;
case "\x1BE": // Move to next line
$this->_newLine();
$this->x = 0;
break;
default:
switch (true) {
case preg_match('#\x1B\[(\d+)B#', $this->ansi, $match): // Move cursor down n lines
$this->old_y = $this->y;
$this->y+= $match[1];
break;
case preg_match('#\x1B\[(\d+);(\d+)H#', $this->ansi, $match): // Move cursor to screen location v,h
$this->old_x = $this->x;
$this->old_y = $this->y;
$this->x = $match[2] - 1;
$this->y = $match[1] - 1;
break;
case preg_match('#\x1B\[(\d+)C#', $this->ansi, $match): // Move cursor right n lines
$this->old_x = $this->x;
$this->x+= $match[1];
break;
case preg_match('#\x1B\[(\d+)D#', $this->ansi, $match): // Move cursor left n lines
$this->old_x = $this->x;
$this->x-= $match[1];
if ($this->x < 0) {
$this->x = 0;
}
break;
case preg_match('#\x1B\[(\d+);(\d+)r#', $this->ansi, $match): // Set top and bottom lines of a window
break;
case preg_match('#\x1B\[(\d*(?:;\d*)*)m#', $this->ansi, $match): // character attributes
$attr_cell = &$this->attr_cell;
$mods = explode(';', $match[1]);
foreach ($mods as $mod) {
switch ($mod) {
case '':
case '0': // Turn off character attributes
$attr_cell = clone $this->base_attr_cell;
break;
case '1': // Turn bold mode on
$attr_cell->bold = true;
break;
case '4': // Turn underline mode on
$attr_cell->underline = true;
break;
case '5': // Turn blinking mode on
$attr_cell->blink = true;
break;
case '7': // Turn reverse video on
$attr_cell->reverse = !$attr_cell->reverse;
$temp = $attr_cell->background;
$attr_cell->background = $attr_cell->foreground;
$attr_cell->foreground = $temp;
break;
default: // set colors
//$front = $attr_cell->reverse ? &$attr_cell->background : &$attr_cell->foreground;
$front = &$attr_cell->{ $attr_cell->reverse ? 'background' : 'foreground' };
//$back = $attr_cell->reverse ? &$attr_cell->foreground : &$attr_cell->background;
$back = &$attr_cell->{ $attr_cell->reverse ? 'foreground' : 'background' };
switch ($mod) {
// @codingStandardsIgnoreStart
case '30': $front = 'black'; break;
case '31': $front = 'red'; break;
case '32': $front = 'green'; break;
case '33': $front = 'yellow'; break;
case '34': $front = 'blue'; break;
case '35': $front = 'magenta'; break;
case '36': $front = 'cyan'; break;
case '37': $front = 'white'; break;
case '40': $back = 'black'; break;
case '41': $back = 'red'; break;
case '42': $back = 'green'; break;
case '43': $back = 'yellow'; break;
case '44': $back = 'blue'; break;
case '45': $back = 'magenta'; break;
case '46': $back = 'cyan'; break;
case '47': $back = 'white'; break;
// @codingStandardsIgnoreEnd
default:
//user_error('Unsupported attribute: ' . $mod);
$this->ansi = '';
break 2;
}
}
}
break;
default:
//user_error("{$this->ansi} is unsupported\r\n");
}
}
$this->ansi = '';
continue;
}
$this->tokenization[count($this->tokenization) - 1].= $source[$i];
switch ($source[$i]) {
case "\r":
$this->x = 0;
break;
case "\n":
$this->_newLine();
break;
case "\x08": // backspace
if ($this->x) {
$this->x--;
$this->attrs[$this->y][$this->x] = clone $this->base_attr_cell;
$this->screen[$this->y] = substr_replace(
$this->screen[$this->y],
$source[$i],
$this->x,
1
);
}
break;
case "\x0F": // shift
break;
case "\x1B": // start ANSI escape code
$this->tokenization[count($this->tokenization) - 1] = substr($this->tokenization[count($this->tokenization) - 1], 0, -1);
//if (!strlen($this->tokenization[count($this->tokenization) - 1])) {
// array_pop($this->tokenization);
//}
$this->ansi.= "\x1B";
break;
default:
$this->attrs[$this->y][$this->x] = clone $this->attr_cell;
if ($this->x > strlen($this->screen[$this->y])) {
$this->screen[$this->y] = str_repeat(' ', $this->x);
}
$this->screen[$this->y] = substr_replace(
$this->screen[$this->y],
$source[$i],
$this->x,
1
);
if ($this->x > $this->max_x) {
$this->x = 0;
$this->_newLine();
} else {
$this->x++;
}
}
}
}
/**
* Add a new line
*
* Also update the $this->screen and $this->history buffers
*
* @access private
*/
function _newLine()
{
//if ($this->y < $this->max_y) {
// $this->y++;
//}
while ($this->y >= $this->max_y) {
$this->history = array_merge($this->history, array(array_shift($this->screen)));
$this->screen[] = '';
$this->history_attrs = array_merge($this->history_attrs, array(array_shift($this->attrs)));
$this->attrs[] = $this->attr_row;
if (count($this->history) >= $this->max_history) {
array_shift($this->history);
array_shift($this->history_attrs);
}
$this->y--;
}
$this->y++;
}
/**
* Returns the current coordinate without preformating
*
* @access private
* @return string
*/
function _processCoordinate($last_attr, $cur_attr, $char)
{
$output = '';
if ($last_attr != $cur_attr) {
$close = $open = '';
if ($last_attr->foreground != $cur_attr->foreground) {
if ($cur_attr->foreground != 'white') {
$open.= '<span style="color: ' . $cur_attr->foreground . '">';
}
if ($last_attr->foreground != 'white') {
$close = '</span>' . $close;
}
}
if ($last_attr->background != $cur_attr->background) {
if ($cur_attr->background != 'black') {
$open.= '<span style="background: ' . $cur_attr->background . '">';
}
if ($last_attr->background != 'black') {
$close = '</span>' . $close;
}
}
if ($last_attr->bold != $cur_attr->bold) {
if ($cur_attr->bold) {
$open.= '<b>';
} else {
$close = '</b>' . $close;
}
}
if ($last_attr->underline != $cur_attr->underline) {
if ($cur_attr->underline) {
$open.= '<u>';
} else {
$close = '</u>' . $close;
}
}
if ($last_attr->blink != $cur_attr->blink) {
if ($cur_attr->blink) {
$open.= '<blink>';
} else {
$close = '</blink>' . $close;
}
}
$output.= $close . $open;
}
$output.= htmlspecialchars($char);
return $output;
}
/**
* Returns the current screen without preformating
*
* @access private
* @return string
*/
function _getScreen()
{
$output = '';
$last_attr = $this->base_attr_cell;
for ($i = 0; $i <= $this->max_y; $i++) {
for ($j = 0; $j <= $this->max_x; $j++) {
$cur_attr = $this->attrs[$i][$j];
$output.= $this->_processCoordinate($last_attr, $cur_attr, isset($this->screen[$i][$j]) ? $this->screen[$i][$j] : '');
$last_attr = $this->attrs[$i][$j];
}
$output.= "\r\n";
}
$output = substr($output, 0, -2);
// close any remaining open tags
$output.= $this->_processCoordinate($last_attr, $this->base_attr_cell, '');
return rtrim($output);
}
/**
* Returns the current screen
*
* @access public
* @return string
*/
function getScreen()
{
return '<pre width="' . ($this->max_x + 1) . '" style="color: white; background: black">' . $this->_getScreen() . '</pre>';
}
/**
* Returns the current screen and the x previous lines
*
* @access public
* @return string
*/
function getHistory()
{
$scrollback = '';
$last_attr = $this->base_attr_cell;
for ($i = 0; $i < count($this->history); $i++) {
for ($j = 0; $j <= $this->max_x + 1; $j++) {
$cur_attr = $this->history_attrs[$i][$j];
$scrollback.= $this->_processCoordinate($last_attr, $cur_attr, isset($this->history[$i][$j]) ? $this->history[$i][$j] : '');
$last_attr = $this->history_attrs[$i][$j];
}
$scrollback.= "\r\n";
}
$base_attr_cell = $this->base_attr_cell;
$this->base_attr_cell = $last_attr;
$scrollback.= $this->_getScreen();
$this->base_attr_cell = $base_attr_cell;
return '<pre width="' . ($this->max_x + 1) . '" style="color: white; background: black">' . $scrollback . '</span></pre>';
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,47 +0,0 @@
<?php
/**
* Pure-PHP ASN.1 Parser
*
* PHP version 5
*
* @category File
* @package ASN1
* @author Jim Wigginton <terrafrost@php.net>
* @copyright 2012 Jim Wigginton
* @license http://www.opensource.org/licenses/mit-license.html MIT License
* @link http://phpseclib.sourceforge.net
*/
namespace phpseclib\File\ASN1;
/**
* ASN.1 Element
*
* Bypass normal encoding rules in phpseclib\File\ASN1::encodeDER()
*
* @package ASN1
* @author Jim Wigginton <terrafrost@php.net>
* @access public
*/
class Element
{
/**
* Raw element value
*
* @var string
* @access private
*/
var $element;
/**
* Constructor
*
* @param string $encoded
* @return \phpseclib\File\ASN1\Element
* @access public
*/
function __construct($encoded)
{
$this->element = $encoded;
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,144 +0,0 @@
<?php
define('PLUGINS_DEST_DIR', __DIR__ . '/dist/releases/plugins');
is_dir(PLUGINS_DEST_DIR) || mkdir(PLUGINS_DEST_DIR, 0777, true);
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(PLUGINS_DEST_DIR, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($files as $fileinfo) {
$fileinfo->isDir() || unlink($fileinfo->getRealPath());
}
$terser = ROOT_DIR . '/node_modules/terser/bin/terser';
$manifest = [];
require ROOT_DIR . '/snappymail/v/0.0.0/app/libraries/RainLoop/Plugins/AbstractPlugin.php';
$keys = [
'author',
'category',
'description',
'file',
'id',
'license',
'name',
'release',
'required',
'type',
'url',
'version'
];
/*
$released = [
'add-x-originating-ip-header',
'avatars',
'backup',
'black-list',
'change-password',
'change-password-froxlor',
'change-password-hestia',
'change-password-hmailserver',
'change-password-ispconfig',
'change-password-poppassd',
'custom-login-mapping',
'imap-contacts-suggestions',
'kolab',
'ldap-contacts-suggestions',
'ldap-identities',
'ldap-login-mapping',
'ldap-mail-accounts',
'login-external',
'login-external-sso',
'login-override',
'login-register',
'login-remote',
'mailbox-detect',
'nextcloud',
'override-smtp-credentials',
'set-remote-addr',
'smtp-use-from-adr-account',
'snowfall-on-login-screen',
'two-factor-auth',
'view-ics',
'white-list'
];
*/
foreach (glob(ROOT_DIR . '/plugins/*', GLOB_NOSORT | GLOB_ONLYDIR) as $dir) {
if (is_file("{$dir}/index.php") && !strpos($dir, '.bak')) {
require "{$dir}/index.php";
$name = basename($dir);
// if (!in_array($name, $released)) continue;
$class = new ReflectionClass(str_replace('-', '', $name) . 'Plugin');
$manifest_item = [];
foreach ($class->getConstants() as $key => $value) {
$key = \strtolower($key);
if (in_array($key, $keys)) {
$manifest_item[$key] = $value;
}
}
$version = $manifest_item['version'];
if (0 < floatval($version)) {
echo "+ {$name} {$version}\n";
// Minify JavaScript
foreach (glob("{$dir}/*.js") as $file) {
if (!strpos($file,'.min')) {
$mfile = str_replace('.js', '.min.js', $file);
passthru("{$terser} {$file} --output {$mfile} --compress 'drop_console' --ecma 6 --mangle");
}
}
foreach (glob("{$dir}/js/*.js") as $file) {
if (!strpos($file,'.min')) {
$mfile = str_replace('.js', '.min.js', $file);
passthru("{$terser} {$file} --output {$mfile} --compress 'drop_console' --ecma 6 --mangle");
}
}
$manifest_item['type'] = 'plugin';
$manifest_item['id'] = $name;
$manifest_item['file'] = "plugins/{$name}-{$version}.tgz";
$tar_destination = PLUGINS_DEST_DIR . "/{$name}-{$version}.tar";
$tgz_destination = PLUGINS_DEST_DIR . "/{$name}-{$version}.tgz";
@unlink($tgz_destination);
@unlink("{$tar_destination}.gz");
$tar = new PharData($tar_destination);
$tar->buildFromDirectory('./plugins/', '/' . \preg_quote("./plugins/{$name}/", '/') . '((?!\.bak).)*$/');
$tar->compress(Phar::GZ);
unlink($tar_destination);
rename("{$tar_destination}.gz", $tgz_destination);
/*
if (Phar::canWrite()) {
$phar_destination = PLUGINS_DEST_DIR . "/{$name}.phar";
@unlink($phar_destination);
$tar = new Phar($phar_destination);
$tar->buildFromDirectory("./plugins/{$name}/", '/^((?!\.bak).)*$/');
$tar->compress(Phar::GZ);
unlink($phar_destination);
rename("{$phar_destination}.gz", $phar_destination);
}
*/
if (isset($options['sign'])) {
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '.escapeshellarg($tgz_destination), $return_var);
$manifest_item['pgp_sig'] = trim(preg_replace('/-----(BEGIN|END) PGP SIGNATURE-----/', '', file_get_contents($tgz_destination.'.asc')));
}
ksort($manifest_item);
$manifest[$name] = $manifest_item;
} else {
echo "- {$name} {$version}\n";
}
} else {
echo "- {$name}\n";
}
}
ksort($manifest);
$manifest = json_encode(array_values($manifest));
$manifest = str_replace('{"', "\n\t{\n\t\t\"", $manifest);
$manifest = str_replace('"}', "\"\n\t}", $manifest);
$manifest = str_replace('}]', "}\n]", $manifest);
$manifest = str_replace('","', "\",\n\t\t\"", $manifest);
$manifest = str_replace('\/', '/', $manifest);
file_put_contents(dirname(PLUGINS_DEST_DIR) . "/packages.json", $manifest);
exit;

View file

@ -1,280 +0,0 @@
#!/usr/bin/php
<?php
define('ROOT_DIR', dirname(__DIR__));
chdir(ROOT_DIR);
$options = getopt('', ['aur','docker','plugins','skip-gulp','debian','nextcloud','owncloud','cpanel','sign']);
if (isset($options['plugins'])) {
require(ROOT_DIR . '/build/plugins.php');
}
$gulp = trim(shell_exec('which gulp'));
if (!$gulp) {
exit('gulp not installed, run as root: npm install --global gulp-cli');
}
$package = json_decode(file_get_contents('package.json'));
/**
* Update files that contain version
*/
// cloudron
$file = ROOT_DIR . '/integrations/cloudron/Dockerfile';
file_put_contents($file, preg_replace('/VERSION=[0-9.]+/', "VERSION={$package->version}", file_get_contents($file)));
$file = ROOT_DIR . '/integrations/cloudron/DESCRIPTION.md';
file_put_contents($file, preg_replace('/<upstream>[^<]*</', "<upstream>{$package->version}<", file_get_contents($file)));
// virtualmin
$file = ROOT_DIR . '/integrations/virtualmin/snappymail.pl';
file_put_contents($file, preg_replace('/return \\( "[0-9]+\\.[0-9]+\\.[0-9]+" \\)/', "return ( \"{$package->version}\" )", file_get_contents($file)));
// Arch User Repository
// https://aur.archlinux.org/packages/snappymail/
$options['aur'] = isset($options['aur']);
// Docker build
$options['docker'] = isset($options['docker']);
if ($options['docker'] && !($docker = trim(shell_exec('which docker')))) {
exit('docker not found');
}
if ($options['docker'] && $options['aur']) {
exit('Conflict between docker and aur');
}
$destPath = "build/dist/releases/webmail/{$package->version}/";
is_dir($destPath) || mkdir($destPath, 0777, true);
$zip_destination = "{$destPath}snappymail-{$package->version}.zip";
$tar_destination = "{$destPath}snappymail-{$package->version}.tar";
@unlink($zip_destination);
@unlink($tar_destination);
@unlink("{$tar_destination}.gz");
if (!isset($options['skip-gulp'])) {
echo "\x1b[33;1m === Gulp === \x1b[0m\n";
passthru($gulp, $return_var);
if ($return_var) {
exit("gulp failed with error code {$return_var}\n");
}
$cmddir = escapeshellcmd(ROOT_DIR) . '/snappymail/v/0.0.0/static';
if ($gzip = trim(shell_exec('which gzip'))) {
echo "\x1b[33;1m === Gzip *.js and *.css === \x1b[0m\n";
passthru("{$gzip} -k --best {$cmddir}/js/*.js");
passthru("{$gzip} -k --best {$cmddir}/js/min/*.js");
passthru("{$gzip} -k --best {$cmddir}/css/admin*.css");
passthru("{$gzip} -k --best {$cmddir}/css/app*.css");
unlink(ROOT_DIR . '/snappymail/v/0.0.0/static/js/boot.js.gz');
unlink(ROOT_DIR . '/snappymail/v/0.0.0/static/js/min/boot.min.js.gz');
}
if ($brotli = trim(shell_exec('which brotli'))) {
echo "\x1b[33;1m === Brotli *.js and *.css === \x1b[0m\n";
passthru("{$brotli} -k --best {$cmddir}/js/*.js");
passthru("{$brotli} -k --best {$cmddir}/js/min/*.js");
passthru("{$brotli} -k --best {$cmddir}/css/admin*.css");
passthru("{$brotli} -k --best {$cmddir}/css/app*.css");
unlink(ROOT_DIR . '/snappymail/v/0.0.0/static/js/boot.js.br');
unlink(ROOT_DIR . '/snappymail/v/0.0.0/static/js/min/boot.min.js.br');
}
}
// Temporary rename folder to speed up PharData
//if (!rename('snappymail/v/0.0.0', "snappymail/v/{$package->version}")){
if (!rename('snappymail/v/0.0.0', "snappymail/v/{$package->version}")) {
exit('Failed to temporary rename snappymail/v/0.0.0');
}
register_shutdown_function(function(){
// Rename folder back to original
@rename("snappymail/v/{$GLOBALS['package']->version}", 'snappymail/v/0.0.0');
});
echo "\x1b[33;1m === Zip/Tar === \x1b[0m\n";
$zip = new ZipArchive();
if (!$zip->open($zip_destination, ZIPARCHIVE::CREATE)) {
exit("Failed to create {$zip_destination}");
}
$tar = new PharData($tar_destination);
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('snappymail/v'), RecursiveIteratorIterator::SELF_FIRST);
foreach ($files as $file) {
$file = str_replace('\\', '/', $file);
//echo "{$file}\n";
// Ignore "." and ".." folders
if (!in_array(substr($file, strrpos($file, '/')+1), array('.', '..'))) {
if (is_dir($file)) {
$zip->addEmptyDir($file);
} else if (is_file($file)) {
$zip->addFile($file);
}
}
}
if ($options['docker']) {
$tar->buildFromDirectory('./snappymail/', "@v/{$package->version}@");
} else {
$tar->buildFromDirectory('./', "@snappymail/v/{$package->version}@");
}
//$zip->addFile('cli/upgrade.sh');
//$tar->addFile('cli/upgrade.sh');
$zip->addFile('data/.htaccess');
$tar->addFile('data/.htaccess');
$zip->addFromString('data/VERSION', $package->version);
$tar->addFromString('data/VERSION', $package->version);
$zip->addFile('data/README.md');
$tar->addFile('data/README.md');
if ($options['aur']) {
$data = '<?php
function __get_custom_data_full_path()
{
return \'/var/lib/snappymail\';
}
';
$zip->addFromString('include.php', $data);
$tar->addFromString('include.php', $data);
} else {
$zip->addFile('_include.php');
$tar->addFile('_include.php');
}
$zip->addFile('.htaccess');
$tar->addFile('.htaccess');
$index = file_get_contents('index.php');
$index = str_replace('0.0.0', $package->version, $index);
$zip->addFromString('index.php', $index);
$tar->addFromString('index.php', $index);
$zip->addFile('README.md');
$tar->addFile('README.md');
$zip->close();
$tar->compress(Phar::GZ);
unlink($tar_destination);
$tar_destination .= '.gz';
echo "{$zip_destination} created\n{$tar_destination} created\n";
if (isset($options['nextcloud'])) {
require(ROOT_DIR . '/build/nextcloud.php');
}
if (isset($options['owncloud'])) {
require(ROOT_DIR . '/build/owncloud.php');
}
if (isset($options['cpanel'])) {
require(ROOT_DIR . '/build/cpanel.php');
}
rename("snappymail/v/{$package->version}", 'snappymail/v/0.0.0');
file_put_contents("{$destPath}core.json", '{
"version": "'.$package->version.'",
"file": "../latest.tar.gz",
"warnings": []
}');
// Arch User Repository
if ($options['aur']) {
// extension_loaded('blake2')
if (!function_exists('b2sum') && $b2sum = trim(shell_exec('which b2sum'))) {
function b2sum($file) {
$file = escapeshellarg($file);
exec("b2sum --binary {$file} 2>&1", $output, $exitcode);
$output = explode(' ', implode("\n", $output));
return $output[0];
}
}
$b2sums = function_exists('b2sum') ? [
b2sum($tar_destination),
b2sum(ROOT_DIR . '/build/arch/snappymail.sysusers'),
b2sum(ROOT_DIR . '/build/arch/snappymail.tmpfiles')
] : [];
file_put_contents('build/arch/.SRCINFO', 'pkgbase = snappymail
pkgdesc = modern PHP webmail client
pkgver = '.$package->version.'
pkgrel = 1
url = https://github.com/the-djmaze/snappymail
arch = any
license = AGPL3
makedepends = php
makedepends = nodejs
makedepends = yarn
makedepends = gulp
depends = php-fpm
optdepends = mariadb: storage backend for contacts
optdepends = php-pgsql: storage backend for contacts
optdepends = php-sqlite: storage backend for contacts
source = snappymail-'.$package->version.'.tar.gz::https://github.com/the-djmaze/snappymail/archive/v'.$package->version.'.tar.gz
source = snappymail.sysusers
source = snappymail.tmpfiles
b2sums = '.implode("\n b2sums = ", $b2sums).'
pkgname = snappymail
');
$file = ROOT_DIR . '/build/arch/PKGBUILD';
if (is_file($file)) {
$PKGBUILD = file_get_contents($file);
$PKGBUILD = preg_replace('/pkgver=[0-9.]+/', "pkgver={$package->version}", $PKGBUILD);
$PKGBUILD = preg_replace('/b2sums=\\([^)]+\\)/s', "b2sums=('".implode("'\n '", $b2sums)."')", $PKGBUILD);
file_put_contents($file, $PKGBUILD);
}
}
// Debian Repository
else if (isset($options['debian'])) {
require(ROOT_DIR . '/build/deb.php');
}
// Docker build
else if ($options['docker']) {
echo "\x1b[33;1m === Docker === \x1b[0m\n";
$zip_filename = "snappymail-{$package->version}.zip";
copy($zip_destination, "./.docker/release/{$zip_filename}");
if ($docker) {
passthru("{$docker} build --pull " . ROOT_DIR . "/.docker/release/ --build-arg FILES_ZIP={$zip_filename} -t snappymail:{$package->version}");
} else {
echo "Docker not installed!\n";
}
}
if (isset($options['sign'])) {
echo "\x1b[33;1m === PGP Sign === \x1b[0m\n";
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '.escapeshellarg($tar_destination), $return_var);
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '.escapeshellarg($zip_destination), $return_var);
if (isset($options['nextcloud'])) {
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '
.escapeshellarg("{$destPath}snappymail-{$package->version}-nextcloud.tar.gz"), $return_var);
}
if (isset($options['owncloud'])) {
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '
.escapeshellarg("{$destPath}snappymail-{$package->version}-owncloud.tar.gz"), $return_var);
}
if (isset($options['cpanel'])) {
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '
.escapeshellarg("{$destPath}snappymail-{$package->version}-cpanel.tar.gz"), $return_var);
}
if (isset($options['debian'])) {
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --armor --detach-sign '
. escapeshellarg(ROOT_DIR . "/build/dist/releases/webmail/{$package->version}/" . basename(DEB_DEST_DIR.'.deb')), $return_var);
// https://github.com/the-djmaze/snappymail/issues/185#issuecomment-1059420588
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --digest-algo SHA512 --clearsign --output '
. escapeshellarg(ROOT_DIR . "/build/dist/releases/webmail/{$package->version}/InRelease") . ' '
. escapeshellarg(ROOT_DIR . "/build/dist/releases/webmail/{$package->version}/Release"), $return_var);
passthru('gpg --local-user 1016E47079145542F8BA133548208BA13290F3EB --digest-algo SHA512 -abs --output '
. escapeshellarg(ROOT_DIR . "/build/dist/releases/webmail/{$package->version}/Release.gpg") . ' '
. escapeshellarg(ROOT_DIR . "/build/dist/releases/webmail/{$package->version}/Release"), $return_var);
}
}

View file

@ -1,51 +0,0 @@
#!/bin/bash
# UPGRADING Snappymail
#
# I've found this to be working fine when upgrading my Snappymail. The
# commands should be used at your own risk. I take no responsibility.
#
# The script can only be run from the SnappyMail install directory!
#
# By Uwe Bieling <pychi@gmx.de>
# and Jordan S (https://github.com/jas8522)
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
LATEST_URL="https://snappymail.eu/repository/latest.tar.gz"
cd "${SCRIPT_DIR}"
OWNERGROUP=`stat -c "%U:%G" snappymail`
OLD_VERSION=`grep "define('APP_VERSION" index.php | awk -F\' '{print $4}'`
# check if we are root
if [[ $(id -u) -ne 0 ]] ; then echo -e "\033[1;31mPlease run as root\033[0m" ; exit 1 ; fi
# check if we are in the correct dir
if [ ! -d snappymail/v ] ; then echo -e "\033[1;31mThis script can only be run from the SnappyMail install directory\033[0m" ; exit 1 ; fi
# Download last release to /tmp
echo -e "\033[1;33mDownloading last release\033[0m"
wget $LATEST_URL -O /tmp/snappymail_latest.tar.gz >/dev/null 2>&1
NEW_VERSION=`tar -tf /tmp/snappymail_latest.tar.gz | grep "snappymail/v/.*/index.php" | awk -F/ '{print $3}'`
if [ $OLD_VERSION != $NEW_VERSION ] ;
then
# Safety First ... make a backup
DSTAMP=`date +%Y-%m-%d`
echo -e "\033[1;33mBacking up snappymail $OLD_VERSION to ../backup_snappymail_${DSTAMP}.tar.gz\033[0m"
tar -czf ../backup_snappymail_${DSTAMP}.tar.gz .
# installing
echo -e "\033[1;33mInstalling $NEW_VERSION\033[0m";
tar -xzf /tmp/snappymail_latest.tar.gz;
# set permissions
echo -e "\033[1;33mSet permissions\033[0m"
find . -type d -exec chmod 755 {} \;
find . -type f -exec chmod 644 {} \;
chmod u+x "${SCRIPT_DIR}/upgrade.sh"
chown -R $OWNERGROUP $SCRIPT_DIR
echo -e "\033[1;32mFinished with snappymail upgrade from $OLD_VERSION to $NEW_VERSION... \033[0m"
else
echo -e "\033[1;31msame Version skipping.. \033[0m";
fi

View file

@ -1,2 +0,0 @@
You can move this folder outside public http access for security.
Edit and rename /_include.php to set the new location.

View file

@ -1,56 +1,147 @@
import ko from 'ko';
import { logoutLink } from 'Common/Links';
import { i18nToNodes, initOnStartOrLangChange } from 'Common/Translator';
import {
$htmlCL,
leftPanelDisabled,
leftPanelType,
bMobileDevice
} from 'Common/Globals';
import { arePopupsVisible } from 'Knoin/Knoin';
import { KeyState } from 'Common/Enums';
import { rootAdmin, rootUser } from 'Common/Links';
import { initOnStartOrLangChange, initNotificationLanguage } from 'Common/Translator';
import { LanguageStore } from 'Stores/Language';
import { initThemes } from 'Stores/Theme';
import LanguageStore from 'Stores/Language';
import ThemeStore from 'Stores/Theme';
import { SelectComponent } from 'Component/Select';
import { CheckboxComponent } from 'Component/Checkbox';
const Settings = rl.settings;
export class AbstractApp {
class AbstractApp {
/**
* @param {RemoteStorage|AdminRemoteStorage} Remote
*/
constructor(Remote) {
this.Remote = Remote;
constructor() {
this.isLocalAutocomplete = true;
this.lastErrorTime = 0;
const $doc = document;
$doc.addEventListener('keydown', (event) => {
if (event && event.ctrlKey) {
$htmlCL.add('rl-ctrl-key-pressed');
}
});
$doc.addEventListener('keyup', (event) => {
if (event && !event.ctrlKey) {
$htmlCL.remove('rl-ctrl-key-pressed');
}
});
const fn = (()=>dispatchEvent(new CustomEvent('rl.auto-logout-refresh'))).debounce(5000);
$doc.addEventListener('mousemove', fn);
$doc.addEventListener('keypress', fn);
$doc.addEventListener('click', fn);
shortcuts.add('escape,enter', '', KeyState.All, () => rl.Dropdowns.detectVisibility());
}
logoutReload(url) {
arePopupsVisible(false);
url = url || logoutLink();
if (location.href !== url) {
setTimeout(() => location.href = url, 100);
remote() {
return null;
}
data() {
return null;
}
/**
* @param {string} link
* @returns {boolean}
*/
download(link) {
if (bMobileDevice) {
open(link, '_self');
focus();
} else {
const oLink = document.createElement('a');
oLink.href = link;
document.body.appendChild(oLink).click();
oLink.remove();
}
return true;
}
/**
* @param {string} token
*/
setClientSideToken(token) {
rl.hash.set();
Settings.set('AuthAccountHash', token);
}
logoutReload(close = false) {
const logoutLink = rl.adminArea() ? rootAdmin() : rootUser();
rl.hash.clear();
close && window.close && window.close();
if (location.href !== logoutLink) {
setTimeout(() => (Settings.app('inIframe') ? parent : window).location.href = logoutLink, 100);
} else {
rl.route.reload();
}
// this does not work due to ViewModelClass.__builded = true;
// rl.settings.set('Auth', false);
// rl.app.start();
}
bootstart() {
const register = (name, ClassObject) => ko.components.register(name, {
template: { element: ClassObject.name },
viewModel: {
createViewModel: (params, componentInfo) => {
params = params || {};
i18nToNodes(componentInfo.element);
return new ClassObject(params);
}
}
});
register('Select', SelectComponent);
register('Checkbox', CheckboxComponent);
const mobile = Settings.app('mobile');
initOnStartOrLangChange();
ko.components.register('SaveTrigger', require('Component/SaveTrigger').default);
ko.components.register('Input', require('Component/Input').default);
ko.components.register('Select', require('Component/Select').default);
ko.components.register('TextArea', require('Component/TextArea').default);
if (Settings.app('materialDesign') && !bMobileDevice) {
ko.components.register('Checkbox', require('Component/MaterialDesign/Checkbox').default);
ko.components.register('CheckboxSimple', require('Component/Checkbox').default);
} else {
ko.components.register('Checkbox', require('Component/Checkbox').default);
ko.components.register('CheckboxSimple', require('Component/Checkbox').default);
}
initOnStartOrLangChange(initNotificationLanguage);
if (!mobile) {
// mobile
window.addEventListener('resize', () => leftPanelDisabled(767 >= window.innerWidth));
} else {
$htmlCL.add('rl-mobile');
leftPanelDisabled(true);
}
leftPanelDisabled.subscribe((bValue) => {
$htmlCL.toggle('rl-left-panel-disabled', bValue);
$htmlCL.toggle('rl-left-panel-enabled', !bValue);
});
leftPanelType.subscribe((sValue) => {
$htmlCL.toggle('rl-left-panel-none', 'none' === sValue);
$htmlCL.toggle('rl-left-panel-short', 'short' === sValue);
});
leftPanelDisabled.valueHasMutated();
LanguageStore.populate();
initThemes();
this.start();
ThemeStore.populate();
}
/**
* @returns {void}
*/
hideLoading() {
const id = id => document.getElementById(id);
id('rl-content').hidden = false;
id('rl-loading').remove();
}
}
export { AbstractApp, AbstractApp as default };

View file

@ -1,8 +1,12 @@
import 'External/ko';
import ko from 'External/Admin/ko';
import { SettingsGet, SettingsAdmin } from 'Common/Globals';
import { initThemes } from 'Stores/Theme';
import { StorageResultType } from 'Common/Enums';
import AppStore from 'Stores/Admin/App';
import CapaStore from 'Stores/Admin/Capa';
import DomainStore from 'Stores/Admin/Domain';
import PluginStore from 'Stores/Admin/Plugin';
import PackageStore from 'Stores/Admin/Package';
import Remote from 'Remote/Admin/Fetch';
import { SettingsAdminScreen } from 'Screen/Admin/Settings';
@ -11,42 +15,110 @@ import { LoginAdminScreen } from 'Screen/Admin/Login';
import { startScreens } from 'Knoin/Knoin';
import { AbstractApp } from 'App/Abstract';
import { AskPopupView } from 'View/Popup/Ask';
export class AdminApp extends AbstractApp {
class AdminApp extends AbstractApp {
constructor() {
super(Remote);
this.weakPassword = ko.observable(false);
}
refresh() {
initThemes();
this.start();
remote() {
return Remote;
}
start() {
// if (!Settings.app('adminAllowed')) {
if (!SettingsAdmin('allowed')) {
reloadDomainList() {
DomainStore.domains.loading(true);
Remote.domainList((result, data) => {
DomainStore.domains.loading(false);
if (StorageResultType.Success === result && data && data.Result) {
DomainStore.domains(
Object.entries(data.Result).map(([name, [enabled, alias]]) => ({
name: name,
disabled: ko.observable(!enabled),
alias: alias,
deleteAccess: ko.observable(false)
}))
);
}
});
}
reloadPluginList() {
PluginStore.plugins.loading(true);
Remote.pluginList((result, data) => {
PluginStore.plugins.loading(false);
if (StorageResultType.Success === result && data && data.Result) {
PluginStore.plugins(
data.Result.map(item => ({
name: item.Name,
disabled: ko.observable(!item.Enabled),
configured: ko.observable(!!item.Configured)
}))
);
}
});
}
reloadPackagesList() {
PackageStore.packages.loading(true);
PackageStore.packagesReal(true);
Remote.packagesList((result, data) => {
PackageStore.packages.loading(false);
if (StorageResultType.Success === result && data && data.Result) {
PackageStore.packagesReal(!!data.Result.Real);
PackageStore.packagesMainUpdatable(!!data.Result.MainUpdatable);
let list = [];
const loading = {};
PackageStore.packages().forEach(item => {
if (item && item.loading()) {
loading[item.file] = item;
}
});
if (Array.isArray(data.Result.List)) {
list = data.Result.List.map(item => {
if (item) {
item.loading = ko.observable(loading[item.file] !== undefined);
return 'core' === item.type && !item.canBeInstalled ? null : item;
}
return null;
}).filter(value => !!value);
}
PackageStore.packages(list);
} else {
PackageStore.packagesReal(false);
}
});
}
bootend() {
if (window.progressJs) {
progressJs.end();
}
}
bootstart() {
super.bootstart();
AppStore.populate();
CapaStore.populate();
this.hideLoading();
if (!rl.settings.app('allowAdminPanel')) {
rl.route.root();
setTimeout(() => location.href = '/', 1);
} else if (SettingsGet('Auth')) {
this.weakPassword(SettingsGet('weakPassword'));
startScreens([SettingsAdminScreen]);
} else {
startScreens([LoginAdminScreen]);
if (rl.settings.get('Auth')) {
startScreens([SettingsAdminScreen]);
} else {
startScreens([LoginAdminScreen]);
}
}
this.bootend();
}
}
AskPopupView.credentials = function(sAskDesc, btnText) {
return new Promise(resolve => {
this.showModal([
sAskDesc,
view => resolve({username:view.username(), password:view.passphrase()}),
() => resolve(null),
true,
3,
btnText
]);
});
};
export default new AdminApp();

File diff suppressed because it is too large Load diff

View file

@ -1,131 +1,128 @@
import * as Links from 'Common/Links';
import { doc, SettingsGet, fireEvent, addEventsListener } from 'Common/Globals';
import { addObservablesTo } from 'External/ko';
let notificator = null,
player = null,
canPlay = type => !!player?.canPlayType(type).replace('no', ''),
class Audio {
notificator = null;
player = null;
audioCtx = window.AudioContext || window.webkitAudioContext,
supported = false;
supportedMp3 = false;
supportedOgg = false;
supportedWav = false;
supportedNotification = false;
play = (url, name) => {
if (player) {
player.src = url;
player.play();
name = name.trim();
fireEvent('audio.start', name.replace(/\.([a-z0-9]{3})$/, '') || 'audio');
constructor() {
this.player = this.createNewObject();
// Safari can't play without user interaction
this.supported = !!this.player && !!this.player.play;
if (this.supported && this.player && this.player.canPlayType) {
this.supportedMp3 = !!this.player.canPlayType('audio/mpeg;').replace(/no/, '');
this.supportedWav = !!this.player.canPlayType('audio/wav; codecs="1"').replace(/no/, '');
this.supportedOgg = !!this.player.canPlayType('audio/ogg; codecs="vorbis"').replace(/no/, '');
this.supportedNotification = this.supported && this.supportedMp3;
}
},
createNewObject = () => {
if (!this.player || (!this.supportedMp3 && !this.supportedOgg && !this.supportedWav)) {
this.supported = false;
this.supportedMp3 = false;
this.supportedOgg = false;
this.supportedWav = false;
this.supportedNotification = false;
}
if (this.supported && this.player) {
const stopFn = () => this.stop();
this.player.addEventListener('ended', stopFn);
this.player.addEventListener('error', stopFn);
addEventListener('audio.api.stop', stopFn);
}
}
createNewObject() {
try {
const player = new Audio;
if (player.canPlayType && player.pause && player.play) {
const player = window.Audio ? new Audio() : null;
if (player && player.canPlayType && player.pause && player.play) {
player.preload = 'none';
player.loop = false;
player.autoplay = false;
player.muted = false;
return player;
}
} catch (e) {
console.error(e);
}
return player;
} catch (e) {} // eslint-disable-line no-empty
return null;
},
// The AudioContext is not allowed to start.
// It must be resumed (or created) after a user gesture on the page. https://goo.gl/7K7WLu
// Setup listeners to attempt an unlock
unlockEvents = [
'click','dblclick',
'contextmenu',
'auxclick',
'mousedown','mouseup',
'pointerup',
'touchstart','touchend',
'keydown','keyup'
],
unlock = () => {
unlockEvents.forEach(type => doc.removeEventListener(type, unlock, true));
if (audioCtx) {
console.log('AudioContext ' + audioCtx.state);
audioCtx.resume();
}
// setTimeout(()=>SMAudio.playNotification(0,1),1);
};
if (audioCtx) {
audioCtx = audioCtx ? new audioCtx : null;
audioCtx.onstatechange = unlock;
}
unlockEvents.forEach(type => doc.addEventListener(type, unlock, true));
/**
* Browsers can't play without user interaction
*/
export const SMAudio = new class {
constructor() {
player || (player = createNewObject());
this.supported = !!player;
this.supportedMp3 = canPlay('audio/mpeg;');
this.supportedWav = canPlay('audio/wav; codecs="1"');
this.supportedOgg = canPlay('audio/ogg; codecs="vorbis"');
if (player) {
const stopFn = () => this.pause();
addEventsListener(player, ['ended','error'], stopFn);
addEventListener('audio.api.stop', stopFn);
}
addObservablesTo(this, {
notifications: false
});
}
paused() {
return !player || player.paused;
return this.supported ? !!this.player.paused : true;
}
stop() {
this.pause();
if (this.supported && this.player.pause) {
this.player.pause();
}
dispatchEvent(new CustomEvent('audio.stop'));
}
pause() {
player?.pause();
fireEvent('audio.stop');
this.stop();
}
clearName(name = '', ext = '') {
name = name.trim();
if (ext && '.' + ext === name.toLowerCase().substr((ext.length + 1) * -1)) {
name = name.substr(0, name.length - 4).trim();
}
return name || 'audio';
}
playMp3(url, name) {
this.supportedMp3 && play(url, name);
if (this.supported && this.supportedMp3) {
this.player.src = url;
this.player.play();
dispatchEvent(new CustomEvent('audio.start', {detail:this.clearName(name, 'mp3')}));
}
}
playOgg(url, name) {
this.supportedOgg && play(url, name);
if (this.supported && this.supportedOgg) {
this.player.src = url;
this.player.play();
name = this.clearName(name, 'oga');
name = this.clearName(name, 'ogg');
dispatchEvent(new CustomEvent('audio.start', {detail:name}));
}
}
playWav(url, name) {
this.supportedWav && play(url, name);
if (this.supported && this.supportedWav) {
this.player.src = url;
this.player.play();
dispatchEvent(new CustomEvent('audio.start', {detail:this.clearName(name, 'wav')}));
}
}
/**
* Used with SoundNotification setting
*/
playNotification(force, silent) {
if (force || this.notifications()) {
if ('running' == audioCtx.state && (this.supportedMp3 || this.supportedOgg)) {
notificator = notificator || createNewObject();
if (notificator) {
// SettingsGet('NotificationSound').startsWith('custom@')
notificator.src = Links.staticLink('sounds/'
+ SettingsGet('NotificationSound')
+ (this.supportedMp3 ? '.mp3' : '.ogg'));
notificator.volume = silent ? 0.01 : 1;
notificator.play();
}
} else {
console.log('No audio: ' + audioCtx.state);
playNotification() {
if (this.supported && this.supportedMp3) {
if (!this.notificator) {
this.notificator = this.createNewObject();
this.notificator.src = Links.sound('new-mail.mp3');
}
if (this.notificator && this.notificator.play) {
this.notificator.play();
}
}
}
};
}
export default new Audio();

View file

@ -1,56 +1,307 @@
let FOLDERS_CACHE = new Map,
FOLDERS_HASH_MAP = new Map,
inboxFolderName = 'INBOX';
import { MessageSetAction } from 'Common/Enums';
import { pInt } from 'Common/Utils';
export const
/**
* @returns {void}
*/
clearCache = () => {
FOLDERS_CACHE.clear();
FOLDERS_HASH_MAP.clear();
},
let FOLDERS_CACHE = {},
FOLDERS_NAME_CACHE = {},
FOLDERS_HASH_CACHE = {},
FOLDERS_UID_NEXT_CACHE = {},
MESSAGE_FLAGS_CACHE = {},
NEW_MESSAGE_CACHE = {},
inboxFolderName = '';
/**
* @returns {string}
*/
getFolderInboxName = () => inboxFolderName,
const REQUESTED_MESSAGE_CACHE = {};
/**
* @returns {string}
*/
setFolderInboxName = name => inboxFolderName = name,
/**
* @returns {void}
*/
export function clear() {
FOLDERS_CACHE = {};
FOLDERS_NAME_CACHE = {};
FOLDERS_HASH_CACHE = {};
FOLDERS_UID_NEXT_CACHE = {};
MESSAGE_FLAGS_CACHE = {};
}
/**
* @param {string} fullNameHash
* @returns {string}
*/
getFolderFromHashMap = fullNameHash => getFolderFromCacheList(FOLDERS_HASH_MAP.get(fullNameHash)),
/**
* @param {string} email
* @param {Function} callback
* @returns {string}
*/
export function getUserPic(email, callback) {
callback('', email.trim());
}
/**
* @param {?FolderModel} folder
*/
setFolder = folder => {
folder.etag = '';
FOLDERS_CACHE.set(folder.fullName, folder);
FOLDERS_HASH_MAP.set(folder.fullNameHash, folder.fullName);
},
/**
* @param {string} folderFullNameRaw
* @param {string} uid
* @returns {string}
*/
export function getMessageKey(folderFullNameRaw, uid) {
return `${folderFullNameRaw}#${uid}`;
}
/**
* @param {string} folderFullName
* @param {string} folderETag
*/
setFolderETag = (folderFullName, folderETag) =>
FOLDERS_CACHE.has(folderFullName) && (FOLDERS_CACHE.get(folderFullName).etag = folderETag),
/**
* @param {string} folder
* @param {string} uid
*/
export function addRequestedMessage(folder, uid) {
REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)] = true;
}
/**
* @param {string} folderFullName
* @returns {?FolderModel}
*/
getFolderFromCacheList = folderFullName =>
FOLDERS_CACHE.get(folderFullName),
/**
* @param {string} folder
* @param {string} uid
* @returns {boolean}
*/
export function hasRequestedMessage(folder, uid) {
return true === REQUESTED_MESSAGE_CACHE[getMessageKey(folder, uid)];
}
/**
* @param {string} folderFullName
*/
removeFolderFromCacheList = folderFullName => FOLDERS_CACHE.delete(folderFullName);
/**
* @param {string} folderFullNameRaw
* @param {string} uid
*/
export function addNewMessageCache(folderFullNameRaw, uid) {
NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)] = true;
}
/**
* @param {string} folderFullNameRaw
* @param {string} uid
*/
export function hasNewMessageAndRemoveFromCache(folderFullNameRaw, uid) {
if (NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)]) {
NEW_MESSAGE_CACHE[getMessageKey(folderFullNameRaw, uid)] = null;
return true;
}
return false;
}
/**
* @returns {void}
*/
export function clearNewMessageCache() {
NEW_MESSAGE_CACHE = {};
}
/**
* @returns {string}
*/
export function getFolderInboxName() {
return inboxFolderName || 'INBOX';
}
/**
* @param {string} folderHash
* @returns {string}
*/
export function getFolderFullNameRaw(folderHash) {
return folderHash && FOLDERS_NAME_CACHE[folderHash] ? FOLDERS_NAME_CACHE[folderHash] : '';
}
/**
* @param {string} folderHash
* @param {string} folderFullNameRaw
*/
export function setFolderFullNameRaw(folderHash, folderFullNameRaw) {
FOLDERS_NAME_CACHE[folderHash] = folderFullNameRaw;
if (!inboxFolderName || 'INBOX' === folderFullNameRaw) {
inboxFolderName = folderFullNameRaw;
}
}
/**
* @param {string} folderFullNameRaw
* @returns {string}
*/
export function getFolderHash(folderFullNameRaw) {
return folderFullNameRaw && FOLDERS_HASH_CACHE[folderFullNameRaw] ? FOLDERS_HASH_CACHE[folderFullNameRaw] : '';
}
/**
* @param {string} folderFullNameRaw
* @param {string} folderHash
*/
export function setFolderHash(folderFullNameRaw, folderHash) {
if (folderFullNameRaw) {
FOLDERS_HASH_CACHE[folderFullNameRaw] = folderHash;
}
}
/**
* @param {string} folderFullNameRaw
* @returns {string}
*/
export function getFolderUidNext(folderFullNameRaw) {
return folderFullNameRaw && FOLDERS_UID_NEXT_CACHE[folderFullNameRaw]
? FOLDERS_UID_NEXT_CACHE[folderFullNameRaw]
: '';
}
/**
* @param {string} folderFullNameRaw
* @param {string} uidNext
*/
export function setFolderUidNext(folderFullNameRaw, uidNext) {
FOLDERS_UID_NEXT_CACHE[folderFullNameRaw] = uidNext;
}
/**
* @param {string} folderFullNameRaw
* @returns {?FolderModel}
*/
export function getFolderFromCacheList(folderFullNameRaw) {
return folderFullNameRaw && FOLDERS_CACHE[folderFullNameRaw] ? FOLDERS_CACHE[folderFullNameRaw] : null;
}
/**
* @param {string} folderFullNameRaw
* @param {?FolderModel} folder
*/
export function setFolderToCacheList(folderFullNameRaw, folder) {
FOLDERS_CACHE[folderFullNameRaw] = folder;
}
/**
* @param {string} folderFullNameRaw
*/
export function removeFolderFromCacheList(folderFullNameRaw) {
setFolderToCacheList(folderFullNameRaw, null);
}
/**
* @param {string} folderFullName
* @param {string} uid
* @returns {?Array}
*/
export function getMessageFlagsFromCache(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
*/
export function setMessageFlagsToCache(folderFullName, uid, flagsCache) {
if (!MESSAGE_FLAGS_CACHE[folderFullName]) {
MESSAGE_FLAGS_CACHE[folderFullName] = {};
}
MESSAGE_FLAGS_CACHE[folderFullName][uid] = flagsCache;
}
/**
* @param {string} folderFullName
*/
export function clearMessageFlagsFromCacheByFolder(folderFullName) {
MESSAGE_FLAGS_CACHE[folderFullName] = {};
}
/**
* @param {(MessageModel|null)} message
*/
export function initMessageFlagsFromCache(message) {
if (message) {
const uid = message.uid,
flags = getMessageFlagsFromCache(message.folderFullNameRaw, uid);
if (flags && flags.length) {
message.flagged(!!flags[1]);
if (!message.isSimpleMessage) {
message.unseen(!!flags[0]);
message.answered(!!flags[2]);
message.forwarded(!!flags[3]);
message.isReadReceipt(!!flags[4]);
message.deletedMark(!!flags[5]);
}
}
if (message.threads().length) {
const unseenSubUid = message.threads().find(sSubUid => {
if (uid !== sSubUid) {
const subFlags = getMessageFlagsFromCache(message.folderFullNameRaw, sSubUid);
return subFlags && subFlags.length && !!subFlags[0];
}
return false;
});
const flaggedSubUid = message.threads().find(sSubUid => {
if (uid !== sSubUid) {
const subFlags = getMessageFlagsFromCache(message.folderFullNameRaw, 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
*/
export function storeMessageFlagsToCache(message) {
if (message) {
setMessageFlagsToCache(message.folderFullNameRaw, message.uid, [
message.unseen(),
message.flagged(),
message.answered(),
message.forwarded(),
message.isReadReceipt(),
message.deletedMark()
]);
}
}
/**
* @param {string} folder
* @param {string} uid
* @param {Array} flags
*/
export function storeMessageFlagsToCacheByFolderAndUid(folder, uid, flags) {
if (Array.isNotEmpty(flags)) {
setMessageFlagsToCache(folder, uid, flags);
}
}
/**
* @param {string} folder
* @param {string} uid
* @param {number} setAction
*/
export function storeMessageFlagsToCacheBySetAction(folder, uid, setAction) {
let unread = 0;
const flags = getMessageFlagsFromCache(folder, uid);
if (Array.isNotEmpty(flags)) {
if (flags[0]) {
unread = 1;
}
switch (setAction) {
case MessageSetAction.SetSeen:
flags[0] = false;
break;
case MessageSetAction.UnsetSeen:
flags[0] = true;
break;
case MessageSetAction.SetFlag:
flags[1] = true;
break;
case MessageSetAction.UnsetFlag:
flags[1] = false;
break;
// no default
}
setMessageFlagsToCache(folder, uid, flags);
}
return unread;
}

View file

@ -1 +1,41 @@
export const MESSAGES_PER_PAGE = 20;
export const MESSAGES_PER_PAGE_VALUES = [10, 20, 30, 50, 100];
export const CONTACTS_PER_PAGE = 50;
export const DEFAULT_AJAX_TIMEOUT = 30000;
export const SEARCH_AJAX_TIMEOUT = 300000;
export const SEND_MESSAGE_AJAX_TIMEOUT = 300000;
export const SAVE_MESSAGE_AJAX_TIMEOUT = 200000;
export const CONTACTS_SYNC_AJAX_TIMEOUT = 200000;
export const UNUSED_OPTION_VALUE = '__UNUSE__';
export const CLIENT_SIDE_STORAGE_INDEX_NAME = 'rlcsc';
export const IMAP_DEFAULT_PORT = 143;
export const IMAP_DEFAULT_SECURE_PORT = 993;
export const SMTP_DEFAULT_PORT = 25;
export const SMTP_DEFAULT_SECURE_PORT = 465;
export const SIEVE_DEFAULT_PORT = 4190;
export const MESSAGE_BODY_CACHE_LIMIT = 15;
export const AJAX_ERROR_LIMIT = 7;
export const TOKEN_ERROR_LIMIT = 10;
export const RAINLOOP_TRIAL_KEY = 'RAINLOOP-TRIAL-KEY';
/* eslint max-len: 0 */
export const DATA_IMAGE_USER_DOT_PIC =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC4AAAAuCAYAAABXuSs3AAAHHklEQVRoQ7VZW08bVxCeXRuwIbTGXIwNtBBaqjwgVUiR8lDlbza9qe1DpVZ9aNQ/0KpPeaJK07SpcuEeCEmUAObm21bfrL9lONjexSYrWfbunj37zXdmvpkz9oIgCKTD0Wg0xPd94TDP83Q0zvWa50vzklSrdanVanqf4/D84GBGr+F+Op3S8fqoJxLOdnZgTvsO/nYhenHA+UC7CWF1uXwkb9++ldPTUwVerVbVqFQqpR8YPjQ0JCMjI5LNDijoRgP3PQVu5+5Eor2XGLg7IV4GkIdHJ/LmzRs5ODiIwNbrdR0O0GCcq4Xz4eFhmZyclP7+tDQaIik/BG5XKQn4SwG3zJTLZXn9+rUclI8UHD5YVoDDN8bSzXhONwL48fFxGR4eilzFZT1uFRIB5yT8BqCdnR3Z3d0VP9Un6XRawYJpggVrZBv38ME4XKtUKnLt2jUplUoy1PR/l3U7T6sVSAQcgMAkj8PDQ9ne3pajoyMRL7zeKsYZWHgWYDGmv78/mmdwcFA+mJlSgziHDWrERrsjEXDXegTi1tZW+DLxI2bxIrqFNYTXyDyCFweMAHCwb8e4RnTNuOsqe3t7sra21pTD0Kct666E8XlcZyzw9/RUUXK5nK5oUinUQI6TQ3cynO/v78vq6qrKXCNwlTiJJpyNGc3nZHp6uqV2dwrQWOCtZBDAV1ZWwsQk7f0wiQn5kffbAu/0/KWBYzIC1+XukfGx0RGZmppKlC2tIV0Bh4aDcZW7HhkfH8urLLZL7T2pihvlkMNnz56FiadHxicL41IsFpN41bkxsYxbRdFo9jwB8KdPn14J8KnSpBQKhQs63nPmbCVRcBUAR2Lq1VVmpksyMTFxAXjcEsQybiegESionjx5osCZOeNe1O4+EhCAX7bQSgQcxRHTMgAgcz5+/Dis/hL4uHU3/B4YGNASGHIKxuEql0k+l05AeIAF1vPnz5VxFFmdDlaJrMtZITJeSsXCOTlMunKxjLtMYOKNjQ158eJFuAuKkUOb5sEwgff19SkJUBVkThZUbnXZrtCKBQ6gbnWIkjZpyne3ejAWoGnA7Icz6irvBLgbOMicCM6TkxPx/LAkbXfgWcsazuE2kFRsKD5Z+CiqDumKncpZvieWcS6dDVD8xiYCNflpJdwcdwJOf9airLmVQ7DPzMxIYWLsXGXoVqLt5k0M3K3JUVPDZdbWNzsCp48TPFdvdnZWUz32nDha7bJ63kgAJPzSdRks9/Kf9xMJAQ1gq2NpaUmy2Yz4zar4nQC3xb99AQwCcGzLAAwuhG8YiWvcOKts+r4GOe5nMhm5efOm9lUA3E3vSZJRrKvE0fnPv//Jy5cvo5cTHIPQbSjhOoqq69evS19f6lxDKK4+sVhigZPtKJqbrQeqxd5+WR4+fKgqgT0k2XX3nhiPgETWXFhYkFzuPZ2yVq1GTSOXpE47/VjgNnD4m4GG7/LhsTx69EiwD4Vr2MwIIxgbAH18fKx1yfz8vEogNvGtWnCuhLZa9UTAreVWFsHy/b/+Vrbdl7E5REMQD2jDoUbByty+/ZnU64GkU2HzyJLhktU1cLv8nARgkYS2d3ajAgwG8qU2oLmDZ92CMaOjo7K4uCiZgbDWaRWgnZhPxLhrMUCvr69riwKZk1LHF7XqrWAO9hJxH6ozNzcnCx/PqztZg9mf6SQMscCtm2C5ke4BGMlHWTUp36036AJajDVrFMzBrhhWslQsSrFYiOqVpMriNYIgqFRq2j3FAb/zffT6zuxFXxsNzs3NTXn16lW4gYiW96w1FyedF+83xG/2FNGCRpU4NjamMsn+OZ9xE5RXqdaDdPpib6RWCzuwKF9RxqI2AVNQBwQYJoK0wdBejnqtEikP3pfP51XjUTESl12FqJEKxsEorARYDD44ONTeID7YpsEnrRvQfWAI2e8WfDaTUSIwJ0iBCmFOtOUAHvVMPp/TPwvYFVYFIuP8l+DBgwdaa2Miqwa0GgYwfeMltovbDfh6c1vIgMYcliSsKv4IWFr6VDHxvldvBAH+1sA+cnl5WYOPmmr9ir+1l9I0Cgz0yjhXjfJJ0JROnmezWbl165ayr/5fqwcBNr7IfhjMqKcvESSM4eRcCasQ3bDNObmKPLdGUGpZsN24cUNLBm9zazu4d++e6qpNBFaTuUS26U5dpuR1CxyA7J9ddrMRqlz4pwLLYawymPd++/2PADt2ugcGwq9gCCdhQ96C6xWwa6j1ceuq+I0EhW0i8MAIVJfeL3d/DVD8EKi12P6/2S2jV/EccVB54O/ejz/9HGCpoBBMta5rXMXLu53D1XAwjhXwvvv+h4BAXVe4bOu3O3ChxF08LiZFG3fel199G9CH3fLyqv24NcB44MRhpdK788U3CpyKwsCw590xmfSpzsBt0Fqc3ud3vtZigxWcVZCklVpSiN0w3q5E/h9TGMIUuA3+EQAAAABJRU5ErkJggg==';

View file

@ -1,56 +1,471 @@
/* eslint quote-props: 0 */
export const
/**
* @enum {string}
*/
ScopeMessageList = 'MessageList',
ScopeFolderList = 'FolderList',
ScopeMessageView = 'MessageView',
ScopeSettings = 'Settings',
export const FileType = {
Unknown: 'unknown',
Text: 'text',
Html: 'html',
Code: 'code',
Eml: 'eml',
WordText: 'word-text',
Pdf: 'pdf',
Image: 'image',
Audio: 'audio',
Video: 'video',
Sheet: 'sheet',
Presentation: 'presentation',
Certificate: 'certificate',
CertificateBin: 'certificate-bin',
Archive: 'archive',
getIconClass: function(type) {
let result = ['icon-file', ''];
switch (type) {
case this.Text:
case this.Eml:
case this.WordText:
result[0] += '-text';
break;
case this.Html:
case this.Code:
result[0] += '-code';
break;
case this.Image:
result[0] += '-image';
break;
case this.Audio:
result[0] += '-music';
break;
case this.Video:
result[0] += '-movie';
break;
case this.Archive:
result[0] += '-zip';
break;
case this.Certificate:
case this.CertificateBin:
result[0] += '-certificate';
break;
case this.Sheet:
result[0] += '-excel';
break;
case this.Presentation:
result[0] += '-chart-graph';
break;
case this.Pdf:
result['icon-none', 'pdf'];
break;
// no default
}
return result;
}
};
/**
* @enum {string}
*/
export const StorageResultType = {
Success: 'success',
Abort: 'abort',
Error: 'error',
Unload: 'unload'
};
/**
* @enum {string}
*/
export const Focused = {
None: 'none',
MessageList: 'message-list',
MessageView: 'message-view',
FolderList: 'folder-list'
};
/**
* @enum {number}
*/
UploadErrorCode = {
export const State = {
Empty: 10,
Login: 20,
Auth: 30
};
/**
* @enum {number}
*/
export const StateType = {
Webmail: 0,
Admin: 1
};
/**
* @enum {string}
*/
export const Capa = {
TwoFactor: 'TWO_FACTOR',
TwoFactorForce: 'TWO_FACTOR_FORCE',
OpenPGP: 'OPEN_PGP',
Prefetch: 'PREFETCH',
Gravatar: 'GRAVATAR',
Folders: 'FOLDERS',
Composer: 'COMPOSER',
Contacts: 'CONTACTS',
Reload: 'RELOAD',
Search: 'SEARCH',
SearchAdv: 'SEARCH_ADV',
MessageActions: 'MESSAGE_ACTIONS',
MessageListActions: 'MESSAGELIST_ACTIONS',
AttachmentsActions: 'ATTACHMENTS_ACTIONS',
DangerousActions: 'DANGEROUS_ACTIONS',
Settings: 'SETTINGS',
Help: 'HELP',
Themes: 'THEMES',
UserBackground: 'USER_BACKGROUND',
Sieve: 'SIEVE',
Filters: 'FILTERS',
AttachmentThumbnails: 'ATTACHMENT_THUMBNAILS',
Templates: 'TEMPLATES',
AutoLogout: 'AUTOLOGOUT',
AdditionalAccounts: 'ADDITIONAL_ACCOUNTS',
Identities: 'IDENTITIES'
};
/**
* @enum {string}
*/
export const KeyState = {
All: 'all',
None: 'none',
ContactList: 'contact-list',
MessageList: 'message-list',
FolderList: 'folder-list',
MessageView: 'message-view',
Compose: 'compose',
Settings: 'settings',
Menu: 'menu',
PopupComposeOpenPGP: 'compose-open-pgp',
PopupMessageOpenPGP: 'message-open-pgp',
PopupViewOpenPGP: 'view-open-pgp',
PopupKeyboardShortcutsHelp: 'popup-keyboard-shortcuts-help',
PopupAsk: 'popup-ask'
};
/**
* @enum {number}
*/
export const FolderType = {
Inbox: 10,
SentItems: 11,
Draft: 12,
Trash: 13,
Spam: 14,
Archive: 15,
NotSpam: 80,
User: 99
};
/**
* @enum {number}
*/
export const ServerFolderType = {
USER: 0,
INBOX: 1,
SENT: 2,
DRAFTS: 3,
JUNK: 4,
TRASH: 5,
IMPORTANT: 10,
FLAGGED: 11,
ALL: 12
};
/**
* @enum {string}
*/
export const LoginSignMeTypeAsString = {
DefaultOff: 'defaultoff',
DefaultOn: 'defaulton',
Unused: 'unused'
};
/**
* @enum {number}
*/
export const LoginSignMeType = {
DefaultOff: 0,
DefaultOn: 1,
Unused: 2
};
/**
* @enum {string}
*/
export const ComposeType = {
Empty: 'empty',
Reply: 'reply',
ReplyAll: 'replyall',
Forward: 'forward',
ForwardAsAttachment: 'forward-as-attachment',
Draft: 'draft',
EditAsNew: 'editasnew'
};
/**
* @enum {number}
*/
export const UploadErrorCode = {
Normal: 0,
FileIsTooBig: 1,
FilePartiallyUploaded: 3,
NoFileUploaded: 4,
MissingTempFolder: 6,
OnSavingFile: 7,
FilePartiallyUploaded: 2,
FileNoUploaded: 3,
MissingTempFolder: 4,
FileOnSaveingError: 5,
FileType: 98,
Unknown: 99
},
};
/**
* @enum {number}
*/
SaveSettingStatus = {
Saving: -2,
export const SetSystemFoldersNotification = {
None: 0,
Sent: 1,
Draft: 2,
Spam: 3,
Trash: 4,
Archive: 5
};
/**
* @enum {number}
*/
export const ClientSideKeyName = {
FoldersLashHash: 0,
MessagesInboxLastHash: 1,
MailBoxListSize: 2,
ExpandedFolders: 3,
FolderListSize: 4,
MessageListSize: 5,
LastReplyAction: 6,
LastSignMe: 7,
ComposeLastIdentityID: 8,
MessageHeaderFullInfo: 9,
MessageAttachmnetControls: 10
};
/**
* @enum {number}
*/
export const MessageSetAction = {
SetSeen: 0,
UnsetSeen: 1,
SetFlag: 2,
UnsetFlag: 3
};
/**
* @enum {number}
*/
export const MessageSelectAction = {
All: 0,
None: 1,
Invert: 2,
Unseen: 3,
Seen: 4,
Flagged: 5,
Unflagged: 6
};
/**
* @enum {number}
*/
export const DesktopNotification = {
Allowed: 0,
NotAllowed: 1,
Denied: 2,
NotSupported: 9
};
/**
* @enum {number}
*/
export const MessagePriority = {
Low: 5,
Normal: 3,
High: 1
};
/**
* @enum {string}
*/
export const EditorDefaultType = {
Html: 'Html',
Plain: 'Plain',
HtmlForced: 'HtmlForced',
PlainForced: 'PlainForced'
};
/**
* @enum {number}
*/
export const ServerSecure = {
None: 0,
SSL: 1,
TLS: 2
};
/**
* @enum {number}
*/
export const SearchDateType = {
All: -1,
Days3: 3,
Days7: 7,
Month: 30
};
/**
* @enum {number}
*/
export const SaveSettingsStep = {
Animate: -2,
Idle: -1,
Success: 1,
Failed: 0
},
TrueResult: 1,
FalseResult: 0
};
/**
* @enum {number}
*/
Notifications = {
RequestError: 1,
RequestAborted: 2,
RequestTimeout: 3,
export const Layout = {
NoPreview: 0,
SidePreview: 1,
BottomPreview: 2
};
// Global
/**
* @enum {string}
*/
export const FilterConditionField = {
From: 'From',
Recipient: 'Recipient',
Subject: 'Subject',
Header: 'Header',
Size: 'Size'
};
/**
* @enum {string}
*/
export const FilterConditionType = {
Contains: 'Contains',
NotContains: 'NotContains',
EqualTo: 'EqualTo',
NotEqualTo: 'NotEqualTo',
Regex: 'Regex',
Over: 'Over',
Under: 'Under'
};
/**
* @enum {string}
*/
export const FiltersAction = {
None: 'None',
MoveTo: 'MoveTo',
Discard: 'Discard',
Vacation: 'Vacation',
Reject: 'Reject',
Forward: 'Forward'
};
/**
* @enum {string}
*/
export const FilterRulesType = {
All: 'All',
Any: 'Any'
};
/**
* @enum {number}
*/
export const SignedVerifyStatus = {
UnknownPublicKeys: -4,
UnknownPrivateKey: -3,
Unverified: -2,
Error: -1,
None: 0,
Success: 1
};
/**
* @enum {number}
*/
export const ContactPropertyType = {
Unknown: 0,
FullName: 10,
FirstName: 15,
LastName: 16,
MiddleName: 16,
Nick: 18,
NamePrefix: 20,
NameSuffix: 21,
Email: 30,
Phone: 31,
Web: 32,
Birthday: 40,
Facebook: 90,
Skype: 91,
GitHub: 92,
Note: 110,
Custom: 250
};
/**
* @enum {number}
*/
export const Ports = {
Imap: 143,
ImapSsl: 993,
Smtp: 25,
SmtpSsl: 465,
SmtpStartTls: 587
};
/**
* @enum {number}
*/
export const Notification = {
InvalidToken: 101,
AuthError: 102,
// User
AccessError: 103,
ConnectionError: 104,
CaptchaError: 105,
SocialFacebookLoginAccessDisable: 106,
SocialTwitterLoginAccessDisable: 107,
SocialGoogleLoginAccessDisable: 108,
DomainNotAllowed: 109,
AccountNotAllowed: 110,
CryptKeyError: 111,
AccountTwoFactorAuthRequired: 120,
AccountTwoFactorAuthError: 121,
CouldNotSaveNewPassword: 130,
CurrentPasswordIncorrect: 131,
NewPasswordShort: 132,
NewPasswordWeak: 133,
NewPasswordForbidden: 134,
ContactsSyncError: 140,
@ -66,9 +481,7 @@ Notifications = {
CantSaveFilters: 351,
CantGetFilters: 352,
CantActivateFiltersScript: 353,
CantDeleteFiltersScript: 354,
// FiltersAreNotCorrect: 355,
FiltersAreNotCorrect: 355,
CantCreateFolder: 400,
CantRenameFolder: 401,
@ -77,31 +490,33 @@ Notifications = {
CantUnsubscribeFolder: 404,
CantDeleteNonEmptyFolder: 405,
// CantSaveSettings: 501,
CantSaveSettings: 501,
CantSavePluginSettings: 502,
DomainAlreadyExists: 601,
CantInstallPackage: 701,
CantDeletePackage: 702,
InvalidPluginPackage: 703,
UnsupportedPluginPackage: 704,
DemoSendMessageError: 750,
DemoAccountError: 751,
AccountAlreadyExists: 801,
AccountDoesNotExist: 802,
AccountSwitchFailed: 803,
MailServerError: 901,
ClientViewError: 902,
InvalidInputArgument: 903,
JsonFalse: 950,
JsonParse: 952,
// JsonTimeout: 953,
AjaxFalse: 950,
AjaxAbort: 951,
AjaxParse: 952,
AjaxTimeout: 953,
UnknownError: 999,
UnknownNotification: 999,
UnknownError: 999
// Admin
CantInstallPackage: 701,
CantDeletePackage: 702,
InvalidPluginPackage: 703,
UnsupportedPluginPackage: 704,
CantSavePluginSettings: 705
,getKeyByValue: function(v) { return Object.keys(this).find(key => this[key] === v); }
};

View file

@ -1,85 +0,0 @@
/* eslint quote-props: 0 */
/**
* @enum {number}
*/
export const FolderType = {
Inbox: 1,
Sent: 2,
Drafts: 3,
Junk: 4, // Spam
Trash: 5,
Archive: 6
/*
IMPORTANT : 10;
FLAGGED : 11;
ALL : 13;
// TODO: SnappyMail
TEMPLATES : 19;
// Kolab
CONFIGURATION : 20;
CALENDAR : 21;
CONTACTS : 22;
TASKS : 23;
NOTES : 24;
FILES : 25;
JOURNAL : 26;
*/
},
/**
* @enum {string}
*/
FolderMetadataKeys = {
// RFC 5464
Comment: '/private/comment',
CommentShared: '/shared/comment',
// RFC 6154
SpecialUse: '/private/specialuse',
// Kolab
KolabFolderType: '/private/vendor/kolab/folder-type',
KolabFolderTypeShared: '/shared/vendor/kolab/folder-type'
},
/**
* @enum {string}
*/
ComposeType = {
Empty: 0,
Reply: 1,
ReplyAll: 2,
Forward: 3,
ForwardAsAttachment: 4,
Draft: 5,
EditAsNew: 6
},
/**
* @enum {number}
*/
ClientSideKeyNameExpandedFolders = 3,
ClientSideKeyNameFolderListSize = 4,
ClientSideKeyNameMessageListSize = 5,
ClientSideKeyNameLastSignMe = 7,
ClientSideKeyNameMessageHeaderFullInfo = 9,
ClientSideKeyNameMessageAttachmentControls = 10,
/**
* @enum {number}
*/
MessageSetAction = {
SetSeen: 0,
UnsetSeen: 1,
SetFlag: 2,
UnsetFlag: 3,
SetDeleted: 4,
UnsetDeleted: 5
},
/**
* @enum {number}
*/
//LayoutNoView = 0,
LayoutSideView = 1,
LayoutBottomView = 2
;

View file

@ -1,282 +0,0 @@
/* eslint key-spacing: 0 */
/* eslint quote-props: 0 */
import { arrayLength, pInt } from 'Common/Utils';
export const RFC822 = 'message/rfc822';
const
cache = {},
app = 'application/',
msOffice = app+'vnd.openxmlformats-officedocument.',
openDoc = app+'vnd.oasis.opendocument.',
sizes = ['B', 'KiB', 'MiB', 'GiB', 'TiB'],
lowerCase = text => text.toLowerCase().trim(),
exts = {
eml: RFC822,
mime: RFC822,
vcard: 'text/vcard',
vcf: 'text/vcard',
htm: 'text/html',
html: 'text/html',
csv: 'text/csv',
ics: 'text/calendar',
xml: 'text/xml',
json: app+'json',
// asc: app+'pgp-signature',
// asc: app+'pgp-keys',
p10: app+'pkcs10',
p7c: app+'pkcs7-mime',
p7m: app+'pkcs7-mime',
p7s: app+'pkcs7-signature',
p12: app+'pkcs12',
pfx: app+'x-pkcs12',
torrent: app+'x-bittorrent',
// scripts
js: app+'javascript',
pl: 'text/perl',
css: 'text/css',
asp: 'text/asp',
php: app+'x-php',
// images
jpg: 'image/jpeg',
ico: 'image/x-icon',
tif: 'image/tiff',
svg: 'image/svg+xml',
svgz: 'image/svg+xml',
// archives
zip: app+'zip',
'7z': app+'x-7z-compressed',
rar: app+'x-rar-compressed',
cab: app+'vnd.ms-cab-compressed',
gz: app+'x-gzip',
tgz: app+'x-gzip',
bz: app+'x-bzip',
bz2: app+'x-bzip2',
deb: app+'x-debian-package',
// audio
mp3: 'audio/mpeg',
wav: 'audio/x-wav',
mp4a: 'audio/mp4',
weba: 'audio/webm',
m3u: 'audio/x-mpegurl',
// video
qt: 'video/quicktime',
mov: 'video/quicktime',
wmv: 'video/windows-media',
avi: 'video/x-msvideo',
'3gp': 'video/3gpp',
'3g2': 'video/3gpp2',
mp4v: 'video/mp4',
mpg4: 'video/mp4',
ogv: 'video/ogg',
m4v: 'video/x-m4v',
asf: 'video/x-ms-asf',
asx: 'video/x-ms-asf',
wm: 'video/x-ms-wm',
wmx: 'video/x-ms-wmx',
wvx: 'video/x-ms-wvx',
movie: 'video/x-sgi-movie',
// adobe
pdf: app+'pdf',
psd: 'image/vnd.adobe.photoshop',
ai: app+'postscript',
eps: app+'postscript',
ps: app+'postscript',
// ms office
doc: app+'msword',
rtf: app+'rtf',
xls: app+'vnd.ms-excel',
ppt: app+'vnd.ms-powerpoint',
docx: msOffice+'wordprocessingml.document',
xlsx: msOffice+'spreadsheetml.sheet',
dotx: msOffice+'wordprocessingml.template',
pptx: msOffice+'presentationml.presentation',
// open office
odt: openDoc+'text',
ods: openDoc+'spreadsheet',
odp: openDoc+'presentation'
};
export const FileType = {
Unknown: 'unknown',
Text: 'text',
Code: 'code',
Eml: 'eml',
Word: 'word',
Pdf: 'pdf',
Image: 'image',
Audio: 'audio',
Video: 'video',
Spreadsheet: 'spreadsheet',
Presentation: 'presentation',
Certificate: 'certificate',
Archive: 'archive',
Calendar: 'calendar'
};
export const FileInfo = {
/**
* @param {string} fileName
* @returns {string}
*/
getExtension: fileName => {
fileName = lowerCase(fileName);
const result = fileName.split('.').pop();
return result === fileName ? '' : result;
},
getContentType: fileName => {
fileName = lowerCase(fileName);
if ('winmail.dat' === fileName) {
return app + 'vnd.ms-tnef';
}
let ext = fileName.split('.').pop();
if (/^(txt|text|def|list|in|ini|log|sql|cfg|conf)$/.test(ext))
return 'text/plain';
if (/^(mpe?g|mpe|m1v|m2v)$/.test(ext))
return 'video/mpeg';
if (/^aif[cf]?$/.test(ext))
return 'audio/aiff';
if (/^(aac|flac|midi|ogg)$/.test(ext))
return 'audio/'+ext;
if (/^(h26[134]|jpgv|mp4|webm)$/.test(ext))
return 'video/'+ext;
if (/^(otf|sfnt|ttf|woff2?)$/.test(ext))
return 'font/'+ext;
if (/^(png|jpeg|gif|tiff|webp)$/.test(ext))
return 'image/'+ext;
return exts[ext] || app+'octet-stream';
},
/**
* @param {string} sExt
* @param {string} sMimeType
* @returns {string}
*/
getType: (ext, mimeType) => {
ext = lowerCase(ext);
mimeType = lowerCase(mimeType).replace('csv/plain', 'text/csv').replace('x-','');
let key = ext + mimeType;
if (cache[key]) {
return cache[key];
}
let result = FileType.Unknown;
const mimeTypeParts = mimeType.split('/'),
type = mimeTypeParts[1].replace('-compressed',''),
match = str => mimeType.includes(str),
archive = /^(zip|7z|tar|rar|gzip|bzip|bzip2)$/;
switch (true) {
case 'image' == mimeTypeParts[0] || ['png', 'jpg', 'jpeg', 'gif', 'webp'].includes(ext):
result = FileType.Image;
break;
case 'audio' == mimeTypeParts[0] || ['mp3', 'ogg', 'oga', 'wav'].includes(ext):
result = FileType.Audio;
break;
case 'video' == mimeTypeParts[0] || 'mkv' == ext || 'avi' == ext:
result = FileType.Video;
break;
case ['php', 'js', 'css', 'xml', 'html'].includes(ext) || 'text/html' == mimeType:
result = FileType.Code;
break;
case 'eml' == ext || ['message/delivery-status', RFC822].includes(mimeType):
result = FileType.Eml;
break;
case 'ics' == ext || mimeType == 'text/calendar':
result = FileType.Calendar;
break;
case 'text' == mimeTypeParts[0] || 'txt' == ext || 'log' == ext:
result = FileType.Text;
break;
case archive.test(type) || archive.test(ext):
result = FileType.Archive;
break;
case 'pdf' == type || 'pdf' == ext:
result = FileType.Pdf;
break;
case [app+'pgp-signature', app+'pgp-keys', exts.p7m, exts.p7s, exts.p12, exts.pfx].includes(mimeType)
|| ['asc', 'pem', 'ppk', 'p7s', 'p7m', 'p12', 'pfx'].includes(ext):
result = FileType.Certificate;
break;
case match(msOffice+'.wordprocessingml') || match(openDoc+'.text') || match('vnd.ms-word')
|| ['rtf', 'msword', 'vnd.msword'].includes(type):
result = FileType.Word;
break;
case match(msOffice+'.spreadsheetml') || match(openDoc+'.spreadsheet') || match('ms-excel'):
result = FileType.Spreadsheet;
break;
case match(msOffice+'.presentationml') || match(openDoc+'.presentation') || match('ms-powerpoint'):
result = FileType.Presentation;
break;
// no default
}
return cache[key] = result;
},
/**
* @param {string} sFileType
* @returns {string}
*/
getTypeIconClass: fileType => {
let result = 'icon-file';
switch (fileType) {
case FileType.Text:
case FileType.Eml:
case FileType.Pdf:
case FileType.Word:
return result + '-text';
case FileType.Code:
case FileType.Image:
case FileType.Audio:
case FileType.Video:
case FileType.Archive:
case FileType.Certificate:
case FileType.Spreadsheet:
case FileType.Presentation:
case FileType.Calendar:
return result + '-' + fileType;
}
return result;
},
getIconClass: (ext, mime) => FileInfo.getTypeIconClass(FileInfo.getType(ext, mime)),
/**
* @param {string} sFileType
* @returns {string}
*/
getAttachmentsIconClass: data => {
if (arrayLength(data)) {
let icons = data
.map(item => item ? FileInfo.getIconClass(FileInfo.getExtension(item.fileName), item.mimeType) : '')
.validUnique();
return (1 === icons?.length && 'icon-file' !== icons[0])
? icons[0]
: 'icon-attachment';
}
return '';
},
friendlySize: bytes => {
bytes = pInt(bytes);
let i = bytes ? Math.floor(Math.log(bytes) / Math.log(1024)) : 0;
return (bytes / Math.pow(1024, i)).toFixed(2>i ? 0 : 1) + ' ' + sizes[i];
}
};

View file

@ -1,211 +0,0 @@
import { RFC822 } from 'Common/File';
import { getFolderInboxName, getFolderFromCacheList } from 'Common/Cache';
import { baseCollator } from 'Common/Translator';
import { SettingsGet } from 'Common/Globals';
import { isArray, arrayLength, pInt } from 'Common/Utils';
import { SettingsUserStore } from 'Stores/User/Settings';
import { FolderUserStore } from 'Stores/User/Folder';
import { MessagelistUserStore } from 'Stores/User/Messagelist';
import Remote from 'Remote/User/Fetch';
let refreshInterval,
// Default every 15 minutes
refreshFoldersInterval = 900000;
export const
setRefreshFoldersInterval = minutes => {
refreshFoldersInterval = Math.max(1, pInt(SettingsGet('minRefreshInterval')), pInt(minutes)) * 60000;
clearInterval(refreshInterval);
refreshInterval = setInterval(() => {
const cF = FolderUserStore.currentFolderFullName(),
iF = getFolderInboxName();
folderInformation(iF);
iF === cF || folderInformation(cF);
folderInformationMultiply();
}, refreshFoldersInterval);
},
sortFolders = folders => {
try {
let collator = baseCollator(true);
folders.sort((a, b) =>
a.isInbox() ? -1 : (b.isInbox() ? 1 : collator.compare(a.fullName, b.fullName))
);
} catch (e) {
console.error(e);
}
},
/**
* @param {Array=} aDisabled
* @param {Array=} aHeaderLines
* @param {Function=} fRenameCallback
* @param {Function=} fDisableCallback
* @param {boolean=} bNoSelectSelectable Used in FolderCreatePopupView
* @returns {Array}
*/
folderListOptionsBuilder = (
aDisabled,
aHeaderLines,
fRenameCallback,
fDisableCallback
) => {
const
aResult = [],
sDeepPrefix = '\u00A0\u00A0\u00A0',
// FolderSystemPopupView should always be true
showUnsubscribed = fRenameCallback ? !SettingsUserStore.hideUnsubscribed() : true,
isDisabled = fDisableCallback || (item => !item.selectable() || aDisabled.includes(item.fullName)),
foldersWalk = folders => {
folders.forEach(oItem => {
if (showUnsubscribed || oItem.hasSubscriptions() || !oItem.exists) {
aResult.push({
id: oItem.fullName,
name:
sDeepPrefix.repeat(oItem.deep) +
fRenameCallback(oItem),
system: false,
disabled: isDisabled(oItem)
});
}
foldersWalk(oItem.subFolders());
});
};
fDisableCallback = fDisableCallback || (() => false);
fRenameCallback = fRenameCallback || (oItem => oItem.name());
isArray(aDisabled) || (aDisabled = []);
isArray(aHeaderLines) && aHeaderLines.forEach(line =>
aResult.push({
id: line[0],
name: line[1],
system: false,
disabled: false
})
);
foldersWalk(FolderUserStore.folderList());
return aResult;
},
/**
* @param {string} folder
* @param {Array=} list = []
*/
folderInformation = (folder, list) => {
if (folder?.trim()) {
let count = 1;
const uids = [];
if (arrayLength(list)) {
list.forEach(messageListItem => {
uids.push(messageListItem.uid);
messageListItem.threads.forEach(uid => uids.push(uid));
});
count = uids.length;
}
if (count) {
Remote.request('FolderInformation', (iError, data) => {
if (!iError && data.Result) {
const result = data.Result,
folderFromCache = getFolderFromCacheList(result.name);
if (folderFromCache) {
const oldHash = folderFromCache.etag,
unreadCountChange = (folderFromCache.unreadEmails() !== result.unreadEmails);
// folderFromCache.revivePropertiesFromJson(result);
folderFromCache.expires = Date.now();
folderFromCache.uidNext = result.uidNext;
folderFromCache.etag = result.etag;
folderFromCache.totalEmails(result.totalEmails);
folderFromCache.unreadEmails(result.unreadEmails);
MessagelistUserStore.notifyNewMessages(folderFromCache.fullName, result.newMessages);
if (!oldHash || unreadCountChange || result.etag !== oldHash) {
if (folderFromCache.fullName === FolderUserStore.currentFolderFullName()) {
MessagelistUserStore.reload();
/*
} else if (getFolderInboxName() === folderFromCache.fullName) {
// Remote.messageList(null, {folder: getFolderFromCacheList(getFolderInboxName())}, true);
Remote.messageList(null, {folder: getFolderInboxName()}, true);
*/
}
}
}
}
}, {
folder: folder,
flagsUids: uids,
uidNext: getFolderFromCacheList(folder)?.uidNext || 0 // Used to check for new messages
});
}
}
},
/**
* @param {boolean=} boot = false
*/
folderInformationMultiply = (boot = false) => {
const folders = FolderUserStore.getNextFolderNames(refreshFoldersInterval);
if (arrayLength(folders)) {
Remote.request('FolderInformationMultiply', (iError, oData) => {
if (!iError && arrayLength(oData.Result)) {
const utc = Date.now();
oData.Result.forEach(item => {
const folder = getFolderFromCacheList(item.name);
if (folder) {
const oldHash = folder.etag,
unreadCountChange = folder.unreadEmails() !== item.unreadEmails;
// folder.revivePropertiesFromJson(item);
folder.expires = utc;
folder.etag = item.etag;
folder.totalEmails(item.totalEmails);
folder.unreadEmails(item.unreadEmails);
if (!oldHash || item.etag !== oldHash) {
if (folder.fullName === FolderUserStore.currentFolderFullName()) {
MessagelistUserStore.reload();
}
} else if (unreadCountChange
&& folder.fullName === FolderUserStore.currentFolderFullName()
&& MessagelistUserStore.length) {
folderInformation(folder.fullName, MessagelistUserStore());
}
}
});
boot && setTimeout(() => folderInformationMultiply(true), 2000);
}
}, {
folders: folders
});
}
},
dropFilesInFolder = (sFolderFullName, files) => {
let count = files.length;
for (const file of files) {
if (RFC822 === file.type) {
let data = new FormData;
data.append('folder', sFolderFullName);
data.append('appendFile', file);
Remote.request('FolderAppend', (iError, data)=>{
iError && console.error(data.message);
0 == --count
&& FolderUserStore.currentFolderFullName() == sFolderFullName
&& MessagelistUserStore.reload(true, true);
}, data);
} else {
--count;
}
}
};

View file

@ -1,22 +0,0 @@
import { doc, appEl, $htmlCL } from 'Common/Globals';
// Fullscreen must be on app, else other popups fail
export const
appFullscreen = () => (doc.fullscreenElement || doc.webkitFullscreenElement) === appEl,
exitFullscreen = () => appFullscreen() && (doc.exitFullscreen || doc.webkitExitFullscreen).call(doc),
isFullscreen = ko.observable(false),
toggleFullscreen = () => isFullscreen() ? exitFullscreen() : appEl.requestFullscreen();
if (appEl) {
let event = 'fullscreenchange';
if (!appEl.requestFullscreen && appEl.webkitRequestFullscreen) {
appEl.requestFullscreen = appEl.webkitRequestFullscreen;
event = 'webkit'+event;
}
if (appEl.requestFullscreen) {
doc.addEventListener(event, () => {
isFullscreen(appFullscreen());
$htmlCL.toggle('rl-fullscreen', appFullscreen());
});
}
}

View file

@ -1,79 +1,68 @@
import ko from 'ko';
import { KeyState } from 'Common/Enums';
let keyScopeFake = 'all';
export const $html = document.documentElement;
export const $htmlCL = $html.classList;
export const
ScopeMenu = 'Menu',
/**
* @type {?}
*/
export const dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 });
doc = document,
/**
* @type {boolean}
*/
export const bMobileDevice = (/android|iphone|ipod|ipad|blackberry|mobile/i).test(
(navigator.userAgent && navigator.userAgent.toLowerCase()) || ''
);
$htmlCL = doc.documentElement.classList,
export const VIEW_MODELS = {
settings: [],
'settings-removed': [],
'settings-disabled': []
};
elementById = id => doc.getElementById(id),
export const moveAction = ko.observable(false);
export const leftPanelDisabled = ko.observable(false);
export const leftPanelType = ko.observable('');
export const leftPanelWidth = ko.observable(0);
appEl = elementById('rl-app'),
Settings = rl.settings,
SettingsGet = Settings.get,
SettingsAdmin = name => (SettingsGet('Admin') || {})[name],
SettingsCapa = name => name && !!(SettingsGet('Capa') || {})[name],
dropdowns = [],
dropdownVisibility = ko.observable(false).extend({ rateLimit: 0 }),
leftPanelDisabled = ko.observable(false),
toggleLeftPanel = () => leftPanelDisabled(!leftPanelDisabled()),
createElement = (name, attr) => {
let el = doc.createElement(name);
attr && Object.entries(attr).forEach(([k,v]) => el.setAttribute(k,v));
return el;
},
fireEvent = (name, detail, cancelable) => dispatchEvent(
new CustomEvent(name, {detail:detail, cancelable: !!cancelable})
),
stopEvent = event => {
event.preventDefault();
event.stopPropagation();
},
formFieldFocused = () => doc.activeElement?.matches('input,textarea'),
addShortcut = (...args) => shortcuts.add(...args),
registerShortcut = (keys, modifiers, scopes, method) =>
addShortcut(keys, modifiers, scopes, event => formFieldFocused() ? true : method(event)),
addEventsListener = (element, events, fn, options) =>
events.forEach(event => element.addEventListener(event, fn, options)),
addEventsListeners = (element, events) =>
Object.entries(events).forEach(([event, fn]) => element.addEventListener(event, fn)),
// keys / shortcuts
keyScopeReal = ko.observable('all'),
keyScope = value => {
if (!value) {
return keyScopeFake;
}
if (ScopeMenu !== value) {
keyScopeFake = value;
if (dropdownVisibility()) {
value = ScopeMenu;
}
}
keyScopeReal(value);
shortcuts.setScope(value);
};
dropdownVisibility.subscribe(value => {
if (value) {
keyScope(ScopeMenu);
} else if (ScopeMenu === shortcuts.getScope()) {
keyScope(keyScopeFake);
leftPanelDisabled.subscribe((value) => {
if (value && moveAction()) {
moveAction(false);
}
});
leftPanelDisabled.subscribe(value => $htmlCL.toggle('rl-left-panel-disabled', value));
moveAction.subscribe((value) => {
if (value && leftPanelDisabled()) {
leftPanelDisabled(false);
}
});
// keys
export const keyScopeReal = ko.observable(KeyState.All);
export const keyScopeFake = ko.observable(KeyState.All);
export const keyScope = ko.computed({
read: () => keyScopeFake(),
write: value => {
if (KeyState.Menu !== value) {
keyScopeFake(value);
if (dropdownVisibility()) {
value = KeyState.Menu;
}
}
keyScopeReal(value);
}
});
keyScopeReal.subscribe(value => shortcuts.setScope(value));
dropdownVisibility.subscribe(value => {
if (value) {
keyScope(KeyState.Menu);
} else if (KeyState.Menu === shortcuts.getScope()) {
keyScope(keyScopeFake());
}
});

View file

@ -1,846 +0,0 @@
import { createElement } from 'Common/Globals';
import { forEachObjectEntry, isArray, pInt } from 'Common/Utils';
import { SettingsUserStore } from 'Stores/User/Settings';
const
tmpl = createElement('template'),
turndown = new TurndownService(),
htmlre = /[&<>"']/g,
httpre = /^(https?:)?\/\//i,
htmlmap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;'
},
keepTagContent = 'form,button,data', // font
allowedTags = [
// Structural Elements:
'blockquote','br','div','figcaption','figure','h1','h2','h3','h4','h5','h6','hgroup','hr','p','wbr',
'article','aside','header','footer','main','section',
'details','summary','nav',
// List Elements
'dd','dl','dt','li','ol','ul',
// Text Formatting Elements
'a','abbr','address','b','bdi','bdo','cite','code','del','dfn',
'em','i','ins','kbd','mark','pre','q','rp','rt','ruby','s','samp','small',
'span','strong','sub','sup','time','u','var',
// Deprecated by HTML Standard
'acronym','big','center','dir','font','marquee',
'nobr','plaintext','rb','rtc','strike','tt',
// Media Elements
'img',//'picture','source',
// Table Elements
'caption','col','colgroup','table','tbody','td','tfoot','th','thead','tr',
// Disallowed but converted later
'style','xmp'
].join(','),
nonEmptyTags = [
'A','B','EM','I','SPAN','STRONG'
],
blockquoteSwitcher = () => {
SettingsUserStore.collapseBlockquotes() &&
// tmpl.content.querySelectorAll('blockquote').forEach(node => {
[...tmpl.content.querySelectorAll('blockquote')].reverse().forEach(node => {
const el = createElement('details', {class:'sm-bq-switcher'});
el.innerHTML = '<summary>•••</summary>';
node.replaceWith(el);
el.append(node);
});
},
replaceWithChildren = node => node.replaceWith(...[...node.childNodes]),
urlRegExp = /https?:\/\/[^\p{C}\p{Z}]+[^\p{C}\p{Z}.]/gu,
// eslint-disable-next-line max-len
email = /(^|\r|\n|\p{C}\p{Z})((?:[^"(),.:;<>@[\]\\\p{C}\p{Z}]+(?:\.[^"(),.:;<>@[\]\\\p{C}\p{Z}]+)*|"(?:\\?[^"\\\p{C}\p{Z}])*")@[^@\p{C}\p{Z}]+[^@\p{C}\p{Z}.])/gui,
// rfc3966
tel = /(tel:(\+[0-9().-]+|[0-9*#().-]+(;phone-context=\+[0-9+().-]+)?))/g,
// Strip tracking
/** TODO: implement other url strippers like from
* https://www.bleepingcomputer.com/news/security/new-firefox-privacy-feature-strips-urls-of-tracking-parameters/
* https://github.com/newhouse/url-tracking-stripper
* https://github.com/svenjacobs/leon
* https://maxchadwick.xyz/tracking-query-params-registry/
* https://github.com/M66B/FairEmail/blob/master/app/src/main/java/eu/faircode/email/UriHelper.java
*/
// eslint-disable-next-line max-len
stripParams = /^(utm_|ec_|fbclid|mc_eid|mkt_tok|_hsenc|vero_id|oly_enc_id|oly_anon_id|__s|Referrer|mailing|elq|bch|trc|ref|correlation_id|pd_|pf_|email_hash)$/i,
urlGetParam = (url, name) => new URL(url).searchParams.get(name) || url,
base64Url = data => atob(data.replace(/_/g,'/').replace(/-/g,'+')),
decode = decodeURIComponent,
stripTracking = url => {
try {
let nurl = url
// Copernica
.replace(/^.+\/(https%253A[^/?&]+).*$/i, (...m) => decode(decode(m[1])))
.replace(/tracking\.(printabout\.nl[^?]+)\?.*/i, (...m) => m[1])
.replace(/(zalando\.nl[^?]+)\?.*/i, (...m) => m[1])
.replace(/^.+(awstrack\.me|redditmail\.com)\/.+(https:%2F%2F[^/]+).*/i, (...m) => decode(m[2]))
.replace(/^.+(www\.google|safelinks\.protection\.outlook\.com|mailchimp\.com).+url=.+$/i,
() => urlGetParam(url, 'url'))
.replace(/^.+click\.godaddy\.com.+$/i, () => urlGetParam(url, 'redir'))
.replace(/^.+delivery-status\.com.+$/i, () => urlGetParam(url, 'fb'))
.replace(/^.+go\.dhlparcel\.nl.+\/([A-Za-z0-9_-]+)$/i, (...m) => base64Url(m[1]))
.replace(/^(.+mopinion\.com.+)\?.*$/i, (...m) => m[1])
.replace(/^.+sellercentral\.amazon\.com\/nms\/redirect.+$/i, () => base64Url(urlGetParam(url, 'u')))
.replace(/^.+amazon\.com\/gp\/r\.html.+$/i, () => urlGetParam(url, 'U'))
// Mandrill
.replace(/^.+\/track\/click\/.+\?p=.+$/i, () => {
let d = urlGetParam(url, 'p');
try {
d = JSON.parse(base64Url(d));
if (d?.p) {
d = JSON.parse(d.p);
}
} catch (e) {
console.error(e);
}
return d?.url || url;
})
// Remove invalid URL characters
.replace(/[\s<>]+/gi, '');
nurl = new URL(nurl);
let s = nurl.searchParams;
[...s.keys()].forEach(key => stripParams.test(key) && s.delete(key));
return nurl.toString();
} catch (e) {
console.dir({
error:e,
url:url
});
}
return url;
},
cleanCSS = source =>
source.trim()
.replace(/;\s*-[^;]+/g, '')
.replace(/^\s*-[^;]+(;|$)/g, '')
.replace(/white-space[^;]+/g, '')
// Drop Microsoft Office style properties
// .replace(/mso-[^:;]+:[^;]+/gi, '')
,
/*
Parses given css string, and returns css object
keys as selectors and values are css rules
eliminates all css comments before parsing
@param source css string to be parsed
@return object css
*/
parseCSS = source => {
const css = [];
css.toString = () => css.reduce(
(ret, tmp) =>
ret + tmp.selector + ' {\n'
+ (tmp.type === 'media' ? tmp.subStyles.toString() : tmp.rules)
+ '}\n'
,
''
);
/**
* Given css array, parses it and then for every selector,
* prepends namespace to prevent css collision issues
*/
css.applyNamespace = namespace => css.forEach(obj => {
if (obj.type === 'media') {
obj.subStyles.applyNamespace(namespace);
} else {
obj.selector = obj.selector.split(',').map(selector =>
(namespace + ' .mail-body ' + selector.replace(/\./g, '.msg-'))
.replace(/\sbody/gi, '')
).join(',');
}
});
if (source) {
source = source
// strip comments
.replace(/\/\*[\s\S]*?\*\//gi, '')
// strip MS Word comments
.replace(/<!--[\s\S]*?-->/gi, '')
// strip HTML, as < is no CSS combinator anyway
.replace(/<[\s\S]*/gi, '');
// .replace(/<\/?[a-z][\s\S]*?>/gi, '');
// unified regex to match css & media queries together
let unified = /(?:(\s*?@(?:media)[\s\S]*?){([\s\S]*?)}\s*?})|(?:([\s\S]*?){([\s\S]*?)})/gi,
arr;
while (true) {
arr = unified.exec(source);
if (arr === null) {
break;
}
let selector = arr[arr[2] === undefined ? 3 : 1].split('\r\n').join('\n').trim()
// Never have more than a single line break in a row
.replace(/\n+/, "\n")
// Remove :root and html
.split(/\s+/g).map(item => item.replace(/^(:root|html)$/, '')).join(' ').trim();
// determine the type
if (selector.includes('@media')) {
// we have a media query
css.push({
selector: selector,
type: 'media',
subStyles: parseCSS(arr[2] + '\n}') //recursively parse media query inner css
});
} else if (selector && !selector.includes('@')) {
// we have standard css
// ignores @import, @keyframe, @font-face statements
css.push({
selector: selector,
rules: cleanCSS(arr[4])
});
}
}
}
return css;
};
export const
/**
* @param {string} text
* @returns {string}
*/
encodeHtml = text => (text?.toString?.() || '' + text).replace(htmlre, m => htmlmap[m]),
/**
* Clears the Message Html for viewing
* @param {string} text
* @returns {string}
*/
cleanHtml = (html, oAttachments, msgId) => {
let aColor;
const
debug = false, // Config()->Get('debug', 'enable', false);
detectHiddenImages = true, // !!SettingsGet('try_to_detect_hidden_images'),
bqLevel = parseInt(SettingsUserStore.maxBlockquotesLevel()),
minFontSize = parseInt(SettingsUserStore.minFontSize()),
result = {
hasExternals: false,
tracking: false,
linkedData: []
},
isMsg = !!msgId,
findAttachmentByCid = cId => oAttachments.findByCid(cId),
findLocationByCid = cId => {
const attachment = findAttachmentByCid(cId);
return attachment?.contentLocation ? attachment : 0;
},
// convert body attributes to CSS
tasks = {
link: value => aColor = value,
text: (value, node) => node.style.color = value,
topmargin: (value, node) => node.style.marginTop = pInt(value) + 'px',
leftmargin: (value, node) => node.style.marginLeft = pInt(value) + 'px',
bottommargin: (value, node) => node.style.marginBottom = pInt(value) + 'px',
rightmargin: (value, node) => node.style.marginRight = pInt(value) + 'px'
},
allowedAttributes = [
// SnappyMail
// 'data-x-href-broken',
// 'data-x-href-tracking',
'data-x-src',
// 'data-x-src-hidden',
// 'data-x-src-tracking',
// 'data-x-src-broken',
// 'data-x-style-url',
// 'data-x-style-broken-urls',
// defaults
'name',
'dir', 'lang', 'style', 'title',
'background', 'bgcolor', 'alt', 'height', 'width', 'src', 'href',
'border', 'bordercolor', 'charset', 'direction',
// a
'download', 'hreflang',
// body
'alink', 'bottommargin', 'leftmargin', 'link', 'rightmargin', 'text', 'topmargin', 'vlink',
// col
'align', 'valign',
// font
'color', 'face', 'size',
// hr
'noshade',
// img
'hspace', 'sizes', 'srcset', 'vspace',
// meter
'low', 'high', 'optimum', 'value',
// ol
'reversed', 'start',
// table
'cols', 'rows', 'frame', 'rules', 'summary', 'cellpadding', 'cellspacing',
// th
'abbr', 'scope',
// td
'colspan', 'rowspan', 'headers'
// others
//'class', 'id', 'target'
];
if (SettingsUserStore.allowStyles()) {
allowedAttributes.push('class');
} else {
msgId = 0;
}
tmpl.innerHTML = html
// .replace(/<pre[^>]*>[\s\S]*?<\/pre>/gi, pre => pre.replace(/\n/g, '\n<br>'))
// Not supported by <template> element
// .replace(/<!doctype[^>]*>/gi, '')
// .replace(/<\?xml[^>]*\?>/gi, '')
// https://github.com/the-djmaze/snappymail/issues/1860
.replace(/<(\/?)x-html(\s[^>]*)?>/gi, '')
.replace(/<(\/?)head(\s[^>]*)?>/gi, '')
.replace(/<(\/?)body(\s[^>]*)?>/gi, '<$1div class="mail-body"$2>')
// .replace(/<\/?(html|head)[^>]*>/gi, '')
// Fix Reddit https://github.com/the-djmaze/snappymail/issues/540
.replace(/<span class="preview-text"[\s\S]+?<\/span>/, '')
// https://github.com/the-djmaze/snappymail/issues/900
.replace(/\u2028/g,' ')
// https://github.com/the-djmaze/snappymail/issues/1415
.replace(/<br>\s*<\/p>/gi,'</p>')
.trim();
html = '';
// Strip all comments
const nodeIterator = document.createNodeIterator(tmpl.content, NodeFilter.SHOW_COMMENT);
while (nodeIterator.nextNode()) {
nodeIterator.referenceNode.remove();
}
/**
* Basic support for Linked Data (Structured Email)
* https://json-ld.org/
* https://structured.email/
**/
tmpl.content.querySelectorAll('script[type="application/ld+json"]').forEach(oElement => {
// Could be array of objects or single object
try {
const data = JSON.parse(oElement.textContent);
(isArray(data) ? data : [data]).forEach(entry => result.linkedData.push(entry));
} catch (e) {
console.error(e, oElement.textContent);
}
});
// https://github.com/the-djmaze/snappymail/issues/1125
tmpl.content.querySelectorAll(keepTagContent).forEach(oElement => replaceWithChildren(oElement));
tmpl.content.querySelectorAll(
':not('+allowedTags+'),a:empty,span:empty'
+ (0 < bqLevel ? ',' + (new Array(1 + bqLevel).fill('blockquote').join(' ')) : '')
).forEach(oElement => oElement.remove());
/* // Is this slower or faster?
).forEach(oElement => {
if (!node || !node.contains(oElement)) {
oElement.remove();
node = oElement;
}
});
*/
// https://github.com/the-djmaze/snappymail/issues/1641
let body = tmpl.content.querySelector('.mail-body');
[...tmpl.content.querySelectorAll('.mail-body + .mail-body')]
.forEach(oElement => body.append(...oElement.childNodes));
/*
.forEach(oElement => {
let bq = createElement('blockquote');
bq.append(...oElement.childNodes);
body.replaceWith(bq);
});
*/
[...tmpl.content.querySelectorAll('*')].forEach(oElement => {
const name = oElement.tagName,
oStyle = oElement.style;
if ('STYLE' === name) {
let css = msgId ? parseCSS(oElement.textContent) : [];
if (css.length) {
css.applyNamespace(msgId);
css = css.toString();
if (SettingsUserStore.removeColors()) {
css = css.replace(/(background-)?color:[^};]+;?/g, '');
}
oElement.textContent = css;
} else {
oElement.remove();
}
return;
}
if ('XMP' === name) {
const pre = createElement('pre');
pre.innerHTML = encodeHtml(oElement.innerHTML);
oElement.replaceWith(pre);
return;
}
// \MailSo\Base\HtmlUtils::ClearTags()
if ('none' == oStyle.display
|| 'hidden' == oStyle.visibility
// || (oStyle.lineHeight && 1 > parseFloat(oStyle.lineHeight)
// || (oStyle.maxHeight && 1 > parseFloat(oStyle.maxHeight)
// || (oStyle.maxWidth && 1 > parseFloat(oStyle.maxWidth)
// || ('0' === oStyle.opacity
) {
oElement.remove();
return;
}
const aAttrsForRemove = [],
className = oElement.className,
hasAttribute = name => oElement.hasAttribute(name),
getAttribute = name => hasAttribute(name) ? oElement.getAttribute(name).trim() : '',
setAttribute = (name, value) => oElement.setAttribute(name, value),
delAttribute = name => {
let value = getAttribute(name);
oElement.removeAttribute(name);
return value;
};
if ('mail-body' === className) {
forEachObjectEntry(tasks, (name, cb) =>
hasAttribute(name) && cb(delAttribute(name), oElement)
);
} else if (msgId && className) {
oElement.className = className.replace(/(^|\s+)/g, '$1msg-');
}
if (oElement.hasAttributes()) {
let i = oElement.attributes.length;
while (i--) {
let sAttrName = oElement.attributes[i].name.toLowerCase();
if (!allowedAttributes.includes(sAttrName) && ('class' !== sAttrName || 'mail-body' !== className)) {
delAttribute(sAttrName);
aAttrsForRemove.push(sAttrName);
}
}
}
let value;
// if ('TABLE' === name || 'TD' === name || 'TH' === name) {
if (!oStyle.backgroundImage) {
if ('TD' !== name && 'TH' !== name) {
['width','height'].forEach(key => {
if (hasAttribute(key)) {
value = delAttribute(key);
oStyle[key] || (oStyle[key] = value.includes('%') ? value : value + 'px');
}
});
// Make width responsive
value = oStyle.width;
if (100 < parseInt(value,10) && !oStyle.maxWidth) {
oStyle.maxWidth = value;
oStyle.width = '100%';
}
// Make height responsive
value = oStyle.removeProperty('height');
if (value && !oStyle.maxHeight) {
oStyle.maxHeight = value;
}
}
}
// } else
if ('A' === name) {
value = oElement.href;
if (!/^([a-z]+):/i.test(value)) {
setAttribute('data-x-href-broken', value);
delAttribute('href');
} else {
oElement.href = stripTracking(value);
if (oElement.href != value) {
result.tracking = true;
setAttribute('data-x-href-tracking', value);
}
setAttribute('target', '_blank');
// setAttribute('rel', 'external nofollow noopener noreferrer');
}
setAttribute('tabindex', '-1');
aColor && !oElement.style.color && (oElement.style.color = aColor);
}
// if (['CENTER','FORM'].includes(name)) {
if (nonEmptyTags.includes(name) && ('' == oElement.textContent.trim())) {
('A' !== name || !oElement.querySelector('IMG')) && replaceWithChildren(oElement);
return;
}
// SVG xlink:href
/*
if (hasAttribute('xlink:href')) {
delAttribute('xlink:href');
}
*/
let skipStyle = false;
if (isMsg) {
value = isMsg && delAttribute('src');
if (value) {
if ('IMG' === name) {
oElement.loading = 'lazy';
let attachment;
if (value.startsWith('cid:'))
{
setAttribute('data-x-src', value);
value = value.slice(4);
attachment = findAttachmentByCid(value);
if (attachment?.download) {
oElement.src = attachment.linkPreview();
// oElement.title || (oElement.title = '+attachment.fileName);
attachment.isInline(true);
attachment.isLinked(true);
}
}
else if ((attachment = findLocationByCid(value)))
{
if (attachment.download) {
oElement.src = attachment.linkPreview();
attachment.isLinked(true);
}
}
else if (detectHiddenImages
&& ((oStyle.maxHeight && 3 > pInt(oStyle.maxHeight)) // TODO: issue with 'in'
|| (oStyle.maxWidth && 3 > pInt(oStyle.maxWidth)) // TODO: issue with 'in'
|| (oStyle.width && 2 > pInt(oStyle.width))
|| [
'email.microsoftemail.com/open',
'github.com/notifications/beacon/',
'/track/open', // mandrillapp.com list-manage.com
'google-analytics.com'
].filter(uri => value.toLowerCase().includes(uri)).length
)) {
skipStyle = true;
oStyle.display = 'none';
// setAttribute('style', 'display:none');
setAttribute('data-x-src-hidden', value);
// result.tracking = true;
}
else if (httpre.test(value))
{
let src = stripTracking(value);
if (src != value) {
result.tracking = true;
setAttribute('data-x-src-tracking', value);
}
setAttribute('data-x-src', src);
result.hasExternals = true;
oElement.alt || (oElement.alt = src.replace(/^.+\/([^/?]+).*$/, '$1').slice(-20));
}
else if (value.startsWith('data:image/'))
{
oElement.src = value;
}
else
{
setAttribute('data-x-src-broken', value);
}
}
else
{
setAttribute('data-x-src-broken', value);
}
}
}
if (hasAttribute('background')) {
oStyle.backgroundImage = 'url("' + delAttribute('background') + '")';
}
if (hasAttribute('bgcolor')) {
oStyle.backgroundColor = delAttribute('bgcolor');
}
if (hasAttribute('color')) {
oStyle.color = delAttribute('color');
}
if (!skipStyle) {
/*
if ('fixed' === oStyle.position) {
oStyle.position = 'absolute';
}
*/
oStyle.removeProperty('behavior');
oStyle.removeProperty('cursor');
oStyle.removeProperty('min-width');
const
urls_remote = [], // 'data-x-style-url'
urls_broken = []; // 'data-x-broken-style-src'
['backgroundImage', 'listStyleImage', 'content'].forEach(property => {
if (oStyle[property]) {
let value = oStyle[property],
found = value.match(/url\s*\(([^)]+)\)/i);
if (found) {
oStyle[property] = null;
found = found[1].replace(/^["'\s]+|["'\s]+$/g, '');
let lowerUrl = found.toLowerCase();
if (lowerUrl.startsWith('cid:')) {
const attachment = findAttachmentByCid(found);
if (attachment?.linkPreview && name) {
oStyle[property] = "url('" + attachment.linkPreview() + "')";
attachment.isInline(true);
attachment.isLinked(true);
}
} else if (httpre.test(lowerUrl)) {
result.hasExternals = true;
urls_remote.push([property, found]);
} else if (lowerUrl.startsWith('data:image/')) {
oStyle[property] = value;
} else {
urls_broken.push([property, found]);
}
}
}
});
// oStyle.removeProperty('background-image');
// oStyle.removeProperty('list-style-image');
if (urls_remote.length) {
setAttribute('data-x-style-url', JSON.stringify(urls_remote));
}
if (urls_broken.length) {
setAttribute('data-x-style-broken-urls', JSON.stringify(urls_broken));
}
// https://github.com/the-djmaze/snappymail/issues/1082
if (minFontSize > pInt(oStyle.fontSize)) {
oStyle.fontSize = minFontSize + "px";
// oStyle.removeProperty('font-size');
}
// Removes background and color
// Many e-mails incorrectly only define one, not both
// And in dark theme mode this kills the readability
if (SettingsUserStore.removeColors()) {
oStyle.removeProperty('background-color');
oStyle.removeProperty('background-image');
oStyle.removeProperty('color');
}
oStyle.cssText && (oStyle.cssText = cleanCSS(oStyle.cssText));
}
if (debug && aAttrsForRemove.length) {
setAttribute('data-removed-attrs', aAttrsForRemove.join(', '));
}
});
isMsg && blockquoteSwitcher();
// return tmpl.content.firstChild;
result.html = tmpl.innerHTML.trim();
return result;
},
/**
* @param {string} html
* @returns {string}
*/
htmlToPlain = html => {
if (SettingsUserStore.markdown()) {
return htmlToMarkdown(html);
}
const
hr = '⎯'.repeat(64),
forEach = (selector, fn) => tmpl.content.querySelectorAll(selector).forEach(fn),
blockquotes = node => {
let bq;
while ((bq = node.querySelector('blockquote'))) {
// Convert child blockquote first
blockquotes(bq);
// Convert blockquote
// bq.innerHTML = '\n' + ('\n' + bq.innerHTML.replace(/\n{3,}/gm, '\n\n').trim() + '\n').replace(/^/gm, '&gt; ');
// replaceWithChildren(bq);
bq.replaceWith(
'\n' + ('\n' + bq.textContent.replace(/\n{3,}/g, '\n\n').trim() + '\n').replace(/^/gm, '> ')
);
}
};
html = html
.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gim, (...args) =>
1 < args.length ? args[1].toString().replace(/\n/g, '<br>') : '')
// Remove line duplication
.replace(/<br><\/div>/gi, '</div>')
.replace(/\r?\n/g, '')
.replace(/\s+/gm, ' ');
while (/<(div|tr)[\s>]/i.test(html)) {
html = html.replace(/\n*<(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n');
}
while (/<\/(div|tr)[\s>]/i.test(html)) {
html = html.replace(/\n*<\/(div|tr)(\s[\s\S]*?)?>\n*/gi, '\n');
}
tmpl.innerHTML = html
.replace(/<t[dh](\s[\s\S]*?)?>/gi, '\t')
.replace(/<\/tr(\s[\s\S]*?)?>/gi, '\n');
forEach('style', node => node.remove());
// lines
forEach('hr', node => node.replaceWith(`\n\n${hr}\n\n`));
// headings
forEach('h1,h2,h3,h4,h5,h6', h => h.replaceWith(`\n\n${'#'.repeat(h.tagName[1])} ${h.textContent}\n\n`));
// paragraphs
forEach('p', node => {
node.prepend('\n\n');
if ('' == node.textContent.trim()) {
node.remove();
} else {
node.after('\n\n');
}
});
// proper indenting and numbering of (un)ordered lists
forEach('ol,ul', node => {
let prefix = '',
parent = node,
ordered = 'OL' == node.tagName,
i = 0;
while ((parent = parent?.parentNode?.closest?.('ol,ul'))) {
prefix = ' ' + prefix;
}
node.querySelectorAll(':scope > li').forEach(li => {
li.prepend('\n' + prefix + (ordered ? `${++i}. ` : ' * '));
});
node.prepend('\n\n');
node.after('\n\n');
});
// Convert anchors
forEach('a', a => {
let txt = a.textContent, href = a.href;
return a.replaceWith(
txt.replace(/[\s()-]+/g, '').includes(href.replace(/^[a-z]:/, '').replace(/[\s()-]+/g, ''))
? txt
: txt + ' ' + href + ' '
);
});
// Bold
forEach('b,strong', b => b.replaceWith(`**${b.textContent}**`));
// Italic
forEach('i,em', i => i.replaceWith(`*${i.textContent}*`));
// Convert line-breaks
tmpl.innerHTML = tmpl.innerHTML
.replace(/\n{3,}/gm, '\n\n')
.replace(/\n<br[^>]*>/g, '\n')
.replace(/<br[^>]*>\n/g, '\n');
forEach('br', br => br.replaceWith('\n'));
// Blockquotes must be last
blockquotes(tmpl.content);
return (tmpl.content.textContent || '').trim();
},
htmlToMarkdown = html => {
tmpl.innerHTML = html;
return turndown.turndown(tmpl.content);
},
/**
* @param {string} plain
* @param {boolean} findEmailAndLinksInText = false
* @returns {string}
*/
plainToHtml = plain => {
plain = plain.toString()
.replace(/\r/g, '')
.replace(/^>[> ]>+/gm, ([match]) => (match ? match.replace(/[ ]+/g, '') : match))
// https://github.com/the-djmaze/snappymail/issues/900
.replace(/\u2028/g,' ');
let bIn = false,
bDo = true,
bStart = true,
aNextText = [],
aText = plain.split('\n');
do {
bDo = false;
aNextText = [];
aText.forEach(sLine => {
bStart = '>' === sLine.slice(0, 1);
if (bStart && !bIn) {
bDo = true;
bIn = true;
aNextText.push('~~~blockquote~~~');
aNextText.push(sLine.slice(1));
} else if (!bStart && bIn) {
if (sLine) {
bIn = false;
aNextText.push('~~~/blockquote~~~');
aNextText.push(sLine);
} else {
aNextText.push(sLine);
}
} else if (bStart && bIn) {
aNextText.push(sLine.slice(1));
} else {
aNextText.push(sLine);
}
});
if (bIn) {
bIn = false;
aNextText.push('~~~/blockquote~~~');
}
aText = aNextText;
} while (bDo);
tmpl.innerHTML = aText.join('\n')
// .replace(/~~~\/blockquote~~~\n~~~blockquote~~~/g, '\n')
.replace(/&/g, '&amp;')
.replace(/>/g, '&gt;')
.replace(/</g, '&lt;')
.replace(urlRegExp, (...m) => {
m[0] = stripTracking(m[0]);
return `<a href="${m[0]}" target="_blank">${m[0]}</a>`;
})
.replace(email, '$1<a href="mailto:$2">$2</a>')
.replace(tel, '<a href="$1">$1</a>')
.replace(/~~~blockquote~~~\s*/g, '<blockquote>')
.replace(/\s*~~~\/blockquote~~~/g, '</blockquote>')
.replace(/\n/g, '<br>');
blockquoteSwitcher();
return tmpl.innerHTML.trim();
};
rl.Utils = {
cleanHtml: cleanHtml,
htmlToPlain: htmlToPlain,
plainToHtml: plainToHtml,
htmlToMarkdown: htmlToMarkdown
// markdownToHtml: md => marked.parse(md)
};

View file

@ -1,51 +1,46 @@
import { SettingsUserStore } from 'Stores/User/Settings';
import { createCKEditor } from 'External/CKEditor.js';
export const
WYSIWYGS = ko.observableArray();
class HtmlEditor {
editor;
blurTimer = 0;
WYSIWYGS.push({
name: 'Squire',
construct: (owner, container, onReady) => onReady(new SquireUI(container))
});
__resizable = false;
__inited = false;
rl.registerWYSIWYG = (name, construct) => WYSIWYGS.push({name, construct});
onBlur = null;
onReady = null;
onModeChange = null;
element;
resize;
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;
constructor(element, onBlur = null, onReady = null, onModeChange = null) {
this.onBlur = onBlur;
this.onReady = onReady;
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));
}
this.element = element;
this.resize = (() => {
try {
this.editor && this.__resizable && this.editor.resize(element.clientWidth, element.clientHeight);
} catch (e) {} // eslint-disable-line no-empty
}).throttle(100);
this.init();
}
blurTrigger() {
if (this.onBlur) {
clearTimeout(this.blurTimer);
this.blurTimer = setTimeout(() => this.onBlur?.(), 200);
this.blurTimer = setTimeout(() => this.onBlur && this.onBlur(), 200);
}
}
@ -53,23 +48,16 @@ export class HtmlEditor {
* @returns {boolean}
*/
isHtml() {
return this.editor ? !this.isPlain() : false;
}
/**
* @returns {boolean}
*/
isPlain() {
return this.editor ? 'plain' === this.editor.mode : false;
return this.editor ? 'plain' !== this.editor.mode : false;
}
/**
* @returns {void}
*/
clearCachedSignature() {
this.onReady(() => this.editor.execCommand('insertSignature', {
this.editor && this.editor.execCommand('insertSignature', {
clearCache: true
}));
});
}
/**
@ -79,83 +67,158 @@ export class HtmlEditor {
* @returns {void}
*/
setSignature(signature, html, insertBefore = false) {
this.onReady(() => this.editor.execCommand('insertSignature', {
this.editor && this.editor.execCommand('insertSignature', {
isHtml: html,
insertBefore: insertBefore,
signature: signature
}));
});
}
/**
* @param {boolean=} wrapIsHtml = false
* @returns {string}
*/
getData() {
getData(wrapIsHtml = false) {
let result = '';
if (this.editor) {
try {
if (this.isPlain()) {
result = this.editor.getPlainData();
if ('plain' === this.editor.mode && this.editor.plugins.plain && this.editor.__plain) {
result = this.editor.__plain.getRawData();
} else {
result = this.editor.getData();
result = wrapIsHtml
? '<div data-html-editor-font-wrapper="true" style="font-family: arial, sans-serif; font-size: 13px;">' +
this.editor.getData() +
'</div>'
: this.editor.getData();
}
} catch (e) {} // eslint-disable-line no-empty
}
return result;
}
/**
* @param {boolean=} wrapIsHtml = false
* @returns {string}
*/
getDataWithHtmlMark() {
return (this.isHtml() ? ':HTML:' : '') + this.getData();
getDataWithHtmlMark(wrapIsHtml = false) {
return (this.isHtml() ? ':HTML:' : '') + this.getData(wrapIsHtml);
}
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();
modeToggle(plain) {
if (this.editor) {
try {
editor.setMode(mode);
if (this.isPlain()) {
editor.setPlainData(data);
} else {
editor.setData(data);
if (plain) {
if ('plain' === this.editor.mode) {
this.editor.setMode('wysiwyg');
}
} else if ('wysiwyg' === this.editor.mode) {
this.editor.setMode('plain');
}
} catch (e) { console.error(e); }
});
} catch (e) {} // eslint-disable-line no-empty
}
}
setHtml(html) {
this.setData('wysiwyg', html/*.replace(/<p[^>]*><\/p>/gi, '')*/);
setHtmlOrPlain(text, focus) {
if (':HTML:' === text.substr(0, 6)) {
this.setHtml(text.substr(6), focus);
} else {
this.setPlain(text, focus);
}
}
setPlain(txt) {
this.setData('plain', txt);
setHtml(html, focus) {
if (this.editor && this.__inited) {
this.clearCachedSignature();
this.modeToggle(true);
html = html.replace(/<p[^>]*><\/p>/gi, '');
try {
this.editor.setData(html);
} catch (e) {} // eslint-disable-line no-empty
focus && this.focus();
}
}
setPlain(plain, focus) {
if (this.editor && this.__inited) {
this.clearCachedSignature();
this.modeToggle(false);
if ('plain' === this.editor.mode && this.editor.plugins.plain && this.editor.__plain) {
this.editor.__plain.setRawData(plain);
} else {
try {
this.editor.setData(plain);
} catch (e) {} // eslint-disable-line no-empty
}
focus && this.focus();
}
}
init() {
if (this.element && !this.editor) {
const onReady = () => {
if (this.editor.removeMenuItem) {
this.editor.removeMenuItem('cut');
this.editor.removeMenuItem('copy');
this.editor.removeMenuItem('paste');
}
this.__resizable = true;
this.__inited = true;
this.resize();
this.onReady && this.onReady();
};
if (window.CKEDITOR) {
this.editor = createCKEditor(this.element);
this.editor.on('instanceReady', onReady);
} else {
this.editor = new SquireUI(this.element, this.editor);
setTimeout(onReady,1);
}
if (this.editor) {
this.editor.on('blur', () => this.blurTrigger());
this.editor.on('focus', () => this.blurTimer && clearTimeout(this.blurTimer));
this.editor.on('mode', () => {
this.blurTrigger();
this.onModeChange && this.onModeChange('plain' !== this.editor.mode);
});
}
}
}
focus() {
this.onReady(() => this.editor.focus());
try {
this.editor && this.editor.focus();
} catch (e) {} // eslint-disable-line no-empty
}
hasFocus() {
try {
return this.editor && !!this.editor.focusManager.hasFocus;
} catch (e) {
return false;
}
}
blur() {
this.onReady(() => this.editor.blur());
try {
this.editor && this.editor.focusManager.blur(true);
} catch (e) {} // eslint-disable-line no-empty
}
clear() {
this.onReady(() => this.isPlain() ? this.setPlain('') : this.setHtml(''));
clear(focus) {
this.setHtml('', focus);
}
}
export { HtmlEditor, HtmlEditor as default };

View file

@ -1,143 +1,372 @@
import { pInt } from 'Common/Utils';
import { doc, Settings, SettingsAdmin } from 'Common/Globals';
import { pString, pInt } from 'Common/Utils';
const
BASE = doc.location.pathname.replace(/\/+$/,'') + '/',
Settings = rl.settings,
ROOT = './',
HASH_PREFIX = '#/',
adminPath = () => rl.adminArea() && !SettingsAdmin('host'),
prefix = () => BASE + '?' + (adminPath() ? SettingsAdmin('path') : '');
export const
SERVER_PREFIX = './?',
SUB_QUERY_PREFIX = '&q[]=',
VERSION = Settings.app('version'),
WEB_PREFIX = Settings.app('webPath') || '',
VERSION_PREFIX = Settings.app('webVersionPath') || 'rainloop/v/' + VERSION + '/',
STATIC_PREFIX = VERSION_PREFIX + 'static/',
ADMIN_HOST_USE = !!Settings.app('adminHostUse'),
ADMIN_PATH = Settings.app('adminPath') || 'admin',
/**
* @param {string=} startupUrl
* @returns {string}
*/
root = () => HASH_PREFIX,
getHash = () => Settings.get('AuthAccountHash') || '0';
/**
* @returns {string}
*/
logoutLink = () => adminPath() ? prefix() : BASE,
/**
* @returns {string}
*/
export function subQueryPrefix() {
return SUB_QUERY_PREFIX;
}
/**
* @param {string} type
* @param {string} hash
* @param {string=} customSpecSuffix
* @returns {string}
*/
serverRequestRaw = (type, hash) =>
BASE + '?/Raw/' + SUB_QUERY_PREFIX + '/'
+ '0/' // Settings.get('accountHash') ?
+ (type
? type + '/' + (hash ? SUB_QUERY_PREFIX + '/' + hash : '')
: ''),
/**
* @param {string=} startupUrl
* @returns {string}
*/
export function root(startupUrl = '') {
return HASH_PREFIX + pString(startupUrl);
}
/**
* @param {string} download
* @returns {string}
*/
attachmentDownload = (download) =>
serverRequestRaw('Download', download),
/**
* @returns {string}
*/
export function rootAdmin() {
return ADMIN_HOST_USE ? ROOT : SERVER_PREFIX + ADMIN_PATH;
}
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')),
/**
* @returns {string}
*/
export function rootUser() {
return ROOT;
}
/**
* @param {string} type
* @returns {string}
*/
serverRequest = type => prefix() + '/' + type + '/' + SUB_QUERY_PREFIX + '/0/',
/**
* @param {string} type
* @param {string} download
* @param {string=} customSpecSuffix
* @returns {string}
*/
export function attachmentRaw(type, download, customSpecSuffix) {
customSpecSuffix = undefined === customSpecSuffix ? getHash() : customSpecSuffix;
return (
SERVER_PREFIX +
'/Raw/' +
SUB_QUERY_PREFIX +
'/' +
customSpecSuffix +
'/' +
type +
'/' +
SUB_QUERY_PREFIX +
'/' +
download
);
}
// Is '?/Css/0/Admin' needed?
cssLink = theme => BASE + '?/Css/0/User/-/' + encodeURI(theme) + '/-/' + Date.now() + '/Hash/-/Json/',
/**
* @param {string} download
* @param {string=} customSpecSuffix
* @returns {string}
*/
export function attachmentDownload(download, customSpecSuffix) {
return attachmentRaw('Download', download, customSpecSuffix);
}
/**
* @param {string} lang
* @param {boolean} isAdmin
* @returns {string}
*/
langLink = (lang, isAdmin) =>
BASE + '?/Lang/0/' + (isAdmin ? 'Admin' : 'App')
+ '/' + encodeURI(lang)
+ '/' + Settings.app('version') + '/',
/**
* @param {string} download
* @param {string=} customSpecSuffix
* @returns {string}
*/
export function attachmentPreview(download, customSpecSuffix) {
return attachmentRaw('View', download, customSpecSuffix);
}
/**
* @param {string} path
* @returns {string}
*/
staticLink = path => Settings.app('webVersionPath') + 'static/' + path,
/**
* @param {string} download
* @param {string=} customSpecSuffix
* @returns {string}
*/
export function attachmentThumbnailPreview(download, customSpecSuffix) {
return attachmentRaw('ViewThumbnail', download, customSpecSuffix);
}
/**
* @param {string} theme
* @returns {string}
*/
themePreviewLink = theme => {
if (theme.endsWith('@nextcloud')) {
theme = theme.slice(0, theme.length - 10).trim();
return parent.OC.webroot + '/themes/' + encodeURI(theme) + '/snappymail/preview.png';
}
let path = 'webVersionPath';
if (theme.endsWith('@custom')) {
theme = theme.slice(0, theme.length - 7).trim();
path = 'webPath';
}
return Settings.app(path) + 'themes/' + encodeURI(theme) + '/images/preview.png';
},
/**
* @param {string} download
* @param {string=} customSpecSuffix
* @returns {string}
*/
export function attachmentPreviewAsPlain(download, customSpecSuffix) {
return attachmentRaw('ViewAsPlain', download, customSpecSuffix);
}
/**
* @param {string} inboxFolderName = 'INBOX'
* @returns {string}
*/
mailbox = (inboxFolderName = 'INBOX') => HASH_PREFIX + 'mailbox/' + inboxFolderName,
/**
* @param {string} download
* @param {string=} customSpecSuffix
* @returns {string}
*/
export function attachmentFramed(download, customSpecSuffix) {
return attachmentRaw('FramedView', download, customSpecSuffix);
}
/**
* @param {string=} screenName = ''
* @returns {string}
*/
settings = (screenName = '') => HASH_PREFIX + 'settings' + (screenName ? '/' + screenName : ''),
/**
* @param {string} type
* @returns {string}
*/
export function serverRequest(type) {
return SERVER_PREFIX + '/' + type + '/' + SUB_QUERY_PREFIX + '/' + getHash() + '/';
}
/**
* @param {string=} screenName
* @returns {string}
*/
admin = screenName => HASH_PREFIX + (
'AdminDomains' == screenName ? 'domains'
: 'AdminSecurity' == screenName ? 'security'
: ''
),
/**
* @returns {string}
*/
export function upload() {
return serverRequest('Upload');
}
/**
* @param {string} folder
* @param {number=} page = 1
* @param {string=} search = ''
* @param {number=} threadUid = 0
* @returns {string}
*/
mailBox = (folder, page, search, threadUid, messageUid) => {
let result = [HASH_PREFIX + 'mailbox'];
/**
* @returns {string}
*/
export function uploadContacts() {
return serverRequest('UploadContacts');
}
if (folder) {
result.push(folder + (threadUid ? '~' + threadUid : ''));
}
/**
* @returns {string}
*/
export function uploadBackground() {
return serverRequest('UploadBackground');
}
if (messageUid) {
result.push('m' + messageUid);
} else {
page = pInt(page, 1);
if (1 < page) {
result.push('p' + page);
}
search && result.push(encodeURI(search));
}
/**
* @returns {string}
*/
export function append() {
return serverRequest('Append');
}
return result.join('/');
},
/**
* @param {string} email
* @returns {string}
*/
export function change(email) {
return serverRequest('Change') + encodeURIComponent(email) + '/';
}
mailBoxMessage = (folder, messageUid) => mailBox(folder, 1, '', 0, pInt(messageUid));
/**
* @param {string} requestHash
* @returns {string}
*/
export function messageViewLink(requestHash) {
return (
SERVER_PREFIX +
'/Raw/' +
SUB_QUERY_PREFIX +
'/' +
getHash() +
'/ViewAsPlain/' +
SUB_QUERY_PREFIX +
'/' +
requestHash
);
}
/**
* @param {string} requestHash
* @returns {string}
*/
export function messageDownloadLink(requestHash) {
return (
SERVER_PREFIX + '/Raw/' + SUB_QUERY_PREFIX + '/' + getHash() + '/Download/' + SUB_QUERY_PREFIX + '/' + requestHash
);
}
/**
* @param {string} email
* @returns {string}
*/
export function avatarLink(email) {
return SERVER_PREFIX + '/Raw/0/Avatar/' + encodeURIComponent(email) + '/';
}
/**
* @param {string} hash
* @returns {string}
*/
export function publicLink(hash) {
return SERVER_PREFIX + '/Raw/0/Public/' + hash + '/';
}
/**
* @param {string} hash
* @returns {string}
*/
export function userBackground(hash) {
return (
SERVER_PREFIX + '/Raw/' + SUB_QUERY_PREFIX + '/' + getHash() + '/UserBackground/' + SUB_QUERY_PREFIX + '/' + hash
);
}
/**
* @param {string} lang
* @param {boolean} isAdmin
* @returns {string}
*/
export function langLink(lang, isAdmin) {
return SERVER_PREFIX + '/Lang/0/' + (isAdmin ? 'Admin' : 'App') + '/' + encodeURI(lang) + '/' + VERSION + '/';
}
/**
* @returns {string}
*/
export function exportContactsVcf() {
return SERVER_PREFIX + '/Raw/' + SUB_QUERY_PREFIX + '/' + getHash() + '/ContactsVcf/';
}
/**
* @returns {string}
*/
export function exportContactsCsv() {
return SERVER_PREFIX + '/Raw/' + SUB_QUERY_PREFIX + '/' + getHash() + '/ContactsCsv/';
}
/**
* @param {string} path
* @returns {string}
*/
export function staticPrefix(path) {
return STATIC_PREFIX + path;
}
/**
* @returns {string}
*/
export function emptyContactPic() {
return staticPrefix('css/images/empty-contact.png');
}
/**
* @param {string} fileName
* @returns {string}
*/
export function sound(fileName) {
return staticPrefix('sounds/' + fileName);
}
/**
* @returns {string}
*/
export function notificationMailIcon() {
return staticPrefix('css/images/icon-message-notification.png');
}
/**
* @returns {string}
*/
export function openPgpJs() {
return staticPrefix('js/min/openpgp.min.js');
}
/**
* @returns {string}
*/
export function openPgpWorkerJs() {
return staticPrefix('js/min/openpgp.worker.min.js');
}
/**
* @returns {string}
*/
export function openPgpWorkerPath() {
return staticPrefix('js/min/');
}
/**
* @param {string} theme
* @returns {string}
*/
export function themePreviewLink(theme) {
let prefix = VERSION_PREFIX;
if ('@custom' === theme.substr(-7)) {
theme = theme.substr(0, theme.length - 7).trim();
prefix = WEB_PREFIX;
}
return prefix + 'themes/' + encodeURI(theme) + '/images/preview.png';
}
/**
* @param {string} inboxFolderName = 'INBOX'
* @returns {string}
*/
export function inbox(inboxFolderName = 'INBOX') {
return HASH_PREFIX + 'mailbox/' + inboxFolderName;
}
/**
* @param {string=} screenName = ''
* @returns {string}
*/
export function settings(screenName = '') {
return HASH_PREFIX + 'settings' + (screenName ? '/' + screenName : '');
}
/**
* @returns {string}
*/
export function about() {
return HASH_PREFIX + 'about';
}
/**
* @param {string} screenName
* @returns {string}
*/
export function admin(screenName) {
let result = HASH_PREFIX;
switch (screenName) {
case 'AdminDomains':
result += 'domains';
break;
case 'AdminSecurity':
result += 'security';
break;
// no default
}
return result;
}
/**
* @param {string} folder
* @param {number=} page = 1
* @param {string=} search = ''
* @param {string=} threadUid = ''
* @returns {string}
*/
export function mailBox(folder, page = 1, search = '', threadUid = '') {
page = pInt(page, 1);
search = pString(search);
let result = HASH_PREFIX + 'mailbox/';
if (folder) {
const resultThreadUid = pInt(threadUid);
result += encodeURI(folder) + (0 < resultThreadUid ? '~' + resultThreadUid : '');
}
if (1 < page) {
result = result.replace(/[/]+$/, '');
result += '/p' + page;
}
if (search) {
result = result.replace(/[/]+$/, '');
result += '/' + encodeURI(search);
}
return result;
}

165
dev/Common/Mime.js Normal file
View file

@ -0,0 +1,165 @@
/* eslint key-spacing: 0 */
/* eslint quote-props: 0 */
const Mime = {
'eml': 'message/rfc822',
'mime': 'message/rfc822',
'txt': 'text/plain',
'text': 'text/plain',
'def': 'text/plain',
'list': 'text/plain',
'in': 'text/plain',
'ini': 'text/plain',
'log': 'text/plain',
'sql': 'text/plain',
'cfg': 'text/plain',
'conf': 'text/plain',
'asc': 'text/plain',
'rtx': 'text/richtext',
'vcard': 'text/vcard',
'vcf': 'text/vcard',
'htm': 'text/html',
'html': 'text/html',
'csv': 'text/csv',
'ics': 'text/calendar',
'ifb': 'text/calendar',
'xml': 'text/xml',
'json': 'application/json',
'swf': 'application/x-shockwave-flash',
'hlp': 'application/winhlp',
'wgt': 'application/widget',
'chm': 'application/vnd.ms-htmlhelp',
'p10': 'application/pkcs10',
'p7c': 'application/pkcs7-mime',
'p7m': 'application/pkcs7-mime',
'p7s': 'application/pkcs7-signature',
'torrent': 'application/x-bittorrent',
// scripts
'js': 'application/javascript',
'pl': 'text/perl',
'css': 'text/css',
'asp': 'text/asp',
'php': 'application/x-httpd-php',
'php3': 'application/x-httpd-php',
'php4': 'application/x-httpd-php',
'php5': 'application/x-httpd-php',
'phtml': 'application/x-httpd-php',
// images
'png': 'image/png',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'jpe': 'image/jpeg',
'jfif': 'image/jpeg',
'gif': 'image/gif',
'bmp': 'image/bmp',
'cgm': 'image/cgm',
'ief': 'image/ief',
'ico': 'image/x-icon',
'tif': 'image/tiff',
'tiff': 'image/tiff',
'svg': 'image/svg+xml',
'svgz': 'image/svg+xml',
'djv': 'image/vnd.djvu',
'djvu': 'image/vnd.djvu',
'webp': 'image/webp',
// archives
'zip': 'application/zip',
'7z': 'application/x-7z-compressed',
'rar': 'application/x-rar-compressed',
'exe': 'application/x-msdownload',
'dll': 'application/x-msdownload',
'scr': 'application/x-msdownload',
'com': 'application/x-msdownload',
'bat': 'application/x-msdownload',
'msi': 'application/x-msdownload',
'cab': 'application/vnd.ms-cab-compressed',
'gz': 'application/x-gzip',
'tgz': 'application/x-gzip',
'bz': 'application/x-bzip',
'bz2': 'application/x-bzip2',
'deb': 'application/x-debian-package',
// fonts
'psf': 'application/x-font-linux-psf',
'otf': 'application/x-font-otf',
'pcf': 'application/x-font-pcf',
'snf': 'application/x-font-snf',
'ttf': 'application/x-font-ttf',
'ttc': 'application/x-font-ttf',
// audio
'mp3': 'audio/mpeg',
'amr': 'audio/amr',
'aac': 'audio/x-aac',
'aif': 'audio/x-aiff',
'aifc': 'audio/x-aiff',
'aiff': 'audio/x-aiff',
'wav': 'audio/x-wav',
'wma': 'audio/x-ms-wma',
'wax': 'audio/x-ms-wax',
'midi': 'audio/midi',
'mp4a': 'audio/mp4',
'ogg': 'audio/ogg',
'weba': 'audio/webm',
'ra': 'audio/x-pn-realaudio',
'ram': 'audio/x-pn-realaudio',
'rmp': 'audio/x-pn-realaudio-plugin',
'm3u': 'audio/x-mpegurl',
// video
'flv': 'video/x-flv',
'qt': 'video/quicktime',
'mov': 'video/quicktime',
'wmv': 'video/windows-media',
'avi': 'video/x-msvideo',
'mpg': 'video/mpeg',
'mpeg': 'video/mpeg',
'mpe': 'video/mpeg',
'm1v': 'video/mpeg',
'm2v': 'video/mpeg',
'3gp': 'video/3gpp',
'3g2': 'video/3gpp2',
'h261': 'video/h261',
'h263': 'video/h263',
'h264': 'video/h264',
'jpgv': 'video/jpgv',
'mp4': 'video/mp4',
'mp4v': 'video/mp4',
'mpg4': 'video/mp4',
'ogv': 'video/ogg',
'webm': 'video/webm',
'm4v': 'video/x-m4v',
'asf': 'video/x-ms-asf',
'asx': 'video/x-ms-asf',
'wm': 'video/x-ms-wm',
'wmx': 'video/x-ms-wmx',
'wvx': 'video/x-ms-wvx',
'movie': 'video/x-sgi-movie',
// adobe
'pdf': 'application/pdf',
'psd': 'image/vnd.adobe.photoshop',
'ai': 'application/postscript',
'eps': 'application/postscript',
'ps': 'application/postscript',
// ms office
'doc': 'application/msword',
'dot': 'application/msword',
'rtf': 'application/rtf',
'xls': 'application/vnd.ms-excel',
'ppt': 'application/vnd.ms-powerpoint',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'dotx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
// open office
'odt': 'application/vnd.oasis.opendocument.text',
'ods': 'application/vnd.oasis.opendocument.spreadsheet'
};
export { Mime, Mime as default };

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

@ -0,0 +1,60 @@
import { i18n } from 'Common/Translator';
export function format(timeStampInUTC, formatStr) {
const now = Date.now(),
time = 0 < timeStampInUTC ? Math.min(now, timeStampInUTC * 1000) : (0 === timeStampInUTC ? now : 0);
if (31536000000 < time) {
const m = new Date(time);
switch (formatStr) {
case 'FROMNOW':
return m.fromNow();
case 'SHORT': {
if (4 >= (now - time) / 3600000)
return m.fromNow();
const ymd = m.format('Ymd'), date = new Date;
if (date.format('Ymd') === ymd)
return i18n('MESSAGE_LIST/TODAY_AT', {TIME: m.format('LT')});
if (new Date(now - 86400000).format('Ymd') === ymd)
return i18n('MESSAGE_LIST/YESTERDAY_AT', {TIME: m.format('LT')});
if (date.getFullYear() === m.getFullYear())
return m.format('d M.');
return m.format('LL');
}
case 'FULL':
return m.format('LLL');
default:
return m.format(formatStr);
}
}
return '';
}
export function timeToNode(element, time) {
try {
time = time || (Date.parse(element.dateTime) / 1000);
if (time) {
let key, m = new Date(time * 1000);
element.dateTime = m.format('Y-m-d\\TH:i:s');
key = element.dataset.momentFormat;
if (key) {
element.textContent = format(time, key);
}
key = element.dataset.momentFormatTitle;
if (key) {
element.title = format(time, key);
}
}
} catch (e) {
// prevent knockout crashes
console.error(e);
}
}
addEventListener('reload-time', () => setTimeout(() =>
document.querySelectorAll('[data-bind*="moment:"]').forEach(element => timeToNode(element))
, 1)
);

View file

@ -1,19 +1,25 @@
import { settingsAddViewModel } from 'Screen/AbstractSettings';
import { SettingsGet } from 'Common/Globals';
import { AbstractViewPopup } from 'Knoin/AbstractViews';
const USER_VIEW_MODELS_HOOKS = [],
ADMIN_VIEW_MODELS_HOOKS = [];
/**
* @param {string} name
* @returns {?}
*/
export function mainSettingsGet(name) {
return rl.settings.get(name);
}
/**
* @param {Function} callback
* @param {string} action
* @param {Object=} parameters
* @param {?number=} timeout
*/
rl.pluginRemoteRequest = (callback, action, parameters, timeout) => {
rl.app.Remote.request('Plugin' + action, callback, parameters, timeout);
};
export function remoteRequest(callback, action, parameters, timeout) {
if (rl.app) {
rl.app.remote().defaultRequest(callback, 'Plugin' + action, parameters, timeout);
}
}
/**
* @param {Function} SettingsViewModelClass
@ -21,9 +27,9 @@ rl.pluginRemoteRequest = (callback, action, parameters, timeout) => {
* @param {string} template
* @param {string} route
*/
rl.addSettingsViewModel = (SettingsViewModelClass, template, labelName, route) => {
export function addSettingsViewModel(SettingsViewModelClass, template, labelName, route) {
USER_VIEW_MODELS_HOOKS.push([SettingsViewModelClass, template, labelName, route]);
};
}
/**
* @param {Function} SettingsViewModelClass
@ -31,17 +37,18 @@ rl.addSettingsViewModel = (SettingsViewModelClass, template, labelName, route) =
* @param {string} template
* @param {string} route
*/
rl.addSettingsViewModelForAdmin = (SettingsViewModelClass, template, labelName, route) => {
export function addSettingsViewModelForAdmin(SettingsViewModelClass, template, labelName, route) {
ADMIN_VIEW_MODELS_HOOKS.push([SettingsViewModelClass, template, labelName, route]);
};
}
/**
* @param {boolean} admin
*/
export function runSettingsViewModelHooks(admin) {
(admin ? ADMIN_VIEW_MODELS_HOOKS : USER_VIEW_MODELS_HOOKS).forEach(view =>
settingsAddViewModel(...view)
);
const Knoin = require('Knoin/Knoin');
(admin ? ADMIN_VIEW_MODELS_HOOKS : USER_VIEW_MODELS_HOOKS).forEach(view => {
Knoin.addSettingsViewModel(view[0], view[1], view[2], view[3]);
});
}
/**
@ -49,7 +56,8 @@ export function runSettingsViewModelHooks(admin) {
* @param {string} name
* @returns {?}
*/
rl.pluginSettingsGet = (pluginSection, name) =>
SettingsGet('Plugins')?.[pluginSection]?.[name];
rl.pluginPopupView = AbstractViewPopup;
export function settingsGet(pluginSection, name) {
let plugins = rl.settings.get('Plugins');
plugins = plugins && null != plugins[pluginSection] ? plugins[pluginSection] : null;
return plugins ? (null == plugins[name] ? null : plugins[name]) : null;
}

View file

@ -1,106 +1,111 @@
import ko from 'ko';
import { addEventsListeners, addShortcut, registerShortcut } from 'Common/Globals';
import { isArray } from 'Common/Utils';
import { koComputable } from 'External/ko';
/*
oCallbacks:
ItemSelect
MiddleClick
canSelect
ItemGetUid
UpOrDown
*/
class Selector {
list;
listChecked;
isListChecked;
let shiftStart;
focusedItem;
selectedItem;
selectedItemUseCallback = true;
iSelectNextHelper = 0;
iFocusedNextHelper = 0;
oContentScrollable;
sItemSelector;
sItemSelectedSelector;
sItemCheckedSelector;
sItemFocusedSelector;
sLastUid = '';
oCallbacks = {};
export class Selector {
/**
* @param {koProperty} koList
* @param {koProperty} koSelectedItem
* @param {koProperty} koFocusedItem
* @param {string} sItemSelector
* @param {string} sItemSelectedSelector
* @param {string} sItemCheckedSelector
* @param {string} sItemFocusedSelector
*/
constructor(
koList,
koSelectedItem,
koFocusedItem,
sItemSelector,
sItemCheckedSelector
sItemSelectedSelector,
sItemCheckedSelector,
sItemFocusedSelector
) {
koFocusedItem = (koFocusedItem || ko.observable(null)).extend({ toggleSubscribeProperty: [this, 'focused'] });
koSelectedItem = (koSelectedItem || ko.observable(null)).extend({ toggleSubscribeProperty: [null, 'selected'] });
this.list = koList;
this.listChecked = koComputable(() => koList.filter(item => item.checked())).extend({ rateLimit: 0 });
this.focusedItem = koFocusedItem;
this.selectedItem = koSelectedItem;
this.listChecked = ko.computed(() => this.list().filter(item => item.checked())).extend({ rateLimit: 0 });
this.isListChecked = ko.computed(() => 0 < this.listChecked().length);
this.iSelectNextHelper = 0;
this.iFocusedNextHelper = 0;
// this.oContentScrollable = null;
this.focusedItem = koFocusedItem || ko.observable(null);
this.selectedItem = koSelectedItem || ko.observable(null);
const itemSelectedThrottle = (item => this.itemSelected(item)).debounce(300);
this.listChecked.subscribe((items) => {
if (items.length) {
if (null === this.selectedItem()) {
if (this.selectedItem.valueHasMutated) {
this.selectedItem.valueHasMutated();
}
} else {
this.selectedItem(null);
}
} else if (this.autoSelect() && this.focusedItem()) {
this.selectedItem(this.focusedItem());
}
}, this);
this.selectedItem.subscribe((item) => {
if (item) {
if (this.isListChecked()) {
this.listChecked().forEach(subItem => subItem.checked(false));
}
if (this.selectedItemUseCallback) {
itemSelectedThrottle(item);
}
} else if (this.selectedItemUseCallback) {
this.itemSelected(null);
}
}, this);
this.selectedItem = this.selectedItem.extend({ toggleSubscribeProperty: [this, 'selected'] });
this.focusedItem = this.focusedItem.extend({ toggleSubscribeProperty: [null, 'focused'] });
this.sItemSelector = sItemSelector;
this.sItemSelectedSelector = sItemSelectedSelector;
this.sItemCheckedSelector = sItemCheckedSelector;
this.sItemFocusedSelector = sItemSelector + '.focused';
this.sItemFocusedSelector = sItemFocusedSelector;
this.sLastUid = '';
this.oCallbacks = {};
this.focusedItem.subscribe(item => item && (this.sLastUid = this.getItemUid(item)), this);
const
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 => {
if (items.length) {
koSelectedItem() ? koSelectedItem(null) : koSelectedItem.valueHasMutated?.();
} else {
this.autoSelect();
}
});
let selectedItemUseCallback = true;
koSelectedItem.subscribe(item => {
if (item) {
// koList.forEach(subItem => subItem.checked(false));
selectedItemUseCallback && itemSelectedThrottle(item);
} else {
selectedItemUseCallback && itemSelected();
}
});
koFocusedItem.subscribe(item => item && (this.sLastUid = this.getItemUid(item)));
/**
* Below code is used to keep checked/focused/selected states when array is refreshed.
*/
let aCheckedCache = [],
let aCache = [],
aCheckedCache = [],
mFocused = null,
mSelected = null;
// Before removing old list
koList.subscribe(
items => {
if (isArray(items)) {
this.list.subscribe(
(items) => {
if (Array.isArray(items)) {
items.forEach(item => {
const uid = this.getItemUid(item);
if (uid) {
if (item) {
const uid = this.getItemUid(item);
aCache.push(uid);
item.checked() && aCheckedCache.push(uid);
if (!mFocused && item.focused()) {
if (null === mFocused && item.focused()) {
mFocused = uid;
}
if (!mSelected && item.selected()) {
if (null === mSelected && item.selected()) {
mSelected = uid;
}
}
@ -111,64 +116,136 @@ export class Selector {
'beforeChange'
);
koList.subscribe(aItems => {
selectedItemUseCallback = false;
this.list.subscribe((aItems) => {
let temp = null,
getNext = false,
isNextFocused = mFocused,
isChecked = false,
isSelected = false,
len = 0;
this.unselect();
const uids = [];
if (isArray(aItems)) {
let temp,
isChecked,
next = this.iFocusedNextHelper || this.iSelectNextHelper;
this.selectedItemUseCallback = false;
this.focusedItem(null);
this.selectedItem(null);
if (Array.isArray(aItems)) {
len = aCheckedCache.length;
aItems.forEach(item => {
const uid = this.getItemUid(item);
if (uid) {
uids.push(uid);
if (mFocused === uid) {
koFocusedItem(item);
mFocused = null;
}
if (null !== mFocused && mFocused === uid) {
this.focusedItem(item);
mFocused = null;
}
if (aCheckedCache.includes(uid)) {
item.checked(true);
isChecked = true;
}
if (0 < len && aCheckedCache.includes(uid)) {
isChecked = true;
item.checked(true);
--len;
}
if (!isChecked && mSelected === uid) {
koSelectedItem(item);
mSelected = null;
}
if (!isChecked && null !== mSelected && mSelected === uid) {
isSelected = true;
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) {
this.iSelectNextHelper && koSelectedItem(temp);
if (0 !== this.iSelectNextHelper) {
this.selectedItem(temp || null);
}
koFocusedItem(temp);
this.focusedItem(temp || null);
this.scrollToFocused();
setTimeout(this.scrollToFocused, 100);
}
this.iSelectNextHelper = 0;
this.iFocusedNextHelper = 0;
}
!isChecked && !koSelectedItem() && this.autoSelect();
}
aCache = [];
aCheckedCache = [];
mFocused = null;
mSelected = null;
selectedItemUseCallback = true;
});
}
itemSelected(item) {
if (this.isListChecked()) {
if (!item) {
(this.oCallbacks.onItemSelect || (()=>{}))(item || null);
}
} else if (item) {
(this.oCallbacks.onItemSelect || (()=>{}))(item);
}
}
/**
* @param {boolean} forceSelect
*/
goDown(forceSelect) {
this.newSelectPosition('ArrowDown', false, forceSelect);
}
/**
* @param {boolean} forceSelect
*/
goUp(forceSelect) {
this.newSelectPosition('ArrowUp', false, forceSelect);
}
unselect() {
this.selectedItem(null);
this.focusedItem(null);
@ -178,18 +255,13 @@ export class Selector {
this.oContentScrollable = contentScrollable;
if (contentScrollable) {
let getItem = selector => {
let el = event.target.closestWithin(selector, contentScrollable);
return el ? ko.dataFor(el) : null;
};
contentScrollable.addEventListener('click', event => {
let el = event.target.closestWithin(this.sItemSelector, contentScrollable);
el && this.actionClick(ko.dataFor(el), event);
addEventsListeners(contentScrollable, {
click: event => {
const el = event.target.closestWithin(this.sItemSelector, contentScrollable);
let item = el && ko.dataFor(el);
el && (this.oCallbacks.click || (()=>1))(event, item) && this.actionClick(item, event);
item = getItem(this.sItemCheckedSelector);
el = event.target.closestWithin(this.sItemCheckedSelector, contentScrollable);
if (el) {
const item = ko.dataFor(el);
if (item) {
if (event.shiftKey) {
this.actionClick(item, event);
@ -198,33 +270,27 @@ export class Selector {
item.checked(!item.checked());
}
}
},
auxclick: event => {
if (1 == event.button) {
const item = getItem(this.sItemSelector);
if (item) {
this.focusedItem(item);
this.oCallbacks.MiddleClick?.(item);
}
}
}
});
registerShortcut('enter,open', '', keyScope, () => {
shortcuts.add('enter', '', keyScope, () => {
const focused = this.focusedItem();
if (focused && !focused.selected()) {
this.actionClick(focused);
return false;
}
return true;
});
addShortcut('arrowup,arrowdown', 'meta', keyScope, () => false);
shortcuts.add('arrowup', 'meta', keyScope, () => false);
shortcuts.add('arrowdown', 'meta', keyScope, () => false);
addShortcut('arrowup,arrowdown', 'shift', keyScope, event => {
shortcuts.add('arrowup,arrowdown', 'shift', keyScope, event => {
this.newSelectPosition(event.key, true);
return false;
});
registerShortcut('arrowup,arrowdown,home,end,pageup,pagedown,space', '', keyScope, event => {
shortcuts.add('arrowup,arrowdown,home,end,pageup,pagedown,insert,space', '', keyScope, event => {
this.newSelectPosition(event.key, false);
return false;
});
@ -234,10 +300,8 @@ export class Selector {
/**
* @returns {boolean}
*/
autoSelect(bForce) {
(bForce || (this.oCallbacks.canSelect || (()=>1))())
&& this.focusedItem()
&& this.selectedItem(this.focusedItem());
autoSelect() {
return !!(this.oCallbacks.onAutoSelect || (()=>true))();
}
/**
@ -245,7 +309,14 @@ export class Selector {
* @returns {string}
*/
getItemUid(item) {
return (item && this.oCallbacks.ItemGetUid?.(item)?.toString()) || '';
let uid = '';
const getItemUidCallback = this.oCallbacks.onItemGetUid || null;
if (getItemUidCallback && item) {
uid = getItemUidCallback(item);
}
return uid.toString();
}
/**
@ -254,68 +325,121 @@ export class Selector {
* @param {boolean=} bForceSelect = false
*/
newSelectPosition(sEventKey, bShiftKey, bForceSelect) {
let result;
let index = 0,
isNext = false,
isStop = false,
result = null;
const up = 'ArrowUp' === sEventKey,
isArrow = up || 'ArrowDown' === sEventKey,
pageStep = 10,
const pageStep = 10,
list = this.list(),
listLen = list.length,
listLen = list ? list.length : 0,
focused = this.focusedItem();
bShiftKey || (shiftStart = -1);
if (' ' === sEventKey) {
focused?.checked(!focused.checked());
} else if (listLen) {
if (focused) {
if (isArrow) {
let i = list.indexOf(focused);
if (bShiftKey) {
shiftStart = -1 < shiftStart ? shiftStart : i;
shiftStart == i
? focused.checked(true)
: ((up ? shiftStart < i : shiftStart > i) && focused.checked(false));
}
if (up) {
i > 0 && (result = list[--i]);
} else if (++i < listLen) {
result = list[i];
}
bShiftKey && result?.checked(true);
result || this.oCallbacks.UpOrDown?.(up);
} else if ('Home' === sEventKey) {
if (0 < listLen) {
if (!focused) {
if (
'ArrowDown' == sEventKey ||
'Insert' == sEventKey ||
' ' == sEventKey ||
'Home' == sEventKey ||
'PageUp' == sEventKey
) {
result = list[0];
} else if ('End' === sEventKey) {
} else if (
'ArrowUp' === sEventKey ||
'End' === sEventKey ||
'PageDown' === sEventKey
) {
result = list[list.length - 1];
}
} else if (focused) {
if (
'ArrowDown' === sEventKey ||
'ArrowUp' === sEventKey ||
'Insert' === sEventKey ||
' ' === sEventKey
) {
list.forEach(item => {
if (!isStop) {
switch (sEventKey) {
case 'ArrowUp':
if (focused === item) {
isStop = true;
} else {
result = item;
}
break;
case 'ArrowDown':
case 'Insert':
if (isNext) {
result = item;
isStop = true;
} else if (focused === item) {
isNext = true;
}
break;
// no default
}
}
});
if (!result && ('ArrowDown' === sEventKey || 'ArrowUp' === sEventKey)) {
(this.oCallbacks.onUpUpOrDownDown || (()=>true))('ArrowUp' === sEventKey);
}
} else if ('Home' === sEventKey || 'End' === sEventKey) {
if ('Home' === sEventKey) {
result = list[0];
} else if ('End' === sEventKey) {
result = list[list.length - 1];
}
} else if ('PageDown' === sEventKey) {
let i = list.indexOf(focused);
if (i < listLen - 1) {
result = list[Math.min(i + pageStep, listLen - 1)];
for (; index < listLen; index++) {
if (focused === list[index]) {
index += pageStep;
index = listLen - 1 < index ? listLen - 1 : index;
result = list[index];
break;
}
}
} else if ('PageUp' === sEventKey) {
let i = list.indexOf(focused);
if (i > 0) {
result = list[Math.max(0, i - pageStep)];
for (index = listLen; 0 <= index; index--) {
if (focused === list[index]) {
index -= pageStep;
index = 0 > index ? 0 : index;
result = list[index];
break;
}
}
}
} else if (
'Home' == sEventKey ||
'PageUp' == sEventKey
) {
result = list[0];
} else if (
'End' === sEventKey ||
'PageDown' === sEventKey
) {
result = list[list.length - 1];
}
}
if (result) {
this.focusedItem(result);
if (focused) {
if (bShiftKey) {
if ('ArrowUp' === sEventKey || 'ArrowDown' === sEventKey) {
focused.checked(!focused.checked());
}
} else if ('Insert' === sEventKey || ' ' === sEventKey) {
focused.checked(!focused.checked());
}
}
if (result) {
this.focusedItem(result);
!this.list.hasChecked() && this.autoSelect(bForceSelect);
this.scrollToFocused();
if ((this.autoSelect() || !!bForceSelect) && !this.isListChecked() && ' ' !== sEventKey) {
this.selectedItem(result);
}
this.scrollToFocused();
} else if (focused) {
if (bShiftKey && ('ArrowUp' === sEventKey || 'ArrowDown' === sEventKey)) {
focused.checked(!focused.checked());
} else if ('Insert' === sEventKey || ' ' === sEventKey) {
focused.checked(!focused.checked());
}
this.focusedItem(focused);
}
}
@ -325,15 +449,16 @@ export class Selector {
scrollToFocused() {
const scrollable = this.oContentScrollable;
if (scrollable) {
let focused = scrollable.querySelector(this.sItemFocusedSelector);
let block, focused = scrollable.querySelector(this.sItemFocusedSelector);
if (focused) {
const fRect = focused.getBoundingClientRect(),
sRect = scrollable.getBoundingClientRect();
if (fRect.top < sRect.top) {
focused.scrollIntoView(true);
block = 'start';
} else if (fRect.bottom > sRect.bottom) {
focused.scrollIntoView(false);
block = 'end';
}
block && focused.scrollIntoView(block === 'start');
} else {
scrollable.scrollTop = 0;
}
@ -347,47 +472,78 @@ export class Selector {
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=} event
*/
actionClick(item, event) {
actionClick(item, event = null) {
if (item) {
let select = true;
if (event && !event.altKey) {
if (event.shiftKey && !event.ctrlKey && !event.metaKey) {
const uid = this.getItemUid(item);
if (uid && this.sLastUid && uid !== this.sLastUid) {
let changeRange = false,
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);
}
});
let click = true;
if (event) {
if (event.shiftKey && !(event.ctrlKey || event.metaKey) && !event.altKey) {
click = false;
if (!this.sLastUid) {
this.sLastUid = this.getItemUid(item);
}
this.sLastUid = uid;
item.checked(!item.checked());
this.eventClickFunction(item, event);
this.focusedItem(item);
return;
}
if (!event.shiftKey && (event.ctrlKey || event.metaKey)) {
select = false;
} else if ((event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey) {
click = false;
this.focusedItem(item);
const selected = this.selectedItem();
if (selected && item !== selected) {
selected.checked(true);
if (this.selectedItem() && item !== this.selectedItem()) {
this.selectedItem().checked(true);
}
item.checked(!item.checked());
}
}
select ? this.selectMessageItem(item) : item.checked(!item.checked());
if (click) {
this.selectMessageItem(item);
}
}
}
@ -401,3 +557,5 @@ export class Selector {
this.scrollToFocused();
}
}
export { Selector, Selector as default };

View file

@ -1,236 +1,285 @@
import ko from 'ko';
import { Notifications, UploadErrorCode } from 'Common/Enums';
import { Notification, UploadErrorCode } from 'Common/Enums';
import { langLink } from 'Common/Links';
import { doc, createElement } from 'Common/Globals';
import { getKeyByValue, forEachObjectEntry } from 'Common/Utils';
import { pInt } from 'Common/Utils';
import { LanguageStore } from 'Stores/Language';
let I18N_DATA = {};
let I18N_DATA = window.rainloopI18N || {};
const
init = () => {
if (rl.I18N) {
I18N_DATA = rl.I18N;
rl.I18N = null;
doc.documentElement.dir = I18N_DATA.LANG_DIR;
return 1;
}
},
const doc = document,
I18N_NOTIFICATION_DATA = {},
I18N_NOTIFICATION_MAP = [
[Notification.InvalidToken, 'NOTIFICATIONS/INVALID_TOKEN'],
[Notification.InvalidToken, 'NOTIFICATIONS/INVALID_TOKEN'],
[Notification.AuthError, 'NOTIFICATIONS/AUTH_ERROR'],
[Notification.AccessError, 'NOTIFICATIONS/ACCESS_ERROR'],
[Notification.ConnectionError, 'NOTIFICATIONS/CONNECTION_ERROR'],
[Notification.CaptchaError, 'NOTIFICATIONS/CAPTCHA_ERROR'],
[Notification.SocialFacebookLoginAccessDisable, 'NOTIFICATIONS/SOCIAL_FACEBOOK_LOGIN_ACCESS_DISABLE'],
[Notification.SocialTwitterLoginAccessDisable, 'NOTIFICATIONS/SOCIAL_TWITTER_LOGIN_ACCESS_DISABLE'],
[Notification.SocialGoogleLoginAccessDisable, 'NOTIFICATIONS/SOCIAL_GOOGLE_LOGIN_ACCESS_DISABLE'],
[Notification.DomainNotAllowed, 'NOTIFICATIONS/DOMAIN_NOT_ALLOWED'],
[Notification.AccountNotAllowed, 'NOTIFICATIONS/ACCOUNT_NOT_ALLOWED'],
i18nKey = key => key.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase(),
[Notification.AccountTwoFactorAuthRequired, 'NOTIFICATIONS/ACCOUNT_TWO_FACTOR_AUTH_REQUIRED'],
[Notification.AccountTwoFactorAuthError, 'NOTIFICATIONS/ACCOUNT_TWO_FACTOR_AUTH_ERROR'],
getNotificationMessage = code => {
let key = getKeyByValue(Notifications, code);
return key ? I18N_DATA.NOTIFICATIONS[key] : '';
},
[Notification.CouldNotSaveNewPassword, 'NOTIFICATIONS/COULD_NOT_SAVE_NEW_PASSWORD'],
[Notification.CurrentPasswordIncorrect, 'NOTIFICATIONS/CURRENT_PASSWORD_INCORRECT'],
[Notification.NewPasswordShort, 'NOTIFICATIONS/NEW_PASSWORD_SHORT'],
[Notification.NewPasswordWeak, 'NOTIFICATIONS/NEW_PASSWORD_WEAK'],
[Notification.NewPasswordForbidden, 'NOTIFICATIONS/NEW_PASSWORD_FORBIDDENT'],
fromNow = date => relativeTime(Math.round((date.getTime() - Date.now()) / 1000));
[Notification.ContactsSyncError, 'NOTIFICATIONS/CONTACTS_SYNC_ERROR'],
export const
translateTrigger = ko.observable(false),
[Notification.CantGetMessageList, 'NOTIFICATIONS/CANT_GET_MESSAGE_LIST'],
[Notification.CantGetMessage, 'NOTIFICATIONS/CANT_GET_MESSAGE'],
[Notification.CantDeleteMessage, 'NOTIFICATIONS/CANT_DELETE_MESSAGE'],
[Notification.CantMoveMessage, 'NOTIFICATIONS/CANT_MOVE_MESSAGE'],
[Notification.CantCopyMessage, 'NOTIFICATIONS/CANT_MOVE_MESSAGE'],
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat
// 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;
[Notification.CantSaveMessage, 'NOTIFICATIONS/CANT_SAVE_MESSAGE'],
[Notification.CantSendMessage, 'NOTIFICATIONS/CANT_SEND_MESSAGE'],
[Notification.InvalidRecipients, 'NOTIFICATIONS/INVALID_RECIPIENTS'],
[Notification.CantSaveFilters, 'NOTIFICATIONS/CANT_SAVE_FILTERS'],
[Notification.CantGetFilters, 'NOTIFICATIONS/CANT_GET_FILTERS'],
[Notification.FiltersAreNotCorrect, 'NOTIFICATIONS/FILTERS_ARE_NOT_CORRECT'],
[Notification.CantCreateFolder, 'NOTIFICATIONS/CANT_CREATE_FOLDER'],
[Notification.CantRenameFolder, 'NOTIFICATIONS/CANT_RENAME_FOLDER'],
[Notification.CantDeleteFolder, 'NOTIFICATIONS/CANT_DELETE_FOLDER'],
[Notification.CantDeleteNonEmptyFolder, 'NOTIFICATIONS/CANT_DELETE_NON_EMPTY_FOLDER'],
[Notification.CantSubscribeFolder, 'NOTIFICATIONS/CANT_SUBSCRIBE_FOLDER'],
[Notification.CantUnsubscribeFolder, 'NOTIFICATIONS/CANT_UNSUBSCRIBE_FOLDER'],
[Notification.CantSaveSettings, 'NOTIFICATIONS/CANT_SAVE_SETTINGS'],
[Notification.CantSavePluginSettings, 'NOTIFICATIONS/CANT_SAVE_PLUGIN_SETTINGS'],
[Notification.DomainAlreadyExists, 'NOTIFICATIONS/DOMAIN_ALREADY_EXISTS'],
[Notification.CantInstallPackage, 'NOTIFICATIONS/CANT_INSTALL_PACKAGE'],
[Notification.CantDeletePackage, 'NOTIFICATIONS/CANT_DELETE_PACKAGE'],
[Notification.InvalidPluginPackage, 'NOTIFICATIONS/INVALID_PLUGIN_PACKAGE'],
[Notification.UnsupportedPluginPackage, 'NOTIFICATIONS/UNSUPPORTED_PLUGIN_PACKAGE'],
[Notification.DemoSendMessageError, 'NOTIFICATIONS/DEMO_SEND_MESSAGE_ERROR'],
[Notification.DemoAccountError, 'NOTIFICATIONS/DEMO_ACCOUNT_ERROR'],
[Notification.AccountAlreadyExists, 'NOTIFICATIONS/ACCOUNT_ALREADY_EXISTS'],
[Notification.AccountDoesNotExist, 'NOTIFICATIONS/ACCOUNT_DOES_NOT_EXIST'],
[Notification.MailServerError, 'NOTIFICATIONS/MAIL_SERVER_ERROR'],
[Notification.InvalidInputArgument, 'NOTIFICATIONS/INVALID_INPUT_ARGUMENT'],
[Notification.UnknownNotification, 'NOTIFICATIONS/UNKNOWN_ERROR'],
[Notification.UnknownError, 'NOTIFICATIONS/UNKNOWN_ERROR']
];
export const trigger = ko.observable(false);
/**
* @param {string} key
* @param {Object=} valueList
* @param {string=} defaulValue
* @returns {string}
*/
export function i18n(key, valueList, defaulValue) {
let valueName = '',
result = I18N_DATA[key];
if (undefined === result) {
result = undefined === defaulValue ? key : defaulValue;
}
if (null != valueList) {
for (valueName in valueList) {
if (Object.prototype.hasOwnProperty.call(valueList, valueName)) {
result = result.replace('%' + valueName + '%', valueList[valueName]);
}
}
if (Intl.RelativeTimeFormat) {
return (new Intl.RelativeTimeFormat(doc.documentElement.lang)).format(seconds, unit);
}
// Safari < 14
abs = Math.abs(seconds);
let rtf = rl.relativeTime.long[unit][0 > seconds ? 'past' : 'future'],
plural = rl.relativeTime.plural(abs);
return (rtf[plural] || rtf).replace('{0}', abs);
},
}
/**
* @param {string} key
* @param {Object=} valueList
* @param {string=} defaulValue
* @returns {string}
*/
i18n = (key, valueList, defaulValue) => {
let result = defaulValue ?? key;
let path = key.split('/');
if (I18N_DATA[path[0]] && path[1]) {
result = I18N_DATA[path[0]][path[1]] || result;
return result;
}
const i18nToNode = element => {
const key = element.dataset.i18n;
if (key) {
if ('[' === key.substr(0, 1)) {
switch (key.substr(0, 6)) {
case '[html]':
element.innerHTML = i18n(key.substr(6));
break;
case '[place':
element.placeholder = i18n(key.substr(13));
break;
case '[title':
element.title = i18n(key.substr(7));
break;
// no default
}
} else {
element.textContent = i18n(key);
}
valueList && forEachObjectEntry(valueList, (key, value) => {
result = result.replace('%' + key + '%', value);
}
};
/**
* @param {Object} elements
* @param {boolean=} animate = false
*/
export function i18nToNodes(element) {
setTimeout(() =>
element.querySelectorAll('[data-i18n]').forEach(item => i18nToNode(item))
, 1);
}
const reloadData = () => {
if (window.rainloopI18N) {
I18N_DATA = window.rainloopI18N || {};
i18nToNodes(doc);
dispatchEvent(new CustomEvent('reload-time'));
trigger(!trigger());
}
window.rainloopI18N = null;
};
/**
* @returns {void}
*/
export function initNotificationLanguage() {
I18N_NOTIFICATION_MAP.forEach((item) => {
I18N_NOTIFICATION_DATA[item[0]] = i18n(item[1]);
});
}
/**
* @param {Function} startCallback
* @param {Function=} langCallback = null
*/
export function initOnStartOrLangChange(startCallback, langCallback = null) {
if (startCallback) {
startCallback();
}
if (langCallback) {
trigger.subscribe(() => {
if (startCallback) {
startCallback();
}
if (langCallback) {
langCallback();
}
});
return result;
},
} else if (startCallback) {
trigger.subscribe(startCallback);
}
}
/**
* @param {Object} elements
* @param {boolean=} animate = false
*/
i18nToNodes = element =>
setTimeout(() =>
element.querySelectorAll('[data-i18n]').forEach(element => {
const key = element.dataset.i18n;
if ('[' === key[0]) {
switch (key.slice(1, 6)) {
case 'html]':
element.innerHTML = i18n(key.slice(6));
break;
case 'place':
element.placeholder = i18n(key.slice(13));
break;
case 'title':
element.title = i18n(key.slice(7));
break;
// no default
/**
* @param {number} code
* @param {*=} message = ''
* @param {*=} defCode = null
* @returns {string}
*/
export function getNotification(code, message = '', defCode = null) {
code = parseInt(code, 10) || 0;
if (Notification.ClientViewError === code && message) {
return message;
}
defCode = defCode ? parseInt(defCode, 10) || 0 : 0;
return undefined === I18N_NOTIFICATION_DATA[code]
? defCode && undefined === I18N_NOTIFICATION_DATA[defCode]
? I18N_NOTIFICATION_DATA[defCode]
: ''
: I18N_NOTIFICATION_DATA[code];
}
/**
* @param {object} response
* @param {number} defCode = Notification.UnknownNotification
* @returns {string}
*/
export function getNotificationFromResponse(response, defCode = Notification.UnknownNotification) {
return response && response.ErrorCode
? getNotification(parseInt(response.ErrorCode, 10) || defCode, response.ErrorMessage || '')
: getNotification(defCode);
}
/**
* @param {*} code
* @returns {string}
*/
export function getUploadErrorDescByCode(code) {
let result = '';
switch (parseInt(code, 10) || 0) {
case UploadErrorCode.FileIsTooBig:
result = i18n('UPLOAD/ERROR_FILE_IS_TOO_BIG');
break;
case UploadErrorCode.FilePartiallyUploaded:
result = i18n('UPLOAD/ERROR_FILE_PARTIALLY_UPLOADED');
break;
case UploadErrorCode.FileNoUploaded:
result = i18n('UPLOAD/ERROR_NO_FILE_UPLOADED');
break;
case UploadErrorCode.MissingTempFolder:
result = i18n('UPLOAD/ERROR_MISSING_TEMP_FOLDER');
break;
case UploadErrorCode.FileOnSaveingError:
result = i18n('UPLOAD/ERROR_ON_SAVING_FILE');
break;
case UploadErrorCode.FileType:
result = i18n('UPLOAD/ERROR_FILE_TYPE');
break;
default:
result = i18n('UPLOAD/ERROR_UNKNOWN');
break;
}
return result;
}
/**
* @param {boolean} admin
* @param {string} language
*/
export function reload(admin, language) {
const start = Date.now();
return new Promise((resolve, reject) => {
return fetch(langLink(language, admin), {cache: 'reload'})
.then(response => {
if (response.ok) {
const type = response.headers.get('Content-Type');
if (type.includes('application/javascript')) {
return response.text();
}
}
} else {
element.textContent = i18n(key);
}
})
, 1),
reject(new Error('Invalid response'))
}, error => {
reject(new Error(error.message))
})
.then(data => {
var script = doc.createElement('script');
script.text = data;
doc.head.appendChild(script).remove();
setTimeout(
() => {
reloadData();
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);
}
}
const isRtl = ['ar', 'ar_sa', 'he', 'he_he', 'ur', 'ur_ir'].includes((language || '').toLowerCase()),
htmlCL = doc.documentElement.classList;
return '';
},
htmlCL.remove('rl-rtl', 'rl-ltr');
htmlCL.add(isRtl ? 'rl-rtl' : 'rl-ltr');
// doc.documentElement.dir = isRtl ? 'rtl' : 'ltr'
timeToNode = (element, time) => {
try {
if (time) {
element.dateTime = new Date(time * 1000).toISOString();
} else {
time = Date.parse(element.dateTime) / 1000;
}
let key = element.dataset.timeFormat;
if (key) {
element.textContent = timestampToString(time, key);
if ('FULL' !== key && 'FROMNOW' !== key) {
element.title = timestampToString(time, 'FULL');
}
}
} catch (e) {
// prevent knockout crashes
console.error(e);
}
},
reloadTime = () => doc.querySelectorAll('time').forEach(element => timeToNode(element)),
/**
* @param {Function} startCallback
* @param {Function=} langCallback = null
*/
initOnStartOrLangChange = (startCallback, langCallback) => {
startCallback?.();
startCallback && translateTrigger.subscribe(startCallback);
langCallback && translateTrigger.subscribe(langCallback);
},
/**
* @param {number} code
* @param {*=} message = ''
* @param {*=} defCode = null
* @returns {string}
*/
getNotification = (code, message = '', defCode = 0) => {
code = pInt(code);
if (Notifications.ClientViewError === code && message) {
return message;
}
return getNotificationMessage(code)
|| getNotificationMessage(pInt(defCode))
|| '';
},
getErrorMessage = (code, data) =>
getNotification(code) || data?.messageAdditional || data?.message || data,
/**
* @param {*} code
* @returns {string}
*/
getUploadErrorDescByCode = code => {
let key = getKeyByValue(UploadErrorCode, parseInt(code, 10));
return i18n('UPLOAD/ERROR_' + (key ? i18nKey(key) : 'UNKNOWN'));
},
/**
* @param {boolean} admin
* @param {string} language
*/
translatorReload = (language, admin) =>
new Promise((resolve, reject) => {
const script = createElement('script');
script.onload = () => {
// reload the data
if (init()) {
i18nToNodes(doc);
translateTrigger(!translateTrigger());
// admin || reloadTime();
}
script.remove();
resolve();
};
script.onerror = () => reject(Error('Language '+language+' failed'));
script.src = langLink(language, admin);
// script.async = true;
doc.head.append(script);
}),
/**
* @param {string} language
* @param {boolean=} isEng = false
* @returns {string}
*/
convertLangName = (language, isEng = false) =>
i18n(
'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/' + language,
null,
language
),
baseCollator = numeric => new Intl.Collator(doc.documentElement.lang, {numeric: !!numeric, sensitivity: 'base'});
init();
resolve();
},
500 < Date.now() - start ? 1 : 500
);
});
});
}

View file

@ -1,31 +1,969 @@
export const
import { ComposeType, SaveSettingsStep, FolderType } from 'Common/Enums';
import { Mime } from 'Common/Mime';
const
doc = document,
tpl = doc.createElement('template'),
isArray = Array.isArray,
arrayLength = array => isArray(array) && array.length,
isFunction = v => typeof v === 'function',
pString = value => null != value ? '' + value : '',
forEachObjectValue = (obj, fn) => Object.values(obj).forEach(fn),
forEachObjectEntry = (obj, fn) => Object.entries(obj).forEach(([key, value]) => fn(key, value)),
pInt = (value, defaultValue = 0) => {
value = parseInt(value, 10);
return isFinite(value) ? value : defaultValue;
htmlmap = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;'
},
htmlspecialchars = str => (''+str).replace(/[&<>"']/g, m => htmlmap[m]);
defaultOptionsAfterRender = (domItem, item) =>
item && undefined !== item.disabled && domItem?.classList.toggle('disabled', domItem.disabled = item.disabled),
/**
* @param {(string|number)} value
* @param {boolean=} includeZero = true
* @returns {boolean}
*/
export function isPosNumeric(value, includeZero = true) {
return null != value && (includeZero ? /^[0-9]*$/ : /^[1-9]+[0-9]*$/).test(value.toString());
}
// unescape(encodeURIComponent()) makes the UTF-16 DOMString to an UTF-8 string
b64Encode = data => btoa(unescape(encodeURIComponent(data))),
/* // Without deprecated 'unescape':
b64Encode = data => btoa(encodeURIComponent(data).replace(
/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode('0x' + p1)
)),
*/
/**
* @param {*} value
* @param {number=} defaultValue = 0
* @returns {number}
*/
export function pInt(value, defaultValue = 0) {
value = parseInt(value, 10);
return isNaN(value) || !isFinite(value) ? defaultValue : value;
}
b64EncodeJSON = data => b64Encode(JSON.stringify(data)),
/**
* @param {*} value
* @returns {string}
*/
export function pString(value) {
return null != value ? '' + value : '';
}
b64EncodeJSONSafe = data => b64EncodeJSON(data).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
/**
* @param {string} queryString
* @returns {Object}
*/
export function simpleQueryParser(queryString) {
const queries = queryString.split('&'),
params = {};
getKeyByValue = (o, v) => Object.keys(o).find(key => o[key] === v);
queries.forEach(temp => {
temp = temp.split('=');
params[decodeURIComponent(temp[0])] = decodeURIComponent(temp[1]);
});
return params;
}
/**
* @param {number=} len = 32
* @returns {string}
*/
export function fakeMd5(len = 32) {
const line = '0123456789abcdefghijklmnopqrstuvwxyz';
len = pInt(len);
let result = '';
while (len--)
result += line.substr(Math.round(Math.random() * 36), 1);
return result;
}
/**
* @param {string} text
* @returns {string}
*/
export function encodeHtml(text) {
return null != text ? htmlspecialchars(text.toString()) : '';
}
/**
* @param {string} text
* @param {number=} len = 100
* @returns {string}
*/
export function splitPlainText(text, len = 100) {
let prefix = '',
subText = '',
result = text,
spacePos = 0,
newLinePos = 0;
while (result.length > len) {
subText = result.substr(0, len);
spacePos = subText.lastIndexOf(' ');
newLinePos = subText.lastIndexOf('\n');
if (-1 !== newLinePos) {
spacePos = newLinePos;
}
if (-1 === spacePos) {
spacePos = len;
}
prefix += subText.substr(0, spacePos) + '\n';
result = result.substr(spacePos + 1);
}
return prefix + result;
}
/**
* @param {any} m
* @returns {any}
*/
export function deModule(m) {
return (m && m.default ? m.default : m) || '';
}
/**
* @returns {boolean}
*/
export function inFocus() {
try {
if (doc.activeElement) {
if (undefined === doc.activeElement.__inFocusCache) {
doc.activeElement.__inFocusCache = doc.activeElement.matches(
'input,textarea,iframe,.cke_editable'
);
}
return !!doc.activeElement.__inFocusCache;
}
} catch (e) {} // eslint-disable-line no-empty
return false;
}
/**
* @param {boolean} force
* @returns {void}
*/
export function removeInFocus(force) {
if (doc.activeElement && doc.activeElement.blur) {
try {
if (force || doc.activeElement.matches('input,textarea')) {
doc.activeElement.blur();
}
} catch (e) {} // eslint-disable-line no-empty
}
}
/**
* @returns {void}
*/
export function removeSelection() {
try {
getSelection().removeAllRanges();
} catch (e) {} // eslint-disable-line no-empty
}
/**
* @param {string} prefix
* @param {string} subject
* @returns {string}
*/
export function replySubjectAdd(prefix, subject) {
prefix = prefix.toUpperCase().trim();
subject = subject.replace(/[\s]+/g, ' ').trim();
let drop = false,
re = 'RE' === prefix,
fwd = 'FWD' === prefix;
const parts = [],
prefixIsRe = !fwd;
if (subject) {
subject.split(':').forEach(part => {
const trimmedPart = part.trim();
if (!drop && (/^(RE|FWD)$/i.test(trimmedPart) || /^(RE|FWD)[[(][\d]+[\])]$/i.test(trimmedPart))) {
if (!re) {
re = !!/^RE/i.test(trimmedPart);
}
if (!fwd) {
fwd = !!/^FWD/i.test(trimmedPart);
}
} else {
parts.push(part);
drop = true;
}
});
}
if (prefixIsRe) {
re = false;
} else {
fwd = false;
}
return ((prefixIsRe ? 'Re: ' : 'Fwd: ') + (re ? 'Re: ' : '') + (fwd ? 'Fwd: ' : '') + parts.join(':').trim()).trim();
}
/**
* @param {(number|string)} sizeInBytes
* @returns {string}
*/
export function friendlySize(sizeInBytes) {
sizeInBytes = pInt(sizeInBytes);
const sizes = ['B', 'KiB', 'MiB', 'GiB', 'TiB'], i = pInt(Math.floor(Math.log(sizeInBytes) / Math.log(1024)));
return (sizeInBytes / Math.pow(1024, i)).toFixed(2>i ? 0 : 1) + sizes[i];
}
/**
* @param {string} theme
* @returns {string}
*/
export const convertThemeName = theme => {
if ('@custom' === theme.substr(-7)) {
theme = theme.substr(0, theme.length - 7).trim();
}
return theme
.replace(/[^a-zA-Z0-9]+/g, ' ')
.replace(/([A-Z])/g, ' $1')
.replace(/\s+/g, ' ')
.trim();
};
/**
*
* @param {string} language
* @param {boolean=} isEng = false
* @returns {string}
*/
export function convertLangName(language, isEng = false) {
return require('Common/Translator').i18n(
'LANGS_NAMES' + (true === isEng ? '_EN' : '') + '/LANG_' + language.toUpperCase().replace(/[^a-zA-Z0-9]+/g, '_'),
null,
language
);
}
/**
* @param {object} domOption
* @param {object} item
* @returns {void}
*/
export function defautOptionsAfterRender(domItem, item) {
if (item && undefined !== item.disabled && domItem) {
domItem.classList.toggle('disabled', domItem.disabled = item.disabled);
}
}
/**
* @param {Function} fCallback
* @param {?} koTrigger
* @param {?} context = null
* @param {number=} timer = 1000
* @returns {Function}
*/
export function settingsSaveHelperFunction(fCallback, koTrigger, context = null, timer = 1000) {
timer = pInt(timer);
return (type, data, cached, requestAction, requestParameters) => {
koTrigger.call(context, data && data.Result ? SaveSettingsStep.TrueResult : SaveSettingsStep.FalseResult);
if (fCallback) {
fCallback.call(context, type, data, cached, requestAction, requestParameters);
}
setTimeout(() => {
koTrigger.call(context, SaveSettingsStep.Idle);
}, timer);
};
}
/**
* @param {object} koTrigger
* @param {mixed} context
* @returns {mixed}
*/
export function settingsSaveHelperSimpleFunction(koTrigger, context) {
return settingsSaveHelperFunction(null, koTrigger, context, 1000);
}
/**
* @param {object} remote
* @param {string} settingName
* @param {string} type
* @param {function} fTriggerFunction
* @returns {function}
*/
export function settingsSaveHelperSubscribeFunction(remote, settingName, type, fTriggerFunction) {
return (value) => {
if (remote) {
switch (type) {
case 'bool':
case 'boolean':
value = value ? '1' : '0';
break;
case 'int':
case 'integer':
case 'number':
value = pInt(value);
break;
case 'trim':
value = value.trim();
break;
default:
value = pString(value);
break;
}
const data = {};
data[settingName] = value;
if (remote.saveAdminConfig) {
remote.saveAdminConfig(fTriggerFunction || null, data);
} else if (remote.saveSettings) {
remote.saveSettings(fTriggerFunction || null, data);
}
}
};
}
/**
* @param {string} html
* @returns {string}
*/
/*eslint-disable max-len*/
const url = /(^|[\s\n]|\/?>)(https:\/\/[-A-Z0-9+\u0026\u2019#/%?=()~_|!:,.;]*[-A-Z0-9+\u0026#/%=~()_|])/gi,
email = /(^|[\s\n]|\/?>)((?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x21\x23-\x5b\x5d-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x21-\x5a\x53-\x7f]|\\[\x21\x23-\x5b\x5d-\x7f])+)\]))/gi;
export function findEmailAndLinks(html) {
return html
.replace(url, '$1<a href="$2" target="_blank">$2</a>')
.replace(email, '$1<a href="mailto:$2">$2</a>');
}
/**
* @param {string} html
* @returns {string}
*/
export function htmlToPlain(html) {
let pos = 0,
limit = 0,
iP1 = 0,
iP2 = 0,
iP3 = 0,
text = '';
const convertBlockquote = (blockquoteText) => {
blockquoteText = '> ' + blockquoteText.trim().replace(/\n/gm, '\n> ');
return blockquoteText.replace(/(^|\n)([> ]+)/gm, (...args) =>
args && 2 < args.length ? args[1] + args[2].replace(/[\s]/g, '').trim() + ' ' : ''
);
};
const convertDivs = (...args) => {
if (args && 1 < args.length) {
let divText = args[1].trim();
if (divText.length) {
divText = divText.replace(/<div[^>]*>([\s\S\r\n]*)<\/div>/gim, convertDivs);
divText = '\n' + divText.trim() + '\n';
}
return divText;
}
return '';
};
const convertPre = (...args) =>
args && 1 < args.length
? args[1]
.toString()
.replace(/[\n]/gm, '<br />')
.replace(/[\r]/gm, '')
: '',
fixAttibuteValue = (...args) => (args && 1 < args.length ? '' + args[1] + htmlspecialchars(args[2]) : ''),
convertLinks = (...args) => (args && 1 < args.length ? args[1].trim() : '');
tpl.innerHTML = html
.replace(/<p[^>]*><\/p>/gi, '')
.replace(/<pre[^>]*>([\s\S\r\n\t]*)<\/pre>/gim, convertPre)
.replace(/[\s]+/gm, ' ')
.replace(/((?:href|data)\s?=\s?)("[^"]+?"|'[^']+?')/gim, fixAttibuteValue)
.replace(/<br[^>]*>/gim, '\n')
.replace(/<\/h[\d]>/gi, '\n')
.replace(/<\/p>/gi, '\n\n')
.replace(/<ul[^>]*>/gim, '\n')
.replace(/<\/ul>/gi, '\n')
.replace(/<li[^>]*>/gim, ' * ')
.replace(/<\/li>/gi, '\n')
.replace(/<\/td>/gi, '\n')
.replace(/<\/tr>/gi, '\n')
.replace(/<hr[^>]*>/gim, '\n_______________________________\n\n')
.replace(/<div[^>]*>([\s\S\r\n]*)<\/div>/gim, convertDivs)
.replace(/<blockquote[^>]*>/gim, '\n__bq__start__\n')
.replace(/<\/blockquote>/gim, '\n__bq__end__\n')
.replace(/<a [^>]*>([\s\S\r\n]*?)<\/a>/gim, convertLinks)
.replace(/<\/div>/gi, '\n')
.replace(/&nbsp;/gi, ' ')
.replace(/&quot;/gi, '"')
.replace(/<[^>]*>/gm, '');
text = splitPlainText(tpl.innerText
.replace(/\n[ \t]+/gm, '\n')
.replace(/[\n]{3,}/gm, '\n\n')
.replace(/&gt;/gi, '>')
.replace(/&lt;/gi, '<')
.replace(/&amp;/gi, '&')
);
pos = 0;
limit = 800;
while (0 < limit) {
--limit;
iP1 = text.indexOf('__bq__start__', pos);
if (-1 < iP1) {
iP2 = text.indexOf('__bq__start__', iP1 + 5);
iP3 = text.indexOf('__bq__end__', iP1 + 5);
if ((-1 === iP2 || iP3 < iP2) && iP1 < iP3) {
text = text.substr(0, iP1) + convertBlockquote(text.substring(iP1 + 13, iP3)) + text.substr(iP3 + 11);
pos = 0;
} else if (-1 < iP2 && iP2 < iP3) {
pos = iP2 - 1;
} else {
pos = 0;
}
} else {
break;
}
}
return text.replace(/__bq__start__/gm, '').replace(/__bq__end__/gm, '');
}
/**
* @param {string} plain
* @param {boolean} findEmailAndLinksInText = false
* @returns {string}
*/
export function plainToHtml(plain) {
plain = plain.toString().replace(/\r/g, '');
plain = plain.replace(/^>[> ]>+/gm, ([match]) => (match ? match.replace(/[ ]+/g, '') : match));
let bIn = false,
bDo = true,
bStart = true,
aNextText = [],
aText = plain.split('\n');
do {
bDo = false;
aNextText = [];
aText.forEach(sLine => {
bStart = '>' === sLine.substr(0, 1);
if (bStart && !bIn) {
bDo = true;
bIn = true;
aNextText.push('~~~blockquote~~~');
aNextText.push(sLine.substr(1));
} else if (!bStart && bIn) {
if (sLine) {
bIn = false;
aNextText.push('~~~/blockquote~~~');
aNextText.push(sLine);
} else {
aNextText.push(sLine);
}
} else if (bStart && bIn) {
aNextText.push(sLine.substr(1));
} else {
aNextText.push(sLine);
}
});
if (bIn) {
bIn = false;
aNextText.push('~~~/blockquote~~~');
}
aText = aNextText;
} while (bDo);
return aText.join('\n')
// .replace(/~~~\/blockquote~~~\n~~~blockquote~~~/g, '\n')
.replace(/&/g, '&amp;')
.replace(/>/g, '&gt;')
.replace(/</g, '&lt;')
.replace(/~~~blockquote~~~[\s]*/g, '<blockquote>')
.replace(/[\s]*~~~\/blockquote~~~/g, '</blockquote>')
.replace(/\n/g, '<br />');
}
rl.Utils = {
htmlToPlain: htmlToPlain,
plainToHtml: plainToHtml
};
/**
* @param {Array} aSystem
* @param {Array} aList
* @param {Array=} aDisabled
* @param {Array=} aHeaderLines
* @param {?number=} iUnDeep
* @param {Function=} fDisableCallback
* @param {Function=} fVisibleCallback
* @param {Function=} fRenameCallback
* @param {boolean=} bSystem
* @param {boolean=} bBuildUnvisible
* @returns {Array}
*/
export function folderListOptionsBuilder(
aSystem,
aList,
aDisabled,
aHeaderLines,
iUnDeep,
fDisableCallback,
fVisibleCallback,
fRenameCallback,
bSystem,
bBuildUnvisible
) {
let /**
* @type {?FolderModel}
*/
bSep = false,
aResult = [];
const sDeepPrefix = '\u00A0\u00A0\u00A0';
bBuildUnvisible = undefined === bBuildUnvisible ? false : !!bBuildUnvisible;
bSystem = null == bSystem ? 0 < aSystem.length : bSystem;
iUnDeep = null == iUnDeep ? 0 : iUnDeep;
fDisableCallback = null != fDisableCallback ? fDisableCallback : null;
fVisibleCallback = null != fVisibleCallback ? fVisibleCallback : null;
fRenameCallback = null != fRenameCallback ? fRenameCallback : null;
if (!isArray(aDisabled)) {
aDisabled = [];
}
if (!isArray(aHeaderLines)) {
aHeaderLines = [];
}
aHeaderLines.forEach(line => {
aResult.push({
id: line[0],
name: line[1],
system: false,
seporator: false,
disabled: false
});
});
bSep = true;
aSystem.forEach(oItem => {
if (fVisibleCallback ? fVisibleCallback(oItem) : true) {
if (bSep && aResult.length) {
aResult.push({
id: '---',
name: '---',
system: false,
seporator: true,
disabled: true
});
}
bSep = false;
aResult.push({
id: oItem.fullNameRaw,
name: fRenameCallback ? fRenameCallback(oItem) : oItem.name(),
system: true,
seporator: false,
disabled:
!oItem.selectable ||
aDisabled.includes(oItem.fullNameRaw) ||
(fDisableCallback ? fDisableCallback(oItem) : false)
});
}
});
bSep = true;
aList.forEach(oItem => {
// if (oItem.subScribed() || !oItem.existen || bBuildUnvisible)
if (
(oItem.subScribed() || !oItem.existen || bBuildUnvisible) &&
(oItem.selectable || oItem.hasSubScribedSubfolders())
) {
if (fVisibleCallback ? fVisibleCallback(oItem) : true) {
if (FolderType.User === oItem.type() || !bSystem || oItem.hasSubScribedSubfolders()) {
if (bSep && aResult.length) {
aResult.push({
id: '---',
name: '---',
system: false,
seporator: true,
disabled: true
});
}
bSep = false;
aResult.push({
id: oItem.fullNameRaw,
name:
new Array(oItem.deep + 1 - iUnDeep).join(sDeepPrefix) +
(fRenameCallback ? fRenameCallback(oItem) : oItem.name()),
system: false,
seporator: false,
disabled:
!oItem.selectable ||
aDisabled.includes(oItem.fullNameRaw) ||
(fDisableCallback ? fDisableCallback(oItem) : false)
});
}
}
}
if (oItem.subScribed() && oItem.subFolders().length) {
aResult = aResult.concat(
folderListOptionsBuilder(
[],
oItem.subFolders(),
aDisabled,
[],
iUnDeep,
fDisableCallback,
fVisibleCallback,
fRenameCallback,
bSystem,
bBuildUnvisible
)
);
}
});
return aResult;
}
/**
* @param {object} element
* @returns {void}
*/
export function selectElement(element) {
let sel = getSelection(),
range = doc.createRange();
sel.removeAllRanges();
range.selectNodeContents(element);
sel.addRange(range);
}
/**
* @param {Object|Array} objectOrObjects
* @returns {void}
*/
export function delegateRunOnDestroy(objectOrObjects) {
if (objectOrObjects) {
if (isArray(objectOrObjects)) {
objectOrObjects.forEach(item => delegateRunOnDestroy(item));
} else {
objectOrObjects.onDestroy && objectOrObjects.onDestroy();
}
}
}
let __themeTimer = 0,
__themeAjax = null;
/**
* @param {string} value
* @param {function=} themeTrigger = noop
* @returns {void}
*/
export function changeTheme(value, themeTrigger = ()=>{}) {
const themeLink = doc.getElementById('app-theme-link'),
clearTimer = () => {
__themeTimer = setTimeout(() => themeTrigger(SaveSettingsStep.Idle), 1000);
__themeAjax = null;
};
let themeStyle = doc.getElementById('app-theme-style'),
url = (themeLink && themeLink.href) || (themeStyle && themeStyle.dataset.href);
if (url) {
url = url.toString()
.replace(/\/-\/[^/]+\/-\//, '/-/' + value + '/-/')
.replace(/\/Css\/[^/]+\/User\//, '/Css/0/User/')
.replace(/\/Hash\/[^/]+\//, '/Hash/-/');
if ('Json/' !== url.substr(-5)) {
url += 'Json/';
}
clearTimeout(__themeTimer);
themeTrigger(SaveSettingsStep.Animate);
if (__themeAjax) {
__themeAjax.abort();
}
let init = {};
if (window.AbortController) {
__themeAjax = new AbortController();
init.signal = __themeAjax.signal;
}
rl.fetchJSON(url, init)
.then(data => {
if (data && isArray(data) && 2 === data.length) {
if (themeLink && !themeStyle) {
themeStyle = doc.createElement('style');
themeStyle.id = 'app-theme-style';
themeLink.after(themeStyle);
themeLink.remove();
}
if (themeStyle) {
themeStyle.textContent = data[1];
themeStyle.dataset.href = url;
themeStyle.dataset.theme = data[0];
}
themeTrigger(SaveSettingsStep.TrueResult);
}
})
.then(clearTimer, clearTimer);
}
}
/**
* @returns {function}
*/
export function computedPagenatorHelper(koCurrentPage, koPageCount) {
return () => {
const currentPage = koCurrentPage(),
pageCount = koPageCount(),
result = [],
fAdd = (index, push = true, customName = '') => {
const data = {
current: index === currentPage,
name: customName ? customName.toString() : index.toString(),
custom: !!customName,
title: customName ? index.toString() : '',
value: index.toString()
};
if (push) {
result.push(data);
} else {
result.unshift(data);
}
};
let prev = 0,
next = 0,
limit = 2;
if (1 < pageCount || (0 < pageCount && pageCount < currentPage)) {
if (pageCount < currentPage) {
fAdd(pageCount);
prev = pageCount;
next = pageCount;
} else {
if (3 >= currentPage || pageCount - 2 <= currentPage) {
limit += 2;
}
fAdd(currentPage);
prev = currentPage;
next = currentPage;
}
while (0 < limit) {
--prev;
++next;
if (0 < prev) {
fAdd(prev, false);
--limit;
}
if (pageCount >= next) {
fAdd(next, true);
--limit;
} else if (0 >= prev) {
break;
}
}
if (3 === prev) {
fAdd(2, false);
} else if (3 < prev) {
fAdd(Math.round((prev - 1) / 2), false, '...');
}
if (pageCount - 2 === next) {
fAdd(pageCount - 1, true);
} else if (pageCount - 2 > next) {
fAdd(Math.round((pageCount + next) / 2), true, '...');
}
// first and last
if (1 < prev) {
fAdd(1, false);
}
if (pageCount > next) {
fAdd(pageCount, true);
}
}
return result;
};
}
/**
* @param {string} fileName
* @returns {string}
*/
export function getFileExtension(fileName) {
fileName = fileName.toLowerCase().trim();
const result = fileName.split('.').pop();
return result === fileName ? '' : result;
}
/**
* @param {string} fileName
* @returns {string}
*/
export function mimeContentType(fileName) {
let ext = '',
result = 'application/octet-stream';
fileName = fileName.toLowerCase().trim();
if ('winmail.dat' === fileName) {
return 'application/ms-tnef';
}
ext = getFileExtension(fileName);
if (ext && ext.length && undefined !== Mime[ext]) {
result = Mime[ext];
}
return result;
}
/**
* @param {string} color
* @returns {boolean}
*/
export function isTransparent(color) {
return 'rgba(0, 0, 0, 0)' === color || 'transparent' === color;
}
/**
* @param {string} url
* @param {number} value
* @param {Function} fCallback
*/
export function resizeAndCrop(url, value, fCallback) {
const img = new Image();
img.onload = function() {
let diff = [0, 0];
const canvas = doc.createElement('canvas'),
ctx = canvas.getContext('2d');
canvas.width = value;
canvas.height = value;
if (this.width > this.height) {
diff = [this.width - this.height, 0];
} else {
diff = [0, this.height - this.width];
}
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, value, value);
ctx.drawImage(this, diff[0] / 2, diff[1] / 2, this.width - diff[0], this.height - diff[1], 0, 0, value, value);
fCallback(canvas.toDataURL('image/jpeg'));
};
img.src = url;
}
/**
* @param {string} mailToUrl
* @param {Function} PopupComposeViewModel
* @returns {boolean}
*/
export function mailToHelper(mailToUrl, PopupComposeViewModel) {
if (
mailToUrl &&
'mailto:' ===
mailToUrl
.toString()
.substr(0, 7)
.toLowerCase()
) {
if (!PopupComposeViewModel) {
return true;
}
mailToUrl = mailToUrl.toString().substr(7);
let to = [],
cc = null,
bcc = null,
params = {};
const email = mailToUrl.replace(/\?.+$/, ''),
query = mailToUrl.replace(/^[^?]*\?/, ''),
EmailModel = require('Model/Email').default;
params = simpleQueryParser(query);
if (undefined !== params.to) {
to = EmailModel.parseEmailLine(decodeURIComponent(email + ',' + params.to));
to = Object.values(
to.reduce((result, value) => {
if (value) {
if (result[value.email]) {
if (!result[value.email].name) {
result[value.email] = value;
}
} else {
result[value.email] = value;
}
}
return result;
}, {})
);
} else {
to = EmailModel.parseEmailLine(email);
}
if (undefined !== params.cc) {
cc = EmailModel.parseEmailLine(decodeURIComponent(params.cc));
}
if (undefined !== params.bcc) {
bcc = EmailModel.parseEmailLine(decodeURIComponent(params.bcc));
}
require('Knoin/Knoin').showScreenPopup(PopupComposeViewModel, [
ComposeType.Empty,
null,
to,
cc,
bcc,
null == params.subject ? null : pString(decodeURIComponent(params.subject)),
null == params.body ? null : plainToHtml(pString(decodeURIComponent(params.body)))
]);
return true;
}
return false;
}

View file

@ -1,374 +0,0 @@
import { Notifications } from 'Common/Enums';
import { MessageSetAction, ComposeType/*, FolderType*/ } from 'Common/EnumsUser';
import { doc, createElement, elementById, dropdowns, dropdownVisibility, SettingsGet, leftPanelDisabled } from 'Common/Globals';
import { plainToHtml } from 'Common/Html';
import { getNotification } from 'Common/Translator';
import { EmailCollectionModel } from 'Model/EmailCollection';
import { MessageUserStore } from 'Stores/User/Message';
import { MessagelistUserStore } from 'Stores/User/Messagelist';
import { SettingsUserStore } from 'Stores/User/Settings';
import * as Local from 'Storage/Client';
import { ThemeStore } from 'Stores/Theme';
import Remote from 'Remote/User/Fetch';
import { attachmentDownload } from 'Common/Links';
import { AccountModel } from 'Model/Account';
import { IdentityModel } from 'Model/Identity';
import { AccountUserStore } from 'Stores/User/Account';
import { IdentityUserStore } from 'Stores/User/Identity';
import { isArray } from 'Common/Utils';
import { showScreenPopup } from 'Knoin/Knoin';
import { IdentityPopupView } from 'View/Popup/Identity';
export const
// 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
* @returns {boolean}
*/
download = (link, name = "") => {
console.log('download: '+link);
// 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) {
let params = {
target: 'zip',
filename: name,
hashes: hashes
};
if (!onError) {
onError = () => alert('Download failed');
}
if (folder) {
params.folder = folder;
// params.uids = uids;
}
Remote.post('AttachmentsActions', fTrigger || null, params)
.then(result => {
let hash = result?.Result?.fileHash;
hash ? download(attachmentDownload(hash), hash+'.zip') : onError();
})
.catch(onError);
}
},
/**
* @returns {function}
*/
computedPaginatorHelper = (koCurrentPage, koPageCount) => {
return () => {
const currentPage = koCurrentPage(),
pageCount = koPageCount(),
result = [],
lang = doc.documentElement.lang,
fAdd = (index, push = true, customName = '') => {
const name = index.toLocaleString(lang),
data = {
current: index === currentPage,
name: customName || name,
title: customName ? name : '',
value: index
};
push ? result.push(data) : result.unshift(data);
};
let prev = 0,
next = 0,
limit = 2;
if (1 < pageCount) {
if (pageCount < currentPage) {
fAdd(pageCount);
prev = pageCount;
next = pageCount;
} else {
if (3 >= currentPage || pageCount - 2 <= currentPage) {
limit += 2;
}
fAdd(currentPage);
prev = currentPage;
next = currentPage;
}
while (0 < limit) {
--prev;
++next;
if (0 < prev) {
fAdd(prev, false);
--limit;
}
if (pageCount >= next) {
fAdd(next, true);
--limit;
} else if (0 >= prev) {
break;
}
}
if (3 === prev) {
fAdd(2, false);
} else if (3 < prev) {
fAdd(Math.round((prev - 1) / 2), false, '…');
}
if (pageCount - 2 === next) {
fAdd(pageCount - 1, true);
} else if (pageCount - 2 > next) {
fAdd(Math.round((pageCount + next) / 2), true, '…');
}
// first and last
if (1 < prev) {
fAdd(1, false);
}
if (pageCount > next) {
fAdd(pageCount, true);
}
}
return result;
};
},
/**
* @param {string} mailToUrl
* @returns {boolean}
*/
mailToHelper = mailToUrl => {
if ('mailto:' === mailToUrl?.slice(0, 7).toLowerCase()) {
mailToUrl = mailToUrl.slice(7).split('?');
const
email = decodeURIComponent(mailToUrl[0]),
params = new URLSearchParams(mailToUrl[1]),
to = params.get('to'),
toEmailModel = value => EmailCollectionModel.fromString(value);
showMessageComposer([
ComposeType.Empty,
null,
toEmailModel(to ? email + ',' + to : email),
toEmailModel(params.get('cc')),
toEmailModel(params.get('bcc')),
params.get('subject'),
plainToHtml(params.get('body') || '')
]);
return true;
}
return false;
},
showMessageComposer = (params = []) =>
{
rl.app.showMessageComposer(params);
},
setLayoutResizer = (source, sClientSideKeyName, mode) =>
{
if (source.layoutResizer && source.layoutResizer.mode != mode) {
source.removeAttribute('style');
}
source.observer?.disconnect();
// source.classList.toggle('resizable', mode);
if (mode) {
const length = Local.get(sClientSideKeyName + mode) || SettingsGet('Resizer' + sClientSideKeyName + mode);
if (length) {
source.style[mode.toLowerCase()] = length + 'px';
}
if (!source.layoutResizer) {
const resizer = createElement('div', {'class':'resizer'}),
save = (data => Remote.saveSettings(0, data)).debounce(500),
size = {},
store = () => {
const value = ('Width' == resizer.mode) ? source.offsetWidth : source.offsetHeight,
prop = resizer.key + resizer.mode;
(value == Local.get(prop)) || Local.set(prop, value);
(value == SettingsGet('Resizer' + prop)) || save({['Resizer' + prop]: value});
},
cssint = s => {
let value = getComputedStyle(source, null)[s].replace('px', '');
if (value.includes('%')) {
value = source.parentElement['offset'+resizer.mode]
* value.replace('%', '') / 100;
}
return parseFloat(value);
};
source.layoutResizer = resizer;
source.append(resizer);
resizer.addEventListener('mousedown', {
handleEvent: function(e) {
if ('mousedown' == e.type) {
const lmode = resizer.mode.toLowerCase();
e.preventDefault();
size.pos = ('width' == lmode) ? e.pageX : e.pageY;
size.min = cssint('min-'+lmode);
size.max = cssint('max-'+lmode);
size.org = cssint(lmode);
addEventListener('mousemove', this);
addEventListener('mouseup', this);
} else if ('mousemove' == e.type) {
const lmode = resizer.mode.toLowerCase(),
length = size.org + (('width' == lmode ? e.pageX : e.pageY) - size.pos);
if (length >= size.min && length <= size.max ) {
source.style[lmode] = length + 'px';
source.observer || store();
}
} else if ('mouseup' == e.type) {
removeEventListener('mousemove', this);
removeEventListener('mouseup', this);
}
}
});
source.observer = window.ResizeObserver ? new ResizeObserver(store) : null;
}
source.layoutResizer.mode = mode;
source.layoutResizer.key = sClientSideKeyName;
source.observer?.observe(source, { box: 'border-box' });
}
},
viewMessage = (oMessage, popup) => {
if (popup) {
oMessage.popupMessage();
} else {
MessageUserStore.error('');
let id = 'rl-msg-' + oMessage.hash,
body = oMessage.body || elementById(id);
if (!body) {
body = createElement('div',{
id:id,
hidden:'',
class:'b-text-part'
+ (oMessage.pgpSigned() ? ' openpgp-signed' : '')
+ (oMessage.pgpEncrypted() ? ' openpgp-encrypted' : '')
+ (oMessage.smimeSigned() ? ' smime-signed' : '')
+ (oMessage.smimeEncrypted() ? ' smime-encrypted' : '')
});
MessageUserStore.purgeCache();
}
body.message = oMessage;
oMessage.body = body;
if (!SettingsUserStore.viewHTML() || !oMessage.viewHtml()) {
oMessage.viewPlain();
}
MessageUserStore.bodiesDom().append(body);
MessageUserStore.loading(false);
oMessage.body.hidden = false;
if (oMessage.isUnseen() && SettingsUserStore.messageReadAuto()) {
MessageUserStore.MessageSeenTimer = setTimeout(
() => MessagelistUserStore.setAction(oMessage.folder, MessageSetAction.SetSeen, [oMessage]),
SettingsUserStore.messageReadDelay() * 1000 // seconds
);
}
}
},
populateMessageBody = (oMessage, popup) => {
if (oMessage) {
popup || MessageUserStore.message(oMessage);
if (oMessage.body) {
viewMessage(oMessage, popup);
} else {
popup || MessageUserStore.loading(true);
Remote.message((iError, oData/*, bCached*/) => {
if (iError) {
if (Notifications.RequestAborted !== iError && !popup) {
MessageUserStore.message(null);
MessageUserStore.error(getNotification(iError));
}
} else {
let json = oData?.Result;
if (json
&& ((
oMessage.hash && oMessage.hash === json.hash
) || (
!oMessage.hash
&& oMessage.folder === json.folder
&& oMessage.uid == json.uid)
)
&& oMessage.revivePropertiesFromJson(json)
) {
/*
if (bCached) {
delete json.flags;
}
oMessage.body.remove();
*/
viewMessage(oMessage, popup);
}
}
popup || MessageUserStore.loading(false);
}, oMessage.folder, oMessage.uid);
}
}
};
leftPanelDisabled.subscribe(value => value && moveAction(0));
moveAction.subscribe(value => value && leftPanelDisabled(false));

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