andryyy 8 年之前
父节点
当前提交
d891bc8894
共有 100 个文件被更改,包括 20416 次插入310 次删除
  1. 1 0
      .env
  2. 8 0
      .gitignore
  3. 5 0
      README.md
  4. 0 124
      css/highlight.css
  5. 0 8
      css/theme.css
  6. 0 178
      css/theme_extra.css
  7. 87 0
      data/Dockerfiles/dovecot/Dockerfile
  8. 28 0
      data/Dockerfiles/dovecot/docker-entrypoint.sh
  9. 9488 0
      data/Dockerfiles/dovecot/imapsync
  10. 72 0
      data/Dockerfiles/dovecot/imapsync_cron.pl
  11. 4 0
      data/Dockerfiles/dovecot/postlogin.sh
  12. 8 0
      data/Dockerfiles/dovecot/rspamd-pipe
  13. 21 0
      data/Dockerfiles/dovecot/supervisord.conf
  14. 0 0
      data/Dockerfiles/memcached/.empty
  15. 0 0
      data/Dockerfiles/mysql/.empty
  16. 0 0
      data/Dockerfiles/nginx/.empty
  17. 26 0
      data/Dockerfiles/pdns/Dockerfile
  18. 18 0
      data/Dockerfiles/php-fpm/Dockerfile
  19. 7 0
      data/Dockerfiles/php-fpm/docker-entrypoint.sh
  20. 33 0
      data/Dockerfiles/postfix/Dockerfile
  21. 16 0
      data/Dockerfiles/postfix/postfix.sh
  22. 17 0
      data/Dockerfiles/postfix/supervisord.conf
  23. 0 0
      data/Dockerfiles/redis/.empty
  24. 26 0
      data/Dockerfiles/rmilter/Dockerfile
  25. 18 0
      data/Dockerfiles/rmilter/supervisord.conf
  26. 27 0
      data/Dockerfiles/rspamd/Dockerfile
  27. 51 0
      data/Dockerfiles/sogo/Dockerfile
  28. 98 0
      data/Dockerfiles/sogo/reconf-domains.sh
  29. 46 0
      data/Dockerfiles/sogo/supervisord.conf
  30. 4 0
      data/assets/passwd/generate_passwords.sh
  31. 27 0
      data/assets/ssl/ca-key.pem
  32. 18 0
      data/assets/ssl/ca.pem
  33. 19 0
      data/assets/ssl/cert.pem
  34. 8 0
      data/assets/ssl/dhparams.pem
  35. 27 0
      data/assets/ssl/key.pem
  36. 247 0
      data/conf/dovecot/dovecot.conf
  37. 24 0
      data/conf/dovecot/sieve_after
  38. 15 0
      data/conf/dovecot/sql/dovecot-dict-sql.conf
  39. 6 0
      data/conf/dovecot/sql/dovecot-mysql.conf
  40. 14 0
      data/conf/mysql/my.cnf
  41. 18 0
      data/conf/nginx/dynmaps.conf
  42. 1 0
      data/conf/nginx/listen.template
  43. 136 0
      data/conf/nginx/site.conf
  44. 1 0
      data/conf/pdns/pdns_custom.lua
  45. 41 0
      data/conf/pdns/recursor.conf
  46. 93 0
      data/conf/postfix/main.cf
  47. 45 0
      data/conf/postfix/master.cf
  48. 654 0
      data/conf/postfix/postscreen_access.cidr
  49. 6 0
      data/conf/postfix/smtp_dsn_filter
  50. 5 0
      data/conf/postfix/sql/mysql_relay_recipient_maps.cf
  51. 5 0
      data/conf/postfix/sql/mysql_tls_enforce_in_policy.cf
  52. 5 0
      data/conf/postfix/sql/mysql_tls_enforce_out_policy.cf
  53. 6 0
      data/conf/postfix/sql/mysql_virtual_alias_domain_catchall_maps.cf
  54. 5 0
      data/conf/postfix/sql/mysql_virtual_alias_domain_mailbox_maps.cf
  55. 5 0
      data/conf/postfix/sql/mysql_virtual_alias_domain_maps.cf
  56. 5 0
      data/conf/postfix/sql/mysql_virtual_alias_maps.cf
  57. 5 0
      data/conf/postfix/sql/mysql_virtual_domains_maps.cf
  58. 5 0
      data/conf/postfix/sql/mysql_virtual_mailbox_maps.cf
  59. 5 0
      data/conf/postfix/sql/mysql_virtual_relay_domain_maps.cf
  60. 5 0
      data/conf/postfix/sql/mysql_virtual_sender_acl.cf
  61. 5 0
      data/conf/postfix/sql/mysql_virtual_spamalias_maps.cf
  62. 42 0
      data/conf/rmilter/rmilter.conf
  63. 22 0
      data/conf/rspamd/dynmaps/authoritative.php
  64. 224 0
      data/conf/rspamd/dynmaps/settings.php
  65. 22 0
      data/conf/rspamd/dynmaps/tags.php
  66. 1 0
      data/conf/rspamd/dynmaps/vars.inc.php
  67. 34 0
      data/conf/rspamd/local.d/dkim.conf
  68. 19 0
      data/conf/rspamd/local.d/metrics.conf
  69. 3 0
      data/conf/rspamd/local.d/options.inc
  70. 1 0
      data/conf/rspamd/local.d/redis.conf
  71. 1 0
      data/conf/rspamd/local.d/rspamd.conf.local
  72. 59 0
      data/conf/rspamd/local.d/statistic.conf
  73. 75 0
      data/conf/rspamd/lua/rspamd.local.lua
  74. 3 0
      data/conf/rspamd/override.d/logging.inc
  75. 7 0
      data/conf/rspamd/override.d/worker-controller.inc
  76. 1 0
      data/conf/rspamd/override.d/worker-normal.inc
  77. 76 0
      data/conf/sogo/sogo.conf
  78. 385 0
      data/web/add.php
  79. 311 0
      data/web/admin.php
  80. 69 0
      data/web/autoconfig.php
  81. 142 0
      data/web/autodiscover.php
  82. 40 0
      data/web/call_sogo_ctrl.php
  83. 5 0
      data/web/css/bootstrap-select.min.css
  84. 40 0
      data/web/css/bootstrap-slider.min.css
  85. 9 0
      data/web/css/bootstrap-switch.min.css
  86. 19 0
      data/web/css/mailbox.css
  87. 46 0
      data/web/css/mailcow.css
  88. 79 0
      data/web/css/tables.css
  89. 212 0
      data/web/delete.php
  90. 664 0
      data/web/edit.php
  91. 二进制
      data/web/favicon.png
  92. 197 0
      data/web/img/cow_mailcow.svg
  93. 二进制
      data/web/img/yubi.ico
  94. 225 0
      data/web/inc/footer.inc.php
  95. 4922 0
      data/web/inc/functions.inc.php
  96. 104 0
      data/web/inc/header.inc.php
  97. 281 0
      data/web/inc/init.sql
  98. 0 0
      data/web/inc/languages.min.css
  99. 二进制
      data/web/inc/languages.png
  100. 506 0
      data/web/inc/lib/U2F.php

+ 1 - 0
.env

@@ -0,0 +1 @@
+mailcow.conf

+ 8 - 0
.gitignore

@@ -0,0 +1,8 @@
+rebuild-images.sh
+data/conf/sogo/sieve.creds
+data/conf/dovecot/dovecot-master.passwd
+mailcow.conf
+mailcow.conf_backup
+data/conf/nginx/listen*active
+data/web/inc/vars.local.inc.php
+site/

+ 5 - 0
README.md

@@ -0,0 +1,5 @@
+# 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)
+
+Please see the official documentation for instructions => [mailcow.email/dockerized](https://mailcow.email/dockerized)

+ 0 - 124
css/highlight.css

@@ -1,124 +0,0 @@
-/*
-This is the GitHub theme for highlight.js
-
-github.com style (c) Vasily Polovnyov <vast@whiteants.net>
-
-*/
-
-.hljs {
-  display: block;
-  overflow-x: auto;
-  color: #333;
-  -webkit-text-size-adjust: none;
-}
-
-.hljs-comment,
-.diff .hljs-header,
-.hljs-javadoc {
-  color: #998;
-  font-style: italic;
-}
-
-.hljs-keyword,
-.css .rule .hljs-keyword,
-.hljs-winutils,
-.nginx .hljs-title,
-.hljs-subst,
-.hljs-request,
-.hljs-status {
-  color: #333;
-  font-weight: bold;
-}
-
-.hljs-number,
-.hljs-hexcolor,
-.ruby .hljs-constant {
-  color: #008080;
-}
-
-.hljs-string,
-.hljs-tag .hljs-value,
-.hljs-phpdoc,
-.hljs-dartdoc,
-.tex .hljs-formula {
-  color: #d14;
-}
-
-.hljs-title,
-.hljs-id,
-.scss .hljs-preprocessor {
-  color: #900;
-  font-weight: bold;
-}
-
-.hljs-list .hljs-keyword,
-.hljs-subst {
-  font-weight: normal;
-}
-
-.hljs-class .hljs-title,
-.hljs-type,
-.vhdl .hljs-literal,
-.tex .hljs-command {
-  color: #458;
-  font-weight: bold;
-}
-
-.hljs-tag,
-.hljs-tag .hljs-title,
-.hljs-rule .hljs-property,
-.django .hljs-tag .hljs-keyword {
-  color: #000080;
-  font-weight: normal;
-}
-
-.hljs-attribute,
-.hljs-variable,
-.lisp .hljs-body,
-.hljs-name {
-  color: #008080;
-}
-
-.hljs-regexp {
-  color: #009926;
-}
-
-.hljs-symbol,
-.ruby .hljs-symbol .hljs-string,
-.lisp .hljs-keyword,
-.clojure .hljs-keyword,
-.scheme .hljs-keyword,
-.tex .hljs-special,
-.hljs-prompt {
-  color: #990073;
-}
-
-.hljs-built_in {
-  color: #0086b3;
-}
-
-.hljs-preprocessor,
-.hljs-pragma,
-.hljs-pi,
-.hljs-doctype,
-.hljs-shebang,
-.hljs-cdata {
-  color: #999;
-  font-weight: bold;
-}
-
-.hljs-deletion {
-  background: #fdd;
-}
-
-.hljs-addition {
-  background: #dfd;
-}
-
-.diff .hljs-change {
-  background: #0086b3;
-}
-
-.hljs-chunk {
-  color: #aaa;
-}

文件差异内容过多而无法显示
+ 0 - 8
css/theme.css


+ 0 - 178
css/theme_extra.css

@@ -1,178 +0,0 @@
-/*
- * Sphinx doesn't have support for section dividers like we do in
- * MkDocs, this styles the section titles in the nav
- *
- * https://github.com/mkdocs/mkdocs/issues/175
- */
-.wy-menu-vertical span {
-    line-height: 18px;
-    padding: 0.4045em 1.618em;
-    display: block;
-    position: relative;
-    font-size: 90%;
-    color: #838383;
-}
-
-.wy-menu-vertical .subnav a {
-    padding: 0.4045em 2.427em;
-}
-
-/*
- * Long navigations run off the bottom of the screen as the nav
- * area doesn't scroll.
- *
- * https://github.com/mkdocs/mkdocs/pull/202
- */
-.wy-nav-side {
-    height: 100%;
-    overflow-y: auto;
-}
-
-/*
- * readthedocs theme hides nav items when the window height is
- * too small to contain them.
- *
- * https://github.com/mkdocs/mkdocs/issues/#348
- */
-.wy-menu-vertical ul {
-  margin-bottom: 2em;
-}
-
-/*
- * Wrap inline code samples otherwise they shoot of the side and
- * can't be read at all.
- *
- * https://github.com/mkdocs/mkdocs/issues/313
- * https://github.com/mkdocs/mkdocs/issues/233
- * https://github.com/mkdocs/mkdocs/issues/834
- */
-code {
-    white-space: pre-wrap;
-    word-wrap: break-word;
-    padding: 2px 5px;
-}
-
-/**
- * Make code blocks display as blocks and give them the appropriate
- * font size and padding.
- *
- * https://github.com/mkdocs/mkdocs/issues/855
- * https://github.com/mkdocs/mkdocs/issues/834
- * https://github.com/mkdocs/mkdocs/issues/233
- */
-pre code {
-  white-space: pre;
-  word-wrap: normal;
-  display: block;
-  padding: 12px;
-  font-size: 12px;
-}
-
-/*
- * Fix link colors when the link text is inline code.
- *
- * https://github.com/mkdocs/mkdocs/issues/718
- */
-a code {
-    color: #2980B9;
-}
-a:hover code {
-    color: #3091d1;
-}
-a:visited code {
-    color: #9B59B6;
-}
-
-/*
- * The CSS classes from highlight.js seem to clash with the
- * ReadTheDocs theme causing some code to be incorrectly made
- * bold and italic.
- *
- * https://github.com/mkdocs/mkdocs/issues/411
- */
-pre .cs, pre .c {
-    font-weight: inherit;
-    font-style: inherit;
-}
-
-/*
- * Fix some issues with the theme and non-highlighted code
- * samples. Without and highlighting styles attached the
- * formatting is broken.
- *
- * https://github.com/mkdocs/mkdocs/issues/319
- */
-.no-highlight {
-  display: block;
-  padding: 0.5em;
-  color: #333;
-}
-
-
-/*
- * Additions specific to the search functionality provided by MkDocs
- */
-
-.search-results article {
-    margin-top: 23px;
-    border-top: 1px solid #E1E4E5;
-    padding-top: 24px;
-}
-
-.search-results article:first-child {
-    border-top: none;
-}
-
-form .search-query {
-    width: 100%;
-    border-radius: 50px;
-    padding: 6px 12px;  /* csslint allow: box-model */
-    border-color: #D1D4D5;
-}
-
-.wy-menu-vertical li ul {
-    display: inherit;
-}
-
-.wy-menu-vertical li ul.subnav ul.subnav{
-    padding-left: 1em;
-}
-
-.wy-menu-vertical .subnav li.current > a {
-    padding-left: 2.42em;
-}
-.wy-menu-vertical .subnav li.current > ul li a {
-    padding-left: 3.23em;
-}
-
-/*
- * Improve inline code blocks within admonitions.
- *
- * https://github.com/mkdocs/mkdocs/issues/656
- */
- .admonition code {
-  color: #404040;
-  border: 1px solid #c7c9cb;
-  border: 1px solid rgba(0, 0, 0, 0.2);
-  background: #f8fbfd;
-  background: rgba(255, 255, 255, 0.7);
-}
-
-/*
- * Account for wide tables which go off the side.
- * Override borders to avoid wierdness on narrow tables.
- * 
- * https://github.com/mkdocs/mkdocs/issues/834
- * https://github.com/mkdocs/mkdocs/pull/1034
- */
-.rst-content .section .docutils {
-    width: 100%;
-    overflow: auto;
-    display: block;
-    border: none;
-}
-
-td, th {
-   border: 1px solid #e1e4e5 !important; /* csslint allow: important */
-   border-collapse: collapse;
-}

+ 87 - 0
data/Dockerfiles/dovecot/Dockerfile

@@ -0,0 +1,87 @@
+From ubuntu:xenial
+MAINTAINER Andre Peters <andre.peters@servercow.de>
+
+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 apt-get update
+RUN apt-get -y install dovecot-common \
+	dovecot-core \
+	dovecot-imapd \
+	dovecot-lmtpd \
+	dovecot-managesieved \
+	dovecot-sieve \
+	dovecot-mysql \
+	dovecot-pop3d \
+	dovecot-dev \
+	syslog-ng \
+	syslog-ng-core \
+	ca-certificates \
+	supervisor \
+	wget \
+	curl \
+	build-essential \
+	autotools-dev \
+	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 \
+	&& ./configure --prefix=/usr \
+	&& 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
+
+EXPOSE 24 10001
+
+ENTRYPOINT ["/docker-entrypoint.sh"]
+CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
+
+RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

+ 28 - 0
data/Dockerfiles/dovecot/docker-entrypoint.sh

@@ -0,0 +1,28 @@
+#!/bin/bash
+set -e
+
+# 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
+
+# Set Dovecot 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/*
+
+[[ ! -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
+
+# Do not do this every start-up, it may take a very long time. So we use a stat check here.
+if [[ $(stat -c %U /var/vmail/) != "vmail" ]] ; then chown -R vmail:vmail /var/vmail ; fi
+
+# Create random master for SOGo sieve features
+RAND_USER=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 16 | head -n 1)
+RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 24 | head -n 1)
+echo ${RAND_USER}:$(doveadm pw -s SHA1 -p ${RAND_PASS}) > /etc/dovecot/dovecot-master.passwd
+echo ${RAND_USER}:${RAND_PASS} > /etc/sogo/sieve.creds
+
+exec "$@"

+ 9488 - 0
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:
+<imapsync@linux-france.org>
+
+To subscribe, send any message (even empty) to:
+<imapsync-subscribe@listes.linux-france.org>
+then just reply to the confirmation message.
+
+To unsubscribe, send a message to:
+<imapsync-unsubscribe@listes.linux-france.org>
+
+To contact the person in charge for the list:
+<imapsync-request@listes.linux-france.org>
+
+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 <gilles.lamiral@laposte.net>
+
+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( "<pre>\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{}, '/>p<a|*th', 'fol*d1', 'fold2'), 'cache_folder: />p<a|*th, fol*d1, fold2 -> /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:', '/>p<a|*th', 'fol*d1', 'fold2'), 'cache_folder: />p<a|*th, fol*d1, fold2 -> /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:<tartanpion@machin.truc>'
+	eq regexmess('From <tartanpion@machin.truc>'),
+	'From mbox 2 add colo');
+
+	ok(          "\n" . 'From <tartanpion@machin.truc>'
+	eq regexmess("\n" . 'From <tartanpion@machin.truc>'),
+	'From mbox 3 add colo') ;
+
+	ok(          "From: zzz\n" . 'From <tartanpion@machin.truc>'
+	eq regexmess("From  zzz\n" . 'From <tartanpion@machin.truc>'),
+	'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 <tartanpion@machin.truc>'),
+	'From mbox 2 remove');
+
+	ok(          "\n" . 'From <tartanpion@machin.truc>'
+	eq regexmess("\n" . 'From <tartanpion@machin.truc>'),
+	'From mbox 3 remove');
+
+	#myprint( "[", regexmess("From  zzz\n" . 'From <tartanpion@machin.truc>'), "]" ) ;
+	ok(          q{}            . 'From <tartanpion@machin.truc>'
+	eq regexmess("From  zzz\n" . 'From <tartanpion@machin.truc>'),
+	'From mbox 4 remove');
+
+
+	ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+
+Hello,
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+From  zzz
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+
+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:<tartanpion@machin.truc>
+
+Hello,
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+From:<tartanpion@machin.truc>
+
+Hello,
+Bye.
+EOM
+	),
+	'regexmess: 1 Delete header Disposition-Notification-To:');
+
+	ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+
+Hello,
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+
+Hello,
+Bye.
+EOM
+),
+	'regexmess: 2 Delete header Disposition-Notification-To:');
+
+	ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+
+Hello,
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+
+Hello,
+Bye.
+EOM
+),
+	'regexmess: 3 Delete header Disposition-Notification-To:');
+
+	ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+Bye.
+EOM
+),
+	'regexmess: 4 Delete header Disposition-Notification-To:');
+
+
+	ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+Bye.
+EOM
+),
+	'regexmess: 5 Delete header Disposition-Notification-To:');
+
+
+ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+
+Hello,
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+
+Hello,
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+Bye.
+EOM
+),
+	'regexmess: 6 Delete header Disposition-Notification-To:');
+
+ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+
+Hello,
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+
+Hello,
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+
+Bye.
+EOM
+),
+	'regexmess: 7 Delete header Disposition-Notification-To:');
+
+
+ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+
+Hello,
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+
+Hello,
+Bye.
+EOM
+),
+	'regexmess: 8 Delete header Disposition-Notification-To:');
+
+
+ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+
+Hello,
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+
+Hello,
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+Bye.
+EOM
+),
+	'regexmess: 9 Delete header Disposition-Notification-To:');
+
+
+
+ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+
+Hello,
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+
+
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+
+Hello,
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+
+
+Bye.
+EOM
+),
+	'regexmess: 10 Delete header Disposition-Notification-To:');
+
+ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+
+Hello,
+
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+
+Hello,
+
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+
+Bye.
+EOM
+),
+	'regexmess: 11 Delete header Disposition-Notification-To:');
+
+ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+
+Hello,
+
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+
+Hello,
+
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+
+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:<tartanpion@machin.truc>
+
+Hello,
+
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+
+Hello,
+
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+
+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 <gilles.lamiral@laposte.net>
+From:<tartanpion@machin.truc>
+
+Hello,
+
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+From:<tartanpion@machin.truc>
+
+Hello,
+
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+
+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 <gilles.lamiral@laposte.net>
+From:<tartanpion@machin.truc>
+
+Hello,
+
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+From:<tartanpion@machin.truc>
+
+Hello,
+
+Bye.
+EOM
+),
+	'regexmess: 15 Delete header Disposition-Notification-To:');
+
+
+ok(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+X-Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+
+Hello,
+
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+
+Hello,
+
+Bye.
+EOM
+),
+	'regexmess: 16 Delete header Disposition-Notification-To:');
+
+ok(
+<<'EOM'
+X-Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+
+Hello,
+
+Bye.
+EOM
+	eq regexmess(
+<<'EOM'
+Disposition-Notification-To: Gilles LAMIRAL <gilles.lamiral@laposte.net>
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+
+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:<tartanpion@machin.truc>
+
+Hello!
+Bye.
+EOM
+),
+    'skipmess: 1 match Content-Type: Message/Partial' ) ;
+
+	ok( 0 == skipmess(
+<<'EOM'
+Date: Sat, 10 Jul 2010 05:34:45 -0700
+From:<tartanpion@machin.truc>
+
+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:<tartanpion@machin.truc>
+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:<tartanpion@machin.truc>
+
+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:<tartanpion@machin.truc>
+
+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:<tartanpion@machin.truc>
+
+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:<tartanpion@machin.truc>
+
+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: <gilles@lamiral.info>
+Received: by lamiral.info (Postfix, from userid 1000)
+        id 21EB12443BF; Mon,  2 Mar 2015 15:38:35 +0100 (CET)
+Subject: test: aethaecohngiexao
+To: <tata@petite.lamiral.info>
+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: <gilles@louloutte.dyndns.org>
+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 <gilles@localhost>; 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 <gilles@localhost> (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 <gilles@localhost>; 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 <gilles@localhost> (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: <mistake@imapsync>' 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( "<h2>Current Values</h2>\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 ) ;
+}
+
+

+ 72 - 0
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);

+ 4 - 0
data/Dockerfiles/dovecot/postlogin.sh

@@ -0,0 +1,4 @@
+#!/bin/sh
+
+export MASTER_USER=$USER
+exec "$@"

+ 8 - 0
data/Dockerfiles/dovecot/rspamd-pipe

@@ -0,0 +1,8 @@
+#!/bin/bash
+if [[ ${2} == "learn_spam" ]]; then
+/usr/bin/curl --data-binary @- http://rspamd:11334/learnspam < /dev/stdin
+elif [[ ${2} == "learn_ham" ]]; then
+/usr/bin/curl --data-binary @- http://rspamd:11334/learnham < /dev/stdin
+fi
+# Always return 0 to satisfy Dovecot...
+exit 0

+ 21 - 0
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

+ 0 - 0
.nojekyll → data/Dockerfiles/memcached/.empty


+ 0 - 0
data/Dockerfiles/mysql/.empty


+ 0 - 0
data/Dockerfiles/nginx/.empty


+ 26 - 0
data/Dockerfiles/pdns/Dockerfile

@@ -0,0 +1,26 @@
+FROM ubuntu:xenial
+MAINTAINER Andre Peters <andre.peters@debinux.de>
+
+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/*

+ 18 - 0
data/Dockerfiles/php-fpm/Dockerfile

@@ -0,0 +1,18 @@
+FROM php:7.1-fpm
+MAINTAINER Andre Peters <andre.peters@servercow.de>
+
+ENV DEBIAN_FRONTEND noninteractive
+
+RUN apt-get update \
+        && apt-get install -y zlib1g-dev libicu-dev g++ libidn11-dev libxml2-dev
+
+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 /
+
+EXPOSE 9000
+
+ENTRYPOINT ["/docker-entrypoint.sh"]
+CMD ["php-fpm"]

+ 7 - 0
data/Dockerfiles/php-fpm/docker-entrypoint.sh

@@ -0,0 +1,7 @@
+#!/bin/bash
+set -e
+
+if [[ ! -d "/data/dkim/txt" || ! -d "/data/dkim/keys" ]] ; then	mkdir -p /data/dkim/{txt,keys} ; chown -R www-data:www-data /data/dkim; fi
+if [[ $(stat -c %U /data/dkim/) != "www-data" ]] ; then chown -R www-data:www-data /data/dkim ; fi
+
+exec "$@"

+ 33 - 0
data/Dockerfiles/postfix/Dockerfile

@@ -0,0 +1,33 @@
+From ubuntu:xenial
+MAINTAINER Andre Peters <andre.peters@servercow.de>
+
+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 apt-get update
+RUN apt-get install -y --no-install-recommends supervisor \
+	postfix \
+	sasl2-bin \
+	libsasl2-modules \
+	postfix \
+	postfix-mysql \
+	postfix-pcre \
+	syslog-ng \
+	syslog-ng-core \
+	ca-certificates
+
+RUN sed -i -E 's/^(\s*)system\(\);/\1unix-stream("\/dev\/log");/' /etc/syslog-ng/syslog-ng.conf
+
+COPY supervisord.conf /etc/supervisor/supervisord.conf
+COPY postfix.sh /opt/postfix.sh
+
+EXPOSE 588
+
+CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
+
+RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

+ 16 - 0
data/Dockerfiles/postfix/postfix.sh

@@ -0,0 +1,16 @@
+#!/bin/bash
+
+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/*
+
+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 infinity
+fi

+ 17 - 0
data/Dockerfiles/postfix/supervisord.conf

@@ -0,0 +1,17 @@
+[supervisord]
+nodaemon=true
+
+[program:syslog-ng]
+command=/usr/sbin/syslog-ng --foreground  --no-caps
+redirect_stderr=true
+autostart=true
+stdout_syslog=true
+
+[program:postfix]
+command=/opt/postfix.sh
+autorestart=true
+
+[program:postfix-maillog]
+command=/usr/bin/tail -f /var/log/mail.log
+stdout_logfile=/dev/fd/1
+stdout_logfile_maxbytes=0

+ 0 - 0
data/Dockerfiles/redis/.empty


+ 26 - 0
data/Dockerfiles/rmilter/Dockerfile

@@ -0,0 +1,26 @@
+FROM ubuntu:xenial
+MAINTAINER Andre Peters <andre.peters@servercow.de>
+
+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 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 \
+	&& apt-get update \
+	&& apt-get --no-install-recommends -y --force-yes install rmilter cron syslog-ng syslog-ng-core supervisor
+
+COPY supervisord.conf /etc/supervisor/supervisord.conf
+
+EXPOSE 9000
+
+RUN sed -i -E 's/^(\s*)system\(\);/\1unix-stream("\/dev\/log");/' /etc/syslog-ng/syslog-ng.conf
+RUN touch /var/log/mail.log && chmod 640 /var/log/mail.log && chown root:adm /var/log/mail.log
+
+CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
+
+RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

+ 18 - 0
data/Dockerfiles/rmilter/supervisord.conf

@@ -0,0 +1,18 @@
+[supervisord]
+nodaemon=true
+
+[program:syslog-ng]
+command=/usr/sbin/syslog-ng --foreground  --no-caps
+redirect_stderr=true
+autostart=true
+stdout_syslog=true
+
+[program:rmilter]
+command=/usr/sbin/rmilter -n -c /etc/rmilter.conf.d/rmilter.conf
+user=_rmilter
+autorestart=true
+
+[program:rmilter-syslog]
+command=/usr/bin/tail -f /var/log/mail.log
+stdout_logfile=/dev/fd/1
+stdout_logfile_maxbytes=0

+ 27 - 0
data/Dockerfiles/rspamd/Dockerfile

@@ -0,0 +1,27 @@
+FROM ubuntu:xenial
+MAINTAINER Andre Peters <andre.peters@debinux.de>
+
+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 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 \
+    && apt-get update \
+    && apt-get -y install rspamd ca-certificates
+
+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"]
+
+RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
+
+USER _rspamd
+
+EXPOSE 11333 11334

+ 51 - 0
data/Dockerfiles/sogo/Dockerfile

@@ -0,0 +1,51 @@
+FROM ubuntu:xenial
+MAINTAINER Andre Peters <andre.peters@servercow.de>
+
+ENV DEBIAN_FRONTEND noninteractive
+ENV LC_ALL C
+ENV GOSU_VERSION 1.9
+
+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 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 \
+    && 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" \
+    && export GNUPGHOME="$(mktemp -d)" \
+    && gpg --keyserver ha.pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4 \
+    && gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu \
+    && rm -r "$GNUPGHOME" /usr/local/bin/gosu.asc \
+    && chmod +x /usr/local/bin/gosu \
+    && gosu nobody true
+
+RUN apt-key adv --keyserver keys.gnupg.net --recv-key 0x810273C4 \
+	&& echo "deb http://packages.inverse.ca/SOGo/nightly/3/ubuntu/ xenial xenial" > /etc/apt/sources.list.d/sogo.list \
+	&& apt-get update \
+	&& apt-get -y --force-yes install sogo sogo-activesync 
+
+RUN sed -i -E 's/^(\s*)system\(\);/\1unix-stream("\/dev\/log");/' /etc/syslog-ng/syslog-ng.conf
+RUN echo '* * * * *   sogo   /usr/sbin/sogo-ealarms-notify' > /etc/cron.d/sogo
+RUN echo '* * * * *   sogo   /usr/sbin/sogo-tool expire-sessions 60' >> /etc/cron.d/sogo
+RUN echo '0 0 * * *   sogo   /usr/sbin/sogo-tool update-autoreply -p /etc/sogo/sieve.creds' >> /etc/cron.d/sogo
+
+COPY ./reconf-domains.sh /
+COPY supervisord.conf /etc/supervisor/supervisord.conf
+
+#EXPOSE 20000
+#EXPOSE 9191
+#EXPOSE 9192
+
+CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
+
+RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

+ 98 - 0
data/Dockerfiles/sogo/reconf-domains.sh

@@ -0,0 +1,98 @@
+#!/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, 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_domain_alias_address gda ON gda.username = mailbox.username
+WHERE mailbox.active = '1';
+EOF
+
+
+mkdir -p /var/lib/sogo/GNUstep/Defaults/
+
+# Generate plist header with timezone data
+cat <<EOF > /var/lib/sogo/GNUstep/Defaults/sogod.plist
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//GNUstep//DTD plist 0.9//EN" "http://www.gnustep.org/plist-0_9.xml">
+<plist version="0.9">
+<dict>
+    <key>OCSAclURL</key>
+    <string>mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_acl</string>
+    <key>OCSCacheFolderURL</key>
+    <string>mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_cache_folder</string>
+    <key>OCSEMailAlarmsFolderURL</key>
+    <string>mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_alarms_folder</string>
+    <key>DomainFieldName</key>
+    <string>domain</string>
+    <key>OCSFolderInfoURL</key>
+    <string>mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_folder_info</string>
+    <key>OCSSessionsFolderURL</key>
+    <string>mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_sessions_folder</string>
+    <key>OCSStoreURL</key>
+    <string>mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_store</string>
+    <key>SOGoProfileURL</key>
+    <string>mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_user_profile</string>
+    <key>SOGoTimeZone</key>
+    <string>${TZ}</string>
+    <key>domains</key>
+    <dict>
+EOF
+
+# Generate multi-domain setup
+while read line
+	do
+	echo "        <key>${line}</key>
+        <dict>
+            <key>SOGoMailDomain</key>
+            <string>${line}</string>
+            <key>SOGoUserSources</key>
+            <array>
+                <dict>
+                    <key>MailFieldNames</key>
+                    <array>
+                        <string>aliases</string>
+                        <string>ad_aliases</string>
+                    </array>
+                    <key>KindFieldName</key>
+                    <string>kind</string>
+                    <key>MultipleBookingsFieldName</key>
+                    <string>multiple_bookings</string>
+                    <key>canAuthenticate</key>
+                    <string>YES</string>
+                    <key>displayName</key>
+                    <string>GAL</string>
+                    <key>id</key>
+                    <string>${line}</string>
+                    <key>isAddressBook</key>
+                    <string>YES</string>
+                    <key>type</key>
+                    <string>sql</string>
+                    <key>userPasswordAlgorithm</key>
+                    <string>ssha256</string>
+                    <key>viewURL</key>
+                    <string>mysql://${DBUSER}:${DBPASS}@mysql:3306/${DBNAME}/sogo_view</string>
+                </dict>
+            </array>
+        </dict>" >> /var/lib/sogo/GNUstep/Defaults/sogod.plist
+done < <(mysql --host mysql -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain;" -B -N)
+
+# Generate footer
+echo '    </dict>
+</dict>
+</plist>' >> /var/lib/sogo/GNUstep/Defaults/sogod.plist
+
+# Fix permissions
+chown sogo:sogo -R /var/lib/sogo/
+chmod 600 /var/lib/sogo/GNUstep/Defaults/sogod.plist
+
+sleep 99999
+
+done

+ 46 - 0
data/Dockerfiles/sogo/supervisord.conf

@@ -0,0 +1,46 @@
+[supervisord]
+nodaemon=true
+
+[program:syslog-ng]
+command=/usr/sbin/syslog-ng --foreground  --no-caps
+redirect_stderr=true
+autostart=true
+stdout_syslog=true
+
+[group:sogo-group]
+programs=reconf-domains,sogo
+
+[program:sogo]
+command=/usr/sbin/sogod
+user=sogo
+autorestart=true
+priority=20
+
+[program:reconf-domains]
+command=/reconf-domains.sh
+autorestart=true
+priority=10
+
+[program:sogo-syslog]
+command=/usr/bin/tail -f /var/log/syslog -f /var/log/sogo/sogo.log
+stdout_logfile=/dev/fd/1
+stdout_logfile_maxbytes=0
+
+[program:cron]
+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
+
+[rpcinterface:supervisor]
+supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
+
+[supervisorctl]
+serverurl=http://localhost:9191

+ 4 - 0
data/assets/passwd/generate_passwords.sh

@@ -0,0 +1,4 @@
+#!/bin/bash
+
+echo DBPASS=$(openssl rand -base64 32 | tr -dc _A-Z-a-z-0-9)
+echo DBROOT=$(openssl rand -base64 32 | tr -dc _A-Z-a-z-0-9)

+ 27 - 0
data/assets/ssl/ca-key.pem

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEogIBAAKCAQEAr+geb+b9p5PgNMDUwtK3NxtmUJrHvnCSxkkLEBPFp/vVvEQV
+eJQN3WNQVXjkqaAXQzWSkTz/URiKAKMSL/nx1+Es6HRXdR79GMc6kY1GBFMfhSwt
+BzblNZ++WpD4JB3bL5prMrf2HiTqe617dlENoqK0kBEDGZq3En07aV+MIRuT8HSW
+B4j1/St2DAKJYGCGVqPDgpFt7+aR8aXGkvoCMKpgiB32tNkmdDfzOdWmAdFS1KaV
+RK+ap9WmwNdMH0BzCud9vhuJjdUZlhfmWuuT5XaGU+fftIlI191XYOEZHe44TLy1
+xWwXUhc+BmhhSj3k75lNvtAxydVrZIyoVxpLHwIDAQABAoIBAARMxkGyAc1Q3hAs
+Dodco0Hjl5Ks1ekf01apfm28Lf63NzhM6cFyzQv2W4ZbWCuVUDxCWPzX4t3Wnbj6
+Q32MvI6sYG6mOWURhtpONG5OZ8G/Tmvw8oDUpLG03/BSzt4DJNJ7EdfBi3CdMmYn
+jXcM8Cpjk8pZwBumHoeDLCqdPU2p1onq+iwMdQDKfq1ESBwEuXIO82x9y/O6zxNZ
+HlWFDS5hLEt0UVWIV5aGK0/M/kzOCKwy7MMbJhHPYKy7Yvno2t+fJ6jiwswyEsCz
+M2Z3DUWzgrT4DdWSV2Bns0X/xID5kH7EXm7GqsudbHHJNnr7LWuowFSOzAX47t+N
+JgrRIaECgYEA1iFfpgO/awiNQLV1VpgA3pqzmFddEh74sFqILkSGd2fxH6BbgXJB
+z+7Cu6jij8Odvf48suKsG9K0OwAehnR/ZgkrQmnKftf/pEZ+iLdsS/T8IZAXjQ4g
+cLlYOOMuqIi8Ev1nT0/++IaSKro0CSDuuNtkqRCQ/ZMJtWzFxw3yZRcCgYEA0k1m
+vaxvkSXqrYvFjDkPM5gUXcE4wx9t6Y4iwmRISLe6bJcfVfaT9oCz4o6p4dxZsoXw
+SFSazFedqwQlzXMrrBd2rHYgg13N9LU9TggZXtSMOdye+dxk24P7GmJdWH5+S5vm
+TqATs105AD95p7X8+H1TpW4liwW+eNjFqqO8HzkCgYBIuVj01z0BqwveOEK7wDA7
+aisoyDMR3nbz3it2G0vX7fNUnG+3jQiRCDQW7ArWbMd8KYaP8rAlWvBfQXEclSBX
+lTGeArQFVHK8Zjy/Thx3x6KB+6AkBfI6lphB6daE4ruNb4bQxwh/e6TU4hyeJRMu
+sUSErt2vYWrgtSqOqkvyzQKBgBKExFZRd+WVLCwqEbQ+VgtaCfkTibcM7nXRkVgC
+0qasnxru19CPDQp43N5HZ8g+yhtBVh5YbOUvle+4Rsfnq2HVExsur8BBo2A4EXTs
+m6dRGiQCPHGOKcd2wMbbAJNJWD+6M7aavAFgZSOTc1gEW4laJ+J6Z43tbI9hr05O
+asNpAoGAFje1QFMYw8s8ZTYVrfwafgDB/BNnQRVnQwTZRXcabAT5O0Wc8hTDPOun
+aBNJ3oKSwBvt7Snsn1G/oeH0aZ6cI8kkYE+dyQ5YqSZZUbJF6g771iNv4lMvxRBN
+BT/ihKzXb/t47z6W8dL5N1BSZ0KrwIrEkqCeHBlAW+G/FArrbZU=
+-----END RSA PRIVATE KEY-----

+ 18 - 0
data/assets/ssl/ca.pem

@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIICzjCCAbigAwIBAgIRALtgn1eSIUIhoGv9nq94NYEwCwYJKoZIhvcNAQELMBIx
+EDAOBgNVBAoTB21haWxjb3cwHhcNMTYxMjEzMTAxMTAwWhcNMTkxMTI4MTAxMTAw
+WjASMRAwDgYDVQQKEwdtYWlsY293MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEAr+geb+b9p5PgNMDUwtK3NxtmUJrHvnCSxkkLEBPFp/vVvEQVeJQN3WNQ
+VXjkqaAXQzWSkTz/URiKAKMSL/nx1+Es6HRXdR79GMc6kY1GBFMfhSwtBzblNZ++
+WpD4JB3bL5prMrf2HiTqe617dlENoqK0kBEDGZq3En07aV+MIRuT8HSWB4j1/St2
+DAKJYGCGVqPDgpFt7+aR8aXGkvoCMKpgiB32tNkmdDfzOdWmAdFS1KaVRK+ap9Wm
+wNdMH0BzCud9vhuJjdUZlhfmWuuT5XaGU+fftIlI191XYOEZHe44TLy1xWwXUhc+
+BmhhSj3k75lNvtAxydVrZIyoVxpLHwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAKww
+DwYDVR0TAQH/BAUwAwEB/zALBgkqhkiG9w0BAQsDggEBAF36qnigBDvvqRI0xksc
+NkGFMB3JLKznOZ0DrWUx121/GHFaNNNeI7ECmyk2eRYKCEZNnxWa1/LJ7GWn7lRU
+JD3OeWbhBUgA+HXoKl/jzXokuXMjYi/eFrgOofk2AqNDA5ioduS6A4vL8UDQc+74
+WSS8za4zoVR4GtxqDG+msRzTNVWXRcaaaWSrWMfZtQcEKIeQGDkcccvZ+mzlFUsH
+G1xxKuOPGjAwrxda4x+FY/dYdPbRV8ua0RQmYUMnROv507QnGZ9FdzdrvisZ67xx
+5BfxbApAyxDD/p7B4Zh1daga2LYGRwMJuwYjXlw9uNxJVQwcxg4nppWF1KZ4Fz0c
+EjY=
+-----END CERTIFICATE-----

+ 19 - 0
data/assets/ssl/cert.pem

@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDBDCCAe6gAwIBAgIQeJMoL/3dxhxhT9EwuRTL/DALBgkqhkiG9w0BAQswEjEQ
+MA4GA1UEChMHbWFpbGNvdzAeFw0xNjEyMTMxMDExMDBaFw0xOTExMjgxMDExMDBa
+MC0xEDAOBgNVBAoTB21haWxjb3cxGTAXBgNVBAMTEG1haWwuZXhhbXBsZS5vcmcw
+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDRg0xT3At9DSb3H5OMp3K1
+MpXAgYyotSK6TS61fC0QEHy2fMXiws7Agcye6Ln7CG63Fe1eN2jkdlefy9xJivS8
+y5w0M8i168v5znzC8fnylL2iOiSYfK/B/oEqfU7YH4RcegO53oDDIUZmi4Frgnu7
+39VVOU1ZyHEVqGJ2H2aAIkoZRjGzumD9Ym4LWGidtKJzBgFt/qmhUeWXipM8w281
+XkQnJU79+x2ywnJSvEZ3r/ZVJC7kbjiVw+/k15k9Cxk6Ik8wmJ0X/+xWxoZomHQI
+1LM0VKAS/iaU95dn2bplvL6jTiiyWAbrMjSKs4XbPt/fIbOicNkj6+CFy0MVfyyH
+AgMBAAGjPzA9MA4GA1UdDwEB/wQEAwIAqDAdBgNVHSUEFjAUBggrBgEFBQcDAgYI
+KwYBBQUHAwEwDAYDVR0TAQH/BAIwADALBgkqhkiG9w0BAQsDggEBAI/jBJa1P8nB
+eHUN5muQmjBVDVOYyWAAEapOe2HYsBcpjaB2H8Iw3DQzJtz6peYeYSCmHRVqFLCm
+VPrq36l9mPUotyPDPlQQAxCj9R2+WbGaJO+N/E1F8FQ94dr3jqwUyfjVPoqEjmIH
+NFkvbA0RJOeBm9oYGdhM0wjOBV9c9MTHFG82nQ/zQeTuPb7GXuKIOXYCxoLNOZMw
+UJ02Cqjv5ImrgOhcstAKX3Ip0urSvZUGvtPla4CGh+M6yDFJ08GzX6OiMIH207RW
+jAbUXXERSUv/7hysdDjGo5HZjCeMzVu9KAxoZXqnmvkk8g2swKWtWBRcoeU1VGx0
+Bx4Q4KMjuYQ=
+-----END CERTIFICATE-----

+ 8 - 0
data/assets/ssl/dhparams.pem

@@ -0,0 +1,8 @@
+-----BEGIN DH PARAMETERS-----
+MIIBCAKCAQEA9iHB0CRDhV8wfBgqnmvuJpl0fzL3qL75R4ZvQHlfMNLrxuIz2x9D
+9zcDhPcBTVzV5Ay0AAkke4wP6r6wDQqXqBP4Y8IOkYAyLh3jM40jfHQzQt+5JdQl
+ond3kiscBsFOch/vMfSLMu3lAb0YhPNTvrxhMz7LcVAWYl82swASupdiKR+MgaQr
+XsugpmDKsHW60VmIM9B7K9Y+rNHwvMWkmISd0KxA8oOy1WJvsVEissMALZDE3c4w
+2xHmO2lXxgEx3aez28736t4m/KW3g9Zr31a1M0KusmfY//fGkPk4NUrLBOS2xrgp
+Y/rG1qSBdcVyerM0Ki93qCyHKYu4ene0OwIBAg==
+-----END DH PARAMETERS-----

+ 27 - 0
data/assets/ssl/key.pem

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEA0YNMU9wLfQ0m9x+TjKdytTKVwIGMqLUiuk0utXwtEBB8tnzF
+4sLOwIHMnui5+whutxXtXjdo5HZXn8vcSYr0vMucNDPItevL+c58wvH58pS9ojok
+mHyvwf6BKn1O2B+EXHoDud6AwyFGZouBa4J7u9/VVTlNWchxFahidh9mgCJKGUYx
+s7pg/WJuC1honbSicwYBbf6poVHll4qTPMNvNV5EJyVO/fsdssJyUrxGd6/2VSQu
+5G44lcPv5NeZPQsZOiJPMJidF//sVsaGaJh0CNSzNFSgEv4mlPeXZ9m6Zby+o04o
+slgG6zI0irOF2z7f3yGzonDZI+vghctDFX8shwIDAQABAoIBAQC9kiLnIgxXGyZt
+pmmYdA6re1jatZ2zLSp+DcY8ul3/0hs195IKCyCOOSQPiR520Pt0t+duP46uYZIJ
+aakp9gxaI5Vz+oMacH/AyaBDuDTj1Mf9WMSyIOfbDVCMRJOppGLcVh62+Gfjp2EO
++h2hTJBuvypFkbK2kVIZOaHVpbXWKw1oYuEcTftk9XfxxvfSMw1HQ12/P2CAcbaa
+jPmVbisunv6kpXtewSBTcaLSYWJf1MYD5Hi8fzkD2FJSXYbfQd8RKvT2rj6FA7ux
+CDMzbYhdnd7lc63OARCIjfCRNtDT1cZ3gR1CQHD98lWxmPQIZukv+w7s/bSrFgnQ
+ROZ0ghBJAoGBAOmE/3d5FDmp0aJNxXynKcRGdpEEM4O40RIdqa2eR6Pa7aTRosao
+z0qVgdFuJrqjlB3jgedxXEX1M0abCUzzM9Q5F7JLl+KsjwRwpkIOkPiyUncLp7LK
+QbY3tvYBIdpjlF1USOMGRL4j11hqr4vQC/yPBF7jj81kCZDTbmZhp82jAoGBAOWu
+ql5QFUOlmqkuWIAFkiLEZhOu+ptqkE+zG50CCGMJIX0dJ2PHXFyNGInomAeT0nbI
+pbnK3x7KeEKiGrAqZFNCTHhApTwkrIj0L/RQbMDZ7u7j1AEUVNFEhIm62kg84FtG
+xtfxVxredE+NQc/tyV3hXegdNZxegALirlcMKIvNAoGAWFwIxk48Ru1o8z72QQqH
+lUsMRicOzwK5qV8r+xPvC6MlVL42F3F8rj4QFwzU/r4yp3SUjNyqC5aSRl8Xj9Re
+gijwPHi6Cf09SHLPliMo29GtvnnchJxfbPF7+23GP3p6gy4HPk/65u9s5nnH3uFk
+B7ad8sGsgg0eSXyXQ4okEn0CgYEAnogPuedGthlxBgMiPMMbmfm7hyyId4t3Ljuu
+/JExnsHnpobf8EPjoVIWNOIhRWGnrCtUEEhR9tvDZCKljyDDfKBPTdU496lMmX8K
+NnToi7gg7iy84T3aSVMktDgPgDrclMPmbZh8CeSvnVUfrtgu3Ci4+4Rlw5eKffNe
+aGDQ/6UCgYAbUq9mRT2WOXIo+Dchi9VzDWgtfOw5VEyqkSpb7hPiIYx5jNaENnVK
+cAi3iqbBgPJBuMlTrKmmaxdmssGOEZNJLuuXLDbCU+f5cpu5PQ4crC6UtRI5rlhp
+8Yc+oiv3HWbSw3sVRpMFB6NP4DnvgFW3B2Wdfb/lNzPCKWqBsX7gWw==
+-----END RSA PRIVATE KEY-----

+ 247 - 0
data/conf/dovecot/dovecot.conf

@@ -0,0 +1,247 @@
+auth_mechanisms = plain login
+#mail_debug = yes
+log_path = /var/log/mail.log
+disable_plaintext_auth = yes
+# Uncomment on NFS share
+#mmap_disable = yes
+#mail_fsync = always
+#mail_nfs_index = yes
+#mail_nfs_storage = yes
+login_log_format_elements = "user=<%u> method=%m rip=%r lip=%l mpid=%e %c %k"
+mail_home = /var/vmail/%d/%n
+mail_location = maildir:~/
+mail_plugins = quota acl zlib antispam
+auth_username_chars = abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-_@
+ssl_protocols = !SSLv3 !SSLv2
+ssl_prefer_server_ciphers = yes
+ssl_cipher_list = EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA
+ssl_options = no_compression
+# Automatically regenerates every week
+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
+  master = yes
+  pass = yes
+}
+passdb {
+  args = /etc/dovecot/sql/dovecot-mysql.conf
+  driver = sql
+}
+namespace inbox {
+  inbox = yes
+  location =
+  separator = /
+  mailbox "Trash" {
+    auto = subscribe
+    special_use = \Trash
+  }
+  mailbox "Deleted Messages" {
+    special_use = \Trash
+  }
+  mailbox "Deleted Items" {
+    special_use = \Trash
+  }
+  mailbox "Gelöschte Objekte" {
+    special_use = \Trash
+  }
+  mailbox "Papierkorb" {
+    special_use = \Trash
+  }
+  mailbox "Itens Excluidos" {
+    special_use = \Trash
+  }
+  mailbox "Itens Excluídos" {
+    special_use = \Trash
+  }
+  mailbox "Lixeira" {
+    special_use = \Trash
+  }
+  mailbox "Prullenbak" {
+    special_use = \Trash
+  }
+  mailbox "Verwijderde items" {
+    special_use = \Trash
+  }
+  mailbox "Archive" {
+    auto = subscribe
+    special_use = \Archive
+  }
+  mailbox "Archiv" {
+    special_use = \Archive
+  }
+  mailbox "Archives" {
+    special_use = \Archive
+  }
+  mailbox "Arquivo" {
+    special_use = \Archive
+  }
+  mailbox "Arquivos" {
+    special_use = \Archive
+  }
+  mailbox "Archief" {
+    special_use = \Archive
+  }
+  mailbox "Sent" {
+    auto = subscribe
+    special_use = \Sent
+  }
+  mailbox "Sent Messages" {
+    special_use = \Sent
+  }
+  mailbox "Sent Items" {
+    special_use = \Sent
+  }
+  mailbox "Gesendet" {
+    special_use = \Sent
+  }
+  mailbox "Gesendete Objekte" {
+    special_use = \Sent
+  }
+  mailbox "Itens Enviados" {
+    special_use = \Sent
+  }
+  mailbox "Enviados" {
+    special_use = \Sent
+  }
+  mailbox "Verzonden items" {
+    special_use = \Sent
+  }
+  mailbox "Verzonden" {
+    special_use = \Sent
+  }
+  mailbox "Drafts" {
+    auto = subscribe
+    special_use = \Drafts
+  }
+  mailbox "Entwürfe" {
+    special_use = \Drafts
+  }
+  mailbox "Rascunhos" {
+    special_use = \Drafts
+  }
+  mailbox "Concepten" {
+    special_use = \Drafts
+  }
+  mailbox "Junk" {
+    auto = subscribe
+    special_use = \Junk
+  }
+  mailbox "Junk E-mail" {
+    special_use = \Junk
+  }
+  mailbox "Spam" {
+    special_use = \Junk
+  }
+  mailbox "Lixo Eletrônico" {
+    special_use = \Junk
+  }
+  mailbox "Ongewenste e-mail" {
+    special_use = \Junk
+  }
+  prefix =
+}
+namespace {
+    type = shared
+    separator = /
+    prefix = Shared/%%u/
+    location = maildir:%%h/:INDEXPVT=~/Shared/%%u
+    subscriptions = no
+    list = yes
+}
+protocols = imap sieve lmtp pop3
+service dict {
+  unix_listener dict {
+    mode = 0660
+    user = vmail
+    group = vmail
+  }
+}
+service auth {
+  inet_listener auth-inet {
+    port = 10001
+  }
+  unix_listener auth-master {
+    mode = 0600
+    user = vmail
+  }
+  unix_listener auth-userdb {
+    mode = 0600
+    user = vmail
+  }
+  user = root
+}
+service managesieve-login {
+  inet_listener sieve {
+    port = 4190
+  }
+  service_count = 1
+  process_min_avail = 2
+  vsz_limit = 128M
+}
+service imap {
+  executable = imap imap-postlogin
+}
+service managesieve {
+  process_limit = 256
+}
+service lmtp {
+  inet_listener lmtp-inet {
+    port = 24
+  }
+  user = vmail
+}
+listen = *,[::]
+ssl_cert = </etc/ssl/mail/cert.pem
+ssl_key = </etc/ssl/mail/key.pem
+userdb {
+  args = /etc/dovecot/sql/dovecot-mysql.conf
+  driver = sql
+}
+protocol imap {
+  mail_plugins = quota imap_quota imap_acl acl zlib imap_zlib antispam
+}
+protocol lmtp {
+  mail_plugins = quota sieve acl zlib
+  auth_socket_path = /var/run/dovecot/auth-master
+}
+protocol sieve {
+  managesieve_logout_format = bytes=%i/%o
+}
+plugin {
+  acl_anyone = allow
+  acl_shared_dict = file:/var/vmail/shared-mailboxes.db
+  acl = vfile
+  quota = dict:Userquota::proxy::sqlquota
+  quota_rule2 = Trash:storage=+100%%
+  antispam_backend = mailtrain
+  antispam_spam    = Junk
+  antispam_trash   = Trash
+  antispam_mail_sendmail = /usr/local/bin/rspamd-pipe
+  antispam_mail_spam     = learn_spam
+  antispam_mail_notspam  = learn_ham
+  # Do not complain about empty parameter
+  antispam_mail_sendmail_args = --blind
+  sieve = /var/vmail/sieve/%u.sieve
+  sieve_after = /var/vmail/sieve/global.sieve
+  sieve_max_script_size = 1M
+  sieve_quota_max_scripts = 0
+  sieve_quota_max_storage = 0
+}
+dict {
+  sqlquota = mysql:/etc/dovecot/sql/dovecot-dict-sql.conf
+}
+remote 127.0.0.1 {
+  disable_plaintext_auth = no
+}
+submission_host = postfix:588
+mail_max_userip_connections = 500
+service imap-postlogin {
+  executable = script-login /usr/local/bin/postlogin.sh
+  unix_listener imap-postlogin {
+  }
+}

+ 24 - 0
data/conf/dovecot/sieve_after

@@ -0,0 +1,24 @@
+require "fileinto";
+require "mailbox";
+require "variables";
+require "subaddress";
+require "envelope";
+
+if header :contains "X-Spam-Flag" "YES" {
+  fileinto "Junk";
+}
+
+if allof (
+  envelope :detail :matches "to" "*",
+  header :contains "X-Moo-Tag" "YES",
+  mailboxexists "INBOX/${s}"
+  ) {
+    fileinto "INBOX/${s}";
+}
+elsif allof (
+  envelope :detail :matches "to" "*",
+  header :contains "X-Moo-Tag" "YES"
+  ) {
+    set :lower "s" "${1}";
+    fileinto :create "INBOX/${s}";
+}

+ 15 - 0
data/conf/dovecot/sql/dovecot-dict-sql.conf

@@ -0,0 +1,15 @@
+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
+}
+

+ 6 - 0
data/conf/dovecot/sql/dovecot-mysql.conf

@@ -0,0 +1,6 @@
+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';

+ 14 - 0
data/conf/mysql/my.cnf

@@ -0,0 +1,14 @@
+[mysqld]
+character-set-client-handshake = FALSE
+character-set-server           = utf8mb4
+collation-server               = utf8mb4_unicode_ci
+innodb_file_per_table          = TRUE
+innodb_file_format             = barracuda
+innodb_large_prefix            = TRUE
+#sql_mode=IGNORE_SPACE,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
+
+[client]
+default-character-set = utf8mb4
+
+[mysql]
+default-character-set = utf8mb4

+ 18 - 0
data/conf/nginx/dynmaps.conf

@@ -0,0 +1,18 @@
+server {
+  listen 8081;
+  index index.php index.html;
+  server_name _;
+  error_log  /var/log/nginx/error.log;
+  access_log /var/log/nginx/access.log;
+  root /dynmaps;
+
+  location ~ \.php$ {
+    try_files $uri =404;
+    fastcgi_split_path_info ^(.+\.php)(/.+)$;
+    fastcgi_pass phpfpm:9000;
+    fastcgi_index index.php;
+    include fastcgi_params;
+    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+    fastcgi_param PATH_INFO $fastcgi_path_info;
+  }
+}

+ 1 - 0
data/conf/nginx/listen.template

@@ -0,0 +1 @@
+listen ${HTTPS_PORT};

+ 136 - 0
data/conf/nginx/site.conf

@@ -0,0 +1,136 @@
+proxy_cache_path /tmp levels=1:2 keys_zone=sogo:10m inactive=24h  max_size=1g;
+server {
+  include /etc/nginx/conf.d/listen.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;
+  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
+  ssl_prefer_server_ciphers on;
+  ssl_ciphers 'EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA';
+  add_header Strict-Transport-Security "max-age=15768000; includeSubDomains";
+  ssl_ecdh_curve secp384r1;
+  index index.php index.html;
+  server_name _ autodiscover.* autoconfig.*;
+  error_log  /var/log/nginx/error.log;
+  access_log /var/log/nginx/access.log;
+  root /web;
+
+  location = /principals/ {
+    rewrite ^ https://$host/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;
+    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;
+  }
+
+  if ($host ~* autodiscover\.(.*)) {
+    rewrite ^(.*) /autodiscover.php last;
+  }
+
+  if ($host ~* autoconfig\.(.*)) {
+    rewrite ^(.*) /autoconfig.php last;
+  }
+
+  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;
+    #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/ {
+    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;
+  }
+
+}

+ 1 - 0
data/conf/pdns/pdns_custom.lua

@@ -0,0 +1 @@
+addNTA("mailcow-network", "nta for local")

+ 41 - 0
data/conf/pdns/recursor.conf

@@ -0,0 +1,41 @@
+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

+ 93 - 0
data/conf/postfix/main.cf

@@ -0,0 +1,93 @@
+biff = no
+append_dot_mydomain = no
+smtpd_tls_cert_file = /etc/ssl/mail/cert.pem
+smtpd_tls_key_file = /etc/ssl/mail/key.pem
+smtpd_use_tls=yes
+smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
+smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
+smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination
+alias_maps = hash:/etc/aliases
+alias_database = hash:/etc/aliases
+relayhost =
+mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
+mailbox_size_limit = 0
+recipient_delimiter = +
+inet_interfaces = all
+inet_protocols = all
+bounce_queue_lifetime = 1d
+broken_sasl_auth_clients = yes
+disable_vrfy_command = yes
+maximal_backoff_time = 1800s
+maximal_queue_lifetime = 1d
+message_size_limit = 26214400
+milter_default_action = accept
+milter_protocol = 6
+minimal_backoff_time = 300s
+plaintext_reject_code = 550
+postscreen_access_list = permit_mynetworks, cidr:/opt/postfix/conf/postscreen_access.cidr
+postscreen_bare_newline_enable = no
+postscreen_blacklist_action = drop
+postscreen_cache_cleanup_interval = 24h
+postscreen_cache_map = proxy:btree:$data_directory/postscreen_cache
+postscreen_dnsbl_action = enforce
+postscreen_dnsbl_sites = b.barracudacentral.org=127.0.0.2*7 dnsbl.inps.de=127.0.0.2*7 bl.mailspike.net=127.0.0.2*5 bl.mailspike.net=127.0.0.[10;11;12]*4 dnsbl.sorbs.net=127.0.0.10*8 dnsbl.sorbs.net=127.0.0.5*6 dnsbl.sorbs.net=127.0.0.7*3 dnsbl.sorbs.net=127.0.0.8*2 dnsbl.sorbs.net=127.0.0.6*2 dnsbl.sorbs.net=127.0.0.9*2 zen.spamhaus.org=127.0.0.[10;11]*8 zen.spamhaus.org=127.0.0.[4..7]*6 zen.spamhaus.org=127.0.0.3*4 zen.spamhaus.org=127.0.0.2*3 hostkarma.junkemailfilter.com=127.0.0.2*3 hostkarma.junkemailfilter.com=127.0.0.4*1 hostkarma.junkemailfilter.com=127.0.1.2*1 wl.mailspike.net=127.0.0.[18;19;20]*-2 hostkarma.junkemailfilter.com=127.0.0.1*-2
+postscreen_dnsbl_threshold = 8
+postscreen_dnsbl_ttl = 5m
+postscreen_greet_action = enforce
+postscreen_greet_banner = $smtpd_banner
+postscreen_greet_ttl = 2d
+postscreen_greet_wait = 3s
+postscreen_non_smtp_command_enable = no
+postscreen_pipelining_enable = no
+proxy_read_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_sender_acl.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_out_policy.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf, $local_recipient_maps $mydestination $virtual_alias_maps $virtual_alias_domains $virtual_mailbox_maps $virtual_mailbox_domains $relay_recipient_maps $relay_domains $canonical_maps $sender_canonical_maps $recipient_canonical_maps $relocated_maps $transport_maps $mynetworks $smtpd_sender_login_maps
+queue_run_delay = 300s
+relay_domains = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_relay_domain_maps.cf
+relay_recipient_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_relay_recipient_maps.cf
+sender_dependent_default_transport_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_out_policy.cf
+smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
+smtp_tls_cert_file = /etc/ssl/mail/cert.pem
+smtp_tls_key_file = /etc/ssl/mail/key.pem
+smtp_tls_loglevel = 1
+smtp_dns_support_level = dnssec
+smtp_tls_security_level = dane
+smtpd_data_restrictions = reject_unauth_pipelining, permit
+smtpd_delay_reject = yes
+smtpd_error_sleep_time = 10s
+smtpd_hard_error_limit = ${stress?1}${stress:5}
+smtpd_helo_required = yes
+smtpd_proxy_timeout = 600s
+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
+smtpd_sasl_type = dovecot
+smtpd_sender_login_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_sender_acl.cf
+smtpd_sender_restrictions = reject_authenticated_sender_login_mismatch, permit_mynetworks, reject_sender_login_mismatch, permit_sasl_authenticated, reject_unlisted_sender, reject_unknown_sender_domain
+smtpd_soft_error_limit = 3
+smtpd_tls_auth_only = yes
+smtpd_tls_dh1024_param_file = /etc/ssl/mail/dhparams.pem
+smtpd_tls_eecdh_grade = strong
+smtpd_tls_exclude_ciphers = ECDHE-RSA-RC4-SHA, RC4, aNULL
+smtpd_tls_loglevel = 1
+smtp_tls_mandatory_protocols = !SSLv2, !SSLv3
+smtp_tls_protocols = !SSLv2, !SSLv3
+lmtp_tls_mandatory_protocols = !SSLv2, !SSLv3
+lmtp_tls_protocols = !SSLv2, !SSLv3
+smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3
+smtpd_tls_protocols = !SSLv2, !SSLv3
+smtpd_tls_mandatory_ciphers = high
+smtpd_tls_security_level = may
+tls_ssl_options = NO_COMPRESSION
+tls_high_cipherlist = EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA
+virtual_alias_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_maps.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_spamalias_maps.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_domain_maps.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_virtual_alias_domain_catchall_maps.cf
+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_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

+ 45 - 0
data/conf/postfix/master.cf

@@ -0,0 +1,45 @@
+smtp       inet  n       -       n       -       1       postscreen
+smtpd      pass  -       -       n       -       -       smtpd
+  -o smtpd_helo_restrictions=permit_mynetworks,reject_non_fqdn_helo_hostname
+smtps    inet  n       -       n       -       -       smtpd
+  -o smtpd_tls_wrappermode=yes
+  -o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
+submission inet n       -       n       -       -       smtpd
+  -o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
+  -o smtpd_enforce_tls=yes
+  -o smtpd_tls_security_level=encrypt
+  -o tls_preempt_cipherlist=yes
+588 inet n      -       n       -       -       smtpd
+  -o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
+  -o smtpd_tls_auth_only=no
+smtp_enforced_tls      unix  -       -       n       -       -       smtp
+  -o smtp_tls_security_level=encrypt
+  -o syslog_name=enforced-tls-smtp
+  -o smtp_delivery_status_filter=pcre:/opt/postfix/conf/smtp_dsn_filter
+tlsproxy   unix  -       -       n       -       0       tlsproxy
+dnsblog    unix  -       -       n       -       0       dnsblog
+pickup     fifo  n       -       n       60      1       pickup
+cleanup    unix  n       -       n       -       0       cleanup
+qmgr       fifo  n       -       n       300     1       qmgr
+tlsmgr     unix  -       -       n       1000?   1       tlsmgr
+rewrite    unix  -       -       n       -       -       trivial-rewrite
+bounce     unix  -       -       n       -       0       bounce
+defer      unix  -       -       n       -       0       bounce
+trace      unix  -       -       n       -       0       bounce
+verify     unix  -       -       n       -       1       verify
+flush      unix  n       -       n       1000?   0       flush
+proxymap   unix  -       -       n       -       -       proxymap
+proxywrite unix  -       -       n       -       1       proxymap
+smtp       unix  -       -       n       -       -       smtp
+relay      unix  -       -       n       -       -       smtp
+showq      unix  n       -       n       -       -       showq
+error      unix  -       -       n       -       -       error
+retry      unix  -       -       n       -       -       error
+discard    unix  -       -       n       -       -       discard
+local      unix  -       n       n       -       -       local
+virtual    unix  -       n       n       -       -       virtual
+lmtp       unix  -       -       n       -       -       lmtp
+anvil      unix  -       -       n       -       1       anvil
+scache     unix  -       -       n       -       1       scache
+maildrop   unix  -       n       n       -       -       pipe flags=DRhu
+    user=vmail argv=/usr/bin/maildrop -d ${recipient}

+ 654 - 0
data/conf/postfix/postscreen_access.cidr

@@ -0,0 +1,654 @@
+# Generated by Postwhite v1.30 on Thu Dec  8 21:11:27 CET 2016
+# https://github.com/stevejenkins/postwhite/
+# 651 total rules
+2a00:1450:4000::/36     permit
+2a01:111:f400::/48      permit
+2a04:35c0::/29  permit
+2c0f:fb50:4000::/36     permit
+5.135.24.0/24   permit
+8.20.114.31     permit
+8.25.194.0/23   permit
+8.25.196.0/23   permit
+12.130.86.238   permit
+13.111.0.0/19   permit
+17.36.0.0/16    permit
+17.41.0.0/16    permit
+17.110.0.0/15   permit
+17.120.0.0/16   permit
+17.133.0.0/16   permit
+17.139.0.0/16   permit
+17.142.0.0/15   permit
+17.151.1.0/24   permit
+17.158.0.0/15   permit
+17.162.0.0/15   permit
+17.164.0.0/16   permit
+17.171.37.0/24  permit
+17.172.0.0/16   permit
+23.21.83.90     permit
+23.23.237.213   permit
+23.96.52.53     permit
+23.100.122.175  permit
+23.103.128.0/19 permit
+23.103.131.7    permit
+23.103.191.0/24 permit
+23.103.198.0/23 permit
+23.103.200.0/21 permit
+23.103.208.0/21 permit
+23.103.224.0/19 permit
+23.253.182.0/24 permit
+23.253.182.103  permit
+23.253.183.0/24 permit
+23.253.183.145  permit
+23.253.183.146  permit
+23.253.183.147  permit
+23.253.183.148  permit
+23.253.183.150  permit
+27.126.146.0/24 permit
+37.59.69.128/25 permit
+37.59.249.0/24  permit
+37.188.97.188/32        permit
+40.92.0.0/14    permit
+40.96.32.50     permit
+40.97.113.34    permit
+40.97.113.210   permit
+40.97.153.146   permit
+40.97.155.26    permit
+40.97.156.114   permit
+40.97.160.2     permit
+40.97.164.146   permit
+40.97.166.138   permit
+40.97.170.154   permit
+40.107.0.0/17   permit
+40.107.128.0/18 permit
+41.74.192.0/22  permit
+41.74.196.0/22  permit
+41.74.200.0/22  permit
+41.74.201.0/24  permit
+41.74.204.0/22  permit
+41.74.205.0/24  permit
+46.19.168.0/23  permit
+50.18.45.249    permit
+50.18.121.236   permit
+50.18.121.248   permit
+50.18.123.221   permit
+50.18.124.70    permit
+50.18.125.97    permit
+50.18.125.237   permit
+50.18.126.162   permit
+50.22.164.201   permit
+50.23.218.192/27        permit
+50.31.32.0/19   permit
+50.31.36.197    permit
+50.31.36.199    permit
+50.31.36.205    permit
+50.31.36.208    permit
+50.31.36.213    permit
+50.31.60.1      permit
+50.31.156.96/27 permit
+50.207.218.237  permit
+51.4.71.62      permit
+52.0.20.102     permit
+52.95.48.152/29 permit
+52.95.49.88/29  permit
+52.205.61.79    permit
+54.172.97.247   permit
+54.173.229.38   permit
+54.214.39.184   permit
+54.240.0.0/18   permit
+54.241.16.209   permit
+54.243.205.80   permit
+54.244.242.0/24 permit
+62.17.146.128/26        permit
+63.80.14.0/23   permit
+63.111.28.137   permit
+63.128.21.0/24  permit
+64.4.22.64/26   permit
+64.18.0.0/20    permit
+64.20.241.45    permit
+64.34.47.128/27 permit
+64.34.57.192/26 permit
+64.79.155.0/24  permit
+64.79.155.192   permit
+64.127.115.252  permit
+64.132.88.0/23  permit
+64.132.92.0/24  permit
+64.135.77.0/24  permit
+64.135.83.0/24  permit
+64.233.160.0/19 permit
+65.39.215.0/24  permit
+65.54.51.64/26  permit
+65.54.61.64/26  permit
+65.54.121.120/29        permit
+65.54.121.124/31        permit
+65.54.190.0/24  permit
+65.54.241.0/24  permit
+65.55.33.64/28  permit
+65.55.34.0/24   permit
+65.55.42.224/28 permit
+65.55.52.224/27 permit
+65.55.77.28     permit
+65.55.78.128/25 permit
+65.55.81.48/28  permit
+65.55.81.54/31  permit
+65.55.85.12     permit
+65.55.88.0/24   permit
+65.55.90.0/24   permit
+65.55.94.0/25   permit
+65.55.111.0/24  permit
+65.55.113.64/26 permit
+65.55.116.0/25  permit
+65.55.126.0/25  permit
+65.55.169.0/24  permit
+65.55.174.0/25  permit
+65.55.178.128/27        permit
+65.55.234.192/26        permit
+65.110.161.77   permit
+65.212.180.36   permit
+65.242.92.0/24  permit
+65.242.92.15    permit
+66.77.16.201/32 permit
+66.102.0.0/20   permit
+66.135.215.0/24 permit
+66.135.222.1    permit
+66.211.168.230/31       permit
+66.211.184.0/23 permit
+66.220.144.128/25       permit
+66.220.155.0/24 permit
+66.220.155.128/25       permit
+66.220.157.0/25 permit
+66.231.80.0/20  permit
+66.249.80.0/20  permit
+67.23.31.6      permit
+67.72.99.26     permit
+67.221.168.65   permit
+67.228.2.24/30  permit
+67.228.21.184/29        permit
+67.228.37.4/30  permit
+67.228.50.32/27 permit
+67.228.50.54/31 permit
+67.231.145.42   permit
+67.231.153.30   permit
+68.232.192.0/20 permit
+69.63.178.128/25        permit
+69.63.179.25    permit
+69.63.184.0/25  permit
+69.65.42.195    permit
+69.65.49.192/29 permit
+69.162.98.0/24  permit
+69.171.232.0/24 permit
+69.171.232.128/25       permit
+69.171.244.0/24 permit
+70.37.151.128/25        permit
+70.42.149.35    permit
+72.3.185.0/24   permit
+72.3.237.64/28  permit
+72.5.230.111/32 permit
+72.14.192.0/18  permit
+72.21.192.0/19  permit
+72.21.212.0/25  permit
+72.21.217.142/32        permit
+72.32.154.0/24  permit
+72.32.217.0/24  permit
+72.32.243.0/24  permit
+72.249.147.250/32       permit
+74.63.63.115    permit
+74.63.63.121    permit
+74.63.194.126   permit
+74.63.234.75    permit
+74.63.236.0/24  permit
+74.86.113.28/30 permit
+74.86.129.240/30        permit
+74.86.131.208/30        permit
+74.86.132.208/30        permit
+74.86.160.160/30        permit
+74.86.164.188/30        permit
+74.86.171.192/30        permit
+74.86.195.28/30 permit
+74.86.207.36/30 permit
+74.86.226.216/30        permit
+74.86.236.240/30        permit
+74.86.241.250/31        permit
+74.112.64.26    permit
+74.112.67.243   permit
+74.112.170.21/32        permit
+74.125.0.0/16   permit
+74.201.84.0/24  permit
+74.201.152.59/32        permit
+74.201.154.0/24 permit
+74.201.155.25/32        permit
+74.201.155.26/32        permit
+74.201.155.27/32        permit
+74.201.155.28/32        permit
+74.201.155.79/32        permit
+74.202.227.52/32        permit
+74.208.4.192/26 permit
+74.208.5.64/26  permit
+74.208.122.0/26 permit
+74.209.250.0/24 permit
+74.209.250.84   permit
+75.126.200.128/27       permit
+75.126.253.0/24 permit
+75.126.253.48   permit
+80.231.25.0/24  permit
+80.231.219.0/24 permit
+81.223.46.0/27  permit
+82.165.159.0/24 permit
+85.222.130.192/26       permit
+85.222.138.192/26       permit
+86.61.88.25     permit
+87.238.80.0/21  permit
+87.253.232.0/21 permit
+91.194.248.0/23 permit
+91.198.22.0/24  permit
+91.211.240.0/24 permit
+91.211.242.0/24 permit
+91.211.243.0/24 permit
+91.220.42.0/24  permit
+94.236.119.0/26 permit
+94.245.112.0/27 permit
+94.245.112.10/31        permit
+94.245.120.64/26        permit
+96.43.144.0/20  permit
+96.43.144.64/28 permit
+96.43.144.64/31 permit
+96.43.147.64/28 permit
+96.43.148.64/28 permit
+96.43.148.64/31 permit
+96.43.151.64/28 permit
+96.43.152.64/27 permit
+96.43.153.64/27 permit
+96.46.150.192/27        permit
+101.53.164.192/26       permit
+103.11.200.0/22 permit
+103.13.69.0/24  permit
+103.28.42.0/24  permit
+103.237.104.0/22        permit
+104.40.211.35   permit
+104.43.195.251  permit
+104.47.0.0/17   permit
+104.130.96.0/28 permit
+104.130.122.0/23        permit
+104.245.209.192/26      permit
+106.50.16.0/28  permit
+107.0.11.224/27 permit
+108.174.0.0/24  permit
+108.174.0.215   permit
+108.174.3.0/24  permit
+108.174.6.0/24  permit
+108.175.18.45   permit
+108.175.30.45   permit
+108.177.8.0/21  permit
+108.177.96.0/19 permit
+111.221.23.128/25       permit
+111.221.26.0/27 permit
+111.221.66.0/25 permit
+111.221.69.128/25       permit
+111.221.112.0/21        permit
+124.47.150.0/24 permit
+124.47.189.0/24 permit
+129.41.77.70    permit
+129.41.169.249  permit
+131.107.0.0/16  permit
+131.107.1.18    permit
+131.107.1.19    permit
+131.107.1.20    permit
+131.107.1.37    permit
+131.107.1.44    permit
+131.107.1.48    permit
+131.107.1.56    permit
+131.253.30.0/24 permit
+131.253.121.20  permit
+134.170.113.0/26        permit
+134.170.140.0/24        permit
+134.170.141.64/26       permit
+134.170.143.0/24        permit
+134.170.174.0/24        permit
+136.146.128.64/27       permit
+136.146.208.16/28       permit
+136.146.210.16/28       permit
+136.147.46.192/26       permit
+136.147.62.192/26       permit
+136.147.128.0/20        permit
+136.147.176.0/20        permit
+146.88.28.0/24  permit
+146.101.78.0/24 permit
+147.243.1.47    permit
+147.243.1.48    permit
+147.243.1.153   permit
+147.243.128.24  permit
+147.243.128.26  permit
+151.101.37.140  permit
+157.55.0.192/26 permit
+157.55.1.128/26 permit
+157.55.2.0/25   permit
+157.55.9.128/25 permit
+157.55.11.0/25  permit
+157.55.49.0/25  permit
+157.55.61.0/24  permit
+157.55.157.128/25       permit
+157.55.158.0/23 permit
+157.55.225.0/25 permit
+157.55.234.0/24 permit
+157.56.24.0/25  permit
+157.56.110.0/23 permit
+157.56.112.0/24 permit
+157.56.120.128/26       permit
+157.56.172.28   permit
+157.56.232.0/21 permit
+157.56.240.0/20 permit
+157.56.248.0/21 permit
+157.151.208.65  permit
+162.88.4.0/24   permit
+162.88.36.0/24  permit
+162.248.185.121 permit
+163.47.180.0/22 permit
+165.254.167.152/30      permit
+165.254.167.156/31      permit
+165.254.167.162/31      permit
+165.254.168.66/31       permit
+165.254.168.68/31       permit
+165.254.168.70/31       permit
+165.254.168.72/31       permit
+166.78.68.0/22  permit
+166.78.68.221   permit
+166.78.69.146   permit
+166.78.69.169   permit
+166.78.69.170   permit
+166.78.71.131   permit
+167.89.0.0/17   permit
+167.89.16.30    permit
+167.89.16.183   permit
+167.89.16.245   permit
+167.89.25.84    permit
+167.89.32.5     permit
+167.89.32.50    permit
+167.89.46.159   permit
+167.89.46.185   permit
+167.89.60.95    permit
+167.89.62.118   permit
+167.89.64.9     permit
+167.89.65.0     permit
+167.89.65.53    permit
+167.89.65.100   permit
+167.89.74.233   permit
+167.89.75.33    permit
+167.89.75.126   permit
+167.89.75.136   permit
+167.89.75.164   permit
+167.89.101.2    permit
+167.89.101.192/28       permit
+167.220.67.238  permit
+172.217.0.0/19  permit
+173.0.84.224/28 permit
+173.0.94.244/30 permit
+173.193.132.0/23        permit
+173.193.132.134/31      permit
+173.193.210.32/27       permit
+173.194.0.0/16  permit
+173.203.79.182  permit
+173.203.81.39   permit
+173.224.160.128/25      permit
+173.224.161.128/25      permit
+173.228.155.0/24        permit
+174.36.80.208/28        permit
+174.36.84.8/29  permit
+174.36.84.16/29 permit
+174.36.84.32/29 permit
+174.36.84.144/29        permit
+174.36.84.240/29        permit
+174.36.85.248/30        permit
+174.36.92.96/27 permit
+174.36.114.128/30       permit
+174.36.114.140/30       permit
+174.36.114.148/30       permit
+174.36.114.152/29       permit
+174.37.67.28/30 permit
+174.37.226.64/27        permit
+174.129.194.241 permit
+174.129.203.189 permit
+174.137.46.0/24 permit
+176.32.105.0/24 permit
+176.32.127.0/24 permit
+178.32.48.128   permit
+178.33.111.144  permit
+178.33.137.208/28       permit
+178.33.221.0/24 permit
+178.236.10.128/26       permit
+178.249.98.16/29        permit
+178.249.202.16/29       permit
+180.189.28.0/24 permit
+182.50.76.0/22  permit
+182.50.78.64/28 permit
+184.173.105.0/24        permit
+184.173.153.0/24        permit
+185.4.120.0/24  permit
+185.4.122.0/24  permit
+185.12.80.0/22  permit
+185.28.196.0/22 permit
+185.90.20.0/22  permit
+188.172.128.0/20        permit
+191.239.213.197 permit
+192.28.128.0/18 permit
+192.30.252.0/22 permit
+192.64.236.0/24 permit
+192.64.237.0/24 permit
+192.64.238.0/24 permit
+192.161.144.0/20        permit
+192.230.81.86   permit
+192.237.158.0/23        permit
+192.237.159.42  permit
+192.237.159.43  permit
+192.254.112.0/20        permit
+192.254.112.60  permit
+192.254.112.98/31       permit
+192.254.113.10  permit
+192.254.113.101 permit
+192.254.114.176 permit
+192.254.115.72  permit
+192.254.118.63  permit
+193.28.178.0/25 permit
+194.64.234.128/27       permit
+194.64.234.129  permit
+194.154.193.192/27      permit
+195.54.172.0/23 permit
+195.130.217.0/24        permit
+198.2.128.0/18  permit
+198.2.128.0/24  permit
+198.2.132.0/22  permit
+198.2.136.0/23  permit
+198.2.177.0/24  permit
+198.2.178.0/24  permit
+198.2.179.0/24  permit
+198.2.180.0/24  permit
+198.2.186.0/23  permit
+198.21.0.0/21   permit
+198.21.3.166    permit
+198.21.4.224    permit
+198.37.144.0/20 permit
+198.37.145.250  permit
+198.37.149.128  permit
+198.37.151.26   permit
+198.61.254.0/23 permit
+198.61.254.231  permit
+198.178.234.57  permit
+198.245.80.0/20 permit
+199.15.176.173  permit
+199.15.212.0/22 permit
+199.15.214.169/32       permit
+199.16.156.0/22 permit
+199.19.0.0/21   permit
+199.59.148.0/22 permit
+199.83.132.86   permit
+199.101.161.130 permit
+199.101.162.0/25        permit
+199.122.120.0/21        permit
+199.127.232.0/22        permit
+199.187.117.209 permit
+199.187.117.233 permit
+199.187.117.234/31      permit
+199.187.117.236/31      permit
+199.187.118.201 permit
+199.187.118.202/31      permit
+199.187.118.204 permit
+199.187.118.209 permit
+199.201.64.23   permit
+199.201.65.23   permit
+199.255.192.0/22        permit
+202.129.242.0/23        permit
+202.177.148.100 permit
+202.177.148.110 permit
+203.32.4.25     permit
+203.55.21.0/24  permit
+203.62.195.0/24 permit
+203.81.17.0/24  permit
+203.122.32.250  permit
+203.145.57.160/27       permit
+204.13.11.48/29 permit
+204.13.11.48/30 permit
+204.13.248.0/22 permit
+204.14.232.0/21 permit
+204.14.232.64/28        permit
+204.14.234.64/28        permit
+204.14.238.0/27 permit
+204.29.186.0/23 permit
+204.75.142.0/24 permit
+204.92.114.187  permit
+204.92.114.203  permit
+204.92.114.204/31       permit
+204.153.121.0/24        permit
+205.139.110.0/23        permit
+205.201.128.0/20        permit
+205.201.131.128/25      permit
+205.201.132.14  permit
+205.201.134.128/25      permit
+205.201.136.0/23        permit
+205.201.137.229 permit
+205.201.139.0/24        permit
+205.201.140.14  permit
+205.207.104.0/22        permit
+205.217.25.132  permit
+205.217.25.135  permit
+205.251.233.32/32       permit
+205.251.233.36/32       permit
+206.25.247.143  permit
+206.25.247.155  permit
+206.165.246.80/29       permit
+206.191.224.0/19        permit
+206.246.157.1   permit
+207.46.4.128/25 permit
+207.46.22.35    permit
+207.46.22.98    permit
+207.46.22.101   permit
+207.46.50.72    permit
+207.46.50.82    permit
+207.46.50.192/26        permit
+207.46.50.224   permit
+207.46.51.64/26 permit
+207.46.52.71    permit
+207.46.52.79    permit
+207.46.58.128/25        permit
+207.46.100.0/24 permit
+207.46.101.128/26       permit
+207.46.116.128/29       permit
+207.46.117.0/24 permit
+207.46.132.128/27       permit
+207.46.163.0/24 permit
+207.46.198.0/25 permit
+207.46.200.0/27 permit
+207.67.38.0/24  permit
+207.67.98.192/27        permit
+207.68.176.0/26 permit
+207.68.176.96/27        permit
+207.82.80.0/24  permit
+207.126.144.0/20        permit
+207.171.160.0/19        permit
+207.211.30.0/24 permit
+207.211.31.0/25 permit
+207.211.41.113  permit
+207.218.90.0/24 permit
+207.250.68.0/24 permit
+208.40.232.70   permit
+208.43.21.28/30 permit
+208.43.21.64/29 permit
+208.43.21.72/30 permit
+208.43.239.136/30       permit
+208.64.132.0/22 permit
+208.66.139.0/25 permit
+208.74.204.0/22 permit
+208.74.204.9    permit
+208.75.120.0/22 permit
+208.75.122.246  permit
+208.76.56.0/21  permit
+208.78.68.0/22  permit
+208.82.236.96/28        permit
+208.82.237.96/28        permit
+208.82.238.96/28        permit
+208.85.50.137   permit
+208.89.13.233   permit
+208.89.13.234/31        permit
+208.89.13.236/31        permit
+208.89.14.201   permit
+208.89.14.202/31        permit
+208.89.14.204   permit
+208.89.14.209   permit
+208.117.48.0/20 permit
+208.185.229.45  permit
+208.201.241.163 permit
+209.43.22.0/28  permit
+209.46.117.168  permit
+209.46.117.179  permit
+209.61.151.0/24 permit
+209.67.98.46    permit
+209.67.98.59    permit
+209.85.128.0/17 permit
+212.4.136.0/26  permit
+212.123.28.40/32        permit
+212.227.15.0/24 permit
+212.227.17.0/27 permit
+212.227.126.128/25      permit
+213.165.64.0/23 permit
+213.167.75.0/24 permit
+213.167.81.0/24 permit
+213.199.128.139 permit
+213.199.128.145 permit
+213.199.138.181 permit
+213.199.138.191 permit
+213.199.154.0/24        permit
+213.199.161.128/27      permit
+213.199.177.0/26        permit
+213.199.180.0/24        permit
+216.17.150.242  permit
+216.17.150.251  permit
+216.32.180.0/23 permit
+216.46.168.197  permit
+216.46.168.222  permit
+216.58.192.0/19 permit
+216.99.5.67     permit
+216.99.5.68     permit
+216.113.160.0/24        permit
+216.113.172.0/25        permit
+216.113.175.0/24        permit
+216.136.162.65  permit
+216.136.162.120/29      permit
+216.136.168.80/28       permit
+216.146.32.0/20 permit
+216.198.0.0/18  permit
+216.203.30.55   permit
+216.203.33.178/31       permit
+216.205.24.0/24 permit
+216.229.156.0/25        permit
+216.239.32.0/19 permit
+217.72.207.0/27 permit
+217.77.141.52   permit
+217.77.141.59   permit
+217.175.193.0/24        permit
+217.175.194.0/23        permit
+217.175.196.0/24        permit
+2001:4860:4000::/36     permit
+2404:6800:4000::/36     permit
+2607:f8b0:4000::/36     permit
+2620:109:c003:104::/64  permit
+2620:109:c006:104::/64  permit
+2620:109:c00d:104::/64  permit
+2620:119:50c0:207::/64  permit
+2800:3f0:4000::/36      permit

+ 6 - 0
data/conf/postfix/smtp_dsn_filter

@@ -0,0 +1,6 @@
+/^4(\.\d+\.\d+ TLS is required, but host \S+ refused to start TLS: .+)/
+        5$1
+/^4(\.\d+\.\d+ TLS is required, but was not offered by host .+)/
+        5$1
+/^4.7.5(.*)/
+        5.7.5$1

+ 5 - 0
data/conf/postfix/sql/mysql_relay_recipient_maps.cf

@@ -0,0 +1,5 @@
+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;

+ 5 - 0
data/conf/postfix/sql/mysql_tls_enforce_in_policy.cf

@@ -0,0 +1,5 @@
+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';

+ 5 - 0
data/conf/postfix/sql/mysql_tls_enforce_out_policy.cf

@@ -0,0 +1,5 @@
+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';

+ 6 - 0
data/conf/postfix/sql/mysql_virtual_alias_domain_catchall_maps.cf

@@ -0,0 +1,6 @@
+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'
+

+ 5 - 0
data/conf/postfix/sql/mysql_virtual_alias_domain_mailbox_maps.cf

@@ -0,0 +1,5 @@
+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'

+ 5 - 0
data/conf/postfix/sql/mysql_virtual_alias_domain_maps.cf

@@ -0,0 +1,5 @@
+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'

+ 5 - 0
data/conf/postfix/sql/mysql_virtual_alias_maps.cf

@@ -0,0 +1,5 @@
+user = mailcow
+password = mysafepasswd
+hosts = mysql
+dbname = mailcow
+query = SELECT goto FROM alias WHERE address='%s' AND active='1';

+ 5 - 0
data/conf/postfix/sql/mysql_virtual_domains_maps.cf

@@ -0,0 +1,5 @@
+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'

+ 5 - 0
data/conf/postfix/sql/mysql_virtual_mailbox_maps.cf

@@ -0,0 +1,5 @@
+user = mailcow
+password = mysafepasswd
+hosts = mysql
+dbname = mailcow
+query = SELECT maildir FROM mailbox WHERE username='%s' AND active = '1'

+ 5 - 0
data/conf/postfix/sql/mysql_virtual_relay_domain_maps.cf

@@ -0,0 +1,5 @@
+user = mailcow
+password = mysafepasswd
+hosts = mysql
+dbname = mailcow
+query = SELECT domain FROM domain WHERE domain='%s' AND backupmx = '1' AND active = '1'

+ 5 - 0
data/conf/postfix/sql/mysql_virtual_sender_acl.cf

@@ -0,0 +1,5 @@
+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' 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 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'

+ 5 - 0
data/conf/postfix/sql/mysql_virtual_spamalias_maps.cf

@@ -0,0 +1,5 @@
+user = mailcow
+password = mysafepasswd
+hosts = mysql
+dbname = mailcow
+query = SELECT goto FROM spamalias WHERE address='%s' AND validity >= UNIX_TIMESTAMP()

+ 42 - 0
data/conf/rmilter/rmilter.conf

@@ -0,0 +1,42 @@
+bind_socket = inet:9900;
+spamd {
+        servers = r:rspamd:11333;
+        connect_timeout = 1s;
+        results_timeout = 20s;
+        error_time = 10;
+        dead_time = 300;
+        maxerrors = 10;
+        reject_message = "Spam or virus message rejected due to high detection score";
+        whitelist = 127.0.0.1/32, [::1]/128;
+        spamd_soft_fail = yes;
+        rspamd_metric = "default";
+        extended_spam_headers = yes;
+        spam_header = "X-Spam-Flag";
+        spam_header_value = "YES";
+};
+redis {
+        servers_grey = redis:6379;
+        servers_limits = redis:6379;
+        servers_id = redis:6379;
+        id_prefix = "message_id.";
+        grey_prefix = "grey.";
+        white_prefix = "white.";
+        connect_timeout = 1s;
+        error_time = 10;
+        dead_time = 300;
+        maxerrors = 10;
+};
+tempdir = /tmp;
+tempfiles_mode = 00600;
+max_size = 20M;
+strict_auth = yes;
+use_dcc = no;
+limits {
+        enable = false;
+};
+greylisting {
+        enable = false;
+}
+dkim {
+        enable = false;
+};

+ 22 - 0
data/conf/rspamd/dynmaps/authoritative.php

@@ -0,0 +1,22 @@
+<?php
+ini_set('error_reporting', 0);
+header('Content-Type: text/plain');
+require_once "vars.inc.php";
+$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);
+$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;
+}
+$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;
+}
+?>

+ 224 - 0
data/conf/rspamd/dynmaps/settings.php

@@ -0,0 +1,224 @@
+<?php
+/*
+The match section performs AND operation on different matches: for example, if you have from and rcpt in the same rule,
+then the rule matches only when from AND rcpt match. For similar matches, the OR rule applies: if you have multiple rcpt matches,
+then any of these will trigger the rule. If a rule is triggered then no more rules are matched.
+*/
+ini_set('error_reporting', '0');
+
+header('Content-Type: text/plain');
+require_once "vars.inc.php";
+
+$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);
+?>
+settings {
+<?php
+$stmt = $pdo->query("SELECT DISTINCT `object` FROM `filterconf` WHERE `option` = 'highspamlevel' OR `option` = 'lowspamlevel'");
+$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+while ($row = array_shift($rows)) {
+	$username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['object']);
+?>
+	score_<?=$username_sane;?> {
+		priority = low;
+<?php
+	$stmt = $pdo->prepare("SELECT `option`, `value` FROM `filterconf` 
+		WHERE (`option` = 'highspamlevel' OR `option` = 'lowspamlevel')
+			AND `object`= :object");
+	$stmt->execute(array(':object' => $row['object']));
+	$spamscore = $stmt->fetchAll(PDO::FETCH_COLUMN|PDO::FETCH_GROUP);
+
+	$stmt = $pdo->prepare("SELECT GROUP_CONCAT(REPLACE(`value`, '*', '.*') SEPARATOR '|') AS `value` FROM `filterconf`
+		WHERE (`object`= :object OR `object`= :object_domain)
+			AND (`option` = 'blacklist_from' OR `option` = 'whitelist_from')");
+	$stmt->execute(array(':object' => $row['object'], ':object_domain' => substr(strrchr($row['object'], "@"), 1)));
+	$grouped_lists = $stmt->fetchAll(PDO::FETCH_COLUMN);
+	$value_sane = preg_replace("/\.\./", ".", (preg_replace("/\*/", ".*", $grouped_lists[0])));
+?>
+		from = "/^((?!<?=$value_sane;?>).)*$/";
+		rcpt = "<?=$row['object'];?>";
+<?php
+	$stmt = $pdo->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_1 = $stmt->fetchAll(PDO::FETCH_ASSOC);
+	while ($row_aliases_1 = array_shift($rows_aliases_1)) {
+?>
+		rcpt = "<?=$row_aliases_1['address'];?>";
+<?php
+	}
+	$stmt = $pdo->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_2 = $stmt->fetchAll(PDO::FETCH_ASSOC);
+  array_filter($rows_aliases_2);
+	while ($row_aliases_2 = array_shift($rows_aliases_2)) {
+    if (!empty($row_aliases_2['aliases'])) {
+?>
+		rcpt = "<?=$row_aliases_2['aliases'];?>";
+<?php
+    }
+	}
+?>
+		apply "default" {
+			actions {
+				reject = <?=$spamscore['highspamlevel'][0];?>;
+				greylist = <?=$spamscore['lowspamlevel'][0] - 1;?>;
+				"add header" = <?=$spamscore['lowspamlevel'][0];?>;
+			}
+		}
+	}
+<?php
+}
+
+/*
+// Start whitelist
+*/
+
+$stmt = $pdo->query("SELECT DISTINCT `object` FROM `filterconf` WHERE `option` = 'whitelist_from'");
+$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+while ($row = array_shift($rows)) {
+	$username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['object']);
+?>
+	whitelist_<?=$username_sane;?> {
+<?php
+	$stmt = $pdo->prepare("SELECT GROUP_CONCAT(REPLACE(`value`, '*', '.*') SEPARATOR '|') AS `value` FROM `filterconf`
+		WHERE `object`= :object
+			AND `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])));
+?>
+		from = "/(<?=$value_sane;?>)/";
+<?php
+	if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
+?>
+		priority = medium;
+		rcpt = "/.*@<?=$row['object'];?>/";
+<?php
+		$stmt = $pdo->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 = "/.*@<?=$row_domain_aliases['alias_domain'];?>/";
+<?php
+		}
+	}
+	else {
+?>
+		priority = high;
+		rcpt = "<?=$row['object'];?>";
+<?php
+	}
+	$stmt = $pdo->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);
+  array_filter($rows_aliases_wl_1);
+	while ($row_aliases_wl_1 = array_shift($rows_aliases_wl_1)) {
+?>
+		rcpt = "<?=$row_aliases_wl_1['address'];?>";
+<?php
+	}
+	$stmt = $pdo->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);
+  array_filter($rows_aliases_wl_2);
+	while ($row_aliases_wl_2 = array_shift($rows_aliases_wl_2)) {
+    if (!empty($row_aliases_wl_2['aliases'])) {
+?>
+		rcpt = "<?=$row_aliases_wl_2['aliases'];?>";
+<?php
+    }
+	}
+?>
+		apply "default" {
+			MAILCOW_MOO = -999.0;
+		}
+	}
+<?php
+}
+
+/*
+// Start blacklist
+*/
+
+$stmt = $pdo->query("SELECT DISTINCT `object` FROM `filterconf` WHERE `option` = 'blacklist_from'");
+$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+while ($row = array_shift($rows)) {
+	$username_sane = preg_replace("/[^a-zA-Z0-9]+/", "", $row['object']);
+?>
+	blacklist_<?=$username_sane;?> {
+<?php
+	$stmt = $pdo->prepare("SELECT GROUP_CONCAT(REPLACE(`value`, '*', '.*') SEPARATOR '|') AS `value` FROM `filterconf`
+		WHERE `object`= :object
+			AND `option` = 'blacklist_from'");
+	$stmt->execute(array(':object' => $row['object']));
+	$grouped_lists = $stmt->fetchAll(PDO::FETCH_COLUMN);
+	$value_sane = preg_replace("/\.\./", ".", (preg_replace("/\*/", ".*", $grouped_lists[0])));
+?>
+		from = "/(<?=$value_sane;?>)/";
+<?php
+	if (!filter_var(trim($row['object']), FILTER_VALIDATE_EMAIL)) {
+?>
+		priority = medium;
+		rcpt = "/.*@<?=$row['object'];?>/";
+<?php
+		$stmt = $pdo->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 = "/.*@<?=$row_domain_aliases['alias_domain'];?>/";
+<?php
+		}
+	}
+	else {
+?>
+		priority = high;
+		rcpt = "<?=$row['object'];?>";
+<?php
+	}
+	$stmt = $pdo->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_bl_1 = $stmt->fetchAll(PDO::FETCH_ASSOC);
+  array_filter($rows_aliases_bl_1);
+	while ($row_aliases_bl_1 = array_shift($rows_aliases_bl_1)) {
+?>
+		rcpt = "<?=$row_aliases_bl_1['address'];?>";
+<?php
+	}
+	$stmt = $pdo->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_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'])) {
+?>
+		rcpt = "<?=$row_aliases_bl_2['aliases'];?>";
+<?php
+    }
+	}
+?>
+		apply "default" {
+			MAILCOW_MOO = 999.0;
+		}
+	}
+<?php
+}
+?>
+}

+ 22 - 0
data/conf/rspamd/dynmaps/tags.php

@@ -0,0 +1,22 @@
+<?php
+ini_set('error_reporting', 0);
+header('Content-Type: text/plain');
+require_once "vars.inc.php";
+$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);
+$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;
+}
+$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 = $stmt->fetchAll(PDO::FETCH_ASSOC);
+while ($row = array_shift($rows)) {
+  echo strtolower(trim($row['tag_ad'])) . PHP_EOL;
+}
+?>

+ 1 - 0
data/conf/rspamd/dynmaps/vars.inc.php

@@ -0,0 +1 @@
+../../../web/inc/vars.inc.php

+ 34 - 0
data/conf/rspamd/local.d/dkim.conf

@@ -0,0 +1,34 @@
+sign_condition =<<EOD
+return function(task)
+  local smtp_from = task:get_from('smtp')
+  local mime_from = task:get_from('mime')
+  local rspamd_logger = require "rspamd_logger"
+  if smtp_from[1]['domain'] ~= nil and smtp_from[1]['domain'] ~= '' then
+    domain = smtp_from[1]['domain']
+    rspamd_logger.infox(task, "set domain found in smtp from field to %s", domain)
+    if not task:get_user() then
+      rspamd_logger.infox(task, "found domain in smtp header field, but user is not authenticated - skipped")
+      return false
+    end
+  elseif mime_from[1]['domain'] ~= nil and mime_from[1]['domain'] ~= '' then
+    domain = mime_from[1]['domain']
+    rspamd_logger.infox(task, "set domain found in mime from field to %s", domain)
+  else
+    rspamd_logger.infox(task, "cannot determine domain for dkim signing")
+    return false
+  end
+  local keyfile = io.open("/data/dkim/keys/" .. domain .. ".dkim")
+  if keyfile then
+    rspamd_logger.infox(task, "found dkim key file for domain %s", domain)
+    keyfile:close()
+    return {
+      key = "/data/dkim/keys/" .. domain .. ".dkim",
+      domain = domain,
+      selector = "dkim"
+    }
+  else
+    rspamd_logger.infox(task, "no key file for domain %s - skipped", domain)
+  end
+  return false
+end
+EOD;

+ 19 - 0
data/conf/rspamd/local.d/metrics.conf

@@ -0,0 +1,19 @@
+actions {
+	reject = 15;
+	add_header = 5;
+	greylist = 4;
+}
+symbol "MAILCOW_AUTH" {
+	description = "mailcow authenticated";
+	score = -20.0;
+}
+group "bayes" {
+	symbol "BAYES_SPAM" {
+		weight = 7.5;
+		description = "Message probably spam, probability: ";
+	}
+	symbol "BAYES_HAM" {
+		weight = -2.5;
+		description = "Message probably ham, probability: ";
+	}
+}

+ 3 - 0
data/conf/rspamd/local.d/options.inc

@@ -0,0 +1,3 @@
+dns {
+    enable_dnssec = true;
+}

+ 1 - 0
data/conf/rspamd/local.d/redis.conf

@@ -0,0 +1 @@
+servers = "redis:6379";

+ 1 - 0
data/conf/rspamd/local.d/rspamd.conf.local

@@ -0,0 +1 @@
+# rspamd.conf.local

+ 59 - 0
data/conf/rspamd/local.d/statistic.conf

@@ -0,0 +1,59 @@
+classifier "bayes" {
+    tokenizer {
+    name = "osb";
+    }
+
+    backend = "redis";
+    servers = "redis:6379";
+    min_tokens = 11;
+    min_learns = 20;
+    autolearn = true;
+
+    per_user = <<EOD
+return function(task)
+    local rcpt = task:get_recipients(1)
+
+if rcpt then
+    one_rcpt = rcpt[1]
+    if one_rcpt['domain'] then
+        return one_rcpt['domain']
+    end
+end
+
+return nil
+end
+EOD
+
+    statfile {
+        symbol = "BAYES_HAM";
+        spam = false;
+    }
+    statfile {
+        symbol = "BAYES_SPAM";
+        spam = true;
+    }
+    learn_condition =<<EOD
+return function(task, is_spam, is_unlearn)
+    local prob = task:get_mempool():get_variable('bayes_prob', 'double')
+
+    if prob then
+        local in_class = false
+        local cl
+        if is_spam then
+            cl = 'spam'
+            in_class = prob >= 0.95
+        else
+            cl = 'ham'
+            in_class = prob <= 0.05
+        end
+
+        if in_class then
+            return false,string.format('already in class %s; probability %.2f%%',
+            cl, math.abs((prob - 0.5) * 200.0))
+        end
+    end
+
+    return true
+end
+EOD
+}

+ 75 - 0
data/conf/rspamd/lua/rspamd.local.lua

@@ -0,0 +1,75 @@
+rspamd_config.MAILCOW_AUTH = {
+	callback = function(task)
+		local uname = task:get_user()
+		if uname then
+			return 1
+		end
+	end
+}
+
+rspamd_config.MAILCOW_MOO = function (task)
+	return true
+end
+
+local modify_subject_map = rspamd_config:add_map({
+  url = 'http://nginx: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',
+  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_env_tagged = task:get_recipients(1)[1]['user']
+    local user_to_tagged = task:get_recipients(2)[1]['user']
+
+    local domain = task:get_recipients(1)[1]['domain']
+
+    local user_env, tag_env = user_env_tagged:match("([^+]+)+(.*)")
+    local user_to, tag_to = user_to_tagged:match("([^+]+)+(.*)")
+
+    local authdomain = auth_domain_map:get_key(domain)
+
+    if tag_env then
+      tag = tag_env
+      user = user_env
+    elseif tag_to then
+      tag = tag_to
+      user = user_env
+    end
+
+    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 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({
+          add_headers = {['X-Moo-Tag'] = 'YES'}
+        })
+      end
+    else
+      rspamd_logger.infox("Skip delimiter handling for untagged message or authenticated user")
+    end
+    return false
+  end
+}

+ 3 - 0
data/conf/rspamd/override.d/logging.inc

@@ -0,0 +1,3 @@
+type = "console";
+systemd = false;
+.include "$CONFDIR/logging.inc"

+ 7 - 0
data/conf/rspamd/override.d/worker-controller.inc

@@ -0,0 +1,7 @@
+bind_socket = "*:11334";
+enable_password = "$2$pppq86q9uns51zd5ekfxecj7bxwaefo3$p7f9xdhamydjhtypcr639it3kqeiknx3dk9on7skjypyi8uwwcmy";
+secure_ip = "192.168.0.0/16";
+secure_ip = "172.16.0.0/12";
+secure_ip = "10.0.0.0/8";
+secure_ip = "127.0.0.1";
+secure_ip = "::1";

+ 1 - 0
data/conf/rspamd/override.d/worker-normal.inc

@@ -0,0 +1 @@
+bind_socket = "*:11333";

+ 76 - 0
data/conf/sogo/sogo.conf

@@ -0,0 +1,76 @@
+{
+    SOGoCalendarDefaultRoles = (
+        PublicViewer,
+        ConfidentialDAndTViewer,
+        PrivateDAndTViewer
+    );
+
+    WOWorkersCount = "20";
+    SOGoACLsSendEMailNotifications = YES;
+    SOGoAppointmentSendEMailNotifications = YES;
+    SOGoDraftsFolderName = "Drafts";
+    SOGoJunkFolderName= "Junk";
+    SOGoMailDomain = "sogo.local";
+    SOGoEnableEMailAlarms = NO;
+    SOGoFoldersSendEMailNotifications = YES;
+    SOGoForwardEnabled = YES;
+
+    // Multi-domain setup
+    // Domains are isolated, you can define visibility options here.
+    // Example:
+
+    // SOGoDomainsVisibility = (
+    //  (domain1.tld, domain5.tld),
+    //  (domain3.tld, domain2.tld)
+    // );
+
+    SOGoIMAPServer = "imap://dovecot:143/?tls=YES";
+    SOGoSieveServer = "sieve://dovecot:4190/?tls=YES";
+    SOGoSMTPServer = "postfix:588";
+    WOPort = "0.0.0.0:20000";
+    SOGoMemcachedHost = "memcached";
+
+    SOGoLanguage = English;
+    SOGoMailAuxiliaryUserAccountsEnabled = YES;
+    SOGoMailCustomFromEnabled = YES;
+    SOGoMailingMechanism = smtp;
+    SOGoSMTPAuthenticationType = plain;
+
+    SxVMemLimit = 512;
+
+    SOGoMaximumPingInterval = 354;
+
+    SOGoInternalSyncInterval = 30;
+    SOGoMaximumSyncInterval = 354;
+
+    SOGoMaximumSyncWindowSize = 0;
+    SOGoMaximumSyncResponseSize = 1024;
+
+    WOWatchDogRequestTimeout = 10;
+    WOListenQueueSize = 300;
+    WONoDetach = YES;
+
+    SOGoIMAPAclConformsToIMAPExt = Yes;
+    SOGoPageTitle = "SOGo Groupware";
+    SOGoFirstDayOfWeek = "1";
+
+    SOGoSieveFolderEncoding = "UTF-8";
+    SOGoPasswordChangeEnabled = NO;
+    SOGoSentFolderName = "Sent";
+    SOGoMailShowSubscribedFoldersOnly = NO;
+    NGImap4ConnectionStringSeparator = "/";
+    SOGoSieveScriptsEnabled = YES;
+    SOGoTrashFolderName = "Trash";
+    SOGoVacationEnabled = YES;
+
+    MySQL4Encoding = "utf8mb4";
+  //SOGoDebugRequests = YES;
+  //SoDebugBaseURL = YES;
+  //ImapDebugEnabled = YES;
+  //SOGoEASDebugEnabled = YES;
+  //LDAPDebugEnabled = YES;
+  //PGDebugEnabled = YES;
+  //MySQL4DebugEnabled = YES;
+  //SOGoUIxDebugEnabled = YES;
+  //WODontZipResponse = YES;
+}

+ 385 - 0
data/web/add.php

@@ -0,0 +1,385 @@
+<?php
+require_once("inc/prerequisites.inc.php");
+$AuthUsers = array("admin", "domainadmin", "user");
+if (!isset($_SESSION['mailcow_cc_role']) OR !in_array($_SESSION['mailcow_cc_role'], $AuthUsers)) {
+	header('Location: /');
+	exit();
+}
+require_once("inc/header.inc.php");
+?>
+<div class="container">
+	<div class="row">
+		<div class="col-md-12">
+			<div class="panel panel-default">
+				<div class="panel-heading">
+					<h3 class="panel-title"><?=$lang['add']['title'];?></h3>
+				</div>
+				<div class="panel-body">
+<?php
+if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "admin"  || $_SESSION['mailcow_cc_role'] == "domainadmin")) {
+	if (isset($_GET['domain']) && $_SESSION['mailcow_cc_role'] == "admin") {
+?>
+				<h4><?=$lang['add']['domain'];?></h4>
+				<form class="form-horizontal" role="form" method="post" action="<?=($FORM_ACTION == "previous") ? $_SESSION['return_to'] : null;?>">
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="domain"><?=$lang['add']['domain'];?>:</label>
+						<div class="col-sm-10">
+						<input type="text" autocorrect="off" autocapitalize="none" class="form-control" name="domain" id="domain">
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="description"><?=$lang['add']['description'];?></label>
+						<div class="col-sm-10">
+						<input type="text" class="form-control" name="description" id="description">
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="aliases"><?=$lang['add']['max_aliases'];?></label>
+						<div class="col-sm-10">
+						<input type="number" class="form-control" name="aliases" id="aliases" value="400">
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="mailboxes"><?=$lang['add']['max_mailboxes'];?></label>
+						<div class="col-sm-10">
+						<input type="number" class="form-control" name="mailboxes" id="mailboxes" value="10">
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="maxquota"><?=$lang['add']['mailbox_quota_m'];?></label>
+						<div class="col-sm-10">
+						<input type="number" class="form-control" name="maxquota" id="maxquota" value="3072">
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="quota"><?=$lang['add']['domain_quota_m'];?></label>
+						<div class="col-sm-10">
+						<input type="number" class="form-control" name="quota" id="quota" value="10240">
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2"><?=$lang['add']['backup_mx_options'];?></label>
+						<div class="col-sm-10">
+							<div class="checkbox">
+							<label><input type="checkbox" name="backupmx"> <?=$lang['add']['relay_domain'];?></label>
+							<br />
+							<label><input type="checkbox" name="relay_all_recipients"> <?=$lang['add']['relay_all'];?></label>
+							<p><?=$lang['add']['relay_all_info'];?></p>
+							</div>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<div class="checkbox">
+							<label><input type="checkbox" name="active" checked> <?=$lang['add']['active'];?></label>
+							</div>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<button type="submit" name="mailbox_add_domain" class="btn btn-success"><?=$lang['add']['save'];?></button>
+						</div>
+					</div>
+					<p><span class="glyphicon glyphicon-exclamation-sign text-danger"></span> <?=$lang['add']['restart_sogo_hint'];?></p>
+				</form>
+<?php
+	}
+	elseif (isset($_GET['alias'])) {
+?>
+				<h4><?=$lang['add']['alias'];?></h4>
+				<p><?=$lang['add']['alias_spf_fail'];?></p>
+				<form class="form-horizontal" role="form" method="post" action="<?=($FORM_ACTION == "previous") ? $_SESSION['return_to'] : null;?>">
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="address"><?=$lang['add']['alias_address'];?></label>
+						<div class="col-sm-10">
+							<textarea autocorrect="off" autocapitalize="none" class="form-control" rows="5" name="address" id="address"></textarea>
+							<p><?=$lang['add']['alias_address_info'];?></p>
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="goto"><?=$lang['add']['target_address'];?></label>
+						<div class="col-sm-10">
+							<textarea autocorrect="off" autocapitalize="none" class="form-control" rows="5" id="goto" name="goto"></textarea>
+							<p><?=$lang['add']['target_address_info'];?></p>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<div class="checkbox">
+							<label><input type="checkbox" name="active" checked> <?=$lang['add']['active'];?></label>
+							</div>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<button type="submit" name="mailbox_add_alias" class="btn btn-success "><?=$lang['add']['save'];?></button>
+						</div>
+					</div>
+				</form>
+	<?php
+	}
+	elseif (isset($_GET['aliasdomain'])) {
+	?>
+				<h4><?=$lang['add']['alias_domain'];?></h4>
+				<form class="form-horizontal" role="form" method="post" action="<?=($FORM_ACTION == "previous") ? $_SESSION['return_to'] : null;?>">
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="alias_domain"><?=$lang['add']['alias_domain'];?></label>
+						<div class="col-sm-10">
+							<textarea autocorrect="off" autocapitalize="none" class="form-control" rows="5" name="alias_domain" id="alias_domain"></textarea>
+							<p><?=$lang['add']['alias_domain_info'];?></p>
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="target_domain"><?=$lang['add']['target_domain'];?></label>
+						<div class="col-sm-10">
+							<select name="target_domain" id="target_domain" title="<?=$lang['add']['select'];?>" required>
+							<?php
+              foreach (mailbox_get_domains() as $domain) {
+								echo "<option>".htmlspecialchars($domain)."</option>";
+							}
+							?>
+							</select>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<div class="checkbox">
+							<label><input type="checkbox" name="active" checked> <?=$lang['add']['active'];?></label>
+							</div>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<button type="submit" name="mailbox_add_alias_domain" class="btn btn-success "><?=$lang['add']['save'];?></button>
+						</div>
+					</div>
+				</form>
+	<?php
+	}
+	elseif (isset($_GET['mailbox'])) {
+	?>
+				<h4><?=$lang['add']['mailbox'];?></h4>
+				<form class="form-horizontal" role="form" method="post" action="<?=($FORM_ACTION == "previous") ? $_SESSION['return_to'] : null;?>">
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="local_part"><?=$lang['add']['mailbox_username'];?></label>
+						<div class="col-sm-10">
+							<input type="text" pattern="[A-Za-z0-9\.!#$%&'*+/=?^_`{|}~-]+" autocorrect="off" autocapitalize="none" class="form-control" name="local_part" id="local_part" required>
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="domain"><?=$lang['add']['domain'];?>:</label>
+						<div class="col-sm-10">
+							<select id="addSelectDomain" name="domain" id="domain" title="<?=$lang['add']['select'];?>" required>
+							<?php
+              foreach (mailbox_get_domains() as $domain) {
+								echo "<option>".htmlspecialchars($domain)."</option>";
+							}
+							?>
+							</select>
+						</div>
+					</div> 
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="name"><?=$lang['add']['full_name'];?></label>
+						<div class="col-sm-10">
+						<input type="text" class="form-control" name="name" id="name">
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="addInputQuota"><?=$lang['add']['quota_mb'];?>
+							<br /><span id="quotaBadge" class="badge">max. - MiB</span>
+						</label>
+						<div class="col-sm-10">
+						<input type="text" class="form-control" name="quota" min="1" max="" id="addInputQuota" disabled value="<?=$lang['add']['select_domain'];?>" required>
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="password"><?=$lang['add']['password'];?></label>
+						<div class="col-sm-10">
+						<input type="password" class="form-control" name="password" id="password" placeholder="">
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="password2"><?=$lang['add']['password_repeat'];?></label>
+						<div class="col-sm-10">
+						<input type="password" class="form-control" name="password2" id="password2" placeholder="">
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<div class="checkbox">
+							<label><input type="checkbox" name="active" checked> <?=$lang['add']['active'];?></label>
+							</div>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<button type="submit" name="mailbox_add_mailbox" class="btn btn-success "><?=$lang['add']['save'];?></button>
+						</div>
+					</div>
+				</form>
+	<?php
+	}
+	elseif (isset($_GET['resource'])) {
+	?>
+				<h4><?=$lang['add']['resource'];?></h4>
+				<form class="form-horizontal" role="form" method="post" action="<?=($FORM_ACTION == "previous") ? $_SESSION['return_to'] : null;?>">
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="description"><?=$lang['add']['description'];?></label>
+						<div class="col-sm-10">
+							<input type="text" class="form-control" name="description" id="description" required>
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="domain"><?=$lang['add']['domain'];?>:</label>
+						<div class="col-sm-10">
+							<select name="domain" id="domain" title="<?=$lang['add']['select'];?>" required>
+							<?php
+              foreach (mailbox_get_domains() as $domain) {
+								echo "<option>".htmlspecialchars($domain)."</option>";
+							}
+							?>
+							</select>
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="domain"><?=$lang['add']['kind'];?>:</label>
+						<div class="col-sm-10">
+							<select name="kind" id="kind" title="<?=$lang['add']['select'];?>" required>
+								<option value="location">Location</option>
+								<option value="group">Group</option>
+								<option value="thing">Thing</option>
+							</select>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<div class="checkbox">
+							<label><input type="checkbox" name="active" checked> <?=$lang['add']['active'];?></label>
+							</div>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<div class="checkbox">
+							<label><input type="checkbox" name="multiple_bookings" checked> <?=$lang['add']['multiple_bookings'];?></label>
+							</div>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<button type="submit" name="mailbox_add_resource" class="btn btn-success "><?=$lang['add']['save'];?></button>
+						</div>
+					</div>
+				</form>
+	<?php
+	}
+	else {
+	?>
+				<div class="alert alert-info" role="alert"><?=$lang['info']['no_action'];?></div>
+	<?php
+	}
+}
+elseif (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "user")) {
+  if (isset($_GET['syncjob'])) {
+?>
+				<h4><?=$lang['add']['syncjob'];?></h4>
+				<p><?=$lang['add']['syncjob_hint'];?></p>
+				<form class="form-horizontal" role="form" method="post" action="<?=($FORM_ACTION == "previous") ? $_SESSION['return_to'] : null;?>">
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="host1"><?=$lang['add']['hostname'];?></label>
+						<div class="col-sm-10">
+						<input type="text" class="form-control" name="host1" id="host1" required>
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="port1">Port</label>
+						<div class="col-sm-10">
+						<input type="number" class="form-control" name="port1" id="port1" min="1" max="65535" value="143" required>
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="user1"><?=$lang['add']['username'];?></label>
+						<div class="col-sm-10">
+						<input type="text" class="form-control" name="user1" id="user1" required>
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="password1"><?=$lang['add']['password'];?></label>
+						<div class="col-sm-10">
+						<input type="text" class="form-control" name="password1" id="password1" required>
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="enc1"><?=$lang['add']['enc_method'];?></label>
+						<div class="col-sm-10">
+							<select name="enc1" id="enc1" title="<?=$lang['add']['select'];?>" required>
+                <option selected>TLS</option>
+                <option>SSL</option>
+                <option>PLAIN</option>
+							</select>
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="mins_interval"><?=$lang['add']['mins_interval'];?></label>
+						<div class="col-sm-10">
+              <input type="number" class="form-control" name="mins_interval" min="10" max="3600" value="20" required>
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="subfolder2"><?=$lang['edit']['subfolder2'];?></label>
+						<div class="col-sm-10">
+						<input type="text" class="form-control" name="subfolder2" id="subfolder2" value="External">
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="maxage"><?=$lang['edit']['maxage'];?></label>
+						<div class="col-sm-10">
+						<input type="number" class="form-control" name="maxage" id="maxage" min="0" max="32000" value="0">
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="exclude"><?=$lang['add']['exclude'];?></label>
+						<div class="col-sm-10">
+						<input type="text" class="form-control" name="exclude" id="exclude" value="(?i)spam|(?i)junk">
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<div class="checkbox">
+							<label><input type="checkbox" name="delete2duplicates" checked> <?=$lang['add']['delete2duplicates'];?></label>
+							</div>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<div class="checkbox">
+							<label><input type="checkbox" name="active" checked> <?=$lang['add']['active'];?></label>
+							</div>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<button type="submit" name="add_syncjob" value="1" class="btn btn-success "><?=$lang['add']['save'];?></button>
+						</div>
+					</div>
+				</form>
+	<?php
+	}
+}
+
+else {
+?>
+				<div class="alert alert-danger" role="alert"><?=$lang['danger']['access_denied'];?></div>
+<?php
+}
+?>
+				</div>
+			</div>
+		</div>
+	</div>
+<a href="<?=$_SESSION['return_to'];?>">&#8592; <?=$lang['add']['previous'];?></a>
+</div> <!-- /container -->
+<script src="js/add.js"></script>
+<?php
+require_once("inc/footer.inc.php");
+?>

+ 311 - 0
data/web/admin.php

@@ -0,0 +1,311 @@
+<?php
+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();
+?>
+<div class="container">
+  <h4><span class="glyphicon glyphicon-user" aria-hidden="true"></span> <?=$lang['admin']['access'];?></h4>
+
+  <div class="panel-group" id="accordion_access">
+    <div class="panel panel-danger">
+      <div class="panel-heading"><?=$lang['admin']['admin_details'];?></div>
+      <div class="panel-body">
+        <form class="form-horizontal" autocapitalize="none" autocorrect="off" role="form" method="post">
+        <?php $admindetails = get_admin_details(); ?>
+          <div class="form-group">
+            <label class="control-label col-sm-3" for="admin_user"><?=$lang['admin']['admin'];?>:</label>
+            <div class="col-sm-9">
+              <input type="text" class="form-control" name="admin_user" id="admin_user" value="<?=htmlspecialchars($admindetails['username']);?>" required>
+              &rdsh; <kbd>a-z A-Z - _ .</kbd>
+            </div>
+          </div>
+          <div class="form-group">
+            <label class="control-label col-sm-3" for="admin_pass"><?=$lang['admin']['password'];?>:</label>
+            <div class="col-sm-9">
+            <input type="password" class="form-control" name="admin_pass" id="admin_pass" placeholder="<?=$lang['admin']['unchanged_if_empty'];?>">
+            </div>
+          </div>
+          <div class="form-group">
+            <label class="control-label col-sm-3" for="admin_pass2"><?=$lang['admin']['password_repeat'];?>:</label>
+            <div class="col-sm-9">
+            <input type="password" class="form-control" name="admin_pass2" id="admin_pass2">
+            </div>
+          </div>
+          <div class="form-group">
+            <div class="col-sm-offset-3 col-sm-9">
+              <button type="submit" name="edit_admin_account" class="btn btn-default"><?=$lang['admin']['save'];?></button>
+            </div>
+          </div>
+        </form>
+        <hr>
+        <div class="row">
+          <div class="col-sm-3 col-xs-5 text-right"><?=$lang['tfa']['tfa'];?>:</div>
+          <div class="col-sm-9 col-xs-7">
+            <p id="tfa_pretty"><?=$tfa_data['pretty'];?></p>
+              <div id="tfa_additional">
+                <?php if($tfa_data['additional']):
+                foreach ($tfa_data['additional'] as $key_info): ?>
+                <form style="display:inline;" method="post">
+                  <input type="hidden" name="unset_tfa_key" value="<?=$key_info['id'];?>" />
+                  <div style="padding:4px;margin:4px" class="label label-<?=($_SESSION['tfa_id'] == $key_info['id']) ? 'success' : 'default'; ?>">
+                  <?=$key_info['key_id'];?>
+                  <a href="#" style="font-weight:bold;color:white" onClick="$(this).closest('form').submit()">[<?=strtolower($lang['admin']['remove']);?>]</a>
+                  </div>
+                </form>
+                <?php endforeach;
+                endif;?>
+              </div>
+              <br />
+          </div>
+        </div>
+        <div class="row">
+          <div class="col-sm-3 col-xs-5 text-right"><?=$lang['tfa']['set_tfa'];?>:</div>
+          <div class="col-sm-9 col-xs-7">
+            <select data-width="auto" id="selectTFA" class="selectpicker" title="<?=$lang['tfa']['select'];?>">
+              <option value="yubi_otp"><?=$lang['tfa']['yubi_otp'];?></option>
+              <option value="u2f"><?=$lang['tfa']['u2f'];?></option>
+              <option value="none"><?=$lang['tfa']['none'];?></option>
+            </select>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="panel panel-default">
+    <div style="cursor:pointer;" class="panel-heading" data-toggle="collapse" data-parent="#accordion_access" data-target="#collapseDomAdmins">
+      <span class="accordion-toggle"><?=$lang['admin']['domain_admins'];?></span>
+    </div>
+      <div id="collapseDomAdmins" class="panel-collapse collapse">
+        <div class="panel-body">
+          <form method="post">
+            <div class="table-responsive">
+            <table class="table table-striped sortable-theme-bootstrap" data-sortable id="domainadminstable">
+              <thead>
+              <tr>
+                <th class="sort-table" style="min-width: 100px;"><?=$lang['admin']['username'];?></th>
+                <th class="sort-table" style="min-width: 166px;"><?=$lang['admin']['admin_domains'];?></th>
+                <th class="sort-table" style="min-width: 76px;"><?=$lang['admin']['active'];?></th>
+                <th class="sort-table" style="min-width: 76px;"><?=$lang['tfa']['tfa'];?></th>
+                <th style="text-align: right; min-width: 200px;" data-sortable="false"><?=$lang['admin']['action'];?></th>
+              </tr>
+              </thead>
+              <tbody>
+                <?php
+                foreach (get_domain_admins() as $domain_admin) {
+                  $da_data = get_domain_admin_details($domain_admin); 
+                  if (!empty($da_data)):
+                ?>
+                <tr id="data">
+                  <td><?=htmlspecialchars(strtolower($domain_admin));?></td>
+                  <td>
+                  <?php
+                  foreach ($da_data['selected_domains'] as $domain) {
+                    echo htmlspecialchars($domain).'<br />';
+                  }
+                  ?>
+                  </td>
+                  <td><?=$da_data['active'];?></td>
+                  <td><?=empty($da_data['tfa_active_int']) ? "✘" : "✔";?></td>
+                  <td style="text-align: right;">
+                    <div class="btn-group">
+                      <a href="edit.php?domainadmin=<?=$domain_admin;?>" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> <?=$lang['admin']['edit'];?></a>
+                      <a href="delete.php?domainadmin=<?=$domain_admin;?>" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> <?=$lang['admin']['remove'];?></a>
+                    </div>
+                  </td>
+                  </td>
+                </tr>
+
+                <?php
+                else:
+                ?>
+                  <tr id="no-data"><td colspan="4" style="text-align: center; font-style: italic;"><?=$lang['admin']['no_record'];?></td></tr>
+                <?php
+                endif;
+                }
+                ?>
+              </tbody>
+            </table>
+            </div>
+          </form>
+          <small>
+          <legend><?=$lang['admin']['add_domain_admin'];?></legend>
+          <form class="form-horizontal" role="form" method="post">
+            <div class="form-group">
+              <label class="control-label col-sm-2" for="username"><?=$lang['admin']['username'];?>:</label>
+              <div class="col-sm-10">
+                <input type="text" class="form-control" name="username" id="username" required>
+                &rdsh; <kbd>a-z A-Z - _ .</kbd>
+              </div>
+            </div>
+            <div class="form-group">
+              <label class="control-label col-sm-2" for="name"><?=$lang['admin']['admin_domains'];?>:</label>
+              <div class="col-sm-10">
+                <select title="<?=$lang['admin']['search_domain_da'];?>" style="width:100%" name="domain[]" size="5" multiple>
+                <?php
+                foreach (mailbox_get_domains() as $domain) {
+                  echo "<option>".htmlspecialchars($domain)."</option>";
+                }
+                ?>
+                </select>
+              </div>
+            </div>
+            <div class="form-group">
+              <label class="control-label col-sm-2" for="password"><?=$lang['admin']['password'];?>:</label>
+              <div class="col-sm-10">
+              <input type="password" class="form-control" name="password" id="password" placeholder="">
+              </div>
+            </div>
+            <div class="form-group">
+              <label class="control-label col-sm-2" for="password2"><?=$lang['admin']['password_repeat'];?>:</label>
+              <div class="col-sm-10">
+              <input type="password" class="form-control" name="password2" id="password2" placeholder="">
+              </div>
+            </div>
+            <div class="form-group">
+              <div class="col-sm-offset-2 col-sm-10">
+                <div class="checkbox">
+                <label><input type="checkbox" name="active" checked> <?=$lang['admin']['active'];?></label>
+                </div>
+              </div>
+            </div>
+            <div class="form-group">
+              <div class="col-sm-offset-2 col-sm-10">
+                <button type="submit" name="add_domain_admin" class="btn btn-default"><?=$lang['admin']['add'];?></button>
+              </div>
+            </div>
+          </form>
+          </small>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <h4><span class="glyphicon glyphicon-wrench" aria-hidden="true"></span> <?=$lang['admin']['configuration'];?></h4>
+  <div class="panel panel-default">
+  <div class="panel-heading"><?=$lang['admin']['dkim_keys'];?></div>
+  <div id="collapseDKIM" class="panel-collapse">
+  <div class="panel-body">
+    <p style="margin-bottom:40px"><?=$lang['admin']['dkim_key_hint'];?></p>
+    <?php
+    foreach(mailbox_get_domains() as $domain) {
+        if (!empty($dkim = dkim_get_key_details($domain))) {
+      ?>
+        <div class="row">
+          <div class="col-xs-3">
+            <p>Domain: <strong><?=htmlspecialchars($domain);?></strong><br />
+              <span class="label label-success"><?=$lang['admin']['dkim_key_valid'];?></span>
+              <span class="label label-info"><?=$dkim['length'];?> bit</span>
+            </p>
+          </div>
+          <div class="col-xs-8">
+              <pre><?=$dkim['dkim_txt'];?></pre>
+          </div>
+          <div class="col-xs-1">
+            <form class="form-inline" method="post">
+              <input type="hidden" name="domain" value="<?=$domain;?>">
+              <input type="hidden" name="dkim_delete_key" value="1">
+                <a href="#" onclick="$(this).closest('form').submit()" data-toggle="tooltip" data-placement="top" title="<?=$lang['user']['delete_now'];?>"><span class="glyphicon glyphicon-remove"></span></a>
+            </form>
+          </div>
+        </div>
+      <?php
+      }
+      else {
+      ?>
+      <div class="row">
+        <div class="col-xs-3">
+          <p>Domain: <strong><?=htmlspecialchars($domain);?></strong><br /><span class="label label-danger"><?=$lang['admin']['dkim_key_missing'];?></span></p>
+        </div>
+        <div class="col-xs-8"><pre>-</pre></div>
+        <div class="col-xs-1">&nbsp;</div>
+      </div>
+      <?php
+      }
+      foreach(mailbox_get_alias_domains($domain) as $alias_domain) {
+        if (!empty($dkim = dkim_get_key_details($alias_domain))) {
+        ?>
+          <div class="row">
+            <div class="col-xs-offset-1 col-xs-2">
+              <p><small>↳ Alias-Domain: <strong><?=htmlspecialchars($alias_domain);?></strong><br /></small>
+                <span class="label label-success"><?=$lang['admin']['dkim_key_valid'];?></span>
+                <span class="label label-info"><?=$dkim['length'];?> bit</span>
+            </p>
+            </div>
+            <div class="col-xs-8">
+              <pre><?=$dkim['dkim_txt'];?></pre>
+            </div>
+            <div class="col-xs-1">
+              <form class="form-inline" method="post">
+                <input type="hidden" name="domain" value="<?=$alias_domain;?>">
+                <input type="hidden" name="dkim_delete_key" value="1">
+                <a href="#" onclick="$(this).closest('form').submit()" data-toggle="tooltip" data-placement="top" title="<?=$lang['user']['delete_now'];?>"><span class="glyphicon glyphicon-remove"></span></a>
+              </form>
+            </div>
+          </div>
+        <?php
+        }
+        else {
+        ?>
+        <div class="row">
+          <div class="col-xs-2 col-xs-offset-1">
+            <p><small>↳ Alias-Domain: <strong><?=htmlspecialchars($alias_domain);?></strong><br /></small><span class="label label-danger"><?=$lang['admin']['dkim_key_missing'];?></span></p>
+          </div>
+        <div class="col-xs-8"><pre>-</pre></div>
+        <div class="col-xs-1">&nbsp;</div>
+        </div>
+        <?php
+        }
+      }
+    }
+    foreach(dkim_get_blind_keys() as $blind) {
+      if (!empty($dkim = dkim_get_key_details($blind))) {
+      ?>
+        <div class="row">
+          <div class="col-xs-3">
+            <p>Domain: <strong><?=htmlspecialchars($blind);?></strong><br /><span class="label label-warning"><?=$lang['admin']['dkim_key_unused'];?></span></p>
+          </div>
+            <div class="col-xs-8">
+              <pre><?=$dkim['dkim_txt'];?></pre>
+            </div>
+            <div class="col-xs-1">
+              <form class="form-inline" method="post">
+                <input type="hidden" name="domain" value="<?=$blind;?>">
+                <input type="hidden" name="dkim_delete_key" value="1">
+                <a href="#" onclick="$(this).closest('form').submit()" data-toggle="tooltip" data-placement="top" title="<?=$lang['user']['delete_now'];?>"><span class="glyphicon glyphicon-remove"></span></a>
+              </form>
+            </div>
+        </div>
+      <?php
+      }
+    }
+    ?>
+    <legend style="margin-top:40px"><?=$lang['admin']['dkim_add_key'];?></legend>
+    <form class="form-inline" role="form" method="post">
+      <div class="form-group">
+        <label for="domain">Domain</label>
+        <input class="form-control" id="domain" name="domain" placeholder="example.org" required>
+      </div>
+      <div class="form-group">
+        <select data-width="200px" class="form-control" id="key_size" name="key_size" title="<?=$lang['admin']['dkim_key_length'];?>" required>
+          <option data-subtext="bits">1024</option>
+          <option data-subtext="bits">2048</option>
+        </select>
+      </div>
+      <button type="submit" name="dkim_add_key" class="btn btn-default"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['add'];?></button>
+    </form>
+  </div>
+  </div>
+  </div>
+</div> <!-- /container -->
+<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js" integrity="sha384-YWP9O4NjmcGo4oEJFXvvYSEzuHIvey+LbXkBNJ1Kd0yfugEZN9NCQNpRYBVC1RvA" crossorigin="anonymous"></script>
+<script src="js/sorttable.js"></script>
+<script src="js/admin.js"></script>
+<?php
+require_once("inc/footer.inc.php");
+} else {
+	header('Location: /');
+	exit();
+}
+?>

+ 69 - 0
data/web/autoconfig.php

@@ -0,0 +1,69 @@
+<?php
+require_once "inc/vars.inc.php";
+if (empty($mailcow_hostname)) { exit(); }
+header("Content-Type: application/xml");
+?>
+<?='<?xml version="1.0"?>';?>
+<clientConfig version="1.1">
+    <emailProvider id="<?=$mailcow_hostname;?>">
+
+      <displayName>A mailcow mail server</displayName>
+      <displayShortName>mail server</displayShortName>
+
+      <incomingServer type="imap">
+         <hostname><?=$mailcow_hostname;?></hostname>
+         <port>993</port>
+         <socketType>SSL</socketType>
+         <username>%EMAILADDRESS%</username>
+         <authentication>password-cleartext</authentication>
+      </incomingServer>
+      <incomingServer type="imap">
+         <hostname><?=$mailcow_hostname;?></hostname>
+         <port>143</port>
+         <socketType>STARTTLS</socketType>
+         <username>%EMAILADDRESS%</username>
+         <authentication>password-cleartext</authentication>
+      </incomingServer>
+
+      <incomingServer type="pop3">
+         <hostname><?=$mailcow_hostname;?></hostname>
+         <port>995</port>
+         <socketType>SSL</socketType>
+         <username>%EMAILADDRESS%</username>
+         <authentication>password-cleartext</authentication>
+      </incomingServer>
+      <incomingServer type="pop3">
+         <hostname><?=$mailcow_hostname;?></hostname>
+         <port>110</port>
+         <socketType>STARTTLS</socketType>
+         <username>%EMAILADDRESS%</username>
+         <authentication>password-cleartext</authentication>
+      </incomingServer>
+
+      <outgoingServer type="smtp">
+         <hostname><?=$mailcow_hostname;?></hostname>
+         <port>465</port>
+         <socketType>SSL</socketType>
+         <username>%EMAILADDRESS%</username>
+         <authentication>password-cleartext</authentication>
+      </outgoingServer>
+
+      <outgoingServer type="smtp">
+         <hostname><?=$mailcow_hostname;?></hostname>
+         <port>587</port>
+         <socketType>STARTTLS</socketType>
+         <username>%EMAILADDRESS%</username>
+         <authentication>password-cleartext</authentication>
+      </outgoingServer>
+
+      <enable visiturl="https://<?=$mailcow_hostname;?>/admin.php">
+         <instruction>If you didn't change the password given to you by the administrator or if you didn't change it in a long time, please consider doing that now.</instruction>
+         <instruction lang="de">Sollten Sie das Ihnen durch den Administrator vergebene Passwort noch nicht geändert haben, empfehlen wir dies nun zu tun. Auch ein altes Passwort sollte aus Sicherheitsgründen geändert werden.</instruction>
+      </enable>
+
+    </emailProvider>
+
+    <webMail>
+      <loginPage url="https://<?=$mailcow_hostname;?>/SOGo/" />
+    </webMail>
+</clientConfig>

+ 142 - 0
data/web/autodiscover.php

@@ -0,0 +1,142 @@
+<?php
+require_once 'inc/vars.inc.php';
+require_once 'inc/functions.inc.php';
+
+ini_set('error_reporting', '0');
+$config = array(
+     'useEASforOutlook' => 'yes',
+     'autodiscoverType' => 'activesync',
+     'imap' => array(
+       'server' => $mailcow_hostname,
+       'port' => '993',
+       'ssl' => 'on',
+     ),
+     'smtp' => array(
+       'server' => $mailcow_hostname,
+       'port' => '465',
+       'ssl' => 'on'
+     ),
+     'activesync' => array(
+       'url' => 'https://'.$mailcow_hostname.'/Microsoft-Server-ActiveSync'
+     )
+);
+
+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';
+	}
+}
+
+$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);
+$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 '<?xml version="1.0" encoding="utf-8" ?><Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">';
+
+      $data = trim(file_get_contents("php://input"));
+      if(!$data) {
+        list($usec, $sec) = explode(' ', microtime());
+        echo '<Response>';
+        echo '<Error Time="' . date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2) . '" Id="2477272013">';
+        echo '<ErrorCode>600</ErrorCode><Message>Invalid Request</Message><DebugData /></Error>';
+        echo '</Response>';
+        echo '</Autodiscover>';
+        exit(0);
+      }
+      $discover = new SimpleXMLElement($data);
+      $email = $discover->Request->EMailAddress;
+
+      if ($config['autodiscoverType'] == 'imap') {
+      ?>
+  <Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
+      <Account>
+          <AccountType>email</AccountType>
+          <Action>settings</Action>
+          <Protocol>
+              <Type>IMAP</Type>
+              <Server><?php echo $config['imap']['server']; ?></Server>
+              <Port><?php echo $config['imap']['port']; ?></Port>
+              <DomainRequired>off</DomainRequired>
+              <LoginName><?php echo $email; ?></LoginName>
+              <SPA>off</SPA>
+              <SSL><?php echo $config['imap']['ssl']; ?></SSL>
+              <AuthRequired>on</AuthRequired>
+          </Protocol>
+          <Protocol>
+              <Type>SMTP</Type>
+              <Server><?php echo $config['smtp']['server']; ?></Server>
+              <Port><?php echo $config['smtp']['port']; ?></Port>
+              <DomainRequired>off</DomainRequired>
+              <LoginName><?php echo $email; ?></LoginName>
+              <SPA>off</SPA>
+              <SSL><?php echo $config['smtp']['ssl']; ?></SSL>
+              <AuthRequired>on</AuthRequired>
+              <UsePOPAuth>on</UsePOPAuth>
+              <SMTPLast>off</SMTPLast>
+          </Protocol>
+      </Account>
+  </Response>
+      <?php
+      }
+      else if ($config['autodiscoverType'] == 'activesync') {
+        $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;
+        }
+      ?>
+  <Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/mobilesync/responseschema/2006">
+      <Culture>en:en</Culture>
+      <User>
+          <DisplayName><?php echo $displayname; ?></DisplayName>
+          <EMailAddress><?php echo $email; ?></EMailAddress>
+      </User>
+      <Action>
+          <Settings>
+              <Server>
+                  <Type>MobileSync</Type>
+                  <Url><?php echo $config['activesync']['url']; ?></Url>
+                  <Name><?php echo $config['activesync']['url']; ?></Name>
+              </Server>
+          </Settings>
+      </Action>
+  </Response>
+      <?php
+      }
+      ?>
+</Autodiscover>
+<?php
+		}
+	}
+}
+?>

+ 40 - 0
data/web/call_sogo_ctrl.php

@@ -0,0 +1,40 @@
+<?php
+session_start();
+$AuthUsers = array("admin");
+if (!isset($_SESSION['mailcow_cc_role']) OR !in_array($_SESSION['mailcow_cc_role'], $AuthUsers)) {
+	echo "Not allowed." . PHP_EOL;
+	exit();
+}
+if ($_GET['ACTION'] == "start") {
+	$request = xmlrpc_encode_request("supervisor.startProcessGroup", 'sogo-group', 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 '<b><span class="pull-right text-warning">' . $response['faultString'] . '</span></b>';
+	}
+	else {
+		echo '<b><span class="pull-right text-success">OK</span></b>';
+	}
+}
+elseif ($_GET['ACTION'] == "stop") {
+	$request = xmlrpc_encode_request("supervisor.stopProcessGroup", 'sogo-group', 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 '<b><span class="pull-right text-warning">' . $response['faultString'] . '</span></b>';
+	}
+	else {
+		echo '<b><span class="pull-right text-success">OK</span></b>';
+	}
+}
+?>

文件差异内容过多而无法显示
+ 5 - 0
data/web/css/bootstrap-select.min.css


文件差异内容过多而无法显示
+ 40 - 0
data/web/css/bootstrap-slider.min.css


文件差异内容过多而无法显示
+ 9 - 0
data/web/css/bootstrap-switch.min.css


+ 19 - 0
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;
+}

+ 46 - 0
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;}

+ 79 - 0
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;
+}

+ 212 - 0
data/web/delete.php

@@ -0,0 +1,212 @@
+<?php
+require_once("inc/prerequisites.inc.php");
+$AuthUsers = array("admin", "domainadmin", "user");
+if (!isset($_SESSION['mailcow_cc_role']) OR !in_array($_SESSION['mailcow_cc_role'], $AuthUsers)) {
+	header('Location: /');
+	exit();
+}
+require_once("inc/header.inc.php");
+?>
+<div class="container">
+	<div class="row">
+		<div class="col-md-12">
+			<div class="panel panel-default">
+				<div class="panel-heading">
+					<h3 class="panel-title"><?=$lang['delete']['title'];?></h3>
+				</div>
+				<div class="panel-body">
+<?php
+if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "admin"  || $_SESSION['mailcow_cc_role'] == "domainadmin")) {
+		// DELETE DOMAIN
+		if (isset($_GET["domain"]) &&
+			is_valid_domain_name($_GET["domain"]) &&
+			!empty($_GET["domain"]) &&
+			$_SESSION['mailcow_cc_role'] == "admin") {
+			$domain = $_GET["domain"];
+			?>
+				<div class="alert alert-warning" role="alert"><?=sprintf($lang['delete']['remove_domain_warning'], htmlspecialchars($_GET["domain"]));?></div>
+				<p><?=$lang['delete']['remove_domain_details'];?></p>
+				<form class="form-horizontal" role="form" method="post" action="/mailbox.php">
+				<input type="hidden" name="domain" value="<?php echo htmlspecialchars($domain) ?>">
+					<div class="form-group">
+						<div class="col-sm-offset-1 col-sm-10">
+							<button type="submit" name="mailbox_delete_domain" class="btn btn-default btn-sm"><?=$lang['delete']['remove_button'];?></button>
+						</div>
+					</div>
+				</form>
+			<?php
+		}
+		// DELETE ALIAS
+		elseif (isset($_GET["alias"]) &&
+			(filter_var($_GET["alias"], FILTER_VALIDATE_EMAIL) || is_valid_domain_name(substr(strrchr($_GET["alias"], "@"), 1))) &&
+			!empty($_GET["alias"])) {
+				$domain = substr(strrchr($_GET["alias"], "@"), 1);
+				if (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+				?>
+					<div class="alert alert-warning" role="alert"><?=sprintf($lang['delete']['remove_alias_warning'], htmlspecialchars($_GET["alias"]));?></div>
+					<p><?=$lang['delete']['remove_alias_details'];?></p>
+					<form class="form-horizontal" role="form" method="post" action="/mailbox.php">
+					<input type="hidden" name="address" value="<?php echo htmlspecialchars($_GET["alias"]) ?>">
+						<div class="form-group">
+							<div class="col-sm-offset-1 col-sm-10">
+								<button type="submit" name="mailbox_delete_alias" class="btn btn-default btn-sm"><?=$lang['delete']['remove_button'];?></button>
+							</div>
+						</div>
+					</form>
+				<?php
+				}
+				else {
+				?>
+					<div class="alert alert-info" role="alert"><?=$lang['info']['no_action'];?></div>
+				<?php
+				}
+		}
+		// DELETE ALIAS DOMAIN
+		elseif (
+			isset($_GET["aliasdomain"]) &&
+			is_valid_domain_name($_GET["aliasdomain"]) && 
+			!empty($_GET["aliasdomain"])) {
+        $alias_domain = $_GET["aliasdomain"];
+        $result = mailbox_get_alias_domain_details($alias_domain);
+				if (!empty($result)) {
+				?>
+					<div class="alert alert-warning" role="alert"><?=sprintf($lang['delete']['remove_domainalias_warning'], htmlspecialchars($_GET["aliasdomain"]));?></div>
+					<form class="form-horizontal" role="form" method="post" action="/mailbox.php">
+					<input type="hidden" name="alias_domain" value="<?php echo htmlspecialchars($alias_domain) ?>">
+						<div class="form-group">
+							<div class="col-sm-offset-1 col-sm-10">
+								<button type="submit" name="mailbox_delete_alias_domain" class="btn btn-default btn-sm"><?=$lang['delete']['remove_button'];?></button>
+							</div>
+						</div>
+					</form>
+				<?php
+				}
+				else {
+				?>
+					<div class="alert alert-info" role="alert"><?=$lang['info']['no_action'];?></div>
+				<?php
+				}
+		}
+		// DELETE DOMAIN ADMIN
+		elseif (isset($_GET["domainadmin"]) &&
+			ctype_alnum(str_replace(array('_', '.', '-'), '', $_GET["domainadmin"])) &&
+			!empty($_GET["domainadmin"]) &&
+        $_SESSION['mailcow_cc_role'] == "admin") {
+				$domain_admin = $_GET["domainadmin"];
+				?>
+				<div class="alert alert-warning" role="alert"><?=sprintf($lang['delete']['remove_domainadmin_warning'], htmlspecialchars($_GET["domainadmin"]));?></div>
+				<form class="form-horizontal" role="form" method="post" action="/admin.php">
+				<input type="hidden" name="username" value="<?=htmlspecialchars($domain_admin);?>">
+					<div class="form-group">
+						<div class="col-sm-offset-1 col-sm-10">
+							<button type="submit" name="delete_domain_admin" class="btn btn-default btn-sm"><?=$lang['delete']['remove_button'];?></button>
+						</div>
+					</div>
+				</form>
+				<?php
+		}
+		// DELETE MAILBOX
+		elseif (isset($_GET["mailbox"]) &&
+			filter_var($_GET["mailbox"], FILTER_VALIDATE_EMAIL) &&
+			!empty($_GET["mailbox"])) {
+				$mailbox = $_GET["mailbox"];
+				if (hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $mailbox)) {
+				?>
+					<div class="alert alert-warning" role="alert"><?=sprintf($lang['delete']['remove_mailbox_warning'], htmlspecialchars($mailbox));?></div>
+					<p><?=$lang['delete']['remove_mailbox_details'];?></p>
+					<form class="form-horizontal" role="form" method="post" action="/mailbox.php">
+					<input type="hidden" name="username" value="<?=htmlspecialchars($mailbox);?>">
+						<div class="form-group">
+							<div class="col-sm-offset-1 col-sm-10">
+								<button type="submit" name="mailbox_delete_mailbox" class="btn btn-default btn-sm"><?=$lang['delete']['remove_button'];?></button>
+							</div>
+						</div>
+					</form>
+				<?php
+				}
+				else {
+				?>
+					<div class="alert alert-info" role="alert"><?=$lang['info']['no_action'];?></div>
+				<?php
+				}
+		}
+		// DELETE RESOURCE
+		elseif (isset($_GET["resource"]) &&
+			filter_var($_GET["resource"], FILTER_VALIDATE_EMAIL) &&
+			!empty($_GET["resource"])) {
+				$resource = $_GET["resource"];
+				if (hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $resource)) {
+				?>
+					<div class="alert alert-warning" role="alert"><?=sprintf($lang['delete']['remove_resource_warning'], htmlspecialchars($resource));?></div>
+					<p><?=$lang['delete']['remove_resource_details'];?></p>
+					<form class="form-horizontal" role="form" method="post" action="/mailbox.php">
+					<input type="hidden" name="name" value="<?=htmlspecialchars($resource);?>">
+						<div class="form-group">
+							<div class="col-sm-offset-1 col-sm-10">
+								<button type="submit" name="mailbox_delete_resource" class="btn btn-default btn-sm"><?=$lang['delete']['remove_button'];?></button>
+							</div>
+						</div>
+					</form>
+				<?php
+				}
+				else {
+				?>
+					<div class="alert alert-info" role="alert"><?=$lang['info']['no_action'];?></div>
+				<?php
+				}
+		}
+		else {
+		?>
+			<div class="alert alert-info" role="alert"><?=$lang['info']['no_action'];?></div>
+		<?php
+		}
+}
+elseif (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "user")) {
+		// DELETE SYNCJOB
+		if (isset($_GET["syncjob"]) &&
+			is_numeric($_GET["syncjob"]) &&
+      filter_var($_SESSION['mailcow_cc_username'], FILTER_VALIDATE_EMAIL)) {
+        $id = $_GET["syncjob"];
+        $result = get_syncjob_details($id);
+        if (!empty($result)) {
+				?>
+					<div class="alert alert-warning" role="alert"><?=sprintf($lang['delete']['remove_syncjob_warning'], htmlspecialchars($result['user2']));?></div>
+					<p><?=$lang['delete']['remove_syncjob_details'];?></p>
+					<form class="form-horizontal" role="form" method="post" action="/user.php">
+					<input type="hidden" name="username" value="<?=htmlspecialchars($mailbox);?>">
+						<div class="form-group">
+							<div class="col-sm-offset-1 col-sm-10">
+								<input type="hidden" name="id" value="<?=$_GET["syncjob"];?>">
+								<button type="submit" name="delete_syncjob" value="1" class="btn btn-default btn-sm"><?=$lang['delete']['remove_button'];?></button>
+							</div>
+						</div>
+					</form>
+				<?php
+				}
+				else {
+				?>
+					<div class="alert alert-info" role="alert"><?=$lang['info']['no_action'];?></div>
+				<?php
+				}
+		}
+		else {
+		?>
+			<div class="alert alert-info" role="alert"><?=$lang['info']['no_action'];?></div>
+		<?php
+		}
+}
+else {
+?>
+	<div class="alert alert-danger" role="alert"><?=$lang['danger']['access_denied'];?></div>
+<?php
+}
+?>
+				</div>
+			</div>
+		</div>
+	</div>
+<a href="<?=$_SESSION['return_to'];?>">&#8592; <?=$lang['delete']['previous'];?></a>
+</div> <!-- /container -->
+<?php
+require_once("inc/footer.inc.php");
+?>

+ 664 - 0
data/web/edit.php

@@ -0,0 +1,664 @@
+<?php
+require_once("inc/prerequisites.inc.php");
+$AuthUsers = array("admin", "domainadmin", "user");
+if (!isset($_SESSION['mailcow_cc_role']) OR !in_array($_SESSION['mailcow_cc_role'], $AuthUsers)) {
+	header('Location: /');
+	exit();
+}
+require_once("inc/header.inc.php");
+?>
+<div class="container">
+	<div class="row">
+		<div class="col-md-12">
+			<div class="panel panel-default">
+				<div class="panel-heading">
+					<h3 class="panel-title"><?=$lang['edit']['title'];?></h3>
+				</div>
+				<div class="panel-body">
+<?php
+if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "admin"  || $_SESSION['mailcow_cc_role'] == "domainadmin")) {
+		if (isset($_GET["alias"]) &&
+			!empty($_GET["alias"])) {
+				$alias = $_GET["alias"];
+        $result = mailbox_get_alias_details($alias);
+				if (!empty($result)) {
+				?>
+					<h4><?=$lang['edit']['alias'];?></h4>
+					<br />
+					<form class="form-horizontal" role="form" method="post" action="<?=($FORM_ACTION == "previous") ? $_SESSION['return_to'] : null;?>">
+					<input type="hidden" name="address" value="<?=htmlspecialchars($alias);?>">
+						<div class="form-group">
+							<label class="control-label col-sm-2" for="goto"><?=$lang['edit']['target_address'];?></label>
+							<div class="col-sm-10">
+								<textarea class="form-control" autocapitalize="none" autocorrect="off" rows="10" id="goto" name="goto"><?=htmlspecialchars($result['goto']) ?></textarea>
+							</div>
+						</div>
+						<div class="form-group">
+							<div class="col-sm-offset-2 col-sm-10">
+								<div class="checkbox">
+								<label><input type="checkbox" name="active" <?php if (isset($result['active_int']) && $result['active_int']=="1") { echo "checked"; }; ?>> <?=$lang['edit']['active'];?></label>
+								</div>
+							</div>
+						</div>
+						<div class="form-group">
+							<div class="col-sm-offset-2 col-sm-10">
+								<button type="submit" name="mailbox_edit_alias" class="btn btn-success btn-sm"><?=$lang['edit']['save'];?></button>
+							</div>
+						</div>
+					</form>
+				<?php
+				}
+				else {
+				?>
+					<div class="alert alert-info" role="alert"><?=$lang['info']['no_action'];?></div>
+				<?php
+				}
+		}
+		elseif (isset($_GET['domainadmin']) && 
+			ctype_alnum(str_replace(array('_', '.', '-'), '', $_GET["domainadmin"])) &&
+			!empty($_GET["domainadmin"]) &&
+			$_GET["domainadmin"] != 'admin' &&
+			$_SESSION['mailcow_cc_role'] == "admin") {
+				$domain_admin = $_GET["domainadmin"];
+        $result = get_domain_admin_details($domain_admin);
+				if (!empty($result)) {
+				?>
+				<h4><?=$lang['edit']['domain_admin'];?></h4>
+				<br />
+				<form class="form-horizontal" role="form" method="post" action="<?=($FORM_ACTION == "previous") ? $_SESSION['return_to'] : null;?>">
+				<input type="hidden" name="username_now" value="<?=htmlspecialchars($domain_admin);?>">
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="username"><?=$lang['edit']['username'];?></label>
+						<div class="col-sm-10">
+              <input class="form-control" type="text" name="username" value="<?=htmlspecialchars($domain_admin);?>" />
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="domain"><?=$lang['edit']['domains'];?></label>
+						<div class="col-sm-10">
+							<select id="domain" name="domain[]" multiple>
+							<?php
+							foreach ($result['selected_domains'] as $domain):
+							?>
+								<option selected><?=htmlspecialchars($domain);?></option>
+							<?php
+							endforeach;
+							foreach ($result['unselected_domains'] as $domain):
+							?>
+								<option><?=htmlspecialchars($domain);?></option>
+							<?php
+							endforeach;
+							?>
+							</select>
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="password"><?=$lang['edit']['password'];?></label>
+						<div class="col-sm-10">
+						<input type="password" class="form-control" name="password" id="password" placeholder="">
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="password2"><?=$lang['edit']['password_repeat'];?></label>
+						<div class="col-sm-10">
+						<input type="password" class="form-control" name="password2" id="password2">
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<div class="checkbox">
+							<label><input type="checkbox" name="active" <?php if (isset($result['active_int']) && $result['active_int']=="1") { echo "checked"; }; ?>> <?=$lang['edit']['active'];?></label>
+							</div>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<div class="checkbox">
+							<label><input type="checkbox" name="disable_tfa"> <?=$lang['tfa']['disable_tfa'];?></label>
+							</div>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<button type="submit" name="edit_domain_admin" class="btn btn-success btn-sm"><?=$lang['edit']['save'];?></button>
+						</div>
+					</div>
+				</form>
+			<?php
+			}
+			else {
+			?>
+				<div class="alert alert-info" role="alert"><?=$lang['info']['no_action'];?></div>
+			<?php
+			}
+	}
+	elseif (isset($_GET['domain']) &&
+		is_valid_domain_name($_GET["domain"]) &&
+		!empty($_GET["domain"])) {
+			$domain = $_GET["domain"];
+      $result = mailbox_get_domain_details($domain);
+			if (!empty($result)) {
+			?>
+				<h4><?=$lang['edit']['domain'];?></h4>
+				<form class="form-horizontal" role="form" method="post" action="<?=($FORM_ACTION == "previous") ? $_SESSION['return_to'] : null;?>">
+				<input type="hidden" name="domain" value="<?=htmlspecialchars($domain);?>">
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="description"><?=$lang['edit']['description'];?></label>
+						<div class="col-sm-10">
+							<input type="text" class="form-control" name="description" id="description" value="<?=htmlspecialchars($result['description']);?>">
+						</div>
+					</div>
+					<?php
+					if ($_SESSION['mailcow_cc_role'] == "admin") {
+					?>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="aliases"><?=$lang['edit']['max_aliases'];?></label>
+						<div class="col-sm-10">
+							<input type="number" class="form-control" name="aliases" id="aliases" value="<?=intval($result['max_num_aliases_for_domain']);?>">
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="mailboxes"><?=$lang['edit']['max_mailboxes'];?></label>
+						<div class="col-sm-10">
+							<input type="number" class="form-control" name="mailboxes" id="mailboxes" value="<?=intval($result['max_num_mboxes_for_domain']);?>">
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="maxquota"><?=$lang['edit']['max_quota'];?></label>
+						<div class="col-sm-10">
+							<input type="number" class="form-control" name="maxquota" id="maxquota" value="<?=intval($result['max_new_mailbox_quota'] / 1048576);?>">
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="quota"><?=$lang['edit']['domain_quota'];?></label>
+						<div class="col-sm-10">
+							<input type="number" class="form-control" name="quota" id="quota" value="<?=intval($result['max_quota_for_domain'] / 1048576);?>">
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2"><?=$lang['edit']['backup_mx_options'];?></label>
+						<div class="col-sm-10">
+							<div class="checkbox">
+								<label><input type="checkbox" name="backupmx" <?=(isset($result['backupmx_int']) && $result['backupmx_int']=="1") ? "checked" : null;?>> <?=$lang['edit']['relay_domain'];?></label>
+								<br />
+								<label><input type="checkbox" name="relay_all_recipients" <?=(isset($result['relay_all_recipients_int']) && $result['relay_all_recipients_int']=="1") ? "checked" : null;?>> <?=$lang['edit']['relay_all'];?></label>
+								<p><?=$lang['edit']['relay_all_info'];?></p>
+							</div>
+						</div>
+					</div>
+					<?php
+					}
+					?>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<div class="checkbox">
+								<label><input type="checkbox" name="active" <?=(isset($result['active_int']) && $result['active_int']=="1") ? "checked" : null;?> <?=($_SESSION['mailcow_cc_role'] == "admin") ? null : "disabled";?>> <?=$lang['edit']['active'];?></label>
+							</div>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<button type="submit" name="mailbox_edit_domain" class="btn btn-success btn-sm"><?=$lang['edit']['save'];?></button>
+						</div>
+					</div>
+				</form>
+				<?php
+        if (!empty($dkim = dkim_get_key_details($domain))) {
+				?>
+        <hr>
+        <div class="row">
+          <div class="col-xs-2">
+            <p>Domain: <strong><?=htmlspecialchars($result['domain_name']);?></strong> (dkim._domainkey)</p>
+          </div>
+          <div class="col-xs-10">
+            <pre><?=$dkim['dkim_txt'];?></pre>
+          </div>
+        </div>
+				<?php
+				}
+        ?>
+		<hr>
+		<div class="row">
+			<div class="col-sm-6">
+				<h4><span class="glyphicon glyphicon-thumbs-up" aria-hidden="true"></span> <?=$lang['user']['spamfilter_wl'];?></h4>
+				<p><?=$lang['user']['spamfilter_wl_desc'];?></p>
+				<div class="row">
+					<div class="col-sm-6"><b><?=$lang['user']['spamfilter_table_rule'];?></b></div>
+					<div class="col-sm-6"><b><?=$lang['user']['spamfilter_table_action'];?></b></div>
+				</div>
+				<?php
+        $get_policy_list = get_policy_list($domain);
+				if (empty($get_policy_list['whitelist'])):
+				?>
+					<div class="row">
+						<div class="col-sm-12"><i><?=$lang['user']['spamfilter_table_empty'];?></i></div>
+					</div>
+				<?php
+				else:
+          foreach($get_policy_list['whitelist'] as $wl):
+          ?>
+          <div class="row striped">
+            <form class="form-inline" method="post">
+            <div class="col-xs-6"><code><?=$wl['value'];?></code></div>
+            <div class="col-xs-6">
+              <?php
+              if ($wl['object'] == $domain):
+              ?>
+                <input type="hidden" name="delete_prefid" value="<?=$wl['prefid'];?>">
+                <input type="hidden" name="delete_policy_list_item">
+                <input type="hidden" name="domain" value="<?=$domain;?>">
+                <a href="#" onclick="$(this).closest('form').submit()" data-toggle="tooltip" data-placement="left" title="<?=$lang['user']['delete_now'];?>"><span class="glyphicon glyphicon-remove"></span></a>
+              <?php
+              else:
+              ?>
+                <span style="cursor:not-allowed"><?=$lang['user']['spamfilter_table_domain_policy'];?></span>
+              <?php
+              endif;
+              ?>
+            </div>
+            </form>
+          </div>
+          <?php
+          endforeach;
+        endif;
+				?>
+				<hr style="margin:5px 0px 7px 0px">
+				<div class="row">
+					<form class="form-inline" method="post">
+					<div class="col-xs-6">
+						<input type="text" class="form-control input-sm" name="object_from" id="object_from" placeholder="*@example.org" required>
+						<input type="hidden" name="object_list" value="wl">
+						<input type="hidden" name="domain" value="<?=$domain;?>">
+					</div>
+					<div class="col-xs-6">
+						<button type="submit" name="add_policy_list_item" class="btn btn-xs btn-default"><?=$lang['user']['spamfilter_table_add'];?></button>
+					</div>
+					</form>
+				</div>
+			</div>
+			<div class="col-sm-6">
+				<h4><span class="glyphicon glyphicon-thumbs-down" aria-hidden="true"></span> <?=$lang['user']['spamfilter_bl'];?></h4>
+				<p><?=$lang['user']['spamfilter_bl_desc'];?></p>
+				<div class="row">
+					<div class="col-sm-6"><b><?=$lang['user']['spamfilter_table_rule'];?></b></div>
+					<div class="col-sm-6"><b><?=$lang['user']['spamfilter_table_action'];?></b></div>
+				</div>
+				<?php
+				if (empty($get_policy_list['blacklist'])):
+				?>
+					<div class="row">
+						<div class="col-sm-12"><i><?=$lang['user']['spamfilter_table_empty'];?></i></div>
+					</div>
+				<?php
+				else:
+          foreach($get_policy_list['blacklist'] as $bl):
+          ?>
+          <div class="row striped">
+            <form class="form-inline" method="post">
+            <div class="col-xs-6"><code><?=$bl['value'];?></code></div>
+            <div class="col-xs-6">
+              <input type="hidden" name="delete_prefid" value="<?=$bl['prefid'];?>">
+              <?php
+              if ($bl['object'] == $domain):
+              ?>
+                <input type="hidden" name="delete_policy_list_item">
+                <input type="hidden" name="domain" value="<?=$domain;?>">
+                <a href="#" onclick="$(this).closest('form').submit()" data-toggle="tooltip" data-placement="left" title="<?=$lang['user']['delete_now'];?>"><span class="glyphicon glyphicon-remove"></span></a>
+              <?php
+              else:
+              ?>
+                <span style="cursor:not-allowed"><?=$lang['user']['spamfilter_table_domain_policy'];?></span>
+              <?php
+              endif;
+              ?>
+            </div>
+            </form>
+          </div>
+          <?php
+          endforeach;
+        endif;
+				?>
+				<hr style="margin:5px 0px 7px 0px">
+				<div class="row">
+					<form class="form-inline" method="post">
+					<div class="col-xs-6">
+						<input type="text" class="form-control input-sm" name="object_from" id="object_from" placeholder="*@example.org" required>
+						<input type="hidden" name="object_list" value="bl">
+						<input type="hidden" name="domain" value="<?=$domain;?>">
+					</div>
+					<div class="col-xs-6">
+						<button type="submit" name="add_policy_list_item" class="btn btn-xs btn-default"><?=$lang['user']['spamfilter_table_add'];?></button>
+					</div>
+					</form>
+				</div>
+			</div>
+		</div>
+        <?php
+			}
+			else {
+			?>
+				<div class="alert alert-info" role="alert"><?=$lang['info']['no_action'];?></div>
+			<?php
+			}
+	}
+	elseif (isset($_GET['aliasdomain']) &&
+		is_valid_domain_name($_GET["aliasdomain"]) &&
+		!empty($_GET["aliasdomain"])) {
+			$alias_domain = $_GET["aliasdomain"];
+      $result = mailbox_get_alias_domain_details($alias_domain);
+      if (!empty($result)) {
+			?>
+				<h4><?=$lang['edit']['edit_alias_domain'];?></h4>
+				<form class="form-horizontal" role="form" method="post" action="<?=($FORM_ACTION == "previous") ? $_SESSION['return_to'] : null;?>">
+					<input type="hidden" name="alias_domain_now" value="<?=htmlspecialchars($alias_domain);?>">
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="alias_domain"><?=$lang['edit']['alias_domain'];?></label>
+						<div class="col-sm-10">
+							<input type="text" class="form-control" name="alias_domain" id="alias_domain" value="<?=htmlspecialchars($result['alias_domain']);?>">
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<div class="checkbox">
+								<label><input type="checkbox" name="active" <?=(isset($result['active_int']) && $result['active_int']=="1") ?  "checked" : null ?>> <?=$lang['edit']['active'];?></label>
+							</div>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<button type="submit" name="mailbox_edit_alias_domain" class="btn btn-success btn-sm"><?=$lang['edit']['save'];?></button>
+						</div>
+					</div>
+				</form>
+				<?php
+        if (!empty($dkim = dkim_get_key_details($alias_domain))) {
+				?>
+        <hr>
+        <div class="row">
+          <div class="col-xs-2">
+            <p>Domain: <strong><?=htmlspecialchars($result['alias_domain']);?></strong> (dkim._domainkey)</p>
+          </div>
+          <div class="col-xs-10">
+          <pre><?=$dkim['dkim_txt'];?></pre>
+          </div>
+        </div>
+				<?php
+				}
+			}
+			else {
+			?>
+				<div class="alert alert-info" role="alert"><?=$lang['info']['no_action'];?></div>
+			<?php
+			}
+	}
+	elseif (isset($_GET['mailbox']) && filter_var($_GET["mailbox"], FILTER_VALIDATE_EMAIL) && !empty($_GET["mailbox"])) {
+    $mailbox = $_GET["mailbox"];
+    $result = mailbox_get_mailbox_details($mailbox);
+    if (!empty($result)) {
+      ?>
+      <h4><?=$lang['edit']['mailbox'];?></h4>
+      <form class="form-horizontal" role="form" method="post" action="<?=($FORM_ACTION == "previous") ? $_SESSION['return_to'] : null;?>">
+      <input type="hidden" name="username" value="<?=htmlspecialchars($result['username']);?>">
+        <div class="form-group">
+          <label class="control-label col-sm-2" for="name"><?=$lang['edit']['full_name'];?>:</label>
+          <div class="col-sm-10">
+          <input type="text" class="form-control" name="name" id="name" value="<?=htmlspecialchars($result['name'], ENT_QUOTES, 'UTF-8');?>">
+          </div>
+        </div>
+        <div class="form-group">
+          <label class="control-label col-sm-2" for="quota"><?=$lang['edit']['quota_mb'];?>:
+            <br /><span id="quotaBadge" class="badge">max. <?=intval($result['max_new_quota'] / 1048576)?> MiB</span>
+          </label>
+          <div class="col-sm-10">
+            <input type="number" name="quota" id="quota" id="destroyable" style="width:100%" min="1" max="<?=intval($result['max_new_quota'] / 1048576);?>" value="<?=intval($result['quota']) / 1048576;?>" class="form-control">
+          </div>
+        </div>
+        <div class="form-group">
+          <label class="control-label col-sm-2" for="sender_acl"><?=$lang['edit']['sender_acl'];?>:</label>
+          <div class="col-sm-10">
+            <select data-width="50%" style="width:100%" id="sender_acl" name="sender_acl[]" size="10" multiple>
+            <?php
+            $sender_acl_handles = mailbox_get_sender_acl_handles($mailbox);
+
+            foreach ($sender_acl_handles['sender_acl_domains']['ro'] as $domain):
+              ?>
+              <option data-subtext="Admin" value="<?=htmlspecialchars($domain);?>" disabled selected><?=htmlspecialchars(sprintf($lang['edit']['dont_check_sender_acl'], $domain));?></option>
+              <?php
+            endforeach;
+
+            foreach ($sender_acl_handles['sender_acl_addresses']['ro'] as $domain):
+              ?>
+            <option data-subtext="Admin" disabled selected><?=htmlspecialchars($alias);?></option>
+              <?php
+            endforeach;
+
+            foreach ($sender_acl_handles['fixed_sender_aliases'] as $alias):
+              ?>
+              <option data-subtext="Alias" disabled selected><?=htmlspecialchars($alias);?></option>
+              <?php
+            endforeach;
+
+            foreach ($sender_acl_handles['sender_acl_domains']['rw'] as $domain):
+              ?>
+              <option value="<?=htmlspecialchars($domain);?>" selected><?=htmlspecialchars(sprintf($lang['edit']['dont_check_sender_acl'], $domain));?></option>
+              <?php
+            endforeach;
+
+            foreach ($sender_acl_handles['sender_acl_domains']['selectable'] as $domain):
+              ?>
+              <option value="<?=htmlspecialchars($domain);?>"><?=htmlspecialchars(sprintf($lang['edit']['dont_check_sender_acl'], $domain));?></option>
+              <?php
+            endforeach;
+
+            foreach ($sender_acl_handles['sender_acl_addresses']['rw'] as $address):
+              ?>
+                <option selected><?=htmlspecialchars($address);?></option>
+              <?php
+            endforeach;
+
+            foreach ($sender_acl_handles['sender_acl_addresses']['selectable'] as $address):
+              ?>
+                <option><?=htmlspecialchars($address);?></option>
+              <?php
+            endforeach;
+
+            ?>
+            </select>
+          </div>
+        </div>
+        <div class="form-group">
+          <label class="control-label col-sm-2" for="password"><?=$lang['edit']['password'];?></label>
+          <div class="col-sm-10">
+          <input type="password" class="form-control" name="password" id="password" placeholder="<?=$lang['edit']['unchanged_if_empty'];?>">
+          </div>
+        </div>
+        <div class="form-group">
+          <label class="control-label col-sm-2" for="password2"><?=$lang['edit']['password_repeat'];?></label>
+          <div class="col-sm-10">
+          <input type="password" class="form-control" name="password2" id="password2">
+          </div>
+        </div>
+        <div class="form-group">
+          <div class="col-sm-offset-2 col-sm-10">
+            <div class="checkbox">
+            <label><input type="checkbox" name="active" <?=($result['active_int']=="1") ? "checked" : null;?>> <?=$lang['edit']['active'];?></label>
+            </div>
+          </div>
+        </div>
+        <div class="form-group">
+          <div class="col-sm-offset-2 col-sm-10">
+            <button type="submit" name="mailbox_edit_mailbox" class="btn btn-success btn-sm"><?=$lang['edit']['save'];?></button>
+          </div>
+        </div>
+      </form>
+    <?php
+    }
+  }
+	elseif (isset($_GET['resource']) && filter_var($_GET["resource"], FILTER_VALIDATE_EMAIL) && !empty($_GET["resource"])) {
+			$resource = $_GET["resource"];
+      $result = mailbox_get_resource_details($resource);
+      if (!empty($result)) {
+        ?>
+				<h4><?=$lang['edit']['resource'];?></h4>
+				<form class="form-horizontal" role="form" method="post" action="<?=($FORM_ACTION == "previous") ? $_SESSION['return_to'] : null;?>">
+          <input type="hidden" name="name" value="<?=htmlspecialchars($result['name']);?>">
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="description"><?=$lang['add']['description'];?></label>
+						<div class="col-sm-10">
+							<input type="text" class="form-control" name="description" id="description" value="<?=htmlspecialchars($result['description'], ENT_QUOTES, 'UTF-8');?>" required>
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="domain"><?=$lang['edit']['kind'];?>:</label>
+						<div class="col-sm-10">
+							<select name="kind" id="kind" title="<?=$lang['edit']['select'];?>" required>
+								<option value="location" <?=($result['kind'] == "location") ? "selected" : null;?>>Location</option>
+								<option value="group" <?=($result['kind'] == "group") ? "selected" : null;?>>Group</option>
+								<option value="thing" <?=($result['kind'] == "thing") ? "selected" : null;?>>Thing</option>
+							</select>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<div class="checkbox">
+							<label><input type="checkbox" name="active" <?=($result['active_int']=="1") ? "checked" : null;?>> <?=$lang['edit']['active'];?></label>
+							</div>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<div class="checkbox">
+							<label><input type="checkbox" name="multiple_bookings" <?=($result['multiple_bookings_int']=="1") ? "checked" : null;?>> <?=$lang['edit']['multiple_bookings'];?></label>
+							</div>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<button type="submit" name="mailbox_edit_resource" class="btn btn-success btn-sm"><?=$lang['edit']['save'];?></button>
+						</div>
+					</div>
+				</form>
+			<?php
+			}
+			else {
+			?>
+				<div class="alert alert-info" role="alert"><?=$lang['info']['no_action'];?></div>
+			<?php
+			}
+	}
+	else {
+	?>
+		<div class="alert alert-info" role="alert"><?=$lang['info']['no_action'];?></div>
+	<?php
+	}
+}
+elseif (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "user")) {
+	if (isset($_GET['syncjob']) &&
+    is_numeric($_GET['syncjob'])) {
+			$id = $_GET["syncjob"];
+      $result = get_syncjob_details($id);
+      if (!empty($result)) {
+			?>
+				<h4><?=$lang['edit']['syncjob'];?></h4>
+				<form class="form-horizontal" role="form" method="post" action="<?=($FORM_ACTION == "previous") ? $_SESSION['return_to'] : null;?>">
+				<input type="hidden" name="id" value="<?=htmlspecialchars($result['id']);?>">
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="host1"><?=$lang['edit']['hostname'];?></label>
+						<div class="col-sm-10">
+						<input type="text" class="form-control" name="host1" id="host1" value="<?=htmlspecialchars($result['host1'], ENT_QUOTES, 'UTF-8');?>">
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="port1">Port</label>
+						<div class="col-sm-10">
+						<input type="number" class="form-control" name="port1" id="port1" min="1" max="65535" value="<?=htmlspecialchars($result['port1'], ENT_QUOTES, 'UTF-8');?>">
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="user1"><?=$lang['edit']['username'];?></label>
+						<div class="col-sm-10">
+						<input type="text" class="form-control" name="user1" id="user1" value="<?=htmlspecialchars($result['user1'], ENT_QUOTES, 'UTF-8');?>">
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="password1"><?=$lang['edit']['password'];?></label>
+						<div class="col-sm-10">
+						<input type="text" class="form-control" name="password1" id="password1" value="<?=htmlspecialchars($result['password1'], ENT_QUOTES, 'UTF-8');?>">
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="enc1"><?=$lang['edit']['encryption'];?>:</label>
+						<div class="col-sm-10">
+							<select id="enc1" name="enc1">
+								<option <?=($result['enc1'] == "TLS") ? "selected" : null;?>>TLS</option>
+								<option <?=($result['enc1'] == "SSL") ? "selected" : null;?>>SSL</option>
+								<option <?=($result['enc1'] == "PLAIN") ? "selected" : null;?>>PLAIN</option>
+							</select>
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="mins_interval"><?=$lang['edit']['mins_interval'];?></label>
+						<div class="col-sm-10">
+              <input type="number" class="form-control" name="mins_interval" min="10" max="3600" value="<?=htmlspecialchars($result['mins_interval'], ENT_QUOTES, 'UTF-8');?>" required>
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="subfolder2"><?=$lang['edit']['subfolder2'];?></label>
+						<div class="col-sm-10">
+						<input type="text" class="form-control" name="subfolder2" id="subfolder2" value="<?=htmlspecialchars($result['subfolder2'], ENT_QUOTES, 'UTF-8');?>">
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="maxage"><?=$lang['edit']['maxage'];?></label>
+						<div class="col-sm-10">
+						<input type="number" class="form-control" name="maxage" id="maxage" value="<?=htmlspecialchars($result['maxage'], ENT_QUOTES, 'UTF-8');?>">
+						</div>
+					</div>
+					<div class="form-group">
+						<label class="control-label col-sm-2" for="exclude"><?=$lang['edit']['exclude'];?></label>
+						<div class="col-sm-10">
+						<input type="text" class="form-control" name="exclude" id="exclude" value="<?=htmlspecialchars($result['exclude'], ENT_QUOTES, 'UTF-8');?>">
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<div class="checkbox">
+							<label><input type="checkbox" name="active" <?=($result['active']=="1") ? "checked" : "";?>> <?=$lang['edit']['active'];?></label>
+							</div>
+						</div>
+					</div>
+					<div class="form-group">
+						<div class="col-sm-offset-2 col-sm-10">
+							<button type="submit" name="edit_syncjob" value="1" class="btn btn-success btn-sm"><?=$lang['edit']['save'];?></button>
+						</div>
+					</div>
+				</form>
+			<?php
+			}
+			else {
+			?>
+				<div class="alert alert-info" role="alert"><?=$lang['info']['no_action'];?></div>
+			<?php
+			}
+	}
+	else {
+	?>
+		<div class="alert alert-info" role="alert"><?=$lang['info']['no_action'];?></div>
+	<?php
+	}
+}
+else {
+?>
+	<div class="alert alert-danger" role="alert"><?=$lang['danger']['access_denied'];?></div>
+<?php
+}
+?>
+				</div>
+			</div>
+		</div>
+	</div>
+<a href="<?=$_SESSION['return_to'];?>">&#8592; <?=$lang['edit']['previous'];?></a>
+</div> <!-- /container -->
+<?php
+require_once("inc/footer.inc.php");
+?>

二进制
data/web/favicon.png


+ 197 - 0
data/web/img/cow_mailcow.svg

@@ -0,0 +1,197 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   version="1.1"
+   id="Layer_1"
+   x="0px"
+   y="0px"
+   width="446.22601"
+   height="396.02499"
+   viewBox="0 0 446.226 396.02499"
+   enable-background="new 0 0 1600 1200"
+   xml:space="preserve"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="cow_mailcow.svg"><metadata
+     id="metadata144"><rdf:RDF><cc:Work
+         rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
+     id="defs142" /><sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1097"
+     inkscape:window-height="1138"
+     id="namedview140"
+     showgrid="false"
+     inkscape:zoom="1.1125147"
+     inkscape:cx="261.00704"
+     inkscape:cy="233.97883"
+     inkscape:window-x="814"
+     inkscape:window-y="0"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="Layer_1"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0" /><g
+     id="g3"
+     transform="translate(-576.88698,-401.988)"><g
+       id="g5"><g
+         id="g7"><g
+           id="email"><path
+             d="m 890.306,557.81 29.26,11.373 0,172.027 c 0,9.753 -7.895,17.649 -17.638,17.649 l -235.998,0 c -9.743,0 -17.638,-7.896 -17.638,-17.649 l 0,-172.026 29.259,-8.937"
+             id="path10"
+             inkscape:connector-curvature="0"
+             style="fill:#5a3620" /><path
+             d="M 758.871,656.221 649.49,747.45 c 2.507,6.648 8.901,11.409 16.44,11.409 l 235.998,0 c 7.536,0 13.933,-4.761 16.444,-11.409 l -107.402,-91.229 -52.099,0 z"
+             id="path12"
+             inkscape:connector-curvature="0"
+             style="fill:#fee70f;fill-opacity:0.89499998" /><g
+             id="g14"><path
+               d="m 810.391,656.686 107.981,90.764 c -0.331,0.881 -0.744,1.726 -1.205,2.536 l 0.028,0.035 c 1.501,-2.596 2.371,-5.594 2.371,-8.81 l 0,-172.004 -109.175,87.479 z"
+               id="path16"
+               inkscape:connector-curvature="0"
+               style="fill:#f9e82d;fill-opacity:1" /><path
+               d="m 649.49,747.45 108.864,-90.764 -110.061,-87.479 0,172.003 c 0,3.216 0.876,6.214 2.367,8.81 l 0.039,-0.035 c -0.466,-0.809 -0.877,-1.654 -1.209,-2.535 z"
+               id="path18"
+               inkscape:connector-curvature="0"
+               style="fill:#f9e82d;fill-opacity:1" /></g></g><path
+           d="m 961.81,681.214 c 0,0 -15.232,16.783 -42.244,14.73 l 0,28.14 c 13.328,-5.185 47.061,-20.036 56.854,-40.809 l -14.61,-2.061 z"
+           id="path20"
+           inkscape:connector-curvature="0"
+           style="fill:#b58765" /><path
+           d="m 984.594,658.413 c 3.59,-9.156 7.701,-11 9.346,-11.346 -49.276,4.542 -32.99,38.693 -32.99,38.693 0,0 6.229,14.728 26.532,13.892 27.063,0.461 35.631,-50.166 35.631,-50.166 -6.654,11.655 -26.404,9.876 -38.519,8.927 z"
+           id="path22"
+           inkscape:connector-curvature="0"
+           style="fill:#fef3df" /><ellipse
+           cx="787.75098"
+           cy="788.54498"
+           rx="210.864"
+           ry="9.4680004"
+           id="ellipse24"
+           style="fill:#f1f2f2" /><path
+           d="m 783.931,446.247 c -66.396,0 -120.223,53.827 -120.223,120.223 0,66.396 53.827,120.221 120.223,120.221 66.397,0 120.222,-53.825 120.222,-120.221 0,-66.395 -53.825,-120.223 -120.222,-120.223 z m -11.96,215.702 c -53.009,0 -95.982,-43.855 -95.982,-97.953 0,-54.098 42.973,-97.952 95.982,-97.952 53.007,0 95.98,43.855 95.98,97.952 -10e-4,54.098 -42.973,97.953 -95.98,97.953 z"
+           id="path26"
+           inkscape:connector-curvature="0"
+           style="opacity:0.1;fill:#3d5263" /><g
+           id="g28"><g
+             id="g30"><polyline
+               points="691.144,492.5 673.257,540.276 686.55,605.582 712.496,631.852      "
+               id="polyline32"
+               style="fill:#3d5263" /><g
+               id="g34"><g
+                 id="g36"><polyline
+                   points="658.248,450.81 673.501,487.076 693.836,496.903 724.04,458.731        "
+                   id="polyline38"
+                   style="fill:#fef3df" /><g
+                   id="g40"><path
+                     d="m 710.634,473.205 c 0,0 -22.482,-25.556 -49.793,-18.975 0,0 4.667,34.118 46.349,44.019 l 2.61,8.533 c 0,0 -65.612,-9.689 -59.339,-67.593 0,0 49.008,-19.884 72.598,15.106"
+                     id="path42"
+                     inkscape:connector-curvature="0"
+                     style="fill:#b58765" /><polyline
+                     points="909.697,450.81 894.447,487.076 874.114,496.903 843.907,458.731         "
+                     id="polyline44"
+                     style="fill:#fef3df" /><path
+                     d="m 857.314,473.205 c 0,0 22.48,-25.556 49.79,-18.975 0,0 -4.664,34.118 -46.347,44.019 l -2.613,8.533 c 0,0 65.611,-9.689 59.339,-67.593 0,0 -49.006,-19.884 -72.6,15.106"
+                     id="path46"
+                     inkscape:connector-curvature="0"
+                     style="fill:#b58765" /></g></g><path
+                 d="m 726.619,647.067 55.945,0 16.40428,-204.81407 c -55.814,0 -112.41728,30.01707 -112.41728,77.85207 0,1.454 0.085,2.787 0.121,4.175 0.127,3.934 0.448,7.585 0.856,11.135 1.689,14.816 5.451,27.177 8.461,43.383 1.452,7.831 5.002,23.374 5.002,23.374 0.056,0.408 0.165,0.804 0.224,1.211 2.535,16.546 11.832,32.027 25.404,43.684 z"
+                 id="path48"
+                 inkscape:connector-curvature="0"
+                 style="fill:#b58765"
+                 sodipodi:nodetypes="cccscccccc" /><path
+                 d="m 781.992,433.489 0,213.577 55.944,0 c 13.572,-11.657 22.867,-27.138 25.406,-43.684 0.058,-0.407 0.163,-0.803 0.221,-1.211 0,0 3.549,-15.543 5.002,-23.374 3.011,-16.206 6.774,-28.567 8.464,-43.381 0.405,-3.552 0.724,-7.203 0.846,-11.137 0.042,-1.388 0.126,-2.721 0.126,-4.175 0,-47.834 -40.191,-86.615 -96.009,-86.615 z"
+                 id="path50"
+                 inkscape:connector-curvature="0"
+                 style="fill:#b58765" /><g
+                 id="g52"><g
+                   id="g54"><path
+                     d="m 860.944,613.502 c 0,28.321 -35.091,51.289 -78.383,51.289 -43.299,0 -78.388,-22.968 -78.388,-51.289 0,-28.325 35.089,-51.289 78.388,-51.289 43.292,0 78.383,22.964 78.383,51.289 z"
+                     id="path56"
+                     inkscape:connector-curvature="0"
+                     style="fill:#fef3df" /></g></g><g
+                 id="g58"><g
+                   id="g60"><g
+                     id="g62"><path
+                       d="m 747.044,605.582 c 0,6.215 -5.04,11.256 -11.261,11.256 -6.21,0 -11.253,-5.041 -11.253,-11.256 0,-6.223 5.043,-11.257 11.253,-11.257 6.22,0 11.261,5.034 11.261,11.257 z"
+                       id="path64"
+                       inkscape:connector-curvature="0"
+                       style="fill:#5a3620" /></g></g><g
+                   id="g66"><g
+                     id="g68"><path
+                       d="m 840.856,605.582 c 0,6.215 -5.037,11.256 -11.257,11.256 -6.218,0 -11.259,-5.041 -11.259,-11.256 0,-6.223 5.041,-11.257 11.259,-11.257 6.22,0 11.257,5.034 11.257,11.257 z"
+                       id="path70"
+                       inkscape:connector-curvature="0"
+                       style="fill:#5a3620" /></g></g></g><g
+                 id="g72"><path
+                   d="m 875.228,525.835 c 0.354,-3.113 0.634,-6.311 0.743,-9.754 0.035,-1.218 0.109,-2.384 0.109,-3.661 0,-40.785 -33.369,-74.043 -80.237,-75.775 l -7.335,0.005 c -0.003,0 -0.003,0 -0.006,0 -0.007,0.018 -28.632,88.422 76.583,140.268 0.946,-4.317 2.078,-9.585 2.73,-13.088 2.64,-14.196 5.934,-25.021 7.413,-37.995 z"
+                   id="path74"
+                   inkscape:connector-curvature="0"
+                   style="fill:#87654a" /></g><g
+                 id="g76"><g
+                   id="g78"><g
+                     id="g80"><g
+                       id="g82"><path
+                         d="m 843.907,519.681 c 0,6.964 -5.65,12.611 -12.618,12.611 -6.963,0 -12.614,-5.646 -12.614,-12.611 0,-6.97 5.651,-12.614 12.614,-12.614 6.968,0 12.618,5.644 12.618,12.614 z"
+                         id="path84"
+                         inkscape:connector-curvature="0"
+                         style="fill:#5a3620" /></g></g></g><g
+                   id="g86"><g
+                     id="g88"><g
+                       id="g90"><path
+                         d="m 752.028,519.681 c 0,6.964 -5.649,12.611 -12.612,12.611 -6.969,0 -12.612,-5.646 -12.612,-12.611 0,-6.97 5.642,-12.614 12.612,-12.614 6.964,0 12.612,5.644 12.612,12.614 z"
+                         id="path92"
+                         inkscape:connector-curvature="0"
+                         style="fill:#5a3620" /></g></g></g><g
+                   id="g94"><g
+                     id="g96"><path
+                       d="m 748.75,515.894 c 0,2.558 -2.071,4.629 -4.63,4.629 -2.558,0 -4.633,-2.072 -4.633,-4.629 0,-2.552 2.076,-4.626 4.633,-4.626 2.559,0 4.63,2.073 4.63,4.626 z"
+                       id="path98"
+                       inkscape:connector-curvature="0"
+                       style="fill:#ffffff" /></g></g><g
+                   id="g100"><g
+                     id="g102"><path
+                       d="m 839.771,515.894 c 0,2.558 -2.073,4.629 -4.629,4.629 -2.558,0 -4.631,-2.072 -4.631,-4.629 0,-2.552 2.072,-4.626 4.631,-4.626 2.555,0 4.629,2.073 4.629,4.626 z"
+                       id="path104"
+                       inkscape:connector-curvature="0"
+                       style="fill:#ffffff" /></g></g></g></g><path
+               d="m 734.557,443.625 c 0,0 -18.236,-25.199 0,-41.637 0,0 13.125,32.012 40.242,31.502"
+               id="path106"
+               inkscape:connector-curvature="0"
+               style="fill:#fef3df" /><path
+               d="m 834.496,443.625 c 0,0 18.236,-25.199 0,-41.637 0,0 -13.126,32.012 -40.242,31.502"
+               id="path108"
+               inkscape:connector-curvature="0"
+               style="fill:#fef3df" /><path
+               d="m 786.264,431.965 c -66.396,0 -120.223,53.827 -120.223,120.223 0,66.396 53.827,120.221 120.223,120.221 66.397,0 120.222,-53.825 120.222,-120.221 10e-4,-66.395 -53.825,-120.223 -120.222,-120.223 z m -11.96,215.702 c -53.009,0 -95.982,-43.855 -95.982,-97.953 0,-54.098 42.973,-97.952 95.982,-97.952 53.007,0 95.979,43.855 95.979,97.952 0,54.098 -42.972,97.953 -95.979,97.953 z"
+               id="path110"
+               inkscape:connector-curvature="0"
+               style="fill:#f1f2f2" /></g><g
+             id="g112"><path
+               d="m 781.737,436.751 c 66.396,0 120.221,53.827 120.221,120.223 0,30.718 -11.526,58.74 -30.482,79.991 21.636,-21.74 35.01,-51.708 35.01,-84.803 0,-66.395 -53.825,-120.222 -120.222,-120.222 -35.678,0 -67.721,15.549 -89.739,40.233 21.772,-21.879 51.91,-35.422 85.212,-35.422 z"
+               id="path114"
+               inkscape:connector-curvature="0"
+               style="fill:#ffffff" /></g></g><path
+           d="m 919.566,695.944 c 0,0 7.562,0.712 13.317,-0.502 l 13.013,16.12 c 0,0 -17.639,9.525 -26.33,12.523 l 0,-28.141 z"
+           id="path116"
+           inkscape:connector-curvature="0"
+           style="opacity:0.1;fill:#3d5263" /></g><path
+         d="m 648.292,659.614 0,81.645 c 0,9.72 7.88,17.6 17.6,17.6 l 236.073,0 c 9.72,0 17.6,-7.88 17.6,-17.6 l 0,-24.902 c 10e-4,0 -175.814,35.524 -271.273,-56.743 z"
+         id="path124"
+         inkscape:connector-curvature="0"
+         style="opacity:0.1;fill:#3d5263" /></g><g
+       id="g126" /></g></svg>

二进制
data/web/img/yubi.ico


+ 225 - 0
data/web/inc/footer.inc.php

@@ -0,0 +1,225 @@
+<?php
+include("inc/tfa_modals.php");
+if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admin"):
+?>
+<div id="RestartSOGo" class="modal fade" role="dialog">
+	<div class="modal-dialog">
+		<div class="modal-content">
+		<div class="modal-header">
+			<button type="button" class="close" data-dismiss="modal">&times;</button>
+			<h4 class="modal-title"><?=$lang['footer']['restart_sogo'];?></h4>
+		</div>
+		<div class="modal-body">
+			<p><?=$lang['footer']['restart_sogo_info'];?></p>
+			<hr />
+			<button class="btn btn-md btn-primary" id="triggerRestartSogo"><?=$lang['footer']['restart_now'];?></button>
+			<br /><br />
+			<div id="statusTriggerRestartSogo"></div>
+		</div>
+		</div>
+	</div>
+</div>
+<?php
+endif;
+?>
+<div style="margin-bottom:100px"></div>
+<script src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/js/bootstrap.min.js"></script>
+<script src="/js/bootstrap-switch.min.js"></script>
+<script src="/js/bootstrap-slider.min.js"></script>
+<script src="/js/bootstrap-select.min.js"></script>
+<script src="/js/u2f-api.js"></script>
+<script>
+// Select language and reopen active URL without POST
+function setLang(sel) {
+	$.post( "<?=$_SERVER['REQUEST_URI'];?>", {lang: sel} );
+	window.location.href = window.location.pathname + window.location.search;
+}
+
+$(document).ready(function() {
+  // Confirm TFA modal
+  <?php if (isset($_SESSION['pending_tfa_method'])):?>
+  $('#ConfirmTFAModal').modal({
+    backdrop: 'static',
+    keyboard: false
+  }); 
+  $('#ConfirmTFAModal').on('shown.bs.modal', function(){
+      $(this).find('#token').focus();
+      // If U2F
+      if(document.getElementById("u2f_auth_data") !== null) {
+        $.ajax({
+          type: "GET",
+          cache: false,
+          dataType: 'script',
+          url: "json_api.php",
+          data: {
+            'action':'get_u2f_auth_challenge',
+            'object':'<?=(isset($_SESSION['pending_mailcow_cc_username'])) ? $_SESSION['pending_mailcow_cc_username'] : null;?>',
+          },
+          success: function(data){
+            data;
+          }
+        });
+        setTimeout(function() {
+          console.log("sign: ", req);
+          u2f.sign(req, function(data) {
+            var form = document.getElementById('u2f_auth_form');
+            var auth = document.getElementById('u2f_auth_data');
+            console.log("Authenticate callback", data);
+            auth.value = JSON.stringify(data);
+            form.submit();
+          });
+        }, 1000);
+      }
+  });
+  <?php endif; ?>
+
+  // Set TFA modals
+
+  $('#selectTFA').change(function () {
+    if ($(this).val() == "yubi_otp") {
+      $('#YubiOTPModal').modal('show');
+      $("option:selected").prop("selected", false);
+    }
+    if ($(this).val() == "u2f") {
+      $('#U2FModal').modal('show');
+      $("option:selected").prop("selected", false);
+      $.ajax({
+        type: "GET",
+        cache: false,
+        dataType: 'script',
+        url: "json_api.php",
+        data: {
+          'action':'get_u2f_reg_challenge',
+          'object':'<?=(isset($_SESSION['mailcow_cc_username'])) ? $_SESSION['mailcow_cc_username'] : null;?>',
+        },
+        success: function(data){
+          data;
+        }
+      });
+      setTimeout(function() {
+        console.log("Register: ", req);
+        u2f.register([req], sigs, function(data) {
+          var form  = document.getElementById('u2f_reg_form');
+          var reg   = document.getElementById('u2f_register_data');
+          console.log("Register callback", data);
+          if (data.errorCode && data.errorCode != 0) {
+            var u2f_return_code = document.getElementById('u2f_return_code');
+            u2f_return_code.style.display = u2f_return_code.style.display === 'none' ? '' : null;
+            if (data.errorCode == "4") { data.errorCode = "4 - The presented device is not eligible for this request. For a registration request this may mean that the token is already registered, and for a sign request it may mean that the token does not know the presented key handle"; }
+            u2f_return_code.innerHTML = 'Error code: ' + data.errorCode;
+            return;
+          }
+          reg.value = JSON.stringify(data);
+          form.submit();
+        });
+      }, 1000);
+    }
+    if ($(this).val() == "none") {
+      $('#DisableTFAModal').modal('show');
+      $("option:selected").prop("selected", false);
+    }
+  });
+
+  // Activate tooltips
+  $(function () {
+    $('[data-toggle="tooltip"]').tooltip()
+  })
+	// Hide alerts after n seconds
+	$("#alert-fade").fadeTo(7000, 500).slideUp(500, function(){
+		$("#alert-fade").alert('close');
+	});
+
+	// Remember last navigation pill
+	(function () {
+		'use strict';
+		$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
+			var id = $(this).parents('[role="tablist"]').attr('id');
+			var key = 'lastTag';
+			if (id) {
+				key += ':' + id;
+			}
+			localStorage.setItem(key, $(e.target).attr('href'));
+		});
+		$('[role="tablist"]').each(function (idx, elem) {
+			var id = $(elem).attr('id');
+			var key = 'lastTag';
+			if (id) {
+				key += ':' + id;
+			}
+			var lastTab = localStorage.getItem(key);
+			if (lastTab) {
+				$('[href="' + lastTab + '"]').tab('show');
+			}
+		});
+	})();
+
+	// Disable submit after submitting form
+	$('form').submit(function() {
+		if ($('form button[type="submit"]').data('submitted') == '1') {
+			return false;
+		} else {
+			$(this).find('button[type="submit"]').first().text('<?=$lang['footer']['loading'];?>');
+			$('form button[type="submit"]').attr('data-submitted', '1');
+			function disableF5(e) { if ((e.which || e.keyCode) == 116 || (e.which || e.keyCode) == 82) e.preventDefault(); };
+			$(document).on("keydown", disableF5);
+		}
+	});
+
+	// IE fix to hide scrollbars when table body is empty
+	$('tbody').filter(function (index) {
+		return $(this).children().length < 1;
+	}).remove();
+
+	// Init Bootstrap Selectpicker
+	$('select').selectpicker();
+
+	// Trigger SOGo restart
+	$('#triggerRestartSogo').click(function(){
+		$(this).prop("disabled",true);
+		$(this).html('<span class="glyphicon glyphicon-refresh glyphicon-spin"></span> ');
+		$('#statusTriggerRestartSogo').text('Stopping SOGo workers, this may take a while... ');
+		$.ajax({
+			method: 'get',
+			url: 'call_sogo_ctrl.php',
+			data: {
+				'ajax': true,
+				'ACTION': 'stop'
+			},
+			success: function(data) {
+				$('#statusTriggerRestartSogo').append(data);
+				$('#statusTriggerRestartSogo').append('<br />Starting SOGo... ');
+				$.ajax({
+					method: 'get',
+					url: 'call_sogo_ctrl.php',
+					data: {
+						'ajax': true,
+						'ACTION': 'start'
+					},
+					success: function(data) {
+						$('#statusTriggerRestartSogo').append(data);
+						$('#triggerRestartSogo').html('<span class="glyphicon glyphicon-ok"></span> ');
+					}
+				});
+			}
+		});
+	});
+});
+</script>
+<?php
+if (isset($_SESSION['return'])):
+?>
+<div class="container">
+	<div style="position:fixed;bottom:8px;right:25px;min-width:300px;max-width:350px;z-index:2000">
+		<div <?=($_SESSION['return']['type'] == 'danger') ? null : 'id="alert-fade"'?> class="alert alert-<?=$_SESSION['return']['type'];?>" role="alert">
+		<a href="#" class="close" data-dismiss="alert"> &times;</a>
+		<?=htmlspecialchars($_SESSION['return']['msg']);?>
+		</div>
+	</div>
+</div>
+<?php
+unset($_SESSION['return']);
+endif;
+?>
+</body>
+</html>
+<?php $stmt = null; $pdo = null; ?>

+ 4922 - 0
data/web/inc/functions.inc.php

@@ -0,0 +1,4922 @@
+<?php
+function hash_password($password) {
+	$salt_str = bin2hex(openssl_random_pseudo_bytes(8));
+	return "{SSHA256}".base64_encode(hash('sha256', $password . $salt_str, true) . $salt_str);
+}
+function hasDomainAccess($username, $role, $domain) {
+	global $pdo;
+	if (!filter_var($username, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
+		return false;
+	}
+	if (empty($domain) || !is_valid_domain_name($domain)) {
+		return false;
+	}
+	if ($role != 'admin' && $role != 'domainadmin' && $role != 'user') {
+		return false;
+	}
+	try {
+		$stmt = $pdo->prepare("SELECT `domain` FROM `domain_admins`
+		WHERE (
+			`active`='1'
+			AND `username` = :username
+			AND (`domain` = :domain1 OR `domain` = (SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain2))
+		)
+    OR 'admin' = :role");
+		$stmt->execute(array(':username' => $username, ':domain1' => $domain, ':domain2' => $domain, ':role' => $role));
+		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+	}
+  catch(PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+	if (!empty($num_results)) {
+		return true;
+	}
+	return false;
+}
+function hasMailboxObjectAccess($username, $role, $object) {
+	global $pdo;
+	if (!filter_var($username, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
+		return false;
+	}
+	if ($role != 'admin' && $role != 'domainadmin' && $role != 'user') {
+		return false;
+	}
+	if ($username == $object) {
+		return true;
+	}
+	try {
+		$stmt = $pdo->prepare("SELECT `domain` FROM `mailbox` WHERE `username` = :object");
+		$stmt->execute(array(':object' => $object));
+		$row = $stmt->fetch(PDO::FETCH_ASSOC);
+    if (isset($row['domain']) && hasDomainAccess($username, $role, $row['domain'])) {
+      return true;
+    }
+	}
+  catch(PDOException $e) {
+		error_log($e);
+		return false;
+	}
+	return false;
+}
+function init_db_schema() {
+  // This will be much better in future releases...
+	global $pdo;
+	try {
+		$stmt = $pdo->prepare("SELECT NULL FROM `admin`, `imapsync`, `tfa`");
+		$stmt->execute();
+	}
+	catch (Exception $e) {
+		$lines = file('/web/inc/init.sql');
+		$data = '';
+		foreach ($lines as $line) {
+			if (substr($line, 0, 2) == '--' || $line == '') {
+				continue;
+			}
+			$data .= $line;
+			if (substr(trim($line), -1, 1) == ';') {
+				$pdo->query($data);
+				$data = '';
+			}
+		}
+    // Create index if not exists
+		$stmt = $pdo->query("SHOW INDEX FROM sogo_acl WHERE KEY_NAME = 'sogo_acl_c_folder_id_idx'");
+		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+		if ($num_results == 0) {
+			$pdo->query("CREATE INDEX sogo_acl_c_folder_id_idx ON sogo_acl(c_folder_id)");
+		}
+		$stmt = $pdo->query("SHOW INDEX FROM sogo_acl WHERE KEY_NAME = 'sogo_acl_c_uid_idx'");
+		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+		if ($num_results == 0) {
+			$pdo->query("CREATE INDEX sogo_acl_c_uid_idx ON sogo_acl(c_uid)");
+		}
+		$_SESSION['return'] = array(
+			'type' => 'success',
+			'msg' => 'Database initialization completed.'
+		);
+	}
+  // Add newly added columns
+  $stmt = $pdo->query("SHOW COLUMNS FROM `mailbox` LIKE 'kind'");
+  $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+  if ($num_results == 0) {
+    $pdo->query("ALTER TABLE `mailbox` ADD `kind` VARCHAR(100) NOT NULL DEFAULT ''");
+  }
+  $stmt = $pdo->query("SHOW COLUMNS FROM `mailbox` LIKE 'multiple_bookings'");
+  $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+  if ($num_results == 0) {
+    $pdo->query("ALTER TABLE `mailbox` ADD `multiple_bookings` tinyint(1) NOT NULL DEFAULT '0'");
+  }
+  $stmt = $pdo->query("SHOW COLUMNS FROM `mailbox` LIKE 'wants_tagged_subject'");
+  $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+  if ($num_results == 0) {
+    $pdo->query("ALTER TABLE `mailbox` ADD `wants_tagged_subject` tinyint(1) NOT NULL DEFAULT '0'");
+  }
+  $stmt = $pdo->query("SHOW COLUMNS FROM `tfa` LIKE 'key_id'");
+  $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+  if ($num_results == 0) {
+    $pdo->query("ALTER TABLE `tfa` ADD `key_id` VARCHAR(255) DEFAULT 'unidentified'");
+  }
+}
+function verify_ssha256($hash, $password) {
+	// Remove tag if any
+	$hash = ltrim($hash, '{SSHA256}');
+	// Decode hash
+	$dhash = base64_decode($hash);
+	// Get first 32 bytes of binary which equals a SHA256 hash
+	$ohash = substr($dhash, 0, 32);
+	// Remove SHA256 hash from decoded hash to get original salt string
+	$osalt = str_replace($ohash, '', $dhash);
+	// Check single salted SHA256 hash against extracted hash
+	if (hash('sha256', $password . $osalt, true) == $ohash) {
+		return true;
+	}
+	else {
+		return false;
+	}
+}
+function doveadm_authenticate($hash, $algorithm, $password) {
+	$descr = array(0 => array('pipe', 'r'), 1 => array('pipe', 'w'), 2 => array('pipe', 'w'));
+	$pipes = array();
+	$process = proc_open("/usr/bin/doveadm pw -s ".$algorithm." -t '".$hash."'", $descr, $pipes);
+	if (is_resource($process)) {
+		fputs($pipes[0], $password);
+		fclose($pipes[0]);
+		while ($f = fgets($pipes[1])) {
+			if (preg_match('/(verified)/', $f)) {
+				proc_close($process);
+				return true;
+			}
+			return false;
+		}
+		fclose($pipes[1]);
+		while ($f = fgets($pipes[2])) {
+			proc_close($process);
+			return false;
+		}
+		fclose($pipes[2]);
+		proc_close($process);
+	}
+	return false;
+}
+function check_login($user, $pass) {
+	global $pdo;
+	if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
+		return false;
+	}
+	$user = strtolower(trim($user));
+	$stmt = $pdo->prepare("SELECT `password` FROM `admin`
+			WHERE `superadmin` = '1'
+			AND `username` = :user");
+	$stmt->execute(array(':user' => $user));
+	$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+	foreach ($rows as $row) {
+		if (verify_ssha256($row['password'], $pass)) {
+      if (get_tfa($user)['name'] != "none") {
+        $_SESSION['pending_mailcow_cc_username'] = $user;
+        $_SESSION['pending_mailcow_cc_role'] = "admin";
+        $_SESSION['pending_tfa_method'] = get_tfa($user)['name'];
+        unset($_SESSION['ldelay']);
+        return "pending";
+      }
+      else {
+        unset($_SESSION['ldelay']);
+        return "admin";
+      }
+		}
+	}
+	$stmt = $pdo->prepare("SELECT `password` FROM `admin`
+			WHERE `superadmin` = '0'
+			AND `active`='1'
+			AND `username` = :user");
+	$stmt->execute(array(':user' => $user));
+	$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+	foreach ($rows as $row) {
+		if (verify_ssha256($row['password'], $pass) !== false) {
+      if (get_tfa($user)['name'] != "none") {
+        $_SESSION['pending_mailcow_cc_username'] = $user;
+        $_SESSION['pending_mailcow_cc_role'] = "domainadmin";
+        $_SESSION['pending_tfa_method'] = get_tfa($user)['name'];
+        unset($_SESSION['ldelay']);
+        return "pending";
+      }
+      else {
+        unset($_SESSION['ldelay']);
+        $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
+        $stmt->execute(array(':user' => $user));
+        return "domainadmin";
+      }
+		}
+	}
+	$stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
+			WHERE `kind` NOT REGEXP 'location|thing|group'
+        AND `active`='1'
+        AND `username` = :user");
+	$stmt->execute(array(':user' => $user));
+	$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+	foreach ($rows as $row) {
+		if (verify_ssha256($row['password'], $pass) !== false) {
+			unset($_SESSION['ldelay']);
+			return "user";
+		}
+	}
+	if (!isset($_SESSION['ldelay'])) {
+		$_SESSION['ldelay'] = "0";
+	}
+	elseif (!isset($_SESSION['mailcow_cc_username'])) {
+		$_SESSION['ldelay'] = $_SESSION['ldelay']+0.5;
+	}
+	sleep($_SESSION['ldelay']);
+}
+function formatBytes($size, $precision = 2) {
+	if(!is_numeric($size)) {
+		return "0";
+	}
+	$base = log($size, 1024);
+	$suffixes = array(' Byte', ' KiB', ' MiB', ' GiB', ' TiB');
+	if ($size == "0") {
+		return "0";
+	}
+	return round(pow(1024, $base - floor($base)), $precision) . $suffixes[floor($base)];
+}
+function edit_admin_account($postarray) {
+	global $lang;
+	global $pdo;
+	if ($_SESSION['mailcow_cc_role'] != "admin") {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+	$username       = $postarray['admin_user'];
+	$username_now   = $_SESSION['mailcow_cc_username'];
+
+	if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username)) || empty ($username)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['username_invalid'])
+		);
+		return false;
+	}
+
+	if (!empty($postarray['admin_pass']) && !empty($postarray['admin_pass2'])) {
+		if ($postarray['admin_pass'] != $postarray['admin_pass2']) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => sprintf($lang['danger']['password_mismatch'])
+			);
+			return false;
+		}
+		$password_hashed = hash_password($postarray['admin_pass']);
+		try {
+			$stmt = $pdo->prepare("UPDATE `admin` SET 
+				`modified` = :modified,
+				`password` = :password_hashed,
+				`username` = :username1
+					WHERE `username` = :username2");
+			$stmt->execute(array(
+				':password_hashed' => $password_hashed,
+				':modified' => date('Y-m-d H:i:s'),
+				':username1' => $username,
+				':username2' => $username_now
+			));
+		}
+		catch (PDOException $e) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => 'MySQL: '.$e
+			);
+			return false;
+		}
+	}
+	else {
+		try {
+			$stmt = $pdo->prepare("UPDATE `admin` SET 
+				`modified` = :modified,
+				`username` = :username1
+					WHERE `username` = :username2");
+			$stmt->execute(array(
+				':username1' => $username,
+				':modified' => date('Y-m-d H:i:s'),
+				':username2' => $username_now
+			));
+		}
+		catch (PDOException $e) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => 'MySQL: '.$e
+			);
+			return false;
+		}
+	}
+	try {
+		$stmt = $pdo->prepare("UPDATE `domain_admins` SET `domain` = 'ALL', `username` = :username1 WHERE `username` = :username2");
+		$stmt->execute(array(':username1' => $username, ':username2' => $username_now));
+		$stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username1 WHERE `username` = :username2");
+		$stmt->execute(array(':username1' => $username, ':username2' => $username_now));
+	}
+	catch (PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+  $_SESSION['mailcow_cc_username'] = $username;
+	$_SESSION['return'] = array(
+		'type' => 'success',
+		'msg' => sprintf($lang['success']['admin_modified'])
+	);
+}
+function set_time_limited_aliases($postarray) {
+	global $lang;
+	global $pdo;
+  (isset($postarray['username'])) ? $username = $postarray['username'] : $username = $_SESSION['mailcow_cc_username'];
+
+  if ($_SESSION['mailcow_cc_role'] != "user" &&
+    $_SESSION['mailcow_cc_role'] != "admin") {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+  }
+  if (filter_var($username, FILTER_VALIDATE_EMAIL)) {
+    if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+  }
+
+	try {
+    $stmt = $pdo->prepare("SELECT `domain` FROM `mailbox` WHERE `username` = :username");
+    $stmt->execute(array(':username' => $username));
+    $domain = $stmt->fetch(PDO::FETCH_ASSOC)['domain'];
+  }
+  catch (PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+
+	switch ($postarray["set_time_limited_aliases"]) {
+		case "generate":
+			if (!is_numeric($postarray["validity"]) || $postarray["validity"] > 672) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => sprintf($lang['danger']['validity_missing'])
+				);
+				return false;
+			}
+			$validity = strtotime("+".$postarray["validity"]." hour"); 
+			$letters = 'abcefghijklmnopqrstuvwxyz1234567890';
+			$random_name = substr(str_shuffle($letters), 0, 24);
+			try {
+				$stmt = $pdo->prepare("INSERT INTO `spamalias` (`address`, `goto`, `validity`) VALUES
+					(:address, :goto, :validity)");
+				$stmt->execute(array(
+					':address' => $random_name . '@' . $domain,
+					':goto' => $username,
+					':validity' => $validity
+				));
+			}
+			catch (PDOException $e) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => 'MySQL: '.$e
+				);
+				return false;
+			}
+			$_SESSION['return'] = array(
+				'type' => 'success',
+				'msg' => sprintf($lang['success']['mailbox_modified'], htmlspecialchars($username))
+			);
+		break;
+		case "deleteall":
+			try {
+				$stmt = $pdo->prepare("DELETE FROM `spamalias` WHERE `goto` = :username");
+				$stmt->execute(array(
+					':username' => $username
+				));
+			}
+			catch (PDOException $e) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => 'MySQL: '.$e
+				);
+				return false;
+			}	
+			$_SESSION['return'] = array(
+				'type' => 'success',
+				'msg' => sprintf($lang['success']['mailbox_modified'], htmlspecialchars($username))
+			);
+		break;
+		case "delete":
+			if (empty($postarray['item']) || !filter_var($postarray['item'], FILTER_VALIDATE_EMAIL)) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => sprintf($lang['danger']['access_denied'])
+				);
+				return false;
+			}
+      $item	= $postarray['item'];
+			try {
+				$stmt = $pdo->prepare("DELETE FROM `spamalias` WHERE `goto` = :username AND `address` = :item");
+				$stmt->execute(array(
+					':username' => $username,
+					':item' => $item
+				));
+			}
+			catch (PDOException $e) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => 'MySQL: '.$e
+				);
+				return false;
+			}	
+			$_SESSION['return'] = array(
+				'type' => 'success',
+				'msg' => sprintf($lang['success']['mailbox_modified'], htmlspecialchars($username))
+			);
+		break;
+		case "extendall":
+			try {
+				$stmt = $pdo->prepare("UPDATE `spamalias` SET `validity` = (`validity` + 3600) WHERE
+          `goto` = :username AND
+					`validity` >= :validity");
+				$stmt->execute(array(
+					':username' => $username,
+					':validity' => time(),
+				));
+			}
+			catch (PDOException $e) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => 'MySQL: '.$e
+				);
+				return false;
+			}
+			$_SESSION['return'] = array(
+				'type' => 'success',
+				'msg' => sprintf($lang['success']['mailbox_modified'], htmlspecialchars($username))
+			);
+		break;
+		case "extend":
+			if (empty($postarray['item']) || !filter_var($postarray['item'], FILTER_VALIDATE_EMAIL)) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => sprintf($lang['danger']['access_denied'])
+				);
+				return false;
+			}
+      $item	= $postarray['item'];
+			try {
+				$stmt = $pdo->prepare("UPDATE `spamalias` SET `validity` = (`validity` + 3600) WHERE 
+          `goto` = :username AND
+					`address` = :item AND
+					`validity` >= :validity");
+				$stmt->execute(array(
+					':username' => $username,
+					':item' => $item,
+					':validity' => time(),
+				));
+			}
+			catch (PDOException $e) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => 'MySQL: '.$e
+				);
+				return false;
+			}
+			$_SESSION['return'] = array(
+				'type' => 'success',
+				'msg' => sprintf($lang['success']['mailbox_modified'], htmlspecialchars($username))
+			);
+		break;
+	}
+}
+function get_time_limited_aliases($username = null) {
+  // 'username' can be be set, if not, default to mailcow_cc_username
+  global $lang;
+	global $pdo;
+  $data = array();
+  if (isset($username) && filter_var($username, FILTER_VALIDATE_EMAIL)) {
+    if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+  }
+  else {
+    $username = $_SESSION['mailcow_cc_username'];
+  }
+  try {
+    $stmt = $pdo->prepare("SELECT `address`,
+      `goto`,
+      `validity`
+        FROM `spamalias`
+          WHERE `goto` = :username
+            AND `validity` >= :unixnow");
+    $stmt->execute(array(':username' => $username, ':unixnow' => time()));
+    $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
+  }
+  catch(PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+  }
+  return $data;
+}
+function edit_user_account($postarray) {
+	global $lang;
+	global $pdo;
+  if (isset($postarray['username']) && filter_var($postarray['username'], FILTER_VALIDATE_EMAIL)) {
+    if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $postarray['username'])) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+    else {
+      $username = $postarray['username'];
+    }
+  }
+  else {
+    $username = $_SESSION['mailcow_cc_username'];
+  }
+	$password_old		= $postarray['user_old_pass'];
+
+	if (isset($postarray['user_new_pass']) && isset($postarray['user_new_pass2'])) {
+		$password_new	= $postarray['user_new_pass'];
+		$password_new2	= $postarray['user_new_pass2'];
+	}
+
+	$stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
+			WHERE `kind` NOT REGEXP 'location|thing|group'
+        AND `username` = :user");
+	$stmt->execute(array(':user' => $username));
+	$row = $stmt->fetch(PDO::FETCH_ASSOC);
+  if (!verify_ssha256($row['password'], $password_old)) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+
+	if (isset($password_new) && isset($password_new2)) {
+		if (!empty($password_new2) && !empty($password_new)) {
+			if ($password_new2 != $password_new) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => sprintf($lang['danger']['password_mismatch'])
+				);
+				return false;
+			}
+			if (strlen($password_new) < "6" ||
+				!preg_match('/[A-Za-z]/', $password_new) ||
+				!preg_match('/[0-9]/', $password_new)) {
+					$_SESSION['return'] = array(
+						'type' => 'danger',
+						'msg' => sprintf($lang['danger']['password_complexity'])
+					);
+					return false;
+			}
+			$password_hashed = hash_password($password_new);
+			try {
+				$stmt = $pdo->prepare("UPDATE `mailbox` SET `modified` = :modified, `password` = :password_hashed WHERE `username` = :username");
+				$stmt->execute(array(
+					':password_hashed' => $password_hashed,
+					':modified' => date('Y-m-d H:i:s'),
+					':username' => $username
+				));
+			}
+			catch (PDOException $e) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => 'MySQL: '.$e
+				);
+				return false;
+			}
+		}
+	}
+	$_SESSION['return'] = array(
+		'type' => 'success',
+		'msg' => sprintf($lang['success']['mailbox_modified'], htmlspecialchars($username))
+	);
+}
+function get_spam_score($username = null) {
+	global $pdo;
+	$default = "5, 15";
+  if (isset($username) && filter_var($username, FILTER_VALIDATE_EMAIL)) {
+    if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
+      return false;
+    }
+  }
+  else {
+    $username = $_SESSION['mailcow_cc_username'];
+  }
+	try {
+		$stmt = $pdo->prepare("SELECT `value` FROM `filterconf` WHERE `object` = :username AND
+			(`option` = 'lowspamlevel' OR `option` = 'highspamlevel')");
+		$stmt->execute(array(':username' => $username));
+		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+	}
+	catch(PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	if (empty($num_results)) {
+		return $default;
+	}
+	else {
+		try {
+			$stmt = $pdo->prepare("SELECT `value` FROM `filterconf` WHERE `option` = 'highspamlevel' AND `object` = :username");
+			$stmt->execute(array(':username' => $username));
+			$highspamlevel = $stmt->fetch(PDO::FETCH_ASSOC);
+
+			$stmt = $pdo->prepare("SELECT `value` FROM `filterconf` WHERE `option` = 'lowspamlevel' AND `object` = :username");
+			$stmt->execute(array(':username' => $username));
+			$lowspamlevel = $stmt->fetch(PDO::FETCH_ASSOC);
+
+			return $lowspamlevel['value'].', '.$highspamlevel['value'];
+		}
+		catch(PDOException $e) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => 'MySQL: '.$e
+			);
+			return false;
+		}
+	}
+}
+function edit_spam_score($postarray) {
+  // Array items
+  // 'username' can be set, defaults to mailcow_cc_username
+  // 'lowspamlevel'
+  // 'highspamlevel'
+	global $lang;
+	global $pdo;
+  if (isset($postarray['username']) && filter_var($postarray['username'], FILTER_VALIDATE_EMAIL)) {
+    if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $postarray['username'])) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+    else {
+      $username = $postarray['username'];
+    }
+  }
+  else {
+    $username = $_SESSION['mailcow_cc_username'];
+  }
+	$lowspamlevel	= explode(',', $postarray['score'])[0];
+	$highspamlevel	= explode(',', $postarray['score'])[1];
+
+	if (!is_numeric($lowspamlevel) || !is_numeric($highspamlevel)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+	try {
+		$stmt = $pdo->prepare("DELETE FROM `filterconf` WHERE `object` = :username
+			AND (`option` = 'lowspamlevel' OR `option` = 'highspamlevel')");
+		$stmt->execute(array(
+			':username' => $username
+		));
+
+		$stmt = $pdo->prepare("INSERT INTO `filterconf` (`object`, `option`, `value`)
+			VALUES (:username, 'highspamlevel', :highspamlevel)");
+		$stmt->execute(array(
+			':username' => $username,
+			':highspamlevel' => $highspamlevel
+		));
+
+		$stmt = $pdo->prepare("INSERT INTO `filterconf` (`object`, `option`, `value`)
+			VALUES (:username, 'lowspamlevel', :lowspamlevel)");
+		$stmt->execute(array(
+			':username' => $username,
+			':lowspamlevel' => $lowspamlevel
+		));
+	}
+	catch (PDOException $e) {
+		$stmt = $pdo->prepare("DELETE FROM `filterconf` WHERE `object` = :username
+			AND (`option` = 'lowspamlevel' OR `option` = 'highspamlevel')");
+		$stmt->execute(array(
+			':username' => $username
+		));
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	$_SESSION['return'] = array(
+		'type' => 'success',
+		'msg' => sprintf($lang['success']['mailbox_modified'], $username)
+	);
+}
+function get_policy_list($object = null) {
+  // 'object' can be be set, if not, default to mailcow_cc_username
+	global $lang;
+	global $pdo;
+  if (isset($object)) {
+    if (!filter_var($object, FILTER_VALIDATE_EMAIL) && is_valid_domain_name($object)) {
+      $object = idn_to_ascii(strtolower(trim($object)));
+      if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $object)) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => sprintf($lang['danger']['access_denied'])
+        );
+        return false;
+      }
+    }
+    elseif (filter_var($object, FILTER_VALIDATE_EMAIL)) {
+      if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $object)) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => sprintf($lang['danger']['access_denied'])
+        );
+        return false;
+      }
+    }
+  }
+  else {
+     $object = $_SESSION['mailcow_cc_username'];
+  }
+  try {
+    // WHITELIST
+    $stmt = $pdo->prepare("SELECT `object`, `value`, `prefid` FROM `filterconf` WHERE `option`='whitelist_from' AND (`object` = :username OR `object` = SUBSTRING_INDEX(:username_domain, '@' ,-1))");
+    $stmt->execute(array(':username' => $object, ':username_domain' => $object));
+    $rows['whitelist'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    // BLACKLIST
+    $stmt = $pdo->prepare("SELECT `object`, `value`, `prefid` FROM `filterconf` WHERE `option`='blacklist_from' AND (`object` = :username OR `object` = SUBSTRING_INDEX(:username_domain, '@' ,-1))");
+    $stmt->execute(array(':username' => $object, ':username_domain' => $object));
+    $rows['blacklist'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
+  }
+  catch(PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+  }
+  return $rows;
+}
+function add_policy_list_item($postarray) {
+  // Array data
+  // Either 'domain' or 'username' can be be set
+  // If none of the above is set, default to mailcow_cc_username
+  //
+  // If 'delete_prefid' then delete item id
+	global $lang;
+	global $pdo;
+  (isset($postarray['username'])) ? $object = $postarray['username'] : null;
+  (isset($postarray['domain']))   ? $object = $postarray['domain'] : null;
+  (!isset($object))               ? $object = $_SESSION['mailcow_cc_username'] : null;
+
+  if (is_valid_domain_name($object)) {
+		if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $object)) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => sprintf($lang['danger']['access_denied'])
+			);
+			return false;
+		}
+    $object = idn_to_ascii(strtolower(trim($object)));
+  }
+  else {
+		if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $object)) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => sprintf($lang['danger']['access_denied'])
+			);
+			return false;
+		}
+  }
+
+	($postarray['object_list'] == "bl") ? $object_list = "blacklist_from" : null;
+	($postarray['object_list'] == "wl") ? $object_list = "whitelist_from" : null;
+	$object_from = preg_replace('/\.+/', '.', rtrim(preg_replace("/\.\*/", "*", trim(strtolower($postarray['object_from']))), '.'));
+  if (!ctype_alnum(str_replace(array('@', '.', '-', '*'), '', $object_from))) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['policy_list_from_invalid'])
+		);
+		return false;
+	}
+	if ($object_list != "blacklist_from" && $object_list != "whitelist_from") {
+    $_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+	try {
+		$stmt = $pdo->prepare("SELECT `object` FROM `filterconf`
+			WHERE (`option` = 'whitelist_from'  OR `option` = 'blacklist_from')
+				AND `object` = :object
+				AND `value` = :object_from");
+		$stmt->execute(array(':object' => $object, ':object_from' => $object_from));
+		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+    if ($num_results != 0) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['policy_list_from_exists'])
+      );
+      return false;
+    }
+	}
+	catch(PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("INSERT INTO `filterconf` (`object`, `option` ,`value`)
+			VALUES (:object, :object_list, :object_from)");
+		$stmt->execute(array(
+			':object' => $object,
+			':object_list' => $object_list,
+			':object_from' => $object_from
+		));
+	}
+	catch (PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	$_SESSION['return'] = array(
+		'type' => 'success',
+		'msg' => sprintf($lang['success']['object_modified'], $object)
+	);
+}
+function delete_policy_list_item($postarray) {
+  // Array data
+  // Either 'domain' or 'username' can be be set
+  // If none of the above is set, default to mailcow_cc_username
+  //
+  // 'delete_prefid' is item to be deleted
+	global $lang;
+	global $pdo;
+  (isset($postarray['username'])) ? $object = $postarray['username'] : null;
+  (isset($postarray['domain']))   ? $object = $postarray['domain'] : null;
+  (!isset($object))               ? $object = $_SESSION['mailcow_cc_username'] : null;
+
+  if (is_valid_domain_name($object)) {
+		if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $object)) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => sprintf($lang['danger']['access_denied'])
+			);
+			return false;
+		}
+    $object = idn_to_ascii(strtolower(trim($object)));
+  }
+  else {
+    if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $object)) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+  }
+
+  if (!is_numeric($postarray['delete_prefid'])) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+
+  try {
+    $stmt = $pdo->prepare("DELETE FROM `filterconf` WHERE `object` = :object AND `prefid` = :prefid");
+    $stmt->execute(array(
+      ':object' => $object,
+      ':prefid' => $postarray['delete_prefid']
+    ));
+  }
+  catch (PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+  $_SESSION['return'] = array(
+    'type' => 'success',
+    'msg' => sprintf($lang['success']['object_modified'], $object)
+  );
+  return true;
+}
+function get_syncjobs($username = null) {
+  // 'username' can be be set, if not, default to mailcow_cc_username
+	global $lang;
+	global $pdo;
+  $data = array();
+  if (isset($username) && filter_var($username, FILTER_VALIDATE_EMAIL)) {
+    if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+  }
+  else {
+    $username = $_SESSION['mailcow_cc_username'];
+  }
+  try {
+    $stmt = $pdo->prepare("SELECT *, CONCAT(LEFT(`password1`, 3), '…') as `password1_short`
+        FROM `imapsync`
+          WHERE `user2` = :username");
+    $stmt->execute(array(':username' => $username));
+    $data = $stmt->fetchAll(PDO::FETCH_ASSOC);
+  }
+  catch(PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+  }
+  return $data;
+}
+function get_syncjob_details($id) {
+	global $lang;
+	global $pdo;
+  $syncjobdetails = array();
+	if ($_SESSION['mailcow_cc_role'] != "user" &&
+		$_SESSION['mailcow_cc_role'] != "admin") {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => sprintf($lang['danger']['access_denied'])
+			);
+			return false;
+	}
+  if (!is_numeric($id)) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  try {
+    $stmt = $pdo->prepare("SELECT * FROM `imapsync` WHERE (`user2` = :username OR 'admin' = :role) AND id = :id");
+    $stmt->execute(array(':id' => $id, ':role' => $_SESSION['mailcow_cc_role'], ':username' => $_SESSION['mailcow_cc_username']));
+    $syncjobdetails = $stmt->fetch(PDO::FETCH_ASSOC);
+  }
+  catch(PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+  }
+  return $syncjobdetails;
+}
+function delete_syncjob($postarray) {
+  // Array items
+  // 'username' can be set, defaults to mailcow_cc_username
+	global $lang;
+	global $pdo;
+  if (isset($postarray['username']) && filter_var($postarray['username'], FILTER_VALIDATE_EMAIL)) {
+    if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $postarray['username'])) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+    else {
+      $username = $postarray['username'];
+    }
+  }
+  else {
+    $username = $_SESSION['mailcow_cc_username'];
+  }
+  $id = $postarray['id'];
+  if (!is_numeric($id)) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  try {
+    $stmt = $pdo->prepare("DELETE FROM `imapsync` WHERE `user2` = :username AND `id`= :id");
+    $stmt->execute(array(
+      ':username' => $username,
+      ':id' => $id,
+    ));
+  }
+  catch (PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+  $_SESSION['return'] = array(
+    'type' => 'success',
+    'msg' => sprintf($lang['success']['mailbox_modified'], htmlspecialchars($username))
+  );
+  return true;
+}
+function add_syncjob($postarray) {
+  // Array items
+  // 'username' can be set, defaults to mailcow_cc_username
+	global $lang;
+	global $pdo;
+  if (isset($postarray['username']) && filter_var($postarray['username'], FILTER_VALIDATE_EMAIL)) {
+    if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $postarray['username'])) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+    else {
+      $username = $postarray['username'];
+    }
+  }
+  else {
+    $username = $_SESSION['mailcow_cc_username'];
+  }
+  isset($postarray['active']) ? $active = '1' : $active = '0';
+  isset($postarray['delete2duplicates']) ? $delete2duplicates = '1' : $delete2duplicates = '0';
+  $port1            = $postarray['port1'];
+  $host1            = $postarray['host1'];
+  $password1        = $postarray['password1'];
+  $exclude          = $postarray['exclude'];
+  $maxage           = $postarray['maxage'];
+  $subfolder2       = $postarray['subfolder2'];
+  $user1            = $postarray['user1'];
+  $mins_interval    = $postarray['mins_interval'];
+  $enc1             = $postarray['enc1'];
+
+  if (empty($subfolder2)) {
+    $subfolder2 = "";
+  }
+  if (!isset($maxage) || !filter_var($maxage, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32767)))) {
+    $maxage = "0";
+  }
+  if (!filter_var($port1, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 65535)))) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  if (!filter_var($mins_interval, FILTER_VALIDATE_INT, array('options' => array('min_range' => 10, 'max_range' => 3600)))) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  if (!is_valid_domain_name($host1)) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  if ($enc1 != "TLS" && $enc1 != "SSL" && $enc1 != "PLAIN") {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  if (@preg_match("/" . $exclude . "/", null) === false) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  try {
+    $stmt = $pdo->prepare("SELECT `user2`, `user1` FROM `imapsync`
+      WHERE `user2` = :user2 AND `user1` = :user1");
+    $stmt->execute(array(':user1' => $user1, ':user2' => $username));
+    $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+  }
+  catch(PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+  if ($num_results != 0) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['object_exists'], htmlspecialchars($host1 . ' / ' . $user1))
+    );
+    return false;
+  }
+  try {
+    $stmt = $pdo->prepare("INSERT INTO `imapsync` (`user2`, `exclude`, `maxage`, `subfolder2`, `host1`, `authmech1`, `user1`, `password1`, `mins_interval`, `port1`, `enc1`, `delete2duplicates`, `active`)
+      VALUES (:user2, :exclude, :maxage, :subfolder2, :host1, :authmech1, :user1, :password1, :mins_interval, :port1, :enc1, :delete2duplicates, :active)");
+    $stmt->execute(array(
+      ':user2' => $username,
+      ':exclude' => $exclude,
+      ':maxage' => $maxage,
+      ':subfolder2' => $subfolder2,
+      ':host1' => $host1,
+      ':authmech1' => 'PLAIN',
+      ':user1' => $user1,
+      ':password1' => $password1,
+      ':mins_interval' => $mins_interval,
+      ':port1' => $port1,
+      ':enc1' => $enc1,
+      ':delete2duplicates' => $delete2duplicates,
+      ':active' => $active,
+    ));
+  }
+  catch(PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+  $_SESSION['return'] = array(
+    'type' => 'success',
+    'msg' => sprintf($lang['success']['mailbox_modified'], $username)
+  );
+  return true;
+}
+function edit_syncjob($postarray) {
+  // Array items
+  // 'username' can be set, defaults to mailcow_cc_username
+	global $lang;
+	global $pdo;
+  if (isset($postarray['username']) && filter_var($postarray['username'], FILTER_VALIDATE_EMAIL)) {
+    if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $postarray['username'])) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+    else {
+      $username = $postarray['username'];
+    }
+  }
+  else {
+    $username = $_SESSION['mailcow_cc_username'];
+  }
+  isset($postarray['active']) ? $active = '1' : $active = '0';
+  isset($postarray['delete2duplicates']) ? $delete2duplicates = '1' : $delete2duplicates = '0';
+  $id               = $postarray['id'];
+  $port1            = $postarray['port1'];
+  $host1            = $postarray['host1'];
+  $password1        = $postarray['password1'];
+  $exclude          = $postarray['exclude'];
+  $maxage           = $postarray['maxage'];
+  $subfolder2       = $postarray['subfolder2'];
+  $user1            = $postarray['user1'];
+  $mins_interval    = $postarray['mins_interval'];
+  $enc1             = $postarray['enc1'];
+
+  if (empty($subfolder2)) {
+    $subfolder2 = "";
+  }
+  if (!isset($maxage) || !filter_var($maxage, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32767)))) {
+    $maxage = "0";
+  }
+  if (!filter_var($port1, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 65535)))) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  if (!filter_var($mins_interval, FILTER_VALIDATE_INT, array('options' => array('min_range' => 10, 'max_range' => 3600)))) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  if (!is_valid_domain_name($host1)) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  if ($enc1 != "TLS" && $enc1 != "SSL" && $enc1 != "PLAIN") {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  if (@preg_match("/" . $exclude . "/", null) === false) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  try {
+    $stmt = $pdo->prepare("SELECT `user2` FROM `imapsync`
+      WHERE `user2` = :user2 AND `id` = :id");
+    $stmt->execute(array(':user2' => $username, ':id' => $id));
+    $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+  }
+  catch(PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+  if (empty($num_results)) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  try {
+    $stmt = $pdo->prepare("UPDATE `imapsync` set `maxage` = :maxage, `subfolder2` = :subfolder2, `exclude` = :exclude, `host1` = :host1, `user1` = :user1, `password1` = :password1, `mins_interval` = :mins_interval, `port1` = :port1, `enc1` = :enc1, `delete2duplicates` = :delete2duplicates, `active` = :active
+      WHERE `user2` = :user2 AND `id` = :id");
+    $stmt->execute(array(
+      ':user2' => $username,
+      ':id' => $id,
+      ':exclude' => $exclude,
+      ':maxage' => $maxage,
+      ':subfolder2' => $subfolder2,
+      ':host1' => $host1,
+      ':user1' => $user1,
+      ':password1' => $password1,
+      ':mins_interval' => $mins_interval,
+      ':port1' => $port1,
+      ':enc1' => $enc1,
+      ':delete2duplicates' => $delete2duplicates,
+      ':active' => $active,
+    ));
+  }
+  catch(PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+  $_SESSION['return'] = array(
+    'type' => 'success',
+    'msg' => sprintf($lang['success']['mailbox_modified'], $username)
+  );
+  return true;
+}
+function edit_tls_policy($postarray) {
+	global $lang;
+	global $pdo;
+  if (isset($postarray['username']) && filter_var($postarray['username'], FILTER_VALIDATE_EMAIL)) {
+    if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $postarray['username'])) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+    else {
+      $username = $postarray['username'];
+    }
+  }
+  else {
+    $username = $_SESSION['mailcow_cc_username'];
+  }
+	isset($postarray['tls_in']) ? $tls_in = '1' : $tls_in = '0';
+	isset($postarray['tls_out']) ? $tls_out = '1' : $tls_out = '0';
+	$username = $_SESSION['mailcow_cc_username'];
+	if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['username_invalid'])
+		);
+		return false;
+	}
+	try {
+		$stmt = $pdo->prepare("UPDATE `mailbox` SET `tls_enforce_out` = :tls_out, `tls_enforce_in` = :tls_in WHERE `username` = :username");
+		$stmt->execute(array(
+			':tls_out' => $tls_out,
+			':tls_in' => $tls_in,
+			':username' => $username
+		));
+	}
+	catch (PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	$_SESSION['return'] = array(
+		'type' => 'success',
+		'msg' => sprintf($lang['success']['mailbox_modified'], $username)
+	);
+}
+function get_tls_policy($username = null) {
+	global $lang;
+	global $pdo;
+  $data = array();
+  if (isset($username) && filter_var($username, FILTER_VALIDATE_EMAIL)) {
+    if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+  }
+  else {
+    $username = $_SESSION['mailcow_cc_username'];
+  }
+	try {
+		$stmt = $pdo->prepare("SELECT `tls_enforce_out`, `tls_enforce_in` FROM `mailbox` WHERE `username` = :username");
+		$stmt->execute(array(':username' => $username));
+		$data = $stmt->fetch(PDO::FETCH_ASSOC);
+	}
+	catch(PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	return $data;
+}
+function edit_delimiter_action($postarray) {
+  // Array items
+  // 'username' can be set, defaults to mailcow_cc_username
+	global $lang;
+	global $pdo;
+  if (isset($postarray['username']) && filter_var($postarray['username'], FILTER_VALIDATE_EMAIL)) {
+    if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $postarray['username'])) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+    else {
+      $username = $postarray['username'];
+    }
+  }
+  else {
+    $username = $_SESSION['mailcow_cc_username'];
+  }
+  ($postarray['tagged_mail_handler'] == "subject") ? $wants_tagged_subject = '1' : $wants_tagged_subject = '0';
+  if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['username_invalid'])
+    );
+    return false;
+  }
+  try {
+    $stmt = $pdo->prepare("UPDATE `mailbox` SET `wants_tagged_subject` = :wants_tagged_subject WHERE `username` = :username");
+    $stmt->execute(array(':username' => $username, ':wants_tagged_subject' => $wants_tagged_subject));
+    $SelectData = $stmt->fetch(PDO::FETCH_ASSOC);
+  }
+  catch(PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+  $_SESSION['return'] = array(
+    'type' => 'success',
+    'msg' => sprintf($lang['success']['mailbox_modified'], $username)
+  );
+  return true;
+}
+function get_delimiter_action($username = null) {
+  // 'username' can be set, defaults to mailcow_cc_username
+	global $lang;
+	global $pdo;
+	$data = array();
+  if (isset($username) && filter_var($username, FILTER_VALIDATE_EMAIL)) {
+    if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
+      return false;
+    }
+  }
+  else {
+    $username = $_SESSION['mailcow_cc_username'];
+  }
+  try {
+    $stmt = $pdo->prepare("SELECT `wants_tagged_subject` FROM `mailbox` WHERE `username` = :username");
+    $stmt->execute(array(':username' => $username));
+    $data = $stmt->fetch(PDO::FETCH_ASSOC);
+  }
+  catch(PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+  return $data;
+}
+function user_get_alias_details($username) {
+	global $lang;
+	global $pdo;
+  if ($_SESSION['mailcow_cc_role'] == "user") {
+    $username	= $_SESSION['mailcow_cc_username'];
+  }
+  if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
+    return false;
+  }
+  try {
+    $data['address'] = $username;
+    $stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(`address` SEPARATOR ', '), '&#10008;') AS `aliases` FROM `alias` WHERE `goto` = :username_goto AND `address` NOT LIKE '@%' AND `address` != :username_address");
+    $stmt->execute(array(':username_goto' => $username, ':username_address' => $username));
+    $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    while ($row = array_shift($run)) {
+      $data['aliases'] = $row['aliases'];
+    }
+    $stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(local_part, '@', alias_domain SEPARATOR ', '), '&#10008;') AS `ad_alias` FROM `mailbox`
+      LEFT OUTER JOIN `alias_domain` on `target_domain` = `domain`
+        WHERE `username` = :username ;");
+    $stmt->execute(array(':username' => $username));
+    $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    while ($row = array_shift($run)) {
+      $data['ad_alias'] = $row['ad_alias'];
+    }
+    $stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(`send_as` SEPARATOR ', '), '&#10008;') AS `send_as` FROM `sender_acl` WHERE `logged_in_as` = :username AND `send_as` NOT LIKE '@%';");
+    $stmt->execute(array(':username' => $username));
+    $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    while ($row = array_shift($run)) {
+      $data['aliases_also_send_as'] = $row['send_as'];
+    }
+    $stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(`send_as` SEPARATOR ', '), '&#10008;') AS `send_as` FROM `sender_acl` WHERE `logged_in_as` = :username AND `send_as` LIKE '@%';");
+    $stmt->execute(array(':username' => $username));
+    $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    while ($row = array_shift($run)) {
+      $data['aliases_send_as_all'] = $row['send_as'];
+    }
+    $stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(`address` SEPARATOR ', '), '&#10008;') as `address` FROM `alias` WHERE `goto` = :username AND `address` LIKE '@%';");
+    $stmt->execute(array(':username' => $username));
+    $run = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    while ($row = array_shift($run)) {
+      $data['is_catch_all'] = $row['address'];
+    }
+    return $data;
+  }
+  catch(PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+}
+function is_valid_domain_name($domain_name) { 
+	if (empty($domain_name)) {
+		return false;
+	}
+	$domain_name = idn_to_ascii($domain_name);
+	return (preg_match("/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*$/i", $domain_name)
+		   && preg_match("/^.{1,253}$/", $domain_name)
+		   && preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $domain_name));
+}
+function add_domain_admin($postarray) {
+	global $lang;
+	global $pdo;
+	$username		= strtolower(trim($postarray['username']));
+	$password		= $postarray['password'];
+	$password2		= $postarray['password2'];
+	isset($postarray['active']) ? $active = '1' : $active = '0';
+	if ($_SESSION['mailcow_cc_role'] != "admin") {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+	if (empty($postarray['domain'])) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['domain_invalid'])
+		);
+		return false;
+	}
+	if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username)) || empty ($username)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['username_invalid'])
+		);
+		return false;
+	}
+	try {
+		$stmt = $pdo->prepare("SELECT `username` FROM `mailbox`
+			WHERE `username` = :username");
+		$stmt->execute(array(':username' => $username));
+		$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+		
+		$stmt = $pdo->prepare("SELECT `username` FROM `admin`
+			WHERE `username` = :username");
+		$stmt->execute(array(':username' => $username));
+		$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+		
+		$stmt = $pdo->prepare("SELECT `username` FROM `domain_admins`
+			WHERE `username` = :username");
+		$stmt->execute(array(':username' => $username));
+		$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+	}
+	catch(PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	foreach ($num_results as $num_results_each) {
+		if ($num_results_each != 0) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => sprintf($lang['danger']['object_exists'], htmlspecialchars($username))
+			);
+			return false;
+		}
+	}
+	if (!empty($password) && !empty($password2)) {
+		if ($password != $password2) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => sprintf($lang['danger']['password_mismatch'])
+			);
+			return false;
+		}
+		$password_hashed = hash_password($password);
+		foreach ($postarray['domain'] as $domain) {
+			if (!is_valid_domain_name($domain)) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => sprintf($lang['danger']['domain_invalid'])
+				);
+				return false;
+			}
+			try {
+				$stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
+						VALUES (:username, :domain, :created, :active)");
+				$stmt->execute(array(
+					':username' => $username,
+					':domain' => $domain,
+					':created' => date('Y-m-d H:i:s'),
+					':active' => $active
+				));
+			}
+			catch (PDOException $e) {
+        delete_domain_admin(array('username' => $username));
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => 'MySQL: '.$e
+				);
+				return false;
+			}
+		}
+		try {
+			$stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `created`, `modified`, `active`)
+				VALUES (:username, :password_hashed, '0', :created, :modified, :active)");
+			$stmt->execute(array(
+				':username' => $username,
+				':password_hashed' => $password_hashed,
+				':created' => date('Y-m-d H:i:s'),
+				':modified' => date('Y-m-d H:i:s'),
+				':active' => $active
+			));
+		}
+		catch (PDOException $e) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => 'MySQL: '.$e
+			);
+			return false;
+		}
+	}
+	else {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['password_empty'])
+		);
+		return false;
+	}
+	$_SESSION['return'] = array(
+		'type' => 'success',
+		'msg' => sprintf($lang['success']['domain_admin_added'], htmlspecialchars($username))
+	);
+}
+function delete_domain_admin($postarray) {
+	global $pdo;
+	global $lang;
+	if ($_SESSION['mailcow_cc_role'] != "admin") {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+	$username = $postarray['username'];
+	if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['username_invalid'])
+		);
+		return false;
+	}
+	try {
+		$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
+		$stmt->execute(array(
+			':username' => $username,
+		));
+		$stmt = $pdo->prepare("DELETE FROM `admin` WHERE `username` = :username");
+		$stmt->execute(array(
+			':username' => $username,
+		));
+	}
+	catch (PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	$_SESSION['return'] = array(
+		'type' => 'success',
+		'msg' => sprintf($lang['success']['domain_admin_removed'], htmlspecialchars($username))
+	);
+}
+function get_domain_admins() {
+	global $pdo;
+	global $lang;
+  $domainadmins = array();
+	if ($_SESSION['mailcow_cc_role'] != "admin") {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+  try {
+    $stmt = $pdo->query("SELECT DISTINCT
+      `username`
+        FROM `domain_admins` 
+          WHERE `username` IN (
+            SELECT `username` FROM `admin`
+              WHERE `superadmin`!='1'
+          )");
+    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    while ($row = array_shift($rows)) {
+      $domainadmins[] = $row['username'];
+    }
+  }
+  catch(PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+  }
+  return $domainadmins;
+}
+function get_domain_admin_details($domain_admin) {
+	global $pdo;
+	global $lang;
+  $domainadmindata = array();
+	if (isset($domain_admin) && $_SESSION['mailcow_cc_role'] != "admin") {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+  if (!isset($domain_admin) && $_SESSION['mailcow_cc_role'] != "domainadmin") {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+  (!isset($domain_admin)) ? $domain_admin = $_SESSION['mailcow_cc_username'] : null;
+  
+  if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $domain_admin))) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['username_invalid'])
+		);
+		return false;
+	}
+  try {
+    $stmt = $pdo->prepare("SELECT
+      `tfa`.`active` AS `tfa_active_int`,
+      `domain_admins`.`username`,
+      `domain_admins`.`created`,
+      `domain_admins`.`active` AS `active_int`,
+      CASE `domain_admins`.`active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`
+        FROM `domain_admins`
+        LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`domain_admins`.`username`
+          WHERE `domain_admins`.`username`= :domain_admin");
+    $stmt->execute(array(
+      ':domain_admin' => $domain_admin
+    ));
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+    $domainadmindata['username'] = $row['username'];
+    $domainadmindata['active'] = $row['active'];
+    $domainadmindata['active_int'] = $row['active_int'];
+    $domainadmindata['tfa_active_int'] = $row['tfa_active_int'];
+    $domainadmindata['created'] = $row['created'];
+    // GET SELECTED
+    $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
+      WHERE `domain` IN (
+        SELECT `domain` FROM `domain_admins`
+          WHERE `username`= :domain_admin)");
+    $stmt->execute(array(':domain_admin' => $domain_admin));
+    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    while($row = array_shift($rows)) {
+      $domainadmindata['selected_domains'][] = $row['domain'];
+    }
+    // GET UNSELECTED
+    $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
+      WHERE `domain` NOT IN (
+        SELECT `domain` FROM `domain_admins`
+          WHERE `username`= :domain_admin)");
+    $stmt->execute(array(':domain_admin' => $domain_admin));
+    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    while($row = array_shift($rows)) {
+      $domainadmindata['unselected_domains'][] = $row['domain'];
+    }
+  }
+  catch(PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+  }
+  return $domainadmindata;
+}
+function set_tfa($postarray) {
+	global $lang;
+	global $pdo;
+	global $yubi;
+	global $u2f;
+
+  if ($_SESSION['mailcow_cc_role'] != "domainadmin" &&
+    $_SESSION['mailcow_cc_role'] != "admin") {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+  }
+  $username = $_SESSION['mailcow_cc_username'];
+  
+  $stmt = $pdo->prepare("SELECT `password` FROM `admin`
+      WHERE `username` = :user");
+  $stmt->execute(array(':user' => $username));
+  $row = $stmt->fetch(PDO::FETCH_ASSOC);
+  if (!verify_ssha256($row['password'], $postarray["confirm_password"])) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  
+	switch ($postarray["tfa_method"]) {
+		case "yubi_otp":
+      (!isset($postarray["key_id"])) ? $key_id = 'unidentified' : $key_id = $postarray["key_id"];
+      $yubico_id = $postarray['yubico_id'];
+      $yubico_key = $postarray['yubico_key'];
+      $yubi = new Auth_Yubico($yubico_id, $yubico_key);
+      if (!$yubi) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => sprintf($lang['danger']['access_denied'])
+        );
+        return false;
+      }
+			if (!ctype_alnum($postarray["otp_token"]) || strlen($postarray["otp_token"]) != 44) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => sprintf($lang['danger']['tfa_token_invalid'])
+				);
+				return false;
+			}
+      $yauth = $yubi->verify($postarray["otp_token"]);
+      if (PEAR::isError($yauth)) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => 'Yubico API: ' . $yauth->getMessage()
+				);
+				return false;
+      }
+			try {
+        // We could also do a modhex translation here
+        $yubico_modhex_id = substr($postarray["otp_token"], 0, 12);
+        $stmt = $pdo->prepare("DELETE FROM `tfa` 
+          WHERE `username` = :username
+            AND (`authmech` != 'yubi_otp')
+            OR (`authmech` = 'yubi_otp' AND `secret` LIKE :modhex)");
+				$stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));
+				$stmt = $pdo->prepare("INSERT INTO `tfa` (`key_id`, `username`, `authmech`, `active`, `secret`) VALUES
+					(:key_id, :username, 'yubi_otp', '1', :secret)");
+				$stmt->execute(array(':key_id' => $key_id, ':username' => $username, ':secret' => $yubico_id . ':' . $yubico_key . ':' . $yubico_modhex_id));
+			}
+			catch (PDOException $e) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => 'MySQL: '.$e
+				);
+				return false;
+			}
+			$_SESSION['return'] = array(
+				'type' => 'success',
+				'msg' => sprintf($lang['success']['object_modified'], htmlspecialchars($username))
+			);
+		break;
+
+		case "u2f":
+      try {
+        (!isset($postarray["key_id"])) ? $key_id = 'unidentified' : $key_id = $postarray["key_id"];
+        $reg = $u2f->doRegister(json_decode($_SESSION['regReq']), json_decode($postarray['token']));
+        $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `authmech` != 'u2f'");
+				$stmt->execute(array(':username' => $username));
+        $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `keyHandle`, `publicKey`, `certificate`, `counter`, `active`) VALUES (?, ?, 'u2f', ?, ?, ?, ?, '1')");
+        $stmt->execute(array($username, $key_id, $reg->keyHandle, $reg->publicKey, $reg->certificate, $reg->counter));
+        $_SESSION['return'] = array(
+          'type' => 'success',
+          'msg' => sprintf($lang['success']['object_modified'], $username)
+        );
+        $_SESSION['regReq'] = null;
+      }
+      catch (Exception $e) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => "U2F: " . $e->getMessage()
+        );
+        $_SESSION['regReq'] = null;
+      }
+		break;
+
+		case "none":
+			try {
+				$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
+				$stmt->execute(array(':username' => $username));
+			}
+			catch (PDOException $e) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => 'MySQL: '.$e
+				);
+				return false;
+			}
+			$_SESSION['return'] = array(
+				'type' => 'success',
+				'msg' => sprintf($lang['success']['object_modified'], htmlspecialchars($username))
+			);
+		break;
+	}
+}
+function unset_tfa_key($postarray) {
+  // Can only unset own keys
+  // Needs at least one key left
+  global $pdo;
+  global $lang;
+  $id = intval($postarray['unset_tfa_key']);
+  if ($_SESSION['mailcow_cc_role'] != "domainadmin" &&
+    $_SESSION['mailcow_cc_role'] != "admin") {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+  }
+  $username = $_SESSION['mailcow_cc_username'];
+  try {
+    if (!is_numeric($id)) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+    $stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa`
+      WHERE `username` = :username AND `active` = '1'");
+    $stmt->execute(array(':username' => $username));
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+    if ($row['keys'] == "1") {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['last_key'])
+      );
+      return false;
+    }
+    $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `id` = :id");
+    $stmt->execute(array(':username' => $username, ':id' => $id));
+    $_SESSION['return'] = array(
+      'type' => 'success',
+      'msg' => sprintf($lang['success']['object_modified'], $username)
+    );
+  }
+  catch (PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+}
+function get_tfa($username = null) {
+	global $pdo;
+  if (isset($_SESSION['mailcow_cc_username'])) {
+    $username = $_SESSION['mailcow_cc_username'];
+  }
+  elseif (empty($username)) {
+    return false;
+  }
+
+  $stmt = $pdo->prepare("SELECT * FROM `tfa`
+      WHERE `username` = :username AND `active` = '1'");
+  $stmt->execute(array(':username' => $username));
+  $row = $stmt->fetch(PDO::FETCH_ASSOC);
+  
+	switch ($row["authmech"]) {
+		case "yubi_otp":
+      $data['name'] = "yubi_otp";
+      $data['pretty'] = "Yubico OTP";
+      $stmt = $pdo->prepare("SELECT `id`, `key_id`, RIGHT(`secret`, 12) AS 'modhex' FROM `tfa` WHERE `authmech` = 'yubi_otp' AND `username` = :username");
+      $stmt->execute(array(
+        ':username' => $username,
+      ));
+      $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+      while($row = array_shift($rows)) {
+        $data['additional'][] = $row;
+      }
+      return $data;
+    break;
+		case "u2f":
+      $data['name'] = "u2f";
+      $data['pretty'] = "Fido U2F";
+      $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username");
+      $stmt->execute(array(
+        ':username' => $username,
+      ));
+      $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+      while($row = array_shift($rows)) {
+        $data['additional'][] = $row;
+      }
+      return $data;
+    break;
+		case "hotp":
+      $data['name'] = "hotp";
+      $data['pretty'] = "HMAC-based OTP";
+      return $data;
+		break;
+ 		case "totp":
+      $data['name'] = "totp";
+      $data['pretty'] = "Time-based OTP";
+      return $data;
+		break;
+    default:
+      $data['name'] = 'none';
+      $data['pretty'] = "-";
+      return $data;
+    break;
+	}
+}
+function verify_tfa_login($username, $token) {
+	global $pdo;
+	global $lang;
+	global $yubi;
+
+  $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa`
+      WHERE `username` = :username AND `active` = '1'");
+  $stmt->execute(array(':username' => $username));
+  $row = $stmt->fetch(PDO::FETCH_ASSOC);
+  
+	switch ($row["authmech"]) {
+		case "yubi_otp":
+			if (!ctype_alnum($token) || strlen($token) != 44) {
+        return false;
+      }
+      $yubico_modhex_id = substr($token, 0, 12);
+      $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
+          WHERE `username` = :username
+          AND `authmech` = 'yubi_otp'
+          AND `active`='1'
+          AND `secret` LIKE :modhex");
+      $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));
+      $row = $stmt->fetch(PDO::FETCH_ASSOC);
+      $yubico_auth = explode(':', $row['secret']);
+      $yubi = new Auth_Yubico($yubico_auth[0], $yubico_auth[1]);
+      $yauth = $yubi->verify($token);
+      if (PEAR::isError($yauth)) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => 'Yubico Authentication error: ' . $yauth->getMessage()
+				);
+				return false;
+      }
+      else {
+        $_SESSION['tfa_id'] = $row['id'];
+        return true;
+      }
+    return false;
+  break;
+  case "u2f":
+    try {
+      global $u2f;
+      $reg = $u2f->doAuthenticate(json_decode($_SESSION['authReq']), get_u2f_registrations($username), json_decode($token));
+      $stmt = $pdo->prepare("UPDATE `tfa` SET `counter` = ? WHERE `id` = ?");
+      $stmt->execute(array($reg->counter, $reg->id));
+      $_SESSION['tfa_id'] = $reg->id;
+      $_SESSION['authReq'] = null;
+      return true;
+    }
+    catch (Exception $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => "U2F: " . $e->getMessage()
+      );
+      $_SESSION['regReq'] = null;
+      return false;
+    }
+    return false;
+  break;
+  case "hotp":
+      return false;
+  break;
+  case "totp":
+      return false;
+  break;
+  default:
+      return false;
+  break;
+	}
+  return false;
+}
+function edit_domain_admin($postarray) {
+	global $lang;
+	global $pdo;
+
+	if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+	// Administrator
+  if ($_SESSION['mailcow_cc_role'] == "admin") {
+    $username     = $postarray['username'];
+    $username_now = $postarray['username_now'];
+    $password     = $postarray['password'];
+    $password2    = $postarray['password2'];
+    isset($postarray['active']) ? $active = '1' : $active = '0';
+
+    if(isset($postarray['domain'])) {
+      foreach ($postarray['domain'] as $domain) {
+        if (!is_valid_domain_name($domain)) {
+          $_SESSION['return'] = array(
+            'type' => 'danger',
+            'msg' => sprintf($lang['danger']['domain_invalid'])
+          );
+          return false;
+        }
+      }
+    }
+
+    if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['username_invalid'])
+      );
+      return false;
+    }
+    if ($username != $username_now) {
+      if (empty(get_domain_admin_details($username_now)['username']) || !empty(get_domain_admin_details($username)['username'])) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => sprintf($lang['danger']['username_invalid'])
+        );
+        return false;
+      }
+    }
+    try {
+      $stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
+      $stmt->execute(array(
+        ':username' => $username_now,
+      ));
+    }
+    catch (PDOException $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => 'MySQL: '.$e
+      );
+      return false;
+    }
+
+    if(isset($postarray['domain'])) {
+      foreach ($postarray['domain'] as $domain) {
+        try {
+          $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
+            VALUES (:username, :domain, :created, :active)");
+          $stmt->execute(array(
+            ':username' => $username,
+            ':domain' => $domain,
+            ':created' => date('Y-m-d H:i:s'),
+            ':active' => $active
+          ));
+        }
+        catch (PDOException $e) {
+          $_SESSION['return'] = array(
+            'type' => 'danger',
+            'msg' => 'MySQL: '.$e
+          );
+          return false;
+        }
+      }
+    }
+
+    if (!empty($password) && !empty($password2)) {
+      if ($password != $password2) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => sprintf($lang['danger']['password_mismatch'])
+        );
+        return false;
+      }
+      $password_hashed = hash_password($password);
+      try {
+        $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username1, `modified` = :modified, `active` = :active, `password` = :password_hashed WHERE `username` = :username2");
+        $stmt->execute(array(
+          ':password_hashed' => $password_hashed,
+          ':username1' => $username,
+          ':username2' => $username_now,
+          ':modified' => date('Y-m-d H:i:s'),
+          ':active' => $active
+        ));
+        if (isset($postarray['disable_tfa'])) {
+          $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
+          $stmt->execute(array(':username' => $username_now));
+        }
+        else {
+          $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username WHERE `username` = :username_now");
+          $stmt->execute(array(':username' => $username, ':username_now' => $username_now));
+        }
+      }
+      catch (PDOException $e) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => 'MySQL: '.$e
+        );
+        return false;
+      }
+    }
+    else {
+      try {
+        $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username1, `modified` = :modified, `active` = :active WHERE `username` = :username2");
+        $stmt->execute(array(
+          ':username1' => $username,
+          ':username2' => $username_now,
+          ':modified' => date('Y-m-d H:i:s'),
+          ':active' => $active
+        ));
+        if (isset($postarray['disable_tfa'])) {
+          $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
+          $stmt->execute(array(':username' => $username));
+        }
+        else {
+          $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username WHERE `username` = :username_now");
+          $stmt->execute(array(':username' => $username, ':username_now' => $username_now));
+        }
+      }
+      catch (PDOException $e) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => 'MySQL: '.$e
+        );
+        return false;
+      }
+    }
+    $_SESSION['return'] = array(
+      'type' => 'success',
+      'msg' => sprintf($lang['success']['domain_admin_modified'], htmlspecialchars($username))
+    );
+  }
+  // Domain administrator
+  // Can only edit itself
+  elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") {
+    $username = $_SESSION['mailcow_cc_username'];
+    $password_old		= $postarray['user_old_pass'];
+    $password_new	= $postarray['user_new_pass'];
+    $password_new2	= $postarray['user_new_pass2'];
+
+    $stmt = $pdo->prepare("SELECT `password` FROM `admin`
+        WHERE `username` = :user");
+    $stmt->execute(array(':user' => $username));
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+    if (!verify_ssha256($row['password'], $password_old)) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+
+    if (!empty($password_new2) && !empty($password_new)) {
+      if ($password_new2 != $password_new) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => sprintf($lang['danger']['password_mismatch'])
+        );
+        return false;
+      }
+      if (strlen($password_new) < "6" ||
+        !preg_match('/[A-Za-z]/', $password_new) ||
+        !preg_match('/[0-9]/', $password_new)) {
+          $_SESSION['return'] = array(
+            'type' => 'danger',
+            'msg' => sprintf($lang['danger']['password_complexity'])
+          );
+          return false;
+      }
+      $password_hashed = hash_password($password_new);
+      try {
+        $stmt = $pdo->prepare("UPDATE `admin` SET `modified` = :modified, `password` = :password_hashed WHERE `username` = :username");
+        $stmt->execute(array(
+          ':password_hashed' => $password_hashed,
+          ':modified' => date('Y-m-d H:i:s'),
+          ':username' => $username
+        ));
+      }
+      catch (PDOException $e) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => 'MySQL: '.$e
+        );
+        return false;
+      }
+    }
+    
+    $_SESSION['return'] = array(
+      'type' => 'success',
+      'msg' => sprintf($lang['success']['domain_admin_modified'], htmlspecialchars($username))
+    );
+  }
+}
+function get_admin_details() {
+  // No parameter to be given, only one admin should exist
+	global $pdo;
+	global $lang;
+  $data = array();
+  if ($_SESSION['mailcow_cc_role'] != 'admin') {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  try {
+    $stmt = $pdo->prepare("SELECT `username`, `modified`, `created` FROM `admin`WHERE `superadmin`='1' AND active='1'");
+    $stmt->execute();
+    $data = $stmt->fetch(PDO::FETCH_ASSOC);
+  }
+  catch(PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+  }
+  return $data;
+}
+function dkim_add_key($postarray) {
+	global $lang;
+	global $pdo;
+  if ($_SESSION['mailcow_cc_role'] != "admin") {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  // if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+    // $_SESSION['return'] = array(
+      // 'type' => 'danger',
+      // 'msg' => sprintf($lang['danger']['access_denied'])
+    // );
+    // return false;
+  // }
+  $key_length	= intval($postarray['key_size']);
+  $domain	= $postarray['domain'];
+  if (!is_valid_domain_name($domain) || !is_numeric($key_length)) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['dkim_domain_or_sel_invalid'])
+    );
+    return false;
+  }
+
+  if (!empty(glob($GLOBALS['MC_DKIM_TXTS'] . '/' . $domain . '.dkim'))) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['dkim_domain_or_sel_invalid'])
+    );
+    return false;
+  }
+
+  $config = array(
+    "digest_alg" => "sha256",
+    "private_key_bits" => $key_length,
+    "private_key_type" => OPENSSL_KEYTYPE_RSA,
+  );
+  if ($keypair_ressource = openssl_pkey_new($config)) {
+    $key_details = openssl_pkey_get_details($keypair_ressource);
+    $pubKey = implode(array_slice(
+        array_filter(
+          explode(PHP_EOL, $key_details['key'])
+        ), 1, -1)
+      );
+    // Save public key to file
+    file_put_contents($GLOBALS['MC_DKIM_TXTS'] . '/' . $domain . '.dkim', $pubKey);
+    // Save private key to file
+    openssl_pkey_export_to_file($keypair_ressource, $GLOBALS['MC_DKIM_KEYS'] . '/' . $domain . '.dkim');
+    $_SESSION['return'] = array(
+      'type' => 'success',
+      'msg' => sprintf($lang['success']['dkim_added'])
+    );
+    return true;
+  }
+  else {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['dkim_domain_or_sel_invalid'])
+    );
+    return false;
+  }
+}
+function dkim_get_key_details($domain) {
+  $data = array();
+  if (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+    $dkim_pubkey_file = escapeshellarg($GLOBALS["MC_DKIM_TXTS"]. "/" . $domain . "." . "dkim");
+    if (file_exists(substr($dkim_pubkey_file, 1, -1))) {
+      $data['pubkey'] = file_get_contents($GLOBALS["MC_DKIM_TXTS"]. "/" . $domain . "." . "dkim");
+      $data['length'] = (strlen($data['pubkey']) < 391) ? 1024 : 2048;
+      $data['dkim_txt'] = 'v=DKIM1;k=rsa;t=s;s=email;p=' . file_get_contents($GLOBALS["MC_DKIM_TXTS"]. "/" . $domain . "." . "dkim");
+    }
+  }
+  return $data;
+}
+function dkim_get_blind_keys() {
+	global $lang;
+  if ($_SESSION['mailcow_cc_role'] != "admin") {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  $domains = array();
+  $dnstxt_folder = scandir($GLOBALS["MC_DKIM_TXTS"]);
+  $dnstxt_files = array_diff($dnstxt_folder, array('.', '..'));
+  foreach($dnstxt_files as $file) {
+    $domains[] = substr($file, 0, -5);
+  }
+  return array_diff($domains, array_merge(mailbox_get_domains(), mailbox_get_alias_domains()));
+}
+function dkim_delete_key($postarray) {
+	global $lang;
+  $domain	= $postarray['domain'];
+
+  if ($_SESSION['mailcow_cc_role'] != "admin") {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  // if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+    // $_SESSION['return'] = array(
+      // 'type' => 'danger',
+      // 'msg' => sprintf($lang['danger']['access_denied'])
+    // );
+    // return false;
+  // }
+  if (!is_valid_domain_name($domain)) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['dkim_domain_or_sel_invalid'])
+    );
+    return false;
+  }
+  exec('rm ' . escapeshellarg($GLOBALS['MC_DKIM_TXTS'] . '/' . $domain . '.dkim'), $out, $return);
+  if ($return != "0") {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['dkim_remove_failed'])
+    );
+    return false;
+  }
+  exec('rm ' . escapeshellarg($GLOBALS['MC_DKIM_KEYS'] . '/' . $domain . '.dkim'), $out, $return);
+  if ($return != "0") {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['dkim_remove_failed'])
+    );
+    return false;
+  }
+  $_SESSION['return'] = array(
+    'type' => 'success',
+    'msg' => sprintf($lang['success']['dkim_removed'])
+  );
+  return true;
+}
+function mailbox_add_domain($postarray) {
+  // Array elements
+  // domain                 string
+  // description            string
+  // aliases                int
+  // mailboxes              int
+  // maxquota               int
+  // quota                  int
+  // active                 int
+  // relay_all_recipients   int
+  // backupmx               int
+	global $pdo;
+	global $lang;
+	if ($_SESSION['mailcow_cc_role'] != "admin") {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+	$domain				= idn_to_ascii(strtolower(trim($postarray['domain'])));
+	$description  = $postarray['description'];
+	$aliases			= $postarray['aliases'];
+	$mailboxes    = $postarray['mailboxes'];
+	$maxquota			= $postarray['maxquota'];
+	$quota				= $postarray['quota'];
+
+	if ($maxquota > $quota) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['mailbox_quota_exceeds_domain_quota'])
+		);
+		return false;
+	}
+
+	isset($postarray['active'])               ? $active = '1'                 : $active = '0';
+	isset($postarray['relay_all_recipients'])	? $relay_all_recipients = '1'   : $relay_all_recipients = '0';
+	isset($postarray['backupmx'])             ? $backupmx = '1'               : $backupmx = '0';
+	isset($postarray['relay_all_recipients']) ? $backupmx = '1'               : true;
+
+	if (!is_valid_domain_name($domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['domain_invalid'])
+		);
+		return false;
+	}
+
+	foreach (array($quota, $maxquota, $mailboxes, $aliases) as $data) {
+		if (!is_numeric($data)) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => sprintf($lang['danger']['object_is_not_numeric'], htmlspecialchars($data))
+			);
+			return false;
+		}
+	}
+
+	try {
+		$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
+			WHERE `domain` = :domain");
+		$stmt->execute(array(':domain' => $domain));
+		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+		$stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain`
+			WHERE `alias_domain` = :domain");
+		$stmt->execute(array(':domain' => $domain));
+		$num_results = $num_results + count($stmt->fetchAll(PDO::FETCH_ASSOC));
+	}
+	catch(PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	if ($num_results != 0) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['domain_exists'], htmlspecialchars($domain))
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("INSERT INTO `domain` (`domain`, `description`, `aliases`, `mailboxes`, `maxquota`, `quota`, `transport`, `backupmx`, `created`, `modified`, `active`, `relay_all_recipients`)
+			VALUES (:domain, :description, :aliases, :mailboxes, :maxquota, :quota, 'virtual', :backupmx, :created, :modified, :active, :relay_all_recipients)");
+		$stmt->execute(array(
+			':domain' => $domain,
+			':description' => $description,
+			':aliases' => $aliases,
+			':mailboxes' => $mailboxes,
+			':maxquota' => $maxquota,
+			':quota' => $quota,
+			':backupmx' => $backupmx,
+			':active' => $active,
+			':created' => date('Y-m-d H:i:s'),
+			':modified' => date('Y-m-d H:i:s'),
+			':relay_all_recipients' => $relay_all_recipients
+		));
+		$_SESSION['return'] = array(
+			'type' => 'success',
+			'msg' => sprintf($lang['success']['domain_added'], htmlspecialchars($domain))
+		);
+	}
+	catch (PDOException $e) {
+    mailbox_delete_domain(array('domain' => $domain));
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+}
+function mailbox_add_alias($postarray) {
+  // Array elements
+  // address  string  (separated by " ", "," ";" "\n") - email address or domain
+  // goto     string  (separated by " ", "," ";" "\n")
+  // active   int
+	global $lang;
+	global $pdo;
+	$addresses  = array_map('trim', preg_split( "/( |,|;|\n)/", $postarray['address']));
+	$gotos      = array_map('trim', preg_split( "/( |,|;|\n)/", $postarray['goto']));
+	isset($postarray['active']) ? $active = '1' : $active = '0';
+	if (empty($addresses[0])) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['alias_empty'])
+		);
+		return false;
+	}
+
+	if (empty($gotos[0])) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['goto_empty'])
+		);
+		return false;
+	}
+
+	foreach ($addresses as $address) {
+		if (empty($address)) {
+			continue;
+		}
+
+		$domain       = idn_to_ascii(substr(strstr($address, '@'), 1));
+		$local_part   = strstr($address, '@', true);
+		$address      = $local_part.'@'.$domain;
+
+		try {
+			$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
+				WHERE `domain`= :domain1 OR `domain` = (SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain2)");
+			$stmt->execute(array(':domain1' => $domain, ':domain2' => $domain));
+			$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+      if ($num_results == 0) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => sprintf($lang['danger']['domain_not_found'], $domain)
+        );
+        return false;
+      }
+
+			$stmt = $pdo->prepare("SELECT `address` FROM `alias`
+				WHERE `address`= :address");
+			$stmt->execute(array(':address' => $address));
+			$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+      if ($num_results != 0) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => sprintf($lang['danger']['is_alias_or_mailbox'], htmlspecialchars($address))
+        );
+        return false;
+      }
+
+			$stmt = $pdo->prepare("SELECT `address` FROM `spamalias`
+				WHERE `address`= :address");
+			$stmt->execute(array(':address' => $address));
+			$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+      if ($num_results != 0) {
+        $_SESSION['return'] = array(
+          'type' => 'danger',
+          'msg' => sprintf($lang['danger']['is_spam_alias'], htmlspecialchars($address))
+        );
+        return false;
+      }
+		}
+		catch(PDOException $e) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => 'MySQL: '.$e
+			);
+			return false;
+		}
+
+		if ((!filter_var($address, FILTER_VALIDATE_EMAIL) === true) && !empty($local_part)) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => sprintf($lang['danger']['alias_invalid'])
+			);
+			return false;
+		}
+
+		if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => sprintf($lang['danger']['access_denied'])
+			);
+			return false;
+		}
+
+		foreach ($gotos as &$goto) {
+			if (empty($goto)) {
+				continue;
+			}
+
+			$goto_domain		= idn_to_ascii(substr(strstr($goto, '@'), 1));
+			$goto_local_part	= strstr($goto, '@', true);
+			$goto				= $goto_local_part.'@'.$goto_domain;
+
+			$stmt = $pdo->prepare("SELECT `username` FROM `mailbox`
+				WHERE `kind` REGEXP 'location|thing|group'
+          AND `username`= :goto");
+			$stmt->execute(array(':goto' => $goto));
+			$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+      if ($num_results != 0) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => sprintf($lang['danger']['goto_invalid'])
+				);
+				return false;
+      }
+
+			if (!filter_var($goto, FILTER_VALIDATE_EMAIL) === true) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => sprintf($lang['danger']['goto_invalid'])
+				);
+				return false;
+			}
+			if ($goto == $address) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => sprintf($lang['danger']['alias_goto_identical'])
+				);
+				return false;
+			}
+		}
+
+		$gotos = array_filter($gotos);
+		$goto = implode(",", $gotos);
+
+		try {
+			$stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `goto`, `domain`, `created`, `modified`, `active`)
+				VALUES (:address, :goto, :domain, :created, :modified, :active)");
+
+			if (!filter_var($address, FILTER_VALIDATE_EMAIL) === true) {
+				$stmt->execute(array(
+					':address' => '@'.$domain,
+					':goto' => $goto,
+					':domain' => $domain,
+					':created' => date('Y-m-d H:i:s'),
+					':modified' => date('Y-m-d H:i:s'),
+					':active' => $active
+				));
+			}
+			else {
+				$stmt->execute(array(
+					':address' => $address,
+					':goto' => $goto,
+					':domain' => $domain,
+					':created' => date('Y-m-d H:i:s'),
+					':modified' => date('Y-m-d H:i:s'),
+					':active' => $active
+				));
+			}
+			$_SESSION['return'] = array(
+				'type' => 'success',
+				'msg' => sprintf($lang['success']['alias_added'])
+			);
+		}
+		catch (PDOException $e) {
+      mailbox_delete_alias(array('address' => $address));
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => 'MySQL: '.$e
+			);
+			return false;
+		}
+	}
+	$_SESSION['return'] = array(
+		'type' => 'success',
+		'msg' => sprintf($lang['success']['alias_added'])
+	);
+}
+function mailbox_add_alias_domain($postarray) {
+  // Array elements
+  // active         int
+  // alias_domain   string
+  // target_domain  string
+	global $lang;
+	global $pdo;
+	isset($postarray['active']) ? $active = '1' : $active = '0';
+	$alias_domain     = idn_to_ascii(strtolower(trim($postarray['alias_domain'])));
+	$target_domain    = idn_to_ascii(strtolower(trim($postarray['target_domain'])));
+
+	if (!is_valid_domain_name($alias_domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['alias_domain_invalid'])
+		);
+		return false;
+	}
+
+	if (!is_valid_domain_name($target_domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['target_domain_invalid'])
+		);
+		return false;
+	}
+
+	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $target_domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+
+	if ($alias_domain == $target_domain) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['aliasd_targetd_identical'])
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
+			WHERE `domain`= :target_domain");
+		$stmt->execute(array(':target_domain' => $target_domain));
+		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+    if ($num_results == 0) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['targetd_not_found'])
+      );
+      return false;
+    }
+
+		$stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `alias_domain`= :alias_domain
+			UNION
+			SELECT `alias_domain` FROM `alias_domain` WHERE `alias_domain`= :alias_domain_in_domain");
+		$stmt->execute(array(':alias_domain' => $alias_domain, ':alias_domain_in_domain' => $alias_domain));
+		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+    if ($num_results != 0) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['aliasd_exists'])
+      );
+      return false;
+    }
+  }
+	catch(PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("INSERT INTO `alias_domain` (`alias_domain`, `target_domain`, `created`, `modified`, `active`)
+			VALUES (:alias_domain, :target_domain, :created, :modified, :active)");
+		$stmt->execute(array(
+			':alias_domain' => $alias_domain,
+			':target_domain' => $target_domain,
+			':created' => date('Y-m-d H:i:s'),
+			':modified' => date('Y-m-d H:i:s'),
+			':active' => $active
+		));
+		$_SESSION['return'] = array(
+			'type' => 'success',
+			'msg' => sprintf($lang['success']['aliasd_added'], htmlspecialchars($alias_domain))
+		);
+	}
+	catch (PDOException $e) {
+    mailbox_delete_alias_domain(array('alias_domain' => $alias_domain));
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+}
+function mailbox_add_mailbox($postarray) {
+  // Array elements
+  // active             int
+  // local_part         string
+  // domain             string
+  // name               string    (username if empty)
+  // password           string
+  // password2          string
+  // quota              int       (MiB)
+  // active             int
+
+	global $pdo;
+	global $lang;
+	$local_part   = strtolower(trim($postarray['local_part']));
+	$domain       = idn_to_ascii(strtolower(trim($postarray['domain'])));
+  $username     = $local_part . '@' . $domain;
+	if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['mailbox_invalid'])
+		);
+		return false;
+	}
+	if (empty($postarray['local_part'])) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['mailbox_invalid'])
+		);
+		return false;
+	}
+	$password     = $postarray['password'];
+	$password2    = $postarray['password2'];
+	$name         = $postarray['name'];
+  $quota_m			= filter_var($postarray['quota'], FILTER_SANITIZE_NUMBER_FLOAT);
+
+	if (empty($name)) {
+		$name = $local_part;
+	}
+
+	isset($postarray['active']) ? $active = '1' : $active = '0';
+
+	$quota_b		= ($quota_m * 1048576);
+	$maildir		= $domain."/".$local_part."/";
+
+	if (!is_valid_domain_name($domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['domain_invalid'])
+		);
+		return false;
+	}
+
+	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("SELECT `mailboxes`, `maxquota`, `quota` FROM `domain`
+			WHERE `domain` = :domain");
+		$stmt->execute(array(':domain' => $domain));
+		$DomainData = $stmt->fetch(PDO::FETCH_ASSOC);
+
+		$stmt = $pdo->prepare("SELECT 
+			COUNT(*) as count,
+			COALESCE(ROUND(SUM(`quota`)/1048576), 0) as `quota`
+				FROM `mailbox`
+					WHERE `kind` NOT REGEXP 'location|thing|group'
+            AND `domain` = :domain");
+		$stmt->execute(array(':domain' => $domain));
+		$MailboxData = $stmt->fetch(PDO::FETCH_ASSOC);
+
+		$stmt = $pdo->prepare("SELECT `local_part` FROM `mailbox` WHERE `local_part` = :local_part and `domain`= :domain");
+		$stmt->execute(array(':local_part' => $local_part, ':domain' => $domain));
+		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+    if ($num_results != 0) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['object_exists'], htmlspecialchars($username))
+      );
+      return false;
+    }
+
+		$stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE address= :username");
+		$stmt->execute(array(':username' => $username));
+		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+    if ($num_results != 0) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['is_alias'], htmlspecialchars($username))
+      );
+      return false;
+    }
+
+		$stmt = $pdo->prepare("SELECT `address` FROM `spamalias` WHERE `address`= :username");
+		$stmt->execute(array(':username' => $username));
+		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+    if ($num_results != 0) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['is_spam_alias'], htmlspecialchars($username))
+      );
+      return false;
+    }
+
+		$stmt = $pdo->prepare("SELECT `domain` FROM `domain` WHERE `domain`= :domain");
+		$stmt->execute(array(':domain' => $domain));
+		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+    if ($num_results == 0) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['domain_not_found'], $domain)
+      );
+      return false;
+    }
+  }
+	catch(PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+
+	if (!is_numeric($quota_m) || $quota_m == "0") {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['quota_not_0_not_numeric'])
+		);
+		return false;
+	}
+
+	if (!empty($password) && !empty($password2)) {
+		if ($password != $password2) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => sprintf($lang['danger']['password_mismatch'])
+			);
+			return false;
+		}
+		$password_hashed = hash_password($password);
+	}
+	else {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['password_empty'])
+		);
+		return false;
+	}
+
+	if ($MailboxData['count'] >= $DomainData['mailboxes']) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['max_mailbox_exceeded'], $MailboxData['count'], $DomainData['mailboxes'])
+		);
+		return false;
+	}
+
+	if ($quota_m > $DomainData['maxquota']) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['mailbox_quota_exceeded'], $DomainData['maxquota'])
+		);
+		return false;
+	}
+
+	if (($MailboxData['quota'] + $quota_m) > $DomainData['quota']) {
+		$quota_left_m = ($DomainData['quota'] - $MailboxData['quota']);
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['mailbox_quota_left_exceeded'], $quota_left_m)
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `maildir`, `quota`, `local_part`, `domain`, `created`, `modified`, `active`) 
+			VALUES (:username, :password_hashed, :name, :maildir, :quota_b, :local_part, :domain, :created, :modified, :active)");
+		$stmt->execute(array(
+			':username' => $username,
+			':password_hashed' => $password_hashed,
+			':name' => $name,
+			':maildir' => $maildir,
+			':quota_b' => $quota_b,
+			':local_part' => $local_part,
+			':domain' => $domain,
+			':created' => date('Y-m-d H:i:s'),
+			':modified' => date('Y-m-d H:i:s'),
+			':active' => $active
+		));
+
+		$stmt = $pdo->prepare("INSERT INTO `quota2` (`username`, `bytes`, `messages`)
+			VALUES (:username, '0', '0')");
+		$stmt->execute(array(':username' => $username));
+
+		$stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `goto`, `domain`, `created`, `modified`, `active`)
+			VALUES (:username1, :username2, :domain, :created, :modified, :active)");
+		$stmt->execute(array(
+			':username1' => $username,
+			':username2' => $username,
+			':domain' => $domain,
+			':created' => date('Y-m-d H:i:s'),
+			':modified' => date('Y-m-d H:i:s'),
+			':active' => $active
+		));
+
+		$_SESSION['return'] = array(
+			'type' => 'success',
+			'msg' => sprintf($lang['success']['mailbox_added'], htmlspecialchars($username))
+		);
+	}
+	catch (PDOException $e) {
+    mailbox_delete_mailbox(array('username' => $username));
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+}
+function mailbox_add_resource($postarray) {
+  // Array elements
+  // active             int
+  // domain             string
+  // description        string
+  // multiple_bookings  int
+  // kind               string
+
+	global $pdo;
+	global $lang;
+	$domain             = idn_to_ascii(strtolower(trim($postarray['domain'])));
+  $description        = $postarray['description'];
+  $local_part         = preg_replace('/[^\da-z]/i', '', preg_quote($description, '/'));
+  $name               = $local_part . '@' . $domain;
+  $kind               = $postarray['kind'];
+	isset($postarray['active']) ? $active = '1' : $active = '0';
+	isset($postarray['multiple_bookings']) ? $multiple_bookings = '1' : $multiple_bookings = '0';
+
+	if (!filter_var($name, FILTER_VALIDATE_EMAIL)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['resource_invalid'])
+		);
+		return false;
+	}
+
+	if (empty($description)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['description_invalid'])
+		);
+		return false;
+  }
+  
+	if ($kind != 'location' && $kind != 'group' && $kind != 'thing') {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['resource_invalid'])
+		);
+		return false;
+	}
+
+	if (!is_valid_domain_name($domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['domain_invalid'])
+		);
+		return false;
+	}
+
+	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :name");
+		$stmt->execute(array(':name' => $name));
+		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+    if ($num_results != 0) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['object_exists'], htmlspecialchars($name))
+      );
+      return false;
+    }
+
+		$stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE address= :name");
+		$stmt->execute(array(':name' => $name));
+		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+    if ($num_results != 0) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['is_alias'], htmlspecialchars($name))
+      );
+      return false;
+    }
+
+		$stmt = $pdo->prepare("SELECT `address` FROM `spamalias` WHERE `address`= :name");
+		$stmt->execute(array(':name' => $name));
+		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+    if ($num_results != 0) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['is_spam_alias'], htmlspecialchars($name))
+      );
+      return false;
+    }
+
+		$stmt = $pdo->prepare("SELECT `domain` FROM `domain` WHERE `domain`= :domain");
+		$stmt->execute(array(':domain' => $domain));
+		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+    if ($num_results == 0) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['domain_not_found'], $domain)
+      );
+      return false;
+    }
+  }
+	catch(PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `maildir`, `quota`, `local_part`, `domain`, `created`, `modified`, `active`, `multiple_bookings`, `kind`) 
+			VALUES (:name, 'RESOURCE', :description, 'RESOURCE', 0, :local_part, :domain, :created, :modified, :active, :multiple_bookings, :kind)");
+		$stmt->execute(array(
+			':name' => $name,
+			':description' => $description,
+			':local_part' => $local_part,
+			':domain' => $domain,
+			':created' => date('Y-m-d H:i:s'),
+			':modified' => date('Y-m-d H:i:s'),
+			':active' => $active,
+			':kind' => $kind,
+			':multiple_bookings' => $multiple_bookings
+		));
+
+		$_SESSION['return'] = array(
+			'type' => 'success',
+			'msg' => sprintf($lang['success']['resource_added'], htmlspecialchars($name))
+		);
+	}
+	catch (PDOException $e) {
+    mailbox_delete_resource(array('name' => $name));
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+}
+function mailbox_edit_alias_domain($postarray) {
+  // Array elements
+  // active             int
+  // alias_domain_now   string
+  // alias_domain       string
+	global $lang;
+	global $pdo;
+	isset($postarray['active']) ? $active = '1' : $active = '0';
+	$alias_domain       = idn_to_ascii(strtolower(trim($postarray['alias_domain'])));
+	$alias_domain_now   = strtolower(trim($postarray['alias_domain_now']));
+	if (!is_valid_domain_name($alias_domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['alias_domain_invalid'])
+		);
+		return false;
+	}
+
+	if (!is_valid_domain_name($alias_domain_now)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['alias_domain_invalid'])
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain`
+				WHERE `alias_domain`= :alias_domain_now");
+		$stmt->execute(array(':alias_domain_now' => $alias_domain_now));
+		$DomainData = $stmt->fetch(PDO::FETCH_ASSOC);
+	}
+	catch(PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $DomainData['target_domain'])) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain`
+		WHERE `target_domain`= :alias_domain");
+		$stmt->execute(array(':alias_domain' => $alias_domain));
+		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+	}
+	catch(PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	if ($num_results != 0) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['aliasd_targetd_identical'])
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("UPDATE `alias_domain` SET
+      `alias_domain` = :alias_domain,
+      `active` = :active,
+      `modified` = :modified,
+        WHERE `alias_domain` = :alias_domain_now");
+		$stmt->execute(array(
+			':alias_domain' => $alias_domain,
+      ':modified' => date('Y-m-d H:i:s'),
+			':alias_domain_now' => $alias_domain_now,
+			':active' => $active
+		));
+	}
+	catch (PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+
+	$_SESSION['return'] = array(
+		'type' => 'success',
+		'msg' => sprintf($lang['success']['aliasd_modified'], htmlspecialchars($alias_domain))
+	);
+}
+function mailbox_edit_alias($postarray) {
+  // Array elements
+  // address            string
+  // goto               string    (separated by " ", "," ";" "\n") - email address or domain
+  // active             int
+	global $lang;
+	global $pdo;
+	$address      = $postarray['address'];
+	$domain       = idn_to_ascii(substr(strstr($address, '@'), 1));
+	$local_part   = strstr($address, '@', true);
+	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+	if (empty($postarray['goto'])) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['goto_empty'])
+		);
+		return false;
+	}
+	$gotos = array_map('trim', preg_split( "/( |,|;|\n)/", $postarray['goto']));
+	foreach ($gotos as &$goto) {
+		if (empty($goto)) {
+			continue;
+		}
+		if (!filter_var($goto, FILTER_VALIDATE_EMAIL)) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' =>sprintf($lang['danger']['goto_invalid'])
+			);
+			return false;
+		}
+		if ($goto == $address) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => sprintf($lang['danger']['alias_goto_identical'])
+			);
+			return false;
+		}
+	}
+	$gotos = array_filter($gotos);
+	$goto = implode(",", $gotos);
+	isset($postarray['active']) ? $active = '1' : $active = '0';
+	if ((!filter_var($address, FILTER_VALIDATE_EMAIL) === true) && !empty($local_part)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['alias_invalid'])
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("UPDATE `alias` SET
+      `goto` = :goto,
+      `active`= :active,
+      `modified` = :modified
+        WHERE `address` = :address");
+		$stmt->execute(array(
+			':goto' => $goto,
+			':active' => $active,
+			':address' => $address,
+      ':modified' => date('Y-m-d H:i:s'),
+		));
+		$_SESSION['return'] = array(
+			'type' => 'success',
+		'msg' => sprintf($lang['success']['alias_modified'], htmlspecialchars($address))
+		);
+	}
+	catch (PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+}
+function mailbox_edit_domain($postarray) {
+  // Array elements
+  // domain                 string
+  // description            string
+  // active                 int
+  // relay_all_recipients   int
+  // backupmx               int
+  // aliases                float
+  // mailboxes              float
+  // maxquota               float
+  // quota                  float     (Byte)
+  // active                 int
+
+	global $lang;
+	global $pdo;
+  
+  $domain       = idn_to_ascii($postarray['domain']);
+	if (!is_valid_domain_name($domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['domain_invalid'])
+		);
+		return false;
+	}
+
+	if ($_SESSION['mailcow_cc_role'] == "domainadmin" && 	hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+    $description  = $postarray['description'];
+    isset($postarray['active']) ? $active = '1' : $active = '0';
+    try {
+      $stmt = $pdo->prepare("UPDATE `domain` SET 
+      `modified`= :modified,
+      `description` = :description
+        WHERE `domain` = :domain");
+      $stmt->execute(array(
+        ':modified' => date('Y-m-d H:i:s'),
+        ':description' => $description,
+        ':domain' => $domain
+      ));
+      $_SESSION['return'] = array(
+        'type' => 'success',
+        'msg' => sprintf($lang['success']['domain_modified'], htmlspecialchars($domain))
+      );
+    }
+    catch (PDOException $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => 'MySQL: '.$e
+      );
+      return false;
+    }
+  }
+  elseif ($_SESSION['mailcow_cc_role'] == "admin") {
+    $description  = $postarray['description'];
+    isset($postarray['active']) ? $active = '1' : $active = '0';
+    $aliases		= filter_var($postarray['aliases'], FILTER_SANITIZE_NUMBER_FLOAT);
+    $mailboxes  = filter_var($postarray['mailboxes'], FILTER_SANITIZE_NUMBER_FLOAT);
+    $maxquota		= filter_var($postarray['maxquota'], FILTER_SANITIZE_NUMBER_FLOAT);
+    $quota			= filter_var($postarray['quota'], FILTER_SANITIZE_NUMBER_FLOAT);
+    isset($postarray['relay_all_recipients']) ? $relay_all_recipients = '1' : $relay_all_recipients = '0';
+    isset($postarray['backupmx']) ? $backupmx = '1' : $backupmx = '0';
+    isset($postarray['relay_all_recipients']) ? $backupmx = '1' : true;
+    try {
+      // GET MAILBOX DATA
+      $stmt = $pdo->prepare("SELECT 
+          COUNT(*) AS count,
+          MAX(COALESCE(ROUND(`quota`/1048576), 0)) AS `maxquota`,
+          COALESCE(ROUND(SUM(`quota`)/1048576), 0) AS `quota`
+            FROM `mailbox`
+              WHERE `kind` NOT REGEXP 'location|thing|group'
+                AND domain = :domain");
+      $stmt->execute(array(':domain' => $domain));
+      $MailboxData = $stmt->fetch(PDO::FETCH_ASSOC);
+      // GET ALIAS DATA
+      $stmt = $pdo->prepare("SELECT COUNT(*) AS `count` FROM `alias`
+          WHERE domain = :domain
+          AND address NOT IN (
+            SELECT `username` FROM `mailbox`
+          )");
+      $stmt->execute(array(':domain' => $domain));
+      $AliasData = $stmt->fetch(PDO::FETCH_ASSOC);
+    }
+    catch(PDOException $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => 'MySQL: '.$e
+      );
+      return false;
+    }
+
+    if ($maxquota > $quota) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['mailbox_quota_exceeds_domain_quota'])
+      );
+      return false;
+    }
+
+    if ($MailboxData['maxquota'] > $maxquota) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['max_quota_in_use'], $MailboxData['maxquota'])
+      );
+      return false;
+    }
+
+    if ($MailboxData['quota'] > $quota) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['domain_quota_m_in_use'], $MailboxData['quota'])
+      );
+      return false;
+    }
+
+    if ($MailboxData['count'] > $mailboxes) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['mailboxes_in_use'], $MailboxData['count'])
+      );
+      return false;
+    }
+
+    if ($AliasData['count'] > $aliases) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['aliases_in_use'], $AliasData['count'])
+      );
+      return false;
+    }
+    try {
+      $stmt = $pdo->prepare("UPDATE `domain` SET 
+      `modified`= :modified,
+      `relay_all_recipients` = :relay_all_recipients,
+      `backupmx` = :backupmx,
+      `active` = :active,
+      `quota` = :quota,
+      `maxquota` = :maxquota,
+      `mailboxes` = :mailboxes,
+      `aliases` = :aliases,
+      `description` = :description
+        WHERE `domain` = :domain");
+      $stmt->execute(array(
+        ':relay_all_recipients' => $relay_all_recipients,
+        ':backupmx' => $backupmx,
+        ':active' => $active,
+        ':quota' => $quota,
+        ':maxquota' => $maxquota,
+        ':mailboxes' => $mailboxes,
+        ':aliases' => $aliases,
+        ':modified' => date('Y-m-d H:i:s'),
+        ':description' => $description,
+        ':domain' => $domain
+      ));
+      $_SESSION['return'] = array(
+        'type' => 'success',
+        'msg' => sprintf($lang['success']['domain_modified'], htmlspecialchars($domain))
+      );
+    }
+    catch (PDOException $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => 'MySQL: '.$e
+      );
+      return false;
+    }
+  }
+}
+function mailbox_edit_mailbox($postarray) {
+	global $lang;
+	global $pdo;
+	isset($postarray['active']) ? $active = '1' : $active = '0';
+	if (!filter_var($postarray['username'], FILTER_VALIDATE_EMAIL)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['username_invalid'])
+		);
+		return false;
+	}
+	$quota_m      = $postarray['quota'];
+	$quota_b      = $quota_m*1048576;
+	$username     = $postarray['username'];
+	$name         = $postarray['name'];
+	$password     = $postarray['password'];
+	$password2    = $postarray['password2'];
+
+	try {
+		$stmt = $pdo->prepare("SELECT `domain`
+			FROM `mailbox`
+				WHERE username = :username");
+		$stmt->execute(array(':username' => $username));
+		$MailboxData1 = $stmt->fetch(PDO::FETCH_ASSOC);
+
+		$stmt = $pdo->prepare("SELECT 
+			COALESCE(ROUND(SUM(`quota`)/1048576), 0) as `quota_m_now`
+				FROM `mailbox`
+					WHERE `username` = :username");
+		$stmt->execute(array(':username' => $username));
+		$MailboxData2 = $stmt->fetch(PDO::FETCH_ASSOC);
+
+		$stmt = $pdo->prepare("SELECT 
+			COALESCE(ROUND(SUM(`quota`)/1048576), 0) as `quota_m_in_use`
+				FROM `mailbox`
+					WHERE `domain` = :domain");
+		$stmt->execute(array(':domain' => $MailboxData1['domain']));
+		$MailboxData3 = $stmt->fetch(PDO::FETCH_ASSOC);
+
+		$stmt = $pdo->prepare("SELECT `quota`, `maxquota`
+			FROM `domain`
+				WHERE `domain` = :domain");
+		$stmt->execute(array(':domain' => $MailboxData1['domain']));
+		$DomainData = $stmt->fetch(PDO::FETCH_ASSOC);
+	}
+	catch(PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+
+	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $MailboxData1['domain'])) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+	if (!is_numeric($quota_m) || $quota_m == "0") {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['quota_not_0_not_numeric'], htmlspecialchars($quota_m))
+		);
+		return false;
+	}
+	if ($quota_m > $DomainData['maxquota']) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['mailbox_quota_exceeded'], $DomainData['maxquota'])
+		);
+		return false;
+	}
+	if (($MailboxData3['quota_m_in_use'] - $MailboxData2['quota_m_now'] + $quota_m) > $DomainData['quota']) {
+		$quota_left_m = ($DomainData['quota'] - $MailboxData3['quota_m_in_use'] + $MailboxData2['quota_m_now']);
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['mailbox_quota_left_exceeded'], $quota_left_m)
+		);
+		return false;
+	}
+
+  // Get sender_acl items set by admin
+  $sender_acl_admin = array_merge(
+    mailbox_get_sender_acl_handles($username)['sender_acl_domains']['ro'],
+    mailbox_get_sender_acl_handles($username)['sender_acl_addresses']['ro']
+  );
+
+  // Get sender_acl items from POST array
+  (isset($postarray['sender_acl'])) ? $sender_acl_domain_admin = $postarray['sender_acl'] : $sender_acl_domain_admin = array();
+
+	if (!empty($sender_acl_domain_admin) || !empty($sender_acl_admin)) {
+    // Check items in POST array
+		foreach ($sender_acl_domain_admin as $sender_acl) {
+			if (!filter_var($sender_acl, FILTER_VALIDATE_EMAIL) && !is_valid_domain_name(ltrim($sender_acl, '@'))) {
+					$_SESSION['return'] = array(
+						'type' => 'danger',
+						'msg' => sprintf($lang['danger']['sender_acl_invalid'])
+					);
+					return false;
+			}
+      if (is_valid_domain_name(ltrim($sender_acl, '@'))) {
+        if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], ltrim($sender_acl, '@'))) {
+					$_SESSION['return'] = array(
+						'type' => 'danger',
+						'msg' => sprintf($lang['danger']['sender_acl_invalid'])
+					);
+					return false;
+        }
+      }
+			if (filter_var($sender_acl, FILTER_VALIDATE_EMAIL)) {
+        if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $sender_acl)) {
+					$_SESSION['return'] = array(
+						'type' => 'danger',
+						'msg' => sprintf($lang['danger']['sender_acl_invalid'])
+					);
+					return false;
+        }
+      }
+    }
+
+    // Merge both arrays
+    $sender_acl_merged = array_merge($sender_acl_domain_admin, $sender_acl_admin);
+
+    try {
+      $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `logged_in_as` = :username");
+      $stmt->execute(array(
+        ':username' => $username
+      ));
+    }
+    catch (PDOException $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => 'MySQL: '.$e
+      );
+      return false;
+    }
+
+		foreach ($sender_acl_merged as $sender_acl) {
+      $domain = ltrim($sender_acl, '@');
+      if (is_valid_domain_name($domain)) {
+        $sender_acl = '@' . $domain;
+      }
+			try {
+				$stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`)
+					VALUES (:sender_acl, :username)");
+				$stmt->execute(array(
+					':sender_acl' => $sender_acl,
+					':username' => $username
+				));
+			}
+			catch (PDOException $e) {
+				$_SESSION['return'] = array(
+					'type' => 'danger',
+					'msg' => 'MySQL: '.$e
+				);
+				return false;
+			}
+		}
+	}
+  else {
+    try {
+      $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `logged_in_as` = :username");
+      $stmt->execute(array(
+        ':username' => $username
+      ));
+    }
+    catch (PDOException $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => 'MySQL: '.$e
+      );
+      return false;
+    }
+  }
+	if (!empty($password) && !empty($password2)) {
+		if ($password != $password2) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => sprintf($lang['danger']['password_mismatch'])
+			);
+			return false;
+		}
+		$password_hashed = hash_password($password);
+		try {
+			$stmt = $pdo->prepare("UPDATE `alias` SET
+					`modified` = :modified,
+					`active` = :active
+						WHERE `address` = :address");
+			$stmt->execute(array(
+				':address' => $username,
+				':modified' => date('Y-m-d H:i:s'),
+				':active' => $active
+			));
+			$stmt = $pdo->prepare("UPDATE `mailbox` SET
+					`modified` = :modified,
+					`active` = :active,
+					`password` = :password_hashed,
+					`name`= :name,
+					`quota` = :quota_b
+						WHERE `username` = :username");
+			$stmt->execute(array(
+				':modified' => date('Y-m-d H:i:s'),
+				':password_hashed' => $password_hashed,
+				':active' => $active,
+				':name' => $name,
+				':quota_b' => $quota_b,
+				':username' => $username
+			));
+			$_SESSION['return'] = array(
+				'type' => 'success',
+				'msg' => sprintf($lang['success']['mailbox_modified'], $username)
+			);
+			return true;
+		}
+		catch (PDOException $e) {
+			$_SESSION['return'] = array(
+				'type' => 'danger',
+				'msg' => 'MySQL: '.$e
+			);
+			return false;
+		}
+	}
+	try {
+		$stmt = $pdo->prepare("UPDATE `alias` SET
+				`modified` = :modified,
+				`active` = :active
+					WHERE `address` = :address");
+		$stmt->execute(array(
+			':address' => $username,
+			':modified' => date('Y-m-d H:i:s'),
+			':active' => $active
+		));
+		$stmt = $pdo->prepare("UPDATE `mailbox` SET
+				`modified` = :modified,
+				`active` = :active,
+				`name`= :name,
+				`quota` = :quota_b
+					WHERE `username` = :username");
+		$stmt->execute(array(
+			':active' => $active,
+			':modified' => date('Y-m-d H:i:s'),
+			':name' => $name,
+			':quota_b' => $quota_b,
+			':username' => $username
+		));
+		$_SESSION['return'] = array(
+			'type' => 'success',
+			'msg' => sprintf($lang['success']['mailbox_modified'], $username)
+		);
+		return true;
+	}
+	catch (PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+}
+function mailbox_edit_resource($postarray) {
+	global $lang;
+	global $pdo;
+
+	isset($postarray['active']) ? $active = '1' : $active = '0';
+	isset($postarray['multiple_bookings']) ? $multiple_bookings = '1' : $multiple_bookings = '0';
+	$name               = $postarray['name'];
+	$kind               = $postarray['kind'];
+	$description        = $postarray['description'];
+
+	if (!filter_var($name, FILTER_VALIDATE_EMAIL)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['resource_invalid'])
+		);
+		return false;
+	}
+
+	if (empty($description)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['description_invalid'])
+		);
+		return false;
+  }
+
+	if ($kind != 'location' && $kind != 'group' && $kind != 'thing') {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['resource_invalid'])
+		);
+		return false;
+	}
+
+  if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $name)) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+
+	try {
+		$stmt = $pdo->prepare("UPDATE `mailbox` SET
+				`modified` = :modified,
+				`active` = :active,
+				`name`= :description,
+				`kind`= :kind,
+				`multiple_bookings`= :multiple_bookings
+          WHERE `username` = :name");
+		$stmt->execute(array(
+			':active' => $active,
+			':modified' => date('Y-m-d H:i:s'),
+			':description' => $description,
+			':multiple_bookings' => $multiple_bookings,
+			':kind' => $kind,
+			':name' => $name
+		));
+		$_SESSION['return'] = array(
+			'type' => 'success',
+			'msg' => sprintf($lang['success']['resource_modified'], $name)
+		);
+		return true;
+	}
+	catch (PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+}
+function mailbox_get_mailboxes($domain = null) {
+	global $lang;
+	global $pdo;
+  $mailboxes = array();
+	if (isset($domain) && !hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+  elseif (isset($domain) && hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+    try {
+      $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `kind` NOT REGEXP 'location|thing|group' AND `domain` != 'ALL' AND `domain` = :domain");
+      $stmt->execute(array(
+        ':domain' => $domain,
+      ));
+      $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+      while($row = array_shift($rows)) {
+        $mailboxes[] = $row['username'];
+      }
+    }
+    catch (PDOException $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => 'MySQL: '.$e
+      );
+      return false;
+    }
+  }
+  else {
+    try {
+      $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `kind` NOT REGEXP 'location|thing|group' AND `domain` IN (SELECT `domain` FROM `domain_admins` WHERE `active` = '1' AND `username` = :username) OR 'admin' = :role");
+      $stmt->execute(array(
+        ':username' => $_SESSION['mailcow_cc_username'],
+        ':role' => $_SESSION['mailcow_cc_role'],
+      ));
+      $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+      while($row = array_shift($rows)) {
+        $mailboxes[] = $row['username'];
+      }
+    }
+    catch (PDOException $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => 'MySQL: '.$e
+      );
+      return false;
+    }
+  }
+  return $mailboxes;
+}
+function mailbox_get_resources($domain = null) {
+	global $lang;
+	global $pdo;
+  $resources = array();
+	if (isset($domain) && !hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+  elseif (isset($domain) && hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+    try {
+      $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `kind` REGEXP 'location|thing|group' AND `domain` != 'ALL' AND `domain` = :domain");
+      $stmt->execute(array(
+        ':domain' => $domain,
+      ));
+      $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+      while($row = array_shift($rows)) {
+        $resources[] = $row['username'];
+      }
+    }
+    catch (PDOException $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => 'MySQL: '.$e
+      );
+      return false;
+    }
+  }
+  else {
+    try {
+      $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `kind` REGEXP 'location|thing|group' AND `domain` IN (SELECT `domain` FROM `domain_admins` WHERE `active` = '1' AND `username` = :username) OR 'admin' = :role");
+      $stmt->execute(array(
+        ':username' => $_SESSION['mailcow_cc_username'],
+        ':role' => $_SESSION['mailcow_cc_role'],
+      ));
+      $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+      while($row = array_shift($rows)) {
+        $resources[] = $row['username'];
+      }
+    }
+    catch (PDOException $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => 'MySQL: '.$e
+      );
+      return false;
+    }
+  }
+  return $resources;
+}
+function mailbox_get_alias_domains($domain = null) {
+  // Get all domains assigned to mailcow_cc_username or domain, if set
+  // Domain admin needs to be active
+  // Domain does not need to be active
+	global $lang;
+	global $pdo;
+  $aliasdomains = array();
+	if (isset($domain) && !hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+  }
+  elseif (isset($domain) && hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+    try {
+      $stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` = :domain");
+      $stmt->execute(array(
+        ':domain' => $domain,
+      ));
+      $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+      while($row = array_shift($rows)) {
+        $aliasdomains[] = $row['alias_domain'];
+      }
+    }
+    catch (PDOException $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => 'MySQL: '.$e
+      );
+      return false;
+    }
+  }
+	else {
+    try {
+      $stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` IN (SELECT `domain` FROM `domain_admins` WHERE `active` = '1' AND `username` = :username) OR 'admin' = :role");
+      $stmt->execute(array(
+        ':username' => $_SESSION['mailcow_cc_username'],
+        ':role' => $_SESSION['mailcow_cc_role'],
+      ));
+      $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+      while($row = array_shift($rows)) {
+        $aliasdomains[] = $row['alias_domain'];
+      }
+    }
+    catch (PDOException $e) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => 'MySQL: '.$e
+      );
+      return false;
+    }
+  }
+  return $aliasdomains;
+}
+function mailbox_get_aliases($domain) {
+	global $lang;
+	global $pdo;
+  $aliases = array();
+	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+
+  try {
+    $stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `address` != `goto` AND `domain` = :domain");
+    $stmt->execute(array(
+      ':domain' => $domain,
+    ));
+    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    while($row = array_shift($rows)) {
+      $aliases[] = $row['address'];
+    }
+  }
+  catch (PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+  return $aliases;
+}
+function mailbox_get_alias_details($address) {
+	global $lang;
+	global $pdo;
+  $aliasdata = array();
+  try {
+    $stmt = $pdo->prepare("SELECT
+      `domain`,
+      `goto`,
+      `address`,
+      `active` as `active_int`,
+      CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`,
+      `created`,
+      `modified`
+        FROM `alias`
+            WHERE `address` = :address AND `address` != `goto`");
+    $stmt->execute(array(
+      ':address' => $address,
+    ));
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+    $aliasdata['domain'] = $row['domain'];
+    $aliasdata['goto'] = $row['goto'];
+    $aliasdata['address'] = $row['address'];
+    (!filter_var($aliasdata['address'], FILTER_VALIDATE_EMAIL)) ? $aliasdata['is_catch_all'] = 1 : $aliasdata['is_catch_all'] = 0;
+    $aliasdata['active'] = $row['active'];
+    $aliasdata['active_int'] = $row['active_int'];
+    $aliasdata['created'] = $row['created'];
+    $aliasdata['modified'] = $row['modified'];
+    if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $aliasdata['domain'])) {
+      $_SESSION['return'] = array(
+        'type' => 'danger',
+        'msg' => sprintf($lang['danger']['access_denied'])
+      );
+      return false;
+    }
+  }
+  catch (PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+  return $aliasdata;
+}
+function mailbox_get_alias_domain_details($aliasdomain) {
+	global $lang;
+	global $pdo;
+  $aliasdomaindata = array();
+  try {
+    $stmt = $pdo->prepare("SELECT
+      `alias_domain`,
+      `target_domain`,
+      `active` AS `active_int`,
+      CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`,
+      `created`,
+      `modified`
+        FROM `alias_domain`
+            WHERE `alias_domain` = :aliasdomain");
+    $stmt->execute(array(
+      ':aliasdomain' => $aliasdomain,
+    ));
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+    $aliasdomaindata['alias_domain'] = $row['alias_domain'];
+    $aliasdomaindata['target_domain'] = $row['target_domain'];
+    $aliasdomaindata['active'] = $row['active'];
+    $aliasdomaindata['active_int'] = $row['active_int'];
+    $aliasdomaindata['created'] = $row['created'];
+    $aliasdomaindata['modified'] = $row['modified'];
+  }
+  catch (PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+  if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $aliasdomaindata['target_domain'])) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  return $aliasdomaindata;
+}
+function mailbox_get_domains() {
+  // Get all domains assigned to mailcow_cc_username
+  // Domain admin needs to be active
+  // Domain does not need to be active
+	global $lang;
+	global $pdo;
+
+  try {
+    $domains = array();
+    $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
+      WHERE (`domain` IN (
+        SELECT `domain` from `domain_admins`
+          WHERE (`active`='1' AND `username` = :username))
+        )
+        OR ('admin'= :role)
+        AND `domain` != 'ALL'");
+    $stmt->execute(array(
+      ':username' => $_SESSION['mailcow_cc_username'],
+      ':role' => $_SESSION['mailcow_cc_role'],
+    ));
+    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    while($row = array_shift($rows)) {
+      $domains[] = $row['domain'];
+    }
+  }
+  catch (PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+  return $domains;
+}
+function mailbox_get_domain_details($domain) {
+	global $lang;
+	global $pdo;
+
+  $domaindata = array();
+	$domain = idn_to_ascii(strtolower(trim($domain)));
+
+	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+
+  try {
+    $stmt = $pdo->prepare("SELECT 
+        `domain`,
+        `description`,
+        `aliases`,
+        `mailboxes`, 
+        `maxquota`,
+        `quota`,
+        `relay_all_recipients` as `relay_all_recipients_int`,
+        `backupmx` as `backupmx_int`,
+        `active` as `active_int`,
+        CASE `relay_all_recipients` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `relay_all_recipients`,
+        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`= :domain");
+    $stmt->execute(array(
+      ':domain' => $domain,
+    ));
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+    $stmt = $pdo->prepare("SELECT COUNT(*) AS `count`, COALESCE(SUM(`quota`), 0) as `in_use` FROM `mailbox` WHERE `kind` NOT REGEXP 'location|thing|group' AND `domain` = :domain");
+    $stmt->execute(array(':domain' => $row['domain']));
+    $MailboxDataDomain	= $stmt->fetch(PDO::FETCH_ASSOC);
+
+    $domaindata['max_new_mailbox_quota']	= ($row['quota'] * 1048576) - $MailboxDataDomain['in_use'];
+    if ($domaindata['max_new_mailbox_quota'] > ($row['maxquota'] * 1048576)) {
+      $domaindata['max_new_mailbox_quota'] = ($row['maxquota'] * 1048576);
+    }
+    $domaindata['quota_used_in_domain'] = $MailboxDataDomain['in_use'];
+    $domaindata['mboxes_in_domain'] = $MailboxDataDomain['count'];
+    $domaindata['mboxes_left'] = $row['mailboxes']	- $MailboxDataDomain['count'];
+    $domaindata['domain_name'] = $row['domain'];
+    $domaindata['description'] = $row['description'];
+    $domaindata['max_num_aliases_for_domain'] = $row['aliases'];
+    $domaindata['max_num_mboxes_for_domain'] = $row['mailboxes'];
+    $domaindata['max_quota_for_mbox'] = $row['maxquota'] * 1048576;
+    $domaindata['max_quota_for_domain'] = $row['quota'] * 1048576;
+    $domaindata['backupmx'] = $row['backupmx'];
+    $domaindata['backupmx_int'] = $row['backupmx_int'];
+    $domaindata['active'] = $row['active'];
+    $domaindata['active_int'] = $row['active_int'];
+    $domaindata['relay_all_recipients'] = $row['relay_all_recipients'];
+    $domaindata['relay_all_recipients_int'] = $row['relay_all_recipients_int'];
+
+    $stmt = $pdo->prepare("SELECT COUNT(*) AS `alias_count` FROM `alias`
+      WHERE `domain`= :domain
+        AND `address` NOT IN (
+          SELECT `username` FROM `mailbox`
+        )");
+    $stmt->execute(array(
+      ':domain' => $domain,
+    ));
+    $AliasData = $stmt->fetch(PDO::FETCH_ASSOC);
+    (isset($AliasData['alias_count'])) ? $domaindata['aliases_in_domain'] = $AliasData['alias_count'] : $domaindata['aliases_in_domain'] = "0";
+  }
+  catch (PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+
+  return $domaindata;
+}
+function mailbox_get_mailbox_details($mailbox) {
+	global $lang;
+	global $pdo;
+  if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $mailbox)) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  $mailboxdata = array();
+  try {
+    $stmt = $pdo->prepare("SELECT
+        `domain`.`backupmx`,
+        `mailbox`.`username`,
+        `mailbox`.`name`,
+        `mailbox`.`active` AS `active_int`,
+        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`.`kind` NOT REGEXP 'location|thing|group' AND `mailbox`.`username` = `quota2`.`username` AND `domain`.`domain` = `mailbox`.`domain` AND `mailbox`.`username` = :mailbox");
+    $stmt->execute(array(
+      ':mailbox' => $mailbox,
+    ));
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+    $stmt = $pdo->prepare("SELECT `maxquota`, `quota` FROM  `domain` WHERE `domain` = :domain");
+    $stmt->execute(array(':domain' => $row['domain']));
+    $DomainQuota  = $stmt->fetch(PDO::FETCH_ASSOC);
+
+    $stmt = $pdo->prepare("SELECT COALESCE(SUM(`quota`), 0) as `in_use` FROM `mailbox` WHERE `kind` NOT REGEXP 'location|thing|group' AND `domain` = :domain AND `username` != :username");
+    $stmt->execute(array(':domain' => $row['domain'], ':username' => $row['username']));
+    $MailboxUsage	= $stmt->fetch(PDO::FETCH_ASSOC);
+
+    $mailboxdata['max_new_quota'] = ($DomainQuota['quota'] * 1048576) - $MailboxUsage['in_use'];
+    if ($mailboxdata['max_new_quota'] > ($DomainQuota['maxquota'] * 1048576)) {
+      $mailboxdata['max_new_quota'] = ($DomainQuota['maxquota'] * 1048576);
+    }
+
+    $mailboxdata['username'] = $row['username'];
+    $mailboxdata['is_relayed'] = $row['backupmx'];
+    $mailboxdata['name'] = $row['name'];
+    $mailboxdata['active'] = $row['active'];
+    $mailboxdata['active_int'] = $row['active_int'];
+    $mailboxdata['domain'] = $row['domain'];
+    $mailboxdata['quota'] = $row['quota'];
+    $mailboxdata['quota_used'] = intval($row['bytes']);
+    $mailboxdata['percent_in_use'] = round((intval($row['bytes']) / intval($row['quota'])) * 100);
+    $mailboxdata['messages'] = $row['messages'];
+    if ($mailboxdata['percent_in_use'] >= 90) {
+      $mailboxdata['percent_class'] = "danger";
+    }
+    elseif ($mailboxdata['percent_in_use'] >= 75) {
+      $mailboxdata['percent_class'] = "warning";
+    }
+    else {
+      $mailboxdata['percent_class'] = "success";
+    }
+  }
+  catch (PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+  return $mailboxdata;
+}
+function mailbox_get_resource_details($resource) {
+	global $lang;
+	global $pdo;
+  $resourcedata = array();
+  if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $resource)) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  try {
+    $stmt = $pdo->prepare("SELECT
+        `username`,
+        `name`,
+        `kind`,
+        `multiple_bookings` AS `multiple_bookings_int`,
+        `local_part`,
+        `active` AS `active_int`,
+        CASE `multiple_bookings` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `multiple_bookings`,
+        CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active`,
+        `domain`
+          FROM `mailbox` WHERE `kind` REGEXP 'location|thing|group' AND `username` = :resource");
+    $stmt->execute(array(
+      ':resource' => $resource,
+    ));
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+    $resourcedata['name'] = $row['username'];
+    $resourcedata['kind'] = $row['kind'];
+    $resourcedata['multiple_bookings'] = $row['multiple_bookings'];
+    $resourcedata['multiple_bookings_int'] = $row['multiple_bookings'];
+    $resourcedata['description'] = $row['name'];
+    $resourcedata['active'] = $row['active'];
+    $resourcedata['active_int'] = $row['active_int'];
+    $resourcedata['domain'] = $row['domain'];
+    $resourcedata['local_part'] = $row['local_part'];
+  }
+  catch (PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+  if (!isset($resourcedata['domain']) ||
+    (isset($resourcedata['domain']) && !hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $resourcedata['domain']))) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+  }
+  
+  return $resourcedata;
+}
+function mailbox_delete_domain($postarray) {
+	global $lang;
+	global $pdo;
+	$domain = $postarray['domain'];
+	if ($_SESSION['mailcow_cc_role'] != "admin") {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+	if (!is_valid_domain_name($domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['domain_invalid'])
+		);
+		return false;
+	}
+	$domain	= idn_to_ascii(strtolower(trim($domain)));
+
+	try {
+		$stmt = $pdo->prepare("SELECT `username` FROM `mailbox`
+			WHERE `domain` = :domain");
+		$stmt->execute(array(':domain' => $domain));
+		$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
+	}
+	catch(PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	if ($num_results != 0 || !empty($num_results)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['domain_not_empty'])
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("DELETE FROM `domain` WHERE `domain` = :domain");
+		$stmt->execute(array(
+			':domain' => $domain,
+		));
+		$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `domain` = :domain");
+		$stmt->execute(array(
+			':domain' => $domain,
+		));
+		$stmt = $pdo->prepare("DELETE FROM `alias` WHERE `domain` = :domain");
+		$stmt->execute(array(
+			':domain' => $domain,
+		));
+		$stmt = $pdo->prepare("DELETE FROM `alias_domain` WHERE `target_domain` = :domain");
+		$stmt->execute(array(
+			':domain' => $domain,
+		));
+		$stmt = $pdo->prepare("DELETE FROM `mailbox` WHERE `domain` = :domain");
+		$stmt->execute(array(
+			':domain' => $domain,
+		));
+		$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `logged_in_as` LIKE :domain");
+		$stmt->execute(array(
+			':domain' => '%@'.$domain,
+		));
+		$stmt = $pdo->prepare("DELETE FROM `quota2` WHERE `username` = :domain");
+		$stmt->execute(array(
+			':domain' => '%@'.$domain,
+		));
+		$stmt = $pdo->prepare("DELETE FROM `spamalias` WHERE `address` = :domain");
+		$stmt->execute(array(
+			':domain' => '%@'.$domain,
+		));
+		$stmt = $pdo->prepare("DELETE FROM `filterconf` WHERE `object` = :domain");
+		$stmt->execute(array(
+			':domain' => '%@'.$domain,
+		));
+	}
+	catch (PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	$_SESSION['return'] = array(
+		'type' => 'success',
+		'msg' => sprintf($lang['success']['domain_removed'], htmlspecialchars($domain))
+	);
+	return true;
+}
+function mailbox_delete_alias($postarray) {
+	global $lang;
+	global $pdo;
+	$address		= $postarray['address'];
+	$local_part		= strstr($address, '@', true);
+  $domain = mailbox_get_alias_details($address)['domain'];
+	try {
+		$stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :address");
+		$stmt->execute(array(':address' => $address));
+		$gotos = $stmt->fetch(PDO::FETCH_ASSOC);
+	}
+	catch(PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	$goto_array = explode(',', $gotos['goto']);
+
+	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+	try {
+		$stmt = $pdo->prepare("DELETE FROM `alias` WHERE `address` = :address AND `address` NOT IN (SELECT `username` FROM `mailbox`)");
+		$stmt->execute(array(
+			':address' => $address
+		));
+	}
+	catch (PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	$_SESSION['return'] = array(
+		'type' => 'success',
+		'msg' => sprintf($lang['success']['alias_removed'], htmlspecialchars($address))
+	);
+
+}
+function mailbox_delete_alias_domain($postarray) {
+	global $lang;
+	global $pdo;
+  $alias_domain = $postarray['alias_domain'];
+	if (!is_valid_domain_name($postarray['alias_domain'])) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['domain_invalid'])
+		);
+		return false;
+	}
+	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
+		);
+		return false;
+	}
+
+	if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $DomainData['target_domain'])) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("DELETE FROM `alias_domain` WHERE `alias_domain` = :alias_domain");
+		$stmt->execute(array(
+			':alias_domain' => $alias_domain,
+		));
+		$stmt = $pdo->prepare("DELETE FROM `alias` WHERE `domain` = :alias_domain");
+		$stmt->execute(array(
+			':alias_domain' => $alias_domain,
+		));
+	}
+	catch (PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	$_SESSION['return'] = array(
+		'type' => 'success',
+		'msg' => sprintf($lang['success']['alias_domain_removed'], htmlspecialchars($alias_domain))
+	);
+}
+function mailbox_delete_mailbox($postarray) {
+	global $lang;
+	global $pdo;
+	$username	= $postarray['username'];
+
+	if (!filter_var($postarray['username'], FILTER_VALIDATE_EMAIL)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+
+	if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("DELETE FROM `alias` WHERE `goto` = :username");
+		$stmt->execute(array(
+			':username' => $username
+		));
+		$stmt = $pdo->prepare("DELETE FROM `quota2` WHERE `username` = :username");
+		$stmt->execute(array(
+			':username' => $username
+		));
+		$stmt = $pdo->prepare("DELETE FROM `mailbox` WHERE `username` = :username");
+		$stmt->execute(array(
+			':username' => $username
+		));
+		$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `logged_in_as` = :username");
+		$stmt->execute(array(
+			':username' => $username
+		));
+		$stmt = $pdo->prepare("DELETE FROM `spamalias` WHERE `goto` = :username");
+		$stmt->execute(array(
+			':username' => $username
+		));
+		$stmt = $pdo->prepare("DELETE FROM `imapsync` WHERE `user2` = :username");
+		$stmt->execute(array(
+			':username' => $username
+		));
+		$stmt = $pdo->prepare("DELETE FROM `filterconf` WHERE `object` = :username");
+		$stmt->execute(array(
+			':username' => $username
+		));
+    $stmt = $pdo->prepare("DELETE FROM `sogo_user_profile` WHERE `c_uid` = :username");
+    $stmt->execute(array(
+      ':username' => $username
+    ));
+    $stmt = $pdo->prepare("DELETE FROM `sogo_cache_folder` WHERE `c_uid` = :username");
+    $stmt->execute(array(
+      ':username' => $username
+    ));
+    $stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE '%/" . $username . "/%' OR `c_uid` = :username");
+    $stmt->execute(array(
+      ':username' => $username
+    ));
+    $stmt = $pdo->prepare("DELETE FROM `sogo_store` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
+    $stmt->execute(array(
+      ':username' => $username
+    ));
+    $stmt = $pdo->prepare("DELETE FROM `sogo_quick_contact` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
+    $stmt->execute(array(
+      ':username' => $username
+    ));
+    $stmt = $pdo->prepare("DELETE FROM `sogo_quick_appointment` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
+    $stmt->execute(array(
+      ':username' => $username
+    ));
+    $stmt = $pdo->prepare("DELETE FROM `sogo_folder_info` WHERE `c_path2` = :username");
+    $stmt->execute(array(
+      ':username' => $username
+    ));
+		$stmt = $pdo->prepare("SELECT `address`, `goto` FROM `alias`
+				WHERE `goto` LIKE :username");
+		$stmt->execute(array(':username' => '%'.$username.'%'));
+		$GotoData = $stmt->fetchAll(PDO::FETCH_ASSOC);
+		foreach ($GotoData as $gotos) {
+			$goto_exploded = explode(',', $gotos['goto']);
+			if (($key = array_search($username, $goto_exploded)) !== false) {
+				unset($goto_exploded[$key]);
+			}
+			$gotos_rebuild = implode(',', $goto_exploded);
+			$stmt = $pdo->prepare("UPDATE `alias` SET
+        `goto` = :goto,
+        `modified` = :modified,
+          WHERE `address` = :address");
+			$stmt->execute(array(
+				':goto' => $gotos_rebuild,
+        ':modified' => date('Y-m-d H:i:s'),
+				':address' => $gotos['address']
+			));
+		}
+	}
+	catch (PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	$_SESSION['return'] = array(
+		'type' => 'success',
+		'msg' => sprintf($lang['success']['mailbox_removed'], htmlspecialchars($username))
+	);
+}
+function mailbox_reset_eas($username) {
+	global $lang;
+	global $pdo;
+
+  (isset($postarray['username'])) ? $username = $postarray['username'] : $username = $_SESSION['mailcow_cc_username'];
+
+	if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+
+	if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+
+	try {
+    $stmt = $pdo->prepare("DELETE FROM `sogo_cache_folder` WHERE `c_uid` = :username");
+    $stmt->execute(array(
+      ':username' => $username
+    ));
+	}
+	catch (PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	$_SESSION['return'] = array(
+		'type' => 'success',
+		'msg' => sprintf($lang['success']['eas_reset'], htmlspecialchars($username))
+	);
+}
+function mailbox_delete_resource($postarray) {
+	global $lang;
+	global $pdo;
+	$name	= $postarray['name'];
+	if (!filter_var($postarray['name'], FILTER_VALIDATE_EMAIL)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+
+	if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $name)) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => sprintf($lang['danger']['access_denied'])
+		);
+		return false;
+	}
+
+	try {
+		$stmt = $pdo->prepare("DELETE FROM `mailbox` WHERE `username` = :username");
+		$stmt->execute(array(
+			':username' => $name
+		));
+    $stmt = $pdo->prepare("DELETE FROM `sogo_user_profile` WHERE `c_uid` = :username");
+    $stmt->execute(array(
+      ':username' => $name
+    ));
+    $stmt = $pdo->prepare("DELETE FROM `sogo_cache_folder` WHERE `c_uid` = :username");
+    $stmt->execute(array(
+      ':username' => $name
+    ));
+    $stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE '%/" . $name . "/%' OR `c_uid` = :username");
+    $stmt->execute(array(
+      ':username' => $name
+    ));
+    $stmt = $pdo->prepare("DELETE FROM `sogo_store` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
+    $stmt->execute(array(
+      ':username' => $name
+    ));
+    $stmt = $pdo->prepare("DELETE FROM `sogo_quick_contact` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
+    $stmt->execute(array(
+      ':username' => $name
+    ));
+    $stmt = $pdo->prepare("DELETE FROM `sogo_quick_appointment` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
+    $stmt->execute(array(
+      ':username' => $name
+    ));
+    $stmt = $pdo->prepare("DELETE FROM `sogo_folder_info` WHERE `c_path2` = :username");
+    $stmt->execute(array(
+      ':username' => $name
+    ));
+	}
+	catch (PDOException $e) {
+		$_SESSION['return'] = array(
+			'type' => 'danger',
+			'msg' => 'MySQL: '.$e
+		);
+		return false;
+	}
+	$_SESSION['return'] = array(
+		'type' => 'success',
+		'msg' => sprintf($lang['success']['resource_removed'], htmlspecialchars($name))
+	);
+}
+function mailbox_get_sender_acl_handles($mailbox) {
+	global $pdo;
+	global $lang;
+	if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => sprintf($lang['danger']['access_denied'])
+    );
+    return false;
+	}
+
+  $data['sender_acl_domains']['ro']               = array();
+  $data['sender_acl_domains']['rw']               = array();
+  $data['sender_acl_domains']['selectable']       = array();
+  $data['sender_acl_addresses']['ro']             = array();
+  $data['sender_acl_addresses']['rw']             = array();
+  $data['sender_acl_addresses']['selectable']     = array();
+  $data['fixed_sender_aliases']                   = array();
+  
+  try {
+    $stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `goto` = :goto AND `address` NOT LIKE '@%'");
+    $stmt->execute(array(':goto' => $mailbox));
+    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    while ($row = array_shift($rows)) {
+      $data['fixed_sender_aliases'][] = $row['address'];
+    }
+
+    // Return array $data['sender_acl_domains/addresses']['ro'] with read-only objects
+    // Return array $data['sender_acl_domains/addresses']['rw'] with read-write objects (can be deleted)
+    $stmt = $pdo->prepare("SELECT REPLACE(`send_as`, '@', '') AS `send_as` FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as AND `send_as` LIKE '@%'");
+    $stmt->execute(array(':logged_in_as' => $mailbox));
+    $domain_rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    while ($domain_row = array_shift($domain_rows)) {
+      if (is_valid_domain_name($domain_row['send_as']) && !hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain_row['send_as'])) {
+        $data['sender_acl_domains']['ro'][] = $domain_row['send_as'];
+        continue;
+      }
+      if (is_valid_domain_name($domain_row['send_as']) && hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain_row['send_as'])) {
+        $data['sender_acl_domains']['rw'][] = $domain_row['send_as'];
+        continue;
+      }
+    }
+
+    $stmt = $pdo->prepare("SELECT `send_as` FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as AND `send_as` NOT LIKE '@%'");
+    $stmt->execute(array(':logged_in_as' => $mailbox));
+    $address_rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    while ($address_row = array_shift($address_rows)) {
+      if (filter_var($address_row['send_as'], FILTER_VALIDATE_EMAIL) && !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $address_row['send_as'])) {
+        $data['sender_acl_addresses']['ro'][] = $address_row['send_as'];
+        continue;
+      }
+      if (filter_var($address_row['send_as'], FILTER_VALIDATE_EMAIL) && hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $address_row['send_as'])) {
+        $data['sender_acl_addresses']['rw'][] = $address_row['send_as'];
+        continue;
+      }
+    }
+
+    $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
+      WHERE `domain` NOT IN (
+        SELECT REPLACE(`send_as`, '@', '') FROM `sender_acl` 
+          WHERE `logged_in_as` = :logged_in_as
+            AND `send_as` LIKE '@%')");
+    $stmt->execute(array(
+      ':logged_in_as' => $mailbox,
+    ));
+    $rows_domain = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    while ($row_domain = array_shift($rows_domain)) {
+      if (is_valid_domain_name($row_domain['domain']) && hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row_domain['domain'])) {
+        $data['sender_acl_domains']['selectable'][] = $row_domain['domain'];
+      }
+    }
+
+    $stmt = $pdo->prepare("SELECT `address` FROM `alias`
+      WHERE `goto` != :goto
+        AND `address` NOT IN (
+          SELECT `send_as` FROM `sender_acl` 
+            WHERE `logged_in_as` = :logged_in_as
+              AND `send_as` NOT LIKE '@%')");
+    $stmt->execute(array(
+      ':logged_in_as' => $mailbox,
+      ':goto' => $mailbox
+    ));
+    $rows_mbox = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    while ($row = array_shift($rows_mbox)) {
+      if (filter_var($row['address'], FILTER_VALIDATE_EMAIL) && hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['address'])) {
+        $data['sender_acl_addresses']['selectable'][] = $row['address'];
+      }
+    }
+  }
+  catch(PDOException $e) {
+    $_SESSION['return'] = array(
+      'type' => 'danger',
+      'msg' => 'MySQL: '.$e
+    );
+    return false;
+  }
+  return $data;
+}
+function get_u2f_registrations($username) {
+  global $pdo;
+  $sel = $pdo->prepare("SELECT * FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = ? AND `active` = '1'");
+  $sel->execute(array($username));
+  return $sel->fetchAll(PDO::FETCH_OBJ);
+}
+?>

+ 104 - 0
data/web/inc/header.inc.php

@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<html lang="<?= $_SESSION['mailcow_locale'] ?>">
+<head>
+<meta charset="utf-8">
+<meta http-equiv="X-UA-Compatible" content="IE=edge">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>mailcow UI</title>
+<!--[if lt IE 9]>
+<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
+<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
+<![endif]-->
+<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.0/jquery.min.js" integrity="sha384-XxcvoeNF5V0ZfksTnV+bejnCsJjOOIzN6UVwF85WBsAnU3zeYh5bloN+L4WLgeNE" crossorigin="anonymous"></script>
+<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/css/bootstrap.min.css">
+<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/bootswatch/3.3.6/<?=strtolower(trim($DEFAULT_THEME));?>/bootstrap.min.css">
+<link rel="stylesheet" href="/css/bootstrap-select.min.css">
+<link rel="stylesheet" href="/css/bootstrap-slider.min.css">
+<link rel="stylesheet" href="/css/bootstrap-switch.min.css">
+<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Source+Sans+Pro:400,600,700&subset=latin,latin-ext">
+<link rel="stylesheet" href="/inc/languages.min.css">
+<link rel="stylesheet" href="/css/mailcow.css">
+<link rel="stylesheet" href="/css/tables.css">
+<?=(preg_match("/mailbox.php/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/mailbox.css">' : null;?>
+<link rel="shortcut icon" href="/favicon.png" type="image/png">
+<link rel="icon" href="/favicon.png" type="image/png">
+</head>
+<body style="padding-top:70px">
+<nav class="navbar navbar-default navbar-fixed-top"  role="navigation">
+	<div class="container-fluid">
+		<div class="navbar-header">
+			<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
+				<span class="sr-only">Toggle navigation</span>
+				<span class="icon-bar"></span>
+				<span class="icon-bar"></span>
+				<span class="icon-bar"></span>
+			</button>
+			<a class="navbar-brand" href="/"><img height="32" alt="mailcow-logo" style="margin-top:-5px;" src="/img/cow_mailcow.svg" /></a>
+		</div>
+		<div id="navbar" class="navbar-collapse collapse">
+			<ul class="nav navbar-nav navbar-right">
+				<?php
+				if (isset($_SESSION['mailcow_locale'])) {
+				?>
+				<li class="dropdown">
+					<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><span class="lang-sm lang-lbl" lang="<?=$_SESSION['mailcow_locale'];?>"></span><span class="caret"></span></a>
+					<ul class="dropdown-menu" role="menu">
+						<li <?=($_SESSION['mailcow_locale'] == 'de') ? 'class="active"' : ''?>> <a href="?<?= http_build_query(array_merge($_GET, array("lang" => "de"))) ?>"><span class="lang-xs lang-lbl-full" lang="de"></span></a></li>
+						<li <?=($_SESSION['mailcow_locale'] == 'en') ? 'class="active"' : ''?>> <a href="?<?= http_build_query(array_merge($_GET, array("lang" => "en"))) ?>"><span class="lang-xs lang-lbl-full" lang="en"></span></a></li>
+						<li <?=($_SESSION['mailcow_locale'] == 'es') ? 'class="active"' : ''?>> <a href="?<?= http_build_query(array_merge($_GET, array("lang" => "es"))) ?>"><span class="lang-xs lang-lbl-full" lang="es"></span></a></li>
+						<li <?=($_SESSION['mailcow_locale'] == 'nl') ? 'class="active"' : ''?>> <a href="?<?= http_build_query(array_merge($_GET, array("lang" => "nl"))) ?>"><span class="lang-xs lang-lbl-full" lang="nl"></span></a></li>
+						<li <?=($_SESSION['mailcow_locale'] == 'pt') ? 'class="active"' : ''?>> <a href="?<?= http_build_query(array_merge($_GET, array("lang" => "pt"))) ?>"><span class="lang-xs lang-lbl-full" lang="pt"></span></a></li>
+					</ul>
+				</li>
+				<?php
+				}
+				if (isset($_SESSION['mailcow_cc_role'])) {
+				?>
+				<li class="dropdown">
+					<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><?=$lang['header']['mailcow_settings'];?><span class="caret"></span></a>
+					<ul class="dropdown-menu" role="menu">
+					<?php
+						if (isset($_SESSION['mailcow_cc_role'])) {
+							if ($_SESSION['mailcow_cc_role'] == "admin") {
+							?>
+								<li <?=(preg_match("/admin/i", $_SERVER['REQUEST_URI'])) ? 'class="active"' : ''?>><a href="/admin.php"><?=$lang['header']['administration'];?></a></li>
+							<?php
+							}
+							if ($_SESSION['mailcow_cc_role'] == "admin" || $_SESSION['mailcow_cc_role'] == "domainadmin") {
+							?>
+								<li <?=(preg_match("/mailbox/i", $_SERVER['REQUEST_URI'])) ? 'class="active"' : ''?>><a href="/mailbox.php"><?=$lang['header']['mailboxes'];?></a></li>
+							<?php
+							}
+							if ($_SESSION['mailcow_cc_role'] != "admin") {
+							?>
+								<li <?=(preg_match("/user/i", $_SERVER['REQUEST_URI'])) ? 'class="active"' : ''?>><a href="/user.php"><?=$lang['header']['user_settings'];?></a></li>
+							<?php
+              }
+						}
+						?>
+					</ul>
+				</li>
+				<?php
+				if ($_SESSION['mailcow_cc_role'] == "admin"):
+				?>
+				<li><a href data-toggle="modal" data-target="#RestartSOGo"><span style="font-size:12px" class="glyphicon glyphicon-refresh" aria-hidden="true"></span> <?=$lang['header']['restart_sogo'];?></a></li>
+				<?php
+				endif;
+				?>
+					<?php
+				}
+				if (!isset($_SESSION["dual-login"]) && isset($_SESSION['mailcow_cc_username'])):
+				?>
+					<li><a style="border-left:1px solid #E7E7E7" href="#" onclick="logout.submit()"><?=sprintf($lang['header']['logged_in_as_logout'], $_SESSION['mailcow_cc_username']);?></a></li>
+				<?php
+				elseif (isset($_SESSION["dual-login"])):
+				?>
+					<li><a style="border-left:1px solid #E7E7E7" href="#" onclick="logout.submit()"><?=sprintf($lang['header']['logged_in_as_logout_dual'], $_SESSION['mailcow_cc_username'], $_SESSION["dual-login"]["username"]);?></a></li>
+				<?php
+				endif;
+				?>
+			</ul>
+		</div><!--/.nav-collapse -->
+	</div><!--/.container-fluid -->
+</nav>
+<form action="/" method="post" id="logout"><input type="hidden" name="logout"></form>

+ 281 - 0
data/web/inc/init.sql

@@ -0,0 +1,281 @@
+CREATE TABLE IF NOT EXISTS `admin` (
+	`username` VARCHAR(255) NOT NULL,
+	`password` VARCHAR(255) NOT NULL,
+	`superadmin` TINYINT(1) NOT NULL DEFAULT '0',
+	`created` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00',
+	`modified` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00',
+	`active` TINYINT(1) NOT NULL DEFAULT '1',
+	PRIMARY KEY (`username`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE IF NOT EXISTS `alias` (
+	`address` VARCHAR(255) NOT NULL,
+	`goto` TEXT NOT NULL,
+	`domain` VARCHAR(255) NOT NULL,
+	`created` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00',
+	`modified` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00',
+	`active` TINYINT(1) NOT NULL DEFAULT '1',
+	PRIMARY KEY (`address`),
+	KEY `domain` (`domain`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE IF NOT EXISTS `sender_acl` (
+	`logged_in_as` VARCHAR(255) NOT NULL,
+	`send_as` VARCHAR(255) NOT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE IF NOT EXISTS `spamalias` (
+	`address` VARCHAR(255) NOT NULL,
+	`goto` TEXT NOT NULL,
+	`validity` INT(11) NOT NULL,
+	PRIMARY KEY (`address`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE IF NOT EXISTS `alias_domain` (
+	`alias_domain` VARCHAR(255) NOT NULL,
+	`target_domain` VARCHAR(255) NOT NULL,
+	`created` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00',
+	`modified` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00',
+	`active` TINYINT(1) NOT NULL DEFAULT '1',
+	PRIMARY KEY (`alias_domain`),
+	KEY `active` (`active`),
+	KEY `target_domain` (`target_domain`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE IF NOT EXISTS `domain` (
+	`domain` VARCHAR(255) NOT NULL,
+	`description` VARCHAR(255),
+	`aliases` INT(10) NOT NULL DEFAULT '0',
+	`mailboxes` INT(10) NOT NULL DEFAULT '0',
+	`maxquota` BIGINT(20) NOT NULL DEFAULT '0',
+	`quota` BIGINT(20) NOT NULL DEFAULT '0',
+	`transport` VARCHAR(255) NOT NULL,
+	`backupmx` TINYINT(1) NOT NULL DEFAULT '0',
+	`relay_all_recipients` TINYINT(1) NOT NULL DEFAULT '0',
+	`created` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00',
+	`modified` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00',
+	`active` TINYINT(1) NOT NULL DEFAULT '1',
+	PRIMARY KEY (`domain`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE IF NOT EXISTS `domain_admins` (
+	`username` VARCHAR(255) NOT NULL,
+	`domain` VARCHAR(255) NOT NULL,
+	`created` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00',
+	`active` TINYINT(1) NOT NULL DEFAULT '1',
+	KEY `username` (`username`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE IF NOT EXISTS `mailbox` (
+	`username` VARCHAR(255) NOT NULL,
+	`password` VARCHAR(255) NOT NULL,
+	`name` VARCHAR(255),
+	`maildir` VARCHAR(255) NOT NULL,
+	`quota` BIGINT(20) NOT NULL DEFAULT '0',
+	`local_part` VARCHAR(255) NOT NULL,
+	`domain` VARCHAR(255) NOT NULL,
+	`created` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00',
+	`modified` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00',
+	`tls_enforce_in` TINYINT(1) NOT NULL DEFAULT '0',
+	`tls_enforce_out` TINYINT(1) NOT NULL DEFAULT '0',
+	`kind` VARCHAR(100) NOT NULL DEFAULT '',
+	`multiple_bookings` TINYINT(1) NOT NULL DEFAULT '0',
+	`wants_tagged_subject` TINYINT(1) NOT NULL DEFAULT '0',
+	`active` TINYINT(1) NOT NULL DEFAULT '1',
+	PRIMARY KEY (`username`),
+	KEY `domain` (`domain`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE IF NOT EXISTS `quota2` (
+	`username` VARCHAR(100) NOT NULL,
+	`bytes` BIGINT(20) NOT NULL DEFAULT '0',
+	`messages` INT(11) NOT NULL DEFAULT '0',
+	PRIMARY KEY (`username`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE IF NOT EXISTS `filterconf` (
+	`object` VARCHAR(100) NOT NULL DEFAULT '',
+	`option` VARCHAR(50) NOT NULL DEFAULT '',
+	`value` VARCHAR(100) NOT NULL DEFAULT '',
+	`prefid` INT(11) NOT NULL AUTO_INCREMENT,
+	PRIMARY KEY (`prefid`),
+	KEY `object` (`object`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE IF NOT EXISTS `imapsync` (
+  `id` INT NOT NULL AUTO_INCREMENT,
+  `user2` VARCHAR(255) NOT NULL,
+  `host1` VARCHAR(255) NOT NULL,
+  `authmech1` ENUM('PLAIN','LOGIN','CRAM-MD5') DEFAULT 'PLAIN',
+  `regextrans2` VARCHAR(255) DEFAULT '',
+  `authmd51` TINYINT(1) NOT NULL DEFAULT 0,
+  `domain2` VARCHAR(255) NOT NULL DEFAULT '',
+  `subfolder2` VARCHAR(255) NOT NULL DEFAULT '',
+  `user1` VARCHAR(255) NOT NULL,
+  `password1` VARCHAR(255) NOT NULL,
+  `exclude` VARCHAR(500) NOT NULL DEFAULT '',
+  `maxage` SMALLINT NOT NULL DEFAULT '0',
+  `mins_interval` VARCHAR(50) NOT NULL,
+  `port1` SMALLINT NOT NULL,
+  `enc1` ENUM('TLS','SSL','PLAIN') DEFAULT 'TLS',
+  `delete2duplicates` TINYINT(1) NOT NULL DEFAULT '1',
+  `returned_text` TEXT,
+  `last_run` TIMESTAMP NULL DEFAULT NULL,
+  `created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `modified` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  `active` TINYINT(1) NOT NULL DEFAULT '0',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE IF NOT EXISTS `tfa` (
+  `id` INT NOT NULL AUTO_INCREMENT,
+  `username` VARCHAR(255) NOT NULL,
+  `authmech` ENUM('yubi_otp', 'u2f', 'hotp', 'totp'),
+  `secret` VARCHAR(255) DEFAULT NULL,
+  `keyHandle` VARCHAR(255) DEFAULT NULL,
+  `publicKey` VARCHAR(255) DEFAULT NULL,
+  `counter` INT NOT NULL DEFAULT '0',
+  `certificate` TEXT,
+  `active` TINYINT(1) NOT NULL DEFAULT '0',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
+
+DROP VIEW IF EXISTS grouped_mail_aliases;
+DROP VIEW IF EXISTS grouped_sender_acl;
+DROP VIEW IF EXISTS grouped_domain_alias_address;
+
+CREATE VIEW grouped_mail_aliases (username, aliases) AS
+SELECT goto, IFNULL(GROUP_CONCAT(address SEPARATOR ' '), '') AS address FROM alias
+WHERE address!=goto
+AND active = '1'
+AND address NOT LIKE '@%'
+GROUP BY goto;
+
+CREATE VIEW grouped_sender_acl (username, send_as) AS
+SELECT logged_in_as, IFNULL(GROUP_CONCAT(send_as SEPARATOR ' '), '') AS send_as FROM sender_acl
+WHERE send_as NOT LIKE '@%'
+GROUP BY logged_in_as;
+
+CREATE VIEW grouped_domain_alias_address (username, ad_alias) AS
+SELECT username, IFNULL(GROUP_CONCAT(local_part, '@', alias_domain SEPARATOR ' '), '') AS ad_alias FROM mailbox
+LEFT OUTER JOIN alias_domain on target_domain=domain GROUP BY username;
+
+CREATE TABLE IF NOT EXISTS sogo_acl (
+	c_folder_id INTEGER NOT NULL,
+	c_object character varying(255) NOT NULL,
+	c_uid character varying(255) NOT NULL,
+	c_role character varying(80) NOT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE IF NOT EXISTS sogo_alarms_folder (
+	c_path          VARCHAR(255) NOT NULL,
+	c_name          VARCHAR(255) NOT NULL,
+	c_uid           VARCHAR(255) NOT NULL,
+	c_recurrence_id INT(11)      DEFAULT NULL,
+	c_alarm_number  INT(11)      NOT NULL,
+	c_alarm_date    INT(11)      NOT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE IF NOT EXISTS sogo_cache_folder (
+	c_uid          VARCHAR(255) NOT NULL,
+	c_path         VARCHAR(255) NOT NULL,
+	c_parent_path  VARCHAR(255) DEFAULT NULL,
+	c_type         TINYINT(3)   unsigned NOT NULL,
+	c_creationdate INT(11)      NOT NULL,
+	c_lastmodified INT(11)      NOT NULL,
+	c_version      INT(11)      NOT NULL DEFAULT '0',
+	c_deleted      TINYINT(4)   NOT NULL DEFAULT '0',
+	c_content      longTEXT,
+	PRIMARY KEY (c_uid,c_path)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE IF NOT EXISTS sogo_folder_info (
+	c_folder_id      BIGINT(20)    unsigned NOT NULL AUTO_INCREMENT,
+	c_path           VARCHAR(255)  NOT NULL,
+	c_path1          VARCHAR(255)  NOT NULL,
+	c_path2          VARCHAR(255)  DEFAULT NULL,
+	c_path3          VARCHAR(255)  DEFAULT NULL,
+	c_path4          VARCHAR(255)  DEFAULT NULL,
+	c_foldername     VARCHAR(255)  NOT NULL,
+	c_location       INTeger NULL,
+	c_quick_location VARCHAR(2048) DEFAULT NULL,
+	c_acl_location   VARCHAR(2048) DEFAULT NULL,
+	c_folder_type    VARCHAR(255)  NOT NULL,
+	PRIMARY KEY (c_path),
+	UNIQUE KEY c_folder_id (c_folder_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE IF NOT EXISTS sogo_quick_appointment (
+	c_folder_id INTeger NOT NULL,
+	c_name character varying(255) NOT NULL,
+	c_uid character varying(255) NOT NULL,
+	c_startdate INTeger,
+	c_enddate INTeger,
+	c_cycleenddate INTeger,
+	c_title character varying(1000) NOT NULL,
+	c_participants TEXT,
+	c_isallday INTeger,
+	c_iscycle INTeger,
+	c_cycleinfo TEXT,
+	c_classification INTeger NOT NULL,
+	c_isopaque INTeger NOT NULL,
+	c_status INTeger NOT NULL,
+	c_priority INTeger,
+	c_location character varying(255),
+	c_orgmail character varying(255),
+	c_partmails TEXT,
+	c_partstates TEXT,
+	c_category character varying(255),
+	c_sequence INTeger,
+	c_component character varying(10) NOT NULL,
+	c_nextalarm INTeger,
+	c_description TEXT,
+	CONSTRAINT sogo_quick_appointment_pkey PRIMARY KEY (c_folder_id, c_name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE IF NOT EXISTS sogo_quick_contact (
+	c_folder_id INTeger NOT NULL,
+	c_name character varying(255) NOT NULL,
+	c_givenname character varying(255),
+	c_cn character varying(255),
+	c_sn character varying(255),
+	c_screenname character varying(255),
+	c_l character varying(255),
+	c_mail character varying(255),
+	c_o character varying(255),
+	c_ou character varying(255),
+	c_telephonenumber character varying(255),
+	c_categories character varying(255),
+	c_component character varying(10) NOT NULL,
+	CONSTRAINT sogo_quick_contact_pkey PRIMARY KEY (c_folder_id, c_name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE IF NOT EXISTS sogo_sessions_folder (
+	c_id           VARCHAR(255) NOT NULL,
+	c_value        VARCHAR(255) NOT NULL,
+	c_creationdate INT(11)      NOT NULL,
+	c_lastseen     INT(11)      NOT NULL,
+	PRIMARY KEY (c_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE IF NOT EXISTS sogo_store (
+	c_folder_id INTeger NOT NULL,
+	c_name character varying(255) NOT NULL,
+	c_content mediumTEXT NOT NULL,
+	c_creationdate INTeger NOT NULL,
+	c_lastmodified INTeger NOT NULL,
+	c_version INTeger NOT NULL,
+	c_deleted INTeger,
+	CONSTRAINT sogo_store_pkey PRIMARY KEY (c_folder_id, c_name)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
+
+CREATE TABLE IF NOT EXISTS sogo_user_profile (
+	c_uid      VARCHAR(255) NOT NULL,
+	c_defaults TEXT,
+	c_settings TEXT,
+	PRIMARY KEY (c_uid)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
+
+INSERT INTO `admin` (username, password, superadmin, created, modified, active) SELECT 'admin', '{SSHA256}K8eVJ6YsZbQCfuJvSUbaQRLr0HPLz5rC9IAp0PAFl0tmNDBkMDc0NDAyOTAxN2Rk', 1, NOW(), NOW(), 1 WHERE NOT EXISTS (SELECT * FROM `admin`);
+DELETE FROM `domain_admins`;
+INSERT INTO `domain_admins` (username, domain, created, active) SELECT `username`, 'ALL', NOW(), 1 FROM `admin` WHERE superadmin='1' AND `username` NOT IN (SELECT `username` FROM `domain_admins`);

文件差异内容过多而无法显示
+ 0 - 0
data/web/inc/languages.min.css


二进制
data/web/inc/languages.png


+ 506 - 0
data/web/inc/lib/U2F.php

@@ -0,0 +1,506 @@
+<?php
+/* Copyright (c) 2014 Yubico AB
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *   * Redistributions of source code must retain the above copyright
+ *     notice, this list of conditions and the following disclaimer.
+ *
+ *   * Redistributions in binary form must reproduce the above
+ *     copyright notice, this list of conditions and the following
+ *     disclaimer in the documentation and/or other materials provided
+ *     with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+namespace u2flib_server;
+
+/** Constant for the version of the u2f protocol */
+const U2F_VERSION = "U2F_V2";
+
+/** Error for the authentication message not matching any outstanding
+ * authentication request */
+const ERR_NO_MATCHING_REQUEST = 1;
+
+/** Error for the authentication message not matching any registration */
+const ERR_NO_MATCHING_REGISTRATION = 2;
+
+/** Error for the signature on the authentication message not verifying with
+ * the correct key */
+const ERR_AUTHENTICATION_FAILURE = 3;
+
+/** Error for the challenge in the registration message not matching the
+ * registration challenge */
+const ERR_UNMATCHED_CHALLENGE = 4;
+
+/** Error for the attestation signature on the registration message not
+ * verifying */
+const ERR_ATTESTATION_SIGNATURE = 5;
+
+/** Error for the attestation verification not verifying */
+const ERR_ATTESTATION_VERIFICATION = 6;
+
+/** Error for not getting good random from the system */
+const ERR_BAD_RANDOM = 7;
+
+/** Error when the counter is lower than expected */
+const ERR_COUNTER_TOO_LOW = 8;
+
+/** Error decoding public key */
+const ERR_PUBKEY_DECODE = 9;
+
+/** Error user-agent returned error */
+const ERR_BAD_UA_RETURNING = 10;
+
+/** Error old OpenSSL version */
+const ERR_OLD_OPENSSL = 11;
+
+/** @internal */
+const PUBKEY_LEN = 65;
+
+class U2F
+{
+    /** @var string  */
+    private $appId;
+
+    /** @var null|string */
+    private $attestDir;
+
+    /** @internal */
+    private $FIXCERTS = array(
+        '349bca1031f8c82c4ceca38b9cebf1a69df9fb3b94eed99eb3fb9aa3822d26e8',
+        'dd574527df608e47ae45fbba75a2afdd5c20fd94a02419381813cd55a2a3398f',
+        '1d8764f0f7cd1352df6150045c8f638e517270e8b5dda1c63ade9c2280240cae',
+        'd0edc9a91a1677435a953390865d208c55b3183c6759c9b5a7ff494c322558eb',
+        '6073c436dcd064a48127ddbf6032ac1a66fd59a0c24434f070d4e564c124c897',
+        'ca993121846c464d666096d35f13bf44c1b05af205f9b4a1e00cf6cc10c5e511'
+    );
+
+    /**
+     * @param string $appId Application id for the running application
+     * @param string|null $attestDir Directory where trusted attestation roots may be found
+     * @throws Error If OpenSSL older than 1.0.0 is used
+     */
+    public function __construct($appId, $attestDir = null)
+    {
+        if(OPENSSL_VERSION_NUMBER < 0x10000000) {
+            throw new Error('OpenSSL has to be at least version 1.0.0, this is ' . OPENSSL_VERSION_TEXT, ERR_OLD_OPENSSL);
+        }
+        $this->appId = $appId;
+        $this->attestDir = $attestDir;
+    }
+
+    /**
+     * Called to get a registration request to send to a user.
+     * Returns an array of one registration request and a array of sign requests.
+     *
+     * @param array $registrations List of current registrations for this
+     * user, to prevent the user from registering the same authenticator several
+     * times.
+     * @return array An array of two elements, the first containing a
+     * RegisterRequest the second being an array of SignRequest
+     * @throws Error
+     */
+    public function getRegisterData(array $registrations = array())
+    {
+        $challenge = $this->createChallenge();
+        $request = new RegisterRequest($challenge, $this->appId);
+        $signs = $this->getAuthenticateData($registrations);
+        return array($request, $signs);
+    }
+
+    /**
+     * Called to verify and unpack a registration message.
+     *
+     * @param RegisterRequest $request this is a reply to
+     * @param object $response response from a user
+     * @param bool $includeCert set to true if the attestation certificate should be
+     * included in the returned Registration object
+     * @return Registration
+     * @throws Error
+     */
+    public function doRegister($request, $response, $includeCert = true)
+    {
+        if( !is_object( $request ) ) {
+            throw new \InvalidArgumentException('$request of doRegister() method only accepts object.');
+        }
+
+        if( !is_object( $response ) ) {
+            throw new \InvalidArgumentException('$response of doRegister() method only accepts object.');
+        }
+
+        if( property_exists( $response, 'errorCode') && $response->errorCode !== 0 ) {
+            throw new Error('User-agent returned error. Error code: ' . $response->errorCode, ERR_BAD_UA_RETURNING );
+        }
+
+        if( !is_bool( $includeCert ) ) {
+            throw new \InvalidArgumentException('$include_cert of doRegister() method only accepts boolean.');
+        }
+
+        $rawReg = $this->base64u_decode($response->registrationData);
+        $regData = array_values(unpack('C*', $rawReg));
+        $clientData = $this->base64u_decode($response->clientData);
+        $cli = json_decode($clientData);
+
+        if($cli->challenge !== $request->challenge) {
+            throw new Error('Registration challenge does not match', ERR_UNMATCHED_CHALLENGE );
+        }
+
+        $registration = new Registration();
+        $offs = 1;
+        $pubKey = substr($rawReg, $offs, PUBKEY_LEN);
+        $offs += PUBKEY_LEN;
+        // decode the pubKey to make sure it's good
+        $tmpKey = $this->pubkey_to_pem($pubKey);
+        if($tmpKey === null) {
+            throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE );
+        }
+        $registration->publicKey = base64_encode($pubKey);
+        $khLen = $regData[$offs++];
+        $kh = substr($rawReg, $offs, $khLen);
+        $offs += $khLen;
+        $registration->keyHandle = $this->base64u_encode($kh);
+
+        // length of certificate is stored in byte 3 and 4 (excluding the first 4 bytes)
+        $certLen = 4;
+        $certLen += ($regData[$offs + 2] << 8);
+        $certLen += $regData[$offs + 3];
+
+        $rawCert = $this->fixSignatureUnusedBits(substr($rawReg, $offs, $certLen));
+        $offs += $certLen;
+        $pemCert  = "-----BEGIN CERTIFICATE-----\r\n";
+        $pemCert .= chunk_split(base64_encode($rawCert), 64);
+        $pemCert .= "-----END CERTIFICATE-----";
+        if($includeCert) {
+            $registration->certificate = base64_encode($rawCert);
+        }
+        if($this->attestDir) {
+            if(openssl_x509_checkpurpose($pemCert, -1, $this->get_certs()) !== true) {
+                throw new Error('Attestation certificate can not be validated', ERR_ATTESTATION_VERIFICATION );
+            }
+        }
+
+        if(!openssl_pkey_get_public($pemCert)) {
+            throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE );
+        }
+        $signature = substr($rawReg, $offs);
+
+        $dataToVerify  = chr(0);
+        $dataToVerify .= hash('sha256', $request->appId, true);
+        $dataToVerify .= hash('sha256', $clientData, true);
+        $dataToVerify .= $kh;
+        $dataToVerify .= $pubKey;
+
+        if(openssl_verify($dataToVerify, $signature, $pemCert, 'sha256') === 1) {
+            return $registration;
+        } else {
+            throw new Error('Attestation signature does not match', ERR_ATTESTATION_SIGNATURE );
+        }
+    }
+
+    /**
+     * Called to get an authentication request.
+     *
+     * @param array $registrations An array of the registrations to create authentication requests for.
+     * @return array An array of SignRequest
+     * @throws Error
+     */
+    public function getAuthenticateData(array $registrations)
+    {
+        $sigs = array();
+        foreach ($registrations as $reg) {
+            if( !is_object( $reg ) ) {
+                throw new \InvalidArgumentException('$registrations of getAuthenticateData() method only accepts array of object.');
+            }
+
+            $sig = new SignRequest();
+            $sig->appId = $this->appId;
+            $sig->keyHandle = $reg->keyHandle;
+            $sig->challenge = $this->createChallenge();
+            $sigs[] = $sig;
+        }
+        return $sigs;
+    }
+
+    /**
+     * Called to verify an authentication response
+     *
+     * @param array $requests An array of outstanding authentication requests
+     * @param array $registrations An array of current registrations
+     * @param object $response A response from the authenticator
+     * @return Registration
+     * @throws Error
+     *
+     * The Registration object returned on success contains an updated counter
+     * that should be saved for future authentications.
+     * If the Error returned is ERR_COUNTER_TOO_LOW this is an indication of
+     * token cloning or similar and appropriate action should be taken.
+     */
+    public function doAuthenticate(array $requests, array $registrations, $response)
+    {
+        if( !is_object( $response ) ) {
+            throw new \InvalidArgumentException('$response of doAuthenticate() method only accepts object.');
+        }
+
+        if( property_exists( $response, 'errorCode') && $response->errorCode !== 0 ) {
+            throw new Error('User-agent returned error. Error code: ' . $response->errorCode, ERR_BAD_UA_RETURNING );
+        }
+
+        /** @var object|null $req */
+        $req = null;
+
+        /** @var object|null $reg */
+        $reg = null;
+
+        $clientData = $this->base64u_decode($response->clientData);
+        $decodedClient = json_decode($clientData);
+        foreach ($requests as $req) {
+            if( !is_object( $req ) ) {
+                throw new \InvalidArgumentException('$requests of doAuthenticate() method only accepts array of object.');
+            }
+
+            if($req->keyHandle === $response->keyHandle && $req->challenge === $decodedClient->challenge) {
+                break;
+            }
+
+            $req = null;
+        }
+        if($req === null) {
+            throw new Error('No matching request found', ERR_NO_MATCHING_REQUEST );
+        }
+        foreach ($registrations as $reg) {
+            if( !is_object( $reg ) ) {
+                throw new \InvalidArgumentException('$registrations of doAuthenticate() method only accepts array of object.');
+            }
+
+            if($reg->keyHandle === $response->keyHandle) {
+                break;
+            }
+            $reg = null;
+        }
+        if($reg === null) {
+            throw new Error('No matching registration found', ERR_NO_MATCHING_REGISTRATION );
+        }
+        $pemKey = $this->pubkey_to_pem($this->base64u_decode($reg->publicKey));
+        if($pemKey === null) {
+            throw new Error('Decoding of public key failed', ERR_PUBKEY_DECODE );
+        }
+
+        $signData = $this->base64u_decode($response->signatureData);
+        $dataToVerify  = hash('sha256', $req->appId, true);
+        $dataToVerify .= substr($signData, 0, 5);
+        $dataToVerify .= hash('sha256', $clientData, true);
+        $signature = substr($signData, 5);
+
+        if(openssl_verify($dataToVerify, $signature, $pemKey, 'sha256') === 1) {
+            $ctr = unpack("Nctr", substr($signData, 1, 4));
+            $counter = $ctr['ctr'];
+            /* TODO: wrap-around should be handled somehow.. */
+            if($counter > $reg->counter) {
+                $reg->counter = $counter;
+                return $reg;
+            } else {
+                throw new Error('Counter too low.', ERR_COUNTER_TOO_LOW );
+            }
+        } else {
+            throw new Error('Authentication failed', ERR_AUTHENTICATION_FAILURE );
+        }
+    }
+
+    /**
+     * @return array
+     */
+    private function get_certs()
+    {
+        $files = array();
+        $dir = $this->attestDir;
+        if($dir && $handle = opendir($dir)) {
+            while(false !== ($entry = readdir($handle))) {
+                if(is_file("$dir/$entry")) {
+                    $files[] = "$dir/$entry";
+                }
+            }
+            closedir($handle);
+        }
+        return $files;
+    }
+
+    /**
+     * @param string $data
+     * @return string
+     */
+    private function base64u_encode($data)
+    {
+        return trim(strtr(base64_encode($data), '+/', '-_'), '=');
+    }
+
+    /**
+     * @param string $data
+     * @return string
+     */
+    private function base64u_decode($data)
+    {
+        return base64_decode(strtr($data, '-_', '+/'));
+    }
+
+    /**
+     * @param string $key
+     * @return null|string
+     */
+    private function pubkey_to_pem($key)
+    {
+        if(strlen($key) !== PUBKEY_LEN || $key[0] !== "\x04") {
+            return null;
+        }
+
+        /*
+         * Convert the public key to binary DER format first
+         * Using the ECC SubjectPublicKeyInfo OIDs from RFC 5480
+         *
+         *  SEQUENCE(2 elem)                        30 59
+         *   SEQUENCE(2 elem)                       30 13
+         *    OID1.2.840.10045.2.1 (id-ecPublicKey) 06 07 2a 86 48 ce 3d 02 01
+         *    OID1.2.840.10045.3.1.7 (secp256r1)    06 08 2a 86 48 ce 3d 03 01 07
+         *   BIT STRING(520 bit)                    03 42 ..key..
+         */
+        $der  = "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01";
+        $der .= "\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07\x03\x42";
+        $der .= "\0".$key;
+
+        $pem  = "-----BEGIN PUBLIC KEY-----\r\n";
+        $pem .= chunk_split(base64_encode($der), 64);
+        $pem .= "-----END PUBLIC KEY-----";
+
+        return $pem;
+    }
+
+    /**
+     * @return string
+     * @throws Error
+     */
+    private function createChallenge()
+    {
+        $challenge = openssl_random_pseudo_bytes(32, $crypto_strong );
+        if( $crypto_strong !== true ) {
+            throw new Error('Unable to obtain a good source of randomness', ERR_BAD_RANDOM);
+        }
+
+        $challenge = $this->base64u_encode( $challenge );
+
+        return $challenge;
+    }
+
+    /**
+     * Fixes a certificate where the signature contains unused bits.
+     *
+     * @param string $cert
+     * @return mixed
+     */
+    private function fixSignatureUnusedBits($cert)
+    {
+        if(in_array(hash('sha256', $cert), $this->FIXCERTS)) {
+            $cert[strlen($cert) - 257] = "\0";
+        }
+        return $cert;
+    }
+}
+
+/**
+ * Class for building a registration request
+ *
+ * @package u2flib_server
+ */
+class RegisterRequest
+{
+    /** Protocol version */
+    public $version = U2F_VERSION;
+
+    /** Registration challenge */
+    public $challenge;
+
+    /** Application id */
+    public $appId;
+
+    /**
+     * @param string $challenge
+     * @param string $appId
+     * @internal
+     */
+    public function __construct($challenge, $appId)
+    {
+        $this->challenge = $challenge;
+        $this->appId = $appId;
+    }
+}
+
+/**
+ * Class for building up an authentication request
+ *
+ * @package u2flib_server
+ */
+class SignRequest
+{
+    /** Protocol version */
+    public $version = U2F_VERSION;
+
+    /** Authentication challenge */
+    public $challenge;
+
+    /** Key handle of a registered authenticator */
+    public $keyHandle;
+
+    /** Application id */
+    public $appId;
+}
+
+/**
+ * Class returned for successful registrations
+ *
+ * @package u2flib_server
+ */
+class Registration
+{
+    /** The key handle of the registered authenticator */
+    public $keyHandle;
+
+    /** The public key of the registered authenticator */
+    public $publicKey;
+
+    /** The attestation certificate of the registered authenticator */
+    public $certificate;
+
+    /** The counter associated with this registration */
+    public $counter = -1;
+}
+
+/**
+ * Error class, returned on errors
+ *
+ * @package u2flib_server
+ */
+class Error extends \Exception
+{
+    /**
+     * Override constructor and make message and code mandatory
+     * @param string $message
+     * @param int $code
+     * @param \Exception|null $previous
+     */
+    public function __construct($message, $code, \Exception $previous = null) {
+        parent::__construct($message, $code, $previous);
+    }
+}

部分文件因为文件数量过多而无法显示