diff --git a/.gitignore b/.gitignore index 23f9e430..9e8412e2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,10 @@ data/conf/sogo/sieve.creds data/conf/dovecot/dovecot-master.passwd mailcow.conf mailcow.conf_backup -data/conf/nginx/listen.active +data/conf/nginx/listen*active +data/conf/nginx/server_name*active +data/conf/postfix/sql +data/conf/dovecot/sql +data/web/inc/vars.local.inc.php +site/ +data/assets/ssl diff --git a/README.md b/README.md index bd305b38..025dc825 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,4 @@ [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=JWBSYHF4SMC68) -Please see the official documentation for instructions => [mailcow.email/dockerized](https://mailcow.email/dockerized) +Please see [the official documentation](https://andryyy.github.io/mailcow-dockerized/) for instructions. diff --git a/data/Dockerfiles/bind9/.empty b/data/Dockerfiles/bind9/.empty new file mode 100644 index 00000000..e69de29b diff --git a/data/Dockerfiles/clamav/Dockerfile b/data/Dockerfiles/clamav/Dockerfile new file mode 100755 index 00000000..5fc44d9a --- /dev/null +++ b/data/Dockerfiles/clamav/Dockerfile @@ -0,0 +1,44 @@ +FROM debian:latest +MAINTAINER https://m-ko.de Markus Kosmal + +# Debian Base to use +ENV DEBIAN_VERSION jessie + +# initial install of av daemon +RUN echo "deb http://http.debian.net/debian/ $DEBIAN_VERSION main contrib non-free" > /etc/apt/sources.list && \ + echo "deb http://http.debian.net/debian/ $DEBIAN_VERSION-updates main contrib non-free" >> /etc/apt/sources.list && \ + echo "deb http://security.debian.org/ $DEBIAN_VERSION/updates main contrib non-free" >> /etc/apt/sources.list && \ + apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y -qq \ + clamav-daemon \ + clamav-freshclam \ + libclamunrar7 \ + wget && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# initial update of av databases +RUN wget -O /var/lib/clamav/main.cvd http://db.local.clamav.net/main.cvd && \ + wget -O /var/lib/clamav/daily.cvd http://db.local.clamav.net/daily.cvd && \ + wget -O /var/lib/clamav/bytecode.cvd http://db.local.clamav.net/bytecode.cvd && \ + chown clamav:clamav /var/lib/clamav/*.cvd + +# permission juggling +RUN mkdir /var/run/clamav && \ + chown clamav:clamav /var/run/clamav && \ + chmod 750 /var/run/clamav + +# av configuration update +RUN sed -i 's/^Foreground .*$/Foreground true/g' /etc/clamav/clamd.conf && \ + echo "TCPSocket 3310" >> /etc/clamav/clamd.conf && \ + sed -i 's/^Foreground .*$/Foreground true/g' /etc/clamav/freshclam.conf + +# volume provision +VOLUME ["/var/lib/clamav"] + +# port provision +EXPOSE 3310 + +# av daemon bootstrapping +COPY bootstrap.sh / +CMD ["/bootstrap.sh"] diff --git a/data/Dockerfiles/clamav/bootstrap.sh b/data/Dockerfiles/clamav/bootstrap.sh new file mode 100755 index 00000000..635e93ea --- /dev/null +++ b/data/Dockerfiles/clamav/bootstrap.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# bootstrap clam av service and clam av database updater shell script +# presented by mko (Markus Kosmal) +set -m + +# start clam service itself and the updater in background as daemon +freshclam -d & +clamd & + +# recognize PIDs +pidlist=`jobs -p` + +# initialize latest result var +latest_exit=0 + +# define shutdown helper +function shutdown() { + trap "" SUBS + + for single in $pidlist; do + if ! kill -0 $pidlist 2>/dev/null; then + wait $pidlist + exitcode=$? + fi + done + + kill $pidlist 2>/dev/null +} + +# run shutdown +trap terminate SUBS +wait + +# return received result +exit $latest_exit diff --git a/data/Dockerfiles/dovecot/Dockerfile b/data/Dockerfiles/dovecot/Dockerfile index 54a24690..abd07c1f 100644 --- a/data/Dockerfiles/dovecot/Dockerfile +++ b/data/Dockerfiles/dovecot/Dockerfile @@ -1,4 +1,4 @@ -From ubuntu:xenial +FROM ubuntu:xenial MAINTAINER Andre Peters ENV DEBIAN_FRONTEND noninteractive @@ -19,13 +19,46 @@ RUN apt-get -y install dovecot-common \ dovecot-mysql \ dovecot-pop3d \ dovecot-dev \ + syslog-ng \ + syslog-ng-core \ + ca-certificates \ + supervisor \ wget \ curl \ build-essential \ autotools-dev \ - automake + automake \ + libauthen-ntlm-perl \ + libcrypt-ssleay-perl \ + libdigest-hmac-perl \ + libfile-copy-recursive-perl \ + libio-compress-perl \ + libio-socket-inet6-perl \ + libio-socket-ssl-perl \ + libio-tee-perl \ + libmodule-scandeps-perl \ + libnet-ssleay-perl \ + libpar-packer-perl \ + libreadonly-perl \ + libterm-readkey-perl \ + libtest-pod-perl \ + libtest-simple-perl \ + libunicode-string-perl \ + liburi-perl \ + libdbi-perl \ + liblockfile-simple-perl \ + libdbd-mysql-perl \ + libipc-run-perl \ + make \ + cpanminus + +RUN sed -i -E 's/^(\s*)system\(\);/\1unix-stream("\/dev\/log");/' /etc/syslog-ng/syslog-ng.conf +RUN cpanm Data::Uniqid Mail::IMAPClient String::Util +RUN echo '* * * * * root /usr/local/bin/imapsync_cron.pl' > /etc/cron.d/imapsync +RUN echo '30 3 * * * vmail /usr/bin/doveadm quota recalc -A' > /etc/cron.d/dovecot-sync WORKDIR /tmp + RUN wget http://hg.dovecot.org/dovecot-antispam-plugin/archive/tip.tar.gz -O - | tar xvz \ && cd /tmp/dovecot-antispam* \ && ./autogen.sh \ @@ -33,10 +66,15 @@ RUN wget http://hg.dovecot.org/dovecot-antispam-plugin/archive/tip.tar.gz -O - | && make \ && make install +COPY ./imapsync /usr/local/bin/imapsync +COPY ./postlogin.sh /usr/local/bin/postlogin.sh +COPY ./imapsync_cron.pl /usr/local/bin/imapsync_cron.pl COPY ./rspamd-pipe /usr/local/bin/rspamd-pipe COPY ./docker-entrypoint.sh / +COPY ./supervisord.conf /etc/supervisor/supervisord.conf RUN chmod +x /usr/local/bin/rspamd-pipe +RUN chmod +x /usr/local/bin/imapsync_cron.pl RUN groupadd -g 5000 vmail RUN useradd -g vmail -u 5000 vmail -d /var/vmail @@ -44,6 +82,6 @@ RUN useradd -g vmail -u 5000 vmail -d /var/vmail EXPOSE 24 10001 ENTRYPOINT ["/docker-entrypoint.sh"] -CMD ["/usr/sbin/dovecot", "-F"] +CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh index 6071cf47..8ef09dba 100755 --- a/data/Dockerfiles/dovecot/docker-entrypoint.sh +++ b/data/Dockerfiles/dovecot/docker-entrypoint.sh @@ -1,11 +1,43 @@ #!/bin/bash set -e -# Set config parameters, escape " in db password +# Hard-code env vars to imapsync due to cron not passing them to the perl script +sed -i "/^\$DBUSER/c\\\$DBUSER='${DBUSER}';" /usr/local/bin/imapsync_cron.pl +sed -i "/^\$DBPASS/c\\\$DBPASS='${DBPASS}';" /usr/local/bin/imapsync_cron.pl +sed -i "/^\$DBNAME/c\\\$DBNAME='${DBNAME}';" /usr/local/bin/imapsync_cron.pl + +[[ ! -d /etc/dovecot/sql/ ]] && mkdir -p /etc/dovecot/sql/ + +# Set Dovecot sql config parameters, escape " in db password DBPASS=$(echo ${DBPASS} | sed 's/"/\\"/g') -sed -i "/^connect/c\connect = \"host=mysql dbname=${DBNAME} user=${DBUSER} password=${DBPASS}\"" /etc/dovecot/sql/* + +cat < /etc/dovecot/sql/dovecot-dict-sql.conf +connect = "host=mysql dbname=${DBNAME} user=${DBNAME} password=${DBPASS}" +map { + pattern = priv/quota/storage + table = quota2 + username_field = username + value_field = bytes +} +map { + pattern = priv/quota/messages + table = quota2 + username_field = username + value_field = messages +} +EOF + +cat < /etc/dovecot/sql/dovecot-mysql.conf +driver = mysql +connect = "host=mysql dbname=${DBNAME} user=${DBNAME} password=${DBPASS}" +default_pass_scheme = SSHA256 +password_query = SELECT password FROM mailbox WHERE username = '%u' AND domain IN (SELECT domain FROM domain WHERE domain='%d' AND active='1') +user_query = SELECT CONCAT('maildir:/var/vmail/',maildir) AS mail, 5000 AS uid, 5000 AS gid, concat('*:bytes=', quota) AS quota_rule FROM mailbox WHERE username = '%u' AND active = '1' +iterate_query = SELECT username FROM mailbox WHERE active='1'; +EOF [[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve +[[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo cat /etc/dovecot/sieve_after > /var/vmail/sieve/global.sieve sievec /var/vmail/sieve/global.sieve chown -R vmail:vmail /var/vmail/sieve diff --git a/data/Dockerfiles/dovecot/imapsync b/data/Dockerfiles/dovecot/imapsync new file mode 100755 index 00000000..8df547c6 --- /dev/null +++ b/data/Dockerfiles/dovecot/imapsync @@ -0,0 +1,9488 @@ +#!/usr/bin/perl + +# $Id: imapsync,v 1.727 2016/08/19 10:30:36 gilles Exp gilles $ +# structure +# pod documentation +# pragmas +# main program +# global variables initialisation +# get_options( ) ; +# default values +# folder loop +# subroutines +# sub usage { +# IMAPClient 3.xx ads + +# pod documentation + +=pod + +=head1 NAME + +imapsync - Email IMAP tool for syncing, copying and migrating email mailboxes. + +The imapsync command synchronises mailboxes between two imap servers. +More than 69 different IMAP server softwares supported with success, +few failures. + +$Revision: 1.727 $ + +=head1 SYNOPSIS + + To synchronize the source imap account + "test1" on server "test1.lamiral.info" with password "secret1" + to the destination imap account + "test2" on server "test2.lamiral.info" with password "secret2" + do: + + imapsync \ + --host1 test1.lamiral.info --user1 test1 --password1 secret1 \ + --host2 test2.lamiral.info --user2 test2 --password2 secret2 + +=head1 REQUIRED ARGUMENTS + +The required argmuments are the six values, three on each sides, +needed to login into the IMAP servers, +a host, a username, and a password, two times. + +=head1 INSTALL + + Imapsync works under any Unix with perl. + Imapsync works under Windows (2000, XP, Vista, Seven) + as a standalone binary software called imapsync.exe + Imapsync works under OS X as a standalone binary + software called imapsync_bin_Darwin. + + Purchase latest imapsync at + http://imapsync.lamiral.info/ + + You'll receive a link to a compressed tarball called imapsync-x.xx.tgz + where x.xx is the version number. Untar the tarball where + you want (on Unix): + + tar xzvf imapsync-x.xx.tgz + + Go into the directory imapsync-x.xx and read the INSTALL file. + As mentioned at http://imapsync.lamiral.info/#install + the INSTALL file can also be found at + http://imapsync.lamiral.info/INSTALL + It is now split in several files for each system + http://imapsync.lamiral.info/INSTALL.d/ + +=head1 CONFIGURATION + +There is no specific configuration file for imapsync, +everything is specified by the command line parameteres +and the default behavior. + +=head1 USAGE + +To get a description of each option just run imapsync +with no argument, like this: + + imapsync + +This description of options is also available at +http://imapsync.lamiral.info/OPTIONS and is +reproduced here: + + usage: ./imapsync [options] + + Several options are mandatory. + str means string + int means integer + reg means regular expression + cmd means command + + --dry : Makes imapsync doing nothing, just print what would + be done without --dry. + + --host1 str : Source or "from" imap server. Mandatory. + --port1 int : Port to connect on host1. Default is 143, 993 if --ssl1 + --user1 str : User to login on host1. Mandatory. + --showpasswords : Shows passwords on output instead of "MASKED". + Useful to restart a complete run by just reading the log. + --password1 str : Password for the user1. + --host2 str : "destination" imap server. Mandatory. + --port2 int : Port to connect on host2. Default is 143, 993 if --ssl2 + --user2 str : User to login on host2. Mandatory. + --password2 str : Password for the user2. + + --passfile1 str : Password file for the user1. It must contain the + password on the first line. This option avoids to show + the password on the command line like --password1 does. + --passfile2 str : Password file for the user2. Contains the password. + + --ssl1 : Use a SSL connection on host1. + --ssl2 : Use a SSL connection on host2. + --tls1 : Use a TLS connection on host1. + --tls2 : Use a TLS connection on host2. + --debugssl int : SSL debug mode from 0 to 4. + --sslargs1 str : Pass any ssl parameter for host1 ssl or tls connection. Example: + --sslargs1 SSL_verify_mode=1 --sslargs1 SSL_version=SSLv3 + See all possibilities in the new() method of IO::Socket::SSL + http://search.cpan.org/perldoc?IO::Socket::SSL#Description_Of_Methods + --sslargs2 str : Pass any ssl parameter for host2 ssl or tls connection. + See --sslargs1 + + --timeout1 int : Connection timeout in seconds for host1. + Default is 120 and 0 means no timeout at all. + --timeout2 int : Connection timeout in seconds for host2. + Default is 120 and 0 means no timeout at all. + + --authmech1 str : Auth mechanism to use with host1: + PLAIN, LOGIN, CRAM-MD5 etc. Use UPPERCASE. + --authmech2 str : Auth mechanism to use with host2. See --authmech1 + + --authuser1 str : User to auth with on host1 (admin user). + Avoid using --authmech1 SOMETHING with --authuser1. + --authuser2 str : User to auth with on host2 (admin user). + --proxyauth1 : Use proxyauth on host1. Requires --authuser1. + Required by Sun/iPlanet/Netscape IMAP servers to + be able to use an administrative user. + --proxyauth2 : Use proxyauth on host2. Requires --authuser2. + + --authmd51 : Use MD5 authentification for host1. + --authmd52 : Use MD5 authentification for host2. + --domain1 str : Domain on host1 (NTLM authentication). + --domain2 str : Domain on host2 (NTLM authentication). + + + --folder str : Sync this folder. + --folder str : and this one, etc. + --folderrec str : Sync this folder recursively. + --folderrec str : and this one, etc. + + --folderfirst str : Sync this folder first. --folderfirst "Work" + --folderfirst str : then this one, etc. + --folderlast str : Sync this folder last. --folderlast "[Gmail]/All Mail" + --folderlast str : then this one, etc. + + --nomixfolders : Do not merge folders when host1 is case sensitive + while host2 is not (like Exchange). Only the first + similar folder is synced (ex: Sent SENT sent -> Sent). + + --skipemptyfolders : Empty host1 folders are not created on host2. + + --f1f2 str1=str2 : Force folder str1 to be synced to str2. + --include reg : Sync folders matching this regular expression + --include reg : or this one, etc. + in case both --include --exclude options are + use, include is done before. + --exclude reg : Skips folders matching this regular expression + Several folders to avoid: + --exclude 'fold1|fold2|f3' skips fold1, fold2 and f3. + --exclude reg : or this one, etc. + + --subfolder2 str : Move whole host1 folders hierarchy under this + host2 folder str . + It does it by adding two --regextrans2 options before + all others. Add --debug to see what's really going on. + + --regextrans2 reg : Apply the whole regex to each destination folders. + --regextrans2 reg : and this one. etc. + When you play with the --regextrans2 option, first + add also the safe options --dry --justfolders + Then, when happy, remove --dry, remove --justfolders. + Have in mind that --regextrans2 is applied after prefix + and separator inversion. + + --tmpdir str : Where to store temporary files and subdirectories. + Will be created if it doesn't exist. + Default is system specific, Unix is /tmp but + it's often small and deleted at reboot. + --tmpdir /var/tmp should be better. + --pidfile str : The file where imapsync pid is written. + --pidfilelocking : Abort if pidfile already exists. Usefull to avoid + concurrent transfers on the same mailbox. + + --nolog : Turn off logging on file + --logfile str : Change the default log filename (can be dirname/filename). + --logdir str : Change the default log directory. Default is LOG_imapsync + + --prefix1 str : Remove prefix to all destination folders + (usually INBOX. or INBOX/ or an empty string "") + you have to use --prefix1 if host1 imap server + does not have NAMESPACE capability, so imapsync + suggests to use it. All other cases are bad. + --prefix2 str : Add prefix to all host2 folders. See --prefix1 + --sep1 str : Host1 separator in case NAMESPACE is not supported. + --sep2 str : Host2 separator in case NAMESPACE is not supported. + + --skipmess reg : Skips messages maching the regex. + Example: 'm/[\x80-ff]/' # to avoid 8bits messages. + --skipmess is applied before --regexmess + --skipmess reg : or this one, etc. + + --pipemess cmd : Apply this cmd command to each message content + before the copy. + --pipemess cmd : and this one, etc. + + --disarmreadreceipts : Disarms read receipts (host2 Exchange issue) + + --regexmess reg : Apply the whole regex to each message before transfer. + Example: 's/\000/ /g' # to replace null by space. + --regexmess reg : and this one, etc. + + --regexflag reg : Apply the whole regex to each flags list. + Example: 's/"Junk"//g' # to remove "Junk" flag. + --regexflag reg : and this one, etc. + + --delete : Deletes messages on host1 server after a successful + transfer. Option --delete has the following behavior: + it marks messages as deleted with the IMAP flag + \Deleted, then messages are really deleted with an + EXPUNGE IMAP command. + + --delete2 : Delete messages in host2 that are not in + host1 server. Useful for backup or pre-sync. + --delete2duplicates : Delete messages in host2 that are duplicates. + Works only without --useuid since duplicates are + detected with an header part of each message. + + --delete2folders : Delete folders in host2 that are not in host1 server. + For safety, first try it like this (it is safe): + --delete2folders --dry --justfolders --nofoldersizes + --delete2foldersonly reg : Deleted only folders matching regex. + Example: --delete2foldersonly "/^Junk$|^INBOX.Junk$/" + --delete2foldersbutnot reg : Do not delete folders matching regex. + Example: --delete2foldersbutnot "/Tasks$|Contacts$|Foo$/" + --noexpunge : Do not expunge messages on host1. + Expunge really deletes messages marked deleted. + Expunge is made at the beginning, on host1 only. + Newly transferred messages are also expunged if + option --delete is given. + No expunge is done on host2 account (unless --expunge2) + --expunge1 : Expunge messages on host1 after messages transfer. + --expunge2 : Expunge messages on host2 after messages transfer. + --uidexpunge2 : uidexpunge messages on the host2 account + that are not on the host1 account, requires --delete2 + --nomixfolders : Avoid merging folders that are considered different on + host1 but the same on destination host2 because of + case sensitivities and insensitivities. + + --syncinternaldates : Sets the internal dates on host2 same as host1. + Turned on by default. Internal date is the date + a message arrived on a host (mtime). + --idatefromheader : Sets the internal dates on host2 same as the + "Date:" headers. + + --maxsize int : Skip messages larger (or equal) than int bytes + --minsize int : Skip messages smaller (or equal) than int bytes + --maxage int : Skip messages older than int days. + final stats (skipped) don't count older messages + see also --minage + --minage int : Skip messages newer than int days. + final stats (skipped) don't count newer messages + You can do (+ are the messages selected): + past|----maxage+++++++++++++++>now + past|+++++++++++++++minage---->now + past|----maxage+++++minage---->now (intersection) + past|++++minage-----maxage++++>now (union) + + --search str : Selects only messages returned by this IMAP SEARCH + command. Applied on both sides. + --search1 str : Same as --search for selecting host1 messages only. + --search2 str : Same as --search for selecting host2 messages only. + --search CRIT equals --search1 CRIT --search2 CRIT + + --exitwhenover int : Stop syncing when total bytes transferred reached. + Gmail per day allows + 2500000000 = 2.5 GB downloaded from Gmail as host2 + 500000000 = 500 MB uploaded to Gmail as host1. + + --maxlinelength int : skip messages with a line length longer than int bytes. + RFC 2822 says it must be no more than 1000 bytes. + + --useheader str : Use this header to compare messages on both sides. + Ex: Message-ID or Subject or Date. + --useheader str and this one, etc. + + --subscribed : Transfers subscribed folders. + --subscribe : Subscribe to the folders transferred on the + host2 that are subscribed on host1. On by default. + --subscribeall : Subscribe to the folders transferred on the + host2 even if they are not subscribed on host1. + + --nofoldersizes : Do not calculate the size of each folder in bytes + and message counts. Default is to calculate them. + --nofoldersizesatend: Do not calculate the size of each folder in bytes + and message counts at the end. Default is on. + --justfoldersizes : Exit after having printed the folder sizes. + + --syncacls : Synchronises acls (Access Control Lists). + --nosyncacls : Does not synchronize acls. This is the default. + Acls in IMAP are not standardized, be careful. + + --usecache : Use cache to speedup. + --nousecache : Do not use cache. Caveat: --useuid --nousecache creates + duplicates on multiple runs. + --useuid : Use uid instead of header as a criterium to recognize + messages. Option --usecache is then implied unless + --nousecache is used. + + --debug : Debug mode. + --debugfolders : Debug mode for the folders part only. + --debugcontent : Debug content of the messages transfered. Huge ouput. + --debugflags : Debug mode for flags. + --debugimap1 : IMAP debug mode for host1. Very verbose. + --debugimap2 : IMAP debug mode for host2. Very verbose. + --debugimap : IMAP debug mode for host1 and host2. + --debugmemory : Debug mode showing memory consumption after each copy. + + --errorsmax int : Exit when int number of errors is reached. Default is 50. + + --tests : Run local non-regression tests. Exit code 0 means all ok. + --testslive : Run a live test with test1.lamiral.info imap server. + Useful to check the basics. Needs internet connexion. + + --version : Print only software version. + --noreleasecheck : Do not check for new imapsync release (a http request). + --releasecheck : Check for new imapsync release (a http request). + --noid : Do not send/receive ID command to imap servers. + --justconnect : Just connect to both servers and print useful + information. Need only --host1 and --host2 options. + --justlogin : Just login to both host1 and host2 with users + credentials, then exit. + --justfolders : Do only things about folders (ignore messages). + + --help : print this help. + + Example: + To synchronize the source imap account + "test1" on server "test1.lamiral.info" with password "secret1" + to the destination imap account + "test2" on server "test2.lamiral.info" with password "secret2" + do: + + imapsync \ + --host1 test1.lamiral.info --user1 test1 --password1 secret1 \ + --host2 test2.lamiral.info --user2 test2 --password2 secret2 + +=cut +# comment + +=pod + +=head1 DESCRIPTION + +Imapsync command is a tool allowing incremental and +recursive imap transfers from one mailbox to another. + +By default all folders are transferred, recursively, all +possible flags (\Seen \Answered \Flagged etc.) are synced too. + +We sometimes need to transfer mailboxes from one imap server to +another. This is called migration. + +Imapsync reduces the amount +of data transferred by not transferring a given message +if it resides already on both sides. Same specific headers +and the transfer is done only once; taken into account are by default +Message-Id and Received header lines. +All flags are +preserved, unread will stay unread, read will stay read, +deleted will stay deleted. You can stop the transfer at any +time and restart it later, imapsync works well with bad +connections and interruptions. + +You can decide to delete the messages from the source mailbox +after a successful transfer, it can be a good feature when migrating +live mailboxes since messages will be only on one side. +In that case, use the --delete option. Option --delete implies +also option --expunge so all messages marked deleted on host1 +will be really deleted. +(you can use --noexpunge to avoid this but I don't see any +good real world scenario for the combination --delete --noexpunge). + +A different scenario is synchronizing a mailbox B from another mailbox A +in case you just want to keep a "live" copy of A in B. +In that case --delete2 has to be used, it deletes messages in host2 +folder B that are not in host1 folder A. If you also need to destroy +host2 folders that are not in host1 then use --delete2folders (see also +--delete2foldersonly and --delete2foldersbutnot). + +Imapsync is not adequate for maintaining two active imap accounts +in synchronization when the user plays independently on both sides. +Use offlineimap (written by John Goerzen) or mbsync (written by +Michael R. Elkins) for 2 ways synchronizations. + + +=head1 OPTIONS + +To get a description of each option just invoke: + + imapsync + +or read the previous section named USAGE, + +or read http://imapsync.lamiral.info/OPTIONS + +=head1 HISTORY + +I wrote imapsync because an enterprise (basystemes) paid me to install +a new imap server without losing huge old mailboxes located on a far +away remote imap server accessible by a low bandwidth link. The tool +imapcp (written in python) could not help me because I had to verify +every mailbox was well transferred and delete it after a good +transfer. imapsync started its life as a copy_folder.pl patch. +The tool copy_folder.pl comes from the Mail-IMAPClient-2.1.3 perl +module tarball source (in the examples/ directory of the tarball). + +=head1 EXAMPLE + +While working on imapsync parameters please run imapsync in +dry mode (no modification induced) with the --dry +option. Nothing bad can be done this way. + +To synchronize the imap account "buddy" (with password "secret1") +on host "imap.src.fr" to the imap account "max" (with password "secret2") +on host "imap.dest.fr": + + imapsync --host1 imap.src.fr --user1 buddy --password1 secret1 \ + --host2 imap.dest.fr --user2 max --password2 secret2 + +Then you will have max's mailbox updated from buddy's +mailbox. + +=head1 SECURITY + +You can use --passfile1 instead of --password1 to give the +password since it is safer. With --password1 option any user +on your host can see the password by using the 'ps auxwwww' +command. Using a variable (like $PASSWORD1) is also +dangerous because of the 'ps auxwwwwe' command. So, saving +the password in a well protected file (600 or rw-------) is +the best solution. + +imasync is not totally protected against sniffers on the +network since passwords may be transferred in plain text +if CRAM-MD5 is not supported by your imap servers. Use +--ssl1 (or --tls1) and --ssl2 (or --tls2) to enable +encryption on host1 and host2. + +You may authenticate as one user (typically an admin user), +but be authorized as someone else, which means you don't +need to know every user's personal password. Specify +--authuser1 "adminuser" to enable this on host1. In this +case, --authmech1 PLAIN will be used by default since it +is the only way to go for now. So don't use --authmech1 SOMETHING +with --authuser1 "adminuser", it will not work. +Same behavior with the --authuser2 option. +Authenticate with an admin account must be supported by your +imap server to work with imapsync. + +When working on Sun/iPlanet/Netscape IMAP servers you must use +--proxyauth1 to enable administrative user to masquerade as another user. +Can also be used on destination server with --proxyauth2 + +You can authenticate with OAUTH when transfering from Google Apps. +The consumer key will be the domain part of the --user, and the +--password will be used as the consumer secret. It does not work +with Google Apps free edition. + +=head1 EXIT STATUS + +imapsync will exit with a 0 status (return code) if everything went good. +Otherwise, it exits with a non-zero status. + +So if you have an unreliable internet connection, you can use this loop +in a Bourne shell: + + while ! imapsync ...; do + echo imapsync not complete + done + +=head1 LICENSE AND COPYRIGHT + +imapsync is free, open, public but not always gratis software +cover by the NOLIMIT Public License. +See the LICENSE file included in the distribution or just read this +simple sentence as it is the licence text: + + "No limit to do anything with this work and this license." + +In case it is not long enough I repeat: + + "No limit to do anything with this work and this license." + +=head1 MAILING-LIST + +The public mailing-list may be the best way to get free support. + +To write on the mailing-list, the address is: + + +To subscribe, send any message (even empty) to: + +then just reply to the confirmation message. + +To unsubscribe, send a message to: + + +To contact the person in charge for the list: + + +The list archives are available at: +http://www.linux-france.org/prj/imapsync_list/ +So consider that the list is public, anyone +can see your post. Use a pseudonym or do not +post to this list if you want to stay private. + +Thank you for your participation. + +=head1 AUTHOR + +Gilles LAMIRAL + +Feedback good or bad is very often welcome. + +Gilles LAMIRAL earns his living by writing, installing, +configuring and teaching free, open and often gratis +softwares. It used to be "always gratis" but now it is +"often" because imapsync is sold by its author, a good +way to stay maintening and supporting free open public +softwares (see the license) over decades. + +=head1 BUGS AND LIMITATIONS + +Help me to help you: follow the following guidelines. + +Report any bugs or feature requests to the public mailing-list +or to the author. + +Before reporting bugs, read the FAQs, the README and the +TODO files. http://imapsync.lamiral.info/ + +Upgrade to last imapsync release, maybe the bug +is already fixed. + +Upgrade to last Mail-IMAPClient Perl module. +http://search.cpan.org/dist/Mail-IMAPClient/ +maybe the bug is already fixed there. + +Make a good title with word "imapsync" in it (my spam filters won't filter it), +Try to write an email title with more words than just "imapsync" or "problem", +a good title is made of keywords summary, but not too long (one visible line). + +Help us to help you: in your report, please include: + + - imapsync version. + + - output near the first failures, a few lines before is good to get the context + of the issue. First failures messages are often more significant than + the last ones. + + - if the issue is always related to the same messages, include the output + with --debug --debugimap, near the failure point. For example, + Isolate a buggy message or two in a folder 'BUG' and use + + imapsync ... --folder 'BUG' --debug --debugimap + + - imap server softwares on both sides and their version number. + + - imapsync with all the options you use, the full command line + you use (except the passwords of course). + + - IMAPClient.pm version. + + - the run context. Do you run imapsync.exe, a unix binary + or the perl script imapsync. + + - operating system running imapsync. + + - virtual software context (vmware, xen etc.) + + - operating systems on both sides and the third side in case + you run imapsync on a foreign host from the both. + +Most of those values can be found as a copy/paste at the begining of the output, +so a carbon copy of the output is a very easy and very good debug report for me. + +One time in your life, read the paper +"How To Ask Questions The Smart Way" +http://www.catb.org/~esr/faqs/smart-questions.html +and then forget it. + +=head1 IMAP SERVERS + +See http://imapsync.lamiral.info/S/imapservers.shtml + +=head1 HUGE MIGRATION + +Pay special attention to options +--subscribed +--subscribe +--delete +--delete2 +--delete2folders +--maxage +--minage +--maxsize +--useuid +--usecache + +If you have many mailboxes to migrate think about a little +shell program. Write a file called file.txt (for example) +containing users and passwords. +The separator used in this example is ';' + +The file.txt file contains: + +user001_1;password001_1;user001_2;password001_2 +user002_1;password002_1;user002_2;password002_2 +user003_1;password003_1;user003_2;password003_2 +user004_1;password004_1;user004_2;password004_2 +user005_1;password005_1;user005_2;password005_2 +... + +On Unix the shell program can be: + + { while IFS=';' read u1 p1 u2 p2; do + imapsync --host1 imap.side1.org --user1 "$u1" --password1 "$p1" \ + --host2 imap.side2.org --user2 "$u2" --password2 "$p2" ... + done ; } < file.txt + +On Windows the batch program can be: + + FOR /F "tokens=1,2,3,4 delims=; eol=#" %%G IN (file.txt) DO imapsync ^ + --host1 imap.side1.org --user1 %%G --password1 %%H ^ + --host2 imap.side2.org --user2 %%I --password2 %%J ... + +The ... have to be replaced by nothing or any imapsync option. +Welcome in shell programming ! + +You will find already written scripts at +http://imapsync.lamiral.info/examples/ + + +=head1 HACKING + +Feel free to hack imapsync as the NOLIMIT license permits it. + +=head1 LINKS + +Entries for imapsync: +https://web.archive.org/web/20070202005121/http://www.imap.org/products/showall.php + +=head1 SIMILAR SOFTWARES + + imap_tools : http://www.athensfbc.com/imap_tools + offlineimap : https://github.com/nicolas33/offlineimap + mbsync : http://isync.sourceforge.net/ + mailsync : http://mailsync.sourceforge.net/ + mailutil : http://www.washington.edu/imap/ + part of the UW IMAP tookit. + imaprepl : http://www.bl0rg.net/software/ + http://freecode.com/projects/imap-repl/ + imapcopy : http://home.arcor.de/armin.diehl/imapcopy/imapcopy.html + migrationtool : http://sourceforge.net/projects/migrationtool/ + imapmigrate : http://sourceforge.net/projects/cyrus-utils/ + wonko_imapsync: http://wonko.com/article/554 + see also file W/tools/wonko_ruby_imapsync + exchange-away : http://exchange-away.sourceforge.net/ + pop2imap : http://www.linux-france.org/prj/pop2imap/ + + +Feedback (good or bad) will often be welcome. + +$Id: imapsync,v 1.727 2016/08/19 10:30:36 gilles Exp gilles $ + +=cut + + +# pragmas + +use strict ; +use warnings ; +++$| ; + +use Carp ; +use Data::Dumper ; +use Digest::HMAC_SHA1 qw( hmac_sha1 ) ; +use Digest::MD5 qw( md5 md5_hex md5_base64 ) ; +use English qw( -no_match_vars ) ; +use Errno qw(EAGAIN EPIPE ECONNRESET) ; +use Fcntl ; +use File::Basename ; +use File::Copy::Recursive ; +use File::Glob qw( :glob ) ; +use File::Path qw( mkpath rmtree ) ; +use File::Spec ; +use File::stat ; +#use Imapsync::Getopt::Long ; +use IO::File ; +use IO::Socket qw(:crlf SOL_SOCKET SO_KEEPALIVE) ; +#use IO::Socket::SSL ; +use IO::Tee ; +use IPC::Open3 'open3' ; +use Mail::IMAPClient 3.30 ; +use MIME::Base64 ; +use POSIX qw(uname SIGALRM) ; +use Term::ReadKey ; +use Test::More ; +use Time::HiRes qw( time sleep ) ; +use Time::Local ; +use Unicode::String ; +use Cwd ; +use Readonly ; + +# constants + +# Let us do like sysexits.h +# /usr/include/sysexits.h + +Readonly my $EX_OK => 0 ; #/* successful termination */ +Readonly my $EX_USAGE => 64 ; #/* command line usage error */ +#Readonly my $EX_DATAERR => 65 ; #/* data format error */ +#Readonly my $EX_NOINPUT => 66 ; #/* cannot open input */ +#Readonly my $EX_NOUSER => 67 ; #/* addressee unknown */ +#Readonly my $EX_NOHOST => 68 ; #/* host name unknown */ +#Readonly my $EX_UNAVAILABLE => 69 ; #/* service unavailable */ +Readonly my $EX_SOFTWARE => 70 ; #/* internal software error */ +#Readonly my $EX_OSERR => 71 ; #/* system error (e.g., can't fork) */ +#Readonly my $EX_OSFILE => 72 ; #/* critical OS file missing */ +#Readonly my $EX_CANTCREAT => 73 ; #/* can't create (user) output file */ +#Readonly my $EX_IOERR => 74 ; #/* input/output error */ +#Readonly my $EX_TEMPFAIL => 75 ; #/* temp failure; user is invited to retry */ +#Readonly my $EX_PROTOCOL => 76 ; #/* remote error in protocol */ +#Readonly my $EX_NOPERM => 77 ; #/* permission denied */ +#Readonly my $EX_CONFIG => 78 ; #/* configuration error */ + +# Mine +Readonly my $EXIT_BY_SIGNAL => 6 ; +Readonly my $EXIT_PID_FILE_ALREADY_EXIST => 8 ; +Readonly my $EXIT_WITH_ERRORS => 111 ; +Readonly my $EXIT_WITH_ERRORS_MAX => 112 ; +Readonly my $EXIT_UNKNOWN => 126 ; + +Readonly my $ERRORS_MAX => 50 ; # exit after 50 errors. + + +Readonly my $INTERVAL_TO_EXIT => 2 ; # interval max to exit instead of reconnect + +Readonly my $SPLIT => 100 ; # By default, 100 at a time, not more. +Readonly my $SPLIT_FACTOR => 10 ; # init_imap() calls Maxcommandlength( $SPLIT_FACTOR * $split ) + # which means default Maxcommandlength is 10*100 = 1000 characters ; + +Readonly my $IMAP_PORT => 143 ; # Well know port for IMAP +Readonly my $IMAP_SSL_PORT => 993 ; # Well know port for IMAP over SSL + +Readonly my $LAST => -1 ; +Readonly my $MINUS_ONE => -1 ; + +Readonly my $RELEASE_NUMBER_EXAMPLE_1 => '1.351' ; +Readonly my $RELEASE_NUMBER_EXAMPLE_2 => 42.4242 ; + + +Readonly my $DEFAULT_TIMEOUT => 120 ; +Readonly my $DEFAULT_NB_RECONNECT_PER_IMAP_COMMAND => 3 ; +Readonly my $DEFAULT_UIDNEXT => 999999 ; +Readonly my $DEFAULT_BUFFER_SIZE => 4096 ; + +Readonly my $DEFAULT_EXPIRATION_TIME_OAUTH2_PK12 => 3600 ; + +Readonly my $PERMISSION_FILTER => 7777 ; + +Readonly my $KIBI => 1024 ; + +Readonly my $NUMBER_10 => 10 ; +Readonly my $NUMBER_42 => 42 ; +Readonly my $NUMBER_100 => 100 ; +Readonly my $NUMBER_200 => 200 ; +Readonly my $NUMBER_300 => 300 ; + +Readonly my $NUMBER_20_000 => 20_000 ; + +Readonly my $QUOTA_PERCENT_LIMIT => 90 ; + +Readonly my $NUMBER_104857600 => 104857600 ; + +Readonly my $SIZE_MAX_STR => 64 ; + +Readonly my $NB_SECONDS_IN_A_DAY => 86400 ; + +Readonly my $STD_CHAR_PER_LINE => 80 ; + +Readonly my $TRUE => 1 ; +Readonly my $FALSE => 0 ; + +Readonly my $LAST_RESSORT_SEPARATOR => q{/} ; + +# global variables + +my( + $sync, + $rcs, + $debug, $debugimap, $debugimap1, $debugimap2, $debugcontent, $debugflags, + $debuglist, $debugdev, $debugmaxlinelength, @debugbasket, $debugcgi, + $host1, $host2, $port1, $port2, + $user1, $user2, $domain1, $domain2, + $password1, $password2, $passfile1, $passfile2, + @folder, @include, @exclude, @folderrec, + @folderfirst, @folderlast, + $prefix1, $prefix2, + $subfolder2, + @regextrans2, @regexmess, @regexflag, @skipmess, @pipemess, $pipemesscheck, + $flagscase, $filterflags, $syncflagsaftercopy, + $sep1, $sep2, + $syncinternaldates, + $idatefromheader, + $syncacls, + $fastio1, $fastio2, + $maxsize, $minsize, $maxage, $minage, + $exitwhenover, + $search, $search1, $search2, + $skipheader, @useheader, + $skipsize, $allowsizemismatch, $foldersizes, $foldersizesatend, $buffersize, + $delete, $delete2, $delete2duplicates, + $expunge, $expunge1, $expunge2, $uidexpunge2, $dry, + $justfoldersizes, + $authmd5, $authmd51, $authmd52, + $subscribed, $subscribe, $subscribeall, + $version, $help, + $justconnect, $justfolders, $justbanner, + $fast, + + $total_bytes_transferred, + $total_bytes_skipped, + $total_bytes_error, + $nb_msg_transferred, + $nb_msg_skipped, + $nb_msg_skipped_dry_mode, + $h1_nb_msg_duplicate, + $h2_nb_msg_duplicate, + $h1_nb_msg_noheader, + $h2_nb_msg_noheader, + $h1_total_bytes_duplicate, + $h2_total_bytes_duplicate, + $h1_nb_msg_deleted, + $h2_nb_msg_deleted, + + $h1_bytes_processed, + $h1_nb_msg_processed, + $h1_nb_msg_start, $h1_bytes_start, + $h2_nb_msg_start, $h2_bytes_start, + $h1_nb_msg_end, $h1_bytes_end, + $h2_nb_msg_end, $h2_bytes_end, + + $timeout, + $timestart_int, $timeend, + $timebefore, + $ssl1, $ssl2, + $ssl1_ssl_version, $ssl2_ssl_version, + $tls1, $tls2, + $uid1, $uid2, + $authuser1, $authuser2, + $proxyauth1, $proxyauth2, + $authmech1, $authmech2, + $split1, $split2, + $reconnectretry1, $reconnectretry2, + $tests, $test_builder, $testsdebug, $testslive, + $justlogin, + $tmpdir, + $releasecheck, + $max_msg_size_in_bytes, + $modulesversion, + $delete2folders, $delete2foldersonly, $delete2foldersbutnot, + $usecache, $debugcache, $cacheaftercopy, + $wholeheaderifneeded, %h1_msgs_copy_by_uid, $useuid, $h2_uidguess, + $addheader, + %h1, %h2, + $checkselectable, $checkmessageexists, + $expungeaftereach, + $abletosearch, + $showpasswords, + $fixslash2, + $messageidnodomain, + $fixInboxINBOX, + $maxlinelength, $maxlinelengthcmd, + $minmaxlinelength, + $uidnext_default, + $fixcolonbug, + $create_folder_old, + $maxmessagespersecond, + $maxbytespersecond, + $skipcrossduplicates, $debugcrossduplicates, + $disarmreadreceipts, + $mixfolders, $skipemptyfolders, + $fetch_hash_set, +); + +# main program + +# global variables initialisation + +$rcs = q{$Id: imapsync,v 1.727 2016/08/19 10:30:36 gilles Exp gilles $} ; + +$total_bytes_transferred = 0; +$total_bytes_skipped = 0; +$total_bytes_error = 0; +$nb_msg_transferred = 0; +$nb_msg_skipped = $nb_msg_skipped_dry_mode = 0; +$h1_nb_msg_deleted = $h2_nb_msg_deleted = 0; +$h1_nb_msg_duplicate = $h2_nb_msg_duplicate = 0; +$h1_nb_msg_noheader = $h2_nb_msg_noheader = 0; +$h1_total_bytes_duplicate = $h2_total_bytes_duplicate = 0; + + +$h1_nb_msg_start = $h1_bytes_start = 0 ; +$h2_nb_msg_start = $h2_bytes_start = 0 ; +$h1_nb_msg_processed = $h1_bytes_processed = 0 ; + +#$h1_nb_msg_end = $h1_bytes_end = 0 ; +#$h2_nb_msg_end = $h2_bytes_end = 0 ; + +$sync->{nb_errors} = 0; +$max_msg_size_in_bytes = 0; + +my %month_abrev = ( + Jan => '00', + Feb => '01', + Mar => '02', + Apr => '03', + May => '04', + Jun => '05', + Jul => '06', + Aug => '07', + Sep => '08', + Oct => '09', + Nov => '10', + Dec => '11', +); + + + + +# @ARGV will be eat by get_options() +my @argv_copy = @ARGV; + +my $cgi_dir = '/var/tmp/imapsync_cgi' ; + +# Under CGI environment +if ( $ENV{SERVER_SOFTWARE} ) { + myprint( "\n" ) ; + myprint( "
\n" ) ;
+        -d $cgi_dir or mkpath $cgi_dir or die "Can not create $cgi_dir: $!\n" ;
+        chdir  $cgi_dir or die "Can not cd to $cgi_dir: $!\n" ;
+}
+
+get_options(  ) ;
+unsetunsafe(  ) if ( $ENV{SERVER_SOFTWARE} ) ;
+
+# Under CGI environment
+if ( $ENV{SERVER_SOFTWARE} ) {
+        myprint( 'Current directory is ' . getcwd(  ) . "\n" ) ;
+        myprint( 'Real user id is ' . getpwuid_any_os( $REAL_USER_ID ) . " (uid $REAL_USER_ID)\n" ) ;
+        myprint( 'Effective user id is ' . getpwuid_any_os( $EFFECTIVE_USER_ID ). " (euid $EFFECTIVE_USER_ID)\n" ) ;
+}
+
+local $SIG{ INT } = sub {
+        my $signame = shift ;
+        catch_reconnect( $sync, $signame ) ;
+} ;
+
+local $SIG{ QUIT } = local $SIG{ TERM } = sub {
+	my $signame = shift ;
+        catch_exit( $sync, $signame ) ;
+} ;
+
+
+$sync->{timestart} = $BASETIME ; # Never too let reading books and perlvar
+
+$sync->{log}        = defined $sync->{log}        ? $sync->{log}        :  1 ;
+$sync->{errorsdump} = defined $sync->{errorsdump} ? $sync->{errorsdump} :  1 ;
+$sync->{errorsmax}  = defined $sync->{errorsmax}  ? $sync->{errorsmax}  : $ERRORS_MAX ;
+
+$sync->{user2} = $user2 ;
+
+if ( $sync->{log} ) {
+        setlogfile( $sync ) ;
+        teelaunch( $sync ) ;
+}
+
+$timestart_int = int( $sync->{timestart} ) ;
+$timebefore =    $sync->{timestart} ;
+
+my $timestart_str = localtime( $sync->{timestart} ) ;
+myprint( "Transfer started at $timestart_str\n" ) ;
+myprint( "PID is $PROCESS_ID\n" ) ;
+myprint( "Log file is $sync->{logfile} ( to change it, use --logfile path ; or use --nolog to turn off logging )\n" ) if ( $sync->{log} ) ;
+$modulesversion = defined  $modulesversion  ? $modulesversion : 1 ;
+
+# If you want releasecheck not to be done by default (like the github maintainer),
+# then uncomment the first "$releasecheck =" line, the line ending with "0 ;".
+# The second line (ending with "1 ;") can stay active or be commented,
+# the result will be the same: no releasecheck by default.
+
+$releasecheck = defined  $releasecheck  ? $releasecheck : 0 ;
+#$releasecheck = defined  $releasecheck  ? $releasecheck : 1 ;
+
+my $warn_release = ( $releasecheck ) ? check_last_release(  ) : q{} ;
+
+# default values
+
+$sync->{pidfile} =  defined  $sync->{pidfile}  ? $sync->{pidfile} : $tmpdir . '/imapsync.pid' ;
+
+$sync->{pidfilelocking} = defined  $sync->{pidfilelocking}  ? $sync->{pidfilelocking} : 0 ;
+
+$wholeheaderifneeded  = defined  $wholeheaderifneeded   ? $wholeheaderifneeded  : 1;
+
+# turn on RFC standard flags correction like \SEEN -> \Seen
+$flagscase = defined  $flagscase  ? $flagscase : 1 ;
+
+# Use PERMANENTFLAGS if available
+$filterflags = defined  $filterflags  ? $filterflags : 1 ;
+
+# sync flags just after an APPEND, some servers ignore the flags given in the APPEND
+# like MailEnable IMAP server.
+# Off by default since it takes time.
+$syncflagsaftercopy = defined  $syncflagsaftercopy   ? $syncflagsaftercopy : 0 ;
+
+
+# Activate --usecache if --useuid is set and no --nousecache
+$usecache = 1 if ( $useuid and ( ! defined  $usecache   ) ) ;
+$cacheaftercopy = 1 if ( $usecache and ( ! defined  $cacheaftercopy  ) ) ;
+
+$checkselectable    = defined  $checkselectable  ? $checkselectable : 1 ;
+$checkmessageexists = defined  $checkmessageexists  ? $checkmessageexists : 0 ;
+$expungeaftereach   = defined  $expungeaftereach  ? $expungeaftereach : 1 ;
+$abletosearch       = defined  $abletosearch  ? $abletosearch : 1 ;
+$checkmessageexists = 0 if ( not $abletosearch ) ;
+$showpasswords      = defined  $showpasswords  ? $showpasswords : 0 ;
+$fixslash2          = defined  $fixslash2  ? $fixslash2 : 1 ;
+$fixInboxINBOX      = defined  $fixInboxINBOX  ? $fixInboxINBOX : 1 ;
+$create_folder_old  = defined  $create_folder_old  ? $create_folder_old : 0 ;
+$mixfolders         = defined  $mixfolders  ? $mixfolders : 1 ;
+$sync->{automap}    = defined  $sync->{automap}  ? $sync->{automap} : 0 ;
+
+$delete2duplicates = 1 if ( $delete2 and ( ! defined  $delete2duplicates  ) ) ;
+
+$maxmessagespersecond = defined  $maxmessagespersecond  ? $maxmessagespersecond : 0 ;
+$maxbytespersecond    = defined  $maxbytespersecond     ? $maxbytespersecond    : 0 ;
+
+myprint( banner_imapsync( @argv_copy ) ) ;
+
+myprint( "Temp directory is $tmpdir  ( to change it use --tmpdir dirpath )\n") ;
+
+is_valid_directory( $tmpdir ) || croak "Error creating tmpdir $tmpdir : $!" ;
+
+if ( $sync->{pidfile} ) {
+        write_pidfile( $sync->{pidfile}, $sync->{pidfilelocking} ) ;
+}
+
+$fixcolonbug = defined  $fixcolonbug  ? $fixcolonbug : 1 ;
+
+if ( $usecache and $fixcolonbug ) { tmpdir_fix_colon_bug(  ) } ;
+
+$modulesversion and myprint( "Modules version list:\n", modulesversion(), "( use --no-modulesversion to turn off printing this Perl modules list )\n" ) ;
+
+my $DEFAULT_SSL_VERIFY ;
+my %SSL_VERIFY_STR ;
+
+if ( $ssl1 or $ssl2 or $tls1 or $tls2) {
+        Readonly $DEFAULT_SSL_VERIFY => IO::Socket::SSL::SSL_VERIFY_NONE(  ) ;
+        Readonly %SSL_VERIFY_STR => (
+                IO::Socket::SSL::SSL_VERIFY_NONE(  ) => 'SSL_VERIFY_NONE' ,
+                IO::Socket::SSL::SSL_VERIFY_PEER(  ) => 'SSL_VERIFY_PEER' ,
+        ) ;
+        $IO::Socket::SSL::DEBUG = $sync->{debugssl} || 1 ;
+        myprint( "SSL debug mode level is --debugssl $IO::Socket::SSL::DEBUG (can be set from 0 meaning no debug to 4 meaning max debug)\n" ) ;
+}
+
+if ( $ssl1 ) {
+        myprint( 'Host1: SSL default mode is like --sslargs1 SSL_verify_mode=' . $DEFAULT_SSL_VERIFY . " meaning $SSL_VERIFY_STR{$DEFAULT_SSL_VERIFY} on host1 (do not check the certificate server)\n" ) ;
+        myprint( 'Host1: Use --sslargs1 SSL_verify_mode=' . IO::Socket::SSL::SSL_VERIFY_PEER(  ) . " for $SSL_VERIFY_STR{IO::Socket::SSL::SSL_VERIFY_PEER(  )} on host1\n" ) ;
+}
+if ( $ssl2 ) {
+        myprint( 'Host2: SSL default mode is like --sslargs2 SSL_verify_mode=' . $DEFAULT_SSL_VERIFY . " meaning $SSL_VERIFY_STR{$DEFAULT_SSL_VERIFY} on host2 (do not check the certificate server)\n" ) ;
+        myprint( 'Host2: Use --sslargs2 SSL_verify_mode=' . IO::Socket::SSL::SSL_VERIFY_PEER(  ) . " for $SSL_VERIFY_STR{IO::Socket::SSL::SSL_VERIFY_PEER(  )} on host2\n" ) ;
+}
+
+
+check_lib_version(  ) or
+  croak "imapsync needs perl lib Mail::IMAPClient release 3.30 or superior.\n";
+
+exit_clean( $sync, $EX_OK ) if ( $justbanner ) ;
+
+
+$split1 ||= $SPLIT ;
+$split2 ||= $SPLIT ;
+
+$host1 || missing_option( '--host1' ) ;
+$port1 ||= ( $ssl1 ) ? $IMAP_SSL_PORT : $IMAP_PORT ;
+
+$host2 || missing_option( '--host2' ) ;
+$port2 ||= ( $ssl2 ) ? $IMAP_SSL_PORT : $IMAP_PORT ;
+
+$debugimap1 = $debugimap2 = 1 if ( $debugimap ) ;
+$debug = 1 if ( $debugimap1 or $debugimap2 ) ;
+
+# By default, don't take size to compare
+$skipsize = (defined $skipsize) ? $skipsize : 1;
+
+$uid1 = defined $uid1 ? $uid1 : 1;
+$uid2 = defined $uid2 ? $uid2 : 1;
+
+$subscribe = defined $subscribe ? $subscribe : 1;
+
+# Allow size mismatch by default
+$allowsizemismatch = defined $allowsizemismatch ? $allowsizemismatch : 1;
+
+$delete2folders = 1
+    if ( defined  $delete2foldersbutnot  or defined  $delete2foldersonly  ) ;
+
+if ( $justconnect ) {
+	justconnect(  ) ;
+	exit_clean( $sync, $EX_OK ) ;
+}
+
+$user1 || missing_option( '--user1' ) ;
+$user2 || missing_option( '--user2' ) ;
+
+$syncinternaldates = defined $syncinternaldates ? $syncinternaldates : 1;
+
+# Turn on expunge if there is not explicit option --noexpunge and option
+# --delete is given.
+# Done because --delete --noexpunge is very dangerous on the second run:
+# the Deleted flag is then synced to all previously transfered messages.
+# So --delete implies --expunge is a better usability default behaviour.
+if ( $delete ) {
+	if ( ! defined  $expunge  ) {
+		myprint( "Info: turning on --expunge1 because --delete --noexpunge1 is very dangerous on the second run.\n" ) ;
+		$expunge = 1 ;
+	}
+		myprint( "Info: if expunging after each message slows down too much the sync then use --noexpungeaftereach to speed up\n" ) ;
+}
+
+if ( $uidexpunge2 and not Mail::IMAPClient->can( 'uidexpunge' ) ) {
+        myprint( "Failure: uidexpunge not supported (IMAPClient release < 3.17), use --expunge2 instead\n" ) ;
+        exit_clean( $sync, $EX_SOFTWARE ) ;
+}
+
+if ( ( $delete2 or $delete2duplicates ) and not defined  $uidexpunge2  ) {
+        if ( Mail::IMAPClient->can( 'uidexpunge' ) ) {
+                myprint( "Info: will act as --uidexpunge2\n" ) ;
+		$uidexpunge2 = 1 ;
+        }elsif ( not defined  $expunge2  ) {
+                 myprint( "Info: will act as --expunge2 (no uidexpunge support)\n" ) ;
+                $expunge2 = 1 ;
+        }
+}
+
+if ( $delete and $delete2 ) {
+	myprint( "Warning: using --delete and --delete2 together is almost always a bad idea, exiting imapsync\n" ) ;
+	exit_clean( $sync, $EX_USAGE ) ;
+}
+
+if ( $idatefromheader ) {
+	myprint( 'Turned ON idatefromheader, ',
+	      "will set the internal dates on host2 from the 'Date:' header line.\n" ) ;
+	$syncinternaldates = 0 ;
+}
+
+if ( $syncinternaldates ) {
+	myprint( 'Info: turned ON syncinternaldates, ',
+	      "will set the internal dates (arrival dates) on host2 same as host1.\n" ) ;
+}else{
+        myprint( "Info: turned OFF syncinternaldates\n" ) ;
+}
+
+if ( defined $authmd5 and $authmd5 ) {
+	$authmd51 = 1 ;
+	$authmd52 = 1 ;
+}
+
+if ( defined $authmd51 and $authmd51 ) {
+	$authmech1 ||= 'CRAM-MD5';
+}
+else{
+	$authmech1 ||= $authuser1 ? 'PLAIN' : 'LOGIN';
+}
+
+if ( defined $authmd52 and $authmd52 ) {
+	$authmech2 ||= 'CRAM-MD5';
+}
+else{
+	$authmech2 ||= $authuser2 ? 'PLAIN' : 'LOGIN';
+}
+
+$authmech1 = uc $authmech1;
+$authmech2 = uc $authmech2;
+
+if (defined $proxyauth1 && !$authuser1) {
+        missing_option( 'With --proxyauth1, --authuser1' ) ;
+}
+
+if (defined $proxyauth2 && !$authuser2) {
+        missing_option( 'With --proxyauth2, --authuser2' ) ;
+}
+
+$authuser1 ||= $user1;
+$authuser2 ||= $user2;
+
+myprint( "Host1: will try to use $authmech1 authentication on host1\n") ;
+myprint( "Host2: will try to use $authmech2 authentication on host2\n") ;
+
+$timeout = defined  $timeout  ? $timeout : $DEFAULT_TIMEOUT ;
+
+$sync->{h1}->{timeout} = defined  $sync->{h1}->{timeout}  ? $sync->{h1}->{timeout} : $timeout ;
+myprint( "Host1: imap connexion timeout is $sync->{h1}->{timeout} seconds\n") ;
+$sync->{h2}->{timeout} = defined  $sync->{h2}->{timeout}  ? $sync->{h2}->{timeout} : $timeout ;
+myprint( "Host2: imap connexion timeout is $sync->{h2}->{timeout} seconds\n" ) ;
+
+$syncacls = defined  $syncacls  ? $syncacls : 0 ;
+
+# No folders sizes if --justfolders, unless really wanted.
+if ( $justfolders and not defined  $foldersizes  ) { $foldersizes = 0 ; }
+
+$foldersizes      = ( defined  $foldersizes       ) ? $foldersizes      : 1 ;
+$foldersizesatend = ( defined  $foldersizesatend  ) ? $foldersizesatend : $foldersizes ;
+
+$fastio1 = defined  $fastio1  ? $fastio1 : 0 ;
+$fastio2 = defined  $fastio2  ? $fastio2 : 0 ;
+
+$reconnectretry1 = defined  $reconnectretry1  ? $reconnectretry1 : $DEFAULT_NB_RECONNECT_PER_IMAP_COMMAND ;
+$reconnectretry2 = defined  $reconnectretry2  ? $reconnectretry2 : $DEFAULT_NB_RECONNECT_PER_IMAP_COMMAND ;
+
+# Since select_msgs() returns no messages when uidnext does not return something
+# then $uidnext_default is never used. So I have to remove it.
+$uidnext_default = $DEFAULT_UIDNEXT ;
+
+@useheader = qw( Message-Id Received ) unless ( @useheader ) ;
+
+my %useheader ;
+
+# Make a hash %useheader of each --useheader 'key' in uppercase
+for ( @useheader ) { $useheader{ uc  $_  } = undef } ;
+
+#myprint( Data::Dumper->Dump( [ \%useheader ] )  ) ;
+#exit ;
+
+myprint( "Host1: IMAP server [$host1] port [$port1] user [$user1]\n" ) ;
+myprint( "Host2: IMAP server [$host2] port [$port2] user [$user2]\n" ) ;
+
+$password1 || $passfile1 || 'PREAUTH' eq $authmech1 || 'EXTERNAL' eq $authmech1 || do {
+	myprint( << 'FIN_PASSFILE'  ) ;
+
+If you are afraid of giving password on the command line arguments, you can put the
+password of user1 in a file named file1 and use "--passfile1 file1" instead of typing it.
+Then give this file restrictive permissions with the command "chmod 600 file1".
+FIN_PASSFILE
+
+	$password1 = ask_for_password( $authuser1 || $user1, $host1 ) ;
+} ;
+
+$password1 = ( defined  $passfile1  ) ? firstline ( $passfile1 ) : $password1 ;
+
+
+$password2 || $passfile2 || 'PREAUTH' eq $authmech2 || 'EXTERNAL' eq $authmech2 || do {
+	myprint( << 'FIN_PASSFILE'  ) ;
+
+If you are afraid of giving password on the command line arguments, you can put the
+password of user2 in a file named file2 and use "--passfile2 file2" instead of typing it.
+Then give this file restrictive permissions with the command "chmod 600 file2".
+FIN_PASSFILE
+
+	$password2 = ask_for_password( $authuser2 || $user2, $host2 ) ;
+} ;
+
+$password2 = ( defined  $passfile2  ) ? firstline ( $passfile2 ) : $password2 ;
+
+
+# need clean up => write methods dry() and dry_message()
+$sync->{dry} = $dry ;
+my $dry_message = q{} ;
+if( $sync->{dry} ) {
+        $dry_message = "\t(not really since --dry mode)" ;
+}
+$sync->{dry_message} = $dry_message ;
+
+
+$search1 ||= $search if ( $search ) ;
+$search2 ||= $search if ( $search ) ;
+
+
+
+if ( $disarmreadreceipts ) {
+	push @regexmess, q{s{\A((?:[^\n]+\r\n)+|)(^Disposition-Notification-To:[^\n]*\n)(\r?\n|.*\n\r?\n)}{$1X-$2$3}ims} ;
+}
+
+$pipemesscheck = ( defined  $pipemesscheck  ) ? $pipemesscheck : 1 ;
+
+if ( @pipemess and $pipemesscheck ) {
+	myprint( 'Checking each --pipemess command, ' 
+                . join( q{, }, @pipemess ) 
+                . ", with an space string. ( Can avoid this check with --nopipemesscheck )\n" ) ;
+	my $string = pipemess( q{ }, @pipemess ) ;
+        # string undef means something was bad.
+        if ( not ( defined  $string  ) ) {
+        	die_clean( "Error: one of --pipemess command is bad, check it\n" ) ;
+        }
+	myprint( "Ok with each --pipemess @pipemess\n"  ) ;
+}
+
+if ( $maxlinelengthcmd ) {
+	myprint( "Checking  --maxlinelengthcmd command,  $maxlinelengthcmd, with an space string.\n"  ) ;
+	my $string = pipemess( q{ }, $maxlinelengthcmd ) ;
+        # string undef means something was bad.
+        if ( not ( defined  $string  ) ) {
+        	die_clean( "Error: --maxlinelengthcmd command is bad, check it\n" ) ;
+        }
+	myprint( "Ok with --maxlinelengthcmd $maxlinelengthcmd\n"  ) ;
+}
+
+if ( @regexmess ) {
+	my $string = regexmess( q{ } ) ;
+	myprint( "Checking each --regexmess command with an space string.\n"  ) ;
+        # string undef means one of the eval regex was bad.
+        if ( not ( defined  $string  ) ) {
+        	die_clean( 'Error: one of --regexmess option is bad, check it' ) ;
+        }
+	myprint( "Ok with each --regexmess\n"  ) ;
+}
+
+if ( @skipmess ) {
+	myprint( "Checking each --skipmess command with an space string.\n"  ) ;
+	my $match = skipmess( q{ } ) ;
+        # match undef means one of the eval regex was bad.
+        if ( not ( defined  $match  ) ) {
+        	die_clean( 'Error: one of --skipmess option is bad, check it' ) ;
+        }
+	myprint( "Ok with each --skipmess\n"  ) ;
+}
+
+if ( @regexflag ) {
+	myprint( "Checking each --regexflag command with an space string.\n"  ) ;
+	my $string = flags_regex( q{ } ) ;
+	# string undef means one of the eval regex was bad.
+	if ( not ( defined  $string  ) ) {
+		die_clean( 'Error: one of --regexflag option is bad, check it' ) ;
+	}
+	myprint( "Ok with each --regexflag\n"  ) ;
+}
+
+$sync->{imap1} = my $imap1 = login_imap($host1, $port1, $user1, $domain1, $password1,
+		   $debugimap1, $sync->{h1}->{timeout}, $fastio1, $ssl1, $tls1,
+		   $authmech1, $authuser1, $reconnectretry1,
+		   $proxyauth1, $uid1, $split1, 'Host1', $sync->{h1} ) ;
+
+$sync->{imap2} = my $imap2 = login_imap($host2, $port2, $user2, $domain2, $password2,
+		 $debugimap2, $sync->{h2}->{timeout}, $fastio2, $ssl2, $tls2,
+		 $authmech2, $authuser2, $reconnectretry2,
+		 $proxyauth2, $uid2, $split2, 'Host2', $sync->{h2} ) ;
+
+
+$debug and myprint( 'Host1 Buffer I/O: ', $imap1->Buffer(), "\n" ) ;
+$debug and myprint( 'Host2 Buffer I/O: ', $imap2->Buffer(), "\n" ) ;
+
+
+die_clean( 'Not authenticated on host1' ) unless $imap1->IsAuthenticated( ) ;
+myprint( "Host1: state Authenticated\n" ) ;
+die_clean( 'Not authenticated on host2' ) unless   $imap2->IsAuthenticated( ) ;
+myprint( "Host2: state Authenticated\n" ) ;
+
+myprint( 'Host1 capability: ', join(q{ }, @{ $imap1->capability_update() || [] }), "\n" ) ;
+myprint( 'Host2 capability: ', join(q{ }, @{ $imap2->capability_update() || [] }), "\n" ) ;
+
+imap_id_stuff( $sync ) ;
+
+#quota( $imap1, 'host1' ) ; # quota on host1 is useless and pollute host2 output.
+quota( $imap2, 'host2', $sync ) ;
+
+if ( $justlogin ) {
+	$imap1->logout(  ) ;
+	$imap2->logout(  ) ;
+	exit_clean( $sync, $EX_OK ) ;
+}
+
+
+#
+# Folder stuff
+#
+
+my (
+        @h1_folders_all , %h1_folders_all , @h1_folders_wanted , %requested_folder ,
+        %h1_subscribed_folder , %h2_subscribed_folder ,
+        @h2_folders_all , %h2_folders_all , %h2_folders_all_UPPER ,
+        @h2_folders_from_1_wanted , %h2_folders_from_1_wanted ,
+        %h2_folders_from_1_several ,
+        %h2_folders_from_1_all ,
+) ;
+
+my $h1_folders_wanted_nb = 0 ; 
+my $h1_folders_wanted_ct = 0 ; # counter of folders done.
+
+# All folders on host1 and host2
+
+@h1_folders_all = sort $imap1->folders(  ) ;
+@h2_folders_all = sort $imap2->folders(  ) ;
+
+myprint( 'Host1: found ', scalar  @h1_folders_all , " folders.\n"  ) ;
+myprint( 'Host2: found ', scalar  @h2_folders_all , " folders.\n"  ) ;
+
+for ( @h1_folders_all ) { $h1_folders_all{ $_ } = 1 } ;
+for ( @h2_folders_all ) {
+	$h2_folders_all{ $_ } = 1 ;
+	$h2_folders_all_UPPER{ uc  $_  } = 1 ;
+} ;
+
+$sync->{h1_folders_all} = \%h1_folders_all ;
+$sync->{h2_folders_all} = \%h2_folders_all ;
+$sync->{h2_folders_all_UPPER} = \%h2_folders_all_UPPER ;
+
+# Make a hash of subscribed folders in both servers.
+
+for ( $imap1->subscribed(  ) ) { $h1_subscribed_folder{ $_ } = 1 } ;
+for ( $imap2->subscribed(  ) ) { $h2_subscribed_folder{ $_ } = 1 } ;
+
+
+if ( defined  $subfolder2  ) {
+	unshift @regextrans2,
+		q's,^${h2_prefix}(.*),${h2_prefix}${subfolder2}${h2_sep}$1,',
+		q's,^INBOX$,${h2_prefix}${subfolder2}${h2_sep}INBOX,' ;
+
+}
+
+if ( $fixInboxINBOX and ( my $reg = fix_Inbox_INBOX_mapping( \%h1_folders_all, \%h2_folders_all ) ) ) {
+	push @regextrans2, $reg ;
+}
+
+if (scalar @folder or $subscribed or scalar @folderrec) {
+	# folders given by option --folder
+	if (scalar @folder) {
+		add_to_requested_folders(@folder);
+	}
+
+	# option --subscribed
+	if ( $subscribed ) {
+		add_to_requested_folders( keys  %h1_subscribed_folder  ) ;
+	}
+
+	# option --folderrec
+	if (scalar @folderrec) {
+		foreach my $folderrec (@folderrec) {
+			add_to_requested_folders($imap1->folders($folderrec));
+		}
+	}
+}
+else {
+	# no include, no folder/subscribed/folderrec options => all folders
+	if (not scalar @include) {
+		myprint( "Including all folders found by default. Use --subscribed or --folder or --folderrec or --include to select specific folders. Use --exclude to unselect specific folders.\n"  ) ;
+		add_to_requested_folders(@h1_folders_all);
+	}
+}
+
+
+# consider (optional) includes and excludes
+if ( scalar  @include  ) {
+	foreach my $include ( @include ) {
+		my @included_folders = grep { /$include/ } @h1_folders_all ;
+		add_to_requested_folders( @included_folders ) ;
+		myprint( "Including folders matching pattern $include\n" . jux_utf8_list( @included_folders )  . "\n"  ) ;
+	}
+}
+
+if ( scalar  @exclude  ) {
+	foreach my $exclude ( @exclude ) {
+		my @requested_folder = sort keys %requested_folder ;
+		my @excluded_folders = grep { /$exclude/ } @requested_folder ;
+		remove_from_requested_folders( @excluded_folders ) ;
+		myprint( "Excluding folders matching pattern $exclude\n" . jux_utf8_list( @excluded_folders ) . "\n"  ) ;
+	}
+}
+
+
+# sort before is not very powerful
+# it adds --folderfirst and --folderlast even if they don't exist on host1
+@h1_folders_wanted = sort_requested_folders(  ) ;
+
+# Remove no selectable folders
+
+
+my @h1_folders_wanted_exist ;
+myprint( "Host1: checking all wanted folders exist.\n"  ) ;
+foreach my $folder ( @h1_folders_wanted ) {
+	( $debug or $sync->{debugfolders} ) and myprint( "Checking $folder exists on host1\n"  ) ;
+	if ( ! exists  $h1_folders_all{ $folder }  ) {
+                myprint( "Host1: warning! ignoring folder $folder because it is not in host1 whole folders list.\n" ) ;
+		next ;
+	}else{
+		push  @h1_folders_wanted_exist, $folder  ;
+	}
+}
+
+@h1_folders_wanted = @h1_folders_wanted_exist ;
+
+
+
+$checkselectable and do {
+	my @h1_folders_wanted_selectable ;
+        myprint( "Host1: checking all wanted folders are selectable. Use --nocheckselectable to avoid this check.\n"  ) ;
+	foreach my $folder ( @h1_folders_wanted ) {
+        	( $debug or $sync->{debugfolders} ) and myprint( "Checking $folder is selectable on host1\n"  ) ;
+        	if ( ! $imap1->selectable( $folder ) ) {
+                                myprint( "Host1: warning! ignoring folder $folder because it is not selectable\n" ) ;
+        	}else{
+			push  @h1_folders_wanted_selectable, $folder  ;
+		}
+	}
+	@h1_folders_wanted = @h1_folders_wanted_selectable ;
+        ( $debug or $sync->{debugfolders} ) and myprint( 'Host1: checking folders took ', timenext(  ), " s\n"  ) ;
+} ;
+
+$sync->{h1_folders_wanted} = \@h1_folders_wanted ;
+
+
+my( $h1_sep, $h2_sep ) ;
+# what are the private folders separators for each server ?
+
+( $debug or $sync->{debugfolders} ) and myprint( "Getting separators\n"  ) ;
+$h1_sep = get_separator( $imap1, $sep1, '--sep1', 'Host1', \@h1_folders_all ) ;
+$h2_sep = get_separator( $imap2, $sep2, '--sep2', 'Host2', \@h2_folders_all ) ;
+
+my( $h1_prefix, $h2_prefix ) ;
+$sync->{ h1_prefix } = $h1_prefix = get_prefix( $imap1, $prefix1, '--prefix1', 'Host1', \@h1_folders_all ) ;
+$sync->{ h2_prefix } = $h2_prefix = get_prefix( $imap2, $prefix2, '--prefix2', 'Host2', \@h2_folders_all ) ;
+
+
+myprint( "Host1 separator and prefix: [$h1_sep][$h1_prefix]\n"  ) ;
+myprint( "Host2 separator and prefix: [$h2_sep][$h2_prefix]\n"  ) ;
+
+automap( $sync ) ;
+
+
+foreach my $h1_fold ( @h1_folders_wanted ) {
+	my $h2_fold ;
+	$h2_fold = imap2_folder_name( $h1_fold ) ;
+	$h2_folders_from_1_wanted{ $h2_fold }++ ;
+        if ( 1 < $h2_folders_from_1_wanted{ $h2_fold } ) {
+        	$h2_folders_from_1_several{ $h2_fold }++ ;
+        }
+}
+@h2_folders_from_1_wanted = sort keys %h2_folders_from_1_wanted;
+
+foreach my $h1_fold ( @h1_folders_all ) {
+	my $h2_fold ;
+	$h2_fold = imap2_folder_name( $h1_fold ) ;
+	$h2_folders_from_1_all{ $h2_fold }++ ;
+}
+
+
+
+myprint( << 'END_LISTING'  ) ;
+
+++++ Listing folders
+All foldernames are presented between brackets like [X] where X is the foldername.
+When a foldername contains non-ASCII characters it is presented in the form
+[X] = [Y] where
+X is the imap foldername you have to use in command line options and
+Y is the uft8 output just printed for convenience, to recognize it.
+
+END_LISTING
+
+print
+  "Host1 folders list:\n",
+  jux_utf8_list( @h1_folders_all ),
+  "\n",
+  "Host2 folders list:\n",
+  jux_utf8_list( @h2_folders_all ),
+  "\n" ;
+
+print
+  'Host1 subscribed folders list: ',
+  jux_utf8_list( sort keys  %h1_subscribed_folder  ), "\n"
+  if ( $subscribed ) ;
+
+my @h2_folders_not_in_1;
+@h2_folders_not_in_1 = list_folders_in_2_not_in_1(  ) ;
+
+if ( @h2_folders_not_in_1 ) {
+	myprint( "Folders in host2 not in host1:\n",
+	jux_utf8_list( @h2_folders_not_in_1 ), "\n" ) ;
+}
+
+
+if ( defined  $sync->{f1f2auto}  ) {
+	myprint( "Folders mapping from --automap feature (use --f1f2 to override any mapping):\n"  ) ;
+	foreach my $h1_fold ( keys %{$sync->{f1f2auto}} ) {
+        	my $h2_fold = $sync->{f1f2auto}{$h1_fold} ;
+		myprintf( "%-40s -> %-40s\n",
+		       jux_utf8( $h1_fold ), jux_utf8( $h2_fold ) ) ;
+        }
+        myprint( "\n"  ) ;
+}
+
+if ( defined  $sync->{f1f2}  ) {
+	myprint( "Folders mapping from --f1f2 options, it overrides --automap:\n"  ) ;
+	foreach my $h1_fold ( keys %{$sync->{f1f2}} ) {
+        	my $h2_fold = $sync->{f1f2}{$h1_fold} ;
+                my $warn = q{} ;
+                if ( not exists  $h1_folders_all{ $h1_fold }  ) {
+                        $warn = "BUT $h1_fold does NOT exist on host1!" ;
+                }
+		myprintf( "%-40s -> %-40s %s\n",
+		       jux_utf8( $h1_fold ), jux_utf8( $h2_fold ), $warn ) ;
+        }
+        myprint( "\n"  ) ;
+}
+
+exit_clean( $sync, $EX_OK ) if ( $sync->{justfolderlists} ) ;
+exit_clean( $sync, $EX_OK ) if ( $sync->{justautomap} ) ;
+
+debugsleep( $sync ) ;
+
+if ( $foldersizes ) {
+        foldersizes_on_h1h2(  ) ;
+}
+
+
+exit_clean( $sync, $EX_OK ) if ( $justfoldersizes ) ;
+
+$sync->{stats} = 1 ;
+
+if ( $sync->{'delete1emptyfolders'} ) {
+        delete1emptyfolders( $sync ) ;
+}
+
+delete_folders_in_2_not_in_1(  ) if $delete2folders ;
+
+# folder loop
+$h1_folders_wanted_nb = scalar  @h1_folders_wanted  ;
+
+myprint( "++++ Looping on each one of $h1_folders_wanted_nb folders to sync\n" ) ;
+
+my $begin_transfer_time = time ;
+
+my %uid_candidate_for_deletion ;
+my %uid_candidate_no_deletion ;
+
+my %h2_folders_of_md5 = (  ) ;
+
+FOLDER: foreach my $h1_fold ( @h1_folders_wanted ) {
+
+        last FOLDER if $imap1->IsUnconnected(  ) ;
+        last FOLDER if $imap2->IsUnconnected(  ) ;
+
+	my $h2_fold = imap2_folder_name( $h1_fold ) ;
+
+	$h1_folders_wanted_ct++ ;
+	myprintf( "Folder %7s %-35s -> %-35s\n", "$h1_folders_wanted_ct/$h1_folders_wanted_nb",
+		jux_utf8( $h1_fold ), jux_utf8( $h2_fold ) ) ;
+        if ( $sync->{debugmemory} ) {
+                myprintf("FL: Memory consumption: %.1f MiB\n", memory_consumption(  ) / $KIBI / $KIBI) ;
+        }
+	# host1 can not be fetched read only, select is needed because of expunge.
+	select_folder( $imap1, $h1_fold, 'Host1' ) or next FOLDER ;
+
+        debugsleep( $sync ) ;
+
+	my $h1_fold_nb_messages = count_from_select( $imap1->History ) ;
+        myprint( "Host1 folder [$h1_fold] has $h1_fold_nb_messages messages in total (mentioned by SELECT)\n" ) ;
+
+        if ( $skipemptyfolders and 0 == $h1_fold_nb_messages ) {
+        	myprint( "Skipping empty host1 folder [$h1_fold]\n"  ) ;
+                next FOLDER ;
+        }
+
+	if ( ! exists  $h2_folders_all{ $h2_fold }  ) {
+		create_folder( $imap2, $h2_fold, $h1_fold ) or next FOLDER ;
+	}
+
+	acls_sync( $h1_fold, $h2_fold ) ;
+
+        # Sometimes the folder on host2 is listed (it exists) but is
+        # not selectable but becomes selectable by a create (Gmail)
+	select_folder( $imap2, $h2_fold, 'Host2' )
+        or ( create_folder( $imap2, $h2_fold, $h1_fold )
+             and select_folder( $imap2, $h2_fold, 'Host2' ) )
+        or next FOLDER ;
+	my @select_results = $imap2->Results(  ) ;
+
+	my $h2_fold_nb_messages = count_from_select( @select_results ) ;
+        myprint( "Host2 folder [$h2_fold] has $h2_fold_nb_messages messages in total (mentioned by SELECT)\n" ) ;
+
+	my $permanentflags2 = permanentflags( @select_results ) ;
+	( $debug or $debugflags ) and myprint( "Host2 folder [$h2_fold] permanentflags: $permanentflags2\n"  ) ;
+
+	if ( $expunge or $expunge1 ){
+		myprint( "Host1: Expunging $h1_fold $dry_message\n"  ) ;
+		unless( $dry ) { $imap1->expunge(  ) } ;
+	}
+
+	if ( ( ( $subscribe and exists $h1_subscribed_folder{ $h1_fold } ) or $subscribeall )
+             and not exists  $h2_subscribed_folder{ $h2_fold }  ) {
+		myprint( "Host2: Subscribing to folder $h2_fold\n"  ) ;
+		unless( $dry ) { $imap2->subscribe( $h2_fold ) } ;
+	}
+
+	next FOLDER if ( $justfolders ) ;
+
+        last FOLDER if $imap1->IsUnconnected(  ) ;
+        last FOLDER if $imap2->IsUnconnected(  ) ;
+
+        my $h1_msgs_all_hash_ref = {  } ;
+	my @h1_msgs = select_msgs( $imap1, $h1_msgs_all_hash_ref, $search1, $h1_fold );
+	last FOLDER if $imap1->IsUnconnected(  ) ;
+
+        my $h1_msgs_nb = scalar  @h1_msgs  ;
+        $h1{ $h1_fold }{ 'messages_nb' } = $h1_msgs_nb ;
+
+	myprint( "Host1 folder [$h1_fold] considering $h1_msgs_nb messages\n"  ) ;
+	( $debug or $debuglist ) and myprint( "Host1 folder [$h1_fold] considering $h1_msgs_nb messages, LIST gives: @h1_msgs\n" ) ;
+        $debug and myprint( "Host1 selecting messages of folder [$h1_fold] took ", timenext(), " s\n" ) ;
+
+        my $h2_msgs_all_hash_ref = {  } ;
+	my @h2_msgs = select_msgs( $imap2, $h2_msgs_all_hash_ref, $search2, $h2_fold ) ;
+	last FOLDER if $imap2->IsUnconnected(  ) ;
+
+        my $h2_msgs_nb = scalar  @h2_msgs  ;
+        $h2{ $h2_fold }{ 'messages_nb' } = $h2_msgs_nb ;
+
+	myprint( "Host2 folder [$h2_fold] considering $h2_msgs_nb messages\n" ) ;
+	( $debug or $debuglist ) and myprint( "Host2 folder [$h2_fold] considering $h2_msgs_nb messages, LIST gives: @h2_msgs\n" ) ;
+        $debug and myprint( "Host2 selecting messages of folder [$h2_fold] took ", timenext(), " s\n" ) ;
+
+	my $cache_base = "$tmpdir/imapsync_cache/" ;
+	my $cache_dir = cache_folder( $cache_base, "$host1/$user1/$host2/$user2", $h1_fold, $h2_fold ) ;
+	my ( $cache_1_2_ref, $cache_2_1_ref ) = ( {}, {} ) ;
+
+	my $h1_uidvalidity = $imap1->uidvalidity(  ) || q{} ;
+	my $h2_uidvalidity = $imap2->uidvalidity(  ) || q{} ;
+
+        last FOLDER if $imap1->IsUnconnected(  ) ;
+        last FOLDER if $imap2->IsUnconnected(  ) ;
+
+	if ( $usecache ) {
+		myprint( "cache directory: $cache_dir\n"  ) ;
+		mkpath( "$cache_dir" ) ;
+		( $cache_1_2_ref, $cache_2_1_ref )
+                = get_cache( $cache_dir, \@h1_msgs, \@h2_msgs, $h1_msgs_all_hash_ref, $h2_msgs_all_hash_ref ) ;
+		myprint( 'CACHE h1 h2: ', scalar  keys %{ $cache_1_2_ref } , " files\n"  ) ;
+		$debug and myprint( '[',
+		    map ( { "$_->$cache_1_2_ref->{$_} " } keys %{ $cache_1_2_ref } ), " ]\n" ) ;
+	}
+
+	my %h1_hash = () ;
+	my %h2_hash = () ;
+
+	my ( %h1_msgs, %h2_msgs ) ;
+	@h1_msgs{ @h1_msgs } = ();
+	@h2_msgs{ @h2_msgs } = ();
+
+	my @h1_msgs_in_cache = sort { $a <=> $b } keys %{ $cache_1_2_ref } ;
+	my @h2_msgs_in_cache = keys %{ $cache_2_1_ref } ;
+
+	my ( %h1_msgs_not_in_cache, %h2_msgs_not_in_cache ) ;
+	%h1_msgs_not_in_cache = %h1_msgs ;
+	%h2_msgs_not_in_cache = %h2_msgs ;
+	delete @h1_msgs_not_in_cache{ @h1_msgs_in_cache } ;
+	delete @h2_msgs_not_in_cache{ @h2_msgs_in_cache } ;
+
+	my @h1_msgs_not_in_cache = keys %h1_msgs_not_in_cache ;
+	#myprint( "h1_msgs_not_in_cache: [@h1_msgs_not_in_cache]\n"  ) ;
+	my @h2_msgs_not_in_cache = keys %h2_msgs_not_in_cache ;
+
+	my @h2_msgs_delete2_not_in_cache = () ;
+	%h1_msgs_copy_by_uid = (  ) ;
+
+	if ( $useuid ) {
+		# use uid so we have to avoid getting header
+		@h1_msgs_copy_by_uid{ @h1_msgs_not_in_cache } = (  ) ;
+		@h2_msgs_delete2_not_in_cache = @h2_msgs_not_in_cache if $usecache ;
+		@h1_msgs_not_in_cache = (  ) ;
+		@h2_msgs_not_in_cache = (  ) ;
+
+		#myprint( "delete2: @h2_msgs_delete2_not_in_cache\n" ) ;
+	}
+
+	$debug and myprint( "Host1 parsing headers of folder [$h1_fold]\n" ) ;
+
+	my ($h1_heads_ref, $h1_fir_ref) = ({}, {});
+	$h1_heads_ref = $imap1->parse_headers([@h1_msgs_not_in_cache], @useheader) if (@h1_msgs_not_in_cache);
+	$debug and myprint( "Host1 parsing headers of folder [$h1_fold] took ", timenext(), " s\n" ) ;
+
+	@{ $h1_fir_ref }{@h1_msgs} = ( undef ) ;
+
+	$debug and myprint( "Host1 getting flags idate and sizes of folder [$h1_fold]\n"  ) ;
+        if ( $abletosearch ) {
+		$h1_fir_ref = $imap1->fetch_hash( \@h1_msgs, 'FLAGS', 'INTERNALDATE', 'RFC822.SIZE', $h1_fir_ref )
+	  	if ( @h1_msgs ) ;
+        }else{
+		my $uidnext = $imap1->uidnext( $h1_fold ) || $uidnext_default ;
+		my $fetch_hash_uids = $fetch_hash_set || "1:$uidnext" ;
+		$h1_fir_ref = $imap1->fetch_hash( $fetch_hash_uids, 'FLAGS', 'INTERNALDATE', 'RFC822.SIZE', $h1_fir_ref )
+		if ( @h1_msgs ) ;
+        }
+	$debug and myprint( "Host1 getting flags idate and sizes of folder [$h1_fold] took ", timenext(), " s\n"  ) ;
+	unless ($h1_fir_ref) {
+		my $error = join( q{}, "Host1 folder $h1_fold: Could not fetch_hash ",
+			scalar @h1_msgs, ' msgs: ', $imap1->LastError || q{}, "\n" ) ;
+		errors_incr( $sync, $error ) ;
+		next FOLDER ;
+	}
+
+	my @h1_msgs_duplicate;
+	foreach my $m (@h1_msgs_not_in_cache) {
+		my $rc = parse_header_msg($imap1, $m, $h1_heads_ref, $h1_fir_ref, 'Host1', \%h1_hash);
+		if (! defined $rc) {
+			my $h1_size = $h1_fir_ref->{$m}->{'RFC822.SIZE'} || 0;
+			myprint( "Host1 $h1_fold/$m size $h1_size ignored (no wanted headers so we ignore this message. To solve this: use --addheader)\n"  ) ;
+			$total_bytes_skipped += $h1_size;
+			$nb_msg_skipped += 1;
+			$h1_nb_msg_noheader +=1;
+                        $h1_nb_msg_processed +=1 ;
+		} elsif(0 == $rc) {
+			# duplicate
+			push @h1_msgs_duplicate, $m;
+			# duplicate, same id same size?
+			my $h1_size = $h1_fir_ref->{$m}->{'RFC822.SIZE'} || 0;
+			$nb_msg_skipped += 1;
+			$h1_total_bytes_duplicate += $h1_size;
+			$h1_nb_msg_duplicate += 1;
+                        $h1_nb_msg_processed +=1 ;
+		}
+	}
+        my $h1_msgs_duplicate_nb = scalar  @h1_msgs_duplicate  ;
+        $h1{ $h1_fold }{ 'duplicates_nb' } = $h1_msgs_duplicate_nb ;
+
+        $debug and myprint( "Host1 selected: $h1_msgs_nb  duplicates: $h1_msgs_duplicate_nb\n"  ) ;
+	$debug and myprint( 'Host1 whole time parsing headers took ', timenext(), " s\n"  ) ;
+
+	$debug and myprint( "Host2 parsing headers of folder [$h2_fold]\n" ) ;
+
+	my ($h2_heads_ref, $h2_fir_ref) = ( {}, {} );
+	$h2_heads_ref =   $imap2->parse_headers([@h2_msgs_not_in_cache], @useheader) if (@h2_msgs_not_in_cache);
+	$debug and myprint( "Host2 parsing headers of folder [$h2_fold] took ", timenext(), " s\n"  ) ;
+
+	$debug and myprint( "Host2 getting flags idate and sizes of folder [$h2_fold]\n"  ) ;
+	@{ $h2_fir_ref }{@h2_msgs} = (  ); # fetch_hash can select by uid with last arg as ref
+
+
+        if ( $abletosearch ) {
+		$h2_fir_ref = $imap2->fetch_hash( \@h2_msgs, 'FLAGS', 'INTERNALDATE', 'RFC822.SIZE', $h2_fir_ref)
+		if (@h2_msgs) ;
+        }else{
+		my $uidnext = $imap2->uidnext( $h2_fold ) || $uidnext_default ;
+		my $fetch_hash_uids = $fetch_hash_set || "1:$uidnext" ;
+		$h2_fir_ref = $imap2->fetch_hash( $fetch_hash_uids, 'FLAGS', 'INTERNALDATE', 'RFC822.SIZE', $h2_fir_ref )
+		if ( @h2_msgs ) ;
+        }
+
+	$debug and myprint( "Host2 getting flags idate and sizes of folder [$h2_fold] took ", timenext(), " s\n"  ) ;
+
+	my @h2_msgs_duplicate;
+	foreach my $m (@h2_msgs_not_in_cache) {
+		my $rc = parse_header_msg($imap2, $m, $h2_heads_ref, $h2_fir_ref, 'Host2', \%h2_hash) ;
+		my $h2_size = $h2_fir_ref->{$m}->{'RFC822.SIZE'} || 0 ;
+		if (! defined  $rc  ) {
+                        myprint( "Host2 $h2_fold/$m size $h2_size ignored (no wanted headers so we ignore this message)\n"  ) ;
+			$h2_nb_msg_noheader += 1 ;
+		} elsif( 0 == $rc ) {
+			# duplicate
+			$h2_nb_msg_duplicate += 1 ;
+			$h2_total_bytes_duplicate += $h2_size ;
+			push  @h2_msgs_duplicate, $m  ;
+		}
+	}
+
+        # %h2_folders_of_md5
+        foreach my $md5 (  keys  %h2_hash  ) {
+        	$h2_folders_of_md5{ $md5 }->{ $h2_fold } ++ ;
+        }
+
+
+        my $h2_msgs_duplicate_nb = scalar  @h2_msgs_duplicate  ;
+        $h2{ $h2_fold }{ 'duplicates_nb' } = $h2_msgs_duplicate_nb ;
+
+        myprint( "Host2 folder $h2_fold selected: $h2_msgs_nb messages,  duplicates: $h2_msgs_duplicate_nb\n" )
+        	if ( $debug or $delete2duplicates or $h2_msgs_duplicate_nb ) ;
+	$debug and myprint( 'Host2 whole time parsing headers took ', timenext(  ), " s\n"  ) ;
+
+	$debug and myprint( "++++ Verifying [$h1_fold] -> [$h2_fold]\n" ) ;
+	# messages in host1 that are not in host2
+
+	my @h1_hash_keys_sorted_by_uid
+	  = sort {$h1_hash{$a}{'m'} <=> $h1_hash{$b}{'m'}} keys %h1_hash;
+
+	#myprint( map { $h1_hash{$_}{'m'} . q{ }} @h1_hash_keys_sorted_by_uid ) ;
+
+	my @h2_hash_keys_sorted_by_uid
+	  = sort {$h2_hash{$a}{'m'} <=> $h2_hash{$b}{'m'}} keys %h2_hash;
+
+
+	if( $delete2duplicates and not exists  $h2_folders_from_1_several{ $h2_fold }  ) {
+		my @h2_expunge ;
+
+		foreach my $h2_msg ( @h2_msgs_duplicate ) {
+			myprint( "msg $h2_fold/$h2_msg marked \\Deleted [duplicate] on host2 $dry_message\n"  ) ;
+			push  @h2_expunge, $h2_msg  if $uidexpunge2 ;
+			unless ( $dry ) {
+				$imap2->delete_message( $h2_msg ) ;
+				$h2_nb_msg_deleted += 1 ;
+			}
+		}
+		my $cnt = scalar @h2_expunge ;
+		if( @h2_expunge and not $expunge2 ) {
+			myprint( "Host2: UidExpunging $cnt message(s) in folder $h2_fold $dry_message\n"  ) ;
+			$imap2->uidexpunge( \@h2_expunge ) if ! $dry ;
+		}
+        	if ( $expunge2 ){
+                	myprint( "Host2: Expunging folder $h2_fold $dry_message\n"  ) ;
+                	$imap2->expunge(  ) if ! $dry ;
+        	}
+	}
+
+	if( $delete2 and not exists  $h2_folders_from_1_several{ $h2_fold }  ) {
+        	# No host1 folders f1a f1b ... going all to same f2 (via --regextrans2)
+		my @h2_expunge;
+		foreach my $m_id (@h2_hash_keys_sorted_by_uid) {
+			#myprint( "$m_id " ) ;
+			unless (exists $h1_hash{$m_id}) {
+				my $h2_msg  = $h2_hash{$m_id}{'m'};
+				my $h2_flags  = $h2_hash{$m_id}{'F'} || q{};
+				my $isdel  = $h2_flags =~ /\B\\Deleted\b/x ? 1 : 0;
+				myprint( "Host2: msg $h2_fold/$h2_msg marked \\Deleted on host2 [$m_id] $dry_message\n" )
+				  if ! $isdel;
+				push @h2_expunge, $h2_msg if $uidexpunge2;
+				unless ($dry or $isdel) {
+					$imap2->delete_message($h2_msg);
+					$h2_nb_msg_deleted += 1;
+				}
+			}
+		}
+		foreach my $h2_msg ( @h2_msgs_delete2_not_in_cache ) {
+			myprint( "Host2: msg $h2_fold/$h2_msg marked \\Deleted [not in cache] on host2 $dry_message\n" ) ;
+                        push @h2_expunge, $h2_msg if $uidexpunge2;
+			unless ($dry) {
+				$imap2->delete_message($h2_msg);
+				$h2_nb_msg_deleted += 1;
+			}
+		}
+		my $cnt = scalar @h2_expunge ;
+
+		if( @h2_expunge and not $expunge2 ) {
+			myprint( "Host2: UidExpunging $cnt message(s) in folder $h2_fold $dry_message\n"  ) ;
+			$imap2->uidexpunge( \@h2_expunge ) if ! $dry ;
+		}
+        	if ( $expunge2 ) {
+                	myprint( "Host2: Expunging folder $h2_fold $dry_message\n"  ) ;
+                	$imap2->expunge(  ) if ! $dry ;
+        	}
+	}
+
+	if( $delete2 and exists  $h2_folders_from_1_several{ $h2_fold }  ) {
+        	myprint( "Host2 folder $h2_fold $h2_folders_from_1_several{ $h2_fold } folders left to sync there\n"  ) ;
+		my @h2_expunge;
+		foreach my $m_id ( @h2_hash_keys_sorted_by_uid ) {
+                	my $h2_msg  = $h2_hash{ $m_id }{ 'm' } ;
+			unless ( exists  $h1_hash{ $m_id }  ) {
+				my $h2_flags  = $h2_hash{ $m_id }{ 'F' } || q{} ;
+				my $isdel  = $h2_flags =~ /\B\\Deleted\b/x ? 1 : 0 ;
+				unless ( $isdel ) {
+                                	$debug and myprint( "Host2: msg $h2_fold/$h2_msg candidate for deletion [$m_id]\n"  ) ;
+					$uid_candidate_for_deletion{ $h2_fold }{ $h2_msg }++ ;
+				}
+			}else{
+                        	$debug and myprint( "Host2: msg $h2_fold/$h2_msg will cancel deletion [$m_id]\n"  ) ;
+                        	$uid_candidate_no_deletion{ $h2_fold }{ $h2_msg }++ ;
+                        }
+		}
+		foreach my $h2_msg ( @h2_msgs_delete2_not_in_cache ) {
+			myprint( "Host2: msg $h2_fold/$h2_msg candidate for deletion [not in cache]\n" ) ;
+                        $uid_candidate_for_deletion{ $h2_fold }{ $h2_msg }++ ;
+		}
+
+		foreach my $h2_msg ( @h2_msgs_in_cache ) {
+			myprint( "Host2: msg $h2_fold/$h2_msg will cancel deletion [in cache]\n" ) ;
+                        $uid_candidate_no_deletion{ $h2_fold }{ $h2_msg }++ ;
+		}
+
+
+                if ( 0 == $h2_folders_from_1_several{ $h2_fold } ) {
+                	# last host1 folder going to $h2_fold
+                        myprint( "Last host1 folder going to $h2_fold\n"  ) ;
+                        foreach my $h2_msg ( keys %{ $uid_candidate_for_deletion{ $h2_fold } } ) {
+                        	$debug and myprint( "Host2: msg $h2_fold/$h2_msg candidate for deletion\n"  ) ;
+                                if ( exists  $uid_candidate_no_deletion{ $h2_fold }{ $h2_msg }  ) {
+                                	$debug and myprint( "Host2: msg $h2_fold/$h2_msg canceled deletion\n"  ) ;
+                                }else{
+                                	myprint( "Host2: msg $h2_fold/$h2_msg marked \\Deleted $dry_message\n" ) ;
+                                        push  @h2_expunge, $h2_msg  if $uidexpunge2 ;
+                                        unless ( $dry ) {
+                                        	$imap2->delete_message( $h2_msg ) ;
+                                        	$h2_nb_msg_deleted += 1 ;
+                                        }
+                                }
+                        }
+                }
+
+		my $cnt = scalar @h2_expunge ;
+		if( @h2_expunge and not $expunge2 ) {
+			myprint( "Host2: UidExpunging $cnt message(s) in folder $h2_fold $dry_message\n"  ) ;
+			$imap2->uidexpunge( \@h2_expunge ) if ! $dry ;
+		}
+        	if ( $expunge2 ) {
+                	myprint( "Host2: Expunging host2 folder $h2_fold $dry_message\n"  ) ;
+                	$imap2->expunge(  ) if ! $dry ;
+        	}
+
+                $h2_folders_from_1_several{ $h2_fold }-- ;
+	}
+
+
+	my $h2_uidnext = $imap2->uidnext( $h2_fold ) ;
+        $debug and myprint( "Host2 uidnext: $h2_uidnext\n"  ) ;
+	$h2_uidguess = $h2_uidnext ;
+	MESS: foreach my $m_id (@h1_hash_keys_sorted_by_uid) {
+        	last FOLDER if $imap1->IsUnconnected(  ) ;
+                last FOLDER if $imap2->IsUnconnected(  ) ;
+
+		#myprint( "h1_nb_msg_processed: $h1_nb_msg_processed\n"  ) ;
+		my $h1_size  = $h1_hash{$m_id}{'s'};
+		my $h1_msg   = $h1_hash{$m_id}{'m'};
+		my $h1_idate = $h1_hash{$m_id}{'D'};
+
+		if ( ( not exists  $h2_hash{ $m_id }  )
+                	and ( not ( exists $h2_folders_of_md5{ $m_id } )
+                              or not $skipcrossduplicates ) ) {
+			# copy
+			my $h2_msg = copy_message( $sync, $h1_msg, $h1_fold, $h2_fold, $h1_fir_ref, $permanentflags2, $cache_dir ) ;
+                        $h2_folders_of_md5{ $m_id }->{ $h2_fold } ++ ;
+                        if( $delete2 and ( exists $h2_folders_from_1_several{ $h2_fold } ) and $h2_msg ) {
+                        	myprint( "Host2: msg $h2_fold/$h2_msg will cancel deletion [fresh copy] on host2\n"  ) ;
+	                        $uid_candidate_no_deletion{ $h2_fold }{ $h2_msg }++ ;
+                        }
+                        last FOLDER if total_bytes_max_reached(  ) ;
+			next MESS;
+		}
+		else{
+		        # already on host2
+                        if ( exists  $h2_hash{ $m_id }  ) {
+				my $h2_msg   = $h2_hash{$m_id}{'m'} ;
+				$debug and myprint( "Host1 found msg $h1_fold/$h1_msg equals Host2 $h2_fold/$h2_msg\n"  ) ;
+                                if ( $usecache ) {
+                                	$debugcache and myprint( "touch $cache_dir/${h1_msg}_$h2_msg\n"  ) ;
+                                	touch( "$cache_dir/${h1_msg}_$h2_msg" )
+                                        or croak( "Couldn't touch $cache_dir/${h1_msg}_$h2_msg" ) ;
+                                }
+                        }elsif( exists  $h2_folders_of_md5{ $m_id }  ) {
+                        	my @folders_dup = keys  %{ $h2_folders_of_md5{ $m_id } }  ;
+                        	( $debug or $debugcrossduplicates ) and myprint( "Host1 found msg $h1_fold/$h1_msg is also in Host2 folders @folders_dup\n"  ) ;
+                        }
+			$total_bytes_skipped += $h1_size ;
+			$nb_msg_skipped += 1 ;
+                        $h1_nb_msg_processed +=1 ;
+                }
+
+                if ( exists  $h2_hash{ $m_id }  ) {
+			#$debug and myprint( "MESSAGE $m_id\n" ) ;
+			my $h2_msg  = $h2_hash{$m_id}{'m'};
+
+                	sync_flags_fir( $h1_fold, $h1_msg, $h2_fold, $h2_msg, $permanentflags2, $h1_fir_ref, $h2_fir_ref ) ;
+	    		# Good
+			my $h2_size = $h2_hash{$m_id}{'s'};
+			$debug and myprint(
+			"Host1 size  msg $h1_fold/$h1_msg = $h1_size <> $h2_size = Host2 $h2_fold/$h2_msg\n" ) ;
+		}
+                last FOLDER if $imap2->IsUnconnected(  ) ;
+
+		if ( $delete ) {
+			delete_message_on_host1( $h1_msg, $h1_fold ) ;
+		}
+	}
+	# END MESS: loop
+        last FOLDER if $imap1->IsUnconnected(  ) ;
+        last FOLDER if $imap2->IsUnconnected(  ) ;
+	MESS_IN_CACHE: foreach my $h1_msg ( @h1_msgs_in_cache ) {
+		my $h2_msg = $cache_1_2_ref->{ $h1_msg } ;
+		$debugcache and myprint( "cache messages update flags $h1_msg->$h2_msg\n" ) ;
+		sync_flags_fir( $h1_fold, $h1_msg, $h2_fold, $h2_msg, $permanentflags2, $h1_fir_ref, $h2_fir_ref ) ;
+		my $h1_size = $h1_fir_ref->{ $h1_msg }->{ 'RFC822.SIZE' } || 0 ;
+		$total_bytes_skipped += $h1_size;
+		$nb_msg_skipped += 1;
+                $h1_nb_msg_processed +=1 ;
+                last FOLDER if $imap2->IsUnconnected(  ) ;
+	}
+
+	#myprint( "Messages by uid: ", map { "$_ " } keys %h1_msgs_copy_by_uid, "\n"  ) ;
+	MESS_BY_UID: foreach my $h1_msg ( sort { $a <=> $b } keys %h1_msgs_copy_by_uid ) {
+		#
+		$debug and myprint( "Copy by uid $h1_fold/$h1_msg\n"  ) ;
+                last FOLDER if $imap1->IsUnconnected(  ) ;
+                last FOLDER if $imap2->IsUnconnected(  ) ;
+		my $h2_msg = copy_message( $sync, $h1_msg, $h1_fold, $h2_fold, $h1_fir_ref, $permanentflags2, $cache_dir ) ;
+                if( $delete2 and exists  $h2_folders_from_1_several{ $h2_fold }  and $h2_msg ) {
+                	myprint( "Host2: msg $h2_fold/$h2_msg will cancel deletion [fresh copy] on host2\n"  ) ;
+	                $uid_candidate_no_deletion{ $h2_fold }{ $h2_msg }++ ;
+                }
+		last FOLDER if total_bytes_max_reached(  ) ;
+	}
+
+	if ( $expunge or $expunge1 ){
+		myprint( "Host1: Expunging folder $h1_fold $dry_message\n"  ) ;
+		unless( $dry ) { $imap1->expunge(  ) } ;
+	}
+	if ( $expunge2 ){
+		myprint( "Host2: Expunging folder $h2_fold $dry_message\n"  ) ;
+		unless( $dry ) { $imap2->expunge(  ) } ;
+	}
+	$debug and myprint( 'Time: ', timenext(  ), " s\n"  ) ;
+}
+
+
+sub total_bytes_max_reached {
+
+	return( 0 ) if not $exitwhenover ;
+	if ( $total_bytes_transferred >= $exitwhenover ) {
+        	myprint( "Maximum bytes transferred reached, $total_bytes_transferred >= $exitwhenover, ending sync\n"  ) ;
+        	return( 1 ) ;
+        }
+
+}
+
+myprint( "++++ End looping on each folder\n"  ) ;
+( $debug or $sync->{debugfolders} ) and myprint( 'Time: ', timenext(  ), " s\n"  ) ;
+
+
+if ( $foldersizesatend ) {
+	myprint( << 'END_SIZE'  ) ;
+
+Folders sizes after the synchronization.
+You can remove this foldersizes listing by using  "--nofoldersizesatend"
+END_SIZE
+
+	foldersizesatend(  ) ;
+}
+
+$imap1->logout(  ) unless lost_connection( $imap1, "for host1 [$host1]" ) ;
+$imap2->logout(  ) unless lost_connection( $imap2, "for host2 [$host2]" ) ;
+
+stats( $sync ) ;
+myprint( errorsdump( $sync->{nb_errors}, errors_log( $sync ) ) ) if ( $sync->{errorsdump} ) ;
+tests_live_result( $sync->{nb_errors} ) if ( $testslive ) ;
+exit_clean( $sync, $EXIT_WITH_ERRORS ) if ( $sync->{nb_errors} ) ;
+exit_clean( $sync, $EX_OK ) ;
+
+# END of main program
+
+
+# subroutines
+sub  myprint  { return print  @ARG ; } 
+sub  myprintf { return printf @ARG ; } 
+
+sub mysprintf {
+        my( $format, @list ) = @ARG ;
+        return sprintf $format, @list ; 
+}
+
+sub unsetunsafe {
+        # Remove all content in unsafe evalued options
+        @regextrans2 = (  ) ;
+        @regexflag = (  ) ;
+        @regexmess = (  ) ;
+        @skipmess = (  ) ;
+        @pipemess = (  ) ;
+        $delete2foldersonly = undef ;
+        $delete2foldersbutnot = undef ;
+        return ;
+}
+
+sub debugsleep {
+        my $mysync = shift ;
+        if ( defined $mysync->{debugsleep} ) {
+                myprint( "Info: sleeping $mysync->{debugsleep}s\n" ) ;
+                sleep $mysync->{debugsleep} ;
+        }
+        return ;
+}
+
+sub foldersizes_on_h1h2 {
+	myprint( << 'END_SIZE'  ) ;
+
+Folders sizes before the synchronization.
+You can remove foldersizes listings by using "--nofoldersizes" and  "--nofoldersizesatend"
+but then you will also loose the ETA (Estimation Time of Arrival) given after each message copy.
+END_SIZE
+
+	( $h1_nb_msg_start, $h1_bytes_start ) = foldersizes( 'Host1', $imap1, $search1, @h1_folders_wanted        ) ;
+	( $h2_nb_msg_start, $h2_bytes_start ) = foldersizes( 'Host2', $imap2, $search2, @h2_folders_from_1_wanted ) ;
+
+        if ( not all_defined( $h1_nb_msg_start, $h1_bytes_start, $h2_nb_msg_start, $h2_bytes_start ) ) {
+                my $error = "Failure getting foldersizes, ETA and final diff will not be displayed\n" ;
+                errors_incr( $sync, $error ) ;
+                $foldersizes = 0 ;
+                $foldersizesatend = 0 ;
+                return ;
+        }
+        
+        my $h2_bytes_limit = $sync->{host2}->{quota_limit_bytes} || 0 ;
+        if ( $h2_bytes_limit and ( $h2_bytes_limit < $h1_bytes_start ) ) {
+        	my $quota_percent = mysprintf( '%.0f', $h1_bytes_start/$h2_bytes_limit ) ;
+                my $error = "Host2: Quota limit will be exceeded! Over $quota_percent % ( $h1_bytes_start bytes / $h2_bytes_limit bytes )\n" ;
+                errors_incr( $sync, $error ) ;
+        }
+        return ;
+}
+
+sub all_defined {
+        if ( not @ARG ) {
+                return 0 ;
+        }
+        foreach my $elem ( @ARG ) {
+                if ( not defined $elem ) {
+                        return 0 ;
+                }
+        }
+        return 1 ;
+}
+
+sub tests_all_defined {
+        is( 0, all_defined(  ),             'all_defined: no param  => 0' ) ;
+        is( 0, all_defined( () ),           'all_defined: void list => 0' ) ;
+        is( 0, all_defined( undef ),        'all_defined: undef     => 0' ) ;
+        is( 0, all_defined( undef, undef ), 'all_defined: undef     => 0' ) ;
+        is( 0, all_defined( 1, undef ),     'all_defined: 1 undef   => 0' ) ;
+        is( 0, all_defined( undef, 1 ),     'all_defined: undef 1   => 0' ) ;
+        is( 1, all_defined( 1, 1 ),         'all_defined: 1 1   => 1' ) ;
+        is( 1, all_defined( (1, 1) ),       'all_defined: (1 1) => 1' ) ;
+        return ;
+}
+
+
+sub imap_id_stuff {
+	my $sync = shift ;
+
+	if ( not $sync->{id} ) { return ; } ;
+
+	$sync->{h1_imap_id} = imap_id( $sync->{imap1}, 'Host1' ) ;
+	#myprint( 'Host1: ' . $sync->{h1_imap_id}  ) ;
+	$sync->{h2_imap_id} = imap_id( $sync->{imap2}, 'Host2' ) ;
+	#myprint( 'Host2: ' . $sync->{h2_imap_id}  ) ;
+
+	return ;
+}
+
+sub imap_id {
+	my ( $imap, $Side ) = @_ ;
+
+	$Side ||= q{} ;
+	my $imap_id_response = q{} ;
+
+	if ( not $imap->has_capability( 'ID' ) ) {
+		 $imap_id_response = 'No ID capability' ;
+                 myprint( "$Side: No ID capability\n"  ) ;
+	}else{
+		my $id_inp = imapsync_id( { side => lc $Side } ) ;
+		myprint( "\n$Side: found ID capability. Sending/receiving ID, presented in raw IMAP for now.\n"
+                . "In order to avoid sending/receiving ID, use option --noid\n" ) ;
+		my $debug_before = $imap->Debug(  ) ;
+		$imap->Debug( 1 ) ;
+		my $id_out = $imap->tag_and_run( 'ID ' . $id_inp ) ;
+		#my $id_out = $imap->tag_and_run( 'ID NIL' ) ;
+                myprint( "\n"  ) ;
+		$imap->Debug( $debug_before ) ;
+		#$imap_id_response = Data::Dumper->Dump( [ $id_out ], [ 'IMAP_ID' ] ) ;
+	}
+	return( $imap_id_response ) ;
+}
+
+sub imapsync_id {
+	my $overhashref = shift ;
+	# See http://tools.ietf.org/html/rfc2971.html
+
+	my $imapsync_id = { } ;
+
+	my $imapsync_id_lamiral = {
+		name          => 'imapsync',
+		version       => imapsync_version(  ),
+		os            => $OSNAME,
+		vendor        => 'Gilles LAMIRAL',
+		'support-url' => 'http://imapsync.lamiral.info/',
+		# Example of date-time:  19-Sep-2015 08:56:07
+		date          => date_from_rcs( q{$Date: 2016/08/19 10:30:36 $ } ),
+	} ;
+
+	my $imapsync_id_github  = {
+		name          => 'imapsync',
+		version       => imapsync_version(  ),
+		os            => $OSNAME,
+		vendor        => 'github',
+		'support-url' => 'https://github.com/imapsync/imapsync',
+		date          => date_from_rcs( q{$Date: 2016/08/19 10:30:36 $ } ),
+	} ;
+
+	$imapsync_id = $imapsync_id_lamiral ;
+	#$imapsync_id = $imapsync_id_github ;
+	my %mix = ( %{ $imapsync_id }, %{ $overhashref } ) ;
+	my $imapsync_id_str = format_for_imap_arg( \%mix ) ;
+	#myprint( "$imapsync_id_str\n"  ) ;
+	return( $imapsync_id_str ) ;
+}
+
+sub tests_imapsync_id {
+	ok( '("name" "imapsync" "version" "111" "os" "beurk" "vendor" "Gilles LAMIRAL" "support-url" "http://imapsync.lamiral.info/" "date" "22-12-1968" "side" "host1")'
+	eq imapsync_id( {
+		version => 111,
+		os => 'beurk',
+		date => '22-12-1968',
+		side => 'host1' } ),
+	'tests_imapsync_id override' ) ;
+
+	return ;
+}
+
+sub format_for_imap_arg {
+	my $ref = shift ;
+
+	my $string = q{} ;
+	my %terms = %{ $ref } ;
+	my @terms = (  ) ;
+	if ( not ( %terms ) ) { return( 'NIL' ) } ;
+	# sort like in RFC then add extra key/values
+	foreach my $key ( qw( name version os os-version vendor support-url address date command arguments environment) ) {
+		if ( $terms{ $key } ) {
+			push  @terms, $key, $terms{ $key }  ;
+			delete $terms{ $key } ;
+		}
+	}
+	push  @terms, %terms  ;
+	$string = '(' . ( join q{ }, map { '"' . $_ . '"' } @terms )  . ')' ;
+	return( $string ) ;
+}
+
+
+
+sub tests_format_for_imap_arg {
+	ok( 'NIL' eq format_for_imap_arg( { } ), 'format_for_imap_arg empty hash ref' ) ;
+	ok( '("name" "toto")' eq format_for_imap_arg( { name => 'toto' } ), 'format_for_imap_arg { name => toto }' ) ;
+	ok( '("name" "toto" "key" "val")' eq format_for_imap_arg( { name => 'toto', key => 'val' } ), 'format_for_imap_arg 2 x key val' ) ;
+	return ;
+}
+
+sub quota {
+	my ( $imap, $side, $sync ) = @_ ;
+
+        my $Side = ucfirst $side ;
+	my $debug_before = $imap->Debug(  ) ;
+	$imap->Debug( 1 ) ;
+	if ( not $imap->has_capability( 'QUOTA' ) ) {
+        	$imap->Debug( $debug_before ) ;
+        	return ;
+        } ;
+	myprint( "\n$Side: found quota, presented in raw IMAP\n"  ) ;
+	my $getquotaroot = $imap->getquotaroot( 'INBOX' ) ;
+        # Gmail INBOX quotaroot is "" but with it Mail::IMAPClient does a literal GETQUOTA {2} \n ""
+        #$imap->quota( 'ROOT' ) ;
+        #$imap->quota( '""' ) ;
+	myprint( "\n"  ) ;
+	$imap->Debug( $debug_before ) ;
+        my $quota_limit_bytes   = quota_extract_storage_limit_in_bytes( $getquotaroot ) ;
+        my $quota_current_bytes = quota_extract_storage_current_in_bytes( $getquotaroot ) ;
+        $sync->{$side}->{quota_limit_bytes}   = $quota_limit_bytes ;
+        $sync->{$side}->{quota_current_bytes} = $quota_current_bytes ;
+        my $quota_percent ;
+        if ( $quota_limit_bytes > 0 ) {
+        	$quota_percent = mysprintf( '%.2f', $NUMBER_100 * $quota_current_bytes / $quota_limit_bytes ) ;
+        }else{
+        	$quota_percent = 0 ;
+        }
+        myprint( "$Side: Quota current storage is $quota_current_bytes bytes. Limit is $quota_limit_bytes bytes. So $quota_percent % full\n"  ) ;
+        if ( $QUOTA_PERCENT_LIMIT < $quota_percent ) {
+        	my $error = "$Side: $quota_percent % full: it is time to find a bigger place! ( $quota_current_bytes bytes / $quota_limit_bytes bytes )\n" ;
+                errors_incr( $sync, $error ) ;
+        }
+	return ;
+}
+
+sub tests_quota_extract_storage_limit_in_bytes {
+	my $imap_output = [
+	'* QUOTAROOT "INBOX" "Storage quota" "Messages quota"',
+        '* QUOTA "Storage quota" (STORAGE 1 104857600)',
+        '* QUOTA "Messages quota" (MESSAGE 2 100000)',
+        '5 OK Getquotaroot completed.'
+	] ;
+        ok( $NUMBER_104857600 * $KIBI == quota_extract_storage_limit_in_bytes( $imap_output ), 'quota_extract_storage_limit_in_bytes ') ;
+        return ;
+}
+
+sub quota_extract_storage_limit_in_bytes {
+	my $imap_output = shift ;
+
+        my $limit_kb ;
+        $limit_kb = ( map { /.*\(\s*STORAGE\s+\d+\s+(\d+)\s*\)/ ? $1 : () } @{ $imap_output } )[0] ;
+        $limit_kb ||= 0 ;
+        $debug and myprint( "storage_limit_kb = $limit_kb\n"  ) ;
+        return( $KIBI * $limit_kb ) ;
+}
+
+
+sub tests_quota_extract_storage_current_in_bytes {
+	my $imap_output = [
+	'* QUOTAROOT "INBOX" "Storage quota" "Messages quota"',
+        '* QUOTA "Storage quota" (STORAGE 1 104857600)',
+        '* QUOTA "Messages quota" (MESSAGE 2 100000)',
+        '5 OK Getquotaroot completed.'
+	] ;
+        ok( 1*$KIBI == quota_extract_storage_current_in_bytes( $imap_output ), 'quota_extract_storage_current_in_bytes: 1 => 1024 ') ;
+        return ;
+}
+
+sub quota_extract_storage_current_in_bytes {
+	my $imap_output = shift ;
+
+        my $current_kb ;
+        $current_kb = ( map { /.*\(\s*STORAGE\s+(\d+)\s+\d+\s*\)/ ? $1 : () } @{ $imap_output } )[0] ;
+        $current_kb ||= 0 ;
+        $debug and myprint( "storage_current_kb = $current_kb\n"  ) ;
+        return( $KIBI * $current_kb ) ;
+
+}
+
+
+sub automap {
+	my ( $sync ) = @_ ;
+
+	if ( $sync->{automap} ) {
+		myprint( "Turned on automapping folders ( use --noautomap to turn off automapping )\n"  ) ;
+	}else{
+		myprint( "Turned off automapping folders ( use --automap to turn on automapping )\n"  ) ;
+		return ;
+	}
+
+        $sync->{h1_special} = special_from_folders_hash( $sync->{imap1}, 'Host1' ) ;
+        $sync->{h2_special} = special_from_folders_hash( $sync->{imap2}, 'Host2' ) ;
+
+	build_possible_special( $sync ) ;
+        build_guess_special(  $sync ) ;
+	build_automap( $sync ) ;
+
+	return ;
+}
+
+
+
+
+sub build_guess_special {
+	my ( $sync ) = shift ;
+
+        foreach my $h1_fold ( sort keys  %{ $sync->{h1_folders_all} }  ) {
+        	my $special = guess_special( $h1_fold, $sync->{possible_special}, $sync->{h1_prefix} ) ;
+        	if ( $special ) {
+                	$sync->{h1_special_guessed}{$h1_fold} = $special ;
+                        my $already_guessed = $sync->{h1_special_guessed}{$special} ;
+                        if ( $already_guessed ) {
+                        	myprint( "Host1: $h1_fold not $special because set to $already_guessed\n"  ) ;
+                        }else{
+	                        $sync->{h1_special_guessed}{$special} = $h1_fold ;
+                        }
+                }
+        }
+        foreach my $h2_fold ( sort keys  %{ $sync->{h2_folders_all} }  ) {
+        	my $special = guess_special( $h2_fold, $sync->{possible_special}, $sync->{h2_prefix} ) ;
+        	if ( $special ) {
+                	$sync->{h2_special_guessed}{$h2_fold} = $special ;
+                        my $already_guessed = $sync->{h2_special_guessed}{$special} ;
+                        if ( $already_guessed ) {
+                        	myprint( "Host2: $h2_fold not $special because set to $already_guessed\n"  ) ;
+                        }else{
+	                        $sync->{h2_special_guessed}{$special} = $h2_fold ;
+                        }
+                }
+        }
+        return ;
+}
+
+sub guess_special {
+	my( $folder, $possible_special_ref, $prefix ) = @_ ;
+
+        my $folder_no_prefix = $folder ;
+        $folder_no_prefix =~ s/${prefix}// ;
+        #$debug and myprint( "folder_no_prefix: $folder_no_prefix\n"  ) ;
+
+        my $guess_special = $possible_special_ref->{ $folder }
+        	|| $possible_special_ref->{ $folder_no_prefix }
+        	|| q{} ;
+
+        return( $guess_special ) ;
+}
+
+sub tests_guess_special {
+	my $possible_special_ref = build_possible_special( my $sync ) ;
+        ok( '\Sent' eq guess_special( 'Sent', $possible_special_ref, q{} ) ,'guess_special: Sent => \Sent' ) ;
+        ok( q{} eq guess_special( 'Blabla', $possible_special_ref, q{} ) ,'guess_special: Blabla => q{}' ) ;
+        ok( '\Sent' eq guess_special( 'INBOX.Sent', $possible_special_ref, 'INBOX.' ) ,'guess_special: INBOX.Sent => \Sent' ) ;
+	return ;
+}
+
+sub build_automap {
+	my ( $sync ) = @_ ;
+
+	foreach my $h1_fold ( @{ $sync->{h1_folders_wanted} } ) {
+		my $h2_fold ;
+		my $h1_special = $sync->{h1_special}{$h1_fold} ;
+                my $h1_special_guessed = $sync->{h1_special_guessed}{$h1_fold} ;
+
+		# Case 1: special on both sides.
+		if ( $h1_special
+                     and exists  $sync->{h2_special}{$h1_special}  ) {
+			$h2_fold = $sync->{h2_special}{$h1_special} ;
+			$sync->{f1f2auto}{ $h1_fold } = $h2_fold ;
+			next ;
+		}
+		# Case 2: special on host1, not on host2
+		if ( $h1_special
+                     and ( not exists  $sync->{h2_special}{$h1_special}  )
+                     and ( exists  $sync->{h2_special_guessed}{$h1_special}  )
+                   ) {
+			# special_guessed on host2
+                        $h2_fold = $sync->{h2_special_guessed}{$h1_special} ;
+                        $sync->{f1f2auto}{ $h1_fold } = $h2_fold ;
+			next ;
+		}
+		# Case 3: no special on host1, special on host2
+                if ( ( not $h1_special )
+                     and ( $h1_special_guessed )
+                     and ( exists  $sync->{h2_special}{$h1_special_guessed}  )
+                ) {
+                	$h2_fold = $sync->{h2_special}{$h1_special_guessed} ;
+                        $sync->{f1f2auto}{ $h1_fold } = $h2_fold ;
+			next ;
+                }
+                # Case 4: no special on both sides.
+                if ( ( not $h1_special )
+                     and ( $h1_special_guessed )
+                     and ( not exists  $sync->{h2_special}{$h1_special_guessed}  )
+                     and ( exists  $sync->{h2_special_guessed}{$h1_special_guessed}  )
+                ) {
+                	$h2_fold = $sync->{h2_special_guessed}{$h1_special_guessed} ;
+                        $sync->{f1f2auto}{ $h1_fold } = $h2_fold ;
+			next ;
+                }
+	}
+	return( $sync->{f1f2auto} ) ;
+}
+
+# I willll probably add what there is at:
+# http://stackoverflow.com/questions/2185391/localized-gmail-imap-folders/2185548#2185548
+sub build_possible_special {
+	my $sync = shift ;
+	my $possible_special = { } ;
+	# All|Archive|Drafts|Flagged|Junk|Sent|Trash
+
+	$possible_special->{'\All'}     = [ 'All', 'All Messages', '&BBIEQQQ1-' ] ;
+	$possible_special->{'\Archive'} = [ 'Archive', 'Archives', '&BBAEQARFBDgEMg-' ] ;
+	$possible_special->{'\Drafts'}  = [ 'Drafts', '&BCcENQRABD0EPgQyBDgEOgQ4-' ] ;
+	$possible_special->{'\Flagged'} = [ 'Flagged', 'Starred', '&BB8EPgQ8BDUERwQ1BD0EPQRLBDU-' ] ;
+	$possible_special->{'\Junk'}    = [ 'Junk', 'Spam', '&BCEEPwQwBDw-' ] ;
+	$possible_special->{'\Sent'}    = [ 'Sent', 'Sent Messages', 'Sent Items',
+                                            'Gesendete Elemente', 'Gesendete Objekte',
+                                            '&AMk-l&AOk-ments envoy&AOk-s', 'Envoy&AOk-',
+                                            'Elementos enviados',
+                                            '&kAFP4W4IMH8wojCkMMYw4A-',
+                                            '&BB4EQgQ,BEAEMAQyBDsENQQ9BD0ESwQ1-'] ;
+	$possible_special->{'\Trash'}   = [ 'Trash', '&BCMENAQwBDsENQQ9BD0ESwQ1-', '&BBoEPgRABDcEOAQ9BDA-' ] ;
+
+	foreach my $special ( qw( \All \Archive \Drafts \Flagged \Junk \Sent \Trash ) ){
+		foreach my $possible_folder ( @{ $possible_special->{$special} } ) {
+			$possible_special->{ $possible_folder } = $special ;
+		} ;
+	}
+        $sync->{possible_special} = $possible_special ;
+	$debug and myprint( Data::Dumper->Dump( [ $possible_special ], [ 'possible_special' ] )  ) ;
+        return( $possible_special ) ;
+}
+
+sub special_from_folders_hash {
+	my ( $imap, $side ) = @_ ;
+	my %special = (  ) ;
+        if ( not( Mail::IMAPClient->can( 'folders_hash' ) ) ) {
+        	my $error =  "$side: To have automagic rfc6154 folder mapping, upgrade Mail::IMAPClient >= 3.34\n" ;
+                errors_incr( $sync, $error ) ;
+                return( \%special ) ; # empty hash ref
+        }
+	my $folders_hash = $imap->folders_hash(  ) ;
+	foreach my $fhash (@{ $folders_hash } ) {
+			my @special =  grep { /\\(?:All|Archive|Drafts|Flagged|Junk|Sent|Trash)/ } @{ $fhash->{attrs} }  ;
+			if ( @special ) {
+				my $special = $special[0] ; # keep first one. Could be not very good.
+				if ( exists  $special{ $special }  ) {
+					myprintf( "%s: special %-20s = %s already asigned to %s\n",
+					        $side, $fhash->{name}, join( q{ }, @special ), $special{ $special } ) ;
+				}else{
+					myprintf( "%s: special %-20s = %s\n",
+					        $side, $fhash->{name}, join( q{ }, @special ) ) ;
+					$special{ $special } = $fhash->{name} ;
+					$special{ $fhash->{name} } = $special ; # double entry value => key
+				}
+			}
+		}
+        myprint( "\n" ) if ( %special ) ;
+	return( \%special ) ;
+}
+
+sub errors_incr {
+	my ( $mysync, @error ) = @ARG ;
+	$sync->{nb_errors}++ ;
+        
+        if ( @error ) {
+		errors_log( $mysync, @error ) ;
+                myprint( @error ) ;
+        }
+        
+        $mysync->{errorsmax} ||= $ERRORS_MAX ;
+	if ( $sync->{nb_errors} >= $mysync->{errorsmax} ) {
+		myprint( "Maximum number of errors $mysync->{errorsmax} reached ( you can change $mysync->{errorsmax} to 100 with --errorsmax 100 ). Exiting.\n"  ) ;
+                if ( $mysync->{errorsdump} ) {
+                        myprint( errorsdump( $sync->{nb_errors}, errors_log( $mysync ) ) ) ;
+                        # again since errorsdump(  ) can be very verbose and masq previous warning
+		        myprint( "Maximum number of errors $mysync->{errorsmax} reached ( you can change $mysync->{errorsmax} to 100 with --errorsmax 100 ). Exiting.\n"  ) ;
+		}
+                exit_clean( $mysync, $EXIT_WITH_ERRORS_MAX ) ;
+	}
+	return ;
+}
+
+sub errors_log {
+        my ( $mysync, @error ) = @ARG ;
+
+        if ( ! $mysync->{errors_log} ) {
+                $mysync->{errors_log} = [] ;
+        }
+
+        if ( @error ) {
+		push  @{ $mysync->{errors_log} }, join( q{}, @error  ) ;
+        }
+        if ( @{ $mysync->{errors_log} } ) {
+                return @{ $mysync->{errors_log} } ;
+        }
+        else {
+                return ;
+        }
+}
+
+sub tests_errors_log {
+
+
+}
+
+
+sub errorsdump {
+        my( $nb_errors, @errors_log ) = @ARG ;
+	my $error_num = 0 ;
+	my $errors_list = q{} ;
+	if ( @errors_log ) {
+		$errors_list = "++++ Listing $nb_errors errors encountered during the sync ( avoid this listing with --noerrorsdump ).\n" ;
+		foreach my $error ( @errors_log ) {
+			$error_num++ ;
+			$errors_list .= "Err $error_num/$nb_errors: $error" ;
+		}
+	}
+	return( $errors_list ) ;
+}
+
+
+sub tests_live_result {
+	my $nb_errors = shift ;
+	if ( $nb_errors  ) {
+		myprint( "Live tests failed with $nb_errors errors\n"  ) ;
+	} else {
+		myprint( "Live tests ended successfully\n"  ) ;
+	}
+	return ;
+}
+
+sub foldersizesatend {
+	timenext(  ) ;
+	return if ( $imap1->IsUnconnected(  ) ) ;
+	return if ( $imap2->IsUnconnected(  ) ) ;
+	# Get all folders on host2 again since new were created
+	@h2_folders_all = sort $imap2->folders();
+	for ( @h2_folders_all ) {
+        	$h2_folders_all{ $_ } = 1 ;
+        	$h2_folders_all_UPPER{ uc  $_  } = 1 ;
+        } ;
+	( $h1_nb_msg_end, $h1_bytes_end ) = foldersizes( 'Host1', $imap1, $search1, @h1_folders_wanted ) ;
+	( $h2_nb_msg_end, $h2_bytes_end ) = foldersizes( 'Host2', $imap2, $search2, @h2_folders_from_1_wanted ) ;
+        if ( not all_defined( $h1_nb_msg_end, $h1_bytes_end, $h2_nb_msg_end, $h2_bytes_end ) ) {
+                my $error = "Failure getting foldersizes, final differences will not be calculated\n" ;
+                errors_incr( $sync, $error ) ;
+        }
+	return ;
+}
+
+sub size_filtered_flag {
+	my $h1_size = shift ;
+
+	if (defined $maxsize and $h1_size >= $maxsize) {
+		return( 1 ) ;
+	}
+	if (defined $minsize and $h1_size <= $minsize) {
+		return( 1 ) ;
+	}
+	return( 0 ) ;
+}
+
+sub sync_flags_fir {
+	my ( $h1_fold, $h1_msg, $h2_fold, $h2_msg, $permanentflags2, $h1_fir_ref, $h2_fir_ref ) = @_ ;
+
+	if ( not defined  $h1_msg  ) { return } ;
+	if ( not defined  $h2_msg  ) { return } ;
+
+	my $h1_size = $h1_fir_ref->{$h1_msg}->{'RFC822.SIZE'} ;
+	return if size_filtered_flag( $h1_size ) ;
+
+	# used cached flag values for efficiency
+	my $h1_flags = $h1_fir_ref->{ $h1_msg }->{ 'FLAGS' } || q{} ;
+	my $h2_flags = $h2_fir_ref->{ $h2_msg }->{ 'FLAGS' } || q{} ;
+
+	sync_flags( $h1_fold, $h1_msg, $h1_flags, $h2_fold, $h2_msg, $h2_flags, $permanentflags2 ) ;
+
+        return ;
+}
+
+sub sync_flags_after_copy {
+	my( $h1_fold, $h1_msg, $h1_flags, $h2_fold, $h2_msg, $permanentflags2 ) = @_ ;
+
+        my @h2_flags = $imap2->flags( $h2_msg ) ;
+        my $h2_flags = "@h2_flags" ;
+        ( $debug or $debugflags ) and myprint( "Host2 flags before resync by STORE on msg $h2_msg: $h2_flags\n"  ) ;
+	sync_flags( $h1_fold, $h1_msg, $h1_flags, $h2_fold, $h2_msg, $h2_flags, $permanentflags2 ) ;
+        return ;
+}
+
+sub sync_flags {
+	my( $h1_fold, $h1_msg, $h1_flags, $h2_fold, $h2_msg, $h2_flags, $permanentflags2 ) = @_ ;
+
+	( $debug or $debugflags ) and
+        myprint( "Host1: flags init msg $h1_fold/$h1_msg flags( $h1_flags ) Host2 $h2_fold/$h2_msg flags( $h2_flags )\n"  ) ;
+
+	$h1_flags = flags_for_host2( $h1_flags, $permanentflags2 ) ;
+
+	$h2_flags = flagscase( $h2_flags ) ;
+
+	( $debug or $debugflags ) and
+        myprint( "Host1 flags filt msg $h1_fold/$h1_msg flags( $h1_flags ) Host2 $h2_fold/$h2_msg flags( $h2_flags )\n"  ) ;
+
+
+	# compare flags - set flags if there a difference
+	my @h1_flags = sort split(q{ }, $h1_flags );
+	my @h2_flags = sort split(q{ }, $h2_flags );
+	my $diff = compare_lists( \@h1_flags, \@h2_flags );
+
+	$diff and ( $debug or $debugflags )
+		and     myprint( "Host2 flags msg $h2_fold/$h2_msg replacing h2 flags( $h2_flags ) with h1 flags( $h1_flags )\n" ) ;
+	# This sets flags so flags can be removed with this
+	# When you remove a \Seen flag on host1 you want to it
+	# to be removed on host2. Just add flags is not what
+	# we need most of the time.
+
+	if ( not $dry and $diff and not $imap2->store( $h2_msg, "FLAGS.SILENT (@h1_flags)" ) ) {
+		my $error_msg = join q{}, "Host2 flags msg $h2_fold/$h2_msg could not add flags [@h1_flags]: ",
+		  $imap2->LastError || q{}, "\n" ;
+		errors_incr( $sync, $error_msg ) ;
+	}
+
+        return ;
+}
+
+
+
+sub _filter {
+	my $str = shift or return q{} ;
+        my $sz  = $SIZE_MAX_STR ;
+        my $len = length $str ;
+        if ( not $debug and $len > $sz*2 ) {
+                my $beg = substr $str, 0, $sz ;
+                my $end = substr $str, -$sz, $sz ;
+                $str = $beg . '...' . $end ;
+        }
+        $str =~ s/\012?\015$//x ;
+        return "(len=$len) " . $str ;
+}
+
+
+
+sub lost_connection {
+	my( $imap, $error_message ) = @_;
+        if ( $imap->IsUnconnected(  ) ) {
+                $sync->{nb_errors}++ ;
+                my $lcomm = $imap->LastIMAPCommand || q{} ;
+                my $einfo = $imap->LastError || @{$imap->History}[$LAST] || q{} ;
+
+                # if string is long try reduce to a more reasonable size
+                $lcomm = _filter( $lcomm ) ;
+                $einfo = _filter( $einfo ) ;
+                myprint( "Failure: last command: $lcomm\n") if ($debug && $lcomm) ;
+                myprint( "Failure: lost connection $error_message: ", $einfo, "\n") ;
+                return( 1 ) ;
+        }
+        else{
+        	return( 0 ) ;
+        }
+}
+
+sub max {
+	my @list = @_ ;
+	return( undef ) if ( 0 == scalar  @list  ) ;
+	my @sorted = sort { $a <=> $b } @list ;
+	return( pop @sorted ) ;
+}
+
+sub tests_max {
+	ok( 0  == max( 0 ),  'max 0' ) ;
+	ok( 1  == max( 1 ),  'max 1' ) ;
+	ok( $MINUS_ONE == max( $MINUS_ONE ), 'max -1') ;
+	ok( not ( defined max(  ) ), 'max no arg' ) ;
+	ok( $NUMBER_100 == max( 1, $NUMBER_100 ), 'max 1 100' ) ;
+	ok( $NUMBER_100 == max( $NUMBER_100, 1 ), 'max 100 1' ) ;
+	ok( $NUMBER_100 == max( $NUMBER_100, $NUMBER_42, 1 ), 'max 100 42 1' ) ;
+	ok( $NUMBER_100 == max( $NUMBER_100, '42', 1 ), 'max 100 42 1' ) ;
+	ok( $NUMBER_100 == max( '100', '42', 1 ), 'max 100 42 1' ) ;
+	#ok( 100 == max( 100, 'haha', 1 ), 'max 100 42 1') ;
+        return ;
+}
+
+
+sub check_lib_version {
+	$debug and myprint( "IMAPClient $Mail::IMAPClient::VERSION\n"  ) ;
+	if ( '2.2.9' eq $Mail::IMAPClient::VERSION ) {
+		myprint( "imapsync no longer supports Mail::IMAPClient 2.2.9, upgrade it\n"  ) ;
+		return 0 ;
+	}
+	else{
+		# 3.x.x is no longer buggy with imapsync.
+                # 3.30 or currently superior is imposed in the Perl "use Mail::IMAPClient line".
+		return 1 ;
+	}
+        return ;
+}
+
+sub module_version_str {
+	my( $module_name, $module_version ) = @_ ;
+	my $str = mysprintf( "%-20s %s\n", $module_name, $module_version ) ;
+        return( $str ) ;
+}
+
+sub modulesversion {
+
+	my @list_version;
+
+	my $v ;
+	eval { require Mail::IMAPClient; $v = $Mail::IMAPClient::VERSION } or $v = q{?} ;
+	push  @list_version, module_version_str( 'Mail::IMAPClient', $v )  ;
+
+	eval { require IO::Socket; $v = $IO::Socket::VERSION } or $v = q{?} ;
+	push  @list_version, module_version_str( 'IO::Socket', $v )  ;
+
+	eval { require IO::Socket::INET; $v = $IO::Socket::INET::VERSION } or $v = q{?} ;
+	push  @list_version, module_version_str( 'IO::Socket::INET', $v )  ;
+
+	eval { require IO::Socket::INET6; $v = $IO::Socket::INET6::VERSION } or $v = q{?} ;
+	push  @list_version, module_version_str( 'IO::Socket::INET6', $v )  ;
+
+	eval { require IO::Socket::SSL ; $v = $IO::Socket::SSL::VERSION } or $v = q{?} ;
+	push  @list_version, module_version_str( 'IO::Socket::SSL ', $v )  ;
+
+	eval { require Net::SSLeay ; $v = $Net::SSLeay::VERSION } or $v = q{?} ;
+	push  @list_version, module_version_str( 'Net::SSLeay ', $v )  ;
+
+	eval { require Compress::Zlib; $v = $Compress::Zlib::VERSION } or $v = q{?} ;
+	push  @list_version, module_version_str( 'Compress::Zlib', $v )  ;
+
+	eval { require Digest::MD5; $v = $Digest::MD5::VERSION } or $v = q{?} ;
+	push  @list_version, module_version_str( 'Digest::MD5', $v )  ;
+
+	eval { require Digest::HMAC_MD5; $v = $Digest::HMAC_MD5::VERSION } or $v = q{?} ;
+	push  @list_version, module_version_str( 'Digest::HMAC_MD5', $v )  ;
+
+	eval { require Digest::HMAC_SHA1; $v = $Digest::HMAC_SHA1::VERSION } or $v = q{?} ;
+	push  @list_version, module_version_str( 'Digest::HMAC_SHA1', $v )  ;
+
+	eval { require Term::ReadKey; $v = $Term::ReadKey::VERSION } or $v = q{?} ;
+	push  @list_version, module_version_str( 'Term::ReadKey', $v )  ;
+
+	eval { require File::Spec; $v = $File::Spec::VERSION } or $v = q{?} ;
+	push  @list_version, module_version_str( 'File::Spec', $v )  ;
+
+	eval { require Time::HiRes; $v = $Time::HiRes::VERSION } or $v = q{?} ;
+	push  @list_version, module_version_str( 'Time::HiRes', $v )  ;
+
+	eval { require Unicode::String; $v = $Unicode::String::VERSION } or $v = q{?} ;
+	push  @list_version, module_version_str( 'Unicode::String', $v )  ;
+
+	eval { require IO::Tee; $v = $IO::Tee::VERSION } or $v = q{?} ;
+	push  @list_version, module_version_str( 'IO::Tee', $v )  ;
+
+	eval { require File::Copy::Recursive; $v = $File::Copy::Recursive::VERSION } or $v = q{?} ;
+	push  @list_version, module_version_str( 'File::Copy::Recursive', $v )  ;
+
+	eval { require Authen::NTLM; $v = $Authen::NTLM::VERSION } or $v = q{?} ;
+	push  @list_version, module_version_str( 'Authen::NTLM', $v )  ;
+
+	eval { require URI::Escape; $v = $URI::Escape::VERSION } or $v = q{?} ;
+	push  @list_version, module_version_str( 'URI::Escape', $v )  ;
+
+	eval { require Data::Uniqid; $v = $Data::Uniqid::VERSION } or $v = q{?} ;
+	push  @list_version, module_version_str( 'Data::Uniqid', $v )  ;
+
+	eval { require JSON; $v = $JSON::VERSION } or $v = q{?} ;
+	push  @list_version, module_version_str( 'JSON', $v )  ;
+
+	eval { require JSON::WebToken; $v = $JSON::WebToken::VERSION } or $v = q{?} ;
+	push  @list_version, module_version_str( 'JSON::WebToken', $v )  ;
+
+	eval { require Crypt::OpenSSL::RSA; $v = $Crypt::OpenSSL::RSA::VERSION } or $v = q{?} ;
+	push  @list_version, module_version_str( 'Crypt::OpenSSL::RSA', $v )  ;
+
+	eval { require LWP; $v = $LWP::VERSION } or $v = q{?} ;
+	push  @list_version, module_version_str( 'LWP', $v )  ;
+
+	eval { require HTML::Entities; $v = $HTML::Entities::VERSION } or $v = q{?} ;
+	push  @list_version, module_version_str( 'HTML::Entities', $v )  ;
+
+	#eval { require Filesys::DfPortable; $v = $Filesys::DfPortable::VERSION } or $v = q{?} ;
+	#push  @list_version, module_version_str( 'Filesys::DfPortable', $v )  ;
+
+	eval { require Getopt::Long; $v = $Getopt::Long::VERSION } or $v = q{?} ;
+	push  @list_version, module_version_str( 'Getopt::Long', $v )  ;
+
+	eval { require Test::MockObject; $v = $Test::MockObject::VERSION } or $v = q{?} ;
+	push  @list_version, module_version_str( 'Test::MockObject', $v )  ;
+
+	return( @list_version ) ;
+}
+
+
+# Construct a command line copy with passwords replaced by MASKED.
+sub command_line_nopassword {
+	my @argv = @_ ;
+	my @argv_nopassword ;
+
+        return( "@argv" ) if $showpasswords ;
+	while ( @argv ) {
+		my $arg = shift @argv ; # option name or value
+		if ( $arg =~ m/-password[12]/x ) {
+			shift @argv ; # password value
+			push  @argv_nopassword, $arg, 'MASKED'  ; # option name and fake value
+		}else{
+			push  @argv_nopassword, $arg ; # same option or value
+		}
+	}
+	return("@argv_nopassword") ;
+}
+
+sub tests_command_line_nopassword {
+
+	ok(q{} eq command_line_nopassword(), 'command_line_nopassword void');
+	ok('--blabla' eq command_line_nopassword('--blabla'), 'command_line_nopassword --blabla');
+	#myprint( command_line_nopassword((qw{ --password1 secret1 })), "\n" ) ;
+	ok('--password1 MASKED' eq command_line_nopassword(qw{ --password1 secret1}), 'command_line_nopassword --password1');
+	ok('--blabla --password1 MASKED --blibli'
+	eq command_line_nopassword(qw{ --blabla --password1 secret1 --blibli }), 'command_line_nopassword --password1 --blibli');
+	$showpasswords = 1 ;
+	ok(q{} eq command_line_nopassword(), 'command_line_nopassword void');
+	ok('--blabla' eq command_line_nopassword('--blabla'), 'command_line_nopassword --blabla');
+	#myprint( command_line_nopassword((qw{ --password1 secret1 })), "\n" ) ;
+	ok('--password1 secret1' eq command_line_nopassword(qw{ --password1 secret1}), 'command_line_nopassword --password1');
+	ok('--blabla --password1 secret1 --blibli'
+	eq command_line_nopassword(qw{ --blabla --password1 secret1 --blibli }), 'command_line_nopassword --password1 --blibli');
+        return ;
+}
+
+sub ask_for_password {
+	my ( $user, $host ) = @_ ;
+	myprint( "What's the password for $user" . '@' . "$host? (not visible while you type, then enter RETURN) "  ) ;
+	Term::ReadKey::ReadMode( 2 ) ;
+	my $password = <> ;
+	chomp $password ;
+	myprint( "\nGot it\n" ) ;
+	Term::ReadKey::ReadMode( 0 ) ;
+	return $password ;
+}
+
+sub catch_exit {
+        my $mysync = shift ;
+        my $signame = shift ;
+        if ( $signame ) {
+                myprint( "\nGot a signal $signame\n" ) ;
+        }
+	stats( $mysync ) ;
+        myprint( "Ended by a signal\n" ) ;
+	exit_clean( $mysync, $EXIT_BY_SIGNAL ) ;
+        return ;
+}
+
+sub catch_reconnect {
+	my $mysync = shift ;
+        my $signame = shift ;
+        myprint( "\nGot a signal $signame\n",
+                "Hit 2 ctr-c within 2 seconds to exit the program\n",
+                "Hit only 1 ctr-c to reconnect to both imap servers\n",
+        ) ;
+        if ( here_twice( $mysync ) ) {
+                myprint( "Got two signals $signame within $INTERVAL_TO_EXIT seconds. Exiting...\n" ) ;
+                catch_exit( $mysync ) ;
+        }
+        else{
+                myprint( "For now only one signal $signame within $INTERVAL_TO_EXIT seconds.\n" ) ;
+        }
+
+        if ( ! defined $mysync->{imap1} ) { return ; }
+        if ( ! defined $mysync->{imap2} ) { return ; }
+        
+
+        myprint( "Info: reconnecting to host1 imap server\n" ) ;
+        $mysync->{imap1}->State( Mail::IMAPClient::Unconnected ) ;
+        $mysync->{imap1}->reconnect(  ) ;
+        myprint( "Info: reconnecting to host2 imap server\n" ) ;
+        $mysync->{imap2}->State( Mail::IMAPClient::Unconnected ) ;
+        $mysync->{imap2}->reconnect(  ) ;
+        myprint( "Info: reconnected to both imap servers\n" ) ;
+        return ;
+}
+
+sub here_twice {
+        my $mysync = shift ;
+        my $now = time ;
+        my $previous = $mysync->{lastcatch} || 0 ;
+        $mysync->{lastcatch} = $now ;
+        
+        if ( $INTERVAL_TO_EXIT >= $now - $previous ) {
+                return $TRUE ;
+        }else{
+                return $FALSE ;
+        }
+}
+
+
+
+
+sub justconnect {
+
+	$imap1 = connect_imap( $host1, $port1, $debugimap1, $ssl1, $tls1, 'Host1', $sync->{h1}->{timeout}, $sync->{h1} ) ;
+	myprint( 'Host1 banner: ', $imap1->Banner(  )  ) ;
+	myprint( 'Host1 capability: ', join(q{ }, $imap1->capability(  ) ), "\n"  ) ;
+	$imap2 = connect_imap( $host2, $port2, $debugimap2, $ssl2, $tls2, 'Host2', $sync->{h2}->{timeout}, $sync->{h2} ) ;
+	myprint( 'Host2 banner: ', $imap2->Banner(  )  ) ;
+	myprint( 'Host2 capability: ', join(q{ }, $imap2->capability(  ) ), "\n"  ) ;
+	$imap1->logout(  ) ;
+	$imap2->logout(  ) ;
+        return ;
+}
+
+sub connect_imap {
+	my( $host, $port, $mydebugimap, $ssl, $tls, $Side, $mytimeout, $h ) = @_ ;
+	my $imap = Mail::IMAPClient->new() ;
+	if ( $ssl ) { set_ssl( $imap, $h ) }
+	if ( $tls ) { $imap->Tls( 1 ) }
+	$imap->Server( $host ) ;
+	$imap->Port( $port ) ;
+	$imap->Debug( $mydebugimap ) ;
+        $imap->Timeout( $mytimeout ) ;
+	$imap->connect(  )
+	  or die_clean( "$Side: Can not open imap connection on [$host]: $@\n" ) ;
+
+        my $banner = $imap->Results()->[0] ;
+        $imap->Banner( $banner ) ;
+
+        if ( $imap->Tls(  ) ) {
+        	set_tls( $imap, $h ) ;
+        	$imap->starttls(  )
+                or die_clean("$Side: Can not go to tls encryption on [$host]:", $imap->LastError, "\n" ) ;
+                myprint( "$Side: Socket successfuly converted to SSL\n"  ) ;
+        }
+        return( $imap ) ;
+}
+
+
+sub login_imap {
+
+	my @allargs = @_ ;
+	my(
+		$host, $port, $user, $domain, $password,
+		$mydebugimap, $mytimeout, $fastio,
+		$ssl, $tls, $authmech, $authuser, $reconnectretry,
+		$proxyauth, $uid, $split, $Side, $h ) = @allargs ;
+
+	my $side = lc $Side ;
+	myprint( "$Side: connecting and login on $side [$host] port [$port] with user [$user]\n"  ) ;
+
+	my $imap = init_imap( @allargs ) ;
+
+	$imap->connect()
+	  or die_clean("$Side failure: can not open imap connection on $side [$host] with user [$user]: $@\n") ;
+
+        my $banner = $imap->Results()->[0] ;
+        $imap->Banner( $banner ) ;
+	myprint( "$Side banner: $banner"  ) ;
+
+        if ( $authmech eq 'PREAUTH' ) {
+        	if ( $imap->IsAuthenticated( ) ) {
+        		$imap->Socket ;
+			myprintf("%s: Assuming PREAUTH for %s\n", $Side, $imap->Server ) ;
+        	}else{
+                	die_clean( "$Side failure: error login on $side [$host] with user [$user] auth [PREAUTH]" ) ;
+                }
+        }
+
+        if ( $imap->Tls(  ) ) {
+		set_tls( $imap, $h ) ;
+        	$imap->starttls(  )
+                or die_clean("$Side failure: Can not go to tls encryption on $side [$host]:", $imap->LastError, "\n" ) ;
+                myprint( "$Side: Socket successfuly converted to SSL\n"  ) ;
+        }
+
+        authenticate_imap( $imap, @allargs ) ;
+
+	myprint( "$Side: success login on [$host] with user [$user] auth [$authmech]\n"  ) ;
+	return( $imap ) ;
+}
+
+
+sub authenticate_imap {
+
+	my($imap,
+           $host, $port, $user, $domain, $password,
+	   $mydebugimap, $mytimeout, $fastio,
+	   $ssl, $tls, $authmech, $authuser, $reconnectretry,
+	   $proxyauth, $uid, $split, $Side, $h ) = @_ ;
+
+	check_capability( $imap, $authmech, $Side ) ;
+
+        if ( $proxyauth ) {
+                $imap->Authmechanism(q{}) ;
+                $imap->User($authuser) ;
+        } else {
+                $imap->Authmechanism( $authmech ) unless ( $authmech eq 'LOGIN'  or $authmech eq 'PREAUTH' ) ;
+                $imap->User($user) ;
+        }
+
+	$imap->Authcallback(\&xoauth)  if ( 'XOAUTH'  eq $authmech ) ;
+	$imap->Authcallback(\&xoauth2) if ( 'XOAUTH2' eq $authmech ) ;
+	$imap->Authcallback(\&plainauth) if ( ( 'PLAIN' eq $authmech ) or ( 'EXTERNAL' eq $authmech )  ) ;
+
+        $imap->Domain($domain) if (defined $domain) ;
+        $imap->Authuser($authuser) ;
+        $imap->Password($password) ;
+
+	unless ( $authmech eq 'PREAUTH' or $imap->login( ) ) {
+		my $info  = "$Side failure: Error login on [$host] with user [$user] auth" ;
+		my $einfo = $imap->LastError || @{$imap->History}[$LAST] ;
+		chomp $einfo ;
+		my $error = "$info [$authmech]: $einfo\n" ;
+                if ( $authmech eq 'LOGIN' or $imap->IsUnconnected(  ) or $authuser ) {
+                	die_clean( $error ) ;
+                }else{
+			myprint( $error  ) ;
+                }
+		myprint( "$Side info: trying LOGIN Auth mechanism on [$host] with user [$user]\n"  ) ;
+		$imap->Authmechanism(q{}) ;
+		$imap->login() or
+		  die_clean("$info [LOGIN]: ", $imap->LastError, "\n") ;
+	}
+
+        if ( $proxyauth ) {
+                if ( ! $imap->proxyauth( $user ) ) {
+                        my $info  = "$Side failure: Error doing proxyauth as user [$user] on [$host] using proxy-login as [$authuser]" ;
+                        my $einfo = $imap->LastError || @{$imap->History}[$LAST] ;
+                        chomp $einfo ;
+                        die_clean( "$info: $einfo\n" ) ;
+                }
+        }
+
+	return ;
+}
+
+sub check_capability {
+
+	my( $imap, $authmech, $Side ) = @_ ;
+
+	if ($imap->has_capability("AUTH=$authmech")
+	    or $imap->has_capability($authmech)
+	   ) {
+		myprintf("%s: %s says it has CAPABILITY for AUTHENTICATE %s\n",
+		       $Side, $imap->Server, $authmech);
+	}
+	else {
+		myprintf("%s: %s says it has NO CAPABILITY for AUTHENTICATE %s\n",
+		       $Side, $imap->Server, $authmech);
+		if ($authmech eq 'PLAIN') {
+			myprint( "$Side: frequently PLAIN is only supported with SSL, ",
+			  "try --ssl or --tls options\n" ) ;
+		}
+	}
+	return ;
+}
+
+sub set_ssl {
+	my ( $imap, $h ) = @_ ;
+        # SSL_version can be
+        #    SSLv3 SSLv2 SSLv23 SSLv23:!SSLv2 (last one is the default in IO-Socket-SSL-1.953)
+        #
+
+        my $sslargs_hash = $h->{sslargs} ;
+
+	my $sslargs_default = {
+		SSL_verify_mode => $DEFAULT_SSL_VERIFY,
+        	SSL_verifycn_scheme => 'imap',
+        } ;
+
+        # initiate with default values
+        my %sslargs_mix = %{ $sslargs_default } ;
+        # now override with passed values
+        @sslargs_mix{ keys %{ $sslargs_hash } } = values %{ $sslargs_hash } ;
+        # remove keys with undef values
+        foreach my $key ( keys %sslargs_mix ) {
+                delete $sslargs_mix{ $key } if ( not defined  $sslargs_mix{ $key }  ) ;
+        }
+        # back to an ARRAY
+        my @sslargs_mix = %sslargs_mix ;
+        #myprint( Data::Dumper->Dump( [ $sslargs_hash, $sslargs_default, \%sslargs_mix, \@sslargs_mix ] )  ) ;
+        $imap->Ssl( \@sslargs_mix ) ;
+	return ;
+}
+
+sub set_tls {
+	my ( $imap, $h ) = @_ ;
+
+        my $sslargs_hash = $h->{sslargs} ;
+
+	my $sslargs_default = {
+		SSL_verify_mode => $DEFAULT_SSL_VERIFY,
+        } ;
+
+        # initiate with default values
+        my %sslargs_mix = %{ $sslargs_default } ;
+        # now override with passed values
+        @sslargs_mix{ keys %{ $sslargs_hash } } = values %{ $sslargs_hash } ;
+        # remove keys with undef values
+        foreach my $key ( keys %sslargs_mix ) {
+                delete $sslargs_mix{ $key } if ( not defined  $sslargs_mix{ $key } ) ;
+        }
+        # back to an ARRAY
+        my @sslargs_mix = %sslargs_mix ;
+
+        $imap->Starttls( \@sslargs_mix ) ;
+	return ;
+}
+
+
+
+
+sub init_imap {
+	my(
+	   $host, $port, $user, $domain, $password,
+	   $mydebugimap, $mytimeout, $fastio,
+	   $ssl, $tls, $authmech, $authuser, $reconnectretry,
+	   $proxyauth, $uid, $split, $Side, $h ) = @_ ;
+
+	my ( $imap ) ;
+
+	$imap = Mail::IMAPClient->new() ;
+
+	if ( $ssl ) { set_ssl( $imap, $h ) }
+	if ( $tls ) { $imap->Tls( 1 ) } # can not do set_tls() here because connect() will directly do a STARTTLS
+	$imap->Clear(1);
+	$imap->Server($host);
+	$imap->Port($port);
+	$imap->Fast_io($fastio);
+	$imap->Buffer($buffersize || $DEFAULT_BUFFER_SIZE);
+	$imap->Uid($uid);
+
+	$imap->Peek(1);
+	$imap->Debug($mydebugimap);
+	defined  $mytimeout  and $imap->Timeout( $mytimeout ) ;
+
+	$imap->Reconnectretry( $reconnectretry ) if ( $reconnectretry ) ;
+	$imap->Ignoresizeerrors( $allowsizemismatch ) ;
+	$split and $imap->Maxcommandlength( $SPLIT_FACTOR * $split ) ;
+
+
+	return( $imap ) ;
+
+}
+
+sub plainauth {
+        my $code = shift;
+        my $imap = shift;
+
+        my $string = mysprintf("%s\x00%s\x00%s", $imap->User,
+                            $imap->Authuser, $imap->Password);
+        return encode_base64("$string", q{});
+}
+
+# Copy from https://github.com/imapsync/imapsync/pull/25/files
+# Changes "use" pragmas to "require".
+# The openssl system call shall be replaced by pure Perl and
+# https://metacpan.org/pod/Crypt::OpenSSL::PKCS12
+
+# Now the Joaquin Lopez code:
+#
+# Used this as an example: https://gist.github.com/gsainio/6322375
+#
+# And this as a reference: https://developers.google.com/accounts/docs/OAuth2ServiceAccount
+# (note there is an http/rest tab, where the real info is hidden away... went on a witch hunt
+# until I noticed that...)
+#
+# This is targeted at gmail to maintain compatibility after google's oauth1 service is deactivated
+# on May 5th, 2015: https://developers.google.com/gmail/oauth_protocol
+# If there are other oauth2 implementations out there, this would need to be modified to be
+# compatible
+#
+# This is a good guide on setting up the google api/apps side of the equation:
+# http://www.limilabs.com/blog/oauth2-gmail-imap-service-account
+#
+# 2016/05/27: Updated to support oauth/key data in the .json files Google now defaults to
+# when creating gmail service accounts. They're easier to work with since they neither
+# requiring decrypting nor specifying the oauth2 client id separately.
+#
+# If the password arg ends in .json, it will assume this new json method, otherwise it
+# will fallback to the "oauth client id;.p12" format it was previously using.
+sub xoauth2 {
+	require JSON::WebToken ;
+	require LWP::UserAgent ;
+	require HTML::Entities ;
+	require JSON ;
+	require JSON::WebToken::Crypt::RSA ;
+	require Crypt::OpenSSL::RSA ;
+        require Encode::Byte ;
+        require IO::Socket::SSL ;
+
+        my $code = shift;
+        my $imap = shift;
+
+        my ($iss,$key);
+
+        if( $imap->Password =~ /^(.*\.json)$/ ) {
+            my $json = JSON->new( ) ;
+            my $filename = $1;
+            $debug and myprint( "XOAUTH2 json file: $filename\n" ) ;
+            open( my $FILE, '<', $filename ) or die_clean( "error [$filename]: $! " ) ;
+            my $jsonfile = $json->decode( join q{}, <$FILE> ) ;
+            close $FILE ;
+
+            $iss = $jsonfile->{client_id};
+            $key = $jsonfile->{private_key};
+            $debug and myprint( "Service account: $iss\n");
+            $debug and myprint( "Private key:\n$key\n");
+        }
+        else {
+            # Get iss (service account address), keyfile name, and keypassword if necessary
+            ( $iss, my $keyfile, my $keypass ) = $imap->Password =~ /([\-\d\w\@\.]+);([a-zA-Z0-9 \_\-\.\/]+);?(.*)?/ ;
+
+            # Assume key password is google default if not provided
+            $keypass = 'notasecret' if not $keypass;
+
+            $debug and myprint( "Service account: $iss\nKey file: $keyfile\nKey password: $keypass\n");
+
+            # Get private key from p12 file (would be better in perl...)
+            $key = `openssl pkcs12 -in "$keyfile" -nodes -nocerts -passin pass:$keypass -nomacver`;
+
+            $debug and myprint( "Private key:\n$key\n");
+        }
+
+        # Create jwt of oauth2 request
+        my $time = time ;
+        my $jwt = JSON::WebToken->encode( {
+        'iss' => $iss, # service account
+        'scope' => 'https://mail.google.com/',
+        'aud' => 'https://www.googleapis.com/oauth2/v3/token',
+        'exp' => $time + $DEFAULT_EXPIRATION_TIME_OAUTH2_PK12,
+        'iat' => $time,
+        'prn' => $imap->User # user to auth as
+        },
+        $key, 'RS256', {'typ' => 'JWT'} ); # Crypt::OpenSSL::RSA needed here.
+
+        # Post oauth2 request
+        my $ua = LWP::UserAgent->new(  ) ;
+        $ua->env_proxy(  ) ;
+
+        my $response = $ua->post('https://www.googleapis.com/oauth2/v3/token',
+        { grant_type => HTML::Entities::encode_entities('urn:ietf:params:oauth:grant-type:jwt-bearer'),
+        assertion => $jwt } ) ;
+
+        unless( $response->is_success(  ) ) {
+                die_clean( $response->code, "\n", $response->content, "\n" ) ;
+        }else{
+                $debug and myprint( $response->content  ) ;
+        }
+
+        # access_token in response is what we need
+        my $data = JSON::decode_json( $response->content ) ;
+
+        # format as oauth2 auth data
+        my $xoauth2_string = encode_base64( 'user=' . $imap->User . "\1auth=Bearer " . $data->{access_token} . "\1\1", q{} ) ;
+
+        $debug and myprint( "XOAUTH2 String: $xoauth2_string\n");
+        return($xoauth2_string);
+}
+
+
+
+
+# xoauth() thanks to Eduardo Bortoluzzi Junior
+sub xoauth {
+        require URI::Escape  ;
+        require Data::Uniqid ;
+
+        my $code = shift;
+        my $imap = shift;
+
+        # The base information needed to construct the OAUTH authentication
+        my $method = 'GET' ;
+        my $url = mysprintf( 'https://mail.google.com/mail/b/%s/imap/', $imap->User ) ;
+        my $urlparm = mysprintf( 'xoauth_requestor_id=%s', URI::Escape::uri_escape( $imap->User ) ) ;
+
+        # For Google Apps, the consumer key is the primary domain
+        # TODO: create a command line argument to define the consumer key
+        my @user_parts = split /@/x, $imap->User ;
+        $debug and myprint( "XOAUTH: consumer key: $user_parts[1]\n" ) ;
+
+        # All the parameters needed to be signed on the XOAUTH
+        my %hash = ();
+        $hash { 'xoauth_requestor_id' } = URI::Escape::uri_escape($imap->User);
+        $hash { 'oauth_consumer_key' } = $user_parts[1];
+        $hash { 'oauth_nonce' } = md5_hex(Data::Uniqid::uniqid(rand(), 1==1));
+        $hash { 'oauth_signature_method' } = 'HMAC-SHA1';
+        $hash { 'oauth_timestamp' } = time ;
+        $hash { 'oauth_version' } = '1.0';
+
+        # Base will hold the string to be signed
+        my $base = "$method&" . URI::Escape::uri_escape( $url ) . q{&} ;
+
+        # The parameters must be in dictionary order before signing
+        my $baseparms = q{} ;
+        foreach my $key ( sort keys %hash ) {
+                if ( length( $baseparms ) > 0 ) {
+                        $baseparms .= q{&} ;
+                }
+
+                $baseparms .= "$key=$hash{$key}" ;
+        }
+
+        $base .= URI::Escape::uri_escape($baseparms);
+        $debug and myprint( "XOAUTH: base request to sign: $base\n" ) ;
+        # Sign it with the consumer secret, informed on the command line (password)
+        my $digest = hmac_sha1( $base, URI::Escape::uri_escape( $imap->Password ) . q{&} ) ;
+
+        # The parameters signed become a parameter and...
+        $hash { 'oauth_signature' } = URI::Escape::uri_escape( substr encode_base64( $digest ), 0, $MINUS_ONE ) ;
+
+        # ... we don't need the requestor_id anymore.
+        delete $hash{'xoauth_requestor_id'} ;
+
+        # Create the final authentication string
+        my $string = $method . q{ } . $url . q{?} . $urlparm .q{ } ;
+
+        # All the parameters must be sorted
+        $baseparms = q{};
+        foreach my $key (sort keys %hash) {
+                if(length($baseparms)>0) {
+                        $baseparms .= q{,} ;
+                }
+
+                $baseparms .= "$key=\"$hash{$key}\"";
+        }
+
+        $string .= $baseparms;
+
+        $debug and myprint( "XOAUTH: authentication string: $string\n" ) ;
+
+       # It must be base64 encoded
+        return encode_base64("$string", q{});
+}
+
+sub server_banner {
+	my $imap = shift;
+	my $banner = $imap->Banner() ||  "No banner\n";
+	return $banner;
+ }
+
+
+sub banner_imapsync {
+
+	my @argv = @_ ;
+
+	my $banner_imapsync = join q{},
+		q{$RCSfile: imapsync,v $ },
+		q{$Revision: 1.727 $ },
+		q{$Date: 2016/08/19 10:30:36 $ },
+		"\n", localhost_info(), "\n",
+		"Command line used:\n",
+		"$0 ", command_line_nopassword( @argv ), "\n" ;
+
+        return( $banner_imapsync ) ;
+}
+
+sub is_valid_directory {
+	my $dir = shift;
+
+	# all good => return ok.
+	return( 1 ) if ( -d $dir and -r _ and -w _ ) ;
+
+	# exist but bad
+	if ( -e $dir and not -d _ ) {
+		myprint( "Error: $dir exists but is not a directory\n"  ) ;
+		return( 0 ) ;
+	}
+	if ( -e $dir and not -w _ ) {
+		my $sb = stat $dir ;
+		myprintf( "Error: directory %s is not writable for user %s, permissions are %04o and owner is %s ( uid %s )\n",
+		         $dir, getpwuid_any_os( $EFFECTIVE_USER_ID ), ($sb->mode & oct($PERMISSION_FILTER) ), getpwuid_any_os( $sb->uid ), $sb->uid(  ) ) ;
+		return( 0 ) ;
+	}
+	# Trying to create it
+	myprint( "Creating directory $dir\n"  ) ;
+	eval { mkpath( $dir ) } ;
+	myprint( "$@" ) if ( $@ )  ;
+	return( 1 ) if ( -d $dir and -r _ and -w _ ) ;
+	return( 0 ) ;
+}
+
+sub tests_is_valid_directory {
+        Readonly my $NB_UNIX_tests_is_valid_directory => 4 ;
+	SKIP: {
+		skip( 'Tests only for Unix', $NB_UNIX_tests_is_valid_directory ) if ( 'MSWin32' eq $OSNAME ) ;
+		ok( 1 == is_valid_directory( '.'), 'is_valid_directory: . good' ) ;
+		ok( 1 == is_valid_directory( './tmp/tests/valid/sub'), 'is_valid_directory: ./tmp/tests/valid/sub good' ) ;
+		diag( 'Error / not writable is on purpose' ) ;
+		ok( 0 == is_valid_directory( '/'), 'is_valid_directory: / bad' ) ;
+		diag( 'Error permission denied on /noway is on purpose' ) ;
+		ok( 0 == is_valid_directory( '/noway'), 'is_valid_directory: /noway bad' ) ;
+	}
+	return ;
+}
+
+sub write_pidfile {
+	my $pid_filename = shift ;
+        my $lock = shift ;
+        
+	myprint( "PID file is $pid_filename ( to change it use --pidfile filepath ; to avoid it use --pidfile \"\" )\n" ) ;
+	if ( -e $pid_filename and $lock ) {
+		myprint( "$pid_filename already exists, another imapsync may be curently running. Aborting imapsync.\n"  ) ;
+                exit $EXIT_PID_FILE_ALREADY_EXIST ;
+	}
+	if ( -e $pid_filename ) {
+		myprint( "$pid_filename already exists, overwriting it ( use --pidfilelocking to avoid concurrent runs )\n"  ) ;
+	}
+
+	open my $FILE_HANDLE, '>', $pid_filename
+        	or do {
+			myprint( "Could not open $pid_filename for writing. Check permissions or disk space."  ) ;
+		return ;
+	} ;
+        myprint( "Wrinting my PID $PROCESS_ID in $pid_filename\n"  ) ;
+	print $FILE_HANDLE $PROCESS_ID ;
+	close $FILE_HANDLE ;
+
+	return( $PROCESS_ID ) ;
+}
+
+sub remove_tmp_files {
+        my $mysync = shift ;
+	unlink $mysync->{pidfile} ;
+	return ;
+}
+
+
+sub exit_clean {
+        my $mysync = shift ;
+	my $status = shift ;
+	$status = defined  $status  ? $status : $EXIT_UNKNOWN ;
+        remove_tmp_files( $mysync ) ;
+        myprint( "Exiting with return value $status\n" ) ;
+        if ( $mysync->{log} ) {
+                myprint( "Log file is $mysync->{logfile} ( to change it, use --logfile filepath ; or use --nolog to turn off logging )\n" ) ;
+                close $mysync->{logfile_handle} ;
+        }
+	exit $status ;
+}
+
+sub die_clean {
+	my @messages = @_ ;
+        remove_tmp_files( $sync ) ;
+	die @messages ;
+}
+
+sub missing_option {
+	my ( $option ) = @_ ;
+	die_clean( "$option option is mandatory, for help run $0 --help\n" ) ;
+	return ;
+}
+
+
+sub fix_Inbox_INBOX_mapping {
+	my( $h1_all, $h2_all ) = @_ ;
+
+	my $regex = q{} ;
+	SWITCH: {
+		if ( exists  $h1_all->{INBOX}  and exists  $h2_all->{INBOX}  ) { $regex = q{} ; last SWITCH ; } ;
+		if ( exists  $h1_all->{Inbox}  and exists  $h2_all->{Inbox}  ) { $regex = q{} ; last SWITCH ; } ;
+		if ( exists  $h1_all->{INBOX}  and exists  $h2_all->{Inbox}  ) { $regex = q{s/^INBOX$/Inbox/x} ; last SWITCH ; } ;
+		if ( exists  $h1_all->{Inbox}  and exists  $h2_all->{INBOX}  ) { $regex = q{s/^Inbox$/INBOX/x} ; last SWITCH ; } ;
+	} ;
+        return( $regex ) ;
+}
+
+sub tests_fix_Inbox_INBOX_mapping {
+
+	my( $h1_all, $h2_all ) ;
+
+	$h1_all = { 'INBOX' => q{} } ;
+	$h2_all = { 'INBOX' => q{} } ;
+	ok( q{} eq fix_Inbox_INBOX_mapping( $h1_all, $h2_all ), 'fix_Inbox_INBOX_mapping: INBOX INBOX' ) ;
+
+	$h1_all = { 'Inbox' => q{} } ;
+	$h2_all = { 'Inbox' => q{} } ;
+	ok( q{} eq fix_Inbox_INBOX_mapping( $h1_all, $h2_all ), 'fix_Inbox_INBOX_mapping: Inbox Inbox' ) ;
+
+	$h1_all = { 'INBOX' => q{} } ;
+	$h2_all = { 'Inbox' => q{} } ;
+	ok( q{s/^INBOX$/Inbox/x} eq fix_Inbox_INBOX_mapping( $h1_all, $h2_all ), 'fix_Inbox_INBOX_mapping: INBOX Inbox' ) ;
+
+	$h1_all = { 'Inbox' => q{} } ;
+	$h2_all = { 'INBOX' => q{} } ;
+	ok( q{s/^Inbox$/INBOX/x} eq fix_Inbox_INBOX_mapping( $h1_all, $h2_all ), 'fix_Inbox_INBOX_mapping: Inbox INBOX' ) ;
+
+	$h1_all = { 'INBOX' => q{} } ;
+	$h2_all = { 'rrrrr' => q{} } ;
+	ok( q{} eq fix_Inbox_INBOX_mapping( $h1_all, $h2_all ), 'fix_Inbox_INBOX_mapping: INBOX rrrrrr' ) ;
+
+	$h1_all = { 'rrrrr' => q{} } ;
+	$h2_all = { 'Inbox' => q{} } ;
+	ok( q{} eq fix_Inbox_INBOX_mapping( $h1_all, $h2_all ), 'fix_Inbox_INBOX_mapping: rrrrr Inbox' ) ;
+
+	return ;
+}
+
+
+sub jux_utf8_list {
+	my @s_inp = @_ ;
+	my $s_out = q{} ;
+	foreach my $s ( @s_inp ) {
+		$s_out .= jux_utf8( $s ) . "\n" ;
+	}
+	return( $s_out ) ;
+}
+
+sub tests_jux_utf8_list {
+	ok( q{} eq jux_utf8_list(  ), 'jux_utf8_list: void' ) ;
+	ok( "[]\n" eq jux_utf8_list( q{} ), 'jux_utf8_list: empty string' ) ;
+	ok( "[INBOX]\n" eq jux_utf8_list( 'INBOX' ), 'jux_utf8_list: INBOX' ) ;
+	ok( "[&ANY-] = [Ö]\n" eq jux_utf8_list( '&ANY-' ), 'jux_utf8_list: &ANY-' ) ;
+	return( 0 ) ;
+}
+
+sub jux_utf8 {
+	# juxtapose utf8 at the right if different
+        my ( $s_utf7 ) =  shift ;
+        my ( $s_utf8 ) =  imap_utf7_decode( $s_utf7 ) ;
+
+        if ( $s_utf7 eq $s_utf8 ) {
+        	#myprint( "[$s_utf7]\n"  ) ;
+        	return( "[$s_utf7]" ) ;
+        }else{
+        	#myprint( "[$s_utf7] = [$s_utf8]\n"  ) ;
+        	return( "[$s_utf7] = [$s_utf8]" ) ;
+        }
+}
+
+# editing utf8 can be tricky without an utf8 editor
+sub tests_jux_utf8 {
+	ok( '[INBOX]' eq jux_utf8( 'INBOX'), 'jux_utf8: INBOX => [INBOX]' ) ;
+	ok( '[&ZTZO9nux-] = [收件箱]' eq jux_utf8( '&ZTZO9nux-'), 'jux_utf8: => [&ZTZO9nux-] = [收件箱]' ) ;
+	ok( '[&ANY-] = [Ö]' eq jux_utf8( '&ANY-'), 'jux_utf8: &ANY- => [&ANY-] = [Ö]' ) ;
+        ok( '[]' eq jux_utf8( q{} ), 'jux_utf8: void => []' ) ;
+        ok( '[+BD8EQAQ1BDQEOwQ+BDM-] = [предлог]' eq jux_utf8( '+BD8EQAQ1BDQEOwQ+BDM-' ), 'jux_utf8: => [+BD8EQAQ1BDQEOwQ+BDM-] = [предлог]' ) ;
+        ok( '[&BB8EQAQ+BDUEOgRC-] = [Проект]'      eq jux_utf8( '&BB8EQAQ+BDUEOgRC-' ),    'jux_utf8: => [&BB8EQAQ+BDUEOgRC-] = [Проект]' ) ;
+
+	return( 0 ) ;
+}
+
+# Copied from http://cpansearch.perl.org/src/FABPOT/Unicode-IMAPUtf7-2.01/lib/Unicode/IMAPUtf7.pm
+# and then fixed with
+# https://rt.cpan.org/Public/Bug/Display.html?id=11172
+sub imap_utf7_decode {
+        my ( $s ) = shift ;
+
+        # Algorithm
+        # On remplace , par / dans les BASE 64 (, entre & et -)
+        # On remplace les &, non suivi d'un - par +
+        # On remplace les &- par &
+        $s =~ s/&([^,&\-]*),([^,\-&]*)\-/&$1\/$2\-/g ;
+        $s =~ s/&(?!\-)/\+/g ;
+        $s =~ s/&\-/&/g ;
+        return( Unicode::String::utf7( $s )->utf8 ) ;
+}
+
+sub imap_utf7_encode {
+	my ( $s ) = @_ ;
+
+	$s = Unicode::String::utf8( $s )->utf7 ;
+
+	$s =~ s/\+([^\/&\-]*)\/([^\/\-&]*)\-/\+$1,$2\-/g ;
+	$s =~ s/&/&\-/g ;
+	$s =~ s/\+([^+\-]+)?\-/&$1\-/g ;
+	return( $s ) ;
+}
+
+
+
+
+sub select_folder {
+	my ( $imap, $folder, $hostside ) = @_ ;
+	if ( ! $imap->select( $folder ) ) {
+		my $error = join q{},
+			"$hostside folder $folder: Could not select: ",
+			$imap->LastError,  "\n" ;
+		errors_incr( $sync, $error ) ;
+		return( 0 ) ;
+	}else{
+		# ok select succeeded
+		return( 1 ) ;
+	}
+}
+
+sub examine_folder {
+	my ( $imap, $folder, $hostside ) = @_ ;
+	if ( ! $imap->examine( $folder ) ) {
+		my $error = join q{},
+			"$hostside folder $folder: Could not examine: ",
+			$imap->LastError,  "\n" ;
+		errors_incr( $sync, $error ) ;
+		return( 0 ) ;
+	}else{
+		# ok select succeeded
+		return( 1 ) ;
+	}
+}
+
+
+
+
+sub count_from_select {
+	my @lines = @_ ;
+        my $count ;
+        foreach my $line ( @lines ) {
+        	#myprint( "line = [$line]\n"  ) ;
+                if ( $line =~ m/^\*\s+(\d+)\s+EXISTS/ ) {
+                	$count = $1 ;
+                        return( $count ) ;
+                }
+        }
+        return( undef ) ;
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+sub create_folder_old {
+	my( $imap, $h2_fold, $h1_fold ) = @_ ;
+
+	myprint( "Creating (old way) folder [$h2_fold] on host2\n" ) ;
+        if ( ( 'INBOX' eq uc  $h2_fold )
+         and ( $imap->exists( $h2_fold ) ) ) {
+                myprint( "Folder [$h2_fold] already exists\n"  ) ;
+                return( 1 ) ;
+        }
+	if ( ! $dry ){
+		if ( ! $imap->create( $h2_fold ) ) {
+			my $error = join q{},
+				"Could not create folder [$h2_fold] from [$h1_fold]: ",
+				$imap->LastError(  ), "\n" ;
+			errors_incr( $sync, $error ) ;
+                        # success if folder exists ("already exists" error)
+                        return( 1 ) if $imap->exists( $h2_fold ) ;
+                        # failure since create failed
+			return( 0 ) ;
+		}else{
+			#create succeeded
+                        myprint( "Created ( the old way ) folder [$h2_fold] on host2\n"  ) ;
+			return( 1 ) ;
+		}
+	}else{
+		# dry mode, no folder so many imap will fail, assuming failure
+                myprint( "Created ( the old way ) folder [$h2_fold] on host2 $dry_message\n"  ) ;
+		return( 0 ) ;
+	}
+}
+
+
+sub create_folder {
+        my( $imap2 , $h2_fold , $h1_fold ) = @_ ;
+        my( @parts , $parent ) ;
+
+        if ( $imap2->IsUnconnected(  ) ) {
+                myprint( "Host2: Unconnected state\n"  ) ;
+                return( 0 ) ;
+        }
+
+	if ( $create_folder_old ) {
+        	return( create_folder_old( $imap2 , $h2_fold , $h1_fold ) ) ;
+	}
+        myprint( "Creating folder [$h2_fold] on host2\n"  ) ;
+        if ( ( 'INBOX' eq uc  $h2_fold  )
+         and ( $imap2->exists( $h2_fold ) ) ) {
+                myprint( "Folder [$h2_fold] already exists\n"  ) ;
+                return( 1 ) ;
+        }
+
+        if ( $mixfolders and $imap2->exists( $h2_fold ) ) {
+                myprint( "Folder [$h2_fold] already exists  (--nomixfolders is not set)\n"  ) ;
+                return( 1 ) ;
+        }
+
+
+        if ( ( not $mixfolders ) and ( $imap2->exists( $h2_fold ) ) ) {
+                myprint( "Folder [$h2_fold] already exists and --nomixfolders is set\n"  ) ;
+                return( 0 ) ;
+        }
+
+        @parts = split /\Q$h2_sep\E/, $h2_fold ;
+        pop @parts ;
+        $parent = join $h2_sep, @parts ;
+        $parent =~ s/^\s+|\s+$//g ;
+        if ( ( $parent ne q{} ) and ( ! $imap2->exists( $parent ) ) ) {
+                create_folder( $imap2 , $parent , $h1_fold ) ;
+        }
+
+        if ( ! $dry ) {
+                if ( ! $imap2->create( $h2_fold ) ) {
+			my $error = join q{},
+				"Could not create folder [$h2_fold] from [$h1_fold]: " ,
+				$imap2->LastError(  ), "\n" ;
+			errors_incr( $sync, $error ) ;
+                        # success if folder exists ("already exists" error)
+                        return( 1 ) if $imap2->exists( $h2_fold ) ;
+                        # failure since create failed
+                        return( 0 ) ;
+                }else{
+                        #create succeeded
+                        myprint( "Created folder [$h2_fold] on host2\n"  ) ;
+                        return( 1 ) ;
+                }
+        }else{
+                # dry mode, no folder so many imap will fail, assuming failure
+                myprint( "Created  folder [$h2_fold] on host2 $dry_message\n"  ) ;
+                if ( ! $justfolders ) {
+			myprint( "Since --dry mode is on and folder [$h2_fold] on host2 does not exist yet, syncing messages will not be simulated.\n"
+			. "To simulate message syncing, use --justfolders without --dry to first create the missing folders then rerun the --dry sync.\n" ) ;
+                }
+		return( 0 ) ;
+        }
+}
+
+
+
+sub tests_folder_routines {
+	ok( !is_requested_folder('folder_foo'), 'is_requested_folder folder_foo 1'               );
+	ok(  add_to_requested_folders('folder_foo'), 'add_to_requested_folders folder_foo'       );
+	ok(  is_requested_folder('folder_foo'), 'is_requested_folder folder_foo 2'               );
+	ok( !is_requested_folder('folder_NO_EXIST'), 'is_requested_folder folder_NO_EXIST'       );
+	ok( !remove_from_requested_folders('folder_foo'), 'removed folder_foo'                   );
+	ok( !is_requested_folder('folder_foo'), 'is_requested_folder folder_foo 3'               );
+	my @f ;
+	ok(  @f = add_to_requested_folders('folder_bar', 'folder_toto'), "add result: @f"        );
+	ok(  is_requested_folder('folder_bar'), 'is_requested_folder 4'                          );
+	ok(  is_requested_folder('folder_toto'), 'is_requested_folder 5'                         );
+	ok(  remove_from_requested_folders('folder_toto'), 'remove_from_requested_folders: '       );
+	ok( !is_requested_folder('folder_toto'), 'is_requested_folder 6'                         );
+	ok( !remove_from_requested_folders('folder_bar'), 'remove_from_requested_folders: empty' ) ;
+
+        ok( 0 == compare_lists( [ sort_requested_folders(  ) ], [] ), 'sort_requested_folders: all empty' ) ;
+	ok(  add_to_requested_folders('M_55'), 'add_to_requested_folders M_55'       );
+        ok( 0 == compare_lists( [ sort_requested_folders(  ) ], [ 'M_55' ] ), 'sort_requested_folders: middle' ) ;
+	@folderfirst = ( 'Z_11' ) ;
+        ok( 0 == compare_lists( [ sort_requested_folders(  ) ], [ 'Z_11', 'M_55' ] ), 'sort_requested_folders: first+middle' ) ;
+	@folderlast = ( 'A_99' ) ;
+        ok( 0 == compare_lists( [ sort_requested_folders(  ) ], [ 'Z_11', 'M_55', 'A_99' ] ), 'sort_requested_folders: first+middle+last 1' ) ;
+
+	ok(  add_to_requested_folders('M_55', 'M_44',), 'add_to_requested_folders M_55 M_44'       );
+        ok( 0 == compare_lists( [ sort_requested_folders(  ) ], [ 'Z_11', 'M_44', 'M_55', 'A_99' ] ), 'sort_requested_folders: first+middle+last 2' ) ;
+	@folderfirst = qw( Z_22  Z_11 ) ;
+	@folderlast  = qw( A_99  A_88 ) ;
+        ok( 0 == compare_lists( [ sort_requested_folders(  ) ], [  'Z_22', 'Z_11', 'M_44', 'M_55', 'A_99', 'A_88' ] ), 'sort_requested_folders: first+middle+last 3' ) ;
+
+	return ;
+}
+
+
+sub sort_requested_folders {
+	my @requested_folders_sorted = () ;
+
+	foreach my $folder ( @folderfirst ) {
+        	remove_from_requested_folders( $folder ) ;
+        }
+
+	foreach my $folder ( @folderlast ) {
+        	remove_from_requested_folders( $folder ) ;
+        }
+
+	my @middle = sort keys %requested_folder ;
+
+        @requested_folders_sorted = ( @folderfirst, @middle, @folderlast ) ;
+
+	return( @requested_folders_sorted ) ;
+}
+
+sub is_requested_folder {
+	my ( $folder ) = @_;
+
+	return( defined  $requested_folder{ $folder }  ) ;
+}
+
+
+sub add_to_requested_folders {
+	my @wanted_folders = @_ ;
+
+	foreach my $folder ( @wanted_folders ) {
+	 	++$requested_folder{ $folder } ;
+	}
+	return( keys  %requested_folder  ) ;
+}
+
+sub remove_from_requested_folders {
+	my @wanted_folders = @_ ;
+
+	foreach my $folder ( @wanted_folders ) {
+	 	delete $requested_folder{ $folder } ;
+	}
+	return( keys %requested_folder ) ;
+}
+
+sub compare_lists {
+	my ($list_1_ref, $list_2_ref) = @_;
+
+	return($MINUS_ONE) if ((not defined $list_1_ref) and defined $list_2_ref);
+	return(0)  if ((not defined $list_1_ref) and not defined $list_2_ref); # end if no list
+	return(1)  if (not defined $list_2_ref); # end if only one list
+
+	if (not ref $list_1_ref ) {$list_1_ref = [$list_1_ref]};
+	if (not ref $list_2_ref ) {$list_2_ref = [$list_2_ref]};
+
+
+	my $last_used_indice = $MINUS_ONE;
+
+
+	ELEMENT:
+	foreach my $indice ( 0 .. $#{ $list_1_ref } ) {
+		$last_used_indice = $indice ;
+
+		# End of list_2
+		return 1 if ($indice > $#{ $list_2_ref } ) ;
+
+		my $element_list_1 = $list_1_ref->[$indice] ;
+		my $element_list_2 = $list_2_ref->[$indice] ;
+		my $balance = $element_list_1 cmp $element_list_2 ;
+		next ELEMENT if ($balance == 0) ;
+		return $balance ;
+	}
+	# each element equal until last indice of list_1
+	return $MINUS_ONE if ($last_used_indice < $#{ $list_2_ref } ) ;
+
+	# same size, each element equal
+	return 0 ;
+}
+
+sub tests_compare_lists {
+
+
+	my $empty_list_ref = [];
+
+	ok( 0 == compare_lists()               , 'compare_lists, no args');
+	ok( 0 == compare_lists(undef)          , 'compare_lists, undef = nothing');
+	ok( 0 == compare_lists(undef, undef)   , 'compare_lists, undef = undef');
+	ok($MINUS_ONE == compare_lists(undef , [])     , 'compare_lists, undef < []');
+	ok($MINUS_ONE == compare_lists(undef , [1])    , 'compare_lists, undef < [1]');
+	ok($MINUS_ONE == compare_lists(undef , [0])    , 'compare_lists, undef < [0]');
+      	ok(+1 == compare_lists([])             , 'compare_lists, [] > nothing');
+        ok(+1 == compare_lists([], undef)      , 'compare_lists, [] > undef');
+	ok( 0 == compare_lists([] , [])        , 'compare_lists, [] = []');
+
+	ok($MINUS_ONE == compare_lists([] , [1])        , 'compare_lists, [] < [1]');
+	ok(+1 == compare_lists([1] , [])        , 'compare_lists, [1] > []');
+
+
+	ok( 0 == compare_lists([1],  1 )          , 'compare_lists, [1] =  1 ') ;
+	ok( 0 == compare_lists( 1 , [1])          , 'compare_lists,  1  = [1]') ;
+	ok( 0 == compare_lists( 1 ,  1 )          , 'compare_lists,  1  =  1 ') ;
+	ok($MINUS_ONE == compare_lists( 0 ,  1 )          , 'compare_lists,  0  <  1 ') ;
+	ok($MINUS_ONE == compare_lists($MINUS_ONE ,  0 )          , 'compare_lists, -1  <  0 ') ;
+	ok($MINUS_ONE == compare_lists( 1 ,  2 )          , 'compare_lists,  1  <  2 ') ;
+	ok(+1 == compare_lists( 2 ,  1 )          , 'compare_lists,  2  >  1 ') ;
+
+
+	ok( 0 == compare_lists([1,2], [1,2])   , 'compare_lists,  [1,2] = [1,2]' ) ;
+	ok($MINUS_ONE == compare_lists([1], [1,2])     , 'compare_lists,    [1] < [1,2]' ) ;
+	ok(+1 == compare_lists([2], [1,2])     , 'compare_lists,    [2] > [1,2]' ) ;
+	ok($MINUS_ONE == compare_lists([1], [1,1])     , 'compare_lists,    [1] < [1,1]' ) ;
+	ok(+1 == compare_lists([1, 1], [1])    , 'compare_lists, [1, 1] >   [1]' ) ;
+	ok( 0 == compare_lists([1 .. $NUMBER_20_000] , [1 .. $NUMBER_20_000])
+                                               , 'compare_lists, [1..20_000] = [1..20_000]' ) ;
+	ok($MINUS_ONE == compare_lists([1], [2])       , 'compare_lists, [1] < [2]') ;
+	ok( 0 == compare_lists([2], [2])       , 'compare_lists, [0] = [2]') ;
+	ok(+1 == compare_lists([2], [1])       , 'compare_lists, [2] > [1]') ;
+
+	ok($MINUS_ONE == compare_lists(['a'],  ['b'])   , 'compare_lists, ["a"] < ["b"]') ;
+	ok( 0 == compare_lists(['a'],  ['a'])   , 'compare_lists, ["a"] = ["a"]') ;
+	ok( 0 == compare_lists(['ab'], ['ab']) , 'compare_lists, ["ab"] = ["ab"]') ;
+	ok(+1 == compare_lists(['b'],  ['a'])   , 'compare_lists, ["b"] > ["a"]') ;
+	ok($MINUS_ONE == compare_lists(['a'],  ['aa'])  , 'compare_lists, ["a"] < ["aa"]') ;
+	ok($MINUS_ONE == compare_lists(['a'],  ['a', 'a']), 'compare_lists, ["a"] < ["a", "a"]') ;
+	ok( 0 == compare_lists([split q{ }, 'a b' ], ['a', 'b']), 'compare_lists, split') ;
+	ok( 0 == compare_lists([sort split q{ }, 'b a' ], ['a', 'b']), 'compare_lists, sort split') ;
+        return ;
+}
+
+
+sub guess_prefix {
+	my @foldernames = @_ ;
+
+	return( undef ) unless ( @foldernames ) ;
+
+	my $prefix_guessed = q{} ;
+	foreach my $folder ( @foldernames ) {
+		next if ( $folder =~ m{^INBOX$}i ) ; # no guessing from INBOX
+		if ( $folder !~ m{^INBOX}i ) {
+			$prefix_guessed = q{} ; # prefix empty guessed
+			last ;
+		}
+		if ( $folder =~ m{^(INBOX(?:\.|\/))}i ) {
+			$prefix_guessed = $1 ;  # prefix Inbox/ or INBOX. guessed
+		}
+	}
+	return( $prefix_guessed ) ;
+}
+
+sub tests_guess_prefix {
+
+	ok( not( defined guess_prefix(  ) ), 'guess_prefix: no args' ) ;
+	ok( q{} eq guess_prefix( 'INBOX' ), 'guess_prefix: INBOX alone' ) ;
+	ok( q{} eq guess_prefix( 'Inbox' ), 'guess_prefix: Inbox alone' ) ;
+	ok( q{} eq guess_prefix( 'INBOX' ), 'guess_prefix: INBOX alone' ) ;
+	ok( 'INBOX/' eq guess_prefix( 'INBOX', 'INBOX/Junk' ), 'guess_prefix: INBOX INBOX/Junk' ) ;
+	ok( 'INBOX.' eq guess_prefix( 'INBOX', 'INBOX.Junk' ), 'guess_prefix: INBOX INBOX.Junk' ) ;
+	ok( 'Inbox/' eq guess_prefix( 'Inbox', 'Inbox/Junk' ), 'guess_prefix: Inbox Inbox/Junk' ) ;
+	ok( 'Inbox.' eq guess_prefix( 'Inbox', 'Inbox.Junk' ), 'guess_prefix: Inbox Inbox.Junk' ) ;
+	ok( 'INBOX/' eq guess_prefix( 'INBOX', 'INBOX/Junk', 'INBOX/rrr' ), 'guess_prefix: INBOX INBOX/Junk INBOX/rrr' ) ;
+	ok( q{} eq guess_prefix( 'INBOX', 'INBOX/Junk', 'INBOX/rrr', 'zzz' ), 'guess_prefix: INBOX INBOX/Junk INBOX/rrr zzz' ) ;
+	ok( q{} eq guess_prefix( 'INBOX', 'Junk' ), 'guess_prefix: INBOX Junk' ) ;
+	ok( q{} eq guess_prefix( 'INBOX', 'Junk' ), 'guess_prefix: INBOX Junk' ) ;
+
+	return ;
+}
+
+sub get_prefix {
+	my( $imap, $prefix_in, $prefix_opt, $Side, $folders_ref ) = @_ ;
+	my( $prefix_out, $prefix_guessed ) ;
+
+	( $debug or $sync->{debugfolders} ) and myprint( "$Side: Getting prefix\n"  ) ;
+	$prefix_guessed = guess_prefix( @{ $folders_ref } ) ;
+	myprint( "$Side: guessing prefix from folder listing: [$prefix_guessed]\n"  ) ;
+	( $debug or $sync->{debugfolders} ) and myprint( "$Side: Calling namespace capability\n"  ) ;
+	if ( $imap->has_capability( 'namespace' ) ) {
+		my $r_namespace = $imap->namespace(  ) ;
+		$prefix_out = $r_namespace->[0][0][0] ;
+                myprint( "$Side: prefix given by NAMESPACE: [$prefix_out]\n"  ) ;
+		if ( defined  $prefix_in  ) {
+                	myprint( "$Side: but using [$prefix_in] given by $prefix_opt\n"  ) ;
+                	$prefix_out = $prefix_in ;
+                	return( $prefix_out ) ;
+                }else{
+                	# all good
+	                return( $prefix_out ) ;
+                }
+	}
+	else{
+        	if ( defined  $prefix_in  ) {
+                	myprint( "$Side: using [$prefix_in] given by $prefix_opt\n"  ) ;
+                	$prefix_out = $prefix_in ;
+                	return( $prefix_out ) ;
+                }else{
+			myprint(
+			  "$Side: No NAMESPACE capability so using guessed prefix [$prefix_guessed]\n",
+			  help_to_guess_prefix( $imap, $prefix_opt ) ) ;
+			return( $prefix_guessed ) ;
+                }
+	}
+        return ;
+}
+
+
+sub guess_separator {
+	my @foldernames = @_ ;
+
+	#return( undef ) unless ( @foldernames ) ;
+
+	my $sep_guessed ;
+	my %counter ;
+	foreach my $folder ( @foldernames ) {
+		$counter{'/'}++  while ( $folder =~ m{/}g ) ;  # count /
+		$counter{'.'}++  while ( $folder =~ m{\.}g ) ; # count .
+		$counter{'\\\\'}++ while ( $folder =~ m{(\\){2}}g ) ; # count \\
+	}
+	my @race_sorted = sort { $counter{ $b } <=> $counter{ $a } } keys  %counter  ;
+	#myprint( "@race_sorted\n"  ) ;
+	$sep_guessed = shift @race_sorted || $LAST_RESSORT_SEPARATOR ; # / when nothing found.
+	return( $sep_guessed ) ;
+}
+
+sub tests_guess_separator {
+	ok( '/' eq  guess_separator(  ), 'guess_separator: no args' ) ;
+	ok( '/' eq guess_separator( 'abcd' ), 'guess_separator: abcd' ) ;
+	ok( '/' eq guess_separator( 'a/b/c.d' ), 'guess_separator: a/b/c.d' ) ;
+	ok( '.' eq guess_separator( 'a.b/c.d' ), 'guess_separator: a.b/c.d' ) ;
+	ok( '\\\\' eq guess_separator( 'a\\\\b\\\\c.c\\\\d/e/f' ), 'guess_separator: a\\\\b\\\\c.c\\\\d/e/f' ) ;
+	return ;
+}
+
+sub get_separator {
+	my( $imap, $sep_in, $sep_opt, $Side, $folders_ref ) = @_ ;
+	my( $sep_out, $sep_guessed ) ;
+
+	( $debug or $sync->{debugfolders} ) and myprint( "$Side: Getting separator\n"  ) ;
+	$sep_guessed = guess_separator( @{ $folders_ref } ) ;
+	myprint( "$Side: guessing separator from folder listing: [$sep_guessed]\n"  ) ;
+
+	( $debug or $sync->{debugfolders} ) and myprint( "$Side: calling namespace capability\n"  ) ;
+	if ( $imap->has_capability( 'namespace' ) ) {
+		$sep_out = $imap->separator(  ) ;
+		if ( defined  $sep_out  ) {
+                	myprint( "$Side: separator given by NAMESPACE: [$sep_out]\n"  ) ;
+                        if ( defined  $sep_in  ) {
+                		myprint( "$Side: but using [$sep_in] given by $sep_opt\n"  ) ;
+                        	$sep_out = $sep_in ;
+                        	return( $sep_out ) ;
+                        }else{
+                        	return( $sep_out ) ;
+                        }
+		}else{
+                	if ( defined  $sep_in  ) {
+                        	myprint( "$Side: NAMESPACE request failed but using [$sep_in] given by $sep_opt\n"  ) ;
+                        	$sep_out = $sep_in ;
+                        	return( $sep_out ) ;
+                        }else{
+				myprint(
+		  		"$Side: NAMESPACE request failed so using guessed separator [$sep_guessed]\n",
+                  		help_to_guess_sep( $imap, $sep_opt ) ) ;
+				return( $sep_guessed ) ;
+                        }
+                }
+	}
+	else{
+        	if ( defined  $sep_in  ) {
+                	myprint( "$Side: No NAMESPACE capability but using [$sep_in] given by $sep_opt\n"  ) ;
+                	$sep_out = $sep_in ;
+                	return( $sep_out ) ;
+                }else{
+			myprint(
+		  	"$Side: No NAMESPACE capability, so using guessed separator [$sep_guessed]\n",
+		      	help_to_guess_sep( $imap, $sep_opt ) ) ;
+			return( $sep_guessed ) ;
+                }
+	}
+        return ;
+}
+
+sub help_to_guess_sep {
+	my( $imap, $sep_opt ) = @_ ;
+
+	my $help_to_guess_sep = "You can set the separator character with the $sep_opt option,\n"
+	. "the complete listing of folders may help you to find it\n"
+	. folders_list_to_help( $imap ) ;
+
+	return( $help_to_guess_sep ) ;
+}
+
+sub help_to_guess_prefix {
+	my( $imap, $prefix_opt ) = @_ ;
+
+	my $help_to_guess_prefix = "You can set the prefix namespace with the $prefix_opt option,\n"
+	. "the folowing listing of folders may help you to find it:\n"
+	. folders_list_to_help( $imap ) ;
+
+	return( $help_to_guess_prefix ) ;
+}
+
+
+sub folders_list_to_help {
+	my($imap) = @_ ;
+
+	my @folders = $imap->folders ;
+	my $listing = join q{}, map { "[$_]\n" } @folders ;
+	return( $listing ) ;
+}
+
+
+sub tests_separator_invert {
+	$fixslash2 = 0 ;
+	ok( not( defined separator_invert(  )  ), 'separator_invert: no args' ) ;
+	ok( not( defined separator_invert( q{} ) ), 'separator_invert: not enough args' ) ;
+	ok( not( defined separator_invert( q{}, q{} ) ), 'separator_invert: not enough args' ) ;
+
+	ok( q{} eq separator_invert( q{}, q{}, q{} ), 'separator_invert: 3 empty strings' ) ;
+	ok( 'lalala' eq separator_invert( 'lalala', q{}, q{} ), 'separator_invert: empty separator' ) ;
+	ok( 'lalala' eq separator_invert( 'lalala', '/', '/' ), 'separator_invert: same separator /' ) ;
+	ok( 'lal/ala' eq separator_invert( 'lal/ala', '/', '/' ), 'separator_invert: same separator / 2' ) ;
+	ok( 'lal.ala' eq separator_invert( 'lal/ala', '/', '.' ), 'separator_invert: separators /.' ) ;
+	ok( 'lal/ala' eq separator_invert( 'lal.ala', '.', '/' ), 'separator_invert: separators ./' ) ;
+	ok( 'la.l/ala' eq separator_invert( 'la/l.ala', '.', '/' ), 'separator_invert: separators ./' ) ;
+
+	ok( 'l/al.ala' eq separator_invert( 'l.al/ala', '/', '.' ), 'separator_invert: separators /.' ) ;
+        $fixslash2 = 1 ;
+	ok( 'l_al.ala' eq separator_invert( 'l.al/ala', '/', '.' ), 'separator_invert: separators /.' ) ;
+
+	return ;
+}
+
+sub separator_invert {
+	my( $h1_fold, $h1_separator, $h2_separator ) = @_ ;
+
+	return( undef ) if ( not defined  $h1_fold  or not defined  $h1_separator  or not defined  $h2_separator  ) ;
+	# The separator we hope we'll never encounter: 00000000 == 0x00
+	my $o_sep = "\000" ;
+
+	my $h2_fold = $h1_fold ;
+	$h2_fold =~ s,\Q$h2_separator,$o_sep,xg ;
+	$h2_fold =~ s,\Q$h1_separator,$h2_separator,xg ;
+	$h2_fold =~ s,\Q$o_sep,$h1_separator,xg ;
+        $h2_fold =~ s,/,_,xg if( $fixslash2 and '/' ne $h2_separator and '/' eq $h1_separator ) ;
+	return( $h2_fold ) ;
+}
+
+
+sub tests_imap2_folder_name {
+
+$h1_prefix = $h2_prefix = q{};
+$h1_sep = '/';
+$h2_sep = '.';
+
+$debug and myprint( <<"EOS"
+prefix1: [$h1_prefix]
+prefix2: [$h2_prefix]
+sep1:[$h1_sep]
+sep2:[$h2_sep]
+EOS
+) ;
+
+$fixslash2 = 0 ;
+ok(q{} eq imap2_folder_name(q{}), 'imap2_folder_name: empty string');
+ok('blabla' eq imap2_folder_name('blabla'), 'imap2_folder_name: blabla');
+ok('spam.spam' eq imap2_folder_name('spam/spam'), 'imap2_folder_name: spam/spam');
+ok('spam/spam' eq imap2_folder_name('spam.spam'), 'imap2_folder_name: spam.spam');
+ok('spam.spam/spam' eq imap2_folder_name('spam/spam.spam'), 'imap2_folder_name: spam/spam.spam');
+ok('s pam.spam/sp  am' eq imap2_folder_name('s pam/spam.sp  am'), 'imap2_folder_name: s pam/spam.sp  am');
+
+$sync->{f1f2}{ 'auto' } = 'moto' ;
+ok( 'moto' eq imap2_folder_name( 'auto' ), 'imap2_folder_name: auto' ) ;
+$sync->{f1f2}{ 'auto/auto' } = 'moto x 2' ;
+ok( 'moto x 2' eq imap2_folder_name( 'auto/auto' ), 'imap2_folder_name: auto/auto' ) ;
+
+@regextrans2 = ('s,/,X,g');
+ok(q{} eq imap2_folder_name(q{}), 'imap2_folder_name: empty string [s,/,X,g]');
+ok('blabla' eq imap2_folder_name('blabla'), 'imap2_folder_name: blabla [s,/,X,g]');
+ok('spam.spam' eq imap2_folder_name('spam/spam'), 'imap2_folder_name: spam/spam [s,/,X,g]');
+ok('spamXspam' eq imap2_folder_name('spam.spam'), 'imap2_folder_name: spam.spam [s,/,X,g]');
+ok('spam.spamXspam' eq imap2_folder_name('spam/spam.spam'), 'imap2_folder_name: spam/spam.spam [s,/,X,g]');
+
+@regextrans2 = ( 's, ,_,g' ) ;
+ok('blabla' eq imap2_folder_name('blabla'), 'imap2_folder_name: blabla [s, ,_,g]');
+ok('bla_bla' eq imap2_folder_name('bla bla'), 'imap2_folder_name: blabla [s, ,_,g]');
+
+@regextrans2 = ( q{s,(.*),\U$1,} ) ;
+ok( 'BLABLA' eq imap2_folder_name( 'blabla' ), q{imap2_folder_name: blabla [s,\U(.*)\E,$1,]} ) ;
+
+$fixslash2 = 1 ;
+@regextrans2 = (  ) ;
+ok(q{} eq imap2_folder_name(q{}), 'imap2_folder_name: empty string');
+ok('blabla' eq imap2_folder_name('blabla'), 'imap2_folder_name: blabla');
+ok('spam.spam' eq imap2_folder_name('spam/spam'), 'imap2_folder_name: spam/spam -> spam.spam');
+ok('spam_spam' eq imap2_folder_name('spam.spam'), 'imap2_folder_name: spam.spam -> spam_spam');
+ok('spam.spam_spam' eq imap2_folder_name('spam/spam.spam'), 'imap2_folder_name: spam/spam.spam -> spam.spam_spam');
+ok('s pam.spam_spa  m' eq imap2_folder_name('s pam/spam.spa  m'), 'imap2_folder_name: s pam/spam.spa m -> s pam.spam_spa  m');
+
+$h1_sep = '.';
+$h2_sep = '/';
+ok(q{} eq imap2_folder_name(q{}), 'imap2_folder_name: empty string');
+ok('blabla' eq imap2_folder_name('blabla'), 'imap2_folder_name: blabla');
+ok('spam.spam' eq imap2_folder_name('spam/spam'), 'imap2_folder_name: spam/spam -> spam.spam');
+ok('spam/spam' eq imap2_folder_name('spam.spam'), 'imap2_folder_name: spam.spam -> spam/spam');
+ok('spam.spam/spam' eq imap2_folder_name('spam/spam.spam'), 'imap2_folder_name: spam/spam.spam -> spam.spam/spam');
+
+
+
+$fixslash2 = 0 ;
+$h1_prefix = q{ };
+
+ok('spam.spam/spam' eq imap2_folder_name('spam/spam.spam'), 'imap2_folder_name: spam/spam.spam -> spam.spam/spam');
+ok('spam.spam/spam' eq imap2_folder_name(' spam/spam.spam'), 'imap2_folder_name:  spam/spam.spam -> spam.spam/spam');
+
+$h1_sep = '.' ;
+$h2_sep = '/' ;
+$h1_prefix = 'INBOX.' ;
+$h2_prefix = q{} ;
+@regextrans2 = ( q{s,(.*),\U$1,} ) ;
+ok( 'BLABLA' eq imap2_folder_name( 'blabla' ), 'imap2_folder_name: blabla' ) ;
+ok( 'TEST/TEST/TEST/TEST' eq imap2_folder_name( 'INBOX.TEST.test.Test.tesT' ), 'imap2_folder_name: INBOX.TEST.test.Test.tesT' ) ;
+@regextrans2 = ( q{s,(.*),\L$1,} ) ;
+ok( 'test/test/test/test' eq imap2_folder_name( 'INBOX.TEST.test.Test.tesT' ), 'imap2_folder_name: INBOX.TEST.test.Test.tesT' ) ;
+
+
+return ;
+
+}
+
+sub imap2_folder_name {
+	my ( $h1_fold ) = @_ ;
+	my ( $h2_fold ) ;
+	if ( $sync->{f1f2}{ $h1_fold } ) {
+		$h2_fold = $sync->{f1f2}{ $h1_fold } ;
+		( $debug or $sync->{debugfolders} ) and myprint( "f1f2 [$h1_fold] -> [$h2_fold]\n"  ) ;
+		return( $h2_fold ) ;
+	}
+	if ( $sync->{f1f2auto}{ $h1_fold } ) {
+		$h2_fold = $sync->{f1f2auto}{ $h1_fold } ;
+		( $debug or $sync->{debugfolders} ) and myprint( "automap [$h1_fold] -> [$h2_fold]\n"  ) ;
+		return( $h2_fold ) ;
+	}
+
+	$h2_fold = prefix_seperator_invertion( $h1_fold ) ;
+	$h2_fold = regextrans2( $h2_fold ) ;
+	return( $h2_fold ) ;
+}
+
+sub prefix_seperator_invertion {
+	my ( $h1_fold ) = @_ ;
+	my ( $h2_fold ) ;
+
+	# first we remove the prefix
+	$h1_fold =~ s/^\Q$h1_prefix\E//x ;
+	( $debug or $sync->{debugfolders} ) and myprint( "removed host1 prefix: [$h1_fold]\n"  ) ;
+	$h2_fold = separator_invert( $h1_fold, $h1_sep, $h2_sep ) ;
+	( $debug or $sync->{debugfolders} ) and myprint( "inverted  separators: [$h2_fold]\n"  ) ;
+	# Adding the prefix supplied by namespace or the --prefix2 option
+	$h2_fold = $h2_prefix . $h2_fold
+	  unless( ( $h2_prefix eq 'INBOX' . $h2_sep ) and ( $h2_fold =~ m/^INBOX$/xi ) ) ;
+	( $debug or $sync->{debugfolders} ) and myprint( "added   host2 prefix: [$h2_fold]\n"  ) ;
+	return( $h2_fold ) ;
+}
+
+sub regextrans2 {
+	my( $h2_fold ) = @_ ;
+	# Transforming the folder name by the --regextrans2 option(s)
+	foreach my $regextrans2 ( @regextrans2 ) {
+	        my $h2_fold_before = $h2_fold ;
+		my $ret = eval "\$h2_fold =~ $regextrans2 ; 1 " ;
+		( $debug or $sync->{debugfolders} ) and myprint( "[$h2_fold_before] -> [$h2_fold] using regextrans2 [$regextrans2]\n"  ) ;
+                if ( not ( defined  $ret  ) or $@ ) {
+			die_clean( "error: eval regextrans2 '$regextrans2': $@\n" ) ;
+                }
+	}
+	return( $h2_fold ) ;
+}
+
+
+sub tests_decompose_regex {
+	ok( 1, 'decompose_regex 1' ) ;
+	ok( 0 == compare_lists( [ q{}, q{} ], [ decompose_regex( q{} ) ] ), 'decompose_regex empty string' ) ;
+	ok( 0 == compare_lists( [ '.*', 'lala' ], [ decompose_regex( 's/.*/lala/' ) ] ), 'decompose_regex s/.*/lala/' ) ;
+	return ;
+}
+
+sub decompose_regex {
+	my $regex = shift ;
+	my( $left_part, $right_part ) ;
+
+	( $left_part, $right_part ) = $regex =~ m{^s/((?:[^/]|\\/)+)/((?:[^/]|\\/)+)/}x;
+        return( q{}, q{} ) if not $left_part ;
+	return( $left_part, $right_part ) ;
+}
+
+
+sub foldersizes {
+
+	my ( $side, $imap, $search_cmd, @folders ) = @_ ;
+	my $total_size = 0 ;
+	my $total_nb = 0 ;
+	my $biggest_in_all = 0 ;
+
+	my $nb_folders = scalar  @folders  ;
+	my $ct_folders = 0 ; # folder counter.
+	myprint( "++++ Calculating sizes of $nb_folders folders on $side\n"  ) ;
+	foreach my $folder ( @folders )     {
+		my $stot = 0 ;
+		my $nb_msgs = 0 ;
+		$ct_folders++ ;
+		myprintf( "$side folder %7s %-35s", "$ct_folders/$nb_folders", jux_utf8( $folder ) ) ;
+                if ( 'Host2' eq $side and not exists  $h2_folders_all_UPPER{ uc  $folder  }  ) {
+		        myprint( " does not exist yet\n") ;
+			next ;
+		}
+                if ( 'Host1' eq $side and not exists  $h1_folders_all{ $folder }  ) {
+		        myprint( " does not exist\n" ) ;
+			next ;
+		}
+
+		last if $imap->IsUnconnected(  ) ;
+		# FTGate is RFC buggy with EXAMINE it does not act as SELECT
+		#unless ( $imap->examine( $folder ) ) {
+		unless ( $imap->select( $folder ) ) {
+			my $error = join q{},
+				"$side Folder $folder: Could not select: ",
+				$imap->LastError,  "\n"  ;
+			errors_incr( $sync, $error ) ;
+			next ;
+		}
+		last if $imap->IsUnconnected(  ) ;
+
+		my $hash_ref = { } ;
+		my @msgs = select_msgs( $imap, undef, $search_cmd, $folder ) ;
+		$nb_msgs = scalar  @msgs  ;
+		my $biggest_in_folder = 0 ;
+		@{ $hash_ref }{ @msgs } = ( undef ) if @msgs ;
+
+		last if $imap->IsUnconnected(  ) ;
+		if ( $nb_msgs > 0 and @msgs ) {
+                	if ( $abletosearch ) {
+				if ( ! $imap->fetch_hash( \@msgs, 'RFC822.SIZE', $hash_ref) ) {
+                                        my $error = "$side failure with fetch_hash: $@" ;
+                                        errors_incr( $sync, $error ) ;
+                                        return ;
+                                }
+                        }else{
+				my $uidnext = $imap->uidnext( $folder ) || $uidnext_default ;
+				my $fetch_hash_uids = $fetch_hash_set || "1:$uidnext" ;
+				if ( ! $imap->fetch_hash( $fetch_hash_uids, 'RFC822.SIZE', $hash_ref ) ) {
+                                        my $error = "$side failure with fetch_hash: $@" ;
+                                        errors_incr( $sync, $error ) ;
+                                        return ;
+                                }
+                        }
+			for ( keys %{ $hash_ref } ) {
+                        	my $size =  $hash_ref->{ $_ }->{ 'RFC822.SIZE' } ;
+                        	$stot    += $size ;
+                                $biggest_in_folder =  max( $biggest_in_folder, $size ) ;
+                        }
+		}
+
+		myprintf( ' Size: %9s', $stot ) ;
+		myprintf( ' Messages: %5s', $nb_msgs ) ;
+		myprintf( " Biggest: %9s\n", $biggest_in_folder ) ;
+		$total_size += $stot ;
+		$total_nb += $nb_msgs ;
+                $biggest_in_all =  max( $biggest_in_all, $biggest_in_folder ) ;
+	}
+	myprintf( "%s Nb folders:      %11s folders\n",    $side, $nb_folders ) ;
+	myprintf( "%s Nb messages:     %11s messages\n",   $side, $total_nb ) ;
+	myprintf( "%s Total size:      %11s bytes (%s)\n", $side, $total_size, bytes_display_string( $total_size ) ) ;
+	myprintf( "%s Biggest message: %11s bytes (%s)\n", $side, $biggest_in_all, bytes_display_string( $biggest_in_all ) ) ;
+	myprintf( "%s Time spent:      %11.1f seconds\n",  $side, timenext(  ) ) ;
+        return( $total_nb, $total_size ) ;
+}
+
+sub timenext {
+	my ( $timenow, $timediff ) ;
+	# $timebefore is global, beurk !
+	$timenow    = time ;
+	$timediff   = $timenow - $timebefore ;
+	$timebefore = $timenow ;
+	return( $timediff ) ;
+}
+
+sub timesince {
+	my $timeinit = shift ;
+	my ( $timenow, $timediff ) ;
+	$timenow    = time ;
+	$timediff   = $timenow - $timeinit ;
+	return( $timediff ) ;
+}
+
+
+
+
+sub tests_flags_regex {
+
+	ok( q{} eq flags_regex(q{} ), 'flags_regex, null string q{}' ) ;
+	ok( q'\Seen NonJunk $Spam' eq flags_regex( q'\Seen NonJunk $Spam' ), 'flags_regex, nothing to do');
+
+	@regexflag = ('I am BAD' ) ;
+        ok( not ( defined flags_regex( q{} ) ), 'flags_regex, bad regex' ) ;
+
+	@regexflag = ( 's/NonJunk//g' ) ;
+	ok( q'\Seen  $Spam' eq flags_regex( q'\Seen NonJunk $Spam' ), q{flags_regex, remove NonJunk: 's/NonJunk//g'} ) ;
+	@regexflag = ( q's/\$Spam//g' ) ;
+	ok( '\Seen NonJunk ' eq flags_regex( q'\Seen NonJunk $Spam' ), q{flags_regex, remove $Spam: 's/\$Spam//g'} ) ;
+
+	@regexflag = ( 's/\\\\Seen//g' ) ;
+
+	ok( q' NonJunk $Spam' eq flags_regex( q'\Seen NonJunk $Spam' ), q{flags_regex, remove \Seen: 's/\\\\\\\\Seen//g'} ) ;
+
+	@regexflag = ( 's/(\s|^)[^\\\\]\w+//g' ) ;
+	ok( '\Seen \Middle \End'   eq flags_regex( q'\Seen NonJunk \Middle $Spam \End' ), q{flags_regex: only \word among \Seen NonJunk \Middle $Spam \End} ) ;
+	ok( ' \Seen \Middle \End1' eq flags_regex( q'Begin \Seen NonJunk \Middle $Spam \End1 End' ), 
+                     q'flags_regex: only \word among Begin \Seen NonJunk \Middle $Spam \End1 End' ) ;
+
+	@regexflag = ( q's/.*?(Keep1|Keep2|Keep3)/$1 /g' ) ;
+	ok('Keep1 Keep2  ReB' eq flags_regex('ReA Keep1 REM Keep2 ReB'), 'Keep only regex' ) ;
+	
+	ok('Keep1 Keep2 ' eq flags_regex( 'REM REM Keep1 Keep2'), 'Keep only regex' ) ;
+	ok('Keep1 Keep2 ' eq flags_regex( 'Keep1 REM REM Keep2'), 'Keep only regex' ) ;
+	ok('Keep1 Keep2 ' eq flags_regex( 'REM Keep1 REM REM  Keep2'), 'Keep only regex' ) ;
+	ok('Keep1 Keep2 ' eq flags_regex( 'Keep1 Keep2'), 'Keep only regex' ) ;
+	ok('Keep1 ' eq flags_regex( 'REM Keep1'), 'Keep only regex' ) ;
+
+	@regexflag = ( q's/(Keep1|Keep2|Keep3) (?!(Keep1|Keep2|Keep3)).*/$1 /g' ) ;
+	ok('Keep1 Keep2 ' eq flags_regex( 'Keep1 Keep2 ReB'), 'Keep only regex' ) ;
+	ok('Keep1 Keep2 ' eq flags_regex( 'Keep1 Keep2 REM REM  REM'), 'Keep only regex' ) ;
+	ok('Keep2 ' eq flags_regex('Keep2 REM REM  REM'), 'Keep only regex' ) ;
+	
+
+	@regexflag = ( q's/.*?(Keep1|Keep2|Keep3)/$1 /g',
+	   's/(Keep1|Keep2|Keep3) (?!(Keep1|Keep2|Keep3)).*/$1 /g');
+	ok('Keep1 Keep2 ' eq flags_regex('REM Keep1 REM Keep2 REM'), 'Keep only regex');
+	ok('Keep1 Keep2 ' eq flags_regex('Keep1 REM Keep2 REM'), 'Keep only regex');
+	ok('Keep1 Keep2 ' eq flags_regex('REM Keep1 Keep2 REM'), 'Keep only regex');
+	ok('Keep1 Keep2 ' eq flags_regex('REM Keep1 REM Keep2'), 'Keep only regex');
+	ok('Keep1 Keep2 Keep3 ' eq flags_regex('REM Keep1 REM Keep2 REM REM Keep3 REM'), 'Keep only regex');
+	ok('Keep1 ' eq flags_regex('REM  REM Keep1 REM REM REM '), 'Keep only regex');
+	ok('Keep1 Keep3 ' eq flags_regex('RE1 Keep1 RE2 Keep3 RE3 RE4 RE5 '), 'Keep only regex');
+
+	@regexflag = ('s/(.*)/$1 jrdH8u/');
+	ok('REM  REM  REM REM REM jrdH8u' eq flags_regex('REM  REM  REM REM REM'), q{Keep only regex 's/(.*)/\$1 jrdH8u/'} ) ;
+	@regexflag = ('s/jrdH8u *//');
+	ok('REM  REM  REM REM REM ' eq flags_regex('REM  REM  REM REM REM jrdH8u'), q{Keep only regex s/jrdH8u *//} ) ;
+
+	@regexflag = (
+	's/(.*)/$1 jrdH8u/',
+	's/.*?(Keep1|Keep2|Keep3|jrdH8u)/$1 /g',
+	's/(Keep1|Keep2|Keep3|jrdH8u) (?!(Keep1|Keep2|Keep3|jrdH8u)).*/$1 /g',
+	's/jrdH8u *//'
+	);
+
+	ok('Keep1 Keep2 ' eq flags_regex('REM Keep1 REM Keep2 REM'), q{Keep only regex 'REM Keep1 REM Keep2 REM'} ) ;
+	ok('Keep1 Keep2 ' eq flags_regex('Keep1 REM Keep2 REM'), 'Keep only regex');
+	ok('Keep1 Keep2 ' eq flags_regex('REM Keep1 Keep2 REM'), 'Keep only regex');
+	ok('Keep1 Keep2 ' eq flags_regex('REM Keep1 REM Keep2'), 'Keep only regex');
+	ok('Keep1 Keep2 Keep3 ' eq flags_regex('REM Keep1 REM Keep2 REM REM Keep3 REM'), 'Keep only regex');
+	ok('Keep1 ' eq flags_regex('REM  REM Keep1 REM REM REM '), 'Keep only regex');
+	ok('Keep1 Keep3 ' eq flags_regex('RE1 Keep1 RE2 Keep3 RE3 RE4 RE5 '), 'Keep only regex');
+	ok(q{} eq flags_regex('REM  REM REM REM REM'), 'Keep only regex');
+
+	@regexflag = (
+	's/(.*)/$1 jrdH8u/',
+	's/.*?(\\\\Seen|\\\\Answered|\\\\Flagged|\\\\Deleted|\\\\Draft|jrdH8u)/$1 /g',
+	's/(\\\\Seen|\\\\Answered|\\\\Flagged|\\\\Deleted|\\\\Draft|jrdH8u) (?!(\\\\Seen|\\\\Answered|\\\\Flagged|\\\\Deleted|\\\\Draft|jrdH8u)).*/$1 /g',
+	's/jrdH8u *//'
+	);
+
+	ok('\\Deleted \\Answered '
+	    eq flags_regex('Blabla $Junk \\Deleted machin \\Answered truc'), 'Keep only regex: Exchange case' ) ;
+	ok( q{} eq flags_regex( q{} ), 'Keep only regex: Exchange case, null string' ) ;
+	ok( q{}
+	   eq flags_regex('Blabla $Junk  machin  truc'), 'Keep only regex: Exchange case, no accepted flags' ) ;
+	ok( '\\Deleted \\Answered \\Draft \\Flagged '
+	    eq flags_regex('\\Deleted    \\Answered  \\Draft \\Flagged '), 'Keep only regex: Exchange case' ) ;
+
+
+	@regexflag = (
+	's/.*?(?:(\\\\(?:Answered|Flagged|Deleted|Seen|Draft)\s?)|$)/defined($1)?$1:q()/eg'
+	);
+
+	ok( '\\Deleted \\Answered '
+	eq flags_regex('Blabla \$Junk \\Deleted machin \\Answered truc'),
+	'Keep only regex: Exchange case (Phil)' ) ;
+
+	ok( q{} eq flags_regex( q{} ), 'Keep only regex: Exchange case, null string (Phil)' ) ;
+
+	ok( q{}
+	eq flags_regex('Blabla $Junk  machin  truc'),
+	'Keep only regex: Exchange case, no accepted flags (Phil)' ) ;
+
+	ok('\\Deleted \\Answered \\Draft \\Flagged '
+	eq flags_regex('\\Deleted    \\Answered  \\Draft \\Flagged '),
+	'Keep only regex: Exchange case (Phil)' ) ;
+
+	return ;
+}
+
+sub flags_regex {
+	my ( $h1_flags ) = @_ ;
+	foreach my $regexflag ( @regexflag ) {
+		my $h1_flags_orig = $h1_flags ;
+		$debugflags and myprint( "eval \$h1_flags =~ $regexflag\n"  ) ;
+		my $ret = eval "\$h1_flags =~ $regexflag ; 1 " ;
+		$debugflags and myprint( "regexflag $regexflag [$h1_flags_orig] -> [$h1_flags]\n"  ) ;
+                if( not ( defined $ret ) or $@ ) {
+			myprint( "Error: eval regexflag '$regexflag': $@\n"  ) ;
+                        return( undef ) ;
+                }
+	}
+	return( $h1_flags ) ;
+}
+
+sub acls_sync {
+	my($h1_fold, $h2_fold) = @_ ;
+	if ( $syncacls ) {
+		my $h1_hash = $imap1->getacl($h1_fold)
+		  or myprint( "Could not getacl for $h1_fold: $@\n" ) ;
+		my $h2_hash = $imap2->getacl($h2_fold)
+		  or myprint( "Could not getacl for $h2_fold: $@\n" ) ;
+		my %users = map { ($_, 1) } ( keys  %{ $h1_hash} , keys %{ $h2_hash }  ) ;
+		foreach my $user (sort keys %users ) {
+			my $acl = $h1_hash->{$user} || 'none' ;
+			myprint( "acl $user: [$acl]\n" ) ;
+			next if ($h1_hash->{$user} && $h2_hash->{$user} &&
+				 $h1_hash->{$user} eq $h2_hash->{$user});
+			unless ($dry) {
+				myprint( "setting acl $h2_fold $user $acl\n" ) ;
+				$imap2->setacl($h2_fold, $user, $acl)
+				  or myprint( "Could not set acl: $@\n" ) ;
+			}
+		}
+	}
+        return ;
+}
+
+
+sub tests_permanentflags {
+
+	my $string;
+	ok(q{} eq permanentflags(' * OK [PERMANENTFLAGS (\* \Draft \Answered)] Limited'),
+	   'permanentflags \*');
+	ok('\Draft \Answered' eq permanentflags(' * OK [PERMANENTFLAGS (\Draft \Answered)] Limited'),
+	   'permanentflags \Draft \Answered');
+	ok('\Draft \Answered'
+	   eq permanentflags('Blabla',
+	                     ' * OK [PERMANENTFLAGS (\Draft \Answered)] Limited',
+			     'Blabla'),
+	   'permanentflags \Draft \Answered'
+	);
+	ok(q{} eq permanentflags('Blabla'), 'permanentflags nothing');
+        return ;
+}
+
+sub permanentflags {
+	my @lines = @_ ;
+
+	foreach my $line (@lines) {
+		if ( $line =~ m{\[PERMANENTFLAGS\s\(([^)]+?)\)\]}x ) {
+			( $debugflags or $debug ) and myprint( "permanentflags: $line"  ) ;
+			my $permanentflags = $1 ;
+			if ( $permanentflags =~ m{\\\*}x ) {
+				$permanentflags = q{} ;
+			}
+			return($permanentflags) ;
+		} ;
+	}
+        return( q{} ) ;
+}
+
+sub tests_flags_filter {
+
+	ok( '\Seen' eq flags_filter('\Seen', '\Draft \Seen \Answered'), 'flags_filter ' );
+	ok( q{} eq flags_filter('\Seen', '\Draft  \Answered'), 'flags_filter ' );
+	ok( '\Seen' eq flags_filter('\Seen', '\Seen'), 'flags_filter ' );
+	ok( '\Seen' eq flags_filter('\Seen', ' \Seen '), 'flags_filter ' );
+	ok( '\Seen \Draft'
+	   eq flags_filter('\Seen \Draft', '\Draft \Seen \Answered'), 'flags_filter ' );
+	ok( '\Seen \Draft'
+	   eq flags_filter('\Seen \Draft', ' \Draft \Seen \Answered '), 'flags_filter ' );
+        return ;
+}
+
+sub flags_filter {
+	my( $flags, $allowed_flags ) = @_ ;
+
+	my @flags = split  /\s+/x, $flags ;
+	my %allowed_flags = map { $_ => 1 } split q{ }, $allowed_flags ;
+	my @flags_out     = map { exists $allowed_flags{$_} ? $_ : () } @flags ;
+
+	my $flags_out = join q{ }, @flags_out ;
+
+	return( $flags_out ) ;
+}
+
+sub flagscase {
+	my $flags = shift ;
+
+	my @flags = split /\s+/x, $flags ;
+	my %rfc_flags = map { $_ => 1 } split q{ }, '\Answered \Flagged \Deleted \Seen \Draft' ;
+	my @flags_out = map { exists $rfc_flags{ ucsecond( lc $_ ) } ? ucsecond( lc $_ ) : $_ } @flags ;
+
+	my $flags_out = join q{ }, @flags_out ;
+
+	return( $flags_out ) ;
+}
+
+sub tests_flagscase {
+	ok( '\Seen' eq flagscase( '\Seen' ), 'flagscase: \Seen -> \Seen' ) ;
+	ok( '\Seen' eq flagscase( '\SEEN' ), 'flagscase: \SEEN -> \Seen' ) ;
+
+	ok( '\Seen \Draft' eq flagscase( '\SEEN \DRAFT' ), 'flagscase: \SEEN \DRAFT -> \Seen \Draft' ) ;
+	ok( '\Draft \Seen' eq flagscase( '\DRAFT \SEEN' ), 'flagscase: \DRAFT \SEEN -> \Draft \Seen' ) ;
+
+	ok( '\Draft LALA \Seen' eq flagscase( '\DRAFT  LALA \SEEN' ), 'flagscase: \DRAFT  LALA \SEEN -> \Draft LALA \Seen' ) ;
+	ok( '\Draft lala \Seen' eq flagscase( '\DRAFT  lala \SEEN' ), 'flagscase: \DRAFT  lala \SEEN -> \Draft lala \Seen' ) ;
+        return ;
+}
+
+
+
+sub ucsecond {
+	my $string = shift ;
+	my $output ;
+
+	return( $string )  if ( 1 >= length $string ) ;
+	
+	$output = ( substr( $string, 0, 1) ) . ( uc substr $string, 1, 1 ) . ( substr $string, 2 ) ;
+	#myprint( "UUU $string -> $output\n"  ) ;
+	return( $output ) ;
+}
+
+
+sub tests_ucsecond {
+	ok( 'aBcde' eq ucsecond( 'abcde' ), 'ucsecond: abcde -> aBcde' ) ;
+	ok( 'ABCDE' eq ucsecond( 'ABCDE' ), 'ucsecond: ABCDE -> ABCDE'  ) ;
+	ok( 'ABCDE' eq ucsecond( 'AbCDE' ), 'ucsecond: AbCDE -> ABCDE'  ) ;
+	ok( 'ABCde' eq ucsecond( 'AbCde' ), 'ucsecond: AbCde -> ABCde'  ) ;
+	ok( 'A'     eq ucsecond( 'A' ),     'ucsecond: A  -> A'  ) ;
+	ok( 'AB'    eq ucsecond( 'Ab' ),    'ucsecond: Ab -> AB' ) ;
+	ok( '\B'    eq ucsecond( '\b' ),    'ucsecond: \b -> \B' ) ;
+	ok( '\Bcde' eq ucsecond( '\bcde' ), 'ucsecond: \bcde -> \Bcde' ) ;
+        return ;
+}
+
+
+sub select_msgs {
+	my ( $imap, $msgs_all_hash_ref, $search_cmd, $folder ) = @_ ;
+	my ( @msgs ) ;
+
+	if ( $abletosearch ) {
+		@msgs = select_msgs_by_search( $imap, $msgs_all_hash_ref, $search_cmd, $folder ) ;
+	}else{
+		@msgs = select_msgs_by_fetch( $imap, $msgs_all_hash_ref, $search_cmd, $folder ) ;
+	}
+	return(  @msgs ) ;
+
+}
+
+sub select_msgs_by_search {
+	my ( $imap, $msgs_all_hash_ref, $search_cmd, $folder ) = @_ ;
+	my ( @msgs, @msgs_all ) ;
+
+        # Need to have the whole list in msgs_all_hash_ref
+        # without calling messages() several times.
+        # Need all messages list to avoid deleting useful cache part
+        # in case of --search or --minage or --maxage
+
+	if ( ( defined  $msgs_all_hash_ref  and $usecache )
+        or ( not defined  $maxage  and not defined  $minage  and not defined  $search_cmd  )
+        ) {
+
+       		$debugdev and myprint( "Calling messages()\n"  ) ;
+		@msgs_all = $imap->messages(  ) ;
+
+                return if ( $#msgs_all == 0 && !defined  $msgs_all[0]  ) ;
+
+                if ( defined  $msgs_all_hash_ref  ) {
+                        @{ $msgs_all_hash_ref }{ @msgs_all } =  () ;
+                }
+                # return all messages
+                if ( not defined  $maxage  and not defined  $minage  and not defined  $search_cmd  ) {
+                        return( @msgs_all ) ;
+                }
+	}
+
+        if ( defined  $search_cmd  ) {
+        	@msgs = $imap->search( $search_cmd ) ;
+                return( @msgs ) ;
+        }
+
+	# we are here only if $maxage or $minage is defined
+        @msgs = select_msgs_by_age( $imap ) ;
+	return( @msgs );
+}
+
+
+sub select_msgs_by_fetch {
+	my ( $imap, $msgs_all_hash_ref, $search_cmd, $folder ) = @_ ;
+	my ( @msgs, @msgs_all, %fetch ) ;
+
+        # Need to have the whole list in msgs_all_hash_ref
+        # without calling messages() several times.
+        # Need all messages list to avoid deleting useful cache part
+        # in case of --search or --minage or --maxage
+
+
+	$debugdev and myprint( "Calling fetch_hash()\n"  ) ;
+	my $uidnext = $imap->uidnext( $folder ) || $uidnext_default ;
+	my $fetch_hash_uids = $fetch_hash_set || "1:$uidnext" ;
+	%fetch = %{$imap->fetch_hash( $fetch_hash_uids, 'INTERNALDATE' ) } ;
+
+        @msgs_all = sort { $a <=> $b } keys  %fetch  ;
+        $debugdev and myprint( "Done fetch_hash()\n"  ) ;
+
+        return if ( $#msgs_all == 0 && !defined  $msgs_all[0]  ) ;
+
+        if ( defined  $msgs_all_hash_ref  ) {
+                 @{ $msgs_all_hash_ref }{ @msgs_all } =  () ;
+        }
+        # return all messages
+        if ( not defined  $maxage  and not defined  $minage  and not defined  $search_cmd  ) {
+                return( @msgs_all ) ;
+        }
+
+        if ( defined  $search_cmd  ) {
+		myprint( "Warning: strange to see --search with --noabletosearch, an error can happen\n"  ) ;
+        	@msgs = $imap->search( $search_cmd ) ;
+                return( @msgs ) ;
+        }
+
+	# we are here only if $maxage or $minage is defined
+	my( @max, @min, $maxage_epoch, $minage_epoch ) ;
+	if ( defined  $maxage  ) { $maxage_epoch = $timestart_int - $NB_SECONDS_IN_A_DAY * $maxage ; }
+	if ( defined  $minage  ) { $minage_epoch = $timestart_int - $NB_SECONDS_IN_A_DAY * $minage ; }
+	foreach my $msg ( @msgs_all ) {
+		my $idate = $fetch{ $msg }->{'INTERNALDATE'} ;
+		#myprint( "$idate\n"  ) ;
+		if ( defined  $maxage  and ( epoch( $idate ) >= $maxage_epoch ) ) {
+			push  @max, $msg  ;
+		}
+		if ( defined  $minage  and ( epoch( $idate ) <= $minage_epoch ) ) {
+			push  @min, $msg  ;
+		}
+	}
+        @msgs = msgs_from_maxmin( \@max, \@min ) ;
+	return( @msgs ) ;
+}
+
+sub select_msgs_by_age {
+	my( $imap ) = @_ ;
+
+	my( @max, @min, @msgs, @inter, @union ) ;
+
+	if ( defined  $maxage  ) {
+		@max = $imap->sentsince( $timestart_int - $NB_SECONDS_IN_A_DAY * $maxage ) ;
+	}
+	if ( defined  $minage  ) {
+		@min = $imap->sentbefore( $timestart_int - $NB_SECONDS_IN_A_DAY * $minage ) ;
+	}
+
+	@msgs = msgs_from_maxmin( \@max, \@min ) ;
+	return( @msgs ) ;
+}
+
+sub msgs_from_maxmin {
+	my( $max_ref, $min_ref ) = @_ ;
+	my( @max, @min, @msgs, @inter, @union ) ;
+
+	@max = @{ $max_ref } ;
+	@min = @{ $min_ref } ;
+
+	SWITCH: {
+		unless( defined  $minage  ) { @msgs = @max ; last SWITCH } ;
+		unless( defined  $maxage  ) { @msgs = @min ; last SWITCH } ;
+		my ( %union, %inter ) ;
+		foreach my $m ( @min, @max ) { $union{ $m }++ && $inter{ $m }++ }
+		@inter = sort { $a <=> $b } keys  %inter  ;
+		@union = sort { $a <=> $b } keys  %union  ;
+		# normal case
+		if ( $minage <= $maxage )  { @msgs = @inter ; last SWITCH } ;
+		# just exclude messages between
+		if ( $minage > $maxage )  { @msgs = @union ; last SWITCH } ;
+
+	}
+	return( @msgs ) ;
+}
+
+sub tests_msgs_from_maxmin {
+	my @msgs ;
+	$maxage = $NUMBER_200 ;
+	@msgs = msgs_from_maxmin( [ '1', '2' ], [ '2', '3' ] ) ;
+	ok( 0 == compare_lists( [ '1', '2' ], \@msgs ), 'msgs_from_maxmin: maxage++' ) ;
+	$minage = $NUMBER_100 ;
+	@msgs = msgs_from_maxmin( [ '1', '2' ], [ '2', '3' ] ) ;
+	ok( 0 == compare_lists( [ '2' ], \@msgs ), 'msgs_from_maxmin:  -maxage++minage-' ) ;
+	$minage = $NUMBER_300 ;
+	@msgs = msgs_from_maxmin( [ '1', '2' ], [ '2', '3' ] ) ;
+	ok( 0 == compare_lists( [ '1', '2', '3' ], \@msgs ), 'msgs_from_maxmin:  ++maxage-minage++' ) ;
+	$maxage = undef ;
+	@msgs = msgs_from_maxmin( [ '1', '2' ], [ '2', '3' ] ) ;
+	ok( 0 == compare_lists( [ '2', '3' ], \@msgs ), 'msgs_from_maxmin:  ++minage-' ) ;
+	return ;
+}
+
+
+sub lastuid {
+	my $imap   = shift ;
+	my $folder = shift ;
+	my $lastuid_guess  = shift ;
+	my $lastuid ;
+
+	# rfc3501: The only reliable way to identify recent messages is to
+	#          look at message flags to see which have the \Recent flag
+	#          set, or to do a SEARCH RECENT.
+	# SEARCH RECENT doesn't work this way on courrier.
+
+	my @recent_messages ;
+	# SEARCH RECENT for each transfer can be expensive with a big folder
+	# Call commented for now
+	#@recent_messages = $imap->recent(  ) ;
+	#myprint( "Recent: @recent_messages\n" ) ;
+
+	my $max_recent ;
+	$max_recent = max( @recent_messages ) ;
+
+	if ( defined  $max_recent  and ($lastuid_guess <= $max_recent ) ) {
+		$lastuid = $max_recent ;
+	}else{
+		$lastuid = $lastuid_guess
+	}
+	return( $lastuid ) ;
+}
+
+sub size_filtered {
+	my( $h1_size, $h1_msg, $h1_fold, $h2_fold  ) = @_ ;
+
+        $h1_size = 0 if ( ! $h1_size ) ; # null if empty or undef
+	if (defined $maxsize and $h1_size > $maxsize) {
+		myprint( "msg $h1_fold/$h1_msg skipped ($h1_size exceeds maxsize limit $maxsize bytes)\n" ) ;
+		$total_bytes_skipped += $h1_size;
+		$nb_msg_skipped += 1;
+		return( 1 ) ;
+	}
+	if (defined $minsize and $h1_size <= $minsize) {
+		myprint( "msg $h1_fold/$h1_msg skipped ($h1_size smaller than minsize $minsize bytes)\n" ) ;
+		$total_bytes_skipped += $h1_size;
+		$nb_msg_skipped += 1;
+		return( 1 ) ;
+	}
+	return( 0 ) ;
+}
+
+sub message_exists {
+	my( $imap, $msg ) = @_ ;
+	return( 1 ) if not $imap->Uid(  ) ;
+
+	my $search_uid ;
+        ( $search_uid ) = $imap->search( "UID $msg" ) ;
+        #myprint( "$search ? $msg\n"  ) ;
+        return( 1 ) if ( $search_uid eq $msg ) ;
+        return( 0 ) ;
+}
+
+sub copy_message {
+	# copy
+
+	my ( $sync, $h1_msg, $h1_fold, $h2_fold, $h1_fir_ref, $permanentflags2, $cache_dir ) = @_ ;
+	( $debug or $dry) and myprint( "msg $h1_fold/$h1_msg copying to $h2_fold $dry_message\n" ) ;
+
+	my $h1_size  = $h1_fir_ref->{$h1_msg}->{'RFC822.SIZE'}  || 0 ;
+	my $h1_flags = $h1_fir_ref->{$h1_msg}->{'FLAGS'}        || q{} ;
+	my $h1_idate = $h1_fir_ref->{$h1_msg}->{'INTERNALDATE'} || q{} ;
+
+
+        if ( size_filtered( $h1_size, $h1_msg, $h1_fold, $h2_fold  ) ) {
+        	$h1_nb_msg_processed +=1 ;
+                return ;
+        }
+
+	debugsleep( $sync ) ;
+	myprint( "- msg $h1_fold/$h1_msg S[$h1_size] F[$h1_flags] I[$h1_idate] has RFC822.SIZE null!\n" ) if ( ! $h1_size )   ;
+
+
+        if ( $checkmessageexists and not message_exists( $imap1, $h1_msg ) ) {
+		$total_bytes_skipped += $h1_size;
+		$nb_msg_skipped += 1;
+        	$h1_nb_msg_processed +=1 ;
+                return ;
+        }
+        if ( $sync->{debugmemory} ) {
+                myprintf("C1: Memory consumption: %.1f MiB\n", memory_consumption(  ) / $KIBI / $KIBI) ;
+        }
+
+	my ( $string, $string_len ) ;
+        ( $string_len ) = message_for_host2( $sync, $h1_msg, $h1_fold, $h1_size, $h1_flags, $h1_idate, $h1_fir_ref, \$string ) ;
+
+        if ( $sync->{debugmemory} ) {
+                myprintf("C2: Memory consumption: %.1f MiB\n", memory_consumption(  ) / $KIBI / $KIBI) ;
+        }
+
+        # not defined or empty $string
+        if ( ( not $string ) and ( not $string_len ) ) {
+		myprint( "- msg $h1_fold/$h1_msg skipped.\n"  ) ;
+		$total_bytes_skipped += $h1_size;
+		$nb_msg_skipped += 1;
+                $h1_nb_msg_processed +=1 ;
+                return ;
+        }
+
+        # Lines too long (or not enough) => do no copy or fix
+        if ( ( defined $maxlinelength ) or ( defined $minmaxlinelength ) ) {
+		$string = linelengthstuff( $string, $h1_fold, $h1_msg, $string_len, $h1_size, $h1_flags, $h1_idate ) ;
+		if ( not defined  $string  ) {
+			$h1_nb_msg_processed +=1 ;
+			$total_bytes_skipped += $h1_size ;
+			$nb_msg_skipped += 1 ;
+			return ;
+		}
+	}
+
+	my $h1_date = date_for_host2( $h1_msg, $h1_idate ) ;
+
+	( $debug or $debugflags ) and
+        myprint( "Host1 flags init msg $h1_fold/$h1_msg date [$h1_date] flags [$h1_flags] size [$h1_size]\n"  ) ;
+
+	$h1_flags = flags_for_host2( $h1_flags, $permanentflags2 ) ;
+
+	( $debug or $debugflags ) and
+        myprint( "Host1 flags filt msg $h1_fold/$h1_msg date [$h1_date] flags [$h1_flags] size [$h1_size]\n"  ) ;
+
+	$h1_date = undef if ($h1_date eq q{});
+
+	my $new_id = append_message_on_host2( \$string, $h1_fold, $h1_msg, $string_len, $h2_fold, $h1_size, $h1_flags, $h1_date, $cache_dir ) ;
+
+	if ( $new_id and $syncflagsaftercopy ) {
+        	sync_flags_after_copy( $h1_fold, $h1_msg, $h1_flags, $h2_fold, $new_id, $permanentflags2 ) ;
+        }
+
+	if ( $sync->{debugmemory} ) {
+        	myprintf("C3: Memory consumption: %.1f MiB\n", memory_consumption(  ) / $KIBI / $KIBI) ;
+        }
+
+        return $new_id ;
+}
+
+
+
+sub linelengthstuff {
+	my( $string, $h1_fold, $h1_msg, $string_len, $h1_size, $h1_flags, $h1_idate  ) = @_ ;
+	my $maxlinelength_string = max_line_length( $string ) ;
+        $debugmaxlinelength and myprint( "msg $h1_fold/$h1_msg maxlinelength: $maxlinelength_string\n"  ) ;
+
+        if ( ( defined $minmaxlinelength )  and ( $maxlinelength_string <= $minmaxlinelength ) ) {
+		my $subject = subject( $string ) ;
+         	$debugdev and myprint( "- msg $h1_fold/$h1_msg skipped S[$h1_size] F[$h1_flags] I[$h1_idate] "
+                      	. "(Subject:[$subject]) (max line length under minmaxlinelength $minmaxlinelength bytes)\n" ) ;
+         	return ;
+        }
+
+        if ( ( defined $maxlinelength )  and ( $maxlinelength_string > $maxlinelength ) ) {
+         	my $subject = subject( $string ) ;
+		if ( $maxlinelengthcmd ) {
+			$string = pipemess( $string, $maxlinelengthcmd ) ;
+			# string undef means something was bad.
+			if ( not ( defined  $string  ) ) {
+				myprint( "- msg $h1_fold/$h1_msg {$string_len} S[$h1_size] F[$h1_flags] I[$h1_idate] "
+				      . "(Subject:[$subject]) could not be successfully transformed by --maxlinelengthcmd option\n" ) ;
+				return ;
+			}else{
+				return $string ;
+			}
+		}
+         	myprint( "- msg $h1_fold/$h1_msg skipped S[$h1_size] F[$h1_flags] I[$h1_idate] "
+                      . "(Subject:[$subject]) (line length exceeds maxlinelength $maxlinelength bytes)\n" ) ;
+		return ;
+	}
+	return $string ;
+}
+
+
+sub message_for_host2 {
+
+# global variable list: 
+# @skipmess
+# @regexmess
+# @pipemess
+# $addheader
+# $debugcontent
+# $debug
+# 
+# API current
+#
+# at failure: 
+#   * return nothing ( will then be undef or () )
+#   * $string_ref content is undef or empty
+# at success:
+#   * return string length ($string_ref content length)
+#   * $string_ref content filled with message
+
+# API future
+# 
+# 
+	my ( $sync, $h1_msg, $h1_fold, $h1_size, $h1_flags, $h1_idate, $h1_fir_ref, $string_ref ) = @_ ;
+
+        # abort when missing a parameter
+        if ( (!$sync) or  (!$h1_msg) or (!$h1_fold) or (!$h1_size) or (!defined $h1_flags) or (!$h1_idate) or (!$h1_fir_ref) or (!$string_ref) ) {
+                return ;
+        }
+
+        if ( $sync->{debugmemory} ) {
+                myprintf("M1: Memory consumption: %.1f MiB\n", memory_consumption(  ) / $KIBI / $KIBI) ;
+        }
+
+        my $imap1 = $sync->{imap1} ;
+	my $string_ok = $imap1->message_to_file( $string_ref, $h1_msg ) ;
+
+        if ( $sync->{debugmemory} ) {
+                myprintf("M2: Memory consumption: %.1f MiB\n", memory_consumption(  ) / $KIBI / $KIBI) ;
+        }
+
+	my $string_len = length_ref( $string_ref  ) ;
+
+
+	unless ( defined  $string_ok  and $string_len ) {
+		# undef or 0 length
+		my $error = join q{},
+			"- msg $h1_fold/$h1_msg {$string_len} S[$h1_size] F[$h1_flags] I[$h1_idate] could not be fetched: ",
+			$imap1->LastError || q{}, "\n"  ;
+		errors_incr( $sync, $error ) ;
+		$total_bytes_error += $h1_size if ( $h1_size ) ;
+                $h1_nb_msg_processed +=1 ;
+		return ;
+	}
+
+	if ( @skipmess ) {
+		my $match = skipmess( ${ $string_ref } ) ;
+                # string undef means the eval regex was bad.
+                if ( not ( defined  $match  ) ) {
+                	myprint(
+			"- msg $h1_fold/$h1_msg {$string_len} S[$h1_size] F[$h1_flags] I[$h1_idate]"
+                        . " could not be skipped by --skipmess option, bad regex\n" ) ;
+                	return ;
+                }
+                if ( $match ) {
+                        my $subject = subject( ${ $string_ref } ) ;
+                        myprint( "- msg $h1_fold/$h1_msg {$string_len} S[$h1_size] F[$h1_flags] I[$h1_idate]"
+                            . " (Subject:[$subject]) skipped by --skipmess\n" ) ;
+                	return ;
+                }
+	}
+
+	if ( @regexmess ) {
+		${ $string_ref } = regexmess( ${ $string_ref } ) ;
+                # string undef means the eval regex was bad.
+                if ( not ( defined  ${ $string_ref }  ) ) {
+                	myprint(
+			"- msg $h1_fold/$h1_msg {$string_len} S[$h1_size] F[$h1_flags] I[$h1_idate]"
+                        . " could not be transformed by --regexmess\n" ) ;
+                	return ;
+                }
+	}
+
+	if ( @pipemess ) {
+		${ $string_ref } = pipemess( ${ $string_ref }, @pipemess ) ;
+                # string undef means something was bad.
+                if ( not ( defined  ${ $string_ref }  ) ) {
+                	myprint(
+			"- msg $h1_fold/$h1_msg {$string_len} S[$h1_size] F[$h1_flags] I[$h1_idate]"
+                        . " could not be successfully transformed by --pipemess option\n" ) ;
+                	return ;
+                }
+	}
+
+        if ( $addheader and defined $h1_fir_ref->{$h1_msg}->{'NO_HEADER'} ) {
+                my $header = add_header( $h1_msg ) ;
+                $debug and myprint( "msg $h1_fold/$h1_msg adding custom header [$header]\n"  ) ;
+                ${ $string_ref } = $header . "\r\n" . ${ $string_ref } ;
+        }
+
+        $string_len = length_ref( $string_ref  ) ;
+
+	$debugcontent and myprint(
+		q{=} x $STD_CHAR_PER_LINE, "\n",
+		"F message content begin next line ($string_len characters long)\n",
+		${ $string_ref },
+		"F message content ended on previous line\n", q{=} x $STD_CHAR_PER_LINE, "\n" ) ;
+
+        if ( $sync->{debugmemory} ) {
+                myprintf("M3: Memory consumption: %.1f MiB\n", memory_consumption(  ) / $KIBI / $KIBI) ;
+        }
+
+	return $string_len ;
+}
+
+sub tests_message_for_host2 {
+        
+        my ( $sync, $h1_msg, $h1_fold, $h1_size, $h1_flags, $h1_idate, $h1_fir_ref, $string_ref ) ;
+        
+        is( undef, message_for_host2(  ), q{message_for_host2: no args} ) ;
+        is( undef, message_for_host2( $sync, $h1_msg, $h1_fold, $h1_size, $h1_flags, $h1_idate, $h1_fir_ref, $string_ref ), q{message_for_host2: undef args} ) ;
+
+        require Test::MockObject ;
+        my $imapT = Test::MockObject->new(  ) ;
+        $sync->{imap1} = $imapT ;
+        my $string ;
+        
+        $h1_msg = 1 ;
+        $h1_fold = 'FoldFoo';
+        $h1_size =  9 ; 
+        $h1_flags = '' ; 
+        $h1_idate = '10-Jul-2015 09:00:00 +0200' ;
+        $h1_fir_ref = {} ;
+        $string_ref = \$string ;
+        $imapT->mock( 'message_to_file',   
+                sub {
+                        my ( $imap, $string_ref, $msg ) = @_ ;
+                        ${$string_ref} = 'blablabla' ;
+                        return length ${$string_ref} ;
+                }
+        ) ;
+        is( 9, message_for_host2( $sync, $h1_msg, $h1_fold, $h1_size, $h1_flags, $h1_idate, $h1_fir_ref, $string_ref ), 
+        q{message_for_host2: msg 1 == "blablabla", length} ) ;
+        is( 'blablabla', $string, q{message_for_host2: msg 1 == "blablabla", value} ) ;
+ 
+        # so far so good
+        # now the --pipemess stuff
+
+	SKIP: {
+                Readonly my $NB_WIN_tests_message_for_host2 => 0 ;
+		skip( 'Not on MSWin32', $NB_WIN_tests_message_for_host2 ) if ('MSWin32' ne $OSNAME) ;
+		# Windows
+		# "type" command does not accept redirection of STDIN with <
+		# "sort" does
+
+	} ;
+
+	SKIP: {
+                Readonly my $NB_UNX_tests_message_for_host2 => 6 ;
+		skip( 'Not on Unix', $NB_UNX_tests_message_for_host2 ) if ('MSWin32' eq $OSNAME) ;
+		# Unix
+                
+                # no change by cat
+                @pipemess = ( 'cat' ) ;
+                is( 9, message_for_host2( $sync, $h1_msg, $h1_fold, $h1_size, $h1_flags, $h1_idate, $h1_fir_ref, $string_ref ), 
+                q{message_for_host2: --pipemess 'cat', length} ) ;
+                is( 'blablabla', $string, q{message_for_host2: --pipemess 'cat', value} ) ;
+
+                
+                # failure by false
+                @pipemess = ( 'false' ) ;
+                is( undef, message_for_host2( $sync, $h1_msg, $h1_fold, $h1_size, $h1_flags, $h1_idate, $h1_fir_ref, $string_ref ), 
+                q{message_for_host2: --pipemess 'false', length} ) ;
+                is( undef, $string, q{message_for_host2: --pipemess 'false', value} ) ;
+
+                # failure by true since no output
+                @pipemess = ( 'true' ) ;
+                is( undef, message_for_host2( $sync, $h1_msg, $h1_fold, $h1_size, $h1_flags, $h1_idate, $h1_fir_ref, $string_ref ), 
+                q{message_for_host2: --pipemess 'true', length} ) ;
+                is( undef, $string, q{message_for_host2: --pipemess 'true', value} ) ;
+        }
+        return ;
+}
+
+sub length_ref {
+        my $string_ref = shift ;
+        my $string_len = defined  ${ $string_ref }  ? length( ${ $string_ref } ) : q{} ; # length or empty string
+        return $string_len ;
+}
+
+sub tests_length_ref {
+        my $notdefined ;
+        is( q{}, length_ref( \$notdefined ), q{length_ref: value not defined} ) ;
+        my $notref ;
+        is( q{}, length_ref( $notref ), q{length_ref: param not a ref} ) ;
+
+        my $lala = 'lala' ;
+        is( 4, length_ref( \$lala ), q{length_ref: lala length == 4} ) ;
+        is( 4, length_ref( \'lili' ), q{length_ref: lili length == 4} ) ;
+        return ;
+}
+
+sub date_for_host2 {
+	my( $h1_msg, $h1_idate ) = @_ ;
+
+	my $h1_date = q{} ;
+
+	if ( $syncinternaldates ) {
+		$h1_date = $h1_idate ;
+		$debug and myprint( "internal date from host1: [$h1_date]\n"  ) ;
+		$h1_date = good_date( $h1_date ) ;
+		$debug and myprint( "internal date from host1: [$h1_date] (fixed)\n"  ) ;
+	}
+
+	if ( $idatefromheader ) {
+		$h1_date = $imap1->get_header( $h1_msg, 'Date' ) ;
+		$debug and myprint( "header date from host1: [$h1_date]\n"  ) ;
+		$h1_date = good_date( $h1_date ) ;
+		$debug and myprint( "header date from host1: [$h1_date] (fixed)\n"  ) ;
+	}
+
+	return( $h1_date ) ;
+}
+
+sub flags_for_host2 {
+	my( $h1_flags, $permanentflags2 ) = @_ ;
+	# RFC 2060: This flag can not be altered by any client
+	$h1_flags =~ s@\\Recent\s?@@xgi ;
+        my $h1_flags_re ;
+        if ( @regexflag and defined( $h1_flags_re = flags_regex( $h1_flags ) ) ) {
+                $h1_flags = $h1_flags_re ;
+        }
+	$h1_flags = flagscase( $h1_flags ) if $flagscase ;
+        $h1_flags = flags_filter( $h1_flags, $permanentflags2) if ( $permanentflags2 and $filterflags ) ;
+
+	return( $h1_flags ) ;
+}
+
+sub subject {
+	my $string = shift ;
+	my $subject = q{} ;
+
+        my $header = extract_header( $string ) ;
+
+        if( $header =~ m/^Subject:\s*([^\n\r]*)\r?$/msx ) {
+        	#myprint( "MMM[$1]\n"  ) ;
+        	$subject = $1 ;
+        }
+	return( $subject ) ;
+}
+
+sub tests_subject {
+	ok( q{} eq subject( q{} ), 'subject: null') ;
+	ok( 'toto le hero' eq subject( 'Subject: toto le hero' ), 'subject: toto le hero') ;
+	ok( 'toto le hero' eq subject( 'Subject:toto le hero' ), 'subject: toto le hero blank') ;
+	ok( 'toto le hero' eq subject( "Subject:toto le hero\r\n" ), 'subject: toto le hero\r\n') ;
+
+        my $MESS ;
+	$MESS = <<'EOF';
+From: lalala
+Subject: toto le hero
+Date: zzzzzz
+
+Boogie boogie
+EOF
+	ok( 'toto le hero' eq subject( $MESS ), 'subject: toto le hero 2') ;
+
+	$MESS = <<'EOF';
+Subject: toto le hero
+From: lalala
+Date: zzzzzz
+
+Boogie boogie
+EOF
+	ok( 'toto le hero' eq subject( $MESS ), 'subject: toto le hero 3') ;
+
+
+	$MESS = <<'EOF';
+From: lalala
+Subject: cuicui
+Date: zzzzzz
+
+Subject: toto le hero
+EOF
+	ok( 'cuicui' eq subject( $MESS ), 'subject: cuicui') ;
+
+	$MESS = <<'EOF';
+From: lalala
+Date: zzzzzz
+
+Subject: toto le hero
+EOF
+	ok( q{} eq subject( $MESS ), 'subject: null but body could') ;
+
+	return ;
+}
+
+
+# GlobVar
+# $dry
+# $max_msg_size_in_bytes
+# $imap2
+# $imap1
+# $total_bytes_error
+# $h1_nb_msg_processed
+# $h2_uidguess
+# $total_bytes_transferred
+# $nb_msg_transferred
+# $begin_transfer_time
+# $time_spent
+# ...
+#
+#
+sub append_message_on_host2 {
+	my( $string_ref, $h1_fold, $h1_msg, $string_len, $h2_fold, $h1_size, $h1_flags, $h1_date, $cache_dir ) = @_ ;
+	if ( $sync->{debugmemory} ) {
+        	myprintf("A1: Memory consumption: %.1f MiB\n", memory_consumption(  ) / $KIBI / $KIBI) ;
+        }
+
+	my $new_id ;
+	if ( ! $dry ) {
+		$max_msg_size_in_bytes = max( $h1_size, $max_msg_size_in_bytes ) ;
+		$new_id = $imap2->append_string( $h2_fold, ${ $string_ref }, $h1_flags, $h1_date ) ;
+	        if ( $sync->{debugmemory} ) {
+        	        myprintf("A2: Memory consumption: %.1f MiB\n", memory_consumption(  ) / $KIBI / $KIBI) ;
+                }
+		if ( ! $new_id){
+                	my $subject = subject( ${ $string_ref } ) ;
+                        my $error_imap = $imap2->LastError || q{} ;
+			my $error = "- msg $h1_fold/$h1_msg {$string_len} couldn't append  (Subject:[$subject]) to folder $h2_fold: $error_imap\n" ;
+			errors_incr( $sync, $error ) ;
+			$total_bytes_error += $h1_size;
+                        $h1_nb_msg_processed +=1 ;
+			return ;
+		}
+		else{
+			# good
+			# $new_id is an id if the IMAP server has the
+			# UIDPLUS capability else just a ref
+			if ( $new_id !~ m{^\d+$}x ) {
+				$new_id = lastuid( $imap2, $h2_fold, $h2_uidguess ) ;
+			}
+			$h2_uidguess += 1 ;
+			$total_bytes_transferred += $h1_size ;
+			$nb_msg_transferred += 1 ;
+                        $h1_nb_msg_processed +=1 ;
+
+                        my $time_spent = timesince( $begin_transfer_time ) ;
+                        my $rate = bytes_display_string( $total_bytes_transferred / $time_spent ) ;
+                        my $eta = eta( $time_spent,
+                                       $h1_nb_msg_processed, $h1_nb_msg_start, $nb_msg_transferred ) ;
+                        my $amount_transferred = bytes_display_string( $total_bytes_transferred ) ;
+			myprintf( "msg %s/%-19s copied to %s/%-10s %.2f msgs/s  %s/s %s copied  %s\n",
+                        $h1_fold, "$h1_msg {$string_len}", $h2_fold, $new_id, $nb_msg_transferred/$time_spent, $rate,
+                        $amount_transferred,
+                        $eta );
+                        sleep_if_needed( $time_spent, $total_bytes_transferred, $nb_msg_transferred ) ;
+                        if ( $usecache and $cacheaftercopy and $new_id =~ m{^\d+$}x ) {
+				$debugcache and myprint( "touch $cache_dir/${h1_msg}_$new_id\n"  ) ;
+				touch( "$cache_dir/${h1_msg}_$new_id" )
+                        	or croak( "Couldn't touch $cache_dir/${h1_msg}_$new_id" ) ;
+                        }
+			if ( $delete ) {
+				delete_message_on_host1( $h1_msg, $h1_fold ) ;
+			}
+			#myprint( "PRESS ENTER" ) and my $a = <> ;
+                        return( $new_id ) ;
+		}
+	}
+	else{
+		# NOOP to avoid timeout on large folders.
+		$imap2->noop(  ) ;
+		$nb_msg_skipped_dry_mode += 1 ;
+                $h1_nb_msg_processed +=1 ;
+	}
+
+	return ;
+}
+
+sub sleep_if_needed {
+	my( $time_spent, $total_bytes_transferred, $nb_msg_transferred ) = @_ ;
+        my $sleep_max_messages = sleep_max_messages( $nb_msg_transferred, $time_spent, $maxmessagespersecond ) ;
+        my $sleep_max_bytes = sleep_max_bytes( $total_bytes_transferred, $time_spent, $maxbytespersecond  ) ;
+        my $sleep_max = max( $sleep_max_messages, $sleep_max_bytes ) ;
+        if ( $sleep_max > 0 ) {
+        	myprintf( "sleeping %.2f s\n", $sleep_max ) ;
+                sleep $sleep_max ;
+        }
+	return ;
+}
+
+sub sleep_max_messages {
+	# how long we have to sleep to go under max_messages_per_second
+        my( $nb_msg_transferred, $time_spent, $maxmessagespersecond ) = @_ ;
+        if ( ( not defined  $maxmessagespersecond  ) or $maxmessagespersecond <= 0 ) { return( 0 ) } ;
+        my $sleep = ( $nb_msg_transferred / $maxmessagespersecond ) - $time_spent ;
+        # the sleep must be positive
+        return( max( 0, $sleep ) ) ;
+}
+
+
+sub tests_sleep_max_messages {
+	ok( 0 == sleep_max_messages( 4, 2, undef ),  'sleep_max_messages: maxmessagespersecond = undef') ;
+	ok( 0 == sleep_max_messages( 4, 2, 0 ),  'sleep_max_messages: maxmessagespersecond = 0') ;
+	ok( 0 == sleep_max_messages( 4, 2, $MINUS_ONE ), 'sleep_max_messages: maxmessagespersecond = -1') ;
+	ok( 0 == sleep_max_messages( 4, 2, 2 ),  'sleep_max_messages: maxmessagespersecond = 2 max reached') ;
+	ok( 2 == sleep_max_messages( 8, 2, 2 ),  'sleep_max_messages: maxmessagespersecond = 2 max over') ;
+	ok( 0 == sleep_max_messages( 2, 2, 2 ),  'sleep_max_messages: maxmessagespersecond = 2 max not reached') ;
+	return ;
+}
+
+
+sub sleep_max_bytes {
+	# how long we have to sleep to go under max_bytes_per_second
+        my( $total_bytes_transferred, $time_spent, $maxbytespersecond ) = @_ ;
+        if ( ( not defined  $maxbytespersecond  ) or $maxbytespersecond <= 0 ) { return( 0 ) } ;
+        my $sleep = ( $total_bytes_transferred / $maxbytespersecond ) - $time_spent ;
+        # the sleep must be positive
+        return( max( 0, $sleep ) ) ;
+}
+
+
+sub tests_sleep_max_bytes {
+	ok( 0 == sleep_max_bytes( 4000, 2, undef ),  'sleep_max_bytes: maxbytespersecond = undef') ;
+	ok( 0 == sleep_max_bytes( 4000, 2, 0 ),  'sleep_max_bytes: maxbytespersecond = 0') ;
+	ok( 0 == sleep_max_bytes( 4000, 2, $MINUS_ONE ), 'sleep_max_bytes: maxbytespersecond = -1') ;
+	ok( 0 == sleep_max_bytes( 4000, 2, 2000 ),  'sleep_max_bytes: maxbytespersecond = 2 max reached') ;
+	ok( 2 == sleep_max_bytes( 8000, 2, 2000 ),  'sleep_max_bytes: maxbytespersecond = 2 max over') ;
+	ok( 0 == sleep_max_bytes( 2000, 2, 2000 ),  'sleep_max_bytes: maxbytespersecond = 2 max not reached') ;
+	return ;
+}
+
+
+
+
+# 6 GlobVar: $dry_message $dry $imap1 $h1_nb_msg_deleted $expunge $expunge1
+sub delete_message_on_host1  {
+	my( $h1_msg, $h1_fold ) = @_ ;
+	my $expunge_message = q{} ;
+	$expunge_message = 'and expunged' if ( $expungeaftereach and ( $expunge or $expunge1 ) ) ;
+	myprint( "Host1 msg $h1_fold/$h1_msg marked deleted $expunge_message $dry_message\n"  ) ;
+        if ( ! $dry ) {
+        	$imap1->delete_message( $h1_msg ) ;
+        	$h1_nb_msg_deleted += 1 ;
+        	$imap1->expunge(  ) if ( $expungeaftereach and ( $expunge or $expunge1 ) ) ;
+        }
+        return ;
+}
+
+
+sub eta {
+	my( $my_time_spent, $h1_nb_processed, $h1_nb_msg_start, $nb_transferred ) = @_ ;
+	return( q{} ) if not $foldersizes ;
+
+        my $time_remaining = time_remaining( $my_time_spent, $h1_nb_processed, $h1_nb_msg_start, $nb_transferred ) ;
+        my $nb_msg_remaining = $h1_nb_msg_start - $h1_nb_processed ;
+        my $eta_date = localtime( time + $time_remaining ) ;
+        return( mysprintf( 'ETA: %s  %1.0f s  %s/%s msgs left', $eta_date, $time_remaining, $nb_msg_remaining, $h1_nb_msg_start ) ) ;
+}
+
+sub time_remaining {
+
+	my( $my_time_spent, $h1_nb_processed, $h1_nb_msg_start, $nb_transferred ) = @_ ;
+
+	my $time_remaining = ( $my_time_spent / $nb_transferred ) * ( $h1_nb_msg_start - $h1_nb_processed ) ;
+	return( $time_remaining ) ;
+}
+
+
+sub tests_time_remaining {
+
+	ok( 1 == time_remaining( 1, 1,  2, 1 ), 'time_remaining: 1, 1, 2, 1 -> 1'  ) ;
+	ok( 1 == time_remaining( 9, 9, 10, 9 ), 'time_remaining: 9, 9, 10, 9 -> 1' ) ;
+	ok( 9 == time_remaining( 1, 1, 10, 1 ), 'time_remaining: 1, 1, 10, 1 -> 1' ) ;
+	return ;
+}
+
+
+sub cache_map {
+	my ( $cache_files_ref, $h1_msgs_ref, $h2_msgs_ref ) = @_;
+	my ( %map1_2, %map2_1, %done2 ) ;
+
+	my $h1_msgs_hash_ref = {  } ;
+	my $h2_msgs_hash_ref = {  } ;
+
+	@{ $h1_msgs_hash_ref }{ @{ $h1_msgs_ref } } = (  ) ;
+	@{ $h2_msgs_hash_ref }{ @{ $h2_msgs_ref } } = (  ) ;
+
+	foreach my $file ( sort @{ $cache_files_ref } ) {
+		$debugcache and myprint( "C12: $file\n"  ) ;
+		( $uid1, $uid2 ) = match_a_cache_file( $file ) ;
+
+		if (  exists( $h1_msgs_hash_ref->{ defined  $uid1  ? $uid1 : q{} } )
+		  and exists( $h2_msgs_hash_ref->{ defined  $uid2  ? $uid2 : q{} } ) ) {
+		  	# keep only the greatest uid2
+			# 130_2301 and
+			# 130_231  => keep only 130 -> 2301
+
+			# keep only the greatest uid1
+			# 1601_260 and
+			#  161_260 => keep only 1601 -> 260
+		  	my $max_uid2 = max( $uid2, $map1_2{ $uid1 } || $MINUS_ONE ) ;
+			if ( exists $done2{ $max_uid2 } ) {
+				if ( $done2{ $max_uid2 } < $uid1 )  {
+					$map1_2{ $uid1 } = $max_uid2 ;
+					delete $map1_2{ $done2{ $max_uid2 } } ;
+					$done2{ $max_uid2 } = $uid1 ;
+				}
+			}else{
+				$map1_2{ $uid1 } = $max_uid2 ;
+				$done2{ $max_uid2 } = $uid1 ;
+			}
+		};
+
+	}
+	%map2_1 = reverse %map1_2 ;
+	return( \%map1_2, \%map2_1) ;
+}
+
+sub tests_cache_map {
+	#$debugcache = 1 ;
+	my @cache_files = qw (
+	100_200
+	101_201
+	120_220
+	142_242
+	143_243
+	177_277
+	177_278
+	177_279
+	155_255
+	180_280
+	181_280
+	182_280
+	130_231
+	130_2301
+	161_260
+	1601_260
+	) ;
+
+	my $msgs_1 = [120, 142, 143, 144, 161, 1601,           177,      182, 130 ];
+	my $msgs_2 = [     242, 243,       260,      299, 377, 279, 255, 280, 231, 2301 ];
+
+	my( $c12, $c21 ) ;
+	ok( ( $c12, $c21 ) = cache_map( \@cache_files, $msgs_1, $msgs_2 ), 'cache_map: 02' );
+	my $a1 = [ sort { $a <=> $b } keys %{ $c12 } ] ;
+	my $a2 = [ sort { $a <=> $b } keys %{ $c21 } ] ;
+	ok( 0 == compare_lists( [ 130, 142, 143,      177, 182, 1601      ], $a1 ), 'cache_map: 03' );
+	ok( 0 == compare_lists( [      242, 243, 260, 279, 280,      2301 ], $a2 ), 'cache_map: 04' );
+	ok( ! $c12->{161},        'cache_map: ! 161 ->  260' );
+	ok( 260  == $c12->{1601}, 'cache_map:  1601 ->  260' );
+	ok( 2301 == $c12->{130},  'cache_map:   130 -> 2301' );
+	#myprint( $c12->{1601}, "\n" ) ;
+	return ;
+
+}
+
+sub cache_dir_fix {
+	my $cache_dir = shift ;
+        $cache_dir =~ s/([;<>\*\|`&\$!#\(\)\[\]\{\}:'"\\])/\\$1/xg ;
+        #myprint( "cache_dir_fix: $cache_dir\n"  ) ;
+	return( $cache_dir ) ;
+}
+
+sub tests_cache_dir_fix {
+	ok( 'lalala' eq  cache_dir_fix('lalala'),  'cache_dir_fix: lalala -> lalala' );
+	ok( 'ii\\\\ii' eq  cache_dir_fix('ii\ii'), 'cache_dir_fix: ii\ii -> ii\\\\ii' );
+	ok( 'ii@ii' eq  cache_dir_fix('ii@ii'),  'cache_dir_fix: ii@ii -> ii@ii' );
+	ok( 'ii@ii\\:ii' eq  cache_dir_fix('ii@ii:ii'), 'cache_dir_fix: ii@ii:ii -> ii@ii\\:ii' );
+	ok( 'i\\\\i\\\\ii' eq  cache_dir_fix('i\i\ii'), 'cache_dir_fix: i\i\ii -> i\\\\i\\\\ii' );
+	ok( 'i\\\\ii' eq  cache_dir_fix('i\\ii'), 'cache_dir_fix: i\\ii -> i\\\\\\\\ii' );
+	ok( '\\\\ ' eq  cache_dir_fix('\\ '), 'cache_dir_fix: \\  -> \\\\\ ' );
+	ok( '\\\\ ' eq  cache_dir_fix('\ '), 'cache_dir_fix: \  -> \\\\\ ' );
+	ok( '\[bracket\]' eq  cache_dir_fix('[bracket]'), 'cache_dir_fix: [bracket] -> \[bracket\]' );
+	return ;
+}
+
+sub cache_dir_fix_win {
+	my $cache_dir = shift ;
+        $cache_dir =~ s/(\[|\])/[$1]/xg ;
+        #myprint( "cache_dir_fix_win: $cache_dir\n"  ) ;
+	return( $cache_dir ) ;
+}
+
+sub tests_cache_dir_fix_win {
+	ok( 'lalala' eq  cache_dir_fix_win('lalala'),  'cache_dir_fix_win: lalala -> lalala' );
+	ok( '[[]bracket[]]' eq  cache_dir_fix_win('[bracket]'), 'cache_dir_fix_win: [bracket] -> [[]bracket[]]' );
+	return ;
+}
+
+
+
+
+sub get_cache {
+	my ( $cache_dir, $h1_msgs_ref, $h2_msgs_ref, $h1_msgs_all_hash_ref, $h2_msgs_all_hash_ref ) = @_;
+
+	$debugcache and myprint( "Entering get_cache\n" ) ;
+
+	-d $cache_dir or return( undef ); # exit if cache directory doesn't exist
+	$debugcache and myprint( "cache_dir    : $cache_dir\n" ) ;
+
+
+        if ( 'MSWin32' ne $OSNAME ) {
+        	$cache_dir = cache_dir_fix( $cache_dir ) ;
+        }else{
+        	$cache_dir = cache_dir_fix_win( $cache_dir ) ;
+        }
+
+	$debugcache and myprint( "cache_dir_fix: $cache_dir\n"  ) ;
+
+	my @cache_files = bsd_glob( "$cache_dir/*" ) ;
+	#$debugcache and myprint( "cache_files: [@cache_files]\n"  ) ;
+
+	$debugcache and myprint( 'cache_files: ', scalar  @cache_files , " files found\n" ) ;
+
+	my( $cache_1_2_ref, $cache_2_1_ref )
+	  = cache_map( \@cache_files, $h1_msgs_ref, $h2_msgs_ref ) ;
+
+	clean_cache( \@cache_files, $cache_1_2_ref, $h1_msgs_all_hash_ref, $h2_msgs_all_hash_ref ) ;
+
+	$debugcache and myprint( "Exiting get_cache\n" ) ;
+	return( $cache_1_2_ref, $cache_2_1_ref ) ;
+}
+
+
+sub tests_get_cache {
+
+	ok( not( get_cache('/cache_no_exist') ), 'get_cache: /cache_no_exist' );
+	ok( ( not -d 'W/tmp/cache/F1/F2' or rmtree( 'W/tmp/cache/F1/F2' )), 'get_cache: rmtree W/tmp/cache/F1/F2' ) ;
+	ok( mkpath( 'W/tmp/cache/F1/F2' ), 'get_cache: mkpath W/tmp/cache/F1/F2' ) ;
+
+	my @test_files_cache = ( qw(
+	W/tmp/cache/F1/F2/100_200
+	W/tmp/cache/F1/F2/101_201
+	W/tmp/cache/F1/F2/120_220
+	W/tmp/cache/F1/F2/142_242
+	W/tmp/cache/F1/F2/143_243
+	W/tmp/cache/F1/F2/177_277
+	W/tmp/cache/F1/F2/177_377
+	W/tmp/cache/F1/F2/177_777
+	W/tmp/cache/F1/F2/155_255
+	) ) ;
+	ok( touch( @test_files_cache ), 'get_cache: touch W/tmp/cache/F1/F2/...' ) ;
+
+
+	# on cache: 100_200 101_201 142_242 143_243 177_277 177_377 177_777 155_255
+	# on live:
+	my $msgs_1 = [120, 142, 143, 144,          177      ];
+	my $msgs_2 = [     242, 243,     299, 377, 777, 255 ];
+
+        my $msgs_all_1 = { 120 => 0, 142 => 0, 143 => 0, 144 => 0, 177 => 0 } ;
+        my $msgs_all_2 = { 242 => 0, 243 => 0, 299 => 0, 377 => 0, 777 => 0, 255 => 0 } ;
+
+	my( $c12, $c21 ) ;
+	ok( ( $c12, $c21 ) = get_cache( 'W/tmp/cache/F1/F2', $msgs_1, $msgs_2, $msgs_all_1, $msgs_all_2 ), 'get_cache: 02' );
+	my $a1 = [ sort { $a <=> $b } keys %{ $c12 } ] ;
+	my $a2 = [ sort { $a <=> $b } keys %{ $c21 } ] ;
+	ok( 0 == compare_lists( [ 142, 143, 177 ], $a1 ), 'get_cache: 03' );
+	ok( 0 == compare_lists( [ 242, 243, 777 ], $a2 ), 'get_cache: 04' );
+	ok( -f 'W/tmp/cache/F1/F2/142_242', 'get_cache: file kept 142_242');
+	ok( -f 'W/tmp/cache/F1/F2/142_242', 'get_cache: file kept 143_243');
+	ok( ! -f 'W/tmp/cache/F1/F2/100_200', 'get_cache: file removed 100_200');
+	ok( ! -f 'W/tmp/cache/F1/F2/101_201', 'get_cache: file removed 101_201');
+
+	# test clean_cache executed
+	$maxage = 2 ;
+	ok( touch(@test_files_cache), 'get_cache: touch W/tmp/cache/F1/F2/...' ) ;
+	ok( ( $c12, $c21 ) = get_cache('W/tmp/cache/F1/F2', $msgs_1, $msgs_2, $msgs_all_1, $msgs_all_2 ), 'get_cache: 02' );
+	ok( -f 'W/tmp/cache/F1/F2/142_242', 'get_cache: file kept 142_242');
+	ok( -f 'W/tmp/cache/F1/F2/142_242', 'get_cache: file kept 143_243');
+	ok( ! -f 'W/tmp/cache/F1/F2/100_200', 'get_cache: file NOT removed 100_200');
+	ok( ! -f 'W/tmp/cache/F1/F2/101_201', 'get_cache: file NOT removed 101_201');
+
+
+	# strange files
+	#$debugcache = 1 ;
+	$maxage = undef ;
+	ok( ( not -d 'W/tmp/cache/rr\uee' or rmtree( 'W/tmp/cache/rr\uee' )), 'get_cache: rmtree W/tmp/cache/rr\uee' ) ;
+	ok( mkpath( 'W/tmp/cache/rr\uee' ), 'get_cache: mkpath W/tmp/cache/rr\uee' ) ;
+
+	@test_files_cache = ( qw(
+	W/tmp/cache/rr\uee/100_200
+	W/tmp/cache/rr\uee/101_201
+	W/tmp/cache/rr\uee/120_220
+	W/tmp/cache/rr\uee/142_242
+	W/tmp/cache/rr\uee/143_243
+	W/tmp/cache/rr\uee/177_277
+	W/tmp/cache/rr\uee/177_377
+	W/tmp/cache/rr\uee/177_777
+	W/tmp/cache/rr\uee/155_255
+	) ) ;
+	ok( touch(@test_files_cache), 'get_cache: touch strange W/tmp/cache/...' ) ;
+
+	# on cache: 100_200 101_201 142_242 143_243 177_277 177_377 177_777 155_255
+	# on live:
+	$msgs_1 = [120, 142, 143, 144,          177      ] ;
+	$msgs_2 = [     242, 243,     299, 377, 777, 255 ] ;
+
+        $msgs_all_1 = { 120 => q{}, 142 => q{}, 143 => q{}, 144 => q{}, 177 => q{} } ;
+        $msgs_all_2 = { 242 => q{}, 243 => q{}, 299 => q{}, 377 => q{}, 777 => q{}, 255 => q{} } ;
+
+	ok( ( $c12, $c21 ) = get_cache('W/tmp/cache/rr\uee', $msgs_1, $msgs_2, $msgs_all_1, $msgs_all_2), 'get_cache: strange path 02' );
+	$a1 = [ sort { $a <=> $b } keys %{ $c12 } ] ;
+	$a2 = [ sort { $a <=> $b } keys %{ $c21 } ] ;
+	ok( 0 == compare_lists( [ 142, 143, 177 ], $a1 ), 'get_cache: strange path 03' );
+	ok( 0 == compare_lists( [ 242, 243, 777 ], $a2 ), 'get_cache: strange path 04' );
+	ok( -f 'W/tmp/cache/rr\uee/142_242', 'get_cache: strange path file kept 142_242');
+	ok( -f 'W/tmp/cache/rr\uee/142_242', 'get_cache: strange path file kept 143_243');
+	ok( ! -f 'W/tmp/cache/rr\uee/100_200', 'get_cache: strange path file removed 100_200');
+	ok( ! -f 'W/tmp/cache/rr\uee/101_201', 'get_cache: strange path file removed 101_201');
+	return ;
+}
+
+sub match_a_cache_file {
+	my $file = shift ;
+	my ( $cache_uid1, $cache_uid2 ) ;
+
+	return( ( undef, undef ) ) if ( ! $file ) ;
+	if ( $file =~ m{(?:^|/)(\d+)_(\d+)$}x ) {
+		$cache_uid1 = $1 ;
+		$cache_uid2 = $2 ;
+	}
+	return( $cache_uid1, $cache_uid2 ) ;
+}
+
+sub tests_match_a_cache_file {
+	my ( $tuid1, $tuid2 ) ;
+	ok( ( $tuid1, $tuid2 ) = match_a_cache_file(  ), 'match_a_cache_file: no arg' ) ;
+	ok( ! defined  $tuid1 , 'match_a_cache_file: no arg 1' ) ;
+	ok( ! defined  $tuid2 , 'match_a_cache_file: no arg 2' ) ;
+
+	ok( ( $tuid1, $tuid2 ) = match_a_cache_file( q{} ), 'match_a_cache_file: empty arg' ) ;
+	ok( ! defined  $tuid1 , 'match_a_cache_file: empty arg 1' ) ;
+	ok( ! defined  $tuid2 , 'match_a_cache_file: empty arg 2' ) ;
+
+	ok( ( $tuid1, $tuid2 ) = match_a_cache_file( '000_000' ), 'match_a_cache_file: 000_000' ) ;
+	ok( '000' eq $tuid1, 'match_a_cache_file: 000_000 1' ) ;
+	ok( '000' eq $tuid2, 'match_a_cache_file: 000_000 2' ) ;
+
+	ok( ( $tuid1, $tuid2 ) = match_a_cache_file( '123_456' ), 'match_a_cache_file: 123_456' ) ;
+	ok( '123' eq $tuid1, 'match_a_cache_file: 123_456 1' ) ;
+	ok( '456' eq $tuid2, 'match_a_cache_file: 123_456 2' ) ;
+
+	ok( ( $tuid1, $tuid2 ) = match_a_cache_file( '/tmp/truc/123_456' ), 'match_a_cache_file: /tmp/truc/123_456' ) ;
+	ok( '123' eq $tuid1, 'match_a_cache_file: /tmp/truc/123_456 1' ) ;
+	ok( '456' eq $tuid2, 'match_a_cache_file: /tmp/truc/123_456 2' ) ;
+
+	ok( ( $tuid1, $tuid2 ) = match_a_cache_file( '/lala123_456' ), 'match_a_cache_file: NO /lala123_456' ) ;
+	ok( ! $tuid1, 'match_a_cache_file: /lala123_456 1' ) ;
+	ok( ! $tuid2, 'match_a_cache_file: /lala123_456 2' ) ;
+
+	ok( ( $tuid1, $tuid2 ) = match_a_cache_file( 'la123_456' ), 'match_a_cache_file: NO la123_456' ) ;
+	ok( ! $tuid1, 'match_a_cache_file: la123_456 1' ) ;
+	ok( ! $tuid2, 'match_a_cache_file: la123_456 2' ) ;
+
+	return ;
+}
+
+sub clean_cache {
+	my ( $cache_files_ref, $cache_1_2_ref, $h1_msgs_all_hash_ref, $h2_msgs_all_hash_ref )  = @_ ;
+
+	$debugcache and myprint( "Entering clean_cache\n" ) ;
+
+	$debugcache and myprint( map { "$_ -> " . $cache_1_2_ref->{ $_ } . "\n" } keys %{ $cache_1_2_ref }  ) ;
+	foreach my $file ( @{ $cache_files_ref } ) {
+		$debugcache and myprint( "$file\n"  ) ;
+		my ( $cache_uid1, $cache_uid2 ) = match_a_cache_file( $file ) ;
+		$debugcache and myprint( "u1: $cache_uid1 u2: $cache_uid2 c12: ", $cache_1_2_ref->{ $cache_uid1 } || q{}, "\n") ;
+#		  or ( ! exists( $cache_1_2_ref->{ $cache_uid1 } ) )
+#		  or ( ! ( $cache_uid2 == $cache_1_2_ref->{ $cache_uid1 } ) )
+		if ( ( not defined  $cache_uid1  )
+		  or ( not defined  $cache_uid2  )
+                  or ( not exists  $h1_msgs_all_hash_ref->{ $cache_uid1 }  )
+                  or ( not exists  $h2_msgs_all_hash_ref->{ $cache_uid2 }  )
+                ) {
+			$debugcache and myprint( "remove $file\n"  ) ;
+			unlink $file or myprint( "$!"  ) ;
+		}
+	}
+
+	$debugcache and myprint( "Exiting clean_cache\n" ) ;
+	return( 1 ) ;
+}
+
+sub tests_clean_cache {
+
+	ok( ( not -d  'W/tmp/cache/G1/G2' or rmtree( 'W/tmp/cache/G1/G2' )), 'clean_cache: rmtree W/tmp/cache/G1/G2' ) ;
+	ok( mkpath( 'W/tmp/cache/G1/G2' ), 'clean_cache: mkpath W/tmp/cache/G1/G2' ) ;
+
+	my @test_files_cache = ( qw(
+	W/tmp/cache/G1/G2/100_200
+	W/tmp/cache/G1/G2/101_201
+	W/tmp/cache/G1/G2/120_220
+	W/tmp/cache/G1/G2/142_242
+	W/tmp/cache/G1/G2/143_243
+	W/tmp/cache/G1/G2/177_277
+	W/tmp/cache/G1/G2/177_377
+	W/tmp/cache/G1/G2/177_777
+	W/tmp/cache/G1/G2/155_255
+	) ) ;
+	ok( touch(@test_files_cache), 'clean_cache: touch W/tmp/cache/G1/G2/...' ) ;
+
+	ok( -f 'W/tmp/cache/G1/G2/100_200', 'clean_cache: 100_200 before' );
+	ok( -f 'W/tmp/cache/G1/G2/142_242', 'clean_cache: 142_242 before' );
+	ok( -f 'W/tmp/cache/G1/G2/177_277', 'clean_cache: 177_277 before' );
+	ok( -f 'W/tmp/cache/G1/G2/177_377', 'clean_cache: 177_377 before' );
+	ok( -f 'W/tmp/cache/G1/G2/177_777', 'clean_cache: 177_777 before' );
+	ok( -f 'W/tmp/cache/G1/G2/155_255', 'clean_cache: 155_255 before' );
+
+	my $cache = {
+		142 => 242,
+		177 => 777,
+	} ;
+
+        my $all_1 = {
+                142 => q{},
+                177 => q{},
+        } ;
+
+        my $all_2 = {
+                200 => q{},
+                242 => q{},
+                777 => q{},
+        } ;
+	ok( clean_cache( \@test_files_cache, $cache, $all_1, $all_2 ), 'clean_cache: ' ) ;
+
+	ok( ! -f 'W/tmp/cache/G1/G2/100_200', 'clean_cache: 100_200 after' );
+	ok(   -f 'W/tmp/cache/G1/G2/142_242', 'clean_cache: 142_242 after' );
+	ok( ! -f 'W/tmp/cache/G1/G2/177_277', 'clean_cache: 177_277 after' );
+	ok( ! -f 'W/tmp/cache/G1/G2/177_377', 'clean_cache: 177_377 after' );
+	ok(   -f 'W/tmp/cache/G1/G2/177_777', 'clean_cache: 177_777 after' );
+	ok( ! -f 'W/tmp/cache/G1/G2/155_255', 'clean_cache: 155_255 after' );
+	return ;
+}
+
+sub tests_clean_cache_2 {
+
+	ok( ( not -d  'W/tmp/cache/G1/G2' or rmtree( 'W/tmp/cache/G1/G2' )), 'clean_cache_2: rmtree W/tmp/cache/G1/G2' ) ;
+	ok( mkpath( 'W/tmp/cache/G1/G2' ), 'clean_cache_2: mkpath W/tmp/cache/G1/G2' ) ;
+
+	my @test_files_cache = ( qw(
+	W/tmp/cache/G1/G2/100_200
+	W/tmp/cache/G1/G2/101_201
+	W/tmp/cache/G1/G2/120_220
+	W/tmp/cache/G1/G2/142_242
+	W/tmp/cache/G1/G2/143_243
+	W/tmp/cache/G1/G2/177_277
+	W/tmp/cache/G1/G2/177_377
+	W/tmp/cache/G1/G2/177_777
+	W/tmp/cache/G1/G2/155_255
+	) ) ;
+	ok( touch(@test_files_cache), 'clean_cache_2: touch W/tmp/cache/G1/G2/...' ) ;
+
+	ok( -f 'W/tmp/cache/G1/G2/100_200', 'clean_cache_2: 100_200 before' );
+	ok( -f 'W/tmp/cache/G1/G2/142_242', 'clean_cache_2: 142_242 before' );
+	ok( -f 'W/tmp/cache/G1/G2/177_277', 'clean_cache_2: 177_277 before' );
+	ok( -f 'W/tmp/cache/G1/G2/177_377', 'clean_cache_2: 177_377 before' );
+	ok( -f 'W/tmp/cache/G1/G2/177_777', 'clean_cache_2: 177_777 before' );
+	ok( -f 'W/tmp/cache/G1/G2/155_255', 'clean_cache_2: 155_255 before' );
+
+	my $cache = {
+		142 => 242,
+		177 => 777,
+	} ;
+
+        my $all_1 = {
+                $NUMBER_100 => q{},
+                142 => q{},
+                177 => q{},
+        } ;
+
+        my $all_2 = {
+                200 => q{},
+                242 => q{},
+                777 => q{},
+        } ;
+
+
+
+	ok( clean_cache( \@test_files_cache, $cache, $all_1, $all_2 ), 'clean_cache_2: ' ) ;
+
+	ok(   -f 'W/tmp/cache/G1/G2/100_200', 'clean_cache_2: 100_200 after' );
+	ok(   -f 'W/tmp/cache/G1/G2/142_242', 'clean_cache_2: 142_242 after' );
+	ok( ! -f 'W/tmp/cache/G1/G2/177_277', 'clean_cache_2: 177_277 after' );
+	ok( ! -f 'W/tmp/cache/G1/G2/177_377', 'clean_cache_2: 177_377 after' );
+	ok(   -f 'W/tmp/cache/G1/G2/177_777', 'clean_cache_2: 177_777 after' );
+	ok( ! -f 'W/tmp/cache/G1/G2/155_255', 'clean_cache_2: 155_255 after' );
+	return ;
+}
+
+
+
+sub tests_mkpath {
+
+	ok( 1 == 1, 'tests_mkpath: 1 == 1' ) ;
+
+	SKIP: {
+		skip( 'Tests only for Unix', 2   ) if ( 'MSWin32' eq $OSNAME ) ;
+		my $long_path_unix = '123456789/' x 30 ;
+		ok( (-d "W/tmp/tests/long/$long_path_unix" or  mkpath( "W/tmp/tests/long/$long_path_unix" ) ), 'tests_mkpath: mkpath > 300 char' ) ;
+		ok( (-d "W/tmp/tests/long/$long_path_unix" and rmtree( 'W/tmp/tests/long/' ) ), 'tests_mkpath: rmtree > 300 char' ) ;
+        } ;
+
+	SKIP: {
+		skip( 'Tests only for MSWin32', 6  ) if ( 'MSWin32' ne $OSNAME ) ;
+		my $long_path_2_prefix =  "$tmpdir\\imapsync_tests" || '\\\?\\E:\\TEMP\\imapsync_tests'  ;
+		myprint( "long_path_2_prefix: $long_path_2_prefix\n"  ) ;
+
+		my $long_path_2   = $long_path_2_prefix . '\\' . '123456789\\' x 10 . 'END' ;
+		my $long_path_300 = $long_path_2_prefix . '\\' . '123456789\\' x 30 . 'END' ;
+
+		myprint( "$long_path_2\n"  ) ;
+
+		#ok( ( -d $long_path_2_prefix and rmtree( $long_path_2_prefix ) ), 'tests_mkpath: rmtree > 200 char' ) ;
+		#ok( ( -d $long_path_2_prefix or mkpath( "\\\\\?\\E:\\\\TEMP\\imapsync_tests" ) ), 'tests_mkpath: -d  small path 1' ) ;
+
+		ok( ( -d $long_path_2_prefix or mkpath( $long_path_2_prefix ) ), 'tests_mkpath: -d mkpath small path' ) ;
+		ok( ( -d $long_path_2_prefix ), 'tests_mkpath: -d mkpath small path done' ) ;
+		ok( ( -d $long_path_2        or mkpath( $long_path_2 ) ),        'tests_mkpath: mkpath > 200 char' ) ;
+		ok( ( -d $long_path_2 ), 'tests_mkpath: -d mkpath > 200 char done' ) ;
+		ok( ( -d $long_path_2_prefix and rmtree( $long_path_2_prefix ) ), 'tests_mkpath: rmtree > 200 char' ) ;
+		ok( (! -d $long_path_2_prefix ), 'tests_mkpath: ! -d rmtree done' ) ;
+
+		myprint( "$long_path_300\n"  ) ;
+		# This one just kill the whole process without a whisper:
+		#ok( ( -d $long_path_300        or mkpath( $long_path_300 ) ),        'tests_mkpath: mkpath fails > 300 char' ) ;
+		#ok( ( -d $long_path_300 and rmtree( $long_path_300 ) ), 'tests_mkpath: rmtree \ > 300 char' ) ;
+	} ;
+
+	return 1 ;
+}
+
+sub tests_touch {
+
+	ok( (-d 'W/tmp/tests/' or  mkpath( 'W/tmp/tests/' )), 'tests_touch: mkpath W/tmp/tests/' ) ;
+	ok( 1 == touch( 'W/tmp/tests/lala'), 'tests_touch: W/tmp/tests/lala') ;
+	ok( 1 == touch( 'W/tmp/tests/\y'), 'tests_touch: W/tmp/tests/\y') ;
+	ok( 0 == touch( '/no/no/no/aaa'), 'tests_touch: not /aaa') ;
+	ok( 1 == touch( 'W/tmp/tests/lili', 'W/tmp/tests/lolo'), 'tests_touch: 2 files') ;
+	ok( 0 == touch( 'W/tmp/tests/\y', '/no/no/aaa'), 'tests_touch: 2 files, 1 fails' ) ;
+	return ;
+}
+
+
+sub touch {
+	my @files = @_ ;
+	my $failures = 0 ;
+
+	foreach my $file ( @files ) {
+		my  $fh = IO::File->new ;
+		if ( $fh->open(">> $file" ) ) {
+			$fh->close ;
+		}else{
+                	myprint( "Could not open file $file in write/append mode\n"  ) ;
+                	$failures++ ;
+                }
+	}
+	return( ! $failures );
+}
+
+
+sub tests_tmpdir_has_colon_bug {
+
+	ok( 0 == tmpdir_has_colon_bug( q{} ),        'tmpdir_has_colon_bug: ' ) ;
+	ok( 0 == tmpdir_has_colon_bug( '/tmp' ),    'tmpdir_has_colon_bug: /tmp' ) ;
+	ok( 1 == tmpdir_has_colon_bug( 'C:' ),      'tmpdir_has_colon_bug: C:' ) ;
+	ok( 1 == tmpdir_has_colon_bug( 'C:\temp' ), 'tmpdir_has_colon_bug: C:\temp' ) ;
+
+        return( 0 ) ;
+}
+
+sub tmpdir_has_colon_bug {
+	my $path = shift ;
+
+	my $path_filtered = filter_forbidden_characters( $path ) ;
+	if ( $path_filtered ne $path ) {
+        	( -d $path_filtered ) and myprint( "Path $path was previously mistakely changed to $path_filtered\n"  ) ;
+        	return( 1 ) ;
+        }
+        return( 0 ) ;
+}
+
+sub tmpdir_fix_colon_bug {
+
+        my $err = 0 ;
+        if ( not (-d $tmpdir and -r _ and -w _) ) {
+                myprint( "tmpdir $tmpdir is not valid\n"  ) ;
+                return( 0 ) ;
+        }
+        my $cachedir_new = "$tmpdir/imapsync_cache" ;
+
+        if ( not tmpdir_has_colon_bug( $cachedir_new ) ) { return( 0 ) } ;
+
+        # check if old cache directory already exists
+        my $cachedir_old = filter_forbidden_characters( $cachedir_new ) ;
+        if ( not ( -d $cachedir_old ) ) {
+                myprint( "Old cache directory $cachedir_new no exists, nothing to do\n"  ) ;
+                return( 1 ) ;
+        }
+        # check if new cache directory already exists
+        if ( -d $cachedir_new ) {
+                myprint( "New fixed cache directory $cachedir_new already exists, not moving the old one $cachedir_old. Fix this manually.\n"  ) ;
+                return( 0 ) ;
+        }else{
+                # move the old one to the new place
+                myprint( "Moving $cachedir_old to $cachedir_new Do not interrupt this task.\n"  ) ;
+                File::Copy::Recursive::rmove( $cachedir_old, $cachedir_new )
+                or do {
+                        myprint( "Could not move $cachedir_old to $cachedir_new\n"  ) ;
+                        $err++ ;
+                } ;
+                # check it succeeded
+                if ( -d $cachedir_new and -r _ and -w _ ) {
+                        myprint( "New fixed cache directory $cachedir_new ok\n"  ) ;
+                }else{
+                        myprint( "New fixed cache directory $cachedir_new does not exist\n"  ) ;
+                        $err++ ;
+                }
+                if ( -d $cachedir_old ) {
+                        myprint( "Old cache directory $cachedir_old still exists\n"  ) ;
+                        $err++ ;
+                }else{
+                        myprint( "Old cache directory $cachedir_old successfuly moved\n"  ) ;
+                }
+        }
+        return( not $err ) ;
+}
+
+
+sub tests_cache_folder {
+
+	ok( '/path/fold1/fold2' eq cache_folder( q{}, '/path', 'fold1', 'fold2'), 'cache_folder: /path, fold1, fold2 -> /path/fold1/fold2' ) ;
+	ok( '/pa_th/fold1/fold2' eq cache_folder( q{}, '/pa*th', 'fold1', 'fold2'), 'cache_folder: /pa*th, fold1, fold2 -> /path/fold1/fold2' ) ;
+	ok( '/_p_a__th/fol_d1/fold2' eq cache_folder( q{}, '/>pp /path/fol_d1/fold2' ) ;
+
+	ok( 'D:/path/fold1/fold2' eq cache_folder( 'D:', '/path', 'fold1', 'fold2'), 'cache_folder: /path, fold1, fold2 -> /path/fold1/fold2' ) ;
+	ok( 'D:/pa_th/fold1/fold2' eq cache_folder( 'D:', '/pa*th', 'fold1', 'fold2'), 'cache_folder: /pa*th, fold1, fold2 -> /path/fold1/fold2' ) ;
+	ok( 'D:/_p_a__th/fol_d1/fold2' eq cache_folder( 'D:', '/>pp /path/fol_d1/fold2' ) ;
+	ok( '//' eq cache_folder( q{}, q{}, q{}, q{}), 'cache_folder:  -> //' ) ;
+	ok( '//_______' eq cache_folder( q{}, q{}, q{}, '*|?:"<>'), 'cache_folder: *|?:"<> -> //_______' ) ;
+	return ;
+}
+
+sub cache_folder {
+	my( $cache_base, $cache_dir, $h1_fold, $h2_fold ) = @_ ;
+
+	my $sep_1 = $h1_sep || '/';
+	my $sep_2 = $h2_sep || '/';
+
+	#myprint( "$cache_dir h1_fold $h1_fold sep1 $sep_1 h2_fold $h2_fold sep2 $sep_2\n" ) ;
+	$h1_fold = convert_sep_to_slash( $h1_fold, $sep_1 ) ;
+	$h2_fold = convert_sep_to_slash( $h2_fold, $sep_2 ) ;
+
+        my $cache_folder = "$cache_base" . filter_forbidden_characters( "$cache_dir/$h1_fold/$h2_fold" ) ;
+	#myprint( "cache_folder [$cache_folder]\n"  ) ;
+        return( $cache_folder ) ;
+}
+
+sub filter_forbidden_characters  {
+	my $string = shift ;
+
+        if ( 'MSWin32' eq $OSNAME ) {
+        	# Move trailing whitespace to _ " a b /c d " -> " a b_/c d_"
+        	$string =~ s{\ (/|$)}{_$1}xg ;
+        }
+        $string =~ s{[\Q*|?:"<>\E]}{_}xg ;
+        #myprint( "[$string]\n"  ) ;
+	return( $string ) ;
+}
+
+sub tests_filter_forbidden_characters  {
+
+	ok( 'a_b' eq filter_forbidden_characters( 'a_b' ), 'filter_forbidden_characters: a_b -> a_b' ) ;
+	ok( 'a_b' eq filter_forbidden_characters( 'a*b' ), 'filter_forbidden_characters: a*b -> a_b' ) ;
+	ok( 'a_b' eq filter_forbidden_characters( 'a|b' ), 'filter_forbidden_characters: a|b -> a_b' ) ;
+	ok( 'a_b' eq filter_forbidden_characters( 'a?b' ), 'filter_forbidden_characters: a?b -> a_b' ) ;
+	ok( 'a_______b' eq filter_forbidden_characters( 'a*|?:"<>b' ), 'filter_forbidden_characters: a*|?:"<>b -> a_______b' ) ;
+
+	SKIP: {
+		skip( 'Not on MSWin32', 1 ) if ( 'MSWin32' eq $OSNAME ) ;
+		ok( ( 'a b ' eq filter_forbidden_characters( 'a b ' ) ), 'filter_forbidden_characters: "a b " -> "a b "' ) ;
+	} ;
+
+	SKIP: {
+		skip( 'Only on MSWin32', 2 ) if ( 'MSWin32' ne $OSNAME ) ;
+		ok( ( ' a b_' eq filter_forbidden_characters( ' a b ' ) ), 'filter_forbidden_characters: "a b " -> "a b_"' ) ;
+		ok( ( ' a b_/ c d_' eq filter_forbidden_characters( ' a b / c d ' ) ), 'filter_forbidden_characters: " a b / c d " -> "a b_/ c d_"' ) ;
+        } ;
+
+	return ;
+}
+
+sub convert_sep_to_slash {
+	my ( $folder, $sep ) = @_ ;
+
+	$folder =~ s{\Q$sep\E}{/}xg ;
+	return( $folder ) ;
+}
+
+sub tests_convert_sep_to_slash {
+
+	ok(q{} eq convert_sep_to_slash(q{}, '/'), 'convert_sep_to_slash: no folder');
+	ok('INBOX' eq convert_sep_to_slash('INBOX', '/'), 'convert_sep_to_slash: INBOX');
+	ok('INBOX/foo' eq convert_sep_to_slash('INBOX/foo', '/'), 'convert_sep_to_slash: INBOX/foo');
+	ok('INBOX/foo' eq convert_sep_to_slash('INBOX_foo', '_'), 'convert_sep_to_slash: INBOX_foo');
+	ok('INBOX/foo/zob' eq convert_sep_to_slash('INBOX_foo_zob', '_'), 'convert_sep_to_slash: INBOX_foo_zob');
+	ok('INBOX/foo' eq convert_sep_to_slash('INBOX.foo', '.'), 'convert_sep_to_slash: INBOX.foo');
+	ok('INBOX/foo/hi' eq convert_sep_to_slash('INBOX.foo.hi', '.'), 'convert_sep_to_slash: INBOX.foo.hi');
+	return ;
+}
+
+
+sub tests_regexmess {
+
+	ok( 'blabla' eq regexmess( 'blabla' ), 'regexmess, no regexmess, nothing to do' ) ;
+
+	@regexmess = ( 'lalala' ) ;
+	ok( not( defined regexmess( 'popopo' ) ), 'regexmess, bad regex lalala' ) ;
+
+	@regexmess = ( 's/p/Z/g' ) ;
+	ok( 'ZoZoZo' eq regexmess( 'popopo' ), 'regexmess, s/p/Z/g' ) ;
+
+	@regexmess = ( 's{c}{C}gxms' ) ;
+	ok("H1: abC\nH2: Cde\n\nBody abC"
+		   eq regexmess( "H1: abc\nH2: cde\n\nBody abc"),
+	   'regexmess, c->C');
+
+	@regexmess = ( 's{\AFrom\ }{From:}gxms' ) ;
+	ok(          q{}
+	eq regexmess(q{}),
+	'From mbox 1 add colon blank');
+
+	ok(          'From:'
+	eq regexmess('From '),
+	'From mbox 2 add colo');
+
+	ok(          "\n" . 'From '
+	eq regexmess("\n" . 'From '),
+	'From mbox 3 add colo') ;
+
+	ok(          "From: zzz\n" . 'From '
+	eq regexmess("From  zzz\n" . 'From '),
+	'From mbox 4 add colo') ;
+
+	@regexmess = ( 's{\AFrom\ [^\n]*(\n)?}{}gxms' ) ;
+	ok(          q{}
+	eq regexmess(q{}),
+	'From mbox 1 remove, blank');
+
+	ok(          q{}
+	eq regexmess('From '),
+	'From mbox 2 remove');
+
+	ok(          "\n" . 'From '
+	eq regexmess("\n" . 'From '),
+	'From mbox 3 remove');
+
+	#myprint( "[", regexmess("From  zzz\n" . 'From '), "]" ) ;
+	ok(          q{}            . 'From '
+	eq regexmess("From  zzz\n" . 'From '),
+	'From mbox 4 remove');
+
+
+	ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Hello,
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+From  zzz
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Hello,
+Bye.
+EOM
+), 'From mbox 5 remove');
+
+
+@regexmess = ( 's{\A((?:[^\n]+\n)+|)^Disposition-Notification-To:[^\n]*\n(\r?\n|.*\n\r?\n)}{$1$2}xms' ) ; # SUPER SUPER BEST!
+	ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Hello,
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+Disposition-Notification-To: Gilles LAMIRAL 
+From:
+
+Hello,
+Bye.
+EOM
+	),
+	'regexmess: 1 Delete header Disposition-Notification-To:');
+
+	ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Hello,
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+Disposition-Notification-To: Gilles LAMIRAL 
+
+Hello,
+Bye.
+EOM
+),
+	'regexmess: 2 Delete header Disposition-Notification-To:');
+
+	ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Hello,
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Disposition-Notification-To: Gilles LAMIRAL 
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Hello,
+Bye.
+EOM
+),
+	'regexmess: 3 Delete header Disposition-Notification-To:');
+
+	ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Disposition-Notification-To: Gilles LAMIRAL 
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Disposition-Notification-To: Gilles LAMIRAL 
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Disposition-Notification-To: Gilles LAMIRAL 
+Bye.
+EOM
+),
+	'regexmess: 4 Delete header Disposition-Notification-To:');
+
+
+	ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Disposition-Notification-To: Gilles LAMIRAL 
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Disposition-Notification-To: Gilles LAMIRAL 
+Bye.
+EOM
+),
+	'regexmess: 5 Delete header Disposition-Notification-To:');
+
+
+ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Hello,
+Disposition-Notification-To: Gilles LAMIRAL 
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Hello,
+Disposition-Notification-To: Gilles LAMIRAL 
+Bye.
+EOM
+),
+	'regexmess: 6 Delete header Disposition-Notification-To:');
+
+ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Hello,
+Disposition-Notification-To: Gilles LAMIRAL 
+
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Hello,
+Disposition-Notification-To: Gilles LAMIRAL 
+
+Bye.
+EOM
+),
+	'regexmess: 7 Delete header Disposition-Notification-To:');
+
+
+ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Hello,
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Hello,
+Bye.
+EOM
+),
+	'regexmess: 8 Delete header Disposition-Notification-To:');
+
+
+ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Hello,
+Disposition-Notification-To: Gilles LAMIRAL 
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Hello,
+Disposition-Notification-To: Gilles LAMIRAL 
+Bye.
+EOM
+),
+	'regexmess: 9 Delete header Disposition-Notification-To:');
+
+
+
+ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Hello,
+Disposition-Notification-To: Gilles LAMIRAL 
+
+
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Hello,
+Disposition-Notification-To: Gilles LAMIRAL 
+
+
+Bye.
+EOM
+),
+	'regexmess: 10 Delete header Disposition-Notification-To:');
+
+ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Hello,
+
+Disposition-Notification-To: Gilles LAMIRAL 
+
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Hello,
+
+Disposition-Notification-To: Gilles LAMIRAL 
+
+Bye.
+EOM
+),
+	'regexmess: 11 Delete header Disposition-Notification-To:');
+
+ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Hello,
+
+Disposition-Notification-To: Gilles LAMIRAL 
+
+Disposition-Notification-To: Gilles LAMIRAL 
+
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Hello,
+
+Disposition-Notification-To: Gilles LAMIRAL 
+
+Disposition-Notification-To: Gilles LAMIRAL 
+
+Bye.
+EOM
+),
+	'regexmess: 12 Delete header Disposition-Notification-To:');
+
+
+@regexmess = ( 's{\A(.*?(?! ^$))^Disposition-Notification-To:(.*?)$}{$1X-Disposition-Notification-To:$2}igxms' ) ; # BAD!
+@regexmess = ( 's{\A((?:[^\n]+\n)+|)(^Disposition-Notification-To:[^\n]*\n)(\r?\n|.*\n\r?\n)}{$1X-$2$3}ims' ) ;
+
+
+ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Hello,
+
+Disposition-Notification-To: Gilles LAMIRAL 
+
+Disposition-Notification-To: Gilles LAMIRAL 
+
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Hello,
+
+Disposition-Notification-To: Gilles LAMIRAL 
+
+Disposition-Notification-To: Gilles LAMIRAL 
+
+Bye.
+EOM
+),
+	'regexmess: 13 Delete header Disposition-Notification-To:');
+
+ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+X-Disposition-Notification-To: Gilles LAMIRAL 
+From:
+
+Hello,
+
+Disposition-Notification-To: Gilles LAMIRAL 
+
+Disposition-Notification-To: Gilles LAMIRAL 
+
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+Disposition-Notification-To: Gilles LAMIRAL 
+From:
+
+Hello,
+
+Disposition-Notification-To: Gilles LAMIRAL 
+
+Disposition-Notification-To: Gilles LAMIRAL 
+
+Bye.
+EOM
+),
+	'regexmess: 14 Delete header Disposition-Notification-To:');
+
+ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+X-Disposition-Notification-To: Gilles LAMIRAL 
+From:
+
+Hello,
+
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+Disposition-Notification-To: Gilles LAMIRAL 
+From:
+
+Hello,
+
+Bye.
+EOM
+),
+	'regexmess: 15 Delete header Disposition-Notification-To:');
+
+
+ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+X-Disposition-Notification-To: Gilles LAMIRAL 
+
+Hello,
+
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+Disposition-Notification-To: Gilles LAMIRAL 
+
+Hello,
+
+Bye.
+EOM
+),
+	'regexmess: 16 Delete header Disposition-Notification-To:');
+
+ok(
+<<'EOM'
+X-Disposition-Notification-To: Gilles LAMIRAL 
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Hello,
+
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Disposition-Notification-To: Gilles LAMIRAL 
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Hello,
+
+Bye.
+EOM
+),
+	'regexmess: 17 Delete header Disposition-Notification-To:');
+
+
+
+# regex to play with Date: from the FAQ
+#@regexmess = 's{\A(.*?(?! ^$))^Date:(.*?)$}{$1Date:$2\nX-Date:$2}gxms'
+
+return ;
+
+}
+
+sub regexmess {
+	my ( $string ) = @_ ;
+	foreach my $regexmess ( @regexmess ) {
+		$debug and myprint( "eval \$string =~ $regexmess\n"  ) ;
+		my $ret = eval "\$string =~ $regexmess ; 1" ;
+                #myprint( "eval [$ret]\n"  ) ;
+                if ( ( not $ret ) or $@ ) {
+			myprint( "Error: eval regexmess '$regexmess': $@"  ) ;
+                        return( undef ) ;
+                }
+	}
+        $debug and myprint( "$string\n" ) ;
+	return( $string ) ;
+}
+
+
+sub tests_skipmess {
+
+	ok( not( defined skipmess( 'blabla' ) ), 'skipmess, no skipmess, no skip' ) ;
+
+	@skipmess = ('[') ;
+	ok( not( defined skipmess( 'popopo' ) ), 'skipmess, bad regex [' ) ;
+
+	@skipmess = ('lalala') ;
+	ok( not( defined skipmess( 'popopo' ) ), 'skipmess, bad regex lalala' ) ;
+
+	@skipmess = ('/popopo/') ;
+	ok( 1 == skipmess( 'popopo' ), 'skipmess, popopo match regex /popopo/' ) ;
+
+	@skipmess = ('/popopo/') ;
+	ok( 0 == skipmess( 'rrrrrr' ), 'skipmess, rrrrrr does not match regex /popopo/' ) ;
+
+	@skipmess = ('m{^$}') ;
+	ok( 1 == skipmess( q{} ),    'skipmess: empty string yes' ) ;
+	ok( 0 == skipmess( 'Hi!' ), 'skipmess: empty string no' ) ;
+
+	@skipmess = ('m{i}') ;
+	ok( 1 == skipmess( 'Hi!' ),  'skipmess: i string yes' ) ;
+	ok( 0 == skipmess( 'Bye!' ), 'skipmess: i string no' ) ;
+
+	@skipmess = ('m{[\x80-\xff]}') ;
+	ok( 0 == skipmess( 'Hi!' ),  'skipmess: i 8bit no' ) ;
+	ok( 1 == skipmess( "\xff" ), 'skipmess: \xff 8bit yes' ) ;
+
+	@skipmess = ('m{A}', 'm{B}') ;
+	ok( 0 == skipmess( 'Hi!' ),  'skipmess: A or B no' ) ;
+	ok( 0 == skipmess( 'lala' ), 'skipmess: A or B no' ) ;
+	ok( 0 == skipmess( "\xff" ), 'skipmess: A or B no' ) ;
+	ok( 1 == skipmess( 'AB' ),   'skipmess: A or B yes' ) ;
+	ok( 1 == skipmess( 'BA' ),   'skipmess: A or B yes' ) ;
+	ok( 1 == skipmess( 'AA' ),   'skipmess: A or B yes' ) ;
+	ok( 1 == skipmess( 'Ok Bye' ), 'skipmess: A or B yes' ) ;
+
+
+	@skipmess = ( 'm#\A((?:[^\n]+\n)+|)^Content-Type: Message/Partial;[^\n]*\n(?:\n|.*\n\n)#ism' ) ; # SUPER BEST!
+
+
+
+	ok( 1 == skipmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+Content-Type: Message/Partial; blabla
+From:
+
+Hello!
+Bye.
+EOM
+),
+    'skipmess: 1 match Content-Type: Message/Partial' ) ;
+
+	ok( 0 == skipmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Hello!
+Bye.
+EOM
+),
+    'skipmess: 2 not match Content-Type: Message/Partial' ) ;
+
+
+	ok( 1 == skipmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+Content-Type: Message/Partial; blabla
+
+Hello!
+Bye.
+EOM
+),
+    'skipmess: 3 match Content-Type: Message/Partial' ) ;
+
+	ok( 0 == skipmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Hello!
+Content-Type: Message/Partial; blabla
+Bye.
+EOM
+),
+    'skipmess: 4 not match Content-Type: Message/Partial' ) ;
+
+
+	ok( 0 == skipmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:
+
+Hello!
+Content-Type: Message/Partial; blabla
+
+Bye.
+EOM
+),
+    'skipmess: 5 not match Content-Type: Message/Partial' ) ;
+
+
+	ok( 1 == skipmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+Content-Type: Message/Partial; blabla
+From:
+
+Hello!
+
+Content-Type: Message/Partial; blabla
+
+Bye.
+EOM
+),
+    'skipmess: 6 match Content-Type: Message/Partial' ) ;
+
+	ok( 1 == skipmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+Content-Type: Message/Partial;
+From:
+
+Hello!
+Bye.
+EOM
+),
+    'skipmess: 7 match Content-Type: Message/Partial' ) ;
+
+	ok( 1 == skipmess(
+<<'EOM'
+Date: Wed, 2 Jul 2014 02:26:40 +0000
+MIME-Version: 1.0
+Content-Type: message/partial;
+	id="TAN_U_P<1404267997.00007489ed17>";
+	number=3;
+	total=3
+
+6HQ6Hh3CdXj77qEGixerQ6zHx0OnQ/Cf5On4W0Y6vtU2crABZQtD46Hx1EOh8dDz4+OnTr1G
+
+
+Hello!
+Bye.
+EOM
+),
+    'skipmess: 8 match Content-Type: Message/Partial' ) ;
+
+
+ok( 1 == skipmess(
+<<'EOM'
+Return-Path: 
+Received: by lamiral.info (Postfix, from userid 1000)
+        id 21EB12443BF; Mon,  2 Mar 2015 15:38:35 +0100 (CET)
+Subject: test: aethaecohngiexao
+To: 
+X-Mailer: mail (GNU Mailutils 2.2)
+Message-Id: <20150302143835.21EB12443BF@lamiral.info>
+Content-Type: message/partial;
+        id="TAN_U_P<1404267997.00007489ed17>";
+        number=3;
+        total=3
+Date: Mon,  2 Mar 2015 15:38:34 +0100 (CET)
+From: gilles@lamiral.info (Gilles LAMIRAL)
+
+test: aethaecohngiexao
+EOM
+),
+    'skipmess: 9 match Content-Type: Message/Partial' ) ;
+
+ok( 1 == skipmess(
+<<'EOM'
+Date: Mon,  2 Mar 2015 15:38:34 +0100 (CET)
+From: gilles@lamiral.info (Gilles LAMIRAL)
+Content-Type: message/partial;
+        id="TAN_U_P<1404267997.00007489ed17>";
+        number=3;
+        total=3
+
+test: aethaecohngiexao
+EOM
+. "lalala\n" x 3000000
+),
+    'skipmess: 10 match Content-Type: Message/Partial' ) ;
+
+ok( 0 == skipmess(
+<<'EOM'
+Date: Mon,  2 Mar 2015 15:38:34 +0100 (CET)
+From: gilles@lamiral.info (Gilles LAMIRAL)
+
+test: aethaecohngiexao
+EOM
+. "lalala\n" x 3000000
+),
+    'skipmess: 11 match Content-Type: Message/Partial' ) ;
+
+
+ok( 0 == skipmess(
+<<"EOM"
+From: fff\r
+To: fff\r
+Subject: Testing imapsync --skipmess\r
+Date: Mon, 22 Aug 2011 08:40:20 +0800\r
+Mime-Version: 1.0\r
+Content-Type: text/plain; charset=iso-8859-1\r
+Content-Transfer-Encoding: 7bit\r
+\r
+EOM
+. qq{!#"$%&'()*+,-./0123456789:;<=>?\@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefg\r\n } x 32730
+),
+    'skipmess: 12 not match Content-Type: Message/Partial' ) ;
+        # Complex regular subexpression recursion limit (32766) exceeded with more lines
+        # exit;
+	return ;
+}
+
+sub skipmess {
+	my ( $string ) = @_ ;
+	my $match ;
+	#myprint( "$string\n"  ) ;
+	foreach my $skipmess ( @skipmess ) {
+		$debug and myprint( "eval \$match = \$string =~ $skipmess\n"  ) ;
+		my $ret = eval "\$match = \$string =~ $skipmess ; 1"  ;
+		#myprint( "eval [$ret]\n"  ) ;
+		$debug and myprint( "match [$match]\n"  ) ;
+		if ( ( not $ret ) or $@ ) {
+			myprint( "Error: eval skipmess '$skipmess': $@"  ) ;
+			return( undef ) ;
+		}
+		return( $match ) if ( $match ) ;
+	}
+	return( $match ) ;
+}
+
+
+
+
+sub tests_bytes_display_string {
+
+        is(    'NA', bytes_display_string(       ), 'bytes_display_string: no args => NA' ) ;
+        is(    'NA', bytes_display_string( undef ), 'bytes_display_string: undef   => NA' ) ;
+        is(    'NA', bytes_display_string( 'blabla' ), 'bytes_display_string: blabla   => NA' ) ;
+        
+	ok(    '0.000 KiB' eq bytes_display_string(       0 ), 'bytes_display_string:       0' ) ;
+	ok(    '0.001 KiB' eq bytes_display_string(       1 ), 'bytes_display_string:       1' ) ;
+	ok(    '0.010 KiB' eq bytes_display_string(      10 ), 'bytes_display_string:      10' ) ;
+	ok(    '1.000 MiB' eq bytes_display_string( 1048575 ), 'bytes_display_string: 1048575' ) ;
+	ok(    '1.000 MiB' eq bytes_display_string( 1048576 ), 'bytes_display_string: 1048576' ) ;
+
+	ok(    '1.000 GiB' eq bytes_display_string( 1073741823 ), 'bytes_display_string: 1073741823 ' ) ;
+	ok(    '1.000 GiB' eq bytes_display_string( 1073741824 ), 'bytes_display_string: 1073741824 ' ) ;
+
+	ok(    '1.000 TiB' eq bytes_display_string( 1099511627775 ), 'bytes_display_string: 1099511627775' ) ;
+	ok(    '1.000 TiB' eq bytes_display_string( 1099511627776 ), 'bytes_display_string: 1099511627776' ) ;
+
+	ok(    '1.000 PiB' eq bytes_display_string( 1125899906842623 ), 'bytes_display_string: 1125899906842623' ) ;
+	ok(    '1.000 PiB' eq bytes_display_string( 1125899906842624 ), 'bytes_display_string: 1125899906842624' ) ;
+
+	ok( '1024.000 PiB' eq bytes_display_string( 1152921504606846975 ), 'bytes_display_string: 1152921504606846975' ) ;
+	ok( '1024.000 PiB' eq bytes_display_string( 1152921504606846976 ), 'bytes_display_string: 1152921504606846976' ) ;
+
+	ok( '1048576.000 PiB' eq bytes_display_string( 1180591620717411303424 ), 'bytes_display_string: 1180591620717411303424' ) ;
+
+        #myprint(  bytes_display_string( 1180591620717411303424 ), "\n"  ) ;
+	return ;
+}
+
+sub bytes_display_string {
+	my ( $bytes ) = @_ ;
+
+	my $readable_value = q{} ;
+
+        if ( ! defined( $bytes ) ) {
+                return( 'NA' ) ;
+        }
+
+        if ( not match_number( $bytes ) ) {
+                return( 'NA' ) ;
+        }
+
+        
+
+	SWITCH: {
+        	if ( abs( $bytes ) < ( 1000 * $KIBI ) ) {
+        		$readable_value = mysprintf( '%.3f KiB', $bytes / $KIBI) ;
+                	last SWITCH ;
+        	}
+        	if ( abs( $bytes ) < ( 1000 * $KIBI * $KIBI ) ) {
+        		$readable_value = mysprintf( '%.3f MiB', $bytes / ($KIBI * $KIBI) ) ;
+        	        last SWITCH ;
+        	}
+        	if ( abs( $bytes ) < ( 1000 * $KIBI * $KIBI * $KIBI) ) {
+			$readable_value = mysprintf( '%.3f GiB', $bytes / ($KIBI * $KIBI * $KIBI) ) ;
+        	        last SWITCH ;
+        	}
+        	if ( abs( $bytes ) < ( 1000 * $KIBI * $KIBI * $KIBI * $KIBI) ) {
+			$readable_value = mysprintf( '%.3f TiB', $bytes / ($KIBI * $KIBI * $KIBI * $KIBI) ) ;
+        	        last SWITCH ;
+        	} else {
+			$readable_value = mysprintf( '%.3f PiB', $bytes / ($KIBI * $KIBI * $KIBI * $KIBI * $KIBI) ) ;
+        	}
+		# if you have exabytes (EiB) of email to transfer, you have too much email!
+	}
+        #myprint( "$bytes = $readable_value\n"  ) ;
+        return( $readable_value ) ;
+}
+
+sub stats {
+        my $sync_loc = shift ;
+
+        if ( ! $sync_loc->{stats} ) {
+                return ;
+        }
+        
+	$timeend = time ;
+	my $timediff = $timeend - $sync_loc->{timestart} ;
+
+	my $timeend_str   = localtime $timeend ;
+
+	my $memory_consumption = 0 ;
+        $memory_consumption = memory_consumption(  ) || 0 ;
+	my $memory_ratio = ($max_msg_size_in_bytes) ?
+		mysprintf('%.1f', $memory_consumption / $max_msg_size_in_bytes) : 'NA' ;
+
+	my $host1_reconnect_count = $imap1->Reconnect_counter() || 0 ;
+	my $host2_reconnect_count = $imap2->Reconnect_counter() || 0 ;
+
+	myprint(  "++++ Statistics\n"  ) ;
+	myprint(  "Transfer started on               : $timestart_str\n"  ) ;
+	myprint(  "Transfer ended on                 : $timeend_str\n"  ) ;
+	myprintf( "Transfer time                     : %.1f sec\n", $timediff ) ;
+	myprint(  "Folders synced                    : $h1_folders_wanted_ct/$h1_folders_wanted_nb synced\n"  ) ;
+	myprint(  "Messages transferred              : $nb_msg_transferred "  ) ;
+	myprint(  "(could be $nb_msg_skipped_dry_mode without dry mode)" ) if ( $dry ) ;
+	myprint(  "\n" ) ;
+	myprint(  "Messages skipped                  : $nb_msg_skipped\n"  ) ;
+	myprint(  "Messages found duplicate on host1 : $h1_nb_msg_duplicate\n"  ) ;
+	myprint(  "Messages found duplicate on host2 : $h2_nb_msg_duplicate\n"  ) ;
+	myprint(  "Messages void (noheader) on host1 : $h1_nb_msg_noheader\n"  ) ;
+	myprint(  "Messages void (noheader) on host2 : $h2_nb_msg_noheader\n"  ) ;
+	myprint(  "Messages deleted on host1         : $h1_nb_msg_deleted\n"  ) ;
+	myprint(  "Messages deleted on host2         : $h2_nb_msg_deleted\n"  ) ;
+        myprintf( "Total bytes transferred           : %s (%s)\n",
+                $total_bytes_transferred,
+                bytes_display_string( $total_bytes_transferred ) ) ;
+        myprintf( "Total bytes duplicate host1       : %s (%s)\n",
+                $h1_total_bytes_duplicate,
+                bytes_display_string( $h1_total_bytes_duplicate) ) ;
+        myprintf( "Total bytes duplicate host2       : %s (%s)\n",
+                $h2_total_bytes_duplicate,
+                bytes_display_string( $h2_total_bytes_duplicate) ) ;
+        myprintf( "Total bytes skipped               : %s (%s)\n",
+                $total_bytes_skipped,
+                bytes_display_string( $total_bytes_skipped ) ) ;
+        myprintf( "Total bytes error                 : %s (%s)\n",
+                $total_bytes_error,
+                bytes_display_string( $total_bytes_error ) ) ;
+	$timediff ||= 1 ; # No division per 0
+	myprintf("Message rate                      : %.1f messages/s\n", $nb_msg_transferred / $timediff ) ;
+	myprintf("Average bandwidth rate            : %.1f KiB/s\n", $total_bytes_transferred / $KIBI / $timediff ) ;
+	#myprint(  "Reconnections to host1            : $host1_reconnect_count\n"  ) ;
+	#myprint(  "Reconnections to host2            : $host2_reconnect_count\n"  ) ;
+	myprintf("Memory consumption                : %.1f MiB\n", $memory_consumption / $KIBI / $KIBI ) ;
+        myprintf("Biggest message                   : %s bytes (%s)\n",
+                $max_msg_size_in_bytes,
+                bytes_display_string( $max_msg_size_in_bytes) ) ;
+	myprint(  "Memory/biggest message ratio      : $memory_ratio\n"  ) ;
+        if ( $foldersizesatend and $foldersizes ) {
+        
+
+        my $nb_msg_start_diff = diff_or_NA( $h2_nb_msg_start, $h1_nb_msg_start ) ;
+        my $bytes_start_diff  = diff_or_NA( $h2_bytes_start,  $h1_bytes_start  ) ;
+        
+	myprintf("Start difference host2 - host1    : %s messages, %s bytes (%s)\n", $nb_msg_start_diff,
+                                                        $bytes_start_diff,
+                                                        bytes_display_string( $bytes_start_diff ) ) ;
+
+        my $nb_msg_end_diff = diff_or_NA( $h2_nb_msg_end, $h1_nb_msg_end ) ;
+        my $bytes_end_diff  = diff_or_NA( $h2_bytes_end,  $h1_bytes_end  ) ;
+        
+	myprintf("Final difference host2 - host1    : %s messages, %s bytes (%s)\n", $nb_msg_end_diff,
+                                                        $bytes_end_diff,
+                                                        bytes_display_string( $bytes_end_diff ) ) ;
+        }
+	myprint(  "Detected $sync->{nb_errors} errors\n\n"  ) ;
+
+	myprint(  $warn_release, "\n"  ) ;
+	myprint(  thank_author(  )  ) ;
+	return ;
+}
+
+sub diff_or_NA {
+        my( $n1, $n2 ) = @ARG ;
+        
+        if ( not defined $n1 or not defined $n2 ) {
+                return 'NA' ;
+        }
+        
+        if ( not match_number( $n1 ) 
+          or not match_number( $n2 ) ) {
+                 return 'NA' ;
+        }
+        
+        return( $n1 - $n2 ) ;
+}
+
+sub match_number {
+        my $n = shift @ARG ;
+        
+        if ( not defined $n ) {
+                return 0 ;
+        }
+        if ( $n =~  /[0-9]+\.?[0-9]?/ ) {
+                return 1 ;
+        }
+        else {
+                return 0 ;
+        }
+}
+
+
+sub tests_match_number {
+
+        is( 0, match_number(   ),        'match_number: no parameters => 0' ) ;
+        is( 0, match_number( undef ),    'match_number:         undef => 0' ) ;
+        is( 0, match_number( 'blabla' ), 'match_number:        blabla => 0' ) ;
+        is( 1, match_number( 0 ),        'match_number:             0 => 1' ) ;
+        is( 1, match_number( 1 ),        'match_number:             1 => 1' ) ;
+        is( 1, match_number( 1.0 ),      'match_number:           1.0 => 1' ) ;
+        is( 1, match_number( 0.0 ),      'match_number:           0.0 => 1' ) ;
+        return ;
+}
+
+
+
+sub tests_diff_or_NA {
+
+        is( 'NA', diff_or_NA(  ),             'diff_or_NA: no parameters => NA' ) ;
+        is( 'NA', diff_or_NA( undef ),        'diff_or_NA: undef         => NA' ) ;
+        is( 'NA', diff_or_NA( undef, undef ), 'diff_or_NA: undef  undef  => NA' ) ;
+        is( 'NA', diff_or_NA( undef, 1 ),     'diff_or_NA: undef  1      => NA' ) ;
+        is( 'NA', diff_or_NA( 1, undef ),     'diff_or_NA: 1      undef  => NA' ) ;
+        is( 'NA', diff_or_NA( 'blabla', 1 ),  'diff_or_NA: blabla 1      => NA' ) ;
+        is( 'NA', diff_or_NA( 1, 'blabla' ),  'diff_or_NA: 1      blabla => NA' ) ;
+        is( 0, diff_or_NA( 1, 1 ),            'diff_or_NA: 1      1      =>  0' ) ;
+        is( 1, diff_or_NA( 1, 0 ),            'diff_or_NA: 1      0      =>  1' ) ;
+        is( -1, diff_or_NA( 0, 1 ),           'diff_or_NA: 0      1      => -1' ) ;
+        is( 0, diff_or_NA( 1.0, 1 ),          'diff_or_NA: 1.0    1      =>  0' ) ;
+        is( 1, diff_or_NA( 1.0, 0 ),          'diff_or_NA: 1.0    0      =>  1' ) ;
+        is( -1, diff_or_NA( 0, 1.0 ),         'diff_or_NA: 0      1.0    => -1' ) ;
+        return ;
+}
+
+sub thank_author {
+	return( "Homepage: http://imapsync.lamiral.info/\n" ) ;
+}
+
+
+sub load_modules {
+
+	if ( $ssl1 or $ssl2 or $tls1 or $tls2) {
+        	# not yet a "use" statement
+        	require IO::Socket::SSL ;
+		if ( $sync->{inet4} ) {
+		        IO::Socket::SSL->import( 'inet4' ) ;
+		}
+		if ( $sync->{inet6} ) {
+		        IO::Socket::SSL->import( 'inet6' ) ;
+		}
+        }
+
+       if ( ( ( not( $password1 or $passfile1 ) )
+	   or (not ( $password2 or $passfile2 ) )
+            )
+	and ( not $help ) ) {
+        	# now a "use" statement
+        	#require Term::ReadKey ;
+        }
+
+	return ;
+}
+
+
+
+sub parse_header_msg {
+	my ( $imap, $m_uid, $s_heads, $s_fir, $side, $s_hash ) = @_ ;
+
+	my $head = $s_heads->{$m_uid} ;
+	my $headnum =  scalar keys  %{ $head }   ;
+	$debug and myprint( "$side uid $m_uid head nb pass one: ", $headnum, "\n"  ) ;
+
+	if ( ( ! $headnum ) and ( $wholeheaderifneeded ) ){
+		myprint( "$side uid $m_uid no header by parse_headers so taking whole header with BODY.PEEK[HEADER]\n"  ) ;
+		$imap->fetch($m_uid, 'BODY.PEEK[HEADER]' ) ;
+		my $whole_header = $imap->_transaction_literals ;
+
+                #myprint( $whole_header ) ;
+                $head = decompose_header( $whole_header ) ;
+
+                $headnum =  scalar  keys  %{ $head }   ;
+	        $debug and myprint( "$side uid $m_uid head nb pass two: ", $headnum, "\n" ) ;
+	}
+
+        #myprint( Data::Dumper->Dump( [ $head, \%useheader ] )  ) ;
+
+	my $headstr ;
+
+        $headstr = header_construct( $head, $side, $m_uid ) ;
+
+	if ( ( ! $headstr) and ( $addheader ) and ( $side eq 'Host1' ) ) {
+        	my $header = add_header( $m_uid ) ;
+		myprint( "Host1 uid $m_uid no header found so adding our own [$header]\n" ) ;
+		$headstr .= uc  $header  ;
+		$s_fir->{$m_uid}->{NO_HEADER} = 1;
+	}
+
+	return if ( ! $headstr ) ;
+
+	my $size  = $s_fir->{$m_uid}->{'RFC822.SIZE'} ;
+	my $flags = $s_fir->{$m_uid}->{'FLAGS'} ;
+	my $idate = $s_fir->{$m_uid}->{'INTERNALDATE'} ;
+	$size = length $headstr  unless ( $size ) ;
+	my $m_md5 = md5_base64( $headstr ) ;
+	$debug and myprint( "$side uid $m_uid sig $m_md5 size $size idate $idate\n"  ) ;
+	my $key ;
+        if ($skipsize) {
+                $key = "$m_md5";
+        }
+	else {
+                $key = "$m_md5:$size";
+        }
+	# 0 return code is used to identify duplicate message hash
+	return 0 if exists $s_hash->{"$key"};
+	$s_hash->{"$key"}{'5'} = $m_md5;
+	$s_hash->{"$key"}{'s'} = $size;
+	$s_hash->{"$key"}{'D'} = $idate;
+	$s_hash->{"$key"}{'F'} = $flags;
+	$s_hash->{"$key"}{'m'} = $m_uid;
+
+	return( 1 ) ;
+}
+
+sub header_construct {
+
+	my( $head, $side, $m_uid ) = @_ ;
+
+        my $headstr ;
+	foreach my $h ( sort keys  %{ $head }  ) {
+                next if ( not ( exists $useheader{ uc  $h  } )
+                      and ( not exists  $useheader{ 'ALL' } )
+                ) ;
+		foreach my $val ( sort @{$head->{$h}} ) {
+
+                        my $H = header_line_normalize( $h, $val ) ;
+
+			# show stuff in debug mode
+			$debug and myprint( "$side uid $m_uid header [$H]", "\n"  ) ;
+
+			if ($skipheader and $H =~ m/$skipheader/xi) {
+				$debug and myprint( "$side uid $m_uid skipping header [$H]\n"  ) ;
+				next ;
+			}
+			$headstr .= "$H" ;
+		}
+	}
+	return( $headstr ) ;
+}
+
+
+sub header_line_normalize {
+	my( $header_key,  $header_val ) = @_ ;
+
+        # no 8-bit data in headers !
+        $header_val =~ s/[\x80-\xff]/X/xog;
+
+        # change tabulations to space (Gmail bug on with "Received:" on multilines)
+        $header_val =~ s/\t/\ /xgo ;
+
+        # remove the first blanks ( dbmail bug? )
+        $header_val =~ s/^\s*//xo;
+
+        # remove the last blanks ( Gmail bug )
+        $header_val =~ s/\s*$//xo;
+
+        # remove successive blanks ( Mailenable does it )
+        $header_val =~ s/\s+/ /xgo;
+
+        # remove Message-Id value domain part ( Mailenable changes it )
+        if ( ( $messageidnodomain ) and ( 'MESSAGE-ID' eq uc  $header_key  ) ) { $header_val =~ s/^([^@]+).*$/$1/xo ; }
+
+        # and uppercase header line
+        # (dbmail and dovecot)
+
+        my $header_line = uc "$header_key: $header_val" ;
+
+	return( $header_line ) ;
+}
+
+sub tests_header_line_normalize {
+
+	ok( ': ' eq header_line_normalize( q{}, q{} ), 'header_line_normalize: empty args' ) ;
+	ok( 'HHH: VVV' eq header_line_normalize( 'hhh', 'vvv' ), 'header_line_normalize: hhh vvv ' ) ;
+	ok( 'HHH: VVV' eq header_line_normalize( 'hhh', '  vvv' ), 'header_line_normalize: remove first blancs' ) ;
+	ok( 'HHH: AA BB CCC D' eq header_line_normalize( 'hhh', 'aa  bb   ccc d' ), 'header_line_normalize: remove succesive blanks' ) ;
+	ok( 'HHH: AA BB CCC' eq header_line_normalize( 'hhh', 'aa  bb   ccc   ' ), 'header_line_normalize: remove last blanks' ) ;
+	ok( 'HHH: VVV XX YY' eq header_line_normalize( 'hhh', "vvv\t\txx\tyy" ), 'header_line_normalize: tabs' ) ;
+	ok( 'HHH: XABX' eq header_line_normalize( 'hhh', "\x80AB\xff" ), 'header_line_normalize: 8bit' ) ;
+
+	return ;
+}
+
+
+sub firstline {
+        # extract the first line of a file (without \n)
+
+        my( $file ) = @_ ;
+        my $line  = q{} ;
+        my $FILE ;
+        open $FILE, '<', $file or do {
+                myprint( "Error opening file $file : $!\n" ) ;
+                return ;
+        } ;
+        $line = <$FILE> || q{} ;
+        close $FILE ;
+        chomp $line ;
+        return $line ;
+}
+
+sub tests_firstline {
+        is( 1 , string_to_file( "blabla\n", 'tmp/firstline.txt' ), 'tests_firstline: put blabla in tmp/firstline.txt' ) ;
+        is( 'blabla' , firstline( 'tmp/firstline.txt' ), 'tests_firstline: get blabla from tmp/firstline.txt' ) ;
+        is( undef , firstline( 'tmp/noexist.txt' ), 'tests_firstline: get blabla from tmp/noexist.txt' ) ;
+        is( 1 , string_to_file( q{}, 'tmp/firstline2.txt' ), 'tests_firstline: put empty string in tmp/firstline2.txt' ) ;
+        is( q{} , firstline( 'tmp/firstline2.txt' ), 'tests_firstline: get empty string from tmp/firstline2.txt' ) ;
+        is( 1 , string_to_file( "\n", 'tmp/firstline3.txt' ), 'tests_firstline: put CR in tmp/firstline3.txt' ) ;
+        is( q{} , firstline( 'tmp/firstline3.txt' ), 'tests_firstline: get empty string from tmp/firstline3.txt' ) ;
+
+        return ;
+}
+
+
+sub file_to_string {
+	my( $file ) = @_ ;
+	my @string ;
+	open my $FILE, '<', $file or die_clean( "Error with file $file : $! " ) ;
+	@string = <$FILE> ;
+	close $FILE ;
+	return( join q{}, @string ) ;
+}
+
+
+sub string_to_file {
+	my( $string, $file ) = @_ ;
+	sysopen( FILE, $file, O_WRONLY|O_TRUNC|O_CREAT, 0600) or die_clean( "$! $file" ) ;
+	print FILE $string ;
+	close FILE ;
+	return 1 ;
+}
+
+q^
+This is a multiline comment.
+Based on David Carter discussion, to do:
+* Call parameters stay the same.
+* Now always "return( $string, $error )". Descriptions below.
+OK * Still    capture STDOUT via "1> $output_tmpfile" to finish in $string and "return( $string, $error )"
+OK * Now also capture STDERR via "2> $error_tmpfile"  to finish in $error  and "return( $string, $error )"
+OK * in case of CHILD_ERROR, return( undef, $error ) 
+  and print $error, with folder/UID/maybeSubject context,
+  on console and at the end with the final error listing. Count this as a sync error.
+* in case of good command, take final $string as is, unless void. In case $error with value then print it.
+* in case of good command and final $string empty, consider it like CHILD_ERROR =>
+  return( undef, $error ) and print $error, with folder/UID/maybeSubject context,
+  on console and at the end with the final error listing. Count this as a sync error. 
+^ if 0 ; # End of multiline comment.
+
+sub pipemess {
+	my ( $string, @commands ) = @_ ;
+	my $error = q{} ;
+        foreach my $command ( @commands ) {
+                my $input_tmpfile  = "$tmpdir/imapsync_tmp_file.$PROCESS_ID.inp.txt" ;
+                my $output_tmpfile = "$tmpdir/imapsync_tmp_file.$PROCESS_ID.out.txt" ;
+                my $error_tmpfile  = "$tmpdir/imapsync_tmp_file.$PROCESS_ID.err.txt" ;
+                string_to_file( $string, $input_tmpfile  ) ;
+                ` $command < $input_tmpfile 1> $output_tmpfile 2> $error_tmpfile ` ;
+                my $is_command_ko = $CHILD_ERROR ;
+                my $error_cmd = file_to_string( $error_tmpfile ) ;
+                chomp( $error_cmd ) ;
+		$string = file_to_string( $output_tmpfile ) ;
+                my $string_len = length( $string ) ;
+                unlink $input_tmpfile, $output_tmpfile, $error_tmpfile ;
+
+		if ( $is_command_ko or ( ! $string_len ) ) {
+			my $cmd_exit_value = $CHILD_ERROR >> 8 ;
+			my $cmd_end_signal = $CHILD_ERROR & 127 ;
+                        my $signal_log = ( $cmd_end_signal ) ? " signal $cmd_end_signal and" : q{} ;
+                        my $error_log = qq{Failure: --pipemess command "$command" ended with$signal_log "$string_len" characters exit value "$cmd_exit_value" and STDERR "$error_cmd"\n} ;
+			myprint( $error_log ) ;
+			if ( wantarray ) {
+                                return @{ [ undef, $error_log ] }
+                        }else{
+                                return ;
+                        }
+		}
+                if ( $error_cmd ) {
+                        $error .= qq{STDERR of --pipemess "$command": $error_cmd\n} ;
+                        myprint(  qq{STDERR of --pipemess "$command": $error_cmd\n} ) ;
+                }
+        }
+        #myprint( "[$string]\n"  ) ;
+        if ( wantarray ) {
+                return ( $string, $error ) ;
+        }else{
+                return $string ;
+        }
+}
+
+
+
+sub tests_pipemess {
+
+	SKIP: {
+                Readonly my $NB_WIN_tests_pipemess => 3 ;
+		skip( 'Not on MSWin32', $NB_WIN_tests_pipemess ) if ('MSWin32' ne $OSNAME) ;
+		# Windows
+		# "type" command does not accept redirection of STDIN with <
+		# "sort" does
+		ok( "nochange\n" eq pipemess( 'nochange', 'sort' ), 'pipemess: nearly no change by sort' ) ;
+		ok( "nochange2\n" eq pipemess( 'nochange2', qw( sort sort ) ), 'pipemess: nearly no change by sort,sort' ) ;
+		# command not found
+		#diag( 'Warning and failure about cacaprout are on purpose' ) ;
+		ok( ! defined( pipemess( q{}, 'cacaprout' ) ), 'pipemess: command not found' ) ;
+
+	} ;
+
+        my ( $stringT, $errorT ) ;
+
+	SKIP: {
+                Readonly my $NB_UNX_tests_pipemess => 25 ;
+		skip( 'Not on Unix', $NB_UNX_tests_pipemess ) if ('MSWin32' eq $OSNAME) ;
+		# Unix
+		ok( 'nochange' eq pipemess( 'nochange', 'cat' ), 'pipemess: no change by cat' ) ;
+
+		ok( 'nochange2' eq pipemess( 'nochange2', 'cat', 'cat' ), 'pipemess: no change by cat,cat' ) ;
+
+		ok( "     1\tnumberize\n" eq pipemess( "numberize\n", 'cat -n' ), 'pipemess: numberize by cat -n' ) ;
+		ok( "     1\tnumberize\n     2\tnumberize\n" eq pipemess( "numberize\nnumberize\n", 'cat -n' ), 'pipemess: numberize by cat -n' ) ;
+
+		ok( "A\nB\nC\n" eq pipemess( "A\nC\nB\n", 'sort' ), 'pipemess: sort' ) ;
+
+		# command not found
+		#diag( 'Warning and failure about cacaprout are on purpose' ) ;
+		is( undef, pipemess( q{}, 'cacaprout' ), 'pipemess: command not found' ) ;
+
+                # success with true but no output at all
+                is( undef, pipemess( q{blabla}, 'true' ), 'pipemess: true but no output' ) ;
+
+                # failure with false and no output at all
+                is( undef, pipemess( q{blabla}, 'false' ), 'pipemess: false and no output' ) ;
+
+		# Failure since pipemess is not a real pipe, so first cat wait for standard input
+		is( q{blabla}, pipemess( q{blabla}, '( cat|cat ) ' ), 'pipemess: ok by ( cat|cat )' ) ;
+
+
+                ( $stringT, $errorT ) = pipemess( 'nochange', 'cat' ) ;
+                is( $stringT, 'nochange', 'pipemess: list context, no change by cat, string' ) ;
+                is( $errorT, q{}, 'pipemess: list context, no change by cat, no error' ) ;
+                
+                ( $stringT, $errorT ) = pipemess( 'dontcare', 'true' ) ;
+                is( $stringT, undef, 'pipemess: list context, true but no output, string' ) ;
+                like( $errorT, qr{Failure: --pipemess command "true" ended with "0" characters exit value "0" and STDERR ""},  'pipemess: list context, true but no output, error' ) ;
+
+                ( $stringT, $errorT ) = pipemess( 'dontcare', 'false' ) ;
+                is( $stringT, undef, 'pipemess: list context, false and no output, string' ) ;
+                like( $errorT, qr{Failure: --pipemess command "false" ended with "0" characters exit value "1" and STDERR ""},  'pipemess: list context, false and no output, error' ) ;
+
+                ( $stringT, $errorT ) = pipemess( 'dontcare', 'echo -n blablabla' ) ;
+                is( $stringT, q{blablabla}, 'pipemess: list context, "echo -n blablabla", string' ) ;
+                is( $errorT, q{},  'pipemess: list context, "echo blablabla", error' ) ;
+
+                
+                ( $stringT, $errorT ) = pipemess( 'dontcare', '( echo -n blablabla 3>&1 1>&2 2>&3 )' ) ;
+                is( $stringT, undef, 'pipemess: list context, "no output STDERR blablabla", string' ) ;
+                like( $errorT,  qr{blablabla"$},  'pipemess: list context, "no output STDERR blablabla", error' ) ;
+
+
+                ( $stringT, $errorT ) = pipemess( 'dontcare', '( echo -n blablabla 3>&1 1>&2 2>&3 )', 'false' ) ;
+                is( $stringT, undef, 'pipemess: list context, "no output STDERR blablabla then false", string' ) ;
+                like( $errorT,  qr{blablabla"$},  'pipemess: list context, "no output STDERR blablabla then false", error' ) ;
+
+                ( $stringT, $errorT ) = pipemess( 'dontcare', 'false', '( echo -n blablabla 3>&1 1>&2 2>&3 )' ) ;
+                is( $stringT, undef, 'pipemess: list context, "false then STDERR blablabla", string' ) ;
+                like( $errorT,  qr{Failure: --pipemess command "false" ended with "0" characters exit value "1" and STDERR ""},  'pipemess: list context, "false then STDERR blablabla", error' ) ;
+
+                ( $stringT, $errorT ) = pipemess( 'dontcare', '( echo rrrrr ; echo -n error_blablabla 3>&1 1>&2 2>&3 )' ) ;
+                like( $stringT, qr{rrrrr}, 'pipemess: list context, "STDOUT rrrrr STDERR error_blablabla", string' ) ;
+                like( $errorT,  qr{STDERR.*error_blablabla},  'pipemess: list context, "STDOUT rrrrr STDERR error_blablabla", error' ) ;
+
+	}
+
+        ( $stringT, $errorT ) = pipemess( 'dontcare', 'cacaprout' ) ;
+        is( $stringT, undef, 'pipemess: list context, cacaprout not found, string' ) ;
+        like( $errorT, qr{Failure: --pipemess command "cacaprout" ended with "0" characters exit value.*}, 'pipemess: list context, cacaprout not found, error' ) ;
+
+	return ;
+}
+
+sub tests_is_a_release_number {
+	ok(is_a_release_number($RELEASE_NUMBER_EXAMPLE_1), 'is_a_release_number 1.351') ;
+	ok(is_a_release_number($RELEASE_NUMBER_EXAMPLE_2), 'is_a_release_number 42.4242') ;
+	ok(is_a_release_number(imapsync_version()), 'is_a_release_number imapsync_version()') ;
+	ok(! is_a_release_number('blabla' ), '! is_a_release_number blabla') ;
+	return ;
+}
+
+sub is_a_release_number {
+	my $number = shift;
+
+	return( $number =~ m{^\d+\.\d+$}xo ) ;
+}
+
+sub check_last_release {
+
+	my $public_release = not_long_imapsync_version_public(  ) ;
+	$debug and myprint( "check_last_release: [$public_release]\n"  ) ;
+	return('unknown') if ($public_release eq 'unknown') ;
+	return('timeout') if ($public_release eq 'timeout') ;
+	return('unknown') if (! is_a_release_number( $public_release ) ) ;
+
+	my $imapsync_here  = imapsync_version();
+
+	if ($public_release > $imapsync_here) {
+		return("New imapsync release $public_release available");
+	}else{
+		return( 'This imapsync is up to date') ;
+	}
+}
+
+sub imapsync_version  {
+	my $rcs_imapsync = '$Id: imapsync,v 1.727 2016/08/19 10:30:36 gilles Exp gilles $ ' ;
+        my $imapsync_version ;
+
+	if ( $rcs_imapsync =~ m{,v\s+(\d+\.\d+)}xo ) {
+		$imapsync_version = $1
+        } else {
+                $imapsync_version = 'UNKNOWN' ;
+        }
+	return( $imapsync_version ) ;
+}
+
+sub tests_imapsync_basename {
+	ok( imapsync_basename() =~ m/imapsync/, 'imapsync_basename: match imapsync');
+	ok( 'blabla'   ne imapsync_basename(), 'imapsync_basename: do not equal blabla');
+	return ;
+}
+
+sub imapsync_basename {
+
+	return basename($0);
+
+}
+
+sub imapsync_version_public {
+
+	my $local_version = imapsync_version();
+	my $imapsync_basename = imapsync_basename();
+	my $agent_info = "$OSNAME system, perl "
+		. mysprintf( '%vd', $PERL_VERSION)
+		. ", Mail::IMAPClient $Mail::IMAPClient::VERSION"
+		. " $imapsync_basename";
+	my $sock = IO::Socket::INET->new(
+		PeerAddr => 'imapsync.lamiral.info',
+		PeerPort => 80,
+		Proto    => 'tcp',
+                ) ;
+	return( 'unknown' ) if not $sock ;
+	print $sock
+		"GET /prj/imapsync/VERSION HTTP/1.0\n",
+		"User-Agent: imapsync/$local_version ($agent_info)\n",
+		"Host: ks.lamiral.info\n\n";
+	my @line = <$sock>;
+	close $sock ;
+	my $last_release = $line[$LAST];
+	chomp $last_release ;
+	return($last_release) ;
+}
+
+sub not_long_imapsync_version_public {
+	#myprint( "Entering not_long_imapsync_version_public\n" ) ;
+
+	my $val;
+
+	# Doesn't work with gethostbyname (see perlipc)
+	#local $SIG{ALRM} = sub { die "alarm\n" };
+
+	if ('MSWin32' eq $OSNAME) {
+		local $SIG{ALRM} = sub { die "alarm\n" };
+	}else{
+
+        	POSIX::sigaction(SIGALRM,
+                         POSIX::SigAction->new(sub { croak 'alarm' } ) )
+        		or myprint( "Error setting SIGALRM handler: $!\n"  ) ;
+	}
+
+	my $ret = eval {
+		alarm 3 ;
+		{
+			$val = imapsync_version_public(  ) ;
+                        #sleep 4 ;
+			#myprint( "End of imapsync_version_public\n"  ) ;
+		}
+		alarm 0 ;
+                1 ;
+	} ;
+        #myprint( "eval [$ret]\n"  ) ;
+	if ( ( not $ret ) or $@ ) {
+		#myprint( "$@" ) ;
+		if ($@ =~ /alarm/) {
+		# timed out
+			return('timeout');
+		}else{
+			alarm 0 ;
+			return('unknown'); # propagate unexpected errors
+		}
+	}else {
+	# Good!
+		return($val);
+	}
+}
+
+sub localhost_info {
+
+	my($infos) = join q{},
+	    "Here is a [$OSNAME] system (",
+	    join(q{ },
+	         uname(),
+	         ),
+                 ")\n",
+	         'with Perl ',
+	         mysprintf( '%vd', $PERL_VERSION),
+	         " Mail::IMAPClient $Mail::IMAPClient::VERSION",
+             ;
+	return($infos) ;
+}
+
+sub memory_consumption {
+	# memory consumed by imapsync until now in bytes
+	return( ( memory_consumption_of_pids(  ) )[0] );
+}
+
+sub tests_memory_consumption {
+
+	like( memory_consumption(  ),  qr{\d+},'memory_consumption no args') ;
+	like( memory_consumption( 1 ), qr{\d+},'memory_consumption 1') ;
+	like( memory_consumption( $PROCESS_ID ), qr{\d+},"memory_consumption_of_pids $PROCESS_ID") ;
+
+	like( memory_consumption_ratio(), qr{\d+},   'memory_consumption_ratio' ) ;
+	like( memory_consumption_ratio(1), qr{\d+},  'memory_consumption_ratio 1' ) ;
+	like( memory_consumption_ratio(10), qr{\d+}, 'memory_consumption_ratio 10' ) ;
+
+	like( memory_consumption(), qr{\d+}, "memory_consumption\n" ) ;
+	return ;
+}
+
+
+
+sub memory_consumption_of_pids {
+
+	my @pid = @_;
+	@pid = (@pid) ? @pid : ($PROCESS_ID) ;
+
+	#myprint( "PIDs: @pid\n" ) ;
+	my @val;
+	if ('MSWin32' eq $OSNAME) {
+		@val = memory_consumption_of_pids_win32(@pid);
+	}else{
+		# Unix
+		my @ps = qx{ ps -o vsz -p @pid } ;
+                #myprint( @ps ) ;
+                #my @ps = backtick( "ps -o vsz -p @pid" ) ;
+		shift @ps; # First line is column name "VSZ"
+		chomp @ps;
+		# convert to octets
+                
+		@val = map { $_ * $KIBI } @ps;
+	}
+	return( @val ) ;
+}
+
+sub memory_consumption_of_pids_win32 {
+	# Windows
+	my @PID = @_;
+	my %PID;
+	# hash of pids as key values
+	map { $PID{$_}++ } @PID;
+
+	# Does not work but should reading the tasklist documentation
+	#@ps = qx{ tasklist /FI "PID eq @PID" };
+
+	my @ps = qx{ tasklist /NH /FO CSV } ;
+        #my @ps = backtick( 'tasklist /NH /FO CSV' ) ;
+	#myprint( "-" x $STD_CHAR_PER_LINE, "\n", @ps, "-" x $STD_CHAR_PER_LINE, "\n" ) ;
+	my @val;
+	foreach my $line (@ps) {
+		my($name, $pid, $mem) = (split ',', $line )[0,1,4];
+		next if (! $pid);
+		#myprint( "[$name][$pid][$mem]" ) ;
+		if ($PID{remove_qq($pid)}) {
+			#myprint( "MATCH !\n" ) ;
+			chomp $mem ;
+			$mem = remove_qq($mem);
+			$mem = remove_Ko($mem);
+			$mem = remove_not_num($mem);
+			#myprint( "[$mem]\n" ) ;
+			push @val, $mem * $KIBI;
+		}
+	}
+	return(@val);
+}
+
+sub backtick {
+	my $command = shift ;
+	my ( $writer, $reader, $err ) ;
+        my @output ;
+        open3( $writer, $reader, $err, $command ) ;
+        @output = <$reader>;  #Output here
+        #my @errors = <$err>;    #Errors here, instead of the console
+        $debugdev and myprint( @output  ) ;
+        return( @output ) ;
+}
+
+sub tests_backtick {
+
+        SKIP: {
+		skip( 'Tests for MSWin32', 3 ) if ('MSWin32' ne $OSNAME) ;
+		my @output ;
+		@output = backtick( 'echo Hello World!' ) ;
+		# Add \r on Windows.
+		ok( "Hello World!\r\n" eq $output[0], 'backtick: echo Hello World!' ) ;
+		$debug and myprint( "[@output]"  ) ;
+		@output = backtick( 'echo Hello & echo World!' ) ;
+		ok( "Hello \r\n" eq $output[0], 'backtick: echo Hello & echo World!' ) ;
+		ok( "World!\r\n" eq $output[1], 'backtick: echo Hello & echo World!' ) ;
+		$debug and myprint( "[@output][$output[0]][$output[1]]"  ) ;
+        } ;
+	SKIP: {
+		skip( 'Tests for Unix', 3 ) if ('MSWin32' eq $OSNAME) ;
+		my @output ;
+		@output = backtick( 'echo Hello World!' ) ;
+		ok( "Hello World!\n" eq $output[0], 'backtick: echo Hello World!' ) ;
+		$debug and myprint( "[@output]"  ) ;
+		@output = backtick( "echo Hello\necho World!" ) ;
+		ok( "Hello\n" eq $output[0], 'backtick: echo Hello; echo World!' ) ;
+		ok( "World!\n" eq $output[1], 'backtick: echo Hello; echo World!' ) ;
+		$debug and myprint( "[@output]"  ) ;
+	}
+        return ;
+}
+
+sub remove_not_num {
+
+	my $string = shift;
+	$string =~ tr/0-9//cd;
+	#myprint( "tr [$string]\n" ) ;
+	return($string);
+}
+
+sub tests_remove_not_num {
+
+	ok('123' eq remove_not_num(123), 'remove_not_num( 123 )' ) ;
+	ok('123' eq remove_not_num('123'), q{remove_not_num( '123' )} ) ;
+	ok('123' eq remove_not_num('12 3'), q{remove_not_num( '12 3' )} ) ;
+	ok('123' eq remove_not_num('a 12 3 Ko'), q{remove_not_num( 'a 12 3 Ko' )} ) ;
+	return ;
+}
+
+sub remove_Ko {
+	my $string = shift;
+	if ($string =~ /^(.*)\sKo$/xo) {
+		return($1);
+	}else{
+		return($string);
+	}
+}
+
+sub remove_qq {
+	my $string = shift;
+	if ($string =~ /^"(.*)"$/xo) {
+		return($1);
+	}else{
+		return($string);
+	}
+}
+
+sub memory_consumption_ratio {
+
+	my ($base) = @_;
+	$base ||= 1;
+	my $consu = memory_consumption();
+	return($consu / $base);
+}
+
+
+sub date_from_rcs {
+	my $d = shift ;
+
+	my %num2mon = qw( 01 Jan 02 Feb 03 Mar 04 Apr 05 May 06 Jun 07 Jul 08 Aug 09 Sep 10 Oct 11 Nov 12 Dec ) ;
+        if ($d =~ m{(\d{4})/(\d{2})/(\d{2})\s(\d{2}):(\d{2}):(\d{2})}xo ) {
+                # Handles the following format
+                # 2015/07/10 11:05:59 -- Generated by RCS Date tag.
+		#myprint( "$d\n"  ) ;
+                #myprint( "header: [$1][$2][$3][$4][$5][$6]\n"  ) ;
+                my ($year, $month, $day, $hour, $min, $sec) = ($1,$2,$3,$4,$5,$6) ;
+                $month = $num2mon{$month} ;
+                $d = "$day-$month-$year $hour:$min:$sec +0000" ;
+		#myprint( "$d\n"  ) ;
+	}
+	return( $d ) ;
+}
+
+sub tests_date_from_rcs {
+	ok('19-Sep-2015 16:11:07 +0000'
+	eq date_from_rcs('Date: 2015/09/19 16:11:07 '), 'date_from_rcs from RCS date' ) ;
+	return ;
+}
+
+sub good_date {
+        # two incoming formats:
+        # header    Tue, 24 Aug 2010 16:00:00 +0200
+	# internal       24-Aug-2010 16:00:00 +0200
+
+        # outgoing format: internal date format
+        #   24-Aug-2010 16:00:00 +0200
+
+    my $d = shift ;
+    return(q{}) if not defined $d;
+
+	SWITCH: {
+    	if ( $d =~ m{(\d?)(\d-...-\d{4})(\s\d{2}:\d{2}:\d{2})(\s(?:\+|-)\d{4})?}xo ) {
+		#myprint( "internal: [$1][$2][$3][$4]\n"  ) ;
+		my ($day_1, $date_rest, $hour, $zone) = ($1,$2,$3,$4) ;
+		$day_1 = '0' if ($day_1 eq q{}) ;
+		$zone  = ' +0000'  if not defined $zone ;
+		$d = $day_1 . $date_rest . $hour . $zone ;
+                last SWITCH ;
+        }
+
+	if ($d =~ m{(?:\w{3,},\s)?(\d{1,2}),?\s+(\w{3,})\s+(\d{2,4})\s+(\d{1,2})(?::|\.)(\d{1,2})(?:(?::|\.)(\d{1,2}))?\s*((?:\+|-)\d{4})?}xo ) {
+        	# Handles any combination of following formats
+                # Tue, 24 Aug 2010 16:00:00 +0200 -- Standard
+                # 24 Aug 2010 16:00:00 +0200 -- Missing Day of Week
+                # Tue, 24 Aug 97 16:00:00 +0200 -- Two digit year
+                # Tue, 24 Aug 1997 16.00.00 +0200 -- Periods instead of colons
+                # Tue, 24 Aug 1997  16:00:00 +0200 -- Extra whitespace between year and hour
+                # Tue, 24 Aug 1997 6:5:2 +0200 -- Single digit hour, min, or second
+                # Tue, 24, Aug 1997 16:00:00 +0200 -- Extra comma
+
+                #myprint( "header: [$1][$2][$3][$4][$5][$6][$7][$8]\n" ) ;
+                my ($day, $month, $year, $hour, $min, $sec, $zone) = ($1,$2,$3,$4,$5,$6,$7,$8);
+                $year = '19' . $year if length($year) == 2 && $year =~ m/^[789]/xo;
+                $year = '20' . $year if length($year) == 2;
+
+                $month = substr $month, 0, 3 if length($month) > 4;
+                $day  = mysprintf( '%02d', $day);
+                $hour = mysprintf( '%02d', $hour);
+                $min  = mysprintf( '%02d', $min);
+                $sec  = '00' if not defined  $sec  ;
+                $sec  = mysprintf( '%02d', $sec ) ;
+                $zone = '+0000' if not defined  $zone  ;
+                $d    = "$day-$month-$year $hour:$min:$sec $zone" ;
+		last SWITCH ;
+	}
+
+	if ($d =~ m{(?:.{3})\s(...)\s+(\d{1,2})\s(\d{1,2}):(\d{1,2}):(\d{1,2})\s(?:\w{3})?\s?(\d{4})}xo ) {
+        	# Handles any combination of following formats
+                # Sun Aug 20 11:55:09 2006
+                # Wed Jan 24 11:58:38 MST 2007
+                # Wed Jan  2 08:40:57 2008
+
+                #myprint( "header: [$1][$2][$3][$4][$5][$6]\n" ) ;
+                my ($month, $day, $hour, $min, $sec, $year) = ($1,$2,$3,$4,$5,$6);
+                $day  = mysprintf( '%02d', $day  ) ;
+                $hour = mysprintf( '%02d', $hour ) ;
+                $min  = mysprintf( '%02d', $min  ) ;
+                $sec  = mysprintf( '%02d', $sec  ) ;
+                $d    = "$day-$month-$year $hour:$min:$sec +0000" ;
+		last SWITCH ;
+	}
+        my %num2mon = qw( 01 Jan 02 Feb 03 Mar 04 Apr 05 May 06 Jun 07 Jul 08 Aug 09 Sep 10 Oct 11 Nov 12 Dec ) ;
+
+        if ($d =~ m{(\d{4})/(\d{2})/(\d{2})\s(\d{2}):(\d{2}):(\d{2})}xo ) {
+                # Handles the following format
+                # 2015/07/10 11:05:59 -- Generated by RCS Date tag.
+		#myprint( "$d\n"  ) ;
+                #myprint( "header: [$1][$2][$3][$4][$5][$6]\n"  ) ;
+                my ($year, $month, $day, $hour, $min, $sec) = ($1,$2,$3,$4,$5,$6) ;
+                $month = $num2mon{$month} ;
+                $d = "$day-$month-$year $hour:$min:$sec +0000" ;
+		#myprint( "$d\n"  ) ;
+		last SWITCH ;
+	}
+
+        if ($d =~ m{(\d{2})/(\d{2})/(\d{2})\s(\d{2}):(\d{2}):(\d{2})}xo ) {
+                # Handles the following format
+                # 02/06/09 22:18:08 -- Generated by AVTECH TemPageR devices
+
+                #myprint( "header: [$1][$2][$3][$4][$5][$6]\n" ) ;
+                my ($month, $day, $year, $hour, $min, $sec) = ($1,$2,$3,$4,$5,$6);
+                $year = '20' . $year;
+                $month = $num2mon{$month};
+                $d = "$day-$month-$year $hour:$min:$sec +0000";
+		last SWITCH ;
+	}
+
+	if ($d =~ m{\w{6,},\s(\w{3})\w+\s+(\d{1,2}),\s(\d{4})\s(\d{2}):(\d{2})\s(AM|PM)}xo ) {
+        	# Handles the following format
+                # Saturday, December 14, 2002 05:00 PM - KBtoys.com order confirmations
+
+                my ($month, $day, $year, $hour, $min, $apm) = ($1,$2,$3,$4,$5,$6);
+
+                $hour += 12 if $apm eq 'PM' ;
+                $day = mysprintf( '%02d', $day ) ;
+                $d = "$day-$month-$year $hour:$min:00 +0000" ;
+                last SWITCH ;
+	}
+
+	if ($d =~ m{(\w{3})\s(\d{1,2})\s(\d{4})\s(\d{2}):(\d{2}):(\d{2})\s((?:\+|-)\d{4})}xo ) {
+        	# Handles the following format
+                # Saturday, December 14, 2002 05:00 PM - jr.com order confirmations
+
+                my ($month, $day, $year, $hour, $min, $sec, $zone) = ($1,$2,$3,$4,$5,$6,$7);
+
+                $day = mysprintf( '%02d', $day ) ;
+                $d = "$day-$month-$year $hour:$min:$sec $zone";
+                last SWITCH ;
+	}
+
+	if ($d =~ m{(\d{1,2})-(\w{3})-(\d{4})}xo ) {
+        	# Handles the following format
+                # 21-Jun-2001 - register.com domain transfer email circa 2001
+
+                my ($day, $month, $year) = ($1,$2,$3);
+                $day = mysprintf( '%02d', $day);
+                $d = "$day-$month-$year 11:11:11 +0000";
+		last SWITCH ;
+	}
+
+    	# unknown or unmatch => return same string
+    	return($d);
+    }
+
+    $d = qq("$d") ;
+    return( $d ) ;
+}
+
+
+sub tests_good_date {
+
+	ok(q{} eq good_date(), 'good_date no arg');
+	ok('"24-Aug-2010 16:00:00 +0200"' eq good_date('24-Aug-2010 16:00:00 +0200'), 'good_date internal 2digit zone');
+	ok('"24-Aug-2010 16:00:00 +0000"' eq good_date('24-Aug-2010 16:00:00'), 'good_date internal 2digit no zone');
+	ok('"01-Sep-2010 16:00:00 +0200"' eq good_date( '1-Sep-2010 16:00:00 +0200'), 'good_date internal SP 1digit');
+	ok('"24-Aug-2010 16:00:00 +0200"' eq good_date('Tue, 24 Aug 2010 16:00:00 +0200'), 'good_date header 2digit zone');
+	ok('"01-Sep-2010 16:00:00 +0000"' eq good_date('Wed, 1 Sep 2010 16:00:00'), 'good_date header SP 1digit zone');
+	ok('"01-Sep-2010 16:00:00 +0200"' eq good_date('Wed, 1 Sep 2010 16:00:00 +0200'), 'good_date header SP 1digit zone');
+	ok('"01-Sep-2010 16:00:00 +0200"' eq good_date('Wed, 1 Sep 2010 16:00:00 +0200 (CEST)'), 'good_date header SP 1digit zone');
+        ok('"06-Feb-2009 22:18:08 +0000"' eq good_date('02/06/09 22:18:08'), 'good_date header TemPageR');
+        ok('"02-Jan-2008 08:40:57 +0000"' eq good_date('Wed Jan  2 08:40:57 2008'), 'good_date header dice.com support 1digit day');
+        ok('"20-Aug-2006 11:55:09 +0000"' eq good_date('Sun Aug 20 11:55:09 2006'), 'good_date header dice.com support 2digit day');
+        ok('"24-Jan-2007 11:58:38 +0000"' eq good_date('Wed Jan 24 11:58:38 MST 2007'), 'good_date header status-now.com');
+        ok('"24-Aug-2010 16:00:00 +0200"' eq good_date('24 Aug 2010 16:00:00 +0200'), 'good_date header missing date of week');
+        ok('"24-Aug-2067 16:00:00 +0200"' eq good_date('Tue, 24 Aug 67 16:00:00 +0200'), 'good_date header 2digit year');
+        ok('"24-Aug-1977 16:00:00 +0200"' eq good_date('Tue, 24 Aug 77 16:00:00 +0200'), 'good_date header 2digit year');
+        ok('"24-Aug-1987 16:00:00 +0200"' eq good_date('Tue, 24 Aug 87 16:00:00 +0200'), 'good_date header 2digit year');
+        ok('"24-Aug-1997 16:00:00 +0200"' eq good_date('Tue, 24 Aug 97 16:00:00 +0200'), 'good_date header 2digit year');
+        ok('"24-Aug-2004 16:00:00 +0200"' eq good_date('Tue, 24 Aug 04 16:00:00 +0200'), 'good_date header 2digit year');
+        ok('"24-Aug-1997 16:00:00 +0200"' eq good_date('Tue, 24 Aug 1997 16.00.00 +0200'), 'good_date header period time sep');
+        ok('"24-Aug-1997 16:00:00 +0200"' eq good_date('Tue, 24 Aug 1997  16:00:00 +0200'), 'good_date header extra white space type1');
+        ok('"24-Aug-1997 05:06:02 +0200"' eq good_date('Tue, 24 Aug 1997 5:6:2 +0200'), 'good_date header 1digit time vals');
+        ok('"24-Aug-1997 05:06:02 +0200"' eq good_date('Tue, 24, Aug 1997 05:06:02 +0200'), 'good_date header extra commas');
+        ok('"01-Oct-2003 12:45:24 +0000"' eq good_date('Wednesday, 01 October 2003 12:45:24 CDT'), 'good_date header no abbrev');
+        ok('"11-Jan-2005 17:58:27 -0500"' eq good_date('Tue,  11  Jan 2005 17:58:27 -0500'), 'good_date extra white space');
+        ok('"18-Dec-2002 15:07:00 +0000"' eq good_date('Wednesday, December 18, 2002 03:07 PM'), 'good_date kbtoys.com orders');
+        ok('"16-Dec-2004 02:01:49 -0500"' eq good_date('Dec 16 2004 02:01:49 -0500'), 'good_date jr.com orders');
+        ok('"21-Jun-2001 11:11:11 +0000"' eq good_date('21-Jun-2001'), 'good_date register.com domain transfer');
+        ok('"18-Nov-2012 18:34:38 +0100"' eq good_date('Sun, 18 Nov 2012 18:34:38 +0100'), 'good_date pop2imap bug (Westeuropäische Normalzeit)');
+	ok('"19-Sep-2015 16:11:07 +0000"' eq good_date('Date: 2015/09/19 16:11:07 '), 'good_date from RCS date' ) ;
+	return ;
+}
+
+
+sub tests_list_keys_in_2_not_in_1 {
+
+	my @list;
+	ok( ! list_keys_in_2_not_in_1( {}, {}), 'list_keys_in_2_not_in_1: {} {}');
+	ok( 0 == compare_lists( [], [ list_keys_in_2_not_in_1( {}, {} ) ] ), 'list_keys_in_2_not_in_1: {} {}');
+	ok( 0 == compare_lists( ['a','b'], [ list_keys_in_2_not_in_1( {}, {'a' => 1, 'b' => 1}) ]), 'list_keys_in_2_not_in_1: {} {a, b}');
+	ok( 0 == compare_lists( ['b'],     [ list_keys_in_2_not_in_1( {'a' => 1}, {'a' => 1, 'b' => 1}) ]), 'list_keys_in_2_not_in_1: {a} {a, b}');
+	ok( 0 == compare_lists( [],        [ list_keys_in_2_not_in_1( {'a' => 1, 'b' => 1}, {'a' => 1, 'b' => 1}) ]), 'list_keys_in_2_not_in_1: {a, b} {a, b}');
+	ok( 0 == compare_lists( [],        [ list_keys_in_2_not_in_1( {'a' => 1, 'b' => 1, 'c' => 1}, {'a' => 1, 'b' => 1}) ]), 'list_keys_in_2_not_in_1: {a, b, c} {a, b}');
+	ok( 0 == compare_lists( ['b'],     [ list_keys_in_2_not_in_1( {'a' => 1, 'c' => 1}, {'a' => 1, 'b' => 1}) ]), 'list_keys_in_2_not_in_1: {a, b, c} {a, b}');
+
+	return ;
+}
+
+sub list_keys_in_2_not_in_1 {
+
+	my $folders1_ref = shift;
+	my $folders2_ref = shift;
+	my @list;
+
+	foreach my $folder ( sort keys %{ $folders2_ref } ) {
+		next if exists $folders1_ref->{$folder};
+		push @list, $folder;
+	}
+	return(@list);
+}
+
+
+sub list_folders_in_2_not_in_1 {
+
+	my (@h2_folders_not_in_h1, %h2_folders_not_in_h1) ;
+	@h2_folders_not_in_h1 = list_keys_in_2_not_in_1( \%h1_folders_all, \%h2_folders_all) ;
+	map { $h2_folders_not_in_h1{$_} = 1} @h2_folders_not_in_h1 ;
+	@h2_folders_not_in_h1 = list_keys_in_2_not_in_1( \%h2_folders_from_1_all, \%h2_folders_not_in_h1) ;
+
+	return( reverse @h2_folders_not_in_h1 );
+}
+
+sub delete_folders_in_2_not_in_1 {
+
+	foreach my $folder (@h2_folders_not_in_1) {
+		if ( defined  $delete2foldersonly  and eval "\$folder !~ $delete2foldersonly" ) {
+			myprint( "Not deleting $folder because of --delete2foldersonly $delete2foldersonly\n"  ) ;
+			next ;
+		}
+		if ( defined  $delete2foldersbutnot  and eval "\$folder =~ $delete2foldersbutnot" ) {
+			myprint( "Not deleting $folder because of --delete2foldersbutnot $delete2foldersbutnot\n"  ) ;
+			next ;
+		}
+		my $res = $dry ; # always success in dry mode!
+		$imap2->unsubscribe( $folder ) if ( ! $dry ) ;
+		$res = $imap2->delete( $folder ) if ( ! $dry ) ;
+		if ( $res ) {
+			myprint( "Deleted $folder", "$dry_message", "\n"  ) ;
+		}else{
+			myprint( "Deleting $folder failed", "\n"  ) ;
+		}
+	}
+	return ;
+}
+
+sub delete_folder {
+        my ( $sync, $imap, $folder, $Side ) = @_ ;
+        if ( ! $sync )   { return ; }
+        if ( ! $imap )   { return ; }
+        if ( ! $folder ) { return ; }
+        $Side ||= 'HostX' ;
+        
+        my $res = $sync->{dry} ; # always success in dry mode!
+        if ( ! $sync->{dry} ) {
+                $imap->unsubscribe( $folder ) ;
+                $res = $imap->delete( $folder ) ;
+        }
+        if ( $res ) {
+        	myprint( "$Side deleted $folder", $sync->{dry_message}, "\n"  ) ;
+                return 1 ;
+        }else{
+        	myprint( "$Side deleting $folder failed", "\n"  ) ;
+                return ;
+        }
+}
+
+sub delete1emptyfolders {
+        my $sync = shift ;
+        if ( ! $sync ) { return ; } # abort if no parameter
+        if ( ! $sync->{delete1emptyfolders} ) { return ; } # abort if --delete1emptyfolders off
+        my $imap = $sync->{imap1} ;
+        if ( ! $imap ) { return ; } # abort if no imap
+        if ( $imap->IsUnconnected(  ) ) { return ; } # abort if diesconnected
+        
+        my %folders_kept ;
+        myprint( qq{Host1 deleting empty folders\n} ) ;
+        foreach my $folder ( reverse sort @{ $sync->{h1_folders_wanted} } ) {
+                my $parenthood = $imap->is_parent( $folder ) ;
+                if ( defined $parenthood and $parenthood ) {
+                        myprint( "Host1 folder $folder has subfolders\n" ) ;
+                        $folders_kept{ $folder }++ ;
+                        next ;
+                }
+                my $nb_messages_select = examine_folder_and_count( $imap, $folder, 'Host1' ) ;
+                if ( ! defined $nb_messages_select ) { next ; } # Select failed => Neither continue nor keep this folder }
+                my $nb_messages_search = scalar( @{ $imap->messages(  ) } ) ;
+                if ( 0 != $nb_messages_select and 0 != $nb_messages_search ) {
+                        myprint( "Host1 folder $folder has messages: $nb_messages_search (search) $nb_messages_select (select)\n" ) ;
+                        $folders_kept{ $folder }++ ;
+                        next ;
+                }
+                if ( 0 != $nb_messages_select + $nb_messages_search ) {
+                        myprint( "Host1 folder $folder odd messages count: $nb_messages_search (search) $nb_messages_select (select)\n" ) ;
+                        $folders_kept{ $folder }++ ;
+                        next ;
+                }
+                # Here we must have 0 messages by messages() aka "SEARCH ALL" and also "EXAMINE"
+                if ( uc $folder eq 'INBOX' ) {
+                        myprint( "Host1 Not deleting $folder\n" ) ;
+                        $folders_kept{ $folder }++ ;
+                        next ; 
+                }
+                myprint( "Host1 deleting empty folder $folder\n" ) ;
+                # can not delete a SELECTed or EXAMINEd folder so closing it
+                # could changed be SELECT INBOX
+                $imap->close(  ) ; # close after examine does not expunge; anyway expunging an empty folder... 
+                if ( delete_folder( $sync, $imap, $folder, 'Host1' ) ) {
+                        next ; # Deleted, good!
+                }else{
+                        $folders_kept{ $folder }++ ;
+                        next ; # Not deleted, bad!
+                }
+        }
+        remove_deleted_folders_from_wanted_list( $sync, %folders_kept ) ;
+        myprint( qq{Host1 ended deleting empty folders\n} ) ;
+        return ;
+}
+
+sub remove_deleted_folders_from_wanted_list {
+        my ( $sync, %folders_kept ) = @ARG ;
+        
+        my @h1_folders_wanted_init = @{ $sync->{h1_folders_wanted} } ;
+        my @h1_folders_wanted_last ;
+        foreach my $folder ( @h1_folders_wanted_init ) {
+                if ( $folders_kept{ $folder } ) {
+                        push @h1_folders_wanted_last, $folder ;
+                }
+        }
+        @{ $sync->{h1_folders_wanted} } = @h1_folders_wanted_last ;
+        return ;
+}
+
+sub examine_folder_and_count {
+        my ( $imap, $folder, $Side ) = @_ ;
+        $Side ||= 'HostX' ;
+        
+        if ( ! examine_folder( $imap, $folder, $Side ) ) {
+                return ;
+        }
+        my $nb_messages_select = count_from_select( $imap->History ) ;
+        return $nb_messages_select ;
+}
+
+
+sub tests_delete1emptyfolders {
+
+        is( undef, delete1emptyfolders(  ), q{delete1emptyfolders: undef} ) ;
+        my $syncT ;
+        is( undef, delete1emptyfolders( $syncT ), q{delete1emptyfolders: undef 2} ) ;
+        my $imapT ;
+        $syncT->{imap1} = $imapT ;
+        is( undef, delete1emptyfolders( $syncT ), q{delete1emptyfolders: undef imap} ) ;
+        
+        require Test::MockObject ;
+        $imapT = Test::MockObject->new(  ) ;
+        $syncT->{imap1} = $imapT ;
+
+        $imapT->set_true( 'IsUnconnected' ) ;
+        is( undef, delete1emptyfolders( $syncT ), q{delete1emptyfolders: Unconnected imap} ) ;
+
+        # Now connected tests
+        $imapT->set_false( 'IsUnconnected' ) ;
+        $imapT->mock( 'LastError', sub { q{LastError mocked} } ) ;
+        
+        $syncT->{delete1emptyfolders} = 0 ;
+        tests_delete1emptyfolders_unit(
+                $syncT,
+                [ qw{ INBOX DELME1 DELME2 } ],
+                [ qw{ INBOX DELME1 DELME2 } ],
+                q{tests_delete1emptyfolders: --delete1emptyfolders OFF}
+        ) ;
+
+        # All are parents => no deletion at all
+        $imapT->set_true( 'is_parent' ) ;
+        $syncT->{delete1emptyfolders} = 1 ;
+        tests_delete1emptyfolders_unit(
+                $syncT,
+                [ qw{ INBOX DELME1 DELME2 } ],
+                [ qw{ INBOX DELME1 DELME2 } ],
+                q{tests_delete1emptyfolders: --delete1emptyfolders ON}
+        ) ;
+
+        # No parents but examine false for all => skip all
+        $imapT->set_false( 'is_parent', 'examine' ) ;
+        
+        tests_delete1emptyfolders_unit(
+                $syncT,
+                [ qw{ INBOX DELME1 DELME2 } ],
+                [  ],
+                q{tests_delete1emptyfolders: EXAMINE fails}
+        ) ;
+
+        # examine ok for all but History bad => skip all
+        $imapT->set_true( 'examine' ) ;
+        $imapT->mock( 'History', sub { ( q{History badly mocked} ) } ) ;
+        tests_delete1emptyfolders_unit(
+                $syncT,
+                [ qw{ INBOX DELME1 DELME2 } ],
+                [  ],
+                q{tests_delete1emptyfolders: examine ok but History badly mocked so count messages fails}
+        ) ;
+
+        # History good but some messages EXISTS == messages() => no deletion
+        $imapT->mock( 'History', sub { ( q{* 2 EXISTS} ) } ) ;
+        $imapT->mock( 'messages', sub { [ qw{ UID_1 UID_2 } ] } ) ;
+        tests_delete1emptyfolders_unit(
+                $syncT,
+                [ qw{ INBOX DELME1 DELME2 } ],
+                [ qw{ INBOX DELME1 DELME2 } ],
+                q{tests_delete1emptyfolders: History EXAMINE ok, several messages}
+        ) ;
+
+        # 0 EXISTS but != messages() => no deletion
+        $imapT->mock( 'History', sub { ( q{* 0 EXISTS} ) } ) ;
+        $imapT->mock( 'messages', sub { [ qw{ UID_1 UID_2 } ] } ) ;
+        tests_delete1emptyfolders_unit(
+                $syncT,
+                [ qw{ INBOX DELME1 DELME2 } ],
+                [ qw{ INBOX DELME1 DELME2 } ],
+                q{tests_delete1emptyfolders: 0 EXISTS but 2 by messages()}
+        ) ;
+
+        # 1 EXISTS but != 0 == messages() => no deletion
+        $imapT->mock( 'History', sub { ( q{* 1 EXISTS} ) } ) ;
+        $imapT->mock( 'messages', sub { [ ] } ) ;
+        tests_delete1emptyfolders_unit(
+                $syncT,
+                [ qw{ INBOX DELME1 DELME2 } ],
+                [ qw{ INBOX DELME1 DELME2 } ],
+                q{tests_delete1emptyfolders: 1 EXISTS but 0 by messages()}
+        ) ;
+
+        # 0 EXISTS and 0 == messages() => deletion except INBOX
+        $imapT->mock( 'History', sub { ( q{* 0 EXISTS} ) } ) ;
+        $imapT->mock( 'messages', sub { [ ] } ) ;
+        $imapT->set_true( qw{ delete close unsubscribe } ) ;
+        $syncT->{dry_message} = q{ (not really since in a mocked test)} ;
+        tests_delete1emptyfolders_unit(
+                $syncT,
+                [ qw{ INBOX DELME1 DELME2 } ],
+                [ qw{ INBOX } ],
+                q{tests_delete1emptyfolders: 0 EXISTS 0 by messages() delete folders, keep INBOX}
+        ) ;
+
+
+
+
+        return ;
+}
+
+sub tests_delete1emptyfolders_unit {
+        my $syncT  = shift ;
+        my $folders1wanted_init_ref = shift ;
+        my $folders1wanted_after_ref = shift ;
+        my $comment = shift || q{delete1emptyfolders:} ;
+        
+        my @folders1wanted_init  = @{ $folders1wanted_init_ref } ;
+        my @folders1wanted_after = @{ $folders1wanted_after_ref } ;
+
+        @{ $syncT->{h1_folders_wanted} } = @folders1wanted_init ;
+        
+        is_deeply( $syncT->{h1_folders_wanted}, \@folders1wanted_init, qq{$comment, init check} ) ;
+        delete1emptyfolders( $syncT ) ;
+        is_deeply( $syncT->{h1_folders_wanted}, \@folders1wanted_after, qq{$comment, after check} ) ;
+        return ;
+}
+
+sub extract_header {
+        my $string = shift ;
+
+        my ( $header ) = split  /\n\n/x, $string ;
+        if ( ! $header ) { return( q{} ) ; }
+        #myprint( "[$header]\n"  ) ;
+        return( $header ) ;
+}
+
+sub tests_extract_header {
+
+
+my $h = <<'EOM';
+Message-Id: <20100428101817.A66CB162474E@plume.est.belle>
+Date: Wed, 28 Apr 2010 12:18:17 +0200 (CEST)
+From: gilles@louloutte.dyndns.org (Gilles LAMIRAL)
+EOM
+chomp $h ;
+ok( $h eq extract_header(
+<<'EOM'
+Message-Id: <20100428101817.A66CB162474E@plume.est.belle>
+Date: Wed, 28 Apr 2010 12:18:17 +0200 (CEST)
+From: gilles@louloutte.dyndns.org (Gilles LAMIRAL)
+
+body
+lalala
+EOM
+), 'extract_header: 1') ;
+
+
+
+	return ;
+}
+
+sub decompose_header{
+        my $string = shift ;
+
+        # a hash, for a keyword header KEY value are list of strings [VAL1, VAL1_other, etc]
+        # Think of multiple "Received:" header lines.
+        my $header = {  } ;
+
+        my ($key, $val ) ;
+        my @line = split /\n|\r\n/x, $string ;
+        foreach my $line ( @line ) {
+                #myprint( "DDD $line\n"  ) ;
+                # End of header
+                last if ( $line =~ m{^$}xo ) ;
+                # Key: value
+                if ( $line =~ m/(^[^:]+):\s(.*)/xo ) {
+                        $key = $1 ;
+                        $val = $2 ;
+                        $debugdev and myprint( "DDD KV [$key] [$val]\n"  ) ;
+                        push  @{ $header->{ $key } }, $val  ;
+                # blanc and value => value from previous line continues
+                }elsif( $line =~ m/^(\s+)(.*)/xo ) {
+                        $val = $2 ;
+                        $debugdev and myprint( "DDD  V [$val]\n"  ) ;
+                        @{ $header->{ $key } }[ $LAST ] .= " $val" if $key ;
+                # dirty line?
+                }else{
+                        next ;
+                }
+        }
+
+        #myprint( Data::Dumper->Dump( [ $header ] )  ) ;
+
+        return( $header ) ;
+}
+
+
+sub tests_decompose_header{
+
+        my $header_dec ;
+
+        $header_dec = decompose_header(
+<<'EOH'
+KEY_1: VAL_1
+KEY_2: VAL_2
+  VAL_2_+
+        VAL_2_++
+KEY_3: VAL_3
+KEY_1: VAL_1_other
+KEY_4: VAL_4
+	VAL_4_+
+KEY_5 BLANC:  VAL_5
+
+KEY_6_BAD_BODY: VAL_6
+EOH
+        ) ;
+
+        ok( 'VAL_3'
+        eq $header_dec->{ 'KEY_3' }[0], 'decompose_header: VAL_3' ) ;
+
+        ok( 'VAL_1'
+        eq $header_dec->{ 'KEY_1' }[0], 'decompose_header: VAL_1' ) ;
+
+        ok( 'VAL_1_other'
+        eq $header_dec->{ 'KEY_1' }[1], 'decompose_header: VAL_1_other' ) ;
+
+        ok( 'VAL_2 VAL_2_+ VAL_2_++'
+        eq $header_dec->{ 'KEY_2' }[0], 'decompose_header: VAL_2 VAL_2_+ VAL_2_++' ) ;
+
+        ok( 'VAL_4 VAL_4_+'
+        eq $header_dec->{ 'KEY_4' }[0], 'decompose_header: VAL_4 VAL_4_+' ) ;
+
+        ok( ' VAL_5'
+        eq $header_dec->{ 'KEY_5 BLANC' }[0], 'decompose_header: KEY_5 BLANC' ) ;
+
+        ok( not( defined  $header_dec->{ 'KEY_6_BAD_BODY' }[0]  ), 'decompose_header: KEY_6_BAD_BODY' ) ;
+
+
+        $header_dec = decompose_header(
+<<'EOH'
+Message-Id: <20100428101817.A66CB162474E@plume.est.belle>
+Date: Wed, 28 Apr 2010 12:18:17 +0200 (CEST)
+From: gilles@louloutte.dyndns.org (Gilles LAMIRAL)
+EOH
+        ) ;
+
+        ok( '<20100428101817.A66CB162474E@plume.est.belle>'
+        eq $header_dec->{ 'Message-Id' }[0], 'decompose_header: 1' ) ;
+
+        $header_dec = decompose_header(
+<<'EOH'
+Return-Path: 
+Received: by plume.est.belle (Postfix, from userid 1000)
+        id 120A71624742; Wed, 28 Apr 2010 01:46:40 +0200 (CEST)
+Subject: test:eekahceishukohpe
+EOH
+) ;
+        ok(
+'by plume.est.belle (Postfix, from userid 1000) id 120A71624742; Wed, 28 Apr 2010 01:46:40 +0200 (CEST)'
+        eq $header_dec->{ 'Received' }[0], 'decompose_header: 2' ) ;
+
+        $header_dec = decompose_header(
+<<'EOH'
+Received: from plume (localhost [127.0.0.1])
+        by plume.est.belle (Postfix) with ESMTP id C6EB73F6C9
+        for ; Mon, 26 Nov 2007 10:39:06 +0100 (CET)
+Received: from plume [192.168.68.7]
+        by plume with POP3 (fetchmail-6.3.6)
+        for  (single-drop); Mon, 26 Nov 2007 10:39:06 +0100 (CET)
+EOH
+        ) ;
+        ok(
+        'from plume (localhost [127.0.0.1]) by plume.est.belle (Postfix) with ESMTP id C6EB73F6C9 for ; Mon, 26 Nov 2007 10:39:06 +0100 (CET)'
+        eq $header_dec->{ 'Received' }[0], 'decompose_header: 3' ) ;
+        ok(
+        'from plume [192.168.68.7] by plume with POP3 (fetchmail-6.3.6) for  (single-drop); Mon, 26 Nov 2007 10:39:06 +0100 (CET)'
+        eq $header_dec->{ 'Received' }[1], 'decompose_header: 3' ) ;
+
+# Bad header beginning with a blank character
+        $header_dec = decompose_header(
+<<'EOH'
+ KEY_1: VAL_1
+KEY_2: VAL_2
+  VAL_2_+
+        VAL_2_++
+KEY_3: VAL_3
+KEY_1: VAL_1_other
+EOH
+        ) ;
+
+        ok( 'VAL_3'
+        eq $header_dec->{ 'KEY_3' }[0], 'decompose_header: Bad header VAL_3' ) ;
+
+        ok( 'VAL_1_other'
+        eq $header_dec->{ 'KEY_1' }[0], 'decompose_header: Bad header VAL_1_other' ) ;
+
+        ok( 'VAL_2 VAL_2_+ VAL_2_++'
+        eq $header_dec->{ 'KEY_2' }[0], 'decompose_header: Bad header VAL_2 VAL_2_+ VAL_2_++' ) ;
+
+	return ;
+}
+
+sub epoch {
+        # incoming format:
+	# internal date 24-Aug-2010 16:00:00 +0200
+
+        # outgoing format: epoch
+
+
+        my $d = shift ;
+        return(q{}) if not defined $d;
+
+        my ( $mday, $month, $year, $hour, $min, $sec, $sign, $zone_h, $zone_m ) ;
+        my $time ;
+
+        if ( $d =~ m{(\d{1,2})-([A-Z][a-z]{2})-(\d{4})\s(\d{2}):(\d{2}):(\d{2})\s((?:\+|-))(\d{2})(\d{2})}xo ) {
+                #myprint( "internal: [$1][$2][$3][$4][$5][$6][$7][$8][$9]\n"  ) ;
+                ( $mday, $month, $year, $hour, $min, $sec, $sign, $zone_h, $zone_m )
+                =  ( $1,   $2,     $3,    $4,    $5,  $6,    $7,     $8,     $9 ) ;
+                #myprint( "( $mday, $month, $year, $hour, $min, $sec, $sign, $zone_h, $zone_m )\n"  ) ;
+
+                $sign = +1 if ( '+' eq $sign ) ;
+                $sign = $MINUS_ONE if ( '-' eq $sign ) ;
+
+                $time = timegm( $sec, $min, $hour, $mday, $month_abrev{$month}, $year )
+                        - $sign * ( 3600 * $zone_h + 60 * $zone_m ) ;
+
+                #myprint( "$time ", scalar localtime($time), "\n");
+        }
+        return( $time ) ;
+}
+
+sub tests_epoch {
+        ok( '1282658400' eq epoch( '24-Aug-2010 16:00:00 +0200' ), 'epoch 24-Aug-2010 16:00:00 +0200 -> 1282658400' ) ;
+        ok( '1282658400' eq epoch( '24-Aug-2010 14:00:00 +0000' ), 'epoch 24-Aug-2010 14:00:00 +0000 -> 1282658400' ) ;
+        ok( '1282658400' eq epoch( '24-Aug-2010 12:00:00 -0200' ), 'epoch 24-Aug-2010 12:00:00 -0200 -> 1282658400' ) ;
+        ok( '1282658400' eq epoch( '24-Aug-2010 16:01:00 +0201' ), 'epoch 24-Aug-2010 16:01:00 +0201 -> 1282658400' ) ;
+        ok( '1282658400' eq epoch( '24-Aug-2010 14:01:00 +0001' ), 'epoch 24-Aug-2010 14:01:00 +0001 -> 1282658400' ) ;
+
+        ok( '1280671200' eq epoch( '1-Aug-2010 16:00:00 +0200' ), 'epoch 1-Aug-2010 16:00:00 +0200 -> 1280671200' ) ;
+        ok( '1280671200' eq epoch( '1-Aug-2010 14:00:00 +0000' ), 'epoch 1-Aug-2010 14:00:00 +0000 -> 1280671200' ) ;
+        ok( '1280671200' eq epoch( '1-Aug-2010 12:00:00 -0200' ), 'epoch 1-Aug-2010 12:00:00 -0200 -> 1280671200' ) ;
+        ok( '1280671200' eq epoch( '1-Aug-2010 16:01:00 +0201' ), 'epoch 1-Aug-2010 16:01:00 +0201 -> 1280671200' ) ;
+        ok( '1280671200' eq epoch( '1-Aug-2010 14:01:00 +0001' ), 'epoch 1-Aug-2010 14:01:00 +0001 -> 1280671200' ) ;
+	return ;
+}
+
+sub add_header {
+	my $header_uid = shift || 'mistake' ;
+	my $header_Message_Id = 'Message-Id: <' . $header_uid . '@imapsync>' ;
+        return( $header_Message_Id ) ;
+}
+
+sub tests_add_header {
+	ok( 'Message-Id: ' eq add_header(), 'add_header no arg' ) ;
+	ok( 'Message-Id: <123456789@imapsync>' eq add_header(123456789), 'add_header 123456789' ) ;
+
+	return ;
+}
+
+sub tests_Banner{
+
+	my $imap = Mail::IMAPClient->new(  ) ;
+        ok( 'lalala' eq $imap->Banner('lalala'), 'Banner set lalala' ) ;
+        ok( 'lalala' eq $imap->Banner(), 'Banner returns lalala' ) ;
+	return ;
+}
+
+
+
+
+sub max_line_length {
+	my $string = shift ;
+        my $max = 0 ;
+
+        while ( $string =~ m/([^\n]*\n?)/msxg ) {
+        	$max = max( $max, length $1 ) ;
+        }
+	return( $max ) ;
+}
+
+sub tests_max_line_length {
+	ok( 0 == max_line_length( q{} ), 'max_line_length: 0 == null string' ) ;
+	ok( 1 == max_line_length( "\n" ), 'max_line_length: 1 == \n' ) ;
+	ok( 1 == max_line_length( "\n\n" ), 'max_line_length: 1 == \n\n' ) ;
+	ok( 1 == max_line_length( "\n" x 500 ), 'max_line_length: 1 == 500 \n' ) ;
+	ok( 1 == max_line_length( 'a' ), 'max_line_length: 1 == a' ) ;
+	ok( 2 == max_line_length( "a\na" ), 'max_line_length: 2 == a\na' ) ;
+	ok( 2 == max_line_length( "a\na\n" ), 'max_line_length: 2 == a\na\n' ) ;
+	ok( 3 == max_line_length( "a\nab\n" ), 'max_line_length: 3 == a\nab\n' ) ;
+	ok( 3 == max_line_length( "a\nab\n" x 10000 ), 'max_line_length: 3 == 10000 a\nab\n' ) ;
+	ok( 3 == max_line_length( "a\nab\nabc" ), 'max_line_length: 3 == a\nab\nabc' ) ;
+
+	ok( 4 == max_line_length( "a\nab\nabc\n" ), 'max_line_length: 4 == a\nab\nabc\n' ) ;
+	ok( 5 == max_line_length( "a\nabcd\nabc\n" ), 'max_line_length: 5 == a\nabcd\nabc\n' ) ;
+	ok( 5 == max_line_length( "a\nabcd\nabc\n\nabcd\nabcd\nabcd\nabcd\nabcd\nabcd\nabcd\nabcd" ), 'max_line_length: 5 == a\nabcd\nabc\n\nabcd\nabcd\nabcd\nabcd\nabcd\nabcd\nabcd\nabcd' ) ;
+	return ;
+}
+
+sub setlogfile {
+        my( $mysync ) = shift ;
+        $mysync->{logdir}  = defined $mysync->{logdir}  ? $mysync->{logdir}  : 'LOG_imapsync' ;
+        $mysync->{logfile} = defined $mysync->{logfile} ? "$mysync->{logdir}/$mysync->{logfile}" :
+                logfile( $mysync->{timestart}, $mysync->{user2}, $mysync->{logdir} ) ;
+        #myprint( "logdir  = $mysync->{logdir}\n"  ) ;
+        #myprint( "logfile = $mysync->{logfile}\n"  ) ;
+        return( $mysync->{logfile} ) ;
+}
+
+sub tests_setlogfile {
+        my $mysync = {
+                timestart => 2,
+                user2     => 'user2',
+        } ;
+
+        ok( 'LOG_imapsync/1970_01_01_01_00_02_user2.txt' eq setlogfile( $mysync ),
+                'setlogfile: default is like LOG_imapsync/1970_01_01_01_00_02_user2.txt' ) ;
+
+        $mysync->{logdir}  = undef ;
+        $mysync->{logfile} = undef ;
+        ok( 'LOG_imapsync/1970_01_01_01_00_02_user2.txt' eq setlogfile( $mysync ),
+                'setlogfile: logdir undef, LOG_imapsync/1970_01_01_01_00_02_user2.txt' ) ;
+
+        $mysync->{logdir} = q{} ;
+        $mysync->{logfile} = undef ;
+        ok( '1970_01_01_01_00_02_user2.txt' eq setlogfile( $mysync ),
+                'setlogfile: logdir empty, 1970_01_01_01_00_02_user2.txt' ) ;
+
+        $mysync->{logdir} = 'vallogdir' ;
+        $mysync->{logfile} = undef ;
+        ok( 'vallogdir/1970_01_01_01_00_02_user2.txt' eq setlogfile( $mysync ),
+                'setlogfile: logdir vallogdir, vallogdir/1970_01_01_01_00_02_user2.txt' ) ;
+
+        $mysync->{logdir}  = 'vallogdir' ;
+        $mysync->{logfile} = 'vallogfile.txt' ;
+        ok( 'vallogdir/vallogfile.txt' eq setlogfile( $mysync ),
+                'setlogfile: logdir vallogdir, logfile vallogfile.txt, vallogdir/vallogfile.txt' ) ;
+
+        return ;
+}
+
+
+sub logfile {
+	my ( $time, $suffix, $dir ) = @_ ;
+
+	$time   ||= 0 ;
+	$suffix ||= q{} ;
+	my $sep_suffix = ( $suffix ) ? '_' : q{} ;
+        $dir    ||= q{} ;
+	my $sep_dir = ( $dir ) ? '/' : q{} ;
+
+	my $date_str = POSIX::strftime( '%Y_%m_%d_%H_%M_%S', localtime $time ) ;
+        my $logfile = "${dir}${sep_dir}${date_str}${sep_suffix}${suffix}.txt" ;
+	$debug and myprint( "date_str: $date_str\n"  ) ;
+	$debug and myprint( "logfile : $logfile\n"  ) ;
+	return( $logfile ) ;
+}
+
+sub tests_logfile {
+	SKIP: {
+		# Too hard to have a well known timezone on Windows
+		skip( 'Too hard to have a well known timezone on Windows', 6 ) if ( 'MSWin32' eq $OSNAME ) ;
+
+		local $ENV{TZ} = 'GMT' ;
+		{ POSIX::tzset unless ('MSWin32' eq $OSNAME) ;
+			ok( '1970_01_01_00_00_00.txt' eq logfile(  ),           'logfile: no args    => 1970_01_01_00_00_00.txt' ) ;
+			ok( '1970_01_01_00_00_00.txt' eq logfile( 0 ),          'logfile: 0          => 1970_01_01_00_00_00.txt' ) ;
+			ok( '1970_01_01_00_01_01.txt' eq logfile( 61 ),         'logfile: 0          => 1970_01_01_00_01_01.txt' ) ;
+			ok( '2010_08_24_14_00_00.txt' eq logfile( 1282658400 ), 'logfile: 1282658400 => 2010_08_24_14_00_00.txt' ) ;
+			ok( '2010_08_24_14_01_01.txt' eq logfile( 1282658461 ), 'logfile: 1282658461 => 2010_08_24_14_01_01.txt' ) ;
+			ok( '2010_08_24_14_01_01_poupinette.txt' eq logfile( 1282658461, 'poupinette' ), 'logfile: 1282658461 poupinette => 2010_08_24_14_01_01_poupinette.txt' ) ;
+                }
+		POSIX::tzset unless ('MSWin32' eq $OSNAME) ;
+	} ;
+	return ;
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+sub tests_million_folders_baby_2 {
+	my %long ;
+	@long{ 1 .. 900_000 } = (1) x 900_000 ;
+	#myprint( %long, "\n"  ) ;
+	my $pasglop = 0 ;
+	foreach my $elem (  1 .. 900_000 ) {
+		#$debug and myprint( "$elem "  ) ;
+		if ( not exists  $long{ $elem }  ) {
+			$pasglop++ ;
+		}
+	}
+        ok( 0 == $pasglop, 'tests_million_folders_baby_2: search among 900_000' ) ;
+	# myprint( "$pasglop\n"  ) ;
+        return ;
+}
+
+
+
+sub tests_always_fail {
+	ok( 0 == 1, '0 == 1' ) ;
+	ok( 1 == 1, '1 == 1' ) ;
+        return ;
+}
+
+sub logfileprepa {
+	my $logfile = shift ;
+
+	my $dirname = dirname( $logfile ) ;
+	is_valid_directory( $dirname ) || return( 0 ) ;
+	return( 1 ) ;
+}
+
+sub teelaunch {
+        my $mysync = shift ;
+	my $logfile = $mysync->{logfile} ;
+	logfileprepa( $logfile ) || croak "Error no valid directory to write log file $logfile : $!" ;
+	my $logfile_handle ;
+	open $logfile_handle, '>', $logfile
+	  or croak( "Can not open $logfile for write: $!" ) ;
+	my $tee = IO::Tee->new( $logfile_handle, \*STDOUT ) ;
+	*STDERR = *$tee{IO} ;
+	select $tee ;
+        $tee->autoflush( 1 ) ;
+        $mysync->{logfile_handle} = $logfile_handle ;
+        $mysync->{tee} = $tee ;
+	return $logfile_handle ;
+}
+
+sub getpwuid_any_os {
+        my $uid = shift ;
+
+        return( scalar  getlogin ) if ( 'MSWin32' eq $OSNAME ) ; # Windows system
+        return( scalar  getpwuid $uid ) ; # Unix system
+}
+
+
+
+sub usage {
+	my $localhost_info = localhost_info();
+	my $thank = thank_author();
+	my $imapsync_release = q{};
+	$imapsync_release = check_last_release() if (not defined $releasecheck);
+        my $escape_char = ( 'MSWin32' eq $OSNAME ) ? '^' : '\\';
+        myprint( <<"EOF" ) ;
+
+ usage: $0 [options]
+
+ Several options are mandatory.
+ str means string
+ int means integer
+ reg means regular expression
+ cmd means command
+
+ --dry               : Makes imapsync doing nothing, just print what would
+                       be done without --dry.
+
+ --host1        str  : Source or "from" imap server. Mandatory.
+ --port1        int  : Port to connect on host1. Default is 143, 993 if --ssl1
+ --user1        str  : User to login on host1. Mandatory.
+ --showpasswords     : Shows passwords on output instead of "MASKED".
+                       Useful to restart a complete run by just reading the log.
+ --password1    str  : Password for the user1.
+ --host2        str  : "destination" imap server. Mandatory.
+ --port2        int  : Port to connect on host2. Default is 143, 993 if --ssl2
+ --user2        str  : User to login on host2. Mandatory.
+ --password2    str  : Password for the user2.
+
+ --passfile1    str  : Password file for the user1. It must contain the
+                       password on the first line. This option avoids to show
+                       the password on the command line like --password1 does.
+ --passfile2    str  : Password file for the user2. Contains the password.
+
+ --ssl1              : Use a SSL connection on host1.
+ --ssl2              : Use a SSL connection on host2.
+ --tls1              : Use a TLS connection on host1.
+ --tls2              : Use a TLS connection on host2.
+ --debugssl     int  : SSL debug mode from 0 to 4.
+ --sslargs1     str  : Pass any ssl parameter for host1 ssl or tls connection. Example:
+                       --sslargs1 SSL_verify_mode=1 --sslargs1 SSL_version=SSLv3
+                       See all possibilities in the new() method of IO::Socket::SSL
+                       http://search.cpan.org/perldoc?IO::Socket::SSL#Description_Of_Methods
+ --sslargs2     str  : Pass any ssl parameter for host2 ssl or tls connection.
+                       See --sslargs1
+
+ --timeout1     int  : Connection timeout in seconds for host1.
+                       Default is 120 and 0 means no timeout at all.
+ --timeout2     int  : Connection timeout in seconds for host2.
+                       Default is 120 and 0 means no timeout at all.
+
+ --authmech1    str  : Auth mechanism to use with host1:
+                       PLAIN, LOGIN, CRAM-MD5 etc. Use UPPERCASE.
+ --authmech2    str  : Auth mechanism to use with host2. See --authmech1
+
+ --authuser1    str  : User to auth with on host1 (admin user).
+                       Avoid using --authmech1 SOMETHING with --authuser1.
+ --authuser2    str  : User to auth with on host2 (admin user).
+ --proxyauth1        : Use proxyauth on host1. Requires --authuser1.
+                       Required by Sun/iPlanet/Netscape IMAP servers to
+                       be able to use an administrative user.
+ --proxyauth2        : Use proxyauth on host2. Requires --authuser2.
+
+ --authmd51          : Use MD5 authentification for host1.
+ --authmd52          : Use MD5 authentification for host2.
+ --domain1      str  : Domain on host1 (NTLM authentication).
+ --domain2      str  : Domain on host2 (NTLM authentication).
+
+
+ --folder       str  : Sync this folder.
+ --folder       str  : and this one, etc.
+ --folderrec    str  : Sync this folder recursively.
+ --folderrec    str  : and this one, etc.
+
+ --folderfirst  str  : Sync this folder first. --folderfirst "Work"
+ --folderfirst  str  : then this one, etc.
+ --folderlast   str  : Sync this folder last. --folderlast "[Gmail]/All Mail"
+ --folderlast   str  : then this one, etc.
+
+ --nomixfolders      : Do not merge folders when host1 is case sensitive
+                       while host2 is not (like Exchange). Only the first
+                       similar folder is synced (ex: Sent SENT sent -> Sent).
+
+ --skipemptyfolders  : Empty host1 folders are not created on host2.
+
+ --include      reg  : Sync folders matching this regular expression
+ --include      reg  : or this one, etc.
+                       in case both --include --exclude options are
+                       use, include is done before.
+ --exclude      reg  : Skips folders matching this regular expression
+                       Several folders to avoid:
+                        --exclude 'fold1|fold2|f3' skips fold1, fold2 and f3.
+ --exclude      reg  : or this one, etc.
+
+ --subfolder2   str  : Move whole host1 folders hierarchy under this
+                       host2 folder  str    .
+                       It does it by adding two --regextrans2 options before
+                       all others. Add --debug to see what's really going on.
+
+ --automap           : guesses folders mapping, for folders like
+                       "Sent", "Junk", "Drafts", "All", "Archive", "Flagged".
+ --f1f2    str1=str2 : Force folder str1 to be synced to str2,
+                       --f1f2 overrides --automap and --regextrans2.
+ --regextrans2  reg  : Apply the whole regex to each destination folders.
+ --regextrans2  reg  : and this one. etc.
+                       When you play with the --regextrans2 option, first
+                       add also the safe options --dry --justfolders
+                       Then, when happy, remove --dry, remove --justfolders.
+                       Have in mind that --regextrans2 is applied after prefix
+                       and separator inversion. For examples see
+                       http://imapsync.lamiral.info/FAQ.d/FAQ.Folders_Mapping.txt
+
+ --tmpdir       str  : Where to store temporary files and subdirectories.
+                       Will be created if it doesn't exist.
+                       Default is system specific, Unix is /tmp but
+                       it's often small and deleted at reboot.
+                       --tmpdir /var/tmp should be better.
+ --pidfile      str  : The file where imapsync pid is written.
+ --pidfilelocking    : Abort if pidfile already exists. Usefull to avoid
+                       concurrent transfers on the same mailbox.
+
+ --nolog             : Turn off logging on file
+ --logfile      str  : Change the default log filename (can be dirname/filename).
+ --logdir       str  : Change the default log directory. Default is LOG_imapsync
+
+ --prefix1      str  : Remove prefix to all destination folders
+                       (usually INBOX. or INBOX/ or an empty string "")
+                       you have to use --prefix1 if host1 imap server
+                       does not have NAMESPACE capability, so imapsync
+                       suggests to use it. All other cases are bad.
+ --prefix2      str  : Add prefix to all host2 folders. See --prefix1
+ --sep1         str  : Host1 separator in case NAMESPACE is not supported.
+ --sep2         str  : Host2 separator in case NAMESPACE is not supported.
+
+ --skipmess     reg  : Skips messages maching the regex.
+                       Example: 'm/[\\x80-ff]/' # to avoid 8bits messages.
+                       --skipmess is applied before --regexmess
+ --skipmess     reg  : or this one, etc.
+
+ --pipemess     cmd  : Apply this cmd command to each message content
+                       before the copy.
+ --pipemess     cmd  : and this one, etc.
+
+ --disarmreadreceipts : Disarms read receipts (host2 Exchange issue)
+
+ --regexmess    reg  : Apply the whole regex to each message before transfer.
+                       Example: 's/\\000/ /g' # to replace null by space.
+ --regexmess    reg  : and this one, etc.
+
+ --regexflag    reg  : Apply the whole regex to each flags list.
+                       Example: 's/\"Junk"//g' # to remove "Junk" flag.
+ --regexflag    reg  : and this one, etc.
+
+ --delete            : Deletes messages on host1 server after a successful
+                       transfer. Option --delete has the following behavior:
+                       it marks messages as deleted with the IMAP flag
+                       \\Deleted, then messages are really deleted with an
+                       EXPUNGE IMAP command.
+
+ --delete2           : Delete messages in host2 that are not in
+                       host1 server. Useful for backup or pre-sync.
+ --delete2duplicates : Delete messages in host2 that are duplicates.
+                       Works only without --useuid since duplicates are
+                       detected with an header part of each message.
+
+ --delete2folders    : Delete folders in host2 that are not in host1 server.
+                       For safety, first try it like this (it is safe):
+                       --delete2folders --dry --justfolders --nofoldersizes
+ --delete2foldersonly   reg : Deleted only folders matching regex.
+                              Example: --delete2foldersonly "/^Junk\$|^INBOX.Junk\$/"
+ --delete2foldersbutnot reg : Do not delete folders matching regex.
+                              Example: --delete2foldersbutnot "/Tasks\$|Contacts\$|Foo\$/"
+ --noexpunge         : Do not expunge messages on host1.
+                       Expunge really deletes messages marked deleted.
+                       Expunge is made at the beginning, on host1 only.
+                       Newly transferred messages are also expunged if
+                       option --delete is given.
+                       No expunge is done on host2 account (unless --expunge2)
+ --expunge1          : Expunge messages on host1 after messages transfer.
+ --expunge2          : Expunge messages on host2 after messages transfer.
+ --uidexpunge2       : uidexpunge messages on the host2 account
+                       that are not on the host1 account, requires --delete2
+ --nomixfolders      : Avoid merging folders that are considered different on
+                       host1 but the same on destination host2 because of
+                       case sensitivities and insensitivities.
+
+ --syncinternaldates : Sets the internal dates on host2 same as host1.
+                       Turned on by default. Internal date is the date
+                       a message arrived on a host (mtime).
+ --idatefromheader   : Sets the internal dates on host2 same as the
+                       "Date:" headers.
+
+ --maxsize      int  : Skip messages larger  (or equal) than  int  bytes
+ --minsize      int  : Skip messages smaller (or equal) than  int  bytes
+ --maxage       int  : Skip messages older than  int  days.
+                       final stats (skipped) don't count older messages
+                       see also --minage
+ --minage       int  : Skip messages newer than  int  days.
+                       final stats (skipped) don't count newer messages
+                       You can do (+ are the messages selected):
+                       past|----maxage+++++++++++++++>now
+                       past|+++++++++++++++minage---->now
+                       past|----maxage+++++minage---->now (intersection)
+                       past|++++minage-----maxage++++>now (union)
+
+ --search       str  : Selects only messages returned by this IMAP SEARCH
+                       command. Applied on both sides.
+ --search1      str  : Same as --search for selecting host1 messages only.
+ --search2      str  : Same as --search for selecting host2 messages only.
+                       --search CRIT equals --search1 CRIT --search2 CRIT
+
+ --exitwhenover int  : Stop syncing when total bytes transferred reached.
+                       Gmail per day allows
+                       2500000000 = 2.5 GB downloaded from Gmail as host2
+                        500000000 = 500 MB uploaded to Gmail as host1.
+
+ --maxlinelength int : skip messages with a line length longer than  int  bytes.
+                       RFC 2822 says it must be no more than 1000 bytes.
+
+ --useheader    str  : Use this header to compare messages on both sides.
+                       Ex: Message-ID or Subject or Date.
+ --useheader    str    and this one, etc.
+
+ --subscribed        : Transfers subscribed folders.
+ --subscribe         : Subscribe to the folders transferred on the
+                       host2 that are subscribed on host1. On by default.
+ --subscribeall      : Subscribe to the folders transferred on the
+                       host2 even if they are not subscribed on host1.
+
+ --nofoldersizes     : Do not calculate the size of each folder in bytes
+                       and message counts. Default is to calculate them.
+ --nofoldersizesatend: Do not calculate the size of each folder in bytes
+                       and message counts at the end. Default is on.
+ --justfoldersizes   : Exit after having printed the folder sizes.
+
+ --syncacls          : Synchronises acls (Access Control Lists).
+ --nosyncacls        : Does not synchronize acls. This is the default.
+                       Acls in IMAP are not standardized, be careful.
+
+ --usecache          : Use cache to speedup.
+ --nousecache        : Do not use cache. Caveat: --useuid --nousecache creates
+                       duplicates on multiple runs.
+ --useuid            : Use uid instead of header as a criterium to recognize
+                       messages. Option --usecache is then implied unless
+                       --nousecache is used.
+
+ --debug             : Debug mode.
+ --debugfolders      : Debug mode for the folders part only.
+ --debugcontent      : Debug content of the messages transfered. Huge ouput.
+ --debugflags        : Debug mode for flags.
+ --debugimap1        : IMAP debug mode for host1. Very verbose.
+ --debugimap2        : IMAP debug mode for host2. Very verbose.
+ --debugimap         : IMAP debug mode for host1 and host2.
+ --debugmemory       : Debug mode showing memory consumption after each copy.
+
+ --errorsmax     int : Exit when int number of errors is reached. Default is 50.
+
+ --tests             : Run local non-regression tests. Exit code 0 means all ok.
+ --testslive         : Run a live test with test1.lamiral.info imap server.
+                       Useful to check the basics. Needs internet connexion.
+
+ --version           : Print only software version.
+ --noreleasecheck    : Do not check for new imapsync release (a http request).
+ --releasecheck      : Check for new imapsync release (a http request).
+ --noid              : Do not send/receive ID command to imap servers.
+ --justconnect       : Just connect to both servers and print useful
+                       information. Need only --host1 and --host2 options.
+ --justlogin         : Just login to both host1 and host2 with users
+                       credentials, then exit.
+ --justfolders       : Do only things about folders (ignore messages).
+
+ --help              : print this help.
+
+ Example: to synchronize imap account "test1" on "test1.lamiral.info"
+                     to  imap account "test2" on "test2.lamiral.info"
+                     with test1 password "secret1"
+                     and  test2 password "secret2"
+
+ $0 $escape_char
+    --host1 test1.lamiral.info --user1 test1 --password1 secret1 $escape_char
+    --host2 test2.lamiral.info --user2 test2 --password2 secret2
+
+$localhost_info
+$rcs
+$imapsync_release
+
+$thank
+EOF
+	return( 1 ) ;
+}
+
+
+sub usage_complete {
+	myprint( <<'EOF'  ) ;
+--skipheader   reg     : Don't take into account header keyword
+                         matching  reg    ex: --skipheader 'X.*'
+
+--skipsize             : Don't take message size into account to compare
+                         messages on both sides. On by default.
+			 Use --no-skipsize for using size comparaison.
+--allowsizemismatch    : allow RFC822.SIZE != fetched msg size
+                         consider also --skipsize to avoid duplicate messages
+                         when running syncs more than one time per mailbox
+
+--reconnectretry1  int : reconnect to host1 if connection is lost up to
+                          int  times per imap command (default is 3)
+--reconnectretry2  int : same as --reconnectretry1 but for host2
+--split1      int      : split the requests in several parts on host1.
+                          int  is the number of messages handled per request.
+                         default is like --split1 500.
+--split2      int      : same thing on host2.
+--nofixInboxINBOX      : Don't fix Inbox INBOX mapping.
+EOF
+	return ;
+}
+
+
+
+sub get_options {
+	# In CGI context arguments are not in @ARGV but in QUERY_STRING variable (with GET).
+	my $numopt = scalar  @ARGV  || length $ENV{'QUERY_STRING'} ;
+	my $argv   = join "\x00", @ARGV ;
+
+	if ( $argv =~ m/-delete\x002/x ) {
+		myprint( "May be you mean --delete2 instead of --delete 2\n"  ) ;
+		exit 1 ;
+	}
+	$sync->{f1f2} = {} ;
+        my $opt_ret = Imapsync::Getopt::Long::GetOptions(
+        'debug!'        => \$debug,
+        'debuglist!'    => \$debuglist,
+        'debugcontent!' => \$debugcontent,
+        'debugsleep=f'  => \$sync->{debugsleep},
+        'debugflags!'   => \$debugflags,
+        'debugimap!'    => \$debugimap,
+        'debugimap1!'   => \$debugimap1,
+        'debugimap2!'   => \$debugimap2,
+        'debugdev!'     => \$debugdev,
+        'debugmemory!'  => \$sync->{debugmemory},
+        'debugfolders!' => \$sync->{debugfolders},
+        'debugssl=i'    => \$sync->{debugssl},
+	'debugbasket=s' => \@debugbasket,
+	'debugcgi!'     => \$debugcgi,
+        'host1=s'     => \$host1,
+        'host2=s'     => \$host2,
+        'port1=i'     => \$port1,
+        'port2=i'     => \$port2,
+	'inet4'       => \$sync->{inet4},
+	'inet6'       => \$sync->{inet6},
+        'user1=s'     => \$user1,
+        'user2=s'     => \$user2,
+        'domain1=s'   => \$domain1,
+        'domain2=s'   => \$domain2,
+        'password1=s' => \$password1,
+        'password2=s' => \$password2,
+        'passfile1=s' => \$passfile1,
+        'passfile2=s' => \$passfile2,
+        'authmd5!'    => \$authmd5,
+        'authmd51!'   => \$authmd51,
+        'authmd52!'   => \$authmd52,
+        'sep1=s'      => \$sep1,
+        'sep2=s'      => \$sep2,
+        'folder=s'    => \@folder,
+        'folderrec=s' => \@folderrec,
+        'include=s'   => \@include,
+        'exclude=s'   => \@exclude,
+        'folderfirst=s' => \@folderfirst,
+        'folderlast=s' => \@folderlast,
+        'prefix1=s'   => \$prefix1,
+        'prefix2=s'   => \$prefix2,
+	'subfolder2=s' => \$subfolder2,
+        'fixslash2!'   => \$fixslash2,
+        'fixInboxINBOX!' => \$fixInboxINBOX,
+        'regextrans2=s' => \@regextrans2,
+        'mixfolders!' => \$mixfolders,
+        'skipemptyfolders!' => \$skipemptyfolders,
+        'regexmess=s' => \@regexmess,
+        'skipmess=s' => \@skipmess,
+        'pipemess=s' => \@pipemess,
+	'pipemesscheck!' => \$pipemesscheck,
+        'disarmreadreceipts!' => \$disarmreadreceipts,
+        'regexflag=s' => \@regexflag,
+        'filterflags!' => \$filterflags,
+        'flagscase!'  => \$flagscase,
+        'syncflagsaftercopy!' => \$syncflagsaftercopy,
+        'delete|delete1!' => \$delete,
+        'delete2!'    => \$delete2,
+        'delete2duplicates!' => \$delete2duplicates,
+        'delete2folders!'    => \$delete2folders,
+        'delete2foldersonly=s' => \$delete2foldersonly,
+        'delete2foldersbutnot=s' => \$delete2foldersbutnot,
+        'syncinternaldates!' => \$syncinternaldates,
+        'idatefromheader!'   => \$idatefromheader,
+        'syncacls!'   => \$syncacls,
+        'maxsize=i'   => \$maxsize,
+        'minsize=i'   => \$minsize,
+        'maxage=i'    => \$maxage,
+        'minage=i'    => \$minage,
+        'search=s'    => \$search,
+        'search1=s'   => \$search1,
+        'search2=s'   => \$search2,
+        'foldersizes!' => \$foldersizes,
+        'foldersizesatend!' => \$foldersizesatend,
+        'dry!'        => \$dry,
+        'expunge!'    => \$expunge,
+        'expunge1!'    => \$expunge1,
+        'expunge2!'    => \$expunge2,
+        'uidexpunge2!' => \$uidexpunge2,
+        'subscribed!' => \$subscribed,
+        'subscribe!'  => \$subscribe,
+        'subscribeall|subscribe_all!'  => \$subscribeall,
+        'justbanner!' => \$justbanner,
+        'justconnect!'=> \$justconnect,
+        'justfolders!'=> \$justfolders,
+        'justfoldersizes!' => \$justfoldersizes,
+        'fast!'       => \$fast,
+        'version'     => \$version,
+        'help'        => \$help,
+        'timeout=i'   => \$timeout,
+        'timeout1=i'   => \$sync->{h1}->{timeout},
+        'timeout2=i'   => \$sync->{h2}->{timeout},
+        'skipheader=s' => \$skipheader,
+        'useheader=s' => \@useheader,
+        'wholeheaderifneeded!'   => \$wholeheaderifneeded,
+        'messageidnodomain!' => \$messageidnodomain,
+        'skipsize!'   => \$skipsize,
+        'allowsizemismatch!' => \$allowsizemismatch,
+        'fastio1!'     => \$fastio1,
+        'fastio2!'     => \$fastio2,
+        'ssl1!'        => \$ssl1,
+        'ssl2!'        => \$ssl2,
+        'ssl1_ssl_version=s' => \$sync->{h1}->{sslargs}->{SSL_version},
+        'ssl2_ssl_version=s' => \$sync->{h2}->{sslargs}->{SSL_version},
+        'sslargs1=s%'        => \$sync->{h1}->{sslargs},
+        'sslargs2=s%'        => \$sync->{h2}->{sslargs},
+        'tls1!'        => \$tls1,
+        'tls2!'        => \$tls2,
+        'uid1!'        => \$uid1,
+        'uid2!'        => \$uid2,
+        'authmech1=s' => \$authmech1,
+        'authmech2=s' => \$authmech2,
+        'authuser1=s' => \$authuser1,
+        'authuser2=s' => \$authuser2,
+        'proxyauth1'  => \$proxyauth1,
+        'proxyauth2'  => \$proxyauth2,
+        'split1=i'    => \$split1,
+        'split2=i'    => \$split2,
+        'buffersize=i' => \$buffersize,
+        'reconnectretry1=i' => \$reconnectretry1,
+        'reconnectretry2=i' => \$reconnectretry2,
+        'tests!'       => \$tests,
+        'testsdebug|tests_debug!' => \$testsdebug,
+        'testslive!'   => \$testslive,
+        'justlogin!'  => \$justlogin,
+        'tmpdir=s'    => \$tmpdir,
+        'pidfile=s'    => \$sync->{pidfile},
+        'pidfilelocking!' => \$sync->{pidfilelocking},
+        'releasecheck!' => \$releasecheck,
+        'modulesversion|modules_version!' => \$modulesversion,
+        'usecache!'    => \$usecache,
+        'cacheaftercopy!' => \$cacheaftercopy,
+        'debugcache!' => \$debugcache,
+        'useuid!'     => \$useuid,
+        'addheader!'  => \$addheader,
+        'exitwhenover=i' => \$exitwhenover,
+        'checkselectable!' => \$checkselectable,
+        'checkmessageexists!' => \$checkmessageexists,
+        'expungeaftereach!' => \$expungeaftereach,
+        'abletosearch!' => \$abletosearch,
+        'showpasswords!' => \$showpasswords,
+        'maxlinelength=i' => \$maxlinelength,
+        'maxlinelengthcmd=s' => \$maxlinelengthcmd,
+        'minmaxlinelength=i' => \$minmaxlinelength,
+        'debugmaxlinelength!' => \$debugmaxlinelength,
+        'fixcolonbug!'           => \$fixcolonbug,
+        'create_folder_old!'     => \$create_folder_old,
+        'maxmessagespersecond=f' => \$maxmessagespersecond,
+        'maxbytespersecond=i'    => \$maxbytespersecond,
+        'skipcrossduplicates!'   => \$skipcrossduplicates,
+        'debugcrossduplicates!'  => \$debugcrossduplicates,
+        'log!'                   => \$sync->{log},
+        'logfile=s'        => \$sync->{logfile},
+        'logdir=s'         => \$sync->{logdir},
+        'errorsmax=i'      => \$sync->{errorsmax},
+        'errorsdump!'      => \$sync->{errorsdump},
+        'fetch_hash_set=s' => \$fetch_hash_set,
+        'automap!'         => \$sync->{automap},
+        'justautomap!'     => \$sync->{justautomap},
+        'id!'              => \$sync->{id},
+        'f1f2=s%'          => \$sync->{f1f2},
+        'justfolderlists!' => \$sync->{justfolderlists},
+        'delete1emptyfolders' => \$sync->{delete1emptyfolders},
+        ) ;
+
+
+	$debugcgi and myprint( map { "$_ => $ENV{$_}\n" } sort keys  %ENV   ) ;
+	$debugcgi and myprint( "@debugbasket\n"  ) ;
+        $debug and myprint( "get options: [$opt_ret]\n"  ) ;
+
+        # just the version
+        myprint( imapsync_version(  ), "\n" ) and exit 0 if ( $version ) ;
+        # $tmpdir is used in tests_pipemess()
+	$tmpdir ||= File::Spec->tmpdir(  ) ;
+	if ( $tests or $testsdebug ) {
+		$test_builder = Test::More->builder ;
+		if ( $tests ) { tests(  ) ; }
+		if ( $testsdebug ) { testsdebug(  ) ; }
+		#$test_builder->reset(  ) ;
+		exit ;
+	}
+
+	#$help = 1 if ! $numopt;
+	load_modules(  );
+
+	# exit with --help option or no option at all
+	$debug and myprint( "numopt:$numopt\n"  ) ;
+        usage(  ) and exit  if ( $help or not $numopt ) ;
+
+	# don't go on if options are not all known.
+        exit $EX_USAGE unless ( $opt_ret ) ;
+
+	# init live varaiables
+	testslive(  ) if ( $testslive ) ;
+
+	return ;
+}
+
+sub testslive {
+	$host1 = 'test1.lamiral.info' ;
+	$user1 = 'test1' ;
+	$password1 = 'secret1' ;
+	$host2 = 'test2.lamiral.info' ;
+	$user2 = 'test2' ;
+	$password2 ='secret2' ;
+	return ;
+}
+
+sub testsdebug {
+      SKIP: {
+                skip 'No test in normal run' if ( not $testsdebug ) ;
+                #tests_bytes_display_string(  ) ;
+                #tests_ucsecond(  ) ;
+                #tests_mkpath(  ) ;
+                #eval { tests_mkpath(  ) ; } or ok( 0 == 1,  'tests_mkpath fail badly?' ) ;
+                #tests_format_for_imap_arg(  ) ;
+                #tests_is_a_release_number(  ) ;
+                #tests_delete1emptyfolders(  ) ;
+                #tests_memory_consumption(  ) ;
+                #tests_imap2_folder_name() ;
+                #tests_length_ref(  ) ;
+		#tests_is_valid_directory(  ) ;
+                #tests_firstline(  ) ;
+                #tests_diff_or_NA(  ) ;
+                #tests_match_number(  ) ;
+                #tests_all_defined(  ) ;
+                #tests_guess_separator(  ) ;
+                tests_pipemess(  ) ;
+                #tests_message_for_host2(  ) ;
+                done_testing(  ) ;
+                note('End of imapsync --tests_debug') ;
+        }
+        return ;
+}
+
+sub tests {
+
+      SKIP: {
+                skip 'No test in normal run' if ( not $tests ) ;
+                tests_folder_routines(  ) ;
+                tests_compare_lists(  ) ;
+                tests_regexmess();
+                tests_skipmess(  ) ;
+                tests_flags_regex();
+                tests_ucsecond(  ) ;
+                tests_permanentflags();
+                tests_flags_filter(  ) ;
+                tests_separator_invert(  ) ;
+                tests_imap2_folder_name() ;
+                tests_command_line_nopassword();
+                tests_good_date(  ) ;
+                tests_max();
+                tests_remove_not_num();
+                tests_memory_consumption( ) ;
+                tests_is_a_release_number();
+                tests_imapsync_basename();
+                tests_list_keys_in_2_not_in_1();
+                tests_convert_sep_to_slash(  ) ;
+                tests_match_a_cache_file(  ) ;
+                tests_cache_map(  ) ;
+                tests_get_cache(  ) ;
+                tests_clean_cache(  ) ;
+                tests_clean_cache_2(  ) ;
+                tests_touch(  ) ;
+                tests_flagscase(  ) ;
+                eval { tests_mkpath(  ) ; } or ok( 0 == 1,  'tests_mkpath fail badly?' ) ;
+                tests_extract_header(  ) ;
+                tests_decompose_header(  ) ;
+                tests_epoch(  ) ;
+                tests_add_header(  ) ;
+                tests_cache_dir_fix(  ) ;
+                tests_cache_dir_fix_win(  ) ;
+                tests_filter_forbidden_characters(  ) ;
+                tests_cache_folder(  ) ;
+                tests_time_remaining(  ) ;
+                tests_decompose_regex(  ) ;
+                tests_Banner(  ) ;
+                tests_backtick(  ) ;
+                tests_bytes_display_string(  ) ;
+                tests_header_line_normalize(  ) ;
+                tests_fix_Inbox_INBOX_mapping(  ) ;
+                tests_max_line_length(  ) ;
+                tests_subject(  ) ;
+                tests_msgs_from_maxmin(  ) ;
+                tests_tmpdir_has_colon_bug(  ) ;
+                tests_sleep_max_messages(  ) ;
+                tests_sleep_max_bytes(  ) ;
+                tests_logfile(  ) ;
+                tests_setlogfile(  ) ;
+                tests_jux_utf8(  ) ;
+                tests_pipemess(  ) ;
+                tests_jux_utf8_list(  ) ;
+                tests_guess_prefix(  ) ;
+                tests_guess_separator(  ) ;
+                tests_format_for_imap_arg(  ) ;
+                tests_imapsync_id(  ) ;
+                tests_date_from_rcs(  ) ;
+                tests_quota_extract_storage_limit_in_bytes(  ) ;
+                tests_quota_extract_storage_current_in_bytes(  ) ;
+                tests_guess_special(  ) ;
+		tests_is_valid_directory(  ) ;
+                tests_delete1emptyfolders(  ) ;
+                tests_message_for_host2(  ) ;
+                tests_length_ref(  ) ;
+                tests_firstline(  ) ;               
+                tests_diff_or_NA(  ) ;
+                #tests_always_fail(  ) ;
+                tests_match_number(  ) ;
+                tests_all_defined(  ) ;
+                done_testing( 693 ) ;
+                note('End of imapsync --tests') ;
+        }
+        return ;
+}
+
+
+
+# IMAPClient 3.xx ads
+
+package Mail::IMAPClient;
+
+sub Tls {
+	my $self  = shift ;
+	my $value = shift ;
+	if ( defined  $value  ) { $self->{TLS} = $value }
+	return $self->{TLS};
+}
+
+sub Reconnect_counter {
+	my $self  = shift ;
+        my $value = shift ;
+	$self->{Reconnect_counter} = 0 if ( not defined  $self->{Reconnect_counter}  ) ;
+	if ( defined  $value  ) { $self->{Reconnect_counter} = $value }
+	return( $self->{Reconnect_counter} ) ;
+}
+
+
+sub Banner {
+	my $self  = shift ;
+	my $value = shift ;
+	if ( defined $value ) { $self->{ BANNER } = $value }
+	return $self->{ BANNER };
+}
+
+sub capability_update {
+	my $self = shift ;
+
+	delete $self->{CAPABILITY} ;
+	return( $self->capability ) ;
+}
+
+
+package Imapsync::Getopt::Long ;
+# Started as a copy of Luke Ross Getopt::Long::CGI
+# https://metacpan.org/release/Getopt-Long-CGI
+# So this section is under the same license as Getopt-Long-CGI Luke Ross wants it,
+# which was Perl 5.6 or later licenses at the date of the copy.
+
+use strict ;
+use warnings ;
+
+use Getopt::Long(  ) ;
+
+
+sub GetOptions {
+    my %options = @_ ;
+
+    if ( not $ENV{SERVER_SOFTWARE} ) {
+        # Not CGI - pass upstream for normal command line handling
+        return Getopt::Long::GetOptions( %options ) ;
+    }
+    my $b_ref = $options{'debugbasket=s'} ;
+    require CGI ;
+    require CGI::Carp ;
+    CGI::Carp->import( 'fatalsToBrowser' ) ;
+
+    my $cgi = CGI->new(  ) ;
+    $cgi->param( 'debugcgi' ) and myprint( "

Current Values

\n" . $cgi->Dump ) ; + + foreach my $key (sort keys %options) { + my $val = $options{$key}; + #push( @{$b_ref}, "opt:[$key] val:[$val]" . ( ('SCALAR' eq ref($val) and defined $$val ) ? " [$$val]" : q{} ) . "\n" ) ; + if ( $key !~ m/^([\w\d\|]+)([=:][isf])?([\+!\@\%])?$/ ) { + push @{$b_ref}, "Unknown opt: [$key]\n" ; + next ; # Unknown item + } + + my $name = [split '|', $1, 1 ]->[0]; + + if (($3 || q{}) eq '+') { + ${ $val } = $cgi->param($name); # "Incremental" integer + } elsif ($2) { + my @values = $cgi->param($name); + my $type = $2; + if (($3 || q{}) eq '%' or ref($val) eq 'HASH') { + my %values = map { split /=/, $_, 1 } @values; + if ($type =~ m/i$/) { + foreach my $k (keys %values) { + $values{$k} = int $values{$k} ; + } + } elsif ($type =~ m/f$/) { + foreach my $k (keys %values) { + $values{$k} = 0 + $values{$k} + } + } + if ( ref($val) eq 'CODE') { + while(my($k, $v) = each %values) { + $val->($name, $k, $v); + } + } elsif ( 'REF' eq ref $val ) { + #push( @{$b_ref}, "refref($$val): " . ref($$val) . " %values= ", %values, "\n\n" ) ; + %{ ${ $val } } = %values; + } else { + #push( @{$b_ref}, "ref($val): " . ref($val) . " %values= ", %values, "\n\n" ) ; + %{ $val } = %values; + } + } else { + if ($type =~ m/i$/) { + @values = map { int $_ } @values; + } elsif ($type =~ m/f$/) { + @values = map { 0 + $_ } @values; + } + if (($3 || q{}) eq '@' or ref($val) eq 'ARRAY') { + if (ref($val) eq 'CODE') { + $val->($name, \@values) + } else { + @{ $val } = @values ; + } + } else { + if (ref($val) eq 'CODE') { + $val->($name, $values[0]); + } else { + ${ $val } = $values[0]; + } + } + } + } else { + # Checkbox + ${ $val } = $cgi->param($name) ? 1 : undef ; + #push( @{$b_ref}, "param($name) ref($val): " . ref($val) . " val=[$$val]\n\n" ) ; + } + } + return( 1 ) ; +} + + diff --git a/data/Dockerfiles/dovecot/imapsync_cron.pl b/data/Dockerfiles/dovecot/imapsync_cron.pl new file mode 100755 index 00000000..5c47eb47 --- /dev/null +++ b/data/Dockerfiles/dovecot/imapsync_cron.pl @@ -0,0 +1,72 @@ +#!/usr/bin/perl + +use DBI; +use File::Temp qw/ mkstemp /; +use LockFile::Simple qw(lock trylock unlock); +use Data::Dumper qw(Dumper); +use IPC::Run 'run'; +use String::Util 'trim'; + +$DBNAME = ''; +$DBUSER = ''; +$DBPASS = ''; + +$run_dir="/tmp"; +$dsn = "DBI:mysql:database=" . $DBNAME . ";host=mysql"; +$lock_file = $run_dir . "/imapsync_busy"; +$lockmgr = LockFile::Simple->make(-autoclean => 1, -max => 1); +$lockmgr->lock($lock_file) || die "can't lock ${lock_file}"; +$dbh = DBI->connect($dsn, $DBUSER, $DBPASS); +open my $file, '<', "/etc/sogo/sieve.creds"; +my $creds = <$file>; +close $file; +my ($master_user, $master_pass) = split /:/, $creds; +my $sth = $dbh->prepare("SELECT id, user1, user2, host1, authmech1, password1, exclude, port1, enc1, delete2duplicates, maxage, subfolder2 FROM imapsync WHERE active = 1 AND (UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(last_run) > mins_interval * 60 OR last_run IS NULL)"); +$sth->execute(); +my $row; + +while ($row = $sth->fetchrow_arrayref()) { + + $id = @$row[0]; + $user1 = @$row[1]; + $user2 = @$row[2]; + $host1 = @$row[3]; + $authmech1 = @$row[4]; + $password1 = @$row[5]; + $exclude = @$row[6]; + $port1 = @$row[7]; + $enc1 = @$row[8]; + $delete2duplicates = @$row[9]; + $maxage = @$row[10]; + $subfolder2 = @$row[11]; + + if ($enc1 eq "TLS") { $enc1 = "--tls1"; } elsif ($enc1 eq "SSL") { $enc1 = "--ssl1"; } else { undef $enc1; } + + run [ "/usr/local/bin/imapsync", + "--timeout1", "10", + "--tmpdir", "/tmp", + "--subscribeall", + ($exclude eq "" ? () : ("--exclude", $exclude)), + ($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)), + ($maxage eq "0" ? () : ('--maxage', $maxage)), + ($delete2duplicates ne "1" ? () : ('--delete2duplicates')), + (!defined($enc1) ? () : ($enc1)), + "--host1", $host1, + "--user1", $user1, + "--password1", $password1, + "--port1", $port1, + "--host2", "localhost", + "--user2", $user2 . '*' . trim($master_user), + "--password2", trim($master_pass), + '--no-modulesversion'], ">", \my $stdout; + + $update = $dbh->prepare("UPDATE imapsync SET returned_text = ?, last_run = NOW() WHERE id = ?"); + $update->bind_param( 1, ${stdout} ); + $update->bind_param( 2, ${id} ); + $update->execute(); +} + +$sth->finish(); +$dbh->disconnect(); + +$lockmgr->unlock($lock_file); diff --git a/data/Dockerfiles/dovecot/postlogin.sh b/data/Dockerfiles/dovecot/postlogin.sh new file mode 100755 index 00000000..343910ff --- /dev/null +++ b/data/Dockerfiles/dovecot/postlogin.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +export MASTER_USER=$USER +exec "$@" diff --git a/data/Dockerfiles/dovecot/supervisord.conf b/data/Dockerfiles/dovecot/supervisord.conf new file mode 100644 index 00000000..45f9ddd5 --- /dev/null +++ b/data/Dockerfiles/dovecot/supervisord.conf @@ -0,0 +1,21 @@ +[supervisord] +nodaemon=true + +[program:syslog-ng] +command=/usr/sbin/syslog-ng --foreground --no-caps +redirect_stderr=true +autostart=true +stdout_syslog=true + +[program:dovecot] +command=/usr/sbin/dovecot -F +autorestart=true + +[program:logfiles] +command=/usr/bin/tail -f /var/log/mail.log /var/log/syslog +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 + +[program:cron] +command=/usr/sbin/cron -f +autorestart=true diff --git a/data/Dockerfiles/pdns/Dockerfile b/data/Dockerfiles/pdns/Dockerfile deleted file mode 100644 index b56dcf9c..00000000 --- a/data/Dockerfiles/pdns/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -FROM ubuntu:xenial -MAINTAINER Andre Peters - -ENV DEBIAN_FRONTEND noninteractive -ENV LC_ALL C - -RUN dpkg-divert --local --rename --add /sbin/initctl \ - && ln -sf /bin/true /sbin/initctl \ - && dpkg-divert --local --rename --add /usr/bin/ischroot \ - && ln -sf /bin/true /usr/bin/ischroot - -RUN echo 'deb http://repo.powerdns.com/ubuntu xenial-rec-40 main' > /etc/apt/sources.list.d/pdns.list - -RUN echo 'Package: pdns-*\n\ -Pin: origin repo.powerdns.com\n\ -Pin-Priority: 600\n' > /etc/apt/preferences.d/pdns - -RUN apt-key adv --fetch-keys http://repo.powerdns.com/FD380FBB-pub.asc \ - && apt-get update \ - && apt-get install -y --force-yes pdns-recursor - -CMD ["/usr/sbin/pdns_recursor"] - -EXPOSE 53/udp - -RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* diff --git a/data/Dockerfiles/php-fpm/Dockerfile b/data/Dockerfiles/php-fpm/Dockerfile index d75287a2..9c0cea14 100644 --- a/data/Dockerfiles/php-fpm/Dockerfile +++ b/data/Dockerfiles/php-fpm/Dockerfile @@ -8,6 +8,7 @@ RUN apt-get update \ RUN docker-php-ext-configure intl RUN docker-php-ext-install intl pdo pdo_mysql xmlrpc +RUN pear install channel://pear.php.net/Net_IDNA2-0.1.1 Auth_SASL Net_IMAP NET_SMTP Net_IDNA2 Mail_mime COPY ./docker-entrypoint.sh / diff --git a/data/Dockerfiles/postfix/Dockerfile b/data/Dockerfiles/postfix/Dockerfile index bc6015d7..6a36f443 100644 --- a/data/Dockerfiles/postfix/Dockerfile +++ b/data/Dockerfiles/postfix/Dockerfile @@ -1,4 +1,4 @@ -From ubuntu:xenial +FROM ubuntu:xenial MAINTAINER Andre Peters ENV DEBIAN_FRONTEND noninteractive @@ -13,6 +13,7 @@ RUN apt-get update RUN apt-get install -y --no-install-recommends supervisor \ postfix \ sasl2-bin \ + libsasl2-modules \ postfix \ postfix-mysql \ postfix-pcre \ diff --git a/data/Dockerfiles/postfix/postfix.sh b/data/Dockerfiles/postfix/postfix.sh index 7aefd612..8c079405 100755 --- a/data/Dockerfiles/postfix/postfix.sh +++ b/data/Dockerfiles/postfix/postfix.sh @@ -2,10 +2,101 @@ trap "postfix stop" EXIT -sed -i "/^user/c\user = ${DBUSER}" /opt/postfix/conf/sql/* -sed -i "/^password/c\password = ${DBPASS}" /opt/postfix/conf/sql/* -sed -i "/^dbname/c\dbname = ${DBNAME}" /opt/postfix/conf/sql/* +[[ ! -d /opt/postfix/conf/sql/ ]] && mkdir -p /opt/postfix/conf/sql/ -postfix -c /opt/postfix/conf start +cat < /opt/postfix/conf/sql/mysql_relay_recipient_maps.cf +user = ${DBUSER} +password = ${DBPASS} +hosts = mysql +dbname = ${DBNAME} +query = SELECT DISTINCT CASE WHEN '%d' IN (SELECT domain FROM domain WHERE relay_all_recipients=1 AND domain='%d' AND backupmx=1) THEN '%s' ELSE (SELECT goto FROM alias WHERE address='%s' AND active='1') END AS result; +EOF -sleep infinity +cat < /opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf +user = ${DBUSER} +password = ${DBPASS} +hosts = mysql +dbname = ${DBNAME} +query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.address WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_in = '1' AND mailbox.active = '1'), 'reject_plaintext_session', 'DUNNO') AS 'tls_enforce_in'; +EOF + +cat < /opt/postfix/conf/sql/mysql_tls_enforce_out_policy.cf +user = ${DBUSER} +password = ${DBPASS} +hosts = mysql +dbname = ${DBNAME} +query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.address WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_out = '1' AND mailbox.active = '1'), 'smtp_enforced_tls:', 'DUNNO') AS 'tls_enforce_out'; +EOF + +cat < /opt/postfix/conf/sql/mysql_virtual_alias_domain_catchall_maps.cf +user = ${DBUSER} +password = ${DBPASS} +hosts = mysql +dbname = ${DBNAME} +query = SELECT goto FROM alias,alias_domain WHERE alias_domain.alias_domain = '%d' and alias.address = CONCAT('@', alias_domain.target_domain) AND alias.active = 1 AND alias_domain.active='1' +EOF + +cat < /opt/postfix/conf/sql/mysql_virtual_alias_domain_maps.cf +user = ${DBUSER} +password = ${DBPASS} +hosts = mysql +dbname = ${DBNAME} +query = SELECT username FROM mailbox,alias_domain WHERE alias_domain.alias_domain = '%d' and mailbox.username = CONCAT('%u', '@', alias_domain.target_domain) AND mailbox.active = 1 AND alias_domain.active='1' +EOF + +cat < /opt/postfix/conf/sql/mysql_virtual_alias_maps.cf +user = ${DBUSER} +password = ${DBPASS} +hosts = mysql +dbname = ${DBNAME} +query = SELECT goto FROM alias WHERE address='%s' AND active='1'; +EOF + +cat < /opt/postfix/conf/sql/mysql_virtual_domains_maps.cf +user = ${DBUSER} +password = ${DBPASS} +hosts = mysql +dbname = ${DBNAME} +query = SELECT alias_domain from alias_domain WHERE alias_domain='%s' AND active='1' UNION SELECT domain FROM domain WHERE domain='%s' AND active = '1' AND backupmx = '0' +EOF + +cat < /opt/postfix/conf/sql/mysql_virtual_mailbox_maps.cf +user = ${DBUSER} +password = ${DBPASS} +hosts = mysql +dbname = ${DBNAME} +query = SELECT maildir FROM mailbox WHERE username='%s' AND active = '1' +EOF + +cat < /opt/postfix/conf/sql/mysql_virtual_relay_domain_maps.cf +user = ${DBUSER} +password = ${DBPASS} +hosts = mysql +dbname = ${DBNAME} +query = SELECT domain FROM domain WHERE domain='%s' AND backupmx = '1' AND active = '1' +EOF + +cat < /opt/postfix/conf/sql/mysql_virtual_sender_acl.cf +user = ${DBUSER} +password = ${DBPASS} +hosts = mysql +dbname = ${DBNAME} +query = SELECT goto FROM alias WHERE address='%s' AND active='1' AND domain IN(SELECT domain FROM domain WHERE domain='%d' AND active='1') UNION SELECT logged_in_as FROM sender_acl WHERE send_as='@%d' OR send_as='%s' OR send_as IN ( SELECT CONCAT ('@',target_domain) FROM alias_domain WHERE alias_domain = '%d') OR send_as IN ( SELECT CONCAT ('%u','@',target_domain) FROM alias_domain WHERE alias_domain = '%d' ) AND logged_in_as NOT IN (SELECT goto FROM alias WHERE address='%s') UNION SELECT username FROM mailbox,alias_domain WHERE alias_domain.alias_domain = '%d' AND mailbox.username = CONCAT('%u','@',alias_domain.target_domain) AND mailbox.active ='1' AND alias_domain.active='1' +EOF + +cat < /opt/postfix/conf/sql/mysql_virtual_spamalias_maps.cf +user = ${DBUSER} +password = ${DBPASS} +hosts = mysql +dbname = ${DBNAME} +query = SELECT goto FROM spamalias WHERE address='%s' AND validity >= UNIX_TIMESTAMP() +EOF + +postconf -c /opt/postfix/conf +if [[ $? != 0 ]]; then + echo "Postfix configuration error, refusing to start." + exit 1 +else + postfix -c /opt/postfix/conf start + sleep 126144000 +fi diff --git a/data/Dockerfiles/rspamd/Dockerfile b/data/Dockerfiles/rspamd/Dockerfile index d4d4508d..8d0a7f0a 100644 --- a/data/Dockerfiles/rspamd/Dockerfile +++ b/data/Dockerfiles/rspamd/Dockerfile @@ -1,5 +1,5 @@ FROM ubuntu:xenial -MAINTAINER Andre Peters +MAINTAINER Andre Peters ENV DEBIAN_FRONTEND noninteractive ENV LC_ALL C @@ -9,16 +9,19 @@ RUN dpkg-divert --local --rename --add /sbin/initctl \ && dpkg-divert --local --rename --add /usr/bin/ischroot \ && ln -sf /bin/true /usr/bin/ischroot -RUN apt-key adv --fetch-keys http://rspamd.com/apt-stable/gpg.key \ - && echo "deb http://rspamd.com/apt-stable/ xenial main" > /etc/apt/sources.list.d/rspamd.list \ +RUN apt-key adv --fetch-keys http://rspamd.com/apt/gpg.key \ + && echo "deb http://rspamd.com/apt/ xenial main" > /etc/apt/sources.list.d/rspamd.list \ && apt-get update \ - && apt-get -y install rspamd ca-certificates + && apt-get -y install rspamd ca-certificates python-pip RUN echo '.include $LOCAL_CONFDIR/local.d/rspamd.conf.local' > /etc/rspamd/rspamd.conf.local -# "Hardcoded" - we need them -RUN echo 'settings = "http://nginx:8081/settings.php";' > /etc/rspamd/modules.d/settings.conf -CMD ["/usr/bin/rspamd","-f", "-u", "_rspamd", "-g", "_rspamd"] +COPY settings.conf /etc/rspamd/modules.d/settings.conf +COPY antivirus.conf /etc/rspamd/modules.d/antivirus.conf + +RUN pip install -U oletools + +CMD /usr/bin/rspamd -f -u _rspamd -g _rspamd RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* diff --git a/data/Dockerfiles/rspamd/antivirus.conf b/data/Dockerfiles/rspamd/antivirus.conf new file mode 100644 index 00000000..cb8049d4 --- /dev/null +++ b/data/Dockerfiles/rspamd/antivirus.conf @@ -0,0 +1,4 @@ +antivirus { + .include(try=true,priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/antivirus.conf" + .include(try=true,priority=10) "$LOCAL_CONFDIR/override.d/antivirus.conf" +} diff --git a/data/Dockerfiles/rspamd/settings.conf b/data/Dockerfiles/rspamd/settings.conf new file mode 100644 index 00000000..4449f091 --- /dev/null +++ b/data/Dockerfiles/rspamd/settings.conf @@ -0,0 +1 @@ +settings = "http://nginx:8081/settings.php"; diff --git a/data/Dockerfiles/sogo/Dockerfile b/data/Dockerfiles/sogo/Dockerfile index 2d043fdd..43960438 100644 --- a/data/Dockerfiles/sogo/Dockerfile +++ b/data/Dockerfiles/sogo/Dockerfile @@ -11,7 +11,14 @@ RUN dpkg-divert --local --rename --add /sbin/initctl \ && ln -sf /bin/true /usr/bin/ischroot RUN apt-get update \ - && apt-get install -y --no-install-recommends apt-transport-https ca-certificates wget syslog-ng syslog-ng-core supervisor mysql-client cron \ + && apt-get install -y --no-install-recommends apt-transport-https \ + ca-certificates \ + wget \ + syslog-ng \ + syslog-ng-core \ + supervisor \ + mysql-client \ + cron \ && dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \ && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \ && wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc" \ @@ -35,8 +42,9 @@ RUN echo '0 0 * * * sogo /usr/sbin/sogo-tool update-autoreply -p /etc/sogo/s COPY ./reconf-domains.sh / COPY supervisord.conf /etc/supervisor/supervisord.conf -EXPOSE 20000 -EXPOSE 9191 +#EXPOSE 20000 +#EXPOSE 9191 +#EXPOSE 9192 CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf diff --git a/data/Dockerfiles/sogo/reconf-domains.sh b/data/Dockerfiles/sogo/reconf-domains.sh index 8cc24052..9ea2f6dd 100755 --- a/data/Dockerfiles/sogo/reconf-domains.sh +++ b/data/Dockerfiles/sogo/reconf-domains.sh @@ -1,22 +1,20 @@ #!/bin/bash +# Wait for MySQL to warm-up +while mysqladmin ping --host mysql --silent; do + # Recreate view mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP VIEW IF EXISTS sogo_view" mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF -CREATE VIEW sogo_view (c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, senderacl, home) AS -SELECT mailbox.username, mailbox.domain, mailbox.username, mailbox.password, mailbox.name, mailbox.username, IFNULL(ga.aliases, ''), IFNULL(gda.ad_alias, ''), IFNULL(gs.send_as, ''), CONCAT('/var/vmail/', maildir) FROM mailbox +CREATE VIEW sogo_view (c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, home, kind, multiple_bookings) AS +SELECT mailbox.username, mailbox.domain, mailbox.username, mailbox.password, mailbox.name, mailbox.username, IFNULL(ga.aliases, ''), IFNULL(gda.ad_alias, ''), CONCAT('/var/vmail/', maildir), mailbox.kind, mailbox.multiple_bookings FROM mailbox LEFT OUTER JOIN grouped_mail_aliases ga ON ga.username = mailbox.username -LEFT OUTER JOIN grouped_sender_acl gs ON gs.username = mailbox.username LEFT OUTER JOIN grouped_domain_alias_address gda ON gda.username = mailbox.username WHERE mailbox.active = '1'; EOF -# Wait for MySQL to warm-up -while ! mysqladmin ping --host mysql --silent; do - sleep 1 -done mkdir -p /var/lib/sogo/GNUstep/Defaults/ @@ -32,8 +30,6 @@ cat < /var/lib/sogo/GNUstep/Defaults/sogod.plist mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_cache_folder OCSEMailAlarmsFolderURL mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_alarms_folder - DomainFieldName - domain OCSFolderInfoURL mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_folder_info OCSSessionsFolderURL @@ -51,11 +47,10 @@ EOF # Generate multi-domain setup while read line do - DOMAIN_SANE=$(echo ${line} | tr '-' 'b' | tr '.' 'p' | tr -cd '[[:alnum:]]') echo " ${line} SOGoMailDomain - ${DOMAIN_SANE} + ${line} SOGoUserSources @@ -63,14 +58,13 @@ while read line aliases ad_aliases - senderacl KindFieldName kind + DomainFieldName + domain MultipleBookingsFieldName multiple_bookings - IMAPLoginFieldName - c_uid canAuthenticate YES displayName @@ -99,4 +93,6 @@ echo ' chown sogo:sogo -R /var/lib/sogo/ chmod 600 /var/lib/sogo/GNUstep/Defaults/sogod.plist -sleep infinite +sleep 99999 + +done diff --git a/data/Dockerfiles/sogo/supervisord.conf b/data/Dockerfiles/sogo/supervisord.conf index b59ff11c..a2d32e76 100644 --- a/data/Dockerfiles/sogo/supervisord.conf +++ b/data/Dockerfiles/sogo/supervisord.conf @@ -7,9 +7,6 @@ redirect_stderr=true autostart=true stdout_syslog=true -[group:sogo-group] -programs=reconf-domains,sogo - [program:sogo] command=/usr/sbin/sogod user=sogo @@ -30,6 +27,12 @@ stdout_logfile_maxbytes=0 command=/usr/sbin/cron -f autorestart=true +[program:sogo-webres] +command=/usr/bin/python -u -m SimpleHTTPServer 9192 +directory=/usr/lib/GNUstep/SOGo/ +user=sogo +autorestart=true + [inet_http_server] port=9191 diff --git a/data/conf/bind9/named.conf b/data/conf/bind9/named.conf new file mode 100644 index 00000000..e3776155 --- /dev/null +++ b/data/conf/bind9/named.conf @@ -0,0 +1,20 @@ +acl internal_networks { + 127.0.0.0/8; + 192.168.0.0/16; + 172.16.0.0/12; + 10.0.0.0/8; +}; + +options { + directory "/var/bind"; + allow-recursion { internal_networks; }; + listen-on { any; }; + listen-on-v6 { none; }; + pid-file "/var/run/named/named.pid"; + allow-transfer { none; }; + dnssec-enable yes; + dnssec-validation yes; + dnssec-lookaside auto; +}; + +include "/etc/bind/bind.keys"; diff --git a/data/conf/dovecot/dovecot.conf b/data/conf/dovecot/dovecot.conf index 9b299847..1d05571e 100644 --- a/data/conf/dovecot/dovecot.conf +++ b/data/conf/dovecot/dovecot.conf @@ -1,6 +1,6 @@ auth_mechanisms = plain login #mail_debug = yes -log_path = /dev/stdout +log_path = /var/log/mail.log disable_plaintext_auth = yes # Uncomment on NFS share #mmap_disable = yes @@ -21,6 +21,7 @@ ssl_dh_parameters_length = 2048 log_timestamp = "%Y-%m-%d %H:%M:%S " recipient_delimiter = + auth_master_user_separator = * +mail_prefetch_count = 30 passdb { driver = passwd-file args = /etc/dovecot/dovecot-master.passwd @@ -130,6 +131,9 @@ namespace inbox { auto = subscribe special_use = \Junk } + mailbox "Junk-E-mail" { + special_use = \Junk + } mailbox "Junk E-mail" { special_use = \Junk } @@ -182,6 +186,9 @@ service managesieve-login { process_min_avail = 2 vsz_limit = 128M } +service imap { + executable = imap imap-postlogin +} service managesieve { process_limit = 256 } @@ -236,3 +243,8 @@ remote 127.0.0.1 { } submission_host = postfix:588 mail_max_userip_connections = 500 +service imap-postlogin { + executable = script-login /usr/local/bin/postlogin.sh + unix_listener imap-postlogin { + } +} diff --git a/data/conf/dovecot/sql/dovecot-dict-sql.conf b/data/conf/dovecot/sql/dovecot-dict-sql.conf deleted file mode 100644 index 0271326e..00000000 --- a/data/conf/dovecot/sql/dovecot-dict-sql.conf +++ /dev/null @@ -1,15 +0,0 @@ -connect = "host=mysql dbname=mailcow user=mailcow password=mysafepasswd" - -map { - pattern = priv/quota/storage - table = quota2 - username_field = username - value_field = bytes -} -map { - pattern = priv/quota/messages - table = quota2 - username_field = username - value_field = messages -} - diff --git a/data/conf/dovecot/sql/dovecot-mysql.conf b/data/conf/dovecot/sql/dovecot-mysql.conf deleted file mode 100644 index edf5ca6c..00000000 --- a/data/conf/dovecot/sql/dovecot-mysql.conf +++ /dev/null @@ -1,6 +0,0 @@ -driver = mysql -connect = "host=mysql dbname=mailcow user=mailcow password=mysafepasswd" -default_pass_scheme = SSHA256 -password_query = SELECT password FROM mailbox WHERE username = '%u' AND domain IN (SELECT domain FROM domain WHERE domain='%d' AND active='1') -user_query = SELECT CONCAT('maildir:/var/vmail/',maildir) AS mail, 5000 AS uid, 5000 AS gid, concat('*:bytes=', quota) AS quota_rule FROM mailbox WHERE username = '%u' AND active = '1' -iterate_query = SELECT username FROM mailbox WHERE active='1'; diff --git a/data/conf/nginx/server_name.active b/data/conf/nginx/server_name.active new file mode 100644 index 00000000..ce429200 --- /dev/null +++ b/data/conf/nginx/server_name.active @@ -0,0 +1 @@ +server_name logs.servercow.de autodiscover.* autoconfig.*; diff --git a/data/conf/nginx/site.conf b/data/conf/nginx/site.conf index 0ea01dfe..d724b4f2 100644 --- a/data/conf/nginx/site.conf +++ b/data/conf/nginx/site.conf @@ -1,5 +1,9 @@ +proxy_cache_path /tmp levels=1:2 keys_zone=sogo:10m inactive=24h max_size=1g; server { - listen 443; + include /etc/nginx/conf.d/listen_ssl.active; + include /etc/nginx/mime.types; + charset utf-8; + override_charset on; ssl on; ssl_certificate /etc/ssl/mail/cert.pem; ssl_certificate_key /etc/ssl/mail/key.pem; @@ -9,13 +13,23 @@ server { add_header Strict-Transport-Security "max-age=15768000; includeSubDomains"; ssl_ecdh_curve secp384r1; index index.php index.html; - server_name _ autodiscover.* autoconfig.*; + include /etc/nginx/conf.d/server_name.active; error_log /var/log/nginx/error.log; access_log /var/log/nginx/access.log; root /web; + location ^~ /.well-known/acme-challenge/ { + allow all; + default_type "text/plain"; + } + + # If behind reverse proxy, forwards the correct IP + set_real_ip_from 172.22.1.1; + real_ip_header X-Forwarded-For; + real_ip_recursive on; + location = /principals/ { - rewrite ^ https://$host/SOGo/dav; + rewrite ^ $scheme://$host:$server_port/SOGo/dav; allow all; } @@ -24,7 +38,7 @@ server { fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass phpfpm:9000; fastcgi_index index.php; - include fastcgi_params; + include /etc/nginx/fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; fastcgi_param PHP_VALUE "max_execution_time = 1200 @@ -35,9 +49,10 @@ server { rewrite ^(/save.+)$ /rspamd$1 last; location /rspamd/ { - proxy_pass http://rspamd:11334/; + proxy_pass http://172.22.1.253:11334/; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; add_header Strict-Transport-Security "max-age=31536000; includeSubdomains"; add_header X-Content-Type-Options nosniff; add_header X-Frame-Options SAMEORIGIN; @@ -48,16 +63,24 @@ server { deny all; } - if ($host ~* autodiscover\.(.*)) { - rewrite ^(.*) /autodiscover.php last; + location ~ /(?:a|A)utodiscover/(?:a|A)utodiscover.xml { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass phpfpm:9000; + include /etc/nginx/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + try_files /autodiscover.php =404; } - if ($host ~* autoconfig\.(.*)) { - rewrite ^(.*) /autoconfig.php last; + location ~ /(?:m|M)ail/(?:c|C)onfig-v1.1.xml { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass phpfpm:9000; + include /etc/nginx/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + try_files /autoconfig.php =404; } location ^~ /Microsoft-Server-ActiveSync { - proxy_pass http://sogo:20000/SOGo/Microsoft-Server-ActiveSync; + proxy_pass http://172.22.1.252:20000/SOGo/Microsoft-Server-ActiveSync; proxy_connect_timeout 1000; proxy_next_upstream timeout error; proxy_send_timeout 1000; @@ -72,45 +95,212 @@ server { proxy_set_header x-webobjects-server-protocol HTTP/1.0; proxy_set_header x-webobjects-remote-host $remote_addr; proxy_set_header x-webobjects-server-name $server_name; - proxy_set_header x-webobjects-server-url $scheme://$host; + proxy_set_header x-webobjects-server-url $scheme://$host:$server_port; proxy_set_header x-webobjects-server-port $server_port; client_body_buffer_size 128k; client_max_body_size 100m; } location ^~ /SOGo { - proxy_pass http://sogo:20000; + proxy_pass http://172.22.1.252:20000; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_set_header x-webobjects-server-protocol HTTP/1.0; proxy_set_header x-webobjects-remote-host $remote_addr; proxy_set_header x-webobjects-server-name $server_name; - proxy_set_header x-webobjects-server-url $scheme://$host; + proxy_set_header x-webobjects-server-url $scheme://$host:$server_port; proxy_set_header x-webobjects-server-port $server_port; - #proxy_connect_timeout 90; - #proxy_send_timeout 90; - #proxy_read_timeout 90; - #proxy_buffer_size 4k; - #proxy_buffers 4 32k; - #proxy_busy_buffers_size 64k; - #proxy_temp_file_write_size 64k; client_body_buffer_size 128k; client_max_body_size 100m; break; } location /SOGo.woa/WebServerResources/ { - alias /usr/lib/GNUstep/SOGo/WebServerResources/; + proxy_pass http://172.22.1.252:9192/WebServerResources/; + proxy_set_header Host $host; + proxy_cache sogo; + proxy_cache_valid 200 1d; + proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; + #alias /usr/lib/GNUstep/SOGo/WebServerResources/; + allow all; + } + + location /.woa/WebServerResources/ { + proxy_pass http://172.22.1.252:9192/WebServerResources/; + proxy_set_header Host $host; + proxy_cache sogo; + proxy_cache_valid 200 1d; + proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; + #alias /usr/lib/GNUstep/SOGo/WebServerResources/; allow all; } location /SOGo/WebServerResources/ { - alias /usr/lib/GNUstep/SOGo/WebServerResources/; + proxy_pass http://172.22.1.252:9192/WebServerResources/; + proxy_set_header Host $host; + proxy_cache sogo; + proxy_cache_valid 200 1d; + proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; + #alias /usr/lib/GNUstep/SOGo/WebServerResources/; allow all; } location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$ { - alias /usr/lib/GNUstep/SOGo/$1.SOGo/Resources/$2; + proxy_pass http://172.22.1.252:9192/$1.SOGo/Resources/$2; + proxy_set_header Host $host; + proxy_cache sogo; + proxy_cache_valid 200 1d; + proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; + #alias /usr/lib/GNUstep/SOGo/$1.SOGo/Resources/$2; } } +server { + include /etc/nginx/conf.d/listen_plain.active; + include /etc/nginx/mime.types; + charset utf-8; + override_charset on; + index index.php index.html; + include /etc/nginx/conf.d/server_name.active; + error_log /var/log/nginx/error.log; + access_log /var/log/nginx/access.log; + root /web; + + location ^~ /.well-known/acme-challenge/ { + allow all; + default_type "text/plain"; + } + + # If behind reverse proxy, forwards the correct IP + set_real_ip_from 172.22.1.1; + real_ip_header X-Forwarded-For; + real_ip_recursive on; + + location = /principals/ { + rewrite ^ $scheme://$host:$server_port/SOGo/dav; + allow all; + } + + location ~ \.php$ { + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass phpfpm:9000; + fastcgi_index index.php; + include /etc/nginx/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_param PHP_VALUE "max_execution_time = 1200 + max_input_time = 1200 + memory_limit = 64M"; + fastcgi_read_timeout 1200; + } + + rewrite ^(/save.+)$ /rspamd$1 last; + location /rspamd/ { + proxy_pass http://172.22.1.253:11334/; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + add_header Strict-Transport-Security "max-age=31536000; includeSubdomains"; + add_header X-Content-Type-Options nosniff; + add_header X-Frame-Options SAMEORIGIN; + add_header X-XSS-Protection "1; mode=block"; + } + + location ^~ /inc/init.sql { + deny all; + } + + location ~ /(?:a|A)utodiscover/(?:a|A)utodiscover.xml { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass phpfpm:9000; + include /etc/nginx/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + try_files /autodiscover.php =404; + } + + location ~ /(?:m|M)ail/(?:c|C)onfig-v1.1.xml { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass phpfpm:9000; + include /etc/nginx/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + try_files /autoconfig.php =404; + } + + location ^~ /Microsoft-Server-ActiveSync { + proxy_pass http://172.22.1.252:20000/SOGo/Microsoft-Server-ActiveSync; + proxy_connect_timeout 1000; + proxy_next_upstream timeout error; + proxy_send_timeout 1000; + proxy_read_timeout 1000; + proxy_buffer_size 8k; + proxy_buffers 4 32k; + proxy_temp_file_write_size 64k; + proxy_busy_buffers_size 64k; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_set_header x-webobjects-server-protocol HTTP/1.0; + proxy_set_header x-webobjects-remote-host $remote_addr; + proxy_set_header x-webobjects-server-name $server_name; + proxy_set_header x-webobjects-server-url $scheme://$host:$server_port; + proxy_set_header x-webobjects-server-port $server_port; + client_body_buffer_size 128k; + client_max_body_size 100m; + } + + location ^~ /SOGo { + proxy_pass http://172.22.1.252:20000; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_set_header x-webobjects-server-protocol HTTP/1.0; + proxy_set_header x-webobjects-remote-host $remote_addr; + proxy_set_header x-webobjects-server-name $server_name; + proxy_set_header x-webobjects-server-url $scheme://$host:$server_port; + proxy_set_header x-webobjects-server-port $server_port; + client_body_buffer_size 128k; + client_max_body_size 100m; + break; + } + + location /SOGo.woa/WebServerResources/ { + proxy_pass http://172.22.1.252:9192/WebServerResources/; + proxy_set_header Host $host; + proxy_cache sogo; + proxy_cache_valid 200 1d; + proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; + #alias /usr/lib/GNUstep/SOGo/WebServerResources/; + allow all; + } + + location /.woa/WebServerResources/ { + proxy_pass http://172.22.1.252:9192/WebServerResources/; + proxy_set_header Host $host; + proxy_cache sogo; + proxy_cache_valid 200 1d; + proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; + #alias /usr/lib/GNUstep/SOGo/WebServerResources/; + allow all; + } + + location /SOGo/WebServerResources/ { + proxy_pass http://172.22.1.252:9192/WebServerResources/; + proxy_set_header Host $host; + proxy_cache sogo; + proxy_cache_valid 200 1d; + proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; + #alias /usr/lib/GNUstep/SOGo/WebServerResources/; + allow all; + } + + location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$ { + proxy_pass http://172.22.1.252:9192/$1.SOGo/Resources/$2; + proxy_set_header Host $host; + proxy_cache sogo; + proxy_cache_valid 200 1d; + proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; + #alias /usr/lib/GNUstep/SOGo/$1.SOGo/Resources/$2; + } + +} diff --git a/data/conf/nginx/templates/listen_plain.template b/data/conf/nginx/templates/listen_plain.template new file mode 100644 index 00000000..68133480 --- /dev/null +++ b/data/conf/nginx/templates/listen_plain.template @@ -0,0 +1 @@ +listen ${HTTP_PORT}; diff --git a/data/conf/nginx/templates/listen_ssl.template b/data/conf/nginx/templates/listen_ssl.template new file mode 100644 index 00000000..e454e832 --- /dev/null +++ b/data/conf/nginx/templates/listen_ssl.template @@ -0,0 +1 @@ +listen ${HTTPS_PORT}; diff --git a/data/conf/nginx/templates/server_name.template b/data/conf/nginx/templates/server_name.template new file mode 100644 index 00000000..261a1ece --- /dev/null +++ b/data/conf/nginx/templates/server_name.template @@ -0,0 +1 @@ +server_name ${MAILCOW_HOSTNAME} autodiscover.* autoconfig.*; diff --git a/data/conf/pdns/pdns_custom.lua b/data/conf/pdns/pdns_custom.lua deleted file mode 100644 index 18588bf2..00000000 --- a/data/conf/pdns/pdns_custom.lua +++ /dev/null @@ -1 +0,0 @@ -addNTA("mailcow-network", "nta for local") diff --git a/data/conf/pdns/recursor.conf b/data/conf/pdns/recursor.conf deleted file mode 100644 index f3270b5b..00000000 --- a/data/conf/pdns/recursor.conf +++ /dev/null @@ -1,41 +0,0 @@ -allow-from=127.0.0.0/8, 192.168.0.0/16, 172.16.0.0/12, 10.0.0.0/8 -config-dir=/etc/powerdns -daemon=no -disable-syslog=yes -dnssec=process -dnssec-log-bogus=yes -dont-query=10.0.0.0/8, 100.64.0.0/10, 169.254.0.0/16, 192.168.0.0/16, 172.16.0.0/12, ::1/128, fc00::/7, fe80::/10, 0.0.0.0/8, 192.0.0.0/24, 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24, 240.0.0.0/4, ::/96, ::ffff:0:0/96, 100::/64, 2001:db8::/32 -export-etc-hosts=off -# forward-zones= -forward-zones-recurse=mailcow-network.=127.0.0.11 -local-address=0.0.0.0 -local-port=53 -loglevel=6 -# lowercase-outgoing=no -lua-config-file=/etc/powerdns/pdns_custom.lua -# max-cache-entries=1000000 -# max-cache-ttl=86400 -# max-mthreads=2048 -# max-negative-ttl=3600 -# max-packetcache-entries=500000 -# max-qperq=50 -# max-tcp-clients=128 -# max-tcp-per-client=0 -# max-total-msec=7000 -# minimum-ttl-override=0 -# network-timeout=1500 -# packetcache-servfail-ttl=60 -# packetcache-ttl=3600 -quiet=yes -# security-poll-suffix=secpoll.powerdns.com. -# serve-rfc1918=yes -# server-down-max-fails=64 -# server-down-throttle-time=60 -setgid=pdns -setuid=pdns -# spoof-nearmiss-max=20 -# stack-size=200000 -# threads=2 -# trace=off -version-string=PowerDNS Recursor -webserver=no diff --git a/data/conf/postfix/main.cf b/data/conf/postfix/main.cf index 4f365677..cbfc4e0c 100644 --- a/data/conf/postfix/main.cf +++ b/data/conf/postfix/main.cf @@ -56,7 +56,7 @@ smtpd_error_sleep_time = 10s smtpd_hard_error_limit = ${stress?1}${stress:5} smtpd_helo_required = yes smtpd_proxy_timeout = 600s -smtpd_recipient_restrictions = check_recipient_access proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf, permit_sasl_authenticated, permit_mynetworks, reject_invalid_helo_hostname, reject_unknown_reverse_client_hostname, reject_unauth_destination +smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, check_recipient_access proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf, reject_invalid_helo_hostname, reject_unknown_reverse_client_hostname, reject_unauth_destination smtpd_sasl_auth_enable = yes smtpd_sasl_authenticated_header = yes smtpd_sasl_path = inet:dovecot:10001 @@ -83,10 +83,11 @@ virtual_alias_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_maps. virtual_gid_maps = static:5000 virtual_mailbox_base = /var/vmail/ virtual_mailbox_domains = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_domains_maps.cf -virtual_mailbox_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_mailbox_maps.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_domain_mailbox_maps.cf +virtual_mailbox_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_mailbox_maps.cf virtual_minimum_uid = 104 virtual_transport = lmtp:inet:dovecot:24 virtual_uid_maps = static:5000 smtpd_milters = inet:rmilter:9900 non_smtpd_milters = inet:rmilter:9900 milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen} +mydestination = localhost.localdomain, localhost diff --git a/data/conf/postfix/sql/mysql_relay_recipient_maps.cf b/data/conf/postfix/sql/mysql_relay_recipient_maps.cf deleted file mode 100644 index 9eb7b159..00000000 --- a/data/conf/postfix/sql/mysql_relay_recipient_maps.cf +++ /dev/null @@ -1,5 +0,0 @@ -user = mailcow -password = mysafepasswd -hosts = mysql -dbname = mailcow -query = SELECT DISTINCT CASE WHEN '%d' IN (SELECT domain FROM domain WHERE relay_all_recipients=1 AND domain='%d' AND backupmx=1) THEN '%s' ELSE (SELECT goto FROM alias WHERE address='%s' AND active='1') END AS result; diff --git a/data/conf/postfix/sql/mysql_tls_enforce_in_policy.cf b/data/conf/postfix/sql/mysql_tls_enforce_in_policy.cf deleted file mode 100644 index de40b580..00000000 --- a/data/conf/postfix/sql/mysql_tls_enforce_in_policy.cf +++ /dev/null @@ -1,5 +0,0 @@ -user = mailcow -password = mysafepasswd -hosts = mysql -dbname = mailcow -query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.address WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_in = '1' AND mailbox.active = '1'), 'reject_plaintext_session', 'DUNNO') AS 'tls_enforce_in'; diff --git a/data/conf/postfix/sql/mysql_tls_enforce_out_policy.cf b/data/conf/postfix/sql/mysql_tls_enforce_out_policy.cf deleted file mode 100644 index 34d61331..00000000 --- a/data/conf/postfix/sql/mysql_tls_enforce_out_policy.cf +++ /dev/null @@ -1,5 +0,0 @@ -user = mailcow -password = mysafepasswd -hosts = mysql -dbname = mailcow -query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.address WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_out = '1' AND mailbox.active = '1'), 'smtp_enforced_tls:', 'DUNNO') AS 'tls_enforce_out'; diff --git a/data/conf/postfix/sql/mysql_virtual_alias_domain_catchall_maps.cf b/data/conf/postfix/sql/mysql_virtual_alias_domain_catchall_maps.cf deleted file mode 100644 index 484a0eac..00000000 --- a/data/conf/postfix/sql/mysql_virtual_alias_domain_catchall_maps.cf +++ /dev/null @@ -1,6 +0,0 @@ -user = mailcow -password = mysafepasswd -hosts = mysql -dbname = mailcow -query = SELECT goto FROM alias,alias_domain WHERE alias_domain.alias_domain = '%d' and alias.address = CONCAT('@', alias_domain.target_domain) AND alias.active = 1 AND alias_domain.active='1' - diff --git a/data/conf/postfix/sql/mysql_virtual_alias_domain_mailbox_maps.cf b/data/conf/postfix/sql/mysql_virtual_alias_domain_mailbox_maps.cf deleted file mode 100644 index 03061109..00000000 --- a/data/conf/postfix/sql/mysql_virtual_alias_domain_mailbox_maps.cf +++ /dev/null @@ -1,5 +0,0 @@ -user = mailcow -password = mysafepasswd -hosts = mysql -dbname = mailcow -query = SELECT maildir FROM mailbox,alias_domain WHERE alias_domain.alias_domain = '%d' and mailbox.username = CONCAT('%u', '@', alias_domain.target_domain) AND mailbox.active = 1 AND alias_domain.active='1' diff --git a/data/conf/postfix/sql/mysql_virtual_alias_domain_maps.cf b/data/conf/postfix/sql/mysql_virtual_alias_domain_maps.cf deleted file mode 100644 index 2718bb00..00000000 --- a/data/conf/postfix/sql/mysql_virtual_alias_domain_maps.cf +++ /dev/null @@ -1,5 +0,0 @@ -user = mailcow -password = mysafepasswd -hosts = mysql -dbname = mailcow -query = SELECT goto FROM alias,alias_domain WHERE alias_domain.alias_domain = '%d' and alias.address = CONCAT('%u', '@', alias_domain.target_domain) AND alias.active = 1 AND alias_domain.active='1' diff --git a/data/conf/postfix/sql/mysql_virtual_alias_maps.cf b/data/conf/postfix/sql/mysql_virtual_alias_maps.cf deleted file mode 100644 index a72c8bd8..00000000 --- a/data/conf/postfix/sql/mysql_virtual_alias_maps.cf +++ /dev/null @@ -1,5 +0,0 @@ -user = mailcow -password = mysafepasswd -hosts = mysql -dbname = mailcow -query = SELECT goto FROM alias WHERE address='%s' AND active='1'; diff --git a/data/conf/postfix/sql/mysql_virtual_domains_maps.cf b/data/conf/postfix/sql/mysql_virtual_domains_maps.cf deleted file mode 100644 index 22e00938..00000000 --- a/data/conf/postfix/sql/mysql_virtual_domains_maps.cf +++ /dev/null @@ -1,5 +0,0 @@ -user = mailcow -password = mysafepasswd -hosts = mysql -dbname = mailcow -query = SELECT alias_domain from alias_domain WHERE alias_domain='%s' AND active='1' UNION SELECT domain FROM domain WHERE domain='%s' AND active = '1' AND backupmx = '0' diff --git a/data/conf/postfix/sql/mysql_virtual_mailbox_maps.cf b/data/conf/postfix/sql/mysql_virtual_mailbox_maps.cf deleted file mode 100644 index bf07cdb2..00000000 --- a/data/conf/postfix/sql/mysql_virtual_mailbox_maps.cf +++ /dev/null @@ -1,5 +0,0 @@ -user = mailcow -password = mysafepasswd -hosts = mysql -dbname = mailcow -query = SELECT maildir FROM mailbox WHERE username='%s' AND active = '1' diff --git a/data/conf/postfix/sql/mysql_virtual_relay_domain_maps.cf b/data/conf/postfix/sql/mysql_virtual_relay_domain_maps.cf deleted file mode 100644 index 6994d02d..00000000 --- a/data/conf/postfix/sql/mysql_virtual_relay_domain_maps.cf +++ /dev/null @@ -1,5 +0,0 @@ -user = mailcow -password = mysafepasswd -hosts = mysql -dbname = mailcow -query = SELECT domain FROM domain WHERE domain='%s' AND backupmx = '1' AND active = '1' diff --git a/data/conf/postfix/sql/mysql_virtual_sender_acl.cf b/data/conf/postfix/sql/mysql_virtual_sender_acl.cf deleted file mode 100644 index 3707a2b2..00000000 --- a/data/conf/postfix/sql/mysql_virtual_sender_acl.cf +++ /dev/null @@ -1,5 +0,0 @@ -user = mailcow -password = mysafepasswd -hosts = mysql -dbname = mailcow -query = SELECT goto FROM alias WHERE address='%s' AND active='1' AND domain IN (SELECT domain FROM domain WHERE domain='%d' AND active='1') UNION SELECT logged_in_as FROM sender_acl WHERE send_as='@%d' OR send_as='%s' AND logged_in_as NOT IN (SELECT goto FROM alias WHERE address='%s') UNION SELECT goto FROM alias,alias_domain WHERE alias_domain.alias_domain = '%d' AND alias.address = CONCAT('%u', '@', alias_domain.target_domain) AND alias.active ='1' AND alias_domain.active='1' diff --git a/data/conf/postfix/sql/mysql_virtual_spamalias_maps.cf b/data/conf/postfix/sql/mysql_virtual_spamalias_maps.cf deleted file mode 100644 index ac8d78ac..00000000 --- a/data/conf/postfix/sql/mysql_virtual_spamalias_maps.cf +++ /dev/null @@ -1,5 +0,0 @@ -user = mailcow -password = mysafepasswd -hosts = mysql -dbname = mailcow -query = SELECT goto FROM spamalias WHERE address='%s' AND validity >= UNIX_TIMESTAMP() diff --git a/data/conf/rspamd/dynmaps/authoritative.php b/data/conf/rspamd/dynmaps/authoritative.php index b2c101f7..ffbfacf6 100644 --- a/data/conf/rspamd/dynmaps/authoritative.php +++ b/data/conf/rspamd/dynmaps/authoritative.php @@ -1,22 +1,34 @@ PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]; -$pdo = new PDO($dsn, $database_user, $database_pass, $opt); -$stmt = $pdo->query("SELECT `domain` FROM `domain`"); -$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); -while ($row = array_shift($rows)) { - echo strtolower(trim($row['domain'])) . PHP_EOL; +try { + $pdo = new PDO($dsn, $database_user, $database_pass, $opt); + $stmt = $pdo->query("SELECT `domain` FROM `domain`"); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while ($row = array_shift($rows)) { + $has_object = 1; + echo strtolower(trim($row['domain'])) . PHP_EOL; + } + $stmt = $pdo->query("SELECT `alias_domain` FROM `alias_domain`"); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while ($row = array_shift($rows)) { + $has_object = 1; + echo strtolower(trim($row['alias_domain'])) . PHP_EOL; + } + if ($has_object == 0) { + echo "dummy@domain.local"; + } } -$stmt = $pdo->query("SELECT `alias_domain` FROM `alias_domain`"); -$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); -while ($row = array_shift($rows)) { - echo strtolower(trim($row['alias_domain'])) . PHP_EOL; +catch (PDOException $e) { + echo "dummy@domain.local"; + exit; } ?> \ No newline at end of file diff --git a/data/conf/rspamd/dynmaps/settings.php b/data/conf/rspamd/dynmaps/settings.php index 1288b642..9be1f696 100644 --- a/data/conf/rspamd/dynmaps/settings.php +++ b/data/conf/rspamd/dynmaps/settings.php @@ -1,19 +1,34 @@ substr($email, 0, $a), 'domain' => substr($email, $a)); +} header('Content-Type: text/plain'); require_once "vars.inc.php"; + +ini_set('error_reporting', 0); + $dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name; $opt = [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]; -$pdo = new PDO($dsn, $database_user, $database_pass, $opt); +try { + $pdo = new PDO($dsn, $database_user, $database_pass, $opt); + $stmt = $pdo->query("SELECT * FROM `filterconf`"); +} +catch (PDOException $e) { + echo 'settings { }'; + exit; +} + ?> settings { fetchAll(PDO::FETCH_COLUMN|PDO::FETCH_GROUP); $stmt = $pdo->prepare("SELECT GROUP_CONCAT(REPLACE(`value`, '*', '.*') SEPARATOR '|') AS `value` FROM `filterconf` - WHERE `object`= :object + WHERE (`object`= :object OR `object`= :object_domain) AND (`option` = 'blacklist_from' OR `option` = 'whitelist_from')"); - $stmt->execute(array(':object' => $row['object'])); - $grouped_lists = $stmt->fetchAll(PDO::FETCH_COLUMN); - $value_sane = preg_replace("/\.\./", ".", (preg_replace("/\*/", ".*", $grouped_lists[0]))); + $stmt->execute(array(':object' => $row['object'], ':object_domain' => substr(strrchr($row['object'], "@"), 1))); + $grouped_lists = $stmt->fetchAll(PDO::FETCH_ASSOC); + array_filter($grouped_lists); + while ($grouped_list = array_shift($grouped_lists)) { + $value_sane = preg_replace("/\.\./", ".", (preg_replace("/\*/", ".*", $grouped_list['value']))); + if (!empty($value_sane)) { ?> from = "/^((?!).)*$/"; + + rcpt = "/\+.*/"; + rcpt = ""; prepare("SELECT `address` FROM `alias` WHERE `goto` = :object_goto AND `address` NOT LIKE '@%' AND `address` != :object_address"); - $stmt->execute(array(':object_goto' => $row['object'], ':object_address' => $row['object'])); + $stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `goto` LIKE :object_goto AND `address` NOT LIKE '@%' AND `address` != :object_address"); + $stmt->execute(array(':object_goto' => '%' . $row['object'] . '%', ':object_address' => $row['object'])); $rows_aliases_1 = $stmt->fetchAll(PDO::FETCH_ASSOC); while ($row_aliases_1 = array_shift($rows_aliases_1)) { + $local = parse_email($row_aliases_1['address'])['local']; + $domain = parse_email($row_aliases_1['address'])['domain']; + if (!empty($local) && !empty($local)) { +?> + rcpt = "/\+.*/"; + rcpt = ""; execute(array(':object' => $row['object'])); $rows_aliases_2 = $stmt->fetchAll(PDO::FETCH_ASSOC); - while ($row_aliases_2 = array_filter(array_shift($rows_aliases_2))) { + array_filter($rows_aliases_2); + while ($row_aliases_2 = array_shift($rows_aliases_2)) { + if (!empty($row_aliases_2['aliases'])) { + $local = parse_email($row_aliases_2['aliases'])['local']; + $domain = parse_email($row_aliases_2['aliases'])['domain']; + if (!empty($local) && !empty($local)) { +?> + rcpt = "/\+.*/"; + rcpt = ""; apply "default" { @@ -68,6 +114,7 @@ while ($row = array_shift($rows)) { "add header" = ; } } + } whitelist_ { - priority = high; prepare("SELECT GROUP_CONCAT(REPLACE(`value`, '*', '.*') SEPARATOR '|') AS `value` FROM `filterconf` WHERE `object`= :object @@ -94,27 +140,47 @@ while ($row = array_shift($rows)) { - rcpt = "/.*@/"; + priority = medium; + rcpt = "/.*@/"; prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` = :object"); $stmt->execute(array(':object' => $row['object'])); $rows_domain_aliases = $stmt->fetchAll(PDO::FETCH_ASSOC); + array_filter($rows_domain_aliases); while ($row_domain_aliases = array_shift($rows_domain_aliases)) { ?> - rcpt = ""; + rcpt = "/.*@/"; + priority = high; + + rcpt = "/\+.*/"; + rcpt = ""; prepare("SELECT `address` FROM `alias` WHERE `goto` = :object_goto AND `address` NOT LIKE '@%' AND `address` != :object_address"); - $stmt->execute(array(':object_goto' => $row['object'], ':object_address' => $row['object'])); + $stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `goto` LIKE :object_goto AND `address` NOT LIKE '@%' AND `address` != :object_address"); + $stmt->execute(array(':object_goto' => '%' . $row['object'] . '%', ':object_address' => $row['object'])); $rows_aliases_wl_1 = $stmt->fetchAll(PDO::FETCH_ASSOC); + array_filter($rows_aliases_wl_1); while ($row_aliases_wl_1 = array_shift($rows_aliases_wl_1)) { + $local = parse_email($row_aliases_wl_1['address'])['local']; + $domain = parse_email($row_aliases_wl_1['address'])['domain']; + if (!empty($local) && !empty($local)) { +?> + rcpt = "/\+.*/"; + rcpt = ""; execute(array(':object' => $row['object'])); $rows_aliases_wl_2 = $stmt->fetchAll(PDO::FETCH_ASSOC); - while ($row_aliases_wl_2 = array_filter(array_shift($rows_aliases_wl_2))) { + array_filter($rows_aliases_wl_2); + while ($row_aliases_wl_2 = array_shift($rows_aliases_wl_2)) { + if (!empty($row_aliases_wl_2['aliases'])) { + $local = parse_email($row_aliases_wl_2['aliases'])['local']; + $domain = parse_email($row_aliases_wl_2['aliases'])['domain']; + if (!empty($local) && !empty($local)) { +?> + rcpt = "/\+.*/"; + rcpt = ""; apply "default" { @@ -147,7 +223,6 @@ while ($row = array_shift($rows)) { $username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['object']); ?> blacklist_ { - priority = medium; prepare("SELECT GROUP_CONCAT(REPLACE(`value`, '*', '.*') SEPARATOR '|') AS `value` FROM `filterconf` WHERE `object`= :object @@ -160,40 +235,70 @@ while ($row = array_shift($rows)) { - rcpt = "/.*@/"; + priority = medium; + rcpt = "/.*@/"; prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` = :object"); $stmt->execute(array(':object' => $row['object'])); $rows_domain_aliases = $stmt->fetchAll(PDO::FETCH_ASSOC); + array_filter($rows_domain_aliases); while ($row_domain_aliases = array_shift($rows_domain_aliases)) { ?> - rcpt = ""; + rcpt = "/.*@/"; + priority = high; + + rcpt = "/\+.*/"; + rcpt = ""; prepare("SELECT `address` FROM `alias` WHERE `goto` = :object_goto AND `address` NOT LIKE '@%' AND `address` != :object_address"); - $stmt->execute(array(':object_goto' => $row['object'], ':object_address' => $row['object'])); - $rows_aliases_wl_1 = $stmt->fetchAll(PDO::FETCH_ASSOC); - while ($row_aliases_wl_1 = array_shift($rows_aliases_wl_1)) { + $stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `goto` LIKE :object_goto AND `address` NOT LIKE '@%' AND `address` != :object_address"); + $stmt->execute(array(':object_goto' => '%' . $row['object'] . '%', ':object_address' => $row['object'])); + $rows_aliases_bl_1 = $stmt->fetchAll(PDO::FETCH_ASSOC); + array_filter($rows_aliases_bl_1); + while ($row_aliases_bl_1 = array_shift($rows_aliases_bl_1)) { + $local = parse_email($row_aliases_bl_1['address'])['local']; + $domain = parse_email($row_aliases_bl_1['address'])['domain']; + if (!empty($local) && !empty($local)) { ?> - rcpt = ""; + rcpt = "/\+.*/"; + + rcpt = ""; prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `aliases` FROM `mailbox` LEFT OUTER JOIN `alias_domain` on `mailbox`.`domain` = `alias_domain`.`target_domain` WHERE `mailbox`.`username` = :object"); $stmt->execute(array(':object' => $row['object'])); - $rows_aliases_wl_2 = $stmt->fetchAll(PDO::FETCH_ASSOC); - while ($row_aliases_wl_2 = array_filter(array_shift($rows_aliases_wl_2))) { + $rows_aliases_bl_2 = $stmt->fetchAll(PDO::FETCH_ASSOC); + array_filter($rows_aliases_bl_2); + while ($row_aliases_bl_2 = array_shift($rows_aliases_bl_2)) { + if (!empty($row_aliases_bl_2['aliases'])) { + $local = parse_email($row_aliases_bl_2['aliases'])['local']; + $domain = parse_email($row_aliases_bl_2['aliases'])['domain']; + if (!empty($local) && !empty($local)) { ?> - rcpt = ""; + rcpt = "/\+.*/"; + rcpt = ""; + apply "default" { @@ -203,4 +308,4 @@ while ($row = array_shift($rows)) { -} +} \ No newline at end of file diff --git a/data/conf/rspamd/dynmaps/tags.php b/data/conf/rspamd/dynmaps/tags.php index cc6435fe..7552575c 100644 --- a/data/conf/rspamd/dynmaps/tags.php +++ b/data/conf/rspamd/dynmaps/tags.php @@ -1,17 +1,41 @@ PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]; -$pdo = new PDO($dsn, $database_user, $database_pass, $opt); -$stmt = $pdo->query("SELECT `username` FROM `mailbox` WHERE `wants_tagged_subject` = '1'"); -$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); -while ($row = array_shift($rows)) { - echo strtolower(trim($row['username'])) . PHP_EOL; +try { + $pdo = new PDO($dsn, $database_user, $database_pass, $opt); + $stmt = $pdo->query("SELECT `username` FROM `mailbox` WHERE `wants_tagged_subject` = '1'"); + $rows_a = $stmt->fetchAll(PDO::FETCH_ASSOC); + while ($row_a = array_shift($rows_a)) { + $stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `goto` REGEXP :username AND goto != `address` AND `address` NOT LIKE '@%'"); + $stmt->execute(array(':username' => '(^|,)'.$row_a['username'].'($|,)')); + $rows_a_a = $stmt->fetchAll(PDO::FETCH_ASSOC); + while ($row_a_a = array_shift($rows_a_a)) { + echo strtolower(trim($row_a_a['address'])) . PHP_EOL; + } + $has_object = 1; + echo strtolower(trim($row_a['username'])) . PHP_EOL; + } + $stmt = $pdo->query("SELECT CONCAT(`mailbox`.`local_part`, '@', `alias_domain`.`alias_domain`) AS `tag_ad` FROM `mailbox` + INNER JOIN `alias_domain` ON `mailbox`.`domain` = `alias_domain`.`target_domain` WHERE `mailbox`.`wants_tagged_subject` = '1';"); + $rows_b = $stmt->fetchAll(PDO::FETCH_ASSOC); + while ($row_b = array_shift($rows_b)) { + $has_object = 1; + echo strtolower(trim($row_b['tag_ad'])) . PHP_EOL; + } + if ($has_object == 0) { + echo "dummy@domain.local"; + } } -?> \ No newline at end of file +catch (PDOException $e) { + echo "dummy@domain.local"; + exit; +} +?> diff --git a/data/conf/rspamd/lua/rspamd.local.lua b/data/conf/rspamd/lua/rspamd.local.lua index 7e254d3b..236734f2 100644 --- a/data/conf/rspamd/lua/rspamd.local.lua +++ b/data/conf/rspamd/lua/rspamd.local.lua @@ -11,42 +11,51 @@ rspamd_config.MAILCOW_MOO = function (task) return true end -local modify_subject_map = rspamd_config:add_map({ - url = 'http://nginx:8081/tags.php', +modify_subject_map = rspamd_config:add_map({ + url = 'http://172.22.1.251:8081/tags.php', type = 'map', description = 'Map of users to use subject tags for' }) -local auth_domain_map = rspamd_config:add_map({ - url = 'http://nginx:8081/authoritative.php', +auth_domain_map = rspamd_config:add_map({ + url = 'http://172.22.1.251:8081/authoritative.php', type = 'map', description = 'Map of domains we are authoritative for' }) -rspamd_config.ADD_DELIMITER_TAG = { - callback = function(task) - local util = require("rspamd_util") - local rspamd_logger = require "rspamd_logger" - local user_tagged = task:get_recipients(1)[1]['user'] - local domain = task:get_recipients(1)[1]['domain'] - local user, tag = user_tagged:match("([^+]+)+(.*)") - local authdomain = auth_domain_map:get_key(domain) +rspamd_config:register_post_filter(function(task) + local util = require("rspamd_util") + local rspamd_logger = require "rspamd_logger" - if tag and authdomain then - rspamd_logger.infox("Domain %s is part of mailcow, start reading tag settings", domain) - local user_untagged = user .. '@' .. domain - rspamd_logger.infox("Querying tag settings for user %1", user_untagged) - if modify_subject_map:get_key(user_untagged) then - rspamd_logger.infox("User wants subject modified for tagged mail") + local tagged_rcpt = task:get_symbol("TAGGED_RCPT") + local user = task:get_recipients(0)[1]['user'] + local domain = task:get_recipients(0)[1]['domain'] + local rcpt = user .. '@' .. domain + local authdomain = auth_domain_map:get_key(domain) + + if tagged_rcpt then + local tag = tagged_rcpt[1].options[1] + rspamd_logger.infox("found tag: %s", tag) + local action = task:get_metric_action('default') + rspamd_logger.infox("metric action now: %s", action) + + if action ~= 'no action' and action ~= 'greylist' then + rspamd_logger.infox("skipping tag handler for action: %s", action) + return false + end + + if authdomain then + rspamd_logger.infox("found mailcow domain %s", domain) + rspamd_logger.infox("querying tag settings for user %s", rcpt) + + if modify_subject_map:get_key(rcpt) then + rspamd_logger.infox("user wants subject modified for tagged mail") local sbj = task:get_header('Subject') - if tag then - rspamd_logger.infox("Found tag %1, will modify subject header", tag) new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?=' task:set_rmilter_reply({ remove_headers = {['Subject'] = 1}, add_headers = {['Subject'] = new_sbj} }) - end else rspamd_logger.infox("Add X-Moo-Tag header") task:set_rmilter_reply({ @@ -54,8 +63,44 @@ rspamd_config.ADD_DELIMITER_TAG = { }) end else - rspamd_logger.infox("Skip delimiter handling for untagged message or authenticated user") + rspamd_logger.infox("skip delimiter handling for unknown domain") end return false end +end) + +rspamd_config.MRAPTOR = { + callback = function(task) + local parts = task:get_parts() + local rspamd_logger = require "rspamd_logger" + local rspamd_regexp = require "rspamd_regexp" + + if parts then + for _,p in ipairs(parts) do + local mtype,subtype = p:get_type() + local re = rspamd_regexp.create_cached('/(office|word|excel)/i') + if re:match(subtype) then + local content = tostring(p:get_content()) + local filename = p:get_filename() + + local file = os.tmpname() + f = io.open(file, "a+") + f:write(content) + f:close() + + local scan = assert(io.popen('PATH=/usr/bin:/usr/local/bin mraptor ' .. file .. '> /dev/null 2>&1; echo $?', 'r')) + local result = scan:read('*all') + local exit_code = string.match(result, "%d+") + rspamd_logger.infox(exit_code) + scan:close() + + if exit_code == "20" then + rspamd_logger.infox("Reject dangerous macro in office file " .. filename) + task:set_pre_result(rspamd_actions['reject'], 'Dangerous macro in office file ' .. filename) + end + + end + end + end + end } diff --git a/data/web/add.php b/data/web/add.php index 273769ba..664d96ad 100644 --- a/data/web/add.php +++ b/data/web/add.php @@ -1,6 +1,6 @@
- +

@@ -112,7 +112,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
- +
@@ -132,29 +132,12 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
- + ".htmlspecialchars($domain).""; + } + ?>
@@ -167,7 +150,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
- +
@@ -188,30 +171,13 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
- +
@@ -247,7 +213,61 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
- + +
+
+ + +

+
"> +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
@@ -259,6 +279,94 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm +

+

+
"> +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ diff --git a/data/web/admin.php b/data/web/admin.php index b3a3ab73..87a4f98f 100644 --- a/data/web/admin.php +++ b/data/web/admin.php @@ -4,251 +4,302 @@ require_once("inc/prerequisites.inc.php"); if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin") { require_once("inc/header.inc.php"); $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; +$tfa_data = get_tfa(); ?>
-

+

-
-
-
-
-
- prepare("SELECT `username` FROM `admin` - WHERE `superadmin`='1' and active='1'"); - $stmt->execute(); - $AdminData = $stmt->fetch(PDO::FETCH_ASSOC); - } - catch(PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - } - ?> - -
- -
- - ↳ a-z A-Z - _ . -
-
-
- -
- -
-
-
- -
- -
-
-
-
- -
-
-
-
-
-
-
- -
-
-
-
-
- - - - - - - - - - - query("SELECT DISTINCT - `username`, - CASE WHEN `active`='1' THEN '".$lang['admin']['yes']."' ELSE '".$lang['admin']['no']."' END AS `active` - FROM `domain_admins` - WHERE `username` IN ( - SELECT `username` FROM `admin` - WHERE `superadmin`!='1' - )"); - $rows_username = $stmt->fetchAll(PDO::FETCH_ASSOC); - } - catch(PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - } - if(!empty($rows_username)): - while ($row_user_state = array_shift($rows_username)): - ?> - - - - - - - +
+
+
+
+ + +
+ +
+ + ↳ a-z A-Z - _ . +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
+ +
+
+
:
+
+

+
+ +
+ +
+ + [] +
+ + +
+
+
+
+
+
:
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
- prepare("SELECT `domain` FROM `domain_admins` WHERE `username` = :username"); - $stmt->execute(array('username' => $row_user_state['username'])); - $rows_domain = $stmt->fetchAll(PDO::FETCH_ASSOC); - } - catch(PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - } - while ($row_domain = array_shift($rows_domain)) { - echo htmlspecialchars($row_domain['domain']).'
'; - } - ?> -
-
- - -
-
+ + + + + + + + + + + + + + + + + + + - - - - -
+ '; + } + ?> + +
+ + +
+
-
- - - -
-
- -
- - ↳ a-z A-Z - _ . -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
-
-
- -
-
-
-
-
- -
-
-
-
-
-
-
-
+ + + + + +
+ + + +
+
+ +
+ + ↳ a-z A-Z - _ . +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ + + -

-
-
-
-
- -
-
-

Domain: (dkim._domainkey)

-
-
-
v=DKIM1;k=rsa;t=s;s=email;p=
-
-
-
- - - -
-
-
- - -
-
- - -
-
- -
- -
-
-
-
- -

-
+

+
+
+
+
+

+ +
+
+

Domain:
+ + bit +

+
+
+
+
+
+
+ + + +
+
+
+ +
+
+

Domain:

+
+
-
+
 
+
+ +
+
+

↳ Alias-Domain:
+ + bit +

+
+
+
+
+
+
+ + + +
+
+
+ +
+
+

↳ Alias-Domain:

+
+
-
+
 
+
+ +
+
+

Domain:

+
+
+
+
+
+
+ + + +
+
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+
+ +
@@ -268,10 +319,10 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
-
+ + - diff --git a/data/web/autodiscover.php b/data/web/autodiscover.php index a503b80c..d729306f 100644 --- a/data/web/autodiscover.php +++ b/data/web/autodiscover.php @@ -1,6 +1,8 @@ 'yes', 'autodiscoverType' => 'activesync', @@ -15,22 +17,44 @@ $config = array( 'ssl' => 'on' ), 'activesync' => array( - 'url' => 'https://' . $mailcow_hostname . '/Microsoft-Server-ActiveSync' + 'url' => 'https://'.$mailcow_hostname.'/Microsoft-Server-ActiveSync' ) ); -// If useEASforOutlook == no, the autodiscoverType option will be replaced to imap. + +if(file_exists('inc/vars.local.inc.php')) { + include_once 'inc/vars.local.inc.php'; +} + +/* ---------- DO NOT MODIFY ANYTHING BEYOND THIS LINE. IGNORE AT YOUR OWN RISK. ---------- */ + if ($config['useEASforOutlook'] == 'no') { if (strpos($_SERVER['HTTP_USER_AGENT'], 'Outlook')) { $config['autodiscoverType'] = 'imap'; } } -// Workaround for short open tags -echo ''; -?> - - PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, +]; +$pdo = new PDO($dsn, $database_user, $database_pass, $opt); +$login_user = strtolower(trim($_SERVER['PHP_AUTH_USER'])); +$as = check_login($login_user, $_SERVER['PHP_AUTH_PW']); + +if (!isset($_SERVER['PHP_AUTH_USER']) OR $as !== "user") { + header('WWW-Authenticate: Basic realm=""'); + header('HTTP/1.0 401 Unauthorized'); + exit; +} else { + if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) { + if ($as === "user") { + header("Content-Type: application/xml"); + echo ''; + + $data = trim(file_get_contents("php://input")); + if(!$data) { list($usec, $sec) = explode(' ', microtime()); echo ''; echo ''; @@ -38,84 +62,81 @@ if(!$data) { echo ''; echo ''; exit(0); -} + } + $discover = new SimpleXMLElement($data); + $email = $discover->Request->EMailAddress; -$discover = new SimpleXMLElement($data); -$email = $discover->Request->EMailAddress; - -if ($config['autodiscoverType'] == 'imap') { -?> - - - email - settings - - IMAP - - - off - - off - - on - - - SMTP - - - off - - off - - on - on - off - - - - PDO::ERRMODE_EXCEPTION, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - PDO::ATTR_EMULATE_PREPARES => false, - ]; - $pdo = new PDO($dsn, $database_user, $database_pass, $opt); - $username = trim($email); - try { - $stmt = $pdo->prepare("SELECT `name` FROM `mailbox` WHERE `username`= :username"); - $stmt->execute(array(':username' => $username)); - $MailboxData = $stmt->fetch(PDO::FETCH_ASSOC); - } - catch(PDOException $e) { - die("Failed to determine name from SQL"); - } - if (!empty($MailboxData['name'])) { - $displayname = utf8_encode($MailboxData['name']); - } - else { - $displayname = $email; - } -?> - - en:en - - - - - - - - MobileSync - - - - - - - + if ($config['autodiscoverType'] == 'imap') { + ?> + + + email + settings + + IMAP + + + off + + off + + on + + + SMTP + + + off + + off + + on + on + off + + + + prepare("SELECT `name` FROM `mailbox` WHERE `username`= :username"); + $stmt->execute(array(':username' => $username)); + $MailboxData = $stmt->fetch(PDO::FETCH_ASSOC); + } + catch(PDOException $e) { + die("Failed to determine name from SQL"); + } + if (!empty($MailboxData['name'])) { + $displayname = utf8_encode($MailboxData['name']); + } + else { + $displayname = $email; + } + ?> + + en:en + + + + + + + + MobileSync + + + + + + + + diff --git a/data/web/call_sogo_ctrl.php b/data/web/call_sogo_ctrl.php index c548bd56..4d6dc952 100644 --- a/data/web/call_sogo_ctrl.php +++ b/data/web/call_sogo_ctrl.php @@ -6,7 +6,7 @@ if (!isset($_SESSION['mailcow_cc_role']) OR !in_array($_SESSION['mailcow_cc_role exit(); } if ($_GET['ACTION'] == "start") { - $request = xmlrpc_encode_request("supervisor.startProcessGroup", 'sogo-group', array('encoding'=>'utf-8')); + $request = xmlrpc_encode_request("supervisor.startProcess", 'reconf-domains', array('encoding'=>'utf-8')); $context = stream_context_create(array('http' => array( 'method' => "POST", 'header' => "Content-Length: " . strlen($request), @@ -18,11 +18,25 @@ if ($_GET['ACTION'] == "start") { echo '' . $response['faultString'] . ''; } else { - echo 'OK'; + sleep(4); + $request = xmlrpc_encode_request("supervisor.startProcess", 'sogo', array('encoding'=>'utf-8')); + $context = stream_context_create(array('http' => array( + 'method' => "POST", + 'header' => "Content-Length: " . strlen($request), + 'content' => $request + ))); + $file = @file_get_contents("http://sogo:9191/RPC2", false, $context) or die("Cannot connect to $remote_server:$listener_port"); + $response = xmlrpc_decode($file); + if (isset($response['faultString'])) { + echo '' . $response['faultString'] . ''; + } + else { + echo 'OK'; + } } } elseif ($_GET['ACTION'] == "stop") { - $request = xmlrpc_encode_request("supervisor.stopProcessGroup", 'sogo-group', array('encoding'=>'utf-8')); + $request = xmlrpc_encode_request("supervisor.stopProcess", 'sogo', array('encoding'=>'utf-8')); $context = stream_context_create(array('http' => array( 'method' => "POST", 'header' => "Content-Length: " . strlen($request), @@ -34,7 +48,21 @@ elseif ($_GET['ACTION'] == "stop") { echo '' . $response['faultString'] . ''; } else { - echo 'OK'; + sleep(1); + $request = xmlrpc_encode_request("supervisor.stopProcess", 'reconf-domains', array('encoding'=>'utf-8')); + $context = stream_context_create(array('http' => array( + 'method' => "POST", + 'header' => "Content-Length: " . strlen($request), + 'content' => $request + ))); + $file = @file_get_contents("http://sogo:9191/RPC2", false, $context) or die("Cannot connect to $remote_server:$listener_port"); + $response = xmlrpc_decode($file); + if (isset($response['faultString'])) { + echo '' . $response['faultString'] . ''; + } + else { + echo 'OK'; + } } } ?> \ No newline at end of file diff --git a/data/web/css/bootstrap-select.min.css b/data/web/css/bootstrap-select.min.css new file mode 100644 index 00000000..d178d824 --- /dev/null +++ b/data/web/css/bootstrap-select.min.css @@ -0,0 +1,6 @@ +/*! + * Bootstrap-select v1.12.2 (http://silviomoreto.github.io/bootstrap-select) + * + * Copyright 2013-2017 bootstrap-select + * Licensed under MIT (https://github.com/silviomoreto/bootstrap-select/blob/master/LICENSE) + */select.bs-select-hidden,select.selectpicker{display:none!important}.bootstrap-select{width:220px\9}.bootstrap-select>.dropdown-toggle{width:100%;padding-right:25px;z-index:1}.bootstrap-select>.dropdown-toggle.bs-placeholder,.bootstrap-select>.dropdown-toggle.bs-placeholder:active,.bootstrap-select>.dropdown-toggle.bs-placeholder:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder:hover{color:#999}.bootstrap-select>select{position:absolute!important;bottom:0;left:50%;display:block!important;width:.5px!important;height:100%!important;padding:0!important;opacity:0!important;border:none}.bootstrap-select>select.mobile-device{top:0;left:0;display:block!important;width:100%!important;z-index:2}.error .bootstrap-select .dropdown-toggle,.has-error .bootstrap-select .dropdown-toggle{border-color:#b94a48}.bootstrap-select.fit-width{width:auto!important}.bootstrap-select:not([class*=col-]):not([class*=form-control]):not(.input-group-btn){width:220px}.bootstrap-select .dropdown-toggle:focus{outline:thin dotted #333!important;outline:5px auto -webkit-focus-ring-color!important;outline-offset:-2px}.bootstrap-select.form-control{margin-bottom:0;padding:0;border:none}.bootstrap-select.form-control:not([class*=col-]){width:100%}.bootstrap-select.form-control.input-group-btn{z-index:auto}.bootstrap-select.form-control.input-group-btn:not(:first-child):not(:last-child)>.btn{border-radius:0}.bootstrap-select.btn-group:not(.input-group-btn),.bootstrap-select.btn-group[class*=col-]{float:none;display:inline-block;margin-left:0}.bootstrap-select.btn-group.dropdown-menu-right,.bootstrap-select.btn-group[class*=col-].dropdown-menu-right,.row .bootstrap-select.btn-group[class*=col-].dropdown-menu-right{float:right}.form-group .bootstrap-select.btn-group,.form-horizontal .bootstrap-select.btn-group,.form-inline .bootstrap-select.btn-group{margin-bottom:0}.form-group-lg .bootstrap-select.btn-group.form-control,.form-group-sm .bootstrap-select.btn-group.form-control{padding:0}.form-group-lg .bootstrap-select.btn-group.form-control .dropdown-toggle,.form-group-sm .bootstrap-select.btn-group.form-control .dropdown-toggle{height:100%;font-size:inherit;line-height:inherit;border-radius:inherit}.form-inline .bootstrap-select.btn-group .form-control{width:100%}.bootstrap-select.btn-group.disabled,.bootstrap-select.btn-group>.disabled{cursor:not-allowed}.bootstrap-select.btn-group.disabled:focus,.bootstrap-select.btn-group>.disabled:focus{outline:0!important}.bootstrap-select.btn-group.bs-container{position:absolute;height:0!important;padding:0!important}.bootstrap-select.btn-group.bs-container .dropdown-menu{z-index:1060}.bootstrap-select.btn-group .dropdown-toggle .filter-option{display:inline-block;overflow:hidden;width:100%;text-align:left}.bootstrap-select.btn-group .dropdown-toggle .caret{position:absolute;top:50%;right:12px;margin-top:-2px;vertical-align:middle}.bootstrap-select.btn-group[class*=col-] .dropdown-toggle{width:100%}.bootstrap-select.btn-group .dropdown-menu{min-width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-select.btn-group .dropdown-menu.inner{position:static;float:none;border:0;padding:0;margin:0;border-radius:0;-webkit-box-shadow:none;box-shadow:none}.bootstrap-select.btn-group .dropdown-menu li{position:relative}.bootstrap-select.btn-group .dropdown-menu li.active small{color:#fff}.bootstrap-select.btn-group .dropdown-menu li.disabled a{cursor:not-allowed}.bootstrap-select.btn-group .dropdown-menu li a{cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.bootstrap-select.btn-group .dropdown-menu li a.opt{position:relative;padding-left:2.25em}.bootstrap-select.btn-group .dropdown-menu li a span.check-mark{display:none}.bootstrap-select.btn-group .dropdown-menu li a span.text{display:inline-block}.bootstrap-select.btn-group .dropdown-menu li small{padding-left:.5em}.bootstrap-select.btn-group .dropdown-menu .notify{position:absolute;bottom:5px;width:96%;margin:0 2%;min-height:26px;padding:3px 5px;background:#f5f5f5;border:1px solid #e3e3e3;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05);pointer-events:none;opacity:.9;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-select.btn-group .no-results{padding:3px;background:#f5f5f5;margin:0 5px;white-space:nowrap}.bootstrap-select.btn-group.fit-width .dropdown-toggle .filter-option{position:static}.bootstrap-select.btn-group.fit-width .dropdown-toggle .caret{position:static;top:auto;margin-top:-1px}.bootstrap-select.btn-group.show-tick .dropdown-menu li.selected a span.check-mark{position:absolute;display:inline-block;right:15px;margin-top:5px}.bootstrap-select.btn-group.show-tick .dropdown-menu li a span.text{margin-right:34px}.bootstrap-select.show-menu-arrow.open>.dropdown-toggle{z-index:1061}.bootstrap-select.show-menu-arrow .dropdown-toggle:before{content:'';border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid rgba(204,204,204,.2);position:absolute;bottom:-4px;left:9px;display:none}.bootstrap-select.show-menu-arrow .dropdown-toggle:after{content:'';border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #fff;position:absolute;bottom:-4px;left:10px;display:none}.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle:before{bottom:auto;top:-3px;border-top:7px solid rgba(204,204,204,.2);border-bottom:0}.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle:after{bottom:auto;top:-3px;border-top:6px solid #fff;border-bottom:0}.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle:before{right:12px;left:auto}.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle:after{right:13px;left:auto}.bootstrap-select.show-menu-arrow.open>.dropdown-toggle:after,.bootstrap-select.show-menu-arrow.open>.dropdown-toggle:before{display:block}.bs-actionsbox,.bs-donebutton,.bs-searchbox{padding:4px 8px}.bs-actionsbox{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bs-actionsbox .btn-group button{width:50%}.bs-donebutton{float:left;width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bs-donebutton .btn-group button{width:100%}.bs-searchbox+.bs-actionsbox{padding:0 8px 4px}.bs-searchbox .form-control{margin-bottom:0;width:100%;float:none} \ No newline at end of file diff --git a/data/web/css/bootstrap-slider.min.css b/data/web/css/bootstrap-slider.min.css new file mode 100644 index 00000000..e55300b2 --- /dev/null +++ b/data/web/css/bootstrap-slider.min.css @@ -0,0 +1,41 @@ +/*! ======================================================= + VERSION 9.7.2 +========================================================= */ +/*! ========================================================= + * bootstrap-slider.js + * + * Maintainers: + * Kyle Kemp + * - Twitter: @seiyria + * - Github: seiyria + * Rohit Kalkur + * - Twitter: @Rovolutionary + * - Github: rovolution + * + * ========================================================= + * + * bootstrap-slider is released under the MIT License + * Copyright (c) 2017 Kyle Kemp, Rohit Kalkur, and contributors + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + * + * ========================================================= */.slider{display:inline-block;vertical-align:middle;position:relative}.slider.slider-horizontal{width:210px;height:20px}.slider.slider-horizontal .slider-track{height:10px;width:100%;margin-top:-5px;top:50%;left:0}.slider.slider-horizontal .slider-selection,.slider.slider-horizontal .slider-track-low,.slider.slider-horizontal .slider-track-high{height:100%;top:0;bottom:0}.slider.slider-horizontal .slider-tick,.slider.slider-horizontal .slider-handle{margin-left:-10px}.slider.slider-horizontal .slider-tick.triangle,.slider.slider-horizontal .slider-handle.triangle{position:relative;top:50%;transform:translateY(-50%);border-width:0 10px 10px 10px;width:0;height:0;border-bottom-color:#0480be;margin-top:0}.slider.slider-horizontal .slider-tick-container{white-space:nowrap;position:absolute;top:0;left:0;width:100%}.slider.slider-horizontal .slider-tick-label-container{white-space:nowrap;margin-top:20px}.slider.slider-horizontal .slider-tick-label-container .slider-tick-label{padding-top:4px;display:inline-block;text-align:center}.slider.slider-horizontal.slider-rtl .slider-track{left:initial;right:0}.slider.slider-horizontal.slider-rtl .slider-tick,.slider.slider-horizontal.slider-rtl .slider-handle{margin-left:initial;margin-right:-10px}.slider.slider-horizontal.slider-rtl .slider-tick-container{left:initial;right:0}.slider.slider-vertical{height:210px;width:20px}.slider.slider-vertical .slider-track{width:10px;height:100%;left:25%;top:0}.slider.slider-vertical .slider-selection{width:100%;left:0;top:0;bottom:0}.slider.slider-vertical .slider-track-low,.slider.slider-vertical .slider-track-high{width:100%;left:0;right:0}.slider.slider-vertical .slider-tick,.slider.slider-vertical .slider-handle{margin-top:-10px}.slider.slider-vertical .slider-tick.triangle,.slider.slider-vertical .slider-handle.triangle{border-width:10px 0 10px 10px;width:1px;height:1px;border-left-color:#0480be;border-right-color:#0480be;margin-left:0;margin-right:0}.slider.slider-vertical .slider-tick-label-container{white-space:nowrap}.slider.slider-vertical .slider-tick-label-container .slider-tick-label{padding-left:4px}.slider.slider-vertical.slider-rtl .slider-track{left:initial;right:25%}.slider.slider-vertical.slider-rtl .slider-selection{left:initial;right:0}.slider.slider-vertical.slider-rtl .slider-tick.triangle,.slider.slider-vertical.slider-rtl .slider-handle.triangle{border-width:10px 10px 10px 0}.slider.slider-vertical.slider-rtl .slider-tick-label-container .slider-tick-label{padding-left:initial;padding-right:4px}.slider.slider-disabled .slider-handle{background-image:-webkit-linear-gradient(top,#dfdfdf 0,#bebebe 100%);background-image:-o-linear-gradient(top,#dfdfdf 0,#bebebe 100%);background-image:linear-gradient(to bottom,#dfdfdf 0,#bebebe 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdfdfdf',endColorstr='#ffbebebe',GradientType=0)}.slider.slider-disabled .slider-track{background-image:-webkit-linear-gradient(top,#e5e5e5 0,#e9e9e9 100%);background-image:-o-linear-gradient(top,#e5e5e5 0,#e9e9e9 100%);background-image:linear-gradient(to bottom,#e5e5e5 0,#e9e9e9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe5e5e5',endColorstr='#ffe9e9e9',GradientType=0);cursor:not-allowed}.slider input{display:none}.slider .tooltip.top{margin-top:-36px}.slider .tooltip-inner{white-space:nowrap;max-width:none}.slider .hide{display:none}.slider-track{position:absolute;cursor:pointer;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#f9f9f9 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#f9f9f9 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#f9f9f9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#fff9f9f9',GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);border-radius:4px}.slider-selection{position:absolute;background-image:-webkit-linear-gradient(top,#f9f9f9 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#f9f9f9 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#f9f9f9 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9',endColorstr='#fff5f5f5',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;border-radius:4px}.slider-selection.tick-slider-selection{background-image:-webkit-linear-gradient(top,#89cdef 0,#81bfde 100%);background-image:-o-linear-gradient(top,#89cdef 0,#81bfde 100%);background-image:linear-gradient(to bottom,#89cdef 0,#81bfde 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff89cdef',endColorstr='#ff81bfde',GradientType=0)}.slider-track-low,.slider-track-high{position:absolute;background:transparent;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;border-radius:4px}.slider-handle{position:absolute;top:0;width:20px;height:20px;background-color:#337ab7;background-image:-webkit-linear-gradient(top,#149bdf 0,#0480be 100%);background-image:-o-linear-gradient(top,#149bdf 0,#0480be 100%);background-image:linear-gradient(to bottom,#149bdf 0,#0480be 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf',endColorstr='#ff0480be',GradientType=0);filter:none;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);border:0 solid transparent}.slider-handle.round{border-radius:50%}.slider-handle.triangle{background:transparent none}.slider-handle.custom{background:transparent none}.slider-handle.custom::before{line-height:20px;font-size:20px;content:'\2605';color:#726204}.slider-tick{position:absolute;width:20px;height:20px;background-image:-webkit-linear-gradient(top,#f9f9f9 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#f9f9f9 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#f9f9f9 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9',endColorstr='#fff5f5f5',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;filter:none;opacity:.8;border:0 solid transparent}.slider-tick.round{border-radius:50%}.slider-tick.triangle{background:transparent none}.slider-tick.custom{background:transparent none}.slider-tick.custom::before{line-height:20px;font-size:20px;content:'\2605';color:#726204}.slider-tick.in-selection{background-image:-webkit-linear-gradient(top,#89cdef 0,#81bfde 100%);background-image:-o-linear-gradient(top,#89cdef 0,#81bfde 100%);background-image:linear-gradient(to bottom,#89cdef 0,#81bfde 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff89cdef',endColorstr='#ff81bfde',GradientType=0);opacity:1} \ No newline at end of file diff --git a/data/web/css/bootstrap-switch.min.css b/data/web/css/bootstrap-switch.min.css new file mode 100644 index 00000000..cbfa013b --- /dev/null +++ b/data/web/css/bootstrap-switch.min.css @@ -0,0 +1,10 @@ +/** + * bootstrap-switch - Turn checkboxes and radio buttons into toggle switches. + * + * @version v3.3.3 + * @homepage http://www.bootstrap-switch.org + * @author Mattia Larentis (http://larentis.eu) + * @license Apache-2.0 + */ + +.bootstrap-switch{display:inline-block;direction:ltr;cursor:pointer;border-radius:4px;border:1px solid #ccc;position:relative;text-align:left;overflow:hidden;line-height:8px;z-index:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;vertical-align:middle;-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.bootstrap-switch .bootstrap-switch-container{display:inline-block;top:0;border-radius:4px;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.bootstrap-switch .bootstrap-switch-handle-off,.bootstrap-switch .bootstrap-switch-handle-on,.bootstrap-switch .bootstrap-switch-label{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;cursor:pointer;display:inline-block!important;height:100%;padding:6px 12px;font-size:14px;line-height:20px}.bootstrap-switch .bootstrap-switch-handle-off,.bootstrap-switch .bootstrap-switch-handle-on{text-align:center;z-index:1}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-primary,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-primary{color:#fff;background:#337ab7}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-info,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-info{color:#fff;background:#5bc0de}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-success,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-success{color:#fff;background:#5cb85c}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-warning,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-warning{background:#f0ad4e;color:#fff}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-danger,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-danger{color:#fff;background:#d9534f}.bootstrap-switch .bootstrap-switch-handle-off.bootstrap-switch-default,.bootstrap-switch .bootstrap-switch-handle-on.bootstrap-switch-default{color:#000;background:#eee}.bootstrap-switch .bootstrap-switch-label{text-align:center;margin-top:-1px;margin-bottom:-1px;z-index:100;color:#333;background:#fff}.bootstrap-switch .bootstrap-switch-handle-on{border-bottom-left-radius:3px;border-top-left-radius:3px}.bootstrap-switch .bootstrap-switch-handle-off{border-bottom-right-radius:3px;border-top-right-radius:3px}.bootstrap-switch input[type=radio],.bootstrap-switch input[type=checkbox]{position:absolute!important;top:0;left:0;margin:0;z-index:-1;opacity:0;filter:alpha(opacity=0)}.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-mini .bootstrap-switch-label{padding:1px 5px;font-size:12px;line-height:1.5}.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-small .bootstrap-switch-label{padding:5px 10px;font-size:12px;line-height:1.5}.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-large .bootstrap-switch-label{padding:6px 16px;font-size:18px;line-height:1.3333333}.bootstrap-switch.bootstrap-switch-disabled,.bootstrap-switch.bootstrap-switch-indeterminate,.bootstrap-switch.bootstrap-switch-readonly{cursor:default!important}.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-disabled .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-indeterminate .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-handle-off,.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-handle-on,.bootstrap-switch.bootstrap-switch-readonly .bootstrap-switch-label{opacity:.5;filter:alpha(opacity=50);cursor:default!important}.bootstrap-switch.bootstrap-switch-animate .bootstrap-switch-container{-webkit-transition:margin-left .5s;-o-transition:margin-left .5s;transition:margin-left .5s}.bootstrap-switch.bootstrap-switch-inverse .bootstrap-switch-handle-on{border-radius:0 3px 3px 0}.bootstrap-switch.bootstrap-switch-inverse .bootstrap-switch-handle-off{border-radius:3px 0 0 3px}.bootstrap-switch.bootstrap-switch-focused{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-off .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-on .bootstrap-switch-label{border-bottom-right-radius:3px;border-top-right-radius:3px}.bootstrap-switch.bootstrap-switch-inverse.bootstrap-switch-on .bootstrap-switch-label,.bootstrap-switch.bootstrap-switch-off .bootstrap-switch-label{border-bottom-left-radius:3px;border-top-left-radius:3px} \ No newline at end of file diff --git a/data/web/css/mailbox.css b/data/web/css/mailbox.css new file mode 100644 index 00000000..b5c69343 --- /dev/null +++ b/data/web/css/mailbox.css @@ -0,0 +1,19 @@ +.panel-heading div { + margin-top: -18px; + font-size: 15px; +} +.panel-heading div span { + margin-left:5px; +} +.panel-body { + display: none; +} +.clickable { + cursor: pointer; +} +.progress { + margin-bottom: 0px; +} +.table>thead>tr>th { + vertical-align: top !important; +} \ No newline at end of file diff --git a/data/web/css/mailcow.css b/data/web/css/mailcow.css new file mode 100644 index 00000000..a47252bc --- /dev/null +++ b/data/web/css/mailcow.css @@ -0,0 +1,46 @@ +#maxmsgsize { min-width: 80px; } +#slider1 .slider-selection { + background: #FFD700; +} +#slider1 .slider-track-high { + background: #FF4500; +} +#slider1 .slider-track-low { + background: #66CD00; +} +.striped:nth-child(odd) { + background-color: #fff; +} +.striped:nth-child(even) { + background-color: #fafafa; + border:1px solid white; +} +.btn { + text-transform: none; +} +.glyphicon-spin { + font-size:12px; + -webkit-animation: spin 2000ms infinite linear; + animation: spin 2000ms infinite linear; +} +@-webkit-keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +pre{white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word;} \ No newline at end of file diff --git a/data/web/css/tables.css b/data/web/css/tables.css new file mode 100644 index 00000000..651e1665 --- /dev/null +++ b/data/web/css/tables.css @@ -0,0 +1,79 @@ +ul[id*="sortable"] { word-wrap: break-word; list-style-type: none; float: left; padding: 0 15px 0 0; width: 48%; cursor:move} +ul[id$="sortable-active"] li {cursor:move; } +ul[id$="sortable-inactive"] li {cursor:move } +.list-heading { cursor:default !important} +.ui-state-disabled { cursor:no-drop; color:#ccc; } +.ui-state-highlight {background: #F5F5F5 !important; height: 41px !important; cursor:move } +table[data-sortable] { + border-collapse: collapse; + border-spacing: 0; +} +table[data-sortable] th { + vertical-align: bottom; + font-weight: bold; +} +table[data-sortable] th, table[data-sortable] td { + text-align: left; + padding: 10px; +} +table[data-sortable] th:not([data-sortable="false"]) { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + -webkit-touch-callout: none; + cursor: pointer; +} +table[data-sortable] th:after { + content: ""; + visibility: hidden; + display: inline-block; + vertical-align: inherit; + height: 0; + width: 0; + border-width: 5px; + border-style: solid; + border-color: transparent; + margin-right: 1px; + margin-left: 10px; + float: right; +} +table[data-sortable] th[data-sortable="false"]:after { + display: none; +} +table[data-sortable] th[data-sorted="true"]:after { + visibility: visible; +} +table[data-sortable] th[data-sorted-direction="descending"]:after { + border-top-color: inherit; + margin-top: 8px; +} +table[data-sortable] th[data-sorted-direction="ascending"]:after { + border-bottom-color: inherit; + margin-top: 3px; +} +table[data-sortable].sortable-theme-bootstrap thead th { + border-bottom: 2px solid #e0e0e0; +} +table[data-sortable].sortable-theme-bootstrap th[data-sorted="true"] { + color: #3a87ad; + background: #d9edf7; + border-bottom-color: #bce8f1; +} +table[data-sortable].sortable-theme-bootstrap th[data-sorted="true"][data-sorted-direction="descending"]:after { + border-top-color: #3a87ad; +} +table[data-sortable].sortable-theme-bootstrap th[data-sorted="true"][data-sorted-direction="ascending"]:after { + border-bottom-color: #3a87ad; +} +table[data-sortable].sortable-theme-bootstrap.sortable-theme-bootstrap-striped tbody > tr:nth-child(odd) > td { + background-color: #f9f9f9; +} +#data td, #no-data td { + vertical-align: middle; +} +.sort-table:hover { + border-bottom-color: #00B7DC !important; +} \ No newline at end of file diff --git a/data/web/delete.php b/data/web/delete.php index f8a53aeb..6ba93d31 100644 --- a/data/web/delete.php +++ b/data/web/delete.php @@ -1,6 +1,6 @@ ">
- +
@@ -49,7 +49,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm ">
- +
@@ -66,27 +66,16 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm isset($_GET["aliasdomain"]) && is_valid_domain_name($_GET["aliasdomain"]) && !empty($_GET["aliasdomain"])) { - $alias_domain = strtolower(trim($_GET["aliasdomain"])); - try { - $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` - WHERE `alias_domain`= :alias_domain"); - $stmt->execute(array(':alias_domain' => $alias_domain)); - $DomainData = $stmt->fetch(PDO::FETCH_ASSOC); - } - catch(PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - } - if (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $DomainData['target_domain'])) { + $alias_domain = $_GET["aliasdomain"]; + $result = mailbox_get_alias_domain_details($alias_domain); + if (!empty($result)) { ?>
- +
@@ -102,7 +91,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm elseif (isset($_GET["domainadmin"]) && ctype_alnum(str_replace(array('_', '.', '-'), '', $_GET["domainadmin"])) && !empty($_GET["domainadmin"]) && - $_SESSION['mailcow_cc_role'] == "admin") { + $_SESSION['mailcow_cc_role'] == "admin") { $domain_admin = $_GET["domainadmin"]; ?> @@ -110,7 +99,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
- +
@@ -121,16 +110,74 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm filter_var($_GET["mailbox"], FILTER_VALIDATE_EMAIL) && !empty($_GET["mailbox"])) { $mailbox = $_GET["mailbox"]; - $domain = substr(strrchr($mailbox, "@"), 1); - if (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) { + if (hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $mailbox)) { ?> - +

- + +
+
+
+ + + + +

+
+ +
+
+ +
+
+
+ + + + + + +

+
+ +
+
+ "> +
diff --git a/data/web/edit.php b/data/web/edit.php index cb8fb15d..5879df6d 100644 --- a/data/web/edit.php +++ b/data/web/edit.php @@ -1,6 +1,6 @@ prepare("SELECT * FROM `alias` - WHERE `address`= :address - AND `goto` != :goto - AND ( - `domain` IN ( - SELECT `domain` FROM `domain_admins` - WHERE `active`='1' - AND `username`= :username - ) - OR 'admin'= :admin - )"); - $stmt->execute(array( - ':address' => $alias, - ':goto' => $alias, - ':username' => $_SESSION['mailcow_cc_username'], - ':admin' => $_SESSION['mailcow_cc_role'] - )); - $result = $stmt->fetch(PDO::FETCH_ASSOC); - } - catch(PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - } - if ($result !== false) { + $result = mailbox_get_alias_details($alias); + if (!empty($result)) { ?>


@@ -62,13 +36,13 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
- +
- +
@@ -86,68 +60,34 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm $_GET["domainadmin"] != 'admin' && $_SESSION['mailcow_cc_role'] == "admin") { $domain_admin = $_GET["domainadmin"]; - try { - $stmt = $pdo->prepare("SELECT * FROM `domain_admins` WHERE `username`= :domain_admin"); - $stmt->execute(array( - ':domain_admin' => $domain_admin - )); - $result = $stmt->fetch(PDO::FETCH_ASSOC); - } - catch(PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - } - if ($result !== false) { + $result = get_domain_admin_details($domain_admin); + if (!empty($result)) { ?>


"> - + +
+ +
+ +
+
@@ -167,13 +107,20 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
- +
- +
+ +
+
+
+
+
+
@@ -189,29 +136,8 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm is_valid_domain_name($_GET["domain"]) && !empty($_GET["domain"])) { $domain = $_GET["domain"]; - try { - $stmt = $pdo->prepare("SELECT * FROM `domain` WHERE `domain`='".$domain."' - AND ( - `domain` IN ( - SELECT `domain` from `domain_admins` - WHERE `active`='1' - AND `username` = :username - ) - OR 'admin'= :admin - )"); - $stmt->execute(array( - ':username' => $_SESSION['mailcow_cc_username'], - ':admin' => $_SESSION['mailcow_cc_role'] - )); - $result = $stmt->fetch(PDO::FETCH_ASSOC); - } - catch(PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - } - if ($result !== false) { + $result = mailbox_get_domain_details($domain); + if (!empty($result)) { ?>

"> @@ -228,34 +154,34 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
- +
- +
- +
- +
- +
- +

@@ -266,37 +192,148 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
- +
- +
-
-
-

Domain: (dkim._domainkey)

-
-
-
v=DKIM1;k=rsa;t=s;s=email;p=
-
-
-
- - - -
-
-
+
+
+
+

Domain: (dkim._domainkey)

+
+
+
+
+
+
+
+
+

+

+
+
+
+
+ +
+
+
+ +
+
+
+
+ + + + + + + + +
+
+
+ +
+
+
+
+ + + +
+
+ +
+
+
+
+
+

+

+
+
+
+
+ +
+
+
+ +
+
+
+
+ + + + + + + + +
+
+
+ +
+
+
+
+ + + +
+
+ +
+
+
+
+
+ @@ -308,31 +345,8 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm is_valid_domain_name($_GET["aliasdomain"]) && !empty($_GET["aliasdomain"])) { $alias_domain = $_GET["aliasdomain"]; - try { - $stmt = $pdo->prepare("SELECT * FROM `alias_domain` - WHERE `alias_domain`= :alias_domain - AND ( - `target_domain` IN ( - SELECT `domain` FROM `domain_admins` - WHERE `active`='1' - AND `username`= :username - ) - OR 'admin'= :admin - )"); - $stmt->execute(array( - ':alias_domain' => $alias_domain, - ':username' => $_SESSION['mailcow_cc_username'], - ':admin' => $_SESSION['mailcow_cc_role'] - )); - $result = $stmt->fetch(PDO::FETCH_ASSOC); - } - catch(PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - } - if ($result !== false) { + $result = mailbox_get_alias_domain_details($alias_domain); + if (!empty($result)) { ?>

"> @@ -346,46 +360,29 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
- +
- +
-
-
-

-
-
-
-
-
._domainkey
-
-
-
-
- -
-
-
+
+
+
+

Domain: (dkim._domainkey)

+
+
+
+
+
prepare("SELECT `username`, `domain`, `name`, `quota`, `active` FROM `mailbox` WHERE `username` = :username1"); - $stmt->execute(array( - ':username1' => $mailbox, - )); - $result = $stmt->fetch(PDO::FETCH_ASSOC); - } - catch(PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - } - if ($result !== false && hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $result['domain'])) { - $left_m = remaining_specs($result['domain'], $_GET['mailbox'])['left_m']; - ?> -

+ $mailbox = $_GET["mailbox"]; + $result = mailbox_get_mailbox_details($mailbox); + if (!empty($result)) { + ?> +

+
"> + +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +

"> - +
- +
- +
- +
- -
-
-
- -
- + + +
- -
- +
+
+ +
- +
+
+ +
+
+
+
+
+ +
+
+ + + + + + +

+
"> + +
+
- + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
@@ -499,7 +629,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
- +
diff --git a/data/web/img/yubi.ico b/data/web/img/yubi.ico new file mode 100644 index 00000000..126c2b17 Binary files /dev/null and b/data/web/img/yubi.ico differ diff --git a/data/web/inc/footer.inc.php b/data/web/inc/footer.inc.php index d2adca8e..5523ec58 100644 --- a/data/web/inc/footer.inc.php +++ b/data/web/inc/footer.inc.php @@ -1,4 +1,5 @@ + + + + + + + + \ No newline at end of file diff --git a/data/web/inc/triggers.inc.php b/data/web/inc/triggers.inc.php index 70e91640..2895420d 100644 --- a/data/web/inc/triggers.inc.php +++ b/data/web/inc/triggers.inc.php @@ -1,4 +1,15 @@ 'danger', 'msg' => $lang['danger']['login_failed'] ); } } + if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin") { - if (isset($_POST["trigger_set_admin"])) { - set_admin_account($_POST); + if (isset($_GET["duallogin"])) { + if (filter_var($_GET["duallogin"], FILTER_VALIDATE_EMAIL)) { + $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :duallogin"); + $stmt->execute(array(':duallogin' => $_GET["duallogin"])); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results != 0) { + $_SESSION["dual-login"]["username"] = $_SESSION['mailcow_cc_username']; + $_SESSION["dual-login"]["role"] = $_SESSION['mailcow_cc_role']; + $_SESSION['mailcow_cc_username'] = $_GET["duallogin"]; + $_SESSION['mailcow_cc_role'] = "user"; + header("Location: /user.php"); + } + } + } + + if (isset($_POST["edit_admin_account"])) { + edit_admin_account($_POST); } - if (isset($_POST["delete_dkim_record"])) { - dkim_table("delete", $_POST); + if (isset($_POST["dkim_delete_key"])) { + dkim_delete_key($_POST); } - if (isset($_POST["add_dkim_record"])) { - dkim_table("add", $_POST); + if (isset($_POST["dkim_add_key"])) { + dkim_add_key($_POST); } - if (isset($_POST["trigger_add_domain_admin"])) { + if (isset($_POST["add_domain_admin"])) { add_domain_admin($_POST); } - if (isset($_POST["trigger_delete_domain_admin"])) { + if (isset($_POST["delete_domain_admin"])) { delete_domain_admin($_POST); } - if (isset($_POST["trigger_edit_domain_admin"])) { - edit_domain_admin($_POST); - } } if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "user") { - if (isset($_POST["trigger_set_user_account"])) { - set_user_account($_POST); + if (isset($_POST["edit_user_account"])) { + edit_user_account($_POST); } - if (isset($_POST["trigger_set_spam_score"])) { - set_spam_score($_POST); + if (isset($_POST["mailbox_reset_eas"])) { + mailbox_reset_eas($_POST); } - if (isset($_POST["trigger_set_tagging_options"])) { - tagging_options('set', $_POST); + if (isset($_POST["edit_spam_score"])) { + edit_spam_score($_POST); } - if (isset($_POST["trigger_set_policy_list"])) { - set_policy_list($_POST); + if (isset($_POST["edit_delimiter_action"])) { + edit_delimiter_action($_POST); } - if (isset($_POST["trigger_set_tls_policy"])) { - set_tls_policy($_POST); + if (isset($_POST["add_policy_list_item"])) { + add_policy_list_item($_POST); } - if (isset($_POST["trigger_set_time_limited_aliases"])) { + if (isset($_POST["delete_policy_list_item"])) { + delete_policy_list_item($_POST); + } + if (isset($_POST["edit_tls_policy"])) { + edit_tls_policy($_POST); + } + if (isset($_POST["add_syncjob"])) { + add_syncjob($_POST); + } + if (isset($_POST["edit_syncjob"])) { + edit_syncjob($_POST); + } + if (isset($_POST["delete_syncjob"])) { + delete_syncjob($_POST); + } + if (isset($_POST["set_time_limited_aliases"])) { set_time_limited_aliases($_POST); } } if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "admin" || $_SESSION['mailcow_cc_role'] == "domainadmin")) { - if (isset($_GET["js"])) { - switch ($_GET["js"]) { - case "remaining_specs": - remaining_specs($_GET['domain'], $_GET['object'], "y"); - break; - } + if (isset($_POST["edit_domain_admin"])) { + edit_domain_admin($_POST); } - if (isset($_POST["trigger_mailbox_action"])) { - switch ($_POST["trigger_mailbox_action"]) { - case "adddomain": - mailbox_add_domain($_POST); - break; - case "addalias": - mailbox_add_alias($_POST); - break; - case "editalias": - mailbox_edit_alias($_POST); - break; - case "addaliasdomain": - mailbox_add_alias_domain($_POST); - break; - case "addmailbox": - mailbox_add_mailbox($_POST); - break; - case "editdomain": - mailbox_edit_domain($_POST); - break; - case "editmailbox": - mailbox_edit_mailbox($_POST); - break; - case "deletedomain": - mailbox_delete_domain($_POST); - break; - case "deletealias": - mailbox_delete_alias($_POST); - break; - case "deletealiasdomain": - mailbox_delete_alias_domain($_POST); - break; - case "editaliasdomain": - mailbox_edit_alias_domain($_POST); - break; - case "deletemailbox": - mailbox_delete_mailbox($_POST); - break; - } + if (isset($_POST["set_tfa"])) { + set_tfa($_POST); + } + if (isset($_POST["unset_tfa_key"])) { + unset_tfa_key($_POST); + } + if (isset($_POST["add_policy_list_item"])) { + add_policy_list_item($_POST); + } + if (isset($_POST["delete_policy_list_item"])) { + delete_policy_list_item($_POST); + } + if (isset($_POST["mailbox_add_domain"])) { + mailbox_add_domain($_POST); + } + if (isset($_POST["mailbox_add_alias"])) { + mailbox_add_alias($_POST); + } + if (isset($_POST["mailbox_add_alias_domain"])) { + mailbox_add_alias_domain($_POST); + } + if (isset($_POST["mailbox_add_mailbox"])) { + mailbox_add_mailbox($_POST); + } + if (isset($_POST["mailbox_add_resource"])) { + mailbox_add_resource($_POST); + } + if (isset($_POST["mailbox_edit_alias"])) { + mailbox_edit_alias($_POST); + } + if (isset($_POST["mailbox_edit_domain"])) { + mailbox_edit_domain($_POST); + } + if (isset($_POST["mailbox_edit_mailbox"])) { + mailbox_edit_mailbox($_POST); + } + if (isset($_POST["mailbox_edit_alias_domain"])) { + mailbox_edit_alias_domain($_POST); + } + if (isset($_POST["mailbox_edit_resource"])) { + mailbox_edit_resource($_POST); + } + if (isset($_POST["mailbox_delete_domain"])) { + mailbox_delete_domain($_POST); + } + if (isset($_POST["mailbox_delete_alias"])) { + mailbox_delete_alias($_POST); + } + if (isset($_POST["mailbox_delete_alias_domain"])) { + mailbox_delete_alias_domain($_POST); + } + if (isset($_POST["mailbox_delete_mailbox"])) { + mailbox_delete_mailbox($_POST); + } + if (isset($_POST["mailbox_delete_resource"])) { + mailbox_delete_resource($_POST); } } ?> diff --git a/data/web/inc/vars.inc.php b/data/web/inc/vars.inc.php index 5c41e6d3..db3f4219 100644 --- a/data/web/inc/vars.inc.php +++ b/data/web/inc/vars.inc.php @@ -1,5 +1,6 @@ diff --git a/data/web/index.php b/data/web/index.php index e6659149..31bafda6 100644 --- a/data/web/index.php +++ b/data/web/index.php @@ -48,6 +48,7 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; diff --git a/data/web/js/add.js b/data/web/js/add.js index e3089c53..1ebcdbb2 100644 --- a/data/web/js/add.js +++ b/data/web/js/add.js @@ -2,13 +2,15 @@ $(document).ready(function() { // add.php // Get max. possible quota for a domain when domain field changes $('#addSelectDomain').on('change', function() { - $.get("add.php", { js:"remaining_specs", domain:this.value, object:"new" }, function(data){ - if (data != '0') { - $("#quotaBadge").html('max. ' + data + ' MiB'); - $('#addInputQuota').attr({"disabled": false, "value": "", "type": "number", "max": data}); + $.get("json_api.php", { action:"get_domain_details", object:this.value }, function(data){ + var result = jQuery.parseJSON( data ); + max_new_mailbox_quota = ( result.max_new_mailbox_quota / 1048576); + if (max_new_mailbox_quota != '0') { + $("#quotaBadge").html('max. ' + max_new_mailbox_quota + ' MiB'); + $('#addInputQuota').attr({"disabled": false, "value": "", "type": "number", "max": max_new_mailbox_quota}); } else { - $("#quotaBadge").html('max. ' + data + ' MiB'); + $("#quotaBadge").html('max. ' + max_new_mailbox_quota + ' MiB'); $('#addInputQuota').attr({"disabled": true, "value": "", "type": "text", "value": "n/a"}); } }); diff --git a/data/web/js/bootstrap-select.min.js b/data/web/js/bootstrap-select.min.js new file mode 100644 index 00000000..d2a33149 --- /dev/null +++ b/data/web/js/bootstrap-select.min.js @@ -0,0 +1,9 @@ +/*! + * Bootstrap-select v1.12.2 (http://silviomoreto.github.io/bootstrap-select) + * + * Copyright 2013-2017 bootstrap-select + * Licensed under MIT (https://github.com/silviomoreto/bootstrap-select/blob/master/LICENSE) + */ +!function(a,b){"function"==typeof define&&define.amd?define(["jquery"],function(a){return b(a)}):"object"==typeof module&&module.exports?module.exports=b(require("jquery")):b(a.jQuery)}(this,function(a){!function(a){"use strict";function b(b){var c=[{re:/[\xC0-\xC6]/g,ch:"A"},{re:/[\xE0-\xE6]/g,ch:"a"},{re:/[\xC8-\xCB]/g,ch:"E"},{re:/[\xE8-\xEB]/g,ch:"e"},{re:/[\xCC-\xCF]/g,ch:"I"},{re:/[\xEC-\xEF]/g,ch:"i"},{re:/[\xD2-\xD6]/g,ch:"O"},{re:/[\xF2-\xF6]/g,ch:"o"},{re:/[\xD9-\xDC]/g,ch:"U"},{re:/[\xF9-\xFC]/g,ch:"u"},{re:/[\xC7-\xE7]/g,ch:"c"},{re:/[\xD1]/g,ch:"N"},{re:/[\xF1]/g,ch:"n"}];return a.each(c,function(){b=b?b.replace(this.re,this.ch):""}),b}function c(b){var c=arguments,d=b;[].shift.apply(c);var e,f=this.each(function(){var b=a(this);if(b.is("select")){var f=b.data("selectpicker"),g="object"==typeof d&&d;if(f){if(g)for(var h in g)g.hasOwnProperty(h)&&(f.options[h]=g[h])}else{var i=a.extend({},k.DEFAULTS,a.fn.selectpicker.defaults||{},b.data(),g);i.template=a.extend({},k.DEFAULTS.template,a.fn.selectpicker.defaults?a.fn.selectpicker.defaults.template:{},b.data().template,g.template),b.data("selectpicker",f=new k(this,i))}"string"==typeof d&&(e=f[d]instanceof Function?f[d].apply(f,c):f.options[d])}});return"undefined"!=typeof e?e:f}String.prototype.includes||!function(){var a={}.toString,b=function(){try{var a={},b=Object.defineProperty,c=b(a,a,a)&&b}catch(a){}return c}(),c="".indexOf,d=function(b){if(null==this)throw new TypeError;var d=String(this);if(b&&"[object RegExp]"==a.call(b))throw new TypeError;var e=d.length,f=String(b),g=f.length,h=arguments.length>1?arguments[1]:void 0,i=h?Number(h):0;i!=i&&(i=0);var j=Math.min(Math.max(i,0),e);return!(g+j>e)&&c.call(d,f,i)!=-1};b?b(String.prototype,"includes",{value:d,configurable:!0,writable:!0}):String.prototype.includes=d}(),String.prototype.startsWith||!function(){var a=function(){try{var a={},b=Object.defineProperty,c=b(a,a,a)&&b}catch(a){}return c}(),b={}.toString,c=function(a){if(null==this)throw new TypeError;var c=String(this);if(a&&"[object RegExp]"==b.call(a))throw new TypeError;var d=c.length,e=String(a),f=e.length,g=arguments.length>1?arguments[1]:void 0,h=g?Number(g):0;h!=h&&(h=0);var i=Math.min(Math.max(h,0),d);if(f+i>d)return!1;for(var j=-1;++j":">",'"':""","'":"'","`":"`"},g={"&":"&","<":"<",">":">",""":'"',"'":"'","`":"`"},h=function(a){var b=function(b){return a[b]},c="(?:"+Object.keys(a).join("|")+")",d=RegExp(c),e=RegExp(c,"g");return function(a){return a=null==a?"":""+a,d.test(a)?a.replace(e,b):a}},i=h(f),j=h(g),k=function(b,c){d.useDefault||(a.valHooks.select.set=d._set,d.useDefault=!0),this.$element=a(b),this.$newElement=null,this.$button=null,this.$menu=null,this.$lis=null,this.options=c,null===this.options.title&&(this.options.title=this.$element.attr("title"));var e=this.options.windowPadding;"number"==typeof e&&(this.options.windowPadding=[e,e,e,e]),this.val=k.prototype.val,this.render=k.prototype.render,this.refresh=k.prototype.refresh,this.setStyle=k.prototype.setStyle,this.selectAll=k.prototype.selectAll,this.deselectAll=k.prototype.deselectAll,this.destroy=k.prototype.destroy,this.remove=k.prototype.remove,this.show=k.prototype.show,this.hide=k.prototype.hide,this.init()};k.VERSION="1.12.2",k.DEFAULTS={noneSelectedText:"Nothing selected",noneResultsText:"No results matched {0}",countSelectedText:function(a,b){return 1==a?"{0} item selected":"{0} items selected"},maxOptionsText:function(a,b){return[1==a?"Limit reached ({n} item max)":"Limit reached ({n} items max)",1==b?"Group limit reached ({n} item max)":"Group limit reached ({n} items max)"]},selectAllText:"Select All",deselectAllText:"Deselect All",doneButton:!1,doneButtonText:"Close",multipleSeparator:", ",styleBase:"btn",style:"btn-default",size:"auto",title:null,selectedTextFormat:"values",width:!1,container:!1,hideDisabled:!1,showSubtext:!1,showIcon:!0,showContent:!0,dropupAuto:!0,header:!1,liveSearch:!1,liveSearchPlaceholder:null,liveSearchNormalize:!1,liveSearchStyle:"contains",actionsBox:!1,iconBase:"glyphicon",tickIcon:"glyphicon-ok",showTick:!1,template:{caret:''},maxOptions:!1,mobile:!1,selectOnTab:!1,dropdownAlignRight:!1,windowPadding:0},k.prototype={constructor:k,init:function(){var b=this,c=this.$element.attr("id");this.$element.addClass("bs-select-hidden"),this.liObj={},this.multiple=this.$element.prop("multiple"),this.autofocus=this.$element.prop("autofocus"),this.$newElement=this.createView(),this.$element.after(this.$newElement).appendTo(this.$newElement),this.$button=this.$newElement.children("button"),this.$menu=this.$newElement.children(".dropdown-menu"),this.$menuInner=this.$menu.children(".inner"),this.$searchbox=this.$menu.find("input"),this.$element.removeClass("bs-select-hidden"),this.options.dropdownAlignRight===!0&&this.$menu.addClass("dropdown-menu-right"),"undefined"!=typeof c&&(this.$button.attr("data-id",c),a('label[for="'+c+'"]').click(function(a){a.preventDefault(),b.$button.focus()})),this.checkDisabled(),this.clickListener(),this.options.liveSearch&&this.liveSearchListener(),this.render(),this.setStyle(),this.setWidth(),this.options.container&&this.selectPosition(),this.$menu.data("this",this),this.$newElement.data("this",this),this.options.mobile&&this.mobile(),this.$newElement.on({"hide.bs.dropdown":function(a){b.$menuInner.attr("aria-expanded",!1),b.$element.trigger("hide.bs.select",a)},"hidden.bs.dropdown":function(a){b.$element.trigger("hidden.bs.select",a)},"show.bs.dropdown":function(a){b.$menuInner.attr("aria-expanded",!0),b.$element.trigger("show.bs.select",a)},"shown.bs.dropdown":function(a){b.$element.trigger("shown.bs.select",a)}}),b.$element[0].hasAttribute("required")&&this.$element.on("invalid",function(){b.$button.addClass("bs-invalid").focus(),b.$element.on({"focus.bs.select":function(){b.$button.focus(),b.$element.off("focus.bs.select")},"shown.bs.select":function(){b.$element.val(b.$element.val()).off("shown.bs.select")},"rendered.bs.select":function(){this.validity.valid&&b.$button.removeClass("bs-invalid"),b.$element.off("rendered.bs.select")}})}),setTimeout(function(){b.$element.trigger("loaded.bs.select")})},createDropdown:function(){var b=this.multiple||this.options.showTick?" show-tick":"",c=this.$element.parent().hasClass("input-group")?" input-group-btn":"",d=this.autofocus?" autofocus":"",e=this.options.header?'
'+this.options.header+"
":"",f=this.options.liveSearch?'':"",g=this.multiple&&this.options.actionsBox?'
":"",h=this.multiple&&this.options.doneButton?'
":"",j='
";return a(j)},createView:function(){var a=this.createDropdown(),b=this.createLi();return a.find("ul")[0].innerHTML=b,a},reloadLi:function(){var a=this.createLi();this.$menuInner[0].innerHTML=a},createLi:function(){var c=this,d=[],e=0,f=document.createElement("option"),g=-1,h=function(a,b,c,d){return""+a+""},j=function(d,e,f,g){return''+d+''};if(this.options.title&&!this.multiple&&(g--,!this.$element.find(".bs-title-option").length)){var k=this.$element[0];f.className="bs-title-option",f.innerHTML=this.options.title,f.value="",k.insertBefore(f,k.firstChild);var l=a(k.options[k.selectedIndex]);void 0===l.attr("selected")&&void 0===this.$element.data("selected")&&(f.selected=!0)}return this.$element.find("option").each(function(b){var f=a(this);if(g++,!f.hasClass("bs-title-option")){var k=this.className||"",l=this.style.cssText,m=f.data("content")?f.data("content"):f.html(),n=f.data("tokens")?f.data("tokens"):null,o="undefined"!=typeof f.data("subtext")?''+f.data("subtext")+"":"",p="undefined"!=typeof f.data("icon")?' ':"",q=f.parent(),r="OPTGROUP"===q[0].tagName,s=r&&q[0].disabled,t=this.disabled||s;if(""!==p&&t&&(p=""+p+""),c.options.hideDisabled&&(t&&!r||s))return void g--;if(f.data("content")||(m=p+''+m+o+""),r&&f.data("divider")!==!0){if(c.options.hideDisabled&&t){if(void 0===q.data("allOptionsDisabled")){var u=q.children();q.data("allOptionsDisabled",u.filter(":disabled").length===u.length)}if(q.data("allOptionsDisabled"))return void g--}var v=" "+q[0].className||"";if(0===f.index()){e+=1;var w=q[0].label,x="undefined"!=typeof q.data("subtext")?''+q.data("subtext")+"":"",y=q.data("icon")?' ':"";w=y+''+i(w)+x+"",0!==b&&d.length>0&&(g++,d.push(h("",null,"divider",e+"div"))),g++,d.push(h(w,null,"dropdown-header"+v,e))}if(c.options.hideDisabled&&t)return void g--;d.push(h(j(m,"opt "+k+v,l,n),b,"",e))}else if(f.data("divider")===!0)d.push(h("",b,"divider"));else if(f.data("hidden")===!0)d.push(h(j(m,k,l,n),b,"hidden is-hidden"));else{var z=this.previousElementSibling&&"OPTGROUP"===this.previousElementSibling.tagName;if(!z&&c.options.hideDisabled)for(var A=a(this).prevAll(),B=0;B ':"";return b=d.options.showSubtext&&c.data("subtext")&&!d.multiple?' '+c.data("subtext")+"":"","undefined"!=typeof c.attr("title")?c.attr("title"):c.data("content")&&d.options.showContent?c.data("content").toString():e+c.html()+b}}).toArray(),f=this.multiple?e.join(this.options.multipleSeparator):e[0];if(this.multiple&&this.options.selectedTextFormat.indexOf("count")>-1){var g=this.options.selectedTextFormat.split(">");if(g.length>1&&e.length>g[1]||1==g.length&&e.length>=2){c=this.options.hideDisabled?", [disabled]":"";var h=this.$element.find("option").not('[data-divider="true"], [data-hidden="true"]'+c).length,i="function"==typeof this.options.countSelectedText?this.options.countSelectedText(e.length,h):this.options.countSelectedText;f=i.replace("{0}",e.length.toString()).replace("{1}",h.toString())}}void 0==this.options.title&&(this.options.title=this.$element.attr("title")),"static"==this.options.selectedTextFormat&&(f=this.options.title),f||(f="undefined"!=typeof this.options.title?this.options.title:this.options.noneSelectedText),this.$button.attr("title",j(a.trim(f.replace(/<[^>]*>?/g,"")))),this.$button.children(".filter-option").html(f),this.$element.trigger("rendered.bs.select")},setStyle:function(a,b){this.$element.attr("class")&&this.$newElement.addClass(this.$element.attr("class").replace(/selectpicker|mobile-device|bs-select-hidden|validate\[.*\]/gi,""));var c=a?a:this.options.style;"add"==b?this.$button.addClass(c):"remove"==b?this.$button.removeClass(c):(this.$button.removeClass(this.options.style),this.$button.addClass(c))},liHeight:function(b){if(b||this.options.size!==!1&&!this.sizeInfo){var c=document.createElement("div"),d=document.createElement("div"),e=document.createElement("ul"),f=document.createElement("li"),g=document.createElement("li"),h=document.createElement("a"),i=document.createElement("span"),j=this.options.header&&this.$menu.find(".popover-title").length>0?this.$menu.find(".popover-title")[0].cloneNode(!0):null,k=this.options.liveSearch?document.createElement("div"):null,l=this.options.actionsBox&&this.multiple&&this.$menu.find(".bs-actionsbox").length>0?this.$menu.find(".bs-actionsbox")[0].cloneNode(!0):null,m=this.options.doneButton&&this.multiple&&this.$menu.find(".bs-donebutton").length>0?this.$menu.find(".bs-donebutton")[0].cloneNode(!0):null;if(i.className="text",c.className=this.$menu[0].parentNode.className+" open",d.className="dropdown-menu open",e.className="dropdown-menu inner",f.className="divider",i.appendChild(document.createTextNode("Inner text")),h.appendChild(i),g.appendChild(h),e.appendChild(g),e.appendChild(f),j&&d.appendChild(j),k){var n=document.createElement("input");k.className="bs-searchbox",n.className="form-control",k.appendChild(n),d.appendChild(k)}l&&d.appendChild(l),d.appendChild(e),m&&d.appendChild(m),c.appendChild(d),document.body.appendChild(c);var o=h.offsetHeight,p=j?j.offsetHeight:0,q=k?k.offsetHeight:0,r=l?l.offsetHeight:0,s=m?m.offsetHeight:0,t=a(f).outerHeight(!0),u="function"==typeof getComputedStyle&&getComputedStyle(d),v=u?null:a(d),w={vert:parseInt(u?u.paddingTop:v.css("paddingTop"))+parseInt(u?u.paddingBottom:v.css("paddingBottom"))+parseInt(u?u.borderTopWidth:v.css("borderTopWidth"))+parseInt(u?u.borderBottomWidth:v.css("borderBottomWidth")),horiz:parseInt(u?u.paddingLeft:v.css("paddingLeft"))+parseInt(u?u.paddingRight:v.css("paddingRight"))+parseInt(u?u.borderLeftWidth:v.css("borderLeftWidth"))+parseInt(u?u.borderRightWidth:v.css("borderRightWidth"))},x={vert:w.vert+parseInt(u?u.marginTop:v.css("marginTop"))+parseInt(u?u.marginBottom:v.css("marginBottom"))+2,horiz:w.horiz+parseInt(u?u.marginLeft:v.css("marginLeft"))+parseInt(u?u.marginRight:v.css("marginRight"))+2};document.body.removeChild(c),this.sizeInfo={liHeight:o,headerHeight:p,searchHeight:q,actionsHeight:r,doneButtonHeight:s,dividerHeight:t,menuPadding:w,menuExtras:x}}},setSize:function(){if(this.findLis(),this.liHeight(),this.options.header&&this.$menu.css("padding-top",0),this.options.size!==!1){var b,c,d,e,f,g,h,i,j=this,k=this.$menu,l=this.$menuInner,m=a(window),n=this.$newElement[0].offsetHeight,o=this.$newElement[0].offsetWidth,p=this.sizeInfo.liHeight,q=this.sizeInfo.headerHeight,r=this.sizeInfo.searchHeight,s=this.sizeInfo.actionsHeight,t=this.sizeInfo.doneButtonHeight,u=this.sizeInfo.dividerHeight,v=this.sizeInfo.menuPadding,w=this.sizeInfo.menuExtras,x=this.options.hideDisabled?".disabled":"",y=function(){var b,c=j.$newElement.offset(),d=a(j.options.container);j.options.container&&!d.is("body")?(b=d.offset(),b.top+=parseInt(d.css("borderTopWidth")),b.left+=parseInt(d.css("borderLeftWidth"))):b={top:0,left:0};var e=j.options.windowPadding;f=c.top-b.top-m.scrollTop(),g=m.height()-f-n-b.top-e[2],h=c.left-b.left-m.scrollLeft(),i=m.width()-h-o-b.left-e[1],f-=e[0],h-=e[3]};if(y(),"auto"===this.options.size){var z=function(){var m,n=function(b,c){return function(d){return c?d.classList?d.classList.contains(b):a(d).hasClass(b):!(d.classList?d.classList.contains(b):a(d).hasClass(b))}},u=j.$menuInner[0].getElementsByTagName("li"),x=Array.prototype.filter?Array.prototype.filter.call(u,n("hidden",!1)):j.$lis.not(".hidden"),z=Array.prototype.filter?Array.prototype.filter.call(x,n("dropdown-header",!0)):x.filter(".dropdown-header");y(),b=g-w.vert,c=i-w.horiz,j.options.container?(k.data("height")||k.data("height",k.height()),d=k.data("height"),k.data("width")||k.data("width",k.width()),e=k.data("width")):(d=k.height(),e=k.width()),j.options.dropupAuto&&j.$newElement.toggleClass("dropup",f>g&&b-w.verti&&c-w.horiz3?3*p+w.vert-2:0,k.css({"max-height":b+"px",overflow:"hidden","min-height":m+q+r+s+t+"px"}),l.css({"max-height":b-q-r-s-t-v.vert+"px","overflow-y":"auto","min-height":Math.max(m-v.vert,0)+"px"})};z(),this.$searchbox.off("input.getSize propertychange.getSize").on("input.getSize propertychange.getSize",z),m.off("resize.getSize scroll.getSize").on("resize.getSize scroll.getSize",z)}else if(this.options.size&&"auto"!=this.options.size&&this.$lis.not(x).length>this.options.size){var A=this.$lis.not(".divider").not(x).children().slice(0,this.options.size).last().parent().index(),B=this.$lis.slice(0,A+1).filter(".divider").length;b=p*this.options.size+B*u+v.vert,j.options.container?(k.data("height")||k.data("height",k.height()),d=k.data("height")):d=k.height(),j.options.dropupAuto&&this.$newElement.toggleClass("dropup",f>g&&b-w.vert');var b,c,d,e=this,f=a(this.options.container),g=function(a){e.$bsContainer.addClass(a.attr("class").replace(/form-control|fit-width/gi,"")).toggleClass("dropup",a.hasClass("dropup")),b=a.offset(),f.is("body")?c={top:0,left:0}:(c=f.offset(),c.top+=parseInt(f.css("borderTopWidth"))-f.scrollTop(),c.left+=parseInt(f.css("borderLeftWidth"))-f.scrollLeft()),d=a.hasClass("dropup")?0:a[0].offsetHeight,e.$bsContainer.css({top:b.top-c.top+d,left:b.left-c.left,width:a[0].offsetWidth})};this.$button.on("click",function(){var b=a(this);e.isDisabled()||(g(e.$newElement),e.$bsContainer.appendTo(e.options.container).toggleClass("open",!b.hasClass("open")).append(e.$menu))}),a(window).on("resize scroll",function(){g(e.$newElement)}),this.$element.on("hide.bs.select",function(){e.$menu.data("height",e.$menu.height()),e.$bsContainer.detach()})},setSelected:function(a,b,c){c||(this.togglePlaceholder(),c=this.findLis().eq(this.liObj[a])),c.toggleClass("selected",b).find("a").attr("aria-selected",b)},setDisabled:function(a,b,c){c||(c=this.findLis().eq(this.liObj[a])),b?c.addClass("disabled").children("a").attr("href","#").attr("tabindex",-1).attr("aria-disabled",!0):c.removeClass("disabled").children("a").removeAttr("href").attr("tabindex",0).attr("aria-disabled",!1)},isDisabled:function(){return this.$element[0].disabled},checkDisabled:function(){var a=this;this.isDisabled()?(this.$newElement.addClass("disabled"),this.$button.addClass("disabled").attr("tabindex",-1).attr("aria-disabled",!0)):(this.$button.hasClass("disabled")&&(this.$newElement.removeClass("disabled"),this.$button.removeClass("disabled").attr("aria-disabled",!1)),this.$button.attr("tabindex")!=-1||this.$element.data("tabindex")||this.$button.removeAttr("tabindex")),this.$button.click(function(){return!a.isDisabled()})},togglePlaceholder:function(){var a=this.$element.val();this.$button.toggleClass("bs-placeholder",null===a||""===a||a.constructor===Array&&0===a.length)},tabIndex:function(){this.$element.data("tabindex")!==this.$element.attr("tabindex")&&this.$element.attr("tabindex")!==-98&&"-98"!==this.$element.attr("tabindex")&&(this.$element.data("tabindex",this.$element.attr("tabindex")),this.$button.attr("tabindex",this.$element.data("tabindex"))),this.$element.attr("tabindex",-98)},clickListener:function(){var b=this,c=a(document);c.data("spaceSelect",!1),this.$button.on("keyup",function(a){/(32)/.test(a.keyCode.toString(10))&&c.data("spaceSelect")&&(a.preventDefault(),c.data("spaceSelect",!1))}),this.$button.on("click",function(){b.setSize()}),this.$element.on("shown.bs.select",function(){if(b.options.liveSearch||b.multiple){if(!b.multiple){var a=b.liObj[b.$element[0].selectedIndex];if("number"!=typeof a||b.options.size===!1)return;var c=b.$lis.eq(a)[0].offsetTop-b.$menuInner[0].offsetTop;c=c-b.$menuInner[0].offsetHeight/2+b.sizeInfo.liHeight/2,b.$menuInner[0].scrollTop=c}}else b.$menuInner.find(".selected a").focus()}),this.$menuInner.on("click","li a",function(c){var d=a(this),f=d.parent().data("originalIndex"),g=b.$element.val(),h=b.$element.prop("selectedIndex"),i=!0;if(b.multiple&&1!==b.options.maxOptions&&c.stopPropagation(),c.preventDefault(),!b.isDisabled()&&!d.parent().hasClass("disabled")){var j=b.$element.find("option"),k=j.eq(f),l=k.prop("selected"),m=k.parent("optgroup"),n=b.options.maxOptions,o=m.data("maxOptions")||!1;if(b.multiple){if(k.prop("selected",!l),b.setSelected(f,!l),d.blur(),n!==!1||o!==!1){var p=n
');t[2]&&(u=u.replace("{var}",t[2][n>1?0:1]),v=v.replace("{var}",t[2][o>1?0:1])),k.prop("selected",!1),b.$menu.append(w),n&&p&&(w.append(a("
"+u+"
")),i=!1,b.$element.trigger("maxReached.bs.select")),o&&q&&(w.append(a("
"+v+"
")),i=!1,b.$element.trigger("maxReachedGrp.bs.select")),setTimeout(function(){b.setSelected(f,!1)},10),w.delay(750).fadeOut(300,function(){a(this).remove()})}}}else j.prop("selected",!1),k.prop("selected",!0),b.$menuInner.find(".selected").removeClass("selected").find("a").attr("aria-selected",!1),b.setSelected(f,!0);!b.multiple||b.multiple&&1===b.options.maxOptions?b.$button.focus():b.options.liveSearch&&b.$searchbox.focus(),i&&(g!=b.$element.val()&&b.multiple||h!=b.$element.prop("selectedIndex")&&!b.multiple)&&(e=[f,k.prop("selected"),l],b.$element.triggerNative("change"))}}),this.$menu.on("click","li.disabled a, .popover-title, .popover-title :not(.close)",function(c){c.currentTarget==this&&(c.preventDefault(),c.stopPropagation(),b.options.liveSearch&&!a(c.target).hasClass("close")?b.$searchbox.focus():b.$button.focus())}),this.$menuInner.on("click",".divider, .dropdown-header",function(a){a.preventDefault(),a.stopPropagation(),b.options.liveSearch?b.$searchbox.focus():b.$button.focus()}),this.$menu.on("click",".popover-title .close",function(){b.$button.click()}),this.$searchbox.on("click",function(a){a.stopPropagation()}),this.$menu.on("click",".actions-btn",function(c){b.options.liveSearch?b.$searchbox.focus():b.$button.focus(),c.preventDefault(),c.stopPropagation(),a(this).hasClass("bs-select-all")?b.selectAll():b.deselectAll()}),this.$element.change(function(){b.render(!1),b.$element.trigger("changed.bs.select",e),e=null})},liveSearchListener:function(){var c=this,d=a('
  • ');this.$button.on("click.dropdown.data-api",function(){c.$menuInner.find(".active").removeClass("active"),c.$searchbox.val()&&(c.$searchbox.val(""),c.$lis.not(".is-hidden").removeClass("hidden"),d.parent().length&&d.remove()),c.multiple||c.$menuInner.find(".selected").addClass("active"),setTimeout(function(){c.$searchbox.focus()},10)}),this.$searchbox.on("click.dropdown.data-api focus.dropdown.data-api touchend.dropdown.data-api",function(a){a.stopPropagation()}),this.$searchbox.on("input propertychange",function(){if(c.$lis.not(".is-hidden").removeClass("hidden"),c.$lis.filter(".active").removeClass("active"),d.remove(),c.$searchbox.val()){var e,f=c.$lis.not(".is-hidden, .divider, .dropdown-header");if(e=c.options.liveSearchNormalize?f.find("a").not(":a"+c._searchStyle()+'("'+b(c.$searchbox.val())+'")'):f.find("a").not(":"+c._searchStyle()+'("'+c.$searchbox.val()+'")'),e.length===f.length)d.html(c.options.noneResultsText.replace("{0}",'"'+i(c.$searchbox.val())+'"')),c.$menuInner.append(d),c.$lis.addClass("hidden");else{e.parent().addClass("hidden");var g,h=c.$lis.not(".hidden");h.each(function(b){var c=a(this);c.hasClass("divider")?void 0===g?c.addClass("hidden"):(g&&g.addClass("hidden"),g=c):c.hasClass("dropdown-header")&&h.eq(b+1).data("optgroup")!==c.data("optgroup")?c.addClass("hidden"):g=null}),g&&g.addClass("hidden"),f.not(".hidden").first().addClass("active")}}})},_searchStyle:function(){var a={begins:"ibegins",startsWith:"ibegins"};return a[this.options.liveSearchStyle]||"icontains"},val:function(a){return"undefined"!=typeof a?(this.$element.val(a),this.render(),this.$element):this.$element.val()},changeAll:function(b){if(this.multiple){"undefined"==typeof b&&(b=!0),this.findLis();var c=this.$element.find("option"),d=this.$lis.not(".divider, .dropdown-header, .disabled, .hidden"),e=d.length,f=[];if(b){if(d.filter(".selected").length===d.length)return}else if(0===d.filter(".selected").length)return;d.toggleClass("selected",b);for(var g=0;g=48&&c.keyCode<=57||c.keyCode>=96&&c.keyCode<=105||c.keyCode>=65&&c.keyCode<=90))return o.options.container?o.$button.trigger("click"):(o.setSize(),o.$menu.parent().addClass("open"),l=!0),void o.$searchbox.focus();if(o.options.liveSearch&&(/(^9$|27)/.test(c.keyCode.toString(10))&&l&&(c.preventDefault(),c.stopPropagation(),o.$menuInner.click(),o.$button.focus()),d=a('[role="listbox"] li'+p,n),m.val()||/(38|40)/.test(c.keyCode.toString(10))||0===d.filter(".active").length&&(d=o.$menuInner.find("li"),d=o.options.liveSearchNormalize?d.filter(":a"+o._searchStyle()+"("+b(q[c.keyCode])+")"):d.filter(":"+o._searchStyle()+"("+q[c.keyCode]+")"))),d.length){if(/(38|40)/.test(c.keyCode.toString(10)))e=d.index(d.find("a").filter(":focus").parent()),g=d.filter(p).first().index(),h=d.filter(p).last().index(),f=d.eq(e).nextAll(p).eq(0).index(),i=d.eq(e).prevAll(p).eq(0).index(),j=d.eq(f).prevAll(p).eq(0).index(),o.options.liveSearch&&(d.each(function(b){a(this).hasClass("disabled")||a(this).data("index",b)}),e=d.index(d.filter(".active")),g=d.first().data("index"),h=d.last().data("index"),f=d.eq(e).nextAll().eq(0).data("index"),i=d.eq(e).prevAll().eq(0).data("index"),j=d.eq(f).prevAll().eq(0).data("index")),k=m.data("prevIndex"),38==c.keyCode?(o.options.liveSearch&&e--,e!=j&&e>i&&(e=i),eh&&(e=h),e==k&&(e=g)),m.data("prevIndex",e),o.options.liveSearch?(c.preventDefault(),m.hasClass("dropdown-toggle")||(d.removeClass("active").eq(e).addClass("active").children("a").focus(),m.focus())):d.eq(e).children("a").focus();else if(!m.is("input")){var r,s,t=[];d.each(function(){a(this).hasClass("disabled")||a.trim(a(this).children("a").text().toLowerCase()).substring(0,1)==q[c.keyCode]&&t.push(a(this).index())}),r=a(document).data("keycount"),r++,a(document).data("keycount",r),s=a.trim(a(":focus").text().toLowerCase()).substring(0,1),s!=q[c.keyCode]?(r=1,a(document).data("keycount",r)):r>=t.length&&(a(document).data("keycount",0),r>t.length&&(r=1)),d.eq(t[r-1]).children("a").focus()}if((/(13|32)/.test(c.keyCode.toString(10))||/(^9$)/.test(c.keyCode.toString(10))&&o.options.selectOnTab)&&l){if(/(32)/.test(c.keyCode.toString(10))||c.preventDefault(),o.options.liveSearch)/(32)/.test(c.keyCode.toString(10))||(o.$menuInner.find(".active a").click(), +m.focus());else{var u=a(":focus");u.click(),u.focus(),c.preventDefault(),a(document).data("spaceSelect",!0)}a(document).data("keycount",0)}(/(^9$|27)/.test(c.keyCode.toString(10))&&l&&(o.multiple||o.options.liveSearch)||/(27)/.test(c.keyCode.toString(10))&&!l)&&(o.$menu.parent().removeClass("open"),o.options.container&&o.$newElement.removeClass("open"),o.$button.focus())}},mobile:function(){this.$element.addClass("mobile-device")},refresh:function(){this.$lis=null,this.liObj={},this.reloadLi(),this.render(),this.checkDisabled(),this.liHeight(!0),this.setStyle(),this.setWidth(),this.$lis&&this.$searchbox.trigger("propertychange"),this.$element.trigger("refreshed.bs.select")},hide:function(){this.$newElement.hide()},show:function(){this.$newElement.show()},remove:function(){this.$newElement.remove(),this.$element.remove()},destroy:function(){this.$newElement.before(this.$element).remove(),this.$bsContainer?this.$bsContainer.remove():this.$menu.remove(),this.$element.off(".bs.select").removeData("selectpicker").removeClass("bs-select-hidden selectpicker")}};var l=a.fn.selectpicker;a.fn.selectpicker=c,a.fn.selectpicker.Constructor=k,a.fn.selectpicker.noConflict=function(){return a.fn.selectpicker=l,this},a(document).data("keycount",0).on("keydown.bs.select",'.bootstrap-select [data-toggle=dropdown], .bootstrap-select [role="listbox"], .bs-searchbox input',k.prototype.keydown).on("focusin.modal",'.bootstrap-select [data-toggle=dropdown], .bootstrap-select [role="listbox"], .bs-searchbox input',function(a){a.stopPropagation()}),a(window).on("load.bs.select.data-api",function(){a(".selectpicker").each(function(){var b=a(this);c.call(b,b.data())})})}(a)}); +//# sourceMappingURL=bootstrap-select.js.map \ No newline at end of file diff --git a/data/web/js/bootstrap-slider.min.js b/data/web/js/bootstrap-slider.min.js new file mode 100644 index 00000000..72363a27 --- /dev/null +++ b/data/web/js/bootstrap-slider.min.js @@ -0,0 +1,5 @@ +/*! ======================================================= + VERSION 9.7.2 +========================================================= */ +"use strict";var _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(a){return typeof a}:function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a},windowIsDefined="object"===("undefined"==typeof window?"undefined":_typeof(window));!function(a){if("function"==typeof define&&define.amd)define(["jquery"],a);else if("object"===("undefined"==typeof module?"undefined":_typeof(module))&&module.exports){var b;try{b=require("jquery")}catch(c){b=null}module.exports=a(b)}else window&&(window.Slider=a(window.jQuery))}(function(a){var b="slider",c="bootstrapSlider";windowIsDefined&&!window.console&&(window.console={}),windowIsDefined&&!window.console.log&&(window.console.log=function(){}),windowIsDefined&&!window.console.warn&&(window.console.warn=function(){});var d;return function(a){function b(){}function c(a){function c(b){b.prototype.option||(b.prototype.option=function(b){a.isPlainObject(b)&&(this.options=a.extend(!0,this.options,b))})}function e(b,c){a.fn[b]=function(e){if("string"==typeof e){for(var g=d.call(arguments,1),h=0,i=this.length;i>h;h++){var j=this[h],k=a.data(j,b);if(k)if(a.isFunction(k[e])&&"_"!==e.charAt(0)){var l=k[e].apply(k,g);if(void 0!==l&&l!==k)return l}else f("no such method '"+e+"' for "+b+" instance");else f("cannot call methods on "+b+" prior to initialization; attempted to call '"+e+"'")}return this}var m=this.map(function(){var d=a.data(this,b);return d?(d.option(e),d._init()):(d=new c(this,e),a.data(this,b,d)),a(this)});return!m||m.length>1?m:m[0]}}if(a){var f="undefined"==typeof console?b:function(a){console.error(a)};return a.bridget=function(a,b){c(b),e(a,b)},a.bridget}}var d=Array.prototype.slice;c(a)}(a),function(a){function e(b,c){function d(a,b){var c="data-slider-"+b.replace(/_/g,"-"),d=a.getAttribute(c);try{return JSON.parse(d)}catch(e){return d}}this._state={value:null,enabled:null,offset:null,size:null,percentage:null,inDrag:!1,over:!1},this.ticksCallbackMap={},this.handleCallbackMap={},"string"==typeof b?this.element=document.querySelector(b):b instanceof HTMLElement&&(this.element=b),c=c?c:{};for(var e=Object.keys(this.defaultOptions),f=0;f0)for(var s=0;s0){for(this.ticksContainer=document.createElement("div"),this.ticksContainer.className="slider-tick-container",f=0;f0)for(this.tickLabelContainer=document.createElement("div"),this.tickLabelContainer.className="slider-tick-label-container",f=0;f0&&(this.options.max=Math.max.apply(Math,this.options.ticks),this.options.min=Math.min.apply(Math,this.options.ticks)),Array.isArray(this.options.value)?(this.options.range=!0,this._state.value=this.options.value):this.options.range?this._state.value=[this.options.value,this.options.max]:this._state.value=this.options.value,this.trackLow=k||this.trackLow,this.trackSelection=j||this.trackSelection,this.trackHigh=l||this.trackHigh,"none"===this.options.selection?(this._addClass(this.trackLow,"hide"),this._addClass(this.trackSelection,"hide"),this._addClass(this.trackHigh,"hide")):("after"===this.options.selection||"before"===this.options.selection)&&(this._removeClass(this.trackLow,"hide"),this._removeClass(this.trackSelection,"hide"),this._removeClass(this.trackHigh,"hide")),this.handle1=m||this.handle1,this.handle2=n||this.handle2,p===!0)for(this._removeClass(this.handle1,"round triangle"),this._removeClass(this.handle2,"round triangle hide"),f=0;f0){for(var d,e,f,g=0,h=1;hthis.options.max?this.options.max:k},toPercentage:function(a){if(this.options.max===this.options.min)return 0;if(this.options.ticks_positions.length>0){for(var b,c,d,e=0,f=0;f0?this.options.ticks[f-1]:0,d=f>0?this.options.ticks_positions[f-1]:0,c=this.options.ticks[f],e=this.options.ticks_positions[f];break}if(f>0){var g=(a-b)/(c-b);return d+g*(e-d)}}return 100*(a-this.options.min)/(this.options.max-this.options.min)}},logarithmic:{toValue:function(a){var b=0===this.options.min?0:Math.log(this.options.min),c=Math.log(this.options.max),d=Math.exp(b+(c-b)*a/100);return d=this.options.min+Math.round((d-this.options.min)/this.options.step)*this.options.step,dthis.options.max?this.options.max:d},toPercentage:function(a){if(this.options.max===this.options.min)return 0;var b=Math.log(this.options.max),c=0===this.options.min?0:Math.log(this.options.min),d=0===a?0:Math.log(a);return 100*(d-c)/(b-c)}}};d=function(a,b){return e.call(this,a,b),this},d.prototype={_init:function(){},constructor:d,defaultOptions:{id:"",min:0,max:10,step:1,precision:0,orientation:"horizontal",value:5,range:!1,selection:"before",tooltip:"show",tooltip_split:!1,handle:"round",reversed:!1,rtl:"auto",enabled:!0,formatter:function(a){return Array.isArray(a)?a[0]+" : "+a[1]:a},natural_arrow_keys:!1,ticks:[],ticks_positions:[],ticks_labels:[],ticks_snap_bounds:0,ticks_tooltip:!1,scale:"linear",focus:!1,tooltip_position:null,labelledby:null,rangeHighlights:[]},getElement:function(){return this.sliderElem},getValue:function(){return this.options.range?this._state.value:this._state.value[0]},setValue:function(a,b,c){a||(a=0);var d=this.getValue();this._state.value=this._validateInputValue(a);var e=this._applyPrecision.bind(this);this.options.range?(this._state.value[0]=e(this._state.value[0]),this._state.value[1]=e(this._state.value[1]),this._state.value[0]=Math.max(this.options.min,Math.min(this.options.max,this._state.value[0])),this._state.value[1]=Math.max(this.options.min,Math.min(this.options.max,this._state.value[1]))):(this._state.value=e(this._state.value),this._state.value=[Math.max(this.options.min,Math.min(this.options.max,this._state.value))],this._addClass(this.handle2,"hide"),"after"===this.options.selection?this._state.value[1]=this.options.max:this._state.value[1]=this.options.min),this.options.max>this.options.min?this._state.percentage=[this._toPercentage(this._state.value[0]),this._toPercentage(this._state.value[1]),100*this.options.step/(this.options.max-this.options.min)]:this._state.percentage=[0,0,100],this._layout();var f=this.options.range?this._state.value:this._state.value[0];return this._setDataVal(f),b===!0&&this._trigger("slide",f),d!==f&&c===!0&&this._trigger("change",{oldValue:d,newValue:f}),this},destroy:function(){this._removeSliderEventHandlers(),this.sliderElem.parentNode.removeChild(this.sliderElem),this.element.style.display="",this._cleanUpEventCallbacksMap(),this.element.removeAttribute("data"),a&&(this._unbindJQueryEventHandlers(),this.$element.removeData("slider"))},disable:function(){return this._state.enabled=!1,this.handle1.removeAttribute("tabindex"),this.handle2.removeAttribute("tabindex"),this._addClass(this.sliderElem,"slider-disabled"),this._trigger("slideDisabled"),this},enable:function(){return this._state.enabled=!0,this.handle1.setAttribute("tabindex",0),this.handle2.setAttribute("tabindex",0),this._removeClass(this.sliderElem,"slider-disabled"),this._trigger("slideEnabled"),this},toggle:function(){return this._state.enabled?this.disable():this.enable(),this},isEnabled:function(){return this._state.enabled},on:function(a,b){return this._bindNonQueryEventHandler(a,b),this},off:function(b,c){a?(this.$element.off(b,c),this.$sliderElem.off(b,c)):this._unbindNonQueryEventHandler(b,c)},getAttribute:function(a){return a?this.options[a]:this.options},setAttribute:function(a,b){return this.options[a]=b,this},refresh:function(){return this._removeSliderEventHandlers(),e.call(this,this.element,this.options),a&&a.data(this.element,"slider",this),this},relayout:function(){return this._resize(),this._layout(),this},_removeSliderEventHandlers:function(){if(this.handle1.removeEventListener("keydown",this.handle1Keydown,!1),this.handle2.removeEventListener("keydown",this.handle2Keydown,!1),this.options.ticks_tooltip){for(var a=this.ticksContainer.getElementsByClassName("slider-tick"),b=0;b=0?c:this.attributes["aria-valuenow"].value,e=parseInt(d,10);b.value[0]=e,b.percentage[0]=a.options.ticks_positions[e],a._setToolTipOnMouseOver(b),a._showTooltip()};return b.addEventListener("mouseenter",d,!1),d},addMouseLeave:function(a,b){var c=function(){a._hideTooltip()};return b.addEventListener("mouseleave",c,!1),c}}},_layout:function(){var a;if(a=this.options.reversed?[100-this._state.percentage[0],this.options.range?100-this._state.percentage[1]:this._state.percentage[1]]:[this._state.percentage[0],this._state.percentage[1]],this.handle1.style[this.stylePos]=a[0]+"%",this.handle1.setAttribute("aria-valuenow",this._state.value[0]),isNaN(this.options.formatter(this._state.value[0]))&&this.handle1.setAttribute("aria-valuetext",this.options.formatter(this._state.value[0])),this.handle2.style[this.stylePos]=a[1]+"%",this.handle2.setAttribute("aria-valuenow",this._state.value[1]),isNaN(this.options.formatter(this._state.value[1]))&&this.handle2.setAttribute("aria-valuetext",this.options.formatter(this._state.value[1])),this.rangeHighlightElements.length>0&&Array.isArray(this.options.rangeHighlights)&&this.options.rangeHighlights.length>0)for(var b=0;b0){var g,h="vertical"===this.options.orientation?"height":"width";g="vertical"===this.options.orientation?"marginTop":this.options.rtl?"marginRight":"marginLeft";var i=this._state.size/(this.options.ticks.length-1);if(this.tickLabelContainer){var j=0;if(0===this.options.ticks_positions.length)"vertical"!==this.options.orientation&&(this.tickLabelContainer.style[g]=-i/2+"px"),j=this.tickLabelContainer.offsetHeight;else for(k=0;kj&&(j=this.tickLabelContainer.childNodes[k].offsetHeight);"horizontal"===this.options.orientation&&(this.sliderElem.style.marginBottom=j+"px")}for(var k=0;k=a[0]&&l<=a[1]&&this._addClass(this.ticks[k],"in-selection"):"after"===this.options.selection&&l>=a[0]?this._addClass(this.ticks[k],"in-selection"):"before"===this.options.selection&&l<=a[0]&&this._addClass(this.ticks[k],"in-selection"),this.tickLabels[k]&&(this.tickLabels[k].style[h]=i+"px","vertical"!==this.options.orientation&&void 0!==this.options.ticks_positions[k]?(this.tickLabels[k].style.position="absolute",this.tickLabels[k].style[this.stylePos]=l+"%",this.tickLabels[k].style[g]=-i/2+"px"):"vertical"===this.options.orientation&&(this.options.rtl?this.tickLabels[k].style.marginRight=this.sliderElem.offsetWidth+"px":this.tickLabels[k].style.marginLeft=this.sliderElem.offsetWidth+"px",this.tickLabelContainer.style[g]=this.sliderElem.offsetWidth/2*-1+"px"))}}var m;if(this.options.range){m=this.options.formatter(this._state.value),this._setText(this.tooltipInner,m),this.tooltip.style[this.stylePos]=(a[1]+a[0])/2+"%","vertical"===this.options.orientation?this._css(this.tooltip,"margin-"+this.stylePos,-this.tooltip.offsetHeight/2+"px"):this._css(this.tooltip,"margin-"+this.stylePos,-this.tooltip.offsetWidth/2+"px");var n=this.options.formatter(this._state.value[0]);this._setText(this.tooltipInner_min,n);var o=this.options.formatter(this._state.value[1]);this._setText(this.tooltipInner_max,o),this.tooltip_min.style[this.stylePos]=a[0]+"%","vertical"===this.options.orientation?this._css(this.tooltip_min,"margin-"+this.stylePos,-this.tooltip_min.offsetHeight/2+"px"):this._css(this.tooltip_min,"margin-"+this.stylePos,-this.tooltip_min.offsetWidth/2+"px"),this.tooltip_max.style[this.stylePos]=a[1]+"%","vertical"===this.options.orientation?this._css(this.tooltip_max,"margin-"+this.stylePos,-this.tooltip_max.offsetHeight/2+"px"):this._css(this.tooltip_max,"margin-"+this.stylePos,-this.tooltip_max.offsetWidth/2+"px")}else m=this.options.formatter(this._state.value[0]),this._setText(this.tooltipInner,m),this.tooltip.style[this.stylePos]=a[0]+"%","vertical"===this.options.orientation?this._css(this.tooltip,"margin-"+this.stylePos,-this.tooltip.offsetHeight/2+"px"):this._css(this.tooltip,"margin-"+this.stylePos,-this.tooltip.offsetWidth/2+"px");if("vertical"===this.options.orientation)this.trackLow.style.top="0",this.trackLow.style.height=Math.min(a[0],a[1])+"%",this.trackSelection.style.top=Math.min(a[0],a[1])+"%",this.trackSelection.style.height=Math.abs(a[0]-a[1])+"%",this.trackHigh.style.bottom="0",this.trackHigh.style.height=100-Math.min(a[0],a[1])-Math.abs(a[0]-a[1])+"%";else{"right"===this.stylePos?this.trackLow.style.right="0":this.trackLow.style.left="0",this.trackLow.style.width=Math.min(a[0],a[1])+"%","right"===this.stylePos?this.trackSelection.style.right=Math.min(a[0],a[1])+"%":this.trackSelection.style.left=Math.min(a[0],a[1])+"%",this.trackSelection.style.width=Math.abs(a[0]-a[1])+"%","right"===this.stylePos?this.trackHigh.style.left="0":this.trackHigh.style.right="0",this.trackHigh.style.width=100-Math.min(a[0],a[1])-Math.abs(a[0]-a[1])+"%";var p=this.tooltip_min.getBoundingClientRect(),q=this.tooltip_max.getBoundingClientRect();"bottom"===this.options.tooltip_position?p.right>q.left?(this._removeClass(this.tooltip_max,"bottom"),this._addClass(this.tooltip_max,"top"),this.tooltip_max.style.top="",this.tooltip_max.style.bottom="22px"):(this._removeClass(this.tooltip_max,"top"),this._addClass(this.tooltip_max,"bottom"),this.tooltip_max.style.top=this.tooltip_min.style.top,this.tooltip_max.style.bottom=""):p.right>q.left?(this._removeClass(this.tooltip_max,"top"),this._addClass(this.tooltip_max,"bottom"),this.tooltip_max.style.top="18px"):(this._removeClass(this.tooltip_max,"bottom"),this._addClass(this.tooltip_max,"top"),this.tooltip_max.style.top=this.tooltip_min.style.top)}},_createHighlightRange:function(a,b){return this._isHighlightRange(a,b)?a>b?{start:b,size:a-b}:{start:a,size:b-a}:null},_isHighlightRange:function(a,b){return a>=0&&100>=a&&b>=0&&100>=b?!0:!1},_resize:function(a){this._state.offset=this._offset(this.sliderElem),this._state.size=this.sliderElem[this.sizePos],this._layout()},_removeProperty:function(a,b){a.style.removeProperty?a.style.removeProperty(b):a.style.removeAttribute(b)},_mousedown:function(a){if(!this._state.enabled)return!1;this._state.offset=this._offset(this.sliderElem),this._state.size=this.sliderElem[this.sizePos];var b=this._getPercentage(a);if(this.options.range){var c=Math.abs(this._state.percentage[0]-b),d=Math.abs(this._state.percentage[1]-b);this._state.dragged=d>c?0:1,this._adjustPercentageForRangeSliders(b)}else this._state.dragged=0;this._state.percentage[this._state.dragged]=b,this._layout(),this.touchCapable&&(document.removeEventListener("touchmove",this.mousemove,!1),document.removeEventListener("touchend",this.mouseup,!1)),this.mousemove&&document.removeEventListener("mousemove",this.mousemove,!1),this.mouseup&&document.removeEventListener("mouseup",this.mouseup,!1),this.mousemove=this._mousemove.bind(this),this.mouseup=this._mouseup.bind(this),this.touchCapable&&(document.addEventListener("touchmove",this.mousemove,!1),document.addEventListener("touchend",this.mouseup,!1)),document.addEventListener("mousemove",this.mousemove,!1),document.addEventListener("mouseup",this.mouseup,!1),this._state.inDrag=!0;var e=this._calculateValue();return this._trigger("slideStart",e),this._setDataVal(e),this.setValue(e,!1,!0),a.returnValue=!1,this.options.focus&&this._triggerFocusOnHandle(this._state.dragged),!0},_touchstart:function(a){if(void 0===a.changedTouches)return void this._mousedown(a);var b=a.changedTouches[0];this.touchX=b.pageX,this.touchY=b.pageY},_triggerFocusOnHandle:function(a){0===a&&this.handle1.focus(),1===a&&this.handle2.focus()},_keydown:function(a,b){if(!this._state.enabled)return!1;var c;switch(b.keyCode){case 37:case 40:c=-1;break;case 39:case 38:c=1}if(c){if(this.options.natural_arrow_keys){var d="vertical"===this.options.orientation&&!this.options.reversed,e="horizontal"===this.options.orientation&&this.options.reversed;(d||e)&&(c=-c)}var f=this._state.value[a]+c*this.options.step,g=f/this.options.max*100;if(this._state.keyCtrl=a,this.options.range){this._adjustPercentageForRangeSliders(g);var h=this._state.keyCtrl?this._state.value[0]:f,i=this._state.keyCtrl?f:this._state.value[1];f=[h,i]}return this._trigger("slideStart",f),this._setDataVal(f),this.setValue(f,!0,!0),this._setDataVal(f),this._trigger("slideStop",f),this._layout(),this._pauseEvent(b),delete this._state.keyCtrl,!1}},_pauseEvent:function(a){a.stopPropagation&&a.stopPropagation(),a.preventDefault&&a.preventDefault(),a.cancelBubble=!0,a.returnValue=!1},_mousemove:function(a){if(!this._state.enabled)return!1;var b=this._getPercentage(a);this._adjustPercentageForRangeSliders(b),this._state.percentage[this._state.dragged]=b,this._layout();var c=this._calculateValue(!0);return this.setValue(c,!0,!0),!1},_touchmove:function(a){if(void 0!==a.changedTouches){var b=a.changedTouches[0],c=b.pageX-this.touchX,d=b.pageY-this.touchY;this._state.inDrag||("vertical"===this.options.orientation&&5>=c&&c>=-5&&(d>=15||-15>=d)?this._mousedown(a):5>=d&&d>=-5&&(c>=15||-15>=c)&&this._mousedown(a))}},_adjustPercentageForRangeSliders:function(a){if(this.options.range){var b=this._getNumDigitsAfterDecimalPlace(a);b=b?b-1:0;var c=this._applyToFixedAndParseFloat(a,b);0===this._state.dragged&&this._applyToFixedAndParseFloat(this._state.percentage[1],b)c?(this._state.percentage[1]=this._state.percentage[0],this._state.dragged=0):0===this._state.keyCtrl&&this._state.value[1]/this.options.max*100a&&(this._state.percentage[1]=this._state.percentage[0],this._state.keyCtrl=0,this.handle1.focus())}},_mouseup:function(){if(!this._state.enabled)return!1;this.touchCapable&&(document.removeEventListener("touchmove",this.mousemove,!1),document.removeEventListener("touchend",this.mouseup,!1)),document.removeEventListener("mousemove",this.mousemove,!1),document.removeEventListener("mouseup",this.mouseup,!1),this._state.inDrag=!1,this._state.over===!1&&this._hideTooltip();var a=this._calculateValue(!0);return this._layout(),this._setDataVal(a),this._trigger("slideStop",a),!1},_calculateValue:function(a){var b;if(this.options.range?(b=[this.options.min,this.options.max],0!==this._state.percentage[0]&&(b[0]=this._toValue(this._state.percentage[0]),b[0]=this._applyPrecision(b[0])),100!==this._state.percentage[1]&&(b[1]=this._toValue(this._state.percentage[1]),b[1]=this._applyPrecision(b[1]))):(b=this._toValue(this._state.percentage[0]),b=parseFloat(b),b=this._applyPrecision(b)),a){for(var c=[b,1/0],d=0;d (http://larentis.eu) + * @license Apache-2.0 + */ + +'use strict';var _createClass=function(){function a(b,c){for(var e,d=0;d',{class:function _class(){var h=[];return h.push(g.options.state?'on':'off'),g.options.size&&h.push(g.options.size),g.options.disabled&&h.push('disabled'),g.options.readonly&&h.push('readonly'),g.options.indeterminate&&h.push('indeterminate'),g.options.inverse&&h.push('inverse'),g.$element.attr('id')&&h.push('id-'+g.$element.attr('id')),h.map(g._getClass.bind(g)).concat([g.options.baseClass],g._getClasses(g.options.wrapperClass)).join(' ')}}),this.$container=a('
    ',{class:this._getClass('container')}),this.$on=a('',{html:this.options.onText,class:this._getClass('handle-on')+' '+this._getClass(this.options.onColor)}),this.$off=a('',{html:this.options.offText,class:this._getClass('handle-off')+' '+this._getClass(this.options.offColor)}),this.$label=a('',{html:this.options.labelText,class:this._getClass('label')}),this.$element.on('init.bootstrapSwitch',this.options.onInit.bind(this,e)),this.$element.on('switchChange.bootstrapSwitch',function(){for(var j=arguments.length,h=Array(j),k=0;k-(f._handleWidth/2);f._dragEnd=!1,f.state(f.options.inverse?!h:h)}else f.state(!f.options.state);f._dragStart=!1}},'mouseleave.bootstrapSwitch':function mouseleaveBootstrapSwitch(){f.$label.trigger('mouseup.bootstrapSwitch')}})}},{key:'_externalLabelHandler',value:function _externalLabelHandler(){var f=this,e=this.$element.closest('label');e.on('click',function(g){g.preventDefault(),g.stopImmediatePropagation(),g.target===e[0]&&f.toggleState()})}},{key:'_formHandler',value:function _formHandler(){var e=this.$element.closest('form');e.data('bootstrap-switch')||e.on('reset.bootstrapSwitch',function(){b.setTimeout(function(){e.find('input').filter(function(){return a(this).data('bootstrap-switch')}).each(function(){return a(this).bootstrapSwitch('state',this.checked)})},1)}).data('bootstrap-switch',!0)}},{key:'_getClass',value:function _getClass(e){return this.options.baseClass+'-'+e}},{key:'_getClasses',value:function _getClasses(e){return a.isArray(e)?e.map(this._getClass.bind(this)):[this._getClass(e)]}}]),d}();a.fn.bootstrapSwitch=function(d){for(var f=arguments.length,e=Array(1tbody >#data').length; var rowCountMailbox = $('#mailboxtable >tbody >#data').length; var rowCountAlias = $('#aliastable >tbody >#data').length; + var rowCountResource = $('#resourcetable >tbody >#data').length; $("#numRowsDomainAlias").text(rowCountDomainAlias); $("#numRowsDomain").text(rowCountDomain); $("#numRowsMailbox").text(rowCountMailbox); $("#numRowsAlias").text(rowCountAlias); + $("#numRowsResource").text(rowCountResource); // Filter table function $.fn.extend({ @@ -16,10 +18,10 @@ $(document).ready(function() { return this.each(function(){ $(this).on('keyup', function(e){ var $this = $(this), - search = $this.val().toLowerCase(), - target = $this.attr('data-filters'), - $target = $(target), - $rows = $target.find('tbody #data'); + search = $this.val().toLowerCase(), + target = $this.attr('data-filters'), + $target = $(target), + $rows = $target.find('tbody #data'); $target.find('tbody .filterTable_no_results').remove(); if(search == '') { $target.find('tbody #no-data').show(); diff --git a/data/web/js/u2f-api.js b/data/web/js/u2f-api.js new file mode 100644 index 00000000..0f06f50d --- /dev/null +++ b/data/web/js/u2f-api.js @@ -0,0 +1,651 @@ +// Copyright 2014-2015 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +/** + * @fileoverview The U2F api. + */ + +'use strict'; + +/** Namespace for the U2F api. + * @type {Object} + */ +var u2f = u2f || {}; + +/** + * The U2F extension id + * @type {string} + * @const + */ +u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; + +/** + * Message types for messsages to/from the extension + * @const + * @enum {string} + */ +u2f.MessageTypes = { + 'U2F_REGISTER_REQUEST': 'u2f_register_request', + 'U2F_SIGN_REQUEST': 'u2f_sign_request', + 'U2F_REGISTER_RESPONSE': 'u2f_register_response', + 'U2F_SIGN_RESPONSE': 'u2f_sign_response' +}; + +/** + * Response status codes + * @const + * @enum {number} + */ +u2f.ErrorCodes = { + 'OK': 0, + 'OTHER_ERROR': 1, + 'BAD_REQUEST': 2, + 'CONFIGURATION_UNSUPPORTED': 3, + 'DEVICE_INELIGIBLE': 4, + 'TIMEOUT': 5 +}; + +/** + * A message type for registration requests + * @typedef {{ + * type: u2f.MessageTypes, + * signRequests: Array, + * registerRequests: ?Array, + * timeoutSeconds: ?number, + * requestId: ?number + * }} + */ +u2f.Request; + +/** + * A message for registration responses + * @typedef {{ + * type: u2f.MessageTypes, + * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), + * requestId: ?number + * }} + */ +u2f.Response; + +/** + * An error object for responses + * @typedef {{ + * errorCode: u2f.ErrorCodes, + * errorMessage: ?string + * }} + */ +u2f.Error; + +/** + * Data object for a single sign request. + * @typedef {{ + * version: string, + * challenge: string, + * keyHandle: string, + * appId: string + * }} + */ +u2f.SignRequest; + +/** + * Data object for a sign response. + * @typedef {{ + * keyHandle: string, + * signatureData: string, + * clientData: string + * }} + */ +u2f.SignResponse; + +/** + * Data object for a registration request. + * @typedef {{ + * version: string, + * challenge: string, + * appId: string + * }} + */ +u2f.RegisterRequest; + +/** + * Data object for a registration response. + * @typedef {{ + * registrationData: string, + * clientData: string + * }} + */ +u2f.RegisterResponse; + + +// Low level MessagePort API support + +/** + * Sets up a MessagePort to the U2F extension using the + * available mechanisms. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + */ +u2f.getMessagePort = function(callback) { + if (typeof chrome != 'undefined' && chrome.runtime) { + // The actual message here does not matter, but we need to get a reply + // for the callback to run. Thus, send an empty signature request + // in order to get a failure response. + var msg = { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: [] + }; + chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() { + if (!chrome.runtime.lastError) { + // We are on a whitelisted origin and can talk directly + // with the extension. + u2f.getChromeRuntimePort_(callback); + } else { + // chrome.runtime was available, but we couldn't message + // the extension directly, use iframe + u2f.getIframePort_(callback); + } + }); + } else if (u2f.isAndroidChrome_()) { + u2f.getAuthenticatorPort_(callback); + } else { + // chrome.runtime was not available at all, which is normal + // when this origin doesn't have access to any extensions. + u2f.getIframePort_(callback); + } +}; + +/** + * Detect chrome running on android based on the browser's useragent. + * @private + */ +u2f.isAndroidChrome_ = function() { + var userAgent = navigator.userAgent; + return userAgent.indexOf('Chrome') != -1 && + userAgent.indexOf('Android') != -1; +}; + +/** + * Connects directly to the extension via chrome.runtime.connect + * @param {function(u2f.WrappedChromeRuntimePort_)} callback + * @private + */ +u2f.getChromeRuntimePort_ = function(callback) { + var port = chrome.runtime.connect(u2f.EXTENSION_ID, + {'includeTlsChannelId': true}); + setTimeout(function() { + callback(new u2f.WrappedChromeRuntimePort_(port)); + }, 0); +}; + +/** + * Return a 'port' abstraction to the Authenticator app. + * @param {function(u2f.WrappedAuthenticatorPort_)} callback + * @private + */ +u2f.getAuthenticatorPort_ = function(callback) { + setTimeout(function() { + callback(new u2f.WrappedAuthenticatorPort_()); + }, 0); +}; + +/** + * A wrapper for chrome.runtime.Port that is compatible with MessagePort. + * @param {Port} port + * @constructor + * @private + */ +u2f.WrappedChromeRuntimePort_ = function(port) { + this.port_ = port; +}; + +/** + * Format a return a sign request. + * @param {Array} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.WrappedChromeRuntimePort_.prototype.formatSignRequest_ = + function(signRequests, timeoutSeconds, reqId) { + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: signRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + }; + +/** + * Format a return a register request. + * @param {Array} signRequests + * @param {Array} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.WrappedChromeRuntimePort_.prototype.formatRegisterRequest_ = + function(signRequests, registerRequests, timeoutSeconds, reqId) { + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + signRequests: signRequests, + registerRequests: registerRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + }; + +/** + * Posts a message on the underlying channel. + * @param {Object} message + */ +u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) { + this.port_.postMessage(message); +}; + +/** + * Emulates the HTML 5 addEventListener interface. Works only for the + * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedChromeRuntimePort_.prototype.addEventListener = + function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message' || name == 'onmessage') { + this.port_.onMessage.addListener(function(message) { + // Emulate a minimal MessageEvent object + handler({'data': message}); + }); + } else { + console.error('WrappedChromeRuntimePort only supports onMessage'); + } + }; + +/** + * Wrap the Authenticator app with a MessagePort interface. + * @constructor + * @private + */ +u2f.WrappedAuthenticatorPort_ = function() { + this.requestId_ = -1; + this.requestObject_ = null; +} + +/** + * Launch the Authenticator intent. + * @param {Object} message + */ +u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) { + var intentLocation = /** @type {string} */ (message); + document.location = intentLocation; +}; + +/** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedAuthenticatorPort_.prototype.addEventListener = + function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message') { + var self = this; + /* Register a callback to that executes when + * chrome injects the response. */ + window.addEventListener( + 'message', self.onRequestUpdate_.bind(self, handler), false); + } else { + console.error('WrappedAuthenticatorPort only supports message'); + } + }; + +/** + * Callback invoked when a response is received from the Authenticator. + * @param function({data: Object}) callback + * @param {Object} message message Object + */ +u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = + function(callback, message) { + var messageObject = JSON.parse(message.data); + var intentUrl = messageObject['intentURL']; + + var errorCode = messageObject['errorCode']; + var responseObject = null; + if (messageObject.hasOwnProperty('data')) { + responseObject = /** @type {Object} */ ( + JSON.parse(messageObject['data'])); + responseObject['requestId'] = this.requestId_; + } + + /* Sign responses from the authenticator do not conform to U2F, + * convert to U2F here. */ + responseObject = this.doResponseFixups_(responseObject); + callback({'data': responseObject}); + }; + +/** + * Fixup the response provided by the Authenticator to conform with + * the U2F spec. + * @param {Object} responseData + * @return {Object} the U2F compliant response object + */ +u2f.WrappedAuthenticatorPort_.prototype.doResponseFixups_ = + function(responseObject) { + if (responseObject.hasOwnProperty('responseData')) { + return responseObject; + } else if (this.requestObject_['type'] != u2f.MessageTypes.U2F_SIGN_REQUEST) { + // Only sign responses require fixups. If this is not a response + // to a sign request, then an internal error has occurred. + return { + 'type': u2f.MessageTypes.U2F_REGISTER_RESPONSE, + 'responseData': { + 'errorCode': u2f.ErrorCodes.OTHER_ERROR, + 'errorMessage': 'Internal error: invalid response from Authenticator' + } + }; + } + + /* Non-conformant sign response, do fixups. */ + var encodedChallengeObject = responseObject['challenge']; + if (typeof encodedChallengeObject !== 'undefined') { + var challengeObject = JSON.parse(atob(encodedChallengeObject)); + var serverChallenge = challengeObject['challenge']; + var challengesList = this.requestObject_['signData']; + var requestChallengeObject = null; + for (var i = 0; i < challengesList.length; i++) { + var challengeObject = challengesList[i]; + if (challengeObject['keyHandle'] == responseObject['keyHandle']) { + requestChallengeObject = challengeObject; + break; + } + } + } + var responseData = { + 'errorCode': responseObject['resultCode'], + 'keyHandle': responseObject['keyHandle'], + 'signatureData': responseObject['signature'], + 'clientData': encodedChallengeObject + }; + return { + 'type': u2f.MessageTypes.U2F_SIGN_RESPONSE, + 'responseData': responseData, + 'requestId': responseObject['requestId'] + } + }; + +/** + * Base URL for intents to Authenticator. + * @const + * @private + */ +u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = + 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE'; + +/** + * Format a return a sign request. + * @param {Array} signRequests + * @param {number} timeoutSeconds (ignored for now) + * @param {number} reqId + * @return {string} + */ +u2f.WrappedAuthenticatorPort_.prototype.formatSignRequest_ = + function(signRequests, timeoutSeconds, reqId) { + if (!signRequests || signRequests.length == 0) { + return null; + } + /* TODO(fixme): stash away requestId, as the authenticator app does + * not return it for sign responses. */ + this.requestId_ = reqId; + /* TODO(fixme): stash away the signRequests, to deal with the legacy + * response format returned by the Authenticator app. */ + this.requestObject_ = { + 'type': u2f.MessageTypes.U2F_SIGN_REQUEST, + 'signData': signRequests, + 'requestId': reqId, + 'timeout': timeoutSeconds + }; + + var appId = signRequests[0]['appId']; + var intentUrl = + u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + + ';S.appId=' + encodeURIComponent(appId) + + ';S.eventId=' + reqId + + ';S.challenges=' + + encodeURIComponent( + JSON.stringify(this.getBrowserDataList_(signRequests))) + ';end'; + return intentUrl; + }; + +/** + * Get the browser data objects from the challenge list + * @param {Array} challenges list of challenges + * @return {Array} list of browser data objects + * @private + */ +u2f.WrappedAuthenticatorPort_ + .prototype.getBrowserDataList_ = function(challenges) { + return challenges + .map(function(challenge) { + var browserData = { + 'typ': 'navigator.id.getAssertion', + 'challenge': challenge['challenge'] + }; + var challengeObject = { + 'challenge' : browserData, + 'keyHandle' : challenge['keyHandle'] + }; + return challengeObject; + }); +}; + +/** + * Format a return a register request. + * @param {Array} signRequests + * @param {Array} enrollChallenges + * @param {number} timeoutSeconds (ignored for now) + * @param {number} reqId + * @return {Object} + */ +u2f.WrappedAuthenticatorPort_.prototype.formatRegisterRequest_ = + function(signRequests, enrollChallenges, timeoutSeconds, reqId) { + if (!enrollChallenges || enrollChallenges.length == 0) { + return null; + } + // Assume the appId is the same for all enroll challenges. + var appId = enrollChallenges[0]['appId']; + var registerRequests = []; + for (var i = 0; i < enrollChallenges.length; i++) { + var registerRequest = { + 'challenge': enrollChallenges[i]['challenge'], + 'version': enrollChallenges[i]['version'] + }; + if (enrollChallenges[i]['appId'] != appId) { + // Only include the appId when it differs from the first appId. + registerRequest['appId'] = enrollChallenges[i]['appId']; + } + registerRequests.push(registerRequest); + } + var registeredKeys = []; + if (signRequests) { + for (i = 0; i < signRequests.length; i++) { + var key = { + 'keyHandle': signRequests[i]['keyHandle'], + 'version': signRequests[i]['version'] + }; + // Only include the appId when it differs from the appId that's + // being registered now. + if (signRequests[i]['appId'] != appId) { + key['appId'] = signRequests[i]['appId']; + } + registeredKeys.push(key); + } + } + var request = { + 'type': u2f.MessageTypes.U2F_REGISTER_REQUEST, + 'appId': appId, + 'registerRequests': registerRequests, + 'registeredKeys': registeredKeys, + 'requestId': reqId, + 'timeoutSeconds': timeoutSeconds + }; + var intentUrl = + u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + + ';S.request=' + encodeURIComponent(JSON.stringify(request)) + + ';end'; + /* TODO(fixme): stash away requestId, this is is not necessary for + * register requests, but here to keep parity with sign. + */ + this.requestId_ = reqId; + return intentUrl; + }; + + +/** + * Sets up an embedded trampoline iframe, sourced from the extension. + * @param {function(MessagePort)} callback + * @private + */ +u2f.getIframePort_ = function(callback) { + // Create the iframe + var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID; + var iframe = document.createElement('iframe'); + iframe.src = iframeOrigin + '/u2f-comms.html'; + iframe.setAttribute('style', 'display:none'); + document.body.appendChild(iframe); + + var channel = new MessageChannel(); + var ready = function(message) { + if (message.data == 'ready') { + channel.port1.removeEventListener('message', ready); + callback(channel.port1); + } else { + console.error('First event on iframe port was not "ready"'); + } + }; + channel.port1.addEventListener('message', ready); + channel.port1.start(); + + iframe.addEventListener('load', function() { + // Deliver the port to the iframe and initialize + iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]); + }); +}; + + +// High-level JS API + +/** + * Default extension response timeout in seconds. + * @const + */ +u2f.EXTENSION_TIMEOUT_SEC = 30; + +/** + * A singleton instance for a MessagePort to the extension. + * @type {MessagePort|u2f.WrappedChromeRuntimePort_} + * @private + */ +u2f.port_ = null; + +/** + * Callbacks waiting for a port + * @type {Array} + * @private + */ +u2f.waitingForPort_ = []; + +/** + * A counter for requestIds. + * @type {number} + * @private + */ +u2f.reqCounter_ = 0; + +/** + * A map from requestIds to client callbacks + * @type {Object.} + * @private + */ +u2f.callbackMap_ = {}; + +/** + * Creates or retrieves the MessagePort singleton to use. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + * @private + */ +u2f.getPortSingleton_ = function(callback) { + if (u2f.port_) { + callback(u2f.port_); + } else { + if (u2f.waitingForPort_.length == 0) { + u2f.getMessagePort(function(port) { + u2f.port_ = port; + u2f.port_.addEventListener('message', + /** @type {function(Event)} */ (u2f.responseHandler_)); + + // Careful, here be async callbacks. Maybe. + while (u2f.waitingForPort_.length) + u2f.waitingForPort_.shift()(u2f.port_); + }); + } + u2f.waitingForPort_.push(callback); + } +}; + +/** + * Handles response messages from the extension. + * @param {MessageEvent.} message + * @private + */ +u2f.responseHandler_ = function(message) { + var response = message.data; + var reqId = response['requestId']; + if (!reqId || !u2f.callbackMap_[reqId]) { + console.error('Unknown or missing requestId in response.'); + return; + } + var cb = u2f.callbackMap_[reqId]; + delete u2f.callbackMap_[reqId]; + cb(response['responseData']); +}; + +/** + * Dispatches an array of sign requests to available U2F tokens. + * @param {Array} signRequests + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sign = function(signRequests, callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = port.formatSignRequest_(signRequests, timeoutSeconds, reqId); + port.postMessage(req); + }); +}; + +/** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * @param {Array} registerRequests + * @param {Array} signRequests + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.register = function(registerRequests, signRequests, + callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = port.formatRegisterRequest_( + signRequests, registerRequests, timeoutSeconds, reqId); + port.postMessage(req); + }); +}; \ No newline at end of file diff --git a/data/web/js/user.js b/data/web/js/user.js index 503b5018..8d9450be 100644 --- a/data/web/js/user.js +++ b/data/web/js/user.js @@ -14,15 +14,20 @@ $(document).ready(function() { $(".passFields").slideUp(); } }); - // Show generate button after time selection - $('#trigger_set_time_limited_aliases').hide(); + $('#generate_tla').hide(); $('#validity').change(function(){ - $('#trigger_set_time_limited_aliases').show(); + $('#generate_tla').show(); }); // Init Bootstrap Switch $.fn.bootstrapSwitch.defaults.onColor = 'success'; $("[name='tls_out']").bootstrapSwitch(); $("[name='tls_in']").bootstrapSwitch(); + + // Log modal + $('#logModal').on('show.bs.modal', function(e) { + var logText = $(e.relatedTarget).data('log-text'); + $(e.currentTarget).find('#logText').html('
    ' + logText + '
    '); + }); }); \ No newline at end of file diff --git a/data/web/json_api.php b/data/web/json_api.php new file mode 100644 index 00000000..d404b0bd --- /dev/null +++ b/data/web/json_api.php @@ -0,0 +1,57 @@ +getRegisterData(get_u2f_registrations($object)); + list($req, $sigs) = $data; + $_SESSION['regReq'] = json_encode($req); + echo 'var req = ' . json_encode($req) . '; var sigs = ' . json_encode($sigs) . ';'; + } + else { + echo '{}'; + } + break; + case "get_u2f_auth_challenge": + if (isset($_SESSION['pending_mailcow_cc_username']) && $_SESSION['pending_mailcow_cc_username'] == $object) { + $reqs = json_encode($u2f->getAuthenticateData(get_u2f_registrations($object))); + $_SESSION['authReq'] = $reqs; + echo 'var req = ' . $reqs . ';'; + } + else { + echo '{}'; + } + break; + default: + echo '{}'; + break; + } + } +} \ No newline at end of file diff --git a/data/web/lang/lang.de.php b/data/web/lang/lang.de.php index 8727ca75..160a306e 100644 --- a/data/web/lang/lang.de.php +++ b/data/web/lang/lang.de.php @@ -10,13 +10,13 @@ $lang['footer']['restart_sogo'] = 'SOGo neustarten'; $lang['footer']['restart_now'] = 'Jetzt neustarten'; $lang['footer']['restart_sogo_info'] = 'Einige Änderungen an Domains benötigen einen Neustart SOGos. Hier können Sie SOGo neustarten.

    Wichtig: Ein korrekter Neustart SOGos kann eine Weile in Anspruch nehmen, bitte warten Sie, bis der Prozess vollständig beendet wurde.'; $lang['dkim']['confirm'] = 'Sind Sie sicher?'; -$lang['danger']['dkim_not_found'] = 'DKIM-Record nicht gefunden'; -$lang['danger']['dkim_remove_failed'] = 'Kann DKIM-Record nicht entfernen'; -$lang['danger']['dkim_add_failed'] = 'Kann DKIM-Record nicht hinzufügen'; +$lang['danger']['dkim_not_found'] = 'DKIM-Key nicht gefunden'; +$lang['danger']['dkim_remove_failed'] = 'Kann DKIM-Key nicht entfernen'; +$lang['danger']['dkim_add_failed'] = 'Kann DKIM-Key nicht hinzufügen'; $lang['danger']['dkim_domain_or_sel_invalid'] = 'DKIM-Domain oder -Selector nicht korrekt'; $lang['danger']['dkim_key_length_invalid'] = 'DKIM Schlüssellänge ungültig'; -$lang['success']['dkim_removed'] = 'DKIM-Record wurde entfernt'; -$lang['success']['dkim_added'] = 'DKIM-Record wurde hinzugefügt'; +$lang['success']['dkim_removed'] = 'DKIM-Key wurde entfernt'; +$lang['success']['dkim_added'] = 'DKIM-Key wurde hinzugefügt'; $lang['danger']['access_denied'] = 'Zugriff verweigert oder unvollständige/ungültige Daten'; $lang['danger']['whitelist_from_invalid'] = 'Whitelist-Eintrag ist ungültig'; $lang['danger']['domain_invalid'] = 'Domainname ist ungültig'; @@ -29,6 +29,7 @@ $lang['danger']['policy_list_from_exists'] = 'Ein Eintrag mit diesem Wert existi $lang['danger']['policy_list_from_invalid'] = 'Eintrag hat ungültiges Format'; $lang['danger']['alias_invalid'] = 'Alias-Adrese ist ungültig'; $lang['danger']['goto_invalid'] = 'Ziel-Adrese ist ungültig'; +$lang['danger']['last_key'] = 'Letzter Key kann nicht gelöscht werden'; $lang['danger']['alias_domain_invalid'] = 'Alias-Domain ist ungültig'; $lang['danger']['target_domain_invalid'] = 'Ziel-Domain ist ungültig'; $lang['danger']['object_exists'] = 'Objekt %s existiert bereits'; @@ -39,6 +40,8 @@ $lang['success']['alias_added'] = 'Alias-Adresse(n) wurden angelegt'; $lang['success']['alias_modified'] = 'Änderungen an Alias %s wurden gespeichert'; $lang['success']['aliasd_modified'] = 'Änderungen an Alias-Domain %s wurden gespeichert'; $lang['success']['mailbox_modified'] = 'Änderungen an Mailbox %s wurden gespeichert'; +$lang['success']['resource_modified'] = "Änderungen an Ressource %s wurden gespeichert"; +$lang['success']['object_modified'] = "Änderungen an Objekt %s wurden gespeichert"; $lang['success']['msg_size_saved'] = 'Limit wurde gesetzt'; $lang['danger']['aliasd_not_found'] = 'Alias-Domain nicht gefunden'; $lang['danger']['targetd_not_found'] = 'Ziel-Domain nicht gefunden'; @@ -54,28 +57,30 @@ $lang['danger']['exit_code_not_null'] = 'Fehler: Exit-Code ist %d'; $lang['danger']['mailbox_not_available'] = 'Mailbox nicht verfügbar'; $lang['danger']['username_invalid'] = 'Benutzername kann nicht verwendet werden'; $lang['danger']['password_mismatch'] = 'Passwort-Wiederholung stimmt nicht überein'; -$lang['danger']['password_complexity'] = 'Passwort entspricht nicht den Vorgaben'; +$lang['danger']['password_complexity'] = 'Passwort entspricht nicht den Richtlinien'; $lang['danger']['password_empty'] = 'Passwort darf nicht leer sein'; $lang['danger']['login_failed'] = 'Anmeldung fehlgeschlagen'; $lang['danger']['mailbox_invalid'] = 'Mailboxname ist ungültig'; +$lang['danger']['resource_invalid'] = 'Ressourcenname ist ungültig'; +$lang['danger']['description_invalid'] = 'Ressourcenbeschreibung ist ungültig'; $lang['danger']['mailbox_invalid_suggest'] = 'Mailboxname ist ungültig, meinten Sie vielleicht "%s"?'; -$lang['info']['fetchmail_planned'] = 'Aufgabe zur Mailabholung wurde geplant. Bitte prüfen Sie den Vorgangsstatus zu einem späteren Zeitpunkt noch einmal.'; -$lang['danger']['fetchmail_source_empty'] = 'Bitte geben Sie einen Quell-Ordner an'; -$lang['danger']['fetchmail_dest_empty'] = 'Bitte geben Sie einen Ziel-Ordner an'; $lang['danger']['is_alias'] = '%s lautet bereits eine Alias-Adresse'; $lang['danger']['is_alias_or_mailbox'] = "Eine Mailbox oder ein Alias mit der Adresse %s ist bereits vorhanden"; $lang['danger']['is_spam_alias'] = '%s lautet bereits eine Spam-Alias-Adresse'; $lang['danger']['quota_not_0_not_numeric'] = 'Speicherplatz muss numerisch und >= 0 sein'; -$lang['danger']['domain_not_found'] = 'Domain nicht gefunden.'; +$lang['danger']['domain_not_found'] = 'Domain "%s" nicht gefunden.'; $lang['danger']['max_mailbox_exceeded'] = 'Anzahl an Mailboxen überschritten (%d von %d)'; $lang['danger']['mailbox_quota_exceeded'] = 'Speicherplatz überschreitet das Limit (max. %d MiB)'; $lang['danger']['mailbox_quota_left_exceeded'] = 'Nicht genügend Speicherplatz vorhanden (Speicherplatz anwendbar: %d MiB)'; $lang['success']['mailbox_added'] = 'Mailbox %s wurde angelegt'; +$lang['success']['resource_added'] = 'Ressource %s wurde angelegt'; $lang['success']['domain_removed'] = 'Domain %s wurde entfernt'; $lang['success']['alias_removed'] = 'Alias-Adresse %s wurde entfernt'; $lang['success']['alias_domain_removed'] = 'Alias-Domain %s wurde entfernt'; $lang['success']['domain_admin_removed'] = 'Domain-Administrator %s wurde entfernt'; $lang['success']['mailbox_removed'] = 'Mailbox %s wurde entfernt'; +$lang['success']['eas_reset'] = "ActiveSync Gerät des Benutzers %s wurden zurückgesetzt"; +$lang['success']['resource_removed'] = 'Ressource %s wurde entfernt'; $lang['danger']['max_quota_in_use'] = 'Mailbox Speicherplatzlimit muss größer oder gleich %d MiB sein'; $lang['danger']['domain_quota_m_in_use'] = 'Domain Speicherplatzlimit muss größer oder gleich %d MiB sein'; $lang['danger']['mailboxes_in_use'] = 'Maximale Anzahl an Mailboxen muss größer oder gleich %d sein'; @@ -84,34 +89,38 @@ $lang['danger']['sender_acl_invalid'] = 'Sender ACL Wert muss eine Adresse oder $lang['danger']['domain_not_empty'] = 'Kann nur leere Domains entfernen'; $lang['warning']['spam_alias_temp_error'] = 'Kann zur Zeit keinen Spam-Alias erstellen, bitte versuchen Sie es später noch einmal.'; $lang['danger']['spam_alias_max_exceeded'] = 'Maximale Anzahl an Spam-Alias-Adressen erreicht'; -$lang['danger']['fetchmail_active'] = 'Ein Vorgang zur Mailabholung ist bereits aktiv, bitte haben Sie etwas Geduld.'; $lang['danger']['validity_missing'] = 'Bitte geben Sie eine Gültigkeitsdauer an'; $lang['user']['on'] = 'Ein'; $lang['user']['off'] = 'Aus'; +$lang['user']['messages'] = "Nachrichten"; +$lang['user']['in_use'] = "Verwendet"; $lang['user']['user_change_fn'] = ''; $lang['user']['user_settings'] = 'Benutzereinstellungen'; $lang['user']['mailbox_settings'] = 'Mailbox-Einstellungen'; $lang['user']['mailbox_details'] = 'Mailbox-Details'; $lang['user']['change_password'] = 'Passwort ändern'; -$lang['user']['new_password'] = 'Neues Passwort:'; +$lang['user']['new_password'] = 'Neues Passwort'; $lang['user']['save_changes'] = 'Änderungen speichern'; -$lang['user']['password_now'] = 'Aktuelles Passwort (Änderungen bestätigen):'; -$lang['user']['new_password_repeat'] = 'Neues Passwort (Wiederholung):'; +$lang['user']['password_now'] = 'Aktuelles Passwort (Änderungen bestätigen)'; +$lang['user']['new_password_repeat'] = 'Neues Passwort (Wiederholung)'; $lang['user']['new_password_description'] = 'Mindestanforderung: 6 Zeichen lang, Buchstaben und Zahlen.'; $lang['user']['did_you_know'] = 'Wussten Sie schon? Sie können Ihre E-Mail-Adresse mit Tags versehen, etwa "ich+Privat@example.com", um Nachrichten automatisch in einem Unterordner (Beispiel: "Privat") abzulegen.'; $lang['user']['spam_aliases'] = 'Temporäre E-Mail Aliasse'; $lang['user']['alias'] = 'Alias'; $lang['user']['aliases'] = 'Aliasse'; +$lang['user']['domain_aliases'] = 'Domain-Alias Adressen'; $lang['user']['is_catch_all'] = 'Ist Catch-All Adresse für Domain(s)'; -$lang['user']['aliases_also_send_as'] = 'Darf außerdem versenden als'; -$lang['user']['aliases_send_as_all'] = 'Absender für folgende Domains nicht prüfen'; +$lang['user']['aliases_also_send_as'] = 'Darf außerdem versenden als Benutzer'; +$lang['user']['aliases_send_as_all'] = 'Absender für folgende Domains und zugehörige Alias-Domains nicht prüfen'; $lang['user']['alias_create_random'] = 'Zufälligen Alias generieren'; $lang['user']['alias_extend_all'] = 'Gültigkeit +1h'; $lang['user']['alias_valid_until'] = 'Gültig bis'; $lang['user']['alias_remove_all'] = 'Alle entfernen'; $lang['user']['alias_time_left'] = 'Zeit verbleibend'; $lang['user']['alias_full_date'] = 'd.m.Y, H:i:s T'; +$lang['user']['syncjob_full_date'] = 'd.m.Y, H:i:s T'; $lang['user']['alias_select_validity'] = 'Bitte Gültigkeit auswählen'; +$lang['user']['sync_jobs'] = 'Sync Jobs'; $lang['user']['hour'] = 'Stunde'; $lang['user']['hours'] = 'Stunden'; $lang['user']['day'] = 'Tag'; @@ -134,6 +143,7 @@ $lang['user']['spamfilter_yellow'] = 'Gelb: Die Nachricht ist vielleicht Spam, w $lang['user']['spamfilter_red'] = 'Rot: Die Nachricht ist eindeutig Spam und wird vom Server abgelehnt'; $lang['user']['spamfilter_default_score'] = 'Standardwert:'; $lang['user']['spamfilter_hint'] = 'Der erste Wert beschreibt den "low spam score", der zweite Wert den "high spam score".'; +$lang['user']['spamfilter_table_domain_policy'] = "n.v. (Domainrichtlinie)"; $lang['user']['tls_policy_warning'] = 'Vorsicht: Entscheiden Sie sich unverschlüsselte Verbindungen abzulehnen, kann dies dazu führen, dass Kontakte Sie nicht mehr erreichen.
    Nachrichten, die die Richtlinie nicht erfüllen, werden durch einen Hard-Fail im Mailsystem abgewiesen.'; $lang['user']['tls_policy'] = 'Verschlüsselungsrichtlinie'; @@ -150,6 +160,22 @@ $lang['user']['tag_in_subject'] = 'In Betreff'; $lang['user']['tag_help_explain'] = 'Als Unterordner: Es wird ein Ordner mit dem Namen des Tags unterhalb der Inbox erstellt ("INBOX/Facebook").
    In Betreff: Der Name des Tags wird dem Betreff angefügt, etwa "[Facebook] Meine Neuigkeiten".'; $lang['user']['tag_help_example'] = 'Beispiel für eine getaggte E-Mail-Adresse: ich+Facebook@example.org'; +$lang['user']['eas_reset'] = 'ActiveSync Geräte-Cache zurücksetzen'; +$lang['user']['eas_reset_now'] = 'Jetzt zurücksetzen'; +$lang['user']['eas_reset_help'] = 'In vielen Fällen kann ein ActiveSync Profil durch das Zurücksetzen des Caches repariert werden.
    Vorsicht: Alle Elemente werden erneut heruntergeladen!'; + +$lang['user']['encryption'] = 'Verschlüsselung'; +$lang['user']['username'] = 'Benutzername'; +$lang['user']['password'] = 'Password'; +$lang['user']['last_run'] = 'Letzte Ausführung'; +$lang['user']['excludes'] = 'Ausschlüsse'; +$lang['user']['interval'] = 'Intervall'; +$lang['user']['active'] = 'Aktiv'; +$lang['user']['action'] = 'Aktion'; +$lang['user']['edit'] = 'Bearbeiten'; +$lang['user']['remove'] = 'Entfernen'; +$lang['user']['delete_now'] = 'Sofort löschen'; +$lang['user']['create_syncjob'] = 'Neuen Sync-Job erstellen'; $lang['start']['dashboard'] = '%s - Dashboard'; $lang['start']['start_rc'] = 'Roundcube öffnen'; @@ -180,10 +206,17 @@ $lang['header']['mailboxes'] = 'Mailboxen'; $lang['header']['user_settings'] = 'Benutzereinstellungen'; $lang['header']['login'] = 'Anmeldung'; $lang['header']['logged_in_as_logout'] = 'Eingeloggt als %s (abmelden)'; +$lang['header']['logged_in_as_logout_dual'] = 'Eingeloggt als %s [%s]'; $lang['header']['locale'] = 'Sprache'; $lang['mailbox']['domain'] = 'Domain'; +$lang['mailbox']['spam_aliases'] = 'Temp. Alias'; $lang['mailbox']['alias'] = 'Alias'; $lang['mailbox']['aliases'] = 'Aliasse'; +$lang['mailbox']['multiple_bookings'] = 'Mehrfachbuchen'; +$lang['mailbox']['kind'] = 'Art'; +$lang['mailbox']['description'] = 'Beschreibung'; +$lang['mailbox']['resources'] = 'Ressourcen'; +$lang['mailbox']['resource_name'] = 'Ressourcenname'; $lang['mailbox']['domains'] = 'Domains'; $lang['mailbox']['mailboxes'] = 'Mailboxen'; $lang['mailbox']['mailbox_quota'] = 'Max. Größe einer Mailbox'; @@ -206,10 +239,12 @@ $lang['mailbox']['msg_num'] = 'Anzahl Nachrichten'; $lang['mailbox']['remove'] = 'Entfernen'; $lang['mailbox']['edit'] = 'Bearbeiten'; $lang['mailbox']['archive'] = 'Archiv-Zugriff'; -$lang['mailbox']['no_record'] = 'Kein Eintrag'; +$lang['mailbox']['no_record'] = 'Kein Eintrag für Objekt %s'; +$lang['mailbox']['no_record_single'] = 'Kein Eintrag'; $lang['mailbox']['add_domain'] = 'Domain hinzufügen'; $lang['mailbox']['add_domain_alias'] = 'Domain-Alias hinzufügen'; $lang['mailbox']['add_mailbox'] = 'Mailbox hinzufügen'; +$lang['mailbox']['add_resource'] = 'Ressource hinzufügen'; $lang['mailbox']['add_alias'] = 'Alias hinzufügen'; $lang['info']['no_action'] = 'Keine Aktion anwendbar'; @@ -219,14 +254,26 @@ $lang['delete']['remove_domain_warning'] = 'Warnung: Sie entfernen die Do $lang['delete']['remove_domainalias_warning'] = 'Warnung: Sie entfernen die Alias-Domain %s!'; $lang['delete']['remove_domainadmin_warning'] = 'Warnung: Sie entfernen den Domain-Administrator %s!'; $lang['delete']['remove_alias_warning'] = 'Warnung: Sie entfernen die Alias-Adresse %s!'; +$lang['delete']['remove_syncjob_warning'] = 'Warnung: Sie entfernen einen Sync-Job des Benutzers %s!'; $lang['delete']['remove_mailbox_warning'] = 'Warnung: Sie entfernen die Mailbox %s!'; $lang['delete']['remove_mailbox_details'] = 'Die Mailbox wird vollständig und permanent entfernt!'; +$lang['delete']['remove_resource_warning'] = 'Warnung: Sie entfernen die Ressource %s!'; +$lang['delete']['remove_resource_details'] = 'Die Ressource wird vollständig und permanent entfernt!'; $lang['delete']['remove_domain_details'] = 'Diese Aktion entfernt ebenfalls Domain-Aliasse.

    Eine Domain muss leer sein, um entfernt zu werden.'; +$lang['delete']['remove_syncjob_details'] = 'Objekte dieses Sync-Jobs werden nicht mehr vom entfernten Server abgeholt.'; $lang['delete']['remove_alias_details'] = 'Benutzer werden keine Nachrichten mehr von dieser Adresse erhalten und versenden koennen!'; $lang['delete']['remove_button'] = 'Entfernen'; $lang['delete']['previous'] = 'Vorherige Seite'; +$lang['edit']['syncjob'] = 'Sync-Job bearbeiten'; $lang['edit']['save'] = 'Änderungen speichern'; +$lang['edit']['username'] = 'Benutzername'; +$lang['edit']['hostname'] = 'Servername'; +$lang['edit']['encryption'] = 'Verschlüsselungsmethode'; +$lang['edit']['maxage'] = 'Maximales Alter in Tagen einer Nachricht, die kopiert werden soll
    (0 = alle Nachrichten kopieren)'; +$lang['edit']['subfolder2'] = 'Ziel-Ordner
    (leer = kein Unterordner)'; +$lang['edit']['mins_interval'] = 'Intervall (min)'; +$lang['edit']['exclude'] = 'Elemente ausschließen (Regex)'; $lang['edit']['archive'] = 'Archiv-Zugriff'; $lang['edit']['max_mailboxes'] = 'Max. Mailboxanzahl:'; $lang['edit']['title'] = 'Objekt bearbeiten'; @@ -263,11 +310,26 @@ $lang['edit']['dkim_txt_name'] = 'TXT-Record Name:'; $lang['edit']['dkim_txt_value'] = 'TXT-Record Wert:'; $lang['edit']['previous'] = 'Vorherige Seite'; $lang['edit']['unchanged_if_empty'] = 'Unverändert, wenn leer'; -$lang['edit']['dont_check_sender_acl'] = 'Absender für Domain %s nicht prüfen'; +$lang['edit']['dont_check_sender_acl'] = 'Absender für Domain %s u. Alias-Dom. nicht prüfen'; +$lang['edit']['multiple_bookings'] = 'Mehrfaches Buchen'; +$lang['edit']['kind'] = 'Art'; +$lang['edit']['resource'] = 'Ressource'; + +$lang['add']['syncjob'] = 'Sync-Job erstellen'; +$lang['add']['syncjob_hint'] = 'Passwörter werden unverschlüsselt abgelegt!'; +$lang['add']['hostname'] = 'Servername'; +$lang['add']['username'] = 'Benutzername'; +$lang['add']['enc_method'] = 'Verschlüsselungsmethode'; +$lang['add']['maxage'] = 'Maximum age of messages that will be polled from remote (0 = ignore age)'; +$lang['add']['subfolder2'] = 'Sync into subfolder on destination'; +$lang['add']['mins_interval'] = 'Abrufintervall (Minuten)'; +$lang['add']['exclude'] = 'Elemente ausschließen (Regex)'; +$lang['add']['delete2duplicates'] = 'Lösche Duplikate im Ziel'; $lang['add']['title'] = 'Objekt anlegen'; $lang['add']['domain'] = 'Domain'; $lang['add']['active'] = 'Aktiv'; +$lang['add']['multiple_bookings'] = 'Mehrfaches Buchen möglich'; $lang['add']['save'] = 'Änderungen speichern'; $lang['add']['description'] = 'Beschreibung:'; $lang['add']['max_aliases'] = 'Max. mögliche Aliasse:'; @@ -289,7 +351,10 @@ $lang['add']['alias_domain'] = 'Alias-Domain'; $lang['add']['select'] = 'Bitte auswählen'; $lang['add']['target_domain'] = 'Ziel-Domain:'; $lang['add']['mailbox'] = 'Mailbox'; +$lang['add']['resource'] = 'Ressource'; +$lang['add']['kind'] = 'Art'; $lang['add']['mailbox_username'] = 'Benutzername (linker Teil der E-Mail-Adresse):'; +$lang['add']['resource_name'] = 'Ressourcenname:'; $lang['add']['full_name'] = 'Vor- und Zuname:'; $lang['add']['quota_mb'] = 'Speicherplatz (MiB):'; $lang['add']['select_domain'] = 'Bitte zuerst eine Domain auswählen'; @@ -310,11 +375,24 @@ $lang['login']['login'] = 'Anmelden'; $lang['login']['previous'] = 'Vorherige Seite'; $lang['login']['delayed'] = 'Login wurde zur Sicherheit um %s Sekunde/n verzögert.'; -$lang['login']['tfa'] = 'Zwei-Faktor-Authentifizierung'; -$lang['login']['tfa_details'] = 'Bitte bestätigen Sie Ihr Einmalpasswort im folgenden Feld'; -$lang['login']['confirm'] = 'Bestätigen'; -$lang['login']['otp'] = 'Einmalpasswort'; -$lang['login']['trash_login'] = 'Login verwerfen'; +$lang['tfa']['tfa'] = "Two-Factor Authentication"; +$lang['tfa']['set_tfa'] = "Konfiguriere Two-Factor Authentication Methode"; +$lang['tfa']['yubi_otp'] = "Yubico OTP Authentifizierung"; +$lang['tfa']['key_id'] = "Ein Name für diesen YubiKey"; +$lang['tfa']['api_register'] = 'mailcow verwendet die Yubico Cloud API. Ein API-Key für den Yubico Stick kann hier bezogen werden.'; +$lang['tfa']['u2f'] = "U2F Authentifizierung"; +$lang['tfa']['hotp'] = "HOTP Authentifizierung"; +$lang['tfa']['totp'] = "TOTP Authentifizierung"; +$lang['tfa']['none'] = "Deaktiviert"; +$lang['tfa']['delete_tfa'] = "Deaktiviere TFA"; +$lang['tfa']['disable_tfa'] = "Deaktiviere TFA bis zur nächsten erfolgreichen Anmeldung"; +$lang['tfa']['confirm_tfa'] = "Please confirm your one-time password in the below field"; +$lang['tfa']['confirm'] = "Bestätigen"; +$lang['tfa']['otp'] = "Einmalpasswort"; +$lang['tfa']['trash_login'] = "Login verwerfen"; +$lang['tfa']['select'] = "Bitte auswählen"; +$lang['tfa']['waiting_usb_auth'] = "Warte auf USB-Gerät...

    Bitte jetzt den vorgesehenen Taster des U2F USB-Gerätes berühren."; +$lang['tfa']['waiting_usb_register'] = "Warte auf USB-Gerät...

    Bitte zuerst das obere Passwortfeld ausfüllen und erst dann den vorgesehenen Taster des U2F USB-Gerätes berühren."; $lang['admin']['search_domain_da'] = 'Domains durchsuchen'; $lang['admin']['restrictions'] = 'Postifx Restriktionen'; @@ -340,9 +418,12 @@ $lang['admin']['msg_size_limit_details'] = 'Diese Einstellung wird Postfix und d $lang['admin']['save'] = 'Änderungen speichern'; $lang['admin']['maintenance'] = 'Wartung und Information'; $lang['admin']['sys_info'] = 'Systeminformation'; -$lang['admin']['dkim_add_key'] = 'DKIM-Record hinzufügen'; -$lang['admin']['dkim_keys'] = 'DKIM-Records'; -$lang['admin']['dkim_key_length'] = 'DKIM Schlüssellänge (Bits)'; +$lang['admin']['dkim_add_key'] = 'DKIM-Key hinzufügen'; +$lang['admin']['dkim_keys'] = 'DKIM-Keys'; +$lang['admin']['dkim_key_valid'] = 'Key gültig'; +$lang['admin']['dkim_key_unused'] = 'Key ohne Zuweisung'; +$lang['admin']['dkim_key_missing'] = 'Key fehlt'; +$lang['admin']['dkim_key_hint'] = 'Der Selector für DKIM-Keys lautet immer dkim.'; $lang['admin']['add'] = 'Hinzufügen'; $lang['admin']['configuration'] = 'Konfiguration'; $lang['admin']['password'] = 'Passwort'; diff --git a/data/web/lang/lang.en.php b/data/web/lang/lang.en.php index 801d8479..0758e538 100644 --- a/data/web/lang/lang.en.php +++ b/data/web/lang/lang.en.php @@ -10,13 +10,13 @@ $lang['footer']['restart_sogo'] = 'Restart SOGo'; $lang['footer']['restart_now'] = 'Restart now'; $lang['footer']['restart_sogo_info'] = 'Some tasks, e.g. adding a domain, require you to restart SOGo to catch changes made in the mailcow UI.

    Important: A graceful restart may take a while to complete, please wait for it to finish.'; $lang['dkim']['confirm'] = "Are you sure?"; -$lang['danger']['dkim_not_found'] = "DKIM record not found"; -$lang['danger']['dkim_remove_failed'] = "Cannot remove selected DKIM record"; -$lang['danger']['dkim_add_failed'] = "Cannot add given DKIM record"; +$lang['danger']['dkim_not_found'] = "DKIM key not found"; +$lang['danger']['dkim_remove_failed'] = "Cannot remove selected DKIM key"; +$lang['danger']['dkim_add_failed'] = "Cannot add given DKIM key"; $lang['danger']['dkim_domain_or_sel_invalid'] = "DKIM domain or selector invalid"; $lang['danger']['dkim_key_length_invalid'] = "DKIM key length invalid"; -$lang['success']['dkim_removed'] = "DKIM record has been removed"; -$lang['success']['dkim_added'] = "DKIM record has been saved"; +$lang['success']['dkim_removed'] = "DKIM key has been removed"; +$lang['success']['dkim_added'] = "DKIM key has been saved"; $lang['danger']['access_denied'] = "Access denied or invalid form data"; $lang['danger']['whitelist_from_invalid'] = "Whitelist entry invalid"; $lang['danger']['domain_invalid'] = "Domain name is invalid"; @@ -24,6 +24,7 @@ $lang['danger']['mailbox_quota_exceeds_domain_quota'] = "Max. quota exceeds doma $lang['danger']['object_is_not_numeric'] = "Value %s is not numeric"; $lang['success']['domain_added'] = "Added domain %s"; $lang['danger']['alias_empty'] = "Alias address must not be empty"; +$lang['danger']['last_key'] = 'Last key cannot be deleted'; $lang['danger']['goto_empty'] = "Goto address must not be empty"; $lang['danger']['policy_list_from_exists'] = "A record with given name exists"; $lang['danger']['policy_list_from_invalid'] = "Record has invalid format"; @@ -41,6 +42,8 @@ $lang['success']['alias_added'] = "Alias address/es has/have been added"; $lang['success']['alias_modified'] = "Changes to alias have been saved"; $lang['success']['aliasd_modified'] = "Changes to alias domain have been saved"; $lang['success']['mailbox_modified'] = "Changes to mailbox %s have been saved"; +$lang['success']['resource_modified'] = "Changes to mailbox %s have been saved"; +$lang['success']['object_modified'] = "Changes to object %s have been saved"; $lang['success']['msg_size_saved'] = "Message size limit has been set"; $lang['danger']['aliasd_not_found'] = "Alias domain not found"; $lang['danger']['targetd_not_found'] = "Target domain not found"; @@ -56,14 +59,13 @@ $lang['danger']['exit_code_not_null'] = "Error: Exit code was %d"; $lang['danger']['mailbox_not_available'] = "Mailbox not available"; $lang['danger']['username_invalid'] = "Username cannot be used"; $lang['danger']['password_mismatch'] = "Confirmation password is not identical"; -$lang['danger']['password_complexity'] = "Password does not meet requirements"; +$lang['danger']['password_complexity'] = "Password does not meet the policy"; $lang['danger']['password_empty'] = "Password must not be empty"; $lang['danger']['login_failed'] = "Login failed"; $lang['danger']['mailbox_invalid'] = "Mailbox name is invalid"; +$lang['danger']['description_invalid'] = 'Resource description is invalid'; +$lang['danger']['resource_invalid'] = "Resource name is invalid"; $lang['danger']['mailbox_invalid_suggest'] = 'Mailbox name is invalid, did you mean to type "%s"?'; -$lang['info']['fetchmail_planned'] = "Task to fetch emails has been planned. Please check the process at a later time."; -$lang['danger']['fetchmail_source_empty'] = "Please define a source folder"; -$lang['danger']['fetchmail_dest_empty'] = "Please define a target folder"; $lang['danger']['is_alias'] = "%s is already known as an alias address"; $lang['danger']['is_alias_or_mailbox'] = "%s is already known as an alias or a mailbox"; $lang['danger']['is_spam_alias'] = "%s is already known as a spam alias address"; @@ -73,11 +75,14 @@ $lang['danger']['max_mailbox_exceeded'] = "Max. mailboxes exceeded (%d of %d)"; $lang['danger']['mailbox_quota_exceeded'] = "Quota exceeds the domain limit (max. %d MiB)"; $lang['danger']['mailbox_quota_left_exceeded'] = "Not enough space left (space left: %d MiB)"; $lang['success']['mailbox_added'] = "Mailbox %s has been added"; +$lang['success']['resource_added'] = "Resource %s has been added"; $lang['success']['domain_removed'] = "Domain %s has been removed"; $lang['success']['alias_removed'] = "Alias-Adresse %s has been removed"; $lang['success']['alias_domain_removed'] = "Alias domain %s has been removed"; $lang['success']['domain_admin_removed'] = "Domain administrator %s has been removed"; $lang['success']['mailbox_removed'] = "Mailbox %s has been removed"; +$lang['success']['eas_reset'] = "ActiveSync devices for user %s were reset"; +$lang['success']['resource_removed'] = "Resource %s has been removed"; $lang['danger']['max_quota_in_use'] = "Mailbox quota must be greater or equal to %d MiB"; $lang['danger']['domain_quota_m_in_use'] = "Domain quota must be greater or equal to %s MiB"; $lang['danger']['mailboxes_in_use'] = "Max. mailboxes must be greater or equal to %d"; @@ -86,34 +91,38 @@ $lang['danger']['sender_acl_invalid'] = "Sender ACL value is invalid"; $lang['danger']['domain_not_empty'] = "Cannot remove non-empty domain"; $lang['warning']['spam_alias_temp_error'] = "Temporary error: Cannot add spam alias, please try again later."; $lang['danger']['spam_alias_max_exceeded'] = "Max. allowed spam alias addresses exceeded"; -$lang['danger']['fetchmail_active'] = "A process is already running, please wait for it to finish."; $lang['danger']['validity_missing'] = 'Please assign a period of validity'; $lang['user']['on'] = "On"; $lang['user']['off'] = "Off"; +$lang['user']['messages'] = "messages"; // "123 messages" +$lang['user']['in_use'] = "Used"; $lang['user']['user_change_fn'] = ""; $lang['user']['user_settings'] = 'User settings'; $lang['user']['mailbox_settings'] = 'Mailbox settings'; $lang['user']['mailbox_details'] = 'Mailbox details'; $lang['user']['change_password'] = 'Change password'; -$lang['user']['new_password'] = 'New password:'; +$lang['user']['new_password'] = 'New password'; $lang['user']['save_changes'] = 'Save changes'; -$lang['user']['password_now'] = 'Current password (confirm changes):'; -$lang['user']['new_password_repeat'] = 'Confirmation password (repeat):'; +$lang['user']['password_now'] = 'Current password (confirm changes)'; +$lang['user']['new_password_repeat'] = 'Confirmation password (repeat)'; $lang['user']['new_password_description'] = 'Requirement: 6 characters long, letters and numbers.'; $lang['user']['did_you_know'] = 'Did you know? You can use tags in your email address ("me+privat@example.com") to move messages to a folder automatically (example: "privat").'; $lang['user']['spam_aliases'] = 'Temporary email aliases'; $lang['user']['alias'] = 'Alias'; $lang['user']['aliases'] = 'Aliases'; +$lang['user']['domain_aliases'] = 'Domain alias addresses'; $lang['user']['is_catch_all'] = 'Catch-all for domain/s'; -$lang['user']['aliases_also_send_as'] = 'Also allowed to send as'; -$lang['user']['aliases_send_as_all'] = 'Do not check sender access for following domains'; +$lang['user']['aliases_also_send_as'] = 'Also allowed to send as user'; +$lang['user']['aliases_send_as_all'] = 'Do not check sender access for the following domain(s) and its alias domains'; $lang['user']['alias_create_random'] = 'Generate random alias'; $lang['user']['alias_extend_all'] = 'Extend aliases by 1 hour'; $lang['user']['alias_valid_until'] = 'Valid until'; $lang['user']['alias_remove_all'] = 'Remove all aliases'; $lang['user']['alias_time_left'] = 'Time left'; $lang['user']['alias_full_date'] = 'd.m.Y, H:i:s T'; +$lang['user']['syncjob_full_date'] = 'd.m.Y, H:i:s T'; $lang['user']['alias_select_validity'] = 'Period of validity'; +$lang['user']['sync_jobs'] = 'Sync jobs'; $lang['user']['hour'] = 'Hour'; $lang['user']['hours'] = 'Hours'; $lang['user']['day'] = 'Day'; @@ -136,12 +145,13 @@ $lang['user']['spamfilter_yellow'] = 'Yellow: this message may be spam, will be $lang['user']['spamfilter_red'] = 'Red: This message is spam and will be rejected by the server'; $lang['user']['spamfilter_default_score'] = 'Default values:'; $lang['user']['spamfilter_hint'] = 'The first value describes the "low spam score", the second represents the "high spam score".'; +$lang['user']['spamfilter_table_domain_policy'] = "n/a (domain policy)"; $lang['user']['tls_policy_warning'] = 'Warning: If you decide to enforce encrypted mail transfer, you may lose emails.
    Messages to not satisfy the policy will be bounced with a hard fail by the mail system.'; $lang['user']['tls_policy'] = 'Encryption policy'; $lang['user']['tls_enforce_in'] = 'Enforce TLS incoming'; $lang['user']['tls_enforce_out'] = 'Enforce TLS outgoing'; -$lang['user']['no_record'] = 'No Record'; +$lang['user']['no_record'] = 'No record'; $lang['user']['misc_settings'] = 'Other profile settings'; $lang['user']['misc_delete_profile'] = 'Other profile settings'; @@ -152,6 +162,22 @@ $lang['user']['tag_in_subject'] = 'In subject'; $lang['user']['tag_help_explain'] = 'In subfolder: a new subfolder named after the tag will be created below INBOX ("INBOX/Facebook").
    In subject: the tags name will be prepended to the mails subject, example: "[Facebook] Meine Neuigkeiten".'; $lang['user']['tag_help_example'] = 'Example for a tagged email address: ich+Facebook@example.org'; +$lang['user']['eas_reset'] = 'Reset ActiveSync device cache'; +$lang['user']['eas_reset_now'] = 'Reset now'; +$lang['user']['eas_reset_help'] = 'In many cases a device cache reset will help to recover a broken ActiveSync profile.
    Attention: All elements will be redownloaded!'; + +$lang['user']['encryption'] = 'Encyrption'; +$lang['user']['username'] = 'Username'; +$lang['user']['password'] = 'Password'; +$lang['user']['last_run'] = 'Last run'; +$lang['user']['excludes'] = 'Excludes'; +$lang['user']['interval'] = 'Interval'; +$lang['user']['active'] = 'Active'; +$lang['user']['action'] = 'Action'; +$lang['user']['edit'] = 'Edit'; +$lang['user']['remove'] = 'Remove'; +$lang['user']['delete_now'] = 'Remove now'; +$lang['user']['create_syncjob'] = 'Create new sync job'; $lang['start']['dashboard'] = '%s - dashboard'; $lang['start']['start_rc'] = 'Open Roundcube'; @@ -182,12 +208,19 @@ $lang['header']['mailboxes'] = 'Mailboxes'; $lang['header']['user_settings'] = 'User settings'; $lang['header']['login'] = 'Login'; $lang['header']['logged_in_as_logout'] = 'Logged in as %s (logout)'; +$lang['header']['logged_in_as_logout_dual'] = 'Logged in as %s [%s]'; $lang['header']['locale'] = 'Language'; $lang['mailbox']['domain'] = 'Domain'; +$lang['mailbox']['spam_aliases'] = 'Temp. alias'; +$lang['mailbox']['multiple_bookings'] = 'Multiple bookings'; +$lang['mailbox']['kind'] = 'Kind'; +$lang['mailbox']['description'] = 'Description'; $lang['mailbox']['alias'] = 'Alias'; +$lang['mailbox']['resource_name'] = 'Resource name'; $lang['mailbox']['aliases'] = 'Aliases'; $lang['mailbox']['domains'] = 'Domains'; $lang['mailbox']['mailboxes'] = 'Mailboxes'; +$lang['mailbox']['resources'] = 'Resources'; $lang['mailbox']['mailbox_quota'] = 'Max. size of a mailbox'; $lang['mailbox']['domain_quota'] = 'Quota'; $lang['mailbox']['active'] = 'Active'; @@ -208,37 +241,53 @@ $lang['mailbox']['msg_num'] = 'Message #'; $lang['mailbox']['remove'] = 'Remove'; $lang['mailbox']['edit'] = 'Edit'; $lang['mailbox']['archive'] = 'Archive'; -$lang['mailbox']['no_record'] = 'No Record'; +$lang['mailbox']['no_record'] = 'No record for object %s'; +$lang['mailbox']['no_record_single'] = 'No record'; $lang['mailbox']['add_domain'] = 'Add domain'; $lang['mailbox']['add_domain_alias'] = 'Add domain alias'; $lang['mailbox']['add_mailbox'] = 'Add mailbox'; +$lang['mailbox']['add_resource'] = 'Add resource'; $lang['mailbox']['add_alias'] = 'Add alias'; +$lang['mailbox']['add_domain_record_first'] = 'Please add a domain first'; $lang['info']['no_action'] = 'No action applicable'; $lang['delete']['title'] = 'Remove object'; $lang['delete']['remove_domain_warning'] = 'Warning: You are about to remove the domain %s!'; +$lang['delete']['remove_syncjob_warning'] = 'Warning: You are about to remove a sync job for user %s!'; $lang['delete']['remove_domainalias_warning'] = 'Warning: You are about to remove the domain alias %s!'; $lang['delete']['remove_domainadmin_warning'] = 'Warning: You are about to remove the domain administrator %s!'; $lang['delete']['remove_alias_warning'] = 'Warning: You are about to remove the alias address %s!'; $lang['delete']['remove_mailbox_warning'] = 'Warning: You are about to remove the mailbox %s!'; $lang['delete']['remove_mailbox_details'] = 'The mailbox will be purged permanently!'; +$lang['delete']['remove_resource_warning'] = 'Warning: You are about to remove the resource %s!'; +$lang['delete']['remove_resource_details'] = 'The resource will be purged permanently!'; $lang['delete']['remove_domain_details'] = 'This also removes domain aliases.

    A domain must be empty to be removed.'; +$lang['delete']['remove_syncjob_details'] = 'Objects from this sync job will not be pulled from the remote server anymore.'; $lang['delete']['remove_alias_details'] = 'Users will no longer be able to receive mail for or send mail from this address.'; $lang['delete']['remove_button'] = 'Remove'; $lang['delete']['previous'] = 'Previous page'; +$lang['edit']['syncjob'] = 'Edit sync job'; +$lang['edit']['save'] = 'Save changes'; +$lang['edit']['username'] = 'Username'; +$lang['edit']['hostname'] = 'Hostname'; +$lang['edit']['encryption'] = 'Encryption'; +$lang['edit']['maxage'] = 'Maximum age of messages in days that will be polled from remote
    (0 = ignore age)'; +$lang['edit']['subfolder2'] = 'Sync into subfolder on destination
    (empty = do not use subfolder)'; +$lang['edit']['mins_interval'] = 'Interval (min)'; +$lang['edit']['exclude'] = 'Exclude objects (regex)'; $lang['edit']['save'] = 'Save changes'; $lang['edit']['archive'] = 'Archive access'; -$lang['edit']['max_mailboxes'] = 'Max. possible mailboxes:'; +$lang['edit']['max_mailboxes'] = 'Max. possible mailboxes'; $lang['edit']['title'] = 'Edit object'; -$lang['edit']['target_address'] = 'Goto address/es (comma-separated):'; +$lang['edit']['target_address'] = 'Goto address/es (comma-separated)'; $lang['edit']['active'] = 'Active'; -$lang['edit']['target_domain'] = 'Target domain:'; -$lang['edit']['password'] = 'Password:'; -$lang['edit']['ratelimit'] = 'Outgoing rate limit/h:'; +$lang['edit']['target_domain'] = 'Target domain'; +$lang['edit']['password'] = 'Password'; +$lang['edit']['ratelimit'] = 'Outgoing rate limit/h'; $lang['danger']['ratelimt_less_one'] = 'Outgoing rate limit/h must not be less than 1'; -$lang['edit']['password_repeat'] = 'Confirmation password (repeat):'; +$lang['edit']['password_repeat'] = 'Confirmation password (repeat)'; $lang['edit']['domain_admin'] = 'Edit domain administrator'; $lang['edit']['domain'] = 'Edit domain'; $lang['edit']['alias_domain'] = 'Alias domain'; @@ -247,14 +296,14 @@ $lang['edit']['domains'] = 'Domains'; $lang['edit']['destroy'] = 'Manual data input'; $lang['edit']['alias'] = 'Edit alias'; $lang['edit']['mailbox'] = 'Edit mailbox'; -$lang['edit']['description'] = 'Description:'; -$lang['edit']['max_aliases'] = 'Max. aliases:'; -$lang['edit']['max_quota'] = 'Max. quota per mailbox (MiB):'; -$lang['edit']['domain_quota'] = 'Domain quota:'; -$lang['edit']['backup_mx_options'] = 'Backup MX options:'; +$lang['edit']['description'] = 'Description'; +$lang['edit']['max_aliases'] = 'Max. aliases'; +$lang['edit']['max_quota'] = 'Max. quota per mailbox (MiB)'; +$lang['edit']['domain_quota'] = 'Domain quota'; +$lang['edit']['backup_mx_options'] = 'Backup MX options'; $lang['edit']['relay_domain'] = 'Relay domain'; $lang['edit']['relay_all'] = 'Relay all recipients'; -$lang['edit']['dkim_signature'] = 'DKIM signature:'; +$lang['edit']['dkim_signature'] = 'DKIM signature'; $lang['edit']['dkim_record_info'] = 'Please add a TXT record with the given value to your DNS settings.'; $lang['edit']['relay_all_info'] = 'If you choose not to relay all recipients, you will need to add a ("blind") mailbox for every single recipient that should be relayed.'; $lang['edit']['full_name'] = 'Full name'; @@ -265,14 +314,30 @@ $lang['edit']['dkim_txt_name'] = 'TXT record name:'; $lang['edit']['dkim_txt_value'] = 'TXT record value:'; $lang['edit']['previous'] = 'Previous page'; $lang['edit']['unchanged_if_empty'] = 'If unchanged leave blank'; -$lang['edit']['dont_check_sender_acl'] = 'Do not check sender for domain %s'; +$lang['edit']['dont_check_sender_acl'] = "Disable sender check for domain %s + alias domains"; +$lang['edit']['multiple_bookings'] = 'Multiple bookings'; +$lang['edit']['kind'] = 'Kind'; +$lang['edit']['resource'] = 'Resource'; + +$lang['add']['syncjob'] = 'Add sync job'; +$lang['add']['syncjob_hint'] = 'Be aware that passwords need to be saved plain-text!'; +$lang['add']['hostname'] = 'Hostname'; +$lang['add']['username'] = 'Username'; +$lang['add']['enc_method'] = 'Encryption method'; +$lang['add']['mins_interval'] = 'Polling interval (minutes)'; +$lang['add']['maxage'] = 'Maximum age of messages that will be polled from remote (0 = ignore age)'; +$lang['add']['subfolder2'] = 'Sync into subfolder on destination'; +$lang['add']['exclude'] = 'Exclude objects (regex)'; +$lang['add']['delete2duplicates'] = 'Delete duplicates on destination'; $lang['add']['title'] = 'Add object'; $lang['add']['domain'] = 'Domain'; $lang['add']['active'] = 'Active'; +$lang['add']['multiple_bookings'] = 'Multiple bookings'; $lang['add']['save'] = 'Save changes'; $lang['add']['description'] = 'Description:'; $lang['add']['max_aliases'] = 'Max. possible aliases:'; +$lang['add']['resource_name'] = 'Resource name'; $lang['add']['max_mailboxes'] = 'Max. possible mailboxes:'; $lang['add']['mailbox_quota_m'] = 'Max. quota per mailbox (MiB):'; $lang['add']['domain_quota_m'] = 'Total domain quota (MiB):'; @@ -291,6 +356,8 @@ $lang['add']['alias_domain'] = 'Alias domain'; $lang['add']['select'] = 'Please select...'; $lang['add']['target_domain'] = 'Target domain:'; $lang['add']['mailbox'] = 'Mailbox'; +$lang['add']['resource'] = 'Resource'; +$lang['add']['kind'] = 'Kind'; $lang['add']['mailbox_username'] = 'Username (left part of an email address):'; $lang['add']['full_name'] = 'Full name:'; $lang['add']['quota_mb'] = 'Quota (MiB):'; @@ -312,11 +379,24 @@ $lang['login']['login'] = 'Login'; $lang['login']['previous'] = "Previous page"; $lang['login']['delayed'] = 'Login was delayed by %s seconds.'; -$lang['login']['tfa'] = "Two-factor authentication"; -$lang['login']['tfa_details'] = "Please confirm your one-time password in the below field"; -$lang['login']['confirm'] = "Confirm"; -$lang['login']['otp'] = "One-time password"; -$lang['login']['trash_login'] = "Trash login"; +$lang['tfa']['tfa'] = "Two-factor authentication"; +$lang['tfa']['set_tfa'] = "Set two-factor authentication method"; +$lang['tfa']['yubi_otp'] = "Yubico OTP authentication"; +$lang['tfa']['key_id'] = "An identifier for your YubiKey"; +$lang['tfa']['api_register'] = 'mailcow uses the Yubico Cloud API. Please get an API key for your key here'; +$lang['tfa']['u2f'] = "U2F authentication"; +$lang['tfa']['hotp'] = "HOTP authentication"; +$lang['tfa']['totp'] = "TOTP authentication"; +$lang['tfa']['none'] = "Deaktiviert"; +$lang['tfa']['delete_tfa'] = "Disable TFA"; +$lang['tfa']['disable_tfa'] = "Disable TFA until next successful login"; +$lang['tfa']['confirm_tfa'] = "Please confirm your one-time password in the below field"; +$lang['tfa']['confirm'] = "Confirm"; +$lang['tfa']['otp'] = "One-time password"; +$lang['tfa']['trash_login'] = "Trash login"; +$lang['tfa']['select'] = "Please select"; +$lang['tfa']['waiting_usb_auth'] = "Waiting for USB device...

    Please tap the button on your U2F USB device now."; +$lang['tfa']['waiting_usb_register'] = "Waiting for USB device...

    Please enter your password above and confirm your U2F registration by tapping the button on your U2F USB device."; $lang['admin']['search_domain_da'] = 'Search domains'; $lang['admin']['restrictions'] = 'Postifx Restrictions'; @@ -340,6 +420,10 @@ $lang['admin']['privacy_anon_mail'] = 'Anonymize outgoing mail'; $lang['admin']['dkim_txt_name'] = 'TXT record name:'; $lang['admin']['dkim_txt_value'] = 'TXT record value:'; $lang['admin']['dkim_key_length'] = 'DKIM key length (bits)'; +$lang['admin']['dkim_key_valid'] = 'Key valid'; +$lang['admin']['dkim_key_unused'] = 'Key unused'; +$lang['admin']['dkim_key_missing'] = 'Key missing'; +$lang['admin']['dkim_key_hint'] = 'Selector for DKIM keys is always dkim.'; $lang['admin']['previous'] = 'Previous page'; $lang['admin']['quota_mb'] = 'Quota (MiB):'; $lang['admin']['sender_acl'] = 'Allow to send as:'; @@ -349,8 +433,8 @@ $lang['admin']['msg_size_limit_details'] = 'Applying a new limit will reload Pos $lang['admin']['save'] = 'Save changes'; $lang['admin']['maintenance'] = 'Maintenance and Information'; $lang['admin']['sys_info'] = 'System information'; -$lang['admin']['dkim_add_key'] = 'Add DKIM record'; -$lang['admin']['dkim_keys'] = 'DKIM records'; +$lang['admin']['dkim_add_key'] = 'Add DKIM key'; +$lang['admin']['dkim_keys'] = 'DKIM keys'; $lang['admin']['add'] = 'Add'; $lang['admin']['configuration'] = 'Configuration'; $lang['admin']['password'] = 'Password'; @@ -374,5 +458,5 @@ $lang['admin']['invalid_max_msg_size'] = 'Invalid max. message size'; $lang['admin']['site_not_found'] = 'Cannot locate mailcow site configuration'; $lang['admin']['public_folder_empty'] = 'Public folder name must not be empty'; $lang['admin']['set_rr_failed'] = 'Cannot set Postfix restrictions'; -$lang['admin']['no_record'] = 'No Record'; +$lang['admin']['no_record'] = 'No record'; ?> diff --git a/data/web/lang/lang.es.php b/data/web/lang/lang.es.php new file mode 100644 index 00000000..dbe3f3d0 --- /dev/null +++ b/data/web/lang/lang.es.php @@ -0,0 +1,378 @@ +
    Importante: Un reinicio sencillo puede tardar un poco en completarse, por favor espere a que termine.'; +$lang['dkim']['confirm'] = "¿Estás Seguro?"; +$lang['danger']['dkim_not_found'] = "Registro DKIM no encontrado"; +$lang['danger']['dkim_remove_failed'] = "No se puede eliminar el registro DKIM seleccionado"; +$lang['danger']['dkim_add_failed'] = "No se puede agregar el registro DKIM dado"; +$lang['danger']['dkim_domain_or_sel_invalid'] = "Dominio DKIM ó selector inválido"; +$lang['danger']['dkim_key_length_invalid'] = "Longitud de la llave DKIM inválida"; +$lang['success']['dkim_removed'] = "Registro DKIM removido"; +$lang['success']['dkim_added'] = "Registro DKIM guardado"; +$lang['danger']['access_denied'] = "Acceso denegado o datos del formulario inválidos"; +$lang['danger']['whitelist_from_invalid'] = "Entrada de la lista blanca inválida"; +$lang['danger']['domain_invalid'] = "Nombre de dominio inválido"; +$lang['danger']['mailbox_quota_exceeds_domain_quota'] = "Cuota máx. excede el limite de cuota del dominio"; +$lang['danger']['object_is_not_numeric'] = "El valor %s no es numérico"; +$lang['success']['domain_added'] = "Dominio agregado %s"; +$lang['danger']['alias_empty'] = "Dirección alias no debe estar vacía"; +$lang['danger']['goto_empty'] = "Dirección \"goto\" no debe estar vacía"; +$lang['danger']['policy_list_from_exists'] = "Un registro con ese nombre ya existe"; +$lang['danger']['policy_list_from_invalid'] = "El registro tiene formato inválido"; +$lang['danger']['whitelist_exists'] = "Ya existe un registro con ese nombre en la lista blanca"; +$lang['danger']['whitelist_from_invalid'] = "Formato inválido para el registro de lista blanca"; +$lang['danger']['alias_invalid'] = "Dirección alias inválida"; +$lang['danger']['goto_invalid'] = "Dirección \"goto\" inválida"; +$lang['danger']['alias_domain_invalid'] = "El dominio alias es inválido"; +$lang['danger']['target_domain_invalid'] = "El dominio \"goto\" es inválido"; +$lang['danger']['object_exists'] = "El objeto %s ya existe"; +$lang['danger']['domain_exists'] = "El dominio %s ya existe"; +$lang['danger']['alias_goto_identical'] = "Las direcciones alias y \"goto\" no deben ser idénticas"; +$lang['danger']['aliasd_targetd_identical'] = "El dominio alias no debe ser igual al dominio destino"; +$lang['success']['alias_added'] = "Dirección/es alias ha/han sidgo agregada"; +$lang['success']['alias_modified'] = "Cambios al alias guardados"; +$lang['success']['aliasd_modified'] = "Cambios al dominio alias guardados"; +$lang['success']['mailbox_modified'] = "Cambios al buzón %s guardados"; +$lang['success']['msg_size_saved'] = "Limite del mensaje establecido"; +$lang['danger']['aliasd_not_found'] = "Dominio alias no encontrado"; +$lang['danger']['targetd_not_found'] = "Dominio destino no encontrado"; +$lang['danger']['aliasd_exists'] = "Dominio alias ya existe"; +$lang['success']['aliasd_added'] = "Agregado dominio alias %s"; +$lang['success']['aliasd_modified'] = "Cambios al dominio alias %s guardados"; +$lang['success']['domain_modified'] = "Cambios al dominio %s guardados"; +$lang['success']['domain_admin_modified'] = "Cambios al administrador del dominio %s guardados"; +$lang['success']['domain_admin_added'] = "Administrador del dominio %s agregado"; +$lang['success']['changes_general'] = 'Cambios guardados'; +$lang['success']['admin_modified'] = "Cambios al administrador guardados"; +$lang['danger']['exit_code_not_null'] = "Error: Código de salida es %d"; +$lang['danger']['mailbox_not_available'] = "Buzón no disponible"; +$lang['danger']['username_invalid'] = "Nombre de usuario no se puede utilizar"; +$lang['danger']['password_mismatch'] = "Confirmación de contraseña no es identica"; +$lang['danger']['password_complexity'] = "La contraseña no cumple con los requisitos"; +$lang['danger']['password_empty'] = "El campo de la contraseña no debe estar vacío"; +$lang['danger']['login_failed'] = "Inicio de sesión fallido"; +$lang['danger']['mailbox_invalid'] = "Nombre de buzón inválido"; +$lang['danger']['mailbox_invalid_suggest'] = 'El nombre del buzón es inválido, ¿pretendías escribir "%s"?'; +$lang['info']['fetchmail_planned'] = "La tarea para buscar correos se ha planeado. Por favor verifica el proceso más tarde."; +$lang['danger']['fetchmail_source_empty'] = "Por favor define una carpeta fuente"; +$lang['danger']['fetchmail_dest_empty'] = "Por favor define una carpeta destino"; +$lang['danger']['is_alias'] = "%s ya está definida como una dirección alias"; +$lang['danger']['is_alias_or_mailbox'] = "%s ya está definido como un alias ó como un buzón"; +$lang['danger']['is_spam_alias'] = "%s ya está definida como una dirección alias de correo no deseado"; +$lang['danger']['quota_not_0_not_numeric'] = "Cuota debe ser numérica y >= 0"; +$lang['danger']['domain_not_found'] = "Dominio no encontrado."; +$lang['danger']['max_mailbox_exceeded'] = "Máx. de buzones superado (%d de %d)"; +$lang['danger']['mailbox_quota_exceeded'] = "Cuota excede el límite de dominio (máx. %d MiB)"; +$lang['danger']['mailbox_quota_left_exceeded'] = "No queda espacio suficiente (espacio libre: %d MiB)"; +$lang['success']['mailbox_added'] = "Buzón %s agregado"; +$lang['success']['domain_removed'] = "Dominio %s removido"; +$lang['success']['alias_removed'] = "Dirección alias %s removida"; +$lang['success']['alias_domain_removed'] = "Dominio alias %s removido"; +$lang['success']['domain_admin_removed'] = "Administrador del dominio %s removido"; +$lang['success']['mailbox_removed'] = "Buzón %s removido"; +$lang['danger']['max_quota_in_use'] = "Cuota del buzón debe ser mayor o igual a %d MiB"; +$lang['danger']['domain_quota_m_in_use'] = "Cuota del dominio debe ser mayor o igual a %d MiB"; +$lang['danger']['mailboxes_in_use'] = "Máx. de buzones debe ser mayor o igual a %d"; +$lang['danger']['aliases_in_use'] = "Máx. de alias debe ser mayor o igual a %d"; +$lang['danger']['sender_acl_invalid'] = "Valor del remitente ACL inválido"; +$lang['danger']['domain_not_empty'] = "No se puede eliminar un dominio que no esté vacío"; +$lang['warning']['spam_alias_temp_error'] = "Error temporal: No se puede agregar ese \"spam alias\", inténtelo más tarde."; +$lang['danger']['spam_alias_max_exceeded'] = "Máx. direcciones \"spam alias\" permitidas excedido"; +$lang['danger']['fetchmail_active'] = "Un proceso ya se está ejecutando, por favor espera a que termine."; +$lang['danger']['validity_missing'] = 'Por favor asigna un periodo de validez'; +$lang['user']['off'] = "Apagado"; +$lang['user']['user_change_fn'] = ""; +$lang['user']['user_settings'] = 'Configuración del usuario'; +$lang['user']['mailbox_settings'] = 'Configuración del buzón'; +$lang['user']['mailbox_details'] = 'Detalles del buzón'; +$lang['user']['change_password'] = 'Cambiar contraseña'; +$lang['user']['new_password'] = 'Nueva contraseña:'; +$lang['user']['save_changes'] = 'Guardar cambios'; +$lang['user']['password_now'] = 'Contraseña actual (confirmar cambios):'; +$lang['user']['new_password_repeat'] = 'Confirmación de contraseña (repetir):'; +$lang['user']['new_password_description'] = 'Requisitos: longitud de 6 caracteres, letras y números.'; +$lang['user']['did_you_know'] = '¿Sabías qué? Puedes utilizar etiquetas en tu dirección email ("me+privat@example.com") para mover mensajes a una carpeta automáticamente (ejemplo: "privat").'; +$lang['user']['spam_aliases'] = 'Alias de email temporales'; +$lang['user']['alias'] = 'Alias'; +$lang['user']['aliases'] = 'Alias'; +$lang['user']['is_catch_all'] = 'Atrapa-Todo para el/los dominio/s'; +$lang['user']['aliases_also_send_as'] = 'También permitido para mandarse como'; +$lang['user']['aliases_send_as_all'] = 'No verifiques el acceso del remitente para los siguientes dominios'; +$lang['user']['alias_create_random'] = 'Generar alias aleatorio'; +$lang['user']['alias_extend_all'] = 'Extender alias por 1 hora'; +$lang['user']['alias_valid_until'] = 'Válido hasta'; +$lang['user']['alias_remove_all'] = 'Eliminar todos los alias'; +$lang['user']['alias_time_left'] = 'Tiempo restante'; +$lang['user']['alias_full_date'] = 'd.m.Y, H:i:s T'; +$lang['user']['alias_select_validity'] = 'Periodo de validez'; +$lang['user']['hour'] = 'Hora'; +$lang['user']['hours'] = 'Horas'; +$lang['user']['day'] = 'Día'; +$lang['user']['week'] = 'Semana'; +$lang['user']['weeks'] = 'Semanas'; +$lang['user']['spamfilter'] = 'Filtro de spam'; +$lang['user']['spamfilter_wl'] = 'Lista blanca'; +$lang['user']['spamfilter_wl_desc'] = 'Direcciones en la lista blanca nunca clasificarán como spam. Probablemente se usará un comodín.'; +$lang['user']['spamfilter_bl'] = 'Lista negra'; +$lang['user']['spamfilter_bl_desc'] = 'Direcciones en la lista negra siempre clasificarán como spam. Probablemente se usará un comodín.'; +$lang['user']['spamfilter_behavior'] = 'Clasificación'; +$lang['user']['spamfilter_table_rule'] = 'Regla'; +$lang['user']['spamfilter_table_action'] = 'Acción'; +$lang['user']['spamfilter_table_empty'] = 'No hay datos para mostrar'; +$lang['user']['spamfilter_table_remove'] = 'eliminar'; +$lang['user']['spamfilter_table_add'] = 'Agregar elemento'; +$lang['user']['spamfilter_default_score'] = 'Calificación de spam:'; +$lang['user']['spamfilter_green'] = 'Verde: éste mensaje no es spam'; +$lang['user']['spamfilter_yellow'] = 'Amarillo: éste mensaje puede ser spam, será etiquetado como spam y trasladado a tu carpeta basura'; +$lang['user']['spamfilter_red'] = 'Rojo: Este mensaje es spam y sera rechazado por el servidor'; +$lang['user']['spamfilter_default_score'] = 'Valores por defecto:'; +$lang['user']['spamfilter_hint'] = 'El primer valor representa la "calificación baja de spam", el segundo representa la "calificación alta de spam".'; + +$lang['user']['tls_policy_warning'] = 'Advertencia: Si decides forzar la transmisión de correo encriptado, puedes perder correos.
    Mensajes que no satisfagan la política serán rebotados con una falla grave en el sistema de correos .'; +$lang['user']['tls_policy'] = 'Política de encriptación'; +$lang['user']['tls_enforce_in'] = 'Aplicar TLS entrante'; +$lang['user']['tls_enforce_out'] = 'Aplicar TLS saliente'; +$lang['user']['no_record'] = 'Sin registro'; + +$lang['user']['misc_settings'] = 'Otras configuraciones de usuario'; +$lang['user']['misc_delete_profile'] = 'Otras configuraciones de usuario'; + +$lang['user']['tag_handling'] = 'Establecer manejo para el correo etiquetado'; +$lang['user']['tag_in_subfolder'] = 'En subcarpeta'; +$lang['user']['tag_in_subject'] = 'En asunto'; +$lang['user']['tag_help_explain'] = 'En subcarpeta: una nueva subcarpeta llamada como la etiqueta será creada debajo de INBOX ("INBOX/Facebook").
    +En asunto: los nombres de las etiquetas serán añadidos al asunto de los correos, ejemplo: "[Facebook] Mis Noticias".'; +$lang['user']['tag_help_example'] = 'Ejemplo de una dirección email etiquetada: mi+Facebook@ejemplo.org'; + +$lang['start']['dashboard'] = '%s - panel'; +$lang['start']['start_rc'] = 'Abrir Roundcube'; +$lang['start']['start_sogo'] = 'Abrir SOGo'; +$lang['start']['mailcow_apps_detail'] = 'Utiliza una aplicación de mailcow para acceder a tus correos, calendario, contactos y más.'; +$lang['start']['mailcow_panel'] = 'Iniciar mailcow UI'; +$lang['start']['mailcow_panel_description'] = 'Mailcow UI está disponible para administradores y usuarios de buzón.'; +$lang['start']['mailcow_panel_detail'] = 'Administradores del dominio crean, modifican o eliminan buzones y alias, cambia dominios y lee información más detallada sobre sus dominios asignados
    + Usuarios de buzón son capaces de crear alias de tiempo limitado (spam alias), cambiar su contraseña y la configuración del filtro de spam.'; +$lang['start']['recommended_config'] = 'Configuración recomendada (sin ActiveSync)'; +$lang['start']['imap_smtp_server'] = 'IMAP- y SMTP datos del servidor'; +$lang['start']['imap_smtp_server_description'] = 'Para la mejor experiencia recomendamos utilizar Mozilla Thunderbird.'; +$lang['start']['imap_smtp_server_badge'] = 'Leer/Escribir correos'; +$lang['start']['imap_smtp_server_auth_info'] = 'Por favor utiliza tu dirección de correo completa y el mecanismo de autenticación PLANO.
    +Tus datos para iniciar sesión serán encriptados por la encriptación obligatoria del servidor'; +$lang['start']['managesieve'] = 'ManageSieve'; +$lang['start']['managesieve_badge'] = 'Filtro de correos'; +$lang['start']['managesieve_description'] = 'Por favor utiliza Mozilla Thunderbird con la extensión nightly sieve.
    Inicia Thunderbird, abre la configuración de complementos y suelta el archivo xpi descargado en la ventana abierta.
    El servidor es %s, utiliza el puerto 4190 si se te pregunta. Los datos para iniciar sesión coinciden con los datos de tu correo.'; +$lang['start']['service'] = 'Servicio'; +$lang['start']['encryption'] = 'Método de encriptación'; +$lang['start']['help'] = 'Mostrar/Ocultar panel de ayuda'; +$lang['start']['hostname'] = 'Hostname'; +$lang['start']['port'] = 'Port'; +$lang['start']['footer'] = ''; +$lang['header']['mailcow_settings'] = 'Configuracion'; +$lang['header']['administration'] = 'Administración'; +$lang['header']['mailboxes'] = 'Buzones'; +$lang['header']['user_settings'] = 'Configuraciones de usuario'; +$lang['header']['login'] = 'Inicio de sesión'; +$lang['header']['logged_in_as_logout'] = 'Sesión iniciada como %s (cerrar sesión)'; +$lang['header']['locale'] = 'Idioma'; +$lang['mailbox']['domain'] = 'Dominio'; +$lang['mailbox']['alias'] = 'Alias'; +$lang['mailbox']['aliases'] = 'Alias'; +$lang['mailbox']['domains'] = 'Dominios'; +$lang['mailbox']['mailboxes'] = 'Buzones'; +$lang['mailbox']['mailbox_quota'] = 'Tamaño máx. de cuota'; +$lang['mailbox']['domain_quota'] = 'Cuota'; +$lang['mailbox']['active'] = 'Activo'; +$lang['mailbox']['action'] = 'Acción'; +$lang['mailbox']['ratelimit'] = 'Límite de la tarifa saliente/h'; +$lang['mailbox']['backup_mx'] = 'Respaldar MX'; +$lang['mailbox']['domain_aliases'] = 'Alias de dominio'; +$lang['mailbox']['target_domain'] = 'Dominio destino'; +$lang['mailbox']['target_address'] = 'Dirección Goto'; +$lang['mailbox']['username'] = 'Nombre de usuario'; +$lang['mailbox']['fname'] = 'Nombre completo'; +$lang['mailbox']['filter_table'] = 'Filtrar tabla'; +$lang['mailbox']['yes'] = '✔'; +$lang['mailbox']['no'] = '✘'; +$lang['mailbox']['quota'] = 'Cuota'; +$lang['mailbox']['in_use'] = 'En uso (%)'; +$lang['mailbox']['msg_num'] = 'Mensaje #'; +$lang['mailbox']['remove'] = 'Eliminar'; +$lang['mailbox']['edit'] = 'Editar'; +$lang['mailbox']['archive'] = 'Archivar'; +$lang['mailbox']['no_record'] = 'Sin registro'; +$lang['mailbox']['add_domain'] = 'Agregar dominio'; +$lang['mailbox']['add_domain_alias'] = 'Agregar alias de dominio'; +$lang['mailbox']['add_mailbox'] = 'Agregar buzón'; +$lang['mailbox']['add_alias'] = 'Agregar alias'; + +$lang['info']['no_action'] = 'No hay acción aplicable'; + +$lang['delete']['title'] = 'Eliminar objeto'; +$lang['delete']['remove_domain_warning'] = 'Advertencia: ¡Estás a punto de eliminar el dominio %s!'; +$lang['delete']['remove_domainalias_warning'] = 'Advertencia: ¡Estás a punto de eliminar el alias de dominio %s!'; +$lang['delete']['remove_domainadmin_warning'] = 'Advertencia: ¡Estás a punto de eliminar el administrador de dominio %s!'; +$lang['delete']['remove_alias_warning'] = 'Advertencia: ¡Estás a punto de eliminar la dirección alias %s!'; +$lang['delete']['remove_mailbox_warning'] = 'Advertencia: ¡Estás a punto de eliminar el buzón %s!'; +$lang['delete']['remove_mailbox_details'] = 'El buzón será purgado permanentemente!'; +$lang['delete']['remove_domain_details'] = 'Esto también eliminará alias de dominio.

    Un dominio debe estar vacío para poder ser eliminado.'; +$lang['delete']['remove_alias_details'] = 'Los usuarios ya no serán capaces de recibir correos o enviar correos desde esta dirección.'; +$lang['delete']['remove_button'] = 'Eliminar'; +$lang['delete']['previous'] = 'Página anterior'; + +$lang['edit']['save'] = 'Guardar cambios'; +$lang['edit']['archive'] = 'Acceso a archivos'; +$lang['edit']['max_mailboxes'] = 'Máx. buzones posibles:'; +$lang['edit']['title'] = 'Editas objeto'; +$lang['edit']['target_address'] = 'Dirección/es goto (separadas por coma):'; +$lang['edit']['active'] = 'Activo'; +$lang['edit']['target_domain'] = 'Dominio destino:'; +$lang['edit']['password'] = 'Contraseña:'; +$lang['edit']['ratelimit'] = 'Límite de la tarifa saliente/h:'; +$lang['danger']['ratelimt_less_one'] = 'El límite de la tarifa saliente/h no puede ser menos que 1'; +$lang['edit']['password_repeat'] = 'Confirmación de contraseña (repetir):'; +$lang['edit']['domain_admin'] = 'Editar administrador del dominio'; +$lang['edit']['domain'] = 'Editar dominio'; +$lang['edit']['alias_domain'] = 'Alias de dominio'; +$lang['edit']['edit_alias_domain'] = 'Editar alias de dominio'; +$lang['edit']['domains'] = 'Dominios'; +$lang['edit']['destroy'] = 'Entrada manual de datos'; +$lang['edit']['alias'] = 'Editar alias'; +$lang['edit']['mailbox'] = 'Editar buzón'; +$lang['edit']['description'] = 'Descripción:'; +$lang['edit']['max_aliases'] = 'Máx. alias:'; +$lang['edit']['max_quota'] = 'Máx. cuota por buzón (MiB):'; +$lang['edit']['domain_quota'] = 'Cuota de dominio:'; +$lang['edit']['backup_mx_options'] = 'Opciones del respaldo MX:'; +$lang['edit']['relay_domain'] = 'Dominio de retransmisión'; +$lang['edit']['relay_all'] = 'Retransmitir todos los recipientes'; +$lang['edit']['dkim_signature'] = 'Firma DKIM:'; +$lang['edit']['dkim_record_info'] = 'Por favor agrega un registro TXT con el siguiente valor a tu configuración DNS.'; +$lang['edit']['relay_all_info'] = 'Si eliges no retransmitir a todos los recipientes, necesitas agregar un buzón "blind"("ciego") por cada recipiente que debe ser retransmitido.'; +$lang['edit']['full_name'] = 'Nombre completo'; +$lang['edit']['full_name'] = 'Nombre completo'; +$lang['edit']['quota_mb'] = 'Cuota (MiB)'; +$lang['edit']['sender_acl'] = 'Permitir envío como:'; +$lang['edit']['sender_acl_info'] = 'Los alias no pueden deseleccionarse.'; +$lang['edit']['dkim_txt_name'] = 'Nombre del registro TXT:'; +$lang['edit']['dkim_txt_value'] = 'Valor del registro TXT:'; +$lang['edit']['previous'] = 'Página anterior'; +$lang['edit']['unchanged_if_empty'] = 'Si no hay cambios dejalo en blanco'; +$lang['edit']['dont_check_sender_acl'] = 'No verifiques remitente para el dominio %s'; + +$lang['add']['title'] = 'Agregar objeto'; +$lang['add']['domain'] = 'Dominio'; +$lang['add']['active'] = 'Activo'; +$lang['add']['save'] = 'Guardar cambios'; +$lang['add']['description'] = 'Descripción:'; +$lang['add']['max_aliases'] = 'Máx. alias posibles:'; +$lang['add']['max_mailboxes'] = 'Máx. buzones posibles:'; +$lang['add']['mailbox_quota_m'] = 'Máx. cuota por buzón (MiB):'; +$lang['add']['domain_quota_m'] = 'Cuota total del dominio (MiB):'; +$lang['add']['backup_mx_options'] = 'Opciones del respaldo MX:'; +$lang['add']['relay_all'] = 'Retransmitir todos los recipientes'; +$lang['add']['relay_domain'] = 'Retransmitir este dominio'; +$lang['add']['relay_all_info'] = 'Si eliges no retransmitir a todos los recipientes, necesitas agregar un buzón "blind"("ciego") por cada recipiente que debe ser retransmitido.'; +$lang['add']['alias'] = 'Alias'; +$lang['add']['alias_spf_fail'] = 'Nota: Si tu dirección destino está en un buzón externo, el servidor de correo que recibe puede rechazar tu mensaje por una falla SPF.'; +$lang['add']['alias_address'] = 'Dirección/es alias:'; +$lang['add']['alias_address_info'] = 'Dirección/es de correo completa/s ó @ejemplo.com, para atrapar todos los mensajes para un dominio (separado por coma). Dominios mailcow solamente.'; +$lang['add']['alias_domain_info'] = 'Nombres de dominio válidos solamente (separado por coma).'; +$lang['add']['target_address'] = 'Direcciones goto:'; +$lang['add']['target_address_info'] = 'Dirección/es de correo completa/s (separado por coma).'; +$lang['add']['alias_domain'] = 'Dominio alias'; +$lang['add']['select'] = 'Por favor selecciona...'; +$lang['add']['target_domain'] = 'Dominio destino:'; +$lang['add']['mailbox'] = 'Buzón'; +$lang['add']['mailbox_username'] = 'Nombre de usuario (parte izquierda de una dirección de correo):'; +$lang['add']['full_name'] = 'Nombre completo:'; +$lang['add']['quota_mb'] = 'Cuota (MiB):'; +$lang['add']['select_domain'] = 'Por favor elige un dominio primero'; +$lang['add']['password'] = 'Constraseña:'; +$lang['add']['password_repeat'] = 'Confirmación de contraseña (repetir):'; +$lang['add']['previous'] = 'Página anterior'; +$lang['add']['restart_sogo_hint'] = '¡Necesitas reiniciar el contenedor del servicio SOGo antes de agregar un nuevo dominio!'; + +$lang['login']['title'] = 'Inicio de sesión'; +$lang['login']['administration'] = 'Administración'; +$lang['login']['administration_details'] = 'Por favor utiliza tu inicio de sesión de Administrador para realizar tareas administrativas.'; +$lang['login']['user_settings'] = 'Configuración de usuario'; +$lang['login']['user_settings_details'] = 'Usuarios de buzón pueden utilizar mailcow UI para cambiar sus contraseñas, crear alias temporales (alias de spam), ajustar el comportamiento del filtro de spam ó importar mensajes desde un servidor IMAP remoto.'; +$lang['login']['username'] = 'Nombre de usuario'; +$lang['login']['password'] = 'Contraseña'; +$lang['login']['reset_password'] = 'Reiniciar mi contraseña'; +$lang['login']['login'] = 'Inicio de sesión'; +$lang['login']['previous'] = "Página anterior"; +$lang['login']['delayed'] = 'El inicio de sesión ha sido retrasado %s segundos.'; + +$lang['login']['tfa'] = "Autenticación de dos factores"; +$lang['login']['tfa_details'] = "Por favor confirma tu contraseña de un solo uso en el campo de abajo"; +$lang['login']['confirm'] = "Confirmar"; +$lang['login']['otp'] = "Contraseña de un solo uso"; +$lang['login']['trash_login'] = "Inicio de sesión basura"; + +$lang['admin']['search_domain_da'] = 'Buscar dominios'; +$lang['admin']['restrictions'] = 'Restricciones Postfix'; +$lang['admin']['rr'] = 'Restricciones Postfix para recipientes'; +$lang['admin']['sr'] = 'Restricciones Postfix para remitentes'; +$lang['admin']['reset_defaults'] = 'Restablecer los valores predeterminados'; +$lang['admin']['sr'] = 'Restricciones Postfix para remitentes'; +$lang['admin']['r_inactive'] = 'Restricciones inactivas'; +$lang['admin']['r_active'] = 'Restricciones activas'; +$lang['admin']['r_info'] = 'Elementos en gris/deshabilitados en la lista de restricciones activas no son reconocidas como restricciones válidas para mailcow y no pueden ser movidas. Restricciones desconocidas serán establecidas en el orden de aparicion de todas maneras.
    Puedes agregar nuevos elementos en inc/vars.local.inc.php para ser capaz de habilitarlas.'; +$lang['admin']['public_folders'] = 'Carpetas Públicas'; +$lang['admin']['public_folders_text'] = 'Un espacio de nombres "Public" (Público) será creado. Debajo del nombre de la carpeta pública se indica el nombre del primer buzón creado automáticamente dentro de este espacio de nombres'; +$lang['admin']['public_folder_name'] = 'Nombre de la carpeta (alfanumérico)'; +$lang['admin']['public_folder_enable'] = 'Habilitar carpeta pública'; +$lang['admin']['public_folder_enable_text'] = 'Activar ésta opción no elimina correos en cualquier otra carpeta pública.'; +$lang['admin']['public_folder_pusf'] = 'Habilitar el indicador visto por usuario'; +$lang['admin']['public_folder_pusf_text'] = 'Un sistema habilitado por indicador "por usuario visto" no marcará un correo como leído para el Usuario B, cuando el Usuario A lo haya visto, pero el Usuario B no.'; +$lang['admin']['privacy'] = 'Privacidad'; +$lang['admin']['privacy_text'] = 'Ésta opción activa una tabla PCRE para remover "User-Agent", "X-Enigmail", "X-Mailer", "X-Originating-IP" y remplaza las cabezeras "Received: from" con localhost/127.0.0.1.'; +$lang['admin']['privacy_anon_mail'] = 'Anonimizar correo saliente'; +$lang['admin']['dkim_txt_name'] = 'Nombre del registro TXT:'; +$lang['admin']['dkim_txt_value'] = 'Valor del registro TXT:'; +$lang['admin']['dkim_key_length'] = 'Longitud de la llave DKIM (bits)'; +$lang['admin']['previous'] = 'Página anterior'; +$lang['admin']['quota_mb'] = 'Cuota (MiB):'; +$lang['admin']['sender_acl'] = 'Permitir envío como:'; +$lang['admin']['msg_size'] = 'Tamaño del mensaje'; +$lang['admin']['msg_size_limit'] = 'Límite del tamaño del mensaje ahora'; +$lang['admin']['msg_size_limit_details'] = 'Aplicando un nuebo límite reiniciará Postfix y el servidor web.'; +$lang['admin']['save'] = 'Guardar cambios'; +$lang['admin']['maintenance'] = 'Mantenimiento e Información'; +$lang['admin']['sys_info'] = 'información del sistema'; +$lang['admin']['dkim_add_key'] = 'Agregar registro DKIM'; +$lang['admin']['dkim_keys'] = 'Registros DKIM'; +$lang['admin']['add'] = 'Agregar'; +$lang['admin']['configuration'] = 'Configuración'; +$lang['admin']['password'] = 'Contraseña'; +$lang['admin']['password_repeat'] = 'Confirmación de contraseña (repetir)'; +$lang['admin']['active'] = 'Activo'; +$lang['admin']['action'] = 'Acción'; +$lang['admin']['add_domain_admin'] = 'Agregar Administrador del dominio'; +$lang['admin']['admin_domains'] = 'Asignaciones de dominio'; +$lang['admin']['domain_admins'] = 'Administradores de dominio'; +$lang['admin']['username'] = 'Nombre de usuario'; +$lang['admin']['edit'] = 'Editar'; +$lang['admin']['remove'] = 'Eliminar'; +$lang['admin']['save'] = 'Guardar cambios'; +$lang['admin']['admin'] = 'Administrador'; +$lang['admin']['admin_details'] = 'Editar detalles del administrador'; +$lang['admin']['unchanged_if_empty'] = 'Si no hay cambios dejalo en blanco'; +$lang['admin']['yes'] = '✔'; +$lang['admin']['no'] = '✘'; +$lang['admin']['access'] = 'Acceso'; +$lang['admin']['invalid_max_msg_size'] = 'Tamaño máx. del mensaje no válido'; +$lang['admin']['site_not_found'] = 'No se puede localizar la configuración del sitio de mailcow'; +$lang['admin']['public_folder_empty'] = 'El nombre de la carpeta pública no debe estar vacío'; +$lang['admin']['set_rr_failed'] = 'No se pueden establecer las restricciones de Postfix'; +$lang['admin']['no_record'] = 'Sin registro'; +?> diff --git a/data/web/lang/lang.nl.php b/data/web/lang/lang.nl.php index 550e5470..35192ddb 100644 --- a/data/web/lang/lang.nl.php +++ b/data/web/lang/lang.nl.php @@ -4,7 +4,10 @@ // Dutch language file */ $lang['footer']['loading'] = "Even geduld a.u.b. ..."; -$lang['getmail']['no_status'] = "Geen vorige status gevonden."; +$lang['header']['restart_sogo'] = 'SOGo herstarten'; +$lang['footer']['restart_sogo'] = 'SOGo herstarten'; +$lang['footer']['restart_now'] = 'Nu opnieuw starten'; +$lang['footer']['restart_sogo_info'] = 'Sommige taken, zoals het toevoegen van een domein, vereisen een herstart van SOGo om de veranderingen door te voeren.

    Belangrijk: Het opnieuw opstarten kan een poos duren, wacht a.u.b. totdat dit volledig voltooid is.'; $lang['dkim']['confirm'] = "Weet u het zeker?"; $lang['danger']['dkim_not_found'] = "DKIM record niet gevonden."; $lang['danger']['dkim_remove_failed'] = "Kan geselecteerde DKIM record niet verwijderen."; @@ -21,8 +24,8 @@ $lang['danger']['object_is_not_numeric'] = "%s is niet numeriek."; $lang['success']['domain_added'] = "Domein toegevoegd: %s."; $lang['danger']['alias_empty'] = "Aliasadres mag niet leeg blijven."; $lang['danger']['goto_empty'] = "Doeladres mag niet leeg blijven."; -$lang['danger']['blacklist_exists'] = "Deze invoer staat op de zwarte lijst."; -$lang['danger']['blacklist_from_invalid'] = "Zwarte lijst invoer heeft een ongeldig format."; +$lang['danger']['policy_list_from_exists'] = "Deze invoer bestaat al."; +$lang['danger']['policy_list_from_invalid'] = "Deze invoer heeft een ongeldig format."; $lang['danger']['whitelist_exists'] = "Deze invoer staat op de witte lijst."; $lang['danger']['whitelist_from_invalid'] = "Witte lijst invoer heeft een ongeldig format."; $lang['danger']['alias_invalid'] = "Aliasadres is ongeldig."; @@ -90,10 +93,10 @@ $lang['user']['user_settings'] = 'Gebruikersinstellingen'; $lang['user']['mailbox_settings'] = 'Postvakinstellingen'; $lang['user']['mailbox_details'] = 'Postvakdetails'; $lang['user']['change_password'] = 'Verander wachtwoord'; -$lang['user']['new_password'] = 'Nieuw wachtwoord:'; +$lang['user']['new_password'] = 'Nieuw wachtwoord'; $lang['user']['save_changes'] = 'Wijzigingen opslaan'; -$lang['user']['password_now'] = 'Huidig wachtwoord (bevestig wijzigingen):'; -$lang['user']['new_password_repeat'] = 'Bevestig wachtwoord (herhalen):'; +$lang['user']['password_now'] = 'Huidig wachtwoord (bevestig wijzigingen)'; +$lang['user']['new_password_repeat'] = 'Bevestig wachtwoord (herhalen)'; $lang['user']['new_password_description'] = 'Vereisten: 6 karakters lang, letters en nummers.'; $lang['user']['did_you_know'] = 'Wist u dat? U kunt tags in het e-mailadres gebruiken ("me+prive@voorbeeld.nl") om berichten automatisch naar een bijbehorende map te sturen (voorbeeld: "prive").'; $lang['user']['spam_aliases'] = 'Tijdelijk e-mailadres'; @@ -120,6 +123,7 @@ $lang['user']['spamfilter_table_rule'] = 'Regel'; $lang['user']['spamfilter_table_action'] = 'Handeling'; $lang['user']['spamfilter_table_empty'] = 'Geen gegevens om weer te geven.'; $lang['user']['spamfilter_table_remove'] = 'verwijder'; +$lang['user']['spamfilter_table_add'] = 'Voeg item toe'; $lang['user']['spamfilter_default_score'] = 'Spamscore:'; $lang['user']['spamfilter_green'] = 'Groen: Dit bericht is geen spam.'; $lang['user']['spamfilter_yellow'] = 'Geel: Dit bericht is mogelijk spam, zal worden gelabeled en verplaatst worden naar de Junk-map.'; @@ -135,7 +139,13 @@ $lang['user']['no_record'] = 'Geen vermelding.'; $lang['user']['misc_settings'] = 'Andere profielinstellingen'; $lang['user']['misc_delete_profile'] = 'Andere profielinstellingen'; -$lang['start']['dashboard'] = '%s - dashboard'; +$lang['user']['tag_handling'] = 'Omgaan met e-mail tags'; +$lang['user']['tag_in_subfolder'] = 'In onderliggende map'; +$lang['user']['tag_in_subject'] = 'In onderwerp'; +$lang['user']['tag_help_explain'] = 'In onderliggende map: maakt onder INBOX een nieuwe map aan met de naam van de tag (bijv.: "INBOX/Facebook").
    +In onderwerp: de tag wordt vóór het oorspronkelijke e-mail onderwerp geplaatst (bijv.: "[Facebook] Mijn nieuws").'; +$lang['user']['tag_help_example'] = 'Voorbeeld van een e-mailadres met tag: ik+Facebook@voorbeeld.org'; +$lang['start']['dashboard'] = '%s - startpagina'; $lang['start']['start_rc'] = 'Open Roundcube'; $lang['start']['start_sogo'] = 'Open SOGo'; $lang['start']['mailcow_apps_detail'] = 'Gebruik een mailcow app om toegang te hebben tot uw e-mails, kalender, contactpersonen en meer.'; @@ -280,6 +290,7 @@ $lang['add']['select_domain'] = 'Selecteer eerst een domein'; $lang['add']['password'] = 'Wachtwoord:'; $lang['add']['password_repeat'] = 'Bevestig wachtwoord (herhalen):'; $lang['add']['previous'] = 'Vorige pagina'; +$lang['add']['restart_sogo_hint'] = 'SOGo dient opnieuw te worden gestart nadat een domein is toegevoegd!'; $lang['login']['title'] = 'Aanmelden'; $lang['login']['administration'] = 'Beheer'; diff --git a/data/web/lang/lang.pt.php b/data/web/lang/lang.pt.php index d2e5cf9b..f8c0e330 100644 --- a/data/web/lang/lang.pt.php +++ b/data/web/lang/lang.pt.php @@ -91,10 +91,10 @@ $lang['user']['user_settings'] = 'Configurações do usuário'; $lang['user']['mailbox_settings'] = 'Configrações da conta'; $lang['user']['mailbox_details'] = 'Detalhes da conta'; $lang['user']['change_password'] = 'Alterar senha'; -$lang['user']['new_password'] = 'Nova senha:'; +$lang['user']['new_password'] = 'Nova senha'; $lang['user']['save_changes'] = 'Salvar'; -$lang['user']['password_now'] = 'Senha atual (confirme a alteração):'; -$lang['user']['new_password_repeat'] = 'Confirmar senha (repetir):'; +$lang['user']['password_now'] = 'Senha atual (confirme a alteração)'; +$lang['user']['new_password_repeat'] = 'Confirmar senha (repetir)'; $lang['user']['new_password_description'] = 'Requerido: mínimo de 6 characteres com letras e números.'; $lang['user']['did_you_know'] = 'Você sabia? Você pode usar tags no endereço de email ("conta+privado@example.com") para classificar as mensagens automaticamente para uma determinada pasta (exemplo: "privado").'; $lang['user']['spam_aliases'] = 'Apelidos temporários'; diff --git a/data/web/mailbox.php b/data/web/mailbox.php index b04523f4..a12a078f 100644 --- a/data/web/mailbox.php +++ b/data/web/mailbox.php @@ -1,7 +1,7 @@ @@ -49,80 +49,32 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; prepare("SELECT - `domain`, - `aliases`, - `mailboxes`, - `maxquota` * 1048576 AS `maxquota`, - `quota` * 1048576 AS `quota`, - CASE `backupmx` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `backupmx`, - CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active` - FROM `domain` WHERE - `domain` IN ( - SELECT `domain` FROM `domain_admins` WHERE `username`= :username AND `active`='1' - ) - OR 'admin'= :admin"); - $stmt->execute(array( - ':username' => $_SESSION['mailcow_cc_username'], - ':admin' => $_SESSION['mailcow_cc_role'], - )); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - } - catch (PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - return false; - } - if(!empty($rows)): - while($row = array_shift($rows)): - try { - $stmt = $pdo->prepare("SELECT COUNT(*) AS `count` FROM `alias` - WHERE `domain`= :domain - AND `address` NOT IN ( - SELECT `username` FROM `mailbox`)"); - $stmt->execute(array(':domain' => $row['domain'])); - $AliasData = $stmt->fetch(PDO::FETCH_ASSOC); - - $stmt = $pdo->prepare("SELECT - COUNT(*) AS `count`, - COALESCE(SUM(`quota`), '0') AS `quota` - FROM `mailbox` - WHERE `domain` = :domain"); - $stmt->execute(array(':domain' => $row['domain'])); - $MailboxData = $stmt->fetch(PDO::FETCH_ASSOC); - } - catch (PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - return false; - } + $domains = mailbox_get_domains(); + if (!empty($domains)): + foreach ($domains as $domain): + $domaindata = mailbox_get_domain_details($domain); ?> - - / - / - - / + + / + / + + / - + - +
    - - + +
    - +
    - - - + + - + @@ -162,6 +114,175 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
    +
    +
    +
    +
    +

    +
    + + + + +
    +
    +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ' . htmlspecialchars($mailboxdata['username']) . '';?> / +
    +
    + % +
    +
    +
    +
    + + + + Login + +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    +
    + + + + +
    +
    +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + +
    +
    + +
    +
    +
    +
    +
    @@ -189,55 +310,43 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; prepare("SELECT - `alias_domain`, - `target_domain`, - CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active` - FROM `alias_domain` - WHERE `target_domain` IN ( - SELECT `domain` FROM `domain_admins` - WHERE `username`= :username - AND `active`='1' - ) - OR 'admin' = :admin"); - $stmt->execute(array( - ':username' => $_SESSION['mailcow_cc_username'], - ':admin' => $_SESSION['mailcow_cc_role'], - )); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - } catch(PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - } - if(!empty($rows)): - while($row = array_shift($rows)): - ?> - - - - - -
    - - -
    - - - + + + + + +
    + + +
    + + + - + + + - + @@ -247,136 +356,7 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
    -
    -
    -
    -
    -

    -
    - - - - -
    -
    -
    - -
    -
    - - - - - - - - - - - - - - - prepare("SELECT - `domain`.`backupmx`, - `mailbox`.`username`, - `mailbox`.`name`, - CASE `mailbox`.`active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`, - `mailbox`.`domain`, - `mailbox`.`quota`, - `quota2`.`bytes`, - `quota2`.`messages` - FROM `mailbox`, `quota2`, `domain` - WHERE (`mailbox`.`username` = `quota2`.`username`) - AND (`domain`.`domain` = `mailbox`.`domain`) - AND (`mailbox`.`domain` IN ( - SELECT `domain` FROM `domain_admins` - WHERE `username`= :username - AND `active`='1' - ) - OR 'admin' = :admin)"); - $stmt->execute(array( - ':username' => $_SESSION['mailcow_cc_username'], - ':admin' => $_SESSION['mailcow_cc_role'], - )); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - } - catch (PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - return false; - } - if(!empty($rows)): - while($row = array_shift($rows)): - ?> - - - - - - - - - - - - - - - - - - - - - - - -
    / - = 90) { - $pbar = "progress-bar-danger"; - } - elseif ($percentInUse >= 75) { - $pbar = "progress-bar-warning"; - } - else { - $pbar = "progress-bar-success"; - } - ?> -
    -
    - % -
    -
    -
    -
    - - -
    -
    - -
    -
    -
    -
    -
    +
    @@ -405,80 +385,52 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; prepare("SELECT - `address`, - `goto`, - `domain`, - CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active` - FROM alias - WHERE ( - `address` NOT IN ( - SELECT `username` FROM `mailbox` - ) - AND `address` != `goto` - ) AND (`domain` IN ( - SELECT `domain` FROM `domain_admins` - WHERE `username` = :username - AND active='1' - ) - OR 'admin' = :admin)"); - $stmt->execute(array( - ':username' => $_SESSION['mailcow_cc_username'], - ':admin' => $_SESSION['mailcow_cc_role'], - )); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - } - catch (PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - return false; - } - if(!empty($rows)): - while($row = array_shift($rows)): + if (!empty($domains)) { + foreach (array_merge(mailbox_get_domains(), mailbox_get_alias_domains()) as $domain) { + $aliases = mailbox_get_aliases($domain); + if (!empty($aliases)) { + foreach ($aliases as $alias) { + $aliasdata = mailbox_get_alias_details($alias); ?> - - Catch-all @ - + Catch-all ' . htmlspecialchars($aliasdata['address']) : htmlspecialchars($aliasdata['address']); ?> - - + +
    - - + +
    - - - + + + + + - + diff --git a/data/web/u2f_api.php b/data/web/u2f_api.php new file mode 100644 index 00000000..ddeb1ece --- /dev/null +++ b/data/web/u2f_api.php @@ -0,0 +1,156 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); +$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ); + +$u2f = new u2flib_server\U2F('https://' . $_SERVER['SERVER_NAME']); + +function getRegs($username) { + global $pdo; + $sel = $pdo->prepare("select * from tfa where username = ?"); + $sel->execute(array($username)); + return $sel->fetchAll(); +} +function addReg($username, $reg) { + global $pdo; + $ins = $pdo->prepare("INSERT INTO `tfa` (`username`, `keyHandle`, `publicKey`, `certificate`, `counter`) values (?, ?, ?, ?, ?)"); + $ins->execute(array($username, $reg->keyHandle, $reg->publicKey, $reg->certificate, $reg->counter)); +} +function updateReg($reg) { + global $pdo; + $upd = $pdo->prepare("update tfa set counter = ? where id = ?"); + $upd->execute(array($reg->counter, $reg->id)); +} +?> + + + +getRegisterData(getRegs($username)); + list($req, $sigs) = $data; + $_SESSION['regReq'] = json_encode($req); +?> + +getMessage(); + } + break; + + case 'authenticate': + try { + $reqs = json_encode($u2f->getAuthenticateData(getRegs($username))); + $_SESSION['authReq'] = $reqs; +?> + +getMessage(); + } + break; + } + } + if (!empty($_POST['u2f_register_data'])) { + try { + $reg = $u2f->doRegister(json_decode($_SESSION['regReq']), json_decode($_POST['u2f_register_data'])); + addReg($username, $reg); + } + catch (Exception $e) { + echo "U2F error: " . $e->getMessage(); + } + finally { + echo "Success"; + $_SESSION['regReq'] = null; + } + } + if (!empty($_POST['u2f_auth_data'])) { + try { + $reg = $u2f->doAuthenticate(json_decode($_SESSION['authReq']), getRegs($username), json_decode($_POST['u2f_auth_data'])); + updateReg($reg); + } + catch (Exception $e) { + echo "U2F error: " . $e->getMessage(); + } + finally { + echo "Success"; + $_SESSION['authReq'] = null; + } + } + } +?> + + +
    +
    + + +
    +
    +
    + +
    +Username:

    +Action:
    + Register
    + Authenticate
    + +
    + + + diff --git a/data/web/user.php b/data/web/user.php index 08d129d7..94f2a762 100644 --- a/data/web/user.php +++ b/data/web/user.php @@ -1,90 +1,134 @@ +
    +

    +
    +
    +
    +
    +
    +

    []

    +
    +
    +
    +
    +
    +
    +

    +
    + +
    + +
    ?? []
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    -

    -

    +

    -
    -
    -
    -
    - -
    -
    +
    +
    +

    []

    -
    -
    - -
    - -
    -
    -
    - -
    - -

    -
    -
    -
    -
    -
    - -
    - -
    -
    -
    -
    - -
    -
    - +

    + $user_get_alias_details = user_get_alias_details($username);?>
    :
    -

    +

    +
    +
    +
    +
    :
    +
    +

    :
    -

    +

    :
    -

    +

    :
    -

    +

    +
    +
    +
    +
    +
    :
    +
    +
    +
    + % +
    +
    +

    / ,


    - +
    :
    - + + + +
    +
    +
    + + 1h + + +
    +
    +
    + - +
    +
    +
    - - -
    +
    - +
    - -
    @@ -189,7 +248,7 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user data-slider-range="true" data-slider-tooltip='always' data-slider-id="slider1" - data-slider-value="[]" + data-slider-value="[]" data-slider-step="1" />

      @@ -203,7 +262,7 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user
    - +
    @@ -217,49 +276,39 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user
    prepare("SELECT `value`, `prefid` FROM `filterconf` WHERE `option`='whitelist_from' AND `object`= :username"); - $stmt->execute(array(':username' => $username)); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - } - catch(PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - } - if (count($rows) == 0): + $get_policy_list = get_policy_list($username); + if (empty($get_policy_list['whitelist'])): ?>
    -
    -
    -
    -
    - - - - - - - -
    -
    -
    - - +
    +
    +
    +
    + + + + + + + +
    +
    +
    +
    @@ -269,7 +318,7 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user
    - +
    @@ -282,48 +331,38 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user
    prepare("SELECT `value`, `prefid` FROM `filterconf` WHERE `option`='blacklist_from' AND `object`= :username"); - $stmt->execute(array(':username' => $username)); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - } - catch(PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - } - if (count($rows) == 0): + if (empty($get_policy_list['blacklist'])): ?>
    -
    -
    -
    -
    - - - - - - - -
    -
    -
    - +
    +
    +
    +
    + + + + + + + +
    +
    +
    +
    @@ -333,7 +372,7 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user
    - +
    @@ -359,19 +398,144 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user
    - +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Server:PortLog
    ' . $row['exclude'] . '';?> min + + Open logs + + +
    + + +
    +
    + +
    +
    +
    + + + + + + +
    + -
    + /bin/bash -c " + sleep 5; + /usr/bin/rspamd -f -u _rspamd -g _rspamd + " depends_on: - - pdns-mailcow + nginx-mailcow: + condition: service_healthy volumes: - ./data/conf/rspamd/override.d/:/etc/rspamd/override.d:ro - ./data/conf/rspamd/local.d/:/etc/rspamd/local.d:ro @@ -60,19 +73,21 @@ services: - dkim-vol-1:/data/dkim - rspamd-vol-1:/var/lib/rspamd restart: always - dns: + dns: - 172.22.1.254 dns_search: mailcow-network networks: mailcow-network: + ipv4_address: 172.22.1.253 aliases: - rspamd php-fpm-mailcow: - image: andryyy/mailcow-dockerized:phpfpm + image: mailcow/phpfpm + build: ./data/Dockerfiles/php-fpm command: "php-fpm -d date.timezone=${TZ}" depends_on: - - pdns-mailcow + - bind9-mailcow volumes: - ./data/web:/web:ro - ./data/conf/rspamd/dynmaps:/dynmaps:ro @@ -93,10 +108,10 @@ services: - phpfpm sogo-mailcow: - image: andryyy/mailcow-dockerized:sogo + image: mailcow/sogo + build: ./data/Dockerfiles/sogo depends_on: - - pdns-mailcow - - mysql-mailcow + - bind9-mailcow environment: - DBNAME=${DBNAME} - DBUSER=${DBUSER} @@ -111,13 +126,15 @@ services: restart: always networks: mailcow-network: + ipv4_address: 172.22.1.252 aliases: - sogo rmilter-mailcow: - image: andryyy/mailcow-dockerized:rmilter + image: mailcow/rmilter + build: ./data/Dockerfiles/rmilter depends_on: - - pdns-mailcow + - bind9-mailcow volumes: - ./data/conf/rmilter/:/etc/rmilter.conf.d/:ro restart: always @@ -130,25 +147,25 @@ services: - rmilter dovecot-mailcow: - image: andryyy/mailcow-dockerized:dovecot + image: mailcow/dovecot + build: ./data/Dockerfiles/dovecot depends_on: - - pdns-mailcow + - bind9-mailcow volumes: - ./data/conf/dovecot:/etc/dovecot - ./data/assets/ssl:/etc/ssl/mail/:ro + - ./data/conf/sogo/:/etc/sogo/ - vmail-vol-1:/var/vmail - volumes_from: - - sogo-mailcow environment: - DBNAME=${DBNAME} - DBUSER=${DBUSER} - DBPASS=${DBPASS} ports: - - "${IMAP_PORT}:143" - - "${IMAPS_PORT}:993" - - "${POP_PORT}:110" - - "${POPS_PORT}:995" - - "${SIEVE_PORT}:4190" + - "${IMAP_PORT:-143}:143" + - "${IMAPS_PORT:-993}:993" + - "${POP_PORT-110}:110" + - "${POPS_PORT:-995}:995" + - "${SIEVE_PORT:-4190}:4190" dns: - 172.22.1.254 dns_search: mailcow-network @@ -160,24 +177,26 @@ services: - dovecot postfix-mailcow: - image: andryyy/mailcow-dockerized:postfix + image: mailcow/postfix + build: ./data/Dockerfiles/postfix depends_on: - - pdns-mailcow + - bind9-mailcow volumes: - ./data/conf/postfix:/opt/postfix/conf - ./data/assets/ssl:/etc/ssl/mail/:ro + - postfix-vol-1:/var/spool/postfix - var-vol-1:/var/log/ environment: - DBNAME=${DBNAME} - DBUSER=${DBUSER} - DBPASS=${DBPASS} ports: - - "${SMTP_PORT}:25" - - "${SMTPS_PORT}:465" - - "${SUBMISSION_PORT}:587" + - "${SMTP_PORT:-25}:25" + - "${SMTPS_PORT:-465}:465" + - "${SUBMISSION_PORT:-587}:587" restart: always hostname: ${MAILCOW_HOSTNAME} - dns: + dns: - 172.22.1.254 dns_search: mailcow-network networks: @@ -188,7 +207,7 @@ services: memcached-mailcow: image: memcached depends_on: - - pdns-mailcow + - bind9-mailcow restart: always dns: - 172.22.1.254 @@ -200,26 +219,37 @@ services: nginx-mailcow: depends_on: - - mysql-mailcow - sogo-mailcow - php-fpm-mailcow - - rspamd-mailcow image: nginx:mainline - volumes_from: - - sogo-mailcow + healthcheck: + test: ["CMD", "ping", "php-fpm-mailcow", "-c", "10"] + interval: 10s + timeout: 30s + retries: 5 + command: /bin/bash -c "envsubst < /etc/nginx/conf.d/templates/listen_plain.template > /etc/nginx/conf.d/listen_plain.active && + envsubst < /etc/nginx/conf.d/templates/listen_ssl.template > /etc/nginx/conf.d/listen_ssl.active && + envsubst < /etc/nginx/conf.d/templates/server_name.template > /etc/nginx/conf.d/server_name.active && + nginx -g 'daemon off;'" + environment: + - HTTPS_PORT=${HTTPS_PORT:-443} + - HTTP_PORT=${HTTP_PORT:-80} + - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} volumes: - ./data/web:/web:ro - ./data/conf/rspamd/dynmaps:/dynmaps:ro - ./data/assets/ssl/:/etc/ssl/mail/:ro - - ./data/conf/nginx/:/etc/nginx/conf.d/:ro + - ./data/conf/nginx/:/etc/nginx/conf.d/:rw dns: - 172.22.1.254 dns_search: mailcow-network ports: - - "443:443" + - "${HTTPS_BIND:-0.0.0.0}:${HTTPS_PORT:-443}:${HTTPS_PORT:-443}" + - "${HTTP_BIND:-127.0.0.1}:${HTTP_PORT:-80}:${HTTP_PORT:-80}" restart: always networks: mailcow-network: + ipv4_address: 172.22.1.251 aliases: - nginx @@ -230,7 +260,6 @@ networks: driver: default config: - subnet: 172.22.1.0/24 - gateway: 172.22.1.1 volumes: vmail-vol-1: @@ -238,4 +267,5 @@ volumes: dkim-vol-1: redis-vol-1: rspamd-vol-1: + postfix-vol-1: var-vol-1: diff --git a/docs/first_steps.md b/docs/first_steps.md new file mode 100644 index 00000000..2202794c --- /dev/null +++ b/docs/first_steps.md @@ -0,0 +1,171 @@ +## SSL (and: How to use Let's Encrypt) + +mailcow dockerized comes with a snakeoil CA "mailcow" and a server certificate in `data/assets/ssl`. Please use your own trusted certificates. + +mailcow uses 3 domain names that should be covered by your new certificate: + +- ${MAILCOW_HOSTNAME} +- autodiscover.**example.org** +- autoconfig.**example.org** + +### Obtain multi-SAN certificate by Let's Encrypt + +This is just an example of how to obtain certificates with certbot. There are several methods! + +1\. Get the certbot client: +``` bash +wget https://dl.eff.org/certbot-auto -O /usr/local/sbin/certbot && chmod +x /usr/local/sbin/certbot +``` + +2\. Make sure you set `HTTP_BIND=0.0.0.0` and `HTTP_PORT=80` in `mailcow.conf` or setup a reverse proxy to enable connections to port 80. If you changed HTTP_BIND, then restart Nginx: +``` bash +docker-compose restart nginx-mailcow +``` + +3\. Request the certificate with the webroot method: +``` bash +cd /path/to/git/clone/mailcow-dockerized +source mailcow.conf +certbot certonly \ + --webroot \ + -w ${PWD}/data/web \ + -d ${MAILCOW_HOSTNAME} \ + -d autodiscover.example.org \ + -d autoconfig.example.org \ + --email you@example.org \ + --agree-tos +``` + +4\. Create hard links to the full path of the new certificates. Assuming you are still in the mailcow root folder: +``` bash +mv data/assets/ssl/cert.{pem,pem.backup} +mv data/assets/ssl/key.{pem,pem.backup} +ln $(readlink -f /etc/letsencrypt/live/${MAILCOW_HOSTNAME}/fullchain.pem) data/assets/ssl/cert.pem +ln $(readlink -f /etc/letsencrypt/live/${MAILCOW_HOSTNAME}/privkey.pem) data/assets/ssl/key.pem +``` + +5\. Restart affected containers: +``` +docker-compose restart postfix-mailcow dovecot-mailcow nginx-mailcow +``` + +When renewing certificates, run the last two steps (link + restart) as post-hook in a script. + +## Rspamd Web UI +At first you may want to setup Rspamds web interface which provides some useful features and information. + +1\. Generate a Rspamd controller password hash: +``` +docker-compose exec rspamd-mailcow rspamadm pw +``` + +2\. Replace the default hash in `data/conf/rspamd/override.d/worker-controller.inc` by your newly generated: +``` +enable_password = "myhash"; +``` + +3\. Restart rspamd: +``` +docker-compose restart rspamd-mailcow +``` + +Open https://${MAILCOW_HOSTNAME}/rspamd in a browser and login! + +## Optional: Reverse proxy + +You don't need to change the Nginx site that comes with mailcow: dockerized. +mailcow: dockerized trusts the default gateway IP 172.22.1.1 as proxy. This is very important to control access to Rspamd's web UI. + +1\. Make sure you change HTTP_BIND and HTTPS_BIND in `mailcow.conf` to a local address and set the ports accordingly, for example: +``` bash +HTTP_BIND=127.0.0.1 +HTTP_PORT=8080 +HTTPS_PORT=127.0.0.1 +HTTPS_PORT=8443 +``` +** IMPORTANT: Do not use port 8081 ** + +Recreate affected containers by running `docker-compose up -d`. + +2\. Configure your local webserver as reverse proxy: + +### Apache 2.4 +``` apache + + ServerName mail.example.org + ServerAlias autodiscover.example.org + ServerAlias autoconfig.example.org + + [...] + # You should proxy to a plain HTTP session to offload SSL processing + ProxyPass / http://127.0.0.1:8080/ + ProxyPassReverse / http://127.0.0.1:8080/ + ProxyPreserveHost Off + your-ssl-configuration-here + [...] + + # If you plan to proxy to a HTTPS host: + #SSLProxyEngine On + + # If you plan to proxy to an untrusted HTTPS host: + #SSLProxyVerify none + #SSLProxyCheckPeerCN off + #SSLProxyCheckPeerName off + #SSLProxyCheckPeerExpire off + +``` + +### Nginx +``` +server { + listen 443; + server_name mail.example.org autodiscover.example.org autoconfig.example.org; + + [...] + your-ssl-configuration-here + location / { + proxy_pass http://127.0.0.1:8080/; + proxy_redirect http://127.0.0.1:8080/ $scheme://$host:$server_port/; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + [...] +} +``` + +## Sender and receiver model + +When a mailbox is created, a user is allowed to send mail from and receive mail for his own mailbox address. + + Mailbox me@example.org is created. example.org is a primary domain. + Note: a mailbox cannot be created in an alias domain. + + me@example.org is only known as me@example.org. + me@example.org is allowed to send as me@example.org. + +We can add an alias domain for example.org: + + Alias domain alias.com is added and assigned to primary domain example.org. + me@example.org is now known as me@example.org and me@alias.com. + me@example.org is now allowed to send as me@example.org and me@alias.com. + +We can add aliases for a mailbox to receive mail for and to send from this new address. + +It is important to know, that you are not able to receive mail for `my-alias@my-alias-domain.tld`. You would need to create this particular alias. + + me@example.org is assigned the alias alias@example.org + me@example.org is now known as alias@example.org, me@alias.com, alias@example.org + + me@example.org is NOT known as alias@alias.com. + +Administrators and domain administrators can edit mailboxes to allow specific users to send as other mailbox users ("delegate" them). + +You can choose between mailbox users or completely disable the sender check for domains. + +### SOGo "mail from" addresses + +Mailbox users can, obviously, select their own mailbox address, as well as all alias addresses and aliases that exist through alias domains. + +If you want to select another _existing_ mailbox user as your "mail from" address, this user has to delegate you access through SOGo (see SOGo documentation). Moreover a mailcow (domain) administrator +needs to grant you access as described above. diff --git a/docs/images/logo.svg b/docs/images/logo.svg new file mode 100644 index 00000000..ea3b2796 --- /dev/null +++ b/docs/images/logo.svg @@ -0,0 +1,179 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..f4aea569 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,48 @@ +# mailcow: dockerized - 🐮 + 🐋 = 💕 + +[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=JWBSYHF4SMC68) + +## Screenshots + +You can find screenshots [on Imgur](http://imgur.com/a/oewYt). + +## Overview + +mailcow dockerized comes with **11 containers** linked in **one bridged network**. + +- Dovecot +- Memcached +- Redis +- MySQL +- Bind9 (Resolver) (formerly PDNS Recursor) +- PHP-FPM +- Postfix +- Nginx +- Rmilter +- Rspamd +- SOGo + +**6 volumes** to keep dynamic data - take care of them! + +- vmail-vol-1 +- dkim-vol-1 +- redis-vol-1 +- mysql-vol-1 +- rspamd-vol-1 +- postfix-vol-1 + +The integrated **mailcow UI** allows administrative work on your mail server instance as well as separated domain administrator and mailbox user access: + +- DKIM key management +- Black- and whitelists per domain and per user +- Spam score managment per-user (reject spam, mark spam, greylist) +- Allow mailbox users to create temporary spam aliases +- Prepend mail tags to subject or move mail to subfolder (per-user) +- Allow mailbox users to toggle incoming and outgoing TLS enforcement +- Allow users to reset SOGo ActiveSync device caches +- imapsync to migrate or pull remote mailboxes regularly +- TFA: Yubi OTP and U2F USB (Google Chrome and derivates only) +- Add domains, mailboxes, aliases, domain aliases and SOGo resources + + +*[Looking for a farm to host your cow?](https://www.servercow.de)* diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 00000000..1274ed84 --- /dev/null +++ b/docs/install.md @@ -0,0 +1,76 @@ +## Install mailcow + +You need Docker and Docker Compose. + +1\. Learn how to install [Docker](https://docs.docker.com/engine/installation/linux/) and [Docker Compose](https://docs.docker.com/compose/install/). + +Quick installation for most operation systems: + +- Docker +``` +curl -sSL https://get.docker.com/ | sh +``` + +- Docker-Compose +``` +curl -L https://github.com/docker/compose/releases/download/$(curl -Ls https://www.servercow.de/docker-compose/latest.php)/docker-compose-$(uname -s)-$(uname -m) > /usr/local/bin/docker-compose +chmod +x /usr/local/bin/docker-compose +``` + +Please use the latest Docker engine available and do not use the engine that ships with your distros repository. + +2\. Clone the master branch of the repository +``` +git clone https://github.com/andryyy/mailcow-dockerized && cd mailcow-dockerized +``` + +3\. Generate a configuration file. Use a FQDN (`host.domain.tld`) as hostname when asked. +``` +./generate_config.sh +``` + +4\. Change configuration if you want or need to. +``` +nano mailcow.conf +``` +If you plan to use a reverse proxy, you can, for example, bind HTTPS to 127.0.0.1 on port 8443 and HTTP to 127.0.0.1 on port 8080. + +5\. Pull the images and run the composer file. The paramter `-d` will start mailcow: dockerized detached: +``` +docker-compose pull +docker-compose up -d +``` + +Done! + +You can now access **https://${MAILCOW_HOSTNAME}** with the default credentials `admin` + password `moohoo`. + +The database will be initialized right after a connection to MySQL can be established. + +## Update mailcow + +There is no update routine. You need to refresh your pulled repository clone and apply your local changes (if any). Actually there are many ways to merge local changes. Here is one to +stash all local changes, pull changes from the remote master branch and apply your stash on top of it. You will most likely see warnings about non-commited changes; you can ignore them: + +``` +# Stash local changes +git stash +# Re-pull master +git pull +# Apply stash and remove it +git stash pop +``` + +Pull new images (if any) and recreate changed containers: + +``` +docker-compose pull +docker-compose up -d --remove-orphans +``` + +Clean-up dangling (unused) images and volumes: + +``` +docker rmi -f $(docker images -f "dangling=true" -q) +docker volume rm $(docker volume ls -qf dangling=true) +``` diff --git a/docs/u_and_e.md b/docs/u_and_e.md new file mode 100644 index 00000000..bcb47517 --- /dev/null +++ b/docs/u_and_e.md @@ -0,0 +1,413 @@ +## Anonymize headers + +Save as `data/conf/postfix/mailcow_anonymize_headers.pcre`: + +``` +/^\s*Received:[^\)]+\)\s+\(Authenticated sender:(.+)/ + REPLACE Received: from localhost (localhost [127.0.0.1]) (Authenticated sender:$1 +/^\s*User-Agent/ IGNORE +/^\s*X-Enigmail/ IGNORE +/^\s*X-Mailer/ IGNORE +/^\s*X-Originating-IP/ IGNORE +/^\s*X-Forward/ IGNORE +``` + +Add this to `data/conf/postfix/main.cf`: +``` +smtp_header_checks = pcre:/opt/postfix/conf/mailcow_anonymize_headers.pcre +``` + +## Backup and restore maildir (simple tar file) + +### Backup + +This line backups the vmail directory to a file backup_vmail.tar.gz in the mailcow root directory: +``` +cd /path/to/mailcow-dockerized +source mailcow.conf +DATE=$(date +"%Y%m%d_%H%M%S") +docker run --rm -it -v $(docker inspect --format '{{ range .Mounts }}{{ if eq .Destination "/var/vmail" }}{{ .Name }}{{ end }}{{ end }}' $(docker-compose ps -q dovecot-mailcow)):/vmail -v ${PWD}:/backup debian:jessie tar cvfz /backup/backup_vmail.tar.gz /vmail +``` + +You can change the path by adjusting ${PWD} (which equals to the current directory) to any path you have write-access to. +Set the filename `backup_vmail.tar.gz` to any custom name, but leave the path as it is. Example: `[...] tar cvfz /backup/my_own_filename_.tar.gz` + +### Restore +``` +cd /path/to/mailcow-dockerized +source mailcow.conf +DATE=$(date +"%Y%m%d_%H%M%S") +docker run --rm -it -v $(docker inspect --format '{{ range .Mounts }}{{ if eq .Destination "/var/vmail" }}{{ .Name }}{{ end }}{{ end }}' $(docker-compose ps -q dovecot-mailcow)):/vmail -v ${PWD}:/backup debian:jessie tar xvfz /backup/backup_vmail.tar.gz +``` + +## Docker Compose Bash completion + +For the tab-tab... :-) + +``` +curl -L https://raw.githubusercontent.com/docker/compose/$(docker-compose version --short)/contrib/completion/bash/docker-compose -o /etc/bash_completion.d/docker-compose +``` +## Black and Whitelist + +Edit a domain as (domain) administrator to add an item to the filter table. + +Beware that a mailbox user can login to mailcow and override a domain policy filter item. + +## Change default language + +Change `data/conf/sogo/sogo.conf` and replace "English" by your preferred language. + +Create a file `data/web/inc/vars.local.inc.php` and add "DEFAULT_LANG" with either "en", "pt", "de" or "nl": +``` + array('verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true) +); +$config['enable_installer'] = false; +$config['smtp_conn_options'] = array( +'ssl' => array('verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true) +); +``` + +Point your browser to `https://myserver/rc/installer` and follow the instructions. +Initialize the database and leave the installer. + +**Delete the directory `data/web/rc/installer` after a successful installation!** + +### Enable change password function in Roundcube + +Open `data/web/rc/config/config.inc.php` and enable the password plugin: + +``` +... +$config['plugins'] = array( + 'archive', + 'password', +); +... +``` + +Open `data/web/rc/plugins/password/password.php`, search for `case 'ssha':` and add above: + +``` + case 'ssha256': + $salt = rcube_utils::random_bytes(8); + $crypted = base64_encode( hash('sha256', $password . $salt, TRUE ) . $salt ); + $prefix = '{SSHA256}'; + break; +``` + +Open `data/web/rc/plugins/password/config.inc.php` and change the following parameters (or add them at the bottom of that file): + +``` +$config['password_driver'] = 'sql'; +$config['password_algorithm'] = 'ssha256'; +$config['password_algorithm_prefix'] = '{SSHA256}'; +$config['password_query'] = "UPDATE mailbox SET password = %P WHERE username = %u"; +``` + +## MySQL + +### Connect +``` +source mailcow.conf +docker-compose exec mysql-mailcow mysql -u${DBUSER} -p${DBPASS} ${DBNAME} +``` + +### Backup +``` +cd /path/to/mailcow-dockerized +source mailcow.conf +DATE=$(date +"%Y%m%d_%H%M%S") +docker-compose exec mysql-mailcow mysqldump --default-character-set=utf8mb4 -u${DBUSER} -p${DBPASS} ${DBNAME} > backup_${DBNAME}_${DATE}.sql +``` + +### Restore +``` +cd /path/to/mailcow-dockerized +source mailcow.conf +docker-compose exec mysql-mailcow mysql -u${DBUSER} -p${DBPASS} ${DBNAME} < backup_file.sql +``` + +## Debugging + +You can use `docker-compose logs $service-name` for all containers. + +Run `docker-compose logs` for all logs at once. + +Follow the log output by running docker-compose with `logs -f`. + +## Redirect port 80 to 443 + +Since February the 28th 2017 mailcow does come with port 80 and 443 enabled. + +Open `mailcow.conf` and set `HTTP_BIND=0.0.0.0`. + +Open `data/conf/nginx/site.conf` and add a new "catch-all" site at the top of that file: + +``` +server { + listen 80 default_server; + server_name _; + return 301 https://$host$request_uri; +} +``` + +Restart the stack, changed containers will be updated: + +`docker-compose up -d` + +## Redis + +### Client + +``` +docker-compose exec redis-mailcow redis-cli +``` + +## Remove persistent data + +- Remove volume `mysql-vol-1` to remove all MySQL data. +- Remove volume `redis-vol-1` to remove all Redis data. +- Remove volume `vmail-vol-1` to remove all contents of `/var/vmail` mounted to `dovecot-mailcow`. +- Remove volume `dkim-vol-1` to remove all DKIM keys. +- Remove volume `rspamd-vol-1` to remove all Rspamd data. + +Running `docker-compose down -v` will **destroy all mailcow: dockerized volumes** and delete any related containers. + +## Reset admin password +Reset mailcow admin to `admin:moohoo`: + +1\. Drop admin table +``` +source mailcow.conf +docker-compose exec mysql-mailcow mysql -u${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP TABLE admin;" +``` + +2\. Open mailcow UI to auto-init the db + +## Rspamd + +### Learn spam and ham + +Rspamd learns mail as spam or ham when you move a message in or out of the junk folder to any mailbox besides trash. +This is archived by using the Dovecot plugin "antispam" and a simple parser script. + +Rspamd also auto-learns mail when a high or low score is detected (see https://rspamd.com/doc/configuration/statistic.html#autolearning) + +The bayes statistics are written to Redis as keys `BAYES_HAM` and `BAYES_SPAM`. + +You can also use Rspamd's web ui to learn ham and/or spam. + +### CLI tools + +``` +docker-compose exec rspamd-mailcow rspamc --help +docker-compose exec rspamd-mailcow rspamadm --help +``` + +See [Rspamd documentation](https://rspamd.com/doc/index.html) + +## Adjust service configurations + +The most important configuration files are mounted from the host into the related containers: + +``` +data/conf +├── bind9 +│   └── named.conf +├── dovecot +│   ├── dovecot.conf +│   ├── dovecot-master.passwd +│   ├── sieve_after +│   └── sql +│   ├── dovecot-dict-sql.conf +│   └── dovecot-mysql.conf +├── mysql +│   └── my.cnf +├── nginx +│   ├── dynmaps.conf +│   ├── site.conf +│   └── templates +│   ├── listen_plain.template +│   ├── listen_ssl.template +│   └── server_name.template +├── pdns +│   ├── pdns_custom.lua +│   └── recursor.conf +├── postfix +│   ├── main.cf +│   ├── master.cf +│   ├── postscreen_access.cidr +│   ├── smtp_dsn_filter +│   └── sql +│   ├── mysql_relay_recipient_maps.cf +│   ├── mysql_tls_enforce_in_policy.cf +│   ├── mysql_tls_enforce_out_policy.cf +│   ├── mysql_virtual_alias_domain_catchall_maps.cf +│   ├── mysql_virtual_alias_domain_maps.cf +│   ├── mysql_virtual_alias_maps.cf +│   ├── mysql_virtual_domains_maps.cf +│   ├── mysql_virtual_mailbox_maps.cf +│   ├── mysql_virtual_relay_domain_maps.cf +│   ├── mysql_virtual_sender_acl.cf +│   └── mysql_virtual_spamalias_maps.cf +├── rmilter +│   └── rmilter.conf +├── rspamd +│   ├── dynmaps +│   │   ├── authoritative.php +│   │   ├── settings.php +│   │   ├── tags.php +│   │   └── vars.inc.php -> ../../../web/inc/vars.inc.php +│   ├── local.d +│   │   ├── dkim.conf +│   │   ├── metrics.conf +│   │   ├── options.inc +│   │   ├── redis.conf +│   │   ├── rspamd.conf.local +│   │   └── statistic.conf +│   ├── lua +│   │   └── rspamd.local.lua +│   └── override.d +│   ├── logging.inc +│   ├── worker-controller.inc +│   └── worker-normal.inc +└── sogo + ├── sieve.creds + └── sogo.conf + +``` + +Just change the according configuration file on the host and restart the related service: +``` +docker-compose restart service-mailcow +``` + +## Tagging + +Mailbox users can tag their mail address like in `me+facebook@example.org` and choose between to setups to handle this tag: + +1\. Move this message to a subfolder "facebook" (will be created lower case if not existing) + +2\. Prepend the tag to the subject: "[facebook] Subject" + +## Two-factor authentication + +So far two methods for TFA are implemented. Both work with the fantastic [Yubikey](https://www.yubico.com). + +While Yubi OTP needs an active internet connection and an API ID and key, U2F will work with any FIDO U2F USB key out of the box, but can only be used when mailcow is accessed over HTTPS. + +Both methods support multiple YubiKeys. + +As administrator you are able to temporary disable a domain administrators TFA login until they successfully logged in. + +The key used to login will be displayed in green, while other keys remain grey. + +### Yubi OTP + +The Yubi API ID and Key will be checked against the Yubico Cloud API. When setting up TFA you will be asked for your personal API account for this key. +The API ID, API key and the first 12 characters (your YubiKeys ID in modhex) are stored in the MySQL table as secret. + +### U2F + +Only Google Chrome (+derivates) and Opera support U2F authentication to this day natively. +For Firefox you will need to install the "U2F Support Add-on" as provided on [mozilla.org](https://addons.mozilla.org/en-US/firefox/addon/u2f-support-add-on/). + +U2F works without an internet connection. + +## Why Bind? + +For DNS blacklist lookups and DNSSEC. + +Most systems use either a public or a local caching DNS resolver. +That's a very bad idea when it comes to filter spam using DNS-based blackhole lists (DNSBL) or similar technics. +Most if not all providers apply a rate limit based on the DNS resolver that is used to query their service. +Using a public resolver like Googles 4x8, OpenDNS or any other shared DNS resolver like your ISPs will hit that limit very soon. diff --git a/generate_config.sh b/generate_config.sh index cb1a516e..ae3862a8 100755 --- a/generate_config.sh +++ b/generate_config.sh @@ -12,8 +12,16 @@ if [[ -f mailcow.conf ]]; then esac fi -read -p "Hostname (FQDN): " -ei "mx.example.org" MAILCOW_HOSTNAME -read -p "Timezone: " -ei "Europe/Berlin" TZ +if [ -z "$MAILCOW_HOSTNAME" ]; then + read -p "Hostname (FQDN): " -ei "mx.example.org" MAILCOW_HOSTNAME +fi + +[[ -a /etc/timezone ]] && TZ=$(cat /etc/timezone) +if [ -z "$TZ" ]; then + read -p "Timezone: " -ei "Europe/Berlin" TZ +else + read -p "Timezone: " -ei ${TZ} TZ +fi cat << EOF > mailcow.conf # ------------------------------ @@ -35,10 +43,23 @@ DBPASS=$(